totally-tubular 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -140,6 +140,11 @@ setCount((prev) => (prev ?? 0) + 1);
140
140
  `useTubular` automatically unsubscribes when the component unmounts, so you
141
141
  don't need to manage cleanup yourself.
142
142
 
143
+ `useTubular` is built on React's `useSyncExternalStore`, so it works with
144
+ server-side rendering: on the server it renders the store's current value
145
+ without subscribing, and on the client it subscribes during hydration. The
146
+ React bindings require React 18 or later.
147
+
143
148
  ### Resetting from React
144
149
 
145
150
  Import `useTubularReset` from `totally-tubular/react`. It returns a stable
@@ -158,6 +163,44 @@ function ResetButton() {
158
163
  The callback identity never changes, so it's safe to pass directly as an event
159
164
  handler or as a dependency to `useEffect`.
160
165
 
166
+ ### Binding hooks to a store
167
+
168
+ Import `createUseTubular` and `createUseTubularReset` from
169
+ `totally-tubular/react` to create hooks hard-locked to a single `Tubular`
170
+ instance, so you don't have to pass the store around at every call site.
171
+
172
+ Call them once, outside your components — they aren't hooks themselves, so
173
+ module scope is safe — and name what they return with a `use` prefix so
174
+ React's rules-of-hooks linting treats it as a hook.
175
+
176
+ ```tsx
177
+ import { createUseTubular, createUseTubularReset } from "totally-tubular/react";
178
+
179
+ // Using the `store` created in the example above
180
+ const useAppState = createUseTubular(store);
181
+ const useAppReset = createUseTubularReset(store);
182
+
183
+ function Counter() {
184
+ const [count, setCount] = useAppState("count");
185
+
186
+ return (
187
+ <button onClick={() => setCount((n) => (n ?? 0) + 1)}>
188
+ Clicked {count} times
189
+ </button>
190
+ );
191
+ }
192
+
193
+ function ResetButton() {
194
+ const handleReset = useAppReset();
195
+ return <button onClick={handleReset}>Reset</button>;
196
+ }
197
+ ```
198
+
199
+ The returned hooks behave exactly like `useTubular` and `useTubularReset`
200
+ minus the store argument. Key paths stay fully type-checked against the bound
201
+ store's state shape, and each hook is scoped to exactly one instance — bound
202
+ hooks from different stores never cross-talk.
203
+
161
204
  ## Things you should know
162
205
 
163
206
  - **Arrays and nested objects are observed at their path, not their contents.**
@@ -168,53 +211,55 @@ handler or as a dependency to `useEffect`.
168
211
 
169
212
  ## Performance benchmarks
170
213
 
214
+ All benchmarks were run with Node.js 26.5.0 using the `tsx` TypeScript interpreter.
215
+
171
216
  ### Apple M3 Max · 36 GB RAM · macOS Sequoia 15.5
172
217
 
173
218
  ```
174
219
  -----shallow state object-----
175
220
 
176
- 1,000 updates to "name" string val with 10 observers: 0.0036749580000000038s
177
- 1,000 updates to "name" string val with 100 observers: 0.0004760830000000027s
178
- 1,000 updates to "name" string val with 1000 observers: 0.002774291000000005s
179
- 10,000 updates to "name" string val with 10 observers: 0.0014766249999999986s
180
- 10,000 updates to "name" string val with 100 observers: 0.0023700000000000045s
181
- 10,000 updates to "name" string val with 1000 observers: 0.010886792s
182
- 100,000 updates to "name" string val with 10 observers: 0.012480834000000001s
183
- 100,000 updates to "name" string val with 100 observers: 0.022641292000000007s
184
- 100,000 updates to "name" string val with 1000 observers: 0.102325125s
185
- 1,000,000 updates to "name" string val with 10 observers: 0.16086666700000002s
186
- 1,000,000 updates to "name" string val with 100 observers: 0.26716437499999995s
187
- 1,000,000 updates to "name" string val with 1000 observers: 1.0726739170000001s
221
+ 1,000 updates to "name" string val with 10 observers: 0.0005343750000000114s
222
+ 1,000 updates to "name" string val with 100 observers: 0.0006880840000000035s
223
+ 1,000 updates to "name" string val with 1000 observers: 0.002814292000000023s
224
+ 10,000 updates to "name" string val with 10 observers: 0.0007928750000000377s
225
+ 10,000 updates to "name" string val with 100 observers: 0.0017347500000000195s
226
+ 10,000 updates to "name" string val with 1000 observers: 0.007635916000000009s
227
+ 100,000 updates to "name" string val with 10 observers: 0.01587337500000001s
228
+ 100,000 updates to "name" string val with 100 observers: 0.020373334s
229
+ 100,000 updates to "name" string val with 1000 observers: 0.12892133400000005s
230
+ 1,000,000 updates to "name" string val with 10 observers: 0.14660462499999993s
231
+ 1,000,000 updates to "name" string val with 100 observers: 0.20982341600000007s
232
+ 1,000,000 updates to "name" string val with 1000 observers: 0.8595329590000002s
188
233
 
189
234
  -----medium state object-----
190
235
 
191
- 1,000 updates to "user.settings.theme" string val with 10 observers: 0.000344333000000006s
192
- 1,000 updates to "user.settings.theme" string val with 100 observers: 0.00034458300000005695s
193
- 1,000 updates to "user.settings.theme" string val with 1000 observers: 0.0011382089999999608s
194
- 10,000 updates to "user.settings.theme" string val with 10 observers: 0.0012657919999999195s
195
- 10,000 updates to "user.settings.theme" string val with 100 observers: 0.002091625000000022s
196
- 10,000 updates to "user.settings.theme" string val with 1000 observers: 0.010777082999999948s
197
- 100,000 updates to "user.settings.theme" string val with 10 observers: 0.009487583000000086s
198
- 100,000 updates to "user.settings.theme" string val with 100 observers: 0.01902945799999998s
199
- 100,000 updates to "user.settings.theme" string val with 1000 observers: 0.11656079100000011s
200
- 1,000,000 updates to "user.settings.theme" string val with 10 observers: 0.14811841699999992s
201
- 1,000,000 updates to "user.settings.theme" string val with 100 observers: 0.27773633299999984s
202
- 1,000,000 updates to "user.settings.theme" string val with 1000 observers: 1.0240415829999998s
236
+ 1,000 updates to "user.settings.theme" string val with 10 observers: 0.0011529580000001261s
237
+ 1,000 updates to "user.settings.theme" string val with 100 observers: 0.002206040999999914s
238
+ 1,000 updates to "user.settings.theme" string val with 1000 observers: 0.006846457999999984s
239
+ 10,000 updates to "user.settings.theme" string val with 10 observers: 0.01919616700000006s
240
+ 10,000 updates to "user.settings.theme" string val with 100 observers: 0.007707957999999963s
241
+ 10,000 updates to "user.settings.theme" string val with 1000 observers: 0.014114458000000014s
242
+ 100,000 updates to "user.settings.theme" string val with 10 observers: 0.012900958000000174s
243
+ 100,000 updates to "user.settings.theme" string val with 100 observers: 0.018197167000000035s
244
+ 100,000 updates to "user.settings.theme" string val with 1000 observers: 0.09116387499999973s
245
+ 1,000,000 updates to "user.settings.theme" string val with 10 observers: 0.18448416599999973s
246
+ 1,000,000 updates to "user.settings.theme" string val with 100 observers: 0.24980879200000028s
247
+ 1,000,000 updates to "user.settings.theme" string val with 1000 observers: 0.8701055419999998s
203
248
 
204
249
  -----deep state object-----
205
250
 
206
- 1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0012417919999998049s
207
- 1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.0003201250000001892s
208
- 1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.0011683750000001964s
209
- 10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0014113330000000133s
210
- 10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.002289249999999811s
211
- 10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.01099350000000004s
212
- 100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.014709209000000101s
213
- 100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.024220374999999877s
214
- 100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.10777845800000023s
215
- 1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.22398191699999961s
216
- 1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.2829787500000002s
217
- 1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 1.1015419579999999s
251
+ 1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0002052080000003116s
252
+ 1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.0002497920000000704s
253
+ 1,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.001332209000000148s
254
+ 10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.0018820000000000618s
255
+ 10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.0024639170000000377s
256
+ 10,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.014082249999999932s
257
+ 100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.021280209000000015s
258
+ 100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.02714695800000027s
259
+ 100,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.09046704200000022s
260
+ 1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 10 observers: 0.2687363329999998s
261
+ 1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 100 observers: 0.3244868329999999s
262
+ 1,000,000 updates to "zdeepSettings.level1.level2.level3.level4.level5.level6.currentValue" string val with 1000 observers: 0.9486893330000002s
218
263
  ```
219
264
 
220
265
  ### Intel i9-13900HX · 32 GB RAM · Windows 11
package/dist/react.d.ts CHANGED
@@ -12,6 +12,11 @@ import type { AllObjectKeys, PropType } from './types.js';
12
12
  * The setter accepts either a new value directly or an updater function that
13
13
  * receives the current value and returns the next one.
14
14
  *
15
+ * The value is read through React's `useSyncExternalStore`, so the hook is
16
+ * safe to use during server-side rendering: the server renders the store's
17
+ * current value without subscribing, and the client subscribes during
18
+ * hydration.
19
+ *
15
20
  * @example
16
21
  * ```tsx
17
22
  * const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
@@ -28,6 +33,37 @@ import type { AllObjectKeys, PropType } from './types.js';
28
33
  * ```
29
34
  */
30
35
  export declare function useTubular<T extends object, K extends string>(tubular: Tubular<T>, key: K & AllObjectKeys<T>): readonly [PropType<T, K> | null, (newValOrCallback: PropType<T, K> | ((prevVal: PropType<T, K> | null) => PropType<T, K>)) => void];
36
+ /**
37
+ * Creates a hook bound to a specific {@link Tubular} instance, so you never
38
+ * have to pass the instance around at every call site.
39
+ *
40
+ * Call this once, outside your components (the factory itself is not a hook,
41
+ * so module scope is safe), and it returns a hook that works exactly like
42
+ * {@link useTubular} minus the first argument — just the key path.
43
+ *
44
+ * The returned function is a hook: name it with a `use` prefix so React's
45
+ * rules-of-hooks linting recognizes it, and call it unconditionally at the top
46
+ * level of your components. Key paths remain fully type-checked against the
47
+ * bound store's state shape.
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
52
+ *
53
+ * const useAppState = createUseTubular(store);
54
+ *
55
+ * function Counter() {
56
+ * const [count, setCount] = useAppState('count');
57
+ * return <button onClick={() => setCount(n => (n ?? 0) + 1)}>{count}</button>;
58
+ * }
59
+ *
60
+ * function Greeting() {
61
+ * const [name] = useAppState('user.name');
62
+ * return <p>Hello, {name}!</p>;
63
+ * }
64
+ * ```
65
+ */
66
+ export declare function createUseTubular<T extends object>(tubular: Tubular<T>): <K extends string>(key: K & AllObjectKeys<T>) => readonly [PropType<T, K & AllObjectKeys<T>> | null, (newValOrCallback: PropType<T, K & AllObjectKeys<T>> | ((prevVal: PropType<T, K & AllObjectKeys<T>> | null) => PropType<T, K & AllObjectKeys<T>>)) => void];
31
67
  /**
32
68
  * A React hook that returns a stable callback to reset a {@link Tubular} store
33
69
  * back to its initial state.
@@ -51,3 +87,30 @@ export declare function useTubular<T extends object, K extends string>(tubular:
51
87
  * ```
52
88
  */
53
89
  export declare function useTubularReset<T extends object>(tubular: Tubular<T>): () => void;
90
+ /**
91
+ * Creates a hook bound to a specific {@link Tubular} instance that returns a
92
+ * stable callback resetting that store back to its initial state.
93
+ *
94
+ * Call this once, outside your components (the factory itself is not a hook,
95
+ * so module scope is safe), and it returns a hook that works exactly like
96
+ * {@link useTubularReset} minus the first argument. Every bound
97
+ * {@link useTubular} (or {@link createUseTubular}) hook watching the store
98
+ * re-renders with the restored value.
99
+ *
100
+ * The returned function is a hook: name it with a `use` prefix so React's
101
+ * rules-of-hooks linting recognizes it, and call it unconditionally at the top
102
+ * level of your components.
103
+ *
104
+ * @example
105
+ * ```tsx
106
+ * const store = new Tubular({ name: 'Alice', count: 0 });
107
+ *
108
+ * const useAppReset = createUseTubularReset(store);
109
+ *
110
+ * function ResetButton() {
111
+ * const handleReset = useAppReset();
112
+ * return <button onClick={handleReset}>Reset</button>;
113
+ * }
114
+ * ```
115
+ */
116
+ export declare function createUseTubularReset<T extends object>(tubular: Tubular<T>): () => () => void;
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useSyncExternalStore } from 'react';
2
2
  /**
3
3
  * A React hook that binds to a {@link Tubular} instance and a specific key path
4
4
  * within its state.
@@ -11,6 +11,11 @@ import { useCallback, useEffect, useState } from 'react';
11
11
  * The setter accepts either a new value directly or an updater function that
12
12
  * receives the current value and returns the next one.
13
13
  *
14
+ * The value is read through React's `useSyncExternalStore`, so the hook is
15
+ * safe to use during server-side rendering: the server renders the store's
16
+ * current value without subscribing, and the client subscribes during
17
+ * hydration.
18
+ *
14
19
  * @example
15
20
  * ```tsx
16
21
  * const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
@@ -27,9 +32,17 @@ import { useCallback, useEffect, useState } from 'react';
27
32
  * ```
28
33
  */
