feat(compiler): add dependency info and ng-content selectors to metadata (#35695)

This commit augments the `FactoryDef` declaration of Angular decorated
classes to contain information about the parameter decorators used in
the constructor. If no constructor is present, or none of the parameters
have any Angular decorators, then this will be represented using the
`null` type. Otherwise, a tuple type is used where the entry at index `i`
corresponds with parameter `i`. Each tuple entry can be one of two types:

1. If the associated parameter does not have any Angular decorators,
   the tuple entry will be the `null` type.
2. Otherwise, a type literal is used that may declare at least one of
   the following properties:
   - "attribute": if `@Attribute` is present. The injected attribute's
   name is used as string literal type, or the `unknown` type if the
   attribute name is not a string literal.
   - "self": if `@Self` is present, always of type `true`.
   - "skipSelf": if `@SkipSelf` is present, always of type `true`.
   - "host": if `@Host` is present, always of type `true`.
   - "optional": if `@Optional` is present, always of type `true`.

   A property is only present if the corresponding decorator is used.

   Note that the `@Inject` decorator is currently not included, as it's
   non-trivial to properly convert the token's value expression to a
   type that is valid in a declaration file.

Additionally, the `ComponentDefWithMeta` declaration that is created for
Angular components has been extended to include all selectors on
`ng-content` elements within the component's template.

This additional metadata is useful for tooling such as the Angular
Language Service, as it provides the ability to offer suggestions for
directives/components defined in libraries. At the moment, such
tooling extracts the necessary information from the _metadata.json_
manifest file as generated by ngc, however this metadata representation
is being replaced by the information emitted into the declaration files.

Resolves FW-1870

PR Close #35695
This commit is contained in:
JoostK 2020-02-26 22:05:44 +01:00 committed by Misko Hevery
parent ff4eb0cb63
commit 32ce8b1326
19 changed files with 371 additions and 67 deletions

View File

@ -715,7 +715,7 @@ export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportA
[key: string]: string;
}, OutputMap extends {
[key: string]: string;
}, QueryFields extends string[]> = ɵComponentDef<T>;
}, QueryFields extends string[], NgContentSelectors extends string[]> = ɵComponentDef<T>;
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> = () => T;
export declare type ɵɵFactoryDef<T, CtorDependencies extends CtorDependency[]> = () => T;
export declare function ɵɵgetCurrentView(): OpaqueViewState;

View File

@ -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);
}

View File

@ -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<NgPluralCase, [{ attribute: "ngPluralCase"; }, null, null, { host: true; }]>`);
});
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<DerivedCmp, "[base]", never, {}, {}, never>;');
'static ɵcmp: ɵngcc0.ɵɵComponentDefWithMeta<DerivedCmp, "[base]", never, {}, {}, never, never>;');
});
it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain',

View File

@ -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<A>;\n static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta');
'foo(x: number): number;\n static ɵfac: ɵngcc0.ɵɵFactoryDef<A, never>;\n static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta');
});
it('should render imports into typings files', () => {

View File

@ -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.
*/

View File

@ -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,

View File

@ -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<ts.Expression> =
/**
* 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
*

View File

@ -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 {

View File

@ -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]}`);

View File

