elit 3.0.1 → 3.0.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.
Files changed (87) hide show
  1. package/dist/build.d.ts +4 -12
  2. package/dist/build.d.ts.map +1 -0
  3. package/dist/chokidar.d.ts +7 -9
  4. package/dist/chokidar.d.ts.map +1 -0
  5. package/dist/cli.d.ts +6 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +17 -4
  8. package/dist/config.d.ts +29 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/dom.d.ts +7 -14
  11. package/dist/dom.d.ts.map +1 -0
  12. package/dist/el.d.ts +19 -191
  13. package/dist/el.d.ts.map +1 -0
  14. package/dist/fs.d.ts +35 -35
  15. package/dist/fs.d.ts.map +1 -0
  16. package/dist/hmr.d.ts +3 -3
  17. package/dist/hmr.d.ts.map +1 -0
  18. package/dist/http.d.ts +20 -22
  19. package/dist/http.d.ts.map +1 -0
  20. package/dist/https.d.ts +12 -15
  21. package/dist/https.d.ts.map +1 -0
  22. package/dist/index.d.ts +10 -629
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/mime-types.d.ts +9 -9
  25. package/dist/mime-types.d.ts.map +1 -0
  26. package/dist/path.d.ts +22 -19
  27. package/dist/path.d.ts.map +1 -0
  28. package/dist/router.d.ts +10 -17
  29. package/dist/router.d.ts.map +1 -0
  30. package/dist/runtime.d.ts +5 -6
  31. package/dist/runtime.d.ts.map +1 -0
  32. package/dist/server.d.ts +105 -7
  33. package/dist/server.d.ts.map +1 -0
  34. package/dist/server.js +14 -2
  35. package/dist/server.mjs +14 -2
  36. package/dist/state.d.ts +21 -27
  37. package/dist/state.d.ts.map +1 -0
  38. package/dist/style.d.ts +14 -55
  39. package/dist/style.d.ts.map +1 -0
  40. package/dist/types.d.ts +26 -240
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/ws.d.ts +14 -17
  43. package/dist/ws.d.ts.map +1 -0
  44. package/dist/wss.d.ts +16 -16
  45. package/dist/wss.d.ts.map +1 -0
  46. package/package.json +3 -2
  47. package/src/build.ts +337 -0
  48. package/src/chokidar.ts +401 -0
  49. package/src/cli.ts +638 -0
  50. package/src/config.ts +205 -0
  51. package/src/dom.ts +817 -0
  52. package/src/el.ts +164 -0
  53. package/src/fs.ts +727 -0
  54. package/src/hmr.ts +137 -0
  55. package/src/http.ts +775 -0
  56. package/src/https.ts +411 -0
  57. package/src/index.ts +14 -0
  58. package/src/mime-types.ts +222 -0
  59. package/src/path.ts +493 -0
  60. package/src/router.ts +237 -0
  61. package/src/runtime.ts +97 -0
  62. package/src/server.ts +1290 -0
  63. package/src/state.ts +468 -0
  64. package/src/style.ts +524 -0
  65. package/{dist/types-Du6kfwTm.d.ts → src/types.ts} +58 -141
  66. package/src/ws.ts +506 -0
  67. package/src/wss.ts +241 -0
  68. package/dist/build.d.mts +0 -20
  69. package/dist/chokidar.d.mts +0 -134
  70. package/dist/dom.d.mts +0 -87
  71. package/dist/el.d.mts +0 -207
  72. package/dist/fs.d.mts +0 -255
  73. package/dist/hmr.d.mts +0 -38
  74. package/dist/http.d.mts +0 -163
  75. package/dist/https.d.mts +0 -108
  76. package/dist/index.d.mts +0 -629
  77. package/dist/mime-types.d.mts +0 -48
  78. package/dist/path.d.mts +0 -163
  79. package/dist/router.d.mts +0 -47
  80. package/dist/runtime.d.mts +0 -97
  81. package/dist/server.d.mts +0 -7
  82. package/dist/state.d.mts +0 -111
  83. package/dist/style.d.mts +0 -159
  84. package/dist/types-C0nGi6MX.d.mts +0 -346
  85. package/dist/types.d.mts +0 -452
  86. package/dist/ws.d.mts +0 -195
  87. package/dist/wss.d.mts +0 -108
