Skip to content

feat(core): propagate inspection paths through React component roots#513

Merged
zh-lx merged 1 commit into
mainfrom
feature/react-component-attribute
Apr 2, 2026
Merged

feat(core): propagate inspection paths through React component roots#513
zh-lx merged 1 commit into
mainfrom
feature/react-component-attribute

Conversation

@zh-lx
Copy link
Copy Markdown
Owner

@zh-lx zh-lx commented Apr 2, 2026

close #505

Summary

This PR improves JSX transformation for React-style components so root nodes
can preserve and propagate data-insp-path from the component call site
instead of always falling back to the component definition location.

What changed

  • detect React component functions and class components more accurately
  • inject dynamic data-insp-path handling for component root JSX elements
  • support component root createElement(...) and createPortal(...) returns
  • resolve root render targets through conditionals, logical expressions,
    arrays, fragments, identifiers, and assignments
  • handle props path propagation for:
    • standard props identifiers
    • destructured props
    • destructured props with defaults
    • components without explicit params
  • improve JSX tag name handling for member expressions and namespaced tags
  • avoid duplicate injection when a path attribute or path prop already exists
  • export helper utilities and add declaration updates needed by the expanded
    test coverage

Tests

  • expand transform-jsx coverage across component detection, root target
    collection, createElement injection, class components, export-default
    anonymous components, and edge-case fallbacks
  • verified with:
pnpm vitest run test/core/server/transform/transform-jsx.test.ts --coverage

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Propagate inspection paths through React component roots

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Propagate inspection paths from component call sites through React component roots
• Support dynamic path injection for function and class components with prop propagation
• Handle createElement() and createPortal() calls with root target collection
• Resolve root render targets through conditionals, logical expressions, arrays, fragments, and
  identifiers
• Expand test coverage with 100+ new test cases for component detection and path injection
Diagram
flowchart LR
  A["Component Detection<br/>Functions & Classes"] -->|"Identify component roots"| B["Root Target Collection<br/>JSX/createElement/Portals"]
  B -->|"Resolve through expressions"| C["Dynamic Path Injection<br/>Props Propagation"]
  C -->|"Inject attributes"| D["Inspection Path Attributes<br/>Call site or definition fallback"]
  E["Static JSX Elements"] -->|"Direct injection"| D
Loading

Grey Divider

File Changes

1. packages/core/src/server/transform/transform-jsx.ts ✨ Enhancement +879/-13

Core JSX transformation with component root path propagation

• Added component function and class detection with uppercase name validation
• Implemented root target collection through conditionals, logical expressions, arrays, fragments,
 and identifier tracking
• Added dynamic path injection for component roots with prop propagation support
• Implemented createElement() and createPortal() call handling with path injection
• Added support for destructured props, assignment patterns, and implicit props parameters
• Implemented JSX member expression and namespaced tag name handling
• Added helper functions for expression unwrapping, binding resolution, and path attribute detection
• Exported test utilities and helper functions for expanded test coverage

packages/core/src/server/transform/transform-jsx.ts


2. packages/core/types/server/transform/transform-jsx.d.ts 📝 Documentation +29/-0

TypeScript declarations for JSX transformation helpers

• Added RootTarget type definition for JSX and createElement nodes
• Added type declarations for internal helper functions used in component detection and path
 injection
• Exported injectCreateElementPath, getJsxNodeName, getCallExpressionName, and
 getExpressionName functions
• Exported __TEST__ object with test utility functions for internal helper validation

packages/core/types/server/transform/transform-jsx.d.ts


3. test/core/server/transform/transform-jsx.test.ts 🧪 Tests +892/-11

Comprehensive test coverage for component path propagation

• Updated existing tests to reflect dynamic path injection behavior for component roots
• Added 100+ new test cases covering function expressions, arrow functions, and class components
• Added tests for component owner name detection and parameter path expression handling
• Added comprehensive createElement() call tests with various prop configurations
• Added tests for conditional, logical, and sequence expressions as root returns
• Added tests for identifier tracking, variable assignments, and cyclic binding detection
• Added tests for createPortal() support, JSX member expressions, and namespaced tags
• Added tests for export default anonymous components and edge cases
• Added tests for helper function internals and boundary conditions

test/core/server/transform/transform-jsx.test.ts


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented Apr 2, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Null props assign crash 🐞 Bug ≡ Correctness
Description
injectCreateElementPath rewrites any non-object props argument into `Object.assign({}, <props>,
{"data-insp-path": ...}), which will throw at runtime when the source is null/undefined` (e.g.
React.createElement("div", null, ...)). This affects both component-root and non-root
createElement calls because the transform runs on every CallExpression it recognizes as
createElement.
Code

