'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});

var _crypto;

function _load_crypto() {
  return (_crypto = _interopRequireDefault(require('crypto')));
}

var _path;

function _load_path() {
  return (_path = _interopRequireDefault(require('path')));
}

var _vm;

function _load_vm() {
  return (_vm = _interopRequireDefault(require('vm')));
}

var _jestUtil;

function _load_jestUtil() {
  return (_jestUtil = require('jest-util'));
}

var _gracefulFs;

function _load_gracefulFs() {
  return (_gracefulFs = _interopRequireDefault(require('graceful-fs')));
}

var _babelCore;

function _load_babelCore() {
  return (_babelCore = require('babel-core'));
}

var _babelPluginIstanbul;

function _load_babelPluginIstanbul() {
  return (_babelPluginIstanbul = _interopRequireDefault(
    require('babel-plugin-istanbul')
  ));
}

var _convertSourceMap;

function _load_convertSourceMap() {
  return (_convertSourceMap = _interopRequireDefault(
    require('convert-source-map')
  ));
}

var _jestHasteMap;

function _load_jestHasteMap() {
  return (_jestHasteMap = _interopRequireDefault(require('jest-haste-map')));
}

var _fastJsonStableStringify;

function _load_fastJsonStableStringify() {
  return (_fastJsonStableStringify = _interopRequireDefault(
    require('fast-json-stable-stringify')
  ));
}

var _slash;

function _load_slash() {
  return (_slash = _interopRequireDefault(require('slash')));
}

var _package;

function _load_package() {
  return (_package = require('../package.json'));
}

var _should_instrument;

function _load_should_instrument() {
  return (_should_instrument = _interopRequireDefault(
    require('./should_instrument')
  ));
}

var _writeFileAtomic;

function _load_writeFileAtomic() {
  return (_writeFileAtomic = _interopRequireDefault(
    require('write-file-atomic')
  ));
}

var _realpathNative;

function _load_realpathNative() {
  return (_realpathNative = require('realpath-native'));
}

var _helpers;

function _load_helpers() {
  return (_helpers = require('./helpers'));
}

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {default: obj};
}

const cache = new Map();
/**
 * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 *
 */

const configToJsonMap = new Map();
// Cache regular expressions to test whether the file needs to be preprocessed
const ignoreCache = new WeakMap();

// To reset the cache for specific changesets (rather than package version).
const CACHE_VERSION = '1';

class ScriptTransformer {
  constructor(config) {
    this._config = config;
    this._transformCache = new Map();
  }

  _getCacheKey(fileData, filename, instrument) {
    if (!configToJsonMap.has(this._config)) {
      // We only need this set of config options that can likely influence
      // cached output instead of all config options.
      configToJsonMap.set(
        this._config,
        (0,
        (_fastJsonStableStringify || _load_fastJsonStableStringify()).default)(
          this._config
        )
      );
    }
    const configString = configToJsonMap.get(this._config) || '';
    const transformer = this._getTransformer(filename);

    if (transformer && typeof transformer.getCacheKey === 'function') {
      return (_crypto || _load_crypto()).default
        .createHash('md5')
        .update(
          transformer.getCacheKey(fileData, filename, configString, {
            instrument: instrument,
            rootDir: this._config.rootDir
          })
        )
        .update(CACHE_VERSION)
        .digest('hex');
    } else {
      return (_crypto || _load_crypto()).default
        .createHash('md5')
        .update(fileData)
        .update(configString)
        .update(instrument ? 'instrument' : '')
        .update(CACHE_VERSION)
        .digest('hex');
    }
  }

