/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/angular/cli/src/command-builder/schematics-command-module.ts
429 строк
14 KB
Alan Agius
feat(@angular/cli): integrate file formatting into update migrations
28 янв 2026, 18:01
28 янв 2026, 18:01
91b9d28
Код
Авторство
О чём код?
/** * @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.dev/license */ import { JsonValue, normalize as devkitNormalize, schema } from '@angular-devkit/core'; import { Collection, UnsuccessfulWorkflowExecution, formats } from '@angular-devkit/schematics'; import { FileSystemCollectionDescription, FileSystemSchematicDescription, NodeWorkflow, } from '@angular-devkit/schematics/tools'; import { relative } from 'node:path'; import { Argv } from 'yargs'; import { isPackageNameSafeForAnalytics } from '../analytics/analytics'; import { EventCustomDimension } from '../analytics/analytics-parameters'; import { getProjectByCwd, getSchematicDefaults } from '../utilities/config'; import { assertIsError } from '../utilities/error'; import { memoize } from '../utilities/memoize'; import { formatFiles } from '../utilities/prettier'; import { isTTY } from '../utilities/tty'; import { CommandModule, CommandModuleImplementation, CommandScope, Options, OtherOptions, } from './command-module'; import { Option, parseJsonSchemaToOptions } from './utilities/json-schema'; import { SchematicEngineHost } from './utilities/schematic-engine-host'; import { subscribeToWorkflow } from './utilities/schematic-workflow'; export const DEFAULT_SCHEMATICS_COLLECTION = '@schematics/angular'; export interface SchematicsCommandArgs { interactive: boolean; force: boolean; 'dry-run': boolean; defaults: boolean; } export interface SchematicsExecutionOptions extends Options<SchematicsCommandArgs> { packageRegistry?: string; } export abstract class SchematicsCommandModule extends CommandModule<SchematicsCommandArgs> implements CommandModuleImplementation<SchematicsCommandArgs> { override scope = CommandScope.In; protected readonly allowPrivateSchematics: boolean = false; async builder(argv: Argv): Promise<Argv<SchematicsCommandArgs>> { return argv .option('interactive', { describe: 'Enable interactive input prompts.', type: 'boolean', default: true, }) .option('dry-run', { describe: 'Run through and reports activity without writing out results.', type: 'boolean', alias: ['d'], default: false, }) .option('defaults', { describe: 'Disable interactive input prompts for options with a default.', type: 'boolean', default: false, }) .option('force', { describe: 'Force overwriting of existing files.', type: 'boolean', default: false, }) .strict(); } /** Get schematic schema options.*/ protected async getSchematicOptions( collection: Collection<FileSystemCollectionDescription, FileSystemSchematicDescription>, schematicName: string, workflow: NodeWorkflow, ): Promise<Option[]> { const schematic = collection.createSchematic(schematicName, true); const { schemaJson } = schematic.description; if (!schemaJson) { return []; } return parseJsonSchemaToOptions(workflow.registry, schemaJson); } @memoize protected getOrCreateWorkflowForBuilder(collectionName: string): NodeWorkflow { return new NodeWorkflow(this.context.root, { resolvePaths: this.getResolvePaths(collectionName), engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), }); } @memoize protected async getOrCreateWorkflowForExecution( collectionName: string, options: SchematicsExecutionOptions, ): Promise<NodeWorkflow> { const { logger, root, packageManager } = this.context; const { force, dryRun, packageRegistry } = options; const workflow = new NodeWorkflow(root, { force, dryRun, packageManager: packageManager.name, // A schema registry is required to allow customizing addUndefinedDefaults registry: new schema.CoreSchemaRegistry(formats.standardFormats), packageRegistry, resolvePaths: this.getResolvePaths(collectionName), schemaValidation: true, optionTransforms: [ // Add configuration file defaults async (schematic, current) => { const projectName = typeof current?.project === 'string' ? current.project : this.getProjectName(); return { ...(await getSchematicDefaults(schematic.collection.name, schematic.name, projectName)), ...current, }; }, ], engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), }); workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults); workflow.registry.useXDeprecatedProvider((msg) => logger.warn(msg)); workflow.registry.addSmartDefaultProvider('projectName', () => this.getProjectName()); const workingDir = devkitNormalize(relative(this.context.root, process.cwd())); workflow.registry.addSmartDefaultProvider('workingDirectory', () => workingDir === '' ? undefined : workingDir, ); workflow.engineHost.registerOptionsTransform(async (schematic, options) => { const { collection: { name: collectionName }, name: schematicName, } = schematic; const analytics = isPackageNameSafeForAnalytics(collectionName) ? await this.getAnalytics() : undefined; analytics?.reportSchematicRunEvent({ [EventCustomDimension.SchematicCollectionName]: collectionName, [EventCustomDimension.SchematicName]: schematicName, ...this.getAnalyticsParameters(options as unknown as {}), }); return options; }); if (options.interactive !== false && isTTY()) { workflow.registry.usePromptProvider(async (definitions: Array<schema.PromptDefinition>) => { let prompts: typeof import('@inquirer/prompts') | undefined; const answers: Record<string, JsonValue> = {}; for (const definition of definitions) { if (options.defaults && definition.default !== undefined) { continue; } // Only load prompt package if needed prompts ??= await import('@inquirer/prompts'); switch (definition.type) { case 'confirmation': answers[definition.id] = await prompts.confirm({ message: definition.message, default: definition.default as boolean | undefined, }); break; case 'list': if (!definition.items?.length) { continue; } answers[definition.id] = await ( definition.multiselect ? prompts.checkbox : prompts.select )({ message: definition.message, validate: (values) => { if (!definition.validator) { return true; } return definition.validator(Object.values(values).map(({ value }) => value)); }, default: definition.multiselect ? undefined : definition.default, choices: definition.items?.map((item) => typeof item == 'string' ? { name: item, value: item, checked: definition.multiselect && Array.isArray(definition.default) ? definition.default?.includes(item) : item === definition.default, } : { ...item, name: item.label, value: item.value, checked: definition.multiselect && Array.isArray(definition.default) ? // eslint-disable-next-line @typescript-eslint/no-explicit-any definition.default?.includes(item.value as any) : item.value === definition.default, }, ), }); break; case 'input': { let finalValue: JsonValue | undefined; answers[definition.id] = await prompts.input({ message: definition.message, default: definition.default as string | undefined, async validate(value) { if (definition.validator === undefined) { return true; } let lastValidation: ReturnType<typeof definition.validator> = false; for (const type of definition.propertyTypes) { let potential; switch (type) { case 'string': potential = String(value); break; case 'integer': case 'number': potential = Number(value); break; default: potential = value; break; } lastValidation = await definition.validator(potential); // Can be a string if validation fails if (lastValidation === true) { finalValue = potential; return true; } } return lastValidation; }, }); // Use validated value if present. // This ensures the correct type is inserted into the final schema options. if (finalValue !== undefined) { answers[definition.id] = finalValue; } break; } } } return answers; }); } return workflow; } @memoize protected async getSchematicCollections(): Promise<Set<string>> { const getSchematicCollections = ( configSection: Record<string, unknown> | undefined, ): Set<string> | undefined => { if (!configSection) { return undefined; } const { schematicCollections } = configSection; if (Array.isArray(schematicCollections)) { return new Set(schematicCollections); } return undefined; }; const { workspace, globalConfiguration } = this.context; if (workspace) { const project = getProjectByCwd(workspace); if (project) { const value = getSchematicCollections(workspace.getProjectCli(project)); if (value) { return value; } } } const value = getSchematicCollections(workspace?.getCli()) ?? getSchematicCollections(globalConfiguration.getCli()); if (value) { return value; } return new Set([DEFAULT_SCHEMATICS_COLLECTION]); } protected parseSchematicInfo( schematic: string | undefined, ): [collectionName: string | undefined, schematicName: string | undefined] { if (schematic?.includes(':')) { const [collectionName, schematicName] = schematic.split(':', 2); return [collectionName, schematicName]; } return [undefined, schematic]; } protected async runSchematic(options: { executionOptions: SchematicsExecutionOptions; schematicOptions: OtherOptions; collectionName: string; schematicName: string; }): Promise<number> { const { logger } = this.context; const { schematicOptions, executionOptions, collectionName, schematicName } = options; const workflow = await this.getOrCreateWorkflowForExecution(collectionName, executionOptions); if (!schematicName) { throw new Error('schematicName cannot be undefined.'); } const { unsubscribe, files } = subscribeToWorkflow(workflow, logger); try { await workflow .execute({ collection: collectionName, schematic: schematicName, options: schematicOptions, logger, allowPrivate: this.allowPrivateSchematics, }) .toPromise(); if (!files.size) { logger.info('Nothing to be done.'); } if (executionOptions.dryRun) { logger.warn(`\nNOTE: The "--dry-run" option means no changes were made.`); return 0; } if (files.size) { // Note: we could use a task executor to format the files but this is simpler. try { await formatFiles(this.context.root, files); } catch (error) { assertIsError(error); logger.warn( `WARNING: Formatting of files failed with the following error: ${error.message}`, ); } } return 0; } catch (err) { // In case the workflow was not successful, show an appropriate error message. if (err instanceof UnsuccessfulWorkflowExecution) { // "See above" because we already printed the error. logger.fatal('The Schematic workflow failed. See above.'); } else { assertIsError(err); logger.fatal(err.message); } return 1; } finally { unsubscribe(); } } private getProjectName(): string | undefined { const { workspace } = this.context; if (!workspace) { return undefined; } const projectName = getProjectByCwd(workspace); if (projectName) { return projectName; } return undefined; } private getResolvePaths(collectionName: string): string[] { const { workspace, root } = this.context; if (collectionName[0] === '.') { // Resolve relative collections from the location of `angular.json` return [root]; } return workspace ? // Workspace collectionName === DEFAULT_SCHEMATICS_COLLECTION ? // Favor __dirname for @schematics/angular to use the build-in version [__dirname, process.cwd(), root] : [process.cwd(), root, __dirname] : // Global [__dirname, process.cwd()]; } }