From 1673a9b781e3e904149f99f2ca4ad8a099b76eba Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Tue, 22 Oct 2024 12:12:17 +0200 Subject: [PATCH 1/2] feat(core): allow running output migration on a subset of paths This change introduces a new configuration parameter to the output as function migration - it is now possible to restrict a set of migrated paths. --- .../output-migration/output-migration.ts | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/core/schematics/migrations/output-migration/output-migration.ts b/packages/core/schematics/migrations/output-migration/output-migration.ts index e2d9171ac0a9..18c8bf21dea7 100644 --- a/packages/core/schematics/migrations/output-migration/output-migration.ts +++ b/packages/core/schematics/migrations/output-migration/output-migration.ts @@ -53,12 +53,27 @@ import { import {ReferenceResult} from '../signal-migration/src/passes/reference_resolution/reference_result'; import {ReferenceKind} from '../signal-migration/src/passes/reference_resolution/reference_kinds'; -interface OutputMigrationData { +export interface MigrationConfig { + /** + * Whether the given output definition should be migrated. + * + * Treating an output as non-migrated means that no references to it are + * migrated, nor the actual declaration (if it's part of the sources). + * + * If no function is specified here, the migration will migrate all + * output and references it discovers in compilation units. This is the + * running assumption for batch mode and LSC mode where the migration + * assumes all seen output are migrated. + */ + shouldMigrate?: (definition: ClassFieldDescriptor, containingFile: ProjectFile) => boolean; +} + +export interface OutputMigrationData { file: ProjectFile; replacements: Replacement[]; } -interface CompilationUnitData { +export interface CompilationUnitData { outputFields: Record; problematicUsages: Record; importReplacements: Record; @@ -68,6 +83,10 @@ export class OutputMigration extends TsurgeFunnelMigration< CompilationUnitData, CompilationUnitData > { + constructor(private readonly config: MigrationConfig = {}) { + super(); + } + override async analyze(info: ProgramInfo): Promise> { const {sourceFiles, program} = info; const outputFieldReplacements: Record = {}; @@ -111,14 +130,24 @@ export class OutputMigration extends TsurgeFunnelMigration< const outputDef = extractSourceOutputDefinition(node, reflector, info); if (outputDef !== null) { const outputFile = projectFile(node.getSourceFile(), info); - - filesWithOutputDeclarations.add(node.getSourceFile()); - addOutputReplacement( - outputFieldReplacements, - outputDef.id, - outputFile, - calculateDeclarationReplacement(info, node, outputDef.aliasParam), - ); + if ( + this.config.shouldMigrate === undefined || + this.config.shouldMigrate( + { + key: outputDef.id, + node: node, + }, + outputFile, + ) + ) { + filesWithOutputDeclarations.add(node.getSourceFile()); + addOutputReplacement( + outputFieldReplacements, + outputDef.id, + outputFile, + calculateDeclarationReplacement(info, node, outputDef.aliasParam), + ); + } } } From a662b22a5fbca46959b45e9cb642aecbdd417884 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Tue, 22 Oct 2024 12:13:38 +0200 Subject: [PATCH 2/2] feat(migrations): expose output as function migration This commit exposes a new ng generate schematic that migrates outputs from the decorator version to the output function. --- .../migrations/output-migration/BUILD.bazel | 4 + .../ng-generate/output-migration/BUILD.bazel | 30 +++++ .../ng-generate/output-migration/index.ts | 108 ++++++++++++++++++ .../ng-generate/output-migration/schema.json | 19 +++ 4 files changed, 161 insertions(+) create mode 100644 packages/core/schematics/ng-generate/output-migration/BUILD.bazel create mode 100644 packages/core/schematics/ng-generate/output-migration/index.ts create mode 100644 packages/core/schematics/ng-generate/output-migration/schema.json diff --git a/packages/core/schematics/migrations/output-migration/BUILD.bazel b/packages/core/schematics/migrations/output-migration/BUILD.bazel index 2cc8519560c4..05e84ed6fd2f 100644 --- a/packages/core/schematics/migrations/output-migration/BUILD.bazel +++ b/packages/core/schematics/migrations/output-migration/BUILD.bazel @@ -6,6 +6,10 @@ ts_library( ["**/*.ts"], exclude = ["*.spec.ts"], ), + visibility = [ + "//packages/core/schematics/ng-generate/output-migration:__pkg__", + "//packages/language-service/src/refactorings:__pkg__", + ], deps = [ "//packages/compiler", "//packages/compiler-cli", diff --git a/packages/core/schematics/ng-generate/output-migration/BUILD.bazel b/packages/core/schematics/ng-generate/output-migration/BUILD.bazel new file mode 100644 index 000000000000..8c4f80d96d2d --- /dev/null +++ b/packages/core/schematics/ng-generate/output-migration/BUILD.bazel @@ -0,0 +1,30 @@ +load("//tools:defaults.bzl", "ts_library") + +package( + default_visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/migrations/google3:__pkg__", + "//packages/core/schematics/ng-generate/signals:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], +) + +filegroup( + name = "static_files", + srcs = ["schema.json"], +) + +ts_library( + name = "output-migration", + srcs = glob(["**/*.ts"]), + tsconfig = "//packages/core/schematics:tsconfig.json", + deps = [ + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/core/schematics/migrations/output-migration:migration", + "//packages/core/schematics/utils", + "//packages/core/schematics/utils/tsurge", + "//packages/core/schematics/utils/tsurge/helpers/angular_devkit", + "@npm//@angular-devkit/schematics", + "@npm//@types/node", + ], +) diff --git a/packages/core/schematics/ng-generate/output-migration/index.ts b/packages/core/schematics/ng-generate/output-migration/index.ts new file mode 100644 index 000000000000..ece835a9b94a --- /dev/null +++ b/packages/core/schematics/ng-generate/output-migration/index.ts @@ -0,0 +1,108 @@ +/** + * @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 {Rule, SchematicsException} from '@angular-devkit/schematics'; + +import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; +import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem'; +import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements'; +import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; +import { + CompilationUnitData, + OutputMigration, +} from '../../migrations/output-migration/output-migration'; +import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge'; + +interface Options { + path: string; + analysisDir: string; +} + +export function migrate(options: Options): Rule { + return async (tree, context) => { + const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); + + if (!buildPaths.length && !testPaths.length) { + throw new SchematicsException( + 'Could not find any tsconfig file. Cannot run output migration.', + ); + } + + const fs = new DevkitMigrationFilesystem(tree); + setFileSystem(fs); + + const migration = new OutputMigration({ + shouldMigrate: (_, file) => { + return ( + file.rootRelativePath.startsWith(fs.normalize(options.path)) && + !/(^|\/)node_modules\//.test(file.rootRelativePath) + ); + }, + }); + + const analysisPath = fs.resolve(options.analysisDir); + const unitResults: CompilationUnitData[] = []; + const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => { + context.logger.info(`Preparing analysis for: ${tsconfigPath}..`); + + const baseInfo = migration.createProgram(tsconfigPath, fs); + const info = migration.prepareProgram(baseInfo); + + // Support restricting the analysis to subfolders for larger projects. + if (analysisPath !== '/') { + info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath)); + info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => + sf.fileName.startsWith(analysisPath), + ); + } + + return {info, tsconfigPath}; + }); + + // Analyze phase. Treat all projects as compilation units as + // this allows us to support references between those. + for (const {info, tsconfigPath} of programInfos) { + context.logger.info(`Scanning for outputs: ${tsconfigPath}..`); + unitResults.push(await migration.analyze(info)); + } + + context.logger.info(``); + context.logger.info(`Processing analysis data between targets..`); + context.logger.info(``); + + const merged = await migration.merge(unitResults); + const replacementsPerFile: Map = new Map(); + + for (const {info, tsconfigPath} of programInfos) { + context.logger.info(`Migrating: ${tsconfigPath}..`); + + const {replacements} = await migration.migrate(merged); + const changesPerFile = groupReplacementsByFile(replacements); + + for (const [file, changes] of changesPerFile) { + if (!replacementsPerFile.has(file)) { + replacementsPerFile.set(file, changes); + } + } + } + + context.logger.info(`Applying changes..`); + for (const [file, changes] of replacementsPerFile) { + const recorder = tree.beginUpdate(file); + for (const c of changes) { + recorder + .remove(c.data.position, c.data.end - c.data.position) + .insertLeft(c.data.position, c.data.toInsert); + } + tree.commitUpdate(recorder); + } + + context.logger.info(''); + context.logger.info(`Successfully migrated to outputs as functions 🎉`); + }; +} diff --git a/packages/core/schematics/ng-generate/output-migration/schema.json b/packages/core/schematics/ng-generate/output-migration/schema.json new file mode 100644 index 000000000000..1ed36d02b1cf --- /dev/null +++ b/packages/core/schematics/ng-generate/output-migration/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "AngularOutputMigration", + "title": "Angular Output migration", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the directory where all outputs should be migrated.", + "x-prompt": "Which directory do you want to migrate?", + "default": "./" + }, + "analysisDir": { + "type": "string", + "description": "Path to the directory that should be analyzed. References to migrated outputs are migrated based on this folder. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.", + "default": "./" + } + } +}