  _getFileCachePath(filename, content, instrument) {
    const baseCacheDir = (
      _jestHasteMap || _load_jestHasteMap()
    ).default.getCacheFilePath(
      this._config.cacheDirectory,
      'jest-transform-cache-' + this._config.name,
      (_package || _load_package()).version
    );
    const cacheKey = this._getCacheKey(content, filename, instrument);
    // Create sub folders based on the cacheKey to avoid creating one
    // directory with many files.
    const cacheDir = (_path || _load_path()).default.join(
      baseCacheDir,
      cacheKey[0] + cacheKey[1]
    );
    const cachePath = (0, (_slash || _load_slash()).default)(
      (_path || _load_path()).default.join(
        cacheDir,
        (_path || _load_path()).default.basename(
          filename,
          (_path || _load_path()).default.extname(filename)
        ) +
          '_' +
          cacheKey
      )
    );
    (0, (_jestUtil || _load_jestUtil()).createDirectory)(cacheDir);

    return cachePath;
  }

  _getTransformPath(filename) {
    for (let i = 0; i < this._config.transform.length; i++) {
      if (new RegExp(this._config.transform[i][0]).test(filename)) {
        return this._config.transform[i][1];
      }
    }
    return null;
  }

  _getTransformer(filename) {
    let transform;
    if (!this._config.transform || !this._config.transform.length) {
      return null;
    }

    const transformPath = this._getTransformPath(filename);
    if (transformPath) {
      const transformer = this._transformCache.get(transformPath);
      if (transformer != null) {
        return transformer;
      }

      // $FlowFixMe
      transform = require(transformPath);
      if (typeof transform.createTransformer === 'function') {
        transform = transform.createTransformer();
      }
      if (typeof transform.process !== 'function') {
        throw new TypeError(
          'Jest: a transform must export a `process` function.'
        );
      }
      this._transformCache.set(transformPath, transform);
    }
    return transform;
  }

  _instrumentFile(filename, content) {
    return (0, (_babelCore || _load_babelCore()).transform)(content, {
      auxiliaryCommentBefore: ' istanbul ignore next ',
      babelrc: false,
      filename: filename,
      plugins: [
        [
          (_babelPluginIstanbul || _load_babelPluginIstanbul()).default,
          {
            compact: false,
            // files outside `cwd` will not be instrumented
            cwd: this._config.rootDir,
            exclude: [],
            useInlineSourceMaps: false
          }
        ]
      ]
    }).code;
  }

  _getRealPath(filepath) {
    try {
      return (
        (0, (_realpathNative || _load_realpathNative()).sync)(filepath) ||
        filepath
      );
    } catch (err) {
      return filepath;
    }
  }

  transformSource(filepath, content, instrument) {
    const filename = this._getRealPath(filepath);
    const transform = this._getTransformer(filename);
    const cacheFilePath = this._getFileCachePath(filename, content, instrument);
    let sourceMapPath = cacheFilePath + '.map';
    // Ignore cache if `config.cache` is set (--no-cache)
    let code = this._config.cache ? readCodeCacheFile(cacheFilePath) : null;

    const shouldCallTransform =
      transform && shouldTransform(filename, this._config);

    // That means that the transform has a custom instrumentation
    // logic and will handle it based on `config.collectCoverage` option
    const transformWillInstrument =
      shouldCallTransform && transform && transform.canInstrument;

    // If we handle the coverage instrumentation, we should try to map code
    // coverage against original source with any provided source map
    const mapCoverage = instrument && !transformWillInstrument;

    if (code) {
      // This is broken: we return the code, and a path for the source map
      // directly from the cache. But, nothing ensures the source map actually
      // matches that source code. They could have gotten out-of-sync in case
      // two separate processes write concurrently to the same cache files.
      return {
        code: code,
        mapCoverage: mapCoverage,
        sourceMapPath: sourceMapPath
      };
    }

    let transformed = {
      code: content,
      map: null
    };

    if (transform && shouldCallTransform) {
      const processed = transform.process(content, filename, this._config, {
        instrument: instrument
      });

      if (typeof processed === 'string') {
        transformed.code = processed;
      } else if (processed != null && typeof processed.code === 'string') {
        transformed = processed;
      } else {
        throw new TypeError(
          "Jest: a transform's `process` function must return a string, " +
            'or an object with `code` key containing this string.'
        );
      }
    }

    if (!transformed.map) {
      //Could be a potential freeze here.
      //See: https://github.com/facebook/jest/pull/5177#discussion_r158883570
      const inlineSourceMap = (
        _convertSourceMap || _load_convertSourceMap()
      ).default.fromSource(transformed.code);
      if (inlineSourceMap) {
        transformed.map = inlineSourceMap.toJSON();
      }
    }

    if (!transformWillInstrument && instrument) {
      code = this._instrumentFile(filename, transformed.code);
    } else {
      code = transformed.code;
    }

    if (transformed.map) {
      const sourceMapContent =
        typeof transformed.map === 'string'
          ? transformed.map
          : JSON.stringify(transformed.map);
      writeCacheFile(sourceMapPath, sourceMapContent);
    } else {
      sourceMapPath = null;
    }

    writeCodeCacheFile(cacheFilePath, code);

    return {
      code: code,
      mapCoverage: mapCoverage,
      sourceMapPath: sourceMapPath
    };
  }

