script_transformer.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. 'use strict';
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. var _crypto;
  6. function _load_crypto() {
  7. return _crypto = _interopRequireDefault(require('crypto'));
  8. }
  9. var _path;
  10. function _load_path() {
  11. return _path = _interopRequireDefault(require('path'));
  12. }
  13. var _vm;
  14. function _load_vm() {
  15. return _vm = _interopRequireDefault(require('vm'));
  16. }
  17. var _jestUtil;
  18. function _load_jestUtil() {
  19. return _jestUtil = require('jest-util');
  20. }
  21. var _gracefulFs;
  22. function _load_gracefulFs() {
  23. return _gracefulFs = _interopRequireDefault(require('graceful-fs'));
  24. }
  25. var _babelCore;
  26. function _load_babelCore() {
  27. return _babelCore = require('babel-core');
  28. }
  29. var _babelPluginIstanbul;
  30. function _load_babelPluginIstanbul() {
  31. return _babelPluginIstanbul = _interopRequireDefault(require('babel-plugin-istanbul'));
  32. }
  33. var _convertSourceMap;
  34. function _load_convertSourceMap() {
  35. return _convertSourceMap = _interopRequireDefault(require('convert-source-map'));
  36. }
  37. var _jestHasteMap;
  38. function _load_jestHasteMap() {
  39. return _jestHasteMap = _interopRequireDefault(require('jest-haste-map'));
  40. }
  41. var _jsonStableStringify;
  42. function _load_jsonStableStringify() {
  43. return _jsonStableStringify = _interopRequireDefault(require('json-stable-stringify'));
  44. }
  45. var _slash;
  46. function _load_slash() {
  47. return _slash = _interopRequireDefault(require('slash'));
  48. }
  49. var _package;
  50. function _load_package() {
  51. return _package = require('../package.json');
  52. }
  53. var _should_instrument;
  54. function _load_should_instrument() {
  55. return _should_instrument = _interopRequireDefault(require('./should_instrument'));
  56. }
  57. var _writeFileAtomic;
  58. function _load_writeFileAtomic() {
  59. return _writeFileAtomic = _interopRequireDefault(require('write-file-atomic'));
  60. }
  61. var _realpathNative;
  62. function _load_realpathNative() {
  63. return _realpathNative = require('realpath-native');
  64. }
  65. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  66. const cache = new Map(); /**
  67. * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
  68. *
  69. * This source code is licensed under the MIT license found in the
  70. * LICENSE file in the root directory of this source tree.
  71. *
  72. *
  73. */
  74. const configToJsonMap = new Map();
  75. // Cache regular expressions to test whether the file needs to be preprocessed
  76. const ignoreCache = new WeakMap();
  77. // To reset the cache for specific changesets (rather than package version).
  78. const CACHE_VERSION = '1';
  79. class ScriptTransformer {
  80. constructor(config) {
  81. this._config = config;
  82. this._transformCache = new Map();
  83. }
  84. _getCacheKey(fileData, filename, instrument) {
  85. if (!configToJsonMap.has(this._config)) {
  86. // We only need this set of config options that can likely influence
  87. // cached output instead of all config options.
  88. configToJsonMap.set(this._config, (0, (_jsonStableStringify || _load_jsonStableStringify()).default)(this._config));
  89. }
  90. const configString = configToJsonMap.get(this._config) || '';
  91. const transformer = this._getTransformer(filename);
  92. if (transformer && typeof transformer.getCacheKey === 'function') {
  93. return (_crypto || _load_crypto()).default.createHash('md5').update(transformer.getCacheKey(fileData, filename, configString, {
  94. instrument,
  95. rootDir: this._config.rootDir
  96. })).update(CACHE_VERSION).digest('hex');
  97. } else {
  98. return (_crypto || _load_crypto()).default.createHash('md5').update(fileData).update(configString).update(instrument ? 'instrument' : '').update(CACHE_VERSION).digest('hex');
  99. }
  100. }
  101. _getFileCachePath(filename, content, instrument) {
  102. const baseCacheDir = (_jestHasteMap || _load_jestHasteMap()).default.getCacheFilePath(this._config.cacheDirectory, 'jest-transform-cache-' + this._config.name, (_package || _load_package()).version);
  103. const cacheKey = this._getCacheKey(content, filename, instrument);
  104. // Create sub folders based on the cacheKey to avoid creating one
  105. // directory with many files.
  106. const cacheDir = (_path || _load_path()).default.join(baseCacheDir, cacheKey[0] + cacheKey[1]);
  107. 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));
  108. (0, (_jestUtil || _load_jestUtil()).createDirectory)(cacheDir);
  109. return cachePath;
  110. }
  111. _getTransformPath(filename) {
  112. for (let i = 0; i < this._config.transform.length; i++) {
  113. if (new RegExp(this._config.transform[i][0]).test(filename)) {
  114. return this._config.transform[i][1];
  115. }
  116. }
  117. return null;
  118. }
  119. _getTransformer(filename) {
  120. let transform;
  121. if (!this._config.transform || !this._config.transform.length) {
  122. return null;
  123. }
  124. const transformPath = this._getTransformPath(filename);
  125. if (transformPath) {
  126. const transformer = this._transformCache.get(transformPath);
  127. if (transformer != null) {
  128. return transformer;
  129. }
  130. // $FlowFixMe
  131. transform = require(transformPath);
  132. if (typeof transform.process !== 'function') {
  133. throw new TypeError('Jest: a transform must export a `process` function.');
  134. }
  135. if (typeof transform.createTransformer === 'function') {
  136. transform = transform.createTransformer();
  137. }
  138. this._transformCache.set(transformPath, transform);
  139. }
  140. return transform;
  141. }
  142. _instrumentFile(filename, content) {
  143. return (0, (_babelCore || _load_babelCore()).transform)(content, {
  144. auxiliaryCommentBefore: ' istanbul ignore next ',
  145. babelrc: false,
  146. filename,
  147. plugins: [[(_babelPluginIstanbul || _load_babelPluginIstanbul()).default, {
  148. // files outside `cwd` will not be instrumented
  149. cwd: this._config.rootDir,
  150. exclude: [],
  151. useInlineSourceMaps: false
  152. }]],
  153. retainLines: true
  154. }).code;
  155. }
  156. _getRealPath(filepath) {
  157. try {
  158. return (0, (_realpathNative || _load_realpathNative()).sync)(filepath) || filepath;
  159. } catch (err) {
  160. return filepath;
  161. }
  162. }
  163. transformSource(filepath, content, instrument) {
  164. const filename = this._getRealPath(filepath);
  165. const transform = this._getTransformer(filename);
  166. const cacheFilePath = this._getFileCachePath(filename, content, instrument);
  167. let sourceMapPath = cacheFilePath + '.map';
  168. // Ignore cache if `config.cache` is set (--no-cache)
  169. let code = this._config.cache ? readCodeCacheFile(cacheFilePath) : null;
  170. const shouldCallTransform = transform && shouldTransform(filename, this._config);
  171. // That means that the transform has a custom instrumentation
  172. // logic and will handle it based on `config.collectCoverage` option
  173. const transformWillInstrument = shouldCallTransform && transform && transform.canInstrument;
  174. // If we handle the coverage instrumentation, we should try to map code
  175. // coverage against original source with any provided source map
  176. const mapCoverage = instrument && !transformWillInstrument;
  177. if (code) {
  178. // This is broken: we return the code, and a path for the source map
  179. // directly from the cache. But, nothing ensures the source map actually
  180. // matches that source code. They could have gotten out-of-sync in case
  181. // two separate processes write concurrently to the same cache files.
  182. return {
  183. code,
  184. mapCoverage,
  185. sourceMapPath
  186. };
  187. }
  188. let transformed = {
  189. code: content,
  190. map: null
  191. };
  192. if (transform && shouldCallTransform) {
  193. const processed = transform.process(content, filename, this._config, {
  194. instrument,
  195. returnSourceString: false
  196. });
  197. if (typeof processed === 'string') {
  198. transformed.code = processed;
  199. } else if (processed != null && typeof processed.code === 'string') {
  200. transformed = processed;
  201. } else {
  202. throw new TypeError("Jest: a transform's `process` function must return a string, " + 'or an object with `code` key containing this string.');
  203. }
  204. }
  205. if (!transformed.map) {
  206. //Could be a potential freeze here.
  207. //See: https://github.com/facebook/jest/pull/5177#discussion_r158883570
  208. const inlineSourceMap = (_convertSourceMap || _load_convertSourceMap()).default.fromSource(transformed.code);
  209. if (inlineSourceMap) {
  210. transformed.map = inlineSourceMap.toJSON();
  211. }
  212. }
  213. if (!transformWillInstrument && instrument) {
  214. code = this._instrumentFile(filename, transformed.code);
  215. } else {
  216. code = transformed.code;
  217. }
  218. if (transformed.map) {
  219. const sourceMapContent = typeof transformed.map === 'string' ? transformed.map : JSON.stringify(transformed.map);
  220. writeCacheFile(sourceMapPath, sourceMapContent);
  221. } else {
  222. sourceMapPath = null;
  223. }
  224. writeCodeCacheFile(cacheFilePath, code);
  225. return {
  226. code,
  227. mapCoverage,
  228. sourceMapPath
  229. };
  230. }
  231. _transformAndBuildScript(filename, options, instrument, fileSource) {
  232. const isInternalModule = !!(options && options.isInternalModule);
  233. const isCoreModule = !!(options && options.isCoreModule);
  234. const content = stripShebang(fileSource || (_gracefulFs || _load_gracefulFs()).default.readFileSync(filename, 'utf8'));
  235. let wrappedCode;
  236. let sourceMapPath = null;
  237. let mapCoverage = false;
  238. const willTransform = !isInternalModule && !isCoreModule && (shouldTransform(filename, this._config) || instrument);
  239. try {
  240. if (willTransform) {
  241. const transformedSource = this.transformSource(filename, content, instrument);
  242. wrappedCode = wrap(transformedSource.code);
  243. sourceMapPath = transformedSource.sourceMapPath;
  244. mapCoverage = transformedSource.mapCoverage;
  245. } else {
  246. wrappedCode = wrap(content);
  247. }
  248. return {
  249. mapCoverage,
  250. script: new (_vm || _load_vm()).default.Script(wrappedCode, {
  251. displayErrors: true,
  252. filename: isCoreModule ? 'jest-nodejs-core-' + filename : filename
  253. }),
  254. sourceMapPath
  255. };
  256. } catch (e) {
  257. if (e.codeFrame) {
  258. e.stack = e.codeFrame;
  259. }
  260. throw e;
  261. }
  262. }
  263. transform(filename, options, fileSource) {
  264. let scriptCacheKey = null;
  265. let instrument = false;
  266. let result = '';
  267. if (!options.isCoreModule) {
  268. instrument = (0, (_should_instrument || _load_should_instrument()).default)(filename, options, this._config);
  269. scriptCacheKey = getScriptCacheKey(filename, this._config, instrument);
  270. result = cache.get(scriptCacheKey);
  271. }
  272. if (result) {
  273. return result;
  274. }
  275. result = this._transformAndBuildScript(filename, options, instrument, fileSource);
  276. if (scriptCacheKey) {
  277. cache.set(scriptCacheKey, result);
  278. }
  279. return result;
  280. }
  281. }
  282. exports.default = ScriptTransformer;
  283. const removeFile = path => {
  284. try {
  285. (_gracefulFs || _load_gracefulFs()).default.unlinkSync(path);
  286. } catch (e) {}
  287. };
  288. const stripShebang = content => {
  289. // If the file data starts with a shebang remove it. Leaves the empty line
  290. // to keep stack trace line numbers correct.
  291. if (content.startsWith('#!')) {
  292. return content.replace(/^#!.*/, '');
  293. } else {
  294. return content;
  295. }
  296. };
  297. /**
  298. * This is like `writeCacheFile` but with an additional sanity checksum. We
  299. * cannot use the same technique for source maps because we expose source map
  300. * cache file paths directly to callsites, with the expectation they can read
  301. * it right away. This is not a great system, because source map cache file
  302. * could get corrupted, out-of-sync, etc.
  303. */
  304. function writeCodeCacheFile(cachePath, code) {
  305. const checksum = (_crypto || _load_crypto()).default.createHash('md5').update(code).digest('hex');
  306. writeCacheFile(cachePath, checksum + '\n' + code);
  307. }
  308. /**
  309. * Read counterpart of `writeCodeCacheFile`. We verify that the content of the
  310. * file matches the checksum, in case some kind of corruption happened. This
  311. * could happen if an older version of `jest-runtime` writes non-atomically to
  312. * the same cache, for example.
  313. */
  314. function readCodeCacheFile(cachePath) {
  315. const content = readCacheFile(cachePath);
  316. if (content == null) {
  317. return null;
  318. }
  319. const code = content.substr(33);
  320. const checksum = (_crypto || _load_crypto()).default.createHash('md5').update(code).digest('hex');
  321. if (checksum === content.substr(0, 32)) {
  322. return code;
  323. }
  324. return null;
  325. }
  326. /**
  327. * Writing to the cache atomically relies on 'rename' being atomic on most
  328. * file systems. Doing atomic write reduces the risk of corruption by avoiding
  329. * two processes to write to the same file at the same time. It also reduces
  330. * the risk of reading a file that's being overwritten at the same time.
  331. */
  332. const writeCacheFile = (cachePath, fileData) => {
  333. try {
  334. (_writeFileAtomic || _load_writeFileAtomic()).default.sync(cachePath, fileData, { encoding: 'utf8' });
  335. } catch (e) {
  336. if (cacheWriteErrorSafeToIgnore(e, cachePath)) {
  337. return;
  338. }
  339. e.message = 'jest: failed to cache transform results in: ' + cachePath + '\nFailure message: ' + e.message;
  340. removeFile(cachePath);
  341. throw e;
  342. }
  343. };
  344. /**
  345. * On Windows, renames are not atomic, leading to EPERM exceptions when two
  346. * processes attempt to rename to the same target file at the same time.
  347. * If the target file exists we can be reasonably sure another process has
  348. * legitimately won a cache write race and ignore the error.
  349. */
  350. const cacheWriteErrorSafeToIgnore = (e, cachePath) => {
  351. return process.platform === 'win32' && e.code === 'EPERM' && (_gracefulFs || _load_gracefulFs()).default.existsSync(cachePath);
  352. };
  353. const readCacheFile = cachePath => {
  354. if (!(_gracefulFs || _load_gracefulFs()).default.existsSync(cachePath)) {
  355. return null;
  356. }
  357. let fileData;
  358. try {
  359. fileData = (_gracefulFs || _load_gracefulFs()).default.readFileSync(cachePath, 'utf8');
  360. } catch (e) {
  361. e.message = 'jest: failed to read cache file: ' + cachePath + '\nFailure message: ' + e.message;
  362. removeFile(cachePath);
  363. throw e;
  364. }
  365. if (fileData == null) {
  366. // We must have somehow created the file but failed to write to it,
  367. // let's delete it and retry.
  368. removeFile(cachePath);
  369. }
  370. return fileData;
  371. };
  372. const getScriptCacheKey = (filename, config, instrument) => {
  373. const mtime = (_gracefulFs || _load_gracefulFs()).default.statSync(filename).mtime;
  374. return filename + '_' + mtime.getTime() + (instrument ? '_instrumented' : '');
  375. };
  376. const shouldTransform = (filename, config) => {
  377. if (!ignoreCache.has(config)) {
  378. if (!config.transformIgnorePatterns || config.transformIgnorePatterns.length === 0) {
  379. ignoreCache.set(config, null);
  380. } else {
  381. ignoreCache.set(config, new RegExp(config.transformIgnorePatterns.join('|')));
  382. }
  383. }
  384. const ignoreRegexp = ignoreCache.get(config);
  385. const isIgnored = ignoreRegexp ? ignoreRegexp.test(filename) : false;
  386. return !!config.transform && !!config.transform.length && !isIgnored;
  387. };
  388. const wrap = content => '({"' + ScriptTransformer.EVAL_RESULT_VARIABLE + '":function(module,exports,require,__dirname,__filename,global,jest){' + content + '\n}});';
  389. ScriptTransformer.EVAL_RESULT_VARIABLE = 'Object.<anonymous>';