ソースを参照

Extracted bundler from `ckeditor5`.

Fixes https://github.com/ckeditor/ckeditor5/issues/271

Changed `gulpfile.js` to use bundler from
`ckeditor5-dev-bundler-rollup`.
Maksymilian Barnaś 9 年 前
コミット
4e0cc3d58a

dev/tasks/bundle/build-config-standard.js → dev/bundles/build-config-standard.js


+ 0 - 189
dev/tasks/bundle/tasks.js

@@ -1,189 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-const fs = require( 'fs' );
-const path = require( 'path' );
-const gulp = require( 'gulp' );
-const gulpCssnano = require( 'gulp-cssnano' );
-const gulpUglify = require( 'gulp-uglify' );
-const gutil = require( 'gulp-util' );
-const runSequence = require( 'run-sequence' );
-const utils = require( './utils' );
-const rollup = require( 'rollup' ).rollup;
-const rollupBabel = require( 'rollup-plugin-babel' );
-const mkdirp = require( 'mkdirp' );
-const { tools } = require( 'ckeditor5-dev-utils' );
-
-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 bundleTmpDir = path.join( bundleDir, 'tmp' );
-	const temporaryEntryFilePath = path.join( bundleTmpDir, 'entryfile.js' );
-
-	const tasks = {
-		/**
-		 * Removes all files from bundle directory.
-		 */
-		clean() {
-			return tools.clean( bundleDir, '*.*' );
-		},
-
-		/**
-		 * Combines whole editor files into two files `ckeditor.js` and `ckeditor.css`.
-		 *
-		 * 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:
-		 *
-		 *		module.exports = {
-		 *			// Name of CKEditor instance exposed as global variable by a bundle.
-		 *			moduleName: 'MyCKEditor',
-		 *
-		 *			// Path to specified editor module.
-		 *			// It could be a path relative to `build/esnext/ckeditor5` directory e.g. `editor-classic/classic`
-		 *			// or path relative to directory where build task will be executed `./full/path/to/custom/editor`.
-		 *			//
-		 *			// Note: file extension is appended automatically.
-		 *			editor: 'editor-classic/classic',
-		 *
-		 *			// List of features.
-		 *			//
-		 *			// Note: file extension is appended automatically.
-		 *			features: [
-		 *				// It could be a plugin name only if plugin is a default CKEditor plugin.
-		 *				'delete',
-		 *
-		 *				// It could be a path relative to `build/esnext/ckeditor5` directory.
-		 *				`typing/typing`,
-		 *
-		 *				// Or it could be a path relative to directory where build task will be executed.
-		 *				'./path/to/some/custom/feature',
-		 *			]
-		 *		};
-		 *
-		 * ** Note: ** Entry file is a physically existing file because rollup requires it.
-		 *
-		 * @param {String} configFilePath Path to the bundle configuration file.
-		 * @returns {Promise} Promise that resolve bundling for CSS and JS.
-		 */
-		generate( configFilePath ) {
-			// When config file is not specified.
-			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 task as failed.
-				return Promise.reject();
-			}
-
-			// Get configuration from the configuration file.
-			const config = require( path.resolve( '.', configFilePath ) );
-
-			// Create a temporary entry file with proper directory structure if not exist.
-			// Entry file can not be a stream because rollup requires physically existing file.
-			mkdirp.sync( bundleTmpDir );
-			fs.writeFileSync( temporaryEntryFilePath, utils.renderEntryFileContent( bundleTmpDir, config ) );
-
-			// Bundling JS by Rollup.
-			function bundleJS() {
-				const outputFile = path.join( bundleDir, 'ckeditor.js' );
-
-				return rollup( {
-					entry: temporaryEntryFilePath,
-					plugins: [
-						rollupBabel( {
-							presets: [ 'es2015-rollup' ]
-						} )
-					]
-				} )
-				.then( ( bundle ) => {
-					return bundle.write( {
-						dest: outputFile,
-						format: 'iife',
-						moduleName: config.moduleName
-					} );
-				} )
-				.then( () => {
-					// If everything went well then remove tmp directory.
-					tools.clean( bundleTmpDir, path.join( '' ) );
-				} )
-				.catch( ( err ) => {
-					// If something went wrong then log error.
-					gutil.log( gutil.colors.red( err.stack ) );
-
-					// And remove tmp directory.
-					tools.clean( bundleTmpDir, path.join( '' ) );
-
-					throw new Error( 'Build error.' );
-				} );
-			}
-
-			// CSS is already bundled by a build task, so we need only to copy it.
-			function bundleCss() {
-				const cssSource = path.join( sourceBuildDir, 'theme', 'ckeditor.css' );
-
-				return utils.copyFile( cssSource, bundleDir );
-			}
-
-			// Lets wait for both - JS and CSS.
-			return Promise.all( [ bundleJS(), bundleCss() ] );
-		},
-
-		minify: {
-			/**
-			 * JS minification by UglifyJS.
-			 */
-			js() {
-				let stream = gulp.src( path.join( bundleDir, 'ckeditor.js' ) )
-					.pipe( gulpUglify() );
-
-				return utils.saveFileFromStreamAsMinified( stream, bundleDir );
-			},
-
-			/**
-			 * CSS minification by cssnano.
-			 */
-			css() {
-				let stream = gulp.src( path.join( bundleDir, 'ckeditor.css' ) )
-					.pipe( gulpCssnano() );
-
-				return utils.saveFileFromStreamAsMinified( stream, bundleDir );
-			}
-		},
-
-		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 );
-			gulp.task( 'bundle:minify:css', tasks.minify.css );
-
-			gulp.task( 'bundle', ( callback ) => {
-				runSequence( 'bundle:generate', [ 'bundle:minify:js', 'bundle:minify:css' ], () => {
-					const files = [ 'ckeditor.js', 'ckeditor.css', 'ckeditor.min.js', 'ckeditor.min.css' ];
-					const filesStats = utils.getFilesSizeStats( files, bundleDir );
-
-					// Show bundle summary on console.
-					utils.showFilesSummary( 'Bundle summary', filesStats );
-
-					// Finish the task.
-					callback();
-				} );
-			} );
-		}
-	};
-
-	return tasks;
-};

