@epoch32/visage 0.0.0-stage → 0.0.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/LICENSE +373 -0
- package/README.md +110 -2
- package/dist/bundle.dev.js +4875 -0
- package/dist/index.js +9633 -0
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/dist/vite/index.js +707 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- package/src/vite/types.ts +65 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { effect, getActiveScope, onCleanup, runInScope, Scope, untrack } from "../signals";
|
|
2
|
+
import { type Child, insertChild, type ReactiveValue, resolveValue } from "./render";
|
|
3
|
+
|
|
4
|
+
export interface ShowProps<T> {
|
|
5
|
+
when: ReactiveValue<T | false | null | undefined>;
|
|
6
|
+
fallback?: Child;
|
|
7
|
+
children: Child | ((item: () => T) => Child);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Conditionally renders JSX children when the `when` condition evaluates to truthy,
|
|
12
|
+
* with fine-grained reactive scope lifecycle management and automatic fallback rendering.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const loggedIn = signal(false);
|
|
17
|
+
*
|
|
18
|
+
* <Show when={loggedIn} fallback={<div>Please log in</div>}>
|
|
19
|
+
* <Dashboard />
|
|
20
|
+
* </Show>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function Show<T>(props: ShowProps<T>): any {
|
|
24
|
+
if (typeof document === "undefined") {
|
|
25
|
+
const condition = resolveValue(props.when);
|
|
26
|
+
if (condition) {
|
|
27
|
+
return typeof props.children === "function" && props.children.length > 0
|
|
28
|
+
? (props.children as (item: () => T) => Child)(() => resolveValue(props.when) as T)
|
|
29
|
+
: typeof props.children === "function"
|
|
30
|
+
? (props.children as () => Child)()
|
|
31
|
+
: props.children;
|
|
32
|
+
}
|
|
33
|
+
return props.fallback ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const marker = document.createTextNode("");
|
|
37
|
+
const initialFrag = document.createDocumentFragment();
|
|
38
|
+
let currentScope: Scope | null = null;
|
|
39
|
+
let currentContent: Node[] = [];
|
|
40
|
+
let isMounted = false;
|
|
41
|
+
|
|
42
|
+
effect(() => {
|
|
43
|
+
const condition = resolveValue(props.when);
|
|
44
|
+
|
|
45
|
+
if (currentScope) {
|
|
46
|
+
currentScope.dispose();
|
|
47
|
+
currentScope = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Clean up old nodes from DOM
|
|
51
|
+
for (const node of currentContent) {
|
|
52
|
+
if (node.parentNode) {
|
|
53
|
+
node.parentNode.removeChild(node);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
currentContent = [];
|
|
57
|
+
|
|
58
|
+
const frag = document.createDocumentFragment();
|
|
59
|
+
|
|
60
|
+
if (condition) {
|
|
61
|
+
currentScope = new Scope(getActiveScope());
|
|
62
|
+
untrack(() => {
|
|
63
|
+
runInScope(currentScope, () => {
|
|
64
|
+
const childRes =
|
|
65
|
+
typeof props.children === "function" && props.children.length > 0
|
|
66
|
+
? (props.children as (item: () => T) => Child)(() => resolveValue(props.when) as T)
|
|
67
|
+
: typeof props.children === "function"
|
|
68
|
+
? (props.children as () => Child)()
|
|
69
|
+
: props.children;
|
|
70
|
+
|
|
71
|
+
insertChild(frag, childRes);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
} else if (props.fallback !== undefined) {
|
|
75
|
+
currentScope = new Scope(getActiveScope());
|
|
76
|
+
untrack(() => {
|
|
77
|
+
runInScope(currentScope, () => {
|
|
78
|
+
insertChild(frag, props.fallback);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
currentContent = Array.from(frag.childNodes);
|
|
84
|
+
|
|
85
|
+
if (marker.parentNode) {
|
|
86
|
+
marker.parentNode.insertBefore(frag, marker);
|
|
87
|
+
} else if (!isMounted) {
|
|
88
|
+
initialFrag.appendChild(frag);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
isMounted = true;
|
|
93
|
+
initialFrag.appendChild(marker);
|
|
94
|
+
|
|
95
|
+
onCleanup(() => {
|
|
96
|
+
if (currentScope) {
|
|
97
|
+
currentScope.dispose();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return initialFrag;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ForProps<T> {
|
|
105
|
+
each: ReactiveValue<readonly T[] | undefined | null>;
|
|
106
|
+
fallback?: Child;
|
|
107
|
+
children: (item: T, index: () => number) => Child;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface ItemRecord<T> {
|
|
111
|
+
item: T;
|
|
112
|
+
scope: Scope;
|
|
113
|
+
nodes: Node[];
|
|
114
|
+
indexSignal: (val?: number) => number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Keyed / referential list renderer with fine-grained item reconciliation.
|
|
119
|
+
*
|
|
120
|
+
* Items in the list are reused across array mutations without re-rendering existing items
|
|
121
|
+
* or tearing down their active reactive scopes.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```tsx
|
|
125
|
+
* const items = signal(["Apple", "Banana", "Cherry"]);
|
|
126
|
+
*
|
|
127
|
+
* <For each={items} fallback={<div>No items found</div>}>
|
|
128
|
+
* {(item, index) => (
|
|
129
|
+
* <li>
|
|
130
|
+
* {index() + 1}. {item}
|
|
131
|
+
* </li>
|
|
132
|
+
* )}
|
|
133
|
+
* </For>
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export function For<T>(props: ForProps<T>): any {
|
|
137
|
+
if (typeof document === "undefined") {
|
|
138
|
+
const list = resolveValue(props.each) ?? [];
|
|
139
|
+
if (list.length === 0) {
|
|
140
|
+
return props.fallback ?? null;
|
|
141
|
+
}
|
|
142
|
+
return list.map((item, idx) => props.children(item, () => idx));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const marker = document.createTextNode("");
|
|
146
|
+
const initialFrag = document.createDocumentFragment();
|
|
147
|
+
let items: ItemRecord<T>[] = [];
|
|
148
|
+
let fallbackScope: Scope | null = null;
|
|
149
|
+
let fallbackNodes: Node[] = [];
|
|
150
|
+
let isMounted = false;
|
|
151
|
+
|
|
152
|
+
effect(() => {
|
|
153
|
+
const list = resolveValue(props.each) ?? [];
|
|
154
|
+
const parent = marker.parentNode ?? (!isMounted ? initialFrag : null);
|
|
155
|
+
|
|
156
|
+
if (list.length === 0) {
|
|
157
|
+
// Clear items
|
|
158
|
+
for (const rec of items) {
|
|
159
|
+
rec.scope.dispose();
|
|
160
|
+
for (const node of rec.nodes) {
|
|
161
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
items = [];
|
|
165
|
+
|
|
166
|
+
// Render fallback
|
|
167
|
+
if (props.fallback !== undefined && fallbackNodes.length === 0 && parent) {
|
|
168
|
+
fallbackScope = new Scope(getActiveScope());
|
|
169
|
+
untrack(() => {
|
|
170
|
+
runInScope(fallbackScope, () => {
|
|
171
|
+
const frag = document.createDocumentFragment();
|
|
172
|
+
insertChild(frag, props.fallback);
|
|
173
|
+
fallbackNodes = Array.from(frag.childNodes);
|
|
174
|
+
if (marker.parentNode) {
|
|
175
|
+
marker.parentNode.insertBefore(frag, marker);
|
|
176
|
+
} else if (!isMounted) {
|
|
177
|
+
initialFrag.appendChild(frag);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Clean up fallback if present
|
|
186
|
+
if (fallbackNodes.length > 0) {
|
|
187
|
+
if (fallbackScope) {
|
|
188
|
+
fallbackScope.dispose();
|
|
189
|
+
fallbackScope = null;
|
|
190
|
+
}
|
|
191
|
+
for (const node of fallbackNodes) {
|
|
192
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
193
|
+
}
|
|
194
|
+
fallbackNodes = [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!parent) return;
|
|
198
|
+
|
|
199
|
+
const newItems: ItemRecord<T>[] = [];
|
|
200
|
+
const prevMap = new Map<T, ItemRecord<T>>();
|
|
201
|
+
|
|
202
|
+
for (const rec of items) {
|
|
203
|
+
prevMap.set(rec.item, rec);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Fast-path: map existing or create new item scopes
|
|
207
|
+
for (let i = 0; i < list.length; i++) {
|
|
208
|
+
const item = list[i]!;
|
|
209
|
+
const existing = prevMap.get(item);
|
|
210
|
+
|
|
211
|
+
if (existing) {
|
|
212
|
+
existing.indexSignal(i);
|
|
213
|
+
newItems.push(existing);
|
|
214
|
+
prevMap.delete(item);
|
|
215
|
+
} else {
|
|
216
|
+
let indexVal = i;
|
|
217
|
+
const indexSubscribers = new Set<() => void>();
|
|
218
|
+
const indexSignal = (...args: [number?]): number => {
|
|
219
|
+
if (args.length > 0 && args[0] !== undefined && args[0] !== indexVal) {
|
|
220
|
+
indexVal = args[0];
|
|
221
|
+
for (const sub of Array.from(indexSubscribers)) sub();
|
|
222
|
+
}
|
|
223
|
+
return indexVal;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const itemScope = new Scope(getActiveScope());
|
|
227
|
+
let itemNodes: Node[] = [];
|
|
228
|
+
|
|
229
|
+
untrack(() => {
|
|
230
|
+
runInScope(itemScope, () => {
|
|
231
|
+
const frag = document.createDocumentFragment();
|
|
232
|
+
const child = props.children(item, indexSignal);
|
|
233
|
+
insertChild(frag, child);
|
|
234
|
+
itemNodes = Array.from(frag.childNodes);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
newItems.push({
|
|
239
|
+
item,
|
|
240
|
+
scope: itemScope,
|
|
241
|
+
nodes: itemNodes,
|
|
242
|
+
indexSignal
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Clean up removed items
|
|
248
|
+
for (const [, removed] of prevMap) {
|
|
249
|
+
removed.scope.dispose();
|
|
250
|
+
for (const node of removed.nodes) {
|
|
251
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Reorder / insert DOM nodes
|
|
256
|
+
if (marker.parentNode) {
|
|
257
|
+
let currentMarker: Node = marker;
|
|
258
|
+
for (let i = newItems.length - 1; i >= 0; i--) {
|
|
259
|
+
const rec = newItems[i]!;
|
|
260
|
+
for (let j = rec.nodes.length - 1; j >= 0; j--) {
|
|
261
|
+
const node = rec.nodes[j]!;
|
|
262
|
+
if (node.nextSibling !== currentMarker) {
|
|
263
|
+
marker.parentNode.insertBefore(node, currentMarker);
|
|
264
|
+
}
|
|
265
|
+
currentMarker = node;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} else if (!isMounted) {
|
|
269
|
+
for (const rec of newItems) {
|
|
270
|
+
for (const node of rec.nodes) {
|
|
271
|
+
initialFrag.appendChild(node);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
items = newItems;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
isMounted = true;
|
|
280
|
+
initialFrag.appendChild(marker);
|
|
281
|
+
|
|
282
|
+
onCleanup(() => {
|
|
283
|
+
for (const rec of items) {
|
|
284
|
+
rec.scope.dispose();
|
|
285
|
+
}
|
|
286
|
+
if (fallbackScope) {
|
|
287
|
+
fallbackScope.dispose();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return initialFrag;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface PortalProps {
|
|
295
|
+
mount?: HTMLElement | null;
|
|
296
|
+
children: Child | (() => Child);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Portal renders children into a different DOM container while preserving reactive context
|
|
301
|
+
*/
|
|
302
|
+
export function Portal(props: PortalProps): any {
|
|
303
|
+
if (typeof document === "undefined") {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const marker = document.createTextNode("");
|
|
308
|
+
let currentScope: Scope | null = null;
|
|
309
|
+
let nodes: Node[] = [];
|
|
310
|
+
|
|
311
|
+
effect(() => {
|
|
312
|
+
const target = props.mount ?? (typeof document !== "undefined" ? document.body : null);
|
|
313
|
+
if (!target) return;
|
|
314
|
+
|
|
315
|
+
if (currentScope) {
|
|
316
|
+
currentScope.dispose();
|
|
317
|
+
currentScope = null;
|
|
318
|
+
}
|
|
319
|
+
for (const node of nodes) {
|
|
320
|
+
if (node.parentNode) {
|
|
321
|
+
node.parentNode.removeChild(node);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
nodes = [];
|
|
325
|
+
|
|
326
|
+
currentScope = new Scope(getActiveScope());
|
|
327
|
+
untrack(() => {
|
|
328
|
+
runInScope(currentScope, () => {
|
|
329
|
+
const childRes =
|
|
330
|
+
typeof props.children === "function" ? (props.children as () => Child)() : props.children;
|
|
331
|
+
const frag = document.createDocumentFragment();
|
|
332
|
+
insertChild(frag, childRes);
|
|
333
|
+
nodes = Array.from(frag.childNodes);
|
|
334
|
+
target.appendChild(frag);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
onCleanup(() => {
|
|
340
|
+
if (currentScope) {
|
|
341
|
+
currentScope.dispose();
|
|
342
|
+
currentScope = null;
|
|
343
|
+
}
|
|
344
|
+
for (const node of nodes) {
|
|
345
|
+
if (node.parentNode) {
|
|
346
|
+
node.parentNode.removeChild(node);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
nodes = [];
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return marker;
|
|
353
|
+
}
|
package/src/dom/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type Component,
|
|
3
|
+
createResource,
|
|
4
|
+
ErrorBoundary,
|
|
5
|
+
type ErrorBoundaryProps,
|
|
6
|
+
type LazyComponent,
|
|
7
|
+
lazy,
|
|
8
|
+
type Resource,
|
|
9
|
+
type ResourceActions,
|
|
10
|
+
Suspense,
|
|
11
|
+
SuspenseContext,
|
|
12
|
+
type SuspenseContextValue,
|
|
13
|
+
type SuspenseProps
|
|
14
|
+
} from "./async";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
For,
|
|
18
|
+
type ForProps,
|
|
19
|
+
Portal,
|
|
20
|
+
type PortalProps,
|
|
21
|
+
Show,
|
|
22
|
+
type ShowProps
|
|
23
|
+
} from "./control-flow";
|
|
24
|
+
export {
|
|
25
|
+
type Child,
|
|
26
|
+
insertChild,
|
|
27
|
+
isReactive,
|
|
28
|
+
type ReactiveValue,
|
|
29
|
+
render,
|
|
30
|
+
resolveValue,
|
|
31
|
+
setAttribute
|
|
32
|
+
} from "./render";
|
package/src/dom/lazy.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useContext } from "../signals/context";
|
|
2
|
+
import type { Child } from "./render";
|
|
3
|
+
import { createResource, SuspenseContext } from "./resource";
|
|
4
|
+
|
|
5
|
+
export type Component<P = any> = (props: P) => Child;
|
|
6
|
+
|
|
7
|
+
export type LazyComponent<T extends Component<any>> = T & {
|
|
8
|
+
preload(): Promise<T>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Lazy load an asynchronous component with automatic `<Suspense>` boundary coordination and preloading.
|
|
13
|
+
*
|
|
14
|
+
* Dynamically imports the component module upon first render or when `.preload()` is invoked.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* // Dynamic code-split component
|
|
19
|
+
* const HeavyChart = lazy(() => import("./HeavyChart"));
|
|
20
|
+
*
|
|
21
|
+
* // Optional early prefetching
|
|
22
|
+
* HeavyChart.preload();
|
|
23
|
+
*
|
|
24
|
+
* // Render inside Suspense
|
|
25
|
+
* <Suspense fallback={<div>Loading chart...</div>}>
|
|
26
|
+
* <HeavyChart data={chartData()} />
|
|
27
|
+
* </Suspense>
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @param importer - Dynamic import function returning a component or `{ default: Component }`
|
|
31
|
+
* @returns A lazy component with a `.preload()` method
|
|
32
|
+
*/
|
|
33
|
+
export function lazy<T extends Component<any>>(
|
|
34
|
+
importer: () => Promise<{ default: T } | T>
|
|
35
|
+
): LazyComponent<T> {
|
|
36
|
+
let loadedComponent: T | null = null;
|
|
37
|
+
let loadingPromise: Promise<T> | null = null;
|
|
38
|
+
|
|
39
|
+
const preload = (): Promise<T> => {
|
|
40
|
+
if (loadedComponent) {
|
|
41
|
+
return Promise.resolve(loadedComponent);
|
|
42
|
+
}
|
|
43
|
+
if (!loadingPromise) {
|
|
44
|
+
loadingPromise = importer()
|
|
45
|
+
.then((mod) => {
|
|
46
|
+
const comp =
|
|
47
|
+
mod && typeof mod === "object" && "default" in mod
|
|
48
|
+
? (mod as { default: T }).default
|
|
49
|
+
: (mod as T);
|
|
50
|
+
loadedComponent = comp;
|
|
51
|
+
return comp;
|
|
52
|
+
})
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
loadingPromise = null;
|
|
55
|
+
throw err;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return loadingPromise;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const LazyWrapper = ((props: any) => {
|
|
62
|
+
if (loadedComponent) {
|
|
63
|
+
return loadedComponent(props);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const promise = preload();
|
|
67
|
+
const suspense = useContext(SuspenseContext);
|
|
68
|
+
if (suspense) {
|
|
69
|
+
suspense.registerPromise(promise);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const res = createResource(() => promise);
|
|
73
|
+
return () => {
|
|
74
|
+
const Comp = res();
|
|
75
|
+
return Comp ? Comp(props) : null;
|
|
76
|
+
};
|
|
77
|
+
}) as LazyComponent<T>;
|
|
78
|
+
|
|
79
|
+
LazyWrapper.preload = preload;
|
|
80
|
+
|
|
81
|
+
return LazyWrapper;
|
|
82
|
+
}
|