tasks.js 11 KB

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