Skip to content

Commit

Permalink
fix(compiler-cli): resolve resource URLs before loading them under en…
Browse files Browse the repository at this point in the history
…ableResourceInlining

Also turn on the feature for Bazel ng_module rules
  • Loading branch information
alexeagle committed Mar 9, 2018
1 parent b5be18f commit a425ab6
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 27 deletions.
4 changes: 4 additions & 0 deletions packages/bazel/src/ng_module.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
68 changes: 43 additions & 25 deletions packages/compiler-cli/src/transformers/inline_resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,64 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ResourceLoader} from '@angular/compiler';
import * as ts from 'typescript';

import {MetadataObject, MetadataValue, isClassMetadata, isMetadataImportedSymbolReferenceExpression, isMetadataSymbolicCallExpression} from '../metadata/index';

import {MetadataTransformer, ValueTransform} from './metadata_cache';

export type ResourceLoader = {
/** A subset of members from AotCompilerHost */
export type ResourcesHost = {
resourceNameToFileName(resourceName: string, containingFileName: string): string | null;
loadResource(path: string): Promise<string>| string;
};

function getResourceLoader(host: ResourcesHost, containingFileName: string) {
class LoaderImpl extends ResourceLoader {
get(url: string): Promise<string>|string {
const fileName = host.resourceNameToFileName(url, containingFileName);
if (fileName) {
return host.loadResource(fileName);
}
return Promise.reject('Can only inline static resources');
}
}
return new LoaderImpl();
}

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 => {
if (isMetadataSymbolicCallExpression(d) &&
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));
}
});
}
return value;
};
}

inlineResource(url: MetadataValue): string|undefined {
if (typeof url === 'string') {
const content = this.host.loadResource(url);
if (typeof content === 'string') {
return content;
updateDecoratorMetadata(loader: ResourceLoader, arg: MetadataObject): MetadataObject {
function inlineResource(url: MetadataValue): string|undefined {
if (typeof url === 'string') {
const content = loader.get(url);
if (typeof content === 'string') {
return content;
}
}
}
}

updateDecoratorMetadata(arg: MetadataObject): MetadataObject {
if (arg['templateUrl']) {
const template = this.inlineResource(arg['templateUrl']);
const template = inlineResource(arg['templateUrl']);
if (template) {
arg['template'] = template;
delete arg.templateUrl;
Expand All @@ -57,7 +74,7 @@ export class InlineResourcesMetadataTransformer implements MetadataTransformer {
if (Array.isArray(styleUrls)) {
let allStylesInlined = true;
const newStyles = styleUrls.map(styleUrl => {
const style = this.inlineResource(styleUrl);
const style = inlineResource(styleUrl);
if (style) return style;
allStylesInlined = false;
return styleUrl;
Expand All @@ -74,8 +91,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)) {
Expand All @@ -86,7 +104,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;
});
Expand All @@ -95,7 +113,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(
Expand All @@ -110,27 +128,27 @@ 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: ResourceLoader): 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));
}

/**
* 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: ResourceLoader, typeChecker: ts.TypeChecker): ts.ClassElement {
// Looking for a member of this shape:
// PropertyDeclaration called decorators, with static modifier
// Initializer is ArrayLiteralExpression
Expand Down Expand Up @@ -172,7 +190,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;
});
Expand Down Expand Up @@ -239,11 +257,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: ResourceLoader): ts.NodeArray<ts.Expression> {
if (args.length !== 1) {
// User should have gotten a type-check error because @Component takes one argument
return args;
Expand Down Expand Up @@ -279,7 +297,7 @@ function updateComponentProperties(
node, ts.createIdentifier('styles'),
ts.createArrayLiteral(ts.visitNodes(styleUrls, (expr: ts.Expression) => {
if (ts.isStringLiteral(expr)) {
const styles = host.loadResource(expr.text);
const styles = loader.get(expr.text);
if (typeof styles === 'string') {
return ts.createLiteral(styles);
}
Expand All @@ -290,7 +308,7 @@ function updateComponentProperties(

case 'templateUrl':
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(
node, ts.createIdentifier('template'), ts.createLiteral(template));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit a425ab6

Please sign in to comment.