@sigil-dev/runtime 0.9.5 → 0.9.7

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/README.md +140 -140
  2. package/index.test.ts +44 -0
  3. package/index.ts +901 -779
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,140 +1,140 @@
1
- # @sigil-dev/runtime
2
-
3
- Signals-based reactivity for the DOM using direct mutations.
4
-
5
- ```bash
6
- bun add @sigil-dev/runtime
7
- ```
8
-
9
- ## What it is
10
-
11
- A small reactive primitive library. Signals track dependencies automatically — when a value changes, only the effects that read it re-run.
12
- Objects and arrays use Proxy for deep reactivity without any special syntax.
13
-
14
- Inspired by Solid's reactive model.
15
-
16
- ## Primitives
17
-
18
- ### `createSignal`
19
-
20
- ```typescript
21
- import { createSignal } from "@sigil-dev/runtime";
22
-
23
- const count = createSignal(0);
24
-
25
- count() // read — 0
26
- count.set(5) // write
27
- count.peek() // read without tracking
28
- ```
29
-
30
- Objects and arrays are deeply reactive via Proxy and you mutate them directly:
31
-
32
- ```typescript
33
- const user = createSignal({ name: "Rei", score: 0 });
34
-
35
- user().score++ // triggers effects that read score
36
- user().name = "Asuka" // triggers effects that read name
37
-
38
- user.set({ name: "Misato", score: 100 }) // replace entire object
39
- ```
40
-
41
- ### `createEffect`
42
-
43
- Runs immediately, re-runs when any signal it read changes. Returns a dispose function.
44
-
45
- ```typescript
46
- import { createEffect } from "@sigil-dev/runtime";
47
-
48
- const dispose = createEffect(() => {
49
- document.title = `Score: ${user().score}`;
50
- return () => console.log("cleanup before rerun");
51
- });
52
-
53
- dispose(); // stop tracking
54
- ```
55
-
56
- Dependencies are tracked automatically. If a signal is conditionally read, the dependency is updated on each run — no stale subscriptions.
57
-
58
- ### `createMemo`
59
-
60
- A derived signal. Cached until its dependencies change.
61
-
62
- ```typescript
63
- import { createMemo } from "@sigil-dev/runtime";
64
-
65
- const fullName = createMemo(() => `${first()} ${last()}`);
66
- fullName() // read like any other signal
67
- ```
68
-
69
- ### `batch`
70
-
71
- Defer effect notifications until a block completes. Useful for multiple related writes.
72
-
73
- ```typescript
74
- import { batch } from "@sigil-dev/runtime";
75
-
76
- batch(() => {
77
- x.set(1);
78
- y.set(2);
79
- // effects fire once, not twice
80
- });
81
- ```
82
-
83
- ### `withEffectScope`
84
-
85
- Group effects so they can all be torn down at once. Essential for component lifecycle.
86
-
87
- ```typescript
88
- import { withEffectScope } from "@sigil-dev/runtime";
89
-
90
- const dispose = withEffectScope(() => {
91
- createEffect(() => console.log(x()));
92
- createEffect(() => console.log(y()));
93
- });
94
-
95
- dispose(); // kills both
96
- ```
97
-
98
- Scopes nest correctly. Inner dispose does not affect outer scope.
99
-
100
- ### Context
101
-
102
- ```typescript
103
- import { createContext, setContext, getContext } from "@sigil-dev/runtime";
104
-
105
- const ThemeKey = createContext<"light" | "dark">();
106
-
107
- setContext(ThemeKey, "dark");
108
- getContext(ThemeKey); // "dark"
109
- ```
110
-
111
- ### Hydration utilities
112
- > **NOTE:** You probably dont want to use this manually.
113
-
114
- `claim`, `claimText`, `claimComment`, `reconcile`, `hydrateKeyedList` are used by the Sigil compiler for SSR hydration. Claim existing server-rendered DOM nodes instead of creating new ones.
115
-
116
- ```typescript
117
- import { claim, claimText, reconcile } from "@sigil-dev/runtime";
118
-
119
- // claim an existing <div> from an SSR pool instead of createElement
120
- const el = claim(nodes, "div", parent);
121
- ```
122
-
123
- ## Design
124
-
125
- No virtual DOM. Effects write directly to DOM nodes. The update path is:
126
-
127
- ```
128
- signal.set(newValue)
129
- → notify subscribers
130
- → effect re-runs
131
- → direct DOM mutation
132
- ```
133
-
134
- If a signal changes, the effects that depend on it run synchronously (or are batched if inside `batch()`).
135
-
136
- Deep reactivity on objects uses Proxy rather than explicit getter/setter pairs. This means you can pass a signal's value to any code that expects a plain object and mutations will still be tracked.
137
-
138
- ## Used by
139
-
140
- `@sigil-dev/compiler` compiles `$state`, `$derived`, and `$effect` macros down to these primitives at build time. You can use this library directly if you want explicit control or are building outside the Sigil compiler pipeline.
1
+ # @sigil-dev/runtime
2
+
3
+ Signals-based reactivity for the DOM using direct mutations.
4
+
5
+ ```bash
6
+ bun add @sigil-dev/runtime
7
+ ```
8
+
9
+ ## What it is
10
+
11
+ A small reactive primitive library. Signals track dependencies automatically — when a value changes, only the effects that read it re-run.
12
+ Objects and arrays use Proxy for deep reactivity without any special syntax.
13
+
14
+ Inspired by Solid's reactive model.
15
+
16
+ ## Primitives
17
+
18
+ ### `createSignal`
19
+
20
+ ```typescript
21
+ import { createSignal } from "@sigil-dev/runtime";
22
+
23
+ const count = createSignal(0);
24
+
25
+ count() // read — 0
26
+ count.set(5) // write
27
+ count.peek() // read without tracking
28
+ ```
29
+
30
+ Objects and arrays are deeply reactive via Proxy and you mutate them directly:
31
+
32
+ ```typescript
33
+ const user = createSignal({ name: "Rei", score: 0 });
34
+
35
+ user().score++ // triggers effects that read score
36
+ user().name = "Asuka" // triggers effects that read name
37
+
38
+ user.set({ name: "Misato", score: 100 }) // replace entire object
39
+ ```
40
+
41
+ ### `createEffect`
42
+
43
+ Runs immediately, re-runs when any signal it read changes. Returns a dispose function.
44
+
45
+ ```typescript
46
+ import { createEffect } from "@sigil-dev/runtime";
47
+
48
+ const dispose = createEffect(() => {
49
+ document.title = `Score: ${user().score}`;
50
+ return () => console.log("cleanup before rerun");
51
+ });
52
+
53
+ dispose(); // stop tracking
54
+ ```
55
+
56
+ Dependencies are tracked automatically. If a signal is conditionally read, the dependency is updated on each run — no stale subscriptions.
57
+
58
+ ### `createMemo`
59
+
60
+ A derived signal. Cached until its dependencies change.
61
+
62
+ ```typescript
63
+ import { createMemo } from "@sigil-dev/runtime";
64
+
65
+ const fullName = createMemo(() => `${first()} ${last()}`);
66
+ fullName() // read like any other signal
67
+ ```
68
+
69
+ ### `batch`
70
+
71
+ Defer effect notifications until a block completes. Useful for multiple related writes.
72
+
73
+ ```typescript
74
+ import { batch } from "@sigil-dev/runtime";
75
+
76
+ batch(() => {
77
+ x.set(1);
78
+ y.set(2);
79
+ // effects fire once, not twice
80
+ });
81
+ ```
82
+
83
+ ### `withEffectScope`
84
+
85
+ Group effects so they can all be torn down at once. Essential for component lifecycle.
86
+
87
+ ```typescript
88
+ import { withEffectScope } from "@sigil-dev/runtime";
89
+
90
+ const dispose = withEffectScope(() => {
91
+ createEffect(() => console.log(x()));
92
+ createEffect(() => console.log(y()));
93
+ });
94
+
95
+ dispose(); // kills both
96
+ ```
97
+
98
+ Scopes nest correctly. Inner dispose does not affect outer scope.
99
+
100
+ ### Context
101
+
102
+ ```typescript
103
+ import { createContext, setContext, getContext } from "@sigil-dev/runtime";
104
+
105
+ const ThemeKey = createContext<"light" | "dark">();
106
+
107
+ setContext(ThemeKey, "dark");
108
+ getContext(ThemeKey); // "dark"
109
+ ```
110
+
111
+ ### Hydration utilities
112
+ > **NOTE:** You probably dont want to use this manually.
113
+
114
+ `claim`, `claimText`, `claimComment`, `reconcile`, `hydrateKeyedList` are used by the Sigil compiler for SSR hydration. Claim existing server-rendered DOM nodes instead of creating new ones.
115
+
116
+ ```typescript
117
+ import { claim, claimText, reconcile } from "@sigil-dev/runtime";
118
+
119
+ // claim an existing <div> from an SSR pool instead of createElement
120
+ const el = claim(nodes, "div", parent);
121
+ ```
122
+
123
+ ## Design
124
+
125
+ No virtual DOM. Effects write directly to DOM nodes. The update path is:
126
+
127
+ ```
128
+ signal.set(newValue)
129
+ → notify subscribers
130
+ → effect re-runs
131
+ → direct DOM mutation
132
+ ```
133
+
134
+ If a signal changes, the effects that depend on it run synchronously (or are batched if inside `batch()`).
135
+
136
+ Deep reactivity on objects uses Proxy rather than explicit getter/setter pairs. This means you can pass a signal's value to any code that expects a plain object and mutations will still be tracked.
137
+
138
+ ## Used by
139
+
140
+ `@sigil-dev/compiler` compiles `$state`, `$derived`, and `$effect` macros down to these primitives at build time. You can use this library directly if you want explicit control or are building outside the Sigil compiler pipeline.
package/index.test.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  ReactiveMap,
34
34
  ReactiveSet,
