react-spot 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -0
- package/package.json +42 -0
- package/src/core/chain-transformer.ts +89 -0
- package/src/core/fiber-utils.ts +684 -0
- package/src/core/source-location-resolver.test.ts +415 -0
- package/src/core/source-location-resolver.ts +801 -0
- package/src/core/types.ts +79 -0
- package/src/index.ts +26 -0
- package/src/react/ReactSpot.tsx +1058 -0
- package/src/react/components/ui/index.ts +1 -0
- package/src/react/components/ui/popover.tsx +24 -0
- package/src/transformers/formatted-message.ts +159 -0
- package/src/transformers/transformer-rule.ts +386 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
clearCaches,
|
|
4
|
+
configureSourceRoot,
|
|
5
|
+
extractStackFrameInfo,
|
|
6
|
+
fetchSourceFile,
|
|
7
|
+
resolveLocation,
|
|
8
|
+
resolveSourcePath,
|
|
9
|
+
} from './source-location-resolver';
|
|
10
|
+
|
|
11
|
+
// ─── extractStackFrameInfo ───────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
describe('extractStackFrameInfo', () => {
|
|
14
|
+
it('parses about://React/Server/file:/// stack frames correctly', () => {
|
|
15
|
+
const frame =
|
|
16
|
+
'at LandingPage (about://React/Server/file:///Users/testuser/Projects/sample-app/web/.next/server/chunks/ssr/%5Broot-of-the-server%5D__63dfaf64._.js?20:141:295)';
|
|
17
|
+
const info = extractStackFrameInfo(frame);
|
|
18
|
+
|
|
19
|
+
expect(info).toBeDefined();
|
|
20
|
+
expect(info?.functionName).toBe('LandingPage');
|
|
21
|
+
expect(info?.url).toBe(
|
|
22
|
+
'about://React/Server/file:///Users/testuser/Projects/sample-app/web/.next/server/chunks/ssr/%5Broot-of-the-server%5D__63dfaf64._.js?20'
|
|
23
|
+
);
|
|
24
|
+
expect(info?.line).toBe(141);
|
|
25
|
+
expect(info?.column).toBe(295);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('parses regular http stack frames as before', () => {
|
|
29
|
+
const frame =
|
|
30
|
+
'at fakeJSXCallSite (http://localhost:3000/_next/static/chunks/2374f_next_dist_compiled_20dc070b._.js:4353:16)';
|
|
31
|
+
const info = extractStackFrameInfo(frame);
|
|
32
|
+
|
|
33
|
+
expect(info).toBeDefined();
|
|
34
|
+
expect(info?.functionName).toBe('fakeJSXCallSite');
|
|
35
|
+
expect(info?.url).toBe(
|
|
36
|
+
'http://localhost:3000/_next/static/chunks/2374f_next_dist_compiled_20dc070b._.js'
|
|
37
|
+
);
|
|
38
|
+
expect(info?.line).toBe(4353);
|
|
39
|
+
expect(info?.column).toBe(16);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('parses rsc:// stack frames', () => {
|
|
43
|
+
const frame =
|
|
44
|
+
'at ServerPage (rsc://React/Server/file:///Users/testuser/Projects/sample-app/web/.next/server/chunks/ssr/file.js:10:20)';
|
|
45
|
+
const info = extractStackFrameInfo(frame);
|
|
46
|
+
|
|
47
|
+
expect(info).toBeDefined();
|
|
48
|
+
expect(info?.functionName).toBe('ServerPage');
|
|
49
|
+
expect(info?.url).toBe(
|
|
50
|
+
'rsc://React/Server/file:///Users/testuser/Projects/sample-app/web/.next/server/chunks/ssr/file.js'
|
|
51
|
+
);
|
|
52
|
+
expect(info?.line).toBe(10);
|
|
53
|
+
expect(info?.column).toBe(20);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── fetchSourceFile ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe('fetchSourceFile', () => {
|
|
60
|
+
const originalFetch = globalThis.fetch;
|
|
61
|
+
let hadWindow: boolean;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
clearCaches();
|
|
65
|
+
hadWindow = typeof globalThis.window !== 'undefined';
|
|
66
|
+
if (!hadWindow) {
|
|
67
|
+
(globalThis as unknown as Record<string, unknown>).window = {
|
|
68
|
+
location: { origin: 'http://localhost:3000' },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
globalThis.fetch = originalFetch;
|
|
75
|
+
if (!hadWindow) {
|
|
76
|
+
(globalThis as unknown as Record<string, unknown>).window = undefined as unknown as Window &
|
|
77
|
+
typeof globalThis;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('fetches regular HTTP URLs directly', async () => {
|
|
82
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
83
|
+
ok: true,
|
|
84
|
+
text: () => Promise.resolve('// js source'),
|
|
85
|
+
});
|
|
86
|
+
globalThis.fetch = mockFetch;
|
|
87
|
+
|
|
88
|
+
const result = await fetchSourceFile('http://localhost:3000/_next/static/chunks/app.js');
|
|
89
|
+
expect(result.content).toBe('// js source');
|
|
90
|
+
expect(mockFetch.mock.calls[0][0]).toBe('http://localhost:3000/_next/static/chunks/app.js');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('resolves root-relative URLs against window.location.origin', async () => {
|
|
94
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
95
|
+
ok: true,
|
|
96
|
+
text: () => Promise.resolve('// relative source'),
|
|
97
|
+
});
|
|
98
|
+
globalThis.fetch = mockFetch;
|
|
99
|
+
|
|
100
|
+
const result = await fetchSourceFile('/_next/static/chunks/app.js');
|
|
101
|
+
expect(result.content).toBe('// relative source');
|
|
102
|
+
expect(mockFetch.mock.calls[0][0]).toBe('http://localhost:3000/_next/static/chunks/app.js');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rejects RSC URLs (handled by Next.js fast path, not fetchSourceFile)', async () => {
|
|
106
|
+
await expect(
|
|
107
|
+
fetchSourceFile('about://React/Server/file:///Users/me/proj/.next/server/chunks/ssr/page.js')
|
|
108
|
+
).rejects.toThrow(/non-fetchable URL scheme/i);
|
|
109
|
+
|
|
110
|
+
await expect(
|
|
111
|
+
fetchSourceFile('rsc://React/Server/file:///Users/me/proj/.next/server/chunks/ssr/page.js')
|
|
112
|
+
).rejects.toThrow(/non-fetchable URL scheme/i);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('rejects other non-http schemes', async () => {
|
|
116
|
+
await expect(fetchSourceFile('chrome-extension://abcdef/background.js')).rejects.toThrow(
|
|
117
|
+
/non-fetchable URL scheme/i
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ─── resolveSourcePath ──────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
describe('resolveSourcePath', () => {
|
|
125
|
+
afterEach(() => {
|
|
126
|
+
configureSourceRoot(undefined);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('strips file:/// protocol and returns the absolute path directly', () => {
|
|
130
|
+
const result = resolveSourcePath(
|
|
131
|
+
'file:///Users/me/project/src/components/Foo.tsx',
|
|
132
|
+
undefined,
|
|
133
|
+
'http://localhost:3000/_next/static/chunks/app_src_abc123._.js'
|
|
134
|
+
);
|
|
135
|
+
expect(result).toBe('/Users/me/project/src/components/Foo.tsx');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('does not double-prefix file:/// sources with sourceRoot', () => {
|
|
139
|
+
configureSourceRoot('/Users/me/project');
|
|
140
|
+
const result = resolveSourcePath(
|
|
141
|
+
'file:///Users/me/project/src/components/Foo.tsx',
|
|
142
|
+
undefined,
|
|
143
|
+
'http://localhost:3000/_next/static/chunks/app_src_abc123._.js'
|
|
144
|
+
);
|
|
145
|
+
// Should return the path as-is from the file:// URL, not prepend sourceRoot
|
|
146
|
+
expect(result).toBe('/Users/me/project/src/components/Foo.tsx');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('resolves relative sources against the source file URL', () => {
|
|
150
|
+
const result = resolveSourcePath(
|
|
151
|
+
'Foo.tsx',
|
|
152
|
+
undefined,
|
|
153
|
+
'http://localhost:5200/src/scenarios/Foo.tsx'
|
|
154
|
+
);
|
|
155
|
+
expect(result).toBe('/src/scenarios/Foo.tsx');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('handles absolute paths starting with / that contain /src/', () => {
|
|
159
|
+
const result = resolveSourcePath(
|
|
160
|
+
'/Users/me/project/src/components/Foo.tsx',
|
|
161
|
+
undefined,
|
|
162
|
+
'http://localhost:3000/app.js'
|
|
163
|
+
);
|
|
164
|
+
expect(result).toBe('/Users/me/project/src/components/Foo.tsx');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('strips webpack:/// prefix', () => {
|
|
168
|
+
const result = resolveSourcePath(
|
|
169
|
+
'webpack:///src/components/Foo.tsx',
|
|
170
|
+
undefined,
|
|
171
|
+
'http://localhost:3000/app.js'
|
|
172
|
+
);
|
|
173
|
+
expect(result).toBe('/src/components/Foo.tsx');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('prepends sourceRoot to Turbopack virtual /src/ path when sourceRoot is configured', () => {
|
|
177
|
+
// Turbopack source maps emit root-relative paths like /src/components/Foo.tsx.
|
|
178
|
+
// Without this fix the /src/ short-circuit would skip sourceRoot, causing
|
|
179
|
+
// the editor to try opening "/src/components/Foo.tsx" which does not exist.
|
|
180
|
+
configureSourceRoot('/Users/me/project');
|
|
181
|
+
const result = resolveSourcePath(
|
|
182
|
+
'/src/components/sidebar/ProbeHintItem.tsx',
|
|
183
|
+
undefined,
|
|
184
|
+
'http://localhost:3000/_next/static/chunks/app_src_abc123._.js'
|
|
185
|
+
);
|
|
186
|
+
expect(result).toBe('/Users/me/project/src/components/sidebar/ProbeHintItem.tsx');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('does not double-prefix when path already starts with sourceRoot', () => {
|
|
190
|
+
// 真实绝对路径(如来自 file:// 后已剥离协议头)不应再次拼接 sourceRoot
|
|
191
|
+
configureSourceRoot('/Users/me/project');
|
|
192
|
+
const result = resolveSourcePath(
|
|
193
|
+
'/Users/me/project/src/components/Foo.tsx',
|
|
194
|
+
undefined,
|
|
195
|
+
'http://localhost:3000/_next/static/chunks/app_src_abc123._.js'
|
|
196
|
+
);
|
|
197
|
+
expect(result).toBe('/Users/me/project/src/components/Foo.tsx');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ─── resolveLocation — Next.js RSC fast path ───────────────────────────────
|
|
202
|
+
|
|
203
|
+
describe('resolveLocation — Next.js dev server integration', () => {
|
|
204
|
+
const originalFetch = globalThis.fetch;
|
|
205
|
+
let hadWindow: boolean;
|
|
206
|
+
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
clearCaches();
|
|
209
|
+
hadWindow = typeof globalThis.window !== 'undefined';
|
|
210
|
+
if (!hadWindow) {
|
|
211
|
+
(globalThis as unknown as Record<string, unknown>).window = {
|
|
212
|
+
location: { origin: 'http://localhost:3000' },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
afterEach(() => {
|
|
218
|
+
globalThis.fetch = originalFetch;
|
|
219
|
+
if (!hadWindow) {
|
|
220
|
+
(globalThis as unknown as Record<string, unknown>).window = undefined as unknown as Window &
|
|
221
|
+
typeof globalThis;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const nextjsRscStackLine =
|
|
226
|
+
'at LandingPage (about://React/Server/file:///Users/testuser/Projects/sample-app/web/.next/server/chunks/ssr/%5Broot-of-the-server%5D__63dfaf64._.js?20:141:295)';
|
|
227
|
+
|
|
228
|
+
it('resolves via __nextjs_original-stack-frames for Next.js RSC URLs', async () => {
|
|
229
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
230
|
+
ok: true,
|
|
231
|
+
json: () =>
|
|
232
|
+
Promise.resolve([
|
|
233
|
+
{
|
|
234
|
+
status: 'fulfilled',
|
|
235
|
+
value: {
|
|
236
|
+
originalStackFrame: {
|
|
237
|
+
file: '/Users/testuser/Projects/sample-app/web/src/app/page.tsx',
|
|
238
|
+
methodName: 'LandingPage',
|
|
239
|
+
arguments: [],
|
|
240
|
+
line1: 42,
|
|
241
|
+
column1: 10,
|
|
242
|
+
ignored: false,
|
|
243
|
+
},
|
|
244
|
+
originalCodeFrame: ' 42 | export default function LandingPage() {',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
]),
|
|
248
|
+
});
|
|
249
|
+
globalThis.fetch = mockFetch;
|
|
250
|
+
|
|
251
|
+
const result = await resolveLocation(nextjsRscStackLine);
|
|
252
|
+
|
|
253
|
+
expect(result).not.toBeNull();
|
|
254
|
+
expect(result?.source).toBe('/Users/testuser/Projects/sample-app/web/src/app/page.tsx');
|
|
255
|
+
expect(result?.line).toBe(42);
|
|
256
|
+
expect(result?.column).toBe(10);
|
|
257
|
+
expect(result?.name).toBe('LandingPage');
|
|
258
|
+
|
|
259
|
+
// Should have posted to __nextjs_original-stack-frames
|
|
260
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
261
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
262
|
+
expect(url).toBe('http://localhost:3000/__nextjs_original-stack-frames');
|
|
263
|
+
expect(options.method).toBe('POST');
|
|
264
|
+
|
|
265
|
+
const body = JSON.parse(options.body);
|
|
266
|
+
expect(body.isServer).toBe(true);
|
|
267
|
+
expect(body.frames).toHaveLength(1);
|
|
268
|
+
expect(body.frames[0].file).toBe(
|
|
269
|
+
'/Users/testuser/Projects/sample-app/web/.next/server/chunks/ssr/[root-of-the-server]__63dfaf64._.js'
|
|
270
|
+
);
|
|
271
|
+
expect(body.frames[0].line1).toBe(141);
|
|
272
|
+
expect(body.frames[0].column1).toBe(295);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('falls back to __nextjs_source-map when __nextjs_original-stack-frames fails', async () => {
|
|
276
|
+
const mockFetch = vi
|
|
277
|
+
.fn()
|
|
278
|
+
// First call: __nextjs_original-stack-frames returns error
|
|
279
|
+
.mockResolvedValueOnce({ ok: false, status: 500 })
|
|
280
|
+
// Second call: __nextjs_source-map returns a valid source map
|
|
281
|
+
.mockResolvedValueOnce({
|
|
282
|
+
ok: true,
|
|
283
|
+
text: () =>
|
|
284
|
+
Promise.resolve(
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
version: 3,
|
|
287
|
+
file: 'output.js',
|
|
288
|
+
sources: ['file:///Users/testuser/Projects/sample-app/web/src/app/page.tsx'],
|
|
289
|
+
sourcesContent: [
|
|
290
|
+
'export default function LandingPage() {\n return <div>Hello</div>;\n}',
|
|
291
|
+
],
|
|
292
|
+
names: ['LandingPage'],
|
|
293
|
+
mappings:
|
|
294
|
+
';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAAmB',
|
|
295
|
+
})
|
|
296
|
+
),
|
|
297
|
+
});
|
|
298
|
+
globalThis.fetch = mockFetch;
|
|
299
|
+
|
|
300
|
+
await resolveLocation(nextjsRscStackLine);
|
|
301
|
+
|
|
302
|
+
// Should have called both endpoints
|
|
303
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
304
|
+
expect(mockFetch.mock.calls[0][0]).toBe('http://localhost:3000/__nextjs_original-stack-frames');
|
|
305
|
+
expect(mockFetch.mock.calls[1][0]).toContain('/__nextjs_source-map?filename=');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('returns null when both Next.js endpoints fail', async () => {
|
|
309
|
+
const mockFetch = vi
|
|
310
|
+
.fn()
|
|
311
|
+
// __nextjs_original-stack-frames: 404
|
|
312
|
+
.mockResolvedValueOnce({ ok: false, status: 404 })
|
|
313
|
+
// __nextjs_source-map: 404
|
|
314
|
+
.mockResolvedValueOnce({ ok: false, status: 404 });
|
|
315
|
+
globalThis.fetch = mockFetch;
|
|
316
|
+
|
|
317
|
+
const result = await resolveLocation(nextjsRscStackLine);
|
|
318
|
+
|
|
319
|
+
expect(result).toBeNull();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('caches Next.js dev server unavailability (404) and skips on subsequent calls', async () => {
|
|
323
|
+
const mockFetch = vi
|
|
324
|
+
.fn()
|
|
325
|
+
// First attempt: 404 for stack-frames, 404 for source-map
|
|
326
|
+
.mockResolvedValueOnce({ ok: false, status: 404 })
|
|
327
|
+
.mockResolvedValueOnce({ ok: false, status: 404 })
|
|
328
|
+
// Second attempt: should skip stack-frames endpoint entirely
|
|
329
|
+
.mockResolvedValueOnce({ ok: false, status: 404 });
|
|
330
|
+
globalThis.fetch = mockFetch;
|
|
331
|
+
|
|
332
|
+
// First call — probes and gets 404, caches unavailability
|
|
333
|
+
await resolveLocation(nextjsRscStackLine);
|
|
334
|
+
|
|
335
|
+
// Second call — should skip __nextjs_original-stack-frames (cached 404)
|
|
336
|
+
// and go directly to __nextjs_source-map
|
|
337
|
+
await resolveLocation(
|
|
338
|
+
'at Foo (about://React/Server/file:///Users/testuser/Projects/other/.next/server/chunks/ssr/foo.js:1:1)'
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// First call: stack-frames(404) + source-map(404) = 2 fetches
|
|
342
|
+
// Second call: only source-map(404) = 1 fetch (skipped stack-frames)
|
|
343
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
344
|
+
// The third fetch should be __nextjs_source-map (not stack-frames)
|
|
345
|
+
expect(mockFetch.mock.calls[2][0]).toContain('/__nextjs_source-map');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('does not use Next.js fast path for non-Next.js RSC URLs', async () => {
|
|
349
|
+
// RSC URL without /.next/ — not a Next.js project
|
|
350
|
+
const nonNextjsRscLine =
|
|
351
|
+
'at Comp (rsc://React/Server/file:///Users/testuser/Projects/other/build/server/page.js:5:10)';
|
|
352
|
+
|
|
353
|
+
const mockFetch = vi.fn();
|
|
354
|
+
globalThis.fetch = mockFetch;
|
|
355
|
+
|
|
356
|
+
// Non-Next.js RSC URLs skip the Next.js fast path and fall through to the
|
|
357
|
+
// standard path where they're rejected as non-fetchable (rsc:// scheme)
|
|
358
|
+
const result = await resolveLocation(nonNextjsRscLine);
|
|
359
|
+
|
|
360
|
+
expect(result).toBeNull();
|
|
361
|
+
// Should NOT have called any Next.js endpoint — the rsc:// URL is rejected
|
|
362
|
+
// by hasNonFetchableScheme before any fetch occurs
|
|
363
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('does not use Next.js fast path for regular HTTP URLs', async () => {
|
|
367
|
+
const httpStackLine = 'at Component (http://localhost:3000/_next/static/chunks/app.js:100:20)';
|
|
368
|
+
|
|
369
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
370
|
+
ok: true,
|
|
371
|
+
text: () => Promise.resolve('// js code without source map'),
|
|
372
|
+
});
|
|
373
|
+
globalThis.fetch = mockFetch;
|
|
374
|
+
|
|
375
|
+
await resolveLocation(httpStackLine);
|
|
376
|
+
|
|
377
|
+
// Should fetch the JS file directly, not use Next.js endpoints
|
|
378
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
379
|
+
expect(mockFetch.mock.calls[0][0]).toBe('http://localhost:3000/_next/static/chunks/app.js');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('caches the resolved result from Next.js dev server (L1 cache)', async () => {
|
|
383
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
384
|
+
ok: true,
|
|
385
|
+
json: () =>
|
|
386
|
+
Promise.resolve([
|
|
387
|
+
{
|
|
388
|
+
status: 'fulfilled',
|
|
389
|
+
value: {
|
|
390
|
+
originalStackFrame: {
|
|
391
|
+
file: '/Users/testuser/Projects/sample-app/web/src/app/page.tsx',
|
|
392
|
+
methodName: 'LandingPage',
|
|
393
|
+
arguments: [],
|
|
394
|
+
line1: 42,
|
|
395
|
+
column1: 10,
|
|
396
|
+
ignored: false,
|
|
397
|
+
},
|
|
398
|
+
originalCodeFrame: null,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
]),
|
|
402
|
+
});
|
|
403
|
+
globalThis.fetch = mockFetch;
|
|
404
|
+
|
|
405
|
+
// First call — hits the endpoint
|
|
406
|
+
const result1 = await resolveLocation(nextjsRscStackLine);
|
|
407
|
+
expect(result1).not.toBeNull();
|
|
408
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
409
|
+
|
|
410
|
+
// Second call — should use L1 cache, no additional fetch
|
|
411
|
+
const result2 = await resolveLocation(nextjsRscStackLine);
|
|
412
|
+
expect(result2).toEqual(result1);
|
|
413
|
+
expect(mockFetch).toHaveBeenCalledTimes(1); // still 1
|
|
414
|
+
});
|
|
415
|
+
});
|