Skip to content

[vitest-plugin] Support cloudflare.config.ts with the new config option - #16424

Draft
edmundhung wants to merge 9 commits into
mainfrom
edmundhung/devx-2645-vitest-native-cf-config
Draft

edmundhung wants to merge 9 commits into
mainfrom
edmundhung/devx-2645-vitest-native-cf-config

Conversation

@edmundhung

@edmundhung edmundhung commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Fixes DEVX-2645.

Add official cloudflare.config.ts support to @cloudflare/vitest-plugin through a new config option, and remove the experimental experimental.newConfig.

cloudflareTest({ config: true }); // or config: "./worker/cloudflare.config.ts"

Depends on #16428, which should merge first: src/pool/auth.ts mirrors the getRemoteBindingsAuth() it adds to @cloudflare/vite-plugin.

Please review the full diff rather than commit by commit: follow-up commits added and then removed a native miniflare surface, and the repository squash-merges. The main changes:

  1. Load cloudflare.config.ts natively under experimental.newConfig.
    • The parsed config is applied to the runner Worker in Miniflare's native WorkerOptions.config shape. It is no longer converted to a Wrangler config and run through unstable_getMiniflareWorkerOptions(), and this path no longer imports wrangler.
    • Local secrets, remote bindings and zone use small copies of @cloudflare/vite-plugin v2's glue, each marked to be kept in sync.
    • Remote bindings authenticate with cf credentials (cf auth login or CLOUDFLARE_API_TOKEN), and messages name cf. The session keeps its auth hook while the account is unchanged, so re-parsing a project doesn't restart it.
    • The config's env, exports and triggers are applied to the runner. Tail consumers aren't supported yet and are dropped with a warning, and so is the assets binding, as there's no assets directory.
    • The Wrangler-config path is unchanged, including its credentials.
    • Durable Object bindings to the config's own Worker now work; the conversion used to break them.
  2. Replace experimental.newConfig with the config option.
    • It takes true, false or a path to the file.
    • As with wrangler, the file is only loaded when the option is set, and setting both is an error. With neither set, behaviour is unchanged.
    • Passing the removed experimental.newConfig fails with a pointer to config.
    • If the file fails to load and its name mentions wrangler, the error suggests the wrangler option.
  3. Move the D1 SQL splitter to @cloudflare/workers-utils. This means readD1Migrations() no longer loads Wrangler. It's an internal refactor, independent of the first two, and can be dropped from this PR on its own.

miniflare with config

config can't be combined with miniflare options, so the Worker is configured entirely by cloudflare.config.ts. A stacked follow-up will add options for test-time changes and auxiliary Workers, modelled on @cloudflare/vite-plugin v2.

Secrets on the config path

