Skip to content

Commit 4bf8e58

Browse files
fix: ensure that each js file served up by vite dev server has an inline sourcemap (#30606)
* fix: ensure that each js file served up by vite dev server has an inline sourcemap * refactor * refactor with more understanding * add changelog * PR comments and other tweaks * add comments * add comments * Apply suggestions from code review
1 parent 50db03b commit 4bf8e58

File tree

3 files changed

+130
-14
lines changed

3 files changed

+130
-14
lines changed

cli/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ _Released 11/19/2024 (PENDING)_
77

88
- Updated the protocol to be able to flex logic based on project config. Addresses [#30560](https://github.com/cypress-io/cypress/issues/30560).
99

10+
**Bugfixes:**
11+
12+
- Fixed an issue where some JS assets were not properly getting sourcemaps included with the vite dev server if they had a cache busting query parameter in the URL. Fixed some scenarios to ensure that the sourcemaps that were included by the vite dev server were inlined. Addressed in [#30606](https://github.com/cypress-io/cypress/pull/30606).
13+
1014
## 13.15.2
1115

1216
_Released 11/5/2024_

npm/vite-dev-server/src/plugins/sourcemap.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,33 @@ export const CypressSourcemap = (
1515
enforce: 'post',
1616
transform (code, id, options?) {
1717
try {
18-
if (/\.js$/i.test(id) && !/\/\/# sourceMappingURL=/i.test(code)) {
19-
/*
20-
The Vite dev server and plugins automatically generate sourcemaps for most files, but they are
21-
only included in the served files if any transpilation actually occurred (JSX, TS, etc). This
22-
means that files that are "pure" JS won't include sourcemaps, so JS spec files that don't require
23-
transpilation won't get code frames in the runner.
24-
25-
A sourcemap for these files is generated (just not served) which is in effect an "identity"
26-
sourcemap mapping 1-to-1 to the output file. We can grab this and pass it along as a sourcemap
27-
we want Vite to embed into the output, giving Cypress a sourcemap to use for codeFrame lookups.
28-
@see https://rollupjs.org/guide/en/#thisgetcombinedsourcemap
18+
// Remove query parameters from the id. This is necessary because some files
19+
// have a cache buster query parameter (e.g. `?v=12345`)
20+
const queryParameterLessId = id.split('?')[0]
2921

30-
We utilize a 'post' plugin here and manually append the sourcemap content to the code. We *should*
22+
// Check if the file has a JavaScript extension and does not have an inlined sourcemap
23+
if (/\.(js|jsx|ts|tsx|vue|svelte|mjs|cjs)$/i.test(queryParameterLessId) && !/\/\/# sourceMappingURL=data/i.test(code)) {
24+
/*
25+
The Vite dev server and plugins automatically generate sourcemaps for most files, but there are
26+
some files that don't have sourcemaps generated for them or are not inlined. We utilize a 'post' plugin
27+
here and manually append the sourcemap content to the code in these cases. We *should*
3128
be able to pass the sourcemap along using the `map` attribute in the return value, but Babel-based
3229
plugins don't work with this which causes sourcemaps to break for files that go through common
3330
plugins like `@vitejs/plugin-react`. By manually appending the sourcemap at this point in time
3431
Babel-transformed files end up with correct sourcemaps.
3532
*/
3633

3734
const sourcemap = this.getCombinedSourcemap()
38-
39-
code += `\n//# sourceMappingURL=${sourcemap.toUrl()}`
35+
const sourcemapUrl = sourcemap.toUrl()
36+
37+
if (/\/\/# sourceMappingURL=/i.test(code)) {
38+
// If the code already has a sourceMappingURL, it is not an inlined sourcemap
39+
// and we should replace it with the new sourcemap
40+
code = code.replace(/\/\/# sourceMappingURL=(.*)$/m, `//# sourceMappingURL=${sourcemapUrl}`)
41+
} else {
42+
// If the code does not have a sourceMappingURL, we should append the new sourcemap
43+
code += `\n//# sourceMappingURL=${sourcemapUrl}`
44+
}
4045

4146
return {
4247
code,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Plugin } from 'vite-5'
2+
import { ViteDevServerConfig } from '../../src/devServer'
3+
import { Vite } from '../../src/getVite'
4+
import { CypressSourcemap } from '../../src/plugins'
5+
import Chai, { expect } from 'chai'
6+
import SinonChai from 'sinon-chai'
7+
import sinon from 'sinon'
8+
9+
Chai.use(SinonChai)
10+
11+
describe('sourcemap plugin', () => {
12+
['js', 'jsx', 'ts', 'tsx', 'vue', 'svelte', 'mjs', 'cjs'].forEach((ext) => {
13+
it(`should append sourcemap to the code if sourceMappingURL is not present for files with extension ${ext}`, () => {
14+
const code = 'console.log("hello world")'
15+
const id = `test.js`
16+
const options = {} as ViteDevServerConfig
17+
const vite = {} as Vite
18+
const plugin = CypressSourcemap(options, vite) as Plugin & { getCombinedSourcemap: () => { toUrl: () => string } }
19+
20+
plugin.getCombinedSourcemap = () => {
21+
return {
22+
toUrl: () => 'data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==',
23+
}
24+
}
25+
26+
expect(plugin.name).to.equal('cypress:sourcemap')
27+
expect(plugin.enforce).to.equal('post')
28+
29+
if (plugin.transform instanceof Function) {
30+
const result = plugin.transform.call(plugin, code, id)
31+
32+
expect(result.code).to.eq('console.log("hello world")\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==')
33+
} else {
34+
throw new Error('transform is not a function')
35+
}
36+
})
37+
38+
it(`should replace sourceMappingURL with sourcemap and handle query parameters for files with extension ${ext}`, () => {
39+
const code = 'console.log("hello world")\n//# sourceMappingURL=old-url'
40+
const id = `test.js?v=12345`
41+
const options = {} as ViteDevServerConfig
42+
const vite = {} as Vite
43+
const plugin = CypressSourcemap(options, vite) as Plugin & { getCombinedSourcemap: () => { toUrl: () => string } }
44+
45+
plugin.getCombinedSourcemap = () => {
46+
return {
47+
toUrl: () => 'data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==',
48+
}
49+
}
50+
51+
expect(plugin.name).to.equal('cypress:sourcemap')
52+
expect(plugin.enforce).to.equal('post')
53+
54+
if (plugin.transform instanceof Function) {
55+
const result = plugin.transform.call(plugin, code, id)
56+
57+
expect(result.code).to.eq('console.log("hello world")\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==')
58+
} else {
59+
throw new Error('transform is not a function')
60+
}
61+
})
62+
})
63+
64+
it('should not append sourcemap to the code if sourceMappingURL is already present', () => {
65+
const code = 'console.log("hello world")\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozfQ=='
66+
const id = `test.js`
67+
const options = {} as ViteDevServerConfig
68+
const vite = {} as Vite
69+
const plugin = CypressSourcemap(options, vite) as Plugin & { getCombinedSourcemap: () => { toUrl: () => string } }
70+
71+
plugin.getCombinedSourcemap = sinon.stub()
72+
73+
expect(plugin.name).to.equal('cypress:sourcemap')
74+
expect(plugin.enforce).to.equal('post')
75+
76+
if (plugin.transform instanceof Function) {
77+
const result = plugin.transform.call(plugin, code, id)
78+
79+
expect(result).to.be.undefined
80+
expect(plugin.getCombinedSourcemap).not.to.have.been.called
81+
} else {
82+
throw new Error('transform is not a function')
83+
}
84+
})
85+
86+
it('should not append sourcemap to the code if the file is not a js, jsx, ts, tsx, vue, mjs, or cjs file', () => {
87+
const code = 'console.log("hello world")'
88+
const id = `test.css`
89+
const options = {} as ViteDevServerConfig
90+
const vite = {} as Vite
91+
const plugin = CypressSourcemap(options, vite) as Plugin & { getCombinedSourcemap: () => { toUrl: () => string } }
92+
93+
plugin.getCombinedSourcemap = sinon.stub()
94+
95+
expect(plugin.name).to.equal('cypress:sourcemap')
96+
expect(plugin.enforce).to.equal('post')
97+
98+
if (plugin.transform instanceof Function) {
99+
const result = plugin.transform.call(plugin, code, id)
100+
101+
expect(result).to.be.undefined
102+
expect(plugin.getCombinedSourcemap).not.to.have.been.called
103+
} else {
104+
throw new Error('transform is not a function')
105+
}
106+
})
107+
})

0 commit comments

Comments
 (0)