* fix(shims): align public API with vendored Next types
* fix(shims): preserve revalidation and runtime behavior
* test(shims): align cache revalidation expectations
* fix(ci): package types for deploy suite
* fix(scss): preprocess SCSS CSS-module composes deps via SassAwareFileSystemLoader
postcss-modules' built-in FileSystemLoader reads files referenced by
`composes: className from './other.module.scss'` as raw text — without
Sass preprocessing. For .scss files this leaves SCSS variables and bare
@import directives in the CSS output, causing LightningCSS to crash with
"Invalid empty selector" during production builds.
Fix: introduce SassAwareFileSystemLoader that delegates to Vite's
`preprocessCSS` API for every `composes:` dependency, so Sass compilation
runs before CSS-module scoping. Register it via `css.modules.Loader` in
vinext's Vite config hook and thread the resolved config through via a new
`setSassLoaderResolvedConfig` call in the `configResolved` hook.
Fixes the following deploy-suite tests (both sass and sass-embedded variants):
- test/e2e/app-dir/scss/nm-module/nm-module.test.ts
- test/e2e/app-dir/scss/nm-module-nested/nm-module-nested.test.ts
- test/e2e/app-dir/scss/composes-external/composes-external.test.ts
Part of #1825 (non-tilde CSS-module/composes cases; complements #1881)
* fix(scss): address review feedback on SassAwareFileSystemLoader
- Guard the css.modules.Loader injection: respect a user's explicit
css.modules: false (mergeConfigRecursively would otherwise re-enable
CSS Modules with an object override) and never clobber a
user-provided custom Loader. The override is precomputed as a typed
const so the conditional spread doesn't disturb the config literal's
contextual typing.
- Surface composes preprocessing failures via config.logger.warn
instead of a bare catch {}, so missing composed classes are
observable; a missing/unreadable dependency file now rejects like
postcss-modules' FileSystemLoader did.
- Use Vite's exported preprocessCSS + PreprocessCSSResult type via a
top-level import instead of the lazy dynamic import and stringly
typed casts.
- Preserve parity for composes from non-*.module.* files: hand
preprocessCSS a virtual *.module.* filename so plain '.css' deps are
still scoped and export tokens extracted, matching the built-in
FileSystemLoader.
- Port the three failing Next.js fixtures (composes-external,
nm-module, nm-module-nested) as production-build regression tests in
tests/scss-composes.test.ts, plus a plain .module.css / non-module
.css parity test. All three SCSS tests reproduce the original
"[lightningcss minify] Invalid empty selector" crash when the
Loader injection is disabled.
- Add sass as a root devDependency so the SCSS tests (including the
previously always-skipped tests/scss.test.ts) actually run in CI;
drop the now-redundant knip ignore entry. Installing sass makes
Vite's SassPreprocessorOptions resolve to the concrete sass types
(previously any), so the tilde-importer's importers array now
carries an explicit SassPreprocessorOptions["importers"] cast.
* fix(scss): bind composes-loader config per plugin instance; scope extensionless deps
- Replace the module-level resolved-config singleton with a per-vinext-
instance binding (createSassAwareFileSystemLoader), so multiple builds in
one process never preprocess composes deps with another build's config.
- Cover extensionless composes deps in the virtual *.module.* rename
(append .module.css) so their tokens are scoped instead of silently
dropped; add a parity test case.
- Pin url() pass-through behaviour for composed deps in the parity test
(verbatim, never mangled into placeholders — matches the built-in
FileSystemLoader, verified against a vanilla Vite build).
* docs(scss): document the per-subtree recursion boundary of the composes loader
Notes the two structural consequences of delegating nested composes
recursion to preprocessCSS (per-subtree duplicate inlining collapsed by
LightningCSS, and no cross-boundary cycle short-circuit for invalid
circular composes), per review.
* fix(scss): fail the build on composed-dep preprocessing errors
Only a missing Sass implementation is downgraded to a logged warning;
any other preprocessCSS error (e.g. a Sass syntax error in the composed
dependency) now propagates and fails the build, matching postcss-modules'
built-in FileSystemLoader instead of shipping a green build with
silently-missing classes. Docstrings updated to accurately describe the
silent empty-token path when no resolved config is bound. Adds a
regression test for the fail-loudly path.
* chore(deps): move sass to the workspace catalog; drop the now-dead dynamic skip in scss.test.ts
Review feedback on #1882: sass belongs in the pnpm workspace catalog
rather than pinned inline in the root package.json. With sass now a
guaranteed root devDependency, the runtime describeIfSass skip in
tests/scss.test.ts is dead code — the suite always runs — so make the
unskip explicit by removing the conditional machinery.
* rebuild lockfile
* 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(release): commit-driven changesets with auto-generated changesets
Adopt stock Changesets for versioning/changelog/publishing, with the only
bespoke surface being a script that creates changesets automatically from
Conventional Commits. Auto-generated changeset files never live on `main` —
they are written to the CI working tree, consumed by changesets/action into a
rolling Version PR, and discarded. Manually authored changesets still work.
- scripts/create-changeset.mjs: derive per-package bumps from Conventional
Commits (paths -> package, type -> bump). Includes the version-vs-tag guard
so a merged Version PR publishes instead of re-opening a PR.
- scripts/version.mjs: `changeset version` + append a `## Contributors` list to
each bumped package's CHANGELOG.md (idempotent, pure rewrite unit-tested).
- .github/workflows/release-pr.yml: version-only changesets/action (no publish).
- .github/workflows/publish.yml: guarded OIDC publish (preserves
`vp pm publish --provenance`); version now comes from `changeset version`.
- Reconcile packages/vinext version (0.0.5 -> 0.0.55) so the guard is coherent.
Unit tests for both scripts pass (35). The release orchestration (Version PR
creation, OIDC publish, gh contributor resolution) can only be validated in a
live CI run.
* refactor(release): collapse to a single changesets workflow
Let changesets/action own as much as possible. Delete the separate publish.yml
(guard job, manual bump, manual OIDC/tag/release/notify steps) and the
release-pr.yml split. One workflow now:
- create-changeset.mjs writes auto changesets to the working tree (its
version-vs-tag guard yields nothing right after a Version PR merges).
- changesets/action maintains the Version PR and, when no changesets remain,
publishes via `changeset publish` with OIDC trusted publishing + provenance,
and creates the git tag + GitHub Release.
Removes the unused release:version script. Contributors list still handled by
scripts/version.mjs as the action's version command.
* refactor(release): convert release scripts to .mts, drop .d.mts files
Replace the .mjs + hand-written .d.mts declaration pairs with real TypeScript
(.mts) source. Node >=24 (the setup default) runs .mts directly via native type
stripping; .mts is unambiguously ESM so it needs no "type": "module" and emits
no MODULE_TYPELESS warning. The cross-import uses an explicit .mts specifier
(Node requires it), permitted in tsc via allowImportingTsExtensions (safe: the
project is noEmit).
- scripts/create-changeset.{mjs,d.mts} -> scripts/create-changeset.mts
- scripts/version.{mjs,d.mts} -> scripts/version.mts
- tsconfig: allowImportingTsExtensions
- release.yml: run node scripts/*.mts
vp check (format + lint + types) clean; 35/35 unit tests pass.
* refactor(release): trim release scripts (~690→~470 source lines)
Cut comment bloat, remove dead code, tighten without dropping behavior:
- Condense verbose JSDoc/@param blocks to one-line purpose comments; keep the
load-bearing "why" (correctness-rule header, insertContributors idempotency).
- Delete unused `newestChangelogVersion` (+ its tests) — exported/tested but
never called.
- Tighten run()/insertContributors/latestTagVersion without behavior change.
Multi-package machinery and the bottom `## Contributors` list are retained per
requirements. vp check clean; 33 unit tests pass.
* feat(release): grouped conventional changelog + filter bot contributors
The default changesets changelog groups by bump level (### Minor Changes) and
renders our changeset summary as a nested bullet dump. Replace it with a real
conventional-commits changelog: version.mts now regroups each release's commits
into ### Features / ### Bug Fixes / ### Performance sections (scope bolded, type
prefix dropped) and rewrites the newest CHANGELOG section, then appends the
## Contributors list with [bot] accounts filtered out.
- create-changeset.mts: extract conventionalParts + collectReleaseCommits +
releaseRangeStart and export the commit-walk so version.mts reuses it (no
duplicate git logic).
- version.mts: groupedChangelogBody + rewriteReleaseSection (idempotent: only
`## <digit>` is a section boundary); dedupeSortLogins drops `[bot]` logins.
- Tests updated for the new pure builders.
vp check clean; 33 unit tests pass. Verified end-to-end with a local dry run.
* refactor(release): simplify per PR review
- discoverPublishablePackages: scan only packages/* (publishable packages don't
live elsewhere), drop the apps/examples/benchmarks scan.
- Delete isReleaseCommit: the "chore: version packages" release commit is a
`chore`, already excluded by parseBumpFromSubject returning null. No need to
hardcode release-message formats.
vp check clean; 31 unit tests pass.
* docs(release): clarify why the version-vs-tag guard exists
Per PR review: the guard was confusing. Document inline that it only suppresses
the post-merge/pre-publish window (version bumped, tag not yet created), which
would otherwise re-open a Version PR instead of publishing. Keeping the
never-on-main design per review decision.
* feat(release): sub-group changelog areas and humanize area names
Within each type section (### Features / ### Bug Fixes / …), areas (commit
scopes) with more than 3 items now get their own `#### <Area>` sub-group with
the scope prefix dropped; smaller areas and scopeless commits fall under
`#### Other`. When no area qualifies the list stays flat. Area names are
humanized (app-router → "App Router", css → "CSS", ppr → "PPR", i18n → "i18n")
in headings and in the bold prefixes.
Also: groupedChangelogBody now renders only known release types (GROUPS) — it
no longer leaks non-release types into an "Other Changes" bucket.
vp check clean; 32 unit tests pass. Verified with a local dry run.
* feat(release): rename changelog Other sub-group to Misc
* feat(release): sub-group areas with 3+ items (was >3)
* fix(release): address PR review (stale refs, contributor API, doc)
- Fix stale references to deleted files: .mjs → .mts and release-pr.yml →
release.yml in .changeset/README.md and the release.yml header comment.
- resolveContributors: one paginated `gh api compare` call instead of one
`gh api commits/<sha>` per commit (N round-trips → 1). This also removes
version.mts's local git() helper, so no more `fatal:` probe noise from it.
No behavior change to the changelog/contributor output (verified via dry run:
same 4 contributors resolved). vp check clean; 32 unit tests pass.
The bot's "no git tags" bootstrap concern does not apply: tags v0.0.10..v0.0.55
exist and CI checks out with fetch-depth: 0, so the guard resolves 0.0.55.
* fix(release): run pinned changeset CLI; drop dead affectedPackages branch
Per second bonk review:
- Invoke the pinned, installed @changesets/cli via `vp exec changeset` instead
of `vp dlx @changesets/cli` (which fetches a floating latest at run time) — in
release.yml publish and version.mts. Removes the reproducibility gap vs the
lockfile-pinned 2.31.0.
- affectedPackages: remove the unreachable `dir === "."` / empty-prefix branch
now that discovery only returns `packages/*` keys.
vp check clean; 32 unit tests pass.
* fix(release): only emit valid GitHub logins in Contributors; doc fix
Per third bonk review:
- resolveContributors: `.author.login // empty` (was `// .commit.author.name`),
so commits with an unlinked email contribute no entry instead of a raw git
display name. dedupeSortLogins now keeps only `[a-zA-Z0-9-]+` shapes, which
also subsumes the previous `[bot]` filter — no more broken `- @Full Name`
mentions.
- .changeset/README.md: suggest `vp exec changeset` instead of the floating
`vp dlx @changesets/cli` for manual changeset authoring.
vp check clean; 32 unit tests pass; dry run resolves the same valid handles.
* test(msw): introduce MSW infrastructure and migrate font-google-build
Replaces ad-hoc `globalThis.fetch` hijacking in tests with Mock Service
Worker. MSW intercepts both `globalThis.fetch` and `node:http`/`https`,
so it covers fetches issued by in-process Vite servers and fixture pages
without each test having to manage its own stub.
The setup file enables `onUnhandledRequest: 'error'` so any future test
that issues an unmocked external request fails loudly instead of
silently hitting the network. Loopback hostnames (localhost, 127.0.0.1,
::1) are bypassed because the integration project deliberately hits
in-process test servers via fetch.
What this PR contains:
- `tests/_msw/{handlers,server,setup}.ts` — shared infra
- `setupFiles` wired into both vitest projects (unit + integration)
- `msw` + `@mswjs/interceptors` added to the workspace catalog
- `msw` postinstall skipped via `allowBuilds` (it only copies a browser
service worker; we exclusively use `msw/node`)
- `tests/font-google-build.test.ts` migrated as the proof of pattern
Temporary handlers for `httpbin.org/uuid` and
`example.com/not-cacheable` keep the existing fixture pages
(`revalidate-tag-test`, `dynamic-error-fetch`) working under the new
guard. These are placeholders flagged with `TODO(msw-migration)` — a
follow-up will replace those live URLs with test-local endpoints.
This is PR 1 of 3:
- PR 2 will migrate `tests/fetch-cache.test.ts`
- PR 3 will migrate `tests/font-google.test.ts` and the font block in
`tests/app-router.test.ts`
* test(msw): restrict MSW to the unit project + use passthrough for loopback
Two fixes to the initial MSW wiring that surfaced when CI ran the full
suite:
1. The integration project is now excluded from MSW. Integration tests
spin up in-process HTTP servers and fixture dev servers and exercise
them via `fetch("http://127.0.0.1:<port>/...")`. Even with a loopback
`passthrough()` handler installed, the @mswjs/interceptors layer
interferes with that traffic in subtle ways:
- `tests/prerender.test.ts > errors without writing .rsc when the
middleware short-circuit fallback RSC request fails` timed out at
30s — the 5xx response body never reached the caller.
- `tests/ecosystem.test.ts > nuqs` hung in `beforeAll` waiting for
its fixture process to come up, because the readiness fetch
stalled inside the interceptor.
Integration tests already talk to real local servers, not the
internet, so the unhandled-request guard buys little there. The
value of MSW for this repo is mocking external HTTP for unit tests
of fetch wrappers (font-google, font-google-build, fetch-cache),
which is preserved.
2. The `onUnhandledRequest` callback in `setup.ts` that tried to
silently let loopback through wasn't sufficient — MSW's interceptor
still ran for those requests and could stall them. Replaced with a
default `http.all(LOOPBACK_URL_PATTERN, () => passthrough())`
handler in `handlers.ts` and a plain `onUnhandledRequest: "error"`
in `setup.ts`. Unit tests like the middleware-rewrite proxy in
`tests/shims.test.ts` and the JSX-in-JS suite need this to keep
their in-process HTTP servers reachable.
* test(msw): address BigBonk review notes
- Update PR description in the commit body to drop the stale mention of
TODO(msw-migration) handlers — they were removed when the integration
project was excluded from MSW; the fixture pages run there.
- Tighten the loopback IPv4 octet regex from `\d+` to `\d{1,3}` so it
doesn't match absurdly long octet strings. `URL.hostname` normalises
real addresses, so this is purely a readability fix.
* test(msw): address remaining BigBonk notes from the initial review
- Add `0.0.0.0` to the loopback passthrough pattern so test servers that
bind to the unspecified address keep working without per-test mocks.
- Add a `TODO(msw-3)` comment in `server.ts` flagging the
`SetupServerApi` deprecation in MSW 2.14.6 — the `FetchInterceptor`-only
construction will need to migrate to `defineNetwork`'s interceptor
config when we bump to MSW 3.
Both raised by BigBonk on the first review of this PR (the second review
focused on the stale description and the loose `\d+` octet bound, which
were addressed in the previous commit).
* test(msw): scope server.use() inside the try block in font-google-build
Minor structural fix from BigBonk's review: moves the `server.use()`
handler registration inside the `try` block so all setup and the
matching `fs.unlink` cleanup share the same try/finally scope.
No behaviour change — `server.use()` is synchronous and infallible, so
this is purely a structural tidy-up.
* test(msw): correct comment in font-google-build to match interceptor config
BigBonk caught that the comment described the default `setupServer()`
behaviour (which uses both `FetchInterceptor` and `ClientRequestInterceptor`),
but this project's `server.ts` deliberately constructs `SetupServerApi`
with only `FetchInterceptor`. Update the comment to reflect that — it
intercepts `globalThis.fetch`, not `node:http`/`node:https`.
No code change, comment only.
* chore: bump pnpm to 10.32.1 and enable vitest agent reporter
* chore: upgrade GitHub Actions to Node 24 compatible versions
* perf: re-land unit/integration split and add CI sharding
- Restore unit/integration vitest project split (lost in vp migration)
- vitest 4 supports fileParallelism per-project (was global-only in v3),
so unit tests now run in parallel (~11s) while integration stays serial
- Add 3-shard matrix for integration tests in CI, cutting wall time from
~4.5min to ~1.5min
- Move kv-cache-handler to integration to avoid flakiness under parallelism
* chore: remove unused deploy-preview slash command workflow
* ci: add sentinel job for stable branch protection rule
* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit 009da10473.
* Update
* Update
* Try revert ci changes
* revert
* Run vp migrate
* Disable typeAware and typeCheck for now
* Better resolve for test
* Use vp dev instead of vite
* Update expect
* Fix NormalizeManifestModuleId
* Try increase timeout
* Update to use vp
* Try new check
* Bring back npx vp
* Migrate CI
* Make next-intl resolvable
* Update
* Update
* Update
* chore: upgrade vitest to 4.1
* fix: clear console.warn spy state before asserting call counts
Vitest 4 changed vi.spyOn to return the same mock when called on an
already-intercepted function. This means console.warn call records from
earlier tests accumulate in the spy, causing toHaveBeenCalledTimes(2) to
see 4 calls instead of 2.
Adding mockClear() after each spy creation resets the call count so the
assertions only reflect calls made within the test itself.
* 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
* 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