Skip to content

Commit

Permalink
fix(core): throw if standalone components are present in `@NgModule.b…
Browse files Browse the repository at this point in the history
…ootstrap` (#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
  • Loading branch information
AndrewKushnir authored and dylhunn committed May 2, 2022
1 parent 9a04ded commit fde4942
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 2 deletions.
1 change: 1 addition & 0 deletions goldens/public-api/compiler-cli/error_code.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export enum ErrorCode {
INVALID_BANANA_IN_BOX = 8101,
MISSING_PIPE = 8004,
MISSING_REFERENCE_TARGET = 8003,
NGMODULE_BOOTSTRAP_IS_STANDALONE = 6009,
NGMODULE_DECLARATION_IS_STANDALONE = 6008,
NGMODULE_DECLARATION_NOT_UNIQUE = 6007,
NGMODULE_INVALID_DECLARATION = 6001,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {DynamicValue, PartialEvaluator, ResolvedValue, SyntheticValue} from '../
import {PerfEvent, PerfRecorder} from '../../../perf';
import {ClassDeclaration, Decorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../../reflection';
import {LocalModuleScopeRegistry, ScopeData} from '../../../scope';
import {getDiagnosticNode} from '../../../scope/src/util';
import {FactoryTracker} from '../../../shims/api';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform';
import {getSourceFile} from '../../../util/src/typescript';
Expand Down Expand Up @@ -235,6 +236,14 @@ export class NgModuleDecoratorHandler implements
const expr = ngModule.get('bootstrap')!;
const bootstrapMeta = this.evaluator.evaluate(expr, forwardRefResolver);
bootstrapRefs = this.resolveTypeList(expr, bootstrapMeta, name, 'bootstrap', 0).references;

// Verify that the `@NgModule.bootstrap` list doesn't have Standalone Components.
for (const ref of bootstrapRefs) {
const dirMeta = this.metaReader.getDirectiveMetadata(ref);
if (dirMeta?.isStandalone) {
diagnostics.push(makeStandaloneBootstrapDiagnostic(node, ref, expr));
}
}
}

const schemas = ngModule.has('schemas') ?
Expand Down Expand Up @@ -834,3 +843,24 @@ function isResolvedModuleWithProviders(sv: SyntheticValue<unknown>):
sv.value.hasOwnProperty('ngModule' as keyof ResolvedModuleWithProviders) &&
sv.value.hasOwnProperty('mwpCall' as keyof ResolvedModuleWithProviders);
}

/**
* Helper method to produce a diagnostics for a situation when a standalone component
* is referenced in the `@NgModule.bootstrap` array.
*/
function makeStandaloneBootstrapDiagnostic(
ngModuleClass: ClassDeclaration, bootstrappedClassRef: Reference<ClassDeclaration>,
rawBootstrapExpr: ts.Expression|null): ts.Diagnostic {
const componentClassName = bootstrappedClassRef.node.name.text;
// Note: this error message should be aligned with the one produced by JIT.
const message = //
`The \`${componentClassName}\` class is a standalone component, which can ` +
`not be used in the \`@NgModule.bootstrap\` array. Use the \`bootstrapApplication\` ` +
`function for bootstrap instead.`;
const relatedInformation: ts.DiagnosticRelatedInformation[]|undefined =
[makeRelatedInformation(ngModuleClass, `The 'bootstrap' array is present on this NgModule.`)];

return makeDiagnostic(
ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE,
getDiagnosticNode(bootstrappedClassRef, rawBootstrapExpr), message, relatedInformation);
}
5 changes: 5 additions & 0 deletions packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ export enum ErrorCode {
*/
NGMODULE_DECLARATION_IS_STANDALONE = 6008,

/**
* Raised when a standalone component is part of the bootstrap list of an NgModule.
*/
NGMODULE_BOOTSTRAP_IS_STANDALONE = 6009,

/**
* Indicates that an NgModule is declared with `id: module.id`. This is an anti-pattern that is
* disabled explicitly in the compiler, that was originally based on a misunderstanding of
Expand Down
49 changes: 49 additions & 0 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7658,6 +7658,55 @@ function allTests(os: string) {
expect(codes).toEqual([ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX)]);
});

it('should produce an error when standalone component is used in @NgModule.bootstrap', () => {
env.tsconfig();

env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
standalone: true,
selector: 'standalone-component',
template: '...',
})
class StandaloneComponent {}
@NgModule({
bootstrap: [StandaloneComponent]
})
class BootstrapModule {}
`);

const diagnostics = env.driveDiagnostics();
const codes = diagnostics.map((diag) => diag.code);
expect(codes).toEqual([ngErrorCode(ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE)]);
});

it('should produce an error when standalone component wrapped in `forwardRef` is used in @NgModule.bootstrap',
() => {
env.tsconfig();

env.write('test.ts', `
import {Component, NgModule, forwardRef} from '@angular/core';
@Component({
standalone: true,
selector: 'standalone-component',
template: '...',
})
class StandaloneComponent {}
@NgModule({
bootstrap: [forwardRef(() => StandaloneComponent)]
})
class BootstrapModule {}
`);

const diagnostics = env.driveDiagnostics();
const codes = diagnostics.map((diag) => diag.code);
expect(codes).toEqual([ngErrorCode(ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE)]);
});

describe('InjectorDef emit optimizations for standalone', () => {
it('should not filter components out of NgModule.imports', () => {
env.write('test.ts', `
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/render3/jit/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ function verifySemanticsOfNgModuleDef(
function verifyComponentIsPartOfNgModule(type: Type<any>) {
type = resolveForwardRef(type);
const existingModule = ownerNgModule.get(type);
if (!existingModule) {
if (!existingModule && !isStandalone(type)) {
errors.push(`Component ${
stringifyForError(
type)} is not part of any NgModule or the module has not been imported into your module.`);
Expand All @@ -338,6 +338,14 @@ function verifySemanticsOfNgModuleDef(
if (!getComponentDef(type)) {
errors.push(`${stringifyForError(type)} cannot be used as an entry component.`);
}
if (isStandalone(type)) {
// Note: this error should be the same as the
// `NGMODULE_BOOTSTRAP_IS_STANDALONE` one in AOT compiler.
errors.push(
`The \`${stringifyForError(type)}\` class is a standalone component, which can ` +
`not be used in the \`@NgModule.bootstrap\` array. Use the \`bootstrapApplication\` ` +
`function for bootstrap instead.`);
}
}

function verifyComponentEntryComponentsIsPartOfNgModule(type: Type<any>) {
Expand Down
67 changes: 66 additions & 1 deletion packages/core/test/acceptance/bootstrap_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, NgModule, NgZone, TestabilityRegistry, ViewEncapsulation} from '@angular/core';
import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, forwardRef, NgModule, NgZone, TestabilityRegistry, ViewEncapsulation} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {withBody} from '@angular/private/testing';
Expand Down Expand Up @@ -224,6 +224,71 @@ describe('bootstrap', () => {
expect(appRef.components.length).toBe(0);
expect(testabilityRegistry.getAllRootElements().length).toBe(0);
}));

it('should throw when standalone component is used in @NgModule.bootstrap',
withBody('<my-app></my-app>', async () => {
@Component({
standalone: true,
selector: 'standalone-comp',
template: '...',
})
class StandaloneComponent {
}

@NgModule({
bootstrap: [StandaloneComponent],
})
class MyModule {
}

try {
await platformBrowserDynamic().bootstrapModule(MyModule);

// This test tries to bootstrap a standalone component using NgModule-based bootstrap
// mechanisms. We expect standalone components to be bootstrapped via
// `bootstrapApplication` API instead.
fail('Expected to throw');
} catch (e: unknown) {
const expectedErrorMessage =
'The `StandaloneComponent` class is a standalone component, ' +
'which can not be used in the `@NgModule.bootstrap` array.';
expect(e).toBeInstanceOf(Error);
expect((e as Error).message).toContain(expectedErrorMessage);
}
}));

it('should throw when standalone component wrapped in `forwardRef` is used in @NgModule.bootstrap',
withBody('<my-app></my-app>', async () => {
@Component({
standalone: true,
selector: 'standalone-comp',
template: '...',
})
class StandaloneComponent {
}

@NgModule({
bootstrap: [forwardRef(() => StandaloneComponent)],
})
class MyModule {
}

try {
await platformBrowserDynamic().bootstrapModule(MyModule);

// This test tries to bootstrap a standalone component using NgModule-based bootstrap
// mechanisms. We expect standalone components to be bootstrapped via
// `bootstrapApplication` API instead.
fail('Expected to throw');
} catch (e: unknown) {
const expectedErrorMessage =
'The `StandaloneComponent` class is a standalone component, which ' +
'can not be used in the `@NgModule.bootstrap` array. Use the `bootstrapApplication` ' +
'function for bootstrap instead.';
expect(e).toBeInstanceOf(Error);
expect((e as Error).message).toContain(expectedErrorMessage);
}
}));
});

describe('PlatformRef cleanup', () => {
Expand Down

0 comments on commit fde4942

Please sign in to comment.