Skip to content

Commit b5be18f

Browse files
alexeaglekara
authored andcommitted
feat(compiler-cli): add resource inlining to ngc (#22615)
When angularCompilerOptions { enableResourceInlining: true }, we replace all templateUrl and styleUrls properties in @component with template/styles PR Close #22615
1 parent 1e6cc42 commit b5be18f

File tree

6 files changed

+555
-8
lines changed

6 files changed

+555
-8
lines changed

packages/compiler-cli/src/transformers/api.ts

+10
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,16 @@ export interface CompilerOptions extends ts.CompilerOptions {
164164
*/
165165
enableSummariesForJit?: boolean;
166166

167+
/**
168+
* Whether to replace the `templateUrl` and `styleUrls` property in all
169+
* @Component decorators with inlined contents in `template` and `styles`
170+
* properties.
171+
* When enabled, the .js output of ngc will have no lazy-loaded `templateUrl`
172+
* or `styleUrl`s. Note that this requires that resources be available to
173+
* load statically at compile-time.
174+
*/
175+
enableResourceInlining?: boolean;
176+
167177
/**
168178
* Tells the compiler to generate definitions using the Render3 style code generation.
169179
* This option defaults to `false`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
import {MetadataObject, MetadataValue, isClassMetadata, isMetadataImportedSymbolReferenceExpression, isMetadataSymbolicCallExpression} from '../metadata/index';
12+
13+
import {MetadataTransformer, ValueTransform} from './metadata_cache';
14+
15+
export type ResourceLoader = {
16+
loadResource(path: string): Promise<string>| string;
17+
};
18+
19+
export class InlineResourcesMetadataTransformer implements MetadataTransformer {
20+
constructor(private host: ResourceLoader) {}
21+
22+
start(sourceFile: ts.SourceFile): ValueTransform|undefined {
23+
return (value: MetadataValue, node: ts.Node): MetadataValue => {
24+
if (isClassMetadata(value) && ts.isClassDeclaration(node) && value.decorators) {
25+
value.decorators.forEach(d => {
26+
if (isMetadataSymbolicCallExpression(d) &&
27+
isMetadataImportedSymbolReferenceExpression(d.expression) &&
28+
d.expression.module === '@angular/core' && d.expression.name === 'Component' &&
29+
d.arguments) {
30+
d.arguments = d.arguments.map(this.updateDecoratorMetadata.bind(this));
31+
}
32+
});
33+
}
34+
return value;
35+
};
36+
}
37+
38+
inlineResource(url: MetadataValue): string|undefined {
39+
if (typeof url === 'string') {
40+
const content = this.host.loadResource(url);
41+
if (typeof content === 'string') {
42+
return content;
43+
}
44+
}
45+
}
46+
47+
updateDecoratorMetadata(arg: MetadataObject): MetadataObject {
48+
if (arg['templateUrl']) {
49+
const template = this.inlineResource(arg['templateUrl']);
50+
if (template) {
51+
arg['template'] = template;
52+
delete arg.templateUrl;
53+
}
54+
}
55+
if (arg['styleUrls']) {
56+
const styleUrls = arg['styleUrls'];
57+
if (Array.isArray(styleUrls)) {
58+
let allStylesInlined = true;
59+
const newStyles = styleUrls.map(styleUrl => {
60+
const style = this.inlineResource(styleUrl);
61+
if (style) return style;
62+
allStylesInlined = false;
63+
return styleUrl;
64+
});
65+
if (allStylesInlined) {
66+
arg['styles'] = newStyles;
67+
delete arg.styleUrls;
68+
}
69+
}
70+
}
71+
72+
return arg;
73+
}
74+
}
75+
76+
export function getInlineResourcesTransformFactory(
77+
program: ts.Program, host: ResourceLoader): ts.TransformerFactory<ts.SourceFile> {
78+
return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => {
79+
const visitor: ts.Visitor = node => {
80+
// Components are always classes; skip any other node
81+
if (!ts.isClassDeclaration(node)) {
82+
return node;
83+
}
84+
85+
// Decorator case - before or without decorator downleveling
86+
// @Component()
87+
const newDecorators = ts.visitNodes(node.decorators, (node: ts.Decorator) => {
88+
if (isComponentDecorator(node, program.getTypeChecker())) {
89+
return updateDecorator(node, host);
90+
}
91+
return node;
92+
});
93+
94+
// Annotation case - after decorator downleveling
95+
// static decorators: {type: Function, args?: any[]}[]
96+
const newMembers = ts.visitNodes(
97+
node.members,
98+
(node: ts.ClassElement) => updateAnnotations(node, host, program.getTypeChecker()));
99+
100+
// Create a new AST subtree with our modifications
101+
return ts.updateClassDeclaration(
102+
node, newDecorators, node.modifiers, node.name, node.typeParameters,
103+
node.heritageClauses || [], newMembers);
104+
};
105+
106+
return ts.visitEachChild(sourceFile, visitor, context);
107+
};
108+
}
109+
110+
/**
111+
* Update a Decorator AST node to inline the resources
112+
* @param node the @Component decorator
113+
* @param host provides access to load resources
114+
*/
115+
function updateDecorator(node: ts.Decorator, host: ResourceLoader): ts.Decorator {
116+
if (!ts.isCallExpression(node.expression)) {
117+
// User will get an error somewhere else with bare @Component
118+
return node;
119+
}
120+
const expr = node.expression;
121+
const newArguments = updateComponentProperties(expr.arguments, host);
122+
return ts.updateDecorator(
123+
node, ts.updateCall(expr, expr.expression, expr.typeArguments, newArguments));
124+
}
125+
126+
/**
127+
* Update an Annotations AST node to inline the resources
128+
* @param node the static decorators property
129+
* @param host provides access to load resources
130+
* @param typeChecker provides access to symbol table
131+
*/
132+
function updateAnnotations(
133+
node: ts.ClassElement, host: ResourceLoader, typeChecker: ts.TypeChecker): ts.ClassElement {
134+
// Looking for a member of this shape:
135+
// PropertyDeclaration called decorators, with static modifier
136+
// Initializer is ArrayLiteralExpression
137+
// One element is the Component type, its initializer is the @angular/core Component symbol
138+
// One element is the component args, its initializer is the Component arguments to change
139+
// e.g.
140+
// static decorators: {type: Function, args?: any[]}[] =
141+
// [{
142+
// type: Component,
143+
// args: [{
144+
// templateUrl: './my.component.html',
145+
// styleUrls: ['./my.component.css'],
146+
// }],
147+
// }];
148+
if (!ts.isPropertyDeclaration(node) || // ts.ModifierFlags.Static &&
149+
!ts.isIdentifier(node.name) || node.name.text !== 'decorators' || !node.initializer ||
150+
!ts.isArrayLiteralExpression(node.initializer)) {
151+
return node;
152+
}
153+
154+
const newAnnotations = node.initializer.elements.map(annotation => {
155+
// No-op if there's a non-object-literal mixed in the decorators values
156+
if (!ts.isObjectLiteralExpression(annotation)) return annotation;
157+
158+
const decoratorType = annotation.properties.find(p => isIdentifierNamed(p, 'type'));
159+
160+
// No-op if there's no 'type' property, or if it's not initialized to the Component symbol
161+
if (!decoratorType || !ts.isPropertyAssignment(decoratorType) ||
162+
!ts.isIdentifier(decoratorType.initializer) ||
163+
!isComponentSymbol(decoratorType.initializer, typeChecker)) {
164+
return annotation;
165+
}
166+
167+
const newAnnotation = annotation.properties.map(prop => {
168+
// No-op if this isn't the 'args' property or if it's not initialized to an array
169+
if (!isIdentifierNamed(prop, 'args') || !ts.isPropertyAssignment(prop) ||
170+
!ts.isArrayLiteralExpression(prop.initializer))
171+
return prop;
172+
173+
const newDecoratorArgs = ts.updatePropertyAssignment(
174+
prop, prop.name,
175+
ts.createArrayLiteral(updateComponentProperties(prop.initializer.elements, host)));
176+
177+
return newDecoratorArgs;
178+
});
179+
180+
return ts.updateObjectLiteral(annotation, newAnnotation);
181+
});
182+
183+
return ts.updateProperty(
184+
node, node.decorators, node.modifiers, node.name, node.questionToken, node.type,
185+
ts.updateArrayLiteral(node.initializer, newAnnotations));
186+
}
187+
188+
function isIdentifierNamed(p: ts.ObjectLiteralElementLike, name: string): boolean {
189+
return !!p.name && ts.isIdentifier(p.name) && p.name.text === name;
190+
}
191+
192+
/**
193+
* Check that the node we are visiting is the actual Component decorator defined in @angular/core.
194+
*/
195+
function isComponentDecorator(node: ts.Decorator, typeChecker: ts.TypeChecker): boolean {
196+
if (!ts.isCallExpression(node.expression)) {
197+
return false;
198+
}
199+
const callExpr = node.expression;
200+
201+
let identifier: ts.Node;
202+
203+
if (ts.isIdentifier(callExpr.expression)) {
204+
identifier = callExpr.expression;
205+
} else {
206+
return false;
207+
}
208+
return isComponentSymbol(identifier, typeChecker);
209+
}
210+
211+
function isComponentSymbol(identifier: ts.Node, typeChecker: ts.TypeChecker) {
212+
// Only handle identifiers, not expressions
213+
if (!ts.isIdentifier(identifier)) return false;
214+
215+
// NOTE: resolver.getReferencedImportDeclaration would work as well but is internal
216+
const symbol = typeChecker.getSymbolAtLocation(identifier);
217+
218+
if (!symbol || !symbol.declarations || !symbol.declarations.length) {
219+
console.error(
220+
`Unable to resolve symbol '${identifier.text}' in the program, does it type-check?`);
221+
return false;
222+
}
223+
224+
const declaration = symbol.declarations[0];
225+
226+
if (!declaration || !ts.isImportSpecifier(declaration)) {
227+
return false;
228+
}
229+
230+
const name = (declaration.propertyName || declaration.name).text;
231+
// We know that parent pointers are set because we created the SourceFile ourselves.
232+
// The number of parent references here match the recursion depth at this point.
233+
const moduleId =
234+
(declaration.parent !.parent !.parent !.moduleSpecifier as ts.StringLiteral).text;
235+
return moduleId === '@angular/core' && name === 'Component';
236+
}
237+
238+
/**
239+
* For each property in the object literal, if it's templateUrl or styleUrls, replace it
240+
* with content.
241+
* @param node the arguments to @Component() or args property of decorators: [{type:Component}]
242+
* @param host provides access to the loadResource method of the host
243+
* @returns updated arguments
244+
*/
245+
function updateComponentProperties(
246+
args: ts.NodeArray<ts.Expression>, host: ResourceLoader): ts.NodeArray<ts.Expression> {
247+
if (args.length !== 1) {
248+
// User should have gotten a type-check error because @Component takes one argument
249+
return args;
250+
}
251+
const componentArg = args[0];
252+
if (!ts.isObjectLiteralExpression(componentArg)) {
253+
// User should have gotten a type-check error because @Component takes an object literal
254+
// argument
255+
return args;
256+
}
257+
const newArgument = ts.updateObjectLiteral(
258+
componentArg, ts.visitNodes(componentArg.properties, (node: ts.ObjectLiteralElementLike) => {
259+
if (!ts.isPropertyAssignment(node)) {
260+
// Error: unsupported
261+
return node;
262+
}
263+
264+
if (ts.isComputedPropertyName(node.name)) {
265+
// computed names are not supported
266+
return node;
267+
}
268+
269+
const name = node.name.text;
270+
switch (name) {
271+
case 'styleUrls':
272+
if (!ts.isArrayLiteralExpression(node.initializer)) {
273+
// Error: unsupported
274+
return node;
275+
}
276+
const styleUrls = node.initializer.elements;
277+
278+
return ts.updatePropertyAssignment(
279+
node, ts.createIdentifier('styles'),
280+
ts.createArrayLiteral(ts.visitNodes(styleUrls, (expr: ts.Expression) => {
281+
if (ts.isStringLiteral(expr)) {
282+
const styles = host.loadResource(expr.text);
283+
if (typeof styles === 'string') {
284+
return ts.createLiteral(styles);
285+
}
286+
}
287+
return expr;
288+
})));
289+
290+
291+
case 'templateUrl':
292+
if (ts.isStringLiteral(node.initializer)) {
293+
const template = host.loadResource(node.initializer.text);
294+
if (typeof template === 'string') {
295+
return ts.updatePropertyAssignment(
296+
node, ts.createIdentifier('template'), ts.createLiteral(template));
297+
}
298+
}
299+
return node;
300+
301+
default:
302+
return node;
303+
}
304+
}));
305+
return ts.createNodeArray<ts.Expression>([newArgument]);
306+
}

packages/compiler-cli/src/transformers/program.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metad
1717

1818
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
1919
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
20+
import {InlineResourcesMetadataTransformer, getInlineResourcesTransformFactory} from './inline_resources';
2021
import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './lower_expressions';
2122
import {MetadataCache, MetadataTransformer} from './metadata_cache';
2223
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
@@ -471,10 +472,16 @@ class AngularCompilerProgram implements Program {
471472
private calculateTransforms(
472473
genFiles: Map<string, GeneratedFile>|undefined, partialModules: PartialModule[]|undefined,
473474
customTransformers?: CustomTransformers): ts.CustomTransformers {
474-
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
475+
const beforeTs: Array<ts.TransformerFactory<ts.SourceFile>> = [];
476+
const metadataTransforms: MetadataTransformer[] = [];
477+
if (this.options.enableResourceInlining) {
478+
beforeTs.push(getInlineResourcesTransformFactory(this.tsProgram, this.hostAdapter));
479+
metadataTransforms.push(new InlineResourcesMetadataTransformer(this.hostAdapter));
480+
}
475481
if (!this.options.disableExpressionLowering) {
476482
beforeTs.push(
477483
getExpressionLoweringTransformFactory(this.loweringMetadataTransform, this.tsProgram));
484+
metadataTransforms.push(this.loweringMetadataTransform);
478485
}
479486
if (genFiles) {
480487
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
@@ -484,12 +491,14 @@ class AngularCompilerProgram implements Program {
484491

485492
// If we have partial modules, the cached metadata might be incorrect as it doesn't reflect
486493
// the partial module transforms.
487-
this.metadataCache = this.createMetadataCache(
488-
[this.loweringMetadataTransform, new PartialModuleMetadataTransformer(partialModules)]);
494+
metadataTransforms.push(new PartialModuleMetadataTransformer(partialModules));
489495
}
490496
if (customTransformers && customTransformers.beforeTs) {
491497
beforeTs.push(...customTransformers.beforeTs);
492498
}
499+
if (metadataTransforms.length > 0) {
500+
this.metadataCache = this.createMetadataCache(metadataTransforms);
501+
}
493502
const afterTs = customTransformers ? customTransformers.afterTs : undefined;
494503
return {before: beforeTs, after: afterTs};
495504
}

0 commit comments

Comments
 (0)