@vorra/core 0.3.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.
@@ -0,0 +1,232 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/reactivity.ts
3
+ /**
4
+ * The currently-executing reactive context (effect or computed).
5
+ * When a signal is read, it registers itself as a dependency of this context.
6
+ */
7
+ let activeContext = null;
8
+ /**
9
+ * Batch depth counter. When > 0, signal writes are queued rather than
10
+ * flushed immediately.
11
+ */
12
+ let batchDepth = 0;
13
+ /** Queue of effects to re-run after the current batch completes. */
14
+ const pendingEffects = /* @__PURE__ */ new Set();
15
+ function trackDep(node) {
16
+ if (activeContext === null) return;
17
+ node.subscribers.add(activeContext);
18
+ activeContext.deps.add(node);
19
+ }
20
+ function unsubscribeContext(ctx) {
21
+ for (const dep of ctx.deps) dep.subscribers.delete(ctx);
22
+ ctx.deps.clear();
23
+ }
24
+ function notifySubscribers(node) {
25
+ for (const sub of [...node.subscribers]) sub.notify();
26
+ }
27
+ /**
28
+ * Groups multiple signal writes into a single flush, preventing intermediate
29
+ * effect executions. Effects only run once after the outermost batch ends.
30
+ *
31
+ * @example
32
+ * batch(() => {
33
+ * firstName.set('Jane');
34
+ * lastName.set('Doe');
35
+ * });
36
+ * // Effects depending on either signal run exactly once.
37
+ */
38
+ function batch(fn) {
39
+ batchDepth++;
40
+ try {
41
+ fn();
42
+ } finally {
43
+ batchDepth--;
44
+ if (batchDepth === 0) flushEffects();
45
+ }
46
+ }
47
+ function flushEffects() {
48
+ while (pendingEffects.size > 0) {
49
+ const snapshot = [...pendingEffects];
50
+ pendingEffects.clear();
51
+ for (const node of snapshot) if (!node.destroyed) runEffect(node);
52
+ }
53
+ }
54
+ function scheduleEffect(node) {
55
+ if (node.scheduled || node.destroyed) return;
56
+ node.scheduled = true;
57
+ pendingEffects.add(node);
58
+ if (batchDepth === 0) flushEffects();
59
+ }
60
+ /**
61
+ * Creates a reactive signal — a piece of state that automatically notifies
62
+ * any effects or computed values that read it when it changes.
63
+ *
64
+ * @example
65
+ * const count = signal(0);
66
+ * count(); // read → 0
67
+ * count.set(1); // write
68
+ * count.update(n => n + 1); // functional update
69
+ */
70
+ function signal(initialValue, options) {
71
+ const node = {
72
+ value: initialValue,
73
+ subscribers: /* @__PURE__ */ new Set(),
74
+ equals: options?.equals ?? Object.is
75
+ };
76
+ function getter() {
77
+ trackDep(node);
78
+ return node.value;
79
+ }
80
+ function setter(value) {
81
+ if (node.equals(node.value, value)) return;
82
+ node.value = value;
83
+ notifySubscribers(node);
84
+ }
85
+ const writableSignal = getter;
86
+ Object.defineProperty(writableSignal, "__type", { value: "signal" });
87
+ writableSignal.set = setter;
88
+ writableSignal.update = (fn) => {
89
+ setter(fn(node.value));
90
+ };
91
+ writableSignal.asReadonly = () => {
92
+ const ro = (() => {
93
+ trackDep(node);
94
+ return node.value;
95
+ });
96
+ Object.defineProperty(ro, "__type", { value: "signal" });
97
+ return ro;
98
+ };
99
+ return writableSignal;
100
+ }
101
+ /**
102
+ * Creates a lazily-evaluated derived value. Re-evaluates only when one of
103
+ * its signal dependencies changes, and only when read.
104
+ *
105
+ * @example
106
+ * const count = signal(2);
107
+ * const doubled = computed(() => count() * 2);
108
+ * doubled(); // → 4
109
+ */
110
+ function computed(compute, options) {
111
+ const node = {
112
+ dirty: true,
113
+ value: void 0,
114
+ deps: /* @__PURE__ */ new Set(),
115
+ compute,
116
+ subscribers: /* @__PURE__ */ new Set(),
117
+ equals: options?.equals ?? Object.is,
118
+ notify() {
119
+ if (!node.dirty) {
120
+ node.dirty = true;
121
+ notifySubscribers(node);
122
+ }
123
+ }
124
+ };
125
+ function getter() {
126
+ if (activeContext !== null) {
127
+ node.subscribers.add(activeContext);
128
+ activeContext.deps.add(node);
129
+ }
130
+ if (node.dirty) {
131
+ unsubscribeContext(node);
132
+ const prevContext = activeContext;
133
+ activeContext = node;
134
+ try {
135
+ const newValue = compute();
136
+ if (node.value === void 0 || !node.equals(node.value, newValue)) node.value = newValue;
137
+ } finally {
138
+ activeContext = prevContext;
139
+ node.dirty = false;
140
+ }
141
+ }
142
+ return node.value;
143
+ }
144
+ const computedSignal = getter;
145
+ Object.defineProperty(computedSignal, "__type", { value: "signal" });
146
+ return computedSignal;
147
+ }
148
+ /**
149
+ * Runs a side-effect function immediately and re-runs it whenever any signal
150
+ * read inside it changes.
151
+ *
152
+ * The function may optionally return a cleanup function that runs before the
153
+ * next execution or when the effect is destroyed.
154
+ *
155
+ * @returns An EffectHandle with a `destroy()` method to stop the effect.
156
+ *
157
+ * @example
158
+ * const count = signal(0);
159
+ * const handle = effect(() => {
160
+ * console.log('count is', count());
161
+ * return () => console.log('cleanup');
162
+ * });
163
+ * handle.destroy(); // stops the effect
164
+ */
165
+ function effect(fn) {
166
+ const node = {
167
+ fn,
168
+ cleanup: void 0,
169
+ deps: /* @__PURE__ */ new Set(),
170
+ scheduled: false,
171
+ destroyed: false,
172
+ notify() {
173
+ scheduleEffect(node);
174
+ }
175
+ };
176
+ runEffect(node);
177
+ return { destroy() {
178
+ if (node.destroyed) return;
179
+ node.destroyed = true;
180
+ if (typeof node.cleanup === "function") node.cleanup();
181
+ unsubscribeContext(node);
182
+ pendingEffects.delete(node);
183
+ } };
184
+ }
185
+ function runEffect(node) {
186
+ if (node.destroyed) return;
187
+ if (typeof node.cleanup === "function") {
188
+ node.cleanup();
189
+ node.cleanup = void 0;
190
+ }
191
+ unsubscribeContext(node);
192
+ node.scheduled = false;
193
+ const prevContext = activeContext;
194
+ activeContext = node;
195
+ try {
196
+ node.cleanup = node.fn();
197
+ } finally {
198
+ activeContext = prevContext;
199
+ }
200
+ }
201
+ /**
202
+ * Reads signals inside `fn` without registering them as dependencies of the
203
+ * current reactive context. Useful for reading state in an effect without
204
+ * creating subscriptions.
205
+ *
206
+ * @example
207
+ * effect(() => {
208
+ * triggerSignal(); // subscribed
209
+ * const val = untrack(() => otherSignal()); // NOT subscribed
210
+ * });
211
+ */
212
+ function untrack(fn) {
213
+ const prevContext = activeContext;
214
+ activeContext = null;
215
+ try {
216
+ return fn();
217
+ } finally {
218
+ activeContext = prevContext;
219
+ }
220
+ }
221
+ function isSignal(value) {
222
+ return typeof value === "function" && value.__type === "signal";
223
+ }
224
+ //#endregion
225
+ exports.batch = batch;
226
+ exports.computed = computed;
227
+ exports.effect = effect;
228
+ exports.isSignal = isSignal;
229
+ exports.signal = signal;
230
+ exports.untrack = untrack;
231
+
232
+ //# sourceMappingURL=reactivity.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactivity.cjs","names":[],"sources":["../src/reactivity.ts"],"sourcesContent":["// =============================================================================\r\n// Vorra Reactivity Core\r\n// signal() / computed() / effect() / batch() / untrack()\r\n// =============================================================================\r\n\r\n// ---------------------------------------------------------------------------\r\n// Types\r\n// ---------------------------------------------------------------------------\r\n\r\nexport type SignalGetter<T> = () => T;\r\nexport type SignalSetter<T> = (value: T | ((prev: T) => T)) => void;\r\nexport type Signal<T> = [SignalGetter<T>, SignalSetter<T>];\r\n\r\nexport interface ReadonlySignal<T> {\r\n (): T;\r\n readonly __type: 'signal';\r\n}\r\n\r\nexport interface WritableSignal<T> extends ReadonlySignal<T> {\r\n set(value: T): void;\r\n update(fn: (prev: T) => T): void;\r\n asReadonly(): ReadonlySignal<T>;\r\n}\r\n\r\nexport interface ComputedSignal<T> extends ReadonlySignal<T> {\r\n readonly __type: 'signal';\r\n}\r\n\r\nexport interface EffectHandle {\r\n /** Stops the effect from running again and releases all subscriptions. */\r\n destroy(): void;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal tracking state\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * The currently-executing reactive context (effect or computed).\r\n * When a signal is read, it registers itself as a dependency of this context.\r\n */\r\nlet activeContext: ReactiveContext | null = null;\r\n\r\n/**\r\n * Batch depth counter. When > 0, signal writes are queued rather than\r\n * flushed immediately.\r\n */\r\nlet batchDepth = 0;\r\n\r\n/** Queue of effects to re-run after the current batch completes. */\r\nconst pendingEffects = new Set<EffectNode>();\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal node types\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface ReactiveContext {\r\n /** Called when a dependency notifies this context of a change. */\r\n notify(): void;\r\n /** The set of signal nodes this context is currently subscribed to. */\r\n deps: Set<SignalNode<unknown>>;\r\n}\r\n\r\ninterface SignalNode<T> {\r\n value: T;\r\n /** All reactive contexts currently subscribed to this signal. */\r\n subscribers: Set<ReactiveContext>;\r\n /** Equality check — defaults to Object.is */\r\n equals: (a: T, b: T) => boolean;\r\n}\r\n\r\ninterface ComputedNode<T> extends ReactiveContext {\r\n dirty: boolean;\r\n value: T | undefined;\r\n deps: Set<SignalNode<unknown>>;\r\n compute: () => T;\r\n subscribers: Set<ReactiveContext>;\r\n equals: (a: T, b: T) => boolean;\r\n}\r\n\r\ninterface EffectNode extends ReactiveContext {\r\n fn: () => void | (() => void);\r\n cleanup: (() => void) | void;\r\n deps: Set<SignalNode<unknown>>;\r\n scheduled: boolean;\r\n destroyed: boolean;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Dependency tracking helpers\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction trackDep<T>(node: SignalNode<T>): void {\r\n if (activeContext === null) return;\r\n node.subscribers.add(activeContext);\r\n activeContext.deps.add(node as SignalNode<unknown>);\r\n}\r\n\r\nfunction unsubscribeContext(ctx: ReactiveContext): void {\r\n for (const dep of ctx.deps) {\r\n dep.subscribers.delete(ctx);\r\n }\r\n ctx.deps.clear();\r\n}\r\n\r\nfunction notifySubscribers(node: SignalNode<unknown>): void {\r\n // Copy subscribers before iterating — a subscriber's notify() could\r\n // mutate the set (e.g. a computed re-subscribing).\r\n for (const sub of [...node.subscribers]) {\r\n sub.notify();\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Batch / flush\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Groups multiple signal writes into a single flush, preventing intermediate\r\n * effect executions. Effects only run once after the outermost batch ends.\r\n *\r\n * @example\r\n * batch(() => {\r\n * firstName.set('Jane');\r\n * lastName.set('Doe');\r\n * });\r\n * // Effects depending on either signal run exactly once.\r\n */\r\nexport function batch(fn: () => void): void {\r\n batchDepth++;\r\n try {\r\n fn();\r\n } finally {\r\n batchDepth--;\r\n if (batchDepth === 0) flushEffects();\r\n }\r\n}\r\n\r\nfunction flushEffects(): void {\r\n // Drain the pending set. Effects may schedule new effects during flush,\r\n // so we loop until the set is empty.\r\n while (pendingEffects.size > 0) {\r\n const snapshot = [...pendingEffects];\r\n pendingEffects.clear();\r\n for (const node of snapshot) {\r\n if (!node.destroyed) runEffect(node);\r\n }\r\n }\r\n}\r\n\r\nfunction scheduleEffect(node: EffectNode): void {\r\n if (node.scheduled || node.destroyed) return;\r\n node.scheduled = true;\r\n pendingEffects.add(node);\r\n if (batchDepth === 0) flushEffects();\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// signal()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a reactive signal — a piece of state that automatically notifies\r\n * any effects or computed values that read it when it changes.\r\n *\r\n * @example\r\n * const count = signal(0);\r\n * count(); // read → 0\r\n * count.set(1); // write\r\n * count.update(n => n + 1); // functional update\r\n */\r\nexport function signal<T>(\r\n initialValue: T,\r\n options?: { equals?: (a: T, b: T) => boolean }\r\n): WritableSignal<T> {\r\n const node: SignalNode<T> = {\r\n value: initialValue,\r\n subscribers: new Set(),\r\n equals: options?.equals ?? Object.is,\r\n };\r\n\r\n function getter(): T {\r\n trackDep(node);\r\n return node.value;\r\n }\r\n\r\n function setter(value: T): void {\r\n if (node.equals(node.value, value)) return;\r\n node.value = value;\r\n notifySubscribers(node as SignalNode<unknown>);\r\n }\r\n\r\n const writableSignal = getter as WritableSignal<T>;\r\n\r\n Object.defineProperty(writableSignal, '__type', { value: 'signal' });\r\n\r\n writableSignal.set = setter;\r\n\r\n writableSignal.update = (fn: (prev: T) => T): void => {\r\n setter(fn(node.value));\r\n };\r\n\r\n writableSignal.asReadonly = (): ReadonlySignal<T> => {\r\n const ro = (() => {\r\n trackDep(node);\r\n return node.value;\r\n }) as ReadonlySignal<T>;\r\n Object.defineProperty(ro, '__type', { value: 'signal' });\r\n return ro;\r\n };\r\n\r\n return writableSignal;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// computed()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a lazily-evaluated derived value. Re-evaluates only when one of\r\n * its signal dependencies changes, and only when read.\r\n *\r\n * @example\r\n * const count = signal(2);\r\n * const doubled = computed(() => count() * 2);\r\n * doubled(); // → 4\r\n */\r\nexport function computed<T>(\r\n compute: () => T,\r\n options?: { equals?: (a: T, b: T) => boolean }\r\n): ComputedSignal<T> {\r\n const node: ComputedNode<T> = {\r\n dirty: true,\r\n value: undefined,\r\n deps: new Set(),\r\n compute,\r\n subscribers: new Set(),\r\n equals: options?.equals ?? Object.is,\r\n notify() {\r\n if (!node.dirty) {\r\n node.dirty = true;\r\n // Propagate dirtiness to downstream subscribers without re-evaluating.\r\n notifySubscribers(node as unknown as SignalNode<unknown>);\r\n }\r\n },\r\n };\r\n\r\n function getter(): T {\r\n // Register this computed as a dependency of the outer context.\r\n if (activeContext !== null) {\r\n (node as unknown as SignalNode<unknown>).subscribers.add(activeContext);\r\n activeContext.deps.add(node as unknown as SignalNode<unknown>);\r\n }\r\n\r\n if (node.dirty) {\r\n // Unsubscribe from old deps before re-running.\r\n unsubscribeContext(node);\r\n\r\n const prevContext = activeContext;\r\n activeContext = node;\r\n try {\r\n const newValue = compute();\r\n if (node.value === undefined || !node.equals(node.value as T, newValue)) {\r\n node.value = newValue;\r\n }\r\n } finally {\r\n activeContext = prevContext;\r\n node.dirty = false;\r\n }\r\n }\r\n\r\n return node.value as T;\r\n }\r\n\r\n const computedSignal = getter as ComputedSignal<T>;\r\n Object.defineProperty(computedSignal, '__type', { value: 'signal' });\r\n return computedSignal;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// effect()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Runs a side-effect function immediately and re-runs it whenever any signal\r\n * read inside it changes.\r\n *\r\n * The function may optionally return a cleanup function that runs before the\r\n * next execution or when the effect is destroyed.\r\n *\r\n * @returns An EffectHandle with a `destroy()` method to stop the effect.\r\n *\r\n * @example\r\n * const count = signal(0);\r\n * const handle = effect(() => {\r\n * console.log('count is', count());\r\n * return () => console.log('cleanup');\r\n * });\r\n * handle.destroy(); // stops the effect\r\n */\r\nexport function effect(fn: () => void | (() => void)): EffectHandle {\r\n const node: EffectNode = {\r\n fn,\r\n cleanup: undefined,\r\n deps: new Set(),\r\n scheduled: false,\r\n destroyed: false,\r\n notify() {\r\n scheduleEffect(node);\r\n },\r\n };\r\n\r\n // Run immediately (synchronously).\r\n runEffect(node);\r\n\r\n return {\r\n destroy() {\r\n if (node.destroyed) return;\r\n node.destroyed = true;\r\n if (typeof node.cleanup === 'function') node.cleanup();\r\n unsubscribeContext(node);\r\n pendingEffects.delete(node);\r\n },\r\n };\r\n}\r\n\r\nfunction runEffect(node: EffectNode): void {\r\n if (node.destroyed) return;\r\n\r\n // Run previous cleanup.\r\n if (typeof node.cleanup === 'function') {\r\n node.cleanup();\r\n node.cleanup = undefined;\r\n }\r\n\r\n // Unsubscribe from previous deps before re-tracking.\r\n unsubscribeContext(node);\r\n\r\n node.scheduled = false;\r\n\r\n const prevContext = activeContext;\r\n activeContext = node;\r\n try {\r\n node.cleanup = node.fn() as (() => void) | void;\r\n } finally {\r\n activeContext = prevContext;\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// untrack()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Reads signals inside `fn` without registering them as dependencies of the\r\n * current reactive context. Useful for reading state in an effect without\r\n * creating subscriptions.\r\n *\r\n * @example\r\n * effect(() => {\r\n * triggerSignal(); // subscribed\r\n * const val = untrack(() => otherSignal()); // NOT subscribed\r\n * });\r\n */\r\nexport function untrack<T>(fn: () => T): T {\r\n const prevContext = activeContext;\r\n activeContext = null;\r\n try {\r\n return fn();\r\n } finally {\r\n activeContext = prevContext;\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// isSignal() type guard\r\n// ---------------------------------------------------------------------------\r\n\r\nexport function isSignal(value: unknown): value is ReadonlySignal<unknown> {\r\n return typeof value === 'function' && (value as ReadonlySignal<unknown>).__type === 'signal';\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ export type SignalGetter<T> = () => T;
2
+ export type SignalSetter<T> = (value: T | ((prev: T) => T)) => void;
3
+ export type Signal<T> = [SignalGetter<T>, SignalSetter<T>];
4
+ export interface ReadonlySignal<T> {
5
+ (): T;
6
+ readonly __type: 'signal';
7
+ }
8
+ export interface WritableSignal<T> extends ReadonlySignal<T> {
9
+ set(value: T): void;
10
+ update(fn: (prev: T) => T): void;
11
+ asReadonly(): ReadonlySignal<T>;
12
+ }
13
+ export interface ComputedSignal<T> extends ReadonlySignal<T> {
14
+ readonly __type: 'signal';
15
+ }
16
+ export interface EffectHandle {
17
+ /** Stops the effect from running again and releases all subscriptions. */
18
+ destroy(): void;
19
+ }
20
+ /**
21
+ * Groups multiple signal writes into a single flush, preventing intermediate
22
+ * effect executions. Effects only run once after the outermost batch ends.
23
+ *
24
+ * @example
25
+ * batch(() => {
26
+ * firstName.set('Jane');
27
+ * lastName.set('Doe');
28
+ * });
29
+ * // Effects depending on either signal run exactly once.
30
+ */
31
+ export declare function batch(fn: () => void): void;
32
+ /**
33
+ * Creates a reactive signal — a piece of state that automatically notifies
34
+ * any effects or computed values that read it when it changes.
35
+ *
36
+ * @example
37
+ * const count = signal(0);
38
+ * count(); // read → 0
39
+ * count.set(1); // write
40
+ * count.update(n => n + 1); // functional update
41
+ */
42
+ export declare function signal<T>(initialValue: T, options?: {
43
+ equals?: (a: T, b: T) => boolean;
44
+ }): WritableSignal<T>;
45
+ /**
46
+ * Creates a lazily-evaluated derived value. Re-evaluates only when one of
47
+ * its signal dependencies changes, and only when read.
48
+ *
49
+ * @example
50
+ * const count = signal(2);
51
+ * const doubled = computed(() => count() * 2);
52
+ * doubled(); // → 4
53
+ */
54
+ export declare function computed<T>(compute: () => T, options?: {
55
+ equals?: (a: T, b: T) => boolean;
56
+ }): ComputedSignal<T>;
57
+ /**
58
+ * Runs a side-effect function immediately and re-runs it whenever any signal
59
+ * read inside it changes.
60
+ *
61
+ * The function may optionally return a cleanup function that runs before the
62
+ * next execution or when the effect is destroyed.
63
+ *
64
+ * @returns An EffectHandle with a `destroy()` method to stop the effect.
65
+ *
66
+ * @example
67
+ * const count = signal(0);
68
+ * const handle = effect(() => {
69
+ * console.log('count is', count());
70
+ * return () => console.log('cleanup');
71
+ * });
72
+ * handle.destroy(); // stops the effect
73
+ */
74
+ export declare function effect(fn: () => void | (() => void)): EffectHandle;
75
+ /**
76
+ * Reads signals inside `fn` without registering them as dependencies of the
77
+ * current reactive context. Useful for reading state in an effect without
78
+ * creating subscriptions.
79
+ *
80
+ * @example
81
+ * effect(() => {
82
+ * triggerSignal(); // subscribed
83
+ * const val = untrack(() => otherSignal()); // NOT subscribed
84
+ * });
85
+ */
86
+ export declare function untrack<T>(fn: () => T): T;
87
+ export declare function isSignal(value: unknown): value is ReadonlySignal<unknown>;
88
+ //# sourceMappingURL=reactivity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactivity.d.ts","sourceRoot":"","sources":["../src/reactivity.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;AACtC,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC;AACpE,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;AAE3D,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,IAAI,CAAC,CAAC;IACN,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;IAC1D,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACjC,UAAU,IAAI,cAAc,CAAC,CAAC,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;IAC1D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,OAAO,IAAI,IAAI,CAAC;CACjB;AAsFD;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAQ1C;AAyBD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,CAAC,EACtB,YAAY,EAAE,CAAC,EACf,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;CAAE,GAC7C,cAAc,CAAC,CAAC,CAAC,CAsCnB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,OAAO,EAAE,MAAM,CAAC,EAChB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;CAAE,GAC7C,cAAc,CAAC,CAAC,CAAC,CA+CnB;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,YAAY,CAwBlE;AA6BD;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQzC;AAMD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAAC,OAAO,CAAC,CAEzE"}
@@ -0,0 +1,226 @@
1
+ //#region src/reactivity.ts
2
+ /**
3
+ * The currently-executing reactive context (effect or computed).
4
+ * When a signal is read, it registers itself as a dependency of this context.
5
+ */
6
+ let activeContext = null;
7
+ /**
8
+ * Batch depth counter. When > 0, signal writes are queued rather than
9
+ * flushed immediately.
10
+ */
11
+ let batchDepth = 0;
12
+ /** Queue of effects to re-run after the current batch completes. */
13
+ const pendingEffects = /* @__PURE__ */ new Set();
14
+ function trackDep(node) {
15
+ if (activeContext === null) return;
16
+ node.subscribers.add(activeContext);
17
+ activeContext.deps.add(node);
18
+ }
19
+ function unsubscribeContext(ctx) {
20
+ for (const dep of ctx.deps) dep.subscribers.delete(ctx);
21
+ ctx.deps.clear();
22
+ }
23
+ function notifySubscribers(node) {
24
+ for (const sub of [...node.subscribers]) sub.notify();
25
+ }
26
+ /**
27
+ * Groups multiple signal writes into a single flush, preventing intermediate
28
+ * effect executions. Effects only run once after the outermost batch ends.
29
+ *
30
+ * @example
31
+ * batch(() => {
32
+ * firstName.set('Jane');
33
+ * lastName.set('Doe');
34
+ * });
35
+ * // Effects depending on either signal run exactly once.
36
+ */
37
+ function batch(fn) {
38
+ batchDepth++;
39
+ try {
40
+ fn();
41
+ } finally {
42
+ batchDepth--;
43
+ if (batchDepth === 0) flushEffects();
44
+ }
45
+ }
46
+ function flushEffects() {
47
+ while (pendingEffects.size > 0) {
48
+ const snapshot = [...pendingEffects];
49
+ pendingEffects.clear();
50
+ for (const node of snapshot) if (!node.destroyed) runEffect(node);
51
+ }
52
+ }
53
+ function scheduleEffect(node) {
54
+ if (node.scheduled || node.destroyed) return;
55
+ node.scheduled = true;
56
+ pendingEffects.add(node);
57
+ if (batchDepth === 0) flushEffects();
58
+ }
59
+ /**
60
+ * Creates a reactive signal — a piece of state that automatically notifies
61
+ * any effects or computed values that read it when it changes.
62
+ *
63
+ * @example
64
+ * const count = signal(0);
65
+ * count(); // read → 0
66
+ * count.set(1); // write
67
+ * count.update(n => n + 1); // functional update
68
+ */
69
+ function signal(initialValue, options) {
70
+ const node = {
71
+ value: initialValue,
72
+ subscribers: /* @__PURE__ */ new Set(),
73
+ equals: options?.equals ?? Object.is
74
+ };
75
+ function getter() {
76
+ trackDep(node);
77
+ return node.value;
78
+ }
79
+ function setter(value) {
80
+ if (node.equals(node.value, value)) return;
81
+ node.value = value;
82
+ notifySubscribers(node);
83
+ }
84
+ const writableSignal = getter;
85
+ Object.defineProperty(writableSignal, "__type", { value: "signal" });
86
+ writableSignal.set = setter;
87
+ writableSignal.update = (fn) => {
88
+ setter(fn(node.value));
89
+ };
90
+ writableSignal.asReadonly = () => {
91
+ const ro = (() => {
92
+ trackDep(node);
93
+ return node.value;
94
+ });
95
+ Object.defineProperty(ro, "__type", { value: "signal" });
96
+ return ro;
97
+ };
98
+ return writableSignal;
99
+ }
100
+ /**
101
+ * Creates a lazily-evaluated derived value. Re-evaluates only when one of
102
+ * its signal dependencies changes, and only when read.
103
+ *
104
+ * @example
105
+ * const count = signal(2);
106
+ * const doubled = computed(() => count() * 2);
107
+ * doubled(); // → 4
108
+ */
109
+ function computed(compute, options) {
110
+ const node = {
111
+ dirty: true,
112
+ value: void 0,
113
+ deps: /* @__PURE__ */ new Set(),
114
+ compute,
115
+ subscribers: /* @__PURE__ */ new Set(),
116
+ equals: options?.equals ?? Object.is,
117
+ notify() {
118
+ if (!node.dirty) {
119
+ node.dirty = true;
120
+ notifySubscribers(node);
121
+ }
122
+ }
123
+ };
124
+ function getter() {
125
+ if (activeContext !== null) {
126
+ node.subscribers.add(activeContext);
127
+ activeContext.deps.add(node);
128
+ }
129
+ if (node.dirty) {
130
+ unsubscribeContext(node);
131
+ const prevContext = activeContext;
132
+ activeContext = node;
133
+ try {
134
+ const newValue = compute();
135
+ if (node.value === void 0 || !node.equals(node.value, newValue)) node.value = newValue;
136
+ } finally {
137
+ activeContext = prevContext;
138
+ node.dirty = false;
139
+ }
140
+ }
141
+ return node.value;
142
+ }
143
+ const computedSignal = getter;
144
+ Object.defineProperty(computedSignal, "__type", { value: "signal" });
145
+ return computedSignal;
146
+ }
147
+ /**
148
+ * Runs a side-effect function immediately and re-runs it whenever any signal
149
+ * read inside it changes.
150
+ *
151
+ * The function may optionally return a cleanup function that runs before the
152
+ * next execution or when the effect is destroyed.
153
+ *
154
+ * @returns An EffectHandle with a `destroy()` method to stop the effect.
155
+ *
156
+ * @example
157
+ * const count = signal(0);
158
+ * const handle = effect(() => {
159
+ * console.log('count is', count());
160
+ * return () => console.log('cleanup');
161
+ * });
162
+ * handle.destroy(); // stops the effect
163
+ */
164
+ function effect(fn) {
165
+ const node = {
166
+ fn,
167
+ cleanup: void 0,
168
+ deps: /* @__PURE__ */ new Set(),
169
+ scheduled: false,
170
+ destroyed: false,
171
+ notify() {
172
+ scheduleEffect(node);
173
+ }
174
+ };
175
+ runEffect(node);
176
+ return { destroy() {
177
+ if (node.destroyed) return;
178
+ node.destroyed = true;
179
+ if (typeof node.cleanup === "function") node.cleanup();
180
+ unsubscribeContext(node);
181
+ pendingEffects.delete(node);
182
+ } };
183
+ }
184
+ function runEffect(node) {
185
+ if (node.destroyed) return;
186
+ if (typeof node.cleanup === "function") {
187
+ node.cleanup();
188
+ node.cleanup = void 0;
189
+ }
190
+ unsubscribeContext(node);
191
+ node.scheduled = false;
192
+ const prevContext = activeContext;
193
+ activeContext = node;
194
+ try {
195
+ node.cleanup = node.fn();
196
+ } finally {
197
+ activeContext = prevContext;
198
+ }
199
+ }
200
+ /**
201
+ * Reads signals inside `fn` without registering them as dependencies of the
202
+ * current reactive context. Useful for reading state in an effect without
203
+ * creating subscriptions.
204
+ *
205
+ * @example
206
+ * effect(() => {
207
+ * triggerSignal(); // subscribed
208
+ * const val = untrack(() => otherSignal()); // NOT subscribed
209
+ * });
210
+ */
211
+ function untrack(fn) {
212
+ const prevContext = activeContext;
213
+ activeContext = null;
214
+ try {
215
+ return fn();
216
+ } finally {
217
+ activeContext = prevContext;
218
+ }
219
+ }
220
+ function isSignal(value) {
221
+ return typeof value === "function" && value.__type === "signal";
222
+ }
223
+ //#endregion
224
+ export { batch, computed, effect, isSignal, signal, untrack };
225
+
226
+ //# sourceMappingURL=reactivity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactivity.js","names":[],"sources":["../src/reactivity.ts"],"sourcesContent":["// =============================================================================\r\n// Vorra Reactivity Core\r\n// signal() / computed() / effect() / batch() / untrack()\r\n// =============================================================================\r\n\r\n// ---------------------------------------------------------------------------\r\n// Types\r\n// ---------------------------------------------------------------------------\r\n\r\nexport type SignalGetter<T> = () => T;\r\nexport type SignalSetter<T> = (value: T | ((prev: T) => T)) => void;\r\nexport type Signal<T> = [SignalGetter<T>, SignalSetter<T>];\r\n\r\nexport interface ReadonlySignal<T> {\r\n (): T;\r\n readonly __type: 'signal';\r\n}\r\n\r\nexport interface WritableSignal<T> extends ReadonlySignal<T> {\r\n set(value: T): void;\r\n update(fn: (prev: T) => T): void;\r\n asReadonly(): ReadonlySignal<T>;\r\n}\r\n\r\nexport interface ComputedSignal<T> extends ReadonlySignal<T> {\r\n readonly __type: 'signal';\r\n}\r\n\r\nexport interface EffectHandle {\r\n /** Stops the effect from running again and releases all subscriptions. */\r\n destroy(): void;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal tracking state\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * The currently-executing reactive context (effect or computed).\r\n * When a signal is read, it registers itself as a dependency of this context.\r\n */\r\nlet activeContext: ReactiveContext | null = null;\r\n\r\n/**\r\n * Batch depth counter. When > 0, signal writes are queued rather than\r\n * flushed immediately.\r\n */\r\nlet batchDepth = 0;\r\n\r\n/** Queue of effects to re-run after the current batch completes. */\r\nconst pendingEffects = new Set<EffectNode>();\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal node types\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface ReactiveContext {\r\n /** Called when a dependency notifies this context of a change. */\r\n notify(): void;\r\n /** The set of signal nodes this context is currently subscribed to. */\r\n deps: Set<SignalNode<unknown>>;\r\n}\r\n\r\ninterface SignalNode<T> {\r\n value: T;\r\n /** All reactive contexts currently subscribed to this signal. */\r\n subscribers: Set<ReactiveContext>;\r\n /** Equality check — defaults to Object.is */\r\n equals: (a: T, b: T) => boolean;\r\n}\r\n\r\ninterface ComputedNode<T> extends ReactiveContext {\r\n dirty: boolean;\r\n value: T | undefined;\r\n deps: Set<SignalNode<unknown>>;\r\n compute: () => T;\r\n subscribers: Set<ReactiveContext>;\r\n equals: (a: T, b: T) => boolean;\r\n}\r\n\r\ninterface EffectNode extends ReactiveContext {\r\n fn: () => void | (() => void);\r\n cleanup: (() => void) | void;\r\n deps: Set<SignalNode<unknown>>;\r\n scheduled: boolean;\r\n destroyed: boolean;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Dependency tracking helpers\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction trackDep<T>(node: SignalNode<T>): void {\r\n if (activeContext === null) return;\r\n node.subscribers.add(activeContext);\r\n activeContext.deps.add(node as SignalNode<unknown>);\r\n}\r\n\r\nfunction unsubscribeContext(ctx: ReactiveContext): void {\r\n for (const dep of ctx.deps) {\r\n dep.subscribers.delete(ctx);\r\n }\r\n ctx.deps.clear();\r\n}\r\n\r\nfunction notifySubscribers(node: SignalNode<unknown>): void {\r\n // Copy subscribers before iterating — a subscriber's notify() could\r\n // mutate the set (e.g. a computed re-subscribing).\r\n for (const sub of [...node.subscribers]) {\r\n sub.notify();\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Batch / flush\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Groups multiple signal writes into a single flush, preventing intermediate\r\n * effect executions. Effects only run once after the outermost batch ends.\r\n *\r\n * @example\r\n * batch(() => {\r\n * firstName.set('Jane');\r\n * lastName.set('Doe');\r\n * });\r\n * // Effects depending on either signal run exactly once.\r\n */\r\nexport function batch(fn: () => void): void {\r\n batchDepth++;\r\n try {\r\n fn();\r\n } finally {\r\n batchDepth--;\r\n if (batchDepth === 0) flushEffects();\r\n }\r\n}\r\n\r\nfunction flushEffects(): void {\r\n // Drain the pending set. Effects may schedule new effects during flush,\r\n // so we loop until the set is empty.\r\n while (pendingEffects.size > 0) {\r\n const snapshot = [...pendingEffects];\r\n pendingEffects.clear();\r\n for (const node of snapshot) {\r\n if (!node.destroyed) runEffect(node);\r\n }\r\n }\r\n}\r\n\r\nfunction scheduleEffect(node: EffectNode): void {\r\n if (node.scheduled || node.destroyed) return;\r\n node.scheduled = true;\r\n pendingEffects.add(node);\r\n if (batchDepth === 0) flushEffects();\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// signal()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a reactive signal — a piece of state that automatically notifies\r\n * any effects or computed values that read it when it changes.\r\n *\r\n * @example\r\n * const count = signal(0);\r\n * count(); // read → 0\r\n * count.set(1); // write\r\n * count.update(n => n + 1); // functional update\r\n */\r\nexport function signal<T>(\r\n initialValue: T,\r\n options?: { equals?: (a: T, b: T) => boolean }\r\n): WritableSignal<T> {\r\n const node: SignalNode<T> = {\r\n value: initialValue,\r\n subscribers: new Set(),\r\n equals: options?.equals ?? Object.is,\r\n };\r\n\r\n function getter(): T {\r\n trackDep(node);\r\n return node.value;\r\n }\r\n\r\n function setter(value: T): void {\r\n if (node.equals(node.value, value)) return;\r\n node.value = value;\r\n notifySubscribers(node as SignalNode<unknown>);\r\n }\r\n\r\n const writableSignal = getter as WritableSignal<T>;\r\n\r\n Object.defineProperty(writableSignal, '__type', { value: 'signal' });\r\n\r\n writableSignal.set = setter;\r\n\r\n writableSignal.update = (fn: (prev: T) => T): void => {\r\n setter(fn(node.value));\r\n };\r\n\r\n writableSignal.asReadonly = (): ReadonlySignal<T> => {\r\n const ro = (() => {\r\n trackDep(node);\r\n return node.value;\r\n }) as ReadonlySignal<T>;\r\n Object.defineProperty(ro, '__type', { value: 'signal' });\r\n return ro;\r\n };\r\n\r\n return writableSignal;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// computed()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a lazily-evaluated derived value. Re-evaluates only when one of\r\n * its signal dependencies changes, and only when read.\r\n *\r\n * @example\r\n * const count = signal(2);\r\n * const doubled = computed(() => count() * 2);\r\n * doubled(); // → 4\r\n */\r\nexport function computed<T>(\r\n compute: () => T,\r\n options?: { equals?: (a: T, b: T) => boolean }\r\n): ComputedSignal<T> {\r\n const node: ComputedNode<T> = {\r\n dirty: true,\r\n value: undefined,\r\n deps: new Set(),\r\n compute,\r\n subscribers: new Set(),\r\n equals: options?.equals ?? Object.is,\r\n notify() {\r\n if (!node.dirty) {\r\n node.dirty = true;\r\n // Propagate dirtiness to downstream subscribers without re-evaluating.\r\n notifySubscribers(node as unknown as SignalNode<unknown>);\r\n }\r\n },\r\n };\r\n\r\n function getter(): T {\r\n // Register this computed as a dependency of the outer context.\r\n if (activeContext !== null) {\r\n (node as unknown as SignalNode<unknown>).subscribers.add(activeContext);\r\n activeContext.deps.add(node as unknown as SignalNode<unknown>);\r\n }\r\n\r\n if (node.dirty) {\r\n // Unsubscribe from old deps before re-running.\r\n unsubscribeContext(node);\r\n\r\n const prevContext = activeContext;\r\n activeContext = node;\r\n try {\r\n const newValue = compute();\r\n if (node.value === undefined || !node.equals(node.value as T, newValue)) {\r\n node.value = newValue;\r\n }\r\n } finally {\r\n activeContext = prevContext;\r\n node.dirty = false;\r\n }\r\n }\r\n\r\n return node.value as T;\r\n }\r\n\r\n const computedSignal = getter as ComputedSignal<T>;\r\n Object.defineProperty(computedSignal, '__type', { value: 'signal' });\r\n return computedSignal;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// effect()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Runs a side-effect function immediately and re-runs it whenever any signal\r\n * read inside it changes.\r\n *\r\n * The function may optionally return a cleanup function that runs before the\r\n * next execution or when the effect is destroyed.\r\n *\r\n * @returns An EffectHandle with a `destroy()` method to stop the effect.\r\n *\r\n * @example\r\n * const count = signal(0);\r\n * const handle = effect(() => {\r\n * console.log('count is', count());\r\n * return () => console.log('cleanup');\r\n * });\r\n * handle.destroy(); // stops the effect\r\n */\r\nexport function effect(fn: () => void | (() => void)): EffectHandle {\r\n const node: EffectNode = {\r\n fn,\r\n cleanup: undefined,\r\n deps: new Set(),\r\n scheduled: false,\r\n destroyed: false,\r\n notify() {\r\n scheduleEffect(node);\r\n },\r\n };\r\n\r\n // Run immediately (synchronously).\r\n runEffect(node);\r\n\r\n return {\r\n destroy() {\r\n if (node.destroyed) return;\r\n node.destroyed = true;\r\n if (typeof node.cleanup === 'function') node.cleanup();\r\n unsubscribeContext(node);\r\n pendingEffects.delete(node);\r\n },\r\n };\r\n}\r\n\r\nfunction runEffect(node: EffectNode): void {\r\n if (node.destroyed) return;\r\n\r\n // Run previous cleanup.\r\n if (typeof node.cleanup === 'function') {\r\n node.cleanup();\r\n node.cleanup = undefined;\r\n }\r\n\r\n // Unsubscribe from previous deps before re-tracking.\r\n unsubscribeContext(node);\r\n\r\n node.scheduled = false;\r\n\r\n const prevContext = activeContext;\r\n activeContext = node;\r\n try {\r\n node.cleanup = node.fn() as (() => void) | void;\r\n } finally {\r\n activeContext = prevContext;\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// untrack()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Reads signals inside `fn` without registering them as dependencies of the\r\n * current reactive context. Useful for reading state in an effect without\r\n * creating subscriptions.\r\n *\r\n * @example\r\n * effect(() => {\r\n * triggerSignal(); // subscribed\r\n * const val = untrack(() => otherSignal()); // NOT subscribed\r\n * });\r\n */\r\nexport function untrack<T>(fn: () => T): T {\r\n const prevContext = activeContext;\r\n activeContext = null;\r\n try {\r\n return fn();\r\n } finally {\r\n activeContext = prevContext;\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// isSignal() type guard\r\n// ---------------------------------------------------------------------------\r\n\r\nexport function isSignal(value: unknown): value is ReadonlySignal<unknown> {\r\n return typeof value === 'function' && (value as ReadonlySignal<unknown>).__type === 'signal';\r\n}\r\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@vorra/core",
3
+ "version": "0.3.0",
4
+ "description": "Vorra core — reactivity primitives and dependency injection",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/ubergeoff/vorra.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "homepage": "https://github.com/ubergeoff/vorra/tree/main/packages/core#readme",
12
+ "keywords": [
13
+ "vorra",
14
+ "signals",
15
+ "reactivity",
16
+ "dependency-injection",
17
+ "framework"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "type": "module",
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.cjs",
35
+ "types": "./dist/index.d.ts"
36
+ },
37
+ "./reactivity": {
38
+ "import": "./dist/reactivity.js",
39
+ "require": "./dist/reactivity.cjs",
40
+ "types": "./dist/reactivity.d.ts"
41
+ },
42
+ "./di": {
43
+ "import": "./dist/di.js",
44
+ "require": "./dist/di.cjs",
45
+ "types": "./dist/di.d.ts"
46
+ },
47
+ "./dom": {
48
+ "import": "./dist/dom.js",
49
+ "require": "./dist/dom.cjs",
50
+ "types": "./dist/dom.d.ts"
51
+ }
52
+ },
53
+ "scripts": {
54
+ "build": "rolldown -c rolldown.config.mjs",
55
+ "clean": "rm -rf dist *.tsbuildinfo"
56
+ },
57
+ "devDependencies": {
58
+ "rolldown": "*",
59
+ "typescript": "*",
60
+ "vitest": "*"
61
+ },
62
+ "sideEffects": false
63
+ }