@@ -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' ;
3133import {
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 */
163178function 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 */
296370function 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 ) ) ;
0 commit comments