Files
blockly/scripts/gulpfiles/build_tasks.js
Christopher Allen a03cd29b68 Separate script to rebuild msg/json/en.json, qqq.json etc.
There are some files in msg/json/ (currently en.json, qqq.json,
constants.json and synonyms.json) that are generated by
scripts/i18n/js_to_json.py as part of the language file build process
- but this only needs to be done when messages.js is updated and
and usually requires some manual cleanup, so remove this step from the
existing buildLangfiles gulp script and create a separate command
('npm run generate:langfiles') to do this when required.
2021-06-22 10:59:24 +01:00

547 lines
16 KiB
JavaScript

/**
* @license
* Copyright 2018 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Gulp script to build Blockly for Node & NPM.
*/
var gulp = require('gulp');
gulp.replace = require('gulp-replace');
gulp.rename = require('gulp-rename');
gulp.sourcemaps = require('gulp-sourcemaps');
var path = require('path');
var fs = require('fs');
var execSync = require('child_process').execSync;
var through2 = require('through2');
var closureCompiler = require('google-closure-compiler').gulp();
var closureDeps = require('google-closure-deps');
var argv = require('yargs').argv;
var {BUILD_DIR} = require('./config');
var {getPackageJson} = require('./helper_tasks');
////////////////////////////////////////////////////////////
// Build //
////////////////////////////////////////////////////////////
const licenseRegex = `\\/\\*\\*
\\* @license
\\* (Copyright \\d+ (Google LLC|Massachusetts Institute of Technology))
( \\* All rights reserved.
)? \\* SPDX-License-Identifier: Apache-2.0
\\*\\/`;
/**
* Helper method for stripping the Google's and MIT's Apache Licenses.
*/
function stripApacheLicense() {
// Strip out Google's and MIT's Apache licences.
// Closure Compiler preserves dozens of Apache licences in the Blockly code.
// Remove these if they belong to Google or MIT.
// MIT's permission to do this is logged in Blockly issue #2412.
return gulp.replace(new RegExp(licenseRegex, "g"), '\n\n\n\n');
// Replace with the same number of lines so that source-maps are not affected.
}
/**
* Closure compiler warning groups used to treat warnings as errors.
* For a full list of closure compiler groups, consult:
* https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/DiagnosticGroups.java#L113
*/
var JSCOMP_ERROR = [
'accessControls',
'checkPrototypalTypes',
'checkRegExp',
'checkTypes',
'checkVars',
'conformanceViolations',
'const',
'constantProperty',
'deprecated',
'deprecatedAnnotations',
'duplicateMessage',
'es5Strict',
'externsValidation',
'extraRequire',
'functionParams',
'globalThis',
'invalidCasts',
'misplacedTypeAnnotation',
// 'missingOverride',
'missingPolyfill',
'missingProperties',
'missingProvide',
// 'missingRequire', As of Jan 8 2021, this enables the strict require check.
// Disabling this until we have fixed all the require issues.
'missingReturn',
// 'missingSourcesWarnings',
'moduleLoad',
'msgDescriptions',
'nonStandardJsDocs',
// 'polymer',
// 'reportUnknownTypes',
// 'strictCheckTypes',
// 'strictMissingProperties',
'strictModuleDepCheck',
// 'strictPrimitiveOperators',
// 'stricterMissingRequire',
'suspiciousCode',
'typeInvalidation',
'undefinedNames',
'undefinedVars',
'underscore',
'unknownDefines',
'unusedLocalVariables',
'unusedPrivateMembers',
'uselessCode',
'untranspilableFeatures',
'visibility'
];
/**
* Helper method for calling the Closure compiler.
* @param {*} compilerOptions
* @param {boolean=} opt_verbose Optional option for verbose logging
* @param {boolean=} opt_warnings_as_error Optional option for treating warnings
* as errors.
* @param {boolean=} opt_strict_typechecker Optional option for enabling strict
* type checking.
*/
function compile(compilerOptions, opt_verbose, opt_warnings_as_error,
opt_strict_typechecker) {
const options = {};
options.compilation_level = 'SIMPLE_OPTIMIZATIONS';
options.warning_level = opt_verbose ? 'VERBOSE' : 'DEFAULT';
options.language_out = 'ECMASCRIPT5_STRICT';
options.rewrite_polyfills = false;
options.hide_warnings_for = 'node_modules';
if (opt_warnings_as_error || opt_strict_typechecker) {
options.jscomp_error = JSCOMP_ERROR;
if (opt_strict_typechecker) {
options.jscomp_error.push('strictCheckTypes');
}
}
const platform = ['native', 'java', 'javascript'];
return closureCompiler({...options, ...compilerOptions}, { platform });
}
/**
* Helper method for possibly adding the Closure library into a sources array.
* @param {Array<string>} srcs
*/
function maybeAddClosureLibrary(srcs) {
if (argv.closureLibrary) {
// If you require Google's Closure library, you can include it in your
// build by adding the --closure-library flag.
// You will also need to include the "google-closure-library" in your list
// of devDependencies.
console.log('Including the google-closure-library in your build.');
if (!fs.existsSync('./node_modules/google-closure-library')) {
throw Error('You must add the google-closure-library to your ' +
'devDependencies in package.json, and run `npm install`.');
}
srcs.push('./node_modules/google-closure-library/closure/goog/**/**/*.js');
}
return srcs;
}
/**
* A helper method to return an closure compiler output wrapper that wraps the
* body in a Universal Module Definition.
* @param {string} namespace The export namespace.
* @param {Array<Object>} dependencies An array of dependencies to inject.
*/
function outputWrapperUMD(namespace, dependencies) {
const amdDeps = dependencies.map(d => '\'' + d.amd + '\'' ).join(', ');
const cjsDeps = dependencies.map(d => `require('${d.cjs}')`).join(', ');
const browserDeps = dependencies.map(d => 'root.' + d.name).join(', ');
const imports = dependencies.map(d => d.name).join(', ');
return `// Do not edit this file; automatically generated by gulp.
/* 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.${namespace} = factory(${browserDeps});
}
}(this, function(${imports}) {
%output%
return ${namespace};
}));
`;
};
/**
* This task builds Blockly's core files.
* blockly_compressed.js
*/
function buildCompressed() {
var packageJson = getPackageJson();
const defines = 'Blockly.VERSION="' + packageJson.version + '"';
return gulp.src(maybeAddClosureLibrary(['core/**/**/*.js']), {base: './'})
.pipe(stripApacheLicense())
.pipe(gulp.sourcemaps.init())
// Directories in Blockly are used to group similar files together
// but are not used to limit access with @package, instead the
// method means something is internal to Blockly and not a public
// API.
// Flatten all files so they're in the same directory, but ensure that
// files with the same name don't conflict.
.pipe(gulp.rename(function(p) {
var dirname = p.dirname.replace(
new RegExp(path.sep.replace(/\\/, '\\\\'), "g"), "-");
p.dirname = "";
p.basename = dirname + "-" + p.basename;
}))
.pipe(compile(
{
dependency_mode: 'PRUNE',
entry_point: './core-requires.js',
js_output_file: 'blockly_compressed.js',
externs: ['./externs/svg-externs.js', './externs/goog-externs.js'],
define: defines,
output_wrapper: outputWrapperUMD('Blockly', [])
},
argv.verbose, argv.debug, argv.strict))
.pipe(gulp.sourcemaps.mapSources(function(sourcePath, file) {
return sourcePath.replace(/-/g, '/');
}))
.pipe(
gulp.sourcemaps.write('.', {includeContent: false, sourceRoot: './'}))
.pipe(gulp.dest(BUILD_DIR));
};
/**
* This task builds the Blockly's built in blocks.
* blocks_compressed.js
*/
function buildBlocks() {
return gulp.src(['blocks/*.js'], {base: './'})
.pipe(stripApacheLicense())
.pipe(gulp.sourcemaps.init())
.pipe(compile({
dependency_mode: 'NONE',
externs: ['./externs/goog-externs.js', './externs/block-externs.js'],
js_output_file: 'blocks_compressed.js',
output_wrapper: outputWrapperUMD('Blockly.Blocks', [{
name: 'Blockly',
amd: './blockly_compressed.js',
cjs: './blockly_compressed.js'
}])
}, argv.verbose, argv.debug, argv.strict))
.pipe(gulp.sourcemaps.write('.', {
includeContent: false,
sourceRoot: './'
}))
.pipe(gulp.dest(BUILD_DIR));
};
/**
* A helper method for building a Blockly code generator.
* @param {string} language Generator language.
* @param {string} namespace Language namespace.
*/
function buildGenerator(language, namespace) {
return gulp.src([`generators/${language}.js`, `generators/${language}/*.js`], {base: './'})
.pipe(stripApacheLicense())
.pipe(gulp.sourcemaps.init())
.pipe(compile({
dependency_mode: 'NONE',
externs: ['./externs/goog-externs.js', './externs/generator-externs.js'],
js_output_file: `${language}_compressed.js`,
output_wrapper: outputWrapperUMD(`Blockly.${namespace}`, [{
name: 'Blockly',
amd: './blockly_compressed.js',
cjs: './blockly_compressed.js'
}])
}, argv.verbose, argv.debug, argv.strict))
.pipe(gulp.sourcemaps.write('.', {
includeContent: false,
sourceRoot: './'
}))
.pipe(gulp.dest(BUILD_DIR));
};
/**
* This task builds the javascript generator.
* javascript_compressed.js
*/
function buildJavascript() {
return buildGenerator('javascript', 'JavaScript');
};
/**
* This task builds the python generator.
* python_compressed.js
*/
function buildPython() {
return buildGenerator('python', 'Python');
};
/**
* This task builds the php generator.
* php_compressed.js
*/
function buildPHP() {
return buildGenerator('php', 'PHP');
};
/**
* This task builds the lua generator.
* lua_compressed.js
*/
function buildLua() {
return buildGenerator('lua', 'Lua');
};
/**
* This task builds the dart generator:
* dart_compressed.js
*/
function buildDart() {
return buildGenerator('dart', 'Dart');
};
/**
* This tasks builds all the generators:
* javascript_compressed.js
* python_compressed.js
* php_compressed.js
* lua_compressed.js
* dart_compressed.js
*/
const buildGenerators = gulp.parallel(
buildJavascript,
buildPython,
buildPHP,
buildLua,
buildDart
);
/**
* This task builds Blockly's uncompressed file.
* blockly_uncompressed.js
*/
function buildUncompressed() {
const closurePath = argv.closureLibrary ?
'node_modules/google-closure-library/closure/goog' :
'closure/goog';
const header = `// Do not edit this file; automatically generated by gulp.
'use strict';
this.IS_NODE_JS = !!(typeof module !== 'undefined' && module.exports);
this.BLOCKLY_DIR = (function(root) {
if (!root.IS_NODE_JS) {
// Find name of current directory.
var scripts = document.getElementsByTagName('script');
var re = new RegExp('(.+)[\\\/]blockly_(.*)uncompressed\\\.js$');
for (var i = 0, script; script = scripts[i]; i++) {
var match = re.exec(script.src);
if (match) {
return match[1];
}
}
alert('Could not detect Blockly\\'s directory name.');
}
return '';
})(this);
this.BLOCKLY_BOOT = function(root) {
// Execute after Closure has loaded.
`;
const footer = `
delete root.BLOCKLY_DIR;
delete root.BLOCKLY_BOOT;
delete root.IS_NODE_JS;
};
if (this.IS_NODE_JS) {
this.BLOCKLY_BOOT(this);
module.exports = Blockly;
} else {
document.write('<script src="' + this.BLOCKLY_DIR +
'/${closurePath}/base.js"></script>');
document.write('<script>this.BLOCKLY_BOOT(this);</script>');
}
`;
let deps = [];
return gulp.src(maybeAddClosureLibrary(['core/**/**/*.js']))
.pipe(through2.obj((file, _enc, cb) => {
const result = closureDeps.parser.parseFile(file.path);
for (const dep of result.dependencies) {
deps.push(dep);
}
cb(null);
}))
.on('end', () => {
// Update the path to closure for any files that we don't know the full path
// of (parsed from a goog.addDependency call).
for (const dep of deps) {
dep.setClosurePath(closurePath);
}
const addDependency = closureDeps.depFile
.getDepFileText(closurePath, deps)
.replace(/\\/g, '\/');
const requires = `goog.addDependency("base.js", [], []);
// Load Blockly.
goog.require('Blockly.requires');
`;
fs.writeFileSync('blockly_uncompressed.js',
header +
addDependency +
requires +
footer);
});
};
/**
* This task regenrates msg/json/en.js and msg/json/qqq.js from
* msg/messages.js.
*/
function generateLangfiles(done) {
// Run js_to_json.py
const jsToJsonCmd = `python scripts/i18n/js_to_json.py \
--input_file ${path.join('msg', 'messages.js')} \
--output_dir ${path.join('msg', 'json')} \
--quiet`;
execSync(jsToJsonCmd, { stdio: 'inherit' });
console.log(`
Regenerated several flies in msg/json/. Now run
git diff msg/json/*.json
and check that operation has not overwritten any modifications made to
hints, etc. by the TranslateWiki volunteers. If it has, backport
their changes to msg/messages.js and re-run 'npm run generate:langfiles'.
Once you are satisfied that any new hints have been backported you may
go ahead and commit the changes, but note that the generate script
will have removed the translator credits - be careful not to commit
this removal!
`);
done();
};
/**
* This task builds Blockly's lang files.
* msg/*.js
*/
function buildLangfiles(done) {
// Run create_messages.py
let json_files = fs.readdirSync(path.join('msg', 'json'));
json_files = json_files.filter(file => file.endsWith('json') &&
!(new RegExp(/(keys|synonyms|qqq|constants)\.json$/).test(file)));
json_files = json_files.map(file => path.join('msg', 'json', file));
const createMessagesCmd = `python ./scripts/i18n/create_messages.py \
--source_lang_file ${path.join('msg', 'json', 'en.json')} \
--source_synonym_file ${path.join('msg', 'json', 'synonyms.json')} \
--source_constants_file ${path.join('msg', 'json', 'constants.json')} \
--key_file ${path.join('msg', 'json', 'keys.json')} \
--output_dir ${path.join('msg', 'js')} \
--quiet ${json_files.join(' ')}`;
execSync(createMessagesCmd, { stdio: 'inherit' });
done();
};
/**
* This task builds Blockly core, blocks and generators together and uses
* closure compiler's ADVANCED_COMPILATION mode.
*/
function buildAdvancedCompilationTest() {
const srcs = [
'tests/compile/main.js', 'tests/compile/test_blocks.js', 'core/**/**/*.js',
'blocks/*.js', 'generators/**/*.js'
];
return gulp.src(maybeAddClosureLibrary(srcs), {base: './'})
.pipe(stripApacheLicense())
.pipe(gulp.sourcemaps.init())
// Directories in Blockly are used to group similar files together
// but are not used to limit access with @package, instead the
// method means something is internal to Blockly and not a public
// API.
// Flatten all files so they're in the same directory, but ensure that
// files with the same name don't conflict.
.pipe(gulp.rename(function(p) {
if (p.dirname.indexOf('core') === 0) {
var dirname = p.dirname.replace(
new RegExp(path.sep.replace(/\\/, '\\\\'), "g"), "-");
p.dirname = "";
p.basename = dirname + "-" + p.basename;
}
}))
.pipe(compile(
{
dependency_mode: 'PRUNE',
compilation_level: 'ADVANCED_OPTIMIZATIONS',
entry_point: './tests/compile/main.js',
js_output_file: 'main_compressed.js',
externs: ['./externs/svg-externs.js', './externs/goog-externs.js'],
},
argv.verbose, argv.strict))
.pipe(gulp.sourcemaps.mapSources(function(sourcePath, file) {
return sourcePath.replace(/-/g, '/');
}))
.pipe(gulp.sourcemaps.write(
'.', {includeContent: false, sourceRoot: '../../'}))
.pipe(gulp.dest('./tests/compile/'));
}
/**
* This tasks builds Blockly's core files:
* blockly_compressed.js
* blocks_compressed.js
* blockly_uncompressed.js
*/
const buildCore = gulp.parallel(
buildCompressed,
buildBlocks,
buildUncompressed
);
/**
* This task builds all of Blockly:
* blockly_compressed.js
* blocks_compressed.js
* javascript_compressed.js
* python_compressed.js
* php_compressed.js
* lua_compressed.js
* dart_compressed.js
* blockly_uncompressed.js
* msg/json/*.js
*/
const build = gulp.parallel(
buildCore,
buildGenerators,
buildLangfiles
);
module.exports = {
build: build,
core: buildCore,
blocks: buildBlocks,
generateLangfiles: generateLangfiles,
langfiles: buildLangfiles,
uncompressed: buildUncompressed,
compressed: buildCompressed,
generators: buildGenerators,
advancedCompilationTest: buildAdvancedCompilationTest,
}