ts2dart
Version:
Transpile TypeScript code to Dart
167 lines (156 loc) • 6.19 kB
text/typescript
import * as ts from 'typescript';
import * as base from './base';
import {FacadeConverter} from './facade_converter';
import {Transpiler} from './main';
export default class ModuleTranspiler extends base.TranspilerBase {
constructor(tr: Transpiler, private fc: FacadeConverter, private generateLibraryName: boolean) {
super(tr);
}
visitNode(node: ts.Node): boolean {
switch (node.kind) {
case ts.SyntaxKind.SourceFile:
let sf = <ts.SourceFile>node;
if (this.generateLibraryName) {
this.emit('library');
this.emit(this.getLibraryName(sf.fileName));
this.emit(';');
}
this.fc.emitExtraImports(sf);
ts.forEachChild(sf, this.visit.bind(this));
break;
case ts.SyntaxKind.EndOfFileToken:
ts.forEachChild(node, this.visit.bind(this));
break;
case ts.SyntaxKind.ImportDeclaration:
let importDecl = <ts.ImportDeclaration>node;
if (importDecl.importClause) {
if (this.isEmptyImport(importDecl)) return true;
this.emit('import');
this.visitExternalModuleReferenceExpr(importDecl.moduleSpecifier);
this.visit(importDecl.importClause);
} else {
this.reportError(importDecl, 'bare import is unsupported');
}
this.emit(';');
break;
case ts.SyntaxKind.ImportClause:
let importClause = <ts.ImportClause>node;
if (importClause.name) this.fc.visitTypeName(importClause.name);
if (importClause.namedBindings) {
this.visit(importClause.namedBindings);
}
break;
case ts.SyntaxKind.NamespaceImport:
let nsImport = <ts.NamespaceImport>node;
this.emit('as');
this.fc.visitTypeName(nsImport.name);
break;
case ts.SyntaxKind.NamedImports:
this.emit('show');
let used = this.filterImports((<ts.NamedImports>node).elements);
if (used.length === 0) {
this.reportError(node, 'internal error, used imports must not be empty');
}
this.visitList(used);
break;
case ts.SyntaxKind.NamedExports:
let exportElements = (<ts.NamedExports>node).elements;
this.emit('show');
if (exportElements.length === 0) this.reportError(node, 'empty export list');
this.visitList((<ts.NamedExports>node).elements);
break;
case ts.SyntaxKind.ImportSpecifier:
case ts.SyntaxKind.ExportSpecifier:
let spec = <ts.ImportOrExportSpecifier>node;
if (spec.propertyName) {
this.reportError(spec.propertyName, 'import/export renames are unsupported in Dart');
}
this.fc.visitTypeName(spec.name);
break;
case ts.SyntaxKind.ExportDeclaration:
let exportDecl = <ts.ExportDeclaration>node;
this.emit('export');
if (exportDecl.moduleSpecifier) {
this.visitExternalModuleReferenceExpr(exportDecl.moduleSpecifier);
} else {
this.reportError(node, 're-exports must have a module URL (export x from "./y").');
}
if (exportDecl.exportClause) this.visit(exportDecl.exportClause);
this.emit(';');
break;
case ts.SyntaxKind.ImportEqualsDeclaration:
let importEqDecl = <ts.ImportEqualsDeclaration>node;
this.emit('import');
this.visit(importEqDecl.moduleReference);
this.emit('as');
this.fc.visitTypeName(importEqDecl.name);
this.emit(';');
break;
case ts.SyntaxKind.ExternalModuleReference:
this.visitExternalModuleReferenceExpr((<ts.ExternalModuleReference>node).expression);
break;
default:
return false;
}
return true;
}
private static isIgnoredImport(e: ts.ImportSpecifier) {
// TODO: unify with facade_converter.ts
let name = base.ident(e.name);
switch (name) {
case 'CONST':
case 'CONST_EXPR':
case 'normalizeBlank':
case 'forwardRef':
case 'ABSTRACT':
case 'IMPLEMENTS':
return true;
default:
return false;
}
}
private visitExternalModuleReferenceExpr(expr: ts.Expression) {
// TODO: what if this isn't a string literal?
let moduleName = <ts.StringLiteral>expr;
let text = moduleName.text;
if (text.match(/^\.\//)) {
// Strip './' to be more Dart-idiomatic.
text = text.substring(2);
} else if (!text.match(/^\.\.\//)) {
// Replace '@angular' with 'angular2' for Dart.
text = text.replace(/^ \//, 'angular2/');
// Unprefixed/absolute imports are package imports.
text = 'package:' + text;
}
this.emit(JSON.stringify(text + '.dart'));
}
private isEmptyImport(n: ts.ImportDeclaration): boolean {
let bindings = n.importClause.namedBindings;
if (bindings.kind !== ts.SyntaxKind.NamedImports) return false;
let elements = (<ts.NamedImports>bindings).elements;
// An import list being empty *after* filtering is ok, but if it's empty in the code itself,
// it's nonsensical code, so probably a programming error.
if (elements.length === 0) this.reportError(n, 'empty import list');
return elements.every(ModuleTranspiler.isIgnoredImport);
}
private filterImports(ns: ts.ImportOrExportSpecifier[]) {
return ns.filter((e) => !ModuleTranspiler.isIgnoredImport(e));
}
// For the Dart keyword list see
// https://www.dartlang.org/docs/dart-up-and-running/ch02.html#keywords
private static DART_RESERVED_WORDS =
('assert break case catch class const continue default do else enum extends false final ' +
'finally for if in is new null rethrow return super switch this throw true try let void ' +
'while with')
.split(/ /);
getLibraryName(fileName: string) {
fileName = this.getRelativeFileName(fileName);
let parts = fileName.split('/');
return parts.filter((p) => p.length > 0)
.map((p) => p.replace(/^@/, ''))
.map((p) => p.replace(/[^\w.]/g, '_'))
.map((p) => p.replace(/\.[jt]s$/g, ''))
.map((p) => ModuleTranspiler.DART_RESERVED_WORDS.indexOf(p) !== -1 ? '_' + p : p)
.join('.');
}
}