| 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);
|
| }
|
|
|