packages/core/src/server/transform/transform-jsx.ts[R775-810]

+  const propsArgument = node.arguments?.[1];
+  if (hasCreateElementPathProperty(propsArgument)) {
+    return;
+  }
+
+  if (!propsArgument) {
+    const typeArgument = node.arguments?.[0];
+    if (!typeArgument?.end) {
+      return;
+    }
+    s.appendLeft(
+      typeArgument.end,
+      `, { ${JSON.stringify(PathName)}: ${pathExpression} }`,
+    );
+    return;
+  }
+
+  if (propsArgument.type === 'ObjectExpression') {
+    s.prependLeft(
+      propsArgument.start + 1,
+      `${JSON.stringify(PathName)}: ${pathExpression}${
+        propsArgument.properties.length ? ', ' : ''
+      }`,
+    );
+    return;
+  }
+
+  s.overwrite(
+    propsArgument.start,
+    propsArgument.end,
+    `Object.assign({}, ${content.slice(
+      propsArgument.start,
+      propsArgument.end,
+    )}, { ${JSON.stringify(PathName)}: ${pathExpression} })`,
+  );
+}
Evidence
injectCreateElementPath only treats a *missing* second argument as “no props”; when a second
argument exists but is a NullLiteral node, it goes through the non-ObjectExpression branch and is
overwritten to Object.assign({}, null, {...}), which throws. Since the main traversal calls
injectStaticCreateElementPath for every CallExpression, any recognized `createElement(..., null,
...)` in transformed files will be rewritten into this crashing form.

packages/core/src/server/transform/transform-jsx.ts[97-133]
packages/core/src/server/transform/transform-jsx.ts[769-810]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`injectCreateElementPath()` rewrites non-object `props` args as `Object.assign({}, <props>, {"data-insp-path": ...})`. If `<props>` is `null` or `undefined` (very common: `React.createElement("div", null, ...)`), the transformed code throws `TypeError: Cannot convert undefined or null to object`.

## Issue Context
This transform runs broadly over JS/TS files (via toolchain plugins) and now rewrites createElement calls, so this can crash real apps that use `createElement(..., null, ...)`.

## Fix Focus Areas
- packages/core/src/server/transform/transform-jsx.ts[769-810]

