Skip to content

Commit 9895e44

Browse files
crisbetothePunderWoman
authored andcommitted
fix(migrations): replace leftover modules with their exports during pruning (#57684)
Currently during the module pruning stage of the standalone migration we assume that any leftover modules which only have `imports` and `exports` can safely be removed. That can be incorrect for the cases where some parts of the app were converted to standalone outside of the migration. These changes update the logic so that such modules are replaced with the `exports` which are used within the specific component. Fixes #51420. PR Close #57684
1 parent 09f2ec0 commit 9895e44

File tree

4 files changed

+315
-36
lines changed

4 files changed

+315
-36
lines changed

packages/core/schematics/ng-generate/standalone-migration/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ function standaloneMigration(
132132
printer,
133133
undefined,
134134
referenceLookupExcludedFiles,
135+
knownInternalAliasRemapper,
135136
);
136137
pendingChanges = result.pendingChanges;
137138
filesToRemove = result.filesToRemove;

packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts

Lines changed: 185 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,23 @@ import {
1717
findClassDeclaration,
1818
findLiteralProperty,
1919
getNodeLookup,
20+
NamedClassDeclaration,
2021
offsetsToNodes,
2122
ReferenceResolver,
2223
UniqueItemTracker,
2324
} from './util';
25+
import {
26+
PotentialImport,
27+
PotentialImportMode,
28+
Reference,
29+
TemplateTypeChecker,
30+
} from '@angular/compiler-cli/private/migrations';
31+
import {
32+
ComponentImportsRemapper,
33+
findImportLocation,
34+
findTemplateDependencies,
35+
potentialImportsToExpressions,
36+
} from './to-standalone';
2437

2538
/** Keeps track of the places from which we need to remove AST nodes. */
2639
interface RemovalLocations {
@@ -39,11 +52,13 @@ export function pruneNgModules(
3952
printer: ts.Printer,
4053
importRemapper?: ImportRemapper,
4154
referenceLookupExcludedFiles?: RegExp,
55+
componentImportRemapper?: ComponentImportsRemapper,
4256
) {
4357
const filesToRemove = new Set<ts.SourceFile>();
4458
const tracker = new ChangeTracker(printer, importRemapper);
4559
const tsProgram = program.getTsProgram();
4660
const typeChecker = tsProgram.getTypeChecker();
61+
const templateTypeChecker = program.compiler.getTemplateTypeChecker();
4762
const referenceResolver = new ReferenceResolver(
4863
program,
4964
host,
@@ -59,11 +74,19 @@ export function pruneNgModules(
5974
};
6075
const classesToRemove = new Set<ts.ClassDeclaration>();
6176
const barrelExports = new UniqueItemTracker<ts.SourceFile, ts.ExportDeclaration>();
77+
const componentImportArrays = new UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>();
6278
const nodesToRemove = new Set<ts.Node>();
6379

6480
sourceFiles.forEach(function walk(node: ts.Node) {
6581
if (ts.isClassDeclaration(node) && canRemoveClass(node, typeChecker)) {
66-
collectRemovalLocations(node, removalLocations, referenceResolver, program);
82+
collectChangeLocations(
83+
node,
84+
removalLocations,
85+
componentImportArrays,
86+
templateTypeChecker,
87+
referenceResolver,
88+
program,
89+
);
6790
classesToRemove.add(node);
6891
} else if (
6992
ts.isExportDeclaration(node) &&
@@ -83,6 +106,15 @@ export function pruneNgModules(
83106
node.forEachChild(walk);
84107
});
85108

109+
replaceInImportsArray(
110+
componentImportArrays,
111+
classesToRemove,
112+
tracker,
113+
typeChecker,
114+
templateTypeChecker,
115+
componentImportRemapper,
116+
);
117+
86118
// We collect all the places where we need to remove references first before generating the
87119
// removal instructions since we may have to remove multiple references from one node.
88120
removeArrayReferences(removalLocations.arrays, tracker);
@@ -123,13 +155,16 @@ export function pruneNgModules(
123155
/**
124156
* Collects all the nodes that a module needs to be removed from.
125157
* @param ngModule Module being removed.
126-
* @param removalLocations
158+
* @param removalLocations Tracks the different places from which the class should be removed.
159+
* @param componentImportArrays Set of `imports` arrays of components that need to be adjusted.
127160
* @param referenceResolver
128161
* @param program
129162
*/
130-
function collectRemovalLocations(
163+
function collectChangeLocations(
131164
ngModule: ts.ClassDeclaration,
132165
removalLocations: RemovalLocations,
166+
componentImportArrays: UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>,
167+
templateTypeChecker: TemplateTypeChecker,
133168
referenceResolver: ReferenceResolver,
134169
program: NgtscProgram,
135170
) {
@@ -148,6 +183,26 @@ function collectRemovalLocations(
148183
for (const node of nodes) {
149184
const closestArray = closestNode(node, ts.isArrayLiteralExpression);
150185
if (closestArray) {
186+
const closestAssignment = closestNode(closestArray, ts.isPropertyAssignment);
187+
188+
// If the module was flagged as being removable, but it's still being used in a standalone
189+
// component's `imports` array, it means that it was likely changed outside of the migration
190+
// and deleting it now will be breaking. Track it separately so it can be handled properly.
191+
if (closestAssignment && isInImportsArray(closestAssignment, closestArray)) {
192+
const closestDecorator = closestNode(closestAssignment, ts.isDecorator);
193+
const closestClass = closestDecorator
194+
? closestNode(closestDecorator, ts.isClassDeclaration)
195+
: null;
196+
const directiveMeta = closestClass
197+
? templateTypeChecker.getDirectiveMetadata(closestClass)
198+
: null;
199+
200+
if (directiveMeta && directiveMeta.isComponent && directiveMeta.isStandalone) {
201+
componentImportArrays.track(closestArray, node);
202+
continue;
203+
}
204+
}
205+
151206
removalLocations.arrays.track(closestArray, node);
152207
continue;
153208
}
@@ -168,6 +223,117 @@ function collectRemovalLocations(
168223
}
169224
}
170225

226+
/**
227+
* Replaces all the leftover modules in imports arrays with their exports.
228+
* @param componentImportArrays All the imports arrays and their nodes that represent NgModules.
229+
* @param classesToRemove Set of classes that were marked for removal.
230+
* @param tracker
231+
* @param typeChecker
232+
* @param templateTypeChecker
233+
* @param importRemapper
234+
*/
235+
function replaceInImportsArray(
236+
componentImportArrays: UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>,
237+
classesToRemove: Set<ts.ClassDeclaration>,
238+
tracker: ChangeTracker,
239+
typeChecker: ts.TypeChecker,
240+
templateTypeChecker: TemplateTypeChecker,
241+
importRemapper?: ComponentImportsRemapper,
242+
) {
243+
for (const [array, toReplace] of componentImportArrays.getEntries()) {
244+
const closestClass = closestNode(array, ts.isClassDeclaration);
245+
246+
if (!closestClass) {
247+
continue;
248+
}
249+
250+
const replacements = new UniqueItemTracker<ts.Node, Reference<NamedClassDeclaration>>();
251+
const usedImports = new Set(
252+
findTemplateDependencies(closestClass, templateTypeChecker).map((ref) => ref.node),
253+
);
254+
255+
for (const node of toReplace) {
256+
const moduleDecl = findClassDeclaration(node, typeChecker);
257+
258+
if (moduleDecl) {
259+
const moduleMeta = templateTypeChecker.getNgModuleMetadata(moduleDecl);
260+
261+
if (moduleMeta) {
262+
moduleMeta.exports.forEach((exp) => {
263+
if (usedImports.has(exp.node as NamedClassDeclaration)) {
264+
replacements.track(node, exp as Reference<NamedClassDeclaration>);
265+
}
266+
});
267+
} else {
268+
// It's unlikely not to have module metadata at this point, but just in
269+
// case unmark the class for removal to reduce the chance of breakages.
270+
classesToRemove.delete(moduleDecl);
271+
}
272+
}
273+
}
274+
275+
replaceModulesInImportsArray(
276+
array,
277+
closestClass,
278+
replacements,
279+
tracker,
280+
templateTypeChecker,
281+
importRemapper,
282+
);
283+
}
284+
}
285+
286+
/**
287+
* Replaces any leftover modules in `imports` arrays with their exports that are used within a
288+
* component.
289+
* @param array Imports array which is being migrated.
290+
* @param componentClass Class that the imports array belongs to.
291+
* @param replacements Map of NgModule references to their exports.
292+
* @param tracker
293+
* @param templateTypeChecker
294+
* @param importRemapper
295+
*/
296+
function replaceModulesInImportsArray(
297+
array: ts.ArrayLiteralExpression,
298+
componentClass: ts.ClassDeclaration,
299+
replacements: UniqueItemTracker<ts.Node, Reference<NamedClassDeclaration>>,
300+
tracker: ChangeTracker,
301+
templateTypeChecker: TemplateTypeChecker,
302+
importRemapper?: ComponentImportsRemapper,
303+
): void {
304+
const newElements: ts.Expression[] = [];
305+
306+
for (const element of array.elements) {
307+
const replacementRefs = replacements.get(element);
308+
309+
if (!replacementRefs) {
310+
newElements.push(element);
311+
continue;
312+
}
313+
314+
const potentialImports: PotentialImport[] = [];
315+
316+
for (const ref of replacementRefs) {
317+
const importLocation = findImportLocation(
318+
ref,
319+
componentClass,
320+
PotentialImportMode.Normal,
321+
templateTypeChecker,
322+
);
323+
324+
if (importLocation) {
325+
potentialImports.push(importLocation);
326+
}
327+
}
328+
329+
newElements.push(
330+
...potentialImportsToExpressions(potentialImports, componentClass, tracker, importRemapper),
331+
);
332+
}
333+
334+
tracker.replaceNode(array, ts.factory.updateArrayLiteralExpression(array, newElements));
335+
}
336+
171337
/**
172338
* Removes all tracked array references.
173339
* @param locations Locations from which to remove the references.
@@ -454,3 +620,19 @@ function findNgModuleDecorator(
454620
const decorators = getAngularDecorators(typeChecker, ts.getDecorators(node) || []);
455621
return decorators.find((decorator) => decorator.name === 'NgModule') || null;
456622
}
623+
624+
/**
625+
* Checks whether a node is used inside of an `imports` array.
626+
* @param closestAssignment The closest property assignment to the node.
627+
* @param closestArray The closest array to the node.
628+
*/
629+
function isInImportsArray(
630+
closestAssignment: ts.PropertyAssignment,
631+
closestArray: ts.ArrayLiteralExpression,
632+
): boolean {
633+
return (
634+
closestAssignment.initializer === closestArray &&
635+
(ts.isIdentifier(closestAssignment.name) || ts.isStringLiteralLike(closestAssignment.name)) &&
636+
closestAssignment.name.text === 'imports'
637+
);
638+
}

packages/core/schematics/ng-generate/standalone-migration/to-standalone.ts

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ function getComponentImportExpressions(
184184
const usedDependenciesInMigration = new Set(
185185
templateDependencies.filter((dep) => allDeclarations.has(dep.node)),
186186
);
187-
const imports: ts.Expression[] = [];
188187
const seenImports = new Set<string>();
189188
const resolvedDependencies: PotentialImport[] = [];
190189

@@ -204,45 +203,58 @@ function getComponentImportExpressions(
204203
}
205204
}
206205

206+
return potentialImportsToExpressions(resolvedDependencies, decl, tracker, importRemapper);
207+
}
208+
209+
/**
210+
* Converts an array of potential imports to an array of expressions that can be
211+
* added to the `imports` array.
212+
* @param potentialImports Imports to be converted.
213+
* @param component Component class to which the imports will be added.
214+
* @param tracker
215+
* @param importRemapper
216+
*/
217+
export function potentialImportsToExpressions(
218+
potentialImports: PotentialImport[],
219+
component: ts.ClassDeclaration,
220+
tracker: ChangeTracker,
221+
importRemapper?: ComponentImportsRemapper,
222+
): ts.Expression[] {
207223
const processedDependencies = importRemapper
208-
? importRemapper(resolvedDependencies, decl)
209-
: resolvedDependencies;
224+
? importRemapper(potentialImports, component)
225+
: potentialImports;
210226

211-
for (const importLocation of processedDependencies) {
227+
return processedDependencies.map((importLocation) => {
212228
if (importLocation.moduleSpecifier) {
213-
const identifier = tracker.addImport(
214-
decl.getSourceFile(),
229+
return tracker.addImport(
230+
component.getSourceFile(),
215231
importLocation.symbolName,
216232
importLocation.moduleSpecifier,
217233
);
218-
imports.push(identifier);
219-
} else {
220-
const identifier = ts.factory.createIdentifier(importLocation.symbolName);
234+
}
221235

222-
if (importLocation.isForwardReference) {
223-
const forwardRefExpression = tracker.addImport(
224-
decl.getSourceFile(),
225-
'forwardRef',
226-
'@angular/core',
227-
);
228-
const arrowFunction = ts.factory.createArrowFunction(
229-
undefined,
230-
undefined,
231-
[],
232-
undefined,
233-
undefined,
234-
identifier,
235-
);
236-
imports.push(
237-
ts.factory.createCallExpression(forwardRefExpression, undefined, [arrowFunction]),
238-
);
239-
} else {
240-
imports.push(identifier);
241-
}
236+
const identifier = ts.factory.createIdentifier(importLocation.symbolName);
237+
238+
if (!importLocation.isForwardReference) {
239+
return identifier;
242240
}
243-
}
244241

245-
return imports;
242+
const forwardRefExpression = tracker.addImport(
243+
component.getSourceFile(),
244+
'forwardRef',
245+
'@angular/core',
246+
);
247+
const arrowFunction = ts.factory.createArrowFunction(
248+
undefined,
249+
undefined,
250+
[],
251+
undefined,
252+
undefined,
253+
identifier,
254+
);
255+
256+
return ts.factory.createCallExpression(forwardRefExpression, undefined, [arrowFunction]);
257+
});
246258
}
247259

248260
/**
@@ -480,7 +492,7 @@ function isNamedPropertyAssignment(
480492
* @param importMode Mode in which to resolve the import target.
481493
* @param typeChecker
482494
*/
483-
function findImportLocation(
495+
export function findImportLocation(
484496
target: Reference<NamedClassDeclaration>,
485497
inComponent: ts.ClassDeclaration,
486498
importMode: PotentialImportMode,
@@ -602,7 +614,7 @@ export function findTestObjectsToMigrate(sourceFile: ts.SourceFile, typeChecker:
602614
* @param decl Component in whose template we're looking for dependencies.
603615
* @param typeChecker
604616
*/
605-
function findTemplateDependencies(
617+
export function findTemplateDependencies(
606618
decl: ts.ClassDeclaration,
607619
typeChecker: TemplateTypeChecker,
608620
): Reference<NamedClassDeclaration>[] {

0 commit comments

Comments
 (0)