Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions packages/vite/src/module-runner/__tests__/evaluatedModules.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest'
import { EvaluatedModules } from '../evaluatedModules'

describe('EvaluatedModules sourcemap extraction', () => {
it('should not crash with InvalidCharacterError when string literals contain sourceMappingURL pattern', () => {
const modules = new EvaluatedModules()

// Create a module with source mapping URL in string literal - this is the exact scenario from the issue
const moduleId = '/test/module.js'
const moduleUrl = 'http://localhost:3000/test/module.js'
const node = modules.ensureModule(moduleId, moduleUrl)

// This is the exact problematic code pattern mentioned in the GitHub issue
node.meta = {
code: `throw lazyDOMException('Invalid character', 'InvalidCharacterError');
^
DOMException [InvalidCharacterError]: Invalid character
at atob (node:buffer:1302:13)
at EvaluatedModules.getModuleSourceMapById (file:///Users/ari/Git/repros/module-runner/node_modules/.pnpm/[email protected]/node_modules/vite/dist/node/module-runner.js:286:59)
at file:///Users/ari/Git/repros/module-runner/index.mjs:24:31

const text = "//# sourceMappingURL=data:application/json;base64,\${encoded}";`,
}

// Before the fix: This would throw InvalidCharacterError: Invalid character
// After the fix: This should not crash and should return null gracefully
expect(() => {
const sourceMap = modules.getModuleSourceMapById(moduleId)
// The result should be null since there's no valid sourcemap, but it shouldn't crash
expect(sourceMap).toBeNull()
}).not.toThrow('Invalid character')
})

it('should correctly extract valid sourcemap when present at end of file', () => {
const modules = new EvaluatedModules()

const moduleId = '/test/valid-module.js'
const moduleUrl = 'http://localhost:3000/test/valid-module.js'
const node = modules.ensureModule(moduleId, moduleUrl)

// Code with string literal AND a valid sourcemap at the end
node.meta = {
code: `function example() {
const text = "//# sourceMappingURL=data:application/json;base64,invalidbase64";
return text;
}

//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC5qcyIsInNvdXJjZXMiOlsidGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9`,
}

const sourceMap = modules.getModuleSourceMapById(moduleId)
expect(sourceMap).toBeTruthy()
expect(sourceMap?.map.version).toBe(3)
expect(sourceMap?.map.file).toBe('test.js')
})

it('should return null if no valid sourcemap is found', () => {
const modules = new EvaluatedModules()

const moduleId = '/test/module2.js'
const moduleUrl = 'http://localhost:3000/test/module2.js'
const node = modules.ensureModule(moduleId, moduleUrl)

node.meta = {
code: `function example() { return "no sourcemap here"; }`,
}

const sourceMap = modules.getModuleSourceMapById(moduleId)
expect(sourceMap).toBeNull()
})

it('should handle invalid base64 gracefully without crashing', () => {
const modules = new EvaluatedModules()

const moduleId = '/test/module3.js'
const moduleUrl = 'http://localhost:3000/test/module3.js'
const node = modules.ensureModule(moduleId, moduleUrl)

node.meta = {
code: `function example() { return "test"; }
//# sourceMappingURL=data:application/json;base64,invalidbase64`,
}

// This should handle the invalid base64 gracefully and not crash
expect(() => {
const sourceMap = modules.getModuleSourceMapById(moduleId)
expect(sourceMap).toBeNull()
}).not.toThrow()
})
})
32 changes: 23 additions & 9 deletions packages/vite/src/module-runner/evaluatedModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import { decodeBase64 } from './utils'
import { DecodedMap } from './sourcemap/decoder'
import type { ResolvedResult } from './types'

const MODULE_RUNNER_SOURCEMAPPING_REGEXP = new RegExp(
`//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`,
)

export class EvaluatedModuleNode {
public importers = new Set<string>()
public imports = new Set<string>()
Expand Down Expand Up @@ -108,12 +104,30 @@ export class EvaluatedModules {
if (!mod) return null
if (mod.map) return mod.map
if (!mod.meta || !('code' in mod.meta)) return null
const mapString = MODULE_RUNNER_SOURCEMAPPING_REGEXP.exec(
mod.meta.code,
)?.[1]

// Find the last occurrence of sourceMappingURL to avoid matching string literals
// Use a more specific regex that expects the sourcemap to be at the end of a line
const regex = new RegExp(
`//# ${SOURCEMAPPING_URL}=data:application/json;base64,([A-Za-z0-9+/=]+)\\s*$`,
'gm',
)
let match
let lastMatch = null
while ((match = regex.exec(mod.meta.code)) !== null) {
lastMatch = match
}

const mapString = lastMatch?.[1]
if (!mapString) return null
mod.map = new DecodedMap(JSON.parse(decodeBase64(mapString)), mod.file)
return mod.map
try {
const decoded = decodeBase64(mapString)
const parsed = JSON.parse(decoded)
mod.map = new DecodedMap(parsed, mod.file)
return mod.map
} catch {
// Invalid base64 or malformed JSON
return null
}
}

public clear(): void {
Expand Down
Loading