ilha 0.14.4 → 0.14.6

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
@@ -60,7 +60,7 @@ const total = atom(
60
60
  );
61
61
  ```
62
62
 
63
- Wrap multiple writes in `batch()`. Derived and mutation atoms use Effect's `Atom.map`, `Atom.transform`, and `Atom.fn` — pass `handle.atom`, then wrap in `atom()`. Use `watch(source, fn)` for side effects on atom changes.
63
+ Wrap multiple writes in `batch()`. Derived and mutation atoms use Effect's `Atom.map`, `Atom.transform`, and `Atom.fn` — pass `handle.atom`, then wrap in `atom()`. Use `watch(source, fn)` for side effects on atom changes. Use `watch.once(fn)` for mount-only work; return a cleanup function to run on unmount. Use `untrack(() => …)` to read without subscribing. Share values with `createContext` / `context()`. Catch subtree failures with `ErrorBoundary`.
64
64
 
65
65
  Use `atom.lazy(() => …)` for one-time initialization or to store a function value.
66
66
 
package/dist/define.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as Component } from "./types-DQVTD0ts.js";
1
+ import { n as Component } from "./types-CyoYWhJf.js";
2
2
  //#region src/define.d.ts
3
3
  declare const define: (name: string, component: Component) => void;
4
4
  //#endregion
package/dist/define.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as mount } from "./mount-hD-OyLIo.js";
1
+ import { t as mount } from "./mount-DiIEX8s7.js";
2
2
 
3
3
  //#region src/define.ts
4
4
  const define = (name, component) => {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as Instruction, c as View, i as Fragment, l as Yielded, n as Component, t as AtomHandle } from "./types-DQVTD0ts.js";
2
- import { t as h } from "./vnode-DHBqDHtZ.js";
1
+ import { a as Instruction, c as UnsafeHtml, d as Yielded, i as Fragment, n as Component, t as AtomHandle, u as View } from "./types-CyoYWhJf.js";
2
+ import { t as h } from "./vnode-C_BdUnIA.js";
3
3
  import * as Effect from "effect/Effect";
4
4
  import * as Stream from "effect/Stream";
5
5
  import * as Atom from "effect/unstable/reactivity/Atom";
@@ -11,10 +11,17 @@ type WatchValue = string | number | boolean | bigint | null | undefined | readon
11
11
  interface WatchRecord {
12
12
  readonly [key: string]: WatchValue | undefined;
13
13
  }
14
- /** Run `fn` when `source` changes — and once on mount. Sync, async, and generator components. */
15
- declare const watch: <A extends WatchValue>(source: AtomHandle<A> | Atom.Atom<A> | Stream.Stream<A, unknown, unknown>, fn: (value: A) => void) => Instruction<undefined>;
14
+ /** Optional disposer returned from `watch.once`. */
15
+ type WatchOnceCleanup = () => void;
16
+ interface WatchFn {
17
+ <A extends WatchValue>(source: AtomHandle<A> | Atom.Atom<A> | Stream.Stream<A, unknown, unknown>, fn: (value: A) => void): Instruction<undefined>;
18
+ once: (fn: () => WatchOnceCleanup | undefined) => Instruction<undefined>;
19
+ }
20
+ declare const watch: WatchFn;
16
21
  //#endregion
17
22
  //#region src/atom.d.ts
23
+ /** Read atoms inside `fn` without subscribing the active render. Sync only. */
24
+ declare const untrack: <A>(fn: () => A) => A;
18
25
  interface AtomFn {
19
26
  <A>(init: A | Atom.Atom<A> | Effect.Effect<A, unknown, AtomRegistry> | Stream.Stream<A, unknown, AtomRegistry>): AtomHandle<A>;
20
27
  lazy: <A>(init: () => A) => AtomHandle<A>;
@@ -22,6 +29,34 @@ interface AtomFn {
22
29
  declare const atom: AtomFn;
23
30
  declare const batch: (f: () => void) => void;
24
31
  //#endregion
32
+ //#region src/context.d.ts
33
+ interface ContextProviderProps<A> {
34
+ value: A;
35
+ children?: View;
36
+ }
37
+ interface IlhaContext<A> {
38
+ readonly id: symbol;
39
+ readonly defaultValue: A;
40
+ readonly Provider: (props: ContextProviderProps<A>) => View;
41
+ }
42
+ declare const createContext: <A>(defaultValue: A) => IlhaContext<A>;
43
+ /** Read a context value as an atom handle. Subscribes the active fiber on read. */
44
+ declare const context: <A>(ctx: IlhaContext<A>) => AtomHandle<A>;
45
+ //#endregion
46
+ //#region src/error-boundary.d.ts
47
+ interface ErrorFallbackProps {
48
+ error: Error;
49
+ reset: () => void;
50
+ }
51
+ type ErrorFallback = (props: ErrorFallbackProps) => View;
52
+ interface ErrorBoundaryProps {
53
+ fallback?: ErrorFallback;
54
+ onError?: (error: Error) => void;
55
+ children?: View;
56
+ }
57
+ /** Catch subtree failures and paint a fallback. Supports reset(). */
58
+ declare const ErrorBoundary: (props: ErrorBoundaryProps) => View;
59
+ //#endregion
25
60
  //#region src/when.d.ts
26
61
  declare const when: <A, E = never, R = never>(stream: Stream.Stream<A, E, R>, body: (value: A) => Generator<Yielded, View | undefined, View>) => Instruction<undefined, E>;
27
62
  //#endregion
@@ -39,4 +74,15 @@ interface MountOptions {
39
74
  declare const mount: (el: Element, fn: Component, opts?: MountOptions) => (() => void);
40
75
  declare const renderToString: (fn: Component, opts?: RenderToStringOptions) => Promise<string>;
41
76
  //#endregion
42
- export { type AtomHandle, type Component, Fragment, type MountOptions, type RenderToStringOptions, type View, atom, batch, h, mount, renderToString, watch, when };
77
+ //#region src/unsafe.d.ts
78
+ /**
79
+ * Mark a string as pre-rendered HTML and paint it without escaping.
80
+ * Use it for markup you built or sanitized yourself — SVG from an external
81
+ * library, Markdown rendered to HTML, embed codes. Never pass untrusted
82
+ * user input: unlike interpolated strings, this content is not escaped and
83
+ * scripts inserted this way do not run, but event attributes and other
84
+ * markup still apply.
85
+ */
86
+ declare const unsafe: (html: string) => UnsafeHtml;
87
+ //#endregion
88
+ export { type AtomHandle, type Component, type ContextProviderProps, ErrorBoundary, type ErrorBoundaryProps, type ErrorFallback, type ErrorFallbackProps, Fragment, type IlhaContext, type MountOptions, type RenderToStringOptions, type UnsafeHtml, type View, type WatchOnceCleanup, atom, batch, context, createContext, h, mount, renderToString, unsafe, untrack, watch, when };
package/dist/index.js CHANGED
@@ -1,13 +1,82 @@
1
- import { a as batch, c as isAtomHandle, d as makeFiber, i as atom, l as closeFiber, n as renderToString, o as handleOwner, r as interpret, s as instr, t as mount, u as getFiber } from "./mount-hD-OyLIo.js";
2
- import { i as Fragment, t as h } from "./vnode-DP76Gn-b.js";
1
+ import { a as interpret, c as handleOwner, d as untrack, f as wrapHandle, g as makeFiber, h as getFiber, i as runSetup, l as instr, m as closeFiber, n as renderToString, o as atom, p as clearFiberView, r as unsafe, s as batch, t as mount, u as isAtomHandle } from "./mount-DiIEX8s7.js";
2
+ import { d as errorView, i as Fragment, p as isFunction, t as h } from "./vnode-DEMDQ0eA.js";
3
3
  import * as Effect from "effect/Effect";
4
4
  import * as Stream from "effect/Stream";
5
+ import * as Atom from "effect/unstable/reactivity/Atom";
5
6
  import * as Exit from "effect/Exit";
6
7
  import * as Fiber from "effect/Fiber";
7
8
  import * as Scope from "effect/Scope";
8
9
  import * as Registry from "effect/unstable/reactivity/AtomRegistry";
9
10
 
11
+ //#region src/context.ts
12
+ const defaults = /* @__PURE__ */ new WeakMap();
13
+ const getDefaultAtom = (ctx) => {
14
+ const existing = defaults.get(ctx);
15
+ if (existing) return existing;
16
+ const made = Atom.make(ctx.defaultValue);
17
+ defaults.set(ctx, made);
18
+ return made;
19
+ };
20
+ const createContext = (defaultValue) => {
21
+ const id = Symbol("ilha.context");
22
+ const Provider = (props) => {
23
+ const fiber = getFiber();
24
+ const { value: next, children } = props;
25
+ const handle = atom(next);
26
+ if (!Object.is(untrack(() => handle()), next)) handle.set(next);
27
+ fiber.contexts ??= /* @__PURE__ */ new Map();
28
+ fiber.contexts.set(id, handle.atom);
29
+ return children;
30
+ };
31
+ return {
32
+ Provider,
33
+ defaultValue,
34
+ id
35
+ };
36
+ };
37
+ /** Read a context value as an atom handle. Subscribes the active fiber on read. */
38
+ const context = (ctx) => {
39
+ const fiber = getFiber();
40
+ let current = fiber;
41
+ while (current) {
42
+ const found = current.contexts?.get(ctx.id);
43
+ if (found) return wrapHandle(found, fiber);
44
+ current = current.parent;
45
+ }
46
+ return wrapHandle(getDefaultAtom(ctx), fiber);
47
+ };
48
+
49
+ //#endregion
50
+ //#region src/error-boundary.ts
51
+ /** Catch subtree failures and paint a fallback. Supports reset(). */
52
+ const ErrorBoundary = (props) => {
53
+ const fiber = getFiber();
54
+ const remount = () => {
55
+ if (fiber.closed) return;
56
+ clearFiberView(fiber);
57
+ const current = fiber.propsBox?.current ?? props;
58
+ runSetup(fiber, () => ErrorBoundary(current));
59
+ };
60
+ fiber.handleError = (error) => {
61
+ if (fiber.closed) return;
62
+ clearFiberView(fiber);
63
+ const { onError, fallback } = fiber.propsBox?.current ?? props;
64
+ if (isFunction(onError)) onError(error);
65
+ if (isFunction(fallback)) {
66
+ fiber.paint(fallback({
67
+ error,
68
+ reset: remount
69
+ }));
70
+ return;
71
+ }
72
+ fiber.paint(errorView(error));
73
+ };
74
+ return props.children;
75
+ };
76
+
77
+ //#endregion
10
78
  //#region src/watch.ts
79
+ const WATCH_ONCE = Symbol("ilha.watch.once");
11
80
  const runStreamWatch = (fiber, effect) => {
12
81
  const scope = Scope.forkUnsafe(fiber.scope);
13
82
  const provided = effect.pipe(Effect.provideService(Registry.AtomRegistry, fiber.registry));
@@ -24,7 +93,7 @@ const useWatchSlot = (fiber, key, mount, fn) => {
24
93
  const existing = fiber.watchSlots[i];
25
94
  if (existing) {
26
95
  if (existing.key === key) {
27
- existing.fnRef.current = fn;
96
+ if (existing.fnRef) existing.fnRef.current = fn;
28
97
  return;
29
98
  }
30
99
  existing.dispose();
@@ -58,12 +127,36 @@ const registerWatch = (fiber, source, fn) => {
58
127
  const atom = isAtomHandle(source) ? source.atom : source;
59
128
  useWatchSlot(fiber, atom, (run) => fiber.registry.subscribe(atom, run, { immediate: true }), fn);
60
129
  };
130
+ /** Run `fn` once on mount. Return a function to clean up on unmount. */
131
+ const watchOnce = (fn) => {
132
+ const fiber = getFiber();
133
+ const i = fiber.watchI ?? 0;
134
+ fiber.watchI = i + 1;
135
+ fiber.watchSlots ??= [];
136
+ const existing = fiber.watchSlots[i];
137
+ if (existing) {
138
+ if (existing.key === WATCH_ONCE) return instr(Effect.void);
139
+ existing.dispose();
140
+ fiber.watchSlots[i] = void 0;
141
+ }
142
+ let cleanup = fn();
143
+ fiber.watchSlots[i] = {
144
+ dispose: () => {
145
+ const run = cleanup;
146
+ cleanup = void 0;
147
+ run?.();
148
+ },
149
+ key: WATCH_ONCE
150
+ };
151
+ return instr(Effect.void);
152
+ };
61
153
  /** Run `fn` when `source` changes — and once on mount. Sync, async, and generator components. */
62
- const watch = (source, fn) => {
154
+ const watchSource = (source, fn) => {
63
155
  const fiber = (isAtomHandle(source) ? handleOwner.get(source) : void 0) ?? getFiber();
64
156
  registerWatch(fiber, source, fn);
65
157
  return instr(Effect.void);
66
158
  };
159
+ const watch = Object.assign(watchSource, { once: watchOnce });
67
160
 
68
161
  //#endregion
69
162
  //#region src/when.ts
@@ -78,10 +171,13 @@ const when = (stream, body) => instr(Effect.gen(function* whenBody() {
78
171
  const src = parent.runtime.ssr ? base.pipe(Stream.take(1)) : base;
79
172
  yield* Stream.runForEach(src, (value) => Effect.sync(() => {
80
173
  if (child) closeFiber(child);
81
- child = makeFiber(parent.runtime, parent.root, parent.paintFn, { onFail: parent.fail });
174
+ child = makeFiber(parent.runtime, parent.root, parent.paintFn, {
175
+ onFail: parent.fail,
176
+ parent
177
+ });
82
178
  interpret((() => body(value)), child);
83
179
  }));
84
180
  }));
85
181
 
86
182
  //#endregion
87
- export { Fragment, atom, batch, h, mount, renderToString, watch, when };
183
+ export { ErrorBoundary, Fragment, atom, batch, context, createContext, h, mount, renderToString, unsafe, untrack, watch, when };
@@ -1,4 +1,4 @@
1
- import { i as Fragment } from "./types-DQVTD0ts.js";
2
- import { n as JSX } from "./vnode-DHBqDHtZ.js";
1
+ import { i as Fragment } from "./types-CyoYWhJf.js";
2
+ import { n as JSX } from "./vnode-C_BdUnIA.js";
3
3
  import { jsx, jsxDEV, jsxs } from "./jsx-runtime.js";
4
4
  export { Fragment, type JSX, jsx, jsxDEV, jsxs };
@@ -1,4 +1,4 @@
1
- import { i as Fragment } from "./vnode-DP76Gn-b.js";
1
+ import { i as Fragment } from "./vnode-DEMDQ0eA.js";
2
2
  import { jsx, jsxDEV, jsxs } from "./jsx-runtime.js";
3
3
 
4
4
  export { Fragment, jsx, jsxDEV, jsxs };
@@ -1,8 +1,8 @@
1
- import { c as View, i as Fragment, o as PropBag, r as ComponentFn } from "./types-DQVTD0ts.js";
2
- import { n as JSX, t as h } from "./vnode-DHBqDHtZ.js";
1
+ import { i as Fragment, o as JsxComponent, r as ComponentFn, s as PropBag, u as View } from "./types-CyoYWhJf.js";
2
+ import { n as JSX, t as h } from "./vnode-C_BdUnIA.js";
3
3
  //#region src/jsx-runtime.d.ts
4
- declare const jsx: (type: string | Fragment | ComponentFn, props: PropBag | null, key?: string | number) => ReturnType<typeof h>;
5
- declare const jsxs: (type: string | Fragment | ComponentFn, props: PropBag | null, key?: string | number) => ReturnType<typeof h>;
6
- declare const jsxDEV: (type: string | Fragment | ComponentFn, props: PropBag | null, key?: string | number) => ReturnType<typeof h>;
4
+ declare const jsx: (type: string | Fragment | ComponentFn | JsxComponent, props: PropBag | null, key?: string | number) => ReturnType<typeof h>;
5
+ declare const jsxs: (type: string | Fragment | ComponentFn | JsxComponent, props: PropBag | null, key?: string | number) => ReturnType<typeof h>;
6
+ declare const jsxDEV: (type: string | Fragment | ComponentFn | JsxComponent, props: PropBag | null, key?: string | number) => ReturnType<typeof h>;
7
7
  //#endregion
8
8
  export { Fragment, type JSX, type View, h, jsx, jsxDEV, jsxs };
@@ -1,4 +1,4 @@
1
- import { i as Fragment, t as h } from "./vnode-DP76Gn-b.js";
1
+ import { i as Fragment, t as h } from "./vnode-DEMDQ0eA.js";
2
2
 
3
3
  //#region src/jsx-runtime.ts
4
4
  const jsx = (type, props, key) => h(type, key === void 0 ? props : {
@@ -1,4 +1,4 @@
1
- import { _ as skip, a as ISLAND, b as failureMessage, c as KEEP, d as errorView, f as isBigInt, g as isString, h as isObject, i as Fragment, l as KEY_ATTR, m as isNumber, n as isSetupFn, o as ISLAND_MOUNT_INTERNAL, p as isFunction, r as isVNode, s as ISLAND_SLOT_TAG, u as SLOT_ATTR, v as unwrap, y as asFailure } from "./vnode-DP76Gn-b.js";
1
+ import { _ as skip, a as ISLAND, b as failureMessage, c as KEEP, d as errorView, f as isBigInt, g as isString, h as isObject, i as Fragment, l as KEY_ATTR, m as isNumber, n as isSetupFn, o as ISLAND_MOUNT_INTERNAL, p as isFunction, r as isVNode, s as ISLAND_SLOT_TAG, u as SLOT_ATTR, v as unwrap, x as toError, y as asFailure } from "./vnode-DEMDQ0eA.js";
2
2
  import * as Effect from "effect/Effect";
3
3
  import * as Stream from "effect/Stream";
4
4
  import * as Atom from "effect/unstable/reactivity/Atom";
@@ -34,17 +34,27 @@ const runInScope = (effect, registry, scope, onOk, onErr, onDone) => {
34
34
  }))) : provided);
35
35
  Effect.runSync(Scope.addFinalizer(scope, Fiber.interrupt(f)));
36
36
  };
37
- const closeFiber = (fiber) => {
38
- if (fiber.closed) return;
39
- fiber.closed = true;
40
- fiber.trackRestore?.();
41
- fiber.trackRestore = void 0;
37
+ /** Clear render subscriptions and child holes without closing the fiber scope. */
38
+ const clearFiberView = (fiber) => {
42
39
  fiber.renderSub?.();
43
40
  fiber.renderSub = void 0;
44
41
  for (const slot of fiber.watchSlots ?? []) slot?.dispose();
45
42
  fiber.watchSlots = [];
43
+ fiber.watchI = 0;
44
+ fiber.primitives = void 0;
45
+ fiber.primitiveI = 0;
46
+ fiber.islandFrame = void 0;
47
+ fiber.keyedHoles = void 0;
46
48
  for (const h of fiber.holes) h.dispose();
47
49
  fiber.holes = [];
50
+ };
51
+ const closeFiber = (fiber) => {
52
+ if (fiber.closed) return;
53
+ fiber.closed = true;
54
+ fiber.trackRestore?.();
55
+ fiber.trackRestore = void 0;
56
+ fiber.handleError = void 0;
57
+ clearFiberView(fiber);
48
58
  Effect.runFork(Scope.close(fiber.scope, Exit.void));
49
59
  };
50
60
  const makeRuntime = (opts) => {
@@ -107,6 +117,7 @@ const makeFiber = (runtime, root, paint, opts) => {
107
117
  inFlight: false,
108
118
  paint: (view) => paint(fiber, view),
109
119
  paintFn: paint,
120
+ parent: opts?.parent,
110
121
  registry: runtime.registry,
111
122
  root,
112
123
  run: (effect, onOk, onErr) => {
@@ -123,6 +134,19 @@ const makeFiber = (runtime, root, paint, opts) => {
123
134
  };
124
135
  return fiber;
125
136
  };
137
+ /** Walk this fiber and ancestors for an error boundary handler. */
138
+ const reportFiberError = (fiber, error) => {
139
+ let current = fiber;
140
+ while (current) {
141
+ const { handleError } = current;
142
+ if (handleError) {
143
+ handleError(error);
144
+ return true;
145
+ }
146
+ current = current.parent;
147
+ }
148
+ return false;
149
+ };
126
150
 
127
151
  //#endregion
128
152
  //#region src/atom.ts
@@ -161,6 +185,16 @@ const instr = (effect) => {
161
185
  const isAtomHandle = (x) => isFunction(x) && x.$$atom === 1;
162
186
  let trackGet;
163
187
  const trackStack = [];
188
+ /** Read atoms inside `fn` without subscribing the active render. Sync only. */
189
+ const untrack = (fn) => {
190
+ const prev = trackGet;
191
+ trackGet = void 0;
192
+ try {
193
+ return fn();
194
+ } finally {
195
+ trackGet = prev;
196
+ }
197
+ };
164
198
  const beginPrimitiveFrame = (fiber) => {
165
199
  const used = fiber.primitiveI ?? 0;
166
200
  const slots = fiber.primitives?.length ?? 0;
@@ -377,6 +411,60 @@ const interpret = (gen, fiber) => {
377
411
  resume();
378
412
  };
379
413
 
414
+ //#endregion
415
+ //#region src/start.ts
416
+ const isGen = (x) => {
417
+ if (x === null || x === void 0) return false;
418
+ const g = x;
419
+ return isFunction(g.next) && isFunction(g.throw);
420
+ };
421
+ const finish = (fiber, view) => {
422
+ if (fiber.closed) return;
423
+ if (view !== void 0) fiber.paint(view);
424
+ };
425
+ const runSetup = (fiber, fn) => {
426
+ const disposeRenderSub = () => {
427
+ fiber.renderSub?.();
428
+ fiber.renderSub = void 0;
429
+ };
430
+ const api = {
431
+ commit: (out, deps) => {
432
+ if (isGen(out)) {
433
+ disposeRenderSub();
434
+ interpret((() => out), fiber);
435
+ return;
436
+ }
437
+ disposeRenderSub();
438
+ if (!fiber.runtime.ssr) fiber.renderSub = subscribeRenderDeps(fiber, deps, () => api.render());
439
+ withFiber(fiber, () => {
440
+ finish(fiber, out);
441
+ });
442
+ },
443
+ render: () => {
444
+ if (fiber.closed) return;
445
+ fiber.runtime.begin();
446
+ beginPrimitiveFrame(fiber);
447
+ withTrackGetRun(fiber, () => fn(), ({ value: out, deps }) => {
448
+ try {
449
+ api.commit(out, deps);
450
+ } catch (error) {
451
+ withFiber(fiber, () => fiber.fail(asFailure(error)));
452
+ } finally {
453
+ fiber.runtime.end();
454
+ }
455
+ }, (e) => {
456
+ withFiber(fiber, () => fiber.fail(asFailure(e)));
457
+ fiber.runtime.end();
458
+ });
459
+ }
460
+ };
461
+ try {
462
+ api.render();
463
+ } catch (error) {
464
+ fiber.fail(asFailure(error));
465
+ }
466
+ };
467
+
380
468
  //#endregion
381
469
  //#region src/events.ts
382
470
  const ACTION_CALL$1 = Symbol.for("ilha.actionCall");
@@ -538,8 +626,13 @@ const morphElementPair = (fromEl, toEl, api) => {
538
626
  fromEl.replaceWith(toEl);
539
627
  return;
540
628
  }
541
- const slotId = toEl.getAttribute(SLOT_ATTR);
542
- if (slotId !== null && fromEl.getAttribute("data-ilha-slot") === slotId) return;
629
+ const toSlot = toEl.getAttribute(SLOT_ATTR);
630
+ const fromSlot = fromEl.getAttribute(SLOT_ATTR);
631
+ if (toSlot !== null && fromSlot === toSlot) return;
632
+ if (toSlot !== null) {
633
+ fromEl.replaceWith(toEl);
634
+ return;
635
+ }
543
636
  if (fromEl.localName === "input") {
544
637
  morphInput(fromEl, toEl);
545
638
  return;
@@ -692,57 +785,22 @@ const serializeStyleAttr = (value) => {
692
785
  };
693
786
 
694
787
  //#endregion
695
- //#region src/start.ts
696
- const isGen = (x) => {
697
- if (x === null || x === void 0) return false;
698
- const g = x;
699
- return isFunction(g.next) && isFunction(g.throw);
700
- };
701
- const finish = (fiber, view) => {
702
- if (fiber.closed) return;
703
- if (view !== void 0) fiber.paint(view);
704
- };
705
- const runSetup = (fiber, fn) => {
706
- const disposeRenderSub = () => {
707
- fiber.renderSub?.();
708
- fiber.renderSub = void 0;
709
- };
710
- const api = {
711
- commit: (out, deps) => {
712
- if (isGen(out)) {
713
- disposeRenderSub();
714
- interpret((() => out), fiber);
715
- return;
716
- }
717
- disposeRenderSub();
718
- if (!fiber.runtime.ssr) fiber.renderSub = subscribeRenderDeps(fiber, deps, () => api.render());
719
- withFiber(fiber, () => {
720
- finish(fiber, out);
721
- });
722
- },
723
- render: () => {
724
- if (fiber.closed) return;
725
- fiber.runtime.begin();
726
- beginPrimitiveFrame(fiber);
727
- withTrackGetRun(fiber, () => fn(), ({ value: out, deps }) => {
728
- try {
729
- api.commit(out, deps);
730
- } catch (error) {
731
- withFiber(fiber, () => fiber.fail(asFailure(error)));
732
- } finally {
733
- fiber.runtime.end();
734
- }
735
- }, (e) => {
736
- withFiber(fiber, () => fiber.fail(asFailure(e)));
737
- fiber.runtime.end();
738
- });
739
- }
740
- };
741
- try {
742
- api.render();
743
- } catch (error) {
744
- fiber.fail(asFailure(error));
745
- }
788
+ //#region src/unsafe.ts
789
+ /**
790
+ * Mark a string as pre-rendered HTML and paint it without escaping.
791
+ * Use it for markup you built or sanitized yourself — SVG from an external
792
+ * library, Markdown rendered to HTML, embed codes. Never pass untrusted
793
+ * user input: unlike interpolated strings, this content is not escaped and
794
+ * scripts inserted this way do not run, but event attributes and other
795
+ * markup still apply.
796
+ */
797
+ const unsafe = (html) => ({
798
+ $$ilhaUnsafe: 1,
799
+ html
800
+ });
801
+ const isUnsafeHtml = (x) => {
802
+ if (!isObject(x)) return false;
803
+ return x.$$ilhaUnsafe === 1;
746
804
  };
747
805
 
748
806
  //#endregion
@@ -862,10 +920,15 @@ const createPainter = (ops) => {
862
920
  openHole: (parent) => {
863
921
  const id = parent.runtime.nextHole();
864
922
  const host = ops.createSlotHost(String(id));
865
- const child = makeFiber(parent.runtime, ops.asRoot(host), (f, v) => api.paintHole(f, v), { onFail: (e) => {
866
- console.error(e);
867
- api.paintHole(child, errorView(e));
868
- } });
923
+ const child = makeFiber(parent.runtime, ops.asRoot(host), (f, v) => api.paintHole(f, v), {
924
+ onFail: (e) => {
925
+ const error = toError(e);
926
+ if (reportFiberError(child, error)) return;
927
+ console.error(error);
928
+ api.paintHole(child, errorView(error));
929
+ },
930
+ parent
931
+ });
869
932
  return {
870
933
  fiber: child,
871
934
  nodes: [host]
@@ -979,9 +1042,10 @@ const createPainter = (ops) => {
979
1042
  if (branded[ISLAND] === true) return materializeIsland(branded, props, fiber);
980
1043
  const k = view.key === null || view.key === void 0 ? void 0 : String(view.key);
981
1044
  const reuse = k ? fiber.keyedHoles?.get(k) : void 0;
1045
+ const run = type;
982
1046
  if (reuse && !reuse.closed) {
983
1047
  if (reuse.propsBox) reuse.propsBox.current = props;
984
- const next = withFiber(fiber, () => type(reuse.propsBox?.current ?? props));
1048
+ const next = withFiber(fiber, () => run(reuse.propsBox?.current ?? props));
985
1049
  if (next && !isThenable(next) && !isSetupFn(next)) reuse.paint(next);
986
1050
  return [ops.asNode(reuse.root)];
987
1051
  }
@@ -995,7 +1059,7 @@ const createPainter = (ops) => {
995
1059
  if (hole.closed) return;
996
1060
  const box = hole.propsBox;
997
1061
  if (!box) return;
998
- runSetup(hole, () => type(box.current));
1062
+ runSetup(hole, () => run(box.current));
999
1063
  });
1000
1064
  trackHole(fiber, hole, void 0, k ? { keyed: true } : void 0);
1001
1065
  return nodes;
@@ -1021,6 +1085,7 @@ const createPainter = (ops) => {
1021
1085
  };
1022
1086
  paintFns.materialize = (view, fiber) => {
1023
1087
  if (skip(view)) return [];
1088
+ if (isUnsafeHtml(view)) return ops.createRaw(view.html, fiber.root);
1024
1089
  if (isTextish(view)) return [ops.createText(String(view))];
1025
1090
  if (isAtomHandle(view)) return materializeAtom(view, fiber);
1026
1091
  if (Stream.isStream(view)) return materializeStream(view, fiber);
@@ -1081,6 +1146,11 @@ const core$1 = createPainter({
1081
1146
  if (root instanceof Element) root.replaceChildren();
1082
1147
  },
1083
1148
  createElement: (tag) => document.createElement(tag),
1149
+ createRaw: (html, parent) => {
1150
+ const range = document.createRange();
1151
+ range.selectNodeContents(parent);
1152
+ return [...range.createContextualFragment(html).childNodes];
1153
+ },
1084
1154
  createSlotHost: (slotId) => {
1085
1155
  const host = document.createElement("span");
1086
1156
  if (slotId !== null) host.setAttribute(SLOT_ATTR, slotId);
@@ -1253,6 +1323,10 @@ const VOID = /* @__PURE__ */ new Set([
1253
1323
  const RAW_TEXT = /* @__PURE__ */ new Set(["script", "style"]);
1254
1324
  const escapeText = (s) => s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1255
1325
  const escapeAttr = (s) => escapeText(s).replaceAll("\"", "&quot;");
1326
+ const createSsrRaw = (html) => ({
1327
+ html,
1328
+ kind: "raw"
1329
+ });
1256
1330
  const createSsrText = (text) => ({
1257
1331
  kind: "text",
1258
1332
  text
@@ -1263,6 +1337,7 @@ const serializeRawText = (tag, text) => {
1263
1337
  };
1264
1338
  const serializeNode = (node) => {
1265
1339
  if (node.kind === "text") return escapeText(node.text);
1340
+ if (node.kind === "raw") return node.html;
1266
1341
  const parts = [];
1267
1342
  for (const [name, value] of node.attrs) parts.push(`${name}="${escapeAttr(value)}"`);
1268
1343
  const attr = parts.length > 0 ? ` ${parts.join(" ")}` : "";
@@ -1319,7 +1394,11 @@ const createSsrElement = (tag) => {
1319
1394
  tag: lower,
1320
1395
  tagName: lower.toUpperCase(),
1321
1396
  get textContent() {
1322
- return children.map((n) => n.kind === "text" ? n.text : n.textContent).join("");
1397
+ return children.map((n) => {
1398
+ if (n.kind === "text") return n.text;
1399
+ if (n.kind === "raw") return n.html;
1400
+ return n.textContent;
1401
+ }).join("");
1323
1402
  },
1324
1403
  set textContent(value) {
1325
1404
  children.length = 0;
@@ -1390,6 +1469,7 @@ const ssrOps = {
1390
1469
  bindEvents: bindSsrEvents,
1391
1470
  clearRoot: (root) => ssrAs(root).replaceChildren(),
1392
1471
  createElement: createSsrElement,
1472
+ createRaw: (html, _parent) => [createSsrRaw(html)],
1393
1473
  createSlotHost: (slotId) => {
1394
1474
  const host = createSsrElement("span");
1395
1475
  if (slotId !== null) host.setAttribute(SLOT_ATTR, slotId);
@@ -1516,4 +1596,4 @@ const renderToString = async (fn, opts) => {
1516
1596
  };
1517
1597
 
1518
1598
  //#endregion
1519
- export { batch as a, isAtomHandle as c, makeFiber as d, atom as i, closeFiber as l, renderToString as n, handleOwner as o, interpret as r, instr as s, mount as t, getFiber as u };
1599
+ export { interpret as a, handleOwner as c, untrack as d, wrapHandle as f, makeFiber as g, getFiber as h, runSetup as i, instr as l, closeFiber as m, renderToString as n, atom as o, clearFiberView as p, unsafe as r, batch as s, mount as t, isAtomHandle as u };
@@ -12,13 +12,20 @@ type EventHandler = (event: Event) => void;
12
12
  interface PropBag {
13
13
  readonly [key: string]: PropValue | undefined;
14
14
  }
15
- type View = VNode | JsonText | AtomHandle<unknown> | Stream.Stream<View, unknown, unknown> | GeneratorFn | Component | View[] | Iterable<View>;
15
+ type View = VNode | UnsafeHtml | JsonText | AtomHandle<unknown> | Stream.Stream<View, unknown, unknown> | GeneratorFn | Component | View[] | Iterable<View>;
16
16
  declare const Fragment: unique symbol;
17
17
  type Fragment = typeof Fragment;
18
18
  type ComponentFn = (props: PropBag) => View | Promise<View | undefined> | undefined;
19
+ /** JSX/tag component with its own props shape (Provider, ErrorBoundary, …). */
20
+ type JsxComponent = (props: never) => View | Promise<View | undefined> | Generator<View, View | undefined, View> | undefined;
21
+ /** Raw HTML view produced by `unsafe()` — painted without escaping. */
22
+ interface UnsafeHtml {
23
+ readonly $$ilhaUnsafe: 1;
24
+ readonly html: string;
25
+ }
19
26
  interface VNode {
20
27
  readonly $$ilha: 1;
21
- readonly type: string | Fragment | ComponentFn;
28
+ readonly type: string | Fragment | ComponentFn | JsxComponent;
22
29
  readonly props: PropBag;
23
30
  readonly children: View[];
24
31
  readonly key?: string | number;
@@ -38,4 +45,4 @@ interface AtomHandle<A> {
38
45
  update: (f: (current: A) => A) => void;
39
46
  }
40
47
  //#endregion
41
- export { Instruction as a, View as c, Fragment as i, Yielded as l, Component as n, PropBag as o, ComponentFn as r, VNode as s, AtomHandle as t };
48
+ export { Instruction as a, UnsafeHtml as c, Yielded as d, Fragment as i, VNode as l, Component as n, JsxComponent as o, ComponentFn as r, PropBag as s, AtomHandle as t, View as u };
@@ -1,8 +1,8 @@
1
- import { c as View, i as Fragment, o as PropBag, r as ComponentFn, s as VNode, t as AtomHandle } from "./types-DQVTD0ts.js";
1
+ import { i as Fragment, l as VNode, o as JsxComponent, r as ComponentFn, s as PropBag, t as AtomHandle, u as View } from "./types-CyoYWhJf.js";
2
2
  //#region src/jsx-types.d.ts
3
3
  declare namespace JSX {
4
4
  export type Element = View;
5
- export type ElementType = string | ((props: PropBag) => View | Promise<View | undefined> | Generator<View, View | undefined, View>);
5
+ export type ElementType = string | ((props: never) => View | Promise<View | undefined> | Generator<View, View | undefined, View> | undefined);
6
6
  export interface ElementChildrenAttribute {
7
7
  children: View;
8
8
  }
@@ -580,6 +580,6 @@ declare namespace JSX {
580
580
  }
581
581
  //#endregion
582
582
  //#region src/vnode.d.ts
583
- declare const h: (type: string | Fragment | ComponentFn, props: PropBag | null | undefined, ...rest: View[]) => VNode;
583
+ declare const h: (type: string | Fragment | ComponentFn | JsxComponent, props: PropBag | null | undefined, ...rest: View[]) => VNode;
584
584
  //#endregion
585
585
  export { JSX as n, h as t };
@@ -10,10 +10,12 @@ const asFailure = (e) => {
10
10
  }
11
11
  return Result.fail(e instanceof Error ? e : new Error(String(e)));
12
12
  };
13
- const failureMessage = (e) => asFailure(e).pipe(Result.match({
14
- onFailure: (err) => err.message,
15
- onSuccess: () => "error"
13
+ /** Normalize any thrown failure into an `Error` instance. */
14
+ const toError = (e) => asFailure(e).pipe(Result.match({
15
+ onFailure: (err) => err,
16
+ onSuccess: () => /* @__PURE__ */ new Error("error")
16
17
  }));
18
+ const failureMessage = (e) => toError(e).message;
17
19
 
18
20
  //#endregion
19
21
  //#region src/shared.ts
@@ -86,4 +88,4 @@ const isSetupFn = (x) => {
86
88
  };
87
89
 
88
90
  //#endregion
89
- export { skip as _, ISLAND as a, failureMessage as b, KEEP as c, errorView as d, isBigInt as f, isString as g, isObject as h, Fragment as i, KEY_ATTR as l, isNumber as m, isSetupFn as n, ISLAND_MOUNT_INTERNAL as o, isFunction as p, isVNode as r, ISLAND_SLOT_TAG as s, h as t, SLOT_ATTR as u, unwrap as v, asFailure as y };
91
+ export { skip as _, ISLAND as a, failureMessage as b, KEEP as c, errorView as d, isBigInt as f, isString as g, isObject as h, Fragment as i, KEY_ATTR as l, isNumber as m, isSetupFn as n, ISLAND_MOUNT_INTERNAL as o, isFunction as p, isVNode as r, ISLAND_SLOT_TAG as s, h as t, SLOT_ATTR as u, unwrap as v, toError as x, asFailure as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
4
4
  "description": "Generator-first island UI on Effect",
5
5
  "keywords": [
6
6
  "effect",