Merge pull request #8433 from atom/mb-bundle-line-ending-selector

Support bundling babel packages and bundle the line-ending-selector package
This commit is contained in:
Max Brunsfeld
2015-08-21 17:45:10 -07:00
22 changed files with 656 additions and 702 deletions

View File

@@ -9,7 +9,7 @@ _ = require 'underscore-plus'
{deprecate, includeDeprecatedAPIs} = require 'grim'
{CompositeDisposable, Emitter} = require 'event-kit'
fs = require 'fs-plus'
{convertStackTrace, convertLine} = require 'coffeestack'
{mapSourcePosition} = require 'source-map-support'
Model = require './model'
{$} = require './space-pen-extensions'
WindowEventHandler = require './window-event-handler'
@@ -196,15 +196,11 @@ class Atom extends Model
#
# Call after this instance has been assigned to the `atom` global.
initialize: ->
sourceMapCache = {}
window.onerror = =>
@lastUncaughtError = Array::slice.call(arguments)
[message, url, line, column, originalError] = @lastUncaughtError
convertedLine = convertLine(url, line, column, sourceMapCache)
{line, column} = convertedLine if convertedLine?
originalError.stack = convertStackTrace(originalError.stack, sourceMapCache) if originalError
{line, column} = mapSourcePosition({source: url, line, column})
eventObject = {message, url, line, column, originalError}

View File

@@ -1,200 +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'")
# 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: '<unknown>'
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
# 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)

63
src/babel.js Normal file
View File

@@ -0,0 +1,63 @@
'use strict'
var crypto = require('crypto')
var path = require('path')
var defaultOptions = require('../static/babelrc.json')
var babel = null
var babelVersionDirectory = null
var PREFIXES = [
'/** @babel */',
'"use babel"',
'\'use babel\''
]
var PREFIX_LENGTH = Math.max.apply(Math, PREFIXES.map(function (prefix) {
return prefix.length
}))
exports.shouldCompile = function (sourceCode) {
var start = sourceCode.substr(0, PREFIX_LENGTH)
return PREFIXES.some(function (prefix) {
return start.indexOf(prefix) === 0
})
}
exports.getCachePath = function (sourceCode) {
if (babelVersionDirectory == null) {
var 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')
}
var options = {filename: filePath}
for (var key in defaultOptions) {
options[key] = defaultOptions[key]
}
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')
}

View File

@@ -16,7 +16,7 @@ process.on 'uncaughtException', (error={}) ->
start = ->
setupAtomHome()
setupCoffeeCache()
setupCompileCache()
if process.platform is 'win32'
SquirrelUpdate = require './squirrel-update'
@@ -77,14 +77,9 @@ setupAtomHome = ->
atomHome = fs.realpathSync(atomHome)
process.env.ATOM_HOME = atomHome
setupCoffeeCache = ->
CoffeeCache = require 'coffee-cash'
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 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'))
CoffeeCache.register()
setupCompileCache = ->
compileCache = require('../compile-cache')
compileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
parseCommandLine = ->
version = app.getVersion()

44
src/coffee-script.js Normal file
View File

@@ -0,0 +1,44 @@
'use strict'
var crypto = require('crypto')
var path = require('path')
var CoffeeScript = null
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) {
if (!CoffeeScript) {
var previousPrepareStackTrace = Error.prepareStackTrace
CoffeeScript = require('coffee-script')
// When it loads, coffee-script reassigns Error.prepareStackTrace. We have
// already reassigned it via the 'source-map-support' module, so we need
// to set it back.
Error.prepareStackTrace = previousPrepareStackTrace
}
var output = CoffeeScript.compile(sourceCode, {
filename: filePath,
sourceFiles: [filePath],
sourceMap: true
})
var js = output.js
js += '\n'
js += '//# sourceMappingURL=data:application/json;base64,'
js += new Buffer(output.v3SourceMap).toString('base64')
js += '\n'
return js
}

View File

@@ -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)

174
src/compile-cache.js Normal file
View File

@@ -0,0 +1,174 @@
'use strict'
var path = require('path')
var fs = require('fs-plus')
var CSON = null
var COMPILERS = {
'.js': require('./babel'),
'.ts': require('./typescript'),
'.coffee': require('./coffee-script')
}
var cacheStats = {}
var cacheDirectory = null
exports.setAtomHomeDirectory = function (atomHome) {
var 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
}
exports.getCacheDirectory = function () {
return cacheDirectory
}
exports.addPathToCache = function (filePath, atomHome) {
this.setAtomHomeDirectory(atomHome)
var extension = path.extname(filePath)
if (extension === '.cson') {
if (!CSON) {
CSON = require('season')
CSON.setCacheDir(this.getCacheDirectory())
}
CSON.readFileSync(filePath)
} else {
var compiler = COMPILERS[extension]
if (compiler) {
compileFileAtPath(compiler, filePath, extension)
}
}
}
exports.getCacheStats = function () {
return cacheStats
}
exports.resetCacheStats = function () {
Object.keys(COMPILERS).forEach(function (extension) {
cacheStats[extension] = {
hits: 0,
misses: 0
}
})
}
function compileFileAtPath (compiler, filePath, extension) {
var sourceCode = fs.readFileSync(filePath, 'utf8')
if (compiler.shouldCompile(sourceCode, filePath)) {
var cachePath = compiler.getCachePath(sourceCode, filePath)
var compiledCode = readCachedJavascript(cachePath)
if (compiledCode != null) {
cacheStats[extension].hits++
} else {
cacheStats[extension].misses++
compiledCode = addSourceURL(compiler.compile(sourceCode, filePath), filePath)
writeCachedJavascript(cachePath, compiledCode)
}
return compiledCode
}
return sourceCode
}
function readCachedJavascript (relativeCachePath) {
var cachePath = path.join(cacheDirectory, relativeCachePath)
if (fs.isFileSync(cachePath)) {
try {
return fs.readFileSync(cachePath, 'utf8')
} catch (error) {}
}
return null
}
function writeCachedJavascript (relativeCachePath, code) {
var cachePath = path.join(cacheDirectory, relativeCachePath)
fs.writeFileSync(cachePath, code, 'utf8')
}
function addSourceURL (jsCode, filePath) {
if (process.platform === 'win32') {
filePath = '/' + path.resolve(filePath).replace(/\\/g, '/')
}
return jsCode + '\n' + '//# sourceURL=' + encodeURI(filePath) + '\n'
}
var INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/mg
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
}
try {
var sourceCode = fs.readFileSync(filePath, 'utf8')
} catch (error) {
console.warn('Error reading source file', error.stack)
return null
}
var compiler = COMPILERS[path.extname(filePath)]
try {
var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath))
} catch (error) {
console.warn('Error reading compiled file', error.stack)
return null
}
if (fileData == null) {
return null
}
var match, lastMatch
INLINE_SOURCE_MAP_REGEXP.lastIndex = 0
while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) {
lastMatch = match
}
if (lastMatch == null) {
return null
}
var sourceMappingURL = lastMatch[1]
var rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1)
try {
var sourceMap = JSON.parse(new Buffer(rawData, 'base64'))
} catch (error) {
console.warn('Error parsing source map', error.stack)
return null
}
return {
map: sourceMap,
url: null
}
}
})
Object.keys(COMPILERS).forEach(function (extension) {
var compiler = COMPILERS[extension]
Object.defineProperty(require.extensions, extension, {
enumerable: true,
writable: false,
value: function (module, filePath) {
var code = compileFileAtPath(compiler, filePath, extension)
return module._compile(code, filePath)
}
})
})
exports.resetCacheStats()

View File

@@ -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, "\\\\")

View File

@@ -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)

53
src/typescript.js Normal file
View File

@@ -0,0 +1,53 @@
'use strict'
var _ = require('underscore-plus')
var crypto = require('crypto')
var path = require('path')
var defaultOptions = {
target: 1,
module: 'commonjs',
sourceMap: true
}
var TypeScriptSimple = null
var typescriptVersionDir = null
exports.shouldCompile = function () {
return true
}
exports.getCachePath = function (sourceCode) {
if (typescriptVersionDir == null) {
var 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
}
var 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')
}