tutuca 0.9.44 → 0.9.45

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.44",
3
+ "version": "0.9.45",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency SPA framework with immutable state and virtual DOM",
6
6
  "main": "./dist/tutuca.js",
@@ -33,7 +33,7 @@
33
33
  "fix": "bunx @biomejs/biome check --write src test/*.js",
34
34
  "tutuca": "bun tools/tutuca.js",
35
35
  "stresstest": "bun scripts/stresstest.js",
36
- "smoke-test": "bun tools/tutuca.js ./test/todo.js lint && bun tools/tutuca.js ./test/todo.js render && bun tools/tutuca.js ./test/json.js lint && bun tools/tutuca.js ./test/json.js render"
36
+ "smoke-test": "bun tools/tutuca.js ./test/todo.js lint && bun tools/tutuca.js ./test/todo.js render && bun tools/tutuca.js ./test/todo.js test && bun tools/tutuca.js ./test/json.js lint && bun tools/tutuca.js ./test/json.js render"
37
37
  },
38
38
  "sideEffects": false,
39
39
  "files": [
@@ -47,6 +47,7 @@
47
47
  "skill"
48
48
  ],
49
49
  "dependencies": {
50
+ "chai": "^6.2.2",
50
51
  "jsdom": "^28.0.0 || ^29.0.0"
51
52
  },
52
53
  "peerDependencies": {
@@ -145,7 +145,6 @@ collects the `class=` and `:class=` literals, hands them to a `compile`
145
145
  function (any margaui-compatible signature), and returns CSS text. Pair
146
146
  with `injectCss(scopeName, css)` to install the result before `start()`.
147
147
 
148
- If a margaui skill is installed in this project (e.g.
149
- `.claude/skills/margaui/`) load it alongside this one when authoring
150
- class lists it lists the available components and their canonical
151
- class strings, which is what the `compile` step expects.
148
+ If a margaui skill is available, load it alongside this one when
149
+ authoring class lists — it lists the available components and their
150
+ canonical class strings, which is what the `compile` step expects.
@@ -1,11 +1,11 @@
1
1
  # Tutuca CLI Reference
2
2
 
3
- The `tutuca` CLI inspects, documents, lints, and renders any module that
4
- follows the *Conventional Module Exports* shape (see `core.md`). Reach
5
- this file when you need command/flag/exit-code details, or when reading
6
- a lint code out of `lint` output. Otherwise the post-edit recipe in
7
- `core.md` (run `lint`, then `render --title "<your example>"`) is
8
- enough.
3
+ The `tutuca` CLI inspects, documents, lints, tests, and renders any
4
+ module that follows the *Conventional Module Exports* shape (see
5
+ `core.md`). Reach this file when you need command/flag/exit-code
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
8
+ behavior changes, then `render --title "<your example>"`) is enough.
9
9
 
10
10
  ## Install / invoke
11
11
 
@@ -36,6 +36,7 @@ prints a one-liner. `tutuca` ↔ `tutuca -h` prints overview.
36
36
  | `docs [name]` | Generate API docs (methods, input handlers, fields with auto-generated accessors) — all or one |
37
37
  | `lint [name]` | Run the linter; exits **2** on any error-level finding |
38
38
  | `render [name]` | Render examples to HTML in a headless DOM. Filter by component name or `--title`/`--view`. Exits **3** on render crash |
39
+ | `test [name]` | Run tests defined by `getTests({ describe, test, expect })`. Filter by component name, `--grep <pattern>`, or `--bail`. Exits **4** on any failure |
39
40
  | `help [cmd]` | Show usage; the only command that does **not** need a module path |
40
41
 
41
42
  ## Global flags
@@ -61,6 +62,7 @@ prints a one-liner. `tutuca` ↔ `tutuca -h` prints overview.
61
62
  | `1` | usage error (bad args, missing module, bad module shape) |
62
63
  | `2` | lint findings at error level |
63
64
  | `3` | render crash |
65
+ | `4` | one or more tests failed |
64
66
 
65
67
  ## Examples
