Comparing version 0.1.2 to 0.1.3
import * as ts from 'typescript'; | ||
export declare function convertDecorators(fileName: string, sourceText: string): { | ||
export declare function convertDecorators(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): { | ||
output: string; | ||
diagnostics: ts.Diagnostic[]; | ||
}; |
@@ -37,2 +37,4 @@ import * as ts from 'typescript'; | ||
emit(str: string): void; | ||
/** Removes comment metacharacters from a string, to make it safe to embed in a comment. */ | ||
escapeForComment(str: string): string; | ||
logWithIndent(message: string): void; | ||
@@ -39,0 +41,0 @@ /** |
@@ -14,2 +14,10 @@ import * as ts from 'typescript'; | ||
/** | ||
* Converts a ts.Symbol to a string. | ||
* Other approaches that don't work: | ||
* - TypeChecker.typeToString translates Array as T[]. | ||
* - TypeChecker.symbolToString emits types without their namespace, | ||
* and doesn't let you pass the flag to control that. | ||
*/ | ||
private symbolToString(sym); | ||
/** | ||
* @param notNull When true, insert a ! before any type references. This | ||
@@ -16,0 +24,0 @@ * is to work around the difference between TS and Closure destructuring. |
@@ -14,4 +14,5 @@ "use strict"; | ||
__extends(ClassRewriter, _super); | ||
function ClassRewriter() { | ||
_super.apply(this, arguments); | ||
function ClassRewriter(typeChecker, sourceFile) { | ||
_super.call(this, sourceFile); | ||
this.typeChecker = typeChecker; | ||
} | ||
@@ -59,13 +60,7 @@ /** | ||
if (param.type) { | ||
switch (param.type.kind) { | ||
case ts.SyntaxKind.TypeReference: | ||
var typeRef = param.type; | ||
// Type reference can be a bare name or a qualified name (foo.bar), | ||
// possibly followed by type arguments (<X, Y>). | ||
// We are making the assumption that a type reference is the same | ||
// name as a ctor for that type, and it's simplest to just use the | ||
// source text. We use `typeName` to avoid emitting type parameters. | ||
paramCtor = typeRef.typeName.getText(); | ||
break; | ||
default: | ||
// param has a type provided, e.g. "foo: Bar". | ||
// Verify that "Bar" is a value (e.g. a constructor) and not just a type. | ||
var sym = this.typeChecker.getTypeAtLocation(param.type).getSymbol(); | ||
if (sym && (sym.flags & ts.SymbolFlags.Value)) { | ||
paramCtor = sym.name; | ||
} | ||
@@ -112,3 +107,3 @@ } | ||
// rewriter to gather+emit its metadata. | ||
var _a = new ClassRewriter(this.file).process(node), output = _a.output, diagnostics = _a.diagnostics; | ||
var _a = new ClassRewriter(this.typeChecker, this.file).process(node), output = _a.output, diagnostics = _a.diagnostics; | ||
(_b = this.diagnostics).push.apply(_b, diagnostics); | ||
@@ -139,2 +134,3 @@ this.emit(output); | ||
if (this.decorators) { | ||
this.emit("/** @nocollapse */\n"); | ||
this.emit("static decorators: DecoratorInvocation[] = [\n"); | ||
@@ -149,2 +145,3 @@ for (var _i = 0, _a = this.decorators; _i < _a.length; _i++) { | ||
if (this.ctorParameters) { | ||
this.emit("/** @nocollapse */\n"); | ||
this.emit("static ctorParameters: {type: Function, decorators?: DecoratorInvocation[]}[] = [\n"); | ||
@@ -173,2 +170,3 @@ for (var _b = 0, _c = this.ctorParameters; _b < _c.length; _b++) { | ||
if (this.propDecorators) { | ||
this.emit("/** @nocollapse */\n"); | ||
this.emit('static propDecorators: {[key: string]: DecoratorInvocation[]} = {\n'); | ||
@@ -220,4 +218,5 @@ for (var _e = 0, _f = Object.keys(this.propDecorators); _e < _f.length; _e++) { | ||
__extends(DecoratorRewriter, _super); | ||
function DecoratorRewriter() { | ||
_super.apply(this, arguments); | ||
function DecoratorRewriter(typeChecker, sourceFile) { | ||
_super.call(this, sourceFile); | ||
this.typeChecker = typeChecker; | ||
} | ||
@@ -231,3 +230,3 @@ DecoratorRewriter.prototype.process = function () { | ||
case ts.SyntaxKind.ClassDeclaration: | ||
var _a = new ClassRewriter(this.file).process(node), output = _a.output, diagnostics = _a.diagnostics; | ||
var _a = new ClassRewriter(this.typeChecker, this.file).process(node), output = _a.output, diagnostics = _a.diagnostics; | ||
(_b = this.diagnostics).push.apply(_b, diagnostics); | ||
@@ -243,5 +242,4 @@ this.emit(output); | ||
}(rewriter_1.Rewriter)); | ||
function convertDecorators(fileName, sourceText) { | ||
var file = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.ES5, true); | ||
return new DecoratorRewriter(file).process(); | ||
function convertDecorators(typeChecker, sourceFile) { | ||
return new DecoratorRewriter(typeChecker, sourceFile).process(); | ||
} | ||
@@ -248,0 +246,0 @@ exports.convertDecorators = convertDecorators; |
@@ -9,9 +9,9 @@ "use strict"; | ||
/** | ||
* File name for the generated Closure externs. | ||
* Internal file name for the generated Closure externs. | ||
* This is never written to disk, but should be unique with respect to | ||
* the js files that may actually exist. | ||
*/ | ||
var externsFileName = 'tsickle_externs.js'; | ||
var internalExternsFileName = 'tsickle_externs.js'; | ||
function usage() { | ||
console.error("usage: tsickle [tsickle args] -- [tsc args]\n\nexample:\n tsickle --output bundle.js -- -p src --noImplicitAny\n\ntsickle flags are:\n --saveTemporaries save intermediate .js files to disk\n --output=PATH write final Closure bundle to PATH\n"); | ||
console.error("usage: tsickle [tsickle args] -- [tsc args]\n\nexample:\n tsickle --output bundle.js -- -p src --noImplicitAny\n\ntsickle flags are:\n --saveTemporaries save intermediate .js files to disk\n --externs=PATH save generated Closure externs.js to PATH\n --output=PATH write final Closure bundle to PATH\n"); | ||
} | ||
@@ -25,3 +25,3 @@ /** | ||
saveTemporaries: null, | ||
outputPath: null | ||
outputPath: null, | ||
}; | ||
@@ -40,2 +40,5 @@ var parsedArgs = minimist(args); | ||
break; | ||
case 'externs': | ||
settings.externsPath = parsedArgs[flag]; | ||
break; | ||
case 'output': | ||
@@ -53,4 +56,4 @@ settings.outputPath = parsedArgs[flag]; | ||
} | ||
if (!settings.outputPath) { | ||
console.error('must specify --output path'); | ||
if (!settings.outputPath && !settings.externsPath) { | ||
console.error('must specify --output or --externs path'); | ||
usage(); | ||
@@ -110,3 +113,3 @@ process.exit(1); | ||
readFile: delegate.readFile, | ||
directoryExists: delegate.directoryExists | ||
directoryExists: delegate.directoryExists, | ||
}; | ||
@@ -139,3 +142,3 @@ function getSourceFile(fileName, languageVersion, onError) { | ||
var tsickleOptions = { | ||
untyped: true | ||
untyped: true, | ||
}; | ||
@@ -165,6 +168,3 @@ // Process each input file with tsickle and save the output. | ||
// Emit, creating a map of fileName => generated JS source. | ||
var jsFiles = (_b = {}, | ||
_b[externsFileName] = tsickleExterns, | ||
_b | ||
); | ||
var jsFiles = {}; | ||
function writeFile(fileName, data) { jsFiles[fileName] = data; } | ||
@@ -180,9 +180,9 @@ var diagnostics = program.emit(undefined, writeFile).diagnostics; | ||
} | ||
for (var _c = 0, _d = Object.keys(jsFiles); _c < _d.length; _c++) { | ||
var fileName = _d[_c]; | ||
for (var _b = 0, _c = Object.keys(jsFiles); _b < _c.length; _b++) { | ||
var fileName = _c[_b]; | ||
var output = tsickle.convertCommonJsToGoogModule(fileName, jsFiles[fileName], pathToModuleName).output; | ||
jsFiles[fileName] = output; | ||
} | ||
jsFiles[internalExternsFileName] = tsickleExterns; | ||
return { jsFiles: jsFiles }; | ||
var _b; | ||
} | ||
@@ -197,3 +197,3 @@ function closureCompile(jsFiles, outFile, callback) { | ||
'language_in': 'ECMASCRIPT6_STRICT', | ||
'compilation_level': 'ADVANCED_OPTIMIZATIONS' | ||
'compilation_level': 'ADVANCED_OPTIMIZATIONS', | ||
}; | ||
@@ -207,3 +207,3 @@ var compiler = new closure.compiler(closureOptions); | ||
path: fileName, | ||
src: jsFiles[fileName] | ||
src: jsFiles[fileName], | ||
}); | ||
@@ -237,8 +237,13 @@ } | ||
} | ||
// Run Closure compiiler to convert JS files to output bundle. | ||
closureCompile(jsFiles, settings.outputPath, function (exitCode, output) { | ||
if (output) | ||
console.error(output); | ||
process.exit(exitCode); | ||
}); | ||
if (settings.externsPath) { | ||
fs.writeFileSync(settings.externsPath, jsFiles[internalExternsFileName]); | ||
} | ||
if (settings.outputPath) { | ||
// Run Closure compiiler to convert JS files to output bundle. | ||
closureCompile(jsFiles, settings.outputPath, function (exitCode, output) { | ||
if (output) | ||
console.error(output); | ||
process.exit(exitCode); | ||
}); | ||
} | ||
var _c; | ||
@@ -245,0 +250,0 @@ } |
@@ -83,2 +83,6 @@ "use strict"; | ||
Rewriter.prototype.emit = function (str) { this.output.push(str); }; | ||
/** Removes comment metacharacters from a string, to make it safe to embed in a comment. */ | ||
Rewriter.prototype.escapeForComment = function (str) { | ||
return str.replace(/\/\*/g, '__').replace(/\*\//g, '__'); | ||
}; | ||
/* tslint:disable: no-unused-variable */ | ||
@@ -107,3 +111,3 @@ Rewriter.prototype.logWithIndent = function (message) { | ||
category: ts.DiagnosticCategory.Error, | ||
code: undefined | ||
code: undefined, | ||
}); | ||
@@ -110,0 +114,0 @@ }; |
@@ -93,2 +93,17 @@ "use strict"; | ||
exports.getJSDocAnnotation = getJSDocAnnotation; | ||
function getIdentifierText(identifier) { | ||
// NOTE: the 'text' property on an Identifier may be escaped if it starts | ||
// with '__', so just use getText(). | ||
return identifier.getText(); | ||
} | ||
/** | ||
* Converts an escaped TypeScript name into the original source name. | ||
* Prefer getIdentifierText() instead if possible. | ||
*/ | ||
function unescapeName(name) { | ||
// See the private function unescapeIdentifier in TypeScript's utilities.ts. | ||
if (name.match(/^___/)) | ||
return name.substr(1); | ||
return name; | ||
} | ||
var VISIBILITY_FLAGS = ts.NodeFlags.Private | ts.NodeFlags.Protected | ts.NodeFlags.Public; | ||
@@ -116,3 +131,3 @@ /** | ||
if (this.externsOutput.length > 0) { | ||
externs = '/** @externs */\n' + this.externsOutput.join(''); | ||
externs = "/** @externs */\n// NOTE: generated by tsickle, do not edit.\n" + this.externsOutput.join(''); | ||
} | ||
@@ -123,3 +138,3 @@ var _a = this.getOutput(), output = _a.output, diagnostics = _a.diagnostics; | ||
externs: externs, | ||
diagnostics: diagnostics | ||
diagnostics: diagnostics, | ||
}; | ||
@@ -142,2 +157,4 @@ }; | ||
switch (node.kind) { | ||
case ts.SyntaxKind.ImportDeclaration: | ||
return this.emitImportDeclaration(node); | ||
case ts.SyntaxKind.ExportDeclaration: | ||
@@ -156,2 +173,4 @@ var exportDecl = node; | ||
case ts.SyntaxKind.InterfaceDeclaration: | ||
this.emitInterface(node); | ||
// Emit the TS interface verbatim, with no tsickle processing of properties. | ||
this.writeRange(node.getFullStart(), node.getEnd()); | ||
@@ -248,2 +267,9 @@ return true; | ||
return true; | ||
case ts.SyntaxKind.JsxText: | ||
// TypeScript seems to accidentally include one extra token of | ||
// text in each JSX text node as a child. Avoid it here by just | ||
// emitting the node's text rather than visiting its children. | ||
// https://github.com/angular/tsickle/issues/76 | ||
this.emit(node.getFullText()); | ||
return true; | ||
default: | ||
@@ -254,2 +280,9 @@ break; | ||
}; | ||
/** | ||
* Given a "export * from ..." statement, rewrites it to instead "export {foo, bar, baz} from | ||
* ...". | ||
* This is necessary because TS transpiles "export *" by just doing a runtime loop | ||
* over the target module's exports, which means Closure won't see the declarations/types | ||
* that are exported. | ||
*/ | ||
Annotator.prototype.expandSymbolsFromExportStar = function (exportDecl) { | ||
@@ -274,3 +307,3 @@ var typeChecker = this.program.getTypeChecker(); | ||
// Expand the export list, then filter it to the symbols we want | ||
// to reexport | ||
// to reexport. | ||
var exports = typeChecker.getExportsOfModule(typeChecker.getSymbolAtLocation(exportDecl.moduleSpecifier)); | ||
@@ -280,3 +313,3 @@ var reexports = {}; | ||
var sym = exports_2[_a]; | ||
var name_1 = sym.name; | ||
var name_1 = unescapeName(sym.name); | ||
if (localSet.hasOwnProperty(name_1)) { | ||
@@ -297,2 +330,89 @@ // This name is shadowed by a local definition, such as: | ||
}; | ||
/** | ||
* Handles emit of an "import ..." statement. | ||
* We need to do a bit of rewriting so that imported types show up under the | ||
* correct name in JSDoc. | ||
* @return true if the decl was handled, false to allow default processing. | ||
*/ | ||
Annotator.prototype.emitImportDeclaration = function (decl) { | ||
if (this.options.untyped) | ||
return false; | ||
var importClause = decl.importClause; | ||
if (!importClause) { | ||
// import './foo'; | ||
return false; // Use default processing. | ||
} | ||
else if (importClause.name) { | ||
// import foo from ...; | ||
this.errorUnimplementedKind(decl, 'TODO: default import'); | ||
return false; // Use default processing. | ||
} | ||
else if (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { | ||
// import * as foo from ...; | ||
return false; // Use default processing. | ||
} | ||
else if (importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) { | ||
// The statement looks like "import {a as b} from ...;". | ||
// | ||
// If a is a type, rewrite it to: | ||
// import {a as tsickle_b} from ...; | ||
// const b = tsickle_b; | ||
// type b = tsickle_b; | ||
// This is because in the generated ES5, the variable chosen in the import statement | ||
// (here, "tsickle_b") will instead get a generated name like module_import.tsickle_b. | ||
// But in our JSDoc we still refer to it as just "b". Importing under a different | ||
// name and then making a local alias evades this. | ||
// (TS uses the namespace-qualified name so that updates to the | ||
// module are reflected in the aliases. We only do this to types, which can't | ||
// change, so the namespace qualification is not needed.) | ||
var namedImports = importClause.namedBindings; | ||
var typeChecker = this.program.getTypeChecker(); | ||
this.writeRange(decl.getFullStart(), decl.getStart()); | ||
// Emit the "import" part, with rewritten binding names for types. | ||
this.emit('import {'); | ||
var renamed = []; | ||
for (var _i = 0, _a = namedImports.elements; _i < _a.length; _i++) { | ||
var imp = _a[_i]; | ||
// imp.propertyName is the name of the property in the module we're importing from, | ||
// while imp.name is the local name. propertyName is undefined in the case where | ||
// it's not being renamed as it's imported. | ||
this.emit(getIdentifierText(imp.propertyName || imp.name)); | ||
// Rename it if the symbol references a type. | ||
var name_2 = getIdentifierText(imp.name); | ||
var sym = typeChecker.getSymbolAtLocation(imp.name); | ||
var isType = (typeChecker.getAliasedSymbol(sym).flags & ts.SymbolFlags.Type) !== 0; | ||
if (isType) { | ||
renamed.push(sym); | ||
this.emit(" as tsickle_" + name_2); | ||
} | ||
else if (imp.propertyName) { | ||
this.emit(" as " + name_2); | ||
} | ||
this.emit(','); | ||
} | ||
this.emit('} from'); | ||
this.writeNode(decl.moduleSpecifier); | ||
this.emit(';\n'); | ||
// Emit aliases that bring the renamed names back to the local names. | ||
for (var _b = 0, renamed_1 = renamed; _b < renamed_1.length; _b++) { | ||
var sym = renamed_1[_b]; | ||
var name_3 = unescapeName(sym.name); | ||
// Note that tsickle_foo types are eventually always both values and types, because | ||
// post-tsickle processing (in Closure-land), all types are also values. | ||
var isValue = (typeChecker.getAliasedSymbol(sym).flags & ts.SymbolFlags.Value) !== 0; | ||
if (!isValue) { | ||
// Once tsickle processes the other module, it will create a var for it. | ||
// But until then we just hack this symbol in to the value namespace. | ||
this.emit("declare var tsickle_" + name_3 + ";\n"); | ||
} | ||
this.emit("const " + name_3 + " = tsickle_" + name_3 + ";\n"); | ||
this.emit("type " + name_3 + " = tsickle_" + name_3 + ";\n"); | ||
} | ||
return true; | ||
} | ||
else { | ||
this.errorUnimplementedKind(decl, 'unexpected kind of import'); | ||
return false; // Use default processing. | ||
} | ||
}; | ||
Annotator.prototype.emitFunctionType = function (fnDecl, extraTags) { | ||
@@ -326,3 +446,3 @@ if (extraTags === void 0) { extraTags = []; } | ||
optional: paramNode.initializer !== undefined || paramNode.questionToken !== undefined, | ||
parameterName: paramSym.name | ||
parameterName: unescapeName(paramSym.getName()), | ||
}; | ||
@@ -342,3 +462,3 @@ var destructuring = (paramNode.name.kind === ts.SyntaxKind.ArrayBindingPattern || | ||
var _d = _c[_b], tagName = _d.tagName, parameterName = _d.parameterName, text = _d.text; | ||
if (tagName === 'param' && parameterName === paramSym.name) { | ||
if (tagName === 'param' && parameterName === newTag.parameterName) { | ||
newTag.text = text; | ||
@@ -365,3 +485,3 @@ break; | ||
type: this.typeToClosure(fnDecl, retType), | ||
text: returnDoc | ||
text: returnDoc, | ||
}); | ||
@@ -400,2 +520,23 @@ } | ||
}; | ||
Annotator.prototype.emitInterface = function (iface) { | ||
if (this.options.untyped) | ||
return; | ||
this.emit("\n/** @record */\n"); | ||
var name = getIdentifierText(iface.name); | ||
this.emit("function " + name + "() {}\n"); | ||
if (iface.typeParameters) { | ||
this.emit("// TODO: type parameters.\n"); | ||
} | ||
if (iface.heritageClauses) { | ||
this.emit("// TODO: derived interfaces.\n"); | ||
} | ||
var memberNamespace = [name, 'prototype']; | ||
for (var _i = 0, _a = iface.members; _i < _a.length; _i++) { | ||
var elem = _a[_i]; | ||
this.visitProperty(memberNamespace, elem); | ||
} | ||
if (iface.flags & ts.NodeFlags.Export) { | ||
this.emit("export {" + name + "};\n"); | ||
} | ||
}; | ||
// emitTypeAnnotationsHelper produces a | ||
@@ -440,4 +581,4 @@ // _tsickle_typeAnnotationsHelper() where none existed in the | ||
this.emit('\n\n static _tsickle_typeAnnotationsHelper() {\n'); | ||
staticProps.forEach(function (p) { return _this.visitProperty([classDecl.name.text], p); }); | ||
var memberNamespace = [classDecl.name.text, 'prototype']; | ||
staticProps.forEach(function (p) { return _this.visitProperty([getIdentifierText(classDecl.name)], p); }); | ||
var memberNamespace = [getIdentifierText(classDecl.name), 'prototype']; | ||
nonStaticProps.forEach(function (p) { return _this.visitProperty(memberNamespace, p); }); | ||
@@ -447,3 +588,22 @@ paramProps.forEach(function (p) { return _this.visitProperty(memberNamespace, p); }); | ||
}; | ||
Annotator.prototype.propertyName = function (prop) { | ||
if (!prop.name) | ||
return null; | ||
switch (prop.name.kind) { | ||
case ts.SyntaxKind.Identifier: | ||
return getIdentifierText(prop.name); | ||
case ts.SyntaxKind.StringLiteral: | ||
// E.g. interface Foo { 'bar': number; } | ||
// If 'bar' is a name that is not valid in Closure then there's nothing we can do. | ||
return prop.name.text; | ||
default: | ||
return null; | ||
} | ||
}; | ||
Annotator.prototype.visitProperty = function (namespace, p) { | ||
var name = this.propertyName(p); | ||
if (!name) { | ||
this.emit("/* TODO: handle strange member:\n" + this.escapeForComment(p.getText()) + "\n*/\n"); | ||
return; | ||
} | ||
var jsDoc = this.getJSDoc(p) || { tags: [] }; | ||
@@ -465,3 +625,3 @@ var existingAnnotation = ''; | ||
this.emit(" @type {" + this.typeToClosure(p) + "} */\n"); | ||
namespace = namespace.concat([p.name.getText()]); | ||
namespace = namespace.concat([name]); | ||
this.emit(namespace.join('.') + ";\n"); | ||
@@ -498,3 +658,15 @@ }; | ||
// E.g. "declare namespace foo {" | ||
namespace = namespace.concat(decl.name.text); | ||
var name_4; | ||
switch (decl.name.kind) { | ||
case ts.SyntaxKind.Identifier: | ||
name_4 = getIdentifierText(decl.name); | ||
break; | ||
case ts.SyntaxKind.StringLiteral: | ||
name_4 = decl.name.text; | ||
break; | ||
default: | ||
this.errorUnimplementedKind(decl.name, 'namespace name'); | ||
break; | ||
} | ||
namespace = namespace.concat(name_4); | ||
var nsName = namespace.join('.'); | ||
@@ -544,4 +716,7 @@ if (!this.emittedNamespaces.hasOwnProperty(nsName)) { | ||
break; | ||
case ts.SyntaxKind.EnumDeclaration: | ||
this.writeExternsEnum(node, namespace); | ||
break; | ||
default: | ||
this.errorUnimplementedKind(node, 'externs generation'); | ||
this.emit("\n/* TODO: " + ts.SyntaxKind[node.kind] + " in " + namespace.join('.') + " */\n"); | ||
break; | ||
@@ -585,2 +760,3 @@ } | ||
break; | ||
case ts.SyntaxKind.MethodSignature: | ||
case ts.SyntaxKind.MethodDeclaration: | ||
@@ -597,5 +773,9 @@ var m = member; | ||
// interface Foo { [key: string]: number; } | ||
// For now, just die unless all the members are regular old | ||
// For now, just skip it unless all the members are regular old | ||
// properties. | ||
this.errorUnimplementedKind(member, 'externs for interface'); | ||
var name_5 = namespace; | ||
if (member.name) { | ||
name_5 = name_5.concat([member.name.getText()]); | ||
} | ||
this.emit("\n/* TODO: " + ts.SyntaxKind[member.kind] + " in " + name_5.join('.') + " */\n"); | ||
} | ||
@@ -607,3 +787,3 @@ } | ||
var identifier = decl.name; | ||
var qualifiedName = namespace.concat([identifier.text]).join('.'); | ||
var qualifiedName = namespace.concat([getIdentifierText(identifier)]).join('.'); | ||
if (exports.closureExternsBlacklist.indexOf(qualifiedName) >= 0) | ||
@@ -632,2 +812,14 @@ return; | ||
}; | ||
Annotator.prototype.writeExternsEnum = function (decl, namespace) { | ||
namespace = namespace.concat([getIdentifierText(decl.name)]); | ||
this.emit('\n/** @const */\n'); | ||
this.emit(namespace.join('.') + " = {};\n"); | ||
for (var _i = 0, _a = decl.members; _i < _a.length; _i++) { | ||
var member = _a[_i]; | ||
var memberName = member.name.getText(); | ||
var name_6 = namespace.concat([memberName]).join('.'); | ||
this.emit('/** @const {number} */\n'); | ||
this.emit(name_6 + ";\n"); | ||
} | ||
}; | ||
/** Emits a type annotation in JSDoc, or {?} if the type is unavailable. */ | ||
@@ -774,3 +966,3 @@ Annotator.prototype.emitJSDocType = function (node, additionalDocTag) { | ||
category: ts.DiagnosticCategory.Warning, | ||
code: undefined | ||
code: undefined, | ||
}; | ||
@@ -823,2 +1015,6 @@ this.options.logWarning(diagnostic); | ||
this.emit("goog.module('" + moduleName + "');"); | ||
// Allow code to use `module.id` to discover its module URL, e.g. to resolve | ||
// a template URL against. | ||
// Uses 'var', as this code is inserted in ES6 and ES5 modes. | ||
this.emit("var module = module || {id: '" + this.file.fileName + "'};"); | ||
var pos = 0; | ||
@@ -904,3 +1100,3 @@ for (var _i = 0, _a = this.file.statements; _i < _a.length; _i++) { | ||
return false; | ||
varName = decl.name.text; | ||
varName = getIdentifierText(decl.name); | ||
if (!decl.initializer || decl.initializer.kind !== ts.SyntaxKind.CallExpression) | ||
@@ -963,2 +1159,3 @@ return false; | ||
}; | ||
// workaround for syntax highlighting bug in Sublime: ` | ||
/** | ||
@@ -973,3 +1170,3 @@ * Returns the string argument if call is of the form | ||
var ident = call.expression; | ||
if (ident.text !== 'require') | ||
if (getIdentifierText(ident) !== 'require') | ||
return null; | ||
@@ -1013,8 +1210,8 @@ // Verify the call takes a single string argument and grab it. | ||
// module_name_var.default | ||
if (propAccess.name.text !== 'default') | ||
if (getIdentifierText(propAccess.name) !== 'default') | ||
break; | ||
if (propAccess.expression.kind !== ts.SyntaxKind.Identifier) | ||
break; | ||
var lhsIdent = propAccess.expression; | ||
if (!this.namespaceImports.hasOwnProperty(lhsIdent.text)) | ||
var lhs = getIdentifierText(propAccess.expression); | ||
if (!this.namespaceImports.hasOwnProperty(lhs)) | ||
break; | ||
@@ -1024,3 +1221,3 @@ // Emit the same expression, with spaces to replace the ".default" part | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
this.emit(lhsIdent.text + " "); | ||
this.emit(lhs + " "); | ||
return true; | ||
@@ -1027,0 +1224,0 @@ default: |
@@ -70,2 +70,35 @@ "use strict"; | ||
/** | ||
* Converts a ts.Symbol to a string. | ||
* Other approaches that don't work: | ||
* - TypeChecker.typeToString translates Array as T[]. | ||
* - TypeChecker.symbolToString emits types without their namespace, | ||
* and doesn't let you pass the flag to control that. | ||
*/ | ||
TypeTranslator.prototype.symbolToString = function (sym) { | ||
// This follows getSingleLineStringWriter in the TypeScript compiler. | ||
var str = ''; | ||
var writeText = function (text) { return str += text; }; | ||
var doNothing = function () { return; }; | ||
var builder = this.typeChecker.getSymbolDisplayBuilder(); | ||
var writer = { | ||
writeKeyword: writeText, | ||
writeOperator: writeText, | ||
writePunctuation: writeText, | ||
writeSpace: writeText, | ||
writeStringLiteral: writeText, | ||
writeParameter: writeText, | ||
writeSymbol: writeText, | ||
writeLine: doNothing, | ||
increaseIndent: doNothing, | ||
decreaseIndent: doNothing, | ||
clear: doNothing, | ||
trackSymbol: function (symbol, enclosingDeclaration, meaning) { | ||
return; | ||
}, | ||
reportInaccessibleThisError: doNothing, | ||
}; | ||
builder.buildSymbolDisplay(sym, writer, this.node); | ||
return str; | ||
}; | ||
/** | ||
* @param notNull When true, insert a ! before any type references. This | ||
@@ -103,2 +136,4 @@ * is to work around the difference between TS and Closure destructuring. | ||
default: | ||
// Continue on to more complex tests below. | ||
break; | ||
} | ||
@@ -114,3 +149,3 @@ var notNullPrefix = notNull ? '!' : ''; | ||
// the InterfaceType. | ||
return type.symbol.name; | ||
return this.symbolToString(type.symbol); | ||
} | ||
@@ -117,0 +152,0 @@ else if (type.flags & ts.TypeFlags.Reference) { |
@@ -23,2 +23,3 @@ require('source-map-support').install(); | ||
noEmitOnError: true, | ||
target: 'es5', | ||
// Specify the TypeScript version we're using. | ||
@@ -62,3 +63,4 @@ typescript: typescript, | ||
// Write external sourcemap next to the js file | ||
tsResult.js.pipe(sourcemaps.write('.')).pipe(gulp.dest('build/src')), | ||
tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: '../../src'})) | ||
.pipe(gulp.dest('build/src')), | ||
tsResult.js.pipe(gulp.dest('build/src')), | ||
@@ -73,7 +75,7 @@ ]); | ||
} | ||
return gulp.src(['test/*.ts', 'src/*.d.ts', 'typings/**/*.d.ts'], {base: '.'}) | ||
return gulp.src(['test/*.ts', 'typings/**/*.d.ts'], {base: '.'}) | ||
.pipe(sourcemaps.init()) | ||
.pipe(ts(tsProject)) | ||
.on('error', onError) | ||
.js.pipe(sourcemaps.write()) | ||
.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: '../..'})) | ||
.pipe(gulp.dest('build/')); // '/test/' comes from base above. | ||
@@ -80,0 +82,0 @@ }); |
{ | ||
"name": "tsickle", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"description": "Transpile TypeScript code to JavaScript with Closure annotations.", | ||
@@ -5,0 +5,0 @@ "main": "build/src/tsickle.js", |
@@ -15,2 +15,4 @@ import * as ts from 'typescript'; | ||
constructor(private typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) { super(sourceFile); } | ||
/** | ||
@@ -56,14 +58,7 @@ * process is the main entry point, rewriting a single class node. | ||
if (param.type) { | ||
switch (param.type.kind) { | ||
case ts.SyntaxKind.TypeReference: | ||
let typeRef = param.type as ts.TypeReferenceNode; | ||
// Type reference can be a bare name or a qualified name (foo.bar), | ||
// possibly followed by type arguments (<X, Y>). | ||
// We are making the assumption that a type reference is the same | ||
// name as a ctor for that type, and it's simplest to just use the | ||
// source text. We use `typeName` to avoid emitting type parameters. | ||
paramCtor = typeRef.typeName.getText(); | ||
break; | ||
default: | ||
// Some other type of type; just ignore it. | ||
// param has a type provided, e.g. "foo: Bar". | ||
// Verify that "Bar" is a value (e.g. a constructor) and not just a type. | ||
let sym = this.typeChecker.getTypeAtLocation(param.type).getSymbol(); | ||
if (sym && (sym.flags & ts.SymbolFlags.Value)) { | ||
paramCtor = sym.name; | ||
} | ||
@@ -112,3 +107,3 @@ } | ||
let {output, diagnostics} = | ||
new ClassRewriter(this.file).process(node as ts.ClassDeclaration); | ||
new ClassRewriter(this.typeChecker, this.file).process(node as ts.ClassDeclaration); | ||
this.diagnostics.push(...diagnostics); | ||
@@ -139,2 +134,3 @@ this.emit(output); | ||
if (this.decorators) { | ||
this.emit(`/** @nocollapse */\n`); | ||
this.emit(`static decorators: DecoratorInvocation[] = [\n`); | ||
@@ -149,2 +145,3 @@ for (let annotation of this.decorators) { | ||
if (this.ctorParameters) { | ||
this.emit(`/** @nocollapse */\n`); | ||
this.emit( | ||
@@ -173,2 +170,3 @@ `static ctorParameters: {type: Function, decorators?: DecoratorInvocation[]}[] = [\n`); | ||
if (this.propDecorators) { | ||
this.emit(`/** @nocollapse */\n`); | ||
this.emit('static propDecorators: {[key: string]: DecoratorInvocation[]} = {\n'); | ||
@@ -217,2 +215,4 @@ for (let name of Object.keys(this.propDecorators)) { | ||
class DecoratorRewriter extends Rewriter { | ||
constructor(private typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) { super(sourceFile); } | ||
process(): {output: string, diagnostics: ts.Diagnostic[]} { | ||
@@ -227,3 +227,3 @@ this.visit(this.file); | ||
let {output, diagnostics} = | ||
new ClassRewriter(this.file).process(node as ts.ClassDeclaration); | ||
new ClassRewriter(this.typeChecker, this.file).process(node as ts.ClassDeclaration); | ||
this.diagnostics.push(...diagnostics); | ||
@@ -238,6 +238,5 @@ this.emit(output); | ||
export function convertDecorators( | ||
fileName: string, sourceText: string): {output: string, diagnostics: ts.Diagnostic[]} { | ||
let file = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.ES5, true); | ||
return new DecoratorRewriter(file).process(); | ||
export function convertDecorators(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): | ||
{output: string, diagnostics: ts.Diagnostic[]} { | ||
return new DecoratorRewriter(typeChecker, sourceFile).process(); | ||
} |
@@ -10,7 +10,7 @@ import * as closure from 'google-closure-compiler'; | ||
/** | ||
* File name for the generated Closure externs. | ||
* Internal file name for the generated Closure externs. | ||
* This is never written to disk, but should be unique with respect to | ||
* the js files that may actually exist. | ||
*/ | ||
const externsFileName = 'tsickle_externs.js'; | ||
const internalExternsFileName = 'tsickle_externs.js'; | ||
@@ -22,2 +22,5 @@ /** Tsickle settings passed on the command line. */ | ||
/** If provided, path to save externs to. */ | ||
externsPath?: string; | ||
/** Path to write the final JS bundle to. */ | ||
@@ -35,2 +38,3 @@ outputPath: string; | ||
--saveTemporaries save intermediate .js files to disk | ||
--externs=PATH save generated Closure externs.js to PATH | ||
--output=PATH write final Closure bundle to PATH | ||
@@ -60,2 +64,5 @@ `); | ||
break; | ||
case 'externs': | ||
settings.externsPath = parsedArgs[flag]; | ||
break; | ||
case 'output': | ||
@@ -73,4 +80,4 @@ settings.outputPath = parsedArgs[flag]; | ||
} | ||
if (!settings.outputPath) { | ||
console.error('must specify --output path'); | ||
if (!settings.outputPath && !settings.externsPath) { | ||
console.error('must specify --output or --externs path'); | ||
usage(); | ||
@@ -199,5 +206,3 @@ process.exit(1); | ||
// Emit, creating a map of fileName => generated JS source. | ||
let jsFiles: {[fileName: string]: string} = { | ||
[externsFileName]: tsickleExterns, | ||
}; | ||
let jsFiles: {[fileName: string]: string} = {}; | ||
function writeFile(fileName: string, data: string): void { jsFiles[fileName] = data; } | ||
@@ -220,2 +225,4 @@ let {diagnostics} = program.emit(undefined, writeFile); | ||
jsFiles[internalExternsFileName] = tsickleExterns; | ||
return {jsFiles}; | ||
@@ -276,7 +283,13 @@ } | ||
// Run Closure compiiler to convert JS files to output bundle. | ||
closureCompile(jsFiles, settings.outputPath, (exitCode, output) => { | ||
if (output) console.error(output); | ||
process.exit(exitCode); | ||
}); | ||
if (settings.externsPath) { | ||
fs.writeFileSync(settings.externsPath, jsFiles[internalExternsFileName]); | ||
} | ||
if (settings.outputPath) { | ||
// Run Closure compiiler to convert JS files to output bundle. | ||
closureCompile(jsFiles, settings.outputPath, (exitCode, output) => { | ||
if (output) console.error(output); | ||
process.exit(exitCode); | ||
}); | ||
} | ||
} | ||
@@ -283,0 +296,0 @@ |
@@ -91,2 +91,7 @@ import * as ts from 'typescript'; | ||
/** Removes comment metacharacters from a string, to make it safe to embed in a comment. */ | ||
escapeForComment(str: string): string { | ||
return str.replace(/\/\*/g, '__').replace(/\*\//g, '__'); | ||
} | ||
/* tslint:disable: no-unused-variable */ | ||
@@ -93,0 +98,0 @@ logWithIndent(message: string) { |
@@ -130,3 +130,18 @@ import * as ts from 'typescript'; | ||
function getIdentifierText(identifier: ts.Identifier): string { | ||
// NOTE: the 'text' property on an Identifier may be escaped if it starts | ||
// with '__', so just use getText(). | ||
return identifier.getText(); | ||
} | ||
/** | ||
* Converts an escaped TypeScript name into the original source name. | ||
* Prefer getIdentifierText() instead if possible. | ||
*/ | ||
function unescapeName(name: string): string { | ||
// See the private function unescapeIdentifier in TypeScript's utilities.ts. | ||
if (name.match(/^___/)) return name.substr(1); | ||
return name; | ||
} | ||
const VISIBILITY_FLAGS = ts.NodeFlags.Private | ts.NodeFlags.Protected | ts.NodeFlags.Public; | ||
@@ -154,3 +169,5 @@ | ||
if (this.externsOutput.length > 0) { | ||
externs = '/** @externs */\n' + this.externsOutput.join(''); | ||
externs = `/** @externs */ | ||
// NOTE: generated by tsickle, do not edit. | ||
` + this.externsOutput.join(''); | ||
} | ||
@@ -181,2 +198,4 @@ let {output, diagnostics} = this.getOutput(); | ||
switch (node.kind) { | ||
case ts.SyntaxKind.ImportDeclaration: | ||
return this.emitImportDeclaration(node as ts.ImportDeclaration); | ||
case ts.SyntaxKind.ExportDeclaration: | ||
@@ -195,2 +214,4 @@ let exportDecl = <ts.ExportDeclaration>node; | ||
case ts.SyntaxKind.InterfaceDeclaration: | ||
this.emitInterface(node as ts.InterfaceDeclaration); | ||
// Emit the TS interface verbatim, with no tsickle processing of properties. | ||
this.writeRange(node.getFullStart(), node.getEnd()); | ||
@@ -286,2 +307,9 @@ return true; | ||
return true; | ||
case ts.SyntaxKind.JsxText: | ||
// TypeScript seems to accidentally include one extra token of | ||
// text in each JSX text node as a child. Avoid it here by just | ||
// emitting the node's text rather than visiting its children. | ||
// https://github.com/angular/tsickle/issues/76 | ||
this.emit(node.getFullText()); | ||
return true; | ||
default: | ||
@@ -293,2 +321,9 @@ break; | ||
/** | ||
* Given a "export * from ..." statement, rewrites it to instead "export {foo, bar, baz} from | ||
* ...". | ||
* This is necessary because TS transpiles "export *" by just doing a runtime loop | ||
* over the target module's exports, which means Closure won't see the declarations/types | ||
* that are exported. | ||
*/ | ||
private expandSymbolsFromExportStar(exportDecl: ts.ExportDeclaration): string[] { | ||
@@ -315,3 +350,3 @@ let typeChecker = this.program.getTypeChecker(); | ||
// Expand the export list, then filter it to the symbols we want | ||
// to reexport | ||
// to reexport. | ||
let exports = | ||
@@ -321,3 +356,3 @@ typeChecker.getExportsOfModule(typeChecker.getSymbolAtLocation(exportDecl.moduleSpecifier)); | ||
for (let sym of exports) { | ||
let name = sym.name; | ||
let name = unescapeName(sym.name); | ||
if (localSet.hasOwnProperty(name)) { | ||
@@ -340,3 +375,87 @@ // This name is shadowed by a local definition, such as: | ||
private emitFunctionType(fnDecl: ts.FunctionLikeDeclaration, extraTags: JSDocTag[] = []) { | ||
/** | ||
* Handles emit of an "import ..." statement. | ||
* We need to do a bit of rewriting so that imported types show up under the | ||
* correct name in JSDoc. | ||
* @return true if the decl was handled, false to allow default processing. | ||
*/ | ||
private emitImportDeclaration(decl: ts.ImportDeclaration): boolean { | ||
if (this.options.untyped) return false; | ||
const importClause = decl.importClause; | ||
if (!importClause) { | ||
// import './foo'; | ||
return false; // Use default processing. | ||
} else if (importClause.name) { | ||
// import foo from ...; | ||
this.errorUnimplementedKind(decl, 'TODO: default import'); | ||
return false; // Use default processing. | ||
} else if (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { | ||
// import * as foo from ...; | ||
return false; // Use default processing. | ||
} else if (importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) { | ||
// The statement looks like "import {a as b} from ...;". | ||
// | ||
// If a is a type, rewrite it to: | ||
// import {a as tsickle_b} from ...; | ||
// const b = tsickle_b; | ||
// type b = tsickle_b; | ||
// This is because in the generated ES5, the variable chosen in the import statement | ||
// (here, "tsickle_b") will instead get a generated name like module_import.tsickle_b. | ||
// But in our JSDoc we still refer to it as just "b". Importing under a different | ||
// name and then making a local alias evades this. | ||
// (TS uses the namespace-qualified name so that updates to the | ||
// module are reflected in the aliases. We only do this to types, which can't | ||
// change, so the namespace qualification is not needed.) | ||
const namedImports = importClause.namedBindings as ts.NamedImports; | ||
const typeChecker = this.program.getTypeChecker(); | ||
this.writeRange(decl.getFullStart(), decl.getStart()); | ||
// Emit the "import" part, with rewritten binding names for types. | ||
this.emit('import {'); | ||
let renamed: ts.Symbol[] = []; | ||
for (const imp of namedImports.elements) { | ||
// imp.propertyName is the name of the property in the module we're importing from, | ||
// while imp.name is the local name. propertyName is undefined in the case where | ||
// it's not being renamed as it's imported. | ||
this.emit(getIdentifierText(imp.propertyName || imp.name)); | ||
// Rename it if the symbol references a type. | ||
let name = getIdentifierText(imp.name); | ||
let sym = typeChecker.getSymbolAtLocation(imp.name); | ||
const isType = (typeChecker.getAliasedSymbol(sym).flags & ts.SymbolFlags.Type) !== 0; | ||
if (isType) { | ||
renamed.push(sym); | ||
this.emit(` as tsickle_${name}`); | ||
} else if (imp.propertyName) { | ||
this.emit(` as ${name}`); | ||
} | ||
this.emit(','); | ||
} | ||
this.emit('} from'); | ||
this.writeNode(decl.moduleSpecifier); | ||
this.emit(';\n'); | ||
// Emit aliases that bring the renamed names back to the local names. | ||
for (const sym of renamed) { | ||
let name = unescapeName(sym.name); | ||
// Note that tsickle_foo types are eventually always both values and types, because | ||
// post-tsickle processing (in Closure-land), all types are also values. | ||
const isValue = (typeChecker.getAliasedSymbol(sym).flags & ts.SymbolFlags.Value) !== 0; | ||
if (!isValue) { | ||
// Once tsickle processes the other module, it will create a var for it. | ||
// But until then we just hack this symbol in to the value namespace. | ||
this.emit(`declare var tsickle_${name};\n`); | ||
} | ||
this.emit(`const ${name} = tsickle_${name};\n`); | ||
this.emit(`type ${name} = tsickle_${name};\n`); | ||
} | ||
return true; | ||
} else { | ||
this.errorUnimplementedKind(decl, 'unexpected kind of import'); | ||
return false; // Use default processing. | ||
} | ||
} | ||
private emitFunctionType(fnDecl: ts.SignatureDeclaration, extraTags: JSDocTag[] = []) { | ||
let typeChecker = this.program.getTypeChecker(); | ||
@@ -370,3 +489,3 @@ let sig = typeChecker.getSignatureFromDeclaration(fnDecl); | ||
optional: paramNode.initializer !== undefined || paramNode.questionToken !== undefined, | ||
parameterName: paramSym.name, | ||
parameterName: unescapeName(paramSym.getName()), | ||
}; | ||
@@ -390,3 +509,3 @@ | ||
for (let {tagName, parameterName, text} of jsDoc.tags) { | ||
if (tagName === 'param' && parameterName === paramSym.name) { | ||
if (tagName === 'param' && parameterName === newTag.parameterName) { | ||
newTag.text = text; | ||
@@ -448,2 +567,24 @@ break; | ||
private emitInterface(iface: ts.InterfaceDeclaration) { | ||
if (this.options.untyped) return; | ||
this.emit(`\n/** @record */\n`); | ||
let name = getIdentifierText(iface.name); | ||
this.emit(`function ${name}() {}\n`); | ||
if (iface.typeParameters) { | ||
this.emit(`// TODO: type parameters.\n`); | ||
} | ||
if (iface.heritageClauses) { | ||
this.emit(`// TODO: derived interfaces.\n`); | ||
} | ||
const memberNamespace = [name, 'prototype']; | ||
for (let elem of iface.members) { | ||
this.visitProperty(memberNamespace, elem); | ||
} | ||
if (iface.flags & ts.NodeFlags.Export) { | ||
this.emit(`export {${name}};\n`); | ||
} | ||
} | ||
// emitTypeAnnotationsHelper produces a | ||
@@ -487,4 +628,4 @@ // _tsickle_typeAnnotationsHelper() where none existed in the | ||
this.emit('\n\n static _tsickle_typeAnnotationsHelper() {\n'); | ||
staticProps.forEach(p => this.visitProperty([classDecl.name.text], p)); | ||
let memberNamespace = [classDecl.name.text, 'prototype']; | ||
staticProps.forEach(p => this.visitProperty([getIdentifierText(classDecl.name)], p)); | ||
let memberNamespace = [getIdentifierText(classDecl.name), 'prototype']; | ||
nonStaticProps.forEach((p) => this.visitProperty(memberNamespace, p)); | ||
@@ -495,3 +636,24 @@ paramProps.forEach((p) => this.visitProperty(memberNamespace, p)); | ||
private visitProperty(namespace: string[], p: ts.PropertyDeclaration|ts.ParameterDeclaration) { | ||
private propertyName(prop: ts.Declaration): string { | ||
if (!prop.name) return null; | ||
switch (prop.name.kind) { | ||
case ts.SyntaxKind.Identifier: | ||
return getIdentifierText(prop.name as ts.Identifier); | ||
case ts.SyntaxKind.StringLiteral: | ||
// E.g. interface Foo { 'bar': number; } | ||
// If 'bar' is a name that is not valid in Closure then there's nothing we can do. | ||
return (prop.name as ts.StringLiteral).text; | ||
default: | ||
return null; | ||
} | ||
} | ||
private visitProperty(namespace: string[], p: ts.Declaration) { | ||
let name = this.propertyName(p); | ||
if (!name) { | ||
this.emit(`/* TODO: handle strange member:\n${this.escapeForComment(p.getText())}\n*/\n`); | ||
return; | ||
} | ||
let jsDoc = this.getJSDoc(p) || {tags: []}; | ||
@@ -511,3 +673,3 @@ let existingAnnotation = ''; | ||
this.emit(` @type {${this.typeToClosure(p)}} */\n`); | ||
namespace = namespace.concat([p.name.getText()]); | ||
namespace = namespace.concat([name]); | ||
this.emit(`${namespace.join('.')};\n`); | ||
@@ -545,3 +707,15 @@ } | ||
// E.g. "declare namespace foo {" | ||
namespace = namespace.concat(decl.name.text); | ||
let name: string; | ||
switch (decl.name.kind) { | ||
case ts.SyntaxKind.Identifier: | ||
name = getIdentifierText(decl.name as ts.Identifier); | ||
break; | ||
case ts.SyntaxKind.StringLiteral: | ||
name = (decl.name as ts.StringLiteral).text; | ||
break; | ||
default: | ||
this.errorUnimplementedKind(decl.name, 'namespace name'); | ||
break; | ||
} | ||
namespace = namespace.concat(name); | ||
let nsName = namespace.join('.'); | ||
@@ -588,4 +762,7 @@ if (!this.emittedNamespaces.hasOwnProperty(nsName)) { | ||
break; | ||
case ts.SyntaxKind.EnumDeclaration: | ||
this.writeExternsEnum(node as ts.EnumDeclaration, namespace); | ||
break; | ||
default: | ||
this.errorUnimplementedKind(node, 'externs generation'); | ||
this.emit(`\n/* TODO: ${ts.SyntaxKind[node.kind]} in ${namespace.join('.')} */\n`); | ||
break; | ||
@@ -630,2 +807,3 @@ } | ||
break; | ||
case ts.SyntaxKind.MethodSignature: | ||
case ts.SyntaxKind.MethodDeclaration: | ||
@@ -643,5 +821,9 @@ let m = <ts.MethodDeclaration>member; | ||
// interface Foo { [key: string]: number; } | ||
// For now, just die unless all the members are regular old | ||
// For now, just skip it unless all the members are regular old | ||
// properties. | ||
this.errorUnimplementedKind(member, 'externs for interface'); | ||
let name = namespace; | ||
if (member.name) { | ||
name = name.concat([member.name.getText()]); | ||
} | ||
this.emit(`\n/* TODO: ${ts.SyntaxKind[member.kind]} in ${name.join('.')} */\n`); | ||
} | ||
@@ -654,3 +836,3 @@ } | ||
let identifier = <ts.Identifier>decl.name; | ||
let qualifiedName = namespace.concat([identifier.text]).join('.'); | ||
let qualifiedName = namespace.concat([getIdentifierText(identifier)]).join('.'); | ||
if (closureExternsBlacklist.indexOf(qualifiedName) >= 0) return; | ||
@@ -677,2 +859,14 @@ this.emitJSDocType(decl); | ||
private writeExternsEnum(decl: ts.EnumDeclaration, namespace: string[]) { | ||
namespace = namespace.concat([getIdentifierText(decl.name)]); | ||
this.emit('\n/** @const */\n'); | ||
this.emit(`${namespace.join('.')} = {};\n`); | ||
for (let member of decl.members) { | ||
let memberName = member.name.getText(); | ||
let name = namespace.concat([memberName]).join('.'); | ||
this.emit('/** @const {number} */\n'); | ||
this.emit(`${name};\n`); | ||
} | ||
} | ||
/** Emits a type annotation in JSDoc, or {?} if the type is unavailable. */ | ||
@@ -871,2 +1065,6 @@ private emitJSDocType(node: ts.Node, additionalDocTag?: string) { | ||
this.emit(`goog.module('${moduleName}');`); | ||
// Allow code to use `module.id` to discover its module URL, e.g. to resolve | ||
// a template URL against. | ||
// Uses 'var', as this code is inserted in ES6 and ES5 modes. | ||
this.emit(`var module = module || {id: '${this.file.fileName}'};`); | ||
@@ -953,3 +1151,3 @@ let pos = 0; | ||
if (decl.name.kind !== ts.SyntaxKind.Identifier) return false; | ||
varName = (decl.name as ts.Identifier).text; | ||
varName = getIdentifierText(decl.name as ts.Identifier); | ||
if (!decl.initializer || decl.initializer.kind !== ts.SyntaxKind.CallExpression) return false; | ||
@@ -1010,2 +1208,3 @@ call = decl.initializer as ts.CallExpression; | ||
} | ||
// workaround for syntax highlighting bug in Sublime: ` | ||
@@ -1020,3 +1219,3 @@ /** | ||
let ident = call.expression as ts.Identifier; | ||
if (ident.text !== 'require') return null; | ||
if (getIdentifierText(ident) !== 'require') return null; | ||
@@ -1057,10 +1256,10 @@ // Verify the call takes a single string argument and grab it. | ||
// module_name_var.default | ||
if (propAccess.name.text !== 'default') break; | ||
if (getIdentifierText(propAccess.name) !== 'default') break; | ||
if (propAccess.expression.kind !== ts.SyntaxKind.Identifier) break; | ||
let lhsIdent = propAccess.expression as ts.Identifier; | ||
if (!this.namespaceImports.hasOwnProperty(lhsIdent.text)) break; | ||
let lhs = getIdentifierText(propAccess.expression as ts.Identifier); | ||
if (!this.namespaceImports.hasOwnProperty(lhs)) break; | ||
// Emit the same expression, with spaces to replace the ".default" part | ||
// so that source maps still line up. | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
this.emit(`${lhsIdent.text} `); | ||
this.emit(`${lhs} `); | ||
return true; | ||
@@ -1067,0 +1266,0 @@ default: |
@@ -71,2 +71,37 @@ import * as ts from 'typescript'; | ||
/** | ||
* Converts a ts.Symbol to a string. | ||
* Other approaches that don't work: | ||
* - TypeChecker.typeToString translates Array as T[]. | ||
* - TypeChecker.symbolToString emits types without their namespace, | ||
* and doesn't let you pass the flag to control that. | ||
*/ | ||
private symbolToString(sym: ts.Symbol): string { | ||
// This follows getSingleLineStringWriter in the TypeScript compiler. | ||
let str = ''; | ||
let writeText = (text: string) => str += text; | ||
let doNothing = () => { return; }; | ||
let builder = this.typeChecker.getSymbolDisplayBuilder(); | ||
let writer: ts.SymbolWriter = { | ||
writeKeyword: writeText, | ||
writeOperator: writeText, | ||
writePunctuation: writeText, | ||
writeSpace: writeText, | ||
writeStringLiteral: writeText, | ||
writeParameter: writeText, | ||
writeSymbol: writeText, | ||
writeLine: doNothing, | ||
increaseIndent: doNothing, | ||
decreaseIndent: doNothing, | ||
clear: doNothing, | ||
trackSymbol(symbol: ts.Symbol, enclosingDeclaration?: ts.Node, meaning?: ts.SymbolFlags) { | ||
return; | ||
}, | ||
reportInaccessibleThisError: doNothing, | ||
}; | ||
builder.buildSymbolDisplay(sym, writer, this.node); | ||
return str; | ||
} | ||
/** | ||
* @param notNull When true, insert a ! before any type references. This | ||
@@ -104,2 +139,3 @@ * is to work around the difference between TS and Closure destructuring. | ||
// Continue on to more complex tests below. | ||
break; | ||
} | ||
@@ -117,3 +153,3 @@ | ||
// the InterfaceType. | ||
return type.symbol.name; | ||
return this.symbolToString(type.symbol); | ||
} else if (type.flags & ts.TypeFlags.Reference) { | ||
@@ -120,0 +156,0 @@ // A reference to another type, e.g. Array<number> refers to Array. |
@@ -1,17 +0,10 @@ | ||
import * as ts from 'typescript'; | ||
import {convertDecorators} from '../src/decorator-annotator'; | ||
import {expect} from 'chai'; | ||
import * as tsickle from '../src/tsickle'; | ||
import * as test_support from './test_support'; | ||
/** | ||
* If true, attempt to compile all the test cases. | ||
* Off by default because compilation takes a long time, and the test cases | ||
* as currently produced are all valid. | ||
*/ | ||
const checkCompilation = false; | ||
const testCaseFileName = 'testcase.ts'; | ||
// If we verify the produced code compiles, this blob of code is some scaffolding | ||
// of some types and variables used in the test cases so the compiler likes it. | ||
// When we verify that the produced code compiles, we need to provide a definition | ||
// of DecoratorInvocation. | ||
const testSupportCode = ` | ||
@@ -22,33 +15,10 @@ interface DecoratorInvocation { | ||
} | ||
let Test1: Function; | ||
let Test2: Function; | ||
let Test3: Function; | ||
let Test4: Function; | ||
let Inject: Function; | ||
let param: any; | ||
class BarService {} | ||
abstract class AbstractService {} | ||
`; | ||
function verifyCompiles(sourceText: string) { | ||
let compilerOptions: ts.CompilerOptions = { | ||
experimentalDecorators: true, | ||
emitDecoratorMetadata: true, | ||
let sources: {[fileName: string]: string} = { | ||
[testCaseFileName]: testSupportCode + sourceText, | ||
}; | ||
let host = ts.createCompilerHost(compilerOptions); | ||
let origGetSourceFile = host.getSourceFile; | ||
host.getSourceFile = function(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile { | ||
if (fileName === testCaseFileName) { | ||
sourceText = testSupportCode + sourceText; | ||
return ts.createSourceFile(fileName, sourceText, languageVersion, true); | ||
} | ||
return origGetSourceFile(fileName, languageVersion); | ||
}; | ||
let program = ts.createProgram([testCaseFileName], compilerOptions, host); | ||
let errors = ts.getPreEmitDiagnostics(program); | ||
if (errors.length > 0) { | ||
console.error(sourceText); | ||
console.error(tsickle.formatDiagnostics(errors)); | ||
} | ||
// This throws an exception on error. | ||
test_support.createProgram(sources); | ||
} | ||
@@ -59,5 +29,10 @@ | ||
function translate(sourceText: string, allowErrors = false) { | ||
let {output, diagnostics} = convertDecorators(testCaseFileName, sourceText); | ||
let sources: {[fileName: string]: string} = { | ||
[testCaseFileName]: sourceText, | ||
}; | ||
let program = test_support.createProgram(sources); | ||
let {output, diagnostics} = | ||
convertDecorators(program.getTypeChecker(), program.getSourceFile(testCaseFileName)); | ||
if (!allowErrors) expect(diagnostics).to.be.empty; | ||
if (checkCompilation) verifyCompiles(output); | ||
verifyCompiles(output); | ||
return {output, diagnostics}; | ||
@@ -72,2 +47,5 @@ } | ||
expect(translate(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
let param: any; | ||
@Test1 | ||
@@ -78,4 +56,8 @@ @Test2(param) | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
let param: any; | ||
class Foo { | ||
field: string; | ||
/** @nocollapse */ | ||
static decorators: DecoratorInvocation[] = [ | ||
@@ -90,9 +72,20 @@ { type: Test1 }, | ||
expect(translate(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
let Test3: Function; | ||
function Test4<T>(param: any): ClassDecorator { return null; } | ||
let param: any; | ||
@Test1({name: 'percentPipe'}, class ZZZ {}) | ||
@Test2 | ||
@Test3() | ||
@Test4<T>(param) | ||
@Test4<string>(param) | ||
class Foo { | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
let Test3: Function; | ||
function Test4<T>(param: any): ClassDecorator { return null; } | ||
let param: any; | ||
class Foo { | ||
/** @nocollapse */ | ||
static decorators: DecoratorInvocation[] = [ | ||
@@ -109,6 +102,9 @@ { type: Test1, args: [{name: 'percentPipe'}, class ZZZ {}, ] }, | ||
expect(translate(` | ||
let Test1: Function; | ||
@Test1 | ||
export class Foo { | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
export class Foo { | ||
/** @nocollapse */ | ||
static decorators: DecoratorInvocation[] = [ | ||
@@ -122,2 +118,4 @@ { type: Test1 }, | ||
expect(translate(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
@Test1 | ||
@@ -131,6 +129,9 @@ export class Foo { | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
export class Foo { | ||
foo() { | ||
class Bar { | ||
static decorators: DecoratorInvocation[] = [ | ||
/** @nocollapse */ | ||
static decorators: DecoratorInvocation[] = [ | ||
{ type: Test2 }, | ||
@@ -140,2 +141,3 @@ ]; | ||
} | ||
/** @nocollapse */ | ||
static decorators: DecoratorInvocation[] = [ | ||
@@ -151,2 +153,3 @@ { type: Test1 }, | ||
expect(translate(` | ||
class BarService {}; | ||
class Foo { | ||
@@ -156,2 +159,3 @@ constructor(bar: BarService, num: number) { | ||
}`).output).to.equal(` | ||
class BarService {}; | ||
class Foo { | ||
@@ -165,2 +169,4 @@ constructor(bar: BarService, num: number) { | ||
expect(translate(` | ||
let Inject: Function; | ||
abstract class AbstractService {} | ||
class Foo { | ||
@@ -170,5 +176,8 @@ constructor(@Inject bar: AbstractService, num: number) { | ||
}`).output).to.equal(` | ||
let Inject: Function; | ||
abstract class AbstractService {} | ||
class Foo { | ||
constructor( bar: AbstractService, num: number) { | ||
} | ||
/** @nocollapse */ | ||
static ctorParameters: {type: Function, decorators?: DecoratorInvocation[]}[] = [ | ||
@@ -183,2 +192,4 @@ {type: AbstractService, decorators: [{ type: Inject }, ]}, | ||
expect(translate(` | ||
let Test1: Function; | ||
class BarService {} | ||
@Test1() | ||
@@ -189,8 +200,12 @@ class Foo { | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
class BarService {} | ||
class Foo { | ||
constructor(bar: BarService, num: number) { | ||
} | ||
/** @nocollapse */ | ||
static decorators: DecoratorInvocation[] = [ | ||
{ type: Test1 }, | ||
]; | ||
/** @nocollapse */ | ||
static ctorParameters: {type: Function, decorators?: DecoratorInvocation[]}[] = [ | ||
@@ -205,2 +220,5 @@ {type: BarService, }, | ||
expect(translate(` | ||
let Inject: Function; | ||
class BarService {} | ||
let param: any; | ||
class Foo { | ||
@@ -210,5 +228,9 @@ constructor(@Inject(param) x: BarService, {a, b}, defArg = 3, optional?: BarService) { | ||
}`).output).to.equal(` | ||
let Inject: Function; | ||
class BarService {} | ||
let param: any; | ||
class Foo { | ||
constructor( x: BarService, {a, b}, defArg = 3, optional?: BarService) { | ||
} | ||
/** @nocollapse */ | ||
static ctorParameters: {type: Function, decorators?: DecoratorInvocation[]}[] = [ | ||
@@ -225,7 +247,12 @@ {type: BarService, decorators: [{ type: Inject, args: [param, ] }, ]}, | ||
expect(translate(` | ||
let Inject: Function; | ||
let APP_ID: any; | ||
class ViewUtils { | ||
constructor(@Inject(APP_ID) private _appId: string) {} | ||
}`).output).to.equal(` | ||
let Inject: Function; | ||
let APP_ID: any; | ||
class ViewUtils { | ||
constructor( private _appId: string) {} | ||
/** @nocollapse */ | ||
static ctorParameters: {type: Function, decorators?: DecoratorInvocation[]}[] = [ | ||
@@ -239,2 +266,3 @@ {type: undefined, decorators: [{ type: Inject, args: [APP_ID, ] }, ]}, | ||
expect(translate(` | ||
let Inject: Function; | ||
class Foo { | ||
@@ -244,5 +272,7 @@ constructor(@Inject typed: Promise<string>) { | ||
}`).output).to.equal(` | ||
let Inject: Function; | ||
class Foo { | ||
constructor( typed: Promise<string>) { | ||
} | ||
/** @nocollapse */ | ||
static ctorParameters: {type: Function, decorators?: DecoratorInvocation[]}[] = [ | ||
@@ -253,2 +283,23 @@ {type: Promise, decorators: [{ type: Inject }, ]}, | ||
}); | ||
it('avoids using interfaces as values', () => { | ||
expect(translate(` | ||
let Inject: Function = null; | ||
class Class {} | ||
interface Iface {} | ||
class Foo { | ||
constructor(@Inject aClass: Class, @Inject aIface: Iface) {} | ||
}`).output).to.equal(` | ||
let Inject: Function = null; | ||
class Class {} | ||
interface Iface {} | ||
class Foo { | ||
constructor( aClass: Class, aIface: Iface) {} | ||
/** @nocollapse */ | ||
static ctorParameters: {type: Function, decorators?: DecoratorInvocation[]}[] = [ | ||
{type: Class, decorators: [{ type: Inject }, ]}, | ||
{type: undefined, decorators: [{ type: Inject }, ]}, | ||
]; | ||
}`); | ||
}); | ||
}); | ||
@@ -269,2 +320,3 @@ | ||
expect(translate(` | ||
let Test1: Function; | ||
class Foo { | ||
@@ -274,4 +326,6 @@ @Test1('somename') | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
class Foo { | ||
bar() {} | ||
/** @nocollapse */ | ||
static propDecorators: {[key: string]: DecoratorInvocation[]} = { | ||
@@ -285,2 +339,3 @@ 'bar': [{ type: Test1, args: ['somename', ] },], | ||
expect(translate(` | ||
let PropDecorator: Function; | ||
class ClassWithDecorators { | ||
@@ -293,5 +348,7 @@ @PropDecorator("p1") @PropDecorator("p2") a; | ||
}`).output).to.equal(` | ||
let PropDecorator: Function; | ||
class ClassWithDecorators { a; | ||
b; | ||
set c(value) {} | ||
/** @nocollapse */ | ||
static propDecorators: {[key: string]: DecoratorInvocation[]} = { | ||
@@ -307,2 +364,4 @@ 'a': [{ type: PropDecorator, args: ["p1", ] },{ type: PropDecorator, args: ["p2", ] },], | ||
` | ||
let Test1: Function; | ||
let param: any; | ||
class Foo { | ||
@@ -316,3 +375,3 @@ @Test1('somename') | ||
.to.equal( | ||
'Error at testcase.ts:3:3: cannot process decorators on ComputedPropertyName'); | ||
'Error at testcase.ts:5:3: cannot process decorators on ComputedPropertyName'); | ||
}); | ||
@@ -319,0 +378,0 @@ |
@@ -34,6 +34,6 @@ import * as fs from 'fs'; | ||
let tests = goldenTests(); | ||
let goldenJs = tests.map(t => t.es6Path); | ||
let externs = tests.map(t => t.externsPath).filter(path => fs.existsSync(path)); | ||
let goldenJs = [].concat(...tests.map(t => t.jsPaths)); | ||
let externs = tests.map(t => t.externsPath).filter(fs.existsSync); | ||
checkClosureCompile(goldenJs, externs, done); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
import {expect} from 'chai'; | ||
import * as fs from 'fs'; | ||
import * as ts from 'typescript'; | ||
@@ -17,2 +17,6 @@ import * as glob from 'glob'; | ||
module: ts.ModuleKind.CommonJS, | ||
jsx: ts.JsxEmit.React, | ||
// Flags below are needed to make sure source paths are correctly set on write calls. | ||
rootDir: path.resolve(process.cwd()), | ||
outDir: '.', | ||
}; | ||
@@ -27,6 +31,5 @@ | ||
export function annotateSource( | ||
inputFileName: string, sourceText: string, options: tsickle.Options = {}): tsickle.Output { | ||
/** Creates a ts.Program from a set of input files. Throws an exception on errors. */ | ||
export function createProgram(sources: {[fileName: string]: string}): ts.Program { | ||
let host = ts.createCompilerHost(compilerOptions); | ||
let original = host.getSourceFile.bind(host); | ||
host.getSourceFile = function( | ||
@@ -36,9 +39,10 @@ fileName: string, languageVersion: ts.ScriptTarget, | ||
if (fileName === cachedLibPath) return cachedLib; | ||
if (fileName === inputFileName) { | ||
return ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true); | ||
if (path.isAbsolute(fileName)) fileName = path.relative(process.cwd(), fileName); | ||
if (sources.hasOwnProperty(fileName)) { | ||
return ts.createSourceFile(fileName, sources[fileName], ts.ScriptTarget.Latest, true); | ||
} | ||
return original(fileName, languageVersion, onError); | ||
throw new Error('unexpected file read of ' + fileName + ' not in ' + Object.keys(sources)); | ||
}; | ||
let program = ts.createProgram([inputFileName], compilerOptions, host); | ||
let program = ts.createProgram(Object.keys(sources), compilerOptions, host); | ||
let diagnostics = ts.getPreEmitDiagnostics(program); | ||
@@ -49,35 +53,16 @@ if (diagnostics.length) { | ||
return tsickle.annotate(program, program.getSourceFile(inputFileName), options); | ||
return program; | ||
} | ||
export function transformSource(inputFileName: string, sourceText: string): string { | ||
let host = ts.createCompilerHost(compilerOptions); | ||
let original = host.getSourceFile.bind(host); | ||
let mainSrc = ts.createSourceFile(inputFileName, sourceText, ts.ScriptTarget.Latest, true); | ||
host.getSourceFile = function( | ||
fileName: string, languageVersion: ts.ScriptTarget, | ||
onError?: (msg: string) => void): ts.SourceFile { | ||
if (fileName === cachedLibPath) return cachedLib; | ||
if (fileName === inputFileName) { | ||
return mainSrc; | ||
} | ||
return original(fileName, languageVersion, onError); | ||
}; | ||
let program = ts.createProgram([inputFileName], compilerOptions, host); | ||
let diagnostics = ts.getPreEmitDiagnostics(program); | ||
if (diagnostics.length) { | ||
throw new Error( | ||
'Failed to parse ' + sourceText + '\n' + tsickle.formatDiagnostics(diagnostics)); | ||
} | ||
/** Emits transpiled output with tsickle postprocessing. Throws an exception on errors. */ | ||
export function emit(program: ts.Program): {[fileName: string]: string} { | ||
let transformed: {[fileName: string]: string} = {}; | ||
let emitRes = | ||
program.emit(mainSrc, (fileName: string, data: string) => { transformed[fileName] = data; }); | ||
let emitRes = program.emit(undefined, (fileName: string, data: string) => { | ||
transformed[fileName] = | ||
tsickle.convertCommonJsToGoogModule(fileName, data, pathToModuleName).output; | ||
}); | ||
if (emitRes.diagnostics.length) { | ||
throw new Error(tsickle.formatDiagnostics(emitRes.diagnostics)); | ||
} | ||
let outputFileName = inputFileName.replace(/.ts$/, '.js'); | ||
expect(Object.keys(transformed)).to.deep.equal([outputFileName]); | ||
let outputSource = transformed[outputFileName]; | ||
return transformed; | ||
@@ -88,19 +73,28 @@ function pathToModuleName(context: string, fileName: string): string { | ||
} | ||
return fileName.replace(/^.+\/test_files\//, 'tsickle_test/') | ||
.replace(/\.tsickle\.js$/, '') | ||
.replace('/', '.'); | ||
return fileName.replace(/\.js$/, '').replace(/\//g, '.'); | ||
} | ||
return tsickle.convertCommonJsToGoogModule(outputFileName, outputSource, pathToModuleName).output; | ||
} | ||
export interface GoldenFileTest { | ||
name: string; | ||
// Path to input file. | ||
tsPath: string; | ||
// Path to golden of post-tsickle processing. | ||
tsicklePath: string; | ||
// Path to golden of post-tsickle externs, if any. | ||
externsPath: string; | ||
// Path to golden of post-tsickle, post TypeScript->ES6 processing. | ||
es6Path: string; | ||
export class GoldenFileTest { | ||
// Path to directory containing test files. | ||
path: string; | ||
// Input .ts/.tsx file names. | ||
tsFiles: string[]; | ||
constructor(path: string, tsFiles: string[]) { | ||
this.path = path; | ||
this.tsFiles = tsFiles; | ||
} | ||
get name(): string { return path.basename(this.path); } | ||
get externsPath(): string { return path.join(this.path, 'externs.js'); } | ||
get tsPaths(): string[] { return this.tsFiles.map(f => path.join(this.path, f)); } | ||
get jsPaths(): string[] { | ||
return this.tsFiles.map(f => path.join(this.path, GoldenFileTest.tsPathToJs(f))); | ||
} | ||
public static tsPathToJs(tsPath: string): string { return tsPath.replace(/\.tsx?$/, '.js'); } | ||
} | ||
@@ -110,31 +104,15 @@ | ||
let basePath = path.join(__dirname, '..', '..', 'test_files'); | ||
let testInputs = glob.sync(path.join(basePath, '*.in.ts')); | ||
let testNames = fs.readdirSync(basePath); | ||
let tests = testInputs.map((testPath) => { | ||
let testName = testPath.match(/\/test_files\/(.*)\.in\.ts$/)[1]; | ||
return { | ||
name: testName, | ||
tsPath: testPath, | ||
tsicklePath: testPath.replace(/\.in\.ts$/, '.tsickle.ts'), | ||
externsPath: testPath.replace(/\.in\.ts$/, '.tsickle_externs.js'), | ||
es6Path: testPath.replace(/\.in\.ts$/, '.tr.js'), | ||
}; | ||
let tests = testNames.map(testName => { | ||
let testDir = path.join(basePath, testName); | ||
testDir = path.relative(process.cwd(), testDir); | ||
let tsPaths = glob.sync(path.join(testDir, '*.ts')); | ||
tsPaths = tsPaths.concat(glob.sync(path.join(testDir, '*.tsx'))); | ||
tsPaths = tsPaths.filter(p => !p.match(/\.tsickle\./)); | ||
let tsFiles = tsPaths.map(f => path.relative(testDir, f)); | ||
return new GoldenFileTest(testDir, tsFiles); | ||
}); | ||
// export_helper*.ts is special, because it is imported by another | ||
// test. It it must be importable as plain './export_helper' so its | ||
// files can't have extensions a ".in.ts" or ".tr.js". | ||
let helperInputs = glob.sync(path.join(basePath, 'export_helper{,_2}.ts')); | ||
for (let testPath of helperInputs) { | ||
let testName = testPath.match(/\/test_files\/(export_helper[^.]*)\.ts$/)[1]; | ||
let exportHelperTestCase: GoldenFileTest = { | ||
name: testName, | ||
tsPath: testPath, | ||
tsicklePath: testPath.replace(/\.ts$/, '.tsickle.ts'), | ||
externsPath: testPath.replace(/\.ts$/, '.tsickle_externs.js'), | ||
es6Path: testPath.replace(/\.ts$/, '.js'), | ||
}; | ||
tests.push(exportHelperTestCase); | ||
} | ||
return tests; | ||
} |
@@ -1,9 +0,10 @@ | ||
import * as ts from 'typescript'; | ||
import {expect} from 'chai'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import {expect} from 'chai'; | ||
import * as ts from 'typescript'; | ||
import * as tsickle from '../src/tsickle'; | ||
import {annotateSource, transformSource, goldenTests} from './test_support'; | ||
import * as test_support from './test_support'; | ||
let RUN_TESTS_MATCHING: RegExp = null; | ||
@@ -59,3 +60,3 @@ // RUN_TESTS_MATCHING = /fields/; | ||
describe('golden tests', () => { | ||
goldenTests().forEach((test) => { | ||
test_support.goldenTests().forEach((test) => { | ||
if (RUN_TESTS_MATCHING && !RUN_TESTS_MATCHING.exec(test.name)) { | ||
@@ -70,29 +71,50 @@ it.skip(test.name); | ||
it(test.name, () => { | ||
let tsSource = fs.readFileSync(test.tsPath, 'utf-8'); | ||
// Read all the inputs into a map, and create a ts.Program from them. | ||
let tsSources: {[fileName: string]: string} = {}; | ||
for (let tsFile of test.tsFiles) { | ||
let tsPath = path.join(test.path, tsFile); | ||
let tsSource = fs.readFileSync(tsPath, 'utf-8'); | ||
tsSources[tsPath] = tsSource; | ||
} | ||
let program = test_support.createProgram(tsSources); | ||
// Run TypeScript through tsickle and compare against goldens. | ||
let warnings: ts.Diagnostic[] = []; | ||
options.logWarning = (diag: ts.Diagnostic) => { warnings.push(diag); }; | ||
let {output, externs, diagnostics} = annotateSource(test.tsPath, tsSource, options); | ||
// Tsickle-annotate all the sources, comparing against goldens, and gather the | ||
// generated externs and tsickle-processed sources. | ||
let allExterns: string = null; | ||
let tsickleSources: {[fileName: string]: string} = {}; | ||
for (let tsPath of Object.keys(tsSources)) { | ||
let warnings: ts.Diagnostic[] = []; | ||
options.logWarning = (diag: ts.Diagnostic) => { warnings.push(diag); }; | ||
// Run TypeScript through tsickle and compare against goldens. | ||
let {output, externs, diagnostics} = | ||
tsickle.annotate(program, program.getSourceFile(tsPath), options); | ||
if (externs) allExterns = externs; | ||
// If there were any diagnostics, convert them into strings for | ||
// the golden output. | ||
let fileOutput = output; | ||
diagnostics.push(...warnings); | ||
if (diagnostics.length > 0) { | ||
// Munge the filenames in the diagnostics so that they don't include | ||
// the tsickle checkout path. | ||
for (let diag of diagnostics) { | ||
let fileName = diag.file.fileName; | ||
diag.file.fileName = fileName.substr(fileName.indexOf('test_files')); | ||
// If there were any diagnostics, convert them into strings for | ||
// the golden output. | ||
let fileOutput = output; | ||
diagnostics.push(...warnings); | ||
if (diagnostics.length > 0) { | ||
// Munge the filenames in the diagnostics so that they don't include | ||
// the tsickle checkout path. | ||
for (let diag of diagnostics) { | ||
let fileName = diag.file.fileName; | ||
diag.file.fileName = fileName.substr(fileName.indexOf('test_files')); | ||
} | ||
fileOutput = tsickle.formatDiagnostics(diagnostics) + '\n====\n' + output; | ||
} | ||
fileOutput = tsickle.formatDiagnostics(diagnostics) + '\n====\n' + output; | ||
let tsicklePath = tsPath.replace(/.ts(x)?$/, '.tsickle.ts$1'); | ||
expect(tsicklePath).to.not.equal(tsPath); | ||
compareAgainstGolden(fileOutput, tsicklePath); | ||
tsickleSources[tsPath] = output; | ||
} | ||
compareAgainstGolden(fileOutput, test.tsicklePath); | ||
compareAgainstGolden(externs, test.externsPath); | ||
compareAgainstGolden(allExterns, test.externsPath); | ||
// Run tsickled TypeScript through TypeScript compiler | ||
// and compare against goldens. | ||
let es6Source = transformSource(test.tsicklePath, output); | ||
compareAgainstGolden(es6Source, test.es6Path); | ||
program = test_support.createProgram(tsickleSources); | ||
let jsSources = test_support.emit(program); | ||
for (let jsPath of Object.keys(jsSources)) { | ||
compareAgainstGolden(jsSources[jsPath], jsPath); | ||
} | ||
}); | ||
@@ -162,10 +184,13 @@ }); | ||
expectCommonJs('a.js', `console.log('hello');`) | ||
.to.equal(`goog.module('a');console.log('hello');`); | ||
.to.equal(`goog.module('a');var module = module || {id: 'a'};console.log('hello');`); | ||
}); | ||
it('adds a goog.module call to empty files', | ||
() => { expectCommonJs('a.js', ``).to.equal(`goog.module('a');`); }); | ||
it('adds a goog.module call to empty files', () => { | ||
expectCommonJs('a.js', ``).to.equal(`goog.module('a');var module = module || {id: 'a'};`); | ||
}); | ||
it('adds a goog.module call to empty-looking files', | ||
() => { expectCommonJs('a.js', `// empty`).to.equal(`goog.module('a');// empty`); }); | ||
it('adds a goog.module call to empty-looking files', () => { | ||
expectCommonJs('a.js', `// empty`) | ||
.to.equal(`goog.module('a');var module = module || {id: 'a'};// empty`); | ||
}); | ||
@@ -176,3 +201,3 @@ it('strips use strict directives', () => { | ||
console.log('hello');`) | ||
.to.equal(`goog.module('a'); | ||
.to.equal(`goog.module('a');var module = module || {id: 'a'}; | ||
console.log('hello');`); | ||
@@ -183,3 +208,4 @@ }); | ||
expectCommonJs('a.js', `var r = require('req/mod');`) | ||
.to.equal(`goog.module('a');var r = goog.require('req$mod');`); | ||
.to.equal( | ||
`goog.module('a');var module = module || {id: 'a'};var r = goog.require('req$mod');`); | ||
}); | ||
@@ -189,3 +215,4 @@ | ||
expectCommonJs('a.js', `require('req/mod');`) | ||
.to.equal(`goog.module('a');var unused_0_ = goog.require('req$mod');`); | ||
.to.equal( | ||
`goog.module('a');var module = module || {id: 'a'};var unused_0_ = goog.require('req$mod');`); | ||
}); | ||
@@ -197,3 +224,3 @@ | ||
require('other');`) | ||
.to.equal(`goog.module('a'); | ||
.to.equal(`goog.module('a');var module = module || {id: 'a'}; | ||
var unused_0_ = goog.require('req$mod'); | ||
@@ -207,3 +234,3 @@ var unused_1_ = goog.require('other');`); | ||
require('req/mod');`) | ||
.to.equal(`goog.module('a'); | ||
.to.equal(`goog.module('a');var module = module || {id: 'a'}; | ||
// Comment | ||
@@ -215,3 +242,4 @@ var unused_0_ = goog.require('req$mod');`); | ||
expectCommonJs('a.js', `const r = require('req/mod');`) | ||
.to.equal(`goog.module('a');var r = goog.require('req$mod');`); | ||
.to.equal( | ||
`goog.module('a');var module = module || {id: 'a'};var r = goog.require('req$mod');`); | ||
}); | ||
@@ -221,3 +249,4 @@ | ||
expectCommonJs('a.js', `__export(require('req/mod'));`) | ||
.to.equal(`goog.module('a');__export(goog.require('req$mod'));`); | ||
.to.equal( | ||
`goog.module('a');var module = module || {id: 'a'};__export(goog.require('req$mod'));`); | ||
}); | ||
@@ -228,3 +257,4 @@ | ||
expectCommonJs('a/b.js', `var r = require('./req/mod');`) | ||
.to.equal(`goog.module('a$b');var r = goog.require('a$req$mod');`); | ||
.to.equal( | ||
`goog.module('a$b');var module = module || {id: 'a/b'};var r = goog.require('a$req$mod');`); | ||
}); | ||
@@ -235,3 +265,3 @@ | ||
var goog_use_Foo_1 = require('goog:foo_bar.baz');`) | ||
.to.equal(`goog.module('a$b'); | ||
.to.equal(`goog.module('a$b');var module = module || {id: 'a/b'}; | ||
var goog_use_Foo_1 = goog.require('foo_bar.baz');`); | ||
@@ -244,3 +274,3 @@ }); | ||
console.log(goog_use_Foo_1.default);`) | ||
.to.equal(`goog.module('a$b'); | ||
.to.equal(`goog.module('a$b');var module = module || {id: 'a/b'}; | ||
var goog_use_Foo_1 = goog.require('use.Foo'); | ||
@@ -257,3 +287,3 @@ console.log(goog_use_Foo_1 );`); | ||
console.log(foo.bar.default);`) | ||
.to.equal(`goog.module('a$b'); | ||
.to.equal(`goog.module('a$b');var module = module || {id: 'a/b'}; | ||
console.log(this.default); | ||
@@ -269,3 +299,3 @@ console.log(foo.bar.default);`); | ||
var foo = bar; | ||
`).to.equal(`goog.module('a$b');/** | ||
`).to.equal(`goog.module('a$b');var module = module || {id: 'a/b'};/** | ||
* docstring here | ||
@@ -282,3 +312,3 @@ */ | ||
foo_1.A, foo_2.B, foo_2.default, foo_3.default; | ||
`).to.equal(`goog.module('a$b');var foo_1 = goog.require('foo'); | ||
`).to.equal(`goog.module('a$b');var module = module || {id: 'a/b'};var foo_1 = goog.require('foo'); | ||
var foo_2 = foo_1; | ||
@@ -285,0 +315,0 @@ foo_1.A, foo_2.B, foo_2 , foo_3.default; |
@@ -5,8 +5,15 @@ { | ||
"noImplicitAny": true, | ||
"noEmit": true | ||
"noEmit": true, | ||
"target": "es5", | ||
"jsx": "react" | ||
}, | ||
"files": [ | ||
"src/google-closure-compiler.d.ts", | ||
"src/decorator-annotator.ts", | ||
"src/main.ts", | ||
"src/rewriter.ts", | ||
"src/tsickle.ts", | ||
"src/type-translator.ts", | ||
"test/decorator-annotator_test.ts", | ||
"test/e2e_test.ts", | ||
"test/test_support.ts", | ||
"test/tsickle_test.ts", | ||
@@ -13,0 +20,0 @@ "typings/tsd.d.ts" |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
150
7215
341977