Skip to content

Commit d35097c

Browse files
committed
Extract parseOpenSourceParam and add tests
1 parent 5a01980 commit d35097c

2 files changed

Lines changed: 172 additions & 4 deletions

File tree

‎packages/devtools-vite/src/utils.ts‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ export const handleDevToolsViteRequest = (
1515
if (!source) {
1616
return
1717
}
18-
const parts = source.match(/^(.*?):(\d+):(\d+)$/)
19-
if (!parts) {
18+
19+
const parsed = parseOpenSourceParam(source)
20+
if (!parsed) {
2021
return
2122
}
22-
23-
const [, file, line, column] = parts
23+
const { file, line, column } = parsed
2424

2525
cb({
2626
type: 'open-source',
@@ -54,6 +54,19 @@ export const handleDevToolsViteRequest = (
5454
})
5555
}
5656

57+
export const parseOpenSourceParam = (
58+
source: string,
59+
) => {
60+
// Capture everything up to the last two colon-separated numeric parts as the file.
61+
// This supports filenames that may themselves contain colons.
62+
const parts = source.match(/^(.+):(\d+):(\d+)$/)
63+
64+
if (!parts) return null
65+
66+
const [, file, line, column] = parts
67+
return { file, line, column }
68+
}
69+
5770
/* export const tryReadFile = async (
5871
filePath: string
5972
) => {
@@ -74,3 +87,4 @@ export const tryParseJson = (jsonString: string) => {
7487
return null
7588
}
7689
} */
90+
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { EventEmitter } from 'node:events'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { normalizePath } from 'vite'
4+
import { handleDevToolsViteRequest, parseOpenSourceParam } from '../src/utils'
5+
6+
function createMockReq(url?: string) {
7+
const emitter = new EventEmitter() as unknown as EventEmitter & {
8+
url?: string
9+
on: (event: string, listener: (...args: Array<any>) => void) => any
10+
}
11+
;(emitter as any).url = url
12+
return emitter as any
13+
}
14+
15+
function createMockRes() {
16+
return {
17+
setHeader: vi.fn(),
18+
write: vi.fn(),
19+
end: vi.fn(),
20+
}
21+
}
22+
23+
describe('handleDevToolsViteRequest', () => {
24+
let next: ReturnType<typeof vi.fn>
25+
let cb: ReturnType<typeof vi.fn>
26+
27+
beforeEach(() => {
28+
next = vi.fn()
29+
cb = vi.fn()
30+
})
31+
32+
it('calls next() when url does not include __tsd', () => {
33+
const req = createMockReq('/some/other/path')
34+
const res = createMockRes()
35+
36+
handleDevToolsViteRequest(req, res as any, next as any, cb as any)
37+
38+
expect(next).toHaveBeenCalledTimes(1)
39+
expect(cb).not.toHaveBeenCalled()
40+
expect(res.setHeader).not.toHaveBeenCalled()
41+
expect(res.write).not.toHaveBeenCalled()
42+
expect(res.end).not.toHaveBeenCalled()
43+
})
44+
45+
it('handles __tsd/open-source with valid source and responds/ends', () => {
46+
const file = 'src/file.ts'
47+
const line = '12'
48+
const column = '5'
49+
const url = `/__tsd/open-source?source=${encodeURIComponent(`${file}:${line}:${column}`)}`
50+
51+
const req = createMockReq(url)
52+
const res = createMockRes()
53+
54+
handleDevToolsViteRequest(req, res as any, next as any, cb as any)
55+
56+
// callback payload
57+
expect(cb).toHaveBeenCalledTimes(1)
58+
const payload = cb.mock.calls[0]?.[0]
59+
expect(payload).toMatchObject({
60+
type: 'open-source',
61+
routine: 'open-source',
62+
})
63+
expect(payload.data.line).toBe(line)
64+
expect(payload.data.column).toBe(column)
65+
66+
const expectedSource = normalizePath(`${process.cwd()}/${file}`)
67+
expect(payload.data.source).toBe(expectedSource)
68+
69+
// response behavior
70+
expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html')
71+
expect(res.write).toHaveBeenCalledWith('<script> window.close(); </script>')
72+
expect(res.end).toHaveBeenCalled()
73+
74+
// next() is not called for handled route
75+
expect(next).not.toHaveBeenCalled()
76+
})
77+
78+
it('does nothing for __tsd/open-source when source is missing', () => {
79+
const req = createMockReq('/__tsd/open-source')
80+
const res = createMockRes()
81+
82+
handleDevToolsViteRequest(req, res as any, next as any, cb as any)
83+
84+
expect(cb).not.toHaveBeenCalled()
85+
expect(res.setHeader).not.toHaveBeenCalled()
86+
expect(res.write).not.toHaveBeenCalled()
87+
expect(res.end).not.toHaveBeenCalled()
88+
expect(next).not.toHaveBeenCalled()
89+
})
90+
91+
it('does nothing for __tsd/open-source when source is malformed', () => {
92+
const malformed = encodeURIComponent('src/file.ts:abc:def')
93+
const req = createMockReq(`/__tsd/open-source?source=${malformed}`)
94+
const res = createMockRes()
95+
96+
handleDevToolsViteRequest(req, res as any, next as any, cb as any)
97+
98+
expect(cb).not.toHaveBeenCalled()
99+
expect(res.setHeader).not.toHaveBeenCalled()
100+
expect(res.write).not.toHaveBeenCalled()
101+
expect(res.end).not.toHaveBeenCalled()
102+
expect(next).not.toHaveBeenCalled()
103+
})
104+
105+
it('parses JSON body for other __tsd requests and writes OK', () => {
106+
const req = createMockReq('/__tsd/some-endpoint')
107+
const res = createMockRes()
108+
109+
handleDevToolsViteRequest(req, res as any, next as any, cb as any)
110+
111+
// simulate streaming body
112+
const chunks = [Buffer.from('{"foo":'), Buffer.from('1}')]
113+
;(req).emit('data', chunks[0])
114+
;(req).emit('data', chunks[1])
115+
;(req).emit('end')
116+
117+
expect(cb).toHaveBeenCalledTimes(1)
118+
expect(cb).toHaveBeenCalledWith({ foo: 1 })
119+
expect(res.write).toHaveBeenCalledWith('OK')
120+
121+
// these are not used in this branch
122+
expect(res.setHeader).not.toHaveBeenCalled()
123+
expect(res.end).not.toHaveBeenCalled()
124+
expect(next).not.toHaveBeenCalled()
125+
})
126+
127+
it('swallows JSON parse errors but still writes OK', () => {
128+
const req = createMockReq('/__tsd/another')
129+
const res = createMockRes()
130+
131+
handleDevToolsViteRequest(req, res as any, next as any, cb as any)
132+
133+
;(req).emit('data', Buffer.from('{ invalid json'))
134+
;(req).emit('end')
135+
136+
expect(cb).not.toHaveBeenCalled()
137+
expect(res.write).toHaveBeenCalledWith('OK')
138+
})
139+
})
140+
141+
describe('parseOpenSourceParam', () => {
142+
it('parses simple filename foo.tsx with line/column', () => {
143+
const input = 'foo.tsx:10:20'
144+
const parsed = parseOpenSourceParam(input)
145+
expect(parsed).toEqual({ file: 'foo.tsx', line: '10', column: '20' })
146+
})
147+
148+
it('parses filename containing colon bar:baz.tsx with line/column', () => {
149+
const input = 'bar:baz.tsx:3:7'
150+
const parsed = parseOpenSourceParam(input)
151+
expect(parsed).toEqual({ file: 'bar:baz.tsx', line: '3', column: '7' })
152+
})
153+
})
154+

0 commit comments

Comments
 (0)