@sigil-dev/runtime 0.8.0 → 0.8.3

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 (3) hide show
  1. package/index.test.ts +1618 -1616
  2. package/index.ts +453 -437
  3. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,32 +1,32 @@
1
1
  export { Boundary } from "./boundary";
2
2
  export {
3
- easeInCubic,
4
- easeInOutCubic,
5
- easeInOutQuad,
6
- easeInOutQuart,
7
- easeInOutQuint,
8
- easeInOutSine,
9
- easeInQuad,
10
- easeInQuart,
11
- easeInQuint,
12
- easeInSine,
13
- easeOutCubic,
14
- easeOutQuad,
15
- easeOutQuart,
16
- easeOutQuint,
17
- easeOutSine,
18
- linear,
3
+ easeInCubic,
4
+ easeInOutCubic,
5
+ easeInOutQuad,
6
+ easeInOutQuart,
7
+ easeInOutQuint,
8
+ easeInOutSine,
9
+ easeInQuad,
10
+ easeInQuart,
11
+ easeInQuint,
12
+ easeInSine,
13
+ easeOutCubic,
14
+ easeOutQuad,
15
+ easeOutQuart,
16
+ easeOutQuint,
17
+ easeOutSine,
18
+ linear,
19
19
  } from "./easing.ts";
20
20
  export { ReactiveMap } from "./reactivity/map.ts";
21
21
  export { ReactiveSet } from "./reactivity/set.ts";
22
22
 
23
23
  export const SIGIL_SAFE = Symbol.for("sigil.safe");
24
24
  export class SafeHtml {
25
- [SIGIL_SAFE] = true;
26
- constructor(public v: string) { }
27
- toString() {
28
- return this.v;
29
- }
25
+ [SIGIL_SAFE] = true;
26
+ constructor(public v: string) {}
27
+ toString() {
28
+ return this.v;
29
+ }
30
30
  }
31
31
 
32
32
  let currentEffect: (() => void) | null = null;
@@ -38,348 +38,361 @@ const pendingEffects = new Set<() => void>();
38
38
  const pendingPreEffects = new Set<() => void>();
39
39
 
40
40
  function flush() {
41
- scheduledFlush = false;
42
- // biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
43
- pendingPreEffects.forEach((e) => e());
44
- pendingPreEffects.clear();
45
- // biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
46
- pendingEffects.forEach((e) => e());
47
- pendingEffects.clear();
41
+ scheduledFlush = false;
42
+ // biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
43
+ pendingPreEffects.forEach((e) => e());
44
+ pendingPreEffects.clear();
45
+ // biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
46
+ pendingEffects.forEach((e) => e());
47
+ pendingEffects.clear();
48
48
  }
49
49
 
50
50
  function scheduleEffect(fn: () => void) {
51
- const target = (fn as any).__sigil_pre ? pendingPreEffects : pendingEffects;
52
- target.add(fn);
53
- if (batchDepth === 0 && !scheduledFlush) {
54
- scheduledFlush = true;
55
- queueMicrotask(flush);
56
- }
51
+ const target = (fn as any).__sigil_pre ? pendingPreEffects : pendingEffects;
52
+ target.add(fn);
53
+ if (batchDepth === 0 && !scheduledFlush) {
54
+ scheduledFlush = true;
55
+ queueMicrotask(flush);
56
+ }
57
57
  }
58
58
 
59
59
  const targetDepsMap = new WeakMap<
60
- object,
61
- Map<string | symbol, Set<() => void>>
60
+ object,
61
+ Map<string | symbol, Set<() => void>>
62
62
  >();
63
63
 
64
64
  function getDeps(target: object, key: string | symbol): Set<() => void> {
65
- let deps = targetDepsMap.get(target);
66
- if (!deps) {
67
- deps = new Map();
68
- targetDepsMap.set(target, deps);
69
- }
70
- let set = deps.get(key);
71
- if (!set) {
72
- set = new Set();
73
- deps.set(key, set);
74
- }
75
- return set;
65
+ let deps = targetDepsMap.get(target);
66
+ if (!deps) {
67
+ deps = new Map();
68
+ targetDepsMap.set(target, deps);
69
+ }
70
+ let set = deps.get(key);
71
+ if (!set) {
72
+ set = new Set();
73
+ deps.set(key, set);
74
+ }
75
+ return set;
76
76
  }
77
77
 
78
78
  function track(target: object, key: string | symbol) {
79
- if (currentEffect && currentDeps) {
80
- const deps = getDeps(target, key);
81
- deps.add(currentEffect);
82
- currentDeps.add(deps); // Set, deduplicates automatically
83
- }
79
+ if (currentEffect && currentDeps) {
80
+ const deps = getDeps(target, key);
81
+ deps.add(currentEffect);
82
+ currentDeps.add(deps); // Set, deduplicates automatically
83
+ }
84
84
  }
