void 1.2.1 → 1.4.0
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/{account-cmd-CdUll_35.mjs → account-cmd-HP8bixYo.mjs} +3 -3
- package/dist/{application-analysis-fEqoSeMp.mjs → application-analysis-DDOQwiXk.mjs} +18 -12
- package/dist/{application-routing-DAhNP4u3.mjs → application-routing-4719u_kf.mjs} +3 -1
- package/dist/auth-g2TQbDLD.d.mts +70 -0
- package/dist/{auth-link-C87vGXM-.mjs → auth-link-CedYpDCy.mjs} +3 -3
- package/dist/{auth-router-6I81r-ix.mjs → auth-router-vafo7x8w.mjs} +11 -6
- package/dist/{better-auth-shared-D-6z0q0f.mjs → better-auth-shared-7FM3Uk2N.mjs} +48 -2
- package/dist/{better-auth-shared-DVk2DkTW.d.mts → better-auth-shared-BHB8kiTE.d.mts} +1 -1
- package/dist/{build-cmd-ex1g-j_e.mjs → build-cmd-9vUAQ1_V.mjs} +1 -1
- package/dist/{cache-oHzrnY3z.mjs → cache-jvwLNb0h.mjs} +1 -1
- package/dist/{cancel-deploy-viAMD0Q_.mjs → cancel-deploy-Dw0UmWKO.mjs} +1 -1
- package/dist/{cf-build-output-BhVxldCw.mjs → cf-build-output-XpNR9zoh.mjs} +122 -5
- package/dist/cf-build-output-bd8Uwv1P.mjs +1 -0
- package/dist/{cf-worker-settings-BECvvTVd.mjs → cf-worker-settings-Dg27RzyT.mjs} +11 -5
- package/dist/cli/cli.mjs +41 -32
- package/dist/cli/cloudflare-operation-process.mjs +35 -8
- package/dist/cli/env-schema-probe.mjs +2 -2
- package/dist/{client-BEkdj1kx.mjs → client-B3LnB_bE.mjs} +8 -1
- package/dist/client-BZb9j66F.mjs +1 -0
- package/dist/{cloudflare-auth-Byo_0_N9.mjs → cloudflare-auth-DyD_kVFr.mjs} +1 -1
- package/dist/{cloudflare-cmd-DMzCv12s.mjs → cloudflare-cmd-D-b_u3T4.mjs} +2 -2
- package/dist/{cloudflare-config-DchHxPTo.mjs → cloudflare-config-CH1AylZT.mjs} +14 -3
- package/dist/{cloudflare-connect-CCRTVg-I.mjs → cloudflare-connect-zTJh8UnN.mjs} +2 -2
- package/dist/{cloudflare-operations-Dh9dEHJj.mjs → cloudflare-operations-Bc-7fN44.mjs} +4 -4
- package/dist/{cloudflare-operations-DsSaw2BT.mjs → cloudflare-operations-BtulCNOg.mjs} +1 -1
- package/dist/{config-BU6hMu1W.mjs → config-BV2ASlpn.mjs} +1 -1
- package/dist/{config-DQfHOKh9.d.mts → config-CAdZVTLN.d.mts} +4 -1
- package/dist/config-D-6q7EBH.mjs +87 -0
- package/dist/{config-DHgSEWT7.mjs → config-OFOAimzt.mjs} +5 -3
- package/dist/config-entry.d.mts +1 -1
- package/dist/{config-BIaXIkkv.mjs → config-tqMM6DwY.mjs} +1 -1
- package/dist/{connect-k7R2rPVA.mjs → connect-BOM6zvFX.mjs} +4 -4
- package/dist/{create-project-DV71ol5d.mjs → create-project-Bxd02EHN.mjs} +1 -1
- package/dist/{create-project-DTuy-l0z.mjs → create-project-C2K0DN2O.mjs} +1 -1
- package/dist/{db-DVOkLH5X.mjs → db-Cdvr3zwG.mjs} +18 -18
- package/dist/{db-types-NGxPwnlt.mjs → db-types-0eCiqYb1.mjs} +1 -1
- package/dist/{delete-BbP5eopu.mjs → delete-lETE6Tpt.mjs} +1 -1
- package/dist/deploy-CX94CmKa.mjs +2 -0
- package/dist/{deploy-7H32EnUs.mjs → deploy-CdM-LAxX.mjs} +473 -148
- package/dist/{deploy-managed-static-DlvcvnQT.mjs → deploy-managed-static-DjeBFG2c.mjs} +43 -20
- package/dist/{deploy-transport-BzGEj0Ge.mjs → deploy-transport-DIq_gFTP.mjs} +14 -13
- package/dist/deploy.d.mts +2 -0
- package/dist/deploy.mjs +8 -5
- package/dist/{dispatch-DoUnxTNo.mjs → dispatch-CLa2LThw.mjs} +76 -7
- package/dist/{domain-CA2wBgra.mjs → domain-DyhNtdYf.mjs} +2 -2
- package/dist/{email-DXpCc77w.mjs → email-DM2wDcBN.mjs} +59 -10
- package/dist/{email-cloudflare-D03g2TkS.mjs → email-cloudflare-PMbAev9Q.mjs} +124 -10
- package/dist/{env-DgOQ9_8S.mjs → env-78hbN-oI.mjs} +3 -3
- package/dist/{env-BJjULvL0.mjs → env-BaMz0j7H.mjs} +6 -1
- package/dist/{env-validation-Dwq3PJzK.mjs → env-validation-B3VobvXm.mjs} +2 -1
- package/dist/{env-validation-BmsdCZEk.mjs → env-validation-BUfFZ6bt.mjs} +1 -1
- package/dist/error-pages-CLIJ7aYq.mjs +37 -0
- package/dist/gen-BhWBT4iu.mjs +1 -0
- package/dist/{gen-BfmcIi2g.mjs → gen-CFmsUsW1.mjs} +3 -3
- package/dist/{github-cmd-CY9LUqLh.mjs → github-cmd-De-_IWbM.mjs} +1 -1
- package/dist/{handler-BbkSPzGF.d.mts → handler-BHktmuTL.d.mts} +4 -2
- package/dist/{headers-D522zd0A.mjs → headers--vaK_chu.mjs} +1 -1
- package/dist/{help-CxSlPpzJ.mjs → help-CKbrhZha.mjs} +16 -1
- package/dist/help-qOZr3wng.mjs +1 -0
- package/dist/index.d.mts +3 -5
- package/dist/index.mjs +370 -140
- package/dist/{info-7VDlhtyy.mjs → info-Yg0-V5Rs.mjs} +4 -4
- package/dist/{init-DxYyOs4-.mjs → init-CDiJQycf.mjs} +241 -206
- package/dist/{link-Ddmod1dZ.mjs → link-Dllip08S.mjs} +2 -2
- package/dist/{list-MogaugiO.mjs → list-BYRP6am0.mjs} +2 -2
- package/dist/{live-Bt8Y_0bg.d.mts → live-BllfDuzA.d.mts} +2 -2
- package/dist/{local-d1-BHBnKWRb.mjs → local-d1-CAwAs9GH.mjs} +1 -1
- package/dist/login-BAPmYnb7.mjs +1 -0
- package/dist/{login-Cttnagmm.mjs → login-PcFy5mlC.mjs} +2 -2
- package/dist/{logs-CAszUWZI.mjs → logs-DmJTBhed.mjs} +2 -2
- package/dist/{migrate-DyT2jJ8y.mjs → migrate-B8S2xTcb.mjs} +5 -66
- package/dist/migrate-BIYTPQE6.mjs +1 -0
- package/dist/nitro.mjs +4 -4
- package/dist/{node-WB7c2N4L.mjs → node-CDOPrGSl.mjs} +5 -5
- package/dist/{operator-args-UnE8Y2eK.mjs → operator-args-BsJIYXaT.mjs} +8 -1
- package/dist/operator-cmd-CjazwOyb.mjs +1 -0
- package/dist/{operator-cmd-skshjMGl.mjs → operator-cmd-DmkmS2wL.mjs} +4 -3
- package/dist/pages/client.d.mts +32 -4
- package/dist/pages/client.mjs +186 -29
- package/dist/pages/index.d.mts +34 -2
- package/dist/pages/index.mjs +96 -55
- package/dist/pages/islands-plugin.d.mts +1 -1
- package/dist/pages/islands-plugin.mjs +9 -3
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/platform-args-BC8_hsDj.mjs +1 -0
- package/dist/{platform-args-DVbVClZl.mjs → platform-args-BMD2gjc_.mjs} +1 -1
- package/dist/{platform-auth-config-C-pm5Qzx.mjs → platform-auth-config-CImnfohz.mjs} +3 -3
- package/dist/{platform-auth-protection-SHhvim1L.mjs → platform-auth-protection-BogLhtHi.mjs} +2 -2
- package/dist/{platform-auth-recovery-DXVWs-wA.mjs → platform-auth-recovery-B-mEaWEi.mjs} +2 -2
- package/dist/{platform-cmd-PiNRkopU.mjs → platform-cmd-CV8NxIIn.mjs} +1 -1
- package/dist/{platform-cmd-D5I1beUX.mjs → platform-cmd-DZK2ixMl.mjs} +4 -4
- package/dist/{platform-domain-jpmBfZeb.mjs → platform-domain-CRvelPGE.mjs} +2 -2
- package/dist/{platform-lifecycle-DyKmuNY6.mjs → platform-lifecycle-CPYY7E9B.mjs} +103 -41
- package/dist/{platform-lifecycle-DH3Yr1qQ.mjs → platform-lifecycle-DwXq7kZt.mjs} +1 -1
- package/dist/{platform-management--vDwSn35.mjs → platform-management-BOnGbMSM.mjs} +1 -1
- package/dist/{platform-management-7m-PLaPJ.mjs → platform-management-lXdgGgAM.mjs} +1 -1
- package/dist/{platform-recovery-BiNpZZab.mjs → platform-recovery-noaALXnE.mjs} +2 -2
- package/dist/{plugin-inference-DUjCd55H.mjs → plugin-inference-CeiB6BG5.mjs} +53 -18
- package/dist/{prepare-BcAE5Sdj.mjs → prepare-BuOS18AM.mjs} +1 -1
- package/dist/{prepare-DA-17DhO.mjs → prepare-D604Yiyr.mjs} +12 -8
- package/dist/prepare-DU-tOpS5.mjs +1 -0
- package/dist/{prerender-render-Cijv7UFo.mjs → prerender-render-CaShbHJ3.mjs} +13 -1
- package/dist/prerender-render.d.mts +1 -0
- package/dist/prerender-render.mjs +1 -1
- package/dist/{project-cmd-DA4K5faU.mjs → project-cmd-DBYqAIbe.mjs} +15 -15
- package/dist/{project-team-CQBNveX3.mjs → project-team-BjCLgTRC.mjs} +1 -1
- package/dist/{project-token-CCalFyQf.mjs → project-token-BB6PwaA9.mjs} +1 -1
- package/dist/{project-tsconfig-Itv1f4WC.mjs → project-tsconfig-CVvk6Z_e.mjs} +24 -2
- package/dist/{project-zero-trust-DPnkiEuT.mjs → project-zero-trust-CWDh1P_t.mjs} +1 -1
- package/dist/{protocol-BBDpJz3G.d.mts → protocol-D5XDySYu.d.mts} +2 -2
- package/dist/{provision-BFLCrnUp.mjs → provision-DfxLCg-d.mjs} +1 -1
- package/dist/{provision-DtROpNNI.mjs → provision-DztLJ97X.mjs} +24 -8
- package/dist/{requests-BpH0kImL.mjs → requests-D_pbzDyz.mjs} +1 -1
- package/dist/{rollback-CXS1tAa-.mjs → rollback-CjfIYsJx.mjs} +2 -2
- package/dist/{route-types-BZi26DVS.mjs → route-types-DvEGmPif.mjs} +109 -30
- package/dist/routes-stub.d.mts +22 -13
- package/dist/runtime/auth.d.mts +2 -2
- package/dist/runtime/better-auth-mysql.d.mts +1 -1
- package/dist/runtime/better-auth-mysql.mjs +1 -1
- package/dist/runtime/better-auth-pg.d.mts +1 -1
- package/dist/runtime/better-auth-pg.mjs +1 -1
- package/dist/runtime/better-auth.d.mts +1 -1
- package/dist/runtime/better-auth.mjs +1 -1
- package/dist/runtime/client-react.d.mts +1 -1
- package/dist/runtime/client-solid.d.mts +1 -1
- package/dist/runtime/client-svelte.d.mts +1 -1
- package/dist/runtime/client-vue.d.mts +1 -1
- package/dist/runtime/client.d.mts +1 -1
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/live-client.d.mts +1 -1
- package/dist/runtime/live.d.mts +1 -1
- package/dist/runtime/log.mjs +19 -1
- package/dist/runtime/routing.d.mts +5 -4
- package/dist/runtime/routing.mjs +14 -73
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +3 -3
- package/dist/runtime/ws.d.mts +1 -1
- package/dist/runtime/ws.mjs +4 -2
- package/dist/{scan-8IzYXiU_.mjs → scan-N9WzdcAl.mjs} +302 -23
- package/dist/{secret-BbXHfLAt.mjs → secret-3X0PxmuX.mjs} +9 -12
- package/dist/static-asset-marker-Btf-6oCD.mjs +32 -0
- package/dist/{types-7AIz0OZu.d.mts → types-w72dljaA.d.mts} +6 -1
- package/dist/{version-metadata-tS39iW09.mjs → version-metadata-D2vfMQzv.mjs} +2 -1
- package/dist/worker-conditions-AWFRcFxR.mjs +11 -0
- package/dist/{wrangler-F96NnLyb.mjs → wrangler-I-scB_1t.mjs} +82 -13
- package/dist/wrangler-UnwdISay.mjs +1 -0
- package/dist/{wrapper-D3sZyh2C.mjs → wrapper-BBl1rC32.mjs} +8 -12
- package/dist/{ws-BH-im_tX.d.mts → ws-B3ClMVDh.d.mts} +3 -2
- package/docs/guide/auth.md +4 -2
- package/docs/guide/edge/redirects.md +22 -0
- package/docs/guide/edge/static-assets.md +10 -0
- package/docs/guide/email/domains.md +1 -1
- package/docs/guide/email/receiving.md +2 -0
- package/docs/guide/email/sending.md +1 -1
- package/docs/guide/pages-routing/actions-and-forms.md +43 -13
- package/docs/guide/pages-routing/markdown.md +2 -0
- package/docs/guide/pages-routing/overview.md +33 -2
- package/docs/guide/platform/administration/email.md +18 -0
- package/docs/guide/platform/installation/maintenance.md +3 -1
- package/docs/guide/platform/installation/prerequisites.md +9 -4
- package/docs/guide/platform/installation/uninstall.md +2 -0
- package/docs/guide/queues.md +13 -1
- package/docs/guide/server-routing.md +40 -2
- package/docs/guide/ssg.md +8 -0
- package/docs/guide/type-safety.md +7 -7
- package/docs/guide/websockets.md +4 -2
- package/docs/integrations/cloudflare.md +16 -7
- package/docs/reference/api/auth.md +6 -2
- package/docs/reference/api/deploy.md +9 -5
- package/docs/reference/api/pages.md +6 -4
- package/docs/reference/cli/deploy.md +10 -9
- package/docs/reference/cli/email.md +13 -0
- package/docs/reference/cli/platform-email.md +2 -2
- package/docs/reference/cli.md +1 -0
- package/docs/reference/config.md +11 -1
- package/docs/reference/structure.md +3 -1
- package/package.json +9 -7
- package/schema.json +4 -0
- package/skills/void/SKILL.md +1 -1
- package/templates/assets/icons.svg +49 -0
- package/templates/assets/react.svg +1 -0
- package/templates/assets/solid.svg +1 -0
- package/templates/assets/stack.svg +35 -0
- package/templates/assets/svelte.svg +1 -0
- package/templates/assets/void.svg +5 -0
- package/templates/assets/vue.svg +1 -0
- package/dist/auth-DkcFflXV.d.mts +0 -42
- package/dist/cf-build-output-BJHuSsVN.mjs +0 -1
- package/dist/client-BiRDZTSd.mjs +0 -1
- package/dist/config-CyUJzi3q.mjs +0 -32
- package/dist/deploy-CE0Ev0Ob.mjs +0 -2
- package/dist/gen-NrOlWZaG.mjs +0 -1
- package/dist/help-BeAFFE5C.mjs +0 -1
- package/dist/login-DgDwlKc_.mjs +0 -1
- package/dist/migrate-n49ZeS1X.mjs +0 -1
- package/dist/operator-cmd-d2MvJOrl.mjs +0 -1
- package/dist/parse-filename-DioPHiR9.mjs +0 -77
- package/dist/platform-args-kQrgxScY.mjs +0 -1
- package/dist/prepare-C4I0f--h.mjs +0 -1
- package/dist/wrangler-BLb3_luP.mjs +0 -1
|
@@ -1,12 +1,68 @@
|
|
|
1
1
|
import { a as join, c as resolve, n as dirname, o as normalize, t as basename } from "./dist-ipQi0Q8y.mjs";
|
|
2
|
-
import "./project-paths-DGu_QZ_Q.mjs";
|
|
3
|
-
import { p as readConfig } from "./config-
|
|
4
|
-
import {
|
|
2
|
+
import { o as TYPED_CONFIG_FILE, s as loadTypedVoidConfigFile } from "./project-paths-DGu_QZ_Q.mjs";
|
|
3
|
+
import { h as validateConfig, p as readConfig } from "./config-OFOAimzt.mjs";
|
|
4
|
+
import { a as persistCloudflareConfig, i as materializeCloudflareConfig, r as ignoreMaterializedCloudflareConfig } from "./cloudflare-config-CH1AylZT.mjs";
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { parseSync } from "vite";
|
|
6
7
|
import { appendSqliteClassMigration, isRecord } from "@void/deploy-core";
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
9
|
import { isDeepStrictEqual } from "node:util";
|
|
9
10
|
import { SyntaxKind, applyEdits, createScanner, findNodeAtLocation, modify, parse, parseTree } from "jsonc-parser";
|
|
11
|
+
//#region src/config-write.ts
|
|
12
|
+
const GENERATED_MARKERS = [
|
|
13
|
+
"// Generated by void migrate.",
|
|
14
|
+
"// Generated from void.json by void migrate.",
|
|
15
|
+
"// Generated by void init."
|
|
16
|
+
];
|
|
17
|
+
function renderVoidConfig(config, marker = GENERATED_MARKERS[0]) {
|
|
18
|
+
return `${marker}\nimport { defineConfig } from 'void/config';\n\nexport default defineConfig(${JSON.stringify(config, null, 2)});\n`;
|
|
19
|
+
}
|
|
20
|
+
function isStaticValue(node) {
|
|
21
|
+
if (!node) return false;
|
|
22
|
+
if (node.type === "Literal") return node.value === null || [
|
|
23
|
+
"string",
|
|
24
|
+
"number",
|
|
25
|
+
"boolean"
|
|
26
|
+
].includes(typeof node.value);
|
|
27
|
+
if (node.type === "UnaryExpression") return node.operator === "-" && node.argument?.type === "Literal" && typeof node.argument.value === "number";
|
|
28
|
+
if (node.type === "ArrayExpression") return node.elements?.every(isStaticValue) ?? false;
|
|
29
|
+
if (node.type !== "ObjectExpression") return false;
|
|
30
|
+
return node.properties?.every((property) => property.type === "Property" && property.kind === "init" && !property.computed && !property.method && (property.key?.type === "Identifier" || property.key?.type === "Literal") && isStaticValue(property.value)) ?? false;
|
|
31
|
+
}
|
|
32
|
+
function isGeneratedStaticConfig(source) {
|
|
33
|
+
if (!GENERATED_MARKERS.some((marker) => source.startsWith(marker))) return false;
|
|
34
|
+
let program;
|
|
35
|
+
try {
|
|
36
|
+
const parsed = parseSync(TYPED_CONFIG_FILE, source, {
|
|
37
|
+
lang: "js",
|
|
38
|
+
sourceType: "module",
|
|
39
|
+
preserveParens: false,
|
|
40
|
+
showSemanticErrors: true
|
|
41
|
+
});
|
|
42
|
+
if (parsed.errors.length > 0 || parsed.comments.length !== 1 || !GENERATED_MARKERS.includes(`// ${parsed.comments[0]?.value.trim()}`)) return false;
|
|
43
|
+
program = parsed.program;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const [importDeclaration, exportDeclaration] = program.body ?? [];
|
|
48
|
+
const call = exportDeclaration?.declaration;
|
|
49
|
+
return program.body?.length === 2 && importDeclaration?.type === "ImportDeclaration" && importDeclaration.source?.value === "void/config" && importDeclaration.specifiers?.length === 1 && importDeclaration.specifiers[0]?.imported?.name === "defineConfig" && importDeclaration.specifiers[0]?.local?.name === "defineConfig" && exportDeclaration?.type === "ExportDefaultDeclaration" && call?.type === "CallExpression" && call.callee?.name === "defineConfig" && call.arguments?.length === 1 && isStaticValue(call.arguments[0]);
|
|
50
|
+
}
|
|
51
|
+
function canRewriteVoidProjectConfig(root) {
|
|
52
|
+
const path = join(root, TYPED_CONFIG_FILE);
|
|
53
|
+
return !existsSync(path) || isGeneratedStaticConfig(readFileSync(path, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
/** Update CLI-owned static config without silently rewriting authored TypeScript. */
|
|
56
|
+
function writeVoidProjectConfig(root, config) {
|
|
57
|
+
validateConfig(config, TYPED_CONFIG_FILE);
|
|
58
|
+
const path = join(root, TYPED_CONFIG_FILE);
|
|
59
|
+
if (existsSync(path)) {
|
|
60
|
+
if (!isGeneratedStaticConfig(readFileSync(path, "utf8"))) throw new Error(`config: ${TYPED_CONFIG_FILE} contains authored TypeScript or comments. Update it directly and rerun the command.`);
|
|
61
|
+
validateConfig(loadTypedVoidConfigFile(path), TYPED_CONFIG_FILE);
|
|
62
|
+
}
|
|
63
|
+
writeFileSync(path, renderVoidConfig(config, "// Generated by void init."));
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
10
66
|
//#region src/cli/wrangler.ts
|
|
11
67
|
const COMPAT_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
68
|
/** UTF-8 byte-order mark. Node's utf-8 read preserves a leading one. */
|
|
@@ -170,10 +226,15 @@ function resolveGeneratedName(base, used, kind) {
|
|
|
170
226
|
function readWranglerCompat(root, config, buildOutputDir) {
|
|
171
227
|
return resolveWranglerCompat(readWranglerCompatFields(root, config, buildOutputDir));
|
|
172
228
|
}
|
|
173
|
-
/**
|
|
174
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Like `readWranglerCompat()`, but pins the default date when no source sets one.
|
|
231
|
+
* With `rootWrangler`, a project with no config gets the date in a root wrangler.jsonc
|
|
232
|
+
* instead of a new void.config.ts, so a framework adapter that reads the root file by
|
|
233
|
+
* default still finds it.
|
|
234
|
+
*/
|
|
235
|
+
function ensureWranglerCompat(root, config, buildOutputDir, options) {
|
|
175
236
|
const fields = readWranglerCompatFields(root, config, buildOutputDir);
|
|
176
|
-
const defaultDate = fields.voidDate ?? fields.wranglerDate ?? fields.buildDate ? void 0 : ensureVoidCompatibilityDate(root,
|
|
237
|
+
const defaultDate = fields.voidDate ?? fields.wranglerDate ?? fields.buildDate ? void 0 : ensureVoidCompatibilityDate(root, options?.rootWrangler === true);
|
|
177
238
|
return resolveWranglerCompat(fields, defaultDate ? { defaultDate } : void 0);
|
|
178
239
|
}
|
|
179
240
|
function readWranglerCompatFields(root, config, buildOutputDir) {
|
|
@@ -205,7 +266,6 @@ function readWranglerCompatFields(root, config, buildOutputDir) {
|
|
|
205
266
|
if (Array.isArray(buildParsed.compatibility_flags) && buildParsed.compatibility_flags.every((f) => typeof f === "string")) buildFlags = buildParsed.compatibility_flags;
|
|
206
267
|
} catch {}
|
|
207
268
|
return {
|
|
208
|
-
voidConfig,
|
|
209
269
|
voidDate: voidConfig?.worker?.compatibility_date,
|
|
210
270
|
voidFlags: voidConfig?.worker?.compatibility_flags,
|
|
211
271
|
wranglerDate,
|
|
@@ -226,14 +286,23 @@ function validateCompatDate(value, source) {
|
|
|
226
286
|
function readVoidConfigIfExists(root) {
|
|
227
287
|
return existsSync(join(root, VOID_CONFIG_FILE)) || existsSync(join(root, "void.config.ts")) ? readConfig(root) : void 0;
|
|
228
288
|
}
|
|
229
|
-
function ensureVoidCompatibilityDate(root,
|
|
289
|
+
function ensureVoidCompatibilityDate(root, rootWrangler) {
|
|
230
290
|
if (existsSync(join(root, "void.config.ts"))) {
|
|
231
291
|
writeProvisionedWranglerConfig(root, { compatibilityDate: LATEST_KNOWN_COMPAT_DATE });
|
|
232
292
|
return LATEST_KNOWN_COMPAT_DATE;
|
|
233
293
|
}
|
|
234
294
|
const filePath = join(root, VOID_CONFIG_FILE);
|
|
235
|
-
|
|
236
|
-
|
|
295
|
+
if (!existsSync(filePath)) {
|
|
296
|
+
if (existsSync(join(root, "wrangler.toml"))) writeFileSync(filePath, `${JSON.stringify({ worker: { compatibility_date: LATEST_KNOWN_COMPAT_DATE } }, null, 2)}\n`);
|
|
297
|
+
else if (rootWrangler || projectWranglerPaths(root).some((path) => existsSync(path))) writeProvisionedWranglerConfig(root, { compatibilityDate: LATEST_KNOWN_COMPAT_DATE });
|
|
298
|
+
else {
|
|
299
|
+
writeVoidProjectConfig(root, { worker: { compatibility_date: LATEST_KNOWN_COMPAT_DATE } });
|
|
300
|
+
ignoreMaterializedCloudflareConfig(root);
|
|
301
|
+
}
|
|
302
|
+
return LATEST_KNOWN_COMPAT_DATE;
|
|
303
|
+
}
|
|
304
|
+
let nextConfig;
|
|
305
|
+
try {
|
|
237
306
|
const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
238
307
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`config: ${VOID_CONFIG_FILE} must be a JSON object.`);
|
|
239
308
|
nextConfig = parsed;
|
|
@@ -241,7 +310,6 @@ function ensureVoidCompatibilityDate(root, config) {
|
|
|
241
310
|
if (error instanceof SyntaxError) throw new Error(`config: Invalid JSON in ${VOID_CONFIG_FILE}.`);
|
|
242
311
|
throw error;
|
|
243
312
|
}
|
|
244
|
-
else if (config) nextConfig = { ...config };
|
|
245
313
|
nextConfig.worker = {
|
|
246
314
|
...nextConfig.worker ?? {},
|
|
247
315
|
compatibility_date: LATEST_KNOWN_COMPAT_DATE
|
|
@@ -666,9 +734,10 @@ function writeProvisionedWranglerConfig(root, patch, options) {
|
|
|
666
734
|
const matches = [];
|
|
667
735
|
for (let i = 0; i < currentList.length; i++) if (currentList[i].binding === entry.binding) matches.push(i);
|
|
668
736
|
if (matches.length > 1) throw new Error(`deploy: cannot update "${configPath}" — queue producer binding ${JSON.stringify(entry.binding)} is declared ${matches.length} times across producers. A producer binding must map to exactly one queue, so wrangler rejects this config. Remove the duplicate producers and retry.`);
|
|
737
|
+
const voidOwned = reconcileManagedQueues && managedProducerBindings.has(String(entry.binding));
|
|
669
738
|
if (matches.length === 1) {
|
|
670
739
|
const existing = currentList[matches[0]];
|
|
671
|
-
if (existing.queue !== entry.queue) throw new Error(`deploy: cannot update "${configPath}" — queue producer binding ${JSON.stringify(entry.binding)} is already bound to queue ${JSON.stringify(existing.queue)}, but queue ${JSON.stringify(entry.queue)} also requests it. A producer binding must map to exactly one queue. Rename or remove the conflicting producer and retry.`);
|
|
740
|
+
if (existing.queue !== entry.queue && !voidOwned) throw new Error(`deploy: cannot update "${configPath}" — queue producer binding ${JSON.stringify(entry.binding)} is already bound to queue ${JSON.stringify(existing.queue)}, but queue ${JSON.stringify(entry.queue)} also requests it. A producer binding must map to exactly one queue. Rename or remove the conflicting producer and retry.`);
|
|
672
741
|
}
|
|
673
742
|
index = matches.length === 1 ? matches[0] : -1;
|
|
674
743
|
workerBindingOwners ??= collectWorkerBindingNames(parseJsonc(text));
|
|
@@ -1399,4 +1468,4 @@ function normalizeMigrationsPath(p) {
|
|
|
1399
1468
|
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
1400
1469
|
}
|
|
1401
1470
|
//#endregion
|
|
1402
|
-
export { syncWranglerWebSockets as A, readWranglerManagedQueueCleanupCandidates as C, sameMigrationsDir as D, resolveLiveBindingConfig as E, writeProvisionedWranglerConfig as M, writeWranglerPublishedTrafficSnapshot as N, syncWranglerBindings as O, readWranglerCustomDomains as S, readWranglerWebSocketTombstones as T, persistMaterializedWranglerConfig as _, LIVE_MIGRATION_TAG as a, readRootWranglerConfig as b, bindingEntriesForField as c, ensureWranglerCompatibilityDate as d, ensureWranglerNodejsAlsFlag as f, normalizeWranglerCompatibilityFlags as g, nextDurableObjectMigrationTag as h, LATEST_KNOWN_COMPAT_DATE as i, updateWranglerCustomDomain as j, syncWranglerDurableObjects as k, bindingNamesForField as l, hasDurableObjectSqliteMigration as m, DEFAULT_LIVE_CLASS as n, acknowledgeWranglerManagedQueueRemovals as o, findBindingNameOwner as p, FRIENDLY_BINDING_NAMES as r, assertNoDuplicateMembers as s, DEFAULT_LIVE_BINDING as t, ensureWranglerCompat as u, readExistingLiveBindingConfig as v, readWranglerPublishedTrafficSnapshot as w, readWranglerCompat as x, readProvisionedBindingEntry as y };
|
|
1471
|
+
export { syncWranglerWebSockets as A, readWranglerManagedQueueCleanupCandidates as C, sameMigrationsDir as D, resolveLiveBindingConfig as E, canRewriteVoidProjectConfig as F, renderVoidConfig as I, writeVoidProjectConfig as L, writeProvisionedWranglerConfig as M, writeWranglerPublishedTrafficSnapshot as N, syncWranglerBindings as O, GENERATED_MARKERS as P, readWranglerCustomDomains as S, readWranglerWebSocketTombstones as T, persistMaterializedWranglerConfig as _, LIVE_MIGRATION_TAG as a, readRootWranglerConfig as b, bindingEntriesForField as c, ensureWranglerCompatibilityDate as d, ensureWranglerNodejsAlsFlag as f, normalizeWranglerCompatibilityFlags as g, nextDurableObjectMigrationTag as h, LATEST_KNOWN_COMPAT_DATE as i, updateWranglerCustomDomain as j, syncWranglerDurableObjects as k, bindingNamesForField as l, hasDurableObjectSqliteMigration as m, DEFAULT_LIVE_CLASS as n, acknowledgeWranglerManagedQueueRemovals as o, findBindingNameOwner as p, FRIENDLY_BINDING_NAMES as r, assertNoDuplicateMembers as s, DEFAULT_LIVE_BINDING as t, ensureWranglerCompat as u, readExistingLiveBindingConfig as v, readWranglerPublishedTrafficSnapshot as w, readWranglerCompat as x, readProvisionedBindingEntry as y };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { b as readRootWranglerConfig } from "./wrangler-I-scB_1t.mjs";
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { a as join, l as toSlash, n as dirname, r as extname } from "./dist-ipQi0Q8y.mjs";
|
|
2
2
|
import { n as readProjectPaths } from "./project-paths-DGu_QZ_Q.mjs";
|
|
3
|
-
import { l as getDatabaseDialect, p as readConfig } from "./config-
|
|
3
|
+
import { l as getDatabaseDialect, p as readConfig } from "./config-OFOAimzt.mjs";
|
|
4
4
|
import { n as isIdentifier, r as isLiteral } from "./ast-CsZ_q_gc.mjs";
|
|
5
5
|
import { t as MagicString } from "./dist-Cegxq8gs.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { c as voidDbBundlerPlugin, r as cloudflareQueueName, t as generateQueueResolverSource } from "./dispatch-CLa2LThw.mjs";
|
|
7
|
+
import { t as WORKER_CONDITIONS } from "./worker-conditions-AWFRcFxR.mjs";
|
|
7
8
|
import { createRequire } from "node:module";
|
|
8
9
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { build, parseSync } from "vite";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
11
12
|
import { createHash } from "node:crypto";
|
|
12
13
|
//#region src/cli/worker-require-transform.ts
|
|
13
14
|
function nodes(root) {
|
|
@@ -237,14 +238,6 @@ async function bundleMigrationHandler(root, workerDir, handlerPath, dialect) {
|
|
|
237
238
|
if (isPostgresql || isMysql) patchWorkerRequireBundle(outDir, isPostgresql ? "PostgreSQL" : "MySQL", "migration bundle");
|
|
238
239
|
for (const file of readdirSync(outDir)) copyFileSync(join(outDir, file), join(workerDir, file));
|
|
239
240
|
}
|
|
240
|
-
const WORKER_CONDITIONS = [
|
|
241
|
-
"workerd",
|
|
242
|
-
"worker",
|
|
243
|
-
"browser",
|
|
244
|
-
"module",
|
|
245
|
-
"import",
|
|
246
|
-
"default"
|
|
247
|
-
];
|
|
248
241
|
const SQL_WORKER_EXTERNAL = [
|
|
249
242
|
/^cloudflare:/,
|
|
250
243
|
/^node:/,
|
|
@@ -414,6 +407,7 @@ async function bundleEntry(root, entry) {
|
|
|
414
407
|
await build({
|
|
415
408
|
root: tmpDir,
|
|
416
409
|
logLevel: "silent",
|
|
410
|
+
resolve: { conditions: [...WORKER_CONDITIONS, "production"] },
|
|
417
411
|
build: {
|
|
418
412
|
outDir,
|
|
419
413
|
emptyOutDir: true,
|
|
@@ -608,7 +602,9 @@ function generateHandlersSource(root, features, dialect) {
|
|
|
608
602
|
lines.push(`import queueHandler${i} from "${absPath}";`);
|
|
609
603
|
}
|
|
610
604
|
lines.push("");
|
|
611
|
-
|
|
605
|
+
const queueNames = features.queues.map((queue) => queue.name);
|
|
606
|
+
const cloudflareNames = features.cloudflareQueueNames;
|
|
607
|
+
lines.push(generateQueueResolverSource(queueNames, cloudflareNames && queueNames.map((name) => cloudflareQueueName(name, cloudflareNames))));
|
|
612
608
|
lines.push("");
|
|
613
609
|
lines.push(scoped ? "async function __runQueue(batch, env, ctx) {" : "export async function handleQueue(batch, env, ctx) {");
|
|
614
610
|
lines.push(" const queueSuffix = batch.queue;");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as StandardSchemaV1 } from "./standard-schema-Fo_vCAZh.mjs";
|
|
2
|
-
import { r as AuthUser } from "./auth-
|
|
3
|
-
import { a as CloudEnv } from "./handler-
|
|
2
|
+
import { r as AuthUser } from "./auth-g2TQbDLD.mjs";
|
|
3
|
+
import { a as CloudEnv } from "./handler-BHktmuTL.mjs";
|
|
4
4
|
import { WebSocketRouteMap } from "void/routes";
|
|
5
5
|
//#region src/runtime/ws.d.ts
|
|
6
6
|
type MaybePromise<T> = T | Promise<T>;
|
|
@@ -125,6 +125,7 @@ declare class VoidSocket<P extends keyof WebSocketRouteMap & string> {
|
|
|
125
125
|
private readonly buffer;
|
|
126
126
|
private reconnectAttempts;
|
|
127
127
|
private closedByUser;
|
|
128
|
+
private reconnectTimer;
|
|
128
129
|
constructor(url: string, options: ConnectCommonOptions);
|
|
129
130
|
private emit;
|
|
130
131
|
private connect;
|
package/docs/guide/auth.md
CHANGED
|
@@ -119,7 +119,7 @@ import { getSession, getUser, requireAuth } from 'void/auth';
|
|
|
119
119
|
- `getSession()` returns `{ user, session } | null`
|
|
120
120
|
- `requireAuth(c)` returns the authenticated user or throws `401`
|
|
121
121
|
|
|
122
|
-
Import `AuthUser` and `AuthSession` from `void/auth` for type annotations. See their fields in the [API reference](../reference/api/auth.md#authuser).
|
|
122
|
+
Import `AuthUser` and `AuthSession` from `void/auth` for type annotations. They include the fields your Better Auth plugins and `user.additionalFields` add, such as `role` from `admin()`. See their fields in the [API reference](../reference/api/auth.md#authuser).
|
|
123
123
|
|
|
124
124
|
## Behavior
|
|
125
125
|
|
|
@@ -129,6 +129,8 @@ Local development needs no secret setup. To preview a production build locally,
|
|
|
129
129
|
|
|
130
130
|
Responses to signed-in requests get `Cache-Control: no-store`, so neither the browser cache nor a shared cache keeps a user's data. A route keeps its own policy when it sets `no-store`, or `private` together with `no-cache`. To answer repeat requests with `304`, use `private, no-cache` with Hono's [`etag()`](https://hono.dev/docs/middleware/builtin/etag) middleware. If you set the `ETag` yourself, make it differ per user, so the `304` doesn't reuse another account's page. The browser cache keeps a copy of a `private, no-cache` page, so the Back button can show it again after the user signs out. Keep the default `no-store` for pages that must not stay in the browser cache.
|
|
131
131
|
|
|
132
|
+
Static files from your build and `public/` keep their own cache policy for signed-in requests, so the browser reuses cached JavaScript and CSS. When your app handles the request, the policy is marked `private`, and a request that refreshes the session cookie gets `no-store`.
|
|
133
|
+
|
|
132
134
|
`POST`, `PUT`, `PATCH` and `DELETE` requests and WebSocket connections from another origin don't use the signed-in session unless that origin is in `trustedOrigins`. If a separate frontend calls your app, add its origin there (see [Customization](#customization)). If your routes use their own cookies, check the request `Origin` yourself, for example with Hono's [`csrf()`](https://hono.dev/docs/middleware/builtin/csrf) middleware.
|
|
133
135
|
|
|
134
136
|
Deployed apps on Cloudflare limit auth requests per client IP with Better Auth's defaults, for example 3 sign-in attempts per 10 seconds. Each Worker instance keeps its own count. Change or turn off the limits with `rateLimit` in `auth.ts`. To share one count across instances, set `rateLimit: { storage: 'database' }`, then run `void db generate` for a direct Cloudflare deploy.
|
|
@@ -184,7 +186,7 @@ For the full set of available options, see the official [Better Auth options ref
|
|
|
184
186
|
|
|
185
187
|
## Database and Migrations
|
|
186
188
|
|
|
187
|
-
Auth tables live alongside your app's tables. During local development, Void creates them automatically.
|
|
189
|
+
Auth tables live alongside your app's tables. During local development, Void creates them automatically. While the dev server runs, saving `auth.ts`, or a local file it imports, updates the auth schema Void generates for your app. New auth tables and columns are created in the local database the next time the dev server starts.
|
|
188
190
|
|
|
189
191
|
Void platforms create auth tables automatically. For a direct Cloudflare deploy, include them in your checked-in migrations:
|
|
190
192
|
|
|
@@ -51,6 +51,28 @@ Path-only sources apply to every domain on the project. Host-prefixed sources ap
|
|
|
51
51
|
|
|
52
52
|
The source must use `https://` and a literal hostname without wildcards or a port. Add the domain to your project with `void domain add` before deploying the rule. A domain awaiting certificate verification starts redirecting once it becomes active.
|
|
53
53
|
|
|
54
|
+
## HTTP to HTTPS
|
|
55
|
+
|
|
56
|
+
Set `routing.alwaysUseHttps` to redirect HTTP requests to HTTPS:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { defineConfig } from 'void/config';
|
|
60
|
+
|
|
61
|
+
export default defineConfig({
|
|
62
|
+
routing: {
|
|
63
|
+
alwaysUseHttps: true,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Void answers with a permanent `308` redirect to the same URL over HTTPS. The path, query string, and request method stay the same. Omitted or `false` means off. `localhost` and loopback addresses such as `127.0.0.1` stay on HTTP, so local development and `vite preview` are unchanged.
|
|
69
|
+
|
|
70
|
+
It works on the Void platform and for Void apps on `void deploy --platform cloudflare`. It needs the Cloudflare target: setting it with `target: 'node'`, `'bun'`, or `'deno'` is a config error.
|
|
71
|
+
|
|
72
|
+
On `void deploy --platform cloudflare`, built files with a content hash in their name, such as `/assets/index-abc123.js`, can be served by Cloudflare before Void runs, so they may still answer over HTTP. Pages always redirect, so browsers load these files over HTTPS. To redirect every request, also turn on [Always Use HTTPS](https://developers.cloudflare.com/ssl/edge-certificates/additional-options/always-use-https/) for your Cloudflare zone.
|
|
73
|
+
|
|
74
|
+
SvelteKit, Nuxt, Astro, Analog, TanStack Start, React Router, and vinext apps run their own Worker, so `void deploy --platform cloudflare` stops with an error when the option is on. Static builds of these frameworks, deployed with `--dir` or `inference.appType`, redirect as usual. For the others, redirect to HTTPS in the framework, or turn on [Always Use HTTPS](https://developers.cloudflare.com/ssl/edge-certificates/additional-options/always-use-https/) for your Cloudflare zone.
|
|
75
|
+
|
|
54
76
|
## Framework `_redirects` files
|
|
55
77
|
|
|
56
78
|
Void reads `_redirects` files produced by supported frameworks. You can also place one in Vite's `publicDir`:
|
|
@@ -37,6 +37,16 @@ With `void deploy --platform cloudflare`, generated JavaScript bundles with cont
|
|
|
37
37
|
|
|
38
38
|
Use [Custom Headers](./headers) to choose a cache policy for other files. Apply immutable caching only to URLs that change whenever their content changes.
|
|
39
39
|
|
|
40
|
+
Cloudflare serves these fingerprinted JavaScript bundles without starting your Worker, so they do not count as Worker requests, when your app:
|
|
41
|
+
|
|
42
|
+
- has no global middleware
|
|
43
|
+
- has no header, redirect, rewrite or fallback rules, and no `routing.notFound` setting
|
|
44
|
+
- uses the default `base` of `/`
|
|
45
|
+
- has no route or page that matches paths in the assets directory (`assets/` by default)
|
|
46
|
+
- builds at most 99 fingerprinted bundles
|
|
47
|
+
|
|
48
|
+
Otherwise these bundles go through your Worker and keep the same caching.
|
|
49
|
+
|
|
40
50
|
## Non-hashed assets
|
|
41
51
|
|
|
42
52
|
Other static files, such as `index.html` and `favicon.ico`, are cached until the next deploy. You do not need to purge them manually.
|
|
@@ -16,7 +16,7 @@ void email domain add acme.com
|
|
|
16
16
|
|
|
17
17
|
`add` opens a Cloudflare API-token template. Restrict the token to the selected account and mail zone before creating it; Workers Scripts permission applies across that account. The CLI accepts a masked paste or a newly copied token and asks you to confirm those restrictions. The platform encrypts the credential for the zone connection, so projects sharing that zone do not need separate ingress Workers.
|
|
18
18
|
|
|
19
|
-
Platform-managed custom inbound email requires the apex of a Cloudflare zone. Cloudflare’s catch-all only covers that apex, so a subdomain of the selected zone cannot receive arbitrary addresses. `add` refuses to register a new apex that already has MX records, or whose catch-all already routes elsewhere, so it never takes over mail hosted with another provider such as Microsoft 365 or Google Workspace. Use an apex that has no mail yet, or the platform’s shared mail address. Running `add` again for a registered apex, for example to renew its token, keeps working. Each custom apex belongs to one project; a project can register more than one apex.
|
|
19
|
+
Platform-managed custom inbound email requires the apex of a Cloudflare zone. Cloudflare’s catch-all only covers that apex, so a subdomain of the selected zone cannot receive arbitrary addresses. `add` refuses to register a new apex that already has MX records, or whose catch-all already routes elsewhere, so it never takes over mail hosted with another provider such as Microsoft 365 or Google Workspace. Use an apex that has no mail yet, or the platform’s shared mail address. To only send from an apex that keeps its current mail provider, ask your platform administrator to register it as [outbound-only](../platform/administration/email.md#sending-from-a-domain-that-already-receives-mail). Running `add` again for a registered apex, for example to renew its token, keeps working. Each custom apex belongs to one project; a project can register more than one apex.
|
|
20
20
|
|
|
21
21
|
If setup is interrupted, use `void email domain status <domain>` to inspect the saved operation before retrying.
|
|
22
22
|
|
|
@@ -94,6 +94,8 @@ deployed `email/` handlers without per-project DNS setup once the platform has v
|
|
|
94
94
|
handlers once its inbound readiness is `ready`. Use `void email domain status`
|
|
95
95
|
to check current provider routing and any remaining setup steps.
|
|
96
96
|
|
|
97
|
+
A project with an [email alias](../../reference/cli/email.md#void-email-alias) also receives at `<alias>+anything@<mail domain>`. Routing ignores which prefix was used: with the slug `acme-store` and the alias `acme`, both `acme-store+support@…` and `acme+support@…` reach `email/support.ts`.
|
|
98
|
+
|
|
97
99
|
For direct deployments, follow [Your own Cloudflare account](./domains.md#your-own-cloudflare-account) to configure inbound addresses.
|
|
98
100
|
|
|
99
101
|
The local dev server provides the outbound email inbox but has no inbound email trigger. Test inbound handlers with `createInboundTestHarness` below.
|
|
@@ -84,7 +84,7 @@ You can send to at most 50 recipients across `to`, `cc`, and `bcc`. Use standard
|
|
|
84
84
|
|
|
85
85
|
`Address` accepts either a string (`"hello@acme.dev"` or `"Name <hello@acme.dev>"`) or an object (`{ email, name? }`). Unicode display names are supported.
|
|
86
86
|
|
|
87
|
-
On the platform the sender is pinned to your project. `from` must be your project's own platform address — `<project-slug>@<mail-domain>` or `<project-slug>+<tag>@<mail-domain>`, optionally with a display name — or any address on a domain registered with `void email domain add` (see [Your own domain on the platform](./domains.md#your-own-domain-on-the-platform)). For example, if your platform's mail domain is `mail.example.com`, you can use `Acme <acme+noreply@mail.example.com>`. Anything else is rejected with `INVALID_FROM`. Omit `from` and Void fills in `<project-slug>+noreply@<mail-domain>` for you.
|
|
87
|
+
On the platform the sender is pinned to your project. `from` must be your project's own platform address — `<project-slug>@<mail-domain>` or `<project-slug>+<tag>@<mail-domain>`, optionally with a display name — or any address on a domain registered with `void email domain add` (see [Your own domain on the platform](./domains.md#your-own-domain-on-the-platform)). For example, if your platform's mail domain is `mail.example.com`, you can use `Acme <acme+noreply@mail.example.com>`. Anything else is rejected with `INVALID_FROM`. Omit `from` and Void fills in `<project-slug>+noreply@<mail-domain>` for you. If the project owner set an [email alias](../../reference/cli/email.md#void-email-alias), `<alias>@<mail-domain>` and `<alias>+<tag>@<mail-domain>` are accepted too; the default stays the slug address.
|
|
88
88
|
|
|
89
89
|
On your own Cloudflare account, `from` defaults to `email.from` from `void.config.ts` and must be on a domain your account can send from; Cloudflare rejects any other sender and `sendEmail` reports it as `INVALID_FROM`.
|
|
90
90
|
|
|
@@ -147,19 +147,23 @@ The types are inferred from your action's `withValidator()` schema in the compan
|
|
|
147
147
|
|
|
148
148
|
`useForm` returns a reactive object with:
|
|
149
149
|
|
|
150
|
-
| Property / Method | Purpose
|
|
151
|
-
| -------------------------------- |
|
|
152
|
-
| `data` / `setData` | Current form values, typed to match the action's body schema.
|
|
153
|
-
| `errors` | Field-level validation errors, keys typed to body field names.
|
|
154
|
-
| `error` | Action error, or `null`.
|
|
155
|
-
| `post`, `put`, `patch`, `delete` | Submit the form with that method. In React these are native form action callbacks.
|
|
156
|
-
| `pending` | `true` while the submission is in flight.
|
|
157
|
-
| `hasChanges` | `true` if form data differs from
|
|
158
|
-
| `wasSuccessful` | `true` after a successful submission. Stays `true` until the next submission.
|
|
159
|
-
| `recentlySuccessful` | `true` for 2 seconds after a successful submission. Useful for flash messages.
|
|
160
|
-
| `reset(...fields)` | Reset form data to
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
150
|
+
| Property / Method | Purpose |
|
|
151
|
+
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
152
|
+
| `data` / `setData` | Current form values, typed to match the action's body schema. |
|
|
153
|
+
| `errors` | Field-level validation errors, keys typed to body field names. |
|
|
154
|
+
| `error` | Action error, or `null`. |
|
|
155
|
+
| `post`, `put`, `patch`, `delete` | Submit the form with that method. In React these are native form action callbacks. |
|
|
156
|
+
| `pending` | `true` while the submission is in flight. |
|
|
157
|
+
| `hasChanges` | `true` if form data differs from the current defaults. After a successful submit, defaults update to the submitted values so `hasChanges` becomes `false`. |
|
|
158
|
+
| `wasSuccessful` | `true` after a successful submission. Stays `true` until the next submission. |
|
|
159
|
+
| `recentlySuccessful` | `true` for 2 seconds after a successful submission. Useful for flash messages. |
|
|
160
|
+
| `reset(...fields)` | Reset form data to the current defaults (last submitted values after success). Field names autocomplete. Pass `{ initial: true }` first to reset to the values passed to `useForm`. |
|
|
161
|
+
| `resetOnSuccess` (option) | When `true`, a successful submit resets both form data and defaults to the mount-time values. Useful for create forms that should clear after save. |
|
|
162
|
+
| `setError(field, message)` | Set a client-side validation error on a field. |
|
|
163
|
+
| `clearErrors(...fields)` | Clear validation errors. Field names autocomplete. |
|
|
164
|
+
| `clearError()` | Clear `error`. |
|
|
165
|
+
|
|
166
|
+
Use `setError()` to display errors from your own client-side validation. Setting an error preserves errors on other fields. Changing a field clears that field's validation error automatically (`setData` in React and Solid, or assigning to `data` in Vue and Svelte).
|
|
163
167
|
|
|
164
168
|
For a native React form, use `form.post`, `form.put`, `form.patch`, or `form.delete` as a native form action. For an awaitable mutation, use `action()`. In Vue, Svelte, and Solid, form submissions return `Promise<void>`; catch thrown errors or use your framework's error handling.
|
|
165
169
|
|
|
@@ -171,6 +175,32 @@ const form = useForm('/users/:id', { name: '' }, { params: { id: '42' } });
|
|
|
171
175
|
<Form form={form} method="put">{/* submits to /users/42 */}</Form>
|
|
172
176
|
```
|
|
173
177
|
|
|
178
|
+
Create forms often need empty fields again after a successful save. Either pass `resetOnSuccess: true` in the third argument, or call `form.reset({ initial: true })` when `form.wasSuccessful` becomes `true`.
|
|
179
|
+
|
|
180
|
+
### React submission callbacks
|
|
181
|
+
|
|
182
|
+
In React, pass `onSuccess` and `onError` to `useForm()` to respond to a submission:
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
const form = useForm(
|
|
186
|
+
'/users',
|
|
187
|
+
{ name: '', email: '' },
|
|
188
|
+
{
|
|
189
|
+
onSuccess: () => setDialogOpen(false),
|
|
190
|
+
onError: (error) => {
|
|
191
|
+
// A field-error map or VoidActionError for a handled action failure.
|
|
192
|
+
console.error(error);
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Each submission uses the callbacks from when it was dispatched. Callbacks run inside the React Action, before the updated UI commits. Use an effect or ref callback to focus an element that appears after submission.
|
|
199
|
+
|
|
200
|
+
Callbacks can return `Promise<void>`. The Action awaits them, and `form.pending` stays true until they complete. Errors thrown or rejected by callbacks reach the nearest React error boundary. Aborted submissions invoke neither callback.
|
|
201
|
+
|
|
202
|
+
Vue, Svelte, and Solid submissions return a promise instead; use `await form.post()` to run work after a submission.
|
|
203
|
+
|
|
174
204
|
## Named Actions
|
|
175
205
|
|
|
176
206
|
When a page needs multiple mutations, such as updating and deleting a user on the same page, export `actions` (plural) instead of `action`:
|
|
@@ -241,6 +241,8 @@ import pages from '@void/md/pages';
|
|
|
241
241
|
// [{ path: "/docs/getting-started", title: "Getting Started", frontmatter: {...}, headings: [...] }, ...]
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
+
The list holds only the pages for the current [environment](./overview.md#environment-only-pages): `.dev` pages on the dev server, `.prod` pages in builds. The suffix is dropped from `path`: `debug.dev.md` is listed as `/debug`.
|
|
245
|
+
|
|
244
246
|
Each entry has this shape:
|
|
245
247
|
|
|
246
248
|
```ts
|
|
@@ -94,13 +94,44 @@ Pages can be flat files such as `about.vue` or directory-based routes such as `a
|
|
|
94
94
|
|
|
95
95
|
<PagesFileTree />
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
Keep one file per route. Void reports an error when two page files resolve to the same path, such as `about.vue` and `about/index.vue`, or the same page in two route groups.
|
|
98
|
+
|
|
99
|
+
Each page can have a companion `.server.ts`, `.server.tsx`, `.server.js`, `.server.jsx`, `.server.mts`, or `.server.mjs` file that runs exclusively on the server. Use `.server.tsx` or `.server.jsx` when the companion contains JSX (see [JSX handlers](../server-routing.md#jsx-handlers) for the JSX runtime). A companion belongs to its matching page and does not create a separate route:
|
|
100
|
+
|
|
101
|
+
| File | Role |
|
|
102
|
+
| ---------------------- | ------------------------------ |
|
|
103
|
+
| `pages/foo.tsx` | Page component at `/foo` |
|
|
104
|
+
| `pages/foo.server.tsx` | Server companion for that page |
|
|
105
|
+
|
|
106
|
+
A companion can export:
|
|
98
107
|
|
|
99
108
|
- A [**loader**](./loaders), which runs on `GET` and returns the data that becomes the page component's props
|
|
100
109
|
- [**Actions**](./actions-and-forms), which handle mutations from forms and programmatic calls. Export a single `action` or multiple [named actions](./actions-and-forms#named-actions) when a page has several mutations
|
|
101
110
|
- `ssr = false` to opt a route out of server-rendered component HTML while keeping server loaders and client-side routing
|
|
102
111
|
|
|
103
|
-
File-based routing rules are the same as [server routing](../server-routing.md): `[param]` for dynamic segments, `[...param]` for catch-all, `(group)/` for route groups.
|
|
112
|
+
File-based routing rules are the same as [server routing](../server-routing.md): `[param]` for dynamic segments, `[...param]` for catch-all, `(group)/` for route groups, and a `.dev` or `.prod` suffix for [environment-only pages](#environment-only-pages).
|
|
113
|
+
|
|
114
|
+
### Environment-only pages
|
|
115
|
+
|
|
116
|
+
Add a `.dev` or `.prod` suffix before the extension to include a page in only one environment. The suffix is not part of the URL:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
pages/
|
|
120
|
+
about.tsx → /about, always
|
|
121
|
+
debug.dev.tsx → /debug, development only
|
|
122
|
+
stats.prod.tsx → /stats, production only
|
|
123
|
+
home.dev.tsx → /home in development
|
|
124
|
+
home.prod.tsx → /home in production
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`.dev` pages run only on the dev server; `.prod` pages run in builds, deployments, and production previews.
|
|
128
|
+
|
|
129
|
+
- A `.dev` page and a `.prod` page can share a URL. A plain page and a suffixed page for the same URL is an error wherever both run: `home.tsx` with `home.dev.tsx` fails on the dev server, and `home.tsx` with `home.prod.tsx` fails the build.
|
|
130
|
+
- The suffix goes **before** `.island`: `chart.dev.island.tsx` is a development-only island page. `chart.island.dev.tsx` is not an island page.
|
|
131
|
+
- A companion repeats the suffix: `home.dev.server.ts` belongs to `home.dev.tsx`.
|
|
132
|
+
- Suffixes apply to page files only. Layouts take no suffix.
|
|
133
|
+
- A resource used only by `.dev` pages and their companions is not provisioned for production.
|
|
134
|
+
- The dev server and builds generate types for their own environment. `void prepare` generates types for both.
|
|
104
135
|
|
|
105
136
|
## How Navigation Works
|
|
106
137
|
|
|
@@ -10,6 +10,8 @@ These commands apply to a platform that enables email. Every project can send fr
|
|
|
10
10
|
|
|
11
11
|
Shared inbound mail uses one exact routing rule per receiving project and enables Cloudflare’s per-zone plus addressing. Creating a project or deploying an app that only sends mail consumes no inbound rule. Existing rules and the apex catch-all remain in place. Cloudflare normally allows [200 routing rules per domain](https://developers.cloudflare.com/email-service/platform/limits/); remove unused rules or request a higher limit if the shared domain reaches capacity. An unrelated rule for a project’s base address or a plus address blocks its setup until that conflict is removed.
|
|
12
12
|
|
|
13
|
+
A project owner can give the shared address one permanent, shorter name with `void email alias <name>`. A receiving project with an alias uses two rules, one for the slug and one for the alias, because the slug address keeps working. Setting an alias is refused while a conflicting rule exists or the domain is at its rule limit, and the alias is released only when the project is deleted.
|
|
14
|
+
|
|
13
15
|
If recipient creation is interrupted, inspect its retained intent:
|
|
14
16
|
|
|
15
17
|
```sh
|
|
@@ -150,3 +152,19 @@ Run recovery with the same platform version that created the persisted intent.
|
|
|
150
152
|
`applied` continues without replaying the write; `not-applied` permits that exact
|
|
151
153
|
step to retry. Secret rotation keeps its staged generation, and removal resumes
|
|
152
154
|
from the unresolved cleanup step. Age alone never authorizes a retry.
|
|
155
|
+
|
|
156
|
+
### Sending from a domain that already receives mail
|
|
157
|
+
|
|
158
|
+
If an apex already receives mail with another provider, such as Microsoft 365 or Google Workspace, register it for sending only:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
void platform email domain-add brand.example --project hr-portal --outbound-only
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The project can then send from any address on the apex, such as `support@brand.example`. It cannot receive mail there: the MX records, the apex SPF record, and incoming mail stay with the current provider.
|
|
165
|
+
|
|
166
|
+
- Cloudflare Email Sending must be available, which needs Workers Paid. Without it, setup is blocked instead of falling back to verified destinations only. Upgrade, then run `domain-sync`.
|
|
167
|
+
- Email Sending adds its own records on `cf-bounce.<domain>` and `_dmarc.<domain>`. If Cloudflare reports that existing records conflict with them, for example several DMARC records, setup is blocked and the error lists each record. Fix them, then run `domain-sync`.
|
|
168
|
+
- Only administrators can register an outbound-only domain, and it must be a zone apex. The platform's shared mail zone cannot be outbound-only.
|
|
169
|
+
- `domain-rotate-secret` does not apply. `domain-remove` stops sending through the platform and leaves the MX records alone.
|
|
170
|
+
- To switch an outbound-only domain to platform-managed inbound mail, or back, remove it first and add it again.
|
|
@@ -100,7 +100,9 @@ Repair recreates missing infrastructure that the installer owns. It does not res
|
|
|
100
100
|
|
|
101
101
|
### Upgrade the platform
|
|
102
102
|
|
|
103
|
-
Keep the platform runtime current when updating the Void CLI.
|
|
103
|
+
Keep the platform runtime current when updating the Void CLI. Upgrade the platform before your team updates its CLI: a newer CLI refuses to work with an older platform and asks for `void platform upgrade`. Install the new CLI on the administrator's machine, run the upgrade below, then have the team update.
|
|
104
|
+
|
|
105
|
+
If the platform will deploy apps that use `void/sandbox`, its runtime token needs **Account → Containers → Edit** and **Account → Cloudchamber → Edit**. A token made from an older setup link may lack them, and an upgrade does not add them. Edit the token in the Cloudflare dashboard, or set `VOID_PLATFORM_RUNTIME_CLOUDFLARE_API_TOKEN` to a new token when you upgrade. See [Cloudflare API Tokens](/guide/platform/installation/credentials#runtime-token-permissions).
|
|
104
106
|
|
|
105
107
|
Use the installed CLI's packaged runtime to upgrade:
|
|
106
108
|
|
|
@@ -13,16 +13,17 @@ To run a Void platform, you need:
|
|
|
13
13
|
|
|
14
14
|
Void creates the Workers, storage, queues, routing, and database tables through the CLI.
|
|
15
15
|
|
|
16
|
-
Workers for Platforms requires
|
|
16
|
+
Workers for Platforms requires its own paid plan. Analytics Engine is available on Workers Free, but you must enable it for the account. Review [Analytics Engine pricing and allowances](https://developers.cloudflare.com/analytics/analytics-engine/pricing/). Your database and Cloudflare usage are billed separately.
|
|
17
17
|
|
|
18
18
|
## Prepare Your Cloudflare Account and Domain
|
|
19
19
|
|
|
20
20
|
In the [Cloudflare dashboard](https://dash.cloudflare.com/), select the account where you want the platform to live:
|
|
21
21
|
|
|
22
22
|
1. Open **Workers & Pages** once to let Cloudflare provision Workers for the account.
|
|
23
|
-
2. Open **Workers for Platforms** and enable its plan. Review [Workers for Platforms pricing](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/
|
|
23
|
+
2. Open **Workers for Platforms** and enable its plan. Review [Workers for Platforms pricing](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/reference/pricing/) before confirming.
|
|
24
24
|
3. Open **R2 Object Storage** and complete its activation. Void creates the bucket later.
|
|
25
|
-
4.
|
|
25
|
+
4. Open **Analytics Engine** and select **Enable** if prompted. You do not need to create a dataset manually. If the page has no **Enable** action and installation reports code 10089, follow the [steps below](#if-cloudflare-rejects-analytics-engine).
|
|
26
|
+
5. For a domain installation, choose a domain you own, such as `example.app`. If you need one, register it with your preferred registrar. Use a spare domain's root: apps will be served at `my-app.example.app`. For workers.dev testing, skip this step and the DNS setup below.
|
|
26
27
|
|
|
27
28
|
If that domain is already in this Cloudflare account, use its existing zone. Otherwise, [add the domain to Cloudflare](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/) and follow the nameserver instructions until the zone is active. A zone is Cloudflare's DNS configuration for a domain; creating one does not buy the domain.
|
|
28
29
|
|
|
@@ -30,7 +31,9 @@ You can also let Void create the zone during installation. Setting it up now let
|
|
|
30
31
|
|
|
31
32
|
### If Cloudflare rejects Analytics Engine
|
|
32
33
|
|
|
33
|
-
The platform stores request metrics and logs in Analytics Engine. If installation stops with "You need to enable Analytics Engine" (code 10089),
|
|
34
|
+
The platform stores request metrics and logs in Analytics Engine. If installation stops with "You need to enable Analytics Engine" (code 10089), open the Analytics Engine dashboard link in the error and select **Enable** for that account. Activation is available on Workers Free; this error does not require a Workers plan upgrade or a replacement API token. Then run the printed `void platform install --resume` command.
|
|
35
|
+
|
|
36
|
+
If your account's dashboard has no **Enable** action, create a disposable Hello World Worker, add an Analytics Engine binding under **Settings** > **Bindings**, and deploy it once from the dashboard. Then resume installation. Use a separate Worker for this step; leave the platform's own Workers unchanged.
|
|
34
37
|
|
|
35
38
|
## Install the CLI and Preview
|
|
36
39
|
|
|
@@ -82,6 +85,8 @@ Use any PostgreSQL provider that accepts connections from your computer and Clou
|
|
|
82
85
|
| [PlanetScale Postgres](https://planetscale.com/docs/postgres/connecting) | Choose **Postgres**, not Vitess/MySQL. Open **Connect**, create role credentials, and use the direct primary connection on port `5432`, not PgBouncer on `6432`. |
|
|
83
86
|
| [Neon](https://neon.com/docs/get-started-with-neon/connect-neon) | Open **Connect**, select the database and owner role, and turn **connection pooling off**. |
|
|
84
87
|
|
|
88
|
+
When sizing your database, include the connection pools for every platform installation and application sharing that server. Adjust each Hyperdrive's [origin connection limit](https://developers.cloudflare.com/hyperdrive/configuration/tune-connection-pool/) to fit the server's capacity, leaving room for migrations, administration, provider-reserved connections, and temporary bursts.
|
|
89
|
+
|
|
85
90
|
Once installation claims the database, continue using that same database for resume and maintenance commands. If installation stops because it cannot use the database, for example because the database is not empty, set `VOID_PLATFORM_DATABASE_URL` to another database's URL before you resume. Uninstall never deletes external PostgreSQL.
|
|
86
91
|
|
|
87
92
|
### Use a provider CLI
|
|
@@ -91,6 +91,8 @@ If installation configured Cloudflare Access, the uninstall plan lists its recor
|
|
|
91
91
|
|
|
92
92
|
Uninstall does not remove the applications created for [project Zero Trust](/guide/platform/administration/zero-trust). While Zero Trust is enabled or still has Access applications, uninstall and `--plan` stop before any change and list those applications. Run `void platform zero-trust disable` first. Uninstall reads this from the installation database, so it also stops when the database cannot be reached.
|
|
93
93
|
|
|
94
|
+
If the installation database is permanently gone and cannot be restored, there is no uninstall command that can clear the local installation record or list its Cloudflare resources. Once you have decided not to recover that installation, delete `~/.void/platforms/<account-id>/<installation-id>.json` and any sibling `<installation-id>.recovery.enc` or `<installation-id>.secrets.enc` files. This removes only local state and encrypted credentials; it does not delete Cloudflare resources. Review and remove those resources manually using [Remove retained resources](#remove-retained-resources).
|
|
95
|
+
|
|
94
96
|
If no platform user exists yet, for example because first administrator setup did
|
|
95
97
|
not finish, nobody can run `zero-trust disable`. Uninstall then turns off project
|
|
96
98
|
Zero Trust and deletes its applications itself. Run it on the computer that installed
|
package/docs/guide/queues.md
CHANGED
|
@@ -10,7 +10,7 @@ Use queues to process work asynchronously, such as sending emails or handling up
|
|
|
10
10
|
|
|
11
11
|
Create a consumer in `queues/`. TypeScript and JavaScript files are supported (`.ts`, `.mts`, `.js`, `.mjs`). Its path determines the queue name: `queues/emails.ts` creates `emails`, and `queues/order/notifications.ts` creates `order-notifications`. Each queue must have a unique name.
|
|
12
12
|
|
|
13
|
-
The resulting name must follow Cloudflare's queue naming rules: 1–63 characters, only letters, digits, and `-`, beginning and ending with a letter or digit.
|
|
13
|
+
The resulting name must follow Cloudflare's queue naming rules: 1–63 characters, only letters, digits, and `-`, beginning and ending with a letter or digit. On `void deploy --platform cloudflare`, the Cloudflare queue also carries the Worker name, so the limit applies to that full name; see [Cloudflare queue names](#cloudflare-queue-names).
|
|
14
14
|
|
|
15
15
|
Export a default handler wrapped with [`defineQueue<T>`](../reference/api/handlers.md#definequeue-t-handler). `T` defines the message body for both the consumer and calls to `send()`:
|
|
16
16
|
|
|
@@ -116,6 +116,18 @@ deletes messages that exhaust their retries without one; see
|
|
|
116
116
|
|
|
117
117
|
Void provisions queues and connects their producers and consumers when you deploy.
|
|
118
118
|
|
|
119
|
+
### Cloudflare queue names
|
|
120
|
+
|
|
121
|
+
Queue names are unique per Cloudflare account. So `void deploy --platform cloudflare` names each queue after the Worker: in a Worker named `app-staging`, `queues/jobs.ts` uses the Cloudflare queue `app-staging-jobs` (a `_` in the Worker name becomes `-`). Two Workers in one account, such as `app-staging` and `app-production`, get separate queues. Your code keeps using the name from `queues/` (`queues.jobs.send()`), and `vp dev` uses that name too.
|
|
122
|
+
|
|
123
|
+
The deploy stops before creating anything when:
|
|
124
|
+
|
|
125
|
+
- a new queue's full name is longer than 63 characters. Shorten the Worker name or the file name.
|
|
126
|
+
- one queue's full name is the name of another file in `queues/`, such as `queues/jobs.ts` and `queues/app-jobs.ts` in Worker `app`. Rename one of the files.
|
|
127
|
+
- another Worker already consumes the Cloudflare queue the deploy would use. A Cloudflare queue has one consumer.
|
|
128
|
+
|
|
129
|
+
A queue that the Worker already consumes keeps its name, so an app deployed before Worker-named queues keeps its queue (for example `jobs`) and the messages in it. The deploy prints a note when it keeps a name. To move to the Worker-named queue, remove the Worker's consumer from the old queue in the Cloudflare dashboard, then deploy again. Messages that reach the old queue before that deploy finishes stay there, so do this when the app is not sending messages, and check that the old queue is empty before you delete it.
|
|
130
|
+
|
|
119
131
|
## Local development
|
|
120
132
|
|
|
121
133
|
In native Void apps, sending a message through a queue binding automatically invokes the consumer in Miniflare. Local delivery uses the same handler as production.
|