#!/usr/bin/env node "use strict"; /** * @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 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.loadEsmModule = exports.main = void 0; // symbol polyfill must go first require("symbol-observable"); const node_1 = require("@angular-devkit/core/node"); const schematics_1 = require("@angular-devkit/schematics"); const tools_1 = require("@angular-devkit/schematics/tools"); const ansi_colors_1 = __importDefault(require("ansi-colors")); const node_fs_1 = require("node:fs"); const path = __importStar(require("node:path")); const yargs_parser_1 = __importStar(require("yargs-parser")); /** * Parse the name of schematic passed in argument, and return a {collection, schematic} named * tuple. The user can pass in `collection-name:schematic-name`, and this function will either * return `{collection: 'collection-name', schematic: 'schematic-name'}`, or it will error out * and show usage. * * In the case where a collection name isn't part of the argument, the default is to use the * schematics package (@angular-devkit/schematics-cli) as the collection. * * This logic is entirely up to the tooling. * * @param str The argument to parse. * @return {{collection: string, schematic: (string)}} */ function parseSchematicName(str) { let collection = '@angular-devkit/schematics-cli'; let schematic = str; if (schematic?.includes(':')) { const lastIndexOfColon = schematic.lastIndexOf(':'); [collection, schematic] = [ schematic.slice(0, lastIndexOfColon), schematic.substring(lastIndexOfColon + 1), ]; } return { collection, schematic }; } function _listSchematics(workflow, collectionName, logger) { try { const collection = workflow.engine.createCollection(collectionName); logger.info(collection.listSchematicNames().join('\n')); } catch (error) { logger.fatal(error instanceof Error ? error.message : `${error}`); return 1; } return 0; } function _createPromptProvider() { return async (definitions) => { const questions = definitions.map((definition) => { const question = { name: definition.id, message: definition.message, default: definition.default, }; const validator = definition.validator; if (validator) { question.validate = (input) => validator(input); // Filter allows transformation of the value prior to validation question.filter = async (input) => { for (const type of definition.propertyTypes) { let value; switch (type) { case 'string': value = String(input); break; case 'integer': case 'number': value = Number(input); break; default: value = input; break; } // Can be a string if validation fails const isValid = (await validator(value)) === true; if (isValid) { return value; } } return input; }; } switch (definition.type) { case 'confirmation': return { ...question, type: 'confirm' }; case 'list': return { ...question, type: definition.multiselect ? 'checkbox' : 'list', choices: definition.items && definition.items.map((item) => { if (typeof item == 'string') { return item; } else { return { name: item.label, value: item.value, }; } }), }; default: return { ...question, type: definition.type }; } }); const { default: inquirer } = await loadEsmModule('inquirer'); return inquirer.prompt(questions); }; } function findUp(names, from) { if (!Array.isArray(names)) { names = [names]; } const root = path.parse(from).root; let currentDir = from; while (currentDir && currentDir !== root) { for (const name of names) { const p = path.join(currentDir, name); if ((0, node_fs_1.existsSync)(p)) { return p; } } currentDir = path.dirname(currentDir); } return null; } /** * return package manager' name by lock file */ function getPackageManagerName() { // order by check priority const LOCKS = { 'package-lock.json': 'npm', 'yarn.lock': 'yarn', 'pnpm-lock.yaml': 'pnpm', }; const lockPath = findUp(Object.keys(LOCKS), process.cwd()); if (lockPath) { return LOCKS[path.basename(lockPath)]; } return 'npm'; } // eslint-disable-next-line max-lines-per-function async function main({ args, stdout = process.stdout, stderr = process.stderr, }) { const { cliOptions, schematicOptions, _ } = parseArgs(args); // Create a separate instance to prevent unintended global changes to the color configuration const colors = ansi_colors_1.default.create(); /** Create the DevKit Logger used through the CLI. */ const logger = (0, node_1.createConsoleLogger)(!!cliOptions.verbose, stdout, stderr, { info: (s) => s, debug: (s) => s, warn: (s) => colors.bold.yellow(s), error: (s) => colors.bold.red(s), fatal: (s) => colors.bold.red(s), }); if (cliOptions.help) { logger.info(getUsage()); return 0; } /** Get the collection an schematic name from the first argument. */ const { collection: collectionName, schematic: schematicName } = parseSchematicName(_.shift() || null); const isLocalCollection = collectionName.startsWith('.') || collectionName.startsWith('/'); /** Gather the arguments for later use. */ const debugPresent = cliOptions.debug !== null; const debug = debugPresent ? !!cliOptions.debug : isLocalCollection; const dryRunPresent = cliOptions['dry-run'] !== null; const dryRun = dryRunPresent ? !!cliOptions['dry-run'] : debug; const force = !!cliOptions.force; const allowPrivate = !!cliOptions['allow-private']; /** Create the workflow scoped to the working directory that will be executed with this run. */ const workflow = new tools_1.NodeWorkflow(process.cwd(), { force, dryRun, resolvePaths: [process.cwd(), __dirname], schemaValidation: true, packageManager: getPackageManagerName(), }); /** If the user wants to list schematics, we simply show all the schematic names. */ if (cliOptions['list-schematics']) { return _listSchematics(workflow, collectionName, logger); } if (!schematicName) { logger.info(getUsage()); return 1; } if (debug) { logger.info(`Debug mode enabled${isLocalCollection ? ' by default for local collections' : ''}.`); } // Indicate to the user when nothing has been done. This is automatically set to off when there's // a new DryRunEvent. let nothingDone = true; // Logging queue that receives all the messages to show the users. This only get shown when no // errors happened. let loggingQueue = []; let error = false; /** * Logs out dry run events. * * All events will always be executed here, in order of discovery. That means that an error would * be shown along other events when it happens. Since errors in workflows will stop the Observable * from completing successfully, we record any events other than errors, then on completion we * show them. * * This is a simple way to only show errors when an error occur. */ workflow.reporter.subscribe((event) => { nothingDone = false; // Strip leading slash to prevent confusion. const eventPath = event.path.startsWith('/') ? event.path.slice(1) : event.path; switch (event.kind) { case 'error': error = true; const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist'; logger.error(`ERROR! ${eventPath} ${desc}.`); break; case 'update': loggingQueue.push(`${colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`); break; case 'create': loggingQueue.push(`${colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`); break; case 'delete': loggingQueue.push(`${colors.yellow('DELETE')} ${eventPath}`); break; case 'rename': const eventToPath = event.to.startsWith('/') ? event.to.slice(1) : event.to; loggingQueue.push(`${colors.blue('RENAME')} ${eventPath} => ${eventToPath}`); break; } }); /** * Listen to lifecycle events of the workflow to flush the logs between each phases. */ workflow.lifeCycle.subscribe((event) => { if (event.kind == 'workflow-end' || event.kind == 'post-tasks-start') { if (!error) { // Flush the log queue and clean the error state. loggingQueue.forEach((log) => logger.info(log)); } loggingQueue = []; error = false; } }); // Show usage of deprecated options workflow.registry.useXDeprecatedProvider((msg) => logger.warn(msg)); // Pass the rest of the arguments as the smart default "argv". Then delete it. workflow.registry.addSmartDefaultProvider('argv', (schema) => 'index' in schema ? _[Number(schema['index'])] : _); // Add prompts. if (cliOptions.interactive && isTTY()) { workflow.registry.usePromptProvider(_createPromptProvider()); } /** * Execute the workflow, which will report the dry run events, run the tasks, and complete * after all is done. * * The Observable returned will properly cancel the workflow if unsubscribed, error out if ANY * step of the workflow failed (sink or task), with details included, and will only complete * when everything is done. */ try { await workflow .execute({ collection: collectionName, schematic: schematicName, options: schematicOptions, allowPrivate: allowPrivate, debug: debug, logger: logger, }) .toPromise(); if (nothingDone) { logger.info('Nothing to be done.'); } else if (dryRun) { logger.info(`Dry run enabled${dryRunPresent ? '' : ' by default in debug mode'}. No files written to disk.`); } return 0; } catch (err) { if (err instanceof schematics_1.UnsuccessfulWorkflowExecution) { // "See above" because we already printed the error. logger.fatal('The Schematic workflow failed. See above.'); } else if (debug && err instanceof Error) { logger.fatal(`An error occured:\n${err.stack}`); } else { logger.fatal(`Error: ${err instanceof Error ? err.message : err}`); } return 1; } } exports.main = main; /** * Get usage of the CLI tool. */ function getUsage() { return ` schematics [collection-name:]schematic-name [options, ...] By default, if the collection name is not specified, use the internal collection provided by the Schematics CLI. Options: --debug Debug mode. This is true by default if the collection is a relative path (in that case, turn off with --debug=false). --allow-private Allow private schematics to be run from the command line. Default to false. --dry-run Do not output anything, but instead just show what actions would be performed. Default to true if debug is also true. --force Force overwriting files that would otherwise be an error. --list-schematics List all schematics from the collection, by name. A collection name should be suffixed by a colon. Example: '@angular-devkit/schematics-cli:'. --no-interactive Disables interactive input prompts. --verbose Show more information. --help Show this message. Any additional option is passed to the Schematics depending on its schema. `; } /** Parse the command line. */ const booleanArgs = [ 'allow-private', 'debug', 'dry-run', 'force', 'help', 'list-schematics', 'verbose', 'interactive', ]; /** Parse the command line. */ function parseArgs(args) { const { _, ...options } = (0, yargs_parser_1.default)(args, { boolean: booleanArgs, default: { 'interactive': true, 'debug': null, 'dry-run': null, }, configuration: { 'dot-notation': false, 'boolean-negation': true, 'strip-aliased': true, 'camel-case-expansion': false, }, }); // Camelize options as yargs will return the object in kebab-case when camel casing is disabled. const schematicOptions = {}; const cliOptions = {}; const isCliOptions = (key) => booleanArgs.includes(key); for (const [key, value] of Object.entries(options)) { if (/[A-Z]/.test(key)) { throw new Error(`Unknown argument ${key}. Did you mean ${(0, yargs_parser_1.decamelize)(key)}?`); } if (isCliOptions(key)) { cliOptions[key] = value; } else { schematicOptions[(0, yargs_parser_1.camelCase)(key)] = value; } } return { _: _.map((v) => v.toString()), schematicOptions, cliOptions, }; } function isTTY() { const isTruthy = (value) => { // Returns true if value is a string that is anything but 0 or false. return value !== undefined && value !== '0' && value.toUpperCase() !== 'FALSE'; }; // If we force TTY, we always return true. const force = process.env['NG_FORCE_TTY']; if (force !== undefined) { return isTruthy(force); } return !!process.stdout.isTTY && !isTruthy(process.env['CI']); } if (require.main === module) { const args = process.argv.slice(2); main({ args }) .then((exitCode) => (process.exitCode = exitCode)) .catch((e) => { throw e; }); } /** * Lazily compiled dynamic import loader function. */ let load; /** * This uses a dynamic import to load a module which may be ESM. * CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript * will currently, unconditionally downlevel dynamic import into a require call. * require calls cannot load ESM code and will result in a runtime error. To workaround * this, a Function constructor is used to prevent TypeScript from changing the dynamic import. * Once TypeScript provides support for keeping the dynamic import this workaround can * be dropped. * * @param modulePath The path of the module to load. * @returns A Promise that resolves to the dynamically imported module. */ function loadEsmModule(modulePath) { load ??= new Function('modulePath', `return import(modulePath);`); return load(modulePath); } exports.loadEsmModule = loadEsmModule;