liveinit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thomas Portelange
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # liveinit
2
+
3
+ A radically lean, ultra-fast, HTML-first application loader and DOM lifecycle observer.
4
+
5
+ `liveinit` provides a robust architecture for initializing Javascript components and delegating stateless actions safely without memory leaks, massive framework overhead, or Virtual DOMs. It bridges the gap between server-rendered HTML and client-side interactivity by turning standard HTML data attributes into reactive lifecycle hooks and event delegations.
6
+
7
+ It is heavily inspired by Stimulus, Alpine, and GitHub Catalyst, but stripped down to the absolute bare minimum (~150 lines of dependency-free modern Javascript).
8
+
9
+ ## Key Advantages
10
+
11
+ - **Zero Global Listeners Overhead:** The declarative `data-command` engine uses a single, global event listener delegator that catches bubbling actions. No tracking individual buttons, no multiple active listeners—saving memory and CPU.
12
+ - **GC & Memory Safe:** `initComponents` seamlessly constructs an `AbortController`. When DOM nodes are removed or replaced (e.g., via AJAX/HTMX), all associated event listeners are instantly aborted and garbage collected cleanly.
13
+ - **O(1) Evaluation Pattern:** Instead of walking entire DOM trees upon mutations, the core observer relies on blazing-fast native `querySelectorAll` with pre-compiled CSS strings.
14
+ - **Graceful Lazy-Loading:** Built-in `IntersectionObserver` optionally defers executing components until the user actually scrolls the element into view.
15
+ - **HTML-first:** Maintain beautiful, standard HTML strings. The parser natively understands relaxed JSON configuration strings, requiring no compilation steps or esoteric syntax constraints inside your templates.
16
+
17
+ ## Quick Start
18
+
19
+ **CDN / no-build (fastest setup):**
20
+
21
+ ```html
22
+ <script src="https://cdn.jsdelivr.net/npm/liveinit@0.1.0/dist/liveinit.min.js" defer></script>
23
+ ```
24
+
25
+ ```html
26
+ <div data-component="Counter" data-component-config="start: 10"></div>
27
+ <button data-command="refresh" data-command-for="#my-table">Reload Data</button>
28
+ <table id="my-table"></table>
29
+
30
+ <script>
31
+ class Counter {
32
+ constructor(el, config) {
33
+ el.textContent = `Count: ${config.start || 0}`;
34
+ }
35
+ }
36
+ window.Counter = Counter; // default resolver reads from window
37
+
38
+ document.addEventListener("command:refresh", () => {
39
+ console.log("refresh table");
40
+ });
41
+ </script>
42
+ ```
43
+
44
+ **Everything is configurable** (custom attributes, event list, resolver, lifecycle hooks, lazy behavior).
45
+ See [docs/initComponents.md](docs/initComponents.md) and [docs/initCommands.md](docs/initCommands.md) for full options.
46
+
47
+ ### Safe Script Loading
48
+
49
+ `liveinit` is structurally immune to script load order issues because of its core architecture:
50
+
51
+ 1. **MutationObserver foundation**: Even if `liveinit` executes before your HTML finishes rendering, it will instantly catch and initialize elements as they stream into the `<body>`.
52
+ 2. **Global Delegation**: `initCommands()` attaches listeners to the document root, meaning it doesn't care if the target buttons exist yet or are added asynchronously later.
53
+
54
+ To guarantee zero race-conditions and best performance, load the script high up in the document (like the `<head>`) with `defer`:
55
+
56
+ ```html
57
+ <script src="..." defer></script>
58
+ ```
59
+
60
+ **Bundler usage:**
61
+
62
+ ```javascript
63
+ import { initComponents, initCommands } from "liveinit";
64
+
65
+ initComponents({
66
+ dropdown: () => import("./components/dropdown.js"),
67
+ "heavy-map": () => import("./components/map.js"),
68
+ });
69
+ initCommands();
70
+ ```
71
+
72
+ ## Documentation
73
+
74
+ The library consists of modular, standalone pieces you can use together or separately:
75
+
76
+ 1. **[API Contract](docs/api.md)** - Stable public exports and return shapes.
77
+ 2. **[Init Components Engine](docs/initComponents.md)** (`initComponents.js`) - The main application loader tying everything together.
78
+ 3. **[Command Engine](docs/initCommands.md)** (`initCommands.js`) - The global configurable event delegator for stateless HTML actions.
79
+ 4. **[Selector Observer](docs/observer.md)** (`observer.js`) - The ultra-fast MutationObserver wrapper.
80
+ 5. **[Lazy Init](docs/lazy.md)** (`lazy.js`) - The IntersectionObserver bindings for deferred loading.
81
+ 6. **[Data Config](docs/parseConfig.md)** (`parseConfig.js`) - A relaxed, HTML-friendly JSON parser.
82
+
83
+ ## Browser Support
84
+
85
+ - `liveinit` targets browsers with native ES modules (`<script type="module">`) and modern DOM APIs.
86
+ - `IntersectionObserver` is optional:
87
+ if unavailable, `lazy()` falls back to immediate initialization instead of throwing.
88
+ - The table below reflects the baseline for the **full feature set** (`type="module"` + `AbortController` + core DOM APIs used by this library).
89
+
90
+ | Browser | Minimum version | First stable release |
91
+ |---------|-----------------|----------------------|
92
+ | Chrome | 66 | April 17, 2018 |
93
+ | Firefox | 57 | November 14, 2017 |
94
+ | Safari | 12.1 | March 25, 2019 |
95
+ | Edge | 16 | October 17, 2017 |
96
+
97
+ - `IntersectionObserver` remains optional (lazy init degrades to immediate init).
98
+ - Quick checks on caniuse:
99
+ - ES modules: <https://caniuse.com/es6-module>
100
+ - IntersectionObserver: <https://caniuse.com/intersectionobserver>
101
+ - AbortController: <https://caniuse.com/abortcontroller>
102
+
103
+ ## Semver Policy
104
+
105
+ - Patch releases (`x.y.z`) include bug fixes and internal changes only.
106
+ - Minor releases (`x.y.0`) can add new features/options in backward-compatible ways.
107
+ - Major releases (`x.0.0`) are used for breaking changes.
108
+
109
+ Breaking changes include:
110
+ - Renaming/removing public exports from `liveinit`.
111
+ - Changing default attribute names or command event naming behavior.
112
+ - Changing lifecycle contracts (for example return shapes from `initCommands()` or observer engine APIs).
113
+ - Raising the documented browser support baseline.
@@ -0,0 +1,2 @@
1
+ /*! liveinit | https://github.com/lekoala/liveinit | @license MIT */
2
+ (()=>{var{defineProperty:C,getOwnPropertyNames:S,getOwnPropertyDescriptor:y}=Object,p=Object.prototype.hasOwnProperty;var N=new WeakMap,b=(J)=>{var B=N.get(J),Q;if(B)return B;if(B=C({},"__esModule",{value:!0}),J&&typeof J==="object"||typeof J==="function")S(J).map((W)=>!p.call(B,W)&&C(B,W,{get:()=>J[W],enumerable:!(Q=y(J,W))||Q.enumerable}));return N.set(J,B),B};var h=(J,B)=>{for(var Q in B)C(J,Q,{get:B[Q],enumerable:!0,configurable:!0,set:(W)=>B[Q]=()=>W})};var u={};h(u,{default:()=>g});var w={};h(w,{parseConfig:()=>P,observer:()=>k,observeOpenShadowRoots:()=>j,lazy:()=>M,initComponents:()=>q,initCommands:()=>I});function P(J){if(typeof J!=="string")return{};let B=J.trim();if(B==="")return{};if(B=B.replace(/([a-zA-Z_$][\w$-]*)\s*:|'((?:\\'|[^'])*)'/g,(Q,W,L)=>{if(W)return`"${W}":`;return`"${L.replace(/\\'/g,"'").replace(/"/g,"\\\"")}"`}),!B.startsWith("{"))B=`{${B}}`;try{return JSON.parse(B)}catch(Q){return console.error(`Failed to parse: ${J}`),{}}}function I(J={}){let B=J.attribute||"data-command",Q=B.replace(/^data-/,""),W=`${B}-on`,L=`${B}-for`,K=`${B}-config`,D=J.events||["click","change","input","submit","focusin","focusout"],O={handleEvent(G){let Z=G&&G.target;if(!Z||typeof Z.closest!=="function")return;let U=Z.closest(`[${B}]`);if(!U)return;let H=U.getAttribute(W)||"click",X=G.type;if(X!==H)return;if(X!=="focusin"&&X!=="focusout")G.preventDefault();let V=U.getAttribute(B),_=U.getAttribute(L),Y=U.getAttribute(K),$=P(Y?Y:"{}"),F=U;if(_)try{F=document.querySelector(_)}catch(T){return}if(F)F.dispatchEvent(new CustomEvent(`${Q}:${V}`,{detail:{originalEvent:G,config:$,trigger:U},bubbles:!0}))}};for(let G=0,Z=D.length;G<Z;G++)document.addEventListener(D[G],O);let x=!0;return{disconnect(){if(!x)return;x=!1;for(let G=0,Z=D.length;G<Z;G++)document.removeEventListener(D[G],O)}}}var E=new WeakMap,A;if(typeof window<"u"&&"IntersectionObserver"in window)A=new IntersectionObserver((J,B)=>{let Q=J.filter((W)=>W.isIntersecting);for(let W=0,L=Q.length;W<L;W++){let K=Q[W].target;B.unobserve(K);let D=E.get(K);if(E.delete(K),D)D(K)}});function M(J,B){if(!A)return B(J),()=>{};return E.set(J,B),A.observe(J),()=>{return E.delete(J),A.unobserve(J)}}function k(J,B,Q=document){let W=new WeakMap;if(!J||!J.length)throw Error("observer requires an array of CSS selector queries.");let L=J.join(","),K=(G,Z)=>{if(!G.matches)return;let U=W.get(G);if(Z)for(let H=0,X=J.length;H<X;H++){let V=J[H];if(G.matches(V)){if(!U)U=new Set,W.set(G,U);if(!U.has(V))U.add(V),B(G,!0,V)}}else if(U)W.delete(G),U.forEach((H)=>{B(G,!1,H)})},D=(G,Z,U,H)=>{if(Z){if(!U.has(G))U.add(G),H.delete(G),K(G,!0)}else if(!H.has(G))H.add(G),U.delete(G),K(G,!1);let X=G.querySelectorAll(L);for(let V=0,_=X.length;V<_;V++){let Y=X[V];if(Z){if(!U.has(Y))U.add(Y),H.delete(Y),K(Y,!0)}else if(!H.has(Y))H.add(Y),U.delete(Y),K(Y,!1)}},O=new MutationObserver((G)=>{let Z=new Set,U=new Set;for(let H=0,X=G.length;H<X;H++){let{addedNodes:V,removedNodes:_}=G[H];for(let Y=0,$=_.length;Y<$;Y++){let F=_[Y];if(F.nodeType===1)D(F,!1,Z,U)}for(let Y=0,$=V.length;Y<$;Y++){let F=V[Y];if(F.nodeType===1)D(F,!0,Z,U)}}});O.observe(Q,{childList:!0,subtree:!0});let x=(G,Z=!0)=>{let U=G instanceof NodeList||Array.isArray(G)?G:[G];for(let H=0,X=U.length;H<X;H++)if(U[H].nodeType===1)K(U[H],Z)};return x(Q.querySelectorAll(L),!0),{evaluate:x,forget:(G)=>W.delete(G),disconnect:()=>O.disconnect()}}function q(J=null,B={}){let Q=B.attribute||"data-component",W=B.lazyAttribute||"data-lazy",L=B.signalKey||"signal",K=B.destroyMethod||"destroy",D=new WeakMap,O=async(H)=>{if(J&&J[H]){let X=await J[H]();return X.default||X}if(window[H])return window[H];return console.warn(`[liveinit] Module '${H}' not found in Registry or global scope.`),null},x=B.resolve||O,G=k([`[${Q}]`],(H,X)=>{if(X){let V=H.getAttribute(`${Q}-config`),_=P(V),Y=H.getAttribute(Q),$={abortController:new AbortController,cancelLazy:null,appModule:null,failed:!1};D.set(H,$),_[L]=$.abortController.signal;let F=async()=>{try{let T=await x(Y);if(T&&!$.abortController.signal.aborted)$.appModule=new T(H,_),$.failed=!1;else if(!T)$.failed=!0}catch(T){$.failed=!0,console.error(T)}};if(H.hasAttribute(W))$.cancelLazy=M(H,F);else F()}else{let V=D.get(H);if(!V)return;if(V.abortController)V.abortController.abort();if(V.cancelLazy)V.cancelLazy();if(V.appModule&&typeof V.appModule[K]==="function")V.appModule[K]();D.delete(H)}}),Z=(H)=>{let X=[];if(!H)return X;if(typeof H.querySelectorAll==="function"){let V=H.querySelectorAll(`[${Q}]`);for(let _=0,Y=V.length;_<Y;_++)X.push(V[_])}if(typeof H.matches==="function"&&H.matches(`[${Q}]`))X.push(H);return X},U=(H=document)=>{let X=Z(H),V=0;for(let _=0,Y=X.length;_<Y;_++){let $=X[_],F=D.get($);if(!F||!F.failed)continue;V++,G.forget($),G.evaluate($,!0)}return V};return{evaluate:G.evaluate,retryFailed:U,forget:G.forget,disconnect:G.disconnect}}var R=new Set,z=null;function j(J){if(typeof J!=="function")throw Error("observeOpenShadowRoots requires an observe callback.");if(typeof Element>"u"||!Element.prototype.attachShadow)return()=>{};if(!z)z=Element.prototype.attachShadow,Element.prototype.attachShadow=function(Q){let W=z.call(this,Q);if(Q&&Q.mode==="open")R.forEach((L)=>{L(W)});return W};R.add(J);let B=!0;return()=>{if(!B)return;if(B=!1,R.delete(J),!R.size&&z)Element.prototype.attachShadow=z,z=null}}I();var f=q();if(typeof document<"u")document.addEventListener("liveinit:refresh",(J)=>{let B=J&&J.detail?J.detail:null,Q=B&&B.root?B.root:document;if(!Q||typeof Q.querySelectorAll!=="function")return;if(f.evaluate(Q.querySelectorAll("[data-component]"),!0),Q.matches&&Q.matches("[data-component]"))f.evaluate(Q,!0);f.retryFailed(Q)});if(typeof window<"u")window.liveinit=w;var g=w;})();
package/docs/api.md ADDED
@@ -0,0 +1,72 @@
1
+ # API Contract
2
+
3
+ This document defines the public API exposed by `liveinit`.
4
+
5
+ ## Exports
6
+
7
+ ```javascript
8
+ import {
9
+ initCommands,
10
+ initComponents,
11
+ lazy,
12
+ observeOpenShadowRoots,
13
+ observer,
14
+ parseConfig
15
+ } from "liveinit";
16
+ ```
17
+
18
+ ## Functions
19
+
20
+ ### `initComponents(Registry?, options?)`
21
+
22
+ Initializes component lifecycle handling for elements matching `data-component` (configurable).
23
+
24
+ - Returns: `{ evaluate, retryFailed, forget, disconnect }` (observer engine handle)
25
+ - Notes:
26
+ - supports global resolver fallback (`window[ComponentName]`) when no registry entry exists
27
+ - injects an `AbortSignal` into component config (default key: `signal`)
28
+ - supports optional lazy init via `data-lazy` (configurable)
29
+ - `retryFailed(root?)` retries components that previously failed to resolve
30
+
31
+ ### `initCommands(options?)`
32
+
33
+ Initializes global command delegation for stateless event dispatching.
34
+
35
+ - Returns: `{ disconnect }`
36
+ - Notes:
37
+ - each call creates a new listener set
38
+ - call `disconnect()` before re-initializing in HMR/microfrontend contexts
39
+
40
+ ### `observer(queries, callback, root?)`
41
+
42
+ MutationObserver wrapper for fixed selector matching.
43
+
44
+ - Returns: `{ evaluate, forget, disconnect }`
45
+ - Notes:
46
+ - safe by default: does not patch `Element.prototype.attachShadow`
47
+
48
+ ### `observeOpenShadowRoots(observe)`
49
+
50
+ Opt-in bridge for future open shadow roots.
51
+
52
+ - Returns: `() => void` (cleanup function)
53
+ - Notes:
54
+ - subscribes to newly created open shadow roots only
55
+ - restores original `attachShadow` when last subscriber is removed
56
+
57
+ ### `lazy(el, cb)`
58
+
59
+ Executes `cb` when `el` intersects viewport.
60
+
61
+ - Returns: `() => void` (cleanup function)
62
+ - Notes:
63
+ - degrades gracefully when `IntersectionObserver` is unavailable (runs immediately)
64
+
65
+ ### `parseConfig(str)`
66
+
67
+ Parses relaxed HTML-friendly config strings into objects.
68
+
69
+ - Returns: `object`
70
+ - Notes:
71
+ - supports unquoted keys and single-quoted strings
72
+ - invalid input returns `{}` (does not throw)
@@ -0,0 +1,111 @@
1
+ # Init Commands (`initCommands.js`)
2
+
3
+ A global event delegation engine designed to handle stateless actions declaratively from HTML variables without writing repetitive event listeners.
4
+
5
+ ## Overview
6
+
7
+ Instead of tracking every button's insertion and removal to attach/detach event listeners, the command engine uses a single, global event listener on the `document` that catches bubbling events and parses their custom target and actions. It then emits a standardized `CustomEvent` that your Javascript modules can listen for.
8
+
9
+ ### Relation to the Open UI Invokers API
10
+
11
+ This engine takes heavy inspiration from the [Open UI Invokers API Explainer](https://open-ui.org/components/invokers.explainer/) (the native `command` and `commandfor` HTML attributes), but diverges to provide a generic, framework-like event bus.
12
+
13
+ **How it's similar:**
14
+
15
+ - It allows declarative, HTML-only event delegation without manual `addEventListener` scripts.
16
+ - It relies on a source `trigger` pointing to a destination `target` using an ID or CSS selector.
17
+
18
+ **How it's different & better for our needs:**
19
+
20
+ 1. **No Polyfill Overhead:** Native `command/commandfor` requires heavy polyfilling for general-purpose use, interfering with shadow DOMs and native prototypes. This engine is pure and lightweight.
21
+ 2. **Beyond Clicks:** The native spec is strictly tied to buttons and clicks. Our engine scales to `input`, `change`, `submit`, `focusin`, and `focusout` via the `data-command-on` attribute.
22
+ 3. **No `--custom` Syntax:** The native spec forces custom actions to use a CSS-variable-like syntax (e.g., `command="--my-action"`). We just use standard strings (`data-command="refresh"`).
23
+ 4. **Rich Configuration:** We built in native JSON configuration strings (`data-command-config`) via `parseConfig.js` to easily pass complex arguments to your controllers.
24
+
25
+ ## Usage
26
+
27
+ Just import the initialization function into your main entry file to start tracking events:
28
+
29
+ ```javascript
30
+ import { initCommands } from 'liveinit';
31
+
32
+ // The engine binds to the document. Custom data attributes and events can be passed here.
33
+ const commandEngine = initCommands({
34
+ attribute: "data-command", // Default
35
+ events: ["click", "change", "input", "submit", "focusin", "focusout"] // Default
36
+ });
37
+
38
+ // Later, cleanup listeners if needed (HMR, teardown, tests, microfrontends)
39
+ commandEngine.disconnect();
40
+ ```
41
+
42
+ `initCommands()` creates a new listener set on each call. In environments like HMR or microfrontends, keep the returned handle and call `disconnect()` before re-initializing to avoid duplicate command dispatches.
43
+
44
+ ### HTML Setup
45
+
46
+ The engine relies on `[data-command]` (configurable) attributes to dispatch standard events.
47
+
48
+ **Basic Click Example:**
49
+
50
+ ```html
51
+ <!-- Clicking this dispatches a `command:refresh` event targeting the `#table` -->
52
+ <button data-command="refresh" data-command-for="#table">Refresh</button>
53
+ ```
54
+
55
+ **Custom Events:**
56
+ By default, the engine listens for `click` events. To listen for other bubbling actions (`change`, `input`, `submit`, `focusin`, `focusout`), use the `data-command-on` attribute.
57
+
58
+ ```html
59
+ <!-- Fires whenever the user types -->
60
+ <input type="search" data-command="search" data-command-on="input" data-command-for="#table">
61
+
62
+ <!-- Intercepts form submissions -->
63
+ <form data-command="save" data-command-on="submit" data-command-for="#table">
64
+ ...
65
+ </form>
66
+ ```
67
+
68
+ **Custom Configurations:**
69
+ To pass extra parameters to the command, use `data-command-config`.
70
+
71
+ ```html
72
+ <button data-command="refresh" data-command-for="#table" data-command-config="silent: true">
73
+ Silent Refresh
74
+ </button>
75
+ ```
76
+
77
+ ### Listening for Commands
78
+
79
+ Inside your components or Javascript, you listen to the emitted `{prefix}:{action}` event on the target element.
80
+ If your configured attribute is `data-command`, the prefix is `command`. If you configure it to `data-action`, the prefix becomes `action`.
81
+
82
+ ```javascript
83
+ const table = document.querySelector("#table");
84
+
85
+ // Assuming the default `data-command` attribute:
86
+ table.addEventListener("command:refresh", (event) => {
87
+ // Extract custom configuration
88
+ const isSilent = event.detail.config.silent;
89
+
90
+ // See which element actually triggered the command
91
+ const buttonEl = event.detail.trigger;
92
+
93
+ // Access the original bubbling event (e.g. MouseEvent or SubmitEvent)
94
+ const originalEvent = event.detail.originalEvent;
95
+
96
+ console.log("Refreshing table...", isSilent);
97
+ });
98
+ ```
99
+
100
+ ## Supported Events
101
+
102
+ The global delegator currently listens for and supports intercepting:
103
+
104
+ - `click` (default)
105
+ - `change`
106
+ - `input`
107
+ - `submit`
108
+ - `focusin`
109
+ - `focusout`
110
+
111
+ *(Note: The engine natively suppresses default browser behaviors like form submissions or hash link jumping automatically when catching these commands, except for focus events).*
@@ -0,0 +1,114 @@
1
+ # Init Components Engine (`initComponents.js`)
2
+
3
+ The high-level component initializer that wires together the `observer`, `lazy` intersection observations, and parsed `parseConfig` configuration.
4
+
5
+ ## Overview
6
+
7
+ `initComponents` is designed to be the main entry point for HTML-first applications. It watches the DOM for specific attributes, parses configuration strings safely, and dynamically lazy-loads and instantiates classes when they enter the DOM.
8
+
9
+ ## Usage
10
+
11
+ ```javascript
12
+ import { initComponents } from 'liveinit';
13
+
14
+ // ---------- Method 1: Zero Setup (Global Scope) ----------
15
+ // If a component is attached to the window (e.g., `window.Dropdown`)
16
+ // this will automatically instantiate `<div data-component="Dropdown">`
17
+ initComponents();
18
+
19
+ // ---------- Method 2: Standard Registry (Code-Splitting) ----------
20
+ // Dictionary mapping the module names (HTML attribute values)
21
+ // to dynamic imports for lazy loading.
22
+ const Registry = {
23
+ 'datatable': () => import('./components/datatable.js'),
24
+ 'heavy-map': () => import('./components/map.js')
25
+ };
26
+ initComponents(Registry);
27
+
28
+ // ---------- Method 3: Advanced Options & Custom Resolvers ----------
29
+ initComponents(null, {
30
+ attribute: 'data-widget', // Default: data-component
31
+ lazyAttribute: 'data-defer', // Default: data-lazy
32
+ signalKey: 'abortSignal', // Default: signal
33
+ destroyMethod: 'dispose', // Default: destroy
34
+ resolve: async (name) => {
35
+ // Write your own logic (e.g. Vite glob imports)
36
+ const modules = import.meta.glob('./widgets/*.js');
37
+ const imported = await modules[`./widgets/${name}.js`]();
38
+ return imported.default;
39
+ }
40
+ });
41
+ ```
42
+
43
+ ### HTML Setup
44
+
45
+ **Immediate Initialization:**
46
+ When the HTML node enters the DOM, it immediately imports the datatable component and initializes it.
47
+
48
+ ```html
49
+ <div data-component="datatable" data-component-config="pageLength: 25"></div>
50
+ ```
51
+
52
+ **Lazy Initialization:**
53
+ By adding the `data-lazy` attribute, the loader waits until the element is scrolled into view (using `IntersectionObserver`) before it downloads and executes the component code.
54
+
55
+ ```html
56
+ <div data-component="heavy-map" data-component-config="lat: 50.85, lng: 4.35" data-lazy></div>
57
+ ```
58
+
59
+ ### Component Structure
60
+
61
+ Your JavaScript component classes will receive two arguments upon instantiation: the DOM `Element`, and a `config` object containing the parsed dataset configurations.
62
+
63
+ By default, an `AbortSignal` is automatically injected into the config as `config.signal` (configurable via `options.signalKey`) to elegantly handle event cleanup when DOM nodes are removed.
64
+
65
+ ```javascript
66
+ export default class DataTable {
67
+ constructor(element, config) {
68
+ this.element = element;
69
+
70
+ // Config properties parsed from `data-component-config`
71
+ this.pageLength = config.pageLength || 10;
72
+
73
+ // Automatically cleaned up when the element leaves the DOM!
74
+ window.addEventListener("resize", this.handleResize, { signal: config.signal });
75
+ }
76
+
77
+ // Optional teardown triggered automatically if the node is removed from the DOM
78
+ // (Configurable via options.destroyMethod, defaults to "destroy")
79
+ destroy() {
80
+ console.log("Datatable unmounted safely.");
81
+ }
82
+ }
83
+ ```
84
+
85
+ ## How It Works
86
+
87
+ 1. Uses `observer.js` to strictly track DOM insertion and removal.
88
+ 2. Uses `lazy.js` to optionally defer the initialization until the element is visible in the viewport.
89
+ 3. Automatically creates an `AbortController` and passes the `.signal` to the component to guarantee no orphaned event listeners cause memory leaks.
90
+ 4. Safely calls `.destroy()` on the class instance when the HTML node is removed (e.g., replaced during an AJAX/HTMX request).
91
+
92
+ ## AJAX / Load Order Notes
93
+
94
+ `initComponents()` evaluates elements as soon as they enter the DOM.
95
+ If an AJAX fragment is injected before its component class is registered on `window`, global fallback resolution can fail.
96
+
97
+ Recommended for async/fragment-heavy apps:
98
+
99
+ 1. Prefer `options.resolve` with async imports so component loading is deterministic.
100
+ 2. Treat `window.ComponentName` fallback as a convenience path (best for simple pages).
101
+ 3. If your scripts register components later, retry previously failed nodes after registration (`engine.retryFailed(fragment)`), or inject scripts before inserting the fragment.
102
+
103
+ If you use the CDN auto bundle (`dist/liveinit.min.js`), a convenience event is available:
104
+
105
+ ```javascript
106
+ document.dispatchEvent(
107
+ new CustomEvent("liveinit:refresh", {
108
+ detail: { root: fragmentElement } // optional, defaults to document
109
+ })
110
+ );
111
+ ```
112
+
113
+ This re-scans `[data-component]` elements in the provided root and initializes any newly available components.
114
+ It also retries nodes that previously failed to resolve.
package/docs/lazy.md ADDED
@@ -0,0 +1,36 @@
1
+ # Lazy Intersection Observer (`lazy.js`)
2
+
3
+ A simple utility that defers the initialization of DOM elements until they enter the user's viewport.
4
+
5
+ ## Overview
6
+
7
+ By leveraging the natively performant `IntersectionObserver`, `lazy.js` prevents heavy external scripts—like maps, charts, or datatables—from unnecessarily slowing down Initial Page Load times.
8
+
9
+ ## Usage
10
+
11
+ ```javascript
12
+ import { lazy } from 'liveinit';
13
+
14
+ // Setup your heavy DOM initialization
15
+ const initMyComponent = (element) => {
16
+ element.innerText = "Wow! I am finally loaded.";
17
+ };
18
+
19
+ // Start watching the element
20
+ const cancelLazyObserver = lazy(myHeavyDiv, initMyComponent);
21
+ ```
22
+
23
+ ### Automatic Cleanup Strategy
24
+
25
+ `lazy()` returns a cleanup function that cancels the observer entirely. This makes it perfect to cleanly tie to the teardown phase of a component lifecycle in case it drops from the page before it's ever scrolled to:
26
+
27
+ ```javascript
28
+ // Disconnects element from IntersectionObserver
29
+ cancelLazyObserver();
30
+ ```
31
+
32
+ *(Note: Once the element actually enters the screen and executes `initMyComponent`, the internal observer automatically calls `.unobserve(el)` and destroys the internal WeakMap entry. The `cancelLazyObserver` function then becomes a harmless no-op string).*
33
+
34
+ ### Fallbacks
35
+
36
+ If `IntersectionObserver` is completely unsupported by the browser, `lazy.js` will instantly fall back to executing the callback immediately to ensure core capabilities function without throwing an error natively.
@@ -0,0 +1,65 @@
1
+ # Selector Observer (`observer.js`)
2
+
3
+ The ultra-fast, pure JS core DOM lifecycle tracker for known, fixed CSS selectors. This module wraps `MutationObserver` directly.
4
+
5
+ ## Overview
6
+
7
+ Unlike many mutation tracking libraries that suffer severe performance penalties by walking the entire DOM on every change, `observer.js` specifically looks for targeted, pre-computed CSS selector matches when nodes drop in and out of the DOM.
8
+
9
+ ## Features
10
+
11
+ - Blazing fast CSS string compilation upfront.
12
+ - Native evaluation for child element searches (`querySelectorAll`).
13
+ - Auto-initialization for matched elements immediately on load.
14
+ - Safe-by-default behavior with no global prototype patching.
15
+
16
+ ## Usage
17
+
18
+ ```javascript
19
+ import { observer } from 'liveinit';
20
+
21
+ // The engine takes an Array of specific CSS selectors it should look for
22
+ const OBSERVED = ['[data-component]', '[data-custom-component]'];
23
+
24
+ // Start observing mutations and immediately evaluate all current DOM
25
+ const engine = observer(OBSERVED, (element, isConnected, selector) => {
26
+ if (isConnected) {
27
+ console.log(`Node ${element.tagName} attached! matched: ${selector}`);
28
+ } else {
29
+ console.log(`Node ${element.tagName} detached! matched: ${selector}`);
30
+ }
31
+ });
32
+ ```
33
+
34
+ ### The Returned Instance
35
+
36
+ The observer call returns the following API:
37
+
38
+ ```javascript
39
+ const { evaluate, forget, disconnect } = engine;
40
+ ```
41
+
42
+ - `evaluate(elements, isConnected = true)`: Manually trigger the engine on dynamically altered nodes. *Because `observer.js` intentionally ignores attribute changes (for performance), if you dynamically add a class or attribute later without re-appending the DOM node, call `evaluate(yourDiv)` to manually trigger the connection lifecycle.*
43
+ - `forget(element)`: Strips an element entirely from the WeakMap memory tracker.
44
+ - `disconnect()`: Unplugs the internal `MutationObserver`.
45
+
46
+ ## Shadow DOM (Opt-In)
47
+
48
+ `observer.js` does not patch `Element.prototype` automatically. If you want to observe future **open** shadow roots, opt in explicitly:
49
+
50
+ ```javascript
51
+ import { observer, observeOpenShadowRoots } from 'liveinit';
52
+
53
+ const engine = observer(['[data-component]'], (el, isConnected) => {
54
+ // ...
55
+ });
56
+
57
+ // Observe all newly created open shadow roots.
58
+ const stopShadowRootBridge = observeOpenShadowRoots((shadowRoot) => {
59
+ engine.evaluate(shadowRoot.querySelectorAll('[data-component]'), true);
60
+ });
61
+
62
+ // Later:
63
+ stopShadowRootBridge();
64
+ engine.disconnect();
65
+ ```
@@ -0,0 +1,39 @@
1
+ # Config Parser (`parseConfig.js`)
2
+
3
+ A utility for parsing flexible HTML configuration strings securely into functional JavaScript Objects.
4
+
5
+ ## Overview
6
+
7
+ Writing Strict JSON inside HTML properties is incredibly painful and prone to error:
8
+
9
+ ```html
10
+ <div data-component-config='{"page": 2, "theme": "dark"}'> <!-- Annoying -->
11
+ ```
12
+
13
+ `parseConfig` transforms the developer experience by explicitly supporting unquoted keys and single quotes, allowing for cleaner HTML:
14
+
15
+ ```html
16
+ <div data-component-config="page: 2, theme: 'dark'"> <!-- Beautiful -->
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```javascript
22
+ import { parseConfig } from 'liveinit';
23
+
24
+ // Pass relaxed strings:
25
+ const parsed = parseConfig("delay: 50, silent: true");
26
+
27
+ /* Result:
28
+ {
29
+ "delay": 50,
30
+ "silent": true
31
+ }
32
+ */
33
+ ```
34
+
35
+ ## Parsing Rules
36
+
37
+ 1. Single Quotes (`'value'`) are explicitly accepted as valid strings.
38
+ 2. Missing Key Quotes (`name: ...`) are wrapped with Double Quotes under-the-hood.
39
+ 3. If structural Regex rules completely fail, the function natively delegates down to `JSON.parse` or falls back to an empty object `{}` rather than throwing breaking errors up the chain.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "liveinit",
3
+ "version": "0.1.0",
4
+ "description": "Lean HTML-first component initializer and command delegator",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "dom",
8
+ "components",
9
+ "observer",
10
+ "events",
11
+ "html-first"
12
+ ],
13
+ "homepage": "https://github.com/lekoala/liveinit#readme",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/lekoala/liveinit.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/lekoala/liveinit/issues"
20
+ },
21
+ "type": "module",
22
+ "main": "./src/index.js",
23
+ "module": "./src/index.js",
24
+ "exports": {
25
+ ".": "./src/index.js"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "dist",
30
+ "README.md",
31
+ "docs"
32
+ ],
33
+ "scripts": {
34
+ "test": "bun test",
35
+ "build": "bun build ./src/auto.js --outfile ./dist/liveinit.min.js --format=iife --minify --banner \"/*! liveinit | https://github.com/lekoala/liveinit | @license MIT */\"",
36
+ "demo": "bun ./demo.html"
37
+ },
38
+ "sideEffects": false,
39
+ "private": false,
40
+ "devDependencies": {
41
+ "@biomejs/biome": "^2.4.4",
42
+ "@happy-dom/global-registrator": "^20.7.0",
43
+ "@types/bun": "latest",
44
+ "happy-dom": "^20.7.0"
45
+ }
46
+ }
package/src/auto.js ADDED
@@ -0,0 +1,33 @@
1
+ import * as liveinit from "./index.js";
2
+
3
+ // Auto-initialize with default settings for immediate use via CDN script tag
4
+ liveinit.initCommands();
5
+ const componentEngine = liveinit.initComponents();
6
+
7
+ // Convenience hook for AJAX/fragment flows in CDN mode.
8
+ // Dispatch `liveinit:refresh` with an optional `detail.root` Element/Document.
9
+ if (typeof document !== "undefined") {
10
+ document.addEventListener("liveinit:refresh", (event) => {
11
+ const detail = event && event.detail ? event.detail : null;
12
+ const root = detail && detail.root ? detail.root : document;
13
+ if (!root || typeof root.querySelectorAll !== "function") return;
14
+
15
+ // Evaluate children
16
+ componentEngine.evaluate(root.querySelectorAll("[data-component]"), true);
17
+
18
+ // Evaluate root itself if it is a component
19
+ if (root.matches && root.matches("[data-component]")) {
20
+ componentEngine.evaluate(root, true);
21
+ }
22
+
23
+ // Retry components that previously failed to resolve.
24
+ componentEngine.retryFailed(root);
25
+ });
26
+ }
27
+
28
+ // Expose the library to the global window object for programmatic access
29
+ if (typeof window !== "undefined") {
30
+ window.liveinit = liveinit;
31
+ }
32
+
33
+ export default liveinit;
package/src/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import initCommands from "./initCommands.js";
2
+ import initComponents from "./initComponents.js";
3
+ import lazy from "./lazy.js";
4
+ import observeOpenShadowRoots from "./observeOpenShadowRoots.js";
5
+ import observer from "./observer.js";
6
+ import parseConfig from "./parseConfig.js";
7
+
8
+ export {
9
+ initCommands,
10
+ initComponents,
11
+ lazy,
12
+ observeOpenShadowRoots,
13
+ observer,
14
+ parseConfig,
15
+ };
@@ -0,0 +1,86 @@
1
+ import parseConfig from "./parseConfig.js";
2
+
3
+ /**
4
+ * Initializes the global command event delegator.
5
+ * @param {Object} [options={}]
6
+ * @param {string} [options.attribute="data-command"] - The HTML attribute used for the command action.
7
+ * @param {string[]} [options.events=["click", "change", "input", "submit", "focusin", "focusout"]] - The array of bubbling events to listen for.
8
+ * @returns {{ disconnect: () => void }} Disconnects all event listeners registered by this init call.
9
+ */
10
+ export default function initCommands(options = {}) {
11
+ const attribute = options.attribute || "data-command";
12
+ const eventPrefix = attribute.replace(/^data-/, "");
13
+ const commandOnAttr = `${attribute}-on`;
14
+ const commandForAttr = `${attribute}-for`;
15
+ const commandConfigAttr = `${attribute}-config`;
16
+
17
+ const events = options.events || [
18
+ "click",
19
+ "change",
20
+ "input",
21
+ "submit",
22
+ "focusin",
23
+ "focusout",
24
+ ];
25
+
26
+ const listener = {
27
+ // One stable listener object for all events:
28
+ // `handleEvent` keeps add/remove symmetric and avoids per-event bound closures.
29
+ handleEvent(event) {
30
+ const eventTarget = event && event.target;
31
+ if (!eventTarget || typeof eventTarget.closest !== "function") return;
32
+
33
+ // Find if the event originated from inside a [data-command] element
34
+ const trigger = eventTarget.closest(`[${attribute}]`);
35
+ if (!trigger) return;
36
+
37
+ // If the event type doesn't match the requested trigger, ignore it
38
+ const expectedEvent = trigger.getAttribute(commandOnAttr) || "click";
39
+ const type = event.type;
40
+ if (type !== expectedEvent) return;
41
+
42
+ if (type !== "focusin" && type !== "focusout") {
43
+ // Prevent Default handles submit, links, and changes correctly usually
44
+ event.preventDefault();
45
+ }
46
+
47
+ const action = trigger.getAttribute(attribute); // e.g., "refresh"
48
+ const targetSelector = trigger.getAttribute(commandForAttr);
49
+ const configString = trigger.getAttribute(commandConfigAttr);
50
+ const config = parseConfig(configString ? configString : "{}");
51
+
52
+ let target = trigger;
53
+ if (targetSelector) {
54
+ try {
55
+ target = document.querySelector(targetSelector);
56
+ } catch (_e) {
57
+ return;
58
+ }
59
+ }
60
+
61
+ if (target) {
62
+ target.dispatchEvent(
63
+ new CustomEvent(`${eventPrefix}:${action}`, {
64
+ detail: { originalEvent: event, config, trigger },
65
+ bubbles: true,
66
+ }),
67
+ );
68
+ }
69
+ },
70
+ };
71
+
72
+ for (let i = 0, len = events.length; i < len; i++) {
73
+ document.addEventListener(events[i], listener);
74
+ }
75
+
76
+ let connected = true;
77
+ return {
78
+ disconnect() {
79
+ if (!connected) return;
80
+ connected = false;
81
+ for (let i = 0, len = events.length; i < len; i++) {
82
+ document.removeEventListener(events[i], listener);
83
+ }
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,156 @@
1
+ import lazy from "./lazy.js";
2
+ import observer from "./observer.js";
3
+ import parseConfig from "./parseConfig.js";
4
+
5
+ /**
6
+ * Initializes components dynamically based on HTML attributes.
7
+ *
8
+ * @param {Record<string, () => Promise<{ default: any }>>} [Registry] - Optional dictionary mapping module names to dynamic import functions.
9
+ * @param {Object} [options={}]
10
+ * @param {string} [options.attribute="data-component"] - The HTML attribute used for binding components.
11
+ * @param {string} [options.lazyAttribute="data-lazy"] - The HTML attribute indicating deferred loading.
12
+ * @param {string} [options.signalKey="signal"] - The key used to inject the AbortSignal into the component's config.
13
+ * @param {string} [options.destroyMethod="destroy"] - The method name called on the component instance during teardown.
14
+ * @param {Function} [options.resolve] - A custom async function `(moduleName) => ModuleClass` to override default resolution.
15
+ * @returns {Object} The observer instance { evaluate, retryFailed, forget, disconnect }.
16
+ */
17
+ export default function initComponents(Registry = null, options = {}) {
18
+ const attribute = options.attribute || "data-component";
19
+ const lazyAttribute = options.lazyAttribute || "data-lazy";
20
+ const signalKey = options.signalKey || "signal";
21
+ const destroyMethod = options.destroyMethod || "destroy";
22
+ const componentState = new WeakMap();
23
+
24
+ // Default resolver: check explicit registry, fallback to global window object
25
+ const defaultResolver = async (moduleName) => {
26
+ if (Registry && Registry[moduleName]) {
27
+ const imported = await Registry[moduleName]();
28
+ // Handle both ES module default exports and CommonJS-style exports
29
+ return imported.default || imported;
30
+ }
31
+
32
+ if (window[moduleName]) {
33
+ return window[moduleName];
34
+ }
35
+
36
+ // Instead of throwing strongly, warn so that async component definitions
37
+ // (late init) do not throw unhandled runtime errors on the first pass.
38
+ // A warning is useful enough for developers to realize a typo or missing class.
39
+ console.warn(`[liveinit] Module '${moduleName}' not found in Registry or global scope.`);
40
+ return null;
41
+ };
42
+
43
+ const resolver = options.resolve || defaultResolver;
44
+
45
+ const engine = observer([`[${attribute}]`], (el, isConnected) => {
46
+ if (isConnected) {
47
+ // 1. Parse relaxed config string
48
+ const configString = el.getAttribute(`${attribute}-config`);
49
+ const config = parseConfig(configString);
50
+ const moduleName = el.getAttribute(attribute);
51
+
52
+ const state = {
53
+ abortController: new AbortController(),
54
+ cancelLazy: null,
55
+ appModule: null,
56
+ failed: false,
57
+ };
58
+ componentState.set(el, state);
59
+ config[signalKey] = state.abortController.signal;
60
+
61
+ // 2. Define the actual initialization logic
62
+ const initModule = async () => {
63
+ try {
64
+ const ModuleClass = await resolver(moduleName);
65
+ // Ensure element wasn't disconnected while we were resolving the module
66
+ // and ensure ModuleClass was actually resolved
67
+ if (ModuleClass && !state.abortController.signal.aborted) {
68
+ state.appModule = new ModuleClass(el, config);
69
+ state.failed = false;
70
+ } else if (!ModuleClass) {
71
+ // Keep state and mark as failed so retries can be targeted later.
72
+ state.failed = true;
73
+ }
74
+ } catch (err) {
75
+ state.failed = true;
76
+ console.error(err);
77
+ }
78
+ };
79
+
80
+ // 3. Check for Lazy Loading
81
+ if (el.hasAttribute(lazyAttribute)) {
82
+ state.cancelLazy = lazy(el, initModule);
83
+ } else {
84
+ initModule();
85
+ }
86
+ } else {
87
+ // --- TEARDOWN ---
88
+ const state = componentState.get(el);
89
+ if (!state) return;
90
+
91
+ // 1. Auto-cleanup all events bound with this signal
92
+ if (state.abortController) {
93
+ state.abortController.abort();
94
+ }
95
+
96
+ // 2. If it was removed before it ever scrolled into view, cancel the observer!
97
+ if (state.cancelLazy) {
98
+ state.cancelLazy();
99
+ }
100
+
101
+ // 3. If the module was actually initialized, destroy it safely.
102
+ if (
103
+ state.appModule &&
104
+ typeof state.appModule[destroyMethod] === "function"
105
+ ) {
106
+ state.appModule[destroyMethod]();
107
+ }
108
+
109
+ componentState.delete(el);
110
+ }
111
+ });
112
+
113
+ const collectCandidates = (root) => {
114
+ const nodes = [];
115
+ if (!root) return nodes;
116
+
117
+ if (typeof root.querySelectorAll === "function") {
118
+ const descendants = root.querySelectorAll(`[${attribute}]`);
119
+ for (let i = 0, len = descendants.length; i < len; i++) {
120
+ nodes.push(descendants[i]);
121
+ }
122
+ }
123
+
124
+ if (
125
+ typeof root.matches === "function" &&
126
+ root.matches(`[${attribute}]`)
127
+ ) {
128
+ nodes.push(root);
129
+ }
130
+
131
+ return nodes;
132
+ };
133
+
134
+ const retryFailed = (root = document) => {
135
+ const nodes = collectCandidates(root);
136
+ let retried = 0;
137
+
138
+ for (let i = 0, len = nodes.length; i < len; i++) {
139
+ const el = nodes[i];
140
+ const state = componentState.get(el);
141
+ if (!state || !state.failed) continue;
142
+ retried++;
143
+ engine.forget(el);
144
+ engine.evaluate(el, true);
145
+ }
146
+
147
+ return retried;
148
+ };
149
+
150
+ return {
151
+ evaluate: engine.evaluate,
152
+ retryFailed,
153
+ forget: engine.forget,
154
+ disconnect: engine.disconnect,
155
+ };
156
+ }
package/src/lazy.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @type {WeakMap<Element,Function>}
3
+ */
4
+ const map = new WeakMap();
5
+ let observer;
6
+
7
+ // Initialize IntersectionObserver for browsers that support it
8
+ if (typeof window !== "undefined" && "IntersectionObserver" in window) {
9
+ observer = new IntersectionObserver((entries, obs) => {
10
+ const activeEntries = entries.filter((entry) => entry.isIntersecting);
11
+ for (let i = 0, len = activeEntries.length; i < len; i++) {
12
+ const t = activeEntries[i].target;
13
+ obs.unobserve(t);
14
+
15
+ // Run
16
+ const fn = map.get(t);
17
+ map.delete(t);
18
+ if (fn) {
19
+ fn(t);
20
+ }
21
+ }
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Element will trigger callback when visible in intersection observer
27
+ * @param {Element} el
28
+ * @param {Function} cb An init callback that gets the element as the first argument
29
+ * @returns {Function} a callback to remove the observer
30
+ */
31
+ export default function lazy(el, cb) {
32
+ // If IntersectionObserver is not supported, initialize immediately
33
+ if (!observer) {
34
+ cb(el);
35
+ // Dummy cleanup
36
+ return () => {};
37
+ }
38
+ map.set(el, cb);
39
+ observer.observe(el);
40
+ return () => {
41
+ map.delete(el);
42
+ return observer.unobserve(el);
43
+ };
44
+ }
@@ -0,0 +1,47 @@
1
+ const subscribers = new Set();
2
+ let originalAttachShadow = null;
3
+
4
+ /**
5
+ * Opt-in helper to observe mutations inside future open shadow roots.
6
+ * Returns a cleanup callback that removes the subscription and restores
7
+ * `Element.prototype.attachShadow` when no subscribers remain.
8
+ *
9
+ * @param {(shadowRoot: ShadowRoot) => void} observe
10
+ * @returns {() => void}
11
+ */
12
+ export default function observeOpenShadowRoots(observe) {
13
+ if (typeof observe !== "function") {
14
+ throw new Error("observeOpenShadowRoots requires an observe callback.");
15
+ }
16
+
17
+ if (typeof Element === "undefined" || !Element.prototype.attachShadow) {
18
+ return () => {};
19
+ }
20
+
21
+ if (!originalAttachShadow) {
22
+ originalAttachShadow = Element.prototype.attachShadow;
23
+ Element.prototype.attachShadow = function (init) {
24
+ const shadowRoot = originalAttachShadow.call(this, init);
25
+ if (init && init.mode === "open") {
26
+ subscribers.forEach((subscriber) => {
27
+ subscriber(shadowRoot);
28
+ });
29
+ }
30
+ return shadowRoot;
31
+ };
32
+ }
33
+
34
+ subscribers.add(observe);
35
+ let active = true;
36
+
37
+ return () => {
38
+ if (!active) return;
39
+ active = false;
40
+ subscribers.delete(observe);
41
+
42
+ if (!subscribers.size && originalAttachShadow) {
43
+ Element.prototype.attachShadow = originalAttachShadow;
44
+ originalAttachShadow = null;
45
+ }
46
+ };
47
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Ultra-fast DOM lifecycle tracker for known, fixed CSS selectors.
3
+ *
4
+ * @param {string[]} queries Array of CSS selectors to observe
5
+ * @param {Function} callback Callback for matches: (element, connected, selector)
6
+ * @param {Document|Element} [root=document] The root element to observe
7
+ * @returns {object} The observer instance { evaluate, forget, disconnect }
8
+ */
9
+ export default function observer(queries, callback, root = document) {
10
+ const liveElements = new WeakMap();
11
+
12
+ if (!queries || !queries.length) {
13
+ throw new Error("observer requires an array of CSS selector queries.");
14
+ }
15
+
16
+ // Compute the master selector string exactly once.
17
+ const selectorString = queries.join(",");
18
+
19
+ const notifyNode = (element, isConnected) => {
20
+ if (!element.matches) return;
21
+
22
+ let activeSelectors = liveElements.get(element);
23
+
24
+ if (isConnected) {
25
+ // Classic, blazing-fast 'for' loop instead of Set iteration
26
+ for (let i = 0, len = queries.length; i < len; i++) {
27
+ const selector = queries[i];
28
+ if (element.matches(selector)) {
29
+ if (!activeSelectors) {
30
+ activeSelectors = new Set();
31
+ liveElements.set(element, activeSelectors);
32
+ }
33
+ if (!activeSelectors.has(selector)) {
34
+ activeSelectors.add(selector);
35
+ callback(element, true, selector);
36
+ }
37
+ }
38
+ }
39
+ } else if (activeSelectors) {
40
+ liveElements.delete(element);
41
+ activeSelectors.forEach((selector) => {
42
+ callback(element, false, selector);
43
+ });
44
+ }
45
+ };
46
+
47
+ const processNode = (node, isConnected, added, removed) => {
48
+ if (isConnected) {
49
+ if (!added.has(node)) {
50
+ added.add(node);
51
+ removed.delete(node);
52
+ notifyNode(node, true);
53
+ }
54
+ } else {
55
+ if (!removed.has(node)) {
56
+ removed.add(node);
57
+ added.delete(node);
58
+ notifyNode(node, false);
59
+ }
60
+ }
61
+
62
+ // Uses the pre-computed string for immediate native C++ evaluation
63
+ const descendants = node.querySelectorAll(selectorString);
64
+ for (let i = 0, len = descendants.length; i < len; i++) {
65
+ const desc = descendants[i];
66
+ if (isConnected) {
67
+ if (!added.has(desc)) {
68
+ added.add(desc);
69
+ removed.delete(desc);
70
+ notifyNode(desc, true);
71
+ }
72
+ } else {
73
+ if (!removed.has(desc)) {
74
+ removed.add(desc);
75
+ added.delete(desc);
76
+ notifyNode(desc, false);
77
+ }
78
+ }
79
+ }
80
+ };
81
+
82
+ const observer = new MutationObserver((records) => {
83
+ const added = new Set();
84
+ const removed = new Set();
85
+
86
+ for (let i = 0, len = records.length; i < len; i++) {
87
+ const { addedNodes, removedNodes } = records[i];
88
+
89
+ for (let j = 0, rLen = removedNodes.length; j < rLen; j++) {
90
+ const node = removedNodes[j];
91
+ if (node.nodeType === 1) processNode(node, false, added, removed);
92
+ }
93
+ for (let j = 0, aLen = addedNodes.length; j < aLen; j++) {
94
+ const node = addedNodes[j];
95
+ if (node.nodeType === 1) processNode(node, true, added, removed);
96
+ }
97
+ }
98
+ });
99
+
100
+ observer.observe(root, { childList: true, subtree: true });
101
+
102
+ const evaluate = (elements, isConnected = true) => {
103
+ const nodes =
104
+ elements instanceof NodeList || Array.isArray(elements)
105
+ ? elements
106
+ : [elements];
107
+ for (let i = 0, len = nodes.length; i < len; i++) {
108
+ if (nodes[i].nodeType === 1) notifyNode(nodes[i], isConnected);
109
+ }
110
+ };
111
+
112
+ // Automatically initialize everything currently in the DOM
113
+ evaluate(root.querySelectorAll(selectorString), true);
114
+
115
+ // Return the stripped-down public API
116
+ return {
117
+ evaluate,
118
+ forget: (el) => liveElements.delete(el),
119
+ disconnect: () => observer.disconnect(),
120
+ };
121
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Parses a simplified, non-strict object string into a JavaScript object.
3
+ * Allows unquoted keys.
4
+ * Allows single-quoted strings.
5
+ * Assumes the root is an object.
6
+ * @param {string} str The configuration string.
7
+ * @returns {object} The parsed JavaScript object.
8
+ */
9
+ export default function parseConfig(str) {
10
+ if (typeof str !== "string") return {};
11
+ let jsonString = str.trim();
12
+ // Empty returns an empty object
13
+ if (jsonString === "") return {};
14
+ // 1. ([a-zA-Z_$][\w$-]+)\s: matches an unquoted key followed by a colon.
15
+ // 2. '((?:\'|[^']))' matches a single-quoted string and its content.
16
+ jsonString = jsonString.replace(
17
+ /([a-zA-Z_$][\w$-]*)\s*:|'((?:\\'|[^'])*)'/g,
18
+ (_match, key, stringContent) => {
19
+ // Case 1: An unquoted key was matched (e.g., "key:").
20
+ if (key) {
21
+ // Return the key, now double-quoted, with the colon.
22
+ return `"${key}":`;
23
+ }
24
+ // Case 2: A single-quoted string was matched (e.g., "'value'").
25
+ // Note: The 'else' is implicit. `key` will be undefined if the second part of the regex matched.
26
+
27
+ // Un-escape any escaped single quotes within the content.
28
+ const unescaped = stringContent.replace(/\\'/g, "'");
29
+ // Escape any double quotes to create a valid JSON string.
30
+ const escaped = unescaped.replace(/"/g, '\\"');
31
+ // Return the content, now wrapped in double quotes.
32
+ return `"${escaped}"`;
33
+ },
34
+ );
35
+ // Ensure the string is wrapped in braces to be a valid JSON object.
36
+ if (!jsonString.startsWith("{")) {
37
+ jsonString = `{${jsonString}}`;
38
+ }
39
+ // Use the built-in JSON.parse.
40
+ try {
41
+ return JSON.parse(jsonString);
42
+ } catch (_e) {
43
+ console.error(`Failed to parse: ${str}`);
44
+ return {}; // Return an empty object on failure.
45
+ }
46
+ }