tutuca 0.9.45 → 0.9.46

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tutuca",
3
- "version": "0.9.45",
3
+ "version": "0.9.46",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency SPA framework with immutable state and virtual DOM",
6
6
  "main": "./dist/tutuca.js",
@@ -148,3 +148,18 @@ with `injectCss(scopeName, css)` to install the result before `start()`.
148
148
  If a margaui skill is available, load it alongside this one when
149
149
  authoring class lists — it lists the available components and their
150
150
  canonical class strings, which is what the `compile` step expects.
151
+
152
+ **Pitfall: `@if.class` payloads are invisible to the scanner.** Classes
153
+ inside `@then` / `@else` (e.g. `@if.class=".active" @then="'btn-success'"
154
+ @else="'btn-ghost'"`) are not literals in `class=` / `:class=`, so
155
+ `compileClassesToStyleText` skips them and the margaui CSS for those
156
+ classes is never emitted — the conditional class renders unstyled.
157
+ Workaround: add a hidden "decoy" view on the component that lists every
158
+ conditional class as a real literal, so the walker picks them up:
159
+
160
+ ```js
161
+ _margauiClasses: html`<p class="btn-success btn-ghost on off"></p>`,
162
+ ```
163
+
164
+ The view does not need to be rendered anywhere — registration is enough
165
+ for the template walker to find it.
@@ -4,7 +4,7 @@ The `tutuca` CLI inspects, documents, lints, tests, and renders any
4
4
  module that follows the *Conventional Module Exports* shape (see
5
5
  `core.md`). Reach this file when you need command/flag/exit-code
6
6
  details, or when reading a lint code out of `lint` output. Otherwise
