script_transformer.js 16 KB

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