micra.js 2.1.0 → 2.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,231 @@
1
+ # Changelog
2
+
3
+ All notable changes to Micra.js will be documented in this file. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows
5
+ [SemVer](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [2.2.0] — 2026-05-27
8
+
9
+ ### Performance — single-pass DOM scan
10
+
11
+ - **Mount cost cut roughly in half.** Internal `applyDirectives`,
12
+ `bindDataOn`, `bindAtEvents`, `bindModels`, `collectRefs`, and
13
+ `renderList` used to walk the DOM 10+ times per render via separate
14
+ `querySelectorAll` calls. They now consume a single pre-computed
15
+ `ScanIndex` built by one `TreeWalker` traversal. The walker
16
+ `FILTER_REJECT`s subtrees rooted at nested `[data-component]` — those
17
+ subtrees aren't even visited.
18
+ - Cross-library benchmark numbers on Firefox 150 / Mac (median of 7 runs):
19
+
20
+ | Scenario | Before | After | Vs Alpine.js | Vs petite-vue |
21
+ | ------------------------- | ------: | --------: | ------------: | ------------: |
22
+ | Mount 100 components | 10.8 ms | **5.6 ms**| × 4.9 faster | × 3.6 faster |
23
+ | Mount 1000 components |128.3 ms |**65.4 ms**| × 7.0 faster | × 2.4 faster |
24
+ | Update 5 of 1000 rows | — | **1 ms** | × 886 faster | × 1002 faster |
25
+ | 10,000 state writes | — | **1 ms** | × 980 faster | × 983 faster |
26
+ | First render 1000 keyed | — | **12 ms** | × 79 faster | × 82 faster |
27
+ | Swap first ↔ last of 1000 | — | **7 ms** | × 131 faster | × 143 faster |
28
+
29
+ Bundle stays at **5.0 KB gzip** — the rewrite removed code, not added it.
30
+
31
+ ### TypeScript — full inference from your component literal
32
+
33
+ - **Method-level type inference.** Both `S` (state shape) and `M`
34
+ (method set) are now inferred from the object literal passed to
35
+ `Micra.define` / `Micra.mount`. Inside method bodies and lifecycle
36
+ hooks **both** `this.state.X` and `this.someMethod()` are fully typed:
37
+
38
+ ```ts
39
+ Micra.define('counter', {
40
+ state: { count: 0 },
41
+ inc() {
42
+ this.state.count++ // ✓ number
43
+ this.dec() // ✓ inferred sibling method
44
+ // this.foo() // ❌ Property 'foo' does not exist
45
+ },
46
+ dec() { this.state.count-- },
47
+ })
48
+ ```
49
+
50
+ - Public `ComponentInstance<S, M>` and `ComponentDefinition<S, M>` now
51
+ take a second generic parameter for methods. `mount()` returns a fully
52
+ typed instance — `inst.inc()` and `inst.state.count` are both checked
53
+ at the call site.
54
+ - New `ComponentMethods` and `ComponentBuiltins` types exported for
55
+ advanced typing.
56
+
57
+ ### Breaking — internal only
58
+
59
+ - The internal directive scan format changed (`DirectiveCache` →
60
+ `ScanIndex`). Internal-only — no public-API change. If you reached
61
+ into internals via deep imports, switch to consuming
62
+ `el.__micraScan` instead of `el.__micraCache`.
63
+
64
+ ## [2.1.0] — 2026-05-25
65
+
66
+ ### Added
67
+
68
+ - **`this.fetch(url, { signal })` now forwards `AbortSignal` to the native
69
+ `fetch()`.** Previously the `signal` option was treated as any other
70
+ GET-option and serialized into the URL as `&signal=[object AbortSignal]`,
71
+ while never reaching the underlying request — so abort silently did
72
+ nothing. After this release:
73
+ - `signal` passes through verbatim to native `fetch()`.
74
+ - `signal` is excluded from the GET-querystring serialization loop.
75
+ - `AbortController#abort()` rejects the in-flight request with an
76
+ `AbortError`, matching native semantics.
77
+ - Enables the canonical search-debounce pattern (drop a stale request when
78
+ a fresher query arrives) without dropping to native `fetch` manually.
79
+ - Migration: none — purely additive, the previous URL-serialization
80
+ behaviour was a bug.
81
+
82
+ ### Tests
83
+
84
+ - 76 new tests for the components and recipes shipped on the docs site
85
+ (14 components + 6 recipes). Total suite: 235 tests across 13 files.
86
+
87
+ ## [2.0.0] — 2026-05-24
88
+
89
+ ### Breaking
90
+
91
+ - **`data-if` now truly unmounts the element from the DOM.** Previously
92
+ `data-if` and `data-show` were aliases — both toggled `style.display`.
93
+ Now `data-if` detaches the element (replacing it with a Comment placeholder)
94
+ when falsy and re-inserts it when truthy. `data-show` keeps the old
95
+ `style.display` behaviour and is the way to express cheap visibility
96
+ toggling.
97
+ - Side effect: `this.refs.X` is `undefined` while the element is detached.
98
+ - DOM listeners on the detached node survive — re-insert preserves identity.
99
+ - `<template data-each>` inside a `data-if=false` subtree is suspended and
100
+ re-renders cleanly when the ancestor returns.
101
+ - **Migration:** if you relied on `data-if` keeping the element in the DOM
102
+ (e.g. you were reading `this.refs.X` while hidden, or animating
103
+ `display` transitions), replace those `data-if` attributes with
104
+ `data-show`.
105
+
106
+ ### Fixed
107
+
108
+ - **`@event` shorthand no longer crosses nested `data-component` boundaries.**
109
+ `bindAtEvents` previously walked all descendants via `queryAll('*')`,
110
+ attaching parent-component handlers to elements owned by a nested child
111
+ component. It now uses `queryOwnAll` like `data-on`/`data-model` already do.
112
+ - **`this.fetch(url, { method: 'POST' })` without a `body` no longer sends
113
+ the options object as the body.** Previously `body` was set to
114
+ `JSON.stringify(options)` (which serialized `{"method":"POST"}` to the
115
+ server). Now the body is omitted unless `options.body` is provided.
116
+
117
+ ### Added
118
+
119
+ - **`queryOwnAll(root, selector)`** in `src/dom/query.ts` — selector variant
120
+ of `queryOwn` for cases where there is no attribute to query by (e.g.
121
+ scanning `*` for `@`-prefixed attribute names).
122
+ - **Recipe: `docs/recipes/sse.md`** — server-sent events pattern using
123
+ `onCreate` + native `EventSource` + `onDestroy` cleanup. No new library
124
+ surface; just the canonical pattern for live data on top of Micra.
125
+
126
+ ### Docs
127
+
128
+ - `docs/directives.md` — full split between `data-if` (unmount) and
129
+ `data-show` (display).
130
+ - `docs/llm-guide.md`, `PROMPT.md`, `llms.txt`, `llms-full.txt` — updated
131
+ the directive table and added a "when to pick which" rule for AI agents.
132
+
133
+ ---
134
+
135
+ ## [1.1.0] — 2026-05-24
136
+
137
+ ### Security
138
+
139
+ - **Directive expressions now shadow non-whitelisted globals.** Identifiers in
140
+ `data-text`, `data-if`, `data-bind`, etc. resolve to state keys, instance
141
+ methods, or one of the whitelisted globals: `Math`, `JSON`, `Date`, `String`,
142
+ `Number`, `Boolean`, `Array`, `Object`, `parseInt`, `parseFloat`, `isNaN`,
143
+ `isFinite`, `NaN`, `Infinity`, `undefined`. Everything else (`window`,
144
+ `document`, `fetch`, `eval`, `setTimeout`, `constructor`, `__proto__`, ...)
145
+ resolves to `undefined`. This blocks the common
146
+ `constructor.constructor("...")()` chain and accidental access to browser
147
+ globals from directive markup. See `docs/directives.md → Security model` for
148
+ the full contract.
149
+ - **`data-html` is now explicitly documented as XSS-prone.** Inline JSDoc
150
+ warning + Security model section in the docs. Sanitize untrusted input on
151
+ the server before binding.
152
+
153
+ ### Fixed
154
+
155
+ - **`destroy()` actually unmounts.** Every DOM listener attached by
156
+ `data-on` / `@event` / `data-model` is now tracked on the instance and
157
+ removed in `destroy()`. Scheduled re-renders after destroy are no-ops.
158
+ Per-element bookkeeping flags are cleared so re-mounting the same DOM
159
+ rebinds cleanly.
160
+ - **Instance methods called from directive expressions now have `this`
161
+ bound to the component.** `data-text="doneCount() + ' done'"` where
162
+ `doneCount` reads `this.state.items` now works as written (previously
163
+ silently returned `undefined` due to `with()` semantics).
164
+ - **`data-model` on focused inputs syncs programmatic state changes.**
165
+ `this.state.q = ''` while the input has focus now clears the field.
166
+ Live typing remains a no-op (state already matches value after the input
167
+ event, so no write happens).
168
+ - **`data-model` on `<input type="number">` / `<input type="range">`
169
+ writes a `number`, not a string.** Empty inputs write `null`. Checkbox
170
+ inputs continue to write booleans.
171
+ - **Duplicate `data-each` keys produce a warning.** Previously rows
172
+ silently collided.
173
+ - **`null` / `undefined` `data-each` keys warn once per render**
174
+ instead of once per item.
175
+ - **`@event` shorthand re-scans the subtree on every render.**
176
+ Replaces the root-level `__micraAtScanned` flag with per-element
177
+ `__micraAtBound`, so `@click` attributes inside markup injected via
178
+ `data-html` get bound on the next render.
179
+ - **`data-bind="class:..."` + `data-class` on the same element now warns**
180
+ via `validateDirectives` — the two directives fight on every render.
181
+ - **`bus.off()` cleans up empty event Sets** instead of leaving them in
182
+ the map.
183
+
184
+ ### Added
185
+
186
+ - **Dev warnings (deduped):**
187
+ - re-entrant `render()` call (typically: a directive expression that
188
+ mutated state) — warns once per instance.
189
+ - runtime errors in directive expressions — warns once per unique
190
+ expression string.
191
+
192
+ ### Changed
193
+
194
+ - **Internal:** `data-bind` and `data-class` specs are pre-parsed once
195
+ into `CachedPairBinding.pairs` in `DirectiveCache` — re-renders skip
196
+ the comma+colon split.
197
+ - **Internal:** `Object.prototype` key membership is pre-computed once
198
+ at module load and cached in a `Set` (faster `safeStateHas`).
199
+ - **Internal:** `data-each` no-key warning is deduped per template
200
+ via `__micraNoKeyWarned`.
201
+
202
+ ### Docs
203
+
204
+ - `docs/directives.md` — new "Security model" section.
205
+ - `docs/llm-guide.md` — Security model, `Micra.off` reference,
206
+ "Things Micra does NOT support" (key modifiers, nested `data-model`,
207
+ `data-if` keeps element in DOM).
208
+ - `docs/examples.md` — inline-edit example now has `data-ref="input"`
209
+ and uses `e.key === 'Enter'` instead of unsupported `@keydown.enter`.
210
+
211
+ ### Bundle size
212
+
213
+ - ~3.7 KB → 4.8 KB gzip. Cost of the security hardening + listener
214
+ cleanup tracking.
215
+
216
+ ### Migration notes
217
+
218
+ - If any directive expression relied on `constructor`, `window`, `fetch`,
219
+ or other non-whitelisted globals, it now resolves to `undefined`. Move
220
+ the access into a component method.
221
+ - If you held onto an instance after calling `destroy()`, you can now
222
+ safely re-mount the same DOM with a new definition.
223
+
224
+ ---
225
+
226
+ ## [1.0.0] — initial release
227
+
228
+ Reactive shallow `Proxy` state, batched microtask rendering, DOM directives
229
+ (`data-text`, `data-html`, `data-if`, `data-show`, `data-bind`, `data-model`,
230
+ `data-class`, `data-on`, `@event`), keyed `data-each` list rendering, event
231
+ bus, SSR `prop()`, `fetch()` helper, idempotent `start()`.
package/README.md CHANGED
@@ -2,21 +2,31 @@
2
2
 
3
3
  Micra.js is a lightweight reactive TypeScript framework for small sites and SaaS apps. It gives you reactive state, DOM directives, keyed list rendering, an event bus, SSR-friendly props, and auto-mounting in about 5 KB gzip.
4
4
 
5
- ## What is Micra.js?
5
+ ## When to use Micra.js
6
6
 
7
- Micra.js is designed for server-rendered apps and small frontends that need a little reactivity without a large framework.
7
+ Built for **server-rendered apps** (Rails, Laravel, Django, Phoenix, ASP.NET) and small SaaS frontends that need a sprinkle of reactivity without a build step.
8
8
 
9
- Use it when you want:
9
+ Reach for Micra.js instead of React/Vue when:
10
10
 
11
- - JS expressions in directives like `data-if="count > 0"`
12
- - keyed list diffing with `data-each` + `data-key`
13
- - auto-mounting with `data-component`, `Micra.define()`, and `Micra.start()`
11
+ - ~5 KB gzip matters (full bundle, not "core")
12
+ - you want to drop a `<script>` tag on an existing page and go — no toolchain
13
+ - you have HTML rendered by your server template engine that needs reactive directives
14
+ - you don't need client-side routing or a full SPA
15
+ - you want **htmx + reactive client state** in the same page
16
+
17
+ Compared to Alpine.js: smaller surface, no `x-*` shorthand soup, AST-validated expressions (no global `window` / `fetch` access from markup), cleaner LLM ergonomics — fewer anti-patterns to fall into.
18
+
19
+ What you get:
20
+
21
+ - reactive `state` via a shallow `Proxy` (top-level writes only)
22
+ - JS expressions in directives: `data-if="count > 0"`
23
+ - keyed list diffing: `data-each` + `data-key`
24
+ - auto-mounting via `data-component` + `Micra.start()`
14
25
  - SSR props from `data-*` attributes via `this.prop()`
15
- - a built-in `this.fetch()` helper
16
- - a small global event bus with `Micra.on()` and `Micra.emit()`
17
- - DOM refs via `data-ref`
18
- - additive class toggling with `data-class`
19
- - simple lifecycle hooks: `onCreate`, `onDestroy`
26
+ - built-in `this.fetch()` helper with `AbortSignal` support
27
+ - global event bus: `Micra.on()` / `Micra.emit()`
28
+ - DOM refs via `data-ref`, additive class toggling via `data-class`
29
+ - lifecycle hooks: `onCreate`, `onDestroy`
20
30
 
21
31
  ## Quick Start
22
32
 
@@ -102,8 +112,8 @@ Micra.start();
102
112
  | ------------ | ---------------------------------------- | ------------------------- |
103
113
  | `data-text` | `data-text="name"` | Set `textContent` |
104
114
  | `data-html` | `data-html="content"` | Set `innerHTML` |
105
- | `data-if` | `data-if="count > 0"` | Toggle display |
106
- | `data-show` | `data-show="loaded"` | Alias of `data-if` |
115
+ | `data-if` | `data-if="count > 0"` | Mount / unmount from DOM |
116
+ | `data-show` | `data-show="loaded"` | Toggle `style.display` |
107
117
  | `data-bind` | `data-bind="href:url, disabled:loading"` | Bind attributes |
108
118
  | `data-model` | `data-model="search"` | Two-way input binding |
109
119
  | `data-each` | `data-each="items" data-key="id"` | List rendering |
@@ -153,13 +163,14 @@ this.refs
153
163
  this.render()
154
164
  this.destroy()
155
165
  this.prop(name, default?)
156
- this.fetch(url, options?)
166
+ this.fetch(url, options?) // supports AbortSignal in options.signal
157
167
  this.emit(event, payload?)
158
168
  this.on(event, handler)
159
169
  ```
160
170
 
161
171
  ## Documentation
162
172
 
173
+ - **AI / LLM code generation:** [`llms.txt`](./llms.txt) (overview) · [`llms-full.txt`](./llms-full.txt) (10 inline recipes + anti-pattern reference) · [`docs/llm-guide.md`](./docs/llm-guide.md) (full guide)
163
174
  - [Getting started](./docs/getting-started.md)
164
175
  - [Core concepts](./docs/concepts.md)
165
176
  - [Directives](./docs/directives.md)
@@ -171,3 +182,17 @@ this.on(event, handler)
171
182
  - Recipes:
172
183
  - [Todo app](./docs/recipes/todo-app.md)
173
184
  - [Server-sent events (SSE)](./docs/recipes/sse.md)
185
+
186
+ ## Code generation with LLMs
187
+
188
+ Micra has a small surface area, but LLMs default to jQuery / vanilla-JS or React patterns that defeat the framework. When generating Micra code (in Claude artifacts, ChatGPT canvas, Cursor, Copilot, etc.), follow these rules:
189
+
190
+ 1. **Lists** go through `<template data-each="items" data-key="id">`. Never `getElementById` / `innerHTML` for component output.
191
+ 2. **Derived values** (counts, totals, filtered subsets) are **methods**, not state fields. State holds raw data only.
192
+ 3. **Event handlers** use `@event` / `data-on`. Never `addEventListener` inside methods — it leaks past `destroy()`. Document-level listeners go in `onCreate` and are removed in `onDestroy`.
193
+ 4. **No manual re-render.** Micra batches a microtask render on every state write — no `this.refresh()` / `this.update()` / `this.renderList()`.
194
+ 5. **State proxy is shallow.** Replace top-level: `state.user = { ...state.user, name: x }`, not `state.user.name = x`.
195
+ 6. **No modifier syntax** like `@keydown.enter` — branch on `e.key === 'Enter'` inside the handler.
196
+ 7. **Use jsDelivr, not unpkg** — `cdn.jsdelivr.net` is in the CSP allowlist of Claude artifacts / ChatGPT canvas; `unpkg.com` is blocked there.
197
+
198
+ Full anti-pattern reference with side-by-side examples: [`docs/llm-guide.md`](./docs/llm-guide.md) and [`llms-full.txt`](./llms-full.txt).
@@ -11,17 +11,23 @@
11
11
  * mount() is called by both the public Micra.mount() API and by start()
12
12
  * (which scans the DOM for [data-component] elements).
13
13
  */
14
- import type { ComponentDefinition, ComponentInstance, StateRecord } from '../types';
14
+ import type { ComponentDefinition, ComponentInstance, StateRecord } from "../types";
15
15
  /**
16
16
  * Mount a component definition onto a DOM element.
17
17
  * Returns the component instance, or null if the root element is not found.
18
18
  *
19
19
  * Already-mounted elements return the existing instance.
20
20
  *
21
+ * Both `S` (state) and `M` (methods) are inferred from the literal — the
22
+ * returned instance is fully typed: `inst.state.X` and `inst.someMethod()`
23
+ * are checked.
24
+ *
21
25
  * @example
22
26
  * const instance = Micra.mount('#counter', {
23
27
  * state: { count: 0 },
24
28
  * inc() { this.state.count++ },
25
29
  * })
30
+ * instance?.inc()
31
+ * instance?.state.count
26
32
  */
27
- export declare function mount<S extends StateRecord>(selector: string | HTMLElement, definition: ComponentDefinition<S>): ComponentInstance<S> | null;
33
+ export declare function mount<S extends StateRecord, M>(selector: string | HTMLElement, definition: ComponentDefinition<S, M>): ComponentInstance<S, M> | null;
@@ -14,13 +14,22 @@ export declare const _instances: Map<HTMLElement, InternalInstance<StateRecord>>
14
14
  /**
15
15
  * Register a component definition under `name`.
16
16
  *
17
+ * Both state shape (`S`) and method set (`M`) are inferred from the literal,
18
+ * so `this.state.X` and `this.someMethod()` are fully typed inside the
19
+ * method bodies and lifecycle hooks.
20
+ *
17
21
  * @example
18
- * define('counter', { state: { count: 0 }, inc() { this.state.count++ } })
22
+ * define('counter', {
23
+ * state: { count: 0 },
24
+ * inc() { this.state.count++ }, // this.state.count: number ✓
25
+ * reset() { this.state.count = 0 }, // this.reset() is also typed ✓
26
+ * onCreate() { this.inc() }, // ✓
27
+ * })
19
28
  */
20
- export declare function define<S extends StateRecord>(name: string, definition: ComponentDefinition<S>): void;
29
+ export declare function define<S extends StateRecord, M>(name: string, definition: ComponentDefinition<S, M>): void;
21
30
  /**
22
31
  * Type-helper — returns `definition` unchanged but lets TypeScript infer `S`
23
- * from the `state` literal so all methods are typed with the correct `this`.
32
+ * and `M` from the literal so all methods are typed with the correct `this`.
24
33
  *
25
34
  * Use this when defining a component outside a `define()` call.
26
35
  *
@@ -31,7 +40,7 @@ export declare function define<S extends StateRecord>(name: string, definition:
31
40
  * })
32
41
  * Micra.define('counter', counter)
33
42
  */
34
- export declare function defineComponent<S extends StateRecord>(definition: ComponentDefinition<S>): ComponentDefinition<S>;
43
+ export declare function defineComponent<S extends StateRecord, M>(definition: ComponentDefinition<S, M>): ComponentDefinition<S, M>;
35
44
  /**
36
45
  * Returns a read-only view of all live instances (keyed by root element).
37
46
  * Useful for DevTools / debugging.
@@ -4,36 +4,32 @@
4
4
  * Responsibilities:
5
5
  * - data-text, data-html, data-if, data-show, data-bind, data-model
6
6
  * - data-class (additive class toggling)
7
- * - Directive result cache (built once per element, reused on re-renders)
8
7
  *
9
- * LLM NOTE: applyDirectives() is called on every render. The directive cache
10
- * (DirectiveCache on el.__micraCache) avoids repeated querySelectorAll on
11
- * re-renders cache is built lazily on the first call for each root element.
8
+ * LLM NOTE: applyDirectives() is called on every render. It consumes a
9
+ * pre-computed ScanIndex (built once by scan.ts and cached on the element).
10
+ * The scan replaced 10+ querySelectorAll calls with a single TreeWalker pass.
12
11
  *
13
12
  * Important: this module does NOT handle data-each — see dom/each.ts.
14
13
  */
15
- import type { InternalInstance, StateRecord } from '../types';
14
+ import type { InternalInstance, ScanIndex, StateRecord } from '../types';
16
15
  import { warn } from '../utils/expr';
17
16
  /**
18
17
  * Apply all non-each directives to a component subtree.
19
18
  *
20
- * For regular Elements: directive bindings are cached in `el.__micraCache`
21
- * after the first call subsequent re-renders skip querySelectorAll entirely.
19
+ * Consumes a pre-computed ScanIndex. data-if runs first so subsequent
20
+ * directives don't write into a tree that's about to be detached this tick.
22
21
  *
23
- * For DocumentFragments (no-key each clones): always re-scan because these
24
- * fragments are new clones on every render.
25
- *
26
- * @param root - Component root Element or DocumentFragment (no-key each clone)
22
+ * @param scan - Pre-computed scan from scan.ts (cached per element)
27
23
  * @param state - Expression state (may include item/index for each rows)
28
24
  * @param rawState - Raw (non-proxy) state for model sync
29
- * @param instance - Component instance (unused here, kept for future hooks)
30
25
  */
31
- export declare function applyDirectives<S extends StateRecord>(root: Element | DocumentFragment, state: StateRecord, rawState: StateRecord, _instance: InternalInstance<S>): void;
26
+ export declare function applyDirectives<S extends StateRecord>(scan: ScanIndex, state: StateRecord, rawState: StateRecord, _instance: InternalInstance<S>): void;
32
27
  /**
33
28
  * Validate directive usage and emit dev warnings.
34
- * Called once after the initial render of a component.
29
+ * Called once after the initial render of a component, with the already-built
30
+ * scan so we don't walk the DOM again.
35
31
  *
36
32
  * @internal
37
33
  */
38
- export declare function validateDirectives(root: Element): void;
34
+ export declare function validateDirectives(scan: ScanIndex): void;
39
35
  export { warn };
@@ -8,18 +8,20 @@
8
8
  * - Apply directives to each row with a scoped itemState
9
9
  *
10
10
  * LLM NOTE: renderList() is called on every render cycle AFTER applyDirectives().
11
- * Only <template> elements with data-each are processed.
11
+ * The template list comes pre-scanned from scan.ts — no DOM queries here.
12
+ * Each row node gets its own ScanIndex cached on `node.__micraScan` so
13
+ * re-renders of that row don't re-walk the DOM.
12
14
  * Keyed mode (data-key present) mutates the DOM in-place — nodes are
13
15
  * created once and reused. Non-keyed mode removes all nodes and re-clones.
14
16
  */
15
17
  import type { InternalInstance, StateRecord } from '../types';
16
18
  /**
17
- * Process all `<template data-each>` elements owned by `root`.
19
+ * Process all `<template data-each>` elements found by the scanner.
18
20
  * Scoped itemState makes `item`, `index`, `$index` available in row expressions.
19
21
  *
20
- * @param root - Component root Element
22
+ * @param templates - Pre-scanned list of <template data-each> elements
21
23
  * @param state - Expression state (proxy merging rawState + instance)
22
24
  * @param rawState - Raw (non-proxy) state — used for model binding
23
25
  * @param instance - Component instance (for event binding)
24
26
  */
25
- export declare function renderList<S extends StateRecord>(root: Element, state: StateRecord, rawState: StateRecord, instance: InternalInstance<S>): void;
27
+ export declare function renderList<S extends StateRecord>(templates: Element[], state: StateRecord, rawState: StateRecord, instance: InternalInstance<S>): void;
@@ -9,29 +9,36 @@
9
9
  * LLM NOTE: Every listener attached here is also recorded in
10
10
  * instance.__micraListeners so destroy() can remove it cleanly.
11
11
  * Re-render skips already-bound elements via per-element __micra* flags.
12
+ *
13
+ * All three binders accept pre-computed element lists from scan.ts —
14
+ * no DOM queries here.
12
15
  */
13
- import type { InternalInstance, StateRecord } from '../types';
16
+ import type { CachedBinding, InternalInstance, StateRecord } from '../types';
14
17
  /**
15
18
  * Bind `data-on="event:method[,event2:method2]"` listeners.
16
19
  * Listeners are bound once — re-render calls are no-ops for already-bound elements.
17
20
  *
18
21
  * Supports modifiers: `click.prevent`, `click.stop`, `click.self`.
19
22
  *
23
+ * @param els - Pre-computed list of [data-on] elements from scan.ts
24
+ *
20
25
  * @example
21
26
  * <button data-on="click:save">Save</button>
22
27
  * <form data-on="submit.prevent:handleSubmit">
23
28
  */
24
- export declare function bindDataOn<S extends StateRecord>(root: Element, instance: InternalInstance<S>): void;
29
+ export declare function bindDataOn<S extends StateRecord>(els: Element[], instance: InternalInstance<S>): void;
25
30
  /**
26
31
  * Bind `@event="method"` shorthand attributes (Stimulus-style).
27
32
  * Bound once per element via `__micraAtBound` — re-renders are no-ops.
28
- * Supports the same modifiers as data-on: `@click.prevent="submit"`.
33
+ *
34
+ * @param els - Pre-computed list of elements with at least one @-prefixed attr
35
+ * (from scan.ts — replaces the old `querySelectorAll('*')` walk)
29
36
  *
30
37
  * @example
31
38
  * <button @click="increment">+</button>
32
39
  * <form @submit.prevent="handleSubmit">
33
40
  */
34
- export declare function bindAtEvents<S extends StateRecord>(root: Element, instance: InternalInstance<S>): void;
41
+ export declare function bindAtEvents<S extends StateRecord>(els: Element[], instance: InternalInstance<S>): void;
35
42
  /**
36
43
  * Two-way binding: `data-model="key"` wires <input>/<select>/<textarea>
37
44
  * to `state[key]`. Binding is attached once per element.
@@ -39,8 +46,11 @@ export declare function bindAtEvents<S extends StateRecord>(root: Element, insta
39
46
  * Numeric inputs (`type="number"` / `type="range"`) write numbers, not strings.
40
47
  * Checkbox inputs write booleans. Everything else writes strings.
41
48
  *
49
+ * @param bindings - Pre-computed model bindings from scan.ts
50
+ * (each carries { el, expr } where expr is the state key)
51
+ *
42
52
  * @example
43
53
  * <input data-model="search"> // updates state.search on every keystroke
44
54
  * <select data-model="sortBy"> // updates state.sortBy on change
45
55
  */
46
- export declare function bindModels<S extends StateRecord>(root: Element, instance: InternalInstance<S>): void;
56
+ export declare function bindModels<S extends StateRecord>(bindings: CachedBinding[], instance: InternalInstance<S>): void;
@@ -2,22 +2,22 @@
2
2
  * src/dom/refs.ts — data-ref collection.
3
3
  *
4
4
  * Responsibilities:
5
- * - After each render, scan for `[data-ref]` elements (owned by this component)
6
- * - Populate `instance.refs` so methods can do `this.refs.chart` etc.
5
+ * - Populate `instance.refs` from a pre-scanned list of [data-ref] elements.
7
6
  *
8
7
  * LLM NOTE: This module is PURE relative to state — it only reads DOM attributes
9
8
  * and writes to instance.refs. It does NOT trigger renders.
10
9
  */
11
10
  import type { InternalInstance, StateRecord } from '../types';
12
11
  /**
13
- * Collect all `[data-ref="name"]` elements owned by this component root into
14
- * `instance.refs`.
12
+ * Build `instance.refs` from the pre-scanned [data-ref] elements.
15
13
  *
16
14
  * Called once after the initial render and again on every re-render (refs may
17
15
  * point to newly created elements after an each-list update).
18
16
  *
17
+ * @param els - List of [data-ref] elements from scan.ts
18
+ *
19
19
  * @example
20
20
  * // HTML: <canvas data-ref="chart">
21
21
  * // JS: this.refs.chart → HTMLCanvasElement
22
22
  */
23
- export declare function collectRefs<S extends StateRecord>(root: Element, instance: InternalInstance<S>): void;
23
+ export declare function collectRefs<S extends StateRecord>(els: Element[], instance: InternalInstance<S>): void;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * src/dom/scan.ts — Single-pass directive/event/ref scanner.
3
+ *
4
+ * Replaces 10+ querySelectorAll calls per render with ONE TreeWalker
5
+ * traversal that classifies every directive attribute in a single visit.
6
+ *
7
+ * Boundaries:
8
+ * - REJECT (skip subtree) on nested [data-component] — same semantics as
9
+ * the old `filterOwn` helper, but applied during the walk so we don't
10
+ * even *visit* those nodes.
11
+ * - <template> contents are not visited (browser TreeWalker default).
12
+ * `<template data-each>` itself IS visited and classified into scan.each;
13
+ * its children are processed by each.ts on every render via scanFragment.
14
+ *
15
+ * Hot-path notes:
16
+ * - We read `el.attributes` once and switch by suffix. No allocations per
17
+ * non-matching attr.
18
+ * - Pair-parsing (`data-bind`, `data-class`) happens here, once, at scan
19
+ * time. Reused on every render.
20
+ */
21
+ import type { ScanIndex } from "../types";
22
+ /**
23
+ * Scan an Element subtree owned by one component. Skips nested
24
+ * [data-component] subtrees entirely. Visits the root itself.
25
+ *
26
+ * Cached on `el.__micraScan` after the first call — subsequent renders
27
+ * are free.
28
+ */
29
+ export declare function scanComponent(root: Element): ScanIndex;
30
+ /**
31
+ * Scan a DocumentFragment (no-key each clone). Not cached — these fragments
32
+ * are temporary and re-cloned every render.
33
+ */
34
+ export declare function scanFragment(frag: DocumentFragment): ScanIndex;
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  *
22
22
  * @module Micra
23
23
  */
24
- export type { StateRecord, UnsubFn, EventHandler, FetchOptions, ComponentInstance, ComponentDefinition, } from './types';
24
+ export type { StateRecord, UnsubFn, EventHandler, FetchOptions, ComponentMethods, ComponentBuiltins, ComponentInstance, ComponentDefinition, } from './types';
25
25
  export { FetchError } from './utils/fetch';
26
26
  export { define, defineComponent, instances, registry, debug } from './core/registry';
27
27
  export { mount } from './core/mount';