7
- the post-edit recipe in `core.md` (run `lint`, then `test` for
7
+ *Verifying changes* in `core.md` (run `lint`, then `test` for
8
8
  behavior changes, then `render --title "<your example>"`) is enough.
9
9
 
10
10
  ## Install / invoke
@@ -83,11 +83,7 @@ Full reference: [cli.md](./cli.md).
83
83
  newline / indent before the first element renders blank silently.
84
84
  Use `view: html\`<el ...>` (or `html\`<el<newline> attr<newline>>...`),
85
85
  never `view: html\`<newline> <el ...>`. Same applies to macro bodies.
86
- - **Macro registry keys are normalized to lowercase.** The HTML parser
87
- lowercases custom tag names, so `<x:Card>` is read as `<x:card>`.
88
- `registerMacros({ Card })` is fine — the key is stored as `card`.
89
- Registering two different macros under the same lowercased name (e.g.
90
- `Card` and `card`) warns via `console.assert`; the later one wins.
86
+ - **Macro registry keys are lowercased.** `<x:Card>` becomes `<x:card>` see *Macros*.
91
87
 
92
88
  ## Bootstrap
93
89
 
@@ -114,7 +110,63 @@ app.start();
114
110
  `app.onChange((info) => ...)` fires after every state change with
115
111
  `{ val, old, info, timestamp }` (logging, persistence). `app.stop()`
116
112
  removes all listeners and cancels cache eviction; pair with
117
- `app.start()` for teardown in tests or SPA navigation.
113
+ `app.start()` to remount cleanly in tests or SPA navigation.
114
+
115
+ ## Mental model
116
+
117
+ Tutuca rests on three invariants: the application state is a single
118
+ immutable root value; the view is a pure function of it; every handler
119
+ takes the old self and returns a new self. The transactor swaps the
120
+ root atomically. Identity-based caching, time-travel-style debugging,
121
+ and the entire dispatch model fall out of these three properties.
122
+
123
+ **The value tree.** Components are nested immutable Records. Children
124
+ live in fields — a list of `Item`, a map of `User`, a scalar `count`.
125
+ "Updating a deep child" means producing a new root that shares
126
+ structure with the old one along the unchanged spine; the renderer
127
+ keys its cache on `===` identity, so unchanged subtrees skip work.
128
+ Every value carries a hidden tag back to its component class, so the
129
+ runtime never needs `instanceof` — it asks the value what it is.
130
+
131
+ **Stack: frames vs scopes.** As the renderer walks the AST it pushes
132
+ `BindFrame`s. A *frame* is a barrier: name lookups (`@x`) stop at it,
133
+ so a child component view sees a clean namespace. A *scope* is
134
+ transparent: iteration `key` / `value` and `@enrich-with` binds layer
135
+ onto the surrounding frame and remain visible to handlers attached to
136
+ the same iteration. `it` (the target of `.field` lookups) is set on
137
+ both.
138
+
139
+ | pushed by | kind | shape |
140
+ | ----------------------------------- | ----- | ------------------------------------ |
141
+ | `<x render=".f">` / `<x render-it>` | frame | `it` = child, fresh binds |
142
+ | `<x render-each>` per iter | frame | `it` = item, binds `{ key }` |
143
+ | `<div @each>` per iter | scope | `it` = item, binds `{ key, value }` |
144
+ | `<x:scope @enrich-with=…>` | scope | `it` unchanged, binds = alter result |
145
+
146
+ For full mechanics see *List Iteration* and *Scope Enrichment*.
147
+ This is why a handler attached to `<div @each>` runs against the
148
+ *parent* component (the scope is transparent — the surrounding frame
149
+ still owns dispatch), while one inside `<x render-it>` runs against
150
+ the *item* (render-it pushed a fresh frame for the child).
151
+
152
+ **Paths, not references.** The DOM is the only thing that survives
153
+ between render and click, so the renderer leaves breadcrumbs:
154
+ `data-cid` / `data-nid` / `data-eid` on rendered elements, and `§…§`
155
+ HTML comments adjacent to iteration entries. On a DOM event the
156
+ runtime walks from the target up to the root, reads those breadcrumbs,
157
+ and rebuilds a *positional* `Path` — an array of steps from the root
158
+ to the value the handler should run against. The same `Path` is reused
159
+ verbatim for `ctx.send`, `ctx.bubble`, and `ctx.request` /
160
+ response: because it's positional rather than a captured reference, an
161
+ async response still lands at the right slot even after intervening
162
+ transactions have rebuilt the root. See *Bubble Events*, *Send /
163
+ Receive*, *Async Requests* for the dispatch APIs.
164
+
165
+ **Why `alter` is its own table.** Alter handlers are pure, evaluated
166
+ on every render, and produce binds (no state change). `input` /
167
+ `receive` / `bubble` / `response` are transactional and produce new
168
+ values. Same lookup mechanism, different contracts — keep them
169
+ separate.
118
170
 
119
171
  ## Notation Reference
120
172
 
@@ -330,7 +382,7 @@ consequences:
330
382
  custom elements with kebab-case attributes plus lowercased property
331
383
  setters (or aliases), and bind via `:kebab-name` from Tutuca templates.
332
384
  - Macro registry keys are lowercased on insert for the same reason
333
- (see "Macros" section below).
385
+ (see *Macros* below).
334
386
 
335
387
  ## Event Handling
336
388
 
@@ -398,7 +450,7 @@ fails. `:mapId=".id"` does *not* invoke a `set mapId` setter
398
450
  — the HTML parser lowercased the attribute name, so the framework assigns
399
451
  to `node.mapid` instead, creating an own property and bypassing the
400
452
  setter. Use kebab-case attributes / lowercased setters when authoring
401
- custom elements for use with Tutuca. See "Attribute Binding" above.
453
+ custom elements for use with Tutuca. See *Attribute Binding* above.
402
454
 
403
455
  ## Conditional Display
404
456
 
@@ -590,6 +642,10 @@ Every handler is called as `handler(...args, ctx)` and returns a
590
642
  returned value into the dispatch path. The four sections below cover
591
643
  each channel in turn.
592
644
 
645
+ `alter` is a fifth handler block, but unlike the four above it isn't
646
+ event-triggered — the renderer invokes alter handlers to produce
647
+ binds, not to update state. See *Mental model* and *Scope Enrichment*.
648
+
593
649
  ## Bubble Events
594
650
 
595
651
  ```js
@@ -643,7 +699,8 @@ ctx.bubble("name", [arg]); // bubble up
643
699
  `ctx.at` returns a `PathBuilder` with `.field(name)`, `.index(name, i)`,
644
700
  and `.key(name, k)`. Each call appends a step to the path before
645
701
  `.send(...)` / `.bubble(...)` fires; the handler runs inside the child
646
- instance with `this` bound to it.
702
+ instance with `this` bound to it. Paths are positional, not references —
703
+ see *Mental model* for why this matters across async boundaries.
647
704
 
648
705
  When to send: bubble emits an *event* that any ancestor with a
649
706
  matching handler can observe; send delivers a *message* to one
@@ -810,4 +867,5 @@ export function getExamples() {
810
867
  items: [{ title, description, value, view }], // value = Comp.make(...)
811
868
  };
812
869
  }
870
+ export function getTests({ describe, test, expect }) { /*...*/ } // optional — see cli.md
813
871
  ```