Skip to content

Framework DOM source mapping#228

Open
aidenybai wants to merge 15 commits intomainfrom
cursor/framework-dom-source-mapping-f6fd
Open

Framework DOM source mapping#228
aidenybai wants to merge 15 commits intomainfrom
cursor/framework-dom-source-mapping-f6fd

Conversation

@aidenybai
Copy link
Owner

@aidenybai aidenybai commented Mar 8, 2026

Add framework-agnostic DOM-to-source resolution support for Solid, Vue, and Svelte to React Grab via a new plugin system, extending its ability to locate source code for non-React elements.


Open in Web Open in Cursor 


Summary by cubic

Adds framework-agnostic DOM→source mapping to react-grab via a new plugin system. In dev, Solid, Vue, and Svelte now resolve file, line, and component info; React and framework stack context lines are merged, deduped, and capped for clearer context.

  • New Features

    • Plugin hooks: resolveElementSource, resolveElementComponentName, resolveElementStackContext.
    • Bundled framework-source plugin:
      • Svelte: reads __svelte_meta for file/line/column.
      • Vue: uses data-v-inspector (line/column) or falls back to __vueParentComponent.type.__file and __name.
      • Solid: inspects delegated handlers ($$click, etc.), scans loaded Vite modules to infer file:line:column near handler bodies (falls back to generated positions).
    • Merges React and framework stack frames, dedupes, and respects a max line count (default 3). Used by selection labels, copy-to-clipboard, open-file, api.getSource, api.getStackContext, and snippet generation.
    • Exported frameworkSourcePlugin; added e2e coverage and a single multi-framework Vite playground under @react-grab/framework-playground (React, Vue, Solid, Svelte) for manual testing.
  • Migration

    • No changes for React-only projects.
    • Svelte: works in dev out of the box.
    • Vue: install vite-plugin-vue-inspector for line/column; otherwise file-only via runtime metadata.
    • Solid: works in dev without build plugins via runtime handler/source scanning.
    • These metadata paths are dev-only and typically removed in production builds.

Written for commit 07b224c. Summary will update on new commits.

cursoragent and others added 7 commits March 8, 2026 11:47
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor
Copy link

cursor bot commented Mar 8, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@vercel
Copy link
Contributor

vercel bot commented Mar 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-grab-website Ready Ready Preview, Comment Mar 9, 2026 10:26am

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 8, 2026

Open in StackBlitz

@react-grab/cli

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cli@228

grab

npm i https://pkg.pr.new/aidenybai/react-grab/grab@228

@react-grab/ami

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/ami@228

@react-grab/amp

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/amp@228

@react-grab/claude-code

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/claude-code@228

@react-grab/codex

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/codex@228

@react-grab/copilot

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/copilot@228

@react-grab/cursor

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cursor@228

@react-grab/droid

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/droid@228

@react-grab/gemini

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/gemini@228

@react-grab/opencode

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/opencode@228

react-grab

npm i https://pkg.pr.new/aidenybai/react-grab@228

@react-grab/relay

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/relay@228

@react-grab/utils

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/utils@228

commit: 07b224c

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 8, 2026

Open in StackBlitz

@react-grab/cli

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cli@228

grab

npm i https://pkg.pr.new/aidenybai/react-grab/grab@228

@react-grab/ami

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/ami@228

@react-grab/amp

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/amp@228

@react-grab/claude-code

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/claude-code@228

@react-grab/codex

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/codex@228

@react-grab/copilot

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/copilot@228

@react-grab/cursor

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cursor@228

@react-grab/droid

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/droid@228

@react-grab/gemini

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/gemini@228

@react-grab/opencode

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/opencode@228

react-grab

npm i https://pkg.pr.new/aidenybai/react-grab@228

@react-grab/relay

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/relay@228

@react-grab/utils

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/utils@228

commit: e996766

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@aidenybai aidenybai marked this pull request as ready for review March 9, 2026 07:41
@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 9, 2026

This run croaked 😵

The workflow encountered an error before any progress could be reported. Please check the link below for details.

Pullfrog  | Rerun failed job ➔View workflow run | Triggered by Pullfrogpullfrog.com𝕏

