utils.js 7.4 KB

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