8
0

build.js 6.6 KB

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