@getforma/core 0.3.1 → 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.
Files changed (42) hide show
  1. package/README.md +190 -16
  2. package/dist/{chunk-KX5WRZH7.js → chunk-DPSAVBCP.js} +3 -4
  3. package/dist/chunk-DPSAVBCP.js.map +1 -0
  4. package/dist/{chunk-FPSLC62A.cjs → chunk-KH3F5NRU.cjs} +2 -4
  5. package/dist/chunk-KH3F5NRU.cjs.map +1 -0
  6. package/dist/{chunk-5H52PKGF.js → chunk-TKGUHASG.js} +20 -10
  7. package/dist/chunk-TKGUHASG.js.map +1 -0
  8. package/dist/{chunk-TSQ7AKFT.cjs → chunk-YSKF3VRA.cjs} +27 -17
  9. package/dist/chunk-YSKF3VRA.cjs.map +1 -0
  10. package/dist/forma-runtime-csp.js +1 -1
  11. package/dist/forma-runtime.js +1 -1
  12. package/dist/formajs-runtime-hardened.global.js +1 -1
  13. package/dist/formajs-runtime-hardened.global.js.map +1 -1
  14. package/dist/formajs-runtime.global.js +1 -1
  15. package/dist/formajs-runtime.global.js.map +1 -1
  16. package/dist/formajs.global.js +1 -1
  17. package/dist/formajs.global.js.map +1 -1
  18. package/dist/index.cjs +102 -75
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +41 -9
  21. package/dist/index.d.ts +41 -9
  22. package/dist/index.js +46 -17
  23. package/dist/index.js.map +1 -1
  24. package/dist/runtime-hardened.cjs +61 -13
  25. package/dist/runtime-hardened.cjs.map +1 -1
  26. package/dist/runtime-hardened.js +62 -14
  27. package/dist/runtime-hardened.js.map +1 -1
  28. package/dist/runtime.cjs +85 -36
  29. package/dist/runtime.cjs.map +1 -1
  30. package/dist/runtime.js +63 -14
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/ssr/index.cjs +18 -10
  33. package/dist/ssr/index.cjs.map +1 -1
  34. package/dist/ssr/index.js +18 -10
  35. package/dist/ssr/index.js.map +1 -1
  36. package/dist/tc39-compat.cjs +3 -3
  37. package/dist/tc39-compat.js +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-5H52PKGF.js.map +0 -1
  40. package/dist/chunk-FPSLC62A.cjs.map +0 -1
  41. package/dist/chunk-KX5WRZH7.js.map +0 -1
  42. package/dist/chunk-TSQ7AKFT.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.
@@ -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
 
@@ -125,15 +162,14 @@ mount(() => <Counter />, '#app');
125
162
 
126
163
  ## CDN Usage
127
164
 
128
- ### Standard (recommended)
129
- ```html
130
- <script src="https://unpkg.com/@getforma/core/dist/forma-runtime.js"></script>
131
- ```
165
+ Both long and short filenames are provided. They are identical files — use whichever you prefer:
132
166
 
133
- ### CSP-Safe (strict Content-Security-Policy)
134
- ```html
135
- <script src="https://unpkg.com/@getforma/core/dist/forma-runtime-csp.js"></script>
136
- ```
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` |
137
173
 
138
174
  > The CSP build uses a hand-written expression parser and never calls `new Function`.
139
175
  > It supports most common patterns. See [examples/csp](./examples/csp) for a working demo.
@@ -231,6 +267,60 @@ const Timer = defineComponent(() => {
231
267
  document.body.appendChild(Timer());
232
268
  ```
233
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
+
234
324
  ### Context (Dependency Injection)
235
325
 
236
326
  ```typescript
@@ -244,27 +334,70 @@ const theme = inject(ThemeCtx); // 'dark'
244
334
 
245
335
  ## Islands Architecture
246
336
 
247
- 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.
248
338
 
249
339
  ```typescript
250
340
  import { activateIslands, createSignal, h } from '@getforma/core';
251
341
 
252
342
  activateIslands({
253
343
  Counter: (el, props) => {
254
- const [count, setCount] = createSignal(props.initial ?? 0);
255
- // Hydrate: attach reactivity to existing server-rendered DOM
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
+ );
256
356
  },
257
357
  });
258
358
  ```
259
359
 
260
360
  ```html
261
361
  <!-- Server-rendered HTML -->
262
- <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}'>
263
363
  <span>5</span>
264
364
  <button>+1</button>
265
365
  </div>
