src/compile-cache.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

// Atom's compile-cache when installing or updating packages, called by apm's Node-js

const path = require('path');
const fs = require('fs-plus');
const sourceMapSupport = require('@atom/source-map-support');

const PackageTranspilationRegistry = require('./package-transpilation-registry');
let CSON = null;

const packageTranspilationRegistry = new PackageTranspilationRegistry();

const COMPILERS = {
  '.js': packageTranspilationRegistry.wrapTranspiler(require('./babel')),
  '.ts': packageTranspilationRegistry.wrapTranspiler(require('./typescript')),
  '.tsx': packageTranspilationRegistry.wrapTranspiler(require('./typescript')),
  '.coffee': packageTranspilationRegistry.wrapTranspiler(
    require('./coffee-script')
  )
};

exports.addTranspilerConfigForPath = function(
  packagePath,
  packageName,
  packageMeta,
  config
) {
  packagePath = fs.realpathSync(packagePath);
  packageTranspilationRegistry.addTranspilerConfigForPath(
    packagePath,
    packageName,
    packageMeta,
    config
  );
};

exports.removeTranspilerConfigForPath = function(packagePath) {
  packagePath = fs.realpathSync(packagePath);
  packageTranspilationRegistry.removeTranspilerConfigForPath(packagePath);
};

const cacheStats = {};
let cacheDirectory = null;

exports.setAtomHomeDirectory = function(atomHome) {
  let 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(cacheDir, 'root');
  }
  this.setCacheDirectory(cacheDir);
};

exports.setCacheDirectory = function(directory) {
  cacheDirectory = directory;
};

exports.getCacheDirectory = function() {
  return cacheDirectory;
};

exports.addPathToCache = function(filePath, atomHome) {
  this.setAtomHomeDirectory(atomHome);
  const extension = path.extname(filePath);

  if (extension === '.cson') {
    if (!CSON) {
      CSON = require('season');
      CSON.setCacheDir(this.getCacheDirectory());
    }
    return CSON.readFileSync(filePath);
  } else {
    const compiler = COMPILERS[extension];
    if (compiler) {
      return 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) {
  const sourceCode = fs.readFileSync(filePath, 'utf8');
  if (compiler.shouldCompile(sourceCode, filePath)) {
    const cachePath = compiler.getCachePath(sourceCode, filePath);
    let compiledCode = readCachedJavaScript(cachePath);
    if (compiledCode != null) {
      cacheStats[extension].hits++;
    } else {
      cacheStats[extension].misses++;
      compiledCode = compiler.compile(sourceCode, filePath);
      writeCachedJavaScript(cachePath, compiledCode);
    }
    return compiledCode;
  }
  return sourceCode;
}

function readCachedJavaScript(relativeCachePath) {
  const cachePath = path.join(cacheDirectory, relativeCachePath);
  if (fs.isFileSync(cachePath)) {
    try {
      return fs.readFileSync(cachePath, 'utf8');
    } catch (error) {}
  }
  return null;
}

function writeCachedJavaScript(relativeCachePath, code) {
  const cachePath = path.join(cacheDirectory, relativeCachePath);
  fs.writeFileSync(cachePath, code, 'utf8');
}

const INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/gm;

exports.install = function(resourcesPath, nodeRequire) {
  const snapshotSourceMapConsumer = {
    originalPositionFor({ line, column }) {
      const { relativePath, row } = snapshotResult.translateSnapshotRow(line);
      return {
        column,
        line: row,
        source: path.join(resourcesPath, 'app', 'static', relativePath),
        name: null
      };
    }
  };

  sourceMapSupport.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 (filePath === '<embedded>') {
        return { map: snapshotSourceMapConsumer };
      }

      if (!cacheDirectory || !fs.isFileSync(filePath)) {
        return null;
      }

      try {
        var sourceCode = fs.readFileSync(filePath, 'utf8');
      } catch (error) {
        console.warn('Error reading source file', error.stack);
        return null;
      }

      let compiler = COMPILERS[path.extname(filePath)];
      if (!compiler) compiler = COMPILERS['.js'];

      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;
      }

      let match, lastMatch;
      INLINE_SOURCE_MAP_REGEXP.lastIndex = 0;
      while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) {
        lastMatch = match;
      }
      if (lastMatch == null) {
        return null;
      }

      const sourceMappingURL = lastMatch[1];
      const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1);

      try {
        var sourceMap = JSON.parse(Buffer.from(rawData, 'base64'));
      } catch (error) {
        console.warn('Error parsing source map', error.stack);
        return null;
      }

      return {
        map: sourceMap,
        url: null
      };
    }
  });

  const prepareStackTraceWithSourceMapping = Error.prepareStackTrace;
  var prepareStackTrace = prepareStackTraceWithSourceMapping;

  function prepareStackTraceWithRawStackAssignment(error, frames) {
    if (error.rawStack) {
      // avoid infinite recursion
      return prepareStackTraceWithSourceMapping(error, frames);
    } else {
      error.rawStack = frames;
      return prepareStackTrace(error, frames);
    }
  }

  Error.stackTraceLimit = 30;

  Object.defineProperty(Error, 'prepareStackTrace', {
    get: function() {
      return prepareStackTraceWithRawStackAssignment;
    },

    set: function(newValue) {
      prepareStackTrace = newValue;
      process.nextTick(function() {
        prepareStackTrace = prepareStackTraceWithSourceMapping;
      });
    }
  });

  // eslint-disable-next-line no-extend-native
  Error.prototype.getRawStack = function() {
    // Access this.stack to ensure prepareStackTrace has been run on this error
    // because it assigns this.rawStack as a side-effect
    this.stack; // eslint-disable-line no-unused-expressions
    return this.rawStack;
  };

  Object.keys(COMPILERS).forEach(function(extension) {
    const compiler = COMPILERS[extension];

    Object.defineProperty(nodeRequire.extensions, extension, {
      enumerable: true,
      writable: false,
      value: function(module, filePath) {
        const code = compileFileAtPath(compiler, filePath, extension);
        return module._compile(code, filePath);
      }
    });
  });
};

exports.supportedExtensions = Object.keys(COMPILERS);
exports.resetCacheStats();