Skip to content

Commit 6377487

Browse files
crisbetopkozlowski-opensource
authored andcommitted
fix(migrations): only exclude bootstrapped declarations from initial standalone migration (#48987)
Currently the standalone migration is set up to skip any modules that have a `bootstrap` array with at least one element. This ends up being misleading for small apps who have everything in the root module. These changes add some logic to only skip the root component. Fixes #48944. PR Close #48987
1 parent 845ef71 commit 6377487

File tree

2 files changed

+105
-23
lines changed

2 files changed

+105
-23
lines changed

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

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,17 @@ export function toStandalone(
3535

3636
for (const sourceFile of sourceFiles) {
3737
const {modules, testObjects} = findModulesToMigrate(sourceFile, typeChecker);
38-
modules.forEach(
39-
module => declarations.push(...extractDeclarationsFromModule(module, templateTypeChecker)));
40-
modulesToMigrate.push(...modules);
38+
39+
for (const module of modules) {
40+
const moduleDeclarations =
41+
extractDeclarationsFromModule(module, typeChecker, templateTypeChecker);
42+
43+
if (moduleDeclarations.length > 0) {
44+
modulesToMigrate.push(module);
45+
declarations.push(...moduleDeclarations);
46+
}
47+
}
48+
4149
testObjectsToMigrate.push(...testObjects);
4250
}
4351

@@ -153,13 +161,11 @@ function migrateNgModuleClass(
153161
node: ts.ClassDeclaration, allDeclarations: Reference<ts.ClassDeclaration>[],
154162
tracker: ChangeTracker, typeChecker: ts.TypeChecker, templateTypeChecker: TemplateTypeChecker) {
155163
const decorator = templateTypeChecker.getNgModuleMetadata(node)?.decorator;
164+
const metadata = decorator ? extractMetadataLiteral(decorator) : null;
156165

157-
if (decorator && ts.isCallExpression(decorator.expression) &&
158-
decorator.expression.arguments.length === 1 &&
159-
ts.isObjectLiteralExpression(decorator.expression.arguments[0])) {
166+
if (metadata) {
160167
moveDeclarationsToImports(
161-
decorator.expression.arguments[0], allDeclarations.map(decl => decl.node), typeChecker,
162-
tracker);
168+
metadata, allDeclarations.map(decl => decl.node), typeChecker, tracker);
163169
}
164170
}
165171

@@ -355,18 +361,12 @@ function findModulesToMigrate(sourceFile: ts.SourceFile, typeChecker: ts.TypeChe
355361
if (ts.isClassDeclaration(node)) {
356362
const decorator = getAngularDecorators(typeChecker, ts.getDecorators(node) || [])
357363
.find(current => current.name === 'NgModule');
358-
const metadata = decorator?.node.expression.arguments[0];
364+
const metadata = decorator ? extractMetadataLiteral(decorator.node) : null;
359365

360-
if (metadata && ts.isObjectLiteralExpression(metadata)) {
366+
if (metadata) {
361367
const declarations = findLiteralProperty(metadata, 'declarations');
362-
const bootstrap = findLiteralProperty(metadata, 'bootstrap');
363-
const hasDeclarations = declarations != null && hasNgModuleMetadataElements(declarations);
364-
const hasBootstrap = bootstrap != null && hasNgModuleMetadataElements(bootstrap);
365-
366-
// Skip modules that bootstrap components since changing them would also involve
367-
// converting `bootstrapModule` calls to `bootstrapApplication`. These declarations
368-
// will be converted to standalone in the `standalone-bootstrap` step.
369-
if (hasDeclarations && !hasBootstrap) {
368+
369+
if (declarations != null && hasNgModuleMetadataElements(declarations)) {
370370
modules.push(node);
371371
}
372372
}
@@ -424,11 +424,53 @@ function findTemplateDependencies(
424424

425425
/** Extracts classes that are referred to in a module's `declarations` array. */
426426
function extractDeclarationsFromModule(
427-
ngModule: ts.ClassDeclaration,
428-
typeChecker: TemplateTypeChecker): Reference<ts.ClassDeclaration>[] {
429-
return typeChecker.getNgModuleMetadata(ngModule)?.declarations.filter(
430-
decl => ts.isClassDeclaration(decl.node)) as Reference<ts.ClassDeclaration>[] ||
431-
[];
427+
ngModule: ts.ClassDeclaration, typeChecker: ts.TypeChecker,
428+
templateTypeChecker: TemplateTypeChecker): Reference<ts.ClassDeclaration>[] {
429+
const metadata = templateTypeChecker.getNgModuleMetadata(ngModule);
430+
431+
if (!metadata) {
432+
return [];
433+
}
434+
435+
const allDeclarations = metadata.declarations.filter(decl => ts.isClassDeclaration(decl.node)) as
436+
Reference<ts.ClassDeclaration>[];
437+
const metaLiteral = metadata.decorator ? extractMetadataLiteral(metadata.decorator) : null;
438+
const bootstrapProp = metaLiteral ? findLiteralProperty(metaLiteral, 'bootstrap') : null;
439+
440+
// If there's no `bootstrap`, we can migrate all the declarations.
441+
if (!bootstrapProp) {
442+
return allDeclarations;
443+
}
444+
445+
// If we can't analyze the `bootstrap` property, we can't safely migrate the module.
446+
if (!ts.isPropertyAssignment(bootstrapProp) ||
447+
!ts.isArrayLiteralExpression(bootstrapProp.initializer)) {
448+
return [];
449+
}
450+
451+
const bootstrappedClasses = new Set<ts.ClassDeclaration>();
452+
453+
for (const el of bootstrapProp.initializer.elements) {
454+
const referencedClass = ts.isIdentifier(el) ? findClassDeclaration(el, typeChecker) : null;
455+
456+
// If we manage to resolve the class, we can use it to filter out declarations that are
457+
// being bootstrapped. Otherwise we risk breaking the app so we skip the entire module.
458+
if (referencedClass) {
459+
bootstrappedClasses.add(referencedClass);
460+
} else {
461+
return [];
462+
}
463+
}
464+
465+
return allDeclarations.filter(ref => !bootstrappedClasses.has(ref.node));
466+
}
467+
468+
/** Extracts the metadata object literal from an Angular decorator. */
469+
function extractMetadataLiteral(decorator: ts.Decorator): ts.ObjectLiteralExpression|null {
470+
return ts.isCallExpression(decorator.expression) && decorator.expression.arguments.length === 1 &&
471+
ts.isObjectLiteralExpression(decorator.expression.arguments[0]) ?
472+
decorator.expression.arguments[0] :
473+
null;
432474
}
433475

434476
/**

packages/core/schematics/test/standalone_migration_spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,46 @@ describe('standalone migration', () => {
13061306
`));
13071307
});
13081308

1309+
it('should migrate declarations that are not being bootstrapped in a root module', async () => {
1310+
writeFile('dir.ts', `
1311+
import {Directive} from '@angular/core';
1312+
1313+
@Directive({selector: '[foo]'})
1314+
export class MyDir {}
1315+
`);
1316+
1317+
writeFile('module.ts', `
1318+
import {NgModule, Component} from '@angular/core';
1319+
import {MyDir} from './dir';
1320+
1321+
@Component({selector: 'root-comp', template: 'hello'})
1322+
export class RootComp {}
1323+
1324+
@NgModule({declarations: [RootComp, MyDir], bootstrap: [RootComp]})
1325+
export class Mod {}
1326+
`);
1327+
1328+
await runMigration('convert-to-standalone');
1329+
1330+
expect(stripWhitespace(tree.readContent('dir.ts'))).toBe(stripWhitespace(`
1331+
import {Directive} from '@angular/core';
1332+
1333+
@Directive({selector: '[foo]', standalone: true})
1334+
export class MyDir {}
1335+
`));
1336+
1337+
expect(stripWhitespace(tree.readContent('module.ts'))).toBe(stripWhitespace(`
1338+
import {NgModule, Component} from '@angular/core';
1339+
import {MyDir} from './dir';
1340+
1341+
@Component({selector: 'root-comp', template: 'hello'})
1342+
export class RootComp {}
1343+
1344+
@NgModule({imports: [MyDir], declarations: [RootComp], bootstrap: [RootComp]})
1345+
export class Mod {}
1346+
`));
1347+
});
1348+
13091349
it('should generate a forwardRef for forward reference within the same file', async () => {
13101350
writeFile('decls.ts', `
13111351
import {Component, Directive} from '@angular/core';

0 commit comments

Comments
 (0)