feat(compiler): implement "enableIvy" compiler option (#21427)

The "enableIvy" compiler option is the initial implementation
of the Render3 (or Ivy) code generation. This commit enables
generation generating "Hello, World" (example in the test)
but not much else. It is currenly only useful for internal Ivy
testing as Ivy is in development.

PR Close #21427
This commit is contained in:
Chuck Jazdzewski
2017-11-20 10:21:17 -08:00
committed by Miško Hevery
parent ce8b5877e2
commit 64d16dee02
27 changed files with 1484 additions and 47 deletions

View File

@ -38,6 +38,8 @@ export function isNgDiagnostic(diagnostic: any): diagnostic is Diagnostic {
}
export interface CompilerOptions extends ts.CompilerOptions {
// NOTE: These comments and aio/content/guides/aot-compiler.md should be kept in sync.
// Write statistics about compilation (e.g. total time, ...)
// Note: this is the --diagnostics command line option from TS (which is @internal
// on ts.CompilerOptions interface).
@ -159,6 +161,17 @@ export interface CompilerOptions extends ts.CompilerOptions {
*/
enableSummariesForJit?: boolean;
/**
* Tells the compiler to generate definitions using the Render3 style code generation.
* This option defaults to `false`.
*
* Not all features are supported with this option enabled. It is only supported
* for experimentation and testing of Render3 style code generation.
*
* @experimental
*/
enableIvy?: boolean;
/** @internal */
collectAllErrors?: boolean;
}

View File

@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, CompileIdentifierMetadata, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StaticSymbol, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassMethod, ClassStmt, CommaExpr, CommentStmt, CompileIdentifierMetadata, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StaticSymbol, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {error} from './util';
export interface Node { sourceSpan: ParseSourceSpan|null; }
@ -50,6 +51,98 @@ export class TypeScriptNodeEmitter {
}
}
/**
* Update the given source file to include the changes specified in module.
*
* The module parameter is treated as a partial module meaning that the statements are added to
* the module instead of replacing the module. Also, any classes are treated as partial classes
* and the included members are added to the class with the same name instead of a new class
* being created.
*/
export function updateSourceFile(
sourceFile: ts.SourceFile, module: PartialModule,
context: ts.TransformationContext): [ts.SourceFile, Map<ts.Node, Node>] {
const converter = new _NodeEmitterVisitor();
const prefixStatements = module.statements.filter(statement => !(statement instanceof ClassStmt));
const classes =
module.statements.filter(statement => statement instanceof ClassStmt) as ClassStmt[];
const classMap = new Map(
classes.map<[string, ClassStmt]>(classStatement => [classStatement.name, classStatement]));
const classNames = new Set(classes.map(classStatement => classStatement.name));
const prefix =
<ts.Statement[]>prefixStatements.map(statement => statement.visitStatement(converter, null));
// Add static methods to all the classes referenced in module.
let newStatements = sourceFile.statements.map(node => {
if (node.kind == ts.SyntaxKind.ClassDeclaration) {
const classDeclaration = node as ts.ClassDeclaration;
const name = classDeclaration.name;
if (name) {
const classStatement = classMap.get(name.text);
if (classStatement) {
classNames.delete(name.text);
const classMemberHolder =
converter.visitDeclareClassStmt(classStatement) as ts.ClassDeclaration;
const newMethods =
classMemberHolder.members.filter(member => member.kind !== ts.SyntaxKind.Constructor);
const newMembers = [...classDeclaration.members, ...newMethods];
return ts.updateClassDeclaration(
classDeclaration,
/* decorators */ classDeclaration.decorators,
/* modifiers */ classDeclaration.modifiers,
/* name */ classDeclaration.name,
/* typeParameters */ classDeclaration.typeParameters,
/* heritageClauses */ classDeclaration.heritageClauses || [],
/* members */ newMembers);
}
}
}
return node;
});
// Validate that all the classes have been generated
classNames.size == 0 ||
error(
`${classNames.size == 1 ? 'Class' : 'Classes'} "${Array.from(classNames.keys()).join(', ')}" not generated`);
// Add imports to the module required by the new methods
const imports = converter.getImports();
if (imports && imports.length) {
// Find where the new imports should go
const index = firstAfter(
newStatements, statement => statement.kind === ts.SyntaxKind.ImportDeclaration ||
statement.kind === ts.SyntaxKind.ImportEqualsDeclaration);
newStatements =
[...newStatements.slice(0, index), ...imports, ...prefix, ...newStatements.slice(index)];
} else {
newStatements = [...prefix, ...newStatements];
}
converter.updateSourceMap(newStatements);
const newSourceFile = ts.updateSourceFileNode(sourceFile, newStatements);
return [newSourceFile, converter.getNodeMap()];
}
// Return the index after the first value in `a` that doesn't match the predicate after a value that
// does or 0 if no values match.
function firstAfter<T>(a: T[], predicate: (value: T) => boolean) {
let index = 0;
const len = a.length;
for (; index < len; index++) {
const value = a[index];
if (predicate(value)) break;
}
if (index >= len) return 0;
for (; index < len; index++) {
const value = a[index];
if (!predicate(value)) break;
}
return index;
}
// A recorded node is a subtype of the node that is marked as being recoreded. This is used
// to ensure that NodeEmitterVisitor.record has been called on all nodes returned by the
// NodeEmitterVisitor
@ -232,9 +325,12 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
const modifiers = this.getModifiers(stmt);
const fields = stmt.fields.map(
field => ts.createProperty(
/* decorators */ undefined, /* modifiers */ undefined, field.name,
/* decorators */ undefined, /* modifiers */ translateModifiers(field.modifiers),
field.name,
/* questionToken */ undefined,
/* type */ undefined, ts.createNull()));
/* type */ undefined,
field.initializer == null ? ts.createNull() :
field.initializer.visitExpression(this, null)));
const getters = stmt.getters.map(
getter => ts.createGetAccessor(
/* decorators */ undefined, /* modifiers */ undefined, getter.name, /* parameters */[],
@ -256,7 +352,8 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
const methods = stmt.methods.filter(method => method.name)
.map(
method => ts.createMethod(
/* decorators */ undefined, /* modifiers */ undefined,
/* decorators */ undefined,
/* modifiers */ translateModifiers(method.modifiers),
/* astriskToken */ undefined, method.name !/* guarded by filter */,
/* questionToken */ undefined, /* typeParameters */ undefined,
method.params.map(
@ -547,3 +644,21 @@ function getMethodName(methodRef: {name: string | null; builtin: BuiltinMethod |
}
throw new Error('Unexpected method reference form');
}
function modifierFromModifier(modifier: StmtModifier): ts.Modifier {
switch (modifier) {
case StmtModifier.Exported:
return ts.createToken(ts.SyntaxKind.ExportKeyword);
case StmtModifier.Final:
return ts.createToken(ts.SyntaxKind.ConstKeyword);
case StmtModifier.Private:
return ts.createToken(ts.SyntaxKind.PrivateKeyword);
case StmtModifier.Static:
return ts.createToken(ts.SyntaxKind.StaticKeyword);
}
return error(`unknown statement modifier`);
}
function translateModifiers(modifiers: StmtModifier[] | null): ts.Modifier[]|undefined {
return modifiers == null ? undefined : modifiers !.map(modifierFromModifier);
}

View File

@ -1,3 +1,4 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
@ -6,7 +7,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
@ -18,7 +19,8 @@ import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, D
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused} from './util';
import {getAngularClassTransformerFactory} from './r3_transform';
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
/**
@ -65,9 +67,10 @@ class AngularCompilerProgram implements Program {
private host: CompilerHost, oldProgram?: Program) {
this.rootNames = [...rootNames];
const [major, minor] = ts.version.split('.');
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
}
Number(major) > 2 || (Number(major) === 2 && Number(minor) >= 4) ||
userError('The Angular Compiler requires TypeScript >= 2.4.');
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
if (oldProgram) {
this.oldProgramLibrarySummaries = oldProgram.getLibrarySummaries();
@ -79,8 +82,6 @@ class AngularCompilerProgram implements Program {
const {host: bundleHost, indexName, errors} =
createBundleIndexHost(options, this.rootNames, host);
if (errors) {
// TODO(tbosch): once we move MetadataBundler from tsc_wrapped into compiler_cli,
// directly create ng.Diagnostic instead of using ts.Diagnostic here.
this._optionsDiagnostics.push(...errors.map(e => ({
category: e.category,
messageText: e.messageText as string,
@ -196,7 +197,55 @@ class AngularCompilerProgram implements Program {
return this.compiler.listLazyRoutes(route, route ? undefined : this.analyzedModules);
}
emit(
emit(parameters: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback
} = {}): ts.EmitResult {
return this.options.enableIvy === true ? this._emitRender3(parameters) :
this._emitRender2(parameters);
}
private _emitRender3(
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback
} = {}): ts.EmitResult {
const emitStart = Date.now();
if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Codegen)) ===
0) {
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
}
const modules = this.compiler.emitAllPartialModules(this.analyzedModules);
const writeTsFile: ts.WriteFileCallback =
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
let genFile: GeneratedFile|undefined;
this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles);
};
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
const tsCustomTansformers = this.calculateTransforms(
/* genFiles */ undefined, /* partialModules */ modules, customTransformers);
const emitResult = emitCallback({
program: this.tsProgram,
host: this.host,
options: this.options,
writeFile: writeTsFile, emitOnlyDtsFiles,
customTransformers: tsCustomTansformers
});
return emitResult;
}
private _emitRender2(
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: {
emitFlags?: EmitFlags,
@ -244,7 +293,8 @@ class AngularCompilerProgram implements Program {
}
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);
};
const tsCustomTansformers = this.calculateTransforms(genFileByFileName, customTransformers);
const tsCustomTansformers = this.calculateTransforms(
genFileByFileName, /* partialModules */ undefined, customTransformers);
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
// Restore the original references before we emit so TypeScript doesn't emit
// a reference to the .d.ts file.
@ -400,13 +450,18 @@ class AngularCompilerProgram implements Program {
}
private calculateTransforms(
genFiles: Map<string, GeneratedFile>,
genFiles: Map<string, GeneratedFile>|undefined, partialModules: PartialModule[]|undefined,
customTransformers?: CustomTransformers): ts.CustomTransformers {
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
if (!this.options.disableExpressionLowering) {
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache, this.tsProgram));
}
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
if (genFiles) {
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
}
if (partialModules) {
beforeTs.push(getAngularClassTransformerFactory(partialModules));
}
if (customTransformers && customTransformers.beforeTs) {
beforeTs.push(...customTransformers.beforeTs);
}

View File

@ -0,0 +1,36 @@
/**
* @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 {PartialModule, Statement, StaticSymbol} from '@angular/compiler';
import * as ts from 'typescript';
import {updateSourceFile} from './node_emitter';
export type Transformer = (sourceFile: ts.SourceFile) => ts.SourceFile;
export type TransformerFactory = (context: ts.TransformationContext) => Transformer;
/**
* Returns a transformer that adds the requested static methods specified by modules.
*/
export function getAngularClassTransformerFactory(modules: PartialModule[]): TransformerFactory {
if (modules.length === 0) {
// If no modules are specified, just return an identity transform.
return () => sf => sf;
}
const moduleMap = new Map(modules.map<[string, PartialModule]>(m => [m.fileName, m]));
return function(context: ts.TransformationContext) {
return function(sourceFile: ts.SourceFile): ts.SourceFile {
const module = moduleMap.get(sourceFile.fileName);
if (module) {
const [newSourceFile] = updateSourceFile(sourceFile, module, context);
return newSourceFile;
}
return sourceFile;
};
};
}

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {syntaxError} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
@ -21,6 +22,14 @@ export function tsStructureIsReused(program: ts.Program): StructureIsReused {
return (program as any).structureIsReused;
}
export function error(msg: string): never {
throw new Error(`Internal error: ${msg}`);
}
export function userError(msg: string): never {
throw syntaxError(msg);
}
export function createMessageDiagnostic(messageText: string): ts.Diagnostic&Diagnostic {
return {
file: undefined,

View File

@ -13,7 +13,9 @@ export type Entry = string | Directory;
export interface Directory { [name: string]: Entry; }
export class MockAotContext {
constructor(public currentDirectory: string, private files: Entry) {}
private files: Entry[];
constructor(public currentDirectory: string, ...files: Entry[]) { this.files = files; }
fileExists(fileName: string): boolean { return typeof this.getEntry(fileName) === 'string'; }
@ -53,19 +55,7 @@ export class MockAotContext {
}
parts.shift();
parts = normalize(parts);
let current = this.files;
while (parts.length) {
const part = parts.shift() !;
if (typeof current === 'string') {
return undefined;
}
const next = (<Directory>current)[part];
if (next === undefined) {
return undefined;
}
current = next;
}
return current;
return first(this.files, files => getEntryFromFiles(parts, files));
}
getDirectories(path: string): string[] {
@ -76,6 +66,31 @@ export class MockAotContext {
return Object.keys(dir).filter(key => typeof dir[key] === 'object');
}
}
override(files: Entry) { return new MockAotContext(this.currentDirectory, files, ...this.files); }
}
function first<T>(a: T[], cb: (value: T) => T | undefined): T|undefined {
for (const value of a) {
const result = cb(value);
if (result != null) return result;
}
}
function getEntryFromFiles(parts: string[], files: Entry) {
let current = files;
while (parts.length) {
const part = parts.shift() !;
if (typeof current === 'string') {
return undefined;
}
const next = (<Directory>current)[part];
if (next === undefined) {
return undefined;
}
current = next;
}
return current;
}
function normalize(parts: string[]): string[] {

View File

@ -1807,4 +1807,42 @@ describe('ngc transformer command-line', () => {
`);
});
});
describe('ivy', () => {
function emittedFile(name: string): string {
const outputName = path.resolve(outDir, name);
expect(fs.existsSync(outputName)).toBe(true);
return fs.readFileSync(outputName, {encoding: 'UTF-8'});
}
it('should emit the hello world example', () => {
write('tsconfig.json', `{
"extends": "./tsconfig-base.json",
"files": ["hello-world.ts"],
"angularCompilerOptions": {
"enableIvy": true
}
}`);
write('hello-world.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'hello-world',
template: 'Hello, world!'
})
export class HelloWorldComponent {
}
@NgModule({
declarations: [HelloWorldComponent]
})
export class HelloWorldModule {}
`);
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]);
expect(exitCode).toBe(0, 'Compile failed');
expect(emittedFile('hello-world.js')).toContain('ngComponentDef');
});
});
});

