Repository navigation
Conversation
…oidzero-dev#518) Fixes voidzero-dev#509 The email domains guide said Void "preserves foreign MX records and enabled catch-alls". That reads as if you can register an apex whose mail stays with another provider. The platform refuses that case on purpose: ``` void email domain add acme.com apex has MX records (any, on a new registration) → 409 "already has MX records" catch-all routes to something else → refused ``` Void never replaces another provider's mail setup. The guide now says that, in the same terms as the CLI reference (`platform-email.md`) and the own-account section. It points to the two options: an apex with no mail yet, or the platform's shared mail address. Docs only. Outbound-only custom domains, the middle path the reporter mentioned, is a separate feature request. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…v#526) Fixes voidzero-dev#472 ## Problem `db/schema.ts` re-exports a table file from outside `db/`, and that file uses `sql` from `void/db` at the top level: ``` route ──▶ void/db ──▶ db/schema.ts ──▶ src/.../projects.schema.ts ──▶ void/db (still loading) sql = undefined ✗ ``` In dev this throws `__vite_ssr_import_0__.sql is not a function`. voidzero-dev#504 fixed the `db/schema/` folder. Files anywhere else still hit the error. ## Fix Vite SSR runs imports in the order they are written. `void/db` now re-exports the drizzle-orm helpers (`sql`, `eq`, …) **first**, before it imports the schema. This applies to all three dialects (SQLite, PostgreSQL, MySQL). ``` void/db 1. export { sql, eq, ... } from drizzle-orm ← ready now 2. driver, env 3. import * as schema ──▶ projects.schema.ts ──▶ void/db.sql ✓ ``` - The fix works wherever the schema file lives. It needs no import-graph tracking. - `resolveId`, `virtual:void-db-schema` and `db/schema/**` are unchanged. - Build output already linked `sql` directly. It still works, and it ships the same modules in a different order. - Still not supported: reading `db` at the top level of a schema file. It needs a request anyway. ## Tests | Test | main | this PR | |---|---|---| | `plugin-drizzle.test.ts`: helper export comes first (sqlite/postgresql/mysql) | ✗ | ✓ | | `plugin-vite-environment.test.ts`: real dev server, voidzero-dev#472 layout, route imports `db` before `@schema` | ✗ `sql is not a function` | ✓ | Both files pass (103/103). Lint, format and the `void` typecheck are clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ng (voidzero-dev#522) Fixes voidzero-dev#520 In a named action, drop the action key before `c.redirect()` merges the request query. `POST /books?delete&page=2` → `c.redirect('/books')` now goes to `/books?page=2`. Other handlers keep the current behavior. `createPageMutationHandler` marks requests that picked a named action (a `WeakSet`). The `c.redirect` wrapper then removes only the first non-empty query segment (the one that picked the action) and keeps the rest exactly as sent. Also adds one sentence to `reference/api/rewrites.md`. ``` POST /books?delete&delete=x&q=a%20b → c.redirect('/books') → /books?delete=x&q=a%20b └─ action ─┘ └──── kept as sent ────┘ ``` Checked: - `routing-runtime.test.ts`: an `it.each` table (7 rows: valued key, leading `&`, a page param with the same name, encoding kept, single action unchanged). 6 rows fail without the fix. - typecheck for `void`, lint, fmt 22 unit tests fail locally on main too. This PR adds no new failures. --------- Co-authored-by: LongYinan <github@lyn.one> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…writes and the page URL (voidzero-dev#530) Follow-ups to voidzero-dev#522. That PR dropped the action key (`?delete`) only from `c.redirect()` inside the action. This covers the other places it leaked. ## What changes | Case: `POST /books?delete&page=2` | Before | After | |---|---|---| | Auth / user middleware `c.redirect('/login')` | `/login?delete&page=2` | `/login?page=2` | | Action `c.rewrite('/api/x')`, then `c.redirect('/done')` | `/done?delete&page=2` | `/done?page=2` | | `c.redirect('/books-v2', 307)` (or 308) | `/books-v2?page=2` (since voidzero-dev#522) | `/books-v2?delete&page=2` | | `PUT /books?update` + `c.redirect('/books')` (302) | `/books` (since voidzero-dev#522) | `/books?update` | | API route under a catch-all named-action page: `POST /api/x?return=/a` + redirect | `?return` dropped (earlier commit here) | `?return=/a` kept | | Action re-renders the page: `page.url` | `/books?delete&page=2` | `/books?page=2` | | …address bar after `useForm` / `action()` | `/books?delete&page=2` | `/books?page=2` | The key goes only when the browser follows with a GET (303, or 301/302 after a POST). Other redirects send the same method again, and the destination needs the key to pick the action. Redirect and rewrite **rules** (`void.json` routing) are unchanged: they keep the query as sent. On Void Cloud, platform dispatch applies them before the Worker and knows nothing about pages, so every target leaves them alone. ## How ``` compile.ts app.use(createNamedActionMarker(matchedRoutes)) ← after routing rules, before auth and user middleware matchedRoutes comes from the app's own resolved hono/route (Hono keys it by a per-copy Symbol) marks only if the first matched Void route handler (tagged: API, page GET, page mutation) is a named-action page; user middleware of any arity is skipped routing.ts c.redirect: drop the first query segment if marked and the browser follows with a GET c.rewrite: no new query → mark the new request too protocol.ts mutation page.url: drop the key if marked client after a POST that renders: if page.url's query == our URL minus its first segment, write that to history pages/action-key.ts shared WeakSet + string helper (no regex). Rolldown drops the WeakSet from client bundles. ``` Docs: the actions guide now says what happens to the query. The rewrites reference mentions middleware and 307/308. ## Flaky tests (second commit) Each of these failed in a local full run under load: | Test | Cause | Fix | |---|---|---| | `plugin-class-a-wrapper` `… during build` | macOS reports `server.mjs`, written just before startup, as changed (2 of 6 runs in a probe). The reload gives the second import a new `Counter`. | `watch: null` (12 of 12 runs stable) | | `plugin-vite-environment` voidzero-dev#472 test | Real dev server: about 1 s alone, >5 s under load | `watch: null`, lighter schema file, 30 s limit | | `plugin-entry-imports` | Imports the whole plugin entry: about 4.2 s alone | 30 s limit | ## Tests - `routing-runtime.test.ts`: middleware, 307, rewrite, rewrite with its own query, and page URL. 5 rows fail on main. - `pages-client.test.ts`: history URL after a POST, 4 rows. 2 fail on main; the other 2 check that single-action URLs stay unchanged. - `pages-compile.test.ts`: the marker is emitted after `routingMiddleware` and before user middleware, only for `actions`. - `vp run test --unit`: all pass, except one `cf-version-runner` test that timed out under load and passes alone (not touched here). Lint, format and typecheck are clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
) React Pages currently pre-includes both React DOM entry points in browser and Worker dependency optimization. Include `react-dom/client` in the browser and `react-dom/server` in the Worker, with regression coverage for those scopes and the existing SSR includes. An installed-package comparison on a two-page React app reduced generated prebundle JavaScript by **1,580,422 bytes** (source maps and metadata excluded): | Environment | Before | After | | --- | ---: | ---: | | Client | 1,660,390 B | 992,097 B | | Worker | 1,669,081 B | 756,952 B | | SSR | 2,705,925 B | 2,705,925 B | The 80-sample benchmark used 20 randomized baseline/branch pairs for each cold/warm cache state, with fresh dev processes and browser contexts. Every sample passed hydration, interaction, and client-navigation checks. Browser transfer was effectively unchanged, and neither cold nor warm process-to-hydration time showed a statistically significant improvement (adjusted p = 0.751 and 0.800). This supports a smaller generated cache; it does not establish a startup speedup. Timings came from a small React fixture with background host activity. Validation: - Focused React plugin tests: 4 passed. - `vp run test --base origin/main`: 10,750 passed, 31 skipped. - `vp run check`: all 33 tasks passed. - Formatting and lint passed for both changed files. - `vp test run --pool=forks --maxWorkers=4`: 14,048 passed, 31 skipped.
…he request (voidzero-dev#533) Follow-up to voidzero-dev#530 (Codex finding on that PR, which got merged before this fix landed). ## Problem A redirect that sends the same request again keeps the action key (307/308, or 301/302 after PUT/PATCH/DELETE). When the destination has its own query, the merge put the key **after** the destination's params: ``` PUT /books?delete&page=2 → c.redirect('/books-v2?tab=details', 302) → /books-v2?tab=details&delete=&page=2 repeated PUT picks "tab" ✗ ``` ## Fix (only in the `c.redirect` wrapper) | Destination | Result | |---|---| | `/books-v2?tab=details` (another page) | `/books-v2?delete&tab=details&page=2` (key first, as sent) | | `/books-v2?tab=1&delete=x` (sets the key too) | `/books-v2?delete=x&tab=1&page=2` (its value wins, moves first) | | `/books?archive` (same page, own first key) | `/books?archive=&page=2` (that key picks the action) | | `/books-v2?` (bare reset) | `/books-v2?delete` (rest dropped, key kept) | | `/books-v2` (no query) | `/books-v2?delete&page=2` (unchanged, byte for byte) | | `#fragment` | stays last | Unchanged: GET follow-ups (key dropped), unmarked requests, `mergeQueryIntoLocation`, redirect rules and platform dispatch. Known limit: a redirect to a **different** page whose first query key is one of that page's actions (`c.redirect('/other?archive', 307)`) still puts the request's key first. The redirect code can't tell an action key from a normal param on another page. ## Tests `routing-runtime.test.ts`: 10 new rows in the named-action table (307 and PUT 302 with a destination query, a valued key, a fragment, a bare `?`, the same key in the destination, a same-page action key). 9 fail on main. The POST 302 row passes on main too: it checks the GET follow-up stays unchanged. routing-runtime, pages-client, pages-compile and compile tests: 205/205. Lint, format and typecheck are clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-dev#528) ## Issue `createManifestLoader()` caches the client manifest after fetching and parsing it. Concurrent HTML renders on a fresh instance can all miss that cache and load separate copies. This duplicates work during overlapping cold loads. Warm requests already reuse the cached manifest. ## Fix Share one pending promise across concurrent manifest loads. Cache successful results and clear the pending promise when it settles, allowing missing manifests and failed loads to be retried. ## Verification Added unit coverage for concurrent loads sharing one fetch and manifest object, completed-cache reuse, and retries after a missing response or rejected fetch. - `vp test run packages/void/test/unit/routing-runtime.test.ts`: 40 tests passed on Node 24.21.0. Benchmarked eight simultaneous cold SSR requests using real generated manifests, with no artificial asset delay. Median burst completion times across nine shuffled runs on Node 24.21.0: | Generated pages | Before | After | Manifest reads/parses | |---|---:|---:|---:| | 1,000 | 28.52 ms | 21.27 ms | 8 → 1 | | 10,000 | 120.55 ms | 49.43 ms | 8 → 1 | Single-request controls performed one load on both revisions; warm bursts performed zero additional loads. HTML output matched, and browser hydration and navigation checks passed. These results describe concurrent cold bursts. Production frequency and full SSG build impact have not been measured. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ero-dev#541) The lowercase check added in voidzero-dev#513 still accepts names such as `my_app`, `my.app`, `my app`, paths, and names longer than a workers.dev DNS label. The scaffolder writes the supplied name directly into package.json, so these inputs should receive actionable validation before files are created or dependencies installed. Extend the validator already shared by the interactive prompt and positional CLI argument to require lowercase ASCII letters, digits, and dashes, with an alphanumeric first/last character and a maximum of 63 characters. Preserve lowercase diagnostics and the interactive default. Add regression coverage for invalid characters, paths, Unicode/control characters, dash boundaries, single-character names, and the 63/64-character boundary; document the accepted project-name format. Cloudflare references: - [Worker name configuration](https://developers.cloudflare.com/workers/wrangler/configuration/#inheritable-keys): alphanumeric characters and dashes; underscores are explicitly disallowed. - [workers.dev limitations](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/#limitations): at most 63 characters, with no leading/trailing dash. Workers without workers.dev can have names up to 255 characters; the scaffolder intentionally uses the workers.dev-compatible limit. Cloudflare itself permits uppercase; this change preserves Void's existing lowercase policy. This narrows scaffold inputs: paths (including `.`) and scoped names are rejected because the input is also used as the package name. Existing project deployment naming and the separately validated Void platform slug remain unchanged. Validation: five dependency-free Node test groups against the production validator/parser passed, covering valid/invalid input tables, boundaries, defaults, and existing argument forwarding. The repository Vite+ tests, affected-test checks, typecheck, and full suite could not run because `vp` is unavailable and the shell proxy is unreachable, preventing dependency installation. --------- Signed-off-by: Christoph Nakazawa <christoph.pojer@gmail.com> Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
…idzero-dev#521) Fixes voidzero-dev#519 Add `"postinstall": "void prepare"` to the starter scripts. Init installs after writing the starter, so a new scaffold gets the types at that point, and a fresh clone gets them on install. This is the same approach as `nuxt prepare`. `EMPTY_PROJECT_ALLOWED_SCRIPTS` also allows `postinstall: void prepare`, so running init again on a starter still sees it as an empty project. Checked: - `init.test.ts` (`postinstall` in the React + D1 starter, and the deleted-starter check) - a 1.1.12 starter cloned fresh with `postinstall` added: `pnpm install` creates `.void/db.d.ts` and `routes.d.ts`, and `tsc` passes (it fails without it) 16 tests in `init.test.ts` fail locally on main too. This PR adds no new failures.
Adds Vite+ to the "Create an app" setup commands and the "Binary Invocation" section of the CLI docs. Also converts the "Run locally" code on the quickstart page to a code group to match the "Create an app" code group ## Notes The icon is a _tiny_ hack. The string `vite+` renders the `vite` icon since non-alphabetical characters are ignored when determining the icon. I have [a PR with `vscode-icons`](vscode-icons/vscode-icons#4425) to add the Vite+ icon there, which will bubble up into Vitepress via `@iconify-json/simple-icons`. I'm happy to submit a follow-up PR once that's live. ## Demo <img width="3024" height="1718" alt="image" src="https://github.com/user-attachments/assets/42df2704-d3c1-4fee-a702-657cbf06cc38" />
…tate (voidzero-dev#531) Fixes voidzero-dev#506 ## Summary `router.visit(url, { preserveState: true })` updated the page but skipped the history write, so the address bar kept the old URL. `makeCommit()` in `packages/void/src/pages/navigation-engine.ts` gated the history write on `!options.preserveState`; it now gates on `!options._popstate`. - Every normal visit writes history; only back/forward traversal (`_popstate`) skips it. - `preserveState` now only affects component state; `replace` still chooses `replaceState` vs `pushState`. - `router.refresh()` defaults to `replace: true`, so it updates the current entry instead of pushing a new one. voidzero-dev#507 is a separate bug and is not addressed here. ## Tests Added 4 cases to `packages/void/test/unit/pages-client.test.ts`: - `preserveState` + push writes the destination URL with `pushState` - `preserveState` + `replace` writes it with `replaceState` - `refresh()` does not push a new history entry - popstate navigation does not write history The push/replace cases fail on `main` and pass with this change. `vp test run test/unit/pages-client.test.ts` passes (53/53); typecheck, fmt and lint pass. ## Follow-up changes (085e16b) - `router.refresh()` now goes through one shared `refreshPage()` helper, used by both the core router and React's own router. React's copy was missed and would have pushed a new entry. - `refresh()` keeps the URL hash. Replacing the entry with `pathname + search` dropped `#hash`. An internal `_refresh` flag makes sure the page is still fetched and not treated as a hash-only visit. - Same-page hash visits (`visit('#x', { preserveState: true })`) now write history too, by the same rule. The voidzero-dev#130 test that asserted otherwise is updated. - A replaced history entry keeps its scroll key, so repeated `refresh()` calls (e.g. polling) don't add one saved position per call to sessionStorage. - Docs: the `router.refresh()` example notes it keeps the same history entry. --------- Co-authored-by: LongYinan <github@lyn.one> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Void's shipped default Sandbox still uses Node 26.10.0, and the GitHub build-agent image uses the floating `node:26-slim` tag. Upgrade the maintained runtime pins to the fixed versions listed in the [October 2026 Node security release](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases) for CVE-2026-111497. - Set `.node-version` to **24.22.1**, which is already consumed by the CI setup and release workflows. - Upgrade the default Sandbox to **node:26.12.1-trixie-slim** and pin the build agent to **node:26.12.1-slim**. Docker Hub manifest inspection confirmed both fixed tags exist, support amd64/arm64, and resolve to the same Debian Trixie image index; the distro is preserved. - Sync the Sandbox Dockerfile example and build-agent README, including the example's existing SDK helper version with the installed 1.0.2 helper. Package compatibility ranges, dependency versions and the lockfile are unchanged. Platform CI already uses `docker build --pull`; there are no additional static cached Node image references to update. ## Validation - `vp install --frozen-lockfile`: passed, including the core package build. - Focused Sandbox configuration, packaging, RPC/runtime and build-container tests: **5 files / 315 tests passed** under Node 24.22.1. - `vp run test --list`: selected all 731 files because `.node-version` changed; that selection was covered by the full verification below. - `vp run test:all`: formatting, lint, all package/playground builds, artifact checks, platform type-aware lint and `vp run check` passed. - Both updated Dockerfiles built successfully using `--pull --platform linux/amd64`. Inspection containers reported **Node v26.12.1** and **Debian 13 (trixie)**. The Sandbox helper was executable and a harmless Brotli round trip passed. The build agent retained Corepack 0.35.0, Bun 1.3.14, builder uid 1000 and its packaged agent entry; pnpm 11.8.0 and Yarn 1.22.22 ran offline as the unprivileged builder. - `git diff --check`: passed. **The full test suite did not pass:** 728 files passed, 2 failed and 1 skipped; **14,262 tests passed, 2 failed and 31 skipped**. The overall `vp run test:all` exit code was 1. 1. `platform/packages/api/test/integration/admin-dashboard.test.ts:491`: the ResizeObserver sizes array remained empty until `expect.poll` timed out. The entire file passed on a focused rerun. 2. `packages/void/test/integration/astro.test.ts`: the initial SSR fetch lost its loopback connection. On the focused rerun, the dev cases passed but the PostgreSQL suite failed in `beforeAll` at line 297: Astro restarted after dependency optimization/tsconfig reload, and Miniflare/Cloudflare Vite plugin type discovery lost a loopback socket (`Server stopped while waiting`, `other side closed`). The same PostgreSQL startup failure was reproduced with the original **Node 24.21.0**, confirmed with both `node --version` and `vp exec node --version`. The fixed 24.22.1 pin was restored afterward. No application or test-source changes were made to hide these failures. This PR prepares the source fix. Existing published packages and running containers still require their normal release/rebuild/rollout before their runtime exposure is remediated.
## Summary
Pages clients eagerly load a manifest that repeats page IDs and layout
chains. Emit compact rows and share identical layout chains to reduce
this payload.
Initialization expands the rows into the existing component and layout
lookup objects. Static routes before the first dynamic route use a path
Map; remaining routes retain ordered regex matching to preserve
precedence.
## Generated structure
Simplified example with two static pages sharing a layout. Loader names
represent lazy imports; dynamic routes are omitted.
Before:
```js
const components = {
about: loadAbout,
contact: loadContact,
layout: loadLayout,
};
const layoutTree = {
about: ["layout"],
contact: ["layout"],
};
const routes = [
{ pattern: /^\/about$/, id: "about" },
{ pattern: /^\/contact$/, id: "contact" },
];
```
After:
```js
const layoutChains = [["layout"]];
const rows = [
["about", loadAbout, 0],
["contact", loadContact, 0],
];
const components = {};
const layoutTree = {};
const staticRouteIds = new Map();
for (const [id, load, chain, path = "/" + id] of rows) {
components[id] = load;
if (chain >= 0) layoutTree[id] = layoutChains[chain];
if (!staticRouteIds.has(path)) staticRouteIds.set(path, id);
}
components.layout = loadLayout;
```
Component and layout selection continue to use object lookups. Static
route matching uses `Map.get(path)`.
> [!CAUTION]
> Yes this format is less readable (by a bit), but it's definitely more
space and runtime efficient for large number of pages. I'd argue this is
a better change.
## Bundle size
React production builds with generated static pages, a homepage, and one
shared layout. Only the `void` package changes between variants; the
adapter and external dependencies are identical. Sizes measure the
emitted Pages client entry.
| Generated pages | Gzip level 6, before → after | Brotli quality 11,
before → after |
|---:|---:|---:|
| 1 | 68,726 → 68,776 B (+0.07%) | 59,489 → 59,516 B (+0.05%) |
| 100 | 72,725 → 72,021 B (−0.97%) | 61,356 → 61,038 B (−0.52%) |
| 1,000 | 107,695 → 98,216 B (−8.80%) | 76,396 → 72,989 B (−4.46%) |
| 5,000 | 249,580 → 206,988 B (−17.07%) | 142,147 → 124,254 B (−12.59%)
|
The complete JavaScript dependency set required to hydrate page 1 also
shrinks; bytes were not moved into another eagerly required chunk.
## Runtime measurements
At 5,000 generated pages:
| Metric | Before | After | Change |
|---|---:|---:|---:|
| Cold manifest parse + initialization | 15.784 ms | 9.659 ms |
**−38.8%** |
| Module initialization alone | 2.529 ms | 4.510 ms | **+78.3%** |
| Retained heap after initialization | 3.03 MiB | 2.66 MiB | **−12.4%**
|
| Warmed last-page route lookup | 153.734 µs | 0.291 µs | **−99.8%** |
The compact representation adds about **2 ms of initialization work**,
while combined cold parse + initialization improves by **6.12 ms**
(paired 95% interval: 5.12–7.15 ms faster; adjusted p = 0.00012). The
one-page cold case increases **29.3%**, equivalent to **0.019 ms**.
Measurements made using:
- Chrome 155 on Apple M5 with 70% idle CPU host
- 16 randomized pairs per page count
- Cold timing uses an isolated function wrapper; initialization and heap
measurements use real ES modules, with heap sampled after forced garbage
collection.
End-to-end loading and hydration speed remain unmeasured.
> [!NOTE]
> **Why keep objects for components and layoutTree?**
>
> <details><summary>Click to see comparison</summary>
> A separate 16-pair experiment replaced both lookup objects with Maps
while keeping compact rows, layout chains, and route matching identical.
>
>
> | Metric at 5,000 generated pages | Objects | Maps | Change |
> |---|---:|---:|---:|
> | Module initialization | 4.590 ms | 4.364 ms | **−4.9%** |
> | Cold parse + initialization | 10.325 ms | 10.267 ms | **−0.6%** |
> | Retained heap after initialization | 2.66 MiB | 2.69 MiB | **+1.2%**
|
> | Component + layout lookup | 10.83 ns | 9.44 ns | **−12.9%** |
>
> Maps improved initialization by **0.226 ms**, but combined cold timing
showed no statistically significant improvement (p = 0.90). Reads saved
approximately **1.4 ns**, while retained heap increased by **32.5 KiB**.
>
> We retained objects because these gains did not establish an overall
startup improvement and would require changing shared runtime and
adapter interfaces. The static-route Map replaces a linear regex scan,
where the benefit is much larger.
> </details>
## Validation
- 15 focused tests across the core and four framework adapters passed.
- Core and installed fixture typechecks passed.
- Ten production SSR builds passed, including identical repeated build
fingerprints.
- Browser hydration, counter interaction, and client navigation passed.
- All 128 samples in each runtime experiment passed their route and
lookup checks.
- Benchmark tests, checks, formatting, and lint passed.
Measurements compare baseline `6ab6559ccec2f905c693bedeedd5734287946f1b`
with candidate `1b5ba4384f06a5df0d0c0092692d61fe13e5f882` and cover
static React Pages output with a shared layout. Dynamic-route-heavy
applications and other engines may produce different results.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: LongYinan <github@lyn.one>
…oidzero-dev#545) Follow-up to voidzero-dev#534. Fixes page routing bugs that were already on main. ## What was broken | Case | Before | |---|---| | `about.tsx` + `about/index.tsx`, `x.tsx` + `x.vue`, `(a)/team.tsx` + `(b)/team.tsx` | Two files for one route, no warning. The server and the client could each pick a different file (one file's loader fed the other file's component). Route groups changed winner between runs. With two loaders, dev failed with `Identifier '_pload_about' has already been declared`. | | `index.tsx` + `index/index.tsx` | Different routes, same page id `index`, so one route rendered the other page. | | `layout/index.tsx` next to `layout.tsx` | The page had the layout's id, so `/layout` rendered the layout as the page. | | `café.tsx`, `a b.tsx` | Never matched during client navigation: the client compared the encoded `/caf%C3%A9` with the raw pattern. | | A clashing page added while `void dev` runs | The dev server process exited. | ## What changes ``` scanPages same route / same page id → one error, every clash listed, sorted [id] + [slug] (same URLs) → one warning; the first page always wins today framework app (Nuxt …) → no check: users.vue + users/index.vue is Nuxt nesting manifest layout id == page id → that layout is keyed "_layout" (only then) client navigation URL → pathname → decodePagePath() → matchRoute(path) decodePagePath copies Hono's getPath decoding (no regex) void dev scan error → logged once, last good route table kept ``` Example error: ``` pages: Multiple page files resolve to the same route: "/about": "about.tsx", "about/index.tsx" "/team": "(a)/team.tsx", "(b)/team.tsx" Keep one page file per route and rename or remove the others. ``` Notes: - Apps without a clash get the same ids and manifest rows. The generated `matchRoute` now takes a decoded pathname. The engine and `matchAdminRoute` follow that. - The dev guard keeps the last good route table for any scan error, not only page clashes. - No repo playground has a clash: all 17 pages dirs were scanned. ## Tests - `pages-scan.test.ts`: the route clash error (exact text), the page id clash, the warning printed once across rescans, and no false positive for `/users/[id]/edit` next to `/users/[userId]/posts` or for `layout.tsx` + `layout/index.tsx`. - `scan.test.ts`: a Void app with `users.vue` + `users/index.vue` throws, and a Nuxt app does not. - `pages-manifest.test.ts`: the layout folder case. Encoded paths are checked against a real Hono app for 19 URLs (`%C3%A9`, `%c3%a9`, `%20`, `%25`, `%2525`, `%2F`, `%zz`, over-encoded ASCII, query/hash), with and without a root `[slug]`. - `plugin-routing.test.ts`: moving `about.tsx` to `about/index.tsx` in dev logs once, keeps the old table, and recovers on unlink. Nothing is logged when an adapter pages plugin is present. - Each new test fails on main. Unit suites (7,564), the browser tests for all four adapters and the smart runner's affected integration files pass locally. ## Not in this PR (already on main) - `routes/` has the same silent first-file-wins clash. `.dev`/`.prod` pairs are intended there, so it needs its own rule. - `a-b.tsx` and `a_b.tsx`, each with a `.server.ts`, both become `_pload_a_b` (dev SyntaxError). - ISR page TTLs compare the encoded pathname with the raw pattern (`@void/edge` `matchSegmentPattern`). - An unnamed catch-all `[...].tsx` matches the bare `/files` on the server but not on the client. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d Worker conditions (voidzero-dev#540) Fixes voidzero-dev#539 `prepareBundledBuildOutput` bundles framework Worker output again with a plain Vite lib build. It now matches `@cloudflare/vite-plugin` in two ways: - **Additional modules.** A plugin applies the Cloudflare Vite plugin's module rules: `.wasm` and `.wasm?module`, `.bin`, `.txt`, `.html` and `.sql`, including `#` subpath imports that resolve to them. Matching imports stay external, and each file is emitted beside the bundle with a path relative to the chunk that imports it. `copyBundle` already uploads these files as `wasm`, `data` and `text` modules. The Cloudflare plugin does not export its rules, so the plugin repeats them. - **Resolve conditions.** The build uses `WORKER_CONDITIONS`, which Void's other Worker bundles already use. The constant moves from `wrapper.ts` to its own module so that `cf-build-output.ts` still loads Vite lazily. Both changes apply to every bundle that `prepareBundledBuildOutput` builds, including the static Worker. ## Tests `packages/void/test/unit/cf-build-output.test.ts`: - A framework Worker with wasm-bindgen-style and plain Wasm, text and data imports, a `#` subpath import and a dynamic import produces `wasm`, `text` and `data` modules and passes the `cf` dry run. - A dependency with `workerd` and `browser` exports resolves to its `workerd` build. Each test fails when its change is removed. `vp run test`, `vp run --filter ./packages/void typecheck`, `vp lint` and `vp fmt --check` pass. I also prepared the output of a SvelteKit app that uses `@resvg/resvg-wasm` and micromark: `index_bg.wasm` is uploaded as a `wasm` module, and the bundle contains the Worker build of `decode-named-character-reference`. ## Follow-up changes (21e826b, 1cb2d9f) ``` import './a.txt' ─┐ ┌─> a.txt (text) import './a.bin' ─┤ same bytes, before: ──> │ one file, one type ✗ │ same bytes, now: ──> └─> a.txt (text) + a.bin (data) ✓ import './sa"me.bin' before: ENOENT ✗ now: sa_me.bin ✓ esm-env DEV before: not folded now: folded to false ✓ ``` - Each module file is emitted once per source path, with its own name and extension. Files with the same bytes are no longer merged, so each import keeps its module type. - The external id holds a counter key instead of the path, and emitted names keep only letters, digits, `.`, `_` and `-`. A quote in a path no longer breaks the import. - The build adds the `production` condition, like `@cloudflare/vite-plugin` does for production builds. It is fixed rather than taken from `NODE_ENV`, because a deploy bundle is always a production build. Without it, the SvelteKit playground bundle lost the production build of `esm-env`. - The reference regex is replaced by a small `indexOf` loop. Each build uses a random marker, so app strings that look like a module id stay as they are. Tests: the Wasm test adds same-bytes `.txt`/`.bin` files, a quoted file name (`'` on Windows) and an app string that looks like the old marker; the conditions test adds a package with `development`/`production` exports. Left out, since they behave the same before this PR (and in `@cloudflare/vite-plugin` for the second): - Packages without `exports` still prefer the `browser` main field. - Wasm reached through an export subpath without a `.wasm` suffix is not kept as a module. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: LongYinan <github@lyn.one> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…voidzero-dev#548) Fixes voidzero-dev#507 ## Summary An older visit whose page update was still running could commit after a newer visit had already committed. It then wrote its URL, head and scroll over the newer page, and disposed the newer page's Deferred props. ``` before: visit('/a') ── applyUpdate(A) ……… slow ……… commit(A) ✗ URL /a, title A, B's Deferred rejected visit('/b') ── applyUpdate(B) ── commit(B) page /b after: commit(A) sees that a newer visit already set the URL → skipped, commit() returns false ``` ### Rule: the address bar only moves forward `makeCommit()` in `packages/void/src/pages/navigation-engine.ts` tracks the newest visit whose URL is in the address bar: | sets it | why | | --- | --- | | a successful commit | its URL was just written | | a back/forward visit, when it starts | the browser has already moved | | ~~a same-page hash visit~~ | it renders nothing, so a page already on screen still commits its URL | An older visit commits only if no newer visit has set the URL. `resource.commit()` now resolves `true`/`false`. ### Cases | case | result | | --- | --- | | A finishes after B committed (the issue) | A skipped; page, URL, head, scroll and B's Deferred props stay on B | | A finishes while B is still loading | A commits `/a` (its page is on screen), then B commits `/b` | | the new page's mount code calls `visit('/b', { replace: true })` | A pushes `/a`, B replaces it; the previous entry is kept | | Back pressed while A is being applied | A never pushes, so forward history is kept | | a hash link (or Back to a same-page hash entry) during A's update | A still commits `/a`, as before, so page and URL match | I first tried "skip A if any newer visit started" (what Inertia and SvelteKit do). Review showed it breaks redirects made from a page's mount code: the redirect's `replace` then overwrote the previous page's entry. So the rule only looks at visits that have already set the URL. ### Callers - **`client.ts`** (Vue, Svelte, Solid): a skipped visit throws an AbortError inside `applyNavigationResource`. The existing catch settles it like any abandoned visit: GET resolves `undefined`, other methods reject with AbortError (`useForm` already ignores that). A view-transition callback that runs after a newer visit started now skips `applyUpdate`, so the older page cannot paint over the newer one. - **React** (`pages-client.ts`): `finishNavigation` uses the result. A skipped visit settles the same way and does not dispatch `finish`. No docs change: nothing changes for users except the bug. ## Tests New `describe('superseded navigation commits (voidzero-dev#507)')` in `packages/void/test/unit/pages-client.test.ts` (10 cases): - normal visits in order (control) - the issue's repro: URL, `router.url`, title, scroll and B's Deferred prop stay on B - A finishing before B commits - a redirect started from the new page's mount - a hash visit during A's update, and Back to a same-page hash entry during it - Back to another page during A's update - a superseded form submission rejects with AbortError - a late view-transition callback - the engine called the way the React adapter calls it Each guard has a test that fails when the guard is removed. 5 of them fail on `main`. `vp test run` passed except one platform browser test that timed out at 5s under full load. That test runs with JavaScript turned off, so this change doesn't reach it; it passes alone and in its project run twice. `vp run test`, `vp run check`, `vp lint` and `vp fmt` pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dzero-dev#551) Fixes voidzero-dev#510 ## What it does An admin can now register an apex whose mail lives with Microsoft 365 or Google Workspace, for **sending only**: ```sh void platform email domain-add brand.example --project hr-portal --outbound-only ``` ``` managed (today) outbound-only (new) ─────────────── ─────────────────── apex MX → Cloudflare Email Routing untouched (M365 / Workspace) catch-all → Void ingress Worker untouched ingress Worker deployed never deployed steps routing_enable … publish sending_onboard → publish receive mail yes no (inbound_status = disabled) send mail yes yes Sending refused → verified_only fallback → blocked, with the reason ``` ## Answers to the 4 questions 1. **Shape.** A column, `email_domain_routes.inbound_mode` (`managed` | `none`, default `managed`), plus a check that `none` is only allowed with `addressing = 'custom'`. `addressing` already plays the "route kind" role, so a second kind would duplicate it. 2. **Who.** Admin only. Owners get a 403 from the service. The owner CLI points them at the admin command when their apex already has foreign MX. 3. **DMARC/SPF.** They live together; Void never edits DMARC or SPF. Before onboarding, the step calls Cloudflare's read-only `POST /email/sending/subdomains/preview`. If it reports any real conflict (not just `*.missing`), the operation is **blocked** with a list (`dmarc.multiple at _dmarc.brand.example; …`). Once the admin fixes those records, `domain-sync` resumes. A domain that is already onboarded skips the preview. 4. **Subdomains.** Not in this PR; apex only. Subdomain senders need several routes per connection, so that is a follow-up. ``` domain-add --outbound-only │ ├─ admin? apex? not the shared zone? ── no ──▶ 4xx, no Cloudflare writes │ ▼ sending_onboard ├─ already onboarded? ─ yes ─▶ onboard (idempotent) ├─ preview conflicts? ─ yes ─▶ blocked + checklist ├─ onboard 4xx ─────────▶ blocked ("needs Workers Paid") ├─ onboard 5xx/408 ─────────▶ unknown (operator recovery, as today) ▼ publish ─▶ outbound ready, inbound disabled ``` ## Safety details - **Removal** keeps the usual step list, but `catchall_detach` and `ingress_delete` finish locally for outbound-only routes, so no Email Routing or Workers calls are made. The stored credential is still cleared, so a Sending-only token can finish removal. - **Mode switch** on an active route returns 409 ("remove it first"). The add dry-run says so (`plan: 'refuse'`). - **Race guard.** If the MX preflight was skipped because the route was active, and the route is no longer active by the time the transaction runs, the add returns 409 "retry" instead of reactivating it without an MX check. - **Status refresh** no longer overwrites a newer Sending result recorded by a setup step. Otherwise, on an outbound-only route, a stale `blocked` would stop all sending. - **Older servers.** The CLI aborts unless the dry-run echoes `target.inboundMode === 'none'`, so an old platform that ignores the flag cannot run full managed setup. - **Rollback.** `compatibility.json` lists 1.1.11 through 1.2.1 as rollback-safe (the column is additive and defaults to `managed`). `platform/meta/operations.md` warns not to run `domain-add` or `domain-sync` on outbound-only domains after a rollback below this release, because an older runtime would treat them as managed and take over the MX. - `domain-rotate-secret` returns 409 for these domains, in both the preview and the real request. ## Not verified live - That Cloudflare onboarding keeps a single existing `_dmarc` record instead of replacing it. The preview is Cloudflare's own check, but this still needs one run against a real test zone. - Removal still does not offboard Email Sending in Cloudflare. That was already true for managed domains and is unchanged here. ## Tests - `admin-upgrade-compatibility.test.ts`: 0061 upgrade from the 0060 schema, defaults, and the check constraint. - `email-connections.test.ts`: foreign-MX apex runs 2 steps with no inbound calls; preview conflict blocks, then resumes; 403 blocks (not `verified_only`), then resumes; managed still falls back; 504 stays unknown; refusals; removal makes no inbound calls. - `operator-email-domains.test.ts`: HTTP flag validation, dry-run text, blocked → sync → ready, status, previews, rotate refused, audit, remove; status-refresh race (confirmed to fail without the fix). - `email-admission.test.ts`: sending admitted, receipt refused. - `operator-cli.test.ts`, `email-cli-domain.test.ts`: flag parsing, old-server guard, owner hint. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…p the Worker for hashed chunks (voidzero-dev#552) Signed-in users never cached JS or CSS, and every static file request started the Worker. A Codiff app on Void ran into this and worked around it with a post-build script. This PR fixes it in Void. ## What was wrong ``` Signed-in GET /assets/<hash>.js (pages / SSR / SPA with edge rules) auth middleware ─► next() ─► file served: "public, max-age=31536000, immutable" ◄── appendAuthHeaders copies getSession's "no-store" + "pragma: no-cache" over it result: no-store (also for anonymous requests that carry any cookie, e.g. _ga=1) ``` The Worker also ran for every file, and for signed-in users each file cost one session read (D1). ## Changes **1. Static files keep their own cache policy** (all targets, `runtime/`) | request | before | after | |---|---|---| | no cookie | `public, …, immutable` | same | | any cookie / signed in | `no-store` | `private, max-age=31536000, immutable` | | session cookie refreshed on this request | `no-store` | `private, no-store` | | page / API response for a signed-in user | `no-store` | same | - The fallback marks responses that come from `ASSETS` (`static-asset-marker.ts`). - The auth middleware treats a response as a file only if user middleware left its Cache-Control, Set-Cookie and **body stream** unchanged. If middleware swapped the body after `next()`, the response is handled like a route again (`no-store`). - `private`, not `public`: user middleware may gate files on the session, so a shared cache must never keep one. **2. Hashed chunks skip the Worker** (native `void deploy --platform cloudflare` only) ``` eligible build? ── no ──► output unchanged │ yes ├─ dist/ssr/wrangler.json run_worker_first: ['/**', '!/assets/a-123.js', '!/assets/b-456.js', …] └─ dist/client/_headers one immutable rule per hashed chunk (Void marker on line 1) ``` - Eligible means: no global middleware; no header, redirect, rewrite or fallback rules; no public `_headers`/`_redirects`; no route, page, WebSocket, prerender or revalidate pattern that can reach the assets folder; base `/`; 1–99 hashed chunks; not a managed (dispatch) build. - Rules are per file, not `/assets/*`. Misses, client routes like `/assets/42`, and custom 404 pages still reach the Worker. - The version-API upload sends the generated `_headers` as a `text/plain` module, as cf does. User-written `_headers` handling is unchanged. - Watch rebuilds keep `_headers` and `run_worker_first` in step. A stale generated `_headers` is removed. - Workers Free supports all of this (static requests are free). ## Not in this PR - An opt-in that would let apps with middleware also skip the Worker (new API, and a security call for gated apps) - Turning on better-auth `cookieCache` by default (changes how quickly a revoked session stops working) - Carrying a D1 read-replica bookmark across requests - Managed (dispatch) bypass ## Verification - New tests: `asset-worker-bypass`, `client-asset-manifest`, `plugin-routing` bypass wiring, `cf-version-runner` `_headers` module, `auth-middleware` (incl. swapped body), `auth-isr-cache-policy` with real better-auth against each fallback (incl. 304), `routing-runtime` marker, Miniflare bypass test (Worker does not run), `ssr-playground` build output. - `vp run check`: 33/33 - Full `vp test run`: 732 files, 14403 tests passed, 0 failed - Repro wiring the real runtime: signed-in / cookie requests now get `private, …` and never `no-store` on files 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o-dev#553) Fixes voidzero-dev#511 ## What this adds A project owner can give the project one short, permanent name on the platform's shared mail domain: ```sh void email alias acme # asks to confirm; --yes skips the prompt void email alias # shows the address and inbound status ``` ``` before after (slug acme-store, alias acme) ────── ───── acme-store@mail.example.com acme-store@mail.example.com ← still works acme-store+support@… acme-store+support@… ← still works acme@mail.example.com ← new acme+support@… ← new ``` Both names send and receive. Handlers route the same way either way: `acme+support@…` and `acme-store+support@…` both reach `email/support.ts`. ## Answers to the three questions ### 1. Alias field, or another way? A field: `projects.email_alias` (nullable, migration 0062). ``` projects slug acme-store (unique, as before) email_alias acme (unique when set) one namespace: a trigger refuses alias = any slug slug = any alias alias = own slug ``` - The DB trigger (`void_project_mailbox_name_guard`, SQLSTATE 23505) keeps slugs and aliases apart in both directions, including on older runtimes after a rollback. - The alias follows the slug rules: shape, 56 characters, reserved names, no `preview-`. Names of five characters or fewer need the same plan permission as short slugs. - Only the owner can set it (`PUT /projects/:id/email/alias`). ### 2. Permanent, or changeable? Permanent. Set once, never changed or removed, and freed only when the project is deleted. - Reply addresses people have saved (`acme+ticket-42@…`) keep working. - No swap logic, so no window where mail bounces. - Because it is permanent, the route checks Cloudflare **before** saving: it refuses a name that an existing rule already routes (including plus addresses), and refuses when the domain has no room for the rules this project will need. ### 3. Does the old slug keep working? Yes, for sending and receiving. An alias project uses two Cloudflare literal rules: ``` Cloudflare Email Routing (shared domain) mail to acme-store+x@ ─▶ rule: acme-store@ ─┐ mail to acme+x@ ─▶ rule: acme@ ─┴─▶ email gateway ─▶ resolve-ingress │ finds project by slug OR alias │ checks readiness of THAT name ▼ proxy router ─▶ project worker (prefix must equal the name used) ``` - Default `from` stays `<slug>+noreply@…`. Apps that want the alias as sender set `from`. - Admission accepts `From` with either name. Replies and forwards can use either name, whatever address the mail came in on. - Deploys only wait for the slug rule. A failing alias rule shows in `void email alias` but never blocks a deploy. ## Rollback ``` upgrade ──▶ alias rules created rollback ─▶ older runtime ignores email_alias; its reconcile removes alias rules; slug mail keeps working upgrade ──▶ next reconcile restores the alias rows and rules (including a removal that stopped part way) ``` - `compatibility.json` needs no change: no release contains 0061, and the existing 1.1.11–1.2.1 entries now cover 0062 (covered by a test). - Known rollback gap, written down in `platform/meta/operations.md`: on an older runtime, creating a project whose slug equals an existing alias returns 500 (the trigger refuses it), not `slug already taken`. ## Rule limit Each receiving alias project uses one more rule out of Cloudflare's default 200 per domain. The admin docs say so. ## Tests - Migration: upgrade from the 1.1.11–1.2.1 schema, trigger refusals, older-runtime inserts. - Shared recipients: second rule, removed / half-removed row restore, per-name readiness, cron pickup, room and conflict checks, deploy not blocked by the alias. - Route: owner only, validation, plan check, taken, Cloudflare conflict, settings. - Ingress, admission, proxy router (mailbox compare, reply pins), gateway validation, generated `handleEmail` prefix strip. - CLI: args, help, the command. Local: typecheck, lint, fmt, `vp run test` (affected) pass. Browser tests can't run on this machine (TLS proxy blocks the Playwright download). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ero-dev#543) ## Issue Saving a page currently re-reads and analyzes unchanged source files, then regenerates routing output even when only the page body changed. This adds unnecessary work as the app grows. ## Fix This change caches file-derived analysis, invalidates edited files through the watcher, and skips route-entry regeneration and route-module invalidation when routing inputs remain unchanged. Recent writes are conservatively re-analyzed to preserve freshness. Layout inventories are deterministic, and returned Markdown metadata cannot mutate the cached analysis. Startup and file changes serialize regeneration so the generated files and successful-input checkpoint stay consistent. Watcher failures are logged and retried on a later save. Auth provider/dialect settings participate in change detection, and missing generated routing files are repaired on the next source save. The implementation is shared across React, Vue, Svelte, and Solid Pages. **The benchmarks below measure React only.** ## Results Measured from file write to the updated heading appearing in Chrome, using Cloudflare dev with local workerd on an Apple M5: | Generated pages | Update | Before | After | Time saved | Change | |---:|---|---:|---:|---:|---:| | 1 | Component HMR | 97.0 ms | 96.4 ms | 0.6 ms | −0.7% | | 1 | Server-page reload | 132.9 ms | 142.9 ms | −10.0 ms | +7.5% | | 100 | Component HMR | 95.2 ms | 96.8 ms | −1.6 ms | +1.7% | | 100 | Server-page reload | 119.5 ms | 141.6 ms | −22.2 ms | +18.5% | | 1,000 | Component HMR | 350.1 ms | 98.6 ms | **251.5 ms** | **−71.8%** | | 1,000 | Server-page reload | 329.8 ms | 167.2 ms | **162.6 ms** | **−49.3%** | Each fixture includes two additional fixed pages. Results average session medians across ten paired sessions per size, with 360 timed saves overall and seeded, sequential variant ordering after warmup. Both 1,000-page improvements are statistically significant after Holm correction across six comparisons (`p = 0.0117`). The 95% savings intervals are **235–268 ms** for HMR and **123–197 ms** for reloads. Smaller fixtures show no statistically clear improvement or regression. Validation: - Regression coverage includes overlapping startup/saves; retry after write, auth-generation, and invalidation failures; changed auth settings; deleted outputs; stable layout ordering; and metadata isolation. Body-only saves return to the fast path after recovery. - Latest-base verification (`36a3697f`): core build, 465 routing/cache/navigation and email unit/integration tests, and all 33 workspace typecheck tasks passed. The targeted run skipped 13 tests. - With preceding base `cd480dde`: affected suite passed 10,936 tests; full suite passed 14,314 tests, with 31 skipped. - Formatting and lint passed. - Real Cloudflare dev with Chrome: loader and middleware body edits ran fresh code without restarting the isolate. A forced generated-file write failure was logged without terminating the server; the next save regenerated the POST route and its types. A body-only save also repaired deleted route types. No browser runtime errors. - Node production Pages coverage is required. Optional Bun/Deno availability probes retain the 10-second timeout. The performance figures above are from the original benchmark run; this follow-up was verified for correctness. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: LongYinan <github@lyn.one>
…o-dev#556) ## Issue OAuth upload validation currently scans every approved asset for each requested asset while holding the project lock. At the allowed 5,000-asset batch size, this can require millions of comparisons. ## Fix This reduces validation from O(approved × requested) to expected O(approved + requested). Build a `Set` of approved `(blake3, md5, size)` tuples, using a local `assetApprovalKey()` helper with `JSON.stringify` for both indexing and lookup. Project matching and exact matching of all three fields are preserved. ## Benchmark results 12 paired runs on Apple M2 Pro, Node v24.21.0. Validation timings include Set construction and JSON encoding; database, presigning, and network I/O are excluded. | Approved assets | Requested assets | Before | After | % reduction | | --- | --- | --- | --- | --- | | 100 | 100 | 0.084 ms | 0.051 ms | 39.1% | | 5,000 | 100 | 4.721 ms | 2.082 ms | 55.9% | | 5,000 | 1,000 | 40.969 ms | 2.317 ms | 94.3% | | 5,000 | 5,000 | **227.016 ms** | **3.340 ms** | **98.5%** | | 20,000 | 5,000 | 808.418 ms | 10.352 ms | 98.7% | The 5,000/5,000 case is about **68× faster**. ## Why the tests changed Expanded coverage verifies multiple approved assets, reordered batches, duplicates, mismatched fields, mixed valid/invalid batches, and strict field types (`"3"` versus `3`). **All 19 tests pass against both the original and optimized implementations.** ## Verification Typechecking passed; 4,447 affected tests and 14,496 full-suite tests passed.
## Issue Trunk CI is broken now. Staging smoke tests still require `no-store` for Access-protected hashed assets, but voidzero-dev#552 intentionally preserves `private, max-age=31536000, immutable`. Require `private` and `immutable` for protected assets, continue rejecting conflicting cache directives, and align the manual smoke-test instructions. ## Fix Fixes the asset-cache assertion in [the staging smoke run](https://github.com/voidzero-dev/void/actions/runs/39804112659/job/119918024323). ## Verification Validation: all 89 existing focused auth/cache/smoke tests passed, all 33 workspace typecheck tasks passed, and formatting and lint checks passed. Unfortunately the failing workflow doesn't run on PRs so this can't be verified on CI until merge.
) GitHub check logs accumulated horizontal indentation after every Unix newline, turning dependency lists and deploy output into a staircase ([reported check](https://github.com/voidzero-dev/voidzero.dev/runs/120314852404)). Build children write to pipes, so their LF delimiters need carriage-return/newline semantics when replayed into the check renderer. Render captured LF delimiters as CRLF and keep plain LF-delimited logs on the text fast path. Preserve raw captured delimiters, intentional indentation, ANSI parser state, and bare-CR progress rewrites. Regressions exercise the real build-agent capture → Markdown path with LF/CRLF, split chunks, colors, progress updates, and deployment boxes, plus a 100-line plain tail exceeding the terminal input budget. Validation: - Reproduced the staircase with failing regression tests before the fix. - 197 focused renderer/capture/oracle tests passed. - `vp run check`: all 33 workspace tasks passed; changed-file formatting and lint passed. - `vp run test --list` and `vp run test`: 4,425 tests passed. Socket/browser tests were blocked by sandbox permissions; one unrelated Zero Trust timeout passed when rerun alone. - `vp test run --bail 1`: full-suite selection attempted once, stopped on Chromium launch/socket permission failures. `vp run build:all` also encountered sandbox-denied loopback sockets during prerendering.
…dzero-dev#582) ## Summary Google sign-in through configured login methods fails at the token exchange. `oauth4webapi`'s `ClientSecretBasic` form-encodes the client ID and secret before Base64 (RFC 6749 §2.3.1), turning every `-`, `.` and `_` into `%2D`, `%2E`, `%5F`. Google compares the raw values without decoding, so `761060634857-….apps.googleusercontent.com` / `GOCSPX-…` never match. Google advertises both methods, and Basic was picked first. Prefer the form body, which providers always decode: ```diff exchange() methods = token_endpoint_auth_methods_supported ?? ['client_secret_basic'] - if methods has client_secret_basic → ClientSecretBasic - elif methods has client_secret_post → ClientSecretPost + if methods has client_secret_post → ClientSecretPost + elif methods has client_secret_basic → ClientSecretBasic else → reject ``` Basic-only providers keep the existing behavior. The legacy `auth/google.ts` path already posts credentials in the body and is unaffected. ## Evidence New unit test: Google connection, client ID `761060634857-abc_def.apps.googleusercontent.com`, discovery listing `['client_secret_post', 'client_secret_basic']`. Expects the raw `client_id` in the token request body and no `Authorization` header. - **Before:** `platform-oidc.test.ts`: 1 failed | 24 passed - **After:** `platform-oidc.test.ts`: 25 passed `platform-login` and `platform-bootstrap` integration tests: 42 passed. The 2 browser cases didn't run locally because Playwright's Chromium isn't installed. `@voidcloud/api` typecheck passes. ## Merge Danger **Door:** two-way **Blast Radius:** OIDC login Providers that advertise both methods now receive `client_secret_post` instead of Basic. Both are standard and advertised by the provider itself.
Fixes voidzero-dev#577 ## Summary `logger.error('save failed', { error })` logged `"error":{}`. `name`, `message`, `stack` and `cause` are non-enumerable on `Error`, so `JSON.stringify` drops them. `format()` in `packages/void/src/runtime/log.ts` now passes a replacer to `JSON.stringify`. The replacer writes each `Error` as its own enumerable fields plus `name`, `message`, `stack` and `cause`. This covers nested errors and `cause` chains. The `{ msg, ...fields }` shape is unchanged. An error that appears a second time (for example `error.cause = error`) is written as `{ name, message }`, so serialization terminates and the other fields are kept. An error with its own `toJSON` still uses it, because `JSON.stringify` calls `toJSON` before the replacer. ## Tests New cases in `packages/void/test/unit/runtime-log.test.ts`: - `Error` fields keep `name`, `message`, `stack` and `cause`. - `Error` subclasses keep their own enumerable fields (`status`). - A cyclic `cause` does not drop the other fields. `vp test run packages/void/test/unit/runtime-log.test.ts`: 3 failed, 12 passed without the fix; 15 passed with it. `vp lint` and `vp fmt --check` are clean on the changed files. ## Docs `docs/reference/api/resources.md` notes that you can pass a caught `Error` in `fields`. --------- Signed-off-by: Christoph Nakazawa <xnakazawa@pm.me> Co-authored-by: Christoph Nakazawa <xnakazawa@pm.me>
cursor
Bot
force-pushed
the
cursor/feat-useform-seterror-dbee
branch
from
October 26, 2026 03:10
97b07ae to
ed7548d
Compare
…ero-dev#554) Fixes voidzero-dev#550 ## Problem `loadVoidAuthConfig()` used a native `import()`. Node keeps the first evaluation of a file, and of every file it imports, until the process exits. So while the dev server ran, the generated auth schema kept using the first `auth.ts` it saw. ``` save auth.ts ──► watcher ──► import(auth.ts) ──► cached copy ✗ save helper ──► (not watched) ✗ fix syntax ──► import(auth.ts) ──► old copy ✗ ``` ## Fix In dev, Void declares a small **Node-only Vite environment**, `void_auth_config` (only for `serve`; no HMR client), and loads `auth.ts` through its module runner: ``` void_auth_config (Vite env, dev only) save a file ──► in its module graph? ──yes──► mark stale ──► regenerate │ next load: stale? ──yes──► moduleGraph.invalidateAll() │ │ ▼ └──────────────► runner.import(auth.ts) ──► .void/better-auth-schema.ts ``` One flag (`authConfigStale`) drives it. It's set by a save of a file in the graph, or by a failed load. - **Dependencies come from Vite's module graph.** Void keeps no list of its own. - **Packages** stay external, so they load once. - **Builds and CLI commands** (`void prepare`, `db generate`, `void build`) keep the plain `import()` from `main`. They run once per process, so caching doesn't matter there. - **Start-up auth migrations** load through the same runner. A restarted dev server (`r`, or Void's own restart) creates the new tables and columns. ### Edge cases | Case | Behavior | |---|---| | `auth.ts` edit / local helper edit | schema regenerates ✓ | | syntax error in a helper, then fix | error logged, then regenerates ✓ | | import of a file that doesn't exist yet, then created | the new file triggers a clean load ✓ | | file saved while a load is running | loads run in order, so the next load picks it up ✓ | | other saves while the config is broken | error logged; the regeneration waits for the fix, as `main` does when the auth config fails to load | | route or unrelated save | config not reloaded ✓ | Vite's own update pass skips this environment (`hotUpdate` returns `[]`), so saves don't log a `page reload` that nothing receives. ### Same pattern as Vite and React Router ``` own env, consumer 'server' runner.import invalidateAll on change Vite runnerImport() ✓ (inline) ✓ n/a (one-shot) React Router routes ✓ (__config_loader) ✓ ✓ this PR ✓ (void_auth_config) ✓ ✓ ``` All APIs used are public exports of `vite` 8 (Environment API, "release candidate": stable within a major). A plain `server.restart()` would not fix voidzero-dev#550: Vite restarts in the same process, so Node's `import()` cache would survive it. ### History The first push used a process-wide `module.registerHooks` resolve hook. That needed its own dependency list, content hashes and CommonJS special cases. After review it's replaced by this Vite-based version, which is smaller and only exists in dev. ## Restart still needed for New auth tables and columns in the **local database**. They're created when the dev server starts or restarts, as before. The docs line says so. ## Checks - `plugin-routing.test.ts` drives a **real Vite dev server** (no watcher, events sent by hand, plus a slow `watchChange` plugin so the test can't depend on Vite's handler order). It covers: - edits to the config and to a helper; - a syntax error, then the fix; - a missing file created later; - a route save that doesn't reload the config. Other tests cover `hotUpdate`, and that the environment is declared only for `serve` (not preview or build). - `plugin-migration.test.ts`: start-up migrations get the dev server's importer. - `auth-config-loader.test.ts`: the exports, the importer, and `devAuthConfigImporter` on a real server. - Each guard was removed once (mutation check) and a test failed each time: stale on save, stale on failure, `invalidateAll`, the created-file reload, and the runner import. - On a built CLI in a scratch app, `vp dev`: - helper edit, config edit, syntax error then fix, and a missing file created later all updated the schema; - routes still served; - an in-process restart added the new column to the local D1 table; - no extra log lines. - `void` package typecheck, lint and fmt pass. Affected tests (`vp run test --base origin/main`, 736 files) pass. The second commit gives the platform status browser test the same 30s timeout as its sibling browser tests. It timed out once under a full parallel run. ### Not changed here (older behavior) - If `auth.ts` itself has a syntax error, auth config discovery (`findVoidAuthConfig`) can't parse it and treats it as "no config", without saying so. - If `auth.ts` imports a file that doesn't exist yet, the **Worker** (`void_worker`) keeps returning 500 even after the file is created; `main` does the same. The generated schema does recover. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Summary Replace the plain `void init` welcome pages with a shared Vite-style layout for React, Vue, Svelte, and Solid, using Void colors, system fonts, and automatic light/dark mode. Preserve the original Pages-mode guidance and the database starters' message list, form, API example, and seed instructions. Bundle a small stack SVG and a theme-aware Void logo with `void`, with Vite attribution embedded in the icon sprite. Keep the shared starter styles in a CSS source file and inline the raw CSS into the CLI bundle, with matching type declarations and build/test configuration. > [!IMPORTANT] > This ships the SVG files with the core `void` package. I'm not a fan of this but the starter code is already implemented within `void` and since the SVG files are just a few KB + won't be bundled into a build, it's not terrible. > > My preference would be to shift the starter code and assets into `create-void`, but then `void init` will have to call `create-vite`, creating a circular dep. Happy to do this in a follow-up PR. > > Also, I'm not sure why `void init` needs to exist when there's `create-void`. Maybe it can be removed? ## Examples Screenshots from the four generated apps. React and Vue include database-backed message forms; Svelte and Solid use Static Pages. | React | Vue | | --- | --- | |  |  | | Svelte | Solid | | --- | --- | |  |  | ## Validation: - `CI=true vp run test`: 734 test files and 14,499 tests passed; 31 tests skipped. - `vp run check`: all 33 typecheck tasks passed. - Targeted lint and formatting checks passed. - Rebuilt CLI smoke test confirms generated `pages/style.css` matches `starter-ui.css` byte-for-byte. - Browser-verified the stack illustration and Void logo in light and dark mode.
…-dev#566) Fixes voidzero-dev#527 ## Summary Named actions no longer pollute history when they re-render the current page. voidzero-dev#530 already removed `?actionName` from the committed URL; voidzero-dev#531 still used `pushState` for every mutation, so each `useForm('/db?create')` / `action()` submit added another back-stack entry at `/db`. - Same-document mutations use `replaceState` instead of `pushState`. - `mutationCommitUrl()` prefers the server `pageData.url` when the action updates query params on the same path (e.g. `POST /books?delete` → `/books?page=2` still pushes once). ## Tests Updated POST history expectations to `replaceState` when the browser URL already matches the committed page URL. Added coverage for URL-changing POSTs and repeated same-page named-action submits. `vp test run packages/void/test/unit/pages-client.test.ts` — 69/69 passed. 🐸 Co-authored-by: Cursor Agent <cursoragent@cursor.com>
voidzero-dev#588) Fixes voidzero-dev#580 ## Summary Static `void deploy --platform cloudflare` always wrote `assets.html_handling: auto-trailing-slash` into the generated `wrangler-static.json`, ignoring `cloudflare.assets.html_handling` from `void.config.ts` / the materialized root Wrangler config. That caused extra 307 redirects for prerendered sites using `about/index.html` with links to `/about`. ## Changes - Read `assets.html_handling` from the root Wrangler config at static deploy time. - Pass it through `buildStaticManifest` into `manifest.assetConfig` so `writeCloudflareStaticDeployConfig` persists the authored value. - Type `cloudflare.assets.html_handling` in Void config as `AssetConfig['html_handling']` from `@void/deploy-core` (no duplicated union). ## Testing - `packages/void/test/unit/package.test.ts` (manifest + wrangler reader) - `packages/deploy-cloudflare/test/unit/static.test.ts` (config writer) - `packages/void/test/unit/cloudflare-deploy.test.ts` (end-to-end static deploy) Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…idzero-dev#589) Fixes voidzero-dev#557 ## Summary Vue Pages generated `patchProps` assigned `comp.props = Object.keys(props)`, which replaced components that already declare runtime props and dropped defaults, required flags, validators, and types. The `__voidPatched` guard then froze that stripped declaration. ## Changes - Add `hasRuntimePropsDeclaration()` in generated SSR and client bundles before patching. - Skip overwriting `comp.props` when the component already has a non-empty runtime declaration. - Keep registering prop keys from page data only for type-only `defineProps<T>()` components that lack runtime props metadata. - Deduplicate the injected helper via `PATCH_PROPS_RUNTIME` in `plugin-pages.ts`. - Add unit tests that execute the generated `patchProps` from renderer and client modules. ## Testing - `pnpm exec vp test run packages/vue/test/unit/plugin-pages.test.ts` - `pnpm exec vp run --filter ./packages/vue typecheck` Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…-dev#587) Fixes voidzero-dev#565 ## Problem With auth on, `vp dev` aliases five better-auth subpaths to files in Void's own copy of better-auth. Each alias used a **string** `find`, and in Vite a string `find` also matches every deeper path: ``` alias find 'better-auth/client' → …/better-auth/dist/client/index.mjs import 'better-auth/client' → …/dist/client/index.mjs ✓ import 'better-auth/client/plugins' → …/dist/client/index.mjs/plugins ✗ (no such file) ``` So any app that passes a Better Auth client plugin (`adminClient()`, `organizationClient()`, …) to `createAuthClient` broke in dev: - the client failed with `Failed to resolve import "better-auth/client/plugins"`; - SSR returned 500 (`No such module …`). `vp build` adds no alias, so it was fine. ## Fix Each `find` is now an anchored RegExp, `^better-auth/<sub>$`. Vite's alias API has only two kinds of `find`: a string, which also matches deeper paths, or a RegExp. So a RegExp is the only way to get an exact match. The repo already does the same (`{ find: /^void$/ }` in `cli/wrapper.ts`). ``` before (string) after (exact) better-auth/client Void's copy ✓ Void's copy ✓ (unchanged) better-auth/react|vue|svelte|solid Void's copy ✓ Void's copy ✓ (unchanged) better-auth/client/plugins broken path ✗ normal resolution ✓ ``` The five exact ids get the same replacement in every Vite step that reads `resolve.alias`: - the alias plugin; - `vite:pre-alias`; - SSR externalization; - the dependency optimizer. So the cold-cache reload protection from voidzero-dev#24 (the reason the aliases exist) is kept. ## Checks - **New test** in `plugin-routing.test.ts`. It starts a real Vite dev server with auth on and resolves: - `better-auth/client` and `/react` from an app file, which has no better-auth of its own. They resolve only through the aliases, to Void's copy. - `better-auth/client/plugins` from inside the package. It resolves to `dist/client/plugins/index.mjs`. - **Mutation check:** the test fails with the old string `find` and with no aliases at all. - **Scratch app from the issue:** a React pages app, auth on, `adminClient()` from `better-auth/client/plugins`, cold cache, `vp dev`. ``` before after SSR GET / 500 No such module 200, authClient.admin.listUsers is a function client module 500 Failed to resolve 200 ``` - `void` typecheck, lint and fmt pass. Affected tests (`vp run test`, 508 files) pass. ## Not changed here - **`client/plugins` is not pinned to Void's copy.** The issue mentions this as optional. Without it, the user's own better-auth provides `client/plugins`, as in `vp build`, and Vite discovers it like any other dependency. That costs one optimizer reload on the first cold-cache page that imports it. Pinning would point the import at Void's better-auth version instead of the one the app installed. That's a product decision, so I left it out. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…idzero-dev#568) Fixes voidzero-dev#523 ## Summary After a successful submit, `useForm` still promotes submitted values to defaults (edit forms / `hasChanges` unchanged). You can now get back to mount-time values: - `reset({ initial: true }, ...fields)` — reset to the values passed to `useForm` on mount - `resetOnSuccess: true` on `useForm` options — clear create forms after success - `reset()` without options still targets current defaults (last submitted values after success) Shared via `parseFormResetArgs` in `void/pages-client`; React / Vue / Svelte / Solid + docs / codegen. ## Tests - `packages/void/test/unit/form-reset.test.ts` - `packages/vue/test/unit/use-form.test.ts` (reset / `reset({ initial: true })` / `resetOnSuccess`) Typecheck: `void`, `@void/react`, `@void/vue`, `@void/svelte`, `@void/solid`. 🐸 Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…idzero-dev#559) ## Summary Depends on [voidzero-dev#561](voidzero-dev#561), which adds JSX server companion support. **Merge after voidzero-dev#561, or together with it.** On `main` without voidzero-dev#561, the page scanner still lists `index.server.tsx` as a page of its own. React and Solid decide whether to build the `pages-client` bundle with `hasNonIslandPages()`. That check counted server companions (`index.server.tsx`, `index.server.jsx`) as ordinary pages. So an island-only app still emitted a `pages-client` bundle it never used. ``` pages/ index.island.tsx island page → islands-client only index.server.tsx server companion → not a page (skipped now) status.server.v2.tsx ordinary page → still needs pages-client ✓ ``` The skip matches voidzero-dev#561's page scanner exactly: a name that **ends in** `.server.tsx` or `.server.jsx` is a companion. A name that only contains `.server.` (`status.server.v2.tsx`) is still a page. ## Tests - `packages/{react,solid}/test/unit/plugin-pages.test.ts`: - island-only pages plus a `.server.tsx` or `.server.jsx` file → no `pages-client`; - an ordinary page next to them (`about.tsx`, `about.jsx`, `blog/…`, `status.server.v2.tsx`) → `pages-client` kept. - **Mutation check:** without the skip, 4 tests fail. With a plain "contains `.server.`" rule, the 2 `status.server.v2.tsx` cases fail. - **With voidzero-dev#561 merged** (no conflicts): - these tests and voidzero-dev#561's own tests pass; - a real island-only React build has no `pages-client` chunk; - `vp preview` serves only the islands script, and the island hydrates (a button click updates it). - Affected tests (`vp run test`, 508 files), lint and typecheck pass. ## Not changed here (older behavior) - `hasNonIslandPages()` checks only the file name. So a file in a `_`-prefixed folder (e.g. `_components/Button.tsx`) still counts as a page, while the scanner skips it. Switching the adapters to the scanner's own result would fix that; it's a separate change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: LongYinan <github@lyn.one> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…unch Chromium (voidzero-dev#591) ## Problem Two platform browser tests launch Chromium under Vitest's 5s default: - `platform-bootstrap.test.ts` › accepts native browser submissions… - `platform-login.test.ts` › confirms a real native linking form… The bootstrap test takes about 4.4s on its own, so under a full parallel run (`vp run test`) it timed out: ``` FAIL platform-bootstrap.test.ts > … native browser submissions … Error: Test timed out in 5000ms. ``` ## Fix Use the same 30s timeout as every other platform browser test: ``` admin-dashboard (2 browser tests) 30_000 ✓ admin-plans 30_000 ✓ platform-status 30_000 ✓ platform-bootstrap 5s → 30_000 platform-login 5s → 30_000 ``` The diff is mostly the formatter re-indenting the two test bodies. With whitespace hidden (`?w=1`) it's +17/−5. ## Checks - Both files pass: 44 tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oidzero-dev#561) ## Summary This adds support for `.tsx` and `.jsx` in HTTP/WebSocket route files and recognizes `.server.tsx`/`.server.jsx` page companions. It also fixes existing `.server.tsx` companions being discovered as standalone pages such as `/foo.server` and included in generated client component manifests. The extension enables JSX syntax; the file's location and naming determine its role. This PR ended up larger than I imagined, mainly because there are many cases to handle. Half of the changes are tests and docs updates. There's also a fair bit of consolidation going on. P.S. TBH I didn't start out wanting to support JSX for WebSockets, but the route conventions + extensions logic was quite entangled and it was hard to untangle them. Adding support for JSX WebSockets was the easiest way forward while not making the code messy. One use case I convinced myself to add support for JSX in WebSockets is chat apps that sends rendered HTML over WebSockets (hopefully that's not too much of a stretch to imagine!). > [!WARNING] > This PR updates the docs with a new feature, and I think docs are published at merge time, which can result in unreleased features appearing in the docs. ## Motivations It's only a matter of time before someone requests for this, given the precedents for JSX-capable HTTP route files. - **[Vercel OG image generation](https://vercel.com/docs/og-image-generation):** the official examples use `api/og.tsx` and `api/og.jsx`, as well as Next.js `app/api/og/route.tsx`, to generate images from JSX inside HTTP handlers. - **[TanStack Start server routes](https://tanstack.com/start/latest/docs/framework/react/guide/server-routes):** the documentation defines HTTP `server.handlers` in `routes/hello.tsx`, alongside a JSX page component. Christoph also mentioned that he [prefers `.tsx`](https://github.com/nkzw-tech/server-template#why-does-this-template-use-tsx-as-a-file-extension). ## Scope | File | Behavior | | --- | --- | | `routes/api.tsx` / `routes/api.jsx` | HTTP handlers at `/api` | | `routes/api.dev.tsx` / `routes/api.prod.jsx` | HTTP handlers restricted to development / production | | `routes/chat.ws.tsx` / `routes/chat.ws.jsx` | WebSocket handlers at `/chat` | | `routes/chat.dev.ws.tsx` / `routes/chat.prod.ws.jsx` | WebSocket handlers restricted to development / production | | `pages/foo.server.tsx` / `pages/foo.server.jsx` | Loaders, actions, and page configuration for the matching page, excluded from standalone page discovery and generated client component manifests | | `pages/foo.tsx` / `pages/foo.jsx` | Existing page component support; no new component formats | Both JSX extensions support `.dev` and `.prod` restrictions. For WebSocket routes, the environment suffix goes before `.ws`. HTTP and WebSocket discovery, filename parsing, page companion lookup, and route binding inference share `ROUTE_EXTENSIONS` (`ts`, `tsx`, `js`, `jsx`, `mjs`, `mts`) from `shared/route-extensions.ts`. The page scanner uses the derived `ROUTE_EXTENSIONS_SET` for membership checks. Both route scanners use the fixed `listRouteFiles()` helper and the existing WebSocket filename matcher to distinguish their roles. `listConventionFiles()` retains its existing script extensions for crons and queues. Discovery and binding inference share the `.well-known` glob policy, including configured source directories. Environment filtering applies only to route files, so `_`-prefixed helpers retain their bindings in both environments. Companion exclusion applies only to supported script extensions, preserving page components such as `web.server.md`. WebSocket resource-name inference strips JSX extensions so changing `chat.ws.ts` to `chat.ws.tsx` or `chat.ws.jsx` preserves the inferred Durable Object identity. Void sets no JSX options: route JSX compiles with the app's own JSX settings (framework plugins, tsconfig for TS files, or `oxc.jsx`). The docs show Hono's JSX via `oxc.jsx` for apps without a JSX framework. The documentation covers supported HTTP, WebSocket, and companion extensions. General enforcement against manually importing `.server.*` modules from client code remains a separate feature. ## Validation - `vp run check`: all 33 workspaces passed. - `vp run test`: 11,214 passed, 31 skipped. - `vp test run --maxWorkers=4`: 14,594 passed, 31 skipped. - Formatting and lint passed. Regression coverage executes JSX HTTP handlers and WebSocket messages using React and Hono runtimes, checks environment filtering and generated types/bindings (including `.well-known` routes and underscored helpers), and executes companion loaders/actions. It also verifies that Vue, Svelte, and Markdown pages with a `.server` suffix remain discoverable. Production-build assertions confirm companion code is present on the server and absent from the generated client graph and assets. ## Follow-up [voidzero-dev#559](voidzero-dev#559) (merged) excludes server companions from React and Solid's client bundle detection, so island-only apps do not emit the full `pages-client` entry. Until this PR lands, `main` still lists a companion such as `index.server.tsx` as its own page (`/index.server`). ## Review and polish (rebased on `main`) Rebased onto `main` (the merge commit is dropped; the four commits are unchanged). A deep review found the code correct; one docs commit was added: ``` "uses your app's configured JSX runtime" → Void sets no JSX runtime React / Solid app framework JSX → render with its server renderer API only / Vue / Svelte no runtime (Vue's jsx: preserve leaves JSX raw → build fails) → docs now show oxc.jsx with hono/jsx (needs hono in the app's deps) ``` Checked in real apps with that setup, `.tsx` and `.jsx` routes, dev and `vp preview`: | App | `/hello` (.tsx) | `/hello2` (.jsx) | existing pages | |---|---|---|---| | Vue pages | 200 | 200 | 200 | | Svelte pages | 200 | 200 | 200 | | API only (with `hono` dep) | 200 | 200 | 200 | Also in that commit: the WebSocket and companion docs link to the JSX section; the companion table drops the `routes/` row and the repeated pair; `reference/structure.md` lists companion extensions; `websockets.md` says `chat.ws.dev.ts` is not a WebSocket route (it is served as an HTTP route at `/chat.ws`). Mutation checks: reverting `ROUTE_EXTENSIONS` to the old list fails 93 tests; removing the `.server` page skip fails 20. Affected tests (`vp run test --base origin/main`, 518 files) pass except one unrelated platform browser test that times out at 5s under full load (passes alone); that is fixed separately. ### Left for the maintainer (product calls) - Docs go live at merge, before the feature ships (as noted above). - `.tsx`/`.jsx` files already in `routes/` that were ignored before now become routes; a release note could tell users to prefix non-route files with `_`. - Two companions for one page (`foo.server.ts` + `foo.server.tsx`) silently use the first by extension order, as `.ts` + `.tsx` already did on `main`. - Older, not from this PR: `a.ts` + `a.tsx` (like `a.ts` + `a.js` today) register two handlers for `/a` without an error. --------- Co-authored-by: LongYinan <github@lyn.one> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ro-dev#569) Live Cloudflare acceptance exposed failures in interrupted platform updates, slow application requests, usage reporting, page revalidation and account prerequisite guidance. This change fixes those paths and preserves the provider resources used by repeatable live tests. ## Changes - Explain R2 activation, the paid Workers for Platforms subscription, and Analytics Engine activation with direct account dashboard links and recovery instructions. Report both known R2/WfP blockers from the existing read-only planning checks without adding probes. Preserve genuine permission failures and provider diagnostic causes. Replace misleading token-permission advice for Analytics Engine code 10089 and document its dashboard Enable action and Workers Free availability. - Resume interrupted credential updates without trying to reactivate immutable Worker versions bound to old secrets. Keep traffic closed, retain update authority across checkpoints, and reject concurrent Worker replacement. Retry transient health-check transport errors only. - Await foreground application responses by default. Keep explicit operator deadlines and bounded storage/background work. A real Sandbox request completes after 44.9 seconds instead of hitting the previous ten-second timeout. - Use the current AI Gateway analytics query and scope the optional managed-build gate to GitHub connection routes, allowing ordinary project usage requests when managed builds are disabled. - Store managed ISR entries in the existing R2 bucket so an eventually consistent KV read cannot restore purged HTML for its full TTL. Keep legacy KV discovery/cleanup; log legacy deletion failures without blocking the HTML/JSON edge purge after successful R2 deletion. Authoritative R2 errors still propagate. Native deployment behavior and bindings are unchanged. - Retain native live-test resources under an explicit namespace, verify data/resource reuse, and serialize the shared CI target. Document connection-pool budgeting for platforms sharing a PostgreSQL server. ## Validation Final local verification on `21bb87ef0`: `vp test run` passed 736 files and 14,547 tests (31 tests skipped, zero unhandled errors), and all 33 package typechecks passed. The prerequisite-message tests pass 184 focused cases; the legacy-KV follow-up passes 39 focused cases. Both path-specific and full-purge endpoint regressions failed before that fix and pass afterward. Affected verification passed 10,974 tests for the CLI changes and 4,907 for the cache follow-up. Core runtime/package and documentation builds, formatting and lint passed. Independent reviews found no remaining actionable issues in these changes. A rebuilt CLI was tested against the unpaid account: the real installation plan reports R2 code 10042 and Workers for Platforms code 10121 together, with account-specific links and retry instructions. It exits before creating installation files, and the existing platform registry is unchanged. The captured real Analytics Engine code 10089 failure was replayed through the updated formatter and now directs the operator to Enable and the appropriate recovery command. The free-account comparison also verified a native static app and stock Void AI Gateway creation. Analytics Engine binding upload failed with 10089 before activation and passed after zero-price dashboard activation while Workers Free remained selected. R2 was not activated and WfP was not purchased; a fresh managed platform cannot complete installation on that account until those prerequisites are satisfied. Airwallex’s generic AI Gateway 500/code 7000 was not reproduced. Both paid-account Cloudflare installations were deployed and independently verified on `852341fc6` with packaged manifest `34f1cea97be8251055b5002ec9eb2cdaa83393be73b81cd6f23532daafb00a25`. Provider readback verified all five active Worker versions at 100% traffic, all 19/15 retained resource IDs, settings and migration history. Three 600-second page-action purges and one all-project purge returned fresh first-load MISS content in the same Cloudflare location, with independent R2 before/deleted/replacement readback. Project usage returns HTTP 200; AI usage matches the provider aggregate after whole-neuron rounding. All 24 managed apps passed final reachability; the kitchen smoke passed 14/14. That live acceptance covered an upgraded and a fresh installation, 24 managed apps (239 functional checks; 81 browser checks across 16 apps), four native app classes, two PostgreSQL variants, two genuine users, teams and ownership, admin/user settings, quota/suspension controls, protected apps, connected-app publishing/private preview, inbound/outbound email, queues, cron, AI, Live SSE, WebSockets and Sandbox. Earlier local acceptance also covered 243 real PostgreSQL checks, playground builds and packed npm/pnpm consumers for four adapters. Remote [CI](https://github.com/voidzero-dev/void/actions/runs/39996202354), [Platform CI](https://github.com/voidzero-dev/void/actions/runs/39996202494), [Prepublish Test](https://github.com/voidzero-dev/void/actions/runs/39996202534), and [Security Analysis](https://github.com/voidzero-dev/void/actions/runs/39996202533) completed successfully on `21bb87ef0`: 19 successful checks, one skipped, none failing or pending. The skipped credentialed `live-e2e` job is separate from the provider-backed acceptance above. Automated review completed on this head with no findings; the legacy-KV review thread is resolved. ## Known limitations The legacy-KV cleanup follow-up has local endpoint regression coverage and has not been redeployed to the acceptance installations; their runtime remains `852341fc6`. No existing MySQL service was available, so MySQL has local wire-protocol coverage only. Verified-email invitation acceptance, direct Google/GitHub account linking, optional GitHub build services and third-party AI BYOK were not configured. Successful project custom-domain attachment remains unverified: the test installation disables that capability, and its denial path preserves DNS and the existing app. Destructive remote teardown is excluded by the retained-resource test policy. An ambiguous pre-fix email receipt remains preserved; recovery and a duplicate send have not been attempted. No additional paid plan was purchased.
…oidzero-dev#592) Fixes voidzero-dev#422 `bundleEntry` bundles the SvelteKit Worker again for `--platform void` with a plain Vite lib build. That build used Vite's default client conditions, so dependencies resolved to their `browser` exports: `@sentry/sveltekit` failed to build, and `postgres` built but picked its Node build. voidzero-dev#540 fixed the same problem for `--platform cloudflare` in `prepareBundledBuildOutput`. `bundleEntry` now uses the same conditions: `WORKER_CONDITIONS` plus `production`. `production` is fixed rather than taken from `NODE_ENV`, because a deploy bundle is always a production build. The module rules part of voidzero-dev#540 (`.wasm`, `.txt`, `.bin`, …) is not covered here (see voidzero-dev#535).
Fixes voidzero-dev#525 ## Summary Adds `setError(field, message)` to `useForm` across React, Vue, Svelte, and Solid so apps can surface client-side validation through the same typed `errors` object as server `withValidator()` responses. Changing a field clears that field's validation error automatically (`setData` / `data` assignment). ## Changes - Add public `setError(field, message)` on `useForm` for all four frameworks. - Clear `errors[field]` on field change via `setData` (React/Solid) or `data` assignment (Vue/Svelte). - Update docs for actions/forms and the pages API reference. - Add Vue unit coverage for `setError` and clear-on-change. ## Testing - `packages/vue/test/unit/use-form.test.ts` - Typecheck `@void/react`, `@void/vue`, `@void/svelte`, `@void/solid` Co-authored-by: froQ <sayhola@froq.me>
cpojer
force-pushed
the
cursor/feat-useform-seterror-dbee
branch
from
October 26, 2026 07:15
ed7548d to
e019196
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes voidzero-dev#525
Summary
Adds
setError(field, message)touseFormacross React, Vue, Svelte, and Solid so apps can surface client-side validation through the same typederrorsobject as serverwithValidator()responses.Changing a field clears that field's validation error automatically:
setData(field, value)clearserrors[field]dataclearserrors[field](snapshot-based watch /$effect)API
setError(field, message)clearErrors(...fields)Tests
packages/vue/test/unit/use-form.test.ts—setErrorand clearing ondatachangeTypecheck
@void/react,@void/vue,@void/svelte,@void/solidFit to voidzero-dev#525
Implements proposal (1)
setError(field, message)and proposal (2) clear-on-change viasetData/dataassignment, unblocking client-side schema validation in userland without a second error state.Related: voidzero-dev#523 (form line; reset behavior is separate in PR voidzero-dev#568 on upstream).