8
0

utils.js 15 KB

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