@sigil-dev/runtime 0.9.3 → 0.9.5

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/index.ts CHANGED
@@ -20,6 +20,7 @@ export {
20
20
  } from "./easing.ts";
21
21
  export { ReactiveMap } from "./reactivity/map.ts";
22
22
  export { ReactiveSet } from "./reactivity/set.ts";
23
+ export { mountTransition, removeAnimated } from "./transition.ts";
23
24
 
24
25
  export const SIGIL_SAFE = Symbol.for("sigil.safe");
25
26
  export class SafeHtml {
@@ -142,6 +143,9 @@ function createReactiveProxy<T extends object>(target: T): T {
142
143
  trigger(target, k);
143
144
  }
144
145
  if (Array.isArray(target)) {
146
+ // delete leaves holes — length must shrink to the new value's,
147
+ // or iteration (spread, reconcile) sees phantom trailing slots
148
+ target.length = (newValue as unknown[]).length;
145
149
  trigger(target, "length");
146
150
  }
147
151
  };
@@ -287,6 +291,23 @@ export function tracking(): boolean {
287
291
  return currentEffect !== null;
288
292
  }
289
293
 
294
+ /**
295
+ * Run fn() outside of any reactive context: signal reads inside fn() do not
296
+ * register dependencies on the enclosing effect.
297
+ */
298
+ export function untrack<T>(fn: () => T): T {
299
+ const prevEffect = currentEffect;
300
+ const prevDeps = currentDeps;
301
+ currentEffect = null;
302
+ currentDeps = null;
303
+ try {
304
+ return fn();
305
+ } finally {
306
+ currentEffect = prevEffect;
307
+ currentDeps = prevDeps;
308
+ }
309
+ }
310
+
290
311
  // A4: $inspect — dev-only effect logging
291
312
  export function createInspectEffect(
292
313
  getValues: () => unknown[],
@@ -360,6 +381,15 @@ export function createProfilerEffect(
360
381
  });
361
382
  }
362
383
 
384
+ // Deferred effect mode for hydration: effects created while the flag is set
385
+ // are queued instead of run immediately, then flushed after DOM insertion.
386
+ const deferredEffectQueue: (() => void)[] = [];
387
+
388
+ export function flushDeferredEffects(): void {
389
+ const pending = deferredEffectQueue.splice(0);
390
+ for (const run of pending) run();
391
+ }
392
+
363
393
  // biome-ignore lint/suspicious/noConfusingVoidType: please shut up its a weird type
364
394
  export function createEffect(fn: () => void | (() => void)): () => void {
365
395
  let cleanup: (() => void) | undefined;
@@ -383,7 +413,13 @@ export function createEffect(fn: () => void | (() => void)): () => void {
383
413
  }
384
414
  };
385
415
 
386
- effect();
416
+ // In deferred mode (hydration), queue the effect instead of running immediately.
417
+ // This ensures the DOM is inserted before effect callbacks query it.
418
+ if ((globalThis as any).__sigilDeferEffects) {
419
+ deferredEffectQueue.push(() => effect());
420
+ } else {
421
+ effect();
422
+ }
387
423
 
388
424
  const dispose = () => {
389
425
  for (const depSet of ownDeps) depSet.delete(effect);
@@ -417,12 +453,137 @@ export function withEffectScope(fn: () => void): () => void {
417
453
  };
418
454
  }
419
455
 
