8
0

tasks.js 11 KB

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