@getforma/core 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-263HW3KN.cjs +35 -0
  3. package/dist/chunk-263HW3KN.cjs.map +1 -0
  4. package/dist/chunk-2QQBKQIF.js +30 -0
  5. package/dist/chunk-2QQBKQIF.js.map +1 -0
  6. package/dist/{chunk-6FW5E54W.cjs → chunk-2Y5US35K.cjs} +127 -103
  7. package/dist/chunk-2Y5US35K.cjs.map +1 -0
  8. package/dist/{chunk-3G7ET4O5.js → chunk-INNOI6TG.js} +121 -94
  9. package/dist/chunk-INNOI6TG.js.map +1 -0
  10. package/dist/{chunk-T33QUD2Y.cjs → chunk-JRQNDXX7.cjs} +43 -43
  11. package/dist/{chunk-T33QUD2Y.cjs.map → chunk-JRQNDXX7.cjs.map} +1 -1
  12. package/dist/{chunk-AFRFF7XL.js → chunk-MIOMT2CB.js} +4 -4
  13. package/dist/{chunk-AFRFF7XL.js.map → chunk-MIOMT2CB.js.map} +1 -1
  14. package/dist/chunk-W7OUWVRA.cjs +106 -0
  15. package/dist/chunk-W7OUWVRA.cjs.map +1 -0
  16. package/dist/chunk-XLVBYXOU.js +100 -0
  17. package/dist/chunk-XLVBYXOU.js.map +1 -0
  18. package/dist/forma-runtime-csp.js +92 -6
  19. package/dist/forma-runtime.js +138 -12
  20. package/dist/formajs-runtime-hardened.global.js +92 -6
  21. package/dist/formajs-runtime-hardened.global.js.map +1 -1
  22. package/dist/formajs-runtime.global.js +138 -12
  23. package/dist/formajs-runtime.global.js.map +1 -1
  24. package/dist/formajs.global.js +192 -95
  25. package/dist/formajs.global.js.map +1 -1
  26. package/dist/http.cjs +11 -11
  27. package/dist/http.js +2 -2
  28. package/dist/index.cjs +66 -66
  29. package/dist/index.d.cts +11 -25
  30. package/dist/index.d.ts +11 -25
  31. package/dist/index.js +6 -6
  32. package/dist/{resource-DeEzxUz6.d.cts → resource-D6HfVgiA.d.cts} +7 -2
  33. package/dist/{resource-BHsgURy0.d.ts → resource-ljs2X5NM.d.ts} +7 -2
  34. package/dist/runtime-hardened.cjs +93 -5
  35. package/dist/runtime-hardened.cjs.map +1 -1
  36. package/dist/runtime-hardened.js +93 -5
  37. package/dist/runtime-hardened.js.map +1 -1
  38. package/dist/runtime.cjs +47 -31
  39. package/dist/runtime.cjs.map +1 -1
  40. package/dist/runtime.js +21 -5
  41. package/dist/runtime.js.map +1 -1
  42. package/dist/server.cjs +7 -7
  43. package/dist/server.d.cts +2 -2
  44. package/dist/server.d.ts +2 -2
  45. package/dist/server.js +2 -2
  46. package/dist/{signal-C9v4akyJ.d.cts → signal-CRBJYQ6B.d.cts} +0 -8
  47. package/dist/{signal-C9v4akyJ.d.ts → signal-CRBJYQ6B.d.ts} +0 -8
  48. package/dist/ssr/index.cjs +23 -42
  49. package/dist/ssr/index.cjs.map +1 -1
  50. package/dist/ssr/index.js +23 -42
  51. package/dist/ssr/index.js.map +1 -1
  52. package/dist/tc39-compat.cjs +3 -6
  53. package/dist/tc39-compat.cjs.map +1 -1
  54. package/dist/tc39-compat.d.cts +1 -1
  55. package/dist/tc39-compat.d.ts +1 -1
  56. package/dist/tc39-compat.js +1 -4
  57. package/dist/tc39-compat.js.map +1 -1
  58. package/package.json +2 -2
  59. package/dist/chunk-3G7ET4O5.js.map +0 -1
  60. package/dist/chunk-6FW5E54W.cjs.map +0 -1
  61. package/dist/chunk-HLM5BZZQ.js +0 -35
  62. package/dist/chunk-HLM5BZZQ.js.map +0 -1
  63. package/dist/chunk-QLPCVK7C.cjs +0 -38
  64. package/dist/chunk-QLPCVK7C.cjs.map +0 -1
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ var alienSignals = require('alien-signals');
4
+
5
+ // src/reactive/dev.ts
6
+ function isDev() {
7
+ if (typeof process !== "undefined") {
8
+ return process.env?.NODE_ENV !== "production";
9
+ }
10
+ return typeof __FORMA_DEV__ !== "undefined" ? !!__FORMA_DEV__ : false;
11
+ }
12
+ var __DEV__ = isDev();
13
+ var _errorHandlers = /* @__PURE__ */ new Set();
14
+ function onError(handler) {
15
+ _errorHandlers.add(handler);
16
+ return () => {
17
+ _errorHandlers.delete(handler);
18
+ };
19
+ }
20
+ function reportError(error, source) {
21
+ for (const handler of _errorHandlers) {
22
+ try {
23
+ handler(error, source ? { source } : {});
24
+ } catch {
25
+ }
26
+ }
27
+ if (isDev()) {
28
+ console.error(`[forma] ${source ?? "Unknown"} error:`, error);
29
+ }
30
+ }
31
+ var signalNames = /* @__PURE__ */ new WeakMap();
32
+ function applySignalSet(s, v, equals) {
33
+ if (typeof v !== "function") {
34
+ if (!equals) {
35
+ s(v);
36
+ return;
37
+ }
38
+ const prevSub2 = alienSignals.setActiveSub(void 0);
39
+ const prev2 = s();
40
+ alienSignals.setActiveSub(prevSub2);
41
+ if (equals(prev2, v)) return;
42
+ s(v);
43
+ if (Object.is(prev2, v)) forceNotify(s);
44
+ return;
45
+ }
46
+ const prevSub = alienSignals.setActiveSub(void 0);
47
+ const prev = s();
48
+ alienSignals.setActiveSub(prevSub);
49
+ const next = v(prev);
50
+ if (equals && equals(prev, next)) return;
51
+ s(next);
52
+ if (equals && Object.is(prev, next)) forceNotify(s);
53
+ }
54
+ function forceNotify(s) {
55
+ alienSignals.trigger(() => {
56
+ s();
57
+ });
58
+ }
59
+ function createSignal(initialValue, options) {
60
+ const s = alienSignals.signal(initialValue);
61
+ const getter = s;
62
+ if (__DEV__ && options?.name) {
63
+ signalNames.set(getter, options.name);
64
+ }
65
+ const eq = options?.equals;
66
+ const setter = (v) => applySignalSet(s, v, eq);
67
+ return [getter, setter];
68
+ }
69
+ var ERR = /* @__PURE__ */ Symbol("formaComputedError");
70
+ function isErrBox(v) {
71
+ return typeof v === "object" && v !== null && ERR in v;
72
+ }
73
+ function createComputed(fn) {
74
+ let errored = false;
75
+ let error;
76
+ let lastGood;
77
+ const raw = alienSignals.computed(() => {
78
+ try {
79
+ const v = fn(lastGood);
80
+ errored = false;
81
+ error = void 0;
82
+ lastGood = v;
83
+ return v;
84
+ } catch (e) {
85
+ errored = true;
86
+ error = e;
87
+ reportError(e, "computed");
88
+ return { [ERR]: e };
89
+ }
90
+ });
91
+ const reader = () => {
92
+ const v = raw();
93
+ if (errored || isErrBox(v)) throw error;
94
+ return v;
95
+ };
96
+ Object.defineProperty(reader, "name", { value: raw.name, configurable: true });
97
+ return reader;
98
+ }
99
+
100
+ exports.__DEV__ = __DEV__;
101
+ exports.createComputed = createComputed;
102
+ exports.createSignal = createSignal;
103
+ exports.onError = onError;
104
+ exports.reportError = reportError;
105
+ //# sourceMappingURL=chunk-W7OUWVRA.cjs.map
106
+ //# sourceMappingURL=chunk-W7OUWVRA.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reactive/dev.ts","../src/reactive/signal.ts","../src/reactive/computed.ts"],"names":["prevSub","setActiveSub","prev","trigger","createRawSignal","rawComputed"],"mappings":"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","file":"chunk-W7OUWVRA.cjs","sourcesContent":["/**\n * Forma Reactive - Dev Mode\n *\n * Development utilities stripped from production builds via __DEV__ flag.\n * Bundlers replace __DEV__ with false → dead-code elimination removes all dev paths.\n */\n\n// __DEV__ is replaced by the bundler (tsup define) for dead-code elimination in\n// dist. For unbundled/raw-source usage it derives from NODE_ENV, and — when the\n// environment is indeterminate — defaults to *false* (production-safe: no console\n// noise) unless a consumer opts in via a global __FORMA_DEV__.\ndeclare const process: { env?: Record<string, string | undefined> } | undefined;\ndeclare const __FORMA_DEV__: boolean | undefined;\n\n/**\n * Lazily evaluate whether we're in a dev environment. Read per-call (not frozen\n * at import) so runtime NODE_ENV changes are honored and so official prod dist\n * builds — where tsup hard-defines __DEV__ = false — stay quiet regardless.\n */\nfunction isDev(): boolean {\n if (typeof process !== 'undefined') {\n return process!.env?.NODE_ENV !== 'production';\n }\n return typeof __FORMA_DEV__ !== 'undefined' ? !!__FORMA_DEV__ : false;\n}\n\nexport const __DEV__: boolean = isDev();\n\n// ---------------------------------------------------------------------------\n// Global error handlers\n// ---------------------------------------------------------------------------\n\n/** Callback signature for a global {@link onError} handler. */\nexport type ErrorHandler = (error: unknown, info?: { source?: string }) => void;\n\nconst _errorHandlers = new Set<ErrorHandler>();\n\n/**\n * Install a global error handler for FormaJS reactive errors. Called when\n * effects, computeds, or event handlers throw. Multiple handlers may be\n * registered; returns an unsubscribe function that removes only this handler.\n *\n * ```ts\n * const off = onError((err, info) => {\n * Sentry.captureException(err, { tags: { source: info?.source } });\n * });\n * // later: off();\n * ```\n */\nexport function onError(handler: ErrorHandler): () => void {\n _errorHandlers.add(handler);\n return () => { _errorHandlers.delete(handler); };\n}\n\n/** @internal */\nexport function reportError(error: unknown, source?: string): void {\n for (const handler of _errorHandlers) {\n try { handler(error, source ? { source } : {}); } catch { /* prevent infinite loop */ }\n }\n if (isDev()) {\n console.error(`[forma] ${source ?? 'Unknown'} error:`, error);\n }\n}\n","/**\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, setActiveSub, trigger } from 'alien-signals';\nimport { __DEV__ } from './dev.js';\n\n// Debug names live in a side table rather than on the getter's `.name`, so\n// alien-signals' isSignal() (which keys off the bound function name) still\n// recognizes named signals.\nconst signalNames = new WeakMap<object, string>();\n\n/** Debug name of a signal getter, if one was provided (dev-only). */\nexport function getSignalName(fn: unknown): string | undefined {\n return typeof fn === 'function' ? signalNames.get(fn as object) : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** A function that reads the current value of a signal. */\nexport type SignalGetter<T> = () => T;\n/** A function that updates a signal — accepts a value or an updater function. */\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 /**\n * Custom equality check. When provided, the setter will read the current\n * value and only update the signal if `equals(prev, next)` returns `false`.\n *\n * Default: none (alien-signals uses strict inequality `!==` internally).\n *\n * ```ts\n * const [pos, setPos] = createSignal(\n * { x: 0, y: 0 },\n * { equals: (a, b) => a.x === b.x && a.y === b.y },\n * );\n *\n * setPos({ x: 0, y: 0 }); // skipped — equals returns true\n * setPos({ x: 1, y: 0 }); // applied — equals returns false\n * ```\n */\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 equals?: (prev: T, next: T) => boolean,\n): void {\n // Fast path: plain value, no equals — no prev read needed. (A running effect\n // writing its own dependency is detected at the effect level via the Pending\n // flag, so signal.ts does not need to do anything special here.)\n if (typeof v !== 'function') {\n if (!equals) {\n s(v);\n return;\n }\n const prevSub = setActiveSub(undefined);\n const prev = s();\n setActiveSub(prevSub);\n if (equals(prev, v)) return; // suppressed by custom equality\n s(v);\n if (Object.is(prev, v)) forceNotify(s);\n return;\n }\n\n // Functional update: read prev without tracking.\n const prevSub = setActiveSub(undefined);\n const prev = s();\n setActiveSub(prevSub);\n const next = (v as (prev: T) => T)(prev);\n if (equals && equals(prev, next)) return; // suppressed by custom equality\n s(next);\n if (equals && Object.is(prev, next)) forceNotify(s);\n}\n\n/**\n * When a custom `equals` decided to APPLY (returned false) but the value is the\n * identical reference, alien-signals suppresses propagation (its guard is\n * `pendingValue !== value`). Force-notify subscribers so `equals: () => false`\n * (\"always notify\") works for mutate-in-place patterns.\n */\nfunction forceNotify<T>(s: RawSignal<T>): void {\n trigger(() => { s(); });\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 *\n * With custom equality:\n *\n * ```ts\n * const [pos, setPos] = createSignal(\n * { x: 0, y: 0 },\n * { equals: (a, b) => a.x === b.x && a.y === b.y },\n * );\n * ```\n */\nexport function createSignal<T>(initialValue: T, options?: SignalOptions<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 if (__DEV__ && options?.name) {\n signalNames.set(getter as object, options.name);\n }\n const eq = options?.equals;\n const setter: SignalSetter<T> = (v: T | ((prev: T) => T)) => applySignalSet(s, v, eq);\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';\nimport { reportError } from './dev.js';\n\n/** Unique per-throw marker so alien-signals sees a value change and propagates. */\nconst ERR = Symbol('formaComputedError');\ninterface ErrBox { [ERR]: unknown; }\nfunction isErrBox(v: unknown): v is ErrBox {\n return typeof v === 'object' && v !== null && ERR in (v as object);\n}\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 * The getter receives the previous value as an argument, enabling\n * efficient diffing patterns without a separate signal:\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 *\n * With previous value (for diffing):\n *\n * ```ts\n * const changes = createComputed((prev) => {\n * const next = items();\n * if (prev) console.log(`changed from ${prev.length} to ${next.length} items`);\n * return next;\n * });\n * ```\n */\nexport function createComputed<T>(fn: (previousValue?: T) => T): () => T {\n // alien-signals' updateComputed leaves a throwing computed flagged clean with a\n // stale value, so a later read silently returns the stale value and never\n // rethrows. We cache the error in closure and rethrow on read until a\n // dependency changes (TC39/Solid). On throw we return a FRESH sentinel object so\n // alien-signals sees a value change and propagates the error to subscribers; the\n // user getter only ever sees the last GOOD value as `previousValue`.\n let errored = false;\n let error: unknown;\n let lastGood: T | undefined;\n\n const raw = rawComputed<T | ErrBox>(() => {\n try {\n const v = fn(lastGood);\n errored = false;\n error = undefined;\n lastGood = v;\n return v;\n } catch (e) {\n errored = true;\n error = e;\n reportError(e, 'computed');\n return { [ERR]: e };\n }\n });\n\n const reader = () => {\n const v = raw();\n if (errored || isErrBox(v)) throw error;\n return v as T;\n };\n // Preserve alien-signals' isComputed() recognition, which keys off the bound\n // function name (`bound computedOper`). Our reader wraps the raw computed, so\n // copy the raw name onto it.\n Object.defineProperty(reader, 'name', { value: raw.name, configurable: true });\n return reader;\n}\n"]}
@@ -0,0 +1,100 @@
1
+ import { signal, computed, setActiveSub, trigger } from 'alien-signals';
2
+
3
+ // src/reactive/dev.ts
4
+ function isDev() {
5
+ if (typeof process !== "undefined") {
6
+ return process.env?.NODE_ENV !== "production";
7
+ }
8
+ return typeof __FORMA_DEV__ !== "undefined" ? !!__FORMA_DEV__ : false;
9
+ }
10
+ var __DEV__ = isDev();
11
+ var _errorHandlers = /* @__PURE__ */ new Set();
12
+ function onError(handler) {
13
+ _errorHandlers.add(handler);
14
+ return () => {
15
+ _errorHandlers.delete(handler);
16
+ };
17
+ }
18
+ function reportError(error, source) {
19
+ for (const handler of _errorHandlers) {
20
+ try {
21
+ handler(error, source ? { source } : {});
22
+ } catch {
23
+ }
24
+ }
25
+ if (isDev()) {
26
+ console.error(`[forma] ${source ?? "Unknown"} error:`, error);
27
+ }
28
+ }
29
+ var signalNames = /* @__PURE__ */ new WeakMap();
30
+ function applySignalSet(s, v, equals) {
31
+ if (typeof v !== "function") {
32
+ if (!equals) {
33
+ s(v);
34
+ return;
35
+ }
36
+ const prevSub2 = setActiveSub(void 0);
37
+ const prev2 = s();
38
+ setActiveSub(prevSub2);
39
+ if (equals(prev2, v)) return;
40
+ s(v);
41
+ if (Object.is(prev2, v)) forceNotify(s);
42
+ return;
43
+ }
44
+ const prevSub = setActiveSub(void 0);
45
+ const prev = s();
46
+ setActiveSub(prevSub);
47
+ const next = v(prev);
48
+ if (equals && equals(prev, next)) return;
49
+ s(next);
50
+ if (equals && Object.is(prev, next)) forceNotify(s);
51
+ }
52
+ function forceNotify(s) {
53
+ trigger(() => {
54
+ s();
55
+ });
56
+ }
57
+ function createSignal(initialValue, options) {
58
+ const s = signal(initialValue);
59
+ const getter = s;
60
+ if (__DEV__ && options?.name) {
61
+ signalNames.set(getter, options.name);
62
+ }
63
+ const eq = options?.equals;
64
+ const setter = (v) => applySignalSet(s, v, eq);
65
+ return [getter, setter];
66
+ }
67
+ var ERR = /* @__PURE__ */ Symbol("formaComputedError");
68
+ function isErrBox(v) {
69
+ return typeof v === "object" && v !== null && ERR in v;
70
+ }
71
+ function createComputed(fn) {
72
+ let errored = false;
73
+ let error;
74
+ let lastGood;
75
+ const raw = computed(() => {
76
+ try {
77
+ const v = fn(lastGood);
78
+ errored = false;
79
+ error = void 0;
80
+ lastGood = v;
81
+ return v;
82
+ } catch (e) {
83
+ errored = true;
84
+ error = e;
85
+ reportError(e, "computed");
86
+ return { [ERR]: e };
87
+ }
88
+ });
89
+ const reader = () => {
90
+ const v = raw();
91
+ if (errored || isErrBox(v)) throw error;
92
+ return v;
93
+ };
94
+ Object.defineProperty(reader, "name", { value: raw.name, configurable: true });
95
+ return reader;
96
+ }
97
+
98
+ export { __DEV__, createComputed, createSignal, onError, reportError };
99
+ //# sourceMappingURL=chunk-XLVBYXOU.js.map
100
+ //# sourceMappingURL=chunk-XLVBYXOU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reactive/dev.ts","../src/reactive/signal.ts","../src/reactive/computed.ts"],"names":["prevSub","prev","createRawSignal","rawComputed"],"mappings":";;;AAmBA,SAAS,KAAA,GAAiB;AACxB,EAAA,IAAI,OAAO,YAAY,WAAA,EAAa;AAClC,IAAA,OAAO,OAAA,CAAS,KAAK,QAAA,KAAa,YAAA;AAAA,EACpC;AACA,EAAA,OAAO,OAAO,aAAA,KAAkB,WAAA,GAAc,CAAC,CAAC,aAAA,GAAgB,KAAA;AAClE;AAEO,IAAM,UAAmB,KAAA;AAShC,IAAM,cAAA,uBAAqB,GAAA,EAAkB;AActC,SAAS,QAAQ,OAAA,EAAmC;AACzD,EAAA,cAAA,CAAe,IAAI,OAAO,CAAA;AAC1B,EAAA,OAAO,MAAM;AAAE,IAAA,cAAA,CAAe,OAAO,OAAO,CAAA;AAAA,EAAG,CAAA;AACjD;AAGO,SAAS,WAAA,CAAY,OAAgB,MAAA,EAAuB;AACjE,EAAA,KAAA,MAAW,WAAW,cAAA,EAAgB;AACpC,IAAA,IAAI;AAAE,MAAA,OAAA,CAAQ,OAAO,MAAA,GAAS,EAAE,MAAA,EAAO,GAAI,EAAE,CAAA;AAAA,IAAG,CAAA,CAAA,MAAQ;AAAA,IAA8B;AAAA,EACxF;AACA,EAAA,IAAI,OAAM,EAAG;AACX,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,QAAA,EAAW,MAAA,IAAU,SAAS,WAAW,KAAK,CAAA;AAAA,EAC9D;AACF;AC/CA,IAAM,WAAA,uBAAkB,OAAA,EAAwB;AAgEhD,SAAS,cAAA,CACP,CAAA,EACA,CAAA,EACA,MAAA,EACM;AAIN,EAAA,IAAI,OAAO,MAAM,UAAA,EAAY;AAC3B,IAAA,IAAI,CAAC,MAAA,EAAQ;AACX,MAAA,CAAA,CAAE,CAAC,CAAA;AACH,MAAA;AAAA,IACF;AACA,IAAA,MAAMA,QAAAA,GAAU,aAAa,MAAS,CAAA;AACtC,IAAA,MAAMC,QAAO,CAAA,EAAE;AACf,IAAA,YAAA,CAAaD,QAAO,CAAA;AACpB,IAAA,IAAI,MAAA,CAAOC,KAAAA,EAAM,CAAC,CAAA,EAAG;AACrB,IAAA,CAAA,CAAE,CAAC,CAAA;AACH,IAAA,IAAI,OAAO,EAAA,CAAGA,KAAAA,EAAM,CAAC,CAAA,cAAe,CAAC,CAAA;AACrC,IAAA;AAAA,EACF;AAGA,EAAA,MAAM,OAAA,GAAU,aAAa,MAAS,CAAA;AACtC,EAAA,MAAM,OAAO,CAAA,EAAE;AACf,EAAA,YAAA,CAAa,OAAO,CAAA;AACpB,EAAA,MAAM,IAAA,GAAQ,EAAqB,IAAI,CAAA;AACvC,EAAA,IAAI,MAAA,IAAU,MAAA,CAAO,IAAA,EAAM,IAAI,CAAA,EAAG;AAClC,EAAA,CAAA,CAAE,IAAI,CAAA;AACN,EAAA,IAAI,UAAU,MAAA,CAAO,EAAA,CAAG,MAAM,IAAI,CAAA,cAAe,CAAC,CAAA;AACpD;AAQA,SAAS,YAAe,CAAA,EAAuB;AAC7C,EAAA,OAAA,CAAQ,MAAM;AAAE,IAAA,CAAA,EAAE;AAAA,EAAG,CAAC,CAAA;AACxB;AAqBO,SAAS,YAAA,CAAgB,cAAiB,OAAA,EAA0E;AACzH,EAAA,MAAM,CAAA,GAAIC,OAAmB,YAAY,CAAA;AACzC,EAAA,MAAM,MAAA,GAAS,CAAA;AACf,EAAA,IAAI,OAAA,IAAW,SAAS,IAAA,EAAM;AAC5B,IAAA,WAAA,CAAY,GAAA,CAAI,MAAA,EAAkB,OAAA,CAAQ,IAAI,CAAA;AAAA,EAChD;AACA,EAAA,MAAM,KAAK,OAAA,EAAS,MAAA;AACpB,EAAA,MAAM,SAA0B,CAAC,CAAA,KAA4B,cAAA,CAAe,CAAA,EAAG,GAAG,EAAE,CAAA;AAEpF,EAAA,OAAO,CAAC,QAAQ,MAAM,CAAA;AACxB;ACxIA,IAAM,GAAA,0BAAa,oBAAoB,CAAA;AAEvC,SAAS,SAAS,CAAA,EAAyB;AACzC,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,QAAQ,GAAA,IAAQ,CAAA;AACxD;AA+BO,SAAS,eAAkB,EAAA,EAAuC;AAOvE,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,QAAA;AAEJ,EAAA,MAAM,GAAA,GAAMC,SAAwB,MAAM;AACxC,IAAA,IAAI;AACF,MAAA,MAAM,CAAA,GAAI,GAAG,QAAQ,CAAA;AACrB,MAAA,OAAA,GAAU,KAAA;AACV,MAAA,KAAA,GAAQ,KAAA,CAAA;AACR,MAAA,QAAA,GAAW,CAAA;AACX,MAAA,OAAO,CAAA;AAAA,IACT,SAAS,CAAA,EAAG;AACV,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,KAAA,GAAQ,CAAA;AACR,MAAA,WAAA,CAAY,GAAG,UAAU,CAAA;AACzB,MAAA,OAAO,EAAE,CAAC,GAAG,GAAG,CAAA,EAAE;AAAA,IACpB;AAAA,EACF,CAAC,CAAA;AAED,EAAA,MAAM,SAAS,MAAM;AACnB,IAAA,MAAM,IAAI,GAAA,EAAI;AACd,IAAA,IAAI,OAAA,IAAW,QAAA,CAAS,CAAC,CAAA,EAAG,MAAM,KAAA;AAClC,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AAIA,EAAA,MAAA,CAAO,cAAA,CAAe,QAAQ,MAAA,EAAQ,EAAE,OAAO,GAAA,CAAI,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,CAAA;AAC7E,EAAA,OAAO,MAAA;AACT","file":"chunk-XLVBYXOU.js","sourcesContent":["/**\n * Forma Reactive - Dev Mode\n *\n * Development utilities stripped from production builds via __DEV__ flag.\n * Bundlers replace __DEV__ with false → dead-code elimination removes all dev paths.\n */\n\n// __DEV__ is replaced by the bundler (tsup define) for dead-code elimination in\n// dist. For unbundled/raw-source usage it derives from NODE_ENV, and — when the\n// environment is indeterminate — defaults to *false* (production-safe: no console\n// noise) unless a consumer opts in via a global __FORMA_DEV__.\ndeclare const process: { env?: Record<string, string | undefined> } | undefined;\ndeclare const __FORMA_DEV__: boolean | undefined;\n\n/**\n * Lazily evaluate whether we're in a dev environment. Read per-call (not frozen\n * at import) so runtime NODE_ENV changes are honored and so official prod dist\n * builds — where tsup hard-defines __DEV__ = false — stay quiet regardless.\n */\nfunction isDev(): boolean {\n if (typeof process !== 'undefined') {\n return process!.env?.NODE_ENV !== 'production';\n }\n return typeof __FORMA_DEV__ !== 'undefined' ? !!__FORMA_DEV__ : false;\n}\n\nexport const __DEV__: boolean = isDev();\n\n// ---------------------------------------------------------------------------\n// Global error handlers\n// ---------------------------------------------------------------------------\n\n/** Callback signature for a global {@link onError} handler. */\nexport type ErrorHandler = (error: unknown, info?: { source?: string }) => void;\n\nconst _errorHandlers = new Set<ErrorHandler>();\n\n/**\n * Install a global error handler for FormaJS reactive errors. Called when\n * effects, computeds, or event handlers throw. Multiple handlers may be\n * registered; returns an unsubscribe function that removes only this handler.\n *\n * ```ts\n * const off = onError((err, info) => {\n * Sentry.captureException(err, { tags: { source: info?.source } });\n * });\n * // later: off();\n * ```\n */\nexport function onError(handler: ErrorHandler): () => void {\n _errorHandlers.add(handler);\n return () => { _errorHandlers.delete(handler); };\n}\n\n/** @internal */\nexport function reportError(error: unknown, source?: string): void {\n for (const handler of _errorHandlers) {\n try { handler(error, source ? { source } : {}); } catch { /* prevent infinite loop */ }\n }\n if (isDev()) {\n console.error(`[forma] ${source ?? 'Unknown'} error:`, error);\n }\n}\n","/**\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, setActiveSub, trigger } from 'alien-signals';\nimport { __DEV__ } from './dev.js';\n\n// Debug names live in a side table rather than on the getter's `.name`, so\n// alien-signals' isSignal() (which keys off the bound function name) still\n// recognizes named signals.\nconst signalNames = new WeakMap<object, string>();\n\n/** Debug name of a signal getter, if one was provided (dev-only). */\nexport function getSignalName(fn: unknown): string | undefined {\n return typeof fn === 'function' ? signalNames.get(fn as object) : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** A function that reads the current value of a signal. */\nexport type SignalGetter<T> = () => T;\n/** A function that updates a signal — accepts a value or an updater function. */\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 /**\n * Custom equality check. When provided, the setter will read the current\n * value and only update the signal if `equals(prev, next)` returns `false`.\n *\n * Default: none (alien-signals uses strict inequality `!==` internally).\n *\n * ```ts\n * const [pos, setPos] = createSignal(\n * { x: 0, y: 0 },\n * { equals: (a, b) => a.x === b.x && a.y === b.y },\n * );\n *\n * setPos({ x: 0, y: 0 }); // skipped — equals returns true\n * setPos({ x: 1, y: 0 }); // applied — equals returns false\n * ```\n */\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 equals?: (prev: T, next: T) => boolean,\n): void {\n // Fast path: plain value, no equals — no prev read needed. (A running effect\n // writing its own dependency is detected at the effect level via the Pending\n // flag, so signal.ts does not need to do anything special here.)\n if (typeof v !== 'function') {\n if (!equals) {\n s(v);\n return;\n }\n const prevSub = setActiveSub(undefined);\n const prev = s();\n setActiveSub(prevSub);\n if (equals(prev, v)) return; // suppressed by custom equality\n s(v);\n if (Object.is(prev, v)) forceNotify(s);\n return;\n }\n\n // Functional update: read prev without tracking.\n const prevSub = setActiveSub(undefined);\n const prev = s();\n setActiveSub(prevSub);\n const next = (v as (prev: T) => T)(prev);\n if (equals && equals(prev, next)) return; // suppressed by custom equality\n s(next);\n if (equals && Object.is(prev, next)) forceNotify(s);\n}\n\n/**\n * When a custom `equals` decided to APPLY (returned false) but the value is the\n * identical reference, alien-signals suppresses propagation (its guard is\n * `pendingValue !== value`). Force-notify subscribers so `equals: () => false`\n * (\"always notify\") works for mutate-in-place patterns.\n */\nfunction forceNotify<T>(s: RawSignal<T>): void {\n trigger(() => { s(); });\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 *\n * With custom equality:\n *\n * ```ts\n * const [pos, setPos] = createSignal(\n * { x: 0, y: 0 },\n * { equals: (a, b) => a.x === b.x && a.y === b.y },\n * );\n * ```\n */\nexport function createSignal<T>(initialValue: T, options?: SignalOptions<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 if (__DEV__ && options?.name) {\n signalNames.set(getter as object, options.name);\n }\n const eq = options?.equals;\n const setter: SignalSetter<T> = (v: T | ((prev: T) => T)) => applySignalSet(s, v, eq);\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';\nimport { reportError } from './dev.js';\n\n/** Unique per-throw marker so alien-signals sees a value change and propagates. */\nconst ERR = Symbol('formaComputedError');\ninterface ErrBox { [ERR]: unknown; }\nfunction isErrBox(v: unknown): v is ErrBox {\n return typeof v === 'object' && v !== null && ERR in (v as object);\n}\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 * The getter receives the previous value as an argument, enabling\n * efficient diffing patterns without a separate signal:\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 *\n * With previous value (for diffing):\n *\n * ```ts\n * const changes = createComputed((prev) => {\n * const next = items();\n * if (prev) console.log(`changed from ${prev.length} to ${next.length} items`);\n * return next;\n * });\n * ```\n */\nexport function createComputed<T>(fn: (previousValue?: T) => T): () => T {\n // alien-signals' updateComputed leaves a throwing computed flagged clean with a\n // stale value, so a later read silently returns the stale value and never\n // rethrows. We cache the error in closure and rethrow on read until a\n // dependency changes (TC39/Solid). On throw we return a FRESH sentinel object so\n // alien-signals sees a value change and propagates the error to subscribers; the\n // user getter only ever sees the last GOOD value as `previousValue`.\n let errored = false;\n let error: unknown;\n let lastGood: T | undefined;\n\n const raw = rawComputed<T | ErrBox>(() => {\n try {\n const v = fn(lastGood);\n errored = false;\n error = undefined;\n lastGood = v;\n return v;\n } catch (e) {\n errored = true;\n error = e;\n reportError(e, 'computed');\n return { [ERR]: e };\n }\n });\n\n const reader = () => {\n const v = raw();\n if (errored || isErrBox(v)) throw error;\n return v as T;\n };\n // Preserve alien-signals' isComputed() recognition, which keys off the bound\n // function name (`bound computedOper`). Our reader wraps the raw computed, so\n // copy the raw name onto it.\n Object.defineProperty(reader, 'name', { value: raw.name, configurable: true });\n return reader;\n}\n"]}
@@ -438,11 +438,32 @@ var FormaRuntime = (function (exports) {
438
438
  }
439
439
  }
