Files
gpuix/example-app/web.ts
T
Tommy D. Rossi 3d759b21c3 Drop the VirtualList wrapper, and fix the starter after review
Oracle review of 7d45d36 plus a bug the review turned up.

**Remove `VirtualList`**

The windowed wrapper is gone from `@gpuix/react`. Windowing on the React side
is application state: pass `itemCount`, `estimatedItemHeight` and `windowStart`
to the host `<virtual-list>` and render only that slice.

A generic wrapper cannot know when to widen its own window. It moved `range`
only when the native list reported a new visible range, so growing `itemCount`
without a scroll left the extra rows unmounted forever. A filter does exactly
that. The todo example hit it immediately: switching from a 2-row view to a
6-row view painted 2 rows.

    view-done    ok
    view-starred ok
    view-inbox   FAILED: "Ship it" never mounted
    view-today   ok

`VirtualList` was never published, so the pending
`.changeset/windowed-virtual-list.md` now describes the native `itemCount` plus
`windowStart` path instead. README and AGENTS.md carry a worked example and the
rule that no wrapper should come back.

**Review fixes**

`web:build` ran `bun run --cwd ../packages/native build:web`, a path that does
not exist once the folder is copied, while the README claimed nothing depended
on the repository. The script is gone. The browser renderer ships inside
`@gpuix/native`; `web.ts` explains the repository-only build in its failure
message.

`example-app/README.md` claimed `<virtual-list>` builds only visible rows. GPUI
builds only visible native rows, but React still mounts every child. Corrected.

`noImplicitAny: false` is dropped from the Quickstart tsconfig and from
`example-app/tsconfig.json`. The example passes under full `strict`, so the
starter should not hand out weaker checking.

The lockfile finding resolved itself: `examples/package.json` on main now
declares `cbor-x` and `msgpackr`, and `bun install` in a clean checkout of HEAD
produces no diff.

**Verified**

`packages/react` 302 tests pass. `example-app` typechecks under full strict,
lints clean, and all four filter views paint through the automation client.

Session: ses_fc1c928b0ffeeHIfdRpxtE0QaV
2026-08-26 16:46:11 +02:00

69 lines
2.2 KiB
TypeScript

/**
* Bundle app.tsx for the browser and serve it.
*
* The Wasm renderer uses shared memory, so every response needs the two
* cross-origin isolation headers. A production host must send the same ones.
*
* `@gpuix/native` resolves to its `browser` entry during a browser build. That
* entry imports the Wasm file, so Bun copies it next to the bundle. Nothing
* here has to know where the package lives.
*/
import { existsSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = path.dirname(fileURLToPath(import.meta.url))
const OUT = path.join(ROOT, 'web-dist')
const bundle = await Bun.build({
entrypoints: [path.join(ROOT, 'app.tsx')],
outdir: OUT,
target: 'browser',
format: 'esm',
naming: 'app.js',
throw: false,
})
if (!bundle.success) {
for (const message of bundle.logs) console.error(message)
// Inside the GPUIX repository `packages/native/wasm/` is gitignored, so the
// browser renderer has to be built once. A copied app never hits this: the
// published `@gpuix/native` ships the Wasm files.
console.error(
'\nweb: if the Wasm renderer is missing and this is the GPUIX repository,' +
' run `bun run build:web` in packages/native'
)
process.exit(1)
}
const ISOLATION = {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
}
function contentType(pathname: string): string {
if (pathname.endsWith('.wasm')) return 'application/wasm'
if (pathname.endsWith('.js') || pathname.endsWith('.mjs')) return 'text/javascript'
return 'text/html'
}
const server = Bun.serve({
port: Number(process.env.PORT || 4173),
fetch(request) {
const { pathname } = new URL(request.url)
if (pathname === '/favicon.ico') {
return new Response(null, { status: 204, headers: ISOLATION })
}
const file =
pathname === '/' ? path.join(ROOT, 'index.html') : path.join(OUT, pathname.slice(1))
if (!file.startsWith(ROOT) || !existsSync(file)) {
return new Response('Not found', { status: 404 })
}
return new Response(Bun.file(file), {
headers: { ...ISOLATION, 'Content-Type': contentType(pathname) },
})
},
})
console.log(`web: http://localhost:${server.port}`)