  _transformAndBuildScript(filename, options, instrument, fileSource) {
    const isInternalModule = !!(options && options.isInternalModule);
    const isCoreModule = !!(options && options.isCoreModule);
    const content = stripShebang(
      fileSource ||
        (_gracefulFs || _load_gracefulFs()).default.readFileSync(
          filename,
          'utf8'
        )
    );

    let wrappedCode;
    let sourceMapPath = null;
    let mapCoverage = false;

    const willTransform =
      !isInternalModule &&
      !isCoreModule &&
      (shouldTransform(filename, this._config) || instrument);

    try {
      if (willTransform) {
        const transformedSource = this.transformSource(
          filename,
          content,
          instrument
        );

        wrappedCode = wrap(transformedSource.code);
        sourceMapPath = transformedSource.sourceMapPath;
        mapCoverage = transformedSource.mapCoverage;
      } else {
        wrappedCode = wrap(content);
      }

      return {
        mapCoverage: mapCoverage,
        script: new (_vm || _load_vm()).default.Script(wrappedCode, {
          displayErrors: true,
          filename: isCoreModule ? 'jest-nodejs-core-' + filename : filename
        }),
        sourceMapPath: sourceMapPath
      };
    } catch (e) {
      if (e.codeFrame) {
        e.stack = e.codeFrame;
      }

      if (
        e instanceof SyntaxError &&
        e.message.includes('Unexpected token') &&
        !e.message.includes(' expected')
      ) {
        throw (0, (_helpers || _load_helpers()).enhanceUnexpectedTokenMessage)(
          e
        );
      }

      throw e;
    }
  }

  transform(filename, options, fileSource) {
    let scriptCacheKey = null;
    let instrument = false;
    let result = '';

    if (!options.isCoreModule) {
      instrument = (0,
      (_should_instrument || _load_should_instrument()).default)(
        filename,
        options,
        this._config
      );
      scriptCacheKey = getScriptCacheKey(filename, instrument);
      result = cache.get(scriptCacheKey);
    }

    if (result) {
      return result;
    }

    result = this._transformAndBuildScript(
      filename,
      options,
      instrument,
      fileSource
    );

    if (scriptCacheKey) {
      cache.set(scriptCacheKey, result);
    }

    return result;
  }
}

exports.default = ScriptTransformer;
const removeFile = path => {
  try {
    (_gracefulFs || _load_gracefulFs()).default.unlinkSync(path);
  } catch (e) {}
};

