@qorejs/qore 0.6.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/src/render.ts ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Qore Renderer - Fine-grained DOM Updates
3
+ * No VDOM, No Diff - Direct signal binding
4
+ *
5
+ * Extended with Server-Side Streaming Support
6
+ */
7
+
8
+ import { effect, signal } from './signal';
9
+ import { StreamRenderer } from './stream';
10
+
11
+ export type VNode = string | number | Node | Component | VNode[];
12
+ export type Component = () => VNode;
13
+
14
+ /**
15
+ * Portal - Render children to a different DOM node
16
+ */
17
+ export function Portal({ children, target }: { children: VNode; target: HTMLElement | string }): null {
18
+ const container = typeof target === 'string' ? document.querySelector(target) : target;
19
+
20
+ if (!container) {
21
+ console.warn('Portal target not found:', String(target));
22
+ return null;
23
+ }
24
+
25
+ const renderToPortal = () => {
26
+ container.innerHTML = '';
27
+ const vnode = typeof children === 'function' ? children() : children;
28
+
29
+ if (Array.isArray(vnode)) {
30
+ vnode.forEach(node => {
31
+ if (node instanceof Node) {
32
+ container.appendChild(node);
33
+ } else if (typeof node === 'string' || typeof node === 'number') {
34
+ container.appendChild(document.createTextNode(String(node)));
35
+ }
36
+ });
37
+ } else if (vnode instanceof Node) {
38
+ container.appendChild(vnode);
39
+ } else if (vnode != null) {
40
+ container.appendChild(document.createTextNode(String(vnode)));
41
+ }
42
+ };
43
+
44
+ if (typeof children === 'function') {
45
+ effect(renderToPortal);
46
+ } else {
47
+ renderToPortal();
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ export function h(
54
+ type: string | Component,
55
+ props: Record<string, any> | null = null,
56
+ ...children: any[]
57
+ ): VNode {
58
+ if (typeof type === 'function') {
59
+ // Component function - pass props and children
60
+ return type({ ...props, children: children.length > 0 ? children.flat() : undefined });
61
+ }
62
+
63
+ const el = document.createElement(type);
64
+
65
+ if (props) {
66
+ for (const [key, value] of Object.entries(props)) {
67
+ if (key.startsWith('on') && typeof value === 'function') {
68
+ el.addEventListener(key.slice(2).toLowerCase(), value);
69
+ } else if (key === 'className') {
70
+ el.className = value;
71
+ } else if (key === 'style' && typeof value === 'object') {
72
+ Object.assign(el.style, value);
73
+ } else if (key === 'ref' && typeof value === 'function') {
74
+ value(el);
75
+ } else if (typeof value !== 'function') {
76
+ el.setAttribute(key, value);
77
+ }
78
+ }
79
+ }
80
+
81
+ const flatChildren = children.flat(Infinity);
82
+ for (const child of flatChildren) {
83
+ if (child != null) {
84
+ if (typeof child === 'string' || typeof child === 'number') {
85
+ el.appendChild(document.createTextNode(String(child)));
86
+ } else if (child instanceof Node) {
87
+ el.appendChild(child);
88
+ }
89
+ }
90
+ }
91
+
92
+ return el;
93
+ }
94
+
95
+ export function text(signalOrValue: (() => string | number) | string | number): Text {
96
+ const node = document.createTextNode('');
97
+
98
+ if (typeof signalOrValue === 'function') {
99
+ effect(() => {
100
+ node.textContent = String(signalOrValue());
101
+ });
102
+ } else {
103
+ node.textContent = String(signalOrValue);
104
+ }
105
+
106
+ return node;
107
+ }
108
+
109
+ export function render(root: HTMLElement, fn: () => VNode): () => void {
110
+ let cleanup: (() => void) | undefined;
111
+
112
+ const run = (): void => {
113
+ cleanup?.();
114
+ root.innerHTML = '';
115
+ const vnode = fn();
116
+
117
+ if (vnode instanceof Node) {
118
+ root.appendChild(vnode);
119
+ } else {
120
+ root.appendChild(document.createTextNode(String(vnode)));
121
+ }
122
+ };
123
+
124
+ const stop = effect(run);
125
+ cleanup = stop;
126
+ return stop;
127
+ }
128
+
129
+ export function show<T>(condition: () => boolean, fn: () => T): T | null {
130
+ return condition() ? fn() : null;
131
+ }
132
+
133
+ export function For<T, U>(
134
+ items: () => T[],
135
+ fn: (item: T, index: () => number) => U
136
+ ): U[] {
137
+ const list = items();
138
+ return list.map((item, i) => fn(item, () => i));
139
+ }
140
+
141
+ export const Fragment = ({ children }: { children: any[] }) => children;
142
+
143
+ // Common tag helpers
144
+ const tag = (name: string) => (props: any = null, ...children: any[]) => h(name, props, ...children);
145
+
146
+ export const div = tag('div');
147
+ export const span = tag('span');
148
+ export const button = tag('button');
149
+ export const input = tag('input');
150
+ export const p = tag('p');
151
+ export const h1 = tag('h1');
152
+ export const h2 = tag('h2');
153
+ export const h3 = tag('h3');
154
+
155
+ // ============== Server-Side Rendering ==============
156
+
157
+ /**
158
+ * Convert VNode to HTML string (SSR)
159
+ * Pure string concatenation, no DOM API dependency, runs in Node.js environment
160
+ */
161
+ export function renderToString(vnode: VNode): string {
162
+ if (vnode == null) return '';
163
+
164
+ if (typeof vnode === 'string' || typeof vnode === 'number') {
165
+ return String(vnode);
166
+ }
167
+
168
+ if (Array.isArray(vnode)) {
169
+ return vnode.map(v => renderToString(v)).join('');
170
+ }
171
+
172
+ if (typeof vnode === 'function') {
173
+ // Component function
174
+ return renderToString(vnode());
175
+ }
176
+
177
+ // Handle DOM nodes (in jsdom/browser environment)
178
+ // Use nodeType detection instead of instanceof to avoid environment dependency
179
+ if (typeof vnode === 'object' && 'nodeType' in vnode) {
180
+ const node = vnode as unknown as { nodeType: number; outerHTML?: string; textContent?: string };
181
+ // Element node
182
+ if (node.nodeType === 1) {
183
+ return node.outerHTML || '';
184
+ }
185
+ // Text/Comment node
186
+ return node.textContent || '';
187
+ }
188
+
189
+ // Other object types
190
+ if (typeof vnode === 'object') {
191
+ return '';
192
+ }
193
+
194
+ return String(vnode);
195
+ }
196
+
197
+ /**
198
+ * Render component to HTML string (SSR)
199
+ */
200
+ export function renderComponentToString(component: Component): string {
201
+ return renderToString(component());
202
+ }
203
+
204
+ /**
205
+ * Stream render to StreamRenderer
206
+ * Support chunked output for large components
207
+ */
208
+ export function renderToStream(
209
+ root: StreamRenderer,
210
+ fn: () => VNode,
211
+ options?: { chunkSize?: number; onChunk?: (chunk: string) => void }
212
+ ): { abort: () => void } {
213
+ const { chunkSize = 1000, onChunk } = options || {};
214
+ let aborted = false;
215
+
216
+ const processChunk = (html: string) => {
217
+ if (aborted) return;
218
+ root.write(html);
219
+ onChunk?.(html);
220
+ };
221
+
222
+ // Process large content in chunks
223
+ const vnode = fn();
224
+ const html = renderToString(vnode);
225
+
226
+ if (html.length <= chunkSize) {
227
+ processChunk(html);
228
+ } else {
229
+ // Output in chunks
230
+ for (let i = 0; i < html.length; i += chunkSize) {
231
+ if (aborted) break;
232
+ processChunk(html.slice(i, i + chunkSize));
233
+ }
234
+ }
235
+
236
+ return {
237
+ abort: () => { aborted = true; }
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Async VNode parsing
243
+ * Support components returning Promise
244
+ */
245
+ export async function renderAsync(vnode: VNode | Promise<VNode>): Promise<string> {
246
+ const resolved = await vnode;
247
+
248
+ if (resolved == null) return '';
249
+
250
+ if (typeof resolved === 'string' || typeof resolved === 'number') {
251
+ return String(resolved);
252
+ }
253
+
254
+ if (Array.isArray(resolved)) {
255
+ const results = await Promise.all(resolved.map(v => renderAsync(v)));
256
+ return results.join('');
257
+ }
258
+
259
+ if (resolved instanceof Node) {
260
+ return resolved instanceof Element ? resolved.outerHTML : resolved.textContent || '';
261
+ }
262
+
263
+ if (typeof resolved === 'function') {
264
+ return renderAsync(resolved());
265
+ }
266
+
267
+ return '';
268
+ }
269
+
270
+ /**
271
+ * Async stream rendering
272
+ * Support streaming output for async components
273
+ */
274
+ export async function renderToStreamAsync(
275
+ renderer: StreamRenderer,
276
+ fn: () => VNode | Promise<VNode>
277
+ ): Promise<void> {
278
+ try {
279
+ const vnode = await fn();
280
+ const html = await renderAsync(vnode);
281
+ renderer.write(html);
282
+ renderer.end();
283
+ } catch (err) {
284
+ renderer.fail(err as Error);
285
+ }
286
+ }
package/src/signal.ts ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Qore Signal System - Fine-grained Reactivity
3
+ */
4
+
5
+ type EffectFn = () => void | (() => void);
6
+
7
+ let activeEffect: EffectNode | null = null;
8
+ let batchDepth = 0;
9
+ const pendingEffects = new Set<EffectNode>();
10
+
11
+ class EffectNode {
12
+ deps = new Set<SignalNode<any>>();
13
+ fn: EffectFn;
14
+ cleanup?: () => void;
15
+
16
+ constructor(fn: EffectFn) {
17
+ this.fn = fn;
18
+ }
19
+
20
+ run(): void {
21
+ // Clean up old dependencies
22
+ for (const dep of this.deps) {
23
+ dep.subs.delete(this);
24
+ }
25
+ this.deps.clear();
26
+
27
+ // Call cleanup before execution
28
+ if (this.cleanup) {
29
+ this.cleanup();
30
+ this.cleanup = undefined;
31
+ }
32
+
33
+ const prevEffect = activeEffect;
34
+ activeEffect = this;
35
+ try {
36
+ const result = this.fn();
37
+ // Save new cleanup function
38
+ if (typeof result === 'function') {
39
+ this.cleanup = result;
40
+ }
41
+ } finally {
42
+ activeEffect = prevEffect;
43
+ }
44
+ }
45
+ }
46
+
47
+ class SignalNode<T> {
48
+ private value: T;
49
+ subs = new Set<EffectNode>();
50
+
51
+ constructor(initial: T) {
52
+ this.value = initial;
53
+ }
54
+
55
+ get(): T {
56
+ if (activeEffect) {
57
+ this.subs.add(activeEffect);
58
+ activeEffect.deps.add(this);
59
+ }
60
+ return this.value;
61
+ }
62
+
63
+ set(newValue: T): void {
64
+ if (this.value === newValue) return;
65
+ this.value = newValue;
66
+ this.notify();
67
+ }
68
+
69
+ private notify(): void {
70
+ const effectsToRun = Array.from(this.subs);
71
+
72
+ if (batchDepth > 0) {
73
+ effectsToRun.forEach(sub => pendingEffects.add(sub));
74
+ return;
75
+ }
76
+
77
+ for (const effect of effectsToRun) {
78
+ effect.run();
79
+ }
80
+ }
81
+ }
82
+
83
+ export interface Signal<T> {
84
+ (value?: T): T;
85
+ peek(): T;
86
+ }
87
+
88
+ export function signal<T>(initial: T): Signal<T> {
89
+ const node = new SignalNode(initial);
90
+
91
+ const sig = (value?: T): T => {
92
+ if (value !== undefined) {
93
+ node.set(value);
94
+ return value;
95
+ }
96
+ return node.get();
97
+ };
98
+
99
+ sig.peek = () => {
100
+ const prev = activeEffect;
101
+ activeEffect = null; // Temporarily disable dependency tracking
102
+ try {
103
+ return node.get();
104
+ } finally {
105
+ activeEffect = prev;
106
+ }
107
+ };
108
+ return sig;
109
+ }
110
+
111
+ export function computed<T>(fn: () => T): Signal<T> {
112
+ let value: T;
113
+ let depsVersion = 0;
114
+ let lastReadVersion = 0;
115
+ const subs = new Set<EffectNode>();
116
+
117
+ // Create effect node to track dependencies
118
+ const effectNode = new EffectNode(() => {
119
+ depsVersion++;
120
+ // Notify all subscribers when dependencies change
121
+ for (const sub of subs) {
122
+ if (batchDepth > 0) {
123
+ pendingEffects.add(sub);
124
+ } else {
125
+ sub.run();
126
+ }
127
+ }
128
+ });
129
+
130
+ // Initial dependency collection - must be after creating effectNode
131
+ const prevEffect = activeEffect;
132
+ activeEffect = effectNode;
133
+ try {
134
+ value = fn();
135
+ } finally {
136
+ activeEffect = prevEffect;
137
+ }
138
+ lastReadVersion = depsVersion;
139
+
140
+ const sig = (val?: T): T => {
141
+ if (val !== undefined) {
142
+ throw new Error('Computed signals are read-only');
143
+ }
144
+
145
+ // Let current effect subscribe to this computed
146
+ if (activeEffect) {
147
+ subs.add(activeEffect);
148
+ }
149
+
150
+ // Recalculate if dependencies have changed
151
+ if (depsVersion > lastReadVersion) {
152
+ const prev = activeEffect;
153
+ activeEffect = effectNode;
154
+ try {
155
+ value = fn();
156
+ } finally {
157
+ activeEffect = prev;
158
+ }
159
+ lastReadVersion = depsVersion;
160
+ }
161
+
162
+ return value;
163
+ };
164
+
165
+ sig.peek = () => {
166
+ if (depsVersion > lastReadVersion) {
167
+ const prev = activeEffect;
168
+ activeEffect = null; // Temporarily disable dependency tracking
169
+ try {
170
+ value = fn();
171
+ } finally {
172
+ activeEffect = prev;
173
+ }
174
+ lastReadVersion = depsVersion;
175
+ }
176
+ return value;
177
+ };
178
+
179
+ return sig;
180
+ }
181
+
182
+ export function effect(fn: EffectFn): () => void {
183
+ const node = new EffectNode(fn);
184
+ node.run();
185
+ return () => {
186
+ // Call cleanup
187
+ if (node.cleanup) {
188
+ node.cleanup();
189
+ node.cleanup = undefined;
190
+ }
191
+ // Clean up dependencies
192
+ for (const dep of node.deps) {
193
+ dep.subs.delete(node);
194
+ }
195
+ node.deps.clear();
196
+ };
197
+ }
198
+
199
+ export function batch<T>(fn: () => T): T {
200
+ batchDepth++;
201
+ try {
202
+ return fn();
203
+ } finally {
204
+ batchDepth--;
205
+ if (batchDepth === 0) {
206
+ const effects = Array.from(pendingEffects);
207
+ pendingEffects.clear();
208
+ for (const eff of effects) {
209
+ eff.run();
210
+ }
211
+ }
212
+ }
213
+ }