tasks.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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 fs = require( 'fs' );
  7. const path = require( 'path' );
  8. const gulp = require( 'gulp' );
  9. const del = require( 'del' );
  10. const merge = require( 'merge-stream' );
  11. const gulpMirror = require( 'gulp-mirror' );
  12. const gulpWatch = require( 'gulp-watch' );
  13. const gutil = require( 'gulp-util' );
  14. const minimist = require( 'minimist' );
  15. const utils = require( './utils' );
  16. module.exports = ( config ) => {
  17. const buildDir = path.join( config.ROOT_DIR, config.BUILD_DIR );
  18. const tasks = {
  19. /**
  20. * Removes the build directory.
  21. */
  22. clean() {
  23. return del( buildDir );
  24. },
  25. src: {
  26. /**
  27. * Returns a stream of all source files.
  28. *
  29. * @param {Boolean} [watch] Whether the files should be watched.
  30. * @returns {Stream}
  31. */
  32. all( watch ) {
  33. return merge( tasks.src.main( watch ), tasks.src.ckeditor5( watch ), tasks.src.packages( watch ) );
  34. },
  35. /**
  36. * Returns a stream with just the main file (`ckeditor5/ckeditor.js`).
  37. *
  38. * @param {Boolean} [watch] Whether to watch the files.
  39. * @returns {Stream}
  40. */
  41. main( watch ) {
  42. const glob = path.join( config.ROOT_DIR, 'ckeditor.js' );
  43. return gulp.src( glob )
  44. .pipe( watch ? gulpWatch( glob ) : utils.noop() );
  45. },
  46. /**
  47. * Returns a stream of all source files from CKEditor 5.
  48. *
  49. * @param {Boolean} [watch] Whether to watch the files.
  50. * @returns {Stream}
  51. */
  52. ckeditor5( watch ) {
  53. const glob = path.join( config.ROOT_DIR, '@(src|tests)', '**', '*' );
  54. return gulp.src( glob, { nodir: true } )
  55. .pipe( watch ? gulpWatch( glob ) : utils.noop() )
  56. .pipe( utils.renameCKEditor5Files() );
  57. },
  58. /**
  59. * Returns a stream of all source files from CKEditor 5 dependencies.
  60. *
  61. * @param {Boolean} [watch] Whether to watch the files.
  62. * @returns {Stream}
  63. */
  64. packages( watch ) {
  65. // Find all CKEditor5 package directories. Resolve symlinks so we watch real directories
  66. // in order to workaround https://github.com/paulmillr/chokidar/issues/419.
  67. const dirs = fs.readdirSync( path.join( config.ROOT_DIR, 'node_modules' ) )
  68. // Look for ckeditor5-* directories.
  69. .filter( ( fileName ) => fileName.indexOf( 'ckeditor5-' ) === 0 )
  70. // Resolve symlinks and keep only directories.
  71. .map( ( fileName ) => {
  72. let filePath = path.join( config.ROOT_DIR, 'node_modules', fileName );
  73. let stat = fs.lstatSync( filePath );
  74. if ( stat.isSymbolicLink() ) {
  75. filePath = fs.realpathSync( filePath );
  76. stat = fs.lstatSync( filePath );
  77. }
  78. if ( stat.isDirectory() ) {
  79. return filePath;
  80. }
  81. // Filter...
  82. return false;
  83. } )
  84. // ...those out.
  85. .filter( ( filePath ) => filePath );
  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. * The main build task which is capable of copying, watching, processing and writing all files
  100. * to the `build/` directory.
  101. *
  102. * @param {Object} options
  103. * @param {String} options.formats
  104. * @param {Boolean} [options.watch]
  105. * @returns {Stream}
  106. */
  107. build( options ) {
  108. //
  109. // NOTE: Error handling in streams is hard.
  110. //
  111. // Most likely this code isn't optimal, but it's a result of 8h spent on search
  112. // for a solution to the ruined pipeline whenever something throws.
  113. //
  114. // Most important fact is that when dest stream emits an error the src stream
  115. // unpipes it. Problem is when you start using tools like multipipe or gulp-mirror,
  116. // because you lose control over the graph of the streams and you cannot reconnect them
  117. // with a full certainty that you connected them correctly, since you'd need to know these
  118. // libs internals.
  119. //
  120. // BTW. No, gulp-plumber is not a solution here because it does not affect the other tools.
  121. //
  122. // Hence, I decided that it'll be best to restart the whole piece. However, I wanted to avoid restarting the
  123. // watcher as it sounds like something heavy.
  124. //
  125. // The flow looks like follows:
  126. //
  127. // 1. codeStream (including logger)
  128. // 2. inputStream
  129. // 3. conversionStream (may throw)
  130. // 4. outputStream
  131. //
  132. // The input and output streams allowed me to easier debug and restart everything. Additionally, the output
  133. // stream is returned to Gulp so it must be stable. I decided to restart also the inputStream because when conversionStream
  134. // throws, then inputStream gets paused. Most likely it's possible to resume it, so we could pipe codeStream directly to
  135. // conversionStream, but it was easier this way.
  136. //
  137. // PS. The assumption is that all errors thrown somewhere inside conversionStream are forwarded to conversionStream.
  138. // Multipipe and gulp-mirror seem to work this way, so we get a single error emitter.
  139. const formats = options.formats.split( ',' );
  140. const codeStream = tasks.src.all( options.watch )
  141. .pipe(
  142. utils.noop( ( file ) => {
  143. gutil.log( `Processing '${ gutil.colors.cyan( file.path ) }'...` );
  144. } )
  145. );
  146. const conversionStreamGenerator = utils.getConversionStreamGenerator( buildDir );
  147. const outputStream = utils.noop();
  148. let inputStream;
  149. let conversionStream;
  150. startStreams();
  151. return outputStream;
  152. // Creates a single stream combining multiple conversion streams.
  153. function createConversionStream() {
  154. const formatPipes = formats.reduce( conversionStreamGenerator, [] );
  155. return gulpMirror.apply( null, formatPipes )
  156. .on( 'error', onError );
  157. }
  158. // Handles error in the combined conversion stream.
  159. // If we don't watch files, make sure that the process terminates ASAP. We could forward the error
  160. // to the output, but there may be some data in the pipeline and our error could be covered
  161. // by dozen of other messages.
  162. // If we watch files, then clean up the old streams and restart the combined conversion stream.
  163. function onError() {
  164. if ( !options.watch ) {
  165. process.exit( 1 );
  166. return;
  167. }
  168. unpipeStreams();
  169. gutil.log( 'Restarting...' );
  170. startStreams();
  171. }
  172. function startStreams() {
  173. inputStream = utils.noop();
  174. conversionStream = createConversionStream();
  175. codeStream
  176. .pipe( inputStream )
  177. .pipe( conversionStream )
  178. .pipe( outputStream );
  179. }
  180. function unpipeStreams() {
  181. codeStream.unpipe( inputStream );
  182. conversionStream.unpipe( outputStream );
  183. }
  184. }
  185. };
  186. gulp.task( 'build:clean', tasks.clean );
  187. gulp.task( 'build', [ 'build:clean' ], () => {
  188. const knownOptions = {
  189. string: [
  190. 'formats'
  191. ],
  192. boolean: [
  193. 'watch'
  194. ],
  195. default: {
  196. formats: 'amd',
  197. watch: false
  198. }
  199. };
  200. const options = minimist( process.argv.slice( 2 ), knownOptions );
  201. return tasks.build( options );
  202. } );
  203. gulp.task( 'build-esnext', [ 'build:clean' ], () => {
  204. return tasks.build( { formats: 'esnext' } );
  205. } );
  206. return tasks;
  207. };