@sigil-dev/runtime 0.5.0 → 0.6.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 (4) hide show
  1. package/index.test.ts +59 -35
  2. package/index.ts +318 -311
  3. package/jsr.json +1 -1
  4. package/package.json +1 -1
package/index.test.ts CHANGED
@@ -753,32 +753,32 @@ describe("hydration: full element tree claim", () => {
753
753
  });
754
754
 
755
755
  describe("Scary reactivity", () => {
756
- test("effect unsubscribes from stale dependencies", () => {
757
- const condition = createSignal(true);
758
- const a = createSignal(1);
759
- const b = createSignal(1);
760
- let runs = 0;
761
-
762
- createEffect(() => {
763
- runs++;
764
- if (condition()) {
765
- a(); // read a only when condition is true
766
- } else {
767
- b(); // read b only when condition is false
768
- }
769
- });
770
-
771
- expect(runs).toBe(1);
772
-
773
- // flip condition, effect should now track b, not a
774
- condition.set(false);
775
- expect(runs).toBe(2);
776
-
777
- // updating a should NOT trigger the effect anymore
778
- a.set(99);
779
- expect(runs).toBe(2); // ← this will fail with current implementation
780
- });
781
- })
756
+ test("effect unsubscribes from stale dependencies", () => {
757
+ const condition = createSignal(true);
758
+ const a = createSignal(1);
759
+ const b = createSignal(1);
760
+ let runs = 0;
761
+
762
+ createEffect(() => {
763
+ runs++;
764
+ if (condition()) {
765
+ a(); // read a only when condition is true
766
+ } else {
767
+ b(); // read b only when condition is false
768
+ }
769
+ });
770
+
771
+ expect(runs).toBe(1);
772
+
773
+ // flip condition, effect should now track b, not a
774
+ condition.set(false);
775
+ expect(runs).toBe(2);
776
+
777
+ // updating a should NOT trigger the effect anymore
778
+ a.set(99);
779
+ expect(runs).toBe(2); // ← this will fail with current implementation
780
+ });
781
+ });
782
782
 
