Преглед на файлове

Refactored bundling tasks.

Oskar Wrobel преди 9 години
родител
ревизия
d941fccc96
променени са 6 файла, в които са добавени 255 реда и са изтрити 127 реда
  1. 1 1
      dev/tasks/bundle/bundle-config.js
  2. 32 26
      dev/tasks/bundle/tasks.js
  3. 69 29
      dev/tasks/bundle/utils.js
  4. 0 63
      dev/tests/bundle/tasks.js
  5. 152 8
      dev/tests/bundle/utils.js
  6. 1 0
      package.json

+ 1 - 1
dev/tasks/bundle/bundle-config.js

@@ -7,7 +7,7 @@
 
 module.exports = {
 	moduleName: 'MyCKEditor',
-	creator: './build/esnext/ckeditor5/editor-classic/classic.js',
+	editor: 'editor-classic/classic.js',
 	features: [
 		'enter',
 		'paragraph',

+ 32 - 26
dev/tasks/bundle/tasks.js

@@ -15,12 +15,14 @@ const runSequence = require( 'run-sequence' );
 const utils = require( './utils' );
 const rollup = require( 'rollup' ).rollup;
 const rollupBabel = require( 'rollup-plugin-babel' );
+const mkdirp = require( 'mkdirp' );
 
 module.exports = ( config ) => {
 	const args = utils.parseArguments();
 	const sourceBuildDir = path.join( config.ROOT_DIR, config.BUILD_DIR, 'esnext' );
 	const bundleDir = path.join( config.ROOT_DIR, config.BUNDLE_DIR );
-	const temporaryEntryFilePath = './tmp/entryfile.js';
+	const bundleTmpDir = path.join( bundleDir, 'tmp' );
+	const temporaryEntryFilePath = path.join( bundleTmpDir, 'entryfile.js' );
 
 	const tasks = {
 		/**
@@ -33,24 +35,29 @@ module.exports = ( config ) => {
 		/**
 		 * Combine whole editor files into two files `ckeditor.js` and `ckeditor.css`.
 		 *
-		 * For JS bundle is needed to pass configuration file `gulp bundle --config path/to/file.js` where
-		 * we need to specify which features and creator we want to include in our bundle.
+		 * For JS bundle is required to pass configuration file `gulp bundle --config path/to/file.js` where
+		 * we need to specify which editor and features we want to include in our bundle.
 		 *
-		 * 		example-config-file.js
-		 * 		modude.exports = {
-		 * 			// Name of CKEditor instance exposed by bundle.
-		 * 			moduleName 'MyCKEditor',
+		 * 		example-config-file.js:
+		 * 		module.exports = {
+		 * 			// Name of CKEditor instance exposed as global variable by a bundle.
+		 * 			moduleName: 'MyCKEditor',
 		 *
-		 * 			// E.g. `path/to/classic-editor/classic.js`.
-		 * 			creator: 'path/to/editor.creator.js'
+		 * 			// Path to specified editor.
+		 * 			// It could be a path relative to `build/esnext/ckeditor5` directory e.g. `classic-editor/classic.js`
+		 * 			// or path relative to directory where build task will be executed `./full/path/to/custom/editor.js`.
+		 * 			editor: 'classic-editor/classic.js'
 		 *
 		 * 			// List of features.
 		 * 			features: [
-		 * 				// It could be whole path.
-		 * 				'path/to/some/feature.js',
+		 * 				// It could be a plugin name only if plugin is a default CKEditor plugin.
+		 * 				'delete',
 		 *
-		 * 				// And it could be only name of feature if feature is default CKEditor feature.
-		 * 				'typing'
+		 * 				// It could be a path relative to `build/esnext/ckeditor5` directory.
+		 * 				`typing/typing.js`
+		 *
+		 * 				// Or it could be path relative to directory where build task will be executed.
+		 * 				'./path/to/some/custom/feature.js',
 		 * 			]
 		 * 		}
 		 *
@@ -62,15 +69,19 @@ module.exports = ( config ) => {
 			if ( !configFilePath ) {
 				// Then log error.
 				gutil.log( gutil.colors.red( `Bundle Error: Path to the config file is required. 'gulp bundle --config path/to/file.js'` ) );
-				// And stop process as failed.
-				process.exit( 1 );
+
+				// And stop task. as failed
+				return Promise.reject();
 			}
 
 			// Get configuration from the configuration file.
 			const config = require( path.resolve( '.', configFilePath ) );
 
-			// Create temporary entry file.
-			fs.writeFileSync( temporaryEntryFilePath, utils.renderEntryFileContent( config ) );
+			// Create a temporary entry file.
+			// Create proper directory structure first if not exist.
+			mkdirp.sync( bundleTmpDir );
+
+			fs.writeFileSync( temporaryEntryFilePath, utils.renderEntryFileContent( bundleTmpDir, config ) );
 
 			/**
 			 * Bundling JS by Rollup.
@@ -92,15 +103,15 @@ module.exports = ( config ) => {
 						moduleName: config.moduleName
 					} );
 				} ).then( () => {
-					// If everything went well then remove temporary entry file.
-					utils.clean( '', temporaryEntryFilePath );
+					// If everything went well then remove tmp directory.
+					utils.clean( bundleTmpDir, path.join( '' ) );
 				} ).catch( ( err ) => {
 					// If something went wrong then log error.
 					gutil.log( gutil.colors.red( `Bundle Error` ) );
 					gutil.log( gutil.colors.red( err.stack ) );
 
-					// And remove temporary entry file.
-					utils.clean( '', temporaryEntryFilePath );
+					// And remove tmp directory.
+					utils.clean( bundleTmpDir, path.join( '' ) );
 				} );
 			}
 
@@ -142,11 +153,6 @@ module.exports = ( config ) => {
 		register() {
 			gulp.task( 'bundle:clean', tasks.clean );
 			gulp.task( 'bundle:generate',
-				[
-					'bundle:clean',
-					'build:js:esnext',
-					'build:themes:esnext'
-				],
 				() => tasks.generate( args.config )
 			);
 			gulp.task( 'bundle:minify:js', tasks.minify.js );

+ 69 - 29
dev/tasks/bundle/utils.js

@@ -33,53 +33,95 @@ const utils = {
 	},
 
 	/**
-	 * Resolves a simplified plugin name to a real path.
+	 * When module path is not relative then treat this path as a path to the one of the ckeditor5 default module
+	 * (relative to ./bundle/exnext/ckedotir5) and add prefix `./build/esnext/ckeditor5/` to this path.
+	 *
+	 * @param {String} modulePath Path the ckeditor5 module.
+	 */
+	getFullPath( modulePath ) {
+		// If path is not a relative path (no leading ./ or ../).
+		if ( modulePath.charAt( 0 ) != '.' ) {
+			return `./${ path.join( 'build/esnext/ckeditor5', modulePath ) }`;
+		}
+
+		return modulePath;
+	},
+
+	/**
+	 * Resolves a simplified plugin name to a real path if only name is passed.
+	 * E.g. 'delete' will be transformed to './build/esnext/ckeditor5/delete/delete.js'.
 	 *
 	 * @param {String} name
 	 * @returns {String} Path to the module.
 	 */
 	getPluginPath( name ) {
-		if ( name.indexOf( '/' ) > 0 ) {
-			return name;
+		if ( name.indexOf( '/' ) >= 0 ) {
+			return utils.getFullPath( name );
 		}
 
-		return './build/esnext/ckeditor5/' + ( name + '/' + name ) + '.js';
+		return utils.getFullPath( `${ name }/${ name }.js` );
+	},
+
+	/**
+	 * Transform first letter of passed string to the upper case.
+	 *
+	 * @params {String} string String that will be transformed.
+	 * @returns {String} Transformed string.
+	 */
+	capitalize( string ) {
+		return string.charAt( 0 ).toUpperCase() + string.slice( 1 );
 	},
 
 	/**
 	 * Render content for entry file which needs to be passed as main file to the Rollup.
 	 *
-	 * @param {Object} data
-	 * @param {String} [data.moduleName] Name of the editor class exposed in bundle. e.g. MyCKEditor.
-	 * @param {String} [data.creator] Path to the editor creator.
-	 * @param {Array<String>} [data.features] List of paths or names to features which need to be included in bundle.
+	 * @param {String} dir Path to the entryfile directory. Import paths need to be relative to this directory.
+	 * @param {Object} data Configuration object.
+	 * @param {String} [data.moduleName] Name of the editor class exposed as global variable by bundle. e.g. MyCKEditor.
+	 * @param {String} [data.editor] Path to the editor version e.g. `classic-editor/classic.js`.
+	 * @param {Array.<String>} [data.features] List of paths or names to features which need to be included in bundle.
 	 * @returns {string}
 	 */
-	renderEntryFileContent( data ) {
-		const creatorName = path.basename( data.creator, '.js' );
-		const creatorPath = path.relative( './tmp', data.creator );
+	renderEntryFileContent( dir, data ) {
+		const creatorName = utils.capitalize( path.basename( data.editor, '.js' ) );
+		const creatorPath = path.relative( dir, utils.getFullPath( data.editor ) );
 		let featureNames = [];
-		let template = `'use strict';\n\n`;
 
-		// Imports babel helpers.
-		template += `import '../node_modules/regenerator-runtime/runtime.js';\n`;
+		function renderPluginImports( features = [] ) {
+			let templateFragment = '';
+
+			for ( let feature of features ) {
+				feature = utils.getPluginPath( feature );
 
-		// Imports editor creator
-		template += `import ${ creatorName } from '${ creatorPath }';\n`;
+				const featurePath = path.relative( dir, feature );
 
-		// Imports editor features.
-		for ( let feature of data.features ) {
-			feature = utils.getPluginPath( feature );
+				// Generate unique feature name.
+				// In case of two ore more features will have the same name but different path
+				// 		'typing', 'path/to/other/plugin/typing'
+				let featureName = utils.capitalize( path.basename( feature, '.js' ) );
+				let i = 0;
 
-			const featureName = path.basename( feature, '.js' );
-			const featurePath = path.relative( './tmp', feature );
+				while ( featureNames.indexOf( featureName ) >= 0 ) {
+					featureName = utils.capitalize( path.basename( feature, `.js` ) ) + ( ++i ).toString();
+				}
 
-			template += `import ${ featureName } from '${ featurePath }';\n`;
-			featureNames.push( featureName );
+				templateFragment += `import ${ featureName } from '${ featurePath }';\n`;
+				featureNames.push( featureName );
+			}
+
+			return templateFragment;
 		}
 
-		// Class definition.
-		template += `\nexport default class ${ data.moduleName } extends ${ creatorName } {
+		return `
+'use strict';
+
+// Babel helpers.
+import '${ path.relative( dir, 'node_modules/regenerator-runtime/runtime.js' ) }';
+
+import ${ creatorName } from '${ creatorPath }';
+${ renderPluginImports( data.features ) }
+
+export default class ${ data.moduleName } extends ${ creatorName } {
 	static create( element, config = {} ) {
 		if ( !config.features ) {
 			config.features = [];
@@ -91,8 +133,6 @@ const utils = {
 	}
 }
 `;
-
-		return template;
 	},
 
 	/**
@@ -148,7 +188,7 @@ const utils = {
 	/**
 	 * Get normal and gzipped size of every passed file in specified directory.
 	 *
-	 * @param {Array<String>} files
+	 * @param {Array.<String>} files
 	 * @param {String} [rootDir='']
 	 * @returns {Array<Object>} List with file size data
 	 */
@@ -172,7 +212,7 @@ const utils = {
 	 *        file.css 500 kB (gzipped: 100 kB)
 	 *
 	 * @param {String} title
-	 * @param {Array<Object>} filesStats
+	 * @param {Array.<Object>} filesStats
 	 */
 	showFilesSummary( title, filesStats ) {
 		const label = gutil.colors.underline( title );

+ 0 - 63
dev/tests/bundle/tasks.js

@@ -1,63 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* global describe, it, beforeEach, afterEach */
-
-'use strict';
-
-const mockery = require( 'mockery' );
-const sinon = require( 'sinon' );
-
-describe( 'build-tasks', () => {
-	let sandbox, tasks, rollupBundleMock, rollupBundleWriteMock;
-	const config = {
-		ROOT_DIR: '.',
-		BUILD_DIR: 'build',
-		BUNDLE_DIR: 'bundle'
-	};
-
-	beforeEach( () => {
-		mockery.enable( {
-			warnOnReplace: false,
-			warnOnUnregistered: false
-		} );
-
-		sandbox = sinon.sandbox.create();
-
-		rollupBundleWriteMock = sandbox.spy();
-		rollupBundleMock = {
-			write: rollupBundleWriteMock
-		};
-
-		mockery.registerMock( 'rollup', {
-			rollup: () => {
-				return {
-					then: ( resolve ) => {
-						resolve( rollupBundleMock );
-					}
-				};
-			}
-		} );
-
-		tasks = require( '../../tasks/bundle/tasks' )( config );
-	} );
-
-	afterEach( () => {
-		mockery.disable();
-		sandbox.restore();
-	} );
-
-	describe( 'generate', () => {
-		it( 'should use rollup to generate js bundle and save bundled file', () => {
-			tasks.generate();
-
-			sinon.assert.calledWithExactly( rollupBundleWriteMock, {
-				dest: 'bundle/ckeditor.js',
-				format: 'iife',
-				moduleName: 'ClassicEditor'
-			} );
-		} );
-	} );
-} );

+ 152 - 8
dev/tests/bundle/utils.js

@@ -28,7 +28,151 @@ describe( 'bundle-utils', () => {
 	} );
 
 	it( 'should be extended by top level utils', () => {
-		expect( utils.clean ).to.be.equal( mainUtils.clean );
+		expect( utils.clean ).to.equal( mainUtils.clean );
+	} );
+
+	describe( 'getFullPath', () => {
+		it( 'should return full path when passed path not relative', () => {
+			expect( utils.getFullPath( 'editor-classic/classic.js' ) ).to.equal( './build/esnext/ckeditor5/editor-classic/classic.js' );
+		} );
+
+		it( 'should return unmodified path when passed path is a relative', () => {
+			expect( utils.getFullPath( './path/to/editor-classic/classic.js' ) ).to.equal( './path/to/editor-classic/classic.js' );
+			expect( utils.getFullPath( '../path/to/editor-classic/classic.js' ) ).to.equal( '../path/to/editor-classic/classic.js' );
+		} );
+	} );
+
+	describe( 'getPluginPath()', () => {
+		it( 'should resolve a simple plugin name to the full path', () => {
+			expect( utils.getPluginPath( 'typing' ) ).to.equal( './build/esnext/ckeditor5/typing/typing.js' );
+		} );
+
+		it( 'should return full path if passed argument is a relative path', () => {
+			expect( utils.getPluginPath( 'typing/typing.js' ) ).to.equal( './build/esnext/ckeditor5/typing/typing.js' );
+		} );
+
+		it( 'should return unmodified plugin path if passed argument is a relative path', () => {
+			expect( utils.getPluginPath( './typing.js' ) ).to.equal( './typing.js' );
+			expect( utils.getPluginPath( '../typing.js' ) ).to.equal( '../typing.js' );
+		} );
+	} );
+
+	describe( 'capitalize()', () => {
+		it( 'should transform first letter of the passed string to uppercase', () => {
+			expect( utils.capitalize( 'string' ) ).to.equal( 'String' );
+			expect( utils.capitalize( 'multi word string' ) ).to.equal( 'Multi word string' );
+		} );
+	} );
+
+	describe( 'renderEntryFileContent()', () => {
+		it( 'should render file content with proper data', () => {
+			const result = utils.renderEntryFileContent( './bundle/tmp', {
+				moduleName: 'MyCKEditor',
+				editor: 'editor-classic/classic.js',
+				features: [
+					'delete',
+					'path/to/default.js',
+					'./path/to/custom.js'
+				]
+			} );
+
+			const expected = `
+'use strict';
+
+// Babel helpers.
+import '../../node_modules/regenerator-runtime/runtime.js';
+
+import Classic from '../../build/esnext/ckeditor5/editor-classic/classic.js';
+import Delete from '../../build/esnext/ckeditor5/delete/delete.js';
+import Default from '../../build/esnext/ckeditor5/path/to/default.js';
+import Custom from '../../path/to/custom.js';
+
+
+export default class MyCKEditor extends Classic {
+	static create( element, config = {} ) {
+		if ( !config.features ) {
+			config.features = [];
+		}
+
+		config.features = [ ...config.features, Delete, Default, Custom ];
+
+		return Classic.create( element, config );
+	}
+}
+`;
+
+			expect( result ).to.equal( expected );
+		} );
+
+		it( 'should render file content with unique plugin names', () => {
+			const result = utils.renderEntryFileContent( './bundle/tmp', {
+				moduleName: 'MyCKEditor',
+				editor: 'editor-classic/classic.js',
+				features: [
+					'plugin',
+					'path/to/plugin.js',
+					'other/path/to/plugin.js'
+				]
+			} );
+
+			const expected = `
+'use strict';
+
+// Babel helpers.
+import '../../node_modules/regenerator-runtime/runtime.js';
+
+import Classic from '../../build/esnext/ckeditor5/editor-classic/classic.js';
+import Plugin from '../../build/esnext/ckeditor5/plugin/plugin.js';
+import Plugin1 from '../../build/esnext/ckeditor5/path/to/plugin.js';
+import Plugin2 from '../../build/esnext/ckeditor5/other/path/to/plugin.js';
+
+
+export default class MyCKEditor extends Classic {
+	static create( element, config = {} ) {
+		if ( !config.features ) {
+			config.features = [];
+		}
+
+		config.features = [ ...config.features, Plugin, Plugin1, Plugin2 ];
+
+		return Classic.create( element, config );
+	}
+}
+`;
+
+			expect( result ).to.equal( expected );
+		} );
+
+		it( 'should render file content with proper without features', () => {
+			const result = utils.renderEntryFileContent( './bundle/tmp', {
+				moduleName: 'MyCKEditor',
+				editor: 'editor-classic/classic.js'
+			} );
+
+			const expected = `
+'use strict';
+
+// Babel helpers.
+import '../../node_modules/regenerator-runtime/runtime.js';
+
+import Classic from '../../build/esnext/ckeditor5/editor-classic/classic.js';
+
+
+export default class MyCKEditor extends Classic {
+	static create( element, config = {} ) {
+		if ( !config.features ) {
+			config.features = [];
+		}
+
+		config.features = [ ...config.features,  ];
+
+		return Classic.create( element, config );
+	}
+}
+`;
+
+			expect( result ).to.equal( expected );
+		} );
 	} );
 
 	describe( 'getFileSize', () => {
@@ -70,14 +214,14 @@ describe( 'bundle-utils', () => {
 		} );
 
 		it( 'should returns an array with two elements', () => {
-			const result = utils.getFilesSizeStats( [ 'sub/dir/file.js', 'other/sub/dir/file.css' ] , 'root/path' );
+			const result = utils.getFilesSizeStats( [ 'sub/dir/file.js', 'other/sub/dir/file.css' ], 'root/path' );
 
 			expect( result ).to.be.an( 'array' );
 			expect( result ).to.have.length( 2 );
 		} );
 
 		it( 'should returns list of object with files stats', () => {
-			const result = utils.getFilesSizeStats( [ 'sub/dir/file.js', 'other/sub/dir/file.css' ] , 'root/path' );
+			const result = utils.getFilesSizeStats( [ 'sub/dir/file.js', 'other/sub/dir/file.css' ], 'root/path' );
 
 			expect( result ).to.be.deep.equal( [
 				{ name: 'file.js', size, gzippedSize },
@@ -88,10 +232,10 @@ describe( 'bundle-utils', () => {
 		it( 'should get files from root directory', () => {
 			let basenameSpy = sandbox.spy( path, 'basename' );
 
-			const result = utils.getFilesSizeStats( [ 'sub/dir/file.js', 'other/sub/dir/file.css' ] , 'root/path' );
+			const result = utils.getFilesSizeStats( [ 'sub/dir/file.js', 'other/sub/dir/file.css' ], 'root/path' );
 
-			expect( result[0] ).to.have.property( 'name' ).equal( 'file.js' );
-			expect( result[1] ).to.have.property( 'name' ).equal( 'file.css' );
+			expect( result[ 0 ] ).to.have.property( 'name', 'file.js' );
+			expect( result[ 1 ] ).to.have.property( 'name', 'file.css' );
 			sinon.assert.calledWithExactly( basenameSpy.firstCall, 'root/path/sub/dir/file.js' );
 			sinon.assert.calledWithExactly( basenameSpy.secondCall, 'root/path/other/sub/dir/file.css' );
 		} );
@@ -101,8 +245,8 @@ describe( 'bundle-utils', () => {
 
 			const result = utils.getFilesSizeStats( [ 'sub/dir/file.js', 'file.css' ] );
 
-			expect( result[0] ).to.have.property( 'name' ).equal( 'file.js' );
-			expect( result[1] ).to.have.property( 'name' ).equal( 'file.css' );
+			expect( result[ 0 ] ).to.have.property( 'name', 'file.js' );
+			expect( result[ 1 ] ).to.have.property( 'name', 'file.css' );
 			sinon.assert.calledWithExactly( basenameSpy.firstCall, 'sub/dir/file.js' );
 			sinon.assert.calledWithExactly( basenameSpy.secondCall, 'file.css' );
 		} );

+ 1 - 0
package.json

@@ -67,6 +67,7 @@
     "jshint-reporter-jscs": "^0.1.0",
     "merge-stream": "^1.0.0",
     "minimist": "^1.2.0",
+    "mkdirp": "^0.5.1",
     "mockery": "^1.4.0",
     "multipipe": "^0.3.0",
     "ncp": "^2.0.0",