View File

@ -0,0 +1,163 @@
/**
* @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 {PartialModule} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {MockAotCompilerHost} from 'compiler/test/aot/test_util';
import {initDomAdapter} from 'platform-browser/src/browser';
import * as ts from 'typescript';
import {getAngularClassTransformerFactory} from '../../src/transformers/r3_transform';
import {Directory, MockAotContext, MockCompilerHost} from '../mocks';
const someGenFilePath = '/somePackage/someGenFile';
const someGenFileName = someGenFilePath + '.ts';
describe('r3_transform_spec', () => {
let context: MockAotContext;
let host: MockCompilerHost;
beforeEach(() => {
context = new MockAotContext('/', FILES);
host = new MockCompilerHost(context);
});
it('should be able to generate a simple identity function', () => {
expect(emitStaticMethod(new o.ReturnStatement(o.variable('v')), ['v']))
.toContain('static someMethod(v) { return v; }');
});
it('should be able to generate a static field declaration',
() => { expect(emitStaticField(o.literal(10))).toContain('SomeClass.someField = 10'); });
it('should be able to import a symbol', () => {
expect(emitStaticMethod(new o.ReturnStatement(
o.importExpr(new o.ExternalReference('@angular/core', 'Component')))))
.toContain('static someMethod() { return i0.Component; } }');
});
it('should be able to modify multiple classes in the same module', () => {
const result = emit(getAngularClassTransformerFactory([{
fileName: someGenFileName,
statements: [
classMethod(new o.ReturnStatement(o.variable('v')), ['v'], 'someMethod', 'SomeClass'),
classMethod(
new o.ReturnStatement(o.variable('v')), ['v'], 'someOtherMethod', 'SomeOtherClass')
]
}]));
expect(result).toContain('static someMethod(v) { return v; }');
expect(result).toContain('static someOtherMethod(v) { return v; }');
});
it('should insert imports after existing imports', () => {
context = context.override({
somePackage: {
'someGenFile.ts': `
import {Component} from '@angular/core';
@Component({selector: 'some-class', template: 'hello!'})
export class SomeClass {}
export class SomeOtherClass {}
`
}
});
host = new MockCompilerHost(context);
expect(emitStaticMethod(new o.ReturnStatement(
o.importExpr(new o.ExternalReference('@angular/core', 'Component')))))
.toContain('const core_1 = require("@angular/core"); const i0 = require("@angular/core");');
});
function emit(factory: ts.TransformerFactory<ts.SourceFile>): string {
let result: string = '';
const program = ts.createProgram(
[someGenFileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host);
const moduleSourceFile = program.getSourceFile(someGenFileName);
const transformers: ts.CustomTransformers = {before: [factory]};
const emitResult = program.emit(
moduleSourceFile, (fileName, data, writeByteOrderMark, onError, sourceFiles) => {
if (fileName.startsWith(someGenFilePath)) {
result = data;
}
}, undefined, undefined, transformers);
return normalizeResult(result);
}
function emitStaticMethod(
stmt: o.Statement | o.Statement[], parameters: string[] = [],
methodName: string = 'someMethod', className: string = 'SomeClass'): string {
const module: PartialModule = {
fileName: someGenFileName,
statements: [classMethod(stmt, parameters, methodName, className)]
};
return emit(getAngularClassTransformerFactory([module]));
}
function emitStaticField(
initializer: o.Expression, fieldName: string = 'someField',
className: string = 'SomeClass'): string {
const module: PartialModule = {
fileName: someGenFileName,
statements: [classField(initializer, fieldName, className)]
};
return emit(getAngularClassTransformerFactory([module]));
}
});
const FILES: Directory = {
somePackage: {
'someGenFile.ts': `
export class SomeClass {}
export class SomeOtherClass {}
`
}
};
function classMethod(
stmt: o.Statement | o.Statement[], parameters: string[] = [], methodName: string = 'someMethod',
className: string = 'SomeClass'): o.ClassStmt {
const statements = Array.isArray(stmt) ? stmt : [stmt];
return new o.ClassStmt(
/* name */ className,
/* parent */ null,
/* fields */[],
/* getters */[],
/* constructorMethod */ new o.ClassMethod(null, [], []),
/* methods */[new o.ClassMethod(
methodName, parameters.map(name => new o.FnParam(name)), statements, null,
[o.StmtModifier.Static])]);
}
function classField(
initializer: o.Expression, fieldName: string = 'someField',
className: string = 'SomeClass'): o.ClassStmt {
return new o.ClassStmt(
/* name */ className,
/* parent */ null,
/* fields */[new o.ClassField(fieldName, null, [o.StmtModifier.Static], initializer)],
/* getters */[],
/* constructorMethod */ new o.ClassMethod(null, [], []),
/* methods */[]);
}
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, '');
}