utils.js 8.8 KB

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