440
440
 
441
- // src/reactive/signal.ts
441
+ // src/reactive/dev.ts
442
+ function isDev() {
443
+ if (typeof process !== "undefined") {
444
+ return process.env?.NODE_ENV !== "production";
445
+ }
446
+ return typeof __FORMA_DEV__ !== "undefined" ? !!__FORMA_DEV__ : false;
447
+ }
448
+ isDev();
449
+ var _errorHandlers = /* @__PURE__ */ new Set();
450
+ function reportError(error, source) {
451
+ for (const handler of _errorHandlers) {
452
+ try {
453
+ handler(error, source ? { source } : {});
454
+ } catch {
455
+ }
456
+ }
457
+ if (isDev()) {
458
+ console.error(`[forma] ${source} error:`, error);
459
+ }
460
+ }
442
461
  function applySignalSet(s, v, equals) {
443
462
  if (typeof v !== "function") {
444
- s(v);
445
- return;
463
+ {
464
+ s(v);
465
+ return;
466
+ }
446
467
  }
447
468
  const prevSub = setActiveSub(void 0);
448
469
  const prev = s();
@@ -462,8 +483,35 @@ var FormaRuntime = (function (exports) {
462
483
  }
463
484
 
464
485
  // src/reactive/computed.ts
486
+ var ERR = /* @__PURE__ */ Symbol("formaComputedError");
487
+ function isErrBox(v) {
488
+ return typeof v === "object" && v !== null && ERR in v;
489
+ }
465
490
  function createComputed(fn) {
466
- return computed(fn);
491
+ let errored = false;
492
+ let error;
493
+ let lastGood;
494
+ const raw = computed(() => {
495
+ try {
496
+ const v = fn(lastGood);
497
+ errored = false;
498
+ error = void 0;
499
+ lastGood = v;
500
+ return v;
501
+ } catch (e) {
502
+ errored = true;
503
+ error = e;
504
+ reportError(e, "computed");
505
+ return { [ERR]: e };
506
+ }
507
+ });
508
+ const reader = () => {
509
+ const v = raw();
510
+ if (errored || isErrBox(v)) throw error;
511
+ return v;
512
+ };
513
+ Object.defineProperty(reader, "name", { value: raw.name, configurable: true });
514
+ return reader;
467
515
  }
468
516
 
469
517
  // src/reactive/batch.ts
@@ -1047,7 +1095,35 @@ var FormaRuntime = (function (exports) {
1047
1095
  };
1048
1096
  }
1049
1097
 
1098
+ // src/security/url-safety.ts
1099
+ var URL_IGNORED_CHARS_RE = /[\u0000-\u0020\u007F-\u009F]/g;
1100
+ var DANGEROUS_SCHEME_RE = /^(?:javascript|vbscript|data:text\/html)/i;
1101
+ var URL_ATTRS = /* @__PURE__ */ new Set([
1102
+ "href",
1103
+ "src",
1104
+ "action",
1105
+ "formaction",
1106
+ "xlink:href",
1107
+ "poster",
1108
+ "background"
1109
+ ]);
1110
+ function isUrlAttr(name) {
1111
+ return URL_ATTRS.has(name.toLowerCase());
1112
+ }
1113
+ function isDangerousUrl(value2) {
1114
+ const normalized = value2.replace(URL_IGNORED_CHARS_RE, "");
1115
+ return DANGEROUS_SCHEME_RE.test(normalized);
1116
+ }
1117
+ function isEventHandlerAttr(name) {
1118
+ return /^on/i.test(name);
1119
+ }
1120
+
1050
1121
  // src/runtime.ts
1122
+ function isUnsafeAttrBinding(name, value2) {
1123
+ if (isEventHandlerAttr(name)) return true;
1124
+ if (isUrlAttr(name) && isDangerousUrl(value2)) return true;
1125
+ return false;
1126
+ }
1051
1127
  var _refetchRegistry = /* @__PURE__ */ new Map();
1052
1128
  function $refetch(id) {
1053
1129
  const fn = _refetchRegistry.get(id);
@@ -2012,7 +2088,12 @@ var FormaRuntime = (function (exports) {
2012
2088
  if (attr.value.includes("{item")) {
2013
2089
  const compiled = compileTemplate(attr.value);
2014
2090
  entries.push({ attr: attr.name, compiled });
2015
- node.setAttribute(attr.name, evaluateCompiledTemplate(compiled, item));
2091
+ const value2 = evaluateCompiledTemplate(compiled, item);
2092
+ if (isUnsafeAttrBinding(attr.name, value2)) {
2093
+ node.removeAttribute(attr.name);
2094
+ } else {
2095
+ node.setAttribute(attr.name, value2);
2096
+ }
2016
2097
  }
2017
2098
  }
2018
2099
  if (entries.length > 0) {
@@ -2784,7 +2865,12 @@ var FormaRuntime = (function (exports) {
2784
2865
  if (val == null || val === false) {
2785
2866
  el.removeAttribute(attrName);
2786
2867
  } else {
2787
- el.setAttribute(attrName, String(val));
2868
+ const str = String(val);
2869
+ if (isUnsafeAttrBinding(attrName, str)) {
2870
+ el.removeAttribute(attrName);
2871
+ } else {
2872
+ el.setAttribute(attrName, str);
2873
+ }
2788
2874
  }
2789
2875
  });
2790
2876
  disposers.push(dispose);
@@ -348,6 +348,33 @@ var FormaRuntime = (() => {
348
348
  }
349
349
  return effectOper.bind(e);
350
350
  }
351
+ function trigger(fn) {
352
+ const sub = {
353
+ deps: void 0,
354
+ depsTail: void 0,
355
+ flags: 2
356
+ };
357
+ const prevSub = setActiveSub(sub);
358
+ try {
359
+ fn();
360
+ } finally {
361
+ activeSub = prevSub;
362
+ let link2 = sub.deps;
363
+ while (link2 !== void 0) {
364
+ const dep = link2.dep;
365
+ link2 = unlink(link2, sub);
366
+ const subs = dep.subs;
367
+ if (subs !== void 0) {
368
+ sub.flags = 0;
369
+ propagate(subs);
370
+ shallowPropagate(subs);
371
+ }
372
+ }
373
+ if (!batchDepth) {
374
+ flush();
375
+ }
376
+ }
377
+ }
351
378
  function updateComputed(c) {
352
379
  ++cycle;
353
380
  c.depsTail = void 0;
@@ -478,16 +505,41 @@ var FormaRuntime = (() => {
478
505
  }
479
506
  }
480
507
 
508
+ // src/reactive/dev.ts
509
+ function isDev() {
510
+ if (typeof process !== "undefined") {
511
+ return process.env?.NODE_ENV !== "production";
512
+ }
513
+ return typeof __FORMA_DEV__ !== "undefined" ? !!__FORMA_DEV__ : false;
514
+ }
515
+ var __DEV__ = isDev();
516
+ var _errorHandlers = /* @__PURE__ */ new Set();
517
+ function reportError(error, source) {
518
+ for (const handler of _errorHandlers) {
519
+ try {
520
+ handler(error, source ? { source } : {});
521
+ } catch {
522
+ }
523
+ }
524
+ if (isDev()) {
525
+ console.error(`[forma] ${source ?? "Unknown"} error:`, error);
526
+ }
527
+ }
528
+
481
529
  // src/reactive/signal.ts
530
+ var signalNames = /* @__PURE__ */ new WeakMap();
482
531
  function applySignalSet(s, v, equals) {
483
532
  if (typeof v !== "function") {
484
- if (equals) {
485
- const prevSub2 = setActiveSub(void 0);
486
- const prev2 = s();
487
- setActiveSub(prevSub2);
488
- if (equals(prev2, v)) return;
533
+ if (!equals) {
534
+ s(v);
535
+ return;
489
536
  }
537
+ const prevSub2 = setActiveSub(void 0);
538
+ const prev2 = s();
539
+ setActiveSub(prevSub2);
540
+ if (equals(prev2, v)) return;
490
541
  s(v);
542
+ if (Object.is(prev2, v)) forceNotify(s);
491
543
  return;
492
544
  }
493
545
  const prevSub = setActiveSub(void 0);
@@ -496,24 +548,33 @@ var FormaRuntime = (() => {
496
548
  const next = v(prev);
497
549
  if (equals && equals(prev, next)) return;
498
550
  s(next);
551
+ if (equals && Object.is(prev, next)) forceNotify(s);
552
+ }
553
+ function forceNotify(s) {
554
+ trigger(() => {
555
+ s();
556
+ });
499
557
  }
500
558
  function createSignal(initialValue, options) {
501
559
  const s = signal(initialValue);
502
560
  const getter = s;
561
+ if (__DEV__ && options?.name) {
562
+ signalNames.set(getter, options.name);
563
+ }
503
564
  const eq = options?.equals;
504
565
  const setter = (v) => applySignalSet(s, v, eq);
505
566
  return [getter, setter];
506
567
  }
507
568
 
508
569
  // src/reactive/root.ts
509
- var currentRoot = null;
570
+ var currentOwner = null;
510
571
  function registerDisposer(dispose) {
511
- if (currentRoot) {
512
- currentRoot.disposers.push(dispose);
572
+ if (currentOwner) {
573
+ currentOwner.disposers.push(dispose);
513
574
  }
514
575
  }
515
576
  function hasActiveRoot() {
516
- return currentRoot !== null;
577
+ return currentOwner !== null;
517
578
  }
518
579
 
519
580
  // src/reactive/effect.ts
@@ -529,8 +590,35 @@ var FormaRuntime = (() => {
529
590
  }
530
591
 
531
592
  // src/reactive/computed.ts
593
+ var ERR = /* @__PURE__ */ Symbol("formaComputedError");
594
+ function isErrBox(v) {
595
+ return typeof v === "object" && v !== null && ERR in v;
596
+ }
532
597
  function createComputed(fn) {
533
- return computed(fn);
598
+ let errored = false;
599
+ let error;
600
+ let lastGood;
601
+ const raw = computed(() => {
602
+ try {
603
+ const v = fn(lastGood);
604
+ errored = false;
605
+ error = void 0;
606
+ lastGood = v;
607
+ return v;
608
+ } catch (e) {
609
+ errored = true;
610
+ error = e;
611
+ reportError(e, "computed");
612
+ return { [ERR]: e };
613
+ }
614
+ });
615
+ const reader = () => {
616
+ const v = raw();
617
+ if (errored || isErrBox(v)) throw error;
618
+ return v;
619
+ };
620
+ Object.defineProperty(reader, "name", { value: raw.name, configurable: true });
621
+ return reader;
534
622
  }
535
623
 
536
624
  // src/reactive/batch.ts
@@ -1116,7 +1204,35 @@ var FormaRuntime = (() => {
1116
1204
  };
1117
1205
  }
1118
1206
 
1207
+ // src/security/url-safety.ts
1208
+ var URL_IGNORED_CHARS_RE = /[\u0000-\u0020\u007F-\u009F]/g;
1209
+ var DANGEROUS_SCHEME_RE = /^(?:javascript|vbscript|data:text\/html)/i;
1210
+ var URL_ATTRS = /* @__PURE__ */ new Set([
1211
+ "href",
1212
+ "src",
1213
+ "action",
1214
+ "formaction",
1215
+ "xlink:href",
1216
+ "poster",
1217
+ "background"
1218
+ ]);
1219
+ function isUrlAttr(name) {
1220
+ return URL_ATTRS.has(name.toLowerCase());
1221
+ }
1222
+ function isDangerousUrl(value2) {
1223
+ const normalized = value2.replace(URL_IGNORED_CHARS_RE, "");
1224
+ return DANGEROUS_SCHEME_RE.test(normalized);
1225
+ }
1226
+ function isEventHandlerAttr(name) {
1227
+ return /^on/i.test(name);
1228
+ }
1229
+
1119
1230
  // src/runtime.ts
1231
+ function isUnsafeAttrBinding(name, value2) {
1232
+ if (isEventHandlerAttr(name)) return true;
1233
+ if (isUrlAttr(name) && isDangerousUrl(value2)) return true;
1234
+ return false;
1235
+ }
1120
1236
  var _refetchRegistry = /* @__PURE__ */ new Map();
1121
1237
  function $refetch(id) {
1122
1238
  const fn = _refetchRegistry.get(id);
@@ -2120,7 +2236,12 @@ var FormaRuntime = (() => {
2120
2236
  if (attr.value.includes("{item")) {
2121
2237
  const compiled = compileTemplate(attr.value);
2122
2238
  entries.push({ attr: attr.name, compiled });
2123
- node.setAttribute(attr.name, evaluateCompiledTemplate(compiled, item));
2239
+ const value2 = evaluateCompiledTemplate(compiled, item);
2240
+ if (isUnsafeAttrBinding(attr.name, value2)) {
2241
+ node.removeAttribute(attr.name);
2242
+ } else {
2243
+ node.setAttribute(attr.name, value2);
2244
+ }
2124
2245
  }
2125
2246
  }
2126
2247
  if (entries.length > 0) {
@@ -2962,7 +3083,12 @@ var FormaRuntime = (() => {
2962
3083
  if (val == null || val === false) {
2963
3084
  el.removeAttribute(attrName);
2964
3085
  } else {
2965
- el.setAttribute(attrName, String(val));
3086
+ const str = String(val);
3087
+ if (isUnsafeAttrBinding(attrName, str)) {
3088
+ el.removeAttribute(attrName);
3089
+ } else {
3090
+ el.setAttribute(attrName, str);
3091
+ }
2966
3092
  }
2967
3093
  });
2968
3094
  disposers.push(dispose);