Skip to content

feat(bolt-vite-react-ts): upgrade to vite 8 - #132

Draft
sheepsteak wants to merge 2 commits into
mainfrom
chris/bolt-vite-react-ts-vite-8
Draft

sheepsteak wants to merge 2 commits into
mainfrom
chris/bolt-vite-react-ts-vite-8

Conversation

@sheepsteak

@sheepsteak sheepsteak commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Problem

bolt-vite-react-ts is still on Vite 5 (vite ^5.4.2, @vitejs/plugin-react ^4.3.1), while bolt-slides moved to Vite 8 in #121. The previous attempt (#120) bundled Vite 8 with React 19, Tailwind 4, TypeScript 6, and removing ESLint, and was reverted in #123.

Solution

Upgrade only the build tooling: vite to ^8.3.3 and @vitejs/plugin-react to ^6.1.2 (the v6 line targets Vite 8). React 18, Tailwind 3 with PostCSS, ESLint, TypeScript, and vite.config.ts are unchanged. The existing resolve.alias and optimizeDeps.exclude options still work in Vite 8.

Implementation Details

  • Lockfile: regenerated with the repo's pinned npm 10.2.3, and npm ci passes with it. Only the Vite toolchain moved: Babel, esbuild, and Rollup drop out, Rolldown and Lightning CSS come in, and postcss goes from 8.4 to 8.5 because Vite 8 requires ^8.5.28.
  • removeFileHash: reapplies the fix from feat(bolt-vite-react-ts): modernize toolchain and remove eslint #120. Vite hashes are base64url and can contain -, so the regex now strips from the first - up to the file extension.
  • Assets snapshot: sorted before comparing, because readdir in the WebContainer returned index.js before index.css. Older Vite 5 checkouts of the template fail the same way, so the ordering isn't specific to Vite 8.
  • Dev preview test: the old assertions never checked the preview. preview.getByText() and preview.getByRole() from @webcontainer/test return locators scoped to the <iframe> element without entering it, and awaiting them doesn't wait; a probe resolved in 0ms with no dev server running. The preview is cross-origin, so in-page queries such as expect.element() can't see into it either. The test now waits from Playwright's side through page.frameLocator(), using a small waitForVisible browser command in vitest.config.ts.
  • setup() snapshot: with a real assertion the dev test failed on both Vite 5 and Vite 8. Restoring the setup() snapshot drops the executable bit from node_modules/vite/bin/vite.js (-rwxrwxr-x after npm install, -rw-r--r-- after restore), so npm run dev exited with spawn vite EACCES. The test now mounts and installs in beforeEach instead. The other test files (angular, astro-shadcn, bolt-expo, nextjs, slidev, sveltekit) use the same setup() and preview.getBy* pattern, so their dev tests aren't checking the preview either. That's left for a follow-up.
  • .bolt/prompt: unchanged. The Vite 8 note feat(bolt-vite-react-ts): modernize toolchain and remove eslint #120 added was about resolve.tsconfigPaths, which this PR doesn't adopt.
  • Other bolt-* templates: bolt-slides is already on Vite 8, and bolt-expo (Metro) and bolt-remotion (Webpack) don't use Vite. bolt-qwik stays on Vite 5: Qwik 1.x, including the latest 1.20.2, declares vite: ">=5 <8", so npm install fails with ERESOLVE, and even with overrides the client build fails under Rolldown. Vite 8 needs the Qwik 2 migration (@qwik.dev/core, currently 2.0.0-rc.2), which can wait until Qwik 2 is stable.

Performance in WebContainer

Vite 8 is slower than Vite 5 on a cold dev server start in WebContainer: the first styled preview takes about 6.5s longer. HMR and production builds are about the same.

Phase (median) Vite 5.4.8 Vite 8.3.3 Vite 8.3.3 + @rolldown/binding-wasm32-wasi
npm install 2.2s 2.2s 2.5s
Cold npm run dev to styled first render 2.4s 8.9s 3.3s
Vite "ready in" on cold start 0.66s 6.97s 1.18s
HMR (new JSX plus a new Tailwind class) 47ms 56ms 61ms
Dev server restart to styled render 1.4s 2.9s 3.3s
npm run build (wall clock) 1.7s 1.8s 1.9s

