micra.js 2.3.0 → 2.3.1

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 CHANGED
@@ -4,6 +4,49 @@ All notable changes to Micra.js will be documented in this file. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows
5
5
  [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.3.1] — 2026-05-30
8
+
9
+ ### Performance
10
+
11
+ - **Batch scheduler now uses `queueMicrotask` instead of
12
+ `Promise.resolve().then(...)`.** Each render batch enqueues a single
13
+ microtask instead of allocating a Promise plus a reaction job, and the
14
+ flush callback is hoisted out of the hot path so it isn't re-created on
15
+ every `schedule()` call. Behaviour is identical — same microtask timing,
16
+ same write-collapsing. No public-API change.
17
+
18
+ ### Internal — dead-code removal
19
+
20
+ - Removed the `src/dom/query.ts` module (`queryAll` / `queryOwn` /
21
+ `queryOwnAll` / `filterOwn`). It had no importers since the 2.2.0
22
+ single-pass scan replaced per-render `querySelectorAll` calls with one
23
+ `TreeWalker` traversal — esbuild already tree-shook it out of the
24
+ bundle, so this is a source-only cleanup.
25
+ - Removed two dead bookkeeping writes: `node.__micraEach` and
26
+ `node.__micraKey` were assigned during list rendering but never read
27
+ (keys live in the keyed-diff `Map`; the no-key path doesn't tag rows).
28
+ Dropped the matching fields from `MicraElement`.
29
+ - Dropped the unused `instance` parameter from `applyDirectives` — it was
30
+ never referenced in the body.
31
+
32
+ ### Docs
33
+
34
+ - New [Rails + Micra recipe](https://github.com/denisfl/micra.js/blob/master/docs/recipes/rails.md)
35
+ (`docs/recipes/rails.md` + a site page): manual importmap integration,
36
+ the `micra-rails` gem with its caveats, a Tasks board demonstrating SSR
37
+ props / CSRF-attached `this.fetch` / cross-component bus, and the Turbo
38
+ Drive / Streams / Frames mount-and-cleanup story.
39
+ - README gains a **TypeScript** section spelling out what's checked
40
+ end-to-end (state, methods, event payloads) versus what isn't (the
41
+ expression strings inside `data-*` attributes).
42
+ - Landing page gains **Speed** (cross-library benchmark cards) and **AI
43
+ sandboxes** (copy-the-LLM-prompt) sections.
44
+
45
+ ### Bundle
46
+
47
+ - **5.5 KB gzip** (5582 bytes) — a few bytes lighter than 2.3.0 after the
48
+ dead-code removal.
49
+
7
50
  ## [2.3.0] — 2026-05-30
8
51
 
9
52
  ### TypeScript — type-safe event bus
package/README.md CHANGED
@@ -71,6 +71,48 @@ npm install micra.js
71
71
  import * as Micra from "micra.js";
72
72
  ```
73
73
 
74
+ ### TypeScript
75
+
76
+ The npm package ships its own `dist/index.d.ts` — no `@types/micra.js` package
77
+ needed. Inside every method body and lifecycle hook, both `this.state.X` and
78
+ `this.someMethod()` are fully checked at the call site (both `state` and the
79
+ method set are inferred from the literal you pass to `Micra.define`).
80
+
81
+ ```ts
82
+ import * as Micra from "micra.js";
83
+
84
+ Micra.define("counter", {
85
+ state: { count: 0 },
86
+ inc() {
87
+ this.state.count++; // ✓ number
88
+ this.dec(); // ✓ inferred sibling method
89
+ // this.foo(); // ✗ Property 'foo' does not exist
90
+ },
91
+ dec() { this.state.count--; },
92
+ });
93
+
94
+ // Type-safe event bus via declaration merging
95
+ declare module "micra.js" {
96
+ interface MicraEvents {
97
+ "cart:updated": { count: number };
98
+ "modal:close": void;
99
+ }
100
+ }
101
+
102
+ Micra.emit("cart:updated", { count: 3 }); // ✓
103
+ Micra.emit("cart:updated", { count: "3" }); // ✗ type error
104
+ Micra.emit("modal:close"); // ✓ void → no args
105
+ ```
106
+
107
+ **What's checked:** imports, state shape, method names, event-bus payloads,
108
+ lifecycle hooks, refs, `Micra.mount()` return type.
109
+
110
+ **What's not:** the expression strings inside `data-text="…"` / `@click="…"`
111
+ attributes — those are plain HTML to the IDE and validated only at mount
112
+ time. Same trade-off as Alpine.js `x-*` and petite-vue `v-*`; the
113
+ alternatives are JSX or a single-file-component compiler, neither of which
114
+ Micra ships.
115
+
74
116
  ## Basic usage
75
117
 
76
118
  A counter mounted automatically from `data-component`:
@@ -183,6 +225,7 @@ this.on(event, handler)
183
225
  - [Todo app](./docs/recipes/todo-app.md)
184
226
  - [Server-sent events (SSE)](./docs/recipes/sse.md)
185
227
  - [htmx bridge](./docs/recipes/htmx.md)
228
+ - [Rails + Micra](./docs/recipes/rails.md)
186
229
 
187
230
  ## Code generation with LLMs
188
231
 
@@ -23,6 +23,10 @@ export declare function createReactiveState<S extends StateRecord>(obj: S, sched
23
23
  * Return a debounce function that defers `render` to the next microtask.
24
24
  * Multiple calls within the same tick collapse to a single render.
25
25
  *
26
+ * Uses `queueMicrotask` so each batch enqueues a single microtask instead of
27
+ * allocating a Promise + reaction job. `flush` is hoisted out of the hot path
28
+ * so it isn't re-created on every schedule() call.
29
+ *
26
30
  * @example
27
31
  * const schedule = createScheduler(render)
28
32
  * schedule() // defers render
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Important: this module does NOT handle data-each — see dom/each.ts.
13
13
  */
14
- import type { InternalInstance, ScanIndex, StateRecord } from '../types';
14
+ import type { ScanIndex, StateRecord } from '../types';
15
15
  import { warn } from '../utils/expr';
16
16
  /**
17
17
  * Apply all non-each directives to a component subtree.
@@ -23,7 +23,7 @@ import { warn } from '../utils/expr';
23
23
  * @param state - Expression state (may include item/index for each rows)
24
24
  * @param rawState - Raw (non-proxy) state for model sync
25
25
  */
26
- export declare function applyDirectives<S extends StateRecord>(scan: ScanIndex, state: StateRecord, rawState: StateRecord, _instance: InternalInstance<S>): void;
26
+ export declare function applyDirectives(scan: ScanIndex, state: StateRecord, rawState: StateRecord): void;
27
27
  /**
28
28
  * Validate directive usage and emit dev warnings.
29
29
  * Called once after the initial render of a component, with the already-built
@@ -5,9 +5,9 @@
5
5
  * traversal that classifies every directive attribute in a single visit.
6
6
  *
7
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.
8
+ * - REJECT (skip subtree) on nested [data-component] — a parent component
9
+ * never processes directives owned by a nested child. Applied during the
10
+ * walk so we don't even *visit* those nodes.
11
11
  * - <template> contents are not visited (browser TreeWalker default).
12
12
  * `<template data-each>` itself IS visited and classified into scan.each;
13
13
  * its children are processed by each.ts on every render — fresh rows
package/dist/micra.cjs.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Micra.js v2.3.0 — https://github.com/micra-js/micra — MIT */
1
+ /* Micra.js v2.3.1 — https://github.com/micra-js/micra — MIT */
2
2
  "use strict";
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -253,13 +253,14 @@ function createReactiveState(obj, schedule, onKey) {
253
253
  }
254
254
  function createScheduler(render) {
255
255
  let pending = false;
256
+ const flush = () => {
257
+ pending = false;
258
+ render();
259
+ };
256
260
  return function schedule() {
257
261
  if (pending) return;
258
262
  pending = true;
259
- Promise.resolve().then(() => {
260
- pending = false;
261
- render();
262
- });
263
+ queueMicrotask(flush);
263
264
  };
264
265
  }
265
266
 
@@ -325,7 +326,7 @@ function applyModel(el, key, rawState) {
325
326
  const desired = stateVal == null ? "" : String(stateVal);
326
327
  if (html.value !== desired) html.value = desired;
327
328
  }
328
- function applyDirectives(scan, state, rawState, _instance) {
329
+ function applyDirectives(scan, state, rawState) {
329
330
  for (const b of scan.if) applyIf(b, state);
330
331
  for (const b of scan.text) applyText(b.el, b.expr, state);
331
332
  for (const b of scan.html) applyHtml(b.el, b.expr, state);
@@ -608,7 +609,6 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, inst
608
609
  let node = keyMap.get(key);
609
610
  if (!node) {
610
611
  node = createRowNode(tmpl, state, instance);
611
- node.__micraKey = key;
612
612
  keyMap.set(key, node);
613
613
  } else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
614
614
  nextNodes.push(node);
@@ -621,7 +621,7 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, inst
621
621
  itemState.index = index;
622
622
  itemState.$index = index;
623
623
  const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
624
- applyDirectives(rowScan, itemState, rawState, instance);
624
+ applyDirectives(rowScan, itemState, rawState);
625
625
  nextNodes.push(node);
626
626
  }
627
627
  for (const [key, node] of keyMap) {
@@ -706,7 +706,7 @@ function renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnch
706
706
  itemState.item = item;
707
707
  itemState.index = i;
708
708
  itemState.$index = i;
709
- applyDirectives(node.__micraScan, itemState, rawState, instance);
709
+ applyDirectives(node.__micraScan, itemState, rawState);
710
710
  nextList[i] = node;
711
711
  }
712
712
  for (let i = nextLen; i < prevLen; i++) {
@@ -721,10 +721,9 @@ function renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnch
721
721
  itemState.item = item;
722
722
  itemState.index = i;
723
723
  itemState.$index = i;
724
- node.__micraEach = true;
725
724
  node.__micraItem = item;
726
725
  node.__micraIndex = i;
727
- applyDirectives(node.__micraScan, itemState, rawState, instance);
726
+ applyDirectives(node.__micraScan, itemState, rawState);
728
727
  nextList[i] = node;
729
728
  frag.append(node);
730
729
  }
@@ -823,7 +822,7 @@ function mount(selector, definition) {
823
822
  try {
824
823
  const mRoot2 = root;
825
824
  const scan = (_a2 = mRoot2.__micraScan) != null ? _a2 : mRoot2.__micraScan = scanComponent(root);
826
- applyDirectives(scan, exprState, rawState, instance);
825
+ applyDirectives(scan, exprState, rawState);
827
826
  renderList(scan.each, exprState, rawState, instance, triggerKey);
828
827
  bindDataOn(scan.on, instance);
829
828
  bindAtEvents(scan.atEvents, instance);