66
68
 
@@ -77,8 +79,77 @@ tutuca ./src/components.js render Button --title "Disabled state"
77
79
  tutuca ./src/components.js lint
78
80
  tutuca ./src/components.js render --title "Disabled state"
79
81
  tutuca ./src/components.js render --title "Disabled state" --pretty
82
+
83
+ # Component-behavior verification: run the suite for one component, or
84
+ # narrow further with --grep. Add tests next to the component (the
85
+ # getTests() export) when the change isn't observable from render alone.
86
+ tutuca ./src/components.js test Counter
87
+ tutuca ./src/components.js test Counter --grep "inc()"
88
+ tutuca ./src/components.js test --bail
89
+ ```
90
+
91
+ ## `test` — running component tests
92
+
93
+ Use `test` after edits that change attributes, instance methods, input
94
+ handlers, or static factories — anything observable from JS rather than
95
+ from rendered HTML. The module opts in by exporting
96
+ `getTests({ describe, test, expect })`:
97
+
98
+ - `describe(Component, fn)` tags the suite with `Component.name` so
99
+ the positional `[name]` filter can pick it.
100
+ - `describe(title, fn)` is untagged; reachable only via `--grep`.
101
+ - `describe(title, { component }, fn)` tags an explicit title with a
102
+ custom component name.
103
+ - `test(title, fn)` — `fn` may be async; assertions use the injected
104
+ chai `expect`.
105
+
106
+ Filters:
107
+
108
+ - `[name]` — only tests whose tagged `componentName` equals `<name>`.
109
+ - `--grep <p>` — substring match against the full path
110
+ (e.g. `"Counter > inc() > works on a negative counter"`).
111
+ - `--bail` — stop on first failure; remaining tests reported as `skip`.
112
+
113
+ Default format is `cli` (a tree with ✓/✗/○ and per-test durations);
114
+ `-f md` and `-f json` work too.
115
+
116
+ A worked `getTests()` export covering methods, input handlers (called
117
+ via `Comp.input.x.call(inst)`), and immutability:
118
+
119
+ ```js
120
+ export function getTests({ describe, test, expect }) {
121
+ describe(Counter, () => {
122
+ describe("inc()", () => { // method
123
+ test("returns a Counter with count + 1", () => {
124
+ const next = Counter.make().inc();
125
+ expect(next).to.be.instanceOf(Counter.Class);
126
+ expect(next.count).to.equal(1);
127
+ });
128
+ test("does not mutate the original instance", () => {
129
+ const c = Counter.make({ count: 7 });
130
+ c.inc();
131
+ expect(c.count).to.equal(7); // immutability
132
+ });
133
+ });
134
+
135
+ describe("dec()", () => { // input handler
136
+ test("returns a Counter with count - 1", () => {
137
+ const next = Counter.input.dec.call(Counter.make());
138
+ expect(next.count).to.equal(-1);
139
+ });
140
+ });
141
+
142
+ test("inc and dec round-trip", () => { // untagged path
143
+ expect(Counter.input.dec.call(Counter.make().inc()).count).to.equal(0);
144
+ });
145
+ });
146
+ }
80
147
  ```
81
148
 
149
+ `describe(Counter, fn)` auto-tags the suite path with `Counter.name`, so
150
+ `tutuca <module> test Counter` picks it up. Untagged `test(...)` at the
151
+ top of a tagged `describe` inherits the tag.
152
+
82
153
  ## Install skill assets
83
154
 
84
155
  `tutuca install-skill` copies bundled Claude Code skill files into
@@ -100,7 +171,7 @@ tutuca install-skill --all --force # overwrite existing files
100
171
 
101
172
  ## Linter Rules
102
173
 
103
- Codes emitted by `lint`. Source: `tools/core/lint-check.js`.
174
+ Codes emitted by `lint`.
104
175
 
105
176
  ### Field references
106
177
 
@@ -15,8 +15,8 @@ the `tutuca` CLI.
15
15
 
16
16
  ## Verifying changes
17
17
 
18
- After editing a Tutuca module, run two checks before declaring the edit
19
- done:
18
+ After editing a Tutuca module, run these checks before declaring the
19
+ edit done:
20
20
 
21
21
  1. **Lint the module** — catches undefined fields/handlers/macros/events
22
22
  (all the `*_NOT_DEFINED` / `*_NOT_REFERENCED` codes):
@@ -26,7 +26,21 @@ done:
26
26
  Exits `2` on any error-level finding. Pass a component name to scope
27
27
  it: `tutuca <module-path> lint Button`.
28
28
 
29
- 2. **Render the example(s) that exercise the feature you changed** —
29
+ 2. **Test component behavior** when the edit changes attributes,
30
+ instance methods, input handlers, or static factories (anything
31
+ observable from JS, not just the rendered HTML), run the test
32
+ suite. The module opts in by exporting
33
+ `getTests({ describe, test, expect })`:
34
+
35
+ tutuca <module-path> test
36
+ tutuca <module-path> test Counter # one component
37
+ tutuca <module-path> test --grep "inc()" # one path
38
+
39
+ Exits `4` on any failure. Skip this step when the change is purely
40
+ templates/styling — `render` already covers that. Full reference,
41
+ including a worked `getTests()` export, in [cli.md](./cli.md).
42
+
43
+ 3. **Render the example(s) that exercise the feature you changed** —
30
44
  confirms the component actually mounts in a headless DOM with the new
31
45
  behavior. Pick the example whose `title` matches the feature, or
32
46
  filter by component:
@@ -775,9 +789,8 @@ import {
775
789
  ```
