From cc3ca95ebe6c7ebf002a60d7ef4d455eb9673103 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 07:50:20 +0000 Subject: [PATCH 01/14] feat: Add source map grabbing functionality Adds a new module for grabbing source map data from bundles. Co-authored-by: aiden --- package.json | 1 + .../react-grab/src/core/source-map-grabber.ts | 114 ++++++++++++++++++ pnpm-lock.yaml | 9 +- 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 packages/react-grab/src/core/source-map-grabber.ts diff --git a/package.json b/package.json index f91d326b2..87d4e0feb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@changesets/cli": "^2.27.10", "oxfmt": "^0.27.0", "turbo": "^2.6.3", + "typescript": "^5.9.3", "untun": "^0.1.3" }, "engines": { diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts new file mode 100644 index 000000000..479598313 --- /dev/null +++ b/packages/react-grab/src/core/source-map-grabber.ts @@ -0,0 +1,114 @@ +import { sourceMapCache, getSourceMap, normalizeFileName } from "bippy/source"; +import type { SourceMap } from "bippy/source"; + +interface SourceMapData { + [filename: string]: string; +} + +const isWeakRef = ( + value: SourceMap | WeakRef | null, +): value is WeakRef => + typeof WeakRef !== "undefined" && value instanceof WeakRef; + +const resolveSourceMap = ( + entry: SourceMap | WeakRef | null, +): SourceMap | null => { + if (entry === null) { + return null; + } + if (isWeakRef(entry)) { + return entry.deref() ?? null; + } + return entry; +}; + +const extractDataFromSourceMap = ( + sourceMap: SourceMap, + data: SourceMapData, +): void => { + const { sources, sourcesContent, sections } = sourceMap; + + if (sections) { + for (const section of sections) { + extractDataFromSourceMap(section.map as SourceMap, data); + } + return; + } + + if (!sourcesContent || sourcesContent.length === 0) { + return; + } + + for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) { + const source = sources[sourceIndex]; + const content = sourcesContent[sourceIndex]; + + if (!source || !content) { + continue; + } + + const normalizedFilename = normalizeFileName(source); + if (normalizedFilename && !data[normalizedFilename]) { + data[normalizedFilename] = content; + } + } +}; + +export const getSourceMapData = (): SourceMapData => { + const data: SourceMapData = {}; + + for (const entry of sourceMapCache.values()) { + const sourceMap = resolveSourceMap(entry); + if (sourceMap) { + extractDataFromSourceMap(sourceMap, data); + } + } + + return data; +}; + +export const getSourceMapDataForUrl = async ( + bundleUrl: string, + fetchFn?: (url: string) => Promise, +): Promise => { + const data: SourceMapData = {}; + + const sourceMap = await getSourceMap(bundleUrl, true, fetchFn); + if (sourceMap) { + extractDataFromSourceMap(sourceMap, data); + } + + return data; +}; + +export const addSourceMapData = ( + existingData: SourceMapData, + newData: SourceMapData, +): SourceMapData => { + return { ...existingData, ...newData }; +}; + +export const getAllSourceMapData = async ( + bundleUrls?: string[], + fetchFn?: (url: string) => Promise, +): Promise => { + const cachedData = getSourceMapData(); + + if (!bundleUrls || bundleUrls.length === 0) { + return cachedData; + } + + const fetchedDataPromises = bundleUrls.map((url) => + getSourceMapDataForUrl(url, fetchFn), + ); + const fetchedDataResults = await Promise.all(fetchedDataPromises); + + let combinedData = cachedData; + for (const fetchedData of fetchedDataResults) { + combinedData = addSourceMapData(combinedData, fetchedData); + } + + return combinedData; +}; + +export type { SourceMapData }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91ce0c3ec..39442140e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: turbo: specifier: ^2.6.3 version: 2.6.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 untun: specifier: ^0.1.3 version: 0.1.3 @@ -10227,7 +10230,7 @@ snapshots: eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.37.0(jiti@2.6.1)) @@ -10260,7 +10263,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -10329,7 +10332,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 From 98deee31f46df5a5f4853eb3358a71b74ab6877e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 07:52:35 +0000 Subject: [PATCH 02/14] Refactor: Improve source map data extraction logic Co-authored-by: aiden --- .../react-grab/src/core/source-map-grabber.ts | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts index 479598313..07bb61788 100644 --- a/packages/react-grab/src/core/source-map-grabber.ts +++ b/packages/react-grab/src/core/source-map-grabber.ts @@ -5,6 +5,11 @@ interface SourceMapData { [filename: string]: string; } +interface SectionMap { + sources: string[]; + sourcesContent?: string[]; +} + const isWeakRef = ( value: SourceMap | WeakRef | null, ): value is WeakRef => @@ -22,19 +27,11 @@ const resolveSourceMap = ( return entry; }; -const extractDataFromSourceMap = ( - sourceMap: SourceMap, +const extractSourcesFromMap = ( + sources: string[], + sourcesContent: (string | null)[] | undefined, data: SourceMapData, ): void => { - const { sources, sourcesContent, sections } = sourceMap; - - if (sections) { - for (const section of sections) { - extractDataFromSourceMap(section.map as SourceMap, data); - } - return; - } - if (!sourcesContent || sourcesContent.length === 0) { return; } @@ -54,6 +51,28 @@ const extractDataFromSourceMap = ( } }; +const extractDataFromSectionMap = ( + sectionMap: SectionMap, + data: SourceMapData, +): void => { + extractSourcesFromMap(sectionMap.sources, sectionMap.sourcesContent, data); +}; + +const extractDataFromSourceMap = ( + sourceMap: SourceMap, + data: SourceMapData, +): void => { + const { sources, sourcesContent, sections } = sourceMap; + + if (sections && sections.length > 0) { + for (const section of sections) { + extractDataFromSectionMap(section.map as SectionMap, data); + } + } + + extractSourcesFromMap(sources, sourcesContent, data); +}; + export const getSourceMapData = (): SourceMapData => { const data: SourceMapData = {}; From 8af09eaedfb5d805cb0da3ae0aabad214cd0b598 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 07:54:38 +0000 Subject: [PATCH 03/14] Refactor: Simplify source map data extraction logic Co-authored-by: aiden --- .../react-grab/src/core/source-map-grabber.ts | 120 ++++++------------ 1 file changed, 40 insertions(+), 80 deletions(-) diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts index 07bb61788..5e01e45fb 100644 --- a/packages/react-grab/src/core/source-map-grabber.ts +++ b/packages/react-grab/src/core/source-map-grabber.ts @@ -5,129 +5,89 @@ interface SourceMapData { [filename: string]: string; } -interface SectionMap { - sources: string[]; - sourcesContent?: string[]; -} - const isWeakRef = ( value: SourceMap | WeakRef | null, ): value is WeakRef => typeof WeakRef !== "undefined" && value instanceof WeakRef; -const resolveSourceMap = ( - entry: SourceMap | WeakRef | null, -): SourceMap | null => { - if (entry === null) { - return null; - } - if (isWeakRef(entry)) { - return entry.deref() ?? null; - } - return entry; -}; +const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { + const filenameToContent: SourceMapData = {}; -const extractSourcesFromMap = ( - sources: string[], - sourcesContent: (string | null)[] | undefined, - data: SourceMapData, -): void => { - if (!sourcesContent || sourcesContent.length === 0) { - return; - } + const processSourcesArray = ( + sources: string[], + sourcesContent: (string | null)[] | undefined, + ) => { + if (!sourcesContent) return; - for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) { - const source = sources[sourceIndex]; - const content = sourcesContent[sourceIndex]; + for (let index = 0; index < sources.length; index++) { + const rawFilename = sources[index]; + const fileContent = sourcesContent[index]; - if (!source || !content) { - continue; - } + if (!rawFilename || !fileContent) continue; - const normalizedFilename = normalizeFileName(source); - if (normalizedFilename && !data[normalizedFilename]) { - data[normalizedFilename] = content; + const filename = normalizeFileName(rawFilename); + if (filename && !filenameToContent[filename]) { + filenameToContent[filename] = fileContent; + } } - } -}; + }; -const extractDataFromSectionMap = ( - sectionMap: SectionMap, - data: SourceMapData, -): void => { - extractSourcesFromMap(sectionMap.sources, sectionMap.sourcesContent, data); -}; - -const extractDataFromSourceMap = ( - sourceMap: SourceMap, - data: SourceMapData, -): void => { - const { sources, sourcesContent, sections } = sourceMap; - - if (sections && sections.length > 0) { - for (const section of sections) { - extractDataFromSectionMap(section.map as SectionMap, data); + if (sourceMap.sections) { + for (const section of sourceMap.sections) { + processSourcesArray(section.map.sources, section.map.sourcesContent); } } - extractSourcesFromMap(sources, sourcesContent, data); + processSourcesArray(sourceMap.sources, sourceMap.sourcesContent); + + return filenameToContent; }; export const getSourceMapData = (): SourceMapData => { - const data: SourceMapData = {}; + const filenameToContent: SourceMapData = {}; + + for (const cacheEntry of sourceMapCache.values()) { + const sourceMap = cacheEntry === null + ? null + : isWeakRef(cacheEntry) + ? cacheEntry.deref() ?? null + : cacheEntry; - for (const entry of sourceMapCache.values()) { - const sourceMap = resolveSourceMap(entry); if (sourceMap) { - extractDataFromSourceMap(sourceMap, data); + Object.assign(filenameToContent, extractSourceMapData(sourceMap)); } } - return data; + return filenameToContent; }; export const getSourceMapDataForUrl = async ( bundleUrl: string, fetchFn?: (url: string) => Promise, ): Promise => { - const data: SourceMapData = {}; - const sourceMap = await getSourceMap(bundleUrl, true, fetchFn); - if (sourceMap) { - extractDataFromSourceMap(sourceMap, data); - } - - return data; -}; - -export const addSourceMapData = ( - existingData: SourceMapData, - newData: SourceMapData, -): SourceMapData => { - return { ...existingData, ...newData }; + return sourceMap ? extractSourceMapData(sourceMap) : {}; }; export const getAllSourceMapData = async ( bundleUrls?: string[], fetchFn?: (url: string) => Promise, ): Promise => { - const cachedData = getSourceMapData(); + const filenameToContent = getSourceMapData(); if (!bundleUrls || bundleUrls.length === 0) { - return cachedData; + return filenameToContent; } - const fetchedDataPromises = bundleUrls.map((url) => - getSourceMapDataForUrl(url, fetchFn), + const fetchResults = await Promise.all( + bundleUrls.map((url) => getSourceMapDataForUrl(url, fetchFn)), ); - const fetchedDataResults = await Promise.all(fetchedDataPromises); - let combinedData = cachedData; - for (const fetchedData of fetchedDataResults) { - combinedData = addSourceMapData(combinedData, fetchedData); + for (const fetchedData of fetchResults) { + Object.assign(filenameToContent, fetchedData); } - return combinedData; + return filenameToContent; }; export type { SourceMapData }; From 916856235ff859df3356e9b1b709848c21104ace Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 07:56:29 +0000 Subject: [PATCH 04/14] Refactor: Improve source map data retrieval logic Co-authored-by: aiden --- .../react-grab/src/core/source-map-grabber.ts | 80 +++++++++++++++---- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts index 5e01e45fb..28ebc49dc 100644 --- a/packages/react-grab/src/core/source-map-grabber.ts +++ b/packages/react-grab/src/core/source-map-grabber.ts @@ -43,15 +43,62 @@ const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { return filenameToContent; }; -export const getSourceMapData = (): SourceMapData => { +const isJavaScriptType = (typeAttribute: string | null): boolean => { + if (!typeAttribute) return true; + const normalizedType = typeAttribute.toLowerCase().trim(); + return ( + normalizedType === "" || + normalizedType === "text/javascript" || + normalizedType === "application/javascript" || + normalizedType === "module" + ); +}; + +const isJavaScriptUrl = (url: string): boolean => { + const pathWithoutQuery = url.split("?")[0]; + return ( + pathWithoutQuery.endsWith(".js") || + pathWithoutQuery.endsWith(".mjs") || + pathWithoutQuery.endsWith(".cjs") + ); +}; + +const getScriptUrlsFromDocument = (): string[] => { + if (typeof document === "undefined") return []; + + const scriptUrls: string[] = []; + const scriptElements = Array.from(document.querySelectorAll("script[src]")); + + for (const script of scriptElements) { + const srcAttribute = script.getAttribute("src"); + const typeAttribute = script.getAttribute("type"); + + if (!srcAttribute) continue; + if (!isJavaScriptType(typeAttribute)) continue; + + try { + const absoluteUrl = new URL(srcAttribute, window.location.href).href; + if (isJavaScriptUrl(absoluteUrl) && !scriptUrls.includes(absoluteUrl)) { + scriptUrls.push(absoluteUrl); + } + } catch { + continue; + } + } + + return scriptUrls; +}; + +export const getSourceMapDataFromCache = (): SourceMapData => { const filenameToContent: SourceMapData = {}; for (const cacheEntry of sourceMapCache.values()) { - const sourceMap = cacheEntry === null - ? null - : isWeakRef(cacheEntry) - ? cacheEntry.deref() ?? null - : cacheEntry; + const sourceMap = + cacheEntry === null + ? null + : isWeakRef(cacheEntry) + ? (cacheEntry.deref() ?? null) + : cacheEntry; if (sourceMap) { Object.assign(filenameToContent, extractSourceMapData(sourceMap)); @@ -69,18 +116,14 @@ export const getSourceMapDataForUrl = async ( return sourceMap ? extractSourceMapData(sourceMap) : {}; }; -export const getAllSourceMapData = async ( - bundleUrls?: string[], +export const getSourceMapDataFromScripts = async ( fetchFn?: (url: string) => Promise, ): Promise => { - const filenameToContent = getSourceMapData(); - - if (!bundleUrls || bundleUrls.length === 0) { - return filenameToContent; - } + const scriptUrls = getScriptUrlsFromDocument(); + const filenameToContent: SourceMapData = {}; const fetchResults = await Promise.all( - bundleUrls.map((url) => getSourceMapDataForUrl(url, fetchFn)), + scriptUrls.map((url) => getSourceMapDataForUrl(url, fetchFn)), ); for (const fetchedData of fetchResults) { @@ -90,4 +133,13 @@ export const getAllSourceMapData = async ( return filenameToContent; }; +export const getSourceMapData = async ( + fetchFn?: (url: string) => Promise, +): Promise => { + const cachedData = getSourceMapDataFromCache(); + const scriptData = await getSourceMapDataFromScripts(fetchFn); + + return { ...cachedData, ...scriptData }; +}; + export type { SourceMapData }; From b7cc22ee6e4bfba30e96004ec7666cc599e42636 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 07:58:19 +0000 Subject: [PATCH 05/14] Refactor: Improve source map detection and caching Co-authored-by: aiden --- .../react-grab/src/core/source-map-grabber.ts | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts index 28ebc49dc..4fc358f50 100644 --- a/packages/react-grab/src/core/source-map-grabber.ts +++ b/packages/react-grab/src/core/source-map-grabber.ts @@ -43,8 +43,37 @@ const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { return filenameToContent; }; -const isJavaScriptType = (typeAttribute: string | null): boolean => { +const NON_JS_EXTENSIONS = [ + ".css", + ".scss", + ".sass", + ".less", + ".json", + ".html", + ".svg", + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".woff", + ".woff2", + ".ttf", + ".eot", +]; + +const isLikelyJavaScriptUrl = (url: string): boolean => { + const pathWithoutQuery = url.split("?")[0].toLowerCase(); + return !NON_JS_EXTENSIONS.some((extension) => + pathWithoutQuery.endsWith(extension), + ); +}; + +const isJavaScriptScriptElement = (script: Element): boolean => { + const typeAttribute = script.getAttribute("type"); + if (!typeAttribute) return true; + const normalizedType = typeAttribute.toLowerCase().trim(); return ( normalizedType === "" || @@ -54,33 +83,26 @@ const isJavaScriptType = (typeAttribute: string | null): boolean => { ); }; -const isJavaScriptUrl = (url: string): boolean => { - const pathWithoutQuery = url.split("?")[0]; - return ( - pathWithoutQuery.endsWith(".js") || - pathWithoutQuery.endsWith(".mjs") || - pathWithoutQuery.endsWith(".cjs") - ); -}; - const getScriptUrlsFromDocument = (): string[] => { if (typeof document === "undefined") return []; const scriptUrls: string[] = []; + const seenUrls = new Set(); const scriptElements = Array.from(document.querySelectorAll("script[src]")); for (const script of scriptElements) { const srcAttribute = script.getAttribute("src"); - const typeAttribute = script.getAttribute("type"); - if (!srcAttribute) continue; - if (!isJavaScriptType(typeAttribute)) continue; + if (!isJavaScriptScriptElement(script)) continue; try { const absoluteUrl = new URL(srcAttribute, window.location.href).href; - if (isJavaScriptUrl(absoluteUrl) && !scriptUrls.includes(absoluteUrl)) { - scriptUrls.push(absoluteUrl); - } + + if (seenUrls.has(absoluteUrl)) continue; + if (!isLikelyJavaScriptUrl(absoluteUrl)) continue; + + seenUrls.add(absoluteUrl); + scriptUrls.push(absoluteUrl); } catch { continue; } @@ -89,6 +111,10 @@ const getScriptUrlsFromDocument = (): string[] => { return scriptUrls; }; +const getCachedSourceMapUrls = (): Set => { + return new Set(sourceMapCache.keys()); +}; + export const getSourceMapDataFromCache = (): SourceMapData => { const filenameToContent: SourceMapData = {}; @@ -112,18 +138,25 @@ export const getSourceMapDataForUrl = async ( bundleUrl: string, fetchFn?: (url: string) => Promise, ): Promise => { - const sourceMap = await getSourceMap(bundleUrl, true, fetchFn); - return sourceMap ? extractSourceMapData(sourceMap) : {}; + try { + const sourceMap = await getSourceMap(bundleUrl, true, fetchFn); + return sourceMap ? extractSourceMapData(sourceMap) : {}; + } catch { + return {}; + } }; export const getSourceMapDataFromScripts = async ( fetchFn?: (url: string) => Promise, ): Promise => { const scriptUrls = getScriptUrlsFromDocument(); + const cachedUrls = getCachedSourceMapUrls(); + const uncachedScriptUrls = scriptUrls.filter((url) => !cachedUrls.has(url)); + const filenameToContent: SourceMapData = {}; const fetchResults = await Promise.all( - scriptUrls.map((url) => getSourceMapDataForUrl(url, fetchFn)), + uncachedScriptUrls.map((url) => getSourceMapDataForUrl(url, fetchFn)), ); for (const fetchedData of fetchResults) { From f03f3a43e619f5340133486eb22afc7308ac5043 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 08:07:09 +0000 Subject: [PATCH 06/14] Refactor: Improve source map data extraction and caching logic Co-authored-by: aiden --- .../react-grab/src/core/source-map-grabber.ts | 134 ++++++++++-------- 1 file changed, 73 insertions(+), 61 deletions(-) diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts index 4fc358f50..6023bce85 100644 --- a/packages/react-grab/src/core/source-map-grabber.ts +++ b/packages/react-grab/src/core/source-map-grabber.ts @@ -6,35 +6,42 @@ interface SourceMapData { } const isWeakRef = ( - value: SourceMap | WeakRef | null, -): value is WeakRef => - typeof WeakRef !== "undefined" && value instanceof WeakRef; + cacheValue: SourceMap | WeakRef | null, +): cacheValue is WeakRef => + typeof WeakRef !== "undefined" && cacheValue instanceof WeakRef; const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { const filenameToContent: SourceMapData = {}; const processSourcesArray = ( - sources: string[], - sourcesContent: (string | null)[] | undefined, + sourceFilenames: string[], + sourceFileContents: (string | null)[] | undefined, ) => { - if (!sourcesContent) return; + if (!sourceFileContents) return; - for (let index = 0; index < sources.length; index++) { - const rawFilename = sources[index]; - const fileContent = sourcesContent[index]; + for ( + let sourceIndex = 0; + sourceIndex < sourceFilenames.length; + sourceIndex++ + ) { + const rawSourceFilename = sourceFilenames[sourceIndex]; + const sourceFileContent = sourceFileContents[sourceIndex]; - if (!rawFilename || !fileContent) continue; + if (!rawSourceFilename || !sourceFileContent) continue; - const filename = normalizeFileName(rawFilename); - if (filename && !filenameToContent[filename]) { - filenameToContent[filename] = fileContent; + const normalizedFilename = normalizeFileName(rawSourceFilename); + if (normalizedFilename && !filenameToContent[normalizedFilename]) { + filenameToContent[normalizedFilename] = sourceFileContent; } } }; if (sourceMap.sections) { - for (const section of sourceMap.sections) { - processSourcesArray(section.map.sources, section.map.sourcesContent); + for (const sourceMapSection of sourceMap.sections) { + processSourcesArray( + sourceMapSection.map.sources, + sourceMapSection.map.sourcesContent, + ); } } @@ -43,7 +50,7 @@ const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { return filenameToContent; }; -const NON_JS_EXTENSIONS = [ +const NON_JAVASCRIPT_EXTENSIONS = [ ".css", ".scss", ".sass", @@ -62,72 +69,73 @@ const NON_JS_EXTENSIONS = [ ".eot", ]; -const isLikelyJavaScriptUrl = (url: string): boolean => { - const pathWithoutQuery = url.split("?")[0].toLowerCase(); - return !NON_JS_EXTENSIONS.some((extension) => - pathWithoutQuery.endsWith(extension), +const isLikelyJavaScriptUrl = (scriptUrl: string): boolean => { + const urlPathWithoutQueryString = scriptUrl.split("?")[0].toLowerCase(); + return !NON_JAVASCRIPT_EXTENSIONS.some((nonJsExtension) => + urlPathWithoutQueryString.endsWith(nonJsExtension), ); }; -const isJavaScriptScriptElement = (script: Element): boolean => { - const typeAttribute = script.getAttribute("type"); +const isJavaScriptScriptElement = (scriptElement: Element): boolean => { + const typeAttribute = scriptElement.getAttribute("type"); if (!typeAttribute) return true; - const normalizedType = typeAttribute.toLowerCase().trim(); + const normalizedTypeAttribute = typeAttribute.toLowerCase().trim(); return ( - normalizedType === "" || - normalizedType === "text/javascript" || - normalizedType === "application/javascript" || - normalizedType === "module" + normalizedTypeAttribute === "" || + normalizedTypeAttribute === "text/javascript" || + normalizedTypeAttribute === "application/javascript" || + normalizedTypeAttribute === "module" ); }; const getScriptUrlsFromDocument = (): string[] => { if (typeof document === "undefined") return []; - const scriptUrls: string[] = []; - const seenUrls = new Set(); + const discoveredScriptUrls: string[] = []; + const alreadyProcessedUrls = new Set(); const scriptElements = Array.from(document.querySelectorAll("script[src]")); - for (const script of scriptElements) { - const srcAttribute = script.getAttribute("src"); + for (const scriptElement of scriptElements) { + const srcAttribute = scriptElement.getAttribute("src"); if (!srcAttribute) continue; - if (!isJavaScriptScriptElement(script)) continue; + if (!isJavaScriptScriptElement(scriptElement)) continue; try { - const absoluteUrl = new URL(srcAttribute, window.location.href).href; + const absoluteScriptUrl = new URL(srcAttribute, window.location.href) + .href; - if (seenUrls.has(absoluteUrl)) continue; - if (!isLikelyJavaScriptUrl(absoluteUrl)) continue; + if (alreadyProcessedUrls.has(absoluteScriptUrl)) continue; + if (!isLikelyJavaScriptUrl(absoluteScriptUrl)) continue; - seenUrls.add(absoluteUrl); - scriptUrls.push(absoluteUrl); + alreadyProcessedUrls.add(absoluteScriptUrl); + discoveredScriptUrls.push(absoluteScriptUrl); } catch { continue; } } - return scriptUrls; + return discoveredScriptUrls; }; -const getCachedSourceMapUrls = (): Set => { +const getAlreadyCachedSourceMapUrls = (): Set => { return new Set(sourceMapCache.keys()); }; export const getSourceMapDataFromCache = (): SourceMapData => { const filenameToContent: SourceMapData = {}; - for (const cacheEntry of sourceMapCache.values()) { - const sourceMap = - cacheEntry === null + for (const sourceMapCacheEntry of sourceMapCache.values()) { + const resolvedSourceMap = + sourceMapCacheEntry === null ? null - : isWeakRef(cacheEntry) - ? (cacheEntry.deref() ?? null) - : cacheEntry; + : isWeakRef(sourceMapCacheEntry) + ? (sourceMapCacheEntry.deref() ?? null) + : sourceMapCacheEntry; - if (sourceMap) { - Object.assign(filenameToContent, extractSourceMapData(sourceMap)); + if (resolvedSourceMap) { + Object.assign(filenameToContent, extractSourceMapData(resolvedSourceMap)); } } @@ -136,10 +144,10 @@ export const getSourceMapDataFromCache = (): SourceMapData => { export const getSourceMapDataForUrl = async ( bundleUrl: string, - fetchFn?: (url: string) => Promise, + fetchFunction?: (url: string) => Promise, ): Promise => { try { - const sourceMap = await getSourceMap(bundleUrl, true, fetchFn); + const sourceMap = await getSourceMap(bundleUrl, true, fetchFunction); return sourceMap ? extractSourceMapData(sourceMap) : {}; } catch { return {}; @@ -147,32 +155,36 @@ export const getSourceMapDataForUrl = async ( }; export const getSourceMapDataFromScripts = async ( - fetchFn?: (url: string) => Promise, + fetchFunction?: (url: string) => Promise, ): Promise => { - const scriptUrls = getScriptUrlsFromDocument(); - const cachedUrls = getCachedSourceMapUrls(); - const uncachedScriptUrls = scriptUrls.filter((url) => !cachedUrls.has(url)); + const allScriptUrls = getScriptUrlsFromDocument(); + const alreadyCachedUrls = getAlreadyCachedSourceMapUrls(); + const scriptUrlsNotYetCached = allScriptUrls.filter( + (scriptUrl) => !alreadyCachedUrls.has(scriptUrl), + ); const filenameToContent: SourceMapData = {}; - const fetchResults = await Promise.all( - uncachedScriptUrls.map((url) => getSourceMapDataForUrl(url, fetchFn)), + const sourceMapFetchResults = await Promise.all( + scriptUrlsNotYetCached.map((scriptUrl) => + getSourceMapDataForUrl(scriptUrl, fetchFunction), + ), ); - for (const fetchedData of fetchResults) { - Object.assign(filenameToContent, fetchedData); + for (const fetchedSourceMapData of sourceMapFetchResults) { + Object.assign(filenameToContent, fetchedSourceMapData); } return filenameToContent; }; export const getSourceMapData = async ( - fetchFn?: (url: string) => Promise, + fetchFunction?: (url: string) => Promise, ): Promise => { - const cachedData = getSourceMapDataFromCache(); - const scriptData = await getSourceMapDataFromScripts(fetchFn); + const cachedSourceMapData = getSourceMapDataFromCache(); + const scriptSourceMapData = await getSourceMapDataFromScripts(fetchFunction); - return { ...cachedData, ...scriptData }; + return { ...cachedSourceMapData, ...scriptSourceMapData }; }; export type { SourceMapData }; From 9e3c64761c2061eaff00688d26d49661c477ac19 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 08:09:41 +0000 Subject: [PATCH 07/14] Refactor: Improve JavaScript URL detection logic Co-authored-by: aiden --- .../react-grab/src/core/source-map-grabber.ts | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts index 6023bce85..f67092d32 100644 --- a/packages/react-grab/src/core/source-map-grabber.ts +++ b/packages/react-grab/src/core/source-map-grabber.ts @@ -50,29 +50,12 @@ const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { return filenameToContent; }; -const NON_JAVASCRIPT_EXTENSIONS = [ - ".css", - ".scss", - ".sass", - ".less", - ".json", - ".html", - ".svg", - ".png", - ".jpg", - ".jpeg", - ".gif", - ".webp", - ".woff", - ".woff2", - ".ttf", - ".eot", -]; - -const isLikelyJavaScriptUrl = (scriptUrl: string): boolean => { +const JAVASCRIPT_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".tsx"]; + +const isJavaScriptUrl = (scriptUrl: string): boolean => { const urlPathWithoutQueryString = scriptUrl.split("?")[0].toLowerCase(); - return !NON_JAVASCRIPT_EXTENSIONS.some((nonJsExtension) => - urlPathWithoutQueryString.endsWith(nonJsExtension), + return JAVASCRIPT_EXTENSIONS.some((jsExtension) => + urlPathWithoutQueryString.endsWith(jsExtension), ); }; @@ -107,7 +90,7 @@ const getScriptUrlsFromDocument = (): string[] => { .href; if (alreadyProcessedUrls.has(absoluteScriptUrl)) continue; - if (!isLikelyJavaScriptUrl(absoluteScriptUrl)) continue; + if (!isJavaScriptUrl(absoluteScriptUrl)) continue; alreadyProcessedUrls.add(absoluteScriptUrl); discoveredScriptUrls.push(absoluteScriptUrl); From e50f741f52127d97e1e4f4d8bd2757249c34714a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 08:11:09 +0000 Subject: [PATCH 08/14] Refactor: Simplify isJavaScriptUrl to only check for .js extension Co-authored-by: aiden --- packages/react-grab/src/core/source-map-grabber.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts index f67092d32..bc4a5acd0 100644 --- a/packages/react-grab/src/core/source-map-grabber.ts +++ b/packages/react-grab/src/core/source-map-grabber.ts @@ -50,13 +50,9 @@ const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { return filenameToContent; }; -const JAVASCRIPT_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".tsx"]; - const isJavaScriptUrl = (scriptUrl: string): boolean => { const urlPathWithoutQueryString = scriptUrl.split("?")[0].toLowerCase(); - return JAVASCRIPT_EXTENSIONS.some((jsExtension) => - urlPathWithoutQueryString.endsWith(jsExtension), - ); + return urlPathWithoutQueryString.endsWith(".js"); }; const isJavaScriptScriptElement = (scriptElement: Element): boolean => { From 1ea762f965e6db05fd288c3ab8c87edc8d02c629 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 08:18:12 +0000 Subject: [PATCH 09/14] Refactor: Improve source map data extraction and retrieval Co-authored-by: aiden --- .../react-grab/src/core/source-map-grabber.ts | 166 +++++++----------- 1 file changed, 64 insertions(+), 102 deletions(-) diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts index bc4a5acd0..ac650e966 100644 --- a/packages/react-grab/src/core/source-map-grabber.ts +++ b/packages/react-grab/src/core/source-map-grabber.ts @@ -6,127 +6,97 @@ interface SourceMapData { } const isWeakRef = ( - cacheValue: SourceMap | WeakRef | null, -): cacheValue is WeakRef => - typeof WeakRef !== "undefined" && cacheValue instanceof WeakRef; + value: SourceMap | WeakRef, +): value is WeakRef => + typeof WeakRef !== "undefined" && value instanceof WeakRef; + +const resolveSourceMapFromCache = ( + cacheEntry: SourceMap | WeakRef | null, +): SourceMap | null => { + if (!cacheEntry) return null; + if (isWeakRef(cacheEntry)) return cacheEntry.deref() ?? null; + return cacheEntry; +}; const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { - const filenameToContent: SourceMapData = {}; + const result: SourceMapData = {}; - const processSourcesArray = ( - sourceFilenames: string[], - sourceFileContents: (string | null)[] | undefined, + const processSources = ( + sources: string[], + contents: (string | null)[] | undefined, ) => { - if (!sourceFileContents) return; - - for ( - let sourceIndex = 0; - sourceIndex < sourceFilenames.length; - sourceIndex++ - ) { - const rawSourceFilename = sourceFilenames[sourceIndex]; - const sourceFileContent = sourceFileContents[sourceIndex]; - - if (!rawSourceFilename || !sourceFileContent) continue; + if (!contents) return; - const normalizedFilename = normalizeFileName(rawSourceFilename); - if (normalizedFilename && !filenameToContent[normalizedFilename]) { - filenameToContent[normalizedFilename] = sourceFileContent; + for (let i = 0; i < sources.length; i++) { + const filename = normalizeFileName(sources[i]); + const content = contents[i]; + if (filename && content && !result[filename]) { + result[filename] = content; } } }; if (sourceMap.sections) { - for (const sourceMapSection of sourceMap.sections) { - processSourcesArray( - sourceMapSection.map.sources, - sourceMapSection.map.sourcesContent, - ); + for (const section of sourceMap.sections) { + processSources(section.map.sources, section.map.sourcesContent); } } - processSourcesArray(sourceMap.sources, sourceMap.sourcesContent); - - return filenameToContent; + processSources(sourceMap.sources, sourceMap.sourcesContent); + return result; }; -const isJavaScriptUrl = (scriptUrl: string): boolean => { - const urlPathWithoutQueryString = scriptUrl.split("?")[0].toLowerCase(); - return urlPathWithoutQueryString.endsWith(".js"); -}; - -const isJavaScriptScriptElement = (scriptElement: Element): boolean => { - const typeAttribute = scriptElement.getAttribute("type"); - - if (!typeAttribute) return true; - - const normalizedTypeAttribute = typeAttribute.toLowerCase().trim(); +const hasJavaScriptType = (script: Element): boolean => { + const type = script.getAttribute("type")?.toLowerCase().trim(); return ( - normalizedTypeAttribute === "" || - normalizedTypeAttribute === "text/javascript" || - normalizedTypeAttribute === "application/javascript" || - normalizedTypeAttribute === "module" + !type || + type === "text/javascript" || + type === "application/javascript" || + type === "module" ); }; -const getScriptUrlsFromDocument = (): string[] => { +const getScriptUrls = (): string[] => { if (typeof document === "undefined") return []; - const discoveredScriptUrls: string[] = []; - const alreadyProcessedUrls = new Set(); - const scriptElements = Array.from(document.querySelectorAll("script[src]")); + const urls = new Set(); - for (const scriptElement of scriptElements) { - const srcAttribute = scriptElement.getAttribute("src"); - if (!srcAttribute) continue; - if (!isJavaScriptScriptElement(scriptElement)) continue; + for (const script of Array.from(document.querySelectorAll("script[src]"))) { + const src = script.getAttribute("src"); + if (!src || !hasJavaScriptType(script)) continue; try { - const absoluteScriptUrl = new URL(srcAttribute, window.location.href) - .href; - - if (alreadyProcessedUrls.has(absoluteScriptUrl)) continue; - if (!isJavaScriptUrl(absoluteScriptUrl)) continue; - - alreadyProcessedUrls.add(absoluteScriptUrl); - discoveredScriptUrls.push(absoluteScriptUrl); + const url = new URL(src, window.location.href).href; + if (url.split("?")[0].endsWith(".js")) { + urls.add(url); + } } catch { continue; } } - return discoveredScriptUrls; -}; - -const getAlreadyCachedSourceMapUrls = (): Set => { - return new Set(sourceMapCache.keys()); + return Array.from(urls); }; export const getSourceMapDataFromCache = (): SourceMapData => { - const filenameToContent: SourceMapData = {}; - - for (const sourceMapCacheEntry of sourceMapCache.values()) { - const resolvedSourceMap = - sourceMapCacheEntry === null - ? null - : isWeakRef(sourceMapCacheEntry) - ? (sourceMapCacheEntry.deref() ?? null) - : sourceMapCacheEntry; - - if (resolvedSourceMap) { - Object.assign(filenameToContent, extractSourceMapData(resolvedSourceMap)); + const result: SourceMapData = {}; + + for (const cacheEntry of sourceMapCache.values()) { + const sourceMap = resolveSourceMapFromCache(cacheEntry); + if (sourceMap) { + Object.assign(result, extractSourceMapData(sourceMap)); } } - return filenameToContent; + return result; }; export const getSourceMapDataForUrl = async ( - bundleUrl: string, - fetchFunction?: (url: string) => Promise, + url: string, + fetchFn?: (url: string) => Promise, ): Promise => { try { - const sourceMap = await getSourceMap(bundleUrl, true, fetchFunction); + const sourceMap = await getSourceMap(url, true, fetchFn); return sourceMap ? extractSourceMapData(sourceMap) : {}; } catch { return {}; @@ -134,36 +104,28 @@ export const getSourceMapDataForUrl = async ( }; export const getSourceMapDataFromScripts = async ( - fetchFunction?: (url: string) => Promise, + fetchFn?: (url: string) => Promise, ): Promise => { - const allScriptUrls = getScriptUrlsFromDocument(); - const alreadyCachedUrls = getAlreadyCachedSourceMapUrls(); - const scriptUrlsNotYetCached = allScriptUrls.filter( - (scriptUrl) => !alreadyCachedUrls.has(scriptUrl), - ); - - const filenameToContent: SourceMapData = {}; + const cachedUrls = new Set(sourceMapCache.keys()); + const uncachedUrls = getScriptUrls().filter((url) => !cachedUrls.has(url)); - const sourceMapFetchResults = await Promise.all( - scriptUrlsNotYetCached.map((scriptUrl) => - getSourceMapDataForUrl(scriptUrl, fetchFunction), - ), + const results = await Promise.all( + uncachedUrls.map((url) => getSourceMapDataForUrl(url, fetchFn)), ); - for (const fetchedSourceMapData of sourceMapFetchResults) { - Object.assign(filenameToContent, fetchedSourceMapData); + const combined: SourceMapData = {}; + for (const data of results) { + Object.assign(combined, data); } - - return filenameToContent; + return combined; }; export const getSourceMapData = async ( - fetchFunction?: (url: string) => Promise, + fetchFn?: (url: string) => Promise, ): Promise => { - const cachedSourceMapData = getSourceMapDataFromCache(); - const scriptSourceMapData = await getSourceMapDataFromScripts(fetchFunction); - - return { ...cachedSourceMapData, ...scriptSourceMapData }; + const cached = getSourceMapDataFromCache(); + const fromScripts = await getSourceMapDataFromScripts(fetchFn); + return { ...cached, ...fromScripts }; }; export type { SourceMapData }; From 63fe4adfb21d4640d51327e669511f7ecb42f12d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 08:39:12 +0000 Subject: [PATCH 10/14] Refactor: Move source map logic to context.ts Co-authored-by: aiden --- packages/react-grab/src/constants.ts | 2 + packages/react-grab/src/core/context.ts | 176 +++++++++++++++++- .../react-grab/src/core/source-map-grabber.ts | 131 ------------- 3 files changed, 176 insertions(+), 133 deletions(-) delete mode 100644 packages/react-grab/src/core/source-map-grabber.ts diff --git a/packages/react-grab/src/constants.ts b/packages/react-grab/src/constants.ts index c2eb7fc65..17679e7e8 100644 --- a/packages/react-grab/src/constants.ts +++ b/packages/react-grab/src/constants.ts @@ -51,6 +51,8 @@ export const ARROW_HEIGHT_PX = 8; export const ARROW_CENTER_PERCENT = 50; export const LABEL_GAP_PX = 4; export const MAX_HTML_FALLBACK_LENGTH = 500; +export const DEFAULT_STACK_CONTEXT_LINES = 3; +export const SOURCE_CONTEXT_LINES = 3; export const PREVIEW_ATTR_VALUE_MAX_LENGTH = 15; export const PREVIEW_MAX_ATTRS = 3; diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index 77eb1743f..0f3f85608 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -2,8 +2,11 @@ import { isSourceFile, normalizeFileName, getOwnerStack, + sourceMapCache, + getSourceMap, StackFrame, } from "bippy/source"; +import type { SourceMap } from "bippy/source"; import { isCapitalized } from "../utils/is-capitalized.js"; import { getFiberFromHostInstance, @@ -13,10 +16,12 @@ import { traverseFiber, } from "bippy"; import { + DEFAULT_STACK_CONTEXT_LINES, MAX_HTML_FALLBACK_LENGTH, PREVIEW_ATTR_VALUE_MAX_LENGTH, PREVIEW_MAX_ATTRS, PREVIEW_PRIORITY_ATTRS, + SOURCE_CONTEXT_LINES, } from "../constants.js"; const NEXT_INTERNAL_COMPONENT_NAMES = new Set([ @@ -55,6 +60,151 @@ const REACT_INTERNAL_COMPONENT_NAMES = new Set([ "SuspenseList", ]); +interface SourceMapData { + [filename: string]: string; +} + +const isWeakRef = ( + value: SourceMap | WeakRef, +): value is WeakRef => + typeof WeakRef !== "undefined" && value instanceof WeakRef; + +const resolveSourceMapFromCache = ( + cacheEntry: SourceMap | WeakRef | null, +): SourceMap | null => { + if (!cacheEntry) return null; + if (isWeakRef(cacheEntry)) return cacheEntry.deref() ?? null; + return cacheEntry; +}; + +const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { + const result: SourceMapData = {}; + + const processSources = ( + sources: string[], + contents: (string | null)[] | undefined, + ) => { + if (!contents) return; + + for (let i = 0; i < sources.length; i++) { + const filename = normalizeFileName(sources[i]); + const content = contents[i]; + if (filename && content && !result[filename]) { + result[filename] = content; + } + } + }; + + if (sourceMap.sections) { + for (const section of sourceMap.sections) { + processSources(section.map.sources, section.map.sourcesContent); + } + } + + processSources(sourceMap.sources, sourceMap.sourcesContent); + return result; +}; + +const hasJavaScriptType = (script: Element): boolean => { + const type = script.getAttribute("type")?.toLowerCase().trim(); + return ( + !type || + type === "text/javascript" || + type === "application/javascript" || + type === "module" + ); +}; + +const getScriptUrls = (): string[] => { + if (typeof document === "undefined") return []; + + const urls = new Set(); + + for (const script of Array.from(document.querySelectorAll("script[src]"))) { + const src = script.getAttribute("src"); + if (!src || !hasJavaScriptType(script)) continue; + + try { + const url = new URL(src, window.location.href).href; + if (url.split("?")[0].endsWith(".js")) { + urls.add(url); + } + } catch { + continue; + } + } + + return Array.from(urls); +}; + +const getSourceMapDataFromCache = (): SourceMapData => { + const result: SourceMapData = {}; + + for (const cacheEntry of sourceMapCache.values()) { + const sourceMap = resolveSourceMapFromCache(cacheEntry); + if (sourceMap) { + Object.assign(result, extractSourceMapData(sourceMap)); + } + } + + return result; +}; + +const getSourceMapDataForUrl = async (url: string): Promise => { + try { + const sourceMap = await getSourceMap(url, true); + return sourceMap ? extractSourceMapData(sourceMap) : {}; + } catch { + return {}; + } +}; + +const getSourceMapDataFromScripts = async (): Promise => { + const cachedUrls = new Set(sourceMapCache.keys()); + const uncachedUrls = getScriptUrls().filter((url) => !cachedUrls.has(url)); + + const results = await Promise.all( + uncachedUrls.map((url) => getSourceMapDataForUrl(url)), + ); + + const combined: SourceMapData = {}; + for (const data of results) { + Object.assign(combined, data); + } + return combined; +}; + +const getSourceMapData = async (): Promise => { + const cached = getSourceMapDataFromCache(); + const fromScripts = await getSourceMapDataFromScripts(); + return { ...cached, ...fromScripts }; +}; + +const getFileExtension = (filename: string): string => { + const match = filename.match(/\.([^.]+)$/); + return match ? match[1] : "js"; +}; + +const getSourceContext = ( + fileContent: string, + lineNumber: number, + contextLines: number, +): string => { + const lines = fileContent.split("\n"); + const startLine = Math.max(0, lineNumber - contextLines - 1); + const endLine = Math.min(lines.length, lineNumber + contextLines); + + const contextSnippet: string[] = []; + const maxLineNumWidth = String(endLine).length; + + for (let i = startLine; i < endLine; i++) { + const lineNum = String(i + 1).padStart(maxLineNumWidth, " "); + contextSnippet.push(`${lineNum} | ${lines[i]}`); + } + + return contextSnippet.join("\n"); +}; + export const checkIsNextProject = (): boolean => { if (typeof document === "undefined") return false; return Boolean( @@ -197,12 +347,13 @@ export const getElementContext = async ( element: Element, options: GetElementContextOptions = {}, ): Promise => { - const { maxLines = 3 } = options; + const { maxLines = DEFAULT_STACK_CONTEXT_LINES } = options; const stack = await getStack(element); const html = getHTMLPreview(element); if (hasSourceFiles(stack)) { const isNextProject = checkIsNextProject(); + const sourceMapData = isNextProject ? await getSourceMapData() : {}; const stackContext: string[] = []; if (stack) { @@ -219,7 +370,28 @@ export const getElementContext = async ( ); continue; } + if (frame.fileName && isSourceFile(frame.fileName)) { + const filename = normalizeFileName(frame.fileName); + const fileContent = sourceMapData[filename]; + + if (isNextProject && fileContent && frame.lineNumber) { + const extension = getFileExtension(filename); + const sourceContext = getSourceContext( + fileContent, + frame.lineNumber, + SOURCE_CONTEXT_LINES, + ); + const locationInfo = frame.columnNumber + ? `${filename}:${frame.lineNumber}:${frame.columnNumber}` + : `${filename}:${frame.lineNumber}`; + + stackContext.push( + `\n\n\`\`\`${extension}\n${sourceContext}\n\`\`\`\nat ${locationInfo}`, + ); + continue; + } + let line = "\n in "; const hasComponentName = frame.functionName && @@ -229,7 +401,7 @@ export const getElementContext = async ( line += `${frame.functionName} (at `; } - line += normalizeFileName(frame.fileName); + line += filename; // HACK: bundlers like vite mess up the line number and column number if (isNextProject && frame.lineNumber && frame.columnNumber) { diff --git a/packages/react-grab/src/core/source-map-grabber.ts b/packages/react-grab/src/core/source-map-grabber.ts deleted file mode 100644 index ac650e966..000000000 --- a/packages/react-grab/src/core/source-map-grabber.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { sourceMapCache, getSourceMap, normalizeFileName } from "bippy/source"; -import type { SourceMap } from "bippy/source"; - -interface SourceMapData { - [filename: string]: string; -} - -const isWeakRef = ( - value: SourceMap | WeakRef, -): value is WeakRef => - typeof WeakRef !== "undefined" && value instanceof WeakRef; - -const resolveSourceMapFromCache = ( - cacheEntry: SourceMap | WeakRef | null, -): SourceMap | null => { - if (!cacheEntry) return null; - if (isWeakRef(cacheEntry)) return cacheEntry.deref() ?? null; - return cacheEntry; -}; - -const extractSourceMapData = (sourceMap: SourceMap): SourceMapData => { - const result: SourceMapData = {}; - - const processSources = ( - sources: string[], - contents: (string | null)[] | undefined, - ) => { - if (!contents) return; - - for (let i = 0; i < sources.length; i++) { - const filename = normalizeFileName(sources[i]); - const content = contents[i]; - if (filename && content && !result[filename]) { - result[filename] = content; - } - } - }; - - if (sourceMap.sections) { - for (const section of sourceMap.sections) { - processSources(section.map.sources, section.map.sourcesContent); - } - } - - processSources(sourceMap.sources, sourceMap.sourcesContent); - return result; -}; - -const hasJavaScriptType = (script: Element): boolean => { - const type = script.getAttribute("type")?.toLowerCase().trim(); - return ( - !type || - type === "text/javascript" || - type === "application/javascript" || - type === "module" - ); -}; - -const getScriptUrls = (): string[] => { - if (typeof document === "undefined") return []; - - const urls = new Set(); - - for (const script of Array.from(document.querySelectorAll("script[src]"))) { - const src = script.getAttribute("src"); - if (!src || !hasJavaScriptType(script)) continue; - - try { - const url = new URL(src, window.location.href).href; - if (url.split("?")[0].endsWith(".js")) { - urls.add(url); - } - } catch { - continue; - } - } - - return Array.from(urls); -}; - -export const getSourceMapDataFromCache = (): SourceMapData => { - const result: SourceMapData = {}; - - for (const cacheEntry of sourceMapCache.values()) { - const sourceMap = resolveSourceMapFromCache(cacheEntry); - if (sourceMap) { - Object.assign(result, extractSourceMapData(sourceMap)); - } - } - - return result; -}; - -export const getSourceMapDataForUrl = async ( - url: string, - fetchFn?: (url: string) => Promise, -): Promise => { - try { - const sourceMap = await getSourceMap(url, true, fetchFn); - return sourceMap ? extractSourceMapData(sourceMap) : {}; - } catch { - return {}; - } -}; - -export const getSourceMapDataFromScripts = async ( - fetchFn?: (url: string) => Promise, -): Promise => { - const cachedUrls = new Set(sourceMapCache.keys()); - const uncachedUrls = getScriptUrls().filter((url) => !cachedUrls.has(url)); - - const results = await Promise.all( - uncachedUrls.map((url) => getSourceMapDataForUrl(url, fetchFn)), - ); - - const combined: SourceMapData = {}; - for (const data of results) { - Object.assign(combined, data); - } - return combined; -}; - -export const getSourceMapData = async ( - fetchFn?: (url: string) => Promise, -): Promise => { - const cached = getSourceMapDataFromCache(); - const fromScripts = await getSourceMapDataFromScripts(fetchFn); - return { ...cached, ...fromScripts }; -}; - -export type { SourceMapData }; From 01f09d4313eea8b45233ee7c51932584e2dc1e4d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 08:42:10 +0000 Subject: [PATCH 11/14] feat: Cache source map data to improve performance Co-authored-by: aiden --- packages/react-grab/src/core/context.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index 0f3f85608..f47173d7b 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -174,10 +174,22 @@ const getSourceMapDataFromScripts = async (): Promise => { return combined; }; +let sourceMapDataCache: SourceMapData | null = null; +let sourceMapDataPromise: Promise | null = null; + const getSourceMapData = async (): Promise => { - const cached = getSourceMapDataFromCache(); - const fromScripts = await getSourceMapDataFromScripts(); - return { ...cached, ...fromScripts }; + if (sourceMapDataCache) return sourceMapDataCache; + + if (!sourceMapDataPromise) { + sourceMapDataPromise = (async () => { + const cached = getSourceMapDataFromCache(); + const fromScripts = await getSourceMapDataFromScripts(); + sourceMapDataCache = { ...cached, ...fromScripts }; + return sourceMapDataCache; + })(); + } + + return sourceMapDataPromise; }; const getFileExtension = (filename: string): string => { From cbb234e730d298a8b361097ba93147b86315a932 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 9 Jan 2026 08:47:55 +0000 Subject: [PATCH 12/14] feat: Attach source context only once per element Co-authored-by: aiden --- packages/react-grab/src/core/context.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index f47173d7b..f22d4ea64 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -369,6 +369,7 @@ export const getElementContext = async ( const stackContext: string[] = []; if (stack) { + let hasAttachedSource = false; for (const frame of stack) { if (stackContext.length >= maxLines) break; @@ -387,7 +388,13 @@ export const getElementContext = async ( const filename = normalizeFileName(frame.fileName); const fileContent = sourceMapData[filename]; - if (isNextProject && fileContent && frame.lineNumber) { + if ( + !hasAttachedSource && + isNextProject && + fileContent && + frame.lineNumber + ) { + hasAttachedSource = true; const extension = getFileExtension(filename); const sourceContext = getSourceContext( fileContent, From 51022ed75ee0de737cc838f44c429c70cbb71277 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Wed, 28 Jan 2026 20:32:31 -0800 Subject: [PATCH 13/14] fix --- packages/react-grab/src/core/context.ts | 30 ++++++++++--------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/react-grab/src/core/context.ts b/packages/react-grab/src/core/context.ts index f22d4ea64..682c99787 100644 --- a/packages/react-grab/src/core/context.ts +++ b/packages/react-grab/src/core/context.ts @@ -369,7 +369,7 @@ export const getElementContext = async ( const stackContext: string[] = []; if (stack) { - let hasAttachedSource = false; + let didAttachSourceSnippet = false; for (const frame of stack) { if (stackContext.length >= maxLines) break; @@ -389,12 +389,12 @@ export const getElementContext = async ( const fileContent = sourceMapData[filename]; if ( - !hasAttachedSource && + !didAttachSourceSnippet && isNextProject && fileContent && frame.lineNumber ) { - hasAttachedSource = true; + didAttachSourceSnippet = true; const extension = getFileExtension(filename); const sourceContext = getSourceContext( fileContent, @@ -411,25 +411,19 @@ export const getElementContext = async ( continue; } - let line = "\n in "; - const hasComponentName = + const isValidSourceComponent = frame.functionName && checkIsSourceComponentName(frame.functionName); - if (hasComponentName) { - line += `${frame.functionName} (at `; - } - - line += filename; - // HACK: bundlers like vite mess up the line number and column number - if (isNextProject && frame.lineNumber && frame.columnNumber) { - line += `:${frame.lineNumber}:${frame.columnNumber}`; - } - - if (hasComponentName) { - line += `)`; - } + const locationSuffix = + isNextProject && frame.lineNumber && frame.columnNumber + ? `:${frame.lineNumber}:${frame.columnNumber}` + : ""; + + const line = isValidSourceComponent + ? `\n in ${frame.functionName} (at ${filename}${locationSuffix})` + : `\n in ${filename}${locationSuffix}`; stackContext.push(line); } From 9e2fe65f5cb65c2b20d025d9bc12d80a0bdd3ae8 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Thu, 29 Jan 2026 00:02:39 -0800 Subject: [PATCH 14/14] fix --- packages/react-grab/src/components/toolbar/index.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/react-grab/src/components/toolbar/index.tsx b/packages/react-grab/src/components/toolbar/index.tsx index 0b18acd80..1cef1ed9a 100644 --- a/packages/react-grab/src/components/toolbar/index.tsx +++ b/packages/react-grab/src/components/toolbar/index.tsx @@ -33,6 +33,12 @@ import { } from "../../constants.js"; import { formatShortcut } from "../../utils/format-shortcut.js"; import { freezeUpdates } from "../../utils/freeze-updates.js"; +import { + freezeGlobalAnimations, + unfreezeGlobalAnimations, + freezePseudoStates, + unfreezePseudoStates, +} from "../../utils/freeze-animations.js"; import { Tooltip } from "../tooltip.jsx"; interface ToolbarProps { @@ -974,7 +980,7 @@ export const Toolbar: Component = (props) => { class={cn( "grid transition-all duration-150 ease-out", isCollapsed() - ? "grid-cols-[0fr] opacity-0" + ? "grid-cols-[0fr] opacity-0 pointer-events-none" : "grid-cols-[1fr] opacity-100", )} > @@ -1010,6 +1016,8 @@ export const Toolbar: Component = (props) => { props.onSelectHoverChange?.(true); if (!unfreezeUpdatesCallback) { unfreezeUpdatesCallback = freezeUpdates(); + freezeGlobalAnimations(); + freezePseudoStates(); } }} onMouseLeave={() => { @@ -1018,6 +1026,8 @@ export const Toolbar: Component = (props) => { if (!props.isActive && !props.isContextMenuOpen) { unfreezeUpdatesCallback?.(); unfreezeUpdatesCallback = null; + unfreezeGlobalAnimations(); + unfreezePseudoStates(); } }} >