Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClassFieldUniqueKey, OutputMigrationData>;
problematicUsages: Record<ClassFieldUniqueKey, true>;
importReplacements: Record<ProjectFileID, {add: Replacement[]; addAndRemove: Replacement[]}>;
Expand All @@ -68,6 +83,10 @@ export class OutputMigration extends TsurgeFunnelMigration<
CompilationUnitData,
CompilationUnitData
> {
constructor(private readonly config: MigrationConfig = {}) {
super();
}

override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
const {sourceFiles, program} = info;
const outputFieldReplacements: Record<ClassFieldUniqueKey, OutputMigrationData> = {};
Expand Down Expand Up @@ -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),
);
}
}
}

Expand Down
30 changes: 30 additions & 0 deletions packages/core/schematics/ng-generate/output-migration/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
108 changes: 108 additions & 0 deletions packages/core/schematics/ng-generate/output-migration/index.ts
Original file line number Diff line number Diff line change
@@ -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<ProjectRootRelativePath, TextUpdate[]> = 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 🎉`);
};
}
19 changes: 19 additions & 0 deletions packages/core/schematics/ng-generate/output-migration/schema.json
Original file line number Diff line number Diff line change
@@ -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": "./"
}
}
}