/ src / compile-cache.js
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();