tasks.js 10 KB

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