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 c9c2408176
commit fb70083339
19 changed files with 371 additions and 67 deletions

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