From c043ecf3178f20178d8d5609034f62dfba727e70 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Mon, 7 Dec 2020 10:52:40 -0800 Subject: [PATCH] fix(dev-infra): allow build-worker to be used in forked process (#40012) Generates a local copy of the build-worker file to allow it to be loaded at runtime in a forked process. PR Close #40012 --- dev-infra/BUILD.bazel | 59 +++--- dev-infra/build-worker.js | 316 ++++++++++++++++++++++++++++ dev-infra/index.bzl | 31 ++- dev-infra/release/build/BUILD.bazel | 4 + 4 files changed, 381 insertions(+), 29 deletions(-) create mode 100644 dev-infra/build-worker.js diff --git a/dev-infra/BUILD.bazel b/dev-infra/BUILD.bazel index d3e3bee4f2..c753f2abfb 100644 --- a/dev-infra/BUILD.bazel +++ b/dev-infra/BUILD.bazel @@ -1,6 +1,6 @@ -load("@build_bazel_rules_nodejs//:index.bzl", "generated_file_test", "pkg_npm") +load("@build_bazel_rules_nodejs//:index.bzl", "pkg_npm") load("@npm//@bazel/typescript:index.bzl", "ts_library") -load("@npm//@bazel/rollup:index.bzl", "rollup_bundle") +load("//dev-infra:index.bzl", "ng_dev_rolled_up_generated_file") ts_library( name = "cli", @@ -24,30 +24,6 @@ ts_library( ], ) -rollup_bundle( - name = "cli_rollup", - args = [ - "--plugin", - "rollup-plugin-hashbang", - ], - entry_point = ":cli.ts", - format = "cjs", - silent = True, - sourcemap = "false", - deps = [ - ":cli", - # TODO(josephperrott): Determine if this plugin is the best method for ensuring the hashbang - # in both local and published use case. - "@npm//rollup-plugin-hashbang", - ], -) - -generated_file_test( - name = "local_ng_dev", - src = "ng-dev.js", - generated = "cli_rollup", -) - genrule( name = "package-json", srcs = [ @@ -93,3 +69,34 @@ pkg_npm( "//dev-infra/ts-circular-dependencies", ], ) + +# Because the angular/angular repository relies on the local repository for running ng-dev commands, +# the rollup generated javascript files are committed into the repository to be used as node +# scripts. To ensure they stay up to date, they are created using a generated file test. +# +# Currently there are two generated files which are needed +# ng-dev.js - The main script representing ng-dev +# build-worker.js - The worker script for the `ng-dev release build` command, allowing it to run +# in a forked process. +ng_dev_rolled_up_generated_file( + name = "ng-dev", + entry_point = ":cli.ts", + rollup_args = [ + "--plugin", + "rollup-plugin-hashbang", + ], + deps = [ + ":cli", + # TODO(josephperrott): Determine if this plugin is the best method for ensuring the hashbang + # in both local and published use case. + "@npm//rollup-plugin-hashbang", + ], +) + +ng_dev_rolled_up_generated_file( + name = "build-worker", + entry_point = "//dev-infra/release/build:build-worker.ts", + deps = [ + "//dev-infra/release/build", + ], +) diff --git a/dev-infra/build-worker.js b/dev-infra/build-worker.js new file mode 100644 index 0000000000..df9afa07ab --- /dev/null +++ b/dev-infra/build-worker.js @@ -0,0 +1,316 @@ +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var tslib = require('tslib'); +var fs = require('fs'); +var path = require('path'); +var chalk = _interopDefault(require('chalk')); +require('inquirer'); +require('inquirer-autocomplete-prompt'); +var shelljs = require('shelljs'); + +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/** Reexport of chalk colors for convenient access. */ +var red = chalk.red; +var green = chalk.green; +var yellow = chalk.yellow; +var bold = chalk.bold; +var blue = chalk.blue; +/** + * Supported levels for logging functions. + * + * Levels are mapped to numbers to represent a hierarchy of logging levels. + */ +var LOG_LEVELS; +(function (LOG_LEVELS) { + LOG_LEVELS[LOG_LEVELS["SILENT"] = 0] = "SILENT"; + LOG_LEVELS[LOG_LEVELS["ERROR"] = 1] = "ERROR"; + LOG_LEVELS[LOG_LEVELS["WARN"] = 2] = "WARN"; + LOG_LEVELS[LOG_LEVELS["LOG"] = 3] = "LOG"; + LOG_LEVELS[LOG_LEVELS["INFO"] = 4] = "INFO"; + LOG_LEVELS[LOG_LEVELS["DEBUG"] = 5] = "DEBUG"; +})(LOG_LEVELS || (LOG_LEVELS = {})); +/** Default log level for the tool. */ +var DEFAULT_LOG_LEVEL = LOG_LEVELS.INFO; +/** Write to the console for at INFO logging level */ +var info = buildLogLevelFunction(function () { return console.info; }, LOG_LEVELS.INFO); +/** Write to the console for at ERROR logging level */ +var error = buildLogLevelFunction(function () { return console.error; }, LOG_LEVELS.ERROR); +/** Write to the console for at DEBUG logging level */ +var debug = buildLogLevelFunction(function () { return console.debug; }, LOG_LEVELS.DEBUG); +/** Write to the console for at LOG logging level */ +// tslint:disable-next-line: no-console +var log = buildLogLevelFunction(function () { return console.log; }, LOG_LEVELS.LOG); +/** Write to the console for at WARN logging level */ +var warn = buildLogLevelFunction(function () { return console.warn; }, LOG_LEVELS.WARN); +/** Build an instance of a logging function for the provided level. */ +function buildLogLevelFunction(loadCommand, level) { + /** Write to stdout for the LOG_LEVEL. */ + var loggingFunction = function () { + var text = []; + for (var _i = 0; _i < arguments.length; _i++) { + text[_i] = arguments[_i]; + } + runConsoleCommand.apply(void 0, tslib.__spread([loadCommand, level], text)); + }; + /** Start a group at the LOG_LEVEL, optionally starting it as collapsed. */ + loggingFunction.group = function (text, collapsed) { + if (collapsed === void 0) { collapsed = false; } + var command = collapsed ? console.groupCollapsed : console.group; + runConsoleCommand(function () { return command; }, level, text); + }; + /** End the group at the LOG_LEVEL. */ + loggingFunction.groupEnd = function () { + runConsoleCommand(function () { return console.groupEnd; }, level); + }; + return loggingFunction; +} +/** + * Run the console command provided, if the environments logging level greater than the + * provided logging level. + * + * The loadCommand takes in a function which is called to retrieve the console.* function + * to allow for jasmine spies to still work in testing. Without this method of retrieval + * the console.* function, the function is saved into the closure of the created logging + * function before jasmine can spy. + */ +function runConsoleCommand(loadCommand, logLevel) { + var text = []; + for (var _i = 2; _i < arguments.length; _i++) { + text[_i - 2] = arguments[_i]; + } + if (getLogLevel() >= logLevel) { + loadCommand().apply(void 0, tslib.__spread(text)); + } + printToLogFile.apply(void 0, tslib.__spread([logLevel], text)); +} +/** + * Retrieve the log level from environment variables, if the value found + * based on the LOG_LEVEL environment variable is undefined, return the default + * logging level. + */ +function getLogLevel() { + var logLevelEnvValue = (process.env["LOG_LEVEL"] || '').toUpperCase(); + var logLevel = LOG_LEVELS[logLevelEnvValue]; + if (logLevel === undefined) { + return DEFAULT_LOG_LEVEL; + } + return logLevel; +} +/** + * The number of columns used in the prepended log level information on each line of the logging + * output file. + */ +var LOG_LEVEL_COLUMNS = 7; +/** Write the provided text to the log file, prepending each line with the log level. */ +function printToLogFile(logLevel) { + var text = []; + for (var _i = 1; _i < arguments.length; _i++) { + text[_i - 1] = arguments[_i]; + } + var logLevelText = (LOG_LEVELS[logLevel] + ":").padEnd(LOG_LEVEL_COLUMNS); +} + +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/** + * Runs an given command as child process. By default, child process + * output will not be printed. + */ +function exec(cmd, opts) { + return shelljs.exec(cmd, tslib.__assign(tslib.__assign({ silent: true }, opts), { async: false })); +} + +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/** Whether ts-node has been installed and is available to ng-dev. */ +function isTsNodeAvailable() { + try { + require.resolve('ts-node'); + return true; + } + catch (_a) { + return false; + } +} + +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/** + * The filename expected for creating the ng-dev config, without the file + * extension to allow either a typescript or javascript file to be used. + */ +var CONFIG_FILE_PATH = '.ng-dev/config'; +/** The configuration for ng-dev. */ +var cachedConfig = null; +/** + * Get the configuration from the file system, returning the already loaded + * copy if it is defined. + */ +function getConfig() { + // If the global config is not defined, load it from the file system. + if (cachedConfig === null) { + // The full path to the configuration file. + var configPath = path.join(getRepoBaseDir(), CONFIG_FILE_PATH); + // Read the configuration and validate it before caching it for the future. + cachedConfig = validateCommonConfig(readConfigFile(configPath)); + } + // Return a clone of the cached global config to ensure that a new instance of the config + // is returned each time, preventing unexpected effects of modifications to the config object. + return tslib.__assign({}, cachedConfig); +} +/** Validate the common configuration has been met for the ng-dev command. */ +function validateCommonConfig(config) { + var errors = []; + // Validate the github configuration. + if (config.github === undefined) { + errors.push("Github repository not configured. Set the \"github\" option."); + } + else { + if (config.github.name === undefined) { + errors.push("\"github.name\" is not defined"); + } + if (config.github.owner === undefined) { + errors.push("\"github.owner\" is not defined"); + } + } + assertNoErrors(errors); + return config; +} +/** + * Resolves and reads the specified configuration file, optionally returning an empty object if the + * configuration file cannot be read. + */ +function readConfigFile(configPath, returnEmptyObjectOnError) { + if (returnEmptyObjectOnError === void 0) { returnEmptyObjectOnError = false; } + // If the the `.ts` extension has not been set up already, and a TypeScript based + // version of the given configuration seems to exist, set up `ts-node` if available. + if (require.extensions['.ts'] === undefined && fs.existsSync(configPath + ".ts") && + isTsNodeAvailable()) { + // Ensure the module target is set to `commonjs`. This is necessary because the + // dev-infra tool runs in NodeJS which does not support ES modules by default. + // Additionally, set the `dir` option to the directory that contains the configuration + // file. This allows for custom compiler options (such as `--strict`). + require('ts-node').register({ dir: path.dirname(configPath), transpileOnly: true, compilerOptions: { module: 'commonjs' } }); + } + try { + return require(configPath); + } + catch (e) { + if (returnEmptyObjectOnError) { + debug("Could not read configuration file at " + configPath + ", returning empty object instead."); + debug(e); + return {}; + } + error("Could not read configuration file at " + configPath + "."); + error(e); + process.exit(1); + } +} +/** + * Asserts the provided array of error messages is empty. If any errors are in the array, + * logs the errors and exit the process as a failure. + */ +function assertNoErrors(errors) { + var e_1, _a; + if (errors.length == 0) { + return; + } + error("Errors discovered while loading configuration file:"); + try { + for (var errors_1 = tslib.__values(errors), errors_1_1 = errors_1.next(); !errors_1_1.done; errors_1_1 = errors_1.next()) { + var err = errors_1_1.value; + error(" - " + err); + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (errors_1_1 && !errors_1_1.done && (_a = errors_1.return)) _a.call(errors_1); + } + finally { if (e_1) throw e_1.error; } + } + process.exit(1); +} +/** Gets the path of the directory for the repository base. */ +function getRepoBaseDir() { + var baseRepoDir = exec("git rev-parse --show-toplevel"); + if (baseRepoDir.code) { + throw Error("Unable to find the path to the base directory of the repository.\n" + + "Was the command run from inside of the repo?\n\n" + + ("ERROR:\n " + baseRepoDir.stderr)); + } + return baseRepoDir.trim(); +} + +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/** Retrieve and validate the config as `ReleaseConfig`. */ +function getReleaseConfig(config = getConfig()) { + var _a, _b, _c; + // List of errors encountered validating the config. + const errors = []; + if (config.release === undefined) { + errors.push(`No configuration defined for "release"`); + } + if (((_a = config.release) === null || _a === void 0 ? void 0 : _a.npmPackages) === undefined) { + errors.push(`No "npmPackages" configured for releasing.`); + } + if (((_b = config.release) === null || _b === void 0 ? void 0 : _b.buildPackages) === undefined) { + errors.push(`No "buildPackages" function configured for releasing.`); + } + if (((_c = config.release) === null || _c === void 0 ? void 0 : _c.generateReleaseNotesForHead) === undefined) { + errors.push(`No "generateReleaseNotesForHead" function configured for releasing.`); + } + assertNoErrors(errors); + return config.release; +} + +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// Start the release package building. +main(); +/** Main function for building the release packages. */ +function main() { + return tslib.__awaiter(this, void 0, void 0, function* () { + if (process.send === undefined) { + throw Error('This script needs to be invoked as a NodeJS worker.'); + } + const config = getReleaseConfig(); + const builtPackages = yield config.buildPackages(); + // Transfer the built packages back to the parent process. + process.send(builtPackages); + }); +} diff --git a/dev-infra/index.bzl b/dev-infra/index.bzl index cd495e8552..34867f0994 100644 --- a/dev-infra/index.bzl +++ b/dev-infra/index.bzl @@ -3,7 +3,32 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.io/license -# File is currently empty but serves as indicator for `rules_nodejs` and instructs it to -# preserve the content output in the NPM install workspace. This allows consumers to use -# rules and targets from within Bazel. e.g. by using `@npm//@angular/dev-infra-private/<..>`. +load("@build_bazel_rules_nodejs//:index.bzl", "generated_file_test") +load("@npm//@bazel/rollup:index.bzl", "rollup_bundle") + +# This file continues to serve as indicator for `rules_nodejs` and instructs it preserve the +# content output in the NPM install workspace. This allows consumers to use rules and targets from +# within Bazel. e.g. by using `@npm//@angular/dev-infra-private/<..>`. # See: https://github.com/bazelbuild/rules_nodejs/commit/4f508b1a0be1f5444e9c13b0439e649449792fef. + +def ng_dev_rolled_up_generated_file(name, entry_point, deps = [], rollup_args = []): + """Rollup and generated file test macro. + + This provides a single macro to create a rollup bundled script and a generated file test for the + created script to ensure it stays up to date in the repository. + """ + rollup_bundle( + name = "%s_bundle" % name, + args = rollup_args, + entry_point = entry_point, + format = "cjs", + silent = True, + sourcemap = "false", + deps = deps, + ) + + generated_file_test( + name = name, + src = "%s.js" % name, + generated = "%s_bundle" % name, + ) diff --git a/dev-infra/release/build/BUILD.bazel b/dev-infra/release/build/BUILD.bazel index 679c0f2c1e..75136639ab 100644 --- a/dev-infra/release/build/BUILD.bazel +++ b/dev-infra/release/build/BUILD.bazel @@ -1,6 +1,10 @@ load("@npm//@bazel/typescript:index.bzl", "ts_library") load("//tools:defaults.bzl", "jasmine_node_test") +exports_files([ + "build-worker.ts", +]) + ts_library( name = "build", srcs = glob(