Commit Graph

1503 Commits

Author SHA1 Message Date
Nathan Nguyen 9cae9cb271 refactor: extract early request pipeline helpers (#983) 2026-04-30 10:03:19 +01:00
Nathan Nguyen 6ea8e51643 fix(app-router): return action-not-found for stale server actions (#985)
* fix(app-router): return action-not-found for stale server actions

Unknown or stale Server Action ids currently fall into the generic action error path and return 500. That hides deployment skew from the client and misses the Next.js action-not-found response contract.

The action loader is an external runtime boundary, so the helper now treats loaded actions as unknown until narrowed, maps recognized missing-action failures to the 404 x-nextjs-action-not-found protocol, and keeps unrelated loader failures on the existing 500 reporter path.

Tests cover fetch actions, progressive form decode misses, missing exports, generic loader failures, and the App Router integration response.

* chore: rerun ci
2026-04-30 08:59:35 +01:00
Divanshu Chauhan (divkix) 155be501e1 fix: accept experimental.swcEnvOptions config without erroring (#977)
* fix: accept experimental.swcEnvOptions config without erroring (#774)

Next.js added experimental.swcEnvOptions for SWC preset-env polyfill injection.
vinext uses Vite for transforms, so polyfill injection is not applicable, but
the config key should be accepted without errors. Add:
- CONFIG_SUPPORT entry in check.ts (unsupported with detail)
- Warning during config resolution in next-config.ts
- Tests for analyzeConfig detection and dot-notation matching

* refactor: align swcEnvOptions messaging and add test coverage

- Change runtime warning from "not yet supported" to "not applicable"

- Update CONFIG_SUPPORT detail for consistent messaging

- Add test coverage for swcEnvOptions warning in next-config.test.ts

* fix(check): update test assertion for swcEnvOptions message

The CONFIG_SUPPORT message for experimental.swcEnvOptions was updated
to indicate it's not applicable since vinext uses Vite instead of SWC.
This updates the corresponding test assertion to match the new message.

Fixes CI failure in tests/check.test.ts
2026-04-30 08:53:58 +01:00
Divanshu Chauhan (divkix) dc28af1cb6 fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764) (#945)
* fix: add ForbiddenBoundary/UnauthorizedBoundary for HTTP access fallback recovery (#764)

Per-layout forbidden/unauthorized boundary files (forbidden.tsx, unauthorized.tsx)
are now discovered by the router and wired into the App Router component tree via
dedicated ForbiddenBoundary and UnauthorizedBoundary client components. Previously,
only NotFoundBoundary existed, causing 403/401 errors thrown during RSC rendering
(e.g. inside Suspense boundaries) to escape into the generic error shell — this
could produce mismatched Flight/HTML data and client-side 'Connection closed' failures.

Next.js upstream: vercel/next.js#92231
  https://github.com/vercel/next.js/pull/92231

* fix: address review feedback on ForbiddenBoundary/UnauthorizedBoundary (#945)

- Export ForbiddenBoundaryInner and UnauthorizedBoundaryInner for testing
- Strengthen tests to import and call getDerivedStateFromError directly
- Use exact equality instead of startsWith for status code digest matching
- Make forbiddenPaths and unauthorizedPaths non-optional on AppRoute (matching notFoundPaths)
- Fix inaccurate ported-from attribution in test header

* fix: remove duplicate forbidden/unauthorized path iteration and update comment

Removes dead code in app-rsc-entry.ts (lines 185-190) that duplicated the existing import registration loops.

Also updates the stale nesting order comment in app-page-route-wiring.tsx to include Unauthorized and Forbidden boundaries.

* fix: align NotFoundBoundary digest matching and tests with Forbidden/Unauthorized patterns

- Use exact === instead of startsWith in NotFoundBoundary digest check
- Add forbiddenPaths/unauthorizedPaths per-layout test fixtures
- Fix property ordering: singular before plural for all boundary types

* fix: resolve rebase conflicts — align manifest code with main refactor

- Use buildAppRscManifestCode for manifest generation (matching main)
- Remove redundant || [] guards for non-optional forbiddenPaths/unauthorizedPaths
- Fix test fixture indices to match new module import count

* fix: add null guards for notFoundPaths, forbiddenPaths, unauthorizedPaths in manifest

TypeError: route.notFoundPaths is not iterable when routes lack per-layout boundary files.

CI was failing on app-router.test.ts with the error. Added null guards in both registerRouteModules() and buildRouteEntries() to handle undefined arrays.

* fix: reorder route-level boundary nesting for merge with main

Move ErrorBoundary after NotFoundBoundary/ForbiddenBoundary/
UnauthorizedBoundary to match Next.js nesting order (Error >
Unauthorized > Forbidden > NotFound). Resolves merge conflict
with bc70e1c.
2026-04-30 07:46:07 +01:00
Divanshu Chauhan (divkix) f0d9aeae56 feat: add unstable_io shim for next/cache (#948) 2026-04-29 21:22:57 +01:00
James Anderson c5da1db4ff refactor: share config support list across checks (#971)
* refactor: share config support list across checks

* fmt
2026-04-29 20:17:07 +00:00
Divanshu Chauhan (divkix) b49974eb4f fix: add tests verifying strings thrown in Server Components are not swallowed (#950)
* fix: add tests verifying strings thrown in Server Components are not swallowed (#810)

Next.js had a bug where createReactServerErrorHandler returned early
for typeof thrownValue === 'string', swallowing the error before
logging. vinext's rscOnError already handles strings correctly (they
flow through the normal error path and get wrapped into Error objects).

Added:
- Fixture page that throws a string in a Server Component
- Integration test verifying the error boundary renders with the string message
- Runtime test confirming rscOnError returns a valid digest for string throws
- Static test confirming generated code has no early string-return path

Upstream: vercel/next.js@b9ca95c62d

* fix: address review feedback on string error handling tests (#950)

- Move prodRscOnError creation into beforeAll to avoid duplicating new Function construction
- Remove brittle positive assertions (instanceof Error, String(error)) from static test; keep only the negative anti-pattern guard
- Use textContentByTestId helper for integration test precision over loose html.toContain
2026-04-29 21:14:04 +01:00
Divanshu Chauhan (divkix) bc70e1c6c2 fix: correct route-level boundary nesting order to match Next.js (#970)
Move NotFoundBoundary wrapping before ErrorBoundary wrapping at the
route level in app-page-route-wiring.tsx. In Next.js, ErrorBoundary
is outermost relative to NotFoundBoundary. The segment-level ordering
was already correct; only the route-level code was inverted.
2026-04-29 21:10:31 +01:00
Divanshu Chauhan (divkix) 3c19cf0ab2 fix: align metadataRouteSuffix exemption list with Next.js (#969)
metadataRouteSuffix exempted robots and manifest in addition to sitemap,
while getMetadataRouteSuffix only exempts sitemap (matching Next.js).
This was latent because robots/manifest are nestable: false, but the
inconsistency could cause subtle bugs if nestable ever changes.

Remove the extra exemptions so both functions match Next.js exactly.
2026-04-29 21:10:13 +01:00
Nathan Nguyen f8d2d39107 refactor: extract app prerender endpoints (#965) 2026-04-29 21:09:51 +01:00
Nathan Nguyen 78edbc3b46 refactor: Extract app route handler dispatch (#968) 2026-04-29 21:05:38 +01:00
Nathan Nguyen 9ab38193eb refactor: extract server action rsc flow (#967) 2026-04-29 18:42:32 +01:00
Nathan Nguyen 776fc9eea6 refactor: extract app rsc manifest construction (#966) 2026-04-29 18:35:08 +01:00
Divanshu Chauhan (divkix) e0854972eb fix: support file:// URLs in cacheHandler config path (#944)
* fix: support file:// URLs in cacheHandler config path (#728)

- Add cacheHandler and cacheMaxMemorySize to NextConfig type
- Add cacheHandler to ResolvedNextConfig with file:// URL handling
- Create resolveCacheHandlerPathToFilesystem() helper using fileURLToPath()
- Convert file:// URLs from import.meta.resolve() to filesystem paths
  during config resolution to prevent broken path concatenation

Matches Next.js commit 9a0214a1 which added the same file:// URL handling
for ESM projects using import.meta.resolve() for the cache handler path.

* fix: drop unused export on resolveCacheHandlerPathToFilesystem

* fix: resolve cacheMaxMemorySize and add cacheHandler tests

- Add cacheMaxMemorySize to ResolvedNextConfig and resolve it from input config,
  preventing silent config swallowing
- Add 7 tests for resolveNextConfig cacheHandler: file:// URL resolution,
  absolute/relative path passthrough, null/undefined defaults
- Also add cacheMaxMemorySize resolution and default tests
2026-04-29 18:32:39 +01:00
Divanshu Chauhan (divkix) cc0001e5ea fix: add experimental.prefetchInlining to compatibility check (#949)
* fix: add experimental.prefetchInlining to compatibility check (#860)

Next.js flipped experimental.prefetchInlining from false to true by default
in PR #92863. vinext does not implement per-segment prefetch inlining — it
uses unified RSC navigation payloads — so no behavioral change is needed.
This adds the config option to the compatibility scanner so vinext check
correctly reports it as supported.

* fix: mark experimental.prefetchInlining as partial, add test

- Change status from supported to partial (per review feedback —
  option is recognized but underlying behavior is not implemented)
- Add test case in tests/check.test.ts matching existing
  experimental.* config patterns
2026-04-29 18:30:20 +01:00
Divanshu Chauhan (divkix) 0846931bcb fix: report stale ISR background revalidation errors via onRequestError (#947)
* fix: report stale ISR background revalidation errors via onRequestError (#775)

Next.js 16 now reports errors during background ISR revalidation of stale
pages via routeModule.onRequestError(). Previously these errors were silently
swallowed (only logged to console). This ensures instrumentation hooks
(Sentry, OpenTelemetry) observe revalidation failures.

- Add errorContext parameter to triggerBackgroundRegeneration() in isr-cache.ts
- Report errors via reportRequestError() with revalidateReason: "stale"
- Update App Router entry template to pass route context to regen callbacks
- Update Pages Router entry template to forward error context through wrappers
- Update dev-server.ts and pages-page-data.ts to pass route context
- Update snapshots and test assertions for generated code changes

* fix: remove dead ISRRegenErrorContext export flagged by knip
2026-04-29 18:23:31 +01:00
Divanshu Chauhan (divkix) 0b030f1c40 fix: support static metadata URL resolver (fillStaticMetadataSegment) (#946)
* fix: support static metadata URL resolver (fillStaticMetadataSegment) (#861)

Port fillStaticMetadataSegment from Next.js (commit 7873aea) to correctly
resolve URLs for static metadata files under dynamic parent segments.

- Add fillStaticMetadataSegment to compute static URLs with "-" placeholders
  for dynamic/catch-all parent segments
- Update scanMetadataFiles to normalize dynamic parents in servedUrl for
  static metadata files (e.g. blog/[slug]/opengraph-image.png → /blog/-/opengraph-image)
- Strip route groups and parallel route slots from URL paths, applying
  djb2 hash suffixes for collision avoidance
- Keep dynamic metadata files on the existing urlPrefix path for
  patternParts-based runtime matching

Ported from Next.js: test/e2e/app-dir/metadata-static-file/

* fix: address review feedback for PR #946

- Fix redundant ternary: both branches of `lastSlash === -1 ? "" : ""` returned empty string
- Extract `computeMetadataRouteSuffix` shared helper to eliminate code duplication between `scanMetadataFiles` inline IIFE and `fillStaticMetadataSegment`/`getMetadataRouteFilename`

Co-Authored-By: ask-bonk[bot] <noreply@github.com>

---------

Co-authored-by: ask-bonk[bot] <noreply@github.com>
2026-04-29 18:17:31 +01:00
Divanshu Chauhan (divkix) 79b5c3711f fix: add next/offline shim with useOffline() hook (#943)
* fix: add next/offline shim with useOffline() hook (#701)

Add a no-op shim for next/offline that exports useOffline() returning false.
Registers it in the shim resolution map, type declarations, and import support
check so libraries importing next/offline don't fail. The full offline retry
behavior is deferred until the feature stabilizes upstream.

* fix: address review feedback for next/offline shim

- Add "use client" directive to offline.ts for RSC plugin compatibility
- Add JSDoc block explaining the stub (matching amp.ts pattern)
- Fix test comment to reference PR instead of implementation file

* Apply suggestion from @james-elicx

---------

Co-authored-by: James Anderson <james@eli.cx>
2026-04-29 17:12:00 +00:00
Nathan Nguyen c63155d913 fix(fonts): stabilize next/font/google runtime registrations (#956)
* fix(fonts): make Google font shim registrations stable

Repeated next/font/google calls currently mint counter-based class names and append new SSR style rules. During dev HMR, the app module can re-evaluate while the shim module stays alive, so stale classes and links accumulate and the first emitted :root variable keeps winning.

The bad invariant was treating each runtime call as a new font registration. Use a deterministic identity for each family and options pair, remove the non-Next :root variable rule, and keep variable declarations scoped to the returned variable class.

Regression coverage verifies stable class identities, no :root emission, and no SSR style growth for repeated equivalent calls.

* fix(fonts): address Google font review feedback

The first PR version left two edge cases in the new stable identity path: crafted font family names could still leak unsafe selector characters into class names, and omitted display hashed differently from explicit display: swap even though both generate the same font URL.

Sanitize the readable family segment used in selectors, normalize display to the same default used by buildGoogleFontsUrl, and tighten the CSS variable regression so it checks CSS added by the current call instead of accumulated module state.

* fix(fonts): canonicalize Google font identity hashing

Equivalent Google font loader options could generate the same CSS and font URL while still producing different class identities. That reintroduced duplicate SSR style rules when option arrays used a different order or representation.

The identity hash now canonicalizes unordered option sets, default-style equivalence, and fallback whitespace before hashing. A focused regression test covers reordered weights and subsets, explicit normal style, and trimmed fallback names.

* fix(fonts): normalize default fallback font identities

Omitting next/font/google fallback produced the same CSS as fallback: ["sans-serif"] but still hashed a different raw option shape. That left the most common default call path outside the deterministic identity dedupe.

The identity hash now uses the resolved fallback list and no longer includes the serialized font-family CSS string. Regression coverage verifies implicit and explicit default fallback calls share class and variable identities, and the :root assertion now checks only styles added by its own test.
2026-04-29 17:23:21 +01:00
Nathan Nguyen e3bc380f66 test(app-router): harden RSC route matching delegation assertion (#954) 2026-04-29 16:54:15 +01:00
Nathan Nguyen 1e5f80e008 refactor(app-router): delegate RSC preload hint normalization (#952)
* refactor(app-router): delegate RSC preload hint normalization

The App Router RSC entry currently owns the React Flight preload hint stream transform inline. That keeps byte-stream buffering logic inside generated code, so the behavior is only indirectly covered by generated-entry snapshots.

Move the stream normalization into a typed server helper and keep the generated RSC entry as a thin wrapper around renderToReadableStream. Add direct coverage for complete lines, split chunks, and final unterminated Flight hint lines.

* test(app-router): cover mixed Flight preload hints
2026-04-29 15:23:16 +00:00
Nathan Nguyen cf5471e95a fix(app-router): fixing cache request leaks (#907) 2026-04-29 14:59:14 +01:00
Nathan Nguyen d0b81db244 refactor(app-router): delegate RSC route matching (#953)
The RSC entry still owned route matching, intercept lookup construction, and dynamic metadata pattern matching inside generated source. That kept request-path behavior in a template string and made direct tests awkward.

Move the matching behavior into a typed server helper, keep route import and manifest emission in the generator, and update entry-template assertions to verify delegation instead of inline matcher code.
2026-04-29 14:25:22 +01:00
Nathan Nguyen 33526bd89c fix(middleware): align cookies and external rewrites (#919)
Middleware cookies set through NextResponse only updated Set-Cookie, so cookies() in the same request could not observe values written by middleware. External middleware rewrites were normalized to pathname and search, so cross-origin destinations were routed locally instead of being proxied.

Mirror middleware cookie mutations into x-middleware-set-cookie and merge that internal header back into the request cookie store. Preserve cross-origin rewrite URLs through middleware execution and proxy them across app, pages, prod, and deploy request paths, while stripping internal middleware headers from client and upstream responses.
2026-04-29 11:14:45 +01:00
Nathan Nguyen dd1dfae3c6 fix(app-router): scope layout params and layout error boundaries (#938)
* fix(app-router): scope layout params and propagate layout head errors

Layouts received full route params, layout generateMetadata failures were swallowed, and layout-thrown forbidden()/unauthorized() fell back through the not-found boundary path. Those diverge from Next.js when nested layouts depend on segment params, when head generation fails before page render, or when HTTP access APIs are thrown from a layout.

The generated RSC entry now delegates param slicing, head resolution, and parent access-boundary selection to typed runtime helpers. The helpers keep route-specific imports in the entry while moving the behavioral core into unit-tested modules.

Tests port Next.js layout params, global-error, forbidden, and unauthorized coverage into focused helper and integration regressions.

* test(app-router): cover layout viewport error boundaries

Layout generateViewport() failures now intentionally share the same boundary path as layout generateMetadata() failures. Copilot flagged that this behavior change needed direct coverage.

Add dev and production-preview compat assertions for co-located error.tsx handling and global-error escalation, with fixtures that throw from layout generateViewport().

* chore(app-router): address head review nits
2026-04-29 10:40:49 +01:00
Nathan Nguyen 37e4e6951d fix(app-router): tag cached pages by route pattern (#923)
Cached App Router pages only carried implicit revalidatePath tags derived from the resolved URL. That left dynamic routes tagged with concrete values, so pattern-scoped invalidation like revalidatePath("/blog/[slug]", "layout") could miss entries rendered at /blog/hello.

The generated RSC entry now delegates implicit tag construction to a typed helper that combines exact pathname tags with route-segment-derived layout, page, and route handler tags. Focused tests cover dynamic route patterns, route groups, root aliases, and route handler leaves.

Refs cloudflare/vinext#921
2026-04-29 10:37:33 +01:00
Nathan Nguyen 213173aaf8 fix(app-routes): preserve returned cookie precedence (#955)
App route handlers appended cookies().set() mutations after cookies already present on the returned Response. That made same-name mutable cookies override the Response cookies, which differs from Next.js route-handler semantics.

Next.js treats mutable cookies as fallbacks and reapplies returned Response cookies as final values. Deduplicate vinext pending mutable cookies by name, filter out pending cookies shadowed by the returned response, and then reappend the returned Set-Cookie headers last.

Adds focused helper coverage plus a nextjs-compat fixture that exercises the live App Route path.
2026-04-29 10:35:03 +01:00
Divanshu Chauhan (divkix) f4d4ad6609 fix: serialize SSR render and head collection in Pages Router (#792) (#951)
Move renderToReadableStream() before buildPagesShellHtml() + clearSsrContext()
so that <Head> and other SSR state collectors (styled-jsx, useServerInsertedHTML)
are populated before they are read. This fixes a race condition where head styles
were silently dropped because they were collected before the page had finished
rendering.

Mirrors Next.js fix: vercel/next.js@9853944

The specific styled-jsx race condition (styledJsxInsertedHTML() called
concurrently with page render via Promise.all) does not exist in vinext because
vinext does not implement a styled-jsx style registry at all. However, the same
class of ordering bug affected vinext's Pages Router prod path where getSSRHeadHTML
was called before rendering completed.
2026-04-29 10:33:55 +01:00
Divanshu Chauhan (divkix) db57d45070 Shim next/root-params (#941)
* fix(app-router): shim next root params

* fix: address review feedback

* address review feedback

* test: update snapshots for root params cleanup change
2026-04-28 21:43:45 +01:00
Divanshu Chauhan (divkix) f972d45034 fix: resolve symlink paths in standalone BFS package copy (#724) (#942)
* fix: resolve symlink paths in standalone BFS package copy (#724)

When resolvePackageJsonPath finds a package via the lookup-paths
fallback, the returned path may be a symlink (e.g., under a hoisted
node_modules/ directory). Calling createRequire() on a symlink
path causes Node module resolution to miss transitive dependencies
because it walks up from the symlink ancestor directories rather
than the real (e.g., .pnpm/) path.

Fix: call fs.realpathSync() on the resolved package.json path
before using it for createRequire() and readPackageJson().

Also adds E2E test coverage for the standalone output mode.

* fix: address review feedback

Add standalone-output to CI E2E matrix, rename server header test
to reflect what it actually asserts, fix unused req parameter.
2026-04-28 21:19:09 +01:00
Nathan Nguyen 16cb9ee5ef fix(app-router): return 405 for non-action mutations to static pages (#940)
* fix(app-router): return 405 for non-action mutations to static pages

Static and SSG App Router page routes currently render HTML or RSC payloads for POST and PUT requests that are not server actions. That diverges from Next.js and makes unsupported mutation methods look successful.

The App Router page render path only handled server actions and route handlers before falling through to page rendering. Add a small page-method policy helper and call it from the generated RSC entry before page rendering so static and SSG candidates return 405 with Allow: GET, HEAD while possible server action POSTs still pass through.

Tests cover the helper policy and the live App Router fixture for static and ISR pages.

* fix(app-router): preserve 405 allow header

Set the App Router page 405 Allow header after merging middleware response headers so middleware cannot clobber the methods advertised by the generated Method Not Allowed response.

Add a focused regression test that keeps middleware header propagation while asserting Allow remains GET, HEAD.
2026-04-28 20:37:49 +01:00
Nathan Nguyen e9139542cb fix(app-router): reject middleware control responses in route handlers (#939)
* fix(app-router): reject middleware control responses in route handlers

App Route handlers currently treat NextResponse.next() and NextResponse.rewrite() as ordinary 200 responses. That diverges from Next.js, where those helpers are middleware control-flow signals and are rejected after a route handler returns.

The route-handler execution path now validates returned responses before cache policy or response finalization runs. Focused unit and integration tests cover the invalid next and rewrite helper responses.

* Update tests/app-route-handler-execution.test.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/vinext/src/server/app-route-handler-response.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(app-router): validate route handler ISR regeneration responses

Stale App Route handler regeneration called runAppRouteHandler directly and could serialize a middleware control response into the ISR cache. That bypassed the validation used by the live route-handler execution path.

Run the same response assertion before background regeneration writes a new APP_ROUTE cache entry, restore exact Next.js error wording for parity, and cover the x-middleware-next boundary semantics.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-28 20:36:17 +01:00
Nathan Nguyen ebeb044e01 fix(app-router): flush server-inserted HTML during SSR streaming (#926)
App Router SSR rendered useServerInsertedHTML callbacks only once before returning the HTML stream. CSS-in-JS registries registered at shell render were cleared before later Suspense chunks, so styles collected after the shell never reached streamed HTML.

Keep callback registrations alive for the stream lifetime and let the tick-buffered HTML transform request insertion HTML on each flush after the initial head injection. This matches Next.js per-chunk server inserted HTML transform and preserves cleanup on stream completion or cancellation.

Adds regression coverage for persistent callback rendering and a Next.js-ported Suspense CSS-in-JS streaming order case.
2026-04-28 19:35:10 +01:00
Nathan Nguyen bee7e6b654 fix(router): reject invalid app route discovery conflicts (#928)
* fix(router): reject invalid app route discovery conflicts

App Router route discovery currently lets upward interception markers climb past the normalized root and relies on generic duplicate-pattern validation when a page and route handler share the same app route.

The scanner now validates visible interception climb depth before resolving the target, and reports an explicit page/route conflict before running generic route pattern validation.

Regression coverage captures the Next.js parity cases for root interception markers, route-group-transparent roots, two-level upward markers, and page plus route handler conflicts.

* fix(router): address app route validation review
2026-04-28 19:06:39 +01:00
Nathan Nguyen 0b6eee2221 fix(app-router): restore hash anchors on history traversal (#929)
* fix(app-router): restore hash anchors on history traversal

App Router hash-only entries can be pushed without saved vinext scroll state while vinext has browser scroll restoration set to manual. On forward traversal, the URL moves back to the fragment but the browser no longer performs native anchor scrolling, so the page can remain at the top.

Restore saved numeric scroll state first, then fall back to Next-compatible hash target scrolling when no vinext scroll state exists. The regression test drives a hash-only Link entry through back and forward traversal.

* test(app-router): cover hash scroll edge cases
v0.0.45
2026-04-28 12:31:40 +01:00
Nathan Nguyen d395994d2e fix(cache): serve stale unstable_cache entries (#932) 2026-04-28 12:22:06 +01:00
Nathan Nguyen e9c4fb75a3 test(app-router): stabilize RSC fetch error redirects (#925)
The RSC fetch error E2E used direct private navigation calls and Playwright route mocking for a redirect chain. Those assumptions are timing-sensitive and can skip the intended RSC request under parallel Playwright runs.

Drive the cases through fixture links and serve the redirect-to-500 chain from middleware so the test observes real browser fetch behavior. This keeps coverage for hard navigation to the post-redirect URL while removing the route redispatch dependency.
2026-04-28 12:03:10 +01:00
Nathan Nguyen c80c756c11 fix(app-router): keep ownerless URL commits from releasing snapshots (#930)
Ownerless URL sync commits from hash navigation and patched history methods could decrement an active client navigation snapshot. That let hooks stop reading the pending render snapshot before the transition committed, briefly exposing stale committed URL state.

The snapshot release invariant was tied only to commit count underflow, not to ownership of the activated snapshot.

Only navigation-owned commits release snapshots now, while App Router superseded-transition cleanup opts into snapshot release without taking pending pathname ownership. A regression test covers external history.pushState during an active pending render snapshot.
2026-04-28 11:36:17 +01:00
Nathan Nguyen ae45ff2cf2 fix(router): ignore pre-root parallel slots for routed layouts (#927)
Routes whose first layout lives below app/ currently inherit slots discovered before that layout and clamp them to layout index 0. That incorrectly passes top-level parallel slots into a separate root layout.

Keep the pre-layout sentinel while scanning ancestors, skip those slots once the route has a real layout, and preserve the existing layout-less scanner metadata path for separate validation work. Add routing tests for the pre-root leak and the valid in-subtree slot case.
2026-04-28 11:32:38 +01:00
Nathan Nguyen 67e963bfa6 fix(cache): scope use cache entries by build id (#924)
* fix(cache): scope use cache entries by build id

Shared "use cache" entries currently key only on the cached function id and serialized arguments. That lets a persistent cache handler reuse serialized results after a deploy when the generated function id and arguments remain stable.

The violated invariant is that a cache entry belongs to the build that produced its function body, matching Next.js buildId key material.

Include __VINEXT_BUILD_ID in the shared cache key when it is available, preserving the existing key shape for environments without a build id. Add a regression test that proves two builds with the same function id do not share the stale value.

* fix(cache): capture use cache build id at registration

Shared "use cache" wrappers should derive their build scope from the bundle that registered the cached function, not from a fresh environment lookup on every invocation.

The prior implementation still produced build-scoped keys, but it kept the build id read inside the hot wrapper path and allowed a mutated environment to change the key for an already-registered function.

Resolve the build id once in registerCachedFunction and close over it when constructing cache keys. Keep the direct process.env.__VINEXT_BUILD_ID access so Vite can inline the build id for processless Worker bundles.
2026-04-28 11:24:47 +01:00
Nathan Nguyen dce7b70329 fix: normalize NextRequest.url through nextUrl (#931) 2026-04-28 11:19:27 +01:00
James Anderson 098240d474 chore(ci): change bonk opencode version (#937) 2026-04-28 09:19:17 +00:00
James Anderson a701d72bab chore(ci): change bonk perms back to normal (#936) 2026-04-28 09:02:43 +00:00
James Anderson 103aef2b2d chore(ci): change bigbonk permissions prereq (#935) 2026-04-28 08:46:16 +00:00
James Anderson 8747b3d1a0 chore(ci): change bonk back to opus 4.6 (#934)
* chore(ci): change bonk back to opus 4.6

* add nathan to bonk workflow

* boink
2026-04-28 08:01:00 +00:00
Nathan Nguyen 18962c51d6 fix(app-router): action redirects (#910)
* Fix app form action redirects

* Trigger CI rerun

* Refactor progressive action handling

* Address app action review feedback

* Trigger CI rerun
2026-04-27 20:40:03 +01:00
Nathan Nguyen 598782e7b5 Make revalidatePath expire route-scoped fetch cache reads (#917)
Fetch cache entries without explicit next.tags could survive revalidatePath(). The page or route cache entry was invalidated, but the regenerated render could still read the old fetch response and reproduce stale data.

The missing boundary was Next.js soft tag semantics: path-derived implicit tags are read-time context for fetch cache lookups, not durable tags stored on the fetch entry.

Thread route-derived soft tags through App Router renders, pass them to fetch cache reads, and teach the memory and KV handlers to treat revalidated soft tags as cache misses without deleting shared fetch entries.

Regression coverage verifies revalidatePath invalidates untagged fetch cache reads and that KV soft-tag misses do not delete the shared entry.
2026-04-27 20:00:38 +01:00
Iago Lima Toledo dcb417dfa3 fix(fonts): google fonts building perfectly (it was just a regex 😭) (#920)
* fix(fonts): google fonts building perfectly (it was just a regex 😭)

* fix(fonts): allow newlines inside bracket clauses

Previous fix's [^;\n]+? blocked the semicolon-less line bug but also
stopped matching multi-line bracket imports/exports — Prettier wraps
specifiers across lines once the clause crosses printWidth.

Clause is now a sequence of brace blocks (newlines allowed inside, but
[^}] keeps the lazy match from spanning past the matching close brace)
or single non-;\n chars. Multi-line shapes match again; the original
swallow-prevention guard still holds.

Reverts the export regex back to [^}]+ — } already delimits, so the
extra \n exclusion was over-defensive.

* fix(fonts): apply formatter to test file

---------

Co-authored-by: MrIago <28714061+MrIago@users.noreply.github.com>
2026-04-27 18:01:40 +01:00
James Anderson fc5f7a6492 fix(dev): hoist socket-error backstop to module top-level so lifecycle events can't remove it (#916)
* debug(dev): hoist socket-error backstop to module top-level + opt-in trace

Two changes intended to pin down why the process-level handler from
PR #913 still doesn't catch the ECONNRESET trace some users report:

1. Move installation from inside configureServer to module top-level,
   guarded by Symbol.for to prevent double-install. Earlier versions
   tied teardown to httpServer 'close', which Vite emits on dep
   re-optimization, full reloads, and other lifecycle events — leaving
   a window where the listener is absent when a stale stream errors.
   Module-level install + Symbol guard removes that window entirely.

2. Add an opt-in console.warn marker behind
   VINEXT_DEBUG_SOCKET_ERRORS=1 that fires whenever the listener
   absorbs a peer-disconnect error. The visible
   `node:events:487 throw er; // Unhandled 'error' event` output
   otherwise leaves it ambiguous whether the listener never ran
   (install / lifecycle issue) or ran but was somehow bypassed
   (different async surface / dispatcher). The marker disambiguates.

No behavior change for users not setting the env flag beyond the
hoisting. Filter codes and re-throw shape are unchanged from #913.

Refs #905.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address bonk review on dev socket-error backstop

Three issues from PR #916 review:

1. Module-top-level install was wrong — it ran during 'vinext build',
   'vp pack', and Vitest worker imports too, so the JSDoc 'dev-only'
   claim was inaccurate and a real ECONNRESET from a pre-render fetch
   in CI would have been silently swallowed. Move the install back into
   configureServer (which only fires in dev) but **without** binding
   teardown to httpServer 'close' — that was the original lifecycle bug
   PR #913 had. Symbol.for guard makes re-invocation a no-op so the
   listener still survives server restarts within a session.

2. Restore the middleware-mode skip dropped in the previous commit.
   Embedders running vinext in middleware mode (Express/Connect) keep
   ownership of their own process-level handlers. Gated on
   server.httpServer, matching the prior PR #913 behavior.

3. Document the listener-ordering implication of installing during
   configureServer (registers late in the queue, so Sentry / structured
   logging / test-runner hooks still observe non-peer-disconnect errors
   before vinext's sync re-throw aborts iteration), and the Symbol.for
   first-loaded-wins caveat for the multi-version case.

Refactor: collapse the duplicated `(err as Error & { code }).code` casts
into a single `peerDisconnectCode` helper that returns the matched code
or undefined, so the debug log path reuses it without recasting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dev): re-hoist socket-error backstop to module load (configureServer too late)

The previous attempt to address bonk's review by moving install back
into configureServer was wrong — it didn't restore working behavior:

  $ VINEXT_DEBUG_SOCKET_ERRORS=1 vp dlx vinext@455a61b dev
  ...
  GET /stage/oliver 200 in 1.1s
  node:events:487
        throw er; // Unhandled 'error' event
  Error: read ECONNRESET
  ...

No `[vinext] dev socket-error backstop installed` startup line. The
function never ran. Cause: in vite-plus's plugin lifecycle,
`server.httpServer` is null at the moment configureServer fires, so
the `if (server.httpServer)` middleware-mode guard skipped install.
Requests still get served (httpServer is created later) but the
listener was never attached.

Hoist back to module load. To address bonk's "loaded in build/test
contexts too" concern, gate install on:

  - `process.env.VITEST === "true"`  → Vitest worker imports
  - `process.argv` contains "build"  → `vinext build` / `vp build`

`process.argv` is read because the CLI entry imports index.ts before
it has a chance to set an env var, and `configResolved` runs too late
for module-load gating.

Updated JSDoc to be honest about the install context, the listener-
ordering implication, the middleware-mode embedder situation, and the
Symbol.for first-loaded-wins caveat.

Refs #905.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(dev): positive-gate socket backstop on argv[2]==='dev'

Replace the negative VITEST/build skip list with a single positive
check: install only when argv[2] === "dev". Matches `vinext dev`,
`vp dev`, `vite dev`, and any CLI following the "<bin> dev"
convention. Everything else — build, Vitest workers, library
embedders with a custom runner — skips install, so genuine
peer-disconnect errors surface normally in those contexts.

Cleaner default: no listener unless we're confident this is a dev
server invocation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(dev): gate socket backstop on Vite's command==='serve'

Replace argv-sniffing with the canonical signal: Vite's `config()`
hook receives `{ command: "serve" | "build" }` directly. Install
the backstop only when `command === "serve"`. Works identically for
`vinext dev`, `vp dev`, `vite dev`, and library embedders that call
`createServer` themselves — anywhere Vite considers itself a dev
server.

`config()` runs before `configureServer` (so before httpServer
matters) and before the dep-optimization or full-reload events that
broke the earlier `httpServer.close`-tied teardown. Symbol.for guard
keeps the install idempotent across server restarts within a session.

Removes:
  - Module-load-time install + argv[2] === "dev" check
  - VITEST / build env-var negative skip list

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dev): narrow gate to !isPreview so vite preview skips install

`command === "serve"` covers both `vite dev` and `vite preview` (the
post-build static server). Preview doesn't stream RSC and doesn't
need this guard, so narrow to `command === "serve" && !env.isPreview`.

`vinext start` runs prod-server.ts directly without Vite, so it was
already correctly excluded — `config()` never fires there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: extend socket-error backstop to vinext start (Next.js parity)

Next.js installs uncaughtException + unhandledRejection handlers in
their router-server unconditionally — no dev/prod gate. Both the
Node.js dev server and the Node.js prod server share the same
process-level error guards. See vercel/next.js
packages/next/src/server/lib/router-server.ts:809-810.

vinext was only guarding the dev path. `vinext start` runs
prod-server.ts directly as a Node HTTP server (used for self-hosted
deploys; Cloudflare Workers prod doesn't load this module — the
runtime owns socket lifecycle there) and had the same theoretical
exposure to peer-disconnect crashes through the pipeline() / fetch()
paths it streams responses through.

Refactor:
  - Extract the install function from index.ts into a new
    src/server/socket-error-backstop.ts (drop the "Dev" prefix —
    no longer dev-only).
  - Keep the call from the vinext:config plugin's config() hook,
    gated on command === "serve" && !isPreview (covers vinext dev /
    vp dev / vite dev / library embedders).
  - Add a parallel call at the top of startProdServer() in
    prod-server.ts (covers vinext start).

vinext is more conservative than Next.js's log-only handler — we
filter strictly on peer-disconnect codes and sync re-throw the rest,
so genuine bugs still surface. The parity is in *where* we install,
not what we swallow.

Vitest workers and `vinext build` never reach either entry point, so
peer-disconnect errors in those contexts surface normally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dev): auto-install socket-error backstop at module load

The Vite config() hook gate (`command === "serve" && !isPreview`)
proved unreliable in vite-plus's lifecycle — the hook didn't fire
(or didn't pass command correctly), and install was silently skipped.
Field reproducer: published 501dd95 had the call site in the bundle
at the right spot, but `VINEXT_DEBUG_SOCKET_ERRORS=1` produced no
startup marker, immediate ECONNRESET crash on first request.

Move the install back to module load — invoked unconditionally as a
side effect when socket-error-backstop.ts is imported. This was the
last shape verified to work via the diagnostic flag.

Drop the import-and-call pattern in index.ts in favor of a
side-effect import (`import "./server/socket-error-backstop.js"`),
which is enough to trigger the auto-install. The import order keeps
the install ahead of every other vinext server code path.

Skip in Vitest workers via `process.env.VITEST === "true"` so
genuine peer-disconnect errors during test runs surface normally.
Build runs are unaffected (short-lived, no peer-disconnect-prone
streams) — matches Next.js's pattern of installing in any
HTTP-serving entry without further gating.

prod-server.ts still calls installSocketErrorBackstop() explicitly
for `vinext start`. Idempotent via Symbol.for guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: call installSocketErrorBackstop directly in index.ts

Remove the side-effect import + auto-install indirection. Just call
the function explicitly at module top-level in index.ts, where it's
obvious. Same observable behavior, clearer code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: gate socket backstop on NODE_ENV to keep prerender ECONNRESET fatal

Bonk's PR #916 review caught a real corruption bug: the build path
calls startProdServer() during prerender (build/run-prerender.ts:181,
build/prerender.ts:398, :720), so the install fires twice during a
build — once at index.ts module load, once inside startProdServer.
User fetch() calls inside prerendered pages can hit ECONNRESET from
flaky upstream APIs. With the backstop installed, those errors are
silently absorbed instead of crashing the build, producing corrupt
prerendered HTML/RSC output.

Add NODE_ENV gating inside installSocketErrorBackstop:
  - Skip if NODE_ENV === "production"  → covers vinext build + prerender
  - Skip if NODE_ENV === "test"        → covers test runners that follow convention
  - Skip if VITEST === "true"          → kept for Vitest specifically

Vite sets NODE_ENV=production for the build command before plugins
load, so the gate fires correctly. Trade-off: vinext start with
NODE_ENV=production set in shell will also skip install — losing
strict Next.js parity for that path. Acceptable: prod-server's
pipeline() callbacks already handle the streaming case, and the
real-world bug reports are all dev-server.

Also fix the now-stale comment at prod-server.ts:817 that still
referenced the reverted "Vite plugin config() hook" install path,
and document the listener-ordering trade-off more honestly in the
JSDoc — `index.ts` imports synchronously at the top of every user's
vite.config.ts, registering vinext's listener earlier than user /
tooling crash reporters. Sync re-throw on non-peer-disconnect errors
still surfaces the crash but later-registered observers don't see
the event; users who need crash-reporter visibility for those errors
must register their handler before importing vinext.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: bypass socket backstop during prerender via VINEXT_PRERENDER

Replace the install-time NODE_ENV gate (which broke vinext start
parity by skipping installation entirely when NODE_ENV=production)
with a fire-time VINEXT_PRERENDER check inside the listener. The
listener is always installed, but during prerender it re-throws all
errors unconditionally — acting as if no listener were present —
so user fetch() ECONNRESETs during prerender crash the build with
the original stack instead of being silently absorbed into corrupt
prerendered output.

The fire-time check (vs. install-time) is necessary because
index.ts loads at Vite plugin import — well before prerender begins
— and the Symbol.for guard then makes any later install call a
no-op. A static install-time gate can't catch the prerender phase
that follows.

Set VINEXT_PRERENDER=1 in run-prerender.ts at the top of
runPrerender() so the flag covers the entire prerender orchestration
including startProdServer setup. prerender.ts already sets the same
var around its actual render passes; this widens the scope.

Restores Next.js-parity install for vinext start (NODE_ENV gate is
gone). Test-runner skip stays install-time on VITEST / NODE_ENV=test
since those contexts genuinely shouldn't have the listener.

Refs PR #916 review feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address bonk PR #916 follow-up review

Three issues bonk flagged after the prerender fix landed:

1. Stale comment in prod-server.ts:816-820 — described the NODE_ENV
   self-gate that was reverted in b3e8759. Replace with one that
   describes the current behavior: idempotent install via Symbol.for
   guard, kept here for entry points that load prod-server without
   index.ts (Next.js parity), prerender bypass is fire-time via
   VINEXT_PRERENDER not install-time.

2. prerender.ts's finally blocks `delete VINEXT_PRERENDER` clobbers
   any value set by the caller. After prerenderApp returns inside
   runPrerender (which sets the flag for the whole orchestration),
   the var is deleted instead of restored to runPrerender's "1",
   leaving narrow gap windows around shared-server close where
   ECONNRESET would be absorbed instead of re-thrown. Save the
   prior value and restore it. Doesn't cause prerender output
   corruption (rendering already complete by these gaps) but it's
   the correct hygiene.

3. Document the orchestrator-induced ECONNRESET trade-off in the
   backstop's JSDoc — unconditional re-throw during prerender
   means a hung-route + orchestrator timeout surfaces the build
   crash as ECONNRESET rather than the route's own error.
   Acceptable but worth calling out so debuggers know to set
   VINEXT_DEBUG_SOCKET_ERRORS=1 to disambiguate.

Plus minor JSDoc wording nit: the "Symbol.for guard prevents
re-install" line was load-bearing in the wrong way — the relevant
point is timing, not de-dup. Reworded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address bonk PR #916 follow-up review (round 2)

- run-prerender.ts: save/restore VINEXT_PRERENDER to avoid leaking the
  env var into Vitest workers (mirrors prerender.ts pattern).
- index.ts: correct stale install-site comment — install gate is
  Vitest-only; build/prerender bypass is fire-time via VINEXT_PRERENDER.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: focused unit test for socket-error-backstop predicate + install gate

Addresses bonk PR #916 review item #3 (carried over). Extracts
'peerDisconnectCode' as an exported pure predicate so the matching
logic can be tested in isolation without process-state mutation, and
adds a thin 'isSocketErrorBackstopInstalled()' query so the test can
verify the Vitest install-gate short-circuit fires in worker processes.

Tests cover:
- ECONNRESET / EPIPE / ECONNABORTED accepted, other codes rejected
- non-Error / null / primitive reasons handled (unhandledRejection
  fires with arbitrary reason values)
- Install gate skips Vitest workers (process.env.VITEST === 'true')

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
2026-04-27 07:55:05 +01:00
Nathan Nguyen 696948aca4 fix(next/font/google): use real axis range, validate options at build time (#885) (#893)
* shim: rewrite buildGoogleFontsUrl on top of the new build-url helper

Replace the inline URL builder in shims/font-google-base.ts with a thin
wrapper around the pure buildGoogleFontsUrl helper from PR 1
(packages/vinext/src/build/google-fonts/build-url.ts). The shim still
takes a FontOptions object and still returns a CSS URL, so external
callers and the existing 84 unit tests are unaffected.

Two visible behaviour changes:

1. Empty options no longer emit `:wght@100..900`. They emit no axis
   segment at all, which makes Google return the default static face
   (HTTP 200) for every font, including narrow-axis families like Sen
   that previously got HTTP 400. This is the dev fallback only; the
   build plugin always pre-resolves the real axis range from metadata
   in production.

2. Italic-only requests no longer drop the ital axis. Pre-fix the outer
   guard `weights.length > 0 || styles.length > 0` entered the block
   but the inner branch only handled the wght path, so a call like
   `Inter({ style: ['italic'] })` produced `family=Inter&display=swap`
   with no italic anywhere in the URL. The shim now derives an ital
   array from style and falls back to weight '400' when ital is set
   without an explicit weight, so Google has somewhere to attach the
   ital axis in the URL.

The shim deliberately does not import font-data.json or validate.ts: a
388 KB metadata file in the Worker bundle would dwarf the rest of the
shim, and the production path already has the metadata-aware variant.

Refs #885.

* plugin: validate options against metadata, surface HTTP errors as build errors

Replace the inline URL builder in plugins/fonts.ts with the pipeline added
in PR 1: validateGoogleFontOptions parses the call's options against the
bundled font metadata, getFontAxes resolves the actual `wght` range for
variable fonts, and buildGoogleFontsUrl emits the URL.

Three behaviour changes:

1. Variable fonts called with no `weight` now request the font's real
   axis range (Sen 400..800, Inter 100..900) instead of a hardcoded
   100..900. This is the user-visible fix for issue #885: Sen returns
   HTTP 200 with all five face files instead of HTTP 400.

2. Validation errors surface as build errors with the source file path
   attached. Unknown family, missing weight on a static font (Anton,
   Archivo Black, etc.), unknown weight, mixing 'variable' with explicit
   weights, axes on non-variable fonts, invalid display values, and
   missing/unknown subsets are now all caught at transform time. Pre-fix
   these silently produced URLs that Google rejected at request time.

3. HTTP non-2xx responses from Google are now distinguished from network
   errors. A 4xx/5xx is wrapped in GoogleFontsHttpError and surfaced
   with the failing URL plus the response body, because pre-fix the
   silent `catch { return; }` skipped self-hosting and let the runtime
   CDN path retry the same broken URL in the browser. Network errors
   (offline dev) still fall through silently to keep `vp dev` usable
   without a connection.

The fontCache key is the URL, so existing on-disk caches under
`.vinext/fonts/<hash>/` will be stale for affected fonts after this
change. One refetch per font on the next build.

Refs #885.

* tests: regression coverage for the #885 narrow-axis bug and validator throws

Five new tests in tests/font-google.test.ts plus subset additions to
existing fixtures whose Inter() / Roboto() calls were missing them.

Shim regressions:
- buildGoogleFontsUrl with empty options must not emit `:wght@100..900`
- buildGoogleFontsUrl with style-only options must keep the ital axis
  visible in the URL

Plugin regressions:
- Sen() with no weight must fetch `:wght@400..800`, not `:wght@100..900`
- unknown families throw a build error with the file path attached
- non-variable fonts called without an explicit weight throw at build
  time instead of silently emitting a broken URL
- HTTP 4xx/5xx from Google surfaces as a build error containing the URL
  and the response body
- network errors (DNS, ECONNREFUSED, AbortError) still fall through
  silently so offline dev keeps working

The existing plugin transform tests passed `Inter({ weight: '400' })`
without subsets; the validator now treats that as a missing-subsets
error (matches Next.js behaviour), so those fixtures gain
`subsets: ['latin']`. The two nested-brace regression tests still
exercise the parser's balanced-brace logic but use a benign
`_placeholder` field instead of `axes: { wght: 400 }`, which is
semantically invalid for Inter under the new validator.

Refs #885.

* address google font review feedback
2026-04-27 07:50:39 +01:00