Skip to content

Commit

Permalink
feat(core): add deps property to component
Browse files Browse the repository at this point in the history
  • Loading branch information
mgechev committed Dec 6, 2018
1 parent a421385 commit 2192263
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 35 deletions.
17 changes: 12 additions & 5 deletions packages/compiler-cli/src/ngtsc/annotations/src/component.ts
Expand Up @@ -6,21 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, ElementSchemaRegistry, Expression, InterpolationConfig, R3ComponentMetadata, R3DirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, InterpolationConfig, R3ComponentMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';

import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {Decorator, ReflectionHost} from '../../host';
import {AbsoluteReference, Reference, ResolvedReference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {Reference, ResolvedReference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {TypeCheckContext, TypeCheckableDirectiveMeta} from '../../typecheck';
import {TypeCheckContext} from '../../typecheck';

import {ResourceLoader} from './api';
import {extractDirectiveMetadata, extractQueriesFromDecorator, parseFieldArrayValue, queriesFromFields} from './directive';
import {generateSetClassMetadataCall} from './metadata';
import {ScopeDirective, SelectorScopeRegistry} from './selector_scope';
import {extractDirectiveGuards, isAngularCore, unwrapExpression} from './util';
import {extractDirectiveGuards, isAngularCore, resolveTypeList, unwrapExpression} from './util';

const EMPTY_MAP = new Map<string, Expression>();
const EMPTY_ARRAY: any[] = [];
Expand Down Expand Up @@ -179,6 +179,13 @@ export class ComponentDecoratorHandler implements
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
}

let deps: Reference<ts.Declaration>[] = [];
if (component.has('deps')) {
const expr = component.get('deps') !;
const declarationMeta = staticallyResolve(expr, this.reflector, this.checker);
deps = resolveTypeList(expr, declarationMeta, 'deps', this.reflector);
}

// If the component has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this component appears in an `@NgModule` scope, its selector can be determined.
if (metadata.selector !== null) {
Expand All @@ -189,7 +196,7 @@ export class ComponentDecoratorHandler implements
directive: ref,
selector: metadata.selector,
exportAs: metadata.exportAs,
inputs: metadata.inputs,
inputs: metadata.inputs, deps,
outputs: metadata.outputs,
queries: metadata.queries.map(query => query.propertyName),
isComponent: true, ...extractDirectiveGuards(node, this.reflector),
Expand Down
Expand Up @@ -50,6 +50,7 @@ export class DirectiveDecoratorHandler implements
this.scopeRegistry.registerDirective(node, {
ref,
directive: ref,
deps: [],
name: node.name !.text,
selector: analysis.selector,
exportAs: analysis.exportAs,
Expand Down
54 changes: 32 additions & 22 deletions packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts
Expand Up @@ -134,41 +134,44 @@ export class SelectorScopeRegistry {
lookupCompilationScopeAsRefs(node: ts.Declaration): CompilationScope<Reference>|null {
node = ts.getOriginalNode(node) as ts.Declaration;

// If the component has no associated module, then it has no compilation scope.
if (!this._declararedTypeToModule.has(node)) {
return null;
// This is the first time the scope for this module is being computed.
const directives: ScopeDirective<Reference<ts.Declaration>>[] = [];
const pipes = new Map<string, Reference<ts.Declaration>>();

// Tracks which declarations already appear in the `CompilationScope`.
const seenSet = new Set<ts.Declaration>();

const metadata = this._directiveToMetadata.get(node);
let deps: Reference<ts.Declaration>[] = [];
if (metadata) {
deps = metadata.deps;
}

const module = this._declararedTypeToModule.get(node) !;
const module = this._declararedTypeToModule.get(node);

// Compilation scope computation is somewhat expensive, so it's cached. Check the cache for
// the module.
if (this._compilationScopeCache.has(module)) {
if (this._compilationScopeCache.has(node)) {
// The compilation scope was cached.
const scope = this._compilationScopeCache.get(module) !;
const scope = this._compilationScopeCache.get(node) !;

// The scope as cached is in terms of References, not Expressions. Converting between them
// requires knowledge of the context file (in this case, the component node's source file).
return scope;
}

// This is the first time the scope for this module is being computed.
const directives: ScopeDirective<Reference<ts.Declaration>>[] = [];
const pipes = new Map<string, Reference<ts.Declaration>>();

// Tracks which declarations already appear in the `CompilationScope`.
const seenSet = new Set<ts.Declaration>();

// Process the declaration scope of the module, and lookup the selector of every declared type.
// The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule
// was not imported from a .d.ts source.
for (const ref of this.lookupScopesOrDie(module !, /* ngModuleImportedFrom */ null)
.compilation) {
if (module !== undefined) {
deps = deps.concat(
this.lookupNgModuleScopesOrDie(module, /* ngModuleImportedFrom */ null).compilation);
}
deps.forEach((ref: Reference<ts.Declaration>) => {
const node = ts.getOriginalNode(ref.node) as ts.Declaration;

// Track whether this `ts.Declaration` has been seen before.
if (seenSet.has(node)) {
continue;
return;
} else {
seenSet.add(node);
}
Expand All @@ -184,7 +187,7 @@ export class SelectorScopeRegistry {
pipes.set(name, ref);
}
}
}
});

const scope: CompilationScope<Reference> = {directives, pipes};

Expand All @@ -204,7 +207,7 @@ export class SelectorScopeRegistry {
return scope !== null ? convertScopeToExpressions(scope, node) : null;
}

private lookupScopesOrDie(node: ts.Declaration, ngModuleImportedFrom: string|null):
private lookupNgModuleScopesOrDie(node: ts.Declaration, ngModuleImportedFrom: string|null):
SelectorScopes {
const result = this.lookupScopes(node, ngModuleImportedFrom);
if (result === null) {
Expand Down Expand Up @@ -245,8 +248,9 @@ export class SelectorScopeRegistry {
...data.declarations,
// Expand imports to the exported scope of those imports.
...flatten(data.imports.map(
ref => this.lookupScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref))
.exported)),
ref =>
this.lookupNgModuleScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref))
.exported)),
// And include the compilation scope of exported modules.
...flatten(
data.exports
Expand Down Expand Up @@ -346,10 +350,16 @@ export class SelectorScopeRegistry {
return null;
}

const metadata = this._directiveToMetadata.get(ref.node);
let deps: Reference<ts.Declaration>[] = [];
if (metadata) {
deps = metadata.deps;
}

return {
ref,
name: clazz.name !.text,
directive: ref,
directive: ref, deps,
isComponent: def.name === 'ngComponentDef', selector,
exportAs: readStringType(def.type.typeArguments[2]),
inputs: readStringMapType(def.type.typeArguments[3]),
Expand Down
Expand Up @@ -38,9 +38,10 @@ describe('ComponentDecoratorHandler', () => {
]);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const selectorRegistry = new SelectorScopeRegistry(checker, host);
const noopLoader = new NoopResourceLoader();
const handler = new ComponentDecoratorHandler(
checker, host, new SelectorScopeRegistry(checker, host), false, new NoopResourceLoader(),
[''], false, true);
checker, host, selectorRegistry, false, noopLoader, [''], false, true);
const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration);
const detected = handler.detect(TestCmp, host.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
Expand Down
Expand Up @@ -9,9 +9,8 @@
import * as ts from 'typescript';

import {TypeScriptReflectionHost} from '../../metadata';
import {AbsoluteReference, ResolvedReference} from '../../metadata/src/resolver';
import {AbsoluteReference, NodeReference, ResolvedReference} from '../../metadata/src/resolver';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {NgModuleDecoratorHandler} from '../src/ng_module';
import {SelectorScopeRegistry} from '../src/selector_scope';

describe('SelectorScopeRegistry', () => {
Expand Down Expand Up @@ -73,7 +72,6 @@ describe('SelectorScopeRegistry', () => {
imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')],
});

const ref = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
registry.registerDirective(ProgramCmp, {
name: 'ProgramCmp',
ref: ProgramCmpRef,
Expand All @@ -84,6 +82,7 @@ describe('SelectorScopeRegistry', () => {
inputs: {},
outputs: {},
queries: [],
deps: [],
hasNgTemplateContextGuard: false,
ngTemplateGuards: [],
});
Expand Down Expand Up @@ -155,6 +154,7 @@ describe('SelectorScopeRegistry', () => {
inputs: {},
outputs: {},
queries: [],
deps: [],
hasNgTemplateContextGuard: false,
ngTemplateGuards: [],
});
Expand All @@ -164,4 +164,116 @@ describe('SelectorScopeRegistry', () => {
expect(scope.directives).toBeDefined();
expect(scope.directives.length).toBe(2);
});

it('local dependencies', () => {
const {program} = makeProgram([
{
name: 'node_modules/@angular/core/index.ts',
contents: `
export interface NgComponentDefWithMeta<A, B, C, D, E, F> {}
export interface NgModuleDef<A, B, C, D> {}
`
},
{
name: 'node_modules/some_library/index.d.ts',
contents: `
import {NgModuleDef} from '@angular/core';
import * as i0 from './component';
export declare class SomeModule {
static ngModuleDef: NgModuleDef<SomeModule, [typeof i0.SomeCmp], never, [typeof i0.SomeCmp]>;
}
`
},
{
name: 'node_modules/some_library/component.d.ts',
contents: `
import {NgComponentDefWithMeta} from '@angular/core';
export declare class SomeCmp {
static ngComponentDef: NgComponentDefWithMeta<SomeCmp, 'some-cmp', never, {}, {}, never>;
}
`
},
{
name: 'entry.ts',
contents: `
export class ProgramCmp {}
export class ProgramModule {}
`
},
{
name: 'local.ts',
contents: `
export class Dir {}
export class Pipe {}
`
},
]);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const ProgramModule =
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration);
const Dir = getDeclaration(program, 'local.ts', 'Dir', ts.isClassDeclaration);
const Pipe = getDeclaration(program, 'local.ts', 'Pipe', ts.isClassDeclaration);
const SomeModule = getDeclaration(
program, 'node_modules/some_library/index.d.ts', 'SomeModule', ts.isClassDeclaration);

expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined();

const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
const DirRef = new ResolvedReference(Dir, Dir.name !);

const registry = new SelectorScopeRegistry(checker, host);

registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],
exports: [],
imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')],
});

registry.registerDirective(ProgramCmp, {
name: 'ProgramCmp',
ref: ProgramCmpRef,
directive: ProgramCmpRef,
selector: 'program-cmp',
isComponent: true,
exportAs: null,
inputs: {},
outputs: {},
queries: [],
deps: [
new AbsoluteReference(Dir, Dir.name !, 'local.ts', 'Dir'),
new AbsoluteReference(Pipe, Pipe.name !, 'local.ts', 'Pipe')
],
hasNgTemplateContextGuard: false,
ngTemplateGuards: [],
});

registry.registerDirective(Dir, {
name: 'Dir',
ref: DirRef,
directive: DirRef,
selector: '[dir]',
isComponent: false,
exportAs: null,
inputs: {},
outputs: {},
queries: [],
deps: [],
hasNgTemplateContextGuard: false,
ngTemplateGuards: [],
});

registry.registerPipe(Pipe, 'pipe');

const scope = registry.lookupCompilationScope(ProgramCmp) !;
expect(scope).toBeDefined();
expect(scope.directives).toBeDefined();
expect(scope.directives.length).toBe(3);
expect(scope.pipes).toBeDefined();
expect(scope.pipes.size).toBe(1);
});
});
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/src/api.ts
Expand Up @@ -17,6 +17,7 @@ import {Reference} from '../../metadata';
*/
export interface TypeCheckableDirectiveMeta extends DirectiveMeta {
ref: Reference<ts.ClassDeclaration>;
deps: Reference<ts.Declaration>[];
queries: string[];
ngTemplateGuards: string[];
hasNgTemplateContextGuard: boolean;
Expand Down
42 changes: 42 additions & 0 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Expand Up @@ -969,6 +969,48 @@ describe('ngtsc behavioral tests', () => {
expect(jsContents).toMatch(/directives: \[DirA,\s+DirB\]/);
});

describe('local scope', () => {
it('should compile a component considering the directives in the local scope', () => {
env.tsconfig();
env.write('test.ts', `
import {Component, Directive} from '@angular/core';
@Directive({selector: '[test]'})
class DirA {}
@Component({
template: '<div test></div>',
deps: [DirA]
})
class SampleCmp {}
`);

env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('directives: [DirA]');
});

it('should compile a component considering the pipes in the local scope', () => {
env.tsconfig();
env.write('test.ts', `
import {Component, Pipe} from '@angular/core';
@Pipe({name: 'test'})
class TestPipe {}
@Component({
template: '<div>{{ "text" | test }}</div>',
deps: [TestPipe]
})
class SampleCmp1 {}
`);

env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('pipes: [TestPipe]');
});
});

describe('duplicate local refs', () => {
const getComponentScript = (template: string): string => `
import {Component, Directive, NgModule} from '@angular/core';
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/render3/view/api.ts
Expand Up @@ -13,6 +13,7 @@ import {ParseSourceSpan} from '../../parse_util';
import * as t from '../r3_ast';
import {R3DependencyMetadata} from '../r3_factory';


/**
* Information needed to compile a directive for the render3 runtime.
*/
Expand Down

0 comments on commit 2192263

Please sign in to comment.