Repository navigation
Description
A React 18 interactive instrument gets React 19's JSX runtime whenever it does not import react@18.x itself. resolveJsxImportSource decides which React the JSX compiles against by scanning the source for /runtime/v1/react@<version> only. An instrument that renders with /runtime/v1/react-dom@18.x/client and uses JSX does not need to import react@18.x: the automatic JSX runtime makes the import unnecessary for any component without hooks. So it falls through to the react@19.x default. React 19's JSX runtime stamps elements with Symbol.for('react.transitional.element'), which React 18's reconciler rejects. The task then fails at render with "Objects are not valid as a React child", which is exactly the failure this derivation exists to prevent (packages/instrument-bundler/AGENTS.md). For the same reason, a source that imports react-dom@18.x and react@19.x is not flagged as mixing majors.
Where
packages/instrument-bundler/src/build.ts:14-38:
const DEFAULT_REACT_PACKAGE = 'react@19.x';
const REACT_PACKAGE_PATTERN = /['"]\/runtime\/v1\/(react@[^'"/]+)/g;
// ...
return `/runtime/v1/${packages.values().next().value ?? DEFAULT_REACT_PACKAGE}`;Reproduce
- Bundle an interactive instrument whose
index.tsximports onlycreateRootfrom/runtime/v1/react-dom@18.x/client.js, declaresconst App = () => <p>hi</p>;, and callscreateRoot(document.body).render(<App />)inrender. - Open it in
apps/web.
Actual: the bundle imports /runtime/v1/react@19.x/jsx-runtime.js next to /runtime/v1/react-dom@18.x/client.js (verified on main), and rendering fails with React 18's "Objects are not valid as a React child" error.
Expected: the JSX compiles against /runtime/v1/react@18.x, and a source that imports react-dom of one major with react of another is a build error.
Tests
packages/instrument-bundler/src/__tests__/build.test.ts: it('should compile JSX against the react paired with an imported react-dom, so a react-dom 18 instrument without hooks still renders'). Add a fixture under src/__tests__/repositories/ (as the package's AGENTS.md asks) that imports only react-dom@18.x/client. Also add it('should reject react-dom and react of different majors in one instrument').
e2e: none needed beyond the fixture, since no built-in instrument uses React 18.
Suggested fix
Match both packages, /['"]\/runtime\/v1\/react(?:-dom)?@([^'"/]+)/g, collect the version, and build react@<version> from it. The existing "more than one version" error then also covers a mismatched react/react-dom pair.
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e).REACT_PACKAGE_PATTERNis unchanged since f1865a3 added it, and no open PR touchesbuild.ts.One grammar, shared with #1803
The interpreter's
REACT_MODULE_PATTERN(packages/instrument-interpreter/src/imports.ts:6) already matchesreact(?:-dom)?@. The bundler's copy never got thereact-domarm, and that drift is this bug. #1803 plans to move the interpreter's pattern intoinstrument-utils, which the bundler does not depend on. So I'd put one parser for/runtime/v1/react(-dom)@<label>(/<subpath>)a level lower, in@opendatacapture/runtime-internal. It is the only first-party package that both the bundler and the interpreter already depend on. The API,react-coreandwebalready depend on it throughinstrument-bundlerorsubject-utils, soinstrument-utilscan take it as aworkspace:*dependency without adding anything to them.The parser returns
{ name, version, subpath }per match. The React rule keeps its JSX-runtime exemption on top of it, andresolveJsxImportSourcereads only the versions. Whichever of #1801 and #1803 lands first adds it. The export goes in bothsrc/index.jsand the hand-writtensrc/index.d.ts. Then update the "Four small helpers" line (packages/runtime-internal/AGENTS.md:3-4) and the test list (:32-33).Corrections to the suggested fix
name@versionin the error. If only the version is captured, the mixed case reportsfound 18.x, 19.xand the author can't tell which import is the odd one.found react-dom@18.x, react@19.xtells them.react-dom@<label>intoreact@<label>is only correct because each react-dom wrapper depends on the react wrapper with the same label (vendor/react-dom@18.x/package.json:23,vendor/react-dom@19.x/package.json:35).runtime/v1/test/vendor-pairing.test.tschecks that the peer resolves to the paired wrapper's copy. It does not check that the labels match. Say so in a comment where the rewrite happens.packages/instrument-bundler/AGENTS.md:34-41describes the scan as looking for/runtime/v1/react@<major>. Update it in the same commit.Tests
build.test.tscases as proposed. The new fixture sits next tointeractive-react18, which imports both packages, and is registered insrc/__tests__/repositories/index.ts. The existinginteractivefixture already imports onlyreact-dom@19.x/client. It avoided this bug only because the default happens to match. If the parser moves, its cases go inpackages/runtime-internal/src/__tests__/index.test.ts, andpackages/instrument-interpreter/src/__tests__/imports.test.tsguardsfindReactImportagainst regressions.testing/src/specs/playground.spec.ts:188-213opens a share link, bundles it with the real bundler (apps/playground/src/components/Viewer/Viewer.tsx:6) and asserts insideiframe[name="interactive-instrument"]. Add a share link with a singleindex.tsxthat imports onlycreateRootfrom/runtime/v1/react-dom@18.x/client.jsand renders a<p>. Click Begin and expect the text in the task frame. Name the file.tsx: the other share-link sources areindex.ts, and thetsloader does not parse JSX. This test fails onmaintoday.#1660, #1817 and #1825 also add tests to
playground.spec.ts, and #1825 adds one test tobuild.test.ts. Expect trivial rebases only.Triage note written with Claude Code. Code references point at
main@d86740e.