UNPKG

20.7 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', {
4 value: true
5});
6exports.default = void 0;
7
8function _crypto() {
9 const data = require('crypto');
10
11 _crypto = function() {
12 return data;
13 };
14
15 return data;
16}
17
18function path() {
19 const data = _interopRequireWildcard(require('path'));
20
21 path = function() {
22 return data;
23 };
24
25 return data;
26}
27
28function _vm() {
29 const data = require('vm');
30
31 _vm = function() {
32 return data;
33 };
34
35 return data;
36}
37
38function _jestUtil() {
39 const data = require('jest-util');
40
41 _jestUtil = function() {
42 return data;
43 };
44
45 return data;
46}
47
48function fs() {
49 const data = _interopRequireWildcard(require('graceful-fs'));
50
51 fs = function() {
52 return data;
53 };
54
55 return data;
56}
57
58function _core() {
59 const data = require('@babel/core');
60
61 _core = function() {
62 return data;
63 };
64
65 return data;
66}
67
68function _babelPluginIstanbul() {
69 const data = _interopRequireDefault(require('babel-plugin-istanbul'));
70
71 _babelPluginIstanbul = function() {
72 return data;
73 };
74
75 return data;
76}
77
78function _convertSourceMap() {
79 const data = require('convert-source-map');
80
81 _convertSourceMap = function() {
82 return data;
83 };
84
85 return data;
86}
87
88function _jestHasteMap() {
89 const data = _interopRequireDefault(require('jest-haste-map'));
90
91 _jestHasteMap = function() {
92 return data;
93 };
94
95 return data;
96}
97
98function _fastJsonStableStringify() {
99 const data = _interopRequireDefault(require('fast-json-stable-stringify'));
100
101 _fastJsonStableStringify = function() {
102 return data;
103 };
104
105 return data;
106}
107
108function _slash() {
109 const data = _interopRequireDefault(require('slash'));
110
111 _slash = function() {
112 return data;
113 };
114
115 return data;
116}
117
118function _writeFileAtomic() {
119 const data = require('write-file-atomic');
120
121 _writeFileAtomic = function() {
122 return data;
123 };
124
125 return data;
126}
127
128function _realpathNative() {
129 const data = require('realpath-native');
130
131 _realpathNative = function() {
132 return data;
133 };
134
135 return data;
136}
137
138function _pirates() {
139 const data = require('pirates');
140
141 _pirates = function() {
142 return data;
143 };
144
145 return data;
146}
147
148var _shouldInstrument = _interopRequireDefault(require('./shouldInstrument'));
149
150var _enhanceUnexpectedTokenMessage = _interopRequireDefault(
151 require('./enhanceUnexpectedTokenMessage')
152);
153
154function _interopRequireDefault(obj) {
155 return obj && obj.__esModule ? obj : {default: obj};
156}
157
158function _interopRequireWildcard(obj) {
159 if (obj && obj.__esModule) {
160 return obj;
161 } else {
162 var newObj = {};
163 if (obj != null) {
164 for (var key in obj) {
165 if (Object.prototype.hasOwnProperty.call(obj, key)) {
166 var desc =
167 Object.defineProperty && Object.getOwnPropertyDescriptor
168 ? Object.getOwnPropertyDescriptor(obj, key)
169 : {};
170 if (desc.get || desc.set) {
171 Object.defineProperty(newObj, key, desc);
172 } else {
173 newObj[key] = obj[key];
174 }
175 }
176 }
177 }
178 newObj.default = obj;
179 return newObj;
180 }
181}
182
183function _defineProperty(obj, key, value) {
184 if (key in obj) {
185 Object.defineProperty(obj, key, {
186 value: value,
187 enumerable: true,
188 configurable: true,
189 writable: true
190 });
191 } else {
192 obj[key] = value;
193 }
194 return obj;
195}
196
197// Use `require` to avoid TS rootDir
198const {version: VERSION} = require('../package.json'); // This data structure is used to avoid recalculating some data every time that
199// we need to transform a file. Since ScriptTransformer is instantiated for each
200// file we need to keep this object in the local scope of this module.
201
202const projectCaches = new WeakMap(); // To reset the cache for specific changesets (rather than package version).
203
204const CACHE_VERSION = '1';
205
206async function waitForPromiseWithCleanup(promise, cleanup) {
207 try {
208 await promise;
209 } finally {
210 cleanup();
211 }
212}
213
214class ScriptTransformer {
215 constructor(config) {
216 _defineProperty(this, '_cache', void 0);
217
218 _defineProperty(this, '_config', void 0);
219
220 _defineProperty(this, '_transformCache', void 0);
221
222 _defineProperty(this, '_transformConfigCache', void 0);
223
224 this._config = config;
225 this._transformCache = new Map();
226 this._transformConfigCache = new Map();
227 let projectCache = projectCaches.get(config);
228
229 if (!projectCache) {
230 projectCache = {
231 configString: (0, _fastJsonStableStringify().default)(this._config),
232 ignorePatternsRegExp: calcIgnorePatternRegExp(this._config),
233 transformRegExp: calcTransformRegExp(this._config),
234 transformedFiles: new Map()
235 };
236 projectCaches.set(config, projectCache);
237 }
238
239 this._cache = projectCache;
240 }
241
242 _getCacheKey(fileData, filename, instrument) {
243 const configString = this._cache.configString;
244
245 const transformer = this._getTransformer(filename);
246
247 if (transformer && typeof transformer.getCacheKey === 'function') {
248 return (0, _crypto().createHash)('md5')
249 .update(
250 transformer.getCacheKey(fileData, filename, configString, {
251 config: this._config,
252 instrument,
253 rootDir: this._config.rootDir
254 })
255 )
256 .update(CACHE_VERSION)
257 .digest('hex');
258 } else {
259 return (0, _crypto().createHash)('md5')
260 .update(fileData)
261 .update(configString)
262 .update(instrument ? 'instrument' : '')
263 .update(filename)
264 .update(CACHE_VERSION)
265 .digest('hex');
266 }
267 }
268
269 _getFileCachePath(filename, content, instrument) {
270 const baseCacheDir = _jestHasteMap().default.getCacheFilePath(
271 this._config.cacheDirectory,
272 'jest-transform-cache-' + this._config.name,
273 VERSION
274 );
275
276 const cacheKey = this._getCacheKey(content, filename, instrument); // Create sub folders based on the cacheKey to avoid creating one
277 // directory with many files.
278
279 const cacheDir = path().join(baseCacheDir, cacheKey[0] + cacheKey[1]);
280 const cacheFilenamePrefix = path()
281 .basename(filename, path().extname(filename))
282 .replace(/\W/g, '');
283 const cachePath = (0, _slash().default)(
284 path().join(cacheDir, cacheFilenamePrefix + '_' + cacheKey)
285 );
286 (0, _jestUtil().createDirectory)(cacheDir);
287 return cachePath;
288 }
289
290 _getTransformPath(filename) {
291 const transformRegExp = this._cache.transformRegExp;
292
293 if (!transformRegExp) {
294 return undefined;
295 }
296
297 for (let i = 0; i < transformRegExp.length; i++) {
298 if (transformRegExp[i][0].test(filename)) {
299 const transformPath = transformRegExp[i][1];
300
301 this._transformConfigCache.set(transformPath, transformRegExp[i][2]);
302
303 return transformPath;
304 }
305 }
306
307 return undefined;
308 }
309
310 _getTransformer(filename) {
311 let transform = null;
312
313 if (!this._config.transform || !this._config.transform.length) {
314 return null;
315 }
316
317 const transformPath = this._getTransformPath(filename);
318
319 if (transformPath) {
320 const transformer = this._transformCache.get(transformPath);
321
322 if (transformer != null) {
323 return transformer;
324 }
325
326 transform = require(transformPath);
327
328 const transformerConfig = this._transformConfigCache.get(transformPath);
329
330 if (typeof transform.createTransformer === 'function') {
331 transform = transform.createTransformer(transformerConfig);
332 }
333
334 if (typeof transform.process !== 'function') {
335 throw new TypeError(
336 'Jest: a transform must export a `process` function.'
337 );
338 }
339
340 this._transformCache.set(transformPath, transform);
341 }
342
343 return transform;
344 }
345
346 _instrumentFile(filename, content) {
347 const result = (0, _core().transformSync)(content, {
348 auxiliaryCommentBefore: ' istanbul ignore next ',
349 babelrc: false,
350 caller: {
351 name: '@jest/transform',
352 supportsStaticESM: false
353 },
354 configFile: false,
355 filename,
356 plugins: [
357 [
358 _babelPluginIstanbul().default,
359 {
360 compact: false,
361 // files outside `cwd` will not be instrumented
362 cwd: this._config.rootDir,
363 exclude: [],
364 useInlineSourceMaps: false
365 }
366 ]
367 ]
368 });
369
370 if (result) {
371 const {code} = result;
372
373 if (code) {
374 return code;
375 }
376 }
377
378 return content;
379 }
380
381 _getRealPath(filepath) {
382 try {
383 return (0, _realpathNative().sync)(filepath) || filepath;
384 } catch (err) {
385 return filepath;
386 }
387 } // We don't want to expose transformers to the outside - this function is just
388 // to warm up `this._transformCache`
389
390 preloadTransformer(filepath) {
391 this._getTransformer(filepath);
392 }
393
394 transformSource(filepath, content, instrument) {
395 const filename = this._getRealPath(filepath);
396
397 const transform = this._getTransformer(filename);
398
399 const cacheFilePath = this._getFileCachePath(filename, content, instrument);
400
401 let sourceMapPath = cacheFilePath + '.map'; // Ignore cache if `config.cache` is set (--no-cache)
402
403 let code = this._config.cache ? readCodeCacheFile(cacheFilePath) : null;
404 const shouldCallTransform = transform && this.shouldTransform(filename); // That means that the transform has a custom instrumentation
405 // logic and will handle it based on `config.collectCoverage` option
406
407 const transformWillInstrument =
408 shouldCallTransform && transform && transform.canInstrument; // If we handle the coverage instrumentation, we should try to map code
409 // coverage against original source with any provided source map
410
411 const mapCoverage = instrument && !transformWillInstrument;
412
413 if (code) {
414 // This is broken: we return the code, and a path for the source map
415 // directly from the cache. But, nothing ensures the source map actually
416 // matches that source code. They could have gotten out-of-sync in case
417 // two separate processes write concurrently to the same cache files.
418 return {
419 code,
420 mapCoverage,
421 sourceMapPath
422 };
423 }
424
425 let transformed = {
426 code: content,
427 map: null
428 };
429
430 if (transform && shouldCallTransform) {
431 const processed = transform.process(content, filename, this._config, {
432 instrument
433 });
434
435 if (typeof processed === 'string') {
436 transformed.code = processed;
437 } else if (processed != null && typeof processed.code === 'string') {
438 transformed = processed;
439 } else {
440 throw new TypeError(
441 "Jest: a transform's `process` function must return a string, " +
442 'or an object with `code` key containing this string.'
443 );
444 }
445 }
446
447 if (!transformed.map) {
448 //Could be a potential freeze here.
449 //See: https://github.com/facebook/jest/pull/5177#discussion_r158883570
450 const inlineSourceMap = (0, _convertSourceMap().fromSource)(
451 transformed.code
452 );
453
454 if (inlineSourceMap) {
455 transformed.map = inlineSourceMap.toJSON();
456 }
457 }
458
459 if (!transformWillInstrument && instrument) {
460 code = this._instrumentFile(filename, transformed.code);
461 } else {
462 code = transformed.code;
463 }
464
465 if (transformed.map) {
466 const sourceMapContent =
467 typeof transformed.map === 'string'
468 ? transformed.map
469 : JSON.stringify(transformed.map);
470 writeCacheFile(sourceMapPath, sourceMapContent);
471 } else {
472 sourceMapPath = null;
473 }
474
475 writeCodeCacheFile(cacheFilePath, code);
476 return {
477 code,
478 mapCoverage,
479 sourceMapPath
480 };
481 }
482
483 _transformAndBuildScript(filename, options, instrument, fileSource) {
484 const isInternalModule = !!(options && options.isInternalModule);
485 const isCoreModule = !!(options && options.isCoreModule);
486 const content = stripShebang(
487 fileSource || fs().readFileSync(filename, 'utf8')
488 );
489 let wrappedCode;
490 let sourceMapPath = null;
491 let mapCoverage = false;
492 const willTransform =
493 !isInternalModule &&
494 !isCoreModule &&
495 (this.shouldTransform(filename) || instrument);
496
497 try {
498 const extraGlobals = (options && options.extraGlobals) || [];
499
500 if (willTransform) {
501 const transformedSource = this.transformSource(
502 filename,
503 content,
504 instrument
505 );
506 wrappedCode = wrap(transformedSource.code, ...extraGlobals);
507 sourceMapPath = transformedSource.sourceMapPath;
508 mapCoverage = transformedSource.mapCoverage;
509 } else {
510 wrappedCode = wrap(content, ...extraGlobals);
511 }
512
513 return {
514 mapCoverage,
515 script: new (_vm()).Script(wrappedCode, {
516 displayErrors: true,
517 filename: isCoreModule ? 'jest-nodejs-core-' + filename : filename
518 }),
519 sourceMapPath
520 };
521 } catch (e) {
522 if (e.codeFrame) {
523 e.stack = e.message + '\n' + e.codeFrame;
524 }
525
526 if (
527 e instanceof SyntaxError &&
528 e.message.includes('Unexpected token') &&
529 !e.message.includes(' expected')
530 ) {
531 throw (0, _enhanceUnexpectedTokenMessage.default)(e);
532 }
533
534 throw e;
535 }
536 }
537
538 transform(filename, options, fileSource) {
539 let scriptCacheKey = undefined;
540 let instrument = false;
541
542 if (!options.isCoreModule) {
543 instrument = (0, _shouldInstrument.default)(
544 filename,
545 options,
546 this._config
547 );
548 scriptCacheKey = getScriptCacheKey(filename, instrument);
549
550 const result = this._cache.transformedFiles.get(scriptCacheKey);
551
552 if (result) {
553 return result;
554 }
555 }
556
557 const result = this._transformAndBuildScript(
558 filename,
559 options,
560 instrument,
561 fileSource
562 );
563
564 if (scriptCacheKey) {
565 this._cache.transformedFiles.set(scriptCacheKey, result);
566 }
567
568 return result;
569 }
570
571 transformJson(filename, options, fileSource) {
572 const isInternalModule = options.isInternalModule;
573 const isCoreModule = options.isCoreModule;
574 const willTransform =
575 !isInternalModule && !isCoreModule && this.shouldTransform(filename);
576
577 if (willTransform) {
578 const {code: transformedJsonSource} = this.transformSource(
579 filename,
580 fileSource,
581 false
582 );
583 return transformedJsonSource;
584 }
585
586 return fileSource;
587 }
588
589 requireAndTranspileModule(moduleName, callback) {
590 // Load the transformer to avoid a cycle where we need to load a
591 // transformer in order to transform it in the require hooks
592 this.preloadTransformer(moduleName);
593 let transforming = false;
594 const revertHook = (0, _pirates().addHook)(
595 (code, filename) => {
596 try {
597 transforming = true;
598 return this.transformSource(filename, code, false).code || code;
599 } finally {
600 transforming = false;
601 }
602 },
603 {
604 exts: [path().extname(moduleName)],
605 ignoreNodeModules: false,
606 matcher: filename => {
607 if (transforming) {
608 // Don't transform any dependency required by the transformer itself
609 return false;
610 }
611
612 return this.shouldTransform(filename);
613 }
614 }
615 );
616
617 const module = require(moduleName);
618
619 if (!callback) {
620 revertHook();
621 return module;
622 }
623
624 try {
625 const cbResult = callback(module);
626
627 if ((0, _jestUtil().isPromise)(cbResult)) {
628 return waitForPromiseWithCleanup(cbResult, revertHook).then(
629 () => module
630 );
631 }
632 } finally {
633 revertHook();
634 }
635
636 return module;
637 }
638 /**
639 * @deprecated use `this.shouldTransform` instead
640 */
641 // @ts-ignore: Unused and private - remove in Jest 25
642
643 _shouldTransform(filename) {
644 return this.shouldTransform(filename);
645 }
646
647 shouldTransform(filename) {
648 const ignoreRegexp = this._cache.ignorePatternsRegExp;
649 const isIgnored = ignoreRegexp ? ignoreRegexp.test(filename) : false;
650 return (
651 !!this._config.transform && !!this._config.transform.length && !isIgnored
652 );
653 }
654}
655
656exports.default = ScriptTransformer;
657
658_defineProperty(ScriptTransformer, 'EVAL_RESULT_VARIABLE', void 0);
659
660const removeFile = path => {
661 try {
662 fs().unlinkSync(path);
663 } catch (e) {}
664};
665
666const stripShebang = content => {
667 // If the file data starts with a shebang remove it. Leaves the empty line
668 // to keep stack trace line numbers correct.
669 if (content.startsWith('#!')) {
670 return content.replace(/^#!.*/, '');
671 } else {
672 return content;
673 }
674};
675/**
676 * This is like `writeCacheFile` but with an additional sanity checksum. We
677 * cannot use the same technique for source maps because we expose source map
678 * cache file paths directly to callsites, with the expectation they can read
679 * it right away. This is not a great system, because source map cache file
680 * could get corrupted, out-of-sync, etc.
681 */
682
683function writeCodeCacheFile(cachePath, code) {
684 const checksum = (0, _crypto().createHash)('md5')
685 .update(code)
686 .digest('hex');
687 writeCacheFile(cachePath, checksum + '\n' + code);
688}
689/**
690 * Read counterpart of `writeCodeCacheFile`. We verify that the content of the
691 * file matches the checksum, in case some kind of corruption happened. This
692 * could happen if an older version of `jest-runtime` writes non-atomically to
693 * the same cache, for example.
694 */
695
696function readCodeCacheFile(cachePath) {
697 const content = readCacheFile(cachePath);
698
699 if (content == null) {
700 return null;
701 }
702
703 const code = content.substr(33);
704 const checksum = (0, _crypto().createHash)('md5')
705 .update(code)
706 .digest('hex');
707
708 if (checksum === content.substr(0, 32)) {
709 return code;
710 }
711
712 return null;
713}
714/**
715 * Writing to the cache atomically relies on 'rename' being atomic on most
716 * file systems. Doing atomic write reduces the risk of corruption by avoiding
717 * two processes to write to the same file at the same time. It also reduces
718 * the risk of reading a file that's being overwritten at the same time.
719 */
720
721const writeCacheFile = (cachePath, fileData) => {
722 try {
723 (0, _writeFileAtomic().sync)(cachePath, fileData, {
724 encoding: 'utf8'
725 });
726 } catch (e) {
727 if (cacheWriteErrorSafeToIgnore(e, cachePath)) {
728 return;
729 }
730
731 e.message =
732 'jest: failed to cache transform results in: ' +
733 cachePath +
734 '\nFailure message: ' +
735 e.message;
736 removeFile(cachePath);
737 throw e;
738 }
739};
740/**
741 * On Windows, renames are not atomic, leading to EPERM exceptions when two
742 * processes attempt to rename to the same target file at the same time.
743 * If the target file exists we can be reasonably sure another process has
744 * legitimately won a cache write race and ignore the error.
745 */
746
747const cacheWriteErrorSafeToIgnore = (e, cachePath) =>
748 process.platform === 'win32' &&
749 e.code === 'EPERM' &&
750 fs().existsSync(cachePath);
751
752const readCacheFile = cachePath => {
753 if (!fs().existsSync(cachePath)) {
754 return null;
755 }
756
757 let fileData;
758
759 try {
760 fileData = fs().readFileSync(cachePath, 'utf8');
761 } catch (e) {
762 e.message =
763 'jest: failed to read cache file: ' +
764 cachePath +
765 '\nFailure message: ' +
766 e.message;
767 removeFile(cachePath);
768 throw e;
769 }
770
771 if (fileData == null) {
772 // We must have somehow created the file but failed to write to it,
773 // let's delete it and retry.
774 removeFile(cachePath);
775 }
776
777 return fileData;
778};
779
780const getScriptCacheKey = (filename, instrument) => {
781 const mtime = fs().statSync(filename).mtime;
782 return filename + '_' + mtime.getTime() + (instrument ? '_instrumented' : '');
783};
784
785const calcIgnorePatternRegExp = config => {
786 if (
787 !config.transformIgnorePatterns ||
788 config.transformIgnorePatterns.length === 0
789 ) {
790 return undefined;
791 }
792
793 return new RegExp(config.transformIgnorePatterns.join('|'));
794};
795
796const calcTransformRegExp = config => {
797 if (!config.transform.length) {
798 return undefined;
799 }
800
801 const transformRegexp = [];
802
803 for (let i = 0; i < config.transform.length; i++) {
804 transformRegexp.push([
805 new RegExp(config.transform[i][0]),
806 config.transform[i][1],
807 config.transform[i][2]
808 ]);
809 }
810
811 return transformRegexp;
812};
813
814const wrap = (content, ...extras) => {
815 const globals = new Set([
816 'module',
817 'exports',
818 'require',
819 '__dirname',
820 '__filename',
821 'global',
822 'jest',
823 ...extras
824 ]);
825 return (
826 '({"' +
827 ScriptTransformer.EVAL_RESULT_VARIABLE +
828 `":function(${Array.from(globals).join(',')}){` +
829 content +
830 '\n}});'
831 );
832}; // TODO: Can this be added to the static property?
833
834ScriptTransformer.EVAL_RESULT_VARIABLE = 'Object.<anonymous>';