diff --git a/package.json b/package.json index 88853e79c..811814a0f 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "semver": "^4.3.3", "serializable": "^1", "service-hub": "^0.6.2", + "source-map-support": "^0.3.2", "space-pen": "3.8.2", "stacktrace-parser": "0.1.1", "temp": "0.8.1", diff --git a/spec/babel-spec.coffee b/spec/babel-spec.coffee index 1f71faa93..421e4c48a 100644 --- a/spec/babel-spec.coffee +++ b/spec/babel-spec.coffee @@ -1,70 +1,19 @@ -babel = require '../src/babel' -crypto = require 'crypto' -grim = require 'grim' - describe "Babel transpiler support", -> - beforeEach -> - jasmine.snapshotDeprecations() - - afterEach -> - jasmine.restoreDeprecationsSnapshot() - - describe "::createBabelVersionAndOptionsDigest", -> - it "returns a digest for the library version and specified options", -> - defaultOptions = - blacklist: [ - 'useStrict' - ] - experimental: true - optional: [ - 'asyncToGenerator' - ] - reactCompat: true - sourceMap: 'inline' - version = '3.0.14' - shasum = crypto.createHash('sha1') - shasum.update('babel-core', 'utf8') - shasum.update('\0', 'utf8') - shasum.update(version, 'utf8') - shasum.update('\0', 'utf8') - shasum.update('{"blacklist": ["useStrict",],"experimental": true,"optional": ["asyncToGenerator",],"reactCompat": true,"sourceMap": "inline",}') - expectedDigest = shasum.digest('hex') - - observedDigest = babel.createBabelVersionAndOptionsDigest(version, defaultOptions) - expect(observedDigest).toEqual expectedDigest - describe 'when a .js file starts with /** use babel */;', -> it "transpiles it using babel", -> transpiled = require('./fixtures/babel/babel-comment.js') expect(transpiled(3)).toBe 4 - expect(grim.getDeprecationsLength()).toBe 0 describe "when a .js file starts with 'use babel';", -> it "transpiles it using babel", -> transpiled = require('./fixtures/babel/babel-single-quotes.js') expect(transpiled(3)).toBe 4 - expect(grim.getDeprecationsLength()).toBe 0 - - describe "when a .js file starts with 'use 6to5';", -> - it "transpiles it using babel and adds a pragma deprecation", -> - expect(grim.getDeprecationsLength()).toBe 0 - transpiled = require('./fixtures/babel/6to5-single-quotes.js') - expect(transpiled(3)).toBe 4 - expect(grim.getDeprecationsLength()).toBe 1 describe 'when a .js file starts with "use babel";', -> it "transpiles it using babel", -> transpiled = require('./fixtures/babel/babel-double-quotes.js') expect(transpiled(3)).toBe 4 - expect(grim.getDeprecationsLength()).toBe 0 - describe 'when a .js file starts with "use 6to5";', -> - it "transpiles it using babel and adds a pragma deprecation", -> - expect(grim.getDeprecationsLength()).toBe 0 - transpiled = require('./fixtures/babel/6to5-double-quotes.js') - expect(transpiled(3)).toBe 4 - expect(grim.getDeprecationsLength()).toBe 1 - - describe "when a .js file does not start with 'use 6to6';", -> + describe "when a .js file does not start with 'use babel';", -> it "does not transpile it using babel", -> expect(-> require('./fixtures/babel/invalid.js')).toThrow() diff --git a/spec/compile-cache-spec.coffee b/spec/compile-cache-spec.coffee deleted file mode 100644 index 6eb6556d0..000000000 --- a/spec/compile-cache-spec.coffee +++ /dev/null @@ -1,39 +0,0 @@ -path = require 'path' -CSON = require 'season' -CoffeeCache = require 'coffee-cash' - -babel = require '../src/babel' -typescript = require '../src/typescript' -CompileCache = require '../src/compile-cache' - -describe "Compile Cache", -> - describe ".addPathToCache(filePath)", -> - it "adds the path to the correct CSON, CoffeeScript, babel or typescript cache", -> - spyOn(CSON, 'readFileSync').andCallThrough() - spyOn(CoffeeCache, 'addPathToCache').andCallThrough() - spyOn(babel, 'addPathToCache').andCallThrough() - spyOn(typescript, 'addPathToCache').andCallThrough() - - CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'cson.cson')) - expect(CSON.readFileSync.callCount).toBe 1 - expect(CoffeeCache.addPathToCache.callCount).toBe 0 - expect(babel.addPathToCache.callCount).toBe 0 - expect(typescript.addPathToCache.callCount).toBe 0 - - CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'coffee.coffee')) - expect(CSON.readFileSync.callCount).toBe 1 - expect(CoffeeCache.addPathToCache.callCount).toBe 1 - expect(babel.addPathToCache.callCount).toBe 0 - expect(typescript.addPathToCache.callCount).toBe 0 - - CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'babel', 'babel-double-quotes.js')) - expect(CSON.readFileSync.callCount).toBe 1 - expect(CoffeeCache.addPathToCache.callCount).toBe 1 - expect(babel.addPathToCache.callCount).toBe 1 - expect(typescript.addPathToCache.callCount).toBe 0 - - CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'typescript', 'valid.ts')) - expect(CSON.readFileSync.callCount).toBe 1 - expect(CoffeeCache.addPathToCache.callCount).toBe 1 - expect(babel.addPathToCache.callCount).toBe 1 - expect(typescript.addPathToCache.callCount).toBe 1 diff --git a/spec/typescript-spec.coffee b/spec/typescript-spec.coffee index 493715d36..152848c74 100644 --- a/spec/typescript-spec.coffee +++ b/spec/typescript-spec.coffee @@ -1,25 +1,4 @@ -typescript = require '../src/typescript' -crypto = require 'crypto' - describe "TypeScript transpiler support", -> - describe "::createTypeScriptVersionAndOptionsDigest", -> - it "returns a digest for the library version and specified options", -> - defaultOptions = - target: 1 # ES5 - module: 'commonjs' - sourceMap: true - version = '1.4.1' - shasum = crypto.createHash('sha1') - shasum.update('typescript', 'utf8') - shasum.update('\0', 'utf8') - shasum.update(version, 'utf8') - shasum.update('\0', 'utf8') - shasum.update(JSON.stringify(defaultOptions)) - expectedDigest = shasum.digest('hex') - - observedDigest = typescript.createTypeScriptVersionAndOptionsDigest(version, defaultOptions) - expect(observedDigest).toEqual expectedDigest - describe "when there is a .ts file", -> it "transpiles it using typescript", -> transpiled = require('./fixtures/typescript/valid.ts') diff --git a/src/babel.coffee b/src/babel.coffee deleted file mode 100644 index afa48eb1c..000000000 --- a/src/babel.coffee +++ /dev/null @@ -1,201 +0,0 @@ -### -Cache for source code transpiled by Babel. - -Inspired by https://github.com/atom/atom/blob/6b963a562f8d495fbebe6abdbafbc7caf705f2c3/src/coffee-cache.coffee. -### - -crypto = require 'crypto' -fs = require 'fs-plus' -path = require 'path' -babel = null # Defer until used -Grim = null # Defer until used - -stats = - hits: 0 - misses: 0 - -defaultOptions = - # Currently, the cache key is a function of: - # * The version of Babel used to transpile the .js file. - # * The contents of this defaultOptions object. - # * The contents of the .js file. - # That means that we cannot allow information from an unknown source - # to affect the cache key for the output of transpilation, which means - # we cannot allow users to override these default options via a .babelrc - # file, because the contents of that .babelrc file will not make it into - # the cache key. It would be great to support .babelrc files once we - # have a way to do so that is safe with respect to caching. - breakConfig: true - - # The Chrome dev tools will show the original version of the file - # when the source map is inlined. - sourceMap: 'inline' - - # Blacklisted features do not get transpiled. Features that are - # natively supported in the target environment should be listed - # here. Because Atom uses a bleeding edge version of Node/io.js, - # I think this can include es6.arrowFunctions, es6.classes, and - # possibly others, but I want to be conservative. - blacklist: [ - 'es6.forOf' - 'useStrict' - ] - - optional: [ - # Target a version of the regenerator runtime that - # supports yield so the transpiled code is cleaner/smaller. - 'asyncToGenerator' - ] - - # Includes support for es7 features listed at: - # http://babeljs.io/docs/usage/experimental/. - stage: 0 - - -### -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') - -createBabelVersionAndOptionsDigest = (version, options) -> - shasum = crypto.createHash('sha1') - # Include the version of babel in the hash. - shasum.update('babel-core', '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? - to5Version = require('babel-core/package.json').version - jsCacheDir = path.join(cacheDir, createBabelVersionAndOptionsDigest(to5Version, 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 babel 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) - babel ?= require 'babel-core' - js = babel.transform(sourceCode, options).code - 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') - if sourceCode.startsWith('"use babel"') or sourceCode.startsWith("'use babel'") or sourceCode.startsWith('/** use babel */') - # Continue. - else if sourceCode.startsWith('"use 6to5"') or sourceCode.startsWith("'use 6to5'") - # Create a manual deprecation since the stack is too deep to use Grim - # which limits the depth to 3 - Grim ?= require 'grim' - stack = [ - { - fileName: __filename - functionName: 'loadFile' - location: "#{__filename}:161:5" - } - { - fileName: filePath - functionName: '' - location: "#{filePath}:1:1" - } - ] - deprecation = - message: "Use the 'use babel' pragma instead of 'use 6to5'" - stacks: [stack] - Grim.addSerializedDeprecation(deprecation) - else - return module._compile(sourceCode, filePath) - - cachePath = getCachePath(sourceCode) - js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath) - module._compile(js, filePath) - -register = -> - Object.defineProperty(require.extensions, '.js', { - 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 - defaultOptions: defaultOptions - - # Visible for testing. - createBabelVersionAndOptionsDigest: createBabelVersionAndOptionsDigest - - addPathToCache: (filePath) -> - return if path.extname(filePath) isnt '.js' - - sourceCode = fs.readFileSync(filePath, 'utf8') - cachePath = getCachePath(sourceCode) - transpile(sourceCode, filePath, cachePath) diff --git a/src/babel.js b/src/babel.js new file mode 100644 index 000000000..eedfef5e2 --- /dev/null +++ b/src/babel.js @@ -0,0 +1,70 @@ +'use strict' + +const _ = require('underscore-plus') +const crypto = require('crypto') +const path = require('path') + +let babel = null +let babelVersionDirectory = null + +// This field is used by the Gruntfile for compiling babel in bundled packages. +exports.defaultOptions = { + + // Currently, the cache key is a function of: + // * The version of Babel used to transpile the .js file. + // * The contents of this defaultOptions object. + // * The contents of the .js file. + // That means that we cannot allow information from an unknown source + // to affect the cache key for the output of transpilation, which means + // we cannot allow users to override these default options via a .babelrc + // file, because the contents of that .babelrc file will not make it into + // the cache key. It would be great to support .babelrc files once we + // have a way to do so that is safe with respect to caching. + breakConfig: true, + + sourceMap: 'inline', + blacklist: ['es6.forOf', 'useStrict'], + optional: ['asyncToGenerator'], + stage: 0 +} + +exports.shouldCompile = function(sourceCode) { + return sourceCode.startsWith('/** use babel */') || + sourceCode.startsWith('"use babel"') || + sourceCode.startsWith("'use babel'") +} + +exports.getCachePath = function(sourceCode) { + if (babelVersionDirectory == null) { + let babelVersion = require('babel-core/package.json').version + babelVersionDirectory = path.join('js', 'babel', createVersionAndOptionsDigest(babelVersion, defaultOptions)) + } + + return path.join( + babelVersionDirectory, + crypto + .createHash('sha1') + .update(sourceCode, 'utf8') + .digest('hex') + ".js" + ) +} + +exports.compile = function(sourceCode, filePath) { + if (!babel) { + babel = require('babel-core') + } + + let options = _.defaults({filename: filePath}, defaultOptions) + return babel.transform(sourceCode, options).code +} + +function createVersionAndOptionsDigest (version, options) { + return crypto + .createHash('sha1') + .update('babel-core', 'utf8') + .update('\0', 'utf8') + .update(version, 'utf8') + .update('\0', 'utf8') + .update(JSON.stringify(options), 'utf8') + .digest('hex') +} diff --git a/src/coffee-script.js b/src/coffee-script.js new file mode 100644 index 000000000..9cd0a0777 --- /dev/null +++ b/src/coffee-script.js @@ -0,0 +1,38 @@ +'use strict' + +const crypto = require('crypto') +const path = require('path') + +// The coffee-script compiler is required eagerly because: +// 1. It is always used. +// 2. It reassigns Error.prepareStackTrace, so we need to make sure that +// the 'source-map-support' module is installed *after* it is loaded. +const CoffeeScript = require('coffee-script') + +exports.shouldCompile = function() { + return true +} + +exports.getCachePath = function(sourceCode) { + return path.join( + "coffee", + crypto + .createHash('sha1') + .update(sourceCode, 'utf8') + .digest('hex') + ".js" + ) +} + +exports.compile = function(sourceCode, filePath) { + let output = CoffeeScript.compile(sourceCode, { + filename: filePath, + sourceMap: true + }) + + let js = output.js + js += '\n' + js += '//# sourceMappingURL=data:application/json;base64,' + js += new Buffer(output.v3SourceMap).toString('base64') + js += '\n' + return js +} diff --git a/src/compile-cache.coffee b/src/compile-cache.coffee deleted file mode 100644 index 8fe8d6711..000000000 --- a/src/compile-cache.coffee +++ /dev/null @@ -1,30 +0,0 @@ -path = require 'path' -CSON = require 'season' -CoffeeCache = require 'coffee-cash' -babel = require './babel' -typescript = require './typescript' - -# This file is required directly by apm so that files can be cached during -# package install so that the first package load in Atom doesn't have to -# compile anything. -exports.addPathToCache = (filePath, atomHome) -> - atomHome ?= process.env.ATOM_HOME - cacheDir = path.join(atomHome, 'compile-cache') - # Use separate compile cache when sudo'ing as root to avoid permission issues - if process.env.USER is 'root' and process.env.SUDO_USER and process.env.SUDO_USER isnt process.env.USER - cacheDir = path.join(cacheDir, 'root') - - CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee')) - CSON.setCacheDir(path.join(cacheDir, 'cson')) - babel.setCacheDirectory(path.join(cacheDir, 'js', 'babel')) - typescript.setCacheDirectory(path.join(cacheDir, 'ts')) - - switch path.extname(filePath) - when '.coffee' - CoffeeCache.addPathToCache(filePath) - when '.cson' - CSON.readFileSync(filePath) - when '.js' - babel.addPathToCache(filePath) - when '.ts' - typescript.addPathToCache(filePath) diff --git a/src/compile-cache.js b/src/compile-cache.js new file mode 100644 index 000000000..0666e7635 --- /dev/null +++ b/src/compile-cache.js @@ -0,0 +1,123 @@ +'use strict' + +const path = require('path') +const CSON = require('season') +const fs = require('fs-plus') + +const COMPILERS = { + '.js': require('./babel'), + '.ts': require('./typescript'), + '.coffee': require('./coffee-script') +} + +for (let extension in COMPILERS) { + let compiler = COMPILERS[extension] + Object.defineProperty(require.extensions, extension, { + enumerable: true, + writable: false, + value: function (module, filePath) { + let code = compileFileAtPath(compiler, filePath) + return module._compile(code, filePath) + } + }) +} + +let cacheDirectory = null + +exports.setAtomHomeDirectory = function (atomHome) { + let cacheDir = path.join(atomHome, 'compile-cache') + if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) { + cacheDir = path.join(cacheDirectory, 'root') + } + this.setCacheDirectory(cacheDir) +} + +exports.setCacheDirectory = function (directory) { + cacheDirectory = directory + CSON.setCacheDir(path.join(cacheDirectory, 'cson')); +} + +exports.getCacheDirectory = function () { + return cacheDirectory +} + +exports.addPathToCache = function (filePath, atomHome) { + this.setAtomHomeDirectory(atomHome) + extension = path.extname(filePath) + if (extension === '.cson') { + return CSON.readFileSync(filePath) + } + + if (compiler = COMPILERS[extension]) { + return compileFileAtPath(compiler, filePath) + } +} + +function compileFileAtPath (compiler, filePath) { + let sourceCode = fs.readFileSync(filePath, 'utf8') + if (compiler.shouldCompile(sourceCode, filePath)) { + let cachePath = compiler.getCachePath(sourceCode, filePath) + let compiledCode = readCachedJavascript(cachePath) + if (compiledCode == null) { + compiledCode = compiler.compile(sourceCode, filePath) + writeCachedJavascript(cachePath, compiledCode) + } + return compiledCode + } + return sourceCode +} + +function readCachedJavascript (relativeCachePath) { + let cachePath = path.join(cacheDirectory, relativeCachePath) + if (fs.isFileSync(cachePath)) { + try { + return fs.readFileSync(cachePath, 'utf8') + } catch (error) {} + } + return null +} + +function writeCachedJavascript (relativeCachePath, code) { + let cachePath = path.join(cacheDirectory, relativeCachePath) + fs.writeFileSync(cachePath, code, 'utf8') +} + +const InlineSourceMapRegExp = /\/\/[#@]\s*sourceMappingURL=([^'"]+)\s*$/g + +require('source-map-support').install({ + handleUncaughtExceptions: false, + + // Most of this logic is the same as the default implementation in the + // source-map-support module, but we've overridden it to read the javascript + // code from our cache directory. + retrieveSourceMap: function (filePath) { + if (!fs.isFileSync(filePath)){ + return null + } + + let sourceCode = fs.readFileSync(filePath, 'utf8') + let compiler = COMPILERS[path.extname(filePath)] + let fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath)) + if (fileData == null) { + return null + } + + let match, lastMatch + InlineSourceMapRegExp.lastIndex = 0 + while ((match = InlineSourceMapRegExp.exec(fileData))) { + lastMatch = match + } + if (lastMatch == null){ + return null + } + + let sourceMappingURL = lastMatch[1] + let rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1) + let sourceMapData = new Buffer(rawData, 'base64').toString() + + return { + map: JSON.parse(sourceMapData), + url: null + } + } +}) diff --git a/src/task.coffee b/src/task.coffee index 337192dcd..6b1162396 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -66,15 +66,11 @@ class Task # * `taskPath` The {String} path to the CoffeeScript/JavaScript file that # exports a single {Function} to execute. constructor: (taskPath) -> - coffeeCacheRequire = "require('#{require.resolve('coffee-cash')}')" - coffeeCachePath = require('coffee-cash').getCacheDirectory() - coffeeStackRequire = "require('#{require.resolve('coffeestack')}')" - stackCachePath = require('coffeestack').getCacheDirectory() + compileCacheRequire = "require('#{require.resolve('./compile-cache')}')" + compileCachePath = require('./compile-cache').getCacheDirectory() taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');" bootstrap = """ - #{coffeeCacheRequire}.setCacheDirectory('#{coffeeCachePath}'); - #{coffeeCacheRequire}.register(); - #{coffeeStackRequire}.setCacheDirectory('#{stackCachePath}'); + #{compileCacheRequire}.setCacheDirectory('#{compileCachePath}'); #{taskBootstrapRequire} """ bootstrap = bootstrap.replace(/\\/g, "\\\\") diff --git a/src/typescript.coffee b/src/typescript.coffee deleted file mode 100644 index 3a54941f3..000000000 --- a/src/typescript.coffee +++ /dev/null @@ -1,106 +0,0 @@ -### -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 # ES5 - module: 'commonjs' - sourceMap: true - -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') - shasum.update(JSON.stringify(options)) - shasum.digest('hex') - -cacheDir = null -jsCacheDir = null - -getCachePath = (sourceCode) -> - digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex') - - unless jsCacheDir? - tssVersion = require('typescript-simple/package.json').version - jsCacheDir = path.join(cacheDir, createTypeScriptVersionAndOptionsDigest(tssVersion, 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) - unless tss? - {TypeScriptSimple} = require 'typescript-simple' - tss = new 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/src/typescript.js b/src/typescript.js new file mode 100644 index 000000000..c372c775b --- /dev/null +++ b/src/typescript.js @@ -0,0 +1,53 @@ +'use strict' + +const _ = require('underscore-plus') +const crypto = require('crypto') +const path = require('path') + +let TypeScriptSimple = null +let typescriptVersionDir = null + +const defaultOptions = { + target: 1, + module: 'commonjs', + sourceMap: true +} + +exports.shouldCompile = function() { + return true +} + +exports.getCachePath = function(sourceCode) { + if (typescriptVersionDir == null) { + let version = require('typescript-simple/package.json').version + typescriptVersionDir = path.join('ts', createVersionAndOptionsDigest(version, defaultOptions)) + } + + return path.join( + typescriptVersionDir, + crypto + .createHash('sha1') + .update(sourceCode, 'utf8') + .digest('hex') + ".js" + ) +} + +exports.compile = function(sourceCode, filePath) { + if (!TypeScriptSimple) { + TypeScriptSimple = require('typescript-simple').TypeScriptSimple + } + + let options = _.defaults({filename: filePath}, defaultOptions) + return new TypeScriptSimple(options, false).compile(sourceCode, filePath) +} + +function createVersionAndOptionsDigest (version, options) { + return crypto + .createHash('sha1') + .update('typescript', 'utf8') + .update('\0', 'utf8') + .update(version, 'utf8') + .update('\0', 'utf8') + .update(JSON.stringify(options), 'utf8') + .digest('hex') +} diff --git a/static/index.js b/static/index.js index 8fe71a6a9..238cb385e 100644 --- a/static/index.js +++ b/static/index.js @@ -41,15 +41,6 @@ window.onload = function() { } } -var getCacheDirectory = function() { - var cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache'); - // Use separate compile cache when sudo'ing as root to avoid permission issues - if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) { - cacheDir = path.join(cacheDir, 'root'); - } - return cacheDir; -} - var setLoadTime = function(loadTime) { if (global.atom) { global.atom.loadTime = loadTime; @@ -67,9 +58,8 @@ var handleSetupError = function(error) { } var setupWindow = function(loadSettings) { - var cacheDir = getCacheDirectory(); - - setupCoffeeCache(cacheDir); + var compileCache = require('../src/compile-cache') + compileCache.setAtomHomeDirectory(process.env.ATOM_HOME) ModuleCache = require('../src/module-cache'); ModuleCache.register(loadSettings); @@ -88,21 +78,11 @@ var setupWindow = function(loadSettings) { }); setupVmCompatibility(); - setupCsonCache(cacheDir); - setupSourceMapCache(cacheDir); - setupBabel(cacheDir); - setupTypeScript(cacheDir); require(loadSettings.bootstrapScript); require('ipc').sendChannel('window-command', 'window:loaded'); } -var setupCoffeeCache = function(cacheDir) { - var CoffeeCache = require('coffee-cash'); - CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee')); - CoffeeCache.register(); -} - var setupAtomHome = function() { if (!process.env.ATOM_HOME) { var home; @@ -121,26 +101,6 @@ var setupAtomHome = function() { } } -var setupBabel = function(cacheDir) { - var babel = require('../src/babel'); - babel.setCacheDirectory(path.join(cacheDir, 'js', 'babel')); - 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')); -} - -var setupSourceMapCache = function(cacheDir) { - require('coffeestack').setCacheDirectory(path.join(cacheDir, 'coffee', 'source-maps')); -} - var setupVmCompatibility = function() { var vm = require('vm'); if (!vm.Script.createContext) {