783
783
  describe("withEffectScope", () => {
784
784
  test("collects effects created during fn()", () => {
@@ -820,8 +820,14 @@ describe("withEffectScope", () => {
820
820
  let runsA = 0;
821
821
  let runsB = 0;
822
822
  const dispose = withEffectScope(() => {
823
- createEffect(() => { a(); runsA++; });
824
- createEffect(() => { b(); runsB++; });
823
+ createEffect(() => {
824
+ a();
825
+ runsA++;
826
+ });
827
+ createEffect(() => {
828
+ b();
829
+ runsB++;
830
+ });
825
831
  });
826
832
  a.set(1);
827
833
  b.set(1);
@@ -841,10 +847,16 @@ describe("withEffectScope", () => {
841
847
  let outsideRuns = 0;
842
848
  let insideRuns = 0;
843
849
 
844
- createEffect(() => { count(); outsideRuns++; });
850
+ createEffect(() => {
851
+ count();
852
+ outsideRuns++;
853
+ });
845
854
 
846
855
  const dispose = withEffectScope(() => {
847
- createEffect(() => { count(); insideRuns++; });
856
+ createEffect(() => {
857
+ count();
858
+ insideRuns++;
859
+ });
848
860
  });
849
861
 
850
862
  count.set(1);
@@ -855,7 +867,7 @@ describe("withEffectScope", () => {
855
867
 
856
868
  count.set(2);
857
869
  expect(outsideRuns).toBe(3); // still running
858
- expect(insideRuns).toBe(2); // stopped
870
+ expect(insideRuns).toBe(2); // stopped
859
871
  });
860
872
 
861
873
  test("nested scopes: inner dispose doesn't affect outer", () => {
@@ -864,10 +876,16 @@ describe("withEffectScope", () => {
864
876
  let innerRuns = 0;
865
877
 
866
878
  const disposeOuter = withEffectScope(() => {
867
- createEffect(() => { count(); outerRuns++; });
879
+ createEffect(() => {
880
+ count();
881
+ outerRuns++;
882
+ });
868
883
  // simulates a child component scope inside a page scope
869
884
  const disposeInner = withEffectScope(() => {
870
- createEffect(() => { count(); innerRuns++; });
885
+ createEffect(() => {
886
+ count();
887
+ innerRuns++;
888
+ });
871
889
  });
872
890
  disposeInner();
873
891
  });
@@ -886,7 +904,10 @@ describe("withEffectScope", () => {
886
904
  let memoRuns = 0;
887
905
  const dispose = withEffectScope(() => {
888
906
  // createMemo internally calls createEffect
889
- const doubled = createMemo(() => { memoRuns++; return count() * 2; });
907
+ const doubled = createMemo(() => {
908
+ memoRuns++;
909
+ return count() * 2;
910
+ });
890
911
  expect(doubled()).toBe(0);
891
912
  });
892
913
  expect(memoRuns).toBe(1);
@@ -902,7 +923,10 @@ describe("withEffectScope", () => {
902
923
  const count = createSignal(0);
903
924
  let runs = 0;
904
925
  const dispose = withEffectScope(() => {
905
- createEffect(() => { count(); runs++; });
926
+ createEffect(() => {
927
+ count();
928
+ runs++;
929
+ });
906
930
  });
907
931
  dispose();
908
932
  dispose(); // second call must not throw or double-fire
package/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export const SIGIL_SAFE = Symbol.for("sigil.safe");
2
2
  export class SafeHtml {
3
- [SIGIL_SAFE] = true;
4
- constructor(public v: string) { }
5
- toString() {
6
- return this.v;
7
- }
3
+ [SIGIL_SAFE] = true;
4
+ constructor(public v: string) {}
5
+ toString() {
6
+ return this.v;
7
+ }
8
8
  }
9
9
 
10
10
  let currentEffect: (() => void) | null = null;
@@ -14,202 +14,204 @@ let batchDepth = 0;
14
14
  const pendingEffects = new Set<() => void>();
15
15
 
16
16
  const targetDepsMap = new WeakMap<
17
- object,
18
- Map<string | symbol, Set<() => void>>
17
+ object,
18
+ Map<string | symbol, Set<() => void>>
19
19
  >();
20
20
 
21
21
  function getDeps(target: object, key: string | symbol): Set<() => void> {
22
- let deps = targetDepsMap.get(target);
23
- if (!deps) {
24
- deps = new Map();
25
- targetDepsMap.set(target, deps);
26
- }
27
- let set = deps.get(key);
28
- if (!set) {
29
- set = new Set();
30
- deps.set(key, set);
31
- }
32
- return set;
22
+ let deps = targetDepsMap.get(target);
23
+ if (!deps) {
24
+ deps = new Map();
25
+ targetDepsMap.set(target, deps);
26
+ }
27
+ let set = deps.get(key);
28
+ if (!set) {
29
+ set = new Set();
30
+ deps.set(key, set);
31
+ }
32
+ return set;
33
33
  }
34
34
 
35
35
  function track(target: object, key: string | symbol) {
36
- if (currentEffect && currentDeps) {
37
- const deps = getDeps(target, key);
38
- deps.add(currentEffect);
39
- currentDeps.add(deps); // Set, deduplicates automatically
40
- }
36
+ if (currentEffect && currentDeps) {
37
+ const deps = getDeps(target, key);
38
+ deps.add(currentEffect);
39
+ currentDeps.add(deps); // Set, deduplicates automatically
40
+ }
41
41
  }
42
42
 
43
43
  function trigger(target: object, key: string | symbol) {
44
- const deps = targetDepsMap.get(target);
45
- if (!deps) return;
46
- const set = deps.get(key);
47
- if (!set) return;
48
- for (const fn of [...set]) {
49
- if (batchDepth > 0) pendingEffects.add(fn);
50
- else fn();
51
- }
44
+ const deps = targetDepsMap.get(target);
45
+ if (!deps) return;
46
+ const set = deps.get(key);
47
+ if (!set) return;
48
+ for (const fn of [...set]) {
49
+ if (batchDepth > 0) pendingEffects.add(fn);
50
+ else fn();
51
+ }
52
52
  }
53
53
 
54
54
  const proxyCache = new WeakMap<object, object>();
55
55
  const reactiveTargets = new WeakSet<object>();
56
56
 
57
57
  function createReactiveProxy<T extends object>(target: T): T {
58
- const existing = proxyCache.get(target);
59
- if (existing) return existing as T;
60
-
61
- const proxy = new Proxy(target, {
62
- get(target, key, receiver) {
63
- if (key === "set") {
64
- return (newValue: T) => {
65
- const oldKeys = new Set(Object.keys(target));
66
- const newKeys = new Set(Object.keys(newValue));
67
- for (const k of oldKeys) {
68
- if (!newKeys.has(k)) {
69
- delete (target as any)[k];
70
- trigger(target, k);
71
- }
72
- }
73
- for (const k of newKeys) {
74
- (target as any)[k] = (newValue as any)[k];
75
- trigger(target, k);
76
- }
77
- if (Array.isArray(target)) {
78
- trigger(target, "length");
79
- }
80
- };
81
- }
82
-
83
- if (key === "peek") {
84
- return () => target;
85
- }
86
-
87
- if (typeof key === "symbol") {
88
- return Reflect.get(target, key, receiver);
89
- }
90
-
91
- track(target, key);
92
- const value = target[key as keyof T];
93
-
94
- if (typeof value === "object" && value !== null) {
95
- return createReactiveProxy(value);
96
- }
97
-
98
- return value;
99
- },
100
-
101
- set(target, key, newValue, receiver) {
102
- const oldValue = target[key as keyof T];
103
- const result = Reflect.set(target, key, newValue, receiver);
104
- if (oldValue !== newValue) {
105
- trigger(target, key as string | symbol);
106
- if (
107
- Array.isArray(target) &&
108
- typeof key === "string" &&
109
- !Number.isNaN(Number(key))
110
- ) {
111
- trigger(target, "length");
112
- }
113
- }
114
- return result;
115
- },
116
-
117
- deleteProperty(target, key) {
118
- const result = Reflect.deleteProperty(target, key);
119
- trigger(target, key as string | symbol);
120
- return result;
121
- },
122
- });
123
-
124
- proxyCache.set(target, proxy);
125
- reactiveTargets.add(target);
126
- return proxy as T;
58
+ const existing = proxyCache.get(target);
59
+ if (existing) return existing as T;
60
+
61
+ const proxy = new Proxy(target, {
62
+ get(target, key, receiver) {
63
+ if (key === "set") {
64
+ return (newValue: T) => {
65
+ const oldKeys = new Set(Object.keys(target));
66
+ const newKeys = new Set(Object.keys(newValue));
67
+ for (const k of oldKeys) {
68
+ if (!newKeys.has(k)) {
69
+ delete (target as any)[k];
70
+ trigger(target, k);
71
+ }
72
+ }
73
+ for (const k of newKeys) {
74
+ (target as any)[k] = (newValue as any)[k];
75
+ trigger(target, k);
76
+ }
77
+ if (Array.isArray(target)) {
78
+ trigger(target, "length");
79
+ }
80
+ };
81
+ }
82
+
83
+ if (key === "peek") {
84
+ return () => target;
85
+ }
86
+
87
+ if (typeof key === "symbol") {
88
+ return Reflect.get(target, key, receiver);
89
+ }
90
+
91
+ track(target, key);
92
+ const value = target[key as keyof T];
93
+
94
+ if (typeof value === "object" && value !== null) {
95
+ return createReactiveProxy(value);
96
+ }
97
+
98
+ return value;
99
+ },
100
+
101
+ set(target, key, newValue, receiver) {
102
+ const oldValue = target[key as keyof T];
103
+ const result = Reflect.set(target, key, newValue, receiver);
104
+ if (oldValue !== newValue) {
105
+ trigger(target, key as string | symbol);
106
+ if (
107
+ Array.isArray(target) &&
108
+ typeof key === "string" &&
109
+ !Number.isNaN(Number(key))
110
+ ) {
111
+ trigger(target, "length");
112
+ }
113
+ }
114
+ return result;
115
+ },
116
+
117
+ deleteProperty(target, key) {
118
+ const result = Reflect.deleteProperty(target, key);
119
+ trigger(target, key as string | symbol);
120
+ return result;
121
+ },
122
+ });
123
+
124
+ proxyCache.set(target, proxy);
125
+ reactiveTargets.add(target);
126
+ return proxy as T;
127
127
  }
128
128
 
129
129
  // ─── createSignal (branches on type) ───
130
130
 
131
131
  export type Signal<T> = {
132
- (): T;
133
- set: (next: T) => void;
134
- peek: () => T;
132
+ (): T;
133
+ set: (next: T) => void;
134
+ peek: () => T;
135
135
  };
136
136
 
137
137
  export function batch(fn: () => void) {
138
- batchDepth++;
139
- fn();
140
- batchDepth--;
141
- if (batchDepth === 0) {
142
- // biome-ignore lint/suspicious/useIterableCallbackReturn: effects returing cleanup is intended
143
- pendingEffects.forEach((e) => e());
144
- pendingEffects.clear();
145
- }
138
+ batchDepth++;
139
+ fn();
140
+ batchDepth--;
141
+ if (batchDepth === 0) {
142
+ // biome-ignore lint/suspicious/useIterableCallbackReturn: effects returing cleanup is intended
143
+ pendingEffects.forEach((e) => e());
144
+ pendingEffects.clear();
145
+ }
146
146
  }
147
147
 
148
148
  export function createSignal<T>(initial: T): Signal<T> {
149
- if (typeof initial === "object" && initial !== null) {
150
- const proxy = createReactiveProxy(initial);
151
- const getter = () => proxy;
152
- getter.set = (next: T) => (proxy as any).set(next);
153
- getter.peek = () => initial;
154
- return getter;
155
- }
156
-
157
- const subs = new Set<() => void>();
158
- let value = initial;
159
-
160
- const getter = () => {
161
- if (currentEffect && currentDeps) {
162
- subs.add(currentEffect);
163
- currentDeps.add(subs);
164
- }
165
- return value;
166
- };
167
-
168
- getter.set = (next: T) => {
169
- value = next;
170
- for (const fn of [...subs]) {
171
- if (batchDepth > 0) pendingEffects.add(fn);
172
- else fn();
173
- }
174
- };
175
-
176
- getter.peek = () => value;
177
-
178
- return getter;
149
+ if (typeof initial === "object" && initial !== null) {
150
+ const proxy = createReactiveProxy(initial);
151
+ const getter = () => proxy;
152
+ getter.set = (next: T) => (proxy as any).set(next);
153
+ getter.peek = () => initial;
154
+ return getter;
155
+ }
156
+
157
+ const subs = new Set<() => void>();
158
+ let value = initial;
159
+
160
+ const getter = () => {
161
+ if (currentEffect && currentDeps) {
162
+ subs.add(currentEffect);
163
+ currentDeps.add(subs);
164
+ }
165
+ return value;
166
+ };
167
+
168
+ getter.set = (next: T) => {
169
+ value = next;
170
+ for (const fn of [...subs]) {
171
+ if (batchDepth > 0) pendingEffects.add(fn);
172
+ else fn();
173
+ }
174
+ };
175
+
176
+ getter.peek = () => value;
177
+
178
+ return getter;
179
179
  }
180
180
 
181
181
  // biome-ignore lint/suspicious/noConfusingVoidType: please shut up its a weird type
182
182
  export function createEffect(fn: () => void | (() => void)): () => void {
183
- let cleanup: (() => void) | void;
184
- const ownDeps = new Set<Set<() => void>>();
185
-
186
- const effect = () => {
187
- if (cleanup) cleanup();
188
- for (const depSet of ownDeps) depSet.delete(effect);
189
- ownDeps.clear();
190
-
191
- const prevEffect = currentEffect;
192
- const prevDeps = currentDeps;
193
- currentEffect = effect;
194
- currentDeps = ownDeps;
195
- try {
196
- cleanup = fn();
197
- } finally {
198
- currentEffect = prevEffect;
199
- currentDeps = prevDeps;
200
- }
201
- };
202
-
203
- effect();
204
-
205
- const dispose = () => {
206
- for (const depSet of ownDeps) depSet.delete(effect);
207
- ownDeps.clear();
208
- if (cleanup) cleanup();
209
- };
210
-
211
- ((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(dispose);
212
- return dispose;
183
+ let cleanup: (() => void) | void;
184
+ const ownDeps = new Set<Set<() => void>>();
185
+
186
+ const effect = () => {
187
+ if (cleanup) cleanup();
188
+ for (const depSet of ownDeps) depSet.delete(effect);
189
+ ownDeps.clear();
190
+
191
+ const prevEffect = currentEffect;
192
+ const prevDeps = currentDeps;
193
+ currentEffect = effect;
194
+ currentDeps = ownDeps;
195
+ try {
196
+ cleanup = fn();
197
+ } finally {
198
+ currentEffect = prevEffect;
199
+ currentDeps = prevDeps;
200
+ }
201
+ };
202
+
203
+ effect();
204
+
205
+ const dispose = () => {
206
+ for (const depSet of ownDeps) depSet.delete(effect);
207
+ ownDeps.clear();
208
+ if (cleanup) cleanup();
209
+ };
210
+
211
+ ((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
212
+ dispose,
213
+ );
214
+ return dispose;
213
215
  }
214
216
 
215
217
  /**
@@ -218,24 +220,24 @@ export function createEffect(fn: () => void | (() => void)): () => void {
218
220
  * Supports nesting: inner scopes don't leak to outer ones.
219
221
  */
220
222
  export function withEffectScope(fn: () => void): () => void {
221
- const prev = (globalThis as any).__sigilEffectScope;
222
- const disposers: (() => void)[] = [];
223
- (globalThis as any).__sigilEffectScope = disposers;
224
- try {
225
- fn();
226
- } finally {
227
- (globalThis as any).__sigilEffectScope = prev;
228
- }
229
- return () => {
230
- for (const d of disposers) d();
231
- disposers.length = 0;
232
- };
223
+ const prev = (globalThis as any).__sigilEffectScope;
224
+ const disposers: (() => void)[] = [];
225
+ (globalThis as any).__sigilEffectScope = disposers;
226
+ try {
227
+ fn();
228
+ } finally {
229
+ (globalThis as any).__sigilEffectScope = prev;
230
+ }
231
+ return () => {
232
+ for (const d of disposers) d();
233
+ disposers.length = 0;
234
+ };
233
235
  }
234
236
 
235
237
  export function createMemo<T>(fn: () => T): Signal<T> {
236
- const inner = createSignal<T>(undefined as T);
237
- createEffect(() => inner.set(fn()));
238
- return inner;
238
+ const inner = createSignal<T>(undefined as T);
239
+ createEffect(() => inner.set(fn()));
240
+ return inner;
239
241
  }
240
242
 
241
243
  /**
@@ -244,23 +246,23 @@ export function createMemo<T>(fn: () => T): Signal<T> {
244
246
  * When falling back to createElement, appends to parent if provided (SPA mode).
245
247
  */
246
248
  export function claim(
247
- nodes: ChildNode[],
248
- tag: string,
249
- parent?: Element,
249
+ nodes: ChildNode[],
250
+ tag: string,
251
+ parent?: Element,
250
252
  ): Element {
251
- for (let i = 0; i < nodes.length; i++) {
252
- const node = nodes[i];
253
- if (
254
- node instanceof Element &&
255
- node.tagName.toLowerCase() === tag.toLowerCase()
256
- ) {
257
- nodes.splice(i, 1);
258
- return node;
259
- }
260
- }
261
- const el = document.createElement(tag);
262
- if (parent) parent.appendChild(el);
263
- return el;
253
+ for (let i = 0; i < nodes.length; i++) {
254
+ const node = nodes[i];
255
+ if (
256
+ node instanceof Element &&
257
+ node.tagName.toLowerCase() === tag.toLowerCase()
258
+ ) {
259
+ nodes.splice(i, 1);
260
+ return node;
261
+ }
262
+ }
263
+ const el = document.createElement(tag);
264
+ if (parent) parent.appendChild(el);
265
+ return el;
264
266
  }
265
267
 
266
268
  /**
@@ -269,16 +271,16 @@ export function claim(
269
271
  * When falling back to createTextNode, appends to parent if provided (SPA mode).
270
272
  */
271
273
  export function claimText(nodes: ChildNode[], parent?: Element): Text {
272
- for (let i = 0; i < nodes.length; i++) {
273
- const node = nodes[i];
274
- if (node instanceof Text) {
275
- nodes.splice(i, 1);
276
- return node;
277
- }
278
- }
279
- const text = document.createTextNode("");
280
- if (parent) parent.appendChild(text);
281
- return text;
274
+ for (let i = 0; i < nodes.length; i++) {
275
+ const node = nodes[i];
276
+ if (node instanceof Text) {
277
+ nodes.splice(i, 1);
278
+ return node;
279
+ }
280
+ }
281
+ const text = document.createTextNode("");
282
+ if (parent) parent.appendChild(text);
283
+ return text;
282
284
  }
283
285
 
284
286
  /**
@@ -287,20 +289,20 @@ export function claimText(nodes: ChildNode[], parent?: Element): Text {
287
289
  * When falling back to createComment, appends to parent if provided (SPA mode).
288
290
  */
289
291
  export function claimComment(
290
- nodes: ChildNode[],
291
- text: string = "",
292
- parent?: Element,
292
+ nodes: ChildNode[],
293
+ text: string = "",
294
+ parent?: Element,
293
295
  ): Comment {
294
- for (let i = 0; i < nodes.length; i++) {
295
- const node = nodes[i];
296
- if (node instanceof Comment && node.data === text) {
297
- nodes.splice(i, 1);
298
- return node;
299
- }
300
- }
301
- const comment = document.createComment(text);
302
- if (parent) parent.appendChild(comment);
303
- return comment;
296
+ for (let i = 0; i < nodes.length; i++) {
297
+ const node = nodes[i];
298
+ if (node instanceof Comment && node.data === text) {
299
+ nodes.splice(i, 1);
300
+ return node;
301
+ }
302
+ }
303
+ const comment = document.createComment(text);
304
+ if (parent) parent.appendChild(comment);
305
+ return comment;
304
306
  }
305
307
 
306
308
  /**
@@ -311,33 +313,33 @@ export function claimComment(
311
313
  * or null to discard.
312
314
  */
313
315
  export function reconcile<T>(
314
- anchor: Partial<Comment>,
315
- keyMap: Map<unknown, Node>,
316
- newItems: T[],
317
- getKey: (item: T) => unknown,
318
- createNode: (item: T) => Node,
319
- // is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
316
+ anchor: Partial<Comment>,
317
+ keyMap: Map<unknown, Node>,
318
+ newItems: T[],
319
+ getKey: (item: T) => unknown,
320
+ createNode: (item: T) => Node,
321
+ // is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
320
322
  ): void {
321
- const newKeys = new Set(newItems.map(getKey));
322
-
323
- // Remove nodes no longer in list (or claim them for hydration)
324
- for (const [key, node] of keyMap) {
325
- if (!newKeys.has(key)) {
326
- node.parentNode?.removeChild(node);
327
- keyMap.delete(key);
328
- }
329
- }
330
-
331
- // Insert/move in order — anchor.before() handles both cases
332
- for (const item of newItems) {
333
- const key = getKey(item);
334
- let node = keyMap.get(key);
335
- if (!node) {
336
- node = createNode(item);
337
- keyMap.set(key, node);
338
- }
339
- anchor.before?.(node);
340
- }
323
+ const newKeys = new Set(newItems.map(getKey));
324
+
325
+ // Remove nodes no longer in list (or claim them for hydration)
326
+ for (const [key, node] of keyMap) {
327
+ if (!newKeys.has(key)) {
328
+ node.parentNode?.removeChild(node);
329
+ keyMap.delete(key);
330
+ }
331
+ }
332
+
333
+ // Insert/move in order — anchor.before() handles both cases
334
+ for (const item of newItems) {
335
+ const key = getKey(item);
336
+ let node = keyMap.get(key);
337
+ if (!node) {
338
+ node = createNode(item);
339
+ keyMap.set(key, node);
340
+ }
341
+ anchor.before?.(node);
342
+ }
341
343
  }
342
344
 
343
345
  /**
@@ -348,59 +350,59 @@ export function reconcile<T>(
348
350
  * builds the keyMap, removes from DOM, then reconcile re-inserts in order.
349
351
  */
350
352
  export function hydrateKeyedList<T>(
351
- nodesVar: ChildNode[],
352
- signalItems: T[],
353
- getKey: (item: T) => unknown,
354
- createNode: (item: T) => Node,
355
- parent?: Element,
353
+ nodesVar: ChildNode[],
354
+ signalItems: T[],
355
+ getKey: (item: T) => unknown,
356
+ createNode: (item: T) => Node,
357
+ parent?: Element,
356
358
  ): { anchor: Comment; keyMap: Map<unknown, Node> } {
357
- // 1. Claim the <!--g--> opening delimiter as anchor
358
- const anchor = claimComment(nodesVar, "g", parent);
359
-
360
- // 2. Walk siblings after anchor, collect elements up to <!--/g-->, build keyMap
361
- const keyMap = new Map<unknown, Node>();
362
- const anchorParent = anchor.parentNode;
363
- if (anchorParent && signalItems.length > 0) {
364
- const claimed: Element[] = [];
365
- let next = anchor.nextSibling;
366
- while (next) {
367
- const current = next;
368
- next = next.nextSibling;
369
- // Stop at closing <!--/g--> delimiter and consume it
370
- if (current instanceof Comment && current.data === "/g") {
371
- current.remove();
372
- break;
373
- }
374
- if (current instanceof Element) {
375
- current.remove();
376
- claimed.push(current);
377
- }
378
- }
379
- // Map claimed elements to keys by index
380
- for (let i = 0; i < claimed.length && i < signalItems.length; i++) {
381
- //@ts-expect-error i is always in bounds
382
- const key = getKey(signalItems[i]);
383
- //@ts-expect-error i is always in bounds
384
- keyMap.set(key, claimed[i]);
385
- }
386
- }
387
-
388
- // 3. Reconcile — keyMap is pre-populated with claimed nodes
389
- reconcile(anchor, keyMap, signalItems, getKey, createNode);
390
- return { anchor, keyMap };
359
+ // 1. Claim the <!--g--> opening delimiter as anchor
360
+ const anchor = claimComment(nodesVar, "g", parent);
361
+
362
+ // 2. Walk siblings after anchor, collect elements up to <!--/g-->, build keyMap
363
+ const keyMap = new Map<unknown, Node>();
364
+ const anchorParent = anchor.parentNode;
365
+ if (anchorParent && signalItems.length > 0) {
366
+ const claimed: Element[] = [];
367
+ let next = anchor.nextSibling;
368
+ while (next) {
369
+ const current = next;
370
+ next = next.nextSibling;
371
+ // Stop at closing <!--/g--> delimiter and consume it
372
+ if (current instanceof Comment && current.data === "/g") {
373
+ current.remove();
374
+ break;
375
+ }
376
+ if (current instanceof Element) {
377
+ current.remove();
378
+ claimed.push(current);
379
+ }
380
+ }
381
+ // Map claimed elements to keys by index
382
+ for (let i = 0; i < claimed.length && i < signalItems.length; i++) {
383
+ //@ts-expect-error i is always in bounds
384
+ const key = getKey(signalItems[i]);
385
+ //@ts-expect-error i is always in bounds
386
+ keyMap.set(key, claimed[i]);
387
+ }
388
+ }
389
+
390
+ // 3. Reconcile — keyMap is pre-populated with claimed nodes
391
+ reconcile(anchor, keyMap, signalItems, getKey, createNode);
392
+ return { anchor, keyMap };
391
393
  }
392
394
 
393
395
  export function insert(parent: Element, value: unknown): void {
394
- if (value === false || value === null || value === undefined) return;
395
- if (Array.isArray(value)) {
396
- for (const item of value) insert(parent, item);
397
- return;
398
- }
399
- if (value instanceof Node) {
400
- parent.appendChild(value);
401
- return;
402
- }
403
- parent.append(String(value));
396
+ if (value === false || value === null || value === undefined) return;
397
+ if (Array.isArray(value)) {
398
+ for (const item of value) insert(parent, item);
399
+ return;
400
+ }
401
+ if (value instanceof Node) {
402
+ parent.appendChild(value);
403
+ return;
404
+ }
405
+ parent.append(String(value));
404
406
  }
405
407
 
406
408
  /**
@@ -410,12 +412,17 @@ export function insert(parent: Element, value: unknown): void {
410
412
  */
411
413
  export type Directive = (node: Element, params: unknown) => void | (() => void);
412
414
 
413
- export function applyDirective(node: Element, directive: Directive | [Directive, unknown]): void {
414
- const [fn, params] = Array.isArray(directive) ? directive : [directive, undefined];
415
- const cleanup = fn(node, params);
416
- if (typeof cleanup === "function") {
417
- createEffect(() => () => cleanup());
418
- }
415
+ export function applyDirective(
416
+ node: Element,
417
+ directive: Directive | [Directive, unknown],
418
+ ): void {
419
+ const [fn, params] = Array.isArray(directive)
420
+ ? directive
421
+ : [directive, undefined];
422
+ const cleanup = fn(node, params);
423
+ if (typeof cleanup === "function") {
424
+ createEffect(() => () => cleanup());
425
+ }
419
426
  }
420
427
 
421
428
  export type ContextKey<T> = symbol & { __type: T };
@@ -424,21 +431,21 @@ const clientContextStore = new Map<symbol, unknown>();
424
431
 
425
432
  let storeProvider: (() => Map<symbol, unknown>) | null = null;
426
433
  export function __setStoreProvider(fn: () => Map<symbol, unknown>) {
427
- storeProvider = fn;
434
+ storeProvider = fn;
428
435
  }
429
436
 
430
437
  function getStore(): Map<symbol, unknown> {
431
- return storeProvider?.() ?? clientContextStore;
438
+ return storeProvider?.() ?? clientContextStore;
432
439
  }
433
440
 
434
441
  export function createContext<T>(): ContextKey<T> {
435
- return Symbol() as ContextKey<T>;
442
+ return Symbol() as ContextKey<T>;
436
443
  }
437
444
 
438
445
  export function setContext<T>(key: ContextKey<T>, value: T): void {
439
- getStore().set(key as unknown as symbol, value);
446
+ getStore().set(key as unknown as symbol, value);
440
447
  }
441
448
 
442
449
  export function getContext<T>(key: ContextKey<T>): T {
443
- return getStore().get(key as unknown as symbol) as T;
450
+ return getStore().get(key as unknown as symbol) as T;
444
451
  }
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigil-dev/runtime",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "exports": "./index.ts",
5
5
  "publish": {
6
6
  "exclude": ["src/**/*.test.ts"]
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.5.0",
5
+ "version": "0.6.0",
6
6
  "description": "Runtime for the Sigil framework",
7
7
  "private": false,
8
8
  "devDependencies": {