@@ -12,6 +12,7 @@ import ts from 'typescript';
1212
1313import { getAngularDecorators , NgDecorator } from '../../utils/ng_decorators' ;
1414import { getImportSpecifier } from '../../utils/typescript/imports' ;
15+ import { closestNode } from '../../utils/typescript/nodes' ;
1516import { isReferenceToImport } from '../../utils/typescript/symbol' ;
1617
1718import { ChangesByFile , ChangeTracker , findClassDeclaration , findLiteralProperty , NamedClassDeclaration } from './util' ;
@@ -44,10 +45,10 @@ export function toStandalone(
4445 }
4546
4647 for ( const node of modulesToMigrate ) {
47- migrateNgModuleClass ( node , tracker , templateTypeChecker ) ;
48+ migrateNgModuleClass ( node , declarations , tracker , typeChecker , templateTypeChecker ) ;
4849 }
4950
50- migrateTestDeclarations ( testObjectsToMigrate , tracker , typeChecker ) ;
51+ migrateTestDeclarations ( testObjectsToMigrate , declarations , tracker , typeChecker ) ;
5152 return tracker . recordChanges ( ) ;
5253}
5354
@@ -131,94 +132,116 @@ function getComponentImportExpressions(
131132/**
132133 * Moves all of the declarations of a class decorated with `@NgModule` to its imports.
133134 * @param node Class being migrated.
135+ * @param allDeclarations All the declarations that are being converted as a part of this migration.
134136 * @param tracker
135137 * @param typeChecker
138+ * @param templateTypeChecker
136139 */
137140function migrateNgModuleClass (
138- node : ts . ClassDeclaration , tracker : ChangeTracker , typeChecker : TemplateTypeChecker ) {
139- const decorator = typeChecker . getNgModuleMetadata ( node ) ?. decorator ;
141+ node : ts . ClassDeclaration , allDeclarations : Reference < ts . ClassDeclaration > [ ] ,
142+ tracker : ChangeTracker , typeChecker : ts . TypeChecker , templateTypeChecker : TemplateTypeChecker ) {
143+ const decorator = templateTypeChecker . getNgModuleMetadata ( node ) ?. decorator ;
140144
141145 if ( decorator && ts . isCallExpression ( decorator . expression ) &&
142146 decorator . expression . arguments . length === 1 &&
143147 ts . isObjectLiteralExpression ( decorator . expression . arguments [ 0 ] ) ) {
144- moveDeclarationsToImports ( decorator . expression . arguments [ 0 ] , tracker ) ;
148+ moveDeclarationsToImports (
149+ decorator . expression . arguments [ 0 ] , allDeclarations . map ( decl => decl . node ) , typeChecker ,
150+ tracker ) ;
145151 }
146152}
147153
148154/**
149155 * Moves all the symbol references from the `declarations` array to the `imports`
150156 * array of an `NgModule` class and removes the `declarations`.
151157 * @param literal Object literal used to configure the module that should be migrated.
158+ * @param allDeclarations All the declarations that are being converted as a part of this migration.
159+ * @param typeChecker
152160 * @param tracker
153161 */
154162function moveDeclarationsToImports (
155- literal : ts . ObjectLiteralExpression , tracker : ChangeTracker ) : void {
156- const properties =
157- literal . properties
158- . map ( prop => {
159- if ( ! isNamedPropertyAssignment ( prop ) ) {
160- return prop ;
161- }
162-
163- // If there's no `imports`, copy the initializer from the `declarations`.
164- if ( prop . name . text === 'declarations' && ! findLiteralProperty ( literal , 'imports' ) ) {
165- return ts . factory . createPropertyAssignment ( 'imports' , prop . initializer ) ;
166- }
167-
168- // Migrate the `imports`.
169- if ( prop . name . text === 'imports' ) {
170- const declarations = findLiteralProperty ( literal , 'declarations' ) ;
171- return declarations && ts . isPropertyAssignment ( declarations ) ?
172- mergeDeclarationsIntoImports ( declarations , prop ) :
173- prop ;
174- }
175-
176- // Retain any remaining properties.
177- return prop ;
178- } )
179- // Drop the `declarations` property.
180- . filter ( prop => isNamedPropertyAssignment ( prop ) && prop . name . text !== 'declarations' ) ;
163+ literal : ts . ObjectLiteralExpression , allDeclarations : ts . ClassDeclaration [ ] ,
164+ typeChecker : ts . TypeChecker , tracker : ChangeTracker ) : void {
165+ const declarationsProp = findLiteralProperty ( literal , 'declarations' ) ;
181166
182- tracker . replaceNode (
183- literal , ts . factory . createObjectLiteralExpression ( properties , true ) , ts . EmitHint . Expression ) ;
184- }
167+ if ( ! declarationsProp ) {
168+ return ;
169+ }
185170
186- /**
187- * Merges the `declarations` and `imports` arrays of an NgModule.
188- * @param declarations Node that declares the `declarations` property.
189- * @param imports Node that declares the `imports` property.
190- */
191- function mergeDeclarationsIntoImports (
192- declarations : ts . PropertyAssignment , imports : ts . PropertyAssignment ) {
193- const importsIsArray = ts . isArrayLiteralExpression ( imports . initializer ) ;
194- const declarationsIsArray = ts . isArrayLiteralExpression ( declarations . initializer ) ;
195- let arrayElements : ts . Expression [ ] ;
196-
197- if ( importsIsArray && declarationsIsArray ) {
198- // Both values are arrays so they can be merged statically.
199- // E.g. `imports: [Import1, Import2, Declaration1]`.
200- arrayElements = [ ...imports . initializer . elements , ...declarations . initializer . elements ] ;
201- } else if ( importsIsArray ) {
202- // Only the imports is an array so we need to use a spread to merge the
203- // declarations. E.g. `imports: [Import1, Import2, ...DECLARATIONS]`.
204- arrayElements =
205- [ ...imports . initializer . elements , ts . factory . createSpreadElement ( declarations . initializer ) ] ;
206- } else if ( declarationsIsArray ) {
207- // Declarations are an array, but imports aren't so we have to generate a spread.
208- // E.g. `imports: [...IMPORTS, Declaration1, Declaration2]`.
209- arrayElements =
210- [ ts . factory . createSpreadElement ( imports . initializer ) , ...declarations . initializer . elements ] ;
211- } else {
212- // Neither the declarations nor the imports are arrays so we have to use spread for
213- // both. E.g. `imports: [...IMPORTS, ...DECLARATIONS]`.
214- arrayElements = [
215- ts . factory . createSpreadElement ( imports . initializer ) ,
216- ts . factory . createSpreadElement ( declarations . initializer )
217- ] ;
171+ const declarationsToPreserve : ts . Expression [ ] = [ ] ;
172+ const declarationsToCopy : ts . Expression [ ] = [ ] ;
173+ const properties : ts . ObjectLiteralElementLike [ ] = [ ] ;
174+ const importsProp = findLiteralProperty ( literal , 'imports' ) ;
175+
176+ // Separate the declarations that we want to keep and ones we need to copy into the `imports`.
177+ if ( ts . isPropertyAssignment ( declarationsProp ) ) {
178+ // If the declarations are an array, we can analyze it to
179+ // find any classes from the current migration.
180+ if ( ts . isArrayLiteralExpression ( declarationsProp . initializer ) ) {
181+ for ( const el of declarationsProp . initializer . elements ) {
182+ if ( ts . isIdentifier ( el ) ) {
183+ const correspondingClass = findClassDeclaration ( el , typeChecker ) ;
184+
185+ if ( ! correspondingClass || allDeclarations . includes ( correspondingClass ) ) {
186+ declarationsToCopy . push ( el ) ;
187+ } else {
188+ declarationsToPreserve . push ( el ) ;
189+ }
190+ } else {
191+ declarationsToCopy . push ( el ) ;
192+ }
193+ }
194+ } else {
195+ // Otherwise create a spread that will be copied into the `imports`.
196+ declarationsToCopy . push ( ts . factory . createSpreadElement ( declarationsProp . initializer ) ) ;
197+ }
198+ }
199+
200+ // If there are no `imports`, create them with the declarations we want to copy.
201+ if ( ! importsProp && declarationsToCopy . length > 0 ) {
202+ properties . push ( ts . factory . createPropertyAssignment (
203+ 'imports' , ts . factory . createArrayLiteralExpression ( declarationsToCopy ) ) ) ;
204+ }
205+
206+ for ( const prop of literal . properties ) {
207+ if ( ! isNamedPropertyAssignment ( prop ) ) {
208+ properties . push ( prop ) ;
209+ continue ;
210+ }
211+
212+ // If we have declarations to preserve, update the existing property, otherwise drop it.
213+ if ( prop === declarationsProp ) {
214+ if ( declarationsToPreserve . length > 0 ) {
215+ properties . push ( ts . factory . updatePropertyAssignment (
216+ prop , prop . name , ts . factory . createArrayLiteralExpression ( declarationsToPreserve ) ) ) ;
217+ }
218+ continue ;
219+ }
220+
221+ // If we have an `imports` array and declarations
222+ // that should be copied, we merge the two arrays.
223+ if ( prop === importsProp && declarationsToCopy . length > 0 ) {
224+ let initializer : ts . Expression ;
225+
226+ if ( ts . isArrayLiteralExpression ( prop . initializer ) ) {
227+ initializer = ts . factory . updateArrayLiteralExpression (
228+ prop . initializer , [ ...prop . initializer . elements , ...declarationsToCopy ] ) ;
229+ } else {
230+ initializer = ts . factory . createArrayLiteralExpression (
231+ [ ts . factory . createSpreadElement ( prop . initializer ) , ...declarationsToCopy ] ) ;
232+ }
233+
234+ properties . push ( ts . factory . updatePropertyAssignment ( prop , prop . name , initializer ) ) ;
235+ continue ;
236+ }
237+
238+ // Retain any remaining properties.
239+ properties . push ( prop ) ;
218240 }
219241
220- return ts . factory . createPropertyAssignment (
221- imports . name , ts . factory . createArrayLiteralExpression ( arrayElements ) ) ;
242+ tracker . replaceNode (
243+ literal , ts . factory . updateObjectLiteralExpression ( literal , properties ) ,
244+ ts . EmitHint . Expression ) ;
222245}
223246
224247/** Adds `standalone: true` to a decorator node. */
@@ -397,17 +420,30 @@ function extractDeclarationsFromModule(
397420 * @param typeChecker
398421 */
399422function migrateTestDeclarations (
400- testObjects : ts . ObjectLiteralExpression [ ] , tracker : ChangeTracker ,
423+ testObjects : ts . ObjectLiteralExpression [ ] ,
424+ declarationsOutsideOfTestFiles : Reference < ts . ClassDeclaration > [ ] , tracker : ChangeTracker ,
401425 typeChecker : ts . TypeChecker ) {
402- const { decorators, componentImports} = analyzeTestingModules ( testObjects , tracker , typeChecker ) ;
426+ const { decorators, componentImports} = analyzeTestingModules ( testObjects , typeChecker ) ;
427+ const allDeclarations : ts . ClassDeclaration [ ] =
428+ declarationsOutsideOfTestFiles . map ( ref => ref . node ) ;
403429
404430 for ( const decorator of decorators ) {
431+ const closestClass = closestNode ( decorator . node , ts . isClassDeclaration ) ;
432+
405433 if ( decorator . name === 'Pipe' || decorator . name === 'Directive' ) {
406434 tracker . replaceNode ( decorator . node , addStandaloneToDecorator ( decorator . node ) ) ;
435+
436+ if ( closestClass ) {
437+ allDeclarations . push ( closestClass ) ;
438+ }
407439 } else if ( decorator . name === 'Component' ) {
408440 const newDecorator = addStandaloneToDecorator ( decorator . node ) ;
409441 const importsToAdd = componentImports . get ( decorator . node ) ;
410442
443+ if ( closestClass ) {
444+ allDeclarations . push ( closestClass ) ;
445+ }
446+
411447 if ( importsToAdd && importsToAdd . size > 0 ) {
412448 tracker . replaceNode (
413449 decorator . node ,
@@ -420,6 +456,10 @@ function migrateTestDeclarations(
420456 }
421457 }
422458 }
459+
460+ for ( const obj of testObjects ) {
461+ moveDeclarationsToImports ( obj , allDeclarations , typeChecker , tracker ) ;
462+ }
423463}
424464
425465/**
@@ -429,8 +469,7 @@ function migrateTestDeclarations(
429469 * @param testObjects Object literals that should be analyzed.
430470 */
431471function analyzeTestingModules (
432- testObjects : ts . ObjectLiteralExpression [ ] , tracker : ChangeTracker ,
433- typeChecker : ts . TypeChecker ) {
472+ testObjects : ts . ObjectLiteralExpression [ ] , typeChecker : ts . TypeChecker ) {
434473 const seenDeclarations = new Set < ts . Declaration > ( ) ;
435474 const decorators : NgDecorator [ ] = [ ] ;
436475 const componentImports = new Map < ts . Decorator , Set < ts . Expression > > ( ) ;
@@ -447,8 +486,6 @@ function analyzeTestingModules(
447486 importsProp . initializer . elements :
448487 null ;
449488
450- moveDeclarationsToImports ( obj , tracker ) ;
451-
452489 for ( const decl of declarations ) {
453490 if ( seenDeclarations . has ( decl ) ) {
454491 continue ;
0 commit comments