85
85
 
86
86
  function trigger(target: object, key: string | symbol) {
87
- const deps = targetDepsMap.get(target);
88
- if (!deps) return;
89
- const set = deps.get(key);
90
- if (!set) return;
91
- for (const fn of [...set]) {
92
- scheduleEffect(fn);
93
- }
87
+ const deps = targetDepsMap.get(target);
88
+ if (!deps) return;
89
+ const set = deps.get(key);
90
+ if (!set) return;
91
+ for (const fn of [...set]) {
92
+ scheduleEffect(fn);
93
+ }
94
94
  }
95
95
 
96
96
  const proxyCache = new WeakMap<object, object>();
97
97
  const proxyToTarget = new WeakMap<object, object>();
98
98
  const reactiveTargets = new WeakSet<object>();
99
99
  const ARRAY_MUTATORS = new Set([
100
- "push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill"
100
+ "push",
101
+ "pop",
102
+ "shift",
103
+ "unshift",
104
+ "splice",
105
+ "sort",
106
+ "reverse",
107
+ "fill",
101
108
  ]);
102
109
 
103
110
  function createReactiveProxy<T extends object>(target: T): T {
104
- const existing = proxyCache.get(target);
105
- if (existing) return existing as T;
106
-
107
- const proxy = new Proxy(target, {
108
- get(target, key, receiver) {
109
- if (Array.isArray(target) && typeof key === "string" && ARRAY_MUTATORS.has(key)) {
110
- return (...args: any[]) => {
111
- const result = (target as any)[key](...args);
112
- trigger(target, "length");
113
- // trigger all numeric keys that changed — for splice/sort
114
- // triggering length is sufficient for most reactive patterns
115
- return result;
116
- };
117
- }
118
- if (key === "set") {
119
- return (newValue: T) => {
120
- const oldKeys = new Set(Object.keys(target));
121
- const newKeys = new Set(Object.keys(newValue));
122
- for (const k of oldKeys) {
123
- if (!newKeys.has(k)) {
124
- delete (target as any)[k];
125
- trigger(target, k);
126
- }
127
- }
128
- for (const k of newKeys) {
129
- (target as any)[k] = (newValue as any)[k];
130
- trigger(target, k);
131
- }
132
- if (Array.isArray(target)) {
133
- trigger(target, "length");
134
- }
135
- };
136
- }
137
-
138
- if (key === "peek") {
139
- return () => target;
140
- }
141
-
142
- if (typeof key === "symbol") {
143
- return Reflect.get(target, key, receiver);
144
- }
145
-
146
- track(target, key);
147
- const value = target[key as keyof T];
148
-
149
- if (typeof value === "object" && value !== null) {
150
- return createReactiveProxy(value);
151
- }
152
-
153
- return value;
154
- },
155
-
156
- set(target, key, newValue, receiver) {
157
- const oldValue = target[key as keyof T];
158
- const result = Reflect.set(target, key, newValue, receiver);
159
- if (oldValue !== newValue) {
160
- trigger(target, key as string | symbol);
161
- if (
162
- Array.isArray(target) &&
163
- typeof key === "string" &&
164
- !Number.isNaN(Number(key))
165
- ) {
166
- trigger(target, "length");
167
- }
168
- }
169
- return result;
170
- },
171
-
172
- deleteProperty(target, key) {
173
- const result = Reflect.deleteProperty(target, key);
174
- trigger(target, key as string | symbol);
175
- return result;
176
- },
177
- });
178
-
179
- proxyCache.set(target, proxy);
180
- proxyToTarget.set(proxy, target);
181
- reactiveTargets.add(target);
182
- return proxy as T;
111
+ const existing = proxyCache.get(target);
112
+ if (existing) return existing as T;
113
+
114
+ const proxy = new Proxy(target, {
115
+ get(target, key, receiver) {
116
+ if (
117
+ Array.isArray(target) &&
118
+ typeof key === "string" &&
119
+ ARRAY_MUTATORS.has(key)
120
+ ) {
121
+ return (...args: any[]) => {
122
+ const result = (target as any)[key](...args);
123
+ trigger(target, "length");
124
+ // trigger all numeric keys that changed — for splice/sort
125
+ // triggering length is sufficient for most reactive patterns
126
+ return result;
127
+ };
128
+ }
129
+ if (key === "set") {
130
+ return (newValue: T) => {
131
+ const oldKeys = new Set(Object.keys(target));
132
+ const newKeys = new Set(Object.keys(newValue));
133
+ for (const k of oldKeys) {
134
+ if (!newKeys.has(k)) {
135
+ delete (target as any)[k];
136
+ trigger(target, k);
137
+ }
138
+ }
139
+ for (const k of newKeys) {
140
+ (target as any)[k] = (newValue as any)[k];
141
+ trigger(target, k);
142
+ }
143
+ if (Array.isArray(target)) {
144
+ trigger(target, "length");
145
+ }
146
+ };
147
+ }
148
+
149
+ if (key === "peek") {
150
+ return () => target;
151
+ }
152
+
153
+ if (typeof key === "symbol") {
154
+ return Reflect.get(target, key, receiver);
155
+ }
156
+
157
+ track(target, key);
158
+ const value = target[key as keyof T];
159
+
160
+ if (typeof value === "object" && value !== null) {
161
+ return createReactiveProxy(value);
162
+ }
163
+
164
+ return value;
165
+ },
166
+
167
+ set(target, key, newValue, receiver) {
168
+ const oldValue = target[key as keyof T];
169
+ const result = Reflect.set(target, key, newValue, receiver);
170
+ if (oldValue !== newValue) {
171
+ trigger(target, key as string | symbol);
172
+ if (
173
+ Array.isArray(target) &&
174
+ typeof key === "string" &&
175
+ !Number.isNaN(Number(key))
176
+ ) {
177
+ trigger(target, "length");
178
+ }
179
+ }
180
+ return result;
181
+ },
182
+
183
+ deleteProperty(target, key) {
184
+ const result = Reflect.deleteProperty(target, key);
185
+ trigger(target, key as string | symbol);
186
+ return result;
187
+ },
188
+ });
189
+
190
+ proxyCache.set(target, proxy);
191
+ proxyToTarget.set(proxy, target);
192
+ reactiveTargets.add(target);
193
+ return proxy as T;
183
194
  }
