8
0

tasks.js 11 KB

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