/** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Gulp tasks to package Blockly for distribution on NPM. */ const gulp = require('gulp'); 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'); gulp.replace = require('gulp-replace'); const path = require('path'); const fs = require('fs'); const {rimraf} = require('rimraf'); const build = require('./build_tasks'); const {getPackageJson} = require('./helper_tasks'); const {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TYPINGS_BUILD_DIR} = require('./config'); // Path to template files for gulp-umd. const TEMPLATE_DIR = 'scripts/package/templates'; /** * 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, template = 'umd.template') { return gulp.umd({ dependencies: function () { return dependencies; }, namespace: function () { return namespace; }, exports: function () { return namespace; }, template: path.join(TEMPLATE_DIR, template) }); }; /** * This task wraps scripts/package/index.js into a UMD module. * * This module is the main entrypoint for the blockly package, and * loads blockly/core, blockly/blocks and blockly/msg/en and then * calls setLocale(en). */ function packageIndex() { return gulp.src('scripts/package/index.js') .pipe(packageUMD('Blockly', [{ name: 'Blockly', amd: 'blockly/core', cjs: 'blockly/core', },{ name: 'en', amd: 'blockly/msg/en', cjs: 'blockly/msg/en', global: 'Blockly.Msg', },{ name: 'blocks', amd: 'blockly/blocks', cjs: 'blockly/blocks', global: 'Blockly.Blocks', }])) .pipe(gulp.dest(RELEASE_DIR)); }; /** * This task copies scripts/package/core-node.js into into the * package. This module will be the 'blockly/core' entrypoint for * node.js environments. * * Note that, unlike index.js, this file does not get a UMD wrapper. * This is because it is only used in node.js environments and so is * guaranteed to be loaded as a CJS module. */ function packageCoreNode() { return gulp.src('scripts/package/core-node.js') .pipe(gulp.dest(RELEASE_DIR)); }; /** * This task wraps each of the files in ${BUILD_DIR/msg/ into a UMD module. * @example import * as En from 'blockly/msg/en'; */ function packageLocales() { // Remove references to goog.provide and goog.require. return gulp.src(`${LANG_BUILD_DIR}/*.js`) .pipe(gulp.replace(/goog\.[^\n]+/g, '')) .pipe(packageUMD('Blockly.Msg', [], 'umd-msg.template')) .pipe(gulp.dest(`${RELEASE_DIR}/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 */ function packageUMDBundle() { const srcs = [ `${RELEASE_DIR}/blockly_compressed.js`, `${RELEASE_DIR}/msg/en.js`, `${RELEASE_DIR}/blocks_compressed.js`, `${RELEASE_DIR}/javascript_compressed.js`, ]; return gulp.src(srcs) .pipe(gulp.concat('blockly.min.js')) .pipe(gulp.dest(`${RELEASE_DIR}`)); }; /** * This task creates shims for the submodule entrypoints, for the * benefit of bundlers and other build tools that do not correctly * support the exports declaration in package.json. These shims just * require() and reexport the corresponding *_compressed.js bundle. * * This should solve issues encountered by users of bundlers that don't * support exports at all (e.g. browserify) as well as ones that don't * support it in certain circumstances (e.g., when using webpack's * resolve.alias configuration option to alias 'blockly' to * 'node_modules/blockly', as we formerly did in most plugins, which * causes webpack to ignore blockly's package.json entirely). * * Assumptions: * - Such bundlers will _completely_ ignore the exports declaration. * - The bundles are intended to be used in a browser—or at least not * in node.js—so the core entrypoint never needs to route to * core-node.js. This is reasonable since there's little reason to * bundle code for node.js, and node.js has supported the exports * clause since at least v12, consideably older than any version of * node.js we officially support. * - It suffices to provide only a CJS entrypoint (because we can only * provide CJS or ESM, not both. (We could in future switch to * providing only an ESM entrypoint instead, though.) * * @param {Function} done Callback to call when done. */ function packageLegacyEntrypoints(done) { for (entrypoint of [ 'core', 'blocks', 'dart', 'javascript', 'lua', 'php', 'python' ]) { const bundle = (entrypoint === 'core' ? 'blockly' : entrypoint) + '_compressed.js'; fs.writeFileSync(path.join(RELEASE_DIR, `${entrypoint}.js`), `// Shim for backwards-compatibility with bundlers that do not // support the 'exports' clause in package.json, to allow them // to load the blockly/${entrypoint} submodule entrypoint. module.exports = require('./${bundle}'); `); } done(); } /** * This task copies all the media/* files into the release directory. */ function packageMedia() { return gulp.src('media/*', {encoding: false}) .pipe(gulp.dest(`${RELEASE_DIR}/media`)); }; /** * This task copies the package.json file into the release directory, * with modifications: * * - The scripts section is removed. * * Prerequisite: buildLangfiles. * * @param {Function} done Callback to call when done. */ function packageJSON(done) { // Copy package.json, so we can safely modify it. const json = JSON.parse(JSON.stringify(getPackageJson())); // Remove unwanted entries. delete json['scripts']; // Set "type": "commonjs", since that's what .js files in the // package root are. This should be a no-op since that's the // default, but by setting it explicitly we ensure that any chage to // the repository top-level package.json to set "type": "module" // won't break the published package accidentally. json.type = 'commonjs'; // Write resulting package.json file to release directory. if (!fs.existsSync(RELEASE_DIR)) { fs.mkdirSync(RELEASE_DIR, {recursive: true}); } fs.writeFileSync(`${RELEASE_DIR}/package.json`, JSON.stringify(json, null, 2)); done(); }; /** * This task copies the scripts/package/README.md file into the * release directory. This file is what developers will see at * https://www.npmjs.com/package/blockly . */ function packageReadme() { return gulp.src('scripts/package/README.md') .pipe(gulp.dest(RELEASE_DIR)); }; /** * This task copies the generated .d.ts files in build/declarations and the * hand-written .d.ts files in typings/ into the release directory. The main * entrypoint file (index.d.ts) is referenced in package.json in the types * property. */ function packageDTS() { const handwrittenSrcs = [ 'typings/*.d.ts', 'typings/msg/*.d.ts', ]; return gulp.src(handwrittenSrcs, {base: 'typings'}) .pipe(gulp.src(`${TYPINGS_BUILD_DIR}/**/*.d.ts`, {ignore: [ `${TYPINGS_BUILD_DIR}/blocks/**/*`, ]})) .pipe(gulp.replace('AnyDuringMigration', 'any')) .pipe(gulp.dest(RELEASE_DIR)); }; /** * This task cleans the release directory (by deleting it). */ function cleanReleaseDir() { // Sanity check. if (RELEASE_DIR === '.' || RELEASE_DIR === '/') { return Promise.reject(`Refusing to rm -rf ${RELEASE_DIR}`); } return rimraf(RELEASE_DIR); } /** * This task prepares the files to be included in the NPM by copying * them into the release directory. * * Prerequisite: build. */ const package = gulp.series( gulp.parallel( build.cleanBuildDir, cleanReleaseDir), build.build, gulp.parallel( packageIndex, packageCoreNode, packageLegacyEntrypoints, packageMedia, gulp.series(packageLocales, packageUMDBundle), packageJSON, packageReadme, packageDTS) ); module.exports = { // Main sequence targets. Each should invoke any immediate prerequisite(s). cleanReleaseDir: cleanReleaseDir, package: package, };