Skip to content

Commit 89256e6

Browse files
crisbetothePunderWoman
authored andcommitted
fix(migrations): replace removed NgModules in tests with their exports (angular#58627)
In angular#57684 the standalone migration was changed so that it replaces any leftover modules with their `exports`, in an attempt to preserve more working code. These changes expand that logic to also cover tests since it's somewhat common internally to only import a component's module without having any references to the component. Note that tests are a bit of a special case, because we don't have access to the template type checker, so instead we copy over all of the `exports` of that module. PR Close angular#58627
1 parent 731e3ea commit 89256e6

File tree

5 files changed

+392
-100
lines changed

5 files changed

+392
-100
lines changed

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

Lines changed: 129 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
findClassDeclaration,
1818
findLiteralProperty,
1919
getNodeLookup,
20+
getTestingImports,
21+
isTestCall,
2022
NamedClassDeclaration,
2123
offsetsToNodes,
2224
ReferenceResolver,
@@ -29,7 +31,7 @@ import {
2931
TemplateTypeChecker,
3032
} from '@angular/compiler-cli/private/migrations';
3133
import {
32-
ComponentImportsRemapper,
34+
DeclarationImportsRemapper,
3335
findImportLocation,
3436
findTemplateDependencies,
3537
potentialImportsToExpressions,
@@ -52,7 +54,7 @@ export function pruneNgModules(
5254
printer: ts.Printer,
5355
importRemapper?: ImportRemapper,
5456
referenceLookupExcludedFiles?: RegExp,
55-
componentImportRemapper?: ComponentImportsRemapper,
57+
declarationImportRemapper?: DeclarationImportsRemapper,
5658
) {
5759
const filesToRemove = new Set<ts.SourceFile>();
5860
const tracker = new ChangeTracker(printer, importRemapper);
@@ -75,6 +77,7 @@ export function pruneNgModules(
7577
const classesToRemove = new Set<ts.ClassDeclaration>();
7678
const barrelExports = new UniqueItemTracker<ts.SourceFile, ts.ExportDeclaration>();
7779
const componentImportArrays = new UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>();
80+
const testArrays = new UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>();
7881
const nodesToRemove = new Set<ts.Node>();
7982

8083
sourceFiles.forEach(function walk(node: ts.Node) {
@@ -83,6 +86,7 @@ export function pruneNgModules(
8386
node,
8487
removalLocations,
8588
componentImportArrays,
89+
testArrays,
8690
templateTypeChecker,
8791
referenceResolver,
8892
program,
@@ -106,13 +110,23 @@ export function pruneNgModules(
106110
node.forEachChild(walk);
107111
});
108112

109-
replaceInImportsArray(
113+
replaceInComponentImportsArray(
110114
componentImportArrays,
111115
classesToRemove,
112116
tracker,
113117
typeChecker,
114118
templateTypeChecker,
115-
componentImportRemapper,
119+
declarationImportRemapper,
120+
);
121+
122+
replaceInTestImportsArray(
123+
testArrays,
124+
removalLocations,
125+
classesToRemove,
126+
tracker,
127+
typeChecker,
128+
templateTypeChecker,
129+
declarationImportRemapper,
116130
);
117131

118132
// We collect all the places where we need to remove references first before generating the
@@ -157,19 +171,22 @@ export function pruneNgModules(
157171
* @param ngModule Module being removed.
158172
* @param removalLocations Tracks the different places from which the class should be removed.
159173
* @param componentImportArrays Set of `imports` arrays of components that need to be adjusted.
174+
* @param testImportArrays Set of `imports` arrays of tests that need to be adjusted.
160175
* @param referenceResolver
161176
* @param program
162177
*/
163178
function collectChangeLocations(
164179
ngModule: ts.ClassDeclaration,
165180
removalLocations: RemovalLocations,
166181
componentImportArrays: UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>,
182+
testImportArrays: UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>,
167183
templateTypeChecker: TemplateTypeChecker,
168184
referenceResolver: ReferenceResolver,
169185
program: NgtscProgram,
170186
) {
171187
const refsByFile = referenceResolver.findReferencesInProject(ngModule.name!);
172188
const tsProgram = program.getTsProgram();
189+
const typeChecker = tsProgram.getTypeChecker();
173190
const nodes = new Set<ts.Node>();
174191

175192
for (const [fileName, refs] of refsByFile) {
@@ -185,21 +202,34 @@ function collectChangeLocations(
185202
if (closestArray) {
186203
const closestAssignment = closestNode(closestArray, ts.isPropertyAssignment);
187204

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.
191205
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;
206+
const closestCall = closestNode(closestAssignment, ts.isCallExpression);
207+
208+
if (closestCall) {
209+
const closestDecorator = closestNode(closestCall, ts.isDecorator);
210+
const closestClass = closestDecorator
211+
? closestNode(closestDecorator, ts.isClassDeclaration)
212+
: null;
213+
const directiveMeta = closestClass
214+
? templateTypeChecker.getDirectiveMetadata(closestClass)
215+
: null;
216+
217+
// If the module was flagged as being removable, but it's still being used in a
218+
// standalone component's `imports` array, it means that it was likely changed
219+
// outside of the migration and deleting it now will be breaking. Track it
220+
// separately so it can be handled properly.
221+
if (directiveMeta && directiveMeta.isComponent && directiveMeta.isStandalone) {
222+
componentImportArrays.track(closestArray, node);
223+
continue;
224+
}
225+
226+
// If the module is removable and used inside a test's `imports`,
227+
// we track it separately so it can be replaced with its `exports`.
228+
const {testBed, catalyst} = getTestingImports(node.getSourceFile());
229+
if (isTestCall(typeChecker, closestCall, testBed, catalyst)) {
230+
testImportArrays.track(closestArray, node);
231+
continue;
232+
}
203233
}
204234
}
205235

@@ -224,21 +254,21 @@ function collectChangeLocations(
224254
}
225255

226256
/**
227-
* Replaces all the leftover modules in imports arrays with their exports.
257+
* Replaces all the leftover modules in component `imports` arrays with their exports.
228258
* @param componentImportArrays All the imports arrays and their nodes that represent NgModules.
229259
* @param classesToRemove Set of classes that were marked for removal.
230260
* @param tracker
231261
* @param typeChecker
232262
* @param templateTypeChecker
233263
* @param importRemapper
234264
*/
235-
function replaceInImportsArray(
265+
function replaceInComponentImportsArray(
236266
componentImportArrays: UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>,
237267
classesToRemove: Set<ts.ClassDeclaration>,
238268
tracker: ChangeTracker,
239269
typeChecker: ts.TypeChecker,
240270
templateTypeChecker: TemplateTypeChecker,
241-
importRemapper?: ComponentImportsRemapper,
271+
importRemapper?: DeclarationImportsRemapper,
242272
) {
243273
for (const [array, toReplace] of componentImportArrays.getEntries()) {
244274
const closestClass = closestNode(array, ts.isClassDeclaration);
@@ -272,36 +302,90 @@ function replaceInImportsArray(
272302
}
273303
}
274304

275-
replaceModulesInImportsArray(
276-
array,
277-
closestClass,
278-
replacements,
279-
tracker,
280-
templateTypeChecker,
281-
importRemapper,
282-
);
305+
replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper);
306+
}
307+
}
308+
309+
/**
310+
* Replaces all the leftover modules in testing `imports` arrays with their exports.
311+
* @param testImportArrays All test `imports` arrays and their nodes that represent modules.
312+
* @param classesToRemove Classes marked for removal by the migration.
313+
* @param tracker
314+
* @param typeChecker
315+
* @param templateTypeChecker
316+
* @param importRemapper
317+
*/
318+
function replaceInTestImportsArray(
319+
testImportArrays: UniqueItemTracker<ts.ArrayLiteralExpression, ts.Node>,
320+
removalLocations: RemovalLocations,
321+
classesToRemove: Set<ts.ClassDeclaration>,
322+
tracker: ChangeTracker,
323+
typeChecker: ts.TypeChecker,
324+
templateTypeChecker: TemplateTypeChecker,
325+
importRemapper?: DeclarationImportsRemapper,
326+
) {
327+
for (const [array, toReplace] of testImportArrays.getEntries()) {
328+
const replacements = new UniqueItemTracker<ts.Node, Reference<NamedClassDeclaration>>();
329+
330+
for (const node of toReplace) {
331+
const moduleDecl = findClassDeclaration(node, typeChecker);
332+
333+
if (moduleDecl) {
334+
const moduleMeta = templateTypeChecker.getNgModuleMetadata(moduleDecl);
335+
336+
if (moduleMeta) {
337+
// Since we don't have access to the template type checker in tests,
338+
// we copy over all the `exports` that aren't flagged for removal.
339+
const exports = moduleMeta.exports.filter(
340+
(exp) => !classesToRemove.has(exp.node as NamedClassDeclaration),
341+
);
342+
343+
if (exports.length > 0) {
344+
exports.forEach((exp) =>
345+
replacements.track(node, exp as Reference<NamedClassDeclaration>),
346+
);
347+
} else {
348+
removalLocations.arrays.track(array, node);
349+
}
350+
} else {
351+
// It's unlikely not to have module metadata at this point, but just in
352+
// case unmark the class for removal to reduce the chance of breakages.
353+
classesToRemove.delete(moduleDecl);
354+
}
355+
}
356+
}
357+
358+
replaceModulesInImportsArray(array, replacements, tracker, templateTypeChecker, importRemapper);
283359
}
284360
}
285361

286362
/**
287-
* Replaces any leftover modules in `imports` arrays with their exports that are used within a
288-
* component.
363+
* Replaces any leftover modules in an `imports` arrays with a set of specified exports
289364
* @param array Imports array which is being migrated.
290-
* @param componentClass Class that the imports array belongs to.
291365
* @param replacements Map of NgModule references to their exports.
292366
* @param tracker
293367
* @param templateTypeChecker
294368
* @param importRemapper
295369
*/
296370
function replaceModulesInImportsArray(
297371
array: ts.ArrayLiteralExpression,
298-
componentClass: ts.ClassDeclaration,
299372
replacements: UniqueItemTracker<ts.Node, Reference<NamedClassDeclaration>>,
300373
tracker: ChangeTracker,
301374
templateTypeChecker: TemplateTypeChecker,
302-
importRemapper?: ComponentImportsRemapper,
375+
importRemapper?: DeclarationImportsRemapper,
303376
): void {
377+
if (replacements.isEmpty()) {
378+
return;
379+
}
380+
304381
const newElements: ts.Expression[] = [];
382+
const identifiers = new Set<string>();
383+
384+
for (const element of array.elements) {
385+
if (ts.isIdentifier(element)) {
386+
identifiers.add(element.text);
387+
}
388+
}
305389

306390
for (const element of array.elements) {
307391
const replacementRefs = replacements.get(element);
@@ -316,7 +400,7 @@ function replaceModulesInImportsArray(
316400
for (const ref of replacementRefs) {
317401
const importLocation = findImportLocation(
318402
ref,
319-
componentClass,
403+
array,
320404
PotentialImportMode.Normal,
321405
templateTypeChecker,
322406
);
@@ -326,9 +410,16 @@ function replaceModulesInImportsArray(
326410
}
327411
}
328412

329-
newElements.push(
330-
...potentialImportsToExpressions(potentialImports, componentClass, tracker, importRemapper),
331-
);
413+
potentialImportsToExpressions(
414+
potentialImports,
415+
array.getSourceFile(),
416+
tracker,
417+
importRemapper,
418+
).forEach((expr) => {
419+
if (!ts.isIdentifier(expr) || !identifiers.has(expr.text)) {
420+
newElements.push(expr);
421+
}
422+
});
332423
}
333424

334425
tracker.replaceNode(array, ts.factory.updateArrayLiteralExpression(array, newElements));

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {getAngularDecorators} from '../../utils/ng_decorators';
1616
import {closestNode} from '../../utils/typescript/nodes';
1717

1818
import {
19-
ComponentImportsRemapper,
19+
DeclarationImportsRemapper,
2020
convertNgModuleDeclarationToStandalone,
2121
extractDeclarationsFromModule,
2222
findTestObjectsToMigrate,
@@ -59,7 +59,7 @@ export function toStandaloneBootstrap(
5959
printer: ts.Printer,
6060
importRemapper?: ImportRemapper,
6161
referenceLookupExcludedFiles?: RegExp,
62-
componentImportRemapper?: ComponentImportsRemapper,
62+
declarationImportRemapper?: DeclarationImportsRemapper,
6363
) {
6464
const tracker = new ChangeTracker(printer, importRemapper);
6565
const typeChecker = program.getTsProgram().getTypeChecker();
@@ -121,7 +121,7 @@ export function toStandaloneBootstrap(
121121
allDeclarations,
122122
tracker,
123123
templateTypeChecker,
124-
componentImportRemapper,
124+
declarationImportRemapper,
125125
);
126126
}
127127

0 commit comments

Comments
 (0)