184
195
 
185
196
  // ─── createSignal (branches on type) ───
186
197
 
187
198
  export type Signal<T> = {
188
- (): T;
189
- set: (next: T) => void;
190
- peek: () => T;
199
+ (): T;
200
+ set: (next: T) => void;
201
+ peek: () => T;
191
202
  };
192
203
 
193
204
  export function batch(fn: () => void) {
194
- batchDepth++;
195
- fn();
196
- batchDepth--;
197
- if (batchDepth === 0) {
198
- scheduledFlush = false;
199
- flush();
200
- }
205
+ batchDepth++;
206
+ fn();
207
+ batchDepth--;
208
+ if (batchDepth === 0) {
209
+ scheduledFlush = false;
210
+ flush();
211
+ }
201
212
  }
202
213
 
203
214
  export function tick(): Promise<void> {
204
- return new Promise((resolve) => queueMicrotask(resolve));
215
+ return new Promise((resolve) => queueMicrotask(resolve));
205
216
  }
206
217
 
207
218
  function createPrimitiveSignal<T>(initial: T): Signal<T> {
208
- const subs = new Set<() => void>();
209
- let value = initial;
219
+ const subs = new Set<() => void>();
220
+ let value = initial;
210
221
 
211
- const getter = () => {
212
- if (currentEffect && currentDeps) {
213
- subs.add(currentEffect);
214
- currentDeps.add(subs);
215
- }
216
- return value;
217
- };
222
+ const getter = () => {
223
+ if (currentEffect && currentDeps) {
224
+ subs.add(currentEffect);
225
+ currentDeps.add(subs);
226
+ }
227
+ return value;
228
+ };
218
229
 
219
- getter.set = (next: T) => {
220
- value = next;
221
- for (const fn of [...subs]) {
222
- scheduleEffect(fn);
223
- }
224
- };
230
+ getter.set = (next: T) => {
231
+ value = next;
232
+ for (const fn of [...subs]) {
233
+ scheduleEffect(fn);
234
+ }
235
+ };
225
236
 
226
- getter.peek = () => value;
237
+ getter.peek = () => value;
227
238
 
228
- return getter;
239
+ return getter;
229
240
  }
230
241
 
231
242
  export function createSignal<T>(initial: T): Signal<T> {
232
- if (typeof initial === "object" && initial !== null) {
233
- const proxy = createReactiveProxy(initial);
234
- const getter = () => {
235
- // array signals: auto-track length so push/pop/splice re-run effects
236
- if (Array.isArray(initial)) {
237
- track(initial, "length");
238
- }
239
- return proxy;
240
- };
241
- getter.set = (next: T) => (proxy as any).set(next);
242
- getter.peek = () => initial;
243
- return getter;
244
- }
245
-
246
- return createPrimitiveSignal(initial);
243
+ if (typeof initial === "object" && initial !== null) {
244
+ const proxy = createReactiveProxy(initial);
245
+ const getter = () => {
246
+ // array signals: auto-track length so push/pop/splice re-run effects
247
+ if (Array.isArray(initial)) {
248
+ track(initial, "length");
249
+ }
250
+ return proxy;
251
+ };
252
+ getter.set = (next: T) => (proxy as any).set(next);
253
+ getter.peek = () => initial;
254
+ return getter;
255
+ }
256
+
257
+ return createPrimitiveSignal(initial);
247
258
  }
