@voltro/cli 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 +49 -0
- package/THIRD-PARTY-NOTICES.md +234 -1
- package/bin/voltro.mjs +71 -1
- package/dist/apiBuild-CvtQeBMs.js +190 -0
- package/dist/apiBuild-DQBNqNZ8.js +2 -0
- package/dist/bin.js +2 -2
- package/dist/{commands-DQy4812j.js → commands-DhyBIs1O.js} +2381 -1768
- package/dist/{dev--jHe1vcu.js → dev-CQxbrpDz.js} +1677 -1626
- package/dist/dev-DYjGqPGD.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/serveCommand-BZzUJIyo.js +1077 -0
- package/dist/serveEntry.js +2 -2
- package/package.json +22 -19
- package/templates/AGENTS.core.md +61 -0
- package/templates/AGENTS.md +61 -0
- package/templates/agent-docs/_manifest.json +4 -4
- package/templates/agent-docs/ai.md +28 -0
- package/templates/agent-docs/authentication.md +5 -2
- package/templates/agent-docs/cli.md +98 -0
- package/templates/agent-docs/data.md +193 -3
- package/templates/agent-docs/database/advancedqueries.md +29 -0
- package/templates/agent-docs/database/querying.md +2 -0
- package/templates/agent-docs/deployment.md +3 -1
- package/templates/agent-docs/internationalization.md +237 -2
- package/templates/agent-docs/observability.md +9 -0
- package/templates/agent-docs/plugins.md +2 -2
- package/templates/agent-docs/reference.md +76 -0
- package/templates/agent-docs/schema-driven-ui.md +551 -2
- package/templates/agent-docs/templates/apibackends.md +10 -8
- package/templates/agent-docs/templates/overview.md +1 -1
- package/templates/agent-docs/testing.md +177 -3
- 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/README.md +9 -4
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +1 -1
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +6 -6
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rbac/tests/notes.create.test.ts +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/templates/baselines/compose/docker/api.Dockerfile +10 -5
- package/templates/baselines/compose-mariadb/docker/api.Dockerfile +10 -5
- package/dist/apiBuild-OpZROja5.js +0 -2
- package/dist/apiBuild-o70rjpVJ.js +0 -184
- package/dist/dev-BKkZglQV.js +0 -2
- package/dist/serveCommand-93rRdEp0.js +0 -1077
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,55 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.4.0] — 2026-07-18
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/protocol, @voltro/plugin-rbac, @voltro/runtime** — **Resource-aware declarative guards + one denial tag.** `guards: [{ scope, resource }]` now actually scopes to a resource. Register a resolver — either `setResourceScopeResolver((req) => Effect<boolean>)` from `@voltro/protocol`, or `rbacPlugin({ resolveResourceRoles: (subject, resourceId) => roleSlugs })` — and the framework asks it, per request, whether the caller holds the scope **on the extracted resource** (a team, workspace, document), before the executor runs (mutations: before the transaction opens). A globally-held scope or `admin:full` still short-circuits without a resolver call; a resolver error fails **closed**. With no resolver registered, `resource` stays advisory (global check) exactly as before, so single-tenant apps are unaffected. **BREAKING — `@voltro/plugin-rbac` no longer exports `Forbidden`.** rbac's `permission()` / `assertPermission()` / `anyPermission()` now fail with protocol's `ScopeError` — the SAME typed error the declarative `guards:` enforcement already raised. One denial tag across the framework: a client that matches `_tag === 'ScopeError'` (or `errorTag(err)`) recognizes both a descriptor-guard denial and a `permission()` denial with one branch. The `@voltro/plugin-rbac/errors` subpath is removed. Migration (the `voltro update` codemod rewrites the mechanical part): - `import { Forbidden } from '@voltro/plugin-rbac/errors'` → `import { ScopeError } from '@voltro/protocol'`; `import { Forbidden } from '@voltro/plugin-rbac'` → `import { ScopeError } from '@voltro/plugin-rbac'` (re-exported from the root). - Every `Forbidden` identifier → `ScopeError` (incl. `error: Forbidden` on a descriptor). - By hand: a constructed `new Forbidden({ required, reason })` becomes `new ScopeError({ required, message })` (the field is `message` and is REQUIRED); and any `Effect.catchTag('Forbidden', …)` / `err._tag === 'Forbidden'` string becomes `'ScopeError'`. Guards are normally caught, not constructed, so most apps only need the import rewrite the codemod does. Also note: `requireScope` / declarative `guards:` enforcement is now async-capable (it awaits the resource resolver when one is registered) — no change to handler code, which never called the internal enforcement directly.
|
|
47
|
+
- **@voltro/client, @voltro/plugin-rbac** — **`@voltro/plugin-rbac/web` is removed — the UI permission gate lives in `@voltro/client`.** The framework shipped `useCan` TWICE, with the same name and the same semantics, over two different React contexts. An app that mounted `<PermissionProvider>` and imported the plugin's `useCan` got an empty scope set and silently hid every gated affordance — no error, just a UI where nothing is permitted. One of the two had to go. `@voltro/client` survives because scopes are a FRAMEWORK concept: `@voltro/protocol` owns `ScopeError`, `guards: [{ scope }]` and `ctx.access`, and rbac is only one way to PRODUCE scopes — an app can register its own `setResourceScopeResolver` over its own tables and never install the plugin. The hook consumes scopes, which is generic; producing them is rbac-specific. Keeping the hook behind the plugin would force an rbac dependency on apps that deliberately don't use rbac, purely to gate a button. `@voltro/plugin-rbac` keeps everything that IS rbac: the roles map, role→scope compilation, the `permission()` / `assertPermission()` / `anyPermission()` server guards, the role tables, and `resolveResourceRoles`. The codemod rewrites every mechanical site: `RbacScopeProvider` → `PermissionProvider`, `canFromScopes` → `canCall`, `canAnyFromScopes` → `canCallAny`, and repoints `useCan` / `useCanAny` / `ADMIN_SCOPE` to `@voltro/client`. A raw `useContext(RbacScopeContext)` is ANNOTATED rather than rewritten — the client equivalent is `usePermissions()`, which returns `{ scopes }` instead of the bare array, so a silent rewrite would compile and then hand back the wrong shape.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **@voltro/runtime, @voltro/cli** — **`read({ where })` on aggregates.** Aggregate reads were all-or-nothing, so every consumer that wanted one slice pulled the whole materialised set over the wire and filtered in the component — the cost scaling with the aggregate, not the slice. `where` entries are ANDed; a scalar means strict equality, an array means IN; filtering runs before `orderBy`/`limit`. Deliberately a data filter over already materialised rows, not a predicate language — aggregates stay aggregates.
|
|
52
|
+
- **@voltro/client** — **Two ergonomics that remove the two most-repeated shapes in a Voltro frontend.** - **`mutate(input, options)` — `onSuccess` / `onError` / `notify`.** Every write site used to re-type `try { await mutate() } catch { toast.error(…) } finally { setSubmitting(false) }`. `pending` already replaced the `finally`; these replace the rest. Load-bearing semantic: supplying an error handler (`onError` or `notify.error`) marks the failure HANDLED — `mutate` resolves with `undefined` instead of rejecting, which is what actually deletes the try/catch. With no handler it rejects exactly as before, so unhandled failures stay loud. `notify` routes to an app-registered sink (`setMutationNotifier`) — the framework stays unbound to any toast library. - **`useSubscription` now returns `loading` and `isEmpty`, and accepts `fallback`.** Call sites branched on `data === undefined`, conflating "no snapshot yet" with "zero rows" — the cause of a flash of empty-state before the first snapshot. They are now distinct, derived once. `fallback` fills `data` while loading without lying about `loading`. Also documented: `error` carries a COLD-START stream failure (check it to avoid an infinite skeleton); a failure after data arrived deliberately does not blank good data — those reach the error bus (`useOnRpcError`).
|
|
53
|
+
- **@voltro/cli** — **Make the client hook surface discoverable — and keep it that way.** A downstream app built its whole frontend on three transport hooks (`useSubscription` / `useMutation` / `useAction`) and hand-rolled forms, tables, upload, permission gates, debounce and pagination, because the agent guide documented ~none of the ~40 client hooks that already ship. Three changes: - **The always-loaded core now carries a "Pick the CLIENT primitive" rubric** — the same decision-rubric treatment the backend primitives get, plus a "you're about to write X → reach for Y" table (per-field `useState` → `useFormBinding`, hand-rolled table → `useDataTable`, `FileReader` → `useUpload`, `useMemo` fan-in → `useDerived`, …). - **The hook reference is complete.** `reference/hooks-overview.md` presented itself as "the client-side hook surface" while documenting 13 of 44 hooks; it now enumerates the schema-driven-UI, files/permissions/utilities, and AI families (en + de). - **A coverage GATE** (`agentsMdTemplate.test.ts`): every exported `use*` hook of `@voltro/client` must appear in the composed agent-docs corpus, or the test fails with the list. Docs can no longer silently fall behind the export surface. Plus **`voltro doctor` now flags hand-rolls**: it scans an app's source for the patterns a shipped primitive covers (hand-rolled form/table, `FileReader` upload, `setTimeout` debounce, `useMemo` fan-in, local Next.js compat shims, hand-rolled presence) and names the primitive to use. Advisory, never fatal — a false-positive lecture must not fail a build.
|
|
54
|
+
- **@voltro/cli** — **Generated `matchError` / `AppError` / `AppErrorTag`.** Codegen now emits a per-app exhaustive error matcher into `rpcGroup.generated.ts`, derived BY REFERENCE from every descriptor's `error:` schema plus the cross-cutting plugin errors. `matchError(err, { [tag]: handler }, fallback?)` dispatches on `_tag` with the handler keys constrained to the app's ACTUAL error tags — so a hand-maintained tag list that silently drifts (dead/renamed tags) is gone; a stale tag is a compile error. Browser-safe (reads `_tag` structurally, no runtime dep). Workflows keep their existing `WorkflowErrors`.
|
|
55
|
+
- **@voltro/cli** — **`voltro doctor` + `voltro update --codemods-only`/`--from`** — two upgrade-path ergonomics from AWB adoption feedback: - **`voltro doctor [app]`** (and **`voltro serve --preflight`**) preflights an API app for production serve: it verifies the precompiled serve bundle exists and, if not, prints the exact fix and exits 1 — so a Dockerfile / CI step catches "prod `voltro serve` with no `voltro build`" at BUILD time instead of at cold-start (0.3.0 made an unbuilt production serve fatal). - **`voltro update --codemods-only` (alias `--run-codemods`) + `--from <version>`** — re-apply the codemods + manual notes for an explicit `[from, to]` delta WITHOUT bumping `package.json` or installing. The recovery path for a hand-edited version bump (`bump package.json` + install first), which otherwise makes `voltro update` report "already on X — nothing to do" and silently skip the codemods. `--from` also overrides the auto-detected source version on a normal update.
|
|
56
|
+
- **@voltro/runtime** — **`encryptField` / `decryptField` — standalone field cipher for raw-SQL paths.** `.encrypted()` columns encrypt/decrypt transparently inside the `ctx.store` middleware, so a code path that reaches the DB by RAW SQL (an auth strategy with no store handle reading/writing a session token, a one-off backfill) bypasses it. `@voltro/runtime` now exposes the SAME registered cipher standalone: `encryptField` encrypts a value; `decryptField` decrypts an `enc:v1:…` value and passes a non-ciphertext value through unchanged (so a raw-SQL path can adopt encryption while pre-existing plaintext rows keep working). Both throw a clear error when no cipher is registered. Encryption stops being all-or-nothing tied to going through `ctx.store`.
|
|
57
|
+
- **@voltro/testing** — **`makeVoltroTestClient` — the missing half of the test story.** The framework shipped a strong BACKEND test story (`makeTestContext`, `mockStore`, `mockAi`, the workflow runner) and nothing for the client. A Voltro frontend is a reactive-data app: with no way to render a component against mocked `useSubscription` / `useMutation` — and to inject a loading state, a stream error, or a failing write — it is structurally untestable, which is why real apps end up with zero frontend tests. `import { makeVoltroTestClient } from '@voltro/testing/client'` returns a `Provider` plus `setSubscription` / `failSubscription` / `resetSubscription` and a `calls` log of every write with its input. A tag ABSENT from the fixture stays in the loading state — the distinction from an empty result is exactly what you want to assert. Deliberately NOT a renderer: it hands you a Provider, so it works with `react-dom/client` + `act`, testing-library, or your own harness, and locks you into none of them.
|
|
58
|
+
- **@voltro/i18n** — **Plurals, Intl formatters, and catalogs a bundler can actually split.** - **`plural(locale, count, forms)` + `usePlural`** — real CLDR categories via `Intl.PluralRules`. Replaces the hardcoded `"{count} epic(s)"` pattern, whose literal `(s)` is simply wrong outside English, and which cannot express languages that distinguish few from many (Polish 2–4 vs 5+). An explicit `zero` form is honoured for exactly 0; ordinals via `{ type: 'ordinal' }`. - **`useFormatDate` / `useRelativeTime` / `useFormatNumber` / `useFormatCurrency` / `useFormatters`** — locale-bound `Intl` wrappers, so an app stops carrying several divergent hand-rolled "X minutes ago" helpers that each round differently. - **`defineCatalogs` + `LazyI18nProvider`** — code-split catalogs. `pickCatalog({ en, de })` is a STATIC import map: every locale is a value-level import, so the bundler must emit them in one chunk and a German-only visitor downloads English too, growing linearly per locale. A map of `() => import()` loaders is the only shape a bundler treats as a chunk boundary. `preload(locale)` before hydration keeps first paint synchronous via `peek()`; the provider's `fallback` is for a locale SWITCH, not first paint. Unknown locales degrade to the base catalog, and a failed chunk load stays retryable.
|
|
59
|
+
- **@voltro/protocol, @voltro/client** — **Nested optimistic: `shapeItem` + bulk (multi-item) targets.** Two additive improvements to path-targeted auto-optimistic: - **`shapeItem`** — the nested counterpart of `shape`, typed to the ITEM of the nested array (not the mutation's output), so a path-target patch reads/returns the item without casting `current`. (Previously the nested shaper was the flat `shape`, pinned to the output row — every adopter had to cast. `shape` stays bound to the output for flat targets; a single field can't be both, so the nested shaper is its own.) - **Bulk `identify`** — a target's `identify` may now return `string[]` to patch/delete MANY items (or top-level rows) in one mutation — the group-drag / batch-edit case where per-item parallel writes used to race. Each patched item keeps its own key. Runtime-compatible: a nested target that still uses `shape` falls back transparently. No codemod — additive.
|
|
60
|
+
- **@voltro/web, @voltro/client** — **Two hooks apps kept hand-rolling, because nothing shipped them.** - **`useTheme` (`@voltro/web`)** — returns `{ theme, resolvedTheme, setTheme }` over the framework's own `voltro:theme` cookie and the `html.dark` class. The bug it removes is specific: an app that hand-rolls a second theme store ends up with two writers on one class, so the toggle's state and the rendered theme can disagree, and the pre-paint script flashes the wrong one. `'system'` resolves through `matchMedia` and tracks live OS changes. - **`useConnectionStatus` (`@voltro/client`)** — `'connected' | 'degraded' | 'offline'`, derived from the only two signals the client honestly has: `navigator.onLine` (browser says the network is gone) and the rpc error bus (a call actually failed). `degraded` means "we saw a failure and no success since"; coming back online clears it, because the failures counted during an offline window ARE that window. Deliberately no polling ping just to colour an indicator.
|
|
61
|
+
- **@voltro/database** — **`.uniqueActive([cols])` now works on mysql / mariadb** (it previously failed loud at migrate — those engines have no partial index). It lowers automatically to a STORED generated column per key column — `CASE WHEN <predicate> THEN CAST(<col> AS CHAR(255)) ELSE NULL END` — plus a UNIQUE over them. NULL-distinct uniqueness means a soft-deleted row's generated columns are all NULL and never collide, so re-creating the key just works — the same resurrection-safe semantics the partial index gives on postgres/sqlite/mssql. The lowering is applied identically on the emit side and the declared-snapshot side, so it round-trips through the declarative differ (verified live on MariaDB: declare→migrate→introspect→re-plan is a no-diff, and a duplicate among active rows is rejected while soft-deleting one frees the key). You write the same `.uniqueActive([...])` on every dialect.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **@voltro/cli** — **Standalone `voltro codegen` now emits the SAME `rpcGroup.generated.ts` as `voltro dev` / `voltro build`.** Previously the standalone command skipped the app's plugins, so it dropped every plugin's cross-cutting error union AND its client RPC routes — an inconsistent generated file (typed plugin errors gone, plugin route tags unresolved) vs the dev/serve boot path. The command now loads `app.config` and gathers the plugins' codegen inputs through the SAME `gatherCodegenPluginImports` the boot path uses (single-sourced, so the three paths can't drift).
|
|
66
|
+
- **@voltro/cli** — **The hook-coverage gate was too weak, and it let dead documentation through.** It asked only whether a hook's NAME appeared in the agent-docs corpus — which a one-line row in a link table satisfies. Ten client hooks had exactly that, and six of those rows linked to a page that never mentions the hook, so a reader following the link learned nothing. Two sharper checks replace it: - **Substance** (`agentsMdTemplate.test.ts`): a hook must appear at least once OUTSIDE a table row, so a bare row no longer counts as documentation. - **Dead references** (`gen-agent-docs.mjs`): a row promising `[`useX`](/docs/…)` must point at a page that actually mentions `useX`. This lives in the generator because that is the only thing which reads the docs tree — and it walks BOTH language trees, since an en-only check leaves every German dead link invisible.
|
|
67
|
+
- **@voltro/cli** — - **Serve bundle: inline the pure-JS SQL drivers (`pg`/`mysql2`/`tedious`) instead of runtime-shimming them.** A production `voltro serve` with a postgres/mysql/mssql store crashed at boot with `Pg.Pool is not a constructor` (and the mysql/mssql equivalents): the driver's `@effect/sql-*` consumer links its leaf as a namespace (`import * as Pg from 'pg'; new Pg.Pool()`), but the native-leaf CJS shim (`module.exports = <leaf>`) did not surface the named members through esbuild's `import * as` interop, so `Pg.Pool` was `undefined`. These drivers are pure JS (no `.node`), so they are now inlined into the serve bundle like the rest of the framework — esbuild links the real module and the namespace resolves correctly. Only genuinely-native leaves (`better-sqlite3`, the turso/libsql addons, `pg-native`) and the dynamically-imported `ioredis`/`nodemailer` still resolve via the runtime shim. Verified end-to-end from a relocated prod tree (a real `POST /rpc` through the inlined `pg`).
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## [0.3.0] — 2026-07-18
|
|
72
|
+
|
|
73
|
+
### ⚠ BREAKING
|
|
74
|
+
|
|
75
|
+
- **@voltro/cli** — `voltro build` precompiles the whole serve path (framework + effect + `@voltro` inlined, app modules as lazy chunks) into a single **serve bundle**, and `voltro serve` boots from it in-process — cutting `serve: ready` from ~1000 ms to ~180 ms (5–6×; the win is larger on a cold scale-to-zero container). The app's declared SQL driver is inlined (only the native leaf like `pg` stays external, resolved at runtime so it survives the deploy relocation). **Production now serves ONLY from the bundle and NEVER transpiles on demand:** the bundle build externalises unresolvable optional peers (e.g. `@react-email/render` behind `@voltro/plugin-mail`) so it always builds; a bundle-build failure is **fatal** (`voltro build` exits non-zero); and an unbuilt production `voltro serve` fails loud instead of falling back to tsx. The build toolchain (`tsx`, `esbuild`, `vite`, `@vitejs/plugin-react`, `@tailwindcss/vite` + their native tree: rolldown/lightningcss/postcss/jiti) moves to **`optionalDependencies`** of `@voltro/cli`, so `pnpm --prod --no-optional deploy` yields a serve image with none of it — a prod API image's `node_modules` drops ~305 MB → ~131 MB, structurally, with no fragile prune list. `voltro dev` and a non-production local `voltro serve` are unchanged (still tsx). **Migration:** in production (`NODE_ENV=production`) run `voltro build` before `voltro serve`. The generated Dockerfiles already do; a custom Dockerfile / start script adds a `voltro build .` step before `voltro serve .` (`voltro update` prints this — see the 0.3.0 codemod note).
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- **@voltro/protocol, @voltro/runtime, @voltro/plugin-rbac** — Declarative authorization `guards:` on `defineMutation` / `defineQuery` / `defineAction`. The framework enforces the declared scope(s) in the dispatch spine BEFORE the executor (for a mutation, before the transaction opens), fails with a typed `ScopeError`, and auto-merges `ScopeError` into the wire error union so the client decodes the denial typed. Guards are browser-safe DATA (scope strings + a pure `resource: (input) => id` extractor). Checks run against the caller's EFFECTIVE scope set — raw subject scopes ∪ `@voltro/plugin-rbac` role-derived scopes — via a new canonical effective-scope seam in `@voltro/protocol` (`effectiveScopes` / `setEffectiveScopes` / `checkGuards`), which rbac now publishes to (so a role-granted scope satisfies a `guards:` entry and the in-handler `permission()` identically). Adds `ctx.access` (`has` / `hasAny` / `require` / `scopes`) — the cast-free typed authorization slice on every handler context. Enforcement is single-sourced in the shared serve pipeline, so `voltro dev` and `voltro serve` can't drift.
|
|
80
|
+
- **@voltro/protocol, @voltro/client** — Nested / path-targeted auto-optimistic. A mutation `target` can now patch a nested array INSIDE a query's value — a JSON array column (`snapshot.projects`) or a computed/shaped result — at item granularity, via `path` (dot-path to the array), `by` (item key, default `id`), and `match` (a pure predicate that scopes the patch to the entries whose current value satisfies it, preventing a patch bleeding across sibling subscriptions that share a source table). Previously auto-optimistic only patched the flat top-level row array keyed by `id`; nested values needed a hand-written `.withOptimistic` reducer. `path`/`by`/`match` are browser-safe descriptor data (a dot-path string + pure predicate), same discipline as `identify`/`shape`. A path insert is applied even on a computed entry (it targets a known document, not a blind top-level add).
|
|
81
|
+
- **@voltro/runtime** — `ctx.store.applyDefined(input, keys)` (and a standalone `applyDefined` export from `@voltro/runtime`) — builds a partial-update patch keeping only the listed keys whose value the caller actually provided (`!== undefined`; a defined falsy value like `0`/`''`/`false` is kept). Collapses the per-field `if (input.x !== undefined) patch.x = input.x` idiom every partial-update mutation hand-writes.
|
|
82
|
+
- **@voltro/database** — `.uniqueActive([cols], opts?)` on the table builder — a portable partial-UNIQUE constraint that holds only among the rows matching a predicate (default `"deletedAt" IS NULL`, pairing with `.softDelete()`). Emits `CREATE UNIQUE INDEX … WHERE` on postgres / sqlite / mssql, so a soft-deleted row leaves the active set and a NEW row with the same key inserts cleanly — no hand-written `generatedAs("CASE WHEN …")` column and no resurrection footgun. On mysql / mariadb (no partial-index support) it FAILS LOUDLY at migrate time rather than silently emitting a full unique index that would forbid re-creating a soft-deleted key — the generated-STORED-column lowering for those dialects is a follow-up. Kept out of the declarative index snapshot (the incremental planner is predicate-blind and would misclassify a unique+partial index as a full constraint), so the fresh-schema DDL path is its sole emitter and there is no re-diff churn. Live-verified against postgres.
|
|
83
|
+
- **@voltro/cli** — `voltro update` upgrades an app to the latest framework: it bumps every `@voltro/*` dependency, installs with the detected package manager, and runs the codemods shipped with the target version. Codemods are authored with `defineCodemod` + an import-scoped ts-morph helper toolkit (`renameImport`, `renameModuleSpecifier`, `renameJsxProp`, `renameObjectKey`, `add`/`removeImport`, structural `changeCallArgs`/`wrapCall`, `annotate`) and run against the app source; a `manual` kind surfaces written steps for changes that can't be automated. Breaking public-API changes now ship a codemod (or an explicit `codemod: none`), enforced by the changelog gate. Framework-owned `_voltro_*` table changes continue to ride the declarative differ on `voltro db apply` / `voltro dev` boot — `update` does not touch the database.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **@voltro/database** — The core-table registry (`registerCoreTables` / `requireActors` / `requireTenants`) now stores its state on a process-global `Symbol.for` singleton, the same mechanism the main table registry already uses — instead of module-local `let` bindings. Module-local state splits when the `@voltro/database` module is duplicated in a process (e.g. resolved through both the `.` and `./sql` entry points, or a bundled framework copy alongside an externally-resolved one): one instance's `registerCoreTables` becomes invisible to the instance that reads it, surfacing as a spurious `core 'actors' table not registered` at store construction. Pinning it to `globalThis` makes every copy share one store, matching the table registry's already-global behaviour.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
42
91
|
## [0.2.2] — 2026-07-17
|
|
43
92
|
|
|
44
93
|
### Added
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -5,7 +5,7 @@ property of its respective copyright holders and is used under the terms of
|
|
|
5
5
|
its license. This file is provided for attribution; it grants no rights in
|
|
6
6
|
@voltro/cli itself, which is proprietary (see LICENSE).
|
|
7
7
|
|
|
8
|
-
Generated from the resolved runtime dependency closure (
|
|
8
|
+
Generated from the resolved runtime dependency closure (198 packages).
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -10139,6 +10139,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
10139
10139
|
SOFTWARE.
|
|
10140
10140
|
```
|
|
10141
10141
|
|
|
10142
|
+
## @ts-morph/common@0.29.0
|
|
10143
|
+
|
|
10144
|
+
License: MIT
|
|
10145
|
+
|
|
10146
|
+
```
|
|
10147
|
+
The MIT License (MIT)
|
|
10148
|
+
|
|
10149
|
+
Copyright (c) 2017 David Sherret
|
|
10150
|
+
|
|
10151
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10152
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10153
|
+
in the Software without restriction, including without limitation the rights
|
|
10154
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10155
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10156
|
+
furnished to do so, subject to the following conditions:
|
|
10157
|
+
|
|
10158
|
+
The above copyright notice and this permission notice shall be included in all
|
|
10159
|
+
copies or substantial portions of the Software.
|
|
10160
|
+
|
|
10161
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
10162
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10163
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
10164
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
10165
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
10166
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
10167
|
+
SOFTWARE.
|
|
10168
|
+
```
|
|
10169
|
+
|
|
10142
10170
|
## @typespec/ts-http-runtime@0.3.5
|
|
10143
10171
|
|
|
10144
10172
|
License: MIT
|
|
@@ -10490,6 +10518,36 @@ See the License for the specific language governing permissions and
|
|
|
10490
10518
|
limitations under the License.
|
|
10491
10519
|
```
|
|
10492
10520
|
|
|
10521
|
+
## balanced-match@4.0.4
|
|
10522
|
+
|
|
10523
|
+
License: MIT
|
|
10524
|
+
|
|
10525
|
+
```
|
|
10526
|
+
(MIT)
|
|
10527
|
+
|
|
10528
|
+
Original code Copyright Julian Gruber <julian@juliangruber.com>
|
|
10529
|
+
|
|
10530
|
+
Port to TypeScript Copyright Isaac Z. Schlueter <i@izs.me>
|
|
10531
|
+
|
|
10532
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
10533
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
10534
|
+
the Software without restriction, including without limitation the rights to
|
|
10535
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
10536
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10537
|
+
so, subject to the following conditions:
|
|
10538
|
+
|
|
10539
|
+
The above copyright notice and this permission notice shall be included in all
|
|
10540
|
+
copies or substantial portions of the Software.
|
|
10541
|
+
|
|
10542
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
10543
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10544
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
10545
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
10546
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
10547
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
10548
|
+
SOFTWARE.
|
|
10549
|
+
```
|
|
10550
|
+
|
|
10493
10551
|
## base64-js@1.5.1
|
|
10494
10552
|
|
|
10495
10553
|
License: MIT
|
|
@@ -10564,6 +10622,36 @@ programs and associated documentation files created by the
|
|
|
10564
10622
|
Original Author, when distributed with the Software.
|
|
10565
10623
|
```
|
|
10566
10624
|
|
|
10625
|
+
## brace-expansion@5.0.6
|
|
10626
|
+
|
|
10627
|
+
License: MIT
|
|
10628
|
+
|
|
10629
|
+
```
|
|
10630
|
+
MIT License
|
|
10631
|
+
|
|
10632
|
+
Copyright Julian Gruber <julian@juliangruber.com>
|
|
10633
|
+
|
|
10634
|
+
TypeScript port Copyright Isaac Z. Schlueter <i@izs.me>
|
|
10635
|
+
|
|
10636
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10637
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10638
|
+
in the Software without restriction, including without limitation the rights
|
|
10639
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10640
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10641
|
+
furnished to do so, subject to the following conditions:
|
|
10642
|
+
|
|
10643
|
+
The above copyright notice and this permission notice shall be included in all
|
|
10644
|
+
copies or substantial portions of the Software.
|
|
10645
|
+
|
|
10646
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
10647
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10648
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
10649
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
10650
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
10651
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
10652
|
+
SOFTWARE.
|
|
10653
|
+
```
|
|
10654
|
+
|
|
10567
10655
|
## buffer@5.6.0
|
|
10568
10656
|
|
|
10569
10657
|
License: MIT
|
|
@@ -10640,6 +10728,34 @@ See the License for the specific language governing permissions and
|
|
|
10640
10728
|
limitations under the License.
|
|
10641
10729
|
```
|
|
10642
10730
|
|
|
10731
|
+
## code-block-writer@13.0.3
|
|
10732
|
+
|
|
10733
|
+
License: MIT
|
|
10734
|
+
|
|
10735
|
+
```
|
|
10736
|
+
The MIT License (MIT)
|
|
10737
|
+
|
|
10738
|
+
Copyright (c) 2015-2024 David Sherret
|
|
10739
|
+
|
|
10740
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10741
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10742
|
+
in the Software without restriction, including without limitation the rights
|
|
10743
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10744
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10745
|
+
furnished to do so, subject to the following conditions:
|
|
10746
|
+
|
|
10747
|
+
The above copyright notice and this permission notice shall be included in all
|
|
10748
|
+
copies or substantial portions of the Software.
|
|
10749
|
+
|
|
10750
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
10751
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10752
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
10753
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
10754
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
10755
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
10756
|
+
SOFTWARE.
|
|
10757
|
+
```
|
|
10758
|
+
|
|
10643
10759
|
## debug@4.4.3
|
|
10644
10760
|
|
|
10645
10761
|
License: MIT
|
|
@@ -12590,6 +12706,68 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
12590
12706
|
THE SOFTWARE.
|
|
12591
12707
|
```
|
|
12592
12708
|
|
|
12709
|
+
## minimatch@10.2.3
|
|
12710
|
+
|
|
12711
|
+
License: BlueOak-1.0.0
|
|
12712
|
+
|
|
12713
|
+
```
|
|
12714
|
+
# Blue Oak Model License
|
|
12715
|
+
|
|
12716
|
+
Version 1.0.0
|
|
12717
|
+
|
|
12718
|
+
## Purpose
|
|
12719
|
+
|
|
12720
|
+
This license gives everyone as much permission to work with
|
|
12721
|
+
this software as possible, while protecting contributors
|
|
12722
|
+
from liability.
|
|
12723
|
+
|
|
12724
|
+
## Acceptance
|
|
12725
|
+
|
|
12726
|
+
In order to receive this license, you must agree to its
|
|
12727
|
+
rules. The rules of this license are both obligations
|
|
12728
|
+
under that agreement and conditions to your license.
|
|
12729
|
+
You must not do anything with this software that triggers
|
|
12730
|
+
a rule that you cannot or will not follow.
|
|
12731
|
+
|
|
12732
|
+
## Copyright
|
|
12733
|
+
|
|
12734
|
+
Each contributor licenses you to do everything with this
|
|
12735
|
+
software that would otherwise infringe that contributor's
|
|
12736
|
+
copyright in it.
|
|
12737
|
+
|
|
12738
|
+
## Notices
|
|
12739
|
+
|
|
12740
|
+
You must ensure that everyone who gets a copy of
|
|
12741
|
+
any part of this software from you, with or without
|
|
12742
|
+
changes, also gets the text of this license or a link to
|
|
12743
|
+
<https://blueoakcouncil.org/license/1.0.0>.
|
|
12744
|
+
|
|
12745
|
+
## Excuse
|
|
12746
|
+
|
|
12747
|
+
If anyone notifies you in writing that you have not
|
|
12748
|
+
complied with [Notices](#notices), you can keep your
|
|
12749
|
+
license by taking all practical steps to comply within 30
|
|
12750
|
+
days after the notice. If you do not do so, your license
|
|
12751
|
+
ends immediately.
|
|
12752
|
+
|
|
12753
|
+
## Patent
|
|
12754
|
+
|
|
12755
|
+
Each contributor licenses you to do everything with this
|
|
12756
|
+
software that would otherwise infringe any patent claims
|
|
12757
|
+
they can license or become able to license.
|
|
12758
|
+
|
|
12759
|
+
## Reliability
|
|
12760
|
+
|
|
12761
|
+
No contributor can revoke this license.
|
|
12762
|
+
|
|
12763
|
+
## No Liability
|
|
12764
|
+
|
|
12765
|
+
**_As far as the law allows, this software comes as is,
|
|
12766
|
+
without any warranty or condition, and no contributor
|
|
12767
|
+
will be liable to anyone for any damages related to this
|
|
12768
|
+
software or this license, under any kind of legal claim._**
|
|
12769
|
+
```
|
|
12770
|
+
|
|
12593
12771
|
## ms@2.1.3
|
|
12594
12772
|
|
|
12595
12773
|
License: MIT
|
|
@@ -12823,6 +13001,33 @@ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
|
12823
13001
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
12824
13002
|
```
|
|
12825
13003
|
|
|
13004
|
+
## path-browserify@1.0.1
|
|
13005
|
+
|
|
13006
|
+
License: MIT
|
|
13007
|
+
|
|
13008
|
+
```
|
|
13009
|
+
MIT License
|
|
13010
|
+
|
|
13011
|
+
Copyright (c) 2013 James Halliday
|
|
13012
|
+
|
|
13013
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
13014
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
13015
|
+
the Software without restriction, including without limitation the rights to
|
|
13016
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
13017
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
13018
|
+
subject to the following conditions:
|
|
13019
|
+
|
|
13020
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13021
|
+
copies or substantial portions of the Software.
|
|
13022
|
+
|
|
13023
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
13024
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
13025
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
13026
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
13027
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
13028
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
13029
|
+
```
|
|
13030
|
+
|
|
12826
13031
|
## path-expression-matcher@1.5.0
|
|
12827
13032
|
|
|
12828
13033
|
License: MIT
|
|
@@ -13982,6 +14187,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
13982
14187
|
SOFTWARE.
|
|
13983
14188
|
```
|
|
13984
14189
|
|
|
14190
|
+
## ts-morph@28.0.0
|
|
14191
|
+
|
|
14192
|
+
License: MIT
|
|
14193
|
+
|
|
14194
|
+
```
|
|
14195
|
+
The MIT License (MIT)
|
|
14196
|
+
|
|
14197
|
+
Copyright (c) 2017 David Sherret
|
|
14198
|
+
|
|
14199
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14200
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14201
|
+
in the Software without restriction, including without limitation the rights
|
|
14202
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14203
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14204
|
+
furnished to do so, subject to the following conditions:
|
|
14205
|
+
|
|
14206
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14207
|
+
copies or substantial portions of the Software.
|
|
14208
|
+
|
|
14209
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14210
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
14211
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
14212
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
14213
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
14214
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
14215
|
+
SOFTWARE.
|
|
14216
|
+
```
|
|
14217
|
+
|
|
13985
14218
|
## tslib@2.8.1
|
|
13986
14219
|
|
|
13987
14220
|
License: 0BSD
|
package/bin/voltro.mjs
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// `framework` executable. `pnpm install` symlinks this into every
|
|
3
|
+
// dependent package's `node_modules/.bin/framework`, so user apps invoke
|
|
4
|
+
// it from their npm scripts as just `voltro dev` / `voltro serve`
|
|
5
|
+
// — same UX as `next dev` or `vite`, without the user knowing the bin
|
|
6
|
+
// lives in our monorepo's framework/ tree.
|
|
7
|
+
//
|
|
8
|
+
// Prefer the bundled, minified `dist/bin.js` (the published artifact);
|
|
9
|
+
// fall back to the TypeScript `src/bin.ts` when running from the
|
|
10
|
+
// monorepo before a build (dev). The tsx ESM loader stays registered in
|
|
11
|
+
// BOTH modes because the CLI imports the USER's `app.config.ts` (and
|
|
12
|
+
// other `.ts` app files) at runtime — Node cannot import those without
|
|
13
|
+
// it. The public bin interface (`voltro <cmd>`) is unchanged.
|
|
2
14
|
|
|
3
15
|
import { spawn } from 'node:child_process'
|
|
4
16
|
import { existsSync } from 'node:fs'
|
|
@@ -6,6 +18,16 @@ import { createRequire } from 'node:module'
|
|
|
6
18
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
19
|
import { dirname, join, resolve } from 'node:path'
|
|
8
20
|
|
|
21
|
+
// FAST PATH — `voltro serve <app>` with a precompiled SERVE BUNDLE boots from that
|
|
22
|
+
// ONE artefact IN THIS PROCESS: no child `node --import tsx`, no CLI command
|
|
23
|
+
// graph. `voltro build` esbuilds the whole serve path (framework + effect +
|
|
24
|
+
// @voltro inlined) into `.framework/dist-api/serveBundle/serveEntry.js` with the
|
|
25
|
+
// app modules as lazy .js chunks — so tsx is UNNECESSARY (nothing loads `.ts` at
|
|
26
|
+
// runtime) and the second node startup + the tsx loader hook are pure overhead on
|
|
27
|
+
// a cold scale-to-zero boot. The bundle's exported `runServe` loads `.env` itself
|
|
28
|
+
// (idempotent) so the boot is fully self-contained here. In PRODUCTION the bundle
|
|
29
|
+
// is MANDATORY — a missing/unusable bundle fails loud (below), never on-demand
|
|
30
|
+
// tsx. Only non-production falls through to the tsx spawn path (a dev convenience).
|
|
9
31
|
const argv = process.argv.slice(2)
|
|
10
32
|
if (argv[0] === 'serve') {
|
|
11
33
|
const appArg = argv.slice(1).find((a) => !a.startsWith('-'))
|
|
@@ -13,6 +35,11 @@ if (argv[0] === 'serve') {
|
|
|
13
35
|
const serveBundle = join(serveRoot, '.framework', 'dist-api', 'serveBundle', 'serveEntry.js')
|
|
14
36
|
if (existsSync(serveBundle)) {
|
|
15
37
|
try {
|
|
38
|
+
// chdir to the app root BEFORE importing the bundle: the bundle's generated
|
|
39
|
+
// entry registers its app-module keys from `process.cwd()` at import time, so
|
|
40
|
+
// cwd must already be the app root (the serve runtime resolves the same keys
|
|
41
|
+
// by walking the app tree). This is what makes the bundle relocation-safe —
|
|
42
|
+
// the Docker deploy moves the app from `apps/x/api` to the image root.
|
|
16
43
|
if (serveRoot !== process.cwd()) process.chdir(serveRoot)
|
|
17
44
|
const mod = await import(pathToFileURL(serveBundle).href)
|
|
18
45
|
if (typeof mod.runServe === 'function') {
|
|
@@ -20,17 +47,42 @@ if (argv[0] === 'serve') {
|
|
|
20
47
|
process.exit(typeof code === 'number' ? code : 0)
|
|
21
48
|
}
|
|
22
49
|
} catch (error) {
|
|
23
|
-
|
|
50
|
+
// Unusable bundle — degrade to the standard tsx path, never crash the boot.
|
|
51
|
+
console.warn('[voltro] serve bundle failed to load:', error?.message ?? error)
|
|
24
52
|
}
|
|
25
53
|
}
|
|
54
|
+
// PRODUCTION MUST serve from the precompiled bundle — never transpile on demand.
|
|
55
|
+
// If we reach here in production the bundle is missing or unusable: FAIL LOUD
|
|
56
|
+
// instead of silently spawning the slow tsx path (which re-transpiles the app on
|
|
57
|
+
// every cold start, defeating the whole precompiled-boot optimisation). `voltro
|
|
58
|
+
// build` produces the bundle and treats a build failure as fatal, so a healthy
|
|
59
|
+
// deploy never hits this. Non-production (dev / an unbuilt local serve) falls
|
|
60
|
+
// through to the tsx path below — on-demand transpile is a DEV-only convenience.
|
|
61
|
+
if (process.env.NODE_ENV === 'production') {
|
|
62
|
+
console.error(
|
|
63
|
+
`[voltro] FATAL: production \`voltro serve\` requires a precompiled serve bundle at\n ${serveBundle}\n` +
|
|
64
|
+
'but it is missing or failed to load. Run `voltro build` before serving — a bundle-build ' +
|
|
65
|
+
'failure is fatal by design; production never transpiles on demand.',
|
|
66
|
+
)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
26
69
|
}
|
|
27
70
|
|
|
28
71
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
29
72
|
const built = resolve(here, '..', 'dist', 'bin.js')
|
|
30
73
|
const entry = existsSync(built) ? built : resolve(here, '..', 'src', 'bin.ts')
|
|
31
74
|
|
|
75
|
+
// Resolve the tsx ESM loader RELATIVE TO @voltro/cli (which depends on it) —
|
|
76
|
+
// NOT the user app's CWD. As a bare `--import tsx`, Node resolves the specifier
|
|
77
|
+
// from the current working directory; strict pnpm nests tsx inside @voltro/cli,
|
|
78
|
+
// so from a consuming app's dir it isn't reachable and a standalone (non-
|
|
79
|
+
// monorepo) install fails at `voltro dev` / `voltro build` with "Cannot find
|
|
80
|
+
// package 'tsx'". Resolving to an absolute URL here sidesteps CWD resolution
|
|
81
|
+
// entirely (the monorepo happened to hoist tsx, which hid this).
|
|
32
82
|
let tsxImport = 'tsx'
|
|
33
83
|
try {
|
|
84
|
+
// import.meta.resolve applies tsx's export map (the ESM auto-register entry);
|
|
85
|
+
// synchronous since Node 20.6.
|
|
34
86
|
tsxImport = import.meta.resolve('tsx')
|
|
35
87
|
} catch {
|
|
36
88
|
try {
|
|
@@ -46,6 +98,11 @@ const child = spawn(
|
|
|
46
98
|
{ stdio: 'inherit' },
|
|
47
99
|
)
|
|
48
100
|
|
|
101
|
+
// Forward signals the parent receives to the child. Without this the
|
|
102
|
+
// shim catches SIGTERM (e.g. from `kill $PID` in test-all.sh) and exits,
|
|
103
|
+
// leaving the spawned Node child as an orphan still bound to its port.
|
|
104
|
+
// Subsequent harness stages then fail to start their own server because
|
|
105
|
+
// the port is held by the orphan.
|
|
49
106
|
const forward = (signal) => () => {
|
|
50
107
|
if (!child.killed) child.kill(signal)
|
|
51
108
|
}
|
|
@@ -61,12 +118,25 @@ child.on('exit', (code, signal) => {
|
|
|
61
118
|
process.exit(code ?? 0)
|
|
62
119
|
})
|
|
63
120
|
|
|
121
|
+
// Parent-death guard. When invoked from a smoke (or any wrapper) that
|
|
122
|
+
// dies ungracefully — `process.exit(1)` skipping `finally`, kill -9,
|
|
123
|
+
// IDE termination — we'd otherwise be reparented to init and keep our
|
|
124
|
+
// tsx child alive holding the port. We watch our parent pid; when it
|
|
125
|
+
// flips to 1 we forward SIGTERM to the child and exit. The 2s interval
|
|
126
|
+
// gives the harness fast-enough orphan cleanup without burning a watch
|
|
127
|
+
// loop per shim. The TSX child has its own ppid watch on US (the shim),
|
|
128
|
+
// so this layer covers the case where the shim itself outlives its
|
|
129
|
+
// parent; the inner layer covers the unusual case of the shim dying
|
|
130
|
+
// while the tsx child somehow survives.
|
|
64
131
|
const initialPpid = process.ppid
|
|
65
132
|
if (initialPpid > 1) {
|
|
66
133
|
const ppidWatch = setInterval(() => {
|
|
67
134
|
if (process.ppid !== initialPpid || process.ppid === 1) {
|
|
68
135
|
clearInterval(ppidWatch)
|
|
69
136
|
if (!child.killed) child.kill('SIGTERM')
|
|
137
|
+
// Give the child a brief window to clean up + exit; if it doesn't,
|
|
138
|
+
// we exit anyway. The child's `on('exit')` above will route us out
|
|
139
|
+
// cleanly if it does.
|
|
70
140
|
setTimeout(() => process.exit(0), 1500).unref()
|
|
71
141
|
}
|
|
72
142
|
}, 2000)
|