@ -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<T>(fn: () => T): T {
export interface SimpleChanges { [propName: string]: any; }
export type ɵɵNgModuleDefWithMeta<ModuleT, DeclarationsT, ImportsT, ExportsT> = any;
export type ɵɵDirectiveDefWithMeta<DirT, SelectorT, ExportAsT, InputsT, OutputsT, QueriesT> = any;
export type ɵɵDirectiveDefWithMeta<
DirT, SelectorT, ExportAsT, InputsT, OutputsT, QueriesT, NgContentSelectorsT> = any;
export type ɵɵPipeDefWithMeta<PipeT, NameT> = any;
export enum ViewEncapsulation {

View File

@ -68,8 +68,8 @@ runInEachFileSystem(os => {
const dtsContents = env.getContents('test.d.ts');
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Dep>;');
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep, never>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
});
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<Store<any>>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Store<any>, never>;');
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Store<any>>;');
});
@ -117,8 +117,8 @@ runInEachFileSystem(os => {
const dtsContents = env.getContents('test.d.ts');
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Dep>;');
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Dep, never>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
});
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<Service>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
});
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<Service>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<Service, never>;');
});
it('should compile @Injectable with an @Optional dependency', () => {
@ -237,7 +237,7 @@ runInEachFileSystem(os => {
expect(dtsContents)
.toContain(
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, "[dir]", never, {}, {}, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir, never>');
});
it('should compile abstract Directives without errors', () => {
@ -259,7 +259,7 @@ runInEachFileSystem(os => {
expect(dtsContents)
.toContain(
'static ɵdir: i0.ɵɵDirectiveDefWithMeta<TestDir, never, never, {}, {}, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestDir, never>');
});
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<TestCmp, "test-cmp", never, {}, {}, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp>');
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp, never>');
});
it('should compile Components (dynamic inline template) without errors', () => {
@ -309,8 +309,9 @@ runInEachFileSystem(os => {
expect(dtsContents)
.toContain(
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp>');
'static ɵcmp: i0.ɵɵComponentDefWithMeta' +
'<TestCmp, "test-cmp", never, {}, {}, never, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp, never>');
});
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<TestCmp, "test-cmp", never, {}, {}, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp>');
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never, never>');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestCmp, never>');
});
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<TestCmp, "test-cmp", never, {}, {}, never>');
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test-cmp", never, {}, {}, never, never>');
expect(dtsContents)
.toContain(
'static ɵmod: i0.ɵɵNgModuleDefWithMeta<TestModule, [typeof TestCmp], never, never>');
@ -1327,7 +1328,7 @@ runInEachFileSystem(os => {
.toContain(
'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }');
expect(dtsContents).toContain('static ɵpipe: i0.ɵɵPipeDefWithMeta<TestPipe, "test-pipe">;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe, never>;');
});
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<TestPipe, "test-pipe">;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe, never>;');
});
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<TestPipe<any>, "test-pipe">;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe<any>>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDef<TestPipe<any>, 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<WithDecorators, [' +
'{ self: true; }, { skipSelf: true; }, { host: true; }, ' +
'{ optional: true; }, { attribute: "attr"; }, { attribute: unknown; }, ' +
'{ optional: true; host: true; skipSelf: true; }, null]>');
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<NoCtor, never>`);
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<EmptyCtor, never>`);
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<WithoutDecorators, never>`);
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<MyCmp, [{ host: true; }]>`);
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<MyPipe, [{ host: true; }]>`);
});
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<Inj, [{ self: true; }]>`);
expect(dtsContents)
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseExisting, [{ self: true; }]>`);
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseClass, [{ self: true; }]>`);
expect(dtsContents)
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseClassWithDeps, [{ self: true; }]>`);
expect(dtsContents)
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseFactory, [{ self: true; }]>`);
expect(dtsContents)
.toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseFactoryWithDeps, [{ self: true; }]>`);
expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDef<InjUseValue, [{ self: true; }]>`);
});
it('should include ng-content selectors in the metadata', () => {
env.write(`test.ts`, `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '<ng-content></ng-content> <ng-content select=".foo"></ng-content>',
})
export class TestCmp {
}
`);
env.driveMain();
const dtsContents = env.getContents('test.d.ts');
expect(dtsContents)
.toContain(
'static ɵcmp: i0.ɵɵComponentDefWithMeta<TestCmp, "test", never, {}, {}, never, ["*", ".foo"]>');
});
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<Testability>;
static ɵfac: i0.ɵɵFactoryDef<Testability, never>;
constructor(ngZone: NgZone) {}
}
`);

View File

@ -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,
};
}

View File

@ -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,

View File

@ -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) {
// `<ng-template>`
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);

View File

@ -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[];
};
/**

View File

@ -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<string|null>): 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

View File

@ -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();

View File

@ -89,6 +89,39 @@ export interface DirectiveType<T> extends Type<T> {
*/
export interface PipeType<T> extends Type<T> { ɵ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<T> {
*/
export type ɵɵComponentDefWithMeta<
T, Selector extends String, ExportAs extends string[], InputMap extends{[key: string]: string},
OutputMap extends{[key: string]: string}, QueryFields extends string[]> = ComponentDef<T>;
OutputMap extends{[key: string]: string}, QueryFields extends string[],
NgContentSelectors extends string[]> = ComponentDef<T>;
/**
* @codeGenApi
*/
export type ɵɵFactoryDef<T> = () => T;
export type ɵɵFactoryDef<T, CtorDependencies extends CtorDependency[]> = () => T;
/**
* Runtime link information for Components.

View File

@ -9,7 +9,7 @@
import {ɵɵComponentDefWithMeta, ɵɵPipeDefWithMeta as PipeDefWithMeta} from '@angular/core';
declare class SuperComponent {
static ɵcmp: ɵɵComponentDefWithMeta<SuperComponent, '[super]', never, {}, {}, never>;
static ɵcmp: ɵɵComponentDefWithMeta<SuperComponent, '[super]', never, {}, {}, never, never>;
}
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<SubComponent, '[sub]', never, {}, {}, never>;
static ɵcmp: ɵɵComponentDefWithMeta<SubComponent, '[sub]', never, {}, {}, never, never>;
}
declare class SuperPipe { static ɵpipe: PipeDefWithMeta<SuperPipe, 'super'>; }