.dev.vars and .env follow the same rules as @cloudflare/vite-plugin and the cf CLI, rather than Wrangler's:

  • they're read from Vite's envDir (the project root by default);
  • only bindings declared as { type: "secret" } are filled;
  • .dev.vars doesn't override text or json bindings;
  • mode-specific .dev.vars.<mode> and .env.<mode> files are read (Vitest's mode defaults to test).

One intentional difference from the Vite plugin, kept from the Wrangler path: CLOUDFLARE_* tool settings such as CLOUDFLARE_API_TOKEN are read from the environment, not from .env. The Vite plugin copies them from .env into process.env, but Vitest runs every project in one process, so that copy would leak one project's values into the others. Supporting them per project is left to a follow-up. CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_<NAME> fills a binding rather than configuring the tool, so like secrets it's read from each project's .env, as in the Vite plugin.

Measurements

A full vitest run of a minimal cloudflare.config.ts project goes from a median of 1.11 s (experimental.newConfig on main) to 0.85 s (n=8, local), mainly because wrangler is no longer imported.


@changeset-bot

changeset-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6400e04

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/vitest-plugin Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@16424

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@16424

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@16424

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@16424

@cloudflare/containers-shared

npm i https://pkg.pr.new/@cloudflare/containers-shared@16424

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@16424

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@16424

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@16424

miniflare

npm i https://pkg.pr.new/miniflare@16424

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@16424

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@16424

@cloudflare/runtime-types

npm i https://pkg.pr.new/@cloudflare/runtime-types@16424

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@16424

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@16424

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@16424

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@16424

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@16424

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@16424

wrangler

npm i https://pkg.pr.new/wrangler@16424

commit: 6400e04

@ask-bonk

ask-bonk Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

LGTM!

github run

@edmundhung
edmundhung force-pushed the edmundhung/devx-2645-vitest-native-cf-config branch from b8c4eca to 78e99da Compare October 22, 2026 14:01
@edmundhung edmundhung changed the title [vitest-plugin] Load cloudflare.config.ts natively and add the cloudflareConfig option [vitest-plugin] Support cloudflare.config.ts with the new config option Oct 22, 2026
@edmundhung
edmundhung force-pushed the edmundhung/devx-2645-vitest-native-cf-config branch from 78e99da to b56a0dd Compare October 22, 2026 14:16
@edmundhung
edmundhung force-pushed the edmundhung/devx-2645-vitest-native-cf-config branch from b56a0dd to 25ebf5f Compare October 22, 2026 15:07
….newConfig

`experimental.newConfig` converted `cloudflare.config.ts` to a Wrangler config
and ran it through Wrangler's `unstable_getMiniflareWorkerOptions()`. The parsed
config is already in the shape Miniflare's native `WorkerOptions.config` takes,
so apply it to the runner Worker directly and stop importing `wrangler` on this
path. Local secrets and remote bindings use copies of `@cloudflare/vite-plugin`'s
glue, and remote bindings authenticate with cf credentials. Wrangler
configuration files are handled exactly as before.

This also fixes Durable Object bindings to the config's own Worker, which the
conversion turned into bindings to another Worker.

Refs DEVX-2645
Load `cloudflare.config.ts` through a stable `config` option: `true` for the
project root, or a path to the file. As with `wrangler`, the file is only
loaded when the option is set, and setting both is an error.
`experimental.newConfig` is removed; passing it now fails with a pointer to
`config` rather than being silently ignored.

If the file fails to load and its name mentions `wrangler`, the error suggests
the `wrangler` option instead.

Refs DEVX-2645
`readD1Migrations()` in `@cloudflare/vitest-plugin` imported all of Wrangler
just to split migration files into statements. Move `splitSqlQuery()` and its
trimmer into `@cloudflare/workers-utils`, with their tests, and import them
from there in Wrangler and the Vitest plugin. Wrangler's
`unstable_splitSqlQuery` export is unchanged.

Refs DEVX-2645
@edmundhung
edmundhung force-pushed the edmundhung/devx-2645-vitest-native-cf-config branch from 25ebf5f to 1b451aa Compare October 23, 2026 13:32
@edmundhung
edmundhung marked this pull request as ready for review October 23, 2026 13:48
@workers-devprod
workers-devprod requested review from a team and petebacondarwin and removed request for a team October 23, 2026 13:49
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/d1
  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/vitest-cloudflare-config.md: [@cloudflare/wrangler]
  • fixtures/vitest-plugin-examples/new-config/README.md: [@cloudflare/wrangler]
  • fixtures/vitest-plugin-examples/new-config/vitest.config.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/AGENTS.md: [@cloudflare/wrangler]
  • packages/vitest-plugin/package.json: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/auth.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/config.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/d1.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/index.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/local-env.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/new-config.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/src/pool/remote-bindings.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/test/d1.test.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/test/new-config-remote-bindings.test.ts: [@cloudflare/wrangler]
  • packages/vitest-plugin/test/new-config.test.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/d1-splitter.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/d1-trimmer.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/index.ts: [@cloudflare/wrangler]
  • packages/workers-utils/tests/d1-splitter.test.ts: [@cloudflare/wrangler]
  • packages/workers-utils/tests/d1-trimmer.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/test-harness.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/cli.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/d1/execute.ts: [@cloudflare/d1 @cloudflare/wrangler]
  • pnpm-lock.yaml: [@cloudflare/wrangler]

@edmundhung
edmundhung marked this pull request as draft October 23, 2026 13:49
@clanky-the-clanker

Copy link
Copy Markdown

💬 Approved with comments — No correctness, security or performance problems found. The only note is an optional README update for the WebAssembly fixture, which should mention that default module rules now also apply with the new config option.

Findings: 1 info

The decision is advisory: this comment never approves, blocks or requests changes on the pull request.

🔧 Fix with your agent

Paste the following into your coding agent to investigate and fix these issues.

Fix the following code review issues found in pull request #16424.

Before making changes, review each issue and present a brief summary table:
- For each finding, state whether you agree, disagree, or need clarification
- If you disagree (e.g., the fix would require disproportionate changes
  for minimal ROI, or the finding is factually incorrect), explain why
- If you need to clarify something before deciding, ask those questions
- Then share your plan for which issues to tackle and in what order

## INFO (1)

### [Documentation] web-assembly fixture README says default module rules need `wrangler.configPath`; the new `config` option enables them too (config.ts:496-498).
- **File:** fixtures/vitest-plugin-examples/web-assembly/README.md:3

📝 Documentation: 💬 1 info

The WebAssembly fixture README is out of date about default module rules.

Recommendation: 💬 Optional Nits

  • [INFO] web-assembly fixture README says default module rules need wrangler.configPath; the new config option enables them too (config.ts:496-498). (fixtures/vitest-plugin-examples/web-assembly/README.md:3)
View full review

fixtures/vitest-plugin-examples/web-assembly/README.md:3 says Wrangler's default module rules are enabled only when wrangler.configPath is set. The config path now adds DEFAULT_MODULE_RULES too (packages/vitest-plugin/src/pool/config.ts:496-498). Consider updating the sentence to cover both options.


✅ All other checks passed

🚀 Release: ✅ LGTM

Risk: Low. The changeset covers the new config option and the removal of experimental.newConfig. Moving the D1 splitter is an internal refactor, so the repository's changeset rules don't require an entry for it.


🔒 Security: ✅ LGTM

  • Credentials: cf credentials come from the cf auth profile. Nothing is hardcoded.
  • Secrets: Secrets from .dev.vars and .env fill only bindings declared as { type: "secret" }. Missing secrets are skipped, and the warning prints their names, not their values.
  • Prompts: In non-interactive or CI contexts, account selection throws an error instead of prompting.
  • Injection: There is no injection, path traversal or command execution. The config path points to the user's own project file, so it is not a trust boundary.
  • Moved code: Moving the D1 SQL splitter to @cloudflare/workers-utils changes no behaviour.

📖 AGENTS.md: ✅ LGTM

The packages/vitest-plugin/AGENTS.md updates match the new config option.


📋 Code Quality: ✅ LGTM

No code quality issues found.


⚡ Performance: ✅ LGTM

  • No wrangler import: The config path no longer imports wrangler. This matches the speed-up reported in the description (median 1.11 s down to 0.85 s).
  • D1 migrations: readD1Migrations() now calls the splitter in @cloudflare/workers-utils instead of lazily loading Wrangler.
  • Remote proxy sessions: Sessions are keyed by config path, and the auth hook is reused while the account stays the same. disposeAllRemoteProxySessions() clears them, so they don't pile up or leak.
  • Env files: .env and .dev.vars are loaded in parallel, once per config load.

Reviewed commit: 1b451aa0

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Devin Review

Comment thread packages/vitest-plugin/src/pool/index.ts Outdated
Comment on lines +496 to +499
options.moduleRules = [
...DEFAULT_MODULE_RULES,
...(options.moduleRules ?? []),
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Custom module rules are shadowed by defaults

With config enabled, DEFAULT_MODULE_RULES precedes user rules. compileModuleRules finalizes each default type first, silently discarding custom rules of that type.

Learn more

The pool uses compileModuleRules when intercepting test module imports. Unless a rule enables fallthrough, the compiler ignores every later rule of the same type. The three defaults here all omit fallthrough, so any user-defined Text, Data, or CompiledWasm rule is discarded. Wrangler orders user rules before defaults in unstable_getMiniflareWorkerOptions.

Example: Setting miniflare.modulesRules: [{ type: "Text", include: ["**/*.md"] }] alongside config: true does not activate text loading for .md files; the default Text rule finalizes that type first.

Recommended fix: Place user rules before default rules, preserving their precedence as in the Wrangler path. Check that fallthrough behavior remains correct for user rules extending a default type.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread packages/vitest-plugin/src/pool/index.ts
Comment on lines +180 to +190
for (const [key, binding] of Object.entries(cloudflareConfig?.env ?? {})) {
if (binding.type === "durable-object") {
result.set(key, {
className: binding.exportName,
// A binding to the config's own Worker targets the (renamed) runner
scriptName:
binding.worker === cloudflareConfig?.worker.name
? undefined
: binding.worker,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Overridden binding breaks Durable Object helpers

When miniflare.bindings overrides a config Durable Object with text, getDurableObjectDesignators still registers it. getSameIsolateNamespaces then rejects even unrelated valid Durable Object stubs.

Learn more

The runner merges miniflare bindings over config bindings in applyCloudflareConfig. The new designator loop instead records the config binding without consulting those overrides. getSameIsolateNamespaces examines every recorded binding and asserts that its runtime value is a Durable Object namespace. One stale map entry causes any subsequent same-isolate helper call to fail.

Example: Config declares OLD and COUNTER as Durable Objects. Pool options set miniflare.bindings.OLD = "disabled". Calling runInDurableObject on a COUNTER stub fails while examining OLD, which is now a string.

Recommended fix: Build the designator map from the effective merged bindings, or skip config entries overridden by any miniflare binding type. Keep precedence consistent with applyCloudflareConfig.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +442 to +460
// A different hook restarts the session, so reuse the session's own hook
// while the account is unchanged
const authKey = JSON.stringify({ accountId: settings.accountId });
const preExistingRemoteProxySessionData =
remoteProxySessionsDataMap.get(configPath);
const remoteProxySessionData = options.remoteBindings
? await maybeStartOrUpdateRemoteProxySession(
{
name: worker.name,
bindings: cloudflareConfig.remoteBindings,
complianceRegion:
settings.complianceRegion === "fedramp-high"
? "fedramp_high"
: settings.complianceRegion,
},
preExistingRemoteProxySessionData ?? null,
(preExistingRemoteProxySessionData?.authKey === authKey
? preExistingRemoteProxySessionData.auth
: undefined) ??

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Profile changes and cached sessions

authKey tracks the configured account, while getRemoteBindingsAuth resolves credentials lazily from a cached profile. Check whether changing credentials during watch mode needs a session restart.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

2 participants