typeof value === "object" && value !== null;

const readString = (value: unknown): string | null =>
typeof value === "string" ? value : null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate isRecord/readString helpers across three new files

Low Severity

The isRecord and readString helper functions are identically defined in get-solid-source-info.ts, get-svelte-source-info.ts, and get-vue-source-info.ts. Additionally, readNumber is duplicated in the Svelte file. These could be extracted to a shared utility to reduce duplication and ensure consistent fixes.

Additional Locations (2)

Fix in Cursor Fix in Web

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 28 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/utils/get-vue-source-info.ts">

<violation number="1" location="packages/react-grab/src/utils/get-vue-source-info.ts:10">
P2: Fix `isRecord` to allow functions, ensuring Vue functional components do not lose their component names.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const VUE_PARENT_COMPONENT_PARENT_PROPERTY_NAME = "parent";

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Fix isRecord to allow functions, ensuring Vue functional components do not lose their component names.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/utils/get-vue-source-info.ts, line 10:

<comment>Fix `isRecord` to allow functions, ensuring Vue functional components do not lose their component names.</comment>

<file context>
@@ -0,0 +1,151 @@
+const VUE_PARENT_COMPONENT_PARENT_PROPERTY_NAME = "parent";
+
+const isRecord = (value: unknown): value is Record<string, unknown> =>
+  typeof value === "object" && value !== null;
+
+const readString = (value: unknown): string | null =>
</file context>
Fix with Cubic

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
lineNumber: sourceInfo.lineNumber,
componentName: sourceInfo.componentName,
};
});
Copy link
Contributor

@vercel vercel bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getFrameworkSourceInfoForApi is exported but never imported or used anywhere in the codebase

Fix on Vercel

Comment on lines +85 to +89
): Promise<string> =>
getResolvedFrameworkStackFrames(element).then((stackFrames) => {
const { maxLines = 3 } = options;
if (maxLines < 1) return "";
if (stackFrames.length < 1) return "";
Copy link
Contributor

@vercel vercel bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getFrameworkStackContext returns empty string "" when no framework stack frames are found, preventing plugin chain resolution by blocking other plugins from attempting resolution

Fix on Vercel

): Record<string, unknown> | null => {
if (!component) return null;
const componentType = component.type;
return isRecord(componentType) ? componentType : null;
Copy link
Contributor

@vercel vercel bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vue functional components lose name and file path metadata because isRecord check rejects functions

Fix on Vercel

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
}

return pluginRegistry.hooks.resolveElementSource(element);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Column number lost for React elements after refactoring

Medium Severity

resolveElementSourceInfo hardcodes columnNumber: null for React stack frame sources. The old code in notifyElementsSelected extracted columnNumber directly from frame.columnNumber, preserving it in the selection notification payload. Now that information is always lost because resolveSourceFromStack doesn't return column numbers and the wrapper doesn't compensate for that.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

const frameworkStack = await resolveFrameworkStack(element);

if (reactSource) return [reactSource, ...frameworkStack];
return frameworkStack;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React source double-counted in merged stack context

Medium Severity

resolveElementStack prepends the React source info into the returned array. Callers then format this via formatElementStack and merge it with getReactStackContext output. Because formatReactStackContext omits line/column numbers for non-Next.js projects while formatElementStack always includes them, mergeStackContext deduplication won't match the two representations of the same React source. This causes the React source to consume two of the three allowed maxLines, crowding out the framework-specific frames the feature is designed to surface.

Additional Locations (2)

Fix in Cursor Fix in Web

frameworkStackContext,
maxLines,
);
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugin source hooks registered but never invoked

Medium Severity

The frameworkSourcePlugin registers resolveElementSource, resolveElementComponentName, and resolveElementStackContext hooks, and the plugin registry exposes them via callHookResolveAsync. However, all core code paths (core/index.tsx, core/context.ts, agent/manager.ts, primitives.ts) import and call resolveElementSourceInfo, resolveElementComponentName, and resolveElementStack directly from ./source/index.js, completely bypassing the plugin registry hooks. These hooks are dead code that will never run.

Additional Locations (1)

Fix in Cursor Fix in Web

