8
0

utils.js 17 KB

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