mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.7
39 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7487886b38 |
fix(prod-server): send headers-only for HEAD in sendCompressed (#1980) (#2058)
sendCompressed wrote the full body for every request. HEAD requests routed through it (bot/crawler-buffered Pages HTML and Pages API routes) still compressed and emitted a payload that Node then discards at the socket level. Short-circuit with res.end() after writeHead when req.method is HEAD, mirroring sendWebResponse. This skips spinning up a compressor for a body that would be thrown away and keeps the buffered sender consistent with the streamed and static-file senders, which already handle HEAD. Co-authored-by: James <james@eli.cx> |
||
|
|
ef003c0ebe |
fix(server): q-value-aware Accept-Encoding negotiation (#2047)
* fix(server): implement q-value-aware Accept-Encoding negotiation and add tests * docs(server): update comments for Accept-Encoding parsing to clarify q-value handling * fix(server): enhance q-value handling in Accept-Encoding parsing and add tests * fix(server): enhance q-value handling in Accept-Encoding parsing and add tests for isEncodingAccepted * fix(tests): update negotiateEncoding tests to reflect zstd as highest preference in prod-server.ts * fix(tests): update negotiateEncoding tests * fix(tests): remove redundant test for explicit refusal overriding wildcard in negotiateEncoding * fix(server): normalize encoding checks in isEncodingAccepted function * refactor(server): extract accept-encoding helpers * fix(tests): remove redundant zlib check and import HAS_ZSTD from accept-encoding module * fix(server): honor Accept-Encoding quality weights * fix(server): repair accept-encoding CI checks * fix(server): harden content encoding negotiation * fix(server): align encoding negotiation with Next.js * refactor(server): remove unused encoding parse state * fix(server): normalize empty vary headers --------- Co-authored-by: James <james@eli.cx> |
||
|
|
33b24a27c9 | refactor(instrumentation): resolve instrumentation hook files in forward-slash space (#2128) | ||
|
|
bacee9ef52 |
fix(pages): emit canonical __NEXT_DATA__ JSON (#2043)
* fix(pages): emit canonical next data script * test(pages): expect canonical next data markup * fix(pages): initialize canonical next data before routing * test(pages): parse canonical next data markup * fix(pages): preserve router bootstrap ordering * test(pages): expect readiness bootstrap import * fix(pages): guard readiness bootstrap without next data |
||
|
|
10b5086f4d |
fix(app-router): align router autoscroll with Next (#2004)
* fix(app-router): align router autoscroll with Next App Router navigation could diverge from Next.js router autoscroll semantics around hydration timing, loading-shell same-page search navigations, and routes whose first committed node is React-hoisted into head. That broke the upstream router-autoscroll deploy-suite because the client marked hydration too early, static loading-shell templates were not usable for navigation, and the fallback scroll path masked the old-handler hoisted-head behavior. The fix moves the Next-compatible hydration marker into a passive effect, wires production hydrateRoot error callbacks to match Next's implicit root-boundary and recoverable-error handling, lets authoritative loading-shell templates drive static route optimistic payloads, and skips fallback document scrolling when the committed document only exposes a React-hoisted head resource. The ported tests cover the upstream router-autoscroll behavior plus focused unit boundaries for prefetch mode, optimistic routing, head-resource detection, and hydrateRoot callbacks. * fix(app-router): hoisted-head scroll fallback is per-intent, not global head scan * test(app-router): add full-chain scroll intent integration test * fix(app-router): match legacy scroll focus parity * fix(app-router): preserve latest scroll navigation intent * fix(app-router): scope scroll and error recovery * fix(app-router): gate navigation failure recovery * fix(app-router): match navigation failure handling * fix(app-router): align scroll completion parity * fix(app-router): retain latest failure target * fix(app-router): preserve route error boundaries * fix(app-router): clear discarded failure targets * fix(app-router): align failure recovery boundaries * fix(app-router): disarm refused recovery targets * fix(app-router): preserve failure target ownership * fix(app-router): match boundary recovery parity --------- Co-authored-by: James <james@eli.cx> |
||
|
|
5ac620c2d0 |
fix(cache): honor fetch opt-outs and force-dynamic revalidate parity (#1907)
* fix(cache): honor fetch opt-outs in app page output
App Router fetches that explicitly opt out of caching could still leave the rendered page eligible for static output, while dynamic = "force-dynamic" was lowered to fetchCache = "force-no-store" and overrode explicit per-fetch cache/revalidate options.
The cache decision was conflating route-level dynamic rendering with a hard segment fetchCache mode. Next.js treats force-dynamic as a default no-store mode only for fetches without explicit cache or revalidate config, and marks explicit uncached fetches as dynamic for the surrounding render.
Mark explicit uncached fetch decisions through the existing dynamic usage signal, keep force-dynamic as a separate per-request fetch default, and preserve Response.url when reconstructing cached fetch responses.
* fix(cache): correct revalidate:false and Response.url semantics
- Split revalidate: false out of the no-store path; it now caches
indefinitely (INFINITE_CACHE) matching upstream patch-fetch.ts.
- Store actual response.url (freshResp.url / response.url) in cached
fetch entries instead of the input request URL.
- Remove markUncachedFetchForPageOutput() from the auth-header fallback
so it matches upstream's autoNoCache path and does not mark pages
dynamic.
- Update tests to reflect correct revalidate: false caching behavior
and add a test for redirect-style response URLs.
* fix(app-router): resolve target route dynamic config during intercepts and revalidation
Prevents the outer route's force-dynamic default from leaking into
interception source routes and ISR revalidation targets.
- app-rsc-entry.ts: expose __resolveRouteDynamicConfig
- app-page-dispatch.ts: pass resolveRouteDynamicConfig to dispatch,
sync currentForceDynamicFetchDefault when the render target changes
- app-page-dispatch.test.ts: 4 tests covering intercept + revalidation
paths for both lost and leaked defaults
* fix(app-router): sync fetchCache and force-dynamic default during action redirects/re-renders
* fix(cache): treat revalidate:false as non-explicit for force-dynamic parity
Upstream patch-fetch.ts computes noFetchConfigAndForceDynamic using
!currentFetchRevalidate (truthiness), so revalidate: false is treated
as 'no fetch revalidate config' and force-dynamic wins. vinext's
hasExplicitRevalidateValue() previously returned true for false,
blocking the force-dynamic no-store default and causing the fetch to
cache for 1 year instead.
Update hasExplicitRevalidateValue to exclude false (and 0) from the
explicit set, aligning with upstream's truthiness check. Add a parity
test covering force-dynamic + next.revalidate: false.
* fix(cache): split revalidate predicates for force-dynamic vs segment defaults
The previous fix to hasExplicitRevalidateValue() treated false and 0 as
non-explicit, which matched upstream's force-dynamic truthiness check
but broke segment cache defaults: default-cache would override
revalidate: 0 to force-cache, and default-no-store would override
revalidate: false to no-store.
Split into two helpers:
- hasExplicitRevalidateValue(): any defined value is explicit (for
segment defaults where 0 and false are explicit opt-outs)
- isFalsyRevalidate(): false and 0 are falsy (for force-dynamic's
noFetchConfigAndForceDynamic parity check)
Add parity tests for default-cache + revalidate: 0 and
default-no-store + revalidate: false.
* refactor(cache): extract ONE_YEAR_SECONDS and cover only-no-store revalidate:false
* fix(cache): keep dynamic fetch observations for auth bypass and cache-key fallback
Restore recordDynamicFetchObservation in the auth-header safety bypass so
auth-keyed fetches still downgrade the page output to fresh render, and
revert the cache-key-generation fallback to a plain observation so an
internal limitation (oversized/unserializable body) does not mark the
whole page dynamic. Lock in force-dynamic vs explicit segment fetchCache
precedence with a test.
* docs(cache): note deliberate no-cache dynamic-marking divergence from upstream
* fix(route-handler): apply segment fetchCache and force-dynamic fetch default to route handler dispatch
Route handlers never called setCurrentFetchCacheMode /
setCurrentForceDynamicFetchDefault, so a force-dynamic route handler did
not get the per-request no-store fetch default that page dispatch applies,
and an explicit fetchCache export on a route handler module was ignored.
Upstream's app-route module copies userland.fetchCache into the work store
and sets workStore.forceDynamic for dynamic = "force-dynamic", which
patch-fetch turns into a no-store default for fetches without explicit
cache config — for route handlers as well as pages. Mirror that in
dispatchAppRouteHandler and re-apply the state inside the background
regeneration request context.
* fix(fetch-cache): tolerate cached entries missing url and document revalidate:false cacheability
* fix(fetch-cache): fall back to the request URL for cached entries missing url
Reconstructing Response.url from the fetch input preserves the
pre-existing guarantee for legacy/foreign cache entries instead of
degrading to "". Also document why the intercept dispatch deliberately
does not save/restore fetch defaults around buildPageElement.
* docs(fetch-cache): note cached responses now join body-stream cleanup
* docs(fetch-cache): note explicit segment fetchCache beats force-dynamic default
* test(fetch-cache): pin tags-only fetch to no-store under force-dynamic
* docs(fetch-cache): document auth-bypass precedence and test no-store + auth
- Note at the auth-safety bypass that explicit no-store/no-cache/revalidate: 0
takes the stronger branch first and fully marks the page dynamic; the bypass
only covers implicitly-cacheable auth-keyed fetches.
- Add a test locking in that explicit no-store with auth headers routes through
markUncachedFetchForPageOutput rather than the softer auth bypass.
- Clarify in dispatchAppRouteHandler that the fetch-cache setters are new
wiring for route handlers, not a mirror of a pre-existing pattern.
* test(route-handler): lock no-store fetch bailing ISR for revalidating handlers
A route handler with revalidate = 60 that performs
fetch(url, { cache: "no-store" }) must skip its ISR cache write and be
marked known-dynamic, now that the patched fetch's explicit no-store
branch calls markDynamicUsage() (upstream patch-fetch parity). Exercises
the real fetch-cache shim and the real headers-shim dynamic-usage pair,
matching the app-route-handler-dispatch wiring.
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
88a9947e14 |
fix(trailing-slash): canonical url trailing slash support (#1888)
* fix: canonical url trailing slash support * fix(metadata): prevent trailing slash on file-like and .well-known canonical URLs * fix(metadata): remove trailing slash handling for .well-known URLs * test: add tests for createAppPageRouteBodyMetadata function with body placement * fix(metadata): match trailing slash URL scope * fix(metadata): resolve URL object alternates --------- Co-authored-by: James <james@eli.cx> |
||
|
|
2d9f3ffc36 |
fix(app-router): accept icons.other as single descriptor (#1492) (#1667)
Next.js's `metadata.icons.other` accepts either a single
`IconDescriptor` or an array of them, but `MetadataHead` iterated
it directly and threw `TypeError: metadata.icons.other is not iterable`
when given a single object. That crash bubbled up into the page
render so no `<link>` tags (favicon, shortcut, apple, custom) reached
the head at all for any page declaring `icons.other` as an object.
Normalize `metadata.icons.other` to an array before iterating, widen
its type to `OtherIconDescriptor | OtherIconDescriptor[]`, and also
forward the optional `type` attribute so descriptors like
`{ rel: "mask-icon", url, type: "image/svg+xml" }` round-trip
correctly. Ported coverage from
`test/e2e/app-dir/metadata-icons/metadata-icons.test.ts`.
Refs cloudflare/vinext#1492
|
||
|
|
4b6b438b94 |
fix(app-router): stream generated metadata for non-html bots (#1585)
* fix(app-router): stream generated metadata for non-html bots Dynamic App Router metadata was always folded into the route head. That diverged from Next.js for normal browser-like requests, where generated metadata is sent through a body outlet while html-limited bots still receive blocking head metadata. Track whether the matched route uses generateMetadata, thread next.config htmlLimitedBots into the generated RSC entry, and choose head versus body placement at page element construction. * test(app-router): cover streaming metadata bot edge cases Generated metadata must keep the default html-limited bot list when serialized config contains a falsy regex source. Empty config strings previously produced an empty regex, which treated every user agent as blocking and moved generated metadata back into the head. Add regression coverage for the default Twitterbot path and the streaming body serializer, then match Next.js by falling back to the default bot regex for any falsy htmlLimitedBots value. * fix(app-router): validate html-limited bot regex config Invalid serialized htmlLimitedBots values could reach request handling and throw while building the bot matcher. That puts a config error on the request path and recompiles the same matcher for repeated requests. Validate serialized regex sources while resolving next.config and reuse compiled bot matchers in the streaming metadata helper. The focused tests cover invalid config, falsy fallback, and matcher reuse. * refactor(config): narrow next config option reads Config resolution had several local assertions around experimental options, Turbopack aliases, output mode, and Sass options. Those assertions made the boundary wider than necessary in the code adjacent to htmlLimitedBots handling. Read optional records, strings, arrays, and body-size inputs through typed narrowing helpers so the resolved config path keeps the same semantics without spreading unchecked asserted values. * fix(config): keep bot regex helper out of server graph Config resolution imported the streaming metadata server module to validate htmlLimitedBots. In RSC-backed App Router integration tests, that cross-layer import can pull server runtime modules through config resolution and surface rsc reference-validation errors. Move the shared html-limited bot matcher cache to a neutral utils module and import it from config and the server streaming metadata helper. This keeps config validation independent from the server runtime graph while preserving the cached matcher behavior. |
||
|
|
8df248863d | fix: match Next metadata image route behavior (#1436) | ||
|
|
cd1013ea33 |
feat(config): add experimental.appShells plumbing (#1415)
* feat(config): add experimental.appShells plumbing Accepts experimental.appShells in next.config and defines process.env.__NEXT_APP_SHELLS=false at build time, matching Next.js PR #93997 plumbing scope. Behavorial implementation is gated on upstream co-flags that vinext does not yet support. - Adds experimental.appShells to CONFIG_SUPPORT as unsupported - Sets __NEXT_APP_SHELLS define to false regardless of config value - Tests for vinext check detection and Vite define injection Fixes #1405 * review: address PR nits - Expand experimental.appShells detail to explain why it's unsupported - Move build-time define tests into their own describe block, so the __NEXT_APP_SHELLS define test isn't nested under basePath * review: drop orphaned server lifecycle from basePath describe The two remaining resolveNextConfig tests import from source directly and don't use the dev server. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
b06a1a1d04 |
fix(i18n): strip locale prefix for API routes (#1408)
* fix(i18n): strip locale prefix for API routes Mirror Next.js's behaviour where a locale-prefixed request path like `/fr/api/ok` is matched against `pages/api/ok` after stripping the locale segment. Previously the locale prefix was kept through the `/api/` startsWith check, so any locale-prefixed API URL 404'd. Add a small helper `stripI18nLocaleForApiRoute(url, i18nConfig)` in `server/pages-i18n.ts` that mirrors Next.js's `normalizeLocalePath().pathname` (see packages/next/src/shared/lib/i18n/normalize-locale-path.ts) and wire it into all three Pages Router request pipelines so dev/prod parity holds: - dev plugin: `packages/vinext/src/index.ts` - prod (Node): `packages/vinext/src/server/prod-server.ts` - prod (Cloudflare worker entry): `packages/vinext/src/deploy.ts` App Router is intentionally unchanged: Next.js does not support the `i18n` config field with App Router, so App Router route handlers under `app/*/route.ts` never see a locale prefix to strip. Refs #1336 (item 3). Items 2 (sticky locale on client navigations) and 4 (default-locale path normalisation) are being handled in parallel PRs. Tests: - Unit tests for `stripI18nLocaleForApiRoute` covering basic, regional (`nl-NL`), query-preserving, unconfigured-prefix, and null-config cases (`tests/pages-i18n.test.ts`). - Dev integration tests (`tests/features.test.ts`) and prod integration tests (`tests/pages-i18n-prod.test.ts`) that GET `/fr/api/ok` and assert a 200 "ok" response against the existing `pages-i18n-domains` fixture, ported from Next.js's `test/e2e/middleware-redirects/test/index.test.ts` ("should redirect to api route with locale"). - Updates to `tests/deploy.test.ts` assertions for the generated worker-entry template strings. * test(after-deploy): update worker-entry assertion after locale strip The Pages Router worker entry now calls handleApiRoute(request, apiLookupUrl, ctx) instead of using resolvedUrl directly, so this generated-code assertion needs the new variable name. Refs #1336 (item 3). |
||
|
|
afc549d624 |
fix(router): normalize trailing slash parity (#1316)
Trailing slash handling treated every non-api path the same on server redirects, so trailingSlash:true added slashes to file-looking catch-all routes and Pages Router imperative navigation skipped the client canonicalization path used by Link. Next.js splits the invariant between route-manifest redirects and client resolveHref normalization: file-looking paths lose a trailing slash, non-file paths gain one, and queries stay attached to the canonical pathname. Share the server redirect decision across Pages Router runtimes and apply the same client helper in Router.push and Router.replace. Adds focused regressions for catch-all dot segments, query preservation, .well-known exclusion, and Pages Router history writes. |
||
|
|
1551f91cbd |
fix(metadata): match Next dynamic route semantics (#1317)
* fix(metadata): match Next dynamic route semantics Metadata routes diverged from Next.js in several observable cases: robots.txt emitted Sitemap before Host, dynamic metadata Response results could miss framework cache headers, TSX sitemap files were not discovered, and rendered metadata URLs treated canonical, manifest, and file-based social image routes as the same URL class. The implementation now follows Next's metadata route extension, cache, robots, and URL resolution rules while keeping file-based social image route markers non-enumerable so resolved metadata shape does not leak internal state. * fix(metadata): gate social image preview fallback File-based social image routes used the preview deployment URL whenever VERCEL_URL was present outside development. That let deployment URLs override an explicit metadataBase in non-preview production environments. Gate preview URL fallback on VERCEL_ENV=preview, add coverage for metadataBase precedence and production fallback URLs, and keep static metadata image cache headers stable in development. |
||
|
|
b69844ec3f |
fix(metadata): apply ancestor templates to title defaults (#1256)
Metadata title defaults currently render without the active ancestor template. That diverges from Next.js when a child layout or page provides title.default under a parent layout title.template. The merge path resolved only the final title after collecting templates, so object defaults skipped the stashed template used for that segment. Resolve each title as it is encountered against the current ancestor template, then stash the current layout template for descendants. Covers the child-layout default regression and updates page-default expectations to match Next.js resolveTitle semantics. |
||
|
|
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. |
||
|
|
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> |
||
|
|
5ee9860b24 |
fix(middleware): preserve NextResponse.next status (#1115)
NextResponse.next({ status }) currently continues routing but drops the middleware response status. That diverges from Next.js middleware handling and makes successful route rendering mask middleware-selected statuses such as 404.
The middleware runtime now exposes a status override for continue responses, and App Router plus Pages Router dev/prod consumers feed that value into their existing middleware response contexts. Rewrite status behavior remains covered by the same path.
Tests cover the middleware runtime contract, App Router context propagation, and a Pages Router dev integration case with a real matched page.
|
||
|
|
2bd7377f70 |
fix(shims): match Next metadata merge and Twitter inheritance (#1055)
* fix(shims): deep-merge nested metadata and add OG/Twitter inheritance
mergeMetadataEntries did a flat top-level key replacement, so setting
openGraph: { title: "Override" } in a child layout erased all root
OpenGraph fields like siteName and images. Next.js resolves these
per-subkey; we now shallow-merge the nested objects for openGraph,
twitter, icons, alternates, robots, and other.
Setting only openGraph.title without twitter.title produced no Twitter
Card tags because vinext skipped Next.js's postProcessMetadata step.
We now auto-fill twitter:title from og:title (or metadata:title),
twitter:description from og:description (or metadata:description),
and twitter:images from og:images, and default twitter:card to
summary_large_image when images are present.
Ported from Next.js:
- https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/metadata/resolve-metadata.ts
- https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts
* fixup! fix(shims): deep-merge nested metadata and add OG/Twitter inheritance
Extract postProcessMetadata from mergeMetadataEntries to avoid
polluting intermediate merge results during layout accumulation.
Run it once at the end of resolveAppPageHead after file-based
metadata has been applied, matching Next.js's accumulator pattern.
Also fix two issues caught by Copilot review and CI:
1. Twitter auto-fill now works even when openGraph is absent —
metadata.title and metadata.description serve as fallback sources.
2. Remove all as assertions from production code; use typed locals
and direct index access via Metadata's string index signature.
3. Update app-page-head.test.ts expectations to match post-processed
output and add missing regression test for no-openGraph case.
* chore: address review feedback — clone input, ?? for title, missing test
- postProcessMetadata now shallow-clones its input to prevent accidental
mutation of the caller's object.
- resolveStringTitle uses ?? instead of || to avoid treating empty
string as falsy (semantically correct but practically no difference).
- Added comment explaining why verification, appleWebApp, appLinks,
and formatDetection are excluded from DEEP_MERGE_KEYS.
- Added test: child can clear a parent openGraph subkey by setting it
to undefined, verifying twitter auto-fill skips cleared images.
* fix(shims): match Next metadata segment replacement
Nested metadata objects were being merged across segments, which kept stale parent OpenGraph, Twitter, alternates, icons, and robots fields when a child segment defined the same key.
Next only merges custom other metadata across segments. Replace the broad nested merge with an explicit other merge, keep OG-to-Twitter post-processing gated by openGraph, and update metadata tests to lock in the compatibility contract.
|
||
|
|
1d7c031e30 | feat(app-router): feed file metadata routes into head output (#891) | ||
|
|
364e36bcd8 |
fix(image): strip priority prop before forwarding to UnpicImage to prevent DOM leak (#662)
* fix(image): strip priority prop before forwarding to UnpicImage to prevent DOM leak
The `priority` prop is a Next.js-specific concept that must never reach the
DOM as an attribute. On the two UnpicImage render paths (remote URL with fill
and remote URL with width+height), `priority={true}` was forwarded directly
to `@unpic/react`'s Image component which did not reliably strip it before
rendering the DOM `<img>`, triggering:
Received `true` for a non-boolean attribute `priority`.
Fix: replace `priority={priority}` with the equivalent HTML semantics —
`loading={priority ? 'eager' : loading ?? 'lazy'}` and
`fetchPriority={priority ? 'high' : undefined}` — matching what the local
image and custom-loader paths already do correctly.
Adds 10 reproduction tests covering both affected render paths.
* test: add 50ms slack to compressed streaming timing assertion to fix CI flakiness
|
||
|
|
5f1681bce7 |
Fix tsconfig alias transforms for RSC builds (#655)
* Fix tsconfig alias transforms for RSC builds * Fix flaky compressed streaming timing test |
||
|
|
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 |
||
|
|
2ef52ad66b |
fix: use page extensions for middleware files (#595)
* fix: use page extensions for middleware files * same thing for instrumentation |
||
|
|
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
|
||
|
|
85f8c493b7 |
fix: handle URL objects in metadata resolveUrl (#465)
* fix: handle URL objects in metadata resolveUrl
The Next.js Metadata API types URL fields as `string | URL` (e.g.
`metadataBase`, `openGraph.url`, icon/image URLs). The internal
`resolveUrl()` function in `MetadataHead` only accepted `string`,
so passing a `URL` object (which is valid per Next.js types) crashed
with `url.startsWith is not a function`.
Add type coercion at the top of `resolveUrl()` to convert `URL`
objects to strings before processing.
Reproduction:
1. Set `metadataBase: new URL("https://example.com")` in a layout
2. Set `openGraph: { url: "/page" }` in a page's metadata
3. `resolveUrl` receives the URL object, calls `.startsWith()` on it
4. Crash: `url.startsWith is not a function`
With this fix, `URL` objects are coerced to strings via `.toString()`.
* fix: update Metadata types and rendering to accept string | URL fields
Address bonk review feedback:
- Add test covering URL object input for alternates.canonical and
openGraph.url to lock in the fix against regressions
- Update Metadata interface to type URL fields as string | URL,
matching Next.js types (openGraph.url/images/videos/audio,
twitter.images, icons, manifest, alternates.canonical/languages/
media/types)
- Add overload to resolveUrl() so it returns string when called with
a non-undefined argument, eliminating string | URL fallback issues
- Fix image/icon array normalisation to handle bare URL objects
alongside strings and descriptor objects
* fmt
* fix: remove dead-code ?? fallbacks and unnecessary test casts
Address bonk re-review feedback:
- Remove 'as unknown as string' casts in the URL-object test since the
Metadata interface now types those fields as string | URL
- Remove all 17 remaining 'resolveUrl(x) ?? x' fallbacks; resolveUrl
is overloaded to return string for any truthy input so these never
fired, and with string | URL fields the fallback value would have
passed a URL object directly to content=/href= attributes
* fmt
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
8d10bc64fd |
fix: MemoryCacheHandler revalidate:0 creates immediately-stale entries (#503)
* fix: MemoryCacheHandler revalidate:0 creates immediately-stale entries MemoryCacheHandler.set had an inconsistent guard on data.revalidate: the ctx path checked `revalidate > 0` but the data path only checked `typeof data.revalidate === "number"`, allowing revalidate:0 to set revalidateAt = Date.now() — making entries immediately stale. This was inconsistent with KVCacheHandler which already had the > 0 guard on both paths. The two handlers would produce different staleness behavior for the same input: Memory → immediately stale, KV → no TTL. Add the missing `> 0` guard to align both code paths and both handlers. * fix: revalidate:0 should skip cache storage entirely Both MemoryCacheHandler and KVCacheHandler had inconsistent handling of revalidate:0. In Next.js, revalidate:0 means "don't cache." But the handlers would still store entries — Memory made them immediately stale (data path lacked > 0 guard), KV cached them forever (both paths rejected 0, so revalidateAt=null meant no expiry). Resolve effective revalidate from both ctx and data paths (data overrides ctx), and early-return without storing when it's 0. Both handlers now use identical logic. Fix existing test data that incidentally used data.revalidate:0 in tests meant to exercise stale-while-revalidate and tag invalidation. |
||
|
|
38dc409a43 |
feat: Add Pages Router i18n domain routing (#471)
* Add Pages Router domain locale routing * Add coverage for Pages Router domain locale behavior * Format Pages Router i18n domain routing changes * Handle basePath in Pages Router i18n domains * Refactor Pages Router i18n domain fixtures * Update Pages Router entry snapshot after rebase * Align domain locale redirects with Next.js * Clarify Pages Router domain locale helpers * Fix case-insensitive locale prefixes * Simplify same-domain locale URL handling * Cover same-domain locale alias redirects * Document Pages Router i18n invariants * Guard preferred locale in domain redirects |
||
|
|
d093c3317b |
fix: Pages Router ISR background regeneration re-renders HTML (#487)
* fix: Pages Router ISR background regeneration re-renders HTML Background regeneration only re-ran getStaticProps but cached the old HTML, so server-rendered content was permanently stale after the first revalidation cycle. The pageData JSON was updated but never used to re-render the page. Now the regeneration callback: - Re-renders the page component with fresh props - Rebuilds __NEXT_DATA__ with fresh props - Updates the revalidate duration map (dev server) - Passes full locale context to getStaticProps Fixed in both dev server and production server entry. * ci: retrigger checks * fix: preserve document shell during prod ISR regeneration The prod entry's regeneration was hardcoding a simple HTML suffix, losing custom _document content between <Main /> and <NextScript />. Now splits the cached HTML at known markers (<div id="__next"> and __NEXT_DATA__ script) to preserve the full document structure: head section, gap content, and tail are carried forward from the original cached entry. Also updates the entry-templates snapshot. * ci: retrigger checks * ci: retrigger checks |
||
|
|
319f9296ff |
feat: ISR caching for App Router (production-only, stale-while-revalidate) (#405)
* feat: add ISR caching for App Router (production-only, stale-while-revalidate)
- Inline ISR helpers (__isrGet, __isrSet, __triggerBackgroundRegeneration,
__isrCacheKey, __pendingRegenerations) in the generated RSC entry
- Cache READ fires before buildPageElement for prod pages with revalidate > 0,
returning cached HTML/headers without loading component modules on HIT/STALE
- Cache WRITE (+ X-Vinext-Cache: MISS header) is guarded by
process.env.NODE_ENV === 'production' so dev never reads/writes the cache
- Background regenration is deduplicated via __pendingRegenerations Map and
registered with ctx.waitUntil for Cloudflare Workers
- Thread ExecutionContext (ctx) from app-router-entry.ts through to _handleRequest
- Dev mode still emits correct Cache-Control headers based on export const revalidate
- Add integration tests: Cache-Control in dev, no X-Vinext-Cache in dev, RSC
requests bypass ISR, pages without revalidate emit no ISR headers
- Add 10 code-generation tests verifying the prod guard, helpers, and structure
* fix: remove unused isrCachePath variable and fix formatting
* fix: cache RSC wire format (rscData) in ISR entries; serve RSC requests from cache
Previously the ISR cache only stored HTML and excluded all RSC requests
from cache reads/writes. This caused two bugs:
1. RSC requests (client-side navigation, link prefetch) always triggered a
full component tree render even when a fresh cache entry existed, wasting
CPU on every soft-nav and prefetch.
2. rscData was never captured — the cache entry had rscData: undefined —
so there was no way to serve RSC requests from cache even if we wanted to.
Fixes:
- Tee the RSC stream before SSR on ISR-eligible HTML requests (prod + revalidate > 0)
to capture the RSC wire format as an ArrayBuffer stored in rscData
- Background regen also tees its RSC stream to keep rscData fresh
- Remove the !isRscRequest guard from the ISR READ block: both HTML requests
and RSC requests now hit the cache. HTML requests get cached html; RSC
requests get cached rscData (falling through to render if rscData is absent)
- HIT and STALE paths branch on isRscRequest: RSC responses get
Content-Type: text/x-component and the rscData bytes; HTML responses get
Content-Type: text/html and the html string
Tests updated:
- Replace 'RSC requests return RSC stream without cache headers' test with
two tests that assert Cache-Control IS emitted for RSC responses on ISR
pages (dev mode), and that Next-Router-Prefetch requests also get it
- Replace .tee() count test with assertion that >=2 tees exist (RSC + HTML)
- Add rscData storage assertions for WRITE and background regen paths
- Add assertion that ISR READ block has no !isRscRequest guard
* fix(isr): fix RSC cache miss and early-exit performance for App Router ISR
Two bugs fixed:
1. RSC requests (client-side nav / prefetch) were never cached because the RSC
stream tee that captures rscData was placed AFTER the 'if (isRscRequest)'
early-return, so the capture branch was never consumed. Move the tee to
immediately after renderToReadableStream — before the isRscRequest branch —
and register a waitUntil in the RSC path to write rscData to the cache.
2. The ISR cache read block fired after the generateStaticParams call, meaning
a cache hit still did the expensive static-params validation. Move the ISR
read to before generateStaticParams so a HIT skips all that work.
Tests: add two new generateRscEntry ISR assertions:
- ISR read index < generateStaticParams index in generated code
- __rscForResponse tee assignment before 'return new Response(__rscForResponse'
* feat(isr): cache RSC prefetch responses immediately as partial entries
RSC prefetch requests (Next-Router-Prefetch and client-side nav) now write
a partial cache entry with rscData + html:'' on MISS. Subsequent RSC requests
serve from cache (HIT) without waiting for an HTML request to populate the
entry first.
The ISR read block treats html:'' as a partial hit: RSC requests get a HIT,
but HTML requests fall through to render and overwrite the entry with a
complete html+rscData entry. This prevents blank HTML responses on first HTML
request after a prefetch-first cold start.
Also adds console.log logging for all ISR cache events (HIT/MISS/STALE/write)
in production to aid debugging on deployed Workers.
* fix(isr): use ALS to thread ExecutionContext into KVCacheHandler without constructor arg
Add runWithExecutionContext/getRequestExecutionContext to next/cache shim
backed by AsyncLocalStorage. The RSC entry handler now wraps each request
in this ALS scope so KVCacheHandler._putInBackground and _deleteInBackground
can call ctx.waitUntil() without needing ctx passed at construction time.
- cache.ts: add ExecutionContextLike interface + ALS helpers
- app-rsc-entry.ts: import runWithExecutionContext, wrap _run in ALS scope
- kv-cache-handler.ts: check ALS ctx first, fall back to constructor ctx
- deploy.ts: simplify generated template to new KVCacheHandler(env.VINEXT_CACHE)
- tests/deploy.test.ts: update expectation to match simplified constructor
* fix(isr): store activeHandler on globalThis to fix cross-environment cache isolation
Cloudflare Workers with Vite RSC loads separate module instances per
environment (worker / RSC / SSR). Previously, activeHandler was a
module-local variable, so setCacheHandler(new KVCacheHandler(...)) in
the worker environment set the worker copy while getCacheHandler() in
the RSC environment still returned a fresh MemoryCacheHandler from
its own copy — causing every ISR read/write to go to in-process
memory that is discarded after each request.
Fix: store the active handler on globalThis via Symbol.for('vinext.cacheHandler'),
the same pattern used by _cacheAls, _ctxAls, and _unstableCacheAls. All
Vite environments share the same globalThis on Cloudflare Workers, so
the KVCacheHandler set in worker/index.ts is now visible to the RSC
entry's getCacheHandler() call, enabling real KV-backed ISR cache
reads and writes.
* fix(isr): allow force-static/error pages to be served from ISR cache, prevent partial RSC write from clobbering complete entry
- Remove !isForceStatic and !isDynamicError from the ISR cache read guard.
Both modes are compatible with ISR: they control dynamic API behaviour during
render, not whether results can be cached. The asymmetry between the read guard
(which excluded them) and the write guard (which did not) caused writes to
succeed but reads to be skipped entirely, so every request returned MISS.
Only isForceDynamic should bypass the ISR cache.
- Add a read-before-write in the RSC partial-write path. When an RSC request
writes a partial cache entry (html: "", rscData set) before the first HTML
request has come in, a concurrent or later RSC request could race and overwrite
a complete html+rscData entry that the HTML write had already landed. The fix
reads the existing entry first and skips the partial write if a fresh, complete
entry (non-empty html) is already present, so the HTML write always wins.
* fix(isr): split HTML and RSC into separate cache keys, eliminating write races
Previously both HTML and RSC data were stored under a single key with an
html:"" partial-entry sentinel to signal 'RSC written, HTML pending'. This
caused write races on eventually-consistent stores like KV: a concurrent RSC
request could overwrite a complete html+rscData entry with a partial one,
or the read-before-write guard added to prevent that would itself race.
This mirrors how Next.js handles it in FileSystemCache: HTML is stored in
<key>.html and RSC in <key>.rsc as independent files, so each request type
reads and writes only its own key with no coordination needed.
Changes:
- Replace __isrCacheKey() with __isrHtmlKey() / __isrRscKey() which append
:html and :rsc suffixes respectively
- ISR read block selects the key based on isRscRequest before the get() call
- RSC write path uses __isrRscKey and stores only rscData (html: "")
- HTML write path uses __isrHtmlKey and stores only html (rscData: undefined)
- Background regen writes both keys independently via Promise.all
- Remove the read-before-write hack (no longer needed)
- Remove partial-entry sentinel comments (concept no longer exists)
- HTML write path no longer awaits __isrRscDataPromise
* fix(kv-cache): always use 30-day KV TTL regardless of revalidate frequency
Previously expirationTtl was 10x the revalidate period (clamped to 60s–30d).
For pages with short revalidate windows (e.g. revalidate=5), this meant entries
were evicted from KV after ~50 seconds of no traffic, causing the next request
to block on a fresh render instead of serving stale content.
KV eviction should never be the reason a stale entry disappears — staleness is
tracked by the revalidateAt timestamp in the stored JSON, not by KV TTL. Always
storing for 30 days means stale content is always available to serve while
background regen runs, and entries only disappear after 30 days of zero traffic
or explicit tag invalidation.
* fix(isr): prefix KV cache keys with per-deployment build ID, add ttlSeconds option to KVCacheHandler
- __isrCacheKey now embeds process.env.__VINEXT_BUILD_ID (statically
replaced by Vite's define at build time) so each deployment gets its
own effective cache namespace in KV — stale entries from previous
deploys are never served as hits
- vinextBuildId (crypto.randomUUID() at plugin scope) is passed to
generateRscEntry and injected as the __VINEXT_BUILD_ID define
- KVCacheHandler accepts an optional ttlSeconds constructor option
(defaults to 30 days) so users can override KV entry lifetime
* regen snaps
* fmt
* refactor(ctx): migrate to canonical request-context shim for ExecutionContext ALS
- shims/cache.ts: remove duplicated ALS implementation; re-export
runWithExecutionContext and getRequestExecutionContext from the
canonical shims/request-context.ts (Symbol.for('vinext.requestContext.als'))
- app-rsc-entry.ts: import runWithExecutionContext from request-context
via absolute path (consistent with pages-server-entry pattern) rather
than the bare 'vinext/shims/request-context' specifier which is not
in the alias map
- deploy.ts: restore missing ${isrSetup} interpolation that was dropped
during rebase conflict resolution
- Regenerate entry-template snapshots
* fix(isr): address PR review — hash collision, dedup key, regen headers, RSC key from HTML path, Infinity revalidate, Response init, deploy KV hint
* fix(isr): use __hasRsc/__hasHtml guards in cache read block; document 30-day flat KV TTL in test
* fix(isr): gate debug logs behind NEXT_PRIVATE_DEBUG_CACHE, add FNV seed comment, document headers limitation
|
||
|
|
dea3c3e44f |
fix: enforce segment boundaries for basePath stripping and redirect prefixing (#393)
* Update deploy basePath stripping * Fix redirect basePath handling * Document basePath segment fix * Fix external redirect basePath |
||
|
|
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 |
||
|
|
4e0294fc4f |
fix: middleware crash in dev mode with cloudflare plugin (#344)
* test: middleware with cloudflare plugin and routing bypass * fix: use module runner for pages middleware * add pages router dev test |
||
|
|
dc8e953f07 |
fix(instrumentation): defer ssrLoadModule to post-middleware hook (#289)
* fix(instrumentation): defer ssrLoadModule call to post-middleware hook Vite 7's SSRCompatModuleRunner requires the SSR environment's transport channel to be initialized before ssrLoadModule() can be called. During configureServer(), the channel is not yet ready, causing a TypeError when instrumentation.ts exists. Move the runInstrumentation() call from the configureServer body into the returned post-middleware function, where environments are fully initialized. Fixes #167 * fix(instrumentation): address review comments on deferred ssrLoadModule - Remove unreachable .catch() on runInstrumentation() since internal try/catch already handles errors and the function never rejects - Mock console.error in transport error test to suppress noisy output and assert the expected error message - Rename misleading test to accurately describe what it verifies * test: address instrumentation review feedback |
||
|
|
af0a839a67 |
fix: preserve multiple Set-Cookie headers in response merging (#297)
* fix: preserve multiple Set-Cookie headers in prod-server and worker entry The response header merging in prod-server.ts and the generated Cloudflare worker entry used Record<string, string> which flattened multiple Set-Cookie headers — the last value won, or cookies with Expires dates got corrupted by comma-joining. Extract mergeResponseHeaders() helper that uses getSetCookie() to preserve array-valued Set-Cookie headers. Apply the same fix to the worker entry template in deploy.ts using the Headers API. Fixes #295 * fix: address review feedback on Set-Cookie header preservation - Add `as string` cast in deploy.ts worker entry for x-middleware-request-* header unpacking (matches prod-server.ts) - Normalize Vary header with Array.isArray check in sendCompressed to handle the widened type correctly - Add edge-case test for middleware cookie as plain string (not array) * fix: apply Set-Cookie fix to example workers, remove redundant toLowerCase - Fix the same Set-Cookie flattening bug in hand-written example worker entries (pages-router-cloudflare, realworld-api-rest) - Remove 4 redundant .toLowerCase() calls — Headers.forEach() always yields lowercase keys * fix: add missing as string cast in deploy.ts middleware header collection * refactor: extract mergeHeaders to shared vinext/server/worker-utils The mergeHeaders function was duplicated in both example worker entries (pages-router-cloudflare, realworld-api-rest) and the deploy.ts template. Extract it to a shared module that example workers import. The deploy.ts template still inlines it (code generation constraint). Reduces 3 copies to 2 (shared module + generated template). * fix: array-accumulate Set-Cookie in config headers, fix JSDoc, add behavioral test - deploy.ts template: config headers section was comma-joining Set-Cookie values using += which corrupts cookies with Expires dates and loses all but the last cookie when multiple config rules match. Matches the array-accumulation pattern already used in prod-server.ts (lines 899-907) and the middleware section above. - worker-utils.ts + deploy.ts template JSDoc: 'Response headers take precedence' was misleading — Set-Cookie is additive, not overriding. Clarify both docs. - tests/deploy.test.ts: add behavioral test for mergeHeaders via worker-utils import (same function inlined in the generated template), replacing reliance on string-contains assertions alone. * fix: indentation in deploy.ts template, array-accumulate Set-Cookie in example workers - deploy.ts: fix extra leading space in config headers block (lines 636-659) and JSDoc lines 740-743 introduced in previous commit - pages-router-cloudflare, realworld-api-rest: config headers section was doing a plain assignment (middlewareHeaders[lk] = h.value) which overwrites array-valued Set-Cookie built up by the middleware section above; use the same array-accumulation pattern as prod-server.ts and the deploy.ts template * fix: remove as any casts in prod-server.ts, add Vary handling to example workers - prod-server.ts lines 904/906: middlewareHeaders is now Record<string, string | string[]> so the as any casts are unnecessary; replace with as string (consistent with the middleware collection section above at line 848) - pages-router-cloudflare, realworld-api-rest: add Vary comma-joining to config headers section to match prod-server.ts (line 908) and deploy.ts template (line 653) --------- Co-authored-by: James <james@eli.cx> |
||
|
|
04d0086731 |
fix: inject default viewport meta (width=device-width, initial-scale=1) (#298)
* fix: inject default viewport meta (width=device-width, initial-scale=1)
mergeViewport() started from an empty object, so when a user exported
a viewport config with only themeColor (no width/initialScale), the
merged result was truthy but missing viewport dimensions. ViewportHead
then skipped the <meta name="viewport"> tag entirely, breaking mobile
rendering.
Seed mergeViewport with DEFAULT_VIEWPORT matching Next.js's
createDefaultViewport() defaults: { width: "device-width", initialScale: 1 }.
User-provided values still override these defaults.
* refactor: export DEFAULT_VIEWPORT and reuse at call sites
Export DEFAULT_VIEWPORT from metadata shim and replace the duplicated
inline literals in app-dev-server.ts to keep the default in one place.
* refactor: simplify mergeViewport call sites to always apply defaults
Since mergeViewport() always seeds from DEFAULT_VIEWPORT (even for
empty lists), the `viewportList.length > 0` guards and `?? DEFAULT_VIEWPORT`
fallbacks at both call sites were redundant. Remove them so the default
viewport is applied unconditionally via mergeViewport itself.
Addresses review feedback from james-elicx on PR #298.
|
||
|
|
9b01dfec65 |
fix: load .env files into process.env before config evaluation (#234)
Closes #228. vinext was not loading .env files, so environment variables defined in .env were unavailable to server-side code (getServerSideProps, API routes, server components) and NEXT_PUBLIC_* vars were missing from the client bundle defines. Uses Vite's loadEnv() in the config hook with an empty prefix to load all .env vars (not just VITE_-prefixed) into process.env before loadNextConfig() runs, matching Next.js behavior. |
||
|
|
8a618af9f6 |
fix: handle malformed percent-encoded URLs gracefully (#107)
Wrap all decodeURIComponent calls on user-controlled URL paths in try/catch to return 400 Bad Request instead of throwing an uncaught URIError that crashes the Node process. A single request with a malformed percent-encoded path (e.g. /%E0%A4%A) could terminate the entire server process. This affected all server entry points: prod server (App + Pages Router), dev server middleware, Cloudflare Worker entry, and generated RSC/middleware handlers. Fixes: - prod-server.ts: App Router and Pages Router request handlers - app-router-entry.ts: Cloudflare Worker entry - app-dev-server.ts: Generated RSC entry handler - index.ts: Dev server connect middleware + generated middleware runner + generated NEXT_LOCALE cookie parser - middleware.ts: Pages Router dev middleware runner Includes 11 regression tests across app-router, pages-router, and features test suites. |
||
|
|
12fea722b6 | Initial public release of vinext |