diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index 2b33edbd71..cbeec1e959 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/reflection", diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index c2364db4db..c5626ef2ad 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -6,13 +6,14 @@ * 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 ts from 'typescript'; import {CycleAnalyzer} from '../../cycles'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; +import {IndexingContext} from '../../indexer'; import {DirectiveMeta, MetadataReader, MetadataRegistry, extractDirectiveGuards} from '../../metadata'; import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; @@ -35,6 +36,7 @@ export interface ComponentHandlerData { meta: R3ComponentMetadata; parsedTemplate: TmplAstNode[]; metadataStmt: Statement|null; + parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate; } /** @@ -169,11 +171,22 @@ export class ComponentDecoratorHandler implements // Parse the template. // If a preanalyze phase was executed, the template may already exist in parsed form, so check // 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)) { // 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); + + // 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 { // The template was not already parsed. Either there's a templateUrl, or an inline template. if (component.has('templateUrl')) { @@ -187,9 +200,9 @@ export class ComponentDecoratorHandler implements const templateStr = this.resourceLoader.load(templateUrl); this.resourceDependencies.recordResourceDependency(node.getSourceFile(), templateUrl); - template = this._parseTemplate( + parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate( component, templateStr, sourceMapUrl(templateUrl), /* templateRange */ undefined, - /* escapedString */ false); + /* escapedString */ false, options); } else { // Expect an inline template to be present. const inlineTemplate = this._extractInlineTemplate(component, relativeContextFilePath); @@ -199,10 +212,11 @@ export class ComponentDecoratorHandler implements 'component is missing a template'); } const {templateStr, templateUrl, templateRange, escapedString} = inlineTemplate; - template = - this._parseTemplate(component, templateStr, templateUrl, templateRange, escapedString); + parseTemplate = (options?: ParseTemplateOptions) => this._parseTemplate( + component, templateStr, templateUrl, templateRange, escapedString, options); } } + const template = parseTemplate(); if (template.errors !== undefined) { throw new Error( @@ -294,7 +308,7 @@ export class ComponentDecoratorHandler implements }, metadataStmt: generateSetClassMetadataCall( node, this.reflector, this.defaultImportRecorder, this.isCore), - parsedTemplate: template.nodes, + parsedTemplate: template.nodes, parseTemplate, }, typeCheck: true, }; @@ -304,6 +318,37 @@ export class ComponentDecoratorHandler implements 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(); + 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 { if (!ts.isClassDeclaration(node)) { return; @@ -576,7 +621,8 @@ export class ComponentDecoratorHandler implements private _parseTemplate( component: Map, templateStr: string, templateUrl: string, - templateRange: LexerRange|undefined, escapedString: boolean): ParsedTemplate { + templateRange: LexerRange|undefined, escapedString: boolean, + options: ParseTemplateOptions = {}): ParsedTemplate { let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces; if (component.has('preserveWhitespaces')) { const expr = component.get('preserveWhitespaces') !; @@ -602,11 +648,14 @@ export class ComponentDecoratorHandler implements } return { - interpolation, ...parseTemplate(templateStr, templateUrl, { - preserveWhitespaces, - interpolationConfig: interpolation, - range: templateRange, escapedString - }), + interpolation, + ...parseTemplate(templateStr, templateUrl, { + preserveWhitespaces, + interpolationConfig: interpolation, + range: templateRange, escapedString, ...options, + }), + isInline: component.has('template'), + file: new ParseSourceFile(templateStr, templateUrl), }; } @@ -668,4 +717,6 @@ interface ParsedTemplate { nodes: TmplAstNode[]; styleUrls: string[]; styles: string[]; + isInline: boolean; + file: ParseSourceFile; } diff --git a/packages/compiler-cli/src/ngtsc/indexer/BUILD.bazel b/packages/compiler-cli/src/ngtsc/indexer/BUILD.bazel index 6151c77e8f..630216cc7d 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/indexer/BUILD.bazel @@ -9,6 +9,9 @@ ts_library( ]), deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/metadata", + "//packages/compiler-cli/src/ngtsc/reflection", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/api.ts b/packages/compiler-cli/src/ngtsc/indexer/src/api.ts index 0a9f98d4cd..e0e3ba10f3 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/api.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {InterpolationConfig, ParseSourceFile} from '@angular/compiler'; -import {ParseTemplateOptions} from '@angular/compiler/src/render3/view/template'; +import {ParseSourceFile} from '@angular/compiler'; import * as ts from 'typescript'; /** @@ -33,7 +32,6 @@ export interface TemplateIdentifier { name: string; span: AbsoluteSourceSpan; kind: IdentifierKind; - file: ParseSourceFile; } /** @@ -42,21 +40,11 @@ export interface TemplateIdentifier { export interface IndexedComponent { name: string; selector: string|null; - sourceFile: string; - content: string; + file: ParseSourceFile; template: { identifiers: Set, - usedComponents: Set, + usedComponents: Set, + 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; -} diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/context.ts b/packages/compiler-cli/src/ngtsc/indexer/src/context.ts index d571e01433..32991eab07 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/context.ts @@ -6,8 +6,55 @@ * 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; + /** + * Unparsed selector of the directive. + */ + selector: string; +} + /** - * Stores analysis information about components in a compilation for and provides methods for - * querying information about components to be used in indexing. + * An intermediate representation of a component. */ -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; + + /** 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(); + + /** + * Adds a component to the context. + */ + addComponent(info: ComponentInfo) { this.components.add(info); } +} diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts index 63bfd991c3..5aa38ee2ba 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts @@ -6,12 +6,9 @@ * 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 {htmlAstToRender3Ast} from '@angular/compiler/src/render3/r3_template_transform'; -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'; +import {AbsoluteSourceSpan, IdentifierKind, TemplateIdentifier} from './api'; /** * 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; } -/** - * 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 * entities (variable references, etc.). Creates an array of Entities found in @@ -72,41 +29,74 @@ function updateIdentifierSpans(identifiers: TemplateIdentifier[], currentNode: N * 11}}]`. */ 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, + private readonly expressionStr = context.sourceSpan.toString(), + private readonly absoluteOffset = context.sourceSpan.start.offset) { 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): + TemplateIdentifier[] { + const visitor = new ExpressionVisitor(context, boundTemplate); visitor.visit(ast); - const identifiers = visitor.identifiers; - - updateIdentifierSpans(identifiers, context); - - return identifiers; + return visitor.identifiers; } visit(ast: AST) { ast.visit(this); } visitMethodCall(ast: MethodCall, context: {}) { - this.addIdentifier(ast, IdentifierKind.Method); + this.visitIdentifier(ast, IdentifierKind.Method); super.visitMethodCall(ast, context); } visitPropertyRead(ast: PropertyRead, context: {}) { - this.addIdentifier(ast, IdentifierKind.Property); + this.visitIdentifier(ast, IdentifierKind.Property); 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({ name: ast.name, - span: ast.span, kind, - file: this.file, + span, + kind, }); } } @@ -119,8 +109,17 @@ class TemplateVisitor extends RecursiveTemplateVisitor { // identifiers of interest found in the template readonly identifiers = new Set(); + /** + * 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) { super(); } + /** * Visits a node in the template. + * * @param node node to visit */ visit(node: HTMLNode) { node.visit(this); } @@ -138,72 +137,30 @@ class TemplateVisitor extends RecursiveTemplateVisitor { this.visitAll(template.references); this.visitAll(template.variables); } - visitBoundText(text: BoundText) { this.addIdentifiers(text); } + visitBoundText(text: BoundText) { this.visitExpression(text); } /** - * Adds identifiers to the visitor's state. - * @param visitedEntities interesting entities to add as identifiers - * @param curretNode node entities were discovered in + * Visits a node's expression and adds its identifiers, if any, to the visitor's state. + * + * @param curretNode node whose expression to visit */ - private addIdentifiers(node: Node&{value: AST}) { - const identifiers = ExpressionVisitor.getIdentifiers(node.value, node); + private visitExpression(node: Node&{value: AST}) { + const identifiers = ExpressionVisitor.getIdentifiers(node.value, node, this.boundTemplate); 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. - * @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 */ -export function getTemplateIdentifiers( - template: TmplAstNode[], options: RestoreTemplateOptions): Set { - const restoredTemplate = restoreTemplate(template, options); - const visitor = new TemplateVisitor(); - visitor.visitAll(restoredTemplate); +export function getTemplateIdentifiers(boundTemplate: BoundTarget): + Set { + const visitor = new TemplateVisitor(boundTemplate); + if (boundTemplate.target.template !== undefined) { + visitor.visitAll(boundTemplate.target.template); + } return visitor.identifiers; } diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/transform.ts b/packages/compiler-cli/src/ngtsc/indexer/src/transform.ts index 60161e2b89..8cad2b5980 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/transform.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {ParseSourceFile} from '@angular/compiler/src/compiler'; import * as ts from 'typescript'; import {IndexedComponent} from './api'; import {IndexingContext} from './context'; +import {getTemplateIdentifiers} from './template'; /** * 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. */ export function generateAnalysis(context: IndexingContext): Map { - throw new Error('Method not implemented.'); + const analysis = new Map(); + + context.components.forEach(({declaration, selector, boundTemplate, templateMeta}) => { + const name = declaration.name.getText(); + + const usedComponents = new Set(); + 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; } diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/indexer/test/BUILD.bazel index 98a9bc2401..afc4e12861 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/indexer/test/BUILD.bazel @@ -10,7 +10,11 @@ ts_library( ]), deps = [ "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/imports", "//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", ], ) diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/context_spec.ts b/packages/compiler-cli/src/ngtsc/indexer/test/context_spec.ts new file mode 100644 index 0000000000..410aa9c40d --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/indexer/test/context_spec.ts @@ -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('
'); + + context.addComponent({ + declaration, + selector: 'c-selector', boundTemplate, + templateMeta: { + isInline: false, + file: new ParseSourceFile('
', util.TESTFILE), + }, + }); + + expect(context.components).toEqual(new Set([ + { + declaration, + selector: 'c-selector', boundTemplate, + templateMeta: { + isInline: false, + file: new ParseSourceFile('
', util.TESTFILE), + }, + }, + ])); + }); +}); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts b/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts index 488235a8a9..15f6b521cd 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts @@ -6,93 +6,123 @@ * found in the LICENSE file at https://angular.io/license */ -import {InterpolationConfig, ParseSourceFile, TmplAstNode, parseTemplate} from '@angular/compiler'; -import {AbsoluteSourceSpan, IdentifierKind, RestoreTemplateOptions} from '..'; +import {AbsoluteSourceSpan, IdentifierKind} from '..'; import {getTemplateIdentifiers} from '../src/template'; +import * as util from './util'; -const TEST_FILE = 'TEST'; - -function parse(template: string): TmplAstNode[] { - return parseTemplate(template, TEST_FILE).nodes; +function bind(template: string) { + return util.getBoundTemplate(template, { + preserveWhitespaces: true, + leadingTriviaChars: [], + }); } describe('getTemplateIdentifiers', () => { - const DEFAULT_RESTORE_OPTIONS: - RestoreTemplateOptions = {interpolationConfig: new InterpolationConfig('{{', '}}')}; - it('should generate nothing in HTML-only template', () => { - const refs = getTemplateIdentifiers(parse('
'), DEFAULT_RESTORE_OPTIONS); + const refs = getTemplateIdentifiers(bind('
')); expect(refs.size).toBe(0); }); it('should ignore comments', () => { - const refs = getTemplateIdentifiers( - parse(` - -
- `), - DEFAULT_RESTORE_OPTIONS); + const refs = getTemplateIdentifiers(bind('')); expect(refs.size).toBe(0); }); - it('should use any interpolation config', () => { - const template = '
((foo))
'; - const refs = getTemplateIdentifiers( - parse(template), {interpolationConfig: new InterpolationConfig('((', '))')}); + it('should handle arbitrary whitespace', () => { + const template = '
\n\n {{foo}}
'; + const refs = getTemplateIdentifiers(bind(template)); const [ref] = Array.from(refs); - 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)); + expect(ref).toEqual({ + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(12, 15), + }); + }); + + it('should ignore identifiers defined in the template', () => { + const template = ` + + {{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', () => { it('should discover component properties', () => { - const template = '
{{foo}}
'; - const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS); + const template = '{{foo}}'; + 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 = '
{{foo}}
'; + 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); const [ref] = Array.from(refs); 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', () => { - const template = '
{{foo()}}
'; - const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS); + const template = '{{foo()}}'; + const refs = getTemplateIdentifiers(bind(template)); + expect(refs.size).toBe(1); const [ref] = Array.from(refs); - expect(ref.name).toBe('foo'); - expect(ref.kind).toBe(IdentifierKind.Method); - expect(ref.span).toEqual(new AbsoluteSourceSpan(7, 10)); - expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE)); + expect(ref).toEqual({ + name: 'foo', + kind: IdentifierKind.Method, + span: new AbsoluteSourceSpan(2, 5), + }); }); - it('should handle arbitrary whitespace', () => { - const template = '
\n\n {{foo}}
'; - const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS); + it('should discover nested properties', () => { + const template = '
{{foo()}}
'; + const refs = getTemplateIdentifiers(bind(template)); - const [ref] = Array.from(refs); - expect(ref.name).toBe('foo'); - expect(ref.kind).toBe(IdentifierKind.Property); - expect(ref.span).toEqual(new AbsoluteSourceSpan(12, 15)); - expect(ref.file).toEqual(new ParseSourceFile(template, TEST_FILE)); + const refArr = Array.from(refs); + expect(refArr).toEqual(jasmine.arrayContaining([{ + name: 'foo', + kind: IdentifierKind.Method, + span: new AbsoluteSourceSpan(13, 16), + }])); }); - it('should handle nested scopes', () => { - const template = '
{{foo}}
'; - const refs = getTemplateIdentifiers(parse(template), DEFAULT_RESTORE_OPTIONS); + 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); const [ref] = Array.from(refs); 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)); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts b/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts new file mode 100644 index 0000000000..e3cfcd9684 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts @@ -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, 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 = '
{{foo}}
'; + 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('
{{foo}}
')), + usedComponents: new Set(), + isInline: false, + file: new ParseSourceFile('
{{foo}}
', util.TESTFILE), + } + }); + }); + + it('should give inline templates the component source file', () => { + const context = new IndexingContext(); + const decl = util.getComponentDeclaration('class C {}', 'C'); + const template = '
{{foo}}
'; + populateContext( + context, decl, 'c-selector', '
{{foo}}
', 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 = '
{{foo}}
'; + 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('
{{foo}}
', util.TESTFILE)); + }); + + it('should emit used components', () => { + const context = new IndexingContext(); + + const templateA = ''; + const declA = util.getComponentDeclaration('class A {}', 'A'); + + const templateB = ''; + 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])); + }); +}); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts new file mode 100644 index 0000000000..a6ae55b45d --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts @@ -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 { + const matcher = new SelectorMatcher(); + 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}); +} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 967d0e67d3..dcf334c664 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -19,7 +19,8 @@ import {ErrorCode, ngErrorCode} from './diagnostics'; 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 {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 {PartialEvaluator} from './partial_evaluator'; import {AbsoluteFsPath, LogicalFileSystem} from './path'; @@ -268,10 +269,6 @@ export class NgtscProgram implements api.Program { return this.routeAnalyzer !.listLazyRoutes(entryRoute); } - getIndexedComponents(): Map { - throw new Error('Method not implemented.'); - } - getLibrarySummaries(): Map { throw new Error('Method not implemented.'); } @@ -434,6 +431,13 @@ export class NgtscProgram implements api.Program { return diagnostics; } + getIndexedComponents(): Map { + const compilation = this.ensureAnalyzed(); + const context = new IndexingContext(); + compilation.index(context); + return generateAnalysis(context); + } + private makeCompilation(): IvyCompilation { const checker = this.tsProgram.getTypeChecker(); diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 2b8967bba9..7f1db50c7c 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -82,7 +82,7 @@ export interface DecoratorHandler { * `IndexingContext`, which stores information about components discovered in the * 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. diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index e32c0abe9e..da694ea6d3 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -254,7 +254,16 @@ export class IvyCompilation { /** * 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 { const resolveSpan = this.perf.start('resolve'); diff --git a/packages/compiler-cli/test/ngtsc/BUILD.bazel b/packages/compiler-cli/test/ngtsc/BUILD.bazel index 82487c5247..4041497eee 100644 --- a/packages/compiler-cli/test/ngtsc/BUILD.bazel +++ b/packages/compiler-cli/test/ngtsc/BUILD.bazel @@ -8,6 +8,7 @@ ts_library( "//packages/compiler", "//packages/compiler-cli", "//packages/compiler-cli/src/ngtsc/diagnostics", + "//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/routing", "//packages/compiler-cli/src/ngtsc/util", diff --git a/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts b/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts new file mode 100644 index 0000000000..b7b2ad3155 --- /dev/null +++ b/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts @@ -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: '
', + }) + 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', '
\n {{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(12, 15), + }]), + usedComponents: new Set(), + isInline: false, + file: new ParseSourceFile('
\n {{foo}}
', 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', '
'); + 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', ''); + 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); + }); + }); +}); diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index c2252c1897..eff7ac9fdc 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -7,6 +7,8 @@ */ 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 * as fs from 'fs'; import * as path from 'path'; @@ -203,6 +205,13 @@ export class NgtscTestEnvironment { const program = createProgram({rootNames, host, options}); return program.listLazyRoutes(entryPoint); } + + driveIndexer(): Map { + const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]); + const host = createCompilerHost({options}); + const program = createProgram({rootNames, host, options}); + return (program as NgtscProgram).getIndexedComponents(); + } } class AugmentedCompilerHost { diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 1a81d4471e..092d25b9f9 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -97,7 +97,7 @@ export {Identifiers as R3Identifiers} from './render3/r3_identifiers'; export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory'; export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_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 {compileBaseDefFromMetadata, R3BaseRefMetaData, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, ParsedHostBindings, verifyHostBindings} from './render3/view/compiler'; export {publishFacade} from './jit_compiler_facade';