mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.0
86 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
494e8d2d5b |
fix(pages): render custom errors for notFound results (#1737)
* fix(pages): render custom errors for notFound results Pages getStaticProps and getServerSideProps notFound HTML results returned the built-in 404 response directly. That skipped the configured pages/404 or pages/_error render path, so _error.getInitialProps never received ctx.req.url or ctx.asPath for the original request. The page-data resolver now returns a typed notFound signal for HTML requests while preserving JSON-shaped 404 data responses. The generated Pages entry rerenders the configured not-found route with the original asPath, and Pages getInitialProps is loaded through a shared typed helper for prod and dev parity. * fix(pages): address PR review feedback on getInitialProps binding, err context, and response-sent handling - Call getInitialProps with component as this receiver - Thread caught errors through to _error.getInitialProps(ctx.err) - Check headersSent || writableEnded in dev server after getInitialProps - Add tests: gSSP notFound, explicit 404, data JSON 404, err on throw, this binding * refactor(pages): use isResponseSent helper in resolvePagesPageData for sent-check consistency The prod block in resolvePagesPageData checked only res.headersSent, while the helper's isResponseSent and the dev server check both headersSent and writableEnded. The prod PagesReqResResponse only declares headersSent (its getter is backed by the ended flag), so this is functionally correct in prod, but the asymmetry is hard to read next to the helper and a future change to the prod response shape would silently break sent-detection. Export isResponseSent from pages-get-initial-props and use it at both gSSP and gIP call sites so the prod resolver reuses the same sent-detection logic as the helper. Also adds tests for the writableEnded-only path and a focused unit test for isResponseSent covering both prod-shaped and node-shaped res mocks. * perf(pages): gate getInitialProps req/res allocation behind presence check The getInitialProps prod block in resolvePagesPageData unconditionally called createGsspReqRes() for every static page without gSP/gSSP, even when the page had no getInitialProps either. This allocated a req/res pair and an unresolved responsePromise that was immediately discarded. Export hasPagesGetInitialProps from pages-get-initial-props.ts and gate the prod block on it, keeping the hot path for plain static pages allocation-free. |
||
|
|
fb233b0a34 |
feat: improve dev error overlay source frames (#1746)
* Improve dev overlay stack source mapping * Polish dev error overlay stack display * Add dev overlay code frames * Align dev overlay message spacing * Add dev overlay error copy action * Fix dev overlay knip exports * Polish dev overlay layout * Polish dev error overlay spacing * Fix dev overlay coverage and source frame metadata * Fix dev overlay recovery for RSC HMR errors * Handle Vite HMR build errors in dev overlay * Fix knip * Use Shadow DOM for dev error overlay |
||
|
|
51690038d8 |
fix(pages-router): cancel in-flight nav on gSSP/gSP data redirect (#1465) (#1691)
* fix(pages-router): return __N_REDIRECT JSON for gSSP/gSP data redirects (#1465) When getServerSideProps/getStaticProps returns `{ redirect }` on a client-side `_next/data` navigation, vinext emitted a real HTTP redirect. The client's fetch transparently followed it to the destination's HTML (not a valid data envelope), forcing a hard reload with console errors and leaving navigation on the wrong page. Mirror Next.js: for data requests, reply 200 with `__N_REDIRECT` / `__N_REDIRECT_STATUS` in pageProps. The client router now detects these markers and re-dispatches a fresh client navigation to the destination, which increments the navigation id and supersedes (cancels) the in-flight navigation so the intermediate page never commits. HTML requests still emit a real HTTP redirect. Fixed in both server paths to keep dev/prod parity (see AGENTS.md): - prod: buildPagesRedirectResponse() in pages-page-data.ts branches on isDataReq - dev: writeGsspRedirect() in dev-server.ts branches on isDataReq - client: navigateClientData() handles pageProps.__N_REDIRECT via handleDataRedirect(), preserving push/replace mode - tests: data-endpoint redirect shape (vitest) + client cancellation (e2e) * fix(pages-router): hard-navigate to external __N_REDIRECT destination (#1465) Address review: handleDataRedirect() passed the originating page URL to the hard-navigation fallback for external/non-absolute __N_REDIRECT destinations, which would loop back to the redirecting page (e.g. an OAuth callback at https://example.com/...). Hard-navigate to the destination instead, matching Next.js's handleHardNavigation({ url: destination }). Drop the now-unused fallback parameter and add a server test asserting an external destination is preserved verbatim in the data envelope. |
||
|
|
b1f2e57bc4 |
fix(pages): preserve import.meta.url source paths (#1598)
* fix(pages): preserve import.meta.url source paths
Pages Router production bundles previously left direct import.meta.url expressions for the bundler to evaluate from generated output. Server renders could expose dist/server/entry.js, and prerendered Pages output could miss the client entry needed for hydration to replace the server value with the Turbopack-style browser source URL.
The fix rewrites direct import.meta.url reads in user modules to source-file URLs at transform time, keeps new URL asset bases intact, and loads the Pages client entry from the build manifest for Node production and prerender servers.
Regression coverage ports the Next.js import-meta deploy test, including the prerender and hydrated browser path.
* fix(tests): run import.meta browser coverage in e2e
The import-meta regression test launched Playwright from a Vitest integration shard. That shard does not install Playwright browser binaries in CI, so it failed before exercising the runtime behavior.
Move the browser assertion into the existing pages-router-prod Playwright project, keep the lower-boundary server and client chunk assertions in Vitest, and cache the import-meta plugin root paths after an early source-text bailout to address review feedback.
* refactor(import-meta-url): canonicalize each module once per transform
The transform path canonicalized the same module id twice: once in the
eligibility check and again when computing the replacement value, so every
rewritten module paid for two fs.realpathSync.native() syscalls.
The eligibility check now returns the canonical id and threads it through to
the value computation, leaving a single realpath per module.
Also widens the extension match to .cts/.mts/.cjs and documents that the
new URL() skip only covers the bare URL identifier, matching Vite's own
asset-detection scope.
* test(pages): drop unrelated prerender from top-level-await setup
The top-level-await production test ran runPrerender in its setup, but the
assertions pass identically whether the routes are prerendered or rendered on
demand. The step added unrelated build work to a test that is not about
prerendering or client-entry injection.
* fix(pages): share client entry fallback
* fix(import-meta-url): address PR review feedback
- Handle ChainExpression (import.meta?.url) in new URL() asset-detection
skip so the optional-chain variant preserves Vite asset resolution
- Add explicit ChainExpression check in visit() to avoid fragile
reliance on coincident inner-node start/end ranges
- Add test for new URL('./file', import.meta?.url) preservation
- Fix decodeHtmlText entity ordering (& before ") in both
unit and E2E test helpers
- Add doc comments: excluded prefixes rationale, node_modules
early-exit optimization intent
* fix(pages): select client entry by marker in build manifest
findClientEntryFileFromManifest returned the first chunk flagged
isEntry, but a client build can emit several entry chunks (client
entry plus instrumentation/middleware) and manifest order is not
guaranteed. The on-disk fallback already disambiguates by the
vinext-client-entry / vinext-app-browser-entry marker; apply the
same check to the manifest path so both lookups agree, falling back
to the first entry only when nothing is marked.
* refactor(pages): address review feedback on client-entry/import-meta-url
- prod-server: hoist findClientEntryFile out of the manifest if/else
since it already handles a missing manifest via the on-disk fallback
- import-meta-url: document the two known limitations (destructured
access, aliased new URL() base) that match Vite's own behavior
- import-meta-url: drop the dead visit(value.callee) for new URL() —
the callee is always the bare URL identifier
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
5d9548dc92 |
fix(pages-router): invoke Document.getInitialProps so docProps reach SSR (#1633)
* fix(pages-router): invoke Document.getInitialProps so docProps reach SSR (#1361) `pages/_document.tsx` files that override `static async getInitialProps` to inject extra props were rendered with an empty `this.props`, because both the dev-server and the production pages response builder created the Document element with `React.createElement(DocumentComponent)` and never called the hook. The Next.js async-modules e2e fixture (`test/e2e/async-modules/pages/_document.jsx`) uses exactly that pattern to surface a `docValue` from `await Document.getInitialProps(ctx)` plus a top-level `await` constant. PR #1381 (issue #1361) fixed the class construction crash, but the deploy-suite continued to fail "csr async page modules" because `<div id="doc-value">` rendered empty. This change adds a small `loadUserDocumentInitialProps` helper used by both the Pages Router dev-server (`streamPageToResponse` shell and the custom-error-page shell) and the production response builder (`buildPagesShellHtml`). It invokes the override (if any) and spreads the resolved props on the Document element, mirroring Next.js's `render.tsx` `<Document {...htmlProps} {...docProps} />` call. The helper short-circuits when the user did not override the base shim, so the existing fast path keeps the same number of awaits. Reference: vercel/next.js packages/next/src/server/render.tsx (`loadDocumentInitialProps`, `documentElement`). Tests: - Adds a new pages-router production e2e case that builds a `_document.tsx` with `static async getInitialProps` + top-level `await`, plus a custom `404.tsx` that uses top-level `await`, and asserts the resolved values reach the rendered HTML. - Unit-tests `loadUserDocumentInitialProps` directly for the override, no-override, and throwing-override cases. Refs #1361 * fix(pages-router): address bonk review on Document.getInitialProps helper Two changes from review feedback on PR #1633: 1. Cache the base getInitialProps reference at module level. The previous helper did `await import("vinext/shims/document")` on every request to compare identity — unnecessary per-request work and an extra await on the fast path where the user has no override. Switched to a static import + module-scoped constant so the identity check is a synchronous reference compare. No-override callers now resolve with one less await. 2. Let user `getInitialProps` errors propagate. Next.js's `loadGetInitialProps` does not catch — a throw surfaces as a 500 to the caller. The previous version logged-and-swallowed the error and returned null, which silently erased docProps on every failing render and made user bugs harder to find. vinext now matches Next.js's contract. Updated the corresponding unit test to assert the throw propagates rather than that the helper resolves to null. Refs #1361, #1633 |
||
|
|
76fe5d8b05 |
fix(pages-router): render custom pages/500 on SSR throw (#1676)
When getServerSideProps (or any other render-time code) threw in the production server entry, vinext collapsed the response into a plain "Internal Server Error" text body. Next.js instead renders the user's custom pages/500.tsx (with pages/_error.tsx as a fallback). Match that behaviour so frameworks that rely on custom error pages — and the Next.js getserversideprops deploy suite — see the expected output. The catch block in `_renderPage` now re-renders through the same entry using `pages/500.tsx` (or the `_error` route) with status 500. A new `__isInternalErrorRender` guard prevents infinite recursion if the error page itself throws; data (`/_next/data/...`) requests still short-circuit to the plain text body since they can't return HTML. Mirrors Next.js base-server.ts `renderToResponseImpl` 500 fallback. The dev server already renders custom 500/error pages via `renderErrorPage()` — this aligns prod with that behaviour and adds explicit regression coverage for both paths via a new gssp-throw fixture. Refs #1458 |
||
|
|
15fbe66c47 |
test(pages-router): regression coverage for edge runtime & OG image API routes (#1338) (#1648)
Adds Pages Router fixtures and dev + production integration tests for the
two scenarios reported as failing in the Next.js Deploy Suite:
- `export const config = { runtime: 'edge' }` API route returns a Web
Response (issue #1338 case 1 — edge API routes were observed as 500).
- `next/og` `ImageResponse` with `runtime: 'edge'` inside a Pages Router
API route returns `image/png` (issue #1338 case 2 — OG routes were
observed as 404).
Ports route handlers from `next.js/test/e2e/edge-pages-support/app/pages/api/hello.js`
and `next.js/test/e2e/og-api/app/pages/api/og.js`. The existing
implementation already wires both code paths correctly via
`handlePagesApiRoute` and the `vinext:og-inline-fetch-assets` plugin; this
PR locks in coverage so a Pages Router-only build cannot silently regress
either path.
Refs #1338
|
||
|
|
f75845ef14 |
fix(pages-router): emit page scripts with defer in <head> by default (#1636)
* fix(pages-router): emit page scripts with defer in <head> by default Next.js' Pages Router emits page scripts with the `defer` attribute in `<head>` when `experimental.disableOptimizedLoading` is false (the default). vinext emits `<script type="module">` (which already defers implicitly), but upstream tests assert the literal `defer` attribute and were failing against the deploy suite. Add `defer` to module scripts emitted by `collectAssetTags` when `experimental.disableOptimizedLoading` is not enabled. Surface the flag in `ResolvedNextConfig` so prod-server respects an explicit opt-out. Browser behaviour is unchanged (module scripts already defer), but parity tests now pass. Fixes #1519. * test(e2e): update hydration regex to allow defer attribute The script tag regex in `tests/e2e/cloudflare-pages-router/hydration.spec.ts` required `type="module"` to be immediately followed by `src="…"`, but the optimized-loading fix inserts a `defer` attribute between them. Relax the regex to allow any whitespace-separated attributes between `type="module"` and `src` so future attribute additions don't break it either. |
||
|
|
a20c49ead2 |
fix(middleware): stop merging original query into middleware rewrite target (#1632)
* fix(middleware): stop merging original query into middleware rewrite target
Next.js middleware constructs the rewrite-target URL itself — by mutating
`request.nextUrl` or by passing a fresh URL — and whatever search params
that URL carries IS the final query. vinext was silently re-merging the
original request's query on top, which resurrected keys that middleware
had explicitly removed via `searchParams.delete(...)`.
Tests:
- Add `mw-clear-query-params` Pages Router fixture + dev test ported from
Next.js `test/e2e/middleware-rewrites/test/index.test.ts` ("should
clear query parameters").
- Add prod-server tests for the original-query-preservation paths
(middleware now propagates query via `request.nextUrl.clone()` in the
fixture, mirroring Next.js `app/middleware.js`).
- Add App Router parity test for original-query-preservation.
Closes #1342
* test(middleware): address bonk review nits for #1342
- Fix misleading comment in app-router.test.ts that referenced Object.assign
merge semantics — the PR's premise is that vinext should NOT auto-merge.
- Add prod-server regression test for searchParams.delete on rewrite target,
mirroring the dev coverage so both code paths are exercised explicitly.
|
||
|
|
35bb1a2250 |
fix(pages-router): return 405 with Allow: GET, HEAD on POST to static pages (#1631)
Pages Router previously rendered HTML for any HTTP method, including POST/PUT/DELETE, on static (no `getServerSideProps`) pages — masking client bugs that Next.js surfaces as 405. Mirrors Next.js's behavior in `base-server.ts` (the `(typeof components.Component === 'string' || isSSG)` gate), which rejects non-GET/HEAD methods on static pages with `Allow: GET, HEAD` and a `Method Not Allowed` body. Pages with `getServerSideProps` continue to accept any method, since user code may legitimately read `req.method` inside gSSP. The new `resolvePagesPageMethodResponse` helper is wired into both the dev-server SSR handler and the prod-server pages-server-entry, and skips `/_error`, `/404`, `/500`, data requests, and explicit status overrides — matching the upstream carve-outs. Refs #1463. The `res.revalidate()` portion of #1463 is left for a follow-up. |
||
|
|
b6ac0c9abd |
test(pages-router): regression test for literal form action=action:foo (#1476) (#1613)
* test(pages-router): regression test for literal form action=action:foo (#1476) Adds a Pages Router fixture page with a literal action:foo string in a form action attribute, plus an integration test asserting the attribute survives SSR serialization intact. This guards against future regressions where React 19 or the RSC plugin might strip the literal string because it resembles a server action ID. Ported from Next.js test/e2e/app-dir/action-in-pages-router. * test: clarify action:foo regression test comment Update the comment to accurately describe that this test verifies React 19 SSR preserves literal string action attributes, not server action invocation. This addresses review feedback on the PR adding the regression test for #1476. |
||
|
|
5b633b0284 |
fix(pages): render masked basePath error routes (#1441)
* fix(pages): render masked basePath error routes
Pages Router route misses under basePath returned generic HTML and masked client navigations to /404 or /_error could fetch the visible as-path instead of the error route. This diverged from Next.js when applications used next/router to preserve a friendly URL while rendering the error component.
The implementation assumed the browser URL and the component route could always collapse to one fetch target. Preserve the masked history URL, fetch the Pages error route as the HTML target, allow its 404 response to hydrate, and render pages/404 for production route misses with the correct status and client module metadata.
* test(pages): keep error navigation coverage out of vitest browser
Vitest integration CI does not install Playwright browsers, so launching Chromium from tests/pages-router.test.ts made the PR fail deterministically even though the upstream deploy harness passed.
Keep the browser-level verification in the upstream deploy-suite run and cover vinext locally at the lower boundaries: production 404 rendering through HTTP and Pages Router masked error-route navigation through the router shim. The generated Pages entry now also shares one SSR manifest module lookup helper between asset tag collection and client module URL resolution.
* fix(prerender): keep custom 404 out of generic page loop
* fix(pages): fall back to _error on route misses
* fix(pages): document the catch-all guard on matchRoute("/404")
The pattern check prevents a dynamic catch-all route (e.g. [...slug]) from
being used as the 404 fallback. Without it, matchRoute can return a
catch-all match and silently hijack the error rendering path.
* fix(pages): preserve error navigation rewrite order
* fix(pages): limit fallback rewrites to route misses
* fix(deploy): skip deferred error rendering for data requests
* fix(pages): preserve ISR response status
* fix(pages): keep module metadata in ISR regeneration
|
||
|
|
61cf2e78f5 |
fix(image): emit /_next/image URLs (#1562)
* fix(image): emit /_next/image URLs to match Next.js Closes #1513 The default image loader and optimization endpoint switched from the vinext-specific /_vinext/image path to Next.js's canonical /_next/image. This unblocks the deploy suite tests that import Next.js's expected URL shape (/_next/image?url=...&w=...&q=...). * refactor(image): use IMAGE_OPTIMIZATION_PATH constant at remaining call sites Replace hardcoded "/_next/image" strings in index.ts, app-rsc-handler.ts, and the generated worker entry templates in deploy.ts with the IMAGE_OPTIMIZATION_PATH constant from server/image-optimization, matching the pattern already used in prod-server.ts. Prevents future drift if the path ever changes again. * feat(image): accept both /_next/image and /_vinext/image at the optimizer Add a VINEXT_IMAGE_OPTIMIZATION_PATH constant and an isImageOptimizationPath() helper, then route through every match site (prod-server, dev server passthrough, app RSC handler, generated worker templates, and shipped example workers). Apps that wire image URLs to either prefix now hit the same handler; new URLs are still emitted via IMAGE_OPTIMIZATION_PATH. |
||
|
|
e5d9c4ca33 |
fix(pages-router): await Promise-shaped gssp props (#1568)
* fix(pages-router): await Promise-shaped gssp props
Next.js explicitly supports a Promise value for `getServerSideProps`
`props`. Pages returning `{ props: Promise.resolve(...) }` previously
rendered empty pageProps because vinext passed the unresolved Promise
straight through to the serializer. Match Next.js' `render.tsx` by
detecting a thenable `props` and awaiting it before continuing.
The fix is applied symmetrically to the dev server gssp dispatch
(`dev-server.ts`) and the prod page-data dispatch (`pages-page-data.ts`)
so the two stay in sync per AGENTS.md. Other return shapes (`notFound`,
`redirect`, `revalidate`, fallback) are untouched.
Closes #1459
* refactor: simplify Promise-shaped props handling
await Promise.resolve(x) already handles both thenables and plain
values, so the .then duck-typing check is unnecessary.
|
||
|
|
37e077dd06 |
fix(pages-router): preserve query string in Link and router.push (#1572)
`parseQueryString` and the parseQuery template embedded in the Pages
Router worker entry both used `url.split("?")[1]`, which silently drops
everything after the second `?`. URLs like `/linker?href=/about?hello=world`
have query `href=/about?hello=world` per RFC 3986, so the previous
implementation truncated `query.href` to `/about` and `<Link>` rendered
without the embedded query string. Switching to an `indexOf`-based slice
keeps the full query value intact.
Closes #1471
|
||
|
|
7ffa2b7f3c |
fix(pages-router): default Cache-Control on gssp responses (#1574)
getServerSideProps responses now ship the Next.js default
`private, no-cache, no-store, max-age=0, must-revalidate` Cache-Control
header in both dev and prod, and forward `res.setHeader('Cache-Control', ...)`
overrides set inside gssp through to the final response.
Mirrors next/src/server/route-modules/pages/pages-handler.ts where a
gssp page resolves to `revalidate: 0` and getCacheControlHeader emits
the same string, gated by `if (!res.getHeader('Cache-Control'))` to
preserve user overrides.
Closes #1461
|
||
|
|
3ebc28f0e1 |
fix: preserve Next.js cache headers for prerendered app pages (#1424)
* fix: preserve Next.js cache headers for prerendered app pages * fix: address cache header review feedback |
||
|
|
81bdd6acd3 |
fix(pages-router): render loading shell for unlisted fallback: true paths (#1414)
* fix(pages-router): render loading shell for unlisted `fallback: true` paths PR #1394 fixed the post-rewrite static-vs-dynamic priority sub-problem from #1331 and explicitly tracked `fallback: true` rewrite behaviour separately. When `getStaticPaths` returns `fallback: true` and a request hits a path not in the pre-rendered list, vinext was SSRing the page fully — running `getStaticProps` and emitting `isFallback: false` in `__NEXT_DATA__`. Next.js instead ships a loading shell with empty props and `useRouter().isFallback === true`, then the client hydrates by fetching the data URL. The upstream regression test `should rewrite to fallback: true page successfully` in `test/e2e/middleware-rewrites/test/index.test.ts` asserts "Loading..." in the SSR HTML, which vinext could not produce. Thread an `isFallback` signal through `resolvePagesPageData`, `buildPagesNextDataScript`, the Pages Router SSR context, and both the prod (`pages-server-entry.ts`) and dev (`dev-server.ts`) renderers: - `getStaticProps` and `getServerSideProps` are skipped on the fallback render (matches Next.js `render.tsx`'s `if (isSSG && !isFallback)` gate around `getStaticProps`). - Data requests (`/_next/data/<buildId>/<page>.json`) still resolve real props so the client can swap in after the shell ships. - `useRouter().isFallback` reads from the SSR context server-side and from `__NEXT_DATA__.isFallback` client-side — both flip to true on the shell render. The dev-server comment that claimed Next.js dev "also renders fully" was incorrect; Next.js's `pages-handler.ts` invokes `doRender` with `isFallback: true` for dev fallback responses, so dev now serves the shell too. Tests: - Updated `tests/pages-router.test.ts` "SSR renders unlisted path with getStaticPaths fallback: true" — previously documented the deviation; now asserts the loading shell, empty props, and `isFallback: true`. - Added a counterpart test that the data URL for the same unlisted path still returns real props from `getStaticProps`. - Added a production-side regression test mirroring the upstream "should rewrite to fallback: true page successfully" expectation. Refs #1331 * test(pages-page-data): include isFallback: false in normalized render assertion The render branch of `resolvePagesPageData` now returns `isFallback` alongside the other fields (added in the parent commit for the `fallback: true` loading shell). Update the strict-shape `toEqual` in the cache-miss unit test so it matches the new return shape. |
||
|
|
512be7f25c |
fix(middleware): preserve route priority on rewrite target resolution (#1394)
* fix(middleware): preserve route priority on rewrite target resolution When `trailingSlash: true` is set in next.config.js, a request like `/rewrite-1/` was matched against the next.config.js rewrite source `/rewrite-1` literally, including the trailing slash. The match failed, so the request fell through to the dynamic `[id]` route and rendered the dynamic page with `id="rewrite-1"` instead of being rewritten to the static target. Strip a single trailing slash from the incoming pathname before matching against config rewrite/redirect/header sources, matching Next.js's `source + '(/)?'` behavior under `trailingSlash: true`. The source pattern itself is preserved so catch-all (`:param*`, `:param+`) and root (`/`) semantics are unaffected. Refs #1331 * address bonk feedback: locale-static path, comment wording, tighter tests - Extract `stripTrailingSlashForConfigMatch` and call it from `matchRedirect` so the locale-static fast path (Map.get on the pathname) also honors trailing slashes — previously a pure locale-static redirect rule could miss when `trailingSlash: true`. - Clarify the comment to reflect that the strip is unconditional defense-in-depth rather than a faithful port of Next.js's conditional `(/)?` suffix. - Disambiguate the pages-router integration tests by asserting on `id="home"` / `id="ssr"` so the assertion can't pass on the wrong page (both pages render "Hello World"). - Add direct unit tests for `matchConfigPattern` trailing-slash behavior covering exact, :param, regex-group, root, and catch-all cases. * extend trailing-slash normalization to matchHeaders Bonk flagged that matchHeaders runs its own compiled regex path that doesn't go through matchConfigPattern, so a header source like `/api/:path*` would miss `/api/foo/` under `trailingSlash: true`. Normalize once at the top of matchHeaders the same way matchRedirect already does, and add a regression test. |
||
|
|
d504f022b0 |
fix(assets): default assetsDir to _next/static (Next.js parity) (#1411)
* fix(assets): default assetsDir to _next/static (Next.js parity) Closes #1337. Supersedes #1383. vinext's default `assetsDir` was Vite's historical `assets/`, so URL emission (`/_next/static/...` via `resolveAssetUrlPrefix("")`) and on-disk layout (`dist/client/assets/...`) disagreed in the empty-`assetPrefix` case. The `build.assetsDir` and `experimental.renderBuiltUrl` Vite overrides only applied when `assetPrefix` was configured, leaving the no-prefix branch serving from a different path than the URL contract Next.js's client runtime and test harness assert against. This commit flips the default to Next.js's canonical layout: - `resolveAssetsDir("")` returns `_next/static` (was `assets`) - `build.assetsDir` is now set unconditionally from `resolveAssetsDir` - Vite's default `base + assetsDir` composition produces correct URLs in the no-prefix case; `renderBuiltUrl` stays gated on `assetPrefix` because it's only needed for the configured cases - Drops the legacy `/assets/` branches in prod-server (3 sites) and static-file-cache (hard cutover — see PR description for rationale) - Updates `_headers` generation, precompress default, fonts plugin `DEFAULT_ASSETS_DIR` to match the new default With the layout aligned, invalid `_next/static/*` requests naturally return plain-text `404 + "Not Found"` from the static-file layer instead of falling through to the page renderer (which would produce an HTML 404 with bootstrap scripts and CSS). This replaces #1383's parity short-circuit with the natural code path: - Node prod-server (App + Pages branches): missing asset under `resolveAppRouterAssetPath` returns `text/plain; charset=utf-8` 404 - Cloudflare worker entry: `isNextStaticPath` recognises asset-shape after ASSETS-binding misses; returns `notFoundStaticAssetResponse` Mirrors Next.js: packages/next/src/server/lib/router-server.ts. Adds `tests/invalid-static-asset-404.test.ts` (3 App Router + 3 Pages Router cases — no prefix / basePath / assetPrefix) ported from Next.js e2e suites. Updates 11 source files + 12 test files; 914+ tests pass in the touched suites (asset-prefix, app-router, pages-router, deploy, font-google, static-file-cache, serve-static, precompress, features, routing, isr-cache, build-optimization, app-rsc/ssr, standalone, middleware, shims). Ported from Next.js: - test/e2e/invalid-static-asset-404-app/*.test.ts - test/e2e/invalid-static-asset-404-pages/*.test.ts * fix(e2e): update cloudflare-pages-router hydration spec for _next/static * PR #1411 approved. Clean fix, 2 nits. Co-authored-by: james-elicx <james-elicx@users.noreply.github.com> * refactor(assets): derive isHashed checks from ASSET_PREFIX_URL_DIR Replace hardcoded '_next/static' string literals in runtime code paths with the ASSET_PREFIX_URL_DIR constant so the asset URL convention has a single source of truth. - server/prod-server.ts isHashed: pathname.includes(`/${ASSET_PREFIX_URL_DIR}/`) - server/prod-server.ts public-dir guard: startsWith(`/${ASSET_PREFIX_URL_DIR}/`) - server/static-file-cache.ts isHashed: both startsWith + includes derived - plugins/fonts.ts DEFAULT_ASSETS_DIR = ASSET_PREFIX_URL_DIR Behavioural no-op — the constant is '_next/static' so the resolved strings are identical. Centralises the URL contract so future changes to ASSET_PREFIX_URL_DIR (or a refactor that derives it from build config) flow through to every consumer. Doc-comment references to '_next/static' left in place — they describe the canonical value for human readers and are not code paths. --------- Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com> Co-authored-by: james-elicx <james-elicx@users.noreply.github.com> |
||
|
|
576c55d7db |
feat(pages-router): implement _next/data JSON endpoint (#1384)
* feat(pages-router): implement _next/data JSON endpoint
Recognize `/_next/data/<buildId>/<page>.json` in dev and prod request
pipelines. The URL is normalized to the page path BEFORE middleware
runs (matching Next.js' `handleNextDataRequest`), and the server emits
`{ pageProps }` as JSON with `Content-Type: application/json` instead
of the rendered HTML.
Closes #1330
* fix(pages-router): address bigbonk findings on _next/data endpoint
- Delegate JSON envelope construction from the generated entry to the
typed `buildNextDataJsonResponse` helper so the envelope shape lives
in one place (entries stay thin per AGENTS.md).
- Return a JSON 404 from `resolvePagesPageData` when `getStaticPaths`
with `fallback: false` rejects the path for a data request. Without
this, client-side navigations to unlisted slugs got an HTML 404 body
which fails to parse as JSON in the Next.js client router.
- Apply the same JSON-404 gating in the dev server for
`getStaticPaths fallback:false`, `getServerSideProps.notFound`, and
`getStaticProps.notFound` paths.
- Read the dev buildId from the plugin's resolved `nextConfig.buildId`
first so a user-supplied `generateBuildId` is honored, matching the
value embedded into the prod server entry.
- Add coverage for `getStaticProps` data requests and the
`fallback:false` JSON-404 path.
* chore: drop unused NextDataMatch export (knip)
* fix(pages-router): JSON 404 for dev stale buildId; HTML 404 for gSSP/gSP notFound on HTML nav
Follow-ups from bigbonk round 2:
- Dev request pipeline now returns a JSON 404 when `_next/data/<wrong>` is
hit, mirroring the prod-server path. Previously the dev fall-through
served Vite's HTML 404, breaking client routers that try to parse the
body as JSON.
- `resolvePagesPageData` now gates the gSSP/gSP `notFound: true` 404 body
on `isDataReq` so HTML navigations get the HTML 404 page and data
requests get the JSON envelope. Previously both shapes always returned
the JSON body, which surfaced text-content-type mismatches on HTML
navigations through gSSP/gSP `notFound`.
|
||
|
|
2d035d47bd |
fix: await top-level await module imports (#1381)
* fix: make next/document default export a class Next.js's `next/document` default export is `class Document extends React.Component`. vinext's shim exported a function, so user `_document.tsx` files that use the class-based form (the common Next.js idiom: `class MyDocument extends Document`) ended up extending a function. React refuses to call class constructors without `new`, which caused SSR to throw "Class constructor MyDocument cannot be invoked without 'new'" — surfacing in e2e as 500 / empty responses. This is the actual root cause of the "async modules render empty pages" report (issue #1361): the Next.js `test/e2e/async-modules` fixture's `pages/_document.jsx` uses `class extends Document`, and once it crashes the whole page renders empty. Top-level await itself propagates correctly through Rolldown's static-import graph already. Adds a class-based Document shim that mirrors Next.js's signature (static `getInitialProps`, instance `render()`), plus production-build e2e coverage for pages, `_app.tsx`, `getStaticProps`, `getServerSideProps`, API routes, and class-based `_document.tsx`, all using top-level `await`. Closes #1361 Ports from Next.js: test/e2e/async-modules/index.test.ts https://github.com/vercel/next.js/blob/canary/test/e2e/async-modules/index.test.ts * fix(next/document): align Document base class with Next.js's typed contract Addresses Bonk review feedback on PR #1381: - Export DocumentContext / DocumentInitialProps types matching Next.js, so subclasses that delegate via `await Document.getInitialProps(ctx)` receive typed objects. The runtime path is still a stub (the Pages Router renderPage/defaultGetInitialProps chain is not wired up), but the signatures pin the contract. - Declare the default Document class export in next-shims.d.ts so strict-mode consumers don't see `any` for `import Document from "next/document"`. - Add a class-extends regression test in tests/document.test.ts — this is the contract that broke in issue #1361 (the original Next.js `pages/_document.jsx` uses `class extends Document`). - Switch the class generic default from `unknown` to `{}` to match Next.js's `class Document<P = {}>`. - Clarify App Router fixture comment: the upstream Next.js async-modules suite is Pages-Router-only, so this is adapted, not ported verbatim. |
||
|
|
bc586788a2 |
fix(middleware): preserve query params on rewrite (#1375)
* fix(middleware): preserve query params on rewrite When middleware (or a `next.config.js` rewrite) sends a request to a new target, vinext was throwing away the original request's query string, breaking `getServerSideProps`/route-handler `query` for any URL of the form `/rewrite-me?key=value`. Match Next.js's behaviour: `Object.assign(parsedUrl.query, rewrittenParsedUrl.query)` — keep the original query first, let the rewrite-target's query win on key conflicts. Applied to: - middleware rewrites (middleware-runtime.ts) - beforeFiles / afterFiles / fallback config rewrites (prod-server.ts, index.ts, deploy.ts) Closes #1342 References: - https://github.com/vercel/next.js/blob/canary/packages/next/src/server/route-modules/route-module.ts (Object.assign merge) - https://github.com/vercel/next.js/blob/canary/test/e2e/edge-pages-support/index.test.ts (rewrite query expectations) * test(query): document URL-encoded space normalization on rewrite merge Addresses Bonk review observations on #1375: - Add test confirming `%20` → `+` normalization through URLSearchParams is intentional and lossless (downstream consumers re-parse). - Drop unreachable `?? ""` fallback in mergeRewriteQuery; `String#split` always yields a string. |
||
|
|
517c2bde19 |
feat(pages-router): expose window.next.router on hydration (#1400)
* feat(pages-router): expose window.next.router on hydration
Promote next/router from a lazy dynamic import inside hydrate() to a
static side-effect import at the top of the generated client entry, so
window.next = { version, router, ... } is published before any test
harness or third-party script reads it.
Without this, the Next.js deploy test suite — which drives client-side
navigations via `browser.eval('next.router.push(...)')` — crashes with
`TypeError: Cannot read properties of undefined (reading 'router')` (~80
test failures across middleware-general, middleware-rewrites,
trailing-slashes, basepath, with-router, and ignore-invalid-popstateevent).
Mirrors Next.js's bootstrap order in
.nextjs-ref/packages/next/src/client/next.ts (line 5–13), which
statically imports the router from './' and sets window.next as a
top-level side effect before initialize/hydrate.
Closes #1329
* feat(router): expose state fields as live getters on Router singleton
window.next.router must expose `pathname`, `route`, `query`, `asPath`,
`basePath`, `locale`, `locales`, `defaultLocale`, `domainLocales`,
`isReady`, `isPreview`, and `isFallback` as live properties — not just
the navigation methods. The Next.js deploy test suite drives navigations
via `browser.eval('next.router.push(...)')` and then asserts on
`browser.eval('next.router.pathname')` to verify the navigation
completed; without the field getters the assertion reads undefined.
Mirrors Next.js's `singletonRouter` shape in
.nextjs-ref/packages/next/src/client/router.ts (lines 32-47), which
uses Object.defineProperty for urlPropertyFields so reads forward to
the active router state.
Strengthens the existing shims.test.ts coverage so future regressions
of the field surface fail at unit-test time.
|
||
|
|
b01fe032fa |
fix(pages-router): avoid getServerSideProps identifier collision in build (#1370)
When a page declared `getServerSideProps` (or `getStaticProps` /
`getStaticPaths`) as a local binding and re-exported it via
`export { getServerSideProps }`, the client-bundle transform overwrote
the `export { ... }` statement with `export const getServerSideProps =
undefined;`. The original local `const getServerSideProps = ...` was
left in place, producing two declarations of the same identifier in
module scope. Rolldown/OXC rejects this with `Identifier
'getServerSideProps' has already been declared`, breaking the entire
production build (and cascading to 48 test failures in Next.js's
`test/e2e/getserversideprops` suite).
Drop the export specifier without emitting a stub, matching Next.js's
`next-ssg-transform` behaviour. The now-unused local declaration becomes
dead code and is tree-shaken later.
Closes #1354
|
||
|
|
9415a238f4 |
feat(config): implement assetPrefix (#1311)
* feat(config): parse and apply next.config assetPrefix at build time Wire `assetPrefix` from next.config through to the Vite build so every emitted JS/CSS/image/static asset URL is prefixed accordingly. Mirrors Next.js semantics: `assetPrefix` controls asset URLs only, independent of `basePath` (which still controls route URLs). * Add `assetPrefix` to NextConfig and ResolvedNextConfig, normalised via a new `normalizeAssetPrefix` helper that trims trailing slashes, accepts absolute URLs (`https://cdn.example.com[/sub]`) and path prefixes (`/custom-asset-prefix`), and rejects non-string / unparseable values. * New `packages/vinext/src/utils/asset-prefix.ts` module collects the shared helpers: `resolveAssetsDir` (on-disk layout), `resolveAssetUrlPrefix` (emitted URL prefix), `assetPrefixPathname` (path portion for runtime matching), and the canonical `_next/static` directory constant. * In the Vite plugin, set `build.assetsDir` so on-disk layout matches the URL (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs). Use `experimental.renderBuiltUrl` to override Vite's default `base + url` URL emission so `assetPrefix` and `basePath` stay independent — routes continue to anchor at `base = basePath + '/'`. * Persist `assetPrefix` in the embedded `vinextConfig` for the Pages Router server and in a new `vinext-runtime-config.json` sibling for the App Router prod server (the RSC entry only embeds `__basePath`). @see https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix * feat(server): serve assetPrefix-prefixed assets at runtime Update the production server to resolve incoming asset requests under `<assetPrefix>/_next/static/<filename>` (path-prefix form) and `/_next/static/<filename>` (absolute-URL form) in addition to the historical `/assets/` URLs. Both routers (Pages and App) share a new `resolveAppRouterAssetPath` helper that returns the lookup path on disk or `null` to fall through. * App Router prod server loads `assetPrefix` from the new `vinext-runtime-config.json` sibling written by the Vite plugin. * Pages Router prod server reads `assetPrefix` from the embedded `vinextConfig` and threads it through the existing `stripBasePath` lookup. * StaticFileCache marks any path under `_next/static/` as content-hashed (alongside the existing `assets/` rule) so emitted bundles get the long-lived `immutable` cache headers. Same change in the slow-path branch of `tryServeStatic`. Cloudflare Workers needs no code change: the ASSETS binding serves `<prefix>/_next/static/...` directly from `dist/client/<prefix>/_next/static/...` because the on-disk layout already mirrors the URL. Absolute-URL prefixes are expected to be served by the CDN, so they don't reach the worker. * test(asset-prefix): add unit + e2e build tests for next.config assetPrefix Covers the surface area added in the previous two commits: * Unit tests on the new helpers: `normalizeAssetPrefix` (path vs URL, trailing-slash trimming, validation), `resolveAssetsDir` / `resolveAssetUrlPrefix` (on-disk vs emitted-URL layout), `assetPrefixPathname` (runtime matching), and `resolveAppRouterAssetPath` (request → on-disk lookup translation). * End-to-end build tests that exercise `vinext build` against the `tests/fixtures/app-basic` app patched with each `assetPrefix` shape: - Path prefix (`/custom-asset-prefix`): SSR HTML references `/custom-asset-prefix/_next/static/` URLs, on-disk layout matches, and fetching the bootstrap URL from the prod server returns 200. - Absolute URL (`https://cdn.example.com`): emitted asset URLs are fully qualified; no `/assets/` leaks. - Combined `basePath: '/app'` + `assetPrefix: '/cdn-prefix'`: routes stay under `/app/` while asset URLs live under `/cdn-prefix/` — proving the two configs stay independent. - Unset (default): historical `/assets/` layout is preserved. Ported from Next.js: test/e2e/app-dir/asset-prefix/asset-prefix.test.ts https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/asset-prefix/asset-prefix.test.ts * test(next-config): add assetPrefix field to makeResolved helper ResolvedNextConfig requires assetPrefix; the test helper was missing it which broke the type check. * fix(config): replace polynomial regex with loop (CodeQL) CodeQL flagged trim().replace(/\/+$/, "") and replace(/^\/+/, "") as polynomial-time backtracking on uncontrolled input. Swap both for explicit while loops with linear-time semantics. Same fix applied to the URL pathname trim in assetPrefixPathname(). * refactor(prod-server): drop dead null fallback; tighten hashed-asset detection - Remove the `?? null` after resolveAppRouterAssetPath; the helper already returns null on miss, so the fallback (and its obsolete comment) were no-ops. - Add explicit startsWith("_next/static/") / startsWith("/_next/static/") prefix checks before the substring fallback in both static-file-cache and prod-server. The substring check stays so assetPrefix-prefixed layouts (e.g. "cdn/_next/static/") are still recognised. * fix(build/precompress): use resolved assetsDir so assetPrefix builds get compressed precompressAssets() hardcoded `<clientDir>/assets/`. When `assetPrefix` is configured the build writes hashed assets to a different subdirectory (`<prefix>/_next/static/` for path prefixes, `_next/static/` for absolute URLs) and the precompression step walked an empty directory, silently emitting zero .br/.gz/.zst files. Cloudflare Workers builds with assetPrefix would then serve uncompressed assets. Thread the resolved subdirectory through from resolveAssetsDir(assetPrefix) at the plugin call site, default to `"assets"` for backward compat, and add focused tests for both assetPrefix layouts. * refactor: drop redundant isHashed startsWith; harden test cleanup against build failures - prod-server.ts: drop the redundant `pathname.startsWith("/_next/static/")` check; `pathname` always has a leading slash so the existing `includes("/_next/static/")` already covers it. - static-file-cache.ts: keep both `startsWith("_next/static/")` and `includes("/_next/static/")` because `relativePath` has no leading slash — the prefix and substring checks cover different layouts (absolute-URL prefix vs path-prefix). Clarify the comment. - tests/asset-prefix.test.ts: register the tmp-dir cleanup with the shared `cleanups` array synchronously inside `buildFixtureWithConfig`, right after `mkdtempSync`. Previously the cleanup was only pushed after the build returned, so a thrown `createBuilder`/`buildApp` would leak the tmp dir. * refactor(build): embed assetPrefix in App Router entry instead of sidecar JSON Mirrors the existing __basePath embed pattern (see entries/app-rsc-entry.ts) and how Pages Router exposes `vinextConfig.assetPrefix` from its own entry. The runtime-config.json sidecar was unnecessary — the App Router entry template already inlines config values as compile-time constants, so assetPrefix should be inlined the same way. Changes: - Add `assetPrefix` to `AppRouterConfig` in entries/app-rsc-entry.ts. - Emit `export const __assetPrefix = <json>` from the generated RSC entry alongside the existing `__basePath` / `__configHeaders` / `__expireTime` constants. Single-line declaration to stay within the "thin generated entry" guideline. - Thread `nextConfig.assetPrefix` through the generateRscEntry call site in index.ts. - App Router prod-server reads `rscModule.__assetPrefix` (with a string fallback for older builds) instead of `vinext-runtime-config.json`. - Delete the vinext-runtime-config.json write in index.ts. Nothing else in the repo reads this file (verified via rg on packages/ tests/ examples/). Pages Router behaviour is unchanged: it already gets assetPrefix from the embedded `vinextConfig` exported by pages-server-entry.ts. Addresses james-elicx review feedback on #1311. * fix(ssr): emit bootstrap as <script src> via bootstrapModules The App Router SSR pipeline previously passed only `bootstrapScriptContent` to React's renderToReadableStream, producing inline `<script>import("/path/to/entry.js")</script>` tags. The URL was therefore inaccessible to anything that reads `script.attribs.src` — including the Next.js asset-prefix fixture test "bundles should return 200 on served assetPrefix" which iterates script tags looking for a `src` attribute prefixed by the configured assetPrefix. Extract the bootstrap URL from the plugin-rsc-generated content and pass it as `bootstrapModules: [url]` so React emits a real `<script type="module" src="url">` tag. The existing `extractModulePreloadHtml` helper already did the same extraction for the modulepreload <link>, so the URL was already exposed (just renamed `extractBootstrapModuleUrl` and lifted out of the head-injection helper to share between the bootstrap and the preload tags). The inline `bootstrapScriptContent` is no longer needed — it was only carrying the dynamic-import call that bootstrapModules now performs natively. Passing both would load the bootstrap module twice. `bootstrapModules` works in both dev and prod: in dev, plugin-rsc emits a Vite dev-server URL (e.g. `/@id/__x00__virtual:vinext-app-browser-entry`) that the browser fetches as a module from the dev server; in prod it's the hashed bundle URL, optionally prefixed by `assetPrefix`. Mirrors Next.js's app-render path which combines both `bootstrapScriptContent` (for session state) and `bootstrapScripts` (for the actual chunk URL). See .nextjs-ref/packages/next/src/server/app-render/required-scripts.tsx. We use `bootstrapModules` instead of `bootstrapScripts` because vinext's chunks are native ES modules (Vite output) — a `type="module"` tag is the correct loader. Tests updated where they asserted on the old `import("URL")` shape: tests/asset-prefix.test.ts, tests/app-router.test.ts, tests/e2e/cloudflare-workers/navigation.spec.ts. Verified all related suites pass locally (asset-prefix, app-router, script-nonce, script-preload, use-client-page-pathname, document, prerender, ISR, fetch-cache, entry-templates, precompress). * fix(build): use resolveAssetsDir for Pages Router client entry fallback scan The Pages Router Cloudflare Workers post-build step scans the on-disk assets directory for the client entry chunk when the SSR manifest lookup doesn't surface one. The directory path was hardcoded to `<clientDir>/assets/`, which is the Vite default — but when `assetPrefix` is configured, the chunk lives under `<prefix>/_next/static/` (path-prefix) or `_next/static/` (absolute-URL prefix). The fallback would silently miss the entry and the worker would ship without its client bootstrap reference. Use the shared `resolveAssetsDir(assetPrefix)` helper that already drives `build.assetsDir` and the prod-server lookup, so this scan works for every supported asset layout. Addresses round-4 reviewer feedback on #1311 (low-priority follow-up item — only fires when the SSR manifest lookup fails). * feat(config): mirror Next.js basePath → assetPrefix fallback Next.js auto-sets `assetPrefix = basePath` when `basePath` is configured and `assetPrefix` is left unset (packages/next/src/server/config.ts:528-531). Without this, an app deployed under a basePath serves its routes correctly but emits its assets from `<basePath>/assets/...` (Vite's default `base + assetsDir` composition) rather than from the Next.js- canonical `<basePath>/_next/static/...`. Add the same fallback in resolveNextConfig, gated by Next.js's own conditions: - basePath !== "" (skip when basePath is unset) - basePath !== "/" (Next.js rejects this earlier; we mirror the guard so we never produce assetPrefix === "/") - assetPrefix === "" (user did not opt out) The fallback feeds the rest of the asset-prefix pipeline (Vite's `build.assetsDir`, `experimental.renderBuiltUrl`, and the prod-server URL→file resolver) so no other code paths need to change. Tests: - tests/next-config.test.ts: 5 unit tests for the four edge cases of the fallback condition. - tests/asset-prefix.test.ts: new end-to-end test that builds with `basePath: "/app"` (no assetPrefix), starts the prod-server, and asserts the bootstrap <script src> lands at /app/_next/static/... (and that the legacy /app/assets/... layout is gone). - tests/pages-router.test.ts: updated the "preserves basePath on backfilled SSR manifest entries" test to expect the new /docs/_next/static/ layout (was /docs/assets/). The manifest assertion is relaxed to startsWith("docs/") because Vite's raw SSR manifest now emits some duplicate-prefixed entries ("docs/docs/_next/static/...") alongside the backfilled ones; both forms anchor under basePath so the prod-server's URL→file lookup remains correct — and the user-visible HTML assertions in the same test are the source of truth. Addresses round-4 reviewer feedback on #1311. * fix(server): Pages Router must not strip basePath before asset lookup The Pages Router prod-server stripped `basePath` from the request pathname BEFORE handing it to `resolveAppRouterAssetPath`, which broke the asset lookup whenever the assetPrefix path-prefix branch was active with a path that happened to equal basePath. Concrete failure: with the round-4 commit `bac8425` mirroring Next.js's `basePath → assetPrefix` fallback, an app configured with `basePath: "/docs"` (and no explicit `assetPrefix`) now resolves `assetPrefix = "/docs"`. Emitted asset URLs are `/docs/_next/static/…` (correct). But the Pages Router branch did: const staticLookupPath = stripBasePath(pathname, basePath); // "/docs/_next/static/x.js" → "/_next/static/x.js" resolveAppRouterAssetPath(staticLookupPath, "/docs", "/docs"); // path-prefix branch needs pathname to start with "/docs/" → null → 404 The App Router branch already passes the un-stripped `pathname` to the same helper (see line ~1140). Match that behaviour. `staticLookupPath` is still computed and used downstream by non-asset paths (image-optimization endpoint, page routing), where the basePath-stripped form is correct. Test coverage: - tests/pages-router.test.ts: extended the existing "preserves basePath on backfilled SSR manifest entries" test to FETCH every emitted asset URL and assert HTTP 200 (previously it only inspected HTML for the URL string). This is the missing assertion that would have caught the bug the first time. - tests/asset-prefix.test.ts: added a parallel Pages Router e2e test that builds an inline pages/ fixture with `basePath: "/docs"`, starts the prod-server, collects every script/link URL from the SSR HTML, and asserts every URL serves a 200 + correct content-type. Confirmed to fail on `bac8425` before this fix and pass after. Addresses round-5 reviewer feedback on #1311. * refactor(ssr): accept either quote style in bootstrap URL regex plugin-rsc currently emits the bootstrap as `import("<url>")` with double quotes, but the contract isn't documented and a future version could switch to single quotes. Tighten the URL extraction regex to `import(["'])([^"']+)\1` so either form works. Addresses non-blocking round-5 review feedback on #1311. * docs(ssr): note CSP implications of bootstrapModules switch The round-3 commit `3a2c886` switched the App Router SSR bootstrap from inline `bootstrapScriptContent` (`<script>import(…)</script>`) to React's `bootstrapModules` option (`<script type="module" src=…>`). That has user-visible CSP consequences worth documenting at the call site so app authors hit them in code search rather than at runtime: - Apps no longer need `script-src 'unsafe-inline'` for the bootstrap (improvement — inline imports previously required it). - Apps that restrict script sources need `'self'` for same-origin, or the CDN origin when assetPrefix is an absolute URL. - React still threads nonce through to the emitted <script> tag, so nonce-based CSP (`script-src 'nonce-…' 'strict-dynamic'`) keeps working unchanged. Addresses non-blocking round-5 review feedback on #1311. |
||
|
|
aed79d46b0 |
test: regression tests for issue #1196 (catch-all route params with basePath + rewrites + middleware) (#1291)
* test: add regression tests for issue #1196 (catch-all route params with basePath + rewrites + middleware) Adds integration and unit tests to verify that catch-all route params are preserved when basePath, rewrites, and middleware are combined. - Integration test in pages-router.test.ts: checks server-rendered __NEXT_DATA__ contains correct page and query for a catch-all route. - Unit test in shims.test.ts: verifies client-side router.query stays intact for a catch-all page under basePath. Both tests are ported from the Next.js test suite and target the regression described in GitHub issue #1196. * test: address review feedback on #1196 regression tests - Use {.*?} regex for consistency with other __NEXT_DATA__ extractions - Drop unused { appRouter: false } option (vinext auto-detects) - Rename shims test to describe what it actually verifies (initial state) - Move vi.resetModules() after window restore for clean teardown ordering * test: add real client-nav test for #1196 catch-all router.query Adds a Router.push() test that mocks fetch and exercises the actual client-side navigation code path: performNavigation → pushState → navigateClient → fetch. The mocked fetch handler captures router state mid-navigation via a Probe component, then asserts router.query.path reflects the destination URL segments after basePath is stripped. This addresses the bonk review feedback that the previous tests only verified initial hydration state and didn't exercise client navigation. The new test also uses the previously-unused buildNavHtml `query` parameter, addressing the dead-signature concern. vinext's navigateClient doesn't construct _next/data/... URLs the way Next.js does, so it doesn't have the architectural pattern that causes #1196 in Next.js. But the route-param extraction path that ultimately produces router.query must still strip basePath from the resolved pathname before matching the catch-all pattern — and that's what this test verifies. --------- Co-authored-by: opencode <noreply@opencode.ai> |
||
|
|
329b89e3c3 |
fix(navigation): populate useParams/useSearchParams under Pages Router (#1303)
Under the Pages Router, hooks from `next/navigation` returned empty
objects:
expect(pagesParamsJSON).toEqual({ slug: 'foobar' }) // got {}
expect(pagesSearchparamsJSON).toEqual({ q: 'pages' }) // got {}
Next.js treats `next/navigation` as a cross-router compat shim. The
Pages Router server (render.tsx) and client (client/index.tsx) wrap
each page with SearchParamsContext / PathParamsContext / PathnameContext
providers populated from the router via `adaptForSearchParams` and
`adaptForPathParams` in
.nextjs-ref/packages/next/src/shared/lib/router/adapters.tsx.
vinext drives those hooks from a module-level navigation context that is
only set for App Router renders. For Pages Router renders the context is
null, so useParams / useSearchParams / usePathname fell through to empty
fallbacks.
This change adds a Pages Router compat source published by next/router
through a Symbol.for global handle. next/navigation reads that handle
when the App Router context is null and derives:
- pathname from window.location / SSR asPath
- searchParams from the URL query string only (not merged route params)
- params from the route pattern matched against the resolved path
The publisher is exposed via globalThis[Symbol.for(...)] rather than a
direct module import so navigation.ts does not become a transitive
dependency of every next/router consumer (which would pull in the
window.history.pushState patch at module-load time and break unit tests
that only need next/router).
Ports the behaviour covered by these Next.js tests:
- test/e2e/app-dir/params-hooks-compat/index.test.ts
- test/e2e/app-dir/use-params/use-params.test.ts ("should work on pages
router")
Adds vitest coverage at tests/pages-router.test.ts ("next/navigation
useParams returns only dynamic route params under Pages Router" and
"next/navigation useSearchParams returns only query string under
Pages Router") via a new fixture page at
tests/fixtures/pages-basic/pages/nav-compat/[slug].tsx that mirrors
.nextjs-ref/test/e2e/app-dir/params-hooks-compat/shared/params-component.js.
|
||
|
|
4aa7663084 |
fix(app-router): hard navigate stale RSC build payloads (#1178)
* fix(app-router): hard navigate stale RSC build payloads App Router soft navigation currently accepts any valid RSC payload after deploy. That lets stale tabs decode RSC responses produced by a different build, which can break client navigation when the server and browser artifacts no longer match. The missing invariant was that every RSC response consumed by the browser must identify the build that produced it, and the browser must reject payloads from a different build before handing them to React. Emit a vinext-owned build ID header on App Router RSC responses, preserve it in prefetched and visited response snapshots, and hard navigate when a response is missing or mismatches the current client build ID. * chore: fix e2e fixture hook lint suppression * chore: avoid hook-like fixture callback name * fix(app-router): preserve build IDs for cached RSC payloads * fix(app-router): use public RSC compatibility IDs |
||
|
|
07be32258f |
fix(router): preserve filesystem routes before afterFiles rewrites (#1166)
* fix(router): preserve filesystem routes before afterFiles rewrites afterFiles rewrites were evaluated before App and Pages filesystem route matches in several runtime paths. That let a rewrite override an existing non-dynamic page, which diverges from Next.js route ordering. The fix checks the page/app match first and only applies afterFiles rewrites when no non-dynamic route wins, while still allowing afterFiles to run before dynamic routes. Regression coverage exercises App RSC handler, Pages dev/prod, and generated Worker wiring. * test(router): align chained afterFiles rewrite expectations The chained rewrite fixture expected an afterFiles rewrite to override a concrete /intermediate page. That is the route-ordering behavior this branch fixes. Update the fixture to cover both intended contracts: middleware can chain into afterFiles when no page file wins, and concrete page files are not overridden by afterFiles rewrites. * refactor(router): remove route-ordering type assertions Validate generated Pages route metadata at the boundary instead of asserting its shape, and make the app handler test fixture route matching explicit. * test(router): cover afterFiles order in Pages Worker Add a built Cloudflare Pages Router regression where a concrete page route must win before an afterFiles rewrite. Align custom Pages Worker entries with the generated worker route-match gate. |
||
|
|
3adf2da4c4 |
chore(lint): enable stricter promise and error rules (#1148)
* chore(lint): enable stricter promise and error rules The lint config did not enforce several promise, exhaustive switch, template expression, self-import, and error-construction checks on source files. That let void callback promise boundaries and empty Error fixtures slip through review. Enable the rules for vinext source files, apply the useful test subset, and make the existing async callback boundaries explicit. Also remove the redundant unknown union and close response bodies in the affected Pages Router tests so server teardown remains deterministic. * chore(lint): require exhaustive test switches Test files already enforce strict async callback and error construction checks, but exhaustive switch checking was source-only. Enable switch exhaustiveness for tests and make the Pages Router response decoder handle the undefined encoding case explicitly. Tighten the allowedDevOrigins suite cleanup so the temporary dev server closes deterministically. |
||
|
|
0e62dc24c7 | Prefer Pages Router route params over same-key query params (#1119) | ||
|
|
634808bea4 |
perf(pages): cache ISR misses from streamed render (#1068)
* perf(pages): cache ISR misses from streamed render Pages Router ISR cache misses currently render the page once for the live response and then render a second React tree to produce cache HTML. That doubles userland render, data reads, suspense work, and render-scoped side effects on every cold cache fill. The cache writer now tees the already-rendered body stream, records the cache branch with streaming UTF-8 decoding, and writes the same shell and body that the client receives. Stream failures report through the Pages Router render error path and never persist partial HTML. Tests cover the single-render cache fill, split UTF-8 chunks, failed stream writes, and the Pages Router fixture state contract. * fix(pages): keep ISR cache error reporting non-fatal * chore: rerun CI |
||
|
|
33526bd89c |
fix(middleware): align cookies and external rewrites (#919)
Middleware cookies set through NextResponse only updated Set-Cookie, so cookies() in the same request could not observe values written by middleware. External middleware rewrites were normalized to pathname and search, so cross-origin destinations were routed locally instead of being proxied. Mirror middleware cookie mutations into x-middleware-set-cookie and merge that internal header back into the request cookie store. Preserve cross-origin rewrite URLs through middleware execution and proxy them across app, pages, prod, and deploy request paths, while stripping internal middleware headers from client and upstream responses. |
||
|
|
14ff303cbd |
feat: csp support (#795)
* fix: support csp * tweaks * tweaks * tweaks * tweaks * tweaks * tweaks * clean * tweaks * clean * fix test * tweaks * fix lock * fix test * trigger ci * tweaks * tweaks |
||
|
|
ec0fb3bb23 |
fix: silent IMPORT_IS_UNDEFINED for proxy.ts (#787)
* fix: silent IMPORT_IS_UNDEFINED for proxy.ts * tweaks |
||
|
|
a83be594f9 |
chore: turn on lint rule to prefer type over interface (#716)
* chore: turn on lint rule to prefer `type` over `interface` * fix vite-hmr.d.ts |
||
|
|
fef54db9b4 |
fix: skip MDX files during RSC scan-build to prevent parse errors (#668)
* fix: skip MDX files during RSC scan-build to prevent parse errors (issue #659) Adds a vinext:rsc-scan-mdx-guard plugin that runs with enforce:"pre" to ensure MDX files are skipped before rsc:scan-strip processes them. This prevents es-module-lexer parse errors when MDX files contain frontmatter (---) or JSX that isn't valid JavaScript syntax. Also adds a regression test that uses MDX files with YAML frontmatter loaded via import.meta.glob to verify the build succeeds. * fix(mdx): return stub module for undetected MDX files during scan-build The guard plugin was a no-op that passed raw MDX through unchanged. When rsc:scan-strip received this raw MDX, es-module-lexer.parse() failed on frontmatter (---) and JSX syntax. The real fix is in vinext:mdx: when mdxDelegate is null (no MDX files in app/ or pages/), return a valid JS stub module instead of undefined. This handles MDX files that enter the build graph via import.meta.glob outside app/pages. - Remove vinext:rsc-scan-mdx-guard plugin (was a no-op) - Modify vinext:mdx to return stub module when mdxDelegate is null - Remove app/mdx-probe/page.mdx from test (was hiding the bug) - Replace fragile '---' assertion with specific frontmatter checks Fixes issue #659 * fix(mdx): lazily compile globbed mdx imports Compile plain .mdx files on demand when they enter the graph via import.meta.glob outside app/pages so scan-build stops crashing without discarding runtime content. Restore the existing happy-path MDX fixture, add focused lazy-compilation coverage, and extract shared Cloudflare fixture setup to keep the regression tests honest. * fix review issues * fix: use absolute path for root tsconfig alias to fix import.meta.glob resolution toViteAliasReplacement was returning "/" (Vite root-relative convention) when the tsconfig path alias pointed to the project root (e.g. "@/*": ["./*"]). This worked for regular imports but broke import.meta.glob because Vite's glob plugin resolves patterns via this.resolve(), which couldn't properly resolve the "/" replacement to matching filesystem files. Changed to return the absolute path instead, which works correctly for both regular imports and glob pattern resolution. Signed-off-by: Divanshu Chauhan <divkix@divkix.me> Made-with: Cursor * refactor: address review feedback on MDX lazy initialization - Add clarifying comments for mdxDelegatePromise caching semantics - Document subtle control flow when hasUserMdxPlugin is true - Suppress duplicate warning on on-demand MDX init (only warn during detection) - Add case-insensitive extension check in transform hook - Add test for query parameter edge cases (?raw, ?v=123, etc.) * refactor: address review feedback on MDX handling - Merge duplicate query-param MDX skip tests in pages-router.test.ts - Tighten hasUserMdxPlugin control flow comment for clarity - Make scanDirForMdx case-insensitive to align with transform check Refs: PR #668 review feedback * docs: clarify mdx delegate fallback comment * chore: trigger ci --------- Signed-off-by: Divanshu Chauhan <divkix@divkix.me> |
||
|
|
207588d164 |
feat: add instrumentation-client support (#679)
* feat: add instrumentation-client support * trigger ci * fix deps * tweaks * tweaks * tweaks * tweaks * rebuild lockfile * fix: add app-with-src to CI E2E matrix --------- Co-authored-by: James <james@eli.cx> |
||
|
|
05cac9d745 |
build: align Vite 8 bundler config (#599)
* build: align Vite 8 bundler config * fmt --------- Co-authored-by: James <james@eli.cx> |
||
|
|
e683e0d84d |
feat: support inline next config (#649)
* feat: support inline next config * Apply suggestion from @james-elicx Co-authored-by: James Anderson <james@eli.cx> * tweaks --------- Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
f709e22209 |
fix: Pages Router SSR streaming (#514)
* fix pages router streaming * handle regressions * fix: align pages response merge behavior * fix: drop stale worker content-length headers * fix: preserve no-body rewrite parity * fix: cancel streamed HEAD responses * test: support merged prod server shape * Fix compressed Pages SSR streaming * Fix ecosystem fixture port collision |
||
|
|
a86b67dff4 |
feat: full next/compat/router support and fix vinext check scanner for shimmed modules (#584)
* feat: full next/compat/router support and fix check scanner for shimmed modules - Export NextRouter interface from shims/router.ts so compat-router.ts can use it - Add NextRouter | null return type to useRouter() in shims/compat-router.ts - Add next/compat/router, next/form, next/web-vitals, next/constants to IMPORT_SUPPORT in check.ts so vinext check no longer flags them as unsupported - Add integration fixture pages and components for next/compat/router in both pages-basic and app-basic fixtures - Add integration tests in tests/pages-router.test.ts and tests/nextjs-compat/hooks.test.ts - Add unit tests in tests/check.test.ts covering all four newly-registered imports * test: update entry-templates snapshots for compat-router-test fixture page * remove type assertion * fix: add declare module for next/compat/router to fixture next-shims.d.ts files * fix: add declare module for next/compat/router to internal next-shims.d.ts * pin to vp setup 1.0.0 |
||
|
|
9cec92beaa |
refactor: migrate ssrLoadModule to moduleRunner.import (#570)
* refactor: migrate ssrLoadModule to moduleRunner.import Replace all server.ssrLoadModule() calls in dev-server.ts and api-handler.ts with runner.import() via the ModuleImporter interface, following the Vite Module Runner migration guide. Remove ssrFixStacktrace calls which are not needed with Module Runner APIs. Update call sites in index.ts to pass the existing getPagesRunner() lazy factory as the runner argument. Update tests to use ModuleImporter-shaped mocks instead of the old ViteDevServer mock. * refactor: address bonk review comments - Fix misleading stack trace comments: ssrFixStacktrace is not applicable to ModuleRunner (not that it rewrites automatically) - Update ALS-ARCHITECTURE.md to reference runner.import() instead of server.ssrLoadModule() - Add importModule() typed helper to instrumentation.ts to centralise the Record<string, any> cast, removing 12 inline eslint-disable comments - Remove dead ssrLoadModule mock from pages-router ISR test (createSSRHandler no longer calls server.ssrLoadModule) |
||
|
|
499ee6a979 |
refactor: use vinext startProdServer HTTP for prerendering (#574)
* refactor: remove wrangler from prerender, use startProdServer HTTP for all builds All builds — plain Node and Cloudflare Workers — now prerender via HTTP against a locally-spawned startProdServer instance. Removes findWranglerConfig, loadWrangler, WranglerWorker, isWorkersBuild branching, and all related fields from PrerenderAppOptions. Fixes generateStaticParamsMap not being accessible from CF bundles by falling through to the RSC handler for prerender endpoints. * . * fix: redirect:manual for prerender fetch, remove wrangler peer dep - Add redirect:manual to renderPage fetch in prerenderPages so that getStaticProps redirect responses are captured instead of followed. Fixes the 'escapes meta refresh URL to prevent HTML injection' test. - Remove wrangler from devDependencies and peerDependencies now that all prerender logic goes through the HTTP prod server path. * refactor: extract readPrerenderSecret to shared utility, fix server-manifest secret rotation - Move readPrerenderSecret to src/build/server-manifest.ts so both build-time code (prerender.ts) and runtime code (prod-server.ts) can import it without creating a circular dependency - Fix vinext:server-manifest plugin calling randomBytes() independently in each Vite environment (rsc + ssr), causing the second env to overwrite the first with a different secret; generate the secret once at plugin creation time via an IIFE closure - Replace the two duplicate manifest-read blocks in prod-server.ts with calls to the shared readPrerenderSecret helper * fix: warn on non-OK response from pages-static-paths prerender endpoint A 403 (stale secret) or 500 from the endpoint was silently treated the same as 'no paths', causing dynamic routes to be skipped without any indication of why. Now logs a warning with the HTTP status code so a stale-secret scenario is visible in the build output. * fix: guard static-params fetch against non-OK responses in prerenderApp A 403 (stale secret) or 500 from /__vinext/prerender/static-params was passed directly to JSON.parse, which throws on a non-JSON body like 'Forbidden'. The rejected Promise was then cached in staticParamsCache, poisoning all subsequent generateStaticParams lookups for that route. - Check res.ok before JSON.parse; return null and warn on non-OK status - Evict the cache entry on rejection so a transient error does not permanently block the route |
||
|
|
75eb3c1e8d |
feat: production prerender pipeline (#553)
* feat: production prerender pipeline (no dev server)
Add prerenderPages() and prerenderApp() that load from production bundles
exclusively — no ViteDevServer dependency. runPrerender() replaces
runPrerenderWithDevServer() (kept as a shim for cli.ts/deploy.ts callers).
Key changes:
- packages/vinext/src/build/prerender.ts — new: prerenderPages/prerenderApp
- packages/vinext/src/build/run-prerender.ts — new: runPrerender + shim
- packages/vinext/src/build/static-export.ts — delegates to prerender layer
- packages/vinext/src/index.ts — adds rscOutDir/ssrOutDir/clientOutDir/disableAppRouter options
- packages/vinext/src/entries/app-rsc-entry.ts — exports generateStaticParamsMap
- packages/vinext/src/entries/pages-server-entry.ts — exports pageRoutes
- packages/vinext/src/shims/cache.ts — adds NoOpCacheHandler
- packages/vinext/src/server/prod-server.ts — serves prerendered .html files
- packages/vinext/src/build/report.ts — integrates PrerenderResult for build report
- tests/helpers.ts — buildPagesFixture/buildAppFixture helpers (isolated outDirs)
- tests/prerender.test.ts — 36 new tests
- tests/static-export.test.ts/app-router.test.ts/pages-router.test.ts — updated
All 487 tests pass. Typecheck, lint, fmt clean.
* fix: build Pages Router SSR bundle for hybrid app+pages projects
For hybrid projects that have both app/ and pages/ directories, the
App Router multi-env build (createBuilder/buildApp) does not produce
a Pages Router SSR bundle. The prerender phase then fails with 'bundle
not found at dist/server/entry.js'.
Fix: after buildApp() completes, detect pages/ presence and run a
separate standalone Vite SSR build with vinext({ disableAppRouter: true })
so the plugin's multi-env environments config does not override the SSR
input/entryFileNames. The build is run with emptyOutDir: false to preserve
RSC artefacts from the App Router build.
* Clean up run-prerender: remove deprecated shim, fix progress arithmetic and typos
* regen snaps
* refactor: clean up prerender pipeline, cache shim, and report utilities
- Extract loadBundle() helper used by both prerenderPages and prerenderApp
- Save/restore CacheHandler around prerender calls (isolation fix)
- Rename isExplicitlyDynamic -> isConfiguredDynamic for clarity
- Export getRscOutputPath and findDir for cross-module reuse
- Import findDir in run-prerender.ts; remove duplicated inline logic
- Fix single-row table corner character in formatBuildReport
- Remove duplicate JSDoc block before revalidateTag in cache shim
- Add SetCtx interface; eliminate (ctx as any) casts in MemoryCacheHandler
- Replace string sentinel in unstable_cache with structural CacheResultWrapper
- Use completedUrls += 1 consistently in Pages Router phase
* fix: remove unused appDir from PrerenderAppOptions and all callers
prerenderApp() never reads appDir — the bundle is loaded via rscBundlePath
and route scanning is done before the call. Remove the field from
PrerenderAppOptions and AppStaticExportOptions, and drop the now-unused
argument from all callers in run-prerender.ts, static-export.ts, and tests.
* refactor: miscellaneous cleanups across prerender, cache, cli, and deploy
* refactor: document gaps, re-detect project after installDeps, fix Vite resolution in deploy.ts
* refactor(prerender): unify onProgress to single call site; document pageCount semantics
Extract renderUrl() inner function from prerenderApp's concurrency loop so
that onProgress is called exactly once per URL at the outer loop level, rather
than being duplicated inside two early-return branches. Eliminates the risk
of accidentally omitting the callback when adding future early exits.
Add a comment in toStaticExportResult clarifying when pageCount and
files.length can diverge (currently they stay in sync).
* build(prerender): write non-export prerender output to dist/server/prerendered-routes/
On Cloudflare Workers, wrangler.jsonc uses not_found_handling: "none" so
every request hits the worker first. Files in dist/client/ are never
auto-served for page requests — they are uploaded but remain inert.
Writing prerendered HTML/RSC to dist/server/prerendered-routes/ keeps
them co-located with server artifacts and away from the static assets
directory.
This also prevents a future issue: when KV pre-population is implemented,
ISR route files must not be in dist/client/ or they would be served as
stale static files forever (bypassing revalidation).
output: 'export' builds are unaffected — static-export.ts passes its
own outDir explicitly, and runPrerender still writes to dist/client/
when mode === 'export'.
* .
* build: suppress IMPORT_IS_UNDEFINED warnings for generateStaticParams
Dynamic route pages that don't export generateStaticParams produce noisy
IMPORT_IS_UNDEFINED warnings because the virtual RSC entry unconditionally
emits mod?.generateStaticParams for every dynamic route. The optional
chaining guards the access safely at runtime; suppress the build-time noise
in the existing onwarn handler alongside the MODULE_LEVEL_DIRECTIVE filter.
* .
* .
* .
* fix conflict regression
* regen snaps
* update cli comment
* add todo for rsc double req
* encode uri components
* encode uri components
* fix: inherit user plugins in hybrid Pages Router secondary build
The secondary `vinext build` step for hybrid (App Router + Pages Router)
projects was constructed with `configFile: false` and only `vinext({ disableAppRouter: true })`,
dropping all user-supplied plugins including `cloudflare()`. This caused a
false-positive "Missing @cloudflare/vite-plugin" error on any hybrid project
with a `wrangler.jsonc` present.
Fix by inheriting the resolved plugin list from the already-completed App
Router builder, filtering out the `vinext:*` sub-plugins and re-injecting
`vinext({ disableAppRouter: true })` in their place. `configFile: false` is
retained to prevent the user's `environments` block from overriding the SSR
input and `entryFileNames`.
* fix: filter vite:react and vite-tsconfig-paths from inherited hybrid build plugins
vinext auto-registers both @vitejs/plugin-react (vite:react*) and
vite-tsconfig-paths when disableAppRouter is false, so inheriting the
resolved plugin list from the App Router builder caused duplicates.
Filter them out alongside the vinext:* sub-plugins.
* fix: filter rsc:* and vite-rsc-* plugins from inherited hybrid build plugins
@vitejs/plugin-rsc registers resolveId for App Router virtual modules
(virtual:vinext-app-ssr-entry etc.) but the corresponding load hooks live
in vinext:* — which we already strip. With rsc:* present but vinext:* absent
the virtual module resolves but can't load, crashing with PLUGIN_ERROR.
Filter rsc:* and vite-rsc-load-module-dev-proxy alongside the other
auto-registered plugin families.
* fix: skip missing-Cloudflare-plugin guard for hybrid Pages Router secondary build
The secondary build uses configFile:false + vinext({disableAppRouter:true})
so it never loads cloudflare() — by design, it's a plain SSR Rollup bundle
with no Workers config. The configResolved guard was incorrectly treating
this as a misconfigured user build.
Revert the inherited-plugins approach (cloudflare() reconstructs the full
multi-env environments block from its own plugin config hook, independent
of vite.config, causing the App SSR virtual entry to be resolved but not
loadable). Instead, gate the guard on !options.disableAppRouter, which is
exclusively set by this internal invocation.
* fix: inherit user transform plugins in hybrid Pages Router secondary build
The secondary SSR build for hybrid projects (app/ + pages/) was introduced
on this branch and always used configFile:false with only
vinext({disableAppRouter:true}), silently dropping any user transform
plugins from vite.config.ts (SVG loaders, CSS-in-JS, etc.).
Fix by loading the raw user config via loadConfigFromFile (before any
plugin config() hooks fire, so cloudflare() hasn't yet injected its
multi-env environments block) and forwarding all plugins except the
families that vinext auto-registers or that would break the plain SSR
build: vinext:*, vite:react*, rsc:*, vite-tsconfig-paths,
vite-rsc-load-module-dev-proxy, and vite-plugin-cloudflare*.
* fix: reset ANSI styling before prerender and route report output
Vite's logger leaves the terminal in a styled state after build output.
Write an ANSI reset before the prerender label and route report so they
always print at full brightness regardless of preceding Vite output.
* fix: skip instrumentation register() during prerender to prevent process hang
Instrumentation modules like @vercel/otel register OpenTelemetry SDK
exporters with background timers that keep the Node process alive
indefinitely. During prerender these side-effects cause the process to
hang after rendering completes, preventing the route report from printing.
Set VINEXT_PRERENDER=1 in prerenderApp/prerenderPages and gate
__ensureInstrumentation() on that env var so instrumentation is skipped
during prerender builds.
* feat: production prerender pipeline for Cloudflare Workers (App Router + Pages Router)
- Add wrangler devDep to packages/vinext; add `@cloudflare/workers-types` to catalog
- Fix prerender.ts: typed wrangler import, Unstable_DevWorker, undici Response casts,
URL-string extraction for dev.fetch() (no Request object), staticParamsMap Proxy
- Remove process.env.VINEXT_PRERENDER gate from /__vinext/prerender/static-params endpoint
- Add cf-app-basic pages/ fixture (index, about, posts/[slug], api/ping)
- Update buildCloudflareAppFixture to also run buildPagesFixture for Pages Router bundle
- Add CF prerender tests: shared beforeAll, nested App Router + Pages Router describes (46/46)
* .
* feat: CF Workers hybrid build Pages Router prerender support
* regen snaps
* fix(prerender): detect CF Workers build via @cloudflare/vite-plugin in node_modules
Replace wrangler.json file-presence check with node_modules detection,
consistent with how deploy.ts detects CF projects. The old check looked
in dist/server/ which never has a wrangler.json, so isWorkersBuild was
always false for CF projects, causing only the 404 to be prerendered.
The generated dist/server/wrangler.json (from @cloudflare/vite-plugin)
is now used as the config path for unstable_dev, with the project-root
wrangler.jsonc as a fallback. This ensures assets.directory is present,
which wrangler 4+ requires.
* fix(prerender): address bonk review comments
- runWithConcurrency: early return on empty items instead of spawning a
spurious worker via the '|| 1' fallback
- staticParamsMap Proxy: flip has() trap to return false so the typeof-fn
check works for routes without generateStaticParams on CF Workers builds;
also handle null return from the proxy fn (no generateStaticParams) in
the parent-params expansion path
- loadWrangler(): extract shared helper with a two-candidate fallback
(wrangler-dist/cli.js → index.js) used by both prerenderApp and
runPrerender, replacing duplicated path resolution + existence checks
- VINEXT_PRERENDER process.env mutation: add explanatory comments
documenting why the global mutation is intentional and safe for the
sequential-call contract
* refactor(prerender): extract findWranglerConfig, pass CF detection through, cache static-params
- Extract findWranglerConfig(serverDir, projectRoot) helper from both prerender.ts and
run-prerender.ts, eliminating the duplicated 4-candidate wrangler.json search
- Add isWorkersBuild and wranglerConfigPath optional fields to PrerenderAppOptions so
runPrerender can pass its already-computed values into prerenderApp, avoiding a
redundant findInNodeModules walk + 4-candidate fs.existsSync loop on every build
- Add a per-build staticParamsCache (Map keyed on pattern+parentParams) inside the CF
Proxy to dedup repeated /__vinext/prerender/static-params round-trips for deeply
nested dynamic routes
- Add clarifying comment on renderUrl's runWithHeadersContext wrapper explaining it is
a no-op for the CF Workers path (rscHandler is an HTTP proxy; ALS context never
crosses the isolate boundary) but kept for shape-compatibility across both modes
* fix(prerender): address remaining bonk review comments
- Security: gate /__vinext/prerender/* endpoints behind VINEXT_PRERENDER=1 check
to prevent exposure in normal deployments (process.env works for both Node and
CF Workers via Miniflare's var injection into process.env)
- Bug: buildUrlFromParams now throws a clear error when a required param is
missing instead of silently producing 'undefined' in the URL
- Design: add TODO comments for layout-level generateStaticParams limitation in
both resolveParentParams() and generateStaticParamsMap
- Correctness: prerenderPages() now uses runtime module exports (getServerSideProps/
getStaticProps) to classify page type on Node builds instead of static file
analysis; CF builds continue to use classifyPagesRoute() as fallback
* fix(prerender): address latest bonk review comments
- Add shape validation for parentParams on the /__vinext/prerender/static-params
endpoint: JSON.parse result is guarded to ensure user generateStaticParams always
receives a plain object, never a primitive, array, or null
- Add .gitignore for tests/fixtures/cf-app-basic/dist/ to prevent accidental commits
of build output if test cleanup fails (CF fixture builds to source tree, unlike
other fixtures that use tmpdirs)
- Add comment on nextConfigOverride shallow merge in run-prerender.ts to make the
limitation explicit for future maintainers
* regen snaps
* fix: build CF fixture in tmpdir instead of source tree
Use createIsolatedFixture in buildCloudflareAppFixture so the CF Vite
build output goes to a tmpdir rather than tests/fixtures/cf-app-basic/dist/.
Adds an optional nodeModulesDir param to createIsolatedFixture so callers
with fixture-scoped deps (like @cloudflare/vite-plugin) can point the
symlink at the fixture's own node_modules instead of the workspace root.
Removes the stopgap .gitignore and the afterAll that deleted dist/ from
the source tree.
* fix: cache-bust prod-server import() to prevent stale module reuse in tests
startProdServer() used a bare file:// URL for its dynamic import() of the RSC
entry bundle. Node's module cache keyed on that URL, so when two test describe
blocks rebuild to the same output path the second invocation always got the
cached module from the first build. The stale module had __instrumentationInitialized
already set to true and globalThis.__VINEXT_onRequestErrorHandler__ pointing at the
first build's instrumentation instance, whose capturedErrors array lived in a
different module instance than the one the production route handler was reading.
Fix: append ?t=<mtime> to the import URL, matching the pattern used by prerender.ts
loadBundle(). Same mtime means same content (cache hit, no-op); new mtime means
a fresh build and gets a fresh module. Applied to both startAppRouterServer and
startPagesRouterServer.
Also removes debug console.log calls from the instrumentation production test.
* fix: use globalThis for instrumentation test state to survive sequential prod builds
In Vitest, the 'App Router Production build' and 'App Router Production
server' describes run in the same process. The first build's preview server
imports dist/server/index.js uncached, setting
globalThis.__VINEXT_onRequestErrorHandler__ to onRequestError_v1 from the
first module instance. The second build is loaded cache-busted (by mtime),
producing a fresh module instance (v2). After v2 sets the handler, v1's
__ensureInstrumentation can re-fire and overwrite it, causing errors from
v2 to be recorded in capturedErrors_v1 (a different array) while the GET
route reads capturedErrors_v2 (empty).
Fix: store capturedErrors and registerCalled on globalThis (same pattern
as the middleware counter) so all module instances write to and read from
the same shared state regardless of which build instance is active.
Also removes debug logging and restores the afterAll dist cleanup that was
commented out during investigation.
* add process.exit(0) at end of build
|
||
|
|
25f6e0fc87 |
chore: enable typeAware and typeCheck, use vp check (#551)
* chore: enable typeAware and typeCheck, use vp check
* Build
* Try cache false
* Revert "Try cache false"
This reverts commit
|
||
|
|
c17d6941be |
chore: migrate to vite plus (#535)
* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit
|
||
|
|
04bff2c823 |
fix: concurrent SSR isolation for pages router head/router state (#532)
* test: add concurrent ssr isolation fixture pages for pages router * test: add pages router dev concurrency isolation tests * fix: register head/router als accessors in ssr module graph * test: add prod concurrency isolation tests * docs: add als architecture and per call scope docs * fix: move als accessor registration to one time setup in createSSRHandler * test: add assertion messages and explicit guards for ci debugging * test: use lookahead in extractMetaContent for attribute order independence * test: add comment explaining prod head test limitations * docs: clarify pr #450 wording in als architecture doc * test: add random delay in fixture getServerSideProps to increase interleaving * test: align concurrent-router query type with ParsedUrlQuery * test: use shared createIsolatedFixture helper in prod concurrency tests * docs: clarify ssrLoadModule guidance in als architecture doc |
||
|
|
21558f3092 |
feat: support @vitejs/plugin-react v6 as peer (#525)
* feat: support @vitejs/plugin-react v6 as peer * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update * Update |