diff --git a/scripts/gulpfiles/build_tasks.js b/scripts/gulpfiles/build_tasks.js index d2d1e0709..f97d0452d 100644 --- a/scripts/gulpfiles/build_tasks.js +++ b/scripts/gulpfiles/build_tasks.js @@ -32,20 +32,34 @@ var {getPackageJson} = require('./helper_tasks'); // Build // //////////////////////////////////////////////////////////// +/** + * Suffix to add to compiled output files. + */ +const COMPILED_SUFFIX = '_compressed'; + /** * A list of chunks. Order matters: later chunks can depend on * earlier ones, but not vice-versa. All chunks are assumed to depend * on the first chunk. * - * Output files will be named _compressed.js. + * The function getChunkOptions will, after running + * closure-calculate-chunks, update each chunk to add the following + * properties: + * + * - .dependencies: a list of the chunks the chunk depends upon. + * - .wrapper: the chunk wrapper. + * + * Output files will be named .js. */ -const CHUNKS = [ +const chunks = [ { name: 'blockly', - entry: 'core/requires.js' + entry: 'core/requires.js', + namespace: 'Blockly', }, { name: 'blocks', - entry: 'blocks/all.js' + entry: 'blocks/all.js', + namespace: 'Blockly.blocks', }, { name: 'javascript', entry: 'generators/javascript/all.js', @@ -460,13 +474,44 @@ function buildLangfiles(done) { done(); }; +/** + * A helper method to return an closure compiler chunk wrapper that + * wraps the compiler output for the given chunk in a Universal Module + * Definition. + */ +function chunkWrapper(chunk) { + const fileNames = chunk.dependencies.map( + d => JSON.stringify(`./${d.name}${COMPILED_SUFFIX}.js`)); + const amdDeps = fileNames.join(', '); + const cjsDeps = fileNames.map(f => `require(${f})`).join(', '); + const browserDeps = + chunk.dependencies.map(d => `root.${d.namespace}`).join(', '); + const imports = chunk.dependencies.map(d => d.namespace).join(', '); + return `// Do not edit this file; automatically generated. + +/* eslint-disable */ +;(function(root, factory) { + if (typeof define === 'function' && define.amd) { // AMD + define([${amdDeps}], factory); + } else if (typeof exports === 'object') { // Node.js + module.exports = factory(${cjsDeps}); + } else { // Browser + root.${chunk.namespace} = factory(${browserDeps}); + } +}(this, function(${imports}) { + %output% +return ${chunk.namespace}; +})); +`; +}; + /** * Get chunking options to pass to Closure Compiler by using * closure-calculate-chunks (hereafter "ccc") to generate them based * on the deps.js file (which must be up to date!). * * The generated options are modified to use the original chunk names - * given in CHUNKS instead of the entry-point based names used by ccc. + * given in chunks instead of the entry-point based names used by ccc. * * @return {{chunk: !Array, js: !Array}} The chunking * information, in the same form as emitted by @@ -478,7 +523,7 @@ function getChunkOptions() { const cccArgs = [ '--closure-library-base-js-path ./closure/goog/base.js', '--deps-file ./tests/deps.js', - ...(CHUNKS.map(chunk => `--entrypoint '${chunk.entry}'`)), + ...(chunks.map(chunk => `--entrypoint '${chunk.entry}'`)), ]; const cccCommand = `closure-calculate-chunks ${cccArgs.join(' ')}`; const rawOptions= JSON.parse(String(execSync(cccCommand))); @@ -503,21 +548,32 @@ function getChunkOptions() { // This is designed to be passed directly as-is as the options // object to the Closure Compiler node API, but we want to replace // the unhelpful entry-point based chunk names (let's call these - // "nicknames") with the ones from CHUNKS. Luckily they will be in + // "nicknames") with the ones from chunks. Luckily they will be in // the same order that the entry points were supplied in - i.e., - // they correspond 1:1 with the entries in CHUNKS. + // they correspond 1:1 with the entries in chunks. const chunkByNickname = Object.create(null); const chunkList = rawOptions.chunk.map((element, index) => { - const [nickname, numJsFiles, depNicks] = element.split(':'); - const chunk = CHUNKS[index]; + const [nickname, numJsFiles, dependencyNicks] = element.split(':'); + const chunk = chunks[index]; chunkByNickname[nickname] = chunk; - const depNames = depNicks ? - depNicks.split(',').map(nick => chunkByNickname[nick].name) .join(',') - : ''; - return `${chunk.name}:${numJsFiles}:${depNames}`; + if (!dependencyNicks) { // Chunk has no dependencies. + chunk.dependencies = []; + return `${chunk.name}:${numJsFiles}`; + } + chunk.dependencies = + dependencyNicks.split(',').map(nick => chunkByNickname[nick]); + const dependencyNames = + chunk.dependencies.map(dependency => dependency.name).join(','); + return `${chunk.name}:${numJsFiles}:${dependencyNames}`; }); - return {chunk: chunkList, js: rawOptions.js}; + // Generate a chunk wrapper for each chunk. + for (const chunk of chunks) { + chunk.wrapper = chunkWrapper(chunk); + } + + const chunkWrappers = chunks.map(chunk => `${chunk.name}:${chunk.wrapper}`); + return {chunk: chunkList, js: rawOptions.js, chunk_wrapper: chunkWrappers}; } /** @@ -581,11 +637,11 @@ function buildCompiled(done) { hide_warnings_for: 'node_modules', // dependency_mode: 'PRUNE', externs: ['./externs/svg-externs.js', /* './externs/goog-externs.js' */], - output_wrapper: outputWrapperUMD('Blockly', []), define: 'Blockly.VERSION="' + packageJson.version + '"', chunk: chunkOptions.chunk, + chunk_wrapper: chunkOptions.chunk_wrapper, // Don't supply the list of source files in chunkOptions.js as an - // option to Closure Compiler; instead feed them to gulp.src. + // option to Closure Compiler; instead feed them as input via gulp.src. }; if (argv.debug || argv.strict) { options.jscomp_error = [...JSCOMP_ERROR]; @@ -602,7 +658,7 @@ function buildCompiled(done) { .pipe(gulp.sourcemaps.init()) .pipe(gulp.rename(flattenCorePaths)) .pipe(closureCompiler(options, pluginOptions)) - .pipe(gulp.rename({suffix: '_compressed'})) + .pipe(gulp.rename({suffix: COMPILED_SUFFIX})) .pipe(gulp.sourcemaps.mapSources(unflattenCorePaths)) .pipe( gulp.sourcemaps.write('.', {includeContent: false, sourceRoot: './'}))