@ox-content/islands 3.1.3 → 3.1.4
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/html-host.cjs +8 -0
- package/dist/html-host.d.cts +88 -0
- package/dist/html-host.d.cts.map +1 -0
- package/dist/html-host.d.mts +88 -0
- package/dist/html-host.d.mts.map +1 -0
- package/dist/html-host.mjs +2 -0
- package/dist/html-host2.cjs +259 -0
- package/dist/html-host2.d.cts +2 -0
- package/dist/html-host2.d.mts +2 -0
- package/dist/html-host2.mjs +226 -0
- package/dist/html-host2.mjs.map +1 -0
- package/dist/index.cjs +15 -0
- package/dist/index.d.cts +19 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/runtime.cjs +5 -0
- package/dist/runtime.d.cts +95 -0
- package/dist/runtime.d.cts.map +1 -0
- package/dist/runtime.d.mts +2 -84
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime2.cjs +359 -0
- package/dist/runtime2.mjs +45 -19
- package/dist/runtime2.mjs.map +1 -1
- package/dist/types.d.cts +86 -0
- package/dist/types.d.cts.map +1 -0
- package/dist/types.d.mts +86 -0
- package/dist/types.d.mts.map +1 -0
- package/package.json +27 -5
- package/dist/runtime2.d.mts +0 -2
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
//#region src/payload.ts
|
|
2
|
+
const RUST_PAYLOAD_KEYS = /* @__PURE__ */ new Set([
|
|
3
|
+
"props",
|
|
4
|
+
"expressions",
|
|
5
|
+
"spreads"
|
|
6
|
+
]);
|
|
7
|
+
const ISLAND_JSON_SCRIPT = /^\s*<script type="application\/json">[\s\S]*?<\/script>/;
|
|
8
|
+
/**
|
|
9
|
+
* Flatten the Rust MDX island payload (`{ props, expressions, spreads }`)
|
|
10
|
+
* into the literal props object hydrate functions expect. Legacy flat
|
|
11
|
+
* `data-ox-props` objects from the Markdown regex path are returned as-is.
|
|
12
|
+
*/
|
|
13
|
+
function unwrapIslandProps(parsed) {
|
|
14
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
15
|
+
const record = parsed;
|
|
16
|
+
const keys = Object.keys(record);
|
|
17
|
+
if (keys.length > 0 && keys.every((key) => RUST_PAYLOAD_KEYS.has(key)) && record.props && typeof record.props === "object" && !Array.isArray(record.props)) return record.props;
|
|
18
|
+
return record;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Slot markup for an island: `data-ox-content` when the regex path set it,
|
|
22
|
+
* otherwise inner HTML with the Rust JSON payload script stripped.
|
|
23
|
+
*/
|
|
24
|
+
function stripIslandPayloadScript(innerHTML) {
|
|
25
|
+
return innerHTML.replace(ISLAND_JSON_SCRIPT, "");
|
|
26
|
+
}
|
|
27
|
+
function readIslandSlotHtml(element) {
|
|
28
|
+
const fromAttr = element.dataset.oxContent;
|
|
29
|
+
if (fromAttr) return fromAttr;
|
|
30
|
+
return stripIslandPayloadScript(element.innerHTML);
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/runtime.ts
|
|
34
|
+
/**
|
|
35
|
+
* Island Architecture Runtime
|
|
36
|
+
*
|
|
37
|
+
* Framework-agnostic Island controller using pure Vanilla JavaScript.
|
|
38
|
+
* No framework dependencies - works with Vue, React, Svelte, or plain JS.
|
|
39
|
+
*/
|
|
40
|
+
const defaultOptions = {
|
|
41
|
+
rootMargin: "200px",
|
|
42
|
+
threshold: 0,
|
|
43
|
+
idleTimeout: 200,
|
|
44
|
+
selector: "[data-ox-island]"
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Parse island configuration from element attributes.
|
|
48
|
+
*/
|
|
49
|
+
function parseIslandConfig(element) {
|
|
50
|
+
const component = element.dataset.oxIsland || "";
|
|
51
|
+
const load = element.dataset.oxLoad || "eager";
|
|
52
|
+
const mediaQuery = element.dataset.oxMedia;
|
|
53
|
+
let props = {};
|
|
54
|
+
try {
|
|
55
|
+
const propsJson = element.dataset.oxProps;
|
|
56
|
+
if (propsJson) props = unwrapIslandProps(JSON.parse(propsJson));
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.warn("[ox-islands] Failed to parse props for", element, e);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
id: element.id || `island-${Math.random().toString(36).slice(2, 9)}`,
|
|
62
|
+
component,
|
|
63
|
+
load,
|
|
64
|
+
mediaQuery,
|
|
65
|
+
props,
|
|
66
|
+
ssr: element.dataset.oxSsr === "true"
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Observe element visibility using IntersectionObserver.
|
|
71
|
+
*/
|
|
72
|
+
function observeVisibility(element, callback, options) {
|
|
73
|
+
const observer = new IntersectionObserver((entries) => {
|
|
74
|
+
if (entries[0].isIntersecting) {
|
|
75
|
+
observer.disconnect();
|
|
76
|
+
callback();
|
|
77
|
+
}
|
|
78
|
+
}, {
|
|
79
|
+
rootMargin: options.rootMargin,
|
|
80
|
+
threshold: options.threshold
|
|
81
|
+
});
|
|
82
|
+
observer.observe(element);
|
|
83
|
+
return () => observer.disconnect();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Observe media query changes.
|
|
87
|
+
*/
|
|
88
|
+
function observeMedia(query, callback) {
|
|
89
|
+
const mql = matchMedia(query);
|
|
90
|
+
if (mql.matches) {
|
|
91
|
+
callback();
|
|
92
|
+
return () => {};
|
|
93
|
+
}
|
|
94
|
+
const handler = () => {
|
|
95
|
+
if (mql.matches) {
|
|
96
|
+
mql.removeEventListener("change", handler);
|
|
97
|
+
callback();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
mql.addEventListener("change", handler);
|
|
101
|
+
return () => mql.removeEventListener("change", handler);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Schedule callback for browser idle time.
|
|
105
|
+
*/
|
|
106
|
+
function scheduleIdle(callback, timeout) {
|
|
107
|
+
if ("requestIdleCallback" in window) {
|
|
108
|
+
const id = requestIdleCallback(callback, { timeout });
|
|
109
|
+
return () => cancelIdleCallback(id);
|
|
110
|
+
}
|
|
111
|
+
const id = setTimeout(callback, timeout);
|
|
112
|
+
return () => clearTimeout(id);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Initialize islands with a hydration function.
|
|
116
|
+
*
|
|
117
|
+
* This is the main entry point for the island system.
|
|
118
|
+
* Pass a hydrate function that knows how to mount your components.
|
|
119
|
+
*
|
|
120
|
+
* @example Vue
|
|
121
|
+
* ```ts
|
|
122
|
+
* import { initIslands } from '@ox-content/islands';
|
|
123
|
+
* import { createApp, h } from 'vue';
|
|
124
|
+
* import Counter from './Counter.vue';
|
|
125
|
+
*
|
|
126
|
+
* const components = { Counter };
|
|
127
|
+
*
|
|
128
|
+
* initIslands((el, props) => {
|
|
129
|
+
* const name = el.dataset.oxIsland!;
|
|
130
|
+
* const Component = components[name];
|
|
131
|
+
* if (!Component) return;
|
|
132
|
+
*
|
|
133
|
+
* const app = createApp({ render: () => h(Component, props) });
|
|
134
|
+
* app.mount(el);
|
|
135
|
+
*
|
|
136
|
+
* return () => app.unmount();
|
|
137
|
+
* });
|
|
138
|
+
* ```
|
|
139
|
+
*
|
|
140
|
+
* @example React
|
|
141
|
+
* ```ts
|
|
142
|
+
* import { initIslands } from '@ox-content/islands';
|
|
143
|
+
* import { createRoot } from 'react-dom/client';
|
|
144
|
+
* import Counter from './Counter';
|
|
145
|
+
*
|
|
146
|
+
* const components = { Counter };
|
|
147
|
+
*
|
|
148
|
+
* initIslands((el, props) => {
|
|
149
|
+
* const name = el.dataset.oxIsland!;
|
|
150
|
+
* const Component = components[name];
|
|
151
|
+
* if (!Component) return;
|
|
152
|
+
*
|
|
153
|
+
* const root = createRoot(el);
|
|
154
|
+
* root.render(<Component {...props} />);
|
|
155
|
+
*
|
|
156
|
+
* return () => root.unmount();
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
160
|
+
* @example Vanilla JS
|
|
161
|
+
* ```ts
|
|
162
|
+
* import { initIslands } from '@ox-content/islands';
|
|
163
|
+
*
|
|
164
|
+
* initIslands((el, props) => {
|
|
165
|
+
* const name = el.dataset.oxIsland!;
|
|
166
|
+
*
|
|
167
|
+
* if (name === 'Counter') {
|
|
168
|
+
* let count = props.initial || 0;
|
|
169
|
+
* const button = el.querySelector('button')!;
|
|
170
|
+
* const handler = () => {
|
|
171
|
+
* count++;
|
|
172
|
+
* button.textContent = String(count);
|
|
173
|
+
* };
|
|
174
|
+
* button.addEventListener('click', handler);
|
|
175
|
+
* return () => button.removeEventListener('click', handler);
|
|
176
|
+
* }
|
|
177
|
+
* });
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
function initIslands(hydrate, options) {
|
|
181
|
+
const opts = {
|
|
182
|
+
...defaultOptions,
|
|
183
|
+
...options
|
|
184
|
+
};
|
|
185
|
+
const instances = [];
|
|
186
|
+
const cleanups = [];
|
|
187
|
+
/**
|
|
188
|
+
* Hydrate a single island element.
|
|
189
|
+
*/
|
|
190
|
+
function hydrateIsland(element, config) {
|
|
191
|
+
let instance = instances.find((i) => i.element === element);
|
|
192
|
+
if (instance?.hydrated || instance?.hydrating) return;
|
|
193
|
+
if (!instance) {
|
|
194
|
+
instance = {
|
|
195
|
+
element,
|
|
196
|
+
config,
|
|
197
|
+
hydrated: false
|
|
198
|
+
};
|
|
199
|
+
instances.push(instance);
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
opts.onHydrateStart?.(element, config);
|
|
203
|
+
element.classList.add("ox-island-loading");
|
|
204
|
+
instance.hydrating = true;
|
|
205
|
+
const result = hydrate(element, config.props);
|
|
206
|
+
if (isPromiseLike(result)) {
|
|
207
|
+
const cleanup = typeof result === "function" ? result : void 0;
|
|
208
|
+
instance.cleanup = cleanup;
|
|
209
|
+
Promise.resolve(result).then((resolvedCleanup) => finishHydration(instance, element, config, cleanup ?? (typeof resolvedCleanup === "function" ? resolvedCleanup : void 0)), (error) => failHydration(instance, element, config, error));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
finishHydration(instance, element, config, result);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
failHydration(instance, element, config, error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function finishHydration(instance, element, config, cleanup) {
|
|
218
|
+
const normalizedCleanup = cleanup || void 0;
|
|
219
|
+
if (!instances.includes(instance)) {
|
|
220
|
+
normalizedCleanup?.();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
element.dataset.oxHydrated = "true";
|
|
224
|
+
element.classList.remove("ox-island-loading");
|
|
225
|
+
instance.cleanup = normalizedCleanup;
|
|
226
|
+
instance.hydrated = true;
|
|
227
|
+
instance.hydrating = false;
|
|
228
|
+
opts.onHydrateEnd?.(element, config);
|
|
229
|
+
}
|
|
230
|
+
function failHydration(instance, element, config, error) {
|
|
231
|
+
if (!instances.includes(instance)) return;
|
|
232
|
+
element.classList.remove("ox-island-loading");
|
|
233
|
+
element.classList.add("ox-island-error");
|
|
234
|
+
element.dataset.oxError = error instanceof Error ? error.message : String(error);
|
|
235
|
+
instance.cleanup = void 0;
|
|
236
|
+
instance.hydrating = false;
|
|
237
|
+
opts.onHydrateError?.(element, config, error instanceof Error ? error : new Error(String(error)));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Schedule hydration based on load strategy.
|
|
241
|
+
*/
|
|
242
|
+
function scheduleHydration(element, config) {
|
|
243
|
+
switch (config.load) {
|
|
244
|
+
case "eager":
|
|
245
|
+
hydrateIsland(element, config);
|
|
246
|
+
break;
|
|
247
|
+
case "idle": {
|
|
248
|
+
const cancel = scheduleIdle(() => hydrateIsland(element, config), opts.idleTimeout);
|
|
249
|
+
cleanups.push(cancel);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case "visible": {
|
|
253
|
+
const cancel = observeVisibility(element, () => hydrateIsland(element, config), {
|
|
254
|
+
rootMargin: opts.rootMargin,
|
|
255
|
+
threshold: opts.threshold
|
|
256
|
+
});
|
|
257
|
+
cleanups.push(cancel);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "media":
|
|
261
|
+
if (config.mediaQuery) {
|
|
262
|
+
const cancel = observeMedia(config.mediaQuery, () => hydrateIsland(element, config));
|
|
263
|
+
cleanups.push(cancel);
|
|
264
|
+
} else hydrateIsland(element, config);
|
|
265
|
+
break;
|
|
266
|
+
default: hydrateIsland(element, config);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
document.querySelectorAll(opts.selector).forEach((element) => {
|
|
270
|
+
const config = parseIslandConfig(element);
|
|
271
|
+
instances.push({
|
|
272
|
+
element,
|
|
273
|
+
config,
|
|
274
|
+
hydrated: false
|
|
275
|
+
});
|
|
276
|
+
scheduleHydration(element, config);
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
instances,
|
|
280
|
+
hydrate(element) {
|
|
281
|
+
hydrateIsland(element, parseIslandConfig(element));
|
|
282
|
+
},
|
|
283
|
+
destroy() {
|
|
284
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
285
|
+
cleanups.length = 0;
|
|
286
|
+
instances.forEach((instance) => {
|
|
287
|
+
if (instance.cleanup) instance.cleanup();
|
|
288
|
+
});
|
|
289
|
+
instances.length = 0;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Create a deferred hydration wrapper.
|
|
295
|
+
*
|
|
296
|
+
* Returns a function that can be called to hydrate islands later.
|
|
297
|
+
* Useful for frameworks that need to register components first.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* const deferredInit = createDeferredInit();
|
|
302
|
+
*
|
|
303
|
+
* // Later, after components are ready
|
|
304
|
+
* const components = await loadComponents();
|
|
305
|
+
* deferredInit((el, props) => {
|
|
306
|
+
* const Component = components[el.dataset.oxIsland!];
|
|
307
|
+
* // ...
|
|
308
|
+
* });
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
function createDeferredInit(options) {
|
|
312
|
+
return (hydrate) => initIslands(hydrate, options);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Check if islands are supported in the current environment.
|
|
316
|
+
*/
|
|
317
|
+
function isIslandsSupported() {
|
|
318
|
+
return typeof window !== "undefined" && typeof document !== "undefined" && typeof IntersectionObserver !== "undefined" && typeof MutationObserver !== "undefined";
|
|
319
|
+
}
|
|
320
|
+
function isPromiseLike(value) {
|
|
321
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
Object.defineProperty(exports, "createDeferredInit", {
|
|
325
|
+
enumerable: true,
|
|
326
|
+
get: function() {
|
|
327
|
+
return createDeferredInit;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
Object.defineProperty(exports, "initIslands", {
|
|
331
|
+
enumerable: true,
|
|
332
|
+
get: function() {
|
|
333
|
+
return initIslands;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
Object.defineProperty(exports, "isIslandsSupported", {
|
|
337
|
+
enumerable: true,
|
|
338
|
+
get: function() {
|
|
339
|
+
return isIslandsSupported;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
Object.defineProperty(exports, "readIslandSlotHtml", {
|
|
343
|
+
enumerable: true,
|
|
344
|
+
get: function() {
|
|
345
|
+
return readIslandSlotHtml;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
Object.defineProperty(exports, "stripIslandPayloadScript", {
|
|
349
|
+
enumerable: true,
|
|
350
|
+
get: function() {
|
|
351
|
+
return stripIslandPayloadScript;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
Object.defineProperty(exports, "unwrapIslandProps", {
|
|
355
|
+
enumerable: true,
|
|
356
|
+
get: function() {
|
|
357
|
+
return unwrapIslandProps;
|
|
358
|
+
}
|
|
359
|
+
});
|
package/dist/runtime2.mjs
CHANGED
|
@@ -188,30 +188,53 @@ function initIslands(hydrate, options) {
|
|
|
188
188
|
* Hydrate a single island element.
|
|
189
189
|
*/
|
|
190
190
|
function hydrateIsland(element, config) {
|
|
191
|
-
|
|
192
|
-
if (instance?.hydrated) return;
|
|
191
|
+
let instance = instances.find((i) => i.element === element);
|
|
192
|
+
if (instance?.hydrated || instance?.hydrating) return;
|
|
193
|
+
if (!instance) {
|
|
194
|
+
instance = {
|
|
195
|
+
element,
|
|
196
|
+
config,
|
|
197
|
+
hydrated: false
|
|
198
|
+
};
|
|
199
|
+
instances.push(instance);
|
|
200
|
+
}
|
|
193
201
|
try {
|
|
194
202
|
opts.onHydrateStart?.(element, config);
|
|
195
203
|
element.classList.add("ox-island-loading");
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
instance.cleanup = cleanup
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
cleanup: cleanup || void 0,
|
|
206
|
-
hydrated: true
|
|
207
|
-
});
|
|
208
|
-
opts.onHydrateEnd?.(element, config);
|
|
204
|
+
instance.hydrating = true;
|
|
205
|
+
const result = hydrate(element, config.props);
|
|
206
|
+
if (isPromiseLike(result)) {
|
|
207
|
+
const cleanup = typeof result === "function" ? result : void 0;
|
|
208
|
+
instance.cleanup = cleanup;
|
|
209
|
+
Promise.resolve(result).then((resolvedCleanup) => finishHydration(instance, element, config, cleanup ?? (typeof resolvedCleanup === "function" ? resolvedCleanup : void 0)), (error) => failHydration(instance, element, config, error));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
finishHydration(instance, element, config, result);
|
|
209
213
|
} catch (error) {
|
|
210
|
-
element
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
failHydration(instance, element, config, error);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function finishHydration(instance, element, config, cleanup) {
|
|
218
|
+
const normalizedCleanup = cleanup || void 0;
|
|
219
|
+
if (!instances.includes(instance)) {
|
|
220
|
+
normalizedCleanup?.();
|
|
221
|
+
return;
|
|
214
222
|
}
|
|
223
|
+
element.dataset.oxHydrated = "true";
|
|
224
|
+
element.classList.remove("ox-island-loading");
|
|
225
|
+
instance.cleanup = normalizedCleanup;
|
|
226
|
+
instance.hydrated = true;
|
|
227
|
+
instance.hydrating = false;
|
|
228
|
+
opts.onHydrateEnd?.(element, config);
|
|
229
|
+
}
|
|
230
|
+
function failHydration(instance, element, config, error) {
|
|
231
|
+
if (!instances.includes(instance)) return;
|
|
232
|
+
element.classList.remove("ox-island-loading");
|
|
233
|
+
element.classList.add("ox-island-error");
|
|
234
|
+
element.dataset.oxError = error instanceof Error ? error.message : String(error);
|
|
235
|
+
instance.cleanup = void 0;
|
|
236
|
+
instance.hydrating = false;
|
|
237
|
+
opts.onHydrateError?.(element, config, error instanceof Error ? error : new Error(String(error)));
|
|
215
238
|
}
|
|
216
239
|
/**
|
|
217
240
|
* Schedule hydration based on load strategy.
|
|
@@ -294,6 +317,9 @@ function createDeferredInit(options) {
|
|
|
294
317
|
function isIslandsSupported() {
|
|
295
318
|
return typeof window !== "undefined" && typeof document !== "undefined" && typeof IntersectionObserver !== "undefined" && typeof MutationObserver !== "undefined";
|
|
296
319
|
}
|
|
320
|
+
function isPromiseLike(value) {
|
|
321
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
322
|
+
}
|
|
297
323
|
//#endregion
|
|
298
324
|
export { stripIslandPayloadScript as a, readIslandSlotHtml as i, initIslands as n, unwrapIslandProps as o, isIslandsSupported as r, createDeferredInit as t };
|
|
299
325
|
|
package/dist/runtime2.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime2.mjs","names":[],"sources":["../src/payload.ts","../src/runtime.ts"],"sourcesContent":["const RUST_PAYLOAD_KEYS = new Set([\"props\", \"expressions\", \"spreads\"]);\nconst ISLAND_JSON_SCRIPT = /^\\s*<script type=\"application\\/json\">[\\s\\S]*?<\\/script>/;\n\n/**\n * Flatten the Rust MDX island payload (`{ props, expressions, spreads }`)\n * into the literal props object hydrate functions expect. Legacy flat\n * `data-ox-props` objects from the Markdown regex path are returned as-is.\n */\nexport function unwrapIslandProps(parsed: unknown): Record<string, unknown> {\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return {};\n }\n\n const record = parsed as Record<string, unknown>;\n const keys = Object.keys(record);\n if (\n keys.length > 0 &&\n keys.every((key) => RUST_PAYLOAD_KEYS.has(key)) &&\n record.props &&\n typeof record.props === \"object\" &&\n !Array.isArray(record.props)\n ) {\n return record.props as Record<string, unknown>;\n }\n\n return record;\n}\n\n/**\n * Slot markup for an island: `data-ox-content` when the regex path set it,\n * otherwise inner HTML with the Rust JSON payload script stripped.\n */\nexport function stripIslandPayloadScript(innerHTML: string): string {\n return innerHTML.replace(ISLAND_JSON_SCRIPT, \"\");\n}\n\nexport function readIslandSlotHtml(element: Pick<HTMLElement, \"dataset\" | \"innerHTML\">): string {\n const fromAttr = element.dataset.oxContent;\n if (fromAttr) return fromAttr;\n return stripIslandPayloadScript(element.innerHTML);\n}\n","/**\n * Island Architecture Runtime\n *\n * Framework-agnostic Island controller using pure Vanilla JavaScript.\n * No framework dependencies - works with Vue, React, Svelte, or plain JS.\n */\n\nimport { unwrapIslandProps } from \"./payload\";\nimport type {\n HydrateFunction,\n InitIslandsOptions,\n IslandConfig,\n IslandInstance,\n IslandController,\n LoadStrategy,\n} from \"./types\";\n\nconst defaultOptions: Required<\n Omit<InitIslandsOptions, \"onHydrateStart\" | \"onHydrateEnd\" | \"onHydrateError\">\n> &\n InitIslandsOptions = {\n rootMargin: \"200px\",\n threshold: 0,\n idleTimeout: 200,\n selector: \"[data-ox-island]\",\n};\n\n/**\n * Parse island configuration from element attributes.\n */\nfunction parseIslandConfig(element: HTMLElement): IslandConfig {\n const component = element.dataset.oxIsland || \"\";\n const load = (element.dataset.oxLoad as LoadStrategy) || \"eager\";\n const mediaQuery = element.dataset.oxMedia;\n\n let props: Record<string, unknown> = {};\n try {\n const propsJson = element.dataset.oxProps;\n if (propsJson) {\n props = unwrapIslandProps(JSON.parse(propsJson));\n }\n } catch (e) {\n console.warn(\"[ox-islands] Failed to parse props for\", element, e);\n }\n\n return {\n id: element.id || `island-${Math.random().toString(36).slice(2, 9)}`,\n component,\n load,\n mediaQuery,\n props,\n ssr: element.dataset.oxSsr === \"true\",\n };\n}\n\n/**\n * Observe element visibility using IntersectionObserver.\n */\nfunction observeVisibility(\n element: HTMLElement,\n callback: () => void,\n options: { rootMargin: string; threshold: number },\n): () => void {\n const observer = new IntersectionObserver(\n (entries) => {\n if (entries[0].isIntersecting) {\n observer.disconnect();\n callback();\n }\n },\n {\n rootMargin: options.rootMargin,\n threshold: options.threshold,\n },\n );\n\n observer.observe(element);\n\n return () => observer.disconnect();\n}\n\n/**\n * Observe media query changes.\n */\nfunction observeMedia(query: string, callback: () => void): () => void {\n const mql = matchMedia(query);\n\n if (mql.matches) {\n callback();\n return () => {};\n }\n\n const handler = () => {\n if (mql.matches) {\n mql.removeEventListener(\"change\", handler);\n callback();\n }\n };\n\n mql.addEventListener(\"change\", handler);\n\n return () => mql.removeEventListener(\"change\", handler);\n}\n\n/**\n * Schedule callback for browser idle time.\n */\nfunction scheduleIdle(callback: () => void, timeout: number): () => void {\n if (\"requestIdleCallback\" in window) {\n const id = requestIdleCallback(callback, { timeout });\n return () => cancelIdleCallback(id);\n }\n\n // Fallback for Safari and older browsers\n const id = setTimeout(callback, timeout);\n return () => clearTimeout(id);\n}\n\n/**\n * Initialize islands with a hydration function.\n *\n * This is the main entry point for the island system.\n * Pass a hydrate function that knows how to mount your components.\n *\n * @example Vue\n * ```ts\n * import { initIslands } from '@ox-content/islands';\n * import { createApp, h } from 'vue';\n * import Counter from './Counter.vue';\n *\n * const components = { Counter };\n *\n * initIslands((el, props) => {\n * const name = el.dataset.oxIsland!;\n * const Component = components[name];\n * if (!Component) return;\n *\n * const app = createApp({ render: () => h(Component, props) });\n * app.mount(el);\n *\n * return () => app.unmount();\n * });\n * ```\n *\n * @example React\n * ```ts\n * import { initIslands } from '@ox-content/islands';\n * import { createRoot } from 'react-dom/client';\n * import Counter from './Counter';\n *\n * const components = { Counter };\n *\n * initIslands((el, props) => {\n * const name = el.dataset.oxIsland!;\n * const Component = components[name];\n * if (!Component) return;\n *\n * const root = createRoot(el);\n * root.render(<Component {...props} />);\n *\n * return () => root.unmount();\n * });\n * ```\n *\n * @example Vanilla JS\n * ```ts\n * import { initIslands } from '@ox-content/islands';\n *\n * initIslands((el, props) => {\n * const name = el.dataset.oxIsland!;\n *\n * if (name === 'Counter') {\n * let count = props.initial || 0;\n * const button = el.querySelector('button')!;\n * const handler = () => {\n * count++;\n * button.textContent = String(count);\n * };\n * button.addEventListener('click', handler);\n * return () => button.removeEventListener('click', handler);\n * }\n * });\n * ```\n */\nexport function initIslands(\n hydrate: HydrateFunction,\n options?: InitIslandsOptions,\n): IslandController {\n const opts = { ...defaultOptions, ...options };\n const instances: IslandInstance[] = [];\n const cleanups: (() => void)[] = [];\n\n /**\n * Hydrate a single island element.\n */\n function hydrateIsland(element: HTMLElement, config: IslandConfig): void {\n // Find existing instance\n const instance = instances.find((i) => i.element === element);\n if (instance?.hydrated) return;\n\n try {\n opts.onHydrateStart?.(element, config);\n\n // Mark as loading\n element.classList.add(\"ox-island-loading\");\n\n // Call the hydrate function\n const cleanup = hydrate(element, config.props);\n\n // Mark as hydrated\n element.dataset.oxHydrated = \"true\";\n element.classList.remove(\"ox-island-loading\");\n\n // Update instance\n if (instance) {\n instance.cleanup = cleanup || undefined;\n instance.hydrated = true;\n } else {\n instances.push({\n element,\n config,\n cleanup: cleanup || undefined,\n hydrated: true,\n });\n }\n\n opts.onHydrateEnd?.(element, config);\n } catch (error) {\n element.classList.remove(\"ox-island-loading\");\n element.classList.add(\"ox-island-error\");\n element.dataset.oxError = error instanceof Error ? error.message : String(error);\n\n opts.onHydrateError?.(\n element,\n config,\n error instanceof Error ? error : new Error(String(error)),\n );\n }\n }\n\n /**\n * Schedule hydration based on load strategy.\n */\n function scheduleHydration(element: HTMLElement, config: IslandConfig): void {\n switch (config.load) {\n case \"eager\":\n hydrateIsland(element, config);\n break;\n\n case \"idle\": {\n const cancel = scheduleIdle(() => hydrateIsland(element, config), opts.idleTimeout);\n cleanups.push(cancel);\n break;\n }\n\n case \"visible\": {\n const cancel = observeVisibility(element, () => hydrateIsland(element, config), {\n rootMargin: opts.rootMargin,\n threshold: opts.threshold,\n });\n cleanups.push(cancel);\n break;\n }\n\n case \"media\": {\n if (config.mediaQuery) {\n const cancel = observeMedia(config.mediaQuery, () => hydrateIsland(element, config));\n cleanups.push(cancel);\n } else {\n // No media query specified, fall back to eager\n hydrateIsland(element, config);\n }\n break;\n }\n\n default:\n hydrateIsland(element, config);\n }\n }\n\n // Find and process all island elements\n const elements = document.querySelectorAll<HTMLElement>(opts.selector);\n\n elements.forEach((element) => {\n const config = parseIslandConfig(element);\n\n // Track instance\n instances.push({\n element,\n config,\n hydrated: false,\n });\n\n // Schedule hydration\n scheduleHydration(element, config);\n });\n\n // Return controller\n return {\n instances,\n\n hydrate(element: HTMLElement): void {\n const config = parseIslandConfig(element);\n hydrateIsland(element, config);\n },\n\n destroy(): void {\n // Cancel pending hydrations\n cleanups.forEach((cleanup) => cleanup());\n cleanups.length = 0;\n\n // Cleanup hydrated instances\n instances.forEach((instance) => {\n if (instance.cleanup) {\n instance.cleanup();\n }\n });\n instances.length = 0;\n },\n };\n}\n\n/**\n * Create a deferred hydration wrapper.\n *\n * Returns a function that can be called to hydrate islands later.\n * Useful for frameworks that need to register components first.\n *\n * @example\n * ```ts\n * const deferredInit = createDeferredInit();\n *\n * // Later, after components are ready\n * const components = await loadComponents();\n * deferredInit((el, props) => {\n * const Component = components[el.dataset.oxIsland!];\n * // ...\n * });\n * ```\n */\nexport function createDeferredInit(\n options?: InitIslandsOptions,\n): (hydrate: HydrateFunction) => IslandController {\n return (hydrate: HydrateFunction) => initIslands(hydrate, options);\n}\n\n/**\n * Check if islands are supported in the current environment.\n */\nexport function isIslandsSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof document !== \"undefined\" &&\n typeof IntersectionObserver !== \"undefined\" &&\n typeof MutationObserver !== \"undefined\"\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runtime2.mjs","names":[],"sources":["../src/payload.ts","../src/runtime.ts"],"sourcesContent":["const RUST_PAYLOAD_KEYS = new Set([\"props\", \"expressions\", \"spreads\"]);\nconst ISLAND_JSON_SCRIPT = /^\\s*<script type=\"application\\/json\">[\\s\\S]*?<\\/script>/;\n\n/**\n * Flatten the Rust MDX island payload (`{ props, expressions, spreads }`)\n * into the literal props object hydrate functions expect. Legacy flat\n * `data-ox-props` objects from the Markdown regex path are returned as-is.\n */\nexport function unwrapIslandProps(parsed: unknown): Record<string, unknown> {\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return {};\n }\n\n const record = parsed as Record<string, unknown>;\n const keys = Object.keys(record);\n if (\n keys.length > 0 &&\n keys.every((key) => RUST_PAYLOAD_KEYS.has(key)) &&\n record.props &&\n typeof record.props === \"object\" &&\n !Array.isArray(record.props)\n ) {\n return record.props as Record<string, unknown>;\n }\n\n return record;\n}\n\n/**\n * Slot markup for an island: `data-ox-content` when the regex path set it,\n * otherwise inner HTML with the Rust JSON payload script stripped.\n */\nexport function stripIslandPayloadScript(innerHTML: string): string {\n return innerHTML.replace(ISLAND_JSON_SCRIPT, \"\");\n}\n\nexport function readIslandSlotHtml(element: Pick<HTMLElement, \"dataset\" | \"innerHTML\">): string {\n const fromAttr = element.dataset.oxContent;\n if (fromAttr) return fromAttr;\n return stripIslandPayloadScript(element.innerHTML);\n}\n","/**\n * Island Architecture Runtime\n *\n * Framework-agnostic Island controller using pure Vanilla JavaScript.\n * No framework dependencies - works with Vue, React, Svelte, or plain JS.\n */\n\nimport { unwrapIslandProps } from \"./payload\";\nimport type {\n HydrateFunction,\n InitIslandsOptions,\n IslandConfig,\n IslandInstance,\n IslandController,\n LoadStrategy,\n} from \"./types\";\n\nconst defaultOptions: Required<\n Omit<InitIslandsOptions, \"onHydrateStart\" | \"onHydrateEnd\" | \"onHydrateError\">\n> &\n InitIslandsOptions = {\n rootMargin: \"200px\",\n threshold: 0,\n idleTimeout: 200,\n selector: \"[data-ox-island]\",\n};\n\n/**\n * Parse island configuration from element attributes.\n */\nfunction parseIslandConfig(element: HTMLElement): IslandConfig {\n const component = element.dataset.oxIsland || \"\";\n const load = (element.dataset.oxLoad as LoadStrategy) || \"eager\";\n const mediaQuery = element.dataset.oxMedia;\n\n let props: Record<string, unknown> = {};\n try {\n const propsJson = element.dataset.oxProps;\n if (propsJson) {\n props = unwrapIslandProps(JSON.parse(propsJson));\n }\n } catch (e) {\n console.warn(\"[ox-islands] Failed to parse props for\", element, e);\n }\n\n return {\n id: element.id || `island-${Math.random().toString(36).slice(2, 9)}`,\n component,\n load,\n mediaQuery,\n props,\n ssr: element.dataset.oxSsr === \"true\",\n };\n}\n\n/**\n * Observe element visibility using IntersectionObserver.\n */\nfunction observeVisibility(\n element: HTMLElement,\n callback: () => void,\n options: { rootMargin: string; threshold: number },\n): () => void {\n const observer = new IntersectionObserver(\n (entries) => {\n if (entries[0].isIntersecting) {\n observer.disconnect();\n callback();\n }\n },\n {\n rootMargin: options.rootMargin,\n threshold: options.threshold,\n },\n );\n\n observer.observe(element);\n\n return () => observer.disconnect();\n}\n\n/**\n * Observe media query changes.\n */\nfunction observeMedia(query: string, callback: () => void): () => void {\n const mql = matchMedia(query);\n\n if (mql.matches) {\n callback();\n return () => {};\n }\n\n const handler = () => {\n if (mql.matches) {\n mql.removeEventListener(\"change\", handler);\n callback();\n }\n };\n\n mql.addEventListener(\"change\", handler);\n\n return () => mql.removeEventListener(\"change\", handler);\n}\n\n/**\n * Schedule callback for browser idle time.\n */\nfunction scheduleIdle(callback: () => void, timeout: number): () => void {\n if (\"requestIdleCallback\" in window) {\n const id = requestIdleCallback(callback, { timeout });\n return () => cancelIdleCallback(id);\n }\n\n // Fallback for Safari and older browsers\n const id = setTimeout(callback, timeout);\n return () => clearTimeout(id);\n}\n\n/**\n * Initialize islands with a hydration function.\n *\n * This is the main entry point for the island system.\n * Pass a hydrate function that knows how to mount your components.\n *\n * @example Vue\n * ```ts\n * import { initIslands } from '@ox-content/islands';\n * import { createApp, h } from 'vue';\n * import Counter from './Counter.vue';\n *\n * const components = { Counter };\n *\n * initIslands((el, props) => {\n * const name = el.dataset.oxIsland!;\n * const Component = components[name];\n * if (!Component) return;\n *\n * const app = createApp({ render: () => h(Component, props) });\n * app.mount(el);\n *\n * return () => app.unmount();\n * });\n * ```\n *\n * @example React\n * ```ts\n * import { initIslands } from '@ox-content/islands';\n * import { createRoot } from 'react-dom/client';\n * import Counter from './Counter';\n *\n * const components = { Counter };\n *\n * initIslands((el, props) => {\n * const name = el.dataset.oxIsland!;\n * const Component = components[name];\n * if (!Component) return;\n *\n * const root = createRoot(el);\n * root.render(<Component {...props} />);\n *\n * return () => root.unmount();\n * });\n * ```\n *\n * @example Vanilla JS\n * ```ts\n * import { initIslands } from '@ox-content/islands';\n *\n * initIslands((el, props) => {\n * const name = el.dataset.oxIsland!;\n *\n * if (name === 'Counter') {\n * let count = props.initial || 0;\n * const button = el.querySelector('button')!;\n * const handler = () => {\n * count++;\n * button.textContent = String(count);\n * };\n * button.addEventListener('click', handler);\n * return () => button.removeEventListener('click', handler);\n * }\n * });\n * ```\n */\nexport function initIslands(\n hydrate: HydrateFunction,\n options?: InitIslandsOptions,\n): IslandController {\n const opts = { ...defaultOptions, ...options };\n const instances: IslandInstance[] = [];\n const cleanups: (() => void)[] = [];\n\n /**\n * Hydrate a single island element.\n */\n function hydrateIsland(element: HTMLElement, config: IslandConfig): void {\n // Find existing instance\n let instance = instances.find((i) => i.element === element);\n if (instance?.hydrated || instance?.hydrating) return;\n if (!instance) {\n instance = {\n element,\n config,\n hydrated: false,\n };\n instances.push(instance);\n }\n\n try {\n opts.onHydrateStart?.(element, config);\n\n // Mark as loading\n element.classList.add(\"ox-island-loading\");\n instance.hydrating = true;\n\n // Call the hydrate function\n const result = hydrate(element, config.props);\n if (isPromiseLike(result)) {\n const cleanup = typeof result === \"function\" ? result : undefined;\n instance.cleanup = cleanup;\n void Promise.resolve(result).then(\n (resolvedCleanup) =>\n finishHydration(\n instance,\n element,\n config,\n cleanup ?? (typeof resolvedCleanup === \"function\" ? resolvedCleanup : undefined),\n ),\n (error) => failHydration(instance, element, config, error),\n );\n return;\n }\n\n finishHydration(instance, element, config, result);\n } catch (error) {\n failHydration(instance, element, config, error);\n }\n }\n\n function finishHydration(\n instance: IslandInstance,\n element: HTMLElement,\n config: IslandConfig,\n cleanup: void | (() => void),\n ): void {\n const normalizedCleanup = cleanup || undefined;\n if (!instances.includes(instance)) {\n normalizedCleanup?.();\n return;\n }\n\n // Mark as hydrated only after synchronous or asynchronous loading/rendering\n // has completed successfully.\n element.dataset.oxHydrated = \"true\";\n element.classList.remove(\"ox-island-loading\");\n\n instance.cleanup = normalizedCleanup;\n instance.hydrated = true;\n instance.hydrating = false;\n\n opts.onHydrateEnd?.(element, config);\n }\n\n function failHydration(\n instance: IslandInstance,\n element: HTMLElement,\n config: IslandConfig,\n error: unknown,\n ): void {\n if (!instances.includes(instance)) return;\n\n element.classList.remove(\"ox-island-loading\");\n element.classList.add(\"ox-island-error\");\n element.dataset.oxError = error instanceof Error ? error.message : String(error);\n instance.cleanup = undefined;\n instance.hydrating = false;\n\n opts.onHydrateError?.(\n element,\n config,\n error instanceof Error ? error : new Error(String(error)),\n );\n }\n\n /**\n * Schedule hydration based on load strategy.\n */\n function scheduleHydration(element: HTMLElement, config: IslandConfig): void {\n switch (config.load) {\n case \"eager\":\n hydrateIsland(element, config);\n break;\n\n case \"idle\": {\n const cancel = scheduleIdle(() => hydrateIsland(element, config), opts.idleTimeout);\n cleanups.push(cancel);\n break;\n }\n\n case \"visible\": {\n const cancel = observeVisibility(element, () => hydrateIsland(element, config), {\n rootMargin: opts.rootMargin,\n threshold: opts.threshold,\n });\n cleanups.push(cancel);\n break;\n }\n\n case \"media\": {\n if (config.mediaQuery) {\n const cancel = observeMedia(config.mediaQuery, () => hydrateIsland(element, config));\n cleanups.push(cancel);\n } else {\n // No media query specified, fall back to eager\n hydrateIsland(element, config);\n }\n break;\n }\n\n default:\n hydrateIsland(element, config);\n }\n }\n\n // Find and process all island elements\n const elements = document.querySelectorAll<HTMLElement>(opts.selector);\n\n elements.forEach((element) => {\n const config = parseIslandConfig(element);\n\n // Track instance\n instances.push({\n element,\n config,\n hydrated: false,\n });\n\n // Schedule hydration\n scheduleHydration(element, config);\n });\n\n // Return controller\n return {\n instances,\n\n hydrate(element: HTMLElement): void {\n const config = parseIslandConfig(element);\n hydrateIsland(element, config);\n },\n\n destroy(): void {\n // Cancel pending hydrations\n cleanups.forEach((cleanup) => cleanup());\n cleanups.length = 0;\n\n // Cleanup hydrated instances\n instances.forEach((instance) => {\n if (instance.cleanup) {\n instance.cleanup();\n }\n });\n instances.length = 0;\n },\n };\n}\n\n/**\n * Create a deferred hydration wrapper.\n *\n * Returns a function that can be called to hydrate islands later.\n * Useful for frameworks that need to register components first.\n *\n * @example\n * ```ts\n * const deferredInit = createDeferredInit();\n *\n * // Later, after components are ready\n * const components = await loadComponents();\n * deferredInit((el, props) => {\n * const Component = components[el.dataset.oxIsland!];\n * // ...\n * });\n * ```\n */\nexport function createDeferredInit(\n options?: InitIslandsOptions,\n): (hydrate: HydrateFunction) => IslandController {\n return (hydrate: HydrateFunction) => initIslands(hydrate, options);\n}\n\n/**\n * Check if islands are supported in the current environment.\n */\nexport function isIslandsSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof document !== \"undefined\" &&\n typeof IntersectionObserver !== \"undefined\" &&\n typeof MutationObserver !== \"undefined\"\n );\n}\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n return (\n value !== null &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as PromiseLike<unknown>).then === \"function\"\n );\n}\n"],"mappings":"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"}
|
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Island Architecture Types
|
|
4
|
+
*
|
|
5
|
+
* Framework-agnostic type definitions for the Island system.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Loading strategy for islands.
|
|
9
|
+
*
|
|
10
|
+
* - `eager`: Hydrate immediately on page load
|
|
11
|
+
* - `idle`: Hydrate when the browser is idle (requestIdleCallback)
|
|
12
|
+
* - `visible`: Hydrate when the element becomes visible (IntersectionObserver)
|
|
13
|
+
* - `media`: Hydrate when a media query matches
|
|
14
|
+
*/
|
|
15
|
+
type LoadStrategy = "eager" | "idle" | "visible" | "media";
|
|
16
|
+
/**
|
|
17
|
+
* Island configuration extracted from data attributes.
|
|
18
|
+
*/
|
|
19
|
+
interface IslandConfig {
|
|
20
|
+
/** Unique island identifier */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Component name to hydrate */
|
|
23
|
+
component: string;
|
|
24
|
+
/** Loading strategy */
|
|
25
|
+
load: LoadStrategy;
|
|
26
|
+
/** Media query for 'media' load strategy */
|
|
27
|
+
mediaQuery?: string;
|
|
28
|
+
/** Component props (JSON serialized in data-ox-props) */
|
|
29
|
+
props: Record<string, unknown>;
|
|
30
|
+
/** Whether the island wrapper already contains server-rendered component HTML. */
|
|
31
|
+
ssr?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Hydration function signature.
|
|
35
|
+
*
|
|
36
|
+
* Called when an island should be hydrated.
|
|
37
|
+
* Returns an optional cleanup function.
|
|
38
|
+
*/
|
|
39
|
+
type HydrateFunction = (element: HTMLElement, props: Record<string, unknown>) => void | (() => void) | PromiseLike<void | (() => void)>;
|
|
40
|
+
/**
|
|
41
|
+
* Component registry mapping component names to hydrate functions.
|
|
42
|
+
*/
|
|
43
|
+
type ComponentRegistry = Map<string, HydrateFunction>;
|
|
44
|
+
/**
|
|
45
|
+
* Options for initializing islands.
|
|
46
|
+
*/
|
|
47
|
+
interface InitIslandsOptions {
|
|
48
|
+
/** Root margin for IntersectionObserver (visible strategy). Default: "200px" */
|
|
49
|
+
rootMargin?: string;
|
|
50
|
+
/** Threshold for IntersectionObserver. Default: 0 */
|
|
51
|
+
threshold?: number;
|
|
52
|
+
/** Timeout for idle callback fallback in ms. Default: 200 */
|
|
53
|
+
idleTimeout?: number;
|
|
54
|
+
/** Custom selector for finding islands. Default: "[data-ox-island]" */
|
|
55
|
+
selector?: string;
|
|
56
|
+
/** Called when an island starts hydrating */
|
|
57
|
+
onHydrateStart?: (element: HTMLElement, config: IslandConfig) => void;
|
|
58
|
+
/** Called when an island finishes hydrating */
|
|
59
|
+
onHydrateEnd?: (element: HTMLElement, config: IslandConfig) => void;
|
|
60
|
+
/** Called when hydration fails */
|
|
61
|
+
onHydrateError?: (element: HTMLElement, config: IslandConfig, error: Error) => void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Island instance tracking.
|
|
65
|
+
*/
|
|
66
|
+
interface IslandInstance {
|
|
67
|
+
element: HTMLElement;
|
|
68
|
+
config: IslandConfig;
|
|
69
|
+
cleanup?: () => void;
|
|
70
|
+
hydrating?: boolean;
|
|
71
|
+
hydrated: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Island controller returned by initIslands.
|
|
75
|
+
*/
|
|
76
|
+
interface IslandController {
|
|
77
|
+
/** All tracked island instances */
|
|
78
|
+
instances: IslandInstance[];
|
|
79
|
+
/** Manually hydrate a specific island */
|
|
80
|
+
hydrate: (element: HTMLElement) => void;
|
|
81
|
+
/** Destroy all islands and cleanup */
|
|
82
|
+
destroy: () => void;
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { IslandController as a, IslandConfig as i, HydrateFunction as n, IslandInstance as o, InitIslandsOptions as r, LoadStrategy as s, ComponentRegistry as t };
|
|
86
|
+
//# sourceMappingURL=types.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.cts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;KAcY;;;;UAKK;;EAEf;;EAEA;;EAEA,MAAM;;EAEN;;EAEA,OAAO;;EAEP;;;;;;;;KASU,mBACV,SAAS,aACT,OAAO,kDACkB;;;;KAKf,oBAAoB,YAAY;;;;UAK3B;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA,kBAAkB,SAAS,aAAa,QAAQ;;EAEhD,gBAAgB,SAAS,aAAa,QAAQ;;EAE9C,kBAAkB,SAAS,aAAa,QAAQ,cAAc,OAAO;;;;;UAMtD;EACf,SAAS;EACT,QAAQ;EACR;EACA;EACA;;;;;UAMe;;EAEf,WAAW;;EAEX,UAAU,SAAS;;EAEnB"}
|