@ilha/router 0.2.6 → 0.3.1
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/README.md +86 -0
- package/dist/index-BsCpWQzC.d.ts +254 -0
- package/dist/index.d.ts +2 -236
- package/dist/index.js +2 -637
- package/dist/src-C2qmhASZ.js +776 -0
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,637 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
//#region src/index.ts
|
|
4
|
-
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
|
|
5
|
-
/**
|
|
6
|
-
* Identity function for declaring a loader. Exists purely as a type anchor and
|
|
7
|
-
* a marker for the Vite plugin to detect by export name.
|
|
8
|
-
*/
|
|
9
|
-
function loader(fn) {
|
|
10
|
-
return fn;
|
|
11
|
-
}
|
|
12
|
-
var Redirect = class {
|
|
13
|
-
__ilhaRedirect = true;
|
|
14
|
-
to;
|
|
15
|
-
status;
|
|
16
|
-
constructor(to, status = 302) {
|
|
17
|
-
this.to = to;
|
|
18
|
-
this.status = status;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
var LoaderError = class {
|
|
22
|
-
__ilhaLoaderError = true;
|
|
23
|
-
status;
|
|
24
|
-
message;
|
|
25
|
-
constructor(status, message) {
|
|
26
|
-
this.status = status;
|
|
27
|
-
this.message = message;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
function redirect(to, status = 302) {
|
|
31
|
-
throw new Redirect(to, status);
|
|
32
|
-
}
|
|
33
|
-
function error(status, message) {
|
|
34
|
-
throw new LoaderError(status, message);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Compose a list of loaders into a single loader. Later loaders win on key
|
|
38
|
-
* collision (page loader overrides layout loader for the same key). All loaders
|
|
39
|
-
* run concurrently within a chain since they share the same abort signal and
|
|
40
|
-
* request — re-fetching is cheap with a request-scoped cache (future work).
|
|
41
|
-
*
|
|
42
|
-
* For v1 we run them in parallel via `Promise.all`. If a loader throws a
|
|
43
|
-
* `Redirect` or `LoaderError`, the composed loader re-throws it unchanged.
|
|
44
|
-
*/
|
|
45
|
-
function composeLoaders(loaders) {
|
|
46
|
-
if (loaders.length === 0) return async () => ({});
|
|
47
|
-
if (loaders.length === 1) return loaders[0];
|
|
48
|
-
return async (ctx) => {
|
|
49
|
-
const results = await Promise.all(loaders.map((l) => l(ctx)));
|
|
50
|
-
return Object.assign({}, ...results);
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function wrapLayout(layout, page) {
|
|
54
|
-
return layout(page);
|
|
55
|
-
}
|
|
56
|
-
function wrapError(handler, page) {
|
|
57
|
-
const Wrapper = ilha.render(() => {
|
|
58
|
-
try {
|
|
59
|
-
return page.toString();
|
|
60
|
-
} catch (e) {
|
|
61
|
-
const route = {
|
|
62
|
-
path: routePath(),
|
|
63
|
-
params: routeParams(),
|
|
64
|
-
search: routeSearch(),
|
|
65
|
-
hash: routeHash()
|
|
66
|
-
};
|
|
67
|
-
return handler({
|
|
68
|
-
message: e.message,
|
|
69
|
-
status: e.status,
|
|
70
|
-
stack: e.stack
|
|
71
|
-
}, route).toString();
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
Wrapper.mount = (host, props) => {
|
|
75
|
-
try {
|
|
76
|
-
return page.mount(host, props);
|
|
77
|
-
} catch (e) {
|
|
78
|
-
const route = {
|
|
79
|
-
path: routePath(),
|
|
80
|
-
params: routeParams(),
|
|
81
|
-
search: routeSearch(),
|
|
82
|
-
hash: routeHash()
|
|
83
|
-
};
|
|
84
|
-
const errorIsland = handler({
|
|
85
|
-
message: e.message,
|
|
86
|
-
status: e.status,
|
|
87
|
-
stack: e.stack
|
|
88
|
-
}, route);
|
|
89
|
-
host.innerHTML = errorIsland.toString();
|
|
90
|
-
return errorIsland.mount(host, props);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
return Wrapper;
|
|
94
|
-
}
|
|
95
|
-
function defineLayout(layout) {
|
|
96
|
-
return layout;
|
|
97
|
-
}
|
|
98
|
-
function buildReverseRegistry(registry) {
|
|
99
|
-
const map = /* @__PURE__ */ new Map();
|
|
100
|
-
for (const [name, island] of Object.entries(registry)) if (!map.has(island)) map.set(island, name);
|
|
101
|
-
return map;
|
|
102
|
-
}
|
|
103
|
-
/** Path of the loader endpoint served by the Vite plugin / production adapter. */
|
|
104
|
-
const LOADER_ENDPOINT = "/__ilha/loader";
|
|
105
|
-
/** In-memory cache for prefetched loader data, keyed by path+search. */
|
|
106
|
-
const prefetchCache = /* @__PURE__ */ new Map();
|
|
107
|
-
async function fetchLoaderData(pathWithSearch, signal) {
|
|
108
|
-
const cached = prefetchCache.get(pathWithSearch);
|
|
109
|
-
if (cached) {
|
|
110
|
-
prefetchCache.delete(pathWithSearch);
|
|
111
|
-
try {
|
|
112
|
-
return await cached;
|
|
113
|
-
} catch {}
|
|
114
|
-
}
|
|
115
|
-
const url = `${LOADER_ENDPOINT}?path=${encodeURIComponent(pathWithSearch)}`;
|
|
116
|
-
try {
|
|
117
|
-
const res = await fetch(url, {
|
|
118
|
-
signal,
|
|
119
|
-
headers: { accept: "application/json" }
|
|
120
|
-
});
|
|
121
|
-
if (!res.ok) {
|
|
122
|
-
try {
|
|
123
|
-
const body = await res.json();
|
|
124
|
-
if (body && typeof body === "object" && "kind" in body) return body;
|
|
125
|
-
} catch {}
|
|
126
|
-
return {
|
|
127
|
-
kind: "error",
|
|
128
|
-
status: res.status,
|
|
129
|
-
message: res.statusText
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return await res.json();
|
|
133
|
-
} catch (e) {
|
|
134
|
-
if (e?.name === "AbortError") throw e;
|
|
135
|
-
return {
|
|
136
|
-
kind: "error",
|
|
137
|
-
status: 0,
|
|
138
|
-
message: e?.message ?? "network error"
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Prefetch loader data for a given path. Safe to call repeatedly — a single
|
|
144
|
-
* inflight request is reused until it either resolves (and is consumed by
|
|
145
|
-
* navigation) or is superseded by another prefetch.
|
|
146
|
-
*/
|
|
147
|
-
function prefetch(pathWithSearch) {
|
|
148
|
-
if (!isBrowser) return;
|
|
149
|
-
if (prefetchCache.has(pathWithSearch)) return;
|
|
150
|
-
const pathOnly = pathWithSearch.split("?")[0] ?? "";
|
|
151
|
-
if (!findRoute(_rou3, "GET", pathOnly)?.data?.loader) return;
|
|
152
|
-
const promise = fetchLoaderData(pathWithSearch).catch((e) => {
|
|
153
|
-
return {
|
|
154
|
-
kind: "error",
|
|
155
|
-
status: 0,
|
|
156
|
-
message: e?.message ?? "prefetch failed"
|
|
157
|
-
};
|
|
158
|
-
});
|
|
159
|
-
prefetchCache.set(pathWithSearch, promise);
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Mounts a route island with proper hydration for client-side navigation.
|
|
163
|
-
* Looks up the island in the reverse registry, runs the loader (via fetch),
|
|
164
|
-
* renders it with hydration markers, and mounts it for interactivity.
|
|
165
|
-
*/
|
|
166
|
-
async function mountRouteWithHydration(island, host, pathWithSearch, signal, registry, reverseRegistry) {
|
|
167
|
-
if (!island) {
|
|
168
|
-
host.innerHTML = `<div data-router-empty></div>`;
|
|
169
|
-
return () => {};
|
|
170
|
-
}
|
|
171
|
-
const hasLoader = !!findRoute(_rou3, "GET", pathWithSearch.split("?")[0] ?? "")?.data?.loader;
|
|
172
|
-
let props = {};
|
|
173
|
-
const loaderResult = hasLoader ? await fetchLoaderData(pathWithSearch, signal) : {
|
|
174
|
-
kind: "data",
|
|
175
|
-
data: {}
|
|
176
|
-
};
|
|
177
|
-
if (loaderResult.kind === "redirect") {
|
|
178
|
-
navigate(loaderResult.to, { replace: true });
|
|
179
|
-
return () => {};
|
|
180
|
-
}
|
|
181
|
-
if (loaderResult.kind === "error") {
|
|
182
|
-
const escaped = String(loaderResult.message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
183
|
-
host.innerHTML = `<div data-router-view data-router-error="${loaderResult.status}">${escaped}</div>`;
|
|
184
|
-
return () => {};
|
|
185
|
-
}
|
|
186
|
-
if (loaderResult.kind === "not-found") {
|
|
187
|
-
host.innerHTML = `<div data-router-empty></div>`;
|
|
188
|
-
return () => {};
|
|
189
|
-
}
|
|
190
|
-
props = loaderResult.data;
|
|
191
|
-
if (!registry) {
|
|
192
|
-
console.warn("[ilha-router] No registry provided for client-side navigation. Island will not be interactive.");
|
|
193
|
-
host.innerHTML = `<div data-router-view>${island.toString(props)}</div>`;
|
|
194
|
-
return () => {};
|
|
195
|
-
}
|
|
196
|
-
const name = reverseRegistry?.get(island) ?? Object.entries(registry).find(([, v]) => v === island)?.[0];
|
|
197
|
-
if (!name) {
|
|
198
|
-
console.warn("[ilha-router] Island not found in registry for client-side navigation.");
|
|
199
|
-
host.innerHTML = `<div data-router-view>${island.toString(props)}</div>`;
|
|
200
|
-
return () => {};
|
|
201
|
-
}
|
|
202
|
-
host.innerHTML = `<div data-router-view>${await island.hydratable(props, {
|
|
203
|
-
name,
|
|
204
|
-
as: "div",
|
|
205
|
-
snapshot: true
|
|
206
|
-
})}</div>`;
|
|
207
|
-
const islandHost = host.querySelector(`[data-ilha="${name}"]`);
|
|
208
|
-
if (islandHost) return island.mount(islandHost);
|
|
209
|
-
return () => {};
|
|
210
|
-
}
|
|
211
|
-
const routePath = context("router.path", "");
|
|
212
|
-
const routeParams = context("router.params", {});
|
|
213
|
-
const routeSearch = context("router.search", "");
|
|
214
|
-
const routeHash = context("router.hash", "");
|
|
215
|
-
function useRoute() {
|
|
216
|
-
return {
|
|
217
|
-
path: routePath,
|
|
218
|
-
params: routeParams,
|
|
219
|
-
search: routeSearch,
|
|
220
|
-
hash: routeHash
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
const activeIsland = context("router.active", null);
|
|
224
|
-
let _records = [];
|
|
225
|
-
let _rou3 = createRouter();
|
|
226
|
-
let _islandToPattern = /* @__PURE__ */ new Map();
|
|
227
|
-
let _patternToData = /* @__PURE__ */ new Map();
|
|
228
|
-
function extractParams(matchParams) {
|
|
229
|
-
const params = {};
|
|
230
|
-
if (matchParams) for (const [k, v] of Object.entries(matchParams)) params[k] = decodeURIComponent(v);
|
|
231
|
-
return params;
|
|
232
|
-
}
|
|
233
|
-
function syncRouteFromURL(url) {
|
|
234
|
-
const parsed = typeof url === "string" ? new URL(url, "http://localhost") : url;
|
|
235
|
-
const match = findRoute(_rou3, "GET", parsed.pathname);
|
|
236
|
-
routePath(parsed.pathname);
|
|
237
|
-
routeParams(extractParams(match?.params));
|
|
238
|
-
routeSearch(parsed.search);
|
|
239
|
-
routeHash(parsed.hash);
|
|
240
|
-
activeIsland(match?.data?.island ?? null);
|
|
241
|
-
}
|
|
242
|
-
/** Client-only fast path — reads directly from `location` instead of parsing a URL. */
|
|
243
|
-
function syncRouteFromLocation() {
|
|
244
|
-
const match = findRoute(_rou3, "GET", location.pathname);
|
|
245
|
-
routePath(location.pathname);
|
|
246
|
-
routeParams(extractParams(match?.params));
|
|
247
|
-
routeSearch(location.search);
|
|
248
|
-
routeHash(location.hash);
|
|
249
|
-
activeIsland(match?.data?.island ?? null);
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Prime route context signals from the current `location` so that islands
|
|
253
|
-
* hydrated by `ilha.mount()` see the correct route values on their first
|
|
254
|
-
* render — preventing a mismatch morph that would destroy hydrated bindings.
|
|
255
|
-
*/
|
|
256
|
-
function prime() {
|
|
257
|
-
if (isBrowser) syncRouteFromLocation();
|
|
258
|
-
}
|
|
259
|
-
function navigate(to, opts = {}) {
|
|
260
|
-
if (!isBrowser) return;
|
|
261
|
-
if (to === location.pathname + location.search + location.hash) return;
|
|
262
|
-
if (opts.replace) history.replaceState(null, "", to);
|
|
263
|
-
else history.pushState(null, "", to);
|
|
264
|
-
syncRouteFromLocation();
|
|
265
|
-
}
|
|
266
|
-
function enableLinkInterception(root = document, options = {}) {
|
|
267
|
-
if (!isBrowser) return () => {};
|
|
268
|
-
const prefetchEnabled = options.prefetch !== false;
|
|
269
|
-
/** Determine whether this anchor is a same-origin in-app link we should handle. */
|
|
270
|
-
function eligibleLink(target, e) {
|
|
271
|
-
const href = target.getAttribute("href");
|
|
272
|
-
if (!href) return false;
|
|
273
|
-
const isAnchorOnly = href.startsWith("#");
|
|
274
|
-
const isBlank = target.getAttribute("target") === "_blank";
|
|
275
|
-
const hasModifier = !!e && (e.ctrlKey || e.metaKey || e.shiftKey);
|
|
276
|
-
const isExternal = !!target.hostname && (target.hostname !== location.hostname || target.protocol !== location.protocol);
|
|
277
|
-
const hasNoIntercept = target.hasAttribute("data-no-intercept");
|
|
278
|
-
return !(isExternal || isAnchorOnly || isBlank || hasModifier || hasNoIntercept);
|
|
279
|
-
}
|
|
280
|
-
const clickHandler = (e) => {
|
|
281
|
-
if (e.defaultPrevented) return;
|
|
282
|
-
const target = e.target.closest("a");
|
|
283
|
-
if (!target) return;
|
|
284
|
-
if (!eligibleLink(target, e)) return;
|
|
285
|
-
e.preventDefault();
|
|
286
|
-
navigate(target.pathname + target.search + target.hash);
|
|
287
|
-
};
|
|
288
|
-
const hoverHandler = (e) => {
|
|
289
|
-
const target = e.target.closest("a");
|
|
290
|
-
if (!target) return;
|
|
291
|
-
const flag = target.getAttribute("data-prefetch");
|
|
292
|
-
if (flag === null || flag === "false") return;
|
|
293
|
-
if (!eligibleLink(target)) return;
|
|
294
|
-
prefetch(target.pathname + target.search);
|
|
295
|
-
};
|
|
296
|
-
root.addEventListener("click", clickHandler);
|
|
297
|
-
if (prefetchEnabled) root.addEventListener("mouseover", hoverHandler, { passive: true });
|
|
298
|
-
return () => {
|
|
299
|
-
root.removeEventListener("click", clickHandler);
|
|
300
|
-
if (prefetchEnabled) root.removeEventListener("mouseover", hoverHandler);
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
const RouterView = ilha.render(() => {
|
|
304
|
-
const island = activeIsland();
|
|
305
|
-
if (!island) return `<div data-router-empty></div>`;
|
|
306
|
-
return `<div data-router-view>${island.toString()}</div>`;
|
|
307
|
-
});
|
|
308
|
-
const RouterLink = ilha.state("href", "").state("label", "").on("[data-link]@click", ({ state, event }) => {
|
|
309
|
-
event.preventDefault();
|
|
310
|
-
navigate(state.href());
|
|
311
|
-
}).on("[data-link]@mouseenter", ({ state }) => {
|
|
312
|
-
const href = state.href();
|
|
313
|
-
if (!href) return;
|
|
314
|
-
if (/^https?:\/\//i.test(href)) try {
|
|
315
|
-
const u = new URL(href);
|
|
316
|
-
if (u.origin !== location.origin) return;
|
|
317
|
-
prefetch(u.pathname + u.search);
|
|
318
|
-
return;
|
|
319
|
-
} catch {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
prefetch(href);
|
|
323
|
-
}).render(({ state }) => html`<a data-link data-prefetch href="${state.href}">${state.label}</a>`);
|
|
324
|
-
function isActive(pattern) {
|
|
325
|
-
const match = findRoute(_rou3, "GET", routePath());
|
|
326
|
-
if (!match) return false;
|
|
327
|
-
return _islandToPattern.get(match.data.island) === pattern;
|
|
328
|
-
}
|
|
329
|
-
function parsedURL(url) {
|
|
330
|
-
return typeof url === "string" ? new URL(url, "http://localhost") : url;
|
|
331
|
-
}
|
|
332
|
-
function defaultRequest(url) {
|
|
333
|
-
try {
|
|
334
|
-
return new Request(url.toString());
|
|
335
|
-
} catch {
|
|
336
|
-
return {
|
|
337
|
-
url: url.toString(),
|
|
338
|
-
headers: new Headers()
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
async function executeLoader(loader, url, params, request, signal) {
|
|
343
|
-
try {
|
|
344
|
-
return {
|
|
345
|
-
kind: "data",
|
|
346
|
-
data: await loader({
|
|
347
|
-
params,
|
|
348
|
-
request,
|
|
349
|
-
url,
|
|
350
|
-
signal
|
|
351
|
-
}) ?? {}
|
|
352
|
-
};
|
|
353
|
-
} catch (e) {
|
|
354
|
-
if (e instanceof Redirect) return {
|
|
355
|
-
kind: "redirect",
|
|
356
|
-
to: e.to,
|
|
357
|
-
status: e.status
|
|
358
|
-
};
|
|
359
|
-
if (e instanceof LoaderError) return {
|
|
360
|
-
kind: "error",
|
|
361
|
-
status: e.status,
|
|
362
|
-
message: e.message
|
|
363
|
-
};
|
|
364
|
-
return {
|
|
365
|
-
kind: "error",
|
|
366
|
-
status: e?.status ?? 500,
|
|
367
|
-
message: e?.message ?? "Loader failed"
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
function router() {
|
|
372
|
-
_records = [];
|
|
373
|
-
_rou3 = createRouter();
|
|
374
|
-
_islandToPattern = /* @__PURE__ */ new Map();
|
|
375
|
-
_patternToData = /* @__PURE__ */ new Map();
|
|
376
|
-
let _popstateCleanup = null;
|
|
377
|
-
let _linkCleanup = null;
|
|
378
|
-
const builder = {
|
|
379
|
-
route(pattern, island, loader) {
|
|
380
|
-
const data = {
|
|
381
|
-
island,
|
|
382
|
-
loader
|
|
383
|
-
};
|
|
384
|
-
_records.push({
|
|
385
|
-
pattern,
|
|
386
|
-
island,
|
|
387
|
-
loader
|
|
388
|
-
});
|
|
389
|
-
addRoute(_rou3, "GET", pattern, data);
|
|
390
|
-
_patternToData.set(pattern, data);
|
|
391
|
-
if (!_islandToPattern.has(island)) _islandToPattern.set(island, pattern);
|
|
392
|
-
return builder;
|
|
393
|
-
},
|
|
394
|
-
attachLoader(pattern, loader) {
|
|
395
|
-
const data = _patternToData.get(pattern);
|
|
396
|
-
if (!data) {
|
|
397
|
-
console.warn(`[ilha-router] attachLoader("${pattern}", …): pattern was never registered via .route(). The loader will be ignored.`);
|
|
398
|
-
return builder;
|
|
399
|
-
}
|
|
400
|
-
data.loader = loader;
|
|
401
|
-
const rec = _records.find((r) => r.pattern === pattern);
|
|
402
|
-
if (rec) rec.loader = loader;
|
|
403
|
-
return builder;
|
|
404
|
-
},
|
|
405
|
-
prime,
|
|
406
|
-
mount(target, { hydrate = false, registry } = {}) {
|
|
407
|
-
if (!isBrowser) {
|
|
408
|
-
console.warn("[ilha-router] mount() called in a non-browser environment");
|
|
409
|
-
return () => {};
|
|
410
|
-
}
|
|
411
|
-
const host = typeof target === "string" ? document.querySelector(target) : target;
|
|
412
|
-
if (!host) {
|
|
413
|
-
console.warn(`[ilha-router] No element found for selector "${target}"`);
|
|
414
|
-
return () => {};
|
|
415
|
-
}
|
|
416
|
-
syncRouteFromLocation();
|
|
417
|
-
const popHandler = () => syncRouteFromLocation();
|
|
418
|
-
window.addEventListener("popstate", popHandler);
|
|
419
|
-
_popstateCleanup = () => window.removeEventListener("popstate", popHandler);
|
|
420
|
-
_linkCleanup = enableLinkInterception(document);
|
|
421
|
-
let unmountView = null;
|
|
422
|
-
let navAbort = null;
|
|
423
|
-
if (hydrate) {
|
|
424
|
-
const viewHost = host.querySelector("[data-router-view]") ?? host;
|
|
425
|
-
let currentMountedIsland = activeIsland();
|
|
426
|
-
const reverseRegistry = registry ? buildReverseRegistry(registry) : void 0;
|
|
427
|
-
let navVersion = 0;
|
|
428
|
-
const NavHandler = ilha.render(() => {
|
|
429
|
-
const current = activeIsland();
|
|
430
|
-
if (current !== currentMountedIsland) {
|
|
431
|
-
const thisNav = ++navVersion;
|
|
432
|
-
navAbort?.abort();
|
|
433
|
-
navAbort = new AbortController();
|
|
434
|
-
const signal = navAbort.signal;
|
|
435
|
-
queueMicrotask(async () => {
|
|
436
|
-
if (thisNav !== navVersion) return;
|
|
437
|
-
unmountView?.();
|
|
438
|
-
try {
|
|
439
|
-
unmountView = await mountRouteWithHydration(current, viewHost, location.pathname + location.search, signal, registry, reverseRegistry);
|
|
440
|
-
} catch (e) {
|
|
441
|
-
if (e?.name === "AbortError") return;
|
|
442
|
-
throw e;
|
|
443
|
-
}
|
|
444
|
-
currentMountedIsland = current;
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
return "";
|
|
448
|
-
});
|
|
449
|
-
const navHost = document.createElement("div");
|
|
450
|
-
navHost.style.display = "none";
|
|
451
|
-
host.appendChild(navHost);
|
|
452
|
-
const unmountNavHandler = NavHandler.mount(navHost);
|
|
453
|
-
return () => {
|
|
454
|
-
++navVersion;
|
|
455
|
-
navAbort?.abort();
|
|
456
|
-
unmountNavHandler();
|
|
457
|
-
navHost.remove();
|
|
458
|
-
unmountView?.();
|
|
459
|
-
_popstateCleanup?.();
|
|
460
|
-
_linkCleanup?.();
|
|
461
|
-
_popstateCleanup = null;
|
|
462
|
-
_linkCleanup = null;
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
let unmountIsland = null;
|
|
466
|
-
let currentMountedIsland = null;
|
|
467
|
-
let navVersion = 0;
|
|
468
|
-
unmountView = RouterView.mount(host);
|
|
469
|
-
/**
|
|
470
|
-
* Fetch loader data and mount the active island. SPA mode also fetches
|
|
471
|
-
* from the loader endpoint — otherwise navigation after the initial SSR
|
|
472
|
-
* render would have no access to loader data.
|
|
473
|
-
*/
|
|
474
|
-
async function mountActiveIsland(island, signal) {
|
|
475
|
-
unmountIsland?.();
|
|
476
|
-
unmountIsland = null;
|
|
477
|
-
currentMountedIsland = island;
|
|
478
|
-
if (!island) return;
|
|
479
|
-
const viewHost = host?.querySelector("[data-router-view]");
|
|
480
|
-
if (!viewHost) return;
|
|
481
|
-
const result = !!findRoute(_rou3, "GET", location.pathname)?.data?.loader ? await fetchLoaderData(location.pathname + location.search, signal) : {
|
|
482
|
-
kind: "data",
|
|
483
|
-
data: {}
|
|
484
|
-
};
|
|
485
|
-
if (signal.aborted) return;
|
|
486
|
-
if (result.kind === "redirect") {
|
|
487
|
-
navigate(result.to, { replace: true });
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
const props = result.kind === "data" ? result.data : {};
|
|
491
|
-
viewHost.innerHTML = island.toString(props);
|
|
492
|
-
unmountIsland = island.mount(viewHost, props);
|
|
493
|
-
}
|
|
494
|
-
navAbort = new AbortController();
|
|
495
|
-
mountActiveIsland(activeIsland(), navAbort.signal);
|
|
496
|
-
const NavHandler = ilha.render(() => {
|
|
497
|
-
const current = activeIsland();
|
|
498
|
-
if (current !== currentMountedIsland) {
|
|
499
|
-
const thisNav = ++navVersion;
|
|
500
|
-
navAbort?.abort();
|
|
501
|
-
navAbort = new AbortController();
|
|
502
|
-
const signal = navAbort.signal;
|
|
503
|
-
queueMicrotask(() => {
|
|
504
|
-
if (thisNav !== navVersion) return;
|
|
505
|
-
mountActiveIsland(current, signal);
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
return "";
|
|
509
|
-
});
|
|
510
|
-
const navHost = document.createElement("div");
|
|
511
|
-
navHost.style.display = "none";
|
|
512
|
-
host.appendChild(navHost);
|
|
513
|
-
const unmountNavHandler = NavHandler.mount(navHost);
|
|
514
|
-
return () => {
|
|
515
|
-
++navVersion;
|
|
516
|
-
navAbort?.abort();
|
|
517
|
-
unmountIsland?.();
|
|
518
|
-
unmountNavHandler();
|
|
519
|
-
navHost.remove();
|
|
520
|
-
unmountView?.();
|
|
521
|
-
_popstateCleanup?.();
|
|
522
|
-
_linkCleanup?.();
|
|
523
|
-
_popstateCleanup = null;
|
|
524
|
-
_linkCleanup = null;
|
|
525
|
-
};
|
|
526
|
-
},
|
|
527
|
-
render(url) {
|
|
528
|
-
syncRouteFromURL(url);
|
|
529
|
-
return RouterView.toString();
|
|
530
|
-
},
|
|
531
|
-
async renderHydratable(url, registry, options = {}, request) {
|
|
532
|
-
const response = await this.renderResponse(url, registry, options, request);
|
|
533
|
-
if (response.kind === "html") return response.html;
|
|
534
|
-
if (response.kind === "error") return response.html;
|
|
535
|
-
return `<meta http-equiv="refresh" content="0; url=${response.to}">`;
|
|
536
|
-
},
|
|
537
|
-
async renderResponse(url, registry, options = {}, request) {
|
|
538
|
-
const parsed = parsedURL(url);
|
|
539
|
-
syncRouteFromURL(parsed);
|
|
540
|
-
const match = findRoute(_rou3, "GET", parsed.pathname);
|
|
541
|
-
const island = match?.data?.island ?? null;
|
|
542
|
-
if (!island) return {
|
|
543
|
-
kind: "html",
|
|
544
|
-
html: `<div data-router-empty></div>`,
|
|
545
|
-
status: 404
|
|
546
|
-
};
|
|
547
|
-
let props = {};
|
|
548
|
-
if (match?.data?.loader) {
|
|
549
|
-
const req = request ?? defaultRequest(parsed);
|
|
550
|
-
const ctrl = new AbortController();
|
|
551
|
-
const result = await executeLoader(match.data.loader, parsed, routeParams(), req, ctrl.signal);
|
|
552
|
-
if (result.kind === "redirect") return {
|
|
553
|
-
kind: "redirect",
|
|
554
|
-
to: result.to,
|
|
555
|
-
status: result.status
|
|
556
|
-
};
|
|
557
|
-
if (result.kind === "error") {
|
|
558
|
-
const escapedMessage = String(result.message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
559
|
-
const html = `<div data-router-view data-router-error="${result.status}">${escapedMessage}</div>`;
|
|
560
|
-
return {
|
|
561
|
-
kind: "error",
|
|
562
|
-
status: result.status,
|
|
563
|
-
message: result.message,
|
|
564
|
-
html
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
props = result.data;
|
|
568
|
-
}
|
|
569
|
-
const name = buildReverseRegistry(registry).get(island);
|
|
570
|
-
if (!name) {
|
|
571
|
-
console.warn(`[ilha-router] renderHydratable: active island for "${routePath()}" is not in the registry. Falling back to plain SSR — the island will not be interactive on the client.`);
|
|
572
|
-
return {
|
|
573
|
-
kind: "html",
|
|
574
|
-
html: `<div data-router-view>${island.toString(props)}</div>`
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
return {
|
|
578
|
-
kind: "html",
|
|
579
|
-
html: `<div data-router-view>${await island.hydratable(props, {
|
|
580
|
-
name,
|
|
581
|
-
as: "div",
|
|
582
|
-
snapshot: true,
|
|
583
|
-
...options
|
|
584
|
-
})}</div>`
|
|
585
|
-
};
|
|
586
|
-
},
|
|
587
|
-
async runLoader(url, request) {
|
|
588
|
-
const parsed = parsedURL(url);
|
|
589
|
-
const match = findRoute(_rou3, "GET", parsed.pathname);
|
|
590
|
-
if (!match?.data?.island) return { kind: "not-found" };
|
|
591
|
-
if (!match.data.loader) return {
|
|
592
|
-
kind: "data",
|
|
593
|
-
data: {}
|
|
594
|
-
};
|
|
595
|
-
const params = extractParams(match.params);
|
|
596
|
-
const req = request ?? defaultRequest(parsed);
|
|
597
|
-
const ctrl = new AbortController();
|
|
598
|
-
return executeLoader(match.data.loader, parsed, params, req, ctrl.signal);
|
|
599
|
-
},
|
|
600
|
-
hydrate(registry, options = {}) {
|
|
601
|
-
if (!isBrowser) {
|
|
602
|
-
console.warn("[ilha-router] hydrate() called in a non-browser environment");
|
|
603
|
-
return () => {};
|
|
604
|
-
}
|
|
605
|
-
const root = options.root ?? document.body;
|
|
606
|
-
const target = options.target ?? root;
|
|
607
|
-
prime();
|
|
608
|
-
const { unmount } = mount(registry, { root });
|
|
609
|
-
const unmountRouter = this.mount(target, {
|
|
610
|
-
hydrate: true,
|
|
611
|
-
registry
|
|
612
|
-
});
|
|
613
|
-
return () => {
|
|
614
|
-
unmount();
|
|
615
|
-
unmountRouter();
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
return builder;
|
|
620
|
-
}
|
|
621
|
-
var src_default = {
|
|
622
|
-
router,
|
|
623
|
-
navigate,
|
|
624
|
-
useRoute,
|
|
625
|
-
isActive,
|
|
626
|
-
enableLinkInterception,
|
|
627
|
-
prime,
|
|
628
|
-
prefetch,
|
|
629
|
-
RouterView,
|
|
630
|
-
RouterLink,
|
|
631
|
-
loader,
|
|
632
|
-
redirect,
|
|
633
|
-
error,
|
|
634
|
-
composeLoaders
|
|
635
|
-
};
|
|
636
|
-
//#endregion
|
|
637
|
-
export { LOADER_ENDPOINT, LoaderError, Redirect, RouterLink, RouterView, composeLoaders, src_default as default, defineLayout, enableLinkInterception, error, isActive, loader, navigate, prefetch, prime, redirect, routeHash, routeParams, routePath, routeSearch, router, useRoute, wrapError, wrapLayout };
|
|
1
|
+
import { C as wrapError, E as setHistoryMode, S as useRoute, T as getHistoryMode, _ as routeParams, a as RouterView, b as router, c as enableLinkInterception, d as loader, f as navigate, g as routeHash, h as redirect, i as RouterLink, l as error, m as prime, n as LoaderError, o as composeLoaders, p as prefetch, r as Redirect, s as defineLayout, t as LOADER_ENDPOINT, u as isActive, v as routePath, w as wrapLayout, x as src_default, y as routeSearch } from "./src-C2qmhASZ.js";
|
|
2
|
+
export { LOADER_ENDPOINT, LoaderError, Redirect, RouterLink, RouterView, composeLoaders, src_default as default, defineLayout, enableLinkInterception, error, getHistoryMode, isActive, loader, navigate, prefetch, prime, redirect, routeHash, routeParams, routePath, routeSearch, router, setHistoryMode, useRoute, wrapError, wrapLayout };
|