35
35
  reconcile,
36
+ setProp,
36
37
  snapshot,
37
38
  tick,
38
39
  tracking,
@@ -761,6 +762,49 @@ describe("claim", () => {
761
762
  expect(result.tagName).toBe("DIV");
762
763
  expect(pool).toEqual([]);
763
764
  });
765
+
766
+ test("creates svg in the SVG namespace on empty pool", () => {
767
+ const result = claim([], "svg");
768
+ expect(result.namespaceURI).toBe("http://www.w3.org/2000/svg");
769
+ });
770
+
771
+ test("creates svg children in the SVG namespace via parent", () => {
772
+ const svg = claim([], "svg");
773
+ const path = claim([], "path", svg);
774
+ expect(path.namespaceURI).toBe("http://www.w3.org/2000/svg");
775
+ });
776
+
777
+ test("keeps HTML elements in the HTML namespace", () => {
778
+ const result = claim([], "div");
779
+ expect(result.namespaceURI).toBe("http://www.w3.org/1999/xhtml");
780
+ });
781
+ });
782
+
783
+ describe("setProp", () => {
784
+ test("sets svg attributes without throwing", () => {
785
+ const svg = claim([], "svg");
786
+ expect(() => setProp(svg, "width", 15)).not.toThrow();
787
+ expect(svg.getAttribute("width")).toBe("15");
788
+ });
789
+
790
+ test("maps className to class on svg", () => {
791
+ const svg = claim([], "svg");
792
+ setProp(svg, "className", "icon");
793
+ expect(svg.getAttribute("class")).toBe("icon");
794
+ });
795
+
796
+ test("removes svg attribute on false/null", () => {
797
+ const svg = claim([], "svg");
798
+ setProp(svg, "width", 15);
799
+ setProp(svg, "width", false);
800
+ expect(svg.hasAttribute("width")).toBe(false);
801
+ });
802
+
803
+ test("keeps property semantics on HTML elements", () => {
804
+ const input = claim([], "input") as HTMLInputElement;
805
+ setProp(input, "value", "hello");
806
+ expect(input.value).toBe("hello");
807
+ });
764
808
  });
765
809
 
766
810
  describe("claimText", () => {