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.
@@ -0,0 +1,801 @@
1
+ import { SourceMapConsumer } from '@jridgewell/source-map';
2
+ import * as convertSourceMap from 'convert-source-map';
3
+
4
+ export interface StackFrameInfo {
5
+ url: string;
6
+ line: number;
7
+ column: number;
8
+ functionName?: string;
9
+ }
10
+
11
+ export interface OriginalSourceInfo {
12
+ source: string;
13
+ line: number;
14
+ column: number;
15
+ name?: string;
16
+ }
17
+
18
+ export interface ResolvedSourceInfo extends OriginalSourceInfo {
19
+ sourceContent?: string;
20
+ }
21
+
22
+ interface CachedSourceMapData {
23
+ sourceContent: string;
24
+ sourceMapContent: string;
25
+ }
26
+
27
+ interface CachedResult {
28
+ originalSource: ResolvedSourceInfo;
29
+ }
30
+
31
+ const MAX_RESULT_CACHE_SIZE = 500;
32
+ const MAX_SOURCE_MAP_CACHE_SIZE = 100;
33
+
34
+ /** Sets a value on a Map, evicting the oldest entry if the map exceeds `maxSize`. */
35
+ function boundedSet<K, V>(map: Map<K, V>, key: K, value: V, maxSize: number): void {
36
+ map.delete(key); // re-insert to refresh position (Map preserves insertion order)
37
+ map.set(key, value);
38
+ if (map.size > maxSize) {
39
+ // The first key is the oldest entry
40
+ const oldest = map.keys().next().value;
41
+ if (oldest !== undefined) {
42
+ map.delete(oldest);
43
+ }
44
+ }
45
+ }
46
+
47
+ const sourceMapCache = new Map<string, CachedSourceMapData>();
48
+ const resultCache = new Map<string, CachedResult>();
49
+
50
+ // ─── Source root configuration ──────────────────────────────────────────────
51
+ // Allows converting URL-relative paths (e.g. /src/scenarios/DeepChain.tsx)
52
+ // into absolute filesystem paths that the editor can open.
53
+ //
54
+ // Set via:
55
+ // 1. configureSourceRoot('/absolute/path/to/project') — programmatic
56
+ // 2. window.__SHOW_COMPONENT_SOURCE_ROOT__ = '/abs/path' — global
57
+ // 3. <ReactSpot sourceRoot="/abs/path" /> — prop (calls #1)
58
+ //
59
+ // When unset, resolved paths are URL-relative (e.g. /src/scenarios/Foo.tsx).
60
+
61
+ let _sourceRoot: string | undefined;
62
+
63
+ export function configureSourceRoot(root: string | undefined): void {
64
+ _sourceRoot = root ? root.replace(/\/+$/, '') : undefined;
65
+ }
66
+
67
+ function getSourceRoot(): string | undefined {
68
+ return (
69
+ _sourceRoot ??
70
+ (typeof window !== 'undefined'
71
+ ? ((window as unknown as Record<string, unknown>).__SHOW_COMPONENT_SOURCE_ROOT__ as
72
+ | string
73
+ | undefined)
74
+ : undefined)
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Resolves a potentially-relative source path from a source map against
80
+ * the URL of the file that contained the source map.
81
+ *
82
+ * Example:
83
+ * rawSource = "DeepChain.tsx"
84
+ * sourceUrl = "http://localhost:5200/src/scenarios/DeepChain.tsx"
85
+ * sourceRoot from source map = "" (empty)
86
+ * → resolved = "/src/scenarios/DeepChain.tsx"
87
+ *
88
+ * If a filesystem sourceRoot is configured:
89
+ * → "/Users/me/project/src/scenarios/DeepChain.tsx"
90
+ */
91
+ /** @internal — exported for testing */
92
+ export function resolveSourcePath(
93
+ rawSource: string,
94
+ sourceMapSourceRoot: string | undefined,
95
+ sourceFileUrl: string
96
+ ): string {
97
+ // Strip file:// protocol — Turbopack emits sources like
98
+ // "file:///Users/me/project/src/Foo.tsx" which are already absolute
99
+ // filesystem paths once the scheme is removed.
100
+ if (rawSource.startsWith('file://')) {
101
+ const stripped = rawSource.replace(/^file:\/\//, '');
102
+ // After removing "file://" we have "/Users/me/project/…" (absolute)
103
+ if (stripped.startsWith('/')) {
104
+ return stripped;
105
+ }
106
+ }
107
+
108
+ // 处理以 / 开头的路径,分两种情况:
109
+ // 1. 真实的文件系统绝对路径(如 /Users/me/project/src/Foo.tsx)—— 直接返回
110
+ // 2. Turbopack 虚拟根相对路径(如 /src/Foo.tsx)—— 需要拼接 fsRoot
111
+ // 区分方法:若 fsRoot 已配置且路径已以 fsRoot 开头,说明是真实绝对路径;
112
+ // 否则一律拼接 fsRoot。不配置 fsRoot 时,含 /src/ 的路径假定已足够深而直接返回。
113
+ if (rawSource.startsWith('/') && !rawSource.startsWith('//')) {
114
+ const fsRoot = getSourceRoot();
115
+ if (fsRoot) {
116
+ // 路径已经以项目根开头,是真实绝对路径,避免重复拼接
117
+ if (rawSource.startsWith(fsRoot)) {
118
+ return rawSource;
119
+ }
120
+ // Turbopack 虚拟根相对路径,拼接项目根后才是真实路径
121
+ return fsRoot + rawSource;
122
+ }
123
+ // 未配置 fsRoot:含 /src/ 或 /node_modules/ 的路径通常已足够深,视为绝对路径直接返回
124
+ if (rawSource.includes('/src/') || rawSource.includes('/node_modules/')) {
125
+ return rawSource;
126
+ }
127
+ return rawSource;
128
+ }
129
+
130
+ // Strip webpack:/// or similar protocol prefixes
131
+ let cleaned = rawSource.replace(/^webpack:\/\/\//, '').replace(/^\.\/?/, '');
132
+
133
+ // If the source map provided a sourceRoot, use it
134
+ if (sourceMapSourceRoot && sourceMapSourceRoot !== '/' && sourceMapSourceRoot !== '') {
135
+ const root = sourceMapSourceRoot.replace(/\/+$/, '');
136
+ cleaned = `${root}/${cleaned}`;
137
+ }
138
+
139
+ // If still relative, resolve against the source file URL
140
+ if (!cleaned.startsWith('/') && !cleaned.startsWith('http')) {
141
+ try {
142
+ const base = new URL(sourceFileUrl);
143
+ const dir = base.pathname.substring(0, base.pathname.lastIndexOf('/') + 1);
144
+ cleaned = dir + cleaned;
145
+ // Normalize /../ and /./ sequences
146
+ cleaned = new URL(cleaned, base.origin).pathname;
147
+ } catch {
148
+ // If URL parsing fails, fall through with what we have
149
+ }
150
+ }
151
+
152
+ // If a filesystem source root is configured, convert URL path → absolute path
153
+ const fsRoot = getSourceRoot();
154
+ if (fsRoot && cleaned.startsWith('/')) {
155
+ return fsRoot + cleaned;
156
+ }
157
+
158
+ return cleaned;
159
+ }
160
+
161
+ /**
162
+ * Extracts URL, line, and column from a stack trace frame.
163
+ *
164
+ * Supported formats:
165
+ * - Chrome/V8: `at fn (url:line:col)` or `at url:line:col`
166
+ * - Firefox/Safari: `fn@url:line:col`
167
+ */
168
+ export function extractStackFrameInfo(stackLine: string): StackFrameInfo | null {
169
+ const patterns = [
170
+ /at\s+([^(]+)\s*\((.+):(\d+):(\d+)\)/, // at fn (url:line:col)
171
+ /at\s+(.+):(\d+):(\d+)/, // at url:line:col
172
+ /([^@]+)@(.+):(\d+):(\d+)/, // fn@url:line:col
173
+ ];
174
+
175
+ for (const pattern of patterns) {
176
+ const match = stackLine.match(pattern);
177
+ if (match) {
178
+ if (match.length === 5) {
179
+ return {
180
+ functionName: match[1].trim(),
181
+ url: match[2],
182
+ line: Number.parseInt(match[3], 10),
183
+ column: Number.parseInt(match[4], 10),
184
+ };
185
+ }
186
+ if (match.length === 4) {
187
+ return {
188
+ url: match[1],
189
+ line: Number.parseInt(match[2], 10),
190
+ column: Number.parseInt(match[3], 10),
191
+ };
192
+ }
193
+ }
194
+ }
195
+
196
+ return null;
197
+ }
198
+
199
+ /**
200
+ * Matches React Server Component debug URLs.
201
+ *
202
+ * React / Next.js emits stack frames with special schemes that embed a
203
+ * `file:///` filesystem path. Known variants:
204
+ * - `rsc://React/Server/file:///…` (React Flight / older Turbopack)
205
+ * - `about://React/Server/file:///…` (newer Turbopack builds)
206
+ *
207
+ * This pattern captures everything after the `file://` portion.
208
+ */
209
+ const REACT_SERVER_URL_RE = /^(?:rsc|about):\/\/React\/Server\/file:\/\/(.+?)(\?.*)?$/;
210
+
211
+ /** Returns `true` when the URL uses a known React Server Component debug scheme. */
212
+ function isReactServerUrl(url: string): boolean {
213
+ return url.startsWith('rsc://React/Server/') || url.startsWith('about://React/Server/');
214
+ }
215
+
216
+ /**
217
+ * Returns `true` when the URL is a Next.js-specific RSC URL.
218
+ *
219
+ * Next.js RSC URLs contain `/.next/` in the path (the build output directory),
220
+ * which distinguishes them from RSC URLs potentially produced by other frameworks.
221
+ */
222
+ function isNextjsRscUrl(url: string): boolean {
223
+ return isReactServerUrl(url) && url.includes('/.next/');
224
+ }
225
+
226
+ /**
227
+ * Extracts the absolute filesystem path from an RSC debug URL.
228
+ * URL-decodes the path to handle percent-encoded characters (e.g. %5B → [).
229
+ *
230
+ * Example:
231
+ * about://React/Server/file:///Users/me/proj/.next/server/chunks/ssr/%5Broot%5D__abc._.js?20
232
+ * → /Users/me/proj/.next/server/chunks/ssr/[root]__abc._.js
233
+ */
234
+ function extractFilePathFromRscUrl(rscUrl: string): string | null {
235
+ const match = rscUrl.match(REACT_SERVER_URL_RE);
236
+ if (!match) return null;
237
+ try {
238
+ return decodeURIComponent(match[1]);
239
+ } catch {
240
+ return match[1];
241
+ }
242
+ }
243
+
244
+ // ─── Next.js Dev Server integration ─────────────────────────────────────────
245
+ // Next.js dev server exposes built-in endpoints for source-map resolution:
246
+ // POST /__nextjs_original-stack-frames — full server-side stack frame resolution
247
+ // GET /__nextjs_source-map?filename=… — raw source map for a given file
248
+ //
249
+ // These leverage the bundler's in-memory compilation state (Webpack / Turbopack)
250
+ // and are more reliable than fetching compiled JS to extract source maps client-side.
251
+
252
+ /** Next.js StackFrame shape expected by `__nextjs_original-stack-frames`. */
253
+ interface NextStackFrame {
254
+ file: string | null;
255
+ methodName: string;
256
+ arguments: string[];
257
+ line1: number | null;
258
+ column1: number | null;
259
+ }
260
+
261
+ interface NextOriginalStackFrameResponse {
262
+ originalStackFrame: (NextStackFrame & { ignored: boolean }) | null;
263
+ originalCodeFrame: string | null;
264
+ }
265
+
266
+ type NextOriginalStackFramesResponse = Array<
267
+ | { status: 'fulfilled'; value: NextOriginalStackFrameResponse }
268
+ | { status: 'rejected'; reason: string }
269
+ >;
270
+
271
+ /**
272
+ * Tracks whether the Next.js dev server's stack-frame endpoint is available.
273
+ * `undefined` = not yet probed, `true` = available, `false` = not available.
274
+ */
275
+ let _nextDevServerAvailable: boolean | undefined;
276
+
277
+ /**
278
+ * Resolves a React Server Component stack frame via the Next.js dev server's
279
+ * built-in `POST /__nextjs_original-stack-frames` endpoint.
280
+ *
281
+ * This performs source map resolution server-side with full access to the
282
+ * bundler's compilation state, which is more reliable than client-side
283
+ * resolution for server-rendered files whose compiled JS isn't directly
284
+ * fetchable via HTTP.
285
+ */
286
+ async function resolveViaNextDevServer(
287
+ frameInfo: StackFrameInfo,
288
+ debug?: boolean
289
+ ): Promise<ResolvedSourceInfo | null> {
290
+ if (_nextDevServerAvailable === false) {
291
+ if (debug) console.log('Next.js dev server endpoint previously unavailable, skipping');
292
+ return null;
293
+ }
294
+
295
+ const filePath = extractFilePathFromRscUrl(frameInfo.url);
296
+ if (!filePath) return null;
297
+
298
+ try {
299
+ const response = await fetch(`${window.location.origin}/__nextjs_original-stack-frames`, {
300
+ method: 'POST',
301
+ headers: { 'Content-Type': 'application/json' },
302
+ body: JSON.stringify({
303
+ frames: [
304
+ {
305
+ file: filePath,
306
+ methodName: frameInfo.functionName || '<unknown>',
307
+ arguments: [],
308
+ line1: frameInfo.line,
309
+ column1: frameInfo.column,
310
+ },
311
+ ],
312
+ isServer: true,
313
+ isEdgeServer: false,
314
+ isAppDirectory: true,
315
+ }),
316
+ });
317
+
318
+ if (!response.ok) {
319
+ if (response.status === 404) {
320
+ _nextDevServerAvailable = false;
321
+ if (debug) console.log('Next.js __nextjs_original-stack-frames not found (404), disabling');
322
+ }
323
+ return null;
324
+ }
325
+
326
+ _nextDevServerAvailable = true;
327
+
328
+ const results: NextOriginalStackFramesResponse = await response.json();
329
+ if (!results.length) return null;
330
+
331
+ const first = results[0];
332
+ if (first.status !== 'fulfilled' || !first.value.originalStackFrame) {
333
+ if (debug)
334
+ console.log(
335
+ 'Next.js stack frame resolution rejected:',
336
+ first.status === 'rejected' ? first.reason : 'no original frame'
337
+ );
338
+ return null;
339
+ }
340
+
341
+ const { originalStackFrame } = first.value;
342
+ let sourcePath = originalStackFrame.file || frameInfo.url;
343
+
344
+ // Next.js returns project-relative paths (e.g. "src/app/page.tsx").
345
+ // Convert to absolute using the configured source root so the editor
346
+ // can open the file directly.
347
+ if (
348
+ sourcePath &&
349
+ !sourcePath.startsWith('/') &&
350
+ !sourcePath.startsWith('file://') &&
351
+ !sourcePath.includes('://')
352
+ ) {
353
+ const fsRoot = getSourceRoot();
354
+ if (debug) {
355
+ console.log('Source path is relative, resolving with sourceRoot:', {
356
+ sourcePath,
357
+ fsRoot,
358
+ });
359
+ }
360
+ if (fsRoot) {
361
+ sourcePath = `${fsRoot}/${sourcePath}`;
362
+ }
363
+ }
364
+
365
+ if (debug) console.log('Final resolved source path:', sourcePath);
366
+
367
+ return {
368
+ source: sourcePath,
369
+ line: originalStackFrame.line1 ?? frameInfo.line,
370
+ column: originalStackFrame.column1 ?? frameInfo.column,
371
+ name: originalStackFrame.methodName || undefined,
372
+ };
373
+ } catch (error) {
374
+ if (debug) console.warn('Next.js dev server resolution failed:', error);
375
+ return null;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Fetches a raw source map from the Next.js dev server's
381
+ * `GET /__nextjs_source-map?filename=…` endpoint.
382
+ *
383
+ * Returns the source map JSON string, or `null` if unavailable.
384
+ */
385
+ async function fetchSourceMapFromNextDevServer(
386
+ filePath: string,
387
+ debug?: boolean
388
+ ): Promise<string | null> {
389
+ try {
390
+ const url = `${window.location.origin}/__nextjs_source-map?filename=${encodeURIComponent(filePath)}`;
391
+ if (debug) console.log('Fetching source map from Next.js dev server:', url);
392
+
393
+ const response = await fetch(url);
394
+ if (!response.ok) {
395
+ if (debug) console.log('__nextjs_source-map returned:', response.status);
396
+ return null;
397
+ }
398
+
399
+ return await response.text();
400
+ } catch (error) {
401
+ if (debug) console.warn('Failed to fetch source map from Next.js dev server:', error);
402
+ return null;
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Returns `true` when the URL uses a scheme that we cannot fetch
408
+ * (e.g. `chrome-extension://`, `blob:`, `data:`, `rsc://`, `about://`, etc.).
409
+ *
410
+ * Allows: `http(s)://` and relative URLs (no scheme).
411
+ *
412
+ * Note: React Server Component URLs (`rsc://`, `about://React/Server/…`) are
413
+ * handled by the Next.js dev server fast path in {@link resolveLocation} and
414
+ * are intentionally *not* fetchable through this path.
415
+ */
416
+ function hasNonFetchableScheme(url: string): boolean {
417
+ // Relative URLs and http(s) are fine
418
+ if (url.startsWith('/') || url.startsWith('http://') || url.startsWith('https://')) return false;
419
+ // Anything else with a "scheme://" prefix is non-fetchable
420
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(url);
421
+ }
422
+
423
+ /**
424
+ * Fetches a source file over HTTP.
425
+ *
426
+ * Accepts absolute `http(s)://` URLs and root-relative paths (resolved against
427
+ * `window.location.origin`). Non-HTTP schemes (including RSC debug URLs) are
428
+ * rejected — RSC resolution is handled by the Next.js fast path in
429
+ * {@link resolveLocation}.
430
+ */
431
+ export async function fetchSourceFile(
432
+ url: string
433
+ ): Promise<{ content: string; effectiveUrl: string }> {
434
+ if (hasNonFetchableScheme(url)) {
435
+ throw new Error(`Non-fetchable URL scheme: ${url}`);
436
+ }
437
+
438
+ const fetchUrl = url.startsWith('http') ? url : `${window.location.origin}${url}`;
439
+
440
+ const response = await fetch(fetchUrl);
441
+ if (!response.ok) {
442
+ throw new Error(`Failed to fetch source file: ${response.status} ${response.statusText}`);
443
+ }
444
+
445
+ const content = await response.text();
446
+ return { content, effectiveUrl: fetchUrl };
447
+ }
448
+
449
+ /**
450
+ * Returns `"inline"` for data-URL source maps, the external URL string
451
+ * for `//# sourceMappingURL=…` comments, or `null` when absent.
452
+ *
453
+ * convert-source-map handles inline (data-URL) maps; for external maps
454
+ * we scan the last 10 lines manually because the library doesn't expose
455
+ * the raw URL.
456
+ */
457
+ function extractSourceMapUrl(sourceContent: string): string | null {
458
+ // 1. Check for inline (data-URL) source maps first
459
+ const converter = convertSourceMap.fromSource(sourceContent);
460
+ if (converter) {
461
+ const sourceMapObj = converter.toObject();
462
+ if (sourceMapObj && Object.keys(sourceMapObj).length > 0) {
463
+ return 'inline';
464
+ }
465
+ }
466
+
467
+ // 2. Fall through to external //# sourceMappingURL=… scan.
468
+ // This is the common case for Turbopack / webpack / esbuild
469
+ // production builds that emit a separate .map file.
470
+ const lines = sourceContent.split('\n');
471
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 10); i--) {
472
+ const match = lines[i].trim().match(/^\/\/[@#]\s*sourceMappingURL=(.+)$/);
473
+ if (match) {
474
+ return match[1].trim();
475
+ }
476
+ }
477
+
478
+ return null;
479
+ }
480
+
481
+ /** Resolves source map content — inline (data URL) or external (fetched). */
482
+ export async function resolveSourceMap(
483
+ sourceContent: string,
484
+ sourceUrl: string
485
+ ): Promise<string | null> {
486
+ const sourceMapUrl = extractSourceMapUrl(sourceContent);
487
+ if (!sourceMapUrl) {
488
+ return null;
489
+ }
490
+
491
+ if (sourceMapUrl === 'inline') {
492
+ const converter = convertSourceMap.fromSource(sourceContent);
493
+ return converter ? converter.toJSON() : null;
494
+ }
495
+
496
+ // Data URL fallback for edge cases not handled by convert-source-map
497
+ if (sourceMapUrl.startsWith('data:')) {
498
+ const base64 = sourceMapUrl.match(/^data:application\/json;(?:charset=utf-8;)?base64,(.+)$/);
499
+ if (base64) {
500
+ return atob(base64[1]);
501
+ }
502
+ const plainJson = sourceMapUrl.match(/^data:application\/json;charset=utf-8,(.+)$/);
503
+ if (plainJson) {
504
+ return decodeURIComponent(plainJson[1]);
505
+ }
506
+ return null;
507
+ }
508
+
509
+ // External source map — resolve to absolute URL
510
+ let absoluteSourceMapUrl: string;
511
+ if (sourceMapUrl.startsWith('http')) {
512
+ absoluteSourceMapUrl = sourceMapUrl;
513
+ } else if (sourceMapUrl.startsWith('/')) {
514
+ const baseUrl = new URL(sourceUrl);
515
+ absoluteSourceMapUrl = `${baseUrl.protocol}//${baseUrl.host}${sourceMapUrl}`;
516
+ } else {
517
+ const baseUrl = new URL(sourceUrl);
518
+ const basePath = baseUrl.pathname.substring(0, baseUrl.pathname.lastIndexOf('/') + 1);
519
+ absoluteSourceMapUrl = `${baseUrl.protocol}//${baseUrl.host}${basePath}${sourceMapUrl}`;
520
+ }
521
+
522
+ try {
523
+ const response = await fetch(absoluteSourceMapUrl);
524
+ if (!response.ok) {
525
+ throw new Error(`Failed to fetch source map: ${response.status} ${response.statusText}`);
526
+ }
527
+ return await response.text();
528
+ } catch (error) {
529
+ console.warn(`Failed to fetch source map from ${absoluteSourceMapUrl}:`, error);
530
+ return null;
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Maps a generated position to the original source via source map.
536
+ * Returns the raw source path as-is — the caller resolves it (see `resolveSourcePath`).
537
+ */
538
+ export async function mapToOriginalSource(
539
+ frameInfo: StackFrameInfo,
540
+ sourceMapContent: string
541
+ ): Promise<{ info: OriginalSourceInfo; sourceRoot?: string } | null> {
542
+ try {
543
+ const sourceMap = JSON.parse(sourceMapContent);
544
+
545
+ const consumer = new SourceMapConsumer(sourceMap);
546
+
547
+ const originalPosition = consumer.originalPositionFor({
548
+ line: frameInfo.line,
549
+ column: frameInfo.column,
550
+ });
551
+
552
+ if (
553
+ originalPosition.source &&
554
+ originalPosition.line !== null &&
555
+ originalPosition.column !== null
556
+ ) {
557
+ return {
558
+ info: {
559
+ source: originalPosition.source,
560
+ line: originalPosition.line,
561
+ column: originalPosition.column,
562
+ name: originalPosition.name || undefined,
563
+ },
564
+ sourceRoot: sourceMap.sourceRoot,
565
+ };
566
+ }
567
+
568
+ return null;
569
+ } catch (error) {
570
+ console.error('Error parsing source map:', error);
571
+ return null;
572
+ }
573
+ }
574
+
575
+ /** Retrieves the original source content embedded in the source map, if available. */
576
+ export async function getOriginalSourceContent(
577
+ originalInfo: OriginalSourceInfo,
578
+ sourceMapContent: string
579
+ ): Promise<string | null> {
580
+ try {
581
+ const sourceMap = JSON.parse(sourceMapContent);
582
+ const consumer = new SourceMapConsumer(sourceMap);
583
+
584
+ const sourceContent = consumer.sourceContentFor(originalInfo.source);
585
+
586
+ return sourceContent;
587
+ } catch (error) {
588
+ console.error('Error getting source content:', error);
589
+ return null;
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Resolves a stack trace line to original source location using source maps.
595
+ * Two-level cache: L1 caches the final resolved result, L2 caches the parsed
596
+ * source map so multiple positions in the same file are fast.
597
+ *
598
+ * When `debug` is `true`, detailed logs are printed to the console showing
599
+ * each step of the resolution pipeline.
600
+ */
601
+ export async function resolveLocation(
602
+ stackLine: string,
603
+ debug?: boolean
604
+ ): Promise<ResolvedSourceInfo | null> {
605
+ try {
606
+ if (debug) console.group('[show-component] resolveLocation');
607
+ if (debug) console.log('Stack line:', stackLine);
608
+
609
+ const frameInfo = extractStackFrameInfo(stackLine);
610
+ if (!frameInfo) {
611
+ if (debug) {
612
+ console.warn('Could not extract frame info from stack line');
613
+ console.groupEnd();
614
+ }
615
+ return null;
616
+ }
617
+
618
+ if (debug) console.log('Extracted frame:', frameInfo);
619
+
620
+ const { url, line, column } = frameInfo;
621
+ const cacheKey = `${url}:${line}:${column}`;
622
+
623
+ // L1: exact result cache
624
+ const cachedResult = resultCache.get(cacheKey);
625
+ if (cachedResult) {
626
+ if (debug) {
627
+ console.log('L1 cache hit:', cachedResult.originalSource);
628
+ console.groupEnd();
629
+ }
630
+ return cachedResult.originalSource;
631
+ }
632
+ if (debug) console.log('L1 cache miss');
633
+
634
+ // ── Next.js RSC fast path ──────────────────────────────────────────────
635
+ // For React Server Component URLs in Next.js, delegate resolution to the
636
+ // built-in dev server endpoints which resolve source maps server-side
637
+ // with full access to the bundler's compilation state. This removes the
638
+ // need for a custom /api/dev/source-file/ handler.
639
+ if (isNextjsRscUrl(url)) {
640
+ if (debug) console.log('Next.js RSC URL detected, trying built-in dev server endpoints');
641
+
642
+ // Strategy 1: POST /__nextjs_original-stack-frames (full server-side resolution)
643
+ const nextResult = await resolveViaNextDevServer(frameInfo, debug);
644
+ if (nextResult) {
645
+ boundedSet(resultCache, cacheKey, { originalSource: nextResult }, MAX_RESULT_CACHE_SIZE);
646
+ if (debug) {
647
+ console.log('Resolved via Next.js __nextjs_original-stack-frames:', nextResult);
648
+ console.groupEnd();
649
+ }
650
+ return nextResult;
651
+ }
652
+
653
+ // Strategy 2: GET /__nextjs_source-map (fetch source map, resolve client-side)
654
+ const filePath = extractFilePathFromRscUrl(url);
655
+ if (filePath) {
656
+ const sourceMapContent = await fetchSourceMapFromNextDevServer(filePath, debug);
657
+ if (sourceMapContent) {
658
+ if (debug) console.log('Got source map via __nextjs_source-map, resolving client-side');
659
+
660
+ const mapResult = await mapToOriginalSource(frameInfo, sourceMapContent);
661
+ if (mapResult) {
662
+ const resolvedSource = resolveSourcePath(
663
+ mapResult.info.source,
664
+ mapResult.sourceRoot,
665
+ url
666
+ );
667
+ const originalSourceContent = await getOriginalSourceContent(
668
+ mapResult.info,
669
+ sourceMapContent
670
+ );
671
+ const result: ResolvedSourceInfo = {
672
+ ...mapResult.info,
673
+ source: resolvedSource,
674
+ sourceContent: originalSourceContent || undefined,
675
+ };
676
+ boundedSet(resultCache, cacheKey, { originalSource: result }, MAX_RESULT_CACHE_SIZE);
677
+ if (debug) {
678
+ console.log('Resolved via Next.js __nextjs_source-map:', result);
679
+ console.groupEnd();
680
+ }
681
+ return result;
682
+ }
683
+ }
684
+ }
685
+
686
+ if (debug) {
687
+ console.warn('All Next.js resolution methods failed for RSC URL:', url);
688
+ console.groupEnd();
689
+ }
690
+ return null;
691
+ }
692
+
693
+ // ── Standard path (Vite, webpack, non-RSC HTTP URLs) ─────────────────
694
+
695
+ // L2: source map cache (keyed by URL)
696
+ let sourceMapData = sourceMapCache.get(url);
697
+ let effectiveUrl = url;
698
+
699
+ if (!sourceMapData) {
700
+ if (debug) console.log('L2 cache miss — fetching source file:', url);
701
+
702
+ const sourceResult = await fetchSourceFile(url);
703
+ effectiveUrl = sourceResult.effectiveUrl;
704
+
705
+ if (debug) console.log('Fetched source, effective URL:', effectiveUrl);
706
+
707
+ const sourceMapContent = await resolveSourceMap(
708
+ sourceResult.content,
709
+ sourceResult.effectiveUrl
710
+ );
711
+ if (!sourceMapContent) {
712
+ if (debug) {
713
+ console.warn('No source map found for:', effectiveUrl);
714
+ console.groupEnd();
715
+ }
716
+ return null;
717
+ }
718
+
719
+ if (debug) console.log('Source map resolved (length:', sourceMapContent.length, ')');
720
+
721
+ sourceMapData = {
722
+ sourceContent: sourceResult.content,
723
+ sourceMapContent,
724
+ };
725
+ boundedSet(sourceMapCache, url, sourceMapData, MAX_SOURCE_MAP_CACHE_SIZE);
726
+ if (url !== effectiveUrl) {
727
+ boundedSet(sourceMapCache, effectiveUrl, sourceMapData, MAX_SOURCE_MAP_CACHE_SIZE);
728
+ }
729
+ } else {
730
+ if (debug) console.log('L2 cache hit for:', url);
731
+ }
732
+
733
+ const mapResult = await mapToOriginalSource(frameInfo, sourceMapData.sourceMapContent);
734
+ if (!mapResult) {
735
+ if (debug) {
736
+ console.warn('Source map lookup returned no result for position', { line, column });
737
+ console.groupEnd();
738
+ }
739
+ return null;
740
+ }
741
+
742
+ if (debug) console.log('Mapped to original:', mapResult.info);
743
+
744
+ const rawSource = mapResult.info.source;
745
+ const resolvedSource = resolveSourcePath(rawSource, mapResult.sourceRoot, effectiveUrl);
746
+
747
+ if (debug) {
748
+ console.log('Source path resolution:', {
749
+ rawSource,
750
+ sourceRoot: mapResult.sourceRoot,
751
+ effectiveUrl,
752
+ resolvedSource,
753
+ });
754
+ }
755
+
756
+ const originalInfo: OriginalSourceInfo = {
757
+ ...mapResult.info,
758
+ source: resolvedSource,
759
+ };
760
+
761
+ // Use the raw (pre-resolved) path for content lookup — that's what the source map indexes by
762
+ const originalSourceContent = await getOriginalSourceContent(
763
+ { ...originalInfo, source: rawSource },
764
+ sourceMapData.sourceMapContent
765
+ );
766
+
767
+ const result: ResolvedSourceInfo = {
768
+ ...originalInfo,
769
+ sourceContent: originalSourceContent || undefined,
770
+ };
771
+
772
+ boundedSet(resultCache, cacheKey, { originalSource: result }, MAX_RESULT_CACHE_SIZE);
773
+
774
+ if (debug) {
775
+ console.log('Resolved result:', {
776
+ source: result.source,
777
+ line: result.line,
778
+ column: result.column,
779
+ name: result.name,
780
+ hasSourceContent: !!result.sourceContent,
781
+ });
782
+ console.groupEnd();
783
+ }
784
+
785
+ return result;
786
+ } catch (error) {
787
+ if (debug) {
788
+ console.error('Resolution failed:', error);
789
+ console.groupEnd();
790
+ }
791
+ console.error('Error resolving stack frame to original source:', error);
792
+ return null;
793
+ }
794
+ }
795
+
796
+ /** Clears all caches (including the Next.js dev server availability flag). */
797
+ export function clearCaches(): void {
798
+ resultCache.clear();
799
+ sourceMapCache.clear();
800
+ _nextDevServerAvailable = undefined;
801
+ }