utils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  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 replace = require( 'gulp-replace' );
  10. const gulpBabel = require( 'gulp-babel' );
  11. const gutil = require( 'gulp-util' );
  12. const gulpFilter = require( 'gulp-filter' );
  13. const multipipe = require( 'multipipe' );
  14. const PassThrough = require( 'stream' ).PassThrough;
  15. const through = require( 'through2' );
  16. const fs = require( 'fs' );
  17. const sass = require( 'node-sass' );
  18. const del = require( 'del' );
  19. const minimist = require( 'minimist' );
  20. const sprite = require( 'gulp-svg-sprite' );
  21. const pipe = require( 'multipipe' );
  22. const filter = require( 'gulp-filter' );
  23. const utils = {
  24. /**
  25. * Code which can be appended to a transpiled (into AMD) test files in order to
  26. * load the 'tests' module and defer launching Bender until it's ready.
  27. *
  28. * Note: This code will not be transpiled so keep it in ES5.
  29. */
  30. benderLauncherCode:
  31. `
  32. var defer = bender.defer();
  33. require( [ 'tests', '/tests/engine/_utils/view.js', '/tests/engine/_utils/model.js' ],
  34. function( tests, view, model ) {
  35. bender.view = view;
  36. bender.model = model;
  37. defer();
  38. },
  39. function( err ) {
  40. // The problem with Require.JS is that there are no stacktraces if we won't log this.
  41. console.error( err );
  42. console.log( err.stack );
  43. } );
  44. `,
  45. /**
  46. * Module formats supported by the builder.
  47. */
  48. SUPPORTED_FORMATS: [ 'esnext', 'amd', 'cjs' ],
  49. /**
  50. * Creates a simple duplex stream.
  51. *
  52. * @param {Function} [callback] A callback which will be executed with each chunk.
  53. * @returns {Stream}
  54. */
  55. noop( callback ) {
  56. if ( !callback ) {
  57. return new PassThrough( { objectMode: true } );
  58. }
  59. return through( { objectMode: true }, ( file, encoding, throughCallback ) => {
  60. callback( file );
  61. throughCallback( null, file );
  62. } );
  63. },
  64. /**
  65. * Saves the files piped into this stream to the `build/` directory.
  66. *
  67. * @param {String} buildDir The `build/` directory path.
  68. * @param {String} format The format of the buildribution (`esnext`, `amd`, or `cjs`).
  69. * @returns {Stream}
  70. */
  71. destBuild( buildDir, format ) {
  72. const destDir = path.join( buildDir, format );
  73. return gulp.dest( destDir );
  74. },
  75. /**
  76. * Creates a function generating convertion streams.
  77. * Used to generate `formats.reduce()` callback where `formats` is an array of formats that should be generated.
  78. *
  79. * @param {String} buildDir The `build/` directory path.
  80. * @returns {Function}
  81. */
  82. getConversionStreamGenerator( buildDir ) {
  83. return ( pipes, format ) => {
  84. const conversionPipes = [];
  85. conversionPipes.push( utils.pickVersionedFile( format ) );
  86. if ( format != 'esnext' ) {
  87. // Convert src files.
  88. const filterSource = gulpFilter( ( file ) => {
  89. return utils.isSourceFile( file ) && utils.isJSFile( file );
  90. }, { restore: true } );
  91. const transpileSource = utils.transpile( format, utils.getBabelOptionsForSource( format ) );
  92. conversionPipes.push(
  93. filterSource,
  94. transpileSource,
  95. utils.fixCoverage(),
  96. filterSource.restore
  97. );
  98. // Convert test files.
  99. const filterTests = gulpFilter( ( file ) => {
  100. return utils.isTestFile( file ) && utils.isJSFile( file );
  101. }, { restore: true } );
  102. const transpileTests = utils.transpile( format, utils.getBabelOptionsForTests( format ) );
  103. conversionPipes.push(
  104. filterTests,
  105. transpileTests,
  106. utils.appendBenderLauncher(),
  107. filterTests.restore
  108. );
  109. }
  110. conversionPipes.push(
  111. utils.destBuild( buildDir, format ),
  112. utils.noop( ( file ) => {
  113. gutil.log( `Finished writing '${ gutil.colors.cyan( file.path ) }'` );
  114. } )
  115. );
  116. pipes.push( multipipe.apply( null, conversionPipes ) );
  117. return pipes;
  118. };
  119. },
  120. /**
  121. * Transpiles files piped into this stream to the given format (`amd` or `cjs`).
  122. *
  123. * @param {String} format
  124. * @returns {Stream}
  125. */
  126. transpile( format, options ) {
  127. return gulpBabel( options )
  128. .on( 'error', function( err ) {
  129. gutil.log( gutil.colors.red( `Error (Babel:${ format })` ) );
  130. gutil.log( gutil.colors.red( err.message ) );
  131. console.log( '\n' + err.codeFrame + '\n' );
  132. } );
  133. },
  134. /**
  135. * Adds istanbul ignore to the code created by babel.
  136. *
  137. * @returns {Stream}
  138. */
  139. fixCoverage() {
  140. return replace(
  141. 'return obj && obj.__esModule ? obj :',
  142. '/* istanbul ignore next */\n\t\t' +
  143. 'return obj && obj.__esModule ? obj :' );
  144. },
  145. /**
  146. * Returns an object with Babel options for the source code.
  147. *
  148. * @param {String} format
  149. * @returns {Object} options
  150. */
  151. getBabelOptionsForSource( format ) {
  152. return {
  153. plugins: utils.getBabelPlugins( format ),
  154. // Ensure that all paths ends with '.js' because Require.JS (unlike Common.JS/System.JS)
  155. // will not add it to module names which look like paths.
  156. resolveModuleSource: utils.appendModuleExtension
  157. };
  158. },
  159. /**
  160. * Returns an object with Babel options for the test code.
  161. *
  162. * @param {String} format
  163. * @returns {Object} options
  164. */
  165. getBabelOptionsForTests( format ) {
  166. return {
  167. plugins: utils.getBabelPlugins( format ),
  168. resolveModuleSource: utils.appendModuleExtension,
  169. moduleIds: true,
  170. moduleId: 'tests'
  171. };
  172. },
  173. /**
  174. * Returns an array of Babel plugins to use.
  175. *
  176. * @param {String} format
  177. * @returns {Array}
  178. */
  179. getBabelPlugins( format ) {
  180. const babelModuleTranspilers = {
  181. amd: 'amd',
  182. cjs: 'commonjs'
  183. };
  184. const babelModuleTranspiler = babelModuleTranspilers[ format ];
  185. if ( !babelModuleTranspiler ) {
  186. throw new Error( `Incorrect format: ${ format }` );
  187. }
  188. return [
  189. // Note: When plugin is specified by its name, Babel loads it from a context of a
  190. // currently transpiled file (in our case - e.g. from ckeditor5-core/src/foo.js).
  191. // Obviously that fails, since we have all the plugins installed only in ckeditor5/
  192. // and we want to have them only there to avoid installing them dozens of times.
  193. //
  194. // Anyway, I haven't found in the docs that you can also pass a plugin instance here,
  195. // but it works... so let's hope it will.
  196. require( `babel-plugin-transform-es2015-modules-${ babelModuleTranspiler }` )
  197. ];
  198. },
  199. /**
  200. * Appends the {@link #benderLauncherCode} at the end of the file.
  201. *
  202. * @returns {Stream}
  203. */
  204. appendBenderLauncher() {
  205. return through( { objectMode: true }, ( file, encoding, callback ) => {
  206. if ( !file.isNull() ) {
  207. file.contents = new Buffer( file.contents.toString() + utils.benderLauncherCode );
  208. }
  209. callback( null, file );
  210. } );
  211. },
  212. /**
  213. * Allows us to pick one of files suffixed with the format (`__esnext`, `__amd`, or `__cjs`) and removes
  214. * files with other suffixes from the stream.
  215. *
  216. * For example: we have `load__esnext.js`, `load__amd.js` and `load__cjs.js`. After applying this
  217. * transformation when compiling code for a specific format the proper file will be renamed to `load.js`.
  218. * Files not matching a specified format will be removed.
  219. *
  220. * @param {String} format
  221. * @returns {Stream}
  222. */
  223. pickVersionedFile( format ) {
  224. const rejectedFormats = utils.SUPPORTED_FORMATS
  225. .filter( ( item ) => item !== format );
  226. const pickRegexp = new RegExp( `__${ format }$` );
  227. const rejectRegexp = new RegExp( `__(${ rejectedFormats.join( '|' ) }).js$` );
  228. const pick = rename( ( path ) => {
  229. path.basename = path.basename.replace( pickRegexp, '' );
  230. } );
  231. const remove = gulpFilter( ( file ) => !rejectRegexp.test( file.path ) );
  232. return multipipe( pick, remove );
  233. },
  234. /**
  235. * Processes paths of files inside CKEditor5 packages.
  236. *
  237. * * `ckeditor5-xxx/src/foo/bar.js` -> `ckeditor5/xxx/foo/bar.js`
  238. * * `ckeditor5-xxx/tests/foo/bar.js` -> `tests/xxx/foo/bar.js`
  239. *
  240. * @returns {Stream}
  241. */
  242. renamePackageFiles() {
  243. return rename( ( file ) => {
  244. const dirFrags = file.dirname.split( path.sep );
  245. // Validate the input for the clear conscious.
  246. if ( dirFrags[ 0 ].indexOf( 'ckeditor5-' ) !== 0 ) {
  247. throw new Error( 'Path should start with "ckeditor5-".' );
  248. }
  249. dirFrags[ 0 ] = dirFrags[ 0 ].replace( /^ckeditor5-/, '' );
  250. const firstFrag = dirFrags[ 1 ];
  251. if ( firstFrag == 'src' ) {
  252. // Remove 'src/'.
  253. dirFrags.splice( 1, 1 );
  254. // Temporary implementation of the UI lib option. See #88.
  255. if ( dirFrags[ 0 ] == 'ui-default' ) {
  256. dirFrags[ 0 ] = 'ui';
  257. }
  258. // And prepend 'ckeditor5/'.
  259. dirFrags.unshift( 'ckeditor5' );
  260. } else if ( firstFrag == 'tests' ) {
  261. // Remove 'tests/' from the package dir.
  262. dirFrags.splice( 1, 1 );
  263. // And prepend 'tests/'.
  264. dirFrags.unshift( 'tests' );
  265. } else {
  266. throw new Error( 'Path should start with "ckeditor5-*/(src|tests|theme)".' );
  267. }
  268. file.dirname = path.join.apply( null, dirFrags );
  269. } );
  270. },
  271. /**
  272. * Processes paths of files inside the main CKEditor5 package.
  273. *
  274. * * `src/foo/bar.js` -> `ckeditor5/foo/bar.js`
  275. * * `tests/foo/bar.js` -> `tests/ckeditor5/foo/bar.js`
  276. *
  277. * @returns {Stream}
  278. */
  279. renameCKEditor5Files() {
  280. return rename( ( file ) => {
  281. const dirFrags = file.dirname.split( path.sep );
  282. const firstFrag = dirFrags[ 0 ];
  283. if ( firstFrag == 'src' ) {
  284. // Replace 'src/' with 'ckeditor5/'.
  285. // src/path.js -> ckeditor5/path.js
  286. dirFrags.splice( 0, 1, 'ckeditor5' );
  287. } else if ( firstFrag == 'tests' ) {
  288. // Insert 'ckeditor5/' after 'tests/'.
  289. // tests/foo.js -> tests/ckeditor5/foo.js
  290. dirFrags.splice( 1, 0, 'ckeditor5' );
  291. } else {
  292. throw new Error( 'Path should start with "src" or "tests".' );
  293. }
  294. file.dirname = path.join.apply( null, dirFrags );
  295. } );
  296. },
  297. /**
  298. * Appends file extension to file URLs. Tries to not touch named modules.
  299. *
  300. * @param {String} source
  301. * @returns {String}
  302. */
  303. appendModuleExtension( source ) {
  304. if ( /^https?:|\.[\/\\]/.test( source ) && !/\.js$/.test( source ) ) {
  305. return source + '.js';
  306. }
  307. return source;
  308. },
  309. /**
  310. * Checks whether a file is a test file.
  311. *
  312. * @param {Vinyl} file
  313. * @returns {Boolean}
  314. */
  315. isTestFile( file ) {
  316. // TODO this should be based on bender configuration (config.tests.*.paths).
  317. if ( !file.relative.startsWith( 'tests' + path.sep ) ) {
  318. return false;
  319. }
  320. const dirFrags = file.relative.split( path.sep );
  321. return !dirFrags.some( dirFrag => {
  322. return dirFrag.startsWith( '_' ) && dirFrag != '_utils-tests';
  323. } );
  324. },
  325. /**
  326. * Checks whether a file is a source file.
  327. *
  328. * @param {Vinyl} file
  329. * @returns {Boolean}
  330. */
  331. isSourceFile( file ) {
  332. return !utils.isTestFile( file );
  333. },
  334. /**
  335. * Checks whether a file is a JS file.
  336. *
  337. * @param {Vinyl} file
  338. * @returns {Boolean}
  339. */
  340. isJSFile( file ) {
  341. return file.path.endsWith( '.js' );
  342. },
  343. /**
  344. * Finds all CKEditor5 package directories in "node_modules" folder.
  345. *
  346. * @param {String} rootDir A root directory containing "node_modules" folder.
  347. * @returns {Array} Array of ckeditor5-* package directory paths.
  348. */
  349. getPackages( rootDir ) {
  350. // Find all CKEditor5 package directories. Resolve symlinks so we watch real directories
  351. // in order to workaround https://github.com/paulmillr/chokidar/issues/419.
  352. return fs.readdirSync( path.join( rootDir, 'node_modules' ) )
  353. // Look for ckeditor5-* directories.
  354. .filter( fileName => {
  355. return fileName.indexOf( 'ckeditor5-' ) === 0;
  356. } )
  357. // Resolve symlinks and keep only directories.
  358. .map( fileName => {
  359. let filePath = path.join( rootDir, 'node_modules', fileName );
  360. let stat = fs.lstatSync( filePath );
  361. if ( stat.isSymbolicLink() ) {
  362. filePath = fs.realpathSync( filePath );
  363. stat = fs.lstatSync( filePath );
  364. }
  365. if ( stat.isDirectory() ) {
  366. return filePath;
  367. }
  368. // Filter...
  369. return false;
  370. } )
  371. // ...those out.
  372. .filter( filePath => filePath );
  373. },
  374. /**
  375. * Filters theme entry points only from a stream of SCSS files.
  376. *
  377. * @returns {Stream}
  378. */
  379. filterThemeEntryPoints() {
  380. return filter( '**/theme.scss' );
  381. },
  382. /**
  383. * Given the input stream of theme entry-point files (theme.scss), this method:
  384. * 1. Collects paths to entry-point.
  385. * 2. Builds the output CSS theme file using aggregated entry-points.
  386. * 3. Returns a stream containing built CSS theme file.
  387. *
  388. * @param {String} fileName The name of the output CSS theme file.
  389. * @returns {Stream}
  390. */
  391. compileThemes( fileName ) {
  392. const paths = [];
  393. const stream = through.obj( collectThemeEntryPoint, renderThemeFromEntryPoints );
  394. function collectThemeEntryPoint( file, enc, callback ) {
  395. paths.push( file.path );
  396. callback();
  397. }
  398. function renderThemeFromEntryPoints( callback ) {
  399. gutil.log( `Compiling '${ gutil.colors.cyan( fileName ) }' from ${ gutil.colors.cyan( paths.length ) } entry points...` );
  400. // Note: Make sure windows\\style\\paths are preserved.
  401. const dataToRender = paths.map( p => `@import "${ p.replace( /\\/g, '\\\\' ) }";` )
  402. .join( '\n' );
  403. try {
  404. const rendered = sass.renderSync( utils.getSassOptions( dataToRender ) );
  405. stream.push( new gutil.File( {
  406. path: fileName,
  407. contents: new Buffer( rendered.css )
  408. } ) );
  409. callback();
  410. } catch ( err ) {
  411. callback( err );
  412. }
  413. }
  414. return stream;
  415. },
  416. /**
  417. * Parses command line arguments and returns them as a user-friendly hash.
  418. *
  419. * @param {String} dataToRender
  420. * @returns {Object}
  421. */
  422. getSassOptions( dataToRender ) {
  423. return {
  424. data: dataToRender,
  425. sourceMap: true,
  426. sourceMapEmbed: true,
  427. outputStyle: 'expanded',
  428. sourceComments: true
  429. };
  430. },
  431. /**
  432. * Removes files and directories specified by `glob` starting from `rootDir`
  433. * and gently informs about deletion.
  434. *
  435. * @param {String} rootDir The path to the root directory (i.e. "dist/").
  436. * @param {String} glob Glob specifying what to clean.
  437. * @returns {Promise}
  438. */
  439. clean( rootDir, glob ) {
  440. return del( path.join( rootDir, glob ) ).then( paths => {
  441. paths.forEach( p => {
  442. gutil.log( `Deleted file '${ gutil.colors.cyan( p ) }'.` );
  443. } );
  444. } );
  445. },
  446. /**
  447. * Parses command line arguments and returns them as a user-friendly hash.
  448. *
  449. * @returns {Object} options
  450. * @returns {Array} [options.formats] Array of specified output formats ("esnext" or "amd").
  451. * @returns {Boolean} [options.watch] A flag which enables watch mode.
  452. */
  453. parseArguments() {
  454. const options = minimist( process.argv.slice( 2 ), {
  455. string: [
  456. 'formats'
  457. ],
  458. boolean: [
  459. 'watch'
  460. ],
  461. default: {
  462. formats: 'amd',
  463. watch: false
  464. }
  465. } );
  466. options.formats = options.formats.split( ',' );
  467. return options;
  468. },
  469. /**
  470. * Given a stream of .svg files it returns a stream containing JavaScript
  471. * icon sprite file.
  472. *
  473. * @returns {Stream}
  474. */
  475. compileIconSprite() {
  476. return sprite( utils.getIconSpriteOptions() );
  477. },
  478. /**
  479. * Returns svg-sprite util options to generate <symbol>-based, JavaScript
  480. * sprite file.
  481. *
  482. * @returns {Object}
  483. */
  484. getIconSpriteOptions() {
  485. return {
  486. shape: {
  487. id: {
  488. // Note: Consider unix/style/paths and windows\\style\\paths.
  489. generator: name => `ck-icon-${ name.match( /([^\/\\]*)\.svg$/ )[ 1 ] }`
  490. },
  491. },
  492. svg: {
  493. xmlDeclaration: false,
  494. doctypeDeclaration: false,
  495. },
  496. mode: {
  497. symbol: {
  498. dest: './', // Flatten symbol/ folder.
  499. inline: true,
  500. render: {
  501. js: {
  502. template: path.join( __dirname, 'iconmanagermodel.tpl' ),
  503. dest: 'iconmanagermodel.js',
  504. }
  505. }
  506. }
  507. }
  508. };
  509. },
  510. /**
  511. * Given a stream of files it returns an array of gulp-mirror streams outputting
  512. * files to `build/[formats]/theme/` directories for each of desired output formats (cjs, amd, etc.).
  513. *
  514. * @param {String} buildDir A path to /build directory.
  515. * @param {Array} formats An array of desired output formats.
  516. * @param {Function} [transformationStream] A stream used to transform files before they're saved to
  517. * desired `build/[formats]/theme` directories. Useful for transpilation.
  518. * @returns {Stream[]} An array of streams.
  519. */
  520. getThemeFormatDestStreams( buildDir, formats, transformationStream ) {
  521. return formats.map( f => {
  522. return pipe(
  523. transformationStream ? transformationStream( f ) : utils.noop(),
  524. gulp.dest( path.join( buildDir, f, 'theme' ) ),
  525. utils.noop( file => {
  526. gutil.log( `Output for ${ gutil.colors.cyan( f ) } is '${ gutil.colors.cyan( file.path ) }'.` );
  527. } )
  528. );
  529. } );
  530. }
  531. };
  532. module.exports = utils;