Skip to content

fix: high-severity bugs in the CLI, runtime and adapters - #402

Merged
Brooooooklyn merged 16 commits into
mainfrom
fix/high-bugs
Oct 12, 2026
Merged

Brooooooklyn merged 16 commits into
mainfrom
fix/high-bugs

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Problem

The pre-open-source review of the SDK, framework and CLI packages confirmed 17 high-severity bugs. Each one breaks a documented command or a common setup:

database   void db reset / seed / export on MySQL 8 · db reset after `void db connect` · TS seeds
init       pnpm build approvals lost · Windows paths with spaces
deploy     pinned-secret Cloudflare deploys · custom binding names on managed deploys
runtime    remote D1 sessions · MySQL migration breakpoints · MySQL auth migrations
adapters   React SSR errors hang · Solid remounts on every update · Svelte island CSS
config     BOM-prefixed wrangler.jsonc · Astro dev ignores src/middleware.ts for good

What this PR fixes

One commit per bug. Each commit message has Bug, Repro, Cause, Fix and Test sections for review.

The review found 17 bugs; this PR has 16 fixes. The Vite+ starter fix (pnpm pinned with no build policy) was dropped: main now installs Vite+ projects through vp and keeps the project's package manager (328c186), so the bug no longer occurs.

Area What a user hits today After this PR
void db reset / seed After void db connect, drops every table in the deployed PostgreSQL/MySQL database with no prompt Asks first on a non-local host (--yes skips; localhost skips)
void db reset (MySQL 8) Drops nothing, still prints "Database reset" Drops all tables
void db seed (TS) Crashes on void gen model barrels after the DB is already wiped Loads extensionless imports
void db export (MySQL 8) Table 'app.undefined' doesn't exist Exports every table; restore runs with FK checks off
void init (pnpm) Rewrites the user's allowBuilds / onlyBuiltDependencies Merges into the user's list
void init (Windows) node spawned through a shell breaks on paths with spaces Spawns node without a shell
void deploy --platform cloudflare Pinned-secret deploys reject limits / placement / cache Uploads limits, smart placement and cache with the version
Managed void deploy Ignores custom D1/KV/R2 binding names Sends them, as docs/reference/config.md says
Astro dev Leaves src/middleware.ts in .gitignore for good Adds the line only while dev runs
Remote mode (remote: true) void/db and auth crash: no withSession Remote D1 binding supports sessions
MySQL migrations Sends --> statement-breakpoint lines to MySQL Splits on breakpoints
MySQL auth migrations Re-runs fail on MySQL 8 (uppercase info-schema keys) Idempotent on MySQL 8
React Pages SSR A page error hangs the request Rejects → 500 page or dev overlay
Solid Pages Remounts page and layouts on every navigation and deferred update Keeps layouts; GET navigation re-creates the page (same as Vue)
Svelte islands Production ships raw, unscoped <style> CSS (or the build fails) Bundles the compiled CSS
Root wrangler.jsonc with BOM Read fails, file treated as missing BOM is stripped, as Wrangler does

How each fix was made

verify   reproduce on the #400 head (failing test or script)
fix      regression test first, then the smallest fix, one commit
test     independent tester: the test fails with the fix reverted, affected tests, typecheck
review   adversarial review of the commit → a verifier checks each claim → fix-up if needed

Decisions

Topic Decision
Reset on a remote DB void db connect still writes the URL to .env. Only reset/seed ask before they drop tables on a non-local host.
Managed custom binding names Projects that already set custom names get new resources under those names on the next deploy. The old DB/KV/STORAGE resources stay.
Pinned-secret deploys Targeted placement (region, host, hostname) still fails, now before any API request: Cloudflare's version endpoint can drop the target, and an --atomic upload is live at once. The error names the key and says what to do: remove cloudflare.placement, or set it to { mode: 'smart' }. logpush and tail_consumers are accepted but not sent. No Cloudflare version upload applies them, with or without secrets (same as observability). With secrets, --atomic deploys and the first deploy after void secret put also keep the live values. Without secrets, those two paths apply them through cf deploy.
React SSR errors Errors before the shell is sent reject the render. Errors after the shell keep React's default client-render fallback.
Solid navigation Same as Vue: GET navigation re-creates the page; mutations, router.refresh() and preserveState keep it.

