@voidbase-cloud/voidbase 0.2.2 → 0.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/CHANGELOG.md +54 -0
- package/README.md +16 -7
- package/bin/voidbase.ts +67 -6
- package/docs/adapter.md +280 -0
- package/docs/ci.md +195 -0
- package/docs/deploy.md +83 -5
- package/docs/releasing.md +39 -31
- package/hooks-plugin.ts +15 -4
- package/package.json +10 -4
- package/routes/api/[...path].ts +0 -5
- package/scripts/cf-builds.ts +228 -0
- package/scripts/ci-browser.sh +60 -0
- package/scripts/ci-cache.sh +40 -0
- package/scripts/ci-lib.sh +40 -0
- package/scripts/ci-oracles.sh +19 -0
- package/scripts/ci-plan.ts +270 -0
- package/scripts/ci-status.ts +126 -0
- package/scripts/ci-suites.sh +11 -1
- package/scripts/ci.sh +188 -0
- package/scripts/gh-release.ts +48 -0
- package/scripts/release.sh +104 -0
- package/scripts/seed-reference.sh +7 -2
- package/scripts/sync-app.ts +1 -0
- package/src/adapter/bundle.ts +130 -0
- package/src/adapter/codegen.ts +269 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/plugin.ts +132 -0
- package/src/adapter/runtime.ts +325 -0
- package/src/adapter/scan.ts +277 -0
- package/src/cloud/rest.ts +10 -2
- package/src/env/define.ts +195 -0
- package/src/node/assets.ts +7 -1
- package/src/node/cloud-init.ts +14 -0
- package/src/node/deploy-cf.ts +124 -16
- package/src/node/secrets.ts +237 -0
- package/src/node/serve.ts +16 -2
- package/src/server/api.ts +7 -2
- package/src/server/app.ts +6 -1
- package/src/server/hooks/index.ts +27 -1
- package/src/server/hooks/migrations.ts +4 -1
- package/src/server/hooks/runtime.ts +10 -2
- package/src/server/jobs.ts +3 -1
- package/src/server/webauthn.ts +23 -6
- package/tsconfig.json +5 -0
- package/tsconfig.node.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,60 @@
|
|
|
3
3
|
Entries after 0.1.0 are compiled by release-please from the Conventional Commits merged since the previous release
|
|
4
4
|
(docs/releasing.md); 0.1.0 was written by hand.
|
|
5
5
|
|
|
6
|
+
## [0.4.0](https://github.com/voidbase-cloud/voidbase/compare/v0.3.0...v0.4.0) (2026-09-07)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **deploy:** one configuration declaration with Void's validators, tiered secret / server / public ([e5ee202](https://github.com/voidbase-cloud/voidbase/commit/e5ee20261520fbfba8e90d80ff4f7aa07bf6bc78))
|
|
12
|
+
|
|
13
|
+
## [0.3.0](https://github.com/voidbase-cloud/voidbase/compare/v0.2.2...v0.3.0) (2026-09-07)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **adapter:** build a Void app into a voidbase app ([e1e221b](https://github.com/voidbase-cloud/voidbase/commit/e1e221b1df4b70b6b513a4ce5c665ed73a40efe8))
|
|
19
|
+
* **adapter:** compile routes, middleware, crons and queues into pb_hooks ([3837897](https://github.com/voidbase-cloud/voidbase/commit/383789715e53a3467a0f7203df06bcd5b31fa491))
|
|
20
|
+
* **adapter:** generate a whole voidbase app into .voidbase/ ([d11db67](https://github.com/voidbase-cloud/voidbase/commit/d11db674fea39954ca8afe8844ec2108040bd944))
|
|
21
|
+
* **adapter:** PocketBase's API inside a Void route ([d6008cc](https://github.com/voidbase-cloud/voidbase/commit/d6008cc0d34a8a857965725efc83dcc76c4c8862))
|
|
22
|
+
* **adapter:** vb_hooks/ and vb_migrations/ as project-root source ([fd861b0](https://github.com/voidbase-cloud/voidbase/commit/fd861b05afcc744481a0453a40a54149d1cd0c7e))
|
|
23
|
+
* **adapter:** vb_hooks/ for PocketBase's event hooks, middleware/ through routerUse ([2baad03](https://github.com/voidbase-cloud/voidbase/commit/2baad03f6f03c57cd98d036b3e8ede6b9d5fe2d0))
|
|
24
|
+
* **adapter:** vb_secrets/, the secrets a Void app declares ([a1fda2b](https://github.com/voidbase-cloud/voidbase/commit/a1fda2b4e17029b1db4380cfa118545b3e0efca8))
|
|
25
|
+
* **deploy:** _redirects in the public dir become Void edge redirects ([8c43427](https://github.com/voidbase-cloud/voidbase/commit/8c434270bcf7e92731d90f1aaaee9c3cea3b89e6))
|
|
26
|
+
* **deploy:** _redirects in the public dir become Void edge redirects ([bf873b4](https://github.com/voidbase-cloud/voidbase/commit/bf873b4ef45806646ee4ee8a80ecbbba5e70307c))
|
|
27
|
+
* **deploy:** host-scoped _redirects become zone Redirect Rules ([cd1f8f7](https://github.com/voidbase-cloud/voidbase/commit/cd1f8f7e0080e55656a905c2c861eb923f624be7))
|
|
28
|
+
* **deploy:** pb_secrets/, the app's secrets declared in git and valued outside it ([da8e4c6](https://github.com/voidbase-cloud/voidbase/commit/da8e4c636ed1b0b67cbfd61b6b6f2030fbdaaa08))
|
|
29
|
+
* **deploy:** serve ./pb_public by default and attach several custom domains ([c8d39bd](https://github.com/voidbase-cloud/voidbase/commit/c8d39bd77ee5ef8e9a2a1c40348ea2715c6c4c18))
|
|
30
|
+
* **deploy:** VOIDBASE_DEPLOY_ZONE_TOKEN for the zone redirect rules ([6013d48](https://github.com/voidbase-cloud/voidbase/commit/6013d485be4886f25096229d74309993a2260e16))
|
|
31
|
+
* **hooks:** routerUse is PocketBase's global middleware, around every request ([12eaa8a](https://github.com/voidbase-cloud/voidbase/commit/12eaa8ab43214d72c225dc25ffb8411b2838978f))
|
|
32
|
+
* **serve:** resolve an extensionless path against <path>.html ([5b7bb26](https://github.com/voidbase-cloud/voidbase/commit/5b7bb26b006d0c0cd5573d1cde99cd457175e5b4))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Bug Fixes
|
|
36
|
+
|
|
37
|
+
* **adapter:** report once per build, not once per Vite environment ([a593b1b](https://github.com/voidbase-cloud/voidbase/commit/a593b1b55a99be6fe1b7cd8b148b1d6c357f48c0))
|
|
38
|
+
* **adapter:** the generated entry finds its own directories ([3ec53cf](https://github.com/voidbase-cloud/voidbase/commit/3ec53cfe7039cd95efece83f2caf535626b56027))
|
|
39
|
+
* **auth:** reject non-canonical base64url token segments like PocketBase ([7d00575](https://github.com/voidbase-cloud/voidbase/commit/7d005752598e640663dfa7cfa69321032ceb7fd2))
|
|
40
|
+
* **ci:** read every pushed commit on a shallow checkout ([11acdf3](https://github.com/voidbase-cloud/voidbase/commit/11acdf39b040c8b280e4d5e292db4ddb24aa3060))
|
|
41
|
+
* **cloud:** detach the queue consumer before destroying an instance ([2eb5797](https://github.com/voidbase-cloud/voidbase/commit/2eb579797351812b8322582f99a9731556947466))
|
|
42
|
+
* **deploy:** a deploy never replaces a secret the Worker already has ([63cc651](https://github.com/voidbase-cloud/voidbase/commit/63cc65148b4bf2bc229c1ec4e7a8a5d2c20e4cfd))
|
|
43
|
+
* **deploy:** one token; name the zone permission the redirect rules need ([16d0537](https://github.com/voidbase-cloud/voidbase/commit/16d0537589e6ca354b4220f50600d0569c2a12de))
|
|
44
|
+
* **deploy:** secrets.json outranks the .env files ([9df1802](https://github.com/voidbase-cloud/voidbase/commit/9df180212dc0027210cb6f589856da972fa3cce9))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### Performance
|
|
48
|
+
|
|
49
|
+
* **auth:** passkey routes in every app, gated on a passkeys collection, loaded on first use ([77af85e](https://github.com/voidbase-cloud/voidbase/commit/77af85e447fd971be4e06e272799268982f88eca))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### Documentation
|
|
53
|
+
|
|
54
|
+
* **ci:** the reference is seeded fresh per run and before the Bun pass ([d3f6d0c](https://github.com/voidbase-cloud/voidbase/commit/d3f6d0c29b35e42b2738faf2f258e9177bebe17e))
|
|
55
|
+
* **deploy:** the site's control plane moved to voidbase-site/cloud ([66f2e8c](https://github.com/voidbase-cloud/voidbase/commit/66f2e8c41fe4f66123534e338610d0975d2dcb10))
|
|
56
|
+
* **surface:** CI live on Cloudflare with incremental runs ([82a5461](https://github.com/voidbase-cloud/voidbase/commit/82a5461b0af283efa34f32c02c0691c12b927bff))
|
|
57
|
+
* **surface:** one CI project on Cloudflare, hot mode and incremental runs ([4665543](https://github.com/voidbase-cloud/voidbase/commit/46655431b081677d1e461f78977ceac1886d94fa))
|
|
58
|
+
* **surface:** the Void adapter ([0228acb](https://github.com/voidbase-cloud/voidbase/commit/0228acb4d7e8105a864c32a48db80bc0f68b3ff1))
|
|
59
|
+
|
|
6
60
|
## [0.2.2](https://github.com/voidbase-cloud/voidbase/compare/v0.2.1...v0.2.2) (2026-09-06)
|
|
7
61
|
|
|
8
62
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# voidbase
|
|
2
2
|
|
|
3
|
+
[](https://release.voidbase.cloud/) The status page of the last green build: every step, every suite, the screenshots.
|
|
4
|
+
|
|
3
5
|
A PocketBase-wire-compatible backend on Cloudflare Workers, built with [Void](https://void.cloud).
|
|
4
6
|
The unmodified PocketBase admin panel (0.40.2) and the unmodified `pocketbase` JS SDK (0.28) are the two oracles that define done.
|
|
5
7
|
|
|
@@ -36,8 +38,8 @@ platform, verifies its checksum and replaces the executable (`--backup` zips `pb
|
|
|
36
38
|
toolchain (`deploy`, `bundle`, `dev`) stays with the npm package.
|
|
37
39
|
|
|
38
40
|
Commits follow Conventional Commits (enforced by husky and CI); release-please turns them into a release PR,
|
|
39
|
-
and merging it publishes to npm
|
|
40
|
-
|
|
41
|
+
and merging it publishes to npm and GitHub Packages and attaches the executables, with the compiled notes; see
|
|
42
|
+
[docs/releasing.md](docs/releasing.md).
|
|
41
43
|
|
|
42
44
|
## Run it like PocketBase
|
|
43
45
|
|
|
@@ -101,14 +103,21 @@ provider variables.
|
|
|
101
103
|
|
|
102
104
|
`voidbase token` prints a Cloudflare dashboard link that creates `VOIDBASE_DEPLOY_CF_API_KEY` with the right
|
|
103
105
|
permissions pre-selected; with that variable set, `voidbase deploy` provisions D1 and R2, generates the Void project
|
|
104
|
-
inside the package (`node_modules/voidbase/.cloud/<name>`), stores the superuser
|
|
105
|
-
Your directory stays `pb_hooks` + `pb_migrations` +
|
|
106
|
+
inside the package (`node_modules/voidbase/.cloud/<name>`), stores the superuser and what `pb_secrets/` declares
|
|
107
|
+
as the Worker's secrets and vars, and uploads the Worker. Your directory stays `pb_hooks` + `pb_migrations` +
|
|
108
|
+
`pb_secrets` + `pb_data`, like a PocketBase folder: `pb_secrets/main.ts` declares the configuration with Void's
|
|
109
|
+
validators (secret, server or public), the git-ignored `pb_secrets/secrets.json` holds the local values, and CI
|
|
110
|
+
deploys with nothing but the deploy token once `voidbase secrets push` has stored the secrets. See
|
|
111
|
+
[docs/deploy.md](docs/deploy.md).
|
|
106
112
|
|
|
107
113
|
## Continuous integration
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
a seeded reference PocketBase (`scripts/seed-reference.sh`),
|
|
111
|
-
|
|
115
|
+
`scripts/ci.sh` is the whole CI (`bun run ci` on a dev machine): it fetches the starter, syncs the panel, starts
|
|
116
|
+
voidbase and a seeded reference PocketBase (`scripts/seed-reference.sh`), runs every suite through
|
|
117
|
+
`scripts/ci-suites.sh` on the Workers and Bun runtimes, then the production-build boots, the prebuilt executable and
|
|
118
|
+
the starter smoke, and renders the status page at [release.voidbase.cloud](https://release.voidbase.cloud/).
|
|
119
|
+
Cloudflare Workers Builds runs it (`scripts/cf-builds.ts setup`); `.github/workflows/cloudflare.yml` only starts the
|
|
120
|
+
builds; see [docs/ci.md](docs/ci.md).
|
|
112
121
|
|
|
113
122
|
## Docs
|
|
114
123
|
|
package/bin/voidbase.ts
CHANGED
|
@@ -18,26 +18,34 @@ const flags: Record<string, string> = {}; const positional: string[] = [];
|
|
|
18
18
|
for (let i = 0; i < argv.length; i++) { const a = argv[i]!; if (a.startsWith("--")) { const [k, v] = a.slice(2).split("="); flags[k!] = v ?? (argv[i + 1] && !argv[i + 1]!.startsWith("--") ? argv[++i]! : "1"); } else positional.push(a); }
|
|
19
19
|
const [cmd, sub, ...rest] = positional;
|
|
20
20
|
const url = (flags.url ?? process.env.VOIDBASE_URL ?? "http://127.0.0.1:8090").replace(/\/$/, "");
|
|
21
|
-
const serveOpts = () => ({ http: flags.http, dir: flags.dir, hooksDir: flags.hooksDir, migrationsDir: flags.migrationsDir, publicDir: flags.publicDir });
|
|
21
|
+
const serveOpts = () => ({ http: flags.http, dir: flags.dir, hooksDir: flags.hooksDir, migrationsDir: flags.migrationsDir, secretsDir: flags.secretsDir, publicDir: flags.publicDir });
|
|
22
22
|
const admin = () => { const [email, password] = (flags.admin ?? `${process.env.VOIDBASE_SUPERUSER_EMAIL ?? "admin@example.com"}:${process.env.VOIDBASE_SUPERUSER_PASSWORD ?? ""}`).split(":") as [string, string]; return { email, password }; };
|
|
23
23
|
const HELP = `voidbase - PocketBase-compatible backend: a single Bun process locally, Cloudflare Workers via Void in production
|
|
24
24
|
|
|
25
|
-
serve [--http 127.0.0.1:8090] [--dir pb_data] [--hooksDir pb_hooks] [--migrationsDir pb_migrations] [--publicDir
|
|
25
|
+
serve [--http 127.0.0.1:8090] [--dir pb_data] [--hooksDir pb_hooks] [--migrationsDir pb_migrations] [--secretsDir pb_secrets] [--publicDir pb_public] [--dev] [--entry main.ts]
|
|
26
26
|
run the server like "pocketbase serve" (--dev restarts when hooks or migrations change;
|
|
27
27
|
--entry runs your own main.ts, the counterpart of a custom PocketBase build)
|
|
28
28
|
superuser upsert <email> <password> create or update a superuser: on the local data directory (--dir) or on a running
|
|
29
29
|
instance (--url, --admin email:pass)
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
adapt [dir] [--public-dir .voidbase/pb_public] [--no-migrations]
|
|
32
|
+
generate a voidbase app under .voidbase/ from a Void app: PocketBase's layout
|
|
33
|
+
(main.ts, package.json, pb_hooks, pb_migrations, pb_public) with the project's
|
|
34
|
+
routes/middleware/crons/queues and whatever src/voidbase/ adds. vite build does
|
|
35
|
+
this too through the voidbaseAdapter() plugin; this is the same pass without Vite.
|
|
36
|
+
init [dir] scaffold .env, pb_hooks/, pb_migrations/, pb_secrets/ in a fresh checkout and sync the panel
|
|
32
37
|
dev [--port 5180] start the Void dev server (vp dev)
|
|
33
38
|
build | preview [--port 5181] production build / run the built Worker locally (vp build / vp preview)
|
|
34
|
-
deploy [--name worker] [--account id] [--domain api.example.com] [--public-dir
|
|
39
|
+
deploy [--name worker] [--account id] [--domain example.com,api.example.com] [--public-dir pb_public] [--dry-run] [--no-queue] [--no-hub] [--no-cron]
|
|
35
40
|
[--analytics] [--rate-limit 300/10]
|
|
36
41
|
go live on your Cloudflare account with VOIDBASE_DEPLOY_CF_API_KEY: creates the D1
|
|
37
42
|
database and R2 bucket, writes cloud/ (voidbase cloud init) with wrangler.jsonc,
|
|
38
43
|
stores the superuser as worker secrets and runs void deploy --backend cloudflare
|
|
39
44
|
deploy --void deploy to the Void platform instead (void auth login first)
|
|
40
45
|
token print the Cloudflare dashboard link that creates VOIDBASE_DEPLOY_CF_API_KEY
|
|
46
|
+
secrets [list] [--dir pb_secrets] what pb_secrets/main.ts declares (secret / server / public), which have a value in
|
|
47
|
+
secrets push [--name worker] secrets.json (git-ignored) or a default, which secrets the Worker has; push stores the
|
|
48
|
+
local secrets on the Worker (vars are set by every deploy)
|
|
41
49
|
update [--dir pb_data] [--backup] prebuilt executable only: fetch the latest GitHub release for this platform, verify
|
|
42
50
|
its checksum and replace the executable (--backup zips pb_data first)
|
|
43
51
|
version print the version
|
|
@@ -78,6 +86,39 @@ switch (cmd) {
|
|
|
78
86
|
await update({ currentVersion: await currentVersion(), dataDir: resolve(flags.dir ?? "pb_data"), backup: !!flags.backup });
|
|
79
87
|
break;
|
|
80
88
|
}
|
|
89
|
+
case "secrets": {
|
|
90
|
+
// pb_secrets/ (src/node/secrets.ts): what is declared, what has a value here, what the Worker has; push the values
|
|
91
|
+
const { secretsState, putWorkerSecrets, workerSecretNames, SECRETS_DIR } = await import("../src/node/secrets");
|
|
92
|
+
const { deployTarget } = await import("../src/node/deploy-cf");
|
|
93
|
+
const dir = resolve(flags.dir ?? process.env.VOIDBASE_SECRETS_DIR ?? SECRETS_DIR);
|
|
94
|
+
const state = await secretsState(dir);
|
|
95
|
+
if (!state.definition) { console.log(`${dir}/main.ts does not exist: nothing declared (voidbase init writes one)`); break; }
|
|
96
|
+
const def = state.definition; const secretNames = def.of("secret");
|
|
97
|
+
if (sub === "push") {
|
|
98
|
+
const { api, account, name } = await deployTarget({ name: flags.name, account: flags.account, log: () => undefined });
|
|
99
|
+
// only the secret tier is pushed: vars are the code's, set by every deploy
|
|
100
|
+
const ev = await def.evaluate({ ...(state.values ?? {}) }, ["secret"]);
|
|
101
|
+
if (ev.invalid.length) throw new Error(`${dir}/secrets.json: ${ev.invalid.map((i) => `${i.name}: ${i.message}`).join(", ")}`);
|
|
102
|
+
const values: Record<string, string> = {}; for (const k of secretNames) if (state.provided.includes(k) && ev.stored[k] !== undefined) values[k] = ev.stored[k]!;
|
|
103
|
+
if (!Object.keys(values).length) { console.log(`nothing to push: none of ${secretNames.join(", ") || "(no secrets declared)"} has a value in ${dir}/secrets.json`); break; }
|
|
104
|
+
const before = await workerSecretNames(api, account.id, name);
|
|
105
|
+
const done = await putWorkerSecrets(api, account.id, name, values).catch((e: Error) => { throw new Error(`${e.message}\n (the Worker "${name}" must exist: voidbase deploy creates it and stores the secrets itself)`); });
|
|
106
|
+
console.log(`pushed ${done.length} secret(s) to worker "${name}" (account ${account.name}): ${done.map((k) => `${k}${before.includes(k) ? " (replaced)" : ""}`).join(", ")}`);
|
|
107
|
+
const left = secretNames.filter((k) => !values[k]); if (left.length) console.log(`no local value, left as they are: ${left.join(", ")}`);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
// list (default): a row per declared name
|
|
111
|
+
let onWorker: string[] | null = null; let worker = "";
|
|
112
|
+
try { const t = await deployTarget({ name: flags.name, account: flags.account, log: () => undefined }); worker = t.name; onWorker = await workerSecretNames(t.api, t.account.id, t.name); } catch { /* no token here: local view only */ }
|
|
113
|
+
console.log(`${dir}: ${def.names.length} declared (${secretNames.length} secret, ${def.of("server").length} server, ${def.of("public").length} public)${state.values ? `, ${state.provided.length} valued in secrets.json` : ", no secrets.json"}${onWorker ? `, worker "${worker}" has ${onWorker.filter((k) => secretNames.includes(k)).length} of the secrets` : " (set VOIDBASE_DEPLOY_CF_API_KEY to compare with the Worker)"}`);
|
|
114
|
+
for (const i of state.info) {
|
|
115
|
+
const where = state.provided.includes(i.name) ? "local value" : i.fallback !== undefined ? `default ${i.access === "secret" ? "(set)" : JSON.stringify(i.fallback)}` : i.optional ? "optional, unset" : "no local value";
|
|
116
|
+
const worker = onWorker && i.access === "secret" ? ` ${onWorker.includes(i.name) ? "on the worker" : "NOT on the worker"}` : "";
|
|
117
|
+
console.log(` ${i.name.padEnd(30)} ${i.access.padEnd(7)} ${where.padEnd(18)}${worker}${i.description ? ` ${i.description}` : ""}`);
|
|
118
|
+
}
|
|
119
|
+
if (state.undeclared.length) console.log(` in secrets.json but not declared (never deployed): ${state.undeclared.join(", ")}`);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
81
122
|
case "token": { const { tokenHelp } = await import("../src/node/deploy-cf"); console.log(tokenHelp()); break; }
|
|
82
123
|
case "bundle": {
|
|
83
124
|
const { buildRelease, pushRelease } = await import("../src/node/bundle");
|
|
@@ -108,12 +149,32 @@ switch (cmd) {
|
|
|
108
149
|
await new Promise(() => undefined);
|
|
109
150
|
break;
|
|
110
151
|
}
|
|
152
|
+
case "adapt": {
|
|
153
|
+
const { adapt } = await import("../src/adapter/index");
|
|
154
|
+
const root = resolve(sub ?? ".");
|
|
155
|
+
const { manifest, written, copied } = await adapt(root, { publicDir: flags["public-dir"], migrations: !flags["no-migrations"], quiet: true });
|
|
156
|
+
for (const u of manifest.unsupported) console.warn(`not carried over: ${u.what} — ${u.why}`);
|
|
157
|
+
for (const c of manifest.collisions) console.warn(`shadowed by voidbase's own API, the app route never runs: ${c}`);
|
|
158
|
+
console.log(`${manifest.mode === "static" ? "static" : "server"} app at ${root}`);
|
|
159
|
+
console.log(` ${manifest.routes.length} route(s), ${manifest.middleware.length} middleware, ${manifest.hooks.length} hook(s), ${manifest.crons.length} cron(s), ${manifest.queues.length} queue(s), ${manifest.migrations.length} migration(s)${manifest.secrets ? `, ${manifest.secrets.names.length} secret(s)` : ""}`);
|
|
160
|
+
console.log(` wrote ${written.join(", ") || "nothing"}${copied ? `, ${copied} entr(ies) into ${flags["public-dir"] ?? ".voidbase/pb_public"}` : ""}`);
|
|
161
|
+
console.log(" run it: bun .voidbase/main.ts --http 127.0.0.1:8090");
|
|
162
|
+
console.log(" deploy it: cd .voidbase && voidbase deploy");
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
111
165
|
case "init": {
|
|
112
166
|
const dir = resolve(sub ?? ".");
|
|
113
|
-
mkdirSync(`${dir}/pb_hooks`, { recursive: true }); mkdirSync(`${dir}/pb_migrations`, { recursive: true });
|
|
167
|
+
mkdirSync(`${dir}/pb_hooks`, { recursive: true }); mkdirSync(`${dir}/pb_migrations`, { recursive: true }); mkdirSync(`${dir}/pb_secrets`, { recursive: true });
|
|
168
|
+
{ // pb_secrets/main.pb.js declares the secrets; secrets.json holds their values and never enters git
|
|
169
|
+
const { declarationScaffold } = await import("../src/node/secrets");
|
|
170
|
+
if (!existsSync(`${dir}/pb_secrets/main.ts`)) writeFileSync(`${dir}/pb_secrets/main.ts`, declarationScaffold());
|
|
171
|
+
const gi = `${dir}/.gitignore`; const have = existsSync(gi) ? await Bun.file(gi).text() : "";
|
|
172
|
+
const lines = ["pb_data/", "pb_secrets/secrets.json"].filter((l) => !have.split("\n").some((x) => x.trim() === l || x.trim() === l.replace(/\/$/, "")));
|
|
173
|
+
if (lines.length) writeFileSync(gi, `${have}${have && !have.endsWith("\n") ? "\n" : ""}${lines.join("\n")}\n`);
|
|
174
|
+
}
|
|
114
175
|
if (!existsSync(`${dir}/.env`)) { cpSync(`${ROOT}/.env.example`, `${dir}/.env`); console.log("wrote .env from .env.example (set VOIDBASE_SUPERUSER_EMAIL/PASSWORD)"); }
|
|
115
176
|
if (!existsSync(`${dir}/pb_hooks/main.pb.js`)) writeFileSync(`${dir}/pb_hooks/main.pb.js`, `/// <reference path="../pb_data/types.d.ts" />\nrouterAdd("GET", "/api/hello", (e) => e.json(200, { hello: "voidbase" }));\n`);
|
|
116
|
-
console.log("pb_hooks/ and
|
|
177
|
+
console.log("pb_hooks/, pb_migrations/ and pb_secrets/ ready (.gitignore covers pb_data/ and pb_secrets/secrets.json)");
|
|
117
178
|
await run("bun", ["scripts/sync-panel.ts"]).catch(() => undefined);
|
|
118
179
|
console.log("\nnext: bun install && ./node_modules/.bin/void db migrate && voidbase dev");
|
|
119
180
|
break;
|
package/docs/adapter.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Running a Void app on voidbase
|
|
2
|
+
|
|
3
|
+
A [Void](https://void.cloud) app and a voidbase app are shaped differently. Void puts server code in `routes/`,
|
|
4
|
+
`middleware/`, `crons/` and `queues/` and builds a Cloudflare Worker; voidbase is a PocketBase project, with the
|
|
5
|
+
site in `pb_public/`, JS hooks in `pb_hooks/` and migrations in `pb_migrations/`. The adapter keeps the project a
|
|
6
|
+
plain Void app and *generates* the voidbase one from it, whole, into a git-ignored `.voidbase/`.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// vite.config.ts
|
|
10
|
+
import { defineConfig } from "vite";
|
|
11
|
+
import { voidPlugin } from "void";
|
|
12
|
+
import { voidbaseAdapter } from "@voidbase-cloud/voidbase/adapter/plugin";
|
|
13
|
+
|
|
14
|
+
export default defineConfig({ plugins: [voidPlugin(), voidbaseAdapter()] });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`vite build` then writes [PocketBase's minimal layout](https://pocketbase.io/docs/going-to-production/#minimal-setup):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
.voidbase/ generated; add it to .gitignore
|
|
21
|
+
main.ts voidbase composed with the project's server code
|
|
22
|
+
package.json
|
|
23
|
+
.gitignore pb_data/
|
|
24
|
+
pb_hooks/ routes/, middleware/, vb_hooks/, crons/ and queues/, compiled
|
|
25
|
+
pb_migrations/ the project's vb_migrations/, plus one file per Drizzle migration
|
|
26
|
+
pb_secrets/ the project's vb_secrets/: the declaration re-exported, the local values beside it
|
|
27
|
+
pb_public/ the client build, served at /
|
|
28
|
+
pb_data/ created on first run
|
|
29
|
+
void-entry.ts what the bundler builds into pb_hooks/void-app.js
|
|
30
|
+
tsconfig.json the fragment the project's tsconfig extends
|
|
31
|
+
shim-db.ts void/db and void/queues at runtime (see below)
|
|
32
|
+
shim-queues.ts
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`voidbase adapt` does the same pass without Vite, which is what CI and a pure-API project need.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bun run build # or: bunx voidbase adapt
|
|
39
|
+
bun .voidbase/main.ts --http 127.0.0.1:8090 # the site, /api, and the panel at /_/
|
|
40
|
+
cd .voidbase && bunx voidbase deploy # one Cloudflare Worker with all three
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What the project owns
|
|
44
|
+
|
|
45
|
+
The project root is Void's, with three additions, all optional and all named for the voidbase thing they are:
|
|
46
|
+
|
|
47
|
+
| path | what it does |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `vb_hooks/` | PocketBase's hooks, one per file, registered once when the app mounts |
|
|
50
|
+
| `vb_migrations/` | PocketBase JS migrations, copied into the generated `pb_migrations/` beside the ones generated from `db/migrations` |
|
|
51
|
+
| `vb_secrets/` | `main.ts` declares the app's configuration with `defineSecrets` and Void's validators, tiered secret / server / public; `secrets.json` (git-ignored) holds the local values |
|
|
52
|
+
|
|
53
|
+
They sit at the project root beside Void's own `db/`, which `vb_migrations/` is the counterpart of. Everything else
|
|
54
|
+
is Void's, and means what Void means by it: `routes/`, `middleware/`, `crons/` and `queues/` are the server code,
|
|
55
|
+
`src/` is library code they import, `pages/` and `public/` are the site.
|
|
56
|
+
|
|
57
|
+
There is no hooks directory to write by hand: `routes/`, `middleware/`, `crons/` and `queues/` **are** the hooks.
|
|
58
|
+
The adapter compiles them into `.voidbase/pb_hooks/`, so the generated app carries its whole route surface where a
|
|
59
|
+
PocketBase app carries it.
|
|
60
|
+
|
|
61
|
+
### How Void code becomes a hook
|
|
62
|
+
|
|
63
|
+
A pb_hooks file runs in a sandbox with PocketBase's globals and a `require` that reaches only its sibling hook
|
|
64
|
+
files. A Void route imports from npm. Three things close that gap:
|
|
65
|
+
|
|
66
|
+
1. **One bundle.** `routes/`, `middleware/`, `vb_hooks/`, `crons/` and `queues/` are built into a single CommonJS file,
|
|
67
|
+
`pb_hooks/void-app.js`, with everything they import (Void's runtime, Drizzle, the app's own modules) inlined.
|
|
68
|
+
The `void` specifier is rewritten to `void/handler` so the Vite plugin does not come with it, and the tsconfig
|
|
69
|
+
aliases (`@schema`, the project's own `@/*`, and the `void/db` / `void/queues` shims) are applied by the
|
|
70
|
+
bundler, an alias pointing at a directory resolving to its index file.
|
|
71
|
+
2. **No node builtins.** `node:async_hooks`, which Void's binding context needs, is rewritten to read
|
|
72
|
+
`globalThis.AsyncLocalStorage`; voidbase's hook runtime publishes it there on both runtimes. Any other builtin
|
|
73
|
+
fails the build with the name of the file that imported it.
|
|
74
|
+
3. **No rewriting of the bundle.** Every hook file normally goes through an await-insertion pass that rewrites
|
|
75
|
+
calls by method name (`delete`, `next`, `send`), which would corrupt bundled code. The generated file opens with
|
|
76
|
+
`// voidbase:raw`, and the compiler leaves it alone.
|
|
77
|
+
|
|
78
|
+
`pb_hooks/void-app.pb.js` is the small hook beside it: the one place the hook globals are in scope. It publishes
|
|
79
|
+
them on `globalThis` and then requires the bundle, in that order, so `pb` already works while a module is being
|
|
80
|
+
imported. `main.ts` is left with nothing but the runner.
|
|
81
|
+
|
|
82
|
+
### Writing API logic
|
|
83
|
+
|
|
84
|
+
`defineHandler` and `defineMiddleware` are how a voidbase app writes its API. A route that only needs Void's own
|
|
85
|
+
runtime (`void/db`, `void/storage`) needs nothing else; one that reads or writes PocketBase collections imports
|
|
86
|
+
PocketBase's API from the adapter, because a bundled route cannot import voidbase directly:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// routes/api/posts/[id].ts -> GET /api/posts/:id
|
|
90
|
+
import { defineHandler } from "void";
|
|
91
|
+
import { authOf, pb, requireAuth } from "@voidbase-cloud/voidbase/adapter";
|
|
92
|
+
|
|
93
|
+
export const GET = defineHandler(requireAuth("users"), async (c) => {
|
|
94
|
+
const post = await pb.$app.findRecordById("posts", c.req.param("id"));
|
|
95
|
+
if (!post) throw new pb.NotFoundError("No such post.");
|
|
96
|
+
if (post.getString("owner") !== authOf(c)?.id) throw new pb.ForbiddenError();
|
|
97
|
+
return { post: post.publicExport() };
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| import | what it is |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `pb.$app` | the data API: `findRecordById`, `findRecordsByFilter`, `save`, `delete`, `settings`, ... |
|
|
104
|
+
| `pb.Record`, `pb.$apis`, `pb.$os` | the rest of the hook surface a route is likely to want |
|
|
105
|
+
| `pb.BadRequestError` and friends | PocketBase's error classes, so a thrown error becomes the right HTTP response |
|
|
106
|
+
| `authOf(c)` | the authenticated record, exactly as a hook's `e.auth` |
|
|
107
|
+
| `requireAuth(...)`, `requireSuperuser()` | the Void-shaped counterparts of `$apis.requireAuth` and `requireSuperuserAuth` |
|
|
108
|
+
|
|
109
|
+
### Hooks
|
|
110
|
+
|
|
111
|
+
PocketBase's *event* hooks (`onBootstrap`, `onRecordCreate`, the mailer hooks) fire on a record or a lifecycle
|
|
112
|
+
moment rather than on a URL, so they have no `routes/` equivalent. They go in `vb_hooks/`: one file, one hook,
|
|
113
|
+
registered once when the app mounts, in file-name order.
|
|
114
|
+
|
|
115
|
+
A file names its hook where the build can read it, without running anything:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// vb_hooks/10.audit.ts -> onRecordAfterCreateSuccess, limited to `posts`
|
|
119
|
+
import { defineHook } from "@voidbase-cloud/voidbase/adapter";
|
|
120
|
+
|
|
121
|
+
export default defineHook("onRecordAfterCreateSuccess", async (e) => {
|
|
122
|
+
await e.next();
|
|
123
|
+
console.log("created", e.record?.id);
|
|
124
|
+
}, "posts");
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The trailing arguments are PocketBase's tags, the collections the hook is limited to. `routerUse`, PocketBase's
|
|
128
|
+
global request middleware, is a hook like any other and can be written here too, but Void already has a directory
|
|
129
|
+
for that, and it means the same thing:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// middleware/20.trace.ts -> every request
|
|
133
|
+
import { defineMiddleware } from "void";
|
|
134
|
+
|
|
135
|
+
export default defineMiddleware(async (c, next) => {
|
|
136
|
+
await next();
|
|
137
|
+
c.res.headers.set("x-trace-id", crypto.randomUUID());
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Two build errors keep the two apart. **A `vb_hooks/` file attached to no hook fails**, by name: there is nowhere to
|
|
142
|
+
register it, and silently dropping it would be worse. So does a name that is not one of PocketBase's hooks, and the
|
|
143
|
+
error offers the nearest real ones. **A `defineHook` left in `middleware/` fails too**, and says to move it: Void's
|
|
144
|
+
middleware is called with `(c, next)`, which is not what a hook handler expects.
|
|
145
|
+
|
|
146
|
+
`pb` is also usable while a module is being imported, which is what lets a plain module under `src/` register
|
|
147
|
+
something of its own. Void's build imports the same modules again to prerender the pages, with no PocketBase in the
|
|
148
|
+
process, so a registration made then is held and replayed, or dropped with the process. Reading anything
|
|
149
|
+
(`pb.$app` and the rest) outside a request, a cron tick or a hook still throws.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
### Configuration and secrets
|
|
153
|
+
|
|
154
|
+
The app's configuration is declared in `vb_secrets/main.ts`, with the validators a Void `env.ts` uses, and valued in
|
|
155
|
+
`vb_secrets/secrets.json`, which stays out of git (add it to `.gitignore`; the generated app's own `.gitignore`
|
|
156
|
+
already lists its copy):
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// vb_secrets/main.ts
|
|
160
|
+
import { defineSecrets, describe, string, number, url } from "@voidbase-cloud/voidbase/secrets";
|
|
161
|
+
|
|
162
|
+
export default defineSecrets({
|
|
163
|
+
SMTP_PASSWORD: describe(string().secret(), "the mail provider's SMTP password"), // the Worker's secrets
|
|
164
|
+
MAX_INSTANCES: number().default(5), // a Worker var
|
|
165
|
+
PUBLIC_API_URL: url().optional().public(), // a var the browser gets too
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
One declaration serves the three places the app runs. The Worker: `voidbase deploy` stores secrets as the Worker's
|
|
170
|
+
secrets and the rest as its vars, and refuses to deploy while a value is invalid or missing everywhere. The build:
|
|
171
|
+
every `public` key is inlined into the client as `import.meta.env.KEY`, parsed and defaulted, and no other key can
|
|
172
|
+
reach it. The static site therefore knows exactly what it was declared to know. Locally, `bun .voidbase/main.ts`
|
|
173
|
+
parses `secrets.json` and the shell into the environment, defaults included. Routes and hooks read
|
|
174
|
+
`c.env.SMTP_PASSWORD` or `pb.$os.getenv("SMTP_PASSWORD")`, or the typed values through the same declaration:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import config from "../../vb_secrets/main";
|
|
178
|
+
const { MAX_INSTANCES } = await config.read((n) => pb.$os.getenv(n)); // a number, on both runtimes
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The adapter writes a re-export into `.voidbase/pb_secrets/main.ts`, where `voidbase serve` and `voidbase deploy`
|
|
182
|
+
look for the declaration, and copies `secrets.json` beside it. A value in `secrets.json` that `main.ts` does not
|
|
183
|
+
declare fails the build, by name: it would silently never reach the Worker. Details, the tiers and the `voidbase
|
|
184
|
+
secrets` commands: `docs/deploy.md`.
|
|
185
|
+
|
|
186
|
+
## What maps to what
|
|
187
|
+
|
|
188
|
+
| Void | voidbase | notes |
|
|
189
|
+
| --- | --- | --- |
|
|
190
|
+
| `index.html`, `public/`, the client build | `.voidbase/pb_public/` | served at `/`; `404.html` is copied from `index.html` so a deep link behaves the same on Bun and on the asset layer |
|
|
191
|
+
| `routes/**/*.ts` | `.voidbase/pb_hooks/void-app.js` | `[id]` → `:id`, `[...rest]` → catch-all, `(group)/` stripped, `_file.ts` ignored, `.dev.ts` / `.prod.ts` honoured |
|
|
192
|
+
| `middleware/*.ts` | `routerUse(...)`, in file order | every request, as in Void (see below) |
|
|
193
|
+
| `vb_hooks/*.ts` | the hook each file names, registered once | `onBootstrap`, `onRecordCreate`, the mailer hooks |
|
|
194
|
+
| `vb_secrets/main.ts` + `secrets.json` | `.voidbase/pb_secrets/main.ts` (a re-export) + `secrets.json` | secrets to the Worker's secrets, the rest to its vars, `public` keys into the client build (see below) |
|
|
195
|
+
| `crons/*.ts` | `cronAdd(<file name>, cron, handler)` | listed by `GET /api/crons`, runnable with `POST /api/crons/<name>` |
|
|
196
|
+
| `queues/*.ts` | a voidbase job per message | `void/queues` and `c.env.QUEUE_<NAME>` produce; the consumer runs on the jobs queue, or inline where there is none |
|
|
197
|
+
| `db/schema.ts` + `void/db` | Drizzle over voidbase's D1 | the same database PocketBase's collections live in |
|
|
198
|
+
| `db/migrations/*.sql` | `.voidbase/pb_migrations/<name>.void.js` | applied and recorded like any other migration, on both runtimes |
|
|
199
|
+
|
|
200
|
+
Point the project's `tsconfig.json` at the adapter's fragment so the editor and Bun agree:
|
|
201
|
+
|
|
202
|
+
```jsonc
|
|
203
|
+
{ "extends": "./.voidbase/tsconfig.json" } // instead of "./.void/tsconfig.json"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Void's own fragment maps `void/db` and `void/queues` to declaration files. That is right for `tsc` and wrong for
|
|
207
|
+
Bun, which honours `paths` at runtime and would resolve those imports to a `.d.ts` that exports nothing. The
|
|
208
|
+
adapter's fragment repoints those two at shims that take the values from Void's published runtime and the types
|
|
209
|
+
from Void's declarations, and passes every other mapping (`@schema`, `void/routes`) through unchanged.
|
|
210
|
+
|
|
211
|
+
## Two shapes of app
|
|
212
|
+
|
|
213
|
+
The adapter looks at what the project actually has:
|
|
214
|
+
|
|
215
|
+
- **Static.** No `routes/`, `middleware/`, `vb_hooks/`, `crons/` or `queues/` (`vb_secrets/` alone changes nothing). The generated app is `main.ts`
|
|
216
|
+
plus `pb_public`.
|
|
217
|
+
- **Server.** Anything else. The bundle and its hook go into `pb_hooks/`. `voidbase deploy`, run from `.voidbase/`,
|
|
218
|
+
ships that directory whole, so the same code serves on Cloudflare.
|
|
219
|
+
|
|
220
|
+
Pages are prerendered when `void.json` sets `"output": "static"`; they land in `pb_public` as plain HTML and need no
|
|
221
|
+
runtime. Pages that still render per request have nowhere to run here, and the build says so.
|
|
222
|
+
|
|
223
|
+
## How the server code runs
|
|
224
|
+
|
|
225
|
+
Void's `defineHandler` returns a plain Hono handler, and voidbase's app is Hono, so handlers run unchanged. Two
|
|
226
|
+
details make that true rather than nearly true:
|
|
227
|
+
|
|
228
|
+
- Routes register through `routerAdd`, the registry every pb_hooks route uses. The RequestEvent it hands the
|
|
229
|
+
handler carries `.c`, the real Hono context, and the adapter overlays the route's own parameters on
|
|
230
|
+
`c.req.param()`, since the hook router matched a pattern rather than Hono's own.
|
|
231
|
+
- Every handler, cron and queue consumer runs inside `withRuntimeEnv`, Void's binding context. That is why
|
|
232
|
+
`void/db`, `void/storage`, `void/env` and `void/queues` resolve against voidbase's D1 and R2 with no shim of
|
|
233
|
+
their own, and why `c.env` carries the app's queue producers.
|
|
234
|
+
|
|
235
|
+
Return values convert exactly as Void converts them (object → JSON, string → HTML, `null` → 204, `Response` as-is),
|
|
236
|
+
because the adapter calls Void's own `convertReturnValue`.
|
|
237
|
+
|
|
238
|
+
## Things to know
|
|
239
|
+
|
|
240
|
+
**voidbase's API wins.** `/api/collections`, `/api/files`, `/api/realtime`, `/api/settings`, `/api/logs`,
|
|
241
|
+
`/api/backups`, `/api/crons`, `/api/batch`, `/api/health`, `/api/webauthn` (voidbase's passkey endpoints) and `/_/`
|
|
242
|
+
are voidbase's own. An app route on one of those paths never runs; the build warns and names it. Everything else
|
|
243
|
+
under `/api` is yours.
|
|
244
|
+
|
|
245
|
+
**Middleware runs on every request.** `middleware/` registers through `routerUse`, PocketBase's own global
|
|
246
|
+
middleware, so it means what Void means by it: every request, in file order, before whatever answers. That includes
|
|
247
|
+
PocketBase's endpoints and the admin panel, so a middleware that throws takes the whole backend with it. Guard on
|
|
248
|
+
the path when a middleware is only meant for the app's own routes.
|
|
249
|
+
|
|
250
|
+
**Queues are one queue.** Cloudflare Queues are provisioned per instance, so every app queue rides voidbase's own
|
|
251
|
+
jobs queue as a `{ type: "queue", queue, body }` message and is fanned back out to the right consumer. Without a
|
|
252
|
+
queue binding (the Bun runtime, or a deploy whose token could not create one) `send` runs the consumer inline. A
|
|
253
|
+
consumer that throws, or calls `retry()`, is retried by voidbase's job runner.
|
|
254
|
+
|
|
255
|
+
**Not carried over.** The build warns and keeps going:
|
|
256
|
+
|
|
257
|
+
| what | why |
|
|
258
|
+
| --- | --- |
|
|
259
|
+
| `pages/` that render per request | prerender them with `"output": "static"` and they land in `pb_public`; there is no page renderer at runtime |
|
|
260
|
+
| `routes/**/*.ws.ts` | document WebSockets are Durable Objects, and voidbase's realtime hub owns that binding |
|
|
261
|
+
| `void/kv` | voidbase binds D1 and R2 only; a collection is the place for that data |
|
|
262
|
+
| `void/isr` | ISR runs in the Void platform's dispatch worker, which a self-hosted app does not have |
|
|
263
|
+
|
|
264
|
+
**Build with Bun.** voidbase ships TypeScript sources, and Vite loads its config through the runtime, so use
|
|
265
|
+
`bunx --bun vite build` (or `bun run build` with Bun as the package manager). `voidbase adapt` has no such
|
|
266
|
+
constraint.
|
|
267
|
+
|
|
268
|
+
**Nothing is written outside `.voidbase/`.** Delete the directory and the next build makes it again, so it belongs
|
|
269
|
+
in `.gitignore` and never in review.
|
|
270
|
+
|
|
271
|
+
## Testing it
|
|
272
|
+
|
|
273
|
+
`test/adapter.ts` converts `test/fixtures/void-app`, boots it with the generated `main.ts` and exercises the whole
|
|
274
|
+
surface: the generated layout, route paths and parameters, literal-beats-parameter ordering, middleware order and
|
|
275
|
+
its reach over PocketBase's own endpoints, `void/db` through the shim, `void/storage`, a queue round trip, a cron
|
|
276
|
+
run, both kinds of migration, an `onBootstrap` hook running exactly once, a tagged event hook, a tsconfig alias
|
|
277
|
+
resolving to a directory's index file, the four build errors (a hook attached to nothing, a misspelt hook name, a
|
|
278
|
+
hook left in `middleware/`, a secret value nothing declares), `vb_secrets/` reaching the app through `$os.getenv`,
|
|
279
|
+
and that nothing is written outside `.voidbase/`. Run it with
|
|
280
|
+
`bun test/adapter.ts`, or as part of `bash scripts/ci.sh` (step `adapter`).
|