| OLD | NEW |
| 1 require('source-map-support').install(); | |
| 2 import {SourceMapGenerator} from 'source-map'; | |
| 3 import * as fs from 'fs'; | 1 import * as fs from 'fs'; |
| 4 import * as path from 'path'; | 2 import * as path from 'path'; |
| 5 import * as ts from 'typescript'; | 3 import * as ts from 'typescript'; |
| 6 | 4 |
| 7 import {TranspilerBase} from './base'; | 5 import * as base from './base'; |
| 6 import {Set, TranspilerBase} from './base'; |
| 7 |
| 8 import mkdirP from './mkdirp'; | 8 import mkdirP from './mkdirp'; |
| 9 import CallTranspiler from './call'; | |
| 10 import DeclarationTranspiler from './declaration'; | 9 import DeclarationTranspiler from './declaration'; |
| 11 import ExpressionTranspiler from './expression'; | 10 import * as merge from './merge'; |
| 12 import ModuleTranspiler from './module'; | 11 import ModuleTranspiler from './module'; |
| 13 import StatementTranspiler from './statement'; | |
| 14 import TypeTranspiler from './type'; | 12 import TypeTranspiler from './type'; |
| 15 import LiteralTranspiler from './literal'; | 13 import {FacadeConverter, NameRewriter} from './facade_converter'; |
| 16 import {FacadeConverter} from './facade_converter'; | |
| 17 import * as dartStyle from 'dart-style'; | 14 import * as dartStyle from 'dart-style'; |
| 18 | 15 |
| 19 export interface TranspilerOptions { | 16 export interface TranspilerOptions { |
| 20 /** | 17 /** |
| 21 * Fail on the first error, do not collect multiple. Allows easier debugging a
s stack traces lead | 18 * Fail on the first error, do not collect multiple. Allows easier debugging a
s stack traces lead |
| 22 * directly to the offending line. | 19 * directly to the offending line. |
| 23 */ | 20 */ |
| 24 failFast?: boolean; | 21 failFast?: boolean; |
| 25 /** Whether to generate 'library a.b.c;' names from relative file paths. */ | 22 /** Whether to generate 'library a.b.c;' names from relative file paths. */ |
| 26 generateLibraryName?: boolean; | 23 generateLibraryName?: boolean; |
| 27 /** Whether to generate source maps. */ | |
| 28 generateSourceMap?: boolean; | |
| 29 /** | 24 /** |
| 30 * A base path to relativize absolute file paths against. This is useful for l
ibrary name | 25 * A base path to relativize absolute file paths against. This is useful for l
ibrary name |
| 31 * generation (see above) and nicer file names in error messages. | 26 * generation (see above) and nicer file names in error messages. |
| 32 */ | 27 */ |
| 33 basePath?: string; | 28 basePath?: string; |
| 34 /** | 29 /** |
| 35 * Translate calls to builtins, i.e. seemlessly convert from `Array` to `List`
, and convert the | 30 * Use dart:html instead of the raw JavaScript DOM when generated Dart code. |
| 36 * corresponding methods. Requires type checking. | |
| 37 */ | 31 */ |
| 38 translateBuiltins?: boolean; | 32 useHtml?: boolean; |
| 39 /** | 33 /** |
| 40 * Enforce conventions of public/private keyword and underscore prefix | 34 * Enforce conventions of public/private keyword and underscore prefix |
| 41 */ | 35 */ |
| 42 enforceUnderscoreConventions?: boolean; | 36 enforceUnderscoreConventions?: boolean; |
| 43 /** | 37 /** |
| 44 * Sets a root path to look for typings used by the facade converter. | 38 * Sets a root path to look for typings used by the facade converter. |
| 45 */ | 39 */ |
| 46 typingsRoot?: string; | 40 typingsRoot?: string; |
| 41 |
| 42 /** |
| 43 * Experimental JS Interop specific option to promote properties with function |
| 44 * types to methods instead of properties with a function type. This the makes |
| 45 * the Dart code more readable at the cost of disallowing setting the value of |
| 46 * the property. |
| 47 * Example JS library that benifits from this option: |
| 48 * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/chartjs/char
t.d.ts |
| 49 */ |
| 50 promoteFunctionLikeMembers?: boolean; |
| 47 } | 51 } |
| 48 | 52 |
| 49 export const COMPILER_OPTIONS: ts.CompilerOptions = { | 53 export const COMPILER_OPTIONS: ts.CompilerOptions = { |
| 50 allowNonTsExtensions: true, | 54 allowNonTsExtensions: true, |
| 51 experimentalDecorators: true, | 55 experimentalDecorators: true, |
| 52 module: ts.ModuleKind.CommonJS, | 56 module: ts.ModuleKind.CommonJS, |
| 53 target: ts.ScriptTarget.ES5, | 57 target: ts.ScriptTarget.ES5, |
| 54 }; | 58 }; |
| 55 | 59 |
| 60 /** |
| 61 * Context to ouput code into. |
| 62 */ |
| 63 export enum OutputContext { |
| 64 Import = 0, |
| 65 Header = 1, |
| 66 Default = 2, |
| 67 } |
| 68 |
| 69 const NUM_OUTPUT_CONTEXTS = 3; |
| 70 |
| 56 export class Transpiler { | 71 export class Transpiler { |
| 57 private output: Output; | 72 private outputs: Output[]; |
| 73 private outputStack: Output[]; |
| 58 private currentFile: ts.SourceFile; | 74 private currentFile: ts.SourceFile; |
| 59 | 75 importsEmitted: Set; |
| 60 // Comments attach to all following AST nodes before the next 'physical' token
. Track the earliest | 76 // Comments attach to all following AST nodes before the next 'physical' token
. Track the earliest |
| 61 // offset to avoid printing comments multiple times. | 77 // offset to avoid printing comments multiple times. |
| 62 private lastCommentIdx: number = -1; | 78 private lastCommentIdx: number = -1; |
| 63 private errors: string[] = []; | 79 private errors: string[] = []; |
| 64 | 80 |
| 65 private transpilers: TranspilerBase[]; | 81 private transpilers: TranspilerBase[]; |
| 82 private declarationTranspiler: DeclarationTranspiler; |
| 66 private fc: FacadeConverter; | 83 private fc: FacadeConverter; |
| 84 private nameRewriter: NameRewriter; |
| 67 | 85 |
| 68 constructor(private options: TranspilerOptions = {}) { | 86 constructor(private options: TranspilerOptions = {}) { |
| 69 // TODO: Remove the angular2 default when angular uses typingsRoot. | 87 this.nameRewriter = new NameRewriter(); |
| 70 this.fc = new FacadeConverter(this, options.typingsRoot || 'angular2/typings
/'); | 88 this.fc = new FacadeConverter(this, options.typingsRoot, this.nameRewriter,
options.useHtml); |
| 89 this.declarationTranspiler = new DeclarationTranspiler( |
| 90 this, this.fc, options.enforceUnderscoreConventions, options.promoteFunc
tionLikeMembers); |
| 71 this.transpilers = [ | 91 this.transpilers = [ |
| 72 new CallTranspiler(this, this.fc), // Has to come before StatementTranspi
ler! | 92 this.declarationTranspiler, |
| 73 new DeclarationTranspiler(this, this.fc, options.enforceUnderscoreConventi
ons), | |
| 74 new ExpressionTranspiler(this, this.fc), | |
| 75 new LiteralTranspiler(this, this.fc), | |
| 76 new ModuleTranspiler(this, this.fc, options.generateLibraryName), | 93 new ModuleTranspiler(this, this.fc, options.generateLibraryName), |
| 77 new StatementTranspiler(this), | |
| 78 new TypeTranspiler(this, this.fc), | 94 new TypeTranspiler(this, this.fc), |
| 79 ]; | 95 ]; |
| 80 } | 96 } |
| 81 | 97 |
| 82 /** | 98 /** |
| 83 * Transpiles the given files to Dart. | 99 * Transpiles the given files to Dart. |
| 84 * @param fileNames The input files. | 100 * @param fileNames The input files. |
| 85 * @param destination Location to write files to. Creates files next to their
sources if absent. | 101 * @param destination Location to write files to. Creates files next to their
sources if absent. |
| 86 */ | 102 */ |
| 87 transpile(fileNames: string[], destination?: string): void { | 103 transpile(fileNames: string[], destination?: string): void { |
| 88 if (this.options.basePath) { | 104 if (this.options.basePath) { |
| 89 this.options.basePath = this.normalizeSlashes(path.resolve(this.options.ba
sePath)); | 105 this.options.basePath = this.normalizeSlashes(path.resolve(this.options.ba
sePath)); |
| 90 } | 106 } |
| 91 fileNames = fileNames.map((f) => this.normalizeSlashes(f)); | 107 fileNames = fileNames.map((f) => this.normalizeSlashes(f)); |
| 92 let host = this.createCompilerHost(); | 108 let host = this.createCompilerHost(); |
| 93 if (this.options.basePath && destination === undefined) { | 109 if (this.options.basePath && destination === undefined) { |
| 94 throw new Error( | 110 throw new Error( |
| 95 'Must have a destination path when a basePath is specified ' + this.op
tions.basePath); | 111 'Must have a destination path when a basePath is specified ' + this.op
tions.basePath); |
| 96 } | 112 } |
| 97 let destinationRoot = destination || this.options.basePath || ''; | 113 let destinationRoot = destination || this.options.basePath || ''; |
| 98 let program = ts.createProgram(fileNames, this.getCompilerOptions(), host); | 114 let program = ts.createProgram(fileNames, this.getCompilerOptions(), host); |
| 99 if (this.options.translateBuiltins) { | 115 this.fc.setTypeChecker(program.getTypeChecker()); |
| 100 this.fc.setTypeChecker(program.getTypeChecker()); | 116 this.declarationTranspiler.setTypeChecker(program.getTypeChecker()); |
| 101 } | |
| 102 | 117 |
| 103 // Only write files that were explicitly passed in. | 118 // Only write files that were explicitly passed in. |
| 104 let fileSet: {[s: string]: boolean} = {}; | 119 let fileSet: {[s: string]: boolean} = {}; |
| 105 fileNames.forEach((f) => fileSet[f] = true); | 120 fileNames.forEach((f) => fileSet[f] = true); |
| 121 let sourceFiles = program.getSourceFiles().filter((sourceFile) => fileSet[so
urceFile.fileName]); |
| 106 | 122 |
| 107 this.errors = []; | 123 this.errors = []; |
| 108 program.getSourceFiles() | 124 |
| 109 .filter((sourceFile) => fileSet[sourceFile.fileName]) | 125 sourceFiles.forEach((f: ts.SourceFile) => { |
| 110 // Do not generate output for .d.ts files. | 126 let dartCode = this.translate(f); |
| 111 .filter((sourceFile: ts.SourceFile) => !sourceFile.fileName.match(/\.d\.
ts$/)) | 127 let outputFile = this.getOutputPath(path.resolve(f.fileName), destinationR
oot); |
| 112 .forEach((f: ts.SourceFile) => { | 128 mkdirP(path.dirname(outputFile)); |
| 113 let dartCode = this.translate(f); | 129 fs.writeFileSync(outputFile, dartCode); |
| 114 let outputFile = this.getOutputPath(path.resolve(f.fileName), destinat
ionRoot); | 130 }); |
| 115 mkdirP(path.dirname(outputFile)); | |
| 116 fs.writeFileSync(outputFile, dartCode); | |
| 117 }); | |
| 118 this.checkForErrors(program); | 131 this.checkForErrors(program); |
| 119 } | 132 } |
| 120 | 133 |
| 121 translateProgram(program: ts.Program): {[path: string]: string} { | 134 translateProgram(program: ts.Program): {[path: string]: string} { |
| 122 if (this.options.translateBuiltins) { | 135 this.fc.setTypeChecker(program.getTypeChecker()); |
| 123 this.fc.setTypeChecker(program.getTypeChecker()); | 136 this.declarationTranspiler.setTypeChecker(program.getTypeChecker()); |
| 124 } | 137 |
| 125 let paths: {[path: string]: string} = {}; | 138 let paths: {[path: string]: string} = {}; |
| 126 this.errors = []; | 139 this.errors = []; |
| 127 program.getSourceFiles() | 140 program.getSourceFiles() |
| 128 .filter( | 141 .filter( |
| 129 (sourceFile: ts.SourceFile) => | 142 (sourceFile: ts.SourceFile) => |
| 130 (!sourceFile.fileName.match(/\.d\.ts$/) && !!sourceFile.fileName
.match(/\.[jt]s$/))) | 143 (!sourceFile.fileName.match(/\.d\.ts$/) && !!sourceFile.fileName
.match(/\.[jt]s$/))) |
| 131 .forEach((f) => paths[f.fileName] = this.translate(f)); | 144 .forEach((f) => paths[f.fileName] = this.translate(f)); |
| 132 this.checkForErrors(program); | 145 this.checkForErrors(program); |
| 133 return paths; | 146 return paths; |
| 134 } | 147 } |
| (...skipping 27 matching lines...) Expand all Loading... |
| 162 getCurrentDirectory: () => '', | 175 getCurrentDirectory: () => '', |
| 163 getNewLine: () => '\n', | 176 getNewLine: () => '\n', |
| 164 }; | 177 }; |
| 165 compilerHost.resolveModuleNames = getModuleResolver(compilerHost); | 178 compilerHost.resolveModuleNames = getModuleResolver(compilerHost); |
| 166 return compilerHost; | 179 return compilerHost; |
| 167 } | 180 } |
| 168 | 181 |
| 169 // Visible for testing. | 182 // Visible for testing. |
| 170 getOutputPath(filePath: string, destinationRoot: string): string { | 183 getOutputPath(filePath: string, destinationRoot: string): string { |
| 171 let relative = this.getRelativeFileName(filePath); | 184 let relative = this.getRelativeFileName(filePath); |
| 172 let dartFile = relative.replace(/.(js|es6|ts)$/, '.dart'); | 185 let dartFile = relative.replace(/.(js|es6|d\.ts|ts)$/, '.dart'); |
| 173 return this.normalizeSlashes(path.join(destinationRoot, dartFile)); | 186 return this.normalizeSlashes(path.join(destinationRoot, dartFile)); |
| 174 } | 187 } |
| 175 | 188 |
| 189 public pushContext(context: OutputContext) { this.outputStack.push(this.output
s[context]); } |
| 190 |
| 191 public popContext() { |
| 192 if (this.outputStack.length === 0) { |
| 193 this.reportError(null, 'Attempting to pop output stack when already empty'
); |
| 194 } |
| 195 this.outputStack.pop(); |
| 196 } |
| 197 |
| 176 private translate(sourceFile: ts.SourceFile): string { | 198 private translate(sourceFile: ts.SourceFile): string { |
| 177 this.currentFile = sourceFile; | 199 this.currentFile = sourceFile; |
| 178 this.output = | 200 this.outputs = []; |
| 179 new Output(sourceFile, this.getRelativeFileName(), this.options.generate
SourceMap); | 201 this.outputStack = []; |
| 202 this.importsEmitted = {}; |
| 203 for (let i = 0; i < NUM_OUTPUT_CONTEXTS; ++i) { |
| 204 this.outputs.push(new Output()); |
| 205 } |
| 206 |
| 180 this.lastCommentIdx = -1; | 207 this.lastCommentIdx = -1; |
| 208 merge.normalizeSourceFile(sourceFile); |
| 209 this.pushContext(OutputContext.Default); |
| 181 this.visit(sourceFile); | 210 this.visit(sourceFile); |
| 182 let result = this.output.getResult(); | 211 this.popContext(); |
| 212 if (this.outputStack.length > 0) { |
| 213 this.reportError( |
| 214 sourceFile, 'Internal error managing output contexts. ' + |
| 215 'Inconsistent push and pop context calls.'); |
| 216 } |
| 217 let result = ''; |
| 218 for (let output of this.outputs) { |
| 219 result += output.getResult(); |
| 220 } |
| 183 return this.formatCode(result, sourceFile); | 221 return this.formatCode(result, sourceFile); |
| 184 } | 222 } |
| 185 | 223 |
| 186 private formatCode(code: string, context: ts.Node) { | 224 private formatCode(code: string, context: ts.Node) { |
| 187 let result = dartStyle.formatCode(code); | 225 let result = dartStyle.formatCode(code); |
| 188 if (result.error) { | 226 if (result.error) { |
| 189 this.reportError(context, result.error); | 227 this.reportError(context, result.error); |
| 228 return code; |
| 190 } | 229 } |
| 191 return result.code; | 230 return result.code; |
| 192 } | 231 } |
| 193 | 232 |
| 194 private checkForErrors(program: ts.Program) { | 233 private checkForErrors(program: ts.Program) { |
| 195 let errors = this.errors; | 234 let errors = this.errors; |
| 196 | 235 |
| 197 let diagnostics = program.getGlobalDiagnostics().concat(program.getSyntactic
Diagnostics()); | 236 let diagnostics = program.getGlobalDiagnostics().concat(program.getSyntactic
Diagnostics()); |
| 198 | 237 |
| 199 if ((errors.length || diagnostics.length) && this.options.translateBuiltins)
{ | 238 if ((errors.length || diagnostics.length)) { |
| 200 // Only report semantic diagnostics if ts2dart failed; this code is not a
generic compiler, so | 239 // Only report semantic diagnostics if facade generation failed; this |
| 201 // only yields TS errors if they could be the cause of ts2dart issues. | 240 // code is not a generic compiler, so only yields TS errors if they could |
| 241 // be the cause of facade generation issues. |
| 202 // This greatly speeds up tests and execution. | 242 // This greatly speeds up tests and execution. |
| 203 diagnostics = diagnostics.concat(program.getSemanticDiagnostics()); | 243 diagnostics = diagnostics.concat(program.getSemanticDiagnostics()); |
| 204 } | 244 } |
| 205 | 245 |
| 206 let diagnosticErrs = diagnostics.map((d) => { | 246 let diagnosticErrs = diagnostics.map((d) => { |
| 207 let msg = ''; | 247 let msg = ''; |
| 208 if (d.file) { | 248 if (d.file) { |
| 209 let pos = d.file.getLineAndCharacterOfPosition(d.start); | 249 let pos = d.file.getLineAndCharacterOfPosition(d.start); |
| 210 let fn = this.getRelativeFileName(d.file.fileName); | 250 let fn = this.getRelativeFileName(d.file.fileName); |
| 211 msg += ` ${fn}:${pos.line + 1}:${pos.character + 1}`; | 251 msg += ` ${fn}:${pos.line + 1}:${pos.character + 1}`; |
| 212 } | 252 } |
| 213 msg += ': '; | 253 msg += ': '; |
| 214 msg += ts.flattenDiagnosticMessageText(d.messageText, '\n'); | 254 msg += ts.flattenDiagnosticMessageText(d.messageText, '\n'); |
| 215 return msg; | 255 return msg; |
| 216 }); | 256 }); |
| 217 if (diagnosticErrs.length) errors = errors.concat(diagnosticErrs); | 257 if (diagnosticErrs.length) errors = errors.concat(diagnosticErrs); |
| 218 | 258 |
| 219 if (errors.length) { | 259 if (errors.length) { |
| 220 let e = new Error(errors.join('\n')); | 260 let e = new Error(errors.join('\n')); |
| 221 e.name = 'TS2DartError'; | 261 e.name = 'DartFacadeError'; |
| 222 throw e; | 262 throw e; |
| 223 } | 263 } |
| 224 } | 264 } |
| 225 | 265 |
| 226 /** | 266 /** |
| 227 * Returns `filePath`, relativized to the program's `basePath`. | 267 * Returns `filePath`, relativized to the program's `basePath`. |
| 228 * @param filePath Optional path to relativize, defaults to the current file's
path. | 268 * @param filePath Optional path to relativize, defaults to the current file's
path. |
| 229 */ | 269 */ |
| 230 getRelativeFileName(filePath?: string) { | 270 getRelativeFileName(filePath?: string) { |
| 231 if (filePath === undefined) filePath = path.resolve(this.currentFile.fileNam
e); | 271 if (filePath === undefined) filePath = path.resolve(this.currentFile.fileNam
e); |
| 232 // TODO(martinprobst): Use path.isAbsolute on node v0.12. | 272 // TODO(martinprobst): Use path.isAbsolute on node v0.12. |
| 233 if (this.normalizeSlashes(path.resolve('/x/', filePath)) !== filePath) { | 273 if (this.normalizeSlashes(path.resolve('/x/', filePath)) !== filePath) { |
| 234 return filePath; // already relative. | 274 return filePath; // already relative. |
| 235 } | 275 } |
| 236 let base = this.options.basePath || ''; | 276 let base = this.options.basePath || ''; |
| 237 if (filePath.indexOf(base) !== 0 && !filePath.match(/\.d\.ts$/)) { | 277 if (filePath.indexOf(base) !== 0 && !filePath.match(/\.d\.ts$/)) { |
| 238 throw new Error(`Files must be located under base, got ${filePath} vs ${ba
se}`); | 278 throw new Error(`Files must be located under base, got ${filePath} vs ${ba
se}`); |
| 239 } | 279 } |
| 240 return this.normalizeSlashes(path.relative(base, filePath)); | 280 return this.normalizeSlashes(path.relative(base, filePath)); |
| 241 } | 281 } |
| 242 | 282 |
| 243 emit(s: string) { this.output.emit(s); } | 283 private get currentOutput(): Output { return this.outputStack[this.outputStack
.length - 1]; } |
| 244 emitNoSpace(s: string) { this.output.emitNoSpace(s); } | 284 |
| 285 emit(s: string) { this.currentOutput.emit(s); } |
| 286 emitNoSpace(s: string) { this.currentOutput.emitNoSpace(s); } |
| 287 maybeLineBreak() { return this.currentOutput.maybeLineBreak(); } |
| 288 enterCodeComment() { return this.currentOutput.enterCodeComment(); } |
| 289 exitCodeComment() { return this.currentOutput.exitCodeComment(); } |
| 290 emitType(s: string, comment: string) { return this.currentOutput.emitType(s, c
omment); } |
| 291 get insideCodeComment() { return this.currentOutput.insideCodeComment; } |
| 245 | 292 |
| 246 reportError(n: ts.Node, message: string) { | 293 reportError(n: ts.Node, message: string) { |
| 247 let file = n.getSourceFile() || this.currentFile; | 294 let file = n.getSourceFile() || this.currentFile; |
| 248 let fileName = this.getRelativeFileName(file.fileName); | 295 let fileName = this.getRelativeFileName(file.fileName); |
| 249 let start = n.getStart(file); | 296 let start = n.getStart(file); |
| 250 let pos = file.getLineAndCharacterOfPosition(start); | 297 let pos = file.getLineAndCharacterOfPosition(start); |
| 251 // Line and character are 0-based. | 298 // Line and character are 0-based. |
| 252 let fullMessage = `${fileName}:${pos.line + 1}:${pos.character + 1}: ${messa
ge}`; | 299 let fullMessage = `${fileName}:${pos.line + 1}:${pos.character + 1}: ${messa
ge}`; |
| 253 if (this.options.failFast) throw new Error(fullMessage); | 300 if (this.options.failFast) throw new Error(fullMessage); |
| 254 this.errors.push(fullMessage); | 301 this.errors.push(fullMessage); |
| 255 } | 302 } |
| 256 | 303 |
| 257 visit(node: ts.Node) { | 304 visit(node: ts.Node) { |
| 258 this.output.addSourceMapping(node); | 305 if (!node) return; |
| 259 let comments = ts.getLeadingCommentRanges(this.currentFile.text, node.getFul
lStart()); | 306 let comments = ts.getLeadingCommentRanges(this.currentFile.text, node.getFul
lStart()); |
| 260 if (comments) { | 307 if (comments) { |
| 261 comments.forEach((c) => { | 308 comments.forEach((c) => { |
| 309 // Warning: the following check means that comments will only be |
| 310 // emitted correctly if Dart code is emitted in the same order it |
| 311 // appeared in the JavaScript AST. |
| 262 if (c.pos <= this.lastCommentIdx) return; | 312 if (c.pos <= this.lastCommentIdx) return; |
| 263 this.lastCommentIdx = c.pos; | 313 this.lastCommentIdx = c.pos; |
| 264 let text = this.currentFile.text.substring(c.pos, c.end); | 314 let text = this.currentFile.text.substring(c.pos, c.end); |
| 265 this.emitNoSpace('\n'); | 315 if (c.pos > 1) { |
| 266 this.emit(this.translateComment(text)); | 316 let prev = this.currentFile.text.substring(c.pos - 2, c.pos); |
| 267 if (c.hasTrailingNewLine) this.emitNoSpace('\n'); | 317 if (prev === '\n\n') { |
| 318 // If the two previous characters are both \n then add a \n |
| 319 // so that we ensure the output has sufficient line breaks to |
| 320 // separate comment blocks. |
| 321 this.currentOutput.emit('\n'); |
| 322 } |
| 323 } |
| 324 this.currentOutput.emitComment(this.translateComment(text)); |
| 268 }); | 325 }); |
| 269 } | 326 } |
| 270 | 327 |
| 271 for (let i = 0; i < this.transpilers.length; i++) { | 328 for (let i = 0; i < this.transpilers.length; i++) { |
| 272 if (this.transpilers[i].visitNode(node)) return; | 329 if (this.transpilers[i].visitNode(node)) return; |
| 273 } | 330 } |
| 274 | 331 |
| 275 this.reportError( | 332 this.reportError( |
| 276 node, | 333 node, |
| 277 'Unsupported node type ' + (<any>ts).SyntaxKind[node.kind] + ': ' + node
.getFullText()); | 334 'Unsupported node type ' + (<any>ts).SyntaxKind[node.kind] + ': ' + node
.getFullText()); |
| 278 } | 335 } |
| 279 | 336 |
| 280 private normalizeSlashes(path: string) { return path.replace(/\\/g, '/'); } | 337 private normalizeSlashes(path: string) { return path.replace(/\\/g, '/'); } |
| 281 | 338 |
| 282 private translateComment(comment: string): string { | 339 private translateComment(comment: string): string { |
| 340 let rawComment = comment; |
| 283 comment = comment.replace(/\{@link ([^\}]+)\}/g, '[$1]'); | 341 comment = comment.replace(/\{@link ([^\}]+)\}/g, '[$1]'); |
| 284 | 342 |
| 285 // Remove the following tags and following comments till end of line. | 343 // Remove the following tags and following comments till end of line. |
| 286 comment = comment.replace(/@param.*$/gm, ''); | 344 comment = comment.replace(/@param.*$/gm, ''); |
| 287 comment = comment.replace(/@throws.*$/gm, ''); | 345 comment = comment.replace(/@throws.*$/gm, ''); |
| 288 comment = comment.replace(/@return.*$/gm, ''); | 346 comment = comment.replace(/@return.*$/gm, ''); |
| 289 | 347 |
| 290 // Remove the following tags. | 348 // Remove the following tags. |
| 291 comment = comment.replace(/@module/g, ''); | 349 comment = comment.replace(/@module/g, ''); |
| 292 comment = comment.replace(/@description/g, ''); | 350 comment = comment.replace(/@description/g, ''); |
| 293 comment = comment.replace(/@deprecated/g, ''); | 351 comment = comment.replace(/@deprecated/g, ''); |
| 294 | 352 |
| 295 return comment; | 353 // Switch to /* */ comments and // comments to /// |
| 354 let sb = ''; |
| 355 for (let line of comment.split('\n')) { |
| 356 line = line.trim(); |
| 357 line = line.replace(/^[\/]\*\*?/g, ''); |
| 358 line = line.replace(/\*[\/]$/g, ''); |
| 359 line = line.replace(/^\*/g, ''); |
| 360 line = line.replace(/^\/\/\/?/g, ''); |
| 361 line = line.trim(); |
| 362 if (line.length > 0) { |
| 363 sb += ' /// ' + line + '\n'; |
| 364 } |
| 365 } |
| 366 if (rawComment[0] === '\n') sb = '\n' + sb; |
| 367 return sb; |
| 296 } | 368 } |
| 297 } | 369 } |
| 298 | 370 |
| 299 export function getModuleResolver(compilerHost: ts.CompilerHost) { | 371 export function getModuleResolver(compilerHost: ts.CompilerHost) { |
| 300 return (moduleNames: string[], containingFile: string): ts.ResolvedModule[] =>
{ | 372 return (moduleNames: string[], containingFile: string): ts.ResolvedModule[] =>
{ |
| 301 let res: ts.ResolvedModule[] = []; | 373 let res: ts.ResolvedModule[] = []; |
| 302 for (let mod of moduleNames) { | 374 for (let mod of moduleNames) { |
| 303 let lookupRes = | 375 let lookupRes = |
| 304 ts.nodeModuleNameResolver(mod, containingFile, COMPILER_OPTIONS, compi
lerHost); | 376 ts.nodeModuleNameResolver(mod, containingFile, COMPILER_OPTIONS, compi
lerHost); |
| 305 if (lookupRes.resolvedModule) { | 377 if (lookupRes.resolvedModule) { |
| 306 res.push(lookupRes.resolvedModule); | 378 res.push(lookupRes.resolvedModule); |
| 307 continue; | 379 continue; |
| 308 } | 380 } |
| 309 lookupRes = ts.classicNameResolver(mod, containingFile, COMPILER_OPTIONS,
compilerHost); | 381 lookupRes = ts.classicNameResolver(mod, containingFile, COMPILER_OPTIONS,
compilerHost); |
| 310 if (lookupRes.resolvedModule) { | 382 if (lookupRes.resolvedModule) { |
| 311 res.push(lookupRes.resolvedModule); | 383 res.push(lookupRes.resolvedModule); |
| 312 continue; | 384 continue; |
| 313 } | 385 } |
| 314 res.push(undefined); | 386 res.push(undefined); |
| 315 } | 387 } |
| 316 return res; | 388 return res; |
| 317 }; | 389 }; |
| 318 } | 390 } |
| 319 | 391 |
| 320 class Output { | 392 class Output { |
| 321 private result: string = ''; | 393 private result: string = ''; |
| 322 private column: number = 1; | 394 private firstColumn: boolean = true; |
| 323 private line: number = 1; | |
| 324 | 395 |
| 325 // Position information. | 396 insideCodeComment: boolean = false; |
| 326 private generateSourceMap: boolean; | 397 private codeCommentResult: string = ''; |
| 327 private sourceMap: SourceMapGenerator; | |
| 328 | 398 |
| 329 constructor( | 399 /** |
| 330 private currentFile: ts.SourceFile, private relativeFileName: string, | 400 * Line break if the current line is not empty. |
| 331 generateSourceMap: boolean) { | 401 */ |
| 332 if (generateSourceMap) { | 402 maybeLineBreak() { |
| 333 this.sourceMap = new SourceMapGenerator({file: relativeFileName + '.dart'}
); | 403 if (this.insideCodeComment) { |
| 334 this.sourceMap.setSourceContent(relativeFileName, this.currentFile.text); | 404 // Avoid line breaks inside code comments. |
| 405 return; |
| 406 } |
| 407 |
| 408 if (!this.firstColumn) { |
| 409 this.emitNoSpace('\n'); |
| 335 } | 410 } |
| 336 } | 411 } |
| 337 | 412 |
| 338 emit(str: string) { | 413 emit(str: string) { |
| 339 this.emitNoSpace(' '); | 414 if (this.result.length > 0) { |
| 415 let buffer = this.insideCodeComment ? this.codeCommentResult : this.result
; |
| 416 let lastChar = buffer[buffer.length - 1]; |
| 417 if (lastChar !== ' ' && lastChar !== '(' && lastChar !== '<' && lastChar !
== '[') { |
| 418 // Avoid emitting a space in obvious cases where a space is not required |
| 419 // to make the output slightly prettier in cases where the DartFormatter |
| 420 // cannot run such as within a comment where code we emit is not quite |
| 421 // valid Dart code. |
| 422 this.emitNoSpace(' '); |
| 423 } |
| 424 } |
| 340 this.emitNoSpace(str); | 425 this.emitNoSpace(str); |
| 341 } | 426 } |
| 342 | 427 |
| 343 emitNoSpace(str: string) { | 428 emitNoSpace(str: string) { |
| 429 if (str.length === 0) return; |
| 430 if (this.insideCodeComment) { |
| 431 this.codeCommentResult += str; |
| 432 return; |
| 433 } |
| 344 this.result += str; | 434 this.result += str; |
| 345 for (let i = 0; i < str.length; i++) { | 435 this.firstColumn = str[str.length - 1] === '\n'; |
| 346 if (str[i] === '\n') { | |
| 347 this.line++; | |
| 348 this.column = 0; | |
| 349 } else { | |
| 350 this.column++; | |
| 351 } | |
| 352 } | |
| 353 } | 436 } |
| 354 | 437 |
| 355 getResult(): string { return this.result + this.generateSourceMapComment(); } | 438 enterCodeComment() { |
| 356 | 439 if (this.insideCodeComment) { |
| 357 addSourceMapping(n: ts.Node) { | 440 throw 'Cannot nest code comments' + this.codeCommentResult; |
| 358 if (!this.generateSourceMap) return; // source maps disabled. | 441 } |
| 359 let file = n.getSourceFile() || this.currentFile; | 442 this.insideCodeComment = true; |
| 360 let start = n.getStart(file); | 443 this.codeCommentResult = ''; |
| 361 let pos = file.getLineAndCharacterOfPosition(start); | |
| 362 | |
| 363 let mapping: SourceMap.Mapping = { | |
| 364 original: {line: pos.line + 1, column: pos.character}, | |
| 365 generated: {line: this.line, column: this.column}, | |
| 366 source: this.relativeFileName, | |
| 367 }; | |
| 368 | |
| 369 this.sourceMap.addMapping(mapping); | |
| 370 } | 444 } |
| 371 | 445 |
| 372 private generateSourceMapComment() { | 446 emitType(s: string, comment: string) { |
| 373 if (!this.sourceMap) return ''; | 447 this.emit(base.formatType(s, comment, this.insideCodeComment)); |
| 374 let base64map = new Buffer(JSON.stringify(this.sourceMap)).toString('base64'
); | 448 } |
| 375 return '\n\n//# sourceMappingURL=data:application/json;base64,' + base64map; | 449 |
| 450 /** |
| 451 * Always emit comments in the main program body outside of the existing code |
| 452 * comment block. |
| 453 */ |
| 454 emitComment(s: string) { |
| 455 if (!this.firstColumn) { |
| 456 this.result += '\n'; |
| 457 } |
| 458 this.result += s; |
| 459 this.firstColumn = true; |
| 460 } |
| 461 |
| 462 exitCodeComment() { |
| 463 if (!this.insideCodeComment) { |
| 464 throw 'Exit code comment called while not within a code comment.'; |
| 465 } |
| 466 this.insideCodeComment = false; |
| 467 this.emitNoSpace(' /*'); |
| 468 let result = dartStyle.formatCode(this.codeCommentResult); |
| 469 let code = this.codeCommentResult; |
| 470 if (!result.error) { |
| 471 code = result.code; |
| 472 } |
| 473 code = code.trim(); |
| 474 this.emitNoSpace(code); |
| 475 this.emitNoSpace('*/'); |
| 476 |
| 477 // Don't really need an exact column, just need to track |
| 478 // that we aren't on the first column. |
| 479 this.firstColumn = false; |
| 480 this.codeCommentResult = ''; |
| 481 } |
| 482 |
| 483 getResult(): string { |
| 484 if (this.insideCodeComment) { |
| 485 throw 'Code comment not property terminated.'; |
| 486 } |
| 487 return this.result; |
| 376 } | 488 } |
| 377 } | 489 } |
| 378 | 490 |
| 379 // CLI entry point | 491 // CLI entry point |
| 380 if (require.main === module) { | 492 if (require.main === module) { |
| 381 let args = require('minimist')(process.argv.slice(2), {base: 'string'}); | 493 let args = require('minimist')(process.argv.slice(2), {base: 'string'}); |
| 382 try { | 494 try { |
| 383 let transpiler = new Transpiler(args); | 495 let transpiler = new Transpiler(args); |
| 384 console.error('Transpiling', args._, 'to', args.destination); | 496 console.error('Transpiling', args._, 'to', args.destination); |
| 385 transpiler.transpile(args._, args.destination); | 497 transpiler.transpile(args._, args.destination); |
| 386 } catch (e) { | 498 } catch (e) { |
| 387 if (e.name !== 'TS2DartError') throw e; | 499 if (e.name !== 'DartFacadeError') throw e; |
| 388 console.error(e.message); | 500 console.error(e.message); |
| 389 process.exit(1); | 501 process.exit(1); |
| 390 } | 502 } |
| 391 } | 503 } |
| OLD | NEW |