feat(ivy): support templateUrl for ngtsc (#24704)

This commit adds support for templateUrl in component templates within
ngtsc. The compilation pipeline is split into sync and async versions,
where asynchronous compilation invokes a special preanalyze() phase of
analysis. The preanalyze() phase can optionally return a Promise which
will delay compilation until it resolves.

A ResourceLoader interface is used to resolve templateUrls to template
strings and can return results either synchronously or asynchronously.
During sync compilation it is an error if the ResourceLoader returns a
Promise.

Two ResourceLoader implementations are provided. One uses 'fs' to read
resources directly from disk and is chosen if the CompilerHost doesn't
provide a readResource method. The other wraps the readResource method
from CompilerHost if it's provided.

PR Close #24704
This commit is contained in:
Alex Rickabaugh
2018-06-26 15:01:09 -07:00
committed by Miško Hevery
parent 0922228024
commit 0c3738a780
8 changed files with 278 additions and 66 deletions

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
export {ResourceLoader} from './src/api';
export {ComponentDecoratorHandler} from './src/component';
export {DirectiveDecoratorHandler} from './src/directive';
export {InjectableDecoratorHandler} from './src/injectable';

View File

@ -0,0 +1,12 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export interface ResourceLoader {
preload?(url: string): Promise<void>|undefined;
load(url: string): string;
}

View File

@ -7,12 +7,14 @@
*/
import {ConstantPool, Expression, R3ComponentMetadata, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {Decorator, ReflectionHost} from '../../host';
import {reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {ResourceLoader} from './api';
import {extractDirectiveMetadata} from './directive';
import {SelectorScopeRegistry} from './selector_scope';
import {isAngularCore} from './util';
@ -25,21 +27,35 @@ const EMPTY_MAP = new Map<string, Expression>();
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean,
private resourceLoader: ResourceLoader) {}
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator)));
}
preanalyze(node: ts.ClassDeclaration, decorator: Decorator): Promise<void>|undefined {
const meta = this._resolveLiteral(decorator);
const component = reflectObjectLiteral(meta);
if (this.resourceLoader.preload !== undefined && component.has('templateUrl')) {
const templateUrl = staticallyResolve(component.get('templateUrl') !, this.checker);
if (typeof templateUrl !== 'string') {
throw new Error(`templateUrl should be a string`);
}
const url = path.posix.resolve(path.dirname(node.getSourceFile().fileName), templateUrl);
return this.resourceLoader.preload(url);
}
return undefined;
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3ComponentMetadata> {
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @Component decorator`);
}
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}
const meta = this._resolveLiteral(decorator);
this.literalCache.delete(decorator);
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
// on it.
@ -55,14 +71,23 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// Next, read the `@Component`-specific fields.
const component = reflectObjectLiteral(meta);
// Resolve and parse the template.
if (!component.has('template')) {
throw new Error(`For now, components must directly have a template.`);
}
const templateExpr = component.get('template') !;
const templateStr = staticallyResolve(templateExpr, this.checker);
if (typeof templateStr !== 'string') {
throw new Error(`Template must statically resolve to a string: ${node.name!.text}`);
let templateStr: string|null = null;
if (component.has('templateUrl')) {
const templateUrl = staticallyResolve(component.get('templateUrl') !, this.checker);
if (typeof templateUrl !== 'string') {
throw new Error(`templateUrl should be a string`);
}
const url = path.posix.resolve(path.dirname(node.getSourceFile().fileName), templateUrl);
templateStr = this.resourceLoader.load(url);
} else if (component.has('template')) {
const templateExpr = component.get('template') !;
const resolvedTemplate = staticallyResolve(templateExpr, this.checker);
if (typeof resolvedTemplate !== 'string') {
throw new Error(`Template must statically resolve to a string: ${node.name!.text}`);
}
templateStr = resolvedTemplate;
} else {
throw new Error(`Component has no template or templateUrl`);
}
let preserveWhitespaces: boolean = false;
@ -123,4 +148,20 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
type: res.type,
};
}
private _resolveLiteral(decorator: Decorator): ts.ObjectLiteralExpression {
if (this.literalCache.has(decorator)) {
return this.literalCache.get(decorator) !;
}
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @Component decorator`);
}
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
}
this.literalCache.set(decorator, meta);
return meta;
}
}