diff --git a/goldens/circular-deps/packages.json b/goldens/circular-deps/packages.json index c9e6612e2d6..903dc3160eb 100644 --- a/goldens/circular-deps/packages.json +++ b/goldens/circular-deps/packages.json @@ -81,10 +81,6 @@ "packages/core/src/linker/component_factory.ts", "packages/core/src/linker/ng_module_factory.ts" ], - [ - "packages/core/src/linker/component_factory_resolver.ts", - "packages/core/src/linker/ng_module_factory.ts" - ], [ "packages/core/src/metadata/directives.ts", "packages/core/src/render3/jit/directive.ts" diff --git a/goldens/public-api/core/index.md b/goldens/public-api/core/index.md index f070eb78942..1dec55ef901 100644 --- a/goldens/public-api/core/index.md +++ b/goldens/public-api/core/index.md @@ -181,9 +181,13 @@ export interface Component extends Directive { encapsulation?: ViewEncapsulation; // @deprecated entryComponents?: Array | any[]>; + // (undocumented) + imports?: (Type | any[])[]; interpolation?: [string, string]; moduleId?: string; preserveWhitespaces?: boolean; + // (undocumented) + standalone?: boolean; styles?: string[]; styleUrls?: string[]; template?: string; @@ -407,6 +411,8 @@ export interface Directive { [key: string]: any; }; selector?: string; + // (undocumented) + standalone?: boolean; } // @public @@ -954,6 +960,8 @@ export const PACKAGE_ROOT_URL: InjectionToken; export interface Pipe { name: string; pure?: boolean; + // (undocumented) + standalone?: boolean; } // @public (undocumented) diff --git a/packages/core/src/metadata/directives.ts b/packages/core/src/metadata/directives.ts index 6d1977badc6..ad989a2fc48 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -287,6 +287,8 @@ export interface Directive { * To ensure the correct behavior, the app must import `@angular/compiler`. */ jit?: true; + + standalone?: boolean; } /** @@ -553,6 +555,11 @@ export interface Component extends Directive { * overridden in compiler options. */ preserveWhitespaces?: boolean; + + standalone?: boolean; + + // TODO: better type here! + imports?: (Type|any[])[]; } /** @@ -624,6 +631,8 @@ export interface Pipe { * even if the arguments have not changed. */ pure?: boolean; + + standalone?: boolean; } /** diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 80e48132fd3..ac9d8862f45 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -7,7 +7,7 @@ */ import {getCompilerFacade, JitCompilerUsage, R3DirectiveMetadataFacade} from '../../compiler/compiler_facade'; -import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface'; +import {R3ComponentMetadataFacade, R3QueryMetadataFacade, R3TemplateDependencyFacade, R3TemplateDependencyKind} from '../../compiler/compiler_facade_interface'; import {resolveForwardRef} from '../../di/forward_ref'; import {getReflect, reflectDependencies} from '../../di/jit/util'; import {Type} from '../../interface/type'; @@ -15,9 +15,11 @@ import {Query} from '../../metadata/di'; import {Component, Directive, Input} from '../../metadata/directives'; import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading'; import {ViewEncapsulation} from '../../metadata/view'; +import {NgModuleDef} from '../../r3_symbols'; +import {flatten} from '../../util/array_utils'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../../util/empty'; import {initNgDevMode} from '../../util/ng_dev_mode'; -import {getComponentDef, getDirectiveDef} from '../definition'; +import {getComponentDef, getDirectiveDef, getNgModuleDef, getPipeDef} from '../definition'; import {NG_COMP_DEF, NG_DIR_DEF, NG_FACTORY_DEF} from '../fields'; import {ComponentType} from '../interfaces/definition'; import {stringifyForError} from '../util/stringify_utils'; @@ -105,6 +107,26 @@ export function compileComponent(type: Type, metadata: Component): void { } } + let declarations: R3TemplateDependencyFacade[]; + + if (metadata.standalone) { + // Standalone components always have themselves in scope. + declarations = [{ + kind: R3TemplateDependencyKind.Directive, + type, + }]; + + // And might have other dependencies in scope, depending on `imports`. + if (metadata.imports) { + declarations.push(...getStandaloneDependencies(flatten(metadata.imports))); + } + } else { + // NgModule-based components are processed first with empty `declarations`, since when the + // component is evaluated the NgModule has not yet been executed. The real scope for the + // component will be patched on after the NgModule is fully evaluated. + declarations = []; + } + const templateUrl = metadata.templateUrl || `ng:///${type.name}/template.html`; const meta: R3ComponentMetadataFacade = { ...directiveMetadata(type, metadata), @@ -113,11 +135,12 @@ export function compileComponent(type: Type, metadata: Component): void { preserveWhitespaces, styles: metadata.styles || EMPTY_ARRAY, animations: metadata.animations, - declarations: [], + declarations, changeDetection: metadata.changeDetection, encapsulation, interpolation: metadata.interpolation, viewProviders: metadata.viewProviders || null, + isStandalone: !!metadata.standalone, }; compilationDepth++; @@ -156,6 +179,54 @@ export function compileComponent(type: Type, metadata: Component): void { }); } +function getStandaloneDependencies(imports: Type[]): R3TemplateDependencyFacade[] { + const dependencies: R3TemplateDependencyFacade[] = []; + for (const rawDep of imports) { + const dep = resolveForwardRef(rawDep); + if (!dep) { + // TODO: real error + throw new Error(`ForwardRef issue?`); + } + const ngModuleDef: NgModuleDef|null = getNgModuleDef(dep); + if (ngModuleDef) { + dependencies.push({ + kind: R3TemplateDependencyKind.NgModule, + type: dep as any, + } as R3TemplateDependencyFacade); + + const scopes = transitiveScopesFor(ngModuleDef.type); + for (const dir of scopes.exported.directives) { + dependencies.push({ + kind: R3TemplateDependencyKind.Directive, + type: dir, + } as R3TemplateDependencyFacade); + } + + for (const dir of scopes.exported.pipes) { + dependencies.push({ + kind: R3TemplateDependencyKind.Pipe, + type: dir, + } as R3TemplateDependencyFacade); + } + } + + const dirDef = getComponentDef(dep) || getDirectiveDef(dep); + const anyDef = dirDef || getPipeDef(dep); + if (anyDef) { + if (!anyDef.standalone) { + // TODO: real error + throw new Error(`What are you doing? You imported a non-standalone thing!`); + } + + dependencies.push({ + kind: dirDef ? R3TemplateDependencyKind.Directive : R3TemplateDependencyKind.Pipe, + type: dep, + } as R3TemplateDependencyFacade); + } + } + return dependencies; +} + function hasSelectorScope(component: Type): component is Type& {ngSelectorScope: Type} { return (component as {ngSelectorScope?: any}).ngSelectorScope !== undefined; @@ -256,9 +327,7 @@ export function directiveMetadata(type: Type, metadata: Directive): R3Direc exportAs: extractExportAs(metadata.exportAs), providers: metadata.providers || null, viewQueries: extractQueriesMetadata(type, propMetadata, isViewQuery), - // TODO(alxhub): pass through the standalone flag from the directive metadata once standalone - // functionality is fully rolled out. - isStandalone: false, + isStandalone: !!metadata.standalone, }; } diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index fc3643dc603..8731c94d2bc 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -195,10 +195,19 @@ export function compileNgModuleDefs( }); } +function isStandalone(type: Type) { + const def = getComponentDef(type) || getDirectiveDef(type) || getPipeDef(type); + return def !== null ? def.standalone : false; +} + function verifySemanticsOfNgModuleDef( moduleType: NgModuleType, allowDuplicateDeclarationsInRoot: boolean, importingModule?: NgModuleType): void { if (verifiedNgModule.get(moduleType)) return; + + // skip verifications of standalone components, direcrtives and pipes + if (isStandalone(moduleType)) return; + verifiedNgModule.set(moduleType, true); moduleType = resolveForwardRef(moduleType); let ngModuleDef: NgModuleDef; @@ -214,9 +223,9 @@ function verifySemanticsOfNgModuleDef( const errors: string[] = []; const declarations = maybeUnwrapFn(ngModuleDef.declarations); const imports = maybeUnwrapFn(ngModuleDef.imports); - flatten(imports).map(unwrapModuleWithProvidersImports).forEach(mod => { - verifySemanticsOfNgModuleImport(mod, moduleType); - verifySemanticsOfNgModuleDef(mod, false, moduleType); + flatten(imports).map(unwrapModuleWithProvidersImports).forEach(modOrStandaloneCmpt => { + verifySemanticsOfNgModuleImport(modOrStandaloneCmpt, moduleType); + verifySemanticsOfNgModuleDef(modOrStandaloneCmpt, false, moduleType); }); const exports = maybeUnwrapFn(ngModuleDef.exports); declarations.forEach(verifyDeclarationsHaveDefinitions); @@ -331,12 +340,14 @@ function verifySemanticsOfNgModuleDef( function verifySemanticsOfNgModuleImport(type: Type, importingModule: Type) { type = resolveForwardRef(type); - if (getComponentDef(type) || getDirectiveDef(type)) { + const directiveDef = getComponentDef(type) || getDirectiveDef(type); + if (directiveDef !== null && !directiveDef.standalone) { throw new Error(`Unexpected directive '${type.name}' imported by the module '${ importingModule.name}'. Please add an @NgModule annotation.`); } - if (getPipeDef(type)) { + const pipeDef = getPipeDef(type); + if (pipeDef !== null && !pipeDef.standalone) { throw new Error(`Unexpected pipe '${type.name}' imported by the module '${ importingModule.name}'. Please add an @NgModule annotation.`); } @@ -399,7 +410,13 @@ export function resetCompiledComponents(): void { */ function computeCombinedExports(type: Type): Type[] { type = resolveForwardRef(type); - const ngModuleDef = getNgModuleDef(type, true); + const ngModuleDef = getNgModuleDef(type); + + // a standalone component, directive or pipe + if (ngModuleDef === null) { + return [type]; + } + return [...flatten(maybeUnwrapFn(ngModuleDef.exports).map((type) => { const ngModuleDef = getNgModuleDef(type); if (ngModuleDef) { @@ -458,6 +475,49 @@ export function patchComponentDefWithScope( componentDef.tView = null; } +/** + * Compute the pair of transitive scopes (compilation scope and exported scope) for a given type + * (eaither a NgModule or a standalone component / directive / pipe). + */ +export function transitiveScopesFor(type: Type): NgModuleTransitiveScopes { + if (isNgModule(type)) { + return transitiveScopesForNgModule(type); + } else if (isStandalone(type)) { + const directiveDef = getComponentDef(type) || getDirectiveDef(type); + if (directiveDef !== null) { + return { + schemas: null, + compilation: { + directives: new Set(), + pipes: new Set(), + }, + exported: { + directives: new Set([type]), + pipes: new Set(), + }, + }; + } + + const pipeDef = getPipeDef(type); + if (pipeDef !== null) { + return { + schemas: null, + compilation: { + directives: new Set(), + pipes: new Set(), + }, + exported: { + directives: new Set(), + pipes: new Set([type]), + }, + }; + } + } + + // TODO: change the error message to be more user-facing and take standalone into account + throw new Error(`${type.name} does not have a module def (ɵmod property)`); +} + /** * Compute the pair of transitive scopes (compilation scope and exported scope) for a given module. * @@ -467,11 +527,8 @@ export function patchComponentDefWithScope( * * @param moduleType module that transitive scope should be calculated for. */ -export function transitiveScopesFor(moduleType: Type): NgModuleTransitiveScopes { - if (!isNgModule(moduleType)) { - throw new Error(`${moduleType.name} does not have a module def (ɵmod property)`); - } - const def = getNgModuleDef(moduleType)!; +export function transitiveScopesForNgModule(moduleType: Type): NgModuleTransitiveScopes { + const def = getNgModuleDef(moduleType, true); if (def.transitiveCompileScopes !== null) { return def.transitiveCompileScopes; @@ -490,18 +547,9 @@ export function transitiveScopesFor(moduleType: Type): NgModuleTransitiveS }; maybeUnwrapFn(def.imports).forEach((imported: Type) => { - const importedType = imported as Type& { - // If imported is an @NgModule: - ɵmod?: NgModuleDef; - }; - - if (!isNgModule(importedType)) { - throw new Error(`Importing ${importedType.name} which does not have a ɵmod property`); - } - // When this module imports another, the imported module's exported directives and pipes are // added to the compilation scope of this module. - const importedScope = transitiveScopesFor(importedType); + const importedScope = transitiveScopesFor(imported); importedScope.exported.directives.forEach(entry => scopes.compilation.directives.add(entry)); importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry)); }); diff --git a/packages/core/src/render3/jit/pipe.ts b/packages/core/src/render3/jit/pipe.ts index ca5294b533b..c5dc68001ba 100644 --- a/packages/core/src/render3/jit/pipe.ts +++ b/packages/core/src/render3/jit/pipe.ts @@ -60,8 +60,6 @@ function getPipeMetadata(type: Type, meta: Pipe): R3PipeMetadataFacade { name: type.name, pipeName: meta.name, pure: meta.pure !== undefined ? meta.pure : true, - // TODO(alxhub): pass through the standalone flag from the pipe metadata once standalone - // functionality is fully rolled out. - isStandalone: false, + isStandalone: !!meta.standalone, }; } diff --git a/packages/core/test/acceptance/standalone_spec.ts b/packages/core/test/acceptance/standalone_spec.ts new file mode 100644 index 00000000000..ec184f0842c --- /dev/null +++ b/packages/core/test/acceptance/standalone_spec.ts @@ -0,0 +1,153 @@ +/** + * @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 {CommonModule} from '@angular/common'; +import {Component, Directive, Input, NgModule, Pipe, PipeTransform} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; + +describe('standalone components, directives and pipes', () => { + it('should render a standalone component', () => { + @Component({ + standalone: true, + template: 'Look at me, no NgModule!', + }) + class StandaloneCmp { + } + + const fixture = TestBed.createComponent(StandaloneCmp); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('Look at me, no NgModule!'); + }); + + it('should render a recursive standalone component', () => { + @Component({ + selector: 'tree', + standalone: true, + template: + `({{level}})`, + imports: [CommonModule] + }) + class TreeCmp { + @Input() level = 0; + } + + @Component({standalone: true, template: '', imports: [TreeCmp]}) + class StandaloneCmp { + } + + const fixture = TestBed.createComponent(StandaloneCmp); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('(3)(2)(1)(0)'); + }); + + it('should render a standalone component with a standalone dependency', () => { + @Component({ + standalone: true, + selector: 'inner-cmp', + template: 'Look at me, no NgModule!', + }) + class InnerCmp { + } + + @Component({ + standalone: true, + template: '', + imports: [InnerCmp], + }) + class StandaloneCmp { + } + + const fixture = TestBed.createComponent(StandaloneCmp); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual('Look at me, no NgModule!'); + }); + + + it('should render a standalone component with an NgModule-based dependency', () => { + @Component({ + selector: 'inner-cmp', + template: 'Look at me, no NgModule (kinda)!', + }) + class InnerCmp { + } + + @NgModule({ + declarations: [InnerCmp], + exports: [InnerCmp], + }) + class Module { + } + + @Component({ + standalone: true, + template: '', + imports: [Module], + }) + class StandaloneCmp { + } + + const fixture = TestBed.createComponent(StandaloneCmp); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual('Look at me, no NgModule (kinda)!'); + }); + + it('should allow exporting standalone components, directives and pipes from NgModule', () => { + @Component({ + selector: 'standalone-cmp', + standalone: true, + template: `standalone`, + }) + class StandaloneCmp { + } + + @Directive({ + selector: '[standalone-dir]', + host: { + '[attr.id]': '"standalone"', + }, + standalone: true + }) + class StandaloneDir { + } + + @Pipe({name: 'standalonePipe', standalone: true}) + class StandalonePipe implements PipeTransform { + transform(value: any) { + return `|${value}`; + } + } + + @NgModule({ + imports: [StandaloneCmp, StandaloneDir, StandalonePipe], + exports: [StandaloneCmp, StandaloneDir, StandalonePipe], + }) + class LibModule { + } + + @Component({ + selector: 'app-cmpt', + template: `{{'standalone' | standalonePipe}}`, + }) + class AppComponent { + } + + TestBed.configureTestingModule({ + imports: [LibModule], + declarations: [AppComponent], + }); + + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('standalone|standalone'); + expect(fixture.nativeElement.querySelector('standalone-cmp').getAttribute('id')) + .toBe('standalone'); + }); +});