266
366
  ```
267
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
+
268
401
  ## Subpath Exports
269
402
 
270
403
  | Import | Description |
@@ -288,12 +421,53 @@ See the [`examples/`](./examples) directory:
288
421
  - **todo** — todo list with `createList` and keyed reconciliation
289
422
  - **data-table** — sortable table with `createList`
290
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
+
291
459
  ## Ecosystem
292
460
 
293
- | Package | Description |
294
- |---------|-------------|
295
- | [@getforma/core](https://www.npmjs.com/package/@getforma/core) | This library `npm install @getforma/core` |
296
- | [forma](https://github.com/getforma-dev/forma) | Rust server framework (forma-ir + forma-server) |
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 |
297
471
 
298
472
  ## License
299
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, createValueSignal };
26
- //# sourceMappingURL=chunk-KX5WRZH7.js.map
27
- //# sourceMappingURL=chunk-KX5WRZH7.js.map
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
- exports.createValueSignal = createValueSignal;
30
- //# sourceMappingURL=chunk-FPSLC62A.cjs.map
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, createValueSignal } from './chunk-KX5WRZH7.js';
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
@@ -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] = createValueSignal(false);
297
- const [error, setError] = createValueSignal(void 0);
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;
@@ -806,6 +806,10 @@ function appendChild(parent, child) {
806
806
  }
807
807
  }
808
808
  function h(tag, props, ...children) {
809
+ if (typeof tag === "function" && tag !== Fragment) {
810
+ const mergedProps = { ...props ?? {}, children };
811
+ return tag(mergedProps);
812
+ }
809
813
  if (tag === Fragment) {
810
814
  const frag = document.createDocumentFragment();
811
815
  for (const child of children) {
@@ -948,6 +952,7 @@ function createShow(when, thenFn, elseFn) {
948
952
  }
949
953
 
950
954
  // src/dom/hydrate.ts
955
+ var ABORT_SYM2 = /* @__PURE__ */ Symbol.for("forma-abort");
951
956
  var hydrating = false;
952
957
  function setHydrating(value2) {
953
958
  hydrating = value2;
@@ -967,7 +972,12 @@ function applyDynamicProps(el, props) {
967
972
  const value2 = props[key];
968
973
  if (typeof value2 !== "function") continue;
969
974
  if (key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && key.length > 2) {
970
- el.addEventListener(key.slice(2).toLowerCase(), value2);
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 });
971
981
  continue;
972
982
  }
973
983
  const fn = value2;
@@ -1469,11 +1479,11 @@ function longestIncreasingSubsequence(arr) {
1469
1479
  return result;
1470
1480
  }
1471
1481
  var SMALL_LIST_THRESHOLD = 32;
1472
- var ABORT_SYM2 = /* @__PURE__ */ Symbol.for("forma-abort");
1482
+ var ABORT_SYM3 = /* @__PURE__ */ Symbol.for("forma-abort");
1473
1483
  var CACHE_SYM2 = /* @__PURE__ */ Symbol.for("forma-attr-cache");
1474
1484
  var DYNAMIC_CHILD_SYM2 = /* @__PURE__ */ Symbol.for("forma-dynamic-child");
1475
1485
  function canPatchStaticElement(target, source) {
1476
- return target instanceof HTMLElement && source instanceof HTMLElement && target.tagName === source.tagName && !target[ABORT_SYM2] && !target[CACHE_SYM2] && !target[DYNAMIC_CHILD_SYM2] && !source[ABORT_SYM2] && !source[CACHE_SYM2] && !source[DYNAMIC_CHILD_SYM2];
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];
1477
1487
  }
1478
1488
  function patchStaticElement(target, source) {
1479
1489
  const sourceAttrNames = /* @__PURE__ */ new Set();
@@ -1757,7 +1767,7 @@ function createList(items, keyFn, renderFn, options) {
1757
1767
  if (cached.item === item) return;
1758
1768
  cached.item = item;
1759
1769
  if (!(node instanceof HTMLElement)) return;
1760
- if (node[ABORT_SYM2] || node[CACHE_SYM2] || node[DYNAMIC_CHILD_SYM2]) {
1770
+ if (node[ABORT_SYM3] || node[CACHE_SYM2] || node[DYNAMIC_CHILD_SYM2]) {
1761
1771
  return;
1762
1772
  }
1763
1773
  const next = untrack(() => renderFn(item, cached.getIndex));
@@ -1804,6 +1814,6 @@ function createList(items, keyFn, renderFn, options) {
1804
1814
  return fragment2;
1805
1815
  }
1806
1816
 
1807
- export { Fragment, __DEV__, batch, cleanup, createEffect, createList, createMemo, createReducer, createRef, createResource, createRoot, createShow, fragment, h, hydrateIsland, internalEffect, longestIncreasingSubsequence, on, onCleanup, onError, popSuspenseContext, pushSuspenseContext, reconcileList, untrack };
1808
- //# sourceMappingURL=chunk-5H52PKGF.js.map
1809
- //# sourceMappingURL=chunk-5H52PKGF.js.map
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