Commit Graph

1925 Commits

Author SHA1 Message Date
Jared Stowell 26d5eabb92 fix: skip synthetic slot sub-routes without parent default fallback (#458)
* Fix nested parallel slot routes

* Fix nested parallel slot discovery

* Fix parallel slot routing parity
2026-03-12 07:47:25 +00:00
Divanshu Chauhan (divkix) 9d0fc0d2fb fix: bind iterator methods on request headers proxy (#480) 2026-03-12 07:46:53 +00:00
James Anderson aba48698c2 feat(build): add Next.js-style route report to vinext build output (#477)
* feat(build): add Next.js-style route report to vinext build output

- Adds build/report.ts with regex-based route classification (static, ISR, dynamic, unknown, API)
- Clean build logger suppresses Vite/Rollup noise; keeps timing lines and errors
- --verbose flag bypasses filtering and shows raw output
- Routes sorted alphabetically by path (filesystem order)
- Legend sorted alphabetically by label
- Unknown routes show a note explaining classification will improve in a future release
- 55 unit tests covering all classification paths and formatting

* fmt

* fix exmaple

* support -v

* .

* .

* fix(build-report): address review comments

- dynamic='error' now classifies as static (not ssr) — it enforces static
  rendering by throwing if dynamic APIs are used, same as force-static
- add TODO comment on extractGetStaticPropsRevalidate regex matching too broadly
- drop -v short alias for --verbose to avoid collision with -v/--version at top level
- classifyPagesRoute now returns unknown on file read failure, consistent with classifyAppRoute
- single-route table now renders with ┌ corner instead of └ (i===0 check takes priority)
- 3 new tests covering the above fixes (58 total)

* test(build-report): replace misleading classifyAppRoute tests with real fixture coverage

The two tests named 'classifies page with revalidate=0 as ssr' and
'classifies revalidate=Infinity as static' both passed null paths and
asserted unknown, not actually exercising the stated code paths.

Replace both with proper integration tests using new fixture files:
- revalidate-zero-test/page.tsx (export const revalidate = 0) → ssr
- revalidate-infinity-test/page.tsx (export const revalidate = Infinity) → static
2026-03-11 19:58:06 +00:00
James Anderson 5b60a70f94 test: headers() and cookies() in server actions and route handlers (#473)
* test: add regression tests for headers() and cookies() in server actions and route handlers

* test: address review comments on action-headers tests

- Remove ghost test that always passed via 404 bailout
- Remove unused getHeaderFromActionSync fixture export
- Fix extractReturnValue to parse full first line instead of greedy {.+} match
- Fix action ID extraction to match by export name instead of array index
- Add comment clarifying why the route handler test is distinct from existing ones

* test: address second round of review comments

- Use fetchHtml helper for page fetches (consistent with other nextjs-compat tests)
- Clarify page.tsx comments: wrapper closures discard return values, named
  export tests are needed for value assertions
2026-03-11 18:07:54 +00:00
Divanshu Chauhan (divkix) 488fb350d4 fix: pin Rollup to patched version for CVE-2026-27606 (#476) 2026-03-11 18:03:57 +00:00
Nathan Nguyen b87ab02d58 feat: metadata parity for appLinks, iTunes, Twitter player/app cards (#452)
* feat: add metadata support for appLinks, iTunes, and Twitter player/app cards

Add four missing Next.js metadata features to the Metadata type and
MetadataHead renderer:

- appLinks: renders al:* property tags (ios, iphone, ipad, android,
  windows_phone, windows, windows_universal, web)
- itunes: renders apple-itunes-app name tag with app-id and app-argument
- twitter player cards: renders twitter:player, twitter:player:stream,
  twitter:player:width, twitter:player:height
- twitter app cards: renders twitter:app:name/id/url per platform
  (iphone, ipad, googleplay)

Includes fixture pages and tests ported from Next.js test suite.

* chore: format metadata files

* fix: resolve twitter player/app and appLinks URLs against metadataBase

* fix: un-nest twitter app card name/url checks to match Next.js behavior

In Next.js, the name, id, and url checks in the twitter app card
platform loop are independent. Our implementation nested name and url
inside the id check, so a platform without an id wouldn't get name or
url tags. This un-nests them to match Next.js parity.

Also adds the missing twitter:app:name:ipad test assertion.

* Update packages/vinext/src/shims/metadata.tsx

---------

Co-authored-by: James Anderson <james@eli.cx>
2026-03-11 17:40:18 +00:00
Divanshu Chauhan (divkix) ddf4d337d5 docs/runtime: warn on external rewrites, sanitize Content-Disposition (#418)
* fix: warn on external rewrites and sanitize Content-Disposition

Add a startup warning when next.config contains rewrites targeting
external origins (e.g. https://...), alerting that credential headers
(cookie, authorization, x-api-key, proxy-authorization) are forwarded.

Sanitize contentDispositionType in image optimization and prod server
to only allow "attachment" — any other value defaults to "inline",
preventing header injection via arbitrary config values.

Closes #414

* fix: improve external rewrite warning and add Content-Disposition sanitization test

Use proper singular/plural in the external rewrite warning message
instead of "rewrite(s)". Add test assertions verifying the warning
includes the source pattern and arrow separator. Add a test confirming
that invalid contentDispositionType values default to "inline".

* fix: correct external rewrite warning to reflect actual header forwarding

proxyExternalRequest copies all request headers, not just credential
headers. Updated warning text to accurately state this and suggest
server-side fetch as an alternative. Tightened test assertion for
exact plural form.

* fix: address PR review feedback on external rewrite warning

Clarify why API routes are preferable (control over forwarded headers),
tighten test assertion to prevent false singular/plural match.

Refs: #430

* fix: refine external rewrite warning wording and add source path assertions

Mention route handlers alongside API routes in the warning suggestion,
and assert that source paths (/proxy1, /proxy2, /proxy3) appear in the
multi-rewrite warning message.

* fix: strip credential headers from outbound proxy requests

External rewrites via proxyExternalRequest now strip authorization,
cookie, proxy-authorization, and x-api-key headers before forwarding
to third-party origins. This prevents leaking user credentials to
external services.

Updated the warning message to reflect that credentials are stripped
(not forwarded) and updated tests to assert the new behavior.

Closes #430

* fix: align app-router credential stripping test with implementation

The integration test in app-router.test.ts was missed when c4a128d added
credential header stripping to proxyExternalRequest. Assertions now
correctly expect credentials to be undefined (stripped), matching the
unit test in shims.test.ts and the actual implementation.

* fix: keep Next.js rewrite forwarding

* fix: restore Next.js external rewrite header forwarding
2026-03-11 16:57:28 +00:00
Jared Stowell b66fd6488c fix: Preserve encoded path delimiters in route discovery and matching (#456)
* Fix App Router decoding collision

* Fix App Router path decoding

* Address bonk review: codegen safe segment decode, static serving, nits

* fix: add codegen strict normalizer, use it at 400-returning call sites

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 16:52:29 +00:00
Divanshu Chauhan (divkix) 9b285e3c9f perf: async I/O + cache for og-inline-fetch-assets transform (#435)
* perf: convert og-inline-fetch-assets transform to async I/O with per-build cache

The `vinext:og-inline-fetch-assets` plugin used synchronous `fs.readFileSync()`
in Vite's transform hook, blocking the transform pipeline. This converts both
Pattern 1 (fetch inlining) and Pattern 2 (readFileSync inlining) to use
`await fs.promises.readFile()` and adds a per-build `Map<string, string>` cache
so repeated reads of the same file (common with shared fonts) hit memory instead
of disk.

* fix: use strict undefined check in og-inline cache and restore spies via afterEach

Replace falsy `if (!fileBase64)` with `if (fileBase64 === undefined)` in both
og-inline patterns to correctly handle hypothetical empty-string cache values.
Add `afterEach(() => vi.restoreAllMocks())` to prevent spy leaks on assertion
failure, replacing the manual mockRestore() call.

* fix: address og-inline review feedback

* style: format og-inline test assertions
2026-03-11 16:44:48 +00:00
Nathan Nguyen b191aff91d fix: TPR zone resolution for multi-part TLD domains (#470)
* fix: TPR zone resolution for multi-part TLD domains

Replace hardcoded 12-entry MULTI_PART_TLDS list with progressive
domain candidate lookup. For "shop.example.co.uk", tries co.uk →
example.co.uk → shop.example.co.uk until a Cloudflare zone matches.

This fixes zone resolution for all public suffixes (.org.au, .co.in,
.gov.uk, etc.) without maintaining a TLD list. Common TLDs (.com, .io)
still resolve on the first API call.

Also fixes the missing fallback — the previous code returned null on
miss despite a TODO comment claiming fallback behavior existed.

* chore: format test file
2026-03-11 16:43:09 +00:00
Divanshu Chauhan (divkix) 20f08bdd86 perf: replace O(n) linear route matching with radix trie (#442)
* perf: replace O(n) linear route matching with radix trie

Every request triggered 3-6 linear scans over the full route list, calling
matchPattern() for every route until a match. For apps with many routes
this means thousands of comparisons per request.

This replaces the linear scan with a trie-based O(depth) lookup in all
4 codebases that share the matching logic:

- pages-router.ts (scanner module, dev time)
- app-router.ts (scanner module, dev time)
- app-rsc-entry.ts (generated entry, runtime)
- pages-server-entry.ts (generated entry, runtime)

The trie enforces correct precedence at each node via traversal order:
static > dynamic > catch-all > optional catch-all. Recursive DFS with
backtracking ensures dead-end branches fall through to alternatives.

matchPattern is retained only in app-rsc-entry.ts for the intercept
lookup (typically 0-5 entries, not worth trie-ifying).

* test: update entry-templates snapshot and assertion for trie-based matching

The server entry no longer inlines matchPattern — it imports the trie
module instead. Update the explicit assertion to check for buildRouteTrie
and trieMatch, and regenerate all entry template snapshots.

* fix: address review comments on trie-based route matching

- Correct "radix trie" → "trie (prefix tree)" in doc comment (no path compression)
- Remove unused `routes` parameter from `matchRoute` in app-rsc-entry
- Replace stream-of-consciousness test comment with concise explanation

* fix: update matchRoute regex in test to match new 1-arg signature

The test was asserting the old `matchRoute(url, routes)` signature in
the generated code. Updated to match the new `matchRoute(url)` signature
after the unused `routes` parameter was removed.
2026-03-11 16:41:21 +00:00
Divanshu Chauhan (divkix) 1b4a796a3f perf: startup and cache micro-optimizations (#436)
* perf: cache startup filesystem scans, optimize base64 and path normalization

- Cache hasMdxFiles() result per root directory to avoid redundant
  recursive filesystem walks when config() fires per Vite environment
- Cache resolvePostcssStringPlugins() result per project root to skip
  repeated existsSync checks across 17 PostCSS config file candidates
- Replace byte-by-byte base64 encode/decode in KV cache handler with
  Buffer APIs for significantly faster serialization
- Pass pre-normalized URL to nodeToWebRequest() in App Router prod
  server to eliminate redundant path normalization in the RSC handler

* fix: validate base64 structural length and improve test spy cleanup

Add length % 4 check to base64ToArrayBuffer to reject structurally
invalid base64 strings that pass the character-set regex but produce
empty/truncated buffers via Buffer.from(). Replace manual mockRestore()
calls with afterEach(vi.restoreAllMocks) to prevent spy leaks on
assertion failure.

* fix: address bonk review comments — composite mdx cache key, postcss comment order, hoist test import

* fix: cache Promise in resolvePostcssStringPlugins to prevent concurrent scan race; add POST urlOverride test

* fmt

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 16:22:58 +00:00
Stephen Zhou 657903055a fix: add client optimizeDeps entries (#469) 2026-03-11 16:02:30 +00:00
Jared Stowell 17dd075885 fix: App Router route-group and slot collision (#447)
* Fix app router route-group conflicts

* Review app router slot discovery

* fix: address review feedback from bonk

- Fix routeSegments bug: store raw filesystem segments (with route groups)
  instead of normalizeVisibleRouteSegments output, so useSelectedLayoutSegments()
  sees the correct segment list at runtime for synthetic slot sub-routes

- Eliminate redundant convertSegmentsToRouteParts call: store the pre-computed
  result in subPathMap alongside rawSegments, reuse it in the merge loop instead
  of re-deriving from already-stripped segments

- Make ownerDir required (non-optional) in ParallelSlot: it is always set by
  discoverParallelSlots; remove the dead ?? '' fallback in slotKey

- Replace O(n) routes.find() with O(1) Map lookup: build routesByPattern before
  the outer loop and keep it updated as synthetic routes are added

- Add comment to normalizeVisibleRouteSegments explaining the intentional difference
  from convertSegmentsToRouteParts (dynamic segments not converted) and that both
  functions must stay in sync

- Remove spurious g flag from normalizeRoutePattern regex: $ anchors to end of
  string so there can only ever be one match

* fix: remove now-unused normalizeVisibleRouteSegments, add doc comment to convertSegmentsToRouteParts

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 13:30:33 +00:00
Jared Stowell f360380112 fix: Pages API body parser for invalid JSON and repeated form keys (#446)
* Fix Pages API parsing parity

* Fix Pages API body parsing parity

* Fix empty form body parity

* Regenerate entry-templates snapshots after merge

* fix: propagate statusText through prod server sendCompressed non-compressed path

The sendCompressed function had a statusText parameter but only used it in
the compressed response path. The non-compressed else branch called
res.writeHead directly without forwarding statusText, so short error
responses (like 'Invalid JSON' at 12 bytes, well below COMPRESS_THRESHOLD)
lost their custom reason phrase and fell back to the default 'Bad Request'.

Fix: replace the direct res.writeHead call in the else branch with the
writeHead closure that already handles the statusText conditional.

Also set statusText on the ApiBodyParseError response in the pages server
entry template so the value is present for prod-server to forward.

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 13:24:36 +00:00
Divanshu Chauhan (divkix) 1c3401e245 perf: TTL-based eviction sweep for prefetch cache (#434)
* perf: add TTL-based eviction sweep for prefetch cache

Before this change, expired prefetch cache entries were only cleaned on
individual access or displaced by FIFO when at the 50-entry cap. On
link-heavy pages, expired entries wasted slots and caused premature
eviction of still-valid entries.

Now storePrefetchResponse() sweeps all expired entries before falling
back to FIFO eviction, so fresh entries survive longer under pressure.

* fix: sync prefetched URL set on cache eviction

Evicted entries were not removed from getPrefetchedUrls(), causing
Link's prefetched.has() check to return true for stale URLs and skip
re-prefetch. Clean up the prefetched set in both sweep and FIFO paths.

Also derive test counts from MAX_PREFETCH_CACHE_SIZE constant and
assert prefetched URL set consistency after eviction.

* fix: address review comments on prefetch cache TTL sweep

- Hoist Date.now() to top of storePrefetchResponse so sweep and new entry timestamp use the same instant
- Export MAX_PREFETCH_CACHE_SIZE in next-shims.d.ts to match runtime export
- Use fixed arbitrary time values in tests instead of real wall clock
- Clarify why prefetchedUrls count excludes the new entry in the below-capacity test

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 12:39:49 +00:00
Jared Stowell 16a14c713b fix: Pages prod config headers and redirects after middleware rewrites (#448)
* Fix headers matching original path

* Review redirect ordering parity

* Fix config redirects order

* Fix dev middleware parity

* Fix dev middleware request parity

* address review: worker header/fallback parity, early-return in attachRouteHandlerMiddlewareContext, cache node request headers, add dev redirect test

* address second-round bonk comments: sanitizeDestination in worker redirect, nodeRequestHeaders comment, app-rsc-entry guard comment, E2E redirect test, updated snapshots

---------

Co-authored-by: James <james@eli.cx>
v0.0.28
2026-03-11 11:40:07 +00:00
Jared Stowell 67d7c2f90c Add Node execution context handling (#460) 2026-03-11 11:22:49 +00:00
Jared Stowell 3b982066d0 fix: align Pages API body parsing and res.send(Buffer) (#428)
* Fix Buffer handling in Pages API res

* fix: skip reporting handled Pages API parse errors

* address review: blank line nit, content-length parity for Buffer, fix duplicate Content-Length in sendCompressed

* fix: set content-length for Buffer in production res.send() to match dev parity

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 11:16:51 +00:00
Nathan Nguyen 47500b180f fix: return route pattern from router.pathname, not resolved path (#463)
* fix: stabilize instrumentation state and hydration E2E tests

Move Cloudflare instrumentation example state from module-level
variables to globalThis so startup and request paths share one owner
across different module instances.

Add waitForHydration() helper to Pages Router E2E tests to eliminate
flaky button-click assertions that raced against React hydration.

* fix: honor the `as` parameter in router.push/replace

The `as` parameter in push() and replace() was silently ignored
(prefixed as _as) in both the useRouter() hook and the Router
singleton. This broke masked URLs and legacy dynamic-route flows.

Extract resolveNavigationTarget() helper that uses `as` when provided,
falling back to resolveUrl(url) otherwise. Apply to all four call
sites: useRouter().push, useRouter().replace, Router.push, Router.replace.

* fix: address PR review feedback

- Add doc comment to resolveNavigationTarget explaining the url/as
  simplification vs Next.js's two-value approach
- Extract waitForHydration to shared tests/e2e/helpers.ts
- Add pathname assertion to verify router.pathname stays as route
  pattern (/posts/[id]) after as-based navigation

* fix: remove pathname assertion that tests unimplemented behavior

router.pathname currently returns the resolved path after client-side
navigation, not the route pattern. The correct behavior (returning
/posts/[id] instead of /posts/42) requires the two-value url/as
approach tracked in #462.

* fix: return route pattern from router.pathname, not resolved path

In Next.js, router.pathname returns the route pattern (e.g.,
"/posts/[id]"), not the resolved path ("/posts/42"). vinext was
deriving pathname from window.location.pathname which gives the
resolved path.

Fix: use __NEXT_DATA__.page (which holds the route pattern) as
pathname, falling back to location.pathname when __NEXT_DATA__ isn't
available. asPath continues to use the resolved browser path.

Closes #462

* fix: set SSR pathname to route pattern to prevent hydration mismatch

All three SSR entry points (dev-server, pages-server-entry,
static-export) were setting pathname to the resolved URL path (e.g.,
"/posts/42") while the client now derives it from __NEXT_DATA__.page
(the route pattern "/posts/[id]"). This caused hydration mismatches
when components render router.pathname.

Fix: use patternToNextFormat(route.pattern) for the SSR context
pathname, matching what __NEXT_DATA__.page already provides.

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 11:00:36 +00:00
Divanshu Chauhan (divkix) 769a98ac85 perf(kv): local tag cache to reduce KV round-trips on cache hit (#433)
* perf(kv): add local in-memory tag cache to reduce KV round-trips

When KVCacheHandler.get() validates tags, it was issuing one kv.get()
per tag in parallel. For entries with N tags, that's N KV round-trips
per cache hit. Same pattern in revalidateTag() — N parallel PUTs.

Add a local Map<string, { timestamp, fetchedAt }> with a 5-second TTL
that caches tag invalidation timestamps. Within the TTL window, tag
checks are served from memory with zero I/O. After TTL expiry, the
next request re-fetches from KV.

Key behaviors:
- revalidateTag() updates the local cache immediately so invalidations
  are reflected without waiting for TTL expiry
- resetRequestCache() clears the local cache for per-request isolation
- NaN tag timestamps are cached and correctly treated as invalidation
- Only uncached/expired tags trigger KV reads (partial cache hits work)

* fix(test): guard fake timers with try/finally to prevent leaks

Wrap vi.useFakeTimers() in try/finally so vi.useRealTimers() runs even
if an assertion fails mid-test, preventing timer leaks into subsequent
tests.

* fix(kv): address bonk review comments on local tag cache

- Fix early return bug: split KV tag fetch loop into populate-cache pass
  then invalidation-check pass, so already-fetched tag results are always
  cached even when an earlier tag triggers an early return
- Fix unbounded _tagCache growth: delete stale entries when encountered
  during the TTL check (cheap eviction instead of accumulation forever)
- Make tag cache TTL configurable via tagCacheTtlMs constructor option
  (default 5000ms); eliminates fake timers in TTL test
- Clarify resetRequestCache() docstring: it is not called per-request by
  vinext, it is an opt-in escape hatch for callers that need explicit isolation
- Add resetRequestCache() test: verifies tags are re-fetched from KV after
  calling resetRequestCache()

* refactor(kv): reuse cached timestamps in invalidation check loop

The second pass over uncachedTags was re-calling Number(tagTime) and
re-reading tagResults[i]. Now it reads directly from _tagCache (which was
just populated in the first pass), eliminating the duplicate number
conversion and making the intent clearer.

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 10:59:25 +00:00
Jared Stowell f035e1a05e fix: object-form query array serialization in next/link and next/router (#457)
* Fix query param handling

* Fix scalar query serialization
2026-03-11 10:46:57 +00:00
Nathan Nguyen 45b72eade5 fix: honor the as parameter in router.push/replace (#453)
* fix: stabilize instrumentation state and hydration E2E tests

Move Cloudflare instrumentation example state from module-level
variables to globalThis so startup and request paths share one owner
across different module instances.

Add waitForHydration() helper to Pages Router E2E tests to eliminate
flaky button-click assertions that raced against React hydration.

* fix: honor the `as` parameter in router.push/replace

The `as` parameter in push() and replace() was silently ignored
(prefixed as _as) in both the useRouter() hook and the Router
singleton. This broke masked URLs and legacy dynamic-route flows.

Extract resolveNavigationTarget() helper that uses `as` when provided,
falling back to resolveUrl(url) otherwise. Apply to all four call
sites: useRouter().push, useRouter().replace, Router.push, Router.replace.

* fix: address PR review feedback

- Add doc comment to resolveNavigationTarget explaining the url/as
  simplification vs Next.js's two-value approach
- Extract waitForHydration to shared tests/e2e/helpers.ts
- Add pathname assertion to verify router.pathname stays as route
  pattern (/posts/[id]) after as-based navigation

* fix: remove pathname assertion that tests unimplemented behavior

router.pathname currently returns the resolved path after client-side
navigation, not the route pattern. The correct behavior (returning
/posts/[id] instead of /posts/42) requires the two-value url/as
approach tracked in #462.
2026-03-11 09:00:27 +00:00
Nathan Nguyen 15cd5d2881 fix: skip locale prefix for absolute and protocol-relative Link hrefs (#455)
* fix: skip locale prefix for absolute and protocol-relative Link hrefs

applyLocaleToHref() blindly prepended /${locale} to any href string,
producing malformed URLs like /fr/https://example.com/about or
/fr///example.com/about for absolute and protocol-relative hrefs.

Guard against http://, https://, and // URLs before applying the
locale prefix — these are external or same-origin absolute URLs that
should not be rewritten.

* fix: skip locale prefix for absolute and protocol-relative URLs in applyNavigationLocale

applyNavigationLocale() in router.ts (used by Router.push() / Router.replace())
had the same bug as applyLocaleToHref() in link.tsx — it would prepend /{locale}
to absolute and protocol-relative URLs, producing malformed hrefs like
/fr/https://example.com/about or /fr///cdn.example.com/img.png.

Add the same guard that was applied to applyLocaleToHref(), and add
corresponding tests covering https://, http://, and // URL patterns.

---------

Co-authored-by: James <james@eli.cx>
2026-03-11 08:38:48 +00:00
Jared Stowell 1551e4406a fix: App Router ISR invalidation parity for fetch and path tags (#445)
* Fix App Router ISR tag parity

* Fix KV tag persistence

* Fix KV cache tag handling
2026-03-11 08:17:50 +00:00
Divanshu Chauhan (divkix) 29649068f4 fix(fetch-cache): register stale-while-revalidate refetch with waitUntil() (#432)
* fix(fetch-cache): register stale-while-revalidate refetch with waitUntil()

The fetch cache's background refetch on stale entries was fire-and-forget.
On Cloudflare Workers, the isolate terminates after the response is sent,
killing in-flight refetches so stale entries never get refreshed.

Register the refetch promise with ExecutionContext.waitUntil() via the
existing getRequestExecutionContext() ALS accessor, matching the pattern
already used by ISR in isr-cache.ts.

* ci: re-trigger CI
2026-03-11 07:47:39 +00:00
James Anderson a08a2b5d92 refactor: make ExecutionContext ALS the single source of truth (#431)
* refactor: make ExecutionContext ALS the single source of truth

Remove ctx as an explicit parameter from internal functions so that all
downstream code consistently retrieves the Workers ExecutionContext via
getRequestExecutionContext() from the ALS. Changes:

- isr-cache.ts: drop ctx? param from triggerBackgroundRegeneration();
  remove duplicate ExecutionContext interface (use ALS directly)
- app-rsc-entry.ts: drop ctx 4th param from generated _handleRequest;
  replace ctx.waitUntil() calls with _getRequestExecutionContext()?.waitUntil()
- kv-cache-handler.ts: remove duplicate ExecutionContext interface;
  import ExecutionContextLike from request-context instead
- isr-cache.test.ts: update ctx.waitUntil test to supply ctx via
  runWithExecutionContext() (ALS) instead of as a direct argument
- Update entry-templates.test.ts snapshots

* fix(test): update _handleRequest signature assertion after ctx param removal
2026-03-11 07:40:52 +00:00
Jared Stowell c4064ebb23 fix: next/form submitter overrides and query-string GET URLs (#426)
* Add submitter handling to form shim

* Update form shim URL validation

* Fix form submit attribute casing
2026-03-11 00:31:52 +00:00
James Anderson 60ca7192c2 fix(isr): use getRequestExecutionContext() from ALS in background regeneration (#429)
Instead of relying on the caller to pass ctx to triggerBackgroundRegeneration,
read it from AsyncLocalStorage via getRequestExecutionContext(). This ensures
waitUntil() is always registered on the live Workers ExecutionContext, fixing
stale responses that were not triggering revalidation on Cloudflare Workers.
2026-03-10 23:58:24 +00:00
Jared Stowell 0775ce55bb fix: return ReadonlyURLSearchParams from useSearchParams (#424)
* Update navigation search params

* Fix server readonly cache scope
2026-03-10 23:56:11 +00:00
Jared Stowell 6ccdaac78e fix: sitemap XML namespaces, video entries, and alternate-language links (#427) 2026-03-10 23:54:56 +00:00
Jared Stowell 88ed5550d1 fix: honor beforePopState when useRouter is mounted (#423) 2026-03-10 23:11:53 +00:00
James Anderson aa878d55ac fix(isr): KVCacheHandler.set() now awaits KV put to prevent perpetual STALE (#425)
* fix(isr): KVCacheHandler.set() now awaits KV put to prevent perpetual STALE

Background ISR regen was calling ctx.waitUntil(renderFn()) where renderFn()
resolved before the KV put network operation completed. set() returned
Promise.resolve() immediately, so await __isrSet() in the regen resolved early,
the waitUntil expired, and the Workers runtime killed the in-flight KV write —
leaving the cache entry perpetually STALE on every request after the TTL.

Fix: replace the fire-and-forget _putInBackground() with _put() which returns
the actual kv.put() promise. set() now returns that promise, so await __isrSet()
only resolves after the write is fully persisted. ctx.waitUntil() is also still
registered as belt-and-suspenders.

Adds three regression tests covering the STALE → regen → HIT lifecycle with a
delayed KV put mock to prove the set() promise is not an immediately-resolved stub.

* fix: remove unused variable flagged by lint
2026-03-10 23:11:24 +00:00
Jared Stowell a6c5601cc2 fix: preserve repeated route and query params in rewrites and redirects (#422)
* Update redirect matcher replacements

* Fix rewrite param substitution

* fix nits
2026-03-10 22:28:36 +00:00
Divanshu Chauhan (divkix) b12eeebfce fix(isr): namespace cache keys by buildId (#419)
* fix(isr): namespace cache keys by buildId

Include buildId in ISR cache key generation to prevent stale cache
reuse across deploys. Key format changes from `<router>:<pathname>` to
`<router>:<buildId>:<pathname>` when buildId is available. Without
buildId the format is unchanged for backward compatibility.

Updated all callers: dev-server.ts (dev), pages-server-entry.ts (prod).

Closes #415

* test: update pages-server-entry snapshot for buildId cache key

The entry template changed to include buildId in ISR cache keys.
Update the snapshot to match the new generated code.

* fix(isr): inline isrCacheKey with fnv1a64 hashing in pages server entry

The generated pages server entry was constructing ISR cache keys inline
without the >200-char fnv1a64 hashing that exists in isr-cache.ts. With
buildId making keys longer, this risks exceeding KV key-length limits.

Inline fnv1a64 and isrCacheKey helpers into the generated entry (same
pattern as isrGet/isrSet/triggerBackgroundRegeneration) and replace both
inline cache key constructions with calls to isrCacheKey. Also rename
isrCacheKeyPrefix to _cachePrefix for consistent variable naming.

* address review: clarify __VINEXT_BUILD_ID is undefined in dev by design

* address review: align app-rsc-entry ISR key format with pages router, add comments

* address review: clarify keep-in-sync comment scope (prefix/hashing, not full key format)

---------

Co-authored-by: James <james@eli.cx>
2026-03-10 22:18:49 +00:00
Jared Stowell ec1b9da131 fix: preserve Pages Router query arrays and hash in asPath (#421)
* Handle array query params

* Fix router query merge order
2026-03-10 22:17:36 +00:00
Jared Stowell 11b27cd0a8 fix: reject conflicting dynamic route siblings during route discovery (#420)
* Validate route patterns in routers

* Validate intercepting routes
2026-03-10 21:48:36 +00:00
James Anderson 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
2026-03-10 21:39:33 +00:00
Jared Stowell cf0786ef34 fix: Reject non-terminal catch-all routes in pages and app routers (#401)
* Fix catch-all route overmatching

* Fix non-terminal slot routes

* Fix malformed slot route discovery

* Add validation for intercept catch-⁤

* Fix intercepting route catch-alls

* bonk

* Fix routing test route literals for typecheck

* Fix app route matcher test fixtures
2026-03-10 20:24:31 +00:00
Divanshu Chauhan (divkix) d38661f809 fix: dedup RSC client references to prevent module duplication in dev (#413)
* fix: dedup RSC client references to prevent module duplication in dev

When the RSC plugin creates client-in-server-package-proxy modules for
"use client" submodules within packages, it re-exports from absolute
file paths. These bypass Vite's pre-bundling in the client environment,
causing the browser to load two separate module instances — one from the
raw ESM path and one from .vite/deps/. This breaks React context
providers/consumers since createContext() runs twice.

Add vinext:client-reference-dedup plugin that intercepts absolute
node_modules path imports originating from proxy virtual modules in the
client environment and redirects them through bare specifier imports,
which Vite's import analysis routes through pre-bundling automatically.

Closes #409

* fix: make dedup test layout a server component to exercise RSC proxy path

Remove "use client" from layout.tsx so ThemeProvider import goes through
RSC → client-in-server-package-proxy (absolute path), while the page's
useTheme goes through client pre-bundling (bare specifier). This ensures
the integration test actually validates the dedup plugin works.

* docs: address PR review feedback for client-reference-dedup plugin

- Document lossy submodule-to-package mapping assumption in resolveId
- Extract load filter regex to named DEDUP_FILTER constant
- Note default export edge case in generated re-export code
2026-03-10 20:22:35 +00:00
Jared Stowell 3a5fabe7ec fix: next/headers readonly semantics and legacy sync compatibility (#402)
* Add headers/cookies legacy tests

* Fix headers sync error handling

* Fix headers sync guards

* bonk

* Update packages/vinext/src/shims/headers.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

* bonk

* Clarify dynamic error comment

---------

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: James <james@eli.cx>
2026-03-10 20:21:55 +00:00
Jared Stowell cc02fcae72 fix: RequestCookie behavior and getAll(name) (#403)
* Verify cookie header parsing

* Fix empty cookie handling

* Fix empty cookie parsing fallback
2026-03-10 20:06:35 +00:00
Jared Stowell 86fe0e7c7e fix: middleware object matcher semantics and i18n parity (#400)
* Fix middleware matcher request usage

* Handle object middleware matchers

* Fix middleware object matcher

* make ci happy

* Fix default locale middleware match

* Fix default-locale matcher logic

* Restore regexp matcher support

* Fix middleware regexp matcher
2026-03-10 20:03:39 +00:00
Jared Stowell f72440f082 fix: Pages Router prod hydration for inlined page modules (#394)
* Clarify chunk manifest structures

* Fix SSR manifest basePath URLs

* Fix SSR manifest basePath handling

* Fix SSR manifest basePath handling

* make ci happy

* bonk
2026-03-10 20:02:21 +00:00
James Anderson 6e2d3aa241 fix: tag GitHub @usernames in release notes, publish release directly (#412)
* fix: tag GitHub @usernames in release notes, publish release directly

* fix: move contributor mentions to end-only Contributors section

* fix: trim SHA to 7 chars in commits.txt sent to AI
2026-03-10 16:16:36 +00:00
James Anderson 36f1775122 feat: AsyncLocalStorage for ExecutionContext (ctx) propagation (#410)
Adds request-context.ts shim with runWithExecutionContext() and
getRequestExecutionContext() so Cloudflare Workers ctx is accessible
anywhere in the call stack without manual threading.

- New shims/request-context.ts with ALS keyed on Symbol.for('vinext.requestContext.als')
- app-router-entry.ts wraps each request in runWithExecutionContext(ctx, ...)
- app-rsc-entry.ts generated code imports the helpers for downstream use
- deploy.ts generated worker entry forwards ctx in fetch(request, env, ctx)
- kv-cache-handler.ts uses getRequestExecutionContext() ?? this.ctx
- 8 new tests in tests/request-context.test.ts
v0.0.27
2026-03-10 15:28:51 +00:00
Jökull Sólberg Auðunsson 70ff9d5788 fix: handle single object for openGraph.images and twitter.images (#411)
* fix: handle single object for openGraph.images and twitter.images in metadata shim

Next.js allows `openGraph.images` and `twitter.images` to be a single
object (e.g. `{ url, width, height }`) in addition to a string or an
array. The metadata shim only handled string and array cases, causing
"imgList is not iterable" when a single object was passed.

Normalize single objects to arrays before iterating. Also update the
Metadata type to reflect the full range of accepted shapes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fmt

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
2026-03-10 15:19:54 +00:00
James Anderson 0faf62ac4c feat: support generateBuildId in next.config and inject build ID at runtime (#408)
* feat: support generateBuildId in next.config and inject build ID at runtime

Resolves generateBuildId() from next.config, falling back to a random UUID (ad-blocker-safe) when not configured or when the function returns null. The resolved ID is injected as process.env.__VINEXT_BUILD_ID via Vite define and embedded into __NEXT_DATA__ for Pages Router (dev, prod, and static export). NextRequest gains a buildId accessor for middleware deployment-skew detection.

* regen snaps

* refactor: extract safeUUID helper and drop buildId param from createSSRHandler

Address review nits: deduplicate the UUID+retry loop into a safeUUID() helper,
and read process.env.__VINEXT_BUILD_ID directly in dev-server instead of
threading it as an explicit parameter through createSSRHandler.

* regen snaps

* fix: stabilize entry-templates snapshot by setting generateBuildId in pages-basic fixture

The random UUID from resolveBuildId was embedded in the server entry snapshot,
making it fail on every run. Set generateBuildId to a fixed string in the
pages-basic test fixture so the snapshot is deterministic.

* fix: move buildId to NextURL to match Next.js API surface

Next.js exposes buildId on nextUrl (request.nextUrl.buildId), not directly
on NextRequest. Move the getter to NextURL and add a pass-through on
NextRequest for convenience. Update tests to assert both paths.
2026-03-10 13:59:00 +00:00
Nathan Nguyen c524f150cc feat: support generateSitemaps() for paginated sitemaps (#399)
* test: add products/sitemap.ts fixture with generateSitemaps

Adds a nested sitemap fixture that exports generateSitemaps() returning
two IDs (0, 1). The default export receives { id } and returns entries
scoped to that batch. Used by the integration tests for paginated
sitemap support.

* feat: support generateSitemaps() for paginated sitemaps

When a sitemap.ts module exports generateSitemaps(), the runtime
metadata route handler now:

1. Matches URLs at /{prefix}/sitemap/{id}.xml instead of the base
   /{prefix}/sitemap.xml (which is no longer served)
2. Calls generateSitemaps() to get the list of valid IDs
3. Validates the requested ID against the list (404 on mismatch)
4. Calls the default export with { id: rawId } and serializes the
   result with sitemapToXml()

This matches Next.js behavior: each paginated sitemap is a standalone
<urlset> document. Next.js does not generate a <sitemapindex>.

Closes #397

* test: add integration tests for generateSitemaps and update snapshots

- /products/sitemap/0.xml serves batch-0 entries
- /products/sitemap/1.xml serves batch-1 entries
- /products/sitemap/99.xml returns 404 (invalid ID)
- /products/sitemap.xml returns 404 (base URL not served)
- scanMetadataFiles discovers nested products/sitemap.ts
- Updated entry template snapshots for new generateSitemaps handling

* fix: address Codex review — typed id and single-segment matching

- Pass the original typed id from generateSitemaps() instead of the raw
  URL string, so numeric IDs stay numeric in the handler
- Reject multi-segment paths (e.g. /sitemap/a/b.xml) to avoid stealing
  requests from legitimate nested routes
- Fix shims.test.ts to find root sitemap by servedUrl, not first match

* fix: address ask-bonk review feedback

- Break long condition into multi-line for readability
- Add TODO for makeThenableParams when upgrading to v16 Promise params
- Add string ID ("featured") to fixture alongside numeric IDs
- Add test: missing .xml extension returns 404 (ported from Next.js)
- Add test: string ID ("featured") is served correctly
- Update entry template snapshots
2026-03-10 09:26:55 +00:00
Nathan Nguyen 0b63ad5237 perf: pre-split route patterns and hoist URL split out of match loop (#396)
* perf: pre-split route patterns and hoist URL split out of match loop

Route matching runs on every request and iterates all routes until a
match is found. Previously, matchPattern() re-split both the URL and
the pattern string on every iteration:

  for (const route of routes) {
    // url.split("/") — same URL, re-split every iteration
    // pattern.split("/") — pattern never pre-computed
    matchPattern(normalizedUrl, route.pattern);
  }

For a 50-route app on a 404, that's 50 URL splits + 50 pattern splits
+ 50 Object.create(null) + 100 .filter(Boolean) calls.

Fix:
- Add `patternParts: string[]` to Route and AppRoute interfaces,
  computed once at scan time
- Split the URL once in matchRoute/matchAppRoute before the loop
- Pass pre-split arrays to matchPattern instead of raw strings
- Apply the same optimization to generated production entries
  (pages-server-entry.ts, app-rsc-entry.ts)
- Fix intercepting route lookup to use the new array-based API

* update entry-templates snapshots

---------

Co-authored-by: James <james@eli.cx>
2026-03-10 08:18:30 +00:00