feat(ivy): integrate indexing pipeline with NgtscProgram (#31151)
Add an IndexingContext class to store indexing information and a transformer module to generate indexing analysis. Integrate the indexing module with the rest of NgtscProgram and add integration tests. Closes #30959 PR Close #31151
This commit is contained in:
parent
3fb73ac62b
commit
74f4f5dfab
|
@ -12,6 +12,7 @@ ts_library(
|
||||||
"//packages/compiler-cli/src/ngtsc/cycles",
|
"//packages/compiler-cli/src/ngtsc/cycles",
|
||||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||||
"//packages/compiler-cli/src/ngtsc/imports",
|
"//packages/compiler-cli/src/ngtsc/imports",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/indexer",
|
||||||
"//packages/compiler-cli/src/ngtsc/metadata",
|
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||||
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
|
||||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, R3ComponentMetadata, R3TargetBinder, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
|
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, ParseSourceFile, ParseTemplateOptions, R3ComponentMetadata, R3TargetBinder, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {CycleAnalyzer} from '../../cycles';
|
import {CycleAnalyzer} from '../../cycles';
|
||||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||||
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
||||||
|
import {IndexingContext} from '../../indexer';
|
||||||
import {DirectiveMeta, MetadataReader, MetadataRegistry, extractDirectiveGuards} from '../../metadata';
|
import {DirectiveMeta, MetadataReader, MetadataRegistry, extractDirectiveGuards} from '../../metadata';
|
||||||
import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance';
|
import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance';
|
||||||
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||||
|
@ -35,6 +36,7 @@ export interface ComponentHandlerData {
|
||||||
meta: R3ComponentMetadata;
|
meta: R3ComponentMetadata;
|
||||||
parsedTemplate: TmplAstNode[];
|
parsedTemplate: TmplAstNode[];
|
||||||
metadataStmt: Statement|null;
|
metadataStmt: Statement|null;
|
||||||
|
parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -169,11 +171,22 @@ export class ComponentDecoratorHandler implements
|
||||||
// Parse the template.
|
// Parse the template.
|
||||||
// If a preanalyze phase was executed, the template may already exist in parsed form, so check
|
// If a preanalyze phase was executed, the template may already exist in parsed form, so check
|
||||||
// the preanalyzeTemplateCache.
|
// the preanalyzeTemplateCache.
|
||||||
let template: ParsedTemplate;
|
// Extract a closure of the template parsing code so that it can be reparsed with different
|
||||||
|
// options if needed, like in the indexing pipeline.
|
||||||
|
let parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate;
|
||||||
if (this.preanalyzeTemplateCache.has(node)) {
|
if (this.preanalyzeTemplateCache.has(node)) {
|
||||||
// The template was parsed in preanalyze. Use it and delete it to save memory.
|
// The template was parsed in preanalyze. Use it and delete it to save memory.
|
||||||
template = this.preanalyzeTemplateCache.get(node) !;
|
const template = this.preanalyzeTemplateCache.get(node) !;
|
||||||
this.preanalyzeTemplateCache.delete(node);
|
this.preanalyzeTemplateCache.delete(node);
|
||||||
|
|
||||||
|
// A pre-analyzed template cannot be reparsed. Pre-analysis is never run with the indexing
|
||||||
|
// pipeline.
|
||||||
|
parseTemplate = (options?: ParseTemplateOptions) => {
|
||||||
|
if (options !== undefined) {
|
||||||
|
throw new Error(`Cannot reparse a pre-analyzed template with new options`);
|
||||||
|
}
|
||||||
|
return template;
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
// The template was not already parsed. Either there's a templateUrl, or an inline template.
|
// The template was not already parsed. Either there's a templateUrl, or an inline template.
|
||||||
if (component.has('templateUrl')) {
|
if (component.has('templateUrl')) {
|
||||||
|
@ -187,9 +200,9 @@ export class ComponentDecoratorHandler implements
|
||||||
const templateStr = this.resourceLoader.load(templateUrl);
|
const templateStr = this.resourceLoader.load(templateUrl);
|
||||||
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), templateUrl);
|
this.resourceDependencies.recordResourceDependency(node.getSourceFile(), templateUrl);
|
||||||
|
|
||||||
template = this._parseTemplate(
|
parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
|
||||||
component, templateStr, sourceMapUrl(templateUrl), /* templateRange */ undefined,
|
component, templateStr, sourceMapUrl(templateUrl), /* templateRange */ undefined,
|
||||||
/* escapedString */ false);
|
/* escapedString */ false, options);
|
||||||
} else {
|
} else {
|
||||||
// Expect an inline template to be present.
|
// Expect an inline template to be present.
|
||||||
const inlineTemplate = this._extractInlineTemplate(component, relativeContextFilePath);
|
const inlineTemplate = this._extractInlineTemplate(component, relativeContextFilePath);
|
||||||
|
@ -199,10 +212,11 @@ export class ComponentDecoratorHandler implements
|
||||||
'component is missing a template');
|
'component is missing a template');
|
||||||
}
|
}
|
||||||
const {templateStr, templateUrl, templateRange, escapedString} = inlineTemplate;
|
const {templateStr, templateUrl, templateRange, escapedString} = inlineTemplate;
|
||||||
template =
|
parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate(
|
||||||
this._parseTemplate(component, templateStr, templateUrl, templateRange, escapedString);
|
component, templateStr, templateUrl, templateRange, escapedString, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const template = parseTemplate();
|
||||||
|
|
||||||
if (template.errors !== undefined) {
|
if (template.errors !== undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -294,7 +308,7 @@ export class ComponentDecoratorHandler implements
|
||||||
},
|
},
|
||||||
metadataStmt: generateSetClassMetadataCall(
|
metadataStmt: generateSetClassMetadataCall(
|
||||||
node, this.reflector, this.defaultImportRecorder, this.isCore),
|
node, this.reflector, this.defaultImportRecorder, this.isCore),
|
||||||
parsedTemplate: template.nodes,
|
parsedTemplate: template.nodes, parseTemplate,
|
||||||
},
|
},
|
||||||
typeCheck: true,
|
typeCheck: true,
|
||||||
};
|
};
|
||||||
|
@ -304,6 +318,37 @@ export class ComponentDecoratorHandler implements
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
index(context: IndexingContext, node: ClassDeclaration, analysis: ComponentHandlerData) {
|
||||||
|
// The component template may have been previously parsed without preserving whitespace or with
|
||||||
|
// `leadingTriviaChar`s, both of which may manipulate the AST into a form not representative of
|
||||||
|
// the source code, making it unsuitable for indexing. The template is reparsed with preserving
|
||||||
|
// options to remedy this.
|
||||||
|
const template = analysis.parseTemplate({
|
||||||
|
preserveWhitespaces: true,
|
||||||
|
leadingTriviaChars: [],
|
||||||
|
});
|
||||||
|
const scope = this.scopeRegistry.getScopeForComponent(node);
|
||||||
|
const selector = analysis.meta.selector;
|
||||||
|
const matcher = new SelectorMatcher<DirectiveMeta>();
|
||||||
|
if (scope !== null) {
|
||||||
|
for (const directive of scope.compilation.directives) {
|
||||||
|
matcher.addSelectables(CssSelector.parse(directive.selector), directive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const binder = new R3TargetBinder(matcher);
|
||||||
|
const boundTemplate = binder.bind({template: template.nodes});
|
||||||
|
|
||||||
|
context.addComponent({
|
||||||
|
declaration: node,
|
||||||
|
selector,
|
||||||
|
boundTemplate,
|
||||||
|
templateMeta: {
|
||||||
|
isInline: template.isInline,
|
||||||
|
file: template.file,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
typeCheck(ctx: TypeCheckContext, node: ClassDeclaration, meta: ComponentHandlerData): void {
|
typeCheck(ctx: TypeCheckContext, node: ClassDeclaration, meta: ComponentHandlerData): void {
|
||||||
if (!ts.isClassDeclaration(node)) {
|
if (!ts.isClassDeclaration(node)) {
|
||||||
return;
|
return;
|
||||||
|
@ -576,7 +621,8 @@ export class ComponentDecoratorHandler implements
|
||||||
|
|
||||||
private _parseTemplate(
|
private _parseTemplate(
|
||||||
component: Map<string, ts.Expression>, templateStr: string, templateUrl: string,
|
component: Map<string, ts.Expression>, templateStr: string, templateUrl: string,
|
||||||
templateRange: LexerRange|undefined, escapedString: boolean): ParsedTemplate {
|
templateRange: LexerRange|undefined, escapedString: boolean,
|
||||||
|
options: ParseTemplateOptions = {}): ParsedTemplate {
|
||||||
let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces;
|
let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces;
|
||||||
if (component.has('preserveWhitespaces')) {
|
if (component.has('preserveWhitespaces')) {
|
||||||
const expr = component.get('preserveWhitespaces') !;
|
const expr = component.get('preserveWhitespaces') !;
|
||||||
|
@ -602,11 +648,14 @@ export class ComponentDecoratorHandler implements
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
interpolation, ...parseTemplate(templateStr, templateUrl, {
|
interpolation,
|
||||||
preserveWhitespaces,
|
...parseTemplate(templateStr, templateUrl, {
|
||||||
interpolationConfig: interpolation,
|
preserveWhitespaces,
|
||||||
range: templateRange, escapedString
|
interpolationConfig: interpolation,
|
||||||
}),
|
range: templateRange, escapedString, ...options,
|
||||||
|
}),
|
||||||
|
isInline: component.has('template'),
|
||||||
|
file: new ParseSourceFile(templateStr, templateUrl),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -668,4 +717,6 @@ interface ParsedTemplate {
|
||||||
nodes: TmplAstNode[];
|
nodes: TmplAstNode[];
|
||||||
styleUrls: string[];
|
styleUrls: string[];
|
||||||
styles: string[];
|
styles: string[];
|
||||||
|
isInline: boolean;
|
||||||
|
file: ParseSourceFile;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,9 @@ ts_library(
|
||||||
]),
|
]),
|
||||||
deps = [
|
deps = [
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/imports",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,8 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {InterpolationConfig, ParseSourceFile} from '@angular/compiler';
|
import {ParseSourceFile} from '@angular/compiler';
|
||||||
import {ParseTemplateOptions} from '@angular/compiler/src/render3/view/template';
|
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +32,6 @@ export interface TemplateIdentifier {
|
||||||
name: string;
|
name: string;
|
||||||
span: AbsoluteSourceSpan;
|
span: AbsoluteSourceSpan;
|
||||||
kind: IdentifierKind;
|
kind: IdentifierKind;
|
||||||
file: ParseSourceFile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,21 +40,11 @@ export interface TemplateIdentifier {
|
||||||
export interface IndexedComponent {
|
export interface IndexedComponent {
|
||||||
name: string;
|
name: string;
|
||||||
selector: string|null;
|
selector: string|null;
|
||||||
sourceFile: string;
|
file: ParseSourceFile;
|
||||||
content: string;
|
|
||||||
template: {
|
template: {
|
||||||
identifiers: Set<TemplateIdentifier>,
|
identifiers: Set<TemplateIdentifier>,
|
||||||
usedComponents: Set<ts.ClassDeclaration>,
|
usedComponents: Set<ts.Declaration>,
|
||||||
|
isInline: boolean,
|
||||||
|
file: ParseSourceFile;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for restoring a parsed template. See `template.ts#restoreTemplate`.
|
|
||||||
*/
|
|
||||||
export interface RestoreTemplateOptions extends ParseTemplateOptions {
|
|
||||||
/**
|
|
||||||
* The interpolation configuration of the template is lost after it already
|
|
||||||
* parsed, so it must be respecified.
|
|
||||||
*/
|
|
||||||
interpolationConfig: InterpolationConfig;
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,8 +6,55 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {BoundTarget, DirectiveMeta, ParseSourceFile} from '@angular/compiler';
|
||||||
|
import {Reference} from '../../imports';
|
||||||
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
|
||||||
|
export interface ComponentMeta extends DirectiveMeta {
|
||||||
|
ref: Reference<ClassDeclaration>;
|
||||||
|
/**
|
||||||
|
* Unparsed selector of the directive.
|
||||||
|
*/
|
||||||
|
selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores analysis information about components in a compilation for and provides methods for
|
* An intermediate representation of a component.
|
||||||
* querying information about components to be used in indexing.
|
|
||||||
*/
|
*/
|
||||||
export class IndexingContext {}
|
export interface ComponentInfo {
|
||||||
|
/** Component TypeScript class declaration */
|
||||||
|
declaration: ClassDeclaration;
|
||||||
|
|
||||||
|
/** Component template selector if it exists, otherwise null. */
|
||||||
|
selector: string|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BoundTarget containing the parsed template. Can also be used to query for directives used in
|
||||||
|
* the template.
|
||||||
|
*/
|
||||||
|
boundTemplate: BoundTarget<ComponentMeta>;
|
||||||
|
|
||||||
|
/** Metadata about the template */
|
||||||
|
templateMeta: {
|
||||||
|
/** Whether the component template is inline */
|
||||||
|
isInline: boolean;
|
||||||
|
|
||||||
|
/** Template file recorded by template parser */
|
||||||
|
file: ParseSourceFile;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context for storing indexing infromation about components of a program.
|
||||||
|
*
|
||||||
|
* An `IndexingContext` collects component and template analysis information from
|
||||||
|
* `DecoratorHandler`s and exposes them to be indexed.
|
||||||
|
*/
|
||||||
|
export class IndexingContext {
|
||||||
|
readonly components = new Set<ComponentInfo>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a component to the context.
|
||||||
|
*/
|
||||||
|
addComponent(info: ComponentInfo) { this.components.add(info); }
|
||||||
|
}
|
||||||
|
|
|
@ -6,12 +6,9 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, HtmlParser, Lexer, MethodCall, ParseSourceFile, PropertyRead, RecursiveAstVisitor, TmplAstNode, TokenType, visitAll} from '@angular/compiler';
|
import {AST, BoundTarget, DirectiveMeta, ImplicitReceiver, MethodCall, PropertyRead, RecursiveAstVisitor} from '@angular/compiler';
|
||||||
import {BoundText, Element, Node, RecursiveVisitor as RecursiveTemplateVisitor, Template} from '@angular/compiler/src/render3/r3_ast';
|
import {BoundText, Element, Node, RecursiveVisitor as RecursiveTemplateVisitor, Template} from '@angular/compiler/src/render3/r3_ast';
|
||||||
import {htmlAstToRender3Ast} from '@angular/compiler/src/render3/r3_template_transform';
|
import {AbsoluteSourceSpan, IdentifierKind, TemplateIdentifier} from './api';
|
||||||
import {I18nMetaVisitor} from '@angular/compiler/src/render3/view/i18n/meta';
|
|
||||||
import {makeBindingParser} from '@angular/compiler/src/render3/view/template';
|
|
||||||
import {AbsoluteSourceSpan, IdentifierKind, RestoreTemplateOptions, TemplateIdentifier} from './api';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A parsed node in a template, which may have a name (if it is a selector) or
|
* A parsed node in a template, which may have a name (if it is a selector) or
|
||||||
|
@ -22,46 +19,6 @@ interface HTMLNode extends Node {
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the location of an identifier to its real anchor in a source code.
|
|
||||||
*
|
|
||||||
* The compiler's expression parser records the location of some expressions in a manner not
|
|
||||||
* useful to the indexer. For example, a `MethodCall` `foo(a, b)` will record the span of the
|
|
||||||
* entire method call, but the indexer is interested only in the method identifier.
|
|
||||||
*
|
|
||||||
* To remedy all this, the visitor tokenizes the template node the expression was discovered in,
|
|
||||||
* and updates the locations of entities found during expression traversal with those of the
|
|
||||||
* tokens.
|
|
||||||
*
|
|
||||||
* TODO(ayazhafiz): Think about how to handle `PropertyRead`s in `BoundAttribute`s. The Lexer
|
|
||||||
* tokenizes the attribute as a string and ignores quotes.
|
|
||||||
*
|
|
||||||
* @param entities entities to update
|
|
||||||
* @param currentNode node expression was in
|
|
||||||
*/
|
|
||||||
function updateIdentifierSpans(identifiers: TemplateIdentifier[], currentNode: Node) {
|
|
||||||
const localSpan = currentNode.sourceSpan;
|
|
||||||
const localExpression = localSpan.toString();
|
|
||||||
|
|
||||||
const lexedIdentifiers =
|
|
||||||
new Lexer().tokenize(localExpression).filter(token => token.type === TokenType.Identifier);
|
|
||||||
|
|
||||||
// Join the relative position of the expression within a node with the absolute position of the
|
|
||||||
// node to get the absolute position of the expression in the source code.
|
|
||||||
const absoluteOffset = currentNode.sourceSpan.start.offset;
|
|
||||||
identifiers.forEach((id, index) => {
|
|
||||||
const lexedId = lexedIdentifiers[index];
|
|
||||||
if (id.name !== lexedId.strValue) {
|
|
||||||
throw new Error(
|
|
||||||
'Impossible state: lexed and parsed expression should contain the same tokens.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = absoluteOffset + lexedId.index;
|
|
||||||
const absoluteSpan = new AbsoluteSourceSpan(start, start + lexedId.strValue.length);
|
|
||||||
id.span = absoluteSpan;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visits the AST of an Angular template syntax expression, finding interesting
|
* Visits the AST of an Angular template syntax expression, finding interesting
|
||||||
* entities (variable references, etc.). Creates an array of Entities found in
|
* entities (variable references, etc.). Creates an array of Entities found in
|
||||||
|
@ -72,41 +29,74 @@ function updateIdentifierSpans(identifiers: TemplateIdentifier[], currentNode: N
|
||||||
* 11}}]`.
|
* 11}}]`.
|
||||||
*/
|
*/
|
||||||
class ExpressionVisitor extends RecursiveAstVisitor {
|
class ExpressionVisitor extends RecursiveAstVisitor {
|
||||||
private readonly file: ParseSourceFile;
|
readonly identifiers: TemplateIdentifier[] = [];
|
||||||
|
|
||||||
private constructor(context: Node, readonly identifiers: TemplateIdentifier[] = []) {
|
private constructor(
|
||||||
|
context: Node, private readonly boundTemplate: BoundTarget<DirectiveMeta>,
|
||||||
|
private readonly expressionStr = context.sourceSpan.toString(),
|
||||||
|
private readonly absoluteOffset = context.sourceSpan.start.offset) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.file = context.sourceSpan.start.file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getIdentifiers(ast: AST, context: Node): TemplateIdentifier[] {
|
/**
|
||||||
const visitor = new ExpressionVisitor(context);
|
* Returns identifiers discovered in an expression.
|
||||||
|
*
|
||||||
|
* @param ast expression AST to visit
|
||||||
|
* @param context HTML node expression is defined in
|
||||||
|
* @param boundTemplate bound target of the entire template, which can be used to query for the
|
||||||
|
* entities expressions target.
|
||||||
|
*/
|
||||||
|
static getIdentifiers(ast: AST, context: Node, boundTemplate: BoundTarget<DirectiveMeta>):
|
||||||
|
TemplateIdentifier[] {
|
||||||
|
const visitor = new ExpressionVisitor(context, boundTemplate);
|
||||||
visitor.visit(ast);
|
visitor.visit(ast);
|
||||||
const identifiers = visitor.identifiers;
|
return visitor.identifiers;
|
||||||
|
|
||||||
updateIdentifierSpans(identifiers, context);
|
|
||||||
|
|
||||||
return identifiers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visit(ast: AST) { ast.visit(this); }
|
visit(ast: AST) { ast.visit(this); }
|
||||||
|
|
||||||
visitMethodCall(ast: MethodCall, context: {}) {
|
visitMethodCall(ast: MethodCall, context: {}) {
|
||||||
this.addIdentifier(ast, IdentifierKind.Method);
|
this.visitIdentifier(ast, IdentifierKind.Method);
|
||||||
super.visitMethodCall(ast, context);
|
super.visitMethodCall(ast, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
visitPropertyRead(ast: PropertyRead, context: {}) {
|
visitPropertyRead(ast: PropertyRead, context: {}) {
|
||||||
this.addIdentifier(ast, IdentifierKind.Property);
|
this.visitIdentifier(ast, IdentifierKind.Property);
|
||||||
super.visitPropertyRead(ast, context);
|
super.visitPropertyRead(ast, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addIdentifier(ast: AST&{name: string}, kind: IdentifierKind) {
|
/**
|
||||||
|
* Visits an identifier, adding it to the identifier store if it is useful for indexing.
|
||||||
|
*
|
||||||
|
* @param ast expression AST the identifier is in
|
||||||
|
* @param kind identifier kind
|
||||||
|
*/
|
||||||
|
private visitIdentifier(ast: AST&{name: string, receiver: AST}, kind: IdentifierKind) {
|
||||||
|
// The definition of a non-top-level property such as `bar` in `{{foo.bar}}` is currently
|
||||||
|
// impossible to determine by an indexer and unsupported by the indexing module.
|
||||||
|
// The indexing module also does not currently support references to identifiers declared in the
|
||||||
|
// template itself, which have a non-null expression target.
|
||||||
|
if (!(ast.receiver instanceof ImplicitReceiver) ||
|
||||||
|
this.boundTemplate.getExpressionTarget(ast) !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the location of the identifier of real interest.
|
||||||
|
// The compiler's expression parser records the location of some expressions in a manner not
|
||||||
|
// useful to the indexer. For example, a `MethodCall` `foo(a, b)` will record the span of the
|
||||||
|
// entire method call, but the indexer is interested only in the method identifier.
|
||||||
|
const localExpression = this.expressionStr.substr(ast.span.start, ast.span.end);
|
||||||
|
const identifierStart = ast.span.start + localExpression.indexOf(ast.name);
|
||||||
|
|
||||||
|
// Join the relative position of the expression within a node with the absolute position
|
||||||
|
// of the node to get the absolute position of the expression in the source code.
|
||||||
|
const absoluteStart = this.absoluteOffset + identifierStart;
|
||||||
|
const span = new AbsoluteSourceSpan(absoluteStart, absoluteStart + ast.name.length);
|
||||||
|
|
||||||
this.identifiers.push({
|
this.identifiers.push({
|
||||||
name: ast.name,
|
name: ast.name,
|
||||||
span: ast.span, kind,
|
span,
|
||||||
file: this.file,
|
kind,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,8 +109,17 @@ class TemplateVisitor extends RecursiveTemplateVisitor {
|
||||||
// identifiers of interest found in the template
|
// identifiers of interest found in the template
|
||||||
readonly identifiers = new Set<TemplateIdentifier>();
|
readonly identifiers = new Set<TemplateIdentifier>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a template visitor for a bound template target. The bound target can be used when
|
||||||
|
* deferred to the expression visitor to get information about the target of an expression.
|
||||||
|
*
|
||||||
|
* @param boundTemplate bound template target
|
||||||
|
*/
|
||||||
|
constructor(private boundTemplate: BoundTarget<DirectiveMeta>) { super(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visits a node in the template.
|
* Visits a node in the template.
|
||||||
|
*
|
||||||
* @param node node to visit
|
* @param node node to visit
|
||||||
*/
|
*/
|
||||||
visit(node: HTMLNode) { node.visit(this); }
|
visit(node: HTMLNode) { node.visit(this); }
|
||||||
|
@ -138,72 +137,30 @@ class TemplateVisitor extends RecursiveTemplateVisitor {
|
||||||
this.visitAll(template.references);
|
this.visitAll(template.references);
|
||||||
this.visitAll(template.variables);
|
this.visitAll(template.variables);
|
||||||
}
|
}
|
||||||
visitBoundText(text: BoundText) { this.addIdentifiers(text); }
|
visitBoundText(text: BoundText) { this.visitExpression(text); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds identifiers to the visitor's state.
|
* Visits a node's expression and adds its identifiers, if any, to the visitor's state.
|
||||||
* @param visitedEntities interesting entities to add as identifiers
|
*
|
||||||
* @param curretNode node entities were discovered in
|
* @param curretNode node whose expression to visit
|
||||||
*/
|
*/
|
||||||
private addIdentifiers(node: Node&{value: AST}) {
|
private visitExpression(node: Node&{value: AST}) {
|
||||||
const identifiers = ExpressionVisitor.getIdentifiers(node.value, node);
|
const identifiers = ExpressionVisitor.getIdentifiers(node.value, node, this.boundTemplate);
|
||||||
identifiers.forEach(id => this.identifiers.add(id));
|
identifiers.forEach(id => this.identifiers.add(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Unwraps and reparses a template, preserving whitespace and with no leading trivial characters.
|
|
||||||
*
|
|
||||||
* A template may previously have been parsed without preserving whitespace, and was definitely
|
|
||||||
* parsed with leading trivial characters (see `parseTemplate` from the compiler package API).
|
|
||||||
* Both of these are detrimental for indexing as they result in a manipulated AST not representing
|
|
||||||
* the template source code.
|
|
||||||
*
|
|
||||||
* TODO(ayazhafiz): Remove once issues with `leadingTriviaChars` and `parseTemplate` are resolved.
|
|
||||||
*/
|
|
||||||
function restoreTemplate(template: TmplAstNode[], options: RestoreTemplateOptions): TmplAstNode[] {
|
|
||||||
// try to recapture the template content and URL
|
|
||||||
// if there was nothing in the template to begin with, this is just a no-op
|
|
||||||
if (template.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const {content: templateStr, url: templateUrl} = template[0].sourceSpan.start.file;
|
|
||||||
|
|
||||||
options.preserveWhitespaces = true;
|
|
||||||
const {interpolationConfig, preserveWhitespaces} = options;
|
|
||||||
|
|
||||||
const bindingParser = makeBindingParser(interpolationConfig);
|
|
||||||
const htmlParser = new HtmlParser();
|
|
||||||
const parseResult = htmlParser.parse(templateStr, templateUrl, {
|
|
||||||
...options,
|
|
||||||
tokenizeExpansionForms: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
|
||||||
throw new Error('Impossible state: template must have been successfully parsed previously.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootNodes = visitAll(
|
|
||||||
new I18nMetaVisitor(interpolationConfig, !preserveWhitespaces), parseResult.rootNodes);
|
|
||||||
|
|
||||||
const {nodes, errors} = htmlAstToRender3Ast(rootNodes, bindingParser);
|
|
||||||
if (errors && errors.length > 0) {
|
|
||||||
throw new Error('Impossible state: template must have been successfully parsed previously.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traverses a template AST and builds identifiers discovered in it.
|
* Traverses a template AST and builds identifiers discovered in it.
|
||||||
* @param template template to extract indentifiers from
|
*
|
||||||
* @param options options for restoring the parsed template to a indexable state
|
* @param boundTemplate bound template target, which can be used for querying expression targets.
|
||||||
* @return identifiers in template
|
* @return identifiers in template
|
||||||
*/
|
*/
|
||||||
export function getTemplateIdentifiers(
|
export function getTemplateIdentifiers(boundTemplate: BoundTarget<DirectiveMeta>):
|
||||||
template: TmplAstNode[], options: RestoreTemplateOptions): Set<TemplateIdentifier> {
|
Set<TemplateIdentifier> {
|
||||||
const restoredTemplate = restoreTemplate(template, options);
|
const visitor = new TemplateVisitor(boundTemplate);
|
||||||
const visitor = new TemplateVisitor();
|
if (boundTemplate.target.template !== undefined) {
|
||||||
visitor.visitAll(restoredTemplate);
|
visitor.visitAll(boundTemplate.target.template);
|
||||||
|
}
|
||||||
return visitor.identifiers;
|
return visitor.identifiers;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,11 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {ParseSourceFile} from '@angular/compiler/src/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import {IndexedComponent} from './api';
|
import {IndexedComponent} from './api';
|
||||||
import {IndexingContext} from './context';
|
import {IndexingContext} from './context';
|
||||||
|
import {getTemplateIdentifiers} from './template';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates `IndexedComponent` entries from a `IndexingContext`, which has information
|
* Generates `IndexedComponent` entries from a `IndexingContext`, which has information
|
||||||
|
@ -17,5 +19,42 @@ import {IndexingContext} from './context';
|
||||||
* The context must be populated before `generateAnalysis` is called.
|
* The context must be populated before `generateAnalysis` is called.
|
||||||
*/
|
*/
|
||||||
export function generateAnalysis(context: IndexingContext): Map<ts.Declaration, IndexedComponent> {
|
export function generateAnalysis(context: IndexingContext): Map<ts.Declaration, IndexedComponent> {
|
||||||
throw new Error('Method not implemented.');
|
const analysis = new Map<ts.Declaration, IndexedComponent>();
|
||||||
|
|
||||||
|
context.components.forEach(({declaration, selector, boundTemplate, templateMeta}) => {
|
||||||
|
const name = declaration.name.getText();
|
||||||
|
|
||||||
|
const usedComponents = new Set<ts.Declaration>();
|
||||||
|
const usedDirs = boundTemplate.getUsedDirectives();
|
||||||
|
usedDirs.forEach(dir => {
|
||||||
|
if (dir.isComponent) {
|
||||||
|
usedComponents.add(dir.ref.node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get source files for the component and the template. If the template is inline, its source
|
||||||
|
// file is the component's.
|
||||||
|
const componentFile = new ParseSourceFile(
|
||||||
|
declaration.getSourceFile().getFullText(), declaration.getSourceFile().fileName);
|
||||||
|
let templateFile: ParseSourceFile;
|
||||||
|
if (templateMeta.isInline) {
|
||||||
|
templateFile = componentFile;
|
||||||
|
} else {
|
||||||
|
templateFile = templateMeta.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis.set(declaration, {
|
||||||
|
name,
|
||||||
|
selector,
|
||||||
|
file: componentFile,
|
||||||
|
template: {
|
||||||
|
identifiers: getTemplateIdentifiers(boundTemplate),
|
||||||
|
usedComponents,
|
||||||
|
isInline: templateMeta.isInline,
|
||||||
|
file: templateFile,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return analysis;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,11 @@ ts_library(
|
||||||
]),
|
]),
|
||||||
deps = [
|
deps = [
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/imports",
|
||||||
"//packages/compiler-cli/src/ngtsc/indexer",
|
"//packages/compiler-cli/src/ngtsc/indexer",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/testing",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* @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 {ParseSourceFile} from '@angular/compiler';
|
||||||
|
import {IndexingContext} from '../src/context';
|
||||||
|
import * as util from './util';
|
||||||
|
|
||||||
|
describe('ComponentAnalysisContext', () => {
|
||||||
|
it('should store and return information about components', () => {
|
||||||
|
const context = new IndexingContext();
|
||||||
|
const declaration = util.getComponentDeclaration('class C {};', 'C');
|
||||||
|
const boundTemplate = util.getBoundTemplate('<div></div>');
|
||||||
|
|
||||||
|
context.addComponent({
|
||||||
|
declaration,
|
||||||
|
selector: 'c-selector', boundTemplate,
|
||||||
|
templateMeta: {
|
||||||
|
isInline: false,
|
||||||
|
file: new ParseSourceFile('<div></div>', util.TESTFILE),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(context.components).toEqual(new Set([
|
||||||
|
{
|
||||||
|
declaration,
|
||||||
|
selector: 'c-selector', boundTemplate,
|
||||||
|
templateMeta: {
|
||||||
|
isInline: false,
|
||||||
|
file: new ParseSourceFile('<div></div>', util.TESTFILE),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,93 +6,123 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {InterpolationConfig, ParseSourceFile, TmplAstNode, parseTemplate} from '@angular/compiler';
|
import {AbsoluteSourceSpan, IdentifierKind} from '..';
|
||||||
import {AbsoluteSourceSpan, IdentifierKind, RestoreTemplateOptions} from '..';
|
|
||||||
import {getTemplateIdentifiers} from '../src/template';
|
import {getTemplateIdentifiers} from '../src/template';
|
||||||
|
import * as util from './util';
|
||||||
|
|
||||||
const TEST_FILE = 'TEST';
|
function bind(template: string) {
|
||||||
|
return util.getBoundTemplate(template, {
|
||||||
function parse(template: string): TmplAstNode[] {
|
preserveWhitespaces: true,
|
||||||
return parseTemplate(template, TEST_FILE).nodes;
|
leadingTriviaChars: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('getTemplateIdentifiers', () => {
|
describe('getTemplateIdentifiers', () => {
|
||||||
const DEFAULT_RESTORE_OPTIONS:
|
|
||||||
RestoreTemplateOptions = {interpolationConfig: new InterpolationConfig('{{', '}}')};
|
|
||||||
|
|
||||||
it('should generate nothing in HTML-only template', () => {
|
it('should generate nothing in HTML-only template', () => {
|
||||||
const refs = getTemplateIdentifiers(parse('<div></div>'), DEFAULT_RESTORE_OPTIONS);
|
const refs = getTemplateIdentifiers(bind('<div></div>'));
|
||||||
|
|
||||||
expect(refs.size).toBe(0);
|
expect(refs.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore comments', () => {
|
it('should ignore comments', () => {
|
||||||
const refs = getTemplateIdentifiers(
|
const refs = getTemplateIdentifiers(bind('<!-- {{comment}} -->'));
|
||||||
parse(`
|
|
||||||
<!-- {{my_module}} -->
|
|
||||||
<div><!-- {{goodbye}} --></div>
|
|
||||||
`),
|
|
||||||
DEFAULT_RESTORE_OPTIONS);
|
|
||||||
|
|
||||||
expect(refs.size).toBe(0);
|
expect(refs.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use any interpolation config', () => {
|
it('should handle arbitrary whitespace', () => {
|
||||||
const template = '<div>((foo))</div>';
|
const template = '<div>\n\n {{foo}}</div>';
|
||||||
const refs = getTemplateIdentifiers(
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
parse(template), {interpolationConfig: new InterpolationConfig('((', '))')});
|
|
||||||
|
|
||||||
const [ref] = Array.from(refs);
|
const [ref] = Array.from(refs);
|
||||||
expect(ref.name).toBe('foo');
|
expect(ref).toEqual({
|
||||||
expect(ref.kind).toBe(IdentifierKind.Property);
|
name: 'foo',
|
||||||
expect(ref.span).toEqual(new AbsoluteSourceSpan(7, 10));
|
kind: IdentifierKind.Property,
|
||||||
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
|
span: new AbsoluteSourceSpan(12, 15),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore identifiers defined in the template', () => {
|
||||||
|
const template = `
|
||||||
|
<input #model />
|
||||||
|
{{model.valid}}
|
||||||
|
`;
|
||||||
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
|
const refArr = Array.from(refs);
|
||||||
|
const modelId = refArr.find(ref => ref.name === 'model');
|
||||||
|
expect(modelId).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generates identifiers for PropertyReads', () => {
|
describe('generates identifiers for PropertyReads', () => {
|
||||||
it('should discover component properties', () => {
|
it('should discover component properties', () => {
|
||||||
const template = '<div>{{foo}}</div>';
|
const template = '{{foo}}';
|
||||||
const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS);
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
expect(refs.size).toBe(1);
|
||||||
|
|
||||||
|
const [ref] = Array.from(refs);
|
||||||
|
expect(ref).toEqual({
|
||||||
|
name: 'foo',
|
||||||
|
kind: IdentifierKind.Property,
|
||||||
|
span: new AbsoluteSourceSpan(2, 5),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should discover nested properties', () => {
|
||||||
|
const template = '<div><span>{{foo}}</span></div>';
|
||||||
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
|
const refArr = Array.from(refs);
|
||||||
|
expect(refArr).toEqual(jasmine.arrayContaining([{
|
||||||
|
name: 'foo',
|
||||||
|
kind: IdentifierKind.Property,
|
||||||
|
span: new AbsoluteSourceSpan(13, 16),
|
||||||
|
}]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore identifiers that are not implicitly received by the template', () => {
|
||||||
|
const template = '{{foo.bar.baz}}';
|
||||||
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
expect(refs.size).toBe(1);
|
expect(refs.size).toBe(1);
|
||||||
|
|
||||||
const [ref] = Array.from(refs);
|
const [ref] = Array.from(refs);
|
||||||
expect(ref.name).toBe('foo');
|
expect(ref.name).toBe('foo');
|
||||||
expect(ref.kind).toBe(IdentifierKind.Property);
|
|
||||||
expect(ref.span).toEqual(new AbsoluteSourceSpan(7, 10));
|
|
||||||
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generates identifiers for MethodCalls', () => {
|
||||||
it('should discover component method calls', () => {
|
it('should discover component method calls', () => {
|
||||||
const template = '<div>{{foo()}}</div>';
|
const template = '{{foo()}}';
|
||||||
const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS);
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
expect(refs.size).toBe(1);
|
||||||
|
|
||||||
const [ref] = Array.from(refs);
|
const [ref] = Array.from(refs);
|
||||||
expect(ref.name).toBe('foo');
|
expect(ref).toEqual({
|
||||||
expect(ref.kind).toBe(IdentifierKind.Method);
|
name: 'foo',
|
||||||
expect(ref.span).toEqual(new AbsoluteSourceSpan(7, 10));
|
kind: IdentifierKind.Method,
|
||||||
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
|
span: new AbsoluteSourceSpan(2, 5),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle arbitrary whitespace', () => {
|
it('should discover nested properties', () => {
|
||||||
const template = '<div>\n\n {{foo}}</div>';
|
const template = '<div><span>{{foo()}}</span></div>';
|
||||||
const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS);
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
|
||||||
const [ref] = Array.from(refs);
|
const refArr = Array.from(refs);
|
||||||
expect(ref.name).toBe('foo');
|
expect(refArr).toEqual(jasmine.arrayContaining([{
|
||||||
expect(ref.kind).toBe(IdentifierKind.Property);
|
name: 'foo',
|
||||||
expect(ref.span).toEqual(new AbsoluteSourceSpan(12, 15));
|
kind: IdentifierKind.Method,
|
||||||
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
|
span: new AbsoluteSourceSpan(13, 16),
|
||||||
|
}]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle nested scopes', () => {
|
it('should ignore identifiers that are not implicitly received by the template', () => {
|
||||||
const template = '<div><span>{{foo}}</span></div>';
|
const template = '{{foo().bar().baz()}}';
|
||||||
const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS);
|
const refs = getTemplateIdentifiers(bind(template));
|
||||||
|
expect(refs.size).toBe(1);
|
||||||
|
|
||||||
const [ref] = Array.from(refs);
|
const [ref] = Array.from(refs);
|
||||||
expect(ref.name).toBe('foo');
|
expect(ref.name).toBe('foo');
|
||||||
expect(ref.kind).toBe(IdentifierKind.Property);
|
|
||||||
expect(ref.span).toEqual(new AbsoluteSourceSpan(13, 16));
|
|
||||||
expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
/**
|
||||||
|
* @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 {BoundTarget, ParseSourceFile} from '@angular/compiler';
|
||||||
|
import {DirectiveMeta} from '../../metadata';
|
||||||
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
import {IndexingContext} from '../src/context';
|
||||||
|
import {getTemplateIdentifiers} from '../src/template';
|
||||||
|
import {generateAnalysis} from '../src/transform';
|
||||||
|
import * as util from './util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds information about a component to a context.
|
||||||
|
*/
|
||||||
|
function populateContext(
|
||||||
|
context: IndexingContext, component: ClassDeclaration, selector: string, template: string,
|
||||||
|
boundTemplate: BoundTarget<DirectiveMeta>, isInline: boolean = false) {
|
||||||
|
context.addComponent({
|
||||||
|
declaration: component,
|
||||||
|
selector,
|
||||||
|
boundTemplate,
|
||||||
|
templateMeta: {
|
||||||
|
isInline,
|
||||||
|
file: new ParseSourceFile(template, util.TESTFILE),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('generateAnalysis', () => {
|
||||||
|
it('should emit component and template analysis information', () => {
|
||||||
|
const context = new IndexingContext();
|
||||||
|
const decl = util.getComponentDeclaration('class C {}', 'C');
|
||||||
|
const template = '<div>{{foo}}</div>';
|
||||||
|
populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template));
|
||||||
|
const analysis = generateAnalysis(context);
|
||||||
|
|
||||||
|
expect(analysis.size).toBe(1);
|
||||||
|
|
||||||
|
const info = analysis.get(decl);
|
||||||
|
expect(info).toEqual({
|
||||||
|
name: 'C',
|
||||||
|
selector: 'c-selector',
|
||||||
|
file: new ParseSourceFile('class C {}', util.TESTFILE),
|
||||||
|
template: {
|
||||||
|
identifiers: getTemplateIdentifiers(util.getBoundTemplate('<div>{{foo}}</div>')),
|
||||||
|
usedComponents: new Set(),
|
||||||
|
isInline: false,
|
||||||
|
file: new ParseSourceFile('<div>{{foo}}</div>', util.TESTFILE),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give inline templates the component source file', () => {
|
||||||
|
const context = new IndexingContext();
|
||||||
|
const decl = util.getComponentDeclaration('class C {}', 'C');
|
||||||
|
const template = '<div>{{foo}}</div>';
|
||||||
|
populateContext(
|
||||||
|
context, decl, 'c-selector', '<div>{{foo}}</div>', util.getBoundTemplate(template),
|
||||||
|
/* inline template */ true);
|
||||||
|
const analysis = generateAnalysis(context);
|
||||||
|
|
||||||
|
expect(analysis.size).toBe(1);
|
||||||
|
|
||||||
|
const info = analysis.get(decl);
|
||||||
|
expect(info).toBeDefined();
|
||||||
|
expect(info !.template.file).toEqual(new ParseSourceFile('class C {}', util.TESTFILE));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give external templates their own source file', () => {
|
||||||
|
const context = new IndexingContext();
|
||||||
|
const decl = util.getComponentDeclaration('class C {}', 'C');
|
||||||
|
const template = '<div>{{foo}}</div>';
|
||||||
|
populateContext(context, decl, 'c-selector', template, util.getBoundTemplate(template));
|
||||||
|
const analysis = generateAnalysis(context);
|
||||||
|
|
||||||
|
expect(analysis.size).toBe(1);
|
||||||
|
|
||||||
|
const info = analysis.get(decl);
|
||||||
|
expect(info).toBeDefined();
|
||||||
|
expect(info !.template.file).toEqual(new ParseSourceFile('<div>{{foo}}</div>', util.TESTFILE));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit used components', () => {
|
||||||
|
const context = new IndexingContext();
|
||||||
|
|
||||||
|
const templateA = '<b-selector></b-selector>';
|
||||||
|
const declA = util.getComponentDeclaration('class A {}', 'A');
|
||||||
|
|
||||||
|
const templateB = '<a-selector></a-selector>';
|
||||||
|
const declB = util.getComponentDeclaration('class B {}', 'B');
|
||||||
|
|
||||||
|
const boundA =
|
||||||
|
util.getBoundTemplate(templateA, {}, [{selector: 'b-selector', declaration: declB}]);
|
||||||
|
const boundB =
|
||||||
|
util.getBoundTemplate(templateB, {}, [{selector: 'a-selector', declaration: declA}]);
|
||||||
|
|
||||||
|
populateContext(context, declA, 'a-selector', templateA, boundA);
|
||||||
|
populateContext(context, declB, 'b-selector', templateB, boundB);
|
||||||
|
|
||||||
|
const analysis = generateAnalysis(context);
|
||||||
|
|
||||||
|
expect(analysis.size).toBe(2);
|
||||||
|
|
||||||
|
const infoA = analysis.get(declA);
|
||||||
|
expect(infoA).toBeDefined();
|
||||||
|
expect(infoA !.template.usedComponents).toEqual(new Set([declB]));
|
||||||
|
|
||||||
|
const infoB = analysis.get(declB);
|
||||||
|
expect(infoB).toBeDefined();
|
||||||
|
expect(infoB !.template.usedComponents).toEqual(new Set([declA]));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* @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 {BoundTarget, CssSelector, ParseTemplateOptions, R3TargetBinder, SelectorMatcher, parseTemplate} from '@angular/compiler';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import {Reference} from '../../imports';
|
||||||
|
import {DirectiveMeta} from '../../metadata';
|
||||||
|
import {ClassDeclaration} from '../../reflection';
|
||||||
|
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||||
|
|
||||||
|
/** Dummy file URL */
|
||||||
|
export const TESTFILE = '/TESTFILE.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a class declaration from a component source code.
|
||||||
|
*/
|
||||||
|
export function getComponentDeclaration(componentStr: string, className: string): ClassDeclaration {
|
||||||
|
const program = makeProgram([{name: TESTFILE, contents: componentStr}]);
|
||||||
|
|
||||||
|
return getDeclaration(
|
||||||
|
program.program, TESTFILE, className,
|
||||||
|
(value: ts.Declaration): value is ClassDeclaration => ts.isClassDeclaration(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a template source code and returns a template-bound target, optionally with information
|
||||||
|
* about used components.
|
||||||
|
*
|
||||||
|
* @param template template to parse
|
||||||
|
* @param options extra template parsing options
|
||||||
|
* @param components components to bind to the template target
|
||||||
|
*/
|
||||||
|
export function getBoundTemplate(
|
||||||
|
template: string, options: ParseTemplateOptions = {},
|
||||||
|
components: Array<{selector: string, declaration: ClassDeclaration}> =
|
||||||
|
[]): BoundTarget<DirectiveMeta> {
|
||||||
|
const matcher = new SelectorMatcher<DirectiveMeta>();
|
||||||
|
components.forEach(({selector, declaration}) => {
|
||||||
|
matcher.addSelectables(CssSelector.parse(selector), {
|
||||||
|
ref: new Reference(declaration),
|
||||||
|
selector,
|
||||||
|
queries: [],
|
||||||
|
ngTemplateGuards: [],
|
||||||
|
hasNgTemplateContextGuard: false,
|
||||||
|
baseClass: null,
|
||||||
|
name: declaration.name.getText(),
|
||||||
|
isComponent: true,
|
||||||
|
inputs: {},
|
||||||
|
outputs: {},
|
||||||
|
exportAs: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const binder = new R3TargetBinder(matcher);
|
||||||
|
|
||||||
|
return binder.bind({template: parseTemplate(template, TESTFILE, options).nodes});
|
||||||
|
}
|
|
@ -19,7 +19,8 @@ import {ErrorCode, ngErrorCode} from './diagnostics';
|
||||||
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
|
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
|
||||||
import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
|
import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
|
||||||
import {IncrementalState} from './incremental';
|
import {IncrementalState} from './incremental';
|
||||||
import {IndexedComponent} from './indexer';
|
import {IndexedComponent, IndexingContext} from './indexer';
|
||||||
|
import {generateAnalysis} from './indexer/src/transform';
|
||||||
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry, MetadataReader} from './metadata';
|
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry, MetadataReader} from './metadata';
|
||||||
import {PartialEvaluator} from './partial_evaluator';
|
import {PartialEvaluator} from './partial_evaluator';
|
||||||
import {AbsoluteFsPath, LogicalFileSystem} from './path';
|
import {AbsoluteFsPath, LogicalFileSystem} from './path';
|
||||||
|
@ -268,10 +269,6 @@ export class NgtscProgram implements api.Program {
|
||||||
return this.routeAnalyzer !.listLazyRoutes(entryRoute);
|
return this.routeAnalyzer !.listLazyRoutes(entryRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
getIndexedComponents(): Map<ts.Declaration, IndexedComponent> {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
getLibrarySummaries(): Map<string, api.LibrarySummary> {
|
getLibrarySummaries(): Map<string, api.LibrarySummary> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
@ -434,6 +431,13 @@ export class NgtscProgram implements api.Program {
|
||||||
return diagnostics;
|
return diagnostics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getIndexedComponents(): Map<ts.Declaration, IndexedComponent> {
|
||||||
|
const compilation = this.ensureAnalyzed();
|
||||||
|
const context = new IndexingContext();
|
||||||
|
compilation.index(context);
|
||||||
|
return generateAnalysis(context);
|
||||||
|
}
|
||||||
|
|
||||||
private makeCompilation(): IvyCompilation {
|
private makeCompilation(): IvyCompilation {
|
||||||
const checker = this.tsProgram.getTypeChecker();
|
const checker = this.tsProgram.getTypeChecker();
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ export interface DecoratorHandler<A, M> {
|
||||||
* `IndexingContext`, which stores information about components discovered in the
|
* `IndexingContext`, which stores information about components discovered in the
|
||||||
* program.
|
* program.
|
||||||
*/
|
*/
|
||||||
index?(context: IndexingContext, node: ClassDeclaration, metadata: M): void;
|
index?(context: IndexingContext, node: ClassDeclaration, metadata: A): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform resolution on the given decorator along with the result of analysis.
|
* Perform resolution on the given decorator along with the result of analysis.
|
||||||
|
|
|
@ -254,7 +254,16 @@ export class IvyCompilation {
|
||||||
/**
|
/**
|
||||||
* Feeds components discovered in the compilation to a context for indexing.
|
* Feeds components discovered in the compilation to a context for indexing.
|
||||||
*/
|
*/
|
||||||
index(context: IndexingContext) { throw new Error('Method not implemented.'); }
|
index(context: IndexingContext) {
|
||||||
|
this.ivyClasses.forEach((ivyClass, declaration) => {
|
||||||
|
for (const match of ivyClass.matchedHandlers) {
|
||||||
|
if (match.handler.index !== undefined && match.analyzed !== null &&
|
||||||
|
match.analyzed.analysis !== undefined) {
|
||||||
|
match.handler.index(context, declaration, match.analyzed.analysis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
resolve(): void {
|
resolve(): void {
|
||||||
const resolveSpan = this.perf.start('resolve');
|
const resolveSpan = this.perf.start('resolve');
|
||||||
|
|
|
@ -8,6 +8,7 @@ ts_library(
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
"//packages/compiler-cli",
|
"//packages/compiler-cli",
|
||||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/indexer",
|
||||||
"//packages/compiler-cli/src/ngtsc/path",
|
"//packages/compiler-cli/src/ngtsc/path",
|
||||||
"//packages/compiler-cli/src/ngtsc/routing",
|
"//packages/compiler-cli/src/ngtsc/routing",
|
||||||
"//packages/compiler-cli/src/ngtsc/util",
|
"//packages/compiler-cli/src/ngtsc/util",
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
/**
|
||||||
|
* @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 {AbsoluteSourceSpan, IdentifierKind} from '@angular/compiler-cli/src/ngtsc/indexer';
|
||||||
|
import {ParseSourceFile} from '@angular/compiler/src/compiler';
|
||||||
|
import * as path from 'path';
|
||||||
|
import {NgtscTestEnvironment} from './env';
|
||||||
|
|
||||||
|
describe('ngtsc component indexing', () => {
|
||||||
|
let env !: NgtscTestEnvironment;
|
||||||
|
|
||||||
|
function testPath(testFile: string): string { return path.posix.join(env.basePath, testFile); }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
env = NgtscTestEnvironment.setup();
|
||||||
|
env.tsconfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('indexing metadata', () => {
|
||||||
|
it('should generate component metadata', () => {
|
||||||
|
const componentContent = `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'test-cmp',
|
||||||
|
template: '<div></div>',
|
||||||
|
})
|
||||||
|
export class TestCmp {}
|
||||||
|
`;
|
||||||
|
env.write('test.ts', componentContent);
|
||||||
|
const indexed = env.driveIndexer();
|
||||||
|
expect(indexed.size).toBe(1);
|
||||||
|
|
||||||
|
const [[decl, indexedComp]] = Array.from(indexed.entries());
|
||||||
|
|
||||||
|
expect(decl.getText()).toContain('export class TestCmp {}');
|
||||||
|
expect(indexedComp).toEqual(jasmine.objectContaining({
|
||||||
|
name: 'TestCmp',
|
||||||
|
selector: 'test-cmp',
|
||||||
|
file: new ParseSourceFile(componentContent, testPath('test.ts')),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should index inline templates', () => {
|
||||||
|
const componentContent = `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'test-cmp',
|
||||||
|
template: '{{foo}}',
|
||||||
|
})
|
||||||
|
export class TestCmp { foo = 0; }
|
||||||
|
`;
|
||||||
|
env.write('test.ts', componentContent);
|
||||||
|
const indexed = env.driveIndexer();
|
||||||
|
const [[_, indexedComp]] = Array.from(indexed.entries());
|
||||||
|
const template = indexedComp.template;
|
||||||
|
|
||||||
|
expect(template).toEqual({
|
||||||
|
identifiers: new Set([{
|
||||||
|
name: 'foo',
|
||||||
|
kind: IdentifierKind.Property,
|
||||||
|
span: new AbsoluteSourceSpan(127, 130),
|
||||||
|
}]),
|
||||||
|
usedComponents: new Set(),
|
||||||
|
isInline: true,
|
||||||
|
file: new ParseSourceFile(componentContent, testPath('test.ts')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should index external templates', () => {
|
||||||
|
env.write('test.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'test-cmp',
|
||||||
|
templateUrl: './test.html',
|
||||||
|
})
|
||||||
|
export class TestCmp { foo = 0; }
|
||||||
|
`);
|
||||||
|
env.write('test.html', '{{foo}}');
|
||||||
|
const indexed = env.driveIndexer();
|
||||||
|
const [[_, indexedComp]] = Array.from(indexed.entries());
|
||||||
|
const template = indexedComp.template;
|
||||||
|
|
||||||
|
expect(template).toEqual({
|
||||||
|
identifiers: new Set([{
|
||||||
|
name: 'foo',
|
||||||
|
kind: IdentifierKind.Property,
|
||||||
|
span: new AbsoluteSourceSpan(2, 5),
|
||||||
|
}]),
|
||||||
|
usedComponents: new Set(),
|
||||||
|
isInline: false,
|
||||||
|
file: new ParseSourceFile('{{foo}}', testPath('test.html')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should index templates compiled without preserving whitespace', () => {
|
||||||
|
env.tsconfig({
|
||||||
|
preserveWhitespaces: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
env.write('test.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'test-cmp',
|
||||||
|
templateUrl: './test.html',
|
||||||
|
})
|
||||||
|
export class TestCmp { foo = 0; }
|
||||||
|
`);
|
||||||
|
env.write('test.html', '<div> \n {{foo}}</div>');
|
||||||
|
const indexed = env.driveIndexer();
|
||||||
|
const [[_, indexedComp]] = Array.from(indexed.entries());
|
||||||
|
const template = indexedComp.template;
|
||||||
|
|
||||||
|
expect(template).toEqual({
|
||||||
|
identifiers: new Set([{
|
||||||
|
name: 'foo',
|
||||||
|
kind: IdentifierKind.Property,
|
||||||
|
span: new AbsoluteSourceSpan(12, 15),
|
||||||
|
}]),
|
||||||
|
usedComponents: new Set(),
|
||||||
|
isInline: false,
|
||||||
|
file: new ParseSourceFile('<div> \n {{foo}}</div>', testPath('test.html')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generated information about used components', () => {
|
||||||
|
env.write('test.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'test-cmp',
|
||||||
|
templateUrl: './test.html',
|
||||||
|
})
|
||||||
|
export class TestCmp {}
|
||||||
|
`);
|
||||||
|
env.write('test.html', '<div></div>');
|
||||||
|
env.write('test_import.ts', `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
import {TestCmp} from './test';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: './test_import.html',
|
||||||
|
})
|
||||||
|
export class TestImportCmp {}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
TestCmp,
|
||||||
|
TestImportCmp,
|
||||||
|
],
|
||||||
|
bootstrap: [TestImportCmp]
|
||||||
|
})
|
||||||
|
export class TestModule {}
|
||||||
|
`);
|
||||||
|
env.write('test_import.html', '<test-cmp></test-cmp>');
|
||||||
|
const indexed = env.driveIndexer();
|
||||||
|
expect(indexed.size).toBe(2);
|
||||||
|
|
||||||
|
const indexedComps = Array.from(indexed.values());
|
||||||
|
const testComp = indexedComps.find(comp => comp.name === 'TestCmp');
|
||||||
|
const testImportComp = indexedComps.find(cmp => cmp.name === 'TestImportCmp');
|
||||||
|
expect(testComp).toBeDefined();
|
||||||
|
expect(testImportComp).toBeDefined();
|
||||||
|
|
||||||
|
expect(testComp !.template.usedComponents.size).toBe(0);
|
||||||
|
expect(testImportComp !.template.usedComponents.size).toBe(1);
|
||||||
|
|
||||||
|
const [usedComp] = Array.from(testImportComp !.template.usedComponents);
|
||||||
|
expect(indexed.get(usedComp)).toEqual(testComp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,6 +7,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CustomTransformers, Program} from '@angular/compiler-cli';
|
import {CustomTransformers, Program} from '@angular/compiler-cli';
|
||||||
|
import {IndexedComponent} from '@angular/compiler-cli/src/ngtsc/indexer';
|
||||||
|
import {NgtscProgram} from '@angular/compiler-cli/src/ngtsc/program';
|
||||||
import {setWrapHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host';
|
import {setWrapHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
@ -203,6 +205,13 @@ export class NgtscTestEnvironment {
|
||||||
const program = createProgram({rootNames, host, options});
|
const program = createProgram({rootNames, host, options});
|
||||||
return program.listLazyRoutes(entryPoint);
|
return program.listLazyRoutes(entryPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
driveIndexer(): Map<ts.Declaration, IndexedComponent> {
|
||||||
|
const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]);
|
||||||
|
const host = createCompilerHost({options});
|
||||||
|
const program = createProgram({rootNames, host, options});
|
||||||
|
return (program as NgtscProgram).getIndexedComponents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AugmentedCompilerHost {
|
class AugmentedCompilerHost {
|
||||||
|
|
|
@ -97,7 +97,7 @@ export {Identifiers as R3Identifiers} from './render3/r3_identifiers';
|
||||||
export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
|
export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
|
||||||
export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_compiler';
|
export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_compiler';
|
||||||
export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
|
export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
|
||||||
export {makeBindingParser, parseTemplate} from './render3/view/template';
|
export {makeBindingParser, parseTemplate, ParseTemplateOptions} from './render3/view/template';
|
||||||
export {R3Reference} from './render3/util';
|
export {R3Reference} from './render3/util';
|
||||||
export {compileBaseDefFromMetadata, R3BaseRefMetaData, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, ParsedHostBindings, verifyHostBindings} from './render3/view/compiler';
|
export {compileBaseDefFromMetadata, R3BaseRefMetaData, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, ParsedHostBindings, verifyHostBindings} from './render3/view/compiler';
|
||||||
export {publishFacade} from './jit_compiler_facade';
|
export {publishFacade} from './jit_compiler_facade';
|
||||||
|
|
Loading…
Reference in New Issue