lume-js 0.3.0 → 0.4.1

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
@@ -5,7 +5,7 @@
5
5
  Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required, no framework lock-in.
6
6
 
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
- [![Version](https://img.shields.io/badge/version-0.2.1-green.svg)](package.json)
8
+ [![Version](https://img.shields.io/badge/version-0.4.1-green.svg)](package.json)
9
9
 
10
10
  ## Why Lume.js?
11
11
 
@@ -29,6 +29,8 @@ Minimal reactive state management using only standard JavaScript and HTML - no c
29
29
 
30
30
  **Lume.js is essentially "Modern Knockout.js" - standards-only reactivity for 2025.**
31
31
 
32
+ 📖 **New to the project?** Read [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) to understand our design philosophy and why certain choices were made.
33
+
32
34
  ---
33
35
 
34
36
  ## Installation
@@ -43,7 +45,10 @@ npm install lume-js
43
45
 
44
46
  ```html
45
47
  <script type="module">
46
- import { state, bindDom } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';
48
+ import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';
49
+
50
+ // For addons:
51
+ import { computed, watch } from 'https://cdn.jsdelivr.net/npm/lume-js/src/addons/index.js';
47
52
  </script>
48
53
  ```
49
54
 
@@ -51,6 +56,16 @@ npm install lume-js
51
56
 
52
57
  ## Quick Start
53
58
 
59
+ ### Examples
60
+
61
+ Run the live examples (including the new Todo app) with Vite:
62
+
63
+ ```bash
64
+ npm run dev
65
+ ```
66
+
67
+ Then open the Examples index (Vite will auto-open): `http://localhost:5173/examples/`
68
+
54
69
  **HTML:**
55
70
  ```html
56
71
  <div>
@@ -64,7 +79,7 @@ npm install lume-js
64
79
 
65
80
  **JavaScript:**
66
81
  ```javascript
67
- import { state, bindDom } from 'lume-js';
82
+ import { state, bindDom, effect } from 'lume-js';
68
83
 
69
84
  // Create reactive state
70
85
  const store = state({
@@ -72,16 +87,24 @@ const store = state({
72
87
  name: 'World'
73
88
  });
74
89
 
75
- // Bind to DOM
90
+ // Bind to DOM (updates on state changes)
76
91
  const cleanup = bindDom(document.body, store);
77
92
 
93
+ // Auto-update document title when name changes
94
+ const effectCleanup = effect(() => {
95
+ document.title = `Hello, ${store.name}!`;
96
+ });
97
+
78
98
  // Update state with standard JavaScript
79
99
  document.getElementById('increment').addEventListener('click', () => {
80
100
  store.count++;
81
101
  });
82
102
 
83
103
  // Cleanup when done (important!)
84
- window.addEventListener('beforeunload', () => cleanup());
104
+ window.addEventListener('beforeunload', () => {
105
+ cleanup();
106
+ effectCleanup();
107
+ });
85
108
  ```
86
109
 
87
110
  That's it! No build step, no custom syntax, just HTML and JavaScript.
@@ -92,7 +115,7 @@ That's it! No build step, no custom syntax, just HTML and JavaScript.
92
115
 
93
116
  ### `state(object)`
94
117
 
95
- Creates a reactive state object using Proxy.
118
+ Creates a reactive state object using Proxy with automatic dependency tracking.
96
119
 
97
120
  ```javascript
98
121
  const store = state({
@@ -109,21 +132,63 @@ store.user.name = 'Bob';
109
132
  ```
110
133
 
111
134
  **Features:**
135
+ - ✅ Automatic dependency tracking for effects
136
+ - ✅ Per-state microtask batching for performance
112
137
  - ✅ Validates input (must be plain object)
113
138
  - ✅ Only triggers updates when value actually changes
114
139
  - ✅ Returns cleanup function from `$subscribe`
140
+ - ✅ Deduplicates effect runs per flush cycle
115
141
 
116
- ### `bindDom(root, store)`
142
+ ### `effect(fn)`
143
+
144
+ Creates an effect that automatically tracks dependencies and re-runs when they change.
145
+
146
+ ```javascript
147
+ const store = state({ count: 0, name: 'Alice' });
148
+
149
+ const cleanup = effect(() => {
150
+ // Only tracks 'count' (name is not accessed)
151
+ console.log(`Count: ${store.count}`);
152
+ });
153
+
154
+ store.count = 5; // Effect re-runs
155
+ store.name = 'Bob'; // Effect does NOT re-run
156
+
157
+ cleanup(); // Stop the effect
158
+ ```
159
+
160
+ **Features:**
161
+ - ✅ Automatic dependency collection (tracks what you actually access)
162
+ - ✅ Dynamic dependencies (re-tracks on every execution)
163
+ - ✅ Returns cleanup function
164
+ - ✅ Prevents infinite recursion
165
+ - ✅ Integrates with per-state batching (no global scheduler)
166
+
167
+ **How it works:** Effects use `globalThis.__LUME_CURRENT_EFFECT__` to track which state properties are accessed during execution. When any tracked property changes, the effect is queued in that state's pending effects set and runs once in the next microtask.
168
+
169
+ ### `bindDom(root, store, options?)`
117
170
 
118
171
  Binds reactive state to DOM elements with `data-bind` attributes.
119
172
 
173
+ **Automatically waits for DOMContentLoaded** if the document is still loading, making it safe to call from anywhere (even in `<head>`).
174
+
120
175
  ```javascript
176
+ // Default: Auto-waits for DOM (safe anywhere)
121
177
  const cleanup = bindDom(document.body, store);
122
178
 
179
+ // Advanced: Force immediate binding (no auto-wait)
180
+ const cleanup = bindDom(myElement, store, { immediate: true });
181
+
123
182
  // Later: cleanup all bindings
124
183
  cleanup();
125
184
  ```
126
185
 
186
+ **Parameters:**
187
+ - `root` (HTMLElement) - Root element to scan for `[data-bind]` attributes
188
+ - `store` (Object) - Reactive state object
189
+ - `options` (Object, optional)
190
+ - `immediate` (Boolean) - Skip auto-wait, bind immediately. Default: `false`
191
+
127
192
  **Supports:**
128
193
  - ✅ Text content: `<span data-bind="count"></span>`
129
194
  - ✅ Input values: `<input data-bind="name">`
@@ -134,14 +199,111 @@ cleanup();
134
199
  - ✅ Radio buttons: `<input type="radio" data-bind="choice">`
135
200
  - ✅ Nested paths: `<span data-bind="user.name"></span>`
136
201
 
137
- **NEW in v0.3.0:**
138
- - Returns cleanup function
139
- - Better error messages with `[Lume.js]` prefix
140
- - Handles edge cases (empty bindings, invalid paths)
202
+ **Multiple Checkboxes Pattern:**
203
+
204
+ For multiple checkboxes, use nested state instead of arrays:
205
+
206
+ ```javascript
207
+ // ✅ Recommended: Nested state objects
208
+ const store = state({
209
+ tags: state({
210
+ javascript: true,
211
+ python: false,
212
+ go: true
213
+ })
214
+ });
215
+ ```
216
+
217
+ ```html
218
+ <input type="checkbox" data-bind="tags.javascript"> JavaScript
219
+ <input type="checkbox" data-bind="tags.python"> Python
220
+ <input type="checkbox" data-bind="tags.go"> Go
221
+ ```
222
+
223
+ Nested state is **more explicit and easier to validate** than array-based bindings. See [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md#why-nested-state-for-multiple-checkboxes-instead-of-arrays) for the full rationale.
224
+
225
+ **Features:**
226
+ - ✅ Auto-waits for DOM if needed (no timing issues!)
227
+ - ✅ Returns cleanup function
228
+ - ✅ Better error messages with `[Lume.js]` prefix
229
+ - ✅ Handles edge cases (empty bindings, invalid paths)
230
+ - ✅ Two-way binding for form inputs
231
+
232
+ ### `isReactive(obj)`
233
+
234
+ Checks whether a value is a reactive proxy created by `state()`.
235
+
236
+ ```javascript
237
+ import { state, isReactive } from 'lume-js';
238
+
239
+ const original = { count: 1 };
240
+ const store = state(original);
241
+
242
+ isReactive(store); // true
243
+ isReactive(original); // false
244
+ isReactive(null); // false
245
+ ```
246
+
247
+ **How it works:**
248
+ Lume.js uses an internal `Symbol` checked via the Proxy `get` trap rather than mutating the proxy or storing external WeakSet state. Accessing `obj[REACTIVE_SYMBOL]` returns `true` only for reactive proxies, and the symbol is not enumerable or visible via `Object.getOwnPropertySymbols`.
249
+
250
+ **Characteristics:**
251
+ - ✅ Zero mutation of the proxy
252
+ - ✅ Invisible to enumeration and reflection
253
+ - ✅ Fast: single symbol identity check in the `get` path
254
+ - ✅ Supports nested reactive states naturally
255
+ - ✅ Skips tracking meta `$`-prefixed methods (e.g. `$subscribe`)
256
+
257
+ **When to use:** Utility/debugging, conditional wrapping patterns like:
258
+ ```javascript
259
+ function ensureReactive(val) {
260
+ return isReactive(val) ? val : state(val);
261
+ }
262
+ ```
263
+
264
+ **Why Auto-Ready?**
265
+
266
+ Works seamlessly regardless of script placement:
267
+
268
+ ```html
269
+ <!-- ✅ Works in <head> -->
270
+ <script type="module">
271
+ import { state, bindDom } from 'lume-js';
272
+ const store = state({ count: 0 });
273
+ bindDom(document.body, store); // Auto-waits for DOM!
274
+ </script>
275
+
276
+ <!-- ✅ Works inline in <body> -->
277
+ <body>
278
+ <span data-bind="count"></span>
279
+ <script type="module">
280
+ // bindDom() waits for DOMContentLoaded automatically
281
+ </script>
282
+ </body>
283
+
284
+ <!-- ✅ Works with defer -->
285
+ <script type="module" defer>
286
+ // Already loaded, executes immediately
287
+ </script>
288
+ ```
289
+
290
+ **When to use `immediate: true`:**
291
+
292
+ Rare scenarios where you're dynamically creating DOM or need precise control:
293
+
294
+ ```javascript
295
+ // Dynamic DOM injection
296
+ const container = document.createElement('div');
297
+ container.innerHTML = '<span data-bind="count"></span>';
298
+ document.body.appendChild(container);
299
+
300
+ // Bind immediately (DOM already exists)
301
+ bindDom(container, store, { immediate: true });
302
+ ```
141
303
 
142
304
  ### `$subscribe(key, callback)`
143
305
 
144
- Manually subscribe to state changes. Returns unsubscribe function.
306
+ Manually subscribe to state changes. Calls callback immediately with current value, then on every change.
145
307
 
146
308
  ```javascript
147
309
  const unsubscribe = store.$subscribe('count', (value) => {
@@ -157,10 +319,135 @@ const unsubscribe = store.$subscribe('count', (value) => {
157
319
  unsubscribe();
158
320
  ```
159
321
 
160
- **NEW in v0.3.0:**
161
- - Returns unsubscribe function (was missing in v0.2.x)
162
- - Validates callback is a function
163
- - Only notifies on actual value changes
322
+ **Features:**
323
+ - Returns unsubscribe function
324
+ - Validates callback is a function
325
+ - Calls immediately with current value (not batched)
326
+ - ✅ Only notifies on actual value changes (via batching)
327
+
328
+ ---
329
+
330
+ ## Addons
331
+
332
+ Lume.js provides optional addons for advanced reactivity patterns. Import from `lume-js/addons`.
333
+
334
+ ### `computed(fn)`
335
+
336
+ Creates a computed value that automatically updates when its dependencies change.
337
+
338
+ ```javascript
339
+ import { state, effect } from 'lume-js';
340
+ import { computed } from 'lume-js/addons';
341
+
342
+ const store = state({ count: 5 });
343
+
344
+ const doubled = computed(() => store.count * 2);
345
+ console.log(doubled.value); // 10
346
+
347
+ store.count = 10;
348
+ // After microtask:
349
+ console.log(doubled.value); // 20 (auto-updated)
350
+
351
+ // Subscribe to changes
352
+ const unsub = doubled.subscribe(value => {
353
+ console.log('Doubled:', value);
354
+ });
355
+
356
+ // Cleanup
357
+ doubled.dispose();
358
+ unsub();
359
+ ```
360
+
361
+ **Features:**
362
+ - ✅ Automatic dependency tracking using `effect()`
363
+ - ✅ Cached values (only recomputes when dependencies change)
364
+ - ✅ Subscribe to changes with `.subscribe(callback)`
365
+ - ✅ Cleanup with `.dispose()`
366
+ - ✅ Error handling (sets to undefined on error)
367
+
368
+ ### `watch(store, key, callback)`
369
+
370
+ Alias for `$subscribe` - observes changes to a specific state key.
371
+
372
+ ```javascript
373
+ import { state } from 'lume-js';
374
+ import { watch } from 'lume-js/addons';
375
+
376
+ const store = state({ count: 0 });
377
+
378
+ const unwatch = watch(store, 'count', (value) => {
379
+ console.log('Count is now:', value);
380
+ });
381
+
382
+ // Cleanup
383
+ unwatch();
384
+ ```
385
+
386
+ **Note:** `watch()` is just a convenience wrapper around `store.$subscribe()`. Use whichever feels more natural.
387
+
388
+ ---
389
+
390
+ ## Choosing the Right Reactive Pattern
391
+
392
+ Lume.js provides three ways to react to state changes. Here's when to use each:
393
+
394
+ | Pattern | Use When | Pros | Cons |
395
+ |---------|----------|------|------|
396
+ | **`bindDom()`** | Syncing state ↔ DOM | Zero code, declarative HTML | DOM-only, no custom logic |
397
+ | **`$subscribe()`** | Listening to specific keys | Explicit, immediate, simple | Manual dependency tracking |
398
+ | **`effect()`** | Auto-run code on any state access | Automatic dependencies, concise | Microtask delay, can infinite loop |
399
+ | **`computed()`** | Deriving values from state | Cached, automatic recompute | Addon import, slight overhead |
400
+
401
+ **Quick Decision Tree:**
402
+
403
+ ```
404
+ Need to update DOM?
405
+ ├─ Yes, just sync form/text → Use bindDom()
406
+ └─ No, custom logic needed
407
+ ├─ Watch single key? → Use $subscribe()
408
+ ├─ Watch multiple keys dynamically? → Use effect()
409
+ └─ Derive a value? → Use computed()
410
+ ```
411
+
412
+ **Examples:**
413
+
414
+ ```javascript
415
+ // 1. bindDom - Zero code DOM sync
416
+ <span data-bind="count"></span>
417
+ bindDom(document.body, store);
418
+
419
+ // 2. $subscribe - Specific key, immediate execution
420
+ store.$subscribe('count', (val) => {
421
+ if (val > 10) showNotification('High!');
422
+ });
423
+
424
+ // 3. effect - Multiple keys, automatic tracking
425
+ effect(() => {
426
+ document.title = `${store.user.name}: ${store.count}`;
427
+ // Tracks both user.name and count automatically
428
+ });
429
+
430
+ // 4. computed - Derive cached value
431
+ import { computed } from 'lume-js/addons';
432
+ const total = computed(() => store.items.reduce((sum, i) => sum + i.price, 0));
433
+ console.log(total.value);
434
+ ```
435
+
436
+ **Gotchas:**
437
+
438
+ - ⚠️ **effect()** runs in next microtask (~0.002ms delay). Use `$subscribe()` for immediate execution.
439
+ - ⚠️ **Don't mutate tracked state inside effect** - causes infinite loops:
440
+ ```javascript
441
+ // ❌ BAD - Infinite loop
442
+ effect(() => {
443
+ store.count++; // Writes to what it reads!
444
+ });
445
+
446
+ // ✅ GOOD - Read-only or separate keys
447
+ effect(() => {
448
+ store.displayCount = store.count * 2; // Different keys
449
+ });
450
+ ```
164
451
 
165
452
  ---
166
453
 
@@ -262,14 +549,65 @@ window.addEventListener('beforeunload', () => {
262
549
  <input type="checkbox" data-bind="user.settings.notifications">
263
550
  ```
264
551
 
552
+ ### Using Effects for Auto-Updates
553
+
554
+ ```javascript
555
+ import { state, effect } from 'lume-js';
556
+
557
+ const store = state({
558
+ firstName: 'Alice',
559
+ lastName: 'Smith'
560
+ });
561
+
562
+ // Auto-update title when name changes
563
+ effect(() => {
564
+ document.title = `${store.firstName} ${store.lastName}`;
565
+ });
566
+
567
+ store.firstName = 'Bob';
568
+ // Title automatically updates to "Bob Smith" in next microtask
569
+ ```
570
+
571
+ ### Computed Values
572
+
573
+ ```javascript
574
+ import { state } from 'lume-js';
575
+ import { computed } from 'lume-js/addons';
576
+
577
+ const cart = state({
578
+ items: state([
579
+ state({ price: 10, quantity: 2 }),
580
+ state({ price: 15, quantity: 1 })
581
+ ])
582
+ });
583
+
584
+ const total = computed(() => {
585
+ return cart.items.reduce((sum, item) =>
586
+ sum + (item.price * item.quantity), 0
587
+ );
588
+ });
589
+
590
+ console.log(total.value); // 35
591
+
592
+ cart.items[0].quantity = 3;
593
+ // After microtask:
594
+ console.log(total.value); // 45
595
+ ```
596
+
265
597
  ### Integration with GSAP
266
598
 
267
599
  ```javascript
268
600
  import gsap from 'gsap';
269
- import { state } from 'lume-js';
601
+ import { state, effect } from 'lume-js';
270
602
 
271
603
  const ui = state({ x: 0, y: 0 });
272
604
 
605
+ // Use effect for automatic animation updates
606
+ effect(() => {
607
+ gsap.to('.box', { x: ui.x, y: ui.y, duration: 0.5 });
608
+ });
609
+
610
+ // Or use $subscribe
273
611
  const unsubX = ui.$subscribe('x', (value) => {
274
612
  gsap.to('.box', { x: value, duration: 0.5 });
275
613
  });
@@ -283,17 +621,28 @@ window.addEventListener('beforeunload', () => unsubX());
283
621
  ### Cleanup Pattern (Important!)
284
622
 
285
623
  ```javascript
624
+ import { state, effect, bindDom } from 'lume-js';
625
+ import { computed } from 'lume-js/addons';
626
+
286
627
  const store = state({ data: [] });
287
628
  const cleanup = bindDom(root, store);
288
629
 
289
630
  const unsub1 = store.$subscribe('data', handleData);
290
631
  const unsub2 = store.$subscribe('status', handleStatus);
291
632
 
633
+ const effectCleanup = effect(() => {
634
+ console.log('Data length:', store.data.length);
635
+ });
636
+
637
+ const total = computed(() => store.data.length * 2);
638
+
292
639
  // Cleanup when component unmounts
293
640
  function destroy() {
294
- cleanup(); // Remove DOM bindings
295
- unsub1(); // Remove subscription 1
296
- unsub2(); // Remove subscription 2
641
+ cleanup(); // Remove DOM bindings
642
+ unsub1(); // Remove subscription 1
643
+ unsub2(); // Remove subscription 2
644
+ effectCleanup(); // Stop effect
645
+ total.dispose(); // Stop computed
297
646
  }
298
647
 
299
648
  // For SPA frameworks
@@ -380,25 +729,27 @@ document.addEventListener('click', () => store.clicks++);
380
729
 
381
730
  ---
382
731
 
383
- ## What's New in v0.3.0?
384
-
385
- ### Breaking Changes
386
- - ✅ `subscribe` `$subscribe` (restored from v0.1.0)
387
- - ✅ `$subscribe` now returns unsubscribe function
388
-
389
- ### New Features
390
- - ✅ TypeScript definitions (`index.d.ts`)
391
- - ✅ `bindDom()` returns cleanup function
732
+ ## What's New
733
+
734
+ ### Core Features
735
+ - ✅ **Automatic dependency tracking** - `effect()` automatically tracks which state properties are accessed
736
+ - ✅ **Per-state batching** - Each state object maintains its own microtask flush for optimal performance
737
+ - ✅ **Effect deduplication** - Effects only run once per flush cycle, even if multiple dependencies change
738
+ - **TypeScript support** - Full type definitions in `index.d.ts`
739
+ - ✅ **Cleanup functions** - All reactive APIs return cleanup/unsubscribe functions
740
+
741
+ ### Addons
742
+ - ✅ **`computed()`** - Memoized computed values with automatic dependency tracking
743
+ - ✅ **`watch()`** - Convenience alias for `$subscribe`
744
+
745
+ ### API Design
746
+ - ✅ `state()` - Create reactive state with automatic tracking support
747
+ - ✅ `effect()` - Core reactivity primitive (automatic dependency collection)
748
+ - ✅ `bindDom()` - DOM binding with two-way sync for form inputs
749
+ - ✅ `$subscribe()` - Manual subscriptions (calls immediately with current value)
750
+ - ✅ All functions return cleanup/unsubscribe functions
392
751
  - ✅ Better error handling with `[Lume.js]` prefix
393
- - ✅ Input validation (only plain objects)
394
- - ✅ Only triggers on actual value changes
395
- - ✅ Support for checkboxes, radio buttons, number inputs
396
- - ✅ Comprehensive example in `/examples/comprehensive/`
397
-
398
- ### Bug Fixes
399
- - ✅ Fixed memory leaks (no cleanup in v0.2.x)
400
- - ✅ Fixed addon examples (used wrong API)
401
- - ✅ Better path resolution with detailed errors
752
+ - ✅ Input validation on all public APIs
402
753
 
403
754
  ---
404
755
 
@@ -413,13 +764,49 @@ document.addEventListener('click', () => store.clicks++);
413
764
 
414
765
  ---
415
766
 
767
+ ## Testing
768
+
769
+ Lume.js uses [Vitest](https://vitest.dev) with a jsdom environment. The test suite mirrors the source tree: files under `tests/core/**` map to `src/core/**`, and `tests/addons/**` map to `src/addons/**`.
770
+
771
+ ```bash
772
+ # Run tests
773
+ npm test
774
+
775
+ # Watch mode
776
+ npm run test:watch
777
+
778
+ # Coverage with HTML report in ./coverage
779
+ npm run coverage
780
+ ```
781
+
782
+ **Import alias:** Tests use an alias so you can import from `src/...` without relative `../../` paths.
783
+
784
+ ```js
785
+ // vitest.config.js
786
+ resolve: {
787
+ alias: {
788
+ src: fileURLToPath(new URL('./src', import.meta.url))
789
+ }
790
+ }
791
+ ```
792
+
793
+ **Current coverage:**
794
+ - 100% statements, functions, and lines
795
+ - 100% branches (including edge-case paths)
796
+ - 67 tests covering core behavior, addons, inputs (text/checkbox/radio/number/range/select/textarea), nested state, reactive identity, and cleanup semantics
797
+
798
+ ---
799
+
416
800
  ## Contributing
417
801
 
418
802
  We welcome contributions! Please:
419
803
 
420
804
  1. **Focus on:** Examples, documentation, bug fixes, performance
421
805
  2. **Avoid:** Adding core features without discussion (keep it minimal!)
422
- 3. **Check:** Project specification for philosophy
806
+ 3. **Read:** [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) to understand our philosophy and why certain choices were made
807
+ 4. **Propose alternatives:** If you think a design decision should be reconsidered, open an issue with your reasoning
808
+
809
+ Before suggesting new features, check if they align with Lume's core principles: standards-only, minimal API, no build step required.
423
810
 
424
811
  ---
425
812
 
package/package.json CHANGED
@@ -1,14 +1,28 @@
1
1
  {
2
2
  "name": "lume-js",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
7
  "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "import": "./src/index.js",
12
+ "types": "./src/index.d.ts"
13
+ },
14
+ "./addons": {
15
+ "import": "./src/addons/index.js",
16
+ "types": "./src/addons/index.d.ts"
17
+ }
18
+ },
8
19
  "scripts": {
9
20
  "dev": "vite",
10
21
  "build": "echo 'No build step needed - zero-runtime library!'",
11
- "preview": "vite preview"
22
+ "size": "node scripts/check-size.js",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "coverage": "vitest run --coverage"
12
26
  },
13
27
  "files": [
14
28
  "src",
@@ -45,7 +59,10 @@
45
59
  },
46
60
  "homepage": "https://github.com/sathvikc/lume-js#readme",
47
61
  "devDependencies": {
48
- "vite": "^7.1.9"
62
+ "vite": "^7.1.9",
63
+ "vitest": "^2.1.4",
64
+ "@vitest/coverage-v8": "^2.1.4",
65
+ "jsdom": "^25.0.1"
49
66
  },
50
67
  "engines": {
51
68
  "node": ">=20.19.0"