tasks.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. const path = require( 'path' );
  7. const gulp = require( 'gulp' );
  8. const merge = require( 'merge-stream' );
  9. const gulpMirror = require( 'gulp-mirror' );
  10. const gulpWatch = require( 'gulp-watch' );
  11. const gulpPlumber = require( 'gulp-plumber' );
  12. const gutil = require( 'gulp-util' );
  13. const utils = require( './utils' );
  14. const runSequence = require( 'run-sequence' );
  15. module.exports = ( config ) => {
  16. const buildDir = path.join( config.ROOT_DIR, config.BUILD_DIR );
  17. const themesGlob = path.join( 'theme', '**', '*.scss' );
  18. const iconsGlob = path.join( 'theme', 'icons', '*.svg' );
  19. const tasks = {
  20. clean: {
  21. /**
  22. * Removes "themes" folder from "./build/{format}" directory.
  23. */
  24. themes() {
  25. return utils.clean( buildDir, path.join( `@(${ utils.parseArguments().formats.join( '|' ) })`, 'theme' ) );
  26. },
  27. /**
  28. * Removes all but "themes" folder from "./build/{format}" directory.
  29. */
  30. js() {
  31. return utils.clean( buildDir, path.join( `@(${ utils.parseArguments().formats.join( '|' ) })`, '!(theme)' ) );
  32. },
  33. /**
  34. * Removes the "./build" directory.
  35. */
  36. all() {
  37. return utils.clean( buildDir, path.join() );
  38. }
  39. },
  40. src: {
  41. js: {
  42. /**
  43. * Returns a stream of all source files.
  44. *
  45. * @param {Boolean} [watch] Whether the files should be watched.
  46. * @returns {Stream}
  47. */
  48. all( watch ) {
  49. return merge( tasks.src.js.main( watch ), tasks.src.js.ckeditor5( watch ), tasks.src.js.packages( watch ) );
  50. },
  51. /**
  52. * Returns a stream with just the main file (`ckeditor5/ckeditor.js`).
  53. *
  54. * @param {Boolean} [watch] Whether to watch the files.
  55. * @returns {Stream}
  56. */
  57. main( watch ) {
  58. const glob = path.join( config.ROOT_DIR, 'ckeditor.js' );
  59. return gulp.src( glob )
  60. .pipe( watch ? gulpWatch( glob ) : utils.noop() );
  61. },
  62. /**
  63. * Returns a stream of all source files from CKEditor 5.
  64. *
  65. * @param {Boolean} [watch] Whether to watch the files.
  66. * @returns {Stream}
  67. */
  68. ckeditor5( watch ) {
  69. const glob = path.join( config.ROOT_DIR, '@(src|tests)', '**', '*' );
  70. return gulp.src( glob, { nodir: true } )
  71. .pipe( watch ? gulpWatch( glob ) : utils.noop() )
  72. .pipe( utils.renameCKEditor5Files() );
  73. },
  74. /**
  75. * Returns a stream of all source files from CKEditor 5 dependencies.
  76. *
  77. * @param {Boolean} [watch] Whether to watch the files.
  78. * @returns {Stream}
  79. */
  80. packages( watch ) {
  81. const dirs = utils.getPackages( config.ROOT_DIR );
  82. const streams = dirs.map( ( dirPath ) => {
  83. const glob = path.join( dirPath, '@(src|tests)', '**', '*' );
  84. // Use parent as a base so we get paths starting with 'ckeditor5-*/src/*' in the stream.
  85. const baseDir = path.parse( dirPath ).dir;
  86. const opts = { base: baseDir, nodir: true };
  87. return gulp.src( glob, opts )
  88. .pipe( watch ? gulpWatch( glob, opts ) : utils.noop() );
  89. } );
  90. return merge.apply( null, streams )
  91. .pipe( utils.renamePackageFiles() );
  92. }
  93. },
  94. /**
  95. * Returns a stream of all theme (*.scss) files.
  96. *
  97. * @returns {Stream}
  98. */
  99. sass() {
  100. const dirs = utils.getPackages( config.ROOT_DIR );
  101. const streams = dirs.map( ( dirPath ) => {
  102. const glob = path.join( dirPath, themesGlob );
  103. const baseDir = path.parse( dirPath ).dir;
  104. const opts = { base: baseDir, nodir: true };
  105. return gulp.src( glob, opts );
  106. } );
  107. return merge.apply( null, streams );
  108. },
  109. icons() {
  110. const dirs = utils.getPackages( config.ROOT_DIR );
  111. const streams = dirs.map( ( dirPath ) => {
  112. const glob = path.join( dirPath, iconsGlob );
  113. const baseDir = path.parse( dirPath ).dir;
  114. const opts = { base: baseDir, nodir: true };
  115. return gulp.src( glob, opts );
  116. } );
  117. return merge.apply( null, streams );
  118. }
  119. },
  120. build: {
  121. /**
  122. * The build task which is capable of copying, watching, processing and writing all JavaScript files
  123. * to the `build/` directory.
  124. *
  125. * @param {Object} options
  126. * @param {String} options.formats
  127. * @param {Boolean} [options.watch]
  128. * @returns {Stream}
  129. */
  130. js( options ) {
  131. //
  132. // NOTE: Error handling in streams is hard.
  133. //
  134. // Most likely this code isn't optimal, but it's a result of 8h spent on search
  135. // for a solution to the ruined pipeline whenever something throws.
  136. //
  137. // Most important fact is that when dest stream emits an error the src stream
  138. // unpipes it. Problem is when you start using tools like multipipe or gulp-mirror,
  139. // because you lose control over the graph of the streams and you cannot reconnect them
  140. // with a full certainty that you connected them correctly, since you'd need to know these
  141. // libs internals.
  142. //
  143. // BTW. No, gulp-plumber is not a solution here because it does not affect the other tools.
  144. //
  145. // Hence, I decided that it'll be best to restart the whole piece. However, I wanted to avoid restarting the
  146. // watcher as it sounds like something heavy.
  147. //
  148. // The flow looks like follows:
  149. //
  150. // 1. codeStream (including logger)
  151. // 2. inputStream
  152. // 3. conversionStream (may throw)
  153. // 4. outputStream
  154. //
  155. // The input and output streams allowed me to easier debug and restart everything. Additionally, the output
  156. // stream is returned to Gulp so it must be stable. I decided to restart also the inputStream because when conversionStream
  157. // throws, then inputStream gets paused. Most likely it's possible to resume it, so we could pipe codeStream directly to
  158. // conversionStream, but it was easier this way.
  159. //
  160. // PS. The assumption is that all errors thrown somewhere inside conversionStream are forwarded to conversionStream.
  161. // Multipipe and gulp-mirror seem to work this way, so we get a single error emitter.
  162. const codeStream = tasks.src.js.all( options.watch )
  163. .pipe(
  164. utils.noop( ( file ) => {
  165. gutil.log( `Processing '${ gutil.colors.cyan( file.path ) }'...` );
  166. } )
  167. );
  168. const conversionStreamGenerator = utils.getConversionStreamGenerator( buildDir );
  169. const outputStream = utils.noop();
  170. let inputStream;
  171. let conversionStream;
  172. startStreams();
  173. return outputStream;
  174. // Creates a single stream combining multiple conversion streams.
  175. function createConversionStream() {
  176. const formatPipes = options.formats.reduce( conversionStreamGenerator, [] );
  177. return gulpMirror.apply( null, formatPipes )
  178. .on( 'error', onError );
  179. }
  180. // Handles error in the combined conversion stream.
  181. // If we don't watch files, make sure that the process terminates ASAP. We could forward the error
  182. // to the output, but there may be some data in the pipeline and our error could be covered
  183. // by dozen of other messages.
  184. // If we watch files, then clean up the old streams and restart the combined conversion stream.
  185. function onError() {
  186. if ( !options.watch ) {
  187. process.exit( 1 );
  188. return;
  189. }
  190. unpipeStreams();
  191. gutil.log( 'Restarting...' );
  192. startStreams();
  193. }
  194. function startStreams() {
  195. inputStream = utils.noop();
  196. conversionStream = createConversionStream();
  197. codeStream
  198. .pipe( inputStream )
  199. .pipe( conversionStream )
  200. .pipe( outputStream );
  201. }
  202. function unpipeStreams() {
  203. codeStream.unpipe( inputStream );
  204. conversionStream.unpipe( outputStream );
  205. }
  206. },
  207. /**
  208. * The task capable of watching, processing and writing CSS files into
  209. * the `build/{format}/theme` directory.
  210. *
  211. * @param {Object} options
  212. * @param {String} options.formats
  213. * @param {Boolean} [options.watch]
  214. * @returns {Stream}
  215. */
  216. sass( options ) {
  217. if ( options.watch ) {
  218. const glob = path.join( config.ROOT_DIR, 'node_modules', 'ckeditor5-*', themesGlob );
  219. // Initial build.
  220. build();
  221. gutil.log( `Watching theme files in '${ gutil.colors.cyan( glob ) }' for changes...` );
  222. return gulp.watch( glob, event => {
  223. gutil.log( `Theme file '${ gutil.colors.cyan( event.path ) }' has been ${ event.type }...` );
  224. // Re-build the entire theme if the file has been changed.
  225. return build();
  226. } );
  227. } else {
  228. return build();
  229. }
  230. function build() {
  231. const dests = utils.getThemeDestsForFormats( buildDir, options.formats );
  232. return tasks.src.sass()
  233. .pipe( gulpPlumber() )
  234. .pipe( utils.filterThemeEntryPoints() )
  235. .pipe( utils.compileThemes( 'ckeditor.css' ) )
  236. .pipe( gulpMirror( dests ) )
  237. .pipe(
  238. utils.noop( ( file ) => {
  239. gutil.log( `Output file saved to '${ gutil.colors.cyan( file.path ) }'.` );
  240. } )
  241. )
  242. .on( 'error', console.log );
  243. }
  244. },
  245. /**
  246. * The task capable of converting SVG files into `build/{format}/theme/icons.js`
  247. * sprite.
  248. *
  249. * @param {Object} options
  250. * @param {String} options.formats
  251. * @returns {Stream}
  252. */
  253. icons( options ) {
  254. const dests = utils.getThemeDestsForFormats( buildDir, options.formats );
  255. return tasks.src.icons()
  256. .pipe( utils.compileIconSprite() )
  257. .pipe( gulpMirror( dests ) )
  258. .pipe(
  259. utils.noop( ( file ) => {
  260. gutil.log( `Output file saved to '${ gutil.colors.cyan( file.path ) }'.` );
  261. } )
  262. );
  263. }
  264. }
  265. };
  266. gulp.task( 'build', callback => {
  267. runSequence( 'build:clean:all', 'build:themes', 'build:js', callback );
  268. } );
  269. gulp.task( 'build:clean:all', tasks.clean.all );
  270. gulp.task( 'build:clean:themes', tasks.clean.themes );
  271. gulp.task( 'build:clean:js', tasks.clean.js );
  272. gulp.task( 'build:themes', ( callback ) => {
  273. runSequence( 'build:clean:themes', 'build:icons', 'build:sass', callback );
  274. } );
  275. gulp.task( 'build:sass', () => {
  276. return tasks.build.sass( utils.parseArguments() );
  277. } );
  278. gulp.task( 'build:icons', () => {
  279. return tasks.build.icons( utils.parseArguments() );
  280. } );
  281. gulp.task( 'build:js', [ 'build:clean:js' ], () => {
  282. return tasks.build.js( utils.parseArguments() );
  283. } );
  284. gulp.task( 'build:js:esnext', [ 'build:clean:js' ], () => {
  285. return tasks.build.js( { formats: [ 'esnext' ] } );
  286. } );
  287. return tasks;
  288. };