const stripShebang = content => {
  // If the file data starts with a shebang remove it. Leaves the empty line
  // to keep stack trace line numbers correct.
  if (content.startsWith('#!')) {
    return content.replace(/^#!.*/, '');
  } else {
    return content;
  }
};

/**
 * This is like `writeCacheFile` but with an additional sanity checksum. We
 * cannot use the same technique for source maps because we expose source map
 * cache file paths directly to callsites, with the expectation they can read
 * it right away. This is not a great system, because source map cache file
 * could get corrupted, out-of-sync, etc.
 */
function writeCodeCacheFile(cachePath, code) {
  const checksum = (_crypto || _load_crypto()).default
    .createHash('md5')
    .update(code)
    .digest('hex');
  writeCacheFile(cachePath, checksum + '\n' + code);
}

/**
 * Read counterpart of `writeCodeCacheFile`. We verify that the content of the
 * file matches the checksum, in case some kind of corruption happened. This
 * could happen if an older version of `jest-runtime` writes non-atomically to
 * the same cache, for example.
 */
function readCodeCacheFile(cachePath) {
  const content = readCacheFile(cachePath);
  if (content == null) {
    return null;
  }
  const code = content.substr(33);
  const checksum = (_crypto || _load_crypto()).default
    .createHash('md5')
    .update(code)
    .digest('hex');
  if (checksum === content.substr(0, 32)) {
    return code;
  }
  return null;
}

/**
 * Writing to the cache atomically relies on 'rename' being atomic on most
 * file systems. Doing atomic write reduces the risk of corruption by avoiding
 * two processes to write to the same file at the same time. It also reduces
 * the risk of reading a file that's being overwritten at the same time.
 */
const writeCacheFile = (cachePath, fileData) => {
  try {
    (_writeFileAtomic || _load_writeFileAtomic()).default.sync(
      cachePath,
      fileData,
      {encoding: 'utf8'}
    );
  } catch (e) {
    if (cacheWriteErrorSafeToIgnore(e, cachePath)) {
      return;
    }

    e.message =
      'jest: failed to cache transform results in: ' +
      cachePath +
      '\nFailure message: ' +
      e.message;
    removeFile(cachePath);
    throw e;
  }
};

/**
 * On Windows, renames are not atomic, leading to EPERM exceptions when two
 * processes attempt to rename to the same target file at the same time.
 * If the target file exists we can be reasonably sure another process has
 * legitimately won a cache write race and ignore the error.
 */
const cacheWriteErrorSafeToIgnore = (e, cachePath) =>
  process.platform === 'win32' &&
  e.code === 'EPERM' &&
  (_gracefulFs || _load_gracefulFs()).default.existsSync(cachePath);

const readCacheFile = cachePath => {
  if (!(_gracefulFs || _load_gracefulFs()).default.existsSync(cachePath)) {
    return null;
  }

  let fileData;
  try {
    fileData = (_gracefulFs || _load_gracefulFs()).default.readFileSync(
      cachePath,
      'utf8'
    );
  } catch (e) {
    e.message =
      'jest: failed to read cache file: ' +
      cachePath +
      '\nFailure message: ' +
      e.message;
    removeFile(cachePath);
    throw e;
  }

  if (fileData == null) {
    // We must have somehow created the file but failed to write to it,
    // let's delete it and retry.
    removeFile(cachePath);
  }
  return fileData;
};

const getScriptCacheKey = (filename, instrument) => {
  const mtime = (_gracefulFs || _load_gracefulFs()).default.statSync(filename)
    .mtime;
  return filename + '_' + mtime.getTime() + (instrument ? '_instrumented' : '');
};

const shouldTransform = (filename, config) => {
  if (!ignoreCache.has(config)) {
    if (
      !config.transformIgnorePatterns ||
      config.transformIgnorePatterns.length === 0
    ) {
      ignoreCache.set(config, null);
    } else {
      ignoreCache.set(
        config,
        new RegExp(config.transformIgnorePatterns.join('|'))
      );
    }
  }

  const ignoreRegexp = ignoreCache.get(config);
  const isIgnored = ignoreRegexp ? ignoreRegexp.test(filename) : false;
  return !!config.transform && !!config.transform.length && !isIgnored;
};

const wrap = content =>
  '({"' +
  ScriptTransformer.EVAL_RESULT_VARIABLE +
  '":function(module,exports,require,__dirname,__filename,global,jest){' +
  content +
  '\n}});';

ScriptTransformer.EVAL_RESULT_VARIABLE = 'Object.<anonymous>';
