diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index f5d623ea0e..15aec30d95 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -15,5 +15,6 @@ ts_library( "//packages/compiler-cli/src/ngtsc/host", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/transform", + "//packages/compiler-cli/src/ngtsc/typecheck", ], ) diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index e394e8c35c..0b76cc9ec1 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -134,7 +134,14 @@ export class ComponentDecoratorHandler implements DecoratorHandler query.propertyName), + isComponent: true, + }); } // Construct the list of view queries. @@ -198,7 +205,9 @@ export class ComponentDecoratorHandler implements DecoratorHandler(); + scope.directives.forEach((meta, selector) => directives.set(selector, meta.directive)); const wrapDirectivesInClosure: boolean = !!containsForwardDecls; analysis = {...analysis, directives, pipes, wrapDirectivesInClosure}; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index c411e47d13..dea96ccaa7 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -40,7 +40,14 @@ export class DirectiveDecoratorHandler implements DecoratorHandler query.propertyName), + isComponent: false, + }); } return {analysis}; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 5714205d6d..60ec75abcd 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -65,13 +65,13 @@ export class NgModuleDecoratorHandler implements DecoratorHandler[] = []; if (ngModule.has('declarations')) { const expr = ngModule.get('declarations') !; const declarationMeta = staticallyResolve(expr, this.reflector, this.checker); declarations = this.resolveTypeList(expr, declarationMeta, 'declarations'); } - let imports: Reference[] = []; + let imports: Reference[] = []; if (ngModule.has('imports')) { const expr = ngModule.get('imports') !; const importsMeta = staticallyResolve( @@ -79,7 +79,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler this._extractModuleFromModuleWithProvidersFn(ref.node)); imports = this.resolveTypeList(expr, importsMeta, 'imports'); } - let exports: Reference[] = []; + let exports: Reference[] = []; if (ngModule.has('exports')) { const expr = ngModule.get('exports') !; const exportsMeta = staticallyResolve( @@ -87,7 +87,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler this._extractModuleFromModuleWithProvidersFn(ref.node)); exports = this.resolveTypeList(expr, exportsMeta, 'exports'); } - let bootstrap: Reference[] = []; + let bootstrap: Reference[] = []; if (ngModule.has('bootstrap')) { const expr = ngModule.get('bootstrap') !; const bootstrapMeta = staticallyResolve(expr, this.reflector, this.checker); @@ -198,8 +198,9 @@ export class NgModuleDecoratorHandler implements DecoratorHandler[] { + const refList: Reference[] = []; if (!Array.isArray(resolvedList)) { throw new FatalDiagnosticError( ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `Expected array when reading property ${name}`); @@ -215,7 +216,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { + return ref instanceof Reference && + (ts.isClassDeclaration(ref.node) || ts.isFunctionDeclaration(ref.node) || + ts.isVariableDeclaration(ref.node)); +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts index ec849c35c5..019f4e395d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts @@ -12,18 +12,18 @@ import * as ts from 'typescript'; import {ReflectionHost} from '../../host'; import {AbsoluteReference, Reference, ResolvedReference, reflectTypeEntityToDeclaration} from '../../metadata'; import {reflectIdentifierOfDeclaration, reflectNameOfDeclaration} from '../../metadata/src/reflector'; +import {TypeCheckableDirectiveMeta} from '../../typecheck'; -import {toR3Reference} from './util'; - +import {extractDirectiveGuards, toR3Reference} from './util'; /** * Metadata extracted for a given NgModule that can be used to compute selector scopes. */ export interface ModuleData { - declarations: Reference[]; - imports: Reference[]; - exports: Reference[]; + declarations: Reference[]; + imports: Reference[]; + exports: Reference[]; } /** @@ -31,11 +31,16 @@ export interface ModuleData { * context of some module. */ export interface CompilationScope { - directives: Map; + directives: Map>; pipes: Map; containsForwardDecls?: boolean; } +export interface ScopeDirective extends TypeCheckableDirectiveMeta { + selector: string; + directive: T; +} + /** * Both transitively expanded scopes for a given NgModule. */ @@ -44,13 +49,13 @@ interface SelectorScopes { * Set of components, directives, and pipes visible to all components being compiled in the * context of some module. */ - compilation: Reference[]; + compilation: Reference[]; /** * Set of components, directives, and pipes added to the compilation scope of any module importing * some module. */ - exported: Reference[]; + exported: Reference[]; } /** @@ -71,9 +76,9 @@ export class SelectorScopeRegistry { private _compilationScopeCache = new Map>(); /** - * Map of components/directives to their selector. + * Map of components/directives to their metadata. */ - private _directiveToSelector = new Map(); + private _directiveToMetadata = new Map>(); /** * Map of pipes to their name. @@ -105,15 +110,16 @@ export class SelectorScopeRegistry { } /** - * Register the selector of a component or directive with the registry. + * Register the metadata of a component or directive with the registry. */ - registerSelector(node: ts.Declaration, selector: string): void { + registerDirective(node: ts.Declaration, metadata: ScopeDirective): void { node = ts.getOriginalNode(node) as ts.Declaration; - if (this._directiveToSelector.has(node)) { - throw new Error(`Selector already registered: ${reflectNameOfDeclaration(node)} ${selector}`); + if (this._directiveToMetadata.has(node)) { + throw new Error( + `Selector already registered: ${reflectNameOfDeclaration(node)} ${metadata.selector}`); } - this._directiveToSelector.set(node, selector); + this._directiveToMetadata.set(node, metadata); } /** @@ -125,11 +131,7 @@ export class SelectorScopeRegistry { this._pipeToName.set(node, name); } - /** - * Produce the compilation scope of a component, which is determined by the module that declares - * it. - */ - lookupCompilationScope(node: ts.Declaration): CompilationScope|null { + lookupCompilationScopeAsRefs(node: ts.Declaration): CompilationScope|null { node = ts.getOriginalNode(node) as ts.Declaration; // If the component has no associated module, then it has no compilation scope. @@ -147,11 +149,11 @@ export class SelectorScopeRegistry { // 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 convertScopeToExpressions(scope, node); + return scope; } // This is the first time the scope for this module is being computed. - const directives = new Map(); + const directives = new Map>>(); const pipes = new Map(); // Process the declaration scope of the module, and lookup the selector of every declared type. @@ -161,10 +163,10 @@ export class SelectorScopeRegistry { const node = ts.getOriginalNode(ref.node) as ts.Declaration; // Either the node represents a directive or a pipe. Look for both. - const selector = this.lookupDirectiveSelector(node); + const metadata = this.lookupDirectiveMetadata(ref); // Only directives/components with selectors get added to the scope. - if (selector != null) { - directives.set(selector, ref); + if (metadata != null) { + directives.set(metadata.selector, {...metadata, directive: ref}); return; } @@ -180,7 +182,16 @@ export class SelectorScopeRegistry { this._compilationScopeCache.set(node, scope); // Convert References to Expressions in the context of the component's source file. - return convertScopeToExpressions(scope, node); + return scope; + } + + /** + * Produce the compilation scope of a component, which is determined by the module that declares + * it. + */ + lookupCompilationScope(node: ts.Declaration): CompilationScope|null { + const scope = this.lookupCompilationScopeAsRefs(node); + return scope !== null ? convertScopeToExpressions(scope, node) : null; } private lookupScopesOrDie(node: ts.Declaration, ngModuleImportedFrom: string|null): @@ -210,7 +221,7 @@ export class SelectorScopeRegistry { } else { // The module wasn't analyzed before, and probably has a precompiled ngModuleDef with a type // annotation that specifies the needed metadata. - data = this._readMetadataFromCompiledClass(node, ngModuleImportedFrom); + data = this._readModuleDataFromCompiledClass(node, ngModuleImportedFrom); // Note that data here could still be null, if the class didn't have a precompiled // ngModuleDef. } @@ -245,17 +256,18 @@ export class SelectorScopeRegistry { } /** - * Lookup the selector of a component or directive class. + * Lookup the metadata of a component or directive class. * * Potentially this class is declared in a .d.ts file or otherwise has a manually created * ngComponentDef/ngDirectiveDef. In this case, the type metadata of that definition is read - * to determine the selector. + * to determine the metadata. */ - private lookupDirectiveSelector(node: ts.Declaration): string|null { - if (this._directiveToSelector.has(node)) { - return this._directiveToSelector.get(node) !; + private lookupDirectiveMetadata(ref: Reference): ScopeDirective|null { + const node = ts.getOriginalNode(ref.node) as ts.Declaration; + if (this._directiveToMetadata.has(node)) { + return this._directiveToMetadata.get(node) !; } else { - return this._readSelectorFromCompiledClass(node); + return this._readMetadataFromCompiledClass(ref as Reference); } } @@ -275,8 +287,8 @@ export class SelectorScopeRegistry { * @param ngModuleImportedFrom module specifier of the import path to assume for all declarations * stemming from this module. */ - private _readMetadataFromCompiledClass(clazz: ts.Declaration, ngModuleImportedFrom: string|null): - ModuleData|null { + private _readModuleDataFromCompiledClass( + clazz: ts.Declaration, ngModuleImportedFrom: string|null): ModuleData|null { // This operation is explicitly not memoized, as it depends on `ngModuleImportedFrom`. // TODO(alxhub): investigate caching of .d.ts module metadata. const ngModuleDef = this.reflector.getMembersOfClass(clazz).find( @@ -304,7 +316,9 @@ export class SelectorScopeRegistry { * Get the selector from type metadata for a class with a precompiled ngComponentDef or * ngDirectiveDef. */ - private _readSelectorFromCompiledClass(clazz: ts.Declaration): string|null { + private _readMetadataFromCompiledClass(ref: Reference): + ScopeDirective|null { + const clazz = ts.getOriginalNode(ref.node) as ts.ClassDeclaration; const def = this.reflector.getMembersOfClass(clazz).find( field => field.isStatic && (field.name === 'ngComponentDef' || field.name === 'ngDirectiveDef')); @@ -317,12 +331,22 @@ export class SelectorScopeRegistry { // The type metadata was the wrong shape. return null; } - const type = def.type.typeArguments[1]; - if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) { - // The type metadata was the wrong type. + const selector = readStringType(def.type.typeArguments[1]); + if (selector === null) { return null; } - return type.literal.text; + + return { + ref, + name: clazz.name !.text, + directive: ref, + isComponent: def.name === 'ngComponentDef', selector, + exportAs: readStringType(def.type.typeArguments[2]), + inputs: readStringMapType(def.type.typeArguments[3]), + outputs: readStringMapType(def.type.typeArguments[4]), + queries: readStringArrayType(def.type.typeArguments[5]), + ...extractDirectiveGuards(clazz, this.reflector), + }; } /** @@ -357,7 +381,7 @@ export class SelectorScopeRegistry { * they themselves were imported from another absolute path. */ private _extractReferencesFromType(def: ts.TypeNode, ngModuleImportedFrom: string|null): - Reference[] { + Reference[] { if (!ts.isTupleTypeNode(def)) { return []; } @@ -394,23 +418,33 @@ function absoluteModuleName(ref: Reference): string|null { return ref.moduleName; } -function convertReferenceMap( +function convertDirectiveReferenceMap( + map: Map>, + context: ts.SourceFile): Map> { + const newMap = new Map>(); + map.forEach((meta, selector) => { + newMap.set(selector, {...meta, directive: toR3Reference(meta.directive, context).value}); + }); + return newMap; +} + +function convertPipeReferenceMap( map: Map, context: ts.SourceFile): Map { - return new Map(Array.from(map.entries()).map(([selector, ref]): [ - string, Expression - ] => [selector, toR3Reference(ref, context).value])); + const newMap = new Map(); + map.forEach((meta, selector) => { newMap.set(selector, toR3Reference(meta, context).value); }); + return newMap; } function convertScopeToExpressions( scope: CompilationScope, context: ts.Declaration): CompilationScope { const sourceContext = ts.getOriginalNode(context).getSourceFile(); - const directives = convertReferenceMap(scope.directives, sourceContext); - const pipes = convertReferenceMap(scope.pipes, sourceContext); + const directives = convertDirectiveReferenceMap(scope.directives, sourceContext); + const pipes = convertPipeReferenceMap(scope.pipes, sourceContext); const declPointer = maybeUnwrapNameOfDeclaration(context); let containsForwardDecls = false; directives.forEach(expr => { - containsForwardDecls = - containsForwardDecls || isExpressionForwardReference(expr, declPointer, sourceContext); + containsForwardDecls = containsForwardDecls || + isExpressionForwardReference(expr.directive, declPointer, sourceContext); }); !containsForwardDecls && pipes.forEach(expr => { containsForwardDecls = @@ -438,4 +472,44 @@ function maybeUnwrapNameOfDeclaration(decl: ts.Declaration): ts.Declaration|ts.I return decl.name; } return decl; -} \ No newline at end of file +} + +function readStringType(type: ts.TypeNode): string|null { + if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) { + return null; + } + return type.literal.text; +} + +function readStringMapType(type: ts.TypeNode): {[key: string]: string} { + if (!ts.isTypeLiteralNode(type)) { + return {}; + } + const obj: {[key: string]: string} = {}; + type.members.forEach(member => { + if (!ts.isPropertySignature(member) || member.type === undefined || member.name === undefined || + !ts.isStringLiteral(member.name)) { + return; + } + const value = readStringType(member.type); + if (value === null) { + return null; + } + obj[member.name.text] = value; + }); + return obj; +} + +function readStringArrayType(type: ts.TypeNode): string[] { + if (!ts.isTupleTypeNode(type)) { + return []; + } + const res: string[] = []; + type.elementTypes.forEach(el => { + if (!ts.isLiteralTypeNode(el) || !ts.isStringLiteral(el.literal)) { + return; + } + res.push(el.literal.text); + }); + return res; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts index 059d6e8933..94de1771e6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts @@ -63,6 +63,8 @@ describe('SelectorScopeRegistry', () => { expect(ProgramModule).toBeDefined(); expect(SomeModule).toBeDefined(); + const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); + const registry = new SelectorScopeRegistry(checker, host); registry.registerModule(ProgramModule, { @@ -71,7 +73,20 @@ describe('SelectorScopeRegistry', () => { imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')], }); - registry.registerSelector(ProgramCmp, 'program-cmp'); + const ref = new ResolvedReference(ProgramCmp, ProgramCmp.name !); + registry.registerDirective(ProgramCmp, { + name: 'ProgramCmp', + ref: ProgramCmpRef, + directive: ProgramCmpRef, + selector: 'program-cmp', + isComponent: true, + exportAs: null, + inputs: {}, + outputs: {}, + queries: [], + hasNgTemplateContextGuard: false, + ngTemplateGuards: [], + }); const scope = registry.lookupCompilationScope(ProgramCmp) !; expect(scope).toBeDefined(); @@ -120,6 +135,8 @@ describe('SelectorScopeRegistry', () => { expect(ProgramModule).toBeDefined(); expect(SomeModule).toBeDefined(); + const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); + const registry = new SelectorScopeRegistry(checker, host); registry.registerModule(ProgramModule, { @@ -128,7 +145,19 @@ describe('SelectorScopeRegistry', () => { imports: [], }); - registry.registerSelector(ProgramCmp, 'program-cmp'); + registry.registerDirective(ProgramCmp, { + name: 'ProgramCmp', + ref: ProgramCmpRef, + directive: ProgramCmpRef, + selector: 'program-cmp', + isComponent: true, + exportAs: null, + inputs: {}, + outputs: {}, + queries: [], + hasNgTemplateContextGuard: false, + ngTemplateGuards: [], + }); const scope = registry.lookupCompilationScope(ProgramCmp) !; expect(scope).toBeDefined(); diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts b/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts index fdb48b9eb9..707be384ba 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/resolver.ts @@ -183,10 +183,10 @@ export class ResolvedReference extends Reference * An `AbsoluteReference` can be resolved to an `Expression`, and if that expression is an import * the module specifier will be an absolute module name, not a relative path. */ -export class AbsoluteReference extends Reference { +export class AbsoluteReference extends Reference { private identifiers: ts.Identifier[] = []; constructor( - node: ts.Node, private primaryIdentifier: ts.Identifier, readonly moduleName: string, + node: T, private primaryIdentifier: ts.Identifier, readonly moduleName: string, readonly symbolName: string) { super(node); }