8
0

utils.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. /* jshint node: true, esnext: true */
  2. 'use strict';
  3. const path = require( 'path' );
  4. const gulp = require( 'gulp' );
  5. const rename = require( 'gulp-rename' );
  6. const gulpBabel = require( 'gulp-babel' );
  7. const gutil = require( 'gulp-util' );
  8. const gulpFilter = require( 'gulp-filter' );
  9. const multipipe = require( 'multipipe' );
  10. const PassThrough = require( 'stream' ).PassThrough;
  11. const through = require( 'through2' );
  12. const utils = {
  13. /**
  14. * Code which can be appended to a transpiled (into AMD) test files in order to
  15. * load the 'tests' module and defer launching Bender until it's ready.
  16. *
  17. * Note: This code will not be transpiled so keep it in ES5.
  18. */
  19. benderLauncherCode:
  20. `
  21. require( [ 'tests' ], bender.defer(), function( err ) {
  22. // The problem with Require.JS is that there are no stacktraces if we won't log this.
  23. console.error( err );
  24. console.log( err.stack );
  25. } );
  26. `,
  27. /**
  28. * Module formats supported by the builder.
  29. */
  30. SUPPORTED_FORMATS: [ 'esnext', 'amd', 'cjs' ],
  31. /**
  32. * Creates a simple duplex stream.
  33. *
  34. * @param {Function} [callback] A callback which will be executed with each chunk.
  35. * @returns {Stream}
  36. */
  37. noop( callback ) {
  38. if ( !callback ) {
  39. return new PassThrough( { objectMode: true } );
  40. }
  41. return through( { objectMode: true }, ( file, encoding, throughCallback ) => {
  42. callback( file );
  43. throughCallback( null, file );
  44. } );
  45. },
  46. /**
  47. * Saves the files piped into this stream to the `dist/` directory.
  48. *
  49. * @param {String} distDir The `dist/` directory path.
  50. * @param {String} format The format of the distribution (`esnext`, `amd`, or `cjs`).
  51. * @returns {Stream}
  52. */
  53. dist( distDir, format ) {
  54. const destDir = path.join( distDir, format );
  55. return gulp.dest( destDir );
  56. },
  57. /**
  58. * Creates a function generating convertion streams.
  59. * Used to generate `formats.reduce()` callback where `formats` is an array of formats that should be generated.
  60. *
  61. * @param {String} distDir The `dist/` directory path.
  62. * @returns {Function}
  63. */
  64. getConversionStreamGenerator( distDir ) {
  65. return ( pipes, format ) => {
  66. const conversionPipes = [];
  67. conversionPipes.push( utils.pickVersionedFile( format ) );
  68. if ( format != 'esnext' ) {
  69. // Convert src files.
  70. const filterSource = gulpFilter( ( file ) => {
  71. return utils.isSourceFile( file ) && utils.isJSFile( file );
  72. }, { restore: true } );
  73. const transpileSource = utils.transpile( format, utils.getBabelOptionsForSource( format ) );
  74. conversionPipes.push(
  75. filterSource,
  76. transpileSource,
  77. filterSource.restore
  78. );
  79. // Convert test files.
  80. const filterTests = gulpFilter( ( file ) => {
  81. return utils.isTestFile( file ) && utils.isJSFile( file );
  82. }, { restore: true } );
  83. const transpileTests = utils.transpile( format, utils.getBabelOptionsForTests( format ) );
  84. conversionPipes.push(
  85. filterTests,
  86. transpileTests,
  87. utils.appendBenderLauncher(),
  88. filterTests.restore
  89. );
  90. }
  91. conversionPipes.push(
  92. utils.dist( distDir, format ),
  93. utils.noop( ( file ) => {
  94. gutil.log( `Finished writing '${ gutil.colors.cyan( file.path ) }'` );
  95. } )
  96. );
  97. pipes.push( multipipe.apply( null, conversionPipes ) );
  98. return pipes;
  99. };
  100. },
  101. /**
  102. * Transpiles files piped into this stream to the given format (`amd` or `cjs`).
  103. *
  104. * @param {String} format
  105. * @returns {Stream}
  106. */
  107. transpile( format, options ) {
  108. return gulpBabel( options )
  109. .on( 'error', function( err ) {
  110. gutil.log( gutil.colors.red( `Error (Babel:${ format })` ) );
  111. gutil.log( gutil.colors.red( err.message ) );
  112. console.log( '\n' + err.codeFrame + '\n' );
  113. } );
  114. },
  115. /**
  116. * Returns an object with Babel options for the source code.
  117. *
  118. * @param {String} format
  119. * @returns {Object} options
  120. */
  121. getBabelOptionsForSource( format ) {
  122. return {
  123. plugins: utils.getBabelPlugins( format ),
  124. // Ensure that all paths ends with '.js' because Require.JS (unlike Common.JS/System.JS)
  125. // will not add it to module names which look like paths.
  126. resolveModuleSource: utils.appendModuleExtension
  127. };
  128. },
  129. /**
  130. * Returns an object with Babel options for the test code.
  131. *
  132. * @param {String} format
  133. * @returns {Object} options
  134. */
  135. getBabelOptionsForTests( format ) {
  136. return {
  137. plugins: utils.getBabelPlugins( format ),
  138. resolveModuleSource: utils.appendModuleExtension,
  139. moduleIds: true,
  140. moduleId: 'tests'
  141. };
  142. },
  143. /**
  144. * Returns an array of Babel plugins to use.
  145. *
  146. * @param {String} format
  147. * @returns {Array}
  148. */
  149. getBabelPlugins( format ) {
  150. const babelModuleTranspilers = {
  151. amd: 'amd',
  152. cjs: 'commonjs'
  153. };
  154. const babelModuleTranspiler = babelModuleTranspilers[ format ];
  155. if ( !babelModuleTranspiler ) {
  156. throw new Error( `Incorrect format: ${ format }` );
  157. }
  158. return [
  159. // Note: When plugin is specified by its name, Babel loads it from a context of a
  160. // currently transpiled file (in our case - e.g. from ckeditor5-core/src/foo.js).
  161. // Obviously that fails, since we have all the plugins installed only in ckeditor5/
  162. // and we want to have them only there to avoid installing them dozens of times.
  163. //
  164. // Anyway, I haven't found in the docs that you can also pass a plugin instance here,
  165. // but it works... so let's hope it will.
  166. require( `babel-plugin-transform-es2015-modules-${ babelModuleTranspiler }` )
  167. ];
  168. },
  169. /**
  170. * Appends the {@link #benderLauncherCode} at the end of the file.
  171. *
  172. * @returns {Stream}
  173. */
  174. appendBenderLauncher() {
  175. return through( { objectMode: true }, ( file, encoding, callback ) => {
  176. if ( !file.isNull() ) {
  177. file.contents = new Buffer( file.contents.toString() + utils.benderLauncherCode );
  178. }
  179. callback( null, file );
  180. } );
  181. },
  182. /**
  183. * Allows us to pick one of files suffixed with the format (`__esnext`, `__amd`, or `__cjs`) and removes
  184. * files with other suffixes from the stream.
  185. *
  186. * For example: we have `load__esnext.js`, `load__amd.js` and `load__cjs.js`. After applying this
  187. * transformation when compiling code for a specific format the proper file will be renamed to `load.js`.
  188. * Files not matching a specified format will be removed.
  189. *
  190. * @param {String} format
  191. * @returns {Stream}
  192. */
  193. pickVersionedFile( format ) {
  194. const rejectedFormats = utils.SUPPORTED_FORMATS
  195. .filter( ( item ) => item !== format );
  196. const pickRegexp = new RegExp( `__${ format }$` );
  197. const rejectRegexp = new RegExp( `__(${ rejectedFormats.join( '|' ) }).js$` );
  198. const pick = rename( ( path ) => {
  199. path.basename = path.basename.replace( pickRegexp, '' );
  200. } );
  201. const remove = gulpFilter( ( file ) => !rejectRegexp.test( file.path ) );
  202. return multipipe( pick, remove );
  203. },
  204. /**
  205. * Processes paths of files inside CKEditor5 packages.
  206. *
  207. * * `ckeditor5-xxx/src/foo/bar.js` -> `ckeditor5/xxx/foo/bar.js`
  208. * * `ckeditor5-xxx/tests/foo/bar.js` -> `tests/xxx/foo/bar.js`
  209. *
  210. * @returns {Stream}
  211. */
  212. renamePackageFiles() {
  213. return rename( ( file ) => {
  214. const dirFrags = file.dirname.split( path.sep );
  215. // Validate the input for the clear conscious.
  216. if ( dirFrags[ 0 ].indexOf( 'ckeditor5-' ) !== 0 ) {
  217. throw new Error( 'Path should start with "ckeditor5-".' );
  218. }
  219. dirFrags[ 0 ] = dirFrags[ 0 ].replace( /^ckeditor5-/, '' );
  220. const firstFrag = dirFrags[ 1 ];
  221. if ( firstFrag == 'src' ) {
  222. // Remove 'src/'.
  223. dirFrags.splice( 1, 1 );
  224. // Temporary implementation of the UI lib option. See #88.
  225. if ( dirFrags[ 0 ] == 'ui-default' ) {
  226. dirFrags[ 0 ] = 'ui';
  227. }
  228. // And prepend 'ckeditor5/'.
  229. dirFrags.unshift( 'ckeditor5' );
  230. } else if ( firstFrag == 'tests' ) {
  231. // Remove 'tests/' from the package dir.
  232. dirFrags.splice( 1, 1 );
  233. // And prepend 'tests/'.
  234. dirFrags.unshift( 'tests' );
  235. } else {
  236. throw new Error( 'Path should start with "ckeditor5-*/(src|tests)".' );
  237. }
  238. file.dirname = path.join.apply( null, dirFrags );
  239. } );
  240. },
  241. /**
  242. * Processes paths of files inside the main CKEditor5 package.
  243. *
  244. * * `src/foo/bar.js` -> `ckeditor5/foo/bar.js`
  245. * * `tests/foo/bar.js` -> `tests/foo/bar.js`
  246. *
  247. * @returns {Stream}
  248. */
  249. renameCKEditor5Files() {
  250. return rename( ( file ) => {
  251. const dirFrags = file.dirname.split( path.sep );
  252. const firstFrag = dirFrags[ 0 ];
  253. if ( firstFrag == 'src' ) {
  254. // Replace 'src/' with 'ckeditor5/'.
  255. // src/path.js -> ckeditor5/path.js
  256. dirFrags.splice( 0, 1, 'ckeditor5' );
  257. } else if ( firstFrag != 'tests' ) {
  258. throw new Error( 'Path should start with "src" or "tests".' );
  259. }
  260. file.dirname = path.join.apply( null, dirFrags );
  261. } );
  262. },
  263. /**
  264. * Appends file extension to file URLs. Tries to not touch named modules.
  265. *
  266. * @param {String} source
  267. * @returns {String}
  268. */
  269. appendModuleExtension( source ) {
  270. if ( /^https?:|\.[\/\\]/.test( source ) && !/\.js$/.test( source ) ) {
  271. return source + '.js';
  272. }
  273. return source;
  274. },
  275. /**
  276. * Checks whether a file is a test file.
  277. *
  278. * @param {Vinyl} file
  279. * @returns {Boolean}
  280. */
  281. isTestFile( file ) {
  282. // TODO this should be based on bender configuration (config.tests.*.paths).
  283. if ( !file.relative.startsWith( 'tests' + path.sep ) ) {
  284. return false;
  285. }
  286. const dirFrags = file.relative.split( path.sep );
  287. return !dirFrags.some( dirFrag => dirFrag.startsWith( '_' ) );
  288. },
  289. /**
  290. * Checks whether a file is a source file.
  291. *
  292. * @param {Vinyl} file
  293. * @returns {Boolean}
  294. */
  295. isSourceFile( file ) {
  296. return !utils.isTestFile( file );
  297. },
  298. /**
  299. * Checks whether a file is a JS file.
  300. *
  301. * @param {Vinyl} file
  302. * @returns {Boolean}
  303. */
  304. isJSFile( file ) {
  305. return file.path.endsWith( '.js' );
  306. }
  307. };
  308. module.exports = utils;