From ad448184b4eefb647249209a75a4cf981be7fa60 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Sat, 29 Oct 2016 14:30:24 -0700 Subject: [PATCH] First pass at per-package transpilation --- src/compile-cache.js | 18 +++- src/package-transpilation-registry.js | 122 ++++++++++++++++++++++++++ src/package.coffee | 15 ++++ 3 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 src/package-transpilation-registry.js diff --git a/src/compile-cache.js b/src/compile-cache.js index ad1bd0a85..702b2659f 100644 --- a/src/compile-cache.js +++ b/src/compile-cache.js @@ -7,12 +7,24 @@ var path = require('path') var fs = require('fs-plus') + +var PackageTranspilationRegistry = require('./package-transpilation-registry') var CSON = null +var packageTranspilationRegistry = new PackageTranspilationRegistry() + var COMPILERS = { - '.js': require('./babel'), - '.ts': require('./typescript'), - '.coffee': require('./coffee-script') + '.js': packageTranspilationRegistry.wrapTranspiler(require('./babel')), + '.ts': packageTranspilationRegistry.wrapTranspiler(require('./typescript')), + '.coffee': packageTranspilationRegistry.wrapTranspiler(require('./coffee-script')) +} + +exports.addTranspilerConfigForPath = function (packagePath, config) { + packageTranspilationRegistry.addTranspilerConfigForPath(packagePath, config) +} + +exports.removeTranspilerConfigForPath = function (packagePath) { + packageTranspilationRegistry.removeTranspilerConfigForPath(packagePath) } var cacheStats = {} diff --git a/src/package-transpilation-registry.js b/src/package-transpilation-registry.js new file mode 100644 index 000000000..caf83961e --- /dev/null +++ b/src/package-transpilation-registry.js @@ -0,0 +1,122 @@ +var crypto = require('crypto') +var fs = require('fs') +var path = require('path') + +var Resolve = null + +function PackageTranspilationRegistry () { + this.configByPackagePath = {} + this.configByFilePath = {} + this.transpilerPaths = {} + this.transpilerHashes = {} +} + +PackageTranspilationRegistry.prototype.addTranspilerConfigForPath = function (packagePath, config) { + packagePath = fs.realpathSync(packagePath) + this.configByPackagePath[packagePath] = Object.assign({}, config, { + path: packagePath + }) +} + +PackageTranspilationRegistry.prototype.removeTranspilerConfigForPath = function (packagePath) { + packagePath = fs.realpathSync(packagePath) + delete this.configByPackagePath[path] +} + +// Wraps the transpiler in an object with the same interface +// that falls back to the original transpiler implementation if and +// only if a package hasn't registered its desire to transpile its own source. +PackageTranspilationRegistry.prototype.wrapTranspiler = function (transpiler) { + var self = this + return { + getCachePath: function (sourceCode, filePath) { + var config = self.getPackageTranspilerConfigForFilePath(filePath) + if (config) { + return self.getCachePath(sourceCode, filePath, config) + } + + return transpiler.getCachePath(sourceCode, filePath) + }, + + compile: function (sourceCode, filePath) { + var config = self.getPackageTranspilerConfigForFilePath(filePath) + if (config) { + return self.transpileWithPackageTranspiler(sourceCode, filePath, config) + } + + return transpiler.compile(sourceCode, filePath) + }, + + shouldCompile: function (sourceCode, filePath) { + if (self.transpilerPaths[filePath]) { + return false + } + var config = self.getPackageTranspilerConfigForFilePath(filePath) + if (config) { + return true + } + + return transpiler.shouldCompile(sourceCode, filePath) + } + } +} + +PackageTranspilationRegistry.prototype.getPackageTranspilerConfigForFilePath = function (filePath) { + if (this.configByFilePath[filePath] !== undefined) return this.configByFilePath[filePath] + + var config = null + var thisPath = filePath + var lastPath = null + // Iterate parents from the file path to the root, checking at each level + // to see if a package manages transpilation for that directory. + // This means searching for a config for `/path/to/file/here.js` only + // only iterates four times, even if there are hundreds of configs registered. + while (thisPath !== lastPath) { // until we reach the root + if (config = this.configByPackagePath[thisPath]) { + this.configByFilePath[filePath] = config + return config + } + + lastPath = thisPath + thisPath = path.resolve(thisPath, '..') + } + + this.configByFilePath[filePath] = null + return null +} + +PackageTranspilationRegistry.prototype.getCachePath = function (sourceCode, filePath, config) { + var transpilerPath = path.join(config.path, config.transpiler) + var transpilerSource = config._transpilerSource || fs.readFileSync(transpilerPath, 'utf8') + config._transpilerSource = transpilerSource + return path.join( + "package-transpile", + crypto + .createHash('sha1') + .update(transpilerSource, 'utf8') + .update(sourceCode, 'utf8') + .digest('hex') + ) +} + +PackageTranspilationRegistry.prototype.transpileWithPackageTranspiler = function (sourceCode, filePath) { + var config = this.configByFilePath[filePath] + + Resolve = Resolve || require('resolve') + var transpilerPath = Resolve.sync(config.transpiler, {basedir: config.path, extensions: Object.keys(require.extensions)}) + if (transpilerPath) { + this.transpilerPaths[transpilerPath] = true + var transpiler = require(transpilerPath) + var result = transpiler.compile(sourceCode, filePath) + if (result === undefined) { + return sourceCode + } else { + return result + } + } else { + var err = new Error("Could not find transpiler '" + config.transpiler + "' from '" + config.path + "'") + console.error(err) + } +} + +module.exports = PackageTranspilationRegistry diff --git a/src/package.coffee b/src/package.coffee index 323dfa8d2..10764460f 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -6,6 +6,7 @@ CSON = require 'season' fs = require 'fs-plus' {Emitter, CompositeDisposable} = require 'event-kit' +CompileCache = require './compile-cache' ModuleCache = require './module-cache' ScopedProperties = require './scoped-properties' BufferedProcess = require './buffered-process' @@ -86,6 +87,7 @@ class Package @loadStylesheets() @registerDeserializerMethods() @activateCoreStartupServices() + @registerTranspilerConfig() @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() @settingsPromise = @loadSettings() if @shouldRequireMainModuleOnLoad() and not @mainModule? @@ -94,6 +96,9 @@ class Package @handleError("Failed to load the #{@name} package", error) this + unload: -> + @unregisterTranspilerConfig() + shouldRequireMainModuleOnLoad: -> not ( @metadata.deserializers? or @@ -247,6 +252,16 @@ class Package @activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) return + registerTranspilerConfig: -> + if @metadata.atomTranspilers + for transpiler in @metadata.atomTranspilers + CompileCache.addTranspilerConfigForPath(@path, transpiler) + + unregisterTranspilerConfig: -> + if @metadata.atomTranspilers + for transpiler in @metadata.atomTranspilers + CompileCache.removeTranspilerConfigForPath(@path) + loadKeymaps: -> if @bundledPackage and @packageManager.packagesCache[@name]? @keymaps = (["#{@packageManager.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of @packageManager.packagesCache[@name].keymaps)