refactor(compiler-cli): Store inline templates and styles in the resource registry (#39482)
The Language Service is not only interested in external resources, but also inline styles and templates. By storing the expression of the inline resources, we can more easily determine if a given position is part of the inline template/style expression. PR Close #39482
This commit is contained in:
parent
3241d922fc
commit
beb935613e
|
@ -262,7 +262,7 @@ export class ComponentDecoratorHandler implements
|
|||
}
|
||||
}
|
||||
const templateResource = template.isInline ?
|
||||
null :
|
||||
{path: null, expression: component.get('template')!} :
|
||||
{path: absoluteFrom(template.templateUrl), expression: template.sourceMapping.node};
|
||||
|
||||
let diagnostics: ts.Diagnostic[]|undefined = undefined;
|
||||
|
@ -666,21 +666,30 @@ export class ComponentDecoratorHandler implements
|
|||
|
||||
private _extractStyleResources(component: Map<string, ts.Expression>, containingFile: string):
|
||||
ReadonlySet<Resource> {
|
||||
const styleUrlsExpr = component.get('styleUrls');
|
||||
// If styleUrls is a literal array of strings, process each resource url individually and
|
||||
// register them. Otherwise, give up and return an empty set.
|
||||
if (styleUrlsExpr === undefined || !ts.isArrayLiteralExpression(styleUrlsExpr) ||
|
||||
!styleUrlsExpr.elements.every(e => ts.isStringLiteralLike(e))) {
|
||||
return new Set();
|
||||
const styles = new Set<Resource>();
|
||||
function stringLiteralElements(array: ts.ArrayLiteralExpression): ts.StringLiteralLike[] {
|
||||
return array.elements.filter(
|
||||
(e: ts.Expression): e is ts.StringLiteralLike => ts.isStringLiteralLike(e));
|
||||
}
|
||||
|
||||
const externalStyles = new Set<Resource>();
|
||||
for (const expression of styleUrlsExpr.elements) {
|
||||
const resourceUrl =
|
||||
this.resourceLoader.resolve((expression as ts.StringLiteralLike).text, containingFile);
|
||||
externalStyles.add({path: absoluteFrom(resourceUrl), expression});
|
||||
// If styleUrls is a literal array, process each resource url individually and
|
||||
// register ones that are string literals.
|
||||
const styleUrlsExpr = component.get('styleUrls');
|
||||
if (styleUrlsExpr !== undefined && ts.isArrayLiteralExpression(styleUrlsExpr)) {
|
||||
for (const expression of stringLiteralElements(styleUrlsExpr)) {
|
||||
const resourceUrl = this.resourceLoader.resolve(expression.text, containingFile);
|
||||
styles.add({path: absoluteFrom(resourceUrl), expression});
|
||||
}
|
||||
}
|
||||
return externalStyles;
|
||||
|
||||
const stylesExpr = component.get('styles');
|
||||
if (stylesExpr !== undefined && ts.isArrayLiteralExpression(stylesExpr)) {
|
||||
for (const expression of stringLiteralElements(stylesExpr)) {
|
||||
styles.add({path: null, expression});
|
||||
}
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
private _preloadAndParseTemplate(
|
||||
|
|
|
@ -18,18 +18,49 @@ import {getDeclaration, makeProgram} from '../../testing';
|
|||
import {ResourceLoader} from '../src/api';
|
||||
import {ComponentDecoratorHandler} from '../src/component';
|
||||
|
||||
export class NoopResourceLoader implements ResourceLoader {
|
||||
resolve(): string {
|
||||
throw new Error('Not implemented.');
|
||||
export class StubResourceLoader implements ResourceLoader {
|
||||
resolve(v: string): string {
|
||||
return v;
|
||||
}
|
||||
canPreload = false;
|
||||
load(): string {
|
||||
throw new Error('Not implemented');
|
||||
load(v: string): string {
|
||||
return '';
|
||||
}
|
||||
preload(): Promise<void>|undefined {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.CompilerHost) {
|
||||
const checker = program.getTypeChecker();
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null);
|
||||
const moduleResolver =
|
||||
new ModuleResolver(program, options, host, /* moduleResolutionCache */ null);
|
||||
const importGraph = new ImportGraph(moduleResolver);
|
||||
const cycleAnalyzer = new CycleAnalyzer(importGraph);
|
||||
const metaRegistry = new LocalMetadataRegistry();
|
||||
const dtsReader = new DtsMetadataReader(checker, reflectionHost);
|
||||
const scopeRegistry = new LocalModuleScopeRegistry(
|
||||
metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
|
||||
null);
|
||||
const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]);
|
||||
const refEmitter = new ReferenceEmitter([]);
|
||||
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
|
||||
const resourceRegistry = new ResourceRegistry();
|
||||
|
||||
const handler = new ComponentDecoratorHandler(
|
||||
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry,
|
||||
resourceRegistry,
|
||||
/* isCore */ false, new StubResourceLoader(), /* rootDirs */['/'],
|
||||
/* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
|
||||
/* enableI18nLegacyMessageIdFormat */ false,
|
||||
/* i18nNormalizeLineEndingsInICUs */ undefined, moduleResolver, cycleAnalyzer, refEmitter,
|
||||
NOOP_DEFAULT_IMPORT_RECORDER, /* depTracker */ null, injectableRegistry,
|
||||
/* annotateForClosureCompiler */ false);
|
||||
return {reflectionHost, handler};
|
||||
}
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('ComponentDecoratorHandler', () => {
|
||||
let _: typeof absoluteFrom;
|
||||
|
@ -51,31 +82,7 @@ runInEachFileSystem(() => {
|
|||
`
|
||||
},
|
||||
]);
|
||||
const checker = program.getTypeChecker();
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null);
|
||||
const moduleResolver =
|
||||
new ModuleResolver(program, options, host, /* moduleResolutionCache */ null);
|
||||
const importGraph = new ImportGraph(moduleResolver);
|
||||
const cycleAnalyzer = new CycleAnalyzer(importGraph);
|
||||
const metaRegistry = new LocalMetadataRegistry();
|
||||
const dtsReader = new DtsMetadataReader(checker, reflectionHost);
|
||||
const scopeRegistry = new LocalModuleScopeRegistry(
|
||||
metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null),
|
||||
new ReferenceEmitter([]), null);
|
||||
const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]);
|
||||
const refEmitter = new ReferenceEmitter([]);
|
||||
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
|
||||
|
||||
const handler = new ComponentDecoratorHandler(
|
||||
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry,
|
||||
new ResourceRegistry(),
|
||||
/* isCore */ false, new NoopResourceLoader(), /* rootDirs */[''],
|
||||
/* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
|
||||
/* enableI18nLegacyMessageIdFormat */ false,
|
||||
/* i18nNormalizeLineEndingsInICUs */ undefined, moduleResolver, cycleAnalyzer, refEmitter,
|
||||
NOOP_DEFAULT_IMPORT_RECORDER, /* depTracker */ null, injectableRegistry,
|
||||
/* annotateForClosureCompiler */ false);
|
||||
const {reflectionHost, handler} = setup(program, options, host);
|
||||
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
|
||||
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
|
||||
if (detected === undefined) {
|
||||
|
@ -94,6 +101,104 @@ runInEachFileSystem(() => {
|
|||
expect(diag.start).toBe(detected.metadata.args![0].getStart());
|
||||
}
|
||||
});
|
||||
|
||||
it('should keep track of inline template', () => {
|
||||
const template = '<span>inline</span>';
|
||||
const {program, options, host} = makeProgram([
|
||||
{
|
||||
name: _('/node_modules/@angular/core/index.d.ts'),
|
||||
contents: 'export const Component: any;',
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: '${template}',
|
||||
}) class TestCmp {}
|
||||
`
|
||||
},
|
||||
]);
|
||||
const {reflectionHost, handler} = setup(program, options, host);
|
||||
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
|
||||
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
|
||||
if (detected === undefined) {
|
||||
return fail('Failed to recognize @Component');
|
||||
}
|
||||
const {analysis} = handler.analyze(TestCmp, detected.metadata);
|
||||
expect(analysis?.resources.template.path).toBeNull();
|
||||
expect(analysis?.resources.template.expression.getText()).toEqual(`'${template}'`);
|
||||
});
|
||||
|
||||
it('should keep track of external template', () => {
|
||||
const templateUrl = '/myTemplate.ng.html';
|
||||
const {program, options, host} = makeProgram([
|
||||
{
|
||||
name: _('/node_modules/@angular/core/index.d.ts'),
|
||||
contents: 'export const Component: any;',
|
||||
},
|
||||
{
|
||||
name: _(templateUrl),
|
||||
contents: '<div>hello world</div>',
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
templateUrl: '${templateUrl}',
|
||||
}) class TestCmp {}
|
||||
`
|
||||
},
|
||||
]);
|
||||
const {reflectionHost, handler} = setup(program, options, host);
|
||||
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
|
||||
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
|
||||
if (detected === undefined) {
|
||||
return fail('Failed to recognize @Component');
|
||||
}
|
||||
const {analysis} = handler.analyze(TestCmp, detected.metadata);
|
||||
expect(analysis?.resources.template.path).toContain(templateUrl);
|
||||
expect(analysis?.resources.template.expression.getText()).toContain(`'${templateUrl}'`);
|
||||
});
|
||||
|
||||
it('should keep track of internal and external styles', () => {
|
||||
const {program, options, host} = makeProgram([
|
||||
{
|
||||
name: _('/node_modules/@angular/core/index.d.ts'),
|
||||
contents: 'export const Component: any;',
|
||||
},
|
||||
{
|
||||
name: _('/myStyle.css'),
|
||||
contents: '<div>hello world</div>',
|
||||
},
|
||||
{
|
||||
name: _('/entry.ts'),
|
||||
contents: `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
// These are ignored because we only attempt to store string literals
|
||||
const ignoredStyleUrl = 'asdlfkj';
|
||||
const ignoredStyle = '';
|
||||
@Component({
|
||||
template: '',
|
||||
styleUrls: ['/myStyle.css', ignoredStyleUrl],
|
||||
styles: ['a { color: red; }', 'b { color: blue; }', ignoredStyle, ...[ignoredStyle]],
|
||||
}) class TestCmp {}
|
||||
`
|
||||
},
|
||||
]);
|
||||
const {reflectionHost, handler} = setup(program, options, host);
|
||||
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
|
||||
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
|
||||
if (detected === undefined) {
|
||||
return fail('Failed to recognize @Component');
|
||||
}
|
||||
const {analysis} = handler.analyze(TestCmp, detected.metadata);
|
||||
expect(analysis?.resources.styles.size).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
function ivyCode(code: ErrorCode): number {
|
||||
|
|
|
@ -261,6 +261,9 @@ export class NgCompiler {
|
|||
const {resourceRegistry} = this.ensureAnalyzed();
|
||||
const styles = resourceRegistry.getStyles(classDecl);
|
||||
const template = resourceRegistry.getTemplate(classDecl);
|
||||
if (template === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {styles, template};
|
||||
}
|
||||
|
|
|
@ -12,23 +12,24 @@ import {AbsoluteFsPath} from '../../file_system';
|
|||
import {ClassDeclaration} from '../../reflection';
|
||||
|
||||
/**
|
||||
* Represents an external resource for a component and contains the `AbsoluteFsPath`
|
||||
* Represents an resource for a component and contains the `AbsoluteFsPath`
|
||||
* to the file which was resolved by evaluating the `ts.Expression` (generally, a relative or
|
||||
* absolute string path to the resource).
|
||||
*
|
||||
* If the resource is inline, the `path` will be `null`.
|
||||
*/
|
||||
export interface Resource {
|
||||
path: AbsoluteFsPath;
|
||||
path: AbsoluteFsPath|null;
|
||||
expression: ts.Expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the external resources of a component.
|
||||
* Represents the either inline or external resources of a component.
|
||||
*
|
||||
* If the component uses an inline template, the template resource will be `null`.
|
||||
* If the component does not have external styles, the `styles` `Set` will be empty.
|
||||
* A resource with a `path` of `null` is considered inline.
|
||||
*/
|
||||
export interface ComponentResources {
|
||||
template: Resource|null;
|
||||
template: Resource;
|
||||
styles: ReadonlySet<Resource>;
|
||||
}
|
||||
|
||||
|
@ -40,17 +41,17 @@ export interface ComponentResources {
|
|||
* assistance.
|
||||
*/
|
||||
export class ResourceRegistry {
|
||||
private templateToComponentsMap = new Map<AbsoluteFsPath, Set<ClassDeclaration>>();
|
||||
private externalTemplateToComponentsMap = new Map<AbsoluteFsPath, Set<ClassDeclaration>>();
|
||||
private componentToTemplateMap = new Map<ClassDeclaration, Resource>();
|
||||
private componentToStylesMap = new Map<ClassDeclaration, Set<Resource>>();
|
||||
private styleToComponentsMap = new Map<AbsoluteFsPath, Set<ClassDeclaration>>();
|
||||
private externalStyleToComponentsMap = new Map<AbsoluteFsPath, Set<ClassDeclaration>>();
|
||||
|
||||
getComponentsWithTemplate(template: AbsoluteFsPath): ReadonlySet<ClassDeclaration> {
|
||||
if (!this.templateToComponentsMap.has(template)) {
|
||||
if (!this.externalTemplateToComponentsMap.has(template)) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return this.templateToComponentsMap.get(template)!;
|
||||
return this.externalTemplateToComponentsMap.get(template)!;
|
||||
}
|
||||
|
||||
registerResources(resources: ComponentResources, component: ClassDeclaration) {
|
||||
|
@ -64,10 +65,12 @@ export class ResourceRegistry {
|
|||
|
||||
registerTemplate(templateResource: Resource, component: ClassDeclaration): void {
|
||||
const {path} = templateResource;
|
||||
if (!this.templateToComponentsMap.has(path)) {
|
||||
this.templateToComponentsMap.set(path, new Set());
|
||||
if (path !== null) {
|
||||
if (!this.externalTemplateToComponentsMap.has(path)) {
|
||||
this.externalTemplateToComponentsMap.set(path, new Set());
|
||||
}
|
||||
this.externalTemplateToComponentsMap.get(path)!.add(component);
|
||||
}
|
||||
this.templateToComponentsMap.get(path)!.add(component);
|
||||
this.componentToTemplateMap.set(component, templateResource);
|
||||
}
|
||||
|
||||
|
@ -83,10 +86,12 @@ export class ResourceRegistry {
|
|||
if (!this.componentToStylesMap.has(component)) {
|
||||
this.componentToStylesMap.set(component, new Set());
|
||||
}
|
||||
if (!this.styleToComponentsMap.has(path)) {
|
||||
this.styleToComponentsMap.set(path, new Set());
|
||||
if (path !== null) {
|
||||
if (!this.externalStyleToComponentsMap.has(path)) {
|
||||
this.externalStyleToComponentsMap.set(path, new Set());
|
||||
}
|
||||
this.externalStyleToComponentsMap.get(path)!.add(component);
|
||||
}
|
||||
this.styleToComponentsMap.get(path)!.add(component);
|
||||
this.componentToStylesMap.get(component)!.add(styleResource);
|
||||
}
|
||||
|
||||
|
@ -98,10 +103,10 @@ export class ResourceRegistry {
|
|||
}
|
||||
|
||||
getComponentsWithStyle(styleUrl: AbsoluteFsPath): ReadonlySet<ClassDeclaration> {
|
||||
if (!this.styleToComponentsMap.has(styleUrl)) {
|
||||
if (!this.externalStyleToComponentsMap.has(styleUrl)) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return this.styleToComponentsMap.get(styleUrl)!;
|
||||
return this.externalStyleToComponentsMap.get(styleUrl)!;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue