diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index abeb13f14..d21e8fa4a 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -1,6 +1,9 @@ fs = require 'fs' path = require 'path' os = require 'os' +glob = require 'glob' +usesBabel = require './lib/uses-babel' +babelOptions = require '../static/babelrc' # Add support for obselete APIs of vm module so we can make some third-party # modules work under node v0.11.x. @@ -10,13 +13,11 @@ _ = require 'underscore-plus' packageJson = require '../package.json' -# Shim harmony collections in case grunt was invoked without harmony -# collections enabled -_.extend(global, require('harmony-collections')) unless global.WeakMap? - module.exports = (grunt) -> + grunt.loadNpmTasks('grunt-babel') grunt.loadNpmTasks('grunt-coffeelint') grunt.loadNpmTasks('grunt-lesslint') + grunt.loadNpmTasks('grunt-standard') grunt.loadNpmTasks('grunt-cson') grunt.loadNpmTasks('grunt-contrib-csslint') grunt.loadNpmTasks('grunt-contrib-coffee') @@ -77,6 +78,11 @@ module.exports = (grunt) -> dest: appDir ext: '.js' + babelConfig = + options: babelOptions + dist: + files: [] + lessConfig = options: paths: [ @@ -141,6 +147,13 @@ module.exports = (grunt) -> pegConfig.glob_to_multiple.src.push("#{directory}/lib/*.pegjs") + for jsFile in glob.sync("#{directory}/lib/**/*.js") + if usesBabel(jsFile) + babelConfig.dist.files.push({ + src: [jsFile] + dest: path.join(appDir, jsFile) + }) + grunt.initConfig pkg: grunt.file.readJSON('package.json') @@ -148,6 +161,8 @@ module.exports = (grunt) -> docsOutputDir: 'docs/output' + babel: babelConfig + coffee: coffeeConfig less: lessConfig @@ -174,6 +189,12 @@ module.exports = (grunt) -> 'spec/*.coffee' ] + standard: + src: [ + 'src/**/*.js' + 'static/*.js' + ] + csslint: options: 'adjoining-classes': false @@ -229,8 +250,8 @@ module.exports = (grunt) -> stderr: false failOnError: false - grunt.registerTask('compile', ['coffee', 'prebuild-less', 'cson', 'peg']) - grunt.registerTask('lint', ['coffeelint', 'csslint', 'lesslint']) + grunt.registerTask('compile', ['babel', 'coffee', 'prebuild-less', 'cson', 'peg']) + grunt.registerTask('lint', ['standard', 'coffeelint', 'csslint', 'lesslint']) grunt.registerTask('test', ['shell:kill-atom', 'run-specs']) ciTasks = ['output-disk-space', 'download-atom-shell', 'download-atom-shell-chromedriver', 'build'] diff --git a/build/lib/uses-babel.coffee b/build/lib/uses-babel.coffee new file mode 100644 index 000000000..dcd480d4e --- /dev/null +++ b/build/lib/uses-babel.coffee @@ -0,0 +1,18 @@ +fs = require 'fs' + +BABEL_PREFIXES = [ + "'use babel'" + '"use babel"' + '/** @babel */' +] + +PREFIX_LENGTH = Math.max(BABEL_PREFIXES.map((prefix) -> prefix.length)...) + +buffer = Buffer(PREFIX_LENGTH) + +module.exports = (filename) -> + file = fs.openSync(filename, 'r') + fs.readSync(file, buffer, 0, PREFIX_LENGTH) + fs.closeSync(file) + BABEL_PREFIXES.some (prefix) -> + prefix is buffer.toString('utf8', 0, prefix.length) diff --git a/build/package.json b/build/package.json index cb41bb0a4..bd0c9a474 100644 --- a/build/package.json +++ b/build/package.json @@ -12,8 +12,9 @@ "formidable": "~1.0.14", "fs-plus": "2.x", "github-releases": "~0.2.0", + "glob": "^5.0.14", "grunt": "~0.4.1", - "grunt-electron-installer": "^0.37.0", + "grunt-babel": "^5.0.1", "grunt-cli": "~0.1.9", "grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git#cfb99aa99811d52687969532bd5a98011ed95bfe", "grunt-contrib-coffee": "~0.12.0", @@ -21,10 +22,11 @@ "grunt-contrib-less": "~0.8.0", "grunt-cson": "0.14.0", "grunt-download-atom-shell": "~0.14.0", + "grunt-electron-installer": "^0.37.0", "grunt-lesslint": "0.17.0", "grunt-peg": "~1.1.0", "grunt-shell": "~0.3.1", - "harmony-collections": "~0.3.8", + "grunt-standard": "^1.0.2", "legal-eagle": "~0.10.0", "minidump": "~0.9", "npm": "2.13.3", diff --git a/package.json b/package.json index 5680909ee..b947df59f 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,7 @@ "babel-core": "^5.8.21", "bootstrap": "^3.3.4", "clear-cut": "^2.0.1", - "coffee-cash": "0.8.0", "coffee-script": "1.8.0", - "coffeestack": "^1.1.2", "color": "^0.7.3", "delegato": "^1", "emissary": "^1.3.3", @@ -32,7 +30,7 @@ "fstream": "0.1.24", "fuzzaldrin": "^2.1", "git-utils": "^3.0.0", - "grim": "1.4.1", + "grim": "1.4.2", "jasmine-json": "~0.0", "jasmine-tagged": "^1.1.4", "jquery": "^2.1.1", @@ -54,6 +52,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", @@ -101,6 +100,7 @@ "image-view": "0.54.0", "incompatible-packages": "0.24.1", "keybinding-resolver": "0.33.0", + "line-ending-selector": "0.0.3", "link": "0.30.0", "markdown-preview": "0.150.0", "metrics": "0.51.0", diff --git a/spec/atom-reporter.coffee b/spec/atom-reporter.coffee index 1fdbd1385..5e52b4b9b 100644 --- a/spec/atom-reporter.coffee +++ b/spec/atom-reporter.coffee @@ -1,24 +1,19 @@ path = require 'path' _ = require 'underscore-plus' -{convertStackTrace} = require 'coffeestack' {View, $, $$} = require '../src/space-pen-extensions' grim = require 'grim' marked = require 'marked' -sourceMaps = {} formatStackTrace = (spec, message='', stackTrace) -> return stackTrace unless stackTrace jasminePattern = /^\s*at\s+.*\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/ firstJasmineLinePattern = /^\s*at [/\\].*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/ - convertedLines = [] + lines = [] for line in stackTrace.split('\n') - convertedLines.push(line) unless jasminePattern.test(line) + lines.push(line) unless jasminePattern.test(line) break if firstJasmineLinePattern.test(line) - stackTrace = convertStackTrace(convertedLines.join('\n'), sourceMaps) - lines = stackTrace.split('\n') - # Remove first line of stack when it is the same as the error message errorMatch = lines[0]?.match(/^Error: (.*)/) lines.shift() if message.trim() is errorMatch?[1]?.trim() diff --git a/spec/babel-spec.coffee b/spec/babel-spec.coffee index 4aec0ef8d..caaaed9f2 100644 --- a/spec/babel-spec.coffee +++ b/spec/babel-spec.coffee @@ -1,64 +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 /** @babel */;', -> + it "transpiles it using babel", -> + transpiled = require('./fixtures/babel/babel-comment.js') + expect(transpiled(3)).toBe 4 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 index 6eb6556d0..94910bb58 100644 --- a/spec/compile-cache-spec.coffee +++ b/spec/compile-cache-spec.coffee @@ -1,39 +1,71 @@ path = require 'path' +temp = require('temp').track() +Babel = require 'babel-core' +CoffeeScript = require 'coffee-script' +{TypeScriptSimple} = require 'typescript-simple' CSON = require 'season' -CoffeeCache = require 'coffee-cash' - -babel = require '../src/babel' -typescript = require '../src/typescript' +CSONParser = require 'season/node_modules/cson-parser' 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() +describe 'CompileCache', -> + [atomHome, fixtures] = [] - 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 + beforeEach -> + fixtures = atom.project.getPaths()[0] + atomHome = temp.mkdirSync('fake-atom-home') - 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 + CSON.setCacheDir(null) + CompileCache.resetCacheStats() - 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 + spyOn(Babel, 'transform').andReturn {code: 'the-babel-code'} + spyOn(CoffeeScript, 'compile').andReturn {js: 'the-coffee-code', v3SourceMap: {}} + spyOn(TypeScriptSimple::, 'compile').andReturn 'the-typescript-code' + spyOn(CSONParser, 'parse').andReturn {the: 'cson-data'} - 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 + afterEach -> + CSON.setCacheDir(CompileCache.getCacheDirectory()) + CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) + + describe 'addPathToCache(filePath, atomHome)', -> + describe 'when the given file is plain javascript', -> + it 'does not compile or cache the file', -> + CompileCache.addPathToCache(path.join(fixtures, 'sample.js'), atomHome) + expect(CompileCache.getCacheStats()['.js']).toEqual {hits: 0, misses: 0} + + describe 'when the given file uses babel', -> + it 'compiles the file with babel and caches it', -> + CompileCache.addPathToCache(path.join(fixtures, 'babel', 'babel-comment.js'), atomHome) + expect(CompileCache.getCacheStats()['.js']).toEqual {hits: 0, misses: 1} + expect(Babel.transform.callCount).toBe 1 + + CompileCache.addPathToCache(path.join(fixtures, 'babel', 'babel-comment.js'), atomHome) + expect(CompileCache.getCacheStats()['.js']).toEqual {hits: 1, misses: 1} + expect(Babel.transform.callCount).toBe 1 + + describe 'when the given file is coffee-script', -> + it 'compiles the file with coffee-script and caches it', -> + CompileCache.addPathToCache(path.join(fixtures, 'coffee.coffee'), atomHome) + expect(CompileCache.getCacheStats()['.coffee']).toEqual {hits: 0, misses: 1} + expect(CoffeeScript.compile.callCount).toBe 1 + + CompileCache.addPathToCache(path.join(fixtures, 'coffee.coffee'), atomHome) + expect(CompileCache.getCacheStats()['.coffee']).toEqual {hits: 1, misses: 1} + expect(CoffeeScript.compile.callCount).toBe 1 + + describe 'when the given file is typescript', -> + it 'compiles the file with typescript and caches it', -> + CompileCache.addPathToCache(path.join(fixtures, 'typescript', 'valid.ts'), atomHome) + expect(CompileCache.getCacheStats()['.ts']).toEqual {hits: 0, misses: 1} + expect(TypeScriptSimple::compile.callCount).toBe 1 + + CompileCache.addPathToCache(path.join(fixtures, 'typescript', 'valid.ts'), atomHome) + expect(CompileCache.getCacheStats()['.ts']).toEqual {hits: 1, misses: 1} + expect(TypeScriptSimple::compile.callCount).toBe 1 + + describe 'when the given file is CSON', -> + it 'compiles the file to JSON and caches it', -> + CompileCache.addPathToCache(path.join(fixtures, 'cson.cson'), atomHome) + expect(CSONParser.parse.callCount).toBe 1 + + CompileCache.addPathToCache(path.join(fixtures, 'cson.cson'), atomHome) + expect(CSONParser.parse.callCount).toBe 1 diff --git a/spec/fixtures/babel/6to5-single-quotes.js b/spec/fixtures/babel/6to5-single-quotes.js deleted file mode 100644 index 16e382ec2..000000000 --- a/spec/fixtures/babel/6to5-single-quotes.js +++ /dev/null @@ -1,3 +0,0 @@ -'use 6to5'; - -module.exports = v => v + 1 diff --git a/spec/fixtures/babel/6to5-double-quotes.js b/spec/fixtures/babel/babel-comment.js similarity index 67% rename from spec/fixtures/babel/6to5-double-quotes.js rename to spec/fixtures/babel/babel-comment.js index 254912bc0..ee9a27373 100644 --- a/spec/fixtures/babel/6to5-double-quotes.js +++ b/spec/fixtures/babel/babel-comment.js @@ -1,3 +1,3 @@ -"use 6to5"; +/** @babel */ module.exports = v => v + 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/atom.coffee b/src/atom.coffee index 7065a0d93..28058f8dd 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -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} diff --git a/src/babel.coffee b/src/babel.coffee deleted file mode 100644 index 201210817..000000000 --- a/src/babel.coffee +++ /dev/null @@ -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: '' - 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) diff --git a/src/babel.js b/src/babel.js new file mode 100644 index 000000000..f53dbc758 --- /dev/null +++ b/src/babel.js @@ -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') +} diff --git a/src/browser/main.coffee b/src/browser/main.coffee index 8d429f15f..fac1191a7 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -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() diff --git a/src/coffee-script.js b/src/coffee-script.js new file mode 100644 index 000000000..90f23bfa5 --- /dev/null +++ b/src/coffee-script.js @@ -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 +} 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..9e4d14e15 --- /dev/null +++ b/src/compile-cache.js @@ -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() 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..c942f542a --- /dev/null +++ b/src/typescript.js @@ -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') +} diff --git a/static/babelrc.json b/static/babelrc.json new file mode 100644 index 000000000..26b70dc41 --- /dev/null +++ b/static/babelrc.json @@ -0,0 +1,7 @@ +{ + "breakConfig": true, + "sourceMap": "inline", + "blacklist": ["es6.forOf", "useStrict"], + "optional": ["asyncToGenerator"], + "stage": 0 +} diff --git a/static/index.js b/static/index.js index 8fe71a6a9..914290321 100644 --- a/static/index.js +++ b/static/index.js @@ -1,234 +1,197 @@ -(function() { +(function () { + var fs = require('fs') + var path = require('path') -var fs = require('fs'); -var path = require('path'); + var loadSettings = null + var loadSettingsError = null -var loadSettings = null; -var loadSettingsError = null; - -window.onload = function() { - try { - var startTime = Date.now(); - - process.on('unhandledRejection', function(error, promise) { - console.error('Unhandled promise rejection %o with error: %o', promise, error); - }); - - // Ensure ATOM_HOME is always set before anything else is required - setupAtomHome(); - - // Normalize to make sure drive letter case is consistent on Windows - process.resourcesPath = path.normalize(process.resourcesPath); - - if (loadSettingsError) { - throw loadSettingsError; - } - - var devMode = loadSettings.devMode || !loadSettings.resourcePath.startsWith(process.resourcesPath + path.sep); - - if (devMode) { - setupDeprecatedPackages(); - } - - if (loadSettings.profileStartup) { - profileStartup(loadSettings, Date.now() - startTime); - } else { - setupWindow(loadSettings); - setLoadTime(Date.now() - startTime); - } - } catch (error) { - handleSetupError(error); - } -} - -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; - console.log('Window load time: ' + global.atom.getWindowLoadTime() + 'ms'); - } -} - -var handleSetupError = function(error) { - var currentWindow = require('remote').getCurrentWindow(); - currentWindow.setSize(800, 600); - currentWindow.center(); - currentWindow.show(); - currentWindow.openDevTools(); - console.error(error.stack || error); -} - -var setupWindow = function(loadSettings) { - var cacheDir = getCacheDirectory(); - - setupCoffeeCache(cacheDir); - - ModuleCache = require('../src/module-cache'); - ModuleCache.register(loadSettings); - ModuleCache.add(loadSettings.resourcePath); - - // Only include deprecated APIs when running core spec - require('grim').includeDeprecatedAPIs = isRunningCoreSpecs(loadSettings); - - // Start the crash reporter before anything else. - require('crash-reporter').start({ - productName: 'Atom', - companyName: 'GitHub', - // By explicitly passing the app version here, we could save the call - // of "require('remote').require('app').getVersion()". - extra: {_version: loadSettings.appVersion} - }); - - 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; - if (process.platform === 'win32') { - home = process.env.USERPROFILE; - } else { - home = process.env.HOME; - } - var atomHome = path.join(home, '.atom'); - try { - atomHome = fs.realpathSync(atomHome); - } catch (error) { - // Ignore since the path might just not exist yet. - } - process.env.ATOM_HOME = atomHome; - } -} - -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) { - vm.Script.createContext = vm.createContext; - } -} - -var setupDeprecatedPackages = function() { - var metadata = require('../package.json'); - if (!metadata._deprecatedPackages) { - try { - metadata._deprecatedPackages = require('../build/deprecated-packages.json'); - } catch(requireError) { - console.error('Failed to setup deprecated packages list', requireError.stack); - } - } -} - -var profileStartup = function(loadSettings, initialTime) { - var profile = function() { - console.profile('startup'); + window.onload = function () { try { var startTime = Date.now() - setupWindow(loadSettings); - setLoadTime(Date.now() - startTime + initialTime); + + process.on('unhandledRejection', function (error, promise) { + console.error('Unhandled promise rejection %o with error: %o', promise, error) + }) + + // Ensure ATOM_HOME is always set before anything else is required + setupAtomHome() + + // Normalize to make sure drive letter case is consistent on Windows + process.resourcesPath = path.normalize(process.resourcesPath) + + if (loadSettingsError) { + throw loadSettingsError + } + + var devMode = loadSettings.devMode || !loadSettings.resourcePath.startsWith(process.resourcesPath + path.sep) + + if (devMode) { + setupDeprecatedPackages() + } + + if (loadSettings.profileStartup) { + profileStartup(loadSettings, Date.now() - startTime) + } else { + setupWindow(loadSettings) + setLoadTime(Date.now() - startTime) + } } catch (error) { - handleSetupError(error); - } finally { - console.profileEnd('startup'); - console.log("Switch to the Profiles tab to view the created startup profile") + handleSetupError(error) } - }; - - var currentWindow = require('remote').getCurrentWindow(); - if (currentWindow.devToolsWebContents) { - profile(); - } else { - currentWindow.openDevTools(); - currentWindow.once('devtools-opened', function() { - setTimeout(profile, 100); - }); - } -} - -var parseLoadSettings = function() { - var rawLoadSettings = decodeURIComponent(location.hash.substr(1)); - try { - loadSettings = JSON.parse(rawLoadSettings); - } catch (error) { - console.error("Failed to parse load settings: " + rawLoadSettings); - loadSettingsError = error; - } -} - -var setupWindowBackground = function() { - if (loadSettings && loadSettings.isSpec) { - return; } - var backgroundColor = window.localStorage.getItem('atom:window-background-color'); - if (!backgroundColor) { - return; + function setLoadTime (loadTime) { + if (global.atom) { + global.atom.loadTime = loadTime + console.log('Window load time: ' + global.atom.getWindowLoadTime() + 'ms') + } } - var backgroundStylesheet = document.createElement('style'); - backgroundStylesheet.type = 'text/css'; - backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + '; }'; - document.head.appendChild(backgroundStylesheet); + function handleSetupError (error) { + var currentWindow = require('remote').getCurrentWindow() + currentWindow.setSize(800, 600) + currentWindow.center() + currentWindow.show() + currentWindow.openDevTools() + console.error(error.stack || error) + } - // Remove once the page loads - window.addEventListener("load", function loadWindow() { - window.removeEventListener("load", loadWindow, false); - setTimeout(function() { - backgroundStylesheet.remove(); - backgroundStylesheet = null; - }, 1000); - }, false); -} + function setupWindow (loadSettings) { + var CompileCache = require('../src/compile-cache') + CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) -var isRunningCoreSpecs = function(loadSettings) { - return !!(loadSettings && - loadSettings.isSpec && - loadSettings.specDirectory && - loadSettings.resourcePath && - path.dirname(loadSettings.specDirectory) === loadSettings.resourcePath); -} + var ModuleCache = require('../src/module-cache') + ModuleCache.register(loadSettings) + ModuleCache.add(loadSettings.resourcePath) -parseLoadSettings(); -setupWindowBackground(); + // Only include deprecated APIs when running core spec + require('grim').includeDeprecatedAPIs = isRunningCoreSpecs(loadSettings) -})(); + // Start the crash reporter before anything else. + require('crash-reporter').start({ + productName: 'Atom', + companyName: 'GitHub', + // By explicitly passing the app version here, we could save the call + // of "require('remote').require('app').getVersion()". + extra: {_version: loadSettings.appVersion} + }) + + setupVmCompatibility() + setupCsonCache(CompileCache.getCacheDirectory()) + + require(loadSettings.bootstrapScript) + require('ipc').sendChannel('window-command', 'window:loaded') + } + + function setupAtomHome () { + if (!process.env.ATOM_HOME) { + var home + if (process.platform === 'win32') { + home = process.env.USERPROFILE + } else { + home = process.env.HOME + } + var atomHome = path.join(home, '.atom') + try { + atomHome = fs.realpathSync(atomHome) + } catch (error) { + // Ignore since the path might just not exist yet. + } + process.env.ATOM_HOME = atomHome + } + } + + function setupCsonCache (cacheDir) { + require('season').setCacheDir(path.join(cacheDir, 'cson')) + } + + function setupVmCompatibility () { + var vm = require('vm') + if (!vm.Script.createContext) { + vm.Script.createContext = vm.createContext + } + } + + function setupDeprecatedPackages () { + var metadata = require('../package.json') + if (!metadata._deprecatedPackages) { + try { + metadata._deprecatedPackages = require('../build/deprecated-packages.json') + } catch(requireError) { + console.error('Failed to setup deprecated packages list', requireError.stack) + } + } + } + + function profileStartup (loadSettings, initialTime) { + function profile () { + console.profile('startup') + try { + var startTime = Date.now() + setupWindow(loadSettings) + setLoadTime(Date.now() - startTime + initialTime) + } catch (error) { + handleSetupError(error) + } finally { + console.profileEnd('startup') + console.log('Switch to the Profiles tab to view the created startup profile') + } + } + + var currentWindow = require('remote').getCurrentWindow() + if (currentWindow.devToolsWebContents) { + profile() + } else { + currentWindow.openDevTools() + currentWindow.once('devtools-opened', function () { + setTimeout(profile, 100) + }) + } + } + + function parseLoadSettings () { + var rawLoadSettings = decodeURIComponent(window.location.hash.substr(1)) + try { + loadSettings = JSON.parse(rawLoadSettings) + } catch (error) { + console.error('Failed to parse load settings: ' + rawLoadSettings) + loadSettingsError = error + } + } + + function setupWindowBackground () { + if (loadSettings && loadSettings.isSpec) { + return + } + + var backgroundColor = window.localStorage.getItem('atom:window-background-color') + if (!backgroundColor) { + return + } + + var backgroundStylesheet = document.createElement('style') + backgroundStylesheet.type = 'text/css' + backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + '; }' + document.head.appendChild(backgroundStylesheet) + + // Remove once the page loads + window.addEventListener('load', function loadWindow () { + window.removeEventListener('load', loadWindow, false) + setTimeout(function () { + backgroundStylesheet.remove() + backgroundStylesheet = null + }, 1000) + }, false) + } + + function isRunningCoreSpecs (loadSettings) { + return !!(loadSettings && + loadSettings.isSpec && + loadSettings.specDirectory && + loadSettings.resourcePath && + path.dirname(loadSettings.specDirectory) === loadSettings.resourcePath) + } + + parseLoadSettings() + setupWindowBackground() +})()