@daz4126/helium 0.31.0 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/helium-lite.js ADDED
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Helium Lite - Lightweight reactive library
3
+ * Core features only: @data, @text, @html, @bind, :attr, @ref, @calculate, @effect, @init, events
4
+ * No AJAX, no @import, no SSE, no Turbo
5
+ */
6
+
7
+ const parseEx = v => { try { return Function(`return(${v})`)() } catch { return v } };
8
+ const tryNum = v => { const t = String(v).trim(), n = +t; return t && !isNaN(n) ? n : v };
9
+ const isValidIdentifier = v => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(v);
10
+ const RESERVED = new Set(['undefined', 'null', 'true', 'false', 'NaN', 'Infinity', 'this', 'arguments']);
11
+ const KEY_MODS = { shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey" };
12
+ const debounce = (f, d) => { let t; return (...a) => (clearTimeout(t), t = setTimeout(f, d, ...a)) };
13
+
14
+ const ATTR_REGEX = /^(@|:|data-he)/;
15
+
16
+ const match = (name, ...attrs) => {
17
+ const p = name.split(/[.:]/)[0];
18
+ if (p === ":" || p === "") return false;
19
+ return attrs.some(a => p === `@${a}` || p === `data-he-${a}`);
20
+ };
21
+
22
+ let HELIUM = null;
23
+
24
+ async function helium(initialState = {}) {
25
+ const inits = [];
26
+ const ALL = Symbol("all");
27
+ const root = document.querySelector('[\\@helium]') || document.querySelector('[data-helium]') || document.body;
28
+ const storageKey = root.getAttribute('@local-storage') || root.getAttribute('data-he-local-storage');
29
+
30
+ if (!HELIUM) {
31
+ HELIUM = {
32
+ observer: null,
33
+ bindings: new Map(),
34
+ refs: new Map(),
35
+ listeners: new WeakMap(),
36
+ processed: new WeakSet(),
37
+ parentKeys: new WeakMap(),
38
+ fnCache: new Map(),
39
+ proxyCache: new WeakMap()
40
+ };
41
+ }
42
+
43
+ const $ = s => document.querySelector(s);
44
+ const html = s => Object.assign(document.createElement("template"), { innerHTML: s.trim() }).content.firstChild;
45
+
46
+ const applyingBindings = new Set();
47
+
48
+ const safeApplyBinding = (b) => {
49
+ if (applyingBindings.has(b)) return;
50
+ applyingBindings.add(b);
51
+ try {
52
+ applyBinding(b);
53
+ } finally {
54
+ applyingBindings.delete(b);
55
+ }
56
+ };
57
+
58
+ const handler = {
59
+ has(t, p) {
60
+ if (typeof p === 'string' && p.startsWith('$')) return false;
61
+ if (Reflect.has(t, p)) return true;
62
+ if (typeof globalThis[p] !== 'undefined') return false;
63
+ return true;
64
+ },
65
+ get(t, p, r) {
66
+ const v = Reflect.get(t, p, r);
67
+ if (v && typeof v === "object") {
68
+ if (HELIUM.proxyCache.has(v)) return HELIUM.proxyCache.get(v);
69
+ const proxy = new Proxy(v, handler);
70
+ HELIUM.proxyCache.set(v, proxy);
71
+ HELIUM.parentKeys.set(v, p);
72
+ return proxy;
73
+ }
74
+ return v;
75
+ },
76
+ set: (t, p, v) => {
77
+ const res = Reflect.set(t, p, v);
78
+ HELIUM.bindings.get(p)?.forEach(safeApplyBinding);
79
+ const parentKey = HELIUM.parentKeys.get(t);
80
+ if (parentKey) HELIUM.bindings.get(parentKey)?.forEach(safeApplyBinding);
81
+ HELIUM.bindings.get(ALL)?.forEach(safeApplyBinding);
82
+ if (storageKey) localStorage.setItem(storageKey, JSON.stringify(state));
83
+ return res;
84
+ }
85
+ };
86
+
87
+ const state = new Proxy({}, handler);
88
+ if (storageKey) try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey))); } catch {}
89
+ Object.assign(state, initialState);
90
+
91
+ const getRefsObject = () => {
92
+ const refs = {};
93
+ for (const [name, element] of HELIUM.refs) {
94
+ refs[name] = element;
95
+ }
96
+ return refs;
97
+ };
98
+
99
+ const createScope = (el, event = {}) => {
100
+ return {
101
+ $,
102
+ $data: state,
103
+ $event: event,
104
+ $el: el,
105
+ $html: html,
106
+ ...getRefsObject()
107
+ };
108
+ };
109
+
110
+ const compile = (expr, withReturn = true) => {
111
+ const key = `${withReturn}:${expr}`;
112
+ if (HELIUM.fnCache.has(key)) return HELIUM.fnCache.get(key);
113
+ try {
114
+ const fn = new Function(
115
+ "$scope",
116
+ withReturn
117
+ ? `with($scope){with($scope.$data){return(${expr.trim()})}}`
118
+ : `with($scope){with($scope.$data){${expr.trim()}}}`
119
+ );
120
+ const compiled = { execute: (scope) => fn(scope), getIds: null };
121
+ HELIUM.fnCache.set(key, compiled);
122
+ return compiled;
123
+ } catch {
124
+ const compiled = { execute: () => expr, getIds: null };
125
+ HELIUM.fnCache.set(key, compiled);
126
+ return compiled;
127
+ }
128
+ };
129
+
130
+ function applyBinding(b, e = {}, elCtx = b.el) {
131
+ const { el, prop, compiled, calc, isHiddenAttr, isRadio } = b;
132
+ const scope = createScope(elCtx, e);
133
+ const r = compiled.execute(scope);
134
+ if (calc) state[calc] = r;
135
+
136
+ if (prop === "innerHTML") {
137
+ const content = Array.isArray(r) ? r.join`` : r;
138
+ return typeof Idiomorph === "object"
139
+ ? Idiomorph.morph(el, content, { morphStyle: 'innerHTML' })
140
+ : el.innerHTML = content;
141
+ }
142
+ if (prop === "class" && r && typeof r === "object")
143
+ return Object.entries(r).forEach(([k, v]) =>
144
+ k.split(/\s+/).forEach(c => el.classList.toggle(c, v)));
145
+
146
+ if (prop === "style" && r && typeof r === "object")
147
+ return el.style.cssText = Object.entries(r)
148
+ .filter(([_, v]) => v != null && v !== false)
149
+ .map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}:${v}`)
150
+ .join("; ");
151
+
152
+ if (prop === "hidden") {
153
+ el.hidden = isHiddenAttr ? !!r : !r;
154
+ return;
155
+ }
156
+
157
+ if (prop in el) {
158
+ if (isRadio && prop !== "checked") el.checked = el.value === r;
159
+ else if (el.type === "radio" && prop !== "checked") el.checked = el.value === r;
160
+ else el[prop] = prop === "textContent" ? (r ?? '') : parseEx(r);
161
+ return;
162
+ }
163
+
164
+ el.setAttribute(prop, parseEx(r));
165
+ }
166
+
167
+ const trackDependenciesProxy = (compiled, el, excludeChanged = false) => {
168
+ const accessed = excludeChanged ? new Map() : new Set();
169
+ const trackProxy = new Proxy(state, {
170
+ has(target, prop) {
171
+ return true;
172
+ },
173
+ get(target, prop) {
174
+ if (typeof prop == 'string') {
175
+ excludeChanged ? !accessed.has(prop) && accessed.set(prop, target[prop]) : accessed.add(prop);
176
+ }
177
+ const val = target[prop];
178
+ return typeof val == "object" && val != null ? new Proxy(val, this) : val;
179
+ }
180
+ });
181
+
182
+ try {
183
+ const scope = {
184
+ $,
185
+ $data: trackProxy,
186
+ $event: {},
187
+ $el: el,
188
+ $html: html,
189
+ ...getRefsObject()
190
+ };
191
+ compiled.execute(scope);
192
+ } catch {}
193
+
194
+ return excludeChanged ? [...accessed.keys()].filter(prop => state[prop] === accessed.get(prop)) : [...accessed];
195
+ };
196
+
197
+ const getDependencies = (compiled, el, excludeChanged = false) => {
198
+ return trackDependenciesProxy(compiled, el, excludeChanged);
199
+ };
200
+
201
+ const cleanup = el => {
202
+ [el, ...el.querySelectorAll('*')].forEach(e => {
203
+ HELIUM.listeners.get(e)?.forEach(({ receiver, event, handler }) => receiver.removeEventListener(event, handler));
204
+ HELIUM.listeners.delete(e);
205
+ });
206
+ };
207
+
208
+ async function processElements(element) {
209
+ const newBindings = [];
210
+ const deferredBindings = [];
211
+
212
+ const heElements = [element, ...element.querySelectorAll("*")].filter(e => {
213
+ if (HELIUM.processed.has(e)) return false;
214
+ for (let i = 0; i < e.attributes.length; i++) {
215
+ if (ATTR_REGEX.test(e.attributes[i].name)) return true;
216
+ }
217
+ return false;
218
+ });
219
+
220
+ const addBinding = (val, b) => {
221
+ HELIUM.bindings.set(val, b.calc ? [b, ...(HELIUM.bindings.get(val) || [])] : [...(HELIUM.bindings.get(val) || []), b]);
222
+ b.calc ? newBindings.unshift(b) : newBindings.push(b);
223
+ };
224
+
225
+ heElements.forEach(el => {
226
+ HELIUM.processed.add(el);
227
+
228
+ const attrs = el.attributes;
229
+ const inputType = el.type?.toLowerCase();
230
+ const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
231
+
232
+ for (let i = 0; i < attrs.length; i++) {
233
+ const { name, value } = attrs[i];
234
+
235
+ if (!name.startsWith('@') && !name.startsWith(':') && !name.startsWith('data-he')) continue;
236
+
237
+ // Initialize state first if needed (skip reserved words, only if not already set)
238
+ if (match(name, "text", "html", "bind") && isValidIdentifier(value) && !RESERVED.has(value) && state[value] == null) {
239
+ state[value] = match(name, "bind") ? (el.type == "checkbox" ? el.checked : tryNum(el.value)) : tryNum(el.textContent);
240
+ }
241
+
242
+ // Process the attribute
243
+ if (name === "@data" || name === "data-he") {
244
+ Object.assign(state, parseEx(value));
245
+ }
246
+ else if (name.startsWith(":") || name.startsWith("data-he-attr:")) {
247
+ const propName = name.startsWith(":") ? name.slice(1) : name.slice(13);
248
+ deferredBindings.push({ el, prop: propName, compiled: compile(value, true) });
249
+ }
250
+ else if (match(name, "ref")) {
251
+ HELIUM.refs.set("$" + value, el);
252
+ }
253
+ else if (match(name, "text", "html")) {
254
+ deferredBindings.push({ el, prop: match(name, "text") ? "textContent" : "innerHTML", compiled: compile(value, true) });
255
+ }
256
+ else if (match(name, "bind")) {
257
+ const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
258
+ const prop = isCheckbox ? "checked" : "value";
259
+ const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
260
+ el.addEventListener(event, inputHandler);
261
+ if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
262
+ HELIUM.listeners.get(el).push({ receiver: el, event, handler: inputHandler });
263
+ deferredBindings.push({ el, prop, compiled: compile(value, true), isRadio });
264
+ if (isCheckbox) el.checked = !!state[value];
265
+ else if (isRadio) el.checked = el.value == state[value];
266
+ else el.value = state[value] ?? "";
267
+ }
268
+ else if (match(name, "hidden", "visible")) {
269
+ const isHidden = match(name, "hidden");
270
+ deferredBindings.push({ el, prop: "hidden", compiled: compile(value, true), isHiddenAttr: isHidden });
271
+ }
272
+ else if (match(name, "calculate")) {
273
+ const calcName = name.split(":")[1];
274
+ if (!(calcName in state)) state[calcName] = undefined;
275
+ deferredBindings.push({ el, calc: calcName, prop: null, compiled: compile(value, true) });
276
+ }
277
+ else if (match(name, "effect")) {
278
+ deferredBindings.push({ el, prop: null, compiled: compile(value, true), keys: name.split(":").slice(1) });
279
+ }
280
+ else if (match(name, "init")) {
281
+ inits.push({ compiled: compile(value, true), el });
282
+ }
283
+ else if (name.startsWith("@") || name.startsWith("data-he")) {
284
+ const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
285
+ const [event, ...mods] = fullName.split(".");
286
+ const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
287
+ const debounceMod = mods.find(m => m.startsWith("debounce"));
288
+ const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
289
+ const _handler = e => {
290
+ if (mods.includes("prevent")) e.preventDefault();
291
+ if (mods.includes("stop")) e.stopPropagation();
292
+ for (const [mod, prop] of Object.entries(KEY_MODS)) if (mods.includes(mod) && !e[prop]) return;
293
+ if (["keydown", "keyup", "keypress"].includes(event)) {
294
+ const last = mods[mods.length - 1];
295
+ if (last && !["prevent", "once", "outside", "document", "stop"].includes(last) && !last.startsWith("debounce") && !Object.keys(KEY_MODS).includes(last)) {
296
+ const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
297
+ if (keyName.toLowerCase() !== last.toLowerCase()) return;
298
+ }
299
+ }
300
+ if (!mods.includes("outside") || !el.contains(e.target)) {
301
+ const scope = createScope(el, e);
302
+ const compiled = compile(value, true);
303
+ compiled.execute(scope);
304
+ }
305
+ if (mods.includes("once")) receiver.removeEventListener(event, handler);
306
+ };
307
+ const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
308
+ const eventOptions = mods.includes("once") ? { once: true } : undefined;
309
+ receiver.addEventListener(event, handler, eventOptions);
310
+ if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
311
+ HELIUM.listeners.get(el).push({ receiver, event, handler });
312
+ }
313
+ }
314
+ });
315
+
316
+ deferredBindings.forEach(b => {
317
+ const tracked = b.keys?.includes("*") ? [ALL] : getDependencies(b.compiled, b.el, true).concat(b.keys || []);
318
+ tracked.forEach(key => addBinding(key, b));
319
+ if (tracked.length === 0) {
320
+ newBindings.push(b);
321
+ }
322
+ });
323
+
324
+ return newBindings;
325
+ }
326
+
327
+ HELIUM.observer?.disconnect();
328
+ HELIUM.observer = new MutationObserver(async (ms) => {
329
+ for (const m of ms) {
330
+ m.removedNodes.forEach((n) => n.nodeType === 1 && cleanup(n));
331
+
332
+ for (const n of m.addedNodes) {
333
+ if (n.nodeType === 1 && !HELIUM.processed.has(n)) {
334
+ const bindings = await processElements(n);
335
+ bindings?.forEach(applyBinding);
336
+ }
337
+ }
338
+ }
339
+ });
340
+ HELIUM.observer.observe(root, { childList: true, subtree: true });
341
+
342
+ const initialBindings = await processElements(root);
343
+ initialBindings.forEach(applyBinding);
344
+ inits.forEach(({ compiled, el }) => {
345
+ const scope = createScope(el);
346
+ compiled.execute(scope);
347
+ });
348
+
349
+ return state;
350
+ }
351
+
352
+ function heliumTeardown() {
353
+ if (HELIUM?.observer) HELIUM.observer.disconnect();
354
+ HELIUM = null;
355
+ }
356
+
357
+ // Browser globals
358
+ if (typeof window !== 'undefined') {
359
+ window.helium = helium;
360
+ window.heliumTeardown = heliumTeardown;
361
+ }
362
+
363
+ // Auto-initialize
364
+ if (typeof document !== 'undefined') {
365
+ document.addEventListener("DOMContentLoaded", () => helium());
366
+ }
367
+
368
+ export { helium, heliumTeardown };
369
+ export default helium;
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { helium, heliumTeardown } from './helium-lite.js';
3
+
4
+ describe('Helium Lite', () => {
5
+ let container;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement('div');
9
+ container.setAttribute('data-helium', '');
10
+ document.body.appendChild(container);
11
+ });
12
+
13
+ afterEach(() => {
14
+ heliumTeardown();
15
+ document.body.innerHTML = '';
16
+ });
17
+
18
+ describe('Default State Values', () => {
19
+ it('should initialize state from @text element content', async () => {
20
+ container.innerHTML = '<span @text="name">John</span>';
21
+ const state = await helium();
22
+ expect(state.name).toBe('John');
23
+ });
24
+
25
+ it('should initialize state from @bind input value', async () => {
26
+ container.innerHTML = '<input @bind="email" value="test@example.com" />';
27
+ const state = await helium();
28
+ expect(state.email).toBe('test@example.com');
29
+ });
30
+
31
+ it('should initialize numeric state from @text', async () => {
32
+ container.innerHTML = '<span @text="count">42</span>';
33
+ const state = await helium();
34
+ expect(state.count).toBe(42);
35
+ });
36
+
37
+ it('should not override initial state with defaults', async () => {
38
+ container.innerHTML = '<span @text="name">Default</span>';
39
+ const state = await helium({ name: 'Custom' });
40
+ expect(state.name).toBe('Custom');
41
+ });
42
+
43
+ it('should initialize checkbox state from checked attribute', async () => {
44
+ container.innerHTML = '<input type="checkbox" @bind="agreed" checked />';
45
+ const state = await helium();
46
+ expect(state.agreed).toBe(true);
47
+ });
48
+ });
49
+
50
+ describe('Basic Reactivity', () => {
51
+ it('should update text on state change', async () => {
52
+ container.innerHTML = '<div @text="message"></div>';
53
+ const state = await helium({ message: 'Hello' });
54
+ expect(container.querySelector('div').textContent).toBe('Hello');
55
+
56
+ state.message = 'World';
57
+ await new Promise(r => setTimeout(r, 0));
58
+ expect(container.querySelector('div').textContent).toBe('World');
59
+ });
60
+
61
+ it('should handle @bind two-way binding', async () => {
62
+ container.innerHTML = '<input @bind="name" /><span @text="name"></span>';
63
+ const state = await helium({ name: 'John' });
64
+
65
+ const input = container.querySelector('input');
66
+ expect(input.value).toBe('John');
67
+
68
+ input.value = 'Jane';
69
+ input.dispatchEvent(new Event('input'));
70
+ await new Promise(r => setTimeout(r, 0));
71
+
72
+ expect(state.name).toBe('Jane');
73
+ expect(container.querySelector('span').textContent).toBe('Jane');
74
+ });
75
+ });
76
+
77
+ describe('Events', () => {
78
+ it('should handle click events', async () => {
79
+ container.innerHTML = '<button @click="count++">Click</button><span @text="count"></span>';
80
+ const state = await helium({ count: 0 });
81
+
82
+ container.querySelector('button').click();
83
+ await new Promise(r => setTimeout(r, 0));
84
+ expect(state.count).toBe(1);
85
+ });
86
+ });
87
+
88
+ describe('@calculate', () => {
89
+ it('should create computed properties', async () => {
90
+ container.innerHTML = '<div @calculate:double="count * 2"></div><span @text="double"></span>';
91
+ const state = await helium({ count: 5 });
92
+ expect(container.querySelector('span').textContent).toBe('10');
93
+ });
94
+ });
95
+
96
+ describe('@ref', () => {
97
+ it('should create element references', async () => {
98
+ container.innerHTML = '<div @ref="myDiv"></div><button @click="$myDiv.textContent = \'Changed\'">Change</button>';
99
+ await helium({});
100
+
101
+ container.querySelector('button').click();
102
+ await new Promise(r => setTimeout(r, 0));
103
+ expect(container.querySelector('div').textContent).toBe('Changed');
104
+ });
105
+ });
106
+
107
+ describe('Dynamic Attributes', () => {
108
+ it('should bind :class', async () => {
109
+ container.innerHTML = '<div :class="className"></div>';
110
+ await helium({ className: 'active' });
111
+ expect(container.querySelector('div').getAttribute('class')).toBe('active');
112
+ });
113
+
114
+ it('should handle :class with object syntax', async () => {
115
+ container.innerHTML = '<div :class="{ active: isActive }"></div>';
116
+ await helium({ isActive: true });
117
+ expect(container.querySelector('div').classList.contains('active')).toBe(true);
118
+ });
119
+ });
120
+ });
package/helium.js CHANGED
@@ -52,6 +52,15 @@ export function createHelium(options = {}) {
52
52
  const prefix = options.prefix ?? 'he'; // 'he' or 'xe'
53
53
  const rootAttr = options.rootAttr ?? 'helium'; // 'helium' or 'xenon'
54
54
 
55
+ // Engine-aware expression parser (CSP-safe when using jexpr engine)
56
+ const evalExpr = v => {
57
+ try {
58
+ const compiled = engine.compile(v, true);
59
+ const scope = engine.createScope ? engine.createScope({ state: {}, el: null, event: {}, refs: {}, $: null, html: null, get: null, post: null, put: null, patch: null, del: null }) : {};
60
+ return compiled.execute(scope);
61
+ } catch { return v; }
62
+ };
63
+
55
64
  // Build prefix-aware regex and matcher
56
65
  const ATTR_REGEX = new RegExp(`^(@|:|data-${prefix})`);
57
66
 
@@ -68,6 +77,7 @@ export function createHelium(options = {}) {
68
77
  const inits = [];
69
78
  const ALL = Symbol("all");
70
79
  const root = document.querySelector(`[\\@${rootAttr}]`) || document.querySelector(`[data-${rootAttr}]`) || document.body;
80
+ const storageKey = root.getAttribute('@local-storage') || root.getAttribute(`data-${prefix}-local-storage`);
71
81
 
72
82
  // Initialize or reuse HELIUM object
73
83
  if (!HELIUM) {
@@ -225,6 +235,7 @@ export function createHelium(options = {}) {
225
235
  const parentKey = HELIUM.parentKeys.get(t);
226
236
  if (parentKey) HELIUM.bindings.get(parentKey)?.forEach(safeApplyBinding);
227
237
  HELIUM.bindings.get(ALL)?.forEach(safeApplyBinding);
238
+ if (storageKey) localStorage.setItem(storageKey, JSON.stringify(state));
228
239
  return res
229
240
  }
230
241
  };
@@ -232,6 +243,9 @@ export function createHelium(options = {}) {
232
243
  // Initialize state if it doesn't exist
233
244
  const state = new Proxy({}, handler);
234
245
 
246
+ // Load from localStorage if key exists
247
+ if (storageKey) try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey))); } catch {}
248
+
235
249
  // Merge initial state
236
250
  Object.assign(state, initialState);
237
251
 
@@ -389,11 +403,14 @@ export function createHelium(options = {}) {
389
403
  const importAttr = el.getAttribute("@import") || el.getAttribute(`data-${prefix}-import`);
390
404
  if (importAttr) {
391
405
  importAttr.split(",").map((m) => m.trim()).forEach((moduleName) => {
392
- // Build path: use as-is for URLs, otherwise add ./ prefix and .js suffix as needed
406
+ // Build path: use as-is for URLs, otherwise resolve relative to document location
393
407
  const isUrl = moduleName.startsWith("http://") || moduleName.startsWith("https://");
394
408
  const hasExtension = moduleName.endsWith(".js");
395
409
  const hasPathPrefix = moduleName.startsWith("/") || moduleName.startsWith("./") || moduleName.startsWith("../");
396
- const path = (isUrl || hasPathPrefix ? "" : "./") + moduleName + (isUrl || hasExtension ? "" : ".js");
410
+ const relativePath = (isUrl || hasPathPrefix ? "" : "./") + moduleName + (isUrl || hasExtension ? "" : ".js");
411
+ // Ensure base URL ends with / so relative paths resolve correctly
412
+ const baseUrl = location.href.endsWith('/') || location.href.includes('.html') ? location.href : location.href + '/';
413
+ const path = isUrl ? relativePath : new URL(relativePath, baseUrl).href;
397
414
  importPromises.push(
398
415
  import(path)
399
416
  .then((module) => {
@@ -434,13 +451,13 @@ export function createHelium(options = {}) {
434
451
  if (!name.startsWith('@') && !name.startsWith(':') && !name.startsWith(`data-${prefix}`)) continue;
435
452
 
436
453
  // Initialize state first if needed (skip reserved words, only if not already set)
437
- if (match(name, "text", "html", "bind") && isValidIdentifier(value) && !RESERVED.has(value) && !(value in state)) {
454
+ if (match(name, "text", "html", "bind") && isValidIdentifier(value) && !RESERVED.has(value) && state[value] == null) {
438
455
  state[value] = match(name, "bind") ? (el.type == "checkbox" ? el.checked : tryNum(el.value)) : tryNum(el.textContent);
439
456
  }
440
457
 
441
458
  // Process the attribute
442
459
  if (name === "@data" || name === `data-${prefix}`) {
443
- Object.assign(state, parseEx(value));
460
+ Object.assign(state, evalExpr(value));
444
461
  }
445
462
  else if (name.startsWith(":") || name.startsWith(`data-${prefix}-attr:`)) {
446
463
  const propName = name.startsWith(":") ? name.slice(1) : name.slice(prefix.length + 11);
@@ -505,7 +522,7 @@ export function createHelium(options = {}) {
505
522
  const target = pairs.map(([target]) => target);
506
523
  const action = pairs.map(([, action]) => action);
507
524
  const options = {
508
- ...(getAttr("options") && parseEx(getAttr("options") || "{}")),
525
+ ...(getAttr("options") && evalExpr(getAttr("options") || "{}")),
509
526
  ...(target && { target }),
510
527
  ...(action && { action }),
511
528
  ...(getAttr("template") && { template: execExpr(getAttr("template")),}),
@@ -587,7 +604,16 @@ export function createHelium(options = {}) {
587
604
  }
588
605
 
589
606
  // Create default instance for backwards compatibility
590
- const { helium: defaultHelium, heliumTeardown: defaultTeardown } = createHelium();
607
+ const { helium: defaultHelium, heliumTeardown: _defaultTeardown } = createHelium();
608
+
609
+ // Flag to disable default auto-init (used by helium-csp.js and other variants)
610
+ let defaultDisabled = false;
611
+
612
+ // Wrapped teardown that also prevents future auto-init
613
+ function defaultTeardown() {
614
+ defaultDisabled = true;
615
+ _defaultTeardown();
616
+ }
591
617
 
592
618
  // Setup browser globals and auto-initialization
593
619
  if (typeof window !== 'undefined') {
@@ -597,11 +623,11 @@ if (typeof window !== 'undefined') {
597
623
 
598
624
  // Initialize on load
599
625
  if (typeof document !== 'undefined') {
600
- document.addEventListener("DOMContentLoaded",_ => defaultHelium());
626
+ document.addEventListener("DOMContentLoaded",_ => !defaultDisabled && defaultHelium());
601
627
  // Turbo integration
602
628
  document.addEventListener("turbo:before-render",_ => defaultTeardown());
603
- document.addEventListener("turbo:render",_ => defaultHelium());
629
+ document.addEventListener("turbo:render",_ => !defaultDisabled && defaultHelium());
604
630
  }
605
631
 
606
- export { createHelium };
632
+ export { defaultTeardown };
607
633
  export default defaultHelium;