Skip to content

Commit fde4942

Browse files
AndrewKushnirdylhunn
authored andcommitted
fix(core): throw if standalone components are present in @NgModule.bootstrap (#45825)
This commit updates the logic to detect a situation when a standalone component is used in the NgModule-based bootstrap (`@NgModule.bootstrap`). Both AOT and JIT compilers are updated to handle this situation. PR Close #45825
1 parent 9a04ded commit fde4942

6 files changed

Lines changed: 160 additions & 2 deletions

File tree

goldens/public-api/compiler-cli/error_code.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export enum ErrorCode {
4747
INVALID_BANANA_IN_BOX = 8101,
4848
MISSING_PIPE = 8004,
4949
MISSING_REFERENCE_TARGET = 8003,
50+
NGMODULE_BOOTSTRAP_IS_STANDALONE = 6009,
5051
NGMODULE_DECLARATION_IS_STANDALONE = 6008,
5152
NGMODULE_DECLARATION_NOT_UNIQUE = 6007,
5253
NGMODULE_INVALID_DECLARATION = 6001,

packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {DynamicValue, PartialEvaluator, ResolvedValue, SyntheticValue} from '../
1717
import {PerfEvent, PerfRecorder} from '../../../perf';
1818
import {ClassDeclaration, Decorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../../reflection';
1919
import {LocalModuleScopeRegistry, ScopeData} from '../../../scope';
20+
import {getDiagnosticNode} from '../../../scope/src/util';
2021
import {FactoryTracker} from '../../../shims/api';
2122
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform';
2223
import {getSourceFile} from '../../../util/src/typescript';
@@ -235,6 +236,14 @@ export class NgModuleDecoratorHandler implements
235236
const expr = ngModule.get('bootstrap')!;
236237
const bootstrapMeta = this.evaluator.evaluate(expr, forwardRefResolver);
237238
bootstrapRefs = this.resolveTypeList(expr, bootstrapMeta, name, 'bootstrap', 0).references;
239+
240+
// Verify that the `@NgModule.bootstrap` list doesn't have Standalone Components.
241+
for (const ref of bootstrapRefs) {
242+
const dirMeta = this.metaReader.getDirectiveMetadata(ref);
243+
if (dirMeta?.isStandalone) {
244+
diagnostics.push(makeStandaloneBootstrapDiagnostic(node, ref, expr));
245+
}
246+
}
238247
}
239248

240249
const schemas = ngModule.has('schemas') ?
@@ -834,3 +843,24 @@ function isResolvedModuleWithProviders(sv: SyntheticValue<unknown>):
834843
sv.value.hasOwnProperty('ngModule' as keyof ResolvedModuleWithProviders) &&
835844
sv.value.hasOwnProperty('mwpCall' as keyof ResolvedModuleWithProviders);
836845
}
846+
847+
/**
848+
* Helper method to produce a diagnostics for a situation when a standalone component
849+
* is referenced in the `@NgModule.bootstrap` array.
850+
*/
851+
function makeStandaloneBootstrapDiagnostic(
852+
ngModuleClass: ClassDeclaration, bootstrappedClassRef: Reference<ClassDeclaration>,
853+
rawBootstrapExpr: ts.Expression|null): ts.Diagnostic {
854+
const componentClassName = bootstrappedClassRef.node.name.text;
855+
// Note: this error message should be aligned with the one produced by JIT.
856+
const message = //
857+
`The \`${componentClassName}\` class is a standalone component, which can ` +
858+
`not be used in the \`@NgModule.bootstrap\` array. Use the \`bootstrapApplication\` ` +
859+
`function for bootstrap instead.`;
860+
const relatedInformation: ts.DiagnosticRelatedInformation[]|undefined =
861+
[makeRelatedInformation(ngModuleClass, `The 'bootstrap' array is present on this NgModule.`)];
862+
863+
return makeDiagnostic(
864+
ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE,
865+
getDiagnosticNode(bootstrappedClassRef, rawBootstrapExpr), message, relatedInformation);
866+
}

packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ export enum ErrorCode {
145145
*/
146146
NGMODULE_DECLARATION_IS_STANDALONE = 6008,
147147

148+
/**
149+
* Raised when a standalone component is part of the bootstrap list of an NgModule.
150+
*/
151+
NGMODULE_BOOTSTRAP_IS_STANDALONE = 6009,
152+
148153
/**
149154
* Indicates that an NgModule is declared with `id: module.id`. This is an anti-pattern that is
150155
* disabled explicitly in the compiler, that was originally based on a misunderstanding of

packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7658,6 +7658,55 @@ function allTests(os: string) {
76587658
expect(codes).toEqual([ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX)]);
76597659
});
76607660

7661+
it('should produce an error when standalone component is used in @NgModule.bootstrap', () => {
7662+
env.tsconfig();
7663+
7664+
env.write('test.ts', `
7665+
import {Component, NgModule} from '@angular/core';
7666+
7667+
@Component({
7668+
standalone: true,
7669+
selector: 'standalone-component',
7670+
template: '...',
7671+
})
7672+
class StandaloneComponent {}
7673+
7674+
@NgModule({
7675+
bootstrap: [StandaloneComponent]
7676+
})
7677+
class BootstrapModule {}
7678+
`);
7679+
7680+
const diagnostics = env.driveDiagnostics();
7681+
const codes = diagnostics.map((diag) => diag.code);
7682+
expect(codes).toEqual([ngErrorCode(ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE)]);
7683+
});
7684+
7685+
it('should produce an error when standalone component wrapped in `forwardRef` is used in @NgModule.bootstrap',
7686+
() => {
7687+
env.tsconfig();
7688+
7689+
env.write('test.ts', `
7690+
import {Component, NgModule, forwardRef} from '@angular/core';
7691+
7692+
@Component({
7693+
standalone: true,
7694+
selector: 'standalone-component',
7695+
template: '...',
7696+
})
7697+
class StandaloneComponent {}
7698+
7699+
@NgModule({
7700+
bootstrap: [forwardRef(() => StandaloneComponent)]
7701+
})
7702+
class BootstrapModule {}
7703+
`);
7704+
7705+
const diagnostics = env.driveDiagnostics();
7706+
const codes = diagnostics.map((diag) => diag.code);
7707+
expect(codes).toEqual([ngErrorCode(ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE)]);
7708+
});
7709+
76617710
describe('InjectorDef emit optimizations for standalone', () => {
76627711
it('should not filter components out of NgModule.imports', () => {
76637712
env.write('test.ts', `

packages/core/src/render3/jit/module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ function verifySemanticsOfNgModuleDef(
326326
function verifyComponentIsPartOfNgModule(type: Type<any>) {
327327
type = resolveForwardRef(type);
328328
const existingModule = ownerNgModule.get(type);
329-
if (!existingModule) {
329+
if (!existingModule && !isStandalone(type)) {
330330
errors.push(`Component ${
331331
stringifyForError(
332332
type)} is not part of any NgModule or the module has not been imported into your module.`);
@@ -338,6 +338,14 @@ function verifySemanticsOfNgModuleDef(
338338
if (!getComponentDef(type)) {
339339
errors.push(`${stringifyForError(type)} cannot be used as an entry component.`);
340340
}
341+
if (isStandalone(type)) {
342+
// Note: this error should be the same as the
343+
// `NGMODULE_BOOTSTRAP_IS_STANDALONE` one in AOT compiler.
344+
errors.push(
345+
`The \`${stringifyForError(type)}\` class is a standalone component, which can ` +
346+
`not be used in the \`@NgModule.bootstrap\` array. Use the \`bootstrapApplication\` ` +
347+
`function for bootstrap instead.`);
348+
}
341349
}
342350

343351
function verifyComponentEntryComponentsIsPartOfNgModule(type: Type<any>) {

packages/core/test/acceptance/bootstrap_spec.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, NgModule, NgZone, TestabilityRegistry, ViewEncapsulation} from '@angular/core';
9+
import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, forwardRef, NgModule, NgZone, TestabilityRegistry, ViewEncapsulation} from '@angular/core';
1010
import {BrowserModule} from '@angular/platform-browser';
1111
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
1212
import {withBody} from '@angular/private/testing';
@@ -224,6 +224,71 @@ describe('bootstrap', () => {
224224
expect(appRef.components.length).toBe(0);
225225
expect(testabilityRegistry.getAllRootElements().length).toBe(0);
226226
}));
227+
228+
it('should throw when standalone component is used in @NgModule.bootstrap',
229+
withBody('<my-app></my-app>', async () => {
230+
@Component({
231+
standalone: true,
232+
selector: 'standalone-comp',
233+
template: '...',
234+
})
235+
class StandaloneComponent {
236+
}
237+
238+
@NgModule({
239+
bootstrap: [StandaloneComponent],
240+
})
241+
class MyModule {
242+
}
243+
244+
try {
245+
await platformBrowserDynamic().bootstrapModule(MyModule);
246+
247+
// This test tries to bootstrap a standalone component using NgModule-based bootstrap
248+
// mechanisms. We expect standalone components to be bootstrapped via
249+
// `bootstrapApplication` API instead.
250+
fail('Expected to throw');
251+
} catch (e: unknown) {
252+
const expectedErrorMessage =
253+
'The `StandaloneComponent` class is a standalone component, ' +
254+
'which can not be used in the `@NgModule.bootstrap` array.';
255+
expect(e).toBeInstanceOf(Error);
256+
expect((e as Error).message).toContain(expectedErrorMessage);
257+
}
258+
}));
259+
260+
it('should throw when standalone component wrapped in `forwardRef` is used in @NgModule.bootstrap',
261+
withBody('<my-app></my-app>', async () => {
262+
@Component({
263+
standalone: true,
264+
selector: 'standalone-comp',
265+
template: '...',
266+
})
267+
class StandaloneComponent {
268+
}
269+
270+
@NgModule({
271+
bootstrap: [forwardRef(() => StandaloneComponent)],
272+
})
273+
class MyModule {
274+
}
275+
276+
try {
277+
await platformBrowserDynamic().bootstrapModule(MyModule);
278+
279+
// This test tries to bootstrap a standalone component using NgModule-based bootstrap
280+
// mechanisms. We expect standalone components to be bootstrapped via
281+
// `bootstrapApplication` API instead.
282+
fail('Expected to throw');
283+
} catch (e: unknown) {
284+
const expectedErrorMessage =
285+
'The `StandaloneComponent` class is a standalone component, which ' +
286+
'can not be used in the `@NgModule.bootstrap` array. Use the `bootstrapApplication` ' +
287+
'function for bootstrap instead.';
288+
expect(e).toBeInstanceOf(Error);
289+
expect((e as Error).message).toContain(expectedErrorMessage);
290+
}
291+
}));
227292
});
228293

229294
describe('PlatformRef cleanup', () => {

0 commit comments

Comments
 (0)