frameworkStackContext,
maxLines,
);
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triplicated React-framework stack merge logic across files

Low Severity

The same React + framework stack context merge pattern — calling getReactStackContext, resolveElementStack, formatElementStack, then conditionally calling mergeStackContext — is copy-pasted across three locations: frameworkSourcePlugin, the getStackContext public API, and getElementContext. A single shared helper would reduce maintenance burden and the risk of these diverging (the context.ts version already differs slightly in its conditional structure).

Additional Locations (2)

Fix in Cursor Fix in Web

locationParts.push(String(sourceInfo.columnNumber));
}
return locationParts.join(":");
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Column appended without line yields misleading location

Low Severity

formatSourceLocation independently checks lineNumber and columnNumber for null, so if lineNumber is null but columnNumber is not, the output becomes filePath:columnNumber — which looks identical to filePath:lineNumber and would mislead anyone navigating to that location. A columnNumber without a lineNumber is meaningless and the column part could be guarded on lineNumber being present.

Fix in Cursor Fix in Web

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 20 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/react-grab/src/primitives.ts">

<violation number="1" location="packages/react-grab/src/primitives.ts:50">
P2: `stackString` does not merge the full React stack context, resulting in a truncated stack trace for React components.</violation>
</file>

<file name="packages/react-grab/src/core/source/index.ts">

<violation number="1" location="packages/react-grab/src/core/source/index.ts:43">
P1: React source is double-counted in the merged stack context. `resolveElementStack` prepends the React source info into its result, but callers also independently fetch `reactStackContext` via `getReactStackContext`. Since `formatReactStackContext` omits line/column for non-Next.js projects while `formatElementStack` always includes them, `mergeStackContext` deduplication won't match the two representations of the same source. This causes the React frame to consume 2 of the 3 allowed `maxLines`, crowding out the framework-specific frames this feature is meant to surface.

Consider either removing the React source from `resolveElementStack` (so it only returns framework frames), or not fetching `reactStackContext` separately when using the merged flow.</violation>
</file>

<file name="packages/react-grab/src/core/plugins/framework-source.ts">

<violation number="1" location="packages/react-grab/src/core/plugins/framework-source.ts:15">
P2: The plugin hooks `resolveElementSource`, `resolveElementComponentName`, and `resolveElementStackContext` registered by `frameworkSourcePlugin` are dead code. All core code paths (`core/index.tsx`, `core/context.ts`, `agent/manager.ts`, `primitives.ts`) now import and call the resolution functions directly from `./source/index.js`, completely bypassing the plugin registry. These hooks will never be invoked, making the plugin registration misleading.</violation>
</file>

<file name="packages/react-grab/src/utils/format-element-stack.ts">

<violation number="1" location="packages/react-grab/src/utils/format-element-stack.ts:11">
P2: Column is appended without guarding on `lineNumber` being present. If `lineNumber` is `null` but `columnNumber` is not (e.g., `5`), the output becomes `file.tsx:5` — indistinguishable from `file.tsx:<line 5>`, which would mislead anyone navigating to that location. Guard the column append on `lineNumber` being non-null.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const reactSource = await resolveReactSourceInfo(element);
const frameworkStack = await resolveFrameworkStack(element);

if (reactSource) return [reactSource, ...frameworkStack];
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: React source is double-counted in the merged stack context. resolveElementStack prepends the React source info into its result, but callers also independently fetch reactStackContext via getReactStackContext. Since formatReactStackContext omits line/column for non-Next.js projects while formatElementStack always includes them, mergeStackContext deduplication won't match the two representations of the same source. This causes the React frame to consume 2 of the 3 allowed maxLines, crowding out the framework-specific frames this feature is meant to surface.

Consider either removing the React source from resolveElementStack (so it only returns framework frames), or not fetching reactStackContext separately when using the merged flow.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/core/source/index.ts, line 43:

<comment>React source is double-counted in the merged stack context. `resolveElementStack` prepends the React source info into its result, but callers also independently fetch `reactStackContext` via `getReactStackContext`. Since `formatReactStackContext` omits line/column for non-Next.js projects while `formatElementStack` always includes them, `mergeStackContext` deduplication won't match the two representations of the same source. This causes the React frame to consume 2 of the 3 allowed `maxLines`, crowding out the framework-specific frames this feature is meant to surface.

