fix(core): verify standalone component imports in JiT (#45777)

This commits adds verifications assuring that items imported into
standalone components are one of:
- standalone component / directive / pipe;
- NgModule;
- forwardRef resolving to one of the above.
It explicitly disallows modules with providers.

PR Close #45777
This commit is contained in:
Pawel Kozlowski
2022-04-21 11:04:04 +02:00
committed by Dylan Hunn
parent 58e8f4b708
commit aafac7228f
4 changed files with 197 additions and 11 deletions
+46 -2
View File
@@ -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<any>, metadata: Component): void {
});
}
function getDependencyTypeForError(type: Type<any>) {
if (getComponentDef(type)) return 'component';
if (getDirectiveDef(type)) return 'directive';
if (getPipeDef(type)) return 'pipe';
return 'type';
}
function verifyStandaloneImport(depType: Type<unknown>, importingType: Type<unknown>) {
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<any>, imports: Type<any>[]): {
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) {
+1 -8
View File
@@ -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<any>;
@@ -610,11 +611,3 @@ function expandModuleWithProviders(value: Type<any>|ModuleWithProviders<{}>): Ty
}
return value;
}
function isModuleWithProviders(value: any): value is ModuleWithProviders<{}> {
return (value as {ngModule?: any}).ngModule !== undefined;
}
function isNgModule<T>(value: Type<T>): value is Type<T>&{ɵmod: NgModuleDef<T>} {
return !!getNgModuleDef(value);
}
+20
View File
@@ -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<T>(value: Type<T>): value is Type<T>&{ɵmod: NgModuleDef<T>} {
return !!getNgModuleDef(value);
}
@@ -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',