@voltro/cli 0.11.0 → 0.11.2
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 +90 -0
- package/THIRD-PARTY-NOTICES.md +1047 -22
- package/bin/voltro.mjs +16 -7
- package/dist/{apiBuild-BUbgGz73.js → apiBuild-BVIiATXr.js} +15 -11
- package/dist/apiBuild-C_KY5oY_.js +2 -0
- package/dist/bin.js +2 -2
- package/dist/bootTiming-BdyP9nYw.js +38 -0
- package/dist/{commands-C1qBfIBI.js → commands-C0ytPFfl.js} +2185 -2039
- package/dist/dev-DH13Ysgs.js +2 -0
- package/dist/{dev-ITlO-epO.js → dev-zJGsTTNb.js} +2202 -2189
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-BeMjgt8x.js → inspectMetrics-BuBwg1yW.js} +1169 -1137
- package/dist/{serveCommand-mpsE3vLY.js → serveCommand-Dd3oLzbo.js} +312 -340
- package/dist/serveEntry.js +3 -3
- package/dist/{start-C_M4fGS2.js → start-ChN6PO-c.js} +338 -323
- package/dist/startEntry.js +2 -2
- package/package.json +23 -17
- package/templates/agent-docs/data.md +72 -1
- package/templates/agent-docs/database/columntypes.md +7 -0
- package/templates/agent-docs/deployment.md +51 -0
- package/templates/agent-docs/internationalization.md +71 -0
- package/templates/agent-docs/multi-tenancy.md +33 -0
- package/templates/agent-docs/testing.md +26 -0
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +8 -8
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/dist/apiBuild-BCv3bekE.js +0 -2
- package/dist/dev-DKASElZP.js +0 -2
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { bt as e } from "./inspectMetrics-BuBwg1yW.js";
|
|
2
|
+
import { t } from "./start-ChN6PO-c.js";
|
|
3
3
|
export { e as loadDotEnv, t as runStartCommand };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
"types": "./dist/serveEntry.d.ts",
|
|
46
46
|
"import": "./dist/serveEntry.js",
|
|
47
47
|
"default": "./dist/serveEntry.js"
|
|
48
|
+
},
|
|
49
|
+
"./startEntry": {
|
|
50
|
+
"types": "./dist/startEntry.d.ts",
|
|
51
|
+
"import": "./dist/startEntry.js",
|
|
52
|
+
"default": "./dist/startEntry.js"
|
|
48
53
|
}
|
|
49
54
|
},
|
|
50
55
|
"main": "./dist/index.js",
|
|
@@ -57,22 +62,22 @@
|
|
|
57
62
|
"@effect/platform-node": "^0.107.0",
|
|
58
63
|
"@effect/sql": "^0.51.1",
|
|
59
64
|
"@effect/workflow": "^0.18.2",
|
|
60
|
-
"@voltro/ai": "0.11.
|
|
61
|
-
"@voltro/cache": "0.11.
|
|
62
|
-
"@voltro/data-transfer": "0.11.
|
|
63
|
-
"@voltro/database": "0.11.
|
|
64
|
-
"@voltro/env": "0.11.
|
|
65
|
-
"@voltro/kv": "0.11.
|
|
66
|
-
"@voltro/logger": "0.11.
|
|
67
|
-
"@voltro/plugin-auth": "0.11.
|
|
68
|
-
"@voltro/plugin-broadcast": "0.11.
|
|
69
|
-
"@voltro/plugin-mail": "0.11.
|
|
70
|
-
"@voltro/plugin-storage": "0.11.
|
|
71
|
-
"@voltro/plugin-webhooks": "0.11.
|
|
72
|
-
"@voltro/protocol": "0.11.
|
|
73
|
-
"@voltro/runtime": "0.11.
|
|
74
|
-
"@voltro/serverless": "0.11.
|
|
75
|
-
"@voltro/workflow": "0.11.
|
|
65
|
+
"@voltro/ai": "0.11.2",
|
|
66
|
+
"@voltro/cache": "0.11.2",
|
|
67
|
+
"@voltro/data-transfer": "0.11.2",
|
|
68
|
+
"@voltro/database": "0.11.2",
|
|
69
|
+
"@voltro/env": "0.11.2",
|
|
70
|
+
"@voltro/kv": "0.11.2",
|
|
71
|
+
"@voltro/logger": "0.11.2",
|
|
72
|
+
"@voltro/plugin-auth": "0.11.2",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.11.2",
|
|
74
|
+
"@voltro/plugin-mail": "0.11.2",
|
|
75
|
+
"@voltro/plugin-storage": "0.11.2",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.11.2",
|
|
77
|
+
"@voltro/protocol": "0.11.2",
|
|
78
|
+
"@voltro/runtime": "0.11.2",
|
|
79
|
+
"@voltro/serverless": "0.11.2",
|
|
80
|
+
"@voltro/workflow": "0.11.2",
|
|
76
81
|
"chokidar": "^5.0.0",
|
|
77
82
|
"ioredis": "^5.11.1",
|
|
78
83
|
"tinyglobby": "^0.2.17",
|
|
@@ -80,6 +85,7 @@
|
|
|
80
85
|
},
|
|
81
86
|
"optionalDependencies": {
|
|
82
87
|
"@tailwindcss/vite": "^4.3.2",
|
|
88
|
+
"@vercel/nft": "^1.10.2",
|
|
83
89
|
"@vitejs/plugin-react": "^6.0.3",
|
|
84
90
|
"esbuild": "^0.28.0",
|
|
85
91
|
"tsx": "^4.23.0",
|
|
@@ -293,6 +293,28 @@ export default async (_input: Record<string, never>, ctx) => {
|
|
|
293
293
|
|
|
294
294
|
For computed queries, `source` is the reactivity trigger. When any row in the source table changes, the runtime re-runs the executor and emits the new value if it changed. If the executor reads more than one table (a join or matrix), declare `source` as an array — the executor re-runs when **any** listed table changes (e.g. `source: ['skills', 'ratings']`).
|
|
295
295
|
|
|
296
|
+
### Type the computed return against `output` — `defineExecutor`
|
|
297
|
+
|
|
298
|
+
A computed executor's return is encoded through the descriptor's `output` schema — but the bare default export's return is **not** type-checked against it. So a handler that builds `{ publishedAt: row.publishedAt.getTime() }` where `output` is `timestampMs` (Type `Date`) compiles green and throws `Expected DateFromSelf, actual 1784…` at *encode* time, which **Dies the subscription**. For a nullable date it's a time-bomb: fine while the value is null, exploding the instant it becomes non-null (a publish, say).
|
|
299
|
+
|
|
300
|
+
Wrap the handler in `defineExecutor(descriptor, fn)` — it pins the return to `Schema.Type<output>`, turning that into a compile error at the handler:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// apps/api/queries/notes.summary.query.server.ts
|
|
304
|
+
import { defineExecutor } from '@voltro/runtime'
|
|
305
|
+
import { notesSummary } from './notes.summary.query'
|
|
306
|
+
|
|
307
|
+
export default defineExecutor(notesSummary, async (_input, ctx) => {
|
|
308
|
+
const notes = await ctx.store.select('notes').all()
|
|
309
|
+
return {
|
|
310
|
+
open: notes.filter((note) => !note.done).length,
|
|
311
|
+
done: notes.filter((note) => note.done).length,
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
It's a runtime identity (returns the handler unchanged) — the whole value is the compile check. The Effect error and requirement channels stay inferred; only the success value is constrained. A **descriptor-return** (reactive) executor is allowed through unchecked: the store produces the rows, so a value-level return type can't express the row-vs-`output` check. `defineExecutor` works the same for `defineMutation` / `defineAction` handlers.
|
|
317
|
+
|
|
296
318
|
## Auto-optimistic source
|
|
297
319
|
|
|
298
320
|
`source` also connects query caches to mutation `target` metadata:
|
|
@@ -625,6 +647,48 @@ The descriptor is the wire contract. The `.mutation.server.ts` file is the serve
|
|
|
625
647
|
5. Commit the transaction.
|
|
626
648
|
6. Drain the batched change events so matching query subscriptions receive new snapshots or deltas.
|
|
627
649
|
|
|
650
|
+
## Missing required columns fail LOUD, at the call
|
|
651
|
+
|
|
652
|
+
`ctx.store.insert` / `upsert` / `insertIgnore` check the payload against the
|
|
653
|
+
table before the statement runs. A column that is NOT NULL, has no default, and
|
|
654
|
+
isn't auto-stamped — a business FK like `teamId`, a plain `timestamp()` like
|
|
655
|
+
`lastRefreshedAt` — must be present, or the write raises a typed
|
|
656
|
+
`TableValidationFailed` naming it:
|
|
657
|
+
|
|
658
|
+
```txt
|
|
659
|
+
TableValidationFailed: missing required column 'lastRefreshedAt'
|
|
660
|
+
— NOT NULL with no default and not auto-stamped
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Without this the omission slips past `tsc` and boot and surfaces only as a raw
|
|
664
|
+
dialect `SqlError: Failed to execute statement` — and only on the INSERT path, so
|
|
665
|
+
it stays dormant until the first row with no existing cache entry. An
|
|
666
|
+
`upsert` / `insertIgnore` whose payload omits one of its own `conflictColumns` is
|
|
667
|
+
named the same way (an absent conflict key can't match its target). The check
|
|
668
|
+
runs after stamping, so auto-id, `tenantId`, and audit columns never trip it, and
|
|
669
|
+
it skips nullable and defaulted columns — the ones you may legitimately omit.
|
|
670
|
+
|
|
671
|
+
### Catch it at COMPILE time — `insertRow` / `upsertRow`
|
|
672
|
+
|
|
673
|
+
The runtime guard above is the backstop. To catch a missing column at compile
|
|
674
|
+
time, use `insertRow` / `upsertRow` — they take the **table object** (not a
|
|
675
|
+
string name), so the payload is typed against the table's required columns:
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
import { insertRow } from '@voltro/database'
|
|
679
|
+
|
|
680
|
+
// ✗ compile error — lastRefreshedAt is NOT NULL with no default
|
|
681
|
+
await insertRow(ctx.store, roadmapEpicStats, { teamId })
|
|
682
|
+
// ✓
|
|
683
|
+
await insertRow(ctx.store, roadmapEpicStats, { teamId, lastRefreshedAt: new Date() })
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
`InferInsertRow<T>` makes a column optional exactly when you may omit it — it's
|
|
687
|
+
nullable, has a `.default()`, or is a framework-filled `id` / `tenantId` / audit
|
|
688
|
+
column — and required otherwise. `upsertRow`'s `conflictColumns` are constrained
|
|
689
|
+
to the table's own columns, so a typo is a compile error too. The string-keyed
|
|
690
|
+
`ctx.store.insert` / `upsert` still work unchanged; the typed seam is opt-in.
|
|
691
|
+
|
|
628
692
|
## Partial updates: `ctx.store.applyDefined`
|
|
629
693
|
|
|
630
694
|
A partial-update mutation should set only the fields the caller actually sent — not overwrite an omitted field with `undefined`. Instead of hand-writing `if (input.x !== undefined) patch.x = input.x` per field, use `ctx.store.applyDefined(input, keys)`:
|
|
@@ -1046,11 +1110,18 @@ cancellation email. Declare the overlap instead of relying on luck:
|
|
|
1046
1110
|
sequence()
|
|
1047
1111
|
.step('draft', () => createDraft.run(input), { undo: (d) => discardDraft.run({ id: d.id }) })
|
|
1048
1112
|
.step('jira', (c) => createTicket.run({ draftId: c.draft.id }), {
|
|
1049
|
-
undo: (t) => deleteJiraDraftTicket.run({ key: t.key }),
|
|
1113
|
+
undo: (t, ctx) => deleteJiraDraftTicket.run({ key: t.key, draftId: ctx.draft.id }),
|
|
1050
1114
|
covers: ['draft'],
|
|
1051
1115
|
})
|
|
1052
1116
|
```
|
|
1053
1117
|
|
|
1118
|
+
An `undo` receives `(result, ctx)` — its own result *and* the context up to that
|
|
1119
|
+
step, the same context `covers` and `when` see. An inverse that needs an id from
|
|
1120
|
+
an earlier step (here `deleteJiraDraftTicket` wants both the jira key and the
|
|
1121
|
+
`draftId`) reads it from `ctx` rather than the step re-returning it just so the
|
|
1122
|
+
undo can reach it. A later step's result is not in `ctx`: it is rolled back
|
|
1123
|
+
before this one, so reading it would be reading something already reversed.
|
|
1124
|
+
|
|
1054
1125
|
If the covering undo *fails*, the covered steps are neither run nor claimed —
|
|
1055
1126
|
whether the cascade got that far is unknown, and both guesses are wrong. They
|
|
1056
1127
|
come back in `compensationUncertain` so you can reconcile.
|
|
@@ -208,6 +208,13 @@ const rows = await ctx.store.query(database.people.descriptor)
|
|
|
208
208
|
rows[0]?.fullName // → 'Mario Lima'
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
+
**A generated column is never caller-supplied.** It's omitted from the typed insert
|
|
212
|
+
payload — `insertRow(store, people, { firstName, lastName })` compiles with no
|
|
213
|
+
`fullName`, and its type is optional even when the column is NOT nullable and has no
|
|
214
|
+
default (the DB owns the value). If a loose `store.insert` passes one anyway, the
|
|
215
|
+
framework strips it before the INSERT — MariaDB and Postgres both reject an explicit
|
|
216
|
+
value for a generated column, so this keeps that error from reaching the dialect.
|
|
217
|
+
|
|
211
218
|
## STORED vs VIRTUAL
|
|
212
219
|
|
|
213
220
|
`{ stored: true }` (the rare-but-useful case):
|
|
@@ -234,6 +234,32 @@ your-product.com {
|
|
|
234
234
|
|
|
235
235
|
The path is **relative** in the bundle, so the host resolves at runtime — **one built image works across every environment**; only the path is fixed. Keep the socket **same-origin** with the web app: the session cookie rides the WS upgrade, and a cross-origin socket wouldn't carry it (every subscription would run anonymous). For an api on a genuinely separate origin, use the api spec's absolute `url` on the web side plus a token-based `headers` resolver instead of a cookie. (One custom `wsPath` per origin — the framework fails the build if two same-origin apis would collide on it.)
|
|
236
236
|
|
|
237
|
+
### Split web/api deployment — the SSR api origin — `serverUrl`
|
|
238
|
+
|
|
239
|
+
When `web` and `api` deploy as **separate services** (e.g. different k8s namespaces, one ingress routing `/api*` → api), the **browser** reaches the api by its relative `wsPath` against the page origin — but a `renderMode:'ssr'` page's `ctx.query` runs in the **web pod**, which has no page origin and must reach the api by **internal DNS**. These are genuinely different endpoints, so `url` (which is *also* the browser's ws origin) can't express the internal one without shipping an unreachable host to the browser.
|
|
240
|
+
|
|
241
|
+
Set the server-only `serverUrl` (or the env override) to the api's internal origin:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// web/app.config.ts
|
|
245
|
+
apis: {
|
|
246
|
+
app: {
|
|
247
|
+
package: '@app/api',
|
|
248
|
+
transport: { wsPath: '/api/sync' }, // browser → relative, same-origin
|
|
249
|
+
serverUrl: process.env.VOLTRO_API_ORIGIN, // web pod → internal DNS (SSR only)
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# or set it purely from ops, no app.config edit — per-api, name upper-cased:
|
|
256
|
+
VOLTRO_API_ORIGIN_APP=http://api.my-namespace.svc.cluster.local
|
|
257
|
+
# or a single shared origin for every api:
|
|
258
|
+
VOLTRO_API_ORIGIN=http://api.my-namespace.svc.cluster.local
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`serverUrl` / `VOLTRO_API_ORIGIN*` is used **only** for the SSR `POST /rpc` and is **never** emitted into the browser bundle. Resolution order: env > `serverUrl` > (under `voltro dev` only) the dev proxy target > an external api's absolute `url`. Under `voltro start`, a package api with **none** of these set makes the SSR query **fail loud** — naming the api and the config to set — instead of dialing the dev `localhost` port and surfacing a cryptic `ECONNREFUSED` in a 500.
|
|
262
|
+
|
|
237
263
|
### `allowedHosts` — fronting the web app on a real domain
|
|
238
264
|
|
|
239
265
|
Vite 8's DNS-rebind guard returns HTTP **403** for any request whose `Host`
|
|
@@ -275,6 +301,7 @@ cause.
|
|
|
275
301
|
| `AI_PROVIDER` | when using `@voltro/ai` | `anthropic` / `openai` / `mock` |
|
|
276
302
|
| `AI_API_KEY` | with `AI_PROVIDER` | Provider's API key |
|
|
277
303
|
| `SSR_CACHE` | optional | `memory` (default) or `postgres` (needs `PG_*` set too) for the ISR cache |
|
|
304
|
+
| `VOLTRO_API_ORIGIN` / `VOLTRO_API_ORIGIN_<NAME>` | split web/api deploy with SSR pages | The api's internal origin the WEB pod uses for SSR `ctx.query` (`http://api.<ns>.svc.cluster.local`). Per-api `_<NAME>` (name upper-cased) wins over the shared one and over `serverUrl`. Never sent to the browser |
|
|
278
305
|
| `PORT` | optional | App's listen port (overrides `app.config.ts.port`) |
|
|
279
306
|
| `VOLTRO_INSPECT` | optional | `off` to disable `_voltro/inspect/*` in prod |
|
|
280
307
|
| `VOLTRO_INSPECT_TOKEN` | recommended | Bearer token guard on inspect endpoints |
|
|
@@ -402,6 +429,24 @@ The phases, in boot order:
|
|
|
402
429
|
|
|
403
430
|
Read this from the container's own logs at its real CPU allotment, not a beefy dev machine: a `modules` phase that dwarfs everything else means the cold start is dominated by evaluating the dependency graph (raise the container's CPU, or keep one instance warm); a fat `store` phase points at the database connection instead. `VOLTRO_BOOT_TIMING` adds a handful of `performance.now()` calls and one log line — it is safe to leave on in production.
|
|
404
431
|
|
|
432
|
+
A **web app** (`voltro start`) reports its own phases under the same flag, on its ready line:
|
|
433
|
+
|
|
434
|
+
```text
|
|
435
|
+
boot phases (ready in 640ms) { bootMs: 640, phases: { modules: 590, config: 6, scan: 12, provider: 22, routes: 4, cdc: 0, ready: 6 } }
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
| Phase | What it covers |
|
|
439
|
+
| --- | --- |
|
|
440
|
+
| `modules` | node init + module-graph load/compile — the dominant cold-start phase, same as the api. Note the web start path has **no serve-bundle equivalent** yet, so `voltro build` does not collapse this phase for a web app the way it does for `voltro serve`; it is the framework graph evaluated unbundled on every cold process |
|
|
441
|
+
| `config` | `app.config.ts` + the typed-env gate |
|
|
442
|
+
| `scan` | reading the built shell, resolving the api origin, the ISR cache backend, and walking `src/pages` |
|
|
443
|
+
| `provider` | importing the precompiled SSR bundle (`dist/server/ssrEntry.js`). Stays small because page modules load lazily — a fat `provider` phase means the SSR bundle regressed to eager imports |
|
|
444
|
+
| `routes` | building per-route metadata. Cheap from the build-time `pageMeta` manifest; fat only when it fell back to importing every page module |
|
|
445
|
+
| `cdc` | postgres `LISTEN` wiring for ISR cache invalidation (zero without it) |
|
|
446
|
+
| `ready` | the HTTP listener flip |
|
|
447
|
+
|
|
448
|
+
**What boot timing cannot see:** `performance.now()` starts at *process* start, so everything before node runs — the container's scale-from-zero scheduling and image pull — is invisible to it. If a request that took many seconds end-to-end shows a `ready in` well under a second, the tail is infrastructure (image size, cold scheduling), not framework boot — split it with your platform's own request/container timestamps, not with more marks.
|
|
449
|
+
|
|
405
450
|
## Precompiled boot
|
|
406
451
|
|
|
407
452
|
`voltro build` precompiles the whole serve path — the framework, Effect, and your app — into a single **serve bundle**, and `voltro serve` boots from it directly. Instead of resolving and compiling the full module graph on every cold process, the boot loads one prebuilt file with your app modules as lazy chunks. This collapses the `modules` phase — `serve: ready` drops from ~1000 ms to ~180 ms; the win is larger on a scale-to-zero container with a cold filesystem, where per-module resolution costs the most. It applies whether your app uses a SQL driver or the memory store: your declared driver (e.g. `@voltro/sql-postgres`) is inlined into the bundle, and only its native binding (`pg`) stays external.
|
|
@@ -415,6 +460,12 @@ Building an API app produces the bundle, and `voltro serve` boots from it. In **
|
|
|
415
460
|
|
|
416
461
|
Because production never transpiles, the serve image needs none of the build toolchain. The framework declares `tsx`, `esbuild`, `vite`, and Tailwind as **optional** dependencies of `@voltro/cli`, and the production Dockerfiles isolate the app with `pnpm --prod --no-optional deploy` — which drops that whole tree (and its native binaries) from the image. A serve image ships only what it runs at runtime: your app, the framework, and the one SQL driver you declared.
|
|
417
462
|
|
|
463
|
+
And it shrinks further, the same way a web image does. The serve bundle is directly executable, so the production API container runs it with `node .framework/dist-api/serveBundle/serveEntry.js` — not `pnpm voltro serve` — with no pnpm process and no `@voltro/cli` bin to resolve at boot. That lets `voltro prune-runtime` drop `@voltro/cli` and the whole inlined framework tree from the runtime image too; what stays is the native SQL driver your app actually declared (traced + kept automatically) — a memory-store api's `node_modules` fell from ~146 MB to ~11 MB in a fixture. The serve entry chdir's to the app root before its app-module registry keys are computed, so the pruned, relocated tree still resolves every module.
|
|
464
|
+
|
|
465
|
+
**Web apps get the same treatment.** `voltro build` also precompiles the `voltro start` runtime into a **start bundle** (`.framework/dist-web/startBundle/startEntry.js`, framework inlined) — so a cold web boot loads one artefact instead of resolving the whole framework graph. This collapses the web `modules` phase the same way the serve bundle does (~17× on a small app in practice), which matters most exactly where it hurts: a scale-from-zero container on a fraction of a vCPU. In production the container runs that bundle **directly** — `node .framework/dist-web/startBundle/startEntry.js`, not `pnpm voltro start` — so there is no pnpm process and no `@voltro/cli` bin to resolve at boot; the bundle is a self-contained, relocation-safe entrypoint (its main-guard chdir's to the app root so cwd-based resolution holds anywhere). In development `voltro start` imports the same bundle; if it ever fails to build or load, that path falls back to the ordinary per-module boot — slower, never broken.
|
|
466
|
+
|
|
467
|
+
**And the image itself shrinks — to almost nothing.** Because the SSR bundle, the start bundle, and the precompiled config all inline + tree-shake the framework, *and the production entrypoint is the bundle itself rather than the CLI*, a booted web app needs from `node_modules` only the runtime-external **native** leaves it actually reaches (a SQL driver an ISR or config path touches) — everything else is already compiled into the bundles. `voltro prune-runtime` traces the real reachable set from that entrypoint (`@vercel/nft`, the same tool behind Next.js `output: standalone`) and drops the rest — including `@voltro/cli` and the whole inlined effect/React tree, which nothing at runtime imports any more. It's automatic — a native driver you use is traced and kept, one you don't is dropped, no per-app allow-list — and for a static/SSR marketing site with no native runtime dependency, `node_modules` collapses to **zero** (measured on a fixture: 151 MB → ~0 B): the image is just the node base plus `.framework`. A build-time **boot smoke** then starts the *pruned* tree with the real `node …/startEntry.js` entrypoint and fails the build unless it reaches `start: ready`, so a slimmed image that can't boot never ships. A smaller image is a faster cold pull on a fresh node — the other half of the scale-from-zero latency the boot bundle can't touch.
|
|
468
|
+
|
|
418
469
|
## Tiers (Voltro Cloud — coming soon)
|
|
419
470
|
|
|
420
471
|
Managed cloud hosting is not yet available; the Pro / Enterprise rows below are the planned managed tiers. Today you self-host and run `voltro dormancy` yourself.
|
|
@@ -115,6 +115,18 @@ export default defineLocale<typeof en>()({
|
|
|
115
115
|
|
|
116
116
|
**Don't roll your own ICU MessageFormat.** Pluralization, gender, and ordinals across 50+ languages is a solved problem; reimplementing it produces bugs that only surface in specific locales (Turkish dotted-i, Arabic plurals, Finnish cases).
|
|
117
117
|
|
|
118
|
+
**Check placeholder parity, not just key parity.** `defineLocale` enforces that every locale has the same KEYS, but not that each message has the same `{var}` set — a translation that drops or renames a `{var}` compiles and boots, then throws `The intl string context variable "date" was not provided` only in that locale, only when the message renders. `assertCatalogParity` catches it up front:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { assertCatalogParity } from '@voltro/i18n'
|
|
122
|
+
import en from './locales/en'
|
|
123
|
+
import de from './locales/de'
|
|
124
|
+
|
|
125
|
+
assertCatalogParity({ en, de }) // throws, listing every {var} drift
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Call it in a test (or at boot with `{ onMismatch: 'warn' }`). Plural argument names are compared; a plural's inner `{# item}` branches are not mistaken for placeholders.
|
|
129
|
+
|
|
118
130
|
## Reading translations in components
|
|
119
131
|
|
|
120
132
|
```tsx
|
|
@@ -144,6 +156,65 @@ const locale = useLocale()
|
|
|
144
156
|
- **`useLocale()`** — the active locale code, as React state inside the provider.
|
|
145
157
|
- **`useMessages()`** — the active locale's RAW catalog (`useMessages()['some.id']`): the unformatted ICU template, not the formatted output. For when you need the raw string — e.g. to feed your own formatter, or a lookup that must not run ICU.
|
|
146
158
|
|
|
159
|
+
## Compile-time keys AND params — `createTypedMessages`
|
|
160
|
+
|
|
161
|
+
`defineLocale` catches a missing KEY at compile time, but the bare `useT` / `useTFn` don't type the KEY or the PARAMS at the *call site* — so `useT('home.greeting')` (a message that needs `{name}`) compiles and then throws at render (`The intl string context variable "name" was not provided`). `createTypedMessages<typeof en>()` binds the catalog's literal message types to `useT` / `useTFn` / `<T>` so both are compile errors:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// src/i18n.ts — call once with your base catalog, re-export the result
|
|
165
|
+
import { createTypedMessages } from '@voltro/i18n'
|
|
166
|
+
import type en from './locales/en'
|
|
167
|
+
|
|
168
|
+
export const { useT, useTFn, T } = createTypedMessages<typeof en>()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// now, everywhere you import from './i18n' instead of '@voltro/i18n':
|
|
173
|
+
useT('home.greeting') // ✗ compile error — expected { name: … }
|
|
174
|
+
useT('home.greeting', { name }) // ✓
|
|
175
|
+
useT('header.search') // ✓ no placeholders → no values arg
|
|
176
|
+
useT('does.not.exist') // ✗ compile error — not a catalog key
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
It's almost pure type refinement — the runtime is the same `useT` / `useTFn` / `<T>` (plus a `.dynamic` escape, below), only the signatures narrow to your catalog. Requires the base catalog to be `as const` (so its message strings survive as literal types).
|
|
180
|
+
|
|
181
|
+
**Scope.** Simple `{name}` and single-argument `{count, number}` placeholders are extracted and required. For a *nested* inline ICU message (`{count, plural, one {…} other {…}}` / `select`), the **top-level arg** (`count`) is required and a branch's inner text is NOT mistaken for a var — so `t('duration', { count })` typechecks and renders. A REAL var nested inside a branch (`other {# — {discipline}}`) is not collected, so it reads as *not required* rather than wrongly required; pass it via the loose values bag. Pluralising in JS over simple `{count}` messages (`items.map((i) => t('cart.one', { count: i.n }))`) stays fully typed. `<T>` gets a typed key with loose values, because its rich-text `<tag>` renderers can't be modelled by placeholder extraction.
|
|
182
|
+
|
|
183
|
+
### Runtime-computed keys — `t.dynamic`
|
|
184
|
+
|
|
185
|
+
For a key you build at runtime, do NOT cast it to the catalog key union — that union spans placeholder-bearing keys, so the call then demands a spurious 2nd ICU arg. Use `t.dynamic`, a plain-string escape with no forced args, on both `useT` and the `useTFn()` result:
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
const t = useTFn()
|
|
189
|
+
t.dynamic(`status.${row.state}`) // ✓ plain string, no forced arg
|
|
190
|
+
t.dynamic(`greeting.${kind}`, { name }) // values still allowed, but loose
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Passing `t` across a package boundary — `LooseTFunction`
|
|
194
|
+
|
|
195
|
+
Type a `t` pass-through param as `TypedTFunction<C>` to keep it strict — the strict type propagates down through helper signatures. At a boundary that *can't* import your catalog (a shared UI package), type the param `LooseTFunction` (`(id: string, values?) => string`) instead of `(...args: any[]) => string`, and pass `t.dynamic` — which IS a `LooseTFunction`. A strict `TypedTFunction` is deliberately NOT assignable to the loose one (a narrowed key param can't satisfy a wider one, and silently allowing it would erase the checking).
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// a shared package that can't see the app catalog
|
|
199
|
+
import type { LooseTFunction } from '@voltro/i18n'
|
|
200
|
+
export const formatError = (t: LooseTFunction, e: AppError): string => t(e.messageKey)
|
|
201
|
+
|
|
202
|
+
// in the app, at the boundary:
|
|
203
|
+
formatError(t.dynamic, err)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
A convenient app-wide barrel captures the bound types once:
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// src/i18n.ts
|
|
210
|
+
import { createTypedMessages, type TypedTFunction } from '@voltro/i18n'
|
|
211
|
+
import type en from './locales/en'
|
|
212
|
+
|
|
213
|
+
export const { useT, useTFn, T } = createTypedMessages<typeof en>()
|
|
214
|
+
export type AppTFunction = TypedTFunction<typeof en> // for helper / prop params
|
|
215
|
+
export type AppMessageKey = Parameters<AppTFunction>[0] // the catalog's key union
|
|
216
|
+
```
|
|
217
|
+
|
|
147
218
|
## Reading a message outside React — `meta({ locale })`
|
|
148
219
|
|
|
149
220
|
`useT` is a hook — it only runs inside a component, against the *active* locale. A page's `meta({ locale })` runs OUTSIDE React (at build / SSR meta-resolution time) and is handed an arbitrary locale string. Use **`pickCatalog`** to resolve a message there:
|
|
@@ -337,6 +337,39 @@ tenant. Use this for per-tenant marketing pages, public listings filtered
|
|
|
337
337
|
by tenant slug, status pages. Never expose any table that should require
|
|
338
338
|
auth this way — the header is client-controlled and unauthenticated.
|
|
339
339
|
|
|
340
|
+
## The empty-tenant sentinel — why a scoped read silently returns empty
|
|
341
|
+
|
|
342
|
+
A subject that authenticated but has **no resolved org** carries the
|
|
343
|
+
empty-string tenant `''`, not `null`. This is deliberate: `null` means a
|
|
344
|
+
`system` subject and reads *unscoped*, so an org-less user must NOT be
|
|
345
|
+
treated as one — that would stream every tenant's rows. Instead the
|
|
346
|
+
auto-merged filter becomes `eq('tenantId', '')`, which matches no real
|
|
347
|
+
row, so the read **fails closed**: it returns empty rather than leaking.
|
|
348
|
+
|
|
349
|
+
The cost is a debugging trap — an empty result with no error looks
|
|
350
|
+
identical to "no such row". So `voltro dev` warns once per tenant-scoped
|
|
351
|
+
table when it happens:
|
|
352
|
+
|
|
353
|
+
```txt
|
|
354
|
+
[warn] tenant-scoped read with an EMPTY tenant — every row is filtered out
|
|
355
|
+
table: weeklyUpdates
|
|
356
|
+
cause: the subject authenticated but has no resolved org (tenantId=''),
|
|
357
|
+
so the auto-merged eq('tenantId', '') matches no real row —
|
|
358
|
+
this is NOT 'no such row'
|
|
359
|
+
fix: resolve the subject to an active org before the read, or mark the
|
|
360
|
+
query .unscoped() if it is deliberately cross-tenant
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
If you see empty reads in development, this line tells you whether the
|
|
364
|
+
cause is the data or an unresolved org. `voltro serve` does not emit it —
|
|
365
|
+
it is a development diagnostic, not a production log.
|
|
366
|
+
|
|
367
|
+
`voltro dev` also warns ONCE, earlier, the first time an authenticated
|
|
368
|
+
subject resolves with no active org at all — "authenticated, but no active
|
|
369
|
+
org → all tenant-scoped reads will be empty" — catching the whole class at
|
|
370
|
+
the door before any specific read. Resolve the user to an active org during
|
|
371
|
+
auth, or route org-less users to an onboarding flow.
|
|
372
|
+
|
|
340
373
|
## Subdomain-based tenant resolution
|
|
341
374
|
|
|
342
375
|
For `tenant1.your-product.com` / `tenant2.your-product.com`, write a
|
|
@@ -320,6 +320,32 @@ Retries are always on and carry no backoff here. Production's jitter exists to d
|
|
|
320
320
|
- **Transport concerns** — connection info, rate limiting, the tenant header. Those are properties of the HTTP hop, not of the procedure; faking them here would only assert against the fake.
|
|
321
321
|
- **Undo capture and the metrics sample** — optional injections the serve entrypoint makes. Their absence changes nothing a handler can observe.
|
|
322
322
|
|
|
323
|
+
## Completing a fixture row — `fixtureRow`
|
|
324
|
+
|
|
325
|
+
`ctx.store` is the real mixin-wrapped store, so `store.insert` runs the same required-column validation production does: a payload that omits a NOT-NULL, no-default, non-auto-stamped column throws `TableValidationFailed` naming the column. That is deliberate — the test store is not laxer than Postgres, so a fixture can't pass on a row the database would reject. But it means a lean fixture (`insert('journal_entries', { id })`, an omitted required FK) now fails.
|
|
326
|
+
|
|
327
|
+
`fixtureRow(table, overrides)` completes the row for you. It fills every column the validator would flag with a schema-typed placeholder, then merges your overrides on top — so you write only the columns the test cares about and the rest are made valid:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { fixtureRow, makeTestContext } from '@voltro/testing'
|
|
331
|
+
import { journalEntries } from '../schema/journal'
|
|
332
|
+
|
|
333
|
+
const ctx = makeTestContext({ subject, store: mockStore({ journal_entries: [] }) })
|
|
334
|
+
|
|
335
|
+
await ctx.store.insert('journal_entries', fixtureRow(journalEntries, {
|
|
336
|
+
amount: '100.00', // the columns THIS test asserts on
|
|
337
|
+
})) // entryNumber (unique), postedAt, … auto-filled
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
What it fills and what it leaves alone:
|
|
341
|
+
|
|
342
|
+
- **Fills** each required column with a value of the right shape — a `oneOf` column takes its first allowed value, a `unique` column gets a distinct value per call (so two fixtures don't collide on the key), `timestamp` / `date` get a fixed epoch, `decimal` / `bigint` a numeric string.
|
|
343
|
+
- **Leaves out** exactly what a caller may legitimately omit: nullable columns, defaulted columns, and the framework auto-stamped set (`id`, `tenantId`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, `deletedAt`, `deletedBy`) — the store stamps those from the subject and the table scheme.
|
|
344
|
+
- **Refuses to guess** a structured type (`json`, `bytes`, `vector`, `array`, `interval`, `raw`): it throws naming the column and telling you to pass it explicitly, rather than inventing a value that fails the codec obscurely.
|
|
345
|
+
- **Your override always wins** — including an explicit `null`. Don't assert on a synthesized placeholder; override anything the test inspects.
|
|
346
|
+
|
|
347
|
+
There is no flag to turn the validation off — a test store that accepts rows production rejects is a fake testing itself. `fixtureRow` is a runtime filler for the loose `store.insert(name, row)` path. For **compile-time** payload typing (a missing required column caught as a type error at the call), use [`insertRow` / `upsertRow`](/docs/data/mutations) from `@voltro/database`, which check against `InferInsertRow<T>`.
|
|
348
|
+
|
|
323
349
|
## Subject + tenant re-scoping
|
|
324
350
|
|
|
325
351
|
`withSubject` and `withTenant` re-scope to a different principal for one block, sharing the **same** underlying data — so cross-subject reads exercise real tenant scoping, not a closure stub. This is how you prove isolation: write as one tenant, then assert another tenant can't see the row.
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@effect/platform": "^0.96.1",
|
|
13
13
|
"@effect/rpc": "^0.75.1",
|
|
14
|
-
"@voltro/ai": "0.11.
|
|
15
|
-
"@voltro/cli": "0.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/protocol": "0.11.
|
|
19
|
-
"@voltro/runtime": "0.11.
|
|
14
|
+
"@voltro/ai": "0.11.2",
|
|
15
|
+
"@voltro/cli": "0.11.2",
|
|
16
|
+
"@voltro/database": "0.11.2",
|
|
17
|
+
"@voltro/env": "0.11.2",
|
|
18
|
+
"@voltro/protocol": "0.11.2",
|
|
19
|
+
"@voltro/runtime": "0.11.2",
|
|
20
20
|
"effect": "^3.21.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@voltro/testing": "0.11.
|
|
23
|
+
"@voltro/testing": "0.11.2",
|
|
24
24
|
"typescript": "^5.7.0",
|
|
25
25
|
"vitest": "^3.0.0"
|
|
26
26
|
}
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-auth": "0.11.
|
|
19
|
-
"@voltro/protocol": "0.11.
|
|
20
|
-
"@voltro/runtime": "0.11.
|
|
21
|
-
"@voltro/sql-postgres": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.2",
|
|
16
|
+
"@voltro/database": "0.11.2",
|
|
17
|
+
"@voltro/env": "0.11.2",
|
|
18
|
+
"@voltro/plugin-auth": "0.11.2",
|
|
19
|
+
"@voltro/protocol": "0.11.2",
|
|
20
|
+
"@voltro/runtime": "0.11.2",
|
|
21
|
+
"@voltro/sql-postgres": "0.11.2",
|
|
22
22
|
"effect": "^3.21.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.11.
|
|
25
|
+
"@voltro/testing": "0.11.2",
|
|
26
26
|
"typescript": "^5.7.0",
|
|
27
27
|
"vitest": "^3.0.0"
|
|
28
28
|
}
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
19
|
-
"@voltro/protocol": "0.11.
|
|
20
|
-
"@voltro/runtime": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.2",
|
|
16
|
+
"@voltro/database": "0.11.2",
|
|
17
|
+
"@voltro/env": "0.11.2",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.11.2",
|
|
19
|
+
"@voltro/protocol": "0.11.2",
|
|
20
|
+
"@voltro/runtime": "0.11.2",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.11.
|
|
24
|
+
"@voltro/testing": "0.11.2",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^3.0.0"
|
|
27
27
|
}
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-deactivation": "0.11.
|
|
19
|
-
"@voltro/protocol": "0.11.
|
|
20
|
-
"@voltro/runtime": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.2",
|
|
16
|
+
"@voltro/database": "0.11.2",
|
|
17
|
+
"@voltro/env": "0.11.2",
|
|
18
|
+
"@voltro/plugin-deactivation": "0.11.2",
|
|
19
|
+
"@voltro/protocol": "0.11.2",
|
|
20
|
+
"@voltro/runtime": "0.11.2",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.11.
|
|
24
|
+
"@voltro/testing": "0.11.2",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^3.0.0"
|
|
27
27
|
}
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@react-email/components": "^1.0.12",
|
|
14
14
|
"@react-email/render": "^1.4.0",
|
|
15
|
-
"@voltro/cli": "0.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-mail": "0.11.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
20
|
-
"@voltro/protocol": "0.11.
|
|
21
|
-
"@voltro/runtime": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.2",
|
|
16
|
+
"@voltro/database": "0.11.2",
|
|
17
|
+
"@voltro/env": "0.11.2",
|
|
18
|
+
"@voltro/plugin-mail": "0.11.2",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.11.2",
|
|
20
|
+
"@voltro/protocol": "0.11.2",
|
|
21
|
+
"@voltro/runtime": "0.11.2",
|
|
22
22
|
"effect": "^3.21.2",
|
|
23
23
|
"react": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.11.
|
|
26
|
+
"@voltro/testing": "0.11.2",
|
|
27
27
|
"typescript": "^5.7.0",
|
|
28
28
|
"vitest": "^3.0.0"
|
|
29
29
|
}
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.96.1",
|
|
14
14
|
"@effect/rpc": "^0.75.1",
|
|
15
|
-
"@voltro/cli": "0.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
19
|
-
"@voltro/plugin-storage": "0.11.
|
|
20
|
-
"@voltro/protocol": "0.11.
|
|
21
|
-
"@voltro/runtime": "0.11.
|
|
22
|
-
"@voltro/sql-mysql": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.2",
|
|
16
|
+
"@voltro/database": "0.11.2",
|
|
17
|
+
"@voltro/env": "0.11.2",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.11.2",
|
|
19
|
+
"@voltro/plugin-storage": "0.11.2",
|
|
20
|
+
"@voltro/protocol": "0.11.2",
|
|
21
|
+
"@voltro/runtime": "0.11.2",
|
|
22
|
+
"@voltro/sql-mysql": "0.11.2",
|
|
23
23
|
"effect": "^3.21.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.11.
|
|
26
|
+
"@voltro/testing": "0.11.2",
|
|
27
27
|
"typescript": "^5.7.0",
|
|
28
28
|
"vitest": "^3.0.0"
|
|
29
29
|
}
|
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
"test": "voltro test"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@voltro/cli": "0.11.
|
|
14
|
-
"@voltro/database": "0.11.
|
|
15
|
-
"@voltro/env": "0.11.
|
|
16
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
17
|
-
"@voltro/plugin-storage": "0.11.
|
|
18
|
-
"@voltro/protocol": "0.11.
|
|
19
|
-
"@voltro/runtime": "0.11.
|
|
13
|
+
"@voltro/cli": "0.11.2",
|
|
14
|
+
"@voltro/database": "0.11.2",
|
|
15
|
+
"@voltro/env": "0.11.2",
|
|
16
|
+
"@voltro/plugin-multitenancy": "0.11.2",
|
|
17
|
+
"@voltro/plugin-storage": "0.11.2",
|
|
18
|
+
"@voltro/protocol": "0.11.2",
|
|
19
|
+
"@voltro/runtime": "0.11.2",
|
|
20
20
|
"effect": "^3.21.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@voltro/testing": "0.11.
|
|
23
|
+
"@voltro/testing": "0.11.2",
|
|
24
24
|
"typescript": "^5.7.0",
|
|
25
25
|
"vitest": "^3.0.0"
|
|
26
26
|
}
|