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), **{ return dict(tsc_wrapped_tsconfig(ctx, files, srcs, **kwargs), **{
"angularCompilerOptions": { "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, "generateCodeForLibraries": False,
"allowEmptyCodegenFiles": True, "allowEmptyCodegenFiles": True,
"enableSummariesForJit": True, "enableSummariesForJit": True,

View File

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

View File

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