feat(compiler-cli): automatically lower lambda expressions in metadata
This commit is contained in:
parent
67dff7bd5d
commit
b6c4af6495
|
@ -25,8 +25,9 @@ export interface CompilerHostContext extends ts.ModuleResolutionHost {
|
|||
assumeFileExists(fileName: string): void;
|
||||
}
|
||||
|
||||
export interface MetadataProvider { getMetadata(source: ts.SourceFile): ModuleMetadata|undefined; }
|
||||
|
||||
export class CompilerHost implements AotCompilerHost {
|
||||
protected metadataCollector = new MetadataCollector();
|
||||
private isGenDirChildOfRootDir: boolean;
|
||||
protected basePath: string;
|
||||
private genDir: string;
|
||||
|
@ -39,7 +40,8 @@ export class CompilerHost implements AotCompilerHost {
|
|||
|
||||
constructor(
|
||||
protected program: ts.Program, protected options: AngularCompilerOptions,
|
||||
protected context: CompilerHostContext, collectorOptions?: CollectorOptions) {
|
||||
protected context: CompilerHostContext, collectorOptions?: CollectorOptions,
|
||||
protected metadataProvider: MetadataProvider = new MetadataCollector()) {
|
||||
// normalize the path so that it never ends with '/'.
|
||||
this.basePath = path.normalize(path.join(this.options.basePath !, '.')).replace(/\\/g, '/');
|
||||
this.genDir = path.normalize(path.join(this.options.genDir !, '.')).replace(/\\/g, '/');
|
||||
|
@ -206,7 +208,7 @@ export class CompilerHost implements AotCompilerHost {
|
|||
}
|
||||
|
||||
const sf = this.getSourceFile(filePath);
|
||||
const metadata = this.metadataCollector.getMetadata(sf);
|
||||
const metadata = this.metadataProvider.getMetadata(sf);
|
||||
return metadata ? [metadata] : [];
|
||||
}
|
||||
|
||||
|
@ -245,7 +247,7 @@ export class CompilerHost implements AotCompilerHost {
|
|||
v3Metadata.metadata[prop] = v1Metadata.metadata[prop];
|
||||
}
|
||||
|
||||
const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath));
|
||||
const exports = this.metadataProvider.getMetadata(this.getSourceFile(dtsFilePath));
|
||||
if (exports) {
|
||||
for (let prop in exports.metadata) {
|
||||
if (!v3Metadata.metadata[prop]) {
|
||||
|
|
|
@ -132,7 +132,7 @@ export class PathMappedCompilerHost extends CompilerHost {
|
|||
} else {
|
||||
const sf = this.getSourceFile(rootedPath);
|
||||
sf.fileName = sf.fileName;
|
||||
const metadata = this.metadataCollector.getMetadata(sf);
|
||||
const metadata = this.metadataProvider.getMetadata(sf);
|
||||
return metadata ? [metadata] : [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,6 +91,10 @@ export interface CompilerOptions extends ts.CompilerOptions {
|
|||
|
||||
// Whether to enable support for <template> and the template attribute (true by default)
|
||||
enableLegacyTemplate?: boolean;
|
||||
|
||||
// Whether to enable lowering expressions lambdas and expressions in a reference value
|
||||
// position.
|
||||
disableExpressionLowering?: boolean;
|
||||
}
|
||||
|
||||
export interface ModuleFilenameResolver {
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* @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 {CompilerHost, CompilerOptions, Program} from './api';
|
||||
|
|
|
@ -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 {CollectorOptions, MetadataCollector, MetadataValue, ModuleMetadata} from '@angular/tsc-wrapped';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export interface LoweringRequest {
|
||||
kind: ts.SyntaxKind;
|
||||
location: number;
|
||||
end: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type RequestLocationMap = Map<number, LoweringRequest>;
|
||||
|
||||
interface Declaration {
|
||||
name: string;
|
||||
node: ts.Node;
|
||||
}
|
||||
|
||||
interface DeclarationInsert {
|
||||
declarations: Declaration[];
|
||||
priorTo: ts.Node;
|
||||
}
|
||||
|
||||
function toMap<T, K>(items: T[], select: (item: T) => K): Map<K, T> {
|
||||
return new Map(items.map<[K, T]>(i => [select(i), i]));
|
||||
}
|
||||
|
||||
function transformSourceFile(
|
||||
sourceFile: ts.SourceFile, requests: RequestLocationMap,
|
||||
context: ts.TransformationContext): ts.SourceFile {
|
||||
const inserts: DeclarationInsert[] = [];
|
||||
|
||||
// Calculate the range of intersting locations. The transform will only visit nodes in this
|
||||
// range to improve the performance on large files.
|
||||
const locations = Array.from(requests.keys());
|
||||
const min = Math.min(...locations);
|
||||
const max = Math.max(...locations);
|
||||
|
||||
function visitSourceFile(sourceFile: ts.SourceFile): ts.SourceFile {
|
||||
function topLevelStatement(node: ts.Node): ts.Node {
|
||||
const declarations: Declaration[] = [];
|
||||
|
||||
function visitNode(node: ts.Node): ts.Node {
|
||||
const nodeRequest = requests.get(node.pos);
|
||||
if (nodeRequest && nodeRequest.kind == node.kind && nodeRequest.end == node.end) {
|
||||
// This node is requested to be rewritten as a reference to the exported name.
|
||||
// Record that the node needs to be moved to an exported variable with the given name
|
||||
const name = nodeRequest.name;
|
||||
declarations.push({name, node});
|
||||
return ts.createIdentifier(name);
|
||||
}
|
||||
if (node.pos <= max && node.end >= min) return ts.visitEachChild(node, visitNode, context);
|
||||
return node;
|
||||
}
|
||||
|
||||
const result = ts.visitEachChild(node, visitNode, context);
|
||||
|
||||
if (declarations.length) {
|
||||
inserts.push({priorTo: result, declarations})
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const traversedSource = ts.visitEachChild(sourceFile, topLevelStatement, context);
|
||||
if (inserts.length) {
|
||||
// Insert the declarations before the rewritten statement that references them.
|
||||
const insertMap = toMap(inserts, i => i.priorTo);
|
||||
const newStatements: ts.Statement[] = [...traversedSource.statements];
|
||||
for (let i = newStatements.length; i >= 0; i--) {
|
||||
const statement = newStatements[i];
|
||||
const insert = insertMap.get(statement);
|
||||
if (insert) {
|
||||
const declarations = insert.declarations.map(
|
||||
i => ts.createVariableDeclaration(
|
||||
i.name, /* type */ undefined, i.node as ts.Expression));
|
||||
const statement = ts.createVariableStatement(
|
||||
/* modifiers */ undefined,
|
||||
ts.createVariableDeclarationList(declarations, ts.NodeFlags.Const));
|
||||
newStatements.splice(i, 0, statement);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert an exports clause to export the declarations
|
||||
newStatements.push(ts.createExportDeclaration(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ undefined,
|
||||
ts.createNamedExports(
|
||||
inserts
|
||||
.reduce(
|
||||
(accumulator, insert) => [...accumulator, ...insert.declarations],
|
||||
[] as Declaration[])
|
||||
.map(
|
||||
declaration => ts.createExportSpecifier(
|
||||
/* propertyName */ undefined, declaration.name)))));
|
||||
return ts.updateSourceFileNode(traversedSource, newStatements);
|
||||
}
|
||||
return traversedSource;
|
||||
}
|
||||
|
||||
return visitSourceFile(sourceFile);
|
||||
}
|
||||
|
||||
export function getExpressionLoweringTransformFactory(requestsMap: RequestsMap):
|
||||
(context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
|
||||
// Return the factory
|
||||
return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => {
|
||||
const requests = requestsMap.getRequests(sourceFile);
|
||||
if (requests && requests.size) {
|
||||
return transformSourceFile(sourceFile, requests, context)
|
||||
}
|
||||
return sourceFile;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RequestsMap { getRequests(sourceFile: ts.SourceFile): RequestLocationMap; }
|
||||
|
||||
interface MetadataAndLoweringRequests {
|
||||
metadata: ModuleMetadata|undefined;
|
||||
requests: RequestLocationMap;
|
||||
}
|
||||
|
||||
export class LowerMetadataCache implements RequestsMap {
|
||||
private collector: MetadataCollector;
|
||||
private metadataCache = new Map<string, MetadataAndLoweringRequests>();
|
||||
|
||||
constructor(options: CollectorOptions, private strict?: boolean) {
|
||||
this.collector = new MetadataCollector(options);
|
||||
}
|
||||
|
||||
getMetadata(sourceFile: ts.SourceFile): ModuleMetadata|undefined {
|
||||
return this.ensureMetadataAndRequests(sourceFile).metadata;
|
||||
}
|
||||
|
||||
getRequests(sourceFile: ts.SourceFile): RequestLocationMap {
|
||||
return this.ensureMetadataAndRequests(sourceFile).requests;
|
||||
}
|
||||
|
||||
private ensureMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests {
|
||||
let result = this.metadataCache.get(sourceFile.fileName);
|
||||
if (!result) {
|
||||
result = this.getMetadataAndRequests(sourceFile);
|
||||
this.metadataCache.set(sourceFile.fileName, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private getMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests {
|
||||
let identNumber = 0;
|
||||
const freshIdent = () => '\u0275' + identNumber++;
|
||||
const requests = new Map<number, LoweringRequest>();
|
||||
const replaceNode =
|
||||
(node: ts.Node) => {
|
||||
const name = freshIdent();
|
||||
requests.set(node.pos, {name, kind: node.kind, location: node.pos, end: node.end});
|
||||
return {__symbolic: 'reference', name};
|
||||
}
|
||||
|
||||
const substituteExpression = (value: MetadataValue, node: ts.Node): MetadataValue => {
|
||||
if (node.kind === ts.SyntaxKind.ArrowFunction ||
|
||||
node.kind === ts.SyntaxKind.FunctionExpression) {
|
||||
return replaceNode(node);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const metadata = this.collector.getMetadata(sourceFile, this.strict, substituteExpression);
|
||||
|
||||
return {metadata, requests};
|
||||
}
|
||||
}
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* @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 path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import {TypeChecker} from '../diagnostics/check_types';
|
|||
|
||||
import {CompilerHost, CompilerOptions, DiagnosticCategory} from './api';
|
||||
import {Diagnostic, EmitFlags, Program} from './api';
|
||||
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
|
||||
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
||||
|
||||
const GENERATED_FILES = /\.ngfactory\.js$|\.ngstyle\.js$|\.ngsummary\.js$/;
|
||||
|
@ -35,7 +36,7 @@ class AngularCompilerProgram implements Program {
|
|||
private aotCompilerHost: AotCompilerHost;
|
||||
private compiler: AotCompiler;
|
||||
private srcNames: string[];
|
||||
private collector: MetadataCollector;
|
||||
private metadataCache: LowerMetadataCache;
|
||||
// Lazily initialized fields
|
||||
private _analyzedModules: NgAnalyzedModules|undefined;
|
||||
private _structuralDiagnostics: Diagnostic[] = [];
|
||||
|
@ -55,13 +56,14 @@ class AngularCompilerProgram implements Program {
|
|||
|
||||
this.tsProgram = ts.createProgram(rootNames, options, host, this.oldTsProgram);
|
||||
this.srcNames = this.tsProgram.getSourceFiles().map(sf => sf.fileName);
|
||||
this.aotCompilerHost = new AotCompilerHost(this.tsProgram, options, host);
|
||||
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
|
||||
this.aotCompilerHost = new AotCompilerHost(
|
||||
this.tsProgram, options, host, /* collectorOptions */ undefined, this.metadataCache);
|
||||
if (host.readResource) {
|
||||
this.aotCompilerHost.loadResource = host.readResource.bind(host);
|
||||
}
|
||||
const {compiler} = createAotCompiler(this.aotCompilerHost, options);
|
||||
this.compiler = compiler;
|
||||
this.collector = new MetadataCollector({quotedNames: true});
|
||||
}
|
||||
|
||||
// Program implementation
|
||||
|
@ -118,11 +120,9 @@ class AngularCompilerProgram implements Program {
|
|||
const emitMap = new Map<string, string>();
|
||||
const result = this.programWithStubs.emit(
|
||||
/* targetSourceFile */ undefined,
|
||||
createWriteFileCallback(emitFlags, this.host, this.collector, this.options, emitMap),
|
||||
cancellationToken, (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, {
|
||||
after: this.options.skipTemplateCodegen ? [] : [getAngularEmitterTransformFactory(
|
||||
this.generatedFiles)]
|
||||
});
|
||||
createWriteFileCallback(emitFlags, this.host, this.metadataCache, emitMap),
|
||||
cancellationToken, (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS,
|
||||
this.calculateTransforms());
|
||||
|
||||
this.generatedFiles.forEach(file => {
|
||||
if (file.source && file.source.length && SUMMARY_JSON_FILES.test(file.genFileUrl)) {
|
||||
|
@ -184,6 +184,21 @@ class AngularCompilerProgram implements Program {
|
|||
return this.generatedFiles && this._generatedFileDiagnostics !;
|
||||
}
|
||||
|
||||
private calculateTransforms(): ts.CustomTransformers {
|
||||
const before: ts.TransformerFactory<ts.SourceFile>[] = [];
|
||||
const after: ts.TransformerFactory<ts.SourceFile>[] = [];
|
||||
if (!this.options.disableExpressionLowering) {
|
||||
before.push(getExpressionLoweringTransformFactory(this.metadataCache));
|
||||
}
|
||||
if (!this.options.skipTemplateCodegen) {
|
||||
after.push(getAngularEmitterTransformFactory(this.generatedFiles));
|
||||
}
|
||||
const result: ts.CustomTransformers = {};
|
||||
if (before.length) result.before = before;
|
||||
if (after.length) result.after = after;
|
||||
return result;
|
||||
}
|
||||
|
||||
private catchAnalysisError(e: any): NgAnalyzedModules {
|
||||
if (isSyntaxError(e)) {
|
||||
const parserErrors = getParseErrors(e);
|
||||
|
@ -257,8 +272,7 @@ export function createProgram(
|
|||
}
|
||||
|
||||
function writeMetadata(
|
||||
emitFilePath: string, sourceFile: ts.SourceFile, collector: MetadataCollector,
|
||||
ngOptions: CompilerOptions) {
|
||||
emitFilePath: string, sourceFile: ts.SourceFile, metadataCache: LowerMetadataCache) {
|
||||
if (/\.js$/.test(emitFilePath)) {
|
||||
const path = emitFilePath.replace(/\.js$/, '.metadata.json');
|
||||
|
||||
|
@ -271,7 +285,7 @@ function writeMetadata(
|
|||
collectableFile = (collectableFile as any).original;
|
||||
}
|
||||
|
||||
const metadata = collector.getMetadata(collectableFile, !!ngOptions.strictMetadataEmit);
|
||||
const metadata = metadataCache.getMetadata(collectableFile);
|
||||
if (metadata) {
|
||||
const metadataText = JSON.stringify([metadata]);
|
||||
writeFileSync(path, metadataText, {encoding: 'utf-8'});
|
||||
|
@ -280,8 +294,8 @@ function writeMetadata(
|
|||
}
|
||||
|
||||
function createWriteFileCallback(
|
||||
emitFlags: EmitFlags, host: ts.CompilerHost, collector: MetadataCollector,
|
||||
ngOptions: CompilerOptions, emitMap: Map<string, string>) {
|
||||
emitFlags: EmitFlags, host: ts.CompilerHost, metadataCache: LowerMetadataCache,
|
||||
emitMap: Map<string, string>) {
|
||||
const withMetadata =
|
||||
(fileName: string, data: string, writeByteOrderMark: boolean,
|
||||
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
|
||||
|
@ -291,7 +305,7 @@ function createWriteFileCallback(
|
|||
}
|
||||
if (!generatedFile && sourceFiles && sourceFiles.length == 1) {
|
||||
emitMap.set(sourceFiles[0].fileName, fileName);
|
||||
writeMetadata(fileName, sourceFiles[0], collector, ngOptions);
|
||||
writeMetadata(fileName, sourceFiles[0], metadataCache);
|
||||
}
|
||||
};
|
||||
const withoutMetadata =
|
||||
|
|
|
@ -308,7 +308,6 @@ describe('ngc command-line', () => {
|
|||
|
||||
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]);
|
||||
expect(exitCode).toEqual(0);
|
||||
|
||||
expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true);
|
||||
expect(fs.existsSync(path.resolve(
|
||||
outDir, 'node_modules', '@angular', 'core', 'src',
|
||||
|
@ -316,6 +315,123 @@ describe('ngc command-line', () => {
|
|||
.toBe(true);
|
||||
});
|
||||
|
||||
describe('expression lowering', () => {
|
||||
beforeEach(() => {
|
||||
writeConfig(`{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"files": ["mymodule.ts"]
|
||||
}`);
|
||||
});
|
||||
|
||||
function compile(): number {
|
||||
const errors: string[] = [];
|
||||
const result = main(['-p', path.join(basePath, 'tsconfig.json')], s => errors.push(s));
|
||||
expect(errors).toEqual([]);
|
||||
return result;
|
||||
}
|
||||
|
||||
it('should be able to lower a lambda expression in a provider', () => {
|
||||
write('mymodule.ts', `
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
class Foo {}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
providers: [{provide: 'someToken', useFactory: () => new Foo()}]
|
||||
})
|
||||
export class MyModule {}
|
||||
`);
|
||||
expect(compile()).toEqual(0);
|
||||
|
||||
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
||||
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
||||
expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }');
|
||||
expect(mymoduleSource).toContain('export { ɵ0');
|
||||
|
||||
const mymodulefactory = path.resolve(outDir, 'mymodule.ngfactory.js');
|
||||
const mymodulefactorySource = fs.readFileSync(mymodulefactory, 'utf8');
|
||||
expect(mymodulefactorySource).toContain('"someToken", i1.ɵ0');
|
||||
});
|
||||
|
||||
it('should be able to lower a function expression in a provider', () => {
|
||||
write('mymodule.ts', `
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
class Foo {}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
providers: [{provide: 'someToken', useFactory: function() {return new Foo();}}]
|
||||
})
|
||||
export class MyModule {}
|
||||
`);
|
||||
expect(compile()).toEqual(0);
|
||||
|
||||
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
||||
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
||||
expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }');
|
||||
expect(mymoduleSource).toContain('export { ɵ0');
|
||||
|
||||
const mymodulefactory = path.resolve(outDir, 'mymodule.ngfactory.js');
|
||||
const mymodulefactorySource = fs.readFileSync(mymodulefactory, 'utf8');
|
||||
expect(mymodulefactorySource).toContain('"someToken", i1.ɵ0');
|
||||
});
|
||||
|
||||
it('should able to lower multiple expressions', () => {
|
||||
write('mymodule.ts', `
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
class Foo {}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
providers: [
|
||||
{provide: 'someToken', useFactory: () => new Foo()},
|
||||
{provide: 'someToken', useFactory: () => new Foo()},
|
||||
{provide: 'someToken', useFactory: () => new Foo()},
|
||||
{provide: 'someToken', useFactory: () => new Foo()}
|
||||
]
|
||||
})
|
||||
export class MyModule {}
|
||||
`);
|
||||
expect(compile()).toEqual(0);
|
||||
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
||||
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
||||
expect(mymoduleSource).toContain('ɵ0 = function () { return new Foo(); }');
|
||||
expect(mymoduleSource).toContain('ɵ1 = function () { return new Foo(); }');
|
||||
expect(mymoduleSource).toContain('ɵ2 = function () { return new Foo(); }');
|
||||
expect(mymoduleSource).toContain('ɵ3 = function () { return new Foo(); }');
|
||||
expect(mymoduleSource).toContain('export { ɵ0, ɵ1, ɵ2, ɵ3');
|
||||
});
|
||||
|
||||
it('should be able to lower an indirect expression', () => {
|
||||
write('mymodule.ts', `
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
class Foo {}
|
||||
|
||||
const factory = () => new Foo();
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
providers: [{provide: 'someToken', useFactory: factory}]
|
||||
})
|
||||
export class MyModule {}
|
||||
`);
|
||||
expect(compile()).toEqual(0);
|
||||
|
||||
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
||||
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
||||
expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }');
|
||||
expect(mymoduleSource).toContain('export { ɵ0');
|
||||
});
|
||||
});
|
||||
|
||||
const shouldExist = (fileName: string) => {
|
||||
if (!fs.existsSync(path.resolve(outDir, fileName))) {
|
||||
throw new Error(`Expected ${fileName} to be emitted (outDir: ${outDir})`);
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* @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 {LoweringRequest, RequestLocationMap, getExpressionLoweringTransformFactory} from '../../src/transformers/lower_expressions';
|
||||
import {Directory, MockAotContext, MockCompilerHost} from '../mocks';
|
||||
|
||||
describe('Expression lowering', () => {
|
||||
it('should be able to lower a simple expression', () => {
|
||||
expect(convert('const a = 1 +◊b: 2◊;')).toBe('const b = 2; const a = 1 + b; export { b };');
|
||||
});
|
||||
|
||||
it('should be able to lower an expression in a decorator', () => {
|
||||
expect(convert(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
provider: [{provide: 'someToken', useFactory:◊l: () => null◊}]
|
||||
})
|
||||
class MyClass {}
|
||||
`)).toContain('const l = () => null; exports.l = l;');
|
||||
});
|
||||
});
|
||||
|
||||
function convert(annotatedSource: string) {
|
||||
const annotations: {start: number, length: number, name: string}[] = [];
|
||||
let adjustment = 0;
|
||||
const unannotatedSource = annotatedSource.replace(
|
||||
/◊([a-zA-Z]+):(.*)◊/g,
|
||||
(text: string, name: string, source: string, index: number): string => {
|
||||
annotations.push({start: index + adjustment, length: source.length, name});
|
||||
adjustment -= text.length - source.length;
|
||||
return source;
|
||||
});
|
||||
|
||||
const baseFileName = 'someFile';
|
||||
const moduleName = '/' + baseFileName;
|
||||
const fileName = moduleName + '.ts';
|
||||
const context = new MockAotContext('/', {[baseFileName + '.ts']: unannotatedSource});
|
||||
const host = new MockCompilerHost(context);
|
||||
|
||||
const sourceFile = ts.createSourceFile(
|
||||
fileName, unannotatedSource, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
||||
const requests = new Map<number, LoweringRequest>();
|
||||
|
||||
for (const annotation of annotations) {
|
||||
const node = findNode(sourceFile, annotation.start, annotation.length);
|
||||
expect(node).toBeDefined();
|
||||
if (node) {
|
||||
const location = node.pos;
|
||||
requests.set(location, {name: annotation.name, kind: node.kind, location, end: node.end});
|
||||
}
|
||||
}
|
||||
|
||||
const program = ts.createProgram(
|
||||
[fileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host);
|
||||
const moduleSourceFile = program.getSourceFile(fileName);
|
||||
const transformers: ts.CustomTransformers = {
|
||||
before: [getExpressionLoweringTransformFactory({
|
||||
getRequests(sourceFile: ts.SourceFile): RequestLocationMap{
|
||||
if (sourceFile.fileName == moduleSourceFile.fileName) {
|
||||
return requests;
|
||||
} else {return new Map();}
|
||||
}
|
||||
})]
|
||||
};
|
||||
let result: string = '';
|
||||
const emitResult = program.emit(
|
||||
moduleSourceFile, (emittedFileName, data, writeByteOrderMark, onError, sourceFiles) => {
|
||||
if (fileName.startsWith(moduleName)) {
|
||||
result = data;
|
||||
}
|
||||
}, undefined, undefined, transformers);
|
||||
return normalizeResult(result);
|
||||
};
|
||||
|
||||
function findNode(node: ts.Node, start: number, length: number): ts.Node|undefined {
|
||||
function find(node: ts.Node): ts.Node|undefined {
|
||||
if (node.getFullStart() == start && node.getEnd() == start + length) {
|
||||
return node;
|
||||
}
|
||||
if (node.getFullStart() <= start && node.getEnd() >= start + length) {
|
||||
return ts.forEachChild(node, find);
|
||||
}
|
||||
}
|
||||
return ts.forEachChild(node, find);
|
||||
}
|
||||
|
||||
function normalizeResult(result: string): string {
|
||||
// Remove TypeScript prefixes
|
||||
// Remove new lines
|
||||
// Squish adjacent spaces
|
||||
// Remove prefix and postfix spaces
|
||||
return result.replace('"use strict";', ' ')
|
||||
.replace('exports.__esModule = true;', ' ')
|
||||
.replace('Object.defineProperty(exports, "__esModule", { value: true });', ' ')
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/ +/g, ' ')
|
||||
.replace(/^ /g, '')
|
||||
.replace(/ $/g, '');
|
||||
}
|
Loading…
Reference in New Issue