456
+ type EffectScopeGlobal = { __sigilEffectScope?: (() => void)[] };
457
+
458
+ /**
459
+ * Run fn() with a detached effect scope. Effects created inside are invisible
460
+ * to enclosing scopes (the scope array is replaced, not merged) and live until
461
+ * dispose() is called — for app bootstrap, routers, and long-lived listeners.
462
+ */
463
+ export function createRoot<T>(fn: (dispose: () => void) => T): T {
464
+ const g = globalThis as EffectScopeGlobal;
465
+ const prev = g.__sigilEffectScope;
466
+ const disposers: (() => void)[] = [];
467
+ g.__sigilEffectScope = disposers;
468
+ const dispose = () => {
469
+ for (const d of disposers.splice(0)) d();
470
+ };
471
+ try {
472
+ return fn(dispose);
473
+ } finally {
474
+ g.__sigilEffectScope = prev;
475
+ }
476
+ }
477
+
478
+ export interface Resource<T> {
479
+ data: () => T | undefined;
480
+ loading: () => boolean;
481
+ error: () => Error | undefined;
482
+ refetch: () => void;
483
+ }
484
+
485
+ /**
486
+ * Async data source with reactive re-fetching. Re-runs when `source()` changes
487
+ * or refetch() is called; stale responses are discarded by request id.
488
+ */
489
+ export function createResource<T>(
490
+ fetcher: (source: unknown, info: { refetch: () => void }) => Promise<T>,
491
+ source?: () => unknown,
492
+ ): Resource<T> {
493
+ const data = createSignal<T | undefined>(undefined);
494
+ const loading = createSignal(true);
495
+ const error = createSignal<Error | undefined>(undefined);
496
+ const version = createSignal(0);
497
+ let requestCount = 0;
498
+ let versionCounter = 0;
499
+
500
+ const refetch = () => {
501
+ versionCounter++;
502
+ version.set(versionCounter);
503
+ };
504
+
505
+ createEffect(() => {
506
+ const src = source?.();
507
+ version(); // track refetch bumps
508
+ const id = ++requestCount;
509
+ loading.set(true);
510
+ error.set(undefined);
511
+ fetcher(src, { refetch }).then(
512
+ (result) => {
513
+ if (id !== requestCount) return; // stale response
514
+ data.set(result);
515
+ loading.set(false);
516
+ },
517
+ (err: unknown) => {
518
+ if (id !== requestCount) return;
519
+ error.set(err instanceof Error ? err : new Error(String(err)));
520
+ loading.set(false);
521
+ },
522
+ );
523
+ });
524
+
525
+ return { data, loading, error, refetch };
526
+ }
527
+
420
528
  export function createMemo<T>(fn: () => T): Signal<T> {
421
529
  const inner = createSignal<T>(undefined as T);
422
530
  createEffect(() => inner.set(fn()));
423
531
  return inner;
424
532
  }
425
533
 