+ 0 - 238
dev/tasks/bundle/utils.js

@@ -1,238 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-const fs = require( 'fs' );
-const path = require( 'path' );
-const gulp = require( 'gulp' );
-const gulpRename = require( 'gulp-rename' );
-const gutil = require( 'gulp-util' );
-const prettyBytes = require( 'pretty-bytes' );
-const gzipSize = require( 'gzip-size' );
-const minimist = require( 'minimist' );
-
-const utils = {
-	/**
-	 * Parses command line arguments and returns them as a user-friendly hash.
-	 *
-	 * @returns {Object} options
-	 * @returns {String} [options.config] Path to the bundle configuration file.
-	 */
-	parseArguments() {
-		const options = minimist( process.argv.slice( 2 ), {
-			string: [
-				'config'
-			]
-		} );
-
-		return options;
-	},
-
-	/**
-	 * 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/ckeditor5) and add prefix `./build/esnext/ckeditor5/` to this path.
-	 *
-	 * This method also adds `.js` extension.
-	 *
-	 * @param {String} modulePath Path to the module (without extension).
-	 */
-	getModuleFullPath( modulePath ) {
-		// If path is not a relative path (no leading ./ or ../).
-		if ( modulePath.charAt( 0 ) != '.' ) {
-			return `./${ path.join( 'build/esnext/ckeditor5', modulePath ) }.js`;
-		}
-
-		return modulePath + '.js';
-	},
-
-	/**
-	 * 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 utils.getModuleFullPath( name );
-		}
-
-		return utils.getModuleFullPath( `${ name }/${ name }` );
-	},
-
-	/**
-	 * Transforms first letter of passed string to the uppercase.
-	 *
-	 * @param {String} string String that will be transformed.
-	 * @returns {String} Transformed string.
-	 */
-	capitalize( string ) {
-		return string.charAt( 0 ).toUpperCase() + string.slice( 1 );
-	},
-
-	/**
-	 * Renders content for the entry file which needs to be passed as main file to the Rollup.
-	 *
-	 * @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 type 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} Entry file content.
-	 */
-	renderEntryFileContent( dir, data ) {
-		const creatorName = utils.capitalize( path.basename( data.editor, '.js' ) );
-		const creatorPath = path.relative( dir, utils.getModuleFullPath( data.editor ) );
-		let featureNames = [];
-
-		// Returns a list of plugin imports.
-		function renderPluginImports( features = [] ) {
-			let templateFragment = '';
-
-			for ( let feature of features ) {
-				feature = utils.getPluginPath( feature );
-
-				const featurePath = path.relative( dir, feature );
-
-				// Generate unique feature name.
-				// In case of two ore more features will have the same name:
-				// 		features: [
-				// 			'typing',
-				// 			'path/to/other/plugin/typing'
-				// 		]
-				let featureName = utils.capitalize( path.basename( feature, '.js' ) );
-				let i = 0;
-
-				while ( featureNames.indexOf( featureName ) >= 0 ) {
-					featureName = utils.capitalize( path.basename( feature, `.js` ) ) + ( ++i ).toString();
-				}
-
-				templateFragment += `import ${ featureName } from '${ featurePath }';\n`;
-				featureNames.push( featureName );
-			}
-
-			return templateFragment;
-		}
-
-		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 = [];
-		}
-
-		config.features = [ ...config.features, ${ featureNames.join( ', ' ) } ];
-
-		return ${ creatorName }.create( element, config );
-	}
-}
-`;
-	},
-
-	/**
-	 * Saves files from stream in specific destination and add `.min` suffix to the name.
-	 *
-	 * @param {Stream} stream Source stream.
-	 * @param {String} destination Path to the destination directory.
-	 * @returns {Stream}
-	 */
-	saveFileFromStreamAsMinified( stream, destination ) {
-		return stream
-			.pipe( gulpRename( {
-				suffix: '.min'
-			} ) )
-			.pipe( gulp.dest( destination ) );
-	},
-
-	/**
-	 * Copies specified file to specified destination.
-	 *
-	 * @param {String} from Source path.
-	 * @param {String} to Destination directory.
-	 * @returns {Promise}
-	 */
-	copyFile( from, to ) {
-		return new Promise( ( resolve ) => {
-			gulp.src( from )
-				.pipe( gulp.dest( to ) )
-				.on( 'finish', resolve );
-		} );
-	},
-
-	/**
-	 * Gets size of the file.
-	 *
-	 * @param {String} path Path to the file.
-	 * @returns {Number} Size in bytes.
-	 */
-	getFileSize( path ) {
-		return fs.statSync( path ).size;
-	},
-
-	/**
-	 * Gets human readable gzipped size of the file.
-	 *
-	 * @param {String} path Path to the file.
-	 * @returns {Number} Size in bytes.
-	 */
-	getGzippedFileSize( path ) {
-		return gzipSize.sync( fs.readFileSync( path ) );
-	},
-
-	/**
-	 * Gets normal and gzipped size of every passed file in specified directory.
-	 *
-	 * @param {Array.<String>} files List of file paths.
-	 * @param {String} [rootDir=''] When each file is in the same directory.
-	 * @returns {Array.<Object>} List with file size data.
-	 *
-	 * Objects contain the following fields:
-	 *
-	 * * name – File name.
-	 * * size – File size in human readable format.
-	 * * gzippedSize – Gzipped file size in human readable format.
-	 */
-	getFilesSizeStats( files, rootDir = '' ) {
-		return files.map( ( file ) => {
-			const filePath = path.join( rootDir, file );
-
-			return {
-				name: path.basename( filePath ),
-				size: utils.getFileSize( filePath ),
-				gzippedSize: utils.getGzippedFileSize( filePath )
-			};
-		} );
-	},
-
-	/**
-	 * Prints on console summary with a list of files with their size stats.
-	 *
-	 *		Title:
-	 *		file.js: 1 MB (gzipped: 400 kB)
-	 *		file.css 500 kB (gzipped: 100 kB)
-	 *
-	 * @param {String} title Summary title.
-	 * @param {Array.<Object>} filesStats
-	 */
-	showFilesSummary( title, filesStats ) {
-		const label = gutil.colors.underline( title );
-		const filesSummary = filesStats.map( ( file ) => {
-			return `${ file.name }: ${ prettyBytes( file.size ) } (gzipped: ${ prettyBytes( file.gzippedSize ) })`;
-		} ).join( '\n' );
-
-		gutil.log( gutil.colors.green( `\n${ label }:\n${ filesSummary }` ) );
-	}
-};
-
-module.exports = utils;

+ 0 - 249
dev/tests/bundle/utils.js

@@ -1,249 +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 chai = require( 'chai' );
-const expect = chai.expect;
-const sinon = require( 'sinon' );
-const path = require( 'path' );
-const fs = require( 'fs' );
-const gzipSize = require( 'gzip-size' );
-
-describe( 'bundle-utils', () => {
-	const utils = require( '../../tasks/bundle/utils' );
-	let sandbox;
-
-	beforeEach( () => {
-		sandbox = sinon.sandbox.create();
-	} );
-
-	afterEach( () => {
-		sandbox.restore();
-	} );
-
-	describe( 'getModuleFullPath', () => {
-		it( 'should return full path when passed path not relative', () => {
-			expect( utils.getModuleFullPath( 'editor-classic/classic' ) ).to.equal( './build/esnext/ckeditor5/editor-classic/classic.js' );
-		} );
-
-		it( 'should return unmodified path when passed path is a relative', () => {
-			expect( utils.getModuleFullPath( './path/to/editor-classic/classic' ) ).to.equal( './path/to/editor-classic/classic.js' );
-			expect( utils.getModuleFullPath( '../path/to/editor-classic/classic' ) ).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' ) ).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' ) ).to.equal( './typing.js' );
-			expect( utils.getPluginPath( '../typing' ) ).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',
-				features: [
-					'delete',
-					'path/to/default',
-					'./path/to/custom'
-				]
-			} );
-
-			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',
-				features: [
-					'plugin',
-					'path/to/plugin',
-					'other/path/to/plugin'
-				]
-			} );
-
-			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'
-			} );
-
-			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', () => {
-		it( 'should return file size in bytes', () => {
-			const filePath = 'path/to/file';
-			const size = 1337;
-			const statSyncMock = sandbox.stub( fs, 'statSync', () => {
-				return { size };
-			} );
-
-			expect( utils.getFileSize( filePath ) ).to.be.equal( size );
-			sinon.assert.calledWithExactly( statSyncMock, filePath );
-		} );
-	} );
-
-	describe( 'getGzippedFileSize', () => {
-		it( 'should return file size in bytes', () => {
-			const filePath = 'path/to/file';
-			const size = 1337;
-			const fileContent = 'some string';
-			const readFileSyncMock = sandbox.stub( fs, 'readFileSync', () => fileContent );
-			const gzipSizeMock = sandbox.stub( gzipSize, 'sync', () => 1337 );
-
-			expect( utils.getGzippedFileSize( filePath ) ).to.be.equal( size );
-			sinon.assert.calledWithExactly( readFileSyncMock, filePath );
-			sinon.assert.calledWithExactly( gzipSizeMock, fileContent );
-		} );
-	} );
-
-	describe( 'getFilesSizeStats', () => {
-		let size, gzippedSize;
-
-		beforeEach( () => {
-			size = 1337;
-			gzippedSize = 543;
-
-			sandbox.stub( utils, 'getFileSize', () => size );
-			sandbox.stub( utils, 'getGzippedFileSize', () => gzippedSize );
-		} );
-
-		it( 'should returns an array with two elements', () => {
-			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' );
-
-			expect( result ).to.be.deep.equal( [
-				{ name: 'file.js', size, gzippedSize },
-				{ name: 'file.css', size, gzippedSize }
-			] );
-		} );
-
-		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' );
-
-			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' );
-		} );
-
-		it( 'should get files if root directory is not specified', () => {
-			let basenameSpy = sandbox.spy( path, 'basename' );
-
-			const result = utils.getFilesSizeStats( [ 'sub/dir/file.js', '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' );
-		} );
-	} );
-} );

+ 25 - 1
gulpfile.js

@@ -8,6 +8,7 @@
 'use strict';
 
 const gulp = require( 'gulp' );
+const runSequence = require( 'run-sequence' );
 
 const config = {
 	ROOT_DIR: '.',
@@ -22,7 +23,6 @@ const config = {
 };
 
 require( './dev/tasks/build/tasks' )( config ).register();
-require( './dev/tasks/bundle/tasks' )( config ).register();
 require( './dev/tasks/test/tasks' )( config ).register();
 require( './dev/tasks/docs/tasks' )( config ).register();
 
@@ -44,3 +44,27 @@ gulp.task( 'st', ckeditor5DevEnv.checkStatus );
 gulp.task( 'relink', ckeditor5DevEnv.relink );
 gulp.task( 'install', ckeditor5DevEnv.installPackage );
 gulp.task( 'exec', ckeditor5DevEnv.execOnRepositories );
+
+// Bundling tasks.
+const ckeditor5DevBundle = require( 'ckeditor5-dev-bundler-rollup' )( config );
+gulp.task( 'bundle:clean', ckeditor5DevBundle.clean );
+gulp.task( 'bundle:generate',
+	[
+		'bundle:clean',
+		'build:js:esnext',
+		'build:themes:esnext'
+	],
+	ckeditor5DevBundle.generateFromConfig
+);
+gulp.task( 'bundle:minify:js', ckeditor5DevBundle.minify.js );
+gulp.task( 'bundle:minify:css', ckeditor5DevBundle.minify.css );
+
+gulp.task( 'bundle', ( callback ) => {
+	runSequence( 'bundle:generate',
+		[
+			'bundle:minify:js',
+			'bundle:minify:css'
+		],
+		() => ckeditor5DevBundle.showSummary( callback )
+	);
+} );