@getforma/core 1.0.1 → 1.0.3

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
+ ---
22
49
 
23
- After `npm install`, you need a bundler to resolve the ES module imports. Here's a minimal Vite setup:
50
+ ## Getting Started with a Bundler
51
+
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,333 @@ 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
+ ---
57
89
 
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.
90
+ ## Coming from React?
59
91
 
60
- **Design principles:**
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.
61
93
 
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.
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`.
67
95
 
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.
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 |
108
+
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."
110
+
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";
159
+
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.
73
174
 
74
- Drop a script tag, write `data-*` attributes. Zero config, zero tooling.
175
+ ### 3. HTML Runtime (no build step)
176
+
177
+ One `<script>` tag. One HTML file. No npm, no bundler, no `node_modules`, no config files. Just open it in a browser.
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
+ That's a working reactive counter. No JavaScript file. No build step. Just HTML.
190
+
191
+ **Here's what you get from a single HTML file with one script tag:**
192
+
193
+ ```html
194
+ <script src="https://cdn.jsdelivr.net/npm/@getforma/core@latest/dist/formajs-runtime.global.js"></script>
195
+
196
+ <div data-forma-state='{
197
+ "query": "",
198
+ "items": ["Apples", "Bananas", "Cherries", "Dates", "Elderberries"],
199
+ "darkMode": false
200
+ }'>
201
+
202
+ <!-- Two-way binding: type in the input, the list filters instantly -->
203
+ <input data-model="{query}" placeholder="Search fruits...">
204
+
205
+ <!-- Computed value: derived from query, updates automatically -->
206
+ <p data-computed="matchCount = items.filter(i => i.toLowerCase().includes(query.toLowerCase())).length"
207
+ data-text="{'Found ' + matchCount + ' results'}"></p>
208
+
209
+ <!-- Conditional rendering: show/hide based on state -->
210
+ <p data-show="{query.length > 0 && matchCount === 0}">No matches found.</p>
211
+
212
+ <!-- List rendering: keyed reconciliation, only changed items re-render -->
213
+ <ul data-list="{items.filter(i => i.toLowerCase().includes(query.toLowerCase()))}">
214
+ <li>{item}</li>
215
+ </ul>
216
+
217
+ <!-- Event handling with $dispatch: cross-component communication -->
218
+ <button data-on:click="{darkMode = !darkMode}">
219
+ Toggle Dark Mode
220
+ </button>
221
+
222
+ <!-- Dynamic classes and attributes -->
223
+ <div data-class:dark="{darkMode}" data-bind:data-theme="{darkMode ? 'dark' : 'light'}">
224
+ Theme is: <span data-text="{darkMode ? 'Dark' : 'Light'}"></span>
225
+ </div>
226
+
227
+ <!-- Persist to localStorage: survives page refresh -->
228
+ <div data-persist="{darkMode}"></div>
229
+ </div>
230
+ ```
231
+
232
+ That single HTML file gives you: reactive state, two-way data binding, computed values, conditional rendering, list rendering with filtering, event handling, dynamic CSS classes, dynamic attributes, and localStorage persistence. **No JavaScript written. No build tools installed.**
233
+
234
+ The expression parser is hand-written and CSP-safe — no `eval()`, no `new Function()` by default. For strict CSP environments, use the hardened build:
235
+
236
+ ```html
237
+ <script src="https://cdn.jsdelivr.net/npm/@getforma/core@latest/dist/formajs-runtime-hardened.global.js"></script>
238
+ ```
239
+
240
+ <details>
241
+ <summary><strong>Full directive reference</strong></summary>
87
242
 
88
243
  | Directive | Description | Example |
89
- |-----------|-------------|---------|
244
+ |---|---|---|
90
245
  | `data-forma-state` | Declare reactive state (JSON) | `data-forma-state='{"count": 0}'` |
91
246
  | `data-text` | Bind text content | `data-text="{count}"` |
92
247
  | `data-show` | Toggle visibility (display) | `data-show="{isOpen}"` |
93
- | `data-if` | Conditional render (add/remove from DOM) | `data-if="{loggedIn}"` |
248
+ | `data-if` | Conditional render (add/remove DOM) | `data-if="{loggedIn}"` |
94
249
  | `data-model` | Two-way binding (inputs) | `data-model="{email}"` |
95
250
  | `data-on:event` | Event handler | `data-on:click="{count++}"` |
96
251
  | `data-class:name` | Conditional CSS class | `data-class:active="{isActive}"` |
97
252
  | `data-bind:attr` | Dynamic attribute | `data-bind:href="{url}"` |
98
- | `data-list` | List rendering with keyed reconciliation | `data-list="{items}"` |
253
+ | `data-list` | List rendering (keyed reconciliation) | `data-list="{items}"` |
99
254
  | `data-computed` | Computed value | `data-computed="doubled = count * 2"` |
100
255
  | `data-persist` | Persist state to localStorage | `data-persist="{count}"` |
101
256
  | `data-fetch` | Fetch data from URL | `data-fetch="GET /api/items → items"` |
102
257
  | `data-transition:*` | Enter/leave CSS transitions | `data-transition:enter="fade-in"` |
103
258
  | `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})}"` |
259
+ | `$el` | Current DOM element | `data-on:click="{$el.classList.toggle('active')}"` |
260
+ | `$dispatch` | Fire CustomEvent (bubbles, crosses Shadow DOM) | `data-on:click="{$dispatch('selected', {id})}"` |
106
261
  | `$refs` | Named element references | `data-on:click="{$refs.myInput.focus()}"` |
107
262
 
108
- CSP-safe expression parser — no `eval()` or `new Function()` by default. For strict CSP environments, use the hardened build:
263
+ </details>
109
264
 
110
- ```html
111
- <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime-hardened.global.js"></script>
112
- ```
265
+ ---
113
266
 
114
- ### 2. Hyperscript — `h()`
267
+ ## The `h()` Function
115
268
 
116
- ```bash
117
- npm install @getforma/core
269
+ `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.
270
+
271
+ ### Signature
272
+
273
+ ```ts
274
+ h(tag, props?, ...children)
118
275
  ```
119
276
 
120
- ```typescript
121
- import { createSignal, h, mount } from '@getforma/core';
277
+ | Parameter | Type | Description |
278
+ |---|---|---|
279
+ | `tag` | `string \| Function` | An HTML tag name (`'div'`, `'button'`) or a component function (`Counter`) |
280
+ | `props` | `object \| null` | Attributes, event handlers, and component props. Pass `null` or `{}` to skip. |
281
+ | `children` | `string \| number \| () => string \| Node \| Array` | Zero or more children — static text, reactive functions, elements, or arrays of any of these. |
122
282
 
123
- const [count, setCount] = createSignal(0);
283
+ ### The key rule
124
284
 
125
- mount(() =>
126
- h('button', { onClick: () => setCount(count() + 1) },
127
- () => `Clicked ${count()} times`
128
- ),
129
- '#app'
130
- );
131
- ```
285
+ **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
286
 
133
- ### 3. JSX
287
+ ### Patterns
134
288
 
135
- Same `h()` function, JSX syntax. Configure your bundler:
289
+ ```ts
290
+ // Static text child
291
+ h("footer", { class: "text-sm" }, "Built with Forma")
136
292
 
137
- ```json
138
- // tsconfig.json
139
- {
140
- "compilerOptions": {
141
- "jsx": "react",
142
- "jsxFactory": "h",
143
- "jsxFragmentFactory": "Fragment"
144
- }
145
- }
146
- ```
293
+ // Reactive text child — updates when count() changes
294
+ h("button", { onClick: fn }, () => `Count: ${count()}`)
147
295
 
148
- ```tsx
149
- import { createSignal, h, Fragment, mount } from '@getforma/core';
296
+ // Multiple children
297
+ h("div", { class: "card" },
298
+ h("h2", null, "Title"),
299
+ h("p", null, "Body text"),
300
+ h("button", { onClick: fn }, "Click"),
301
+ )
150
302
 
151
- const [count, setCount] = createSignal(0);
303
+ // Children as an array (useful for dynamic lists)
304
+ h("ul", null, items.map(item => h("li", null, item.name)))
152
305
 
153
- function Counter() {
154
- return (
155
- <>
156
- <p>{() => `Count: ${count()}`}</p>
157
- <button onClick={() => setCount(count() + 1)}>+1</button>
158
- </>
159
- );
160
- }
306
+ // No props, just children
307
+ h("p", null, "Hello world")
161
308
 
162
- mount(() => <Counter />, '#app');
309
+ // Component function with props
310
+ h(Counter, { initial: 5 })
311
+
312
+ // Nested reactive children
313
+ h("div", null,
314
+ () => showHeader() ? h("h1", null, "Welcome") : null,
315
+ h("p", null, () => `You have ${count()} items`),
316
+ )
163
317
  ```
164
318
 
165
- ## CDN Usage
319
+ ### JSX equivalence
166
320
 
167
- ### Script tag (IIFE auto-initializes)
321
+ JSX is syntactic sugar that compiles to `h()` calls. These are identical:
168
322
 
169
- ```html
170
- <!-- unpkg -->
171
- <script src="https://unpkg.com/@getforma/core@1.0.0/dist/formajs-runtime.global.js"></script>
323
+ ```tsx
324
+ // JSX
325
+ <button class="btn" onClick={() => setCount((c) => c + 1)}>
326
+ {() => `Count: ${count()}`}
327
+ </button>
172
328
 
173
- <!-- jsDelivr (faster globally) -->
174
- <script src="https://cdn.jsdelivr.net/npm/@getforma/core@1.0.0/dist/formajs-runtime.global.js"></script>
329
+ // h()
330
+ h("button", { class: "btn", onClick: () => setCount((c) => c + 1) },
331
+ () => `Count: ${count()}`
332
+ )
175
333
  ```
176
334
 
177
- ### ESM import (no bundler, modern browsers)
335
+ ---
178
336
 
179
- ```html
180
- <script type="module">
181
- import { createSignal, h, mount } from 'https://cdn.jsdelivr.net/npm/@getforma/core@1.0.0/dist/index.js';
337
+ ## Why FormaJS?
182
338
 
183
- const [count, setCount] = createSignal(0);
184
- mount(() => h('button', { onClick: () => setCount(count() + 1) }, () => `${count()}`), '#app');
185
- </script>
186
- ```
339
+ 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
340
 
188
- ### All CDN builds
341
+ **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
342
 
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` |
343
+ **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.
344
+
345
+ **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.
346
+
347
+ **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.
348
+
349
+ **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).
350
+
351
+ **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.
352
+
353
+ ---
196
354
 
197
- Available from both `unpkg.com/@getforma/core@VERSION/dist/` and `cdn.jsdelivr.net/npm/@getforma/core@VERSION/dist/`.
355
+ ## The Rust Compiler (Optional)
198
356
 
199
- For production, always pin the version (e.g., `@1.0.0`). Unversioned URLs resolve to latest.
357
+ 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.
200
358
 
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.
359
+ ### What the compiler does
360
+
361
+ | Without compiler | With compiler |
362
+ |---|---|
363
+ | `h()` calls create DOM elements at runtime | `h()` calls are pre-compiled to `template()` + `cloneNode()` for faster initial render |
364
+ | SSR requires Node.js (`renderToString`) | SSR runs natively in Rust via the FMIR binary walker — no JS runtime on the server |
365
+ | Standard JS bundle shipped to the client | Components compile to FMIR (Forma Module IR), a compact binary format sent over the wire |
366
+ | Islands hydrate from HTML + JS | Islands hydrate from FMIR binary — smaller payload, faster parse |
367
+
368
+ ### When to add it
369
+
370
+ You don't need the compiler to get started, prototype, or even ship to production. Add it when:
371
+
372
+ - **SSR without Node.js** — your backend is Rust/Axum and you don't want a Node.js sidecar just for rendering.
373
+ - **Faster initial render** — pre-compiled templates skip the `h()` → `createElement` path and go straight to `cloneNode()`.
374
+ - **Smaller payloads** — FMIR binary is more compact than the equivalent JavaScript for complex component trees.
375
+ - **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.
376
+
377
+ ### Architecture
378
+
379
+ ```
380
+ TypeScript/JSX components
381
+
382
+ @getforma/compiler (TS → FMIR binary)
383
+
384
+ forma-ir (Rust: parse + walk FMIR)
385
+
386
+ forma-server (Rust/Axum: SSR + asset serving + CSP)
387
+
388
+ HTML response (server-rendered, islands hydrate on client)
389
+ ```
390
+
391
+ All entry points — JSX, `h()`, and the HTML Runtime — work both with and without the compiler:
392
+
393
+ | | Without Compiler | With Compiler |
394
+ |---|---|---|
395
+ | HTML Runtime | `data-*` directives | + SSR from IR walker |
396
+ | `h()` hyperscript | `createSignal` + `h()` | + compiled templates |
397
+ | JSX | `createSignal` + JSX | + compiled templates + SSR |
398
+ | Islands | `activateIslands()` | + FMIR hydration |
399
+
400
+ ---
203
401
 
204
402
  ## Core API
205
403
 
206
404
  ### Signals
207
405
 
208
- ```typescript
209
- import { createSignal, createEffect, createComputed, batch } from '@getforma/core';
406
+ ```ts
407
+ import { createSignal, createEffect, createComputed, batch } from "@getforma/core";
210
408
 
211
409
  const [count, setCount] = createSignal(0);
212
410
  const doubled = createComputed(() => count() * 2);
213
411
 
214
- createEffect(() => console.log('count:', count()));
412
+ createEffect(() => console.log("count:", count()));
215
413
 
216
414
  batch(() => {
217
415
  setCount(1);
@@ -219,25 +417,21 @@ batch(() => {
219
417
  });
220
418
  ```
221
419
 
222
- #### Custom Equality
420
+ **Custom equality** — skip updates when the value hasn't meaningfully changed:
223
421
 
224
- Skip updates when the new value is equal to the current value:
225
-
226
- ```typescript
422
+ ```ts
227
423
  const [pos, setPos] = createSignal(
228
424
  { x: 0, y: 0 },
229
425
  { equals: (a, b) => a.x === b.x && a.y === b.y },
230
426
  );
231
427
 
232
- setPos({ x: 0, y: 0 }); // skipped — values are equal
233
- setPos({ x: 1, y: 0 }); // applied — values differ
428
+ setPos({ x: 0, y: 0 }); // skipped — equal
429
+ setPos({ x: 1, y: 0 }); // applied — different
234
430
  ```
235
431
 
236
- #### Computed with Previous Value
237
-
238
- The computed getter receives the previous value for efficient diffing:
432
+ **Computed with previous value** — the getter receives the previous result:
239
433
 
240
- ```typescript
434
+ ```ts
241
435
  const changes = createComputed((prev) => {
242
436
  const current = items();
243
437
  if (prev) console.log(`${prev.length} → ${current.length} items`);
@@ -245,218 +439,217 @@ const changes = createComputed((prev) => {
245
439
  });
246
440
  ```
247
441
 
248
- #### Reactive Introspection
249
-
250
- Type guards and utilities from alien-signals 3.x:
442
+ **Reactive introspection** — type guards and utilities from alien-signals 3.x:
251
443
 
252
- ```typescript
253
- import { isSignal, isComputed, getBatchDepth, trigger } from '@getforma/core';
444
+ ```ts
445
+ import { isSignal, isComputed, getBatchDepth, trigger } from "@getforma/core";
254
446
 
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
447
+ isSignal(count); // true
448
+ isComputed(doubled); // true
449
+ getBatchDepth(); // 0 outside batch, 1+ inside
450
+ trigger(doubled); // force recomputation
259
451
  ```
260
452
 
261
453
  ### Conditional Rendering
262
454
 
263
- ```typescript
264
- import { createSignal, createShow, createSwitch, h } from '@getforma/core';
455
+ ```ts
456
+ import { createSignal, createShow, createSwitch, h } from "@getforma/core";
265
457
 
266
458
  const [loggedIn, setLoggedIn] = createSignal(false);
267
459
 
268
- // createShow — toggle between two branches
269
- createShow(loggedIn,
270
- () => h('p', null, 'Welcome back'),
271
- () => h('p', null, 'Please sign in'),
460
+ // Two branches
461
+ createShow(
462
+ loggedIn,
463
+ () => h("p", null, "Welcome back"),
464
+ () => h("p", null, "Please sign in"),
272
465
  );
273
466
 
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'));
467
+ // Multi-branch with caching
468
+ const [view, setView] = createSignal("home");
469
+
470
+ createSwitch(
471
+ view,
472
+ [
473
+ { match: "home", render: () => h("div", null, "Home") },
474
+ { match: "settings", render: () => h("div", null, "Settings") },
475
+ ],
476
+ () => h("div", null, "404 Not Found"),
477
+ );
280
478
  ```
281
479
 
282
480
  ### List Rendering
283
481
 
284
- ```typescript
285
- import { createSignal, createList, h } from '@getforma/core';
482
+ ```ts
483
+ import { createSignal, createList, h } from "@getforma/core";
286
484
 
287
485
  const [items, setItems] = createSignal([
288
- { id: 1, name: 'Alice' },
289
- { id: 2, name: 'Bob' },
486
+ { id: 1, name: "Alice" },
487
+ { id: 2, name: "Bob" },
290
488
  ]);
291
489
 
292
490
  createList(
293
491
  items,
294
- (item) => item.id, // key function
295
- (item) => h('li', null, item.name),
492
+ (item) => item.id,
493
+ (item) => h("li", null, item.name),
296
494
  );
297
495
  ```
298
496
 
299
- ### Store (deep reactivity)
497
+ ### Store (Deep Reactivity)
300
498
 
301
- ```typescript
302
- import { createStore } from '@getforma/core';
499
+ ```ts
500
+ import { createStore } from "@getforma/core";
303
501
 
304
502
  const [state, setState] = createStore({
305
- user: { name: 'Alice', prefs: { theme: 'dark' } },
503
+ user: { name: "Alice", prefs: { theme: "dark" } },
306
504
  items: [1, 2, 3],
307
505
  });
308
506
 
309
507
  // Read reactively — tracked at the exact property path
310
- state.user.name; // 'Alice'
311
- state.items[0]; // 1
508
+ state.user.name; // "Alice"
509
+ state.items[0]; // 1
312
510
 
313
- // Setter API — partial object merge (batched)
314
- setState({ user: { ...state.user, name: 'Bob' } });
315
- setState(prev => ({ items: [...prev.items, 4] }));
511
+ // Setter API — partial merge
512
+ setState({ user: { ...state.user, name: "Bob" } });
513
+ setState((prev) => ({ items: [...prev.items, 4] }));
316
514
 
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
515
+ // Or mutate directly — only affected subscribers update
516
+ state.user.name = "Bob";
517
+ state.items.push(4);
320
518
  ```
321
519
 
322
- ### Components
520
+ > **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
521
 
324
- ```typescript
325
- import { defineComponent, onMount, onUnmount, h } from '@getforma/core';
522
+ ### Components & Lifecycle
523
+
524
+ ```ts
525
+ import { createSignal, defineComponent, onMount, onUnmount, h } from "@getforma/core";
326
526
 
327
527
  const Timer = defineComponent(() => {
328
528
  const [seconds, setSeconds] = createSignal(0);
329
529
 
330
530
  onMount(() => {
331
- const id = setInterval(() => setSeconds(s => s + 1), 1000);
531
+ const id = setInterval(() => setSeconds((s) => s + 1), 1000);
332
532
  return () => clearInterval(id); // cleanup on unmount
333
533
  });
334
534
 
335
- return h('span', null, () => `${seconds()}s`);
535
+ return h("span", null, () => `${seconds()}s`);
336
536
  });
337
537
 
338
538
  document.body.appendChild(Timer());
339
539
  ```
340
540
 
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.
541
+ `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:
345
542
 
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:
347
-
348
- ```typescript
543
+ ```ts
349
544
  // These are equivalent:
350
545
  onMount(() => {
351
546
  const id = setInterval(tick, 1000);
352
547
  return () => clearInterval(id);
353
548
  });
354
549
 
355
- // vs.
356
550
  onMount(() => {
357
551
  const id = setInterval(tick, 1000);
358
552
  onUnmount(() => clearInterval(id));
359
553
  });
360
554
  ```
361
555
 
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
- ```
556
+ ### Context (Dependency Injection)
370
557
 
371
- **Global error handler.** Register a handler for errors in effects and lifecycle callbacks:
558
+ ```ts
559
+ import { createContext, provide, inject } from "@getforma/core";
372
560
 
373
- ```typescript
374
- import { onError } from '@getforma/core';
561
+ const ThemeCtx = createContext("light");
375
562
 
376
- onError((error, info) => {
377
- console.error(`[${info?.source}]`, error);
378
- });
563
+ provide(ThemeCtx, "dark");
564
+ const theme = inject(ThemeCtx); // "dark"
379
565
  ```
380
566
 
381
- **Error boundaries.** Catch rendering errors and display fallback UI with a retry option:
567
+ ### Reducer
382
568
 
383
- ```typescript
384
- import { createErrorBoundary, h } from '@getforma/core';
569
+ ```ts
570
+ import { createReducer } from "@getforma/core";
385
571
 
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
- ),
572
+ const [state, dispatch] = createReducer(
573
+ (state, action) => {
574
+ switch (action.type) {
575
+ case "INCREMENT": return { count: state.count + 1 };
576
+ case "DECREMENT": return { count: state.count - 1 };
577
+ default: return state;
578
+ }
579
+ },
580
+ { count: 0 },
392
581
  );
582
+
583
+ dispatch({ type: "INCREMENT" }); // state() === { count: 1 }
393
584
  ```
394
585
 
395
- ### Context (Dependency Injection)
586
+ ### History (Undo / Redo)
587
+
588
+ ```ts
589
+ import { createHistory } from "@getforma/core";
396
590
 
397
- ```typescript
398
- import { createContext, provide, inject } from '@getforma/core';
591
+ const [state, setState, { undo, redo, canUndo, canRedo }] = createHistory({ text: "" });
399
592
 
400
- const ThemeCtx = createContext('light');
593
+ setState({ text: "hello" });
594
+ setState({ text: "hello world" });
401
595
 
402
- provide(ThemeCtx, 'dark');
403
- const theme = inject(ThemeCtx); // 'dark'
596
+ undo(); // state.text === "hello"
597
+ canUndo(); // true
598
+ redo(); // state.text === "hello world"
404
599
  ```
405
600
 
406
- ### History (undo/redo)
601
+ ### Error Handling
407
602
 
408
- ```typescript
409
- import { createHistory } from '@getforma/core';
603
+ `mount()` fails fast — if the selector doesn't match, it throws:
410
604
 
411
- const [state, setState, { undo, redo, canUndo, canRedo }] = createHistory({ text: '' });
605
+ ```ts
606
+ mount(() => h("p", null, "hello"), "#nonexistent");
607
+ // Error: mount: container not found — "#nonexistent"
608
+ ```
412
609
 
413
- setState({ text: 'hello' });
414
- setState({ text: 'hello world' });
610
+ **Global error handler** for effects and lifecycle callbacks:
415
611
 
416
- undo(); // state.text === 'hello'
417
- canUndo(); // true
418
- redo(); // state.text === 'hello world'
612
+ ```ts
613
+ import { onError } from "@getforma/core";
614
+
615
+ onError((error, info) => {
616
+ console.error(`[${info?.source}]`, error);
617
+ });
419
618
  ```
420
619
 
421
- ### Reducer
620
+ **Error boundaries** — catch rendering errors with fallback UI:
422
621
 
423
- ```typescript
424
- import { createReducer } from '@getforma/core';
622
+ ```ts
623
+ import { createErrorBoundary, h } from "@getforma/core";
425
624
 
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 },
625
+ createErrorBoundary(
626
+ () => h(UnstableComponent),
627
+ (error, retry) =>
628
+ h("div", null,
629
+ h("p", null, `Something went wrong: ${error.message}`),
630
+ h("button", { onClick: retry }, "Retry"),
631
+ ),
435
632
  );
436
-
437
- dispatch({ type: 'INCREMENT' }); // state() === { count: 1 }
438
633
  ```
439
634
 
635
+ ---
636
+
440
637
  ## Islands Architecture
441
638
 
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.
639
+ 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
640
 
444
- ```typescript
445
- import { activateIslands, createSignal, h } from '@getforma/core';
641
+ ```ts
642
+ import { activateIslands, createSignal, h } from "@getforma/core";
446
643
 
447
644
  activateIslands({
448
645
  Counter: (el, props) => {
449
646
  const [count, setCount] = createSignal(props?.initial ?? 0);
450
647
 
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');
648
+ el.classList.add("is-hydrated");
454
649
 
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'),
650
+ return h("div", null,
651
+ h("span", null, () => String(count())),
652
+ h("button", { onClick: () => setCount((c) => c + 1) }, "+1"),
460
653
  );
461
654
  },
462
655
  });
@@ -470,135 +663,169 @@ activateIslands({
470
663
  </div>
471
664
  ```
472
665
 
473
- Each island is activated inside its own `createRoot` scope with error isolation — a broken island never takes down its siblings.
666
+ Each island runs in its own `createRoot` scope with error isolation — a broken island never takes down its siblings.
474
667
 
475
668
  ### Hydration Triggers
476
669
 
477
670
  Control when an island hydrates via `data-forma-hydrate`:
478
671
 
479
672
  | Trigger | When it hydrates | Use case |
480
- |---------|-----------------|----------|
481
- | `load` (default) | Immediately on page load | Above-the-fold interactive content |
673
+ |---|---|---|
674
+ | `load` (default) | Immediately on page load | Above-the-fold content |
482
675
  | `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 |
676
+ | `idle` | During browser idle time | Non-critical functionality |
677
+ | `interaction` | On first `pointerdown` or `focusin` | Skeleton + skin pattern |
485
678
 
486
679
  ```html
487
680
  <div data-forma-island="1" data-forma-component="Comments" data-forma-hydrate="visible">
488
- <!-- Only loads JS when scrolled into view -->
681
+ <!-- JS loads only when scrolled into view -->
489
682
  </div>
490
683
  ```
491
684
 
492
685
  ### Island Disposal
493
686
 
494
- When swapping module content (e.g., inside `<forma-stage>` Shadow DOM), dispose islands to prevent leaked effects and listeners:
687
+ When swapping content (e.g., inside `<forma-stage>` Shadow DOM), dispose islands to prevent leaked effects:
495
688
 
496
- ```typescript
497
- import { deactivateIsland, deactivateAllIslands } from '@getforma/core';
689
+ ```ts
690
+ import { deactivateIsland, deactivateAllIslands } from "@getforma/core";
498
691
 
499
- // Dispose all active islands under a root
500
692
  deactivateAllIslands(shadowRoot);
501
-
502
- // Or dispose a single island
503
693
  deactivateIsland(islandElement);
504
694
  ```
505
695
 
696
+ ---
697
+
698
+ ## CDN Builds
699
+
700
+ ### Script tag (IIFE — auto-initializes)
701
+
702
+ ```html
703
+ <!-- jsDelivr (recommended) -->
704
+ <script src="https://cdn.jsdelivr.net/npm/@getforma/core@1.0.1/dist/formajs-runtime.global.js"></script>
705
+
706
+ <!-- unpkg -->
707
+ <script src="https://unpkg.com/@getforma/core@1.0.1/dist/formajs-runtime.global.js"></script>
708
+ ```
709
+
710
+ ### ESM import (modern browsers, no bundler)
711
+
712
+ ```html
713
+ <script type="module">
714
+ import { createSignal, h, mount } from "https://cdn.jsdelivr.net/npm/@getforma/core@1.0.1/dist/index.js";
715
+
716
+ const [count, setCount] = createSignal(0);
717
+ mount(() => h("button", { onClick: () => setCount((c) => c + 1) }, () => `${count()}`), "#app");
718
+ </script>
719
+ ```
720
+
721
+ ### All builds
722
+
723
+ | Build | Filename |
724
+ |---|---|
725
+ | Standard (recommended) | `formajs-runtime.global.js` |
726
+ | CSP-safe (no `new Function`) | `formajs-runtime-hardened.global.js` |
727
+ | Standard (short alias) | `forma-runtime.js` |
728
+ | CSP-safe (short alias) | `forma-runtime-csp.js` |
729
+
730
+ Available from `unpkg.com/@getforma/core@VERSION/dist/` and `cdn.jsdelivr.net/npm/@getforma/core@VERSION/dist/`.
731
+
732
+ ---
733
+
506
734
  ## Subpath Exports
507
735
 
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.
736
+ The main entry point (`@getforma/core`) has **zero network code** — no fetch, no WebSocket, no `process.env`. Network-capable modules are separate imports:
509
737
 
510
738
  | Import | Description |
511
- |--------|-------------|
512
- | `@getforma/core` | Signals, `h()`, `mount()`, lists, stores, components, islands, events, DOM utils |
739
+ |---|---|
740
+ | `@getforma/core` | Signals, `h()`, mount, lists, stores, components, islands, events, DOM utils |
513
741
  | `@getforma/core/http` | `createFetch`, `fetchJSON`, `createSSE`, `createWebSocket` |
514
742
  | `@getforma/core/storage` | `createLocalStorage`, `createSessionStorage`, `createIndexedDB` |
515
743
  | `@getforma/core/server` | `createAction`, `$$serverFunction`, `handleRPC`, `createRPCMiddleware` |
516
744
  | `@getforma/core/runtime` | HTML Runtime — `initRuntime()`, `mount()`, `unmount()` |
517
- | `@getforma/core/runtime/global` | HTML Runtime global build (IIFE, for `<script>` tags) |
518
745
  | `@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
746
  | `@getforma/core/ssr` | Server-side rendering — `renderToString()`, `renderToStream()` |
522
747
  | `@getforma/core/tc39` | TC39-compatible `Signal.State` and `Signal.Computed` classes |
523
748
 
524
- ```typescript
525
- // Core (zero network code)
526
- import { createSignal, h, mount, createStore } from '@getforma/core';
749
+ ```ts
750
+ // Core zero network code
751
+ import { createSignal, h, mount, createStore } from "@getforma/core";
527
752
 
528
- // HTTP (only if needed)
529
- import { createFetch, createSSE } from '@getforma/core/http';
753
+ // HTTP only when needed
754
+ import { createFetch, createSSE } from "@getforma/core/http";
530
755
 
531
- // Storage (only if needed)
532
- import { createLocalStorage } from '@getforma/core/storage';
756
+ // Storage only when needed
757
+ import { createLocalStorage } from "@getforma/core/storage";
533
758
 
534
- // Server (only if needed)
535
- import { createAction, $$serverFunction } from '@getforma/core/server';
759
+ // Server only when needed
760
+ import { createAction, $$serverFunction } from "@getforma/core/server";
536
761
  ```
