tutuca 0.13.2 → 0.14.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/dist/tutuca-cli.js +611 -458
- package/dist/tutuca-dev.ext.js +642 -486
- package/dist/tutuca-dev.js +653 -497
- package/dist/tutuca-dev.min.js +4 -4
- package/dist/tutuca-extra.ext.js +625 -451
- package/dist/tutuca-extra.js +632 -458
- package/dist/tutuca-extra.min.js +3 -3
- package/dist/tutuca.ext.js +625 -451
- package/dist/tutuca.js +632 -458
- package/dist/tutuca.min.js +3 -3
- package/package.json +1 -1
- package/skill/tutuca/SKILL.md +2 -2
- package/skill/tutuca/advanced.md +48 -17
- package/skill/tutuca/component-design.md +2 -2
- package/skill/tutuca/core.md +7 -1
- package/skill/tutuca/messages-and-intents.md +1 -1
- package/skill/tutuca/patterns/README.md +1 -1
- package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +11 -6
- package/skill/tutuca/patterns/share-state-across-the-tree.md +18 -3
- package/skill/tutuca/semantics.md +43 -28
- package/skill/tutuca-source/tutuca.ext.js +625 -451
package/skill/tutuca/advanced.md
CHANGED
|
@@ -79,14 +79,38 @@ it: whoever is nearest above and provides that name answers, and a
|
|
|
79
79
|
`*name` that names the component's own `provide` resolves to that
|
|
80
80
|
(including its own).
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
More than one component may provide the same name. The live render stack
|
|
83
|
+
is a normal shadowing stack, so the **nearest rendered provider wins** —
|
|
84
|
+
a `Theme` nested inside another `Theme` overrides it for its own subtree,
|
|
85
|
+
and nothing has to name a producer.
|
|
86
86
|
|
|
87
87
|
A lookup name starting with an **uppercase** letter is a component
|
|
88
88
|
**type** rather than a value; see *Looking up component types*.
|
|
89
89
|
|
|
90
|
+
### Ambient names without a provider
|
|
91
|
+
|
|
92
|
+
A name can also be registered **lexically**, as an absolute path from the
|
|
93
|
+
app state root — for a session, a theme, or anything host-owned that no
|
|
94
|
+
component in the tree should have to publish:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import { path } from "tutuca";
|
|
98
|
+
|
|
99
|
+
app.registerComponents([Workspace, Toolbar], {
|
|
100
|
+
paths: { session: path().field("session"), theme: path().field("ui").field("theme") },
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`path()` is the same address builder `ctx.at` hands a handler — `.field(name)`,
|
|
105
|
+
`.index(name, i)`, `.key(name, k)`, chained freely from the state root.
|
|
106
|
+
|
|
107
|
+
Descendants declare `lookup: ["session", "theme"]` and read or render
|
|
108
|
+
`*session` / `*theme` as usual. The mounted root does not have to publish
|
|
109
|
+
them, and no wrapper component needs to exist solely to. Register on a
|
|
110
|
+
nested scope (`scope.registerPaths({…})`) to narrow a name; nearest
|
|
111
|
+
registration wins. The order is still `dyn lex`, so a rendered provider
|
|
112
|
+
answers before a registered path.
|
|
113
|
+
|
|
90
114
|
### Looking up component types
|
|
91
115
|
|
|
92
116
|
The same two environments answer a **type** name, and a handler asks with
|
|
@@ -139,11 +163,14 @@ component-render target and an iteration source:
|
|
|
139
163
|
```
|
|
140
164
|
|
|
141
165
|
A `provide` value must be **addressable** — a `.field` or a `.seq[.key]`
|
|
142
|
-
seq-access, nothing else.
|
|
143
|
-
render
|
|
144
|
-
path — is a lint error
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
seq-access, nothing else. It is both read as `*name` *and* used as the
|
|
167
|
+
path a render target resumes at, so a method or constant — which has no
|
|
168
|
+
path — is a lint error (`PROVIDE_NOT_ADDRESSABLE`), and the bad provide is
|
|
169
|
+
dropped: `*name` then reads its `default`, or `null`. If a dynamic binding
|
|
170
|
+
reads as its fallback everywhere, lint first and suspect the producer's
|
|
171
|
+
expression. A `lookup` `default`, by contrast, is only a value fallback and
|
|
172
|
+
accepts the full grammar, including constants like `'gray'`. A `provide`
|
|
173
|
+
can be a sequence/map item access:
|
|
147
174
|
|
|
148
175
|
```js
|
|
149
176
|
const Root = component({
|
|
@@ -160,14 +187,18 @@ There is **no `*name[.key]` form** — a consumer never indexes a dynamic
|
|
|
160
187
|
var. The seq-access lives in the producer's `provide` declaration; the
|
|
161
188
|
consumer just reads the resolved value as `*name`.
|
|
162
189
|
|
|
163
|
-
**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
A published **type** is not a render target: it has no path, so
|
|
191
|
+
`<x render="*Cell">` stays unresolvable by construction.
|
|
192
|
+
|
|
193
|
+
**Resuming at the value's path.** The component rendered via
|
|
194
|
+
`<x render="*selected">` physically lives at the producer (e.g.
|
|
195
|
+
`Root.items`), not under the consumer. A provider publishes the value
|
|
196
|
+
*and* the absolute path it lives at, and rendering pushes that path as a
|
|
197
|
+
continuation frame. So an event inside that subtree mutates the selected
|
|
198
|
+
value where it really is, while bubbling pops back out to the visual
|
|
199
|
+
caller that wrote the `*name` and continues through its ancestry. Editing
|
|
200
|
+
the entry in the consumer and the same entry in the producer's own view
|
|
201
|
+
update in lock-step.
|
|
171
202
|
|
|
172
203
|
## Pseudo-`x` (`@x`)
|
|
173
204
|
|
|
@@ -160,8 +160,8 @@ A compact worked version of the first three (`method`, `send`/`receive`, and
|
|
|
160
160
|
- [messages-and-intents.md](./messages-and-intents.md) — the channels in depth
|
|
161
161
|
(`send`/`receive`, `intent` and its routes, `sendAtRoot`) and integrating
|
|
162
162
|
with the outside world.
|
|
163
|
-
- [advanced.md](./advanced.md) — `provide` / `lookup` / `*name
|
|
164
|
-
clean-namespace boundary.
|
|
163
|
+
- [advanced.md](./advanced.md) — `provide` / `lookup` / `*name`, lexically
|
|
164
|
+
registered paths, and the clean-namespace boundary.
|
|
165
165
|
- [patterns/coordinate-components.md](./patterns/coordinate-components.md),
|
|
166
166
|
[patterns/share-state-across-the-tree.md](./patterns/share-state-across-the-tree.md),
|
|
167
167
|
[patterns/render-a-child-component.md](./patterns/render-a-child-component.md) —
|
package/skill/tutuca/core.md
CHANGED
|
@@ -381,6 +381,12 @@ native `Map`, and native `Set` stay native. Wrap nested component data with
|
|
|
381
381
|
| `new Map()` | map | `draft.x.set(key, value)` |
|
|
382
382
|
| `new Set()` | set | `draft.x.add(value)`, `draft.x.delete(value)` |
|
|
383
383
|
|
|
384
|
+
**Every numeric default is a float, whole numbers included** — `count: 0` is a
|
|
385
|
+
float field, not an int, because a JS literal can't say which one it means
|
|
386
|
+
(`0.0` IS `0`). Ask for int explicitly when you want the truncation:
|
|
387
|
+
`count: { type: "int", defaultValue: 0 }` turns a later `3.14` into `3`, while
|
|
388
|
+
the plain `count: 0` keeps it.
|
|
389
|
+
|
|
384
390
|
Fields do not generate setters or collection mutators. Define only the named
|
|
385
391
|
handlers your view/API needs; the linter reports a field/method name collision.
|
|
386
392
|
|
|
@@ -915,7 +921,7 @@ storybook in your own page — see [storybook.md](./storybook.md).
|
|
|
915
921
|
- [margaui.md](./margaui.md) — setting up MargaUI styling: install
|
|
916
922
|
(CDN / npm / vendoring), theme CSS, and `compileClassesToStyleText`.
|
|
917
923
|
- [semantics.md](./semantics.md) — runtime semantics: path steps, the
|
|
918
|
-
transaction lifecycle,
|
|
924
|
+
transaction lifecycle, located continuations, and async key pinning
|
|
919
925
|
(`livePath`).
|
|
920
926
|
- [testing.md](./testing.md) — `getTests` shape and the handler calling
|
|
921
927
|
convention for tests.
|
|
@@ -409,7 +409,7 @@ rules are in [semantics.md](./semantics.md).
|
|
|
409
409
|
blocks overview, and *Conventional Module Exports*.
|
|
410
410
|
- [component-design.md](./component-design.md) — which channel to reach for when.
|
|
411
411
|
- [semantics.md](./semantics.md) — the path/transaction model behind these
|
|
412
|
-
channels: path steps, the transaction lifecycle,
|
|
412
|
+
channels: path steps, the transaction lifecycle, continuation frames, and the
|
|
413
413
|
key-pinning rules `livePath` toggles.
|
|
414
414
|
- [testing.md](./testing.md) — driving message and intent flows from tests.
|
|
415
415
|
- [cli.md](./cli.md) — the full linter rule list, exit codes, and
|
|
@@ -26,7 +26,7 @@ task.
|
|
|
26
26
|
## Context & dynamic bindings
|
|
27
27
|
|
|
28
28
|
- [Share state across the tree](share-state-across-the-tree.md) — `provide` / `lookup` and reading `*name`.
|
|
29
|
-
- [Edit through a dynamic target](edit-through-a-dynamic-target.md) — render `*name` and
|
|
29
|
+
- [Edit through a dynamic target](edit-through-a-dynamic-target.md) — render `*name` and edit the owner's value in place.
|
|
30
30
|
|
|
31
31
|
## Composition
|
|
32
32
|
|
|
@@ -19,9 +19,14 @@ const Toolbar = component({
|
|
|
19
19
|
});
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Because `*active`
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
Because `*active` carries a real **path** alongside its value (not a copy),
|
|
23
|
+
rendering it *resumes* there: the render site pushes `Workspace.sheet` as the
|
|
24
|
+
active position, so an event fired inside the rendered child mutates
|
|
25
|
+
`Workspace.sheet` itself, and the owner and any other view of the same value
|
|
26
|
+
update in lock-step. Bubbling still returns to the `Toolbar` that wrote the
|
|
27
|
+
`*active`, so an unhandled message keeps walking the visual ancestry.
|
|
28
|
+
|
|
29
|
+
A `provide` can point at a seq-access (`.items[.selectedKey]`) to expose "the
|
|
30
|
+
selected item", and a nested provider of the same name shadows an outer one for
|
|
31
|
+
its own subtree. This is the **edit** counterpart of the
|
|
32
|
+
share-state-across-the-tree recipe.
|
|
@@ -19,7 +19,7 @@ const Producer = component({
|
|
|
19
19
|
provide: { entries: ".items" },
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
// consumer —
|
|
22
|
+
// consumer — names what it WANTS, never who provides it
|
|
23
23
|
const Consumer = component({
|
|
24
24
|
name: "Selector",
|
|
25
25
|
lookup: [{ name: "entries", default: ".items" }],
|
|
@@ -32,7 +32,22 @@ const Consumer = component({
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
`provide` publishes a field under a name; a descendant's `lookup` resolves
|
|
35
|
-
`*name` to the nearest matching producer
|
|
36
|
-
|
|
35
|
+
`*name` to the nearest matching producer in the live render ancestry, falling
|
|
36
|
+
back to `default` when none is above it. Several components may publish one
|
|
37
|
+
name — the nearest rendered one wins. `*name` works wherever a `.field` does
|
|
38
|
+
for iteration/rendering.
|
|
39
|
+
|
|
40
|
+
For a value nothing in the tree should have to publish — a session, a theme —
|
|
41
|
+
register it on the scope instead, as an absolute path from the state root:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { path } from "tutuca";
|
|
45
|
+
|
|
46
|
+
app.registerComponents(comps, { paths: { theme: path().field("theme") } });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Consumers still declare `lookup: ["theme"]` and read `*theme`; a rendered
|
|
50
|
+
provider of the same name still wins over it.
|
|
51
|
+
|
|
37
52
|
This is the **read** side; to edit the producer's value through the dynamic,
|
|
38
53
|
see the edit-through-a-dynamic-target recipe.
|
|
@@ -31,26 +31,29 @@ references* in [core.md](./core.md)). The step kinds:
|
|
|
31
31
|
| `SeqStep` | a sequence entry by **literal** key/index | `.items[2]` |
|
|
32
32
|
| `SeqAccessStep` | a sequence entry whose key is **read from another field** | `.sheets[.selId]` |
|
|
33
33
|
| `EachRenderItStep` | an iterated `render-it` item | `<x render-it>` per iter |
|
|
34
|
-
| `DynStep` / `DynEachStep` | a dynamic-var (`*x`) render target — a teleport marker | `<x render="*x">` |
|
|
35
34
|
| `BindStep` / `EachBindStep` | nothing — frame-only (carry scope binds, no addressing) | `@each`, `@enrich-with` |
|
|
36
35
|
|
|
37
36
|
`SeqAccessStep` is the important one for async correctness: it stores the
|
|
38
37
|
field *names* `seqField` and `keyField`, and resolves the key from the
|
|
39
38
|
live data each time it runs — see *Key resolution & async races* below.
|
|
40
39
|
|
|
41
|
-
###
|
|
40
|
+
### A dispatch path is a stack of continuation frames
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
What reconstruction actually returns is a `DispatchPath`: a list of frames,
|
|
43
|
+
each `{ base, items }`. Ordinary rendering extends the top frame's `items`;
|
|
44
|
+
rendering a located binding (`<x render="*sel">`) pushes a NEW frame based
|
|
45
|
+
at the value's own absolute path — see *Rendering with a resumed path*
|
|
46
|
+
below. Two projections come off it:
|
|
44
47
|
|
|
45
|
-
- **`compact()` → the dispatch path.** Drops frame-only steps
|
|
46
|
-
step per crossed component
|
|
47
|
-
walks every component
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
real location.
|
|
53
|
-
`lookup` / `setValue` to read and write state.
|
|
48
|
+
- **`compact()` → the dispatch path.** Drops frame-only steps inside every
|
|
49
|
+
frame independently, keeping one step per crossed component. `popStep()`
|
|
50
|
+
over it walks every component, and at the top of a frame returns to the
|
|
51
|
+
visual caller. Used to drive `ctx.send` / `ctx.intent` and to locate
|
|
52
|
+
handlers.
|
|
53
|
+
- **`toTransactionPath()` → the transaction path.** The ACTIVE frame alone:
|
|
54
|
+
its absolute base followed by its own steps, so a mutation lands on the
|
|
55
|
+
data's real location. The saved caller frames affect bubbling only. Used
|
|
56
|
+
by `lookup` / `setValue` to read and write state.
|
|
54
57
|
|
|
55
58
|
## Reconstructing a path from the DOM
|
|
56
59
|
|
|
@@ -111,21 +114,32 @@ The walk object is shared across hops on purpose: that is what makes the
|
|
|
111
114
|
one-shot **per intent** rather than per hop, so a second `ctx.reply` — from this
|
|
112
115
|
handler or one three hops up — finds the walk already ended.
|
|
113
116
|
|
|
114
|
-
##
|
|
117
|
+
## Rendering with a resumed path
|
|
115
118
|
|
|
116
119
|
A component rendered through `<x render="*sel">` *physically lives* at the
|
|
117
120
|
producer that declared `provide: { sel: … }`, not under the consumer that
|
|
118
|
-
wrote the render.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
the
|
|
128
|
-
|
|
121
|
+
wrote the render. A provider evaluates both halves of a lowercase binding:
|
|
122
|
+
its value, and the absolute path of that value. A descendant retrieves the
|
|
123
|
+
nearest pair and renders the value after pushing its path as a continuation
|
|
124
|
+
frame; the frame's base is recorded in the DOM (`base` in the `§Comp§` meta,
|
|
125
|
+
`data-rp` on a fragment-rooted body) so reconstruction can rebuild it.
|
|
126
|
+
|
|
127
|
+
Consequently:
|
|
128
|
+
|
|
129
|
+
- mutation uses the ACTIVE frame, so an event inside the resumed component
|
|
130
|
+
updates the provider's data, and the consumer's view of it updates in
|
|
131
|
+
lock-step;
|
|
132
|
+
- bubbling reaches the top of that frame and pops directly back to the
|
|
133
|
+
visual caller, then continues through the caller's ancestry.
|
|
134
|
+
|
|
135
|
+
There is no producer search, producer id, interior list, or path rewriting
|
|
136
|
+
at dispatch. Nested providers therefore shadow by live render ancestry: the
|
|
137
|
+
nearest one that actually rendered wins. Authoring view: *Resuming at the
|
|
138
|
+
value's path* in [advanced.md](./advanced.md).
|
|
139
|
+
|
|
140
|
+
When the producer's `provide` value is a seq-access (`.sheets[.selId]`), the
|
|
141
|
+
frame's base ends in a `SeqAccessStep` — which is where async key races come
|
|
142
|
+
from.
|
|
129
143
|
|
|
130
144
|
## Key resolution & async races
|
|
131
145
|
|
|
@@ -143,8 +157,9 @@ path — each `SeqAccessStep(seq, keyField)` becomes a literal
|
|
|
143
157
|
`SeqStep(seq, resolvedKey)`. The pinned path is stored on the
|
|
144
158
|
`IntentWalk`, so the answer updates the item that raised the intent
|
|
145
159
|
regardless of later key changes. (Pinning runs on the transaction path,
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
because the `SeqAccessStep` may have come from a resumed frame's base. The
|
|
161
|
+
answer still DISPATCHES at the raiser's position, frames and all; the pinned
|
|
162
|
+
path only decides where its mutation lands.)
|
|
148
163
|
|
|
149
164
|
**Opt out per intent with `livePath: true`:**
|
|
150
165
|
|
|
@@ -191,5 +206,5 @@ things per step kind:
|
|
|
191
206
|
- [messages-and-intents.md](./messages-and-intents.md) — the dispatch **API**:
|
|
192
207
|
addressed `send`-`receive` vs routed `intent`, `ctx.at`, `$unknown`,
|
|
193
208
|
intent-handler registration, and the `livePath` option.
|
|
194
|
-
- [advanced.md](./advanced.md) — dynamic bindings (`*x`)
|
|
195
|
-
view of
|
|
209
|
+
- [advanced.md](./advanced.md) — dynamic bindings (`*x`), lexically
|
|
210
|
+
registered paths, and the authoring view of resuming at a value's path.
|