Commit Graph

45 Commits

Author SHA1 Message Date
James Anderson ce29f9c6aa fix(scss): preprocess SCSS CSS-module composes deps via SassAwareFileSystemLoader (#1882)
* 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
2026-06-11 00:29:51 +00:00
Nathan Nguyen 9a721ca287 chore(deps): bump knip and node types (#1806)
* chore(deps): bump knip and node types

Bump Knip to 6.16.0 and @types/node to 25.9.2.

Knip 6.16 reports previously unreported unused exported types, stale ignore entries, and external CLI binaries. Remove unnecessary type exports, refine the script entry coverage, and keep only narrow binary ignores for system or user-project commands.

Validation: vp run knip; vp run check.

* chore: retrigger ci
2026-06-07 21:45:14 +01:00
Jerry Zhao c8975a48cf chore(deps): bump playwright to 1.60.0 (#1796)
* chore(deps): bump playwright to 1.60.0

* u
2026-06-06 22:49:23 +01:00
Jerry Zhao fb9151ed9d chore(deps): bump vite-plus toolchain to 0.1.24 (#1767)
* chore(deps): bump vite-plus toolchain to 0.1.24

* bump setup-vp

* .

---------

Co-authored-by: James <james@eli.cx>
2026-06-05 15:36:59 +00:00
Nathan Nguyen dec0ee5369 chore: bump Next.js and React patch versions (#1721)
* chore: bump next and react patches

* update other areas and bump down peer dep

* .

---------

Co-authored-by: James <james@eli.cx>
2026-06-03 09:38:21 +00:00
James Anderson 60dfb11c62 chore(deps): bump @vitejs/plugin-rsc to 0.5.27 (#1709)
* chore(deps): bump `@vitejs/plugin-rsc` to 0.5.27

* test(rsc): cover `export * from` in 'use client' modules (#1352)

Ports the build fixture from #1373 to verify the `@vitejs/plugin-rsc`
0.5.27 bump resolves the upstream "unsupported ExportAllDeclaration"
error. Covers bare `export *`, nested/renamed re-exports,
`export * as Ns from`, and destructured re-exported bindings.

The version bump fixes this natively, so the vinext-side workaround
plugin from #1373 is no longer needed.

Refs #1352
2026-06-01 11:01:28 +01:00
James Anderson d03c5aa045 test(msw): introduce MSW infrastructure and migrate font-google-build (#1444)
* 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.
2026-05-22 13:03:08 +00:00
yyh 371817a07b chore: upgrade vite plus (#1410) 2026-05-21 14:37:09 +01:00
Ghxst 85f2c10fab chore: update @vitejs/plugin-rsc to 0.5.26 (#1293) 2026-05-18 08:28:33 +00:00
James Anderson 1c0bdacda6 feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest (#1239)
* feat(apps/web): /compatibility page backed by D1 + deploy-suite ingest

Adds a /compatibility page to apps/web that visualises Next.js
compatibility over time, populated by the nightly deploy-suite workflow.

  - D1 binding (`vinext-metrics`) with Drizzle schema and migrations.
    Tables are namespaced (`compat_runs`, `compat_file_results`) so
    future metric kinds can live alongside.
  - POST /api/compatibility ingest endpoint. Authenticates with a
    `COMPAT_INGEST_SECRET` worker secret and upserts by
    (kind, runKey).
  - GitHub-style contribution grid (one dot per test file) and a
    pass-rate-over-time line chart. Both are SVG, no chart libs.
    Pass rate excludes Next.js-skipped tests from the denominator.
  - Shared header/footer extracted into app/_components and rendered
    by the root layout. Nav uses next/link for soft client nav.
  - Workflow change: on top of the existing per-test pass/fail/skip
    aggregation, the report job now POSTs results to the ingest
    endpoint. Guarded to only run when (a) filter=all and
    (b) targeting main, so partial runs and branch spot-checks don't
    pollute the historical record.

The first nightly run after merge will seed the production D1.

* address review + add ISR with KV cache

Review fixes (apps/web/app/api/compatibility/route.ts):
  - constant-time secret comparison via SHA-256 + bytewise XOR
  - single-statement atomic upsert with onConflictDoUpdate (no
    SELECT->INSERT race for concurrent retried runs)
  - top-level try/catch with structured error body so workflow logs
    show why ingest failed instead of a generic 500
  - MAX_FILES=2000 cap in validation as defensive bound

Page perf (apps/web/app/compatibility/page.tsx):
  - latest-run and trend D1 queries run in parallel via Promise.all

ISR + KV:
  - new vinext-web-cache KV namespace bound as VINEXT_CACHE
  - worker installs KVCacheHandler once per isolate
  - both pages opt into ISR with revalidate = 300; compat page drops
    force-dynamic

* address bonk re-review

  - Drop the ?kind= query param. Only one kind exists today; gating it
    behind an unused param invited ISR cache pollution. Hardcoded to
    "deploy" with a comment pointing future kinds at a dedicated route.

  - Wrap the DELETE + chunked INSERTs in a single `db.batch()` so a
    crash mid-write can no longer leave a run with zero file results.
    D1 executes batch statements inside one transaction.

  - Resolve workflow expressions once via step-level `env:` and read
    them as `process.env.*` from the github-script body. Removes the
    fragile splicing of `${{ inputs.next-ref || 'v16.2.6' }}` into JS
    string literals (the nested single quotes were the worst offender).
2026-05-15 15:10:36 +00:00
James Anderson d9906b94d6 chore: add basic vinext site (#1236)
* chore: scaffold new next app with vinext

* basic app

* add to deploy workflow

* fmt
2026-05-15 13:41:10 +00:00
James Anderson 1ac7b6514b fix(examples): next build failure for app-router-playground (#1234) 2026-05-15 13:40:59 +01:00
MK (fengmk2) 9d5a2e9a77 chore(deps): bump vite-plus to 0.1.21 (#1190)
- Bump vite, vite-plus, vitest catalog entries to 0.1.21
- Refresh pnpm-lock.yaml
- Regenerate AGENTS.md via vp config postinstall
- Drop unused imports (PHASE_PRODUCTION_BUILD, patternToNextFormat) flagged by upgraded lint rules
- Move react-hooks/rules-of-hooks suppression in tests/e2e/fixtures.ts to the use(errors) call site
- Add react/no-children-prop and react/no-danger-with-children suppressions on the intentional precedence test in tests/head.test.ts
2026-05-14 09:14:31 +01:00
James Anderson a27193fcaf chore: upgrade pnpm 10 to 11 and enable frozenLockfile (#1185)
* chore: upgrade pnpm 10 to 11 and enable frozenLockfile

- Bump packageManager from pnpm@10.32.1 to pnpm@11.1.1
- Migrate .npmrc settings to pnpm-workspace.yaml (pnpm 11 only reads auth/registry from .npmrc)
  - auto-install-peers=false -> autoInstallPeers: false
  - Remove playground save-exact=true (minor pnpm add preference, not needed for builds)
- Convert onlyBuiltDependencies list to allowBuilds map (pnpm 11 format)
- Add frozenLockfile: true and preferFrozenLockfile: true
- Delete .npmrc files (contained no auth settings)

* fix: handle pnpm 11 strictDepBuilds, preserve autoInstallPeers, remove minimumReleaseAgeExclude

- Add unrs-resolver: false to allowBuilds (reviewed, deny build scripts)
- Keep autoInstallPeers: false to match existing lockfile
- Remove minimumReleaseAgeExclude entirely
- Append allowBuilds to scaffolded project's pnpm-workspace.yaml after
  create-next-app so subsequent pnpm steps don't abort
2026-05-13 09:15:02 +00:00
dependabot[bot] e9d1749b95 chore(deps): bump next from 16.2.5 to 16.2.6 (#1183)
* bump nextjs

* .

---------

Co-authored-by: James <james@eli.cx>
2026-05-12 14:18:00 +00:00
dependabot[bot] 477411ae1b chore(deps): bump next-intl from 4.9.1 to 4.9.2 (#1160)
Bumps [next-intl](https://github.com/amannn/next-intl) from 4.9.1 to 4.9.2.
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.9.1...v4.9.2)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-version: 4.9.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 13:24:21 +00:00
Nathan Nguyen 826bab4530 feat: implement next web vitals hook (#1161) 2026-05-11 17:15:12 +01:00
Divanshu Chauhan (divkix) 3a9046dce2 fix(image): support images.dangerouslyAllowLocalIP for private-IP rejections (#1153)
* fix(image): support images.dangerouslyAllowLocalIP for private-IP rejections

Ports Next.js commit 5452439f3db2a78967178ca4180b27fb48393a19 (PR #91686)
to vinext's next/image shim and server-side image config.

Changes:
- Add isPrivateIp() helper in shims/image-config.ts (IPv4 + IPv6, no deps)
- Update validateRemoteUrl in shims/image.tsx to reject literal private-IP
  hostnames unless dangerouslyAllowLocalIP is true
- Wire dangerouslyAllowLocalIP through next-config type, Vite define,
  global.d.ts, and server-side ImageConfig
- Add unit tests for isPrivateIp and component-level private-IP guard

Closes #1065

* fix(image): use ipaddr.js for correct private-IP classification

Addresses review feedback on the SSRF guard:
- Replaces the hand-rolled isPrivateIp with ipaddr.js (same library Next.js
  uses), eliminating bugs in IPv4/IPv6 range checks
- Fixes incorrect 2001:2f::/32 check (not a real range) and adds all missing
  non-unicast ranges: CGNAT, multicast, reserved, benchmarking, teredo,
  documentation, discard, NAT64
- Correctly handles ::ffff: IPv4-mapped addresses via isIPv4MappedAddress()
- Adds explanatory comments for best-effort client-side guard and
  dangerouslyAllowLocalIP server-side config
- Adds comments in tests explaining the vi.resetModules() pattern

* fix(image): externalize ipaddr.js to prevent SSR crash

ipaddr.js is a CJS UMD module that assigns to `this.ipaddr` at the
bottom of its wrapper. When Vite's SSR module runner loads it with
`noExternal: true`, the UMD wrapper executes with `this = undefined`
(strict mode in ESM context), producing:

  TypeError: Cannot set properties of undefined (setting 'ipaddr')

This crashed every page render that imports `next/image` (via the
image-config.ts shim). The image-optimization-parity tests failed
with 500 errors.

Fix: add `ipaddr.js` to `ssr.external` in all 3 Vite environment
configs (top-level, RSC, SSR) so Node's native loader handles the
CJS module correctly.
2026-05-11 13:21:02 +01:00
James Anderson b631b5da4a chore(deps): bump next.js version (#1112)
* chore(deps): bump next.js version

* fix tests
2026-05-06 17:32:01 +00:00
James Anderson c74faed38c chore(deps): bump react version (#1110) 2026-05-06 16:46:47 +00:00
James Anderson c1c81886d2 test: add coverage summaries for integration tests in ci (#1024) 2026-05-02 21:17:27 +00:00
dependabot[bot] 89a351b44b chore(deps): bump postcss from 8.5.3 to 8.5.10 (#889)
Co-authored-by: James <james@eli.cx>
2026-04-24 20:44:46 +00:00
James Anderson ff5e14c41d setup knip (#886)
* setup knip

* nits
2026-04-24 21:30:57 +01:00
dependabot[bot] 41d363e8b2 chore(deps): bump vite-plus from 0.1.12 to 0.1.17 (#858)
* chore(deps): bump vite-plus from 0.1.12 to 0.1.17

Bumps [vite-plus](https://github.com/voidzero-dev/vite-plus/tree/HEAD/packages/cli) from 0.1.12 to 0.1.17.
- [Release notes](https://github.com/voidzero-dev/vite-plus/releases)
- [Commits](https://github.com/voidzero-dev/vite-plus/commits/v0.1.17/packages/cli)

---
updated-dependencies:
- dependency-name: vite-plus
  dependency-version: 0.1.17
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* bump setup-vp

* update agents

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James <james@eli.cx>
2026-04-17 07:43:02 +00:00
dependabot[bot] cff49ecb2e chore(deps): bump sanitize-html from 2.17.2 to 2.17.3 (#856)
Bumps [sanitize-html](https://github.com/apostrophecms/apostrophe/tree/HEAD/packages/sanitize-html) from 2.17.2 to 2.17.3.
- [Changelog](https://github.com/apostrophecms/apostrophe/blob/main/packages/sanitize-html/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/apostrophe/commits/sanitize-html@2.17.3/packages/sanitize-html)

---
updated-dependencies:
- dependency-name: sanitize-html
  dependency-version: 2.17.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 08:21:27 +01:00
dependabot[bot] 84a5519628 chore(deps): bump next-intl from 4.8.3 to 4.9.1 (#829)
Bumps [next-intl](https://github.com/amannn/next-intl) from 4.8.3 to 4.9.1.
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.8.3...v4.9.1)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-version: 4.9.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 09:28:51 +01:00
James Anderson 8be547c6bb chore(deps): bump next (#824)
* bump next

* update constants
2026-04-11 11:40:30 +01:00
James Anderson 1e6b7e681d rebuild lockfile (#804) 2026-04-09 06:40:11 +00:00
Steve Faulkner f910e74045 fix(app-router): reject cyclic Flight payloads in server actions (#798)
* fix(app-router): reject cyclic Flight payloads in server actions

* test(app-router): update generated entry snapshots
2026-04-08 22:11:15 -05:00
James Anderson 1a339c7ca4 chore: bump vulnerable deps (#773) 2026-04-03 23:41:47 +01:00
James Anderson 7625d4a215 remove unused rsc-html-stream dep (#771) 2026-04-03 22:21:25 +01:00
James Anderson 49656ed5cf fix: security audit findings from issue #741 (#757) (#769)
* fix: security audit findings from issue #741

CI shell injection (high):
- publish.yml: use env var for inputs.bump in case statement
- nextjs-tracker.yml: use env vars for since_hours and dry_run inputs
- nextjs-tracker.yml: use env var in Skip step echo

Test file false positive (critical flag):
- tests/vite-hmr-websocket.test.ts: add gitleaks:allow for RFC 6455
  example WebSocket nonce (dGhlIHNhbXBsZSBub25jZQ==)

innerHTML / XSS (high):
- packages/vinext/src/shims/script.tsx: add security comment documenting
  that dangerouslySetInnerHTML is developer-supplied inline script only
- packages/vinext/src/shims/head.ts: document tag is bounded to
  RAW_CONTENT_TAGS (script/style), not user input

dangerouslySetInnerHTML in example (medium):
- examples/hackernews/components/comment.jsx: sanitize HN API HTML
  with DOMPurify before rendering
- examples/hackernews/package.json: add dompurify dependency
- pnpm-workspace.yaml: add dompurify and @types/dompurify to catalog

Non-literal RegExp (medium): documented as internal config values only,
no user input reaches any of the 6 flagged patterns. See safeRegExp()
in config-matchers.ts which already enforces ReDoS protection.

Deprecation cleanup:
- examples/hackernews/tsconfig.json: remove deprecated baseUrl option
  (moduleResolution: bundler handles resolution; all imports are relative)
- examples/hackernews/tsconfig.json: add noEmit: true to prevent TypeScript
  from attempting to write over existing .js files in lib/
- examples/hackernews/package.json: add missing @cloudflare/workers-types
  devDependency (was referenced in tsconfig types but not installed)

* fix: address bonk review comments

- Switch dompurify → isomorphic-dompurify so sanitize() works during
  SSR (DOMPurify is a no-op without window/document; isomorphic-dompurify
  bundles jsdom for server-side use)
- Revert package.json whitespace to 2-space indentation (was inadvertently
  changed to 4-space, creating noise in the diff)
- Replace dompurify + @types/dompurify catalog entries with
  isomorphic-dompurify (ships its own types, no separate @types needed)
- Add missing trailing newline to tsconfig.json

* fix: address bonk review comments on PR #769

- Switch from isomorphic-dompurify to sanitize-html, and move
  sanitization to the server-side data boundary (lib/get-comments.js).
  isomorphic-dompurify depends on jsdom which is Node.js-specific and
  does not work in the Cloudflare Workers runtime. sanitize-html is
  pure-JS (htmlparser2-based) with no DOM dependency and works in both
  Workers and Node.js. Sanitizing once at the data boundary means both
  the SSR'd initial HTML response and client re-renders receive safe text.
- Remove the DOMPurify import from comment.jsx; the client component no
  longer needs to sanitize since text is already clean when passed as a prop.
- Replace isomorphic-dompurify with sanitize-html in the workspace catalog.
- Remove inert eslint-disable-next-line comment from script.tsx; the project
  uses oxlint which ignores ESLint-specific inline suppression directives.

---------

Co-authored-by: Luke Percy <lpercyagile@gmail.com>
2026-04-03 12:47:24 +01:00
James Anderson bd2e41f848 chore: turn on pnpm supply chain rules (#729) 2026-03-31 14:15:31 +01:00
Steve Faulkner 05cac9d745 build: align Vite 8 bundler config (#599)
* build: align Vite 8 bundler config

* fmt

---------

Co-authored-by: James <james@eli.cx>
2026-03-28 15:16:03 +00:00
Steve Faulkner a62e5c4901 chore(benchmarks): remove Vite 7 runner, update Next.js to 16.2.1 (#699)
* 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
2026-03-27 19:23:50 -05:00
Stephen Zhou fd7618c94a chore: move pnpm settings to pnpm-workspace.yaml (#671) 2026-03-23 09:47:39 +00:00
Stephen Zhou 746dc5b522 feat!: fumadocs example, mark clientReferenceDedup experimental (#577)
* chore: init fumadocs

* vinext init

* fix dev

* tweaks

* tweaks

* tweaks

* prefer exports

* tweaks

* try new approach

* update

* tweaks

* Update

* exclude deps

* update

* revert

* tweaks

* tweaks

* tweaks

* tweaks

* pre include @unpic/react

* clientReferenceDedup

* tweaks
2026-03-19 07:17:10 +00:00
dependabot[bot] 0b43cad172 chore(deps): bump next from 16.1.6 to 16.1.7 (#579)
* chore(deps): bump next from 16.1.6 to 16.1.7

Bumps [next](https://github.com/vercel/next.js) from 16.1.6 to 16.1.7.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.1.6...v16.1.7)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.1.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* add benchmarks

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: James <james@eli.cx>
2026-03-18 09:01:38 +00:00
James Anderson 75eb3c1e8d feat: production prerender pipeline (#553)
* feat: production prerender pipeline (no dev server)

Add prerenderPages() and prerenderApp() that load from production bundles
exclusively — no ViteDevServer dependency. runPrerender() replaces
runPrerenderWithDevServer() (kept as a shim for cli.ts/deploy.ts callers).

Key changes:
- packages/vinext/src/build/prerender.ts — new: prerenderPages/prerenderApp
- packages/vinext/src/build/run-prerender.ts — new: runPrerender + shim
- packages/vinext/src/build/static-export.ts — delegates to prerender layer
- packages/vinext/src/index.ts — adds rscOutDir/ssrOutDir/clientOutDir/disableAppRouter options
- packages/vinext/src/entries/app-rsc-entry.ts — exports generateStaticParamsMap
- packages/vinext/src/entries/pages-server-entry.ts — exports pageRoutes
- packages/vinext/src/shims/cache.ts — adds NoOpCacheHandler
- packages/vinext/src/server/prod-server.ts — serves prerendered .html files
- packages/vinext/src/build/report.ts — integrates PrerenderResult for build report
- tests/helpers.ts — buildPagesFixture/buildAppFixture helpers (isolated outDirs)
- tests/prerender.test.ts — 36 new tests
- tests/static-export.test.ts/app-router.test.ts/pages-router.test.ts — updated

All 487 tests pass. Typecheck, lint, fmt clean.

* fix: build Pages Router SSR bundle for hybrid app+pages projects

For hybrid projects that have both app/ and pages/ directories, the
App Router multi-env build (createBuilder/buildApp) does not produce
a Pages Router SSR bundle. The prerender phase then fails with 'bundle
not found at dist/server/entry.js'.

Fix: after buildApp() completes, detect pages/ presence and run a
separate standalone Vite SSR build with vinext({ disableAppRouter: true })
so the plugin's multi-env environments config does not override the SSR
input/entryFileNames. The build is run with emptyOutDir: false to preserve
RSC artefacts from the App Router build.

* Clean up run-prerender: remove deprecated shim, fix progress arithmetic and typos

* regen snaps

* refactor: clean up prerender pipeline, cache shim, and report utilities

- Extract loadBundle() helper used by both prerenderPages and prerenderApp
- Save/restore CacheHandler around prerender calls (isolation fix)
- Rename isExplicitlyDynamic -> isConfiguredDynamic for clarity
- Export getRscOutputPath and findDir for cross-module reuse
- Import findDir in run-prerender.ts; remove duplicated inline logic
- Fix single-row table corner character in formatBuildReport
- Remove duplicate JSDoc block before revalidateTag in cache shim
- Add SetCtx interface; eliminate (ctx as any) casts in MemoryCacheHandler
- Replace string sentinel in unstable_cache with structural CacheResultWrapper
- Use completedUrls += 1 consistently in Pages Router phase

* fix: remove unused appDir from PrerenderAppOptions and all callers

prerenderApp() never reads appDir — the bundle is loaded via rscBundlePath
and route scanning is done before the call. Remove the field from
PrerenderAppOptions and AppStaticExportOptions, and drop the now-unused
argument from all callers in run-prerender.ts, static-export.ts, and tests.

* refactor: miscellaneous cleanups across prerender, cache, cli, and deploy

* refactor: document gaps, re-detect project after installDeps, fix Vite resolution in deploy.ts

* refactor(prerender): unify onProgress to single call site; document pageCount semantics

Extract renderUrl() inner function from prerenderApp's concurrency loop so
that onProgress is called exactly once per URL at the outer loop level, rather
than being duplicated inside two early-return branches. Eliminates the risk
of accidentally omitting the callback when adding future early exits.

Add a comment in toStaticExportResult clarifying when pageCount and
files.length can diverge (currently they stay in sync).

* build(prerender): write non-export prerender output to dist/server/prerendered-routes/

On Cloudflare Workers, wrangler.jsonc uses not_found_handling: "none" so
every request hits the worker first. Files in dist/client/ are never
auto-served for page requests — they are uploaded but remain inert.
Writing prerendered HTML/RSC to dist/server/prerendered-routes/ keeps
them co-located with server artifacts and away from the static assets
directory.

This also prevents a future issue: when KV pre-population is implemented,
ISR route files must not be in dist/client/ or they would be served as
stale static files forever (bypassing revalidation).

output: 'export' builds are unaffected — static-export.ts passes its
own outDir explicitly, and runPrerender still writes to dist/client/
when mode === 'export'.

* .

* build: suppress IMPORT_IS_UNDEFINED warnings for generateStaticParams

Dynamic route pages that don't export generateStaticParams produce noisy
IMPORT_IS_UNDEFINED warnings because the virtual RSC entry unconditionally
emits mod?.generateStaticParams for every dynamic route. The optional
chaining guards the access safely at runtime; suppress the build-time noise
in the existing onwarn handler alongside the MODULE_LEVEL_DIRECTIVE filter.

* .

* .

* .

* fix conflict regression

* regen snaps

* update cli comment

* add todo for rsc double req

* encode uri components

* encode uri components

* fix: inherit user plugins in hybrid Pages Router secondary build

The secondary `vinext build` step for hybrid (App Router + Pages Router)
projects was constructed with `configFile: false` and only `vinext({ disableAppRouter: true })`,
dropping all user-supplied plugins including `cloudflare()`. This caused a
false-positive "Missing @cloudflare/vite-plugin" error on any hybrid project
with a `wrangler.jsonc` present.

Fix by inheriting the resolved plugin list from the already-completed App
Router builder, filtering out the `vinext:*` sub-plugins and re-injecting
`vinext({ disableAppRouter: true })` in their place. `configFile: false` is
retained to prevent the user's `environments` block from overriding the SSR
input and `entryFileNames`.

* fix: filter vite:react and vite-tsconfig-paths from inherited hybrid build plugins

vinext auto-registers both @vitejs/plugin-react (vite:react*) and
vite-tsconfig-paths when disableAppRouter is false, so inheriting the
resolved plugin list from the App Router builder caused duplicates.
Filter them out alongside the vinext:* sub-plugins.

* fix: filter rsc:* and vite-rsc-* plugins from inherited hybrid build plugins

@vitejs/plugin-rsc registers resolveId for App Router virtual modules
(virtual:vinext-app-ssr-entry etc.) but the corresponding load hooks live
in vinext:* — which we already strip. With rsc:* present but vinext:* absent
the virtual module resolves but can't load, crashing with PLUGIN_ERROR.
Filter rsc:* and vite-rsc-load-module-dev-proxy alongside the other
auto-registered plugin families.

* fix: skip missing-Cloudflare-plugin guard for hybrid Pages Router secondary build

The secondary build uses configFile:false + vinext({disableAppRouter:true})
so it never loads cloudflare() — by design, it's a plain SSR Rollup bundle
with no Workers config. The configResolved guard was incorrectly treating
this as a misconfigured user build.

Revert the inherited-plugins approach (cloudflare() reconstructs the full
multi-env environments block from its own plugin config hook, independent
of vite.config, causing the App SSR virtual entry to be resolved but not
loadable). Instead, gate the guard on !options.disableAppRouter, which is
exclusively set by this internal invocation.

* fix: inherit user transform plugins in hybrid Pages Router secondary build

The secondary SSR build for hybrid projects (app/ + pages/) was introduced
on this branch and always used configFile:false with only
vinext({disableAppRouter:true}), silently dropping any user transform
plugins from vite.config.ts (SVG loaders, CSS-in-JS, etc.).

Fix by loading the raw user config via loadConfigFromFile (before any
plugin config() hooks fire, so cloudflare() hasn't yet injected its
multi-env environments block) and forwarding all plugins except the
families that vinext auto-registers or that would break the plain SSR
build: vinext:*, vite:react*, rsc:*, vite-tsconfig-paths,
vite-rsc-load-module-dev-proxy, and vite-plugin-cloudflare*.

* fix: reset ANSI styling before prerender and route report output

Vite's logger leaves the terminal in a styled state after build output.
Write an ANSI reset before the prerender label and route report so they
always print at full brightness regardless of preceding Vite output.

* fix: skip instrumentation register() during prerender to prevent process hang

Instrumentation modules like @vercel/otel register OpenTelemetry SDK
exporters with background timers that keep the Node process alive
indefinitely. During prerender these side-effects cause the process to
hang after rendering completes, preventing the route report from printing.

Set VINEXT_PRERENDER=1 in prerenderApp/prerenderPages and gate
__ensureInstrumentation() on that env var so instrumentation is skipped
during prerender builds.

* feat: production prerender pipeline for Cloudflare Workers (App Router + Pages Router)

- Add wrangler devDep to packages/vinext; add `@cloudflare/workers-types` to catalog
- Fix prerender.ts: typed wrangler import, Unstable_DevWorker, undici Response casts,
  URL-string extraction for dev.fetch() (no Request object), staticParamsMap Proxy
- Remove process.env.VINEXT_PRERENDER gate from /__vinext/prerender/static-params endpoint
- Add cf-app-basic pages/ fixture (index, about, posts/[slug], api/ping)
- Update buildCloudflareAppFixture to also run buildPagesFixture for Pages Router bundle
- Add CF prerender tests: shared beforeAll, nested App Router + Pages Router describes (46/46)

* .

* feat: CF Workers hybrid build Pages Router prerender support

* regen snaps

* fix(prerender): detect CF Workers build via @cloudflare/vite-plugin in node_modules

Replace wrangler.json file-presence check with node_modules detection,
consistent with how deploy.ts detects CF projects. The old check looked
in dist/server/ which never has a wrangler.json, so isWorkersBuild was
always false for CF projects, causing only the 404 to be prerendered.

The generated dist/server/wrangler.json (from @cloudflare/vite-plugin)
is now used as the config path for unstable_dev, with the project-root
wrangler.jsonc as a fallback. This ensures assets.directory is present,
which wrangler 4+ requires.

* fix(prerender): address bonk review comments

- runWithConcurrency: early return on empty items instead of spawning a
  spurious worker via the '|| 1' fallback
- staticParamsMap Proxy: flip has() trap to return false so the typeof-fn
  check works for routes without generateStaticParams on CF Workers builds;
  also handle null return from the proxy fn (no generateStaticParams) in
  the parent-params expansion path
- loadWrangler(): extract shared helper with a two-candidate fallback
  (wrangler-dist/cli.js → index.js) used by both prerenderApp and
  runPrerender, replacing duplicated path resolution + existence checks
- VINEXT_PRERENDER process.env mutation: add explanatory comments
  documenting why the global mutation is intentional and safe for the
  sequential-call contract

* refactor(prerender): extract findWranglerConfig, pass CF detection through, cache static-params

- Extract findWranglerConfig(serverDir, projectRoot) helper from both prerender.ts and
  run-prerender.ts, eliminating the duplicated 4-candidate wrangler.json search
- Add isWorkersBuild and wranglerConfigPath optional fields to PrerenderAppOptions so
  runPrerender can pass its already-computed values into prerenderApp, avoiding a
  redundant findInNodeModules walk + 4-candidate fs.existsSync loop on every build
- Add a per-build staticParamsCache (Map keyed on pattern+parentParams) inside the CF
  Proxy to dedup repeated /__vinext/prerender/static-params round-trips for deeply
  nested dynamic routes
- Add clarifying comment on renderUrl's runWithHeadersContext wrapper explaining it is
  a no-op for the CF Workers path (rscHandler is an HTTP proxy; ALS context never
  crosses the isolate boundary) but kept for shape-compatibility across both modes

* fix(prerender): address remaining bonk review comments

- Security: gate /__vinext/prerender/* endpoints behind VINEXT_PRERENDER=1 check
  to prevent exposure in normal deployments (process.env works for both Node and
  CF Workers via Miniflare's var injection into process.env)
- Bug: buildUrlFromParams now throws a clear error when a required param is
  missing instead of silently producing 'undefined' in the URL
- Design: add TODO comments for layout-level generateStaticParams limitation in
  both resolveParentParams() and generateStaticParamsMap
- Correctness: prerenderPages() now uses runtime module exports (getServerSideProps/
  getStaticProps) to classify page type on Node builds instead of static file
  analysis; CF builds continue to use classifyPagesRoute() as fallback

* fix(prerender): address latest bonk review comments

- Add shape validation for parentParams on the /__vinext/prerender/static-params
  endpoint: JSON.parse result is guarded to ensure user generateStaticParams always
  receives a plain object, never a primitive, array, or null
- Add .gitignore for tests/fixtures/cf-app-basic/dist/ to prevent accidental commits
  of build output if test cleanup fails (CF fixture builds to source tree, unlike
  other fixtures that use tmpdirs)
- Add comment on nextConfigOverride shallow merge in run-prerender.ts to make the
  limitation explicit for future maintainers

* regen snaps

* fix: build CF fixture in tmpdir instead of source tree

Use createIsolatedFixture in buildCloudflareAppFixture so the CF Vite
build output goes to a tmpdir rather than tests/fixtures/cf-app-basic/dist/.
Adds an optional nodeModulesDir param to createIsolatedFixture so callers
with fixture-scoped deps (like @cloudflare/vite-plugin) can point the
symlink at the fixture's own node_modules instead of the workspace root.
Removes the stopgap .gitignore and the afterAll that deleted dist/ from
the source tree.

* fix: cache-bust prod-server import() to prevent stale module reuse in tests

startProdServer() used a bare file:// URL for its dynamic import() of the RSC
entry bundle. Node's module cache keyed on that URL, so when two test describe
blocks rebuild to the same output path the second invocation always got the
cached module from the first build. The stale module had __instrumentationInitialized
already set to true and globalThis.__VINEXT_onRequestErrorHandler__ pointing at the
first build's instrumentation instance, whose capturedErrors array lived in a
different module instance than the one the production route handler was reading.

Fix: append ?t=<mtime> to the import URL, matching the pattern used by prerender.ts
loadBundle(). Same mtime means same content (cache hit, no-op); new mtime means
a fresh build and gets a fresh module. Applied to both startAppRouterServer and
startPagesRouterServer.

Also removes debug console.log calls from the instrumentation production test.

* fix: use globalThis for instrumentation test state to survive sequential prod builds

In Vitest, the 'App Router Production build' and 'App Router Production
server' describes run in the same process. The first build's preview server
imports dist/server/index.js uncached, setting
globalThis.__VINEXT_onRequestErrorHandler__ to onRequestError_v1 from the
first module instance. The second build is loaded cache-busted (by mtime),
producing a fresh module instance (v2). After v2 sets the handler, v1's
__ensureInstrumentation can re-fire and overwrite it, causing errors from
v2 to be recorded in capturedErrors_v1 (a different array) while the GET
route reads capturedErrors_v2 (empty).

Fix: store capturedErrors and registerCalled on globalThis (same pattern
as the middleware counter) so all module instances write to and read from
the same shared state regardless of which build instance is active.

Also removes debug logging and restores the afterAll dist cleanup that was
commented out during investigation.

* add process.exit(0) at end of build
2026-03-16 19:18:37 +00:00
Stephen Zhou 25f6e0fc87 chore: enable typeAware and typeCheck, use vp check (#551)
* chore: enable typeAware and typeCheck, use vp check

* Build

* Try cache false

* Revert "Try cache false"

This reverts commit 5f76ed02f1.

* Update

* workaround for vp check

* Try no workaround

* Fix check

* Update to 0.1.12

* denyWarnings
2026-03-16 11:00:47 -05:00
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* 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
2026-03-15 10:50:13 +00:00
Stephen Zhou 20285ccb83 chore: move to pnpm catalogs (#531) 2026-03-13 17:14:35 +00:00
James Anderson e02fedca1c fix: exclude benchmarks/nextjs from pnpm workspace to prevent next dep hoisting (#508)
Adding benchmarks/* to the workspace in the vite v8 bump caused pnpm to
hoist next out of benchmarks/nextjs/node_modules into the monorepo root.
Turbopack then couldn't find next/package.json from the project directory,
failing with a workspace root inference error.

Fix by listing benchmarks/vinext and benchmarks/vinext-rolldown explicitly
instead of using the glob, leaving benchmarks/nextjs isolated with its own
node_modules.

Also remove the eslint key from next.config.ts — it was removed from the
NextConfig type in Next.js 16 and caused an 'Unrecognized key' warning.
2026-03-12 18:27:23 +00:00
James Anderson c3dd62dbc1 bump vite beta usage to v8 (#506) 2026-03-12 17:57:46 +00:00
Steve Faulkner 12fea722b6 Initial public release of vinext 2026-02-24 09:29:39 -06:00