@voltro/ui-shadcn 0.77.1 → 0.79.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 +96 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,102 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.79.0] — 2026-09-23
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **Probes answer on the app port from the first moment, on every command** — `@voltro/cli`, `@voltro/runtime`, `@voltro/voltro`
|
|
47
|
+
|
|
48
|
+
`voltro dev`, `voltro serve` and `voltro start` now bind their app port as the FIRST thing they do once they know it, and answer `/internal/liveness` (200), `/internal/readiness` (503 until the app serves, then 200 with the dependency ping) and `/internal/startup` (JSON `{ phase, ready, elapsedMs, … }`, 503 → 200) there through the whole boot — a slow database connect, a fingerprint check or a cold migrate included. Every other request and every WebSocket upgrade gets `503 + Retry-After: 1` until ready, then the app serves on the same socket and the probe paths keep answering before routing. There is ONE server and ONE port, so a probe path means the same thing on every command; `voltro dev` no longer binds a second probe server on app port + 1 and no longer reads `VOLTRO_DEV_HEALTH_PORT`. Migration: point every probe of a pod running `voltro dev` at the app port and delete the variable — the codemod prints the exact steps and finds the variable in compose, Helm and CI files.
|
|
49
|
+
|
|
50
|
+
**`voltro update` carries you across this** — codemod `0.79.0/01_probes-on-the-app-port`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.79.0).
|
|
51
|
+
- **A rejected credential is refused on the default tenant policy too** — `@voltro/cli`, `@voltro/protocol`, `@voltro/voltro`
|
|
52
|
+
|
|
53
|
+
A request whose credential a strategy rejects (an expired or malformed token, a revoked session or key) now ends with `Unauthenticated` (`credential rejected: <reason>`) on every anonymous-tenant policy. Until now that held under `auth: { anonymousTenantId }` and `auth: { anonymousTenantRequired: true }`, but not for an app that sets neither. There, a procedure without a guard served the caller as an anonymous one, and an open query answered public data to a token that had just been refused. A caller who sends no credential is unaffected: it still resolves to the tenant its `x-tenant` header names, or to the development tenant. `composeAuthStrategies` gains `anonymousTenantDefault` for that default tenant. Unlike a `fallback`, it does not take over the rejected-credential decision. A client that kept sending a stale cookie or token and relied on the anonymous answer now receives `Unauthenticated`, which the error bus reports and `wireAuthRefresh` acts on.
|
|
54
|
+
|
|
55
|
+
**`voltro update` carries you across this** — codemod `0.79.0/03_rejected-credentials-refused-on-the-default-policy`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.79.0).
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **A 1366 over a 4-byte character says which config field fixes it** — `@voltro/database`
|
|
60
|
+
|
|
61
|
+
When MySQL or MariaDB refuse a write with `Incorrect string value: '\xF0…'` (errno 1366 over a four-byte UTF-8 lead byte — an emoji into a `utf8mb3` column), the store's `detail:` line now carries a `hint:` naming the column and the remedy: declare `store: { dialect, charset: 'utf8mb4' }` in `app.config.ts` and run `voltro db plan`. A 1366 for any other reason gets no advice about character sets.
|
|
62
|
+
- **The store's character set is declared once and every table follows it** — `@voltro/database`, `@voltro/cli`, `@voltro/devtools-ui`
|
|
63
|
+
|
|
64
|
+
On MySQL and MariaDB the framework's `CREATE TABLE` named no character set, so a `text()` column inherited whatever the database was created with while a `json()` column did not (MariaDB's `JSON` is `LONGTEXT CHARACTER SET utf8mb4`, MySQL's has none) — and on a `utf8mb3` database one table refused an emoji in its text column with errno 1366 and took it in its json column. `app.config.ts` now takes `store: { dialect: 'mariadb' | 'mysql', charset: 'utf8mb4' | 'utf8mb3' }`: every declared table is created with it as its default and, when a live table's string columns are stored otherwise, converted to it by a new `convert-charset` plan operation — a table rewrite that drops and re-creates the foreign keys touching the table, so a pair bound by a constraint converts together instead of one side failing with errno 150. Widening to `utf8mb4` is `safe`; narrowing to `utf8mb3` is `lossy` and waits for the destructive opt-in. json columns keep the engine's own charset (neither engine lets it be declared). Unset, nothing changes. A charset on any other dialect is refused at boot, and a `DB_DIALECT` override off the mysql family drops it with a logged line rather than silently.
|
|
65
|
+
|
|
66
|
+
The public signatures that changed only gain optional trailing parameters (`declaredSnapshot`, `emitNamespacedSchemaSql`, `emitNamespaceProvisionDdl`, the schema-emit options) and one union member (`convert-charset` in the migration operation kind), so existing calls compile unchanged. Only an exhaustive `switch` over that operation kind would need a new case.
|
|
67
|
+
- **The plan and the doctor name tables whose text cannot hold what json can** — `@voltro/cli`, `@voltro/database`
|
|
68
|
+
|
|
69
|
+
Without a declared store charset, `voltro db plan` prints — under the plan — the MySQL / MariaDB tables whose text columns are stored in a character set narrower than their json columns' `utf8mb4`, and the tables whose string columns are not all in one character set (a column converted by hand). `voltro doctor` has the same section, reading `information_schema`, with three outcomes that never print alike: not applicable off the mysql family, not checked with the reason when no database is reachable, and the findings — against the declaration when one is set. A healthy `utf8mb4` database reports nothing; in `--json` the section is `storeCharset`.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- **A handler's own `Unauthenticated` reaches the client typed, on any procedure** — `@voltro/protocol`, `@voltro/cli`, `@voltro/runtime`
|
|
74
|
+
|
|
75
|
+
`Unauthenticated` and `ScopeError` are now members of every procedure's wire error union — queries, mutations, actions, streams and workflow starts, whether or not the descriptor declares a guard — and `Unauthenticated` of every event's. They used to ride only a procedure with an enforced `guards:` entry, on the reasoning that an open procedure cannot produce them. It can: the auth middleware refuses an open procedure with a rejected credential exactly as it refuses a guarded one, and a handler may refuse on its own with `assertAuthenticated` or `requireScope`. On a procedure without a guard the server judged that handler-side refusal undeclared and collapsed it, so the browser received `{ _tag: 'InternalError', message: 'Unauthenticated: …' }` — the typed error's name inside a defect that no `_tag` match and no credential-refresh policy could see — and the generated client had no schema to decode the middleware's own refusal with. Both ends now carry the same union, so the value arrives as `{ _tag: 'Unauthenticated' }` on the failure channel over `POST /rpc` and over the WebSocket, and a gRPC surface maps it to `UNAUTHENTICATED` rather than `FAILED_PRECONDITION`. Measured under `voltro dev` and `voltro serve` with a validly signed, expired JWT driven through the real client over the socket.
|
|
76
|
+
- **`voltro build` walks an api app's module closure once, not once per handler file** — `@voltro/cli`
|
|
77
|
+
|
|
78
|
+
Each api artefact (`apiEntry.js`, the serve bundle) is preceded by a closure-discovery pass that only reads esbuild's metafile. It used to hand esbuild every discovery file as its own entry point, and without code splitting esbuild links every entry point separately, each on its own copy of the module graph, all at once. On a synthetic api app of 6,747 modules (4,514 handler files) that probe alone peaked at 4.3 GiB in the esbuild service — ten times the bundle it precedes — and it ran twice per build, so the whole build peaked at 5–6 GiB and could be refused by a shared host with less free memory than that. The probe now links one generated entry that imports every discovery file; the closure it finds is the same set. Measured on the same app the build's peak is under 2 GiB (esbuild 1.9 GiB, Node 0.5 GiB), at 3,048 modules 1.0 GiB, at 1,047 modules 0.9 GiB. The build page documents the expectation.
|
|
79
|
+
- **The file-migration `phase` rewrite reaches aliased, held and nested specs** — `@voltro/cli`
|
|
80
|
+
|
|
81
|
+
For a project crossing 0.78.0, the `0.78.0/01_file-migration-phase` rewrite now also finds `migration` imported under another name, a spec held in a local `const`, and migrations in subdirectories of `migrations/`. All three are shapes the runner discovers, and the rewrite used to skip them without a word. A spec built outside the call (by a helper, or imported) is not rewritten, because the change would land in code other callers share. Instead the rewrite names it by file and line. `migration()` still refuses a spec without a phase when the file loads.
|
|
82
|
+
- **A subscription refused in-band reaches the error bus and the refresh policy** — `@voltro/client`
|
|
83
|
+
|
|
84
|
+
A guard's refusal on a subscription travels in-band — a `{ _tag: 'error' }` event on that one stream, so a sibling subscription on the shared socket is not stalled. The subscription cache set the snapshot's error from it and told nobody else: the api's error bus, which `wireAuthRefresh` and `useOnRpcError` listen on, heard only streams that failed outright. So an expired credential on a read never asked the host for a fresh token and never reached an error reporter. The in-band value is now emitted to the bus with `source: 'subscription'` and `kind: 'handler'`, the same event a failed stream produces.
|
|
85
|
+
- **`voltro support collect` no longer fails on a temporary directory under src/** — `@voltro/cli`
|
|
86
|
+
|
|
87
|
+
The conflict-key inventory in `voltro support collect` found its files with a glob that skips dot-directories, then loaded them through a call that walked the directories again. So a temporary directory that an editor, a test run or a build tool creates and removes under `src/` could fail the whole collection with `Directory not found`, and an unreadable dot-directory failed it with `EACCES`. Only the files the glob returned are loaded now. A file removed between the two steps is skipped.
|
|
88
|
+
- **Tenant namespaces on MySQL / MariaDB provision without USE and take the charset** — `@voltro/database`, `@voltro/cli`
|
|
89
|
+
|
|
90
|
+
Provisioning a tenant namespace on the mysql family emitted `USE <database>` followed by unqualified DDL. MySQL 8.4 refuses `USE` in the prepared protocol (errno 1295), so under `tenancy.isolation: 'namespace'` no tenant could be provisioned on MySQL at all; on MariaDB the `USE` outlived its transaction on the pooled connection, which then served its next unqualified statement in the tenant database. The tenant DDL is now database-qualified, as the store's namespace view already was. The declared store charset reaches the tenant too: a new tenant database is created with it as its default and its tables carry it. A tenant provisioned before the declaration is named per namespace by `voltro db plan` and `voltro doctor` rather than silently left as it was.
|
|
91
|
+
- **`voltro update` moves the `@voltro/*` pins in `pnpm-workspace.yaml` too** — `@voltro/cli`
|
|
92
|
+
|
|
93
|
+
`voltro update` used to bump only the package.json files. A pnpm workspace that also names framework versions in `pnpm-workspace.yaml` was left behind in two ways. `overrides:` entries held the tree on the old release. And `minimumReleaseAgeExclude:` entries (`'@voltro/<pkg>@<version>'`, which admit a release younger than pnpm's release-age window) went stale, so pnpm refused the new release for that whole window. Both blocks now move with the bump. An entry moves only when it names exactly the version being left; an entry at any other version is printed as `kept`. A block written as a flow list is named rather than rewritten. The packages named there count for `--wait`, and the file is restored along with the package.json files when an install fails before anything lands.
|
|
94
|
+
- **`voltro update --wait` waits up to 45 minutes, longer than a release holds** — `@voltro/cli`
|
|
95
|
+
|
|
96
|
+
`voltro update --wait` polls the registry until every package it bumps is published at the target version, `@voltro/dashboard` included. It used to give up after 20 minutes. A release, though, holds `latest` for up to 30 minutes after its other packages finish uploading while `@voltro/dashboard` is still being built. So an update that started polling at the first upload could fail after a release that went on to complete normally. The ceiling is now 45 minutes, which covers the release's hold plus its upload phase. The update still reports the late packages while it waits, and on timeout it still restores the bumps byte for byte.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## [0.78.0] — 2026-09-22
|
|
101
|
+
|
|
102
|
+
### ⚠ BREAKING
|
|
103
|
+
|
|
104
|
+
- **A file migration declares whether it runs before or after the schema apply** — `@voltro/database`, `@voltro/cli`
|
|
105
|
+
|
|
106
|
+
`migration({ … })` now requires `phase: 'before-schema' | 'after-schema'`. `'before-schema'` is the order every file migration has always run in — first, before the planner diffs declared against live — and is unchanged. The new `'after-schema'` phase runs LAST in the same job, once the plan has been applied and the plugin steps and change triggers have converged, which is where a body that needs the new shape belongs: a back-fill into a table the same deploy creates could previously only meet its own `skipUnless` guard, be recorded, and never run again. Both phases run under the same failure gate in `voltro migrate`, `voltro db apply`, `voltro db files` and the `voltro dev` auto-migrate; `voltro db apply --plan` refuses a pending `before-schema` migration as before and applies a pending `after-schema` one. Migration: run `voltro update` (the codemod adds `phase: 'before-schema'` to every existing migration), or add the field by hand.
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- **A migrate run names a skip the same run makes permanent** — `@voltro/database`, `@voltro/cli`
|
|
111
|
+
|
|
112
|
+
A `before-schema` file migration that stops at `ctx.skipUnless` because a schema object is absent is recorded as applied and never offered again — so when the plan computed immediately afterwards CREATES that object, the skip could never have been anything else. `voltro db apply`, `voltro migrate` and the `voltro dev` auto-migrate now say so, naming the objects the body found missing that the plan goes on to create and the phase that would have run it. The objects are measured from the `ctx.schema` existence questions the body actually asked, not read out of the skip reason, so a body that skips for a reason unrelated to the schema is not reported. Nothing is refused: by the time both facts exist the migration is already recorded.
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
|
|
116
|
+
- **`latest` waits for the whole release, the dashboard included** — `@voltro/cli`
|
|
117
|
+
|
|
118
|
+
Every `@voltro/*` package of a release carries the same version, and `@voltro/dashboard` is published from its own repository once the rest are on the registry — so for the length of that run the release existed for every package but one, while `latest` already named it. Measured across the three releases before this change: the dashboard landed 8m43s, 9m00s and 8m17s after `@voltro/cli`. An installation started in that window against the newest published version bumps every pin and then fails with `ERR_PNPM_NO_MATCHING_VERSION` on the one package that does not have it yet. The release now holds the `latest` tag until every package of the lockstep set is downloadable — the dashboard is waited for exactly like a tarball that has not reached the CDN — so the newest published version is one that can be installed whole. Until it is complete, `latest` stays on the previous release rather than naming a partial one. An update that names an explicit version can still start mid-release, because a version resolves the moment its own package uploads; `voltro update --wait` remains the way to cover that.
|
|
119
|
+
- **A migration body guarding on `schema.tableExists` runs in a database-free test** — `@voltro/database`, `@voltro/runtime`
|
|
120
|
+
|
|
121
|
+
A file migration whose first statement is the guard the docs recommend — `ctx.skipUnless(ctx.schema.tableExists('…'), '…')` — could not be driven over the in-memory store: `skipUnless` worked without a SQL client, but its predicate was a `ctx.schema` call and every one of those refused, so the only way to test such a body was to split the guard out of `up`. A migration context built without a client now answers the two read-only probes the store can answer honestly about itself — `schema.tableExists` from the store's own table map, `schema.columnExists` from the keys the held rows carry — through the new optional `DataStore.storedTables()` capability, which `InMemoryDataStore` and its transaction view implement. A table that is present and empty exists; `columnExists` on an empty table refuses rather than answering `false`, because it holds no evidence either way. `schema.snapshot`, `schema.indexExists`, `schema.foreignKeyExists` and every schema change still refuse — a store has no catalog, and a fabricated answer is one a migration would branch on — and each refusal now names what to do instead.
|
|
122
|
+
|
|
123
|
+
Two more fixes on the same surface, both found by running a PROMISE-form body through the guard rather than only an Effect one. `skipUnless` reached the runner wrapped in a `FiberFailure`, so the migration FAILED instead of being recorded as skipped. And a `ctx.schema` call from a Promise body inside a transactional migration asked the pool for a connection instead of using the transaction's, which on a single-connection dialect is the connection the transaction is holding — the migration hung forever. Every `ctx.schema` member now executes on the store's own connection, so both body shapes reach the same outcome on every dialect.
|
|
124
|
+
- **A storage change no longer reads as `text → text` in a migration plan** — `@voltro/cli`, `@voltro/database`
|
|
125
|
+
|
|
126
|
+
A change to a column's storage PARAMETERS — a text length, a decimal precision — keeps the column's type, so the planned operation carries the same type on both sides. Two readers rendered that as the type change it is not: the plan row showed `tier text → text`, which is indistinguishable from a no-op, and the rolling-deploy advisory said "the new text type can reject their writes", which names nothing to act on. Both now say the storage is what moves, and the advisory's remedy is the one that applies — widen now, narrow in a later deploy once every writer produces values that fit. The numbers were never lost: the operation's reason carries them (`text length 65535 → 16`, with the count query for a narrowing) and still does. Reachable for an ordinary edit: adding `.oneOf([...])` to an existing unbounded `text()` column renders a bounded column on every dialect that can express a width, measured on MySQL and MariaDB.
|
|
127
|
+
- **TypeScript 7 removed thirteen tsconfig options, not the three we named** — `@voltro/cli`
|
|
128
|
+
|
|
129
|
+
The TypeScript 7 upgrade note named three removed tsconfig options. A workspace that followed it hit a fourth — `"esModuleInterop": false`, in twelve of its configs, where the ones that set `true` typechecked cleanly so only part of the workspace failed. Adding a fourth bullet would have left nine more waiting, so the set was measured instead: every boolean option TypeScript 6.0.3 accepts, written into a tsconfig in both spellings, plus every value of every enum option, typechecked against TypeScript 7.0.2. Thirteen are refused, in two shapes — an option whose value alone is gone (`esModuleInterop=false`, `allowSyntheticDefaultImports=false`, `alwaysStrict=false`, `target=ES5`, `module=AMD`/`System`/`UMD`, `moduleResolution=node10`/`Classic`) and one removed outright (`downlevelIteration`, `outFile`, `baseUrl`). A new note carries all of them with their replacements and three `git grep` lines that find every occurrence at once, and it fires only for a config that actually carries one — the accepted spelling stays quiet. It also says the thing the codes cannot: TS5108 covers several unrelated removals, so what identifies a case is the option and value in the message, not the code. The framework's own templates carry none of the thirteen; checked, not assumed.
|
|
130
|
+
|
|
131
|
+
The gate matches a PATTERN rather than a literal, which is new for a codemod that reads config: JSON permits whitespace wherever a token boundary allows, so `"esModuleInterop" : false` and a literal list of spellings disagree — and a spelling the list forgets is silence from the gate, which reads as nothing to do.
|
|
132
|
+
- **Workflow submission rows carry one identity shape again** — `@voltro/cli`, `@voltro/workflow`
|
|
133
|
+
|
|
134
|
+
Starts of a workflow with a declared concurrency limit wrote their internal submission row with an identity prefix one character shorter than the one every other start path writes, so `_voltro_workflow_submissions` ended up holding an identity shape nothing declared. That is not cosmetic: pool release recovery pages this table by id under the database's own collation, and the two shapes sort in one order there and the opposite order in the serving process — measured on MariaDB with `utf8mb3_general_ci`, where a census over a mixed page stopped at the row that crosses the shapes and left every pooled permit held. All start paths now mint a new identity through one function. The one other shape this table holds is deliberate — a retry request reuses its own content-addressed identity so a retried reservation finds its own row — and recovery tolerates it. Rows written by earlier versions keep their identity and are read normally; nothing needs to be migrated.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
42
138
|
## [0.77.1] — 2026-09-22
|
|
43
139
|
|
|
44
140
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/ui-shadcn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"description": "Voltro's first-party shadcn/ui kit: Tailwind v4 design tokens (light + dark), 30+ primitives, layout compositions, styled widgets for the @voltro/ui seam, and the canonical theme/language preference-cookie helpers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@radix-ui/react-toggle-group": "^1.1.19",
|
|
53
53
|
"@shikijs/langs": "^4.4.3",
|
|
54
54
|
"@shikijs/themes": "^4.4.3",
|
|
55
|
-
"@voltro/ui": "0.
|
|
55
|
+
"@voltro/ui": "0.79.0",
|
|
56
56
|
"class-variance-authority": "^0.7.1",
|
|
57
57
|
"clsx": "^2.1.1",
|
|
58
58
|
"shiki": "^4.4.3",
|