16 Commits

Author SHA1 Message Date
Nathan Nguyen 405e46af1d fix: preserve middleware redirect headers in production (#365)
* fix: preserve middleware redirect headers (Set-Cookie) in production

middleware.ts correctly collects non-internal headers (e.g. Set-Cookie)
on redirect responses, but three consumers dropped them:

- prod-server.ts: sent only Location header, ignored responseHeaders
- deploy.ts: worker entry sent only Location header
- index.ts: inline runMiddleware codegen didn't collect headers on redirect
  (it already did for next() and rewrite(), just not redirect)

The dev server (index.ts:2934) handled this correctly, making this a
dev/prod parity bug where cookies set during middleware redirects worked
in development but were silently dropped in production.

* test: update entry template snapshot for redirect header fix

---------

Co-authored-by: James <james@eli.cx>
2026-03-09 09:52:45 +00:00
Divanshu Chauhan (divkix) 1a2fd61092 fix: fail build when cloudflare() plugin is missing (#358)
* fix: fail build when cloudflare() plugin is missing

Upgrade the deploy-time warning to a hard error and add a build-time
check in configResolved that detects a wrangler config without the
cloudflare() plugin registered.

Previously, forgetting to add cloudflare() to vite.config.ts produced
a cryptic "could not resolve virtual:vinext-rsc-entry" error deep in
wrangler esbuild. Now the build fails early with an actionable message
showing exactly what to add.

The validation only fires during builds (not dev) and is skipped when
Nitro is the deployment target.

Closes #325

* refactor: extract shared utilities for cloudflare plugin guard

Address code review feedback on PR #358:

- Extract `hasWranglerConfig(root)` utility from `detectProject()` inline
  check, reuse in both `deploy.ts` and `index.ts:configResolved`
- Extract `formatMissingCloudflarePluginError({ isAppRouter, configFile? })`
  shared error formatter used by both build-time and deploy-time guards
- Fix hardcoded `vite.config.ts` references: build-time guard now uses
  `config.configFile`, deploy-time guard uses generic "your Vite config"
- Add 10 unit tests covering both new utilities

* Update packages/vinext/src/deploy.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

---------

Co-authored-by: James Anderson <james@eli.cx>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-03-08 22:32:58 +00:00
MD YUNUS faca3dd663 fix(cli): ensure type:module before Vite loads vite.config.ts (fixes #184) (#197)
Projects that lack "type":"module" in package.json caused Vite to bundle
vite.config.ts through esbuild's CJS path, producing a .vite-temp/*.mjs file
that contains a CJS require() for @cloudflare/vite-plugin. On Node 22 in CI
(GitHub Actions ubuntu-latest), require()-ing an .mjs file is not supported
and the build fails with:

  Error: Dynamic require of "…/@cloudflare/vite-plugin/dist/index.mjs" is
  not supported

Fix: call ensureESModule(root) + renameCJSConfigs(root) in both the dev() and
buildApp() CLI commands, before Vite is loaded. This mirrors what vinext init
already does, but applies lazily for projects that were set up manually or
before the init step was introduced.

- renameCJSConfigs renames postcss.config.js / tailwind.config.js / .eslintrc.js
  etc. to .cjs first, to avoid breakage in those files after ESM mode is enabled
- ensureESModule adds "type":"module" to package.json
- Both are no-ops when the project is already correctly configured
- A warning is emitted when a change is made, pointing users to vinext init
- 9 new unit tests cover the full scenario including multi-file rename

Fixes #184
2026-03-08 17:10:11 +00:00
Nathan Nguyen 2f21bb3a7d fix: pass NextFetchEvent as second argument to middleware (#340)
* fix: pass NextFetchEvent as second argument to middleware

All 3 middleware invocation sites were calling `middlewareFn(nextRequest)`
with only one argument. The `NextMiddleware` type expects `(request, event)`
but `NextFetchEvent` was never instantiated or passed. Any middleware using
`event.waitUntil()` would throw at runtime because `event` is `undefined`.

Fix: instantiate `NextFetchEvent({ page })` at each call site, pass it as
the second argument, and drain `_waitUntilPromises` via
`Promise.allSettled()` (fire-and-forget) after middleware returns.

Call sites fixed:
- `packages/vinext/src/server/middleware.ts` (shared runner, used by prod)
- `packages/vinext/src/index.ts` (Pages Router dev codegen)
- `packages/vinext/src/server/app-dev-server.ts` (App Router dev codegen)

* address review: add drainWaitUntil(), wire ctx.waitUntil() in Workers entry, fix async test

* fix deploy tests: update assertions for ctx-aware fetch and runMiddleware signatures

* address review: fix misleading comment, update JSDoc, use queueMicrotask in test

* pass undefined explicitly to runMiddleware in prod-server (no ctx on Node.js)

---------

Co-authored-by: James <james@eli.cx>
2026-03-08 10:01:47 +00:00
James Anderson 70b18536ab fix: warn when existing vite.config.ts is missing cloudflare plugin on deploy (#315)
* fix: warn when existing vite.config.ts is missing cloudflare plugin on deploy

Closes #312.

- Add `viteConfigHasCloudflarePlugin()` to deploy.ts: when `vinext deploy`
  finds an existing vite.config.ts that doesn't import @cloudflare/vite-plugin,
  it now prints an actionable warning with the exact config fix rather than
  silently proceeding to a broken build.
- Add README prerequisites section to the Cloudflare Workers deploy docs:
  covers wrangler login vs CLOUDFLARE_API_TOKEN (with required permissions)
  and how to supply account_id.
- Add 7 unit tests for `viteConfigHasCloudflarePlugin()`.

* fix: pages router warning shows cloudflare() not cloudflare(())
2026-03-07 14:50:58 +00:00
James Anderson 09d9d4cde0 refactor: extract applyMiddlewareRequestHeaders into config-matchers (#305)
* refactor: extract applyMiddlewareRequestHeaders into config-matchers

Move the x-middleware-request-* unpacking + postMwReqCtx rebuild logic
out of prod-server.ts and deploy.ts and into a shared
applyMiddlewareRequestHeaders() in config-matchers.ts, eliminating ~30
lines of duplicated code from each callsite.

* fix(test): update deploy test to reflect applyMiddlewareRequestHeaders refactor
2026-03-07 11:11:56 +00:00
harris.dev c03cb3f4b4 fix(deploy): monorepo-aware lock file and node_modules resolution (#122)
* fix(deploy): monorepo-aware lock file and node_modules resolution

vinext deploy failed in monorepos in two ways:

1. Wrong package manager detected — detectPackageManager only checked for
   bun.lockb (legacy binary format), missing bun.lock (text format, Bun
   v1.0+). It also only searched the immediate project directory, so any
   monorepo where the lock file lives at the workspace root fell through to
   npm, causing a confusing "npm error Invalid Version:" failure.

2. Dependency detection and wrangler binary path hard-coded to the app
   directory — hasCloudflarePlugin, hasRscPlugin, hasWrangler, hasMdxRollup
   all checked {root}/node_modules only, producing false negatives for
   hoisted packages. runWranglerDeploy then crashed with ENOENT because
   the wrangler binary was at the workspace root, not the app root.

Changes:
- Add walkUpUntil<T>(start, check) to utils/project.ts — a shared primitive
  that walks ancestor directories until check() returns non-null. Both the
  lock-file and node_modules walkers are built on top of this.
- Rewrite detectPackageManager / detectPackageManagerName using walkUpUntil;
  add bun.lock (text) check alongside existing bun.lockb (binary).
- Add findInNodeModules(start, subPath) using walkUpUntil — walks up to find
  node_modules/{subPath}, returning the absolute path or null.
- Use findInNodeModules in detectProject (hasCloudflarePlugin, hasRscPlugin,
  hasWrangler), getMissingDeps (hasMdxRollup), and runWranglerDeploy.

Tests (12 new):
- detectPackageManager: pnpm, yarn, bun.lock, bun.lockb, npm fallback,
  monorepo root traversal, closest-wins precedence
- findInNodeModules: package lookup, binary lookup, null fallback,
  monorepo root traversal, closest-wins precedence

* fix(test): clear npm_config_user_agent in npm fallback test

On CI, pnpm sets process.env.npm_config_user_agent which causes
detectPackageManagerName to return 'pnpm' instead of falling through
to the npm default. Save and restore the env var around the assertion.

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-07 01:37:59 +00:00
Nathan Nguyen af0a839a67 fix: preserve multiple Set-Cookie headers in response merging (#297)
* fix: preserve multiple Set-Cookie headers in prod-server and worker entry

The response header merging in prod-server.ts and the generated Cloudflare
worker entry used Record<string, string> which flattened multiple Set-Cookie
headers — the last value won, or cookies with Expires dates got corrupted
by comma-joining.

Extract mergeResponseHeaders() helper that uses getSetCookie() to preserve
array-valued Set-Cookie headers. Apply the same fix to the worker entry
template in deploy.ts using the Headers API.

Fixes #295

* fix: address review feedback on Set-Cookie header preservation

- Add `as string` cast in deploy.ts worker entry for x-middleware-request-*
  header unpacking (matches prod-server.ts)
- Normalize Vary header with Array.isArray check in sendCompressed to handle
  the widened type correctly
- Add edge-case test for middleware cookie as plain string (not array)

* fix: apply Set-Cookie fix to example workers, remove redundant toLowerCase

- Fix the same Set-Cookie flattening bug in hand-written example worker
  entries (pages-router-cloudflare, realworld-api-rest)
- Remove 4 redundant .toLowerCase() calls — Headers.forEach() always
  yields lowercase keys

* fix: add missing as string cast in deploy.ts middleware header collection

* refactor: extract mergeHeaders to shared vinext/server/worker-utils

The mergeHeaders function was duplicated in both example worker entries
(pages-router-cloudflare, realworld-api-rest) and the deploy.ts template.
Extract it to a shared module that example workers import. The deploy.ts
template still inlines it (code generation constraint).

Reduces 3 copies to 2 (shared module + generated template).

* fix: array-accumulate Set-Cookie in config headers, fix JSDoc, add behavioral test

- deploy.ts template: config headers section was comma-joining Set-Cookie values
  using += which corrupts cookies with Expires dates and loses all but the last
  cookie when multiple config rules match. Matches the array-accumulation pattern
  already used in prod-server.ts (lines 899-907) and the middleware section above.
- worker-utils.ts + deploy.ts template JSDoc: 'Response headers take precedence'
  was misleading — Set-Cookie is additive, not overriding. Clarify both docs.
- tests/deploy.test.ts: add behavioral test for mergeHeaders via worker-utils import
  (same function inlined in the generated template), replacing reliance on
  string-contains assertions alone.

* fix: indentation in deploy.ts template, array-accumulate Set-Cookie in example workers

- deploy.ts: fix extra leading space in config headers block (lines 636-659)
  and JSDoc lines 740-743 introduced in previous commit
- pages-router-cloudflare, realworld-api-rest: config headers section was
  doing a plain assignment (middlewareHeaders[lk] = h.value) which overwrites
  array-valued Set-Cookie built up by the middleware section above; use the
  same array-accumulation pattern as prod-server.ts and the deploy.ts template

* fix: remove as any casts in prod-server.ts, add Vary handling to example workers

- prod-server.ts lines 904/906: middlewareHeaders is now Record<string, string | string[]>
  so the as any casts are unnecessary; replace with as string (consistent with the
  middleware collection section above at line 848)
- pages-router-cloudflare, realworld-api-rest: add Vary comma-joining to config
  headers section to match prod-server.ts (line 908) and deploy.ts template (line 653)

---------

Co-authored-by: James <james@eli.cx>
2026-03-06 23:53:23 +00:00
Stringer fe0a48dde4 fix: lowercase package name before sanitizing Worker name (#159)
* fix: lowercase package name before sanitizing Worker name

* test: assert exact sanitized project name value

Strengthen the sanitization test to assert the exact expected value
('my-app-v2') instead of just matching a regex pattern. The regex
assertion passed even when the bug was present.

---------

Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
2026-03-03 17:35:54 +00:00
Dane b70f4037fb fix: preserve x-middleware-request-* headers in generated runMiddleware (#175)
* fix: preserve middleware request override headers in runMiddleware

* Strip x-middleware-* headers, detect bun, update fixtures

Preserve and unpack x-middleware-request-* headers while removing any other x-middleware-* headers in the worker entry (defense-in-depth). Add bun package manager detection in project utils (detectPackageManager and detectPackageManagerName). Update tests to assert the new header-stripping behavior. Update pages-basic fixture entry: add explicit void for promise calls, inject a middleware header override route, add dangerous-scheme blocking for Link hrefs, include credentials on prefetch, update dynamic import asset names and file metadata, and rename several getServerSideProps helper identifiers to avoid collisions.

* Normalize absolute file paths in fixture

Update fileName annotations in tests/fixtures/pages-basic/dist/server/entry.js to replace local developer paths (/Users/sunilpai/...) with CI runner paths (/home/runner/work/...). This cleans up generated JSXDEV metadata in the compiled test fixture to avoid leaking local machine paths and make outputs reproducible.

---------

Co-authored-by: Sunil Pai <spai@cloudflare.com>
2026-02-27 22:29:46 +00:00
Steve Faulkner 75703d3b72 Align Pages Router worker entry with prod-server request handling (#171)
* Skip deploy previews for fork PRs that lack Cloudflare secrets

Fork PRs don't have access to repository secrets (CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ACCOUNT_ID), so deploy/smoke-test/comment steps always fail.
Add a fork detection condition to skip these steps gracefully. The build
steps still run, so example builds are still validated for fork PRs.

* fix: align Pages Router worker entry with prod-server request handling

The generated Cloudflare Worker entry for Pages Router apps was missing
several request handling steps that the Node.js production server
(prod-server.ts) already handled. This brings the two in sync:

- Run middleware (runMiddleware) before routing
- Apply next.config.js redirects, rewrites (before/after/fallback), and headers
- Handle basePath stripping and trailing slash normalization
- Merge middleware response headers with correct precedence
- Guard renderPage with typeof check

Also updates the two Pages Router examples (pages-router-cloudflare,
realworld-api-rest) and exports vinext/config/config-matchers so the
worker entry can import the shared matching utilities.

* chore: sync pnpm-lock.yaml with upstream package.json changes
2026-02-27 13:01:34 -06:00
Steve Faulkner 16988a5403 fix: add request validation to dev server (#98) 2026-02-25 22:51:16 -06:00
Steve Faulkner 61554eaf37 fix: install react-server-dom-webpack during init and deploy for App Router (#31)
* fix: install react-server-dom-webpack during init and deploy for App Router

vinext init was missing react-server-dom-webpack from its dependency list,
causing 'Could not resolve react-server-dom-webpack/server.edge' errors on
fresh create-next-app projects. Additionally, react-server-dom-webpack@19.2.4
requires react@^19.2.4, but create-next-app ships react@19.2.3 -- the version
mismatch caused npm to nest rsdw inside vinext/node_modules where Vite can't
resolve it.

Fix: add react-server-dom-webpack to getInitDeps for App Router, detect when
the installed React version is too old, and upgrade react/react-dom as regular
dependencies before installing dev deps (avoiding ERESOLVE peer-dep conflicts
and keeping react in 'dependencies' rather than 'devDependencies').

Also applies the same fix to vinext deploy for parity, and adds a CI job that
scaffolds a real create-next-app project, runs vinext init, and verifies the
dev server returns HTTP 200.

Closes #22

* refactor: use createRequire for package resolution instead of direct node_modules reads

Address review feedback (PR #18 pattern): replace fs.existsSync(node_modules/...)
with createRequire-based resolution, which handles hoisting, pnpm symlinks,
monorepos, and Yarn PnP correctly.

- deploy.ts: extract isPackageResolvable() helper using createRequire, use it
  in getMissingDeps for react-server-dom-webpack detection
- init.ts: refactor getReactUpgradeDeps() to use createRequire + findPackageVersion
  walk-up pattern instead of reading node_modules/react/package.json directly
- tests: add isPackageResolvable unit tests, use dependency injection for
  getMissingDeps rsdw test (Vitest's createRequire intercepts resolution)

* ci: add Windows to create-next-app smoke test

Run the create-next-app compatibility test on both ubuntu-latest and
windows-latest. Replace bash-specific server health check (curl, kill,
backgrounding) with a cross-platform Node script using child_process
and http. Use runner.temp instead of /tmp for portable paths.

* fix: use pathToFileURL for dynamic import() calls on Windows

On Windows, Node's ESM loader requires file:// URLs for absolute paths.
Bare paths like D:\... are rejected with ERR_UNSUPPORTED_ESM_URL_SCHEME.

Wrap all dynamic import() calls that receive absolute paths from
require.resolve() or path.join() with pathToFileURL().href:

- index.ts: RSC plugin import, RSC transforms import, PostCSS config
  loading, PostCSS plugin resolution (5 call sites)
- cli.ts: Vite module import from project root

* fix: pathToFileURL for cache-runtime codegen and test imports

Additional Windows fixes found via PR #43 review:

- index.ts: use pathToFileURL for runtimeModulePath in 'use cache'
  transform code generation (2 locations) — these embed absolute paths
  into generated import() statements via JSON.stringify
- pages-router.test.ts: wrap 9 import(serverEntryPath) calls with
  pathToFileURL so tests work on Windows
2026-02-25 08:38:54 -06:00
Steve Faulkner 05ef04788c feat(deploy): add --env support with security and parsing improvements (#49)
* feat(deploy): add --env support for wrangler deploy

Adds vinext deploy --env <name> and threads it through to wrangler deploy --env <name>. Keeps --preview working as shorthand for preview environment, adds unit tests for wrangler arg construction, and updates README/help examples. Refs #33.

* fix(deploy): use execFileSync and util.parseArgs for --env support

Build on #44 by liuxiaopai-ai — adds --env flag for wrangler deploy.

Security: switch from execSync(shell string) to execFileSync(binary, args)
to eliminate command injection via unsanitized --env/--name values.

Replace hand-rolled flag parsing (parseStringFlag, parseNumericFlag,
rawArgs.includes) with Node.js built-in util.parseArgs. This gives us
strict mode (throws on unknown flags), automatic --flag=value handling,
and proper error messages for missing values.

Also: buildWranglerDeployArgs now returns { args, env } directly instead
of re-parsing the args array for the log message.

---------

Co-authored-by: root <root@localhost.localdomain>
2026-02-25 06:57:44 -06:00
Fred K. Schott 2b04681406 fix: resolve vinext init issues with missing dep, duplicate rsc plugin, and peer dep conflict (#15)
- Install vinext itself as a devDependency during init (#11)
- Remove explicit rsc() from generated configs since vinext auto-registers it (#12)
- Widen react/react-dom peer dep to >=19.2.0 for create-next-app compat (#13)

Co-authored-by: Fred K. Schott <fschott@cloudflare.com>
2026-02-24 12:29:26 -06:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00