Commit Graph

6 Commits

Author SHA1 Message Date
James Anderson 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
2026-06-29 17:33:51 +00:00
James Anderson 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
2026-06-26 17:09:19 +01:00
James Anderson 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
2026-06-23 20:20:36 +01:00
Divanshu Chauhan (divkix) bf5b09c46f refactor(pages-router): unify triplicated request pipeline into runPagesRequest (#1782) (#1860)
* feat(pages-router): add runPagesRequest pipeline owner with focused tests

Introduces server/pages-request-pipeline.ts which owns the canonical
Next.js 9-step request ordering once. Returns a PagesPipelineResult
discriminated union: type:response for prod/worker callers that supply
render/api callbacks, type:render|api|next intents for dev callers that
omit them. Mirrors createAppRscHandler's thin-closure injection pattern.

Two latent drift bugs reconciled vs the worker copy:
- middlewareStatus now uses result.status ?? result.rewriteStatus
- applyMiddlewareRequestHeaders now passes preserveCredentialHeaders

Adds 28 focused behavior tests covering all pipeline branches.
Adds ./server/pages-request-pipeline export to package.json.

* refactor(pages-router): migrate prod-server.ts to delegate to runPagesRequest

Replaces the ~400-line inlined try block in startPagesRouterServer with a
thin adapter that calls runPagesRequest(webRequest, deps). The adapter
retains only Node-specific concerns: open-redirect guard, decode/400,
prerender endpoint, static assets, image opt, basePath strip, _next/data
normalization, and compression/streaming output.

Extends renderPage callback signature with optional stagedHeaders param
so CSP nonces and other pre-render header injection continue to work.
Copies __vinextStreamedHtmlResponse marker through mergeHeaders to
preserve stream-vs-buffer decision in the adapter.

276 pages-router tests, 112 routing tests, 28 pipeline tests all pass.

* refactor(pages-router): migrate deploy.ts worker template to delegate to runPagesRequest

Collapses the ~280-line inlined fetch handler into ~65 lines. Worker adapter
keeps only: registerConfiguredCacheAdapters, open-redirect guard, static-asset
404, x-nextjs-data header capture, filterInternalHeaders+cloneRequestWithHeaders,
basePath strip with hadBasePath, and image optimization. Delegates the rest to
runPagesRequest via PagesPipelineDeps.

Removes inlined imports: matchRedirect, matchRewrite, preserveRedirectDestinationQuery,
requestContextFromRequest, applyMiddlewareRequestHeaders, isExternalUrl, proxyExternalRequest,
sanitizeDestination, applyConfigHeadersToHeaderRecord, normalizeTrailingSlash, mergeHeaders,
normalizeDefaultLocalePathname, stripI18nLocaleForApiRoute, mergeRewriteQuery.

Updates deploy.test.ts assertions to verify delegation rather than inlined
step logic. 245 deploy tests pass.

* refactor(pages-router): migrate index.ts dev to runPagesRequest, delete parallel helpers

Replaces the inlined dev Pages Router pipeline with runPagesRequest. The
adapter keeps Node-specific concerns: Vite skips, cross-origin guard, image
302 redirect, .html normalization, open-redirect guard, decode/400, basePath
strip with hadBasePath, trailing-slash, _next/data normalization, rawHeaders
snapshot, cloudflare-plugin delegation.

Creates devRunMiddlewareAdapter closure that wraps the dev runMiddleware
(runner, path, i18n, basePath, trailingSlash, isDataRequest args) and
returns a MiddlewareResult-compatible object. Sets VINEXT_MW_CTX_HEADER for
hybrid app+pages mode as a side effect before returning.

Adds writeWebResponseToNodeRes() helper to stream Web Response to Node res
preserving multi-value Set-Cookie. Adds requestHeaders field to render/api
intents so dev adapter can flush post-middleware request headers to req.headers
before calling createSSRHandler/handleApiRoute.

Deletes:
- applyRedirects (lines ~4829-4869)
- proxyExternalRewriteNode (lines ~4867-4928)
- applyRewrites (lines ~4929-4951)
- applyHeaders (lines ~4951-5015)

276 pages-router tests, 112 routing tests, 245 deploy tests, 28 pipeline
tests all pass. vp check clean.

* refactor(pages-router): format deploy.test.ts string literals consistently

* fix(pages-router): restore hasAppDir dev gate; fix external proxy body/headers; update stale after-deploy assertions

Three regressions introduced by the dev pipeline refactor:

1. Hybrid app+pages dev mode: the 'render' intent branch in handlePagesMiddleware
   unconditionally called createSSRHandler, missing the original hasAppDir fallthrough
   gate. App Router page requests in the hybrid app-basic fixture returned 404 instead
   of being deferred to the RSC plugin. Restored by matching the route before calling
   the Pages SSR handler and returning next() when there is no pages match and hasAppDir
   is true.

2. External proxy body/headers: proxyExternalRewriteNode read the body from the Node
   IncomingMessage and the post-middleware headers from req. The refactored pipeline used
   a body-less webRequest, so POST bodies were empty and middleware-modified request
   headers (x-hello-from-middleware1 etc.) were not forwarded. Fixed by adding an
   optional proxyExternal dep to PagesPipelineDeps; the dev adapter supplies it, building
   a proper Request from the pipeline's current (post-middleware) headers and the Node
   req body stream. Prod and worker adapters fall through to proxyExternalRequest as before.

3. Stale after-deploy assertions: two tests checked old inline call-site strings that
   no longer appear in the generated worker entry after the delegation refactor. ctx is
   still forwarded (deploy.ts:654,657); updated assertions to match the new call sites.

* fix(pages-router): restore public-file serving + API content-type; gate dev api flushes

Three behavioral regressions from the pipeline extraction, plus the bonk
review findings:

- prod-server (Node): restore post-middleware public-directory static file
  serving (original step 5b). The adapter now supplies a serveStaticFile dep
  that the pipeline calls after middleware (so middleware can intercept) and
  before rewrites. Without it, public/ files (favicon.ico, robots.txt, etc.)
  fell through to the renderer and 404'd. Adds a {type:"handled"} result for
  callbacks that write their own output.

- prod-server (Node): restore the application/octet-stream content-type default
  for API responses. The unified response path collapsed API and page fallbacks
  to text/html; the pipeline now tags API results with isApiResponse so the Node
  adapter picks octet-stream (arbitrary data) over text/html (content-sniffing
  hazard). [bonk finding 1]

- index.ts (dev): gate the staged-header/req-header flushes behind a pages-api
  match, mirroring the original 'if (apiMatch)' guard. The unconditional
  flushRequestHeaders() wiped req.headers (incl. VINEXT_MW_CTX_HEADER) on the
  api-miss -> next() path, dropping middleware context for app/api/* routes in
  hybrid app+pages dev mode.

- pipeline: note {type:"next"} is reserved/unused [bonk finding 2]; document
  the intentional external-proxy merge asymmetry on the bare proxyExternal
  returns [bonk finding 3].

Adds 5 pipeline tests (serveStaticFile/handled ordering, isApiResponse tag).

* fix(pages-router): forward staged headers to worker renderPage; gate data-request defer

Two divergences found in code review of the extraction:

- deploy.ts (worker): the renderPage adapter dropped the pipeline's 4th
  stagedHeaders arg, passing undefined for the SSR renderer's middlewareHeaders
  param — so middleware-set CSP nonces were never applied to rendered HTML on
  the worker, unlike the prod path. Widen the wrapper to forward stagedHeaders.

- pipeline: shouldDeferErrorPageOnMiss only gated on isDataReq, which the worker
  never sets (it doesn't normalize /_next/data paths). The pre-refactor worker
  gated on the x-nextjs-data header (isDataRequest), so a data-request miss now
  wrongly deferred + ran fallback rewrites + re-rendered. Gate on both signals;
  no-op for Node/dev where a data request already has isDataReq=true.

Updates the after-deploy assertion to the new call site and adds a pipeline
test for the data-request defer gate.

* fix(pages-router): address code-review findings (content-type, redirect query, dedup)

- prod content-type: passthrough responses (middleware short-circuits, external
  proxies) no longer get a text/html default injected. Generalize the response
  tag to defaultContentType (render -> text/html, api -> octet-stream, passthrough
  -> unset); the Node adapter sends untagged responses verbatim via sendWebResponse,
  matching pre-refactor behavior.

- redirect query: config-redirect Location now uses the raw req.url query (via a
  new rawSearch dep supplied by the Node prod + dev adapters) instead of
  new URL().search, which re-encoded chars and truncated at a literal '#'. The
  worker keeps url.search (it only ever had a Web Request). Restores the original
  prod/dev behavior.

- efficiency: matchPageRoute no longer runs twice per request on the common
  no-afterFiles-rewrite path; reuse the Step 12 match unless afterFiles changed
  the pathname.

- dedup: collapse prod-server's mergeWebResponse into the canonical mergeHeaders
  (worker-utils) via delegation, removing the duplicate implementation and the
  now-orphaned hasHeader/stripHeaders helpers. Existing mergeWebResponse tests
  validate the delegation.

Updates pipeline tests for defaultContentType and adds a passthrough-verbatim
assertion. 36 pipeline + 309 features + 276 pages-router + 245 deploy + 9
after-deploy + 112 routing pass; vp check clean.

---------

Co-authored-by: James <james@eli.cx>
2026-06-09 11:47:31 +01:00
James Anderson b06a1a1d04 fix(i18n): strip locale prefix for API routes (#1408)
* fix(i18n): strip locale prefix for API routes

Mirror Next.js's behaviour where a locale-prefixed request path like
`/fr/api/ok` is matched against `pages/api/ok` after stripping the
locale segment. Previously the locale prefix was kept through the
`/api/` startsWith check, so any locale-prefixed API URL 404'd.

Add a small helper `stripI18nLocaleForApiRoute(url, i18nConfig)` in
`server/pages-i18n.ts` that mirrors Next.js's
`normalizeLocalePath().pathname` (see
packages/next/src/shared/lib/i18n/normalize-locale-path.ts) and wire it
into all three Pages Router request pipelines so dev/prod parity holds:

- dev plugin: `packages/vinext/src/index.ts`
- prod (Node): `packages/vinext/src/server/prod-server.ts`
- prod (Cloudflare worker entry): `packages/vinext/src/deploy.ts`

App Router is intentionally unchanged: Next.js does not support the
`i18n` config field with App Router, so App Router route handlers
under `app/*/route.ts` never see a locale prefix to strip.

Refs #1336 (item 3). Items 2 (sticky locale on client navigations) and
4 (default-locale path normalisation) are being handled in parallel
PRs.

Tests:
- Unit tests for `stripI18nLocaleForApiRoute` covering basic, regional
  (`nl-NL`), query-preserving, unconfigured-prefix, and null-config
  cases (`tests/pages-i18n.test.ts`).
- Dev integration tests (`tests/features.test.ts`) and prod integration
  tests (`tests/pages-i18n-prod.test.ts`) that GET `/fr/api/ok` and
  assert a 200 "ok" response against the existing
  `pages-i18n-domains` fixture, ported from Next.js's
  `test/e2e/middleware-redirects/test/index.test.ts` ("should redirect
  to api route with locale").
- Updates to `tests/deploy.test.ts` assertions for the generated
  worker-entry template strings.

* test(after-deploy): update worker-entry assertion after locale strip

The Pages Router worker entry now calls handleApiRoute(request,
apiLookupUrl, ctx) instead of using resolvedUrl directly, so this
generated-code assertion needs the new variable name.

Refs #1336 (item 3).
2026-05-21 15:02:18 +01:00
James Anderson c66b86e8f5 fix(after): wire next/after to Workers ctx.waitUntil in deploy mode (#1403)
Pages Router API routes did not receive the per-request Workers
`ExecutionContext`, so any `after()` (or other shim) call inside a
`pages/api/*` handler had no `ctx.waitUntil()` to keep the isolate
alive past the response. Thread `ctx` through `handleApiRoute`
→ `handlePagesApiRoute`, and wrap the user handler in
`runWithExecutionContext(ctx, ...)` so the ALS surfaces the ctx
for downstream `after()` calls.

Also pass a Node-shaped execution context from `vinext start` so
behavior is consistent across the dev / start / deploy server
paths.

Closes #1365
2026-05-21 10:04:38 +00:00