Skip to content

Commit 52df3f7

Browse files
author
Matthew Preble
committed
Produce source maps when instrumenting code (jestjs#5739)
1 parent 8607684 commit 52df3f7

File tree

3 files changed

+143
-27
lines changed

3 files changed

+143
-27
lines changed

packages/jest-transform/src/ScriptTransformer.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,12 @@ export default class ScriptTransformer {
191191
return transform;
192192
}
193193

194-
private _instrumentFile(filename: Config.Path, content: string): string {
195-
const result = babelTransform(content, {
194+
private _instrumentFile(
195+
filename: Config.Path,
196+
input: TransformedSource,
197+
canMapToInput: boolean,
198+
): TransformedSource {
199+
const result = babelTransform(input.code, {
196200
auxiliaryCommentBefore: ' istanbul ignore next ',
197201
babelrc: false,
198202
caller: {
@@ -209,21 +213,28 @@ export default class ScriptTransformer {
209213
// files outside `cwd` will not be instrumented
210214
cwd: this._config.rootDir,
211215
exclude: [],
216+
// Needed for correct coverage as soon as we start storing a source map of the instrumented code
217+
inputSourceMap: input.map,
212218
useInlineSourceMaps: false,
213219
},
214220
],
215221
],
222+
/**
223+
* Necessary to be able to map back to original source from the instrumented code
224+
* the inline map is needed for debugging functionality
225+
* and exposing it as a separate file is needed for e.g. mapping stack traces
226+
* convenient to use 'both' here and avoid extracting the source map
227+
*
228+
* Prior behavior of emitting no map when we can't map back to original source is preserved
229+
*/
230+
sourceMaps: canMapToInput ? 'both' : false,
216231
});
217232

218-
if (result) {
219-
const {code} = result;
220-
221-
if (code) {
222-
return code;
223-
}
233+
if (result && result.code) {
234+
return {code: result.code, map: result.map};
224235
}
225236

226-
return content;
237+
return {code: input.code};
227238
}
228239

229240
private _getRealPath(filepath: Config.Path): Config.Path {
@@ -312,17 +323,36 @@ export default class ScriptTransformer {
312323
}
313324
}
314325

326+
// Apply instrumentation to the code if necessary, keeping the instrumented code and new map
327+
let map = transformed.map;
315328
if (!transformWillInstrument && instrument) {
316-
code = this._instrumentFile(filename, transformed.code);
329+
/**
330+
* We can map the original source code to the instrumented code ONLY if
331+
* - the process of transforming the code produced a source map e.g. ts-jest
332+
* - we did not transform the source code
333+
*
334+
* Otherwise we cannot make any statements about how the instrumented code
335+
* corresponds to the original code, and should NOT emit any source maps
336+
*
337+
*/
338+
const shouldEmitSourceMaps = (!!transform && !!map) || !transform;
339+
const instrumented = this._instrumentFile(
340+
filename,
341+
transformed,
342+
shouldEmitSourceMaps,
343+
);
344+
code = instrumented.code;
345+
346+
if (instrumented.map) {
347+
map = instrumented.map;
348+
}
317349
} else {
318350
code = transformed.code;
319351
}
320352

321-
if (transformed.map) {
353+
if (map) {
322354
const sourceMapContent =
323-
typeof transformed.map === 'string'
324-
? transformed.map
325-
: JSON.stringify(transformed.map);
355+
typeof map === 'string' ? map : JSON.stringify(map);
326356
writeCacheFile(sourceMapPath, sourceMapContent);
327357
} else {
328358
sourceMapPath = null;

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
@@ -78,7 +78,7 @@ exports[`ScriptTransformer transforms a file properly 1`] = `
7878
/* istanbul ignore next */
7979
function cov_25u22311x4() {
8080
var path = "/fruits/banana.js";
81-
var hash = "4be0f6184160be573fc43f7c2a5877c28b7ce249";
81+
var hash = "3f8e915bed83285455a8a16aa04dc0cf5242d755";
8282
var global = new Function("return this")();
8383
var gcv = "__coverage__";
8484
var coverageData = {
@@ -102,8 +102,9 @@ function cov_25u22311x4() {
102102
},
103103
f: {},
104104
b: {},
105+
inputSourceMap: null,
105106
_coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
106-
hash: "4be0f6184160be573fc43f7c2a5877c28b7ce249"
107+
hash: "3f8e915bed83285455a8a16aa04dc0cf5242d755"
107108
};
108109
var coverage = global[gcv] || (global[gcv] = {});
109110
@@ -122,13 +123,14 @@ function cov_25u22311x4() {
122123
123124
cov_25u22311x4().s[0]++;
124125
module.exports = "banana";
126+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImJhbmFuYS5qcyJdLCJuYW1lcyI6WyJtb2R1bGUiLCJleHBvcnRzIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUFBQSxNQUFNLENBQUNDLE9BQVAsR0FBaUIsUUFBakIiLCJzb3VyY2VzQ29udGVudCI6WyJtb2R1bGUuZXhwb3J0cyA9IFwiYmFuYW5hXCI7Il19
125127
`;
126128

127129
exports[`ScriptTransformer transforms a file properly 2`] = `
128130
/* istanbul ignore next */
129131
function cov_23yvu8etmu() {
130132
var path = "/fruits/kiwi.js";
131-
var hash = "7705dd5fcfbc884dcea7062944cfb8cc5d141d1a";
133+
var hash = "8b5afd38d79008f13ebc229b89ef82b12ee9447a";
132134
var global = new Function("return this")();
133135
var gcv = "__coverage__";
134136
var coverageData = {
@@ -190,8 +192,9 @@ function cov_23yvu8etmu() {
190192
"0": 0
191193
},
192194
b: {},
195+
inputSourceMap: null,
193196
_coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
194-
hash: "7705dd5fcfbc884dcea7062944cfb8cc5d141d1a"
197+
hash: "8b5afd38d79008f13ebc229b89ef82b12ee9447a"
195198
};
196199
var coverage = global[gcv] || (global[gcv] = {});
197200
@@ -216,6 +219,7 @@ module.exports = () => {
216219
cov_23yvu8etmu().s[1]++;
217220
return "kiwi";
218221
};
222+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImtpd2kuanMiXSwibmFtZXMiOlsibW9kdWxlIiwiZXhwb3J0cyJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQUEsTUFBTSxDQUFDQyxPQUFQLEdBQWlCLE1BQU07QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFNLENBQTdCIiwic291cmNlc0NvbnRlbnQiOlsibW9kdWxlLmV4cG9ydHMgPSAoKSA9PiBcImtpd2lcIjsiXX0=
219223
`;
220224

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

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

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,7 @@ describe('ScriptTransformer', () => {
400400

401401
const result = scriptTransformer.transform(
402402
'/fruits/banana.js',
403-
makeGlobalConfig({
404-
collectCoverage: true,
405-
}),
403+
makeGlobalConfig(),
406404
);
407405
expect(result.sourceMapPath).toEqual(expect.any(String));
408406
const mapStr = JSON.stringify(map);
@@ -433,9 +431,7 @@ describe('ScriptTransformer', () => {
433431

434432
const result = scriptTransformer.transform(
435433
'/fruits/banana.js',
436-
makeGlobalConfig({
437-
collectCoverage: true,
438-
}),
434+
makeGlobalConfig(),
439435
);
440436
expect(result.sourceMapPath).toEqual(expect.any(String));
441437
expect(writeFileAtomic.sync).toBeCalledTimes(2);
@@ -504,9 +500,7 @@ describe('ScriptTransformer', () => {
504500

505501
const result = scriptTransformer.transform(
506502
'/fruits/banana.js',
507-
makeGlobalConfig({
508-
collectCoverage: true,
509-
}),
503+
makeGlobalConfig(),
510504
);
511505
expect(result.sourceMapPath).toEqual(expect.any(String));
512506
expect(writeFileAtomic.sync).toBeCalledTimes(2);
@@ -541,6 +535,94 @@ describe('ScriptTransformer', () => {
541535
expect(writeFileAtomic.sync).toHaveBeenCalledTimes(1);
542536
});
543537

538+
it('should write a source map for the instrumented file when transformed', () => {
539+
config = {
540+
...config,
541+
transform: [['^.+\\.js$', 'preprocessor-with-sourcemaps']],
542+
};
543+
const scriptTransformer = new ScriptTransformer(config);
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 sort-keys */
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+
{
578+
encoding: 'utf8',
579+
},
580+
);
581+
582+
// Inline source map allows debugging of original source when running instrumented code
583+
expect(result.code).toContain('//# sourceMappingURL');
584+
});
585+
586+
it('should write a source map for the instrumented file when not transformed', () => {
587+
const scriptTransformer = new ScriptTransformer(config);
588+
589+
// A map from the original source to the instrumented output
590+
/* eslint-disable sort-keys */
591+
const instrumentedCodeMap = {
592+
version: 3,
593+
sources: ['banana.js'],
594+
names: ['module', 'exports'],
595+
mappings:
596+
';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAAA,MAAM,CAACC,OAAP,GAAiB,QAAjB',
597+
sourcesContent: ['module.exports = "banana";'],
598+
};
599+
/* eslint-enable sort-keys */
600+
601+
require('preprocessor-with-sourcemaps').process.mockReturnValue({
602+
code: 'content',
603+
map: null,
604+
});
605+
606+
const result = scriptTransformer.transform(
607+
'/fruits/banana.js',
608+
makeGlobalConfig({
609+
collectCoverage: true,
610+
}),
611+
);
612+
expect(result.sourceMapPath).toEqual(expect.any(String));
613+
expect(writeFileAtomic.sync).toBeCalledTimes(2);
614+
expect(writeFileAtomic.sync).toBeCalledWith(
615+
result.sourceMapPath,
616+
JSON.stringify(instrumentedCodeMap),
617+
{
618+
encoding: 'utf8',
619+
},
620+
);
621+
622+
// Inline source map allows debugging of original source when running instrumented code
623+
expect(result.code).toContain('//# sourceMappingURL');
624+
});
625+
544626
it('passes expected transform options to getCacheKey', () => {
545627
config = {...config, transform: [['^.+\\.js$', 'test_preprocessor']]};
546628
const scriptTransformer = new ScriptTransformer(config);

0 commit comments

Comments
 (0)