Skip to content

Commit 35c82d4

Browse files
author
Matthew Preble
committed
Improve source map handling when instrumenting transformed code (jestjs#5739)
1 parent 99c6fb1 commit 35c82d4

File tree

3 files changed

+130
-31
lines changed

3 files changed

+130
-31
lines changed

packages/jest-transform/src/ScriptTransformer.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,15 @@ export default class ScriptTransformer {
205205

206206
private _instrumentFile(
207207
filename: Config.Path,
208-
content: string,
208+
input: TransformedSource,
209209
supportsDynamicImport: boolean,
210210
supportsStaticESM: boolean,
211-
): string {
212-
const result = babelTransform(content, {
211+
canMapToInput: boolean,
212+
): TransformedSource {
213+
const inputCode = typeof input === 'string' ? input : input.code;
214+
const inputMap = typeof input === 'string' ? null : input.map;
215+
216+
const result = babelTransform(inputCode, {
213217
auxiliaryCommentBefore: ' istanbul ignore next ',
214218
babelrc: false,
215219
caller: {
@@ -228,21 +232,19 @@ export default class ScriptTransformer {
228232
cwd: this._config.rootDir,
229233
exclude: [],
230234
extension: false,
235+
inputSourceMap: inputMap,
231236
useInlineSourceMaps: false,
232237
},
233238
],
234239
],
240+
sourceMaps: canMapToInput ? 'both' : false,
235241
});
236242

237-
if (result) {
238-
const {code} = result;
239-
240-
if (code) {
241-
return code;
242-
}
243+
if (result && result.code) {
244+
return result as TransformResult;
243245
}
244246

245-
return content;
247+
return input;
246248
}
247249

248250
private _getRealPath(filepath: Config.Path): Config.Path {
@@ -287,18 +289,14 @@ export default class ScriptTransformer {
287289
const transformWillInstrument =
288290
shouldCallTransform && transform && transform.canInstrument;
289291

290-
// If we handle the coverage instrumentation, we should try to map code
291-
// coverage against original source with any provided source map
292-
const mapCoverage = instrument && !transformWillInstrument;
293-
294292
if (code) {
295293
// This is broken: we return the code, and a path for the source map
296294
// directly from the cache. But, nothing ensures the source map actually
297295
// matches that source code. They could have gotten out-of-sync in case
298296
// two separate processes write concurrently to the same cache files.
299297
return {
300298
code,
301-
mapCoverage,
299+
mapCoverage: false,
302300
originalCode: content,
303301
sourceMapPath,
304302
};
@@ -333,9 +331,8 @@ export default class ScriptTransformer {
333331
//Could be a potential freeze here.
334332
//See: https://github.com/facebook/jest/pull/5177#discussion_r158883570
335333
const inlineSourceMap = sourcemapFromSource(transformed.code);
336-
337334
if (inlineSourceMap) {
338-
transformed.map = inlineSourceMap.toJSON();
335+
transformed.map = inlineSourceMap.toObject();
339336
}
340337
} catch (e) {
341338
const transformPath = this._getTransformPath(filename);
@@ -347,22 +344,38 @@ export default class ScriptTransformer {
347344
}
348345
}
349346

347+
// Apply instrumentation to the code if necessary, keeping the instrumented code and new map
348+
let map = transformed.map;
350349
if (!transformWillInstrument && instrument) {
351-
code = this._instrumentFile(
350+
/**
351+
* We can map the original source code to the instrumented code ONLY if
352+
* - the process of transforming the code produced a source map e.g. ts-jest
353+
* - we did not transform the source code
354+
*
355+
* Otherwise we cannot make any statements about how the instrumented code corresponds to the original code,
356+
* and we should NOT emit any source maps
357+
*
358+
*/
359+
const shouldEmitSourceMaps = (!!transform && !!map) || !transform;
360+
361+
const instrumented = this._instrumentFile(
352362
filename,
353-
transformed.code,
363+
transformed,
354364
supportsDynamicImport,
355365
supportsStaticESM,
366+
shouldEmitSourceMaps,
356367
);
368+
369+
code =
370+
typeof instrumented === 'string' ? instrumented : instrumented.code;
371+
map = typeof instrumented === 'string' ? null : instrumented.map;
357372
} else {
358373
code = transformed.code;
359374
}
360375

361-
if (transformed.map) {
376+
if (map) {
362377
const sourceMapContent =
363-
typeof transformed.map === 'string'
364-
? transformed.map
365-
: JSON.stringify(transformed.map);
378+
typeof map === 'string' ? map : JSON.stringify(map);
366379
writeCacheFile(sourceMapPath, sourceMapContent);
367380
} else {
368381
sourceMapPath = null;
@@ -372,7 +385,7 @@ export default class ScriptTransformer {
372385

373386
return {
374387
code,
375-
mapCoverage,
388+
mapCoverage: false,
376389
originalCode: content,
377390
sourceMapPath,
378391
};
@@ -396,7 +409,6 @@ export default class ScriptTransformer {
396409

397410
let code = content;
398411
let sourceMapPath: string | null = null;
399-
let mapCoverage = false;
400412

401413
const willTransform =
402414
!isInternalModule &&
@@ -415,12 +427,11 @@ export default class ScriptTransformer {
415427

416428
code = transformedSource.code;
417429
sourceMapPath = transformedSource.sourceMapPath;
418-
mapCoverage = transformedSource.mapCoverage;
419430
}
420431

421432
return {
422433
code,
423-
mapCoverage,
434+
mapCoverage: false,
424435
originalCode: content,
425436
sourceMapPath,
426437
};

packages/jest-transform/src/__tests__/__snapshots__/script_transformer.test.js.snap

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ exports[`ScriptTransformer transforms a file properly 1`] = `
8080
/* istanbul ignore next */
8181
function cov_25u22311x4() {
8282
var path = "/fruits/banana.js";
83-
var hash = "4be0f6184160be573fc43f7c2a5877c28b7ce249";
83+
var hash = "3f8e915bed83285455a8a16aa04dc0cf5242d755";
8484
var global = new Function("return this")();
8585
var gcv = "__coverage__";
8686
var coverageData = {
@@ -104,8 +104,9 @@ function cov_25u22311x4() {
104104
},
105105
f: {},
106106
b: {},
107+
inputSourceMap: null,
107108
_coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
108-
hash: "4be0f6184160be573fc43f7c2a5877c28b7ce249"
109+
hash: "3f8e915bed83285455a8a16aa04dc0cf5242d755"
109110
};
110111
var coverage = global[gcv] || (global[gcv] = {});
111112
@@ -125,13 +126,14 @@ function cov_25u22311x4() {
125126
cov_25u22311x4();
126127
cov_25u22311x4().s[0]++;
127128
module.exports = "banana";
129+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImJhbmFuYS5qcyJdLCJuYW1lcyI6WyJtb2R1bGUiLCJleHBvcnRzIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQUEsTUFBTSxDQUFDQyxPQUFQLEdBQWlCLFFBQWpCIiwic291cmNlc0NvbnRlbnQiOlsibW9kdWxlLmV4cG9ydHMgPSBcImJhbmFuYVwiOyJdfQ==
128130
`;
129131

130132
exports[`ScriptTransformer transforms a file properly 2`] = `
131133
/* istanbul ignore next */
132134
function cov_23yvu8etmu() {
133135
var path = "/fruits/kiwi.js";
134-
var hash = "7705dd5fcfbc884dcea7062944cfb8cc5d141d1a";
136+
var hash = "8b5afd38d79008f13ebc229b89ef82b12ee9447a";
135137
var global = new Function("return this")();
136138
var gcv = "__coverage__";
137139
var coverageData = {
@@ -193,8 +195,9 @@ function cov_23yvu8etmu() {
193195
"0": 0
194196
},
195197
b: {},
198+
inputSourceMap: null,
196199
_coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
197-
hash: "7705dd5fcfbc884dcea7062944cfb8cc5d141d1a"
200+
hash: "8b5afd38d79008f13ebc229b89ef82b12ee9447a"
198201
};
199202
var coverage = global[gcv] || (global[gcv] = {});
200203
@@ -220,6 +223,7 @@ module.exports = () => {
220223
cov_23yvu8etmu().s[1]++;
221224
return "kiwi";
222225
};
226+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImtpd2kuanMiXSwibmFtZXMiOlsibW9kdWxlIiwiZXhwb3J0cyJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUFBLE1BQU0sQ0FBQ0MsT0FBUCxHQUFpQixNQUFNO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBTSxDQUE3QiIsInNvdXJjZXNDb250ZW50IjpbIm1vZHVsZS5leHBvcnRzID0gKCkgPT4gXCJraXdpXCI7Il19
223227
`;
224228

225229
exports[`ScriptTransformer uses multiple preprocessors 1`] = `

packages/jest-transform/src/__tests__/script_transformer.test.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,90 @@ describe('ScriptTransformer', () => {
535535
expect(writeFileAtomic.sync).toHaveBeenCalledTimes(1);
536536
});
537537

538+
it('should write a source map for the instrumented file when transformed', () => {
539+
const transformerConfig = {
540+
...config,
541+
transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']],
542+
};
543+
const scriptTransformer = new ScriptTransformer(transformerConfig);
544+
545+
const map = {
546+
mappings: ';AAAA',
547+
version: 3,
548+
};
549+
550+
// A map from the original source to the instrumented output
551+
/* eslint-disable sort-keys */
552+
const instrumentedCodeMap = {
553+
version: 3,
554+
sources: ['banana.js'],
555+
names: ['content'],
556+
mappings: ';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAAA,OAAO',
557+
sourcesContent: ['content'],
558+
};
559+
/* eslint-enable */
560+
561+
require('preprocessor-with-sourcemaps').process.mockReturnValue({
562+
code: 'content',
563+
map,
564+
});
565+
566+
const result = scriptTransformer.transform(
567+
'/fruits/banana.js',
568+
makeGlobalConfig({
569+
collectCoverage: true,
570+
}),
571+
);
572+
expect(result.sourceMapPath).toEqual(expect.any(String));
573+
expect(writeFileAtomic.sync).toBeCalledTimes(2);
574+
expect(writeFileAtomic.sync).toBeCalledWith(
575+
result.sourceMapPath,
576+
JSON.stringify(instrumentedCodeMap),
577+
expect.anything(),
578+
);
579+
580+
// Inline source map allows debugging of original source when running instrumented code
581+
expect(result.code).toContain('//# sourceMappingURL');
582+
});
583+
584+
it('should write a source map for the instrumented file when not transformed', () => {
585+
const scriptTransformer = new ScriptTransformer(config);
586+
587+
// A map from the original source to the instrumented output
588+
/* eslint-disable sort-keys */
589+
const instrumentedCodeMap = {
590+
version: 3,
591+
sources: ['banana.js'],
592+
names: ['module', 'exports'],
593+
mappings:
594+
';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAAA,MAAM,CAACC,OAAP,GAAiB,QAAjB',
595+
sourcesContent: ['module.exports = "banana";'],
596+
};
597+
/* eslint-enable */
598+
599+
require('preprocessor-with-sourcemaps').process.mockReturnValue({
600+
code: 'content',
601+
map: null,
602+
});
603+
604+
const result = scriptTransformer.transform(
605+
'/fruits/banana.js',
606+
makeGlobalConfig({
607+
collectCoverage: true,
608+
}),
609+
);
610+
expect(result.sourceMapPath).toEqual(expect.any(String));
611+
expect(writeFileAtomic.sync).toBeCalledTimes(2);
612+
expect(writeFileAtomic.sync).toBeCalledWith(
613+
result.sourceMapPath,
614+
JSON.stringify(instrumentedCodeMap),
615+
expect.anything(),
616+
);
617+
618+
// Inline source map allows debugging of original source when running instrumented code
619+
expect(result.code).toContain('//# sourceMappingURL');
620+
});
621+
538622
it('passes expected transform options to getCacheKey', () => {
539623
config = {...config, transform: [['^.+\\.js$', 'test_preprocessor']]};
540624
const scriptTransformer = new ScriptTransformer(config);

0 commit comments

Comments
 (0)