Skip to content

Commit f8b38e3

Browse files
fix(module-runner): don't crash stack-trace source mapping when globalThis.Buffer is absent (#22945)
Co-authored-by: Cursor <[email protected]>
1 parent c4df6ef commit f8b38e3

3 files changed

Lines changed: 98 additions & 4 deletions

File tree

packages/vite/src/module-runner/sourcemap/interceptor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { OriginalMapping } from '@jridgewell/trace-mapping'
22
import type { ModuleRunner } from '../runner'
3-
import { posixDirname, posixResolve } from '../utils'
3+
import { decodeBase64, posixDirname, posixResolve } from '../utils'
44
import type { EvaluatedModules } from '../evaluatedModules'
55
import { slash } from '../../shared/utils'
66
import { DecodedMap, getOriginalPosition } from './decoder'
@@ -154,7 +154,7 @@ function retrieveSourceMap(source: string) {
154154
if (reSourceMap.test(sourceMappingURL)) {
155155
// Support source map URL as a data url
156156
const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1)
157-
sourceMapData = Buffer.from(rawData, 'base64').toString()
157+
sourceMapData = decodeBase64(rawData)
158158
sourceMappingURL = source
159159
} else {
160160
// Support source map URLs relative to the source URL

packages/vite/src/module-runner/utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import { isWindows } from '../shared/utils'
44
const textDecoder = new TextDecoder()
55

66
export const decodeBase64: (base64: string) => string = (() => {
7-
if (typeof Buffer === 'function' && typeof Buffer.from === 'function') {
8-
return (base64: string) => Buffer.from(base64, 'base64').toString('utf-8')
7+
// Capture the binding at module load: realms may legitimately delete
8+
// `globalThis.Buffer` afterwards (e.g. browser-parity tests), and this
9+
// decoder runs lazily while preparing stack traces
10+
const capturedBuffer = typeof Buffer === 'function' ? Buffer : undefined
11+
if (capturedBuffer && typeof capturedBuffer.from === 'function') {
12+
return (base64: string) =>
13+
capturedBuffer.from(base64, 'base64').toString('utf-8')
914
}
1015

1116
return (base64: string) =>

packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, expect } from 'vitest'
44
import type { ViteDevServer } from '../../..'
55
import type { ModuleRunnerContext } from '../../../../module-runner'
66
import { ESModulesEvaluator } from '../../../../module-runner'
7+
import { SOURCEMAPPING_URL } from '../../../../shared/constants'
78
import {
89
createFixtureEditor,
910
createModuleRunnerTester,
@@ -156,6 +157,29 @@ describe('module runner initialization', async () => {
156157
]
157158
`)
158159
})
160+
161+
it('should not crash when preparing a stack trace while globalThis.Buffer is absent', async ({
162+
runner,
163+
server,
164+
}) => {
165+
const mod = await runner.import('/fixtures/throws-error-method.ts')
166+
const methodError = await getError(() => mod.throwError())
167+
168+
// Realms may legitimately remove `Buffer` after modules were loaded
169+
// (e.g. browser-parity tests). Preparing a stack in that window must not
170+
// depend on the global still existing.
171+
const bufferBackup = globalThis.Buffer
172+
Reflect.deleteProperty(globalThis, 'Buffer')
173+
try {
174+
// `.stack` access lazily invokes `prepareStackTrace`, which decodes the
175+
// inline source map of the runner module
176+
expect(serializeStack(server, methodError)).toBe(
177+
' at Module.throwError (<root>/fixtures/throws-error-method.ts:6:9)',
178+
)
179+
} finally {
180+
globalThis.Buffer = bufferBackup
181+
}
182+
})
159183
})
160184

161185
describe('module runner with node:vm executor', async () => {
@@ -193,3 +217,68 @@ describe('module runner with node:vm executor', async () => {
193217
).not.toThrow()
194218
})
195219
})
220+
221+
describe('module runner interceptor with inline data: source map', async () => {
222+
const syntheticFile = '/virtual-bufferless/inline-map.js'
223+
// Minimal map pointing every generated position at line 1, column 0 of
224+
// "inline-map-original.ts"
225+
const syntheticSourceMap = {
226+
version: 3,
227+
sources: ['inline-map-original.ts'],
228+
names: [],
229+
mappings: 'AAAA',
230+
}
231+
// The comment token is composed from SOURCEMAPPING_URL so that this spec
232+
// file itself never contains it (see shared/constants.ts)
233+
const syntheticFileContent =
234+
`throw new Error('example')\n` +
235+
`//# ${SOURCEMAPPING_URL}=data:application/json;base64,${btoa(
236+
JSON.stringify(syntheticSourceMap),
237+
)}\n`
238+
239+
class Evaluator extends ESModulesEvaluator {
240+
async runInlinedModule(_: ModuleRunnerContext, __: string) {
241+
const initModule = runInThisContext(
242+
'() => { throw new Error("example") }',
243+
{ filename: syntheticFile },
244+
)
245+
246+
initModule()
247+
}
248+
}
249+
250+
const it = await createModuleRunnerTester(
251+
{},
252+
{
253+
sourcemapInterceptor: {
254+
// Serves a file that is not part of the runner module graph and
255+
// carries an inline base64 source map, so mapping goes through
256+
// `retrieveSourceMap` instead of the runner graph
257+
retrieveFile(id) {
258+
if (id === syntheticFile) {
259+
return syntheticFileContent
260+
}
261+
},
262+
},
263+
evaluator: new Evaluator(),
264+
},
265+
)
266+
267+
it('should not crash when decoding an inline source map while globalThis.Buffer is absent', async ({
268+
runner,
269+
}) => {
270+
const error: Error = await runner
271+
.import('/fixtures/a.ts')
272+
.catch((err) => err)
273+
274+
const bufferBackup = globalThis.Buffer
275+
Reflect.deleteProperty(globalThis, 'Buffer')
276+
try {
277+
expect(error.stack).toContain(
278+
'/virtual-bufferless/inline-map-original.ts:1:1',
279+
)
280+
} finally {
281+
globalThis.Buffer = bufferBackup
282+
}
283+
})
284+
})

0 commit comments

Comments
 (0)