micra.js 2.3.0 → 2.3.2
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 +82 -0
- package/README.md +60 -0
- package/dist/core/reactive.d.ts +4 -0
- package/dist/dom/directives.d.ts +2 -2
- package/dist/dom/scan.d.ts +3 -3
- package/dist/micra.cjs.js +21 -31
- package/dist/micra.cjs.js.map +2 -2
- package/dist/micra.esm.js +21 -31
- package/dist/micra.esm.js.map +2 -2
- package/dist/micra.js +21 -31
- package/dist/micra.js.map +2 -2
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +0 -2
- package/llms-full.txt +110 -15
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/src/core/mount.ts +1 -1
- package/src/core/reactive.ts +6 -1
- package/src/dom/directives.ts +1 -3
- package/src/dom/each.ts +22 -7
- package/src/dom/scan.ts +3 -3
- package/src/types.ts +0 -2
- package/src/utils/expr.ts +3 -4
- package/src/dom/query.ts +0 -50
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,88 @@ 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.2] — 2026-06-10
|
|
8
|
+
|
|
9
|
+
### Fixed — `data-each` row root detection
|
|
10
|
+
|
|
11
|
+
- **A pretty-printed template with one root element is no longer wrapped
|
|
12
|
+
in `<micra-each-item>`.** Single-root detection used
|
|
13
|
+
`frag.childNodes.length === 1`, which counts whitespace text nodes — so
|
|
14
|
+
`<template data-each>\n <tr>…</tr>\n</template>` (three child nodes:
|
|
15
|
+
text, element, text) took the multi-root path and wrapped every row.
|
|
16
|
+
For table rows this put invalid content inside `<tbody>` and broke
|
|
17
|
+
`tbody > tr` child selectors.
|
|
18
|
+
- Exact semantics of the new check (top-level child nodes only, O(1)-ish
|
|
19
|
+
per row): plain whitespace (space, `\t`, `\n`, `\f`, `\r`) beside the
|
|
20
|
+
single element is ignored; **NBSP and any other visible character keep
|
|
21
|
+
the wrapper** (they render, so they must survive); **comment nodes
|
|
22
|
+
beside the root are dropped** — they don't render and aren't worth
|
|
23
|
+
invalid wrapper content inside `<tbody>`.
|
|
24
|
+
- Found by the official `isKeyed` compliance check while preparing the
|
|
25
|
+
[js-framework-benchmark](https://github.com/krausest/js-framework-benchmark)
|
|
26
|
+
submission — Micra now passes it for run / remove / swap.
|
|
27
|
+
- Affects both keyed and non-keyed paths (shared `createRowNode`).
|
|
28
|
+
- Internal: `ALLOWED_GLOBALS` in the expression evaluator is now built
|
|
29
|
+
from a split string (identical semantics, smaller minified output).
|
|
30
|
+
Bundle: **5.5 KB gzip** (5632 bytes — exactly at the size guard; the
|
|
31
|
+
next feature pays for itself or raises the limit consciously).
|
|
32
|
+
|
|
33
|
+
### Internal — LLM-benchmark harness hardening (no library impact)
|
|
34
|
+
|
|
35
|
+
Post-review fixes to `bench-llm/` so published numbers are trustworthy:
|
|
36
|
+
windows now close even when an assertion fails (stray timers no longer
|
|
37
|
+
misattribute errors to the next generation); errors aggregate across all
|
|
38
|
+
pages of multi-scenario tasks; quoted `>` inside template attributes no
|
|
39
|
+
longer mangles pages; ESM micra imports are rewritten to UMD bindings
|
|
40
|
+
instead of being dropped; the injected bundle is marked with
|
|
41
|
+
`data-harness-bundle` (single source of truth for loader and lint);
|
|
42
|
+
`Object.groupBy` replaced for Node 20 compatibility; `--only` no longer
|
|
43
|
+
overwrites aggregate results; the `@next` publish guard distinguishes
|
|
44
|
+
"version not published" from registry/network failures.
|
|
45
|
+
|
|
46
|
+
## [2.3.1] — 2026-05-30
|
|
47
|
+
|
|
48
|
+
### Performance
|
|
49
|
+
|
|
50
|
+
- **Batch scheduler now uses `queueMicrotask` instead of
|
|
51
|
+
`Promise.resolve().then(...)`.** Each render batch enqueues a single
|
|
52
|
+
microtask instead of allocating a Promise plus a reaction job, and the
|
|
53
|
+
flush callback is hoisted out of the hot path so it isn't re-created on
|
|
54
|
+
every `schedule()` call. Behaviour is identical — same microtask timing,
|
|
55
|
+
same write-collapsing. No public-API change.
|
|
56
|
+
|
|
57
|
+
### Internal — dead-code removal
|
|
58
|
+
|
|
59
|
+
- Removed the `src/dom/query.ts` module (`queryAll` / `queryOwn` /
|
|
60
|
+
`queryOwnAll` / `filterOwn`). It had no importers since the 2.2.0
|
|
61
|
+
single-pass scan replaced per-render `querySelectorAll` calls with one
|
|
62
|
+
`TreeWalker` traversal — esbuild already tree-shook it out of the
|
|
63
|
+
bundle, so this is a source-only cleanup.
|
|
64
|
+
- Removed two dead bookkeeping writes: `node.__micraEach` and
|
|
65
|
+
`node.__micraKey` were assigned during list rendering but never read
|
|
66
|
+
(keys live in the keyed-diff `Map`; the no-key path doesn't tag rows).
|
|
67
|
+
Dropped the matching fields from `MicraElement`.
|
|
68
|
+
- Dropped the unused `instance` parameter from `applyDirectives` — it was
|
|
69
|
+
never referenced in the body.
|
|
70
|
+
|
|
71
|
+
### Docs
|
|
72
|
+
|
|
73
|
+
- New [Rails + Micra recipe](https://github.com/denisfl/micra.js/blob/master/docs/recipes/rails.md)
|
|
74
|
+
(`docs/recipes/rails.md` + a site page): manual importmap integration,
|
|
75
|
+
the `micra-rails` gem with its caveats, a Tasks board demonstrating SSR
|
|
76
|
+
props / CSRF-attached `this.fetch` / cross-component bus, and the Turbo
|
|
77
|
+
Drive / Streams / Frames mount-and-cleanup story.
|
|
78
|
+
- README gains a **TypeScript** section spelling out what's checked
|
|
79
|
+
end-to-end (state, methods, event payloads) versus what isn't (the
|
|
80
|
+
expression strings inside `data-*` attributes).
|
|
81
|
+
- Landing page gains **Speed** (cross-library benchmark cards) and **AI
|
|
82
|
+
sandboxes** (copy-the-LLM-prompt) sections.
|
|
83
|
+
|
|
84
|
+
### Bundle
|
|
85
|
+
|
|
86
|
+
- **5.5 KB gzip** (5582 bytes) — a few bytes lighter than 2.3.0 after the
|
|
87
|
+
dead-code removal.
|
|
88
|
+
|
|
7
89
|
## [2.3.0] — 2026-05-30
|
|
8
90
|
|
|
9
91
|
### TypeScript — type-safe event bus
|
package/README.md
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
# Micra.js
|
|
2
2
|
|
|
3
|
+
[](https://github.com/denisfl/micra.js/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/micra.js)
|
|
5
|
+
[](https://bundlephobia.com/package/micra.js)
|
|
6
|
+
[](./dist/index.d.ts)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
3
9
|
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
10
|
|
|
11
|
+
## Project status
|
|
12
|
+
|
|
13
|
+
- **Stable, SemVer-disciplined.** Breaking changes only in majors; every
|
|
14
|
+
release documented in [CHANGELOG.md](./CHANGELOG.md) with migration notes.
|
|
15
|
+
- **Tested.** 255 tests across 14 suites run on every push and before every
|
|
16
|
+
npm publish; the build fails if the bundle exceeds **5.5 KB gzip**.
|
|
17
|
+
- **Typed.** Ships its own `.d.ts` — state, methods, and event-bus payloads
|
|
18
|
+
are checked end-to-end (see [TypeScript](#typescript)).
|
|
19
|
+
- **Security policy.** See [SECURITY.md](./SECURITY.md) — private reporting,
|
|
20
|
+
72-hour acknowledgement, supported-versions table.
|
|
21
|
+
|
|
5
22
|
## When to use Micra.js
|
|
6
23
|
|
|
7
24
|
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.
|
|
@@ -71,6 +88,48 @@ npm install micra.js
|
|
|
71
88
|
import * as Micra from "micra.js";
|
|
72
89
|
```
|
|
73
90
|
|
|
91
|
+
### TypeScript
|
|
92
|
+
|
|
93
|
+
The npm package ships its own `dist/index.d.ts` — no `@types/micra.js` package
|
|
94
|
+
needed. Inside every method body and lifecycle hook, both `this.state.X` and
|
|
95
|
+
`this.someMethod()` are fully checked at the call site (both `state` and the
|
|
96
|
+
method set are inferred from the literal you pass to `Micra.define`).
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import * as Micra from "micra.js";
|
|
100
|
+
|
|
101
|
+
Micra.define("counter", {
|
|
102
|
+
state: { count: 0 },
|
|
103
|
+
inc() {
|
|
104
|
+
this.state.count++; // ✓ number
|
|
105
|
+
this.dec(); // ✓ inferred sibling method
|
|
106
|
+
// this.foo(); // ✗ Property 'foo' does not exist
|
|
107
|
+
},
|
|
108
|
+
dec() { this.state.count--; },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Type-safe event bus via declaration merging
|
|
112
|
+
declare module "micra.js" {
|
|
113
|
+
interface MicraEvents {
|
|
114
|
+
"cart:updated": { count: number };
|
|
115
|
+
"modal:close": void;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Micra.emit("cart:updated", { count: 3 }); // ✓
|
|
120
|
+
Micra.emit("cart:updated", { count: "3" }); // ✗ type error
|
|
121
|
+
Micra.emit("modal:close"); // ✓ void → no args
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**What's checked:** imports, state shape, method names, event-bus payloads,
|
|
125
|
+
lifecycle hooks, refs, `Micra.mount()` return type.
|
|
126
|
+
|
|
127
|
+
**What's not:** the expression strings inside `data-text="…"` / `@click="…"`
|
|
128
|
+
attributes — those are plain HTML to the IDE and validated only at mount
|
|
129
|
+
time. Same trade-off as Alpine.js `x-*` and petite-vue `v-*`; the
|
|
130
|
+
alternatives are JSX or a single-file-component compiler, neither of which
|
|
131
|
+
Micra ships.
|
|
132
|
+
|
|
74
133
|
## Basic usage
|
|
75
134
|
|
|
76
135
|
A counter mounted automatically from `data-component`:
|
|
@@ -183,6 +242,7 @@ this.on(event, handler)
|
|
|
183
242
|
- [Todo app](./docs/recipes/todo-app.md)
|
|
184
243
|
- [Server-sent events (SSE)](./docs/recipes/sse.md)
|
|
185
244
|
- [htmx bridge](./docs/recipes/htmx.md)
|
|
245
|
+
- [Rails + Micra](./docs/recipes/rails.md)
|
|
186
246
|
|
|
187
247
|
## Code generation with LLMs
|
|
188
248
|
|
package/dist/core/reactive.d.ts
CHANGED
|
@@ -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
|
package/dist/dom/directives.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
package/dist/dom/scan.d.ts
CHANGED
|
@@ -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] —
|
|
9
|
-
*
|
|
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.
|
|
1
|
+
/* Micra.js v2.3.2 — https://github.com/micra-js/micra — MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -119,23 +119,9 @@ function debug() {
|
|
|
119
119
|
var exprCache = /* @__PURE__ */ new Map();
|
|
120
120
|
var warnedRuntime = /* @__PURE__ */ new Set();
|
|
121
121
|
var SIMPLE_PATH = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/;
|
|
122
|
-
var ALLOWED_GLOBALS =
|
|
123
|
-
"Math",
|
|
124
|
-
|
|
125
|
-
"Date",
|
|
126
|
-
"String",
|
|
127
|
-
"Number",
|
|
128
|
-
"Boolean",
|
|
129
|
-
"Array",
|
|
130
|
-
"Object",
|
|
131
|
-
"parseInt",
|
|
132
|
-
"parseFloat",
|
|
133
|
-
"isNaN",
|
|
134
|
-
"isFinite",
|
|
135
|
-
"NaN",
|
|
136
|
-
"Infinity",
|
|
137
|
-
"undefined"
|
|
138
|
-
]);
|
|
122
|
+
var ALLOWED_GLOBALS = new Set(
|
|
123
|
+
"Math,JSON,Date,String,Number,Boolean,Array,Object,parseInt,parseFloat,isNaN,isFinite,NaN,Infinity,undefined".split(",")
|
|
124
|
+
);
|
|
139
125
|
var PARAM_S = "$s";
|
|
140
126
|
var PARAM_SAFE = "$safe";
|
|
141
127
|
var SAFE_OUTER = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
@@ -253,13 +239,14 @@ function createReactiveState(obj, schedule, onKey) {
|
|
|
253
239
|
}
|
|
254
240
|
function createScheduler(render) {
|
|
255
241
|
let pending = false;
|
|
242
|
+
const flush = () => {
|
|
243
|
+
pending = false;
|
|
244
|
+
render();
|
|
245
|
+
};
|
|
256
246
|
return function schedule() {
|
|
257
247
|
if (pending) return;
|
|
258
248
|
pending = true;
|
|
259
|
-
|
|
260
|
-
pending = false;
|
|
261
|
-
render();
|
|
262
|
-
});
|
|
249
|
+
queueMicrotask(flush);
|
|
263
250
|
};
|
|
264
251
|
}
|
|
265
252
|
|
|
@@ -325,7 +312,7 @@ function applyModel(el, key, rawState) {
|
|
|
325
312
|
const desired = stateVal == null ? "" : String(stateVal);
|
|
326
313
|
if (html.value !== desired) html.value = desired;
|
|
327
314
|
}
|
|
328
|
-
function applyDirectives(scan, state, rawState
|
|
315
|
+
function applyDirectives(scan, state, rawState) {
|
|
329
316
|
for (const b of scan.if) applyIf(b, state);
|
|
330
317
|
for (const b of scan.text) applyText(b.el, b.expr, state);
|
|
331
318
|
for (const b of scan.html) applyHtml(b.el, b.expr, state);
|
|
@@ -573,8 +560,13 @@ function renderList(templates, state, rawState, instance, triggerKey) {
|
|
|
573
560
|
function createRowNode(tmpl, state, instance) {
|
|
574
561
|
const frag = tmpl.content.cloneNode(true);
|
|
575
562
|
let node;
|
|
576
|
-
|
|
577
|
-
|
|
563
|
+
const first = frag.firstElementChild;
|
|
564
|
+
const single = !!first && !first.nextElementSibling && !Array.prototype.some.call(
|
|
565
|
+
frag.childNodes,
|
|
566
|
+
(c) => c.nodeType === 3 && /[^\x00- ]/.test(c.textContent)
|
|
567
|
+
);
|
|
568
|
+
if (single) {
|
|
569
|
+
node = first;
|
|
578
570
|
} else {
|
|
579
571
|
node = document.createElement("micra-each-item");
|
|
580
572
|
node.style.display = "contents";
|
|
@@ -608,7 +600,6 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, inst
|
|
|
608
600
|
let node = keyMap.get(key);
|
|
609
601
|
if (!node) {
|
|
610
602
|
node = createRowNode(tmpl, state, instance);
|
|
611
|
-
node.__micraKey = key;
|
|
612
603
|
keyMap.set(key, node);
|
|
613
604
|
} else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
|
|
614
605
|
nextNodes.push(node);
|
|
@@ -621,7 +612,7 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, inst
|
|
|
621
612
|
itemState.index = index;
|
|
622
613
|
itemState.$index = index;
|
|
623
614
|
const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
|
|
624
|
-
applyDirectives(rowScan, itemState, rawState
|
|
615
|
+
applyDirectives(rowScan, itemState, rawState);
|
|
625
616
|
nextNodes.push(node);
|
|
626
617
|
}
|
|
627
618
|
for (const [key, node] of keyMap) {
|
|
@@ -706,7 +697,7 @@ function renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnch
|
|
|
706
697
|
itemState.item = item;
|
|
707
698
|
itemState.index = i;
|
|
708
699
|
itemState.$index = i;
|
|
709
|
-
applyDirectives(node.__micraScan, itemState, rawState
|
|
700
|
+
applyDirectives(node.__micraScan, itemState, rawState);
|
|
710
701
|
nextList[i] = node;
|
|
711
702
|
}
|
|
712
703
|
for (let i = nextLen; i < prevLen; i++) {
|
|
@@ -721,10 +712,9 @@ function renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnch
|
|
|
721
712
|
itemState.item = item;
|
|
722
713
|
itemState.index = i;
|
|
723
714
|
itemState.$index = i;
|
|
724
|
-
node.__micraEach = true;
|
|
725
715
|
node.__micraItem = item;
|
|
726
716
|
node.__micraIndex = i;
|
|
727
|
-
applyDirectives(node.__micraScan, itemState, rawState
|
|
717
|
+
applyDirectives(node.__micraScan, itemState, rawState);
|
|
728
718
|
nextList[i] = node;
|
|
729
719
|
frag.append(node);
|
|
730
720
|
}
|
|
@@ -823,7 +813,7 @@ function mount(selector, definition) {
|
|
|
823
813
|
try {
|
|
824
814
|
const mRoot2 = root;
|
|
825
815
|
const scan = (_a2 = mRoot2.__micraScan) != null ? _a2 : mRoot2.__micraScan = scanComponent(root);
|
|
826
|
-
applyDirectives(scan, exprState, rawState
|
|
816
|
+
applyDirectives(scan, exprState, rawState);
|
|
827
817
|
renderList(scan.each, exprState, rawState, instance, triggerKey);
|
|
828
818
|
bindDataOn(scan.on, instance);
|
|
829
819
|
bindAtEvents(scan.atEvents, instance);
|