Known limits (not in this PR)

  • Astro: after a hard kill (SIGKILL, crash), or in apps that ran dev with an older Void, the src/middleware.ts line stays in .gitignore until you delete it by hand.
  • void db seed: the database is reset before the seed file loads, so a seed that fails to load leaves an empty database. Base has this too.
  • Remote D1 sessions: withSession() ignores constraints and bookmarks (Remote D1 binding: withSession() ignores constraints and bookmarks #403).
  • Svelte islands: CSS of child components that render only on the server is still missing.
  • MySQL migrations: a failed multi-statement migration cannot resume. Base has this too.

Rollout

main ── #399 (Cloudflare e2e fixes) ── #400 (cleanup) ── this PR (high-severity bugs)

Merge #399 and #400 first.

Testing

Check Result
vp run build:all + node test/check-playground-build-artifacts.mjs pass
vp run check pass
vp run test --base <#400 head> pass (480 files, 10,261 tests)
Windows CI (test: windows) pass
Regression tests One per bug; each fails when its fix is reverted

🤖 Generated with Claude Code

https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH

@Brooooooklyn
Brooooooklyn added this pull request to stack #401 October 11, 2026 15:07
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T09:51:29.881550Z c71da13 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://fix-high-bugs-void-cloud.voidzero-docs.workers.dev (commit c71da13)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://4c72a26d-void-cloud.voidzero-docs.workers.dev c71da13 2026-10-12T09:48:03.297Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://63e7405a-void-cloud.voidzero-docs.workers.dev 26f35ae 2026-10-12T09:35:46.923Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://213eab49-void-cloud.voidzero-docs.workers.dev 0c32e53 2026-10-12T09:02:23.769Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://7459d254-void-cloud.voidzero-docs.workers.dev efb1748 2026-10-12T08:52:11.052Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://6dbe6bad-void-cloud.voidzero-docs.workers.dev fc7e4c2 2026-10-12T06:23:14.648Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://a7e21844-void-cloud.voidzero-docs.workers.dev 04b2063 2026-10-12T06:13:26.798Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://b2ead3c8-void-cloud.voidzero-docs.workers.dev 20aea49 2026-10-12T04:48:53.067Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://9185f22b-void-cloud.voidzero-docs.workers.dev f7ce2ff 2026-10-12T04:22:43.739Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://1d423867-void-cloud.voidzero-docs.workers.dev 2e6689e 2026-10-12T02:57:07.295Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://5229dc69-void-cloud.voidzero-docs.workers.dev 6349ffc 2026-10-11T15:41:07.400Z Visit the dashboard ↗

View all previews: View all previews ↗

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 200c27a050

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/void/src/runtime/remote/d1.ts
Comment thread packages/svelte/src/plugin-islands.ts
@Brooooooklyn
Brooooooklyn force-pushed the fix/high-bugs branch 2 times, most recently from 6349ffc to 2e6689e Compare October 12, 2026 02:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e6689e5ef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/solid/src/plugin-pages.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7ce2ff9d3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/void/src/cli/cf-version-api.ts
@Brooooooklyn
Brooooooklyn requested a review from a team as a code owner October 12, 2026 06:22
@Brooooooklyn
Brooooooklyn removed this pull request from stack #401 October 12, 2026 08:38
Base automatically changed from chore/oss-cleanup to main October 12, 2026 08:50
@Brooooooklyn
Brooooooklyn force-pushed the fix/high-bugs branch 3 times, most recently from 0c32e53 to 26f35ae Compare October 12, 2026 09:34

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26f35aecb4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/void/src/cli/db.ts
Brooooooklyn and others added 7 commits October 12, 2026 17:46
Bug: In a PostgreSQL/MySQL project, `void db connect <url>` (or
`--provider neon`) saves one URL as both `.env` DATABASE_URL and the
linked project's deployment database. A later `void db reset` or
`void db seed` then drops every table in that deployed database with
no prompt. The docs said reset "does not affect the deployed database".
Repro: void db connect postgresql://user:pass@prod-host/app; create a
table there; run `void db reset` or `void db seed`. The table is gone
and no prompt was shown.
Cause: resetDatabase in packages/void/src/cli/db.ts treats whatever
`.env` DATABASE_URL points at as a throwaway local database and runs
DROP TABLE on every table (PostgreSQL public schema, MySQL current
database) with no target check and no confirmation. runConnect writes
the same URL as the deployment URL.
Fix: resetDatabase now calls confirmDropAllTables before the
PostgreSQL/MySQL drops. localhost, 127.0.0.1, ::1 and Unix sockets
skip it: Hyperdrive reaches only public hosts, so these can never be
the deployed database. For PostgreSQL the host comes from pg's own
Client parser, not from the URL authority, because pg lets a `?host=`
query parameter or PGHOST (when the URL has no host) replace it. MySQL
uses the URL host, which is what mysql2 connects to. Any other host gets a prompt
that names the database and host (never the credentials). Without a
TTY the command stops unless `--yes` is passed. reset and seed accept
`--yes`; CLI help and docs/reference/cli/database.md are updated.
`void db connect` and D1 behave as before.
Test: packages/void/test/unit/db-commands.test.ts "PostgreSQL: asks
before dropping every table in a non-local DATABASE_URL" fails without
the fix (reset runs to completion instead of stopping). Parser:
db-args.test.ts "parses --yes for reset and seed". The same test also
runs reset with DATABASE_URL
postgresql://user:secret@localhost/prod?host=db.example.com and expects
it to stop. The pg mock extends the real pg Client, so pg parses the
URL itself.
Note: Scripts or CI that reset/seed a non-local host (for example a
compose service named `postgres`) now need `--yes`. Stopping without a
TTY is on purpose, unlike `db execute --remote`, which skips its prompt
without a TTY. `void db connect` still writes the deployment URL to
.env; changing that is left to the owner.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: On MySQL 8.x, `void db reset` prints "Database reset. Applied N
migrations." but drops nothing. Old rows and `_void_migrations` stay,
so no migration runs again. `void db seed` resets first the same way,
then its seed SQL fails on the old rows (e.g. "Duplicate entry '1' for
key 'users.PRIMARY'").
Repro: with `"database": "mysql"` in void.json and a local MySQL 8
DATABASE_URL, run `void db migrate`, insert a row, run `void db reset`.
The row is still there. The server log shows only
"DROP TABLE IF EXISTS `undefined`".
Cause: resetDatabase in packages/void/src/cli/db.ts runs
`SELECT table_name FROM information_schema.tables ...` and reads
`row.table_name`. Since 8.0, MySQL returns that unaliased column under
the key `TABLE_NAME`, so the read is undefined and every DROP targets a
table named `undefined`.
Fix: alias the column (`SELECT table_name AS table_name`) so the key is
lowercase on 5.7 and 8.x. The code that reads the rows is unchanged.
Test: packages/void/test/unit/db-commands.test.ts, "void db reset >
MySQL: drops every table and re-applies migrations". Its fake mysql2
returns unaliased info-schema columns as `TABLE_NAME`, like MySQL 8.
It fails without the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: `void db seed` with a TS/JS seed (db/seed.ts, .mts, .js, .mjs or
--file) fails with ERR_MODULE_NOT_FOUND when db/schema.ts is a barrel
like `export * from "./schema/posts";`. That is the exact file
`void gen model` writes, and the form the database guide shows. The
command resets the local database before it loads the seed, so the
user loses their data and gets no seed rows. Same for sqlite,
postgresql and mysql.

Repro: `void gen model posts title:string`, add db/seed.ts that
inserts into schema.posts, run `void db seed` under plain Node:
"Cannot find module '.../db/schema/posts' imported from
.../db/schema.ts", and the table is left empty.

Cause: importLocalModule in packages/void/src/cli/db.ts loaded the
seed file and the schema with Node's native import(). Native ESM
needs a file extension on relative specifiers, so the extensionless
re-export does not resolve. drizzle-kit and the Vite plugin accept
that form; only the seed loader did not.

Fix: load these modules with jiti, the same loader (and the same
options) already used for void.config.ts. jiti resolves extensionless
relative imports. moduleCache: false keeps the old fresh-load
behavior of the `?t=` query, and interopDefault: false keeps plain
namespace objects, so the default/`seed` export lookup and the schema
export filter behave as before. gen output and the reset order are
unchanged.

Test: packages/void/test/unit/db-seed.test.ts, "void db seed > seeds
when db/schema.ts re-exports an extensionless module". It runs the
command in a child Node process, because Vitest's module runner
resolves extensionless imports and hides the bug. The child start
is slow on a busy machine, so the test has a 30s timeout and the
child a 20s limit. It fails without the fix with ERR_MODULE_NOT_FOUND.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: `void init` in a pnpm project rewrote the allowBuilds map in
pnpm-workspace.yaml to only Void's four entries. In a pnpm workspace
member it edits the shared root file, so the whole monorepo was hit.
The user's own build approvals were deleted, an explicit
`better-sqlite3: true` (which Void's SQLite error tells users to set)
went back to false, and the legacy onlyBuiltDependencies list was
dropped. pnpm 12 then blocks those builds on the next install. When
allowBuilds was written on one line (`allowBuilds: {...}`), init added
a second `allowBuilds:` key, and every pnpm command then failed with
"duplicate mapping key".

Repro: in an existing Vite app with pnpm-lock.yaml, write
pnpm-workspace.yaml as
  allowBuilds:
    "@parcel/watcher": true
    better-sqlite3: true
  onlyBuiltDependencies:
    - bcrypt
and run `void init`. Only better-sqlite3: false, esbuild, sharp and
workerd are left.

Cause: ensurePnpmAllowBuilds in packages/void/src/cli/init.ts removed
the whole allowBuilds and onlyBuiltDependencies blocks and inserted a
fixed list. Its key regex only matches the block form, so a same-line
value stayed in place and a duplicate key was added.

Fix: removeTopLevelYamlBlock now also returns the lines it removes.
pnpmAllowBuildsYaml keeps the existing entries as written, turns each
legacy onlyBuiltDependencies item into `name: true`, and adds only the
Void defaults that are missing. pnpm's "set this to true or false"
placeholder is replaced only for a name init has a value for. A
same-line allowBuilds value is left untouched, and init warns with the
entries to add. Fresh projects and pnpm's placeholder file still get
exactly the four Void defaults, so the existing tests are unchanged.
The legacy list is also read when its items start at column 0
(`onlyBuiltDependencies:` then `- bcrypt`), the form pnpm's own docs
use. Before, those items were left behind as a stray top-level
sequence, which made the file invalid YAML.

Test: packages/void/test/unit/init.test.ts, "keeps the existing pnpm
build approvals when adding the Void defaults". It covers the indented
and the column-0 legacy lists and fails without the fix.

Note: a value the user already set for a Void entry (for example
better-sqlite3: true) now wins over Void's default, and legacy
onlyBuiltDependencies entries become allowBuilds approvals before that
block is removed. This matches how init merged the list before the
move to allowBuilds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: On Windows, `void init` with a database starter fails with
"Failed to generate the starter database migration. 'C:\Program' is
not recognized as an internal or external command". This happens when
node is installed in the default MSI location
(C:\Program Files\nodejs\node.exe). When the project path has a space,
drizzle-kit gets split arguments instead ("Unrecognized options for
command 'generate'"). `void gen migration <name>` on a project that
has a migration journal fails the same way. When node is under a path
with spaces, such as C:\Program Files, the Yarn PnP check in
`void init` ("does `void` resolve from here?") also wrongly answers
no. Its args hold no paths, so a project path with a space does not
break it. Node also prints a DEP0190 deprecation warning.

Repro: on Windows with node under C:\Program Files, run `void init` in
an empty folder and pick the D1 starter. Or run
`void gen migration backfill` in a project that has
db/migrations/meta/_journal.json.

Cause: three spawnSync(process.execPath, args) calls set
`shell: process.platform === 'win32'`. These are
generateStarterMigration and the PnP probe in isVoidResolvableFrom
(packages/void/src/cli/init.ts), and runMigration
(packages/void/src/cli/gen.ts). With a shell, Node joins the file and
the args with plain spaces and does not quote them. Then it runs
`cmd.exe /d /s /c "<joined>"`, and cmd splits that string on
whitespace again. The option was copied from the package-manager
install spawn, which needs a shell for the pnpm/npm/yarn .cmd shims.
process.execPath is a real .exe and needs no shell.

Fix: drop the `shell` option from those three node spawns, so the args
reach node as a real argv. db.ts and deploy-drift.ts already spawn
node and drizzle-kit this way. The package-manager install spawn keeps
`shell` on Windows, because it still needs it for the .cmd shims.

Test: packages/void/test/unit/init.test.ts "generateStarterMigration >
runs drizzle-kit and the Yarn PnP probe without a shell on Windows",
and packages/void/test/unit/gen.test.ts "gen migration > runs
drizzle-kit without a shell on Windows". Both set process.platform to
win32 and check that the node spawns get no shell. Both fail without
the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: `void deploy --platform cloudflare` fails on every deploy after the
first with "Cloudflare Worker setting limits cannot be uploaded with
pinned secrets." when the Worker already has a remote secret (from
`void secret put`, or the BETTER_AUTH_SECRET an auth app stores on its
first deploy) and the config sets the documented `worker.limits.cpu_ms`,
or passes `placement`, `cache`, `logpush` or `tail_consumers` through
`cloudflare`. Nothing is uploaded and there is no fallback.

Repro: an app with `worker: { limits: { cpu_ms: 30000 } }`; run
`void secret put FOO`; deploy once (works); deploy again (fails).

Cause: once the live Worker has a secret, ordinary revisions upload
through uploadPinnedCloudflareVersion in
packages/void/src/cli/cf-version-api.ts, so the new version can inherit
secrets from the exact active version. That function only accepted a
fixed list of Build Output Worker keys and threw on anything else. The
Build Output carries limits, placement, cache, logpush and
tailConsumers, and the version POST never sent the first three. The
installed `cf` version upload, used before the Worker had secrets,
sends limits, placement and cache. `cf workers versions create` does
not apply logpush either.

Fix: accept those keys. Send `limits` (cpu_ms, subrequests), `placement`
and `cache_options` (enabled, cross_version_cache) in the Workers beta
version body, which documents all three. For placement, `mode: 'off'`
sends nothing, and a hint or `mode: 'smart'` sends smart placement, the
same as cf does for its own version uploads. Region, host or hostname
placement fails before any API request. The error names the key that is
set and says what to do: "Cloudflare placement.region cannot be deployed
while the Worker has secrets. Remove cloudflare.placement from
void.config.ts, or set it to { mode: 'smart' }." Public reports show
that the beta version endpoint can drop the target of a targeted
placement. With `--atomic`, the new version is live at once, so it
could serve traffic without its target before a read-back can find the
problem. `logpush` and `tail_consumers` are Worker-level settings that
version uploads cannot set, so they are accepted but not sent, the same
as `observability` on this path. The change stays inside the pinned
uploader; the cf path and the rest of the deploy flow are untouched.

Test: packages/void/test/unit/cf-version-runner.test.ts, "uploads Worker
limits, placement and cache with pinned secrets". Without the fix it
rejects with "Cloudflare Worker setting tailConsumers cannot be uploaded
with pinned secrets." The same file has "rejects targeted placement with
pinned secrets before any upload", for region, host and hostname. It
checks the full error for each key, hint included, and that the fake
API gets no request. If targeted placement reaches the version POST,
the upload succeeds and the test fails.

Note: no ordinary version upload applies a change to `logpush` or
`tail_consumers`, with or without pinned secrets. Cloudflare documents
both as not available for version uploads. The live Worker keeps its
current values, as it already does for `observability` on this path.
With pinned secrets, this is also true for `--atomic` deploys and for
the first deploy after `void secret put`. Without secrets, those two
paths run `cf deploy`, which applies them with a Worker settings
update. Applying them here would need that separate update.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: `void deploy` to a Void platform, for a Void app or a Class A
framework app whose void.config.ts sets custom names such as
`inference: { bindings: { db: 'MY_DB', kv: 'MY_KV', storage: 'MY_BUCKET' } }`,
asks the platform for the default DB, KV and STORAGE bindings. The built
worker reads MY_DB, MY_KV and MY_BUCKET, so every void/db, void/kv and
void/storage call throws "env: Missing Cloudflare binding 'MY_KV'." and
the route returns 500. Migrations still run, so the deploy looks healthy.

Repro: set `inference.bindings.kv = 'MY_KV'` and `storage = 'MY_BUCKET'`,
run `void deploy`; the finalize manifest carries
`bindings: { kv: ['KV'], r2: ['STORAGE'] }`, while the built wrangler.json
and worker use MY_KV and MY_BUCKET.

Cause: runFullDeploy (packages/void/src/cli/deploy-managed-full.ts) builds
the package options for assembleVoidManifest without `bindingNames`, so
applyResourceBindings in cli/package.ts falls back to DB/KV/STORAGE. The
platform binds each resource under the manifest name. Only the framework
path (runFrameworkDeploy) passed the names.

Fix: pass `analysis.bindingNames` in the package options, the same way
runFrameworkDeploy does. With no custom names, resolveBindingNames returns
undefined and the defaults stay unchanged; it never sets a sandbox name,
so the Sandbox binding is unaffected.

Test: packages/void/test/integration/full-deploy-manifest.test.ts,
"sends custom inference.bindings names in the manifest bindings". It
fails without the fix (got kv: ['KV'], r2: ['STORAGE']).

Note: the platform keys resources by binding name, so an existing managed
project with custom names gets new MY_* resources on its next deploy and
migrations re-run on the new D1 database. The old DB/KV/STORAGE resources
stay in place; the worker could never reach them through void/db, void/kv
or void/storage, so they hold only migration or out-of-band data.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Brooooooklyn and others added 9 commits October 12, 2026 17:46
Bug: In an Astro app that uses voidPlugin() and has no middleware,
`astro dev` / `void dev` writes a temporary src/middleware.ts for the
/__void/scheduled and /__void/queue dev routes and appends
`src/middleware.ts` to the app's .gitignore. When dev stops, the file is
removed but the .gitignore line stays. Any src/middleware.ts the developer
writes later (for example an auth guard) is silently ignored by git:
`git status` does not show it, `git add -A` skips it, and CI or deploys
built from git ship without it.
Repro: Astro app with .gitignore = "node_modules/" and no middleware; run
`astro dev`, press Ctrl-C; .gitignore now ends with `src/middleware.ts`.
Create src/middleware.ts and run `git add -A`: the file is not staged
(`git check-ignore -v` points at the added line).
Cause: astroDevTriggerInjectPlugin in
packages/void/src/plugin-framework-triggers.ts calls ensureGitignoreLines
for a path the user owns, and its cleanup only deletes the file. Nothing
ever removes a .gitignore line. Nuxt and Analog are not affected because
they ignore file names that only Void writes.
Fix: ensureGitignoreLines now returns the lines it actually appended. The
Astro cleanup (dev server close and process exit) removes exactly those
lines with a new removeGitignoreLines helper, then clears the list so the
second cleanup call does nothing. A line that was already in .gitignore
before dev started is treated as the user's and kept. While dev runs the
generated file is still ignored, so it cannot be committed by mistake.
Nuxt and Analog ignore the return value and behave as before. Also drop
the leftover `src/middleware.ts` line from playground/astro/.gitignore,
which an earlier dev session added.
Test: packages/void/test/unit/plugin-framework-triggers.test.ts,
"astroDevTriggerInjectPlugin > stops ignoring src/middleware.ts after the
dev server closes". It fails without the fix: .gitignore still reads
"node_modules/\nsrc/middleware.ts\n" after close.
Note: A hard kill (SIGKILL, crash) skips cleanup, so the line stays, as
the generated file already does. Apps that got the line from an older
Void keep it: Void cannot tell its old line from one the user wrote, so
there is no migration; those developers must delete it by hand.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: With `remote: true` in void.config.ts (or `VOID_REMOTE=1 void dev`),
any route that uses `db` from `void/db`, `createDb()`, or Better Auth
(its adapter wraps `void/db`) fails with "TypeError: d1.withSession is
not a function". Build-time prerender in remote mode hits the same path.
Repro: in an app with D1, run `void connect`, `void project link`, then
`VOID_REMOTE=1 void dev` and request a route that runs
`db.select().from(users)`.
Cause: `createDb()` and `createPrimaryDb()` in runtime/db.ts, and the
generated `void/db` module in plugin-drizzle.ts, now call
`d1.withSession(...)` before handing the binding to Drizzle. In remote
mode, `createRemoteEnv()` swaps env.DB for `ProxyD1Database`
(runtime/remote/d1.ts), which only had prepare/batch/exec/dump.
Fix: add `withSession()` (returns the same proxy) and `getBookmark()`
(returns null) to `ProxyD1Database`. Each proxied statement is its own
HTTP call that the platform runs on the deployed binding without a
session, so there is no replica or bookmark state to keep. Drizzle's D1
session only calls prepare/bind/all/raw/run/batch on the client, which
the proxy already has, so no other file needs to change.
Test: packages/void/test/unit/remote/d1.test.ts, "ProxyD1Database >
createDb() > runs Drizzle queries through the remote binding". It fails
without the fix with "TypeError: d1.withSession is not a function".

Note: session constraints and bookmarks are not forwarded to the
platform in remote mode; `getBookmark()` always returns null. Forwarding
them would need a platform protocol change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: on a MySQL app, a migration made by `void db generate` with more
than one statement fails to apply. This hits `void db migrate` (local
and remote), `void db reset`/`seed`, `void dev` auto-migrate, direct
Cloudflare deploys, and managed deploys through POST /__void/migrate.
MySQL returns ER_PARSE_ERROR near `--> statement-breakpoint`. The
statements before the first breakpoint stay applied (MySQL DDL
commits on its own), the migration is not recorded, and every retry
then fails with "Table ... already exists".
Repro: a schema with two tables gives drizzle-kit output like
  CREATE TABLE `a` (...);
  --> statement-breakpoint
  CREATE TABLE `b` (...);
Run `void db migrate` against MySQL 8: table `a` is created, then the
parse error, and `_void_migrations` stays empty.
Cause: applyMySqlMigrations in migrations/runner-mysql.ts and
runMySqlMigrations in runtime/migration-handler-mysql.ts send the
whole file as one multi-statement query. drizzle-kit puts a
`--> statement-breakpoint` line between statements. PostgreSQL reads
it as a comment. MySQL does not, because MySQL needs whitespace after
`--` for a comment. The D1 path already splits on these lines; the
MySQL paths never did.
Fix: export the existing line-based splitMigrationSql from
runtime/migration-handler.ts (D1 behavior unchanged) and run each
chunk as its own query in both MySQL runners. This splits the same
way drizzle-orm's own migrator does. Files with no breakpoint lines
come back as one chunk, so hand-written multi-statement migrations
still run as one query (the connection keeps multipleStatements).
Test: packages/void/test/unit/runner-mysql.test.ts, "runs each drizzle
statement-breakpoint chunk as its own query". It fails without the
fix (the whole file arrives as one query).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: On a MySQL app with auth enabled, the second auth migration run
fails with "Duplicate key name 'user_email_uidx'". This hits `void dev`
on every restart (printed as "Migration error: ...") and the deployed
POST /__void/migrate endpoint (returns 500). New auth fields and the
varchar-to-text widening of OAuth token columns are never applied.

Repro: Point a MySQL 8 database at an auth app, run the auth migrations
once (tables are created), then run them again, or add a field to the
`user` table and run them again.

Cause: applyMySqlAuthMigrations in
packages/void/src/runtime/auth-migrations.ts reads `row.table_name`,
`row.column_name`, `row.data_type` and `row.index_name` from unaliased
information_schema queries. MySQL 8 labels those columns in uppercase
(TABLE_NAME, ...), so every lookup is undefined and every auth table
looks missing. CREATE TABLE IF NOT EXISTS is a no-op, then CREATE UNIQUE
INDEX (no IF NOT EXISTS in MySQL) throws. Once the lookups work, the
column-diff path emits `ADD COLUMN IF NOT EXISTS`, which MySQL 8 rejects
as a syntax error (that form is MariaDB-only).

Fix: Alias each information_schema column to the lowercase name the code
reads, and emit `ADD COLUMN IF NOT EXISTS` only for PostgreSQL. The MySQL
path adds a column only after information_schema shows it is missing,
so the guard is not needed there; SQLite output is unchanged. The unused
character_maximum_length column is dropped from the rewritten query.

Test: packages/void/test/unit/auth-migrations.test.ts, "re-runs MySQL
auth migrations against MySQL 8 uppercase information_schema labels".
Without the fix it receives CREATE TABLE IF NOT EXISTS plus a duplicate
CREATE UNIQUE INDEX instead of a single ADD COLUMN.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: an HTML request to a React Pages route whose page or layout throws
while rendering (for example reading props.user.name with no user) never
gets a response. In workerd the request hangs until the runtime cancels
it ("Worker's code had hung"), and Node logs an uncaught exception. The
custom 500 page and the dev error overlay never show. The platform admin
UI renders through the same function and hangs the same way.
Repro: a React page component that throws during render; request it
with a browser (not an X-VoidPages fetch).
Cause: renderReactPage in packages/react/src/runtime/pages-server.ts
passed an onError to renderToReadableStream that rethrows every error.
React calls onError from its own scheduled work loop without a guard, so
the throw skips React's shell rejection, escapes as an uncaught
exception, and the renderToReadableStream promise never settles. Even a
non-throwing onError would not reject: the blanket Suspense around the
page turns its errors into a client-render fallback.
Fix: onError no longer throws. It records the first error seen before
the shell is ready; renderReactPage then cancels the React stream and
rejects with that original error, so the Pages protocol tags it and
serves the 500 page or dev overlay as it already expects. Errors after
the shell is sent keep React's client-render fallback and are logged
with console.error (React's default). The deferred-shell cancel reason
is still ignored. Only this function changes; callers are untouched.
Test: packages/react/test/unit/pages-server.test.ts, "rejects with the
error a page throws while rendering"; without the fix it times out
after 5s and reports the error as an uncaught exception.

Note: an error thrown on the first render pass, even inside a user's own
Suspense boundary, now fails the page with a 500 instead of React's
client-render fallback; only errors after the shell is sent fall back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: In a Solid Pages app (voidSolid()), every client-side update rebuilt
the whole page tree. SPA navigation through Link, router.visit or
back/forward replaced the root layout, so layout state was lost. A
useForm submit that came back with validation errors re-created the page
and cleared the inputs the user had typed. A resolving defer() prop
re-created the page and every layout too. The preserveState option on
Link and router.visit had no effect.

Repro: in playground/solid-pages, open /, set a marker on the <nav>
element, click the Users link: the <nav> is a new element. On /users,
type a name only and submit: "Email is required" shows, but the name
input is empty again.

Cause: generateClientEntry in packages/solid/src/plugin-pages.ts built
the page and the layout chain inside the NavigationContext.Provider
children getter, which read the component, props and layouts signals.
Solid wraps provider children in a memo, so any change to those signals
(applyUpdate on every navigation or mutation, onDeferredUpdate on every
deferred value) disposed and rebuilt everything. applyUpdate also
dropped its 4th { preserveState } argument, and the page got a plain
props object, so the rebuild was the only way new props reached it.

Fix: render each layout depth and the page in its own memo slot, keyed
on the layout component at that depth or on a page object. A layout is
re-created only when its component changes. The page is re-created on
GET navigation, when its route or component changes, or when the route
params change. With preserveState on the same route and params
(back/forward, mutations, router.refresh) it stays and receives props
through a reactive mergeProps proxy, which also carries deferred values
in place. The route and params checks matter for back/forward: the
popstate handler always passes preserveState, so without the params
check going back from /users to /users/2 after leaving /users/1 kept the
/users/1 page, and a useForm() there, which reads its URL and defaults
once, still targeted the old record. The page object stores the route
ID (pageData.component) because two page files that re-export one
component resolve to the same function, so comparing components alone
kept the old route's page. The inner slot is still created before its
layout, so hydration keys keep matching the server render. useForm's
saved defaults are now a signal, so hasChanges turns false after a
successful submit now that the page is no longer rebuilt. The playground
deferred page reads its prop inside JSX, as the loaders guide shows for
Solid. The Solid API reference describes when the page is kept.

Test: packages/solid/test/integration/solid-pages-playground.test.ts,
"layout and page state persist across navigation and validation
errors". It fails without the fix (nav marker is undefined). "back/forward
to another record of the same page re-creates the page" loads /users/1,
marks the h1, pushes /users/2 and /users, then goes back. It fails
without the params check (the h1 shows Bob but keeps the marker).
"back/forward to another route that shares the page component re-creates
the page" uses the new playground pages twins/a.tsx and twins/b.tsx,
which both re-export twins/_shared.tsx. It loads /twins/a, marks the h1,
pushes /twins/b and /, then goes back. It fails without the route check
(the path shows /twins/b but the h1 keeps the marker).

Note: Solid now matches the Vue rule (GET navigation re-creates the
page; mutations, router.refresh() and back/forward keep it), except that
a change of route or route params always re-creates the Solid page.
Pages that copy a prop once in the component body no longer see deferred
or preserved updates, and inputs keep their values after a successful
submit, as in React and Vue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: `vp build` / `void deploy` of a Svelte pages app fails when an
`.island.svelte` page, or any layout in its chain (plain `layout.svelte`
included), has a `<style>` block. The build stops with
`[plugin vite-plugin-svelte:compile] .../pages/layout.svelte?island-style&index=0&lang.css ... Expected token }`.
`vp dev` does not use this path, so it looks fine there.

Repro: in playground/svelte-pages add `<style>nav { padding: 8px; }</style>`
to pages/layout.svelte (or a `<style>` block to
pages/blog/index.island.svelte) and run `vp build`.

Cause: generateIslandClientEntry in packages/svelte/src/plugin-islands.ts
imported each raw `<style>` body as `<file>.svelte?island-style&index=N&lang.css`.
That id still matches vite-plugin-svelte's `.svelte(?|#|$)` transform
filter, so the CSS text was compiled as a Svelte component and the
compiler failed on the first `{`. Even without that, the CSS was the raw
unscoped source: it would not match the `svelte-xxxx` classes in the SSR
markup, and `:global(...)` would ship as an invalid selector. When every
page is an island page, this entry is the only CSS source in production.

Fix: the client entry now imports one `<file>.svelte.css?island-style`
per component that has a `<style>` (Svelte allows one top-level style).
The build-only loader reads the component file and compiles it with
vite-plugin-svelte's own API (taken from its `vite-plugin-svelte:config`
plugin): the app's `preprocess` runs first, then `compileSvelte` with the
resolved compiler options. It returns only the compiled CSS, so the scope
hashes match the SSR markup.
The component is compiled outside the module graph on purpose. Island
pages and their layouts render only on the server, so they may import
server-only code (`void/durable`, packages with only `node`/`workerd`
exports). Loading them with `this.load` would resolve those imports in
the client build and fail it. No island page JS enters the client bundle.
The id keeps the component's directory, so CSS url() resolves as before,
and it no longer matches the `.svelte?` filter.
vite-plugin-svelte's own `?svelte&type=style` id is not reused: it only
reads CSS already cached on the module, and its cssScopeTo drops the CSS
when the component is not imported in the client build.
Files the preprocessor reads, such as Sass partials, go to
`this.addWatchFile`, as in vite-plugin-svelte's preprocess plugin, so
`vite build --watch` rebuilds the island CSS when they change.

Test: packages/svelte/test/unit/plugin-islands.test.ts, "bundles the
scoped CSS of island pages and their layouts". The styled island page
and layout both import a package with only `node`/`workerd` exports. It
fails without the fix with the same `Expected token }` errors, and with a
`this.load` based loader with `"." is not exported under the conditions`.
"watches files the style preprocessor reads" checks that a style
preprocessor's `dependencies` are watch files; without the loop, only
the component file is watched.

Note: the style loader uses vite-plugin-svelte's internal plugin API,
which is not in its public types. If `compileSvelte` or `idParser` is
missing there, the build stops and asks for the
@sveltejs/vite-plugin-svelte version that @void/svelte depends on. A
changed call signature (as between 7.2 and 7.3) still fails the build,
with a TypeError. "bundles the scoped CSS of island pages and their
layouts" runs a real build against the locked version, so a lockfile
bump that changes this API fails it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: a root wrangler.jsonc/wrangler.json saved with a UTF-8 BOM (common
for files written by Windows editors) is valid for Wrangler, but Void
treats it as unreadable. `vite dev` / `vite build` stop with "db: cannot
read malformed Wrangler configuration". `void deploy --platform
cloudflare` on an app with a durable-objects/ file treats the file as
missing and overwrites it with only `durable_objects` and `migrations`,
so `name`, `main`, `routes` and every other key are lost. For typed
projects the truncated config is then saved as the lock's resolved
state. The managed deploy live-binding sync reaches the same reader.
Plain `void deploy` (no --platform, no saved destination), which the
deployment guide names as the way to link an existing Worker, stops
with "Set the existing Worker's name in a valid wrangler.jsonc before
linking to Cloudflare." even though the name is set.

Repro: write wrangler.jsonc as U+FEFF + `{ // app, "name": "existing",
"main": ..., "routes": [...] }`, then call syncWranglerDurableObjects()
with one Durable Object module (or readRootWranglerConfig()). The file
ends up as `{ durable_objects, migrations }` (or the reader throws
"Failed to parse JSONC"). With the same BOM file and no saved
destination, maybeLinkCloudflareDeploy() throws instead of offering to
link the Worker.

Cause: two JSONC readers pass the text straight to jsonc-parser: the
private parseJsonc() in packages/void/src/cli/wrangler.ts and the
onboarding check in maybeLinkCloudflareDeploy()
(packages/void/src/cli/cloudflare-link.ts). Node's utf-8 read keeps the
BOM. jsonc-parser still returns the object, but also reports the BOM as
a syntax error at offset 0, and both readers treat any error as a bad
file. readOrCreateProjectWranglerConfig() swallows that error and
returns an empty config with created: true, so its callers write their
keys over the user's file. Void's other JSONC readers
(generated-wrangler-config, migrate) and the comment-preserving writers
in wrangler.ts already strip the BOM.

Fix: strip one leading BOM before parsing in both readers. In
wrangler.ts, parseJsonc() uses the file's existing UTF8_BOM constant,
which fixes every reader in that file at once
(readOrCreateProjectWranglerConfig, readRootWranglerConfig, the Durable
Object, compat and WebSocket readers). In cloudflare-link.ts the
onboarding read uses the same `.replace(/^\uFEFF/, '')` as
generated-wrangler-config; its checks and error messages are unchanged,
and it still never writes the file. No other behaviour changes.

Test:
- packages/void/test/unit/wrangler.test.ts, "keeps existing keys of a
  BOM-prefixed config when adding Durable Objects". Without the fix
  readRootWranglerConfig() throws "Failed to parse JSONC", and the sync
  replaces the file with only durable_objects and migrations.
- packages/void/test/unit/cloudflare-link.test.ts, "offers to link a
  Worker whose config starts with a UTF-8 BOM". Without the fix
  maybeLinkCloudflareDeploy() throws "Set the existing Worker's name in
  a valid wrangler.jsonc"; with it, it returns 'existing' and the file
  bytes, BOM included, are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
Bug: `void db export` in an app with "database": "mysql" fails on
MySQL 8 with "Table 'app.undefined' doesn't exist" (with or without
--output, --no-schema, --no-data). Once the table names are read
correctly, the dump still cannot be restored when a child table's
name sorts before its parent's: a full dump fails with "Failed to
open the referenced table", and a --no-schema dump fails with
"Cannot add or update a child row: a foreign key constraint fails".

Repro: on MySQL 8, create posts(id PK) and comments(post_id FK ->
posts.id) with one row each, then run `void db export --output
dump.sql`. It throws. With the name read fixed, load the dump into an
empty database: comments is created (or filled) before posts and the
foreign key check rejects it.

Cause: runExport in packages/void/src/cli/db.ts lists tables with an
unaliased `SELECT table_name FROM information_schema.tables`. MySQL 8
labels that column `TABLE_NAME`, so `row.table_name` is undefined and
every table becomes "undefined". The loop then writes tables in name
order, SHOW CREATE TABLE keeps foreign keys inline, and the dump never
turns FOREIGN_KEY_CHECKS off, so restore order breaks on parent/child
pairs.

Fix: alias the column (`table_name AS table_name`), the same fix
`void db reset` already uses, and wrap the MySQL dump in
`SET FOREIGN_KEY_CHECKS = 0;` ... `SET FOREIGN_KEY_CHECKS = 1;`. The
wrapper is smaller than sorting tables by dependency and also covers
foreign key cycles. Only the MySQL branch of runExport changes.

Test: packages/void/test/unit/db-commands.test.ts, "void db export >
MySQL: exports every table with foreign key checks off". Without the
fix the export queries `undefined` and has no SET lines.

Note: every MySQL export (full, --table, --no-schema, --no-data) now
starts with FOREIGN_KEY_CHECKS = 0 and ends with = 1, the MySQL
default. A restore into a session that already had the checks off
ends with them on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
@Brooooooklyn
Brooooooklyn merged commit 6117df3 into main Oct 12, 2026
24 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/high-bugs branch October 12, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant