Skip to content

fix(shims): avoid jsx-runtime named exports in error boundary#45

Closed
liuxiaopai-ai wants to merge 2 commits intocloudflare:mainfrom
liuxiaopai-ai:fix/issue-25-jsx-runtime
Closed

fix(shims): avoid jsx-runtime named exports in error boundary#45
liuxiaopai-ai wants to merge 2 commits intocloudflare:mainfrom
liuxiaopai-ai:fix/issue-25-jsx-runtime

Conversation

@liuxiaopai-ai
Copy link
Copy Markdown
Contributor

Addresses #25.

  • Make error-boundary shim resilient to different React packaging by avoiding brittle named imports from react/jsx-runtime
  • Add regression test

@southpolesteve
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review

The core fix is correct — replacing JSX syntax with React.createElement() in error-boundary.tsx eliminates the implicit dependency on named exports from react/jsx-runtime, which is the root cause of #25. The test is solid and the approach is sound.

However, this fix is incomplete. The same vulnerability exists in 6 other .tsx shim files that still use JSX syntax:

File JSX usage
link.tsx <LinkStatusContext.Provider>, <a>
metadata.tsx 80+ JSX elements (<meta>, <title>, <link>, <>)
legacy-image.tsx <Image>
form.tsx <form>
document.tsx <html>, <head>, <body>, <div>
image.tsx <img>, <UnpicImage>

All of these will fail with the same SyntaxError: does not provide an export named 'jsx' under the same conditions described in #25. The fix should either be applied consistently across all shim files, or (better) the root cause should be addressed at the Vite config level so JSX compilation works regardless of user tsconfig.

Two inline comments below on the test.

Comment thread tests/error-boundary.test.ts Outdated
"../packages/vinext/src/shims/error-boundary.tsx",
);

await fs.writeFile(jsxRuntimePath, "export default {};\n");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This mock doesn't faithfully reproduce the failure scenario from #25. The issue is that react/jsx-runtime exposes jsx/jsxs/Fragment only via a default export (CJS interop), not as named exports. An empty default export (export default {};) won't trigger the same code path — if the module loaded without JSX, there's nothing that would call jsx() on this object.

A more faithful mock would export a default with the expected shape but no named exports, e.g.:

Suggested change
await fs.writeFile(jsxRuntimePath, "export default {};\n");
await fs.writeFile(jsxRuntimePath, "const jsx = () => null; const jsxs = () => null; const Fragment = 'div'; export default { jsx, jsxs, Fragment };\n");

That said, the current test still validates what matters (module loads without crashing), so this is a minor nit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5847f3d. Updated the mock to export a default object with jsx/jsxs/Fragment to better match the failure mode.

});

const mod = await server.ssrLoadModule(shimPath);
expect(typeof mod.ErrorBoundary).toBe("function");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider also asserting that instantiation/rendering works, not just that the exports are functions. For example, you could verify that React.createElement(mod.ErrorBoundary, { fallback: () => null }, 'child') doesn't throw. Right now, if someone accidentally broke the constructor signature, this test would still pass.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — also addressed in 5847f3d. Added an instantiation assertion for ErrorBoundary in the runtime-compat test, in addition to export-shape checks.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Feb 25, 2026

Review posted successfully. Here's a summary of the feedback:

The fix itself is correct — converting JSX to React.createElement() in error-boundary.tsx removes the brittle dependency on react/jsx-runtime named exports. The test passes and the regression test approach (spinning up a Vite server with a mock react/jsx-runtime) is reasonable.

Key concern: the fix is incomplete. Six other .tsx shim files (link.tsx, metadata.tsx, legacy-image.tsx, form.tsx, document.tsx, image.tsx) still use JSX syntax and will break under the exact same conditions. Either all shims need the same treatment, or the root cause should be fixed at the Vite plugin level (e.g., by ensuring the JSX transform is configured correctly regardless of user tsconfig).

Minor test feedback:

  1. The mock react/jsx-runtime (export default {};) doesn't faithfully represent the real failure scenario — a more realistic mock would export the jsx/jsxs/Fragment functions on the default export.
  2. The test only checks that the exports are functions; it could also verify that basic instantiation works.

github run

@liuxiaopai-ai
Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review — I pushed a follow-up commit to address the two inline test nits:

  • use a more realistic default-export-only react/jsx-runtime mock ({ jsx, jsxs, Fragment } on default export)
  • add an instantiation assertion for ErrorBoundary in the runtime-compat test

Commit: 5847f3d

On scope: agreed this pattern can exist in other shim .tsx files. I opened #135 to track the broader follow-up so this PR can stay focused on fixing #25's error-boundary path.

If you'd prefer, I can also expand this PR to cover all affected shim files directly.

@liuxiaopai-ai
Copy link
Copy Markdown
Contributor Author

Follow-up for the broader shim coverage is now up as #136 (linked to #135), so this PR can stay focused on the error-boundary path from #25.

@FredKSchott
Copy link
Copy Markdown
Collaborator

Thanks @liuxiaopai-ai!

Closing in favor of #155

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.

4 participants