776
790
 
777
791
  Because every `immutable` export is reachable through `tutuca`, if an
778
- `immutable-js` skill is installed in this project (e.g.
779
- `.claude/skills/immutable-js/`) load it alongside this one its guidance
780
- applies directly to the values you'll be reading and writing.
792
+ `immutable-js` skill is available, load it alongside this one — its
793
+ guidance applies directly to the values you'll be reading and writing.
781
794
 
782
795
  ## Conventional Module Exports
783
796
 
package/skill/SKILL.md DELETED
@@ -1,46 +0,0 @@
1
- ---
2
- name: tutuca
3
- description: Authoring or reviewing tutuca components, html`` views, macros, or running the `tutuca` CLI. Covers field types, @-directives, bubble/receive/response handlers, and the post-edit `tutuca <module> lint` + `tutuca <module> render --title …` verification recipe.
4
- ---
5
-
6
- # Tutuca
7
-
8
- Tutuca is an immutable-state SPA framework: components have typed
9
- `fields`, auto-generated mutators (`setX`, `pushInX`, …), HTML-template
10
- `view`s with `@`-prefixed directives, and `bubble` / `receive` /
11
- `response` handlers for orchestration.
12
-
13
- ## Verifying changes
14
-
15
- After editing a tutuca module, run two checks before declaring the edit
16
- done:
17
-
18
- 1. **Lint** — catches undefined fields/handlers/macros/events. Exits
19
- `2` on any error-level finding.
20
-
21
- ```sh
22
- tutuca <module-path> lint
23
- ```
24
-
25
- 2. **Render the example that exercises the feature you changed** —
26
- confirms the component mounts in a headless DOM with the new
27
- behavior. Exits `3` on render crash.
28
-
29
- ```sh
30
- tutuca <module-path> render --title "<example title>"
31
- ```
32
-
33
- If no example covers the feature, add one to `getExamples()` first —
34
- that's how the feature becomes verifiable.
35
-
36
- ## Routing
37
-
38
- | Task | File |
39
- | ---------------------------------------------------------------------------------------------- | ------------------------------- |
40
- | Authoring `component({...})`, `html\`...\`` views, macros, fields, events, lists, styles | [core.md](./core.md) |
41
- | CLI commands, flags, exit codes, full linter rule list | [cli.md](./cli.md) |
42
- | Drag & drop, dynamic bindings (`*x`), pseudo-`x`, custom seq types, Tailwind/MargaUI | [advanced.md](./advanced.md) |
43
-
44
- Read `core.md` first. Reach for `cli.md` or `advanced.md` only when the
45
- task touches them — both files are referenced inline from `core.md` so
46
- you'll be pointed there when relevant.
package/skill/advanced.md DELETED
@@ -1,146 +0,0 @@
1
- # Tutuca — Advanced Topics
2
-
3
- Reach this file only when the task touches drag & drop, context-style
4
- "dynamic bindings", pseudo-`x` (the `<x>`-stripping workaround inside
5
- `<select>`/`<table>`/`<tr>`), registering a custom seq type, or
6
- compiling Tailwind / MargaUI classes. For everything else, `core.md`
7
- is the right place.
8
-
9
- ## Drag and Drop
10
-
11
- ```html
12
- <div
13
- @each=".items"
14
- draggable="true"
15
- data-dragtype="my-item"
16
- data-droptarget="my-item"
17
- @on.drop="onDrop @key dragInfo event"
18
- ></div>
19
- ```
20
-
21
- ```js
22
- input: {
23
- onDrop(targetKey, dragInfo, e) {
24
- const sourceKey = dragInfo.lookupBind("key"); // any bind from source render
25
- return this.setItems(this.items.moveKeyBeforeKey(sourceKey, targetKey));
26
- },
27
- }
28
- ```
29
-
30
- Tutuca auto-manages two attrs during a drag — style them with CSS:
31
-
32
- ```css
33
- [data-dragging="1"] {
34
- opacity: 0.5;
35
- }
36
- [data-draggingover="my-item"] {
37
- outline: 1px dashed;
38
- }
39
- ```
40
-
41
- Touch is wired up too (drag fires after a small move threshold).
42
-
43
- ## Dynamic Bindings
44
-
45
- For passing values "context-style" through nested components without prop
46
- drilling. Define on the producer; alias on consumers; resolve as `*name`.
47
-
48
- ```js
49
- const Theme = component({
50
- name: "Theme",
51
- fields: { color: "blue" },
52
- dynamic: { color: ".color" },
53
- on: {
54
- stackEnter() {
55
- return ["color"];
56
- },
57
- },
58
- });
59
- const Child = component({
60
- dynamic: { color: { for: "Theme.color", default: "'gray'" } },
61
- view: html`<p :style="color: {*color}"></p>`,
62
- });
63
- ```
64
-
65
- `on.stackEnter()` is required only on the **producer** (the component
66
- declaring `dynamic: { name: ".field" }` to expose a value). It returns
67
- the list of dynamic-binding names this component pushes onto the stack
68
- when entering its render. Consumers (which only alias via
69
- `{ for: "Producer.name", default: ... }`) don't need it.
70
-
71
- ## Pseudo-`x` (`@x`)
72
-
73
- Tutuca's special operations (`render`, `render-it`, `render-each`, `text`,
74
- `show`, `hide`, `slot`) live on the `<x>` tag. That works almost
75
- everywhere, but the browser's HTML parser refuses to keep `<x>` (or any
76
- unknown tag) as a child of certain elements: `<select>` only allows
77
- `<option>` / `<optgroup>`, `<table>` only allows `<thead>` / `<tbody>` /
78
- `<tr>`, `<tr>` only allows `<th>` / `<td>`, etc. Drop `<x render-each>`
79
- inside one of those and the parser silently strips it.
80
-
81
- The escape hatch: prefix the **first** attribute on a *legal* tag with
82
- `@x`. Tutuca treats that tag as if it were `<x>` and reads the next
83
- attribute as the special op.
84
-
85
- ```html
86
- <!-- ❌ <x> stripped by the HTML parser inside <select> -->
87
- <select>
88
- <x render-each=".items" as="option"></x>
89
- </select>
90
-
91
- <!-- ✅ pseudo-x: <option @x render-each=".items" as="option"> -->
92
- <select>
93
- <option @x render-each=".items" as="option"></option>
94
- </select>
95
- ```
96
-
97
- Notes:
98
-
99
- - `@x` must be the **first** attribute; the special op (`render-each`,
100
- `render`, `text`, `show`, ...) is the second.
101
- - The host tag (here `<option>`) is otherwise ignored — only the special
102
- op runs. Tutuca produces the rendered children directly.
103
- - Same trick works inside `<tr>`, `<table>`, `<colgroup>`, `<dl>`,
104
- `<details>`, or anywhere else the parser would discard `<x>`.
105
-
106
- ## Registering a custom seq type
107
-
108
- To make `@each` recognize your own collection class, install a
109
- `SEQ_INFO` walker on its prototype:
110
-
111
- ```js
112
- import { SEQ_INFO } from "tutuca";
113
-
114
- class MyClass {
115
- // ...
116
- }
117
- MyClass.prototype[SEQ_INFO] = (seq, visit) => {
118
- for (const [k, v] of seq.entries()) visit(k, v, "data-sk");
119
- };
120
- ```
121
-
122
- `SEQ_INFO` is `Symbol.for("tutuca.seqInfo")`, so the same identity
123
- is shared across module graphs (source vs. bundled tutuca). The
124
- renderer reads `seq[SEQ_INFO]` directly (no `.constructor` lookup),
125
- which is why the walker goes on the prototype, not as a static.
126
-
127
- The third arg to `visit` is the data attribute used for stable-key
128
- diffing (typically `"data-sk"` for "sequence key").
129
-
130
- ## Tailwind / MargaUI Class Compilation (extra build)
131
-
132
- ```js
133
- import { compileClassesToStyleText, injectCss, tutuca } from "tutuca/extra";
134
- import { compile } from "https://esm.sh/margaui";
135
-
136
- const app = tutuca("#app");
137
- app.registerComponents([Comp]);
138
- const css = await compileClassesToStyleText(app, compile);
139
- injectCss("myapp", css);
140
- app.start();
141
- ```
142
-
143
- `compileClassesToStyleText` walks every registered component's templates,
144
- collects the `class=` and `:class=` literals, hands them to a `compile`
145
- function (any margaui-compatible signature), and returns CSS text. Pair
146
- with `injectCss(scopeName, css)` to install the result before `start()`.
package/skill/cli.md DELETED
@@ -1,117 +0,0 @@
1
- # Tutuca CLI Reference
2
-
3
- The `tutuca` CLI inspects, documents, lints, and renders any module that
4
- follows the *Conventional Module Exports* shape (see `core.md`). Reach
5
- this file when you need command/flag/exit-code details, or when reading
6
- a lint code out of `lint` output. Otherwise the post-edit recipe in
7
- `core.md` (run `lint`, then `render --title "<your example>"`) is
8
- enough.
9
-
10
- ## Install / invoke
11
-
12
- ```sh
13
- # project local
14
- npm i --save-dev tutuca
15
- npx tutuca <module-path> <command> [name] [flags]
16
-
17
- # global
18
- npm i -g tutuca
19
- tutuca <module-path> <command> [name] [flags]
20
-
21
- # from a checkout of this repo
22
- bun tools/tutuca.js <module-path> <command> [name] [flags]
23
- ```
24
-
25
- The module path comes **first**, the command second, an optional component
26
- name third. `tutuca help` prints the full reference; `tutuca help <command>`
27
- prints a one-liner. `tutuca` ↔ `tutuca -h` prints overview.
28
-
29
- ## Commands
30
-
31
- | Command | Purpose |
32
- | --------------- | ---------------------------------------------------------------------------------------------------------------------- |
33
- | `info` | Summarize which `getX()` exports are present and counts |
34
- | `list` | List components with their views and fields (name + type) |
35
- | `examples` | Print `getExamples()` content (title, items, per section) |
36
- | `docs [name]` | Generate API docs (methods, input handlers, fields with auto-generated accessors) — all or one |
37
- | `lint [name]` | Run the linter; exits **2** on any error-level finding |
38
- | `render [name]` | Render examples to HTML in a headless DOM. Filter by component name or `--title`/`--view`. Exits **3** on render crash |
39
- | `help [cmd]` | Show usage; the only command that does **not** need a module path |
40
-
41
- ## Global flags
42
-
43
- ```
44
- -f, --format <cli|md|json|html> output format
45
- defaults: info/list/examples/lint → cli
46
- docs/render → md
47
- html only valid for render
48
- json works for every command
49
- -o, --output <file> write to file instead of stdout
50
- --pretty pretty-print HTML (md/html) via prettier;
51
- JSON formatter uses indent 2
52
- --module <path> alternative to first-positional module path
53
- -h, --help show help (overview, or for one command)
54
- ```
55
-
56
- ## Exit codes
57
-
58
- | Code | Meaning |
59
- | ---- | -------------------------------------------------------- |
60
- | `0` | success |
61
- | `1` | usage error (bad args, missing module, bad module shape) |
62
- | `2` | lint findings at error level |
63
- | `3` | render crash |
64
-
65
- ## Examples
66
-
67
- ```sh
68
- tutuca ./src/components.js info # quick overview
69
- tutuca ./src/components.js list # components, views, fields
70
- tutuca ./src/components.js docs Button -f json # one component, JSON
71
- tutuca ./src/components.js render -f html --pretty -o out/examples.html
72
- tutuca ./src/components.js render Button --title "Disabled state"
73
-
74
- # Post-edit verification: lint, then render the example for the feature
75
- # you just changed (add the example first if none covers it). Add
76
- # --pretty when you need to read the HTML to verify structure.
77
- tutuca ./src/components.js lint
78
- tutuca ./src/components.js render --title "Disabled state"
79
- tutuca ./src/components.js render --title "Disabled state" --pretty
80
- ```
81
-
82
- ## Linter Rules
83
-
84
- Codes emitted by `lint`. Source: `tools/core/lint-check.js`.
85
-
86
- ### Field references
87
-
88
- - `FIELD_VAL_NOT_DEFINED` — `.field` not declared in `fields`.
89
-
90
- ### Input-handler / method confusion
91
-
92
- - `INPUT_HANDLER_NOT_IMPLEMENTED` — bare handler name not in `input`.
93
- - `INPUT_HANDLER_NOT_REFERENCED` — `input` entry never used.
94
- - `INPUT_HANDLER_METHOD_NOT_IMPLEMENTED` — `.handler` not in `methods`.
95
- - `INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD` — bare name resolves to `methods` (use `.name`).
96
- - `INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER` — `.name` resolves to `input` (drop the dot).
97
-
98
- ### Iteration helpers (`alter`)
99
-
100
- - `ALT_HANDLER_NOT_DEFINED` — `@when` / `@enrich-with` / `@loop-with` name not in `alter`.
101
- - `ALT_HANDLER_NOT_REFERENCED` — `alter` entry never used.
102
-
103
- ### Templates / events
104
-
105
- - `RENDER_IT_OUTSIDE_OF_LOOP` — `<x render-it>` outside `@each` / `render-each`.
106
- - `UNKNOWN_EVENT_MODIFIER` — `+mod` not in the recognized modifier set.
107
- - `UNKNOWN_HANDLER_ARG_NAME` — handler arg name not a built-in / declared component.
108
- - `DUPLICATE_ATTR_DEFINITION` — same attr set by literal + `:attr` + `@if.attr` on one element.
109
- - `UNKNOWN_DIRECTIVE` — `@name` directive not recognized (typo or unsupported).
110
- - `UNKNOWN_X_OP` — first attribute on `<x>` (or pseudo-`@x`) is not a known op.
111
- - `UNKNOWN_X_ATTR` — extra attribute on `<x op>` not consumed by the op and not a known wrapper (`show`/`hide`).
112
-
113
- ### Names registered with the app
114
-
115
- - `UNKNOWN_REQUEST_NAME` — `!name` not registered.
116
- - `UNKNOWN_COMPONENT_NAME` — component type not registered.
117
- - `UNKNOWN_MACRO_ARG` — macro attr not declared in defaults.