discoverInheritedParallelSlots reads every ancestor directory of each route with fs.readdirSync to find @slot dirs. Because routes share ancestors, a directory with N sibling routes is read once per descendant route, making the scan super-linear in route-directory width. Memoize the reads in a per-scan WeakMap<matcher, Map<dir, Dirent[]>> (same pattern as findFileProbeCache/findSlotSubPagesCache), scoped to one scan so it's collected afterwards. On a 10k-route fixture this cuts build wall time ~52% (~86s -> ~42s) and syscall time ~61%, output byte-identical.
Benchmark validator tests mock GitHub API paths for cloudflare/vinext, but fork CI sets GITHUB_REPOSITORY to the fork owner.
Pin the spawned validator process to cloudflare/vinext so the test remains hermetic across upstream and fork push runs.
* fix(app-router): honor global not found opt-in
* fix(app-router): render default global 404 standalone
* test(app-router): avoid fixture workspace lockfile entry
* test(app-router): keep fixture esm outside workspace
* fix(app-router): add default global 404 shell
* test(app-router): cover client not found boundary
* fix(app-router): gate global not found discovery
* 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