tasks.js 7.2 KB

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