diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index f4740cfb851..88ee48941e1 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -8,7 +8,7 @@ import {getCompilerFacade, JitCompilerUsage, R3DirectiveMetadataFacade} from '../../compiler/compiler_facade'; import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface'; -import {resolveForwardRef} from '../../di/forward_ref'; +import {isForwardRef, resolveForwardRef} from '../../di/forward_ref'; import {getReflect, reflectDependencies} from '../../di/jit/util'; import {Type} from '../../interface/type'; import {Query} from '../../metadata/di'; @@ -26,6 +26,7 @@ import {stringifyForError} from '../util/stringify_utils'; import {angularCoreEnv} from './environment'; import {getJitOptions} from './jit_options'; import {flushModuleScopingQueueAsMuchAsPossible, patchComponentDefWithScope, transitiveScopesFor} from './module'; +import {isModuleWithProviders} from './util'; /** * Keep track of the compilation depth to avoid reentrancy issues during JIT compilation. This @@ -186,6 +187,48 @@ export function compileComponent(type: Type, metadata: Component): void { }); } +function getDependencyTypeForError(type: Type) { + if (getComponentDef(type)) return 'component'; + if (getDirectiveDef(type)) return 'directive'; + if (getPipeDef(type)) return 'pipe'; + return 'type'; +} + +function verifyStandaloneImport(depType: Type, importingType: Type) { + if (isForwardRef(depType)) { + depType = resolveForwardRef(depType); + if (!depType) { + throw new Error(`Expected forwardRef function, imported from "${ + stringifyForError(importingType)}", to return a standalone entity or NgModule but got "${ + stringifyForError(depType) || depType}".`); + } + } + + if (getNgModuleDef(depType) == null) { + const def = getComponentDef(depType) || getDirectiveDef(depType) || getPipeDef(depType); + if (def != null) { + // if a component, directive or pipe is imported make sure that it is standalone + if (!def.standalone) { + throw new Error(`The "${stringifyForError(depType)}" ${ + getDependencyTypeForError(depType)}, imported from "${ + stringifyForError( + importingType)}", is not standalone. Did you forget to add the standalone: true flag?`); + } + } else { + // it can be either a module with provider or an unknown (not annotated) type + if (isModuleWithProviders(depType)) { + throw new Error(`A module with providers was imported from "${ + stringifyForError( + importingType)}". Modules with providers are not supported in standalone components imports.`); + } else { + throw new Error(`The "${stringifyForError(depType)}" type, imported from "${ + stringifyForError( + importingType)}", must be a standalone component / directive / pipe or an NgModule. Did you forget to add the required @Component / @Directive / @Pipe or @NgModule annotation?`); + } + } + } +} + /** * Build memoized `directiveDefs` and `pipeDefs` functions for the component definition of a * standalone component, which process `imports` and filter out directives and pipes. The use of @@ -205,8 +248,9 @@ function getStandaloneDefFunctions(type: Type, imports: Type[]): { cachedDirectiveDefs = [getComponentDef(type)!]; for (const rawDep of imports) { - const dep = resolveForwardRef(rawDep); + ngDevMode && verifyStandaloneImport(rawDep, type); + const dep = resolveForwardRef(rawDep); if (!!getNgModuleDef(dep)) { const scope = transitiveScopesFor(dep); for (const dir of scope.exported.directives) { diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index f31bf3d14dd..f039b5d89ea 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -27,6 +27,7 @@ import {stringifyForError} from '../util/stringify_utils'; import {angularCoreEnv} from './environment'; import {patchModuleCompilation} from './module_patch'; +import {isModuleWithProviders, isNgModule} from './util'; interface ModuleQueueItem { moduleType: Type; @@ -610,11 +611,3 @@ function expandModuleWithProviders(value: Type|ModuleWithProviders<{}>): Ty } return value; } - -function isModuleWithProviders(value: any): value is ModuleWithProviders<{}> { - return (value as {ngModule?: any}).ngModule !== undefined; -} - -function isNgModule(value: Type): value is Type&{ɵmod: NgModuleDef} { - return !!getNgModuleDef(value); -} diff --git a/packages/core/src/render3/jit/util.ts b/packages/core/src/render3/jit/util.ts new file mode 100644 index 00000000000..e41f3910272 --- /dev/null +++ b/packages/core/src/render3/jit/util.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ModuleWithProviders} from '../../di/interface/provider'; +import {Type} from '../../interface/type'; +import {NgModuleDef} from '../../metadata/ng_module_def'; +import {getNgModuleDef} from '../definition'; + +export function isModuleWithProviders(value: any): value is ModuleWithProviders<{}> { + return (value as {ngModule?: any}).ngModule !== undefined; +} + +export function isNgModule(value: Type): value is Type&{ɵmod: NgModuleDef} { + return !!getNgModuleDef(value); +} diff --git a/packages/core/test/acceptance/standalone_spec.ts b/packages/core/test/acceptance/standalone_spec.ts index 452ea614e48..7f303bf21dc 100644 --- a/packages/core/test/acceptance/standalone_spec.ts +++ b/packages/core/test/acceptance/standalone_spec.ts @@ -412,7 +412,6 @@ describe('standalone components, directives and pipes', () => { @NgModule({exports: [ModuleWithAService]}) class ExportingModule { } - @Component({ selector: 'standalone', standalone: true, @@ -428,6 +427,136 @@ describe('standalone components, directives and pipes', () => { expect(fixture.nativeElement.textContent).toBe('(service)'); }); + it('should error when forwardRef does not resolve to a truthy value', () => { + @Component({ + selector: 'test', + standalone: true, + imports: [forwardRef(() => null)], + template: '', + }) + class TestComponent { + } + expect(() => { + TestBed.createComponent(TestComponent); + }) + .toThrowError( + 'Expected forwardRef function, imported from "TestComponent", to return a standalone entity or NgModule but got "null".'); + }); + + it('should error when a non-standalone component is imported', () => { + @Component({ + selector: 'not-a-standalone', + template: '', + }) + class NonStandaloneCmp { + } + + @Component({ + selector: 'standalone', + standalone: true, + template: '', + imports: [NonStandaloneCmp], + }) + class StandaloneCmp { + } + + expect(() => { + TestBed.createComponent(StandaloneCmp); + }) + .toThrowError( + 'The "NonStandaloneCmp" component, imported from "StandaloneCmp", is not standalone. Did you forget to add the standalone: true flag?'); + }); + + it('should error when a non-standalone directive is imported', () => { + @Directive({selector: '[not-a-standalone]'}) + class NonStandaloneDirective { + } + + @Component({ + selector: 'standalone', + standalone: true, + template: '', + imports: [NonStandaloneDirective], + }) + class StandaloneCmp { + } + + expect(() => { + TestBed.createComponent(StandaloneCmp); + }) + .toThrowError( + 'The "NonStandaloneDirective" directive, imported from "StandaloneCmp", is not standalone. Did you forget to add the standalone: true flag?'); + }); + + it('should error when a non-standalone pipe is imported', () => { + @Pipe({name: 'not-a-standalone'}) + class NonStandalonePipe { + } + + @Component({ + selector: 'standalone', + standalone: true, + template: '', + imports: [NonStandalonePipe], + }) + class StandaloneCmp { + } + + expect(() => { + TestBed.createComponent(StandaloneCmp); + }) + .toThrowError( + 'The "NonStandalonePipe" pipe, imported from "StandaloneCmp", is not standalone. Did you forget to add the standalone: true flag?'); + }); + + it('should error when an unknown type is imported', () => { + class SthElse {} + + @Component({ + selector: 'standalone', + standalone: true, + template: '', + imports: [SthElse], + }) + class StandaloneCmp { + } + + expect(() => { + TestBed.createComponent(StandaloneCmp); + }) + .toThrowError( + 'The "SthElse" type, imported from "StandaloneCmp", must be a standalone component / directive / pipe or an NgModule. Did you forget to add the required @Component / @Directive / @Pipe or @NgModule annotation?'); + }); + + it('should error when a module with providers is imported', () => { + @NgModule() + class OtherModule { + } + + @NgModule() + class LibModule { + static forComponent() { + return {ngModule: OtherModule}; + } + } + + @Component({ + standalone: true, + template: '', + // we need to import a module with a provider in a nested array since module with providers + // are disallowed on the type level + imports: [[LibModule.forComponent()]], + }) + class StandaloneCmp { + } + + expect(() => { + TestBed.createComponent(StandaloneCmp); + }) + .toThrowError( + 'A module with providers was imported from "StandaloneCmp". Modules with providers are not supported in standalone components imports.'); + }); + it('should support forwardRef imports', () => { @Component({ selector: 'test',