Method: @webcontainer/test with Vitest browser mode in headless Chromium on an Apple Silicon Mac. Each run launches a fresh Chromium and boots a fresh WebContainer. The first two columns have 5 runs each. The third column comes from a separate batch of 4 runs, interleaved with 4 Vite 8 control runs that matched the first batch within 6%. "Styled first render" is a Playwright wait inside the preview iframe until the starter text is present and Tailwind's bg-gray-100 is the computed background. Running 4 rounds in one browser session gives the same pattern, so the cost comes back with every WebContainer boot, not just in a cold browser.

Cause. In WebContainer, npm installs Rolldown's native @rolldown/binding-linux-x64-gnu, which can't load there. Rolldown 1.2.13 no longer lists @rolldown/binding-wasm32-wasi in its optionalDependencies. So its binding loader falls back to a WebContainer-specific branch that runs pnpm i @rolldown/binding-wasm32-wasi@1.2.13 into /tmp/rolldown-1.2.13 the first time Rolldown is imported. That's 7 packages, including an 11.4 MB WASM binding. In a fresh WebContainer the first import('rolldown') took 6.2s (pnpm reported "Done in 5.2s"), and the next two took 356ms and 335ms. /tmp doesn't carry over to a new WebContainer, so every new project pays the download.

Possible mitigation (not in this PR). Rolldown's loader checks node_modules/@rolldown/binding-wasm32-wasi before it falls back to the download. So adding "@rolldown/binding-wasm32-wasi": "1.2.13" to devDependencies gives the third column: cold start drops from 8.9s to 3.3s, install takes about 0.2s longer, and the download ran in 0 of 4 runs (4 of 4 without it). The costs:

  • The pin has to match the Rolldown version Vite resolves (vite@8.3.3 depends on rolldown ~1.2.11), and the loader doesn't check the version unless NAPI_RS_ENFORCE_VERSION_CHECK is set.
  • It adds 11.4 MB to node_modules on every machine, unused outside WebContainer.

Providing or caching the Rolldown WASM binding in WebContainer itself would fix every Vite 8 project without pins. Even with either fix, Vite 8 stays about 0.9s slower on cold start and about 2x slower on dev server restart than Vite 5. That remaining cost comes from running Rolldown and Oxc as WASM.

Test plan

  • npm ci, npm run build, npm run typecheck, tsc --noEmit -p tsconfig.node.json, and npm run lint pass in bolt-vite-react-ts
  • Dev server starts on Vite 8.3.3 and resolves @/ imports
  • test/bolt-vite-react-ts.test.ts passes in Chromium: build in a WebContainer, and the dev server preview renders and updates on edit, asserted inside the iframe
  • The new preview wait times out when the expected text is missing, both with no dev server and for a heading that never renders
  • WebContainer timing compared against Vite 5 (see above)
  • Firefox test project
  • New Bolt project from this template

Bump vite to ^8.3.3 and @vitejs/plugin-react to ^6.1.2 and regenerate
the lockfile. Make the build test tolerant of `-` in Vite's base64url
hashes and of readdir ordering.
@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@sheepsteak
sheepsteak marked this pull request as ready for review October 13, 2026 14:45
@sheepsteak
sheepsteak marked this pull request as draft October 13, 2026 15:30
preview.getByText() and preview.getByRole() from @webcontainer/test return
locators scoped to the <iframe> element without entering it, and awaiting
them doesn't wait for anything, so the dev test passed without checking the
preview. Wait from Playwright's side through page.frameLocator() instead,
since the preview is cross-origin and in-page queries can't see into it.

With a real assertion the test failed on both Vite 5 and Vite 8: restoring
the setup() snapshot drops the executable bit from
node_modules/vite/bin/vite.js, so `npm run dev` exits with
`spawn vite EACCES`. Install in beforeEach instead of restoring the snapshot.
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.

2 participants