@zerotal/arch 1.7.4 → 1.8.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 +86 -2
- package/README.md +4 -2
- package/api-surface.md +2 -0
- package/docs/about.md +16 -16
- package/docs/admin/actions.md +9 -9
- package/docs/admin/auth.md +2 -2
- package/docs/admin/dashboard.md +5 -5
- package/docs/admin/extending-ui.md +12 -12
- package/docs/admin/extending.md +16 -7
- package/docs/admin/forms.md +4 -4
- package/docs/admin/index.md +2 -2
- package/docs/admin/operations.md +9 -9
- package/docs/admin/resources.md +3 -3
- package/docs/admin/structure.md +6 -6
- package/docs/admin/tables.md +8 -8
- package/docs/admin/testing.md +4 -4
- package/docs/ai.md +118 -11
- package/docs/application.md +12 -12
- package/docs/arch.md +25 -3
- package/docs/assets.md +11 -11
- package/docs/audit.md +13 -13
- package/docs/authentication.md +39 -39
- package/docs/authorization.md +13 -13
- package/docs/broadcasting/channels.md +2 -2
- package/docs/broadcasting/client.md +2 -2
- package/docs/broadcasting/events.md +9 -10
- package/docs/broadcasting/index.md +1 -1
- package/docs/broadcasting/testing.md +5 -5
- package/docs/cache.md +15 -13
- package/docs/carbon.md +35 -35
- package/docs/changelog.md +268 -1
- package/docs/client/index.md +13 -13
- package/docs/commands.md +36 -4
- package/docs/components.md +116 -115
- package/docs/config-system.md +4 -4
- package/docs/container.md +27 -27
- package/docs/context.md +27 -27
- package/docs/contributing.md +28 -0
- package/docs/controllers.md +10 -10
- package/docs/conventions.md +23 -15
- package/docs/cookies.md +3 -3
- package/docs/csrf.md +6 -6
- package/docs/database.md +22 -17
- package/docs/deployment.md +99 -2
- package/docs/devtools.md +16 -16
- package/docs/email-verification.md +8 -8
- package/docs/encryption.md +7 -7
- package/docs/errors.md +6 -6
- package/docs/events.md +12 -11
- package/docs/flow/components.md +32 -32
- package/docs/flow/decorators.md +18 -18
- package/docs/flow/events.md +16 -16
- package/docs/flow/forms.md +17 -17
- package/docs/flow/icons.md +5 -5
- package/docs/flow/index.md +21 -21
- package/docs/flow/layouts.md +99 -35
- package/docs/flow/lifecycle.md +16 -16
- package/docs/flow/models.md +9 -9
- package/docs/flow/pagination.md +8 -8
- package/docs/flow/performance.md +7 -7
- package/docs/flow/references.md +3 -3
- package/docs/flow/routing.md +26 -26
- package/docs/flow/testing.md +20 -20
- package/docs/getting-started.md +18 -13
- package/docs/health.md +17 -8
- package/docs/helpers.md +17 -17
- package/docs/i18n.md +10 -10
- package/docs/inertia/devtools.md +4 -4
- package/docs/inertia/index.md +6 -6
- package/docs/inertia/props.md +21 -21
- package/docs/inertia/rendering.md +9 -9
- package/docs/inertia/ssr.md +2 -2
- package/docs/lifecycle.md +2 -2
- package/docs/lock.md +11 -11
- package/docs/logger.md +12 -12
- package/docs/media.md +22 -22
- package/docs/middleware.md +17 -17
- package/docs/migrations.md +13 -13
- package/docs/monitor.md +6 -6
- package/docs/notifications.md +24 -24
- package/docs/orm/casts.md +14 -14
- package/docs/orm/factories.md +8 -8
- package/docs/orm/index.md +22 -20
- package/docs/orm/lifecycle.md +13 -13
- package/docs/orm/queries.md +28 -28
- package/docs/orm/relationships.md +16 -16
- package/docs/orm/serialization.md +11 -11
- package/docs/package-development.md +10 -10
- package/docs/pagination.md +14 -14
- package/docs/password-reset.md +6 -6
- package/docs/providers.md +11 -11
- package/docs/query-builder.md +18 -18
- package/docs/queue.md +14 -12
- package/docs/rate-limiting.md +6 -6
- package/docs/responses.md +14 -14
- package/docs/roles-and-2fa.md +9 -9
- package/docs/routing.md +44 -44
- package/docs/scaffolding.md +1 -1
- package/docs/scheduler.md +10 -10
- package/docs/seeding.md +6 -6
- package/docs/session.md +16 -14
- package/docs/social.md +14 -14
- package/docs/storage.md +25 -13
- package/docs/structure.md +2 -2
- package/docs/support-policy.md +23 -10
- package/docs/telemetry.md +12 -12
- package/docs/tenancy.md +34 -40
- package/docs/testing/browser.md +6 -6
- package/docs/testing/console.md +3 -3
- package/docs/testing/database.md +10 -10
- package/docs/testing/flow-browser.md +6 -6
- package/docs/testing/http.md +12 -12
- package/docs/testing/index.md +29 -3
- package/docs/testing/mocking.md +8 -8
- package/docs/upgrade.md +1 -1
- package/docs/validator.md +19 -19
- package/docs/view.md +14 -14
- package/package.json +4 -4
- package/src/config.ts +8 -0
- package/src/index.ts +25 -1
- package/src/install/ArchInstallCommand.ts +46 -3
- package/src/install/guidelines.ts +84 -0
- package/src/install/shape.ts +177 -0
- package/src/install/skills.ts +271 -0
- package/src/provider/ArchProvider.ts +148 -0
- package/src/tools/searchDocs.ts +7 -0
package/docs/changelog.md
CHANGED
|
@@ -27,6 +27,247 @@ the section for every version you cross and apply its migration notes, not only
|
|
|
27
27
|
majors. [Releases and versioning](/docs/support-policy#releases-and-versioning) explains
|
|
28
28
|
when that carve-out ends.
|
|
29
29
|
|
|
30
|
+
## 1.8.0 — 2026-08-24
|
|
31
|
+
|
|
32
|
+
The first render mode, the codemod runner 2.0 depends on, and four failures that
|
|
33
|
+
each looked like something other than what they were.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`static interactive = false` — the first rung of Flow's render modes.** Every component
|
|
38
|
+
until now was maximally interactive: rendered on the server, dehydrated into a snapshot,
|
|
39
|
+
tracked by the client, reachable over a socket. Right for a counter, wasteful for a nav rail.
|
|
40
|
+
A static component is rendered in full by its parent and nothing else — no `onDehydrate`, no
|
|
41
|
+
snapshot, no `<script type="application/json">`, no entry in the client's registry, and no
|
|
42
|
+
`data-flow-root`, which would freeze it at its first render since its only route to an update
|
|
43
|
+
is the parent re-rendering it. It takes no place in `_childIds` and does not shift its
|
|
44
|
+
interactive siblings' ids, so no sibling remounts and loses its state when a static one
|
|
45
|
+
appears above it. `lazy`, `defer` and `stream` throw rather than being ignored: each waits
|
|
46
|
+
for the client to ask for the real render, and a static child never registers to do the
|
|
47
|
+
asking.
|
|
48
|
+
|
|
49
|
+
Opt-in — nothing existing changes. `this.isInteractive` reports the mode from inside the
|
|
50
|
+
component, and being a new public member it takes that name away from applications; it is on
|
|
51
|
+
the documented reserved list. See [Static children](/docs/flow/layouts#static-children).
|
|
52
|
+
|
|
53
|
+
- **`zt upgrade` — the codemod runner.** The 2.0 ledger's rule is that every entry that can
|
|
54
|
+
have a codemod has one before 2.0 ships, and until now nothing had been built, which made
|
|
55
|
+
the ledger a list of changes nobody could afford to make.
|
|
56
|
+
|
|
57
|
+
**Dry by default**, which is backwards from most tools and deliberate: it rewrites source
|
|
58
|
+
across a whole project, and the first run should be something you can read and disagree with.
|
|
59
|
+
`--write` applies it. Nothing is written until the whole plan is known, so a run that fails
|
|
60
|
+
halfway leaves no half-upgraded tree, and `--dry` exercises the same code path as the real
|
|
61
|
+
thing rather than a parallel one that can drift from it. Codemods see each other's output,
|
|
62
|
+
because two of them touching one file across a version range is ordinary.
|
|
63
|
+
|
|
64
|
+
**What it could not do is the headline.** A codemod that walks past what it does not
|
|
65
|
+
understand is worse than none, since the changes it _did_ make imply the job is finished. So
|
|
66
|
+
every codemod returns two lists and the runner prints the second last and loudest, with file,
|
|
67
|
+
line and a reason. The first codemod covers the deprecated aliases — `BaseModel` → `Model`,
|
|
68
|
+
`routes:types` → `route:types`, `serve --dev` → `dev`. See [Commands](/docs/commands).
|
|
69
|
+
|
|
70
|
+
- **`--clean` on `assets:build` and `inertia:build`.** Pruning is conservative by default:
|
|
71
|
+
chunk-shaped filenames, plus whatever the last build on this machine recorded in `.zerotal/`.
|
|
72
|
+
That cannot recognise output some other naming produced. `--clean` needs no record — the
|
|
73
|
+
output directory belongs to the build, and what the build did not write does not belong in
|
|
74
|
+
it. It refuses `public/` and the project root, where deleting what was not rebuilt takes the
|
|
75
|
+
app's images and favicon with it, which is the one failure here that building again cannot
|
|
76
|
+
undo. Pruning stays the default; only you can say the directory holds nothing else.
|
|
77
|
+
|
|
78
|
+
- **Agent skills, from `@zerotal/arch`.** `AGENTS.md` is short because every prompt it lands
|
|
79
|
+
in pays for its whole length, so it points rather than teaches. That has a cost: an agent
|
|
80
|
+
gets a map and no detail, and the detail is where the expensive mistakes live. A skill is a
|
|
81
|
+
file with a one-line description that costs nothing until an agent decides it is relevant,
|
|
82
|
+
so a procedure can be written out in full. Two ship — one on changing the schema (who owns
|
|
83
|
+
it in your app, the mixin columns nothing declares, and why an unguarded `ALTER TABLE`
|
|
84
|
+
collides during a release's `migrate`) and one on shipping a release (naming your own deploy
|
|
85
|
+
steps, replacing the asset directory rather than merging into it, `trustedProxies` behind a
|
|
86
|
+
proxy, and the pipe that hides a test suite's exit status).
|
|
87
|
+
|
|
88
|
+
Written to `.agents/skills`, plus `.claude/skills` when that agent is detected. To replace
|
|
89
|
+
one this ships, edit it and delete its marker line — a `SKILL.md` without the marker is
|
|
90
|
+
yours and is never rewritten. `ArchConfig({ skills: false })` turns the feature off. Run
|
|
91
|
+
`zt arch:update` to install them.
|
|
92
|
+
|
|
93
|
+
- **`zt doctor` reports agent instructions that no longer describe your project.** Every fact
|
|
94
|
+
in the generated block moves without anyone thinking about the file: add a migrations
|
|
95
|
+
directory, turn `synchronize` off, install a package. It goes on reading as current while
|
|
96
|
+
describing the app you used to have, and guidance that is confidently out of date gets
|
|
97
|
+
followed rather than questioned. Skills rot the same way and are easier to miss, since
|
|
98
|
+
nothing reads one until an agent decides it is relevant — by which point it is being acted
|
|
99
|
+
on. The check regenerates both and compares, and names `zt arch:update` as the fix. A
|
|
100
|
+
warning, not a failure: it misleads a reader, it does not stop the app working.
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
|
|
104
|
+
- **A Flow page with nothing interactive on it opens no socket.** Every page connected at
|
|
105
|
+
boot, unconditionally — so a marketing page, a docs article or a rendered report held a
|
|
106
|
+
WebSocket per visitor, open on both ends for the life of the visit, to carry nothing. Both
|
|
107
|
+
paths that write to the socket take a `FlowComponent`, so with none registered there was not
|
|
108
|
+
a frame that _could_ be sent. The connection is made when something needs it now: after the
|
|
109
|
+
initial scan, after an SPA navigation, after a patch registers a child. `<Link navigate>`
|
|
110
|
+
fetches over HTTP, so a static page with links stays disconnected. A routed page honours the
|
|
111
|
+
same static, which is the half that matters — a page is a component, and one whose children
|
|
112
|
+
are static but which is interactive itself still connects.
|
|
113
|
+
|
|
114
|
+
- **The `@zerotal/arch` agent block describes how your app is set up, not only what it
|
|
115
|
+
installed.** A package list answers "what is available here", which is not the question that
|
|
116
|
+
decides what an agent should write: the framework's contracts are not uniform across
|
|
117
|
+
projects, and the places they differ are the places where guessing wrong compiles cleanly and
|
|
118
|
+
fails at runtime. `AGENTS.md` now states the four facts that change an instruction — who owns
|
|
119
|
+
the schema, whether route names are typed, whether `exactOptionalPropertyTypes` or
|
|
120
|
+
`noUncheckedIndexedAccess` are on, and whether there are tests to run. Read off disk rather
|
|
121
|
+
than from a booted app, because a project that will not boot is often why the agent surface
|
|
122
|
+
is being installed. `.env` is deliberately not among the files read: this output is committed
|
|
123
|
+
and pasted into prompts, and a detector that reads secrets is one refactor away from emitting
|
|
124
|
+
them. Re-run `zt arch:update` to pick it up.
|
|
125
|
+
|
|
126
|
+
### Fixed
|
|
127
|
+
|
|
128
|
+
- **No mail could be sent over port 587.** A STARTTLS upgrade hands back a new socket and
|
|
129
|
+
leaves the old one attached, still firing its callbacks — and what that one delivers from
|
|
130
|
+
then on is the undecrypted TLS stream. Both sets of handlers appended to a single reply
|
|
131
|
+
buffer, so handshake records and ciphertext sat in the middle of the server's replies and no
|
|
132
|
+
line in the buffer matched a reply any more: the driver waited out its timeout without ever
|
|
133
|
+
parsing the `250`, and the server logged a connection lost after STARTTLS. Measured, 1,737
|
|
134
|
+
bytes of ciphertext went into the discarded socket's handler while the TLS handler received
|
|
135
|
+
the replies.
|
|
136
|
+
|
|
137
|
+
`close` and `error` were worse than `data`. The plaintext socket ending is a normal part of
|
|
138
|
+
handing over to TLS, and it marked the live connection closed — rejecting whatever was
|
|
139
|
+
waiting on the session that had just replaced it. Each set of callbacks now captures the
|
|
140
|
+
generation it was installed for, and an upgrade bumps it.
|
|
141
|
+
|
|
142
|
+
- **An Inertia `303` redirect left the browser doing nothing at all.** `X-Inertia: true` was
|
|
143
|
+
set inside the 302-to-303 conversion, so it only ever reached a redirect that arrived as a
|
|
144
|
+
301 or 302 from a non-GET handler. A handler returning the 303 the protocol asks for skipped
|
|
145
|
+
the only line that marked its response — and `redirect(to, 303)` is what
|
|
146
|
+
[Authentication](/docs/authentication) tells people to write, in eight places. The form
|
|
147
|
+
submitted, the row was written, the mail went out, and the fields stayed filled in: a hang
|
|
148
|
+
from both ends, which is the worst shape a failure can take. Marking now happens for every
|
|
149
|
+
redirect status on an Inertia request, with the conversion a separate decision on top of it.
|
|
150
|
+
`307` and `308` are marked but left alone, since preserving the method is the whole reason to
|
|
151
|
+
choose them.
|
|
152
|
+
|
|
153
|
+
- **Answering the busy-port menu killed `serve --dev` on the spot.** The banner printed, then
|
|
154
|
+
`exited with code 1`, and nothing said why. Reading a prompt locks Bun's stdin stream, and
|
|
155
|
+
the lock is deliberately held for the life of the command so a second prompt can still read —
|
|
156
|
+
so the dev deck taking the terminal over threw `ReadableStream is locked`. It died inside the
|
|
157
|
+
alternate screen buffer, and restoring the terminal on the way out erased the error along
|
|
158
|
+
with everything else drawn there, which is why this was reported as "it just exits" rather
|
|
159
|
+
than as the error it was. The prompt hands stdin back where it took it; a deck that still
|
|
160
|
+
cannot have stdin degrades to streaming rather than dying, and a dev-mode failure stops the
|
|
161
|
+
deck before it reports.
|
|
162
|
+
|
|
163
|
+
- **Two builds sharing an output directory deleted each other's files.** Nothing forbids
|
|
164
|
+
`inertia:build` and `assets:build` writing to the same place, and the defaults invite it: one
|
|
165
|
+
writes to `public/assets`, `app.assets.outDir` often names the same directory, and the
|
|
166
|
+
default release pipeline runs them one after the other. The record of what to prune was one
|
|
167
|
+
flat list per directory, so each build read the other's files as its own previous build and
|
|
168
|
+
removed them. The release ended with whichever ran last and nothing reported a problem — the
|
|
169
|
+
build that lost still said "Build complete" on its way out, and the page it served then 404'd
|
|
170
|
+
its own script. The record is keyed by entry point now: a file another build claimed is not
|
|
171
|
+
this one's to remove, while chunks nobody claims are still swept.
|
|
172
|
+
|
|
173
|
+
- **`zt doctor` failed a schema configuration that works.** Sync on plus migrations present
|
|
174
|
+
read as "the schema needs exactly one source of truth", which misses the documented
|
|
175
|
+
arrangement where sync builds the schema from the models so a fresh clone runs without a
|
|
176
|
+
migration step, and `synchronize` is an expression that is false in production, where the
|
|
177
|
+
deploy runs `migrate`. The two never apply in the same environment. It fails in production
|
|
178
|
+
now, where the deploy really does run both, and warns elsewhere. A check that cries wolf
|
|
179
|
+
against a correct configuration is what stops `zt doctor` ever being trusted to gate a
|
|
180
|
+
deploy.
|
|
181
|
+
|
|
182
|
+
### Documented
|
|
183
|
+
|
|
184
|
+
- **Replace a release directory, do not merge into it.** Chasing 195 orphaned chunks on a
|
|
185
|
+
server showed the build was never the problem — ten releases of a code-split app into one
|
|
186
|
+
directory hold steady at the build's own output, and every one of the reporting app's 68
|
|
187
|
+
chunks is referenced. What accumulates is the _release_: the archive is extracted over the
|
|
188
|
+
running directory, so every file in it is written and every file not in it is left alone, and
|
|
189
|
+
nothing on that machine ever runs a build. They stay publicly fetchable at their
|
|
190
|
+
content-hashed URLs, which is how copy that was taken down went on being served.
|
|
191
|
+
[Deployment](/docs/deployment) now gives the two spellings that replace the directory.
|
|
192
|
+
|
|
193
|
+
## 1.7.5 — 2026-08-23
|
|
194
|
+
|
|
195
|
+
Two bugs that shipped to every deployed app, a package promoted to `stable`, and
|
|
196
|
+
the gates that would have caught both.
|
|
197
|
+
|
|
198
|
+
### Changed
|
|
199
|
+
|
|
200
|
+
- **`@zerotal/arch` is `stable`.** Reviewed ahead of its 1.9.0 date. The API follows
|
|
201
|
+
SemVer strictly from here, and that promise covers the **MCP tool contract** — tool
|
|
202
|
+
names, their arguments, and the shape of what they return. That is what an agent
|
|
203
|
+
client is configured against, and nothing type-level can see it: `archTools` has the
|
|
204
|
+
same signature however the tools are named. `mcp-surface.md` records all nine and CI
|
|
205
|
+
diffs it on every change. The protocol revision the server speaks is not covered; it
|
|
206
|
+
follows the protocol.
|
|
207
|
+
|
|
208
|
+
- **INTERNAL — the writers behind `arch:install` are no longer public API.**
|
|
209
|
+
`detectAgents`, `applyMcpConfig`, `applyBlock`, `buildGuidelines` and the rest are
|
|
210
|
+
`@internal`: still exported, still working, no longer promised. Their only caller is
|
|
211
|
+
the install command, and freezing them would have committed the shape of `.mcp.json`
|
|
212
|
+
writing to the rest of the 1.x line on behalf of a caller who never arrived.
|
|
213
|
+
|
|
214
|
+
- **INTERNAL — `api-surface.md` honours `@internal` across every package.** The
|
|
215
|
+
contract has always read "anything importable without an `@internal` marker keeps its
|
|
216
|
+
shape", and the generator did not read the tag — so symbols already marked internal
|
|
217
|
+
were recorded as though promised. 374 entries across 13 packages are omitted now,
|
|
218
|
+
every one verified marked. Nothing changes at runtime or in the types; the file
|
|
219
|
+
listing the promises now lists the promises.
|
|
220
|
+
|
|
221
|
+
- **A modal locks the page behind it.** `<Modal>` and `<Drawer>` trapped focus
|
|
222
|
+
correctly while the page underneath kept scrolling, which on a phone reads as the
|
|
223
|
+
dialog having broken the page.
|
|
224
|
+
|
|
225
|
+
- **Flow marks the active nav link for everyone.** `<Link navigate>` set
|
|
226
|
+
`data-current`, which styles a link, and nothing that announces it. It sets
|
|
227
|
+
`aria-current="page"` alongside now, so a screen reader can tell which of thirty nav
|
|
228
|
+
items is the current page.
|
|
229
|
+
|
|
230
|
+
### Fixed
|
|
231
|
+
|
|
232
|
+
- **Assets were cache-busted in development and not in production.** `asset()` appended
|
|
233
|
+
`?v=` only when a dev version was set, so every deployed Zerotal app served the
|
|
234
|
+
previous build's JavaScript and CSS to anyone with a warm cache — indefinitely, since
|
|
235
|
+
the URL never changed. The version is now derived from the built files themselves, so
|
|
236
|
+
it is stable across restarts and moves when the files do.
|
|
237
|
+
|
|
238
|
+
- **DevTools mounted on production pages.** The provider is gated on the environment, so
|
|
239
|
+
the endpoints are absent outside development — and the client took that to mean it
|
|
240
|
+
could start anyway, pinning a floating panel to the page whose tabs read
|
|
241
|
+
`Could not read the map — HTTP 404`. It now mounts only when the server half says it
|
|
242
|
+
is there, via a `<meta>` the middleware writes, and makes no request at all on a
|
|
243
|
+
public hostname.
|
|
244
|
+
|
|
245
|
+
- **Browser tests drove an unstyled site.** `Router.static("/", public)` is registered
|
|
246
|
+
only for the `web` environment, and a test app is not one — so every `FlowBrowser`
|
|
247
|
+
suite served pages without their stylesheet. Invisible to assertions that read text;
|
|
248
|
+
fatal for anything measuring layout.
|
|
249
|
+
|
|
250
|
+
### Documented
|
|
251
|
+
|
|
252
|
+
- **Every TypeScript example in the documentation is compiled against the real
|
|
253
|
+
packages**, on every pull request. 1,593 blocks. The gate found examples importing
|
|
254
|
+
symbols that do not exist (`currentUser`, `Layout` from the wrong package), calling
|
|
255
|
+
methods that were renamed (`Cache.put`), and configuring fields with `env()` where the
|
|
256
|
+
type is a literal union. Blocks deliberately written as fragments say so in their
|
|
257
|
+
fence and are recorded by key, so a new one is a deliberate act rather than a silent
|
|
258
|
+
exemption.
|
|
259
|
+
|
|
260
|
+
- **A break cannot ship without a release note.** `api:surface:check` demands a
|
|
261
|
+
regenerated snapshot when an export changes and then goes quiet, so the changelog was
|
|
262
|
+
defended by remembering — and 1.7.3 shipped the removal of Flow's `this.title(…)` with
|
|
263
|
+
no BREAKING entry. That entry is now in 1.7.3's notes, the support policy counts three
|
|
264
|
+
breaks rather than two, and `breaking:check` reads the snapshot diff so the next one
|
|
265
|
+
cannot pass silently.
|
|
266
|
+
|
|
267
|
+
- **A maturity label falls due.** The review release for a package below `stable` lives
|
|
268
|
+
in its `package.json` as `maturityReview`, and the package-conventions gate fails once
|
|
269
|
+
the version reaches it.
|
|
270
|
+
|
|
30
271
|
## 1.7.4 — 2026-08-21
|
|
31
272
|
|
|
32
273
|
A debug panel that was reaching production, a column type MySQL would not index, and
|
|
@@ -89,7 +330,33 @@ A debug panel that was reaching production, a column type MySQL would not index,
|
|
|
89
330
|
|
|
90
331
|
## 1.7.3 — 2026-08-20
|
|
91
332
|
|
|
92
|
-
Two fields that accepted input and threw it away,
|
|
333
|
+
Two fields that accepted input and threw it away, a CI job that was testing nothing, and a
|
|
334
|
+
name given back to applications.
|
|
335
|
+
|
|
336
|
+
### Changed
|
|
337
|
+
|
|
338
|
+
- **BREAKING — `this.title(…)` is removed from Flow components.** Declare `static title`
|
|
339
|
+
instead, as a string or a function of the component:
|
|
340
|
+
|
|
341
|
+
```ts fragment
|
|
342
|
+
// Before
|
|
343
|
+
override async mount(): Promise<void> {
|
|
344
|
+
this.title(`Search: ${this.query}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// After
|
|
348
|
+
static title = (c: SearchPage) => (c.query ? `Search: ${c.query}` : "Search");
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The instance method held a name four separate components wanted for their own data — a
|
|
352
|
+
media row, a guide, a review, an issue — for a one-line accessor that belongs on the class.
|
|
353
|
+
The static form is also the better one: it is resolved on the server for every render and
|
|
354
|
+
every patch, so a title that depends on state follows it without an action remembering to
|
|
355
|
+
update it.
|
|
356
|
+
|
|
357
|
+
A call to `this.title(…)` on a component that declares its own `title` field now sets that
|
|
358
|
+
field instead of the document title, which is silent. Search your components for
|
|
359
|
+
`this.title(` before upgrading; every hit is either a migration or was already shadowed.
|
|
93
360
|
|
|
94
361
|
### Fixed
|
|
95
362
|
|
package/docs/client/index.md
CHANGED
|
@@ -10,7 +10,7 @@ looking up an address. `@zerotal/client` is the outbound HTTP client — a thin,
|
|
|
10
10
|
expressive wrapper over `fetch` that adds what every real integration ends up
|
|
11
11
|
needing anyway and that is tedious to get right by hand.
|
|
12
12
|
|
|
13
|
-
```ts
|
|
13
|
+
```ts fragment
|
|
14
14
|
import { Client } from "@zerotal/client";
|
|
15
15
|
|
|
16
16
|
const charge = await Client.post("https://api.stripe.com/v1/charges", {
|
|
@@ -71,7 +71,7 @@ otherwise. A 2xx returns; anything else throws, which [Errors](#errors) covers.
|
|
|
71
71
|
|
|
72
72
|
### Query parameters
|
|
73
73
|
|
|
74
|
-
```ts
|
|
74
|
+
```ts fragment
|
|
75
75
|
await Client.get("https://api.example.com/v1/charges", undefined, {
|
|
76
76
|
query: { limit: 25, status: "succeeded", created: { gte: 1_700_000_000 } },
|
|
77
77
|
});
|
|
@@ -87,7 +87,7 @@ A plain object or array is JSON-encoded with the matching `Content-Type`. Anythi
|
|
|
87
87
|
a raw string — passes straight through, so the runtime sets the header itself
|
|
88
88
|
(including multipart boundaries):
|
|
89
89
|
|
|
90
|
-
```ts
|
|
90
|
+
```ts fragment
|
|
91
91
|
// Form-encoded, which several gateways still require
|
|
92
92
|
await Client.post(
|
|
93
93
|
"https://api.example.com/v1/charges",
|
|
@@ -99,7 +99,7 @@ await Client.post(
|
|
|
99
99
|
|
|
100
100
|
Per request, merged over the client's defaults:
|
|
101
101
|
|
|
102
|
-
```ts
|
|
102
|
+
```ts fragment
|
|
103
103
|
await Client.post("https://api.example.com/v1/messages", payload, {
|
|
104
104
|
headers: { "Idempotency-Key": crypto.randomUUID() },
|
|
105
105
|
});
|
|
@@ -125,7 +125,7 @@ export default ClientConfig({
|
|
|
125
125
|
A `token` may also be a function, including an async one, which is how credentials
|
|
126
126
|
that expire are handled — it is resolved per request:
|
|
127
127
|
|
|
128
|
-
```ts
|
|
128
|
+
```ts fragment
|
|
129
129
|
export default ClientConfig({
|
|
130
130
|
token: async () => await currentAccessToken(),
|
|
131
131
|
});
|
|
@@ -136,7 +136,7 @@ Change it at runtime with `setToken(token)`, or pass `null` to clear it. Its typ
|
|
|
136
136
|
|
|
137
137
|
For an API that wants something other than a bearer token, set the header directly:
|
|
138
138
|
|
|
139
|
-
```ts
|
|
139
|
+
```ts fragment
|
|
140
140
|
export default ClientConfig({
|
|
141
141
|
headers: { "X-Api-Key": env("PARTNER_API_KEY", "") },
|
|
142
142
|
});
|
|
@@ -147,7 +147,7 @@ export default ClientConfig({
|
|
|
147
147
|
`onUnauthorized` receives the error and a `retry` function, so a token can be
|
|
148
148
|
refreshed and the original request replayed once:
|
|
149
149
|
|
|
150
|
-
```ts
|
|
150
|
+
```ts fragment
|
|
151
151
|
export default ClientConfig({
|
|
152
152
|
onUnauthorized: async (error, retry) => retry({ Authorization: `Bearer ${await refresh()}` }),
|
|
153
153
|
});
|
|
@@ -158,7 +158,7 @@ export default ClientConfig({
|
|
|
158
158
|
There is no timeout by default, because the right one depends on the upstream. Set a
|
|
159
159
|
default and override per request:
|
|
160
160
|
|
|
161
|
-
```ts
|
|
161
|
+
```ts fragment
|
|
162
162
|
export default ClientConfig({ timeout: 10_000 });
|
|
163
163
|
|
|
164
164
|
// This one is slow and we accept that
|
|
@@ -174,7 +174,7 @@ A network blip or a `503` is worth trying again; a `422` never is. `retry` retri
|
|
|
174
174
|
idempotent requests on network errors, 5xx and 429 with exponential backoff, and
|
|
175
175
|
honours a `Retry-After` header when the server sends one:
|
|
176
176
|
|
|
177
|
-
```ts
|
|
177
|
+
```ts fragment
|
|
178
178
|
export default ClientConfig({ retry: 2 });
|
|
179
179
|
|
|
180
180
|
await Client.post("https://api.example.com/v1/charges", body, { retry: false });
|
|
@@ -189,7 +189,7 @@ statuses qualify.
|
|
|
189
189
|
|
|
190
190
|
A non-2xx throws `ApiClientError`, carrying what you need to decide what happened:
|
|
191
191
|
|
|
192
|
-
```ts
|
|
192
|
+
```ts fragment
|
|
193
193
|
import { ApiClientError } from "@zerotal/client";
|
|
194
194
|
|
|
195
195
|
try {
|
|
@@ -219,7 +219,7 @@ everything queued behind it. A `CircuitBreaker` stops after a threshold of
|
|
|
219
219
|
consecutive failures, fails fast for a cooldown, then lets a single request through
|
|
220
220
|
to test the water:
|
|
221
221
|
|
|
222
|
-
```ts
|
|
222
|
+
```ts fragment
|
|
223
223
|
export default ClientConfig({
|
|
224
224
|
circuitBreaker: { threshold: 5, cooldownMs: 30_000 },
|
|
225
225
|
});
|
|
@@ -237,7 +237,7 @@ to the same upstream and should trip together.
|
|
|
237
237
|
|
|
238
238
|
Upload with `FormData`; download by asking for the body you want:
|
|
239
239
|
|
|
240
|
-
```ts
|
|
240
|
+
```ts fragment
|
|
241
241
|
const form = new FormData();
|
|
242
242
|
form.append("file", Bun.file("./invoice.pdf"));
|
|
243
243
|
await Client.post("https://api.example.com/v1/documents", form);
|
|
@@ -255,7 +255,7 @@ const pdf = await Client.get("https://api.example.com/v1/documents/doc_1", undef
|
|
|
255
255
|
`onResponse` runs after every 2xx with the `ResponseContext`. Both take one function
|
|
256
256
|
or an array:
|
|
257
257
|
|
|
258
|
-
```ts
|
|
258
|
+
```ts fragment
|
|
259
259
|
export default ClientConfig({
|
|
260
260
|
onRequest: (config) => {
|
|
261
261
|
config.headers["X-Request-Id"] = crypto.randomUUID();
|
package/docs/commands.md
CHANGED
|
@@ -72,7 +72,7 @@ signature's first token is the name; `{arg}` is required, `{arg?}` optional,
|
|
|
72
72
|
`{arg=default}` has a default, `{--flag}` is a boolean flag, and `{--flag=}` /
|
|
73
73
|
`{--flag=default}` is a string flag.
|
|
74
74
|
|
|
75
|
-
```typescript
|
|
75
|
+
```typescript fragment
|
|
76
76
|
// in a service provider or bootstrap script
|
|
77
77
|
const runner = app.container.tryMake("commands");
|
|
78
78
|
|
|
@@ -110,7 +110,7 @@ so it wins a name collision. The directory is configurable via
|
|
|
110
110
|
|
|
111
111
|
To register a folder from somewhere else, call `discover()` yourself:
|
|
112
112
|
|
|
113
|
-
```typescript
|
|
113
|
+
```typescript fragment
|
|
114
114
|
// in a service provider or bootstrap script
|
|
115
115
|
await runner.discover("./vendor/acme/commands");
|
|
116
116
|
```
|
|
@@ -124,7 +124,7 @@ returns the list of registered names.
|
|
|
124
124
|
The `Command` base class provides coloured output helpers and interactive
|
|
125
125
|
prompts. The prompts read from stdin and only work on a real TTY:
|
|
126
126
|
|
|
127
|
-
```typescript
|
|
127
|
+
```typescript fragment
|
|
128
128
|
// inside a command's run()
|
|
129
129
|
this.info("Success"); // green
|
|
130
130
|
this.warn("Heads up"); // yellow
|
|
@@ -291,7 +291,7 @@ Dev processes
|
|
|
291
291
|
Your app has the last word. `app.dev.disable` removes a process by name, and
|
|
292
292
|
registering the same name again replaces it rather than adding a second tab:
|
|
293
293
|
|
|
294
|
-
```ts
|
|
294
|
+
```ts fragment
|
|
295
295
|
// config/app.ts
|
|
296
296
|
export default AppConfig({
|
|
297
297
|
dev: {
|
|
@@ -315,6 +315,38 @@ Packages register their own — see
|
|
|
315
315
|
| `bun zt doctor` | Check the app for silent misconfigurations |
|
|
316
316
|
| `bun zt key:generate` | Generate a new `APP_KEY` and write it to `.env` |
|
|
317
317
|
| `bun zt lint:packages` | Check every workspace package against convention rules |
|
|
318
|
+
| `bun zt upgrade` | Apply the codemods for a version upgrade |
|
|
319
|
+
|
|
320
|
+
### Upgrading between versions
|
|
321
|
+
|
|
322
|
+
`bun zt upgrade --to <version>` applies the codemods a version gap calls for —
|
|
323
|
+
mechanical rewrites the framework can make on your behalf when an API changes.
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
bun zt upgrade --to 2.0.0 # print the plan, change nothing
|
|
327
|
+
bun zt upgrade --to 2.0.0 --write # apply it
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**It writes nothing unless you ask.** That is the opposite of most tools and
|
|
331
|
+
deliberate: it rewrites source across your whole project, so the first run should
|
|
332
|
+
be something you can read and disagree with. `--from` defaults to the `zerotal`
|
|
333
|
+
version in your `package.json`.
|
|
334
|
+
|
|
335
|
+
The half worth reading is the last one. A codemod that quietly walks past
|
|
336
|
+
something it does not understand is worse than none, because the changes it _did_
|
|
337
|
+
make suggest the job is finished — so anything it recognised and deliberately did
|
|
338
|
+
not touch is listed with a file, a line and a reason:
|
|
339
|
+
|
|
340
|
+
```text
|
|
341
|
+
1 place(s) need a decision this cannot make for you:
|
|
342
|
+
app/models/Post.ts:9 export function all<T extends BaseModel>(rows: T[]): T[] {
|
|
343
|
+
`BaseModel` in a type position. It resolves to the same class as `Model`, so
|
|
344
|
+
this compiles either way — renaming it is a readability call, not a
|
|
345
|
+
correctness one.
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Run it again after applying: a second run should report no changes. Codemods are
|
|
349
|
+
idempotent, and that is the cheapest way to confirm one did what it said.
|
|
318
350
|
|
|
319
351
|
`doctor` runs every static sanity check against the booted app and prints each
|
|
320
352
|
finding with its fix: APP_KEY strength, `database.synchronize` colliding with
|