semajsx 0.5.1 → 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/client-CButR91p.mjs +740 -0
- package/dist/client-CButR91p.mjs.map +1 -0
- package/dist/dom/index.d.mts +2 -2
- package/dist/dom/jsx-dev-runtime.d.mts +3 -3
- package/dist/dom/jsx-runtime.d.mts +3 -3
- package/dist/{helpers-CfRDJgcP.d.mts → helpers-C8GKdDrJ.d.mts} +3 -3
- package/dist/{helpers-CfRDJgcP.d.mts.map → helpers-C8GKdDrJ.d.mts.map} +1 -1
- package/dist/{index-Ch9GwToI.d.mts → index-D_FIlSk3.d.mts} +3 -3
- package/dist/{index-Ch9GwToI.d.mts.map → index-D_FIlSk3.d.mts.map} +1 -1
- package/dist/{index-B1pjI-Su.d.mts → index-PYr1aNIz.d.mts} +2 -2
- package/dist/{index-B1pjI-Su.d.mts.map → index-PYr1aNIz.d.mts.map} +1 -1
- package/dist/index.d.mts +6 -6
- package/dist/{island-marker-BJIO07Vj.d.mts → island-marker-Dne5tuWe.d.mts} +1 -1
- package/dist/island-marker-Dne5tuWe.d.mts.map +1 -0
- package/dist/{jsx-fNlLjLou.d.mts → jsx-CFnuxPMI.d.mts} +2 -2
- package/dist/{jsx-fNlLjLou.d.mts.map → jsx-CFnuxPMI.d.mts.map} +1 -1
- package/dist/{jsx-runtime-BFuFPDzn.d.mts → jsx-runtime-Dc77fsnM.d.mts} +3 -3
- package/dist/{jsx-runtime-BFuFPDzn.d.mts.map → jsx-runtime-Dc77fsnM.d.mts.map} +1 -1
- package/dist/{jsx-runtime-BBi9E0Hz.d.mts → jsx-runtime-tIuFmhTh.d.mts} +4 -4
- package/dist/{jsx-runtime-BBi9E0Hz.d.mts.map → jsx-runtime-tIuFmhTh.d.mts.map} +1 -1
- package/dist/{lucide-CVtHepGM.mjs → lucide-C5BghhSl.mjs} +1 -1
- package/dist/{lucide-CVtHepGM.mjs.map → lucide-C5BghhSl.mjs.map} +1 -1
- package/dist/{resource-BQI6AeJ0.d.mts → resource-CNwiNxJX.d.mts} +2 -2
- package/dist/{resource-BQI6AeJ0.d.mts.map → resource-CNwiNxJX.d.mts.map} +1 -1
- package/dist/signal/index.d.mts +2 -2
- package/dist/{signal-BwxUlXKs.d.mts → signal-BcaF-fWG.d.mts} +1 -1
- package/dist/{signal-BwxUlXKs.d.mts.map → signal-BcaF-fWG.d.mts.map} +1 -1
- package/dist/{src-L88LbwEv.mjs → src-75qcxwT_.mjs} +2 -2
- package/dist/{src-L88LbwEv.mjs.map → src-75qcxwT_.mjs.map} +1 -1
- package/dist/{src-DuSN6go_.mjs → src-B4VBiHa8.mjs} +116 -4
- package/dist/src-B4VBiHa8.mjs.map +1 -0
- package/dist/ssg/index.d.mts +2 -2
- package/dist/ssg/index.mjs +2 -2
- package/dist/ssg/plugins/docs-theme.d.mts +7 -4
- package/dist/ssg/plugins/docs-theme.d.mts.map +1 -1
- package/dist/ssg/plugins/docs-theme.mjs +172 -47
- package/dist/ssg/plugins/docs-theme.mjs.map +1 -1
- package/dist/ssg/plugins/lucide.d.mts +2 -2
- package/dist/ssg/plugins/lucide.mjs +1 -1
- package/dist/ssr/client.d.mts +7 -6
- package/dist/ssr/client.d.mts.map +1 -1
- package/dist/ssr/client.mjs +4 -682
- package/dist/ssr/index.d.mts +2 -2
- package/dist/ssr/index.d.mts.map +1 -1
- package/dist/ssr/index.mjs +1 -1
- package/dist/style/index.d.mts +2 -2
- package/dist/style/react.d.mts +2 -2
- package/dist/style/vue.d.mts +2 -2
- package/dist/terminal/index.d.mts +4 -4
- package/dist/terminal/jsx-dev-runtime.d.mts +4 -4
- package/dist/terminal/jsx-runtime.d.mts +4 -4
- package/dist/{types-D0jRO840.d.mts → types-Bj5q5x2Q.d.mts} +1 -1
- package/dist/{types-D0jRO840.d.mts.map → types-Bj5q5x2Q.d.mts.map} +1 -1
- package/dist/{types-C9fiRu6l.d.mts → types-BmDIxXiP.d.mts} +2 -2
- package/dist/{types-C9fiRu6l.d.mts.map → types-BmDIxXiP.d.mts.map} +1 -1
- package/dist/{types-CZMcXQTW.d.mts → types-C83YtOen.d.mts} +2 -2
- package/dist/{types-CZMcXQTW.d.mts.map → types-C83YtOen.d.mts.map} +1 -1
- package/dist/{types-BlaUrkq0.d.mts → types-CVPg8ByY.d.mts} +2 -2
- package/dist/{types-BlaUrkq0.d.mts.map → types-CVPg8ByY.d.mts.map} +1 -1
- package/dist/{types-DucvOZQ2.d.mts → types-ii0bAipe.d.mts} +2 -2
- package/dist/{types-DucvOZQ2.d.mts.map → types-ii0bAipe.d.mts.map} +1 -1
- package/package.json +7 -1
- package/dist/island-marker-BJIO07Vj.d.mts.map +0 -1
- package/dist/src-DuSN6go_.mjs.map +0 -1
- package/dist/ssr/client.mjs.map +0 -1
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { t as isSignal } from "./utils-DbTAs943.mjs";
|
|
2
|
+
import { h, v as Fragment } from "./src-DW3tIczg.mjs";
|
|
3
|
+
import { d as setProperty, u as render } from "./src-BqX3sryB.mjs";
|
|
4
|
+
|
|
5
|
+
//#region ../ssr/src/client/hydrate.ts
|
|
6
|
+
/**
|
|
7
|
+
* Type guard for async iterators
|
|
8
|
+
*/
|
|
9
|
+
function isAsyncIterator(value) {
|
|
10
|
+
if (!value || typeof value !== "object") return false;
|
|
11
|
+
const obj = value;
|
|
12
|
+
return typeof obj[Symbol.asyncIterator] === "function" || typeof obj.next === "function" && typeof obj.return === "function";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Hydrate a server-rendered DOM tree with client-side interactivity
|
|
16
|
+
* Unlike render(), this preserves existing DOM and only attaches event listeners
|
|
17
|
+
*
|
|
18
|
+
* @param vnode - The VNode to hydrate
|
|
19
|
+
* @param container - The DOM container with server-rendered content
|
|
20
|
+
* @returns The hydrated root node
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const vnode = <Counter initial={5} />
|
|
25
|
+
* const container = document.querySelector('[data-island-id="island-0"]')
|
|
26
|
+
* hydrate(vnode, container)
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function hydrate(vnode, container) {
|
|
30
|
+
const nodeToHydrate = container.firstChild;
|
|
31
|
+
if (!nodeToHydrate) {
|
|
32
|
+
console.warn("[Hydrate] Container is empty, falling back to render");
|
|
33
|
+
const rendered = renderNode(vnode, container);
|
|
34
|
+
if (rendered) container.appendChild(rendered);
|
|
35
|
+
return rendered;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
hydrateNode(vnode, nodeToHydrate, container);
|
|
39
|
+
return nodeToHydrate;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("[Hydrate] Error during hydration:", error);
|
|
42
|
+
console.warn("[Hydrate] Falling back to client-side rendering");
|
|
43
|
+
container.innerHTML = "";
|
|
44
|
+
return renderNode(vnode, container);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Hydrate a VNode onto an existing DOM node
|
|
49
|
+
*/
|
|
50
|
+
function hydrateNode(vnode, domNode, parentElement) {
|
|
51
|
+
if (vnode == null) return;
|
|
52
|
+
if (isSignal(vnode)) {
|
|
53
|
+
hydrateSignalNode(vnode, domNode, parentElement);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (typeof vnode === "string" || typeof vnode === "number") {
|
|
57
|
+
if (domNode.nodeType === Node.TEXT_NODE) {
|
|
58
|
+
const expectedText = String(vnode);
|
|
59
|
+
if (domNode.textContent !== expectedText) {
|
|
60
|
+
console.warn("[Hydrate] Text mismatch, updating:", domNode.textContent, "->", expectedText);
|
|
61
|
+
domNode.textContent = expectedText;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(vnode)) {
|
|
67
|
+
let currentDomNode = domNode;
|
|
68
|
+
for (const child of vnode) if (currentDomNode) {
|
|
69
|
+
hydrateNode(child, currentDomNode, parentElement);
|
|
70
|
+
currentDomNode = currentDomNode.nextSibling;
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (typeof vnode !== "object" || !("type" in vnode)) return;
|
|
75
|
+
const vnodeTyped = vnode;
|
|
76
|
+
if (vnodeTyped.type === "#signal") {
|
|
77
|
+
const signal = vnodeTyped.props?.signal;
|
|
78
|
+
if (signal && isSignal(signal)) hydrateSignalNode(signal, domNode, parentElement);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (vnodeTyped.type === Fragment) {
|
|
82
|
+
let currentDomNode = domNode;
|
|
83
|
+
for (const child of vnodeTyped.children) if (currentDomNode) {
|
|
84
|
+
hydrateNode(child, currentDomNode, parentElement);
|
|
85
|
+
currentDomNode = currentDomNode.nextSibling;
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (typeof vnodeTyped.type === "function") {
|
|
90
|
+
const props = vnodeTyped.children && vnodeTyped.children.length > 0 ? {
|
|
91
|
+
...vnodeTyped.props,
|
|
92
|
+
children: vnodeTyped.children
|
|
93
|
+
} : vnodeTyped.props || {};
|
|
94
|
+
let result = vnodeTyped.type(props);
|
|
95
|
+
if (result instanceof Promise) {
|
|
96
|
+
result.then((resolved) => hydrateNode(resolved, domNode, parentElement));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (isAsyncIterator(result)) {
|
|
100
|
+
result.next().then(({ value }) => {
|
|
101
|
+
hydrateNode(value, domNode, parentElement);
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
hydrateNode(result, domNode, parentElement);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (typeof vnodeTyped.type === "string") {
|
|
109
|
+
if (domNode.nodeType === Node.TEXT_NODE) return;
|
|
110
|
+
if (domNode.nodeType !== Node.ELEMENT_NODE) {
|
|
111
|
+
console.warn("[Hydrate] Expected element, got:", domNode.nodeType);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const element = domNode;
|
|
115
|
+
if (element.tagName.toLowerCase() !== vnodeTyped.type.toLowerCase()) {
|
|
116
|
+
console.warn("[Hydrate] Tag mismatch:", element.tagName, "vs", vnodeTyped.type);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
hydrateProperties(element, vnodeTyped.props || {});
|
|
120
|
+
hydrateChildren(element, vnodeTyped.children);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Hydrate properties onto an element
|
|
126
|
+
* This is where we attach event listeners and set up reactive properties
|
|
127
|
+
*/
|
|
128
|
+
function hydrateProperties(element, props) {
|
|
129
|
+
for (const [key, value] of Object.entries(props)) {
|
|
130
|
+
if (key === "children" || key === "key" || key === "ref") continue;
|
|
131
|
+
if (key.startsWith("on")) {
|
|
132
|
+
const eventName = key.slice(2).toLowerCase();
|
|
133
|
+
if (typeof value === "function") element.addEventListener(eventName, value);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (isSignal(value)) {
|
|
137
|
+
setProperty(element, key, value.value);
|
|
138
|
+
value.subscribe((newValue) => {
|
|
139
|
+
setProperty(element, key, newValue);
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (props.ref) {
|
|
145
|
+
if (typeof props.ref === "function") props.ref(element);
|
|
146
|
+
else if (typeof props.ref === "object" && props.ref !== null) props.ref.current = element;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Hydrate children elements
|
|
151
|
+
*/
|
|
152
|
+
function hydrateChildren(element, children) {
|
|
153
|
+
let currentDomNode = element.firstChild;
|
|
154
|
+
for (const child of children) {
|
|
155
|
+
if (!currentDomNode) {
|
|
156
|
+
console.warn("[Hydrate] Missing DOM node for child, appending");
|
|
157
|
+
const newNode = renderNode(child, element);
|
|
158
|
+
if (newNode) element.appendChild(newNode);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
hydrateNode(child, currentDomNode, element);
|
|
162
|
+
currentDomNode = currentDomNode.nextSibling;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Hydrate a signal VNode
|
|
167
|
+
* Set up reactivity to replace content when signal changes
|
|
168
|
+
*/
|
|
169
|
+
function hydrateSignalNode(signal, domNode, parentElement) {
|
|
170
|
+
const currentValue = signal.value;
|
|
171
|
+
if (currentValue == null || currentValue === false || Array.isArray(currentValue) && currentValue.length === 0) if (domNode.nodeType === Node.COMMENT_NODE) {} else console.warn("[Hydrate] Expected comment marker for empty signal, got:", domNode.nodeType);
|
|
172
|
+
else if (typeof currentValue === "string" || typeof currentValue === "number") {
|
|
173
|
+
if (domNode.nodeType === Node.TEXT_NODE) {
|
|
174
|
+
const expectedText = String(currentValue);
|
|
175
|
+
if (domNode.textContent !== expectedText) {
|
|
176
|
+
console.warn("[Hydrate] Signal text mismatch:", domNode.textContent, "->", expectedText);
|
|
177
|
+
domNode.textContent = expectedText;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else hydrateNode(currentValue, domNode, parentElement);
|
|
181
|
+
let anchor;
|
|
182
|
+
let currentNodes = [];
|
|
183
|
+
if (domNode.nodeType === Node.COMMENT_NODE) anchor = domNode;
|
|
184
|
+
else {
|
|
185
|
+
anchor = document.createComment("signal-anchor");
|
|
186
|
+
if (domNode.parentNode) domNode.parentNode.insertBefore(anchor, domNode);
|
|
187
|
+
currentNodes = [domNode];
|
|
188
|
+
}
|
|
189
|
+
signal.subscribe((newValue) => {
|
|
190
|
+
const parent = anchor.parentNode;
|
|
191
|
+
if (!parent) return;
|
|
192
|
+
for (const node of currentNodes) if (node.parentNode) node.parentNode.removeChild(node);
|
|
193
|
+
currentNodes = [];
|
|
194
|
+
const newNode = renderNode(newValue, parentElement);
|
|
195
|
+
if (newNode) if (newNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
196
|
+
const fragment = newNode;
|
|
197
|
+
const children = Array.from(fragment.childNodes);
|
|
198
|
+
let insertAfter = anchor;
|
|
199
|
+
for (const child of children) {
|
|
200
|
+
parent.insertBefore(child, insertAfter.nextSibling);
|
|
201
|
+
insertAfter = child;
|
|
202
|
+
currentNodes.push(child);
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
parent.insertBefore(newNode, anchor.nextSibling);
|
|
206
|
+
currentNodes = [newNode];
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Render a VNode to a DOM node (fallback when hydration fails)
|
|
212
|
+
* This is a simplified version of render() just for hydration fallback
|
|
213
|
+
*/
|
|
214
|
+
function renderNode(vnode, parentElement) {
|
|
215
|
+
if (vnode == null || vnode === false || vnode === true) return document.createComment("empty");
|
|
216
|
+
if (isSignal(vnode)) return renderNode(vnode.value, parentElement);
|
|
217
|
+
if (typeof vnode === "string" || typeof vnode === "number") return document.createTextNode(String(vnode));
|
|
218
|
+
if (Array.isArray(vnode)) {
|
|
219
|
+
if (vnode.length === 0) return document.createComment("empty");
|
|
220
|
+
const fragment = document.createDocumentFragment();
|
|
221
|
+
for (const child of vnode) {
|
|
222
|
+
const node = renderNode(child, parentElement);
|
|
223
|
+
if (node) fragment.appendChild(node);
|
|
224
|
+
}
|
|
225
|
+
return fragment;
|
|
226
|
+
}
|
|
227
|
+
if (typeof vnode === "object" && "type" in vnode) {
|
|
228
|
+
const vnodeTyped = vnode;
|
|
229
|
+
if (vnodeTyped.type === "#text") return document.createTextNode(String(vnodeTyped.props?.nodeValue || ""));
|
|
230
|
+
if (vnodeTyped.type === "#signal") {
|
|
231
|
+
const signal = vnodeTyped.props?.signal;
|
|
232
|
+
if (signal && isSignal(signal)) return renderNode(signal.value, parentElement);
|
|
233
|
+
return document.createTextNode("");
|
|
234
|
+
}
|
|
235
|
+
if (vnodeTyped.type === Fragment) {
|
|
236
|
+
const fragment = document.createDocumentFragment();
|
|
237
|
+
for (const child of vnodeTyped.children) {
|
|
238
|
+
const node = renderNode(child, parentElement);
|
|
239
|
+
if (node) fragment.appendChild(node);
|
|
240
|
+
}
|
|
241
|
+
return fragment;
|
|
242
|
+
}
|
|
243
|
+
if (typeof vnodeTyped.type === "function") return renderNode(vnodeTyped.type(vnodeTyped.props || {}), parentElement);
|
|
244
|
+
if (typeof vnodeTyped.type === "string") {
|
|
245
|
+
const element = document.createElement(vnodeTyped.type);
|
|
246
|
+
const props = vnodeTyped.props || {};
|
|
247
|
+
for (const [key, value] of Object.entries(props)) {
|
|
248
|
+
if (key === "children" || key === "key") continue;
|
|
249
|
+
setProperty(element, key, value);
|
|
250
|
+
}
|
|
251
|
+
for (const child of vnodeTyped.children) {
|
|
252
|
+
const childNode = renderNode(child, element);
|
|
253
|
+
if (childNode) element.appendChild(childNode);
|
|
254
|
+
}
|
|
255
|
+
return element;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Hydrate an island by ID
|
|
262
|
+
* Handles both single-element islands (with data-island-id) and fragment islands (with comment markers)
|
|
263
|
+
*
|
|
264
|
+
* @param islandId - The island ID to hydrate
|
|
265
|
+
* @param Component - The component function to render
|
|
266
|
+
* @param markHydrated - Callback to mark the island as hydrated
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```tsx
|
|
270
|
+
* import { hydrateIsland, markIslandHydrated } from '@semajsx/ssr/client';
|
|
271
|
+
* import Counter from './Counter';
|
|
272
|
+
*
|
|
273
|
+
* hydrateIsland('counter-0', Counter, markIslandHydrated);
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
function hydrateIsland(islandId, Component, markHydrated) {
|
|
277
|
+
const element = document.querySelector(`[data-island-id="${islandId}"]`);
|
|
278
|
+
if (element) {
|
|
279
|
+
const props = JSON.parse(element.getAttribute("data-island-props") || "{}");
|
|
280
|
+
const parent = element.parentNode;
|
|
281
|
+
if (!parent) return;
|
|
282
|
+
const vnode = {
|
|
283
|
+
type: Component,
|
|
284
|
+
props,
|
|
285
|
+
children: []
|
|
286
|
+
};
|
|
287
|
+
const temp = document.createElement("div");
|
|
288
|
+
render(vnode, temp);
|
|
289
|
+
const children = Array.from(temp.childNodes);
|
|
290
|
+
for (const child of children) parent.insertBefore(child, element);
|
|
291
|
+
parent.removeChild(element);
|
|
292
|
+
markHydrated(islandId);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
|
296
|
+
let startComment = null;
|
|
297
|
+
let comment;
|
|
298
|
+
while (comment = walker.nextNode()) if (comment.textContent === `island:${islandId}`) {
|
|
299
|
+
startComment = comment;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
if (startComment) {
|
|
303
|
+
const script = document.querySelector(`script[data-island="${islandId}"]`);
|
|
304
|
+
const props = script ? JSON.parse(script.textContent || "{}") : {};
|
|
305
|
+
const nodesToRemove = [];
|
|
306
|
+
let sibling = startComment.nextSibling;
|
|
307
|
+
let endComment = null;
|
|
308
|
+
while (sibling) {
|
|
309
|
+
if (sibling.nodeType === Node.COMMENT_NODE && sibling.textContent === `/island:${islandId}`) {
|
|
310
|
+
endComment = sibling;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
nodesToRemove.push(sibling);
|
|
314
|
+
sibling = sibling.nextSibling;
|
|
315
|
+
}
|
|
316
|
+
for (const node of nodesToRemove) node.parentNode?.removeChild(node);
|
|
317
|
+
const vnode = {
|
|
318
|
+
type: Component,
|
|
319
|
+
props,
|
|
320
|
+
children: []
|
|
321
|
+
};
|
|
322
|
+
const parent = startComment.parentNode;
|
|
323
|
+
if (parent) {
|
|
324
|
+
const temp = document.createElement("div");
|
|
325
|
+
render(vnode, temp);
|
|
326
|
+
const children = Array.from(temp.childNodes);
|
|
327
|
+
for (const child of children) parent.insertBefore(child, endComment);
|
|
328
|
+
}
|
|
329
|
+
startComment.parentNode?.removeChild(startComment);
|
|
330
|
+
if (endComment) endComment.parentNode?.removeChild(endComment);
|
|
331
|
+
if (script) script.parentNode?.removeChild(script);
|
|
332
|
+
markHydrated(islandId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Find all islands on the page (both element and fragment types)
|
|
337
|
+
*/
|
|
338
|
+
function findAllIslands() {
|
|
339
|
+
const islands = [];
|
|
340
|
+
const elements = document.querySelectorAll("[data-island-id]");
|
|
341
|
+
for (const el of elements) {
|
|
342
|
+
const id = el.getAttribute("data-island-id");
|
|
343
|
+
const propsStr = el.getAttribute("data-island-props");
|
|
344
|
+
if (id) islands.push({
|
|
345
|
+
id,
|
|
346
|
+
props: propsStr ? JSON.parse(propsStr) : {},
|
|
347
|
+
element: el
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
|
351
|
+
let comment;
|
|
352
|
+
while (comment = walker.nextNode()) {
|
|
353
|
+
const match = comment.textContent?.match(/^island:(.+)$/);
|
|
354
|
+
if (match && match[1]) {
|
|
355
|
+
const id = match[1];
|
|
356
|
+
let endComment = null;
|
|
357
|
+
let sibling = comment.nextSibling;
|
|
358
|
+
while (sibling) {
|
|
359
|
+
if (sibling.nodeType === Node.COMMENT_NODE && sibling.textContent === `/island:${id}`) {
|
|
360
|
+
endComment = sibling;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
sibling = sibling.nextSibling;
|
|
364
|
+
}
|
|
365
|
+
const script = document.querySelector(`script[type="application/json"][data-island="${id}"]`);
|
|
366
|
+
const props = script ? JSON.parse(script.textContent || "{}") : {};
|
|
367
|
+
islands.push({
|
|
368
|
+
id,
|
|
369
|
+
props,
|
|
370
|
+
startComment: comment,
|
|
371
|
+
endComment: endComment || void 0
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return islands;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Hydrate all islands on the page
|
|
379
|
+
* This function is typically called once after the page loads
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* ```tsx
|
|
383
|
+
* // In your client entry point
|
|
384
|
+
* import { hydrateIslands } from '@semajsx/ssr/client'
|
|
385
|
+
*
|
|
386
|
+
* // Wait for DOM to be ready
|
|
387
|
+
* if (document.readyState === 'loading') {
|
|
388
|
+
* document.addEventListener('DOMContentLoaded', hydrateIslands)
|
|
389
|
+
* } else {
|
|
390
|
+
* hydrateIslands()
|
|
391
|
+
* }
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
async function hydrateIslands() {
|
|
395
|
+
const islands = findAllIslands();
|
|
396
|
+
if (islands.length === 0) return;
|
|
397
|
+
console.log(`[SemaJSX] Found ${islands.length} islands to hydrate`);
|
|
398
|
+
const hydrations = islands.map((island) => waitForIslandScript(island));
|
|
399
|
+
await Promise.all(hydrations);
|
|
400
|
+
console.log(`[SemaJSX] All islands hydrated`);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Wait for an island's script to load and hydrate it
|
|
404
|
+
* The actual hydration is performed by the island's entry point script
|
|
405
|
+
* This function just waits for it to complete
|
|
406
|
+
*/
|
|
407
|
+
async function waitForIslandScript(island) {
|
|
408
|
+
const { id: islandId, element, startComment } = island;
|
|
409
|
+
if (element?.hasAttribute("data-hydrated")) return;
|
|
410
|
+
if (startComment?.parentElement?.querySelector(`[data-island-hydrated="${islandId}"]`)) return;
|
|
411
|
+
return new Promise((resolve) => {
|
|
412
|
+
const maxAttempts = 200;
|
|
413
|
+
let attempts = 0;
|
|
414
|
+
const checkInterval = setInterval(() => {
|
|
415
|
+
if (element ? element.hasAttribute("data-hydrated") : document.querySelector(`[data-island-hydrated="${islandId}"]`) !== null) {
|
|
416
|
+
clearInterval(checkInterval);
|
|
417
|
+
resolve();
|
|
418
|
+
} else if (++attempts >= maxAttempts) {
|
|
419
|
+
clearInterval(checkInterval);
|
|
420
|
+
console.warn(`[SemaJSX] Island ${islandId} hydration timeout`);
|
|
421
|
+
resolve();
|
|
422
|
+
}
|
|
423
|
+
}, 50);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Get island info by ID
|
|
428
|
+
*/
|
|
429
|
+
function getIslandInfo(islandId) {
|
|
430
|
+
const element = document.querySelector(`[data-island-id="${islandId}"]`);
|
|
431
|
+
if (element) {
|
|
432
|
+
const propsStr = element.getAttribute("data-island-props");
|
|
433
|
+
return {
|
|
434
|
+
id: islandId,
|
|
435
|
+
props: propsStr ? JSON.parse(propsStr) : {},
|
|
436
|
+
element
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const script = document.querySelector(`script[type="application/json"][data-island="${islandId}"]`);
|
|
440
|
+
if (script) {
|
|
441
|
+
const props = JSON.parse(script.textContent || "{}");
|
|
442
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
|
443
|
+
let comment;
|
|
444
|
+
while (comment = walker.nextNode()) if (comment.textContent === `island:${islandId}`) return {
|
|
445
|
+
id: islandId,
|
|
446
|
+
props,
|
|
447
|
+
startComment: comment
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Manual hydration for a specific island
|
|
454
|
+
* Useful for lazy-loading islands on interaction
|
|
455
|
+
*
|
|
456
|
+
* @param islandId - The island ID to hydrate
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```tsx
|
|
460
|
+
* // Lazy load an island on click
|
|
461
|
+
* button.addEventListener('click', () => {
|
|
462
|
+
* hydrateIslandById('island-0')
|
|
463
|
+
* })
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
async function hydrateIslandById(islandId) {
|
|
467
|
+
const island = getIslandInfo(islandId);
|
|
468
|
+
if (!island) {
|
|
469
|
+
console.error(`[SemaJSX] Island not found: ${islandId}`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
await waitForIslandScript(island);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Check if islands are present on the page
|
|
476
|
+
*/
|
|
477
|
+
function hasIslands() {
|
|
478
|
+
if (document.querySelectorAll("[data-island-id]").length > 0) return true;
|
|
479
|
+
return document.querySelectorAll("script[type=\"application/json\"][data-island]").length > 0;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Get all island IDs on the page
|
|
483
|
+
*/
|
|
484
|
+
function getIslandIds() {
|
|
485
|
+
const ids = [];
|
|
486
|
+
const elements = document.querySelectorAll("[data-island-id]");
|
|
487
|
+
for (const el of elements) {
|
|
488
|
+
const id = el.getAttribute("data-island-id");
|
|
489
|
+
if (id) ids.push(id);
|
|
490
|
+
}
|
|
491
|
+
const scripts = document.querySelectorAll("script[type=\"application/json\"][data-island]");
|
|
492
|
+
for (const script of scripts) {
|
|
493
|
+
const id = script.getAttribute("data-island");
|
|
494
|
+
if (id) ids.push(id);
|
|
495
|
+
}
|
|
496
|
+
return ids;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Mark an island as hydrated
|
|
500
|
+
* This should be called by the island entry point after hydration completes
|
|
501
|
+
*/
|
|
502
|
+
function markIslandHydrated(islandId) {
|
|
503
|
+
const element = document.querySelector(`[data-island-id="${islandId}"]`);
|
|
504
|
+
if (element) {
|
|
505
|
+
element.setAttribute("data-hydrated", "true");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const script = document.querySelector(`script[type="application/json"][data-island="${islandId}"]`);
|
|
509
|
+
if (script) {
|
|
510
|
+
script.setAttribute("data-island-hydrated", islandId);
|
|
511
|
+
script.remove();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Reconstruct VNode children from serialized JSON data.
|
|
516
|
+
*
|
|
517
|
+
* Uses the island module's exports as a registry to resolve component names
|
|
518
|
+
* (prefixed with "$") back to their actual functions.
|
|
519
|
+
*
|
|
520
|
+
* @param serialized - Serialized children array from SSR
|
|
521
|
+
* @param registry - Module exports mapping component names to functions
|
|
522
|
+
*/
|
|
523
|
+
function reconstructChildren(serialized, registry) {
|
|
524
|
+
const result = [];
|
|
525
|
+
for (const node of serialized) {
|
|
526
|
+
if (node === null) continue;
|
|
527
|
+
if (typeof node === "string") {
|
|
528
|
+
result.push(node);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (Array.isArray(node) && node.length === 3 && typeof node[0] === "string") {
|
|
532
|
+
const [type, props, children] = node;
|
|
533
|
+
if (type === "$island") {
|
|
534
|
+
result.push(null);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const resolvedChildren = children ? reconstructChildren(children, registry) : [];
|
|
538
|
+
if (type.startsWith("$")) {
|
|
539
|
+
const name = type.slice(1);
|
|
540
|
+
const component = registry[name];
|
|
541
|
+
if (!component || typeof component !== "function") {
|
|
542
|
+
console.warn(`[Hydrate] Unknown component "${name}" in island children`);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
result.push(h(component, props || {}, ...resolvedChildren));
|
|
546
|
+
} else result.push(h(type, props || {}, ...resolvedChildren));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Hydrate all islands with a given component source
|
|
553
|
+
* Finds all elements with data-island-src and hydrates them
|
|
554
|
+
*
|
|
555
|
+
* @param componentSrc - The component source key (e.g., "components/Counter")
|
|
556
|
+
* @param Component - The component function to render
|
|
557
|
+
* @param registry - Optional module exports for reconstructing island children
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```tsx
|
|
561
|
+
* import { hydrateAllIslands } from '@semajsx/ssr/client';
|
|
562
|
+
* import * as CounterModule from './Counter';
|
|
563
|
+
*
|
|
564
|
+
* hydrateAllIslands('components/Counter', CounterModule.Counter, CounterModule);
|
|
565
|
+
* ```
|
|
566
|
+
*/
|
|
567
|
+
function hydrateAllIslands(componentSrc, Component, registry) {
|
|
568
|
+
const elements = document.querySelectorAll(`[data-island-src="${componentSrc}"]`);
|
|
569
|
+
const scripts = document.querySelectorAll(`script[type="application/json"][data-island-src="${componentSrc}"]`);
|
|
570
|
+
elements.forEach((element) => {
|
|
571
|
+
const islandId = element.getAttribute("data-island-id");
|
|
572
|
+
if (!islandId) return;
|
|
573
|
+
if (element.hasAttribute("data-hydrated")) return;
|
|
574
|
+
try {
|
|
575
|
+
const props = JSON.parse(element.getAttribute("data-island-props") || "{}");
|
|
576
|
+
if (registry) {
|
|
577
|
+
const childrenScript = document.querySelector(`script[type="application/json"][data-island-children="${islandId}"]`);
|
|
578
|
+
if (childrenScript) {
|
|
579
|
+
try {
|
|
580
|
+
props.children = reconstructChildren(JSON.parse(childrenScript.textContent || "[]"), registry);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
console.warn("[Hydrate] Failed to reconstruct island children:", e);
|
|
583
|
+
}
|
|
584
|
+
childrenScript.remove();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
hydrateNode({
|
|
588
|
+
type: Component,
|
|
589
|
+
props,
|
|
590
|
+
children: []
|
|
591
|
+
}, element, element.parentNode);
|
|
592
|
+
element.setAttribute("data-hydrated", "true");
|
|
593
|
+
} catch (error) {
|
|
594
|
+
console.error(`[Hydrate] Island "${islandId}" hydration failed:`, error);
|
|
595
|
+
element.setAttribute("data-hydration-error", "true");
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
scripts.forEach((script) => {
|
|
599
|
+
const islandId = script.getAttribute("data-island");
|
|
600
|
+
if (!islandId) return;
|
|
601
|
+
if (script.hasAttribute("data-island-hydrated")) return;
|
|
602
|
+
try {
|
|
603
|
+
const props = JSON.parse(script.textContent || "{}");
|
|
604
|
+
if (registry) {
|
|
605
|
+
const childrenScript = document.querySelector(`script[type="application/json"][data-island-children="${islandId}"]`);
|
|
606
|
+
if (childrenScript) {
|
|
607
|
+
try {
|
|
608
|
+
props.children = reconstructChildren(JSON.parse(childrenScript.textContent || "[]"), registry);
|
|
609
|
+
} catch (e) {
|
|
610
|
+
console.warn("[Hydrate] Failed to reconstruct island children:", e);
|
|
611
|
+
}
|
|
612
|
+
childrenScript.remove();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const startMarker = `island:${islandId}`;
|
|
616
|
+
const endMarker = `/island:${islandId}`;
|
|
617
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT, null);
|
|
618
|
+
let startNode = null;
|
|
619
|
+
let endNode = null;
|
|
620
|
+
let node;
|
|
621
|
+
while (node = walker.nextNode()) if (node.nodeValue === startMarker) startNode = node;
|
|
622
|
+
else if (node.nodeValue === endMarker) {
|
|
623
|
+
endNode = node;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
if (startNode && endNode && startNode.parentNode) {
|
|
627
|
+
const parent = startNode.parentNode;
|
|
628
|
+
const container = document.createElement("div");
|
|
629
|
+
let current = startNode.nextSibling;
|
|
630
|
+
while (current && current !== endNode) {
|
|
631
|
+
const next = current.nextSibling;
|
|
632
|
+
container.appendChild(current);
|
|
633
|
+
current = next;
|
|
634
|
+
}
|
|
635
|
+
hydrateNode({
|
|
636
|
+
type: Component,
|
|
637
|
+
props,
|
|
638
|
+
children: []
|
|
639
|
+
}, container, container);
|
|
640
|
+
while (container.firstChild) parent.insertBefore(container.firstChild, endNode);
|
|
641
|
+
script.setAttribute("data-island-hydrated", islandId);
|
|
642
|
+
script.remove();
|
|
643
|
+
}
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error(`[Hydrate] Fragment island "${islandId}" hydration failed:`, error);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region ../ssr/src/client/client-resource.ts
|
|
652
|
+
let _manifest = null;
|
|
653
|
+
const loadedStyles = /* @__PURE__ */ new Set();
|
|
654
|
+
/**
|
|
655
|
+
* Set the client manifest (called during initialization)
|
|
656
|
+
*/
|
|
657
|
+
function setManifest(manifest) {
|
|
658
|
+
_manifest = manifest;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Get the current manifest
|
|
662
|
+
*/
|
|
663
|
+
function getManifest() {
|
|
664
|
+
return _manifest;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Resolve a CSS path using the manifest
|
|
668
|
+
*/
|
|
669
|
+
function resolveCSS(href) {
|
|
670
|
+
if (!_manifest) return href;
|
|
671
|
+
const lookupPath = href.startsWith("/") ? href.slice(1) : href;
|
|
672
|
+
return _manifest.css[lookupPath] || href;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Resolve an asset path using the manifest
|
|
676
|
+
*/
|
|
677
|
+
function resolveAsset(src) {
|
|
678
|
+
if (!_manifest) return src;
|
|
679
|
+
const lookupPath = src.startsWith("/") ? src.slice(1) : src;
|
|
680
|
+
return _manifest.assets[lookupPath] || src;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Dynamically load a stylesheet
|
|
684
|
+
*/
|
|
685
|
+
function loadStylesheet(href) {
|
|
686
|
+
const resolvedHref = resolveCSS(href);
|
|
687
|
+
if (loadedStyles.has(resolvedHref)) return Promise.resolve();
|
|
688
|
+
return new Promise((resolve, reject) => {
|
|
689
|
+
if (document.querySelector(`link[href="${resolvedHref}"]`)) {
|
|
690
|
+
loadedStyles.add(resolvedHref);
|
|
691
|
+
resolve();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const link = document.createElement("link");
|
|
695
|
+
link.rel = "stylesheet";
|
|
696
|
+
link.href = resolvedHref;
|
|
697
|
+
link.onload = () => {
|
|
698
|
+
loadedStyles.add(resolvedHref);
|
|
699
|
+
resolve();
|
|
700
|
+
};
|
|
701
|
+
link.onerror = () => {
|
|
702
|
+
reject(/* @__PURE__ */ new Error(`Failed to load stylesheet: ${resolvedHref}`));
|
|
703
|
+
};
|
|
704
|
+
document.head.appendChild(link);
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Create client-side resource tools
|
|
709
|
+
*
|
|
710
|
+
* @example
|
|
711
|
+
* ```tsx
|
|
712
|
+
* import { clientResource } from '@semajsx/ssr/client';
|
|
713
|
+
*
|
|
714
|
+
* const { Style, url } = clientResource();
|
|
715
|
+
*
|
|
716
|
+
* export default function Counter() {
|
|
717
|
+
* return (
|
|
718
|
+
* <>
|
|
719
|
+
* <Style href="./counter.css" />
|
|
720
|
+
* <img src={url('./icon.png')} />
|
|
721
|
+
* </>
|
|
722
|
+
* );
|
|
723
|
+
* }
|
|
724
|
+
* ```
|
|
725
|
+
*/
|
|
726
|
+
function clientResource() {
|
|
727
|
+
return {
|
|
728
|
+
Style({ href }) {
|
|
729
|
+
if (typeof document !== "undefined") loadStylesheet(href);
|
|
730
|
+
return null;
|
|
731
|
+
},
|
|
732
|
+
url(path) {
|
|
733
|
+
return resolveAsset(path);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
//#endregion
|
|
739
|
+
export { resolveCSS as a, getIslandInfo as c, hydrateAllIslands as d, hydrateIsland as f, markIslandHydrated as h, resolveAsset as i, hasIslands as l, hydrateIslands as m, getManifest as n, setManifest as o, hydrateIslandById as p, loadStylesheet as r, getIslandIds as s, clientResource as t, hydrate as u };
|
|
740
|
+
//# sourceMappingURL=client-CButR91p.mjs.map
|