/** * @license * Copyright 2018 Google LLC * * 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. */ /** * @fileoverview Gulp script to build Blockly for Node & NPM. * Run this script by calling "npm install" in this directory. */ var gulp = require('gulp'); gulp.shell = require('gulp-shell'); gulp.concat = require('gulp-concat'); gulp.replace = require('gulp-replace'); gulp.rename = require('gulp-rename'); gulp.insert = require('gulp-insert'); gulp.umd = require('gulp-umd'); var path = require('path'); var fs = require('fs'); var rimraf = require('rimraf'); var execSync = require('child_process').execSync; var closureCompiler = require('google-closure-compiler').gulp(); var packageJson = require('./package.json'); var argv = require('yargs').argv; //////////////////////////////////////////////////////////// // Build // //////////////////////////////////////////////////////////// const licenseRegex = `\\/\\*\\* \\* @license \\* (Copyright \\d+ (Google LLC|Massachusetts Institute of Technology)) ( \\* 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.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' ], {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-requires.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'); goog.provide('Blockly.Blocks'); goog.provide('Blockly.Comment'); goog.provide('Blockly.FieldCheckbox'); goog.provide('Blockly.FieldColour'); goog.provide('Blockly.FieldDropdown'); goog.provide('Blockly.FieldImage'); goog.provide('Blockly.FieldLabel'); goog.provide('Blockly.FieldMultilineInput'); goog.provide('Blockly.FieldNumber'); goog.provide('Blockly.FieldTextInput'); goog.provide('Blockly.FieldVariable'); goog.provide('Blockly.Mutator');`; 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=\{[^;]*\};\n?/, '')) // Remove Blockly Fields to be compatible with Blockly. .pipe(gulp.replace(/Blockly\.Field[^=\(]+=\{[^;]*\};/g, '')) .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'); goog.provide('Blockly.utils.string');`; 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=\{[^;]*\};\s*Blockly.utils.string={};\n?/, '')) .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) { // 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' }); const requires = `\n// Load Blockly.\ngoog.require('Blockly.requires');\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, `../../core`)) .pipe(gulp.insert.wrap(header, requires + 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' )); //////////////////////////////////////////////////////////// // Typings // //////////////////////////////////////////////////////////// // Generates the TypeScript definition file (d.ts) for Blockly. // As well as generating the typings of each of the files under core/ and msg/, // the script also pulls in a number of part files from typings/parts. // This includes the header (incl License), additional useful interfaces // including Blockly Options and Google Closure typings. gulp.task('typings', function (cb) { const tmpDir = './typings/tmp'; const blocklySrcs = [ "core/", "core/components", "core/components/tree", "core/components/menu", "core/keyboard_nav", "core/renderers/common", "core/renderers/measurables", "core/theme", "core/utils", "msg/" ]; // Clean directory if exists. if (fs.existsSync(tmpDir)) { rimraf.sync(tmpDir); } fs.mkdirSync(tmpDir); // Find all files that will be included in the typings file. let files = []; blocklySrcs.forEach((src) => { files = files.concat(fs.readdirSync(src) .filter(fn => fn.endsWith('.js')) .map(fn => path.join(src, fn))); }); // Generate typings file for each file. files.forEach((file) => { const typescriptFileName = `${path.join(tmpDir, file)}.d.ts`; if (file.indexOf('core/msg.js') > -1) { return; } const cmd = `node ./node_modules/typescript-closure-tools/definition-generator/src/main.js ${file} ${typescriptFileName}`; console.log(`Generating typings for ${file}`); execSync(cmd, { stdio: 'inherit' }); }); const srcs = [ 'typings/parts/blockly-header.d.ts', 'typings/parts/blockly-interfaces.d.ts', `${tmpDir}/core/**`, `${tmpDir}/core/components/**`, `${tmpDir}/core/components/tree/**`, `${tmpDir}/core/components/menu/**`, `${tmpDir}/core/keyboard_nav/**`, `${tmpDir}/core/renderers/common/**`, `${tmpDir}/core/renderers/measurables/**`, `${tmpDir}/core/utils/**`, `${tmpDir}/core/theme/**`, `${tmpDir}/msg/**` ]; return gulp.src(srcs) .pipe(gulp.concat('blockly.d.ts')) .pipe(gulp.dest('typings')) .on('end', function () { // Clean up tmp directory. if (fs.existsSync(tmpDir)) { rimraf.sync(tmpDir); } }); }); //////////////////////////////////////////////////////////// // NPM packaging tasks // //////////////////////////////////////////////////////////// // The destination path where all the NPM distribution files will go. const packageDistribution = './dist'; /** * A helper method for wrapping a file into a Universal Module Definition. * @param {string} namespace The export namespace. * @param {Array} dependencies An array of dependencies to inject. */ function packageUMD(namespace, dependencies) { return gulp.umd({ dependencies: function () { return dependencies; }, namespace: function () { return namespace; }, exports: function () { return namespace; }, template: path.join(__dirname, 'package/templates/umd.template') }); }; /** * A helper method for wrapping a file into a CommonJS module for Node.js. * @param {string} namespace The export namespace. * @param {Array} dependencies An array of dependencies to inject. */ function packageCommonJS(namespace, dependencies) { return gulp.umd({ dependencies: function () { return dependencies; }, namespace: function () { return namespace; }, exports: function () { return namespace; }, template: path.join(__dirname, 'package/templates/node.template') }); }; /** * This task wraps blockly_compressed.js into a UMD module. * @example import 'blockly/blockly'; */ gulp.task('package-blockly', function() { return gulp.src('blockly_compressed.js') .pipe(packageUMD('Blockly', [])) .pipe(gulp.rename('blockly.js')) .pipe(gulp.dest(packageDistribution)); }); /** * This task wraps blocks_compressed.js into a CommonJS module for Node.js. * This is an equivelant task to package-blockly but for Node.js. * @example import 'blockly/blockly-node'; */ gulp.task('package-blockly-node', function() { // Override textToDomDocument, providing a Node.js alternative to DOMParser. return gulp.src('blockly_compressed.js') .pipe(gulp.insert.append(` if (typeof DOMParser !== 'function') { var DOMParser = require("jsdom/lib/jsdom/living").DOMParser; var XMLSerializer = require("jsdom/lib/jsdom/living").XMLSerializer; var doc = Blockly.utils.xml.textToDomDocument( ''); Blockly.utils.xml.document = function() { return doc; }; }`)) .pipe(packageCommonJS('Blockly', [])) .pipe(gulp.rename('blockly-node.js')) .pipe(gulp.dest(packageDistribution)); }) /** * This task wraps blocks_compressed.js into a UMD module. * @example import 'blockly/blocks'; */ gulp.task('package-blocks', function() { return gulp.src('blocks_compressed.js') .pipe(gulp.insert.prepend(` Blockly.Blocks={};`)) .pipe(packageUMD('Blockly.Blocks', [{ name: 'Blockly', amd: './core', cjs: './core', }])) .pipe(gulp.rename('blocks.js')) .pipe(gulp.dest(packageDistribution)); }); /** * This task wraps package/index.js into a UMD module. * We implicitly require the Node entry point in CommonJS environments, * and the Browser entry point for AMD environments. * @example import * as Blockly from 'blockly'; */ gulp.task('package-index', function() { return gulp.src('package/index.js') .pipe(packageUMD('Blockly', [{ name: 'Blockly', amd: './browser', cjs: './node', }])) .pipe(gulp.rename('index.js')) .pipe(gulp.dest(packageDistribution)); }); /** * This task wraps package/browser/index.js into a UMD module. * By default, the module includes Blockly core and built-in blocks, * as well as the JavaScript code generator and the English block * localization files. * This module is configured (in package.json) to replaces the module * built by package-node in browser environments. * @example import * as Blockly from 'blockly/browser'; */ gulp.task('package-browser', function() { return gulp.src('package/browser/index.js') .pipe(packageUMD('Blockly', [{ name: 'Blockly', amd: './core-browser', cjs: './core-browser', },{ name: 'En', amd: './msg/en', cjs: './msg/en', },{ name: 'BlocklyBlocks', amd: './blocks', cjs: './blocks', },{ name: 'BlocklyJS', amd: './javascript', cjs: './javascript', }])) .pipe(gulp.rename('browser.js')) .pipe(gulp.dest(packageDistribution)); }); /** * This task wraps package/browser/core.js into a UMD module. * By default, the module includes the Blockly core package and a * helper method to set the locale. * This module is configured (in package.json) to replaces the module * built by package-node-core in browser environments. * @example import * as Blockly from 'blockly/core'; */ gulp.task('package-core', function() { return gulp.src('package/browser/core.js') .pipe(packageUMD('Blockly', [{ name: 'Blockly', amd: './blockly', cjs: './blockly', }])) .pipe(gulp.rename('core-browser.js')) .pipe(gulp.dest(packageDistribution)); }); /** * This task wraps package/node/index.js into a CommonJS module for Node.js. * By default, the module includes Blockly core and built-in blocks, * as well as all the code generators and the English block localization files. * This module is configured (in package.json) to be replaced by the module * built by package-browser in browser environments. * @example import * as Blockly from 'blockly/node'; */ gulp.task('package-node', function() { return gulp.src('package/node/index.js') .pipe(packageCommonJS('Blockly', [{ name: 'Blockly', cjs: './core', },{ name: 'En', cjs: './msg/en', },{ name: 'BlocklyBlocks', cjs: './blocks', },{ name: 'BlocklyJS', cjs: './javascript', },{ name: 'BlocklyPython', cjs: './python', },{ name: 'BlocklyPHP', cjs: './php', },{ name: 'BlocklyLua', cjs: './lua', }, { name: 'BlocklyDart', cjs: './dart', }])) .pipe(gulp.rename('node.js')) .pipe(gulp.dest(packageDistribution)); }); /** * This task wraps package/node/core.js into a CommonJS module for Node.js. * By default, the module includes the Blockly core package for Node.js * and a helper method to set the locale. * This module is configured (in package.json) to be replaced by the module * built by package-core in browser environments. * @example import * as Blockly from 'blockly/core'; */ gulp.task('package-node-core', function() { return gulp.src('package/node/core.js') .pipe(packageCommonJS('Blockly', [{ name: 'Blockly', amd: './blockly-node', cjs: './blockly-node', }])) .pipe(gulp.rename('core.js')) .pipe(gulp.dest(packageDistribution)); }); /** * A helper method for packaging a Blockly code generator into a UMD module. * @param {string} file Source file name. * @param {string} rename Destination file name. * @param {string} generator Generator export namespace. */ function packageGenerator(file, rename, generator) { return gulp.src(file) .pipe(packageUMD(generator, [{ name: 'Blockly', amd: './core', cjs: './core', }])) .pipe(gulp.rename(rename)) .pipe(gulp.dest(packageDistribution)); }; /** * This task wraps javascript_compressed.js into a UMD module. * @example import 'blockly/javascript'; */ gulp.task('package-javascript', function() { return packageGenerator('javascript_compressed.js', 'javascript.js', 'Blockly.JavaScript'); }); /** * This task wraps python_compressed.js into a UMD module. * @example import 'blockly/python'; */ gulp.task('package-python', function() { return packageGenerator('python_compressed.js', 'python.js', 'Blockly.Python'); }); /** * This task wraps lua_compressed.js into a UMD module. * @example import 'blockly/lua'; */ gulp.task('package-lua', function() { return packageGenerator('lua_compressed.js', 'lua.js', 'Blockly.Lua'); }); /** * This task wraps dart_compressed.js into a UMD module. * @example import 'blockly/dart'; */ gulp.task('package-dart', function() { return packageGenerator('dart_compressed.js', 'dart.js', 'Blockly.Dart'); }); /** * This task wraps php_compressed.js into a UMD module. * @example import 'blockly/php'; */ gulp.task('package-php', function() { return packageGenerator('php_compressed.js', 'php.js', 'Blockly.PHP'); }); /** * This task wraps each of the msg/js/* files into a UMD module. * @example import * as En from 'blockly/msg/en'; */ gulp.task('package-locales', function() { // Remove references to goog.provide and goog.require. return gulp.src('msg/js/*.js') .pipe(gulp.replace(/goog\.[^\n]+/g, '')) .pipe(gulp.insert.prepend(` var Blockly = {};Blockly.Msg={};`)) .pipe(packageUMD('Blockly.Msg', [{ name: 'Blockly', amd: '../core', cjs: '../core', }])) .pipe(gulp.dest(`${packageDistribution}/msg`)); }); /** * This task creates a UMD bundle of Blockly which includes the Blockly * core files, the built-in blocks, the JavaScript code generator and the * English localization files. * @example */ gulp.task('package-umd-bundle', function() { var srcs = [ 'blockly_compressed.js', 'msg/js/en.js', 'blocks_compressed.js', 'javascript_compressed.js' ]; return gulp.src(srcs) .pipe(gulp.concat('blockly.min.js')) .pipe(packageUMD('Blockly', [])) .pipe(gulp.dest(`${packageDistribution}`)) }); /** * This task copies all the media/* files into the distribution directory. */ gulp.task('package-media', function() { return gulp.src('./media/*') .pipe(gulp.dest(`${packageDistribution}/media`)); }); /** * This task copies the package.json file into the distribution directory. */ gulp.task('package-json', function() { return gulp.src('./package.json') .pipe(gulp.dest(`${packageDistribution}`)) }); /** * This task copies the package/README.md file into the distribution directory. * This file is what developers will see at https://www.npmjs.com/package/blockly. */ gulp.task('package-readme', function() { return gulp.src('./package/README.md') .pipe(gulp.dest(`${packageDistribution}`)) }); /** * This task copies the typings/blockly.d.ts TypeScript definition file into the * distribution directory. * The bundled declaration file is referenced in package.json in the types property. */ gulp.task('package-dts', function() { return gulp.src('./typings/blockly.d.ts') .pipe(gulp.dest(`${packageDistribution}`)) }); /** * This task prepares the NPM distribution files under the /dist directory. */ gulp.task('package', gulp.parallel( 'package-index', 'package-browser', 'package-node', 'package-core', 'package-node-core', 'package-blockly', 'package-blockly-node', 'package-blocks', 'package-javascript', 'package-python', 'package-lua', 'package-dart', 'package-php', 'package-locales', 'package-media', 'package-umd-bundle', 'package-json', 'package-readme', 'package-dts' )); // The release task prepares Blockly for an npm release. // It rebuilds the Blockly compressed files and updates the TypeScript // typings, and then packages all the npm release files into the /dist directory gulp.task('release', gulp.series(['build', 'typings', function() { // Clean directory if exists if (fs.existsSync(packageDistribution)) { rimraf.sync(packageDistribution); } fs.mkdirSync(packageDistribution); }, 'package'])); // The default task builds Blockly. gulp.task('default', gulp.series(['build']));