diff --git a/goldens/public-api/core/core.d.ts b/goldens/public-api/core/core.d.ts index f47b73703b..bdb2bf34df 100644 --- a/goldens/public-api/core/core.d.ts +++ b/goldens/public-api/core/core.d.ts @@ -715,7 +715,7 @@ export declare type ɵɵComponentDefWithMeta = ɵComponentDef; +}, QueryFields extends string[], NgContentSelectors extends string[]> = ɵComponentDef; export declare function ɵɵcomponentHostSyntheticListener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): typeof ɵɵcomponentHostSyntheticListener; @@ -834,7 +834,7 @@ export declare function ɵɵembeddedViewStart(viewBlockId: number, decls: number export declare function ɵɵenableBindings(): void; -export declare type ɵɵFactoryDef = () => T; +export declare type ɵɵFactoryDef = () => T; export declare function ɵɵgetCurrentView(): OpaqueViewState; diff --git a/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts index 746145f689..25cc214d52 100644 --- a/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts @@ -91,6 +91,7 @@ export class DtsRenderer { const endOfClass = dtsClass.dtsDeclaration.getEnd(); dtsClass.compilation.forEach(declaration => { const type = translateType(declaration.type, importManager); + markForEmitAsSingleLine(type); const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile); const newStatement = ` static ${declaration.name}: ${typeStr};\n`; outputText.appendRight(endOfClass - 1, newStatement); @@ -176,3 +177,8 @@ export class DtsRenderer { return dtsMap; } } + +function markForEmitAsSingleLine(node: ts.Node) { + ts.setEmitFlags(node, ts.EmitFlags.SingleLine); + ts.forEachChild(node, markForEmitAsSingleLine); +} diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index bde8f468a0..328ea2ee44 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -399,9 +399,22 @@ runInEachFileSystem(() => { expect(dtsContents) .toContain(`export declare class ${exportedName} extends PlatformLocation`); // And that ngcc's modifications to that class use the correct (exported) name - expect(dtsContents).toContain(`static ɵfac: ɵngcc0.ɵɵFactoryDef<${exportedName}>`); + expect(dtsContents).toContain(`static ɵfac: ɵngcc0.ɵɵFactoryDef<${exportedName}, never>`); }); + it('should include constructor metadata in factory definitions', () => { + mainNgcc({ + basePath: '/node_modules', + targetEntryPointPath: '@angular/common', + propertiesToConsider: ['esm2015'] + }); + + const dtsContents = fs.readFile(_('/node_modules/@angular/common/common.d.ts')); + expect(dtsContents) + .toContain( + `static ɵfac: ɵngcc0.ɵɵFactoryDef`); + }); + it('should add generic type for ModuleWithProviders and generate exports for private modules', () => { compileIntoApf('test-package', { @@ -1589,7 +1602,7 @@ runInEachFileSystem(() => { const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`)); expect(dtsContents) .toContain( - 'static ɵcmp: ɵngcc0.ɵɵComponentDefWithMeta;'); + 'static ɵcmp: ɵngcc0.ɵɵComponentDefWithMeta;'); }); it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain', diff --git a/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts index ac4294d51f..cb78fa5f42 100644 --- a/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts @@ -130,7 +130,7 @@ runInEachFileSystem(() => { result.find(f => f.path === _('/node_modules/test-package/typings/file.d.ts')) !; expect(typingsFile.contents) .toContain( - 'foo(x: number): number;\n static ɵfac: ɵngcc0.ɵɵFactoryDef;\n static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta'); + 'foo(x: number): number;\n static ɵfac: ɵngcc0.ɵɵFactoryDef;\n static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta'); }); it('should render imports into typings files', () => { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 246b1ab031..3568e4f20a 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -313,6 +313,7 @@ export class ComponentDecoratorHandler implements ...metadata, template: { nodes: template.emitNodes, + ngContentSelectors: template.ngContentSelectors, }, encapsulation, interpolation: template.interpolation, @@ -770,12 +771,13 @@ export class ComponentDecoratorHandler implements interpolation = InterpolationConfig.fromArray(value as[string, string]); } - const {errors, nodes: emitNodes, styleUrls, styles} = parseTemplate(templateStr, templateUrl, { - preserveWhitespaces, - interpolationConfig: interpolation, - range: templateRange, escapedString, - enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat, - }); + const {errors, nodes: emitNodes, styleUrls, styles, ngContentSelectors} = + parseTemplate(templateStr, templateUrl, { + preserveWhitespaces, + interpolationConfig: interpolation, + range: templateRange, escapedString, + enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat, + }); // Unfortunately, the primary parse of the template above may not contain accurate source map // information. If used directly, it would result in incorrect code locations in template @@ -804,6 +806,7 @@ export class ComponentDecoratorHandler implements diagNodes, styleUrls, styles, + ngContentSelectors, errors, template: templateStr, templateUrl, isInline: component.has('template'), @@ -923,6 +926,11 @@ export interface ParsedTemplate { */ styles: string[]; + /** + * Any ng-content selectors extracted from the template. + */ + ngContentSelectors: string[]; + /** * Whether the template was inline. */ diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index 3f67fafcad..98b81843f9 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -274,6 +274,7 @@ function extractInjectableCtorDeps( function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata { const meta: R3DependencyMetadata = { token: new WrappedNodeExpr(dep), + attribute: null, host: false, resolved: R3ResolvedDependencyType.Token, optional: false, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 6cca49f67b..7cddaae9f8 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Expression, ExternalExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler'; +import {Expression, ExternalExpr, LiteralExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError, makeDiagnostic} from '../../diagnostics'; @@ -48,6 +48,7 @@ export function getConstructorDependencies( } ctorParams.forEach((param, idx) => { let token = valueReferenceToExpression(param.typeValueReference, defaultImportRecorder); + let attribute: Expression|null = null; let optional = false, self = false, skipSelf = false, host = false; let resolved = R3ResolvedDependencyType.Token; @@ -74,7 +75,13 @@ export function getConstructorDependencies( ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(dec), `Unexpected number of arguments to @Attribute().`); } - token = new WrappedNodeExpr(dec.args[0]); + const attributeName = dec.args[0]; + token = new WrappedNodeExpr(attributeName); + if (ts.isStringLiteralLike(attributeName)) { + attribute = new LiteralExpr(attributeName.text); + } else { + attribute = new WrappedNodeExpr(ts.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)); + } resolved = R3ResolvedDependencyType.Attribute; } else { throw new FatalDiagnosticError( @@ -93,7 +100,7 @@ export function getConstructorDependencies( kind: ConstructorDepErrorKind.NO_SUITABLE_TOKEN, param, }); } else { - deps.push({token, optional, self, skipSelf, host, resolved}); + deps.push({token, attribute, optional, self, skipSelf, host, resolved}); } }); if (errors.length === 0) { @@ -369,7 +376,8 @@ const parensWrapperTransformerFactory: ts.TransformerFactory = /** * Wraps all functions in a given expression in parentheses. This is needed to avoid problems * where Tsickle annotations added between analyse and transform phases in Angular may trigger - * automatic semicolon insertion, e.g. if a function is the expression in a `return` statement. More + * automatic semicolon insertion, e.g. if a function is the expression in a `return` statement. + * More * info can be found in Tsickle source code here: * https://github.com/angular/tsickle/blob/d7974262571c8a17d684e5ba07680e1b1993afdd/src/jsdoc_transformer.ts#L1021 * diff --git a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts index ee4e47b795..6e8c9abaed 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts @@ -205,7 +205,7 @@ export class IvyDeclarationDtsTransform implements DtsTransform { const newMembers = fields.map(decl => { const modifiers = [ts.createModifier(ts.SyntaxKind.StaticKeyword)]; const typeRef = translateType(decl.type, imports); - emitAsSingleLine(typeRef); + markForEmitAsSingleLine(typeRef); return ts.createProperty( /* decorators */ undefined, /* modifiers */ modifiers, @@ -226,9 +226,9 @@ export class IvyDeclarationDtsTransform implements DtsTransform { } } -function emitAsSingleLine(node: ts.Node) { +function markForEmitAsSingleLine(node: ts.Node) { ts.setEmitFlags(node, ts.EmitFlags.SingleLine); - ts.forEachChild(node, emitAsSingleLine); + ts.forEachChild(node, markForEmitAsSingleLine); } export class ReturnTypeTransform implements DtsTransform { diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index a41a9fc8a3..a55348229d 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -447,12 +447,12 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { `An ExpressionType with type arguments cannot have multiple levels of type arguments`); } - const typeArgs = type.typeParams.map(param => param.visitType(this, context)); + const typeArgs = type.typeParams.map(param => this.translateType(param, context)); return ts.createTypeReferenceNode(typeNode.typeName, typeArgs); } visitArrayType(type: ArrayType, context: Context): ts.ArrayTypeNode { - return ts.createArrayTypeNode(this.translateType(type, context)); + return ts.createArrayTypeNode(this.translateType(type.of, context)); } visitMapType(type: MapType, context: Context): ts.TypeLiteralNode { @@ -497,8 +497,18 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { throw new Error('Method not implemented.'); } - visitLiteralExpr(ast: LiteralExpr, context: Context): ts.LiteralTypeNode { - return ts.createLiteralTypeNode(ts.createLiteral(ast.value as string)); + visitLiteralExpr(ast: LiteralExpr, context: Context): ts.TypeNode { + if (ast.value === null) { + return ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword); + } else if (ast.value === undefined) { + return ts.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); + } else if (typeof ast.value === 'boolean') { + return ts.createLiteralTypeNode(ts.createLiteral(ast.value)); + } else if (typeof ast.value === 'number') { + return ts.createLiteralTypeNode(ts.createLiteral(ast.value)); + } else { + return ts.createLiteralTypeNode(ts.createLiteral(ast.value)); + } } visitLocalizedString(ast: LocalizedString, context: Context): never { @@ -578,6 +588,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { return ts.createTypeReferenceNode(node, /* typeArguments */ undefined); } else if (ts.isTypeNode(node)) { return node; + } else if (ts.isLiteralExpression(node)) { + return ts.createLiteralTypeNode(node); } else { throw new Error( `Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`); @@ -590,8 +602,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { return ts.createTypeQueryNode(expr as ts.Identifier); } - private translateType(expr: Type, context: Context): ts.TypeNode { - const typeNode = expr.visitType(this, context); + private translateType(type: Type, context: Context): ts.TypeNode { + const typeNode = type.visitType(this, context); if (!ts.isTypeNode(typeNode)) { throw new Error( `A Type must translate to a TypeNode, but was ${ts.SyntaxKind[typeNode.kind]}`); diff --git a/packages/compiler-cli/test/ngtsc/fake_core/index.ts b/packages/compiler-cli/test/ngtsc/fake_core/index.ts index 6c296d2137..17ae37d6bc 100644 --- a/packages/compiler-cli/test/ngtsc/fake_core/index.ts +++ b/packages/compiler-cli/test/ngtsc/fake_core/index.ts @@ -34,6 +34,7 @@ export const Inject = callableParamDecorator(); export const Self = callableParamDecorator(); export const SkipSelf = callableParamDecorator(); export const Optional = callableParamDecorator(); +export const Host = callableParamDecorator(); export const ContentChild = callablePropDecorator(); export const ContentChildren = callablePropDecorator(); @@ -68,7 +69,8 @@ export function forwardRef(fn: () => T): T { export interface SimpleChanges { [propName: string]: any; } export type ɵɵNgModuleDefWithMeta = any; -export type ɵɵDirectiveDefWithMeta = any; +export type ɵɵDirectiveDefWithMeta< + DirT, SelectorT, ExportAsT, InputsT, OutputsT, QueriesT, NgContentSelectorsT> = any; export type ɵɵPipeDefWithMeta = any; export enum ViewEncapsulation { diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 9921e67f1a..db54ed8e05 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -68,8 +68,8 @@ runInEachFileSystem(os => { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef;'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); }); it('should compile Injectables with a generic service', () => { @@ -86,7 +86,7 @@ runInEachFileSystem(os => { const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Store.ɵprov ='); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef>;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef, never>;'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef>;'); }); @@ -117,8 +117,8 @@ runInEachFileSystem(os => { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef;'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); }); it('should compile Injectables with providedIn and factory without errors', () => { @@ -143,7 +143,7 @@ runInEachFileSystem(os => { expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); }); it('should compile Injectables with providedIn and factory with deps without errors', () => { @@ -172,7 +172,7 @@ runInEachFileSystem(os => { expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); }); it('should compile @Injectable with an @Optional dependency', () => { @@ -237,7 +237,7 @@ runInEachFileSystem(os => { expect(dtsContents) .toContain( 'static ɵdir: i0.ɵɵDirectiveDefWithMeta'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); }); it('should compile abstract Directives without errors', () => { @@ -259,7 +259,7 @@ runInEachFileSystem(os => { expect(dtsContents) .toContain( 'static ɵdir: i0.ɵɵDirectiveDefWithMeta'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); }); it('should compile Components (inline template) without errors', () => { @@ -283,8 +283,8 @@ runInEachFileSystem(os => { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ɵcmp: i0.ɵɵComponentDefWithMeta'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); + 'static ɵcmp: i0.ɵɵComponentDefWithMeta'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); }); it('should compile Components (dynamic inline template) without errors', () => { @@ -309,8 +309,9 @@ runInEachFileSystem(os => { expect(dtsContents) .toContain( - 'static ɵcmp: i0.ɵɵComponentDefWithMeta'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); + 'static ɵcmp: i0.ɵɵComponentDefWithMeta' + + ''); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); }); it('should compile Components (function call inline template) without errors', () => { @@ -337,8 +338,8 @@ runInEachFileSystem(os => { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ɵcmp: i0.ɵɵComponentDefWithMeta'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); + 'static ɵcmp: i0.ɵɵComponentDefWithMeta'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef'); }); it('should compile Components (external template) without errors', () => { @@ -935,7 +936,7 @@ runInEachFileSystem(os => { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ɵcmp: i0.ɵɵComponentDefWithMeta'); + 'static ɵcmp: i0.ɵɵComponentDefWithMeta'); expect(dtsContents) .toContain( 'static ɵmod: i0.ɵɵNgModuleDefWithMeta'); @@ -1327,7 +1328,7 @@ runInEachFileSystem(os => { .toContain( 'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }'); expect(dtsContents).toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); }); it('should compile pure Pipes without errors', () => { @@ -1352,7 +1353,7 @@ runInEachFileSystem(os => { .toContain( 'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }'); expect(dtsContents).toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef;'); }); it('should compile Pipes with dependencies', () => { @@ -1393,7 +1394,7 @@ runInEachFileSystem(os => { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta, "test-pipe">;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef>;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef, never>;'); }); it('should include @Pipes in @NgModule scopes', () => { @@ -2574,6 +2575,141 @@ runInEachFileSystem(os => { `FooCmp.ɵfac = function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵɵinjectAttribute("test"), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i0.Injector), i0.ɵɵdirectiveInject(i0.Renderer2), i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(i0.ViewContainerRef)); }`); }); + it('should include constructor dependency metadata for directives/components/pipes', () => { + env.write(`test.ts`, ` + import {Attribute, Component, Directive, Pipe, Self, SkipSelf, Host, Optional} from '@angular/core'; + + export class MyService {} + export function dynamic() {}; + + @Directive() + export class WithDecorators { + constructor( + @Self() withSelf: MyService, + @SkipSelf() withSkipSelf: MyService, + @Host() withHost: MyService, + @Optional() withOptional: MyService, + @Attribute("attr") withAttribute: string, + @Attribute(dynamic()) withAttributeDynamic: string, + @Optional() @SkipSelf() @Host() withMany: MyService, + noDecorators: MyService) {} + } + + @Directive() + export class NoCtor {} + + @Directive() + export class EmptyCtor { + constructor() {} + } + + @Directive() + export class WithoutDecorators { + constructor(noDecorators: MyService) {} + } + + @Component({ template: 'test' }) + export class MyCmp { + constructor(@Host() withHost: MyService) {} + } + + @Pipe({ name: 'test' }) + export class MyPipe { + constructor(@Host() withHost: MyService) {} + } + `); + + env.driveMain(); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'static ɵfac: i0.ɵɵFactoryDef'); + expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef`); + }); + + it('should include constructor dependency metadata for @Injectable', () => { + env.write(`test.ts`, ` + import {Injectable, Self, Host} from '@angular/core'; + + export class MyService {} + + @Injectable() + export class Inj { + constructor(@Self() service: MyService) {} + } + + @Injectable({ useExisting: MyService }) + export class InjUseExisting { + constructor(@Self() service: MyService) {} + } + + @Injectable({ useClass: MyService }) + export class InjUseClass { + constructor(@Self() service: MyService) {} + } + + @Injectable({ useClass: MyService, deps: [[new Host(), MyService]] }) + export class InjUseClassWithDeps { + constructor(@Self() service: MyService) {} + } + + @Injectable({ useFactory: () => new Injectable(new MyService()) }) + export class InjUseFactory { + constructor(@Self() service: MyService) {} + } + + @Injectable({ useFactory: (service: MyService) => new Injectable(service), deps: [[new Host(), MyService]] }) + export class InjUseFactoryWithDeps { + constructor(@Self() service: MyService) {} + } + + @Injectable({ useValue: new Injectable(new MyService()) }) + export class InjUseValue { + constructor(@Self() service: MyService) {} + } + `); + + env.driveMain(); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents) + .toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents) + .toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents) + .toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents) + .toContain(`static ɵfac: i0.ɵɵFactoryDef`); + expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef`); + }); + + it('should include ng-content selectors in the metadata', () => { + env.write(`test.ts`, ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'test', + template: ' ', + }) + export class TestCmp { + } + `); + + env.driveMain(); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents) + .toContain( + 'static ɵcmp: i0.ɵɵComponentDefWithMeta'); + }); + it('should generate queries for components', () => { env.write(`test.ts`, ` import {Component, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core'; @@ -6520,7 +6656,7 @@ export const Foo = Foo__PRE_R3__; export declare class NgZone {} export declare class Testability { - static ɵfac: i0.ɵɵFactoryDef; + static ɵfac: i0.ɵɵFactoryDef; constructor(ngZone: NgZone) {} } `); diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index b1095f801d..172eaa2a3f 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -296,11 +296,12 @@ function convertR3DependencyMetadata(facade: R3DependencyMetadataFacade): R3Depe } return { token: tokenExpr, + attribute: null, resolved: facade.resolved, host: facade.host, optional: facade.optional, self: facade.self, - skipSelf: facade.skipSelf + skipSelf: facade.skipSelf, }; } diff --git a/packages/compiler/src/render3/r3_factory.ts b/packages/compiler/src/render3/r3_factory.ts index c3ede764a5..e0bcc39ad8 100644 --- a/packages/compiler/src/render3/r3_factory.ts +++ b/packages/compiler/src/render3/r3_factory.ts @@ -141,6 +141,13 @@ export interface R3DependencyMetadata { */ token: o.Expression; + /** + * If an @Attribute decorator is present, this is the literal type of the attribute name, or + * the unknown type if no literal type is available (e.g. the attribute name is an expression). + * Will be null otherwise. + */ + attribute: o.Expression|null; + /** * An enum indicating whether this dependency has special meaning to Angular and needs to be * injected specially. @@ -180,6 +187,7 @@ export interface R3FactoryFn { export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn { const t = o.variable('t'); const statements: o.Statement[] = []; + let ctorDepsType: o.Type = o.NONE_TYPE; // The type to instantiate via constructor invocation. If there is no delegated factory, meaning // this type is always created by constructor invocation, then this is the type-to-create @@ -197,6 +205,8 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn { ctorExpr = new o.InstantiateExpr( typeForCtor, injectDependencies(meta.deps, meta.injectFn, meta.target === R3FactoryTarget.Pipe)); + + ctorDepsType = createCtorDepsType(meta.deps); } } else { const baseFactory = o.variable(`ɵ${meta.name}_BaseFactory`); @@ -269,8 +279,9 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): R3FactoryFn { [new o.FnParam('t', o.DYNAMIC_TYPE)], body, o.INFERRED_TYPE, undefined, `${meta.name}_Factory`), statements, - type: o.expressionType( - o.importExpr(R3.FactoryDef, [typeWithParameters(meta.type.type, meta.typeArgumentCount)])) + type: o.expressionType(o.importExpr( + R3.FactoryDef, + [typeWithParameters(meta.type.type, meta.typeArgumentCount), ctorDepsType])) }; } @@ -319,6 +330,49 @@ function compileInjectDependency( } } +function createCtorDepsType(deps: R3DependencyMetadata[]): o.Type { + let hasTypes = false; + const attributeTypes = deps.map(dep => { + const type = createCtorDepType(dep); + if (type !== null) { + hasTypes = true; + return type; + } else { + return o.literal(null); + } + }); + + if (hasTypes) { + return o.expressionType(o.literalArr(attributeTypes)); + } else { + return o.NONE_TYPE; + } +} + +function createCtorDepType(dep: R3DependencyMetadata): o.LiteralMapExpr|null { + const entries: {key: string, quoted: boolean, value: o.Expression}[] = []; + + if (dep.resolved === R3ResolvedDependencyType.Attribute) { + if (dep.attribute !== null) { + entries.push({key: 'attribute', value: dep.attribute, quoted: false}); + } + } + if (dep.optional) { + entries.push({key: 'optional', value: o.literal(true), quoted: false}); + } + if (dep.host) { + entries.push({key: 'host', value: o.literal(true), quoted: false}); + } + if (dep.self) { + entries.push({key: 'self', value: o.literal(true), quoted: false}); + } + if (dep.skipSelf) { + entries.push({key: 'skipSelf', value: o.literal(true), quoted: false}); + } + + return entries.length > 0 ? o.literalMap(entries) : null; +} + /** * A helper function useful for extracting `R3DependencyMetadata` from a Render2 * `CompileTypeMetadata` instance. @@ -348,7 +402,7 @@ export function dependenciesFromGlobalMetadata( // Construct the dependency. deps.push({ token, - resolved, + attribute: null, resolved, host: !!dependency.isHost, optional: !!dependency.isOptional, self: !!dependency.isSelf, diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index a75d384ebc..84245d0066 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -52,6 +52,7 @@ export interface Render3ParseResult { errors: ParseError[]; styles: string[]; styleUrls: string[]; + ngContentSelectors: string[]; } export function htmlAstToRender3Ast( @@ -73,6 +74,7 @@ export function htmlAstToRender3Ast( errors: allErrors, styleUrls: transformer.styleUrls, styles: transformer.styles, + ngContentSelectors: transformer.ngContentSelectors, }; } @@ -80,6 +82,7 @@ class HtmlAstToIvyAst implements html.Visitor { errors: ParseError[] = []; styles: string[] = []; styleUrls: string[] = []; + ngContentSelectors: string[] = []; private inI18nBlock: boolean = false; constructor(private bindingParser: BindingParser) {} @@ -189,6 +192,8 @@ class HtmlAstToIvyAst implements html.Visitor { const selector = preparsedElement.selectAttr; const attrs: t.TextAttribute[] = element.attrs.map(attr => this.visitAttribute(attr)); parsedElement = new t.Content(selector, attrs, element.sourceSpan, element.i18n); + + this.ngContentSelectors.push(selector); } else if (isTemplateElement) { // `` const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta); diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 5b38e0a6a6..5b6a2d5594 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -129,6 +129,12 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata { * Parsed nodes of the template. */ nodes: t.Node[]; + + /** + * Any ng-content selectors extracted from the template. Contains `null` when an ng-content + * element without selector is present. + */ + ngContentSelectors: string[]; }; /** diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index a481bdb674..eb924357e8 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -22,7 +22,7 @@ import {CONTENT_ATTR, HOST_ATTR} from '../../style_compiler'; import {BindingParser} from '../../template_parser/binding_parser'; import {OutputContext, error} from '../../util'; import {BoundEvent} from '../r3_ast'; -import {R3FactoryTarget, compileFactoryFunction} from '../r3_factory'; +import {R3DependencyMetadata, R3FactoryTarget, R3ResolvedDependencyType, compileFactoryFunction} from '../r3_factory'; import {Identifiers as R3} from '../r3_identifiers'; import {Render3ParseResult} from '../r3_template_transform'; import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util'; @@ -124,7 +124,9 @@ export function compileDirectiveFromMetadata( addFeatures(definitionMap, meta); const expression = o.importExpr(R3.defineDirective).callFn([definitionMap.toLiteralMap()]); - const type = createTypeForDef(meta, R3.DirectiveDefWithMeta); + const typeParams = createDirectiveTypeParams(meta); + const type = o.expressionType(o.importExpr(R3.DirectiveDefWithMeta, typeParams)); + return {expression, type}; } @@ -252,7 +254,11 @@ export function compileComponentFromMetadata( } const expression = o.importExpr(R3.defineComponent).callFn([definitionMap.toLiteralMap()]); - const type = createTypeForDef(meta, R3.ComponentDefWithMeta); + + + const typeParams = createDirectiveTypeParams(meta); + typeParams.push(stringArrayAsType(meta.template.ngContentSelectors)); + const type = o.expressionType(o.importExpr(R3.ComponentDefWithMeta, typeParams)); return {expression, type}; } @@ -311,7 +317,7 @@ export function compileComponentFromRender2( const meta: R3ComponentMetadata = { ...directiveMetadataFromGlobalMetadata(component, outputCtx, reflector), selector: component.selector, - template: {nodes: render3Ast.nodes}, + template: {nodes: render3Ast.nodes, ngContentSelectors: render3Ast.ngContentSelectors}, directives: [], pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx), viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx), @@ -470,24 +476,24 @@ function stringMapAsType(map: {[key: string]: string | string[]}): o.Type { return o.expressionType(o.literalMap(mapValues)); } -function stringArrayAsType(arr: string[]): o.Type { +function stringArrayAsType(arr: ReadonlyArray): o.Type { return arr.length > 0 ? o.expressionType(o.literalArr(arr.map(value => o.literal(value)))) : o.NONE_TYPE; } -function createTypeForDef(meta: R3DirectiveMetadata, typeBase: o.ExternalReference): o.Type { +function createDirectiveTypeParams(meta: R3DirectiveMetadata): o.Type[] { // On the type side, remove newlines from the selector as it will need to fit into a TypeScript // string literal, which must be on one line. const selectorForType = meta.selector !== null ? meta.selector.replace(/\n/g, '') : null; - return o.expressionType(o.importExpr(typeBase, [ + return [ typeWithParameters(meta.type.type, meta.typeArgumentCount), selectorForType !== null ? stringAsType(selectorForType) : o.NONE_TYPE, meta.exportAs !== null ? stringArrayAsType(meta.exportAs) : o.NONE_TYPE, stringMapAsType(meta.inputs), stringMapAsType(meta.outputs), stringArrayAsType(meta.queries.map(q => q.propertyName)), - ])); + ]; } // Define and update any view queries diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 18ad2a784d..afbee7b6d4 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -1983,8 +1983,13 @@ export interface ParseTemplateOptions { * @param options options to modify how the template is parsed */ export function parseTemplate( - template: string, templateUrl: string, options: ParseTemplateOptions = {}): - {errors?: ParseError[], nodes: t.Node[], styleUrls: string[], styles: string[]} { + template: string, templateUrl: string, options: ParseTemplateOptions = {}): { + errors?: ParseError[], + nodes: t.Node[], + styleUrls: string[], + styles: string[], + ngContentSelectors: string[] +} { const {interpolationConfig, preserveWhitespaces, enableI18nLegacyMessageIdFormat} = options; const bindingParser = makeBindingParser(interpolationConfig); const htmlParser = new HtmlParser(); @@ -1993,7 +1998,13 @@ export function parseTemplate( {leadingTriviaChars: LEADING_TRIVIA_CHARS, ...options, tokenizeExpansionForms: true}); if (parseResult.errors && parseResult.errors.length > 0) { - return {errors: parseResult.errors, nodes: [], styleUrls: [], styles: []}; + return { + errors: parseResult.errors, + nodes: [], + styleUrls: [], + styles: [], + ngContentSelectors: [] + }; } let rootNodes: html.Node[] = parseResult.rootNodes; @@ -2020,12 +2031,13 @@ export function parseTemplate( } } - const {nodes, errors, styleUrls, styles} = htmlAstToRender3Ast(rootNodes, bindingParser); + const {nodes, errors, styleUrls, styles, ngContentSelectors} = + htmlAstToRender3Ast(rootNodes, bindingParser); if (errors && errors.length > 0) { - return {errors, nodes: [], styleUrls: [], styles: []}; + return {errors, nodes: [], styleUrls: [], styles: [], ngContentSelectors: []}; } - return {nodes, styleUrls, styles}; + return {nodes, styleUrls, styles, ngContentSelectors}; } const elementRegistry = new DomElementSchemaRegistry(); diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 34f5538cb8..f89895e2d8 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -89,6 +89,39 @@ export interface DirectiveType extends Type { */ export interface PipeType extends Type { ɵpipe: never; } +/** + * An object literal of this type is used to represent the metadata of a constructor dependency. + * The type itself is never referred to from generated code. + */ +export type CtorDependency = { + /** + * If an `@Attribute` decorator is used, this represents the injected attribute's name. If the + * attribute name is a dynamic expression instead of a string literal, this will be the unknown + * type. + */ + attribute?: string | unknown; + + /** + * If `@Optional()` is used, this key is set to true. + */ + optional?: true; + + /** + * If `@Host` is used, this key is set to true. + */ + host?: true; + + /** + * If `@Self` is used, this key is set to true. + */ + self?: true; + + /** + * If `@SkipSelf` is used, this key is set to true. + */ + skipSelf?: true; +} | null; + /** * @codeGenApi */ @@ -236,12 +269,13 @@ export interface DirectiveDef { */ export type ɵɵComponentDefWithMeta< T, Selector extends String, ExportAs extends string[], InputMap extends{[key: string]: string}, - OutputMap extends{[key: string]: string}, QueryFields extends string[]> = ComponentDef; + OutputMap extends{[key: string]: string}, QueryFields extends string[], + NgContentSelectors extends string[]> = ComponentDef; /** * @codeGenApi */ -export type ɵɵFactoryDef = () => T; +export type ɵɵFactoryDef = () => T; /** * Runtime link information for Components. diff --git a/packages/core/test/strict_types/inheritance_spec.ts b/packages/core/test/strict_types/inheritance_spec.ts index 29edf02795..c49fc0a9ed 100644 --- a/packages/core/test/strict_types/inheritance_spec.ts +++ b/packages/core/test/strict_types/inheritance_spec.ts @@ -9,7 +9,7 @@ import {ɵɵComponentDefWithMeta, ɵɵPipeDefWithMeta as PipeDefWithMeta} from '@angular/core'; declare class SuperComponent { - static ɵcmp: ɵɵComponentDefWithMeta; + static ɵcmp: ɵɵComponentDefWithMeta; } declare class SubComponent extends SuperComponent { @@ -18,7 +18,7 @@ declare class SubComponent extends SuperComponent { // would produce type errors when the "strictFunctionTypes" option is enabled. onlyInSubtype: string; - static ɵcmp: ɵɵComponentDefWithMeta; + static ɵcmp: ɵɵComponentDefWithMeta; } declare class SuperPipe { static ɵpipe: PipeDefWithMeta; }