Consider either removing the React source from `resolveElementStack` (so it only returns framework frames), or not fetching `reactStackContext` separately when using the merged flow.</comment>

<file context>
@@ -0,0 +1,67 @@
+  const reactSource = await resolveReactSourceInfo(element);
+  const frameworkStack = await resolveFrameworkStack(element);
+
+  if (reactSource) return [reactSource, ...frameworkStack];
+  return frameworkStack;
+};
</file context>
Fix with Cubic

const stackString = await getStackContext(element);
const stack = (await getReactStack(element)) ?? [];
const elementStack = await resolveElementStack(element);
const stackString = formatElementStack(elementStack);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: stackString does not merge the full React stack context, resulting in a truncated stack trace for React components.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/primitives.ts, line 50:

<comment>`stackString` does not merge the full React stack context, resulting in a truncated stack trace for React components.</comment>

<file context>
@@ -31,23 +32,24 @@ export interface ReactGrabElementContext {
-  const stackString = await getStackContext(element);
+  const stack = (await getReactStack(element)) ?? [];
+  const elementStack = await resolveElementStack(element);
+  const stackString = formatElementStack(elementStack);
   const htmlPreview = getHTMLPreview(element);
-  const componentName = getComponentDisplayName(element);
</file context>
Fix with Cubic

export const frameworkSourcePlugin: Plugin = {
name: "framework-source",
hooks: {
resolveElementSource: (element) => resolveElementSourceInfo(element),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The plugin hooks resolveElementSource, resolveElementComponentName, and resolveElementStackContext registered by frameworkSourcePlugin are dead code. All core code paths (core/index.tsx, core/context.ts, agent/manager.ts, primitives.ts) now import and call the resolution functions directly from ./source/index.js, completely bypassing the plugin registry. These hooks will never be invoked, making the plugin registration misleading.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/core/plugins/framework-source.ts, line 15:

<comment>The plugin hooks `resolveElementSource`, `resolveElementComponentName`, and `resolveElementStackContext` registered by `frameworkSourcePlugin` are dead code. All core code paths (`core/index.tsx`, `core/context.ts`, `agent/manager.ts`, `primitives.ts`) now import and call the resolution functions directly from `./source/index.js`, completely bypassing the plugin registry. These hooks will never be invoked, making the plugin registration misleading.</comment>

<file context>
@@ -1,21 +1,39 @@
   name: "framework-source",
   hooks: {
-    resolveElementSource: (element) => getFrameworkSourceInfo(element),
+    resolveElementSource: (element) => resolveElementSourceInfo(element),
     resolveElementComponentName: (element) =>
-      getFrameworkComponentName(element),
</file context>
Fix with Cubic

if (sourceInfo.lineNumber !== null) {
locationParts.push(String(sourceInfo.lineNumber));
}
if (sourceInfo.columnNumber !== null) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Column is appended without guarding on lineNumber being present. If lineNumber is null but columnNumber is not (e.g., 5), the output becomes file.tsx:5 — indistinguishable from file.tsx:<line 5>, which would mislead anyone navigating to that location. Guard the column append on lineNumber being non-null.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/src/utils/format-element-stack.ts, line 11:

<comment>Column is appended without guarding on `lineNumber` being present. If `lineNumber` is `null` but `columnNumber` is not (e.g., `5`), the output becomes `file.tsx:5` — indistinguishable from `file.tsx:<line 5>`, which would mislead anyone navigating to that location. Guard the column append on `lineNumber` being non-null.</comment>

<file context>
@@ -0,0 +1,36 @@
+  if (sourceInfo.lineNumber !== null) {
+    locationParts.push(String(sourceInfo.lineNumber));
+  }
+  if (sourceInfo.columnNumber !== null) {
+    locationParts.push(String(sourceInfo.columnNumber));
+  }
</file context>
Fix with Cubic


return {
filePath: sourceFromStack.filePath,
lineNumber: sourceFromStack.lineNumber ?? null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveReactSourceInfo hardcodes columnNumber to null instead of extracting it from the stack frame

Fix on Vercel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants