mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
codex/cacheability-platform-io
74 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e2b9145fc9 | fix(cloudflare): discover prewarm paths from staged worker | ||
|
|
c1a78a73d8 |
fix(build): support trailing-slash static exports (#3081)
* fix(build): support trailing-slash static exports * fix(ci): initialize static export preview routes * fix(build): emit trailing-slash 404 fallback * docs(build): clarify static export page count |
||
|
|
4146d69021 |
fix(cloudflare): verify Worker version IDs during CDN warmup (#3072)
* fix(cloudflare): revalidate stale warmup retries * fix(cloudflare): report CDN warmup retry progress * test(cloudflare): retry prewarm verification on fresh Worker * feat(cloudflare): configure CDN readiness probes * fix(cloudflare): validate CDN warmup version overrides * fix(cloudflare): verify CDN warmup version ids * test(cloudflare): opt into dangerous warmup fallback * fix(cloudflare): harden warmup version validation * docs(cloudflare): clarify version binding environments |
||
|
|
e2e2b177eb |
fix(cloudflare): complete warmup response and promotion contracts (#3046)
* test(cloudflare): prove deployed Pages CDN warmup * fix(cloudflare): reject incomplete no-promote warmup * fix(cloudflare): keep warm discovery fail-closed * test(cloudflare): prove prewarmed App HTML reuse * fix(cloudflare): enforce no-promote warmup contract * fix(cloudflare): warm terminal ISR responses |
||
|
|
369e031883 |
test(cloudflare): preserve independent RSC warm proof (#3020)
* test(cloudflare): preserve independent RSC warm proof * test(cloudflare): allow cache propagation after promotion |
||
|
|
8bb09481d6 |
feat(cloudflare): prewarm canonical ISR RSC requests (#3002)
* feat(cloudflare): prewarm canonical ISR RSC requests * fix(cloudflare): forward preview deploy options * fix(cloudflare): harden canonical RSC prewarming * fix(test): wait for preview alias propagation * fix(cloudflare): guarantee exact RSC prewarm reuse * fix(cloudflare): harden canonical RSC prewarming * fix(cloudflare): preserve definitive RSC warm identity * fix(cloudflare): warm immutable worker versions * test(form): expect Next-compatible absolute navigation * fix(cloudflare): harden RSC prewarm deployment * test(cloudflare): stabilize deployed RSC proof * test(cloudflare): await RSC prefetch completion * test(cloudflare): isolate RSC readiness probes * fix(cloudflare): warm RSC through version overrides * fix(cloudflare): parse labeled Worker version IDs * fix(cloudflare): warm document requests as HTML * fix(cloudflare): retry staged version propagation * test(cloudflare): seed prewarm proof with prior Worker * fix(cloudflare): await version override propagation * fix(cloudflare): serialize staged cache warming * fix(cloudflare): verify warmed RSC build identity * fix(cloudflare): harden staged RSC cache warming * fix(cache): keep RSC prewarm identity build-scoped * fix(cloudflare): gate warmup on uploaded build * fix(cache): certify canonical RSC warmup responses * fix(cache): harden canonical RSC warmup propagation * fix(cache): close RSC warmup review gaps * fix(cache): preserve explicit response cache policies * feat(cloudflare): prewarm canonical RSC variants without prerendering * fix(cloudflare): forward deploy config from CLI * fix(cache): close RSC prewarm reuse gaps * fix(cloudflare): parse labeled Worker version IDs * fix(router): preserve canonical prefetch invalidation * test(cloudflare): align RSC seed Worker bindings * fix(cloudflare): scope warmup deadlines per request * test(cloudflare): avoid caching stale warmup seed * fix(cloudflare): fail permanent warmup errors promptly * fix(router): keep contextual RSC requests hashed * test(cloudflare): await promoted warmup Worker * test(cloudflare): retry browser warmup source * revert: restore focused warmup deadline fix * fix(cloudflare): remove warmup propagation deadline * fix(cloudflare): allow slower warmup responses * fix(cloudflare): confirm warmed cache hits * fix(cloudflare): confirm cache hits without backoff * fix(cloudflare): avoid duplicate cache warm requests * test(cloudflare): allow prewarm propagation * test(cloudflare): isolate loading prewarm propagation * test(cloudflare): await promoted prewarm worker * fix(cloudflare): wait for warm cache propagation before promotion * test(cloudflare): allow promoted warm cache to settle * test(cloudflare): wait after promoted build is reachable * test(cloudflare): poll deployed version before warm cache checks * test(cloudflare): trace warm cache identity * test(cloudflare): trace prewarm verification location * test(cloudflare): compare warm cache clients * test(cloudflare): trace bypassed response identity * fix(cloudflare): serialize warm cache variants * test(cloudflare): report prewarm cache miss context * test(cloudflare): await target RSC deployment readiness * test(cloudflare): await stable worker promotion * test(cloudflare): isolate RSC prewarm target * fix(cloudflare): defer CDN warmup retries * fix(cloudflare): verify warmed RSC variants * test(cloudflare): trace RSC warm cache paths * fix(cloudflare): separate loading-shell warm cache key * test(cloudflare): make RSC warm reuse proof deterministic * test(cloudflare): tolerate stale promoted Worker routing * fix(rsc): enforce definitive loading-shell cache key |
||
|
|
1c05615159 |
fix(build): preserve portable CommonJS module identity (#2826)
* fix(pages): preserve CJS globals in bundled dependencies * fix(pages): use portable emitted CJS module identity * test(build): expect emitted proxy module identity * fix(build): support linked CommonJS package identity * fix(build): harden linked CommonJS transforms * fix(build): harden emitted CommonJS identity * test(pages): account for fixture route table growth * refactor(plugins): reuse shared module utilities * test(cloudflare): cover Pages bundled CJS globals * test(cloudflare): tolerate unmounted dev source identity * fix(build): preserve nested Cloudflare module identity * refactor(build): resolve deployed module filenames explicitly |
||
|
|
efa916579b |
fix(app-router): settle optimistic search navigations (#2952)
* test(app-router): reproduce deployed optimistic search navigation stall Add a production-shaped provider filter that combines useOptimistic with same-path search navigation and streamed server results. Cover local controls and run a bounded provider cycle against the deployed Workers example, where the intermittent commit failure occurs.\n\nRefs #2865. * test(app-router): reproduce local optimistic navigation stall * fix(app-router): settle optimistic search navigations * fix(app-router): preserve streaming during optimistic recovery --------- Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> |
||
|
|
c296ad34db |
fix(app-router): retain shared layouts across loading shells (#2940)
* test(app-router): reproduce shared layout teardown on soft navigation Add a `/layout-identity` fixture to the app-router-cloudflare example and e2e coverage showing that a shared `[slug]` layout is discarded during a soft navigation that only changes segments below it. The fixture pairs the shared layout with a `loading.tsx` boundary above it, a parallel slot beside `children`, and a nested dynamic segment with its own layout, matching the shape a real app has. Removing the `loading.tsx` boundary makes every assertion pass, which isolates it as the trigger. The five layout-identity assertions fail on main; the soft-navigation control passes, so the teardown is the router discarding a layout it should have retained rather than a full document load. * fix(app-router): retain shared layouts across loading shells * fix(app-router): preserve omitted slot segment identity * test(app-router): follow shared param canonicalizer |
||
|
|
c2362842c9 |
fix(build): support deployed web worker graphs (#2314)
* fix(build): support deployed web worker graphs * fix(build): address worker parity review findings * fix(build): preserve web worker URL origins * test(e2e): cover deployed web workers * test(e2e): port upstream Cloudflare worker suite * fix(build): preserve worker asset semantics * fix(build): tolerate minimal resolved config mocks * test(worker): classify Vite-equivalent coverage |
||
|
|
cf689f2e43 |
fix(pages): run middleware before image endpoint (#2776)
* fix(pages): run middleware before image endpoint * fix(pages): preserve image rewrite query |
||
|
|
2d08de067d |
fix(pages): preserve raw data URLs for middleware (#2775)
* fix(pages): preserve raw data URLs for middleware * fix(pages): preserve data route path with skipped normalization |
||
|
|
3deb467b21 |
fix(pages): preserve gSSP headers on redirects (#2771)
* fix(pages): preserve gSSP headers on redirects * test(pages): cover data redirect headers |
||
|
|
948a14a7e6 | fix(examples): declare complex purge secret (#2774) | ||
|
|
350865c466 |
fix(create-vinext-app): generate next env on first run (#2768)
* fix(create-vinext-app): generate next env on first run * fix(types): keep next env renderer internal |
||
|
|
f517288f40 |
fix(server): reject unsupported static asset methods (#2714)
* fix(server): reject unsupported static asset methods * fix(server): route public asset mutations to 405 * chore: retrigger CI * fix(server): preserve static method parity across runtimes * fix(server): discard static probe responses safely * fix(server): sanitize static method responses * fix(server): preserve static method status * fix(server): strip stale static range headers * fix(server): preserve static rewrite ordering * fix(dev): cache public file routes * fix(dev): harden cached public route handling * fix(worker): avoid unnecessary asset probes * fix(server): preserve static asset lookup coverage * fix(build): keep public path encoder internal --------- Co-authored-by: James <james@eli.cx> |
||
|
|
1c69ce3fef |
fix(pages-router): load custom _app/_document via resolved file paths in dev (#2692)
* fix(pages-router): load custom _app/_document via resolved file paths in dev The dev server imported pages/_app and pages/_document by extensionless id and relied on the module runner applying custom resolve.extensions (e.g. ".page.tsx" from pageExtensions). The runner no longer resolves those, so apps using pageExtensions silently lost their custom App/Document (the load failure is swallowed). Import the file path resolved by findFileWithExts instead, at all three call sites. * test(e2e): add pages-router-complex example app and behaviour suite A deliberately convoluted Pages Router app that serves as a compatibility target: the kinds of patterns that only surface in large, long-lived enterprise pages-router codebases, each pinned by a Playwright spec whose behaviour is verified against real Next.js (73/73 under next dev --webpack). Highlights: custom pageExtensions everywhere (middleware.page.ts, instrumentation.page.ts), app-shell getInitialProps with a TTL-memoised chrome fetch and embedded-shell/draft bypasses, class _document with its own GIP, a zone route dimension driven by middleware rewrites and a real i18next/react-i18next runtime, catch-all routes with static-sibling precedence and cacheable-404/redirect hygiene, record-driven template branching on a dynamic/static/dynamic route, an urql-style server-snapshot data layer, shallow routing + router.events + next/compat/router + next/navigation hooks in pages, next/image with a custom loader, draft-mode gateway APIs, a function-form next.config, and a Cloudflare setup matching the vinext init scaffold. The playwright project (pages-router-complex, port 4199) is intentionally not in the CI e2e matrix: vinext currently passes 59/73, and the failures are the compat backlog documented in the example README. * fix(pages-router): cover compound page extensions in CI |
||
|
|
71dbb45ca8 |
fix(pages): isolate on-demand revalidation requests (#2495)
* fix(pages): pin revalidate loopback origin * test(pages): cover production revalidation origin * fix(pages): preserve internal revalidation boundaries * fix(pages): preserve on-demand revalidation boundaries * fix(pages): dispatch Worker revalidation internally * fix(pages): authenticate revalidation request context * fix(pages): isolate revalidation transport headers * test(pages): account for generated route table size * fix(pages): align on-demand revalidation semantics * fix(pages): align revalidation response cache parity * fix(pages): preserve regenerated ISR representations * fix(pages): align ISR cache representations * fix(pages): preserve canonical ISR representations * fix(pages): align cached response parity * fix(pages): preserve custom App render props * fix(pages): preserve optional App page props * fix(pages): preserve optional App props on the client * fix(pages): normalize client App page props * fix(pages): preserve App data merge semantics * fix(pages): keep redirect status helper internal * fix(pages): match Next.js terminal ISR behavior * fix(pages): preserve custom app error envelopes * fix(pages): match Next.js dev revalidation semantics * test(pages): align dev revalidation parity coverage * chore(pages): remove stale cache helper exports * fix(cache): preserve explicit no-store context |
||
|
|
4f7eec967e |
fix(router): avoid repeated App path decoding (#2556)
* fix(router): avoid repeated App path decoding * test(router): align encoded middleware pathname * fix(router): match Next encoded path semantics * fix(prerender): accept canonical encoded app params * fix(router): preserve encoded interception params * test(build): tolerate watch output replacement * test(router): cover repeatedly encoded production paths * fix(router): preserve action request route identity * refactor(middleware): remove stale pathname argument * refactor(router): align fallback interception paths * refactor(router): derive request path identity from value * fix(router): preserve encoded route parity * fix(router): preserve raw config path matching * fix(router): preserve raw encoded route identity * fix(router): canonicalize encoded dot segments * fix(router): restore encoded path parity after main merge * fix(app-router): exact-match generated params * fix(app-router): chain generated parent params * fix(app-router): preserve generated param ownership * fix(app-router): preserve per-result param ownership * fix(app-router): preserve params after empty parallel generation * fix(app-router): chain parallel static params by branch |
||
|
|
b859a03bbd |
feat(types): ship Next-compatible types without Next.js (#2612)
* feat(types): add vendored Next.js declarations * feat(types): expose fallback types through vinext * fix(types): support readonly Node environment declarations * fix(ci): install vendored types before vinext tarballs * test(types): avoid brittle packed consumer pins * fix(ci): configure pnpm 11 local type overrides * test(types): avoid registry metadata in packed install * test(types): isolate packed declaration install * feat(init): add standard script aliases for new apps * fix(create-vinext-app): run typegen portably * fix(typegen): report next env updates * fix(create-vinext-app): use standard scripts only * refactor(create-vinext-app): use exhaustive package manager return * fix(create-vinext-app): fall back for unknown package managers |
||
|
|
eba70463d4 |
feat(cloudflare): warm prerendered paths before deploy (#2481)
* feat(cloudflare): warm prerendered paths before deploy * fix(cloudflare): warm production CDN for uploaded versions * fix(cloudflare): resolve Worker names for CDN warmup * fix(cloudflare): harden CDN warmup diagnostics * fix(cloudflare): harden CDN warmup deploy flow * fix(cloudflare): clarify CDN warmup trigger failures * feat(cloudflare): warm cdn from build path manifest * fix(cloudflare): emit warm paths for skip-build deploys * fix(cloudflare): avoid stale skip-build path manifests * test(cloudflare): lock skip-build prerender manifest gate * fix(cloudflare): warm fallback deploy URL from triggers * fix(cloudflare): clarify CDN warmup deploy logs * feat(init): offer CDN warmup deploy script * update wrangler configs * fix(init): default CDN warmup to yes * fix(cloudflare): explain first warm deploy failure * fix(cloudflare): keep deploy errors concise * fix(init): make generated home page cacheable |
||
|
|
5825c6807f |
fix(app-router): preserve client state during action revalidation (#2517)
* fix(app-router): preserve client state during action revalidation * test(app-router): pin loading boundary to slot entry * test(app-router): deploy action revalidation repro |
||
|
|
a3910e7aa9 |
feat(cloudflare): add unified worker entry (#2416)
* feat(cloudflare): add default pages router worker entry * feat(cloudflare): add unified worker entry * chore(web): use unified worker entry * chore(examples): use built-in workers-cache entry * chore(examples): keep hackernews on app router entry * chore(cloudflare): rename unified entry to fetch handler * chore(cloudflare): keep pages entry source helper test-local |
||
|
|
0a48f0f7fb |
feat(cloudflare): move deploy command to cloudflare package (#2405)
* feat(cloudflare): move deploy command to cloudflare package * fix(cloudflare): expose deploy cli bin |
||
|
|
89036154cc |
fix(pages-router): unify client asset bootstrap (#2378)
* fix(pages-router): hydrate worker-backed dev pages * refactor(pages-router): unify client asset bootstrap * fix(app): preinitialize bootstrap dependencies * test(app): distinguish preinit scripts from bootstrap * fix(app): root-anchor bootstrap preinit modules * test(app): cover bootstrap preinitialization in browsers * fix(app): share client assets with nested SSR modules * test(app): verify bootstrap preinit CSP nonces * test(app): serve CSP nonce in worker fixture * fix(test): read CSP script nonce property * fix(cloudflare): emit client assets beside final workers * fix(build): resolve pages client assets from source * fix(app): register client assets in ssr graph * fix(pages-router): harden client bootstrap resolution * fix(build): emit client assets for node outputs * fix(build): package client assets in standalone output * test(app): reference worker nonce fixture explicitly * fix(build): preserve client assets across environments * refactor(build): unify client asset outputs * fix(build): stabilize custom client asset outputs * fix(build): finalize client assets after app builds * fix(build): reuse client assets in hybrid builds * fix(build): scope hybrid client assets metadata * test(e2e): colocate Cloudflare worker fixture |
||
|
|
25a3c26f7a |
feat(init): scaffold for cloudflare and node (#2279)
* feat(init): select deployment platform * refactor(init): AST-update deployment config * feat(init): configure Cloudflare cache and images * fix(init): avoid backtracking in config updates * fix(init): cover packaged Cloudflare setup * ci: allow Cloudflare init smoke builds * ci: fix packaged Cloudflare dependency install * test(e2e): isolate browser fixture servers * chore: allow Windows taskkill binary * fix(init): configure Cloudflare image adapter * fix(init): address platform setup review * fix(init): address cache setup review * fix(init): hide workers cache option * fix(init): align generated config indentation * fix(init): validate cloudflare setup before mutation * fix(init): preserve additive config syntax * fix(init): tidy interactive prompt spacing * fix(images): rename Cloudflare optimizer builder * refactor(images): remove draft name migration * fix(init): install dependencies after setup * fix(init): clarify setup recovery steps * fix(init): ignore Wrangler state for Cloudflare * fix lockfile |
||
|
|
b4843d75c1 |
feat(images): configure image optimization via vinext({ images }) adapter (#1873)
* feat(images): configure image optimization via vinext({ images }) adapter
Move server-side image optimization from a hand-wired custom worker entry to a
declarative `vinext({ images: { optimizer } })` option, mirroring the cache
adapter pattern. The default entries now handle `/_next/image` through a
registered optimizer, so no custom worker is required, and the same config
works across all targets — optimizing on Cloudflare, gracefully serving images
unoptimized on Node/dev where the binding is unavailable (like KV cache
degrading to in-memory).
- add an ImageOptimizer registry (set/getImageOptimizer +
handleConfiguredImageOptimization) in server/image-optimization.ts
- generate `virtual:vinext-image-adapters` (registerConfiguredImageOptimizer)
from the new `images` plugin option
- add @vinext/cloudflare/image/image-adapter: imageAdapter() builder + runtime
factory reading the env.IMAGES binding
- handle /_next/image in the default app-router entry and the generated Pages
worker via the registry; inline next.config `images` (allowed widths +
security headers) into the RSC entry
- vinext deploy points App Router `main` at vinext/server/app-router-entry
(no generated worker) and prints a hint to enable the optimizer
next.config `images` (remotePatterns, deviceSizes, dangerouslyAllowSVG, etc.)
continues to drive the standard Next.js options; the vite-config
`images.optimizer` only selects the runtime transform backend.
* fix(images): cover deploy image-hint helpers and preserve optimizer this-binding
The Check CI job failed because knip flagged viteConfigHasImageAdapter and
formatImageOptimizationHint as unused exports — they were only called inside
deploy.ts. Cover both with unit tests in tests/deploy.test.ts (mirroring the
existing viteConfigHasCacheAdapter / formatMissingCacheAdapterError suites),
which also closes the coverage gap for the new deploy hint path.
Also wrap the registered optimizer's transformImage in
handleConfiguredImageOptimization instead of detaching the method, so an
optimizer implemented as a class instance keeps its this binding.
* fix(images): honor configured deviceSizes/imageSizes on the App Router Node prod server
Review follow-up (ask-bonk):
- The App Router prod server (vinext start) validated /_next/image widths
against the hardcoded Next.js defaults, rejecting valid optimizer URLs with
400 when the app configures custom images.deviceSizes/imageSizes — while the
Cloudflare worker entry and the Pages prod path already honored them. Read
the __imageAllowedWidths constant inlined into the RSC entry (falling back to
the defaults for older builds), matching how __assetPrefix/__basePath are read.
- Lock in the this-binding behavior of handleConfiguredImageOptimization with a
class-instance optimizer test.
* fix(images): pass an explicit empty allowed-widths config through on vinext start
Review follow-up (ask-bonk, awareness note): the old-build fallback guard
conflated a missing __imageAllowedWidths export with an explicit empty
deviceSizes/imageSizes config, mapping the latter to the Next.js defaults on
the Node App Router path while the Cloudflare worker passes the empty array
straight through. Only fall back to the defaults when the export is absent.
* refactor(images): read App Router image config from the RSC entry, retire the JSON sidecar
Review follow-up (ask-bonk): the App Router had two parallel build-time sources
for next.config images security/header settings — the __imageConfig constant
inlined into the RSC entry (read by the Cloudflare worker entry) and the
image-config.json sidecar written by the vinext:image-config plugin (read by
vinext start). Unify on the RSC entry export: prod-server now reads
rscModule.__imageConfig, keeping image-config.json only as a read-side fallback
for dist outputs built by older vinext versions, and the sidecar writer plugin
is removed.
* fix(deploy): keep wrangler main on a user-authored worker entry for App Router
Review follow-up (ask-bonk): an App Router app with a custom worker/index.ts
but no wrangler.jsonc would have had its custom worker silently dropped —
generateWranglerConfig unconditionally pointed main at the default
vinext/server/app-router-entry. Respect hasWorkerEntry so a user-authored
worker keeps winning for both routers, with a regression test.
* fix(images): expose Cloudflare optimizer under images path
* fix(deploy): install Cloudflare image adapter package
* fix(examples): declare Cloudflare image adapter package
* test(images): update App Router image config codegen assertions
* fix(examples): configure image optimizer adapters
|
||
|
|
6e998ec114 |
fix(middleware): run on missing build assets (#2242)
* fix(middleware): run on missing build assets * fix(middleware): preserve missing asset rewrites on workers |
||
|
|
656a381de9 |
fix(og): constrain inlined assets to trusted roots (#2172)
* fix(og): constrain inlined assets to trusted roots * fix(og): canonicalize asset package boundaries * test(og): cover canonical boundary intersections * fix(og): support file-symlinked dependency modules * docs(og): clarify logical asset boundary resolution * fix(og): pin symlinked modules to matching packages * fix(og): keep linked package boundaries narrow * fix(og): reject workspace-root asset boundaries * fix(og): name-pin external package boundaries * fix(og): validate linked package ownership * fix(og): track linked package ownership * fix(og): harden linked package provenance * fix(og): confine captured alias packages * fix(og): pin captured directory aliases * fix(og): require alias package ownership * fix(og): support single-segment aliases * perf(og): preserve alias lookup precedence * refactor(og): extract asset ownership policy * test(og): cover linked package fonts in e2e * fix(og): recognize resolved package aliases * fix(og): support regex package aliases * test(og): isolate linked font fixture * docs(test): clarify linked font coverage |
||
|
|
a3d2f92152 |
fix(router): honor hybrid pages route priority (#1997)
* fix(router): honor hybrid pages route priority * fix(router): share hybrid owner decision with client navigation PR #1997 fixed the server-side route ownership for direct document loads, but the same invariant broke for client-side soft navigations and the matching prefetch path: - navigateClientSide() always delegated to the App runtime's RSC fetch, even when the Pages route had higher priority. renderPagesFallback() short-circuits RSC requests with null, so the App catch-all won. - prefetchUrl() prefetched an RSC stream for any URL that matched an App route, again ignoring Pages ownership. - The Pages entry was loaded on every hybrid request, even when a static App route had already matched (Pages can never win in that case). Expose the Pages route manifest on the client via a new __VINEXT_PAGES_LINK_PREFETCH_ROUTES__ window global emitted by both the App and Pages browser entries. The link shim consults a shared resolveHybridClientRouteOwner helper that mirrors the server-side pagesRouteHasPriorityOverAppRoute comparison. When Pages owns the URL, the click handler issues a window.location navigation and the prefetch path returns early. Gate the renderPagesFallback call behind a static-App-route check in handleAppRscRequest: when a static App route matches, the bridge cannot win, so skip the eager Pages entry load. Centralise the comparison in a new resolveHybridRouteOwner helper so server and client reach the same answer for the same (URL, route pair). Adds the missing client-navigation coverage to the use-params e2e fixture (Link from /app/ to /pages-dir/foobar) and unit tests for the shared owner decision. * fix(router): mirror server hybrid owner decision on the client PR review flagged two split-brain bugs in the previous hybrid ownership fix: a hand-copied client comparator and a Link-only ownership gate that left programmatic App Router navigations on the wrong path. The hand-copied routePrecedence in hybrid-client-route-owner.ts omitted the static-prefix reduction that lives in routing/utils.ts#routePrecedence, and used a strict-less-than comparison that returned App for identical dynamic patterns. The server returns Pages for both cases (Pages providers sort ahead of App providers, and routePrecedence subtracts 50 per static prefix segment). The split produced a real ownership disagreement on overlapping patterns like /_sites/:slug* (Pages) vs /:slug* (App). Add a shared compareHybridRoutePatterns to routing/utils.ts as the single source of truth: the static/dynamic short-circuits plus a sortRoutes call (which carries the static-prefix reduction and the Pages-first equal-pattern tiebreak). The server pagesRouteHasPriorityOverAppRoute and the client resolveHybridClientRouteOwner both delegate to it, so they cannot diverge. Drop the hand-copied routePrecedence entirely. Wire the ownership check at the App navigation runtime boundary so useRouter().push, useRouter().replace, gesturePush, and form submits all get the same hard-nav contract as the Link click handler. Specifically: - navigateClientSide: after same-origin normalization, if Pages owns the URL, hard-navigate via window.location and return (matching the existing external-URL branch). - _appRouter.prefetch: short-circuit RSC URL construction for Pages-owned targets so we do not warm an unusable cache entry. Centralise the existing two inline hard-nav branches in navigateClientSide into a hardNavigateTo helper for clarity. Tests: - Direct unit tests for compareHybridRoutePatterns covering identical-dynamic tiebreak, static-prefix dynamic overlap, static-prefix catch-all overlap, and infix-static bonus. - Direct unit tests for resolveHybridClientRouteOwner mirroring the server assertions plus a basePath-stripping test. - e2e: useRouter().push('/pages-dir/foobar') from an App page resolves to the Pages document. - e2e: useRouter().prefetch('/pages-dir/foobar') issues zero RSC requests for the target URL. * docs(router): fix stale score in static-prefix catch-all test comment The hand-copied comparator note quoted the dynamic-segment scores (51 / 1000) for the optional-catch-all example (/_sites/:slug* vs /:slug*). The current optional-catch-all scoring actually produces 1951 vs 2000. The test assertion was correct; only the comment was stale. * test(use-params): fix direct-load single dynamic param to use /a instead of /a/b * fix(router): compare hybrid routes structurally * fix(router): reject hybrid route conflicts * fix(router): refresh hybrid route ownership * fix(router): preserve hybrid routing lifecycle * fix(router): recheck pages routes after rewrites * fix(router): preserve rewritten pages queries * fix(router): preserve rewritten route ownership * fix(router): resolve client rewrites sequentially * fix(router): apply rewrite phases sequentially * fix(router): preserve rewrite params and endpoints * fix(router): hand off endpoint navigations * fix(router): preserve rewrite fragment params * test(e2e): avoid hybrid fixture route conflicts * test(pages): await async config validation * test(hybrid): avoid duplicate page fixtures * chore(router): clarify hybrid priority semantics * refactor(router): remove duplicate app route matcher * docs(router): clarify client hybrid comparator --------- Co-authored-by: James <james@eli.cx> |
||
|
|
2e83e7f558 |
fix(edge-wasm): handle *.wasm?module imports in non-Cloudflare builds (#1877)
* fix(edge-wasm): handle \`*.wasm?module\` imports in non-Cloudflare builds (#1351) In plain Node.js builds (deploy-suite / standalone `vinext start`), Rolldown has no built-in handler for the `?module` query on `.wasm` imports, so middleware and edge API routes that use: import wasm from './add.wasm?module' would throw at bundle time. Root cause: the `?module` query is a Cloudflare Workers / workerd convention handled by `@cloudflare/vite-plugin`'s `additionalModulesPlugin` (enforce:"pre"). When that plugin is absent (e.g. after `vinext init` which emits a plain `vite.config.ts` with only `vinext()`), no plugin resolves the query. Fix: add a new `vinext:wasm-module-import` Vite plugin (enforce:"pre") that, in the absence of @cloudflare/vite-plugin: 1. Intercepts `*.wasm?module` imports in `resolveId`. 2. Reads the WASM binary at load time and inlines it as base64. 3. Exports `await WebAssembly.compile(buffer)` via a virtual module — valid in Node.js (top-level await in ESM). The `hasCloudflarePlugin` guard is checked inside the hook handler (not as an array-spread condition) so it reflects the value set by the `config` hook after plugin registration. workerd forbids compiling WASM from bytes at runtime, so this path intentionally never runs when @cloudflare/vite-plugin is present. Fixes #1351. * fix(wasm-module-import): suppress no-control-regex lint on virtual module regexes Replace bare null-byte with unicode-escape in the two filter/replace regexes and add oxlint-disable-next-line comments following the same convention used in client-reference-dedup.ts. Fixes the Check job failure. * fix(wasm-module-import): exclude @vercel/og from plugin scope, add resolveId tests - Add importer guard in resolveId to skip imports originating from @vercel/og (resvg.wasm, yoga.wasm): vinext:og-font-patch converts those to dynamic imports with a .catch() disk-read fallback; if wasm-module-import intercepts them, the Node.js fallback never runs, the ~1.3 MB resvg WASM ships twice, and findEmittedWasmAsset dedup in vinext:og-assets breaks. Guard mirrors the pattern in the existing isVinextOgShimImporter() helper. - Remove dead `return null` after `this.error()` (which throws); use `bytes!` non-null assertion to satisfy TypeScript. - Add resolveId test suite: happy path, ?module query stripping, null resolver, og-collision guard (plain + virtual-prefix importer), and non-og pass-through. Fixes ask-bonk findings on PR #1877. * refactor(wasm-module-import): drop non-null assertion, document TLA and filter intent * test(wasm-module-import): add build-level coverage, document runtime-gating assumption * refactor(wasm-module-import): extract to plugins/, guard client environment, watch wasm file in dev * fix(wasm-module-import): delegate target-owned modules * refactor(wasm-module-import): share query stripping |
||
|
|
de33c3e5a0 |
fix(build): inline ../-relative font assets in OG routes (#1866)
* fix(build): inline ../-relative font assets in OG routes
The vinext:og-inline-fetch-assets plugin only matched paths starting
with "./" (e.g. "./font.ttf"), but Next.js test fixtures for OG custom
fonts use "../"-relative paths like "../../../assets/typewr__.ttf".
Without the inline, at runtime:
- Cloudflare Workers: import.meta.url is "worker" (not a URL), so
new URL("../../../assets/...", import.meta.url) throws TypeError.
- Node.js: fetch() does not support file:// URLs, so the inlined
file:// URL produced by the import-meta-url plugin fails.
Fix: relax the regex from `\.\/[^"']+` to `\.[^"']+` so any
dot-relative path (./x, ../x, ../../x, etc.) is inlined as base64.
Apply the same fix to the readFileSync(fileURLToPath(...)) pattern.
Failing tests addressed:
- test/e2e/og-routes-custom-font: should render og with custom font
for app routes (edge runtime fetch + Node.js fs.promises.readFile)
- test/e2e/app-dir/metadata-font: should handle custom fonts in both
edge and nodejs runtime
* fix(build): inline OG font assets when a formatter adds a trailing comma
The vinext:og-inline-fetch-assets regex only matched the single-line,
comma-less form of `fetch(new URL(...)).then((res) => res.arrayBuffer())`.
When a formatter (Prettier `trailingComma: "all"`, oxfmt) wraps the call
across lines it appends a trailing comma:
.then((res) =>
res.arrayBuffer(),
)
which no longer matched, so the font was left as a runtime fetch. On
Cloudflare Workers `import.meta.url` is "worker", so
`new URL("../...", "worker")` throws "TypeError: Invalid URL" and the OG
route returns 500 — even though the ../-relative path itself was already
supported. Relax both `.then(...)` alternatives with an optional `,?`.
Add real e2e coverage (`/api/og-custom-font`) mirroring Next.js'
og-routes-custom-font fixture: an edge OG route that loads
assets/noto-sans.ttf via
`fetch(new URL("../../../assets/noto-sans.ttf", import.meta.url))`. It
runs in tests/e2e/og-image.spec.ts across the app-router (Node dev),
cloudflare-dev and cloudflare-workers (workerd) projects, with the route
and font added to both the app-basic fixture and the app-router-cloudflare
example. Also adds a unit test for the formatted/trailing-comma shape in
tests/og-inline.test.ts.
* fix(build): inline OG font assets with a semicolon-terminated block body
Follow-up from the /bigbonk review on PR #1866: the block-body alternative
of the og-inline-fetch-assets regex ended `…\.arrayBuffer\(\)\s*\}?\s*,?\s*\)`
with no allowance for a `;` before `}`, so formatter output such as
.then((res) => {
return res.arrayBuffer();
})
.then(function (res) { return res.arrayBuffer(); })
was left as a runtime fetch — which throws "Invalid URL" on Workers
(import.meta.url === "worker"). Add `;?` before the block-body `}` and unit
tests for the arrow- and function-expression block-body forms.
|
||
|
|
e40870e114 |
fix(og): lazy-load @vercel/og to keep it out of the main worker entry (#1774)
* fix(og): lazy-load @vercel/og to code-split it out of the worker entry
The next/og shim statically imported @vercel/og, so a top-level
`import { ImageResponse } from "next/og"` inlined ~800 KB of satori + resvg +
wasm/fonts into the always-loaded server entry. Import it via dynamic import()
inside the async stream callback so it is always a separate chunk.
app-router-cloudflare dist/server/index.js: ~1.67 MB -> ~875 KB.
* chore: drop changeset
* fix(og): copy resvg.wasm from split chunk + drop example workaround
Address review feedback on the lazy @vercel/og change:
- vinext:og-assets scanned only index.js for the resvg.wasm reference to
decide whether to copy the Node.js disk-read fallback asset. Now that
@vercel/og is code-split into its own chunk, that reference is no longer in
index.js, so the copy was skipped (breaking the Node target's OG fallback).
Scan all emitted chunks via the writeBundle bundle arg instead.
- app-router-playground used a manual `await import("next/og")` workaround
that is no longer needed; switch it to the idiomatic static import.
|
||
|
|
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 |
||
|
|
f0f6aa72e0 |
feat(cache): configure cache adapters from vite plugin config (#1733)
* feat(cache): configure cache adapters from vite plugin config
Add a `cache` option to the vinext() plugin so CDN and data cache
adapters can be declared in vite.config instead of calling
setDataCacheHandler() / setCdnCacheAdapter() from a worker entry:
vinext({
cache: {
cdn: { adapter: require.resolve('vinext/cloudflare/cache/cdn-adapter') },
data: { adapter: require.resolve('vinext/cloudflare/cache/kv-data-adapter') },
},
})
Each slot points at an adapter module whose default export is a factory
(DataCacheAdapterFactory / CdnCacheAdapterFactory). The plugin generates
a virtual:vinext-cache-adapters module that the App Router worker entry
calls per request (self-guarded, once per isolate), passing the host env
so binding-backed adapters (e.g. KV) can read their namespace.
Ships ready-made Cloudflare adapter entry points:
- vinext/cloudflare/cache/kv-data-adapter (KVCacheHandler)
- vinext/cloudflare/cache/cdn-adapter (CloudflareCdnCacheAdapter)
* feat(cache): add typed adapter builders (kvDataAdapter/cdnAdapter)
Instead of `{ adapter: require.resolve(...) }`, each adapter module now
also exports a config-time builder from the same path:
import { cdnAdapter } from 'vinext/cloudflare/cache/cdn-adapter';
import { kvDataAdapter } from 'vinext/cloudflare/cache/kv-data-adapter';
vinext({ cache: { cdn: cdnAdapter(), data: kvDataAdapter({ binding: 'MY_KV' }) } })
A builder returns a plain, serializable { adapter, options } descriptor —
it never touches the Workers runtime, so nothing throws at config / build
/ dev time when bindings aren't available. Descriptor `options` (e.g. the
KV binding name) are inlined into the generated registration module and
forwarded to the factory's { env, options } context, where the binding is
resolved lazily on the first request.
- shims/cache-adapter: descriptors + options-aware factory/context types
- kv-data-adapter: kvDataAdapter() builder + configurable binding/appPrefix/ttl
- cdn-adapter: cdnAdapter() builder
- raw { adapter, options } path form still supported
* test(cache): verify absolute (require.resolve) local adapter path bundles
Real Cloudflare build pointing cache.data at a local adapter file by
absolute path (what require.resolve('./adapter') yields). Proves the
generated registration module resolves the absolute import, bundles the
local adapter into the worker, and does not need any Workers context at
build time.
* refactor(cache): builder require.resolve + register across all routers/runtimes
Addresses review feedback:
* Move adapters into their own runtime modules instead of re-exporting.
Each adapter is now a builder module (kv-data-adapter.ts / cdn-adapter.ts)
plus a sibling *.runtime.ts holding the default-export factory. Type
definitions have a single home in shims/cache-adapter.ts (dropped the
re-export shim; index.ts imports the config type from there).
* The exposed builder utility resolves the relative runtime path internally
via import.meta.resolve (the ESM require.resolve), so the descriptor carries
an absolute path to the runtime factory rather than a bare specifier — the
example is just kvDataAdapter({ binding }), no require.resolve at the call site.
* Register configured cache handlers EVERYWHERE, not just the App Router worker:
- App Router: the generated RSC entry passes registerConfiguredCacheAdapters
into createAppRscHandler, which calls it per request — covering Workers,
the Node server, and dev through the one shared handler.
- Pages Router: the generated server entry registers in renderPage and
handleApiRoute (Node/dev), and the generated worker registers with env
(Workers, for KV bindings).
Registration self-guards (first call with real env wins) and is now resilient:
a factory that throws on an incompatible runtime is logged and skipped, so the
default handler stays in place instead of failing every request.
Tests: generator-level assertions that every router/runtime entry wires
registration, plus the existing builder/codegen/factory and full-build coverage.
vp check clean; app-router (339) and pages-router (272) suites pass.
* refactor(cache): keep all Cloudflare adapter code under cloudflare/
The adapter factory contract lived in shims/cache-adapter.ts (outside
cloudflare/), and the Cloudflare adapters reached out to it. Move the
contract into cloudflare/cache/adapter.ts so every Cloudflare-specific
cache adapter file is self-contained under cloudflare/ — importing only
cloudflare-local modules and the core CacheHandler/CdnCacheAdapter
interfaces it implements.
The plugin's config schema (CacheAdapterDescriptor / VinextCacheConfig)
is genuinely framework-level (it's the vinext() `cache` option), so it
moves into the codegen module the plugin already owns; index.ts imports
it from there. Builders return a structural { adapter, options } so they
don't import the descriptor type either. Deletes shims/cache-adapter.ts.
* refactor(cache): merge KV/CDN classes into the runtime adapter files
All Cloudflare cache code now lives in one directory, cloudflare/cache/,
and each runtime file holds both the implementation class and its
config-driven factory (no separate class module to reach for):
- kv-cache-handler.ts -> cache/kv-data-adapter.runtime.ts
(KVCacheHandler + ENTRY_PREFIX + createKvDataCacheAdapter default export)
- cloudflare-cdn-cache.ts -> cache/cdn-adapter.runtime.ts
(CloudflareCdnCacheAdapter + createCloudflareCdnCacheAdapter default export)
Updated importers: cloudflare/index.ts re-exports the classes from the
runtime files, tpr.ts pulls ENTRY_PREFIX from there, shims/cdn-cache.ts
imports the edge adapter from there, and the tests follow the moved paths.
git mv preserves history.
vp check clean; cache/kv/cdn/app-route/tpr/shims suites pass (1300+ tests).
* chore(cache): trim low-value comments added in this branch
Remove narrating/redundant comments that just restated the code; keep
the non-obvious why (registration ordering/resilience, import.meta.resolve
rationale, edge cache-control semantics). No code changes.
* review: address PR #1733 feedback
- Make registerCacheAdapters a required field on the RSC handler options
(the generated entry already passes it; test factory updated).
- Remove the separate cloudflare/cache/adapter.ts contract file; inline the
factory param types directly into the two runtime adapters.
- Drop the CloudflareCdnCacheAdapter re-export from cloudflare/index.ts.
- Fold the virtual:vinext-cache-adapters declaration into global.d.ts and
delete the standalone .d.ts.
- Remove the ./cloudflare/cache/* package.json export for now; README uses a
local-adapter require.resolve example with a note that the built-in adapter
export paths are pending.
- Rename the config-driven KV default binding to VINEXT_KV_CACHE (imperative
deploy/tpr path keeps VINEXT_CACHE — flagged on the thread).
* refactor(cache): align KV binding name to VINEXT_KV_CACHE everywhere
Rename the KV cache binding from VINEXT_CACHE to VINEXT_KV_CACHE across the
whole codebase so the config-driven adapter, the imperative deploy-generated
worker, TPR's wrangler detection, and the apps/web example all agree. The
unrelated X-Vinext-Cache response-header constant (VINEXT_CACHE_HEADER) is
untouched.
* tidy
* .
* .
* .
* .
* .
* Move apps/web cache to plugin config
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
---------
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
|
||
|
|
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. |
||
|
|
61cf2e78f5 |
fix(image): emit /_next/image URLs (#1562)
* fix(image): emit /_next/image URLs to match Next.js Closes #1513 The default image loader and optimization endpoint switched from the vinext-specific /_vinext/image path to Next.js's canonical /_next/image. This unblocks the deploy suite tests that import Next.js's expected URL shape (/_next/image?url=...&w=...&q=...). * refactor(image): use IMAGE_OPTIMIZATION_PATH constant at remaining call sites Replace hardcoded "/_next/image" strings in index.ts, app-rsc-handler.ts, and the generated worker entry templates in deploy.ts with the IMAGE_OPTIMIZATION_PATH constant from server/image-optimization, matching the pattern already used in prod-server.ts. Prevents future drift if the path ever changes again. * feat(image): accept both /_next/image and /_vinext/image at the optimizer Add a VINEXT_IMAGE_OPTIMIZATION_PATH constant and an isImageOptimizationPath() helper, then route through every match site (prod-server, dev server passthrough, app RSC handler, generated worker templates, and shipped example workers). Apps that wire image URLs to either prefix now hit the same handler; new URLs are still emitted via IMAGE_OPTIMIZATION_PATH. |
||
|
|
1ac7b6514b |
fix(examples): next build failure for app-router-playground (#1234)
|
||
|
|
2acec092be | chore: add next-env.d.ts to .gitignore (#1224) | ||
|
|
a27193fcaf |
chore: upgrade pnpm 10 to 11 and enable frozenLockfile (#1185)
* chore: upgrade pnpm 10 to 11 and enable frozenLockfile - Bump packageManager from pnpm@10.32.1 to pnpm@11.1.1 - Migrate .npmrc settings to pnpm-workspace.yaml (pnpm 11 only reads auth/registry from .npmrc) - auto-install-peers=false -> autoInstallPeers: false - Remove playground save-exact=true (minor pnpm add preference, not needed for builds) - Convert onlyBuiltDependencies list to allowBuilds map (pnpm 11 format) - Add frozenLockfile: true and preferFrozenLockfile: true - Delete .npmrc files (contained no auth settings) * fix: handle pnpm 11 strictDepBuilds, preserve autoInstallPeers, remove minimumReleaseAgeExclude - Add unrs-resolver: false to allowBuilds (reviewed, deny build scripts) - Keep autoInstallPeers: false to match existing lockfile - Remove minimumReleaseAgeExclude entirely - Append allowBuilds to scaffolded project's pnpm-workspace.yaml after create-next-app so subsequent pnpm steps don't abort |
||
|
|
97f878ac94 |
fix: remove duplicate next-shims.d.ts from fixtures (#1170)
* fix: remove duplicate next-shims.d.ts from fixtures Signed-off-by: Deepam Goyal <deepam02goyal@gmail.com> * fix: use type intersections Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> * fix: remove duplicate next-shims.d.ts from examples Apply the same consolidation from fixtures to examples/realworld-api-rest and examples/pages-router-cloudflare. Point their tsconfigs at the shared packages/vinext/src/shims/next-shims.d.ts instead of keeping local copies. * fix: declare Link value before default export in next/link shim The deleted fixture copies all had `const Link: ComponentType<LinkProps>` before the default export. Without it the default import resolves to `any` (masked by skipLibCheck). Add the missing value declaration. --------- Signed-off-by: Deepam Goyal <deepam02goyal@gmail.com> Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> Co-authored-by: James <james@eli.cx> |
||
|
|
07be32258f |
fix(router): preserve filesystem routes before afterFiles rewrites (#1166)
* fix(router): preserve filesystem routes before afterFiles rewrites afterFiles rewrites were evaluated before App and Pages filesystem route matches in several runtime paths. That let a rewrite override an existing non-dynamic page, which diverges from Next.js route ordering. The fix checks the page/app match first and only applies afterFiles rewrites when no non-dynamic route wins, while still allowing afterFiles to run before dynamic routes. Regression coverage exercises App RSC handler, Pages dev/prod, and generated Worker wiring. * test(router): align chained afterFiles rewrite expectations The chained rewrite fixture expected an afterFiles rewrite to override a concrete /intermediate page. That is the route-ordering behavior this branch fixes. Update the fixture to cover both intended contracts: middleware can chain into afterFiles when no page file wins, and concrete page files are not overridden by afterFiles rewrites. * refactor(router): remove route-ordering type assertions Validate generated Pages route metadata at the boundary instead of asserting its shape, and make the app handler test fixture route matching explicit. * test(router): cover afterFiles order in Pages Worker Add a built Cloudflare Pages Router regression where a concrete page route must win before an afterFiles rewrite. Align custom Pages Worker entries with the generated worker route-match gate. |
||
|
|
696948aca4 |
fix(next/font/google): use real axis range, validate options at build time (#885) (#893)
* shim: rewrite buildGoogleFontsUrl on top of the new build-url helper
Replace the inline URL builder in shims/font-google-base.ts with a thin
wrapper around the pure buildGoogleFontsUrl helper from PR 1
(packages/vinext/src/build/google-fonts/build-url.ts). The shim still
takes a FontOptions object and still returns a CSS URL, so external
callers and the existing 84 unit tests are unaffected.
Two visible behaviour changes:
1. Empty options no longer emit `:wght@100..900`. They emit no axis
segment at all, which makes Google return the default static face
(HTTP 200) for every font, including narrow-axis families like Sen
that previously got HTTP 400. This is the dev fallback only; the
build plugin always pre-resolves the real axis range from metadata
in production.
2. Italic-only requests no longer drop the ital axis. Pre-fix the outer
guard `weights.length > 0 || styles.length > 0` entered the block
but the inner branch only handled the wght path, so a call like
`Inter({ style: ['italic'] })` produced `family=Inter&display=swap`
with no italic anywhere in the URL. The shim now derives an ital
array from style and falls back to weight '400' when ital is set
without an explicit weight, so Google has somewhere to attach the
ital axis in the URL.
The shim deliberately does not import font-data.json or validate.ts: a
388 KB metadata file in the Worker bundle would dwarf the rest of the
shim, and the production path already has the metadata-aware variant.
Refs #885.
* plugin: validate options against metadata, surface HTTP errors as build errors
Replace the inline URL builder in plugins/fonts.ts with the pipeline added
in PR 1: validateGoogleFontOptions parses the call's options against the
bundled font metadata, getFontAxes resolves the actual `wght` range for
variable fonts, and buildGoogleFontsUrl emits the URL.
Three behaviour changes:
1. Variable fonts called with no `weight` now request the font's real
axis range (Sen 400..800, Inter 100..900) instead of a hardcoded
100..900. This is the user-visible fix for issue #885: Sen returns
HTTP 200 with all five face files instead of HTTP 400.
2. Validation errors surface as build errors with the source file path
attached. Unknown family, missing weight on a static font (Anton,
Archivo Black, etc.), unknown weight, mixing 'variable' with explicit
weights, axes on non-variable fonts, invalid display values, and
missing/unknown subsets are now all caught at transform time. Pre-fix
these silently produced URLs that Google rejected at request time.
3. HTTP non-2xx responses from Google are now distinguished from network
errors. A 4xx/5xx is wrapped in GoogleFontsHttpError and surfaced
with the failing URL plus the response body, because pre-fix the
silent `catch { return; }` skipped self-hosting and let the runtime
CDN path retry the same broken URL in the browser. Network errors
(offline dev) still fall through silently to keep `vp dev` usable
without a connection.
The fontCache key is the URL, so existing on-disk caches under
`.vinext/fonts/<hash>/` will be stale for affected fonts after this
change. One refetch per font on the next build.
Refs #885.
* tests: regression coverage for the #885 narrow-axis bug and validator throws
Five new tests in tests/font-google.test.ts plus subset additions to
existing fixtures whose Inter() / Roboto() calls were missing them.
Shim regressions:
- buildGoogleFontsUrl with empty options must not emit `:wght@100..900`
- buildGoogleFontsUrl with style-only options must keep the ital axis
visible in the URL
Plugin regressions:
- Sen() with no weight must fetch `:wght@400..800`, not `:wght@100..900`
- unknown families throw a build error with the file path attached
- non-variable fonts called without an explicit weight throw at build
time instead of silently emitting a broken URL
- HTTP 4xx/5xx from Google surfaces as a build error containing the URL
and the response body
- network errors (DNS, ECONNREFUSED, AbortError) still fall through
silently so offline dev keeps working
The existing plugin transform tests passed `Inter({ weight: '400' })`
without subsets; the validator now treats that as a missing-subsets
error (matches Next.js behaviour), so those fixtures gain
`subsets: ['latin']`. The two nested-brace regression tests still
exercise the parser's balanced-brace logic but use a benign
`_placeholder` field instead of `axes: { wght: 400 }`, which is
semantically invalid for Inter under the new validator.
Refs #885.
* address google font review feedback
|
||
|
|
a9d3455487 |
test: regression tests for inherited intercepting route slot deduplication (#721)
* fix: allow inherited intercepting routes to share target patterns * fix: validate intercepting route target consistency per page * refactor: simplify intercept target pattern collection by using Set for deduplication * fixup: use main's inline Set dedup, remove helper export, restore .gitignore * fix(test): pass explicit out dirs and appDir to buildApp to fix UNRESOLVED_IMPORT in RSC build * chore: restore .gitignore to match main --------- Co-authored-by: James <james@eli.cx> |
||
|
|
12c747f686 |
fix: restore baseUrl in hackernews tsconfig to fix build (#770)
Removing baseUrl in the security audit PR (#741) silently broke the hackernews example build: loading.tsx, page.tsx and other app files import components and lib via bare specifiers ('components/skeletons', 'lib/get-comments', etc.) that Vite resolves using the tsconfig baseUrl. Without it Rolldown cannot find those modules and the build fails. Restore 'baseUrl: "."' so Vite continues to resolve those imports correctly. Other examples (app-router-playground, fumadocs-docs-template) already keep baseUrl for the same reason. |
||
|
|
49656ed5cf |
fix: security audit findings from issue #741 (#757) (#769)
* fix: security audit findings from issue #741 CI shell injection (high): - publish.yml: use env var for inputs.bump in case statement - nextjs-tracker.yml: use env vars for since_hours and dry_run inputs - nextjs-tracker.yml: use env var in Skip step echo Test file false positive (critical flag): - tests/vite-hmr-websocket.test.ts: add gitleaks:allow for RFC 6455 example WebSocket nonce (dGhlIHNhbXBsZSBub25jZQ==) innerHTML / XSS (high): - packages/vinext/src/shims/script.tsx: add security comment documenting that dangerouslySetInnerHTML is developer-supplied inline script only - packages/vinext/src/shims/head.ts: document tag is bounded to RAW_CONTENT_TAGS (script/style), not user input dangerouslySetInnerHTML in example (medium): - examples/hackernews/components/comment.jsx: sanitize HN API HTML with DOMPurify before rendering - examples/hackernews/package.json: add dompurify dependency - pnpm-workspace.yaml: add dompurify and @types/dompurify to catalog Non-literal RegExp (medium): documented as internal config values only, no user input reaches any of the 6 flagged patterns. See safeRegExp() in config-matchers.ts which already enforces ReDoS protection. Deprecation cleanup: - examples/hackernews/tsconfig.json: remove deprecated baseUrl option (moduleResolution: bundler handles resolution; all imports are relative) - examples/hackernews/tsconfig.json: add noEmit: true to prevent TypeScript from attempting to write over existing .js files in lib/ - examples/hackernews/package.json: add missing @cloudflare/workers-types devDependency (was referenced in tsconfig types but not installed) * fix: address bonk review comments - Switch dompurify → isomorphic-dompurify so sanitize() works during SSR (DOMPurify is a no-op without window/document; isomorphic-dompurify bundles jsdom for server-side use) - Revert package.json whitespace to 2-space indentation (was inadvertently changed to 4-space, creating noise in the diff) - Replace dompurify + @types/dompurify catalog entries with isomorphic-dompurify (ships its own types, no separate @types needed) - Add missing trailing newline to tsconfig.json * fix: address bonk review comments on PR #769 - Switch from isomorphic-dompurify to sanitize-html, and move sanitization to the server-side data boundary (lib/get-comments.js). isomorphic-dompurify depends on jsdom which is Node.js-specific and does not work in the Cloudflare Workers runtime. sanitize-html is pure-JS (htmlparser2-based) with no DOM dependency and works in both Workers and Node.js. Sanitizing once at the data boundary means both the SSR'd initial HTML response and client re-renders receive safe text. - Remove the DOMPurify import from comment.jsx; the client component no longer needs to sanitize since text is already clean when passed as a prop. - Replace isomorphic-dompurify with sanitize-html in the workspace catalog. - Remove inert eslint-disable-next-line comment from script.tsx; the project uses oxlint which ignores ESLint-specific inline suppression directives. --------- Co-authored-by: Luke Percy <lpercyagile@gmail.com> |
||
|
|
063bf00be5 |
fix(build): eliminate Vite 8 treeshake.preset warning (#746)
* fix(build): eliminate Vite 8 treeshake.preset warning Add version-gated getClientTreeshakeConfigForVite() function that returns Rollup-compatible config (with preset) for Vite 7 and Rolldown-compatible config (without preset) for Vite 8+. The treeshake.preset option is Rollup-specific and causes warnings in Vite 8 which uses Rolldown. The moduleSideEffects: 'no-external' option is valid in both bundlers and is preserved in all configurations. Changes: - Add getClientTreeshakeConfigForVite(viteMajorVersion) function - Update 4 usage sites to use version-gated function - Mark clientTreeshakeConfig as @deprecated for backward compatibility - Export getClientTreeshakeConfigForVite for testing - Add comprehensive tests for Vite 7/8/9+ compatibility - Update existing tests to expect Vite 8 format Fixes #540 * refactor(build): address PR review feedback for treeshake config Remove dead workaround plugin from playground - Deleted strip-rolldown-incompatible-rollup-options plugin from examples/app-router-playground/vite.config.ts - This workaround is no longer needed since getClientTreeshakeConfigForVite() never emits treeshake.preset for Vite 8+ Add documentation comment about Rolldown defaults - Enhanced JSDoc for getClientTreeshakeConfigForVite() to explain behavior gap - Documents how Rolldown defaults compare to Rollup's recommended preset - Clarifies that moduleSideEffects: no-external is the key optimization Fix test import style - Changed from dynamic await import() to static imports - Matches existing test patterns in build-optimization.test.ts - Removes async from test cases that don't need it Addresses review comments from PR #746 * docs(build): clarify Rolldown treeshake divergence in comments Address PR #746 review feedback from @ask-bonk: - JSDoc: Explicitly call out unknownGlobalSideEffects as a known acceptable divergence (Rolldown defaults to true vs Rollup recommended preset's false). Makes it clear this is intentional, not an oversight. - Inline comment: Note that Rolldown's built-in defaults already cover what Rollup's 'recommended' preset provides (annotations, correctContext, tryCatchDeoptimization). Provides immediate context at the code site. Both changes are documentation-only; no functional impact. * chore: address PR review feedback on treeshake config - Update comment to reference getClientTreeshakeConfigForVite instead of deprecated clientTreeshakeConfig - Remove redundant not.toHaveProperty('preset') assertions from tests (toEqual already does exact matching) Addresses review feedback from @ask-bonk[bot] on PR #745 |
||
|
|
1ff166eb92 |
chore: turn on more lint rules (#714)
* chore: turn on more lint rules * fmt * add todos |
||
|
|
083e973752 |
fix(build): use createBuilder for Pages Router so cloudflare plugin runs (#711)
* fix(build): use createBuilder for Pages Router so cloudflare plugin runs * fix environments * fix for tests * another guard |
||
|
|
892fce6315 | fix(app-router-playground): clean warnings and stabilize checks (#694) |