void 0.10.11 → 0.10.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.mjs +14 -14
- package/dist/cli/env-schema-probe.mjs +2 -2
- package/dist/{config-E03l1C_h.mjs → config-qHGgPuWT.mjs} +23 -0
- package/dist/{db-PZBsLSGb.mjs → db-C0i0sYMS.mjs} +3 -3
- package/dist/{deploy-DT8wsPZd.mjs → deploy-jBJT1fUT.mjs} +408 -66
- package/dist/{env-AAHU02L6.mjs → env-CZy5MorI.mjs} +1 -1
- package/dist/{env-validation-BdDlGhbN.mjs → env-validation-Dea3v3ej.mjs} +1 -1
- package/dist/{gen-oup1xBN0.mjs → gen-BzXf3Jh2.mjs} +1 -1
- package/dist/{git-metadata-CBKaL0v5.mjs → git-metadata-D-gurjzG.mjs} +6 -5
- package/dist/{github-cmd-BdNaOVNa.mjs → github-cmd-DKcGUNsj.mjs} +1 -1
- package/dist/{headers-BQknpzkn.mjs → headers-ChAADPQu.mjs} +1 -1
- package/dist/index.mjs +91 -43
- package/dist/{init-Dl2PKuQn.mjs → init-7JCAcKNQ.mjs} +2 -2
- package/dist/{node-BDx8pmhq.mjs → node-pRg81HqV.mjs} +3 -3
- package/dist/pages/index.mjs +3 -3
- package/dist/pages/islands-plugin.mjs +1 -1
- package/dist/{prepare-DOBTY0o4.mjs → prepare-BvvgAz-3.mjs} +3 -3
- package/dist/{preset-BGrvB4Bl.mjs → preset-BjyR3lzz.mjs} +55 -2
- package/dist/{provision-BLrCEBbI.mjs → provision-CPx2ZxsH.mjs} +2 -2
- package/dist/{route-types-COI2DsZv.mjs → route-types-z1jtHEi_.mjs} +8 -1
- package/dist/{scan-DYXkrasO.mjs → scan-ChWt4pX1.mjs} +12 -13
- package/dist/{scan-DEwlM_Xy.mjs → scan-NU4xKGci.mjs} +4 -1
- package/package.json +2 -2
- package/schema.json +18 -0
- package/skills/migrate-vite-cloudflare-to-void/SKILL.md +1 -1
- package/skills/void/docs/guide/app-types.md +43 -0
- package/skills/void/docs/guide/deployment.md +68 -1
- package/skills/void/docs/guide/edge/static-assets.md +37 -1
- package/skills/void/docs/guide/server-routing.md +21 -1
- package/skills/void/docs/guide/websockets.md +3 -0
- package/skills/void/docs/integrations/frameworks/overview.md +1 -0
- package/skills/void/docs/node_modules/void/CLAUDE.md +3 -1
- package/skills/void/docs/node_modules/void/skills/migrate-vite-cloudflare-to-void/SKILL.md +1 -1
- package/skills/void/docs/reference/cli.md +24 -0
- package/skills/void/docs/reference/config.md +42 -1
- package/skills/void/docs/reference/structure.md +1 -0
|
@@ -2,7 +2,7 @@ import { a as join, i as isAbsolute, t as basename } from "./dist-DaKKDf8D.mjs";
|
|
|
2
2
|
import { n as readProjectPaths } from "./project-paths-BQd7OmIo.mjs";
|
|
3
3
|
import { n as globSync, t as glob } from "./dist-BuiRJkTd.mjs";
|
|
4
4
|
import { c as isIdentifier, d as isObjectExpression, l as isLiteral, s as isCallExpression } from "./entry-D7yy4xVH.mjs";
|
|
5
|
-
import { o as
|
|
5
|
+
import { c as parseWebSocketFilename, o as isRouteEnabledForEnvironment, s as parseRouteFilename, t as scanPages } from "./scan-NU4xKGci.mjs";
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import { parseSync } from "vite";
|
|
@@ -261,19 +261,19 @@ function buildWebSocketRoute(root, filePath, paths = readProjectPaths(root)) {
|
|
|
261
261
|
hasAuthGate: info?.hasAuthGate ?? false
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
|
-
function parseWebSocketFiles(root, files, paths = readProjectPaths(root)) {
|
|
265
|
-
return files.filter((f) => isWebSocketRouteFile(f)).filter((f) => !f.split("/").some((segment) => segment.startsWith("_"))).map((filePath) => buildWebSocketRoute(root, filePath, paths)).sort((a, b) => {
|
|
264
|
+
function parseWebSocketFiles(root, files, paths = readProjectPaths(root), environment) {
|
|
265
|
+
return files.filter((f) => isWebSocketRouteFile(f)).filter((f) => !f.split("/").some((segment) => segment.startsWith("_"))).filter((filePath) => isRouteEnabledForEnvironment(parseWebSocketFilename(filePath), environment)).map((filePath) => buildWebSocketRoute(root, filePath, paths)).sort((a, b) => {
|
|
266
266
|
if (a.catchAll !== b.catchAll) return a.catchAll ? 1 : -1;
|
|
267
267
|
return a.pattern.localeCompare(b.pattern);
|
|
268
268
|
});
|
|
269
269
|
}
|
|
270
|
-
async function scanWebSocketRoutes(root, paths = readProjectPaths(root)) {
|
|
270
|
+
async function scanWebSocketRoutes(root, paths = readProjectPaths(root), environment) {
|
|
271
271
|
return parseWebSocketFiles(root, await glob(`**/*.{${HANDLER_EXTENSIONS.join(",")}}`, {
|
|
272
272
|
cwd: paths.routesDir,
|
|
273
273
|
absolute: false
|
|
274
|
-
}).catch(() => []), paths);
|
|
274
|
+
}).catch(() => []), paths, environment);
|
|
275
275
|
}
|
|
276
|
-
function scanWebSocketRoutesSync(root, paths = readProjectPaths(root)) {
|
|
276
|
+
function scanWebSocketRoutesSync(root, paths = readProjectPaths(root), environment) {
|
|
277
277
|
const pattern = `**/*.{${HANDLER_EXTENSIONS.join(",")}}`;
|
|
278
278
|
let files;
|
|
279
279
|
try {
|
|
@@ -284,7 +284,7 @@ function scanWebSocketRoutesSync(root, paths = readProjectPaths(root)) {
|
|
|
284
284
|
} catch {
|
|
285
285
|
files = [];
|
|
286
286
|
}
|
|
287
|
-
return parseWebSocketFiles(root, files, paths);
|
|
287
|
+
return parseWebSocketFiles(root, files, paths, environment);
|
|
288
288
|
}
|
|
289
289
|
function isWebSocketPatternFile(filePath) {
|
|
290
290
|
return isWebSocketRouteFile(filePath);
|
|
@@ -329,7 +329,7 @@ async function scanRoutes(root, options) {
|
|
|
329
329
|
cwd: paths.routesDir,
|
|
330
330
|
absolute: false
|
|
331
331
|
}),
|
|
332
|
-
scanWebSocketRoutes(root, paths),
|
|
332
|
+
scanWebSocketRoutes(root, paths, options?.environment),
|
|
333
333
|
glob(ROUTE_PATTERN, {
|
|
334
334
|
cwd: paths.middlewareDir,
|
|
335
335
|
absolute: false
|
|
@@ -343,9 +343,8 @@ async function scanRoutes(root, options) {
|
|
|
343
343
|
]);
|
|
344
344
|
const routesDir = paths.routesDir;
|
|
345
345
|
return {
|
|
346
|
-
routes: routeFiles.filter((f) => !isWebSocketPatternFile(f)).filter((f) => !f.split("/").some((s) => s.startsWith("_"))).map((
|
|
347
|
-
|
|
348
|
-
route.methods = detectHttpExports(join(routesDir, f));
|
|
346
|
+
routes: routeFiles.filter((f) => !isWebSocketPatternFile(f)).filter((f) => !f.split("/").some((s) => s.startsWith("_"))).map((file) => parseRouteFilename(file)).filter((route) => isRouteEnabledForEnvironment(route, options?.environment)).map((route) => {
|
|
347
|
+
route.methods = detectHttpExports(join(routesDir, route.filePath));
|
|
349
348
|
return route;
|
|
350
349
|
}).sort(compareRouteSpecificity),
|
|
351
350
|
websockets,
|
|
@@ -367,7 +366,7 @@ function scanMiddlewareSync(root, paths = readProjectPaths(root)) {
|
|
|
367
366
|
}
|
|
368
367
|
return parseMiddlewareDefinitions(middlewareFiles);
|
|
369
368
|
}
|
|
370
|
-
function scanRoutePatternsSync(root, paths = readProjectPaths(root)) {
|
|
369
|
+
function scanRoutePatternsSync(root, paths = readProjectPaths(root), environment) {
|
|
371
370
|
let routeFiles;
|
|
372
371
|
try {
|
|
373
372
|
routeFiles = globSync(ROUTE_PATTERN, {
|
|
@@ -377,7 +376,7 @@ function scanRoutePatternsSync(root, paths = readProjectPaths(root)) {
|
|
|
377
376
|
} catch {
|
|
378
377
|
routeFiles = [];
|
|
379
378
|
}
|
|
380
|
-
return routeFiles.filter((f) => !isWebSocketPatternFile(f)).filter((f) => !f.split("/").some((s) => s.startsWith("_"))).map((f) => parseRouteFilename(f)).sort(compareRouteSpecificity);
|
|
379
|
+
return routeFiles.filter((f) => !isWebSocketPatternFile(f)).filter((f) => !f.split("/").some((s) => s.startsWith("_"))).map((f) => parseRouteFilename(f)).filter((route) => isRouteEnabledForEnvironment(route, environment)).sort(compareRouteSpecificity);
|
|
381
380
|
}
|
|
382
381
|
//#endregion
|
|
383
382
|
export { scanQueuesSync as a, loadVoidAuthConfig as c, scanWebSocketRoutesSync as i, scanRoutePatternsSync as n, scanJobsSync as o, scanRoutes as r, findVoidAuthConfig as s, scanMiddlewareSync as t };
|
|
@@ -74,6 +74,9 @@ function parseRouteFilename(relativePath) {
|
|
|
74
74
|
function parseWebSocketFilename(relativePath) {
|
|
75
75
|
return parsePath(relativePath, true);
|
|
76
76
|
}
|
|
77
|
+
function isRouteEnabledForEnvironment(route, environment) {
|
|
78
|
+
return environment === void 0 || route.env === null || route.env === environment;
|
|
79
|
+
}
|
|
77
80
|
//#endregion
|
|
78
81
|
//#region src/pages/layout-resolution.ts
|
|
79
82
|
function isNamedLayout(layout) {
|
|
@@ -348,4 +351,4 @@ function extractFrontmatterLayout(source) {
|
|
|
348
351
|
return value;
|
|
349
352
|
}
|
|
350
353
|
//#endregion
|
|
351
|
-
export { resolveNamedLayout as a, resolveLayoutIds as i, resolveEffectiveLayoutChain as n,
|
|
354
|
+
export { resolveNamedLayout as a, parseWebSocketFilename as c, resolveLayoutIds as i, resolveEffectiveLayoutChain as n, isRouteEnabledForEnvironment as o, resolveLayoutChain as r, parseRouteFilename as s, scanPages as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "void",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.13",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/voidzero-dev/void.git",
|
|
@@ -356,7 +356,7 @@
|
|
|
356
356
|
"valibot": ">=1.0.0-beta.7",
|
|
357
357
|
"vite": "^8.0.0",
|
|
358
358
|
"zod": "^3.25.0 || ^4.0.0",
|
|
359
|
-
"@void/md": "0.10.
|
|
359
|
+
"@void/md": "0.10.13"
|
|
360
360
|
},
|
|
361
361
|
"peerDependenciesMeta": {
|
|
362
362
|
"@void/md": {
|
package/schema.json
CHANGED
|
@@ -144,6 +144,11 @@
|
|
|
144
144
|
"type": "object",
|
|
145
145
|
"description": "Routing configuration — headers, redirects, rewrites, fallbacks, revalidation, prerendering.",
|
|
146
146
|
"properties": {
|
|
147
|
+
"notFound": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"enum": ["single-page-application", "404-page", "none"],
|
|
150
|
+
"description": "Override how the asset layer answers a request that matched no asset and no worker route. Void infers \"single-page-application\" when the worker does not own HTML routing; set \"404-page\" for a static-site-generator build so unknown URLs serve 404.html with a real HTTP 404 instead of index.html with a 200. Does not change run_worker_first. Ignored for SvelteKit, Nuxt, Analog, and Astro deploys, which pin not_found_handling to \"none\" because the framework's own worker owns unmatched HTML."
|
|
151
|
+
},
|
|
147
152
|
"revalidate": {
|
|
148
153
|
"description": "Edge caching TTL in seconds. A number sets a global TTL for all SSR pages. An object maps URL patterns to TTLs (first match wins). Set to 0 to disable caching for a path.",
|
|
149
154
|
"oneOf": [
|
|
@@ -355,6 +360,19 @@
|
|
|
355
360
|
"type": "object",
|
|
356
361
|
"description": "Environment variables (plain text, not secrets)",
|
|
357
362
|
"additionalProperties": { "type": "string" }
|
|
363
|
+
},
|
|
364
|
+
"limits": {
|
|
365
|
+
"type": "object",
|
|
366
|
+
"description": "Worker resource limit overrides.",
|
|
367
|
+
"properties": {
|
|
368
|
+
"cpu_ms": {
|
|
369
|
+
"type": "integer",
|
|
370
|
+
"minimum": 1,
|
|
371
|
+
"maximum": 300000,
|
|
372
|
+
"description": "Caps Workers CPU time per request, in milliseconds (max 300000 = 5 minutes). The platform plan may cap it lower."
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
"additionalProperties": false
|
|
358
376
|
}
|
|
359
377
|
},
|
|
360
378
|
"additionalProperties": false
|
|
@@ -103,7 +103,7 @@ Apply these filename rules exactly when mapping old handlers to `routes/`:
|
|
|
103
103
|
1. Extension and suffix parsing order
|
|
104
104
|
|
|
105
105
|
- Strip extension (`.ts`, `.js`, `.mts`, `.mjs`).
|
|
106
|
-
- Strip env suffix (`.dev`, `.prod`).
|
|
106
|
+
- Strip env suffix (`.dev`, `.prod`). The suffix restricts the route to that environment, so only use it for handlers that must not ship to the other one.
|
|
107
107
|
- Strip HTTP method suffix (`.get`, `.post`, `.put`, `.delete`, `.patch`).
|
|
108
108
|
- Strip trailing `index` segment.
|
|
109
109
|
- Remove route group segments `(group-name)` from URL path.
|
|
@@ -73,6 +73,48 @@ At the edge, the resolution order is:
|
|
|
73
73
|
|
|
74
74
|
**Deploy:** `void deploy` or `void deploy --dir <path>` uploads static files directly. Assets are served from the edge with automatic caching.
|
|
75
75
|
|
|
76
|
+
## Adding a backend to a static site
|
|
77
|
+
|
|
78
|
+
A static site generator plus a few API routes is a Void app, not a static site. Auto-detection sees the SSG dependency first (priority 2 below), so if you leave `inference.appType` unset, `void deploy` refuses rather than deploying the site and silently dropping `routes/`. Tell it which you meant.
|
|
79
|
+
|
|
80
|
+
To deploy the site **and** the backend, run both builds from one command and let Void's build fold the generated site into its client output:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
// void.json
|
|
84
|
+
{
|
|
85
|
+
"inference": {
|
|
86
|
+
"appType": "void",
|
|
87
|
+
"build": "vitepress build && vite build"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// vite.config.ts
|
|
94
|
+
import { defineConfig } from 'vite';
|
|
95
|
+
import { voidPlugin } from 'void';
|
|
96
|
+
|
|
97
|
+
export default defineConfig({
|
|
98
|
+
plugins: [voidPlugin()],
|
|
99
|
+
// The SSG's output becomes the static assets of the Void build.
|
|
100
|
+
publicDir: '.vitepress/dist',
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`vitepress build` emits the site, then `vite build` copies `publicDir` into `dist/client` and emits the worker into `dist/ssr`. Keep `voidPlugin()` in the **root** `vite.config.ts` only — not in the generator's own config (e.g. `.vitepress/config.ts`).
|
|
105
|
+
|
|
106
|
+
Because the worker now owns unmatched requests, add [`routing.notFound`](../reference/config.md#routing-notfound) if you want the generator's `404.html` instead of the SPA-style `index.html` fallback:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "routing": { "notFound": "404-page" } }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
To deploy the static output only and **not** the backend, say so explicitly:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "inference": { "appType": "static" } }
|
|
116
|
+
```
|
|
117
|
+
|
|
76
118
|
## Auto-Detection
|
|
77
119
|
|
|
78
120
|
When running `void deploy` and no `inference.appType` is set in `void.json`, the detection logic runs in this order:
|
|
@@ -81,6 +123,7 @@ When running `void deploy` and no `inference.appType` is set in `void.json`, the
|
|
|
81
123
|
| -------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
82
124
|
| 1 | `--dir` flag | Static (or SPA with `--spa`) |
|
|
83
125
|
| 2 | Known SSG in dependencies (`vitepress`, `@docusaurus/core`) | Static, builds with SSG CLI |
|
|
126
|
+
| 2a | ...and backend files also exist | Refused — set `inference.appType` yourself |
|
|
84
127
|
| 3 | `@tanstack/react-start`, `@react-router/dev`, `@sveltejs/kit`, `nuxt`, `@analogjs/platform`, or `astro` in deps | Framework |
|
|
85
128
|
| 4 | Backend files exist (`routes/`, `pages/`, `middleware/`, `crons/`, `queues/`, SSR entry) | Void app |
|
|
86
129
|
| 5 | `vite` or `vite-plus` in dependencies, no backend files | SPA, builds with `vite build` or `vp build` |
|
|
@@ -62,10 +62,77 @@ If your CI pipeline runs typechecking or other static analysis before deploy, ru
|
|
|
62
62
|
| `VOID_TOKEN` | Auth token (for CI, skips OAuth) |
|
|
63
63
|
| `VOID_PROJECT` | Project slug override |
|
|
64
64
|
|
|
65
|
-
## GitHub
|
|
65
|
+
## GitHub
|
|
66
66
|
|
|
67
67
|
You can deploy from a GitHub repo on every push to `main`. Running `void init --github` generates `.github/workflows/void-deploy.yml` in your project.
|
|
68
68
|
|
|
69
|
+
### Void GitHub App (recommended)
|
|
70
|
+
|
|
71
|
+
No `.github/workflows/void-deploy.yml` or repository `VOID_TOKEN` is required.
|
|
72
|
+
|
|
73
|
+
1. Initialize your Void project
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
void init
|
|
77
|
+
# Or, for existing Void project:
|
|
78
|
+
void project link
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
2. Install the Void GitHub app
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
void github install
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You should receive GitHub url asking you to install and authorize `Void Deploy`.
|
|
88
|
+
|
|
89
|
+
In the browser, select the GitHub account/organization and grant access to the repository.
|
|
90
|
+
|
|
91
|
+
:::details If someone already installed the App for your organization
|
|
92
|
+
Join that installation instead:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
void github join
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
:::
|
|
99
|
+
|
|
100
|
+
3. Link your repository to your project
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
void github connect --executor container
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
4. Verify the connection
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
void github status
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
You should receive an output like:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
Repository <owner/repository>
|
|
116
|
+
Branch main
|
|
117
|
+
Build executor container
|
|
118
|
+
Deploy workflow .github/workflows/void-deploy.yml (unused for container builds)
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
5. Push to the configured branch to trigger a new deploy
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git push origin main
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
6. Follow the build
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
void build logs --follow
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### GitHub Actions
|
|
135
|
+
|
|
69
136
|
The workflow authenticates with [GitHub OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) — there is **no long-lived `VOID_TOKEN` secret to store or rotate**. `void deploy` detects GitHub Actions and does the exchange itself: it requests a short-lived OIDC token from GitHub (audience `void`), exchanges it at `POST $VOID_API_URL/auth/github-oidc` for a short-lived project-scoped deploy token (about 15 minutes on the free tier; longer on higher plans, capped at ~60 minutes), and deploys — so the workflow is a single `void deploy` step. `permissions: id-token: write` is still required: it is what lets the job (and the CLI) mint the OIDC token.
|
|
70
137
|
|
|
71
138
|
```yaml
|
|
@@ -97,9 +97,45 @@ This is the path needed for preview auth and other middleware. Cloudflare's plat
|
|
|
97
97
|
|
|
98
98
|
For Void apps with only `/api` routes, Void keeps the platform SPA fallback and scopes `run_worker_first` to `/api` and `/api/*`. Static assets and non-API SPA navigations stay on the asset platform. API requests, including browser document navigations such as OAuth callbacks, reach the worker instead of being rewritten to `index.html`.
|
|
99
99
|
|
|
100
|
+
### Unmatched requests
|
|
101
|
+
|
|
102
|
+
`not_found_handling` decides what the asset layer does with a request that matched no asset and no worker route. Void infers it:
|
|
103
|
+
|
|
104
|
+
| App shape | Inferred value | Result for an unknown URL |
|
|
105
|
+
| ------------------------------------------------- | -------------------------------- | -------------------------------- |
|
|
106
|
+
| Pages or SSR | `none` | The worker's own 404 |
|
|
107
|
+
| Worker owns HTML, no `pages/`, no SSR entry | `none` + worker fallback | `index.html` with status **200** |
|
|
108
|
+
| Asset-first (only `/api` routes, or none) | `single-page-application` | `index.html` with status **200** |
|
|
109
|
+
| Framework deploy (SvelteKit, Nuxt, Analog, Astro) | `none` — pinned, not overridable | The framework worker's own 404 |
|
|
110
|
+
|
|
111
|
+
The middle row is a worker with `middleware/`, a route outside `/api`, a document websocket, or Live, but no `pages/` and no SSR entry. `run_worker_first: ['/**']` sends everything to the worker, which bypasses the platform's SPA switch, so the generated worker does that fallback itself for HTML navigations — after your middleware has run, so auth gates and OAuth callbacks still see the request first.
|
|
112
|
+
|
|
113
|
+
The SPA fallback is right for a single-page app, where deep links must boot the client router. It is wrong for a site whose HTML was generated per page: unknown URLs return 200 instead of 404, and the generator's `404.html` is never served. Nothing in a built asset tree distinguishes the two, so Void does not guess — override it with [`routing.notFound`](../../reference/config.md#routing-notfound):
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{ "routing": { "notFound": "404-page" } }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Accepted values are `"single-page-application"`, `"404-page"`, and `"none"`. `run_worker_first` keeps its inferred value, so API routes, auth, and `/__void/*` still reach the worker first. Setting anything other than `"single-page-application"` also turns off the worker-side `index.html` fallback described above — otherwise it would answer the request before `not_found_handling` was ever consulted. With `"404-page"` the worker serves whatever the asset binding returns for the unmatched path, which is Cloudflare's nearest `404.html` — but only for HTML navigations (requests whose `Accept` includes `text/html`), so an API route that deliberately returns a 404 keeps its own body, status and headers. If the build has no `404.html`, the worker's own 404 is kept.
|
|
120
|
+
|
|
121
|
+
The last row is the exception: `routing.notFound` is **ignored** for SvelteKit, Nuxt, Analog, and Astro deploys, and `void deploy` warns when you set it. Those deploys emit no `run_worker_first`, so the asset layer already answers first and real prerendered files win — `not_found_handling` would only change what the framework's own `env.ASSETS.fetch()` delegation returns, where `"single-page-application"` turns genuine 404s into the prerendered home page at 200 and `"404-page"` takes the 404 away from the framework's own error route.
|
|
122
|
+
|
|
123
|
+
TanStack Start, React Router, and vinext follow the rows above on a managed `void deploy` — that path resolves the asset config itself and applies it to the uploaded worker. Void writes no `assets` policy into their generated worker wrangler config:
|
|
124
|
+
|
|
125
|
+
| Framework | Generated worker config |
|
|
126
|
+
| -------------- | ---------------------------- |
|
|
127
|
+
| TanStack Start | `dist/server/wrangler.json` |
|
|
128
|
+
| React Router | `build/server/wrangler.json` |
|
|
129
|
+
| vinext (App) | `dist/server/wrangler.json` |
|
|
130
|
+
| vinext (Pages) | `dist/ssr/wrangler.json` |
|
|
131
|
+
|
|
132
|
+
So on a self-hosted `wrangler deploy` the setting takes effect only if that config declares a complete `assets` policy of its own — `binding`, `directory`, `not_found_handling`, and `run_worker_first`. Void leaves those fields alone, so a policy you write yourself is honored by the generated wrapper; with no policy at all, Cloudflare's default applies. `vite build` warns when `routing.notFound` is set so the choice is not silent.
|
|
133
|
+
|
|
100
134
|
### Generated config
|
|
101
135
|
|
|
102
|
-
Void owns the generated asset routing policy during dev and build. If a root `wrangler.jsonc` contains stale `not_found_handling` or `run_worker_first` values, Void replaces those fields so generated config cannot accidentally change which layer sees a request first.
|
|
136
|
+
Void owns the generated asset routing policy during dev and build for Void apps. If a root `wrangler.jsonc` contains stale `not_found_handling` or `run_worker_first` values, Void replaces those fields so generated config cannot accidentally change which layer sees a request first.
|
|
137
|
+
|
|
138
|
+
TanStack Start and React Router are the exception: Void generates no asset policy for them and leaves both fields to your own wrangler config. Writing `not_found_handling` alone would make the asset layer answer unmatched requests and the framework worker would never run, and completing the policy needs `assets.binding` and `assets.directory` that the framework owns, not Void.
|
|
103
139
|
|
|
104
140
|
## API routes and SSR pages
|
|
105
141
|
|
|
@@ -14,7 +14,27 @@ Create route handlers in `routes/**/*.ts`. Each file maps to a URL path based on
|
|
|
14
14
|
|
|
15
15
|
Dynamic segments use brackets: `[id]` becomes a route parameter and `[...slug]` becomes a catch-all. Files or directories starting with `_` are ignored. Directories wrapped in parentheses like `(admin)` are route groups. They help organize files without changing the URL.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
### Environment-only routes
|
|
18
|
+
|
|
19
|
+
Add a `.dev` or `.prod` suffix before the extension to include a route in only one environment:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
routes/
|
|
23
|
+
api/health.ts → always
|
|
24
|
+
api/debug.dev.ts → development only
|
|
25
|
+
api/metrics.prod.ts → production only
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`dev` is the Vite dev server. `prod` is every build, `void deploy`, and `vite preview` — preview serves a production build, so it uses the production routes.
|
|
29
|
+
|
|
30
|
+
An excluded route is stripped from the worker bundle, from the generated types, and from the generated route and WebSocket config. It is not compiled and it is not deployed.
|
|
31
|
+
|
|
32
|
+
Two things do not follow the suffix:
|
|
33
|
+
|
|
34
|
+
- `void prepare` boots no Vite, so it has no environment to read. It generates types for every route in both environments.
|
|
35
|
+
- [Binding inference](../reference/resource-inference.md) scans your source for imports and does not read the suffix. A `void/storage` import inside `api/debug.dev.ts` still adds an R2 binding to your production config, and `void deploy` still provisions the bucket. Set the binding explicitly with [`inference.bindings`](../reference/config.md#inference-bindings) if you do not want it.
|
|
36
|
+
|
|
37
|
+
The suffix applies to files in `routes/` only. It has no effect in `pages/`, `middleware/`, `crons/`, or `queues/`.
|
|
18
38
|
|
|
19
39
|
Each file exports named HTTP method constants to handle specific methods:
|
|
20
40
|
|
|
@@ -28,6 +28,9 @@ Filename rules match regular server routes:
|
|
|
28
28
|
- `[id].ws.ts` becomes `:id`
|
|
29
29
|
- `[...slug].ws.ts` becomes a catch-all
|
|
30
30
|
- route groups like `(marketing)/chat.ws.ts` are ignored in the URL
|
|
31
|
+
- `chat.dev.ws.ts` / `chat.prod.ws.ts` restrict the route to one environment
|
|
32
|
+
|
|
33
|
+
The environment suffix goes **before** `.ws`. `chat.ws.dev.ts` is not recognised.
|
|
31
34
|
|
|
32
35
|
## `defineRoom()`
|
|
33
36
|
|
|
@@ -77,6 +77,7 @@ Most configuration is inferred automatically. Use `void.json` to override defaul
|
|
|
77
77
|
| `routing.prerender` | Paths to prerender as static HTML at deploy time |
|
|
78
78
|
| `routing.revalidate` | Default revalidation TTL in seconds for cached responses |
|
|
79
79
|
| `inference.bindings` | Override inferred bindings. You only need this if auto-detection is not doing what you want. Accepts `true` or a custom binding name such as `"db": "MY_DB"`. |
|
|
80
|
+
| `routing.notFound` | **Ignored.** SvelteKit, Nuxt, Analog, and Astro pin `not_found_handling` to `"none"` — the framework worker owns unmatched HTML. `void deploy` warns if set. |
|
|
80
81
|
|
|
81
82
|
See [Configuration](../../reference/config.md) for the full reference.
|
|
82
83
|
|
|
@@ -144,7 +144,7 @@ Build output uses `.mjs` / `.d.mts` extensions (`vp pack` / tsdown default).
|
|
|
144
144
|
Applied in order to a path relative to `routes/`:
|
|
145
145
|
|
|
146
146
|
1. Strip extension (`.ts`, `.js`, `.mts`, `.mjs`)
|
|
147
|
-
2. Strip env suffix (`.dev`, `.prod`)
|
|
147
|
+
2. Strip env suffix (`.dev`, `.prod`) and record it on `RouteDefinition.env`
|
|
148
148
|
3. Strip trailing `index`
|
|
149
149
|
4. Remove route group dirs `(groupname)`
|
|
150
150
|
5. Convert `[param]` → `:param`, `[...param]` → `:param{.+}`, `[...]` → `*`
|
|
@@ -152,6 +152,8 @@ Applied in order to a path relative to `routes/`:
|
|
|
152
152
|
|
|
153
153
|
HTTP methods are determined by **named exports** in each file (detected by `detect-exports.ts` via `parseSync`), not by filename suffixes. Files/dirs starting with `_` are ignored by the scanner.
|
|
154
154
|
|
|
155
|
+
The recorded `env` is enforced by `isRouteEnabledForEnvironment()`. The scanners take an optional `environment` (`'dev' | 'prod'`); when it is omitted no filtering happens. `dev` is the Vite dev server, `prod` is every build, `void deploy`, and `vite preview`. A dev-only route is absent from the production bundle, types, and generated route/WebSocket config. Two paths do not follow it: `void prepare` boots no Vite, passes no `environment`, and so generates types for both environments; and `inferProjectBindings()` (`plugin-inference.ts`) globs source files without parsing route filenames, so a `void/storage` import in a `.dev.ts` route still emits and provisions an R2 binding in production. Over-emitting is the deliberate safe direction — under-emitting would drop a binding the running code needs — and the environment is not knowable at the `inferProjectBindings()` call site anyway (`index.ts:678`, versus `routeEnvironment` at `index.ts:1324`). The suffix is honoured in `routes/` only, not in `pages/`, `middleware/`, `crons/`, or `queues/`.
|
|
156
|
+
|
|
155
157
|
## Code Generation (`void gen`)
|
|
156
158
|
|
|
157
159
|
Rails/Laravel-style scaffolding commands for common boilerplate:
|
|
@@ -103,7 +103,7 @@ Apply these filename rules exactly when mapping old handlers to `routes/`:
|
|
|
103
103
|
1. Extension and suffix parsing order
|
|
104
104
|
|
|
105
105
|
- Strip extension (`.ts`, `.js`, `.mts`, `.mjs`).
|
|
106
|
-
- Strip env suffix (`.dev`, `.prod`).
|
|
106
|
+
- Strip env suffix (`.dev`, `.prod`). The suffix restricts the route to that environment, so only use it for handlers that must not ship to the other one.
|
|
107
107
|
- Strip HTTP method suffix (`.get`, `.post`, `.put`, `.delete`, `.patch`).
|
|
108
108
|
- Strip trailing `index` segment.
|
|
109
109
|
- Remove route group segments `(group-name)` from URL path.
|
|
@@ -271,6 +271,30 @@ For Drizzle projects, deploy performs a read-only schema drift check. If a new m
|
|
|
271
271
|
|
|
272
272
|
Every deploy writes a structured JSONL trace to `~/.void/logs/deploy-<timestamp>.jsonl` regardless of `--debug`. On failure the path is printed at the end of the error message so you can attach it when reporting platform issues. `VOID_DEPLOY_DEBUG=1` is accepted as an alternate trigger for stderr mirroring.
|
|
273
273
|
|
|
274
|
+
When a deploy fails after it starts, the CLI also prints a summary of that trace under the error, so the cause is visible where the file is not — a CI runner, for example, is discarded with the job. The summary has two blocks: every `error` record with its flattened cause chain, then the last 20 records as a timeline.
|
|
275
|
+
|
|
276
|
+
Pre-flight failures print no summary. A missing project, a rejected flag combination, or an unsupported `--backend cloudflare` feature stops before any trace exists, and each of those prints its own message explaining what to change. A build failure prints no summary either — the build streams its own output straight to the terminal.
|
|
277
|
+
|
|
278
|
+
Void masks the credentials it emits itself: signed query parameters, bearer tokens, and any field whose key names a credential.
|
|
279
|
+
|
|
280
|
+
Masking your own values is left to your CI platform, which holds the secrets and masks them before the log is written. GitHub Actions does this for everything under `secrets.*`. Void does not guess at credential-shaped variable names, and it does not parse credentials out of values you supplied — a password inside a `DATABASE_URL` in your build command prints as written. Register such values as CI secrets, or keep them out of the build command.
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
■ deploy: Deploy failed: deploy in progress
|
|
284
|
+
│ Deployment: dpl_7zgitxdrxxz9
|
|
285
|
+
│ Detailed log: ~/.void/logs/deploy-2026-08-21T03-24-19-764Z.jsonl
|
|
286
|
+
│
|
|
287
|
+
│ Errors
|
|
288
|
+
│ 9.0s deploy_server_error
|
|
289
|
+
│ deploymentId=dpl_7zgitxdrxxz9
|
|
290
|
+
│ message=deploy in progress
|
|
291
|
+
│
|
|
292
|
+
│ Last 20 of 26 entries
|
|
293
|
+
│ 8.4s info finalize_start assets=98 workers=0
|
|
294
|
+
│ 8.6s info stream_deployment_id deploymentId=dpl_7zgitxdrxxz9
|
|
295
|
+
│ 9.0s error deploy_server_error message=deploy in progress
|
|
296
|
+
```
|
|
297
|
+
|
|
274
298
|
Project resolution precedence:
|
|
275
299
|
|
|
276
300
|
1. `--project <name>`
|
|
@@ -254,6 +254,7 @@ Curated Cloudflare Workers configuration. Cloudflare-targeted apps require an ex
|
|
|
254
254
|
| `compatibility_date` | `string` | Cloudflare Workers compatibility date |
|
|
255
255
|
| `compatibility_flags` | `string[]` | Cloudflare Workers compatibility flags |
|
|
256
256
|
| `vars` | `object` | Plain-text worker variables |
|
|
257
|
+
| `limits` | `object` | Worker resource limit overrides |
|
|
257
258
|
|
|
258
259
|
```json
|
|
259
260
|
{
|
|
@@ -269,6 +270,18 @@ Curated Cloudflare Workers configuration. Cloudflare-targeted apps require an ex
|
|
|
269
270
|
|
|
270
271
|
`worker.vars` values must be strings. They are merged into Worker bindings before `.env` files are loaded, so project `.env` values override `worker.vars` for local dev/build. Do not put secrets here; use `env.ts` plus `void secret put` for production secrets.
|
|
271
272
|
|
|
273
|
+
`worker.limits.cpu_ms` caps Workers CPU time per request, in milliseconds. It must be an integer between 1 and 300000 (Cloudflare's hard maximum, 5 minutes). A deploy that requests more than your account plan's ceiling fails with an error naming both the requested value and the plan maximum. Only a rollback clamps: rolling back to a deployment whose configured limit now exceeds your plan applies the plan ceiling instead of failing. If your plan changes so that a previously valid value now exceeds the ceiling, deploys keep failing until you lower `cpu_ms` in `void.json`. On the self-hosted path (`void deploy --backend cloudflare`, or a bare `wrangler deploy` of the build output), the value is written into the generated `dist/ssr/wrangler.json` as `limits.cpu_ms` and enforced by Cloudflare directly. The Void plan ceiling does not apply there, but your Cloudflare account's own CPU-time allowance still does: the Workers Free plan caps CPU at 10 ms per request, and the Workers Paid plan allows up to 300000 ms. A value above what your Cloudflare account permits is constrained or rejected by Cloudflare, not by Void.
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"worker": {
|
|
278
|
+
"limits": {
|
|
279
|
+
"cpu_ms": 30000
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
272
285
|
### `routing`
|
|
273
286
|
|
|
274
287
|
Routing and edge configuration for headers, redirects, rewrites, and caching.
|
|
@@ -378,6 +391,26 @@ Paths to prerender as static HTML at deploy time. Each path must start with `/`.
|
|
|
378
391
|
}
|
|
379
392
|
```
|
|
380
393
|
|
|
394
|
+
#### `routing.notFound`
|
|
395
|
+
|
|
396
|
+
Override how the asset layer answers a request that matched no asset and no worker route. One of `"single-page-application"`, `"404-page"`, or `"none"`. If omitted, Void infers it — see [Static Assets](../guide/edge/static-assets.md#unmatched-requests).
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"routing": {
|
|
401
|
+
"notFound": "404-page"
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Set `"404-page"` when your assets come from a static site generator (VitePress, Docusaurus, …) and you want unknown URLs to serve the generator's own `404.html` with a real HTTP 404. Without it, an app whose only backend is API routes keeps the inferred SPA fallback, so every unknown URL returns `index.html` with a `200` — correct for a single-page app, wrong for a generated site, and bad for crawlers.
|
|
407
|
+
|
|
408
|
+
`run_worker_first` is untouched, so API routes, auth, and `/__void/*` still reach the worker first. When the worker owns HTML routing it also serves the resolved behavior itself, since `run_worker_first: ["/**"]` means the platform switch never runs — see [Unmatched requests](../guide/edge/static-assets.md#unmatched-requests).
|
|
409
|
+
|
|
410
|
+
This does **not** apply to SvelteKit, Nuxt, Analog, or Astro deploys: those pin `not_found_handling` to `"none"` because the framework's own worker owns unmatched HTML, and `void deploy` warns if you set `routing.notFound` anyway.
|
|
411
|
+
|
|
412
|
+
TanStack Start, React Router, and vinext honor it on a managed `void deploy`, which resolves the asset config itself. Void writes no `assets` policy into their generated worker wrangler config (`dist/server` for TanStack Start and vinext App, `build/server` for React Router, `dist/ssr` for vinext Pages), so on a self-hosted `wrangler deploy` it applies only if that config declares a complete `assets` policy of its own — `binding`, `directory`, `not_found_handling`, and `run_worker_first`. Void leaves those fields untouched, so your own policy is honored; with none, Cloudflare's default applies. `vite build` warns when it is set. See [Static Assets](../guide/edge/static-assets.md#unmatched-requests) for the per-framework paths.
|
|
413
|
+
|
|
381
414
|
### `inference`
|
|
382
415
|
|
|
383
416
|
Configuration for build-time inference, including how Void detects your app type, bindings, and build process.
|
|
@@ -403,12 +436,20 @@ Custom names are used in the deploy manifest, Wrangler config generation/sync, r
|
|
|
403
436
|
|
|
404
437
|
#### `inference.build`
|
|
405
438
|
|
|
406
|
-
Override the build command. Useful for frameworks with their own CLIs (Nuxt, Astro) or static apps with custom build scripts. If omitted, the CLI uses the detected default build command for the current app type.
|
|
439
|
+
Override the build command. Useful for frameworks with their own CLIs (Nuxt, Astro) or static apps with custom build scripts. If omitted, the CLI uses the detected default build command for the current app type. It applies to every app type, including `"void"` (where the default is `vite build`).
|
|
407
440
|
|
|
408
441
|
```json
|
|
409
442
|
{ "inference": { "build": "nuxt build --preset cloudflare-module" } }
|
|
410
443
|
```
|
|
411
444
|
|
|
445
|
+
The command is run through a shell from the project root, with the project's own `node_modules/.bin` on `PATH`, so bare binaries and `&&` chains work:
|
|
446
|
+
|
|
447
|
+
```json
|
|
448
|
+
{ "inference": { "build": "vitepress build && vite build" } }
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
That chain is how a static site generator and a Void backend ship together — see [Adding a backend to a static site](../guide/app-types.md#adding-a-backend-to-a-static-site).
|
|
452
|
+
|
|
412
453
|
#### `inference.scanDirs`
|
|
413
454
|
|
|
414
455
|
Override which directories are scanned for [binding inference](./resource-inference.md). Paths are relative to the project root. Replaces the default directories for the current mode.
|
|
@@ -80,6 +80,7 @@ File-based HTTP API endpoints built on [Hono](https://hono.dev). Each file expor
|
|
|
80
80
|
- **Catch-all**: `[...slug]` matches the remaining path
|
|
81
81
|
- **Route groups**: `(admin)/` organizes files without affecting URL paths
|
|
82
82
|
- **Files starting with `_`** are ignored
|
|
83
|
+
- **Environment suffix**: `debug.dev.ts` builds in development only, `metrics.prod.ts` in production only
|
|
83
84
|
|
|
84
85
|
See [Server Routing](/guide/server-routing) for the full guide.
|
|
85
86
|
|