* ci: add Next.js Pages Router deploy suite harness
Add the workflow, scripts, and manifest generator for running the
Next.js Pages Router deploy test suite against vinext.
Triggers:
- Nightly cron (06:00 UTC) against main
- Manual dispatch with configurable vinext-ref, next-ref, and concurrency
Extracted from #922.
* refactor: align deploy suite harness with Next.js adapter testing docs
- Rename scripts to match Next.js convention: e2e-deploy.sh, e2e-logs.sh,
e2e-cleanup.sh
- Split workflow into build + test jobs (build once, shard 16 ways) per
the documented adapter testing pattern
- Generalize workflow from pages-only to configurable: add suite-filter
input (pages/app/all, defaults to pages)
- Add --filter flag to manifest generator (replaces the pages-hardcoded
version)
- Accept ADAPTER_DIR (Next.js docs convention) alongside VINEXT_DIR
- Rename workflow file to nextjs-deploy-suite.yml
- Use matrix.group strings ("1/16") instead of shard integers, matching
the docs example
* ci: default suite-filter to app
* ci: bump Next.js default to v16.2.6, move cron to 02:00 UTC
* address review: add report job, clean up cache paths, document compat shims
- Add a report job that aggregates .results.json across all shards into
a GitHub Actions summary (pass/fail/skip counts, failed test details,
passed suite list) and uploads a JSON report artifact
- Upload test results from every shard (not just on failure)
- Remove redundant next.js/ from cache paths (already under .)
- Add comment explaining why IS_TURBOPACK_TEST=1 is needed
- Clean up stale PID file in e2e-cleanup.sh
- Add header comments to all scripts explaining their purpose and contract
- Document the hardcoded warning messages and .next/trace shim
- Document the YAML catalog parser assumptions
- Fix [[ ]] -> [ ] inconsistency in run-nextjs-deploy-suite.sh
* ci: add temporary PR trigger to test workflow [remove before merge]
* fix: address deploy script failures found in CI and local testing
Bugs found and fixed:
- IMMUTABLE_ASSET_TOKEN marker renamed to NEXT_SUPPORTS_IMMUTABLE_ASSETS
to match what next-deploy.ts parseIdsFromCliOuput() actually parses.
Without this, every test fails with 'Failed to get supportsImmutableAssets'.
- Deploy script now injects "type": "module" into test app package.json
and generates a vite.config.ts. Without type:module, Rolldown emits .mjs
files but the RSC plugin's cross-environment imports expect .js. This
mirrors what `vinext init` does (steps 3 and 5).
- Added vp (Vite+) fallback to run_pnpm() for environments where pnpm
and corepack are not available but vp is.
- Cleanup script now uses process group kills (kill -TERM -PID) and a
port-based lsof fallback to handle orphaned child processes from
vp/pnpm exec wrappers.
- Error cleanup trap now dumps the last 80 lines of the build log and
last 40 lines of the server log to stderr, so failures are visible
in CI output (previously only showed file sizes).
- Test job uses run-install: false since the workspace is already built
in the cache from the build job.
Tested locally: deploy script succeeds, curl returns valid HTML, logs
script outputs all three required markers (BUILD_ID, DEPLOYMENT_ID,
NEXT_SUPPORTS_IMMUTABLE_ASSETS), cleanup kills the server.
* fix: use vinext init in deploy script, add next.config.js to CJS rename list
Replace the manual type:module / vite.config.ts / CJS rename logic in
e2e-deploy.sh with a single `vinext init --skip-check --force` call.
This runs after pnpm install (so deps are available) and before
vinext build.
Also add next.config.js to the CJS_CONFIG_FILES list in project.ts.
Without this, vinext init adds type:module but doesn't rename CJS
next.config.js files, causing 'module is not defined in ES module
scope' errors. This was the root cause of the CI failures — Next.js
test fixtures use module.exports in next.config.js.
All 61 init tests pass.
* fix: convert CJS next.config.js to ESM instead of renaming to .cjs
Next.js doesn't support next.config.cjs, so we can't rename it. Instead,
convert module.exports/require() to export default/import in-place after
vinext init adds "type": "module".
Also revert adding next.config.js to CJS_CONFIG_FILES — vinext init
should not rename it since it's not a generic config file.
Tolerate pnpm 10+ ERR_PNPM_IGNORED_BUILDS exit code 1 on install (the
install completes, packages are in node_modules, but pnpm exits non-zero
due to unapproved build scripts). Verify node_modules/vinext exists
as a safety check.
Tested against the real Next.js actions-streaming fixture: CJS
next.config.js converted, app builds, server starts, HTTP 200.
* fix: emit both IMMUTABLE_ASSET_TOKEN and NEXT_SUPPORTS_IMMUTABLE_ASSETS
v16.2.x parses IMMUTABLE_ASSET_TOKEN, canary renamed it to
NEXT_SUPPORTS_IMMUTABLE_ASSETS. Emit both for cross-version compat.
* fix: enable JSX in .js files, convert CJS next.config.ts to ESM
Add vinext:jsx-in-js transform plugin to handle JSX in plain .js files.
Next.js allows JSX in .js files (Babel/SWC handle it transparently),
but Vite 8's built-in vite:oxc plugin excludes .js files by default
(exclude: /\.js$/) AND infers lang: 'js' from the extension (which
disables JSX parsing). Neither can be fixed via config alone.
The plugin runs with enforce: 'pre' before vite:oxc, transforming .js
files with OXC using lang: 'jsx' so JSX is compiled before vite:oxc
sees the code. This unblocks ~60 test suites in the deploy suite.
Also extend the deploy script's CJS-to-ESM config converter to handle
next.config.ts files (not just .js), fixing ~6 more test failures.
Tested: 1284 tests pass (jsx-in-js + shims + routing + app-router),
zero regressions.
* ci: trigger clean workflow run
* fix: improve CJS-to-ESM converter for next.config files
The naive regex converter missed two patterns:
- require('mod')(args) produced invalid ESM (import X from 'mod'(args))
- require() calls inside function bodies were not converted
New converter handles:
- module.exports = X → export default X
- const X = require('mod') → import X from 'mod'
- const X = require('mod')(args) → import _X from 'mod'; const X = _X(args)
- require('mod') in expressions → (await import('mod')).default
Tested against bundle-analyzer, path, and inline require patterns.
* .
* Apply suggestion from @james-elicx
* fix: handle destructured requires in CJS converter, read port from file in cleanup
- Add pattern for const { a, b } = require('mod') → import { a, b } from 'mod'
- Read port from PORT_FILE in cleanup_on_error trap instead of using $PORT
variable which may be unset if the script fails before port allocation
- Add TODO noting remaining CJS edge cases (dynamic require, require.resolve)
* Apply suggestions from code review
Co-authored-by: James Anderson <james@eli.cx>
* Apply suggestion from @james-elicx
The benchmarks workflow uploads the artifact via
actions/upload-artifact (writes only to the run's artifact store, not
the repo) and POSTs results to benchmarks.vinext.workers.dev using
secrets.BENCHMARK_UPLOAD_TOKEN. The GitHub token is only used for the
checkout, so contents: read is sufficient. Top-level style matches
the rest of the repo.
The create-next-app job was failing with ERR_PNPM_UNEXPECTED_STORE.
create-next-app's bundled pnpm (10.x) installs node_modules linked to
the v10 store; vp's bundled pnpm recently bumped to v11, which then
refuses to add packages to the v10-linked node_modules.
Anchor the scaffolded project to vinext's own pinned pnpm version
(read from root package.json's packageManager field) and invoke that
pnpm directly via npx for the install step. This:
- keeps store layouts aligned between create-next-app's install and
the subsequent vinext add
- stays in sync with future pnpm bumps without further CI edits
- bypasses vp's bundled pnpm only for this layering step; the rest of
the job (vp exec, vp dlx) is unchanged
* fix: resolve symlink paths in standalone BFS package copy (#724)
When resolvePackageJsonPath finds a package via the lookup-paths
fallback, the returned path may be a symlink (e.g., under a hoisted
node_modules/ directory). Calling createRequire() on a symlink
path causes Node module resolution to miss transitive dependencies
because it walks up from the symlink ancestor directories rather
than the real (e.g., .pnpm/) path.
Fix: call fs.realpathSync() on the resolved package.json path
before using it for createRequire() and readPackageJson().
Also adds E2E test coverage for the standalone output mode.
* fix: address review feedback
Add standalone-output to CI E2E matrix, rename server header test
to reflect what it actually asserts, fix unused req parameter.
Bots throw "max_tokens not supported, use max_completion_tokens" on
1.4.6 with gpt-5.x reasoning models. opencode 1.4.6 predates the
cf-ai-gateway plugin's chat.params hook (PR #22864, landed in 1.4.7),
which strips maxOutputTokens for OpenAI reasoning models so the
@ai-sdk/openai-compatible default no longer leaks the rejected field.
The earlier ProviderInitError on 1.14.x was a separate regression in
opencode's npm install path: the major bump introduced loadOptions(dir)
via @npmcli/config inside Npm.add, which fails on CI runners and bubbles
up as InstallFailedError, then gets re-wrapped at provider.ts:1522 as
ProviderInitError with the original cause swallowed by the session
error path. Other Cloudflare repos (workers-sdk, workerd, kumo) hit the
same regression on anthropic and pinned 1.4.6 / 1.2.27 to dodge it.
1.4.11 is the last 1.4.x release: it has the chat.params max_tokens
patch and predates the loadOptions install regression. Same
ai-gateway-provider 3.1.2, same @ai-sdk pins as 1.4.6, so the install
path stays the known-good arborist.reify call shape that works for
workers-sdk + anthropic on 1.4.6.
Verified locally with opencode 1.14.25 + cf-ai-gateway/openai/gpt-5.4 +
fake creds: empty-cache short-circuit and loadOptions both reproduce
the silent ProviderInitError. 1.4.11 lacks both code paths.
Every bonk and bigbonk run since opencode 1.14.25 throws
UnknownError: ProviderInitError during cf-ai-gateway SDK init,
regardless of model. Verified across gpt-5.5 (#898), gpt-5.4 (#900),
and the throw fires before any per-model code path runs.
workers-sdk hit the same regression and pinned opencode_version to
1.4.6 with an inline comment naming ProviderInitError as the cause.
Several other Cloudflare repos pin 1.2.27 / 1.4.6 for the same
reason. vinext is the only Cloudflare repo running OAI through
cf-ai-gateway, so the OAI-specific path stayed unobserved until #898
re-tried it on 1.14.25.
Pinning to 1.4.6 here reuses workers-sdk's known-good version. The
gpt-5.4 model from #900 stays; gpt-5.4 predates 1.4.6's bundled
snapshot by over a month, so registry resolution is fine.
bonk and bigbonk currently throw ProviderInitError on every invocation
after the gpt-5.5 switch in #898. The lookup-side regression that #899
patched was masked by the opencode bump (1.14.22 -> 1.14.25) but a
second failure now surfaces during SDK init for cloudflare-ai-gateway.
opencode swallows the underlying cause, and the gpt-5.5 path through
ai-gateway-provider has no working precedent in this repo, so iterating
on it would block PR review indefinitely.
Roll the model back to gpt-5.4, which has a known-good path through
opencode + ai-gateway-provider + Cloudflare AI Gateway, and remove the
opencode.json workaround introduced in #899. Reasoning effort stays at
"xhigh" per James's preference; opencode will fall back to its default
effort if gpt-5.4 doesn't accept it.
Once gpt-5.5 SDK init is fixed upstream (opencode + ai-gateway-provider
+ models.dev cloudflare-ai-gateway entry), we can roll forward again.
* ci(bonk): switch bonk workflow to GPT-5.5 xhigh variant
Updates the bonk workflow to use cloudflare-ai-gateway/openai/gpt-5.5
with reasoning effort variant xhigh, replacing the previous
claude-opus-4-7 model.
https://claude.ai/code/session_012n6RFuqfQGQY7AaayqLcaG
* ci(bonk): extend GPT-5.5 xhigh switch to bigbonk, agent defs, and docs
- bigbonk workflow: cloudflare-ai-gateway/openai/gpt-5.5 with variant xhigh (was claude-opus-4-7 + max)
- viguy and reviewer OpenCode agent definitions: openai/gpt-5.5 (was anthropic/claude-opus-4-7)
- CONTRIBUTING.md: update recommended setup and BigBonk description to GPT-5.5 xhigh
The Next.js tracker workflow and agent are intentionally left on Claude
Opus 4.7.
https://claude.ai/code/session_012n6RFuqfQGQY7AaayqLcaG
---------
Co-authored-by: Claude <noreply@anthropic.com>
* chore: upgrade workflows from claude-opus-4-6 to claude-opus-4-7
Update model references in bonk, bigbonk, and nextjs-tracker workflows
to use the latest Opus 4.7 model via the Cloudflare AI Gateway.
https://claude.ai/code/session_018pEfYehfR4YDA1Ekmu1vn3
* chore: upgrade opencode agents from claude-opus-4-6 to claude-opus-4-7
Update model references in reviewer, viguy, and nextjs-tracker agent
configs to use the latest Opus 4.7 model.
https://claude.ai/code/session_018pEfYehfR4YDA1Ekmu1vn3
---------
Co-authored-by: Claude <noreply@anthropic.com>
* chore: harden CI supply chain
- Pin voidzero-dev/setup-vp to commit SHA across all workflows
- Pin create-next-app and pkg-pr-new to exact versions
* pull setup-vp into setup action
---------
Co-authored-by: James <james@eli.cx>
* 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>
* test: add static export E2E tests (#564)
Add Playwright E2E tests for `output: "export"` static builds covering
both App Router and Pages Router. Includes a lightweight static file
server with path traversal protection, proper error handling, and
startup validation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update tests/e2e/static-export/serve-static.mjs
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
The agent kept launching a Task subagent to "explore the vinext codebase
structure" despite instructions not to, consuming the entire 30-minute
timeout. The task permission deny in the agent config was not effective.
Fix: embed the commits JSON directly into the prompt so the agent has
all data immediately and no reason to explore. Also add explicit
"do not explore, do not launch subagents" at the end of the prompt.
The ask-bonk action doesn't support a prompt_file input. The prompt was
silently ignored, causing the scheduled run to fail with 'PROMPT input
is required'. Set the prompt as a multiline step output and reference it
via the prompt input instead.
Daily scheduled job that scans Next.js canary commits, uses an AI agent
to classify relevance to vinext, and opens labeled tracking issues for
anything that matters. Supports a dry-run mode for validation.
* 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
* feat: full next/compat/router support and fix check scanner for shimmed modules
- Export NextRouter interface from shims/router.ts so compat-router.ts can use it
- Add NextRouter | null return type to useRouter() in shims/compat-router.ts
- Add next/compat/router, next/form, next/web-vitals, next/constants to IMPORT_SUPPORT in check.ts so vinext check no longer flags them as unsupported
- Add integration fixture pages and components for next/compat/router in both pages-basic and app-basic fixtures
- Add integration tests in tests/pages-router.test.ts and tests/nextjs-compat/hooks.test.ts
- Add unit tests in tests/check.test.ts covering all four newly-registered imports
* test: update entry-templates snapshots for compat-router-test fixture page
* remove type assertion
* fix: add declare module for next/compat/router to fixture next-shims.d.ts files
* fix: add declare module for next/compat/router to internal next-shims.d.ts
* pin to vp setup 1.0.0
The version bump step modifies package.json without committing, leaving
the working tree dirty. pnpm publish enforces git-checks by default and
refuses to publish. Pass --no-git-checks before the -- separator so
pnpm skips the check while --provenance still reaches npm.
* 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
* fix: tag GitHub @usernames in release notes, publish release directly
* fix: move contributor mentions to end-only Contributors section
* fix: trim SHA to 7 chars in commits.txt sent to AI
* 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