mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
@vinext/cloudflare@0.1.0
77 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6f689c7b14 | chore: rebuild lockfile (#1815) | ||
|
|
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 |
||
|
|
9a721ca287 |
chore(deps): bump knip and node types (#1806)
* chore(deps): bump knip and node types Bump Knip to 6.16.0 and @types/node to 25.9.2. Knip 6.16 reports previously unreported unused exported types, stale ignore entries, and external CLI binaries. Remove unnecessary type exports, refine the script entry coverage, and keep only narrow binary ignores for system or user-project commands. Validation: vp run knip; vp run check. * chore: retrigger ci |
||
|
|
f9d83fd8f2 |
fix(build): bundle @vinext/cloudflare into vinext to break dependency cycle (#1797)
* fix(build): bundle @vinext/cloudflare into vinext to break dependency cycle vinext consumed a few runtime helpers (KVCacheHandler, CloudflareCdnCacheAdapter, ENTRY_PREFIX) from @vinext/cloudflare via a runtime `dependencies` edge. Combined with @vinext/cloudflare's `peerDependencies` on vinext, this formed a cycle that forced changesets to force-major @vinext/cloudflare on every vinext release (mitigated defensively in #1764). Bundle the small amount of @vinext/cloudflare code vinext actually imports into vinext's dist via tsdown's deps.neverBundle, and move @vinext/cloudflare to devDependencies. The published install graph now points one way (@vinext/cloudflare -> vinext). @vinext/cloudflare remains a published package: user vite.config files still import cdnAdapter()/kvDataAdapter(), and the generated worker resolves its *.runtime.js factories by absolute path. Its bundled-in vinext/shims/* imports stay external and resolve via vinext's own package exports (Node self-reference). Drop the now-obsolete create-next-app CI steps that packed and overrode @vinext/cloudflare (vinext's tarball no longer declares it as a dependency). * refactor(build): clarify @vinext/cloudflare bundling config Use a single intent-revealing neverBundle predicate with a BUNDLED_DEPS carve-out and document why the carve-out must live in neverBundle (tsdown rejects skipNodeModulesBundle + alwaysBundle as mutually exclusive, and a catch-all neverBundle takes precedence over alwaysBundle). * fix(build): bundle @vinext/cloudflare via source alias, not external predicate The previous approach replaced deps.skipNodeModulesBundle with a custom neverBundle predicate. That broke tsdown's rewriting of vinext's own `vinext/shims/*` tsconfig-path self-imports to relative paths, leaving them as bare `vinext/*` specifiers in 46 files. Across Vite's separate RSC/SSR/client dev module graphs those bare self-references resolved to distinct module instances, breaking identity checks (e.g. `instanceof ReadonlyURLSearchParams`). Keep skipNodeModulesBundle: true untouched and instead bundle @vinext/cloudflare by aliasing its cache/* subpath to source. skipNodeModulesBundle externalizes bare package specifiers before tsconfig paths apply, so the alias rewrites the import to a file path up front; tsdown then treats it as local source and bundles it. The bundled code's own vinext/shims/* imports still resolve to vinext's relative output (single module instance). Output is byte-identical to the pre-change build except the added bundled cloudflare files and one cosmetic import reorder. |
||
|
|
c8975a48cf |
chore(deps): bump playwright to 1.60.0 (#1796)
* chore(deps): bump playwright to 1.60.0 * u |
||
|
|
fb9151ed9d |
chore(deps): bump vite-plus toolchain to 0.1.24 (#1767)
* chore(deps): bump vite-plus toolchain to 0.1.24 * bump setup-vp * . --------- Co-authored-by: James <james@eli.cx> |
||
|
|
8d00797ada |
feat(cache): extract Cloudflare cache adapters into @vinext/cloudflare (#1748)
* refactor(cache): extract Cloudflare cache adapters into @vinext/cloudflare Move the Cloudflare KV data cache and edge CDN cache adapters out of vinext into a new publishable @vinext/cloudflare package: - cache/kv-data-adapter(.runtime).ts (KVCacheHandler, kvDataAdapter) - cache/cdn-adapter(.runtime).ts (CloudflareCdnCacheAdapter, cdnAdapter) tpr.ts stays in vinext. vinext now depends on @vinext/cloudflare (workspace:*) and the package declares vinext as a peer dep; both build from source via tsconfig paths so there is no build-order cycle. The vinext/cloudflare barrel still re-exports KVCacheHandler for back-compat. Wires up tsconfig paths, a vitest source alias, root build/postinstall, and the preview/publish workflows for the new package. Updates internal consumers (apps/web, examples/workers-cache), docs, and tests. * ci(create-next-app): install @vinext/cloudflare from local tarball vinext now depends on @vinext/cloudflare, which isn't published to npm yet. The create-next-app smoke test packs vinext locally and resolves its deps from the registry, so the install (and dev server) failed with ERR_PNPM_FETCH_404 for @vinext/cloudflare. Pack @vinext/cloudflare alongside vinext and add a pnpm override in the scaffolded project pointing at the local tarball so the dependency resolves offline. * refactor(cloudflare): address review feedback - Remove the root barrel export from @vinext/cloudflare; expose only the ./cache/* subpaths via a wildcard export (no root main/types). - vinext/cloudflare re-exports KVCacheHandler from the full subpath. - Drop the redundant .npmignore (the package.json "files" allowlist already restricts the publish to dist). - Remove the unsupported imperative setCacheHandler/KVCacheHandler usage from both READMEs; the cache plugin config is the supported approach. - Simplify test wiring: drop the now-unused @vinext/cloudflare tsconfig path and dedupe the vitest source alias into a shared constant. * chore(cloudflare): drop unused vite devDependency The @vinext/cloudflare config uses vite-plus and nothing imports vite, so the vite devDependency was unused. build/check/knip stay green without it. * Apply suggestion from @james-elicx |
||
|
|
61d5fe00a6 |
feat(release): commit-driven auto-generated changesets (#1753)
* feat(release): commit-driven changesets with auto-generated changesets
Adopt stock Changesets for versioning/changelog/publishing, with the only
bespoke surface being a script that creates changesets automatically from
Conventional Commits. Auto-generated changeset files never live on `main` —
they are written to the CI working tree, consumed by changesets/action into a
rolling Version PR, and discarded. Manually authored changesets still work.
- scripts/create-changeset.mjs: derive per-package bumps from Conventional
Commits (paths -> package, type -> bump). Includes the version-vs-tag guard
so a merged Version PR publishes instead of re-opening a PR.
- scripts/version.mjs: `changeset version` + append a `## Contributors` list to
each bumped package's CHANGELOG.md (idempotent, pure rewrite unit-tested).
- .github/workflows/release-pr.yml: version-only changesets/action (no publish).
- .github/workflows/publish.yml: guarded OIDC publish (preserves
`vp pm publish --provenance`); version now comes from `changeset version`.
- Reconcile packages/vinext version (0.0.5 -> 0.0.55) so the guard is coherent.
Unit tests for both scripts pass (35). The release orchestration (Version PR
creation, OIDC publish, gh contributor resolution) can only be validated in a
live CI run.
* refactor(release): collapse to a single changesets workflow
Let changesets/action own as much as possible. Delete the separate publish.yml
(guard job, manual bump, manual OIDC/tag/release/notify steps) and the
release-pr.yml split. One workflow now:
- create-changeset.mjs writes auto changesets to the working tree (its
version-vs-tag guard yields nothing right after a Version PR merges).
- changesets/action maintains the Version PR and, when no changesets remain,
publishes via `changeset publish` with OIDC trusted publishing + provenance,
and creates the git tag + GitHub Release.
Removes the unused release:version script. Contributors list still handled by
scripts/version.mjs as the action's version command.
* refactor(release): convert release scripts to .mts, drop .d.mts files
Replace the .mjs + hand-written .d.mts declaration pairs with real TypeScript
(.mts) source. Node >=24 (the setup default) runs .mts directly via native type
stripping; .mts is unambiguously ESM so it needs no "type": "module" and emits
no MODULE_TYPELESS warning. The cross-import uses an explicit .mts specifier
(Node requires it), permitted in tsc via allowImportingTsExtensions (safe: the
project is noEmit).
- scripts/create-changeset.{mjs,d.mts} -> scripts/create-changeset.mts
- scripts/version.{mjs,d.mts} -> scripts/version.mts
- tsconfig: allowImportingTsExtensions
- release.yml: run node scripts/*.mts
vp check (format + lint + types) clean; 35/35 unit tests pass.
* refactor(release): trim release scripts (~690→~470 source lines)
Cut comment bloat, remove dead code, tighten without dropping behavior:
- Condense verbose JSDoc/@param blocks to one-line purpose comments; keep the
load-bearing "why" (correctness-rule header, insertContributors idempotency).
- Delete unused `newestChangelogVersion` (+ its tests) — exported/tested but
never called.
- Tighten run()/insertContributors/latestTagVersion without behavior change.
Multi-package machinery and the bottom `## Contributors` list are retained per
requirements. vp check clean; 33 unit tests pass.
* feat(release): grouped conventional changelog + filter bot contributors
The default changesets changelog groups by bump level (### Minor Changes) and
renders our changeset summary as a nested bullet dump. Replace it with a real
conventional-commits changelog: version.mts now regroups each release's commits
into ### Features / ### Bug Fixes / ### Performance sections (scope bolded, type
prefix dropped) and rewrites the newest CHANGELOG section, then appends the
## Contributors list with [bot] accounts filtered out.
- create-changeset.mts: extract conventionalParts + collectReleaseCommits +
releaseRangeStart and export the commit-walk so version.mts reuses it (no
duplicate git logic).
- version.mts: groupedChangelogBody + rewriteReleaseSection (idempotent: only
`## <digit>` is a section boundary); dedupeSortLogins drops `[bot]` logins.
- Tests updated for the new pure builders.
vp check clean; 33 unit tests pass. Verified end-to-end with a local dry run.
* refactor(release): simplify per PR review
- discoverPublishablePackages: scan only packages/* (publishable packages don't
live elsewhere), drop the apps/examples/benchmarks scan.
- Delete isReleaseCommit: the "chore: version packages" release commit is a
`chore`, already excluded by parseBumpFromSubject returning null. No need to
hardcode release-message formats.
vp check clean; 31 unit tests pass.
* docs(release): clarify why the version-vs-tag guard exists
Per PR review: the guard was confusing. Document inline that it only suppresses
the post-merge/pre-publish window (version bumped, tag not yet created), which
would otherwise re-open a Version PR instead of publishing. Keeping the
never-on-main design per review decision.
* feat(release): sub-group changelog areas and humanize area names
Within each type section (### Features / ### Bug Fixes / …), areas (commit
scopes) with more than 3 items now get their own `#### <Area>` sub-group with
the scope prefix dropped; smaller areas and scopeless commits fall under
`#### Other`. When no area qualifies the list stays flat. Area names are
humanized (app-router → "App Router", css → "CSS", ppr → "PPR", i18n → "i18n")
in headings and in the bold prefixes.
Also: groupedChangelogBody now renders only known release types (GROUPS) — it
no longer leaks non-release types into an "Other Changes" bucket.
vp check clean; 32 unit tests pass. Verified with a local dry run.
* feat(release): rename changelog Other sub-group to Misc
* feat(release): sub-group areas with 3+ items (was >3)
* fix(release): address PR review (stale refs, contributor API, doc)
- Fix stale references to deleted files: .mjs → .mts and release-pr.yml →
release.yml in .changeset/README.md and the release.yml header comment.
- resolveContributors: one paginated `gh api compare` call instead of one
`gh api commits/<sha>` per commit (N round-trips → 1). This also removes
version.mts's local git() helper, so no more `fatal:` probe noise from it.
No behavior change to the changelog/contributor output (verified via dry run:
same 4 contributors resolved). vp check clean; 32 unit tests pass.
The bot's "no git tags" bootstrap concern does not apply: tags v0.0.10..v0.0.55
exist and CI checks out with fetch-depth: 0, so the guard resolves 0.0.55.
* fix(release): run pinned changeset CLI; drop dead affectedPackages branch
Per second bonk review:
- Invoke the pinned, installed @changesets/cli via `vp exec changeset` instead
of `vp dlx @changesets/cli` (which fetches a floating latest at run time) — in
release.yml publish and version.mts. Removes the reproducibility gap vs the
lockfile-pinned 2.31.0.
- affectedPackages: remove the unreachable `dir === "."` / empty-prefix branch
now that discovery only returns `packages/*` keys.
vp check clean; 32 unit tests pass.
* fix(release): only emit valid GitHub logins in Contributors; doc fix
Per third bonk review:
- resolveContributors: `.author.login // empty` (was `// .commit.author.name`),
so commits with an unlinked email contribute no entry instead of a raw git
display name. dedupeSortLogins now keeps only `[a-zA-Z0-9-]+` shapes, which
also subsumes the previous `[bot]` filter — no more broken `- @Full Name`
mentions.
- .changeset/README.md: suggest `vp exec changeset` instead of the floating
`vp dlx @changesets/cli` for manual changeset authoring.
vp check clean; 32 unit tests pass; dry run resolves the same valid handles.
|
||
|
|
48e932e615 |
feat(cache): split CDN and data cache adapters; add Cloudflare edge adapter (#1693)
* feat(cache): split CDN and data cache adapters; add Cloudflare edge adapter Separate two caching concerns behind distinct adapters: - Data cache handler (existing CacheHandler): fetch, "use cache", unstable_cache. Canonical get/setDataCacheHandler; get/setCacheHandler kept as deprecated aliases. - CDN cache adapter (new): page-level ISR serving strategy — readPage/writePage, buildResponseHeaders (header map), ownsBackgroundRevalidation, revalidate. DefaultCdnCacheAdapter delegates storage to the data cache, so default behavior is unchanged. Page/route ISR now routes through the CDN adapter (isr-cache, app-page-cache); revalidateTag/revalidatePath/updateTag invalidate the data cache and purge the CDN adapter (default no-op). Add CloudflareCdnCacheAdapter (edge-managed): readPage null / writePage no-op, ownsBackgroundRevalidation false, emits CDN-Cache-Control for SWR + Cache-Control: no-store (no browser storage) + Cache-Tag, and purges via the request-context cache. Auto-selected via a global detector when the request context exposes a cache handle; explicit setCdnCacheAdapter wins. The request-context type stays generic (cache: unknown). Adds next.config cdnCacheHandler (symmetric with cacheHandler) and updates the worker codegen to setDataCacheHandler. * address review: drop config plumbing; refine Cloudflare cache headers - Remove the cdnCacheHandler next.config plumbing entirely (deferred); next-config.ts is back to baseline. - CloudflareCdnCacheAdapter: emit the edge directive on CDN-Cache-Control as 'public, max-age=…, stale-while-revalidate=…' (max-age, not s-maxage, so the edge caches + SWRs), and set the browser-facing Cache-Control to 'public, max-age=0, must-revalidate' so a browser never serves a stored copy without revalidating against the edge. * chore: fix formatting (vp check) for cache adapter files * feat(cache): route App Router route handlers + Pages Router ISR through the CDN adapter Closes the two parity gaps from review: route-handler and Pages Router ISR responses now emit the CDN adapter's headers (CDN-Cache-Control + Cache-Tag on edge adapters) instead of a hardcoded Cache-Control. - Add shared applyCdnResponseHeaders() in cache-control.ts; app-page-cache now uses it (drops its local helper). - Route handlers: applyRouteHandlerRevalidateHeader (fresh) and buildRouteHandlerCachedResponse (HIT/STALE) go through the adapter; routeTags hoisted in execution so the fresh response carries Cache-Tag. - Pages Router: fresh ISR response emits a path-based Cache-Tag (matching revalidatePath); HIT/STALE served response routes through the adapter. - CloudflareCdnCacheAdapter: guard so non-cacheable policies (no-store/no-cache/private) are never promoted to CDN-Cache-Control. Default behavior unchanged (adapter yields a single identical Cache-Control). * address review: simplify applyCdnResponseHeaders + strip CDN headers from stored route values - applyCdnResponseHeaders now clears only Cache-Control (the header vinext stamps internally); the adapter's own headers are applied via set() which overrides, so pre-clearing adapter-specific headers was redundant and presumptuous (per review). - buildAppRouteCacheValue strips cdn-cache-control / cache-tag so CDN policy headers are never baked into a stored route value (re-derived from the adapter on every served response). - pages-page-data buildPagesCacheResponse now uses applyCdnResponseHeaders (Headers) for consistency with every other call site. * revert presumptuous CDN header strip in buildAppRouteCacheValue Hardcoding cdn-cache-control/cache-tag in the store denylist presumes a specific adapter's header names (the same presumption rejected for applyCdnResponseHeaders) and is also unnecessary: CDN policy headers never reach a stored route value — the edge adapter's writePage is a no-op and the default adapter never emits them. Back to the original denylist. * example(workers-cache): add demo app for route-cache testing (#1695) * example(workers-cache): add demo app for route-cache testing Adds the examples/workers-cache-cloudflare demo app from feat/route-cache-request-context so the route-cache CDN adapter changes on this branch can be tested. * ci: trigger PR workflows on any base branch, not just main Drop the `branches: [main]` filter from the pull_request trigger in ci.yml, deploy-examples.yml, and preview-release.yml so these workflows run on PRs against any base branch (e.g. stacked PRs). * update lockfile * ci(deploy-examples): add workers-cache-cloudflare to deploy matrix Pull in the deploy matrix + preview-URL comment entry from feat/route-cache-request-context so the workers-cache demo app gets built, deployed, and linked on PR previews. * fix(cache): auto-select edge CDN adapter from request context, no import needed The edge-managed CDN cache adapter was only activated when a detector got registered as a side effect of importing `vinext/cloudflare`. Apps with a hand-written worker entry (e.g. the workers-cache demo) never imported it, so ISR silently fell back to the origin-managed default — emitting plain `Cache-Control` instead of `CDN-Cache-Control` / `Cache-Tag`. The adapter is platform-agnostic (it only touches the generic request-context cache surface), so move it into core as `RequestContextCdnCacheAdapter` and have `getCdnCacheAdapter()` select it directly. Resolution is now: 1. explicit `setCdnCacheAdapter()` (always wins) 2. request-context cache present (`ctx.cache`) -> edge adapter 3. otherwise -> origin-managed default Drops the detector-registry indirection and the import/registration requirement. `CloudflareCdnCacheAdapter` is kept as a re-export alias for backwards compatibility. * fix(cache): select Cloudflare edge adapter from resolver, keep it in the cloudflare module Previous commit moved the adapter into core — revert that. The CloudflareCdnCacheAdapter stays in cloudflare/cloudflare-cdn-cache.ts; the core resolver imports it and instantiates it as the built-in default when the request context exposes a host cache (ctx.cache). Drops the detector-registry side-effect-import requirement; resolution is explicit -> ctx.cache edge adapter -> origin-managed default. * example(workers-cache): show CDN-Cache-Control in the probe headers * example(workers-cache): rename example app from workers-cache-cloudflare to workers-cache Rename the example directory and update its package name, wrangler worker name, the deploy-examples matrix + preview-URL list, and the lockfile. * fix(cache): give bare stale-while-revalidate an explicit window for the CF edge The framework emits a value-less `stale-while-revalidate` (Vercel's unbounded extension). Cloudflare follows RFC 5861 and ignores the bare directive, so the edge had no stale window — entries hard-expired at max-age and the next request was a MISS instead of UPDATING. Normalize bare SWR to an explicit 1-year window in the edge adapter's toEdgeCacheControl so Cloudflare actually serves stale while revalidating. * use link component * fix(cache): let the CDN adapter own the default Cache-Control when none is set Rendered responses that produced no cacheable policy (e.g. dynamic App Router pages) were going out with no Cache-Control at all, bypassing the CDN adapter — so on the edge Cloudflare applied its own default caching heuristic instead of the adapter's policy. Add a guard in finalizeAppRscResponse (the single App Router egress, already run for every page/route-handler/metadata/not-found response) that, when no Cache-Control is present, routes through the adapter to supply the default: the edge adapter emits no-store (never accidentally edge-cache an unspecified response), the default adapter leaves it absent (unchanged). Runs only when the header is absent, so it never clobbers a policy a renderer already applied (incl. CDN-Cache-Control). Also stop applyCdnResponseHeaders from stamping an empty Cache-Control value. * refactor(cdn-cache): align adapter method names with data cache + gate ctx.cache auto-detection Address PR #1693 review feedback: - Align CdnCacheAdapter field naming with the data cache adapter (CacheHandler): readPage->get, writePage->set, revalidate->revalidateTag. buildResponseHeaders / ownsBackgroundRevalidation stay CDN-specific (no CacheHandler equivalent). Updated both implementations and all call sites. - Gate ctx.cache auto-detection behind VINEXT_CDN_CACHE_AUTO_DETECT (default off) so edge-managed page ISR is opt-in until deployment skew protection is figured out. Removed the dedicated _edgeAdapter variable; the resolved edge adapter is now stored on the single active-adapter global slot that setCdnCacheAdapter uses. Enable the flag for the workers-cache demo via wrangler.jsonc vars. * test(cdn-cache): update tests for renamed adapter API + flag-gated auto-detect Align the CDN adapter unit tests with the refactor: - get/set/revalidateTag method names (was readPage/writePage/revalidate) - bare stale-while-revalidate now normalized to an explicit window - auto-detection is gated behind VINEXT_CDN_CACHE_AUTO_DETECT (no detector / no vinext/cloudflare side-effect import) * chore: reconcile pnpm-lock.yaml after merge The merge auto-resolved pnpm-lock.yaml into a broken state (missing @vitejs/plugin-rsc entry), so `vp install` failed at CI setup. Regenerated with pnpm install --no-frozen-lockfile; frozen install now passes. |
||
|
|
dec0ee5369 |
chore: bump Next.js and React patch versions (#1721)
* chore: bump next and react patches * update other areas and bump down peer dep * . --------- Co-authored-by: James <james@eli.cx> |
||
|
|
60dfb11c62 |
chore(deps): bump @vitejs/plugin-rsc to 0.5.27 (#1709)
* chore(deps): bump `@vitejs/plugin-rsc` to 0.5.27 * test(rsc): cover `export * from` in 'use client' modules (#1352) Ports the build fixture from #1373 to verify the `@vitejs/plugin-rsc` 0.5.27 bump resolves the upstream "unsupported ExportAllDeclaration" error. Covers bare `export *`, nested/renamed re-exports, `export * as Ns from`, and destructured re-exported bindings. The version bump fixes this natively, so the vinext-side workaround plugin from #1373 is no longer needed. Refs #1352 |
||
|
|
d03c5aa045 |
test(msw): introduce MSW infrastructure and migrate font-google-build (#1444)
* test(msw): introduce MSW infrastructure and migrate font-google-build
Replaces ad-hoc `globalThis.fetch` hijacking in tests with Mock Service
Worker. MSW intercepts both `globalThis.fetch` and `node:http`/`https`,
so it covers fetches issued by in-process Vite servers and fixture pages
without each test having to manage its own stub.
The setup file enables `onUnhandledRequest: 'error'` so any future test
that issues an unmocked external request fails loudly instead of
silently hitting the network. Loopback hostnames (localhost, 127.0.0.1,
::1) are bypassed because the integration project deliberately hits
in-process test servers via fetch.
What this PR contains:
- `tests/_msw/{handlers,server,setup}.ts` — shared infra
- `setupFiles` wired into both vitest projects (unit + integration)
- `msw` + `@mswjs/interceptors` added to the workspace catalog
- `msw` postinstall skipped via `allowBuilds` (it only copies a browser
service worker; we exclusively use `msw/node`)
- `tests/font-google-build.test.ts` migrated as the proof of pattern
Temporary handlers for `httpbin.org/uuid` and
`example.com/not-cacheable` keep the existing fixture pages
(`revalidate-tag-test`, `dynamic-error-fetch`) working under the new
guard. These are placeholders flagged with `TODO(msw-migration)` — a
follow-up will replace those live URLs with test-local endpoints.
This is PR 1 of 3:
- PR 2 will migrate `tests/fetch-cache.test.ts`
- PR 3 will migrate `tests/font-google.test.ts` and the font block in
`tests/app-router.test.ts`
* test(msw): restrict MSW to the unit project + use passthrough for loopback
Two fixes to the initial MSW wiring that surfaced when CI ran the full
suite:
1. The integration project is now excluded from MSW. Integration tests
spin up in-process HTTP servers and fixture dev servers and exercise
them via `fetch("http://127.0.0.1:<port>/...")`. Even with a loopback
`passthrough()` handler installed, the @mswjs/interceptors layer
interferes with that traffic in subtle ways:
- `tests/prerender.test.ts > errors without writing .rsc when the
middleware short-circuit fallback RSC request fails` timed out at
30s — the 5xx response body never reached the caller.
- `tests/ecosystem.test.ts > nuqs` hung in `beforeAll` waiting for
its fixture process to come up, because the readiness fetch
stalled inside the interceptor.
Integration tests already talk to real local servers, not the
internet, so the unhandled-request guard buys little there. The
value of MSW for this repo is mocking external HTTP for unit tests
of fetch wrappers (font-google, font-google-build, fetch-cache),
which is preserved.
2. The `onUnhandledRequest` callback in `setup.ts` that tried to
silently let loopback through wasn't sufficient — MSW's interceptor
still ran for those requests and could stall them. Replaced with a
default `http.all(LOOPBACK_URL_PATTERN, () => passthrough())`
handler in `handlers.ts` and a plain `onUnhandledRequest: "error"`
in `setup.ts`. Unit tests like the middleware-rewrite proxy in
`tests/shims.test.ts` and the JSX-in-JS suite need this to keep
their in-process HTTP servers reachable.
* test(msw): address BigBonk review notes
- Update PR description in the commit body to drop the stale mention of
TODO(msw-migration) handlers — they were removed when the integration
project was excluded from MSW; the fixture pages run there.
- Tighten the loopback IPv4 octet regex from `\d+` to `\d{1,3}` so it
doesn't match absurdly long octet strings. `URL.hostname` normalises
real addresses, so this is purely a readability fix.
* test(msw): address remaining BigBonk notes from the initial review
- Add `0.0.0.0` to the loopback passthrough pattern so test servers that
bind to the unspecified address keep working without per-test mocks.
- Add a `TODO(msw-3)` comment in `server.ts` flagging the
`SetupServerApi` deprecation in MSW 2.14.6 — the `FetchInterceptor`-only
construction will need to migrate to `defineNetwork`'s interceptor
config when we bump to MSW 3.
Both raised by BigBonk on the first review of this PR (the second review
focused on the stale description and the loose `\d+` octet bound, which
were addressed in the previous commit).
* test(msw): scope server.use() inside the try block in font-google-build
Minor structural fix from BigBonk's review: moves the `server.use()`
handler registration inside the `try` block so all setup and the
matching `fs.unlink` cleanup share the same try/finally scope.
No behaviour change — `server.use()` is synchronous and infallible, so
this is purely a structural tidy-up.
* test(msw): correct comment in font-google-build to match interceptor config
BigBonk caught that the comment described the default `setupServer()`
behaviour (which uses both `FetchInterceptor` and `ClientRequestInterceptor`),
but this project's `server.ts` deliberately constructs `SetupServerApi`
with only `FetchInterceptor`. Update the comment to reflect that — it
intercepts `globalThis.fetch`, not `node:http`/`node:https`.
No code change, comment only.
|
||
|
|
371817a07b | chore: upgrade vite plus (#1410) | ||
|
|
c6e8bda55e |
feat(app-router): support app/global-not-found.tsx (Next.js 16 parity) (#1301)
When users define app/global-not-found.tsx, route-miss 404s should render that module standalone — it provides its own <html>/<body> and replaces the root layout, matching the behavior of Next.js 16's experimental.globalNotFound feature. https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/global-not-found https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/app-render.tsx#L495-L520 Implementation: - Discover app/global-not-found.{tsx,ts,jsx,js} alongside the existing global-error scan (src/index.ts). - Thread the path through generateRscEntry via AppRouterConfig and buildAppRscManifestCode so the generated entry imports it and binds it as globalNotFoundModule. - Pass the module into createAppFallbackRenderer; renderHttpAccessFallback switches to the global-not-found document when statusCode === 404, the route is null (route-miss), and no boundary override was supplied. - Add a skipLayoutWrapping flag to renderAppPageHttpAccessFallback so the standalone document isn't wrapped in the root layout, and so the RSC payload omits layout-id entries that would otherwise refer to a layout tree the markup doesn't contain. Page-triggered notFound() calls keep the existing path: they still resolve through the regular not-found.tsx boundary inside layouts — global-not-found.tsx only takes over for route-miss 404s. Tests: - tests/app-fallback-renderer.test.ts adds four unit tests covering the new branch (route-miss with global-not-found, page-call with global-not-found, absent global-not-found, 403/401 unaffected). - tests/entry-templates.test.ts adds four codegen assertions covering the manifest import and the generated-entry wiring in both directions (present and absent). - tests/nextjs-compat/global-not-found.test.ts ports the basic Next.js fixture as an integration test against a dev server, asserting that the route-miss 404 renders global-not-found's <html> standalone with exactly one <html>/<body>, and that page-call notFound() does not use the global-not-found document. |
||
|
|
85f2c10fab | chore: update @vitejs/plugin-rsc to 0.5.26 (#1293) | ||
|
|
1c0bdacda6 |
feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest (#1239)
* feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest
Adds a /compatibility page to apps/web that visualises Next.js
compatibility over time, populated by the nightly deploy-suite workflow.
- D1 binding (`vinext-metrics`) with Drizzle schema and migrations.
Tables are namespaced (`compat_runs`, `compat_file_results`) so
future metric kinds can live alongside.
- POST /api/compatibility ingest endpoint. Authenticates with a
`COMPAT_INGEST_SECRET` worker secret and upserts by
(kind, runKey).
- GitHub-style contribution grid (one dot per test file) and a
pass-rate-over-time line chart. Both are SVG, no chart libs.
Pass rate excludes Next.js-skipped tests from the denominator.
- Shared header/footer extracted into app/_components and rendered
by the root layout. Nav uses next/link for soft client nav.
- Workflow change: on top of the existing per-test pass/fail/skip
aggregation, the report job now POSTs results to the ingest
endpoint. Guarded to only run when (a) filter=all and
(b) targeting main, so partial runs and branch spot-checks don't
pollute the historical record.
The first nightly run after merge will seed the production D1.
* address review + add ISR with KV cache
Review fixes (apps/web/app/api/compatibility/route.ts):
- constant-time secret comparison via SHA-256 + bytewise XOR
- single-statement atomic upsert with onConflictDoUpdate (no
SELECT->INSERT race for concurrent retried runs)
- top-level try/catch with structured error body so workflow logs
show why ingest failed instead of a generic 500
- MAX_FILES=2000 cap in validation as defensive bound
Page perf (apps/web/app/compatibility/page.tsx):
- latest-run and trend D1 queries run in parallel via Promise.all
ISR + KV:
- new vinext-web-cache KV namespace bound as VINEXT_CACHE
- worker installs KVCacheHandler once per isolate
- both pages opt into ISR with revalidate = 300; compat page drops
force-dynamic
* address bonk re-review
- Drop the ?kind= query param. Only one kind exists today; gating it
behind an unused param invited ISR cache pollution. Hardcoded to
"deploy" with a comment pointing future kinds at a dedicated route.
- Wrap the DELETE + chunked INSERTs in a single `db.batch()` so a
crash mid-write can no longer leave a run with zero file results.
D1 executes batch statements inside one transaction.
- Resolve workflow expressions once via step-level `env:` and read
them as `process.env.*` from the github-script body. Removes the
fragile splicing of `${{ inputs.next-ref || 'v16.2.6' }}` into JS
string literals (the nested single quotes were the worst offender).
|
||
|
|
d9906b94d6 |
chore: add basic vinext site (#1236)
* chore: scaffold new next app with vinext * basic app * add to deploy workflow * fmt |
||
|
|
1ac7b6514b |
fix(examples): next build failure for app-router-playground (#1234)
|
||
|
|
9d5a2e9a77 |
chore(deps): bump vite-plus to 0.1.21 (#1190)
- Bump vite, vite-plus, vitest catalog entries to 0.1.21 - Refresh pnpm-lock.yaml - Regenerate AGENTS.md via vp config postinstall - Drop unused imports (PHASE_PRODUCTION_BUILD, patternToNextFormat) flagged by upgraded lint rules - Move react-hooks/rules-of-hooks suppression in tests/e2e/fixtures.ts to the use(errors) call site - Add react/no-children-prop and react/no-danger-with-children suppressions on the intentional precedence test in tests/head.test.ts |
||
|
|
e9d1749b95 |
chore(deps): bump next from 16.2.5 to 16.2.6 (#1183)
* bump nextjs * . --------- Co-authored-by: James <james@eli.cx> |
||
|
|
477411ae1b |
chore(deps): bump next-intl from 4.9.1 to 4.9.2 (#1160)
Bumps [next-intl](https://github.com/amannn/next-intl) from 4.9.1 to 4.9.2. - [Release notes](https://github.com/amannn/next-intl/releases) - [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/next-intl/compare/v4.9.1...v4.9.2) --- updated-dependencies: - dependency-name: next-intl dependency-version: 4.9.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
826bab4530 | feat: implement next web vitals hook (#1161) | ||
|
|
3a9046dce2 |
fix(image): support images.dangerouslyAllowLocalIP for private-IP rejections (#1153)
* fix(image): support images.dangerouslyAllowLocalIP for private-IP rejections Ports Next.js commit 5452439f3db2a78967178ca4180b27fb48393a19 (PR #91686) to vinext's next/image shim and server-side image config. Changes: - Add isPrivateIp() helper in shims/image-config.ts (IPv4 + IPv6, no deps) - Update validateRemoteUrl in shims/image.tsx to reject literal private-IP hostnames unless dangerouslyAllowLocalIP is true - Wire dangerouslyAllowLocalIP through next-config type, Vite define, global.d.ts, and server-side ImageConfig - Add unit tests for isPrivateIp and component-level private-IP guard Closes #1065 * fix(image): use ipaddr.js for correct private-IP classification Addresses review feedback on the SSRF guard: - Replaces the hand-rolled isPrivateIp with ipaddr.js (same library Next.js uses), eliminating bugs in IPv4/IPv6 range checks - Fixes incorrect 2001:2f::/32 check (not a real range) and adds all missing non-unicast ranges: CGNAT, multicast, reserved, benchmarking, teredo, documentation, discard, NAT64 - Correctly handles ::ffff: IPv4-mapped addresses via isIPv4MappedAddress() - Adds explanatory comments for best-effort client-side guard and dangerouslyAllowLocalIP server-side config - Adds comments in tests explaining the vi.resetModules() pattern * fix(image): externalize ipaddr.js to prevent SSR crash ipaddr.js is a CJS UMD module that assigns to `this.ipaddr` at the bottom of its wrapper. When Vite's SSR module runner loads it with `noExternal: true`, the UMD wrapper executes with `this = undefined` (strict mode in ESM context), producing: TypeError: Cannot set properties of undefined (setting 'ipaddr') This crashed every page render that imports `next/image` (via the image-config.ts shim). The image-optimization-parity tests failed with 500 errors. Fix: add `ipaddr.js` to `ssr.external` in all 3 Vite environment configs (top-level, RSC, SSR) so Node's native loader handles the CJS module correctly. |
||
|
|
b631b5da4a |
chore(deps): bump next.js version (#1112)
* chore(deps): bump next.js version * fix tests |
||
|
|
c74faed38c | chore(deps): bump react version (#1110) | ||
|
|
c1c81886d2 | test: add coverage summaries for integration tests in ci (#1024) | ||
|
|
1d7c031e30 | feat(app-router): feed file metadata routes into head output (#891) | ||
|
|
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. |
||
|
|
89a351b44b |
chore(deps): bump postcss from 8.5.3 to 8.5.10 (#889)
Co-authored-by: James <james@eli.cx> |
||
|
|
ff5e14c41d |
setup knip (#886)
* setup knip * nits |
||
|
|
8f224051b7 |
Preserve intercepted app-router state across server actions (#854)
* Preserve interception context on app-router actions * Keep intercept slot overrides on action rerenders * Pin React versions in static-export fixture * refactor(app-page-request): unify intercept resolution into a single state machine Collapse the parallel GET/POST intercept lookup paths into one shared internal helper that returns a tagged union: none, current-route, or source-route. Both resolveAppPageInterceptMatch and resolveAppPageActionRerenderTarget now read from the same lookup result, eliminating the double findIntercept() scan on current-route fallbacks. Also add the two missing resolveAppPageActionRerenderTarget fallthrough test cases (no intercept, current-route intercept with no matching action route) and a regression assertion that the current-route branch only performs one intercept lookup. |
||
|
|
41d363e8b2 |
chore(deps): bump vite-plus from 0.1.12 to 0.1.17 (#858)
* chore(deps): bump vite-plus from 0.1.12 to 0.1.17 Bumps [vite-plus](https://github.com/voidzero-dev/vite-plus/tree/HEAD/packages/cli) from 0.1.12 to 0.1.17. - [Release notes](https://github.com/voidzero-dev/vite-plus/releases) - [Commits](https://github.com/voidzero-dev/vite-plus/commits/v0.1.17/packages/cli) --- updated-dependencies: - dependency-name: vite-plus dependency-version: 0.1.17 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * bump setup-vp * update agents --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James <james@eli.cx> |
||
|
|
cff49ecb2e |
chore(deps): bump sanitize-html from 2.17.2 to 2.17.3 (#856)
Bumps [sanitize-html](https://github.com/apostrophecms/apostrophe/tree/HEAD/packages/sanitize-html) from 2.17.2 to 2.17.3. - [Changelog](https://github.com/apostrophecms/apostrophe/blob/main/packages/sanitize-html/CHANGELOG.md) - [Commits](https://github.com/apostrophecms/apostrophe/commits/sanitize-html@2.17.3/packages/sanitize-html) --- updated-dependencies: - dependency-name: sanitize-html dependency-version: 2.17.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
84a5519628 |
chore(deps): bump next-intl from 4.8.3 to 4.9.1 (#829)
Bumps [next-intl](https://github.com/amannn/next-intl) from 4.8.3 to 4.9.1. - [Release notes](https://github.com/amannn/next-intl/releases) - [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/next-intl/compare/v4.8.3...v4.9.1) --- updated-dependencies: - dependency-name: next-intl dependency-version: 4.9.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
8be547c6bb |
chore(deps): bump next (#824)
* bump next * update constants |
||
|
|
1e6b7e681d | rebuild lockfile (#804) | ||
|
|
8d29f76512 |
fix(plugins): track font call ranges during transform (#785)
* fix(plugins): track font call ranges during transform The injectSelfHostedCss function was calling s.overwrite() to transform font calls but not adding the call range to overwrittenRanges. This could potentially cause issues when multiple font calls exist in the same file, as subsequent transforms might not properly detect that a previous call was already processed. Add the call range to overwrittenRanges after each font call transformation to ensure proper tracking. Related to #751 - defensive consistency fix * test(fonts): remove duplicate test and add E2E build test for #751 Removes the near-duplicate unit test 'transforms multiple fonts with identical options patterns' from font-google.test.ts. This test was a duplicate of 'handles multiple font imports in one file' and did not actually reproduce issue #751. Adds a proper integration test in font-google-build.test.ts that uses createBuilder + buildApp to exercise the actual Vite build pipeline where issue #751 occurred. Includes fixture at app-basic/app/font-google-multiple/ with Geist + Geist_Mono fonts matching the original issue reproduction. The actual #751 bug (Unexpected token error) was already fixed in previous commits #719 (double-comma) and #723 (nested-brace). The defensive overwrittenRanges tracking remains in fonts.ts as correct defensive code. All 78 font-related tests pass, plus 291 app-router tests. * style(fonts): fix lint and formatting issues in #751 test - Remove unused beforeAll import - Remove unnecessary Plugin type import and cast - Fix formatting from vp check --fix * style: fix formatting in font-google-multiple layout * refactor(tests): move font-google-multiple to standalone fixture and improve assertions Moves the font-google-multiple fixture from a nested location inside app-basic to a standalone fixture at tests/fixtures/font-google-multiple/. This prevents side effects for other tests that build the app-basic fixture. Updates the build test to remove misleading comments about reproducing #751. The overwrittenRanges.push() fix in fonts.ts is a defensive consistency improvement, not a fix for issue #751. - Removes the nested fixture from app-basic/app/font-google-multiple/ - Creates minimal standalone fixture with package.json and vite.config.ts - Updates test comments to clarify this tests general build validation - Removes incorrect claim about reproducing #751 * fix(tests): address PR review feedback on font transform build test - Fix vite import to vite-plus per repo conventions - Strengthen build test assertions to verify font transform output - Add missing package.json to font-google-multiple fixture - Remove unused vite.config.ts from fixture - Remove #751 references (this is a defensive fix, not a fix for that issue) * chore: update pnpm-lock.yaml for font-google-multiple fixture * fix(tests): handle pre-existing node_modules and correct build output extension in font-google-build test Two issues causing CI failure: 1. EEXIST on symlink: fixture's node_modules could be a real directory (from pnpm install), not just a stale symlink. Use fs.rm with force:true before creating the symlink to handle both cases. 2. Wrong output extension: build produces index.js not index.mjs (likely a Vite 8/Rolldown default extension change). |
||
|
|
f910e74045 |
fix(app-router): reject cyclic Flight payloads in server actions (#798)
* fix(app-router): reject cyclic Flight payloads in server actions * test(app-router): update generated entry snapshots |
||
|
|
1a339c7ca4 | chore: bump vulnerable deps (#773) | ||
|
|
7625d4a215 | remove unused rsc-html-stream dep (#771) | ||
|
|
49656ed5cf |
fix: security audit findings from issue #741 (#757) (#769)
* fix: security audit findings from issue #741 CI shell injection (high): - publish.yml: use env var for inputs.bump in case statement - nextjs-tracker.yml: use env vars for since_hours and dry_run inputs - nextjs-tracker.yml: use env var in Skip step echo Test file false positive (critical flag): - tests/vite-hmr-websocket.test.ts: add gitleaks:allow for RFC 6455 example WebSocket nonce (dGhlIHNhbXBsZSBub25jZQ==) innerHTML / XSS (high): - packages/vinext/src/shims/script.tsx: add security comment documenting that dangerouslySetInnerHTML is developer-supplied inline script only - packages/vinext/src/shims/head.ts: document tag is bounded to RAW_CONTENT_TAGS (script/style), not user input dangerouslySetInnerHTML in example (medium): - examples/hackernews/components/comment.jsx: sanitize HN API HTML with DOMPurify before rendering - examples/hackernews/package.json: add dompurify dependency - pnpm-workspace.yaml: add dompurify and @types/dompurify to catalog Non-literal RegExp (medium): documented as internal config values only, no user input reaches any of the 6 flagged patterns. See safeRegExp() in config-matchers.ts which already enforces ReDoS protection. Deprecation cleanup: - examples/hackernews/tsconfig.json: remove deprecated baseUrl option (moduleResolution: bundler handles resolution; all imports are relative) - examples/hackernews/tsconfig.json: add noEmit: true to prevent TypeScript from attempting to write over existing .js files in lib/ - examples/hackernews/package.json: add missing @cloudflare/workers-types devDependency (was referenced in tsconfig types but not installed) * fix: address bonk review comments - Switch dompurify → isomorphic-dompurify so sanitize() works during SSR (DOMPurify is a no-op without window/document; isomorphic-dompurify bundles jsdom for server-side use) - Revert package.json whitespace to 2-space indentation (was inadvertently changed to 4-space, creating noise in the diff) - Replace dompurify + @types/dompurify catalog entries with isomorphic-dompurify (ships its own types, no separate @types needed) - Add missing trailing newline to tsconfig.json * fix: address bonk review comments on PR #769 - Switch from isomorphic-dompurify to sanitize-html, and move sanitization to the server-side data boundary (lib/get-comments.js). isomorphic-dompurify depends on jsdom which is Node.js-specific and does not work in the Cloudflare Workers runtime. sanitize-html is pure-JS (htmlparser2-based) with no DOM dependency and works in both Workers and Node.js. Sanitizing once at the data boundary means both the SSR'd initial HTML response and client re-renders receive safe text. - Remove the DOMPurify import from comment.jsx; the client component no longer needs to sanitize since text is already clean when passed as a prop. - Replace isomorphic-dompurify with sanitize-html in the workspace catalog. - Remove inert eslint-disable-next-line comment from script.tsx; the project uses oxlint which ignores ESLint-specific inline suppression directives. --------- Co-authored-by: Luke Percy <lpercyagile@gmail.com> |
||
|
|
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> |
||
|
|
9a07314146 |
chore(deps): bump h3 from 2.0.1-rc.16 to 2.0.1-rc.20 (#703)
Bumps [h3](https://github.com/h3js/h3) from 2.0.1-rc.16 to 2.0.1-rc.20. - [Release notes](https://github.com/h3js/h3/releases) - [Changelog](https://github.com/h3js/h3/blob/main/CHANGELOG.md) - [Commits](https://github.com/h3js/h3/compare/v2.0.1-rc.16...v2.0.1-rc.20) --- updated-dependencies: - dependency-name: h3 dependency-version: 2.0.1-rc.20 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
05cac9d745 |
build: align Vite 8 bundler config (#599)
* build: align Vite 8 bundler config * fmt --------- Co-authored-by: James <james@eli.cx> |
||
|
|
fd7618c94a | chore: move pnpm settings to pnpm-workspace.yaml (#671) | ||
|
|
6936601a90 | chore(deps): bump nitro for h3 vulnerability (#621) | ||
|
|
746dc5b522 |
feat!: fumadocs example, mark clientReferenceDedup experimental (#577)
* chore: init fumadocs * vinext init * fix dev * tweaks * tweaks * tweaks * prefer exports * tweaks * try new approach * update * tweaks * Update * exclude deps * update * revert * tweaks * tweaks * tweaks * tweaks * pre include @unpic/react * clientReferenceDedup * tweaks |
||
|
|
0b43cad172 |
chore(deps): bump next from 16.1.6 to 16.1.7 (#579)
* chore(deps): bump next from 16.1.6 to 16.1.7 Bumps [next](https://github.com/vercel/next.js) from 16.1.6 to 16.1.7. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v16.1.6...v16.1.7) --- updated-dependencies: - dependency-name: next dependency-version: 16.1.7 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * add benchmarks --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James <james@eli.cx> |
||
|
|
499ee6a979 |
refactor: use vinext startProdServer HTTP for prerendering (#574)
* refactor: remove wrangler from prerender, use startProdServer HTTP for all builds All builds — plain Node and Cloudflare Workers — now prerender via HTTP against a locally-spawned startProdServer instance. Removes findWranglerConfig, loadWrangler, WranglerWorker, isWorkersBuild branching, and all related fields from PrerenderAppOptions. Fixes generateStaticParamsMap not being accessible from CF bundles by falling through to the RSC handler for prerender endpoints. * . * fix: redirect:manual for prerender fetch, remove wrangler peer dep - Add redirect:manual to renderPage fetch in prerenderPages so that getStaticProps redirect responses are captured instead of followed. Fixes the 'escapes meta refresh URL to prevent HTML injection' test. - Remove wrangler from devDependencies and peerDependencies now that all prerender logic goes through the HTTP prod server path. * refactor: extract readPrerenderSecret to shared utility, fix server-manifest secret rotation - Move readPrerenderSecret to src/build/server-manifest.ts so both build-time code (prerender.ts) and runtime code (prod-server.ts) can import it without creating a circular dependency - Fix vinext:server-manifest plugin calling randomBytes() independently in each Vite environment (rsc + ssr), causing the second env to overwrite the first with a different secret; generate the secret once at plugin creation time via an IIFE closure - Replace the two duplicate manifest-read blocks in prod-server.ts with calls to the shared readPrerenderSecret helper * fix: warn on non-OK response from pages-static-paths prerender endpoint A 403 (stale secret) or 500 from the endpoint was silently treated the same as 'no paths', causing dynamic routes to be skipped without any indication of why. Now logs a warning with the HTTP status code so a stale-secret scenario is visible in the build output. * fix: guard static-params fetch against non-OK responses in prerenderApp A 403 (stale secret) or 500 from /__vinext/prerender/static-params was passed directly to JSON.parse, which throws on a non-JSON body like 'Forbidden'. The rejected Promise was then cached in staticParamsCache, poisoning all subsequent generateStaticParams lookups for that route. - Check res.ok before JSON.parse; return null and warn on non-OK status - Evict the cache entry on rejection so a transient error does not permanently block the route |
||
|
|
75eb3c1e8d |
feat: production prerender pipeline (#553)
* feat: production prerender pipeline (no dev server)
Add prerenderPages() and prerenderApp() that load from production bundles
exclusively — no ViteDevServer dependency. runPrerender() replaces
runPrerenderWithDevServer() (kept as a shim for cli.ts/deploy.ts callers).
Key changes:
- packages/vinext/src/build/prerender.ts — new: prerenderPages/prerenderApp
- packages/vinext/src/build/run-prerender.ts — new: runPrerender + shim
- packages/vinext/src/build/static-export.ts — delegates to prerender layer
- packages/vinext/src/index.ts — adds rscOutDir/ssrOutDir/clientOutDir/disableAppRouter options
- packages/vinext/src/entries/app-rsc-entry.ts — exports generateStaticParamsMap
- packages/vinext/src/entries/pages-server-entry.ts — exports pageRoutes
- packages/vinext/src/shims/cache.ts — adds NoOpCacheHandler
- packages/vinext/src/server/prod-server.ts — serves prerendered .html files
- packages/vinext/src/build/report.ts — integrates PrerenderResult for build report
- tests/helpers.ts — buildPagesFixture/buildAppFixture helpers (isolated outDirs)
- tests/prerender.test.ts — 36 new tests
- tests/static-export.test.ts/app-router.test.ts/pages-router.test.ts — updated
All 487 tests pass. Typecheck, lint, fmt clean.
* fix: build Pages Router SSR bundle for hybrid app+pages projects
For hybrid projects that have both app/ and pages/ directories, the
App Router multi-env build (createBuilder/buildApp) does not produce
a Pages Router SSR bundle. The prerender phase then fails with 'bundle
not found at dist/server/entry.js'.
Fix: after buildApp() completes, detect pages/ presence and run a
separate standalone Vite SSR build with vinext({ disableAppRouter: true })
so the plugin's multi-env environments config does not override the SSR
input/entryFileNames. The build is run with emptyOutDir: false to preserve
RSC artefacts from the App Router build.
* Clean up run-prerender: remove deprecated shim, fix progress arithmetic and typos
* regen snaps
* refactor: clean up prerender pipeline, cache shim, and report utilities
- Extract loadBundle() helper used by both prerenderPages and prerenderApp
- Save/restore CacheHandler around prerender calls (isolation fix)
- Rename isExplicitlyDynamic -> isConfiguredDynamic for clarity
- Export getRscOutputPath and findDir for cross-module reuse
- Import findDir in run-prerender.ts; remove duplicated inline logic
- Fix single-row table corner character in formatBuildReport
- Remove duplicate JSDoc block before revalidateTag in cache shim
- Add SetCtx interface; eliminate (ctx as any) casts in MemoryCacheHandler
- Replace string sentinel in unstable_cache with structural CacheResultWrapper
- Use completedUrls += 1 consistently in Pages Router phase
* fix: remove unused appDir from PrerenderAppOptions and all callers
prerenderApp() never reads appDir — the bundle is loaded via rscBundlePath
and route scanning is done before the call. Remove the field from
PrerenderAppOptions and AppStaticExportOptions, and drop the now-unused
argument from all callers in run-prerender.ts, static-export.ts, and tests.
* refactor: miscellaneous cleanups across prerender, cache, cli, and deploy
* refactor: document gaps, re-detect project after installDeps, fix Vite resolution in deploy.ts
* refactor(prerender): unify onProgress to single call site; document pageCount semantics
Extract renderUrl() inner function from prerenderApp's concurrency loop so
that onProgress is called exactly once per URL at the outer loop level, rather
than being duplicated inside two early-return branches. Eliminates the risk
of accidentally omitting the callback when adding future early exits.
Add a comment in toStaticExportResult clarifying when pageCount and
files.length can diverge (currently they stay in sync).
* build(prerender): write non-export prerender output to dist/server/prerendered-routes/
On Cloudflare Workers, wrangler.jsonc uses not_found_handling: "none" so
every request hits the worker first. Files in dist/client/ are never
auto-served for page requests — they are uploaded but remain inert.
Writing prerendered HTML/RSC to dist/server/prerendered-routes/ keeps
them co-located with server artifacts and away from the static assets
directory.
This also prevents a future issue: when KV pre-population is implemented,
ISR route files must not be in dist/client/ or they would be served as
stale static files forever (bypassing revalidation).
output: 'export' builds are unaffected — static-export.ts passes its
own outDir explicitly, and runPrerender still writes to dist/client/
when mode === 'export'.
* .
* build: suppress IMPORT_IS_UNDEFINED warnings for generateStaticParams
Dynamic route pages that don't export generateStaticParams produce noisy
IMPORT_IS_UNDEFINED warnings because the virtual RSC entry unconditionally
emits mod?.generateStaticParams for every dynamic route. The optional
chaining guards the access safely at runtime; suppress the build-time noise
in the existing onwarn handler alongside the MODULE_LEVEL_DIRECTIVE filter.
* .
* .
* .
* fix conflict regression
* regen snaps
* update cli comment
* add todo for rsc double req
* encode uri components
* encode uri components
* fix: inherit user plugins in hybrid Pages Router secondary build
The secondary `vinext build` step for hybrid (App Router + Pages Router)
projects was constructed with `configFile: false` and only `vinext({ disableAppRouter: true })`,
dropping all user-supplied plugins including `cloudflare()`. This caused a
false-positive "Missing @cloudflare/vite-plugin" error on any hybrid project
with a `wrangler.jsonc` present.
Fix by inheriting the resolved plugin list from the already-completed App
Router builder, filtering out the `vinext:*` sub-plugins and re-injecting
`vinext({ disableAppRouter: true })` in their place. `configFile: false` is
retained to prevent the user's `environments` block from overriding the SSR
input and `entryFileNames`.
* fix: filter vite:react and vite-tsconfig-paths from inherited hybrid build plugins
vinext auto-registers both @vitejs/plugin-react (vite:react*) and
vite-tsconfig-paths when disableAppRouter is false, so inheriting the
resolved plugin list from the App Router builder caused duplicates.
Filter them out alongside the vinext:* sub-plugins.
* fix: filter rsc:* and vite-rsc-* plugins from inherited hybrid build plugins
@vitejs/plugin-rsc registers resolveId for App Router virtual modules
(virtual:vinext-app-ssr-entry etc.) but the corresponding load hooks live
in vinext:* — which we already strip. With rsc:* present but vinext:* absent
the virtual module resolves but can't load, crashing with PLUGIN_ERROR.
Filter rsc:* and vite-rsc-load-module-dev-proxy alongside the other
auto-registered plugin families.
* fix: skip missing-Cloudflare-plugin guard for hybrid Pages Router secondary build
The secondary build uses configFile:false + vinext({disableAppRouter:true})
so it never loads cloudflare() — by design, it's a plain SSR Rollup bundle
with no Workers config. The configResolved guard was incorrectly treating
this as a misconfigured user build.
Revert the inherited-plugins approach (cloudflare() reconstructs the full
multi-env environments block from its own plugin config hook, independent
of vite.config, causing the App SSR virtual entry to be resolved but not
loadable). Instead, gate the guard on !options.disableAppRouter, which is
exclusively set by this internal invocation.
* fix: inherit user transform plugins in hybrid Pages Router secondary build
The secondary SSR build for hybrid projects (app/ + pages/) was introduced
on this branch and always used configFile:false with only
vinext({disableAppRouter:true}), silently dropping any user transform
plugins from vite.config.ts (SVG loaders, CSS-in-JS, etc.).
Fix by loading the raw user config via loadConfigFromFile (before any
plugin config() hooks fire, so cloudflare() hasn't yet injected its
multi-env environments block) and forwarding all plugins except the
families that vinext auto-registers or that would break the plain SSR
build: vinext:*, vite:react*, rsc:*, vite-tsconfig-paths,
vite-rsc-load-module-dev-proxy, and vite-plugin-cloudflare*.
* fix: reset ANSI styling before prerender and route report output
Vite's logger leaves the terminal in a styled state after build output.
Write an ANSI reset before the prerender label and route report so they
always print at full brightness regardless of preceding Vite output.
* fix: skip instrumentation register() during prerender to prevent process hang
Instrumentation modules like @vercel/otel register OpenTelemetry SDK
exporters with background timers that keep the Node process alive
indefinitely. During prerender these side-effects cause the process to
hang after rendering completes, preventing the route report from printing.
Set VINEXT_PRERENDER=1 in prerenderApp/prerenderPages and gate
__ensureInstrumentation() on that env var so instrumentation is skipped
during prerender builds.
* feat: production prerender pipeline for Cloudflare Workers (App Router + Pages Router)
- Add wrangler devDep to packages/vinext; add `@cloudflare/workers-types` to catalog
- Fix prerender.ts: typed wrangler import, Unstable_DevWorker, undici Response casts,
URL-string extraction for dev.fetch() (no Request object), staticParamsMap Proxy
- Remove process.env.VINEXT_PRERENDER gate from /__vinext/prerender/static-params endpoint
- Add cf-app-basic pages/ fixture (index, about, posts/[slug], api/ping)
- Update buildCloudflareAppFixture to also run buildPagesFixture for Pages Router bundle
- Add CF prerender tests: shared beforeAll, nested App Router + Pages Router describes (46/46)
* .
* feat: CF Workers hybrid build Pages Router prerender support
* regen snaps
* fix(prerender): detect CF Workers build via @cloudflare/vite-plugin in node_modules
Replace wrangler.json file-presence check with node_modules detection,
consistent with how deploy.ts detects CF projects. The old check looked
in dist/server/ which never has a wrangler.json, so isWorkersBuild was
always false for CF projects, causing only the 404 to be prerendered.
The generated dist/server/wrangler.json (from @cloudflare/vite-plugin)
is now used as the config path for unstable_dev, with the project-root
wrangler.jsonc as a fallback. This ensures assets.directory is present,
which wrangler 4+ requires.
* fix(prerender): address bonk review comments
- runWithConcurrency: early return on empty items instead of spawning a
spurious worker via the '|| 1' fallback
- staticParamsMap Proxy: flip has() trap to return false so the typeof-fn
check works for routes without generateStaticParams on CF Workers builds;
also handle null return from the proxy fn (no generateStaticParams) in
the parent-params expansion path
- loadWrangler(): extract shared helper with a two-candidate fallback
(wrangler-dist/cli.js → index.js) used by both prerenderApp and
runPrerender, replacing duplicated path resolution + existence checks
- VINEXT_PRERENDER process.env mutation: add explanatory comments
documenting why the global mutation is intentional and safe for the
sequential-call contract
* refactor(prerender): extract findWranglerConfig, pass CF detection through, cache static-params
- Extract findWranglerConfig(serverDir, projectRoot) helper from both prerender.ts and
run-prerender.ts, eliminating the duplicated 4-candidate wrangler.json search
- Add isWorkersBuild and wranglerConfigPath optional fields to PrerenderAppOptions so
runPrerender can pass its already-computed values into prerenderApp, avoiding a
redundant findInNodeModules walk + 4-candidate fs.existsSync loop on every build
- Add a per-build staticParamsCache (Map keyed on pattern+parentParams) inside the CF
Proxy to dedup repeated /__vinext/prerender/static-params round-trips for deeply
nested dynamic routes
- Add clarifying comment on renderUrl's runWithHeadersContext wrapper explaining it is
a no-op for the CF Workers path (rscHandler is an HTTP proxy; ALS context never
crosses the isolate boundary) but kept for shape-compatibility across both modes
* fix(prerender): address remaining bonk review comments
- Security: gate /__vinext/prerender/* endpoints behind VINEXT_PRERENDER=1 check
to prevent exposure in normal deployments (process.env works for both Node and
CF Workers via Miniflare's var injection into process.env)
- Bug: buildUrlFromParams now throws a clear error when a required param is
missing instead of silently producing 'undefined' in the URL
- Design: add TODO comments for layout-level generateStaticParams limitation in
both resolveParentParams() and generateStaticParamsMap
- Correctness: prerenderPages() now uses runtime module exports (getServerSideProps/
getStaticProps) to classify page type on Node builds instead of static file
analysis; CF builds continue to use classifyPagesRoute() as fallback
* fix(prerender): address latest bonk review comments
- Add shape validation for parentParams on the /__vinext/prerender/static-params
endpoint: JSON.parse result is guarded to ensure user generateStaticParams always
receives a plain object, never a primitive, array, or null
- Add .gitignore for tests/fixtures/cf-app-basic/dist/ to prevent accidental commits
of build output if test cleanup fails (CF fixture builds to source tree, unlike
other fixtures that use tmpdirs)
- Add comment on nextConfigOverride shallow merge in run-prerender.ts to make the
limitation explicit for future maintainers
* regen snaps
* fix: build CF fixture in tmpdir instead of source tree
Use createIsolatedFixture in buildCloudflareAppFixture so the CF Vite
build output goes to a tmpdir rather than tests/fixtures/cf-app-basic/dist/.
Adds an optional nodeModulesDir param to createIsolatedFixture so callers
with fixture-scoped deps (like @cloudflare/vite-plugin) can point the
symlink at the fixture's own node_modules instead of the workspace root.
Removes the stopgap .gitignore and the afterAll that deleted dist/ from
the source tree.
* fix: cache-bust prod-server import() to prevent stale module reuse in tests
startProdServer() used a bare file:// URL for its dynamic import() of the RSC
entry bundle. Node's module cache keyed on that URL, so when two test describe
blocks rebuild to the same output path the second invocation always got the
cached module from the first build. The stale module had __instrumentationInitialized
already set to true and globalThis.__VINEXT_onRequestErrorHandler__ pointing at the
first build's instrumentation instance, whose capturedErrors array lived in a
different module instance than the one the production route handler was reading.
Fix: append ?t=<mtime> to the import URL, matching the pattern used by prerender.ts
loadBundle(). Same mtime means same content (cache hit, no-op); new mtime means
a fresh build and gets a fresh module. Applied to both startAppRouterServer and
startPagesRouterServer.
Also removes debug console.log calls from the instrumentation production test.
* fix: use globalThis for instrumentation test state to survive sequential prod builds
In Vitest, the 'App Router Production build' and 'App Router Production
server' describes run in the same process. The first build's preview server
imports dist/server/index.js uncached, setting
globalThis.__VINEXT_onRequestErrorHandler__ to onRequestError_v1 from the
first module instance. The second build is loaded cache-busted (by mtime),
producing a fresh module instance (v2). After v2 sets the handler, v1's
__ensureInstrumentation can re-fire and overwrite it, causing errors from
v2 to be recorded in capturedErrors_v1 (a different array) while the GET
route reads capturedErrors_v2 (empty).
Fix: store capturedErrors and registerCalled on globalThis (same pattern
as the middleware counter) so all module instances write to and read from
the same shared state regardless of which build instance is active.
Also removes debug logging and restores the afterAll dist cleanup that was
commented out during investigation.
* add process.exit(0) at end of build
|