8
0

utils.js 7.6 KB

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