Commit Graph

12 Commits

Author SHA1 Message Date
Nathan Nguyen cda19e1c58 fix(app-router): preserve recent segment state with Activity BFCache (#1739)
* fix(app-router): preserve recent segment state with Activity BFCache

Fresh App Router navigations were reusing the active React tree when moving between dynamic segment values. That leaked client state into a newly entered segment and made the upstream back-forward-cache suite fail before it even reached browser traversal assertions.

The root cause was that vinext had history-entry bfcacheId plumbing but no separate segment state-key cache. Next.js treats those identities separately: render state is keyed by segment identity/pathname, while useRouter().bfcacheId identifies a concrete history entry.

Add state-key maps for app router slots, render retained entries through React.Activity when cacheComponents is enabled, keep the active one-entry keyed reset path otherwise, and port focused Next.js regression coverage for the three-entry BFCache behavior.

* fix(bfcache): store stateKeyMap in retained slot entries and re-provide on render

Review feedback requested two changes:

1. Add stateKeyMap to BfcacheSlotEntry so each retained BFCache entry carries
the state-key map that belonged to it when active, rather than borrowing the
current navigation's map. This prevents nested hidden entries from being rendered
with old elements but new pathname-derived state keys — an identity corruption
bug that upstream Next.js avoids by rendering each retained entry from its own
state.

2. Add a nested dynamic regression test: /nested/[section]/item/[id] with client
counters in both the [section] layout and [id] page. The existing flat /page/[n]
tests do not exercise nested retained-entry identity, which is exactly where the
missing state-key-map bug lives.

Changes:
- BfcacheSlotEntry gains stateKeyMap?: Readonly<Record<string, string>>
- updateBfcacheSlotEntries accepts and stores an optional stateKeyMap argument
- BfcacheSlotBoundary stores the current stateKeyMap alongside elements when
caching the active entry
- Both the Activity visible/hidden path and the non-cacheComponents single-entry
path now wrap rendered entries in BfcacheStateKeyMapContext.Provider using the
entry's own stateKeyMap (falling back to current map for forward compatibility)
- New e2e test verifies nested layout + page counter preservation across
segment navigations

* refactor(bfcache): extract shared provider stack into BfcacheEntryProviders

BfcacheSlotBoundary rendered the BfcacheStateKeyMapContext / ElementsContext /
SegmentContext provider stack in two places: the one-entry keyed reset path and
the per-entry Activity retention list. The two stacks were identical except for
where the React key sat, so the retained-entry bookkeeping and its render shape
were tangled together and harder to audit than needed.

Extract BfcacheEntryProviders as a pure presentational helper and render it from
both paths. The keyed-reset path keeps its remount semantics: the key now sits on
the BfcacheEntryProviders element, so a fresh active stateKey still remounts the
whole provider subtree.

Hook ordering is unchanged. All hooks stay above the early returns in
BfcacheSlotBoundary, so the boundary still satisfies the Rules of Hooks across the
activeStateKey undefined-to-defined transition. Covered by the slot BFCache
entry-state suite in tests/shims.test.ts.

* test(bfcache): isolate cacheComponents BFCache coverage in a dedicated fixture

The back/forward-cache e2e suite ran against the shared app-basic fixture with
cacheComponents: true. app-basic backs the single app-router Playwright server
that every app-router spec uses, so enabling cacheComponents globally changed a
core DOM invariant for unrelated tests: inactive route trees stay mounted as
hidden Activity DOM. The contamination already leaked a :visible workaround into
the interception-modal bfcacheId spec, which had nothing to do with Activity.

cacheComponents is an opt-in semantic mode, not default app-router behaviour, so
it does not belong on the shared baseline fixture. Move the BFCache routes into a
dedicated tests/fixtures/app-bfcache fixture with cacheComponents: true, served by
a new app-router-bfcache Playwright project on port 4183, and run the
back-forward-cache spec there against the project baseURL. The :visible locators
stay in that spec, where duplicate hidden DOM is the expected contract.

Remove cacheComponents and the BFCache routes from app-basic so the shared fixture
models default behaviour again, and revert the :visible workaround in
use-router-bfcache-id.spec.ts now that the modal route no longer retains hidden
DOM. Fix the nested item page import while moving it: nested/[section]/item/[id]/
page.tsx imported ../counter, but the counter lives two levels up at
nested/[section]/counter.tsx, so it must be ../../counter.

Validation: PLAYWRIGHT_PROJECT=app-router-bfcache (5 passed) and the full
use-router-bfcache-id spec under app-router (11 passed), plus the slot BFCache
unit suite (1172 passed) and vp check (formatting + lint + types clean).

* ci: run the app-router-bfcache e2e project in the matrix

The dedicated app-router-bfcache Playwright project was added to
playwright.config.ts but not to the CI E2E matrix, so the back/forward-cache
specs never ran in CI. Add it to the matrix project list so the BFCache suite
runs on every push. It uses the default chromium browser, so the existing setup
and browser-cache steps apply unchanged.

* fix(bfcache): keep persisted slots mounted when cacheComponents is off

Without cacheComponents there is no Activity retention, yet BfcacheSlotBoundary
still keyed the active entry by its segment stateKey. That stateKey tracks the
pathname (createBfcacheSegmentIdentity), so any slot whose identity moves with
the URL (shared layouts, interception source slots) was remounted on navigation,
discarding client state that survives a normal route change. The baseline router
reconciles these slots in place.

This regressed seven existing app-router e2e tests once cacheComponents was no
longer masking it on the shared fixture: interception source-context and modal
preservation in advanced.spec.ts, and layout-persistence.spec.ts dynamic-segment
layout counter survival. Fresh-entry reset is driven by userland bfcacheId keying
in the fixtures, not by remounting the slot subtree, so the keying was pure
overreach in the non-cacheComponents path.

Render the single active entry unkeyed when cacheComponents is off so the boundary
reconciles in place like the baseline. The keyed Activity retention path is
unchanged for cacheComponents.

Validation: full app-router project 436 passed / 0 failed (the seven regressions
now pass); use-router-bfcache-id 11 passed (fresh-push reset intact); app-bfcache
5 passed (Activity retention intact); shims unit suite 1024 passed.

* docs(test): point bfcacheId spec comment at the app-router-bfcache project

The header noted that Activity-backed preservation is covered in
back-forward-cache.spec.ts, but that spec moved to the dedicated app-bfcache
fixture under the app-router-bfcache project. Name the project and path so the
cross-reference stays accurate.

* docs: align cacheComponents comment with unkeyed active-segment behavior

* test(bfcache): assert fresh bfcacheId after Activity-restored return

* style: collapse locator chains per oxfmt

* refactor(bfcache): split slot entry order from snapshots

* fix(bfcache): keep non-cacheComponents slot path baseline

* fix(bfcache): use string cacheComponents define

* test: remove external fetch from revalidate tag fixture

* fix(bfcache): harden Activity state key retention

* docs(bfcache): document cacheComponents Activity caveats

* fix(bfcache): preserve navigation context for fallback SSR

Fallback boundary rendering can outlive the original request-scoped navigation context. That made app SSR fail fast with a missing navigation context when loading-boundary digest routes rendered their HTTP fallback shell.

The SSR boundary contract still requires a navigation context for real request renders, but boundary renderers now synthesize the same typed shape from the request URL and matched params when request scope has already been cleared.

* refactor(bfcache): normalize pathname in state-key map, document invariant callers

Moves hydration-pathname normalization into createBfcacheSegmentStateKeyMap so SSR and client always produce byte-identical Activity keys. Documents the requireNavigationContext guaranteeing callers per review feedback.

* fix(bfcache): tolerate malformed state-key pathnames

* docs(bfcache): document known Next.js divergence and dead-branch contract

* chore(deps): update lockfile for vite 0.1.24

* fix(app-router): preserve encoded delimiters in bfcache keys

* docs(bfcache): track router-owned retention follow-up

* chore: retrigger ci

* fix(app-router): preserve redirect bridge fallback
2026-06-07 23:07:25 +01:00
Stephen Zhou 9126344cd8 fix: use loader for client reference (#1141)
* fix: split client chunk

* add test

* fix ci deps

* install deps

* use loader

* tweaks
2026-05-14 13:18:58 +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
Stephen Zhou 207588d164 feat: add instrumentation-client support (#679)
* feat: add instrumentation-client support

* trigger ci

* fix deps

* tweaks

* tweaks

* tweaks

* tweaks

* rebuild lockfile

* fix: add app-with-src to CI E2E matrix

---------

Co-authored-by: James <james@eli.cx>
2026-03-28 21:21:36 +00:00
Raed Alharbi a515f07a49 test: add static export E2E tests (#564) (#686)
* test: add static export E2E tests (#564)

Add Playwright E2E tests for `output: "export"` static builds covering
both App Router and Pages Router. Includes a lightweight static file
server with path traversal protection, proper error handling, and
startup validation.

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

* Update tests/e2e/static-export/serve-static.mjs

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-03-28 09:06:06 +00:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* chore: migrate to vite plus

* Disable typeAware and typeCheck

* Update CI

* Fix CI

* Fix test

* Clean

* Run test with vp

* Try revert

* react: false In test

* Fix test

* Revert "Try revert"

This reverts commit 009da10473.

* Update

* Update

* Try revert ci changes

* revert

* Run vp migrate

* Disable typeAware and typeCheck for now

* Better resolve for test

* Use vp dev instead of vite

* Update expect

* Fix NormalizeManifestModuleId

* Try increase timeout

* Update to use vp

* Try new check

* Bring back npx vp

* Migrate CI

* Make next-intl resolvable

* Update

* Update

* Update
2026-03-15 10:50:13 +00:00
James Anderson 764a496ce7 add oxfmt formatter (#380)
* add oxfmt formatter: config, scripts, CI, editor setup, docs

* rebuild lockfile

* fix: add Format to required checks list, remove dead ignore pattern

* run fmt

* add format to agents.md again
2026-03-09 14:56:14 +00:00
James Anderson 7fdcdd3c69 feat(og): add @next/og / @vercel/og support with Cloudflare Workers compatibility (#356)
* feat(og): add @next/og support with Cloudflare Workers compatibility

- Add vinext:og-font-fix transform plugin that patches @vercel/og/dist/index.edge.js
  at Vite transform time: inlines the fallback font as base64 (avoids fetch() with
  import.meta.url which breaks in workerd) and defers WASM init to first use (avoids
  WebAssembly.instantiate() being blocked in Node.js module runner)
- Add @vercel/og to optimizeDeps.exclude so Vite's esbuild pre-bundler doesn't cache
  the module before the transform hook runs
- Add vinext:og-assets build plugin that copies font/WASM assets to RSC output dir
- Add /app/api/og route to app-router-cloudflare example and app-basic fixture
- Add shared Playwright e2e spec (tests/e2e/og-image.spec.ts) asserting PNG validity,
  correct 1200x630 dimensions, param-driven content, and determinism
- Register og-image.spec.ts in app-router, cloudflare-workers, and cloudflare-dev
  Playwright projects (18 tests total, all passing)

* refactor(og): generalize fetch+import.meta.url asset inlining, drop WASM lazy-init patch

Replace the @vercel/og-specific vinext:og-font-fix plugin with a general
vinext:og-inline-fetch-assets plugin that:

- Matches ANY fetch(new URL('./asset', import.meta.url)).then(res => res.arrayBuffer())
  pattern, not just the hardcoded noto-sans filename
- Resolves each asset relative to the module's on-disk path (via the transform id)
  and inlines it as base64 — works for any font version or additional assets
- Applies to any library using this pattern, not just @vercel/og

Also removes the WASM lazy-init patch: now that @vercel/og is in optimizeDeps.exclude,
the module runs through workerd (not the Node.js pre-bundler cache) where
WebAssembly.instantiate() works fine. The lazy-init workaround was only needed
when the pre-bundler cache bypassed the transform hook.

The og-assets build plugin is updated to only copy resvg.wasm (not the font,
which is now inlined and will no longer appear in the bundle by filename).

* fix(og): inline readFileSync assets from @vercel/og node build

The production RSC bundle resolves @vercel/og under the 'import' export
condition, getting dist/index.node.js instead of dist/index.edge.js.
The node build loads font and WASM via fs.readFileSync(fileURLToPath(
new URL('./file', import.meta.url))), which breaks after bundling because
import.meta.url points to the bundle output file, not the dist/ dir.

Extend vinext:og-inline-fetch-assets with a second regex pattern that
matches readFileSync(fileURLToPath(new URL(...))) and replaces it with
Buffer.from('<base64>', 'base64'), inlining both noto-sans font and
resvg.wasm at transform time. This eliminates the ENOENT errors seen
in the production build CI tests.

* refactor(og): address code review feedback

- Remove Fix 1 (font inlining) from vinext:og-font-patch — it was dead
  code since vinext:og-inline-fetch-assets (earlier in the plugin array)
  already handles the same fetch(new URL(...)) pattern generically. Only
  Fix 2 (yoga WASM extraction) remains in og-font-patch.

- Use split().join() instead of String.replace() in both Pattern 1 and
  Pattern 2 loops of og-inline-fetch-assets. String.replace(string, ...)
  only replaces the first occurrence; split().join() replaces all of them,
  which matters for libraries that may use the same pattern multiple times.

* refactor(og): use replaceAll instead of split().join()
2026-03-08 20:50:10 +00:00
James Anderson 4e0294fc4f fix: middleware crash in dev mode with cloudflare plugin (#344)
* test: middleware with cloudflare plugin and routing bypass

* fix: use module runner for pages middleware

* add pages router dev test
2026-03-08 14:04:11 +00:00
James Anderson cb2bbc65b6 test: app router instrumentation with cloudflare plugin (#333) 2026-03-07 23:04:41 +00:00
James Anderson 80acf27933 fix: register instrumentation in rsc entry and put error handler in global scope (#330)
* test: instrumentation

* store onRequestError on globalThis

* add instrumentation registration app-dev-server

* guard against double calling runinstrumentation for appdir
2026-03-07 22:40:27 +00:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00