@getforma/core 0.3.0 → 0.6.1
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 +201 -13
- package/dist/{chunk-KX5WRZH7.js → chunk-DPSAVBCP.js} +3 -4
- package/dist/chunk-DPSAVBCP.js.map +1 -0
- package/dist/{chunk-FPSLC62A.cjs → chunk-KH3F5NRU.cjs} +2 -4
- package/dist/chunk-KH3F5NRU.cjs.map +1 -0
- package/dist/{chunk-VKKPX4YU.js → chunk-TKGUHASG.js} +79 -19
- package/dist/chunk-TKGUHASG.js.map +1 -0
- package/dist/{chunk-VP5EOGM5.cjs → chunk-YSKF3VRA.cjs} +87 -26
- package/dist/chunk-YSKF3VRA.cjs.map +1 -0
- package/dist/forma-runtime-csp.js +2 -0
- package/dist/forma-runtime.js +2 -0
- package/dist/formajs-runtime-hardened.global.js +1 -1
- package/dist/formajs-runtime-hardened.global.js.map +1 -1
- package/dist/formajs-runtime.global.js +1 -1
- package/dist/formajs-runtime.global.js.map +1 -1
- package/dist/formajs.global.js +1 -1
- package/dist/formajs.global.js.map +1 -1
- package/dist/index.cjs +154 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -35
- package/dist/index.d.ts +83 -35
- package/dist/index.js +99 -40
- package/dist/index.js.map +1 -1
- package/dist/runtime-hardened.cjs +224 -60
- package/dist/runtime-hardened.cjs.map +1 -1
- package/dist/runtime-hardened.d.cts +86 -0
- package/dist/runtime-hardened.d.ts +86 -0
- package/dist/runtime-hardened.js +225 -61
- package/dist/runtime-hardened.js.map +1 -1
- package/dist/runtime.cjs +248 -83
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +226 -61
- package/dist/runtime.js.map +1 -1
- package/dist/ssr/index.cjs +69 -73
- package/dist/ssr/index.cjs.map +1 -1
- package/dist/ssr/index.d.cts +4 -0
- package/dist/ssr/index.d.ts +4 -0
- package/dist/ssr/index.js +69 -73
- package/dist/ssr/index.js.map +1 -1
- package/dist/tc39-compat.cjs +3 -3
- package/dist/tc39-compat.js +1 -1
- package/package.json +24 -3
- package/dist/chunk-FPSLC62A.cjs.map +0 -1
- package/dist/chunk-KX5WRZH7.js.map +0 -1
- package/dist/chunk-VKKPX4YU.js.map +0 -1
- package/dist/chunk-VP5EOGM5.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -18,6 +18,41 @@ Or use the CDN (no build step required):
|
|
|
18
18
|
<script src="https://unpkg.com/@getforma/core/dist/formajs-runtime.global.js"></script>
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
### Getting Started with a Bundler
|
|
22
|
+
|
|
23
|
+
After `npm install`, you need a bundler to resolve the ES module imports. Here's a minimal Vite setup:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @getforma/core
|
|
27
|
+
npm install -D vite
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<!-- index.html -->
|
|
32
|
+
<div id="app"></div>
|
|
33
|
+
<script type="module" src="./main.ts"></script>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// main.ts
|
|
38
|
+
import { createSignal, h, mount } from '@getforma/core';
|
|
39
|
+
|
|
40
|
+
const [count, setCount] = createSignal(0);
|
|
41
|
+
|
|
42
|
+
mount(() =>
|
|
43
|
+
h('button', { onClick: () => setCount(count() + 1) },
|
|
44
|
+
() => `Clicked ${count()} times`
|
|
45
|
+
),
|
|
46
|
+
'#app'
|
|
47
|
+
);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx vite
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For **esbuild**, **tsup**, or other bundlers — no special config is needed. FormaJS ships standard ESM and CJS via `package.json` exports.
|
|
55
|
+
|
|
21
56
|
## Why FormaJS?
|
|
22
57
|
|
|
23
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.
|
|
@@ -27,7 +62,7 @@ Most UI libraries make you choose: simple but limited (Alpine, htmx), or powerfu
|
|
|
27
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/).
|
|
28
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.
|
|
29
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.
|
|
30
|
-
- **CSP-safe
|
|
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.
|
|
31
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.
|
|
32
67
|
|
|
33
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.
|
|
@@ -65,6 +100,8 @@ Drop a script tag, write `data-*` attributes. Zero config, zero tooling.
|
|
|
65
100
|
| `data-persist` | Persist state to localStorage | `data-persist="{count}"` |
|
|
66
101
|
| `data-fetch` | Fetch data from URL | `data-fetch="GET /api/items → items"` |
|
|
67
102
|
| `data-transition:*` | Enter/leave CSS transitions | `data-transition:enter="fade-in"` |
|
|
103
|
+
| `$el` | Reference to the current DOM element | `data-on:click="{$el.classList.toggle('active')}"` |
|
|
104
|
+
| `$dispatch` | Fire a CustomEvent (bubbles, crosses Shadow DOM) | `data-on:click="{$dispatch('selected', {id: itemId})}"` |
|
|
68
105
|
|
|
69
106
|
CSP-safe expression parser — no `eval()` or `new Function()` by default. For strict CSP environments, use the hardened build:
|
|
70
107
|
|
|
@@ -123,7 +160,19 @@ function Counter() {
|
|
|
123
160
|
mount(() => <Counter />, '#app');
|
|
124
161
|
```
|
|
125
162
|
|
|
126
|
-
|
|
163
|
+
## CDN Usage
|
|
164
|
+
|
|
165
|
+
Both long and short filenames are provided. They are identical files — use whichever you prefer:
|
|
166
|
+
|
|
167
|
+
| Build | URL |
|
|
168
|
+
|-------|-----|
|
|
169
|
+
| **Standard** (recommended) | `unpkg.com/@getforma/core/dist/formajs-runtime.global.js` |
|
|
170
|
+
| Standard (short alias) | `unpkg.com/@getforma/core/dist/forma-runtime.js` |
|
|
171
|
+
| **CSP-safe** (no `new Function`) | `unpkg.com/@getforma/core/dist/formajs-runtime-hardened.global.js` |
|
|
172
|
+
| CSP-safe (short alias) | `unpkg.com/@getforma/core/dist/forma-runtime-csp.js` |
|
|
173
|
+
|
|
174
|
+
> The CSP build uses a hand-written expression parser and never calls `new Function`.
|
|
175
|
+
> It supports most common patterns. See [examples/csp](./examples/csp) for a working demo.
|
|
127
176
|
|
|
128
177
|
## Core API
|
|
129
178
|
|
|
@@ -218,6 +267,60 @@ const Timer = defineComponent(() => {
|
|
|
218
267
|
document.body.appendChild(Timer());
|
|
219
268
|
```
|
|
220
269
|
|
|
270
|
+
#### Lifecycle: `onMount` vs `onUnmount`
|
|
271
|
+
|
|
272
|
+
- **`onMount(fn)`** — runs after the component's DOM is created. If `fn` returns a function, that function is automatically registered as an unmount callback.
|
|
273
|
+
- **`onUnmount(fn)`** — explicitly registers a cleanup function that runs when the component is disposed.
|
|
274
|
+
|
|
275
|
+
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:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// These are equivalent:
|
|
279
|
+
onMount(() => {
|
|
280
|
+
const id = setInterval(tick, 1000);
|
|
281
|
+
return () => clearInterval(id);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// vs.
|
|
285
|
+
onMount(() => {
|
|
286
|
+
const id = setInterval(tick, 1000);
|
|
287
|
+
onUnmount(() => clearInterval(id));
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Error Handling
|
|
292
|
+
|
|
293
|
+
**`mount()` fails fast.** If the container selector doesn't match any element, it throws:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
mount(() => h('p', null, 'hello'), '#nonexistent');
|
|
297
|
+
// Error: mount: container not found — "#nonexistent"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Global error handler.** Register a handler for errors in effects and lifecycle callbacks:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { onError } from '@getforma/core';
|
|
304
|
+
|
|
305
|
+
onError((error, info) => {
|
|
306
|
+
console.error(`[${info?.source}]`, error);
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Error boundaries.** Catch rendering errors and display fallback UI with a retry option:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { createErrorBoundary, h } from '@getforma/core';
|
|
314
|
+
|
|
315
|
+
createErrorBoundary(
|
|
316
|
+
() => h(UnstableComponent),
|
|
317
|
+
(error, retry) => h('div', null,
|
|
318
|
+
h('p', null, `Something went wrong: ${error.message}`),
|
|
319
|
+
h('button', { onClick: retry }, 'Retry'),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
```
|
|
323
|
+
|
|
221
324
|
### Context (Dependency Injection)
|
|
222
325
|
|
|
223
326
|
```typescript
|
|
@@ -231,34 +334,80 @@ const theme = inject(ThemeCtx); // 'dark'
|
|
|
231
334
|
|
|
232
335
|
## Islands Architecture
|
|
233
336
|
|
|
234
|
-
For server-rendered HTML, activate independent interactive regions
|
|
337
|
+
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.
|
|
235
338
|
|
|
236
339
|
```typescript
|
|
237
340
|
import { activateIslands, createSignal, h } from '@getforma/core';
|
|
238
341
|
|
|
239
342
|
activateIslands({
|
|
240
343
|
Counter: (el, props) => {
|
|
241
|
-
const [count, setCount] = createSignal(props
|
|
242
|
-
|
|
344
|
+
const [count, setCount] = createSignal(props?.initial ?? 0);
|
|
345
|
+
|
|
346
|
+
// el is the island's root HTMLElement — useful for layout measurement,
|
|
347
|
+
// focus management, CSS classes, or reading extra data-* attributes.
|
|
348
|
+
el.classList.add('is-hydrated');
|
|
349
|
+
|
|
350
|
+
// Return the same tree shape as the SSR output.
|
|
351
|
+
// Hydration matches this against existing DOM — no elements are created.
|
|
352
|
+
return h('div', null,
|
|
353
|
+
h('span', null, () => String(count())),
|
|
354
|
+
h('button', { onClick: () => setCount(c => c + 1) }, '+1'),
|
|
355
|
+
);
|
|
243
356
|
},
|
|
244
357
|
});
|
|
245
358
|
```
|
|
246
359
|
|
|
247
360
|
```html
|
|
248
361
|
<!-- Server-rendered HTML -->
|
|
249
|
-
<div data-forma-island="Counter" data-forma-props='{"initial": 5}'>
|
|
362
|
+
<div data-forma-island="0" data-forma-component="Counter" data-forma-props='{"initial": 5}'>
|
|
250
363
|
<span>5</span>
|
|
251
364
|
<button>+1</button>
|
|
252
365
|
</div>
|
|
253
366
|
```
|
|
254
367
|
|
|
368
|
+
Each island is activated inside its own `createRoot` scope with error isolation — a broken island never takes down its siblings.
|
|
369
|
+
|
|
370
|
+
### Hydration Triggers
|
|
371
|
+
|
|
372
|
+
Control when an island hydrates via `data-forma-hydrate`:
|
|
373
|
+
|
|
374
|
+
| Trigger | When it hydrates | Use case |
|
|
375
|
+
|---------|-----------------|----------|
|
|
376
|
+
| `load` (default) | Immediately on page load | Above-the-fold interactive content |
|
|
377
|
+
| `visible` | When island enters viewport | Below-the-fold components |
|
|
378
|
+
| `idle` | During browser idle time (`requestIdleCallback`) | Non-critical functionality |
|
|
379
|
+
| `interaction` | On first `pointerdown` or `focusin` | Skeleton+skin pattern |
|
|
380
|
+
|
|
381
|
+
```html
|
|
382
|
+
<div data-forma-island="1" data-forma-component="Comments" data-forma-hydrate="visible">
|
|
383
|
+
<!-- Only loads JS when scrolled into view -->
|
|
384
|
+
</div>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Island Disposal
|
|
388
|
+
|
|
389
|
+
When swapping module content (e.g., inside `<forma-stage>` Shadow DOM), dispose islands to prevent leaked effects and listeners:
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
import { deactivateIsland, deactivateAllIslands } from '@getforma/core';
|
|
393
|
+
|
|
394
|
+
// Dispose all active islands under a root
|
|
395
|
+
deactivateAllIslands(shadowRoot);
|
|
396
|
+
|
|
397
|
+
// Or dispose a single island
|
|
398
|
+
deactivateIsland(islandElement);
|
|
399
|
+
```
|
|
400
|
+
|
|
255
401
|
## Subpath Exports
|
|
256
402
|
|
|
257
403
|
| Import | Description |
|
|
258
404
|
|--------|-------------|
|
|
259
405
|
| `@getforma/core` | Signals, `h()`, `mount()`, lists, stores, components |
|
|
260
406
|
| `@getforma/core/runtime` | HTML Runtime — `initRuntime()`, `mount()`, `unmount()` |
|
|
407
|
+
| `@getforma/core/runtime/global` | HTML Runtime global build (IIFE, for `<script>` tags) |
|
|
261
408
|
| `@getforma/core/runtime-hardened` | Runtime with `new Function()` locked off (strict CSP) |
|
|
409
|
+
| `@getforma/core/runtime-csp` | Alias for `runtime-hardened` (CSP-safe build) |
|
|
410
|
+
| `@getforma/core/runtime-csp/global` | CSP-safe global build (IIFE, for `<script>` tags) |
|
|
262
411
|
| `@getforma/core/ssr` | Server-side rendering — `renderToString()`, `renderToStream()` |
|
|
263
412
|
| `@getforma/core/tc39` | TC39-compatible `Signal.State` and `Signal.Computed` classes |
|
|
264
413
|
|
|
@@ -268,18 +417,57 @@ See the [`examples/`](./examples) directory:
|
|
|
268
417
|
|
|
269
418
|
- **counter** — minimal `h()` counter
|
|
270
419
|
- **counter-jsx** — same counter with JSX syntax
|
|
420
|
+
- **csp** — CSP-safe runtime with strict Content-Security-Policy meta tag
|
|
271
421
|
- **todo** — todo list with `createList` and keyed reconciliation
|
|
272
422
|
- **data-table** — sortable table with `createList`
|
|
273
423
|
|
|
424
|
+
## How Is This Different from Solid?
|
|
425
|
+
|
|
426
|
+
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:
|
|
427
|
+
|
|
428
|
+
| | Solid | FormaJS |
|
|
429
|
+
|-|-------|---------|
|
|
430
|
+
| **Build requirement** | Always needs a compiler (JSX transform) | CDN runtime works with zero build step; bundler is optional |
|
|
431
|
+
| **Entry points** | JSX-first | HTML Runtime (`data-*` attributes), `h()` hyperscript, or JSX |
|
|
432
|
+
| **CSP** | Relies on compiler output | Hand-written expression parser; hardened build has no `new Function()` |
|
|
433
|
+
| **Islands** | Via [solid-start](https://start.solidjs.com/) meta-framework | Built-in `activateIslands()` — no meta-framework needed |
|
|
434
|
+
| **Ecosystem** | Mature (router, meta-framework, devtools) | Minimal — reactive core only, you bring the architecture |
|
|
435
|
+
| **Size** | ~7KB | ~15KB (includes runtime parser, stores, SSR) |
|
|
436
|
+
|
|
437
|
+
**When to choose FormaJS:** You want to progressively enhance server-rendered HTML, need CSP compliance without a build step, or prefer a single library that scales from a `<script>` tag to a full SSR pipeline.
|
|
438
|
+
|
|
439
|
+
**When to choose Solid:** You want a mature ecosystem with routing, SSR meta-framework, and community-built component libraries.
|
|
440
|
+
|
|
441
|
+
## Stability
|
|
442
|
+
|
|
443
|
+
Some features are more battle-tested than others:
|
|
444
|
+
|
|
445
|
+
| Feature | Status | Notes |
|
|
446
|
+
|---------|--------|-------|
|
|
447
|
+
| Signals (`createSignal`, `createEffect`, `createComputed`, `batch`) | **Stable** | Core primitive, well-tested |
|
|
448
|
+
| `h()` / JSX rendering | **Stable** | |
|
|
449
|
+
| `mount()`, `createShow`, `createSwitch`, `createList` | **Stable** | |
|
|
450
|
+
| HTML Runtime (`data-*` directives) | **Stable** | Expression parser covers common patterns |
|
|
451
|
+
| CSP-hardened runtime | **Stable** | No `new Function()`, tested with strict CSP headers |
|
|
452
|
+
| `createStore` (deep reactivity) | **Stable** | |
|
|
453
|
+
| Components (`defineComponent`, lifecycle) | **Stable** | |
|
|
454
|
+
| Context (`createContext`, `provide`, `inject`) | **Stable** | |
|
|
455
|
+
| Islands (`activateIslands`) | **Stable** | 10 activation + 88 hydration tests |
|
|
456
|
+
| SSR (`renderToString`, `renderToStream`) | **Beta** | Functional, API may evolve |
|
|
457
|
+
| TC39 Signals compat (`Signal.State`, `Signal.Computed`) | **Beta** | 9 tests, but tracks an evolving TC39 proposal |
|
|
458
|
+
|
|
274
459
|
## Ecosystem
|
|
275
460
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
|
279
|
-
|
|
280
|
-
| [@getforma/
|
|
281
|
-
| [
|
|
282
|
-
| [
|
|
461
|
+
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.
|
|
462
|
+
|
|
463
|
+
| Package | Language | Description |
|
|
464
|
+
|---------|----------|-------------|
|
|
465
|
+
| [@getforma/core](https://www.npmjs.com/package/@getforma/core) | TypeScript | This library — reactive DOM, signals, islands, SSR hydration |
|
|
466
|
+
| [@getforma/compiler](https://github.com/getforma-dev/forma-tools) | TypeScript | TypeScript-to-FMIR compiler, Vite plugin, esbuild SSR plugin |
|
|
467
|
+
| [@getforma/build](https://github.com/getforma-dev/forma-tools) | TypeScript | esbuild pipeline with content hashing, compression, manifest |
|
|
468
|
+
| [@getforma/create-app](https://github.com/getforma-dev/create-forma-app) | TypeScript | `npx @getforma/create-app` — scaffold a new Forma project |
|
|
469
|
+
| [forma-ir](https://crates.io/crates/forma-ir) | Rust | FMIR binary format: parser, walker, WASM exports |
|
|
470
|
+
| [forma-server](https://crates.io/crates/forma-server) | Rust | Axum middleware for SSR page rendering, asset serving, CSP |
|
|
283
471
|
|
|
284
472
|
## License
|
|
285
473
|
|
|
@@ -17,11 +17,10 @@ function createSignal(initialValue) {
|
|
|
17
17
|
const setter = (v) => applySignalSet(s, v);
|
|
18
18
|
return [getter, setter];
|
|
19
19
|
}
|
|
20
|
-
var createValueSignal = createSignal;
|
|
21
20
|
function createComputed(fn) {
|
|
22
21
|
return computed(fn);
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
export { createComputed, createSignal
|
|
26
|
-
//# sourceMappingURL=chunk-
|
|
27
|
-
//# sourceMappingURL=chunk-
|
|
24
|
+
export { createComputed, createSignal };
|
|
25
|
+
//# sourceMappingURL=chunk-DPSAVBCP.js.map
|
|
26
|
+
//# sourceMappingURL=chunk-DPSAVBCP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reactive/signal.ts","../src/reactive/computed.ts"],"names":["createRawSignal","rawComputed"],"mappings":";;;AAmDA,SAAS,cAAA,CACP,GACA,CAAA,EACM;AACN,EAAA,IAAI,OAAO,MAAM,UAAA,EAAY;AAC3B,IAAA,CAAA,CAAE,CAAC,CAAA;AACH,IAAA;AAAA,EACF;AAEA,EAAA,aAAA,EAAc;AACd,EAAA,MAAM,OAAO,CAAA,EAAE;AACf,EAAA,cAAA,EAAe;AACf,EAAA,CAAA,CAAG,CAAA,CAAqB,IAAI,CAAC,CAAA;AAC/B;AAYO,SAAS,aAAgB,YAAA,EAA+D;AAC7F,EAAA,MAAM,CAAA,GAAIA,OAAmB,YAAY,CAAA;AACzC,EAAA,MAAM,MAAA,GAAS,CAAA;AACf,EAAA,MAAM,MAAA,GAA0B,CAAC,CAAA,KAA4B,cAAA,CAAe,GAAG,CAAC,CAAA;AAEhF,EAAA,OAAO,CAAC,QAAQ,MAAM,CAAA;AACxB;ACtDO,SAAS,eAAkB,EAAA,EAAsB;AACtD,EAAA,OAAOC,SAAY,EAAE,CAAA;AACvB","file":"chunk-DPSAVBCP.js","sourcesContent":["/**\n * Forma Reactive - Signal\n *\n * Fine-grained reactive primitive backed by alien-signals.\n * API: createSignal returns [getter, setter] tuple following SolidJS conventions.\n *\n * TC39 Signals equivalent: Signal.State\n */\n\nimport { signal as createRawSignal, pauseTracking, resumeTracking } from 'alien-signals';\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport type SignalGetter<T> = () => T;\nexport type SignalSetter<T> = (v: T | ((prev: T) => T)) => void;\n\nexport interface SignalOptions<T> {\n /** Debug name — attached to getter in dev mode for devtools inspection. */\n name?: string;\n /** Custom equality check. Default: Object.is (via alien-signals). */\n equals?: (prev: T, next: T) => boolean;\n}\n\n/**\n * Wrap a value so the setter treats it as a literal value, not a functional updater.\n *\n * When `T` is itself a function type, passing a function to the setter is\n * ambiguous -- it looks like a functional update (`prev => next`). Use\n * `value()` to disambiguate:\n *\n * ```ts\n * const [getFn, setFn] = createSignal<() => void>(() => console.log('a'));\n *\n * // BUG: interpreted as a functional update -- calls the arrow with prev\n * // setFn(() => console.log('b'));\n *\n * // Correct: wraps in a thunk so the setter stores it as-is\n * setFn(value(() => console.log('b')));\n * ```\n */\nexport function value<T>(v: T): () => T {\n return () => v;\n}\n\ntype RawSignal<T> = {\n (): T;\n (value: T): void;\n};\n\nfunction applySignalSet<T>(\n s: RawSignal<T>,\n v: T | ((prev: T) => T),\n): void {\n if (typeof v !== 'function') {\n s(v);\n return;\n }\n\n pauseTracking();\n const prev = s();\n resumeTracking();\n s((v as (prev: T) => T)(prev));\n}\n\n/**\n * Create a reactive signal.\n *\n * ```ts\n * const [count, setCount] = createSignal(0);\n * console.log(count()); // 0\n * setCount(1);\n * setCount(prev => prev + 1);\n * ```\n */\nexport function createSignal<T>(initialValue: T): [get: SignalGetter<T>, set: SignalSetter<T>] {\n const s = createRawSignal<T>(initialValue) as RawSignal<T>;\n const getter = s as unknown as SignalGetter<T>;\n const setter: SignalSetter<T> = (v: T | ((prev: T) => T)) => applySignalSet(s, v);\n\n return [getter, setter];\n}\n","/**\n * Forma Reactive - Computed\n *\n * Lazy, cached derived value that participates in the reactive graph.\n * Backed by alien-signals for automatic dependency tracking\n * and cache invalidation.\n *\n * TC39 Signals equivalent: Signal.Computed\n */\n\nimport { computed as rawComputed } from 'alien-signals';\n\n/**\n * Create a lazy, cached computed value.\n *\n * Note: Unlike SolidJS's createComputed (which is an eager synchronous\n * side effect), this is a lazy cached derivation — equivalent to\n * SolidJS's createMemo. Both createComputed and createMemo in FormaJS\n * are identical.\n *\n * ```ts\n * const [count, setCount] = createSignal(0);\n * const doubled = createComputed(() => count() * 2);\n * console.log(doubled()); // 0\n * setCount(5);\n * console.log(doubled()); // 10\n * ```\n */\nexport function createComputed<T>(fn: () => T): () => T {\n return rawComputed(fn);\n}\n"]}
|
|
@@ -19,13 +19,11 @@ function createSignal(initialValue) {
|
|
|
19
19
|
const setter = (v) => applySignalSet(s, v);
|
|
20
20
|
return [getter, setter];
|
|
21
21
|
}
|
|
22
|
-
var createValueSignal = createSignal;
|
|
23
22
|
function createComputed(fn) {
|
|
24
23
|
return alienSignals.computed(fn);
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
exports.createComputed = createComputed;
|
|
28
27
|
exports.createSignal = createSignal;
|
|
29
|
-
|
|
30
|
-
//# sourceMappingURL=chunk-
|
|
31
|
-
//# sourceMappingURL=chunk-FPSLC62A.cjs.map
|
|
28
|
+
//# sourceMappingURL=chunk-KH3F5NRU.cjs.map
|
|
29
|
+
//# sourceMappingURL=chunk-KH3F5NRU.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reactive/signal.ts","../src/reactive/computed.ts"],"names":["pauseTracking","resumeTracking","createRawSignal","rawComputed"],"mappings":";;;;;AAmDA,SAAS,cAAA,CACP,GACA,CAAA,EACM;AACN,EAAA,IAAI,OAAO,MAAM,UAAA,EAAY;AAC3B,IAAA,CAAA,CAAE,CAAC,CAAA;AACH,IAAA;AAAA,EACF;AAEA,EAAAA,0BAAA,EAAc;AACd,EAAA,MAAM,OAAO,CAAA,EAAE;AACf,EAAAC,2BAAA,EAAe;AACf,EAAA,CAAA,CAAG,CAAA,CAAqB,IAAI,CAAC,CAAA;AAC/B;AAYO,SAAS,aAAgB,YAAA,EAA+D;AAC7F,EAAA,MAAM,CAAA,GAAIC,oBAAmB,YAAY,CAAA;AACzC,EAAA,MAAM,MAAA,GAAS,CAAA;AACf,EAAA,MAAM,MAAA,GAA0B,CAAC,CAAA,KAA4B,cAAA,CAAe,GAAG,CAAC,CAAA;AAEhF,EAAA,OAAO,CAAC,QAAQ,MAAM,CAAA;AACxB;ACtDO,SAAS,eAAkB,EAAA,EAAsB;AACtD,EAAA,OAAOC,sBAAY,EAAE,CAAA;AACvB","file":"chunk-KH3F5NRU.cjs","sourcesContent":["/**\n * Forma Reactive - Signal\n *\n * Fine-grained reactive primitive backed by alien-signals.\n * API: createSignal returns [getter, setter] tuple following SolidJS conventions.\n *\n * TC39 Signals equivalent: Signal.State\n */\n\nimport { signal as createRawSignal, pauseTracking, resumeTracking } from 'alien-signals';\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport type SignalGetter<T> = () => T;\nexport type SignalSetter<T> = (v: T | ((prev: T) => T)) => void;\n\nexport interface SignalOptions<T> {\n /** Debug name — attached to getter in dev mode for devtools inspection. */\n name?: string;\n /** Custom equality check. Default: Object.is (via alien-signals). */\n equals?: (prev: T, next: T) => boolean;\n}\n\n/**\n * Wrap a value so the setter treats it as a literal value, not a functional updater.\n *\n * When `T` is itself a function type, passing a function to the setter is\n * ambiguous -- it looks like a functional update (`prev => next`). Use\n * `value()` to disambiguate:\n *\n * ```ts\n * const [getFn, setFn] = createSignal<() => void>(() => console.log('a'));\n *\n * // BUG: interpreted as a functional update -- calls the arrow with prev\n * // setFn(() => console.log('b'));\n *\n * // Correct: wraps in a thunk so the setter stores it as-is\n * setFn(value(() => console.log('b')));\n * ```\n */\nexport function value<T>(v: T): () => T {\n return () => v;\n}\n\ntype RawSignal<T> = {\n (): T;\n (value: T): void;\n};\n\nfunction applySignalSet<T>(\n s: RawSignal<T>,\n v: T | ((prev: T) => T),\n): void {\n if (typeof v !== 'function') {\n s(v);\n return;\n }\n\n pauseTracking();\n const prev = s();\n resumeTracking();\n s((v as (prev: T) => T)(prev));\n}\n\n/**\n * Create a reactive signal.\n *\n * ```ts\n * const [count, setCount] = createSignal(0);\n * console.log(count()); // 0\n * setCount(1);\n * setCount(prev => prev + 1);\n * ```\n */\nexport function createSignal<T>(initialValue: T): [get: SignalGetter<T>, set: SignalSetter<T>] {\n const s = createRawSignal<T>(initialValue) as RawSignal<T>;\n const getter = s as unknown as SignalGetter<T>;\n const setter: SignalSetter<T> = (v: T | ((prev: T) => T)) => applySignalSet(s, v);\n\n return [getter, setter];\n}\n","/**\n * Forma Reactive - Computed\n *\n * Lazy, cached derived value that participates in the reactive graph.\n * Backed by alien-signals for automatic dependency tracking\n * and cache invalidation.\n *\n * TC39 Signals equivalent: Signal.Computed\n */\n\nimport { computed as rawComputed } from 'alien-signals';\n\n/**\n * Create a lazy, cached computed value.\n *\n * Note: Unlike SolidJS's createComputed (which is an eager synchronous\n * side effect), this is a lazy cached derivation — equivalent to\n * SolidJS's createMemo. Both createComputed and createMemo in FormaJS\n * are identical.\n *\n * ```ts\n * const [count, setCount] = createSignal(0);\n * const doubled = createComputed(() => count() * 2);\n * console.log(doubled()); // 0\n * setCount(5);\n * console.log(doubled()); // 10\n * ```\n */\nexport function createComputed<T>(fn: () => T): () => T {\n return rawComputed(fn);\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createComputed, createSignal
|
|
1
|
+
import { createComputed, createSignal } from './chunk-DPSAVBCP.js';
|
|
2
2
|
import { effect, startBatch, endBatch, pauseTracking, resumeTracking } from 'alien-signals';
|
|
3
3
|
|
|
4
4
|
// src/reactive/root.ts
|
|
@@ -21,7 +21,7 @@ function createRoot(fn) {
|
|
|
21
21
|
return fn(dispose);
|
|
22
22
|
} finally {
|
|
23
23
|
currentRoot = rootStack.pop() ?? null;
|
|
24
|
-
if (rootStack.length === 0
|
|
24
|
+
if (rootStack.length === 0) {
|
|
25
25
|
rootStack.length = 0;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -293,8 +293,8 @@ function getSuspenseContext() {
|
|
|
293
293
|
// src/reactive/resource.ts
|
|
294
294
|
function createResource(source, fetcher, options) {
|
|
295
295
|
const [data, setData] = createSignal(options?.initialValue);
|
|
296
|
-
const [loading, setLoading] =
|
|
297
|
-
const [error, setError] =
|
|
296
|
+
const [loading, setLoading] = createSignal(false);
|
|
297
|
+
const [error, setError] = createSignal(void 0);
|
|
298
298
|
const suspenseCtx = getSuspenseContext();
|
|
299
299
|
let abortController = null;
|
|
300
300
|
let fetchVersion = 0;
|
|
@@ -562,8 +562,47 @@ function handleEvent(el, key, value2) {
|
|
|
562
562
|
);
|
|
563
563
|
}
|
|
564
564
|
function handleInnerHTML(el, _key, value2) {
|
|
565
|
-
|
|
566
|
-
|
|
565
|
+
if (typeof value2 === "function") {
|
|
566
|
+
internalEffect(() => {
|
|
567
|
+
const resolved = value2();
|
|
568
|
+
if (resolved == null) {
|
|
569
|
+
el.innerHTML = "";
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (typeof resolved !== "object" || !("__html" in resolved)) {
|
|
573
|
+
throw new TypeError(
|
|
574
|
+
"dangerouslySetInnerHTML: expected { __html: string }, got " + typeof resolved
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
const html = resolved.__html;
|
|
578
|
+
if (typeof html !== "string") {
|
|
579
|
+
throw new TypeError(
|
|
580
|
+
"dangerouslySetInnerHTML: __html must be a string, got " + typeof html
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const cache = getCache(el);
|
|
584
|
+
if (cache["innerHTML"] === html) return;
|
|
585
|
+
cache["innerHTML"] = html;
|
|
586
|
+
el.innerHTML = html;
|
|
587
|
+
});
|
|
588
|
+
} else {
|
|
589
|
+
if (value2 == null) {
|
|
590
|
+
el.innerHTML = "";
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (typeof value2 !== "object" || !("__html" in value2)) {
|
|
594
|
+
throw new TypeError(
|
|
595
|
+
"dangerouslySetInnerHTML: expected { __html: string }, got " + typeof value2
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const html = value2.__html;
|
|
599
|
+
if (typeof html !== "string") {
|
|
600
|
+
throw new TypeError(
|
|
601
|
+
"dangerouslySetInnerHTML: __html must be a string, got " + typeof html
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
el.innerHTML = html;
|
|
605
|
+
}
|
|
567
606
|
}
|
|
568
607
|
function handleXLink(el, key, value2) {
|
|
569
608
|
const localName = key.slice(6);
|
|
@@ -685,7 +724,18 @@ function applyStaticProp(el, key, value2) {
|
|
|
685
724
|
return;
|
|
686
725
|
}
|
|
687
726
|
if (key === "dangerouslySetInnerHTML") {
|
|
688
|
-
|
|
727
|
+
if (typeof value2 !== "object" || !("__html" in value2)) {
|
|
728
|
+
throw new TypeError(
|
|
729
|
+
"dangerouslySetInnerHTML: expected { __html: string }, got " + typeof value2
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
const html = value2.__html;
|
|
733
|
+
if (typeof html !== "string") {
|
|
734
|
+
throw new TypeError(
|
|
735
|
+
"dangerouslySetInnerHTML: __html must be a string, got " + typeof html
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
el.innerHTML = html;
|
|
689
739
|
return;
|
|
690
740
|
}
|
|
691
741
|
if (key.charCodeAt(0) === 120 && key.startsWith("xlink:")) {
|
|
@@ -756,6 +806,10 @@ function appendChild(parent, child) {
|
|
|
756
806
|
}
|
|
757
807
|
}
|
|
758
808
|
function h(tag, props, ...children) {
|
|
809
|
+
if (typeof tag === "function" && tag !== Fragment) {
|
|
810
|
+
const mergedProps = { ...props ?? {}, children };
|
|
811
|
+
return tag(mergedProps);
|
|
812
|
+
}
|
|
759
813
|
if (tag === Fragment) {
|
|
760
814
|
const frag = document.createDocumentFragment();
|
|
761
815
|
for (const child of children) {
|
|
@@ -843,10 +897,10 @@ function createShow(when, thenFn, elseFn) {
|
|
|
843
897
|
let currentNode = null;
|
|
844
898
|
let lastTruthy = null;
|
|
845
899
|
let currentDispose = null;
|
|
846
|
-
const DEBUG_LABEL = thenFn.toString().slice(0, 60);
|
|
847
900
|
const showDispose = internalEffect(() => {
|
|
848
901
|
const truthy = !!when();
|
|
849
902
|
const DEBUG = typeof globalThis.__FORMA_DEBUG__ !== "undefined";
|
|
903
|
+
const DEBUG_LABEL = DEBUG ? thenFn.toString().slice(0, 60) : "";
|
|
850
904
|
if (truthy === lastTruthy) {
|
|
851
905
|
if (DEBUG) console.log("[forma:show] skip (same)", truthy, DEBUG_LABEL);
|
|
852
906
|
return;
|
|
@@ -898,18 +952,19 @@ function createShow(when, thenFn, elseFn) {
|
|
|
898
952
|
}
|
|
899
953
|
|
|
900
954
|
// src/dom/hydrate.ts
|
|
955
|
+
var ABORT_SYM2 = /* @__PURE__ */ Symbol.for("forma-abort");
|
|
901
956
|
var hydrating = false;
|
|
902
957
|
function setHydrating(value2) {
|
|
903
958
|
hydrating = value2;
|
|
904
959
|
}
|
|
905
960
|
function isDescriptor(v) {
|
|
906
|
-
return v != null && typeof v === "object" && v.type === "element";
|
|
961
|
+
return v != null && typeof v === "object" && "type" in v && v.type === "element";
|
|
907
962
|
}
|
|
908
963
|
function isShowDescriptor(v) {
|
|
909
|
-
return v != null && typeof v === "object" && v.type === "show";
|
|
964
|
+
return v != null && typeof v === "object" && "type" in v && v.type === "show";
|
|
910
965
|
}
|
|
911
966
|
function isListDescriptor(v) {
|
|
912
|
-
return v != null && typeof v === "object" && v.type === "list";
|
|
967
|
+
return v != null && typeof v === "object" && "type" in v && v.type === "list";
|
|
913
968
|
}
|
|
914
969
|
function applyDynamicProps(el, props) {
|
|
915
970
|
if (!props) return;
|
|
@@ -917,7 +972,12 @@ function applyDynamicProps(el, props) {
|
|
|
917
972
|
const value2 = props[key];
|
|
918
973
|
if (typeof value2 !== "function") continue;
|
|
919
974
|
if (key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && key.length > 2) {
|
|
920
|
-
el
|
|
975
|
+
let ac = el[ABORT_SYM2];
|
|
976
|
+
if (!ac) {
|
|
977
|
+
ac = new AbortController();
|
|
978
|
+
el[ABORT_SYM2] = ac;
|
|
979
|
+
}
|
|
980
|
+
el.addEventListener(key.slice(2).toLowerCase(), value2, { signal: ac.signal });
|
|
921
981
|
continue;
|
|
922
982
|
}
|
|
923
983
|
const fn = value2;
|
|
@@ -1087,7 +1147,7 @@ function adoptBranchContent(desc, regionStart, regionEnd) {
|
|
|
1087
1147
|
}
|
|
1088
1148
|
function adoptNode(desc, ssrEl) {
|
|
1089
1149
|
if (!ssrEl || ssrEl.tagName !== desc.tag.toUpperCase()) {
|
|
1090
|
-
console.warn(`Hydration mismatch: expected <${desc.tag}>, got <${ssrEl?.tagName?.toLowerCase() ?? "nothing"}>`);
|
|
1150
|
+
if (__DEV__) console.warn(`Hydration mismatch: expected <${desc.tag}>, got <${ssrEl?.tagName?.toLowerCase() ?? "nothing"}>`);
|
|
1091
1151
|
const fresh = descriptorToElement(desc);
|
|
1092
1152
|
if (ssrEl) ssrEl.replaceWith(fresh);
|
|
1093
1153
|
return;
|
|
@@ -1419,11 +1479,11 @@ function longestIncreasingSubsequence(arr) {
|
|
|
1419
1479
|
return result;
|
|
1420
1480
|
}
|
|
1421
1481
|
var SMALL_LIST_THRESHOLD = 32;
|
|
1422
|
-
var
|
|
1482
|
+
var ABORT_SYM3 = /* @__PURE__ */ Symbol.for("forma-abort");
|
|
1423
1483
|
var CACHE_SYM2 = /* @__PURE__ */ Symbol.for("forma-attr-cache");
|
|
1424
1484
|
var DYNAMIC_CHILD_SYM2 = /* @__PURE__ */ Symbol.for("forma-dynamic-child");
|
|
1425
1485
|
function canPatchStaticElement(target, source) {
|
|
1426
|
-
return target instanceof HTMLElement && source instanceof HTMLElement && target.tagName === source.tagName && !target[
|
|
1486
|
+
return target instanceof HTMLElement && source instanceof HTMLElement && target.tagName === source.tagName && !target[ABORT_SYM3] && !target[CACHE_SYM2] && !target[DYNAMIC_CHILD_SYM2] && !source[ABORT_SYM3] && !source[CACHE_SYM2] && !source[DYNAMIC_CHILD_SYM2];
|
|
1427
1487
|
}
|
|
1428
1488
|
function patchStaticElement(target, source) {
|
|
1429
1489
|
const sourceAttrNames = /* @__PURE__ */ new Set();
|
|
@@ -1707,7 +1767,7 @@ function createList(items, keyFn, renderFn, options) {
|
|
|
1707
1767
|
if (cached.item === item) return;
|
|
1708
1768
|
cached.item = item;
|
|
1709
1769
|
if (!(node instanceof HTMLElement)) return;
|
|
1710
|
-
if (node[
|
|
1770
|
+
if (node[ABORT_SYM3] || node[CACHE_SYM2] || node[DYNAMIC_CHILD_SYM2]) {
|
|
1711
1771
|
return;
|
|
1712
1772
|
}
|
|
1713
1773
|
const next = untrack(() => renderFn(item, cached.getIndex));
|
|
@@ -1754,6 +1814,6 @@ function createList(items, keyFn, renderFn, options) {
|
|
|
1754
1814
|
return fragment2;
|
|
1755
1815
|
}
|
|
1756
1816
|
|
|
1757
|
-
export { Fragment, batch, cleanup, createEffect, createList, createMemo, createReducer, createRef, createResource, createRoot, createShow, fragment, h, hydrateIsland, internalEffect,
|
|
1758
|
-
//# sourceMappingURL=chunk-
|
|
1759
|
-
//# sourceMappingURL=chunk-
|
|
1817
|
+
export { Fragment, __DEV__, batch, cleanup, createEffect, createList, createMemo, createReducer, createRef, createResource, createRoot, createShow, fragment, h, hydrateIsland, internalEffect, on, onCleanup, onError, popSuspenseContext, pushSuspenseContext, reconcileList, reportError, untrack };
|
|
1818
|
+
//# sourceMappingURL=chunk-TKGUHASG.js.map
|
|
1819
|
+
//# sourceMappingURL=chunk-TKGUHASG.js.map
|