mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.6
122 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
53e33babe2 | fix(perf): ensure full profiles exist (#2175) | ||
|
|
51b2c34958 | fix(perf): allow publisher PR comments (#2174) | ||
|
|
715ed317ed |
fix(perf): preserve permissioned cache roots (#2171)
* fix(perf): preserve permissioned cache roots * fix(perf): run trusted same-repo harness * fix(perf): ignore reaped benchmark zombies * fix(perf): profile the build process tree * fix(perf): wait for benchmark process cleanup * fix(perf): profile the real build command * fix(perf): profile the Vite Plus build entry * fix(perf): avoid nested profiling sudo * fix(perf): validate traces across profiles |
||
|
|
a2823aef7c | fix(perf): allow isolated Vite task cache (#2170) | ||
|
|
44b0c9dcff |
chore(perf): pair pull request benchmark baselines (#2159)
* fix(perf): pair pull request benchmark baselines * fix(perf): bootstrap benchmark fingerprint rollout * fix(perf): harden paired benchmark publishing * fix(perf): allow isolated build output creation * fix(perf): transfer isolated build outputs * fix(perf): verify isolated output permissions * fix(perf): validate diagnostic trace categories * fix(perf): retain trusted benchmark manifests * fix(perf): close benchmark validation gaps --------- Co-authored-by: James <james@eli.cx> |
||
|
|
42f8700f15 |
fix(router): enhance navigation with as mask for URL handling (#2076)
* fix(router): enhance navigation with `as` mask for URL handling * fix(router): implement bracket-pattern interpolation for dynamic routes in Pages Router * fix(router): preserve masked state in beforePopState * fix(router): cover basepath navigation in ci * docs(router): clarify navigation state invariants * refactor(router): require complete history state * refactor(router): share dynamic href interpolation * test(router): cover localized masked navigation * fix(router): keep interpolation internals private --------- Co-authored-by: James <james@eli.cx> |
||
|
|
a253b9e9e8 |
chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#2161)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
44802b7dd6 | fix(perf): stabilize stale pull request benchmarks (#2153) | ||
|
|
1d6eb006f1 |
fix(perf): trace sandboxed benchmark processes (#2144)
* fix(perf): trace sandboxed benchmark processes * fix(perf): preserve profiler cache under sudo * fix(perf): expose profiler helpers under sudo * chore(perf): format profiler environment * fix(perf): expose privileged trace artifacts * fix(perf): profile within benchmark sandbox * fix(perf): isolate profiler home environment * fix(perf): prepare kernel profiler access |
||
|
|
e46e2dbb05 |
feat(web): add performance benchmark dashboard (#2131)
* feat(web): add performance benchmark dashboard * fix(web): clean up benchmark dashboard exports * fix(web): support benchmark runs without baselines * fix(perf): support targeted main baseline runs * fix(web): revalidate benchmark commit baselines * feat(perf): improve vinext trace profiles * fix(web): neutralize insignificant benchmark diffs * fix(perf): replace existing benchmark runs * fix(perf): expose vinext trace frames * feat(perf): retain complete vinext trace detail * feat(perf): filter flame graph categories * fix(web): compact flame graph rows * fix(perf): replace runs by commit identity * fix(benchmarks): preserve complete performance traces * fix(benchmarks): use commit time for performance runs * fix(benchmarks): store performance profiles in R2 * fix(benchmarks): stream performance profiles to R2 * fix(benchmarks): decode raw performance profiles * fix(benchmarks): show inclusive filtered profile time * fix(benchmarks): resize filtered flame graphs * fix(benchmarks): render narrow flame frames * perf(benchmarks): bound flame graph detail * ci(perf): split benchmark publication privileges * fix(perf): harden benchmark publication * fix(perf): initialize runner paths in job * fix(perf): install through vite plus * fix(perf): disable pnpm hooks during install * fix(perf): select trusted harness ref * fix(perf): resolve vite plus for isolated user * fix(perf): grant isolated tool access * fix(perf): share benchmark cache cleanup * fix(perf): harden trace publication * chore(perf): format migration snapshots * feat(web): restore benchmarks navigation |
||
|
|
c3dcdba1fe |
fix(pages): honor basePath in dev module loaders (#2033)
* fix(pages): honor basePath in dev module loaders * fix(pages): preserve basePath in ISR loaders * style(pages): format dev module loader changes * fix(pages): preserve dynamic route module paths * test(pages): run basePath dev coverage in CI |
||
|
|
930e03c449 |
fix(ppr): gate fallback shells until request-time resume is supported (#1716)
* fix(app-router): prerender cacheComponents root-param fallback shells PR 1 of 4: core model + build integration. Model: app-ppr-fallback-shell, pregenerated-concrete-paths, prerender-manifest Build: prerender/run-prerender fallback shell artifact generation Tests: createAppPprFallbackShells, pregenerated-concrete-paths core * feat(ppr): add fallback shell payload identity * feat(ppr): safely serve fallback shell cache entries * refactor(ppr): extract fallback shell render pipeline and dedupe getViteMajorVersion * fix(ppr): restore safe-serving CI checks The safe-serving branch failed CI because a client-side server-only guard rejected valid 'use server' action modules, a fallback-shell dependency cast no longer matched its helper type, apps/web lost the Cloudflare cache adapter source path during typecheck, and Knip could not resolve the documented Cloudflare cache adapter subpaths. The server-only guard now reads the directive prologue and skips top-level 'use server' modules, fallback-shell regeneration passes an explicit typed dependency object, apps/web gets the workspace Cloudflare cache path, the Cloudflare package exports explicit adapter subpaths, Knip uses a relative source import in the dispatch unit test, and the readiness test awaits the actual cache-ready promise. * fix(ppr): guard fallback shell reads for queries * fix(ppr): normalize pregenerated concrete paths * fix(ppr): address review comments on fallback shell serving and react runtime check * fix(ci): restore knip ignores for unlisted binaries and prune dependencies * fix(ppr): restrict bracket check in fallback detection to legacy manifests * fix(ppr): prefer stable ESM react-dom/static.edge before CJS fallback The loadStaticPrerender dev-mode path hard-codes react-dom's internal CJS file layout (cjs/react-dom-server.edge.development.js). A React upgrade could silently break fallback-shell rendering in dev without affecting production because the function never tried the stable ESM entry in development. Change the order so that react-dom/static.edge is attempted first in all environments. Only fall back to the CJS path when the ESM entry does not export prerender() and we are in a development runtime. This lets future React dev builds that expose prerender() from the ESM entry work automatically, and it surfaces the CJS-path failure only when the stable path is genuinely unavailable. Also handle the CJS default-export interop shape that Node.js ESM produces when dynamically importing a CommonJS module. * refactor(ppr): address four review cleanup items 1. Remove redundant double-normalization in seed-cache.ts addPregeneratedConcretePath already normalizes internally, so the call-site normalizePregeneratedPathname() was redundant. Dropping it gives a single source of truth for pathname normalization. 2. Lazily construct FallbackShellRenderDeps in tryServePprFallbackShell Extract the large FallbackShellRenderDeps object into a top-level buildFallbackShellRenderDeps helper. The closure in the probe loop now just delegates, so the heavy object is only created when a STALE entry actually triggers regeneration, not on every HIT probe. 3. Add dev-only warning for legacy bracket heuristic When isFallbackShellArtifactPath falls back to the bracket substring scan (route.fallback === undefined), emit a console.warn in dev so developers know a legacy manifest is being used and concrete URLs with literal brackets may be misclassified. 4. Update pregenerated-concrete-paths.test.ts to colon notation The production manifest writer uses colon notation (/:slug) for the route field, but the test fixtures used bracket notation (/[slug]). Update the fixtures so the test format matches production, exercising the real key format that the safe-serving guard depends on. * test(ppr): compact fallback shell dispatch fixtures * refactor(ppr): name fallback shell render phases * refactor(app-router): deduplicate PPR fallback shell and app page cache render paths * fix(ppr): avoid serving incomplete fallback shells * chore: drop redundant cloudflare cache aliases * fix(ppr): track fallback navigation hooks * test(ppr): render navigation hook probes * test(ppr): type navigation hook probes * refactor(ppr): remove unused shell regeneration path * refactor(ppr): remove unused shell result type * fix(ppr): safely gate fallback shell prerendering * fix(ppr): keep fallback shell gate internal * fix(ppr): mark generated dynamic fallback shells * test(ppr): align fallback fixture and search params --------- Co-authored-by: James <james@eli.cx> |
||
|
|
d1a80cd668 |
fix(app-router): preserve front redirects through fallback route handlers (#2000)
* fix(app-router): preserve front redirects through fallback route handlers * test(e2e): assert single bootstrap hydration in front-redirect test * chore: remove dead stripDeploymentIdQuery export * fix(app-router): handle cancelled bootstrap streams * docs(app-router): clarify bootstrap recovery * test(e2e): tolerate dangling fixture symlink * test(e2e): run front redirect regression in CI * fix(app-router): always hide internal RSC queries * fix(app-router): preserve visible hydration URL * test(e2e): scope visible URL regression * refactor(app-router): trim front redirect residue * test(e2e): group fixture setup command * test(e2e): tighten front redirect assertions * fix(app-router): preserve ordinary route handler URLs * fix(app-router): normalize edge route handler URLs * docs(app-router): explain route URL parity * fix(app-router): preserve Node RSC request URLs * fix(app-router): normalize route handler RSC paths * fix(app-router): forward RSC tokens through rewrites --------- Co-authored-by: James <james@eli.cx> |
||
|
|
e018a08708 |
fix(pages-router): restore scroll across reload history traversal (#1905)
* fix(pages-router): restore scroll across reload history traversal
Pages Router pages with experimental.scrollRestoration currently expose window.next.router before the popstate runtime is installed and only keep Vinext scroll coordinates in the in-memory history state. After a reload, a back or forward traversal can run before the runtime is ready and has no durable scroll snapshot for the target history entry.
The violated invariant is that the Pages Router owns browser scroll restoration whenever the Next.js scroll restoration flag is enabled, with one stable history key per entry and routeChangeComplete emitted after the new page commits.
Resolve the Next.js config flag into process.env.__NEXT_SCROLL_RESTORATION, install the Pages Router runtime during next/router evaluation, persist Next-style sessionStorage scroll snapshots per history key, and render navigations through a stable commit boundary before emitting routeChangeComplete.
Regression coverage ports Next.js reload-scroll-backforward-restoration and verifies manual restoration plus exact scroll positions across reload, back, and forward traversal.
* fix(pages-router): sequence routeChangeComplete after scroll restoration and settle on render failures
* fix(router): guard against missing window.history in stampInitialHistoryState
* fix(router): defer hash scroll until after routeChangeComplete
Move hash scrolling out of renderPagesRouterElement's commit callback
and into the post-routeChangeComplete path, matching Next.js ordering:
x/y restoration happens during render commit, hash scrolling happens
after the completion event.
Also fix E2E test expectRouteChangeComplete to use one-shot event
listeners instead of checking for stale text, and add listener cleanup
to the fixture to prevent accumulation.
* fix(test): remove post-push deadlock and throw on missing events in E2E helper
expectRouteChangeComplete() now throws if window.next.router.events is
absent instead of silently resolving. Remove the redundant
expectRouteChangeComplete call after pushWithPagesRouter in the third
test — the awaited router.push already resolves after routeChangeComplete,
so registering a listener after it would deadlock.
* fix(test): replace as any casts with type-safe window.next narrowing in E2E scroll tests
* fix(router): narrow renderPagesRouterElement scroll param, drop dead hash branch
* fix(router): reset x/y scroll on hash routes before routeChangeComplete and cancel pending render on supersede
- Pass {x:0, y:0} through commit boundary for all scrollable
navigations, not just hash-less ones (hash scroll deferred to after
routeChangeComplete as before).
- Cancel pending render-commit Promise when a newer navigation starts,
preventing hang when the first root.render() commit is dropped by
React re-reconciliation.
- Add regression tests for both edge cases.
* fix(router): use SSR-safe layout effect in commit boundary and pin shallow scroll ordering
Address bonk review on #1905:
- swap the commit boundary's raw useLayoutEffect for the SSR-safe
useNonWarningLayoutEffect pattern from shims/image.tsx so Pages Router
SSR renders no longer log React's useLayoutEffect warning
- document that the default-noop commit boundary intentionally also wraps
SSR and initial hydration
- document the intentional shallow-scroll-before-routeChangeComplete
ordering and add a unit test pinning it
* fix(router): keep history-state scroll restore on popstate when scrollRestoration flag is off
Without experimental.scrollRestoration, popstate must still honour the
__vinext_scrollX/Y saved into history state on push, since a soft popstate
re-renders content the browser's native restoration can't position. Also
document why the popstate-time sessionStorage snapshot depends on
history.scrollRestoration = "manual".
* fix(router): guard render-commit canceller against late commits from superseded trees
* fix(router): defer current-history-key commit until beforePopState allows navigation
When beforePopState cancels a popstate the app stays on the previous
history entry, but _currentHistoryKey had already been updated to the
target key, skewing subsequent scroll-position bookkeeping. Move the
key commit below the beforePopState early-return, matching the
deliberate deferral of _lastPathnameAndSearch.
* chore(router): note intentional eager scroll snapshot; fix fixture link increment
- Document that the outgoing-entry scroll snapshot is deliberately written
before the beforePopState gate: on cancellation the app stays on
currentKey, so the saved value remains that entry's live scroll.
- Coerce id to a number in the pages-scroll-restoration fixture Link so
the href increments numerically instead of string-concatenating.
* fix(router): keep manual scroll restoration opt-in Pages-Router-only
Gate installManualScrollRestoration on the document not being an App
Router one (window.next.appDir === true): with the now-global
__NEXT_SCROLL_RESTORATION define, an App Router app that value-imports
next/router for compat would otherwise have its native browser scroll
restoration disabled at module eval. Upstream only flips
history.scrollRestoration inside the Pages Router constructor, which
never runs on App Router documents.
Also document the hash-only popstate path discarding the per-entry
__next_scroll_<key> snapshot as intentional upstream parity (change()'s
onlyAHashChange branch passes null to this.set and only scrollToHash
runs), and add unit coverage for both behaviors.
* ci: run pages-scroll-restoration E2E project in the CI matrix
Add the missing E2E matrix entry so the ported Next.js regression spec
(reload-scroll-backforward-restoration.spec.ts) actually executes in CI,
matching the shape of the sibling single-shard Pages Router entries.
Also document why the soft-popstate scroll fallback differs between
manual restoration on ({ x: 0, y: 0 }) and off (null -> native auto
restoration handles unstamped entries), per review.
* refactor(router): fold App Router gate into manualScrollRestoration constant
The popstate handler's sessionStorage save/read branches gated on the
module-level constant, which lacked the window.next?.appDir check that
installManualScrollRestoration used, leaving those branches relying
solely on the __N state filter to stay inert on App Router documents.
Fold the appDir check into the constant so every consumer is
Pages-Router-only by the same gate; the App Router bootstrap stamps
window.next.appDir before any client module evaluates, so the
module-eval-time check is reliable.
* test(pages-scroll-restoration): default getServerSideProps id to string "0"
Route params always arrive as strings, so the numeric 0 fallback was
inconsistent with the string values the component relies on
(Number(id) + 1 href, id === "error" check). Default to "0" for type
consistency.
* test(pages-scroll-restoration): align fixture deps and vinext import with pages-basic
Import vinext via the workspace package name instead of reaching into
dist by relative path, and declare the same react/react-dom/vinext
dependencies as the other fixtures so the fixture is self-describing
and the pnpm-lock.yaml importer entry is populated.
* fix(router): snapshot outgoing scroll on hash-only pushes
Upstream Next.js snapshots the outgoing entry's scroll into
__next_scroll_<key> inside Router.push() itself, before change()'s
onlyAHashChange short-circuit, so hash-only pushes still capture the
departed entry's position. Our hash-only push branch returned before
saveScrollPosition() ran, so a later back-popstate to that entry
restored {x: 0, y: 0} instead of the position the user had reached.
Save the outgoing scroll before updateHistory mints the new key,
matching the full-navigation push path and upstream ordering.
* docs(router): clarify manual scroll-target fallback chain precedence
* test(shims): tag scrollRestoration App Router test as eval-order guard
* docs(router): note missing-snapshot fallback intent at forcedScroll default
---------
Co-authored-by: James <james@eli.cx>
|
||
|
|
835e6b9943 |
fix(app-router): suppress redirect console errors in production onCaughtError (root-layout-redirect) (#1878)
* fix(app-router): suppress redirect console errors in production onCaughtError (#1830) When redirect() is called from a 'use client' root layout (or any client component), React catches the NEXT_REDIRECT sentinel via RedirectErrorBoundary and then calls the hydrateRoot onCaughtError callback. In dev mode vinext already filtered these via devOnCaughtError, but the production path had no onCaughtError at all — React's built-in fallback calls console.error, producing a spurious browser console error that fails the Next.js compat test assertion `expect(foundErrors).toBe(false)`. Fix: add prodOnCaughtError to app-browser-error.ts that silently drops navigation signal errors (NEXT_REDIRECT, NEXT_NOT_FOUND, NEXT_HTTP_ERROR_FALLBACK) and logs all other caught errors, preserving React's default behavior. Add fixture tests/fixtures/root-layout-redirect/ (dedicated 'use client' root layout with redirect()) and Playwright project root-layout-redirect (port 4184) with spec porting the Next.js root-layout-redirect > should work using browser. Part of #1830 * fix: include result/ page in root-layout-redirect fixture * fix: add lockfile entry for root-layout-redirect fixture, use prod server, import isNavigationSignalError Three fixes to unblock catastrophically-failing CI: 1. pnpm-lock.yaml: Add missing importer entry for tests/fixtures/root-layout-redirect. The fixture was added without updating the lockfile, causing ERR_PNPM_OUTDATED_LOCKFILE on every CI job during `vp install`. 2. playwright.config.ts: Switch the root-layout-redirect server from `npx vp dev` (dev mode) to `vinext build + vinext start` (production mode). The fix under test — prodOnCaughtError — is only active in production builds; running in dev mode exercised the pre-existing devOnCaughtError path and never touched the new code. 3. app-browser-error.ts: Replace the inline isNavigationSignalDigest helper with an import of isNavigationSignalError from utils/navigation-signal.ts. The module is dependency-free and already in the production browser bundle; inlining it reintroduced the drift risk that navigation-signal.ts was created to prevent. Addresses ask-bonk findings: blocking (test ran in dev mode) and maintainability (inline classifier duplication). * test: add prodOnCaughtError unit tests, run root-layout-redirect e2e in CI, scope gitignore negation |
||
|
|
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 |
||
|
|
e7757e48b7 | ci(bigbonk): reduce job timeout to 10 minutes (#1813) | ||
|
|
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. |
||
|
|
df066cb486 |
ci: optimize CI sharding (#1768)
* ci: add fifth integration shard
* ci: weight integration shards from timing data
* ci: shard app-router e2e
* ci: rerun optimization experiment
* ci: add sixth weighted integration shard
* ci: rebalance weighted integration shards
* ci: shard unit tests
* ci: rebalance integration shards from current timings
* ci: move weighted integration shard list from YAML into script + timing manifest
* ci: move weighted integration shard list from YAML into script + timing manifest
* feat(ci): derive integration shard weights from real CI timings with provenance
Integration shard weights lived in a hand-seeded flat path->ms map
("aggregation": "manual seed"). A reviewer could not tell a measured
number from a guess, and the guesses were wrong: favicon-short-circuit
was seeded at 5s but runs ~35s in CI across five runs, a 7x under-weight
that mis-packed the shards. The seed had no provenance and no way to
regenerate from real data.
Restructure the manifest to a v2 provenance model: per file estimateMs
(the weight the planner uses), plus medianMs/p75Ms/samples and a
generatedFrom.runs list, an estimator metric, and generatedAt. Add
scripts/ci-integration-timings-refresh.mjs to aggregate Vitest blob
reports downloaded from successful CI runs (p75 per file, nearest-rank)
and rewrite the manifest deterministically, failing closed when the
blobs do not cover every discovered file. The manifest here was
regenerated from 5 successful runs (30 blobs); all six shards now pack
to 84s.
Extract planning and blob parsing into scripts/lib/* so the fragile
Vite+ blob-parser probe lives in one place. Replace the O(files*shards)
lightest-group scan with an O(n log m) binary min-heap and collapse the
three duplicated local-search move/swap helpers into one makespanAfter +
transfer primitive. Behavior preserved: the --check gate still verifies
every file lands in exactly one shard.
Harden --check to fail closed on no discovered files, missing, stale,
malformed/zero/negative timings, shard-count drift, and bucket coverage.
Add an advisory --recommend mode that models the optimal shard count from
real weights and flags when integration has dropped below the competing
cross-job bottleneck. It is advisory only and never runs in CI; the count
stays declarative in manifest.shardTotal with the matrix enforced
against it.
* ci: pass integration shard file list via env to avoid template injection
The integration shard step expanded ${{ steps.shard.outputs.files }}
directly into the run: block. That output is a list of test file paths
discovered from `vp test list`, and on pull_request runs a filename is
attacker-controllable: a fork PR adding a file whose name contains shell
metacharacters would inject it into the runner shell. GitHub code
scanning (zizmor) flagged this as template-injection, alert 163.
Route the file list and the other computed values through env vars and
reference them in the script, leaving $SHARD_FILES unquoted so the shell
still word-splits it into separate file arguments. The shell now treats
the value as data, never as script text. Verified with zizmor: the
pre-fix workflow reports template-injection on this line, the fixed
workflow reports no findings.
* feat(ci): require refresh blobs to back the claimed --run provenance
The refresh tool recorded every --run id as provenance but only checked
that each discovered file had at least one timing sample. Passing five
--run ids with blobs for a single complete run still produced a manifest
claiming five-run provenance while every file held one sample. The
manifest could claim stronger provenance than the blob directory backs.
A test file runs in exactly one shard per run, so one complete run
yields exactly one sample per file. Require samples === runIds.length for
every discovered file: too few means a claimed run's blobs are missing,
too many means the directory holds blobs beyond the claimed runs.
--allow-partial relaxes the check to "at least one sample per file" for
the re-run-failed-shard case while still recording the true per-file
sample count.
* experiment: run integration at 5 shards to benchmark the latency/cost knee
Temporary, for benchmarking only. Repacks the same provenance weights
into 5 integration shards instead of 6 (manifest shardTotal and matrix
set to 5, Check gate updated to match) so the 5 vs 6 trade-off can be
measured with the same weights, unit split, and E2E split. To be
reverted to 6 after the run is captured.
* experiment: go aggressive on wall-clock (8 integration, 3 unit, 3 app-router E2E)
Runner minutes are free on this public repo, so the objective is pure
wall-clock. Attack the whole critical-path cluster at once: integration
to 8 shards (~63s test load each, near the per-file floor), unit to 3,
and the app-router E2E project to 3-way so none of them becomes the new
ceiling once the others drop. Report job left as-is. Benchmarking only;
final counts settle after the run lands.
* ci: set integration to 10 shards, the wall-clock floor on free CI
Public repo, so runner minutes are free and the objective is pure
wall-clock. At 10 shards each integration shard carries ~51s of test
load; combined with the serial report tail this brings the integration
critical path down to roughly where the un-shardable create-next-app
(windows) job sits, so additional shards stop moving the overall wall.
Keeps unit at 3 shards and the app-router E2E project at 3-way from the
prior step. Benchmarking continues; counts can still change.
* fix(ci): default refresh shard count to the existing manifest, not a constant
ci-integration-timings-refresh.mjs defaulted --shard-total to a hardcoded
6. The documented refresh command in ci.yml omits --shard-total, so once
the matrix moved past 6 shards, following the advertised workflow rewrote
shardTotal: 6 into the manifest and the next run failed the Verify
integration shard manifest step with shard-count drift.
Default to the current manifest's shardTotal instead. manifest.shardTotal
is the single source of truth for the count: the matrix mirrors it and
--check enforces no drift, so a plain refresh now preserves whatever the
matrix uses. An explicit --shard-total still overrides it for an
intentional count change, and a missing count with no existing manifest
now fails with a clear message instead of silently picking a number.
Found by Codex review on
|
||
|
|
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> |
||
|
|
e372ff336a |
test: rebalance app router integration shards (#1765)
Vitest integration CI shards by test file, so the 340-test App Router integration file becomes one oversized scheduling unit. That makes the slowest shard depend on one monolithic file even after adding native shards. Split the App Router integration coverage into focused files and increase the integration matrix to four native Vitest shards. This keeps fileParallelism disabled for shared-fixture safety while giving CI smaller scheduling units to distribute. Validation: vp check tests/app-router-*.test.ts vite.config.ts .github/workflows/ci.yml; vp test run --project integration tests/app-router-client-preloading.test.ts tests/app-router-dev-server.test.ts tests/app-router-external-rewrite.test.ts tests/app-router-font-google-prod.test.ts tests/app-router-isr-codegen.test.ts tests/app-router-malformed-url.test.ts tests/app-router-metadata-routes.test.ts tests/app-router-middleware-next-request.test.ts tests/app-router-next-config-codegen.test.ts tests/app-router-next-config-dev.test.ts tests/app-router-origin-check.test.ts tests/app-router-production-build.test.ts tests/app-router-production-server.test.ts tests/app-router-rsc-flight-hint.test.ts tests/app-router-rsc-plugin.test.ts tests/app-router-static-export.test.ts tests/app-router-worker-entry.test.ts |
||
|
|
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. |
||
|
|
b16b9ed055 |
chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#1720)
* chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * Apply suggestion from @james-elicx --------- 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> |
||
|
|
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> |
||
|
|
03845f52b2 |
ci(bonk): bump models to Opus 4.8 high and GPT-5.5 high, opencode 1.15.13 (#1708)
* ci(bonk): switch reviews to GPT-5.5 xhigh Bonk and BigBonk still routed review runs to Claude Opus 4.6 with the max variant. That leaves review commands and local opencode review agents on the old Anthropic model instead of the intended OpenAI xhigh setup. The workflows now route through Cloudflare AI Gateway to openai/gpt-5.5, request the xhigh variant, and pin opencode to 1.14.40 so the cf-ai-gateway reasoning_effort propagation fix from anomalyco/opencode#25573 is present. The viguy and reviewer agent frontmatter plus contributor docs now describe the same GPT-5.5 review setup. * ci(bonk): keep regular Bonk on Opus 4.7 The review model update should split regular Bonk and BigBonk. Regular Bonk is meant to use Claude Opus 4.7, while BigBonk should use GPT-5.5 with xhigh reasoning. Point the /bonk workflow back to the Anthropic Opus 4.7 model through Cloudflare AI Gateway and keep its max variant. Leave /bigbonk on GPT-5.5 xhigh, and update contributor docs to describe the two review paths. * ci(bonk): flip Bonk and BigBonk models The requested review model split is regular Bonk on GPT-5.5 xhigh and BigBonk on Claude Opus 4.6 max. Update only the Bonk workflow model and variant inputs. Leave the local opencode agent frontmatter and contributor docs unchanged from the existing PR branch. * ci(bonk): align agents with review workflows Bonk should run GPT-5.5 xhigh through the reviewer agent, while BigBonk keeps the viguy agent on Claude Opus 4.6 max. Route the Bonk workflow to the reviewer agent, set viguy's default model to Claude Opus 4.6, and use the same 0.2 temperature for both opencode agent definitions. * ci(bonk): use direct model ids in agent defaults OpenCode agent frontmatter should use the normal provider/model ids. The workflow inputs still need Cloudflare AI Gateway-prefixed model ids because the GitHub action routes through AI Gateway credentials. Set viguy back to anthropic/claude-opus-4-6 and reviewer to openai/gpt-5.5 while leaving the Bonk and BigBonk workflow model overrides unchanged. * Update CONTRIBUTING.md Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> * Update CONTRIBUTING.md Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> * ci(bonk): bump to Opus 4.8 high, GPT-5.5 high, opencode 1.15.13 * ci(bonk): bump models to Opus 4.8 high and GPT-5.5 high, opencode 1.15.13 * ci(bonk): bump models to Opus 4.8 high and GPT-5.5 high, opencode 1.15.13 --------- Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> |
||
|
|
8ae5abd56c | fix(app-router): preserve forwarded action redirect wrappers (#1582) | ||
|
|
217aecd84e |
fix(ci): grant contents:read to publish workflow's CI gate (#1620)
The publish workflow declares `permissions: {}` at the top level, which
revokes all permissions. When it invokes the reusable ci.yml workflow,
the called workflow inherits this restriction and cannot request
`contents: read` for actions/checkout, failing with:
The workflow is requesting 'contents: read', but is only allowed
'contents: none'.
Grant the permission explicitly at the ci job level so the reusable
workflow can use it.
|
||
|
|
601a394582 |
ci: address zizmor security audit findings (#1577)
* ci: address zizmor security audit findings
Run `zizmor .` and address all findings:
- Pin actions to commit SHAs with version comments (unpinned-uses).
Dependabot picks up the version comment for future bumps.
- Move `contents: write` / `id-token: write` to the publish job in
publish.yml; set `permissions: {}` at the workflow level
(excessive-permissions).
- Add `permissions: {}` to deploy-examples.yml workflow + scope
smoke-test job to `contents: read` (excessive-permissions).
- Move `${{ ... }}` expansions inside run blocks into `env:` vars
in publish.yml / benchmarks.yml / nextjs-deploy-suite.yml
(template-injection).
- Set `persist-credentials: false` on all checkouts that don't push;
publish.yml needs them for `git push origin v$VERSION`, so it
carries an inline `zizmor: ignore[artipacked]` instead.
- Keep dependabot's 1-day cooldown but annotate with `zizmor: ignore`
(intentional fast cooldown).
`zizmor .` is now clean.
* ci: add zizmor GitHub Actions workflow
Run zizmor on every push to main and every PR via the upstream
`zizmorcore/zizmor-action`. Findings surface in the GitHub Code
Scanning UI (Security → Code scanning alerts).
* ci(publish): explain zizmor artipacked ignore
|
||
|
|
fe700996d9 |
ci: extract Playwright cache plumbing into a composite action (#1438)
Two workflows now share an identical playwright-version + cache-restore pattern: ci.yml (which owns the cache, used by E2E jobs) and nextjs-deploy-suite.yml (which restores it read-only, added in #1435). Pull the duplication into .github/actions/playwright-cache so the cache key format can't drift between the two callers — if it ever does, the deploy-suite stops hitting ci.yml's cache and the upstream Playwright extraction hang from 2026-05-22 (runs 26265986264 .. 26281758803) reappears. The composite action: - Reads the playwright version from a configurable lockfile path (`lockfile` input, defaults to pnpm-lock.yaml). - Restores ~/.cache/ms-playwright keyed `playwright-${browser}-${runner.os}-v${version}`. - Supports two modes: `cache` (restore + save, default — for the workflow that owns the cache) and `restore` (read-only — for downstream consumers that don't want to fight over the save). - Exposes `cache-hit`, `version`, and `key` outputs so callers can gate install steps or use the version for sibling cache keys (e.g. ci.yml's apt-archives cache for WebKit deps). No behavioural change — same cache keys, same restore-keys, same ~/.cache/ms-playwright path. |
||
|
|
68ae68a57c |
ci(deploy-suite): restore Playwright browsers from ci.yml's cache (#1435)
Follow-up to #1428 and #1430. The previous two PRs added log markers and skipped the wedged transitive postinstall, but the underlying problem remained: the explicit `pnpm playwright install chromium chromium-headless-shell` in the prepare script hits the same upstream extraction hang (download to 100%, then no progress until step timeout). What's actually different between the runs that worked (May 19-21) and the runs that hang (May 22): nothing on our side. Same Chrome 145.0.7632.6, same Playwright 1.58.2, same Node 24.16.0, same runner image fleet. On the May 21 success the extraction took 2.7 seconds after 100% download; on May 22 it never completes (>9 min, killed by the 15-min step timeout). Some upstream change — bad bytes from cdn.playwright.dev, an Azure storage hiccup affecting fsync, a Playwright installer interaction we can't see from here — is wedging extraction. We can't fix it from this repo. What we *can* do: stop downloading. vinext's own ci.yml already keeps a `playwright-chromium-Linux-v1.58.2` cache hot (261 MB, last accessed within minutes on every CI run today). The deploy-suite has never used this cache — its only cache (the post-build `Save build artifacts` step) is keyed per-run, so every nightly starts with an empty ~/.cache/ms-playwright and re-runs the install that's now hanging. Add a restore step before the prepare that uses ci.yml's exact key format. On hit, `pnpm playwright install` sees the browsers already present and is a fast no-op. On miss (e.g. Next.js bumps Playwright before vinext catches up), we fall through to the existing behavior. Restore-only: ci.yml owns the save side. |
||
|
|
0a8c3819ab |
chore(deps): bump cloudflare/wrangler-action from 3 to 4 (#1433)
Bumps [cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action) from 3 to 4. - [Release notes](https://github.com/cloudflare/wrangler-action/releases) - [Changelog](https://github.com/cloudflare/wrangler-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/cloudflare/wrangler-action/compare/v3...v4) --- updated-dependencies: - dependency-name: cloudflare/wrangler-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
aa7730ca6d |
chore(deps): bump actions/download-artifact from 7 to 8 (#1432)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
47f697c98e |
chore(deps): bump actions/github-script from 7 to 9 (#1431)
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 9. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v7...v9) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '9' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: James Anderson <james@eli.cx> |
||
|
|
4fba647a2c |
ci(deploy-suite): skip wedged playwright-chromium postinstall (#1430)
Follow-up to #1428. The previous PR added log markers, which revealed the actual hang: `playwright-chromium`'s postinstall (`node install.js`, triggered transitively by the Next.js workspace install) reaches 100% on the 167 MiB Chrome zip download from cdn.playwright.dev, then never prints "Done" — extraction/verification silently wedges. Every other postinstall in the same install (cpu-features, sharp, esbuild, etc.) finishes cleanly. Confirmed on runs 26265986264, 26279790373, and the post-#1428 retry 26281061094 — same fingerprint each time. Set `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` on the Prepare step so the transitive `playwright-chromium` postinstall becomes a no-op. The explicit `pnpm playwright install chromium chromium-headless-shell` that runs later in `scripts/run-nextjs-deploy-suite.sh` provisions the browsers we actually use at test time, populating ~/.cache/ms-playwright which is cached and restored by the test shards. |
||
|
|
37cfeb11ce |
ci(deps): add dependabot config with 1-day cooldown (#1429)
* ci(deps): add dependabot config with 1-day cooldown Mirrors pnpm-workspace.yaml's `minimumReleaseAge: 1440` (24h) so Dependabot won't open PRs for versions younger than the cooldown window we already apply to local installs. * ci(deps): run dependabot daily instead of weekly |
||
|
|
b203fa7fd6 |
ci(deploy-suite): unstick Next.js prepare step (#1428)
The nightly deploy suite hung for 29m+ in "Prepare Next.js checkout" on two consecutive runs (26265986264, 26279790373), eating the 30m job timeout. Same v16.2.6 SHA (ee6e79b1) built fine the three previous nights, so it's environmental, not source drift. The first `pnpm install` completes around the 30s mark and then logs go silent until cancellation. With no per-command markers, we couldn't tell which of the remaining commands actually hangs. This patch: - Drops `--with-deps` from the build-job playwright install. It runs `apt-get install` for browser system libraries, which is by far the most plausible silent-hang culprit (stuck dpkg lock / slow apt repo). The 16 test-shard runners each spin up a fresh ubuntu-latest and already run `playwright install` without `--with-deps` — so the apt state never transferred anyway. Doing it on the build runner is wasted work even when it doesn't hang. - Removes the redundant second `pnpm install`. The workspace symlinks survive the build step; nothing between the build and re-install adds new packages. - Adds timestamped `>>> ...` markers around each prepare command so a future hang is diagnosable from the logs. - Adds `timeout-minutes: 15` on the Prepare step so a future hang fails fast (well under the 30m job cap) instead of starving the whole job. |
||
|
|
f27c40a77f |
chore: split e2e compatibility by App Router vs Pages Router (#1321)
* feat(compat): split e2e compatibility by App Router vs Pages Router
The /compatibility page previously showed one undifferentiated grid of
~1000 Next.js test files. This change classifies each test by which
router(s) its fixture exercises (App, Pages, both, or unknown) and
surfaces the breakdown in the UI.
How:
- A new `router` column on `compat_file_results` (enum: app | pages |
both | unknown). Defaults to 'unknown' so pre-classifier rows still
render — they just show up under 'Other'.
- A new `scripts/classify-nextjs-suites.mjs` walks each test's fixture
directory and looks for app/page.tsx, app/route.ts, app/layout.tsx,
pages/*.tsx (excluding _app / _document / _error specials). A fixture
with real routes in both folders is classified as 'both' — these are
the genuine parity tests and counting them only once would hide a
router-specific failure.
- Edge cases handled in the classifier:
* pageExtensions naming (layout.page.tsx → layout)
* Suites whose .test.ts lives in a test/ subdir alongside the fixture
* Inline-fixture suites under test/e2e/app-dir/ (no on-disk routes,
but path convention says App Router)
* APP_ROUTER_NON_APP_DIR_SUITES curated override
* Skips node_modules and .next when scanning fixtures
- The nightly workflow now runs the classifier in the build job once
per run (the cheap part — happens with Next.js still on disk),
uploads the suite → router map as an artifact, and the report job
joins it into the ingest payload before POSTing.
- The compatibility page gains a 'By router' card row showing per-router
pass rates and file counts (parity tests are counted toward both
router buckets — adding them exceeds the total, see the explainer
card). The contribution grid grows filter chips above it for
interactive narrowing.
Verified:
- vp check (format + type + lint) clean
- 14 new classifier unit tests pass (vp test run tests/classify-nextjs-suites.test.ts)
- apps/web build:vinext succeeds end-to-end
- Local classifier run against .nextjs-ref reports
app=576 / pages=325 / both=113 / unknown=22 out of 1036 suites, with
spot-checked classifications matching expectations.
Backward compatibility: the ingest endpoint treats `router` as optional;
existing workflow runs and historical rows continue to function and
render as 'Other' until the next nightly classifies them.
* refactor(compat): store router classification in its own table
Replaces the inline `router` column on `compat_file_results` (introduced
in the previous commit) with a dedicated `compat_suite_meta` table,
classification keyed by `suite`. The /compatibility UI LEFT JOINs the
two tables at query time.
Rationale (per design discussion in PR #1321):
- Classifications conceptually describe test files, not test runs. Storing
them per-row coupled their cadence to results ingestion, which is wrong
when (a) the Next.js ref bumps and re-classifies everything, (b) an
override fix lands without re-running tests, or (c) a partial test run
still wants fresh classifications.
- One row per suite (PK on `suite`) means re-classifying is an upsert,
not a backfill loop. Provenance (`next_ref`, `classified_at`) is
stored on the row for debugging.
- Decoupling lets the workflow POST classifications from the build job
immediately after running the classifier, without round-tripping through
the report job. Results ingestion stays focused on results.
Endpoints:
- `POST /api/compatibility` no longer accepts `router` per file
(reverted to its pre-PR shape).
- `POST /api/compatibility/classify` (new): accepts
`{ nextRef, classifiedAt?, suites: [{ suite, router }] }`,
upserts in chunks (25 rows per INSERT to fit SQLite's variable cap),
shares auth with the results endpoint via a new `_auth.ts` helper.
Workflow:
- The build job now POSTs the classification map directly to the new
endpoint after running the classifier. Same guardrails as the results
POST (only full-suite, only against main).
- The report job no longer needs to download / merge the classification
artifact — it's already in D1 by the time results land.
- The classification JSON is still uploaded as a workflow artifact for
manual re-submission / debugging.
Schema:
- New table `compat_suite_meta(suite PK, router, next_ref, classified_at)`
with an index on `router` for the per-router count queries.
- `compat_file_results` reverts to its pre-PR columns. No data migration
needed because the column was only ever populated on this branch.
Trade-off: classification changes are now retroactive — re-classifying a
suite updates how it appears in every historical run. This is usually
what you want (corrections heal the whole history) but means the trend
chart isn't a strict point-in-time record. The provenance fields on the
meta row let you tie a reclassification back to a specific Next.js ref.
Verified:
- vp check clean
- 14 classifier unit tests still pass
- apps/web build:vinext succeeds; /api/compatibility/classify shows up
in the route list
* refactor(compat): drop redundant next_ref from compat_suite_meta
The Next.js ref a classification was produced against is already
recoverable from the run history — compat_runs records next_ref per
run, and the most recent classification's ref is implicit (it's the
ref of the most recent classify POST, which the workflow always pairs
with a run).
The future-proofing case for per-ref classifications would want a
composite (suite, next_ref) PK rather than a single global row anyway,
so this column doesn't help with that scenario either. classified_at
is enough for the debug case ('when was this last computed?').
Changes:
- Drop next_ref column from compat_suite_meta (3 columns now:
suite PK, router, classified_at)
- /api/compatibility/classify no longer requires nextRef in the body
- Workflow no longer passes NEXT_REF when building the classify payload
- Migration regenerated as 0001_romantic_skullbuster.sql (one fewer
column on the CREATE TABLE)
- Chunk size bumped to 33 rows/INSERT (100-var cap / 3 columns)
Verified: vp check clean, classifier tests pass, apps/web build succeeds.
* feat(compat-ui): share router filter between grid and trend chart
Lifts the router-filter state out of ContributionGrid into a new
CompatibilityViews client wrapper that owns the Kumo segmented Tabs
control. The grid and the line chart both consume the active filter
as a prop, so changing the tab updates both visualisations in lockstep.
The line chart now plots per-router series. The trend query was
rewritten to aggregate via JOIN against compat_suite_meta — one row
per run with app/pages/both/unknown rollups in a single round-trip,
~90 runs × ~1000 file rows over an indexed join. The TrendPoint
carries all five series; the chart picks one based on the filter
without re-fetching.
Other UI tweaks in this commit:
- Replaced the hand-rolled pill row with Kumo Tabs (variant='segmented',
size='sm'). Fixes a weird active state (bg-kumo-default + text-kumo-base
was using the text color for backgrounds, producing a saturated
inversion).
- 'Parity' → 'Mixed' in user-visible labels. Internal identifiers
(DB enum 'both', bucket variable 'parity') unchanged.
- Removed the standalone 'Other' stat card; the segmented Tabs still
expose the 'Other' filter so unclassified suites are reachable.
- Reorganised the page into one 'Test files and trend' section
containing both visualisations, gated by the shared Tabs.
- .gitignore: ignore .dev.vars (Wrangler's local-secrets convention)
so the COMPAT_INGEST_SECRET we use for local testing never gets
committed by accident.
Verified:
- vp check clean (format + type + lint)
- 14 classifier unit tests pass
- apps/web build:vinext succeeds; both API endpoints + page render
* fix(compat): address PR review comments
Addresses actionable items from the two /bigbonk review passes:
1. scanFixture: skip recursion into already-checked app/ and pages/
(with a wrinkle around fixture wrappers).
The previous code unconditionally pushed every child onto the walk
stack, which (a) wasted work re-walking subtrees the route checker
already covered and (b) risked a false positive on App Router route
groups literally named 'pages' (e.g. app/pages/index.tsx would have
tripped the Pages Router detector).
Naive fix (skip descent into any app/ or pages/) regressed ~10 real
Next.js fixtures that use a wrapping directory literally named 'app'
as the test app's project root, with the real app/ and pages/ nested
inside (test/e2e/og-api, test/e2e/middleware-static-files, etc.).
Final fix: a directory named app or pages is treated as a fixture
wrapper (and recursed into) if it contains a top-level next.config.*
OR an inner app/ alongside an inner pages/. Otherwise it's handed to
the route checker, which either finds real routes or returns nothing
and the walk skips it. The wrapper detector intentionally does NOT
treat middleware.{js,ts} as a wrapper signal, because Next.js tests
put noop middleware.js files inside real App Router app/ to assert
the file is ignored at that level (test/e2e/app-dir/app-middleware).
Verified against the full test/e2e/ tree of the local Next.js
checkout: 1036 suites classify identically to the pre-fix output
(576 app / 325 pages / 113 both / 22 unknown), with the
app/pages/index.tsx false-positive now correctly handled.
2. list-nextjs-e2e-suites.mjs: add the same import.meta.url guard the
classifier already has, so importing the module programmatically
doesn't immediately parse process.argv and write a file.
3. _auth.ts: early-exit when the X-Compat-Secret header is missing
or empty, before paying two SHA-256 digests. The constant-time
guarantee we care about (don't leak the contents of the expected
secret via length or prefix matching) is preserved — we only
short-circuit on values we already know can't match a non-empty
secret.
4. Migration 0001_romantic_skullbuster.sql: add trailing newline.
Tests:
- 3 new regression tests added (18 total, all pass):
* fixture wrapper with next.config.js + inner app/ + pages/
* fixture wrapper with next.config.js + inner pages/ only
* App Router app/ containing a route group named 'pages'
* App Router app/ containing a noop middleware.js
- vp check clean
- Classifier output diff against current Next.js HEAD: empty
* fix(compat-ui): unexport cellMatchesFilter (knip)
CI's knip step flagged `cellMatchesFilter` as an unused export. It's only
used inside contribution-grid.tsx itself — the shared wrapper has its
own bucketing logic in compatibility-views.tsx. Drop the `export`.
* fix(compat): address third-pass review comments
1. sqlExcluded: narrow parameter type to a closed union
("router" | "classified_at") so the no-user-input invariant for
sql.raw is compiler-enforced rather than relying on a code comment.
2. ContributionGrid: gate the SVG render on visibleCells.length > 0,
and also clamp svgWidth/svgHeight to >= 0. Previously, when a
filter emptied the grid, the SVG rendered with width=-3/height=-3
alongside the placeholder div (browsers clamped silently but it
was invalid SVG).
3. compatibility/page.tsx 'How this works' card: add a short note
that per-router trend lines use the latest classification, so
reclassifying a suite updates how it appears in historical runs.
The aggregate "All" line is unaffected. Avoids a confusing
support question if users notice old per-router numbers shift
after a classifier improvement.
4. scanFixture readability: simplify the early-return checks from
'if (hasApp && hasPages)' to 'if (hasPages)' / 'if (hasApp)',
since the other flag was just set on the line above. No
behavioural change; classifier output against the full Next.js
test/e2e tree is byte-identical (1036 suites, 576/325/113/22).
5. /api/compatibility/classify body validator: bounds-check
classifiedAt. Reject NaN/Infinity and any timestamp earlier
than 2023-01-01 UTC. Catches the common seconds-vs-milliseconds
mistake and accidental 0/-1 values from buggy callers. The
only legitimate caller (the GH workflow) sends Date.now() so
this is purely defensive.
Verified:
- vp check clean (format + type + lint + knip)
- 18 classifier unit tests pass
- apps/web build:vinext succeeds
- Classifier output against current Next.js HEAD is unchanged
* refactor(compat-ui): extract shared router bucketing module
Addresses items 1, 3, and 4 from the fourth review pass.
1. Trend GROUP BY note (item 1): added an inline comment explaining
that selecting r.created_at while grouping by r.id only is a SQLite
functional-dependency affordance. Standard SQL (postgres etc.)
would reject it; the comment heads off a future 'fix' that breaks
the query if it's ever ported.
2. Unified router bucketing (item 3): the three places that sliced
cells by router filter all used slightly different naming
conventions ('parity' vs 'both', 'other' vs 'unknown'). Extracted
the canonical logic to a new ./router-buckets module with one
shared vocabulary (the same as RouterKind), and updated all three
call sites:
- contribution-grid.tsx: cellMatchesFilter from shared module
- compatibility-views.tsx: countByFilter for tab labels
- page.tsx: bucketByRouter + bucketPassRate for stat cards,
byRouter.parity/other renamed to byRouter.both/unknown
Doc comment at the top of router-buckets.ts pins the
'Mixed counts in both app and pages' rule in one place. No
behavioural change.
3. passRate function rename (item 4): renamed the line chart's local
ratio helper from passRate -> computePassRateRatio to avoid
shadowing the passRate variable in page.tsx (and to distinguish
it from bucketPassRate in router-buckets which returns a
percentage, not a ratio).
Not addressed (intentionally):
- 'Mixed (both)' vs 'Parity'/'Interop' label (item 2): the user
explicitly chose 'Mixed' over 'Parity' in an earlier message.
- trendRowsDesc rename (item 5): reviewer self-tagged 'very minor';
renaming would muddle the diff.
- deriveSuiteGroup inconsistent levels (item 5b): pre-existing
tooltip-only behaviour, not introduced by this PR.
Verified:
- vp check clean (format + type + lint + knip)
- 18 classifier unit tests pass
- apps/web build:vinext succeeds
|
||
|
|
7075d210e7 |
feat(cli): native App Router route type generation (#1144)
* Add native App Router route typegen * Address route typegen review feedback * Respect typegen target directory * Require explicit LayoutProps route * Serialize route type regeneration * Derive layout params from route graph * Scope layout slot typegen by route group * Map typegen slots to owner layouts * Preserve slot-local layout type keys * Generate next-env.d.ts during typegen * Address review feedback for route typegen * Defer route sort and tighten typegen helpers * Stabilize Windows create-next-app smoke check * Address route typegen review fixes * Match Next route type import * Stabilize Windows create-next-app smoke * Skip redundant route param shape writes * Defer dev route type generation |
||
|
|
746d6b5970 | chore(ci): bump Node to 24 for native URLPattern support (#1283) | ||
|
|
3e8db450f4 |
ci(nextjs-deploy): add test-path filter input (#1280)
Lets workflow_dispatch runs target a single suite (e.g. test/e2e/edge-async-local-storage) instead of the full ~16-shard run, shortening the iteration loop when investigating a specific failure. The compat-ingest step is gated off for path-filtered runs so partial snapshots don't drag the historical pass-rate trend. |
||
|
|
c7453909e2 |
fix(workflow): use correct workers.dev URL for compat ingest (#1241)
* fix: use vinext-web.vinext.workers.dev as the ingest URL The default URL I picked (cloudflare.workers.dev) was a guess — workers *.workers.dev URLs are namespaced under the account subdomain, which for this account is vinext.workers.dev (see scripts/smoke-test.sh and the deploy-examples workflow). curl was getting NXDOMAIN. Still overridable via the COMPAT_INGEST_URL repo variable for custom domains later. * fix(workflow): also archive compat-ingest.json in the report artifact When the live POST to /api/compatibility fails (DNS, expired secret, rotated secret, etc.) we previously had no way to recover the run without re-running the 40-minute suite. The ingest payload was being generated but only the human-readable deploy-suite-report.json was uploaded. Add compat-ingest.json to the same artifact so a maintainer can download it and curl it through manually. |
||
|
|
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 |
||
|
|
600580eec0 |
ci(e2e): cache WebKit apt archives and skip browser install on cache hit (#1228)
WebKit E2E jobs were dominated by two costs: 1. `playwright install --with-deps webkit` re-downloaded the browser binaries (or at least re-validated them) on every run even when the browser cache was hit, because the install step was unconditional. 2. The `--with-deps` flag always ran `apt-get install` for WebKit's system libraries (~30+ packages), and apt re-downloaded the .deb files on every run since runner archives aren't preserved across runs. This commit: - Splits browser install from system dep install so each can be cached on its own schedule. - Gates the browser install on `cache-hit`, so cached runs skip the Playwright CLI entirely on the hot path. - Adds a second cache for `/var/cache/apt/archives` (mirrored through `~/.cache/apt-archives`) so apt finds previously downloaded .deb files and skips redownloading them. The install itself still runs every time, but the slow network portion goes away. - Keys both caches on the resolved Playwright version from pnpm-lock.yaml rather than the full lockfile hash, with a fallback `restore-keys` so unrelated lockfile churn doesn't invalidate the cache. Chromium jobs also benefit from skipping the Playwright CLI on cache hits. |
||
|
|
bc8cee2385 |
fix(deploy-suite): extract CJS->ESM converter to standalone script (#1209)
* fix(deploy-suite): extract CJS->ESM converter to standalone script
The CJS->ESM next.config converter was inlined into scripts/e2e-deploy.sh
via 'node -e ...' with bash single-quote escapes ('"'"'). The escaping
rebalanced quotes such that later code (a comment containing '(e.g.') sat
outside any single-quoted bash region, causing 'bash -n' to fail with:
scripts/e2e-deploy.sh: line 490: syntax error near unexpected token `('
The script died before doing anything in every deploy-suite shard, producing
2480 identical 'Custom deploy script failed: undefined (2)' failures and a
misleading 9.5% pass rate (almost all 'passes' were 'should skip next deploy'
no-ops). See run 25869359604.
- Extract the converter to scripts/cjs-to-esm-config.mjs (same regex
pipeline, no behavioural change).
- Replace the inlined 'node -e' block with a plain 'node ./script.mjs'
invocation.
- Add a 'bash -n scripts/*.sh' gate to the Check job so a future regression
here can't ship green.
* Apply suggestion from @james-elicx
|
||
|
|
843df83de9 |
ci(deploy-suite): default to all suites and disable test retries (#1208)
The Next.js deploy suite defaulted to only running app router tests with 3 attempts per test (run-tests.js DEFAULT_NUM_RETRIES = 2). Switch the default suite-filter to 'all' so nightly runs cover pages router too, and pass --retries 0 so failures surface on the first attempt instead of masking flakes behind retries. |
||
|
|
747bb0ec76 |
ci: add Next.js deploy suite harness (#1189)
* ci: add Next.js Pages Router deploy suite harness Add the workflow, scripts, and manifest generator for running the Next.js Pages Router deploy test suite against vinext. Triggers: - Nightly cron (06:00 UTC) against main - Manual dispatch with configurable vinext-ref, next-ref, and concurrency Extracted from #922. * refactor: align deploy suite harness with Next.js adapter testing docs - Rename scripts to match Next.js convention: e2e-deploy.sh, e2e-logs.sh, e2e-cleanup.sh - Split workflow into build + test jobs (build once, shard 16 ways) per the documented adapter testing pattern - Generalize workflow from pages-only to configurable: add suite-filter input (pages/app/all, defaults to pages) - Add --filter flag to manifest generator (replaces the pages-hardcoded version) - Accept ADAPTER_DIR (Next.js docs convention) alongside VINEXT_DIR - Rename workflow file to nextjs-deploy-suite.yml - Use matrix.group strings ("1/16") instead of shard integers, matching the docs example * ci: default suite-filter to app * ci: bump Next.js default to v16.2.6, move cron to 02:00 UTC * address review: add report job, clean up cache paths, document compat shims - Add a report job that aggregates .results.json across all shards into a GitHub Actions summary (pass/fail/skip counts, failed test details, passed suite list) and uploads a JSON report artifact - Upload test results from every shard (not just on failure) - Remove redundant next.js/ from cache paths (already under .) - Add comment explaining why IS_TURBOPACK_TEST=1 is needed - Clean up stale PID file in e2e-cleanup.sh - Add header comments to all scripts explaining their purpose and contract - Document the hardcoded warning messages and .next/trace shim - Document the YAML catalog parser assumptions - Fix [[ ]] -> [ ] inconsistency in run-nextjs-deploy-suite.sh * ci: add temporary PR trigger to test workflow [remove before merge] * fix: address deploy script failures found in CI and local testing Bugs found and fixed: - IMMUTABLE_ASSET_TOKEN marker renamed to NEXT_SUPPORTS_IMMUTABLE_ASSETS to match what next-deploy.ts parseIdsFromCliOuput() actually parses. Without this, every test fails with 'Failed to get supportsImmutableAssets'. - Deploy script now injects "type": "module" into test app package.json and generates a vite.config.ts. Without type:module, Rolldown emits .mjs files but the RSC plugin's cross-environment imports expect .js. This mirrors what `vinext init` does (steps 3 and 5). - Added vp (Vite+) fallback to run_pnpm() for environments where pnpm and corepack are not available but vp is. - Cleanup script now uses process group kills (kill -TERM -PID) and a port-based lsof fallback to handle orphaned child processes from vp/pnpm exec wrappers. - Error cleanup trap now dumps the last 80 lines of the build log and last 40 lines of the server log to stderr, so failures are visible in CI output (previously only showed file sizes). - Test job uses run-install: false since the workspace is already built in the cache from the build job. Tested locally: deploy script succeeds, curl returns valid HTML, logs script outputs all three required markers (BUILD_ID, DEPLOYMENT_ID, NEXT_SUPPORTS_IMMUTABLE_ASSETS), cleanup kills the server. * fix: use vinext init in deploy script, add next.config.js to CJS rename list Replace the manual type:module / vite.config.ts / CJS rename logic in e2e-deploy.sh with a single `vinext init --skip-check --force` call. This runs after pnpm install (so deps are available) and before vinext build. Also add next.config.js to the CJS_CONFIG_FILES list in project.ts. Without this, vinext init adds type:module but doesn't rename CJS next.config.js files, causing 'module is not defined in ES module scope' errors. This was the root cause of the CI failures — Next.js test fixtures use module.exports in next.config.js. All 61 init tests pass. * fix: convert CJS next.config.js to ESM instead of renaming to .cjs Next.js doesn't support next.config.cjs, so we can't rename it. Instead, convert module.exports/require() to export default/import in-place after vinext init adds "type": "module". Also revert adding next.config.js to CJS_CONFIG_FILES — vinext init should not rename it since it's not a generic config file. Tolerate pnpm 10+ ERR_PNPM_IGNORED_BUILDS exit code 1 on install (the install completes, packages are in node_modules, but pnpm exits non-zero due to unapproved build scripts). Verify node_modules/vinext exists as a safety check. Tested against the real Next.js actions-streaming fixture: CJS next.config.js converted, app builds, server starts, HTTP 200. * fix: emit both IMMUTABLE_ASSET_TOKEN and NEXT_SUPPORTS_IMMUTABLE_ASSETS v16.2.x parses IMMUTABLE_ASSET_TOKEN, canary renamed it to NEXT_SUPPORTS_IMMUTABLE_ASSETS. Emit both for cross-version compat. * fix: enable JSX in .js files, convert CJS next.config.ts to ESM Add vinext:jsx-in-js transform plugin to handle JSX in plain .js files. Next.js allows JSX in .js files (Babel/SWC handle it transparently), but Vite 8's built-in vite:oxc plugin excludes .js files by default (exclude: /\.js$/) AND infers lang: 'js' from the extension (which disables JSX parsing). Neither can be fixed via config alone. The plugin runs with enforce: 'pre' before vite:oxc, transforming .js files with OXC using lang: 'jsx' so JSX is compiled before vite:oxc sees the code. This unblocks ~60 test suites in the deploy suite. Also extend the deploy script's CJS-to-ESM config converter to handle next.config.ts files (not just .js), fixing ~6 more test failures. Tested: 1284 tests pass (jsx-in-js + shims + routing + app-router), zero regressions. * ci: trigger clean workflow run * fix: improve CJS-to-ESM converter for next.config files The naive regex converter missed two patterns: - require('mod')(args) produced invalid ESM (import X from 'mod'(args)) - require() calls inside function bodies were not converted New converter handles: - module.exports = X → export default X - const X = require('mod') → import X from 'mod' - const X = require('mod')(args) → import _X from 'mod'; const X = _X(args) - require('mod') in expressions → (await import('mod')).default Tested against bundle-analyzer, path, and inline require patterns. * . * Apply suggestion from @james-elicx * fix: handle destructured requires in CJS converter, read port from file in cleanup - Add pattern for const { a, b } = require('mod') → import { a, b } from 'mod' - Read port from PORT_FILE in cleanup_on_error trap instead of using $PORT variable which may be unset if the script fails before port allocation - Add TODO noting remaining CJS edge cases (dynamic require, require.resolve) * Apply suggestions from code review Co-authored-by: James Anderson <james@eli.cx> * Apply suggestion from @james-elicx |
||
|
|
9126344cd8 |
fix: use loader for client reference (#1141)
* fix: split client chunk * add test * fix ci deps * install deps * use loader * tweaks |