Releases: sathvikc/lume-js
v2.0.0
Lume.js v2.0.0 Release Notes
Release Date: May 5, 2026
🎉 What's New
Lume.js v2.0.0 stable is a major evolution from v1.0.0. The core APIs (state(), effect(), bindDom()) remain unchanged in signature, but the internals are completely rearchitected for security and extensibility. New capabilities include an extensible DOM handler system, a plugin wrapper, explicit effect dependencies, minified CDN bundles, and a suite of new addons.
🏆 Highlights
- 319 tests | 100% funcs, 99.61% stmts/lines, 99.29% branches across 15 source files (+205 from v1.0.0)
- Core bundle: 2.45KB gzipped / 3KB budget (81.5%)
- Extensible handler system: Add custom
data-*attribute behaviors vialume-js/handlers - Plugin system via
withPlugins(): Intercept reads, writes, and notifications without touching core repeat()stabilized: Keyed list rendering promoted from@experimentalto fully supported- Security hardening: Eliminated
globalThis.__LUME_CURRENT_EFFECT__spoofing vector - Minified CDN bundles:
dist/index.min.mjs,dist/addons.min.mjs,dist/handlers.min.mjs - Console-less environment safety: Works in service workers and embedded engines
📦 Key Features
1. Extensible Handler System (NEW in v2)
Custom data-* attribute handlers for DOM binding, shipped as lume-js/handlers.
import { bindDom } from 'lume-js';
import { show, classToggle } from 'lume-js/handlers';
bindDom(document.body, store, {
handlers: [show, classToggle('active')]
});Built-in handlers: data-hidden, data-disabled, data-checked, data-required, data-aria-expanded, data-aria-hidden. User handlers override built-ins with the same attribute name. lume-js/handlers did not exist in v1.0.0.
2. Plugin System via withPlugins() (NEW in v2)
Intercept reads, writes, subscriptions, and notifications without modifying core.
import { state } from 'lume-js';
import { withPlugins, createDebugPlugin } from 'lume-js/addons';
const store = withPlugins(state({ count: 0 }), [
createDebugPlugin({ label: 'counter' })
]);Plugin hooks: onInit, onGet, onSet, onNotify, onSubscribe. Full error isolation per hook. withPlugins() returns a new proxy — core state() is untouched. Includes $dispose() for cleanup in long-lived SPAs.
3. effect() with Explicit Dependencies (NEW in v2)
effect(fn) auto-tracking existed in v1.0.0. v2 adds an explicit dependency overload for side-effects that should not auto-track all reads.
import { effect } from 'lume-js';
// Auto-tracking (unchanged from v1):
effect(() => {
console.log(store.count); // tracks 'count' automatically
});
// Explicit dependencies (NEW in v2):
effect(() => {
analytics.track('count', store.count);
}, [[store, 'count']]);4. New Addons (NEW in v2)
withPlugins()— intercept reads, writes, subscriptions, and notifications without modifying core. Already covered in section 2 above.createCleanupGroup()— collect cleanup/unsubscribe functions, dispose all at once. Ideal for route changes or component unmount.hydrateState()— reads initial state from<script type="application/json">in server-rendered HTML. Zero-config SSR hydration.debugaddon — log all reactive operations with configurable output.
5. repeat() Promoted to Stable (IMPROVED in v2)
repeat() existed in v1.0.0 as @experimental. In v2.0.0 it is fully supported with 59 tests and zero open correctness issues.
import { repeat } from 'lume-js/addons';
repeat(listContainer, store, 'items', {
render: (item) => `<li>${item.name}</li>`,
key: (item) => item.id
});Refactored in v2: reconcileDOM and applyPreservation helpers extracted from inline code for maintainability. Focus/scroll preservation and lifecycle hooks (onInsert, onRemove, onMove) existed in v1.0.0.
6. Minified CDN Bundles (NEW in v2)
Terser-generated *.min.mjs for direct <script type="module"> CDN usage.
<script type="module">
import { state, bindDom, effect } from 'https://unpkg.com/lume-js@2.0.0';
import { withPlugins, repeat } from 'https://unpkg.com/lume-js@2.0.0/addons';
</script>7. Security & Architecture Hardening (IMPROVED in v2)
- Replaced
globalThis.__LUME_CURRENT_EFFECT__with module-scopedcurrentEffect+getCurrentEffect()export. Third-party scripts can no longer spoof dependency tracking. - Scope-based read tracking via
withReadObserver:state.jshas zero knowledge ofeffect.jswhen no effect is running. Multiple simultaneous observers supported via aSetof active readers. - Console-less safety:
src/utils/log.jsensures the library works in service workers and embedded engines without crashing on missingconsole.
📊 Stats & Improvements
Test Coverage
- 319 tests | 100% funcs, 99.61% stmts/lines, 99.29% branches across 14 source files
- +205 tests since v1.0.0 (114 → 319)
- New test suites: handlers, integration, withPlugins, debug, cleanupGroup, hydrateState
Bundle Size
- Core: 2.45KB gzipped (81.5% of 3KB budget)
- Core files:
core/bindDom.js— 1.04KBcore/effect.js— 0.48KBcore/state.js— 0.92KB
Performance (vs v1.0.0)
- Unsubscribe:
indexOf + splicereplaces.filter()— O(n) without allocation - Flush loop: Stable-index
whileloops replace array spreads — zero per-flush allocations withPluginsflush: Single microtask via$beforeFlushhook- Effect cleanup:
splice(0)replaces[...cleanups]spread - Console-less environments: No runtime errors in service workers
📚 Resources
- Website: sathvikc.github.io/lume-js/
- Documentation: README.md
- State API: docs/api/core/state.md
- bindDom API: docs/api/core/bindDom.md
- Addon Docs: docs/api/addons
- Cleanup Guide: docs/guides/cleanup-and-dispose.md
- SSR Hydration Guide: docs/guides/ssr-hydration.md
- Changelog: CHANGELOG.md
⚙️ Installation
npm install lume-js<!-- CDN -->
<script type="module">
import { state, bindDom, effect } from 'https://unpkg.com/lume-js@2.0.0';
import { withPlugins, repeat } from 'https://unpkg.com/lume-js@2.0.0/addons';
</script>🚀 Migration Guide
From v1.0.0 to v2.0.0
Breaking Changes
-
isReactive()import path changed// v1.0.0: import { isReactive } from 'lume-js'; // v2.0.0: import { isReactive } from 'lume-js/addons';
-
Package imports from
dist/(pre-built)
v1.0.0 shipped raw source files asmain. v2.0.0 ships pre-builtdist/index.mjs.
Bundlers and direct imports work identically; CDN URLs should use the new path if referencing files directly.
New Features You Can Adopt
-
Custom DOM handlers:
import { bindDom } from 'lume-js'; import { show } from 'lume-js/handlers'; bindDom(root, store, { handlers: [show] });
-
Plugins via
withPlugins():import { withPlugins } from 'lume-js/addons'; const store = withPlugins(state({ count: 0 }), [myPlugin]);
-
Explicit effect dependencies (for side-effects that should not auto-track):
effect(() => { analytics.track('count', store.count); }, [[store, 'count']]);
-
createCleanupGroup()— batch dispose:import { createCleanupGroup } from 'lume-js/addons'; const group = createCleanupGroup(); group.add(effect(() => { ... })); group.add(bindDom(root, store)); group.dispose();
-
hydrateState()— SSR hydration:import { hydrateState } from 'lume-js/addons'; const store = hydrateState('server-state');
-
Minified CDN bundles:
<script type="module"> import { state } from 'https://unpkg.com/lume-js@2.0.0/dist/index.min.mjs'; </script>
💬 Getting Help
- Questions: Open a GitHub Discussion
- Bugs: Open a GitHub Issue
Full Changelog: v1.0.0...v2.0.0
v2.0.0-beta.3
Lume.js v2.0.0-beta.3 Release Notes
Release Date: May 5, 2026
🎉 What's New
🛡️ Audit-Driven Hardening & Type Safety
Lume.js v2.0.0-beta.3 is a pre-stable hardening release focused on closing all audited P0/P1 items that are safe to fix without a full benchmark regression suite. This release fixes TypeScript declaration bugs for consumers, patches a real memory leak in withPlugins, optimizes the effect cleanup hot path, and promotes the repeat addon from experimental to stable. No breaking changes from beta.2.
🏆 Highlights
- TypeScript declarations fixed: Invalid
Symbol('lume.reactive')replaced with properunique symbol;withReadObservernow typed in public entry point withPluginsmemory leak patched:$dispose()method exposed to clean up$beforeFlushhooks — prevents unbounded growth when re-wrapping stores- Effect cleanup optimized:
cleanups.splice(0)replaces[...cleanups]spread — zero iterator allocation per effect re-run repeat()promoted to stable:@experimentalJSDoc tag removed after 59 tests and zero open correctness issues in beta.2- Test Coverage: 319 tests | 100% stmts/branches/funcs/lines across all 16 source files
- Bundle Size: 2.45KB gzipped core (81.5% of 3KB budget)
📦 Key Features
1. TypeScript Consumer-Facing Fixes
Invalid reactive brand symbol (index.d.ts)
The computed property name readonly [Symbol('lume.reactive')]?: true; was invalid TypeScript — Symbol() call expressions cannot appear in interface property positions. Replaced with a declared unique symbol.
// Before (beta.2): INVALID — TypeScript error for consumers
readonly [Symbol('lume.reactive')]?: true;
// After (beta.3):
declare const lumeReactiveSymbol: unique symbol;
readonly [lumeReactiveSymbol]?: true;withReadObserver type declaration
Exported from src/core/state.js since beta.2 but missing from src/index.js and src/index.d.ts. Now discoverable with full (onRead, fn) signature.
// Now works:
import { withReadObserver } from 'lume-js';
// Type-safe signature is included in index.d.ts (marked @internal)Stale JSDoc example corrected
The createDebugPlugin documentation still showed the removed state(obj, { plugins }) API from beta.1. Updated to the current withPlugins(state(obj), [...]) pattern.
2. withPlugins Memory Leak Fix
Root cause: withPlugins() registered runNotifyHooks via store.$beforeFlush() but never stored the returned unsubscribe. Since runNotifyHooks is a fresh closure per call, the $beforeFlush dedupe (indexOf === -1) couldn't prevent growth — each wrapper had a different function reference. In long-lived SPAs with route changes, this caused unbounded beforeFlushHooks growth.
Fix:
- Capture
flushUnsub = store.$beforeFlush(runNotifyHooks) - Expose
store.$dispose()on the returned proxy that callsflushUnsub()and clearspendingNotifications
const store = withPlugins(state({ count: 0 }), [myPlugin]);
// ... later, on route change:
store.$dispose(); // removes beforeFlush hook, clears pending state3. Effect Cleanup Performance
Before: const oldCleanups = [...cleanups]; — spreads the subscriptions array via the iterator protocol, allocating a new Array on every effect re-run.
After: const oldCleanups = cleanups.splice(0); — single native operation that returns removed items and empties the source array. Zero behavioral change; restore/dispose paths remain identical.
4. repeat() Stabilized
The keyed reconciler, focus/scroll preservation, and DOM lifecycle hooks have been stable since beta.2. The @experimental JSDoc tag has been removed — repeat() is now a fully supported addon.
5. New Addons (from beta.2 → reflected in docs)
createCleanupGroup()— collects cleanup/unsubscribe functions and disposes them all at oncehydrateState()— reads initial state from<script type="application/json">for zero-config SSR hydration- Minified CDN bundles —
dist/index.min.mjs,dist/addons.min.mjs,dist/handlers.min.mjsvia Terser
📊 Stats & Improvements
Test Coverage
- 319 tests | 100% stmts/branches/funcs/lines across all 16 source files
- +16 tests added since beta.2 (2 new
$disposetests, addon coverage expansion) - Edge case coverage for: plugin disposal, multi-wrap isolation, type declaration validity
Bundle Size
- Core: 2.45KB gzipped (81.5% of 3KB budget)
- Core files:
core/bindDom.js— 1.04KBcore/effect.js— 0.48KBcore/state.js— 0.92KB
Performance
- Effect cleanup: Eliminated per-run array iterator allocation —
splice(0)vs[...cleanups]spread withPluginsdisposal: Proper cleanup path for dynamic plugin wrapping/unwrapping in SPAs
📚 Resources
- Documentation: README.md
- State API: docs/api/core/state.md
- bindDom API: docs/api/core/bindDom.md
- Addon Docs: docs/api/addons
- Cleanup Guide: docs/guides/cleanup-and-dispose.md
- SSR Hydration Guide: docs/guides/ssr-hydration.md
- Changelog: CHANGELOG.md
⚙️ Installation
For Beta Testers
npm install lume-js@2.0.0-beta.3<!-- CDN -->
<script type="module">
import { state, bindDom } from 'https://unpkg.com/lume-js@2.0.0-beta.3';
import { withPlugins, repeat } from 'https://unpkg.com/lume-js@2.0.0-beta.3/addons';
</script>🚀 Migration Guide
From v2.0.0-beta.2 to v2.0.0-beta.3
No breaking changes. All APIs remain stable.
Optional improvements you can adopt:
-
Dispose plugin-wrapped stores on teardown:
const store = withPlugins(state({ count: 0 }), [myPlugin]); // On route change or component unmount: store.$dispose();
-
TypeScript users:
withReadObserveris now importable fromlume-jswith full type support (marked@internal— primarily for advanced use)
From v2.0.0-beta.1
See v2.0.0-beta.2 release notes for the state(obj, {plugins}) → withPlugins() migration.
⚠️ Beta Release Notice
This is a beta release intended for testing and feedback. The v2.0 API is stabilizing and approaching the stable release. No breaking changes are anticipated between beta.3 and v2.0.0 stable.
💬 Getting Help
- Questions: Open a GitHub Discussion
- Bugs: Open a GitHub Issue
Full Changelog: v2.0.0-beta.2...v2.0.0-beta.3
v2.0.0-beta.2
Lume.js v2.0.0-beta.2 Release Notes
Release Date: May 3, 2026
🎉 What's New
🏗️ Architecture Hardening & Core Decoupling
Lume.js v2.0.0-beta.2 is a major architectural overhaul focused on correctness, performance, and environmental safety. The core state.js module is now completely decoupled from effect.js via a scope-based read observer system, the plugin system has been moved out of core into a dedicated addon, and 11 critical bugs have been fixed across the core and addon layers.
👉 Explore the updated API docs: docs/api/core/state.md
🏆 Highlights
withReadObserverScope-Based Read Tracking:state.jsis now pure when no reader is active — no permanent reference toeffect.js- Plugin System Moved to Addons:
state(obj, { plugins })→withPlugins(state(obj), [plugins])fromlume-js/addons - 11 Bug Fixes: Frozen objects, flush loop isolation, duplicate keys in
repeat(), phantom options, brand symbol implementation, and more - Test Coverage: 303 tests | 100% stmts/branches/funcs/lines across all 15 source files
- Bundle Size: 2.45KB gzipped core (81.5% of 3KB budget)
- Performance: Eliminated double microtask flush in
withPlugins, eliminated per-flush array allocations, replacedfilter-based unsubscribe withindexOf+splice - Environment Hardening: Console-less environment safety via
src/utils/log.js(service workers, embedded engines)
📦 Key Features
1. Scope-Based Read Tracking (withReadObserver)
The global registerEffectSystem has been replaced with a synchronous withReadObserver(onRead, fn) scope. Read tracking is only active during the body of an effect's execution. Multiple simultaneous observers (nested effects, devtools) are supported via a Set of active readers.
// Before (alpha.2 / beta.1):
// state.js held a permanent callback reference to effect.js
// After (beta.2):
// state.js has no knowledge of effect.js
// Read tracking is scoped to the synchronous fn body
withReadObserver(
(store, key) => { /* register dependency */ },
() => { /* effect body — reads are tracked here only */ }
);state.jsis pure when no reader is active- Eliminates third-party spoofing via
globalThis.__LUME_CURRENT_EFFECT__→ replaced with module-scopedcurrentEffect
2. Plugin System Extracted to Addons
Plugins are no longer passed to state(). Use the new withPlugins() addon wrapper instead.
// Before (beta.1):
import { state } from 'lume-js';
const store = state(obj, { plugins: [myPlugin] });
// After (beta.2):
import { state } from 'lume-js';
import { withPlugins } from 'lume-js/addons';
const store = withPlugins(state(obj), [myPlugin]);withPlugins()returns the same reactive proxy with plugin hooks attached- Zero runtime overhead if no plugins are used
isReactive()also moved tolume-js/addons(duck-typing via$subscribecheck)
3. Critical Bug Fixes
| Bug | Fix |
|---|---|
| Frozen/sealed object rejection | state() now throws early instead of silently crashing |
| Flush loop error isolation | Scheduler recovers after catastrophic errors; flushScheduled always reset via try/finally |
repeat() duplicate keys |
Duplicate keys are skipped instead of overwriting element references |
withPlugins blocked writes |
onNotify no longer fires when onSet returns oldValue to block an update |
state() phantom options |
Removed non-existent options parameter from TypeScript declaration |
| Runtime brand symbol | Symbol.for('lume.reactive') is now actually stamped by state.js |
bindDom error swallowing |
Removed blanket try/catch; unexpected errors propagate with full stack traces |
repeat() subscribe cleanup |
Handles RxJS-style Subscription objects with .unsubscribe() |
computed.dispose() |
Added disposed flag to cancel stale microtasks |
$beforeFlush deduplication |
Same function reference registered multiple times no longer duplicates execution |
📊 Stats & Improvements
Test Coverage
- 303 tests | 100% stmts/branches/funcs/lines across all 15 source files
- 54 new tests added since beta.1
- Edge case coverage for: select-multiple binding, nested effect tracking, re-entry guards, handler overrides, plugin edge cases
Bundle Size
- Core: 2.45KB gzipped (81.5% of 3KB budget)
- Core files:
core/bindDom.js— 1.04KBcore/effect.js— 0.48KBcore/state.js— 0.92KB
Performance
- Eliminated double microtask flush in
withPlugins— collapsedonNotify+ subscriber flushes into single microtask - Eliminated per-flush array allocations in
state.flush— replaced spreads with stable-indexwhileloops and pre-sized arrays - Replaced
filter-based unsubscribe withindexOf+splice— O(n) without allocation
📚 Resources
- Documentation: README.md
- State API: docs/api/core/state.md
- bindDom API: docs/api/core/bindDom.md
- Addon Docs: docs/api/addons
- Changelog: CHANGELOG.md
⚙️ Installation
For Beta Testers
npm install lume-js@2.0.0-beta.2<!-- CDN -->
<script type="module">
import { state, bindDom } from 'https://unpkg.com/lume-js@2.0.0-beta.2';
import { withPlugins } from 'https://unpkg.com/lume-js@2.0.0-beta.2/addons';
</script>🚀 Migration Guide
From v2.0.0-beta.1 to v2.0.0-beta.2
⚠️ Breaking Changes
-
Plugin System Moved to Addons
// Before (beta.1): const store = state(obj, { plugins: [myPlugin] }); // After (beta.2): import { withPlugins } from 'lume-js/addons'; const store = withPlugins(state(obj), [myPlugin]);
-
isReactive()Import Path Changed// Before (beta.1): import { isReactive } from 'lume-js'; // After (beta.2): import { isReactive } from 'lume-js/addons';
-
resolvePath()Removed from Public API- Internal utility inlined into
bindDom.js src/core/utils.jsdeleted
- Internal utility inlined into
New Features You Can Adopt:
withPlugins()Addon: Cleaner separation of concerns — core stays minimal- Console-less Safety: Library now works in service workers and embedded engines
- Improved Error Propagation:
bindDomno longer swallows unexpected errors
⚠️ Beta Release Notice
This is a beta release intended for testing and feedback. The v2.0 API is stabilizing but may still see minor refinements before the stable release.
💬 Getting Help
- Questions: Open a GitHub Discussion
- Bugs: Open a GitHub Issue
Full Changelog: v2.0.0-beta.1...v2.0.0-beta.2
v2.0.0-beta.1
Lume.js v2.0.0-beta.1 Release Notes
Release Date: February 28, 2026
🎉 What's New
🔧 Extensible Handler System for Reactive DOM Binding
Lume.js v2.0.0-beta.1 introduces a powerful, extensible handler system for bindDom that transforms how reactive attributes are applied to DOM elements. Instead of hardcoded attribute lists, you can now define and register custom reactive handlers with a simple plain-object contract.
👉 Explore the handler API: docs/api/core/handlers.md
🏆 Highlights
- Extensible Handler System: Custom reactive
data-*attribute handlers via plain-object contract - Built-in Handlers:
data-hidden,data-disabled,data-checked,data-required,data-aria-*(backward compatible) - New
lume-js/handlersModule:show,boolAttr(),ariaAttr(),classToggle(),stringAttr(),htmlAttrs()presets - Test Coverage: 249 tests (up from 193 in alpha.2) | 100% code coverage
- Bundle Size: 2.39KB gzipped core (+60 bytes from handler interface, still within 3KB budget)
- Performance: Optimized cleanup loops, hot-path fast-path checks, WeakMap-based binding storage
- TypeScript: Full handler types, utility types, and enhanced definitions
📦 Key Features
1. Extensible Handler System
Handlers are plain objects with an attr string and an apply(el, val) function. No framework API required.
import { bindDom } from 'lume-js';
// Custom handler: toggle a CSS class based on state
const highlightHandler = {
attr: 'data-highlight',
apply(el, val) {
el.classList.toggle('highlight', !!val);
}
};
bindDom(document.body, store, {
handlers: [highlightHandler]
});- User handlers override built-ins with the same
attr(Map deduplication) - Arrays auto-flattened (supports
classToggle()returning multiple handlers) - Compiled selectors built from handler list for O(n) DOM performance
2. New lume-js/handlers Module (0.33KB gzipped, tree-shakeable)
import { show, boolAttr, ariaAttr, classToggle, stringAttr, htmlAttrs } from 'lume-js/handlers';
// show — inverse of data-hidden
bindDom(container, store, { handlers: [show] });
// <div data-show="isVisible">...</div>
// boolAttr — any boolean attribute
bindDom(container, store, { handlers: [boolAttr('readonly')] });
// <input data-readonly="isReadOnly" />
// classToggle — CSS class toggling
bindDom(container, store, { handlers: [classToggle('active', 'error')] });
// <div data-class-active="isActive" data-class-error="hasError">...</div>
// htmlAttrs() preset — standard HTML attributes
import { htmlAttrs } from 'lume-js/handlers';
bindDom(container, store, { handlers: htmlAttrs });show—data-show="key"shows element when truthyboolAttr(name)— toggle any boolean attribute viatoggleAttribute()ariaAttr(name)— any ARIA attribute with autoaria-prefix handlingclassToggle(...names)— CSS class togglingstringAttr(name)— string attributes with null removalformHandlerspreset —[boolAttr('readonly')]a11yHandlerspreset —[ariaAttr('pressed'), ariaAttr('selected'), ariaAttr('disabled')]htmlAttrs()preset — 20+ standard HTML attributes as reactive handlers
3. Internal Architecture Refactoring
bindDomrefactored from hardcodedBOOLEAN_ATTRS/ARIA_ATTRSarrays to composable handler pattern- Zero breaking changes — existing code works without modification
- WeakMap-based binding storage for memory efficiency
📊 Stats & Improvements
Test Coverage
- 249 tests (up from 193 in alpha.2)
- Added 48 new handler tests covering
show,classToggle,boolAttr,ariaAttr,stringAttr, custom handlers, composition, overrides, presets, and edge cases - 100% code coverage maintained
Bundle Size
- Core: 2.39KB gzipped
- Budget: 3KB gzipped (temporarily raised from 2KB to accommodate new features)
- Reason: Added handler interface and built-in handlers. Optimization planned for stable release.
Performance
- Optimized cleanup loops in
effect.jsandcomputed.js - Added fast-path checks in
state.jshot paths - WeakMap-based binding storage in
bindDom.js
📚 Resources
- Documentation: README.md
- Handler API: docs/api/core/handlers.md
- bindDom API: docs/api/core/bindDom.md
- Changelog: CHANGELOG.md
⚙️ Installation
For Beta Testers
npm install lume-js@2.0.0-beta.1<!-- CDN -->
<script type="module">
import { state, bindDom } from 'https://unpkg.com/lume-js@2.0.0-beta.1';
import { show, classToggle } from 'https://unpkg.com/lume-js@2.0.0-beta.1/handlers';
</script>🚀 Migration Guide
From v2.0.0-alpha.2 to v2.0.0-beta.1
No breaking changes. All alpha.2 code continues to work as-is.
New Features You Can Adopt:
- Custom Handlers: Replace inline DOM manipulation with declarative handlers
- Handler Presets: Use
htmlAttrs()for one-shot reactive HTML attribute binding - Improved TypeScript: Enhanced types for handler interfaces and utility types
⚠️ Beta Release Notice
This is a beta release intended for testing and feedback. The API is largely stable but may still undergo refinement before v2.0.0 stable.
💬 Getting Help
- Questions: Open a GitHub Discussion
- Bugs: Open a GitHub Issue
Full Changelog: v2.0.0-alpha.2...v2.0.0-beta.1
v2.0.0-alpha.2
Lume.js v2.0.0-alpha.2 Release Notes
Release Date: January 14, 2026
🎉 What's New
🔌 Explicit Effect Dependencies & Debugging Tools
Lume.js v2.0.0-alpha.2 brings significant enhancements to developer experience and control. We've introduced "Explicit Mode" for effects to give you fine-grained control over reactivity, a powerful new Debug Addon for inspecting your stores, and major performance optimizations for DOM binding.
👉 Explore the new Effect docs: docs/api/core/effect.md
🏆 Highlights
- Explicit Effects: Dual-mode
effect()support (Auto-tracking vs Explicit Tuples) - Debug Addon: Zero-config debugging tool with colored logs and stats
- Performance: Event delegation in
bindDom(N listeners → 1 listener) - Repeat API: Improved
create/updateseparation for cleaner list rendering - Test Coverage: 193 passing tests (up from 162 in alpha.1)
- Bundle Size: ~2.1KB gzipped (temporarily slightly over budget due to new features)
📦 Key Features
1. Explicit Effect Dependencies
You can now choose between "Magic" (auto-tracking) and "Explicit" (dependency array) modes.
import { state, effect } from 'lume-js';
const store = state({ count: 0, user: 'Alice' });
// Old way: Auto-tracking (still default!)
effect(() => {
console.log(store.count);
});
// NEW: Explicit Mode
// Only re-runs when 'count' changes, even if we read other properties!
effect(() => {
console.log(`User ${store.user} has count: ${store.count}`);
}, [[store, 'count']]); // <--- Only tracks 'count'2. Debug Addon 🐞
A developer-friendly tool to inspect reactive state operations.
import { createDebugPlugin, debug } from 'lume-js/addons';
const store = state({ count: 0 }, {
plugins: [createDebugPlugin({ label: 'my-store' })]
});
debug.enable(); // Turn on logging
store.count++;
// Console: [my-store] SET count: 0 → 1Features:
- Global Controls:
debug.enable(),debug.filter('key') - Stats API:
debug.logStats()shows a table of GET/SET/NOTIFY operations - Stack Traces: Find exactly where a state change originated
3. Event Delegation in bindDom
We've rewritten bindDom to use a single event listener on the root element instead of attaching listeners to every input.
- O(1) lookup using
Map - Huge memory savings for large forms
- Completely internal change (no API breakage)
📚 Resources
- Documentation: README.md
- Effect API: docs/api/core/effect.md
- Debug Addon: docs/api/addons/debug.md
- Changelog: CHANGELOG.md
📊 Stats & Improvements
Test Coverage
- 193 tests (up from 162)
- Added 6 new tests for Explicit Effects
- Added 24 new tests for Debug Addon
- 100% code coverage maintained
Bundle Size
- Core: 2.14 KB gzipped (slightly over 2KB budget)
- Reason: Added explicit dependency parsing and event delegation logic. Optimization planned for Beta release.
⚙️ Installation
For Early Adopters
npm install lume-js@next⚠️ Alpha Release Notice
This is an alpha release intended for testing and feedback.
Not recommended for production until stable v2.0.0.
💬 Getting Help
- Questions: Open a GitHub Discussion
- Bugs: Open a GitHub Issue
Happy coding with Lume.js! 🌟
Full Changelog: v2.0.0-alpha.1...v2.0.0-alpha.2
v2.0.0-alpha.1
Lume.js v2.0.0-alpha.1 Release Notes
Release Date: December 19, 2025
🎉 What's New
🔌 Plugin System (Phase 1)
Lume.js v2.0 introduces a powerful plugin system that allows you to extend and customize reactive behavior while maintaining the standards-only philosophy. This alpha release marks the first phase of our v2.0 roadmap.
👉 Explore the plugin system: docs/api/core/plugins.md
🏆 Highlights
- Plugin System: 5 lifecycle hooks for complete control over state behavior
- Backward Compatible: All v1.0 code works unchanged - plugins are opt-in
- Type-Safe: Full TypeScript definitions for all plugin APIs
- Documentation: Comprehensive plugin guide, design decisions, and 4 working examples
- Bundle Size: 1.98KB / 2KB gzipped (slight increase, will optimize in Phase 2)
- 100% Test Coverage: 148 passing tests (up from 114 in v1.0.0)
📦 Plugin System Features
5 Lifecycle Hooks
import { state } from 'lume-js';
const store = state({ count: 0 }, {
plugins: [
{
// Called once when state is created
onInit() {
console.log('State initialized');
},
// Intercept property reads (effects track key, not value)
onGet(key, value) {
console.log('Get:', key, value);
return value; // Can transform value
},
// Intercept property writes
onSet(key, newValue, oldValue) {
console.log('Set:', key, newValue);
return newValue; // Return transformed value
},
// Called when a property subscription is added
onSubscribe(key) {
console.log('Subscribe:', key);
},
// Called before subscribers are notified
onNotify(key, value) {
console.log('Notify:', key, value);
}
}
]
});
store.count = 5; // Plugin hooks fire!Hook Execution Order
- onGet → Transform value before effects track (effects track key access, not value)
- onSet → Validate/transform before update
- onSubscribe → Called when subscription is added
- onNotify → Called before each subscriber notification
Key Design Decisions
- ✅ Effects track key access, not transformed values - Plugin
onGethooks run first, effects observe key access regardless of value transformations - ✅ Plugins are opt-in - Zero overhead if you don't use them
- ✅ Array-based plugin order - Predictable execution (index 0 runs first)
- ✅ Chain pattern - Each plugin receives output of previous plugin (onGet, onSet)
- ✅ No magic - Standard JavaScript patterns only
📚 Example Plugins
1. Debug Plugin
Logs all state operations for development:
import { state } from 'lume-js';
const debugPlugin = {
onSet(key, newValue, oldValue) {
console.log(`[Debug] Set ${key}: ${oldValue} → ${newValue}`);
return newValue;
},
onSubscribe(key) {
console.log(`[Debug] Subscribed to "${key}"`);
},
onNotify(key, value) {
console.log(`[Debug] Notifying subscribers of "${key}" = ${value}`);
}
};
const store = state({ count: 0 }, {
plugins: [debugPlugin]
});
store.count = 5;
// Logs: [Debug] Set count: 0 → 5
// Logs: [Debug] Subscribed to "count"
// Logs: [Debug] Notifying subscribers of "count" = 52. Validation Plugin
Enforces data constraints:
const validationPlugin = {
onSet(key, newValue, oldValue) {
if (key === 'email' && !newValue.includes('@')) {
console.error('Invalid email:', newValue);
return oldValue; // Keep old value on validation error
}
return newValue;
}
};
const store = state({ email: 'user@example.com' }, {
plugins: [validationPlugin]
});
store.email = 'invalid'; // Blocked! Email unchanged.3. History Plugin
Time-travel debugging:
import { state } from 'lume-js';
const history = { undoStack: [], redoStack: [], isUndoRedo: false };
const historyPlugin = {
onSet(key, newValue, oldValue) {
if (!history.isUndoRedo && newValue !== oldValue) {
history.undoStack.push({ key, value: oldValue });
history.redoStack = []; // Clear redo stack on new change
}
return newValue;
}
};
const store = state({ count: 0 }, { plugins: [historyPlugin] });
store.count = 1;
store.count = 2;
store.count = 3;
// Undo function
function undo() {
if (history.undoStack.length === 0) return;
const lastChange = history.undoStack.pop();
history.redoStack.push({ key: lastChange.key, value: store[lastChange.key] });
history.isUndoRedo = true;
store[lastChange.key] = lastChange.value;
history.isUndoRedo = false;
}
undo(); // count = 2
undo(); // count = 14. Transform Plugin
Normalize/sanitize data:
import { state } from 'lume-js';
const transformPlugin = {
onSet(key, newValue) {
if (key === 'username' && typeof newValue === 'string') {
return newValue.toLowerCase().trim(); // Auto-normalize
}
return newValue;
}
};
const store = state({ username: '' }, { plugins: [transformPlugin] });
store.username = ' ALICE '; // Stored as "alice"
console.log(store.username); // "alice"📚 Resources
- Documentation: README.md
- Plugin Guide: docs/api/core/plugins.md
- Design Decisions: docs/design/design-decisions.md#why-plugin-system-v20
- Examples: Run
npm run devand visithttp://localhost:5173/examples/plugin-demo/ - Changelog: CHANGELOG.md
- GitHub: https://github.com/sathvikc/lume-js
- npm: https://www.npmjs.com/package/lume-js
📊 Stats & Improvements
Test Coverage
- 148 tests (up from 114 in v1.0.0)
- 34 new tests for the plugin system
- 100% code coverage maintained
- Plugin lifecycle tests, error handling, and integration tests
Bundle Size
- Core: 1.98 KB / 2.00 KB gzipped (99.0% of budget)
- Addons: 0.83 KB gzipped (unchanged from v1.0.0)
- Note: Temporary 2KB budget - will decrease when effect/bindDom move to addons in Phase 2
Documentation
- ✅ Comprehensive plugin guide (30+ examples)
- ✅ Design decisions document updated
- ✅ 4 working plugin examples (debug, validation, history, transform)
- ✅ TypeScript definitions complete
- ✅ README updated with v2.0+ markers
⚙️ Installation
For Early Adopters (v2.0.0-alpha.1)
npm install lume-js@nextFor Production (v1.0.0 - Stable)
npm install lume-js🚀 Migration Guide
From v1.0.0 to v2.0.0-alpha.1
No Breaking Changes! This is a backward-compatible feature release.
All v1.0 code works unchanged. The plugin system is opt-in:
// v1.0 code (still works)
const store = state({ count: 0 });
// v2.0 code (opt-in plugins)
const store = state({ count: 0 }, {
plugins: [debugPlugin, validationPlugin]
});⚠️ Alpha Release Notice
This is an alpha release intended for:
- ✅ Early adopters who want to try the plugin system
- ✅ Community feedback on the plugin API design
- ✅ Testing in non-production environments
Not recommended for production until stable v2.0.0 is released.
🔧 API Reference
state(initialState, options)
function state<T extends Record<string, any>>(
initialState: T,
options?: {
plugins?: Plugin[]
}
): T & ReactiveState;Plugin Interface
interface Plugin {
name: string;
onInit?(): void;
onGet?(key: string | symbol, value: any): any;
onSet?(key: string | symbol, newValue: any, oldValue: any): any;
onSubscribe?(key: string | symbol): void;
onNotify?(key: string | symbol, value: any): void;
}Plugin Example
import { state } from 'lume-js';
// Simple debug plugin
const debugPlugin: Plugin = {
name: 'debug',
onSet(key, newValue, oldValue) {
console.log(`Set ${String(key)}: ${oldValue} → ${newValue}`);
return newValue;
}
};
const store = state({ count: 0 }, {
plugins: [debugPlugin]
});Note: Plugins are simple objects - no helper functions needed. See docs/api/core/plugins.md for complete examples including debug, validation, history, and transform patterns.
🐛 Known Issues
None at this time. If you find a bug, please open an issue.
💬 Getting Help
- Questions: Open a GitHub Discussion
- Bugs: Open a GitHub Issue
- Feature Requests: Open an issue with the
enhancementlabel - Feedback: We'd love to hear your thoughts on the plugin system!
Happy coding with Lume.js! 🌟
Built for developers who want extensibility without the framework tax.
Full Changelog: v1.0.0...v2.0.0-alpha.1
v1.0.0
Lume.js v1.0.0 Release Notes
Release Date: November 29, 2025
🎉 What's New
🚀 First Stable Release
Lume.js is now production-ready, with a stable API, comprehensive documentation, and an official website:
👉 Explore the new site: sathvikc.github.io/lume-js/
🏆 Highlights
- Stable API: All core and most addon APIs are now stable and ready for production.
- Website Launch: Official docs, guides, and interactive examples now available at sathvikc.github.io/lume-js/.
- Comprehensive Documentation: Full API, guides, and tutorials included.
- 100% Test Coverage: 114 passing tests covering all features and edge cases.
- Performance: <2KB gzipped, no virtual DOM, zero dependencies, standards-only reactivity.
- Features:
- Core:
state,bindDom,effect, two-way binding, nested state, subscriptions - Addons:
computed,watch,isReactive repeatAddon: Experimental — efficient keyed list rendering, but API may evolve in future releases- TypeScript definitions for all APIs
- Tree-shaking and bundler optimization
- Core:
- Examples: Todo app, Tic-Tac-Toe, repeat-test, and more
⚠️ Experimental Status
The repeat addon is marked experimental in v1.0.0:
- API may evolve based on real-world usage feedback
- Performance characteristics being validated in production scenarios
- Edge cases may be discovered that require API adjustments
We encourage you to try it and provide feedback! The core functionality is solid and well-tested, but we want to gather user experience before locking the API.
📚 Resources
- Website: sathvikc.github.io/lume-js/
- Documentation: README.md
- Design Philosophy: docs/design/design-decisions.md
- Changelog: CHANGELOG.md
- Examples: Run
npm run devand visithttp://localhost:5173/examples/ - GitHub: https://github.com/sathvikc/lume-js
- npm: https://www.npmjs.com/package/lume-js
🐛 Known Issues
None at this time. If you find a bug, please open an issue.
💬 Getting Help
- Questions: Open a GitHub Discussion
- Bugs: Open a GitHub Issue
- Feature Requests: Open an issue with the
enhancementlabel
Happy coding with Lume.js! 🌟
Built for developers who want reactivity without the framework tax.
Full Changelog: v0.5.0...v1.0.0
v0.5.0
Lume.js v0.5.0 Release Notes
Release Date: November 28, 2025
🎉 What's New
List Rendering with repeat Addon (@experimental)
The star of this release is the new repeat addon - an efficient list rendering solution that brings the convenience of v-for/x-for while staying true to Lume's standards-only philosophy.
import { repeat } from 'lume-js/addons/repeat.js';
repeat('#list', store, 'todos', {
key: todo => todo.id,
render: (todo, el) => {
if (!el.dataset.init) {
el.innerHTML = `<input value="${todo.text}">`;
el.dataset.init = 'true';
}
}
});Key Features:
- ✅ Element Reuse by Key - Same DOM nodes persist across updates (no recreation)
- ✅ Focus Preservation - Maintains
activeElementand text selection during updates - ✅ Scroll Preservation - Intelligent scroll position handling for add/remove/reorder
- ✅ Automatic Subscription - Subscribes to array changes automatically
- ✅ Minimal DOM Operations - Only updates what changed
- ✅ Memory Efficient - Automatic cleanup on element removal
Important Note: The repeat addon requires immutable array updates for performance and simplicity:
// ❌ Won't trigger update
store.items.push(newItem);
// ✅ Triggers update
store.items = [...store.items, newItem];This trade-off enables instant reference equality checks (oldArray === newArray) instead of expensive deep array proxying or monkey-patching.
Design Philosophy Documentation
Added comprehensive DESIGN_DECISIONS.md explaining:
- Why standards-only approach matters
- Why objects instead of primitives
- Why immutable array updates in
repeat - Why no custom event handling syntax
- Why nested state must be explicitly wrapped
- And many more architectural decisions
This document helps contributors understand the "why" behind Lume's design and serves as a guide for future feature proposals.
📊 Stats & Improvements
Test Coverage
- 114 tests (up from 67 in v0.4.1)
- 47 new tests for the
repeataddon - 100% code coverage maintained
- Added regression tests for array mutation behavior
- Added integration tests for
repeat+computed
Examples
Updated and expanded examples:
- Todo App - Now uses
repeatfor list rendering - Tic-Tac-Toe - Demonstrates time travel and computed state
- Repeat Test - Comprehensive automated test suite for preservation features
- Comprehensive Example - Updated with latest patterns
Documentation
- Clarified immutable array requirement warnings throughout
- Added explicit
@experimentalmarker forrepeataddon - Improved README with better examples and API docs
- Added Contributing section emphasizing design philosophy
🔧 API Reference
repeat(container, store, key, options)
Parameters:
container(String|HTMLElement) - CSS selector or element to render intostore(Object) - Reactive state objectkey(String) - Property name of the array in storeoptions(Object):key(Function) - Unique key extractor:item => item.idrender(Function) - Render function:(item, el) => { /* update el */ }preserveFocus(Function|null) - Focus preservation strategy (default: built-in)preserveScroll(Function|null) - Scroll preservation strategy (default: built-in)
Returns: Cleanup function
Example:
const cleanup = repeat('#todo-list', store, 'todos', {
key: todo => todo.id,
render: (todo, el) => {
if (!el.dataset.init) {
el.innerHTML = `
<input type="checkbox" class="toggle">
<label></label>
<button class="destroy">×</button>
`;
el.dataset.init = 'true';
}
el.querySelector('.toggle').checked = todo.completed;
el.querySelector('label').textContent = todo.text;
}
});
// Later: cleanup when done
cleanup();Disabling Preservation:
repeat('#list', store, 'items', {
key: item => item.id,
render: (item, el) => { el.textContent = item.name; },
preserveFocus: null, // Disable focus preservation
preserveScroll: null // Disable scroll preservation
});🚀 Migration Guide
From v0.4.x to v0.5.0
No Breaking Changes! This is a minor version with new features only.
If you're manually rendering lists and want to try repeat:
Before (manual rendering):
store.$subscribe('items', (items) => {
const container = document.querySelector('#list');
container.innerHTML = items.map(item => `
<li>${item.name}</li>
`).join('');
});After (using repeat):
import { repeat } from 'lume-js/addons/repeat.js';
repeat('#list', store, 'items', {
key: item => item.id,
render: (item, el) => {
el.textContent = item.name;
}
});Important: Remember to update arrays immutably:
// Add item
store.items = [...store.items, newItem];
// Remove item
store.items = store.items.filter(item => item.id !== id);
// Update item
store.items = store.items.map(item =>
item.id === id ? { ...item, completed: true } : item
);
// Reorder
store.items = [...store.items].sort((a, b) => a.order - b.order);⚠️ Experimental Status
The repeat addon is marked @experimental because:
- API may evolve based on real-world usage feedback
- Performance characteristics being validated in production scenarios
- Edge cases may be discovered that require API adjustments
We encourage you to try it and provide feedback! The core functionality is solid and well-tested, but we want to gather user experience before locking the API.
📦 Bundle Size
Core library remains under 2KB (~1.45 KB) gzipped
📚 Resources
- Documentation: README.md
- Design Philosophy: DESIGN_DECISIONS.md
- Changelog: CHANGELOG.md
- Examples: Run
npm run devand visithttp://localhost:5173/examples/ - GitHub: https://github.com/sathvikc/lume-js
- npm: https://www.npmjs.com/package/lume-js
🐛 Known Issues
None at this time. If you find a bug, please open an issue.
💬 Getting Help
- Questions: Open a GitHub Discussion
- Bugs: Open a GitHub Issue
- Feature Requests: Open an issue with the
enhancementlabel
Happy coding with Lume.js! 🌟
Built for developers who want reactivity without the framework tax.
Full Changelog: v0.4.1...v0.5.0
v0.4.1
Release Notes: v0.4.1
🐛 Bug Fixes & Stability Improvements
This patch release improves developer experience and distribution quality while maintaining full backward compatibility with v0.4.0.
🆕 New Features
1. isReactive() - Type Guard for Reactive Proxies
Check whether an object is a Lume.js reactive proxy created by state().
import { state, isReactive } from 'lume-js';
const original = { count: 1 };
const store = state(original);
isReactive(store); // true
isReactive(original); // false
isReactive(null); // falseUse Cases:
- Conditional wrapping patterns
- Type guards in TypeScript
- Debugging and validation
- Library integration
Implementation:
- Zero mutation of the proxy
- Uses internal symbol checked via Proxy
gettrap - Symbol is not enumerable or visible via
Object.getOwnPropertySymbols - Fast: single identity check, no WeakSet lookups
- Skips tracking for
$-prefixed meta methods (e.g.,$subscribe)
Example - Defensive Wrapping:
function ensureReactive(val) {
return isReactive(val) ? val : state(val);
}2. Auto-Ready DOM Binding (DOMContentLoaded Auto-Wait)
bindDom() now automatically waits for DOMContentLoaded if the document is still loading, eliminating timing issues.
// ✅ Safe to call from <head> or anywhere
import { state, bindDom } from 'lume-js';
const store = state({ count: 0 });
bindDom(document.body, store); // Auto-waits if needed!Features:
- ✅ No manual
DOMContentLoadedevent handling needed - ✅ Safe in
<head>, inline<script>, or deferred scripts - ✅ Opt-out available with
{ immediate: true }option - ✅ Cleanup function works correctly in both modes
Advanced Usage:
// Force immediate binding (skip auto-wait)
const cleanup = bindDom(myElement, store, { immediate: true });Benefits:
- Zero friction for beginners
- Eliminates "DOM not ready" errors
- Works seamlessly with any script placement
- Standards-based (uses native
DOMContentLoadedevent)
📦 Distribution Improvements
1. Package Exports Field (Tree-Shaking Optimized)
Added proper exports field for better bundler support and tree-shaking.
{
"exports": {
".": {
"import": "./src/index.js",
"types": "./src/index.d.ts"
},
"./addons": {
"import": "./src/addons/index.js",
"types": "./src/addons/index.d.ts"
}
}
}Benefits:
- ✅ Explicit public API surface
- ✅ Better tree-shaking in Webpack, Rollup, Vite
- ✅ Prevents accidental deep imports
- ✅ Future-proof for package modernization
2. Side-Effects Declaration
Marked package as side-effect-free for aggressive tree-shaking.
{
"sideEffects": false
}Impact:
- Unused exports are fully removed by bundlers
- Smaller production bundles
- Better optimization opportunities
3. Separate Addon TypeScript Definitions
Created src/addons/index.d.ts for addon-specific types, ensuring proper IntelliSense when importing from lume-js/addons.
// Full type safety with addon imports
import { computed, watch } from 'lume-js/addons';Structure:
- Core types:
src/index.d.ts(state, bindDom, effect, isReactive) - Addon types:
src/addons/index.d.ts(computed, watch) - Shared types imported via relative path
📚 Documentation Updates
1. README Enhancements
- ✅ Added
isReactive()API documentation with examples - ✅ Documented auto-ready
bindDom()behavior - ✅ Added "When to use
immediate: true" guidance - ✅ Clarified script placement scenarios
- ✅ Updated TypeScript usage examples
2. DESIGN_DECISIONS.md Updates
- ✅ Documented
isReactive()implementation rationale - ✅ Explained
$-prefixed method exclusion from effect tracking - ✅ Added auto-ready design decision
- ✅ Document history updated
3. TypeScript Improvements
- ✅ Complete type definitions for all v0.4.0+ features
- ✅ Proper
Computed<T>interface with error handling - ✅ Generic type safety for
watch() - ✅ JSDoc examples for better IntelliSense
🔧 Internal Improvements
1. Effect Tracking Refinement
- Skip tracking for
$subscribeand other$-prefixed methods - Prevents meta-method access from creating spurious dependencies
- Cleaner separation between API methods and reactive properties
2. Reactive Marker Implementation
- Uses symbol-based identity check via Proxy
gettrap - No WeakSet or external storage needed
- Invisible to enumeration and reflection
- Zero performance overhead
3. Cleanup Pattern Consistency
- All subscription APIs return unsubscribe functions
- Cleanup works correctly before and after DOMContentLoaded
- Proper event listener removal in auto-ready mode
📊 Bundle Size
Core library remains under 2KB (~1.45 KB) gzipped
⚙️ Testing
- ✅ 67 tests passing (100% coverage maintained)
- ✅ New tests for
isReactive() - ✅ New tests for auto-ready
bindDom()behavior - ✅ Tests for
immediate: trueoption - ✅ Tests for cleanup before DOMContentLoaded
- ✅ Effect tracking exclusion tests
🔄 Migration Guide
From v0.4.0 to v0.4.1
No breaking changes - fully backward compatible!
New Features You Can Adopt:
1. Use isReactive() for Type Guards:
import { state, isReactive } from 'lume-js';
function processData(data) {
if (!isReactive(data)) {
data = state(data);
}
// Now guaranteed to be reactive
}2. Simplify Script Placement:
<!-- Before: Manual DOMContentLoaded handling -->
<script type="module">
document.addEventListener('DOMContentLoaded', () => {
bindDom(document.body, store);
});
</script>
<!-- After: Just call bindDom() -->
<script type="module">
bindDom(document.body, store); // Auto-waits!
</script>3. TypeScript Addon Imports:
// Now fully typed
import { computed, watch } from 'lume-js/addons';🐛 Known Issues
None. This is a stability release.
📝 Full Changelog
Added:
isReactive(obj)function for reactive type checking- Auto-ready
bindDom()with DOMContentLoaded auto-wait { immediate: true }option forbindDom()exportsfield in package.jsonsideEffects: falsedeclarationsrc/addons/index.d.tsfor addon types- Comprehensive TypeScript definitions
CHANGELOG.mdconsolidating release history
Fixed:
- Effect tracking now skips
$-prefixed meta methods - Cleanup function works correctly in auto-ready mode
- TypeScript addon import resolution
Improved:
- Documentation clarity for new features
- Type safety for all APIs
- Tree-shaking optimization
- Developer experience for script placement
📅 Release Date
November 20, 2025
🔗 Links
- GitHub: https://github.com/sathvikc/lume-js
- npm: https://www.npmjs.com/package/lume-js
- Documentation: See README.md
- Full Test Suite: Run
npm test
Full Changelog: v0.4.0...v0.4.1
v0.4.0
🎉 Major Release - Automatic Dependency Tracking
This is a significant upgrade that brings automatic reactivity to Lume.js while maintaining the standards-only philosophy.
⚡ Breaking Changes
computed() Now Uses Automatic Dependency Tracking
Before (v0.3.0):
const store = state({ count: 0 });
const doubled = computed(() => store.count * 2);
// Had to manually recompute
store.$subscribe('count', () => doubled.recompute());After (v0.4.0):
const store = state({ count: 0 });
const doubled = computed(() => store.count * 2);
// Auto-updates when store.count changes - no manual recompute!
store.count = 5;
console.log(doubled.value); // 10 (after microtask)Migration: Remove all manual .recompute() calls and $subscribe calls used for recomputation. The computed() function now automatically tracks dependencies and updates.
🚀 New Features
1. effect() - Core Automatic Dependency Tracking
New core primitive for creating reactive effects that automatically track which state properties they access.
import { state, effect } from 'lume-js';
const store = state({ count: 0, name: 'Alice' });
const cleanup = effect(() => {
// Automatically tracks 'count' (name not accessed)
document.title = `Count: ${store.count}`;
});
store.count = 5; // Effect re-runs automatically
store.name = 'Bob'; // Effect does NOT re-run
cleanup(); // Stop the effectFeatures:
- ✅ Automatic dependency collection
- ✅ Dynamic dependencies (re-tracks on every run)
- ✅ Returns cleanup function
- ✅ Prevents infinite recursion
- ✅ Integrates with per-state batching
2. Per-State Microtask Batching
State updates are now automatically batched using microtasks for optimal performance.
const store = state({ count: 0 });
effect(() => {
console.log('Count:', store.count);
});
// Multiple updates in same tick
store.count = 1;
store.count = 2;
store.count = 3;
// Effect only runs ONCE in next microtask with final value (3)Benefits:
- ✅ Reduces unnecessary re-renders
- ✅ Better performance for rapid updates
- ✅ Per-state batching (no global scheduler)
- ✅ Effects deduplicated per flush cycle
3. Enhanced computed() with Auto-Tracking
The computed() addon now uses effect() internally for automatic dependency tracking.
import { computed } from 'lume-js/addons';
const cart = state({
items: [
state({ price: 10, quantity: 2 }),
state({ price: 15, quantity: 1 })
]
});
const total = computed(() => {
return cart.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
});
console.log(total.value); // 35
cart.items[0].quantity = 3;
// Auto-updates to 45 in next microtaskNew API:
- ✅
.value- Get computed value (cached) - ✅
.subscribe(callback)- Watch for changes (returns unsubscribe) - ✅
.dispose()- Cleanup and stop tracking - ❌
.recompute()- REMOVED (automatic now)
🐛 Bug Fixes
Better Error Handling
- ✅
effect()errors logged with[Lume.js effect]prefix - ✅
computed()errors logged with[Lume.js computed]prefix - ✅ Errors set computed value to
undefined
📚 Documentation Updates
Enhanced README
- ✅ Added
effect()documentation and examples - ✅ Updated
computed()docs for automatic tracking - ✅ Added comparison of reactive patterns (effect vs subscribe)
- ✅ New examples showcasing auto-tracking
- ✅ Updated cleanup patterns
- ✅ Better TypeScript definitions
New Sections
- Testing guide with Vitest
- Bundle size information
- Effect vs Subscribe decision guide
- Advanced reactivity patterns
🔧 Technical Changes
State Management (src/core/state.js)
- ✅ Added
globalThis.__LUME_CURRENT_EFFECT__for dependency tracking - ✅ Proxy getter now registers effect dependencies
- ✅ Per-state
pendingNotificationsMap for batching - ✅ Per-state
pendingEffectsSet for deduplication - ✅
scheduleFlush()usesqueueMicrotask()
Effect System (src/core/effect.js) - NEW
- ✅ Automatic dependency collection via proxy getters
- ✅ Re-tracks dependencies on every execution
- ✅ Cleanup old subscriptions before re-running
- ✅ Prevents infinite recursion with
isRunningflag - ✅ Returns cleanup function
Computed (src/addons/computed.js)
- ✅ Now uses
effect()internally - ✅ Removed manual
.recompute()method - ✅ Auto-tracks dependencies
- ✅ Caches values and notifies subscribers on change
- ✅
.dispose()method for cleanup
TypeScript Definitions (src/index.d.ts)
- ✅ Added
effect()type definition - ✅ Updated
computed()return type (removedrecompute) - ✅ Better generic type support
- ✅ Unsubscribe type alias
📊 Stats
- 57 tests with 100% coverage
- Bundle size: Still under 2KB gzipped ✅
🎯 Migration Guide
If you used computed():
Remove manual recomputation:
const doubled = computed(() => store.count * 2);
- store.$subscribe('count', () => doubled.recompute());
+ // No longer needed - automatic!Remove .recompute() calls:
- doubled.recompute();
+ // Auto-updates when dependencies changeIf you used $subscribe() for derived state:
Consider switching to effect():
- const unsub1 = store.$subscribe('firstName', updateTitle);
- const unsub2 = store.$subscribe('lastName', updateTitle);
+ const cleanup = effect(() => {
+ document.title = `${store.firstName} ${store.lastName}`;
+ });If you manually batched updates:
Remove batching code:
- let timeout;
- store.$subscribe('value', (val) => {
- clearTimeout(timeout);
- timeout = setTimeout(() => updateUI(val), 0);
- });
+ effect(() => {
+ updateUI(store.value); // Auto-batched!
+ });⚠️ Notes
Effect Execution Timing
Effects run in the next microtask after state changes. If you need synchronous updates, use $subscribe() instead.
// Async (batched) - preferred for most cases
effect(() => {
console.log(store.count); // Runs in next microtask
});
// Sync (immediate) - use when timing matters
store.$subscribe('count', (val) => {
console.log(val); // Runs immediately
});Per-State Batching
If an effect depends on multiple state objects, it may run once per state that changes (by design). This keeps the architecture simple and predictable.
const store1 = state({ a: 1 });
const store2 = state({ b: 2 });
effect(() => {
console.log(store1.a + store2.b);
});
// Both change in same tick
store1.a = 10;
store2.b = 20;
// Effect runs TWICE (once per state flush)
// This is intentional - keeps effects simpleFull Changelog: v0.3.0...v0.4.0