@@ -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. */
2639interface 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+ }
0 commit comments