@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/dist/index.js +1193 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
- package/src/component.ts +26 -0
- package/src/error.ts +144 -0
- package/src/index.ts +95 -0
- package/src/render.ts +286 -0
- package/src/signal.ts +213 -0
- package/src/ssr.ts +341 -0
- package/src/stream.ts +432 -0
- package/src/utils.ts +116 -0
- package/src/virtual-list.ts +405 -0
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
|
+
}
|