248
259
 
249
260
  // A1: $state.raw — create signal without Proxy wrapping for objects/arrays
250
261
  export function createRawSignal<T>(initial: T): Signal<T> {
251
- return createPrimitiveSignal(initial);
262
+ return createPrimitiveSignal(initial);
252
263
  }
253
264
 
254
265
  // A2: $state.snapshot — deep un-proxy a signal value
255
266
  function deepUnproxy<T>(value: T): T {
256
- if (typeof value !== "object" || value === null) return value;
257
- const raw = proxyToTarget.get(value as object);
258
- if (!raw) return value; // not a proxy, return as-is
259
- if (Array.isArray(raw)) {
260
- return raw.map(deepUnproxy) as T;
261
- }
262
- const result: Record<string, unknown> = {};
263
- for (const key of Object.keys(raw)) {
264
- result[key] = deepUnproxy((raw as Record<string, unknown>)[key]);
265
- }
266
- return result as T;
267
+ if (typeof value !== "object" || value === null) return value;
268
+ const raw = proxyToTarget.get(value as object);
269
+ if (!raw) return value; // not a proxy, return as-is
270
+ if (Array.isArray(raw)) {
271
+ return raw.map(deepUnproxy) as T;
272
+ }
273
+ const result: Record<string, unknown> = {};
274
+ for (const key of Object.keys(raw)) {
275
+ result[key] = deepUnproxy((raw as Record<string, unknown>)[key]);
276
+ }
277
+ return result as T;
267
278
  }
268
279
 
269
280
  export function snapshot<T>(value: T): T {
270
- return deepUnproxy(value);
281
+ return deepUnproxy(value);
271
282
  }
272
283
 
273
284
  // A3: $effect.tracking() — check if inside reactive context
274
285
  export function tracking(): boolean {
275
- return currentEffect !== null;
286
+ return currentEffect !== null;
276
287
  }
277
288
 
278
289
  // A4: $inspect — dev-only effect logging
279
290
  export function createInspectEffect(
280
- getValues: () => unknown[],
281
- cb?: (vals: unknown[]) => void,
291
+ getValues: () => unknown[],
292
+ cb?: (vals: unknown[]) => void,
282
293
  ): void {
283
- if (typeof process !== "undefined" && process.env.NODE_ENV === "production")
284
- return;
285
- createEffect(() => {
286
- const vals = getValues();
287
- if (cb) cb(vals);
288
- else console.log(...vals);
289
- });
294
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "production")
295
+ return;
296
+ createEffect(() => {
297
+ const vals = getValues();
298
+ if (cb) cb(vals);
299
+ else console.log(...vals);
300
+ });
290
301
  }
291
302
 
292
303
  // A5: $effect.pre — effect that flushes before DOM update
