@daz4126/helium 0.30.0 → 0.31.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.
@@ -0,0 +1,138 @@
1
+ import http from 'http';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const PORT = process.env.PORT || 3456;
8
+
9
+ // Helper to send SSE events
10
+ const sendSSE = (res, event, data, id = null) => {
11
+ if (id) res.write(`id: ${id}\n`);
12
+ if (event) res.write(`event: ${event}\n`);
13
+ res.write(`data: ${data}\n\n`);
14
+ };
15
+
16
+ // Delay helper
17
+ const delay = ms => new Promise(r => setTimeout(r, ms));
18
+
19
+ const server = http.createServer(async (req, res) => {
20
+ const url = new URL(req.url, `http://localhost:${PORT}`);
21
+
22
+ // Serve static files
23
+ if (url.pathname === '/' || url.pathname === '/index.html') {
24
+ res.writeHead(200, { 'Content-Type': 'text/html' });
25
+ res.end(fs.readFileSync(path.join(__dirname, 'sse-simple.html')));
26
+ return;
27
+ }
28
+
29
+ if (url.pathname === '/demo') {
30
+ res.writeHead(200, { 'Content-Type': 'text/html' });
31
+ res.end(fs.readFileSync(path.join(__dirname, 'sse-demo.html')));
32
+ return;
33
+ }
34
+
35
+ if (url.pathname === '/helium.js' || url.pathname === '/../helium.js') {
36
+ res.writeHead(200, { 'Content-Type': 'application/javascript' });
37
+ res.end(fs.readFileSync(path.join(__dirname, '..', 'helium.js')));
38
+ return;
39
+ }
40
+
41
+ // SSE Endpoints
42
+ if (url.pathname === '/api/hello-world') {
43
+ res.writeHead(200, {
44
+ 'Content-Type': 'text/event-stream',
45
+ 'Cache-Control': 'no-cache',
46
+ 'Connection': 'keep-alive'
47
+ });
48
+
49
+ // Stream letters one at a time with 500ms delay
50
+ // Just send data - target comes from @target attribute on the button
51
+ const message = 'Hello from Helium SSE!';
52
+ let output = '';
53
+ for (const char of message) {
54
+ output += char;
55
+ sendSSE(res, null, `<span>${output}<span style="opacity:0.5">|</span></span>`);
56
+ await delay(500);
57
+ }
58
+ // Final message without cursor
59
+ sendSSE(res, null, `<span style="color: green; font-weight: bold;">${message}</span>`);
60
+ res.end();
61
+ return;
62
+ }
63
+
64
+ if (url.pathname === '/api/count') {
65
+ res.writeHead(200, {
66
+ 'Content-Type': 'text/event-stream',
67
+ 'Cache-Control': 'no-cache',
68
+ 'Connection': 'keep-alive'
69
+ });
70
+
71
+ for (let i = 1; i <= 10; i++) {
72
+ sendSSE(res, '#counter', `<span>${i}</span>`, `count-${i}`);
73
+ await delay(300);
74
+ }
75
+ sendSSE(res, '#counter', `<span style="color: green;">Done!</span>`);
76
+ res.end();
77
+ return;
78
+ }
79
+
80
+ if (url.pathname === '/api/dashboard') {
81
+ res.writeHead(200, {
82
+ 'Content-Type': 'text/event-stream',
83
+ 'Cache-Control': 'no-cache',
84
+ 'Connection': 'keep-alive'
85
+ });
86
+
87
+ // Send to different targets using event: field
88
+ await delay(200);
89
+ sendSSE(res, '#header', '<h3>Dashboard Loaded</h3>');
90
+ await delay(300);
91
+ sendSSE(res, '#stats', `<div>Users: 1,234</div><div>Revenue: $5,678</div>`);
92
+ await delay(200);
93
+ sendSSE(res, '#header', '<h3>Dashboard Loaded <span style="color:green;">&#10003;</span></h3>');
94
+ res.end();
95
+ return;
96
+ }
97
+
98
+ if (url.pathname === '/api/time') {
99
+ res.writeHead(200, {
100
+ 'Content-Type': 'text/event-stream',
101
+ 'Cache-Control': 'no-cache',
102
+ 'Connection': 'keep-alive'
103
+ });
104
+
105
+ // This updates state.currentTime (not a DOM element)
106
+ sendSSE(res, 'currentTime', new Date().toLocaleTimeString());
107
+ res.end();
108
+ return;
109
+ }
110
+
111
+ if (url.pathname === '/api/events') {
112
+ res.writeHead(200, {
113
+ 'Content-Type': 'text/event-stream',
114
+ 'Cache-Control': 'no-cache',
115
+ 'Connection': 'keep-alive'
116
+ });
117
+
118
+ const events = ['User logged in', 'File uploaded', 'Comment added', 'Order placed', 'Payment received'];
119
+
120
+ for (let i = 0; i < events.length; i++) {
121
+ const time = new Date().toLocaleTimeString();
122
+ sendSSE(res, '#events', `<div class="event-item">[${time}] ${events[i]}</div>`, `event-${i}`);
123
+ await delay(800);
124
+ }
125
+ sendSSE(res, '#events', `<div class="event-item" style="color: gray;">[Stream ended]</div>`);
126
+ res.end();
127
+ return;
128
+ }
129
+
130
+ // 404
131
+ res.writeHead(404);
132
+ res.end('Not found');
133
+ });
134
+
135
+ server.listen(PORT, () => {
136
+ console.log(`SSE Demo server running at http://localhost:${PORT}`);
137
+ console.log('Press Ctrl+C to stop');
138
+ });
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Helium SSE - Simple Demo</title>
7
+ <style>
8
+ body {
9
+ font-family: system-ui, sans-serif;
10
+ max-width: 600px;
11
+ margin: 4rem auto;
12
+ padding: 0 1rem;
13
+ text-align: center;
14
+ }
15
+ button {
16
+ padding: 1rem 2rem;
17
+ font-size: 1.2rem;
18
+ cursor: pointer;
19
+ background: #4a90d9;
20
+ color: white;
21
+ border: none;
22
+ border-radius: 8px;
23
+ margin-bottom: 2rem;
24
+ }
25
+ button:hover { background: #357abd; }
26
+ button:active { background: #2a5f8f; }
27
+ #message {
28
+ padding: 2rem;
29
+ background: #f5f5f5;
30
+ border-radius: 8px;
31
+ min-height: 3rem;
32
+ font-size: 1.5rem;
33
+ border: 2px solid #ddd;
34
+ }
35
+ </style>
36
+ </head>
37
+ <body data-helium>
38
+ <h1>Helium SSE Demo</h1>
39
+
40
+ <button @get="/api/hello-world" @target="#message">Start</button>
41
+
42
+ <div id="message">&nbsp;</div>
43
+
44
+ <script type="module">
45
+ import helium from '../helium.js';
46
+ helium();
47
+ </script>
48
+ </body>
49
+ </html>
@@ -0,0 +1,251 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Test Password Strength</title>
5
+ <style>
6
+ body { font-family: sans-serif; padding: 2rem; }
7
+ .progress-bar { width: 100%; height: 20px; background: #eee; border-radius: 10px; overflow: hidden; }
8
+ .progress-fill { height: 100%; transition: width 0.3s; }
9
+ </style>
10
+ </head>
11
+ <body>
12
+
13
+ <div data-helium @data="{ password: '' }">
14
+ <h2>Password Strength Test</h2>
15
+
16
+ <input type="text" @bind="password" placeholder="Enter password">
17
+
18
+ <p>Password value: "<span @text="password"></span>"</p>
19
+ <p>Password length: <span @text="password.length"></span></p>
20
+
21
+ <span @calculate:strength="password ? (password.length < 4 ? 'Weak' : password.length < 8 ? 'Medium' : 'Strong') : 'Enter a password'"></span>
22
+
23
+ <p>Strength: <b @text="strength">???</b></p>
24
+ </div>
25
+
26
+ <script>
27
+ // Unminified helium for debugging
28
+ const parseEx=v=>{try{return Function(`return(${v})`)()}catch{return v}};
29
+ const tryNum=v=>{const t=String(v).trim(),n=+t;return t&&!isNaN(n)?n:v};
30
+ const isValidIdentifier=v=>/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(v);
31
+ const RESERVED=new Set(['undefined','null','true','false','NaN','Infinity','this','arguments']);
32
+ const INPUT_EVENTS={form:"submit",input:"input",textarea:"input",select:"change"};
33
+ const debounce=(f,d)=>{let t;return(...a)=>(clearTimeout(t),t=setTimeout(f,d,...a))};
34
+
35
+ const defaultEngine={
36
+ compile(expr,withReturn=true){
37
+ try{
38
+ const fn=new Function("$scope",withReturn?`with($scope){with($scope.$data){return(${expr.trim()})}}`:`with($scope){with($scope.$data){${expr.trim()}}}`);
39
+ return{execute:scope=>fn(scope),getIds:null};
40
+ }catch(e){
41
+ console.error('Compile error:', expr, e);
42
+ return{execute:()=>expr,getIds:null};
43
+ }
44
+ },
45
+ createScope(ctx){
46
+ return{$:ctx.$,$data:ctx.state,$event:ctx.event,$el:ctx.el,...ctx.refs};
47
+ }
48
+ };
49
+
50
+ function createHelium(options={}){
51
+ const engine=options.engine||defaultEngine;
52
+ const prefix=options.prefix||'he';
53
+ const rootAttr=options.rootAttr||'helium';
54
+ const ATTR_REGEX=new RegExp(`^(@|:|data-${prefix})`);
55
+ const match=(name,...attrs)=>{
56
+ const p=name.split(/[.:]/)[0];
57
+ if(p===":"||p==="")return false;
58
+ return attrs.some(a=>p===`@${a}`||p===`data-${prefix}-${a}`);
59
+ };
60
+ let HELIUM=null;
61
+
62
+ async function helium(initialState={}){
63
+ console.log('[HELIUM] Starting...');
64
+ const ALL=Symbol("all");
65
+ const root=document.querySelector(`[\\@${rootAttr}]`)||document.querySelector(`[data-${rootAttr}]`)||document.body;
66
+ console.log('[HELIUM] Root:', root);
67
+
68
+ if(!HELIUM){
69
+ HELIUM={bindings:new Map(),refs:new Map(),listeners:new WeakMap(),processed:new WeakSet(),fnCache:new Map(),proxyCache:new WeakMap()};
70
+ }
71
+
72
+ const $=s=>document.querySelector(s);
73
+ const applyingBindings=new Set();
74
+ const safeApplyBinding=(b)=>{
75
+ if(applyingBindings.has(b))return;
76
+ applyingBindings.add(b);
77
+ try{applyBinding(b);}finally{applyingBindings.delete(b);}
78
+ };
79
+
80
+ const handler={
81
+ get(t,p,r){
82
+ const v=Reflect.get(t,p,r);
83
+ if(v&&typeof v==="object"){
84
+ if(HELIUM.proxyCache.has(v))return HELIUM.proxyCache.get(v);
85
+ const proxy=new Proxy(v,handler);
86
+ HELIUM.proxyCache.set(v,proxy);
87
+ return proxy;
88
+ }
89
+ return v;
90
+ },
91
+ set:(t,p,v)=>{
92
+ console.log('[PROXY SET]', p, '=', v);
93
+ const res=Reflect.set(t,p,v);
94
+ const bindings = HELIUM.bindings.get(p);
95
+ console.log('[PROXY SET] Bindings for', p, ':', bindings?.length || 0);
96
+ bindings?.forEach(safeApplyBinding);
97
+ HELIUM.bindings.get(ALL)?.forEach(safeApplyBinding);
98
+ return res;
99
+ }
100
+ };
101
+
102
+ const state=new Proxy({},handler);
103
+ Object.assign(state,initialState);
104
+
105
+ const getRefsObject=()=>{const refs={};for(const[name,element]of HELIUM.refs){refs[name]=element;}return refs;};
106
+ const createScope=(el,event={})=>engine.createScope({$,state,event,el,refs:getRefsObject()});
107
+
108
+ const compile=(expr,withReturn=true)=>{
109
+ const key=`${withReturn}:${expr}`;
110
+ if(HELIUM.fnCache.has(key))return HELIUM.fnCache.get(key);
111
+ const compiled=engine.compile(expr,withReturn);
112
+ HELIUM.fnCache.set(key,compiled);
113
+ return compiled;
114
+ };
115
+
116
+ function applyBinding(b){
117
+ const{el,prop,compiled,calc}=b;
118
+ const scope=createScope(el);
119
+ const r=compiled.execute(scope);
120
+ console.log('[APPLY]', 'calc=', calc, 'prop=', prop, 'result=', r);
121
+ if(calc){
122
+ console.log('[APPLY] Setting state.' + calc + ' =', r);
123
+ state[calc]=r;
124
+ }
125
+ if(prop==="textContent")el.textContent=r??'';
126
+ else if(prop==="style"&&r&&typeof r==="object"){
127
+ el.style.cssText=Object.entries(r).filter(([_,v])=>v!=null&&v!==false).map(([k,v])=>`${k}:${v}`).join("; ");
128
+ }
129
+ else if(prop&&prop in el)el[prop]=r;
130
+ }
131
+
132
+ const trackDependenciesProxy=(compiled,el,excludeChanged=false)=>{
133
+ const accessed=excludeChanged?new Map():new Set();
134
+ const trackProxy=new Proxy(state,{
135
+ has(target,prop){return true;},
136
+ get(target,prop){
137
+ if(typeof prop=='string'){
138
+ if(excludeChanged){
139
+ if(!accessed.has(prop))accessed.set(prop,target[prop]);
140
+ }else{
141
+ accessed.add(prop);
142
+ }
143
+ }
144
+ const val=target[prop];
145
+ return typeof val=="object"&&val!=null?new Proxy(val,this):val;
146
+ }
147
+ });
148
+ try{
149
+ const scope=engine.createScope({$,state:trackProxy,event:{},el,refs:getRefsObject()});
150
+ compiled.execute(scope);
151
+ }catch(e){console.error('[TRACK ERROR]', e);}
152
+ const result = excludeChanged?[...accessed.keys()].filter(prop=>state[prop]===accessed.get(prop)):[...accessed];
153
+ console.log('[TRACK]', 'excludeChanged=', excludeChanged, 'result=', result);
154
+ return result;
155
+ };
156
+
157
+ const getDependencies=(compiled,el,excludeChanged=false)=>{
158
+ return trackDependenciesProxy(compiled,el,excludeChanged);
159
+ };
160
+
161
+ const newBindings=[];
162
+ const deferredBindings=[];
163
+
164
+ const heElements=[root,...root.querySelectorAll("*")].filter(e=>{
165
+ if(HELIUM.processed.has(e))return false;
166
+ for(let i=0;i<e.attributes.length;i++){
167
+ if(ATTR_REGEX.test(e.attributes[i].name))return true;
168
+ }
169
+ return false;
170
+ });
171
+
172
+ console.log('[HELIUM] Found', heElements.length, 'elements to process');
173
+
174
+ const addBinding=(val,b)=>{
175
+ console.log('[ADD BINDING]', 'key=', String(val), 'calc=', b.calc, 'prop=', b.prop);
176
+ HELIUM.bindings.set(val,b.calc?[b,...(HELIUM.bindings.get(val)||[])]:[...(HELIUM.bindings.get(val)||[]),b]);
177
+ b.calc?newBindings.unshift(b):newBindings.push(b);
178
+ };
179
+
180
+ heElements.forEach(el=>{
181
+ HELIUM.processed.add(el);
182
+ const attrs=el.attributes;
183
+ const inputType=el.type?.toLowerCase();
184
+ const isCheckbox=inputType=="checkbox";
185
+
186
+ for(let i=0;i<attrs.length;i++){
187
+ const{name,value}=attrs[i];
188
+ if(!name.startsWith('@')&&!name.startsWith(':')&&!name.startsWith(`data-${prefix}`))continue;
189
+
190
+ console.log('[PROCESS]', el.tagName, name, '=', value);
191
+
192
+ if(match(name,"text","bind")&&isValidIdentifier(value)&&!RESERVED.has(value)&&!(value in state)){
193
+ state[value]=match(name,"bind")?tryNum(el.value):tryNum(el.textContent);
194
+ }
195
+
196
+ if(name==="@data"||name===`data-${prefix}`){
197
+ Object.assign(state,parseEx(value));
198
+ console.log('[DATA] State is now:', {...state});
199
+ }
200
+ else if(name.startsWith(":")){
201
+ const propName=name.slice(1);
202
+ deferredBindings.push({el,prop:propName,compiled:compile(value,true)});
203
+ }
204
+ else if(match(name,"text")){
205
+ deferredBindings.push({el,prop:"textContent",compiled:compile(value,true)});
206
+ }
207
+ else if(match(name,"bind")){
208
+ const event="input";
209
+ const inputHandler=e=>{
210
+ console.log('[INPUT]', value, '=', e.target.value);
211
+ state[value]=e.target.value;
212
+ };
213
+ el.addEventListener(event,inputHandler);
214
+ deferredBindings.push({el,prop:"value",compiled:compile(value,true)});
215
+ el.value=state[value]??"";
216
+ }
217
+ else if(match(name,"calculate")){
218
+ const calcName = name.split(":")[1];
219
+ console.log('[CALCULATE] Found @calculate:' + calcName);
220
+ deferredBindings.push({el,calc:calcName,prop:null,compiled:compile(value,true)});
221
+ }
222
+ }
223
+ });
224
+
225
+ console.log('[HELIUM] Processing', deferredBindings.length, 'deferred bindings');
226
+
227
+ deferredBindings.forEach(b=>{
228
+ const tracked=getDependencies(b.compiled,b.el,true);
229
+ console.log('[DEFERRED]', 'calc=', b.calc, 'prop=', b.prop, 'tracked=', tracked);
230
+ tracked.forEach(key=>addBinding(key,b));
231
+ if(tracked.length===0){
232
+ newBindings.push(b);
233
+ }
234
+ });
235
+
236
+ console.log('[HELIUM] Applying', newBindings.length, 'initial bindings');
237
+ newBindings.forEach(applyBinding);
238
+
239
+ console.log('[HELIUM] Done. State:', {...state});
240
+ return state;
241
+ }
242
+
243
+ return{helium};
244
+ }
245
+
246
+ const{helium}=createHelium();
247
+ document.addEventListener("DOMContentLoaded",()=>helium());
248
+ </script>
249
+
250
+ </body>
251
+ </html>