Skip to content

Commit aafac72

Browse files
pkozlowski-opensourcedylhunn
authored andcommitted
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
1 parent 58e8f4b commit aafac72

File tree

4 files changed

+197
-11
lines changed

4 files changed

+197
-11
lines changed

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {getCompilerFacade, JitCompilerUsage, R3DirectiveMetadataFacade} from '../../compiler/compiler_facade';
1010
import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface';
11-
import {resolveForwardRef} from '../../di/forward_ref';
11+
import {isForwardRef, resolveForwardRef} from '../../di/forward_ref';
1212
import {getReflect, reflectDependencies} from '../../di/jit/util';
1313
import {Type} from '../../interface/type';
1414
import {Query} from '../../metadata/di';
@@ -26,6 +26,7 @@ import {stringifyForError} from '../util/stringify_utils';
2626
import {angularCoreEnv} from './environment';
2727
import {getJitOptions} from './jit_options';
2828
import {flushModuleScopingQueueAsMuchAsPossible, patchComponentDefWithScope, transitiveScopesFor} from './module';
29+
import {isModuleWithProviders} from './util';
2930

3031
/**
3132
* 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 {
186187
});
187188
}
188189

190+
function getDependencyTypeForError(type: Type<any>) {
191+
if (getComponentDef(type)) return 'component';
192+
if (getDirectiveDef(type)) return 'directive';
193+
if (getPipeDef(type)) return 'pipe';
194+
return 'type';
195+
}
196+
197+
function verifyStandaloneImport(depType: Type<unknown>, importingType: Type<unknown>) {
198+
if (isForwardRef(depType)) {
199+
depType = resolveForwardRef(depType);
200+
if (!depType) {
201+
throw new Error(`Expected forwardRef function, imported from "${
202+
stringifyForError(importingType)}", to return a standalone entity or NgModule but got "${
203+
stringifyForError(depType) || depType}".`);
204+
}
205+
}
206+
207+
if (getNgModuleDef(depType) == null) {
208+
const def = getComponentDef(depType) || getDirectiveDef(depType) || getPipeDef(depType);
209+
if (def != null) {
210+
// if a component, directive or pipe is imported make sure that it is standalone
211+
if (!def.standalone) {
212+
throw new Error(`The "${stringifyForError(depType)}" ${
213+
getDependencyTypeForError(depType)}, imported from "${
214+
stringifyForError(
215+
importingType)}", is not standalone. Did you forget to add the standalone: true flag?`);
216+
}
217+
} else {
218+
// it can be either a module with provider or an unknown (not annotated) type
219+
if (isModuleWithProviders(depType)) {
220+
throw new Error(`A module with providers was imported from "${
221+
stringifyForError(
222+
importingType)}". Modules with providers are not supported in standalone components imports.`);
223+
} else {
224+
throw new Error(`The "${stringifyForError(depType)}" type, imported from "${
225+
stringifyForError(
226+
importingType)}", must be a standalone component / directive / pipe or an NgModule. Did you forget to add the required @Component / @Directive / @Pipe or @NgModule annotation?`);
227+
}
228+
}
229+
}
230+
}
231+
189232
/**
190233
* Build memoized `directiveDefs` and `pipeDefs` functions for the component definition of a
191234
* 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>[]): {
205248
cachedDirectiveDefs = [getComponentDef(type)!];
206249

207250
for (const rawDep of imports) {
208-
const dep = resolveForwardRef(rawDep);
251+
ngDevMode && verifyStandaloneImport(rawDep, type);
209252

253+
const dep = resolveForwardRef(rawDep);
210254
if (!!getNgModuleDef(dep)) {
211255
const scope = transitiveScopesFor(dep);
212256
for (const dir of scope.exported.directives) {

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {stringifyForError} from '../util/stringify_utils';
2727

2828
import {angularCoreEnv} from './environment';
2929
import {patchModuleCompilation} from './module_patch';
30+
import {isModuleWithProviders, isNgModule} from './util';
3031

3132
interface ModuleQueueItem {
3233
moduleType: Type<any>;
@@ -610,11 +611,3 @@ function expandModuleWithProviders(value: Type<any>|ModuleWithProviders<{}>): Ty
610611
}
611612
return value;
612613
}
613-
614-
function isModuleWithProviders(value: any): value is ModuleWithProviders<{}> {
615-
return (value as {ngModule?: any}).ngModule !== undefined;
616-
}
617-
618-
function isNgModule<T>(value: Type<T>): value is Type<T>&{ɵmod: NgModuleDef<T>} {
619-
return !!getNgModuleDef(value);
620-
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ModuleWithProviders} from '../../di/interface/provider';
10+
import {Type} from '../../interface/type';
11+
import {NgModuleDef} from '../../metadata/ng_module_def';
12+
import {getNgModuleDef} from '../definition';
13+
14+
export function isModuleWithProviders(value: any): value is ModuleWithProviders<{}> {
15+
return (value as {ngModule?: any}).ngModule !== undefined;
16+
}
17+
18+
export function isNgModule<T>(value: Type<T>): value is Type<T>&{ɵmod: NgModuleDef<T>} {
19+
return !!getNgModuleDef(value);
20+
}

packages/core/test/acceptance/standalone_spec.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,6 @@ describe('standalone components, directives and pipes', () => {
412412
@NgModule({exports: [ModuleWithAService]})
413413
class ExportingModule {
414414
}
415-
416415
@Component({
417416
selector: 'standalone',
418417
standalone: true,
@@ -428,6 +427,136 @@ describe('standalone components, directives and pipes', () => {
428427
expect(fixture.nativeElement.textContent).toBe('(service)');
429428
});
430429

430+
it('should error when forwardRef does not resolve to a truthy value', () => {
431+
@Component({
432+
selector: 'test',
433+
standalone: true,
434+
imports: [forwardRef(() => null)],
435+
template: '',
436+
})
437+
class TestComponent {
438+
}
439+
expect(() => {
440+
TestBed.createComponent(TestComponent);
441+
})
442+
.toThrowError(
443+
'Expected forwardRef function, imported from "TestComponent", to return a standalone entity or NgModule but got "null".');
444+
});
445+
446+
it('should error when a non-standalone component is imported', () => {
447+
@Component({
448+
selector: 'not-a-standalone',
449+
template: '',
450+
})
451+
class NonStandaloneCmp {
452+
}
453+
454+
@Component({
455+
selector: 'standalone',
456+
standalone: true,
457+
template: '',
458+
imports: [NonStandaloneCmp],
459+
})
460+
class StandaloneCmp {
461+
}
462+
463+
expect(() => {
464+
TestBed.createComponent(StandaloneCmp);
465+
})
466+
.toThrowError(
467+
'The "NonStandaloneCmp" component, imported from "StandaloneCmp", is not standalone. Did you forget to add the standalone: true flag?');
468+
});
469+
470+
it('should error when a non-standalone directive is imported', () => {
471+
@Directive({selector: '[not-a-standalone]'})
472+
class NonStandaloneDirective {
473+
}
474+
475+
@Component({
476+
selector: 'standalone',
477+
standalone: true,
478+
template: '',
479+
imports: [NonStandaloneDirective],
480+
})
481+
class StandaloneCmp {
482+
}
483+
484+
expect(() => {
485+
TestBed.createComponent(StandaloneCmp);
486+
})
487+
.toThrowError(
488+
'The "NonStandaloneDirective" directive, imported from "StandaloneCmp", is not standalone. Did you forget to add the standalone: true flag?');
489+
});
490+
491+
it('should error when a non-standalone pipe is imported', () => {
492+
@Pipe({name: 'not-a-standalone'})
493+
class NonStandalonePipe {
494+
}
495+
496+
@Component({
497+
selector: 'standalone',
498+
standalone: true,
499+
template: '',
500+
imports: [NonStandalonePipe],
501+
})
502+
class StandaloneCmp {
503+
}
504+
505+
expect(() => {
506+
TestBed.createComponent(StandaloneCmp);
507+
})
508+
.toThrowError(
509+
'The "NonStandalonePipe" pipe, imported from "StandaloneCmp", is not standalone. Did you forget to add the standalone: true flag?');
510+
});
511+
512+
it('should error when an unknown type is imported', () => {
513+
class SthElse {}
514+
515+
@Component({
516+
selector: 'standalone',
517+
standalone: true,
518+
template: '',
519+
imports: [SthElse],
520+
})
521+
class StandaloneCmp {
522+
}
523+
524+
expect(() => {
525+
TestBed.createComponent(StandaloneCmp);
526+
})
527+
.toThrowError(
528+
'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?');
529+
});
530+
531+
it('should error when a module with providers is imported', () => {
532+
@NgModule()
533+
class OtherModule {
534+
}
535+
536+
@NgModule()
537+
class LibModule {
538+
static forComponent() {
539+
return {ngModule: OtherModule};
540+
}
541+
}
542+
543+
@Component({
544+
standalone: true,
545+
template: '',
546+
// we need to import a module with a provider in a nested array since module with providers
547+
// are disallowed on the type level
548+
imports: [[LibModule.forComponent()]],
549+
})
550+
class StandaloneCmp {
551+
}
552+
553+
expect(() => {
554+
TestBed.createComponent(StandaloneCmp);
555+
})
556+
.toThrowError(
557+
'A module with providers was imported from "StandaloneCmp". Modules with providers are not supported in standalone components imports.');
558+
});
559+
431560
it('should support forwardRef imports', () => {
432561
@Component({
433562
selector: 'test',

0 commit comments

Comments
 (0)