@getforma/core 1.0.0 → 1.0.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/README.md CHANGED
@@ -2,9 +2,30 @@
2
2
 
3
3
  [![CI](https://github.com/getforma-dev/formajs/actions/workflows/ci.yml/badge.svg)](https://github.com/getforma-dev/formajs/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/@getforma/core)](https://www.npmjs.com/package/@getforma/core)
5
+ [![Socket Badge](https://socket.dev/api/badge/npm/package/@getforma/core)](https://socket.dev/npm/package/@getforma/core)
5
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
7
 
7
- Reactive DOM library with fine-grained signals. No virtual DOM — `h()` creates real elements, signals update only what changed. ~15KB gzipped.
8
+ Reactive DOM library with fine-grained signals. No virtual DOM — signals update only the DOM nodes that changed. Components run once. ~15 KB gzipped.
9
+
10
+ ```tsx
11
+ import { createSignal, h, mount } from "@getforma/core";
12
+
13
+ const [count, setCount] = createSignal(0);
14
+
15
+ function Counter() {
16
+ return (
17
+ <button onClick={() => setCount((c) => c + 1)}>
18
+ {() => `Clicked ${count()} times`}
19
+ </button>
20
+ );
21
+ }
22
+
23
+ mount(() => <Counter />, "#app");
24
+ ```
25
+
26
+ No re-renders. No dependency arrays. No `useMemo`. The button text updates because it reads `count()` inside a reactive function — nothing else in the tree is touched.
27
+
28
+ ---
8
29
 
9
30
  ## Install
10
31
 
@@ -12,15 +33,23 @@ Reactive DOM library with fine-grained signals. No virtual DOM — `h()` creates
12
33
  npm install @getforma/core
13
34
  ```
14
35
 
15
- Or use the CDN (no build step required):
36
+ Or use a CDN no build step, no bundler:
16
37
 
17
38
  ```html
39
+ <!-- jsDelivr (recommended) -->
40
+ <script src="https://cdn.jsdelivr.net/npm/@getforma/core@latest/dist/formajs-runtime.global.js"></script>
41
+
42
+ <!-- unpkg -->
18
43
  <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script>
19
44
  ```
20
45
 
21
- ### Getting Started with a Bundler
46
+ > **Production:** Pin the version (e.g., `@getforma/core@1.0.1`) instead of `@latest`.
47
+
48
+ ---
49
+
50
+ ## Getting Started with a Bundler
22
51
 
23
- After `npm install`, you need a bundler to resolve the ES module imports. Here's a minimal Vite setup:
52
+ After `npm install`, you need a bundler to resolve ES module imports. Here's a minimal Vite setup:
24
53
 
25
54
  ```bash
26
55
  npm install @getforma/core
@@ -33,17 +62,20 @@ npm install -D vite
33
62
  <script type="module" src="./main.ts"></script>
34
63
  ```
35
64
 
36
- ```typescript
65
+ ```ts
37
66
  // main.ts
38
- import { createSignal, h, mount } from '@getforma/core';
67
+ import { createSignal, h, mount } from "@getforma/core";
39
68
 
40
69
  const [count, setCount] = createSignal(0);
41
70
 
42
- mount(() =>
43
- h('button', { onClick: () => setCount(count() + 1) },
44
- () => `Clicked ${count()} times`
45
- ),
46
- '#app'
71
+ mount(
72
+ () =>
73
+ h(
74
+ "button",
75
+ { onClick: () => setCount((c) => c + 1) },
76
+ () => `Clicked ${count()} times`,
77
+ ),
78
+ "#app",
47
79
  );
48
80
  ```
49
81
 
@@ -51,167 +83,288 @@ mount(() =>
51
83
  npx vite
52
84
  ```
53
85
 
54
- For **esbuild**, **tsup**, or other bundlers no special config is needed. FormaJS ships standard ESM and CJS via `package.json` exports.
86
+ **Any bundler works.** Vite, esbuild, tsup, webpack, Rollup FormaJS ships standard ESM and CJS via `package.json` exports. No plugins, no special config.
55
87
 
56
- ## Why FormaJS?
88
+ ---
89
+
90
+ ## Coming from React?
91
+
92
+ If you know React, you already know ~80% of FormaJS. Components are functions. Props flow down. You import, export, and compose the same way. The difference is *how reactivity works* — and it's simpler.
57
93
 
58
- Most UI libraries make you choose: simple but limited (Alpine, htmx), or powerful but complex (React, Vue, Svelte). FormaJS gives you a single reactive core that scales from a CDN script tag to a full-stack Rust SSR pipeline.
94
+ React re-runs your entire component function on every state change, diffs a virtual DOM, and patches the real one. FormaJS runs each component **once**. Signals update only the specific DOM nodes that read them. No reconciliation, no stale closures, no `useCallback`.
59
95
 
60
- **Design principles:**
96
+ | React | FormaJS | What changes |
97
+ |---|---|---|
98
+ | `useState` | `createSignal` | Same `[value, setter]` tuple |
99
+ | `useMemo` | `createComputed` | No dependency array — auto-tracks |
100
+ | `useEffect` | `createEffect` | No dependency array — auto-tracks |
101
+ | `useReducer` | `createReducer` | Same dispatch pattern |
102
+ | `useContext` | `createContext` / `inject` | Same provider pattern |
103
+ | `React.memo` | *Not needed* | Components already run once |
104
+ | Component functions | Same | `function Counter(props) { ... }` |
105
+ | Props | Same | `<Counter count={count} />` |
106
+ | Children | Same | Rest params or `props.children` |
107
+ | Import / export | Same | Standard ES modules |
61
108
 
62
- - **Real DOM, not virtual DOM.** `h('div')` returns an actual `HTMLDivElement`. Signals mutate it directly. No diffing pass, no reconciliation overhead for simple updates. Inspired by [Solid](https://www.solidjs.com/).
63
- - **Fine-grained reactivity.** Powered by [alien-signals](https://github.com/nicolo-ribaudo/alien-signals). When a signal changes, only the specific DOM text node or attribute that depends on it updates — not the whole component tree.
64
- - **Three entry points, one engine.** HTML Runtime (like Alpine — zero build step), `h()` hyperscript (like Preact), or JSX. All share the same signal graph. Pick the right tool for the job, upgrade without rewriting.
65
- - **CSP-safe capable.** The HTML Runtime includes a hand-written expression parser. The standard build enables `new Function()` as a fallback for complex expressions; the hardened build (`forma.hardened.js`) locks it off entirely for strict CSP environments.
66
- - **Islands over SPAs.** `activateIslands()` hydrates independent regions of server-rendered HTML. Each island is self-contained. Ship less JavaScript, keep server-rendered content instant.
109
+ **The mental model:** "I write components the same way, pass props the same way, compose the same way but I never think about re-renders, dependency arrays, or memoization. Signals just work."
67
110
 
68
- **What FormaJS is not:** It's not a framework with opinions about routing, data fetching, or state management patterns. It's a reactive DOM library. You bring the architecture.
111
+ ---
69
112
 
70
113
  ## Three Ways to Use FormaJS
71
114
 
72
- ### 1. HTML Runtime (no build step)
115
+ All three share the same signal graph and reactive engine. Pick the one that fits your project — or mix them.
116
+
117
+ ### 1. JSX
118
+
119
+ The most familiar path for React and Solid developers. JSX compiles to `h()` calls — it's syntactic sugar, not a different system.
120
+
121
+ Configure your bundler (TypeScript or Babel):
122
+
123
+ ```jsonc
124
+ // tsconfig.json
125
+ {
126
+ "compilerOptions": {
127
+ "jsx": "react",
128
+ "jsxFactory": "h",
129
+ "jsxFragmentFactory": "Fragment"
130
+ }
131
+ }
132
+ ```
133
+
134
+ ```tsx
135
+ import { createSignal, h, Fragment, mount } from "@getforma/core";
136
+
137
+ const [count, setCount] = createSignal(0);
138
+
139
+ function Counter() {
140
+ return (
141
+ <>
142
+ <p>{() => `Count: ${count()}`}</p>
143
+ <button onClick={() => setCount((c) => c + 1)}>+1</button>
144
+ </>
145
+ );
146
+ }
147
+
148
+ mount(() => <Counter />, "#app");
149
+ ```
150
+
151
+ Under the hood, the JSX above compiles to the exact `h()` calls shown in the next section. There's no JSX-specific runtime — it's the same function.
152
+
153
+ ### 2. Hyperscript — `h()`
154
+
155
+ No JSX transform needed. Same reactive behavior, explicit function calls.
156
+
157
+ ```ts
158
+ import { createSignal, h, mount } from "@getforma/core";
73
159
 
74
- Drop a script tag, write `data-*` attributes. Zero config, zero tooling.
160
+ const [count, setCount] = createSignal(0);
161
+
162
+ mount(
163
+ () =>
164
+ h(
165
+ "button",
166
+ { onClick: () => setCount((c) => c + 1) },
167
+ () => `Clicked ${count()} times`,
168
+ ),
169
+ "#app",
170
+ );
171
+ ```
172
+
173
+ See [The `h()` function](#the-h-function) below for the full signature and all call patterns.
174
+
175
+ ### 3. HTML Runtime (no build step)
176
+
177
+ Drop a script tag, write `data-*` attributes. Zero config, zero tooling — works from a CDN.
75
178
 
76
179
  ```html
77
- <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script>
180
+ <script src="https://cdn.jsdelivr.net/npm/@getforma/core@latest/dist/formajs-runtime.global.js"></script>
78
181
 
79
- <div data-forma-state='{"count": 0}'>
182
+ <div data-forma-state='{ "count": 0 }'>
80
183
  <p data-text="{count}"></p>
81
184
  <button data-on:click="{count++}">+1</button>
82
185
  <button data-on:click="{count = 0}">Reset</button>
83
186
  </div>
84
187
  ```
85
188
 
86
- #### Supported Directives
189
+ The expression parser is hand-written — no `eval()`, no `new Function()` by default. For strict CSP environments, use the hardened build:
190
+
191
+ ```html
192
+ <script src="https://cdn.jsdelivr.net/npm/@getforma/core@latest/dist/formajs-runtime-hardened.global.js"></script>
193
+ ```
194
+
195
+ <details>
196
+ <summary><strong>Full directive reference</strong></summary>
87
197
 
88
198
  | Directive | Description | Example |
89
- |-----------|-------------|---------|
199
+ |---|---|---|
90
200
  | `data-forma-state` | Declare reactive state (JSON) | `data-forma-state='{"count": 0}'` |
91
201
  | `data-text` | Bind text content | `data-text="{count}"` |
92
202
  | `data-show` | Toggle visibility (display) | `data-show="{isOpen}"` |
93
- | `data-if` | Conditional render (add/remove from DOM) | `data-if="{loggedIn}"` |
203
+ | `data-if` | Conditional render (add/remove DOM) | `data-if="{loggedIn}"` |
94
204
  | `data-model` | Two-way binding (inputs) | `data-model="{email}"` |
95
205
  | `data-on:event` | Event handler | `data-on:click="{count++}"` |
96
206
  | `data-class:name` | Conditional CSS class | `data-class:active="{isActive}"` |
97
207
  | `data-bind:attr` | Dynamic attribute | `data-bind:href="{url}"` |
98
- | `data-list` | List rendering with keyed reconciliation | `data-list="{items}"` |
208
+ | `data-list` | List rendering (keyed reconciliation) | `data-list="{items}"` |
99
209
  | `data-computed` | Computed value | `data-computed="doubled = count * 2"` |
100
210
  | `data-persist` | Persist state to localStorage | `data-persist="{count}"` |
101
211
  | `data-fetch` | Fetch data from URL | `data-fetch="GET /api/items → items"` |
102
212
  | `data-transition:*` | Enter/leave CSS transitions | `data-transition:enter="fade-in"` |
103
213
  | `data-ref` | Register element for `$refs` access | `data-ref="myInput"` |
104
- | `$el` | Reference to the current DOM element | `data-on:click="{$el.classList.toggle('active')}"` |
105
- | `$dispatch` | Fire a CustomEvent (bubbles, crosses Shadow DOM) | `data-on:click="{$dispatch('selected', {id: itemId})}"` |
214
+ | `$el` | Current DOM element | `data-on:click="{$el.classList.toggle('active')}"` |
215
+ | `$dispatch` | Fire CustomEvent (bubbles, crosses Shadow DOM) | `data-on:click="{$dispatch('selected', {id})}"` |
106
216
  | `$refs` | Named element references | `data-on:click="{$refs.myInput.focus()}"` |
107
217
 
108
- CSP-safe expression parser — no `eval()` or `new Function()` by default. For strict CSP environments, use the hardened build:
218
+ </details>
109
219
 
110
- ```html
111
- <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime-hardened.global.js"></script>
112
- ```
220
+ ---
113
221
 
114
- ### 2. Hyperscript — `h()`
222
+ ## The `h()` Function
115
223
 
116
- ```bash
117
- npm install @getforma/core
224
+ `h()` is the core of FormaJS rendering. Every component — whether written in JSX, hyperscript, or compiled from the HTML Runtime — resolves to `h()` calls that create real DOM elements.
225
+
226
+ ### Signature
227
+
228
+ ```ts
229
+ h(tag, props?, ...children)
118
230
  ```
119
231
 
120
- ```typescript
121
- import { createSignal, h, mount } from '@getforma/core';
232
+ | Parameter | Type | Description |
233
+ |---|---|---|
234
+ | `tag` | `string \| Function` | An HTML tag name (`'div'`, `'button'`) or a component function (`Counter`) |
235
+ | `props` | `object \| null` | Attributes, event handlers, and component props. Pass `null` or `{}` to skip. |
236
+ | `children` | `string \| number \| () => string \| Node \| Array` | Zero or more children — static text, reactive functions, elements, or arrays of any of these. |
122
237
 
123
- const [count, setCount] = createSignal(0);
238
+ ### The key rule
124
239
 
125
- mount(() =>
126
- h('button', { onClick: () => setCount(count() + 1) },
127
- () => `Clicked ${count()} times`
128
- ),
129
- '#app'
130
- );
131
- ```
240
+ **If a child is a function, it's reactive.** FormaJS wraps it in an effect so the DOM text node or subtree updates automatically when signals inside it change. If a child is a plain string or number, it's static — rendered once, never touched again.
132
241
 
133
- ### 3. JSX
242
+ ### Patterns
134
243
 
135
- Same `h()` function, JSX syntax. Configure your bundler:
244
+ ```ts
245
+ // Static text child
246
+ h("footer", { class: "text-sm" }, "Built with Forma")
136
247
 
137
- ```json
138
- // tsconfig.json
139
- {
140
- "compilerOptions": {
141
- "jsx": "react",
142
- "jsxFactory": "h",
143
- "jsxFragmentFactory": "Fragment"
144
- }
145
- }
146
- ```
248
+ // Reactive text child — updates when count() changes
249
+ h("button", { onClick: fn }, () => `Count: ${count()}`)
147
250
 
148
- ```tsx
149
- import { createSignal, h, Fragment, mount } from '@getforma/core';
251
+ // Multiple children
252
+ h("div", { class: "card" },
253
+ h("h2", null, "Title"),
254
+ h("p", null, "Body text"),
255
+ h("button", { onClick: fn }, "Click"),
256
+ )
150
257
 
151
- const [count, setCount] = createSignal(0);
258
+ // Children as an array (useful for dynamic lists)
259
+ h("ul", null, items.map(item => h("li", null, item.name)))
152
260
 
153
- function Counter() {
154
- return (
155
- <>
156
- <p>{() => `Count: ${count()}`}</p>
157
- <button onClick={() => setCount(count() + 1)}>+1</button>
158
- </>
159
- );
160
- }
261
+ // No props, just children
262
+ h("p", null, "Hello world")
263
+
264
+ // Component function with props
265
+ h(Counter, { initial: 5 })
161
266
 
162
- mount(() => <Counter />, '#app');
267
+ // Nested reactive children
268
+ h("div", null,
269
+ () => showHeader() ? h("h1", null, "Welcome") : null,
270
+ h("p", null, () => `You have ${count()} items`),
271
+ )
163
272
  ```
164
273
 
165
- ## CDN Usage
274
+ ### JSX equivalence
166
275
 
167
- ### Script tag (IIFE auto-initializes)
276
+ JSX is syntactic sugar that compiles to `h()` calls. These are identical:
168
277
 
169
- ```html
170
- <!-- unpkg -->
171
- <script src="https://unpkg.com/@getforma/core@0.8.1/dist/formajs-runtime.global.js"></script>
278
+ ```tsx
279
+ // JSX
280
+ <button class="btn" onClick={() => setCount((c) => c + 1)}>
281
+ {() => `Count: ${count()}`}
282
+ </button>
172
283
 
173
- <!-- jsDelivr (faster globally) -->
174
- <script src="https://cdn.jsdelivr.net/npm/@getforma/core@0.8.1/dist/formajs-runtime.global.js"></script>
284
+ // h()
285
+ h("button", { class: "btn", onClick: () => setCount((c) => c + 1) },
286
+ () => `Count: ${count()}`
287
+ )
175
288
  ```
176
289
 
177
- ### ESM import (no bundler, modern browsers)
290
+ ---
178
291
 
179
- ```html
180
- <script type="module">
181
- import { createSignal, h, mount } from 'https://cdn.jsdelivr.net/npm/@getforma/core@0.8.1/dist/index.js';
292
+ ## Why FormaJS?
182
293
 
183
- const [count, setCount] = createSignal(0);
184
- mount(() => h('button', { onClick: () => setCount(count() + 1) }, () => `${count()}`), '#app');
185
- </script>
186
- ```
294
+ Most UI libraries force a choice: simple but limited (Alpine, htmx), or powerful but heavy (React, Vue, Svelte). FormaJS gives you a single reactive core that scales from a CDN script tag to a compiled Rust SSR pipeline.
187
295
 
188
- ### All CDN builds
296
+ **Components run once.** No virtual DOM, no diffing, no reconciliation overhead. `h('div')` returns an actual `HTMLDivElement`. When a signal changes, only the specific text node or attribute that reads it updates — not the component, not the tree.
189
297
 
190
- | Build | Filename |
191
- |-------|----------|
192
- | **Standard** (recommended) | `formajs-runtime.global.js` |
193
- | **CSP-safe** (no `new Function`) | `formajs-runtime-hardened.global.js` |
194
- | Standard (short alias) | `forma-runtime.js` |
195
- | CSP-safe (short alias) | `forma-runtime-csp.js` |
298
+ **Fine-grained reactivity.** Powered by [alien-signals](https://github.com/nicolo-ribaudo/alien-signals) 3.x. The signal graph tracks dependencies automatically. No dependency arrays, no stale closures, no `useCallback` / `useMemo` ceremony.
299
+
300
+ **Three entry points, one engine.** HTML Runtime (like Alpine — zero build step), `h()` hyperscript (like Preact), or JSX (like React/Solid). All share the same signal graph. Start with a CDN script tag, graduate to a full build pipeline without rewriting.
301
+
302
+ **Islands over SPAs.** `activateIslands()` hydrates independent regions of server-rendered HTML. Each island is self-contained with error isolation, deferred hydration triggers (`visible`, `idle`, `interaction`), and disposal for module swaps.
303
+
304
+ **CSP-safe.** The HTML Runtime includes a hand-written expression parser — no `eval()`, no `new Function()`. The hardened build locks it off entirely, with zero `new Function` in the dist (verified by dead code elimination).
305
+
306
+ **What FormaJS is not:** It's not a framework with opinions about routing, data fetching, or state management. It's a reactive DOM library. You bring the architecture.
307
+
308
+ ---
309
+
310
+ ## The Rust Compiler (Optional)
311
+
312
+ Everything above works without the Rust compiler. You can build a complete application with just `npm install @getforma/core` and a bundler. The compiler is an **optimization layer** — you add it when performance and deployment constraints demand it.
313
+
314
+ ### What the compiler does
315
+
316
+ | Without compiler | With compiler |
317
+ |---|---|
318
+ | `h()` calls create DOM elements at runtime | `h()` calls are pre-compiled to `template()` + `cloneNode()` for faster initial render |
319
+ | SSR requires Node.js (`renderToString`) | SSR runs natively in Rust via the FMIR binary walker — no JS runtime on the server |
320
+ | Standard JS bundle shipped to the client | Components compile to FMIR (Forma Module IR), a compact binary format sent over the wire |
321
+ | Islands hydrate from HTML + JS | Islands hydrate from FMIR binary — smaller payload, faster parse |
322
+
323
+ ### When to add it
324
+
325
+ You don't need the compiler to get started, prototype, or even ship to production. Add it when:
196
326
 
197
- Available from both `unpkg.com/@getforma/core@VERSION/dist/` and `cdn.jsdelivr.net/npm/@getforma/core@VERSION/dist/`.
327
+ - **SSR without Node.js** — your backend is Rust/Axum and you don't want a Node.js sidecar just for rendering.
328
+ - **Faster initial render** — pre-compiled templates skip the `h()` → `createElement` path and go straight to `cloneNode()`.
329
+ - **Smaller payloads** — FMIR binary is more compact than the equivalent JavaScript for complex component trees.
330
+ - **The full Forma stack** — `@getforma/compiler` → FMIR → `forma-ir` (Rust parser) → `forma-server` (Axum SSR) gives you a complete pipeline at ~$18/month deployment cost.
198
331
 
199
- For production, always pin the version (e.g., `@0.8.1`). Unversioned URLs resolve to latest.
332
+ ### Architecture
200
333
 
201
- > The CSP build uses a hand-written expression parser and never calls `new Function`.
202
- > It supports most common patterns. See [examples/csp](./examples/csp) for a working demo.
334
+ ```
335
+ TypeScript/JSX components
336
+
337
+ @getforma/compiler (TS → FMIR binary)
338
+
339
+ forma-ir (Rust: parse + walk FMIR)
340
+
341
+ forma-server (Rust/Axum: SSR + asset serving + CSP)
342
+
343
+ HTML response (server-rendered, islands hydrate on client)
344
+ ```
345
+
346
+ All entry points — JSX, `h()`, and the HTML Runtime — work both with and without the compiler:
347
+
348
+ | | Without Compiler | With Compiler |
349
+ |---|---|---|
350
+ | HTML Runtime | `data-*` directives | + SSR from IR walker |
351
+ | `h()` hyperscript | `createSignal` + `h()` | + compiled templates |
352
+ | JSX | `createSignal` + JSX | + compiled templates + SSR |
353
+ | Islands | `activateIslands()` | + FMIR hydration |
354
+
355
+ ---
203
356
 
204
357
  ## Core API
205
358
 
206
359
  ### Signals
207
360
 
208
- ```typescript
209
- import { createSignal, createEffect, createComputed, batch } from '@getforma/core';
361
+ ```ts
362
+ import { createSignal, createEffect, createComputed, batch } from "@getforma/core";
210
363
 
211
364
  const [count, setCount] = createSignal(0);
212
365
  const doubled = createComputed(() => count() * 2);
213
366
 
214
- createEffect(() => console.log('count:', count()));
367
+ createEffect(() => console.log("count:", count()));
215
368
 
216
369
  batch(() => {
217
370
  setCount(1);
@@ -219,25 +372,21 @@ batch(() => {
219
372
  });
220
373
  ```
221
374
 
222
- #### Custom Equality
223
-
224
- Skip updates when the new value is equal to the current value:
375
+ **Custom equality** — skip updates when the value hasn't meaningfully changed:
225
376
 
226
- ```typescript
377
+ ```ts
227
378
  const [pos, setPos] = createSignal(
228
379
  { x: 0, y: 0 },
229
380
  { equals: (a, b) => a.x === b.x && a.y === b.y },
230
381
  );
231
382
 
232
- setPos({ x: 0, y: 0 }); // skipped — values are equal
233
- setPos({ x: 1, y: 0 }); // applied — values differ
383
+ setPos({ x: 0, y: 0 }); // skipped — equal
384
+ setPos({ x: 1, y: 0 }); // applied — different
234
385
  ```
235
386
 
236
- #### Computed with Previous Value
237
-
238
- The computed getter receives the previous value for efficient diffing:
387
+ **Computed with previous value** — the getter receives the previous result:
239
388
 
240
- ```typescript
389
+ ```ts
241
390
  const changes = createComputed((prev) => {
242
391
  const current = items();
243
392
  if (prev) console.log(`${prev.length} → ${current.length} items`);
@@ -245,218 +394,217 @@ const changes = createComputed((prev) => {
245
394
  });
246
395
  ```
247
396
 
248
- #### Reactive Introspection
397
+ **Reactive introspection** — type guards and utilities from alien-signals 3.x:
249
398
 
250
- Type guards and utilities from alien-signals 3.x:
399
+ ```ts
400
+ import { isSignal, isComputed, getBatchDepth, trigger } from "@getforma/core";
251
401
 
252
- ```typescript
253
- import { isSignal, isComputed, getBatchDepth, trigger } from '@getforma/core';
254
-
255
- isSignal(count); // true — is this a signal getter?
256
- isComputed(doubled); // true — is this a computed value?
257
- getBatchDepth(); // 0 outside batch, 1+ inside
258
- trigger(doubled); // force recomputation even if deps unchanged
402
+ isSignal(count); // true
403
+ isComputed(doubled); // true
404
+ getBatchDepth(); // 0 outside batch, 1+ inside
405
+ trigger(doubled); // force recomputation
259
406
  ```
260
407
 
261
408
  ### Conditional Rendering
262
409
 
263
- ```typescript
264
- import { createSignal, createShow, createSwitch, h } from '@getforma/core';
410
+ ```ts
411
+ import { createSignal, createShow, createSwitch, h } from "@getforma/core";
265
412
 
266
413
  const [loggedIn, setLoggedIn] = createSignal(false);
267
414
 
268
- // createShow — toggle between two branches
269
- createShow(loggedIn,
270
- () => h('p', null, 'Welcome back'),
271
- () => h('p', null, 'Please sign in'),
415
+ // Two branches
416
+ createShow(
417
+ loggedIn,
418
+ () => h("p", null, "Welcome back"),
419
+ () => h("p", null, "Please sign in"),
272
420
  );
273
421
 
274
- // createSwitch — multi-branch with caching
275
- const [view, setView] = createSignal('home');
276
- createSwitch(view, [
277
- { match: 'home', render: () => h('div', null, 'Home') },
278
- { match: 'settings', render: () => h('div', null, 'Settings') },
279
- ], () => h('div', null, '404 Not Found'));
422
+ // Multi-branch with caching
423
+ const [view, setView] = createSignal("home");
424
+
425
+ createSwitch(
426
+ view,
427
+ [
428
+ { match: "home", render: () => h("div", null, "Home") },
429
+ { match: "settings", render: () => h("div", null, "Settings") },
430
+ ],
431
+ () => h("div", null, "404 Not Found"),
432
+ );
280
433
  ```
281
434
 
282
435
  ### List Rendering
283
436
 
284
- ```typescript
285
- import { createSignal, createList, h } from '@getforma/core';
437
+ ```ts
438
+ import { createSignal, createList, h } from "@getforma/core";
286
439
 
287
440
  const [items, setItems] = createSignal([
288
- { id: 1, name: 'Alice' },
289
- { id: 2, name: 'Bob' },
441
+ { id: 1, name: "Alice" },
442
+ { id: 2, name: "Bob" },
290
443
  ]);
291
444
 
292
445
  createList(
293
446
  items,
294
- (item) => item.id, // key function
295
- (item) => h('li', null, item.name),
447
+ (item) => item.id,
448
+ (item) => h("li", null, item.name),
296
449
  );
297
450
  ```
298
451
 
299
- ### Store (deep reactivity)
452
+ ### Store (Deep Reactivity)
300
453
 
301
- ```typescript
302
- import { createStore } from '@getforma/core';
454
+ ```ts
455
+ import { createStore } from "@getforma/core";
303
456
 
304
457
  const [state, setState] = createStore({
305
- user: { name: 'Alice', prefs: { theme: 'dark' } },
458
+ user: { name: "Alice", prefs: { theme: "dark" } },
306
459
  items: [1, 2, 3],
307
460
  });
308
461
 
309
462
  // Read reactively — tracked at the exact property path
310
- state.user.name; // 'Alice'
311
- state.items[0]; // 1
463
+ state.user.name; // "Alice"
464
+ state.items[0]; // 1
312
465
 
313
- // Setter API — partial object merge (batched)
314
- setState({ user: { ...state.user, name: 'Bob' } });
315
- setState(prev => ({ items: [...prev.items, 4] }));
466
+ // Setter API — partial merge
467
+ setState({ user: { ...state.user, name: "Bob" } });
468
+ setState((prev) => ({ items: [...prev.items, 4] }));
316
469
 
317
- // Or mutate directly via proxy — only affected subscribers update
318
- state.user.name = 'Bob'; // only "user.name" subscribers notified
319
- state.items.push(4); // array mutation batched automatically
470
+ // Or mutate directly — only affected subscribers update
471
+ state.user.name = "Bob";
472
+ state.items.push(4);
320
473
  ```
321
474
 
322
- ### Components
475
+ > **Note:** `Object.keys(state)`, `for...in`, and spread (`{...state}`) are NOT reactive. Use signals or explicit arrays for collections that need to react to membership changes.
323
476
 
324
- ```typescript
325
- import { defineComponent, onMount, onUnmount, h } from '@getforma/core';
477
+ ### Components & Lifecycle
478
+
479
+ ```ts
480
+ import { createSignal, defineComponent, onMount, onUnmount, h } from "@getforma/core";
326
481
 
327
482
  const Timer = defineComponent(() => {
328
483
  const [seconds, setSeconds] = createSignal(0);
329
484
 
330
485
  onMount(() => {
331
- const id = setInterval(() => setSeconds(s => s + 1), 1000);
486
+ const id = setInterval(() => setSeconds((s) => s + 1), 1000);
332
487
  return () => clearInterval(id); // cleanup on unmount
333
488
  });
334
489
 
335
- return h('span', null, () => `${seconds()}s`);
490
+ return h("span", null, () => `${seconds()}s`);
336
491
  });
337
492
 
338
493
  document.body.appendChild(Timer());
339
494
  ```
340
495
 
341
- #### Lifecycle: `onMount` vs `onUnmount`
342
-
343
- - **`onMount(fn)`** — runs after the component's DOM is created. If `fn` returns a function, that function is automatically registered as an unmount callback.
344
- - **`onUnmount(fn)`** — explicitly registers a cleanup function that runs when the component is disposed.
345
-
346
- Both mechanisms feed into the same cleanup queue — the `onMount` return shorthand is convenience for the common pattern of setting up and tearing down in one place:
496
+ `onMount(fn)` runs after DOM creation. If `fn` returns a function, it registers as an unmount callback. `onUnmount(fn)` explicitly registers cleanup. Both feed the same cleanup queue:
347
497
 
348
- ```typescript
498
+ ```ts
349
499
  // These are equivalent:
350
500
  onMount(() => {
351
501
  const id = setInterval(tick, 1000);
352
502
  return () => clearInterval(id);
353
503
  });
354
504
 
355
- // vs.
356
505
  onMount(() => {
357
506
  const id = setInterval(tick, 1000);
358
507
  onUnmount(() => clearInterval(id));
359
508
  });
360
509
  ```
361
510
 
362
- ### Error Handling
363
-
364
- **`mount()` fails fast.** If the container selector doesn't match any element, it throws:
365
-
366
- ```typescript
367
- mount(() => h('p', null, 'hello'), '#nonexistent');
368
- // Error: mount: container not found — "#nonexistent"
369
- ```
511
+ ### Context (Dependency Injection)
370
512
 
371
- **Global error handler.** Register a handler for errors in effects and lifecycle callbacks:
513
+ ```ts
514
+ import { createContext, provide, inject } from "@getforma/core";
372
515
 
373
- ```typescript
374
- import { onError } from '@getforma/core';
516
+ const ThemeCtx = createContext("light");
375
517
 
376
- onError((error, info) => {
377
- console.error(`[${info?.source}]`, error);
378
- });
518
+ provide(ThemeCtx, "dark");
519
+ const theme = inject(ThemeCtx); // "dark"
379
520
  ```
380
521
 
381
- **Error boundaries.** Catch rendering errors and display fallback UI with a retry option:
522
+ ### Reducer
382
523
 
383
- ```typescript
384
- import { createErrorBoundary, h } from '@getforma/core';
524
+ ```ts
525
+ import { createReducer } from "@getforma/core";
385
526
 
386
- createErrorBoundary(
387
- () => h(UnstableComponent),
388
- (error, retry) => h('div', null,
389
- h('p', null, `Something went wrong: ${error.message}`),
390
- h('button', { onClick: retry }, 'Retry'),
391
- ),
527
+ const [state, dispatch] = createReducer(
528
+ (state, action) => {
529
+ switch (action.type) {
530
+ case "INCREMENT": return { count: state.count + 1 };
531
+ case "DECREMENT": return { count: state.count - 1 };
532
+ default: return state;
533
+ }
534
+ },
535
+ { count: 0 },
392
536
  );
537
+
538
+ dispatch({ type: "INCREMENT" }); // state() === { count: 1 }
393
539
  ```
394
540
 
395
- ### Context (Dependency Injection)
541
+ ### History (Undo / Redo)
542
+
543
+ ```ts
544
+ import { createHistory } from "@getforma/core";
396
545
 
397
- ```typescript
398
- import { createContext, provide, inject } from '@getforma/core';
546
+ const [state, setState, { undo, redo, canUndo, canRedo }] = createHistory({ text: "" });
399
547
 
400
- const ThemeCtx = createContext('light');
548
+ setState({ text: "hello" });
549
+ setState({ text: "hello world" });
401
550
 
402
- provide(ThemeCtx, 'dark');
403
- const theme = inject(ThemeCtx); // 'dark'
551
+ undo(); // state.text === "hello"
552
+ canUndo(); // true
553
+ redo(); // state.text === "hello world"
404
554
  ```
405
555
 
406
- ### History (undo/redo)
556
+ ### Error Handling
557
+
558
+ `mount()` fails fast — if the selector doesn't match, it throws:
407
559
 
408
- ```typescript
409
- import { createHistory } from '@getforma/core';
560
+ ```ts
561
+ mount(() => h("p", null, "hello"), "#nonexistent");
562
+ // Error: mount: container not found — "#nonexistent"
563
+ ```
410
564
 
411
- const [state, setState, { undo, redo, canUndo, canRedo }] = createHistory({ text: '' });
565
+ **Global error handler** for effects and lifecycle callbacks:
412
566
 
413
- setState({ text: 'hello' });
414
- setState({ text: 'hello world' });
567
+ ```ts
568
+ import { onError } from "@getforma/core";
415
569
 
416
- undo(); // state.text === 'hello'
417
- canUndo(); // true
418
- redo(); // state.text === 'hello world'
570
+ onError((error, info) => {
571
+ console.error(`[${info?.source}]`, error);
572
+ });
419
573
  ```
420
574
 
421
- ### Reducer
575
+ **Error boundaries** — catch rendering errors with fallback UI:
422
576
 
423
- ```typescript
424
- import { createReducer } from '@getforma/core';
577
+ ```ts
578
+ import { createErrorBoundary, h } from "@getforma/core";
425
579
 
426
- const [state, dispatch] = createReducer(
427
- (state, action) => {
428
- switch (action.type) {
429
- case 'INCREMENT': return { count: state.count + 1 };
430
- case 'DECREMENT': return { count: state.count - 1 };
431
- default: return state;
432
- }
433
- },
434
- { count: 0 },
580
+ createErrorBoundary(
581
+ () => h(UnstableComponent),
582
+ (error, retry) =>
583
+ h("div", null,
584
+ h("p", null, `Something went wrong: ${error.message}`),
585
+ h("button", { onClick: retry }, "Retry"),
586
+ ),
435
587
  );
436
-
437
- dispatch({ type: 'INCREMENT' }); // state() === { count: 1 }
438
588
  ```
439
589
 
590
+ ---
591
+
440
592
  ## Islands Architecture
441
593
 
442
- For server-rendered HTML, activate independent interactive regions. Each island callback receives the root DOM element and parsed props, then returns a component tree — the same `h()` calls you'd use for client-side rendering. The hydration system walks the descriptor tree against the existing SSR DOM, attaching event handlers and reactive bindings without recreating elements.
594
+ Hydrate independent interactive regions of server-rendered HTML. Each island callback receives the root element and parsed props, then returns a component tree. The hydration system walks the tree against existing SSR DOM, attaching handlers and reactive bindings without recreating elements.
443
595
 
444
- ```typescript
445
- import { activateIslands, createSignal, h } from '@getforma/core';
596
+ ```ts
597
+ import { activateIslands, createSignal, h } from "@getforma/core";
446
598
 
447
599
  activateIslands({
448
600
  Counter: (el, props) => {
449
601
  const [count, setCount] = createSignal(props?.initial ?? 0);
450
602
 
451
- // el is the island's root HTMLElement — useful for layout measurement,
452
- // focus management, CSS classes, or reading extra data-* attributes.
453
- el.classList.add('is-hydrated');
603
+ el.classList.add("is-hydrated");
454
604
 
455
- // Return the same tree shape as the SSR output.
456
- // Hydration matches this against existing DOM — no elements are created.
457
- return h('div', null,
458
- h('span', null, () => String(count())),
459
- h('button', { onClick: () => setCount(c => c + 1) }, '+1'),
605
+ return h("div", null,
606
+ h("span", null, () => String(count())),
607
+ h("button", { onClick: () => setCount((c) => c + 1) }, "+1"),
460
608
  );
461
609
  },
462
610
  });
@@ -470,135 +618,169 @@ activateIslands({
470
618
  </div>
471
619
  ```
472
620
 
473
- Each island is activated inside its own `createRoot` scope with error isolation — a broken island never takes down its siblings.
621
+ Each island runs in its own `createRoot` scope with error isolation — a broken island never takes down its siblings.
474
622
 
475
623
  ### Hydration Triggers
476
624
 
477
625
  Control when an island hydrates via `data-forma-hydrate`:
478
626
 
479
627
  | Trigger | When it hydrates | Use case |
480
- |---------|-----------------|----------|
481
- | `load` (default) | Immediately on page load | Above-the-fold interactive content |
628
+ |---|---|---|
629
+ | `load` (default) | Immediately on page load | Above-the-fold content |
482
630
  | `visible` | When island enters viewport | Below-the-fold components |
483
- | `idle` | During browser idle time (`requestIdleCallback`) | Non-critical functionality |
484
- | `interaction` | On first `pointerdown` or `focusin` | Skeleton+skin pattern |
631
+ | `idle` | During browser idle time | Non-critical functionality |
632
+ | `interaction` | On first `pointerdown` or `focusin` | Skeleton + skin pattern |
485
633
 
486
634
  ```html
487
635
  <div data-forma-island="1" data-forma-component="Comments" data-forma-hydrate="visible">
488
- <!-- Only loads JS when scrolled into view -->
636
+ <!-- JS loads only when scrolled into view -->
489
637
  </div>
490
638
  ```
491
639
 
492
640
  ### Island Disposal
493
641
 
494
- When swapping module content (e.g., inside `<forma-stage>` Shadow DOM), dispose islands to prevent leaked effects and listeners:
642
+ When swapping content (e.g., inside `<forma-stage>` Shadow DOM), dispose islands to prevent leaked effects:
495
643
 
496
- ```typescript
497
- import { deactivateIsland, deactivateAllIslands } from '@getforma/core';
644
+ ```ts
645
+ import { deactivateIsland, deactivateAllIslands } from "@getforma/core";
498
646
 
499
- // Dispose all active islands under a root
500
647
  deactivateAllIslands(shadowRoot);
501
-
502
- // Or dispose a single island
503
648
  deactivateIsland(islandElement);
504
649
  ```
505
650
 
651
+ ---
652
+
653
+ ## CDN Builds
654
+
655
+ ### Script tag (IIFE — auto-initializes)
656
+
657
+ ```html
658
+ <!-- jsDelivr (recommended) -->
659
+ <script src="https://cdn.jsdelivr.net/npm/@getforma/core@1.0.1/dist/formajs-runtime.global.js"></script>
660
+
661
+ <!-- unpkg -->
662
+ <script src="https://unpkg.com/@getforma/core@1.0.1/dist/formajs-runtime.global.js"></script>
663
+ ```
664
+
665
+ ### ESM import (modern browsers, no bundler)
666
+
667
+ ```html
668
+ <script type="module">
669
+ import { createSignal, h, mount } from "https://cdn.jsdelivr.net/npm/@getforma/core@1.0.1/dist/index.js";
670
+
671
+ const [count, setCount] = createSignal(0);
672
+ mount(() => h("button", { onClick: () => setCount((c) => c + 1) }, () => `${count()}`), "#app");
673
+ </script>
674
+ ```
675
+
676
+ ### All builds
677
+
678
+ | Build | Filename |
679
+ |---|---|
680
+ | Standard (recommended) | `formajs-runtime.global.js` |
681
+ | CSP-safe (no `new Function`) | `formajs-runtime-hardened.global.js` |
682
+ | Standard (short alias) | `forma-runtime.js` |
683
+ | CSP-safe (short alias) | `forma-runtime-csp.js` |
684
+
685
+ Available from `unpkg.com/@getforma/core@VERSION/dist/` and `cdn.jsdelivr.net/npm/@getforma/core@VERSION/dist/`.
686
+
687
+ ---
688
+
506
689
  ## Subpath Exports
507
690
 
508
- The main entry point (`@getforma/core`) has **zero network code** — no `fetch`, no WebSocket, no `process.env`. Network-capable modules are separate subpath imports.
691
+ The main entry point (`@getforma/core`) has **zero network code** — no fetch, no WebSocket, no `process.env`. Network-capable modules are separate imports:
509
692
 
510
693
  | Import | Description |
511
- |--------|-------------|
512
- | `@getforma/core` | Signals, `h()`, `mount()`, lists, stores, components, islands, events, DOM utils |
694
+ |---|---|
695
+ | `@getforma/core` | Signals, `h()`, mount, lists, stores, components, islands, events, DOM utils |
513
696
  | `@getforma/core/http` | `createFetch`, `fetchJSON`, `createSSE`, `createWebSocket` |
514
697
  | `@getforma/core/storage` | `createLocalStorage`, `createSessionStorage`, `createIndexedDB` |
515
698
  | `@getforma/core/server` | `createAction`, `$$serverFunction`, `handleRPC`, `createRPCMiddleware` |
516
699
  | `@getforma/core/runtime` | HTML Runtime — `initRuntime()`, `mount()`, `unmount()` |
517
- | `@getforma/core/runtime/global` | HTML Runtime global build (IIFE, for `<script>` tags) |
518
700
  | `@getforma/core/runtime-hardened` | Runtime with `new Function()` locked off (strict CSP) |
519
- | `@getforma/core/runtime-csp` | Alias for `runtime-hardened` (CSP-safe build) |
520
- | `@getforma/core/runtime-csp/global` | CSP-safe global build (IIFE, for `<script>` tags) |
521
701
  | `@getforma/core/ssr` | Server-side rendering — `renderToString()`, `renderToStream()` |
522
702
  | `@getforma/core/tc39` | TC39-compatible `Signal.State` and `Signal.Computed` classes |
523
703
 
524
- ```typescript
525
- // Core (zero network code)
526
- import { createSignal, h, mount, createStore } from '@getforma/core';
704
+ ```ts
705
+ // Core zero network code
706
+ import { createSignal, h, mount, createStore } from "@getforma/core";
527
707
 
528
- // HTTP (only if needed)
529
- import { createFetch, createSSE } from '@getforma/core/http';
708
+ // HTTP only when needed
709
+ import { createFetch, createSSE } from "@getforma/core/http";
530
710
 
531
- // Storage (only if needed)
532
- import { createLocalStorage } from '@getforma/core/storage';
711
+ // Storage only when needed
712
+ import { createLocalStorage } from "@getforma/core/storage";
533
713
 
534
- // Server (only if needed)
535
- import { createAction, $$serverFunction } from '@getforma/core/server';
714
+ // Server only when needed
715
+ import { createAction, $$serverFunction } from "@getforma/core/server";
536
716
  ```
537
717
 
538
- ## Examples
718
+ ---
539
719
 
540
- See the [`examples/`](./examples) directory:
720
+ ## How Is This Different from Solid?
541
721
 
542
- - **counter**minimal `h()` counter
543
- - **counter-jsx** — same counter with JSX syntax
544
- - **csp** — CSP-safe runtime with strict Content-Security-Policy meta tag
545
- - **todo** — todo list with `createList` and keyed reconciliation
546
- - **data-table** — sortable table with `createList`
722
+ FormaJS shares Solid's core insight fine-grained signals updating the real DOM without a virtual DOM. If you know Solid, you'll feel at home.
547
723
 
548
- ## How Is This Different from Solid?
724
+ The differences are in scope and delivery: FormaJS adds built-in islands hydration without a meta-framework, CSP compliance without a build step, three entry points (CDN, hyperscript, JSX) sharing one signal graph, and a Rust SSR path that eliminates Node.js from the server. Solid gives you a mature JavaScript ecosystem with routing, a meta-framework (SolidStart), devtools, and community component libraries.
549
725
 
550
- FormaJS shares Solid's core insight fine-grained signals updating the real DOM without a virtual DOM. If you know Solid, you'll feel at home. The differences are in scope and delivery:
726
+ **Choose FormaJS** when you want islands baked in, CSP safety out of the box, a Rust backend without a Node.js sidecar, or a CDN-first starting point that scales to a full compiled pipeline.
551
727
 
552
- | | Solid | FormaJS |
553
- |-|-------|---------|
554
- | **Build requirement** | Always needs a compiler (JSX transform) | CDN runtime works with zero build step; bundler is optional |
555
- | **Entry points** | JSX-first | HTML Runtime (`data-*` attributes), `h()` hyperscript, or JSX |
556
- | **CSP** | Relies on compiler output | Hand-written expression parser; hardened build has no `new Function()` |
557
- | **Islands** | Via [solid-start](https://start.solidjs.com/) meta-framework | Built-in `activateIslands()` — no meta-framework needed |
558
- | **Ecosystem** | Mature (router, meta-framework, devtools) | Minimal — reactive core only, you bring the architecture |
559
- | **SSR runtime** | Node.js required | Node.js via `renderToString`, or Rust walker (no JS runtime on the server) |
560
- | **Size** | ~7KB | ~15KB (includes runtime parser, stores, SSR) |
728
+ **Choose Solid** when you want a mature JS ecosystem, SolidStart for full-stack JS, community devtools, and your backend is already Node.js.
561
729
 
562
- **When to choose FormaJS:** You want islands hydration built into the library, not bolted on through a meta-framework. You need CSP compliance without a build step. You want three entry points (CDN, hyperscript, JSX) sharing one signal graph. Or you're building on a Rust backend and want your frontend reactive layer to integrate natively with the server stack.
730
+ > FormaJS is the reactive layer of the [Forma stack](https://getforma.dev). The full pipeline compiles components to FMIR binary, renders them in Rust via `forma-ir`, and serves pages through `forma-server` SSR without Node.js, binary IR over the wire, deployed for ~$18/month.
563
731
 
564
- **When to choose Solid:** You want a mature JavaScript ecosystem with routing, SSR meta-framework (SolidStart), devtools, and community-built component libraries. Your backend is Node.js and you want SSR in the same language as your frontend.
732
+ ---
565
733
 
566
- > FormaJS is the reactive layer of the [Forma stack](https://getforma.dev). The full pipeline compiles components to a binary IR (FMIR), renders them in Rust via `forma-ir`, and serves pages through `forma-server` — SSR without Node.js, binary IR over the wire, deployed for ~$18/month.
734
+ ## Examples
567
735
 
568
- ## Stability
736
+ See the [`examples/`](./examples) directory:
737
+
738
+ | Example | Description |
739
+ |---|---|
740
+ | **counter** | Minimal `h()` counter |
741
+ | **counter-jsx** | Same counter with JSX syntax |
742
+ | **csp** | CSP-safe runtime with strict `Content-Security-Policy` |
743
+ | **todo** | Todo list with `createList` and keyed reconciliation |
744
+ | **data-table** | Sortable table with `createList` |
745
+
746
+ ---
569
747
 
570
- Some features are more battle-tested than others:
748
+ ## Stability
571
749
 
572
750
  | Feature | Status | Notes |
573
- |---------|--------|-------|
574
- | Signals (`createSignal`, `createEffect`, `createComputed`, `batch`) | **Stable** | Core primitive, well-tested. Custom `equals` option supported. |
751
+ |---|---|---|
752
+ | Signals (`createSignal`, `createEffect`, `createComputed`, `batch`) | **Stable** | Core primitive. Custom `equals` supported. |
575
753
  | Reactive introspection (`isSignal`, `isComputed`, `trigger`, `getBatchDepth`) | **Stable** | alien-signals 3.x type guards |
576
- | `h()` / JSX rendering | **Stable** | |
754
+ | `h()` / JSX rendering | **Stable** | Function components supported |
577
755
  | `mount()`, `createShow`, `createSwitch`, `createList` | **Stable** | |
578
- | HTML Runtime (`data-*` directives) | **Stable** | Expression parser covers common patterns |
579
- | CSP-hardened runtime | **Stable** | No `new Function()`, tested with strict CSP headers |
756
+ | HTML Runtime (`data-*` directives) | **Stable** | CSP-safe expression parser |
757
+ | CSP-hardened runtime | **Stable** | Zero `new Function()` in dist |
580
758
  | `createStore` (deep reactivity) | **Stable** | |
581
759
  | Components (`defineComponent`, lifecycle) | **Stable** | |
582
760
  | Context (`createContext`, `provide`, `inject`) | **Stable** | |
583
761
  | Islands (`activateIslands`, disposal, triggers) | **Stable** | 10 activation + 88 hydration + 10 trigger tests |
584
762
  | `createHistory` (undo/redo) | **Stable** | |
585
- | `createReducer` | **Stable** | 8 tests |
586
- | `data-fetch`, `data-transition:*` | **Stable** | Fully implemented in HTML Runtime |
763
+ | `createReducer` | **Stable** | |
764
+ | `data-fetch`, `data-transition:*`, `data-ref` | **Stable** | |
587
765
  | SSR (`renderToString`, `renderToStream`) | **Beta** | Functional, API may evolve |
588
- | TC39 Signals compat (`Signal.State`, `Signal.Computed`) | **Beta** | 9 tests, but tracks an evolving TC39 proposal |
766
+ | TC39 Signals compat (`Signal.State`, `Signal.Computed`) | **Beta** | Tracks an evolving TC39 proposal |
767
+
768
+ ---
589
769
 
590
770
  ## Ecosystem
591
771
 
592
- FormaJS is the reactive frontend layer of a full-stack Rust + TypeScript framework. The pipeline flows: TypeScript components → `@getforma/compiler` → FMIR binary → `forma-ir` (parse) → `forma-server` (render) → Axum HTTP response.
772
+ FormaJS is the reactive frontend layer of a full-stack Rust + TypeScript framework.
593
773
 
594
774
  | Package | Language | Description |
595
- |---------|----------|-------------|
775
+ |---|---|---|
596
776
  | [@getforma/core](https://www.npmjs.com/package/@getforma/core) | TypeScript | This library — reactive DOM, signals, islands, SSR hydration |
597
777
  | [@getforma/compiler](https://github.com/getforma-dev/forma-tools) | TypeScript | TypeScript-to-FMIR compiler, Vite plugin, esbuild SSR plugin |
598
778
  | [@getforma/build](https://github.com/getforma-dev/forma-tools) | TypeScript | esbuild pipeline with content hashing, compression, manifest |
599
779
  | [@getforma/create-app](https://github.com/getforma-dev/create-forma-app) | TypeScript | `npx @getforma/create-app` — scaffold a new Forma project |
600
780
  | [forma-ir](https://crates.io/crates/forma-ir) | Rust | FMIR binary format: parser, walker, WASM exports |
601
- | [forma-server](https://crates.io/crates/forma-server) | Rust | Axum middleware for SSR page rendering, asset serving, CSP |
781
+ | [forma-server](https://crates.io/crates/forma-server) | Rust | Axum middleware for SSR, asset serving, CSP |
782
+
783
+ ---
602
784
 
603
785
  ## License
604
786