tutuca 0.9.89 → 0.9.91
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/dist/tutuca-cli.js +52 -9
- package/dist/tutuca-dev.ext.js +34 -4
- package/dist/tutuca-dev.js +34 -4
- package/dist/tutuca-dev.min.js +2 -2
- package/dist/tutuca-extra.ext.js +27 -2
- package/dist/tutuca-extra.js +27 -2
- package/dist/tutuca-extra.min.js +2 -2
- package/dist/tutuca-storybook.js +59 -7
- package/dist/tutuca.ext.js +27 -2
- package/dist/tutuca.js +27 -2
- package/dist/tutuca.min.js +2 -2
- package/package.json +1 -1
- package/skill/tutuca/SKILL.md +1 -0
- package/skill/tutuca/cli.md +4 -26
- package/skill/tutuca/core.md +20 -1
- package/skill/tutuca/patterns/README.md +4 -0
- package/skill/tutuca/patterns/add-a-story.md +26 -0
- package/skill/tutuca/request-response.md +10 -1
- package/skill/tutuca/storybook.md +151 -0
- package/skill/tutuca-source/tutuca.ext.js +27 -2
package/package.json
CHANGED
package/skill/tutuca/SKILL.md
CHANGED
|
@@ -30,6 +30,7 @@ When authoring tutuca code, also load these if available:
|
|
|
30
30
|
| Authoring `component({...})`, `html`...`` views, macros, fields, events, lists, styles | [core.md](./core.md) |
|
|
31
31
|
| Designing components — responsibilities, state ownership, channel choice, do's & don'ts | [component-design.md](./component-design.md) |
|
|
32
32
|
| CLI commands, flags, exit codes, full linter rule list | [cli.md](./cli.md) |
|
|
33
|
+
| Authoring `*.dev.js` story modules, `getExamples()` sections, per-example request mocking, running `tutuca storybook` | [storybook.md](./storybook.md) |
|
|
33
34
|
| `bubble` / `send`-`receive` / async `request`-`response` channels, `$unknown`, request-handler registration | [request-response.md](./request-response.md) |
|
|
34
35
|
| Drag & drop, dynamic bindings (`*x`), pseudo-`x`, custom seq types, Tailwind/MargaUI | [advanced.md](./advanced.md) |
|
|
35
36
|
| Runtime semantics — path steps, transaction lifecycle, dyn-var teleporting, async key pinning (`livePath`) | [semantics.md](./semantics.md) |
|
package/skill/tutuca/cli.md
CHANGED
|
@@ -217,33 +217,11 @@ version-pinned CDN. All tutuca specifiers resolve to a single runtime, which
|
|
|
217
217
|
component scope/identity requires. `--out` always pins the CDN so the static
|
|
218
218
|
artifact is portable (host it from the project root so `/*.dev.js` paths resolve).
|
|
219
219
|
|
|
220
|
-
###
|
|
220
|
+
### Authoring `.dev.js` story modules
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
production or the UI**. The `.dev.js` suffix is the contract — your app imports
|
|
226
|
-
its real components directly and never a `.dev.js`, and a production build glob
|
|
227
|
-
can exclude `**/*.dev.js`. Because they follow the full module convention, the
|
|
228
|
-
same files are valid targets for `tutuca test`/`lint`/`render` too.
|
|
229
|
-
|
|
230
|
-
```js
|
|
231
|
-
// counter.dev.js — lives next to counter.js
|
|
232
|
-
import { component, html } from "tutuca";
|
|
233
|
-
import { Counter } from "./counter.js";
|
|
234
|
-
|
|
235
|
-
export function getComponents() {
|
|
236
|
-
return [Counter];
|
|
237
|
-
}
|
|
238
|
-
export function getExamples() {
|
|
239
|
-
return { title: "Counter", items: [{ title: "Basic", value: Counter.make({}) }] };
|
|
240
|
-
}
|
|
241
|
-
export function getTests({ describe, test, expect }) {
|
|
242
|
-
describe(Counter, () => {
|
|
243
|
-
test("starts at zero", () => expect(Counter.make({}).count).toBe(0));
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
```
|
|
222
|
+
The `*.dev.js` convention (a dev-only module holding `getComponents()` +
|
|
223
|
+
`getExamples()` + `getTests()`, never shipped), the example/section shape, and
|
|
224
|
+
per-example request mocking are covered in [storybook.md](./storybook.md).
|
|
247
225
|
|
|
248
226
|
## Install skill assets
|
|
249
227
|
|
package/skill/tutuca/core.md
CHANGED
|
@@ -1005,12 +1005,31 @@ export function getExamples() {
|
|
|
1005
1005
|
return {
|
|
1006
1006
|
title: "...",
|
|
1007
1007
|
description: "...",
|
|
1008
|
-
|
|
1008
|
+
// value = Comp.make(...); requestHandlers (optional) mocks this example's requests
|
|
1009
|
+
items: [{ title, description, value, view, requestHandlers }],
|
|
1009
1010
|
};
|
|
1010
1011
|
}
|
|
1011
1012
|
export function getTests({ describe, test, expect }) { /*...*/ } // optional — see cli.md
|
|
1012
1013
|
```
|
|
1013
1014
|
|
|
1015
|
+
An example item may carry an optional **`requestHandlers`** map — per-example
|
|
1016
|
+
mocks for the request handlers its component triggers, used by the storybook
|
|
1017
|
+
only. Each is a plain async function keyed by request name; it overrides the
|
|
1018
|
+
module's real `getRequestHandlers()` handler **for that example instance only**,
|
|
1019
|
+
so two examples of the same component can show different responses side by side.
|
|
1020
|
+
Return a fixture, `throw` to exercise the error path, or never resolve to hold a
|
|
1021
|
+
loading state:
|
|
1022
|
+
|
|
1023
|
+
```js
|
|
1024
|
+
items: [
|
|
1025
|
+
{ title: "Loaded", value: Widget.make(),
|
|
1026
|
+
requestHandlers: { async load() { return [{ id: 1, name: "Ada" }]; } } },
|
|
1027
|
+
{ title: "Error", value: Widget.make(),
|
|
1028
|
+
requestHandlers: { async load() { throw new Error("boom"); } } },
|
|
1029
|
+
{ title: "Default", value: Widget.make() }, // no mock → real handler / 404
|
|
1030
|
+
]
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1014
1033
|
Best practice: have `getComponents()` return **every** component the module
|
|
1015
1034
|
defines — child and helper components included — and give each one at least
|
|
1016
1035
|
one item in `getExamples()`. A component left out of `getComponents()` is
|
|
@@ -41,3 +41,7 @@ task.
|
|
|
41
41
|
## Component communication
|
|
42
42
|
|
|
43
43
|
- [Coordinate components](coordinate-components.md) — `bubble`, `send`/`receive`, async `request`/`response`.
|
|
44
|
+
|
|
45
|
+
## Stories & catalog
|
|
46
|
+
|
|
47
|
+
- [Add a story for a component](add-a-story.md) — a `*.dev.js` with `getComponents()` + `getExamples()`, optional per-example request mocks.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Add a story for a component
|
|
2
|
+
|
|
3
|
+
**Problem:** show a component (and its states) in the storybook.
|
|
4
|
+
|
|
5
|
+
Create `foo.dev.js` next to `foo.js`:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { Foo } from "./foo.js";
|
|
9
|
+
|
|
10
|
+
export function getComponents() {
|
|
11
|
+
return [Foo];
|
|
12
|
+
}
|
|
13
|
+
export function getExamples() {
|
|
14
|
+
return { title: "Foo", items: [
|
|
15
|
+
{ title: "Empty", value: Foo.make({}) },
|
|
16
|
+
{ title: "Loaded", value: Foo.make({ isLoading: true }),
|
|
17
|
+
requestHandlers: { async load() { return [{ id: 1 }]; } } },
|
|
18
|
+
] };
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`value` must be a real `Foo.make(...)` instance, not a plain object. Add a
|
|
23
|
+
`requestHandlers` map to an item to mock that example's requests
|
|
24
|
+
(fixture / `throw` / never-resolve) — these are storybook-only. Run
|
|
25
|
+
`tutuca storybook` to view, or `--dry-run --json` to smoke-test. See
|
|
26
|
+
[storybook.md](../storybook.md).
|
|
@@ -274,7 +274,7 @@ You can also fire several in one handler
|
|
|
274
274
|
### The request-handler contract
|
|
275
275
|
|
|
276
276
|
Registered request handlers run with **no `this`** (they're invoked as
|
|
277
|
-
`fn.apply(null, args)`), so they can't read component state — pass
|
|
277
|
+
`fn.apply(null, [...args, ctx])`), so they can't read component state — pass
|
|
278
278
|
everything they need through `args`
|
|
279
279
|
(`ctx.request("persistState", [{ key, value }])`). They're plain async
|
|
280
280
|
functions or closures. Aggregate handlers from sub-modules with spread:
|
|
@@ -285,6 +285,15 @@ export function getRequestHandlers() {
|
|
|
285
285
|
}
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
+
The handler also receives a request context as its **final argument**
|
|
289
|
+
(consistent with `receive`/`input`/`response`, where `ctx` is last) — usually
|
|
290
|
+
ignored, but available when needed. Like every ctx it exposes
|
|
291
|
+
`ctx.walkPath(callback)`, which walks the component instances on the issuing
|
|
292
|
+
path **leaf→root**, calling `callback(Component, instance)` (return `false` to
|
|
293
|
+
stop early). It captures the immutable dispatch root/path, so it may be called
|
|
294
|
+
before or after an `await`. (The storybook uses this to let an example mock the
|
|
295
|
+
request handlers its component triggers — per example, in isolation.)
|
|
296
|
+
|
|
288
297
|
### Chaining from a response handler
|
|
289
298
|
|
|
290
299
|
A `response` handler gets the full `ctx`, so it can issue further
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Storybook
|
|
2
|
+
|
|
3
|
+
Reach this file when authoring `*.dev.js` story modules or running
|
|
4
|
+
`tutuca storybook` — defining `getExamples()` sections, mocking requests per
|
|
5
|
+
example, or rendering a live component catalog. For the framework primer see
|
|
6
|
+
[core.md](./core.md); for the full CLI flag/exit-code table see
|
|
7
|
+
[cli.md](./cli.md); for the `getTests` shape see [testing.md](./testing.md).
|
|
8
|
+
|
|
9
|
+
## Mental model
|
|
10
|
+
|
|
11
|
+
`tutuca storybook [dir]` recursively discovers co-located `*.dev.js` modules,
|
|
12
|
+
mounts them via the shipped `tutuca/storybook` library, and serves an ephemeral
|
|
13
|
+
page — no config, no HTML to write. It is **batteries-included by default**:
|
|
14
|
+
before serving it runs each module's `getTests()` in the terminal, the page
|
|
15
|
+
wires margaui styling, and the browser runs `check(app)`. Each is individually
|
|
16
|
+
disablable with a `--no-*` flag. All tutuca specifiers resolve to **one
|
|
17
|
+
runtime** — component scope and identity require it.
|
|
18
|
+
|
|
19
|
+
## The `.dev.js` module
|
|
20
|
+
|
|
21
|
+
A `*.dev.js` file is a **dev-only module**: it holds stories, tests, and
|
|
22
|
+
development-time helpers for nearby components, and is **never shipped to
|
|
23
|
+
production or the UI**. The `.dev.js` suffix is the contract — your app imports
|
|
24
|
+
its real components directly and never a `.dev.js`, and a production build glob
|
|
25
|
+
can exclude `**/*.dev.js`. Because it follows the full module convention, the
|
|
26
|
+
same file is a valid target for `tutuca lint` / `test` / `render` too.
|
|
27
|
+
|
|
28
|
+
| Export | Returns | Used for |
|
|
29
|
+
| ------ | ------- | -------- |
|
|
30
|
+
| `getComponents()` | `[Comp, ...]` | stories — return **every** component the module defines, children and helpers included |
|
|
31
|
+
| `getExamples()` | one section, or an array of sections | the catalog cards |
|
|
32
|
+
| `getTests({ describe, test, expect })` | tests | the pre-serve test run (optional) |
|
|
33
|
+
| `getMacros()` | `{ name: macro }` | macros referenced in views (optional) |
|
|
34
|
+
| `getRequestHandlers()` | `{ name: async fn }` | the module's **real** request handlers (optional) |
|
|
35
|
+
| `getRoot()` | `Root.make({...})` | root state when examples need it (optional) |
|
|
36
|
+
|
|
37
|
+
## Authoring stories (`getExamples`)
|
|
38
|
+
|
|
39
|
+
Return one section, or an array of sections to group examples under multiple
|
|
40
|
+
headings. A section is `{ title, description?, items: [...] }`:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { component, html } from "tutuca";
|
|
44
|
+
import { Counter } from "./counter.js";
|
|
45
|
+
|
|
46
|
+
export function getComponents() {
|
|
47
|
+
return [Counter];
|
|
48
|
+
}
|
|
49
|
+
export function getExamples() {
|
|
50
|
+
return { // one section, or an array of these
|
|
51
|
+
title: "Counter",
|
|
52
|
+
description: "A button that counts clicks.", // optional
|
|
53
|
+
items: [
|
|
54
|
+
{ title: "Basic", description: "starts at zero", value: Counter.make({ count: 0 }) },
|
|
55
|
+
{ title: "Pre-filled", value: Counter.make({ count: 5 }) },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Item fields:
|
|
62
|
+
|
|
63
|
+
- `title` — required.
|
|
64
|
+
- `description?` — shown under the card title.
|
|
65
|
+
- `value` — required, the instance to render, usually `Comp.make({...})`.
|
|
66
|
+
- `view?` — selects a pushed named view, rendered via `@push-view` in the card.
|
|
67
|
+
- `requestHandlers?` — per-example request mocks (next section).
|
|
68
|
+
|
|
69
|
+
The storybook sorts sections by title and renders a sidebar with a filter, so
|
|
70
|
+
one example item per meaningful state reads as a state matrix.
|
|
71
|
+
|
|
72
|
+
## Mocking requests per example
|
|
73
|
+
|
|
74
|
+
An item's optional `requestHandlers` map holds async functions keyed by request
|
|
75
|
+
name that override the module's real `getRequestHandlers()` handler **for that
|
|
76
|
+
one example instance only** — so two examples of the same component show
|
|
77
|
+
different responses side by side. The three idioms:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
items: [
|
|
81
|
+
{ title: "Loaded", value: Widget.make({ isLoading: true }),
|
|
82
|
+
requestHandlers: { async load() { return [{ id: 1, name: "Ada" }]; } } }, // fixture
|
|
83
|
+
{ title: "Error", value: Widget.make({ isLoading: true }),
|
|
84
|
+
requestHandlers: { async load() { throw new Error("boom"); } } }, // error path
|
|
85
|
+
{ title: "Loading", value: Widget.make({ isLoading: true }),
|
|
86
|
+
requestHandlers: { load() { return new Promise(() => {}); } } }, // never resolves
|
|
87
|
+
{ title: "Default", value: Widget.make() }, // no mock → real handler / "Request not found"
|
|
88
|
+
]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
How it resolves: the storybook registers one meta-handler per request name. On
|
|
92
|
+
dispatch it walks the issuing component's path leaf→root to find the nearest
|
|
93
|
+
example carrying a mock for that name (**nearest example wins**), else falls
|
|
94
|
+
back to the module's real handler, else surfaces `Request not found: <name>`.
|
|
95
|
+
This is **storybook-only** — at runtime your real `getRequestHandlers()` apply.
|
|
96
|
+
See [request-response.md](./request-response.md) for the handler contract (the
|
|
97
|
+
`ctx` is the handler's final argument). `tutuca storybook --dry-run --json`
|
|
98
|
+
lists each example's mocked names.
|
|
99
|
+
|
|
100
|
+
## Stories as tests (`getTests`)
|
|
101
|
+
|
|
102
|
+
`getTests` runs through the same machinery as `tutuca test`; the storybook runs
|
|
103
|
+
it in the terminal before serving (skip with `--no-tests`). `describe(Comp, fn)`
|
|
104
|
+
auto-tags the suite by `Comp.name`:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
export function getTests({ describe, test, expect }) {
|
|
108
|
+
describe(Counter, () => {
|
|
109
|
+
test("starts at zero", () => expect(Counter.make({}).count).toBe(0));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See [testing.md](./testing.md) for the full `getTests` shape and how to call
|
|
115
|
+
methods / input / receive / bubble / response / alter handlers.
|
|
116
|
+
|
|
117
|
+
## Running it
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
tutuca storybook # scan + serve the current directory
|
|
121
|
+
tutuca storybook ./packages/ui # scan + serve another directory
|
|
122
|
+
tutuca storybook --dry-run # prep + print what would be shown, don't serve (smoke test)
|
|
123
|
+
tutuca storybook --dry-run --json # same, machine-readable for agents
|
|
124
|
+
tutuca storybook --out ./_site # write a static index.html + bootstrap instead of serving
|
|
125
|
+
tutuca storybook --no-tests # skip the pre-serve getTests() run
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Runtime resolution (convention over configuration): a local
|
|
129
|
+
`node_modules/tutuca` install if present, else the CLI's own `dist`, else the
|
|
130
|
+
version-pinned CDN. `--out` always pins the CDN so the artifact is portable —
|
|
131
|
+
host it from the project root so `/*.dev.js` paths resolve. See [cli.md](./cli.md)
|
|
132
|
+
for the exhaustive flag list and exit codes.
|
|
133
|
+
|
|
134
|
+
## Footguns
|
|
135
|
+
|
|
136
|
+
- ⚠️ `value` must be a real instance (`Comp.make(...)`), not a plain object or
|
|
137
|
+
the class itself — examples need an addressable instance for event dispatch.
|
|
138
|
+
- ⚠️ `requestHandlers` mocks are **storybook-only** and per-instance; don't rely
|
|
139
|
+
on them in `getTests` or production code.
|
|
140
|
+
- ⚠️ Never import a `.dev.js` from app/production code — the suffix is the
|
|
141
|
+
ship / no-ship boundary.
|
|
142
|
+
- ⚠️ An example whose component triggers a request with no real handler and no
|
|
143
|
+
per-example mock surfaces `Request not found: <name>`.
|
|
144
|
+
- ⚠️ Keep one tutuca runtime — mixed specifiers or installs break scope identity.
|
|
145
|
+
|
|
146
|
+
## Verify
|
|
147
|
+
|
|
148
|
+
After editing a `*.dev.js`: `tutuca lint <module>.dev.js` →
|
|
149
|
+
`tutuca test <module>.dev.js` → `tutuca storybook --dry-run --json <dir>`
|
|
150
|
+
(smoke-test discovery, counts, and mocked names without serving), then
|
|
151
|
+
`tutuca storybook <dir>` to view it live.
|
|
@@ -250,6 +250,17 @@ class Path {
|
|
|
250
250
|
}
|
|
251
251
|
return curVal;
|
|
252
252
|
}
|
|
253
|
+
resolveChain(root) {
|
|
254
|
+
const out = [root];
|
|
255
|
+
let curVal = root;
|
|
256
|
+
for (const step of this.steps) {
|
|
257
|
+
curVal = step.lookup(curVal, NONE);
|
|
258
|
+
if (curVal === NONE)
|
|
259
|
+
break;
|
|
260
|
+
out.push(curVal);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
253
264
|
setValue(root, v) {
|
|
254
265
|
const intermediates = new Array(this.steps.length);
|
|
255
266
|
let curVal = root;
|
|
@@ -2926,6 +2937,7 @@ class Transactor {
|
|
|
2926
2937
|
const txnPath = path.toTransactionPath();
|
|
2927
2938
|
const curLeaf = txnPath.lookup(curRoot);
|
|
2928
2939
|
const handler = this.comps.getRequestFor(curLeaf, name) ?? mkReq404(name);
|
|
2940
|
+
const reqCtx = new RequestContext(path, this, parent, curRoot);
|
|
2929
2941
|
const resHandlerName = opts?.onResName ?? name;
|
|
2930
2942
|
const resPath = opts?.livePath ? null : txnPath.pinKeys(curRoot);
|
|
2931
2943
|
const push = (specificName, baseName, singleArg, result, error) => {
|
|
@@ -2934,7 +2946,7 @@ class Transactor {
|
|
|
2934
2946
|
this.pushTransaction(t);
|
|
2935
2947
|
};
|
|
2936
2948
|
try {
|
|
2937
|
-
const result = await handler.fn.apply(null, args);
|
|
2949
|
+
const result = await handler.fn.apply(null, [...args, reqCtx]);
|
|
2938
2950
|
push(opts?.onOkName, resHandlerName, result, result, null);
|
|
2939
2951
|
} catch (error) {
|
|
2940
2952
|
push(opts?.onErrorName, resHandlerName, error, null, error);
|
|
@@ -3177,10 +3189,20 @@ class Task {
|
|
|
3177
3189
|
}
|
|
3178
3190
|
|
|
3179
3191
|
class Dispatcher {
|
|
3180
|
-
constructor(path, transactor, parentTransaction) {
|
|
3192
|
+
constructor(path, transactor, parentTransaction, root = transactor.state.val) {
|
|
3181
3193
|
this.path = path;
|
|
3182
3194
|
this.transactor = transactor;
|
|
3183
3195
|
this.parent = parentTransaction;
|
|
3196
|
+
this.root = root;
|
|
3197
|
+
}
|
|
3198
|
+
walkPath(callback) {
|
|
3199
|
+
const comps = this.transactor.comps;
|
|
3200
|
+
const chain = this.path.toTransactionPath().resolveChain(this.root);
|
|
3201
|
+
for (let i = chain.length - 1;i >= 0; i--) {
|
|
3202
|
+
const comp = comps.getCompFor(chain[i]);
|
|
3203
|
+
if (comp && callback(comp, chain[i]) === false)
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3184
3206
|
}
|
|
3185
3207
|
get at() {
|
|
3186
3208
|
return new PathChanges(this);
|
|
@@ -3217,6 +3239,9 @@ class EventContext extends Dispatcher {
|
|
|
3217
3239
|
}
|
|
3218
3240
|
}
|
|
3219
3241
|
|
|
3242
|
+
class RequestContext extends Dispatcher {
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3220
3245
|
class PathChanges extends PathBuilder {
|
|
3221
3246
|
constructor(dispatcher) {
|
|
3222
3247
|
super();
|