diff --git a/gulpfile.js b/gulpfile.js index 931bba5e7..386535042 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -36,15 +36,295 @@ var fs = require('fs'); var rimraf = require('rimraf'); var execSync = require('child_process').execSync; -// Rebuilds Blockly, including the following: -// - blockly_compressed.js -// - blocks_compressed.js -// - Localization string tables in msg/js/*.js -// - Generators in generators/*.js -// These files are already up-to-date in the master branch. -gulp.task('build', gulp.shell.task([ - 'python build.py' -])); +var closureCompiler = require('google-closure-compiler').gulp(); +var packageJson = require('./package.json'); +var argv = require('yargs').argv; + + +//////////////////////////////////////////////////////////// +// Build // +//////////////////////////////////////////////////////////// + +const licenseRegex = `\\/\\*\\* + \\* @license + \\* [\\w: ]+ + \\* + \\* (Copyright \\d+ (Google Inc.|Massachusetts Institute of Technology)) + \\* (https://developers.google.com/blockly/|All rights reserved.) + \\* + \\* Licensed under the Apache License, Version 2.0 \\(the "License"\\); + \\* you may not use this file except in compliance with the License. + \\* You may obtain a copy of the License at + \\* + \\* http://www.apache.org/licenses/LICENSE-2.0 + \\* + \\* Unless required by applicable law or agreed to in writing, software + \\* distributed under the License is distributed on an "AS IS" BASIS, + \\* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + \\* See the License for the specific language governing permissions and + \\* limitations under the License. + \\*\\/`; + +/** + * 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"), ''); +} + +/** + * Helper method for prepending the auto-generated header text. + */ +function prependHeader() { + return gulp.insert.prepend(`// Do not edit this file; automatically generated by gulp.\n`); +} + +/** + * Helper method for calling the Closure compiler. + * @param {*} compilerOptions + * @param {boolean=} opt_verbose Optional option for verbose logging + */ +function compile(compilerOptions, opt_verbose) { + if (!compilerOptions) compilerOptions = {}; + compilerOptions.compilation_level = 'SIMPLE_OPTIMIZATIONS'; + compilerOptions.warning_level = opt_verbose ? 'VERBOSE' : 'DEFAULT'; + compilerOptions.language_out = 'ECMASCRIPT5_STRICT'; + compilerOptions.rewrite_polyfills = false; + compilerOptions.generate_exports = true; + compilerOptions.hide_warnings_for = 'node_modules'; + + const platform = ['native', 'java', 'javascript']; + + return closureCompiler(compilerOptions, { platform }); +} + +/** + * This task builds Blockly's core files. + * blockly_compressed.js + */ +gulp.task('build-core', function () { + const defines = 'Blockly.VERSION="' + packageJson.version + '"'; + return gulp.src([ + 'core/**/**/*.js', + './node_modules/google-closure-library/closure/goog/base.js' + ], {base: './'}) + // 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, "g"), "-"); + p.dirname = ""; + p.basename = dirname + "-" + p.basename; + })) + .pipe(stripApacheLicense()) + .pipe(compile({ + dependency_mode: 'PRUNE', + entry_point: './core-blockly.js', + js_output_file: 'blockly_compressed.js', + externs: './externs/svg-externs.js', + define: defines + }, argv.verbose)) + .pipe(prependHeader()) + .pipe(gulp.dest('./')); +}); + +/** + * This task builds the Blockly's built in blocks. + * blocks_compressed.js + */ +gulp.task('build-blocks', function () { + const provides = `goog.provide('Blockly');\ngoog.provide('Blockly.Blocks');\n`; + return gulp.src('blocks/*.js', {base: './'}) + // Add Blockly.Blocks to be compatible with the compiler. + .pipe(gulp.replace(`goog.provide('Blockly.Constants.Colour');`, + `${provides}goog.provide('Blockly.Constants.Colour');`)) + .pipe(stripApacheLicense()) + .pipe(compile({ + dependency_mode: 'NONE', + js_output_file: 'blocks_compressed.js' + }, argv.verbose)) + .pipe(gulp.replace('\'use strict\';', '\'use strict\';\n\n\n')) + // Remove Blockly.Blocks to be compatible with Blockly. + .pipe(gulp.replace('var Blockly={Blocks:{}};', '')) + .pipe(prependHeader()) + .pipe(gulp.dest('./')); +}); + +/** + * A helper method for building a Blockly code generator. + * @param {string} language Generator language. + * @param {string} namespace Language namespace. + */ +function buildGenerator(language, namespace) { + var provides = `goog.provide('Blockly.Generator');\ngoog.provide('Blockly.utils.string');\n`; + return gulp.src([`generators/${language}.js`, `generators/${language}/*.js`], {base: './'}) + .pipe(stripApacheLicense()) + // Add Blockly.Generator and Blockly.utils.string to be compatible with the compiler. + .pipe(gulp.replace(`goog.provide('Blockly.${namespace}');`, + `${provides}goog.provide('Blockly.${namespace}');`)) + .pipe(compile({ + dependency_mode: 'NONE', + js_output_file: `${language}_compressed.js` + }, argv.verbose)) + .pipe(gulp.replace('\'use strict\';', '\'use strict\';\n\n\n')) + // Remove Blockly.Generator and Blockly.utils.string to be compatible with Blockly. + .pipe(gulp.replace('var Blockly={Generator:{},utils:{}};Blockly.utils.string={};', '')) + .pipe(prependHeader()) + .pipe(gulp.dest('./')); +}; + +/** + * This task builds the javascript generator. + * javascript_compressed.js + */ +gulp.task('build-javascript', function() { + return buildGenerator('javascript', 'JavaScript'); +}); + +/** + * This task builds the python generator. + * python_compressed.js + */ +gulp.task('build-python', function() { + return buildGenerator('python', 'Python'); +}); + +/** + * This task builds the php generator. + * php_compressed.js + */ +gulp.task('build-php', function() { + return buildGenerator('php', 'PHP'); +}); + +/** + * This task builds the lua generator. + * lua_compressed.js + */ +gulp.task('build-lua', function() { + return buildGenerator('lua', 'Lua'); +}); + +/** + * This task builds the dart generator: + * dart_compressed.js + */ +gulp.task('build-dart', function() { + return buildGenerator('dart', 'Dart'); +}); + +/** + * This task builds Blockly's uncompressed file. + * blockly_uncompressed.js + */ +gulp.task('build-uncompressed', function() { + const header = `// Do not edit this file; automatically generated by build.py. +'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) { + var dir = ''; + if (root.IS_NODE_JS) { + dir = 'blockly'; + } else { + dir = this.BLOCKLY_DIR.match(/[^\\/]+$/)[0]; + } + // 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 { + // Delete any existing Closure (e.g. Soy's nogoog_shim). + document.write(''); + // Load fresh Closure Library. + document.write(''); + document.write(''); +} +`; + const file = 'blockly_uncompressed.js'; + // Run depswriter.py and which scans the core directory and writes out a ``goog.addDependency`` line for each file. + const cmd = `python ./node_modules/google-closure-library/closure/bin/build/depswriter.py \ + --root_with_prefix="./core ../core" > ${file}`; + execSync(cmd, { stdio: 'inherit' }); + + let providesBuilder = `\n// Load Blockly.\n`; + const provides = new Set(); + const dependencies = fs.readFileSync(file, "utf8"); + const re = /\'(Blockly[^\']*)\'/gi; + let m; + while (m = re.exec(dependencies)) { + provides.add(`goog.require('${m[1]}');`); + } + providesBuilder += Array.from(provides).sort().join('\n') + '\n'; + + return gulp.src(file) + // Remove comments so we're compatible with the build.py script + .pipe(gulp.replace(/\/\/.*\n/gm, '')) + // Replace quotes to be compatible with build.py + .pipe(gulp.replace(/\'(.*\.js)\'/gm, '"$1"')) + // Find the Blockly directory name and replace it with a JS variable. + // This allows blockly_uncompressed.js to be compiled on one computer and be + // used on another, even if the directory name differs. + .pipe(gulp.replace(/\.\.\/core/gm, `../../" + dir + "/core`)) + .pipe(gulp.insert.wrap(header, providesBuilder + footer)) + .pipe(gulp.dest('./')); +}); + +/** + * 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 + */ +gulp.task('build', gulp.parallel( + 'build-core', + 'build-blocks', + 'build-javascript', + 'build-python', + 'build-php', + 'build-lua', + 'build-dart' +)); + +//////////////////////////////////////////////////////////// +// Node // +//////////////////////////////////////////////////////////// // Concatenates the necessary files to load Blockly in a Node.js VM. Blockly's // individual libraries target use in a browser, where globals (via the window diff --git a/package.json b/package.json index 27f9b77b1..5e2890abb 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,13 @@ "name": "Neil Fraser" }, "scripts": { - "prepare": "gulp blockly_node_javascript_en", + "build": "gulp build", "lint": "eslint .", - "typings": "gulp typings", - "test": "tests/run_all_tests.sh", "package": "gulp package", - "release": "gulp release" + "prepare": "gulp blockly_node_javascript_en", + "release": "gulp release", + "test": "tests/run_all_tests.sh", + "typings": "gulp typings" }, "main": "./index.js", "umd": "./blockly.min.js", @@ -54,7 +55,8 @@ "pngjs": "^3.4.0", "rimraf": "^2.6.3", "typescript-closure-tools": "^0.0.7", - "webdriverio": "^5.11.5" + "webdriverio": "^5.11.5", + "yargs": "^14.0.0" }, "jshintConfig": { "globalstrict": true,