Comparing version 0.1.5 to 0.1.6
@@ -48,1 +48,8 @@ import * as ts from 'typescript'; | ||
} | ||
/** Returns the string contents of a ts.Identifier. */ | ||
export declare function getIdentifierText(identifier: ts.Identifier): string; | ||
/** | ||
* Converts an escaped TypeScript name into the original source name. | ||
* Prefer getIdentifierText() instead if possible. | ||
*/ | ||
export declare function unescapeName(name: string): string; |
import * as ts from 'typescript'; | ||
export { convertDecorators } from './decorator-annotator'; | ||
export { processES5 as convertCommonJsToGoogModule } from './es5processor'; | ||
export interface Options { | ||
@@ -42,15 +43,1 @@ untyped?: boolean; | ||
export declare function annotate(program: ts.Program, file: ts.SourceFile, options?: Options): Output; | ||
/** | ||
* Converts TypeScript's JS+CommonJS output to Closure goog.module etc. | ||
* For use as a postprocessing step *after* TypeScript emits JavaScript. | ||
* | ||
* @param fileName The source file name, without an extension. | ||
* @param pathToModuleName A function that maps a filesystem .ts path to a | ||
* Closure module name, as found in a goog.require('...') statement. | ||
* The context parameter is the referencing file, used for resolving | ||
* imports with relative paths like "import * as foo from '../foo';". | ||
*/ | ||
export declare function convertCommonJsToGoogModule(fileName: string, content: string, pathToModuleName: (context: string, fileName: string) => string): { | ||
output: string; | ||
referencedModules: string[]; | ||
}; |
import * as ts from 'typescript'; | ||
export declare function assertTypeChecked(sourceFile: ts.SourceFile): void; | ||
export declare function typeToDebugString(type: ts.Type): string; | ||
@@ -3,0 +4,0 @@ export declare function symbolToDebugString(sym: ts.Symbol): string; |
@@ -16,6 +16,32 @@ "use strict"; | ||
function ClassRewriter(typeChecker, sourceFile) { | ||
var _this = this; | ||
_super.call(this, sourceFile); | ||
this.typeChecker = typeChecker; | ||
this.decoratorsToLower = function (n) { return n.decorators.filter(function (d) { return _this.shouldLower(d); }); }; | ||
} | ||
/** | ||
* Determines whether the given decorator should be re-written as an annotation. | ||
*/ | ||
ClassRewriter.prototype.shouldLower = function (decorator) { | ||
// Walk down the expression to find the identifier of the decorator function | ||
var node = decorator; | ||
while (node.kind !== ts.SyntaxKind.Identifier) { | ||
switch (node.kind) { | ||
case ts.SyntaxKind.Decorator: | ||
node = node.expression; | ||
break; | ||
case ts.SyntaxKind.CallExpression: | ||
node = node.expression; | ||
break; | ||
default: | ||
return false; | ||
} | ||
} | ||
var decSym = this.typeChecker.getSymbolAtLocation(node); | ||
if (decSym.flags & ts.SymbolFlags.Alias) { | ||
decSym = this.typeChecker.getAliasedSymbol(decSym); | ||
} | ||
return decSym.getDocumentationComment().some(function (c) { return c.text.indexOf('@Annotation') >= 0; }); | ||
}; | ||
/** | ||
* process is the main entry point, rewriting a single class node. | ||
@@ -25,3 +51,5 @@ */ | ||
if (node.decorators) { | ||
this.decorators = node.decorators.slice(); | ||
var toLower = this.decoratorsToLower(node); | ||
if (toLower.length > 0) | ||
this.decorators = toLower; | ||
} | ||
@@ -58,4 +86,4 @@ var pos = node.getFullStart(); | ||
if (param.decorators) { | ||
decorators = param.decorators.slice(); | ||
hasDecoratedParam = true; | ||
decorators = this.decoratorsToLower(param); | ||
hasDecoratedParam = decorators.length > 0; | ||
} | ||
@@ -65,3 +93,3 @@ if (param.type) { | ||
// Verify that "Bar" is a value (e.g. a constructor) and not just a type. | ||
var sym = this.typeChecker.getSymbolAtLocation(param.type); | ||
var sym = this.typeChecker.getTypeAtLocation(param.type).getSymbol(); | ||
if (sym && (sym.flags & ts.SymbolFlags.Value)) { | ||
@@ -96,3 +124,5 @@ paramCtor = new type_translator_1.TypeTranslator(this.typeChecker, param.type).symbolToString(sym); | ||
var name = method.name.text; | ||
var decorators = method.decorators.slice(); | ||
var decorators = this.decoratorsToLower(method); | ||
if (decorators.length === 0) | ||
return; | ||
if (!this.propDecorators) | ||
@@ -125,4 +155,3 @@ this.propDecorators = {}; | ||
case ts.SyntaxKind.Decorator: | ||
// Skip emit of all decorators, as they are specially handled. | ||
return true; | ||
return this.shouldLower(node); | ||
default: | ||
@@ -242,2 +271,3 @@ return false; | ||
function convertDecorators(typeChecker, sourceFile) { | ||
type_translator_1.assertTypeChecked(sourceFile); | ||
return new DecoratorRewriter(typeChecker, sourceFile).process(); | ||
@@ -244,0 +274,0 @@ } |
@@ -0,3 +1,3 @@ | ||
#!/usr/bin/env node | ||
"use strict"; | ||
var closure = require('google-closure-compiler'); | ||
var fs = require('fs'); | ||
@@ -7,11 +7,6 @@ var minimist = require('minimist'); | ||
var ts = require('typescript'); | ||
var cli_support = require('./cli_support'); | ||
var tsickle = require('./tsickle'); | ||
/** | ||
* 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 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 --externs=PATH save generated Closure externs.js to PATH\n --output=PATH write final Closure bundle to PATH\n"); | ||
console.error("usage: tsickle [tsickle options] -- [tsc options]\n\nexample:\n tsickle --externs=foo/externs.js -- -p src --noImplicitAny\n\ntsickle flags are:\n --externs=PATH save generated Closure externs.js to PATH\n --untyped convert every type in TypeScript to the Closure {?} type\n"); | ||
} | ||
@@ -23,6 +18,3 @@ /** | ||
function loadSettingsFromArgs(args) { | ||
var settings = { | ||
saveTemporaries: null, | ||
outputPath: null, | ||
}; | ||
var settings = { isUntyped: false }; | ||
var parsedArgs = minimist(args); | ||
@@ -37,10 +29,7 @@ for (var _i = 0, _a = Object.keys(parsedArgs); _i < _a.length; _i++) { | ||
break; | ||
case 'saveTemporaries': | ||
settings.saveTemporaries = true; | ||
break; | ||
case 'externs': | ||
settings.externsPath = parsedArgs[flag]; | ||
break; | ||
case 'output': | ||
settings.outputPath = parsedArgs[flag]; | ||
case 'untyped': | ||
settings.isUntyped = true; | ||
break; | ||
@@ -56,7 +45,2 @@ case '_': | ||
} | ||
if (!settings.outputPath && !settings.externsPath) { | ||
console.error('must specify --output or --externs path'); | ||
usage(); | ||
process.exit(1); | ||
} | ||
// Arguments after the '--' arg are arguments to tsc. | ||
@@ -129,3 +113,3 @@ var tscArgs = parsedArgs['_']; | ||
*/ | ||
function toClosureJS(options, fileNames) { | ||
function toClosureJS(options, fileNames, isUntyped) { | ||
// Parse and load the program without tsickle processing. | ||
@@ -140,6 +124,4 @@ // This is so: | ||
} | ||
// TODO(evanm): let the user configure tsickle options via the command line. | ||
// Or, better, just make tsickle always work without needing any options. | ||
var tsickleOptions = { | ||
untyped: true, | ||
untyped: isUntyped, | ||
}; | ||
@@ -175,37 +157,11 @@ // Process each input file with tsickle and save the output. | ||
} | ||
// Postprocess generated JS. | ||
function pathToModuleName(context, fileName) { | ||
// TODO(evanm): something more sophisticated here? | ||
return fileName.replace('/', '.'); | ||
} | ||
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; | ||
if (path.extname(fileName) !== '.map') { | ||
var output = tsickle.convertCommonJsToGoogModule(fileName, jsFiles[fileName], cli_support.pathToModuleName).output; | ||
jsFiles[fileName] = output; | ||
} | ||
} | ||
jsFiles[internalExternsFileName] = tsickleExterns; | ||
return { jsFiles: jsFiles }; | ||
return { jsFiles: jsFiles, externs: tsickleExterns }; | ||
} | ||
function closureCompile(jsFiles, outFile, callback) { | ||
var closureOptions = { | ||
// Read input files from stdin as JSON. | ||
'json_streams': 'IN', | ||
// Write output file to disk. | ||
'js_output_file': outFile, | ||
'warning_level': 'VERBOSE', | ||
'language_in': 'ECMASCRIPT6_STRICT', | ||
'compilation_level': 'ADVANCED_OPTIMIZATIONS', | ||
}; | ||
var compiler = new closure.compiler(closureOptions); | ||
var process = compiler.run(function (exitCode, stdout, stderr) { callback(exitCode, stderr); }); | ||
var jsonInput = []; | ||
for (var _i = 0, _a = Object.keys(jsFiles); _i < _a.length; _i++) { | ||
var fileName = _a[_i]; | ||
jsonInput.push({ | ||
path: fileName, | ||
src: jsFiles[fileName], | ||
}); | ||
} | ||
process.stdin.end(JSON.stringify(jsonInput)); | ||
} | ||
function main(args) { | ||
@@ -220,3 +176,4 @@ var _a = loadSettingsFromArgs(args), settings = _a.settings, tscArgs = _a.tscArgs; | ||
var jsFiles; | ||
(_c = toClosureJS(options, fileNames), jsFiles = _c.jsFiles, errors = _c.errors, _c); | ||
var externs; | ||
(_c = toClosureJS(options, fileNames, settings.isUntyped), jsFiles = _c.jsFiles, externs = _c.externs, errors = _c.errors, _c); | ||
if (errors && errors.length > 0) { | ||
@@ -230,19 +187,9 @@ console.error(tsickle.formatDiagnostics(errors)); | ||
} | ||
if (settings.saveTemporaries) { | ||
for (var _i = 0, _d = Object.keys(jsFiles); _i < _d.length; _i++) { | ||
var fileName = _d[_i]; | ||
fs.writeFileSync(fileName, jsFiles[fileName]); | ||
} | ||
for (var _i = 0, _d = Object.keys(jsFiles); _i < _d.length; _i++) { | ||
var fileName = _d[_i]; | ||
fs.writeFileSync(fileName, jsFiles[fileName]); | ||
} | ||
if (settings.externsPath) { | ||
fs.writeFileSync(settings.externsPath, jsFiles[internalExternsFileName]); | ||
fs.writeFileSync(settings.externsPath, externs); | ||
} | ||
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; | ||
@@ -249,0 +196,0 @@ } |
@@ -116,3 +116,21 @@ "use strict"; | ||
exports.Rewriter = Rewriter; | ||
/** Returns the string contents of a ts.Identifier. */ | ||
function getIdentifierText(identifier) { | ||
// NOTE: the 'text' property on an Identifier may be escaped if it starts | ||
// with '__', so just use getText(). | ||
return identifier.getText(); | ||
} | ||
exports.getIdentifierText = getIdentifierText; | ||
/** | ||
* 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; | ||
} | ||
exports.unescapeName = unescapeName; | ||
//# sourceMappingURL=rewriter.js.map |
@@ -8,6 +8,8 @@ "use strict"; | ||
var ts = require('typescript'); | ||
var rewriter_1 = require('./rewriter'); | ||
var type_translator_1 = require('./type-translator'); | ||
var rewriter_1 = require('./rewriter'); | ||
var decorator_annotator_1 = require('./decorator-annotator'); | ||
exports.convertDecorators = decorator_annotator_1.convertDecorators; | ||
var es5processor_1 = require('./es5processor'); | ||
exports.convertCommonJsToGoogModule = es5processor_1.processES5; | ||
/** | ||
@@ -94,17 +96,2 @@ * Symbols that are already declared as externs in Closure, that should | ||
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; | ||
@@ -307,3 +294,3 @@ /** | ||
var sym = exports_2[_a]; | ||
var name_1 = unescapeName(sym.name); | ||
var name_1 = rewriter_1.unescapeName(sym.name); | ||
if (localSet.hasOwnProperty(name_1)) { | ||
@@ -325,2 +312,16 @@ // This name is shadowed by a local definition, such as: | ||
/** | ||
* @return the ts.Symbol if an identifier's symbol points at a type. | ||
*/ | ||
Annotator.prototype.getTypeSymbol = function (ident) { | ||
var typeChecker = this.program.getTypeChecker(); | ||
var sym = typeChecker.getSymbolAtLocation(ident); | ||
var isType = (typeChecker.getAliasedSymbol(sym).flags & ts.SymbolFlags.Type) !== 0; | ||
if (isType) { | ||
return sym; | ||
} | ||
else { | ||
return null; | ||
} | ||
}; | ||
/** | ||
* Handles emit of an "import ..." statement. | ||
@@ -339,15 +340,8 @@ * We need to do a bit of rewriting so that imported types show up under the | ||
} | ||
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: | ||
else if (importClause.name || importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) { | ||
// importClause.name implies | ||
// import a from ...; | ||
// namedBindings being NamedImports implies | ||
// import {a as b} from ...; | ||
// In both cases, if a is a type, rewrite it to something like: | ||
// import {a as tsickle_b} from ...; | ||
@@ -363,28 +357,43 @@ // const b = tsickle_b; | ||
// 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 {'); | ||
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); | ||
if (importClause.name) { | ||
// import a from ...; | ||
var name_2 = rewriter_1.getIdentifierText(importClause.name); | ||
var renameSym = this.getTypeSymbol(importClause.name); | ||
if (renameSym) { | ||
renamed.push(renameSym); | ||
this.emit("tsickle_" + name_2); | ||
} | ||
else if (imp.propertyName) { | ||
this.emit(" as " + name_2); | ||
else { | ||
this.emit(name_2); | ||
} | ||
this.emit(','); | ||
} | ||
this.emit('} from'); | ||
else { | ||
// import {a as b} from ...; | ||
this.emit('{'); | ||
var namedImports = importClause.namedBindings; | ||
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(rewriter_1.getIdentifierText(imp.propertyName || imp.name)); | ||
var name_3 = rewriter_1.getIdentifierText(imp.name); | ||
var renameSym = this.getTypeSymbol(imp.name); | ||
if (renameSym) { | ||
renamed.push(renameSym); | ||
this.emit(" as tsickle_" + name_3); | ||
} | ||
else if (imp.propertyName) { | ||
this.emit(" as " + name_3); | ||
} | ||
this.emit(','); | ||
} | ||
this.emit('}'); | ||
} | ||
this.emit(' from'); | ||
this.writeNode(decl.moduleSpecifier); | ||
@@ -395,3 +404,3 @@ this.emit(';\n'); | ||
var sym = renamed_1[_b]; | ||
var name_3 = unescapeName(sym.name); | ||
var name_4 = rewriter_1.unescapeName(sym.name); | ||
// Note that tsickle_foo types are eventually always both values and types, because | ||
@@ -403,9 +412,13 @@ // post-tsickle processing (in Closure-land), all types are also values. | ||
// But until then we just hack this symbol in to the value namespace. | ||
this.emit("declare var tsickle_" + name_3 + ";\n"); | ||
this.emit("declare var tsickle_" + name_4 + ";\n"); | ||
} | ||
this.emit("const " + name_3 + " = tsickle_" + name_3 + ";\n"); | ||
this.emit("type " + name_3 + " = tsickle_" + name_3 + ";\n"); | ||
this.emit("const " + name_4 + " = tsickle_" + name_4 + ";\n"); | ||
this.emit("type " + name_4 + " = tsickle_" + name_4 + ";\n"); | ||
} | ||
return true; | ||
} | ||
else if (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { | ||
// import * as foo from ...; | ||
return false; // Use default processing. | ||
} | ||
else { | ||
@@ -444,3 +457,3 @@ this.errorUnimplementedKind(decl, 'unexpected kind of import'); | ||
optional: paramNode.initializer !== undefined || paramNode.questionToken !== undefined, | ||
parameterName: unescapeName(paramSym.getName()), | ||
parameterName: rewriter_1.unescapeName(paramSym.getName()), | ||
}; | ||
@@ -519,4 +532,9 @@ var destructuring = (paramNode.name.kind === ts.SyntaxKind.ArrayBindingPattern || | ||
return; | ||
// If this symbol is both a type and a value, we cannot emit both into Closure's | ||
// single namespace. | ||
var sym = this.program.getTypeChecker().getSymbolAtLocation(iface.name); | ||
if (sym.flags & ts.SymbolFlags.Value) | ||
return; | ||
this.emit("\n/** @record */\n"); | ||
var name = getIdentifierText(iface.name); | ||
var name = rewriter_1.getIdentifierText(iface.name); | ||
this.emit("function " + name + "() {}\n"); | ||
@@ -577,4 +595,4 @@ if (iface.typeParameters) { | ||
this.emit('\n\n static _tsickle_typeAnnotationsHelper() {\n'); | ||
staticProps.forEach(function (p) { return _this.visitProperty([getIdentifierText(classDecl.name)], p); }); | ||
var memberNamespace = [getIdentifierText(classDecl.name), 'prototype']; | ||
staticProps.forEach(function (p) { return _this.visitProperty([rewriter_1.getIdentifierText(classDecl.name)], p); }); | ||
var memberNamespace = [rewriter_1.getIdentifierText(classDecl.name), 'prototype']; | ||
nonStaticProps.forEach(function (p) { return _this.visitProperty(memberNamespace, p); }); | ||
@@ -589,3 +607,3 @@ paramProps.forEach(function (p) { return _this.visitProperty(memberNamespace, p); }); | ||
case ts.SyntaxKind.Identifier: | ||
return getIdentifierText(prop.name); | ||
return rewriter_1.getIdentifierText(prop.name); | ||
case ts.SyntaxKind.StringLiteral: | ||
@@ -653,9 +671,9 @@ // E.g. interface Foo { 'bar': number; } | ||
// E.g. "declare namespace foo {" | ||
var name_4; | ||
var name_5; | ||
switch (decl.name.kind) { | ||
case ts.SyntaxKind.Identifier: | ||
name_4 = getIdentifierText(decl.name); | ||
name_5 = rewriter_1.getIdentifierText(decl.name); | ||
break; | ||
case ts.SyntaxKind.StringLiteral: | ||
name_4 = decl.name.text; | ||
name_5 = decl.name.text; | ||
break; | ||
@@ -666,3 +684,3 @@ default: | ||
} | ||
namespace = namespace.concat(name_4); | ||
namespace = namespace.concat(name_5); | ||
var nsName = namespace.join('.'); | ||
@@ -769,7 +787,7 @@ if (!this.emittedNamespaces.hasOwnProperty(nsName)) { | ||
// properties. | ||
var name_5 = namespace; | ||
var name_6 = namespace; | ||
if (member.name) { | ||
name_5 = name_5.concat([member.name.getText()]); | ||
name_6 = name_6.concat([member.name.getText()]); | ||
} | ||
this.emit("\n/* TODO: " + ts.SyntaxKind[member.kind] + " in " + name_5.join('.') + " */\n"); | ||
this.emit("\n/* TODO: " + ts.SyntaxKind[member.kind] + " in " + name_6.join('.') + " */\n"); | ||
} | ||
@@ -781,3 +799,3 @@ } | ||
var identifier = decl.name; | ||
var qualifiedName = namespace.concat([getIdentifierText(identifier)]).join('.'); | ||
var qualifiedName = namespace.concat([rewriter_1.getIdentifierText(identifier)]).join('.'); | ||
if (exports.closureExternsBlacklist.indexOf(qualifiedName) >= 0) | ||
@@ -807,3 +825,3 @@ return; | ||
Annotator.prototype.writeExternsEnum = function (decl, namespace) { | ||
namespace = namespace.concat([getIdentifierText(decl.name)]); | ||
namespace = namespace.concat([rewriter_1.getIdentifierText(decl.name)]); | ||
this.emit('\n/** @const */\n'); | ||
@@ -814,5 +832,5 @@ this.emit(namespace.join('.') + " = {};\n"); | ||
var memberName = member.name.getText(); | ||
var name_6 = namespace.concat([memberName]).join('.'); | ||
var name_7 = namespace.concat([memberName]).join('.'); | ||
this.emit('/** @const {number} */\n'); | ||
this.emit(name_6 + ";\n"); | ||
this.emit(name_7 + ";\n"); | ||
} | ||
@@ -969,268 +987,7 @@ }; | ||
if (options === void 0) { options = {}; } | ||
type_translator_1.assertTypeChecked(file); | ||
return new Annotator(program, file, options).annotate(); | ||
} | ||
exports.annotate = annotate; | ||
/** | ||
* PostProcessor postprocesses TypeScript compilation output JS, to rewrite commonjs require()s into | ||
* goog.require(). | ||
*/ | ||
var PostProcessor = (function (_super) { | ||
__extends(PostProcessor, _super); | ||
function PostProcessor(file, pathToModuleName) { | ||
_super.call(this, file); | ||
this.pathToModuleName = pathToModuleName; | ||
/** | ||
* namespaceImports collects the variables for imported goog.modules. | ||
* If the original TS input is: | ||
* import foo from 'goog:bar'; | ||
* then TS produces: | ||
* var foo = require('goog:bar'); | ||
* and this class rewrites it to: | ||
* var foo = require('goog.bar'); | ||
* After this step, namespaceImports['foo'] is true. | ||
* (This is used to rewrite 'foo.default' into just 'foo'.) | ||
*/ | ||
this.namespaceImports = {}; | ||
/** | ||
* moduleVariables maps from module names to the variables they're assigned to. | ||
* Continuing the above example, moduleVariables['goog.bar'] = 'foo'. | ||
*/ | ||
this.moduleVariables = {}; | ||
/** strippedStrict is true once we've stripped a "use strict"; from the input. */ | ||
this.strippedStrict = false; | ||
/** unusedIndex is used to generate fresh symbols for unnamed imports. */ | ||
this.unusedIndex = 0; | ||
} | ||
PostProcessor.prototype.process = function () { | ||
// TODO(evanm): only emit the goog.module *after* the first comment, | ||
// so that @suppress statements work. | ||
var moduleName = this.pathToModuleName('', this.file.fileName); | ||
// NB: No linebreak after module call so sourcemaps are not offset. | ||
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; | ||
for (var _i = 0, _a = this.file.statements; _i < _a.length; _i++) { | ||
var stmt = _a[_i]; | ||
this.writeRange(pos, stmt.getFullStart()); | ||
this.visitTopLevel(stmt); | ||
pos = stmt.getEnd(); | ||
} | ||
this.writeRange(pos, this.file.getEnd()); | ||
var referencedModules = Object.keys(this.moduleVariables); | ||
// Note: don't sort referencedModules, as the keys are in the same order | ||
// they occur in the source file. | ||
var output = this.getOutput().output; | ||
return { output: output, referencedModules: referencedModules }; | ||
}; | ||
/** | ||
* visitTopLevel processes a top-level ts.Node and emits its contents. | ||
* | ||
* It's separate from the normal Rewriter recursive traversal | ||
* because some top-level statements are handled specially. | ||
*/ | ||
PostProcessor.prototype.visitTopLevel = function (node) { | ||
switch (node.kind) { | ||
case ts.SyntaxKind.ExpressionStatement: | ||
// Check for "use strict" and skip it if necessary. | ||
if (!this.strippedStrict && this.isUseStrict(node)) { | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
this.strippedStrict = true; | ||
return; | ||
} | ||
// Check for: | ||
// - "require('foo');" (a require for its side effects) | ||
// - "__export(require(...));" (an "export * from ...") | ||
if (this.emitRewrittenRequires(node)) { | ||
return; | ||
} | ||
// Otherwise fall through to default processing. | ||
break; | ||
case ts.SyntaxKind.VariableStatement: | ||
// Check for a "var x = require('foo');". | ||
if (this.emitRewrittenRequires(node)) | ||
return; | ||
break; | ||
default: | ||
break; | ||
} | ||
this.visit(node); | ||
}; | ||
/** isUseStrict returns true if node is a "use strict"; statement. */ | ||
PostProcessor.prototype.isUseStrict = function (node) { | ||
if (node.kind !== ts.SyntaxKind.ExpressionStatement) | ||
return false; | ||
var exprStmt = node; | ||
var expr = exprStmt.expression; | ||
if (expr.kind !== ts.SyntaxKind.StringLiteral) | ||
return false; | ||
var literal = expr; | ||
return literal.text === 'use strict'; | ||
}; | ||
/** | ||
* emitRewrittenRequires rewrites require()s into goog.require() equivalents. | ||
* | ||
* @return True if the node was rewritten, false if needs ordinary processing. | ||
*/ | ||
PostProcessor.prototype.emitRewrittenRequires = function (node) { | ||
// We're looking for requires, of one of the forms: | ||
// - "var importName = require(...);". | ||
// - "require(...);". | ||
// Find the CallExpression contained in either of these. | ||
var varName; // E.g. importName in the above example. | ||
var call; // The require(...) expression. | ||
if (node.kind === ts.SyntaxKind.VariableStatement) { | ||
// It's possibly of the form "var x = require(...);". | ||
var varStmt = node; | ||
// Verify it's a single decl (and not "var x = ..., y = ...;"). | ||
if (varStmt.declarationList.declarations.length !== 1) | ||
return false; | ||
var decl = varStmt.declarationList.declarations[0]; | ||
// Grab the variable name (avoiding things like destructuring binds). | ||
if (decl.name.kind !== ts.SyntaxKind.Identifier) | ||
return false; | ||
varName = getIdentifierText(decl.name); | ||
if (!decl.initializer || decl.initializer.kind !== ts.SyntaxKind.CallExpression) | ||
return false; | ||
call = decl.initializer; | ||
} | ||
else if (node.kind === ts.SyntaxKind.ExpressionStatement) { | ||
// It's possibly of the form: | ||
// - require(...); | ||
// - __export(require(...)); | ||
// Both are CallExpressions. | ||
var exprStmt = node; | ||
var expr = exprStmt.expression; | ||
if (expr.kind !== ts.SyntaxKind.CallExpression) | ||
return false; | ||
call = expr; | ||
var require_1 = this.isExportRequire(call); | ||
if (require_1) { | ||
var modName_1 = this.pathToModuleName(this.file.fileName, require_1); | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
this.emit("__export(goog.require('" + modName_1 + "'));"); | ||
// Mark that this module was imported; it doesn't have an associated | ||
// variable so just call the variable "*". | ||
this.moduleVariables[modName_1] = '*'; | ||
return true; | ||
} | ||
} | ||
else { | ||
// It's some other type of statement. | ||
return false; | ||
} | ||
var require = this.isRequire(call); | ||
if (!require) | ||
return false; | ||
// Even if it's a bare require(); statement, introduce a variable for it. | ||
// This avoids a Closure error. | ||
if (!varName) { | ||
varName = "unused_" + this.unusedIndex++ + "_"; | ||
} | ||
var modName; | ||
if (require.match(/^goog:/)) { | ||
// This is a namespace import, of the form "goog:foo.bar". | ||
// Fix it to just "foo.bar", and save the variable name. | ||
modName = require.substr(5); | ||
this.namespaceImports[varName] = true; | ||
} | ||
else { | ||
modName = this.pathToModuleName(this.file.fileName, require); | ||
} | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
if (this.moduleVariables.hasOwnProperty(modName)) { | ||
this.emit("var " + varName + " = " + this.moduleVariables[modName] + ";"); | ||
} | ||
else { | ||
this.emit("var " + varName + " = goog.require('" + modName + "');"); | ||
this.moduleVariables[modName] = varName; | ||
} | ||
return true; | ||
}; | ||
// workaround for syntax highlighting bug in Sublime: ` | ||
/** | ||
* Returns the string argument if call is of the form | ||
* require('foo') | ||
*/ | ||
PostProcessor.prototype.isRequire = function (call) { | ||
// Verify that the call is a call to require(...). | ||
if (call.expression.kind !== ts.SyntaxKind.Identifier) | ||
return null; | ||
var ident = call.expression; | ||
if (getIdentifierText(ident) !== 'require') | ||
return null; | ||
// Verify the call takes a single string argument and grab it. | ||
if (call.arguments.length !== 1) | ||
return null; | ||
var arg = call.arguments[0]; | ||
if (arg.kind !== ts.SyntaxKind.StringLiteral) | ||
return null; | ||
return arg.text; | ||
}; | ||
/** | ||
* Returns the inner string if call is of the form | ||
* __export(require('foo')) | ||
*/ | ||
PostProcessor.prototype.isExportRequire = function (call) { | ||
if (call.expression.kind !== ts.SyntaxKind.Identifier) | ||
return null; | ||
var ident = call.expression; | ||
if (ident.getText() !== '__export') | ||
return null; | ||
// Verify the call takes a single string argument and grab it. | ||
if (call.arguments.length !== 1) | ||
return null; | ||
var arg = call.arguments[0]; | ||
if (arg.kind !== ts.SyntaxKind.CallExpression) | ||
return null; | ||
return this.isRequire(arg); | ||
}; | ||
/** | ||
* maybeProcess is called during the recursive traversal of the program's AST. | ||
* | ||
* @return True if the node was processed/emitted, false if it should be emitted as is. | ||
*/ | ||
PostProcessor.prototype.maybeProcess = function (node) { | ||
switch (node.kind) { | ||
case ts.SyntaxKind.PropertyAccessExpression: | ||
var propAccess = node; | ||
// We're looking for an expression of the form: | ||
// module_name_var.default | ||
if (getIdentifierText(propAccess.name) !== 'default') | ||
break; | ||
if (propAccess.expression.kind !== ts.SyntaxKind.Identifier) | ||
break; | ||
var lhs = getIdentifierText(propAccess.expression); | ||
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(lhs + " "); | ||
return true; | ||
default: | ||
break; | ||
} | ||
return false; | ||
}; | ||
return PostProcessor; | ||
}(rewriter_1.Rewriter)); | ||
/** | ||
* Converts TypeScript's JS+CommonJS output to Closure goog.module etc. | ||
* For use as a postprocessing step *after* TypeScript emits JavaScript. | ||
* | ||
* @param fileName The source file name, without an extension. | ||
* @param pathToModuleName A function that maps a filesystem .ts path to a | ||
* Closure module name, as found in a goog.require('...') statement. | ||
* The context parameter is the referencing file, used for resolving | ||
* imports with relative paths like "import * as foo from '../foo';". | ||
*/ | ||
function convertCommonJsToGoogModule(fileName, content, pathToModuleName) { | ||
var file = ts.createSourceFile(fileName, content, ts.ScriptTarget.ES5, true); | ||
return new PostProcessor(file, pathToModuleName).process(); | ||
} | ||
exports.convertCommonJsToGoogModule = convertCommonJsToGoogModule; | ||
//# sourceMappingURL=tsickle.js.map |
"use strict"; | ||
var ts = require('typescript'); | ||
function assertTypeChecked(sourceFile) { | ||
if (!('resolvedModules' in sourceFile)) { | ||
throw new Error('must provide typechecked program'); | ||
} | ||
} | ||
exports.assertTypeChecked = assertTypeChecked; | ||
/** | ||
* Determines if fileName refers to a builtin lib.d.ts file. | ||
* This is a terrible hack but it mirrors a similar thing done in Clutz. | ||
*/ | ||
function isBuiltinLibDTS(fileName) { | ||
return fileName.match(/\blib\.[^/]+\.d\.ts$/) != null; | ||
} | ||
/** | ||
* @return True if the named type is considered compatible with the Closure-defined | ||
* type of the same name, e.g. "Array". Note that we don't actually enforce | ||
* that the types are actually compatible, but mostly just hope that they are due | ||
* to being derived from the same HTML specs. | ||
*/ | ||
function isClosureProvidedType(symbol) { | ||
return symbol.declarations.every(function (n) { return isBuiltinLibDTS(n.getSourceFile().fileName); }); | ||
} | ||
function typeToDebugString(type) { | ||
@@ -139,3 +161,6 @@ var basicTypes = [ | ||
var notNullPrefix = notNull ? '!' : ''; | ||
if (type.flags & (ts.TypeFlags.Interface | ts.TypeFlags.Class)) { | ||
if (type.flags & ts.TypeFlags.Class) { | ||
return this.symbolToString(type.symbol); | ||
} | ||
else if (type.flags & ts.TypeFlags.Interface) { | ||
// Note: ts.InterfaceType has a typeParameters field, but that | ||
@@ -148,2 +173,11 @@ // specifies the parameters that the interface type *expects* | ||
// the InterfaceType. | ||
if (type.symbol.flags & ts.SymbolFlags.Value) { | ||
// The symbol is both a type and a value. | ||
// For user-defined types in this state, we don't have a Closure name | ||
// for the type. See the type_and_value test. | ||
if (!isClosureProvidedType(type.symbol)) { | ||
this.warn("type/symbol conflict for " + type.symbol.name + ", using {?} for now"); | ||
return '?'; | ||
} | ||
} | ||
return this.symbolToString(type.symbol); | ||
@@ -150,0 +184,0 @@ } |
{ | ||
"name": "tsickle", | ||
"version": "0.1.5", | ||
"version": "0.1.6", | ||
"description": "Transpile TypeScript code to JavaScript with Closure annotations.", | ||
@@ -21,3 +21,3 @@ "main": "build/src/tsickle.js", | ||
"chai": "^2.1.1", | ||
"clang-format": "1.0.37", | ||
"clang-format": "^1.0.41", | ||
"glob": "^7.0.0", | ||
@@ -24,0 +24,0 @@ "google-closure-compiler": "^20160315.2.0", |
# Tsickle - TypeScript to Closure Annotator [![Build Status](https://travis-ci.org/angular/tsickle.svg?branch=master)](https://travis-ci.org/angular/tsickle) | ||
Tsickle processes TypeScript and adds [Closure Compiler](https://github.com/google/closure-compiler/) | ||
-compatible JSDoc annotations. This allows using TypeScript to transpile your sources, and then | ||
Closure Compiler to bundle and optimize them, while taking advantage of type information in Closure | ||
Compiler. | ||
Tsickle processes TypeScript and adds [Closure Compiler]-compatible JSDoc | ||
annotations. This allows using TypeScript to transpile your sources, and then | ||
Closure Compiler to bundle and optimize them, while taking advantage of type | ||
information in Closure Compiler. | ||
[Closure Compiler]: https://github.com/google/closure-compiler/ | ||
## Installation | ||
- execute `npm i` to install the dependencies, | ||
- Execute `npm i` to install the dependencies. | ||
## Gulp tasks | ||
## Usage | ||
- `gulp watch` executes the unit tests in watch mode (use `gulp test.unit` for a single run), | ||
### Project Setup | ||
Tsickle works by wrapping `tsc`. To use it, you must set up your project such | ||
that it builds correctly when you run `tsc` from the command line, by | ||
configuring the settings in `tsconfig.json`. | ||
If you have complicated tsc command lines and flags in a build file (like a | ||
gulpfile etc.) Tsickle won't know about it. Another reason it's nice to put | ||
everything in `tsconfig.json` is so your editor inherits all these settings as | ||
well. | ||
### Invocation | ||
Run `tsickle --help` for the full syntax, but basically you provide any tsickle | ||
specific options and use it as a TypeScript compiler. | ||
## Development | ||
### Gulp tasks | ||
- `gulp watch` executes the unit tests in watch mode (use `gulp test.unit` for a | ||
single run), | ||
- `gulp test.e2e` executes the e2e tests, | ||
- `gulp test.check-format` checks the source code formatting using `clang-format`, | ||
- `gulp test.check-format` checks the source code formatting using | ||
`clang-format`, | ||
- `gulp test` runs unit tests, e2e tests and checks the source code formatting. | ||
Export the environment variable `UPDATE_GOLDENS=1` to have the test suite | ||
rewrite the golden files when you run it. |
import * as ts from 'typescript'; | ||
import {Rewriter} from './rewriter'; | ||
import {TypeTranslator} from './type-translator'; | ||
import {TypeTranslator, assertTypeChecked} from './type-translator'; | ||
@@ -19,2 +19,30 @@ // ClassRewriter rewrites a single "class Foo {...}" declaration. | ||
/** | ||
* Determines whether the given decorator should be re-written as an annotation. | ||
*/ | ||
private shouldLower(decorator: ts.Decorator) { | ||
// Walk down the expression to find the identifier of the decorator function | ||
let node: ts.Node = decorator; | ||
while (node.kind !== ts.SyntaxKind.Identifier) { | ||
switch (node.kind) { | ||
case ts.SyntaxKind.Decorator: | ||
node = (node as ts.Decorator).expression; | ||
break; | ||
case ts.SyntaxKind.CallExpression: | ||
node = (node as ts.CallExpression).expression; | ||
break; | ||
default: | ||
return false; | ||
} | ||
} | ||
let decSym = this.typeChecker.getSymbolAtLocation(node); | ||
if (decSym.flags & ts.SymbolFlags.Alias) { | ||
decSym = this.typeChecker.getAliasedSymbol(decSym); | ||
} | ||
return decSym.getDocumentationComment().some(c => c.text.indexOf('@Annotation') >= 0); | ||
} | ||
private decoratorsToLower = (n: ts.Node) => n.decorators.filter(d => this.shouldLower(d)); | ||
/** | ||
* process is the main entry point, rewriting a single class node. | ||
@@ -24,3 +52,4 @@ */ | ||
if (node.decorators) { | ||
this.decorators = node.decorators.slice(); | ||
let toLower = this.decoratorsToLower(node); | ||
if (toLower.length > 0) this.decorators = toLower; | ||
} | ||
@@ -56,4 +85,4 @@ let pos = node.getFullStart(); | ||
if (param.decorators) { | ||
decorators = param.decorators.slice(); | ||
hasDecoratedParam = true; | ||
decorators = this.decoratorsToLower(param); | ||
hasDecoratedParam = decorators.length > 0; | ||
} | ||
@@ -63,3 +92,3 @@ if (param.type) { | ||
// Verify that "Bar" is a value (e.g. a constructor) and not just a type. | ||
let sym = this.typeChecker.getSymbolAtLocation(param.type); | ||
let sym = this.typeChecker.getTypeAtLocation(param.type).getSymbol(); | ||
if (sym && (sym.flags & ts.SymbolFlags.Value)) { | ||
@@ -95,3 +124,4 @@ paramCtor = new TypeTranslator(this.typeChecker, param.type).symbolToString(sym); | ||
let name = (method.name as ts.Identifier).text; | ||
let decorators: ts.Decorator[] = method.decorators.slice(); | ||
let decorators: ts.Decorator[] = this.decoratorsToLower(method); | ||
if (decorators.length === 0) return; | ||
if (!this.propDecorators) this.propDecorators = {}; | ||
@@ -125,4 +155,3 @@ this.propDecorators[name] = decorators; | ||
case ts.SyntaxKind.Decorator: | ||
// Skip emit of all decorators, as they are specially handled. | ||
return true; | ||
return this.shouldLower(node as ts.Decorator); | ||
default: | ||
@@ -239,3 +268,4 @@ return false; | ||
{output: string, diagnostics: ts.Diagnostic[]} { | ||
assertTypeChecked(sourceFile); | ||
return new DecoratorRewriter(typeChecker, sourceFile).process(); | ||
} |
112
src/main.ts
@@ -1,2 +0,3 @@ | ||
import * as closure from 'google-closure-compiler'; | ||
#!/usr/bin/env node | ||
import * as fs from 'fs'; | ||
@@ -7,33 +8,23 @@ import * as minimist from 'minimist'; | ||
import * as cli_support from './cli_support'; | ||
import * as tsickle from './tsickle'; | ||
/** | ||
* 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 internalExternsFileName = 'tsickle_externs.js'; | ||
/** Tsickle settings passed on the command line. */ | ||
interface Settings { | ||
/** If true, write temporary .js files to disk. Useful for debugging. */ | ||
saveTemporaries?: boolean; | ||
/** If provided, path to save externs to. */ | ||
externsPath?: string; | ||
/** Path to write the final JS bundle to. */ | ||
outputPath: string; | ||
/** If provided, convert every type to the Closure {?} type */ | ||
isUntyped: boolean; | ||
} | ||
function usage() { | ||
console.error(`usage: tsickle [tsickle args] -- [tsc args] | ||
console.error(`usage: tsickle [tsickle options] -- [tsc options] | ||
example: | ||
tsickle --output bundle.js -- -p src --noImplicitAny | ||
tsickle --externs=foo/externs.js -- -p src --noImplicitAny | ||
tsickle flags are: | ||
--saveTemporaries save intermediate .js files to disk | ||
--externs=PATH save generated Closure externs.js to PATH | ||
--output=PATH write final Closure bundle to PATH | ||
--untyped convert every type in TypeScript to the Closure {?} type | ||
`); | ||
@@ -47,6 +38,3 @@ } | ||
function loadSettingsFromArgs(args: string[]): {settings: Settings, tscArgs: string[]} { | ||
let settings: Settings = { | ||
saveTemporaries: null, | ||
outputPath: null, | ||
}; | ||
let settings: Settings = {isUntyped: false}; | ||
let parsedArgs = minimist(args); | ||
@@ -60,10 +48,7 @@ for (let flag of Object.keys(parsedArgs)) { | ||
break; | ||
case 'saveTemporaries': | ||
settings.saveTemporaries = true; | ||
break; | ||
case 'externs': | ||
settings.externsPath = parsedArgs[flag]; | ||
break; | ||
case 'output': | ||
settings.outputPath = parsedArgs[flag]; | ||
case 'untyped': | ||
settings.isUntyped = true; | ||
break; | ||
@@ -79,7 +64,2 @@ case '_': | ||
} | ||
if (!settings.outputPath && !settings.externsPath) { | ||
console.error('must specify --output or --externs path'); | ||
usage(); | ||
process.exit(1); | ||
} | ||
// Arguments after the '--' arg are arguments to tsc. | ||
@@ -163,4 +143,4 @@ let tscArgs = parsedArgs['_']; | ||
*/ | ||
function toClosureJS(options: ts.CompilerOptions, fileNames: string[]): | ||
{jsFiles?: {[fileName: string]: string}, errors?: ts.Diagnostic[]} { | ||
function toClosureJS(options: ts.CompilerOptions, fileNames: string[], isUntyped: boolean): | ||
{jsFiles?: {[fileName: string]: string}, externs?: string, errors?: ts.Diagnostic[]} { | ||
// Parse and load the program without tsickle processing. | ||
@@ -176,6 +156,4 @@ // This is so: | ||
// TODO(evanm): let the user configure tsickle options via the command line. | ||
// Or, better, just make tsickle always work without needing any options. | ||
const tsickleOptions: tsickle.Options = { | ||
untyped: true, | ||
untyped: isUntyped, | ||
}; | ||
@@ -215,44 +193,13 @@ | ||
// Postprocess generated JS. | ||
function pathToModuleName(context: string, fileName: string): string { | ||
// TODO(evanm): something more sophisticated here? | ||
return fileName.replace('/', '.'); | ||
} | ||
for (let fileName of Object.keys(jsFiles)) { | ||
let {output} = | ||
tsickle.convertCommonJsToGoogModule(fileName, jsFiles[fileName], pathToModuleName); | ||
jsFiles[fileName] = output; | ||
if (path.extname(fileName) !== '.map') { | ||
let {output} = tsickle.convertCommonJsToGoogModule( | ||
fileName, jsFiles[fileName], cli_support.pathToModuleName); | ||
jsFiles[fileName] = output; | ||
} | ||
} | ||
jsFiles[internalExternsFileName] = tsickleExterns; | ||
return {jsFiles}; | ||
return {jsFiles, externs: tsickleExterns}; | ||
} | ||
function closureCompile( | ||
jsFiles: {[fileName: string]: string}, outFile: string, | ||
callback: (exitCode: number, output: string) => void): void { | ||
const closureOptions: closure.CompileOptions = { | ||
// Read input files from stdin as JSON. | ||
'json_streams': 'IN', | ||
// Write output file to disk. | ||
'js_output_file': outFile, | ||
'warning_level': 'VERBOSE', | ||
'language_in': 'ECMASCRIPT6_STRICT', | ||
'compilation_level': 'ADVANCED_OPTIMIZATIONS', | ||
}; | ||
let compiler = new closure.compiler(closureOptions); | ||
let process = compiler.run((exitCode, stdout, stderr) => { callback(exitCode, stderr); }); | ||
let jsonInput: closure.JSONStreamFile[] = []; | ||
for (let fileName of Object.keys(jsFiles)) { | ||
jsonInput.push({ | ||
path: fileName, | ||
src: jsFiles[fileName], | ||
}); | ||
} | ||
process.stdin.end(JSON.stringify(jsonInput)); | ||
} | ||
function main(args: string[]) { | ||
@@ -268,3 +215,4 @@ let {settings, tscArgs} = loadSettingsFromArgs(args); | ||
let jsFiles: {[fileName: string]: string}; | ||
({jsFiles, errors} = toClosureJS(options, fileNames)); | ||
let externs: string; | ||
({jsFiles, externs, errors} = toClosureJS(options, fileNames, settings.isUntyped)); | ||
if (errors && errors.length > 0) { | ||
@@ -279,19 +227,9 @@ console.error(tsickle.formatDiagnostics(errors)); | ||
if (settings.saveTemporaries) { | ||
for (let fileName of Object.keys(jsFiles)) { | ||
fs.writeFileSync(fileName, jsFiles[fileName]); | ||
} | ||
for (let fileName of Object.keys(jsFiles)) { | ||
fs.writeFileSync(fileName, jsFiles[fileName]); | ||
} | ||
if (settings.externsPath) { | ||
fs.writeFileSync(settings.externsPath, jsFiles[internalExternsFileName]); | ||
fs.writeFileSync(settings.externsPath, externs); | ||
} | ||
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); | ||
}); | ||
} | ||
} | ||
@@ -298,0 +236,0 @@ |
@@ -125,1 +125,18 @@ import * as ts from 'typescript'; | ||
} | ||
/** Returns the string contents of a ts.Identifier. */ | ||
export 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. | ||
*/ | ||
export function unescapeName(name: string): string { | ||
// See the private function unescapeIdentifier in TypeScript's utilities.ts. | ||
if (name.match(/^___/)) return name.substr(1); | ||
return name; | ||
} |
import * as ts from 'typescript'; | ||
import {TypeTranslator} from './type-translator'; | ||
import {Rewriter} from './rewriter'; | ||
import {Rewriter, getIdentifierText, unescapeName} from './rewriter'; | ||
import {TypeTranslator, assertTypeChecked} from './type-translator'; | ||
export {convertDecorators} from './decorator-annotator'; | ||
export {processES5 as convertCommonJsToGoogModule} from './es5processor'; | ||
@@ -130,18 +133,2 @@ export interface Options { | ||
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; | ||
@@ -369,2 +356,16 @@ | ||
/** | ||
* @return the ts.Symbol if an identifier's symbol points at a type. | ||
*/ | ||
private getTypeSymbol(ident: ts.Identifier): ts.Symbol { | ||
const typeChecker = this.program.getTypeChecker(); | ||
const sym = typeChecker.getSymbolAtLocation(ident); | ||
const isType = (typeChecker.getAliasedSymbol(sym).flags & ts.SymbolFlags.Type) !== 0; | ||
if (isType) { | ||
return sym; | ||
} else { | ||
return null; | ||
} | ||
} | ||
/** | ||
* Handles emit of an "import ..." statement. | ||
@@ -382,13 +383,9 @@ * We need to do a bit of rewriting so that imported types show up under the | ||
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: | ||
} else if ( | ||
importClause.name || importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) { | ||
// importClause.name implies | ||
// import a from ...; | ||
// namedBindings being NamedImports implies | ||
// import {a as b} from ...; | ||
// In both cases, if a is a type, rewrite it to something like: | ||
// import {a as tsickle_b} from ...; | ||
@@ -404,28 +401,41 @@ // const b = tsickle_b; | ||
// 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()); | ||
this.writeRange(decl.getFullStart(), decl.getStart()); | ||
// Emit the "import" part, with rewritten binding names for types. | ||
this.emit('import {'); | ||
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)); | ||
if (importClause.name) { | ||
// import a from ...; | ||
const name = getIdentifierText(importClause.name); | ||
const renameSym = this.getTypeSymbol(importClause.name); | ||
if (renameSym) { | ||
renamed.push(renameSym); | ||
this.emit(`tsickle_${name}`); | ||
} else { | ||
this.emit(name); | ||
} | ||
} else { | ||
// import {a as b} from ...; | ||
this.emit('{'); | ||
const namedImports = importClause.namedBindings as ts.NamedImports; | ||
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}`); | ||
let name = getIdentifierText(imp.name); | ||
let renameSym = this.getTypeSymbol(imp.name); | ||
if (renameSym) { | ||
renamed.push(renameSym); | ||
this.emit(` as tsickle_${name}`); | ||
} else if (imp.propertyName) { | ||
this.emit(` as ${name}`); | ||
} | ||
this.emit(','); | ||
} | ||
this.emit(','); | ||
this.emit('}'); | ||
} | ||
this.emit('} from'); | ||
this.emit(' from'); | ||
this.writeNode(decl.moduleSpecifier); | ||
@@ -449,2 +459,5 @@ this.emit(';\n'); | ||
return true; | ||
} else if (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { | ||
// import * as foo from ...; | ||
return false; // Use default processing. | ||
} else { | ||
@@ -563,2 +576,8 @@ this.errorUnimplementedKind(decl, 'unexpected kind of import'); | ||
if (this.options.untyped) return; | ||
// If this symbol is both a type and a value, we cannot emit both into Closure's | ||
// single namespace. | ||
let sym = this.program.getTypeChecker().getSymbolAtLocation(iface.name); | ||
if (sym.flags & ts.SymbolFlags.Value) return; | ||
this.emit(`\n/** @record */\n`); | ||
@@ -1005,265 +1024,4 @@ let name = getIdentifierText(iface.name); | ||
export function annotate(program: ts.Program, file: ts.SourceFile, options: Options = {}): Output { | ||
assertTypeChecked(file); | ||
return new Annotator(program, file, options).annotate(); | ||
} | ||
/** | ||
* PostProcessor postprocesses TypeScript compilation output JS, to rewrite commonjs require()s into | ||
* goog.require(). | ||
*/ | ||
class PostProcessor extends Rewriter { | ||
/** | ||
* namespaceImports collects the variables for imported goog.modules. | ||
* If the original TS input is: | ||
* import foo from 'goog:bar'; | ||
* then TS produces: | ||
* var foo = require('goog:bar'); | ||
* and this class rewrites it to: | ||
* var foo = require('goog.bar'); | ||
* After this step, namespaceImports['foo'] is true. | ||
* (This is used to rewrite 'foo.default' into just 'foo'.) | ||
*/ | ||
namespaceImports: {[varName: string]: boolean} = {}; | ||
/** | ||
* moduleVariables maps from module names to the variables they're assigned to. | ||
* Continuing the above example, moduleVariables['goog.bar'] = 'foo'. | ||
*/ | ||
moduleVariables: {[moduleName: string]: string} = {}; | ||
/** strippedStrict is true once we've stripped a "use strict"; from the input. */ | ||
strippedStrict: boolean = false; | ||
/** unusedIndex is used to generate fresh symbols for unnamed imports. */ | ||
unusedIndex: number = 0; | ||
constructor( | ||
file: ts.SourceFile, | ||
private pathToModuleName: (context: string, fileName: string) => string) { | ||
super(file); | ||
} | ||
process(): {output: string, referencedModules: string[]} { | ||
// TODO(evanm): only emit the goog.module *after* the first comment, | ||
// so that @suppress statements work. | ||
const moduleName = this.pathToModuleName('', this.file.fileName); | ||
// NB: No linebreak after module call so sourcemaps are not offset. | ||
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}'};`); | ||
let pos = 0; | ||
for (let stmt of this.file.statements) { | ||
this.writeRange(pos, stmt.getFullStart()); | ||
this.visitTopLevel(stmt); | ||
pos = stmt.getEnd(); | ||
} | ||
this.writeRange(pos, this.file.getEnd()); | ||
let referencedModules = Object.keys(this.moduleVariables); | ||
// Note: don't sort referencedModules, as the keys are in the same order | ||
// they occur in the source file. | ||
let {output} = this.getOutput(); | ||
return {output, referencedModules}; | ||
} | ||
/** | ||
* visitTopLevel processes a top-level ts.Node and emits its contents. | ||
* | ||
* It's separate from the normal Rewriter recursive traversal | ||
* because some top-level statements are handled specially. | ||
*/ | ||
visitTopLevel(node: ts.Node) { | ||
switch (node.kind) { | ||
case ts.SyntaxKind.ExpressionStatement: | ||
// Check for "use strict" and skip it if necessary. | ||
if (!this.strippedStrict && this.isUseStrict(node)) { | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
this.strippedStrict = true; | ||
return; | ||
} | ||
// Check for: | ||
// - "require('foo');" (a require for its side effects) | ||
// - "__export(require(...));" (an "export * from ...") | ||
if (this.emitRewrittenRequires(node)) { | ||
return; | ||
} | ||
// Otherwise fall through to default processing. | ||
break; | ||
case ts.SyntaxKind.VariableStatement: | ||
// Check for a "var x = require('foo');". | ||
if (this.emitRewrittenRequires(node)) return; | ||
break; | ||
default: | ||
break; | ||
} | ||
this.visit(node); | ||
} | ||
/** isUseStrict returns true if node is a "use strict"; statement. */ | ||
isUseStrict(node: ts.Node): boolean { | ||
if (node.kind !== ts.SyntaxKind.ExpressionStatement) return false; | ||
let exprStmt = node as ts.ExpressionStatement; | ||
let expr = exprStmt.expression; | ||
if (expr.kind !== ts.SyntaxKind.StringLiteral) return false; | ||
let literal = expr as ts.StringLiteral; | ||
return literal.text === 'use strict'; | ||
} | ||
/** | ||
* emitRewrittenRequires rewrites require()s into goog.require() equivalents. | ||
* | ||
* @return True if the node was rewritten, false if needs ordinary processing. | ||
*/ | ||
emitRewrittenRequires(node: ts.Node): boolean { | ||
// We're looking for requires, of one of the forms: | ||
// - "var importName = require(...);". | ||
// - "require(...);". | ||
// Find the CallExpression contained in either of these. | ||
let varName: string; // E.g. importName in the above example. | ||
let call: ts.CallExpression; // The require(...) expression. | ||
if (node.kind === ts.SyntaxKind.VariableStatement) { | ||
// It's possibly of the form "var x = require(...);". | ||
let varStmt = node as ts.VariableStatement; | ||
// Verify it's a single decl (and not "var x = ..., y = ...;"). | ||
if (varStmt.declarationList.declarations.length !== 1) return false; | ||
let decl = varStmt.declarationList.declarations[0]; | ||
// Grab the variable name (avoiding things like destructuring binds). | ||
if (decl.name.kind !== ts.SyntaxKind.Identifier) return false; | ||
varName = getIdentifierText(decl.name as ts.Identifier); | ||
if (!decl.initializer || decl.initializer.kind !== ts.SyntaxKind.CallExpression) return false; | ||
call = decl.initializer as ts.CallExpression; | ||
} else if (node.kind === ts.SyntaxKind.ExpressionStatement) { | ||
// It's possibly of the form: | ||
// - require(...); | ||
// - __export(require(...)); | ||
// Both are CallExpressions. | ||
let exprStmt = node as ts.ExpressionStatement; | ||
let expr = exprStmt.expression; | ||
if (expr.kind !== ts.SyntaxKind.CallExpression) return false; | ||
call = expr as ts.CallExpression; | ||
let require = this.isExportRequire(call); | ||
if (require) { | ||
let modName = this.pathToModuleName(this.file.fileName, require); | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
this.emit(`__export(goog.require('${modName}'));`); | ||
// Mark that this module was imported; it doesn't have an associated | ||
// variable so just call the variable "*". | ||
this.moduleVariables[modName] = '*'; | ||
return true; | ||
} | ||
} else { | ||
// It's some other type of statement. | ||
return false; | ||
} | ||
let require = this.isRequire(call); | ||
if (!require) return false; | ||
// Even if it's a bare require(); statement, introduce a variable for it. | ||
// This avoids a Closure error. | ||
if (!varName) { | ||
varName = `unused_${this.unusedIndex++}_`; | ||
} | ||
let modName: string; | ||
if (require.match(/^goog:/)) { | ||
// This is a namespace import, of the form "goog:foo.bar". | ||
// Fix it to just "foo.bar", and save the variable name. | ||
modName = require.substr(5); | ||
this.namespaceImports[varName] = true; | ||
} else { | ||
modName = this.pathToModuleName(this.file.fileName, require); | ||
} | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
if (this.moduleVariables.hasOwnProperty(modName)) { | ||
this.emit(`var ${varName} = ${this.moduleVariables[modName]};`); | ||
} else { | ||
this.emit(`var ${varName} = goog.require('${modName}');`); | ||
this.moduleVariables[modName] = varName; | ||
} | ||
return true; | ||
} | ||
// workaround for syntax highlighting bug in Sublime: ` | ||
/** | ||
* Returns the string argument if call is of the form | ||
* require('foo') | ||
*/ | ||
isRequire(call: ts.CallExpression): string { | ||
// Verify that the call is a call to require(...). | ||
if (call.expression.kind !== ts.SyntaxKind.Identifier) return null; | ||
let ident = call.expression as ts.Identifier; | ||
if (getIdentifierText(ident) !== 'require') return null; | ||
// Verify the call takes a single string argument and grab it. | ||
if (call.arguments.length !== 1) return null; | ||
let arg = call.arguments[0]; | ||
if (arg.kind !== ts.SyntaxKind.StringLiteral) return null; | ||
return (arg as ts.StringLiteral).text; | ||
} | ||
/** | ||
* Returns the inner string if call is of the form | ||
* __export(require('foo')) | ||
*/ | ||
isExportRequire(call: ts.CallExpression): string { | ||
if (call.expression.kind !== ts.SyntaxKind.Identifier) return null; | ||
let ident = call.expression as ts.Identifier; | ||
if (ident.getText() !== '__export') return null; | ||
// Verify the call takes a single string argument and grab it. | ||
if (call.arguments.length !== 1) return null; | ||
let arg = call.arguments[0]; | ||
if (arg.kind !== ts.SyntaxKind.CallExpression) return null; | ||
return this.isRequire(arg as ts.CallExpression); | ||
} | ||
/** | ||
* maybeProcess is called during the recursive traversal of the program's AST. | ||
* | ||
* @return True if the node was processed/emitted, false if it should be emitted as is. | ||
*/ | ||
protected maybeProcess(node: ts.Node): boolean { | ||
switch (node.kind) { | ||
case ts.SyntaxKind.PropertyAccessExpression: | ||
let propAccess = node as ts.PropertyAccessExpression; | ||
// We're looking for an expression of the form: | ||
// module_name_var.default | ||
if (getIdentifierText(propAccess.name) !== 'default') break; | ||
if (propAccess.expression.kind !== ts.SyntaxKind.Identifier) 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(`${lhs} `); | ||
return true; | ||
default: | ||
break; | ||
} | ||
return false; | ||
} | ||
} | ||
/** | ||
* Converts TypeScript's JS+CommonJS output to Closure goog.module etc. | ||
* For use as a postprocessing step *after* TypeScript emits JavaScript. | ||
* | ||
* @param fileName The source file name, without an extension. | ||
* @param pathToModuleName A function that maps a filesystem .ts path to a | ||
* Closure module name, as found in a goog.require('...') statement. | ||
* The context parameter is the referencing file, used for resolving | ||
* imports with relative paths like "import * as foo from '../foo';". | ||
*/ | ||
export function convertCommonJsToGoogModule( | ||
fileName: string, content: string, pathToModuleName: (context: string, fileName: string) => | ||
string): {output: string, referencedModules: string[]} { | ||
let file = ts.createSourceFile(fileName, content, ts.ScriptTarget.ES5, true); | ||
return new PostProcessor(file, pathToModuleName).process(); | ||
} |
import * as ts from 'typescript'; | ||
export function assertTypeChecked(sourceFile: ts.SourceFile) { | ||
if (!('resolvedModules' in sourceFile)) { | ||
throw new Error('must provide typechecked program'); | ||
} | ||
} | ||
/** | ||
* Determines if fileName refers to a builtin lib.d.ts file. | ||
* This is a terrible hack but it mirrors a similar thing done in Clutz. | ||
*/ | ||
function isBuiltinLibDTS(fileName: string): boolean { | ||
return fileName.match(/\blib\.[^/]+\.d\.ts$/) != null; | ||
} | ||
/** | ||
* @return True if the named type is considered compatible with the Closure-defined | ||
* type of the same name, e.g. "Array". Note that we don't actually enforce | ||
* that the types are actually compatible, but mostly just hope that they are due | ||
* to being derived from the same HTML specs. | ||
*/ | ||
function isClosureProvidedType(symbol: ts.Symbol): boolean { | ||
return symbol.declarations.every(n => isBuiltinLibDTS(n.getSourceFile().fileName)); | ||
} | ||
export function typeToDebugString(type: ts.Type): string { | ||
@@ -143,3 +167,5 @@ const basicTypes: ts.TypeFlags[] = [ | ||
if (type.flags & (ts.TypeFlags.Interface | ts.TypeFlags.Class)) { | ||
if (type.flags & ts.TypeFlags.Class) { | ||
return this.symbolToString(type.symbol); | ||
} else if (type.flags & ts.TypeFlags.Interface) { | ||
// Note: ts.InterfaceType has a typeParameters field, but that | ||
@@ -152,2 +178,11 @@ // specifies the parameters that the interface type *expects* | ||
// the InterfaceType. | ||
if (type.symbol.flags & ts.SymbolFlags.Value) { | ||
// The symbol is both a type and a value. | ||
// For user-defined types in this state, we don't have a Closure name | ||
// for the type. See the type_and_value test. | ||
if (!isClosureProvidedType(type.symbol)) { | ||
this.warn(`type/symbol conflict for ${type.symbol.name}, using {?} for now`); | ||
return '?'; | ||
} | ||
} | ||
return this.symbolToString(type.symbol); | ||
@@ -154,0 +189,0 @@ } else if (type.flags & ts.TypeFlags.Reference) { |
@@ -10,2 +10,3 @@ goog.module('test_files.jsdoc_types.untyped.jsdoc_types');var module = module || {id: 'test_files/jsdoc_types.untyped/jsdoc_types.js'};/** | ||
var module2_3 = module2_1; | ||
var default_1 = goog.require('test_files.jsdoc_types.untyped.default'); | ||
// Check that imported types get the proper names in JSDoc. | ||
@@ -23,1 +24,4 @@ let /** @type {?} */ useNamespacedClass = new module1.Class(); | ||
let /** @type {?} */ useLocalValue = module2_1.value; | ||
// Check a default import. | ||
let /** @type {?} */ useDefaultClass = new default_1.default(); | ||
let /** @type {?} */ useDefaultClassAsType = null; |
@@ -11,2 +11,3 @@ /** | ||
import {Interface} from './module2'; | ||
import DefaultClass from './default'; | ||
@@ -26,2 +27,6 @@ // Check that imported types get the proper names in JSDoc. | ||
// This is purely a value; it doesn't need renaming. | ||
let /** @type {?} */ useLocalValue = value; | ||
let /** @type {?} */ useLocalValue = value; | ||
// Check a default import. | ||
let /** @type {?} */ useDefaultClass = new DefaultClass(); | ||
let /** @type {?} */ useDefaultClassAsType: DefaultClass = null; |
@@ -15,2 +15,4 @@ goog.module('test_files.jsdoc_types.jsdoc_types');var module = module || {id: 'test_files/jsdoc_types/jsdoc_types.js'};/** | ||
const Interface = module2_4.Interface; | ||
var default_1 = goog.require('test_files.jsdoc_types.default'); | ||
const DefaultClass = default_1.default; | ||
// Check that imported types get the proper names in JSDoc. | ||
@@ -28,1 +30,4 @@ let /** @type {module1.Class} */ useNamespacedClass = new module1.Class(); | ||
let /** @type {number} */ useLocalValue = module2_1.value; | ||
// Check a default import. | ||
let /** @type {DefaultClass} */ useDefaultClass = new DefaultClass(); | ||
let /** @type {DefaultClass} */ useDefaultClassAsType = null; |
@@ -11,2 +11,3 @@ /** | ||
import {Interface} from './module2'; | ||
import DefaultClass from './default'; | ||
@@ -26,2 +27,6 @@ // Check that imported types get the proper names in JSDoc. | ||
// This is purely a value; it doesn't need renaming. | ||
let useLocalValue = value; | ||
let useLocalValue = value; | ||
// Check a default import. | ||
let useDefaultClass = new DefaultClass(); | ||
let useDefaultClassAsType: DefaultClass = null; |
@@ -24,3 +24,7 @@ /** | ||
import tsickle_DefaultClass from './default'; | ||
const DefaultClass = tsickle_DefaultClass; | ||
type DefaultClass = tsickle_DefaultClass; | ||
// Check that imported types get the proper names in JSDoc. | ||
@@ -39,2 +43,6 @@ let /** @type {module1.Class} */ useNamespacedClass = new module1.Class(); | ||
// This is purely a value; it doesn't need renaming. | ||
let /** @type {number} */ useLocalValue = value; | ||
let /** @type {number} */ useLocalValue = value; | ||
// Check a default import. | ||
let /** @type {DefaultClass} */ useDefaultClass = new DefaultClass(); | ||
let /** @type {DefaultClass} */ useDefaultClassAsType: DefaultClass = null; |
@@ -0,4 +1,7 @@ | ||
import {expect} from 'chai'; | ||
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'; | ||
@@ -40,10 +43,32 @@ | ||
function expectUnchanged(sourceText: string) { | ||
expect(translate(sourceText).output).to.equal(sourceText); | ||
} | ||
it('rejects non-typechecked inputs', () => { | ||
let sourceText = 'let x = 3;'; | ||
let program = test_support.createProgram(sources(sourceText)); | ||
let goodSourceFile = program.getSourceFile(testCaseFileName); | ||
expect(() => convertDecorators(program.getTypeChecker(), goodSourceFile)).to.not.throw(); | ||
let badSourceFile = | ||
ts.createSourceFile(testCaseFileName, sourceText, ts.ScriptTarget.ES6, true); | ||
expect(() => convertDecorators(program.getTypeChecker(), badSourceFile)).to.throw(); | ||
}); | ||
describe('class decorator rewriter', () => { | ||
it('leaves plain classes alone', | ||
() => { expect(translate(`class Foo {}`).output).to.equal(`class Foo {}`); }); | ||
it('leaves plain classes alone', () => { expectUnchanged(`class Foo {}`); }); | ||
it('leaves un-marked decorators alone', () => { | ||
expectUnchanged(` | ||
let Decor: Function; | ||
@Decor class Foo { | ||
constructor(@Decor p: number) {} | ||
@Decor m(): void {} | ||
}`); | ||
}); | ||
it('transforms decorated classes', () => { | ||
expect(translate(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
/** @Annotation */ let Test1: Function; | ||
/** @Annotation */ let Test2: Function; | ||
let param: any; | ||
@@ -55,4 +80,4 @@ @Test1 | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
/** @Annotation */ let Test1: Function; | ||
/** @Annotation */ let Test2: Function; | ||
let param: any; | ||
@@ -71,6 +96,6 @@ class Foo { | ||
expect(translate(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
let Test3: Function; | ||
function Test4<T>(param: any): ClassDecorator { return null; } | ||
/** @Annotation */ let Test1: Function; | ||
/** @Annotation */ let Test2: Function; | ||
/** @Annotation */ let Test3: Function; | ||
/** @Annotation */ function Test4<T>(param: any): ClassDecorator { return null; } | ||
let param: any; | ||
@@ -83,6 +108,6 @@ @Test1({name: 'percentPipe'}, class ZZZ {}) | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
let Test3: Function; | ||
function Test4<T>(param: any): ClassDecorator { return null; } | ||
/** @Annotation */ let Test1: Function; | ||
/** @Annotation */ let Test2: Function; | ||
/** @Annotation */ let Test3: Function; | ||
/** @Annotation */ function Test4<T>(param: any): ClassDecorator { return null; } | ||
let param: any; | ||
@@ -102,7 +127,7 @@ class Foo { | ||
expect(translate(` | ||
let Test1: Function; | ||
/** @Annotation */ let Test1: Function; | ||
@Test1 | ||
export class Foo { | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
/** @Annotation */ let Test1: Function; | ||
export class Foo { | ||
@@ -118,4 +143,4 @@ /** @nocollapse */ | ||
expect(translate(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
/** @Annotation */ let Test1: Function; | ||
/** @Annotation */ let Test2: Function; | ||
@Test1 | ||
@@ -129,4 +154,4 @@ export class Foo { | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
let Test2: Function; | ||
/** @Annotation */ let Test1: Function; | ||
/** @Annotation */ let Test2: Function; | ||
export class Foo { | ||
@@ -151,3 +176,3 @@ foo() { | ||
it('ignores ctors that have no applicable injects', () => { | ||
expect(translate(` | ||
expectUnchanged(` | ||
import {BarService} from 'bar'; | ||
@@ -157,7 +182,2 @@ class Foo { | ||
} | ||
}`).output).to.equal(` | ||
import {BarService} from 'bar'; | ||
class Foo { | ||
constructor(bar: BarService, num: number) { | ||
} | ||
}`); | ||
@@ -168,3 +188,3 @@ }); | ||
expect(translate(` | ||
let Inject: Function; | ||
/** @Annotation */ let Inject: Function; | ||
abstract class AbstractService {} | ||
@@ -175,3 +195,3 @@ class Foo { | ||
}`).output).to.equal(` | ||
let Inject: Function; | ||
/** @Annotation */ let Inject: Function; | ||
abstract class AbstractService {} | ||
@@ -192,3 +212,3 @@ class Foo { | ||
import {BarService} from 'bar'; | ||
let Test1: Function; | ||
/** @Annotation */ let Test1: Function; | ||
@Test1() | ||
@@ -200,3 +220,3 @@ class Foo { | ||
import {BarService} from 'bar'; | ||
let Test1: Function; | ||
/** @Annotation */ let Test1: Function; | ||
class Foo { | ||
@@ -220,3 +240,3 @@ constructor(bar: BarService, num: number) { | ||
import * as bar from 'bar'; | ||
let Inject: Function; | ||
/** @Annotation */ let Inject: Function; | ||
let param: any; | ||
@@ -228,3 +248,3 @@ class Foo { | ||
import * as bar from 'bar'; | ||
let Inject: Function; | ||
/** @Annotation */ let Inject: Function; | ||
let param: any; | ||
@@ -246,3 +266,3 @@ class Foo { | ||
expect(translate(` | ||
let Inject: Function; | ||
/** @Annotation */ let Inject: Function; | ||
let APP_ID: any; | ||
@@ -252,3 +272,3 @@ class ViewUtils { | ||
}`).output).to.equal(` | ||
let Inject: Function; | ||
/** @Annotation */ let Inject: Function; | ||
let APP_ID: any; | ||
@@ -266,3 +286,3 @@ class ViewUtils { | ||
expect(translate(` | ||
let Inject: Function; | ||
/** @Annotation */ let Inject: Function; | ||
class Foo { | ||
@@ -272,3 +292,3 @@ constructor(@Inject typed: Promise<string>) { | ||
}`).output).to.equal(` | ||
let Inject: Function; | ||
/** @Annotation */ let Inject: Function; | ||
class Foo { | ||
@@ -286,3 +306,3 @@ constructor( typed: Promise<string>) { | ||
expect(translate(` | ||
let Inject: Function = null; | ||
/** @Annotation */ let Inject: Function = null; | ||
class Class {} | ||
@@ -293,3 +313,3 @@ interface Iface {} | ||
}`).output).to.equal(` | ||
let Inject: Function = null; | ||
/** @Annotation */ let Inject: Function = null; | ||
class Class {} | ||
@@ -310,8 +330,5 @@ interface Iface {} | ||
it('leaves ordinary methods alone', () => { | ||
expect(translate(` | ||
expectUnchanged(` | ||
class Foo { | ||
bar() {} | ||
}`).output).to.equal(` | ||
class Foo { | ||
bar() {} | ||
}`); | ||
@@ -322,3 +339,3 @@ }); | ||
expect(translate(` | ||
let Test1: Function; | ||
/** @Annotation */ let Test1: Function; | ||
class Foo { | ||
@@ -328,3 +345,3 @@ @Test1('somename') | ||
}`).output).to.equal(` | ||
let Test1: Function; | ||
/** @Annotation */ let Test1: Function; | ||
class Foo { | ||
@@ -341,3 +358,3 @@ bar() {} | ||
expect(translate(` | ||
let PropDecorator: Function; | ||
/** @Annotation */ let PropDecorator: Function; | ||
class ClassWithDecorators { | ||
@@ -350,3 +367,3 @@ @PropDecorator("p1") @PropDecorator("p2") a; | ||
}`).output).to.equal(` | ||
let PropDecorator: Function; | ||
/** @Annotation */ let PropDecorator: Function; | ||
class ClassWithDecorators { a; | ||
@@ -366,3 +383,3 @@ b; | ||
` | ||
let Test1: Function; | ||
/** @Annotation */ let Test1: Function; | ||
let param: any; | ||
@@ -369,0 +386,0 @@ class Foo { |
import * as fs from 'fs'; | ||
import * as ts from 'typescript'; | ||
import * as glob from 'glob'; | ||
import * as path from 'path'; | ||
import * as ts from 'typescript'; | ||
import * as cli_support from '../src/cli_support'; | ||
import * as tsickle from '../src/tsickle'; | ||
@@ -58,3 +59,3 @@ | ||
transformed[fileName] = | ||
tsickle.convertCommonJsToGoogModule(fileName, data, pathToModuleName).output; | ||
tsickle.convertCommonJsToGoogModule(fileName, data, cli_support.pathToModuleName).output; | ||
}); | ||
@@ -65,9 +66,2 @@ if (emitRes.diagnostics.length) { | ||
return transformed; | ||
function pathToModuleName(context: string, fileName: string): string { | ||
if (fileName[0] === '.') { | ||
fileName = path.join(path.dirname(context), fileName); | ||
} | ||
return fileName.replace(/\.js$/, '').replace(/\//g, '.'); | ||
} | ||
} | ||
@@ -74,0 +68,0 @@ |
@@ -165,159 +165,1 @@ import {expect} from 'chai'; | ||
}); | ||
describe('convertCommonJsToGoogModule', () => { | ||
function pathToModuleName(context: string, fileName: string) { | ||
if (fileName[0] === '.') { | ||
fileName = path.join(path.dirname(context), fileName); | ||
} | ||
return fileName.replace(/\//g, '$').replace(/_/g, '__'); | ||
} | ||
function expectCommonJs(fileName: string, content: string) { | ||
fileName = fileName.substring(0, fileName.lastIndexOf('.')); | ||
return expect(tsickle.convertCommonJsToGoogModule(fileName, content, pathToModuleName).output); | ||
} | ||
it('adds a goog.module call', () => { | ||
// NB: no line break added below. | ||
expectCommonJs('a.js', `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');var module = module || {id: 'a'};`); | ||
}); | ||
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`); | ||
}); | ||
it('strips use strict directives', () => { | ||
// NB: no line break added below. | ||
expectCommonJs('a.js', `"use strict"; | ||
console.log('hello');`) | ||
.to.equal(`goog.module('a');var module = module || {id: 'a'}; | ||
console.log('hello');`); | ||
}); | ||
it('converts require calls', () => { | ||
expectCommonJs('a.js', `var r = require('req/mod');`) | ||
.to.equal( | ||
`goog.module('a');var module = module || {id: 'a'};var r = goog.require('req$mod');`); | ||
}); | ||
it('converts require calls without assignments on first line', () => { | ||
expectCommonJs('a.js', `require('req/mod');`) | ||
.to.equal( | ||
`goog.module('a');var module = module || {id: 'a'};var unused_0_ = goog.require('req$mod');`); | ||
}); | ||
it('converts require calls without assignments on a new line', () => { | ||
expectCommonJs('a.js', ` | ||
require('req/mod'); | ||
require('other');`) | ||
.to.equal(`goog.module('a');var module = module || {id: 'a'}; | ||
var unused_0_ = goog.require('req$mod'); | ||
var unused_1_ = goog.require('other');`); | ||
}); | ||
it('converts require calls without assignments after comments', () => { | ||
expectCommonJs('a.js', ` | ||
// Comment | ||
require('req/mod');`) | ||
.to.equal(`goog.module('a');var module = module || {id: 'a'}; | ||
// Comment | ||
var unused_0_ = goog.require('req$mod');`); | ||
}); | ||
it('converts const require calls', () => { | ||
expectCommonJs('a.js', `const r = require('req/mod');`) | ||
.to.equal( | ||
`goog.module('a');var module = module || {id: 'a'};var r = goog.require('req$mod');`); | ||
}); | ||
it('converts export * statements', () => { | ||
expectCommonJs('a.js', `__export(require('req/mod'));`) | ||
.to.equal( | ||
`goog.module('a');var module = module || {id: 'a'};__export(goog.require('req$mod'));`); | ||
}); | ||
it('resolves relative module URIs', () => { | ||
// See below for more fine-grained unit tests. | ||
expectCommonJs('a/b.js', `var r = require('./req/mod');`) | ||
.to.equal( | ||
`goog.module('a$b');var module = module || {id: 'a/b'};var r = goog.require('a$req$mod');`); | ||
}); | ||
it('avoids mangling module names in goog: imports', () => { | ||
expectCommonJs('a/b.js', ` | ||
var goog_use_Foo_1 = require('goog:foo_bar.baz');`) | ||
.to.equal(`goog.module('a$b');var module = module || {id: 'a/b'}; | ||
var goog_use_Foo_1 = goog.require('foo_bar.baz');`); | ||
}); | ||
it('resolves default goog: module imports', () => { | ||
expectCommonJs('a/b.js', ` | ||
var goog_use_Foo_1 = require('goog:use.Foo'); | ||
console.log(goog_use_Foo_1.default);`) | ||
.to.equal(`goog.module('a$b');var module = module || {id: 'a/b'}; | ||
var goog_use_Foo_1 = goog.require('use.Foo'); | ||
console.log(goog_use_Foo_1 );`); | ||
// NB: the whitespace above matches the .default part, so that | ||
// source maps are not impacted. | ||
}); | ||
it('leaves single .default accesses alone', () => { | ||
// This is a repro for a bug when no goog: symbols are found. | ||
expectCommonJs('a/b.js', ` | ||
console.log(this.default); | ||
console.log(foo.bar.default);`) | ||
.to.equal(`goog.module('a$b');var module = module || {id: 'a/b'}; | ||
console.log(this.default); | ||
console.log(foo.bar.default);`); | ||
}); | ||
it('inserts the module after "use strict"', () => { | ||
expectCommonJs('a/b.js', `/** | ||
* docstring here | ||
*/ | ||
"use strict"; | ||
var foo = bar; | ||
`).to.equal(`goog.module('a$b');var module = module || {id: 'a/b'};/** | ||
* docstring here | ||
*/ | ||
var foo = bar; | ||
`); | ||
}); | ||
it('deduplicates module imports', () => { | ||
expectCommonJs('a/b.js', `var foo_1 = require('goog:foo'); | ||
var foo_2 = require('goog:foo'); | ||
foo_1.A, foo_2.B, foo_2.default, foo_3.default; | ||
`).to.equal(`goog.module('a$b');var module = module || {id: 'a/b'};var foo_1 = goog.require('foo'); | ||
var foo_2 = foo_1; | ||
foo_1.A, foo_2.B, foo_2 , foo_3.default; | ||
`); | ||
}); | ||
it('gathers referenced modules', () => { | ||
let {referencedModules} = tsickle.convertCommonJsToGoogModule( | ||
'a/b', ` | ||
require('../foo/bare-require'); | ||
var googRequire = require('goog:foo.bar'); | ||
var es6RelativeRequire = require('./relative'); | ||
var es6NonRelativeRequire = require('non/relative'); | ||
__export(require('./export-star'); | ||
`, | ||
pathToModuleName); | ||
return expect(referencedModules).to.deep.equal([ | ||
'foo$bare-require', | ||
'foo.bar', | ||
'a$relative', | ||
'non$relative', | ||
'a$export-star', | ||
]); | ||
}); | ||
}); |
@@ -11,2 +11,3 @@ { | ||
"src/decorator-annotator.ts", | ||
"src/es5processor.ts", | ||
"src/main.ts", | ||
@@ -18,2 +19,3 @@ "src/rewriter.ts", | ||
"test/e2e_test.ts", | ||
"test/es5processor_test.ts", | ||
"test/test_support.ts", | ||
@@ -20,0 +22,0 @@ "test/tsickle_test.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
361184
169
7511
45