@zerotal/arch 1.8.1 → 1.10.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/api-surface.md +6 -6
- package/docs/admin/actions.md +15 -0
- package/docs/admin/auth.md +10 -0
- package/docs/admin/dashboard.md +12 -0
- package/docs/admin/extending-ui.md +14 -0
- package/docs/admin/forms.md +15 -0
- package/docs/admin/operations.md +12 -0
- package/docs/admin/resources.md +6 -0
- package/docs/admin/tables.md +21 -0
- package/docs/audit.md +5 -0
- package/docs/authentication.md +110 -1
- package/docs/broadcasting/references.md +17 -0
- package/docs/cache.md +5 -0
- package/docs/carbon.md +5 -0
- package/docs/changelog.md +281 -0
- package/docs/client/index.md +17 -0
- package/docs/commands.md +6 -0
- package/docs/components.md +73 -0
- package/docs/config-system.md +54 -0
- package/docs/cookies.md +6 -0
- package/docs/deployment.md +151 -13
- package/docs/devtools.md +5 -0
- package/docs/email-verification.md +26 -1
- package/docs/encryption.md +21 -0
- package/docs/errors.md +2 -0
- package/docs/flow/components.md +54 -0
- package/docs/flow/forms.md +57 -0
- package/docs/flow/references.md +14 -0
- package/docs/getting-started.md +38 -0
- package/docs/health.md +19 -0
- package/docs/helpers.md +150 -0
- package/docs/i18n.md +5 -0
- package/docs/inertia/middleware.md +44 -0
- package/docs/inertia/props.md +70 -0
- package/docs/inertia/ssr.md +95 -10
- package/docs/lock.md +15 -0
- package/docs/logger.md +38 -0
- package/docs/middleware.md +31 -0
- package/docs/migrations.md +47 -0
- package/docs/monitor.md +59 -0
- package/docs/notifications.md +11 -0
- package/docs/orm/casts.md +6 -0
- package/docs/orm/lifecycle.md +18 -0
- package/docs/orm/queries.md +10 -0
- package/docs/orm/relationships.md +30 -0
- package/docs/queue.md +10 -0
- package/docs/rate-limiting.md +84 -21
- package/docs/responses.md +23 -0
- package/docs/routing.md +16 -0
- package/docs/scheduler.md +82 -8
- package/docs/session.md +6 -0
- package/docs/social.md +10 -0
- package/docs/storage.md +21 -0
- package/docs/support-policy.md +13 -1
- package/docs/telemetry.md +8 -0
- package/docs/tenancy.md +6 -0
- package/docs/testing/index.md +105 -0
- package/docs/upgrade.md +48 -0
- package/docs/validator.md +9 -0
- package/docs/view.md +6 -0
- package/package.json +3 -3
- package/src/install/guidelines.ts +1 -1
- package/src/mcp/stdio.ts +3 -3
- package/src/tools/_probe.ts +2 -2
package/docs/deployment.md
CHANGED
|
@@ -38,6 +38,7 @@ deliberate: systemd, your container runtime or your deploy script owns process
|
|
|
38
38
|
lifecycle, and this gives it a gate to restart behind.
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
+
bun zt deploy:production --check # run the gate only — see below
|
|
41
42
|
bun zt deploy:production --dry-run # print the plan, run none of it
|
|
42
43
|
bun zt deploy:production --skip-migrations # release without touching the schema
|
|
43
44
|
bun zt deploy:production --probe=https://example.com # real handshake at the end
|
|
@@ -63,18 +64,46 @@ the wrong database.
|
|
|
63
64
|
|
|
64
65
|
Each entry is a `DeployTarget`:
|
|
65
66
|
|
|
66
|
-
| Field
|
|
67
|
-
|
|
|
68
|
-
| `url`
|
|
69
|
-
| `steps`
|
|
67
|
+
| Field | Meaning |
|
|
68
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `url` | The public URL in this environment. What `--probe` handshakes against when given no URL of its own. |
|
|
70
|
+
| `steps` | Override the release steps. Defaults to `DEFAULT_DEPLOY_STEPS` — `assets:build`, `inertia:build`, `migrate`. Each names a `zt` command, and one that is not registered is skipped, so an app without Inertia simply has no Inertia step. |
|
|
71
|
+
| `preflight` | Your own commands, run in the preflight phase — after the config validators and `doctor`, before anything is built or migrated. A non-zero exit refuses the release. Defaults to `release:check` when the app registers a command by that name. |
|
|
70
72
|
|
|
71
73
|
Omit the file entirely and you get `DEFAULT_DEPLOY_TARGETS`: `production` and
|
|
72
74
|
`staging`, both with the default steps.
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
### Your own release gate
|
|
77
|
+
|
|
78
|
+
The framework's preflight knows the things a framework can know: that this really is the
|
|
79
|
+
environment you think it is, that the config validators pass, that `doctor` is happy —
|
|
80
|
+
which now includes refusing a production release whose `mail.driver` is still `log`,
|
|
81
|
+
because mail written to a log file is delivered to nobody and says so nowhere. It
|
|
82
|
+
cannot know that this workspace has no cancellation policy, or that the owner account
|
|
83
|
+
is still on the password `admin:create` issued it. Those refusals are yours.
|
|
84
|
+
|
|
85
|
+
Write them as a command and name it `release:check` (exported as
|
|
86
|
+
`CONVENTIONAL_PREFLIGHT_COMMAND`). The pipeline finds it by name — nothing to wire up:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// app/commands/ReleaseCheckCommand.ts
|
|
90
|
+
import { Command } from "zerotal";
|
|
91
|
+
|
|
92
|
+
export class ReleaseCheckCommand extends Command {
|
|
93
|
+
static commandName = "release:check";
|
|
94
|
+
static description = "Refuse a release this app is not ready for";
|
|
95
|
+
static needsApp = true;
|
|
96
|
+
|
|
97
|
+
async run(): Promise<void> {
|
|
98
|
+
const problems: string[] = [];
|
|
99
|
+
if (Bun.env["APP_KEY"] === "base64:CHANGE_ME") problems.push("APP_KEY is the example one.");
|
|
100
|
+
if (problems.length > 0) throw new Error(problems.join(" "));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A throw, or any non-zero exit, stops the release before `assets:build` has run. To run
|
|
106
|
+
something else — or more than one thing — name them:
|
|
78
107
|
|
|
79
108
|
```ts
|
|
80
109
|
// config/deploy.ts
|
|
@@ -82,19 +111,42 @@ export default {
|
|
|
82
111
|
targets: {
|
|
83
112
|
production: {
|
|
84
113
|
url: "https://example.com",
|
|
85
|
-
|
|
114
|
+
preflight: ["release:check", "smoke:mail"],
|
|
86
115
|
},
|
|
87
116
|
},
|
|
88
117
|
};
|
|
89
118
|
```
|
|
90
119
|
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
A name in `preflight` that is not a registered command **fails the deploy** rather than being
|
|
121
|
+
skipped. That is the opposite of how `steps` treats an absent command, and deliberately so: a
|
|
122
|
+
missing `inertia:build` means the app has no Inertia, while a missing gate means the gate is
|
|
123
|
+
not running — which is the state this exists to prevent. A gate nothing calls is a comment.
|
|
93
124
|
|
|
94
125
|
Two things worth adding while you are there: `assets:build` and `inertia:build` accept
|
|
95
126
|
`--clean`, which removes anything in the output directory the build did not write — see
|
|
96
127
|
[Build assets](#build-assets).
|
|
97
128
|
|
|
129
|
+
### `--check`: the gate on its own
|
|
130
|
+
|
|
131
|
+
A release script has a moment where the new code is on disk and the service has not
|
|
132
|
+
restarted yet. That is the moment to ask whether this release is fit to go live, and
|
|
133
|
+
`--check` is the whole preflight and nothing else:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# on the box, after the new release is unpacked and before the restart
|
|
137
|
+
APP_ENV=production bun zt deploy:production --check || exit 1
|
|
138
|
+
systemctl restart app
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
It runs the environment check, the config validators with production semantics,
|
|
142
|
+
`doctor`, and your own `release:check` — everything that can refuse — and builds
|
|
143
|
+
nothing, migrates nothing, restarts nothing. Exit 0 and restart; exit non-zero and
|
|
144
|
+
keep serving the previous release, which is the point. A workspace that has lost its
|
|
145
|
+
banking details, or had its mail driver knocked back to `log`, never goes live broken.
|
|
146
|
+
|
|
147
|
+
`--check` and `--dry-run` answer different questions. `--dry-run` prints the plan
|
|
148
|
+
without running any of it, including the gate. `--check` runs the gate for real.
|
|
149
|
+
|
|
98
150
|
> **Note** — `deploy:<env>` runs **where the app runs**, with that environment's
|
|
99
151
|
> variables. It does not reach another machine over SSH. Run it on the box, or in
|
|
100
152
|
> the container build, as the step before the restart.
|
|
@@ -180,6 +232,54 @@ bun zt migrate
|
|
|
180
232
|
Auto-`synchronize` is **hard-off in production** — generate and commit
|
|
181
233
|
[migrations](/docs/migrations) during development and run them on deploy.
|
|
182
234
|
|
|
235
|
+
## Back up the database
|
|
236
|
+
|
|
237
|
+
On SQLite the database is one file, which makes `cp` look like a backup. It is not one. A
|
|
238
|
+
live SQLite database has pages in flight; copying the file while the server is serving can
|
|
239
|
+
capture a half-written page, and the result is a file that sits in your retention directory
|
|
240
|
+
for months and turns out to be corrupt on the one morning you open it.
|
|
241
|
+
|
|
242
|
+
`zt db:backup` uses SQLite's own `VACUUM INTO`, which takes a read lock and writes a
|
|
243
|
+
complete database while the server keeps serving. It needs no `sqlite3` binary on the box:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# in your project root
|
|
247
|
+
bun zt db:backup --dir=/var/backups/app --keep=30 --require-rows=bookings,invoices
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
| Flag | What it does |
|
|
251
|
+
| ---------------- | ---------------------------------------------------------------------------- |
|
|
252
|
+
| `--dir` | Where snapshots go. Default `storage/backups`. |
|
|
253
|
+
| `--keep` | How many to keep, newest first. `0` keeps every one. Default `14`. |
|
|
254
|
+
| `--require-rows` | Tables that must not be empty in the snapshot. **Set this.** |
|
|
255
|
+
| `--rehearse` | Also perform the restore — copy the snapshot, open the copy, check it there. |
|
|
256
|
+
|
|
257
|
+
Every snapshot is opened and integrity-checked the moment it is written, and **every failure
|
|
258
|
+
path exits non-zero**. That is what makes it safe to run from a timer: a bad night leaves a
|
|
259
|
+
failed unit somebody can see, rather than a green one and no file.
|
|
260
|
+
|
|
261
|
+
`--require-rows` is the flag that turns "a file was written" into "the file has the business
|
|
262
|
+
in it". An empty `bookings` table in a snapshot of a live system is not a small discrepancy,
|
|
263
|
+
and it is invisible in a byte count.
|
|
264
|
+
|
|
265
|
+
Run `--rehearse` on a schedule of its own — weekly is plenty. A backup nobody has restored is
|
|
266
|
+
a hope, and the restore is the operation you will be doing at 3am.
|
|
267
|
+
|
|
268
|
+
```ini
|
|
269
|
+
# /etc/systemd/system/app-backup.service
|
|
270
|
+
[Service]
|
|
271
|
+
Type=oneshot
|
|
272
|
+
WorkingDirectory=/srv/app
|
|
273
|
+
Environment=APP_ENV=production
|
|
274
|
+
ExecStart=/srv/app/node_modules/.bin/bun zt db:backup --keep=30 --require-rows=bookings
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Pair it with a `.timer`, and let the failed unit be your alert. Retention is handled by
|
|
278
|
+
`--keep`; anything the command did not write is never touched.
|
|
279
|
+
|
|
280
|
+
On PostgreSQL or MySQL the command refuses and points at `pg_dump` / `mysqldump`, which is
|
|
281
|
+
where that job belongs.
|
|
282
|
+
|
|
183
283
|
## Build assets
|
|
184
284
|
|
|
185
285
|
Build the frontend bundle as a release step, so `public/` holds compiled output before
|
|
@@ -233,6 +333,31 @@ Ordering matters if the old release is still serving traffic: clearing the direc
|
|
|
233
333
|
its bundles away, so do it as close to the swap as you can, or stage the release in a new
|
|
234
334
|
directory and move it into place.
|
|
235
335
|
|
|
336
|
+
### `assets:prune` — clearing up after the extract instead of before it
|
|
337
|
+
|
|
338
|
+
Clearing the directory first has a cost the ordering note above hints at: between the
|
|
339
|
+
`rm -rf` and the new files landing, the release still serving traffic has no bundles.
|
|
340
|
+
If you would rather extract over the top and tidy afterwards, `assets:build` records
|
|
341
|
+
what it wrote and `assets:prune` removes what that record does not claim:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
# on the build machine — the record is written under .zerotal/
|
|
345
|
+
bun zt assets:build
|
|
346
|
+
tar -czf release.tgz public .zerotal # ship the record with the release
|
|
347
|
+
|
|
348
|
+
# on the server, after extracting and before the restart
|
|
349
|
+
bun zt assets:prune --dry-run # list what would go
|
|
350
|
+
bun zt assets:prune
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Ship `.zerotal/` with the release** — that is the part that makes it work. Without
|
|
354
|
+
the record, "which files belong to this release" has no answer on a server that never
|
|
355
|
+
ran a build, and `assets:prune` says so and removes nothing rather than guessing.
|
|
356
|
+
|
|
357
|
+
It is conservative by design: it deletes a file only when the record does not claim it
|
|
358
|
+
_and_ it is named the way the bundler names a code-split chunk. Your images and
|
|
359
|
+
favicon are never candidates, because an output directory is usually `public/`.
|
|
360
|
+
|
|
236
361
|
### `--clean` for a directory the build does not own outright
|
|
237
362
|
|
|
238
363
|
The cleanup above recognises the filenames `Bun.build()` produces. An app that sets its own
|
|
@@ -350,6 +475,11 @@ The symptom is a legitimate visitor getting a 429 they did not earn.
|
|
|
350
475
|
Count the proxies you actually run. Setting `trustedProxies: 3` with one proxy in front
|
|
351
476
|
reads an entry the client supplied.
|
|
352
477
|
|
|
478
|
+
`zt doctor` warns about this: a production-like deployment with a registered throttle and no
|
|
479
|
+
`trustedProxies` is reported as almost certainly wrong. It is a warning rather than a failure
|
|
480
|
+
because the framework cannot see your deployment — an app served directly, with nothing in
|
|
481
|
+
front of it, is correctly configured exactly as it stands.
|
|
482
|
+
|
|
353
483
|
### Never gate the transport path
|
|
354
484
|
|
|
355
485
|
**Browsers do not attach basic-auth credentials to a WebSocket handshake.** An HTTP auth
|
|
@@ -513,8 +643,9 @@ Run the worker as a **second** container/service from the same image with the co
|
|
|
513
643
|
`bun zt worker`.
|
|
514
644
|
|
|
515
645
|
On a server with no Node installed, `bun install` can fail on a transitive package whose
|
|
516
|
-
`postinstall` shells out to `node`.
|
|
517
|
-
|
|
646
|
+
`postinstall` shells out to `node`. The `bun` npm package is the usual one — its
|
|
647
|
+
`postinstall` downloads a Bun binary the box already has, and there is no `node` to run the
|
|
648
|
+
script with. `--ignore-scripts` resolves it, but check what you are skipping first:
|
|
518
649
|
|
|
519
650
|
```bash
|
|
520
651
|
# every package with an install script, before you skip them all
|
|
@@ -523,6 +654,13 @@ for p in node_modules/*/package.json node_modules/@*/*/package.json; do
|
|
|
523
654
|
done
|
|
524
655
|
```
|
|
525
656
|
|
|
657
|
+
### `startZerotal` options
|
|
658
|
+
|
|
659
|
+
`StartZerotalOptions` is what `zt.ts` may pass — currently `configDir`, for an app whose config
|
|
660
|
+
does not live at `./config`. `isDevSurfaceAllowed(env)` is the check every dev-only surface
|
|
661
|
+
gates on, exported so an app's own dev tooling can gate the same way; it **fails closed**, so an
|
|
662
|
+
unset `APP_ENV` does not qualify.
|
|
663
|
+
|
|
526
664
|
## Next steps
|
|
527
665
|
|
|
528
666
|
- [Configuration](/docs/config-system) — environment variables and config files.
|
package/docs/devtools.md
CHANGED
|
@@ -1004,6 +1004,11 @@ meaningfully different from `{}` — a node can be both a branch and a leaf.
|
|
|
1004
1004
|
unfiltered list plus whether it heads a group (`groupKey`, `groupSize`) or is a
|
|
1005
1005
|
folded follow-up (`child`).
|
|
1006
1006
|
|
|
1007
|
+
## Types
|
|
1008
|
+
|
|
1009
|
+
`TraceStoreOptions` sets how many traces are kept and for how long.
|
|
1010
|
+
`DevtoolsClientOptions` configures the browser side.
|
|
1011
|
+
|
|
1007
1012
|
## Next steps
|
|
1008
1013
|
|
|
1009
1014
|
- [Logger](/docs/logger) — structured logging that surfaces in the Logs tab.
|
|
@@ -49,12 +49,29 @@ register ──▶ Url.sign(/auth/verify?id&email, ttl) ──▶ email link
|
|
|
49
49
|
|
|
50
50
|
## Migration
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Whether you need one **depends on what your schema's source of truth is** — `zt doctor`
|
|
53
|
+
reports which mode this app is in.
|
|
54
|
+
|
|
55
|
+
- **Models are the source of truth** (`synchronize` on). You do not need a migration. The
|
|
56
|
+
mixin registers `email_verified_at` imperatively and the table is built from what the
|
|
57
|
+
models declare, so the column comes with it.
|
|
58
|
+
- **Migrations are the source of truth** (`synchronize` off — the production default). You
|
|
59
|
+
do. The boot-time concern only _adds_ a column to a table that already exists; it never
|
|
60
|
+
revisits one it has seen. A `create users` migration that does not mention
|
|
61
|
+
`email_verified_at` produces a table without it, and then every query touching the column
|
|
62
|
+
fails with `no such column: email_verified_at` — all at once, in tests that have nothing to
|
|
63
|
+
do with email, with nothing pointing at the mixin.
|
|
64
|
+
|
|
65
|
+
`zt doctor` fails on that combination and names the table, so you find it before the suite
|
|
66
|
+
does.
|
|
67
|
+
|
|
68
|
+
Write the migration **guarded**:
|
|
53
69
|
|
|
54
70
|
```typescript fragment
|
|
55
71
|
// database/migrations/002_add_email_verified_at.ts
|
|
56
72
|
export default class AddEmailVerifiedAt extends Migration {
|
|
57
73
|
async up(schema: Schema) {
|
|
74
|
+
if (await schema.hasColumn("users", "email_verified_at")) return;
|
|
58
75
|
await schema.table("users", (table) => {
|
|
59
76
|
table.timestamp("email_verified_at").nullable();
|
|
60
77
|
});
|
|
@@ -67,6 +84,14 @@ export default class AddEmailVerifiedAt extends Migration {
|
|
|
67
84
|
}
|
|
68
85
|
```
|
|
69
86
|
|
|
87
|
+
The guard is not defensive habit. Any database that has booted the app since the mixin was
|
|
88
|
+
composed already **has** the column — the concern added it. An unguarded migration then fails
|
|
89
|
+
with `duplicate column name`, during the `migrate` step of `deploy:production`, which is the
|
|
90
|
+
worst possible moment to learn this.
|
|
91
|
+
|
|
92
|
+
`remember_token` from `Authenticatable` is provisioned the same way and carries the same
|
|
93
|
+
condition.
|
|
94
|
+
|
|
70
95
|
## User model
|
|
71
96
|
|
|
72
97
|
Expose the column and a convenience getter the middleware and controller can read:
|
package/docs/encryption.md
CHANGED
|
@@ -373,6 +373,27 @@ it works only after the app has booted. In a unit test that never boots, constru
|
|
|
373
373
|
| `Url.sign(base, params?, min?, secret?)` | `(base: string, params?, expiresInMinutes?, secret?) => string` | Build an HMAC-signed, time-limited URL. |
|
|
374
374
|
| `Url.verify(url, secret?)` | `(signedUrl: string, secret?: string) => boolean` | Verify a signed URL; `false` if tampered or expired. |
|
|
375
375
|
|
|
376
|
+
## Hashing helpers
|
|
377
|
+
|
|
378
|
+
Three pure helpers sit beside `Crypt`, for the cases that are not encryption:
|
|
379
|
+
|
|
380
|
+
| Helper | Use |
|
|
381
|
+
| --------------------- | ---------------------------------------------------------------------- |
|
|
382
|
+
| `sha256Hex(value)` | A hex digest. For a lookup key or a blind index, never for a password. |
|
|
383
|
+
| `hmacHex(value, key)` | A keyed digest — a webhook signature, a tamper-evident token. |
|
|
384
|
+
| `safeEqual(a, b)` | Constant-time comparison. Use it for every secret comparison. |
|
|
385
|
+
|
|
386
|
+
**`safeEqual` is the one that matters.** `a === b` on a token returns as soon as two bytes
|
|
387
|
+
differ, and the time it took is a measurement of how much of the prefix was right — enough, over
|
|
388
|
+
many attempts, to recover a secret a character at a time. Comparing anything an attacker
|
|
389
|
+
supplies against anything you hold goes through `safeEqual`.
|
|
390
|
+
|
|
391
|
+
### Signed URLs
|
|
392
|
+
|
|
393
|
+
`URLSigner` is what `signedUrl()` and `ValidateSignatureMiddleware` are built on. Reach for it
|
|
394
|
+
directly when you need to sign or verify outside a request — a link minted by a scheduled job,
|
|
395
|
+
or a signature checked by a worker.
|
|
396
|
+
|
|
376
397
|
## Next steps
|
|
377
398
|
|
|
378
399
|
- [Authentication](/docs/authentication) — where hashed passwords are verified at login.
|
package/docs/errors.md
CHANGED
|
@@ -273,6 +273,8 @@ So a package that owns an error class can contribute a **diagnosis**, rendered a
|
|
|
273
273
|
|
|
274
274
|
It works on SQLite, PostgreSQL and MySQL — matched on the driver's error code where there is one (`42P01`, `42703`, `1146`, `1054`) and on the message otherwise.
|
|
275
275
|
|
|
276
|
+
`bun zt doctor` asks the same question before anything breaks, and warns when migrations are pending — see [Migrations](/docs/migrations#running-migrations). The overlay is reactive by nature: it needs a request to have already failed, which means somebody has already lost the thread of what they were doing.
|
|
277
|
+
|
|
276
278
|
> **The button exists only in development.** The endpoint behind it refuses unless [`devSurfacesEnabled()`](/docs/deployment) is true, and that gate **fails closed**: an unset `APP_ENV` does not qualify. The route is not even registered otherwise.
|
|
277
279
|
>
|
|
278
280
|
> It also requires a single-use token minted into the page, and passes the same origin check the WebSocket endpoints use. A dev server on `localhost:3000` is reachable by any site you have open in another tab, and "run every pending migration" is not something a random page should be able to trigger.
|
package/docs/flow/components.md
CHANGED
|
@@ -859,6 +859,60 @@ Content-Security-Policy:
|
|
|
859
859
|
}
|
|
860
860
|
```
|
|
861
861
|
|
|
862
|
+
## Types
|
|
863
|
+
|
|
864
|
+
Every built-in component exports the type of its own props, named after it. Reach for one when
|
|
865
|
+
you wrap a component rather than use it directly — the usual reason an app needs the type at all:
|
|
866
|
+
|
|
867
|
+
```tsx
|
|
868
|
+
import { Modal, type ModalProps } from "@zerotal/flow";
|
|
869
|
+
|
|
870
|
+
/** Our confirm dialog: the same API, one decision already made. */
|
|
871
|
+
export function ConfirmModal(props: Omit<ModalProps, "closeable">) {
|
|
872
|
+
return <Modal closeable={false} {...props} />;
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
```text
|
|
877
|
+
DescriptionProps DisclosureProps DrawerProps DropdownProps
|
|
878
|
+
ErrorBoundaryProps ErrorProps ErrorsProps
|
|
879
|
+
FieldsetProps FileUploadProps FlashProps ForProps
|
|
880
|
+
InfiniteScrollProps LegendProps LinkProps
|
|
881
|
+
ListboxProps LoadingProps ModalProps PagerProps
|
|
882
|
+
SectionContentProps SectionOutletProps
|
|
883
|
+
VirtualizeProps
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
`ListboxOption` is one entry in a `<Listbox>`, and `UploadRef` is what `<FileUpload>` hands back
|
|
887
|
+
for a file the server has accepted.
|
|
888
|
+
|
|
889
|
+
### The flash API
|
|
890
|
+
|
|
891
|
+
`this.flash(…)` is the short form. The builder underneath it is exported, and so are the shapes
|
|
892
|
+
it produces — worth having when a helper composes a message rather than writing one inline:
|
|
893
|
+
|
|
894
|
+
| Type | What it is |
|
|
895
|
+
| -------------------- | ------------------------------------------------------------------------ |
|
|
896
|
+
| `FlashLevel` | `"info" \| "success" \| "warning" \| "error"` — what colours the toast. |
|
|
897
|
+
| `FlashMessage` | One flash: its level, text, and any actions. |
|
|
898
|
+
| `FlashOptions` | Per-message overrides — duration, position, whether it can be dismissed. |
|
|
899
|
+
| `FlashBuilder` | The fluent form, for a message assembled in more than one step. |
|
|
900
|
+
| `FlashPosition` | Where the container puts it, matching `<Flash position>`. |
|
|
901
|
+
| `FlashAction` | A button on the toast: its label and what it does. |
|
|
902
|
+
| `FlashActionStyle` | How that button is drawn. |
|
|
903
|
+
| `FlashActionVariant` | Its emphasis. |
|
|
904
|
+
| `FlashCallback` | What runs when the action is pressed. |
|
|
905
|
+
| `RedirectFlash` | A flash that survives a redirect, so it appears on the page you land on. |
|
|
906
|
+
|
|
907
|
+
`ErrorField` and `ValidationRules` are the validation side of the same story: what one field's
|
|
908
|
+
errors look like, and the rules a component declares.
|
|
909
|
+
|
|
910
|
+
### Elsewhere
|
|
911
|
+
|
|
912
|
+
`DurableOption` configures `@durable` state, `SessionOptions` the session a component reads, and
|
|
913
|
+
`UrlOptions` how `@url` state is written into the query string. `EventName` is the union of
|
|
914
|
+
events a component can listen for.
|
|
915
|
+
|
|
862
916
|
## Next steps
|
|
863
917
|
|
|
864
918
|
- [Flow overview](/docs/flow) — the guide's front page and the rest of the sections.
|
package/docs/flow/forms.md
CHANGED
|
@@ -545,6 +545,63 @@ export class AvatarUploader extends Component {
|
|
|
545
545
|
dedicated `temp` disk are natural follow-ups; the `TemporaryUploadedFile` API is designed to
|
|
546
546
|
absorb them without changing component code.
|
|
547
547
|
|
|
548
|
+
## Enhancing a plain form — no component
|
|
549
|
+
|
|
550
|
+
Everything above needs a Flow component. This does not.
|
|
551
|
+
|
|
552
|
+
A page that is just server-rendered HTML — no `Router.flow`, no `Component` — can still have a
|
|
553
|
+
form that submits without the page flashing. Add `data-enhance`:
|
|
554
|
+
|
|
555
|
+
```html
|
|
556
|
+
<form method="post" action="/subscribe" data-enhance>
|
|
557
|
+
<p class="error">{{ error }}</p>
|
|
558
|
+
<input name="email" />
|
|
559
|
+
<button type="submit">Subscribe</button>
|
|
560
|
+
</form>
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
The form posts through `fetch`, and **the matching form in the response replaces this one in
|
|
564
|
+
place**. A validation error re-renders the form with the message in it, and that is what lands
|
|
565
|
+
on the page — the rest of the document is untouched, so nothing scrolls, nothing flashes, and
|
|
566
|
+
what the person typed is still there.
|
|
567
|
+
|
|
568
|
+
### Include the script
|
|
569
|
+
|
|
570
|
+
The enhancement is a separate, dependency-free bundle. Flow pages get
|
|
571
|
+
`/__flow/runtime.js`; a plain page gets nothing, which is the whole reason this exists. Put the
|
|
572
|
+
tag in the layout that renders your non-Flow pages:
|
|
573
|
+
|
|
574
|
+
```tsx
|
|
575
|
+
import { flowEnhanceTag } from "@zerotal/flow";
|
|
576
|
+
|
|
577
|
+
// in your layout's <head>
|
|
578
|
+
flowEnhanceTag(); // <script src="/__flow/enhance.js" defer></script>
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Nothing fails if you forget it — the forms simply post the way they always did. That is the
|
|
582
|
+
design, and it is also why the tag is easy to leave out and never notice.
|
|
583
|
+
|
|
584
|
+
### What it does
|
|
585
|
+
|
|
586
|
+
| Situation | What happens |
|
|
587
|
+
| ---------------- | ------------------------------------------------------------------------------- |
|
|
588
|
+
| Normal submit | The response's matching form replaces this one; focus and caret are restored. |
|
|
589
|
+
| Validation error | Same — the server re-rendered the form, so the error is in the markup. |
|
|
590
|
+
| Redirect | Followed, the document swapped, and `pushState`d so the address bar agrees. |
|
|
591
|
+
| Network failure | Falls back to a native submit, so nothing typed is lost. |
|
|
592
|
+
| No JavaScript | An ordinary form post. `data-enhance` is additive; there is no fallback to rot. |
|
|
593
|
+
|
|
594
|
+
`data-enhance-target="#selector"` replaces something else instead of the form — a results
|
|
595
|
+
panel, a list. `data-enhance="false"` opts a form out. While a submission is in flight the form
|
|
596
|
+
carries `data-enhance-busy`, which is a styling hook and a re-entry guard.
|
|
597
|
+
|
|
598
|
+
A `flow:enhanced` event fires on `window` after each swap, with
|
|
599
|
+
`detail.navigated` saying whether it followed a redirect.
|
|
600
|
+
|
|
601
|
+
> **Note** — This is enhancement, not a component. There is no state, no socket and no server
|
|
602
|
+
> round-trip beyond the form's own post. When the page needs live state, reach for a
|
|
603
|
+
> [component](/docs/flow/components).
|
|
604
|
+
|
|
548
605
|
## Next steps
|
|
549
606
|
|
|
550
607
|
- [Flow overview](/docs/flow) — the guide's front page and the rest of the sections.
|
package/docs/flow/references.md
CHANGED
|
@@ -216,6 +216,20 @@ override async render() {
|
|
|
216
216
|
| `x-show="$flow.open && $flow.count > 0"` | Raw Alpine expression |
|
|
217
217
|
| `flow:click="increment"` | Hand-written Flow directive (accepted but not needed in JSX) |
|
|
218
218
|
|
|
219
|
+
## Plain-form enhancement
|
|
220
|
+
|
|
221
|
+
For forms on pages with no Flow component. See
|
|
222
|
+
[Forms](/docs/flow/forms#enhancing-a-plain-form--no-component).
|
|
223
|
+
|
|
224
|
+
| Attribute / export | Meaning |
|
|
225
|
+
| --------------------- | -------------------------------------------------------------------------------------- |
|
|
226
|
+
| `data-enhance` | Submit through `fetch` and patch the response in. `"false"` opts out. |
|
|
227
|
+
| `data-enhance-target` | CSS selector for what to replace, instead of the form itself. |
|
|
228
|
+
| `data-enhance-busy` | Set on the form while a submission is in flight. Styling hook and re-entry guard. |
|
|
229
|
+
| `flowEnhanceTag()` | The `<script>` tag to put in a non-Flow layout. |
|
|
230
|
+
| `FLOW_ENHANCE_PATH` | `/__flow/enhance.js` — the path the bundle is served at. |
|
|
231
|
+
| `flow:enhanced` | Window event after each swap; `detail.navigated` says whether a redirect was followed. |
|
|
232
|
+
|
|
219
233
|
## Client expressions
|
|
220
234
|
|
|
221
235
|
Inside a client expression — `onClick={() => this.X(...)}` — `this.` resolves to the live client runtime (no server round-trip to start it). You write the **same names as on the server** — no `$`-prefixed syntax, and it all type-checks.
|
package/docs/getting-started.md
CHANGED
|
@@ -16,6 +16,44 @@ provider.
|
|
|
16
16
|
is configured for **SQLite**, which needs nothing installed — PostgreSQL and
|
|
17
17
|
MySQL are supported and are a `DATABASE_URL` away.
|
|
18
18
|
|
|
19
|
+
### One project, one Bun
|
|
20
|
+
|
|
21
|
+
`engines.bun` is a floor, not a lock, and nothing in npm enforces it. A project can end up
|
|
22
|
+
with two runtimes in it without anyone choosing that: the shell's `bun`, and a different one
|
|
23
|
+
in `node_modules/bun`, put there by a transitive peer dependency nobody declared. The work
|
|
24
|
+
then splits between them — the server served by one, the suite run by the other — and nothing
|
|
25
|
+
says so.
|
|
26
|
+
|
|
27
|
+
That is expensive because the difference is real but narrow. The SQLite bindings, `node:`
|
|
28
|
+
compatibility and the test runner itself all change between releases, so a couple of
|
|
29
|
+
assertions happen to be runtime-sensitive and the rest are not. When those two fail you go
|
|
30
|
+
looking for a bug in the code they touch. And a suite that passes is not evidence either: it
|
|
31
|
+
only means no test happened to stand on a difference.
|
|
32
|
+
|
|
33
|
+
So **`zt` refuses to run when the two disagree**:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Two Bun runtimes are in play. This process is Bun 1.3.14, but the project
|
|
37
|
+
installs Bun 1.4.0 (/srv/app/node_modules/bun/package.json).
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is not a pin. The version to agree on is whichever one the project installed, so
|
|
41
|
+
`bun update bun` moves it and nothing needs editing — what is enforced is that there is only
|
|
42
|
+
one. Fix it either way:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun update bun # move the installed one to match your shell
|
|
46
|
+
node_modules/.bin/bun # or run everything through the installed one
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Most projects never see this, because most have no `bun` in `node_modules` to disagree with.
|
|
50
|
+
To boot anyway — mid-upgrade, say — set `ZT_ALLOW_RUNTIME_MISMATCH=1`, which downgrades the
|
|
51
|
+
refusal to a warning. The refusal is a `RuntimeMismatchError`, which carries both versions in
|
|
52
|
+
its `context` for anything scripting around it.
|
|
53
|
+
|
|
54
|
+
`zt test` spawns the binary running it, not whatever `bun` resolves to on `PATH`, so the
|
|
55
|
+
suite runs on the runtime the guard just checked.
|
|
56
|
+
|
|
19
57
|
## Create a new project
|
|
20
58
|
|
|
21
59
|
```bash
|
package/docs/health.md
CHANGED
|
@@ -271,6 +271,25 @@ What a check returns to describe its own state. A check may also return nothing
|
|
|
271
271
|
| `message` | `string` | Optional human-readable detail. |
|
|
272
272
|
| `meta` | `Record<string, unknown>` | Optional structured metadata surfaced in the report. |
|
|
273
273
|
|
|
274
|
+
### Types
|
|
275
|
+
|
|
276
|
+
| Type | What it is |
|
|
277
|
+
| ---------------------- | --------------------------------------------------------------------------- |
|
|
278
|
+
| `HealthStatus` | `"ok" \| "degraded" \| "down"` — what one check, and the aggregate, report. |
|
|
279
|
+
| `HealthCheckReport` | One check's result: its name, status, duration, and any message. |
|
|
280
|
+
| `HealthConfigShape` | What `HealthConfig()` accepts. |
|
|
281
|
+
| `ResolvedHealthConfig` | The same after defaults are filled in — what the endpoint actually reads. |
|
|
282
|
+
|
|
283
|
+
### The doctor's own types
|
|
284
|
+
|
|
285
|
+
`zt doctor` is extensible: a provider contributes checks through `doctorChecks()`, and
|
|
286
|
+
`runDoctor(app)` runs them all. A check returns a `DoctorCheckResult` — `ok`, `warn` or `fail`,
|
|
287
|
+
with a message and the `fix` printed under it — and the report is `DoctorReportEntry[]`, each
|
|
288
|
+
pairing a check with what it found.
|
|
289
|
+
|
|
290
|
+
A `fail` refuses a deploy; a `warn` does not. Choose deliberately: a check that warns about
|
|
291
|
+
something fatal is ignored, and one that fails on something conditional gets worked around.
|
|
292
|
+
|
|
274
293
|
## Next steps
|
|
275
294
|
|
|
276
295
|
- [Telemetry](/docs/telemetry) — collect the metrics behind your readiness checks.
|