package/src/state.ts ADDED
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Elit - State Management
3
+ */
4
+
5
+ import type { State, StateOptions, VNode, VirtualListController, Child, Props } from './types';
6
+ import { dom } from './dom';
7
+
8
+ // State management helpers
9
+ export const createState = <T>(initial: T, options?: StateOptions): State<T> =>
10
+ dom.createState(initial, options);
11
+
12
+ export const computed = <T extends any[], R>(
13
+ states: { [K in keyof T]: State<T[K]> },
14
+ fn: (...values: T) => R
15
+ ): State<R> => dom.computed(states, fn);
16
+
17
+ export const effect = (fn: () => void): void => dom.effect(fn);
18
+
19
+ // Performance helpers
20
+ export const batchRender = (container: string | HTMLElement, vNodes: VNode[]): HTMLElement =>
21
+ dom.batchRender(container, vNodes);
22
+
23
+ export const renderChunked = (
24
+ container: string | HTMLElement,
25
+ vNodes: VNode[],
26
+ chunkSize?: number,
27
+ onProgress?: (current: number, total: number) => void
28
+ ): HTMLElement => dom.renderChunked(container, vNodes, chunkSize, onProgress);
29
+
30
+ export const createVirtualList = <T>(
31
+ container: HTMLElement,
32
+ items: T[],
33
+ renderItem: (item: T, index: number) => VNode,
34
+ itemHeight?: number,
35
+ bufferSize?: number
36
+ ): VirtualListController => dom.createVirtualList(container, items, renderItem, itemHeight, bufferSize);
37
+
38
+ export const lazy = <T extends any[], R>(loadFn: () => Promise<(...args: T) => R>) =>
39
+ dom.lazy(loadFn);
40
+
41
+ export const cleanupUnused = (root: HTMLElement): number =>
42
+ dom.cleanupUnusedElements(root);
43
+
44
+ // Throttle helper
45
+ export const throttle = <T extends any[]>(fn: (...args: T) => void, delay: number) => {
46
+ let timer: NodeJS.Timeout | null = null;
47
+ return (...args: T) => {
48
+ if (!timer) {
49
+ timer = setTimeout(() => {
50
+ timer = null;
51
+ fn(...args);
52
+ }, delay);
53
+ }
54
+ };
55
+ };
56
+
57
+ // Debounce helper
58
+ export const debounce = <T extends any[]>(fn: (...args: T) => void, delay: number) => {
59
+ let timer: NodeJS.Timeout | null = null;
60
+ return (...args: T) => {
61
+ timer && clearTimeout(timer);
62
+ timer = setTimeout(() => fn(...args), delay);
63
+ };
64
+ };
65
+
66
+ // ===== Shared State - syncs with elit-server =====
67
+
68
+ type StateChangeCallback<T = any> = (value: T, oldValue: T) => void;
69
+
70
+ interface StateMessage {
71
+ type: 'state:init' | 'state:update' | 'state:subscribe' | 'state:unsubscribe' | 'state:change';
72
+ key: string;
73
+ value?: any;
74
+ timestamp?: number;
75
+ }
76
+
77
+ /**
78
+ * Shared State - syncs with elit-server
79
+ */
80
+ export class SharedState<T = any> {
81
+ private localState: State<T>;
82
+ private ws: WebSocket | null = null;
83
+ private pendingUpdates: T[] = [];
84
+ private previousValue: T;
85
+
86
+ constructor(
87
+ public readonly key: string,
88
+ defaultValue: T,
89
+ private wsUrl?: string
90
+ ) {
91
+ this.localState = createState(defaultValue);
92
+ this.previousValue = defaultValue;
93
+ this.connect();
94
+ }
95
+
96
+ /**
97
+ * Get current value
98
+ */
99
+ get value(): T {
100
+ return this.localState.value;
101
+ }
102
+
103
+ /**
104
+ * Set new value and sync to server
105
+ */
106
+ set value(newValue: T) {
107
+ this.previousValue = this.localState.value;
108
+ this.localState.value = newValue;
109
+ this.sendToServer(newValue);
110
+ }
111
+
112
+ /**
113
+ * Get the underlying Elit State (for reactive binding)
114
+ */
115
+ get state(): State<T> {
116
+ return this.localState;
117
+ }
118
+
119
+ /**
120
+ * Subscribe to changes (returns Elit State for reactive)
121
+ */
122
+ onChange(callback: StateChangeCallback<T>): () => void {
123
+ return this.localState.subscribe((newValue) => {
124
+ const oldValue = this.previousValue;
125
+ this.previousValue = newValue;
126
+ callback(newValue, oldValue);
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Update value using a function
132
+ */
133
+ update(updater: (current: T) => T): void {
134
+ this.value = updater(this.value);
135
+ }
136
+
137
+ /**
138
+ * Connect to WebSocket
139
+ */
140
+ private connect(): void {
141
+ if (typeof window === 'undefined') return;
142
+
143
+ const url = this.wsUrl || `ws://${location.host}`;
144
+ this.ws = new WebSocket(url);
145
+
146
+ this.ws.addEventListener('open', () => {
147
+ this.subscribe();
148
+
149
+ // Send pending updates
150
+ while (this.pendingUpdates.length > 0) {
151
+ const value = this.pendingUpdates.shift();
152
+ this.sendToServer(value!);
153
+ }
154
+ });
155
+
156
+ this.ws.addEventListener('message', (event) => {
157
+ this.handleMessage(event.data);
158
+ });
159
+
160
+ this.ws.addEventListener('close', () => {
161
+ // Reconnect after delay
162
+ setTimeout(() => this.connect(), 1000);
163
+ });
164
+
165
+ this.ws.addEventListener('error', (error) => {
166
+ console.error('[SharedState] WebSocket error:', error);
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Subscribe to server state
172
+ */
173
+ private subscribe(): void {
174
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
175
+
176
+ this.ws.send(JSON.stringify({
177
+ type: 'state:subscribe',
178
+ key: this.key
179
+ }));
180
+ }
181
+
182
+ /**
183
+ * Handle message from server
184
+ */
185
+ private handleMessage(data: string): void {
186
+ try {
187
+ const msg = JSON.parse(data) as StateMessage;
188
+
189
+ if (msg.key !== this.key) return;
190
+
191
+ if (msg.type === 'state:init' || msg.type === 'state:update') {
192
+ // Update local state without sending back to server
193
+ this.localState.value = msg.value;
194
+ }
195
+ } catch (error) {
196
+ // Ignore parse errors (could be HMR messages)
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Send value to server
202
+ */
203
+ private sendToServer(value: T): void {
204
+ if (!this.ws) return;
205
+
206
+ if (this.ws.readyState !== WebSocket.OPEN) {
207
+ // Queue update for when connection is ready
208
+ this.pendingUpdates.push(value);
209
+ return;
210
+ }
211
+
212
+ this.ws.send(JSON.stringify({
213
+ type: 'state:change',
214
+ key: this.key,
215
+ value
216
+ }));
217
+ }
218
+
219
+ /**
220
+ * Disconnect
221
+ */
222
+ disconnect(): void {
223
+ if (this.ws) {
224
+ this.ws.close();
225
+ this.ws = null;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Destroy state and cleanup
231
+ */
232
+ destroy(): void {
233
+ this.disconnect();
234
+ this.localState.destroy();
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Create a shared state that syncs with elit-server
240
+ */
241
+ export function createSharedState<T>(
242
+ key: string,
243
+ defaultValue: T,
244
+ wsUrl?: string
245
+ ): SharedState<T> {
246
+ return new SharedState(key, defaultValue, wsUrl);
247
+ }
248
+
249
+ /**
250
+ * Shared State Manager for managing multiple shared states
251
+ */
252
+ class SharedStateManager {
253
+ private states = new Map<string, SharedState<any>>();
254
+
255
+ /**
256
+ * Create or get a shared state
257
+ */
258
+ create<T>(key: string, defaultValue: T, wsUrl?: string): SharedState<T> {
259
+ if (this.states.has(key)) {
260
+ return this.states.get(key) as SharedState<T>;
261
+ }
262
+
263
+ const state = new SharedState<T>(key, defaultValue, wsUrl);
264
+ this.states.set(key, state);
265
+ return state;
266
+ }
267
+
268
+ /**
269
+ * Get existing state
270
+ */
271
+ get<T>(key: string): SharedState<T> | undefined {
272
+ return this.states.get(key) as SharedState<T>;
273
+ }
274
+
275
+ /**
276
+ * Delete a state
277
+ */
278
+ delete(key: string): boolean {
279
+ const state = this.states.get(key);
280
+ if (state) {
281
+ state.destroy();
282
+ return this.states.delete(key);
283
+ }
284
+ return false;
285
+ }
286
+
287
+ /**
288
+ * Clear all states
289
+ */
290
+ clear(): void {
291
+ this.states.forEach(state => state.destroy());
292
+ this.states.clear();
293
+ }
294
+ }
295
+
296
+ // Export singleton instance
297
+ export const sharedStateManager = new SharedStateManager();
298
+
299
+ // ===== Reactive Rendering Helpers =====
300
+
301
+ // Helper function to schedule RAF updates (reused in reactive and reactiveAs)
302
+ const scheduleRAFUpdate = (rafId: number | null, updateFn: () => void): number => {
303
+ rafId && cancelAnimationFrame(rafId);
304
+ return requestAnimationFrame(() => {
305
+ updateFn();
306
+ });
307
+ };
308
+
309
+ // Helper function to render content to fragment (reused in reactive and reactiveAs)
310
+ const renderToFragment = (content: VNode | Child, isVNode?: boolean): DocumentFragment => {
311
+ const fragment = document.createDocumentFragment();
312
+
313
+ if (isVNode && content && typeof content === 'object' && 'tagName' in content) {
314
+ const { children } = content as VNode;
315
+ for (const child of children) {
316
+ dom.renderToDOM(child, fragment);
317
+ }
318
+ } else {
319
+ dom.renderToDOM(content, fragment);
320
+ }
321
+
322
+ return fragment;
323
+ };
324
+
325
+ // Helper function to update element props (reused in reactive)
326
+ const updateElementProps = (element: HTMLElement | SVGElement, props: Props): void => {
327
+ for (const key in props) {
328
+ const value = props[key];
329
+ if (key === 'ref') continue;
330
+
331
+ if (key === 'class' || key === 'className') {
332
+ (element as HTMLElement).className = Array.isArray(value) ? value.join(' ') : (value || '');
333
+ } else if (key === 'style' && typeof value === 'object') {
334
+ const s = (element as HTMLElement).style;
335
+ for (const k in value) (s as any)[k] = value[k];
336
+ } else if (key.startsWith('on')) {
337
+ (element as any)[key.toLowerCase()] = value;
338
+ } else if (value != null && value !== false) {
339
+ element.setAttribute(key, String(value === true ? '' : value));
340
+ } else {
341
+ element.removeAttribute(key);
342
+ }
343
+ }
344
+ };
345
+
346
+ // Reactive element helpers
347
+ export const reactive = <T>(state: State<T>, renderFn: (value: T) => VNode | Child): VNode => {
348
+ let rafId: number | null = null;
349
+ let elementRef: HTMLElement | SVGElement | null = null;
350
+ let placeholder: Comment | null = null;
351
+ let isInDOM = true;
352
+
353
+ const initialResult = renderFn(state.value);
354
+ const isVNodeResult = initialResult && typeof initialResult === 'object' && 'tagName' in initialResult;
355
+ const initialIsNull = initialResult == null || initialResult === false;
356
+
357
+ const updateElement = () => {
358
+ if (!elementRef && !placeholder) return;
359
+
360
+ const newResult = renderFn(state.value);
361
+ const resultIsNull = newResult == null || newResult === false;
362
+
363
+ if (resultIsNull) {
364
+ if (isInDOM && elementRef) {
365
+ placeholder = document.createComment('reactive');
366
+ elementRef.parentNode?.replaceChild(placeholder, elementRef);
367
+ isInDOM = false;
368
+ }
369
+ } else {
370
+ if (!isInDOM && placeholder && elementRef) {
371
+ placeholder.parentNode?.replaceChild(elementRef, placeholder);
372
+ placeholder = null;
373
+ isInDOM = true;
374
+ }
375
+
376
+ if (elementRef) {
377
+ const isCurrentVNode = !!(isVNodeResult && newResult && typeof newResult === 'object' && 'tagName' in newResult);
378
+ if (isCurrentVNode) {
379
+ const { props } = newResult as VNode;
380
+ updateElementProps(elementRef, props);
381
+ }
382
+ const fragment = renderToFragment(newResult, isCurrentVNode);
383
+ elementRef.textContent = '';
384
+ elementRef.appendChild(fragment);
385
+ dom.getElementCache().set(elementRef, true);
386
+ }
387
+ }
388
+ };
389
+
390
+ state.subscribe(() => {
391
+ rafId = scheduleRAFUpdate(rafId, () => {
392
+ updateElement();
393
+ rafId = null;
394
+ });
395
+ });
396
+
397
+ const refCallback = (el: HTMLElement | SVGElement) => {
398
+ elementRef = el;
399
+ if (initialIsNull && el.parentNode) {
400
+ placeholder = document.createComment('reactive');
401
+ el.parentNode.replaceChild(placeholder, el);
402
+ isInDOM = false;
403
+ }
404
+ };
405
+
406
+ if (isVNodeResult) {
407
+ const vnode = initialResult as VNode;
408
+ return {
409
+ tagName: vnode.tagName,
410
+ props: { ...vnode.props, ref: refCallback },
411
+ children: vnode.children
412
+ };
413
+ }
414
+
415
+ return { tagName: 'span', props: { ref: refCallback }, children: [initialResult] };
416
+ };
417
+
418
+ // Reactive element with custom wrapper tag
419
+ export const reactiveAs = <T>(
420
+ tagName: string,
421
+ state: State<T>,
422
+ renderFn: (value: T) => VNode | Child,
423
+ props: Props = {}
424
+ ): VNode => {
425
+ let rafId: number | null = null;
426
+ let elementRef: HTMLElement | SVGElement | null = null;
427
+
428
+ state.subscribe(() => {
429
+ rafId = scheduleRAFUpdate(rafId, () => {
430
+ if (elementRef) {
431
+ const newResult = renderFn(state.value);
432
+
433
+ if (newResult == null || newResult === false) {
434
+ (elementRef as HTMLElement).style.display = 'none';
435
+ elementRef.textContent = '';
436
+ } else {
437
+ (elementRef as HTMLElement).style.display = '';
438
+ const fragment = renderToFragment(newResult, false);
439
+ elementRef.textContent = '';
440
+ elementRef.appendChild(fragment);
441
+ }
442
+ dom.getElementCache().set(elementRef, true);
443
+ }
444
+ rafId = null;
445
+ });
446
+ });
447
+
448
+ const refCallback = (el: HTMLElement | SVGElement) => {
449
+ elementRef = el;
450
+ };
451
+
452
+ return { tagName, props: { ...props, ref: refCallback }, children: [renderFn(state.value)] };
453
+ };
454
+
455
+ export const text = (state: State<any> | any): VNode | string =>
456
+ (state && state.value !== undefined)
457
+ ? reactive(state, v => ({ tagName: 'span', props: {}, children: [String(v)] }))
458
+ : String(state);
459
+
460
+ export const bindValue = <T extends string | number>(state: State<T>): Props => ({
461
+ value: state.value,
462
+ oninput: (e: Event) => { state.value = (e.target as HTMLInputElement).value as T; }
463
+ });
464
+
465
+ export const bindChecked = (state: State<boolean>): Props => ({
466
+ checked: state.value,
467
+ onchange: (e: Event) => { state.value = (e.target as HTMLInputElement).checked; }
468
+ });