29
34
  export function useTubular(tubular, key) {
30
- /** state */
31
- const [val, setVal] = useState(() => tubular.read(key));
32
35
  /** callbacks */
36
+ const subscribe = useCallback((onStoreChange) => {
37
+ const observeCb = () => {
38
+ onStoreChange();
39
+ };
40
+ tubular.observe(key, observeCb);
41
+ return () => {
42
+ tubular.unobserve(key, observeCb);
43
+ };
44
+ }, [tubular, key]);
45
+ const getSnapshot = useCallback(() => tubular.read(key), [tubular, key]);
33
46
  const handleUpdateVal = useCallback((newValOrCallback) => {
34
47
  if (typeof newValOrCallback === 'function') {
35
48
  tubular.update(key, newValOrCallback);
@@ -38,20 +51,43 @@ export function useTubular(tubular, key) {
38
51
  tubular.update(key, () => newValOrCallback);
39
52
  }
40
53
  }, [tubular, key]);
41
- /** effects */
42
- useEffect(() => {
43
- // Sync in case the value changed between render and effect registration
44
- setVal(tubular.read(key));
45
- const observeCb = (newVal) => {
46
- setVal(newVal);
47
- };
48
- tubular.observe(key, observeCb);
49
- return () => {
50
- tubular.unobserve(key, observeCb);
51
- };
52
- }, [tubular, key]);
54
+ /** state */
55
+ const val = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
53
56
  return [val, handleUpdateVal];
54
57
  }
58
+ /**
59
+ * Creates a hook bound to a specific {@link Tubular} instance, so you never
60
+ * have to pass the instance around at every call site.
61
+ *
62
+ * Call this once, outside your components (the factory itself is not a hook,
63
+ * so module scope is safe), and it returns a hook that works exactly like
64
+ * {@link useTubular} minus the first argument — just the key path.
65
+ *
66
+ * The returned function is a hook: name it with a `use` prefix so React's
67
+ * rules-of-hooks linting recognizes it, and call it unconditionally at the top
68
+ * level of your components. Key paths remain fully type-checked against the
69
+ * bound store's state shape.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * const store = new Tubular({ user: { name: 'Alice' }, count: 0 });
74
+ *
75
+ * const useAppState = createUseTubular(store);
76
+ *
77
+ * function Counter() {
78
+ * const [count, setCount] = useAppState('count');
79
+ * return <button onClick={() => setCount(n => (n ?? 0) + 1)}>{count}</button>;
80
+ * }
81
+ *
82
+ * function Greeting() {
83
+ * const [name] = useAppState('user.name');
84
+ * return <p>Hello, {name}!</p>;
85
+ * }
86
+ * ```
87
+ */
88
+ export function createUseTubular(tubular) {
89
+ return (key) => useTubular(tubular, key);
90
+ }
55
91
  /**
56
92
  * A React hook that returns a stable callback to reset a {@link Tubular} store
57
93
  * back to its initial state.
@@ -77,3 +113,32 @@ export function useTubular(tubular, key) {
77
113
  export function useTubularReset(tubular) {
78
114
  return useCallback(() => tubular.reset(), [tubular]);
79
115
  }
116
+ /**
117
+ * Creates a hook bound to a specific {@link Tubular} instance that returns a
118
+ * stable callback resetting that store back to its initial state.
119
+ *
120
+ * Call this once, outside your components (the factory itself is not a hook,
121
+ * so module scope is safe), and it returns a hook that works exactly like
122
+ * {@link useTubularReset} minus the first argument. Every bound
123
+ * {@link useTubular} (or {@link createUseTubular}) hook watching the store
124
+ * re-renders with the restored value.
125
+ *
126
+ * The returned function is a hook: name it with a `use` prefix so React's
127
+ * rules-of-hooks linting recognizes it, and call it unconditionally at the top
128
+ * level of your components.
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * const store = new Tubular({ name: 'Alice', count: 0 });
133
+ *
134
+ * const useAppReset = createUseTubularReset(store);
135
+ *
136
+ * function ResetButton() {
137
+ * const handleReset = useAppReset();
138
+ * return <button onClick={handleReset}>Reset</button>;
139
+ * }
140
+ * ```
141
+ */
142
+ export function createUseTubularReset(tubular) {
143
+ return () => useTubularReset(tubular);
144
+ }
package/dist/types.d.ts CHANGED
@@ -8,7 +8,7 @@ type IsCollection<T> = T extends readonly any[] | Map<any, any> | Set<any> ? tru
8
8
  /**
9
9
  * Produces every dot-separated key path into T as a string union.
10
10
  *
11
- * - The `Depth` tuple acts as a recursion counter: when its length reaches 5 the
11
+ * - The `Depth` tuple acts as a recursion counter: when its length reaches 10 the
12
12
  * type resolves to `never`, which caps the union size and prevents the language
13
13
  * server from hanging on deeply-nested or self-referential shapes.
14
14
  * - Top-level keys are included as bare strings (`K`), so this type supersedes
@@ -18,7 +18,7 @@ type IsCollection<T> = T extends readonly any[] | Map<any, any> | Set<any> ? tru
18
18
  * - Returning `never` (not `''`) from every terminal branch avoids the need for
19
19
  * a secondary `Extract<D, string>` pass over the full union.
20
20
  */
21
- export type DotNestedKeys<T, Depth extends 1[] = []> = Depth['length'] extends 5 ? never : T extends object ? IsCollection<T> extends true ? never : {
21
+ export type DotNestedKeys<T, Depth extends 1[] = []> = Depth['length'] extends 10 ? never : T extends object ? IsCollection<T> extends true ? never : {
22
22
  [K in keyof T & string]: IsCollection<T[K]> extends true ? K : T[K] extends object ? K | `${K}.${DotNestedKeys<T[K], [...Depth, 1]>}` : K;
23
23
  }[keyof T & string] : never;
24
24
  /**
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "A completely framework-agnostic, JavaScript / TypeScript state management and update broadcasting library wit zero dependencies, solid performance and a micro file size.",
4
4
  "name": "totally-tubular",
5
5
  "type": "module",
6
- "version": "0.4.0",
6
+ "version": "0.5.0",
7
7
  "keywords": [
8
8
  "state",
9
9
  "management",
@@ -26,27 +26,24 @@
26
26
  "files": [
27
27
  "dist/"
28
28
  ],
29
- "scripts": {
30
- "build": "rm -rf dist/ && tsc --project ./tsconfig.build.json",
31
- "clean": "rm -rf dist/",
32
- "test": "bun test",
33
- "test:watch": "bun test --watch"
34
- },
35
29
  "devDependencies": {
36
30
  "@better-builds/lets-version": "^1.6.0",
37
31
  "@biomejs/biome": "^2.4.16",
38
32
  "@commitlint/cli": "^20.5.3",
39
33
  "@commitlint/config-conventional": "^20.5.3",
40
- "@happy-dom/global-registrator": "^20.10.2",
41
34
  "@testing-library/react": "^16.3.2",
42
35
  "@testing-library/user-event": "^14.6.1",
43
- "@types/bun": "latest",
44
36
  "@types/react": "^19.2.17",
37
+ "@types/node": "^26.1.1",
45
38
  "@types/react-dom": "^19.2.3",
39
+ "@vitejs/plugin-react": "^6.0.3",
46
40
  "happy-dom": "^20.10.2",
47
- "react-dom": "^19.2.7",
48
41
  "react": "^19.2.7",
49
- "typescript": "^5.9.3"
42
+ "react-dom": "^19.2.7",
43
+ "tsx": "^4.23.0",
44
+ "typescript": "^5.9.3",
45
+ "vite": "^8.1.4",
46
+ "vitest": "^4.1.10"
50
47
  },
51
48
  "peerDependencies": {
52
49
  "react": ">=18"
@@ -74,5 +71,12 @@
74
71
  "default": "./dist/types.js"
75
72
  },
76
73
  "./package.json": "./package.json"
74
+ },
75
+ "scripts": {
76
+ "build": "rm -rf dist/ && tsc --project ./tsconfig.build.json",
77
+ "clean": "rm -rf dist/",
78
+ "test": "vitest test",
79
+ "test:performance": "tsx ./performanceTests.ts",
80
+ "test:watch": "vitest test --watch"
77
81
  }
78
- }
82
+ }