compile-cache.js
1 'use strict'; 2 3 // For now, we're not using babel or ES6 features like `let` and `const` in 4 // this file, because `apm` requires this file directly in order to pre-warm 5 // Atom's compile-cache when installing or updating packages, using an older 6 // version of node.js 7 8 var path = require('path'); 9 var fs = require('fs-plus'); 10 var sourceMapSupport = require('@atom/source-map-support'); 11 12 var PackageTranspilationRegistry = require('./package-transpilation-registry'); 13 var CSON = null; 14 15 var packageTranspilationRegistry = new PackageTranspilationRegistry(); 16 17 var COMPILERS = { 18 '.js': packageTranspilationRegistry.wrapTranspiler(require('./babel')), 19 '.ts': packageTranspilationRegistry.wrapTranspiler(require('./typescript')), 20 '.tsx': packageTranspilationRegistry.wrapTranspiler(require('./typescript')), 21 '.coffee': packageTranspilationRegistry.wrapTranspiler( 22 require('./coffee-script') 23 ) 24 }; 25 26 exports.addTranspilerConfigForPath = function( 27 packagePath, 28 packageName, 29 packageMeta, 30 config 31 ) { 32 packagePath = fs.realpathSync(packagePath); 33 packageTranspilationRegistry.addTranspilerConfigForPath( 34 packagePath, 35 packageName, 36 packageMeta, 37 config 38 ); 39 }; 40 41 exports.removeTranspilerConfigForPath = function(packagePath) { 42 packagePath = fs.realpathSync(packagePath); 43 packageTranspilationRegistry.removeTranspilerConfigForPath(packagePath); 44 }; 45 46 var cacheStats = {}; 47 var cacheDirectory = null; 48 49 exports.setAtomHomeDirectory = function(atomHome) { 50 var cacheDir = path.join(atomHome, 'compile-cache'); 51 if ( 52 process.env.USER === 'root' && 53 process.env.SUDO_USER && 54 process.env.SUDO_USER !== process.env.USER 55 ) { 56 cacheDir = path.join(cacheDir, 'root'); 57 } 58 this.setCacheDirectory(cacheDir); 59 }; 60 61 exports.setCacheDirectory = function(directory) { 62 cacheDirectory = directory; 63 }; 64 65 exports.getCacheDirectory = function() { 66 return cacheDirectory; 67 }; 68 69 exports.addPathToCache = function(filePath, atomHome) { 70 this.setAtomHomeDirectory(atomHome); 71 var extension = path.extname(filePath); 72 73 if (extension === '.cson') { 74 if (!CSON) { 75 CSON = require('season'); 76 CSON.setCacheDir(this.getCacheDirectory()); 77 } 78 return CSON.readFileSync(filePath); 79 } else { 80 var compiler = COMPILERS[extension]; 81 if (compiler) { 82 return compileFileAtPath(compiler, filePath, extension); 83 } 84 } 85 }; 86 87 exports.getCacheStats = function() { 88 return cacheStats; 89 }; 90 91 exports.resetCacheStats = function() { 92 Object.keys(COMPILERS).forEach(function(extension) { 93 cacheStats[extension] = { 94 hits: 0, 95 misses: 0 96 }; 97 }); 98 }; 99 100 function compileFileAtPath(compiler, filePath, extension) { 101 var sourceCode = fs.readFileSync(filePath, 'utf8'); 102 if (compiler.shouldCompile(sourceCode, filePath)) { 103 var cachePath = compiler.getCachePath(sourceCode, filePath); 104 var compiledCode = readCachedJavaScript(cachePath); 105 if (compiledCode != null) { 106 cacheStats[extension].hits++; 107 } else { 108 cacheStats[extension].misses++; 109 compiledCode = compiler.compile(sourceCode, filePath); 110 writeCachedJavaScript(cachePath, compiledCode); 111 } 112 return compiledCode; 113 } 114 return sourceCode; 115 } 116 117 function readCachedJavaScript(relativeCachePath) { 118 var cachePath = path.join(cacheDirectory, relativeCachePath); 119 if (fs.isFileSync(cachePath)) { 120 try { 121 return fs.readFileSync(cachePath, 'utf8'); 122 } catch (error) {} 123 } 124 return null; 125 } 126 127 function writeCachedJavaScript(relativeCachePath, code) { 128 var cachePath = path.join(cacheDirectory, relativeCachePath); 129 fs.writeFileSync(cachePath, code, 'utf8'); 130 } 131 132 var INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/gm; 133 134 exports.install = function(resourcesPath, nodeRequire) { 135 const snapshotSourceMapConsumer = { 136 originalPositionFor({ line, column }) { 137 const { relativePath, row } = snapshotResult.translateSnapshotRow(line); 138 return { 139 column, 140 line: row, 141 source: path.join(resourcesPath, 'app', 'static', relativePath), 142 name: null 143 }; 144 } 145 }; 146 147 sourceMapSupport.install({ 148 handleUncaughtExceptions: false, 149 150 // Most of this logic is the same as the default implementation in the 151 // source-map-support module, but we've overridden it to read the javascript 152 // code from our cache directory. 153 retrieveSourceMap: function(filePath) { 154 if (filePath === '<embedded>') { 155 return { map: snapshotSourceMapConsumer }; 156 } 157 158 if (!cacheDirectory || !fs.isFileSync(filePath)) { 159 return null; 160 } 161 162 try { 163 var sourceCode = fs.readFileSync(filePath, 'utf8'); 164 } catch (error) { 165 console.warn('Error reading source file', error.stack); 166 return null; 167 } 168 169 var compiler = COMPILERS[path.extname(filePath)]; 170 if (!compiler) compiler = COMPILERS['.js']; 171 172 try { 173 var fileData = readCachedJavaScript( 174 compiler.getCachePath(sourceCode, filePath) 175 ); 176 } catch (error) { 177 console.warn('Error reading compiled file', error.stack); 178 return null; 179 } 180 181 if (fileData == null) { 182 return null; 183 } 184 185 var match, lastMatch; 186 INLINE_SOURCE_MAP_REGEXP.lastIndex = 0; 187 while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) { 188 lastMatch = match; 189 } 190 if (lastMatch == null) { 191 return null; 192 } 193 194 var sourceMappingURL = lastMatch[1]; 195 var rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1); 196 197 try { 198 var sourceMap = JSON.parse(Buffer.from(rawData, 'base64')); 199 } catch (error) { 200 console.warn('Error parsing source map', error.stack); 201 return null; 202 } 203 204 return { 205 map: sourceMap, 206 url: null 207 }; 208 } 209 }); 210 211 var prepareStackTraceWithSourceMapping = Error.prepareStackTrace; 212 var prepareStackTrace = prepareStackTraceWithSourceMapping; 213 214 function prepareStackTraceWithRawStackAssignment(error, frames) { 215 if (error.rawStack) { 216 // avoid infinite recursion 217 return prepareStackTraceWithSourceMapping(error, frames); 218 } else { 219 error.rawStack = frames; 220 return prepareStackTrace(error, frames); 221 } 222 } 223 224 Error.stackTraceLimit = 30; 225 226 Object.defineProperty(Error, 'prepareStackTrace', { 227 get: function() { 228 return prepareStackTraceWithRawStackAssignment; 229 }, 230 231 set: function(newValue) { 232 prepareStackTrace = newValue; 233 process.nextTick(function() { 234 prepareStackTrace = prepareStackTraceWithSourceMapping; 235 }); 236 } 237 }); 238 239 // eslint-disable-next-line no-extend-native 240 Error.prototype.getRawStack = function() { 241 // Access this.stack to ensure prepareStackTrace has been run on this error 242 // because it assigns this.rawStack as a side-effect 243 this.stack; // eslint-disable-line no-unused-expressions 244 return this.rawStack; 245 }; 246 247 Object.keys(COMPILERS).forEach(function(extension) { 248 var compiler = COMPILERS[extension]; 249 250 Object.defineProperty(nodeRequire.extensions, extension, { 251 enumerable: true, 252 writable: false, 253 value: function(module, filePath) { 254 var code = compileFileAtPath(compiler, filePath, extension); 255 return module._compile(code, filePath); 256 } 257 }); 258 }); 259 }; 260 261 exports.supportedExtensions = Object.keys(COMPILERS); 262 exports.resetCacheStats();