293
- export function createEffectPre(fn: () => void | (() => void)): () => void {
294
- let cleanup: (() => void) | void;
295
- const ownDeps = new Set<Set<() => void>>();
296
-
297
- const effect = () => {
298
- if (cleanup) cleanup();
299
- for (const depSet of ownDeps) depSet.delete(effect);
300
- ownDeps.clear();
301
-
302
- const prevEffect = currentEffect;
303
- const prevDeps = currentDeps;
304
- currentEffect = effect;
305
- currentDeps = ownDeps;
306
- try {
307
- cleanup = fn();
308
- } finally {
309
- currentEffect = prevEffect;
310
- currentDeps = prevDeps;
311
- }
312
- };
313
-
314
- (effect as any).__sigil_pre = true;
315
- effect();
316
-
317
- const dispose = () => {
318
- for (const depSet of ownDeps) depSet.delete(effect);
319
- ownDeps.clear();
320
- if (cleanup) cleanup();
321
- };
322
-
323
- ((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
324
- dispose,
325
- );
326
- return dispose;
304
+ export function createEffectPre(
305
+ fn: () => undefined | (() => void),
306
+ ): () => void {
307
+ let cleanup: (() => void) | undefined;
308
+ const ownDeps = new Set<Set<() => void>>();
309
+
310
+ const effect = () => {
311
+ if (cleanup) cleanup();
312
+ for (const depSet of ownDeps) depSet.delete(effect);
313
+ ownDeps.clear();
314
+
315
+ const prevEffect = currentEffect;
316
+ const prevDeps = currentDeps;
317
+ currentEffect = effect;
318
+ currentDeps = ownDeps;
319
+ try {
320
+ cleanup = fn();
321
+ } finally {
322
+ currentEffect = prevEffect;
323
+ currentDeps = prevDeps;
324
+ }
325
+ };
326
+
327
+ (effect as any).__sigil_pre = true;
328
+ effect();
329
+
330
+ const dispose = () => {
331
+ for (const depSet of ownDeps) depSet.delete(effect);
332
+ ownDeps.clear();
333
+ if (cleanup) cleanup();
334
+ };
335
+
336
+ ((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
337
+ dispose,
338
+ );
339
+ return dispose;
327
340
  }
328
341
 
329
342
  // A9: Profiler effect timing (dev)
330
343
  export function createProfilerEffect(
331
- name: string,
332
- fn: () => void | (() => void),
344
+ name: string,
345
+ fn: () => undefined | (() => void),
333
346
  ): () => void {
334
- return createEffect(() => {
335
- if (
336
- typeof process !== "undefined" &&
337
- process.env.NODE_ENV === "production"
338
- ) {
339
- return fn();
340
- }
341
- const start = performance.now();
342
- const cleanup = fn();
343
- const end = performance.now();
344
- console.log(`[Profiler] ${name}: ${(end - start).toFixed(2)}ms`);
345
- return cleanup;
346
- });
347
+ return createEffect(() => {
348
+ if (
349
+ typeof process !== "undefined" &&
350
+ process.env.NODE_ENV === "production"
351
+ ) {
352
+ return fn();
353
+ }
354
+ const start = performance.now();
355
+ const cleanup = fn();
356
+ const end = performance.now();
357
+ console.log(`[Profiler] ${name}: ${(end - start).toFixed(2)}ms`);
358
+ return cleanup;
359
+ });
347
360
  }
348
361
 
349
362
  // biome-ignore lint/suspicious/noConfusingVoidType: please shut up its a weird type
350
363
  export function createEffect(fn: () => void | (() => void)): () => void {
351
- let cleanup: (() => void) | void;
352
- const ownDeps = new Set<Set<() => void>>();
353
-
354
- const effect = () => {
355
- if (cleanup) cleanup();
356
- for (const depSet of ownDeps) depSet.delete(effect);
357
- ownDeps.clear();
358
-
359
- const prevEffect = currentEffect;
360
- const prevDeps = currentDeps;
361
- currentEffect = effect;
362
- currentDeps = ownDeps;
363
- try {
364
- cleanup = fn();
365
- } finally {
366
- currentEffect = prevEffect;
367
- currentDeps = prevDeps;
368
- }
369
- };
370
-
371
- effect();
372
-
373
- const dispose = () => {
374
- for (const depSet of ownDeps) depSet.delete(effect);
375
- ownDeps.clear();
376
- if (cleanup) cleanup();
377
- };
378
-
379
- ((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
380
- dispose,
381
- );
382
- return dispose;
364
+ let cleanup: (() => void) | undefined;
365
+ const ownDeps = new Set<Set<() => void>>();
366
+
367
+ const effect = () => {
368
+ if (cleanup) cleanup();
369
+ for (const depSet of ownDeps) depSet.delete(effect);
370
+ ownDeps.clear();
371
+
372
+ const prevEffect = currentEffect;
373
+ const prevDeps = currentDeps;
374
+ currentEffect = effect;
375
+ currentDeps = ownDeps;
376
+ try {
377
+ cleanup = fn();
378
+ } finally {
379
+ currentEffect = prevEffect;
380
+ currentDeps = prevDeps;
381
+ }
382
+ };
383
+
384
+ effect();
385
+
386
+ const dispose = () => {
387
+ for (const depSet of ownDeps) depSet.delete(effect);
388
+ ownDeps.clear();
389
+ if (cleanup) cleanup();
390
+ };
391
+
392
+ ((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
393
+ dispose,
394
+ );
395
+ return dispose;
383
396
  }
384
397
 
385
398
  /**
@@ -388,24 +401,24 @@ export function createEffect(fn: () => void | (() => void)): () => void {
388
401
  * Supports nesting: inner scopes don't leak to outer ones.
389
402
  */
390
403
  export function withEffectScope(fn: () => void): () => void {
391
- const prev = (globalThis as any).__sigilEffectScope;
392
- const disposers: (() => void)[] = [];
393
- (globalThis as any).__sigilEffectScope = disposers;
394
- try {
395
- fn();
396
- } finally {
397
- (globalThis as any).__sigilEffectScope = prev;
398
- }
399
- return () => {
400
- for (const d of disposers) d();
401
- disposers.length = 0;
402
- };
404
+ const prev = (globalThis as any).__sigilEffectScope;
405
+ const disposers: (() => void)[] = [];
406
+ (globalThis as any).__sigilEffectScope = disposers;
407
+ try {
408
+ fn();
409
+ } finally {
410
+ (globalThis as any).__sigilEffectScope = prev;
411
+ }
412
+ return () => {
413
+ for (const d of disposers) d();
414
+ disposers.length = 0;
415
+ };
403
416
  }
404
417
 
405
418
  export function createMemo<T>(fn: () => T): Signal<T> {
406
- const inner = createSignal<T>(undefined as T);
407
- createEffect(() => inner.set(fn()));
408
- return inner;
419
+ const inner = createSignal<T>(undefined as T);
420
+ createEffect(() => inner.set(fn()));
421
+ return inner;
409
422
  }
410
423
 
411
424
  /**
@@ -414,23 +427,23 @@ export function createMemo<T>(fn: () => T): Signal<T> {
414
427
  * When falling back to createElement, appends to parent if provided (SPA mode).
415
428
  */
416
429
  export function claim(
417
- nodes: ChildNode[],
418
- tag: string,
419
- parent?: Element,
430
+ nodes: ChildNode[],
431
+ tag: string,
432
+ parent?: Element,
420
433
  ): Element {
421
- for (let i = 0; i < nodes.length; i++) {
422
- const node = nodes[i];
423
- if (
424
- node instanceof Element &&
425
- node.tagName.toLowerCase() === tag.toLowerCase()
426
- ) {
427
- nodes.splice(i, 1);
428
- return node;
429
- }
430
- }
431
- const el = document.createElement(tag);
432
- if (parent) parent.appendChild(el);
433
- return el;
434
+ for (let i = 0; i < nodes.length; i++) {
435
+ const node = nodes[i];
436
+ if (
437
+ node instanceof Element &&
438
+ node.tagName.toLowerCase() === tag.toLowerCase()
439
+ ) {
440
+ nodes.splice(i, 1);
441
+ return node;
442
+ }
443
+ }
444
+ const el = document.createElement(tag);
445
+ if (parent) parent.appendChild(el);
446
+ return el;
434
447
  }
435
448
 
436
449
  /**
@@ -439,16 +452,16 @@ export function claim(
439
452
  * When falling back to createTextNode, appends to parent if provided (SPA mode).
440
453
  */
441
454
  export function claimText(nodes: ChildNode[], parent?: Element): Text {
442
- for (let i = 0; i < nodes.length; i++) {
443
- const node = nodes[i];
444
- if (node instanceof Text) {
445
- nodes.splice(i, 1);
446
- return node;
447
- }
448
- }
449
- const text = document.createTextNode("");
450
- if (parent) parent.appendChild(text);
451
- return text;
455
+ for (let i = 0; i < nodes.length; i++) {
456
+ const node = nodes[i];
457
+ if (node instanceof Text) {
458
+ nodes.splice(i, 1);
459
+ return node;
460
+ }
461
+ }
462
+ const text = document.createTextNode("");
463
+ if (parent) parent.appendChild(text);
464
+ return text;
452
465
  }
453
466
 
454
467
  /**
@@ -457,20 +470,20 @@ export function claimText(nodes: ChildNode[], parent?: Element): Text {
457
470
  * When falling back to createComment, appends to parent if provided (SPA mode).
458
471
  */
459
472
  export function claimComment(
460
- nodes: ChildNode[],
461
- text: string = "",
462
- parent?: Element,
473
+ nodes: ChildNode[],
474
+ text: string = "",
475
+ parent?: Element,
463
476
  ): Comment {
464
- for (let i = 0; i < nodes.length; i++) {
465
- const node = nodes[i];
466
- if (node instanceof Comment && node.data === text) {
467
- nodes.splice(i, 1);
468
- return node;
469
- }
470
- }
471
- const comment = document.createComment(text);
472
- if (parent) parent.appendChild(comment);
473
- return comment;
477
+ for (let i = 0; i < nodes.length; i++) {
478
+ const node = nodes[i];
479
+ if (node instanceof Comment && node.data === text) {
480
+ nodes.splice(i, 1);
481
+ return node;
482
+ }
483
+ }
484
+ const comment = document.createComment(text);
485
+ if (parent) parent.appendChild(comment);
486
+ return comment;
474
487
  }
475
488
 
476
489
  /**
@@ -481,33 +494,33 @@ export function claimComment(
481
494
  * or null to discard.
482
495
  */
483
496
  export function reconcile<T>(
484
- anchor: Partial<Comment>,
485
- keyMap: Map<unknown, Node>,
486
- newItems: T[],
487
- getKey: (item: T) => unknown,
488
- createNode: (item: T) => Node,
489
- // is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
497
+ anchor: Partial<Comment>,
498
+ keyMap: Map<unknown, Node>,
499
+ newItems: T[],
500
+ getKey: (item: T) => unknown,
501
+ createNode: (item: T) => Node,
502
+ // is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
490
503
  ): void {
491
- const newKeys = new Set(newItems.map(getKey));
492
-
493
- // Remove nodes no longer in list (or claim them for hydration)
494
- for (const [key, node] of keyMap) {
495
- if (!newKeys.has(key)) {
496
- node.parentNode?.removeChild(node);
497
- keyMap.delete(key);
498
- }
499
- }
500
-
501
- // Insert/move in order — anchor.before() handles both cases
502
- for (const item of newItems) {
503
- const key = getKey(item);
504
- let node = keyMap.get(key);
505
- if (!node) {
506
- node = createNode(item);
507
- keyMap.set(key, node);
508
- }
509
- anchor.before?.(node);
510
- }
504
+ const newKeys = new Set(newItems.map(getKey));
505
+
506
+ // Remove nodes no longer in list (or claim them for hydration)
507
+ for (const [key, node] of keyMap) {
508
+ if (!newKeys.has(key)) {
509
+ node.parentNode?.removeChild(node);
510
+ keyMap.delete(key);
511
+ }
512
+ }
513
+
514
+ // Insert/move in order — anchor.before() handles both cases
515
+ for (const item of newItems) {
516
+ const key = getKey(item);
517
+ let node = keyMap.get(key);
518
+ if (!node) {
519
+ node = createNode(item);
520
+ keyMap.set(key, node);
521
+ }
522
+ anchor.before?.(node);
523
+ }
511
524
  }
512
525
 
513
526
  /**
@@ -518,63 +531,63 @@ export function reconcile<T>(
518
531
  * builds the keyMap, removes from DOM, then reconcile re-inserts in order.
519
532
  */
520
533
  export function hydrateKeyedList<T>(
521
- nodesVar: ChildNode[],
522
- signalItems: T[],
523
- getKey: (item: T) => unknown,
524
- createNode: (item: T) => Node,
525
- parent?: Element,
534
+ nodesVar: ChildNode[],
535
+ signalItems: T[],
536
+ getKey: (item: T) => unknown,
537
+ createNode: (item: T) => Node,
538
+ parent?: Element,
526
539
  ): { anchor: Comment; keyMap: Map<unknown, Node> } {
527
- // 1. Claim the <!--g--> opening delimiter as anchor
528
- const anchor = claimComment(nodesVar, "g", parent);
529
-
530
- // 2. Walk siblings after anchor, collect elements up to <!--/g-->, build keyMap
531
- const keyMap = new Map<unknown, Node>();
532
- const anchorParent = anchor.parentNode;
533
- if (anchorParent && signalItems.length > 0) {
534
- const claimed: Element[] = [];
535
- let next = anchor.nextSibling;
536
- while (next) {
537
- const current = next;
538
- next = next.nextSibling;
539
- // Stop at closing <!--/g--> delimiter and consume it
540
- if (current instanceof Comment && current.data === "/g") {
541
- current.remove();
542
- break;
543
- }
544
- if (current instanceof Element) {
545
- current.remove();
546
- claimed.push(current);
547
- }
548
- }
549
- // Map claimed elements to keys by index
550
- for (let i = 0; i < claimed.length && i < signalItems.length; i++) {
551
- //@ts-expect-error i is always in bounds
552
- const key = getKey(signalItems[i]);
553
- //@ts-expect-error i is always in bounds
554
- keyMap.set(key, claimed[i]);
555
- }
556
- }
557
-
558
- // 3. Reconcile — keyMap is pre-populated with claimed nodes
559
- reconcile(anchor, keyMap, signalItems, getKey, createNode);
560
- return { anchor, keyMap };
540
+ // 1. Claim the <!--g--> opening delimiter as anchor
541
+ const anchor = claimComment(nodesVar, "g", parent);
542
+
543
+ // 2. Walk siblings after anchor, collect elements up to <!--/g-->, build keyMap
544
+ const keyMap = new Map<unknown, Node>();
545
+ const anchorParent = anchor.parentNode;
546
+ if (anchorParent && signalItems.length > 0) {
547
+ const claimed: Element[] = [];
548
+ let next = anchor.nextSibling;
549
+ while (next) {
550
+ const current = next;
551
+ next = next.nextSibling;
552
+ // Stop at closing <!--/g--> delimiter and consume it
553
+ if (current instanceof Comment && current.data === "/g") {
554
+ current.remove();
555
+ break;
556
+ }
557
+ if (current instanceof Element) {
558
+ current.remove();
559
+ claimed.push(current);
560
+ }
561
+ }
562
+ // Map claimed elements to keys by index
563
+ for (let i = 0; i < claimed.length && i < signalItems.length; i++) {
564
+ //@ts-expect-error i is always in bounds
565
+ const key = getKey(signalItems[i]);
566
+ //@ts-expect-error i is always in bounds
567
+ keyMap.set(key, claimed[i]);
568
+ }
569
+ }
570
+
571
+ // 3. Reconcile — keyMap is pre-populated with claimed nodes
572
+ reconcile(anchor, keyMap, signalItems, getKey, createNode);
573
+ return { anchor, keyMap };
561
574
  }
562
575
 
563
576
  export function insert(parent: Element, value: unknown): void {
564
- if (value === false || value === null || value === undefined) return;
565
- if (typeof value === "function") {
566
- insert(parent, value());
567
- return;
568
- }
569
- if (Array.isArray(value)) {
570
- for (const item of value) insert(parent, item);
571
- return;
572
- }
573
- if (value instanceof Node) {
574
- parent.appendChild(value);
575
- return;
576
- }
577
- parent.append(String(value));
577
+ if (value === false || value === null || value === undefined) return;
578
+ if (typeof value === "function") {
579
+ insert(parent, value());
580
+ return;
581
+ }
582
+ if (Array.isArray(value)) {
583
+ for (const item of value) insert(parent, item);
584
+ return;
585
+ }
586
+ if (value instanceof Node) {
587
+ parent.appendChild(value);
588
+ return;
589
+ }
590
+ parent.append(String(value));
578
591
  }
579
592
 
580
593
  /**
@@ -582,19 +595,22 @@ export function insert(parent: Element, value: unknown): void {
582
595
  * The directive function receives (element, params) and may return a cleanup function.
583
596
  * Cleanup runs when the enclosing $effect re-runs or the component is destroyed.
584
597
  */
585
- export type Directive = (node: Element, params: unknown) => void | (() => void);
598
+ export type Directive = (
599
+ node: Element,
600
+ params: unknown,
601
+ ) => undefined | (() => void);
586
602
 
587
603
  export function applyDirective(
588
- node: Element,
589
- directive: Directive | [Directive, unknown],
604
+ node: Element,
605
+ directive: Directive | [Directive, unknown],
590
606
  ): void {
591
- const [fn, params] = Array.isArray(directive)
592
- ? directive
593
- : [directive, undefined];
594
- const cleanup = fn(node, params);
595
- if (typeof cleanup === "function") {
596
- createEffect(() => () => cleanup());
597
- }
607
+ const [fn, params] = Array.isArray(directive)
608
+ ? directive
609
+ : [directive, undefined];
610
+ const cleanup = fn(node, params);
611
+ if (typeof cleanup === "function") {
612
+ createEffect(() => () => cleanup());
613
+ }
598
614
  }
599
615
 
600
616
  export type ContextKey<T> = symbol & { __type: T };
@@ -603,35 +619,35 @@ const clientContextStore = new Map<symbol, unknown>();
603
619
 
604
620
  let storeProvider: (() => Map<symbol, unknown>) | null = null;
605
621
  export function __setStoreProvider(fn: () => Map<symbol, unknown>) {
606
- storeProvider = fn;
622
+ storeProvider = fn;
607
623
  }
608
624
 
609
625
  function getStore(): Map<symbol, unknown> {
610
- return storeProvider?.() ?? clientContextStore;
626
+ return storeProvider?.() ?? clientContextStore;
611
627
  }
612
628
 
613
629
  export function createContext<T>(): ContextKey<T> {
614
- return Symbol() as ContextKey<T>;
630
+ return Symbol() as ContextKey<T>;
615
631
  }
616
632
 
617
633
  export function setContext<T>(key: ContextKey<T>, value: T): void {
618
- getStore().set(key as unknown as symbol, value);
634
+ getStore().set(key as unknown as symbol, value);
619
635
  }
620
636
 
621
637
  export function getContext<T>(key: ContextKey<T>): T {
622
- return getStore().get(key as unknown as symbol) as T;
638
+ return getStore().get(key as unknown as symbol) as T;
623
639
  }
624
640
 
625
641
  const _hydrationStack: ChildNode[][] = [];
626
642
 
627
643
  export function pushHydrationNodes(nodes: ChildNode[]): void {
628
- _hydrationStack.push(nodes);
644
+ _hydrationStack.push(nodes);
629
645
  }
630
646
 
631
647
  export function popHydrationNodes(): void {
632
- _hydrationStack.pop();
648
+ _hydrationStack.pop();
633
649
  }
634
650
 
635
651
  export function getHydrationNodes(): ChildNode[] {
636
- return _hydrationStack[_hydrationStack.length - 1] ?? [];
652
+ return _hydrationStack[_hydrationStack.length - 1] ?? [];
637
653
  }