mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
codex/cacheability-platform-io
27 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6946d43db6 | fix(cache): preserve explicit route handler policy | ||
|
|
a5b6d05b83 | fix(app): authenticate static file signals (#3077) | ||
|
|
0bc39ef5bd |
fix(pages): apply fallback rewrites after API misses (#2827)
* fix(pages): apply fallback rewrites after API misses * fix(pages): distinguish API normalization from rewrites |
||
|
|
9f1018cd23 |
fix(middleware): preserve headers for empty override value (#2767)
* fix(middleware): preserve headers for empty override value * fix(middleware): ignore stray headers for empty override * fix(middleware): require override list for forwarded headers * test(middleware): cover stray request header value * test(middleware): pair valid and stray header overrides * fix(middleware): preserve unlisted forwarded headers * fix(middleware): preserve literal forwarded headers |
||
|
|
0b58bbc8ff |
fix(metadata): preserve Content-Length for fully buffered responses (#2703)
* fix(metadata): preserve Content-Length for fully buffered responses closeAfterResponseWithBody() wraps every body-bearing response in a TransformStream to support function-form after(), which strips Content-Length even when the body is already fully materialized. Metadata file convention responses (robots(), sitemap(), manifest(), static icons) always serialize to a string or byte array with no producer left, so mark those bodies (markFullyBufferedBody) and skip the wrap when nothing is registered. Arbitrary Route Handler responses and a metadata route's Response passthrough stay wrapped: their body's producer may still call after() after the handler resolves, so a "nothing registered yet" check can't prove them safe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqasUFjUPt2Q8gkrzrLGyB * fix(metadata): preserve deferred response lifecycle --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: James <james@eli.cx> |
||
|
|
2fcc4443cc |
test(isr): cover production cache lifecycle (#2651)
* fix(isr): preserve cache headers on initial render * fix(isr): preserve streaming on initial cache misses * test(isr): run production lifecycle coverage in CI * test(isr): wait for asynchronous cache writes --------- Co-authored-by: James <james@eli.cx> |
||
|
|
de94652a1d |
fix(shims): align public API with vendored Next types (#2617)
* fix(shims): align public API with vendored Next types * fix(shims): preserve revalidation and runtime behavior * test(shims): align cache revalidation expectations * fix(ci): package types for deploy suite |
||
|
|
23dd15f763 |
fix(prod-server): stop double-evaluating the server bundle when chunks import the entry back (#1924)
* fix(prod-server): stop double-evaluating the server bundle when chunks import the entry back The production server imported the built server entry with a ?t=<mtime> query string. Node keys its ESM cache on the full URL including the query, so any code-split chunk that resolves the same entry file by its plain URL evaluated the entire server bundle a second time. Default Vite builds on both supported majors (Rollup on Vite 7, Rolldown on Vite 8) hoist modules shared between the entry's static graph and lazy route chunks into the entry chunk, and the chunks import them back as ../../index.js; realpath- canonicalized chunk ids also reach the entry via its real path when the server was started through a symlinked path. Module-level singletons then diverged between the two copies: boot-time initialization ran on the instance the server imported while route handlers read the never- initialized duplicate. Import the entry by its bare canonical (realpathed) file:// URL on first use so all importers converge on one module instance, and only append the ?t= cache-buster when the same path is imported again with a different mtime — the same-process rebuild scenario the query existed for. * docs(prod-server): clarify rebuild-branch tradeoff and probe constraints (#1924) - note in resolveServerEntryImportUrl's JSDoc that the ?t= rebuild branch trades back the single-instance guarantee (bare chunk back-imports keep resolving to the first build); freshness + single instance only hold together on the first import of a path - clarify the realpath try/catch only covers realpathSync.native failures, not missing entry paths (those still throw at statSync, as before) - document that the prod-singleton-state fixture probe is only valid on a freshly built output path |
||
|
|
d2ffc83670 |
fix(config): define cache components flag as boolean (#1903)
* fix(config): define cache components flag as boolean Disabled cache components currently compile to a truthy "false" string. That diverges from Next.js and makes bundled user code that checks !!process.env.__NEXT_CACHE_COMPONENTS select the cache-components path even when next.config rewrites select the legacy destination. Mirror Next.js by defining process.env.__NEXT_CACHE_COMPONENTS as a boolean expression, and keep the local slot shim tolerant of both boolean defines and string env values. Add a faithful revalidatePath-with-rewrites production regression that proves rewritten static and dynamic routes invalidate after revalidating the resolved destination path. * test(prod-server): cover cacheComponents enabled revalidatePath with rewrites * test(prod-server): skip dist/ copy in cacheComponents fs.cpSync filter Addresses review feedback: fs.cpSync(APP_FIXTURE_DIR, fixtureRoot) copies the earlier-built dist/ directory into the temp fixture before it is immediately deleted. Adding a filter skips the unnecessary copy. |
||
|
|
de33c3e5a0 |
fix(build): inline ../-relative font assets in OG routes (#1866)
* fix(build): inline ../-relative font assets in OG routes
The vinext:og-inline-fetch-assets plugin only matched paths starting
with "./" (e.g. "./font.ttf"), but Next.js test fixtures for OG custom
fonts use "../"-relative paths like "../../../assets/typewr__.ttf".
Without the inline, at runtime:
- Cloudflare Workers: import.meta.url is "worker" (not a URL), so
new URL("../../../assets/...", import.meta.url) throws TypeError.
- Node.js: fetch() does not support file:// URLs, so the inlined
file:// URL produced by the import-meta-url plugin fails.
Fix: relax the regex from `\.\/[^"']+` to `\.[^"']+` so any
dot-relative path (./x, ../x, ../../x, etc.) is inlined as base64.
Apply the same fix to the readFileSync(fileURLToPath(...)) pattern.
Failing tests addressed:
- test/e2e/og-routes-custom-font: should render og with custom font
for app routes (edge runtime fetch + Node.js fs.promises.readFile)
- test/e2e/app-dir/metadata-font: should handle custom fonts in both
edge and nodejs runtime
* fix(build): inline OG font assets when a formatter adds a trailing comma
The vinext:og-inline-fetch-assets regex only matched the single-line,
comma-less form of `fetch(new URL(...)).then((res) => res.arrayBuffer())`.
When a formatter (Prettier `trailingComma: "all"`, oxfmt) wraps the call
across lines it appends a trailing comma:
.then((res) =>
res.arrayBuffer(),
)
which no longer matched, so the font was left as a runtime fetch. On
Cloudflare Workers `import.meta.url` is "worker", so
`new URL("../...", "worker")` throws "TypeError: Invalid URL" and the OG
route returns 500 — even though the ../-relative path itself was already
supported. Relax both `.then(...)` alternatives with an optional `,?`.
Add real e2e coverage (`/api/og-custom-font`) mirroring Next.js'
og-routes-custom-font fixture: an edge OG route that loads
assets/noto-sans.ttf via
`fetch(new URL("../../../assets/noto-sans.ttf", import.meta.url))`. It
runs in tests/e2e/og-image.spec.ts across the app-router (Node dev),
cloudflare-dev and cloudflare-workers (workerd) projects, with the route
and font added to both the app-basic fixture and the app-router-cloudflare
example. Also adds a unit test for the formatted/trailing-comma shape in
tests/og-inline.test.ts.
* fix(build): inline OG font assets with a semicolon-terminated block body
Follow-up from the /bigbonk review on PR #1866: the block-body alternative
of the og-inline-fetch-assets regex ended `…\.arrayBuffer\(\)\s*\}?\s*,?\s*\)`
with no allowance for a `;` before `}`, so formatter output such as
.then((res) => {
return res.arrayBuffer();
})
.then(function (res) { return res.arrayBuffer(); })
was left as a runtime fetch — which throws "Invalid URL" on Workers
(import.meta.url === "worker"). Add `;?` before the block-body `}` and unit
tests for the arrow- and function-expression block-body forms.
|
||
|
|
49d753217b |
fix(app-router): propagate "use cache" tags to route-handler ISR entries (#1453) (#1848)
* fix(app-router): propagate "use cache" tags to route-handler ISR entries (#1453) * fix(app-router): bubble nested use-cache tags to parent on data cache HIT (#1453) |
||
|
|
8edc010a10 |
fix(fetch-cache): dedupe identical render fetches (#1134)
* fix(fetch-cache): dedupe identical render fetches Uncached and no-store fetches currently bypass the persistent fetch cache by calling the original fetch directly. That diverges from Next.js, where the patched fetch wraps the original fetch in a request-scoped dedupe layer before persistent cache policy runs, so repeated GET and HEAD fetches during a render share one network response. Add request-scoped fetch dedupe state to the fetch cache context and route network misses and cache-bypass fetches through it. The dedupe key follows the Next.js method, header, mode, redirect, credentials, referrer, referrerPolicy, and integrity semantics while still opting out for abort signals, keepalive, and side-effecting methods. Update fetch-cache coverage for uncached render dedupe, independent response bodies, request-scope isolation, trace header exclusion, and the changed no-store/no-cache interaction. * fix(fetch-cache): give each cloned dedupe response its own Headers Pass `new Headers(response.headers)` per clone instead of sharing the same Headers object across both branches and the dedupe entry. Matches Next.js' `cloneResponse` and avoids a future-correctness trap if any code path mutates response headers post-fetch. Also drops two redundant `runWithFetchDedupe` calls inside `dispatchAppPage` (the ISR revalidation render path and the intercept stream render path) — both sites are inline anonymous functions that already inherit the dedupe scope from the outer `dispatchAppPage` / `runAppPageRevalidationContext` wrap, so the inner calls were no-ops and just obscured that inheritance. * test(fetch-cache): cover Request input dedupe + always clone bodyless responses Always construct fresh Response objects in cloneDedupeResponse, including the bodyless path that previously returned the same `response` reference for both tuple slots. Mirrors Next.js' cloneResponse and avoids any "disturbed response" surprise if a runtime tracks consumption state on the shared reference. Factor the per-clone construction into buildDedupeClone() so bodied and bodyless clones share the headers-copy + url-restore + finalizer-register treatment. Adds a comment near entry.response assignment noting that the unconsumed tee branch is bounded by the render scope: when runWithFetchDedupe exits, the dedupe map becomes unreachable and the FinalizationRegistry cancels any still-unconsumed branch. Also adds two tests covering the Request-object input path of createFetchDedupeCandidate — one verifying dedupe applies for identical Request inputs, one verifying differing non-trace headers on Request inputs prevent dedupe. * fix(fetch-cache): drop failed dedupe entries so later callers can retry When the original fetch rejects, the dedupe entry stayed in the map with response: null. Subsequent callers within the same render scope chained on the rejected promise — propagation was correct, but they could never retry, because the failed entry blocked fresh attempts for the rest of the scope. React.cache() (which Next.js uses) avoids this because each call site naturally retries on failure. Splice the entry out of the URL bucket on rejection so a later fetch to the same URL within the same render scope creates a fresh entry and re-issues the upstream request. Also align the fetch-dedupe-metadata fixture with fetch-dedupe-isr-metadata by replacing the `as CountBody` cast with a runtime assertCountBody check. * docs(fetch-cache): document scope inheritance + harden ISR test stabilization Rename `dispatchAppPageWithDedupe` to `dispatchAppPageInner` so the name reflects that it runs *inside* the dedupe scope rather than activating it. Add comments at the four `runWithFetchDedupe` / `renderToReadableStream` sites explaining how each one relates to the surrounding dedupe scope: - app-page-render and app-page-boundary: defensive wrap, no-op under dispatch; standalone callers must keep an outer scope alive across async stream consumption since `runWithFetchDedupe` of a synchronous fn only covers the synchronous portion. - ISR revalidation render and intercept render in dispatch: explicitly note why no inner wrap is needed (the outer revalidation context / dispatch wrapper already activated dedupe). - runWithFetchDedupe doc: document the ALS-scope-vs-async-consumption caveat that ties this together. Replace the fixed 200ms post-condition sleep in the ISR background dedupe test with a 500ms count-stabilization poll. A stray third upstream fetch (which would betray dedupe leaking across the metadata + page render boundary) is now caught regardless of when it fires. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
31b3e17843 |
fix(thenable-params): protect well-known properties and unify page/route handler implementation (#1050)
Route handlers used a local Object.assign(Promise.resolve(...), params) helper that did not protect any well-known properties from shadowing. Page params used a Proxy but only guarded then/catch/finally, missing React's Promise status field and other well-known properties. Next.js explicitly reserves a well-known property set (then, catch, finally, status, toString, hasOwnProperty, etc.) so that params with those names do not break Promise behaviour or React introspection. This change: - Expands the shared makeThenableParams shim to protect the full well-known property set, matching Next.js reflect-utils.ts. - Replaces the weak route-handler local helper with the shared shim. - Adds unit tests for status and Object.prototype method shadowing. - Adds App Router integration tests for pages and route handlers with params named then, catch, finally, and status. Ported from Next.js: - https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/utils/reflect-utils.ts - https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/cache-components/cache-components.params.test.ts |
||
|
|
246aa1d72e |
fix: add SWR non-blocking guard tests for route handler ISR (#975)
* fix: add SWR non-blocking guard tests for route handler ISR (#959) Audit confirms vinext correctly uses ctx.waitUntil() for all background cache-write and regeneration work — no equivalent of the Next.js pendingWaitUntil / pipe-readable blocking pattern exists. ## Changes - Add slow ISR route handler fixture (revalidate=1, 1s handler delay) - Add response-time assertion to existing STALE test (< 500ms) - Add new test verifying stale response completes fast despite slow background regeneration Ported from Next.js: test/e2e/app-dir/use-cache-swr/use-cache-swr.test.ts * docs: clarify porting attribution in SWR ISR tests Adjust comments to note test pattern is adapted from Next.js "use cache" SWR tests, not a direct translation. The mechanism differs (export const revalidate vs "use cache"). |
||
|
|
e9139542cb |
fix(app-router): reject middleware control responses in route handlers (#939)
* fix(app-router): reject middleware control responses in route handlers App Route handlers currently treat NextResponse.next() and NextResponse.rewrite() as ordinary 200 responses. That diverges from Next.js, where those helpers are middleware control-flow signals and are rejected after a route handler returns. The route-handler execution path now validates returned responses before cache policy or response finalization runs. Focused unit and integration tests cover the invalid next and rewrite helper responses. * Update tests/app-route-handler-execution.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/vinext/src/server/app-route-handler-response.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(app-router): validate route handler ISR regeneration responses Stale App Route handler regeneration called runAppRouteHandler directly and could serialize a middleware control response into the ISR cache. That bypassed the validation used by the live route-handler execution path. Run the same response assertion before background regeneration writes a new APP_ROUTE cache entry, restore exact Next.js error wording for parity, and cover the x-middleware-next boundary semantics. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> |
||
|
|
7c5ca2f193 |
fix: apply middleware request-header overrides to App Route request objects (#809)
* fix: apply middleware request-header overrides to App Route request objects App Route handlers received a tracked NextRequest built from the original request object even when middleware had supplied request-header overrides. That meant headers() saw the middleware-modified view via ALS context, but request.headers inside the route handler still exposed the original Authorization/Cookie headers. Fix: - preserve a clone of the middleware response headers before internal x-middleware-* headers are stripped for response safety - thread that preserved header set into the typed App Route execution module - rebuild the underlying Request with buildRequestHeadersFromMiddlewareResponse() before constructing the tracked NextRequest Also adds an App Route fixture and integration test proving that request.headers and headers() now agree after middleware deletes credential headers and injects a trusted internal header. * chore: trigger fresh CI run for PR #809 * regen snaps --------- Co-authored-by: James <james@eli.cx> |
||
|
|
30c86e015a | refactor: extract app route handler request runtime (#618) | ||
|
|
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
|
||
|
|
f12c2005a6 |
feat: add ISR caching for App Router route handlers (#523)
* feat: add ISR caching for App Router route handlers Route handlers with `export const revalidate = N` now get server-side ISR caching (MISS/HIT/STALE with background regeneration), matching the existing page ISR behavior. Previously only Cache-Control headers were emitted — the handler re-executed on every request. - Add `__isrRouteKey` helper (suffix "route") for cache key construction - ISR cache READ before handler execution (HIT skips handler entirely) - ISR cache WRITE on MISS via `response.clone()` + `waitUntil` - STALE serves stale data and triggers background regen with clean context (synthetic request, empty headers/cookies) - Guards: production-only, GET/HEAD only, skips when handler is dynamic (uses headers()/cookies()), skips when handler sets own Cache-Control, filters Infinity from revalidateSeconds - Reuses existing CachedRouteValue (kind: "APP_ROUTE") and KV serialization * chore: update entry-templates snapshots for route handler ISR * test: add auto-HEAD ISR cache test for route handlers * fix: review findings — force-dynamic guard, consistent header filtering in cache writes * test: poll for ISR background regen instead of fixed sleep Replace fixed 1000ms sleep with a polling loop (up to 5s) to wait for the background regen to complete. The fixed sleep was flaky because regen timing varies with system load. Also bumps the stale wait from 1500→2000ms for slow CI. * fix: capture request.url before regen closure to avoid pinning Request Hoist request.url into a local const (__revalUrl) before the background regeneration closure. This lets the original Request object (with its body stream and headers) be GC'd sooner on memory-constrained Workers isolates. * fix: address remaining review findings - Hoist url.searchParams into __revalSearchParams before the regen closure so the outer URL object can be GC'd during background regen (same class of fix as the request.url hoisting) - Add test for force-dynamic + revalidate route handler guard - Add comment noting order-dependent test cache state |
||
|
|
d29ee0eebb |
fix(route-handlers): align Allow and default-export behavior with Next.js (#497)
* fix: ensure route handlers send correct Allow header * Inline route handler Allow helpers in RSC entry * fix: refresh entry snapshots and fmt * fix(route-handlers): align Allow and default-export behavior with Next.js * fix(route-handlers): align default-export behavior with Next.js per review Follow-up for PR #497 review feedback (requested changes). Changes: - stop throwing on default export in route handlers - log dev-only warning when default export is detected - ignore default export for dispatch so default-only handlers return 405 - keep sorted Allow on implicit OPTIONS and no Allow on 405 - update integration/e2e tests, snapshots, and tracking notes |
||
|
|
0db5c8d99b | fix: avoid shared cache headers for dynamic GET handlers (#381) | ||
|
|
764a496ce7 |
add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs * rebuild lockfile * fix: add Format to required checks list, remove dead ignore pattern * run fmt * add format to agents.md again |
||
|
|
7fdcdd3c69 |
feat(og): add @next/og / @vercel/og support with Cloudflare Workers compatibility (#356)
* feat(og): add @next/og support with Cloudflare Workers compatibility
- Add vinext:og-font-fix transform plugin that patches @vercel/og/dist/index.edge.js
at Vite transform time: inlines the fallback font as base64 (avoids fetch() with
import.meta.url which breaks in workerd) and defers WASM init to first use (avoids
WebAssembly.instantiate() being blocked in Node.js module runner)
- Add @vercel/og to optimizeDeps.exclude so Vite's esbuild pre-bundler doesn't cache
the module before the transform hook runs
- Add vinext:og-assets build plugin that copies font/WASM assets to RSC output dir
- Add /app/api/og route to app-router-cloudflare example and app-basic fixture
- Add shared Playwright e2e spec (tests/e2e/og-image.spec.ts) asserting PNG validity,
correct 1200x630 dimensions, param-driven content, and determinism
- Register og-image.spec.ts in app-router, cloudflare-workers, and cloudflare-dev
Playwright projects (18 tests total, all passing)
* refactor(og): generalize fetch+import.meta.url asset inlining, drop WASM lazy-init patch
Replace the @vercel/og-specific vinext:og-font-fix plugin with a general
vinext:og-inline-fetch-assets plugin that:
- Matches ANY fetch(new URL('./asset', import.meta.url)).then(res => res.arrayBuffer())
pattern, not just the hardcoded noto-sans filename
- Resolves each asset relative to the module's on-disk path (via the transform id)
and inlines it as base64 — works for any font version or additional assets
- Applies to any library using this pattern, not just @vercel/og
Also removes the WASM lazy-init patch: now that @vercel/og is in optimizeDeps.exclude,
the module runs through workerd (not the Node.js pre-bundler cache) where
WebAssembly.instantiate() works fine. The lazy-init workaround was only needed
when the pre-bundler cache bypassed the transform hook.
The og-assets build plugin is updated to only copy resvg.wasm (not the font,
which is now inlined and will no longer appear in the bundle by filename).
* fix(og): inline readFileSync assets from @vercel/og node build
The production RSC bundle resolves @vercel/og under the 'import' export
condition, getting dist/index.node.js instead of dist/index.edge.js.
The node build loads font and WASM via fs.readFileSync(fileURLToPath(
new URL('./file', import.meta.url))), which breaks after bundling because
import.meta.url points to the bundle output file, not the dist/ dir.
Extend vinext:og-inline-fetch-assets with a second regex pattern that
matches readFileSync(fileURLToPath(new URL(...))) and replaces it with
Buffer.from('<base64>', 'base64'), inlining both noto-sans font and
resvg.wasm at transform time. This eliminates the ENOENT errors seen
in the production build CI tests.
* refactor(og): address code review feedback
- Remove Fix 1 (font inlining) from vinext:og-font-patch — it was dead
code since vinext:og-inline-fetch-assets (earlier in the plugin array)
already handles the same fetch(new URL(...)) pattern generically. Only
Fix 2 (yoga WASM extraction) remains in og-font-patch.
- Use split().join() instead of String.replace() in both Pattern 1 and
Pattern 2 loops of og-inline-fetch-assets. String.replace(string, ...)
only replaces the first occurrence; split().join() replaces all of them,
which matters for libraries that may use the same pattern multiple times.
* refactor(og): use replaceAll instead of split().join()
|
||
|
|
a5d65b851f |
test: add a fixme for middleware invocation count tracking (#350)
* test: add a fixme for middleware invocation count tracking * test: clarify why registerCalled uses ESM live binding not globalThis |
||
|
|
80acf27933 |
fix: register instrumentation in rsc entry and put error handler in global scope (#330)
* test: instrumentation * store onRequestError on globalThis * add instrumentation registration app-dev-server * guard against double calling runinstrumentation for appdir |
||
|
|
a78266481b |
feat: support export const revalidate on App Router GET route handlers (#320)
* chore: add .worktrees/ to gitignore * feat: support `export const revalidate` on App Router GET route handlers Read the route segment config `revalidate` from route handler modules and set `Cache-Control: s-maxage=N, stale-while-revalidate` on GET responses, matching Next.js behavior. Only applies when the handler does not set its own Cache-Control header. Unskip two E2E fixme cases and add vitest coverage. * fix: guard revalidate=0 and add proper test fixtures - Skip Cache-Control injection when revalidate is 0 (means "never cache") - Add /api/custom-cache fixture that sets its own Cache-Control header - Add /api/no-cache fixture with revalidate=0 - Fix "does not override" test to use a fixture that actually sets Cache-Control - Add revalidate=0 test case |
||
|
|
12fea722b6 | Initial public release of vinext |