Files
Tommy D. Rossi 7d6e1f97ed Serve the browser example through Bun's dev server with React Fast Refresh
An edit to `examples/chat.tsx` now hot-updates the running page instead of
reloading it. Components swap in place and `useState` survives, so the GPUI
canvas, the composer text and the scroll position all stay where they were.
A reload re-downloads a ~19 MB Wasm module; this avoids that entirely.

`scripts/web.ts` drops the `Bun.build` step and the hand-written static file
map. It now registers `examples/web.html` as a Bun HTML route with
`development: { hmr: true }`, so Bun bundles the page, watches the graph and
applies the React Fast Refresh transform.

Fast Refresh never talks to a renderer directly. It goes through the React
DevTools global hook:

    Bun HMR runtime
      -> refreshRuntime.injectIntoGlobalHook(window)
    @gpuix/react reconciler.ts
      -> reconciler.injectIntoDevTools()
      -> hands the hook scheduleRefresh + setRefreshHandler
    react-refresh
      -> performReactRefresh() -> scheduleRefresh(root)

That injection already existed. What was missing was a bundler that runs the
transform, and a test proving the wiring holds.

`examples/web-chat.tsx` must NOT call `import.meta.hot.accept("./chat", ...)`.
Bun runs an importer's dependency-accept callback even when the imported module
already self-accepted for Fast Refresh, so the callback remounted the tree on
top of a successful refresh and wiped every hook. That looks exactly like Fast
Refresh being broken, and it is not.

The Wasm half is a singleton: `WebGpuixRenderer::init` fails with "GPUIX web is
already running" once its thread-local app exists, and gpui_web appends its own
canvas to <body>. Living in node_modules is not what protects it, because Bun
bundles it into the same client registry. What protects it is that Bun re-runs
only the changed module and then walks upward through its importers, so an
unchanged dependency stays evaluated and cached.

packages/native/.cargo/config.toml links with `--shared-memory`, so the
generated `WebAssembly.Memory` is `shared: true` and the document must be
cross-origin isolated. `Bun.serve` has no way to add headers to an HTML route
(oven-sh/bun#16873), so the bundled document is registered on a private path and
re-sent from `/` with COOP + COEP. Only the top-level document needs them:
`require-corp` constrains cross-origin subresources, and every asset the dev
server emits is same-origin.

`packages/react/src/__tests__/fast-refresh.test.tsx` mounts through the GPU test
renderer, clicks to set state, swaps the component family, and asserts the new
output keeps the old state. It fails when `injectIntoDevTools()` is removed.

This matters because the failure mode is silent. Without the injection there is
no error and no page reload: Bun still accepts the update and still calls
`performReactRefresh()`, which iterates zero mounted roots and schedules
nothing. The bundle changes and the painted UI stays stale.

Do not assert on the return value of `injectIntoDevTools()`. It ends in
`hook.checkDCE ? true : false` and react-refresh installs no `checkDCE`, so a
working injection still reports `false`.

- `--build-only` / `--rebuild` / auto-build-if-missing in scripts/web.ts, and
  the missing check tests both wasm-bindgen outputs, not just the `.wasm`
- `examples/tsconfig.json` gets `noEmit: true`; nothing there is ever compiled
  by tsc and a stray `tsc -p .` littered a `.js` next to every `.tsx`
- `@types/bun` as a root devDependency plus a `typeRoots` entry so
  `scripts/tsconfig.json` types `Bun.serve` and the `*.html` bundler import
- README documents that automation locator bounds are canvas pixels, not CSS
  pixels. Do not use `window.devicePixelRatio`; a tool can override the viewport
  scale after GPUI has sized its canvas. Use `canvas.width / canvas.clientWidth`

Validated in Chrome with playwriter: text edits and a module-level colour
constant edit both repaint the canvas with state preserved, and a revert
restores a byte-identical screenshot.

Session: ses_fc233909fffeHxAGjrf2jkWSet
2026-08-26 17:59:44 +02:00

18 lines
518 B
JSON

{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@gpuix/react",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
// Bun runs these files straight from source and `compile-chat.ts` bundles
// them. Nothing here is ever compiled by tsc, so an accidental `tsc -p .`
// must not litter the folder with .js next to every .tsx.
"noEmit": true
},
"include": ["./*.tsx"]
}