534
+ /**
535
+ * Reconcile a list of keyed DOM nodes.
536
+ * anchor.before(node) handles both insert and move natively.
537
+ * @param claimNode - Optional hydration callback. Called with (element, key) for
538
+ * server-rendered elements removed from the DOM. Returns the element to use,
539
+ * or null to discard.
540
+ */
541
+ const lastItemFor = new WeakMap<object, unknown>();
542
+
543
+ export function reconcile<T>(
544
+ anchor: Partial<Comment>,
545
+ keyMap: Map<unknown, Node>,
546
+ newItems: T[],
547
+ getKey: (item: T, index: number) => unknown,
548
+ createNode: (item: T) => Node,
549
+ // is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
550
+ ): void {
551
+ const newKeys = new Set(newItems.map((item, i) => getKey(item, i)));
552
+
553
+ // Remove nodes no longer in list (or claim them for hydration)
554
+ for (const [key, node] of keyMap) {
555
+ if (!newKeys.has(key)) {
556
+ node.parentNode?.removeChild(node);
557
+ keyMap.delete(key);
558
+ }
559
+ }
560
+
561
+ // Insert/move in order — anchor.before() handles both cases.
562
+ // A surviving node whose item instance changed is rebuilt: compiled row
563
+ // bodies bake item reads at creation time, so immutable-style updates
564
+ // (new object, same key) must swap the node to show fresh content.
565
+ for (let i = 0; i < newItems.length; i++) {
566
+ const item = newItems[i]!;
567
+ const key = getKey(item, i);
568
+ let node = keyMap.get(key);
569
+ if (
570
+ node &&
571
+ lastItemFor.has(node) &&
572
+ !Object.is(lastItemFor.get(node), item)
573
+ ) {
574
+ node.parentNode?.removeChild(node);
575
+ keyMap.delete(key);
576
+ node = undefined;
577
+ }
578
+ if (!node) {
579
+ node = createNode(item);
580
+ lastItemFor.set(node, item);
581
+ keyMap.set(key, node);
582
+ }
583
+ anchor.before?.(node);
584
+ }
585
+ }
586
+
426
587
  /**
427
588
  * Claim an existing element from a hydration pool by tag name, or create a new one.
428
589
  * Removes the claimed node from the pool array.
@@ -488,43 +649,6 @@ export function claimComment(
488
649
  return comment;
489
650
  }
490
651
 
491
- /**
492
- * Reconcile a list of keyed DOM nodes.
493
- * anchor.before(node) handles both insert and move natively.
494
- * @param claimNode - Optional hydration callback. Called with (element, key) for
495
- * server-rendered elements removed from the DOM. Returns the element to use,
496
- * or null to discard.
497
- */
498
- export function reconcile<T>(
499
- anchor: Partial<Comment>,
500
- keyMap: Map<unknown, Node>,
501
- newItems: T[],
502
- getKey: (item: T) => unknown,
503
- createNode: (item: T) => Node,
504
- // is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
505
- ): void {
506
- const newKeys = new Set(newItems.map(getKey));
507
-
508
- // Remove nodes no longer in list (or claim them for hydration)
509
- for (const [key, node] of keyMap) {
510
- if (!newKeys.has(key)) {
511
- node.parentNode?.removeChild(node);
512
- keyMap.delete(key);
513
- }
514
- }
515
-
516
- // Insert/move in order — anchor.before() handles both cases
517
- for (const item of newItems) {
518
- const key = getKey(item);
519
- let node = keyMap.get(key);
520
- if (!node) {
521
- node = createNode(item);
522
- keyMap.set(key, node);
523
- }
524
- anchor.before?.(node);
525
- }
526
- }
527
-
528
652
  /**
529
653
  * Hydrate a keyed list: claim the comment anchor, walk its sibling elements
530
654
  * to build a keyMap, then reconcile with signal data.
@@ -535,7 +659,7 @@ export function reconcile<T>(
535
659
  export function hydrateKeyedList<T>(
536
660
  nodesVar: ChildNode[],
537
661
  signalItems: T[],
538
- getKey: (item: T) => unknown,
662
+ getKey: (item: T, index: number) => unknown,
539
663
  createNode: (item: T) => Node,
540
664
  parent?: Element,
541
665
  ): { anchor: Comment; keyMap: Map<unknown, Node> } {
@@ -564,7 +688,7 @@ export function hydrateKeyedList<T>(
564
688
  // Map claimed elements to keys by index
565
689
  for (let i = 0; i < claimed.length && i < signalItems.length; i++) {
566
690
  //@ts-expect-error i is always in bounds
567
- const key = getKey(signalItems[i]);
691
+ const key = getKey(signalItems[i], i);
568
692
  //@ts-expect-error i is always in bounds
569
693
  keyMap.set(key, claimed[i]);
570
694
  }
package/jsr.json CHANGED
@@ -1,8 +1,8 @@
1
- {
2
- "name": "@sigil-dev/runtime",
3
- "version": "0.6.0",
4
- "exports": "./index.ts",
5
- "publish": {
6
- "exclude": ["src/**/*.test.ts"]
7
- }
8
- }
1
+ {
2
+ "name": "@sigil-dev/runtime",
3
+ "version": "0.6.0",
4
+ "exports": "./index.ts",
5
+ "publish": {
6
+ "exclude": ["src/**/*.test.ts"]
7
+ }
8
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@sigil-dev/runtime",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.9.3",
5
+ "version": "0.9.5",
6
6
  "description": "Runtime for the Sigil framework",
7
7
  "private": false,
8
8
  "devDependencies": {
package/reactivity/map.ts CHANGED
@@ -1,98 +1,98 @@
1
- import { createSignal, type Signal } from "../index.ts";
2
-
3
- export class ReactiveMap<K, V> extends Map<K, V> {
4
- #keyVersions: Map<K, Signal<number>> = new Map();
5
- #structVersion: Signal<number>;
6
- #structVal: number = 0;
7
- #getKeyVersion(key: K): Signal<number> {
8
- let sig = this.#keyVersions.get(key);
9
- if (!sig) {
10
- const inner = createSignal(0);
11
- this.#keyVersions.set(key, inner);
12
- sig = inner;
13
- }
14
- return sig;
15
- }
16
- #bumpKey(key: K) {
17
- const sig = this.#keyVersions.get(key);
18
- if (sig) {
19
- const v = sig.peek() + 1;
20
- (sig as Signal<number>).set(v);
21
- }
22
- }
23
- #bumpStruct() {
24
- this.#structVal++;
25
- this.#structVersion.set(this.#structVal);
26
- }
27
-
28
- constructor(entries?: readonly (readonly [K, V])[] | null) {
29
- super(entries);
30
- this.#structVersion = createSignal(0);
31
- if (entries) {
32
- for (const [k] of entries) this.#getKeyVersion(k);
33
- }
34
- }
35
-
36
- get(key: K): V | undefined {
37
- this.#getKeyVersion(key)();
38
- return super.get(key);
39
- }
40
-
41
- set(key: K, value: V): this {
42
- const existed = super.has(key);
43
- super.set(key, value);
44
- this.#bumpKey(key);
45
- if (!existed) this.#bumpStruct();
46
- return this;
47
- }
48
-
49
- delete(key: K): boolean {
50
- const result = super.delete(key);
51
- if (result) {
52
- this.#bumpKey(key);
53
- this.#bumpStruct();
54
- }
55
- return result;
56
- }
57
-
58
- clear(): void {
59
- if (this.size > 0) {
60
- for (const key of this.keys()) this.#bumpKey(key);
61
- super.clear();
62
- this.#bumpStruct();
63
- }
64
- }
65
-
66
- forEach(
67
- callbackfn: (value: V, key: K, map: Map<K, V>) => void,
68
- thisArg?: unknown,
69
- ): void {
70
- this.#structVersion();
71
- super.forEach(callbackfn, thisArg);
72
- }
73
-
74
- get size(): number {
75
- this.#structVersion();
76
- return super.size;
77
- }
78
-
79
- entries(): IterableIterator<[K, V]> {
80
- this.#structVersion();
81
- return super.entries();
82
- }
83
-
84
- keys(): IterableIterator<K> {
85
- this.#structVersion();
86
- return super.keys();
87
- }
88
-
89
- values(): IterableIterator<V> {
90
- this.#structVersion();
91
- return super.values();
92
- }
93
-
94
- [Symbol.iterator](): IterableIterator<[K, V]> {
95
- this.#structVersion();
96
- return super[Symbol.iterator]();
97
- }
98
- }
1
+ import { createSignal, type Signal } from "../index.ts";
2
+
3
+ export class ReactiveMap<K, V> extends Map<K, V> {
4
+ #keyVersions: Map<K, Signal<number>> = new Map();
5
+ #structVersion: Signal<number>;
6
+ #structVal: number = 0;
7
+ #getKeyVersion(key: K): Signal<number> {
8
+ let sig = this.#keyVersions.get(key);
9
+ if (!sig) {
10
+ const inner = createSignal(0);
11
+ this.#keyVersions.set(key, inner);
12
+ sig = inner;
13
+ }
14
+ return sig;
15
+ }
16
+ #bumpKey(key: K) {
17
+ const sig = this.#keyVersions.get(key);
18
+ if (sig) {
19
+ const v = sig.peek() + 1;
20
+ (sig as Signal<number>).set(v);
21
+ }
22
+ }
23
+ #bumpStruct() {
24
+ this.#structVal++;
25
+ this.#structVersion.set(this.#structVal);
26
+ }
27
+
28
+ constructor(entries?: readonly (readonly [K, V])[] | null) {
29
+ super(entries);
30
+ this.#structVersion = createSignal(0);
31
+ if (entries) {
32
+ for (const [k] of entries) this.#getKeyVersion(k);
33
+ }
34
+ }
35
+
36
+ get(key: K): V | undefined {
37
+ this.#getKeyVersion(key)();
38
+ return super.get(key);
39
+ }
40
+
41
+ set(key: K, value: V): this {
42
+ const existed = super.has(key);
43
+ super.set(key, value);
44
+ this.#bumpKey(key);
45
+ if (!existed) this.#bumpStruct();
46
+ return this;
47
+ }
48
+
49
+ delete(key: K): boolean {
50
+ const result = super.delete(key);
51
+ if (result) {
52
+ this.#bumpKey(key);
53
+ this.#bumpStruct();
54
+ }
55
+ return result;
56
+ }
57
+
58
+ clear(): void {
59
+ if (this.size > 0) {
60
+ for (const key of this.keys()) this.#bumpKey(key);
61
+ super.clear();
62
+ this.#bumpStruct();
63
+ }
64
+ }
65
+
66
+ forEach(
67
+ callbackfn: (value: V, key: K, map: Map<K, V>) => void,
68
+ thisArg?: unknown,
69
+ ): void {
70
+ this.#structVersion();
71
+ super.forEach(callbackfn, thisArg);
72
+ }
73
+
74
+ get size(): number {
75
+ this.#structVersion();
76
+ return super.size;
77
+ }
78
+
79
+ entries(): IterableIterator<[K, V]> {
80
+ this.#structVersion();
81
+ return super.entries();
82
+ }
83
+
84
+ keys(): IterableIterator<K> {
85
+ this.#structVersion();
86
+ return super.keys();
87
+ }
88
+
89
+ values(): IterableIterator<V> {
90
+ this.#structVersion();
91
+ return super.values();
92
+ }
93
+
94
+ [Symbol.iterator](): IterableIterator<[K, V]> {
95
+ this.#structVersion();
96
+ return super[Symbol.iterator]();
97
+ }
98
+ }
package/reactivity/set.ts CHANGED
@@ -1,74 +1,74 @@
1
- import { createSignal, type Signal } from "../index.ts";
2
-
3
- export class ReactiveSet<T> extends Set<T> {
4
- #version: Signal<number>;
5
- #versionVal: number = 0;
6
-
7
- constructor(values?: readonly T[] | null) {
8
- super(values);
9
- this.#version = createSignal(0);
10
- }
11
-
12
- #bump() {
13
- this.#versionVal++;
14
- this.#version.set(this.#versionVal);
15
- }
16
-
17
- has(value: T): boolean {
18
- this.#version();
19
- return super.has(value);
20
- }
21
-
22
- add(value: T): this {
23
- const sizeBefore = super.size;
24
- super.add(value);
25
- if (super.size !== sizeBefore) this.#bump();
26
- return this;
27
- }
28
-
29
- delete(value: T): boolean {
30
- const result = super.delete(value);
31
- if (result) this.#bump();
32
- return result;
33
- }
34
-
35
- clear(): void {
36
- if (this.size > 0) {
37
- super.clear();
38
- this.#bump();
39
- }
40
- }
41
-
42
- forEach(
43
- callbackfn: (value: T, value2: T, set: Set<T>) => void,
44
- thisArg?: unknown,
45
- ): void {
46
- this.#version();
47
- super.forEach(callbackfn, thisArg);
48
- }
49
-
50
- get size(): number {
51
- this.#version();
52
- return super.size;
53
- }
54
-
55
- entries(): IterableIterator<[T, T]> {
56
- this.#version();
57
- return super.entries();
58
- }
59
-
60
- keys(): IterableIterator<T> {
61
- this.#version();
62
- return super.keys();
63
- }
64
-
65
- values(): IterableIterator<T> {
66
- this.#version();
67
- return super.values();
68
- }
69
-
70
- [Symbol.iterator](): IterableIterator<T> {
71
- this.#version();
72
- return super[Symbol.iterator]();
73
- }
74
- }
1
+ import { createSignal, type Signal } from "../index.ts";
2
+
3
+ export class ReactiveSet<T> extends Set<T> {
4
+ #version: Signal<number>;
5
+ #versionVal: number = 0;
6
+
7
+ constructor(values?: readonly T[] | null) {
8
+ super(values);
9
+ this.#version = createSignal(0);
10
+ }
11
+
12
+ #bump() {
13
+ this.#versionVal++;
14
+ this.#version.set(this.#versionVal);
15
+ }
16
+
17
+ has(value: T): boolean {
18
+ this.#version();
19
+ return super.has(value);
20
+ }
21
+
22
+ add(value: T): this {
23
+ const sizeBefore = super.size;
24
+ super.add(value);
25
+ if (super.size !== sizeBefore) this.#bump();
26
+ return this;
27
+ }
28
+
29
+ delete(value: T): boolean {
30
+ const result = super.delete(value);
31
+ if (result) this.#bump();
32
+ return result;
33
+ }
34
+
35
+ clear(): void {
36
+ if (this.size > 0) {
37
+ super.clear();
38
+ this.#bump();
39
+ }
40
+ }
41
+
42
+ forEach(
43
+ callbackfn: (value: T, value2: T, set: Set<T>) => void,
44
+ thisArg?: unknown,
45
+ ): void {
46
+ this.#version();
47
+ super.forEach(callbackfn, thisArg);
48
+ }
49
+
50
+ get size(): number {
51
+ this.#version();
52
+ return super.size;
53
+ }
54
+
55
+ entries(): IterableIterator<[T, T]> {
56
+ this.#version();
57
+ return super.entries();
58
+ }
59
+
60
+ keys(): IterableIterator<T> {
61
+ this.#version();
62
+ return super.keys();
63
+ }
64
+
65
+ values(): IterableIterator<T> {
66
+ this.#version();
67
+ return super.values();
68
+ }
69
+
70
+ [Symbol.iterator](): IterableIterator<T> {
71
+ this.#version();
72
+ return super[Symbol.iterator]();
73
+ }
74
+ }
package/test-preload.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { Window } from "happy-dom";
2
-
3
- const window = new Window();
4
- (globalThis as any).document = window.document;
5
- (globalThis as any).Element = window.Element;
6
- (globalThis as any).Text = window.Text;
7
- (globalThis as any).Node = window.Node;
8
- (globalThis as any).HTMLElement = window.HTMLElement;
9
- (globalThis as any).Comment = window.Comment;
1
+ import { Window } from "happy-dom";
2
+
3
+ const window = new Window();
4
+ (globalThis as any).document = window.document;
5
+ (globalThis as any).Element = window.Element;
6
+ (globalThis as any).Text = window.Text;
7
+ (globalThis as any).Node = window.Node;
8
+ (globalThis as any).HTMLElement = window.HTMLElement;
9
+ (globalThis as any).Comment = window.Comment;