@hyperfixi/reactivity 2.4.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,772 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ default: () => index_default,
24
+ reactive: () => reactive,
25
+ reactivityPlugin: () => reactivityPlugin
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/signals.ts
30
+ function debugEnabled() {
31
+ try {
32
+ return typeof localStorage !== "undefined" && localStorage.getItem("hyperfixi:debug") !== null;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ function debugWarn(message, err) {
38
+ if (!debugEnabled()) return;
39
+ if (typeof console === "undefined") return;
40
+ console.warn(`[@hyperfixi/reactivity] ${message}`, err);
41
+ }
42
+ var globalDepKey = (name) => `global:${name}`;
43
+ var elementDepKey = (el, name) => `element:${reactiveIdFor(el)}:${name}`;
44
+ var domDepKey = (el, prop) => `dom:${reactiveIdFor(el)}:${prop}`;
45
+ var _reactiveIds = /* @__PURE__ */ new WeakMap();
46
+ var _idCounter = 0;
47
+ function reactiveIdFor(el) {
48
+ const existing = _reactiveIds.get(el);
49
+ if (existing !== void 0) return existing;
50
+ const id = ++_idCounter;
51
+ _reactiveIds.set(el, id);
52
+ return id;
53
+ }
54
+ var Effect = class {
55
+ constructor(r, expression, handler, element) {
56
+ this.r = r;
57
+ this.expression = expression;
58
+ this.handler = handler;
59
+ this.element = element;
60
+ }
61
+ dependencies = /* @__PURE__ */ new Map();
62
+ lastValue = void 0;
63
+ _stopped = false;
64
+ _consecutiveTriggers = 0;
65
+ get stopped() {
66
+ return this._stopped;
67
+ }
68
+ /**
69
+ * Run the effect for the first time: collect dependencies, subscribe, call
70
+ * handler with the initial value. Errors in expression evaluation are caught
71
+ * and silently swallowed (matches upstream's tolerance).
72
+ */
73
+ async initialize() {
74
+ if (this._stopped) return;
75
+ try {
76
+ const value = await this.r._runWithEffect(this, this.expression);
77
+ this.lastValue = value;
78
+ if (value !== void 0 && value !== null) {
79
+ await this.handler(value);
80
+ }
81
+ } catch (err) {
82
+ debugWarn("effect.initialize failed", err);
83
+ }
84
+ }
85
+ /**
86
+ * Re-run the effect after a notify. Cycle-detects (halts at 101 consecutive
87
+ * triggers). If the new value is Object.is-equal to the last, the handler
88
+ * is skipped. If the owning element has disconnected, the effect stops
89
+ * itself and returns.
90
+ */
91
+ async run() {
92
+ if (this._stopped) return;
93
+ if (this.element && !this.element.isConnected) {
94
+ this.stop();
95
+ return;
96
+ }
97
+ this._consecutiveTriggers++;
98
+ if (this._consecutiveTriggers > 100) {
99
+ if (typeof console !== "undefined") {
100
+ console.error(
101
+ "[@hyperfixi/reactivity] Effect halted: > 100 consecutive triggers (cycle detected)."
102
+ );
103
+ }
104
+ this.stop();
105
+ return;
106
+ }
107
+ try {
108
+ const value = await this.r._runWithEffect(this, this.expression);
109
+ if (Object.is(value, this.lastValue)) return;
110
+ this.lastValue = value;
111
+ await this.handler(value);
112
+ } catch (err) {
113
+ debugWarn("effect.run failed", err);
114
+ }
115
+ }
116
+ resetTriggerCount() {
117
+ this._consecutiveTriggers = 0;
118
+ }
119
+ /**
120
+ * Stop the effect and remove it from all dependency subscriptions. Safe to
121
+ * call multiple times.
122
+ */
123
+ stop() {
124
+ if (this._stopped) return;
125
+ this._stopped = true;
126
+ this.r._unsubscribeEffect(this);
127
+ this.dependencies.clear();
128
+ }
129
+ };
130
+ var Reactive = class {
131
+ currentEffect = null;
132
+ pending = /* @__PURE__ */ new Set();
133
+ scheduled = false;
134
+ globalSubs = /* @__PURE__ */ new Map();
135
+ elementState = /* @__PURE__ */ new WeakMap();
136
+ getElementState(el) {
137
+ let s = this.elementState.get(el);
138
+ if (!s) {
139
+ s = {
140
+ symbolSubs: /* @__PURE__ */ new Map(),
141
+ caretVars: /* @__PURE__ */ new Map(),
142
+ domHandlers: /* @__PURE__ */ new Map(),
143
+ effects: /* @__PURE__ */ new Set()
144
+ };
145
+ this.elementState.set(el, s);
146
+ }
147
+ return s;
148
+ }
149
+ /**
150
+ * Internal helper invoked by Effect — installs `this` as the current effect,
151
+ * runs the expression, then restores the previous current-effect pointer.
152
+ * Track* methods (called from read-paths) consult `currentEffect` to know
153
+ * which effect to subscribe.
154
+ */
155
+ async _runWithEffect(e, fn) {
156
+ const prev = this.currentEffect;
157
+ this.currentEffect = e;
158
+ this._unsubscribeEffect(e);
159
+ e.dependencies.clear();
160
+ try {
161
+ return await fn();
162
+ } finally {
163
+ this.currentEffect = prev;
164
+ }
165
+ }
166
+ // ---------------------------------------------------------------------------
167
+ // Track (read-path) — invoked from interceptors / evaluators.
168
+ // ---------------------------------------------------------------------------
169
+ trackGlobal(name) {
170
+ const e = this.currentEffect;
171
+ if (!e) return;
172
+ const key = globalDepKey(name);
173
+ if (e.dependencies.has(key)) return;
174
+ e.dependencies.set(key, { key, kind: "global", name, element: null });
175
+ let subs = this.globalSubs.get(name);
176
+ if (!subs) {
177
+ subs = /* @__PURE__ */ new Set();
178
+ this.globalSubs.set(name, subs);
179
+ }
180
+ subs.add(e);
181
+ }
182
+ trackElement(el, name) {
183
+ const e = this.currentEffect;
184
+ if (!e) return;
185
+ const key = elementDepKey(el, name);
186
+ if (e.dependencies.has(key)) return;
187
+ e.dependencies.set(key, { key, kind: "element", name, element: el });
188
+ const state = this.getElementState(el);
189
+ let subs = state.symbolSubs.get(name);
190
+ if (!subs) {
191
+ subs = /* @__PURE__ */ new Set();
192
+ state.symbolSubs.set(name, subs);
193
+ }
194
+ subs.add(e);
195
+ state.effects.add(e);
196
+ }
197
+ trackDomProperty(el, prop) {
198
+ const e = this.currentEffect;
199
+ if (!e) return;
200
+ const key = domDepKey(el, prop);
201
+ if (e.dependencies.has(key)) return;
202
+ e.dependencies.set(key, { key, kind: "dom", name: prop, element: el });
203
+ const state = this.getElementState(el);
204
+ let handler = state.domHandlers.get(prop);
205
+ if (!handler) {
206
+ const subs = /* @__PURE__ */ new Set();
207
+ const listener = () => {
208
+ for (const effect of subs) this.schedule(effect);
209
+ };
210
+ el.addEventListener("input", listener);
211
+ el.addEventListener("change", listener);
212
+ handler = {
213
+ subs,
214
+ detach: () => {
215
+ el.removeEventListener("input", listener);
216
+ el.removeEventListener("change", listener);
217
+ }
218
+ };
219
+ state.domHandlers.set(prop, handler);
220
+ }
221
+ handler.subs.add(e);
222
+ state.effects.add(e);
223
+ }
224
+ // ---------------------------------------------------------------------------
225
+ // Notify (write-path) — schedules dependent effects for re-run.
226
+ // ---------------------------------------------------------------------------
227
+ notifyGlobal(name) {
228
+ const subs = this.globalSubs.get(name);
229
+ if (!subs) return;
230
+ for (const e of subs) this.schedule(e);
231
+ }
232
+ notifyElement(el, name) {
233
+ const state = this.elementState.get(el);
234
+ if (!state) return;
235
+ const subs = state.symbolSubs.get(name);
236
+ if (!subs) return;
237
+ for (const e of subs) this.schedule(e);
238
+ }
239
+ // ---------------------------------------------------------------------------
240
+ // Effect lifecycle.
241
+ // ---------------------------------------------------------------------------
242
+ /**
243
+ * Create + initialize an effect. Returns a disposer that stops the effect.
244
+ * Callers are expected to register the disposer with the core runtime's
245
+ * cleanup registry so it fires on element removal.
246
+ */
247
+ createEffect(expression, handler, owner) {
248
+ const e = new Effect(this, expression, handler, owner);
249
+ if (owner) this.getElementState(owner).effects.add(e);
250
+ queueMicrotask(() => {
251
+ void e.initialize();
252
+ });
253
+ return () => e.stop();
254
+ }
255
+ /**
256
+ * Stop all effects owned by an element. Called by the reactivity plugin's
257
+ * cleanup hook registered via `runtime.getCleanupRegistry()`.
258
+ */
259
+ stopElementEffects(el) {
260
+ const state = this.elementState.get(el);
261
+ if (!state) return;
262
+ for (const e of Array.from(state.effects)) e.stop();
263
+ state.effects.clear();
264
+ }
265
+ /**
266
+ * Internal: remove an effect's entries from all subscriber sets. Called by
267
+ * `Effect.stop()`.
268
+ */
269
+ _unsubscribeEffect(e) {
270
+ for (const dep of e.dependencies.values()) {
271
+ if (dep.kind === "global") {
272
+ const subs = this.globalSubs.get(dep.name);
273
+ subs?.delete(e);
274
+ } else if (dep.kind === "element" && dep.element) {
275
+ const state = this.elementState.get(dep.element);
276
+ const subs = state?.symbolSubs.get(dep.name);
277
+ subs?.delete(e);
278
+ state?.effects.delete(e);
279
+ } else if (dep.kind === "dom" && dep.element) {
280
+ const state = this.elementState.get(dep.element);
281
+ const handler = state?.domHandlers.get(dep.name);
282
+ handler?.subs.delete(e);
283
+ if (handler && handler.subs.size === 0) {
284
+ handler.detach();
285
+ state?.domHandlers.delete(dep.name);
286
+ }
287
+ state?.effects.delete(e);
288
+ }
289
+ }
290
+ }
291
+ // ---------------------------------------------------------------------------
292
+ // Caret-variable storage — `^name` reads/writes.
293
+ // ---------------------------------------------------------------------------
294
+ /**
295
+ * Whether `el` is a `dom-scope="isolated"` boundary. Walks of `^var`
296
+ * lookups stop at boundary elements that don't define the var, so nested
297
+ * components don't accidentally read or write each other's state.
298
+ */
299
+ isIsolationBoundary(el) {
300
+ return typeof el.getAttribute === "function" && el.getAttribute("dom-scope") === "isolated";
301
+ }
302
+ /**
303
+ * Walk up the DOM tree from `lookupRoot`, returning the first element whose
304
+ * state has `name` defined. Stops at any `dom-scope="isolated"` boundary
305
+ * that doesn't itself define the var. Returns `null` if no owner is found.
306
+ */
307
+ findCaretOwner(lookupRoot, name) {
308
+ let el = lookupRoot;
309
+ while (el) {
310
+ const state = this.elementState.get(el);
311
+ if (state && state.caretVars.has(name)) return el;
312
+ if (this.isIsolationBoundary(el)) return null;
313
+ el = el.parentElement;
314
+ }
315
+ return null;
316
+ }
317
+ /**
318
+ * Read a DOM-scoped variable. Walks up from `lookupRoot`, tracking each
319
+ * element visited as an `element` dep (so writes at any ancestor notify
320
+ * dependent effects). Stops at any `dom-scope="isolated"` boundary that
321
+ * doesn't itself define the var.
322
+ */
323
+ readCaret(lookupRoot, name) {
324
+ let el = lookupRoot;
325
+ while (el) {
326
+ this.trackElement(el, name);
327
+ const state = this.elementState.get(el);
328
+ if (state && state.caretVars.has(name)) {
329
+ return state.caretVars.get(name);
330
+ }
331
+ if (this.isIsolationBoundary(el)) return void 0;
332
+ el = el.parentElement;
333
+ }
334
+ return void 0;
335
+ }
336
+ /**
337
+ * Write a DOM-scoped variable. If `target` is provided, writes there
338
+ * directly; otherwise walks up from `lookupRoot` to find the existing owner,
339
+ * falling back to `lookupRoot` itself if no owner exists.
340
+ */
341
+ writeCaret(lookupRoot, name, value, target) {
342
+ const owner = target ?? this.findCaretOwner(lookupRoot, name) ?? lookupRoot;
343
+ const state = this.getElementState(owner);
344
+ state.caretVars.set(name, value);
345
+ this.notifyElement(owner, name);
346
+ }
347
+ // ---------------------------------------------------------------------------
348
+ // Scheduler — microtask-batched flush.
349
+ // ---------------------------------------------------------------------------
350
+ schedule(e) {
351
+ if (e.stopped) return;
352
+ this.pending.add(e);
353
+ if (this.scheduled) return;
354
+ this.scheduled = true;
355
+ queueMicrotask(() => void this.flush());
356
+ }
357
+ async flush() {
358
+ try {
359
+ while (this.pending.size > 0) {
360
+ const batch = Array.from(this.pending);
361
+ this.pending.clear();
362
+ for (const e of batch) {
363
+ if (e.stopped) continue;
364
+ await e.run();
365
+ if (!this.pending.has(e)) e.resetTriggerCount();
366
+ }
367
+ }
368
+ } finally {
369
+ this.scheduled = false;
370
+ }
371
+ }
372
+ };
373
+ var reactive = new Reactive();
374
+
375
+ // src/caret-var.ts
376
+ function coerceToElement(value) {
377
+ if (value instanceof Element) return value;
378
+ if (Array.isArray(value) && value[0] instanceof Element) return value[0];
379
+ return null;
380
+ }
381
+ function parseCaretPrefix(token, ctx) {
382
+ const pctx = ctx;
383
+ const ident = pctx.advance();
384
+ if (!ident || !ident.value) {
385
+ throw new Error("Expected identifier after '^'");
386
+ }
387
+ let onTarget = null;
388
+ const next = pctx.peek();
389
+ if (next && next.value === "on") {
390
+ pctx.advance();
391
+ onTarget = pctx.parseExpr(86);
392
+ }
393
+ const startTok = token;
394
+ return {
395
+ type: "caretVar",
396
+ name: ident.value,
397
+ onTarget,
398
+ start: startTok?.start ?? 0,
399
+ end: startTok?.end ?? 0,
400
+ line: startTok?.line,
401
+ column: startTok?.column
402
+ };
403
+ }
404
+ function makeEvaluateCaretVar(runtime) {
405
+ return async function evaluateCaretVar(node, ctx) {
406
+ const n = node;
407
+ const context = ctx;
408
+ let anchor = context.me ?? null;
409
+ if (n.onTarget) {
410
+ const resolved = await runtime.execute(n.onTarget, context);
411
+ const el = coerceToElement(resolved);
412
+ if (el) anchor = el;
413
+ }
414
+ if (!anchor) return void 0;
415
+ return reactive.readCaret(anchor, n.name);
416
+ };
417
+ }
418
+ function makeWriteCaretVar(runtime) {
419
+ return async function writeCaretVar(node, value, ctx) {
420
+ const n = node;
421
+ const context = ctx;
422
+ const anchor = context.me ?? null;
423
+ if (!anchor) return;
424
+ let target;
425
+ if (n.onTarget) {
426
+ const resolved = await runtime.execute(n.onTarget, context);
427
+ const el = coerceToElement(resolved);
428
+ if (el) target = el;
429
+ }
430
+ reactive.writeCaret(anchor, n.name, value, target);
431
+ };
432
+ }
433
+
434
+ // src/live.ts
435
+ function parseLiveFeature(ctx, token) {
436
+ const pctx = ctx;
437
+ const body = pctx.parseCommandListUntilEnd();
438
+ if (!pctx.isAtEnd() && pctx.check("end")) pctx.match("end");
439
+ const tok = token;
440
+ return {
441
+ type: "liveFeature",
442
+ body,
443
+ start: tok?.start ?? 0,
444
+ end: pctx.getPosition().end,
445
+ line: tok?.line,
446
+ column: tok?.column
447
+ };
448
+ }
449
+ function makeEvaluateLiveFeature(runtime) {
450
+ return async function evaluateLiveFeature(node, ctx) {
451
+ const context = ctx;
452
+ const owner = context.me ?? document.body;
453
+ const n = node;
454
+ const stop = reactive.createEffect(
455
+ async () => {
456
+ for (const cmd of n.body) {
457
+ await runtime.execute(cmd, context);
458
+ }
459
+ },
460
+ () => {
461
+ },
462
+ owner
463
+ );
464
+ context.registerCleanup?.(owner, stop, "live-effect");
465
+ return void 0;
466
+ };
467
+ }
468
+
469
+ // src/when.ts
470
+ function parseWhenFeature(ctx, token) {
471
+ const pctx = ctx;
472
+ const watched = [pctx.parseExpression()];
473
+ while (pctx.match("or")) {
474
+ watched.push(pctx.parseExpression());
475
+ }
476
+ pctx.consume("changes", "Expected 'changes' after when expression list");
477
+ const body = pctx.parseCommandListUntilEnd();
478
+ if (!pctx.isAtEnd() && pctx.check("end")) pctx.match("end");
479
+ const tok = token;
480
+ return {
481
+ type: "whenFeature",
482
+ watched,
483
+ body,
484
+ start: tok?.start ?? 0,
485
+ end: pctx.getPosition().end,
486
+ line: tok?.line,
487
+ column: tok?.column
488
+ };
489
+ }
490
+ function makeEvaluateWhenFeature(runtime) {
491
+ return async function evaluateWhenFeature(node, ctx) {
492
+ const context = ctx;
493
+ const owner = context.me ?? document.body;
494
+ const n = node;
495
+ for (const watchedExpr of n.watched) {
496
+ const stop = reactive.createEffect(
497
+ async () => {
498
+ return await runtime.execute(watchedExpr, context);
499
+ },
500
+ async (newValue) => {
501
+ const subCtx = { ...context, it: newValue, result: newValue };
502
+ for (const cmd of n.body) {
503
+ await runtime.execute(cmd, subCtx);
504
+ }
505
+ },
506
+ owner
507
+ );
508
+ context.registerCleanup?.(owner, stop, "when-effect");
509
+ }
510
+ return void 0;
511
+ };
512
+ }
513
+
514
+ // src/bind.ts
515
+ function parseBindFeature(ctx, token) {
516
+ const pctx = ctx;
517
+ const left = pctx.parseExpression();
518
+ if (!(pctx.match("to") || pctx.match("and") || pctx.match("with"))) {
519
+ throw new Error("bind requires 'to', 'and', or 'with' between the two sides");
520
+ }
521
+ const right = pctx.parseExpression();
522
+ if (pctx.check("end")) pctx.match("end");
523
+ const tok = token;
524
+ return {
525
+ type: "bindFeature",
526
+ left,
527
+ right,
528
+ start: tok?.start ?? 0,
529
+ end: pctx.getPosition().end,
530
+ line: tok?.line,
531
+ column: tok?.column
532
+ };
533
+ }
534
+ function detectProperty(el) {
535
+ const tag = el.tagName;
536
+ if (tag === "INPUT") {
537
+ const type = el.type;
538
+ if (type === "checkbox" || type === "radio") return "checked";
539
+ if (type === "number" || type === "range") return "valueAsNumber";
540
+ return "value";
541
+ }
542
+ if (tag === "TEXTAREA" || tag === "SELECT") return "value";
543
+ const ce = el.contentEditable;
544
+ if (ce === "true") return "textContent";
545
+ if ("value" in el) return "value";
546
+ return null;
547
+ }
548
+ function unwrapExplicitProperty(node) {
549
+ if (!node || node.type !== "memberExpression" && node.type !== "possessiveExpression") {
550
+ return null;
551
+ }
552
+ if (node.type === "memberExpression" && node.computed === true) return null;
553
+ const property = node.property;
554
+ const object = node.object;
555
+ if (!property || !object || property.type !== "identifier") return null;
556
+ const name = property.name ?? "";
557
+ if (!name) return null;
558
+ return { element: object, propertyName: name };
559
+ }
560
+ function isChainedMember(node) {
561
+ if (!node) return false;
562
+ return node.type === "memberExpression" || node.type === "possessiveExpression";
563
+ }
564
+ function isFormLikeElement(el) {
565
+ const tag = el.tagName;
566
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
567
+ if (el.contentEditable === "true") return true;
568
+ return false;
569
+ }
570
+ function debugEnabled2() {
571
+ try {
572
+ return typeof localStorage !== "undefined" && localStorage.getItem("hyperfixi:debug") !== null;
573
+ } catch {
574
+ return false;
575
+ }
576
+ }
577
+ function makeEvaluateBindFeature(runtime) {
578
+ return async function evaluateBindFeature(node, ctx) {
579
+ const context = ctx;
580
+ const owner = context.me ?? document.body;
581
+ const n = node;
582
+ const leftIsVar = isVarRef(n.left);
583
+ const rightIsVar = isVarRef(n.right);
584
+ let varSide = null;
585
+ let domSide = null;
586
+ if (leftIsVar && !rightIsVar) {
587
+ varSide = varRefInfo(n.left);
588
+ domSide = { exprNode: n.right };
589
+ } else if (rightIsVar && !leftIsVar) {
590
+ varSide = varRefInfo(n.right);
591
+ domSide = { exprNode: n.left };
592
+ } else if (leftIsVar && rightIsVar) {
593
+ throw new Error("bind: cannot bind two symbols together (need a DOM side)");
594
+ } else {
595
+ throw new Error("bind: could not identify a symbol side");
596
+ }
597
+ const explicit = unwrapExplicitProperty(domSide.exprNode);
598
+ if (explicit && isChainedMember(explicit.element)) {
599
+ throw new Error(
600
+ "bind: multi-level property access (e.g., `#el.a.b`) is not supported in v1 \u2014 restructure to a single property write or pass the element directly for auto-detection (e.g., `bind $x to #el`)."
601
+ );
602
+ }
603
+ const elementExpr = explicit ? explicit.element : domSide.exprNode;
604
+ const domValue = await runtime.execute(elementExpr, context);
605
+ if (!(domValue instanceof Element)) {
606
+ const valueType = domValue === null ? "null" : domValue === void 0 ? "undefined" : typeof domValue;
607
+ const snippet = domValue !== null && domValue !== void 0 ? ` "${String(domValue).slice(0, 40)}"` : "";
608
+ const suggestion = explicit ? "" : " If you meant to write to a property, use the explicit form: `<selector>'s <property>`.";
609
+ throw new Error(
610
+ `bind: right-hand side did not resolve to an element (got ${valueType}${snippet}).${suggestion}`
611
+ );
612
+ }
613
+ const el = domValue;
614
+ const prop = explicit ? explicit.propertyName : detectProperty(el);
615
+ if (!prop) {
616
+ throw new Error(
617
+ `bind: could not auto-detect property for <${el.tagName.toLowerCase()}> \u2014 use explicit \`<expr>'s <property>\` form`
618
+ );
619
+ }
620
+ const installDomToVar = !explicit || isFormLikeElement(el);
621
+ if (explicit && !installDomToVar && debugEnabled2() && typeof console !== "undefined") {
622
+ console.warn(
623
+ `[@hyperfixi/reactivity] bind: DOM\u2192var skipped for <${el.tagName.toLowerCase()}>.${prop} \u2014 no input/change event source.`
624
+ );
625
+ }
626
+ const readDom = () => {
627
+ if (prop === "valueAsNumber") return el.valueAsNumber;
628
+ if (prop === "checked") return el.checked;
629
+ if (prop === "textContent") return el.textContent ?? "";
630
+ return el[prop];
631
+ };
632
+ const writeDom = (value) => {
633
+ if (prop === "valueAsNumber") {
634
+ const n2 = Number(value);
635
+ el.valueAsNumber = Number.isNaN(n2) ? null : n2;
636
+ } else if (prop === "checked") {
637
+ el.checked = Boolean(value);
638
+ } else if (prop === "textContent") {
639
+ el.textContent = value == null ? "" : String(value);
640
+ } else {
641
+ el[prop] = value;
642
+ }
643
+ };
644
+ const readVar = () => {
645
+ if (varSide.isGlobal) return context.globals?.get(varSide.name);
646
+ return context.locals?.get(varSide.name);
647
+ };
648
+ const writeVar = (value) => {
649
+ if (varSide.isGlobal) {
650
+ context.globals?.set(varSide.name, value);
651
+ reactive.notifyGlobal(varSide.name);
652
+ } else {
653
+ context.locals?.set(varSide.name, value);
654
+ reactive.notifyElement(owner, varSide.name);
655
+ }
656
+ };
657
+ let stopDomToVar = null;
658
+ if (installDomToVar) {
659
+ stopDomToVar = reactive.createEffect(
660
+ () => {
661
+ reactive.trackDomProperty(el, prop);
662
+ return readDom();
663
+ },
664
+ (newValue) => {
665
+ writeVar(newValue);
666
+ },
667
+ owner
668
+ );
669
+ }
670
+ const stopVarToDom = reactive.createEffect(
671
+ () => {
672
+ if (varSide.isGlobal) reactive.trackGlobal(varSide.name);
673
+ else reactive.trackElement(owner, varSide.name);
674
+ return readVar();
675
+ },
676
+ (newValue) => {
677
+ if (newValue === void 0) return;
678
+ writeDom(newValue);
679
+ },
680
+ owner
681
+ );
682
+ if (stopDomToVar) context.registerCleanup?.(owner, stopDomToVar, "bind-dom-to-var");
683
+ context.registerCleanup?.(owner, stopVarToDom, "bind-var-to-dom");
684
+ return void 0;
685
+ };
686
+ }
687
+ function isVarRef(node) {
688
+ if (!node) return false;
689
+ if (node.type !== "identifier") return false;
690
+ const scope = node.scope;
691
+ if (scope === "local" || scope === "global") return true;
692
+ const name = node.name ?? "";
693
+ return name.startsWith("$") || name.startsWith(":");
694
+ }
695
+ function varRefInfo(node) {
696
+ const rawName = node.name ?? "";
697
+ const scope = node.scope;
698
+ let isGlobal;
699
+ let name;
700
+ if (scope === "global") {
701
+ isGlobal = true;
702
+ name = rawName;
703
+ } else if (scope === "local") {
704
+ isGlobal = false;
705
+ name = rawName;
706
+ } else if (rawName.startsWith("$")) {
707
+ isGlobal = true;
708
+ name = rawName.slice(1);
709
+ } else if (rawName.startsWith(":")) {
710
+ isGlobal = false;
711
+ name = rawName.slice(1);
712
+ } else {
713
+ isGlobal = false;
714
+ name = rawName;
715
+ }
716
+ if (!name) {
717
+ throw new Error("bind: variable reference has empty name");
718
+ }
719
+ return { name, isGlobal };
720
+ }
721
+
722
+ // src/index.ts
723
+ var reactivityPlugin = {
724
+ name: "@hyperfixi/reactivity",
725
+ version: "2.3.1",
726
+ install(ctx) {
727
+ const { parserExtensions, runtime } = ctx;
728
+ if (parserExtensions.hasFeature("live")) return;
729
+ parserExtensions.registerFeature("live", parseLiveFeature);
730
+ parserExtensions.registerFeature("when", parseWhenFeature);
731
+ parserExtensions.registerFeature("bind", parseBindFeature);
732
+ parserExtensions.registerPrefixOperator("^", 85, parseCaretPrefix);
733
+ parserExtensions.registerNodeEvaluator(
734
+ "liveFeature",
735
+ makeEvaluateLiveFeature(runtime)
736
+ );
737
+ parserExtensions.registerNodeEvaluator(
738
+ "whenFeature",
739
+ makeEvaluateWhenFeature(runtime)
740
+ );
741
+ parserExtensions.registerNodeEvaluator(
742
+ "bindFeature",
743
+ makeEvaluateBindFeature(runtime)
744
+ );
745
+ parserExtensions.registerNodeEvaluator(
746
+ "caretVar",
747
+ makeEvaluateCaretVar(runtime)
748
+ );
749
+ parserExtensions.registerNodeWriter("caretVar", makeWriteCaretVar(runtime));
750
+ parserExtensions.registerGlobalWriteHook((name, _value, _context) => {
751
+ reactive.notifyGlobal(name);
752
+ });
753
+ parserExtensions.registerGlobalReadHook((name, _context) => {
754
+ reactive.trackGlobal(name);
755
+ });
756
+ parserExtensions.registerLocalWriteHook((name, _value, context) => {
757
+ const owner = context.me ?? null;
758
+ if (owner) reactive.notifyElement(owner, name);
759
+ });
760
+ parserExtensions.registerLocalReadHook((name, context) => {
761
+ const owner = context.me ?? null;
762
+ if (owner) reactive.trackElement(owner, name);
763
+ });
764
+ }
765
+ };
766
+ var index_default = reactivityPlugin;
767
+ // Annotate the CommonJS export names for ESM import in node:
768
+ 0 && (module.exports = {
769
+ reactive,
770
+ reactivityPlugin
771
+ });
772
+ //# sourceMappingURL=index.cjs.map