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
This commit is contained in:
parent
1e6cc42a01
commit
b5be18f405
@ -164,6 +164,16 @@ export interface CompilerOptions extends ts.CompilerOptions {
|
|||||||
*/
|
*/
|
||||||
enableSummariesForJit?: boolean;
|
enableSummariesForJit?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to replace the `templateUrl` and `styleUrls` property in all
|
||||||
|
* @Component decorators with inlined contents in `template` and `styles`
|
||||||
|
* properties.
|
||||||
|
* When enabled, the .js output of ngc will have no lazy-loaded `templateUrl`
|
||||||
|
* or `styleUrl`s. Note that this requires that resources be available to
|
||||||
|
* load statically at compile-time.
|
||||||
|
*/
|
||||||
|
enableResourceInlining?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells the compiler to generate definitions using the Render3 style code generation.
|
* Tells the compiler to generate definitions using the Render3 style code generation.
|
||||||
* This option defaults to `false`.
|
* This option defaults to `false`.
|
||||||
|
306
packages/compiler-cli/src/transformers/inline_resources.ts
Normal file
306
packages/compiler-cli/src/transformers/inline_resources.ts
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {MetadataObject, MetadataValue, isClassMetadata, isMetadataImportedSymbolReferenceExpression, isMetadataSymbolicCallExpression} from '../metadata/index';
|
||||||
|
|
||||||
|
import {MetadataTransformer, ValueTransform} from './metadata_cache';
|
||||||
|
|
||||||
|
export type ResourceLoader = {
|
||||||
|
loadResource(path: string): Promise<string>| string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class InlineResourcesMetadataTransformer implements MetadataTransformer {
|
||||||
|
constructor(private host: ResourceLoader) {}
|
||||||
|
|
||||||
|
start(sourceFile: ts.SourceFile): ValueTransform|undefined {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (arg['templateUrl']) {
|
||||||
|
const template = this.inlineResource(arg['templateUrl']);
|
||||||
|
if (template) {
|
||||||
|
arg['template'] = template;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInlineResourcesTransformFactory(
|
||||||
|
program: ts.Program, host: ResourceLoader): ts.TransformerFactory<ts.SourceFile> {
|
||||||
|
return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => {
|
||||||
|
const visitor: ts.Visitor = node => {
|
||||||
|
// Components are always classes; skip any other node
|
||||||
|
if (!ts.isClassDeclaration(node)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decorator case - before or without decorator downleveling
|
||||||
|
// @Component()
|
||||||
|
const newDecorators = ts.visitNodes(node.decorators, (node: ts.Decorator) => {
|
||||||
|
if (isComponentDecorator(node, program.getTypeChecker())) {
|
||||||
|
return updateDecorator(node, host);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Annotation case - after decorator downleveling
|
||||||
|
// static decorators: {type: Function, args?: any[]}[]
|
||||||
|
const newMembers = ts.visitNodes(
|
||||||
|
node.members,
|
||||||
|
(node: ts.ClassElement) => updateAnnotations(node, host, program.getTypeChecker()));
|
||||||
|
|
||||||
|
// Create a new AST subtree with our modifications
|
||||||
|
return ts.updateClassDeclaration(
|
||||||
|
node, newDecorators, node.modifiers, node.name, node.typeParameters,
|
||||||
|
node.heritageClauses || [], newMembers);
|
||||||
|
};
|
||||||
|
|
||||||
|
return ts.visitEachChild(sourceFile, visitor, context);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a Decorator AST node to inline the resources
|
||||||
|
* @param node the @Component decorator
|
||||||
|
* @param host provides access to load resources
|
||||||
|
*/
|
||||||
|
function updateDecorator(node: ts.Decorator, host: 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);
|
||||||
|
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 typeChecker provides access to symbol table
|
||||||
|
*/
|
||||||
|
function updateAnnotations(
|
||||||
|
node: ts.ClassElement, host: ResourceLoader, typeChecker: ts.TypeChecker): ts.ClassElement {
|
||||||
|
// Looking for a member of this shape:
|
||||||
|
// PropertyDeclaration called decorators, with static modifier
|
||||||
|
// Initializer is ArrayLiteralExpression
|
||||||
|
// One element is the Component type, its initializer is the @angular/core Component symbol
|
||||||
|
// One element is the component args, its initializer is the Component arguments to change
|
||||||
|
// e.g.
|
||||||
|
// static decorators: {type: Function, args?: any[]}[] =
|
||||||
|
// [{
|
||||||
|
// type: Component,
|
||||||
|
// args: [{
|
||||||
|
// templateUrl: './my.component.html',
|
||||||
|
// styleUrls: ['./my.component.css'],
|
||||||
|
// }],
|
||||||
|
// }];
|
||||||
|
if (!ts.isPropertyDeclaration(node) || // ts.ModifierFlags.Static &&
|
||||||
|
!ts.isIdentifier(node.name) || node.name.text !== 'decorators' || !node.initializer ||
|
||||||
|
!ts.isArrayLiteralExpression(node.initializer)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newAnnotations = node.initializer.elements.map(annotation => {
|
||||||
|
// No-op if there's a non-object-literal mixed in the decorators values
|
||||||
|
if (!ts.isObjectLiteralExpression(annotation)) return annotation;
|
||||||
|
|
||||||
|
const decoratorType = annotation.properties.find(p => isIdentifierNamed(p, 'type'));
|
||||||
|
|
||||||
|
// No-op if there's no 'type' property, or if it's not initialized to the Component symbol
|
||||||
|
if (!decoratorType || !ts.isPropertyAssignment(decoratorType) ||
|
||||||
|
!ts.isIdentifier(decoratorType.initializer) ||
|
||||||
|
!isComponentSymbol(decoratorType.initializer, typeChecker)) {
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newAnnotation = annotation.properties.map(prop => {
|
||||||
|
// No-op if this isn't the 'args' property or if it's not initialized to an array
|
||||||
|
if (!isIdentifierNamed(prop, 'args') || !ts.isPropertyAssignment(prop) ||
|
||||||
|
!ts.isArrayLiteralExpression(prop.initializer))
|
||||||
|
return prop;
|
||||||
|
|
||||||
|
const newDecoratorArgs = ts.updatePropertyAssignment(
|
||||||
|
prop, prop.name,
|
||||||
|
ts.createArrayLiteral(updateComponentProperties(prop.initializer.elements, host)));
|
||||||
|
|
||||||
|
return newDecoratorArgs;
|
||||||
|
});
|
||||||
|
|
||||||
|
return ts.updateObjectLiteral(annotation, newAnnotation);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ts.updateProperty(
|
||||||
|
node, node.decorators, node.modifiers, node.name, node.questionToken, node.type,
|
||||||
|
ts.updateArrayLiteral(node.initializer, newAnnotations));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIdentifierNamed(p: ts.ObjectLiteralElementLike, name: string): boolean {
|
||||||
|
return !!p.name && ts.isIdentifier(p.name) && p.name.text === name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the node we are visiting is the actual Component decorator defined in @angular/core.
|
||||||
|
*/
|
||||||
|
function isComponentDecorator(node: ts.Decorator, typeChecker: ts.TypeChecker): boolean {
|
||||||
|
if (!ts.isCallExpression(node.expression)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const callExpr = node.expression;
|
||||||
|
|
||||||
|
let identifier: ts.Node;
|
||||||
|
|
||||||
|
if (ts.isIdentifier(callExpr.expression)) {
|
||||||
|
identifier = callExpr.expression;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isComponentSymbol(identifier, typeChecker);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isComponentSymbol(identifier: ts.Node, typeChecker: ts.TypeChecker) {
|
||||||
|
// Only handle identifiers, not expressions
|
||||||
|
if (!ts.isIdentifier(identifier)) return false;
|
||||||
|
|
||||||
|
// NOTE: resolver.getReferencedImportDeclaration would work as well but is internal
|
||||||
|
const symbol = typeChecker.getSymbolAtLocation(identifier);
|
||||||
|
|
||||||
|
if (!symbol || !symbol.declarations || !symbol.declarations.length) {
|
||||||
|
console.error(
|
||||||
|
`Unable to resolve symbol '${identifier.text}' in the program, does it type-check?`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const declaration = symbol.declarations[0];
|
||||||
|
|
||||||
|
if (!declaration || !ts.isImportSpecifier(declaration)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = (declaration.propertyName || declaration.name).text;
|
||||||
|
// We know that parent pointers are set because we created the SourceFile ourselves.
|
||||||
|
// The number of parent references here match the recursion depth at this point.
|
||||||
|
const moduleId =
|
||||||
|
(declaration.parent !.parent !.parent !.moduleSpecifier as ts.StringLiteral).text;
|
||||||
|
return moduleId === '@angular/core' && name === 'Component';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @returns updated arguments
|
||||||
|
*/
|
||||||
|
function updateComponentProperties(
|
||||||
|
args: ts.NodeArray<ts.Expression>, host: ResourceLoader): ts.NodeArray<ts.Expression> {
|
||||||
|
if (args.length !== 1) {
|
||||||
|
// User should have gotten a type-check error because @Component takes one argument
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
const componentArg = args[0];
|
||||||
|
if (!ts.isObjectLiteralExpression(componentArg)) {
|
||||||
|
// User should have gotten a type-check error because @Component takes an object literal
|
||||||
|
// argument
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
const newArgument = ts.updateObjectLiteral(
|
||||||
|
componentArg, ts.visitNodes(componentArg.properties, (node: ts.ObjectLiteralElementLike) => {
|
||||||
|
if (!ts.isPropertyAssignment(node)) {
|
||||||
|
// Error: unsupported
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts.isComputedPropertyName(node.name)) {
|
||||||
|
// computed names are not supported
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = node.name.text;
|
||||||
|
switch (name) {
|
||||||
|
case 'styleUrls':
|
||||||
|
if (!ts.isArrayLiteralExpression(node.initializer)) {
|
||||||
|
// Error: unsupported
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const styleUrls = node.initializer.elements;
|
||||||
|
|
||||||
|
return ts.updatePropertyAssignment(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
})));
|
||||||
|
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return ts.createNodeArray<ts.Expression>([newArgument]);
|
||||||
|
}
|
@ -17,6 +17,7 @@ import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metad
|
|||||||
|
|
||||||
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
|
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
|
||||||
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
|
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
|
||||||
|
import {InlineResourcesMetadataTransformer, getInlineResourcesTransformFactory} from './inline_resources';
|
||||||
import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './lower_expressions';
|
import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './lower_expressions';
|
||||||
import {MetadataCache, MetadataTransformer} from './metadata_cache';
|
import {MetadataCache, MetadataTransformer} from './metadata_cache';
|
||||||
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
||||||
@ -471,10 +472,16 @@ class AngularCompilerProgram implements Program {
|
|||||||
private calculateTransforms(
|
private calculateTransforms(
|
||||||
genFiles: Map<string, GeneratedFile>|undefined, partialModules: PartialModule[]|undefined,
|
genFiles: Map<string, GeneratedFile>|undefined, partialModules: PartialModule[]|undefined,
|
||||||
customTransformers?: CustomTransformers): ts.CustomTransformers {
|
customTransformers?: CustomTransformers): ts.CustomTransformers {
|
||||||
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
|
const beforeTs: Array<ts.TransformerFactory<ts.SourceFile>> = [];
|
||||||
|
const metadataTransforms: MetadataTransformer[] = [];
|
||||||
|
if (this.options.enableResourceInlining) {
|
||||||
|
beforeTs.push(getInlineResourcesTransformFactory(this.tsProgram, this.hostAdapter));
|
||||||
|
metadataTransforms.push(new InlineResourcesMetadataTransformer(this.hostAdapter));
|
||||||
|
}
|
||||||
if (!this.options.disableExpressionLowering) {
|
if (!this.options.disableExpressionLowering) {
|
||||||
beforeTs.push(
|
beforeTs.push(
|
||||||
getExpressionLoweringTransformFactory(this.loweringMetadataTransform, this.tsProgram));
|
getExpressionLoweringTransformFactory(this.loweringMetadataTransform, this.tsProgram));
|
||||||
|
metadataTransforms.push(this.loweringMetadataTransform);
|
||||||
}
|
}
|
||||||
if (genFiles) {
|
if (genFiles) {
|
||||||
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
|
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
|
||||||
@ -484,12 +491,14 @@ class AngularCompilerProgram implements Program {
|
|||||||
|
|
||||||
// If we have partial modules, the cached metadata might be incorrect as it doesn't reflect
|
// If we have partial modules, the cached metadata might be incorrect as it doesn't reflect
|
||||||
// the partial module transforms.
|
// the partial module transforms.
|
||||||
this.metadataCache = this.createMetadataCache(
|
metadataTransforms.push(new PartialModuleMetadataTransformer(partialModules));
|
||||||
[this.loweringMetadataTransform, new PartialModuleMetadataTransformer(partialModules)]);
|
|
||||||
}
|
}
|
||||||
if (customTransformers && customTransformers.beforeTs) {
|
if (customTransformers && customTransformers.beforeTs) {
|
||||||
beforeTs.push(...customTransformers.beforeTs);
|
beforeTs.push(...customTransformers.beforeTs);
|
||||||
}
|
}
|
||||||
|
if (metadataTransforms.length > 0) {
|
||||||
|
this.metadataCache = this.createMetadataCache(metadataTransforms);
|
||||||
|
}
|
||||||
const afterTs = customTransformers ? customTransformers.afterTs : undefined;
|
const afterTs = customTransformers ? customTransformers.afterTs : undefined;
|
||||||
return {before: beforeTs, after: afterTs};
|
return {before: beforeTs, after: afterTs};
|
||||||
}
|
}
|
||||||
|
@ -1244,6 +1244,51 @@ describe('ngc transformer command-line', () => {
|
|||||||
expect(main(['-p', path.join(basePath, 'app', 'tsconfig-app.json')], errorSpy)).toBe(0);
|
expect(main(['-p', path.join(basePath, 'app', 'tsconfig-app.json')], errorSpy)).toBe(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('enableResourceInlining', () => {
|
||||||
|
it('should inline templateUrl and styleUrl in JS and metadata', () => {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"files": ["mymodule.ts"],
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableResourceInlining": true
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
write('my.component.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
@Component({
|
||||||
|
templateUrl: './my.component.html',
|
||||||
|
styleUrls: ['./my.component.css'],
|
||||||
|
})
|
||||||
|
export class MyComp {}
|
||||||
|
`);
|
||||||
|
write('my.component.html', `<h1>Some template content</h1>`);
|
||||||
|
write('my.component.css', `h1 {color: blue}`);
|
||||||
|
write('mymodule.ts', `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {MyComp} from './my.component';
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComp]})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath]);
|
||||||
|
expect(exitCode).toEqual(0);
|
||||||
|
outDir = path.resolve(basePath, 'built');
|
||||||
|
const outputJs = fs.readFileSync(path.join(outDir, 'my.component.js'), {encoding: 'utf-8'});
|
||||||
|
expect(outputJs).not.toContain('templateUrl');
|
||||||
|
expect(outputJs).not.toContain('styleUrls');
|
||||||
|
expect(outputJs).toContain('Some template content');
|
||||||
|
expect(outputJs).toContain('color: blue');
|
||||||
|
|
||||||
|
const outputMetadata =
|
||||||
|
fs.readFileSync(path.join(outDir, 'my.component.metadata.json'), {encoding: 'utf-8'});
|
||||||
|
expect(outputMetadata).not.toContain('templateUrl');
|
||||||
|
expect(outputMetadata).not.toContain('styleUrls');
|
||||||
|
expect(outputMetadata).toContain('Some template content');
|
||||||
|
expect(outputMetadata).toContain('color: blue');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('expression lowering', () => {
|
describe('expression lowering', () => {
|
||||||
@ -1972,12 +2017,12 @@ describe('ngc transformer command-line', () => {
|
|||||||
it('on simple services', () => {
|
it('on simple services', () => {
|
||||||
const source = compileService(`
|
const source = compileService(`
|
||||||
import {Injectable, NgModule} from '@angular/core';
|
import {Injectable, NgModule} from '@angular/core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Service {
|
export class Service {
|
||||||
constructor(public param: string) {}
|
constructor(public param: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
providers: [{provide: Service, useValue: new Service('test')}],
|
providers: [{provide: Service, useValue: new Service('test')}],
|
||||||
})
|
})
|
||||||
@ -1988,7 +2033,7 @@ describe('ngc transformer command-line', () => {
|
|||||||
it('on a service with a base class service', () => {
|
it('on a service with a base class service', () => {
|
||||||
const source = compileService(`
|
const source = compileService(`
|
||||||
import {Injectable, NgModule} from '@angular/core';
|
import {Injectable, NgModule} from '@angular/core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Dep {}
|
export class Dep {}
|
||||||
|
|
||||||
@ -1997,7 +2042,7 @@ describe('ngc transformer command-line', () => {
|
|||||||
}
|
}
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Service extends Base {}
|
export class Service extends Base {}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
providers: [Service],
|
providers: [Service],
|
||||||
})
|
})
|
||||||
|
177
packages/compiler-cli/test/transformers/inline_resources_spec.ts
Normal file
177
packages/compiler-cli/test/transformers/inline_resources_spec.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {MetadataCollector, isClassMetadata} from '../../src/metadata/index';
|
||||||
|
import {InlineResourcesMetadataTransformer, getInlineResourcesTransformFactory} from '../../src/transformers/inline_resources';
|
||||||
|
import {MetadataCache} from '../../src/transformers/metadata_cache';
|
||||||
|
import {MockAotContext, MockCompilerHost} from '../mocks';
|
||||||
|
|
||||||
|
describe('inline resources transformer', () => {
|
||||||
|
describe('decorator input', () => {
|
||||||
|
describe('should not touch unrecognized decorators', () => {
|
||||||
|
it('Not from @angular/core', () => {
|
||||||
|
expect(convert(`declare const Component: Function;
|
||||||
|
@Component({templateUrl: './thing.html'}) class Foo {}`))
|
||||||
|
.toContain('templateUrl');
|
||||||
|
});
|
||||||
|
it('missing @ sign', () => {
|
||||||
|
expect(convert(`import {Component} from '@angular/core';
|
||||||
|
Component({templateUrl: './thing.html'}) class Foo {}`))
|
||||||
|
.toContain('templateUrl');
|
||||||
|
});
|
||||||
|
it('too many arguments to @Component', () => {
|
||||||
|
expect(convert(`import {Component} from '@angular/core';
|
||||||
|
@Component(1, {templateUrl: './thing.html'}) class Foo {}`))
|
||||||
|
.toContain('templateUrl');
|
||||||
|
});
|
||||||
|
it('wrong argument type to @Component', () => {
|
||||||
|
expect(convert(`import {Component} from '@angular/core';
|
||||||
|
@Component([{templateUrl: './thing.html'}]) class Foo {}`))
|
||||||
|
.toContain('templateUrl');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace templateUrl', () => {
|
||||||
|
const actual = convert(`import {Component} from '@angular/core';
|
||||||
|
@Component({
|
||||||
|
templateUrl: './thing.html',
|
||||||
|
otherProp: 3,
|
||||||
|
}) export class Foo {}`);
|
||||||
|
expect(actual).not.toContain('templateUrl:');
|
||||||
|
expect(actual.replace(/\s+/g, ' '))
|
||||||
|
.toContain(
|
||||||
|
'Foo = __decorate([ core_1.Component({ template: "Some template", otherProp: 3, }) ], Foo)');
|
||||||
|
});
|
||||||
|
it('should replace styleUrls', () => {
|
||||||
|
const actual = convert(`import {Component} from '@angular/core';
|
||||||
|
@Component({
|
||||||
|
styleUrls: ['./thing1.css', './thing2.css'],
|
||||||
|
})
|
||||||
|
export class Foo {}`);
|
||||||
|
expect(actual).not.toContain('styleUrls:');
|
||||||
|
expect(actual).toContain('styles: [".some_style {}", ".some_other_style {}"]');
|
||||||
|
});
|
||||||
|
it('should handle empty styleUrls', () => {
|
||||||
|
const actual = convert(`import {Component} from '@angular/core';
|
||||||
|
@Component({styleUrls: []}) export class Foo {}`);
|
||||||
|
expect(actual).not.toContain('styleUrls:');
|
||||||
|
expect(actual).toContain('styles: []');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('annotation input', () => {
|
||||||
|
it('should replace templateUrl', () => {
|
||||||
|
const actual = convert(`import {Component} from '@angular/core';
|
||||||
|
declare const NotComponent: Function;
|
||||||
|
|
||||||
|
export class Foo {
|
||||||
|
static decorators: {type: Function, args?: any[]}[] = [
|
||||||
|
{
|
||||||
|
type: NotComponent,
|
||||||
|
args: [],
|
||||||
|
},{
|
||||||
|
type: Component,
|
||||||
|
args: [{
|
||||||
|
templateUrl: './thing.html'
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(actual).not.toContain('templateUrl:');
|
||||||
|
expect(actual.replace(/\s+/g, ' '))
|
||||||
|
.toMatch(
|
||||||
|
/Foo\.decorators = [{ .*type: core_1\.Component, args: [{ template: "Some template" }]/);
|
||||||
|
});
|
||||||
|
it('should replace styleUrls', () => {
|
||||||
|
const actual = convert(`import {Component} from '@angular/core';
|
||||||
|
declare const NotComponent: Function;
|
||||||
|
|
||||||
|
export class Foo {
|
||||||
|
static decorators: {type: Function, args?: any[]}[] = [{
|
||||||
|
type: Component,
|
||||||
|
args: [{
|
||||||
|
styleUrls: ['./thing1.css', './thing2.css'],
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(actual).not.toContain('styleUrls:');
|
||||||
|
expect(actual.replace(/\s+/g, ' '))
|
||||||
|
.toMatch(
|
||||||
|
/Foo\.decorators = [{ .*type: core_1\.Component, args: [{ style: "Some template" }]/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('metadata transformer', () => {
|
||||||
|
it('should transform decorators', () => {
|
||||||
|
const source = `import {Component} from '@angular/core';
|
||||||
|
@Component({
|
||||||
|
templateUrl: './thing.html',
|
||||||
|
styleUrls: ['./thing1.css', './thing2.css'],
|
||||||
|
})
|
||||||
|
export class Foo {}
|
||||||
|
`;
|
||||||
|
const sourceFile = ts.createSourceFile(
|
||||||
|
'someFile.ts', source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
||||||
|
const cache = new MetadataCache(
|
||||||
|
new MetadataCollector(), /* strict */ true,
|
||||||
|
[new InlineResourcesMetadataTransformer({loadResource})]);
|
||||||
|
const metadata = cache.getMetadata(sourceFile);
|
||||||
|
expect(metadata).toBeDefined('Expected metadata from test source file');
|
||||||
|
if (metadata) {
|
||||||
|
const classData = metadata.metadata['Foo'];
|
||||||
|
expect(classData && isClassMetadata(classData))
|
||||||
|
.toBeDefined(`Expected metadata to contain data for Foo`);
|
||||||
|
if (classData && isClassMetadata(classData)) {
|
||||||
|
expect(JSON.stringify(classData)).not.toContain('templateUrl');
|
||||||
|
expect(JSON.stringify(classData)).toContain('"template":"Some template"');
|
||||||
|
expect(JSON.stringify(classData)).not.toContain('styleUrls');
|
||||||
|
expect(JSON.stringify(classData))
|
||||||
|
.toContain('"styles":[".some_style {}",".some_other_style {}"]');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadResource(path: string): Promise<string>|string {
|
||||||
|
if (path === './thing.html') return 'Some template';
|
||||||
|
if (path === './thing1.css') return '.some_style {}';
|
||||||
|
if (path === './thing2.css') return '.some_other_style {}';
|
||||||
|
throw new Error('No fake data for path ' + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convert(source: string) {
|
||||||
|
const baseFileName = 'someFile';
|
||||||
|
const moduleName = '/' + baseFileName;
|
||||||
|
const fileName = moduleName + '.ts';
|
||||||
|
const context = new MockAotContext('/', {[baseFileName + '.ts']: source});
|
||||||
|
const host = new MockCompilerHost(context);
|
||||||
|
|
||||||
|
const sourceFile =
|
||||||
|
ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
||||||
|
const program = ts.createProgram(
|
||||||
|
[fileName], {
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
|
target: ts.ScriptTarget.ES2017,
|
||||||
|
},
|
||||||
|
host);
|
||||||
|
const moduleSourceFile = program.getSourceFile(fileName);
|
||||||
|
const transformers: ts.CustomTransformers = {
|
||||||
|
before: [getInlineResourcesTransformFactory(program, {loadResource})]
|
||||||
|
};
|
||||||
|
let result = '';
|
||||||
|
const emitResult = program.emit(
|
||||||
|
moduleSourceFile, (emittedFileName, data, writeByteOrderMark, onError, sourceFiles) => {
|
||||||
|
if (fileName.startsWith(moduleName)) {
|
||||||
|
result = data;
|
||||||
|
}
|
||||||
|
}, undefined, undefined, transformers);
|
||||||
|
return result;
|
||||||
|
}
|
@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"inlineSourceMap": true,
|
"inlineSourceMap": true,
|
||||||
"lib": ["es5", "dom", "es2015.promise", "es2015.collection", "es2015.iterable"],
|
"lib": ["es5", "dom", "es2015.promise", "es2015.collection", "es2015.iterable", "es2015.core"],
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user