Index: lib/main.ts |
diff --git a/lib/main.ts b/lib/main.ts |
index 639e9ce343d1f6b4711170a4ec11efcb4ec9de0d..7c3c0d6d4579597d8f5d48e93a3c074be3dbdb45 100644 |
--- a/lib/main.ts |
+++ b/lib/main.ts |
@@ -1,19 +1,16 @@ |
-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 * as base 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 +21,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 +38,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 +57,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 +112,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 +182,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); |
} |
@@ -187,6 +225,7 @@ export class Transpiler { |
let result = dartStyle.formatCode(code); |
if (result.error) { |
this.reportError(context, result.error); |
+ return code; |
} |
return result.code; |
} |
@@ -196,9 +235,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 +258,7 @@ export class Transpiler { |
if (errors.length) { |
let e = new Error(errors.join('\n')); |
- e.name = 'TS2DartError'; |
+ e.name = 'DartFacadeError'; |
throw e; |
} |
} |
@@ -240,8 +280,15 @@ 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); } |
+ maybeLineBreak() { return this.currentOutput.maybeLineBreak(); } |
+ enterCodeComment() { return this.currentOutput.enterCodeComment(); } |
+ exitCodeComment() { return this.currentOutput.exitCodeComment(); } |
+ emitType(s: string, comment: string) { return this.currentOutput.emitType(s, comment); } |
+ get insideCodeComment() { return this.currentOutput.insideCodeComment; } |
reportError(n: ts.Node, message: string) { |
let file = n.getSourceFile() || this.currentFile; |
@@ -255,16 +302,26 @@ 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.emit(this.translateComment(text)); |
- if (c.hasTrailingNewLine) this.emitNoSpace('\n'); |
+ if (c.pos > 1) { |
+ let prev = this.currentFile.text.substring(c.pos - 2, c.pos); |
+ if (prev === '\n\n') { |
+ // If the two previous characters are both \n then add a \n |
+ // so that we ensure the output has sufficient line breaks to |
+ // separate comment blocks. |
+ this.currentOutput.emit('\n'); |
+ } |
+ } |
+ this.currentOutput.emitComment(this.translateComment(text)); |
}); |
} |
@@ -280,6 +337,7 @@ export class Transpiler { |
private normalizeSlashes(path: string) { return path.replace(/\\/g, '/'); } |
private translateComment(comment: string): string { |
+ let rawComment = comment; |
comment = comment.replace(/\{@link ([^\}]+)\}/g, '[$1]'); |
// Remove the following tags and following comments till end of line. |
@@ -292,7 +350,21 @@ export class Transpiler { |
comment = comment.replace(/@description/g, ''); |
comment = comment.replace(/@deprecated/g, ''); |
- return comment; |
+ // Switch to /* */ comments and // comments to /// |
+ let sb = ''; |
+ for (let line of comment.split('\n')) { |
+ line = line.trim(); |
+ line = line.replace(/^[\/]\*\*?/g, ''); |
+ line = line.replace(/\*[\/]$/g, ''); |
+ line = line.replace(/^\*/g, ''); |
+ line = line.replace(/^\/\/\/?/g, ''); |
+ line = line.trim(); |
+ if (line.length > 0) { |
+ sb += ' /// ' + line + '\n'; |
+ } |
+ } |
+ if (rawComment[0] === '\n') sb = '\n' + sb; |
+ return sb; |
} |
} |
@@ -319,60 +391,100 @@ 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; |
+ |
+ 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'); |
} |
} |
emit(str: string) { |
- this.emitNoSpace(' '); |
+ if (this.result.length > 0) { |
+ let buffer = this.insideCodeComment ? this.codeCommentResult : this.result; |
+ let lastChar = buffer[buffer.length - 1]; |
+ if (lastChar !== ' ' && lastChar !== '(' && lastChar !== '<' && lastChar !== '[') { |
+ // Avoid emitting a space in obvious cases where a space is not required |
+ // to make the output slightly prettier in cases where the DartFormatter |
+ // cannot run such as within a comment where code we emit is not quite |
+ // valid Dart code. |
+ this.emitNoSpace(' '); |
+ } |
+ } |
this.emitNoSpace(str); |
} |
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(); } |
+ enterCodeComment() { |
+ if (this.insideCodeComment) { |
+ throw 'Cannot nest code comments' + this.codeCommentResult; |
+ } |
+ this.insideCodeComment = true; |
+ this.codeCommentResult = ''; |
+ } |
- 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); |
+ emitType(s: string, comment: string) { |
+ this.emit(base.formatType(s, comment, this.insideCodeComment)); |
+ } |
- let mapping: SourceMap.Mapping = { |
- original: {line: pos.line + 1, column: pos.character}, |
- generated: {line: this.line, column: this.column}, |
- source: this.relativeFileName, |
- }; |
+ /** |
+ * Always emit comments in the main program body outside of the existing code |
+ * comment block. |
+ */ |
+ emitComment(s: string) { |
+ if (!this.firstColumn) { |
+ this.result += '\n'; |
+ } |
+ this.result += s; |
+ this.firstColumn = true; |
+ } |
- this.sourceMap.addMapping(mapping); |
+ exitCodeComment() { |
+ if (!this.insideCodeComment) { |
+ throw 'Exit code comment called while not within a code comment.'; |
+ } |
+ this.insideCodeComment = false; |
+ this.emitNoSpace(' /*'); |
+ let result = dartStyle.formatCode(this.codeCommentResult); |
+ let code = this.codeCommentResult; |
+ if (!result.error) { |
+ code = result.code; |
+ } |
+ code = code.trim(); |
+ this.emitNoSpace(code); |
+ this.emitNoSpace('*/'); |
+ |
+ // 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 +496,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); |
} |