* fix(dev): treat .js as JSX in the optimizeDeps scanner
The dep optimizer (scanner + pre-bundler) runs its own Rolldown/esbuild
pipeline that does not go through the `vinext:jsx-in-js` transform plugin,
so JSX in plain `.js`/`.mjs` source files made the dependency scan fail with
"Unexpected JSX expression" and aborted pre-bundling.
Configure the dep optimizer to treat `.js`/`.mjs` as JSX
(`optimizeDeps.rolldownOptions.moduleTypes` on Vite 8,
`optimizeDeps.esbuildOptions.loader` on Vite 7), mirroring how the main
transform treats `.js`/`.mjs` (its `/\.m?js$/` filter). Applied to the
top-level optimizeDeps and the per-environment (rsc/ssr/client) blocks via
getDepOptimizeNodeEnvOptions.
The motivating real-world symptom is that, once the scan aborts,
pre-bundling is skipped and UMD/CJS deps can fail to interop under SSR
("window is not defined"). That downstream cascade runs through a different
optimizer path and is not what this change is verified to fix — the added
tests assert only that the dependency scan no longer aborts on
JSX-in-`.js`/`.mjs`.
* test(dev): cover pages jsx optimizer scan
* test(dev): strengthen jsx optimizer scan coverage
* fix(dev): apply jsx optimizer config to pages build clients
---------
Co-authored-by: James <james@eli.cx>
* feat(pages): enforce reactStrictMode by wrapping client root in <StrictMode>
`reactStrictMode: true` was recognized but not enforced — the app root was
never wrapped in <React.StrictMode>, so dev-time strict checks (double-invoked
effects/render, deprecation warnings) were silently lost.
Resolve `reactStrictMode` from next.config (preserved as `boolean | null` so
each router applies its own default) and, for the Pages Router, wrap the
client tree in <React.StrictMode> when the value is `true`. The default
matches Next.js: `null`/unset is OFF for the Pages Router
(`reactStrictMode === null ? false` in define-env.ts).
The wrap lives in `wrapWithRouterContext` (next/router) — the single seam every
render path funnels through: the initial hydration entry (production AND the
dev server's inline hydration script) and every client-side navigation
`root.render()` in shims/router.ts. This mirrors Next.js, whose `doRender`
closure wraps in <React.StrictMode> for both the initial hydrate and subsequent
`reactRoot.render()` calls (client/index.tsx). Wrapping only the production
client entry would have been inert — StrictMode does nothing in production, and
the dev server hydrates via a separate template — so the flag is also threaded
into createSSRHandler and the dev hydration script. The wrap is gated on a
client-only `window.__VINEXT_REACT_STRICT_MODE__` flag so the server-rendered
tree is never wrapped (Next.js wraps client-side only); StrictMode renders no
DOM, so SSR markup and hydration are unaffected. The CommitBoundary stays
outside StrictMode so its commit effect is not double-invoked (Next.js keeps
`<Root>` outside <StrictMode> too).
`vinext check` reports reactStrictMode as "partial": enforced for the Pages
Router, but the App Router is not yet wrapped (its root is mounted by the RSC
client runtime, not vinext-owned code, and Next.js defaults App Router strict
mode on).
* test(pages): cover strict mode navigation renders
---------
Co-authored-by: James <james@eli.cx>
* perf(build): parallelize prerender across a pool of render processes
Build-time prerender rendered every static route by fetching it from a
single in-process production server driven by a promise pool. React
SSR/RSC rendering is CPU-bound JS, so the pool only overlaps I/O — every
render serialized on one core, and raising --prerender-concurrency did
nothing. Next.js forks a worker pool and saturates every core.
Fork a pool of production-server child processes (one per core, capped)
and round-robin the per-route render fetch across them, keeping route
scanning, getStaticPaths/static-params resolution, file writing and the
manifest on the main process. Pool size scales by cores AND routes, so
small apps, low-memory machines, and --prerender-concurrency 1 keep the
single in-process server (no fork, no regression); running from source
(no built .js worker entry) also falls back to single-process.
child_process, not worker_threads: worker threads contend for CPU on this
workload (measured ~2x slower per route and non-scaling), which is also
why Next.js uses processes.
react.dev (809 routes, cold cache, same machine): 29.9s -> 15.1s, now
faster than its own Next.js build (~19.3s). An 801-route static fixture:
~22s -> ~2-6s. Prerender output is byte-identical to the single-process
path on deterministic renders; workers install the same NoOp cache
handler the in-process path uses. A worker that exits unexpectedly fails
the build loudly instead of shipping partial output.
* fix(build): harden prerender worker pool
---------
Co-authored-by: James <james@eli.cx>
* fix(config): resolve and bundle extensionless .cjs config imports
`vinext init` renames CJS config files (tailwind.config.js,
postcss.config.js) to .cjs when it adds "type": "module", and app code
imports them extensionlessly (import cfg from "../tailwind.config").
Two problems blocked this on the app module graph:
1. vinext overrides resolve.extensions for every Vite environment with a
list that, like Vite's default, omits .cjs/.cts, so the extensionless
import failed with [UNRESOLVED_IMPORT]. Append .cjs/.cts (lowest
priority) to buildViteResolveExtensions' default list.
2. Once resolved, vite-plugin-commonjs rewrote the .cjs module.exports to
ESM export {}, but rolldown infers moduleType: cjs from the extension
and re-parsed the output as CommonJS, failing with "Cannot use export
statement outside a module". Return false from the commonjs() filter for
project-local .cjs/.cts so vite-plugin-commonjs skips them and rolldown's
own CJS interop bundles them. Everything else returns undefined, which
preserves the plugin's defaults, including its existing skip of
node_modules .cjs files.
Fixescloudflare/vinext#13.
* fix(config): harden local cjs commonjs filter
* docs(config): fix cjs regression references
* test(config): cover extensionless cts config imports
* docs(config): clarify cjs resolve extension defaults
---------
Co-authored-by: James <james@eli.cx>
* test(routing): cover optional catch-all root with empty params
Add a regression test for a ROOT-level optional catch-all page
pages/[[...markdownPath]].js whose getStaticPaths emits the empty-params
entry { markdownPath: [] } (the react.dev shape). Verifies the dev server
serves the root / HTML, the /_next/data/<id>/index.json endpoint, a
non-root concrete path, and a 404 for unlisted paths under fallback:false.
The existing optional catch-all test only covers a non-root subpath; this
guards the empty-params-at-root case, which routes through the trie's
0-segment optional catch-all match (route-trie.ts) and the empty-array
path normalization (route-pattern.ts / pages-page-data.ts).
* test(routing): tighten optional catch-all root coverage
---------
Co-authored-by: James <james@eli.cx>
The font self-hosting unit tests depended on live Google Fonts CSS and font file requests. When the CSS fetch hit a recoverable network error, the plugin correctly skipped _vinext injection for offline fallback, but the test expected self-hosted output.
Mock both fonts.googleapis.com CSS and fonts.gstatic.com font binaries for the self-hosting assertions. The offline fallback behavior remains covered by the explicit network-error test.
The dev overlay recovery spec armed its reload canary after only confirming that the navigation runtime existed. That was not enough to prove the app-router client tree had hydrated the Link handler, so CI could occasionally follow the anchor as a hard navigation before the soft-nav path was ready.
Reuse the shared app-router hydration helper before setting the canary and clicking the recovery link. The test still exercises the real browser click path, but no longer races client hydration.
The trusted benchmark publisher validates measuredAt by fetching commit metadata with gh api. Using the repository commit endpoint also returns commit diff files and stats, which can exceed Node's default child process output buffer for large merge commits.
Fetch the raw Git commit object instead, which still exposes committer.date without diff payloads. Update the validator tests to expect the lighter Git Database endpoint and response shape.