create-hozu 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/skill/SKILL.md +10 -11
- package/skill/changing.md +77 -23
- package/skill/reference.md +12 -1
- package/templates/guide.md +5 -2
package/README.md
CHANGED
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -5,10 +5,10 @@ description: Build or change an app with the Hozu framework (packages @hozu/*, f
|
|
|
5
5
|
|
|
6
6
|
# Hozu authoring guide
|
|
7
7
|
|
|
8
|
-
Hozu is not in your training data. These files are the whole API;
|
|
8
|
+
Hozu is not in your training data. These files are the whole API; skip `node_modules/@hozu`.
|
|
9
9
|
- **Changing an app:** read `changing.md` first, then only the app's own files.
|
|
10
|
-
- **Building an app:** read this file, run `hozu add feature <name> --page
|
|
11
|
-
|
|
10
|
+
- **Building an app:** read this file, run `hozu add feature <name> --page / --with auth,detail,toggle,filter,remove`
|
|
11
|
+
(auth = sign-in, per-user data) and edit the texts it lists; don't print the generated files.
|
|
12
12
|
- **`reference.md`** when the task needs it: routes, DOM fields, no-JS forms, `head`, 404/500, field errors,
|
|
13
13
|
sessions, languages, env, HTTP, Markdown, images, preview, PWA, page tests, deployment.
|
|
14
14
|
- **A diagnostic you do not understand:** `diagnostics.md`.
|
|
@@ -36,18 +36,18 @@ features/<name>/
|
|
|
36
36
|
model.ts schemas, events, query / mutation / tag / fn, the machine
|
|
37
37
|
views.ts views, contracts, feature()
|
|
38
38
|
```
|
|
39
|
-
Relative imports end in `.ts`.
|
|
40
39
|
|
|
41
40
|
## Commands (from the app directory)
|
|
42
41
|
```
|
|
43
|
-
pnpm exec hozu check # after every edit: types,
|
|
42
|
+
pnpm exec hozu check # after every edit: types, rules, contracts
|
|
44
43
|
pnpm exec hozu check --update-lock # accept an intended behaviour change
|
|
45
|
-
pnpm exec hozu add feature items --page /
|
|
46
|
-
pnpm exec hozu
|
|
44
|
+
pnpm exec hozu add feature items --page / --with detail,toggle # a working feature, wired in
|
|
45
|
+
pnpm exec hozu map # outline of the app with file:line
|
|
46
|
+
pnpm exec hozu get / --select button --forms # pages without a server: text, attributes, forms
|
|
47
47
|
pnpm exec hozu post / --field title=A --next 'POST / title=a' --next / # submit a form like a browser
|
|
48
48
|
```
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
Diagnostics give `file:line`, cause and fix: apply the fix. `get` / `post` start from fresh in-memory data;
|
|
50
|
+
chain steps with `--next`. Run `node serve.ts` only to use the app. Relative imports end in `.ts`.
|
|
51
51
|
|
|
52
52
|
## model.ts
|
|
53
53
|
```ts
|
|
@@ -128,12 +128,11 @@ export const Board = ui.view({
|
|
|
128
128
|
]),
|
|
129
129
|
})
|
|
130
130
|
```
|
|
131
|
-
- `ui.<tag>(attrs, children)`;
|
|
131
|
+
- `ui.<tag>(attrs, children)`; values are literals, references or guards. `ui.each(list, 'id', (item) => node)`.
|
|
132
132
|
- `class` is a static string of Tailwind classes that must exist (HZ026). Conditional classes:
|
|
133
133
|
`toggle: { 'bg-indigo-600 text-white': op.eq(ctx.tab, t) }`. CSS variables: `vars: { '--hue': item.hue }`.
|
|
134
134
|
- Events: `on: { click: ui.send(Event, payload) }`. Payload fields: literals, references, `ui.dom.value`,
|
|
135
135
|
`ui.dom.form('name')`, `ui.dom.checked`, `ui.dom.valueAsNumber`, `ui.dom.key`.
|
|
136
|
-
- `ui.each(list, 'id', (item) => node)` (key `null` for primitives).
|
|
137
136
|
- Links: `ui.link(route, params, search)`, never a string path (HZ032). The third argument exists only when the
|
|
138
137
|
route declares `search` (`null` = all defaults). Filters that belong in the URL are `search` links, not context.
|
|
139
138
|
- A form whose submit reads only `ui.dom.form(...)`, literals, context, params and search also works without JS.
|
package/skill/changing.md
CHANGED
|
@@ -1,28 +1,78 @@
|
|
|
1
1
|
# Changing a Hozu app
|
|
2
2
|
|
|
3
|
-
Keep the loop short:
|
|
3
|
+
Keep the loop short: map once, edit everything, check once, verify once.
|
|
4
4
|
|
|
5
5
|
## 1. Read
|
|
6
6
|
- The change request.
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
where it keeps views, contracts and `feature()`.
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## 2.
|
|
14
|
-
|
|
7
|
+
- `pnpm exec hozu map`: every route, query, mutation, event, state, view and contract, each with its `file:line`.
|
|
8
|
+
Open only the lines the change touches. Below, *model* is where the app keeps schemas, events, effects and
|
|
9
|
+
the machine (`model.ts`), and *views* where it keeps views, contracts and `feature()`.
|
|
10
|
+
- The API is in `SKILL.md`. Use a recipe below when one fits; open `patterns.md` / `reference.md` only for
|
|
11
|
+
something else.
|
|
12
|
+
|
|
13
|
+
## 2. Recipes
|
|
14
|
+
Names follow `hozu add feature items`: `Item`, `NewItem`, `Add`, `addItem`, `itemsMachine`, `ItemsBoard`.
|
|
15
|
+
|
|
16
|
+
### A field chosen in the add form (an enum)
|
|
17
|
+
- **model:**
|
|
18
|
+
- `export const Priority = z.enum(['low', 'normal', 'high'])`;
|
|
19
|
+
- add `priority: Priority` to `Item`, `NewItem` and the `Add` payload;
|
|
20
|
+
- context: `priority: Priority`, with `priority: 'normal'` in `initialContext`;
|
|
21
|
+
- `fields` gets `priority: z.string().nullable()`, with `priority: null` in `initialContext` and in the `Add`
|
|
22
|
+
assign that resets it;
|
|
23
|
+
- the `Add` assign also gets `op.set(ctx.priority, e.priority)`, and the add `invoke` input becomes
|
|
24
|
+
`{ title: ctx.draft, priority: ctx.priority }`.
|
|
25
|
+
- **views:**
|
|
26
|
+
- the form's submit sends `{ title: ui.dom.form('title'), priority: ui.dom.form('priority') }`;
|
|
27
|
+
- inside the form add
|
|
28
|
+
`ui.select({ name: 'priority', 'aria-label': 'Priority', class: 'rounded border px-2' }, ['low', 'normal', 'high'].map((p) => ui.option({ value: p, selected: p === 'normal' }, [p])))`;
|
|
29
|
+
- in the item: `ui.span({ class: 'text-xs' }, [item.priority])`.
|
|
30
|
+
- **Contracts:**
|
|
31
|
+
- add `priority: 'normal'` to every `Add` payload, to the add effect's input, and to the `done` result;
|
|
32
|
+
- `rejectsInvalid`'s `data.fields` and `changes.fields` get `priority: null`.
|
|
33
|
+
- **server:** store `priority` (seed items included) and return it.
|
|
34
|
+
|
|
35
|
+
### An action button that works on many items (e.g. "Clear done")
|
|
36
|
+
- **model:**
|
|
37
|
+
- `export const ClearDone = event({ payload: z.object({}) })`;
|
|
38
|
+
- `export const clearDone = mutation({ input: z.object({}), output: z.object({ removed: z.number() }), invalidates: () => [itemsTag()] })`;
|
|
39
|
+
- in `idle`: `on(ClearDone, { target: 'clearing', assign: () => [op.set(ctx.error, null)] })`;
|
|
40
|
+
- a state
|
|
41
|
+
`clearing: { ignore: [...], invoke: invoke(clearDone, { input: {}, done: [{ target: 'idle' }], failed: { Unexpected: [{ target: 'idle', assign: (e) => [op.set(ctx.error, e.message)] }] } }) }`;
|
|
42
|
+
- add `ClearDone` to every busy state's `ignore`, and give `clearing` the same list.
|
|
43
|
+
- **views:** the control
|
|
44
|
+
`ui.form({ on: { submit: ui.send(ClearDone, {}) } }, [ui.button({ type: 'submit', class: 'text-sm underline' }, ['Clear done'])])`.
|
|
45
|
+
- **Contracts:**
|
|
46
|
+
- `given: { state: 'idle' }`, when `[{ send: ClearDone, payload: {} }, { done: clearDone, result: { removed: 1 } }]`,
|
|
47
|
+
expect `{ state: 'idle', effects: [{ effect: clearDone, input: {} }] }`;
|
|
48
|
+
- a second one for `failed … 'Unexpected'` from `clearing`;
|
|
49
|
+
- add `ClearDone`, `clearDone` and both contracts to `declarations`.
|
|
50
|
+
- **server:**
|
|
51
|
+
`implement(clearDone, () => { const before = items.length; items.splice(0, items.length, ...items.filter((i) => !i.done)); return { removed: before - items.length } })`.
|
|
52
|
+
- **Try it:** `hozu post / --button 'Clear done' --next /`.
|
|
53
|
+
|
|
54
|
+
### A field shown on the detail page
|
|
55
|
+
In the detail view's `ready`: `ui.p({}, ['Priority: ', item.priority])`. The detail query already returns the whole
|
|
56
|
+
item.
|
|
57
|
+
|
|
58
|
+
### A detail page, when the feature has none
|
|
59
|
+
Run a fresh scaffold into a scratch app with `--with detail`, and copy the parts it prints:
|
|
60
|
+
- the route with params;
|
|
61
|
+
- the `get` query and its resolver;
|
|
62
|
+
- the detail view;
|
|
63
|
+
- the link in the list;
|
|
64
|
+
- `ui.page(...)` with `head` and `entries`.
|
|
65
|
+
|
|
66
|
+
### Other changes
|
|
67
|
+
| Change | Touch |
|
|
15
68
|
|---|---|
|
|
16
|
-
| New
|
|
17
|
-
|
|
|
18
|
-
| New
|
|
19
|
-
| New page | `routes.ts` (`params`, `search`) → a view with `route`, added to `declarations` → `ui.page(...)` in `hozu.config.ts` (with `head`, and `entries` when the route has params). |
|
|
20
|
-
| New filter / sort / page number that should be in the URL | the route's `search` schema (with a default) → links with `ui.link(route, params, { key: value })` → read `search.key` in the view. No machine change. |
|
|
69
|
+
| New UI-only state (a tab) | model: the context field and its initial value, an event, an `on` that `op.set`s it (add the event to every busy state's `ignore`) → views: the control, a contract, the event in `declarations`. |
|
|
70
|
+
| Filter / sort / page in the URL | the route's `search` schema (with a default) → links with `ui.link(route, params, { key: value })` → read `search.key` in the view. No machine change. |
|
|
71
|
+
| New page | `routes.ts` → a view with `route`, in `declarations` → `ui.page(...)` in `hozu.config.ts` (`head`, and `entries` when the route has params). |
|
|
21
72
|
|
|
22
73
|
Whenever the machine changes:
|
|
23
|
-
- Add one contract per new transition; HZ016 prints each missing one ready to paste.
|
|
24
|
-
|
|
25
|
-
- Every busy state `ignore`s every event its visible controls can send. HZ005 prints the missing `ignore` entries.
|
|
74
|
+
- Add one contract per new transition; HZ016 prints each missing one ready to paste.
|
|
75
|
+
- Every busy state `ignore`s every event its visible controls can send; HZ005 prints the missing entries.
|
|
26
76
|
|
|
27
77
|
## 3. Check (once, after all edits)
|
|
28
78
|
```
|
|
@@ -32,9 +82,13 @@ Fix what it reports. When the behaviour change is intended and everything is cle
|
|
|
32
82
|
`pnpm exec hozu check --update-lock`. HZ018 asks for this.
|
|
33
83
|
|
|
34
84
|
## 4. Verify (once, no server needed)
|
|
35
|
-
- Pages
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
85
|
+
- **Pages:** `pnpm exec hozu get / /items/i1` prints the status, title, alerts and visible text.
|
|
86
|
+
- **Attributes and forms:** `--select button` (or `'[role=alert]'`, `a[href]`, `#id`) prints elements with their
|
|
87
|
+
attributes, e.g. `aria-pressed`; `--forms` lists each form's fields and buttons. Never start a server for this.
|
|
88
|
+
- **Forms:** `pnpm exec hozu post / --field title=A --field priority=high --next /items` fills the form like a
|
|
89
|
+
browser (other fields keep their defaults). It follows the redirect, then runs the next steps in the same
|
|
90
|
+
process.
|
|
91
|
+
- **Chaining:** chain what must share data, e.g. `--next 'POST / title=a'` (fields as `a=1&b=2`) or
|
|
92
|
+
`--next /items/i3`.
|
|
93
|
+
- **A form with only a button:** `--button 'Clear done'`, or `--next 'POST / @Clear done'`. With one form per item,
|
|
94
|
+
`--field id=t2` picks the item's form.
|
package/skill/reference.md
CHANGED
|
@@ -51,6 +51,10 @@ Every mutation also has the framework error `Invalid` = `{ message, fields }`: o
|
|
|
51
51
|
(see `patterns.md`).
|
|
52
52
|
|
|
53
53
|
## Sessions
|
|
54
|
+
- **Start from the scaffold:** `hozu add feature notes --page / --with auth` writes `features/account`
|
|
55
|
+
(sign-in page, sign-out, `me`), the session cookie in `serve.ts`, per-user resolvers, and a redirect to `/login`
|
|
56
|
+
when signed out. Replace the name-only sign-in with real credentials before production; set `SESSION_SECRET`
|
|
57
|
+
(and `SESSION_SECURE=true` behind HTTPS).
|
|
54
58
|
- `project({ session: z.object({ user: z.string() }) })` declares the identity. Queries with `scope: 'user'` and
|
|
55
59
|
mutations receive `session`; public resolvers never do.
|
|
56
60
|
- `createServer({ session: (request) => value })`, or `sessionCookie({ name, secret })` from
|
|
@@ -108,8 +112,12 @@ There are no rewrites: one URL has one owner.
|
|
|
108
112
|
dimensions). With `@hozu/image` installed, pass `images: await optimizeImages(build)` to `createServer` (and
|
|
109
113
|
`hozu build` does it itself): raster assets get WebP `srcset` widths and `sizes`.
|
|
110
114
|
- **Share images:** `head.render` → `image: ui.og({ title, subtitle })` renders a 1200×630 card; pass
|
|
111
|
-
`og: ogImage` (from `@hozu/image`) to `createServer`.
|
|
115
|
+
`og: ogImage` (from `@hozu/image`) to `createServer`. On a static host, use a file instead:
|
|
116
|
+
`image: ui.asset(new URL('./share.png', import.meta.url))` (made absolute with `site.url`).
|
|
112
117
|
- **Fonts:** a local `@font-face` gets a size-matched `"<Family> Fallback"` automatically.
|
|
118
|
+
- **Page transitions:** the stylesheet turns on cross-document view transitions, so links between pages cross-fade
|
|
119
|
+
instead of flashing (no JS). Turn them off with `@view-transition { navigation: none; }` in `app.css`; style them
|
|
120
|
+
with `::view-transition-*`.
|
|
113
121
|
|
|
114
122
|
## Preview (drafts)
|
|
115
123
|
`createServer({ preview: { secret } })`; `GET /_hozu/preview?secret=…&path=/posts/a` turns preview on (a signed
|
|
@@ -132,6 +140,9 @@ Cloudflare Workers or Vercel the server is `createHandler({ build, manifest, res
|
|
|
132
140
|
`@hozu/runtime-server` with `export default { fetch: handler.fetch }`, where
|
|
133
141
|
`import * as render from './dist/server/render.js'` is the page code `hozu build` generates (edge runtimes cannot
|
|
134
142
|
generate it at startup). Page cache and tag revalidation are per instance.
|
|
143
|
+
A fully static site (GitHub Pages, any file host): `exportStatic({ build, styles, resolvers, outDir })` from
|
|
144
|
+
`@hozu/adapter-static` writes every page without per-request data, plus the files they link to, and lists skipped
|
|
145
|
+
routes.
|
|
135
146
|
```ts
|
|
136
147
|
import manifest from './dist/manifest.json' with { type: 'json' }
|
|
137
148
|
import * as render from './dist/server/render.js'
|
package/templates/guide.md
CHANGED
|
@@ -9,14 +9,17 @@ A web app built with Hozu (`@hozu/*`). Hozu is not in your training data.
|
|
|
9
9
|
|
|
10
10
|
## The loop
|
|
11
11
|
```
|
|
12
|
-
__RUN__ hozu add feature tasks --page /
|
|
12
|
+
__RUN__ hozu add feature tasks --page / --with detail,toggle,filter,remove # then edit the texts it lists
|
|
13
|
+
# add auth to the list for sign-in and per-user data
|
|
14
|
+
__RUN__ hozu map # outline of the app with file:line, before a change
|
|
13
15
|
__RUN__ hozu check # after every change: types, rules, contracts
|
|
14
16
|
__RUN__ hozu check --update-lock # only to accept a clean, intended behaviour change
|
|
15
|
-
__RUN__ hozu get /
|
|
17
|
+
__RUN__ hozu get / --select button --forms # try pages without a server: text, attributes, forms
|
|
16
18
|
__RUN__ hozu post / --field title=Ship --next / # submit a form like a browser
|
|
17
19
|
```
|
|
18
20
|
__NOTE__
|
|
19
21
|
## Rules
|
|
22
|
+
- After `hozu add feature`, do not print the generated files: edit the texts it lists; `hozu map` shows the rest.
|
|
20
23
|
- Apply the fix each diagnostic gives; do not work around a rule.
|
|
21
24
|
- Every behaviour change comes with a contract change.
|
|
22
25
|
- Do not edit `__SKILL__/`: `__RUN__ hozu skill` rewrites it for the installed Hozu version.
|