fix(compiler-cli): resolve resource URLs before loading them under enableResourceInlining (#22688)

Also turn on the feature for Bazel ng_module rules

PR Close #22688
This commit is contained in:
Alex Eagle 2018-03-09 11:54:40 -08:00 committed by Kara Erickson
parent fa451bcd19
commit 123efba388
3 changed files with 61 additions and 51 deletions

View File

@ -88,6 +88,10 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs):
return dict(tsc_wrapped_tsconfig(ctx, files, srcs, **kwargs), **{
"angularCompilerOptions": {
# Always assume that resources can be loaded statically at build time
# TODO(alexeagle): if someone has a legitimate use case for dynamic
# template loading, maybe we need to make this configurable.
"enableResourceInlining": True,
"generateCodeForLibraries": False,
"allowEmptyCodegenFiles": True,
"enableSummariesForJit": True,

View File

@ -12,14 +12,41 @@ import {MetadataObject, MetadataValue, isClassMetadata, isMetadataImportedSymbol
import {MetadataTransformer, ValueTransform} from './metadata_cache';
export type ResourceLoader = {
const PRECONDITIONS_TEXT =
'angularCompilerOptions.enableResourceInlining requires all resources to be statically resolvable.';
/** A subset of members from AotCompilerHost */
export type ResourcesHost = {
resourceNameToFileName(resourceName: string, containingFileName: string): string | null;
loadResource(path: string): Promise<string>| string;
};
export type StaticResourceLoader = {
get(url: string | MetadataValue): string;
};
function getResourceLoader(host: ResourcesHost, containingFileName: string): StaticResourceLoader {
return {
get(url: string | MetadataValue): string{
if (typeof url !== 'string') {
throw new Error('templateUrl and stylesUrl must be string literals. ' + PRECONDITIONS_TEXT);
} const fileName = host.resourceNameToFileName(url, containingFileName);
if (fileName) {
const content = host.loadResource(fileName);
if (typeof content !== 'string') {
throw new Error('Cannot handle async resource. ' + PRECONDITIONS_TEXT);
}
return content;
} throw new Error(`Failed to resolve ${url} from ${containingFileName}. ${PRECONDITIONS_TEXT}`);
}
};
}
export class InlineResourcesMetadataTransformer implements MetadataTransformer {
constructor(private host: ResourceLoader) {}
constructor(private host: ResourcesHost) {}
start(sourceFile: ts.SourceFile): ValueTransform|undefined {
const loader = getResourceLoader(this.host, sourceFile.fileName);
return (value: MetadataValue, node: ts.Node): MetadataValue => {
if (isClassMetadata(value) && ts.isClassDeclaration(node) && value.decorators) {
value.decorators.forEach(d => {
@ -27,7 +54,7 @@ export class InlineResourcesMetadataTransformer implements MetadataTransformer {
isMetadataImportedSymbolReferenceExpression(d.expression) &&
d.expression.module === '@angular/core' && d.expression.name === 'Component' &&
d.arguments) {
d.arguments = d.arguments.map(this.updateDecoratorMetadata.bind(this));
d.arguments = d.arguments.map(this.updateDecoratorMetadata.bind(this, loader));
}
});
}
@ -35,37 +62,16 @@ export class InlineResourcesMetadataTransformer implements MetadataTransformer {
};
}
inlineResource(url: MetadataValue): string|undefined {
if (typeof url === 'string') {
const content = this.host.loadResource(url);
if (typeof content === 'string') {
return content;
}
}
}
updateDecoratorMetadata(arg: MetadataObject): MetadataObject {
updateDecoratorMetadata(loader: StaticResourceLoader, arg: MetadataObject): MetadataObject {
if (arg['templateUrl']) {
const template = this.inlineResource(arg['templateUrl']);
if (template) {
arg['template'] = template;
delete arg.templateUrl;
}
arg['template'] = loader.get(arg['templateUrl']);
delete arg.templateUrl;
}
if (arg['styleUrls']) {
const styleUrls = arg['styleUrls'];
if (Array.isArray(styleUrls)) {
let allStylesInlined = true;
const newStyles = styleUrls.map(styleUrl => {
const style = this.inlineResource(styleUrl);
if (style) return style;
allStylesInlined = false;
return styleUrl;
});
if (allStylesInlined) {
arg['styles'] = newStyles;
delete arg.styleUrls;
}
arg['styles'] = styleUrls.map(styleUrl => loader.get(styleUrl));
delete arg.styleUrls;
}
}
@ -74,8 +80,9 @@ export class InlineResourcesMetadataTransformer implements MetadataTransformer {
}
export function getInlineResourcesTransformFactory(
program: ts.Program, host: ResourceLoader): ts.TransformerFactory<ts.SourceFile> {
program: ts.Program, host: ResourcesHost): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => {
const loader = getResourceLoader(host, sourceFile.fileName);
const visitor: ts.Visitor = node => {
// Components are always classes; skip any other node
if (!ts.isClassDeclaration(node)) {
@ -86,7 +93,7 @@ export function getInlineResourcesTransformFactory(
// @Component()
const newDecorators = ts.visitNodes(node.decorators, (node: ts.Decorator) => {
if (isComponentDecorator(node, program.getTypeChecker())) {
return updateDecorator(node, host);
return updateDecorator(node, loader);
}
return node;
});
@ -95,7 +102,7 @@ export function getInlineResourcesTransformFactory(
// static decorators: {type: Function, args?: any[]}[]
const newMembers = ts.visitNodes(
node.members,
(node: ts.ClassElement) => updateAnnotations(node, host, program.getTypeChecker()));
(node: ts.ClassElement) => updateAnnotations(node, loader, program.getTypeChecker()));
// Create a new AST subtree with our modifications
return ts.updateClassDeclaration(
@ -110,15 +117,15 @@ export function getInlineResourcesTransformFactory(
/**
* Update a Decorator AST node to inline the resources
* @param node the @Component decorator
* @param host provides access to load resources
* @param loader provides access to load resources
*/
function updateDecorator(node: ts.Decorator, host: ResourceLoader): ts.Decorator {
function updateDecorator(node: ts.Decorator, loader: StaticResourceLoader): ts.Decorator {
if (!ts.isCallExpression(node.expression)) {
// User will get an error somewhere else with bare @Component
return node;
}
const expr = node.expression;
const newArguments = updateComponentProperties(expr.arguments, host);
const newArguments = updateComponentProperties(expr.arguments, loader);
return ts.updateDecorator(
node, ts.updateCall(expr, expr.expression, expr.typeArguments, newArguments));
}
@ -126,11 +133,12 @@ function updateDecorator(node: ts.Decorator, host: ResourceLoader): ts.Decorator
/**
* Update an Annotations AST node to inline the resources
* @param node the static decorators property
* @param host provides access to load resources
* @param loader provides access to load resources
* @param typeChecker provides access to symbol table
*/
function updateAnnotations(
node: ts.ClassElement, host: ResourceLoader, typeChecker: ts.TypeChecker): ts.ClassElement {
node: ts.ClassElement, loader: StaticResourceLoader,
typeChecker: ts.TypeChecker): ts.ClassElement {
// Looking for a member of this shape:
// PropertyDeclaration called decorators, with static modifier
// Initializer is ArrayLiteralExpression
@ -172,7 +180,7 @@ function updateAnnotations(
const newDecoratorArgs = ts.updatePropertyAssignment(
prop, prop.name,
ts.createArrayLiteral(updateComponentProperties(prop.initializer.elements, host)));
ts.createArrayLiteral(updateComponentProperties(prop.initializer.elements, loader)));
return newDecoratorArgs;
});
@ -239,11 +247,11 @@ function isComponentSymbol(identifier: ts.Node, typeChecker: ts.TypeChecker) {
* For each property in the object literal, if it's templateUrl or styleUrls, replace it
* with content.
* @param node the arguments to @Component() or args property of decorators: [{type:Component}]
* @param host provides access to the loadResource method of the host
* @param loader provides access to the loadResource method of the host
* @returns updated arguments
*/
function updateComponentProperties(
args: ts.NodeArray<ts.Expression>, host: ResourceLoader): ts.NodeArray<ts.Expression> {
args: ts.NodeArray<ts.Expression>, loader: StaticResourceLoader): ts.NodeArray<ts.Expression> {
if (args.length !== 1) {
// User should have gotten a type-check error because @Component takes one argument
return args;
@ -279,10 +287,8 @@ function updateComponentProperties(
node, ts.createIdentifier('styles'),
ts.createArrayLiteral(ts.visitNodes(styleUrls, (expr: ts.Expression) => {
if (ts.isStringLiteral(expr)) {
const styles = host.loadResource(expr.text);
if (typeof styles === 'string') {
return ts.createLiteral(styles);
}
const styles = loader.get(expr.text);
return ts.createLiteral(styles);
}
return expr;
})));
@ -290,11 +296,9 @@ function updateComponentProperties(
case 'templateUrl':
if (ts.isStringLiteral(node.initializer)) {
const template = host.loadResource(node.initializer.text);
if (typeof template === 'string') {
return ts.updatePropertyAssignment(
node, ts.createIdentifier('template'), ts.createLiteral(template));
}
const template = loader.get(node.initializer.text);
return ts.updatePropertyAssignment(
node, ts.createIdentifier('template'), ts.createLiteral(template));
}
return node;

View File

@ -122,7 +122,8 @@ describe('metadata transformer', () => {
'someFile.ts', source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
const cache = new MetadataCache(
new MetadataCollector(), /* strict */ true,
[new InlineResourcesMetadataTransformer({loadResource})]);
[new InlineResourcesMetadataTransformer(
{loadResource, resourceNameToFileName: (u: string) => u})]);
const metadata = cache.getMetadata(sourceFile);
expect(metadata).toBeDefined('Expected metadata from test source file');
if (metadata) {
@ -164,7 +165,8 @@ function convert(source: string) {
host);
const moduleSourceFile = program.getSourceFile(fileName);
const transformers: ts.CustomTransformers = {
before: [getInlineResourcesTransformFactory(program, {loadResource})]
before: [getInlineResourcesTransformFactory(
program, {loadResource, resourceNameToFileName: (u: string) => u})]
};
let result = '';
const emitResult = program.emit(