refactor(ivy): extract selector scope logic to a new ngtsc package (#28852)
This commit splits apart selector_scope.ts in ngtsc and extracts the logic into two separate classes, the LocalModuleScopeRegistry and the DtsModuleScopeResolver. The logic is cleaned up significantly and new tests are added to verify behavior. LocalModuleScopeRegistry implements the NgModule semantics for compilation scopes, and handles NgModules declared in the current compilation unit. DtsModuleScopeResolver implements simpler logic for export scopes and handles NgModules declared in .d.ts files. This is done in preparation for the addition of re-export logic to solve StrictDeps issues. PR Close #28852
This commit is contained in:
parent
fafabc0b92
commit
15c065f9a0
|
@ -32,6 +32,7 @@ ts_library(
|
|||
"//packages/compiler-cli/src/ngtsc/path",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/routing",
|
||||
"//packages/compiler-cli/src/ngtsc/scope",
|
||||
"//packages/compiler-cli/src/ngtsc/shims",
|
||||
"//packages/compiler-cli/src/ngtsc/switch",
|
||||
"//packages/compiler-cli/src/ngtsc/transform",
|
||||
|
|
|
@ -17,6 +17,7 @@ ts_library(
|
|||
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
||||
"//packages/compiler-cli/src/ngtsc/path",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/scope",
|
||||
"//packages/compiler-cli/src/ngtsc/transform",
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"@ngdeps//@types/convert-source-map",
|
||||
|
|
|
@ -10,11 +10,12 @@ import * as path from 'canonical-path';
|
|||
import * as fs from 'fs';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations';
|
||||
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../ngtsc/annotations';
|
||||
import {CycleAnalyzer, ImportGraph} from '../../../ngtsc/cycles';
|
||||
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, ReferenceEmitter} from '../../../ngtsc/imports';
|
||||
import {PartialEvaluator} from '../../../ngtsc/partial_evaluator';
|
||||
import {AbsoluteFsPath, LogicalFileSystem} from '../../../ngtsc/path';
|
||||
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../ngtsc/scope';
|
||||
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../ngtsc/transform';
|
||||
import {DecoratedClass} from '../host/decorated_class';
|
||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
|
@ -71,7 +72,9 @@ export class DecorationAnalyzer {
|
|||
// on whether a bestGuessOwningModule is present in the Reference.
|
||||
new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)),
|
||||
]);
|
||||
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.refEmitter);
|
||||
dtsModuleScopeResolver =
|
||||
new MetadataDtsModuleScopeResolver(this.typeChecker, this.reflectionHost);
|
||||
scopeRegistry = new LocalModuleScopeRegistry(this.dtsModuleScopeResolver);
|
||||
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker);
|
||||
moduleResolver = new ModuleResolver(this.program, this.options, this.host);
|
||||
importGraph = new ImportGraph(this.moduleResolver);
|
||||
|
@ -81,7 +84,7 @@ export class DecorationAnalyzer {
|
|||
new ComponentDecoratorHandler(
|
||||
this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceManager,
|
||||
this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
|
||||
this.moduleResolver, this.cycleAnalyzer),
|
||||
this.moduleResolver, this.cycleAnalyzer, this.refEmitter),
|
||||
new DirectiveDecoratorHandler(
|
||||
this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore),
|
||||
new InjectableDecoratorHandler(this.reflectionHost, this.isCore, /* strictCtorDeps */ false),
|
||||
|
|
|
@ -16,6 +16,7 @@ ts_library(
|
|||
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/routing",
|
||||
"//packages/compiler-cli/src/ngtsc/scope",
|
||||
"//packages/compiler-cli/src/ngtsc/transform",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
|
|
|
@ -16,4 +16,3 @@ export {InjectableDecoratorHandler} from './src/injectable';
|
|||
export {NgModuleDecoratorHandler} from './src/ng_module';
|
||||
export {PipeDecoratorHandler} from './src/pipe';
|
||||
export {NoopReferencesRegistry, ReferencesRegistry} from './src/references_registry';
|
||||
export {CompilationScope, SelectorScopeRegistry} from './src/selector_scope';
|
||||
|
|
|
@ -12,9 +12,10 @@ import * as ts from 'typescript';
|
|||
|
||||
import {CycleAnalyzer} from '../../cycles';
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {ModuleResolver, Reference} from '../../imports';
|
||||
import {ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
||||
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
|
||||
import {LocalModuleScopeRegistry, ScopeDirective, extractDirectiveGuards} from '../../scope';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
import {TypeCheckContext} from '../../typecheck';
|
||||
import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
|
||||
|
@ -22,8 +23,7 @@ import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
|
|||
import {ResourceLoader} from './api';
|
||||
import {extractDirectiveMetadata, extractQueriesFromDecorator, parseFieldArrayValue, queriesFromFields} from './directive';
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {ScopeDirective, SelectorScopeRegistry} from './selector_scope';
|
||||
import {extractDirectiveGuards, isAngularCore, isAngularCoreReference, unwrapExpression} from './util';
|
||||
import {isAngularCore, isAngularCoreReference, unwrapExpression} from './util';
|
||||
|
||||
const EMPTY_MAP = new Map<string, Expression>();
|
||||
const EMPTY_ARRAY: any[] = [];
|
||||
|
@ -41,10 +41,11 @@ export class ComponentDecoratorHandler implements
|
|||
DecoratorHandler<ComponentHandlerData, Decorator> {
|
||||
constructor(
|
||||
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
|
||||
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean,
|
||||
private scopeRegistry: LocalModuleScopeRegistry, private isCore: boolean,
|
||||
private resourceLoader: ResourceLoader, private rootDirs: string[],
|
||||
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean,
|
||||
private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer) {}
|
||||
private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer,
|
||||
private refEmitter: ReferenceEmitter) {}
|
||||
|
||||
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
|
||||
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
||||
|
@ -222,14 +223,13 @@ export class ComponentDecoratorHandler implements
|
|||
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
|
||||
}
|
||||
|
||||
// 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 the component has a selector, it should be registered with the `LocalModuleScopeRegistry`
|
||||
// so that when this component appears in an `@NgModule` scope, its selector can be determined.
|
||||
if (metadata.selector !== null) {
|
||||
const ref = new Reference(node);
|
||||
this.scopeRegistry.registerDirective(node, {
|
||||
this.scopeRegistry.registerDirective({
|
||||
ref,
|
||||
name: node.name !.text,
|
||||
directive: ref,
|
||||
selector: metadata.selector,
|
||||
exportAs: metadata.exportAs,
|
||||
inputs: metadata.inputs,
|
||||
|
@ -313,10 +313,13 @@ export class ComponentDecoratorHandler implements
|
|||
}
|
||||
|
||||
typeCheck(ctx: TypeCheckContext, node: ts.Declaration, meta: ComponentHandlerData): void {
|
||||
const scope = this.scopeRegistry.lookupCompilationScopeAsRefs(node);
|
||||
const matcher = new SelectorMatcher<ScopeDirective<any>>();
|
||||
if (!ts.isClassDeclaration(node)) {
|
||||
return;
|
||||
}
|
||||
const scope = this.scopeRegistry.getScopeForComponent(node);
|
||||
const matcher = new SelectorMatcher<ScopeDirective>();
|
||||
if (scope !== null) {
|
||||
for (const meta of scope.directives) {
|
||||
for (const meta of scope.compilation.directives) {
|
||||
matcher.addSelectables(CssSelector.parse(meta.selector), meta);
|
||||
}
|
||||
ctx.addTemplate(node as ts.ClassDeclaration, meta.parsedTemplate, matcher);
|
||||
|
@ -324,26 +327,33 @@ export class ComponentDecoratorHandler implements
|
|||
}
|
||||
|
||||
resolve(node: ts.ClassDeclaration, analysis: ComponentHandlerData): void {
|
||||
const context = node.getSourceFile();
|
||||
// Check whether this component was registered with an NgModule. If so, it should be compiled
|
||||
// under that module's compilation scope.
|
||||
const scope = this.scopeRegistry.lookupCompilationScope(node);
|
||||
const scope = this.scopeRegistry.getScopeForComponent(node);
|
||||
let metadata = analysis.meta;
|
||||
if (scope !== null) {
|
||||
// Replace the empty components and directives from the analyze() step with a fully expanded
|
||||
// scope. This is possible now because during resolve() the whole compilation unit has been
|
||||
// fully analyzed.
|
||||
const {pipes, containsForwardDecls} = scope;
|
||||
const directives =
|
||||
scope.directives.map(dir => ({selector: dir.selector, expression: dir.directive}));
|
||||
const directives = scope.compilation.directives.map(
|
||||
dir => ({selector: dir.selector, expression: this.refEmitter.emit(dir.ref, context)}));
|
||||
const pipes = new Map<string, Expression>();
|
||||
for (const pipe of scope.compilation.pipes) {
|
||||
pipes.set(pipe.name, this.refEmitter.emit(pipe.ref, context));
|
||||
}
|
||||
|
||||
// Scan through the references of the `scope.directives` array and check whether
|
||||
// any import which needs to be generated for the directive would create a cycle.
|
||||
const origin = node.getSourceFile();
|
||||
const cycleDetected =
|
||||
scope.directives.some(meta => this._isCyclicImport(meta.directive, origin)) ||
|
||||
Array.from(scope.pipes.values()).some(pipe => this._isCyclicImport(pipe, origin));
|
||||
const cycleDetected = directives.some(dir => this._isCyclicImport(dir.expression, origin)) ||
|
||||
Array.from(pipes.values()).some(pipe => this._isCyclicImport(pipe, origin));
|
||||
if (!cycleDetected) {
|
||||
const wrapDirectivesAndPipesInClosure: boolean = !!containsForwardDecls;
|
||||
const wrapDirectivesAndPipesInClosure =
|
||||
directives.some(
|
||||
dir => isExpressionForwardReference(dir.expression, node.name !, origin)) ||
|
||||
Array.from(pipes.values())
|
||||
.some(pipe => isExpressionForwardReference(pipe, node.name !, origin));
|
||||
metadata.directives = directives;
|
||||
metadata.pipes = pipes;
|
||||
metadata.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure;
|
||||
|
@ -446,3 +456,17 @@ function getTemplateRange(templateExpr: ts.Expression) {
|
|||
endPos: templateExpr.getEnd() - 1,
|
||||
};
|
||||
}
|
||||
|
||||
function isExpressionForwardReference(
|
||||
expr: Expression, context: ts.Node, contextSource: ts.SourceFile): boolean {
|
||||
if (isWrappedTsNodeExpr(expr)) {
|
||||
const node = ts.getOriginalNode(expr.node);
|
||||
return node.getSourceFile() === contextSource && context.pos < node.pos;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr<ts.Node> {
|
||||
return expr instanceof WrappedNodeExpr;
|
||||
}
|
||||
|
|
|
@ -13,11 +13,12 @@ import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
|||
import {Reference} from '../../imports';
|
||||
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
|
||||
import {LocalModuleScopeRegistry} from '../../scope/src/local';
|
||||
import {extractDirectiveGuards} from '../../scope/src/util';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {SelectorScopeRegistry} from './selector_scope';
|
||||
import {extractDirectiveGuards, getValidConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util';
|
||||
import {getValidConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util';
|
||||
|
||||
const EMPTY_OBJECT: {[key: string]: string} = {};
|
||||
|
||||
|
@ -29,7 +30,7 @@ export class DirectiveDecoratorHandler implements
|
|||
DecoratorHandler<DirectiveHandlerData, Decorator> {
|
||||
constructor(
|
||||
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
|
||||
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
|
||||
private scopeRegistry: LocalModuleScopeRegistry, private isCore: boolean) {}
|
||||
|
||||
readonly precedence = HandlerPrecedence.PRIMARY;
|
||||
|
||||
|
@ -58,9 +59,8 @@ export class DirectiveDecoratorHandler implements
|
|||
// when this directive appears in an `@NgModule` scope, its selector can be determined.
|
||||
if (analysis && analysis.selector !== null) {
|
||||
const ref = new Reference(node);
|
||||
this.scopeRegistry.registerDirective(node, {
|
||||
this.scopeRegistry.registerDirective({
|
||||
ref,
|
||||
directive: ref,
|
||||
name: node.name !.text,
|
||||
selector: analysis.selector,
|
||||
exportAs: analysis.exportAs,
|
||||
|
|
|
@ -14,12 +14,12 @@ import {Reference, ReferenceEmitter} from '../../imports';
|
|||
import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
|
||||
import {Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection';
|
||||
import {NgModuleRouteAnalyzer} from '../../routing';
|
||||
import {LocalModuleScopeRegistry} from '../../scope';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
import {getSourceFile} from '../../util/src/typescript';
|
||||
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {ReferencesRegistry} from './references_registry';
|
||||
import {SelectorScopeRegistry} from './selector_scope';
|
||||
import {getValidConstructorDependencies, isAngularCore, toR3Reference, unwrapExpression} from './util';
|
||||
|
||||
export interface NgModuleAnalysis {
|
||||
|
@ -37,9 +37,9 @@ export interface NgModuleAnalysis {
|
|||
export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis, Decorator> {
|
||||
constructor(
|
||||
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
|
||||
private scopeRegistry: SelectorScopeRegistry, private referencesRegistry: ReferencesRegistry,
|
||||
private isCore: boolean, private routeAnalyzer: NgModuleRouteAnalyzer|null,
|
||||
private refEmitter: ReferenceEmitter) {}
|
||||
private scopeRegistry: LocalModuleScopeRegistry,
|
||||
private referencesRegistry: ReferencesRegistry, private isCore: boolean,
|
||||
private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter) {}
|
||||
|
||||
readonly precedence = HandlerPrecedence.PRIMARY;
|
||||
|
||||
|
@ -114,9 +114,10 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
|||
bootstrap = this.resolveTypeList(expr, bootstrapMeta, 'bootstrap');
|
||||
}
|
||||
|
||||
// Register this module's information with the SelectorScopeRegistry. This ensures that during
|
||||
// the compile() phase, the module's metadata is available for selector scope computation.
|
||||
this.scopeRegistry.registerModule(node, {declarations, imports, exports});
|
||||
// Register this module's information with the LocalModuleScopeRegistry. This ensures that
|
||||
// during the compile() phase, the module's metadata is available for selector scope
|
||||
// computation.
|
||||
this.scopeRegistry.registerNgModule(node, {declarations, imports, exports});
|
||||
|
||||
const valueContext = node.getSourceFile();
|
||||
|
||||
|
@ -183,16 +184,15 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
|||
}
|
||||
const context = getSourceFile(node);
|
||||
for (const decl of analysis.declarations) {
|
||||
if (this.scopeRegistry.requiresRemoteScope(decl.node)) {
|
||||
const scope = this.scopeRegistry.lookupCompilationScopeAsRefs(decl.node);
|
||||
if (this.scopeRegistry.getRequiresRemoteScope(decl.node)) {
|
||||
const scope =
|
||||
this.scopeRegistry.getScopeOfModule(ts.getOriginalNode(node) as ts.Declaration);
|
||||
if (scope === null) {
|
||||
continue;
|
||||
}
|
||||
const directives: Expression[] = [];
|
||||
const pipes: Expression[] = [];
|
||||
scope.directives.forEach(
|
||||
(directive, _) => { directives.push(this.refEmitter.emit(directive.ref, context) !); });
|
||||
scope.pipes.forEach(pipe => pipes.push(this.refEmitter.emit(pipe, context) !));
|
||||
const directives = scope.compilation.directives.map(
|
||||
directive => this.refEmitter.emit(directive.ref, context));
|
||||
const pipes = scope.compilation.pipes.map(pipe => this.refEmitter.emit(pipe.ref, context));
|
||||
const directiveArray = new LiteralArrayExpr(directives);
|
||||
const pipesArray = new LiteralArrayExpr(pipes);
|
||||
const declExpr = this.refEmitter.emit(decl, context) !;
|
||||
|
|
|
@ -10,12 +10,13 @@ import {LiteralExpr, R3PipeMetadata, Statement, WrappedNodeExpr, compilePipeFrom
|
|||
import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {Reference} from '../../imports';
|
||||
import {PartialEvaluator} from '../../partial_evaluator';
|
||||
import {Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||
import {LocalModuleScopeRegistry} from '../../scope/src/local';
|
||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
|
||||
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {SelectorScopeRegistry} from './selector_scope';
|
||||
import {getValidConstructorDependencies, isAngularCore, unwrapExpression} from './util';
|
||||
|
||||
export interface PipeHandlerData {
|
||||
|
@ -26,7 +27,7 @@ export interface PipeHandlerData {
|
|||
export class PipeDecoratorHandler implements DecoratorHandler<PipeHandlerData, Decorator> {
|
||||
constructor(
|
||||
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
|
||||
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
|
||||
private scopeRegistry: LocalModuleScopeRegistry, private isCore: boolean) {}
|
||||
|
||||
readonly precedence = HandlerPrecedence.PRIMARY;
|
||||
|
||||
|
@ -78,7 +79,8 @@ export class PipeDecoratorHandler implements DecoratorHandler<PipeHandlerData, D
|
|||
throw new FatalDiagnosticError(
|
||||
ErrorCode.VALUE_HAS_WRONG_TYPE, pipeNameExpr, `@Pipe.name must be a string`);
|
||||
}
|
||||
this.scopeRegistry.registerPipe(clazz, pipeName);
|
||||
const ref = new Reference(clazz);
|
||||
this.scopeRegistry.registerPipe({ref, name: pipeName});
|
||||
|
||||
let pure = true;
|
||||
if (pipe.has('pure')) {
|
||||
|
|
|
@ -1,562 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Expression, WrappedNodeExpr} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference, ReferenceEmitter} from '../../imports';
|
||||
import {ReflectionHost, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectTypeEntityToDeclaration} from '../../reflection';
|
||||
import {TypeCheckableDirectiveMeta} from '../../typecheck';
|
||||
|
||||
import {extractDirectiveGuards} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* Metadata extracted for a given NgModule that can be used to compute selector scopes.
|
||||
*/
|
||||
export interface ModuleData {
|
||||
declarations: Reference<ts.Declaration>[];
|
||||
imports: Reference<ts.Declaration>[];
|
||||
exports: Reference<ts.Declaration>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transitively expanded maps of directives and pipes visible to a component being compiled in the
|
||||
* context of some module.
|
||||
*/
|
||||
export interface CompilationScope<T> {
|
||||
directives: ScopeDirective<T>[];
|
||||
pipes: Map<string, T>;
|
||||
containsForwardDecls?: boolean;
|
||||
}
|
||||
|
||||
export interface ScopeDirective<T> extends TypeCheckableDirectiveMeta {
|
||||
selector: string;
|
||||
directive: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Both transitively expanded scopes for a given NgModule.
|
||||
*/
|
||||
interface SelectorScopes {
|
||||
/**
|
||||
* Set of components, directives, and pipes visible to all components being compiled in the
|
||||
* context of some module.
|
||||
*/
|
||||
compilation: Reference<ts.Declaration>[];
|
||||
|
||||
/**
|
||||
* Set of components, directives, and pipes added to the compilation scope of any module importing
|
||||
* some module.
|
||||
*/
|
||||
exported: Reference<ts.Declaration>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry which records and correlates static analysis information of Angular types.
|
||||
*
|
||||
* Once a compilation unit's information is fed into the SelectorScopeRegistry, it can be asked to
|
||||
* produce transitive `CompilationScope`s for components.
|
||||
*/
|
||||
export class SelectorScopeRegistry {
|
||||
/**
|
||||
* Map of modules declared in the current compilation unit to their (local) metadata.
|
||||
*/
|
||||
private _moduleToData = new Map<ts.Declaration, ModuleData>();
|
||||
|
||||
/**
|
||||
* Map of modules to their cached `CompilationScope`s.
|
||||
*/
|
||||
private _compilationScopeCache = new Map<ts.Declaration, CompilationScope<Reference>>();
|
||||
|
||||
/**
|
||||
* Map of components/directives to their metadata.
|
||||
*/
|
||||
private _directiveToMetadata = new Map<ts.Declaration, ScopeDirective<Reference>>();
|
||||
|
||||
/**
|
||||
* Map of pipes to their name.
|
||||
*/
|
||||
private _pipeToName = new Map<ts.Declaration, string>();
|
||||
|
||||
/**
|
||||
* Components that require remote scoping.
|
||||
*/
|
||||
private _requiresRemoteScope = new Set<ts.Declaration>();
|
||||
|
||||
/**
|
||||
* Map of components/directives/pipes to their module.
|
||||
*/
|
||||
private _declararedTypeToModule = new Map<ts.Declaration, ts.Declaration>();
|
||||
|
||||
constructor(
|
||||
private checker: ts.TypeChecker, private reflector: ReflectionHost,
|
||||
private refEmitter: ReferenceEmitter) {}
|
||||
|
||||
/**
|
||||
* Register a module's metadata with the registry.
|
||||
*/
|
||||
registerModule(node: ts.Declaration, data: ModuleData): void {
|
||||
node = ts.getOriginalNode(node) as ts.Declaration;
|
||||
|
||||
if (this._moduleToData.has(node)) {
|
||||
throw new Error(`Module already registered: ${reflectNameOfDeclaration(node)}`);
|
||||
}
|
||||
this._moduleToData.set(node, data);
|
||||
|
||||
// Register all of the module's declarations in the context map as belonging to this module.
|
||||
data.declarations.forEach(decl => {
|
||||
this._declararedTypeToModule.set(ts.getOriginalNode(decl.node) as ts.Declaration, node);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the metadata of a component or directive with the registry.
|
||||
*/
|
||||
registerDirective(node: ts.Declaration, metadata: ScopeDirective<Reference>): void {
|
||||
node = ts.getOriginalNode(node) as ts.Declaration;
|
||||
|
||||
if (this._directiveToMetadata.has(node)) {
|
||||
throw new Error(
|
||||
`Selector already registered: ${reflectNameOfDeclaration(node)} ${metadata.selector}`);
|
||||
}
|
||||
this._directiveToMetadata.set(node, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the name of a pipe with the registry.
|
||||
*/
|
||||
registerPipe(node: ts.Declaration, name: string): void {
|
||||
node = ts.getOriginalNode(node) as ts.Declaration;
|
||||
|
||||
this._pipeToName.set(node, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a component (identified by its `ts.Declaration`) as requiring its `directives` scope to be
|
||||
* set remotely, from the file of the @NgModule which declares the component.
|
||||
*/
|
||||
setComponentAsRequiringRemoteScoping(component: ts.Declaration): void {
|
||||
this._requiresRemoteScope.add(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given component requires its `directives` scope to be set remotely.
|
||||
*/
|
||||
requiresRemoteScope(component: ts.Declaration): boolean {
|
||||
return this._requiresRemoteScope.has(ts.getOriginalNode(component) as ts.Declaration);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)) {
|
||||
// The compilation scope was cached.
|
||||
const scope = this._compilationScopeCache.get(module) !;
|
||||
|
||||
// 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, node.getSourceFile().fileName)
|
||||
.compilation) {
|
||||
const node = ts.getOriginalNode(ref.node) as ts.Declaration;
|
||||
|
||||
// Track whether this `ts.Declaration` has been seen before.
|
||||
if (seenSet.has(node)) {
|
||||
continue;
|
||||
} else {
|
||||
seenSet.add(node);
|
||||
}
|
||||
|
||||
// Either the node represents a directive or a pipe. Look for both.
|
||||
const metadata = this.lookupDirectiveMetadata(ref);
|
||||
// Only directives/components with selectors get added to the scope.
|
||||
if (metadata !== null) {
|
||||
directives.push({...metadata, directive: ref});
|
||||
} else {
|
||||
const name = this.lookupPipeName(node);
|
||||
if (name !== null) {
|
||||
pipes.set(name, ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scope: CompilationScope<Reference> = {directives, pipes};
|
||||
|
||||
// Many components may be compiled in the same scope, so cache it.
|
||||
this._compilationScopeCache.set(node, scope);
|
||||
|
||||
// Convert References to Expressions in the context of the component's source file.
|
||||
return scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce the compilation scope of a component, which is determined by the module that declares
|
||||
* it.
|
||||
*/
|
||||
lookupCompilationScope(node: ts.Declaration): CompilationScope<Expression>|null {
|
||||
const scope = this.lookupCompilationScopeAsRefs(node);
|
||||
return scope !== null ? convertScopeToExpressions(scope, node, this.refEmitter) : null;
|
||||
}
|
||||
|
||||
private lookupScopesOrDie(
|
||||
node: ts.Declaration, ngModuleImportedFrom: string|null,
|
||||
resolutionContext: string): SelectorScopes {
|
||||
const result = this.lookupScopes(node, ngModuleImportedFrom, resolutionContext);
|
||||
if (result === null) {
|
||||
throw new Error(`Module not found: ${reflectNameOfDeclaration(node)}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup `SelectorScopes` for a given module.
|
||||
*
|
||||
* This function assumes that if the given module was imported from an absolute path
|
||||
* (`ngModuleImportedFrom`) then all of its declarations are exported at that same path, as well
|
||||
* as imports and exports from other modules that are relatively imported.
|
||||
*/
|
||||
private lookupScopes(
|
||||
node: ts.Declaration, ngModuleImportedFrom: string|null,
|
||||
resolutionContext: string): SelectorScopes|null {
|
||||
let data: ModuleData|null = null;
|
||||
|
||||
// Either this module was analyzed directly, or has a precompiled ngModuleDef.
|
||||
if (this._moduleToData.has(node)) {
|
||||
// The module was analyzed before, and thus its data is available.
|
||||
data = this._moduleToData.get(node) !;
|
||||
} else {
|
||||
// The module wasn't analyzed before, and probably has a precompiled ngModuleDef with a type
|
||||
// annotation that specifies the needed metadata.
|
||||
data = this._readModuleDataFromCompiledClass(node, ngModuleImportedFrom, resolutionContext);
|
||||
// Note that data here could still be null, if the class didn't have a precompiled
|
||||
// ngModuleDef.
|
||||
}
|
||||
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const context = node.getSourceFile().fileName;
|
||||
|
||||
return {
|
||||
compilation: [
|
||||
...data.declarations,
|
||||
// Expand imports to the exported scope of those imports.
|
||||
...flatten(data.imports.map(
|
||||
ref =>
|
||||
this.lookupScopesOrDie(ref.node as ts.Declaration, ref.ownedByModuleGuess, context)
|
||||
.exported)),
|
||||
// And include the compilation scope of exported modules.
|
||||
...flatten(
|
||||
data.exports
|
||||
.map(
|
||||
ref => this.lookupScopes(
|
||||
ref.node as ts.Declaration, ref.ownedByModuleGuess, context))
|
||||
.filter((scope: SelectorScopes | null): scope is SelectorScopes => scope !== null)
|
||||
.map(scope => scope.exported))
|
||||
],
|
||||
exported: flatten(data.exports.map(ref => {
|
||||
const scope =
|
||||
this.lookupScopes(ref.node as ts.Declaration, ref.ownedByModuleGuess, context);
|
||||
if (scope !== null) {
|
||||
return scope.exported;
|
||||
} else {
|
||||
return [ref];
|
||||
}
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 metadata.
|
||||
*/
|
||||
private lookupDirectiveMetadata(ref: Reference<ts.Declaration>): ScopeDirective<Reference>|null {
|
||||
const node = ts.getOriginalNode(ref.node) as ts.Declaration;
|
||||
if (this._directiveToMetadata.has(node)) {
|
||||
return this._directiveToMetadata.get(node) !;
|
||||
} else {
|
||||
return this._readMetadataFromCompiledClass(ref as Reference<ts.ClassDeclaration>);
|
||||
}
|
||||
}
|
||||
|
||||
private lookupPipeName(node: ts.Declaration): string|null {
|
||||
if (this._pipeToName.has(node)) {
|
||||
return this._pipeToName.get(node) !;
|
||||
} else {
|
||||
return this._readNameFromCompiledClass(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts
|
||||
* file, or in a .ts file with a handwritten definition).
|
||||
*
|
||||
* @param clazz the class of interest
|
||||
* @param ngModuleImportedFrom module specifier of the import path to assume for all declarations
|
||||
* stemming from this module.
|
||||
*/
|
||||
private _readModuleDataFromCompiledClass(
|
||||
clazz: ts.Declaration, ngModuleImportedFrom: string|null,
|
||||
resolutionContext: string): 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(
|
||||
member => member.name === 'ngModuleDef' && member.isStatic);
|
||||
if (ngModuleDef === undefined) {
|
||||
return null;
|
||||
} else if (
|
||||
// Validate that the shape of the ngModuleDef type is correct.
|
||||
ngModuleDef.type === null || !ts.isTypeReferenceNode(ngModuleDef.type) ||
|
||||
ngModuleDef.type.typeArguments === undefined ||
|
||||
ngModuleDef.type.typeArguments.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read the ModuleData out of the type arguments.
|
||||
const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments;
|
||||
return {
|
||||
declarations: this._extractReferencesFromType(
|
||||
declarationMetadata, ngModuleImportedFrom, resolutionContext),
|
||||
exports:
|
||||
this._extractReferencesFromType(exportMetadata, ngModuleImportedFrom, resolutionContext),
|
||||
imports:
|
||||
this._extractReferencesFromType(importMetadata, ngModuleImportedFrom, resolutionContext),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selector from type metadata for a class with a precompiled ngComponentDef or
|
||||
* ngDirectiveDef.
|
||||
*/
|
||||
private _readMetadataFromCompiledClass(ref: Reference<ts.ClassDeclaration>):
|
||||
ScopeDirective<Reference>|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'));
|
||||
if (def === undefined) {
|
||||
// No definition could be found.
|
||||
return null;
|
||||
} else if (
|
||||
def.type === null || !ts.isTypeReferenceNode(def.type) ||
|
||||
def.type.typeArguments === undefined || def.type.typeArguments.length < 2) {
|
||||
// The type metadata was the wrong shape.
|
||||
return null;
|
||||
}
|
||||
const selector = readStringType(def.type.typeArguments[1]);
|
||||
if (selector === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
ref,
|
||||
name: clazz.name !.text,
|
||||
directive: ref,
|
||||
isComponent: def.name === 'ngComponentDef', selector,
|
||||
exportAs: readStringArrayType(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),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selector from type metadata for a class with a precompiled ngComponentDef or
|
||||
* ngDirectiveDef.
|
||||
*/
|
||||
private _readNameFromCompiledClass(clazz: ts.Declaration): string|null {
|
||||
const def = this.reflector.getMembersOfClass(clazz).find(
|
||||
field => field.isStatic && field.name === 'ngPipeDef');
|
||||
if (def === undefined) {
|
||||
// No definition could be found.
|
||||
return null;
|
||||
} else if (
|
||||
def.type === null || !ts.isTypeReferenceNode(def.type) ||
|
||||
def.type.typeArguments === undefined || def.type.typeArguments.length < 2) {
|
||||
// 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.
|
||||
return null;
|
||||
}
|
||||
return type.literal.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a `TypeNode` which is a tuple of references to other types, and return `Reference`s to
|
||||
* them.
|
||||
*
|
||||
* This operation assumes that these types should be imported from `ngModuleImportedFrom` unless
|
||||
* they themselves were imported from another absolute path.
|
||||
*/
|
||||
private _extractReferencesFromType(
|
||||
def: ts.TypeNode, ngModuleImportedFrom: string|null,
|
||||
resolutionContext: string): Reference<ts.Declaration>[] {
|
||||
if (!ts.isTupleTypeNode(def)) {
|
||||
return [];
|
||||
}
|
||||
return def.elementTypes.map(element => {
|
||||
if (!ts.isTypeQueryNode(element)) {
|
||||
throw new Error(`Expected TypeQueryNode`);
|
||||
}
|
||||
const type = element.exprName;
|
||||
if (ngModuleImportedFrom !== null) {
|
||||
const {node, from} = reflectTypeEntityToDeclaration(type, this.checker);
|
||||
const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
|
||||
return new Reference(node, {specifier, resolutionContext});
|
||||
} else {
|
||||
const {node} = reflectTypeEntityToDeclaration(type, this.checker);
|
||||
return new Reference(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function flatten<T>(array: T[][]): T[] {
|
||||
return array.reduce((accum, subArray) => {
|
||||
accum.push(...subArray);
|
||||
return accum;
|
||||
}, [] as T[]);
|
||||
}
|
||||
|
||||
function convertDirectiveReferenceList(
|
||||
input: ScopeDirective<Reference>[], context: ts.SourceFile,
|
||||
refEmitter: ReferenceEmitter): ScopeDirective<Expression>[] {
|
||||
return input.map(meta => {
|
||||
const directive = refEmitter.emit(meta.directive, context);
|
||||
if (directive === null) {
|
||||
throw new Error(`Could not write expression to reference ${meta.directive.node}`);
|
||||
}
|
||||
return {...meta, directive};
|
||||
});
|
||||
}
|
||||
|
||||
function convertPipeReferenceMap(
|
||||
map: Map<string, Reference>, context: ts.SourceFile,
|
||||
refEmitter: ReferenceEmitter): Map<string, Expression> {
|
||||
const newMap = new Map<string, Expression>();
|
||||
map.forEach((meta, selector) => {
|
||||
const pipe = refEmitter.emit(meta, context);
|
||||
if (pipe === null) {
|
||||
throw new Error(`Could not write expression to reference ${meta.node}`);
|
||||
}
|
||||
newMap.set(selector, pipe);
|
||||
});
|
||||
return newMap;
|
||||
}
|
||||
|
||||
function convertScopeToExpressions(
|
||||
scope: CompilationScope<Reference>, context: ts.Declaration,
|
||||
refEmitter: ReferenceEmitter): CompilationScope<Expression> {
|
||||
const sourceContext = ts.getOriginalNode(context).getSourceFile();
|
||||
const directives = convertDirectiveReferenceList(scope.directives, sourceContext, refEmitter);
|
||||
const pipes = convertPipeReferenceMap(scope.pipes, sourceContext, refEmitter);
|
||||
const declPointer = maybeUnwrapNameOfDeclaration(context);
|
||||
let containsForwardDecls = false;
|
||||
directives.forEach(meta => {
|
||||
containsForwardDecls = containsForwardDecls ||
|
||||
isExpressionForwardReference(meta.directive, declPointer, sourceContext);
|
||||
});
|
||||
!containsForwardDecls && pipes.forEach(expr => {
|
||||
containsForwardDecls =
|
||||
containsForwardDecls || isExpressionForwardReference(expr, declPointer, sourceContext);
|
||||
});
|
||||
return {directives, pipes, containsForwardDecls};
|
||||
}
|
||||
|
||||
function isExpressionForwardReference(
|
||||
expr: Expression, context: ts.Node, contextSource: ts.SourceFile): boolean {
|
||||
if (isWrappedTsNodeExpr(expr)) {
|
||||
const node = ts.getOriginalNode(expr.node);
|
||||
return node.getSourceFile() === contextSource && context.pos < node.pos;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr<ts.Node> {
|
||||
return expr instanceof WrappedNodeExpr;
|
||||
}
|
||||
|
||||
function maybeUnwrapNameOfDeclaration(decl: ts.Declaration): ts.Declaration|ts.Identifier {
|
||||
if ((ts.isClassDeclaration(decl) || ts.isVariableDeclaration(decl)) && decl.name !== undefined &&
|
||||
ts.isIdentifier(decl.name)) {
|
||||
return decl.name;
|
||||
}
|
||||
return decl;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -213,20 +213,3 @@ export function forwardRefResolver(
|
|||
}
|
||||
return expandForwardRef(args[0]);
|
||||
}
|
||||
|
||||
export function extractDirectiveGuards(node: ts.Declaration, reflector: ReflectionHost): {
|
||||
ngTemplateGuards: string[],
|
||||
hasNgTemplateContextGuard: boolean,
|
||||
} {
|
||||
const methods = nodeStaticMethodNames(node, reflector);
|
||||
const ngTemplateGuards = methods.filter(method => method.startsWith('ngTemplateGuard_'))
|
||||
.map(method => method.split('_', 2)[1]);
|
||||
const hasNgTemplateContextGuard = methods.some(name => name === 'ngTemplateContextGuard');
|
||||
return {hasNgTemplateContextGuard, ngTemplateGuards};
|
||||
}
|
||||
|
||||
function nodeStaticMethodNames(node: ts.Declaration, reflector: ReflectionHost): string[] {
|
||||
return reflector.getMembersOfClass(node)
|
||||
.filter(member => member.kind === ClassMemberKind.Method && member.isStatic)
|
||||
.map(member => member.name);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ ts_library(
|
|||
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
||||
"//packages/compiler-cli/src/ngtsc/path",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/scope",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
|
|
|
@ -13,10 +13,10 @@ import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
|||
import {ModuleResolver, ReferenceEmitter} from '../../imports';
|
||||
import {PartialEvaluator} from '../../partial_evaluator';
|
||||
import {TypeScriptReflectionHost} from '../../reflection';
|
||||
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
|
||||
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {ResourceLoader} from '../src/api';
|
||||
import {ComponentDecoratorHandler} from '../src/component';
|
||||
import {SelectorScopeRegistry} from '../src/selector_scope';
|
||||
|
||||
export class NoopResourceLoader implements ResourceLoader {
|
||||
resolve(): string { throw new Error('Not implemented.'); }
|
||||
|
@ -48,11 +48,13 @@ describe('ComponentDecoratorHandler', () => {
|
|||
const moduleResolver = new ModuleResolver(program, options, host);
|
||||
const importGraph = new ImportGraph(moduleResolver);
|
||||
const cycleAnalyzer = new CycleAnalyzer(importGraph);
|
||||
const scopeRegistry =
|
||||
new LocalModuleScopeRegistry(new MetadataDtsModuleScopeResolver(checker, reflectionHost));
|
||||
const refEmitter = new ReferenceEmitter([]);
|
||||
|
||||
const handler = new ComponentDecoratorHandler(
|
||||
reflectionHost, evaluator,
|
||||
new SelectorScopeRegistry(checker, reflectionHost, new ReferenceEmitter([])), false,
|
||||
new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer);
|
||||
reflectionHost, evaluator, scopeRegistry, false, new NoopResourceLoader(), [''], false,
|
||||
true, moduleResolver, cycleAnalyzer, refEmitter);
|
||||
const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration);
|
||||
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
|
||||
if (detected === undefined) {
|
||||
|
|
|
@ -8,17 +8,16 @@
|
|||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ReferenceEmitter} from '../../imports';
|
||||
import {PartialEvaluator} from '../../partial_evaluator';
|
||||
import {TypeScriptReflectionHost} from '../../reflection';
|
||||
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
|
||||
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {DirectiveDecoratorHandler} from '../src/directive';
|
||||
import {SelectorScopeRegistry} from '../src/selector_scope';
|
||||
|
||||
|
||||
describe('DirectiveDecoratorHandler', () => {
|
||||
it('should use the `ReflectionHost` to detect class inheritance', () => {
|
||||
const {program, options, host} = makeProgram([
|
||||
const {program} = makeProgram([
|
||||
{
|
||||
name: 'node_modules/@angular/core/index.d.ts',
|
||||
contents: 'export const Directive: any;',
|
||||
|
@ -40,9 +39,9 @@ describe('DirectiveDecoratorHandler', () => {
|
|||
const checker = program.getTypeChecker();
|
||||
const reflectionHost = new TestReflectionHost(checker);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker);
|
||||
const handler = new DirectiveDecoratorHandler(
|
||||
reflectionHost, evaluator,
|
||||
new SelectorScopeRegistry(checker, reflectionHost, new ReferenceEmitter([])), false);
|
||||
const scopeRegistry =
|
||||
new LocalModuleScopeRegistry(new MetadataDtsModuleScopeResolver(checker, reflectionHost));
|
||||
const handler = new DirectiveDecoratorHandler(reflectionHost, evaluator, scopeRegistry, false);
|
||||
|
||||
const analyzeDirective = (dirName: string) => {
|
||||
const DirNode = getDeclaration(program, 'entry.ts', dirName, ts.isClassDeclaration);
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports';
|
||||
import {LogicalFileSystem} from '../../path';
|
||||
import {TypeScriptReflectionHost} from '../../reflection';
|
||||
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {getRootDirs} from '../../util/src/typescript';
|
||||
import {SelectorScopeRegistry} from '../src/selector_scope';
|
||||
|
||||
describe('SelectorScopeRegistry', () => {
|
||||
it('absolute imports work', () => {
|
||||
const {program, options, host} = makeProgram([
|
||||
{
|
||||
name: 'node_modules/@angular/core/index.d.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 {SomeCmp} 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 {}
|
||||
`
|
||||
},
|
||||
]);
|
||||
const checker = program.getTypeChecker();
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const ProgramModule =
|
||||
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
|
||||
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', 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 Reference(ProgramCmp);
|
||||
const refEmitter = makeReferenceEmitter(program, checker, options, host);
|
||||
const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter);
|
||||
|
||||
registry.registerModule(ProgramModule, {
|
||||
declarations: [new Reference(ProgramCmp)],
|
||||
exports: [],
|
||||
imports: [new Reference(
|
||||
SomeModule,
|
||||
{specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})],
|
||||
});
|
||||
|
||||
const ref = new Reference(ProgramCmp);
|
||||
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();
|
||||
expect(scope.directives).toBeDefined();
|
||||
expect(scope.directives.length).toBe(2);
|
||||
});
|
||||
|
||||
it('exports of third-party libs work', () => {
|
||||
const {program, options, host} = makeProgram([
|
||||
{
|
||||
name: 'node_modules/@angular/core/index.d.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 {NgComponentDefWithMeta, NgModuleDef} from '@angular/core';
|
||||
|
||||
export declare class SomeModule {
|
||||
static ngModuleDef: NgModuleDef<SomeModule, [typeof SomeCmp], never, [typeof SomeCmp]>;
|
||||
}
|
||||
|
||||
export declare class SomeCmp {
|
||||
static ngComponentDef: NgComponentDefWithMeta<SomeCmp, 'some-cmp', never, {}, {}, never>;
|
||||
}
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
contents: `
|
||||
export class ProgramCmp {}
|
||||
export class ProgramModule {}
|
||||
`
|
||||
},
|
||||
]);
|
||||
const checker = program.getTypeChecker();
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const ProgramModule =
|
||||
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
|
||||
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', 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 Reference(ProgramCmp);
|
||||
const refEmitter = makeReferenceEmitter(program, checker, options, host);
|
||||
const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter);
|
||||
|
||||
registry.registerModule(ProgramModule, {
|
||||
declarations: [new Reference(ProgramCmp)],
|
||||
exports: [new Reference(
|
||||
SomeModule,
|
||||
{specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})],
|
||||
imports: [],
|
||||
});
|
||||
|
||||
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();
|
||||
expect(scope.directives).toBeDefined();
|
||||
expect(scope.directives.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
function makeReferenceEmitter(
|
||||
program: ts.Program, checker: ts.TypeChecker, options: ts.CompilerOptions,
|
||||
host: ts.CompilerHost): ReferenceEmitter {
|
||||
const rootDirs = getRootDirs(host, options);
|
||||
return new ReferenceEmitter([
|
||||
new LocalIdentifierStrategy(),
|
||||
new AbsoluteModuleStrategy(program, checker, options, host),
|
||||
new LogicalProjectStrategy(checker, new LogicalFileSystem(rootDirs)),
|
||||
]);
|
||||
}
|
|
@ -12,18 +12,18 @@ import * as ts from 'typescript';
|
|||
import * as api from '../transformers/api';
|
||||
import {nocollapseHack} from '../transformers/nocollapse_hack';
|
||||
|
||||
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry, SelectorScopeRegistry} from './annotations';
|
||||
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry} from './annotations';
|
||||
import {BaseDefDecoratorHandler} from './annotations/src/base_def';
|
||||
import {CycleAnalyzer, ImportGraph} from './cycles';
|
||||
import {ErrorCode, ngErrorCode} from './diagnostics';
|
||||
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
|
||||
import {AbsoluteModuleStrategy, FileToModuleHost, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
|
||||
import {FileToModuleStrategy} from './imports/src/emitter';
|
||||
import {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
|
||||
import {PartialEvaluator} from './partial_evaluator';
|
||||
import {AbsoluteFsPath, LogicalFileSystem} from './path';
|
||||
import {TypeScriptReflectionHost} from './reflection';
|
||||
import {HostResourceLoader} from './resource_loader';
|
||||
import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing';
|
||||
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope';
|
||||
import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, generatedFactoryTransform} from './shims';
|
||||
import {ivySwitchTransform} from './switch';
|
||||
import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform';
|
||||
|
@ -339,7 +339,9 @@ export class NgtscProgram implements api.Program {
|
|||
}
|
||||
|
||||
const evaluator = new PartialEvaluator(this.reflector, checker);
|
||||
const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, this.refEmitter);
|
||||
const depScopeReader = new MetadataDtsModuleScopeResolver(checker, this.reflector);
|
||||
const scopeRegistry = new LocalModuleScopeRegistry(depScopeReader);
|
||||
|
||||
|
||||
// If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in
|
||||
// order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there
|
||||
|
@ -360,7 +362,8 @@ export class NgtscProgram implements api.Program {
|
|||
new ComponentDecoratorHandler(
|
||||
this.reflector, evaluator, scopeRegistry, this.isCore, this.resourceManager,
|
||||
this.rootDirs, this.options.preserveWhitespaces || false,
|
||||
this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer),
|
||||
this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer,
|
||||
this.refEmitter),
|
||||
new DirectiveDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore),
|
||||
new InjectableDecoratorHandler(
|
||||
this.reflector, this.isCore, this.options.strictInjectionParameters || false),
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "scope",
|
||||
srcs = glob([
|
||||
"index.ts",
|
||||
"src/**/*.ts",
|
||||
]),
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/imports",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/typecheck",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"@ngdeps//typescript",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export {ExportScope, ScopeData, ScopeDirective, ScopePipe} from './src/api';
|
||||
export {DtsModuleScopeResolver, MetadataDtsModuleScopeResolver} from './src/dependency';
|
||||
export {LocalModuleScope, LocalModuleScopeRegistry, LocalNgModuleData} from './src/local';
|
||||
export {extractDirectiveGuards} from './src/util';
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {TypeCheckableDirectiveMeta} from '../../typecheck';
|
||||
|
||||
/**
|
||||
* Data for one of a given NgModule's scopes (either compilation scope or export scopes).
|
||||
*/
|
||||
export interface ScopeData {
|
||||
/**
|
||||
* Directives in the exported scope of the module.
|
||||
*/
|
||||
directives: ScopeDirective[];
|
||||
|
||||
/**
|
||||
* Pipes in the exported scope of the module.
|
||||
*/
|
||||
pipes: ScopePipe[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An export scope of an NgModule, containing the directives/pipes it contributes to other NgModules
|
||||
* which import it.
|
||||
*/
|
||||
export interface ExportScope {
|
||||
/**
|
||||
* The scope exported by an NgModule, and available for import.
|
||||
*/
|
||||
exported: ScopeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for a given directive within an NgModule's scope.
|
||||
*/
|
||||
export interface ScopeDirective extends TypeCheckableDirectiveMeta {
|
||||
/**
|
||||
* Unparsed selector of the directive.
|
||||
*/
|
||||
selector: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for a given pipe within an NgModule's scope.
|
||||
*/
|
||||
export interface ScopePipe {
|
||||
ref: Reference<ts.Declaration>;
|
||||
name: string;
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {ReflectionHost} from '../../reflection';
|
||||
|
||||
import {ExportScope, ScopeDirective, ScopePipe} from './api';
|
||||
import {extractDirectiveGuards, extractReferencesFromType, readStringArrayType, readStringMapType, readStringType} from './util';
|
||||
|
||||
export interface DtsModuleScopeResolver {
|
||||
resolve(ref: Reference<ts.ClassDeclaration>): ExportScope|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads Angular metadata from classes declared in .d.ts files and computes an `ExportScope`.
|
||||
*
|
||||
* Given an NgModule declared in a .d.ts file, this resolver can produce a transitive `ExportScope`
|
||||
* of all of the directives/pipes it exports. It does this by reading metadata off of Ivy static
|
||||
* fields on directives, components, pipes, and NgModules.
|
||||
*/
|
||||
export class MetadataDtsModuleScopeResolver {
|
||||
/**
|
||||
* Cache which holds fully resolved scopes for NgModule classes from .d.ts files.
|
||||
*/
|
||||
private cache = new Map<ts.ClassDeclaration, ExportScope|null>();
|
||||
|
||||
constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {}
|
||||
|
||||
/**
|
||||
* Resolve a `Reference`'d NgModule from a .d.ts file and produce a transitive `ExportScope`
|
||||
* listing the directives and pipes which that NgModule exports to others.
|
||||
*
|
||||
* This operation relies on a `Reference` instead of a direct TypeScrpt node as the `Reference`s
|
||||
* produced depend on how the original NgModule was imported.
|
||||
*/
|
||||
resolve(ref: Reference<ts.ClassDeclaration>): ExportScope|null {
|
||||
const clazz = ref.node;
|
||||
if (!clazz.getSourceFile().isDeclarationFile) {
|
||||
throw new Error(
|
||||
`Debug error: DtsModuleScopeResolver.read(${ref.debugName} from ${clazz.getSourceFile().fileName}), but not a .d.ts file`);
|
||||
}
|
||||
|
||||
if (this.cache.has(clazz)) {
|
||||
return this.cache.get(clazz) !;
|
||||
}
|
||||
|
||||
// Build up the export scope - those directives and pipes made visible by this module.
|
||||
const directives: ScopeDirective[] = [];
|
||||
const pipes: ScopePipe[] = [];
|
||||
|
||||
const meta = this.readModuleMetadataFromClass(ref);
|
||||
if (meta === null) {
|
||||
this.cache.set(clazz, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only the 'exports' field of the NgModule's metadata is important. Imports and declarations
|
||||
// don't affect the export scope.
|
||||
for (const exportRef of meta.exports) {
|
||||
// Attempt to process the export as a directive.
|
||||
const directive = this.readScopeDirectiveFromClassWithDef(exportRef);
|
||||
if (directive !== null) {
|
||||
directives.push(directive);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt to process the export as a pipe.
|
||||
const pipe = this.readScopePipeFromClassWithDef(exportRef);
|
||||
if (pipe !== null) {
|
||||
pipes.push(pipe);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt to process the export as a module.
|
||||
const exportScope = this.resolve(exportRef);
|
||||
if (exportScope !== null) {
|
||||
// It is a module. Add exported directives and pipes to the current scope.
|
||||
directives.push(...exportScope.exported.directives);
|
||||
pipes.push(...exportScope.exported.pipes);
|
||||
continue;
|
||||
}
|
||||
|
||||
// The export was not a directive, a pipe, or a module. This is an error.
|
||||
// TODO(alxhub): produce a ts.Diagnostic
|
||||
throw new Error(`Exported value ${exportRef.debugName} was not a directive, pipe, or module`);
|
||||
}
|
||||
|
||||
return {
|
||||
exported: {directives, pipes},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts
|
||||
* file, or in a .ts file with a handwritten definition).
|
||||
*
|
||||
* @param ref `Reference` to the class of interest, with the context of how it was obtained.
|
||||
*/
|
||||
private readModuleMetadataFromClass(ref: Reference<ts.Declaration>): RawDependencyMetadata|null {
|
||||
const clazz = ref.node;
|
||||
const resolutionContext = clazz.getSourceFile().fileName;
|
||||
// This operation is explicitly not memoized, as it depends on `ref.ownedByModuleGuess`.
|
||||
// TODO(alxhub): investigate caching of .d.ts module metadata.
|
||||
const ngModuleDef = this.reflector.getMembersOfClass(clazz).find(
|
||||
member => member.name === 'ngModuleDef' && member.isStatic);
|
||||
if (ngModuleDef === undefined) {
|
||||
return null;
|
||||
} else if (
|
||||
// Validate that the shape of the ngModuleDef type is correct.
|
||||
ngModuleDef.type === null || !ts.isTypeReferenceNode(ngModuleDef.type) ||
|
||||
ngModuleDef.type.typeArguments === undefined ||
|
||||
ngModuleDef.type.typeArguments.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read the ModuleData out of the type arguments.
|
||||
const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments;
|
||||
return {
|
||||
declarations: extractReferencesFromType(
|
||||
this.checker, declarationMetadata, ref.ownedByModuleGuess, resolutionContext),
|
||||
exports: extractReferencesFromType(
|
||||
this.checker, exportMetadata, ref.ownedByModuleGuess, resolutionContext),
|
||||
imports: extractReferencesFromType(
|
||||
this.checker, importMetadata, ref.ownedByModuleGuess, resolutionContext),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read directive (or component) metadata from a referenced class in a .d.ts file.
|
||||
*/
|
||||
private readScopeDirectiveFromClassWithDef(ref: Reference<ts.ClassDeclaration>): ScopeDirective
|
||||
|null {
|
||||
const clazz = ref.node;
|
||||
const def = this.reflector.getMembersOfClass(clazz).find(
|
||||
field =>
|
||||
field.isStatic && (field.name === 'ngComponentDef' || field.name === 'ngDirectiveDef'));
|
||||
if (def === undefined) {
|
||||
// No definition could be found.
|
||||
return null;
|
||||
} else if (
|
||||
def.type === null || !ts.isTypeReferenceNode(def.type) ||
|
||||
def.type.typeArguments === undefined || def.type.typeArguments.length < 2) {
|
||||
// The type metadata was the wrong shape.
|
||||
return null;
|
||||
}
|
||||
const selector = readStringType(def.type.typeArguments[1]);
|
||||
if (selector === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
ref,
|
||||
name: clazz.name !.text,
|
||||
isComponent: def.name === 'ngComponentDef', selector,
|
||||
exportAs: readStringArrayType(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),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read pipe metadata from a referenced class in a .d.ts file.
|
||||
*/
|
||||
private readScopePipeFromClassWithDef(ref: Reference<ts.ClassDeclaration>): ScopePipe|null {
|
||||
const def = this.reflector.getMembersOfClass(ref.node).find(
|
||||
field => field.isStatic && field.name === 'ngPipeDef');
|
||||
if (def === undefined) {
|
||||
// No definition could be found.
|
||||
return null;
|
||||
} else if (
|
||||
def.type === null || !ts.isTypeReferenceNode(def.type) ||
|
||||
def.type.typeArguments === undefined || def.type.typeArguments.length < 2) {
|
||||
// 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.
|
||||
return null;
|
||||
}
|
||||
const name = type.literal.text;
|
||||
return {ref, name};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw metadata read from the .d.ts info of an ngModuleDef field on a compiled NgModule class.
|
||||
*/
|
||||
interface RawDependencyMetadata {
|
||||
declarations: Reference<ts.ClassDeclaration>[];
|
||||
imports: Reference<ts.ClassDeclaration>[];
|
||||
exports: Reference<ts.ClassDeclaration>[];
|
||||
}
|
|
@ -0,0 +1,282 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
|
||||
import {ExportScope, ScopeData, ScopeDirective, ScopePipe} from './api';
|
||||
import {DtsModuleScopeResolver} from './dependency';
|
||||
|
||||
export interface LocalNgModuleData {
|
||||
declarations: Reference<ts.Declaration>[];
|
||||
imports: Reference<ts.Declaration>[];
|
||||
exports: Reference<ts.Declaration>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A scope produced for an NgModule declared locally (in the current program being compiled).
|
||||
*
|
||||
* The `LocalModuleScope` contains the compilation scope, the transitive set of directives and pipes
|
||||
* visible to any component declared in this module. It also contains an `ExportScope`, the
|
||||
* transitive set of directives and pipes
|
||||
*/
|
||||
export interface LocalModuleScope extends ExportScope { compilation: ScopeData; }
|
||||
|
||||
/**
|
||||
* A registry which collects information about NgModules, Directives, Components, and Pipes which
|
||||
* are local (declared in the ts.Program being compiled), and can produce `LocalModuleScope`s
|
||||
* which summarize the compilation scope of a component.
|
||||
*
|
||||
* This class implements the logic of NgModule declarations, imports, and exports and can produce,
|
||||
* for a given component, the set of directives and pipes which are "visible" in that component's
|
||||
* template.
|
||||
*
|
||||
* The `LocalModuleScopeRegistry` has two "modes" of operation. During analysis, data for each
|
||||
* individual NgModule, Directive, Component, and Pipe is added to the registry. No attempt is made
|
||||
* to traverse or validate the NgModule graph (imports, exports, etc). After analysis, one of
|
||||
* `getScopeOfModule` or `getScopeForComponent` can be called, which traverses the NgModule graph
|
||||
* and applies the NgModule logic to generate a `LocalModuleScope`, the full scope for the given
|
||||
* module or component.
|
||||
*/
|
||||
export class LocalModuleScopeRegistry {
|
||||
/**
|
||||
* Tracks whether the registry has been asked to produce scopes for a module or component. Once
|
||||
* this is true, the registry cannot accept registrations of new directives/pipes/modules as it
|
||||
* would invalidate the cached scope data.
|
||||
*/
|
||||
private sealed = false;
|
||||
|
||||
/**
|
||||
* Metadata for each local NgModule registered.
|
||||
*/
|
||||
private ngModuleData = new Map<ts.Declaration, LocalNgModuleData>();
|
||||
|
||||
/**
|
||||
* Metadata for each local directive registered.
|
||||
*/
|
||||
private directiveData = new Map<ts.Declaration, ScopeDirective>();
|
||||
|
||||
/**
|
||||
* Metadata for each local pipe registered.
|
||||
*/
|
||||
private pipeData = new Map<ts.Declaration, ScopePipe>();
|
||||
|
||||
/**
|
||||
* A map of components from the current compilation unit to the NgModule which declared them.
|
||||
*
|
||||
* As components and directives are not distinguished at the NgModule level, this map may also
|
||||
* contain directives. This doesn't cause any problems but isn't useful as there is no concept of
|
||||
* a directive's compilation scope.
|
||||
*/
|
||||
private declarationToModule = new Map<ts.Declaration, ts.Declaration>();
|
||||
|
||||
/**
|
||||
* A cache of calculated `LocalModuleScope`s for each NgModule declared in the current program.
|
||||
*/
|
||||
private cache = new Map<ts.Declaration, LocalModuleScope>();
|
||||
|
||||
/**
|
||||
* Tracks whether a given component requires "remote scoping".
|
||||
*
|
||||
* Remote scoping is when the set of directives which apply to a given component is set in the
|
||||
* NgModule's file instead of directly on the ngComponentDef (which is sometimes needed to get
|
||||
* around cyclic import issues). This is not used in calculation of `LocalModuleScope`s, but is
|
||||
* tracked here for convenience.
|
||||
*/
|
||||
private remoteScoping = new Set<ts.Declaration>();
|
||||
|
||||
constructor(private dependencyScopeReader: DtsModuleScopeResolver) {}
|
||||
|
||||
/**
|
||||
* Add an NgModule's data to the registry.
|
||||
*/
|
||||
registerNgModule(clazz: ts.Declaration, data: LocalNgModuleData): void {
|
||||
this.assertCollecting();
|
||||
this.ngModuleData.set(clazz, data);
|
||||
for (const decl of data.declarations) {
|
||||
this.declarationToModule.set(decl.node, clazz);
|
||||
}
|
||||
}
|
||||
|
||||
registerDirective(directive: ScopeDirective): void {
|
||||
this.assertCollecting();
|
||||
this.directiveData.set(directive.ref.node, directive);
|
||||
}
|
||||
|
||||
registerPipe(pipe: ScopePipe): void {
|
||||
this.assertCollecting();
|
||||
this.pipeData.set(pipe.ref.node, pipe);
|
||||
}
|
||||
|
||||
getScopeForComponent(clazz: ts.ClassDeclaration): LocalModuleScope|null {
|
||||
if (!this.declarationToModule.has(clazz)) {
|
||||
return null;
|
||||
}
|
||||
return this.getScopeOfModule(this.declarationToModule.get(clazz) !);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects registered data for a module and its directives/pipes and convert it into a full
|
||||
* `LocalModuleScope`.
|
||||
*
|
||||
* This method implements the logic of NgModule imports and exports.
|
||||
*/
|
||||
getScopeOfModule(clazz: ts.Declaration): LocalModuleScope|null {
|
||||
// Seal the registry to protect the integrity of the `LocalModuleScope` cache.
|
||||
this.sealed = true;
|
||||
|
||||
// Look for cached data if available.
|
||||
if (this.cache.has(clazz)) {
|
||||
return this.cache.get(clazz) !;
|
||||
}
|
||||
|
||||
// `clazz` should be an NgModule previously added to the registry. If not, a scope for it
|
||||
// cannot be produced.
|
||||
if (!this.ngModuleData.has(clazz)) {
|
||||
return null;
|
||||
}
|
||||
const ngModule = this.ngModuleData.get(clazz) !;
|
||||
|
||||
// At this point, the goal is to produce two distinct transitive sets:
|
||||
// - the directives and pipes which are visible to components declared in the NgModule.
|
||||
// - the directives and pipes which are exported to any NgModules which import this one.
|
||||
|
||||
// Directives and pipes in the compilation scope.
|
||||
const compilationDirectives = new Map<ts.Declaration, ScopeDirective>();
|
||||
const compilationPipes = new Map<ts.Declaration, ScopePipe>();
|
||||
|
||||
// Directives and pipes exported to any importing NgModules.
|
||||
const exportDirectives = new Map<ts.Declaration, ScopeDirective>();
|
||||
const exportPipes = new Map<ts.Declaration, ScopePipe>();
|
||||
|
||||
// The algorithm is as follows:
|
||||
// 1) Add directives/pipes declared in the NgModule to the compilation scope.
|
||||
// 2) Add all of the directives/pipes from each NgModule imported into the current one to the
|
||||
// compilation scope. At this point, the compilation scope is complete.
|
||||
// 3) For each entry in the NgModule's exports:
|
||||
// a) Attempt to resolve it as an NgModule with its own exported directives/pipes. If it is
|
||||
// one, add them to the export scope of this NgModule.
|
||||
// b) Otherwise, it should be a class in the compilation scope of this NgModule. If it is,
|
||||
// add it to the export scope.
|
||||
// c) If it's neither an NgModule nor a directive/pipe in the compilation scope, then this
|
||||
// is an error.
|
||||
|
||||
// 1) add declarations.
|
||||
for (const decl of ngModule.declarations) {
|
||||
if (this.directiveData.has(decl.node)) {
|
||||
const directive = this.directiveData.get(decl.node) !;
|
||||
compilationDirectives.set(
|
||||
decl.node, {...directive, ref: decl as Reference<ts.ClassDeclaration>});
|
||||
} else if (this.pipeData.has(decl.node)) {
|
||||
const pipe = this.pipeData.get(decl.node) !;
|
||||
compilationPipes.set(decl.node, {...pipe, ref: decl});
|
||||
} else {
|
||||
// TODO(alxhub): produce a ts.Diagnostic. This can't be an error right now since some
|
||||
// ngtools tests rely on analysis of broken components.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) process imports.
|
||||
for (const decl of ngModule.imports) {
|
||||
const importScope = this.getExportedScope(decl);
|
||||
if (importScope === null) {
|
||||
// TODO(alxhub): produce a ts.Diagnostic
|
||||
throw new Error(`Unknown import: ${decl.debugName}`);
|
||||
}
|
||||
for (const directive of importScope.exported.directives) {
|
||||
compilationDirectives.set(directive.ref.node, directive);
|
||||
}
|
||||
for (const pipe of importScope.exported.pipes) {
|
||||
compilationPipes.set(pipe.ref.node, pipe);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) process exports.
|
||||
// Exports can contain modules, components, or directives. They're processed differently.
|
||||
// Modules are straightforward. Directives and pipes from exported modules are added to the
|
||||
// export maps. Directives/pipes are different - they might be exports of declared types or
|
||||
// imported types.
|
||||
for (const decl of ngModule.exports) {
|
||||
// Attempt to resolve decl as an NgModule.
|
||||
const importScope = this.getExportedScope(decl);
|
||||
if (importScope !== null) {
|
||||
// decl is an NgModule.
|
||||
for (const directive of importScope.exported.directives) {
|
||||
exportDirectives.set(directive.ref.node, directive);
|
||||
}
|
||||
for (const pipe of importScope.exported.pipes) {
|
||||
exportPipes.set(pipe.ref.node, pipe);
|
||||
}
|
||||
} else if (compilationDirectives.has(decl.node)) {
|
||||
// decl is a directive or component in the compilation scope of this NgModule.
|
||||
const directive = compilationDirectives.get(decl.node) !;
|
||||
exportDirectives.set(decl.node, directive);
|
||||
} else if (compilationPipes.has(decl.node)) {
|
||||
// decl is a pipe in the compilation scope of this NgModule.
|
||||
const pipe = compilationPipes.get(decl.node) !;
|
||||
exportPipes.set(decl.node, pipe);
|
||||
} else {
|
||||
// decl is an unknown export.
|
||||
// TODO(alxhub): produce a ts.Diagnostic
|
||||
throw new Error(`Unknown export: ${decl.debugName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, produce the `LocalModuleScope` with both the compilation and export scopes.
|
||||
const scope = {
|
||||
compilation: {
|
||||
directives: Array.from(compilationDirectives.values()),
|
||||
pipes: Array.from(compilationPipes.values()),
|
||||
},
|
||||
exported: {
|
||||
directives: Array.from(exportDirectives.values()),
|
||||
pipes: Array.from(exportPipes.values()),
|
||||
},
|
||||
};
|
||||
this.cache.set(clazz, scope);
|
||||
return scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a component requires remote scoping.
|
||||
*/
|
||||
getRequiresRemoteScope(node: ts.Declaration): boolean { return this.remoteScoping.has(node); }
|
||||
|
||||
/**
|
||||
* Set a component as requiring remote scoping.
|
||||
*/
|
||||
setComponentAsRequiringRemoteScoping(node: ts.Declaration): void { this.remoteScoping.add(node); }
|
||||
|
||||
/**
|
||||
* Look up the `ExportScope` of a given `Reference` to an NgModule.
|
||||
*
|
||||
* The NgModule in question may be declared locally in the current ts.Program, or it may be
|
||||
* declared in a .d.ts file.
|
||||
*/
|
||||
private getExportedScope(ref: Reference<ts.Declaration>): ExportScope|null {
|
||||
if (ref.node.getSourceFile().isDeclarationFile) {
|
||||
// The NgModule is declared in a .d.ts file. Resolve it with the `DependencyScopeReader`.
|
||||
if (!ts.isClassDeclaration(ref.node)) {
|
||||
// TODO(alxhub): produce a ts.Diagnostic
|
||||
throw new Error(`Reference to an NgModule ${ref.debugName} which isn't a class?`);
|
||||
}
|
||||
return this.dependencyScopeReader.resolve(ref as Reference<ts.ClassDeclaration>);
|
||||
} else {
|
||||
// The NgModule is declared locally in the current program. Resolve it from the registry.
|
||||
return this.getScopeOfModule(ref.node);
|
||||
}
|
||||
}
|
||||
|
||||
private assertCollecting(): void {
|
||||
if (this.sealed) {
|
||||
throw new Error(`Assertion: LocalModuleScopeRegistry is not COLLECTING`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {ClassMemberKind, ReflectionHost, reflectTypeEntityToDeclaration} from '../../reflection';
|
||||
import {nodeDebugInfo} from '../../util/src/typescript';
|
||||
|
||||
export function extractReferencesFromType(
|
||||
checker: ts.TypeChecker, def: ts.TypeNode, ngModuleImportedFrom: string | null,
|
||||
resolutionContext: string): Reference<ts.ClassDeclaration>[] {
|
||||
if (!ts.isTupleTypeNode(def)) {
|
||||
return [];
|
||||
}
|
||||
return def.elementTypes.map(element => {
|
||||
if (!ts.isTypeQueryNode(element)) {
|
||||
throw new Error(`Expected TypeQueryNode: ${nodeDebugInfo(element)}`);
|
||||
}
|
||||
const type = element.exprName;
|
||||
const {node, from} = reflectTypeEntityToDeclaration(type, checker);
|
||||
if (!ts.isClassDeclaration(node)) {
|
||||
throw new Error(`Expected ClassDeclaration: ${nodeDebugInfo(node)}`);
|
||||
}
|
||||
const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
|
||||
if (specifier !== null) {
|
||||
return new Reference(node, {specifier, resolutionContext});
|
||||
} else {
|
||||
return new Reference(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function readStringType(type: ts.TypeNode): string|null {
|
||||
if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) {
|
||||
return null;
|
||||
}
|
||||
return type.literal.text;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
|
||||
export function extractDirectiveGuards(node: ts.Declaration, reflector: ReflectionHost): {
|
||||
ngTemplateGuards: string[],
|
||||
hasNgTemplateContextGuard: boolean,
|
||||
} {
|
||||
const methods = nodeStaticMethodNames(node, reflector);
|
||||
const ngTemplateGuards = methods.filter(method => method.startsWith('ngTemplateGuard_'))
|
||||
.map(method => method.split('_', 2)[1]);
|
||||
const hasNgTemplateContextGuard = methods.some(name => name === 'ngTemplateContextGuard');
|
||||
return {hasNgTemplateContextGuard, ngTemplateGuards};
|
||||
}
|
||||
|
||||
function nodeStaticMethodNames(node: ts.Declaration, reflector: ReflectionHost): string[] {
|
||||
return reflector.getMembersOfClass(node)
|
||||
.filter(member => member.kind === ClassMemberKind.Method && member.isStatic)
|
||||
.map(member => member.name);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob([
|
||||
"**/*.ts",
|
||||
]),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler-cli/src/ngtsc/imports",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/scope",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"@ngdeps//typescript",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
|
||||
deps = [
|
||||
":test_lib",
|
||||
"//tools/testing:node_no_angular",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {TypeScriptReflectionHost} from '../../reflection';
|
||||
import {makeProgram} from '../../testing/in_memory_typescript';
|
||||
|
||||
import {ExportScope} from '../src/api';
|
||||
import {MetadataDtsModuleScopeResolver} from '../src/dependency';
|
||||
|
||||
const MODULE_FROM_NODE_MODULES_PATH = /.*node_modules\/(\w+)\/index\.d\.ts$/;
|
||||
|
||||
/**
|
||||
* Simple metadata types are added to the top of each testing file, for convenience.
|
||||
*/
|
||||
const PROLOG = `
|
||||
export declare type ModuleMeta<A, B, C, D> = never;
|
||||
export declare type ComponentMeta<A, B, C, D, E, F> = never;
|
||||
export declare type DirectiveMeta<A, B, C, D, E, F> = never;
|
||||
export declare type PipeMeta<A, B> = never;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Construct the testing environment with a given set of absolute modules and their contents.
|
||||
*
|
||||
* This returns both the `MetadataDtsModuleScopeResolver` and a `refs` object which can be
|
||||
* destructured to retrieve references to specific declared classes.
|
||||
*/
|
||||
function makeTestEnv(modules: {[module: string]: string}): {
|
||||
refs: {[name: string]: Reference<ts.ClassDeclaration>},
|
||||
resolver: MetadataDtsModuleScopeResolver,
|
||||
} {
|
||||
// Map the modules object to an array of files for `makeProgram`.
|
||||
const files = Object.keys(modules).map(moduleName => {
|
||||
return {
|
||||
name: `node_modules/${moduleName}/index.d.ts`,
|
||||
contents: PROLOG + (modules as any)[moduleName],
|
||||
};
|
||||
});
|
||||
const {program} = makeProgram(files);
|
||||
const checker = program.getTypeChecker();
|
||||
const resolver =
|
||||
new MetadataDtsModuleScopeResolver(checker, new TypeScriptReflectionHost(checker));
|
||||
|
||||
// Resolver for the refs object.
|
||||
const get = (target: {}, name: string): Reference<ts.ClassDeclaration> => {
|
||||
for (const sf of program.getSourceFiles()) {
|
||||
const symbol = checker.getSymbolAtLocation(sf) !;
|
||||
const exportedSymbol = symbol.exports !.get(name as ts.__String);
|
||||
if (exportedSymbol !== undefined) {
|
||||
const decl = exportedSymbol.valueDeclaration as ts.ClassDeclaration;
|
||||
const specifier = MODULE_FROM_NODE_MODULES_PATH.exec(sf.fileName) ![1];
|
||||
return new Reference(decl, {specifier, resolutionContext: sf.fileName});
|
||||
}
|
||||
}
|
||||
throw new Error('Class not found: ' + name);
|
||||
};
|
||||
|
||||
return {
|
||||
resolver,
|
||||
refs: new Proxy({}, {get}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('MetadataDtsModuleScopeResolver', () => {
|
||||
it('should produce an accurate scope for a basic NgModule', () => {
|
||||
const {resolver, refs} = makeTestEnv({
|
||||
'test': `
|
||||
export declare class Dir {
|
||||
static ngDirectiveDef: DirectiveMeta<Dir, '[dir]', ['exportAs'], {'input': 'input2'},
|
||||
{'output': 'output2'}, ['query']>;
|
||||
}
|
||||
|
||||
export declare class Module {
|
||||
static ngModuleDef: ModuleMeta<Module, [typeof Dir], never, [typeof Dir]>;
|
||||
}
|
||||
`
|
||||
});
|
||||
const {Dir, Module} = refs;
|
||||
const scope = resolver.resolve(Module) !;
|
||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
||||
});
|
||||
|
||||
it('should produce an accurate scope when a module is exported', () => {
|
||||
const {resolver, refs} = makeTestEnv({
|
||||
'test': `
|
||||
export declare class Dir {
|
||||
static ngDirectiveDef: DirectiveMeta<Dir, '[dir]', never, never, never, never>;
|
||||
}
|
||||
|
||||
export declare class ModuleA {
|
||||
static ngModuleDef: ModuleMeta<ModuleA, [typeof Dir], never, [typeof Dir]>;
|
||||
}
|
||||
|
||||
export declare class ModuleB {
|
||||
static ngModuleDef: ModuleMeta<ModuleB, never, never, [typeof ModuleA]>;
|
||||
}
|
||||
`
|
||||
});
|
||||
const {Dir, ModuleB} = refs;
|
||||
const scope = resolver.resolve(ModuleB) !;
|
||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
||||
});
|
||||
|
||||
it('should resolve correctly across modules', () => {
|
||||
const {resolver, refs} = makeTestEnv({
|
||||
'declaration': `
|
||||
export declare class Dir {
|
||||
static ngDirectiveDef: DirectiveMeta<Dir, '[dir]', never, never, never, never>;
|
||||
}
|
||||
|
||||
export declare class ModuleA {
|
||||
static ngModuleDef: ModuleMeta<ModuleA, [typeof Dir], never, [typeof Dir]>;
|
||||
}
|
||||
`,
|
||||
'exported': `
|
||||
import * as d from 'declaration';
|
||||
|
||||
export declare class ModuleB {
|
||||
static ngModuleDef: ModuleMeta<ModuleB, never, never, [typeof d.ModuleA]>;
|
||||
}
|
||||
`
|
||||
});
|
||||
const {Dir, ModuleB} = refs;
|
||||
const scope = resolver.resolve(ModuleB) !;
|
||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
||||
|
||||
// Explicitly verify that the directive has the correct owning module.
|
||||
expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration');
|
||||
});
|
||||
});
|
||||
|
||||
function scopeToRefs(scope: ExportScope): Reference<ts.Declaration>[] {
|
||||
const directives = scope.exported.directives.map(dir => dir.ref);
|
||||
const pipes = scope.exported.pipes.map(pipe => pipe.ref);
|
||||
return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !));
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {ScopeData, ScopeDirective, ScopePipe} from '../src/api';
|
||||
import {DtsModuleScopeResolver} from '../src/dependency';
|
||||
import {LocalModuleScopeRegistry} from '../src/local';
|
||||
|
||||
function registerFakeRefs(registry: LocalModuleScopeRegistry):
|
||||
{[name: string]: Reference<ts.ClassDeclaration>} {
|
||||
const get = (target: {}, name: string): Reference<ts.ClassDeclaration> => {
|
||||
const sf = ts.createSourceFile(
|
||||
name + '.ts', `export class ${name} {}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
||||
const clazz = sf.statements[0] as ts.ClassDeclaration;
|
||||
const ref = new Reference(clazz);
|
||||
if (name.startsWith('Dir') || name.startsWith('Cmp')) {
|
||||
registry.registerDirective(fakeDirective(ref));
|
||||
} else if (name.startsWith('Pipe')) {
|
||||
registry.registerPipe(fakePipe(ref));
|
||||
}
|
||||
return ref;
|
||||
};
|
||||
return new Proxy({}, {get});
|
||||
}
|
||||
|
||||
describe('LocalModuleScopeRegistry', () => {
|
||||
let registry !: LocalModuleScopeRegistry;
|
||||
|
||||
beforeEach(() => { registry = new LocalModuleScopeRegistry(new MockDtsModuleScopeResolver()); });
|
||||
|
||||
it('should produce an accurate LocalModuleScope for a basic NgModule', () => {
|
||||
const {Dir1, Dir2, Pipe1, Module} = registerFakeRefs(registry);
|
||||
|
||||
registry.registerNgModule(Module.node, {
|
||||
imports: [],
|
||||
declarations: [Dir1, Dir2, Pipe1],
|
||||
exports: [Dir1, Pipe1],
|
||||
});
|
||||
|
||||
const scope = registry.getScopeOfModule(Module.node) !;
|
||||
expect(scopeToRefs(scope.compilation)).toEqual([Dir1, Dir2, Pipe1]);
|
||||
expect(scopeToRefs(scope.exported)).toEqual([Dir1, Pipe1]);
|
||||
});
|
||||
|
||||
it('should produce accurate LocalModuleScopes for a complex module chain', () => {
|
||||
const {DirA, DirB, DirCI, DirCE, ModuleA, ModuleB, ModuleC} = registerFakeRefs(registry);
|
||||
|
||||
registry.registerNgModule(
|
||||
ModuleA.node, {imports: [ModuleB], declarations: [DirA], exports: []});
|
||||
registry.registerNgModule(
|
||||
ModuleB.node, {exports: [ModuleC, DirB], declarations: [DirB], imports: []});
|
||||
registry.registerNgModule(
|
||||
ModuleC.node, {declarations: [DirCI, DirCE], exports: [DirCE], imports: []});
|
||||
|
||||
const scopeA = registry.getScopeOfModule(ModuleA.node) !;
|
||||
expect(scopeToRefs(scopeA.compilation)).toEqual([DirA, DirB, DirCE]);
|
||||
expect(scopeToRefs(scopeA.exported)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not treat exported modules as imported', () => {
|
||||
const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry);
|
||||
|
||||
registry.registerNgModule(ModuleA.node, {exports: [ModuleB], imports: [], declarations: []});
|
||||
registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []});
|
||||
|
||||
const scopeA = registry.getScopeOfModule(ModuleA.node) !;
|
||||
expect(scopeToRefs(scopeA.compilation)).toEqual([]);
|
||||
expect(scopeToRefs(scopeA.exported)).toEqual([Dir]);
|
||||
});
|
||||
|
||||
it('should deduplicate declarations and exports', () => {
|
||||
const {DirA, ModuleA, DirB, ModuleB, ModuleC} = registerFakeRefs(registry);
|
||||
|
||||
registry.registerNgModule(ModuleA.node, {
|
||||
declarations: [DirA, DirA],
|
||||
imports: [ModuleB, ModuleC],
|
||||
exports: [DirA, DirA, DirB, ModuleB],
|
||||
});
|
||||
registry.registerNgModule(ModuleB.node, {declarations: [DirB], imports: [], exports: [DirB]});
|
||||
registry.registerNgModule(ModuleC.node, {declarations: [], imports: [], exports: [ModuleB]});
|
||||
|
||||
const scope = registry.getScopeOfModule(ModuleA.node) !;
|
||||
expect(scopeToRefs(scope.compilation)).toEqual([DirA, DirB]);
|
||||
expect(scopeToRefs(scope.exported)).toEqual([DirA, DirB]);
|
||||
});
|
||||
|
||||
it('should preserve reference identities in module metadata', () => {
|
||||
const {Dir, Module} = registerFakeRefs(registry);
|
||||
const idSf = ts.createSourceFile('id.ts', 'var id;', ts.ScriptTarget.Latest, true);
|
||||
|
||||
// Create a new Reference to Dir, with a special `ts.Identifier`, and register the directive
|
||||
// using it. This emulates what happens when an NgModule declares a Directive.
|
||||
const idVar = idSf.statements[0] as ts.VariableStatement;
|
||||
const id = idVar.declarationList.declarations[0].name as ts.Identifier;
|
||||
const DirInModule = new Reference(Dir.node);
|
||||
DirInModule.addIdentifier(id);
|
||||
registry.registerNgModule(Module.node, {exports: [], imports: [], declarations: [DirInModule]});
|
||||
|
||||
const scope = registry.getScopeOfModule(Module.node) !;
|
||||
expect(scope.compilation.directives[0].ref.getIdentityIn(idSf)).toBe(id);
|
||||
});
|
||||
|
||||
it('should allow directly exporting a directive that\'s not imported', () => {
|
||||
const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry);
|
||||
|
||||
registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [ModuleB], declarations: []});
|
||||
registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []});
|
||||
|
||||
const scopeA = registry.getScopeOfModule(ModuleA.node) !;
|
||||
expect(scopeToRefs(scopeA.exported)).toEqual([Dir]);
|
||||
});
|
||||
|
||||
it('should not allow directly exporting a directive that\'s not imported', () => {
|
||||
const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry);
|
||||
|
||||
registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [], declarations: []});
|
||||
registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []});
|
||||
|
||||
expect(() => registry.getScopeOfModule(ModuleA.node)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
function fakeDirective(ref: Reference<ts.ClassDeclaration>): ScopeDirective {
|
||||
const name = ref.debugName !;
|
||||
return {
|
||||
ref,
|
||||
name,
|
||||
selector: `[${ref.debugName}]`,
|
||||
isComponent: name.startsWith('Cmp'),
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
exportAs: null,
|
||||
queries: [],
|
||||
hasNgTemplateContextGuard: false,
|
||||
ngTemplateGuards: [],
|
||||
};
|
||||
}
|
||||
|
||||
function fakePipe(ref: Reference<ts.ClassDeclaration>): ScopePipe {
|
||||
const name = ref.debugName !;
|
||||
return {ref, name};
|
||||
}
|
||||
|
||||
class MockDtsModuleScopeResolver implements DtsModuleScopeResolver {
|
||||
resolve(ref: Reference<ts.ClassDeclaration>): null { return null; }
|
||||
}
|
||||
|
||||
function scopeToRefs(scopeData: ScopeData): Reference<ts.Declaration>[] {
|
||||
const directives = scopeData.directives.map(dir => dir.ref);
|
||||
const pipes = scopeData.pipes.map(pipe => pipe.ref);
|
||||
return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !));
|
||||
}
|
|
@ -80,3 +80,9 @@ export function getRootDirs(host: ts.CompilerHost, options: ts.CompilerOptions):
|
|||
}
|
||||
return rootDirs.map(rootDir => AbsoluteFsPath.fromUnchecked(rootDir));
|
||||
}
|
||||
|
||||
export function nodeDebugInfo(node: ts.Node): string {
|
||||
const sf = getSourceFile(node);
|
||||
const {line, character} = ts.getLineAndCharacterOfPosition(sf, node.pos);
|
||||
return `[${sf.fileName}: ${ts.SyntaxKind[node.kind]} @ ${line}:${character}]`;
|
||||
}
|
||||
|
|
|
@ -1173,9 +1173,7 @@ describe('compiler compliance: styling', () => {
|
|||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: '
|
||||
<div myWidthDir myHeightDir myClassDir></div>
|
||||
',
|
||||
template: '<div myWidthDir myHeightDir myClassDir></div>',
|
||||
})
|
||||
export class MyComponent {
|
||||
}
|
||||
|
|
|
@ -1913,8 +1913,8 @@ describe('ngtsc behavioral tests', () => {
|
|||
env.driveMain();
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents)
|
||||
.toContain(
|
||||
'i0.ɵsetComponentScope(NormalComponent, [i1.NormalComponent, CyclicComponent], [])');
|
||||
.toMatch(
|
||||
/i\d\.ɵsetComponentScope\(NormalComponent,\s+\[NormalComponent,\s+CyclicComponent\],\s+\[\]\)/);
|
||||
expect(jsContents).not.toContain('/*__PURE__*/ i0.ɵsetComponentScope');
|
||||
});
|
||||
});
|
||||
|
@ -2565,12 +2565,13 @@ describe('ngtsc behavioral tests', () => {
|
|||
beforeEach(() => {
|
||||
env.tsconfig();
|
||||
env.write('node_modules/@angular/router/index.d.ts', `
|
||||
import {ModuleWithProviders} from '@angular/core';
|
||||
import {ModuleWithProviders, ɵNgModuleDefWithMeta as NgModuleDefWithMeta} from '@angular/core';
|
||||
|
||||
export declare var ROUTES;
|
||||
export declare class RouterModule {
|
||||
static forRoot(arg1: any, arg2: any): ModuleWithProviders<RouterModule>;
|
||||
static forChild(arg1: any): ModuleWithProviders<RouterModule>;
|
||||
static ngModuleDef: NgModuleDefWithMeta<RouterModule, never, never, never>
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue