@getforma/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +251 -13
  2. package/package.json +3 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm](https://img.shields.io/npm/v/@getforma/core)](https://www.npmjs.com/package/@getforma/core)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- Reactive DOM library with fine-grained signals, islands architecture, and SSR hydration. ~15KB gzipped.
7
+ Reactive DOM library with fine-grained signals. No virtual DOM — `h()` creates real elements, signals update only what changed. ~15KB gzipped.
8
8
 
9
9
  ## Install
10
10
 
@@ -12,7 +12,71 @@ Reactive DOM library with fine-grained signals, islands architecture, and SSR hy
12
12
  npm install @getforma/core
13
13
  ```
14
14
 
15
- ## Quick Start
15
+ Or use the CDN (no build step required):
16
+
17
+ ```html
18
+ <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script>
19
+ ```
20
+
21
+ ## Why FormaJS?
22
+
23
+ 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.
24
+
25
+ **Design principles:**
26
+
27
+ - **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/).
28
+ - **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.
29
+ - **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.
30
+ - **CSP-safe by default.** The HTML Runtime includes a hand-written expression parser. `new Function()` is an opt-in fallback, not a requirement. Ship to strict CSP environments without worry.
31
+ - **Islands over SPAs.** `activateIslands()` hydrates independent regions of server-rendered HTML. Each island is self-contained. Ship less JavaScript, keep server-rendered content instant.
32
+
33
+ **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.
34
+
35
+ ## Three Ways to Use FormaJS
36
+
37
+ ### 1. HTML Runtime (no build step)
38
+
39
+ Drop a script tag, write `data-*` attributes. Zero config, zero tooling.
40
+
41
+ ```html
42
+ <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script>
43
+
44
+ <div data-forma-state='{"count": 0}'>
45
+ <p data-text="{count}"></p>
46
+ <button data-on:click="{count++}">+1</button>
47
+ <button data-on:click="{count = 0}">Reset</button>
48
+ </div>
49
+ ```
50
+
51
+ #### Supported Directives
52
+
53
+ | Directive | Description | Example |
54
+ |-----------|-------------|---------|
55
+ | `data-forma-state` | Declare reactive state (JSON) | `data-forma-state='{"count": 0}'` |
56
+ | `data-text` | Bind text content | `data-text="{count}"` |
57
+ | `data-show` | Toggle visibility (display) | `data-show="{isOpen}"` |
58
+ | `data-if` | Conditional render (add/remove from DOM) | `data-if="{loggedIn}"` |
59
+ | `data-model` | Two-way binding (inputs) | `data-model="{email}"` |
60
+ | `data-on:event` | Event handler | `data-on:click="{count++}"` |
61
+ | `data-class:name` | Conditional CSS class | `data-class:active="{isActive}"` |
62
+ | `data-bind:attr` | Dynamic attribute | `data-bind:href="{url}"` |
63
+ | `data-list` | List rendering with keyed reconciliation | `data-list="{items}"` |
64
+ | `data-computed` | Computed value | `data-computed="doubled = count * 2"` |
65
+ | `data-persist` | Persist state to localStorage | `data-persist="{count}"` |
66
+ | `data-fetch` | Fetch data from URL | `data-fetch="GET /api/items → items"` |
67
+ | `data-transition:*` | Enter/leave CSS transitions | `data-transition:enter="fade-in"` |
68
+
69
+ CSP-safe expression parser — no `eval()` or `new Function()` by default. For strict CSP environments, use the hardened build:
70
+
71
+ ```html
72
+ <script src="https://unpkg.com/@getforma/core/dist/formajs-runtime-hardened.global.js"></script>
73
+ ```
74
+
75
+ ### 2. Hyperscript — `h()`
76
+
77
+ ```bash
78
+ npm install @getforma/core
79
+ ```
16
80
 
17
81
  ```typescript
18
82
  import { createSignal, h, mount } from '@getforma/core';
@@ -27,21 +91,195 @@ mount(() =>
27
91
  );
28
92
  ```
29
93
 
30
- ## Features
94
+ ### 3. JSX
95
+
96
+ Same `h()` function, JSX syntax. Configure your bundler:
97
+
98
+ ```json
99
+ // tsconfig.json
100
+ {
101
+ "compilerOptions": {
102
+ "jsx": "react",
103
+ "jsxFactory": "h",
104
+ "jsxFragmentFactory": "Fragment"
105
+ }
106
+ }
107
+ ```
108
+
109
+ ```tsx
110
+ import { createSignal, h, Fragment, mount } from '@getforma/core';
111
+
112
+ const [count, setCount] = createSignal(0);
113
+
114
+ function Counter() {
115
+ return (
116
+ <>
117
+ <p>{() => `Count: ${count()}`}</p>
118
+ <button onClick={() => setCount(count() + 1)}>+1</button>
119
+ </>
120
+ );
121
+ }
122
+
123
+ mount(() => <Counter />, '#app');
124
+ ```
125
+
126
+ If you use `@getforma/build`, JSX is preconfigured — just write `.tsx` files.
127
+
128
+ ## Core API
129
+
130
+ ### Signals
131
+
132
+ ```typescript
133
+ import { createSignal, createEffect, createComputed, batch } from '@getforma/core';
134
+
135
+ const [count, setCount] = createSignal(0);
136
+ const doubled = createComputed(() => count() * 2);
137
+
138
+ createEffect(() => console.log('count:', count()));
139
+
140
+ batch(() => {
141
+ setCount(1);
142
+ setCount(2); // effect fires once with value 2
143
+ });
144
+ ```
145
+
146
+ ### Conditional Rendering
147
+
148
+ ```typescript
149
+ import { createSignal, createShow, createSwitch, h } from '@getforma/core';
150
+
151
+ const [loggedIn, setLoggedIn] = createSignal(false);
152
+
153
+ // createShow — toggle between two branches
154
+ createShow(loggedIn,
155
+ () => h('p', null, 'Welcome back'),
156
+ () => h('p', null, 'Please sign in'),
157
+ );
158
+
159
+ // createSwitch — multi-branch with caching
160
+ const [view, setView] = createSignal('home');
161
+ createSwitch(view, [
162
+ { match: 'home', render: () => h('div', null, 'Home') },
163
+ { match: 'settings', render: () => h('div', null, 'Settings') },
164
+ ], () => h('div', null, '404 Not Found'));
165
+ ```
166
+
167
+ ### List Rendering
168
+
169
+ ```typescript
170
+ import { createSignal, createList, h } from '@getforma/core';
171
+
172
+ const [items, setItems] = createSignal([
173
+ { id: 1, name: 'Alice' },
174
+ { id: 2, name: 'Bob' },
175
+ ]);
176
+
177
+ createList(
178
+ items,
179
+ (item) => item.id, // key function
180
+ (item) => h('li', null, item.name),
181
+ );
182
+ ```
183
+
184
+ ### Store (deep reactivity)
185
+
186
+ ```typescript
187
+ import { createStore } from '@getforma/core';
188
+
189
+ const [state, setState] = createStore({
190
+ user: { name: 'Alice', prefs: { theme: 'dark' } },
191
+ items: [1, 2, 3],
192
+ });
193
+
194
+ // Read reactively
195
+ state.user.name; // 'Alice'
196
+
197
+ // Mutate — only affected subscribers update
198
+ setState('user', 'name', 'Bob');
199
+ setState('items', items => [...items, 4]);
200
+ ```
201
+
202
+ ### Components
203
+
204
+ ```typescript
205
+ import { defineComponent, onMount, onUnmount, h } from '@getforma/core';
206
+
207
+ const Timer = defineComponent(() => {
208
+ const [seconds, setSeconds] = createSignal(0);
209
+
210
+ onMount(() => {
211
+ const id = setInterval(() => setSeconds(s => s + 1), 1000);
212
+ return () => clearInterval(id); // cleanup on unmount
213
+ });
214
+
215
+ return h('span', null, () => `${seconds()}s`);
216
+ });
217
+
218
+ document.body.appendChild(Timer());
219
+ ```
220
+
221
+ ### Context (Dependency Injection)
222
+
223
+ ```typescript
224
+ import { createContext, provide, inject } from '@getforma/core';
225
+
226
+ const ThemeCtx = createContext('light');
227
+
228
+ provide(ThemeCtx, 'dark');
229
+ const theme = inject(ThemeCtx); // 'dark'
230
+ ```
231
+
232
+ ## Islands Architecture
233
+
234
+ For server-rendered HTML, activate independent interactive regions:
235
+
236
+ ```typescript
237
+ import { activateIslands, createSignal, h } from '@getforma/core';
238
+
239
+ activateIslands({
240
+ Counter: (el, props) => {
241
+ const [count, setCount] = createSignal(props.initial ?? 0);
242
+ // Hydrate: attach reactivity to existing server-rendered DOM
243
+ },
244
+ });
245
+ ```
246
+
247
+ ```html
248
+ <!-- Server-rendered HTML -->
249
+ <div data-forma-island="Counter" data-forma-props='{"initial": 5}'>
250
+ <span>5</span>
251
+ <button>+1</button>
252
+ </div>
253
+ ```
254
+
255
+ ## Subpath Exports
256
+
257
+ | Import | Description |
258
+ |--------|-------------|
259
+ | `@getforma/core` | Signals, `h()`, `mount()`, lists, stores, components |
260
+ | `@getforma/core/runtime` | HTML Runtime — `initRuntime()`, `mount()`, `unmount()` |
261
+ | `@getforma/core/runtime-hardened` | Runtime with `new Function()` locked off (strict CSP) |
262
+ | `@getforma/core/ssr` | Server-side rendering — `renderToString()`, `renderToStream()` |
263
+ | `@getforma/core/tc39` | TC39-compatible `Signal.State` and `Signal.Computed` classes |
264
+
265
+ ## Examples
266
+
267
+ See the [`examples/`](./examples) directory:
31
268
 
32
- - **Real DOM** — `h()` creates actual DOM elements with reactive bindings. No virtual DOM, no diffing overhead.
33
- - **Fine-grained reactivity** — signals, effects, computed values via alien-signals. Only what changed updates.
34
- - **Islands architecture** — `activateIslands()` for independent hydration of server-rendered HTML regions.
35
- - **SSR hydration** — `adoptNode()` walks server-rendered DOM and attaches reactive bindings without re-creating elements.
36
- - **Conditional rendering** — `createShow()`, `createSwitch()` with branch caching for O(1) toggle.
37
- - **List rendering** — `createList()` with LIS-based keyed reconciliation, handles 50K+ rows.
38
- - **State management** — `createStore()` with deep reactivity, history, persistence.
269
+ - **counter** — minimal `h()` counter
270
+ - **counter-jsx** — same counter with JSX syntax
271
+ - **todo** — todo list with `createList` and keyed reconciliation
272
+ - **data-table** — sortable table with `createList`
39
273
 
40
274
  ## Ecosystem
41
275
 
42
- - [forma](https://github.com/getforma-dev/forma) Rust server framework (forma-ir + forma-server)
43
- - [forma-tools](https://github.com/getforma-dev/forma-tools) — Build tooling (@getforma/compiler + @getforma/build)
44
- - [create-forma-app](https://github.com/getforma-dev/create-forma-app) — CLI scaffolder
276
+ | Package | Description |
277
+ |---------|-------------|
278
+ | [@getforma/core](https://www.npmjs.com/package/@getforma/core) | This library `npm install @getforma/core` |
279
+ | [@getforma/compiler](https://www.npmjs.com/package/@getforma/compiler) | SSR compiler — `.tsx` to FMIR binary |
280
+ | [@getforma/build](https://www.npmjs.com/package/@getforma/build) | esbuild wrapper with JSX + SSR preconfigured |
281
+ | [create-forma-app](https://www.npmjs.com/package/@getforma/create-app) | `npx @getforma/create-app` project scaffolder |
282
+ | [forma](https://github.com/getforma-dev/forma) | Rust server framework (forma-ir + forma-server) |
45
283
 
46
284
  ## License
47
285
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@getforma/core",
3
3
  "author": "Forma <victor@getforma.dev>",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Real DOM reactive library — fine-grained signals, islands architecture, SSR hydration. No virtual DOM, no diffing. ~15KB gzipped.",
6
6
  "type": "module",
7
7
  "main": "./dist/index.cjs",
@@ -77,6 +77,7 @@
77
77
  "typecheck": "tsc --noEmit",
78
78
  "test": "vitest run",
79
79
  "test:watch": "vitest",
80
+ "test:coverage": "vitest run --coverage",
80
81
  "prepublishOnly": "npm run build"
81
82
  },
82
83
  "keywords": [
@@ -113,6 +114,7 @@
113
114
  "rollup": "^4.59.0"
114
115
  },
115
116
  "devDependencies": {
117
+ "@vitest/coverage-v8": "^4.0.18",
116
118
  "fake-indexeddb": "^6.2.3",
117
119
  "happy-dom": "^20.8.3",
118
120
  "tsup": "^8.5.1",