lume-js 1.0.0 → 2.0.0-alpha.2

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
@@ -4,9 +4,13 @@
4
4
 
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
+ > **Current Release:** v1.0.0 (stable) | **Next Release:** v2.0.0-alpha.2
8
+ > Install stable: `npm install lume-js@1.0.0`
9
+ > Install next: `npm install lume-js@next`
10
+
7
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
- [![Version](https://img.shields.io/badge/version-1.0.0-green.svg)](package.json)
9
- [![Tests](https://img.shields.io/badge/tests-114%20passing-brightgreen.svg)](tests/)
12
+ [![Version](https://img.shields.io/badge/version-2.0.0--alpha.2-orange.svg)](package.json)
13
+ [![Tests](https://img.shields.io/badge/tests-193%20passing-brightgreen.svg)](tests/)
10
14
  [![Size](https://img.shields.io/badge/size-%3C2KB-blue.svg)](dist/)
11
15
 
12
16
  ## Why Lume.js?
@@ -93,8 +97,10 @@ Full documentation is available in the [docs/](docs/) directory:
93
97
  - **[Tutorial: Build Tic-Tac-Toe](docs/tutorials/build-tic-tac-toe.md)**
94
98
  - **[Working with Arrays](docs/tutorials/working-with-arrays.md)**
95
99
  - **API Reference**
96
- - [Core (state, bindDom, effect)](docs/api/core/state.md)
97
- - [Addons (computed, repeat)](docs/api/addons/computed.md)
100
+ - [Core (state, bindDom)](docs/api/core/state.md)
101
+ - [Effect System](docs/api/core/effect.md)
102
+ - [Plugins (v2.0+)](docs/api/core/plugins.md)
103
+ - [Addons (computed, repeat, debug)](docs/api/addons/computed.md)
98
104
  - **[Design Philosophy](docs/design/design-decisions.md)**
99
105
 
100
106
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lume-js",
3
- "version": "1.0.0",
3
+ "version": "2.0.0-alpha.2",
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",
@@ -23,7 +23,9 @@
23
23
  "size": "node scripts/check-size.js",
24
24
  "test": "vitest run",
25
25
  "test:watch": "vitest",
26
- "coverage": "vitest run --coverage"
26
+ "coverage": "vitest run --coverage",
27
+ "typecheck": "tsc --noEmit",
28
+ "validate": "npm run size && npm run typecheck && npm test"
27
29
  },
28
30
  "files": [
29
31
  "src",
@@ -53,7 +55,7 @@
53
55
  "license": "MIT",
54
56
  "repository": {
55
57
  "type": "git",
56
- "url": "https://github.com/sathvikc/lume-js.git"
58
+ "url": "git+https://github.com/sathvikc/lume-js.git"
57
59
  },
58
60
  "bugs": {
59
61
  "url": "https://github.com/sathvikc/lume-js/issues"
@@ -69,6 +71,8 @@
69
71
  "marked": "^17.0.1",
70
72
  "postcss": "^8.5.6",
71
73
  "tailwindcss": "^4.1.17",
74
+ "terser": "^5.44.1",
75
+ "typescript": "^5.9.3",
72
76
  "vite": "^7.1.9",
73
77
  "vitest": "^2.1.4"
74
78
  },
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Lume-JS Debug Addon
3
+ *
4
+ * Developer-friendly logging and inspection of reactive state operations.
5
+ * Critical for adoption - hard to debug = hard to adopt.
6
+ *
7
+ * Usage:
8
+ * import { createDebugPlugin, debug } from "lume-js/addons";
9
+ *
10
+ * const store = state({ count: 0 }, {
11
+ * plugins: [createDebugPlugin({ label: 'myStore' })]
12
+ * });
13
+ *
14
+ * debug.enable(); // Enable logging
15
+ * debug.filter('count'); // Only log 'count' key
16
+ * debug.stats(); // Show statistics
17
+ *
18
+ * @module addons/debug
19
+ */
20
+
21
+ // Global debug state
22
+ let globalEnabled = true;
23
+ let globalFilter = null; // string, RegExp, or null
24
+ const stats = new Map(); // label -> { gets: Map, sets: Map, notifies: Map }
25
+
26
+ /**
27
+ * Check if a key matches the current filter
28
+ * @param {string} key
29
+ * @returns {boolean}
30
+ */
31
+ function matchesFilter(key) {
32
+ if (globalFilter === null) return true;
33
+ if (typeof globalFilter === 'string') {
34
+ return key.includes(globalFilter);
35
+ }
36
+ if (globalFilter instanceof RegExp) {
37
+ return globalFilter.test(key);
38
+ }
39
+ return true;
40
+ }
41
+
42
+ /**
43
+ * Get or create stats entry for a label
44
+ * @param {string} label
45
+ * @returns {object}
46
+ */
47
+ function getStats(label) {
48
+ if (!stats.has(label)) {
49
+ stats.set(label, {
50
+ gets: new Map(),
51
+ sets: new Map(),
52
+ notifies: new Map()
53
+ });
54
+ }
55
+ return stats.get(label);
56
+ }
57
+
58
+ /**
59
+ * Increment a stat counter
60
+ * @param {string} label
61
+ * @param {'gets'|'sets'|'notifies'} type
62
+ * @param {string} key
63
+ */
64
+ function incrementStat(label, type, key) {
65
+ const s = getStats(label);
66
+ const map = s[type];
67
+ map.set(key, (map.get(key) || 0) + 1);
68
+ }
69
+
70
+ /**
71
+ * Format value for logging (truncate long values)
72
+ * @param {any} value
73
+ * @returns {string}
74
+ */
75
+ function formatValue(value) {
76
+ try {
77
+ const json = JSON.stringify(value);
78
+ if (json && json.length > 100) {
79
+ return json.slice(0, 97) + '...';
80
+ }
81
+ return json;
82
+ } catch {
83
+ return String(value);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Create a debug plugin instance for a reactive state store.
89
+ *
90
+ * @param {object} [options] - Configuration options
91
+ * @param {string} [options.label='store'] - Label for log messages
92
+ * @param {boolean} [options.logGet=false] - Log property reads (can be noisy)
93
+ * @param {boolean} [options.logSet=true] - Log property writes
94
+ * @param {boolean} [options.logNotify=true] - Log subscriber notifications
95
+ * @param {boolean} [options.trace=false] - Show stack trace for SET operations
96
+ * @returns {object} Plugin object for state()
97
+ *
98
+ * @example
99
+ * const store = state({ count: 0 }, {
100
+ * plugins: [createDebugPlugin({ label: 'counter' })]
101
+ * });
102
+ *
103
+ * @example
104
+ * // With stack traces for debugging where state changes originate
105
+ * const store = state({ count: 0 }, {
106
+ * plugins: [createDebugPlugin({ label: 'counter', trace: true })]
107
+ * });
108
+ */
109
+ export function createDebugPlugin(options = {}) {
110
+ const label = options.label ?? 'store';
111
+
112
+ // IMPORTANT: Do NOT destructure options here!
113
+ // Options may contain getters for dynamic runtime toggling (e.g., from UI).
114
+ // Destructuring would copy values once at creation time, breaking reactivity.
115
+ // Use getOpt() helper to read options dynamically in each hook.
116
+ const getOpt = (name, defaultVal) => {
117
+ const val = options[name];
118
+ return val !== undefined ? val : defaultVal;
119
+ };
120
+
121
+ return {
122
+ name: `debug:${label}`,
123
+
124
+ onInit: () => {
125
+ if (globalEnabled) {
126
+ console.log(`%c[${label}]%c initialized`, 'color: #888; font-weight: bold', 'color: inherit');
127
+ }
128
+ },
129
+
130
+ onGet: (key, value) => {
131
+ // Skip internal properties
132
+ if (typeof key === 'string' && key.startsWith('$')) {
133
+ return value;
134
+ }
135
+
136
+ incrementStat(label, 'gets', key);
137
+
138
+ if (globalEnabled && getOpt('logGet', false) && matchesFilter(key)) {
139
+ console.log(
140
+ `%c[${label}]%c GET %c${key}%c = ${formatValue(value)}`,
141
+ 'color: #888; font-weight: bold',
142
+ 'color: #4CAF50',
143
+ 'color: #2196F3; font-weight: bold',
144
+ 'color: inherit'
145
+ );
146
+ }
147
+
148
+ return value;
149
+ },
150
+
151
+ onSet: (key, newValue, oldValue) => {
152
+ // Skip internal properties
153
+ if (typeof key === 'string' && key.startsWith('$')) {
154
+ return newValue;
155
+ }
156
+
157
+ incrementStat(label, 'sets', key);
158
+
159
+ if (globalEnabled && getOpt('logSet', true) && matchesFilter(key)) {
160
+ console.log(
161
+ `%c[${label}]%c SET %c${key}%c: ${formatValue(oldValue)} → ${formatValue(newValue)}`,
162
+ 'color: #888; font-weight: bold',
163
+ 'color: #FF9800',
164
+ 'color: #2196F3; font-weight: bold',
165
+ 'color: inherit'
166
+ );
167
+
168
+ // Show stack trace if enabled (helps find where state changes originate)
169
+ if (getOpt('trace', false)) {
170
+ console.trace(`%c[${label}] Stack trace for ${key}`, 'color: #888');
171
+ }
172
+ }
173
+
174
+ return newValue;
175
+ },
176
+
177
+ onSubscribe: (key) => {
178
+ if (globalEnabled && matchesFilter(key)) {
179
+ console.log(
180
+ `%c[${label}]%c SUBSCRIBE %c${key}`,
181
+ 'color: #888; font-weight: bold',
182
+ 'color: #9C27B0',
183
+ 'color: #2196F3; font-weight: bold'
184
+ );
185
+ }
186
+ },
187
+
188
+ onNotify: (key, value) => {
189
+ // Skip internal properties
190
+ if (typeof key === 'string' && key.startsWith('$')) {
191
+ return;
192
+ }
193
+
194
+ incrementStat(label, 'notifies', key);
195
+
196
+ if (globalEnabled && getOpt('logNotify', true) && matchesFilter(key)) {
197
+ console.log(
198
+ `%c[${label}]%c NOTIFY %c${key}%c = ${formatValue(value)}`,
199
+ 'color: #888; font-weight: bold',
200
+ 'color: #E91E63',
201
+ 'color: #2196F3; font-weight: bold',
202
+ 'color: inherit'
203
+ );
204
+ }
205
+ }
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Global debug controls
211
+ */
212
+ export const debug = {
213
+ /**
214
+ * Enable debug logging globally
215
+ */
216
+ enable() {
217
+ globalEnabled = true;
218
+ console.log('%c[lume-debug]%c Logging enabled', 'color: #888; font-weight: bold', 'color: #4CAF50');
219
+ },
220
+
221
+ /**
222
+ * Disable debug logging globally
223
+ */
224
+ disable() {
225
+ globalEnabled = false;
226
+ console.log('%c[lume-debug]%c Logging disabled', 'color: #888; font-weight: bold', 'color: #F44336');
227
+ },
228
+
229
+ /**
230
+ * Check if debug logging is currently enabled
231
+ * @returns {boolean}
232
+ */
233
+ isEnabled() {
234
+ return globalEnabled;
235
+ },
236
+
237
+ /**
238
+ * Filter logs by key pattern
239
+ * @param {string|RegExp|null} pattern - Pattern to match, or null to clear filter
240
+ */
241
+ filter(pattern) {
242
+ globalFilter = pattern;
243
+ if (pattern === null) {
244
+ console.log('%c[lume-debug]%c Filter cleared', 'color: #888; font-weight: bold', 'color: inherit');
245
+ } else {
246
+ console.log(`%c[lume-debug]%c Filter set: ${pattern}`, 'color: #888; font-weight: bold', 'color: inherit');
247
+ }
248
+ },
249
+
250
+ /**
251
+ * Get current filter pattern
252
+ * @returns {string|RegExp|null}
253
+ */
254
+ getFilter() {
255
+ return globalFilter;
256
+ },
257
+
258
+ /**
259
+ * Get statistics data (silent - no console output)
260
+ * Use logStats() if you want to see stats in console.
261
+ * @returns {object} Stats object for programmatic access
262
+ */
263
+ stats() {
264
+ const result = {};
265
+
266
+ for (const [label, data] of stats) {
267
+ result[label] = {
268
+ gets: Object.fromEntries(data.gets),
269
+ sets: Object.fromEntries(data.sets),
270
+ notifies: Object.fromEntries(data.notifies)
271
+ };
272
+ }
273
+
274
+ return result;
275
+ },
276
+
277
+ /**
278
+ * Log statistics summary to console (with formatting)
279
+ * @returns {object} Stats object for programmatic access
280
+ */
281
+ logStats() {
282
+ const result = this.stats();
283
+
284
+ if (Object.keys(result).length === 0) {
285
+ console.log('%c[lume-debug]%c No stats collected yet', 'color: #888; font-weight: bold', 'color: inherit');
286
+ return result;
287
+ }
288
+
289
+ console.group('%c[lume-debug] Statistics', 'color: #888; font-weight: bold');
290
+
291
+ for (const [label, data] of Object.entries(result)) {
292
+ console.group(`%c${label}`, 'color: #2196F3; font-weight: bold');
293
+
294
+ // Use console.table for better formatted output
295
+ const tableData = [];
296
+ const allKeys = new Set([
297
+ ...Object.keys(data.gets),
298
+ ...Object.keys(data.sets),
299
+ ...Object.keys(data.notifies)
300
+ ]);
301
+
302
+ for (const key of allKeys) {
303
+ tableData.push({
304
+ key,
305
+ gets: data.gets[key] || 0,
306
+ sets: data.sets[key] || 0,
307
+ notifies: data.notifies[key] || 0
308
+ });
309
+ }
310
+
311
+ if (tableData.length > 0) {
312
+ console.table(tableData);
313
+ }
314
+
315
+ console.groupEnd();
316
+ }
317
+
318
+ console.groupEnd();
319
+
320
+ return result;
321
+ },
322
+
323
+ /**
324
+ * Reset all collected statistics
325
+ */
326
+ resetStats() {
327
+ stats.clear();
328
+ console.log('%c[lume-debug]%c Stats reset', 'color: #888; font-weight: bold', 'color: inherit');
329
+ }
330
+ };
@@ -115,19 +115,19 @@ export type ScrollPreservation = (container: HTMLElement, context?: Preservation
115
115
  export interface RepeatOptions<T> {
116
116
  /** Function to extract unique key from item */
117
117
  key: (item: T) => string | number;
118
-
118
+
119
119
  /** Function to render/update an item's element */
120
120
  render: (item: T, element: HTMLElement, index: number) => void;
121
-
121
+
122
122
  /** Element tag name or factory function (default: 'div') */
123
123
  element?: string | (() => HTMLElement);
124
-
124
+
125
125
  /**
126
126
  * Focus preservation strategy (default: defaultFocusPreservation)
127
127
  * Set to null to disable focus preservation
128
128
  */
129
129
  preserveFocus?: FocusPreservation | null;
130
-
130
+
131
131
  /**
132
132
  * Scroll preservation strategy (default: defaultScrollPreservation)
133
133
  * Set to null to disable scroll preservation
@@ -256,3 +256,97 @@ export function repeat<T>(
256
256
  arrayKey: string,
257
257
  options: RepeatOptions<T>
258
258
  ): Unsubscribe;
259
+
260
+ /**
261
+ * Options for createDebugPlugin
262
+ */
263
+ export interface DebugPluginOptions {
264
+ /** Label for log messages (default: 'store') */
265
+ label?: string;
266
+ /** Log property reads - can be noisy (default: false) */
267
+ logGet?: boolean;
268
+ /** Log property writes (default: true) */
269
+ logSet?: boolean;
270
+ /** Log subscriber notifications (default: true) */
271
+ logNotify?: boolean;
272
+ /** Show stack trace for SET operations - helps find where state changes originate (default: false) */
273
+ trace?: boolean;
274
+ }
275
+
276
+ /**
277
+ * State plugin with lifecycle hooks for debugging
278
+ */
279
+ export interface DebugPlugin {
280
+ name: string;
281
+ onInit?: () => void;
282
+ onGet?: (key: string, value: any) => any;
283
+ onSet?: (key: string, newValue: any, oldValue: any) => any;
284
+ onSubscribe?: (key: string) => void;
285
+ onNotify?: (key: string, value: any) => void;
286
+ }
287
+
288
+ /**
289
+ * Create a debug plugin instance for a reactive state store.
290
+ * Logs state operations to the console with colored output.
291
+ *
292
+ * @param options - Configuration options
293
+ * @returns Plugin object for state()
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * import { state } from 'lume-js';
298
+ * import { createDebugPlugin } from 'lume-js/addons';
299
+ *
300
+ * const store = state({ count: 0 }, {
301
+ * plugins: [createDebugPlugin({ label: 'counter' })]
302
+ * });
303
+ * ```
304
+ */
305
+ export function createDebugPlugin(options?: DebugPluginOptions): DebugPlugin;
306
+
307
+ /**
308
+ * Statistics for a single store
309
+ */
310
+ export interface DebugStats {
311
+ gets: Record<string, number>;
312
+ sets: Record<string, number>;
313
+ notifies: Record<string, number>;
314
+ }
315
+
316
+ /**
317
+ * Global debug controls for Lume.js
318
+ */
319
+ export interface Debug {
320
+ /** Enable debug logging globally */
321
+ enable(): void;
322
+ /** Disable debug logging globally */
323
+ disable(): void;
324
+ /** Check if debug logging is enabled */
325
+ isEnabled(): boolean;
326
+ /** Filter logs by key pattern (string, RegExp, or null to clear) */
327
+ filter(pattern: string | RegExp | null): void;
328
+ /** Get current filter pattern */
329
+ getFilter(): string | RegExp | null;
330
+ /** Get statistics data (silent - no console output) */
331
+ stats(): Record<string, DebugStats>;
332
+ /** Log statistics to console with table formatting */
333
+ logStats(): Record<string, DebugStats>;
334
+ /** Reset all collected statistics */
335
+ resetStats(): void;
336
+ }
337
+
338
+ /**
339
+ * Global debug controls
340
+ *
341
+ * @example
342
+ * ```typescript
343
+ * import { debug } from 'lume-js/addons';
344
+ *
345
+ * debug.enable(); // Enable logging
346
+ * debug.filter('count'); // Only log keys containing 'count'
347
+ * debug.stats(); // Show statistics
348
+ * debug.disable(); // Disable logging
349
+ * ```
350
+ */
351
+ export const debug: Debug;
352
+
@@ -1,3 +1,4 @@
1
1
  export { computed } from "./computed.js";
2
2
  export { watch } from "./watch.js";
3
- export { repeat } from "./repeat.js";
3
+ export { repeat } from "./repeat.js";
4
+ export { createDebugPlugin, debug } from "./debug.js";