8
0

utils.js 9.8 KB

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