* feat: support the React Compiler via react: { compiler: true }
@vitejs/plugin-react 6.1.0 added an experimental `compiler` option backed by
oxc-transform-react. vinext already forwards its `react` option to that plugin,
but enabling the compiler broke the build for apps with JSX in plain `.js`
files: `vite:react-compiler` and `vinext:jsx-in-js` both run at `enforce: "pre"`
and the React plugin was registered first, so the compiler parsed those files as
plain JS and failed with `Unexpected JSX expression`.
Register `vinext:jsx-in-js` before `@vitejs/plugin-react`, bump the plugin-react
catalog entry to ^6.1.0, add oxc-transform-react as a dev dependency for the
tests, and document the option.
* fix: fail fast when react.compiler is set on plugin-react 6.0
Addresses review feedback.
@vitejs/plugin-react 6.0 accepts an unknown `compiler` key and drops it, and it
still satisfies vinext's published peer range, so a project that upgrades vinext
without upgrading the plugin would silently get no React Compiler. Probe the
plugin list the factory returns and throw an actionable error when the compiler
plugin is missing.
Also drops the hand-authored changeset: AGENTS.md says changesets are generated
from Conventional Commits in CI.
* fix: keep the compiler version error out of the import failure wrapper
Addresses review feedback.
The trailing `.catch()` wrapped every rejection from the plugin-react promise
chain, including the compiler version check, so the actionable message and its
install command were demoted to a nested `cause` behind
`vinext: Failed to load @vitejs/plugin-react.`. Attach the wrapper to the
dynamic import itself so only genuine import failures are relabeled.
* fix: move only the compiler plugin instead of the whole React group
Addresses review feedback.
Reordering the entire @vitejs/plugin-react group put `vinext:jsx-in-js` ahead of
`vite:react-babel`, so on plugin-react 5.x, which is still in the supported peer
range and does expose a `babel` option, a user-configured Babel transform would
stop seeing JSX in plain `.js` files. Split `vite:react-compiler` out of the
group and register just that plugin after `vinext:jsx-in-js`. Every other plugin
keeps the position it had before this PR.
Also documents that components using `<style jsx>` are not memoized, since
`vinext:styled-jsx` lowers their JSX through the Next.js SWC transform first.
Running the compiler ahead of it is not an option: the styled-jsx plugin selects
modules by matching `<style` in the source, and lowered JSX no longer matches,
which would silently disable styled-jsx entirely.
* fix(ci): account for React Compiler test dependency
---------
Co-authored-by: James <james@eli.cx>
* chore(deps): upgrade vite-plus to 0.2.6
* fix(app-router): suppress benign AbortError from superseded navigations
A fast follow-up navigation aborts the in-flight navigation's RSC fetch
mid-stream. When the aborted stream still holds an un-consumed React Flight
chunk (e.g. streamed metadata the superseded route never rendered), React
reports the resulting AbortError globally as a window `error` event rather
than to a specific consumer, which trips the "no console errors" e2e
assertion (metadata-icons.spec.ts).
Install a page-lifetime window listener at bootstrap that preventDefault()s
these benign navigation AbortErrors, mirroring the existing redirect-error
bridge. Installed once (not in a component effect) so there is no listener
gap while the router tree re-renders mid-navigation.
The vite-plus 0.2.6 toolchain shifted navigation timing enough to expose
this latent race on CI.
* chore: adapt to vite-plus 0.2.6 toolchain
Follow-up to the vite-plus 0.2.6 bump; keeps CI green under the new
oxfmt/oxlint/rolldown toolchain. No published runtime change.
- Formatting: oxfmt 0.60.0 reformats README.md, apps/web/next.config.ts and
tests/nextjs-compat/TRACKING.md (collapses empty-object-with-comment;
unpads markdown tables). Applied `vp check --fix`.
- Lint: oxlint 1.75.0 now flags dynamic `import("node:path")` under the
existing no-restricted-imports rule. Disabled inline in loadStaticPrerender
with a reason -- the resolved path feeds a dynamic import(), so pathslash's
forward-slash canonicalization buys nothing there.
- Test: rolldown code-splits the server build, so the font markers moved out
of index.js into _next/static/* chunks. Scan the whole server output for
the markers instead of index.js alone (verified they still exist).
* Revert "fix(app-router): suppress benign AbortError from superseded navigations"
Drop the runtime AbortError suppressor to keep this PR a pure toolchain bump.
The metadata-icons "rapid icon replacement" failure it addressed is a
pre-existing, timing-dependent race (not caused by the upgrade). Assessing
whether it triggers stably on CI / locally before deciding how and where to
fix it.
This reverts commit 84454d5fd of this branch.
The Cloudflare App Router `vite.config.ts` examples in README.md and in the
migrate-to-vinext skill both register `@vitejs/plugin-rsc` explicitly. Copying
either verbatim fails the build immediately:
[vinext] Duplicate @vitejs/plugin-rsc detected.
vinext auto-registers @vitejs/plugin-rsc when app/ is detected.
vinext has auto-registered the plugin since the `app/` detection landed
(packages/vinext/src/index.ts), and every deployed App Router example
(app-router-cloudflare, app-router-playground, hackernews) omits `rsc()`.
The docs are the stale part.
Both examples now match the working examples, plus a note on the non-obvious
bit: `@vitejs/plugin-rsc` is an optional peer, so it must be installed but not
registered, and `rsc: false` is the escape hatch for owning the registration.
Co-authored-by: piffie <1213363+piffie@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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
* 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
* 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>
PR #1701 added `resolve.noExternal: true` while loading a TypeScript
next.config so a baseUrl-local file could shadow an installed package of the
same name. That de-externalizes every installed package and forces it through
Vite's module runner, which breaks CJS config plugins that call `require` /
`require.resolve` at runtime — e.g. `@next/mdx`, which failed the
app-router-playground production build with "require is not defined".
This was not caught pre-merge because deploy-examples is skipped for fork PRs.
Drop the blanket `noExternal: true`. Installed packages stay externalized (so
CJS config plugins keep working), and the native Vite 8 `resolve.tsconfigPaths`
resolver still resolves baseUrl-local bare imports that have no installed
package of the same name (the real Next.js tsconfig-extends fixture). The only
behavior we give up is shadowing an installed package with a baseUrl-local file
of the same name, which no Next.js fixture exercises.
- Update the shadowing test to assert the installed package wins.
- Add a regression test for an @next/mdx-style CJS plugin that calls
`require.resolve` at runtime (red before this change).
- Document the package-precedence behavior in the README limitations entry.
* fix(config): resolve next.config baseUrl imports from tsconfig
next.config.ts could resolve tsconfig paths aliases but not bare imports covered by compilerOptions.baseUrl. This broke configs that inherit baseUrl through tsconfig extends and import local modules such as "bar" from next.config.ts.
Carry the effective baseUrl out of the tsconfig reader and add a narrow Vite runner resolver for bare specifiers so local files resolve before normal package fallback. Add a regression ported from the Next.js tsconfig-extends deploy fixture.
* fix(config): preserve baseUrl priority over packages
next.config.ts imports that matched both a local tsconfig baseUrl target and an installed package resolved to the package first. That violated the TypeScript and Next lookup order where baseUrl local resolution wins before package fallback.
Vite's module runner externalized installed bare packages before resolveId hooks ran, so the baseUrl resolver only handled missing packages. Keep config imports inside the resolver pipeline, run the baseUrl resolver before package resolution, and scope it to project-owned importers so package dependencies are not rewritten through the app baseUrl.
Adds focused regression coverage for local-over-package shadowing, package fallback, node_modules importer isolation, and existing CJS/ESM package imports from next.config.ts.
* fix(config): keep baseUrl lookup scoped to TypeScript configs
next.config.mjs could inherit TypeScript baseUrl package shadowing because the config runner noExternal override and baseUrl resolver were enabled for every config file type. That leaked Next's TypeScript config transpilation semantics into non-TypeScript config loading.
Gate the baseUrl resolver and noExternal runner override behind TypeScript config extensions while keeping existing alias loading behavior unchanged. Add a regression for mjs package shadowing and update the tsconfig helper comment to name baseUrl explicitly.
* refactor(config): resolve next.config baseUrl via native Vite 8 tsconfigPaths
Replace the hand-rolled tsconfigBaseUrlResolverPlugin with Vite 8's native
resolve.tsconfigPaths (oxc-resolver tsconfig: 'auto'), which already mirrors
Next.js's SWC paths + baseUrl handling: follows extends, resolves baseUrl-local
bare imports before package fallback, and scopes the app baseUrl to
project-owned importers via per-importer tsconfig discovery.
Gate it behind the existing Vite-major check (matching index.ts). Vite 7 has no
native equivalent, so baseUrl bare imports in next.config.ts are a documented
Vite 7/8 capability gap; paths aliases still work on both via resolve.alias.
Extract getViteMajorVersion into utils/vite-version.ts so next-config.ts and
index.ts share one detection path without a circular import.
---------
Co-authored-by: James <james@eli.cx>
* 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.
* feat: add standalone self-host output for vinext build
Generate dist/standalone from output: 'standalone' with a runnable server entry and runtime deps, and align init scripts with vinext build/start for production self-host workflows.
* fmt
* Address bonk review: fix duplicate PHASE constant, complete lazy-chunks extraction, remove dead imports, add process.exit to standalone path
* Address bonk nits: document regex patterns, move PHASE re-exports to bottom, add assets field to BuildManifestChunk
* refactor(standalone): use Vite bundle graph for server externals instead of regex scan
Replace the fragile post-build regex scan of emitted JS files and the
package.json#dependencies over-seeding with a new
vinext:server-externals-manifest Vite plugin. The plugin collects
chunk.imports + chunk.dynamicImports from the SSR/RSC writeBundle hook
and writes dist/server/vinext-externals.json — the authoritative,
compiler-derived list of packages left external in the server bundle.
emitStandaloneOutput now reads this manifest as its sole seed for the
BFS node_modules copy, so standalone output only includes packages the
server actually imports at runtime. Removed collectServerExternalPackages,
walkFiles, the four regex constants, and the package.json deps seeding.
* fix(standalone): copy vinext runtime deps, add node_modules filter to cpSync, remove unused pendingWrites
* fix(standalone): address round-6 bonk nits — explicit root, manifest comment, pre-flight check, bare-specifier comment, export-from re-export
* fix(standalone): address round 7 bonk review comments
- harden outDir resolution in server-externals-manifest: replace fragile
walk-up heuristic with direct basename check (ssr -> dirname, else dir),
avoiding misfires when a user's project path contains a 'server' segment
- remove redundant 'chunk' alias in server-externals-manifest writeBundle;
use 'item' directly after the type guard narrows it to OutputChunk
- add node_modules filter to vinext dist/ cpSync in standalone.ts, matching
the same defensive filter used for app package copies
- rewrite standalone server.js as pure ESM using import.meta.dirname
(Node >= 21.2, vinext requires >= 22); drop CJS require/__dirname and
switch standalone package.json to 'type':'module'
- add explanatory comment for DEFAULT_PHASE constant in next-config.ts
- add comment in cli.ts noting pre-flight path and resolveVinextPackageRoot
must stay in sync
* fix(standalone): address round 8 bonk review comments
- fix test import: use 'vite-plus/test' instead of 'vitest' in
standalone-build.test.ts, matching the convention used by every other
test file in the repo (documented in AGENTS.md)
- extract resolveVinextPackageRoot into utils/vinext-root.ts so cli.ts
and standalone.ts share a single source of truth; removes the coupling
comment added in round 7 and the duplicated path traversal logic
- add node_modules filter to dist/client and dist/server cpSync calls for
defensive consistency with the vinext dist/ and package copies
* interface -> type
* fix(standalone): address round 9 bonk review comments
- add node_modules filter to public/ cpSync for defensive consistency
with all other cpSync calls in emitStandaloneOutput
- warn on malformed vinext-externals.json instead of silently returning
an empty list; includes the error string to aid debugging
- add doc comment on copyPackageAndRuntimeDeps clarifying that the return
value is the full accumulated set (including pre-existing entries), not
only packages copied by the current call
- document HOST (not HOSTNAME) env var in README standalone section, with
a note explaining the difference from Next.js standalone behaviour
* fix(standalone): address round 10 bonk review comments
- add comment in writeStandaloneServerEntry explaining why the import of
vinext/server/prod-server is intentional as a static import: it is a
documented exports-map entry, always present in the copied dist tree,
and a static import gives a clearer ERR_MODULE_NOT_FOUND at startup
- generalise outDir sub-directory detection in server-externals-manifest:
replace basename === 'ssr' with basename !== 'server' so any future
sub-directory environment (e.g. 'edge') is handled without code changes
- add JSDoc on runtimeDeps clarifying that it returns both dependencies
and optionalDependencies
* fix(next-config): drop PHASE_DEVELOPMENT_SERVER from re-export
No external consumer imports it from next-config.ts — only
PHASE_PRODUCTION_BUILD is used externally (by cli.ts). Removing it
minimises the public surface area of the module.
---------
Co-authored-by: GPU VM <copilot-gpu-vm@example.com>
Co-authored-by: James <james@eli.cx>
* chore(benchmarks): remove Vite 7 runner, update Next.js to 16.2.1
The vinext-rolldown benchmark runner was identical to the vinext runner
(both resolved to the same Vite version via catalog), producing
duplicate results on the dashboard. Remove it entirely and simplify
to a two-runner comparison: Next.js (Turbopack) vs vinext (Vite 8).
Also bump the Next.js benchmark dependency from 16.1.7 to 16.2.1 to
pick up recent performance improvements.
* fix: remove vinext-rolldown from pnpm-workspace.yaml, fix migration comment
* feat: support inline next config
* Apply suggestion from @james-elicx
Co-authored-by: James Anderson <james@eli.cx>
* tweaks
---------
Co-authored-by: James Anderson <james@eli.cx>
* feat: add vinext:optimize-imports plugin for barrel import rewriting
Barrel imports like `import { Slot } from "radix-ui"` cause RSC crashes
because Vite eagerly evaluates all re-exported sub-packages, some of which
call React.createContext() — unavailable in the react-server condition.
This plugin rewrites barrel imports into direct sub-module imports on the
server, matching Next.js's optimizePackageImports behavior:
- Parses barrel entry files to build export maps (namespace, named,
default re-exports, and import-then-export patterns)
- resolveId hook handles pnpm strict hoisting via Vite's own resolver
- Only runs on server environments; client uses Vite's dep optimizer
- Lazy-initialized Set<string> for O(1) package lookups
- Respects experimental.optimizePackageImports from next.config
- Includes Next.js default package list plus radix-ui
Closescloudflare/vinext#100Closescloudflare/vinext#137
* fix: add missing packages to DEFAULT_OPTIMIZE_PACKAGES
Add the full effect ecosystem (17 packages) and all react-icons/*
sub-packages (32 packages) to match Next.js's built-in defaults.
* fix: address bonk review — entryPathCache, buildStart init, Windows path fix, trailing semicolons, fixture-based transform tests
* fix: correct bySource grouping for mixed namespace+named imports, clear barrelCaches on rebuild
- Key bySource map on `${source}::${isNamespace}` instead of just source
so that a namespace re-export and a named re-export from the same sub-module
produce two separate import statements rather than one corrupted one.
Caught by the new 'produces separate statements for namespace and named
imports from the same source' test.
- Clear barrelCaches.exportMapCache and barrelCaches.subpkgOrigin in buildStart
alongside entryPathCache so stale barrel analysis doesn't survive rebuilds.
- Add regression test for the mixed namespace+named grouping bug.
* fix: resolve relative barrel re-export paths to absolute, emit default imports for default re-exports
Barrels like lodash-es and lucide-react use relative re-export paths
(e.g. `export { chunk } from './chunk.js'`). Writing those verbatim caused
Vite to resolve './chunk.js' against the importing user file rather than
the barrel package directory.
Fix: resolve any relative source path against the barrel entry's directory
before emitting the replacement import statement, producing an absolute
filesystem path that Vite resolves correctly regardless of where the
importing file lives.
Also fix: `export { default as X }` re-exports now produce `import X from`
(a default import) instead of `import { default as X }` (invalid syntax
that only works via ASI / bundler leniency).
Tests updated to assert absolute paths and added a dedicated test for
default re-export handling.
* refactor: extract barrel optimization to plugins/optimize-imports.ts, add Windows path fix and react-server TODO
- Move all barrel types, helpers, DEFAULT_OPTIMIZE_PACKAGES, and the
vinext:optimize-imports IIFE from index.ts into a new
packages/vinext/src/plugins/optimize-imports.ts file with a factory
function createOptimizeImportsPlugin(getNextConfig, getRoot)
- Forward-slash normalize resolved absolute paths so emitted import
statements use '/' separators on Windows (path.sep join)
- Add TODO comment in resolveExportsValue noting that the 'react-server'
condition should be added in a future pass
- Add test for ImportDefaultSpecifier: import MyFoo from 'pkg' rewrites
to import MyFoo from '<abs>/sub' (22 tests total, all passing)
* fix(optimize-imports): address sixth-pass review comments
- Fix astName() to throw on unexpected nodes instead of String(value)
- Normalize all resolvePackageEntry return values to forward slashes (Windows fix)
- Fix originalName falsy guard: && → !== undefined
- Pre-build quotedPackages in buildStart to avoid per-file template literal allocations
- Add first-wins comment on subpkgOrigin registration
- Add clarifying comment to lucide-react test fixture
* fix(optimize-imports): remove EnvironmentPluginContext cast, expand subpkgOrigin comment
Vite augments rolldown's MinimalPluginContext with `environment: Environment`
via declaration merging, so both resolveId and transform have `this.environment`
typed without any cast. Drop the EnvironmentPluginContext interface and the
`as unknown as EnvironmentPluginContext` cast in the transform handler, using
direct `this.environment` access consistently with resolveId.
Also expand the subpkgOrigin first-wins comment to make explicit why first-wins
is safe: the sub-package specifier resolves to the same path regardless of which
barrel's importer context is used.
* fix(optimize-imports): address seventh-pass review nits
- Fix test imports: vitest → vite-plus/test, Plugin from vite → vite-plus,
following the convention used by 82 of 85 test files in this repo
- Add TODO comment for extensionless absolute paths produced when barrel
sources lack file extensions (Vite resolves these correctly in practice)
- Expand subpkgOrigin first-wins comment to explicitly note the nested
node_modules edge case and why it is out of scope for the default list
* fix(optimize-imports): address james-elicx review comments
- Support wildcard re-exports (export * from './sub') by recursively
parsing sub-modules and merging their exports into the barrel map
- Add circular-reference guard to prevent infinite loops in recursive
export map building
- Refactor buildBarrelExportMap into buildExportMapFromFile helper
with shared recursive logic
- Use switch statements for spec.type handling instead of if-else chains
- Remove non-null assertions and type assertions; use optional chaining
and typeof guards throughout
- Extract resolvePackageInfo helper to separate concerns in
resolvePackageEntry
- Tests now import directly from optimize-imports.ts instead of via
the full vinext() plugin array
- Remove _buildBarrelExportMap re-export from index.ts (no longer needed
since the plugin has its own file)
- Fix type assertion for optimizePackageImports in next-config.ts using
a proper type-guard filter
- Apply README nit: 'is always optimized' → 'are always optimized'
- Add tests for wildcard export resolution, no-overwrite semantics,
and circular re-export safety
* fix(optimize-imports): address ask-bonk review comments
- Resolve relative sources to absolute paths inside buildExportMapFromFile
so nested wildcard re-exports (e.g. antd-style components/) resolve against
the correct sub-module directory instead of the barrel root
- Add react-server condition to resolveExportsValue (before node/import) for
RSC-compatible entry points in packages like react and react-dom
- Add NOTE comment in resolvePackageEntry documenting that only exports["."]
is checked, not subpath exports
- Eliminate double readFile call in buildBarrelExportMap by passing the
already-read content to buildExportMapFromFile via new initialContent param
- Update subpkgOrigin guard to check for absolute paths (not startsWith ".")
since relative sources are now stored as absolute paths in the export map
- Add test for nested subdirectory wildcard path resolution correctness
- Update existing unit tests to expect absolute paths in export map entries
* fix(optimize-imports): only prefer react-server condition in RSC environment
SSR renders with the full React runtime and must not resolve react-server
export condition entries. Thread preferReactServer (env.name === 'rsc')
through resolveExportsValue and resolvePackageEntry, and key entryPathCache
by environment prefix to keep RSC and SSR barrel entries separate.
* fix(optimize-imports): address review nits — TODO comment, type annotation, test names, wildcard dir test
- Add TODO comment on allResolved bail-out suggesting debug logging
- Remove redundant ': string[]' annotation on optimizePackageImports (inferred)
- Rename namespace re-export test to use generic X/barrel names instead of Slot/lucide-react
- Update syntax-error test to match new empty-map-on-parse-error behavior (not null)
- Add test for directory-style 'export * from "./components"' where components/index.js exists
* fix(optimize-imports): address review nits — wildcard extensions, double-read, readFile hoisting, subpkg registration guard
- Add /index.mjs, /index.tsx, and .tsx to wildcard candidate list for ESM-first and TypeScript-first packages
- Pass candidateContent to buildExportMapFromFile in wildcard loop to avoid reading each sub-module file twice
- Add comment clarifying the no-op resolveEntry callback pattern
- Hoist readFileSafe closure outside the import-declaration for-loop (one allocation per transform, not per barrel import)
- Gate subpkgOrigin registration loop with registeredBarrels Set so 50 files importing the same barrel don't each iterate the full export map
* fix(optimize-imports): remove redundant double-cache, extract PluginCtx type alias
- Remove the redundant exportMapCache.set() in buildBarrelExportMap — buildExportMapFromFile
already stores the result under the same key; the extra set was a no-op but misleading
- Extract PluginCtx type alias for the environment cast so all three hook sites share one
definition and the verbose inline cast doesn't repeat across resolveId and transform
* fix(optimize-imports): hoist readFileSafe to module level, add namespace import test
- Move readFileSafe from handler scope to module level since it captures
nothing from the per-file closure; avoids a per-call function allocation
- Add test verifying import * as Pkg from 'barrel' is left unchanged
(ImportNamespaceSpecifier sets allResolved = false — can't optimize a
full namespace import to a sub-module)
* fix(optimize-imports): make barrel resolution async, add debug logging, update tests
* fix(optimize-imports): address review feedback — extensions, env-keyed subpkgOrigin, debug gate, graceful astName
* fix: scope registeredBarrels key to env to prevent RSC registration blocking SSR
When RSC and SSR resolve the same barrel entry path (common — most packages
have no react-server export condition), the bare barrelEntry key in
registeredBarrels caused RSC's registration to suppress SSR's inner loop
entirely. SSR's subpkgOrigin map was left empty, making resolveId depend
solely on the cross-env fallback — fragile coupling that could silently break.
Change the registeredBarrels key from barrelEntry to `${envKey}:barrelEntry`
so each environment runs the inner loop and populates its own subpkgOrigin
map independently. Matches the existing entryPathCache key pattern.
Add a regression test covering the shared-barrel-entry scenario.
* fix: handle inline export declarations in barrel sub-modules (date-fns)
date-fns barrels use `export * from "./formatDistanceToNow.js"` where the
sub-module directly declares `export function formatDistanceToNow(...) {}`.
buildExportMapFromFile only handled ExportNamedDeclaration with a source
(re-exports) or specifiers (local re-bindings) — it silently skipped inline
declarations (export function/class/const/let/var), leaving the name absent
from the export map and causing the plugin to log "skipping: could not
resolve specifier" and leave the barrel import unchanged.
Handle the declaration branch: FunctionDeclaration and ClassDeclaration
contribute their id.name; VariableDeclaration iterates all declarators.
The source for inline declaration entries is the sub-module file itself.
Also extend AstBodyNode.declaration typing from `unknown` to the minimal
shape covering all three declaration kinds.
Add three unit tests covering export function, export const (multiple
declarators), and export class; all 38 tests pass.
* refactor: remove DEBUG-gated logging from optimize-imports plugin
* refactor: always emit skip log without DEBUG env gating
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: James <james@eli.cx>
* add oxfmt formatter: config, scripts, CI, editor setup, docs
* rebuild lockfile
* fix: add Format to required checks list, remove dead ignore pattern
* run fmt
* add format to agents.md again
* refactor: delete app-dev-server.ts and update all references to entries/ directly
The file was already a pure re-export shim with no logic of its own.
Update every import/dynamic-import site to point straight at the
individual entry modules:
entries/app-rsc-entry.ts ← generateRscEntry, AppRouterConfig
entries/app-ssr-entry.ts ← generateSsrEntry
entries/app-browser-entry.ts ← generateBrowserEntry
Sites updated:
packages/vinext/src/index.ts
tests/entry-templates.test.ts
tests/app-router.test.ts
tests/shims.test.ts
Also update stale comments in:
server/middleware-codegen.ts, server/request-pipeline.ts,
server/instrumentation.ts, shims/metadata.tsx,
tests/rsc-streaming.test.ts, tests/nextjs-compat/rsc-context-lazy-stream.test.ts,
examples/app-router-cloudflare/instrumentation*.ts
* docs: update app-dev-server.ts references in markdown files to entries/
* 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(())
Rework the README deployment section into two clear paths:
- Cloudflare Workers (native) with cloudflare:workers bindings docs
- Other platforms (via Nitro) with collapsible examples for Vercel,
Netlify, AWS, Deno, Node.js
Frame vinext as 'works everywhere' with Cloudflare as the first native
target and more native adapters planned (#80). Update the agent skill
and config references with the same guidance.
* 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.
* Run safe CI for external contributors, add /deploy-preview slash command
Switch ci.yml from pull_request to pull_request_target so lint, typecheck,
vitest, and e2e run automatically for fork PRs without needing approval.
No secrets are used, so this is safe with untrusted code.
Add a /deploy-preview slash command workflow that lets maintainers trigger
deploy previews on fork PRs. Gated by author_association (org members,
collaborators, repo owners only).
Cloudflare employees who push branches to the main repo continue to get
automatic deploy previews via the existing deploy-examples.yml workflow.
* Revert ci.yml to pull_request, skip deploy-examples for fork PRs
Simpler approach: keep ci.yml on pull_request (first-time contributor
approval is fine, avoids cache poisoning concern with pull_request_target).
The actual fix: add a job-level if condition to deploy-examples.yml so the
entire workflow is skipped for fork PRs. This prevents the noisy
failed/skipped deploy checks on external contributor PRs.
Update docs in AGENTS.md and README.md to reflect the approach.
* feat(cli): load .env files with Next.js precedence
* fix(dotenv): address review feedback on .env loading
- Document Vite double-loading interaction in loadDotenv JSDoc
- Fix escaped dollar sign bug ($100 was not unescaped when $ wasn't
followed by a valid variable name)
- Add type cast for parseEnv return value
- Expand test coverage: loadedEnv assertions, production mode
precedence, ${VAR} expansion, escaped dollars, circular refs,
missing files, empty result
- Add getDotenvFiles unit tests for all three modes
- Add 'Prefer Node.js Built-in APIs' guideline to AGENTS.md
Co-authored-by: liuxiaopai-ai <liuxiaopai-ai@users.noreply.github.com>
* test(dotenv): add cross-file expansion and multi-line value tests
Address review suggestions: test that .env can reference vars from
higher-priority .env.development.local, and that parseEnv handles
quoted multi-line values correctly.
---------
Co-authored-by: root <root@localhost.localdomain>
Co-authored-by: liuxiaopai-ai <liuxiaopai-ai@users.noreply.github.com>
* 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>