Index: lib/main.ts |
diff --git a/lib/main.ts b/lib/main.ts |
index 639e9ce343d1f6b4711170a4ec11efcb4ec9de0d..9f28aac76db76694be9dd5771bd5034e1f2ebf7b 100644 |
--- a/lib/main.ts |
+++ b/lib/main.ts |
@@ -1,19 +1,15 @@ |
-require('source-map-support').install(); |
-import {SourceMapGenerator} from 'source-map'; |
import * as fs from 'fs'; |
import * as path from 'path'; |
import * as ts from 'typescript'; |
-import {TranspilerBase} from './base'; |
+import {Set, TranspilerBase} from './base'; |
+ |
import mkdirP from './mkdirp'; |
-import CallTranspiler from './call'; |
import DeclarationTranspiler from './declaration'; |
-import ExpressionTranspiler from './expression'; |
+import * as merge from './merge'; |
import ModuleTranspiler from './module'; |
-import StatementTranspiler from './statement'; |
import TypeTranspiler from './type'; |
-import LiteralTranspiler from './literal'; |
-import {FacadeConverter} from './facade_converter'; |
+import {FacadeConverter, NameRewriter} from './facade_converter'; |
import * as dartStyle from 'dart-style'; |
export interface TranspilerOptions { |
@@ -24,18 +20,15 @@ export interface TranspilerOptions { |
failFast?: boolean; |
/** Whether to generate 'library a.b.c;' names from relative file paths. */ |
generateLibraryName?: boolean; |
- /** Whether to generate source maps. */ |
- generateSourceMap?: boolean; |
/** |
* A base path to relativize absolute file paths against. This is useful for library name |
* generation (see above) and nicer file names in error messages. |
*/ |
basePath?: string; |
/** |
- * Translate calls to builtins, i.e. seemlessly convert from `Array` to `List`, and convert the |
- * corresponding methods. Requires type checking. |
+ * Use dart:html instead of the raw JavaScript DOM when generated Dart code. |
*/ |
- translateBuiltins?: boolean; |
+ useHtml?: boolean; |
/** |
* Enforce conventions of public/private keyword and underscore prefix |
*/ |
@@ -44,6 +37,16 @@ export interface TranspilerOptions { |
* Sets a root path to look for typings used by the facade converter. |
*/ |
typingsRoot?: string; |
+ |
+ /** |
+ * Experimental JS Interop specific option to promote properties with function |
+ * types to methods instead of properties with a function type. This the makes |
+ * the Dart code more readable at the cost of disallowing setting the value of |
+ * the property. |
+ * Example JS library that benifits from this option: |
+ * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/chartjs/chart.d.ts |
+ */ |
+ promoteFunctionLikeMembers?: boolean; |
} |
export const COMPILER_OPTIONS: ts.CompilerOptions = { |
@@ -53,28 +56,40 @@ export const COMPILER_OPTIONS: ts.CompilerOptions = { |
target: ts.ScriptTarget.ES5, |
}; |
+/** |
+ * Context to ouput code into. |
+ */ |
+export enum OutputContext { |
+ Import = 0, |
+ Header = 1, |
+ Default = 2, |
+} |
+ |
+const NUM_OUTPUT_CONTEXTS = 3; |
+ |
export class Transpiler { |
- private output: Output; |
+ private outputs: Output[]; |
+ private outputStack: Output[]; |
private currentFile: ts.SourceFile; |
- |
+ importsEmitted: Set; |
// Comments attach to all following AST nodes before the next 'physical' token. Track the earliest |
// offset to avoid printing comments multiple times. |
private lastCommentIdx: number = -1; |
private errors: string[] = []; |
private transpilers: TranspilerBase[]; |
+ private declarationTranspiler: DeclarationTranspiler; |
private fc: FacadeConverter; |
+ private nameRewriter: NameRewriter; |
constructor(private options: TranspilerOptions = {}) { |
- // TODO: Remove the angular2 default when angular uses typingsRoot. |
- this.fc = new FacadeConverter(this, options.typingsRoot || 'angular2/typings/'); |
+ this.nameRewriter = new NameRewriter(); |
+ this.fc = new FacadeConverter(this, options.typingsRoot, this.nameRewriter, options.useHtml); |
+ this.declarationTranspiler = new DeclarationTranspiler( |
+ this, this.fc, options.enforceUnderscoreConventions, options.promoteFunctionLikeMembers); |
this.transpilers = [ |
- new CallTranspiler(this, this.fc), // Has to come before StatementTranspiler! |
- new DeclarationTranspiler(this, this.fc, options.enforceUnderscoreConventions), |
- new ExpressionTranspiler(this, this.fc), |
- new LiteralTranspiler(this, this.fc), |
+ this.declarationTranspiler, |
new ModuleTranspiler(this, this.fc, options.generateLibraryName), |
- new StatementTranspiler(this), |
new TypeTranspiler(this, this.fc), |
]; |
} |
@@ -96,32 +111,29 @@ export class Transpiler { |
} |
let destinationRoot = destination || this.options.basePath || ''; |
let program = ts.createProgram(fileNames, this.getCompilerOptions(), host); |
- if (this.options.translateBuiltins) { |
- this.fc.setTypeChecker(program.getTypeChecker()); |
- } |
+ this.fc.setTypeChecker(program.getTypeChecker()); |
+ this.declarationTranspiler.setTypeChecker(program.getTypeChecker()); |
// Only write files that were explicitly passed in. |
let fileSet: {[s: string]: boolean} = {}; |
fileNames.forEach((f) => fileSet[f] = true); |
+ let sourceFiles = program.getSourceFiles().filter((sourceFile) => fileSet[sourceFile.fileName]); |
this.errors = []; |
- program.getSourceFiles() |
- .filter((sourceFile) => fileSet[sourceFile.fileName]) |
- // Do not generate output for .d.ts files. |
- .filter((sourceFile: ts.SourceFile) => !sourceFile.fileName.match(/\.d\.ts$/)) |
- .forEach((f: ts.SourceFile) => { |
- let dartCode = this.translate(f); |
- let outputFile = this.getOutputPath(path.resolve(f.fileName), destinationRoot); |
- mkdirP(path.dirname(outputFile)); |
- fs.writeFileSync(outputFile, dartCode); |
- }); |
+ |
+ sourceFiles.forEach((f: ts.SourceFile) => { |
+ let dartCode = this.translate(f); |
+ let outputFile = this.getOutputPath(path.resolve(f.fileName), destinationRoot); |
+ mkdirP(path.dirname(outputFile)); |
+ fs.writeFileSync(outputFile, dartCode); |
+ }); |
this.checkForErrors(program); |
} |
translateProgram(program: ts.Program): {[path: string]: string} { |
- if (this.options.translateBuiltins) { |
- this.fc.setTypeChecker(program.getTypeChecker()); |
- } |
+ this.fc.setTypeChecker(program.getTypeChecker()); |
+ this.declarationTranspiler.setTypeChecker(program.getTypeChecker()); |
+ |
let paths: {[path: string]: string} = {}; |
this.errors = []; |
program.getSourceFiles() |
@@ -169,17 +181,42 @@ export class Transpiler { |
// Visible for testing. |
getOutputPath(filePath: string, destinationRoot: string): string { |
let relative = this.getRelativeFileName(filePath); |
- let dartFile = relative.replace(/.(js|es6|ts)$/, '.dart'); |
+ let dartFile = relative.replace(/.(js|es6|d\.ts|ts)$/, '.dart'); |
return this.normalizeSlashes(path.join(destinationRoot, dartFile)); |
} |
+ public pushContext(context: OutputContext) { this.outputStack.push(this.outputs[context]); } |
+ |
+ public popContext() { |
+ if (this.outputStack.length === 0) { |
+ this.reportError(null, 'Attempting to pop output stack when already empty'); |
+ } |
+ this.outputStack.pop(); |
+ } |
+ |
private translate(sourceFile: ts.SourceFile): string { |
this.currentFile = sourceFile; |
- this.output = |
- new Output(sourceFile, this.getRelativeFileName(), this.options.generateSourceMap); |
+ this.outputs = []; |
+ this.outputStack = []; |
+ this.importsEmitted = {}; |
+ for (let i = 0; i < NUM_OUTPUT_CONTEXTS; ++i) { |
+ this.outputs.push(new Output()); |
+ } |
+ |
this.lastCommentIdx = -1; |
+ merge.normalizeSourceFile(sourceFile); |
+ this.pushContext(OutputContext.Default); |
this.visit(sourceFile); |
- let result = this.output.getResult(); |
+ this.popContext(); |
+ if (this.outputStack.length > 0) { |
+ this.reportError( |
+ sourceFile, 'Internal error managing output contexts. ' + |
+ 'Inconsistent push and pop context calls.'); |
+ } |
+ let result = ''; |
+ for (let output of this.outputs) { |
+ result += output.getResult(); |
+ } |
return this.formatCode(result, sourceFile); |
} |
@@ -196,9 +233,10 @@ export class Transpiler { |
let diagnostics = program.getGlobalDiagnostics().concat(program.getSyntacticDiagnostics()); |
- if ((errors.length || diagnostics.length) && this.options.translateBuiltins) { |
- // Only report semantic diagnostics if ts2dart failed; this code is not a generic compiler, so |
- // only yields TS errors if they could be the cause of ts2dart issues. |
+ if ((errors.length || diagnostics.length)) { |
+ // Only report semantic diagnostics if facade generation failed; this |
+ // code is not a generic compiler, so only yields TS errors if they could |
+ // be the cause of facade generation issues. |
// This greatly speeds up tests and execution. |
diagnostics = diagnostics.concat(program.getSemanticDiagnostics()); |
} |
@@ -218,7 +256,7 @@ export class Transpiler { |
if (errors.length) { |
let e = new Error(errors.join('\n')); |
- e.name = 'TS2DartError'; |
+ e.name = 'DartFacadeError'; |
throw e; |
} |
} |
@@ -240,8 +278,12 @@ export class Transpiler { |
return this.normalizeSlashes(path.relative(base, filePath)); |
} |
- emit(s: string) { this.output.emit(s); } |
- emitNoSpace(s: string) { this.output.emitNoSpace(s); } |
+ private get currentOutput(): Output { return this.outputStack[this.outputStack.length - 1]; } |
+ |
+ emit(s: string) { this.currentOutput.emit(s); } |
+ emitNoSpace(s: string) { this.currentOutput.emitNoSpace(s); } |
+ enterCodeComment() { return this.currentOutput.enterCodeComment(); } |
+ exitCodeComment() { return this.currentOutput.exitCodeComment(); } |
reportError(n: ts.Node, message: string) { |
let file = n.getSourceFile() || this.currentFile; |
@@ -255,14 +297,17 @@ export class Transpiler { |
} |
visit(node: ts.Node) { |
- this.output.addSourceMapping(node); |
+ if (!node) return; |
let comments = ts.getLeadingCommentRanges(this.currentFile.text, node.getFullStart()); |
if (comments) { |
comments.forEach((c) => { |
+ // Warning: the following check means that comments will only be |
+ // emitted correctly if Dart code is emitted in the same order it |
+ // appeared in the JavaScript AST. |
if (c.pos <= this.lastCommentIdx) return; |
this.lastCommentIdx = c.pos; |
let text = this.currentFile.text.substring(c.pos, c.end); |
- this.emitNoSpace('\n'); |
+ this.currentOutput.maybeLineBreak(); |
this.emit(this.translateComment(text)); |
if (c.hasTrailingNewLine) this.emitNoSpace('\n'); |
}); |
@@ -292,6 +337,9 @@ export class Transpiler { |
comment = comment.replace(/@description/g, ''); |
comment = comment.replace(/@deprecated/g, ''); |
+ // Hack to make comment indentation does not look as bad when dart format is |
+ // run on files with multiple nested modules. |
+ comment = comment.replace(/\n ( +[*])/g, '\n$1'); |
return comment; |
} |
} |
@@ -319,19 +367,22 @@ export function getModuleResolver(compilerHost: ts.CompilerHost) { |
class Output { |
private result: string = ''; |
- private column: number = 1; |
- private line: number = 1; |
- |
- // Position information. |
- private generateSourceMap: boolean; |
- private sourceMap: SourceMapGenerator; |
- |
- constructor( |
- private currentFile: ts.SourceFile, private relativeFileName: string, |
- generateSourceMap: boolean) { |
- if (generateSourceMap) { |
- this.sourceMap = new SourceMapGenerator({file: relativeFileName + '.dart'}); |
- this.sourceMap.setSourceContent(relativeFileName, this.currentFile.text); |
+ private firstColumn: boolean = true; |
+ |
+ private insideCodeComment: boolean = false; |
+ private codeCommentResult: string = ''; |
+ |
+ /** |
+ * Line break if the current line is not empty. |
+ */ |
+ maybeLineBreak() { |
+ if (this.insideCodeComment) { |
+ // Avoid line breaks inside code comments. |
+ return; |
+ } |
+ |
+ if (!this.firstColumn) { |
+ this.emitNoSpace('\n'); |
} |
} |
@@ -341,38 +392,49 @@ class Output { |
} |
emitNoSpace(str: string) { |
- this.result += str; |
- for (let i = 0; i < str.length; i++) { |
- if (str[i] === '\n') { |
- this.line++; |
- this.column = 0; |
- } else { |
- this.column++; |
- } |
+ if (str.length === 0) return; |
+ if (this.insideCodeComment) { |
+ this.codeCommentResult += str; |
+ return; |
} |
+ this.result += str; |
+ this.firstColumn = str[str.length - 1] === '\n'; |
} |
- getResult(): string { return this.result + this.generateSourceMapComment(); } |
- |
- addSourceMapping(n: ts.Node) { |
- if (!this.generateSourceMap) return; // source maps disabled. |
- let file = n.getSourceFile() || this.currentFile; |
- let start = n.getStart(file); |
- let pos = file.getLineAndCharacterOfPosition(start); |
- |
- let mapping: SourceMap.Mapping = { |
- original: {line: pos.line + 1, column: pos.character}, |
- generated: {line: this.line, column: this.column}, |
- source: this.relativeFileName, |
- }; |
+ enterCodeComment() { |
+ if (this.insideCodeComment) { |
+ throw 'Cannot nest code comments'; |
+ } |
+ this.insideCodeComment = true; |
+ this.codeCommentResult = ''; |
+ } |
- this.sourceMap.addMapping(mapping); |
+ exitCodeComment() { |
+ if (!this.insideCodeComment) { |
+ throw 'Exit code comment called while not within a code comment.'; |
+ } |
+ this.insideCodeComment = false; |
+ this.emit('/*'); |
+ let result = dartStyle.formatCode(this.codeCommentResult); |
+ if (result.error) { |
+ // Fall back to the raw expression if the formatter returns an error. |
+ // Ideally the formatter would handle a wider range of dart expressions. |
+ this.emit(this.codeCommentResult.trim()); |
+ } else { |
+ this.emit(result.code.trim()); |
+ } |
+ this.emit('*/'); |
+ // Don't really need an exact column, just need to track |
+ // that we aren't on the first column. |
+ this.firstColumn = false; |
+ this.codeCommentResult = ''; |
} |
- private generateSourceMapComment() { |
- if (!this.sourceMap) return ''; |
- let base64map = new Buffer(JSON.stringify(this.sourceMap)).toString('base64'); |
- return '\n\n//# sourceMappingURL=data:application/json;base64,' + base64map; |
+ getResult(): string { |
+ if (this.insideCodeComment) { |
+ throw 'Code comment not property terminated.'; |
+ } |
+ return this.result; |
} |
} |
@@ -384,7 +446,7 @@ if (require.main === module) { |
console.error('Transpiling', args._, 'to', args.destination); |
transpiler.transpile(args._, args.destination); |
} catch (e) { |
- if (e.name !== 'TS2DartError') throw e; |
+ if (e.name !== 'DartFacadeError') throw e; |
console.error(e.message); |
process.exit(1); |
} |