@zoijs/core 1.3.2 → 1.5.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 CHANGED
@@ -1,103 +1,132 @@
1
- # Changelog
2
-
3
- All notable changes to Zoijs are documented here. The format is based on
4
- [Keep a Changelog](https://keepachangelog.com/), and Zoijs follows
5
- [Semantic Versioning](https://semver.org/) (see `VERSIONING.md`).
6
-
7
- ## [1.3.2] — 2026-06-26
8
-
9
- ### Fixed
10
- - **Focus is preserved across a keyed reorder.** The 1.3.1 minimal-move change can
11
- move the subtree that holds the focused element, which blurs it in browsers.
12
- `each` now captures focus + caret position before reordering and restores them
13
- after, so reordering a list never steals focus or selection — whichever nodes
14
- happen to move. Verified in Chromium, Firefox, and WebKit
15
- (`browser-tests/regression.spec.js`).
16
-
17
- ## [1.3.1] 2026-06-26
18
-
19
- ### Performance
20
- - **Minimal DOM moves in `each`.** Keyed-list reconciliation now uses a
21
- longest-increasing-subsequence pass, so a reorder moves the **fewest nodes
22
- possible**moving one item across a list is a single DOM move (it could be up
23
- to N before). No API or behavior change; the final order is identical and reused
24
- nodes keep their identity (focus, input values, and scroll survive reorders).
25
- Proven by move-count tests (`tests/lis.test.js`); numbers in `bench/`.
26
-
27
- ## [1.3.0]2026-06-26
28
-
29
- ### Added
30
- - **`boundary(child, fallback)`.** A render-time error boundary: it renders
31
- `child`, and if `child` throws **synchronously while building its markup** (a
32
- setup/render error that would otherwise break the whole `mount`), it disposes the
33
- partial work so an `effect` created before the throw can't leak — and renders
34
- `fallback` (a value, or `(error) => value`) instead. Catches synchronous
35
- setup/render throws only; errors in reactive *updates* are already contained per
36
- binding, and *async* errors belong to `@zoijs/resource` / `@zoijs/action`'s
37
- `error()` state. Logs in dev, silent in production. The public surface is now
38
- **nine** functions (additive MINOR). See [RFC 0004](docs/rfcs/0004-error-boundary.md).
39
-
40
- ## [1.2.0] 2026-06-26
41
-
42
- ### Added
43
- - **`effect(fn)`.** A public reactive effect runs a side effect immediately and
44
- re-runs whenever a reactive value it reads changes (automatic dependency
45
- tracking, microtask-batched). The function may return a cleanup that runs before
46
- the next run and on dispose (same convention as a `ref`); `effect` auto-disposes
47
- with its owner (component / list item) and returns `{ dispose }` for early
48
- teardown. This is the public completion of the reactive trio (`createState` /
49
- `computed` / `effect`) the engine already used it internally for bindings. Use
50
- it for side effects *outside* the view (persist on change, sync `document.title`,
51
- drive a non-Zoijs widget); for on-screen content, keep using a binding
52
- (`${() => …}`). The public surface is now **eight** functions (additive MINOR per
53
- `VERSIONING.md`). See [RFC 0003](docs/rfcs/0003-effect-and-svg.md).
54
-
55
- ### Notes
56
- - The **`svg`** helper considered alongside `effect` was **deferred**: templates
57
- rooted at `<svg>` already render correctly, and only dynamic-SVG *composition* is
58
- affected — a minority need. See [RFC 0003](docs/rfcs/0003-effect-and-svg.md) §6.
59
-
60
- ## [1.1.0] 2026-06-25
61
-
62
- ### Added
63
- - **Element refs.** A new `ref` binding gives you the rendered DOM element:
64
- `html\`<input ref=${(el) => el.focus()} />\``. The callback runs once, just after
65
- the element is inserted (so `focus`/`scroll`/`measure`/`canvas` work), is not
66
- reactive, and may return a cleanup function that runs on unmount or list-item
67
- removal. Works inside keyed `each` lists. Non-function values are ignored with a
68
- dev-mode warning and never become a DOM attribute. No new export — `ref` is a
69
- binding semantic, so the seven-function public surface is unchanged (additive
70
- MINOR per `VERSIONING.md`). See [Element refs](docs/concepts/refs.md) and
71
- [RFC 0001](docs/rfcs/0001-element-refs.md).
72
-
73
- ## [1.0.0] 2026-06-24
74
-
75
- First stable release. The public API is frozen at seven functions.
76
-
77
- ### Public API
78
- - `html` — tagged-template renderer (no JSX, no build step).
79
- - `mount(component, target)` `unmount()`.
80
- - `createState(value)` `{ get, set, peek }`.
81
- - `computed(fn)` `{ get, peek }` lazy, cached, value-gated.
82
- - `each(items, keyFn, renderFn)` keyed list reconciliation.
83
- - `configure({ dev })` — development/production mode.
84
- - `onCleanup(fn)` — teardown for components and list items.
85
-
86
- ### Features
87
- - Fine-grained, direct DOM updates (no Virtual DOM); setup runs once.
88
- - Push-pull reactive core with automatic dependency tracking and microtask batching.
89
- - Owner-scoped cleanup; deterministic teardown on unmount and list-item removal.
90
- - Context-aware template parser (quoted/unquoted/partial/multi-hole attributes,
91
- boolean/URL/aria/data attributes, SVG, nested templates and lists).
92
- - Secure by default: inert text, URL-scheme allowlist (control-char resistant),
93
- `data:` raster-image rules, `on*`/`srcdoc` blocked, function-only handlers,
94
- no `eval`, CSP- and Trusted-Types-friendly.
95
- - TypeScript definitions with generics for state/computed/lists.
96
-
97
- ### Tooling
98
- - 100+ unit/DOM tests (jsdom), real-browser tests on Chromium/Firefox/WebKit
99
- (Playwright), and TypeScript type tests.
100
- - No build step required at any point.
101
-
102
- [1.1.0]: https://github.com/Zoijs/zoijs/releases/tag/core-v1.1.0
103
- [1.0.0]: https://github.com/Zoijs/zoijs/releases/tag/core-v1.0.0
1
+ # Changelog
2
+
3
+ All notable changes to Zoijs are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/), and Zoijs follows
5
+ [Semantic Versioning](https://semver.org/) (see `VERSIONING.md`).
6
+
7
+ ## [1.5.0] — 2026-06-26
8
+
9
+ ### Added
10
+ - **DOM-free template compiler + a `@zoijs/core/server` subpath.** `html\`…\`` now
11
+ compiles to a static HTML string + part descriptors **without touching the DOM**;
12
+ the `<template>` element is built lazily on first client render. This means a
13
+ component can be evaluated on a server (no DOM) so [`@zoijs/ssr`](https://www.npmjs.com/package/@zoijs/ssr)
14
+ can render it to a string. The new subpath exposes the building blocks a server
15
+ renderer needs — the static HTML/parts of a result, template/list markers, and the
16
+ **same** security predicates the client uses (`escapeText`, `escapeAttr`,
17
+ `isSafeUrl`, `isSafeAttributeName`, `URL_ATTRS`) so server and client make
18
+ identical safety decisions. Client rendering is byte-for-byte unchanged; the
19
+ learnable nine-function main surface is unchanged. See
20
+ [RFC 0008](docs/rfcs/0008-ssr.md).
21
+
22
+ ## [1.4.0] 2026-06-26
23
+
24
+ ### Added
25
+ - **Devtools inspection hook** (`@zoijs/core/devtools`). A new, dev-only, read-only
26
+ seam that lets an inspector — [`@zoijs/devtools`](https://www.npmjs.com/package/@zoijs/devtools)
27
+ or a browser extension observe the reactive graph: states, computeds, effects,
28
+ the edges between them, and **which DOM node each binding updates**. It's reached
29
+ through a dedicated subpath (`import { attachInspector } from "@zoijs/core/devtools"`),
30
+ so the learnable **nine-function** main surface is unchanged. The hook is off by
31
+ default (a single null check until something attaches), never instruments the hot
32
+ read path (`.get()`), and is a no-op under `configure({ dev: false })` so a
33
+ production app pays no measurable cost and exposes nothing. See
34
+ [RFC 0005](docs/rfcs/0005-devtools-hook.md).
35
+
36
+ ## [1.3.2] 2026-06-26
37
+
38
+ ### Fixed
39
+ - **Focus is preserved across a keyed reorder.** The 1.3.1 minimal-move change can
40
+ move the subtree that holds the focused element, which blurs it in browsers.
41
+ `each` now captures focus + caret position before reordering and restores them
42
+ after, so reordering a list never steals focus or selection — whichever nodes
43
+ happen to move. Verified in Chromium, Firefox, and WebKit
44
+ (`browser-tests/regression.spec.js`).
45
+
46
+ ## [1.3.1] 2026-06-26
47
+
48
+ ### Performance
49
+ - **Minimal DOM moves in `each`.** Keyed-list reconciliation now uses a
50
+ longest-increasing-subsequence pass, so a reorder moves the **fewest nodes
51
+ possible** moving one item across a list is a single DOM move (it could be up
52
+ to N before). No API or behavior change; the final order is identical and reused
53
+ nodes keep their identity (focus, input values, and scroll survive reorders).
54
+ Proven by move-count tests (`tests/lis.test.js`); numbers in `bench/`.
55
+
56
+ ## [1.3.0] 2026-06-26
57
+
58
+ ### Added
59
+ - **`boundary(child, fallback)`.** A render-time error boundary: it renders
60
+ `child`, and if `child` throws **synchronously while building its markup** (a
61
+ setup/render error that would otherwise break the whole `mount`), it disposes the
62
+ partial work — so an `effect` created before the throw can't leak — and renders
63
+ `fallback` (a value, or `(error) => value`) instead. Catches synchronous
64
+ setup/render throws only; errors in reactive *updates* are already contained per
65
+ binding, and *async* errors belong to `@zoijs/resource` / `@zoijs/action`'s
66
+ `error()` state. Logs in dev, silent in production. The public surface is now
67
+ **nine** functions (additive MINOR). See [RFC 0004](docs/rfcs/0004-error-boundary.md).
68
+
69
+ ## [1.2.0] 2026-06-26
70
+
71
+ ### Added
72
+ - **`effect(fn)`.** A public reactive effect — runs a side effect immediately and
73
+ re-runs whenever a reactive value it reads changes (automatic dependency
74
+ tracking, microtask-batched). The function may return a cleanup that runs before
75
+ the next run and on dispose (same convention as a `ref`); `effect` auto-disposes
76
+ with its owner (component / list item) and returns `{ dispose }` for early
77
+ teardown. This is the public completion of the reactive trio (`createState` /
78
+ `computed` / `effect`)the engine already used it internally for bindings. Use
79
+ it for side effects *outside* the view (persist on change, sync `document.title`,
80
+ drive a non-Zoijs widget); for on-screen content, keep using a binding
81
+ (`${() => …}`). The public surface is now **eight** functions (additive MINOR per
82
+ `VERSIONING.md`). See [RFC 0003](docs/rfcs/0003-effect-and-svg.md).
83
+
84
+ ### Notes
85
+ - The **`svg`** helper considered alongside `effect` was **deferred**: templates
86
+ rooted at `<svg>` already render correctly, and only dynamic-SVG *composition* is
87
+ affected a minority need. See [RFC 0003](docs/rfcs/0003-effect-and-svg.md) §6.
88
+
89
+ ## [1.1.0] 2026-06-25
90
+
91
+ ### Added
92
+ - **Element refs.** A new `ref` binding gives you the rendered DOM element:
93
+ `html\`<input ref=${(el) => el.focus()} />\``. The callback runs once, just after
94
+ the element is inserted (so `focus`/`scroll`/`measure`/`canvas` work), is not
95
+ reactive, and may return a cleanup function that runs on unmount or list-item
96
+ removal. Works inside keyed `each` lists. Non-function values are ignored with a
97
+ dev-mode warning and never become a DOM attribute. No new export — `ref` is a
98
+ binding semantic, so the seven-function public surface is unchanged (additive
99
+ MINOR per `VERSIONING.md`). See [Element refs](docs/concepts/refs.md) and
100
+ [RFC 0001](docs/rfcs/0001-element-refs.md).
101
+
102
+ ## [1.0.0] — 2026-06-24
103
+
104
+ First stable release. The public API is frozen at seven functions.
105
+
106
+ ### Public API
107
+ - `html` — tagged-template renderer (no JSX, no build step).
108
+ - `mount(component, target)` → `unmount()`.
109
+ - `createState(value)` → `{ get, set, peek }`.
110
+ - `computed(fn)` → `{ get, peek }` — lazy, cached, value-gated.
111
+ - `each(items, keyFn, renderFn)` — keyed list reconciliation.
112
+ - `configure({ dev })` — development/production mode.
113
+ - `onCleanup(fn)` — teardown for components and list items.
114
+
115
+ ### Features
116
+ - Fine-grained, direct DOM updates (no Virtual DOM); setup runs once.
117
+ - Push-pull reactive core with automatic dependency tracking and microtask batching.
118
+ - Owner-scoped cleanup; deterministic teardown on unmount and list-item removal.
119
+ - Context-aware template parser (quoted/unquoted/partial/multi-hole attributes,
120
+ boolean/URL/aria/data attributes, SVG, nested templates and lists).
121
+ - Secure by default: inert text, URL-scheme allowlist (control-char resistant),
122
+ `data:` raster-image rules, `on*`/`srcdoc` blocked, function-only handlers,
123
+ no `eval`, CSP- and Trusted-Types-friendly.
124
+ - TypeScript definitions with generics for state/computed/lists.
125
+
126
+ ### Tooling
127
+ - 100+ unit/DOM tests (jsdom), real-browser tests on Chromium/Firefox/WebKit
128
+ (Playwright), and TypeScript type tests.
129
+ - No build step required at any point.
130
+
131
+ [1.1.0]: https://github.com/Zoijs/zoijs/releases/tag/core-v1.1.0
132
+ [1.0.0]: https://github.com/Zoijs/zoijs/releases/tag/core-v1.0.0
package/README.md CHANGED
@@ -1,154 +1,173 @@
1
- # Zoijs
2
-
3
- A lightweight frontend framework you don't have to learn before you use it — **plain HTML, CSS, and JavaScript**, no JSX, no build step, no Virtual DOM.
4
-
5
- **[Documentation](https://zoijs.dev)** · **[GitHub](https://github.com/Zoijs)** · **[npm](https://www.npmjs.com/package/@zoijs/core)**
6
-
7
- ```bash
8
- npm install @zoijs/core
9
- ```
10
-
11
- ```js
12
- import { html, mount, createState } from "@zoijs/core";
13
-
14
- function Counter() {
15
- const count = createState(0);
16
- return html`<button onclick=${() => count.set(count.get() + 1)}>${() => count.get()}</button>`;
17
- }
18
- mount(Counter, "#app");
19
- ```
20
-
21
- ## 📚 Documentation
22
-
23
- **New here? Start at [zoijs.dev](https://zoijs.dev) (or the [docs folder](docs/README.md)) — designed to get you productive in under 30 minutes.**
24
-
25
- - [Installation](docs/installation.md) · [Your First App](docs/first-app.md) · [Core Concepts](docs/concepts/core-concepts.md)
26
- - [Tutorials](docs/README.md#tutorials-build-something) · [API Reference](docs/api-reference.md) · [Examples](docs/examples.md)
27
- - [Troubleshooting](docs/troubleshooting.md) · [FAQ](docs/faq.md) · [Migrating from React/Vue/Solid/Lit/vanilla](docs/README.md#coming-from-another-framework)
28
-
29
- ---
30
-
31
- ## Mission
32
-
33
- Make building modern web applications feel as approachable as writing plain HTML, CSS, and JavaScript — so that any developer, on day one, can ship real software without first learning a framework.
34
-
35
- The framework should disappear into the skills developers already have. The whole mental model is three verbs: **write a function that returns `html`, put `createState` values in it, `mount` it.**
36
-
37
- ## Goals
38
-
39
- - **Beginner-friendly** — concepts you already know from vanilla JS/HTML/CSS.
40
- - **No build step** — runs from a single `<script type="module">`.
41
- - **No Virtual DOM** — fine-grained, direct DOM updates; only what changed is touched.
42
- - **Minimal runtime** — the browser does the heavy lifting (native `<template>`, `cloneNode`, events).
43
- - **Secure by default** — inert text rendering, URL-scheme guards, handler references (never strings), no `eval`.
44
- - **Small & readable** — a junior developer can read the source.
45
-
46
- See [`docs/Phase-1-MVP-Spec.md`](docs/Phase-1-MVP-Spec.md) for the full specification.
47
-
48
- ## Setup
49
-
50
- No build step is required — the framework is plain ES modules.
51
-
52
- ```bash
53
- # from the framework/ directory
54
-
55
- # run the counter example (serves the project root over http so ES modules resolve)
56
- npm run dev
57
- # then open: http://localhost:7310/examples/counter/ (keep the trailing slash)
58
-
59
- # run the tests (DOM tests run automatically via jsdom)
60
- npm test
61
- ```
62
-
63
- > Tips:
64
- > - ES module imports need to be served over `http://`, not opened as a `file://` path. `npm run dev` handles that.
65
- > - Use the **trailing slash** on the example URL (`/examples/counter/`). Without it, some static servers resolve `./app.js` against the wrong directory and the app won't load.
66
-
67
- ## Testing
68
-
69
- | Command | What it runs |
70
- |---|---|
71
- | `npm test` | Unit + DOM tests via jsdom (fast, no browser) |
72
- | `npm run test:unit` | Pure-logic tests only (no DOM) |
73
- | `npm run test:types` | TypeScript type-checks (`tsc --noEmit`) |
74
- | `npm run test:browser` | Real-browser tests in Chromium, Firefox, WebKit (Playwright) |
75
-
76
- Browser tests live in `browser-tests/` and run the example apps plus framework regressions against real engines. First-time setup downloads the browsers:
77
-
78
- ```bash
79
- npm install
80
- npx playwright install chromium firefox webkit
81
- npm run test:browser
82
- ```
83
-
84
- Playwright starts a static server automatically (`npx serve` on port 7310) — still no build step.
85
-
86
- ## Browser support
87
-
88
- Modern evergreen browsers. Verified automatically (Playwright) in:
89
-
90
- | Browser | Engine | Status |
91
- |---|---|---|
92
- | Chrome / Edge | Chromium | ✅ tested |
93
- | Firefox | Gecko | ✅ tested |
94
- | Safari | WebKit | ✅ tested |
95
-
96
- Relies on baseline-modern platform APIs: ES modules, `<template>`, `TreeWalker`, `Proxy`, `queueMicrotask`, `replaceChildren`, `addEventListener`, `setAttributeNS`. No IE support, no transpilation, no polyfills.
97
-
98
- ## Public API
99
-
100
- ```js
101
- import { html, mount, createState, computed, each, configure, onCleanup } from "@zoijs/core";
102
- ```
103
-
104
- - `html` tagged template; parsed once, cached.
105
- - `mount(component, target)` — render a component; returns `unmount()`.
106
- - `createState(value)` — a reactive value (`get` / `set` / `peek`).
107
- - `computed(fn)` — a lazy, cached, **value-gated** derived value (`get` / `peek`).
108
- - `each(itemsFn, keyFn, renderFn)` — keyed list rendering (reuse / move / remove nodes).
109
- - `configure({ dev })` — toggle development warnings (default `dev: true`).
110
- - `onCleanup(fn)` — register teardown for a component or list item (timers, subscriptions).
111
-
112
- See the [Documentation site](docs/README.md) for the full guide, tutorials, and API reference.
113
-
114
- **TypeScript:** ships type definitions ([`src/index.d.ts`](src/index.d.ts)) for autocomplete and optional type-checking JS-first, no build step required. `createState<T>`, `computed<T>`, and `each<T>` are generic. Type-check with `npm run test:types`.
115
-
116
- ## What's built
117
-
118
- - Fine-grained text/attribute bindings — `${() => state.get()}` updates one node in place; setup runs once (no re-render).
119
- - Native events, secure-by-default rendering (inert text, URL-scheme guards, no `eval`).
120
- - `computed()` derived values lazy, cached, nestable, and **value-gated** (unchanged results don't wake downstream).
121
- - `each()` keyed list reconciliation preserves focus / input / scroll on reorder.
122
- - Microtask batching, push-pull dependency tracking, **owner-scoped cleanup** (unmount and removed items dispose their subscriptions).
123
- - Production mode via `configure({ dev: false })` — no build step.
124
- - Safety: self-triggering effects are warned + stopped; a throwing binding doesn't break others.
125
-
126
- **Out of scope (by design):** router, CLI, plugins, SSR, global store, TypeScript-first setup, Virtual DOM.
127
-
128
- ## Project Structure
129
-
130
- ```
131
- framework/
132
- package.json
133
- README.md
134
- src/
135
- core/
136
- html.js # html() parse template into a cached blueprint
137
- mount.js # mount() — entry point; instantiate + attach + cleanup
138
- renderer.js # internal: bind dynamic slots, apply fine-grained updates
139
- reactivity/
140
- state.js # createState() + internal dependency tracking
141
- utils/
142
- dom.js # small native-DOM helpers
143
- security.js # escaping, URL-scheme allowlist, attribute-name guards
144
- index.js # public entry — re-exports html, mount, createState
145
- examples/
146
- counter/ # the first working app
147
- tests/ # basic unit tests (node --test)
148
- docs/
149
- Phase-1-MVP-Spec.md
150
- ```
151
-
152
- ## License
153
-
154
- MIT
1
+ # Zoijs
2
+
3
+ A lightweight frontend framework you don't have to learn before you use it — **plain HTML, CSS, and JavaScript**, no JSX, no build step, no Virtual DOM.
4
+
5
+ **[Documentation](https://zoijs.dev)** · **[GitHub](https://github.com/Zoijs)** · **[npm](https://www.npmjs.com/package/@zoijs/core)**
6
+
7
+ ```bash
8
+ npm install @zoijs/core
9
+ ```
10
+
11
+ ```js
12
+ import { html, mount, createState } from "@zoijs/core";
13
+
14
+ function Counter() {
15
+ const count = createState(0);
16
+ return html`<button onclick=${() => count.set(count.get() + 1)}>${() => count.get()}</button>`;
17
+ }
18
+ mount(Counter, "#app");
19
+ ```
20
+
21
+ ## 📚 Documentation
22
+
23
+ **New here? Start at [zoijs.dev](https://zoijs.dev) (or the [docs folder](docs/README.md)) — designed to get you productive in under 30 minutes.**
24
+
25
+ - [Installation](docs/installation.md) · [Your First App](docs/first-app.md) · [Core Concepts](docs/concepts/core-concepts.md)
26
+ - [Tutorials](docs/README.md#tutorials-build-something) · [API Reference](docs/api-reference.md) · [Examples](docs/examples.md)
27
+ - [Troubleshooting](docs/troubleshooting.md) · [FAQ](docs/faq.md) · [Migrating from React/Vue/Solid/Lit/vanilla](docs/README.md#coming-from-another-framework)
28
+
29
+ ---
30
+
31
+ ## Mission
32
+
33
+ Make building modern web applications feel as approachable as writing plain HTML, CSS, and JavaScript — so that any developer, on day one, can ship real software without first learning a framework.
34
+
35
+ The framework should disappear into the skills developers already have. The whole mental model is three verbs: **write a function that returns `html`, put `createState` values in it, `mount` it.**
36
+
37
+ ## Goals
38
+
39
+ - **Beginner-friendly** — concepts you already know from vanilla JS/HTML/CSS.
40
+ - **No build step** — runs from a single `<script type="module">`.
41
+ - **No Virtual DOM** — fine-grained, direct DOM updates; only what changed is touched.
42
+ - **Minimal runtime** — the browser does the heavy lifting (native `<template>`, `cloneNode`, events).
43
+ - **Secure by default** — inert text rendering, URL-scheme guards, handler references (never strings), no `eval`.
44
+ - **Small & readable** — a junior developer can read the source.
45
+
46
+ See [`docs/Phase-1-MVP-Spec.md`](docs/Phase-1-MVP-Spec.md) for the full specification.
47
+
48
+ ## Setup
49
+
50
+ No build step is required — the framework is plain ES modules.
51
+
52
+ ```bash
53
+ # from the framework/ directory
54
+
55
+ # run the counter example (serves the project root over http so ES modules resolve)
56
+ npm run dev
57
+ # then open: http://localhost:7310/examples/counter/ (keep the trailing slash)
58
+
59
+ # run the tests (DOM tests run automatically via jsdom)
60
+ npm test
61
+ ```
62
+
63
+ > Tips:
64
+ > - ES module imports need to be served over `http://`, not opened as a `file://` path. `npm run dev` handles that.
65
+ > - Use the **trailing slash** on the example URL (`/examples/counter/`). Without it, some static servers resolve `./app.js` against the wrong directory and the app won't load.
66
+
67
+ ## Testing
68
+
69
+ | Command | What it runs |
70
+ |---|---|
71
+ | `npm test` | Unit + DOM tests via jsdom (fast, no browser) |
72
+ | `npm run test:unit` | Pure-logic tests only (no DOM) |
73
+ | `npm run test:types` | TypeScript type-checks (`tsc --noEmit`) |
74
+ | `npm run test:browser` | Real-browser tests in Chromium, Firefox, WebKit (Playwright) |
75
+
76
+ Browser tests live in `browser-tests/` and run the example apps plus framework regressions against real engines. First-time setup downloads the browsers:
77
+
78
+ ```bash
79
+ npm install
80
+ npx playwright install chromium firefox webkit
81
+ npm run test:browser
82
+ ```
83
+
84
+ Playwright starts a static server automatically (`npx serve` on port 7310) — still no build step.
85
+
86
+ ## Browser support
87
+
88
+ Modern evergreen browsers. Verified automatically (Playwright) in:
89
+
90
+ | Browser | Engine | Status |
91
+ |---|---|---|
92
+ | Chrome / Edge | Chromium | ✅ tested |
93
+ | Firefox | Gecko | ✅ tested |
94
+ | Safari | WebKit | ✅ tested |
95
+
96
+ Relies on baseline-modern platform APIs: ES modules, `<template>`, `TreeWalker`, `Proxy`, `queueMicrotask`, `replaceChildren`, `addEventListener`, `setAttributeNS`. No IE support, no transpilation, no polyfills.
97
+
98
+ ## Public API
99
+
100
+ The whole framework is **nine functions** — learnable in one sitting and frozen for 1.x:
101
+
102
+ ```js
103
+ import {
104
+ html, mount, each, boundary,
105
+ createState, computed, effect,
106
+ configure, onCleanup,
107
+ } from "@zoijs/core";
108
+ ```
109
+
110
+ - `html` — tagged template; parsed once, cached.
111
+ - `mount(component, target)` — render a component; returns `unmount()`.
112
+ - `each(itemsFn, keyFn, renderFn)` keyed list rendering (reuse / move / remove nodes).
113
+ - `boundary(child, fallback)` — render-time error boundary: if `child` throws while building its markup, dispose the partial work and render `fallback`.
114
+ - `createState(value)`a reactive value (`get` / `set` / `peek`).
115
+ - `computed(fn)` — a lazy, cached, **value-gated** derived value (`get` / `peek`).
116
+ - `effect(fn)` — a side effect that re-runs when a value it reads changes; returns `{ dispose }` and may return a per-run cleanup.
117
+ - `configure({ dev })` — toggle development warnings (default `dev: true`).
118
+ - `onCleanup(fn)` register teardown for a component or list item (timers, subscriptions).
119
+
120
+ Plus the **`ref`** binding (`html\`<input ref=${(el) => el.focus()} />\``) no export; it's a template
121
+ attribute that hands you the rendered element.
122
+
123
+ **Devtools (dev-only).** A separate subpath, `@zoijs/core/devtools`, exposes a read-only inspection
124
+ hook `attachInspector(inspector)` and `inspecting()` that [`@zoijs/devtools`](https://www.npmjs.com/package/@zoijs/devtools)
125
+ (or a browser extension) uses to observe the reactive graph. It's off by default, never instruments
126
+ the hot read path, and is a no-op under `configure({ dev: false })`, so it costs production nothing and
127
+ leaves the nine-function main surface unchanged.
128
+
129
+ See the [Documentation site](https://zoijs.dev) for the full guide, tutorials, and API reference.
130
+
131
+ **TypeScript:** ships type definitions ([`src/index.d.ts`](src/index.d.ts)) for autocomplete and optional type-checking — JS-first, no build step required. `createState<T>`, `computed<T>`, and `each<T>` are generic. Type-check with `npm run test:types`.
132
+
133
+ ## What's built
134
+
135
+ - Fine-grained text/attribute bindings — `${() => state.get()}` updates one node in place; setup runs once (no re-render).
136
+ - Native events, secure-by-default rendering (inert text, URL-scheme guards, no `eval`).
137
+ - `computed()` derived values lazy, cached, nestable, and **value-gated** (unchanged results don't wake downstream).
138
+ - `effect()` side effects — re-run on change, with owner-scoped auto-disposal and a per-run cleanup.
139
+ - `boundary()` render-time error boundary — a failing subtree shows a fallback instead of breaking `mount`.
140
+ - `each()` keyed list reconciliation — minimal DOM moves; preserves focus / input / scroll on reorder.
141
+ - Microtask batching, push-pull dependency tracking, **owner-scoped cleanup** (unmount and removed items dispose their subscriptions).
142
+ - Production mode via `configure({ dev: false })` — no build step.
143
+ - Safety: self-triggering effects are warned + stopped; a throwing binding doesn't break others.
144
+
145
+ **Out of scope (by design):** router, CLI, plugins, SSR, global store, TypeScript-first setup, Virtual DOM.
146
+
147
+ ## Project Structure
148
+
149
+ ```
150
+ framework/
151
+ package.json
152
+ README.md
153
+ src/
154
+ core/
155
+ html.js # html() — parse template into a cached blueprint
156
+ mount.js # mount() — entry point; instantiate + attach + cleanup
157
+ renderer.js # internal: bind dynamic slots, apply fine-grained updates
158
+ reactivity/
159
+ state.js # createState() + internal dependency tracking
160
+ utils/
161
+ dom.js # small native-DOM helpers
162
+ security.js # escaping, URL-scheme allowlist, attribute-name guards
163
+ index.js # public entry — re-exports html, mount, createState
164
+ examples/
165
+ counter/ # the first working app
166
+ tests/ # basic unit tests (node --test)
167
+ docs/
168
+ Phase-1-MVP-Spec.md
169
+ ```
170
+
171
+ ## License
172
+
173
+ MIT