## Fix guidance
- Special-case `NullLiteral` (and optionally an `Identifier` named `undefined`) as “no props”: replace the argument with a new object literal containing the path property.
- For general non-object expressions, avoid null/undefined crashes by wrapping the expression with a nullish fallback, e.g. `Object.assign({}, (${orig} ?? {}), { ... })` (or equivalent) so runtime null/undefined do not throw.
- Add/extend a unit test that asserts `React.createElement("div", null, ...)` is transformed into non-throwing code (no `Object.assign({}, null, ...)`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Non-React createElement rewritten 🐞 Bug ≡ Correctness
Description
The transform treats any callee whose name/property is createElement as React createElement, so
calls like document.createElement('div') will also be rewritten with injected
props/Object.assign wrappers. Because the Vite/Esbuild integrations classify all JS/TS files as
fileType='jsx', this change can mutate non-React code across a project.
Code

packages/core/src/server/transform/transform-jsx.ts[R124-133]

+      if (node.type === 'CallExpression') {
+        injectStaticCreateElementPath(
+          node,
+          s,
+          content,
+          filePath,
+          dynamicInjectedCreateElementCalls,
+          escapeTags,
+        );
      }
Evidence
injectStaticCreateElementPath is invoked for every CallExpression during traversal, and
isCreateElementCall matches purely by callee name/property (getCallExpressionName returns the
member property.name). The Vite/Esbuild plugins set fileType='jsx' for any JS/TS extension via
isJsTypeFile, so this createElement rewriting can run on ordinary DOM code (which commonly
contains document.createElement).

packages/core/src/server/transform/transform-jsx.ts[124-133]
packages/core/src/server/transform/transform-jsx.ts[716-739]
packages/core/src/server/transform/transform-jsx.ts[851-885]
packages/vite/src/index.ts[120-130]
packages/esbuild/src/index.ts[70-85]
packages/core/src/shared/utils.ts[32-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The transform currently rewrites any `.createElement(...)` call (or `createElement(...)`) based solely on the callee name/property. This will incorrectly rewrite non-React APIs such as `document.createElement(...)`, potentially changing runtime behavior and, combined with the props-injection implementation, can even introduce runtime throws.

## Issue Context
Tooling integrations (e.g., Vite/Esbuild) run the JSX transform over *all* JS/TS files (not just files containing JSX), so the false-positive surface is large.

## Fix Focus Areas
- packages/core/src/server/transform/transform-jsx.ts[124-133]
- packages/core/src/server/transform/transform-jsx.ts[851-885]
- packages/vite/src/index.ts[120-130]
- packages/esbuild/src/index.ts[70-85]

## Fix guidance
- Tighten `isCreateElementCall`/`isCreatePortalCall` detection. Options include:
 - Only match `React.createElement` / `ReactDOM.createPortal` (MemberExpression where object resolves to `React`/`ReactDOM`).
 - Allow bare `createElement`/`createPortal` only if the identifier is bound to a known React import in the current file (inspect `path.scope.getBinding()` and its import source).
 - Explicitly exclude DOM patterns like `document.createElement` by checking the member expression object.
- Add regression tests demonstrating that `document.createElement('div')` is NOT rewritten by `transformJsx`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Injected binding name collision 🐞 Bug ☼ Reliability
Description
The transform injects fixed parameter bindings __codeInspectorProps/__codeInspectorPath into
component functions without checking for existing bindings, which can introduce duplicate bindings
(syntax error) or shadow user variables. This can happen when adding an implicit props param or when
injecting into destructured props patterns.
Code

packages/core/src/server/transform/transform-jsx.ts[R388-430]

+function getPropagatedPathExpression(
+  path: any,
+  s: MagicString,
+  content: string,
+) {
+  const firstParam = path.node.params?.[0];
+  if (!firstParam) {
+    // 无参组件需要补一个隐式 props 形参,才能读取调用点传进来的路径。
+    const insertPosition = getEmptyParamsInsertPosition(path.node, content);
+    if (insertPosition == null) {
+      return '';
+    }
+    s.prependLeft(insertPosition, ImplicitPropsBinding);
+    return `${ImplicitPropsBinding} && ${ImplicitPropsBinding}[${JSON.stringify(PathName)}]`;
+  }
+
+  return getParameterPathExpression(firstParam, s);
+}
+
+function getParameterPathExpression(param: any, s: MagicString): string {
+  if (param.type === 'Identifier') {
+    return `${param.name} && ${param.name}[${JSON.stringify(PathName)}]`;
+  }
+
+  if (param.type === 'AssignmentPattern') {
+    return getParameterPathExpression(param.left, s);
+  }
+
+  if (param.type === 'ObjectPattern') {
+    const existingBinding = getObjectPatternPathBinding(param);
+    if (existingBinding) {
+      return existingBinding;
+    }
+
+    // 解构 props 时补出 data-insp-path,尽量不改变用户原有写法。
+    s.prependLeft(
+      param.start + 1,
+      `${JSON.stringify(PathName)}: ${PropagatedPathBinding}${
+        param.properties.length ? ', ' : ''
+      }`,
+    );
+    return PropagatedPathBinding;
+  }
Evidence
The injected identifiers are hard-coded constants and are inserted directly into source via
MagicString (prependLeft) without consulting path.scope or checking existing names in the
parameter pattern/scope. If user code already declares either identifier in the same function (as a
param binding or block-scoped binding), the output can become invalid JS or change variable
resolution.

packages/core/src/server/transform/transform-jsx.ts[13-18]
packages/core/src/server/transform/transform-jsx.ts[388-405]
packages/core/src/server/transform/transform-jsx.ts[407-433]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The transform injects fixed identifier names (`__codeInspectorProps`, `__codeInspectorPath`) into user code. If those names already exist in the same scope/pattern, the transform can create duplicate bindings (parse error) or shadow user values.

## Issue Context
Babel scope info is available on traversal paths (`path.scope`), but the current code does not use it to avoid collisions.

## Fix Focus Areas
- packages/core/src/server/transform/transform-jsx.ts[13-18]
- packages/core/src/server/transform/transform-jsx.ts[388-405]
- packages/core/src/server/transform/transform-jsx.ts[407-433]

## Fix guidance
- Before injecting, check for binding collisions via `path.scope.hasBinding(...)` / `hasOwnBinding(...)` and for destructuring, ensure the generated binding name isn’t already present in the object pattern.
- If a collision exists, choose a unique variant (e.g., `__codeInspectorProps1`, `__codeInspectorPath1`) and use that consistently in the injected expression.
- Add a unit test where the user already declares `__codeInspectorProps`/`__codeInspectorPath` and assert the transform still produces valid code.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (9dab80d) to head (1b22e93).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##              main      #513    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           23        23            
  Lines         2821      3561   +740     
  Branches       695       927   +232     
==========================================
+ Hits          2821      3561   +740     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +775 to +810
const propsArgument = node.arguments?.[1];
if (hasCreateElementPathProperty(propsArgument)) {
return;
}

if (!propsArgument) {
const typeArgument = node.arguments?.[0];
if (!typeArgument?.end) {
return;
}
s.appendLeft(
typeArgument.end,
`, { ${JSON.stringify(PathName)}: ${pathExpression} }`,
);
return;
}

if (propsArgument.type === 'ObjectExpression') {
s.prependLeft(
propsArgument.start + 1,
`${JSON.stringify(PathName)}: ${pathExpression}${
propsArgument.properties.length ? ', ' : ''
}`,
);
return;
}

s.overwrite(
propsArgument.start,
propsArgument.end,
`Object.assign({}, ${content.slice(
propsArgument.start,
propsArgument.end,
)}, { ${JSON.stringify(PathName)}: ${pathExpression} })`,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Null props assign crash 🐞 Bug ≡ Correctness

injectCreateElementPath rewrites any non-object props argument into `Object.assign({}, <props>,
{"data-insp-path": ...}), which will throw at runtime when the source is null/undefined` (e.g.
React.createElement("div", null, ...)). This affects both component-root and non-root
createElement calls because the transform runs on every CallExpression it recognizes as
createElement.
Agent Prompt
## Issue description
`injectCreateElementPath()` rewrites non-object `props` args as `Object.assign({}, <props>, {"data-insp-path": ...})`. If `<props>` is `null` or `undefined` (very common: `React.createElement("div", null, ...)`), the transformed code throws `TypeError: Cannot convert undefined or null to object`.

## Issue Context
This transform runs broadly over JS/TS files (via toolchain plugins) and now rewrites createElement calls, so this can crash real apps that use `createElement(..., null, ...)`.

## Fix Focus Areas
- packages/core/src/server/transform/transform-jsx.ts[769-810]

## Fix guidance
- Special-case `NullLiteral` (and optionally an `Identifier` named `undefined`) as “no props”: replace the argument with a new object literal containing the path property.
- For general non-object expressions, avoid null/undefined crashes by wrapping the expression with a nullish fallback, e.g. `Object.assign({}, (${orig} ?? {}), { ... })` (or equivalent) so runtime null/undefined do not throw.
- Add/extend a unit test that asserts `React.createElement("div", null, ...)` is transformed into non-throwing code (no `Object.assign({}, null, ...)`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +124 to 133
if (node.type === 'CallExpression') {
injectStaticCreateElementPath(
node,
s,
content,
filePath,
dynamicInjectedCreateElementCalls,
escapeTags,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Non-react createelement rewritten 🐞 Bug ≡ Correctness

The transform treats any callee whose name/property is createElement as React createElement, so
calls like document.createElement('div') will also be rewritten with injected
props/Object.assign wrappers. Because the Vite/Esbuild integrations classify all JS/TS files as
fileType='jsx', this change can mutate non-React code across a project.
Agent Prompt
## Issue description
The transform currently rewrites any `.createElement(...)` call (or `createElement(...)`) based solely on the callee name/property. This will incorrectly rewrite non-React APIs such as `document.createElement(...)`, potentially changing runtime behavior and, combined with the props-injection implementation, can even introduce runtime throws.

## Issue Context
Tooling integrations (e.g., Vite/Esbuild) run the JSX transform over *all* JS/TS files (not just files containing JSX), so the false-positive surface is large.

## Fix Focus Areas
- packages/core/src/server/transform/transform-jsx.ts[124-133]
- packages/core/src/server/transform/transform-jsx.ts[851-885]
- packages/vite/src/index.ts[120-130]
- packages/esbuild/src/index.ts[70-85]

## Fix guidance
- Tighten `isCreateElementCall`/`isCreatePortalCall` detection. Options include:
  - Only match `React.createElement` / `ReactDOM.createPortal` (MemberExpression where object resolves to `React`/`ReactDOM`).
  - Allow bare `createElement`/`createPortal` only if the identifier is bound to a known React import in the current file (inspect `path.scope.getBinding()` and its import source).
  - Explicitly exclude DOM patterns like `document.createElement` by checking the member expression object.
- Add regression tests demonstrating that `document.createElement('div')` is NOT rewritten by `transformJsx`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@zh-lx zh-lx merged commit 2d51f9d into main Apr 2, 2026
4 checks passed
@zh-lx zh-lx deleted the feature/react-component-attribute branch April 2, 2026 13:26
@jeremytenjo
Copy link
Copy Markdown

Thank you @zh-lx, you are the best.

@zh-lx
Copy link
Copy Markdown
Owner Author

zh-lx commented Apr 2, 2026

Thank you @zh-lx, you are the best.

Yor're welcome, also thanks for your feedback.

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.

Show component name in 'Click node to locate' popup

2 participants