From 722089be4531300e991b92bd384fc31d7ed44579 Mon Sep 17 00:00:00 2001 From: basarat Date: Sun, 8 Mar 2015 14:49:22 +1100 Subject: [PATCH] feat(typescript) initial commit of built in typescript support --- package.json | 1 + src/typescript-transpile.js | 152 ++++++++++++++++++++++++++++++++++++ src/typescript.coffee | 141 +++++++++++++++++++++++++++++++++ static/index.js | 7 ++ 4 files changed, 301 insertions(+) create mode 100644 src/typescript-transpile.js create mode 100644 src/typescript.coffee diff --git a/package.json b/package.json index 2ec0091a3..60f96cb34 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "temp": "0.8.1", "text-buffer": "^4.1.5", "theorist": "^1.0.2", + "typescript": "^1.4.1", "underscore-plus": "^1.6.6" }, "packageDependencies": { diff --git a/src/typescript-transpile.js b/src/typescript-transpile.js new file mode 100644 index 000000000..64d832d1c --- /dev/null +++ b/src/typescript-transpile.js @@ -0,0 +1,152 @@ +// From https://github.com/teppeis/typescript-simple/blob/master/index.js +// with https://github.com/teppeis/typescript-simple/pull/7 + +var fs = require('fs'); +var os = require('os'); +var path = require('path'); +var ts = require('typescript'); +var FILENAME_TS = 'file.ts'; +function tss(code, options) { + if (options) { + return new tss.TypeScriptSimple(options).compile(code); + } + else { + return defaultTss.compile(code); + } +} +var tss; +(function (tss) { + var TypeScriptSimple = (function () { + /** + * @param {ts.CompilerOptions=} options TypeScript compile options (some options are ignored) + */ + function TypeScriptSimple(options, doSemanticChecks) { + if (options === void 0) { options = {}; } + if (doSemanticChecks === void 0) { doSemanticChecks = true; } + this.doSemanticChecks = doSemanticChecks; + this.service = null; + this.outputs = {}; + this.files = {}; + if (options.target == null) { + options.target = ts.ScriptTarget.ES5; + } + if (options.module == null) { + options.module = ts.ModuleKind.None; + } + this.options = options; + } + /** + * @param {string} code TypeScript source code to compile + * @param {string} only needed if you plan to use sourceMaps. Provide the complete filePath relevant to you + * @return {string} The JavaScript with inline sourceMaps if sourceMaps were enabled + */ + TypeScriptSimple.prototype.compile = function (code, filename) { + if (filename === void 0) { filename = FILENAME_TS; } + if (!this.service) { + this.service = this.createService(); + } + var file = this.files[FILENAME_TS]; + file.text = code; + file.version++; + return this.toJavaScript(this.service, filename); + }; + TypeScriptSimple.prototype.createService = function () { + var _this = this; + var defaultLib = this.getDefaultLibFilename(this.options); + var defaultLibPath = path.join(this.getTypeScriptBinDir(), defaultLib); + this.files[defaultLib] = { version: 0, text: fs.readFileSync(defaultLibPath).toString() }; + this.files[FILENAME_TS] = { version: 0, text: '' }; + var servicesHost = { + getScriptFileNames: function () { return [_this.getDefaultLibFilename(_this.options), FILENAME_TS]; }, + getScriptVersion: function (filename) { return _this.files[filename] && _this.files[filename].version.toString(); }, + getScriptSnapshot: function (filename) { + var file = _this.files[filename]; + return { + getText: function (start, end) { return file.text.substring(start, end); }, + getLength: function () { return file.text.length; }, + getLineStartPositions: function () { return []; }, + getChangeRange: function (oldSnapshot) { return undefined; } + }; + }, + getCurrentDirectory: function () { return process.cwd(); }, + getScriptIsOpen: function () { return true; }, + getCompilationSettings: function () { return _this.options; }, + getDefaultLibFilename: function (options) { + return _this.getDefaultLibFilename(options); + }, + log: function (message) { return console.log(message); } + }; + return ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); + }; + TypeScriptSimple.prototype.getTypeScriptBinDir = function () { + return path.dirname(require.resolve('typescript')); + }; + TypeScriptSimple.prototype.getDefaultLibFilename = function (options) { + if (options.target === ts.ScriptTarget.ES6) { + return 'lib.es6.d.ts'; + } + else { + return 'lib.d.ts'; + } + }; + /** + * converts {"version":3,"file":"file.js","sourceRoot":"","sources":["file.ts"],"names":[],"mappings":"AAAA,IAAI,CAAC,GAAG,MAAM,CAAC"} + * to {"version":3,"sources":["foo/test.ts"],"names":[],"mappings":"AAAA,IAAI,CAAC,GAAG,MAAM,CAAC","file":"foo/test.ts","sourcesContent":["var x = 'test';"]} + * derived from : https://github.com/thlorenz/convert-source-map + */ + TypeScriptSimple.prototype.getInlineSourceMap = function (mapText, filename) { + var sourceMap = JSON.parse(mapText); + sourceMap.file = filename; + sourceMap.sources = [filename]; + sourceMap.sourcesContent = [this.files[FILENAME_TS].text]; + delete sourceMap.sourceRoot; + return JSON.stringify(sourceMap); + }; + TypeScriptSimple.prototype.toJavaScript = function (service, filename) { + if (filename === void 0) { filename = FILENAME_TS; } + var output = service.getEmitOutput(FILENAME_TS); + // Meaning of succeeded is driven by whether we need to check for semantic errors or not + var succeeded = output.emitOutputStatus === ts.EmitReturnStatus.Succeeded; + if (!this.doSemanticChecks) { + // We have an output. It implies syntactic success + if (!succeeded) + succeeded = !!output.outputFiles.length; + } + if (succeeded) { + var outputFilename = FILENAME_TS.replace(/ts$/, 'js'); + var file = output.outputFiles.filter(function (file) { return file.name === outputFilename; })[0]; + // Fixed in v1.5 https://github.com/Microsoft/TypeScript/issues/1653 + var text = file.text.replace(/\r\n/g, os.EOL); + // If we have sourceMaps convert them to inline sourceMaps + if (this.options.sourceMap) { + var sourceMapFilename = FILENAME_TS.replace(/ts$/, 'js.map'); + var sourceMapFile = output.outputFiles.filter(function (file) { return file.name === sourceMapFilename; })[0]; + // Transform sourcemap + var sourceMapText = sourceMapFile.text; + sourceMapText = this.getInlineSourceMap(sourceMapText, filename); + var base64SourceMapText = new Buffer(sourceMapText).toString('base64'); + text = text.replace('//# sourceMappingURL=' + sourceMapFilename, '//# sourceMappingURL=data:application/json;base64,' + base64SourceMapText); + } + return text; + } + var allDiagnostics = service.getCompilerOptionsDiagnostics().concat(service.getSyntacticDiagnostics(FILENAME_TS)); + if (this.doSemanticChecks) + allDiagnostics = allDiagnostics.concat(service.getSemanticDiagnostics(FILENAME_TS)); + throw new Error(this.formatDiagnostics(allDiagnostics)); + }; + TypeScriptSimple.prototype.formatDiagnostics = function (diagnostics) { + return diagnostics.map(function (d) { + if (d.file) { + return 'L' + d.file.getLineAndCharacterFromPosition(d.start).line + ': ' + d.messageText; + } + else { + return d.messageText; + } + }).join(os.EOL); + }; + return TypeScriptSimple; + })(); + tss.TypeScriptSimple = TypeScriptSimple; +})(tss || (tss = {})); +var defaultTss = new tss.TypeScriptSimple(); +module.exports = tss; diff --git a/src/typescript.coffee b/src/typescript.coffee new file mode 100644 index 000000000..e47e7b2a1 --- /dev/null +++ b/src/typescript.coffee @@ -0,0 +1,141 @@ +### +Cache for source code transpiled by TypeScript. + +Inspired by https://github.com/atom/atom/blob/7a719d585db96ff7d2977db9067e1d9d4d0adf1a/src/babel.coffee +### + +crypto = require 'crypto' +fs = require 'fs-plus' +path = require 'path' +tss = null # Defer until used + +stats = + hits: 0 + misses: 0 + +defaultOptions = + target: 1 + module: 'commonjs' + sourceMap: true + +### +shasum - Hash with an update() method. +value - Must be a value that could be returned by JSON.parse(). +### +updateDigestForJsonValue = (shasum, value) -> + # Implmentation is similar to that of pretty-printing a JSON object, except: + # * Strings are not escaped. + # * No effort is made to avoid trailing commas. + # These shortcuts should not affect the correctness of this function. + type = typeof value + if type is 'string' + shasum.update('"', 'utf8') + shasum.update(value, 'utf8') + shasum.update('"', 'utf8') + else if type in ['boolean', 'number'] + shasum.update(value.toString(), 'utf8') + else if value is null + shasum.update('null', 'utf8') + else if Array.isArray value + shasum.update('[', 'utf8') + for item in value + updateDigestForJsonValue(shasum, item) + shasum.update(',', 'utf8') + shasum.update(']', 'utf8') + else + # value must be an object: be sure to sort the keys. + keys = Object.keys value + keys.sort() + + shasum.update('{', 'utf8') + for key in keys + updateDigestForJsonValue(shasum, key) + shasum.update(': ', 'utf8') + updateDigestForJsonValue(shasum, value[key]) + shasum.update(',', 'utf8') + shasum.update('}', 'utf8') + +createTypeScriptVersionAndOptionsDigest = (version, options) -> + shasum = crypto.createHash('sha1') + # Include the version of typescript in the hash. + shasum.update('typescript', 'utf8') + shasum.update('\0', 'utf8') + shasum.update(version, 'utf8') + shasum.update('\0', 'utf8') + updateDigestForJsonValue(shasum, options) + shasum.digest('hex') + +cacheDir = null +jsCacheDir = null + +getCachePath = (sourceCode) -> + digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex') + + unless jsCacheDir? + tsVersion = require('typescript/package.json').version + jsCacheDir = path.join(cacheDir, createTypeScriptVersionAndOptionsDigest(tsVersion, defaultOptions)) + + path.join(jsCacheDir, "#{digest}.js") + +getCachedJavaScript = (cachePath) -> + if fs.isFileSync(cachePath) + try + cachedJavaScript = fs.readFileSync(cachePath, 'utf8') + stats.hits++ + return cachedJavaScript + null + +# Returns the TypeScript options that should be used to transpile filePath. +createOptions = (filePath) -> + options = filename: filePath + for key, value of defaultOptions + options[key] = value + options + +transpile = (sourceCode, filePath, cachePath) -> + options = createOptions(filePath) + tss ?= new (require './typescript-transpile').TypeScriptSimple(options, false) + js = tss.compile(sourceCode, filePath) + stats.misses++ + + try + fs.writeFileSync(cachePath, js) + + js + +# Function that obeys the contract of an entry in the require.extensions map. +# Returns the transpiled version of the JavaScript code at filePath, which is +# either generated on the fly or pulled from cache. +loadFile = (module, filePath) -> + sourceCode = fs.readFileSync(filePath, 'utf8') + cachePath = getCachePath(sourceCode) + js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath) + module._compile(js, filePath) + +register = -> + Object.defineProperty(require.extensions, '.ts', { + enumerable: true + writable: false + value: loadFile + }) + +setCacheDirectory = (newCacheDir) -> + if cacheDir isnt newCacheDir + cacheDir = newCacheDir + jsCacheDir = null + +module.exports = + register: register + setCacheDirectory: setCacheDirectory + getCacheMisses: -> stats.misses + getCacheHits: -> stats.hits + + # Visible for testing. + createTypeScriptVersionAndOptionsDigest: createTypeScriptVersionAndOptionsDigest + + addPathToCache: (filePath) -> + return if path.extname(filePath) isnt '.ts' + + sourceCode = fs.readFileSync(filePath, 'utf8') + cachePath = getCachePath(sourceCode) + transpile(sourceCode, filePath, cachePath) diff --git a/static/index.js b/static/index.js index e8142a698..d99a9e9e7 100644 --- a/static/index.js +++ b/static/index.js @@ -47,6 +47,7 @@ window.onload = function() { setupCsonCache(cacheDir); setupSourceMapCache(cacheDir); setupBabel(cacheDir); + setupTypeScript(cacheDir); require(loadSettings.bootstrapScript); require('ipc').sendChannel('window-command', 'window:loaded'); @@ -95,6 +96,12 @@ var setupBabel = function(cacheDir) { babel.register(); } +var setupTypeScript = function(cacheDir) { + var typescript = require('../src/typescript'); + typescript.setCacheDirectory(path.join(cacheDir, 'typescript')); + typescript.register(); +} + var setupCsonCache = function(cacheDir) { require('season').setCacheDir(path.join(cacheDir, 'cson')); }