537
762
 
538
- ## Examples
763
+ ---
539
764
 
540
- See the [`examples/`](./examples) directory:
765
+ ## How Is This Different from Solid?
541
766
 
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`
767
+ 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
768
 
548
- ## How Is This Different from Solid?
769
+ 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
770
 
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:
771
+ **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
772
 
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) |
773
+ **Choose Solid** when you want a mature JS ecosystem, SolidStart for full-stack JS, community devtools, and your backend is already Node.js.
561
774
 
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.
775
+ > 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
776
 
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.
777
+ ---
565
778
 
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.
779
+ ## Examples
567
780
 
568
- ## Stability
781
+ See the [`examples/`](./examples) directory:
782
+
783
+ | Example | Description |
784
+ |---|---|
785
+ | **counter** | Minimal `h()` counter |
786
+ | **counter-jsx** | Same counter with JSX syntax |
787
+ | **csp** | CSP-safe runtime with strict `Content-Security-Policy` |
788
+ | **todo** | Todo list with `createList` and keyed reconciliation |
789
+ | **data-table** | Sortable table with `createList` |
790
+
791
+ ---
569
792
 
570
- Some features are more battle-tested than others:
793
+ ## Stability
571
794
 
572
795
  | Feature | Status | Notes |
573
- |---------|--------|-------|
574
- | Signals (`createSignal`, `createEffect`, `createComputed`, `batch`) | **Stable** | Core primitive, well-tested. Custom `equals` option supported. |
796
+ |---|---|---|
797
+ | Signals (`createSignal`, `createEffect`, `createComputed`, `batch`) | **Stable** | Core primitive. Custom `equals` supported. |
575
798
  | Reactive introspection (`isSignal`, `isComputed`, `trigger`, `getBatchDepth`) | **Stable** | alien-signals 3.x type guards |
576
- | `h()` / JSX rendering | **Stable** | |
799
+ | `h()` / JSX rendering | **Stable** | Function components supported |
577
800
  | `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 |
801
+ | HTML Runtime (`data-*` directives) | **Stable** | CSP-safe expression parser |
802
+ | CSP-hardened runtime | **Stable** | Zero `new Function()` in dist |
580
803
  | `createStore` (deep reactivity) | **Stable** | |
581
804
  | Components (`defineComponent`, lifecycle) | **Stable** | |
582
805
  | Context (`createContext`, `provide`, `inject`) | **Stable** | |
583
806
  | Islands (`activateIslands`, disposal, triggers) | **Stable** | 10 activation + 88 hydration + 10 trigger tests |
584
807
  | `createHistory` (undo/redo) | **Stable** | |
585
- | `createReducer` | **Stable** | 8 tests |
586
- | `data-fetch`, `data-transition:*` | **Stable** | Fully implemented in HTML Runtime |
808
+ | `createReducer` | **Stable** | |
809
+ | `data-fetch`, `data-transition:*`, `data-ref` | **Stable** | |
587
810
  | 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 |
811
+ | TC39 Signals compat (`Signal.State`, `Signal.Computed`) | **Beta** | Tracks an evolving TC39 proposal |
812
+
813
+ ---
589
814
 
590
815
  ## Ecosystem
591
816
 
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.
817
+ FormaJS is the reactive frontend layer of a full-stack Rust + TypeScript framework.
593
818
 
594
819
  | Package | Language | Description |
595
- |---------|----------|-------------|
820
+ |---|---|---|
596
821
  | [@getforma/core](https://www.npmjs.com/package/@getforma/core) | TypeScript | This library — reactive DOM, signals, islands, SSR hydration |
597
822
  | [@getforma/compiler](https://github.com/getforma-dev/forma-tools) | TypeScript | TypeScript-to-FMIR compiler, Vite plugin, esbuild SSR plugin |
598
823
  | [@getforma/build](https://github.com/getforma-dev/forma-tools) | TypeScript | esbuild pipeline with content hashing, compression, manifest |
599
824
  | [@getforma/create-app](https://github.com/getforma-dev/create-forma-app) | TypeScript | `npx @getforma/create-app` — scaffold a new Forma project |
600
825
  | [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 |
826
+ | [forma-server](https://crates.io/crates/forma-server) | Rust | Axum middleware for SSR, asset serving, CSP |
827
+
828
+ ---
602
829
 
603
830
  ## License
604
831