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/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), + ); + } } } 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": "./" + } + } +}