@ilha/router 0.6.9 → 0.8.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/README.md CHANGED
@@ -135,10 +135,19 @@ You can still register loaders, but they run on the client (via the loader endpo
135
135
 
136
136
  ## Core API
137
137
 
138
- ### `router()`
138
+ ### `router(options?)`
139
139
 
140
140
  Creates a new router instance and **resets the route registry**. Always call `router()` fresh — never share instances across server requests.
141
141
 
142
+ | Option | Type | Default | Description |
143
+ | ------------------------ | ------------------- | ------- | ----------------------------------------------------------------------------------------- |
144
+ | `mode` | `"spa" \| "static"` | `"spa"` | `"static"` disables client navigation — hydrate with `hydrateStatic()` |
145
+ | `interceptLinks` | `boolean` | `true` | Intercept internal `<a>` clicks for SPA navigation |
146
+ | `notFound` | `Island` | — | Custom 404 island (SSR status 404; mounted with a full lifecycle in the browser) |
147
+ | `allowExternalRedirects` | `boolean` | `false` | Allow loader `redirect()` to cross-origin URLs; blocked targets become a 500 error |
148
+ | `loaderTimeout` | `number` | — | Abort + fail a loader after this many ms (enforced even if the loader ignores its signal) |
149
+ | `viewTransitions` | `boolean` | `false` | Wrap client view swaps in `document.startViewTransition()` when supported |
150
+
142
151
  Returns a `RouterBuilder`.
143
152
 
144
153
  ---
@@ -147,7 +156,9 @@ Returns a `RouterBuilder`.
147
156
 
148
157
  Registers a route. Patterns are matched in **declaration order** — first match wins. Uses [rou3](https://github.com/h3js/rou3) for matching, the same engine as Nitro.
149
158
 
150
- The optional `loader` is a data-fetching function that runs before the page renders. Its return value is passed as input props to the island. On the client, loaders are fetched via the `/__ilha/loader` endpoint on navigation.
159
+ The optional `loader` is a data-fetching function that runs before the page renders. Its return value is passed as input props to the island. The loader runs **wherever the router runs**: during SSR it executes on the server; when the route was registered in the browser (a plain SPA, hash mode, `file://`), client navigations execute it locally — no server or `/__ilha/loader` endpoint needed. Routes marked via `.markLoader()` (the SSR-split pages build) still fetch from the endpoint.
160
+
161
+ A locally-executed loader receives a synthetic `Request` (no cookies or server context) — rely on `url`, `params`, and `signal`. Loader `redirect()`s are checked against the same cross-origin policy as on the server (`allowExternalRedirects`).
151
162
 
152
163
  ```ts
153
164
  import { loader } from "@ilha/router";
@@ -344,6 +355,35 @@ router().route("/user/:id", userPage).attachLoader("/user/:id", serverLoader);
344
355
 
345
356
  ---
346
357
 
358
+ #### `.clientLoader(pattern, loader)` — runtime
359
+
360
+ Attaches a loader that runs **in the browser** on client navigations, instead of fetching from the `/__ilha/loader` endpoint. Used by the FS-routing codegen for `clientLoad` exports; also available for manual routers. When a route has both a server loader and a client loader, the client loader wins on client navigations and the server loader runs during SSR. No-op (with a warning) if the pattern was never registered via `.route()`.
361
+
362
+ ```ts
363
+ router()
364
+ .route("/dashboard", dashboardPage)
365
+ .clientLoader(
366
+ "/dashboard",
367
+ loader(async ({ signal }) => ({ stats: await fetchStats({ signal }) })),
368
+ );
369
+ ```
370
+
371
+ ---
372
+
373
+ #### `.errorBoundary(pattern, handler)` — runtime
374
+
375
+ Attaches the route's `+error` boundary so **loader** errors render through it — on the server (`renderResponse` returns the boundary's HTML with the error status) and on client navigations. Render errors are already handled by `wrapError` inside the island; this closes the gap for errors thrown before rendering starts. The FS-routing codegen wires the nearest `+error.ts` automatically; manual routers can call it directly. A throwing boundary falls back to the minimal inline error.
376
+
377
+ ```ts
378
+ router()
379
+ .route("/user/:id", userPage, userLoader)
380
+ .errorBoundary("/user/:id", (err, route) =>
381
+ ilha.render(() => `<h1>${err.status ?? 500}</h1><p>${err.message}</p>`),
382
+ );
383
+ ```
384
+
385
+ ---
386
+
347
387
  ### `setHistoryMode(mode)` · `getHistoryMode()`
348
388
 
349
389
  Selects the history strategy used by the router. Defaults to `"history"` (HTML5 History API, reads/writes `location.pathname`). Set to `"hash"` to store the route in `location.hash` instead — see the [Hash mode](#hash-mode) section above for when to use it.
@@ -376,6 +416,31 @@ No-op on the server.
376
416
 
377
417
  ---
378
418
 
419
+ ### `navigating()`
420
+
421
+ Reactive — `true` while a client navigation (loader fetch + view swap) is in flight. Read it inside any island render to drive a progress bar or spinner; it re-renders when the state flips. Also available as `useRoute().navigating`.
422
+
423
+ ```ts
424
+ import { navigating } from "@ilha/router";
425
+
426
+ const Spinner = ilha.render(() => (navigating() ? `<div class="bar" />` : ""));
427
+ ```
428
+
429
+ ---
430
+
431
+ ### `invalidate()`
432
+
433
+ Re-runs the current route's loader and re-renders the view with fresh data — call it after a mutation. Resolves when the view has updated. No-op on the server or when no router is mounted.
434
+
435
+ ```ts
436
+ import { invalidate } from "@ilha/router";
437
+
438
+ await api.deleteItem(id);
439
+ await invalidate(); // current page refetches and re-renders
440
+ ```
441
+
442
+ ---
443
+
379
444
  ### `prime()`
380
445
 
381
446
  Standalone export of the same signal-priming function available as `.prime()` on the builder. Useful when managing the priming step separately from the router instance.
@@ -894,6 +959,31 @@ export default defineLayout((children) => /* … */);
894
959
 
895
960
  Layout loaders are composed automatically — you do not need to call `composeLoaders()` manually.
896
961
 
962
+ ### Client loaders (`clientLoad`)
963
+
964
+ A page or layout can export a `clientLoad` function that runs **in the browser** on client navigations, instead of fetching from the loader endpoint. Use it for data that is fetchable from the client anyway (public APIs, the app's own REST endpoints) — it saves a server round-trip per navigation, and it works on static hosts with no loader endpoint at all.
965
+
966
+ ```ts
967
+ // src/pages/dashboard.ts
968
+ import { loader } from "@ilha/router";
969
+ import ilha from "ilha";
970
+
971
+ export const clientLoad = loader(async ({ signal }) => {
972
+ const stats = await fetch("/api/stats", { signal }).then((r) => r.json());
973
+ return { stats };
974
+ });
975
+
976
+ export default ilha.input<{ stats: Stats }>().render(/* … */);
977
+ ```
978
+
979
+ Rules and caveats:
980
+
981
+ - `clientLoad` is bundled into the client — never put secrets, database clients, or server-only imports in it. Keep those in `load`, which stays server-only.
982
+ - A page can export **both**: `load` runs during SSR (first paint), `clientLoad` runs on client navigations instead of the endpoint fetch. Make them return the same shape.
983
+ - Layout `clientLoad`s compose with the page's, layouts first — the page wins on key collision, mirroring server loaders.
984
+ - With SSR + hydration, a `clientLoad`-only page is server-rendered **without** its data; the router runs `clientLoad` on the client right after hydration and re-renders the route with the loaded props.
985
+ - `clientLoad` receives a synthetic `Request` — rely on `url`, `params`, and `signal`, not cookies or headers.
986
+
897
987
  ### Error boundaries
898
988
 
899
989
  A `+error.ts` catches any error thrown during rendering of pages in its directory and all subdirectories. The nearest boundary wins. If an inner boundary re-throws, the next outer boundary takes over.
@@ -1007,15 +1097,15 @@ Or use the one-liner: `pageRouter.hydrate(registry)`.
1007
1097
 
1008
1098
  On the **server**, loaders run inside `.renderHydratable()` / `.renderResponse()`. Their return value is serialized into `data-ilha-props` on the island element so the client can rehydrate without re-fetching.
1009
1099
 
1010
- On the **client**, navigations fetch loader data from the `/__ilha/loader` endpoint before mounting the next island. The endpoint is served automatically by the Vite plugin (dev) and the Nitro adapter (production).
1100
+ On the **client**, navigations resolve loader data before mounting the next island. Routes with a loader registered in the browser — a manual `.route(path, island, loader)` or an FS-routing `clientLoad` export — run that loader locally, with no network round-trip. Routes with only a server loader (`markLoader()` / a `load` export) fetch from the `/__ilha/loader` endpoint, served automatically by the Vite plugin (dev) and the Nitro adapter (production).
1011
1101
 
1012
1102
  ```
1013
1103
  server client (navigation)
1014
- ──────────────────────────── ─────────────────────────────────────
1015
- renderHydratable GET /__ilha/loader?path=/user/42
1016
- → executeLoader(…) → runLoader("/user/42")
1017
- → island.hydratable(props) → fetchLoaderData("/user/42")
1018
- → data-ilha-props="{…}" → mountRouteWithHydration(island, host, …)
1104
+ ──────────────────────────── ─────────────────────────────────────────
1105
+ renderHydratable local loader (clientLoad / .route loader)?
1106
+ → executeLoader(…) yes runLocalLoader() in-browser
1107
+ → island.hydratable(props) no → fetchLoaderData(…) GET /__ilha/loader?path=/user/42
1108
+ → data-ilha-props="{…}" → mountRouteWithHydration(island, host, …)
1019
1109
  ```
1020
1110
 
1021
1111
  ---
package/dist/codegen.d.ts CHANGED
@@ -7,6 +7,11 @@ export interface GenerateOptions {
7
7
  * mode. Default: `true`.
8
8
  */
9
9
  interceptLinks?: boolean;
10
+ /**
11
+ * Fail codegen (instead of warning) on duplicate route patterns or registry
12
+ * name collisions. Recommended for production builds. Default: `false`.
13
+ */
14
+ strict?: boolean;
10
15
  }
11
16
  /** Paths for all generated files derived from the base output directory. */
12
17
  export interface GeneratedPaths {
package/dist/hash.d.ts CHANGED
@@ -7,10 +7,10 @@ export interface LogicalLocation {
7
7
  export interface HistoryAdapter {
8
8
  /** Read the current logical URL (the one routes are matched against). */
9
9
  readLocation(): LogicalLocation;
10
- /** Push a new logical URL onto the history stack. */
11
- push(to: string): void;
12
- /** Replace the current history entry with a new logical URL. */
13
- replace(to: string): void;
10
+ /** Push a new logical URL onto the history stack. `state` is stored on the history entry. */
11
+ push(to: string, state?: unknown): void;
12
+ /** Replace the current history entry with a new logical URL. `state` is stored on the history entry. */
13
+ replace(to: string, state?: unknown): void;
14
14
  /** Subscribe to logical-URL changes. Returns a cleanup function. */
15
15
  onChange(handler: () => void): () => void;
16
16
  /**
package/dist/index.d.ts CHANGED
@@ -109,6 +109,11 @@ export declare function wrapError(handler: ErrorHandler, page: Island<any, any>)
109
109
  export declare function defineLayout(layout: LayoutHandler): LayoutHandler;
110
110
  export interface NavigateOptions {
111
111
  replace?: boolean;
112
+ /**
113
+ * When `false`, keep the current scroll position instead of scrolling to the
114
+ * top (or to the URL hash target) after navigation. Default: `true`.
115
+ */
116
+ scroll?: boolean;
112
117
  }
113
118
  export type RouterMode = "spa" | "static";
114
119
  export interface RouterOptions {
@@ -128,6 +133,30 @@ export interface RouterOptions {
128
133
  * Default: `true`.
129
134
  */
130
135
  interceptLinks?: boolean;
136
+ /**
137
+ * Island rendered when no route matches the current URL — both on the
138
+ * server (with a 404 status) and in the client `RouterView`.
139
+ */
140
+ notFound?: Island<any, any>;
141
+ /**
142
+ * Allow loader `redirect()` targets pointing at other origins. When `false`
143
+ * (default), absolute cross-origin redirect targets are rejected with a 500
144
+ * — redirect targets frequently carry user input (`?next=` params), and
145
+ * rejecting external targets by default prevents open redirects.
146
+ */
147
+ allowExternalRedirects?: boolean;
148
+ /**
149
+ * Abort a route loader after this many milliseconds during SSR / loader
150
+ * endpoint execution. `0`/`undefined` disables the timeout. The loader's
151
+ * `ctx.signal` also aborts when the incoming `Request`'s signal aborts.
152
+ */
153
+ loaderTimeout?: number;
154
+ /**
155
+ * Wrap client-side view swaps in `document.startViewTransition()` when the
156
+ * browser supports it (falls back to an instant swap otherwise).
157
+ * Default: `false`.
158
+ */
159
+ viewTransitions?: boolean;
131
160
  }
132
161
  export interface HydratableRenderOptions extends Partial<Omit<HydratableOptions, "name">> {
133
162
  /**
@@ -186,6 +215,22 @@ export interface RouterBuilder {
186
215
  * was never registered via `.route()`.
187
216
  */
188
217
  attachLoader(pattern: string, loader: Loader<any>): RouterBuilder;
218
+ /**
219
+ * Attach a loader that runs **in the browser** on client navigations,
220
+ * instead of fetching from the loader endpoint. Used by the FS-routing
221
+ * codegen for `clientLoad` exports; also available for manual routers.
222
+ * When a route has both, the client loader wins on client navigations and
223
+ * the server loader runs during SSR. No-op if the pattern was never
224
+ * registered via `.route()`.
225
+ */
226
+ clientLoader(pattern: string, loader: Loader<any>): RouterBuilder;
227
+ /**
228
+ * Attach the route's nearest `+error` boundary so **loader** errors render
229
+ * through it (render errors are already handled by `wrapError` inside the
230
+ * island). Used by the FS-routing codegen; also available for manual
231
+ * routers. No-op if the pattern was never registered via `.route()`.
232
+ */
233
+ errorBoundary(pattern: string, handler: ErrorHandler): RouterBuilder;
189
234
  /**
190
235
  * Mark an already-registered route as having a server-side loader without
191
236
  * importing that loader into the client bundle. Used by FS-routing codegen
@@ -253,39 +298,24 @@ export declare const LOADER_ENDPOINT = "/__ilha/loader";
253
298
  * navigation) or is superseded by another prefetch.
254
299
  */
255
300
  export declare function prefetch(pathWithSearch: string): void;
256
- export declare const routePath: {
257
- (): string;
258
- (value: string): void;
259
- };
260
- export declare const routeParams: {
261
- (): Record<string, string>;
262
- (value: Record<string, string>): void;
263
- };
264
- export declare const routeSearch: {
265
- (): string;
266
- (value: string): void;
267
- };
268
- export declare const routeHash: {
269
- (): string;
270
- (value: string): void;
271
- };
301
+ export declare function routePath(value?: string): string;
302
+ export declare function routeParams(value?: Record<string, string>): Record<string, string>;
303
+ export declare function routeSearch(value?: string): string;
304
+ export declare function routeHash(value?: string): string;
305
+ /** Reactive: `true` while a client navigation (loader fetch + view swap) is in flight. */
306
+ export declare function navigating(): boolean;
307
+ /**
308
+ * Re-run the current route's loader and re-render the view with fresh data —
309
+ * e.g. after a mutation. Resolves when the view has updated. No-op on the
310
+ * server or when no router is mounted.
311
+ */
312
+ export declare function invalidate(): Promise<void>;
272
313
  export declare function useRoute(): {
273
- path: {
274
- (): string;
275
- (value: string): void;
276
- };
277
- params: {
278
- (): Record<string, string>;
279
- (value: Record<string, string>): void;
280
- };
281
- search: {
282
- (): string;
283
- (value: string): void;
284
- };
285
- hash: {
286
- (): string;
287
- (value: string): void;
288
- };
314
+ path: typeof routePath;
315
+ params: typeof routeParams;
316
+ search: typeof routeSearch;
317
+ hash: typeof routeHash;
318
+ navigating: typeof navigating;
289
319
  };
290
320
  /**
291
321
  * Prime route context signals from the current `location` so that islands
@@ -293,6 +323,27 @@ export declare function useRoute(): {
293
323
  * render — preventing a mismatch morph that would destroy hydrated bindings.
294
324
  */
295
325
  export declare function prime(): void;
326
+ export interface Navigation {
327
+ /** Logical URL (path + search + hash) being navigated away from. */
328
+ from: string;
329
+ /** Logical URL being navigated to. */
330
+ to: string;
331
+ /** `"push"`/`"replace"` for programmatic navigations, `"pop"` for history traversal. */
332
+ type: "push" | "replace" | "pop";
333
+ }
334
+ export type BeforeNavigateHook = (nav: Navigation & {
335
+ cancel(): void;
336
+ }) => void;
337
+ export type AfterNavigateHook = (nav: Navigation) => void;
338
+ /**
339
+ * Run before a programmatic navigation commits. Call `nav.cancel()` to keep
340
+ * the current URL (e.g. unsaved-changes guards). Not invoked for browser
341
+ * back/forward — the URL has already changed by the time `popstate` fires.
342
+ * Returns an unsubscribe function.
343
+ */
344
+ export declare function beforeNavigate(fn: BeforeNavigateHook): () => void;
345
+ /** Run after a navigation (push, replace, or pop) has committed. Returns an unsubscribe function. */
346
+ export declare function afterNavigate(fn: AfterNavigateHook): () => void;
296
347
  export declare function navigate(to: string, opts?: NavigateOptions): void;
297
348
  export interface LinkInterceptionOptions {
298
349
  /**
@@ -306,7 +357,15 @@ export interface LinkInterceptionOptions {
306
357
  export declare function enableLinkInterception(root?: Element | Document, options?: LinkInterceptionOptions): () => void;
307
358
  export declare const RouterView: Island<Record<string, unknown>, Record<never, never>>;
308
359
  export declare const RouterLink: Island<Record<string, unknown>, Omit<Omit<Record<never, never>, K> & Record<"href", string>, "label"> & Record<"label", string>>;
309
- export declare function isActive(pattern: string): boolean;
360
+ export interface IsActiveOptions {
361
+ /**
362
+ * When `false`, `isActive("/docs")` also matches nested paths like
363
+ * `/docs/getting-started` (prefix match on the current path).
364
+ * Default: `true` (the matched route's pattern must equal `pattern`).
365
+ */
366
+ exact?: boolean;
367
+ }
368
+ export declare function isActive(pattern: string, options?: IsActiveOptions): boolean;
310
369
  /**
311
370
  * Contribute `<head>` data from inside an island's `.render()` body or a
312
371
  * layout. During SSR this collects into the active render window; on the
@@ -330,6 +389,8 @@ declare const _default: {
330
389
  enableLinkInterception: typeof enableLinkInterception;
331
390
  prime: typeof prime;
332
391
  prefetch: typeof prefetch;
392
+ beforeNavigate: typeof beforeNavigate;
393
+ afterNavigate: typeof afterNavigate;
333
394
  RouterView: Island<Record<string, unknown>, Record<never, never>>;
334
395
  RouterLink: Island<Record<string, unknown>, Omit<Omit<Record<never, never>, K> & Record<"href", string>, "label"> & Record<"label", string>>;
335
396
  loader: typeof loader;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{C as e,D as t,E as n,O as r,S as i,T as a,_ as o,a as s,b as c,c as l,d as u,f as d,g as f,h as p,i as m,l as h,m as g,n as _,o as v,p as y,r as b,s as x,t as S,u as C,v as w,w as T,x as E,y as D}from"./src-B5twcrhj.js";export{S as LOADER_ENDPOINT,_ as LoaderError,b as Redirect,m as RouterLink,s as RouterView,v as composeLoaders,e as default,x as defineLayout,l as enableLinkInterception,h as error,t as getHistoryMode,C as head,u as isActive,d as loader,y as navigate,g as prefetch,p as prime,f as redirect,o as routeHash,w as routeParams,D as routePath,c as routeSearch,E as router,i as serializeHead,r as setHistoryMode,T as useRoute,a as wrapError,n as wrapLayout};
1
+ import{A as e,C as t,D as n,E as r,M as i,O as a,S as o,T as s,_ as c,a as l,b as u,c as d,d as f,f as p,g as m,h,i as g,j as _,k as v,l as y,m as b,n as x,o as S,p as C,r as w,s as T,t as E,u as D,v as O,w as k,x as A,y as j}from"./src-8LBo_Ieg.js";export{E as LOADER_ENDPOINT,x as LoaderError,w as Redirect,g as RouterLink,l as RouterView,S as afterNavigate,T as beforeNavigate,d as composeLoaders,n as default,y as defineLayout,D as enableLinkInterception,f as error,_ as getHistoryMode,p as head,C as invalidate,b as isActive,h as loader,m as navigate,c as navigating,O as prefetch,j as prime,u as redirect,A as routeHash,o as routeParams,t as routePath,k as routeSearch,s as router,r as serializeHead,i as setHistoryMode,a as useRoute,v as wrapError,e as wrapLayout};
@@ -0,0 +1,8 @@
1
+ import{existsSync as e,readFileSync as t,watch as n}from"node:fs";import{basename as r,dirname as i,extname as a,join as o,relative as s,resolve as c,sep as l}from"node:path";import{createUnplugin as u}from"unplugin";import{mkdir as d,readFile as f,readdir as p,writeFile as m}from"node:fs/promises";function h(e){return e.replace(/\\/g,`/`)}const g=/\.(test|spec|d)\.(ts|tsx)$/,_=/^\s*export\s+(?:const|let|var|async\s+function|function)\s+load\b/m,v=/^\s*export\s+(?:const|let|var|async\s+function|function)\s+clientLoad\b/m;async function y(e){try{let t=(await f(e,`utf8`)).replace(/^\s*\/\/.*$/gm,``);return{load:_.test(t),clientLoad:v.test(t)}}catch(t){return t?.code!==`ENOENT`&&console.warn(`[ilha-router] failed to read ${e} while detecting loader exports:`,t),{load:!1,clientLoad:!1}}}function b(e){return e.startsWith(`[...`)&&e.endsWith(`]`)?`**:${e.slice(4,-1)}`:e.startsWith(`[`)&&e.endsWith(`]`)?`:${e.slice(1,-1)}`:e}function x(e){return e.startsWith(`(`)&&e.endsWith(`)`)?``:b(e)}function S(e,t){let n=h(s(e,t)),r=n.slice(0,-a(n).length).split(`/`),i=[...r.slice(0,-1).map(x),b(r.at(-1))];return i.at(-1)===`index`&&i.pop(),`/`+i.filter(Boolean).join(`/`)||`/`}function C(e){return e===`/`?`index`:e.replace(/^\//,``).replace(/\*\*:[^/]*/g,e=>e.length>3?e.slice(3):`wildcard`).replace(/:/g,``).replace(/\*\*/g,`wildcard`).replace(/\//g,`-`).replace(/[^a-zA-Z0-9-]/g,``)||`page`}function w(e){return e===`/`?3:e.includes(`**`)?0:e.includes(`:`)?1:2}function T(e){return[...e].sort((e,t)=>{let n=w(t.pattern)-w(e.pattern);if(n!==0)return n;let r=t.pattern.split(`/`).length-e.pattern.split(`/`).length;return r===0?e.pattern.localeCompare(t.pattern):r})}function E(e,t,n,r){let a=h(s(e,i(t))),c=a===``?[]:a.split(`/`);return[e,...c.map((t,n)=>o(e,...c.slice(0,n+1)))].flatMap(e=>{let t=`${o(e,r)}.tsx`;if(n.has(t))return[t];let i=`${o(e,r)}.ts`;return n.has(i)?[i]:[]})}async function D(e,t=0){if(t>20)return console.warn(`[ilha:pages] Max scan depth (20) reached at ${e} — skipping`),[];let n=[];try{for(let r of await p(e,{withFileTypes:!0})){let i=o(e,r.name);r.isDirectory()?n.push(...await D(i,t+1)):r.isFile()&&/\.(ts|tsx)$/.test(r.name)&&!g.test(r.name)&&n.push(i)}}catch(e){if(e.code===`ENOENT`)return[];throw e}return n}async function O(e){let t=await D(e),n=new Set(t),i=t.filter(e=>!r(e).startsWith(`+`)),a=new Map,o=e=>{let t=a.get(e);return t||(t=y(e),a.set(e,t)),t};return Promise.all(i.map(async t=>{let r=S(e,t),i=E(e,t,n,`+layout`),a=E(e,t,n,`+error`),[s,...c]=await Promise.all([y(t),...i.map(o)]),l=i.filter((e,t)=>c[t].load),u=i.filter((e,t)=>c[t].clientLoad);return{file:t,pattern:r,name:C(r),layouts:i,errors:a,hasLoader:s.load,loaderLayouts:l,hasClientLoader:s.clientLoad,clientLoaderLayouts:u}}))}function k(e,t,n){if(e.length===0){console.warn(`[ilha:pages] No pages found in ${t}`);return}let r=new Map,i=new Map,a=[];for(let t of e){let e=r.get(t.pattern);e?a.push(`Duplicate route pattern "${t.pattern}"\n first: ${e}\n second: ${t.file}\n The first match wins — the second page will never be reached.`):r.set(t.pattern,t.file);let n=i.get(t.name);n?a.push(`Registry name collision: "${t.name}" is used by both\n ${n}\n ${t.file}\n Hydration may not work correctly for one of these routes.`):i.set(t.name,t.file)}if(a.length!==0){if(n)throw Error(`[ilha:pages] Route validation failed:\n\n${a.join(`
2
+
3
+ `)}`);for(let e of a)console.warn(`[ilha:pages] ${e}`)}}function A(e){return{serverFile:o(e,`pages.server.ts`),clientFile:o(e,`pages.client.ts`),loadersFile:o(e,`loaders.ts`)}}async function j(e,t,n={}){let r=n.mode??`spa`,i=n.interceptLinks,a=r===`static`,o=T(await O(e));k(o,e,n.strict===!0),await d(t,{recursive:!0});let{serverFile:s,clientFile:c,loadersFile:l}=A(t),u=await F(s,M(o,s)),f=await F(c,N(o,c,{isStatic:a,interceptLinks:i}));a||await F(l,P(o,l,s)),(u||f)&&await I(t)}function M(e,t){let n=e=>{let n=h(s(i(t),e));return n.startsWith(`.`)?n:`./${n}`},r=[`import { router, wrapLayout, wrapError } from "@ilha/router";`,`import type { Island } from "ilha";`],a=[],o=[],c=[];for(let[t,i]of e.entries()){r.push(`import { default as _page${t} } from ${JSON.stringify(n(i.file))};`);for(let[e,a]of i.layouts.entries())r.push(`import { default as _layout${t}_${e} } from ${JSON.stringify(n(a))};`);for(let[e,a]of i.errors.entries())r.push(`import { default as _error${t}_${e} } from ${JSON.stringify(n(a))};`);let s=`_page${t}`;for(let e=i.errors.length-1;e>=0;e--)s=`wrapError(_error${t}_${e}, ${s})`;for(let e=i.layouts.length-1;e>=0;e--)s=`wrapLayout(_layout${t}_${e}, ${s})`;let l=`_wrapped${t}`;a.push(`const ${l} = ${s};`),o.push(` ${JSON.stringify(i.name)}: ${l}`+(t<e.length-1?`,`:``)),c.push(` .route(${JSON.stringify(i.pattern)}, ${l})`+(i.hasLoader||i.loaderLayouts.length>0?`.markLoader(${JSON.stringify(i.pattern)})`:``)),i.errors.length>0&&c.push(` .errorBoundary(${JSON.stringify(i.pattern)}, _error${t}_${i.errors.length-1})`)}return[`// @generated by @ilha/router — do not edit`,`// Server module. Use for SSR and SSG/prerender.`,`// Import via: import { pageRouter, registry } from "ilha:pages/server";`,``,...r,``,...a,``,`export const registry: Record<string, Island<any, any>> = {`,...o,`};`,``,`export const pageRouter = router()`,...c,` ;`].join(`
4
+ `)}function N(e,t,n){let{isStatic:r,interceptLinks:a}=n,o=e=>{let n=h(s(i(t),e));return n.startsWith(`.`)?n:`./${n}`},c=e=>`${o(e)}?client`,l=e=>`${o(e)}?client-loader`,u=!1,d=r?[`import { router as _router, wrapLayout, wrapError } from "@ilha/router";`,`import type { Island } from "ilha";`]:[`import { router, wrapLayout, wrapError } from "@ilha/router";`,`import type { Island } from "ilha";`],f=[],p=[],m=[];for(let[t,n]of e.entries()){d.push(`import { default as _page${t} } from ${JSON.stringify(c(n.file))};`);for(let[e,r]of n.layouts.entries())d.push(`import { default as _layout${t}_${e} } from ${JSON.stringify(c(r))};`);for(let[e,r]of n.errors.entries())d.push(`import { default as _error${t}_${e} } from ${JSON.stringify(c(r))};`);let i=`_page${t}`;for(let e=n.errors.length-1;e>=0;e--)i=`wrapError(_error${t}_${e}, ${i})`;for(let e=n.layouts.length-1;e>=0;e--)i=`wrapLayout(_layout${t}_${e}, ${i})`;let a=`_wrapped${t}`;if(f.push(`const ${a} = ${i};`),p.push(` ${JSON.stringify(n.name)}: ${a}`+(t<e.length-1?`,`:``)),!r){m.push(` .route(${JSON.stringify(n.pattern)}, ${a})`+(n.hasLoader||n.loaderLayouts.length>0?`.markLoader(${JSON.stringify(n.pattern)})`:``));let e=[];for(let[r,i]of n.clientLoaderLayouts.entries()){let n=`_cl${t}_l${r}`;d.push(`import { clientLoad as ${n} } from ${JSON.stringify(l(i))};`),e.push(n)}if(n.hasClientLoader){let r=`_cl${t}`;d.push(`import { clientLoad as ${r} } from ${JSON.stringify(l(n.file))};`),e.push(r)}if(e.length>0){let t=e.length===1?e[0]:`composeLoaders([${e.join(`, `)}])`;e.length>1&&(u=!0),m.push(` .clientLoader(${JSON.stringify(n.pattern)}, ${t})`)}n.errors.length>0&&m.push(` .errorBoundary(${JSON.stringify(n.pattern)}, _error${t}_${n.errors.length-1})`)}}u&&(d[0]=d[0].replace(`{ router`,`{ composeLoaders, router`));let g=r?`_router({ mode: "static" })`:`router(${a===!1?`{ interceptLinks: false }`:``})`,_=[`// @generated by @ilha/router — do not edit`,`// Client module. Use for browser hydration.`,`// Import via: import { pageRouter, registry } from "ilha:pages/client";`,``,...d,``,...f,``,`export const registry: Record<string, Island<any, any>> = {`,...p,`};`,``];return r?_.push(`export const pageRouter = ${g};`):_.push(`export const pageRouter = ${g}`,...m,` ;`),_.join(`
5
+ `)}function P(e,t,n){let r=e=>{let n=h(s(i(t),e));return n.startsWith(`.`)?n:`./${n}`},a=e.filter(e=>e.hasLoader||e.loaderLayouts.length>0);if(a.length===0)return[`// @generated by @ilha/router — do not edit`,`// This project has no loader exports; this file is intentionally empty.`,``,`export {};`,``].join(`
6
+ `);let o=r(n).replace(/\.tsx?$/,``),c=[`import { pageRouter } from ${JSON.stringify(o)};`],l=!1,u=[];for(let[e,t]of a.entries()){let n=[];for(let[i,a]of t.loaderLayouts.entries()){let t=`_p${e}_l${i}`;c.push(`import { load as ${t} } from ${JSON.stringify(r(a))};`),n.push(t)}if(t.hasLoader){let i=`_p${e}`;c.push(`import { load as ${i} } from ${JSON.stringify(r(t.file))};`),n.push(i)}let i=n.length===1?n[0]:`composeLoaders([${n.join(`, `)}])`;n.length>1&&(l=!0),u.push(`pageRouter.attachLoader(${JSON.stringify(t.pattern)}, ${i});`)}return l&&c.unshift(`import { composeLoaders } from "@ilha/router";`),[`// @generated by @ilha/router — do not edit`,`// Server-only. Import this module from your SSR entry to wire loaders`,`// onto pageRouter. Importing it from the client is a no-op but wastes`,`// bundle size — rely on the default build pipeline to keep it out.`,``,...c,``,...u,``].join(`
7
+ `)}async function F(e,t){try{if(await f(e,`utf8`)===t)return!1}catch{}return await m(e,t,`utf8`),!0}async function I(e){await F(o(e,`pages.d.ts`),[`// @generated by @ilha/router — do not edit`,``,`declare module "ilha:pages/server" {`,` import type { RouterBuilder } from "@ilha/router";`,` import type { Island } from "ilha";`,` export const pageRouter: RouterBuilder;`,` export const registry: Record<string, Island<any, any>>;`,`}`,``,`declare module "ilha:pages/client" {`,` import type { RouterBuilder } from "@ilha/router";`,` import type { Island } from "ilha";`,` export const pageRouter: RouterBuilder;`,` export const registry: Record<string, Island<any, any>>;`,`}`,``,`declare module "ilha:loaders" {`,` // Side-effect-only module. Importing it attaches loaders to pageRouter.`,`}`,``].join(`
8
+ `))}const L=`\0ilha:pages/server`,R=`\0ilha:pages/client`,z=`\0ilha:loaders`,B=[L,R,z];function V(e){try{return JSON.parse(t(e,`utf8`))}catch{return null}}function H(t,n){let r=t;for(;;){let t=o(r,`node_modules`,n,`package.json`);if(e(t))return V(t);let a=i(r);if(a===r)return null;r=a}}function U(e){let t=V(o(e,`package.json`));if(!t)return[];let n={...t.dependencies??{},...t.devDependencies??{}},r=[];for(let t of Object.keys(n)){if(t===`ilha`)continue;let n=H(e,t);if(!n)continue;let i=n.peerDependencies??{},a=n.dependencies??{};(`ilha`in i||`ilha`in a)&&r.push(t)}return r}function W(e,t){let n=c(e,t.dir??`src/pages`),r=c(e,t.outDir??`.ilha`),{serverFile:i,clientFile:a,loadersFile:o}=A(r);return{pagesDir:n,outDir:r,serverFile:i,clientFile:a,loadersFile:o}}function G(e){let t,n,i,a,o,s=r=>{({pagesDir:t,outDir:n,serverFile:i,clientFile:a,loadersFile:o}=W(r,e))},c=async()=>{try{await j(t,n,{mode:e.mode,interceptLinks:e.interceptLinks,strict:e.strict})}catch(t){if(console.error(`[ilha:pages] codegen failed:`,t),e.strict)throw t}},u=e=>e===t||e.startsWith(t+l);return{get pagesDir(){return t},get outDir(){return n},get serverFile(){return i},get clientFile(){return a},get loadersFile(){return o},setPaths:s,regen:c,shouldRegenOnChange:e=>{if(!u(e))return!1;let t=r(e);return t.startsWith(`+`)||/\.(ts|tsx)$/.test(t)},isUnderPagesDir:u}}async function K(e,t,n){n(t)&&await e.regen()}function q(e,t,n){if(t===`ilha:pages/server`)return L;if(t===`ilha:pages/client`)return R;if(t===`ilha:loaders`)return z;for(let r of[`?client-loader`,`?client`]){if(!t.endsWith(r))continue;let i=t.slice(0,-r.length),a=n?c(n.replace(/\?.*$/,``),`..`,i):c(i);return!e.pagesDir||!e.isUnderPagesDir(a)?void 0:a+r}}function J(e,t){if(t===`\0ilha:pages/server`){let t=e.serverFile.replace(/\.tsx?$/,``);return`export { pageRouter, registry } from ${JSON.stringify(t)};`}if(t===`\0ilha:pages/client`){let t=e.clientFile.replace(/\.tsx?$/,``);return`export { pageRouter, registry } from ${JSON.stringify(t)};`}if(t===`\0ilha:loaders`){let t=e.loadersFile.replace(/\.tsx?$/,``);return`import ${JSON.stringify(t)};`}if(t.endsWith(`?client-loader`)){let e=t.slice(0,-14);return`export { clientLoad } from ${JSON.stringify(e)};`}if(t.endsWith(`?client`)){let e=t.slice(0,-7);return`export { default } from ${JSON.stringify(e)};`}}function Y(e,t){return async n=>{e.isUnderPagesDir(n)&&(await e.regen(),await t())}}function X(t,r){let i=null,a=null,s=!1,c=()=>{i=n(t.pagesDir,{recursive:!0},(e,n)=>{n&&r(o(t.pagesDir,n))})};return e(t.pagesDir)?c():(a=setInterval(()=>{s||!e(t.pagesDir)||(clearInterval(a),a=null,c(),r(o(t.pagesDir,`.`)))},1e3),a.unref?.()),()=>{s=!0,a&&clearInterval(a),i?.close()}}const Z=u((e={})=>{let t=G(e);return{name:`ilha:pages`,async buildStart(){t.pagesDir||t.setPaths(process.cwd()),this.addWatchFile?.(t.pagesDir),await t.regen()},async watchChange(e){await K(t,e,e=>t.shouldRegenOnChange(e))},resolveId(e,n){return q(t,e,n)},load(e){return J(t,e)},vite:{config(e){let t=[`ilha`,`@ilha/store`,`@ilha/router`,`alien-signals`,...U(e.root?c(e.root):process.cwd())],n=e.ssr?.noExternal,r=n===!0?!0:[...new Set([...Array.isArray(n)?n:n==null?[]:[n],...t])];return{resolve:{dedupe:[...new Set([...e.resolve?.dedupe??[],...t])]},ssr:{noExternal:r},optimizeDeps:{...e.optimizeDeps,include:[...new Set([...e.optimizeDeps?.include??[],`ilha`,`ilha/jsx-runtime`,`ilha/jsx-dev-runtime`,`@ilha/store`,`alien-signals`])]}}},configResolved(e){t.setPaths(e.root)},configureServer(e){e.watcher.add(t.pagesDir);let n=Y(t,async()=>{for(let t of B){let n=e.moduleGraph.getModuleById(t);n&&e.moduleGraph.invalidateModule(n)}e.hot.send({type:`full-reload`})});e.watcher.on(`add`,n),e.watcher.on(`addDir`,n),e.watcher.on(`unlink`,n),e.watcher.on(`change`,async e=>{t.shouldRegenOnChange(e)&&await n(e)})}},rspack(e){t.setPaths(e.options.context??process.cwd());let n=Y(t,()=>{e.watching&&e.invalidate()}),r;e.hooks.watchRun.tap(`ilha:pages`,()=>{r?.(),r=X(t,n)}),e.hooks.shutdown.tap(`ilha:pages`,()=>r?.())}}});export{Z as t};
package/dist/plugin.d.ts CHANGED
@@ -8,6 +8,8 @@ export declare const RESOLVED_LOADERS = "\0ilha:loaders";
8
8
  export declare const RESOLVED_VIRTUAL_IDS: readonly ["\0ilha:pages/server", "\0ilha:pages/client", "\0ilha:loaders"];
9
9
  /** Query suffix used on page/layout imports in the client file. */
10
10
  export declare const CLIENT_QUERY = "?client";
11
+ /** Query suffix that re-exports a page/layout's `clientLoad` for the browser bundle. */
12
+ export declare const CLIENT_LOADER_QUERY = "?client-loader";
11
13
  export interface IlhaPagesOptions {
12
14
  /** Directory containing page files. Default: `src/pages` */
13
15
  dir?: string;
@@ -26,6 +28,11 @@ export interface IlhaPagesOptions {
26
28
  * Default: `true`.
27
29
  */
28
30
  interceptLinks?: boolean;
31
+ /**
32
+ * Fail codegen on duplicate route patterns / registry name collisions
33
+ * instead of warning. Recommended for CI/production builds. Default: `false`.
34
+ */
35
+ strict?: boolean;
29
36
  }
30
37
  export declare function resolvePluginPaths(root: string, options: IlhaPagesOptions): {
31
38
  pagesDir: string;
@@ -47,7 +54,7 @@ export interface PagesPluginState {
47
54
  }
48
55
  export declare function createPagesPluginState(options: IlhaPagesOptions): PagesPluginState;
49
56
  export declare function regenFromPagesChange(state: PagesPluginState, file: string, shouldRegen: (file: string) => boolean): Promise<void>;
50
- export declare function resolvePagesId(_state: PagesPluginState, id: string, importer?: string): string | undefined;
57
+ export declare function resolvePagesId(state: PagesPluginState, id: string, importer?: string): string | undefined;
51
58
  export declare function loadPagesModule(state: PagesPluginState, id: string): string | undefined;
52
59
  type InvalidateModules = () => void | Promise<void>;
53
60
  export declare function createStructuralInvalidate(state: PagesPluginState, invalidate: InvalidateModules): (file: string) => Promise<void>;
@@ -2,4 +2,4 @@ export { wrapLayout, wrapError, type LayoutHandler, type ErrorHandler, type Rout
2
2
  export { ilhaPages, type IlhaPagesOptions } from "./plugin";
3
3
  import { type IlhaPagesOptions } from "./plugin";
4
4
  /** Rolldown plugin — use via `@ilha/router/rolldown`. */
5
- export declare function pages(options?: IlhaPagesOptions): import("unplugin").RolldownPlugin<any> | import("unplugin").RolldownPlugin<any>[];
5
+ export declare function pages(options?: IlhaPagesOptions): import("rolldown").Plugin<any> | import("rolldown").Plugin<any>[];
package/dist/rolldown.js CHANGED
@@ -1 +1 @@
1
- import{E as e,T as t}from"./src-B5twcrhj.js";import{t as n}from"./plugin-C07ziXtR.js";function r(e={}){return n.rolldown(e)}export{n as ilhaPages,r as pages,t as wrapError,e as wrapLayout};
1
+ import{A as e,k as t}from"./src-8LBo_Ieg.js";import{t as n}from"./plugin-BW2tnuyF.js";function r(e={}){return n.rolldown(e)}export{n as ilhaPages,r as pages,t as wrapError,e as wrapLayout};
package/dist/rspack.js CHANGED
@@ -1 +1 @@
1
- import{E as e,T as t}from"./src-B5twcrhj.js";import{t as n}from"./plugin-C07ziXtR.js";function r(e={}){return n.rspack(e)}export{n as ilhaPages,r as pages,t as wrapError,e as wrapLayout};
1
+ import{A as e,k as t}from"./src-8LBo_Ieg.js";import{t as n}from"./plugin-BW2tnuyF.js";function r(e={}){return n.rspack(e)}export{n as ilhaPages,r as pages,t as wrapError,e as wrapLayout};
@@ -0,0 +1,4 @@
1
+ import e,{ISLAND_MOUNT_INTERNAL as t,context as n,html as r,mount as i}from"ilha";import{addRoute as a,createRouter as o,findRoute as s}from"rou3";const c=typeof window<`u`&&typeof document<`u`,l={readLocation(){return c?{pathname:location.pathname,search:location.search,hash:location.hash}:{pathname:`/`,search:``,hash:``}},push(e,t){c&&history.pushState(t??null,``,e)},replace(e,t){c&&history.replaceState(t??null,``,e)},onChange(e){return c?(window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)):()=>{}},toLinkHref(e){return e},extractLogicalPath(e){let t=e.getAttribute(`href`);return!t||e.protocol&&!/^(http:|https:)$/.test(e.protocol)||t.startsWith(`#`)||e.hostname&&(e.hostname!==location.hostname||e.protocol!==location.protocol)?null:e.pathname+e.search+e.hash}};function u(e){let t=e.startsWith(`#`)?e.slice(1):e,n=t===``?`/`:t.startsWith(`/`)?t:`/`+t,r=new URL(n,`http://_`);return{pathname:r.pathname,search:r.search,hash:r.hash}}const d={readLocation(){return c?u(location.hash):{pathname:`/`,search:``,hash:``}},push(e,t){c&&history.pushState(t??null,``,e.startsWith(`#`)?e:`#`+e)},replace(e,t){c&&history.replaceState(t??null,``,e.startsWith(`#`)?e:`#`+e)},onChange(e){return c?(window.addEventListener(`popstate`,e),window.addEventListener(`hashchange`,e),()=>{window.removeEventListener(`popstate`,e),window.removeEventListener(`hashchange`,e)}):()=>{}},toLinkHref(e){return e.startsWith(`#`)?e:`#`+e},extractLogicalPath(e){let t=e.getAttribute(`href`);if(!t||e.protocol&&!/^(http:|https:)$/.test(e.protocol))return null;if(t.startsWith(`#`)){let e=t.slice(1);return e===``||!e.startsWith(`/`)?null:e}if(/^https?:\/\//i.test(t))try{let e=new URL(t);if(e.origin!==location.origin||!e.hash||e.hash===`#`)return null;let n=e.hash.slice(1);return n.startsWith(`/`)?n:null}catch{return null}return t}};let f=`history`,p=l;function m(e){f=e,p=e===`hash`?d:l}function h(){return f}function g(){return p}const _=typeof window<`u`&&typeof document<`u`;function v(e){return e}var y=class{__ilhaRedirect=!0;to;status;constructor(e,t=302){this.to=e,this.status=t}},b=class{__ilhaLoaderError=!0;status;message;constructor(e,t){this.status=e,this.message=t}};function x(e,t=302){throw new y(e,t)}function ee(e,t){throw new b(e,t)}function S(e){return e.length===0?async()=>({}):e.length===1?e[0]:async t=>{let n=await Promise.all(e.map(e=>e(t)));return Object.assign({},...n)}}const C=Symbol.for(`ilha.router.wrapLayout.leaf`),te=Symbol.for(`ilha.router.wrapLayout.handler`);function ne(e){let t=e.match(/^<([a-zA-Z][\w-]*)\s[^>]*>([\s\S]*)<\/\1>\s*$/);return t?t[2]:e}function re(e){let t=e.match(/^<([a-zA-Z][\w-]*)\s([^>]*)>/);return t?{tag:t[1],attrs:t[2]}:null}const ie=/<(pre|script|style|textarea)\b/i;function ae(e,t,n){let r=RegExp(`<${n}\\b`,`gi`),i=RegExp(`</${n}>`,`gi`),a=1,o=t;for(;a>0&&o<e.length;){r.lastIndex=o,i.lastIndex=o;let t=r.exec(e),n=i.exec(e);if(!n)return null;if(t&&t.index<n.index)a+=1,o=t.index+t[0].length;else{if(--a,a===0)return n.index+n[0].length;o=n.index+n[0].length}}return o}function oe(e,t){let n=t;for(;n<e.length;){if(e.startsWith(`<!--`,n)){let t=e.indexOf(`-->`,n);if(t===-1)return null;n=t+3;continue}let t=e.slice(n).match(ie);if(t&&t.index!=null&&t.index>=0){let r=n+t.index,i=!1,a=n;for(;a<r;){let t=e.indexOf(`<!--`,a);if(t===-1||t>=r)break;let o=e.indexOf(`-->`,t);if(o===-1)return null;if(r<o+3){n=o+3,i=!0;break}a=o+3}if(i)continue;let o=e.indexOf(`<div`,n),s=e.indexOf(`</div>`,n);if(!(o!==-1&&o<r||s!==-1&&s<r)){let i=t[1].toLowerCase(),a=ae(e,r+t[0].length,i);if(a===null)return null;n=a;continue}}let r=e.indexOf(`<div`,n),i=e.indexOf(`</div>`,n);return i===-1&&r===-1?null:r===-1||i!==-1&&i<r?{kind:`close`,index:i}:{kind:`open`,index:r}}return null}function se(e){let t=[];for(let n of e.matchAll(/<div\s[^>]*data-ilha-slot="k:page"[^>]*>/g)){let r=n.index+n[0].length,i=1,a=r;for(;i>0;){let n=oe(e,a);if(!n)break;if(n.kind===`open`)i+=1,a=n.index+4;else{if(--i,i===0){t.push({openEnd:r,closeStart:n.index});break}a=n.index+6}}}return t}function w(e,t,n){let r=se(e);if(r.length===0)return e;let i=n===`innermost`?r[r.length-1]:r[0];return e.slice(0,i.openEnd)+t+e.slice(i.closeStart)}function ce(e,t){let n=e[te];if(!n)return e.toString(t);let r=e[C]??e;return n(Object.assign(r.key(`page`),{toString:()=>``})).toString(t)}async function le(e,t,n,r){let i=ne(await t.hydratable(n,r));return w(w(ce(e,n),``,`innermost`),i,`innermost`)}function ue(e,n){let r=n[C]??n,i=r===n?null:n,a=e(Object.assign(n.key(`page`),{toString:n.toString.bind(n)}));a[C]=r,a[te]=e;function o(e){let t=[...e.querySelectorAll(`[data-ilha-slot="k:page"]`)].filter(t=>{let n=t.closest(`[data-ilha]`);return n===null||n===e});return t.length===0?e:t[t.length-1]}function s(e,t){let n=e=>{delete e._skipOnMount,t.setAttribute(`data-ilha-state`,JSON.stringify(e))};if(t.hasAttribute(`data-ilha-state`)){let r=e.getAttribute(`data-ilha-state`);if(r)try{n(JSON.parse(r));return}catch{}let i=t.getAttribute(`data-ilha-state`);if(i)try{let e=JSON.parse(i);delete e._skipOnMount,t.setAttribute(`data-ilha-state`,JSON.stringify(e))}catch{}return}let r=e.getAttribute(`data-ilha-state`);if(r)try{n(JSON.parse(r));return}catch{}t.childNodes.length>0&&t.setAttribute(`data-ilha-state`,`{}`)}function c(e){let n=e[t];if(typeof n!=`function`)return;e[t]=(e,t)=>{let r=e.closest(`[data-ilha]`);return r&&r!==e&&s(r,e),n(e,t)};let r=e.mount.bind(e);e.mount=(e,t)=>{let n=e.closest(`[data-ilha]`);return n&&n!==e&&s(n,e),r(e,t)}}c(r);let l=a.mount.bind(a),u=a[t];function d(e){s(e,o(e))}return a.mount=(e,t)=>(d(e),l(e,t)),a[t]=(e,t)=>(d(e),typeof u==`function`?u(e,t):{unmount:l(e,t),updateProps:()=>{}}),a.hydratable=async(e,t)=>{if(!t?.name)throw Error(`wrapLayout: hydratable requires options.name`);let n=e??{},o=await r.hydratable(n,t),s=re(o);if(!s)return o;let c=ne(o);i&&(c=await le(i,r,n,t));let l=w(w(ce(a,n),``,`first`),c,`first`);return`<${s.tag} ${s.attrs}>${l}</${s.tag}>`},a}function de(n,r){let i=e.render(()=>{try{return r.toString()}catch(e){let t={path:A(),params:j(),search:M(),hash:N()};return n({message:e.message,status:e.status,stack:e.stack},t).toString()}});return i.mount=(e,t)=>{try{return r.mount(e,t)}catch(r){let i={path:A(),params:j(),search:M(),hash:N()},a=n({message:r.message,status:r.status,stack:r.stack},i);return e.innerHTML=a.toString(),a.mount(e,t)}},i[t]=(e,i)=>{try{let n=r[t];return typeof n==`function`?n(e,i):{unmount:r.mount(e,i),updateProps:()=>{}}}catch(t){let r={path:A(),params:j(),search:M(),hash:N()},a=n({message:t.message,status:t.status,stack:t.stack},r);return e.innerHTML=a.toString(),{unmount:a.mount(e,i),updateProps:()=>{}}}},i.hydratable=async(e,t)=>{if(!t?.name)throw Error(`wrapError: hydratable requires options.name`);return r.hydratable(e??{},t)},i}function fe(e){return e}function pe(e){let t=new Map;for(let[n,r]of Object.entries(e))t.has(r)||t.set(r,n);return t}const me=`/__ilha/loader`,T=new Map;let he=!1;async function E(e){let t=document.startViewTransition?.bind(document);if(!he||!t)return e();let n,r,i=!1;if(await t(()=>{try{n=e()}catch(e){throw i=!0,r=e,e}}).updateCallbackDone?.catch(()=>{}),i)throw r;return n}async function ge(e,t,n,r){let i=new URL(n,location.origin),a=z(t),o=[],s=await pt(e,i,a,ft(i),r??new AbortController().signal,e=>o.push(e));if(r?.throwIfAborted(),s.kind===`redirect`){let e=ut(s.to,i,Ge);return e.ok?{kind:`redirect`,to:e.to,status:s.status}:(console.warn(`[ilha-router] Blocked unsafe redirect target "${s.to}". Set allowExternalRedirects: true to allow cross-origin redirects.`),{kind:`error`,status:500,message:`Unsafe redirect target`})}return s.kind===`data`?o.length>0?{kind:`data`,data:s.data,headEntries:o}:{kind:`data`,data:s.data}:s}async function _e(e,t){let n=T.get(e);if(n&&(T.delete(e),Date.now()<=n.expires))try{t?.throwIfAborted();let e=await n.promise;return t?.throwIfAborted(),e}catch(e){if(e?.name===`AbortError`)throw e}let r=e.split(`?`)[0]??``,i=s(R,`GET`,r),a=i?.data?.clientLoader??i?.data?.loader;if(a)return ge(a,i?.params,e,t);let o=`${me}?path=${encodeURIComponent(e)}`;try{let e=await fetch(o,{signal:t,headers:{accept:`application/json`}});if(!e.ok){try{let t=await e.json();if(t&&typeof t==`object`&&`kind`in t)return t}catch{}return{kind:`error`,status:e.status,message:e.statusText}}return await e.json()}catch(e){if(e?.name===`AbortError`)throw e;return{kind:`error`,status:0,message:e?.message??`network error`}}}function D(e){if(!_)return;let t=T.get(e);if(t&&Date.now()<=t.expires)return;let n=e.split(`?`)[0]??``;if(!s(R,`GET`,n)?.data?.hasLoader)return;let r=_e(e).catch(e=>({kind:`error`,status:0,message:e?.message??`prefetch failed`}));T.set(e,{promise:r,expires:Date.now()+3e4})}async function ve(e,t,n,r,i,a){if(!e){if(W){let e=W;return E(()=>{t.innerHTML=`<div data-router-view data-router-not-found>${e.toString()}</div>`;let n=t.firstElementChild;return n?e.mount(n):()=>{}})}return await E(()=>{t.innerHTML=`<div data-router-empty></div>`}),()=>{}}let o=s(R,`GET`,n.split(`?`)[0]??``),c=!!o?.data?.hasLoader,l={},u=c?await _e(n,r):{kind:`data`,data:{}};if(u.kind===`redirect`)return Ue(u.to),()=>{};if(u.kind===`error`){let e=o?.data?.errorHandler;if(e)return E(()=>ye(e,t,u.status,u.message));let n=it(u.message);return await E(()=>{t.innerHTML=`<div data-router-view data-router-error="${u.status}">${n}</div>`}),()=>{}}if(u.kind===`not-found`)return await E(()=>{t.innerHTML=`<div data-router-empty></div>`}),()=>{};l=u.data;let d={entries:[...u.headEntries??[]]};if(!i){console.warn(`[ilha-router] No registry provided for client-side navigation. Island will not be interactive.`);let n=await Z(d,()=>e.toString(l));return await E(()=>{X(d.entries),t.innerHTML=`<div data-router-view>${n}</div>`}),()=>{}}let f=a?.get(e)??Object.entries(i).find(([,t])=>t===e)?.[0];if(!f){console.warn(`[ilha-router] Island not found in registry for client-side navigation.`);let n=await Z(d,()=>e.toString(l));return await E(()=>{X(d.entries),t.innerHTML=`<div data-router-view>${n}</div>`}),()=>{}}let p=await Z(d,()=>e.hydratable(l,{name:f,as:`div`,snapshot:!0}));return E(()=>{X(d.entries),t.innerHTML=`<div data-router-view>${p}</div>`;let n=t.querySelector(`[data-ilha="${f}"]`);return n?e.mount(n):()=>{}})}function ye(e,t,n,r){try{let i=e({message:r,status:n},{path:A(),params:j(),search:M(),hash:N()});t.innerHTML=`<div data-router-view data-router-error="${n}">${i.toString()}</div>`;let a=t.firstElementChild;return a?i.mount(a):()=>{}}catch(e){return console.error(`[ilha-router] error boundary threw while rendering a loader error:`,e),t.innerHTML=`<div data-router-view data-router-error="${n}"></div>`,()=>{}}}let O=null,be=null;async function xe(){return O||(be||=import(`node:async_hooks`).then(({AsyncLocalStorage:e})=>(O=new e,O)),be)}_||xe().catch(()=>{});function k(){return _?null:O?.getStore()??null}function Se(){return{path:``,params:{},search:``,hash:``,island:null}}const Ce=n(`router.path`,``),we=n(`router.params`,{}),Te=n(`router.search`,``),Ee=n(`router.hash`,``);function A(e){let t=k();return arguments.length>0?t?t.path=e:(Ce(e),e):t?t.path:Ce()}function j(e){let t=k();return arguments.length>0?t?t.params=e:(we(e),e):t?t.params:we()}function M(e){let t=k();return arguments.length>0?t?t.search=e:(Te(e),e):t?t.search:Te()}function N(e){let t=k();return arguments.length>0?t?t.hash=e:(Ee(e),e):t?t.hash:Ee()}const P=n(`router.navigating`,0);function De(){return P()>0}let F=null;function Oe(){return!_||!F?Promise.resolve():F()}function I(){if(!_)return()=>{};P(P()+1);let e=!1;return()=>{e||(e=!0,P(Math.max(0,P()-1)))}}function ke(){return{path:A,params:j,search:M,hash:N,navigating:De}}const Ae=n(`router.active`,null);function L(e){let t=k();return arguments.length>0?t?t.island=e??null:(Ae(e??null),e??null):t?t.island:Ae()}let R=o();function z(e){let t={};if(e)for(let[n,r]of Object.entries(e))t[n]=decodeURIComponent(r);return t}function je(e,t=R){let n=typeof e==`string`?new URL(e,`http://localhost`):e,r=s(t,`GET`,n.pathname);A(n.pathname),j(z(r?.params)),M(n.search),N(n.hash),L(r?.data?.island??null)}function Me(){let e=g().readLocation(),t=s(R,`GET`,e.pathname);A(e.pathname),j(z(t?.params)),M(e.search),N(e.hash),L(t?.data?.island??null)}function B(){_&&Me()}const Ne=new Set,Pe=new Set;function Fe(e){return Ne.add(e),()=>Ne.delete(e)}function Ie(e){return Pe.add(e),()=>Pe.delete(e)}function Le(e){for(let t of Pe)try{t(e)}catch(e){console.error(`[ilha-router] afterNavigate hook threw:`,e)}}const Re=new Map;let ze=0,V=0;function H(){if(!_)return 0;let e=history.state;return typeof e?.__ilhaNavKey==`number`?e.__ilhaNavKey:0}function Be(){Re.set(H(),{x:window.scrollX,y:window.scrollY})}function Ve(e){requestAnimationFrame(()=>{if(e&&e!==`#`){let t=document.getElementById(e.slice(1))??document.querySelector(`a[name="${Y(e.slice(1))}"]`);if(t){t.scrollIntoView();return}}window.scrollTo(0,0)})}function He(){let e=Re.get(H());e&&requestAnimationFrame(()=>window.scrollTo(e.x,e.y))}function U(e,t={}){if(!_)return;let n=g(),r=n.readLocation(),i=r.pathname+r.search+r.hash;if(e===i)return;let a=t.replace?`replace`:`push`,o=!1;for(let t of Ne)try{t({from:i,to:e,type:a,cancel:()=>o=!0})}catch(e){console.error(`[ilha-router] beforeNavigate hook threw:`,e)}o||(t.replace?n.replace(e,{__ilhaNavKey:H()}):(Be(),ze=Math.max(ze+1,H()+1),n.push(e,{__ilhaNavKey:ze})),V=H(),Me(),t.scroll!==!1&&Ve(n.readLocation().hash),Le({from:i,to:e,type:a}))}function Ue(e){if(/^https?:\/\//i.test(e)){try{let t=new URL(e);if(t.origin===location.origin){U(t.pathname+t.search+t.hash,{replace:!0});return}}catch{return}location.assign(e);return}U(e,{replace:!0})}function We(e=document,t={}){if(!_)return()=>{};let n=t.prefetch!==!1;function r(e,t){let n=e.getAttribute(`target`)===`_blank`,r=!!t&&(t.ctrlKey||t.metaKey||t.shiftKey||t.altKey),i=e.hasAttribute(`data-no-intercept`),a=e.hasAttribute(`download`),o=/\bexternal\b/i.test(e.getAttribute(`rel`)??``);return n||r||i||a||o?null:g().extractLogicalPath(e)}let i=e=>{if(e.defaultPrevented||typeof e.button==`number`&&e.button!==0)return;let t=e.target.closest(`a`);if(!t)return;let n=r(t,e);n!==null&&(e.preventDefault(),U(n))},a=e=>{let t=e.target.closest(`a`);if(!t)return;let n=t.getAttribute(`data-prefetch`);if(n===null||n===`false`)return;let i=r(t);i!==null&&D(i.split(`#`)[0]??i)};return e.addEventListener(`click`,i),n&&e.addEventListener(`mouseover`,a,{passive:!0}),()=>{e.removeEventListener(`click`,i),n&&e.removeEventListener(`mouseover`,a)}}let W=null,Ge=!1;const G=e.render(()=>{let e=L();return e?`<div data-router-view>${e.toString()}</div>`:W?`<div data-router-view data-router-not-found>${W.toString()}</div>`:`<div data-router-empty></div>`}),Ke=e.state(`href`,``).state(`label`,``).on(`[data-link]@click`,({state:e,event:t})=>{t.preventDefault(),U(e.href())}).on(`[data-link]@mouseenter`,({state:e})=>{let t=e.href();if(t){if(/^https?:\/\//i.test(t))try{let e=new URL(t);if(e.origin!==location.origin)return;D(e.pathname+e.search);return}catch{return}D(t)}}).render(({state:e})=>r`<a data-link data-prefetch href="${()=>g().toLinkHref(e.href())}"
2
+ >${e.label}</a
3
+ >`);function qe(e,t={}){if(t.exact===!1){let t=A(),n=e.endsWith(`/`)?e.slice(0,-1):e;return t===n||t===n+`/`||t.startsWith(n+`/`)}let n=s(R,`GET`,A());return n?n.data.pattern===e:!1}const K=`data-ilha-head`,Je=`data-ilha-router-html`,Ye=`data-ilha-router-body`;let q=null,J=null,Xe=null;async function Ze(){return J||(Xe||=import(`node:async_hooks`).then(({AsyncLocalStorage:e})=>(J=new e,J)),Xe)}function Qe(){return _?q:J?.getStore()??null}function $e(e){let t=Qe();if(!t){_||console.warn(`[ilha-router] head() called outside an SSR render window — ignored.`);return}t.entries.push(e)}function Y(e){return typeof CSS<`u`&&typeof CSS.escape==`function`?CSS.escape(e):e.replace(/\\/g,`\\\\`).replace(/"/g,`\\"`)}function et(e){return`charset`in e?`meta[charset][${K}]`:`name`in e?`meta[name="${Y(e.name)}"][${K}]`:`property`in e?`meta[property="${Y(e.property)}"][${K}]`:`http-equiv`in e?`meta[http-equiv="${Y(e[`http-equiv`])}"][${K}]`:null}function tt(e){return e.rel&&e.href?`link[rel="${Y(e.rel)}"][href="${Y(e.href)}"][${K}]`:null}function X(e){if(!_)return;let t,n,r=[],i=[],a={},o={};for(let s of e)s.title!==void 0&&(t=s.title),s.titleTemplate!==void 0&&(n=s.titleTemplate),s.meta&&r.push(...s.meta),s.link&&i.push(...s.link),s.htmlAttrs&&(a={...a,...s.htmlAttrs}),s.bodyAttrs&&(o={...o,...s.bodyAttrs});let s=st(t,n);s!==void 0&&(document.title=s);let c=ot(r,at),l=ot(i,e=>`${e.rel??``}:${e.href??``}`),u=new Set;for(let e of c){let t=et(e);if(!t)continue;let n=document.querySelector(t);n||(n=document.createElement(`meta`),n.setAttribute(K,``),document.head.appendChild(n));for(let[t,r]of Object.entries(e))n.setAttribute(t,r);u.add(n)}for(let e of l){let t=tt(e),n=t?document.querySelector(t):null;n||(n=document.createElement(`link`),n.setAttribute(K,``),document.head.appendChild(n));for(let[t,r]of Object.entries(e))n.setAttribute(t,r);u.add(n)}for(let e of[...document.head.querySelectorAll(`[${K}]`)])u.has(e)||e.remove();let d=document.documentElement,f=(d.getAttribute(Je)??``).split(/\s+/).filter(Boolean);for(let e of f)d.removeAttribute(e);let p=Object.keys(a);for(let[e,t]of Object.entries(a))d.setAttribute(e,t);p.length?d.setAttribute(Je,p.join(` `)):d.removeAttribute(Je);let m=document.body,h=(m.getAttribute(Ye)??``).split(/\s+/).filter(Boolean);for(let e of h)m.removeAttribute(e);let g=Object.keys(o);for(let[e,t]of Object.entries(o))m.setAttribute(e,t);g.length?m.setAttribute(Ye,g.join(` `)):m.removeAttribute(Ye)}async function Z(e,t){if(_){let n=q;q=e;try{return await t()}finally{q=n}}return await(await Ze()).run(e,()=>Promise.resolve(t()))}const nt={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`};function rt(e){return String(e).replace(/[&<>"']/g,e=>nt[e])}function it(e){return String(e).replace(/[&<>]/g,e=>nt[e])}function Q(e){return Object.entries(e).map(([e,t])=>` ${e}="${rt(t)}"`).join(``)}function at(e){return`charset`in e?`charset`:`name`in e?`name:${e.name}`:`property`in e?`property:${e.property}`:`http-equiv`in e?`http-equiv:${e[`http-equiv`]}`:JSON.stringify(e)}function ot(e,t){let n=new Map;for(let r of e)n.set(t(r),r);return[...n.values()]}function st(e,t){return t===void 0?e:typeof t==`function`?t(e):t.replace(/%s/g,e??``)}function $(e){let t,n,r=[],i=[],a=[],o={},s={};for(let c of e)c.title!==void 0&&(t=c.title),c.titleTemplate!==void 0&&(n=c.titleTemplate),c.meta&&r.push(...c.meta),c.link&&i.push(...c.link),c.script&&a.push(...c.script),c.htmlAttrs&&(o={...o,...c.htmlAttrs}),c.bodyAttrs&&(s={...s,...c.bodyAttrs});let c=st(t,n),l=[];c!==void 0&&l.push(`<title>${rt(c)}</title>`);for(let e of ot(r,at))l.push(`<meta${Q({...e,[K]:``})} />`);for(let e of ot(i,e=>`${e.rel??``}:${e.href??``}`))l.push(`<link${Q({...e,[K]:``})} />`);for(let e of a){let{children:t,...n}=e,r=(t??``).replace(/<\/script/gi,`<\\/script`);l.push(`<script${Q(n)}>${r}<\/script>`)}return{headTags:l.join(`
4
+ `),htmlAttrs:Q(o),bodyAttrs:Q(s)}}function ct(e){return typeof e==`string`?new URL(e,`http://localhost`):e}function lt(){try{return typeof process<`u`&&!!process.env&&process.env.NODE_ENV!==`production`}catch{return!1}}function ut(e,t,n){if(e.startsWith(`/`)&&!e.startsWith(`//`))return{ok:!0,to:e};try{let r=new URL(e,t);return/^https?:$/.test(r.protocol)?r.origin===t.origin?{ok:!0,to:r.pathname+r.search+r.hash}:n?{ok:!0,to:r.href}:{ok:!1}:{ok:!1}}catch{return{ok:!1}}}function dt(e,t){let n=new AbortController,r=()=>n.abort(),i=e?.signal;i&&(i.aborted?r():i.addEventListener(`abort`,r,{once:!0}));let a;return t&&t>0&&(a=setTimeout(r,t)),{signal:n.signal,done:()=>{a!==void 0&&clearTimeout(a),i?.removeEventListener(`abort`,r)}}}function ft(e){try{return new Request(e.toString())}catch{return{url:e.toString(),headers:new Headers}}}async function pt(e,t,n,r,i,a){let o=[],s=a??(e=>o.push(e));try{let a=Promise.resolve(e({params:n,request:r,url:t,signal:i,head:s}));a.catch(()=>{});let c={kind:`data`,data:await Promise.race([a,new Promise((e,t)=>{let n=()=>t(new b(504,`Loader aborted or timed out`));i.aborted?n():i.addEventListener(`abort`,n,{once:!0})})])??{}};return o.length>0&&(c.head=$(o)),c}catch(e){return e instanceof y?{kind:`redirect`,to:e.to,status:e.status}:e instanceof b?{kind:`error`,status:e.status,message:e.message}:(console.error(`[ilha-router] loader failed:`,e),{kind:`error`,status:typeof e?.status==`number`?e.status:500,message:lt()?e?.message??`Loader failed`:`Internal error`})}}function mt(t={}){let n=t.mode??`spa`,r=t.interceptLinks!==!1,c=t.allowExternalRedirects===!0,l=t.loaderTimeout,u=[],d=o(),f=new Map,p=t.notFound??null;R=d,W=p,Ge=c,he=t.viewTransitions===!0;let m=null,v=null,y={route(e,t,n){let r=!!n,i={island:t,pattern:e,loader:n,hasLoader:r};return u.push({pattern:e,island:t,loader:n,hasLoader:r}),a(d,`GET`,e,i),f.set(e,i),y},attachLoader(e,t){let n=f.get(e);if(!n)return console.warn(`[ilha-router] attachLoader("${e}", …): pattern was never registered via .route(). The loader will be ignored.`),y;n.loader=t,n.hasLoader=!0;let r=u.find(t=>t.pattern===e);return r&&(r.loader=t,r.hasLoader=!0),y},clientLoader(e,t){let n=f.get(e);if(!n)return console.warn(`[ilha-router] clientLoader("${e}", …): pattern was never registered via .route(). The loader will be ignored.`),y;n.clientLoader=t,n.hasLoader=!0;let r=u.find(t=>t.pattern===e);return r&&(r.hasLoader=!0),y},errorBoundary(e,t){let n=f.get(e);return n?(n.errorHandler=t,y):(console.warn(`[ilha-router] errorBoundary("${e}", …): pattern was never registered via .route(). The boundary will be ignored.`),y)},markLoader(e){let t=f.get(e);if(!t)return console.warn(`[ilha-router] markLoader("${e}"): pattern was never registered via .route(). The loader marker will be ignored.`),y;t.hasLoader=!0;let n=u.find(t=>t.pattern===e);return n&&(n.hasLoader=!0),y},routes(){return u.map(e=>({...e}))},prime:B,hydrateStatic(e,t={}){if(!_)return()=>{};let n=t.root??document.body;B();let{unmount:r}=i(e,{root:n});return r},mount(t,{hydrate:i=!1,registry:a,interceptLinks:o}={}){if(!_)return console.warn(`[ilha-router] mount() called in a non-browser environment`),()=>{};let c=typeof t==`string`?document.querySelector(t):t;if(!c)return console.warn(`[ilha-router] No element found for selector "${t}"`),()=>{};if(Me(),V=H(),n===`static`)return console.warn(`[ilha-router] router.mount() called in static mode. Use router.hydrateStatic(registry) instead.`),()=>{};let l=!0,u=`scrollRestoration`in history?history.scrollRestoration:null;u!==null&&(history.scrollRestoration=`manual`),m=g().onChange(()=>{if(!l)return;let e=A()+M()+N();Re.set(V,{x:window.scrollX,y:window.scrollY}),V=H(),Me(),He(),Le({from:e,to:A()+M()+N(),type:`pop`})}),v=o??r?We(document):null;let d=null,f=null;if(i){h()===`hash`&&console.warn("[ilha-router] mount({ hydrate: true }) was called in hash mode. SSR + hydration assumes the server can render the active route, but in hash mode the server only ever sees the document URL. Use plain SPA mode (`mount(target)` without `hydrate: true`) for hash-mode apps.");let t=c.querySelector(`[data-router-view]`)??c,n=L(),r=a?pe(a):void 0,i=0,o=e.render(()=>{let e=L();if(e!==n){let o=++i;f?.abort(),f=new AbortController;let s=f.signal;queueMicrotask(async()=>{if(o!==i)return;let c=I();d?.(),d=null;try{let n=g().readLocation();d=await ve(e,t,n.pathname+n.search,s,a,r)}catch(e){if(e?.name===`AbortError`)return;console.error(`[ilha-router] navigation failed:`,e),t.innerHTML=`<div data-router-view data-router-error="500"></div>`;return}finally{c()}n=e})}return``}),p=document.createElement(`div`);p.style.display=`none`,c.appendChild(p);let _=o.mount(p);return(async()=>{let e=L();if(!e)return;let n=g().readLocation(),o=n.pathname+n.search,c=s(R,`GET`,n.pathname);if(c?.data?.clientLoader){let n=++i,s=new AbortController;f=s;try{let c=await ve(e,t,o,s.signal,a,r);n===i?d=c:c()}catch(e){e?.name!==`AbortError`&&console.error(`[ilha-router] initial client loader render failed:`,e)}return}let u=c?.data?.hasLoader?await _e(o):{kind:`data`,data:{}};if(u.kind===`redirect`||u.kind===`error`)return;let p=u.kind===`data`?u.data:{},m={entries:[...u.kind===`data`?u.headEntries??[]:[]]};await Z(m,()=>e.toString(p)),l&&X(m.entries)})(),F=async()=>{let e=L(),o=++i;f?.abort();let s=new AbortController;f=s;let c=I();try{let c=g().readLocation(),l=await ve(e,t,c.pathname+c.search,s.signal,a,r);o===i?(d?.(),d=l,n=e):l()}catch(e){e?.name!==`AbortError`&&console.error(`[ilha-router] invalidate failed:`,e)}finally{c()}},()=>{l=!1,++i,F=null,f?.abort(),_(),p.remove(),d?.(),v?.(),m?.(),v=null,m=null,u!==null&&(history.scrollRestoration=u)}}let p=null,y=null,b=0;d=G.mount(c);async function x(e,t){if(p?.(),p=null,y=e,!e){let e=c?.querySelector(`[data-router-not-found]`);W&&e&&(p=W.mount(e));return}let n=c?.querySelector(`[data-router-view]`);if(!n)return;let r=g().readLocation(),i=s(R,`GET`,r.pathname),a=i?.data?.hasLoader?await _e(r.pathname+r.search,t):{kind:`data`,data:{}};if(t.aborted)return;if(a.kind===`redirect`){Ue(a.to);return}if(a.kind===`error`){let e=i?.data?.errorHandler;if(e){p=await E(()=>ye(e,n,a.status,a.message));return}let t=it(a.message);await E(()=>{n.innerHTML=`<div data-router-error="${a.status}">${t}</div>`});return}let o=a.kind===`data`?a.data:{},l={entries:[...a.kind===`data`?a.headEntries??[]:[]]},u=await Z(l,()=>e.toString(o));p=await E(()=>(X(l.entries),n.innerHTML=u,e.mount(n,o)))}f=new AbortController,x(L(),f.signal).catch(e=>{e?.name!==`AbortError`&&console.error(`[ilha-router] initial mount failed:`,e)});let ee=e.render(()=>{let e=L();if(e!==y){let t=++b;f?.abort(),f=new AbortController;let n=f.signal;queueMicrotask(()=>{if(t!==b)return;let r=I();x(e,n).catch(e=>{e?.name!==`AbortError`&&console.error(`[ilha-router] navigation failed:`,e)}).finally(r)})}return``}),S=document.createElement(`div`);S.style.display=`none`,c.appendChild(S);let C=ee.mount(S);return F=async()=>{++b,f?.abort(),f=new AbortController;let e=I();try{await x(L(),f.signal)}catch(e){e?.name!==`AbortError`&&console.error(`[ilha-router] invalidate failed:`,e)}finally{e()}},()=>{l=!1,++b,F=null,f?.abort(),p?.(),C(),S.remove(),d?.(),v?.(),m?.(),v=null,m=null,u!==null&&(history.scrollRestoration=u)}},render(e){let t=()=>(je(e,d),G.toString());return!_&&O?O.run(Se(),t):t()},async renderHydratable(e,t,n={},r){let i=await this.renderResponse(e,t,n,r);return i.kind===`html`||i.kind===`error`?i.html:`<meta http-equiv="refresh" content="0; url=${rt(i.to)}">`},async renderResponse(e,t,n={},r){if(!_){let i=await xe();if(!i.getStore())return i.run(Se(),()=>b(e,t,n,r))}return b(e,t,n,r)},async runLoader(e,t){let n=ct(e),r=s(d,`GET`,n.pathname);if(!r?.data?.island)return{kind:`not-found`};if(!r.data.loader)return{kind:`data`,data:{}};let i=z(r.params),a=t??ft(n),o=dt(t,l),u={entries:[]};try{let e=await pt(r.data.loader,n,i,a,o.signal,e=>u.entries.push(e));if(e.kind===`redirect`){let t=ut(e.to,n,c);return t.ok?{...e,to:t.to}:(console.warn(`[ilha-router] Blocked unsafe redirect target "${e.to}". Set allowExternalRedirects: true to allow cross-origin redirects.`),{kind:`error`,status:500,message:`Unsafe redirect target`})}return e.kind!==`data`||u.entries.length===0?e:{...e,head:$(u.entries),headEntries:u.entries}}finally{o.done()}},hydrate(e,t={}){if(!_)return console.warn(`[ilha-router] hydrate() called in a non-browser environment`),()=>{};let n=t.root??document.body,r=t.target??n;B();let{unmount:a}=i(e,{root:n}),o=this.mount(r,{hydrate:!0,registry:e,interceptLinks:t.interceptLinks});return()=>{a(),o()}}};async function b(e,t,n={},r){let{baseHead:i,...a}=n,o=ct(e);je(o,d);let u=s(d,`GET`,o.pathname),f=u?.data?.island??null;if(!f){let e={entries:i?[i]:[]};return p?{kind:`html`,html:`<div data-router-view data-router-not-found>${await Z(e,()=>p.toString())}</div>`,status:404,head:$(e.entries)}:{kind:`html`,html:`<div data-router-empty></div>`,status:404,head:i?$([i]):void 0}}let m={entries:i?[i]:[]},h={};if(u?.data?.loader){let e=r??ft(o),t=dt(r,l),n;try{n=await pt(u.data.loader,o,j(),e,t.signal,e=>m.entries.push(e))}finally{t.done()}if(n.kind===`redirect`){let e=ut(n.to,o,c);if(!e.ok)console.warn(`[ilha-router] Blocked unsafe redirect target "${n.to}". Set allowExternalRedirects: true to allow cross-origin redirects.`),n={kind:`error`,status:500,message:`Unsafe redirect target`};else return{kind:`redirect`,to:e.to,status:n.status}}if(n.kind===`error`){let e=u.data.errorHandler;if(e)try{let t=e({message:n.message,status:n.status},{path:A(),params:j(),search:M(),hash:N()}),r=await Z(m,()=>t.toString());return{kind:`error`,status:n.status,message:n.message,html:`<div data-router-view data-router-error="${n.status}">${r}</div>`,head:$(m.entries)}}catch(e){console.error(`[ilha-router] error boundary threw while rendering a loader error:`,e)}let t=it(n.message),r=`<div data-router-view data-router-error="${n.status}">${t}</div>`;return{kind:`error`,status:n.status,message:n.message,html:r,head:$(m.entries)}}h=n.data}let g=pe(t).get(f);return g?{kind:`html`,html:`<div data-router-view>${await Z(m,()=>f.hydratable(h,{name:g,as:`div`,snapshot:!0,...a}))}</div>`,head:$(m.entries)}:(console.warn(`[ilha-router] renderHydratable: active island for "${A()}" is not in the registry. Falling back to plain SSR — the island will not be interactive on the client.`),{kind:`html`,html:`<div data-router-view>${await Z(m,()=>f.toString(h))}</div>`,head:$(m.entries)})}return y}var ht={router:mt,navigate:U,useRoute:ke,isActive:qe,enableLinkInterception:We,prime:B,prefetch:D,beforeNavigate:Fe,afterNavigate:Ie,RouterView:G,RouterLink:Ke,loader:v,redirect:x,error:ee,composeLoaders:S,head:$e};export{ue as A,A as C,ht as D,$ as E,m as M,ke as O,j as S,mt as T,De as _,G as a,x as b,S as c,ee as d,$e as f,U as g,v as h,Ke as i,h as j,de as k,fe as l,qe as m,b as n,Ie as o,Oe as p,y as r,Fe as s,me as t,We as u,D as v,M as w,N as x,B as y};
package/dist/ssr.d.ts CHANGED
@@ -90,4 +90,5 @@ export declare class IlhaHandler {
90
90
  document(body: string, head?: SerializedHead): string;
91
91
  /** Handle an incoming request and return a full HTML / redirect Response. */
92
92
  handle(request: Request): Promise<Response>;
93
+ private handleInner;
93
94
  }
package/dist/ssr.js CHANGED
@@ -1,15 +1,15 @@
1
- import"./src-B5twcrhj.js";import{pageRouter as e,registry as t}from"ilha:pages/server";import"ilha:loaders";function n({client:e,server:t}){return e.merge(t)}function r(e){return e}function i(e){let t=e[`data-vite-dev-id`]?` data-vite-dev-id="${e[`data-vite-dev-id`]}"`:``;return`<link rel="stylesheet" href="${e.href}"${t} />`}var a=class{assets;lang;appId;clientEntry;head;renderOptions;constructor(e){this.assets=e.assets,this.lang=e.lang??`en`,this.appId=e.appId??`app`,this.clientEntry=e.clientEntry??`/entry-client.js`,this.head=e.head??{},this.renderOptions=e.renderOptions??{}}document(e,t){let n=this.assets.entry??this.clientEntry,r=this.assets.css.map(i).join(`
2
- `),a=t?.headTags.includes(`<title`)?``:`<title>Ilha</title>
3
- `,o=t?.headTags?`\n ${t.headTags}`:``,s=t?.htmlAttrs??``;return`<!doctype html>
4
- <html lang="${s.match(/\blang="([^"]*)"/)?.[1]??this.lang}"${s.replace(/\s*lang="[^"]*"/,``)}>
1
+ import"./src-8LBo_Ieg.js";import{pageRouter as e,registry as t}from"ilha:pages/server";import"ilha:loaders";function n({client:e,server:t}){return e.merge(t)}function r(e){return e}const i={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`};function a(e){return e.replace(/[&<>"']/g,e=>i[e])}function o(e){let t=e[`data-vite-dev-id`]?` data-vite-dev-id="${a(e[`data-vite-dev-id`])}"`:``;return`<link rel="stylesheet" href="${a(e.href)}"${t} />`}var s=class{assets;lang;appId;clientEntry;head;renderOptions;constructor(e){this.assets=e.assets,this.lang=e.lang??`en`,this.appId=e.appId??`app`,this.clientEntry=e.clientEntry??`/entry-client.js`,this.head=e.head??{},this.renderOptions=e.renderOptions??{}}document(e,t){let n=this.assets.entry??this.clientEntry,r=this.assets.css.map(o).join(`
2
+ `),i=t?.headTags.includes(`<title`)?``:`<title>Ilha</title>
3
+ `,s=t?.headTags?`\n ${t.headTags}`:``,c=t?.htmlAttrs??``;return`<!doctype html>
4
+ <html lang="${c.match(/\blang="([^"]*)"/)?.[1]??this.lang}"${c.replace(/\s*lang="[^"]*"/,``)}>
5
5
  <head>
6
6
  <meta charset="UTF-8" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
- ${a}<link rel="icon" href="/favicon.svg" />
9
- ${r}${o}
8
+ ${i}<link rel="icon" href="/favicon.svg" />
9
+ ${r}${s}
10
10
  </head>
11
11
  <body${t?.bodyAttrs??``}>
12
- <div id="${this.appId}">${e}</div>
13
- <script type="module" src="${n}"><\/script>
12
+ <div id="${a(this.appId)}">${e}</div>
13
+ <script type="module" src="${a(n)}"><\/script>
14
14
  </body>
15
- </html>`}async handle(n){let r=new URL(n.url);if(r.pathname===`/__ilha/loader`){let t=r.searchParams.get(`path`)??`/`,i=await e.runLoader(t,n),a=i.kind===`error`?i.status:i.kind===`not-found`?404:200;return new Response(JSON.stringify(i),{status:a,headers:{"content-type":`application/json;charset=utf-8`}})}let i=r.href.slice(r.origin.length),a={...this.renderOptions,baseHead:this.head},o=await e.renderResponse(i,t,a,n);if(o.kind===`redirect`)return new Response(null,{status:o.status,headers:{location:o.to}});let s=o.kind===`error`?o.status:o.status??200;return new Response(this.document(o.html,o.head),{status:s,headers:{"content-type":`text/html;charset=utf-8`}})}};export{a as IlhaHandler,r as appHead,n as mergeAssets};
15
+ </html>`}async handle(e){try{return await this.handleInner(e)}catch(e){return console.error(`[ilha-router] request handling failed:`,e),new Response(`Internal Server Error`,{status:500,headers:{"content-type":`text/plain;charset=utf-8`}})}}async handleInner(n){let r=new URL(n.url);if(r.pathname===`/__ilha/loader`){if(n.method!==`GET`&&n.method!==`HEAD`)return new Response(JSON.stringify({kind:`error`,status:405,message:`Method Not Allowed`}),{status:405,headers:{"content-type":`application/json;charset=utf-8`,allow:`GET, HEAD`}});let t=r.searchParams.get(`path`)??`/`,i=await e.runLoader(t,n),a=i.kind===`error`?i.status:i.kind===`not-found`?404:200;return new Response(JSON.stringify(i),{status:a,headers:{"content-type":`application/json;charset=utf-8`,"cache-control":`no-store`}})}let i=r.href.slice(r.origin.length),a={...this.renderOptions,baseHead:this.head},o=await e.renderResponse(i,t,a,n);if(o.kind===`redirect`)return new Response(null,{status:o.status,headers:{location:o.to}});let s=o.kind===`error`?o.status:o.status??200;return new Response(this.document(o.html,o.head),{status:s,headers:{"content-type":`text/html;charset=utf-8`}})}};export{s as IlhaHandler,r as appHead,n as mergeAssets};
package/dist/vite.js CHANGED
@@ -1 +1 @@
1
- import{E as e,T as t}from"./src-B5twcrhj.js";import{t as n}from"./plugin-C07ziXtR.js";function r(e={}){return n.vite(e)}export{n as ilhaPages,r as pages,t as wrapError,e as wrapLayout};
1
+ import{A as e,k as t}from"./src-8LBo_Ieg.js";import{t as n}from"./plugin-BW2tnuyF.js";function r(e={}){return n.vite(e)}export{n as ilhaPages,r as pages,t as wrapError,e as wrapLayout};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/router",
3
- "version": "0.6.9",
3
+ "version": "0.8.0",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "keywords": [
6
6
  "frontend",
@@ -1,6 +0,0 @@
1
- import{existsSync as e,readFileSync as t,watch as n}from"node:fs";import{basename as r,dirname as i,extname as a,join as o,relative as s,resolve as c,sep as l}from"node:path";import{createUnplugin as u}from"unplugin";import{mkdir as d,readFile as f,readdir as p,writeFile as m}from"node:fs/promises";function h(e){return e.replace(/\\/g,`/`)}const g=/\.(test|spec|d)\.(ts|tsx)$/,_=/^\s*export\s+(?:const|let|var|async\s+function|function)\s+load\b/m;async function v(e){try{let t=(await f(e,`utf8`)).replace(/^\s*\/\/.*$/gm,``);return _.test(t)}catch{return!1}}function y(e){return e.startsWith(`[...`)&&e.endsWith(`]`)?`**:${e.slice(4,-1)}`:e.startsWith(`[`)&&e.endsWith(`]`)?`:${e.slice(1,-1)}`:e}function b(e){return e.startsWith(`(`)&&e.endsWith(`)`)?``:y(e)}function x(e,t){let n=h(s(e,t)),r=n.slice(0,-a(n).length).split(`/`),i=[...r.slice(0,-1).map(b),y(r.at(-1))];return i.at(-1)===`index`&&i.pop(),`/`+i.filter(Boolean).join(`/`)||`/`}function S(e){return e===`/`?`index`:e.replace(/^\//,``).replace(/\*\*:[^/]*/g,e=>e.length>3?e.slice(3):`wildcard`).replace(/:/g,``).replace(/\*\*/g,`wildcard`).replace(/\//g,`-`).replace(/[^a-zA-Z0-9-]/g,``)||`page`}function C(e){return e===`/`?3:e.includes(`**`)?0:e.includes(`:`)?1:2}function w(e){return[...e].sort((e,t)=>{let n=C(t.pattern)-C(e.pattern);if(n!==0)return n;let r=t.pattern.split(`/`).length-e.pattern.split(`/`).length;return r===0?e.pattern.localeCompare(t.pattern):r})}function T(e,t,n,r){let a=h(s(e,i(t))),c=a===``?[]:a.split(`/`);return[e,...c.map((t,n)=>o(e,...c.slice(0,n+1)))].flatMap(e=>{let t=`${o(e,r)}.tsx`;if(n.has(t))return[t];let i=`${o(e,r)}.ts`;return n.has(i)?[i]:[]})}async function E(e,t=0){if(t>20)return console.warn(`[ilha:pages] Max scan depth (20) reached at ${e} — skipping`),[];let n=[];try{for(let r of await p(e,{withFileTypes:!0})){let i=o(e,r.name);r.isDirectory()?n.push(...await E(i,t+1)):r.isFile()&&/\.(ts|tsx)$/.test(r.name)&&!g.test(r.name)&&n.push(i)}}catch(e){if(e.code===`ENOENT`)return[];throw e}return n}async function D(e){let t=await E(e),n=new Set(t),i=t.filter(e=>!r(e).startsWith(`+`)),a=new Map,o=e=>{let t=a.get(e);return t||(t=v(e),a.set(e,t)),t};return Promise.all(i.map(async t=>{let r=x(e,t),i=T(e,t,n,`+layout`),a=T(e,t,n,`+error`),[s,...c]=await Promise.all([v(t),...i.map(o)]),l=i.filter((e,t)=>c[t]);return{file:t,pattern:r,name:S(r),layouts:i,errors:a,hasLoader:s,loaderLayouts:l}}))}function O(e,t){if(e.length===0){console.warn(`[ilha:pages] No pages found in ${t}`);return}let n=new Map,r=new Map;for(let t of e){let e=n.get(t.pattern);e?console.warn(`[ilha:pages] Duplicate route pattern "${t.pattern}"\n first: ${e}\n second: ${t.file}\n The first match wins — the second page will never be reached.`):n.set(t.pattern,t.file);let i=r.get(t.name);i?console.warn(`[ilha:pages] Registry name collision: "${t.name}" is used by both\n ${i}\n ${t.file}\n Hydration may not work correctly for one of these routes.`):r.set(t.name,t.file)}}function k(e){return{serverFile:o(e,`pages.server.ts`),clientFile:o(e,`pages.client.ts`),loadersFile:o(e,`loaders.ts`)}}async function A(e,t,n={}){let r=n.mode??`spa`,i=n.interceptLinks,a=r===`static`,o=w(await D(e));O(o,e),await d(t,{recursive:!0});let{serverFile:s,clientFile:c,loadersFile:l}=k(t),u=await P(s,j(o,s)),f=await P(c,M(o,c,{isStatic:a,interceptLinks:i}));a||await P(l,N(o,l,s)),(u||f)&&await F(t)}function j(e,t){let n=e=>{let n=h(s(i(t),e));return n.startsWith(`.`)?n:`./${n}`},r=[`import { router, wrapLayout, wrapError } from "@ilha/router";`,`import type { Island } from "ilha";`],a=[],o=[],c=[];for(let[t,i]of e.entries()){r.push(`import { default as _page${t} } from ${JSON.stringify(n(i.file))};`);for(let[e,a]of i.layouts.entries())r.push(`import { default as _layout${t}_${e} } from ${JSON.stringify(n(a))};`);for(let[e,a]of i.errors.entries())r.push(`import { default as _error${t}_${e} } from ${JSON.stringify(n(a))};`);let s=`_page${t}`;for(let e=i.errors.length-1;e>=0;e--)s=`wrapError(_error${t}_${e}, ${s})`;for(let e=i.layouts.length-1;e>=0;e--)s=`wrapLayout(_layout${t}_${e}, ${s})`;let l=`_wrapped${t}`;a.push(`const ${l} = ${s};`),o.push(` ${JSON.stringify(i.name)}: ${l}`+(t<e.length-1?`,`:``)),c.push(` .route(${JSON.stringify(i.pattern)}, ${l})`+(i.hasLoader||i.loaderLayouts.length>0?`.markLoader(${JSON.stringify(i.pattern)})`:``))}return[`// @generated by @ilha/router — do not edit`,`// Server module. Use for SSR and SSG/prerender.`,`// Import via: import { pageRouter, registry } from "ilha:pages/server";`,``,...r,``,...a,``,`export const registry: Record<string, Island<any, any>> = {`,...o,`};`,``,`export const pageRouter = router()`,...c,` ;`].join(`
2
- `)}function M(e,t,n){let{isStatic:r,interceptLinks:a}=n,o=e=>{let n=h(s(i(t),e));return n.startsWith(`.`)?n:`./${n}`},c=e=>`${o(e)}?client`,l=r?[`import { router as _router, wrapLayout, wrapError } from "@ilha/router";`,`import type { Island } from "ilha";`]:[`import { router, wrapLayout, wrapError } from "@ilha/router";`,`import type { Island } from "ilha";`],u=[],d=[],f=[];for(let[t,n]of e.entries()){l.push(`import { default as _page${t} } from ${JSON.stringify(c(n.file))};`);for(let[e,r]of n.layouts.entries())l.push(`import { default as _layout${t}_${e} } from ${JSON.stringify(c(r))};`);for(let[e,r]of n.errors.entries())l.push(`import { default as _error${t}_${e} } from ${JSON.stringify(c(r))};`);let i=`_page${t}`;for(let e=n.errors.length-1;e>=0;e--)i=`wrapError(_error${t}_${e}, ${i})`;for(let e=n.layouts.length-1;e>=0;e--)i=`wrapLayout(_layout${t}_${e}, ${i})`;let a=`_wrapped${t}`;u.push(`const ${a} = ${i};`),d.push(` ${JSON.stringify(n.name)}: ${a}`+(t<e.length-1?`,`:``)),r||f.push(` .route(${JSON.stringify(n.pattern)}, ${a})`+(n.hasLoader||n.loaderLayouts.length>0?`.markLoader(${JSON.stringify(n.pattern)})`:``))}let p=r?`_router({ mode: "static" })`:`router(${a===!1?`{ interceptLinks: false }`:``})`,m=[`// @generated by @ilha/router — do not edit`,`// Client module. Use for browser hydration.`,`// Import via: import { pageRouter, registry } from "ilha:pages/client";`,``,...l,``,...u,``,`export const registry: Record<string, Island<any, any>> = {`,...d,`};`,``];return r?m.push(`export const pageRouter = ${p};`):m.push(`export const pageRouter = ${p}`,...f,` ;`),m.join(`
3
- `)}function N(e,t,n){let r=e=>{let n=h(s(i(t),e));return n.startsWith(`.`)?n:`./${n}`},a=e.filter(e=>e.hasLoader||e.loaderLayouts.length>0);if(a.length===0)return[`// @generated by @ilha/router — do not edit`,`// This project has no loader exports; this file is intentionally empty.`,``,`export {};`,``].join(`
4
- `);let o=r(n).replace(/\.tsx?$/,``),c=[`import { pageRouter } from ${JSON.stringify(o)};`],l=!1,u=[];for(let[e,t]of a.entries()){let n=[];for(let[i,a]of t.loaderLayouts.entries()){let t=`_p${e}_l${i}`;c.push(`import { load as ${t} } from ${JSON.stringify(r(a))};`),n.push(t)}if(t.hasLoader){let i=`_p${e}`;c.push(`import { load as ${i} } from ${JSON.stringify(r(t.file))};`),n.push(i)}let i=n.length===1?n[0]:`composeLoaders([${n.join(`, `)}])`;n.length>1&&(l=!0),u.push(`pageRouter.attachLoader(${JSON.stringify(t.pattern)}, ${i});`)}return l&&c.unshift(`import { composeLoaders } from "@ilha/router";`),[`// @generated by @ilha/router — do not edit`,`// Server-only. Import this module from your SSR entry to wire loaders`,`// onto pageRouter. Importing it from the client is a no-op but wastes`,`// bundle size — rely on the default build pipeline to keep it out.`,``,...c,``,...u,``].join(`
5
- `)}async function P(e,t){try{if(await f(e,`utf8`)===t)return!1}catch{}return await m(e,t,`utf8`),!0}async function F(e){await P(o(e,`pages.d.ts`),[`// @generated by @ilha/router — do not edit`,``,`declare module "ilha:pages/server" {`,` import type { RouterBuilder } from "@ilha/router";`,` import type { Island } from "ilha";`,` export const pageRouter: RouterBuilder;`,` export const registry: Record<string, Island<any, any>>;`,`}`,``,`declare module "ilha:pages/client" {`,` import type { RouterBuilder } from "@ilha/router";`,` import type { Island } from "ilha";`,` export const pageRouter: RouterBuilder;`,` export const registry: Record<string, Island<any, any>>;`,`}`,``,`declare module "ilha:loaders" {`,` // Side-effect-only module. Importing it attaches loaders to pageRouter.`,`}`,``].join(`
6
- `))}const I=`\0ilha:pages/server`,L=`\0ilha:pages/client`,R=`\0ilha:loaders`,z=[I,L,R];function B(e){try{return JSON.parse(t(e,`utf8`))}catch{return null}}function V(t,n){let r=t;for(;;){let t=o(r,`node_modules`,n,`package.json`);if(e(t))return B(t);let a=i(r);if(a===r)return null;r=a}}function H(e){let t=B(o(e,`package.json`));if(!t)return[];let n={...t.dependencies??{},...t.devDependencies??{}},r=[];for(let t of Object.keys(n)){if(t===`ilha`)continue;let n=V(e,t);if(!n)continue;let i=n.peerDependencies??{},a=n.dependencies??{};(`ilha`in i||`ilha`in a)&&r.push(t)}return r}function U(e,t){let n=c(e,t.dir??`src/pages`),r=c(e,t.outDir??`.ilha`),{serverFile:i,clientFile:a,loadersFile:o}=k(r);return{pagesDir:n,outDir:r,serverFile:i,clientFile:a,loadersFile:o}}function W(e){let t,n,i,a,o,s=r=>{({pagesDir:t,outDir:n,serverFile:i,clientFile:a,loadersFile:o}=U(r,e))},c=async()=>{try{await A(t,n,{mode:e.mode,interceptLinks:e.interceptLinks})}catch(e){console.error(`[ilha:pages] codegen failed:`,e)}},u=e=>e===t||e.startsWith(t+l);return{get pagesDir(){return t},get outDir(){return n},get serverFile(){return i},get clientFile(){return a},get loadersFile(){return o},setPaths:s,regen:c,shouldRegenOnChange:e=>{if(!u(e))return!1;let t=r(e);return t.startsWith(`+`)||/\.(ts|tsx)$/.test(t)},isUnderPagesDir:u}}async function G(e,t,n){n(t)&&await e.regen()}function K(e,t,n){if(t===`ilha:pages/server`)return I;if(t===`ilha:pages/client`)return L;if(t===`ilha:loaders`)return R;if(t.endsWith(`?client`)){let e=t.slice(0,-7);return(n?c(n.replace(/\?.*$/,``),`..`,e):c(e))+`?client`}}function q(e,t){if(t===`\0ilha:pages/server`){let t=e.serverFile.replace(/\.tsx?$/,``);return`export { pageRouter, registry } from ${JSON.stringify(t)};`}if(t===`\0ilha:pages/client`){let t=e.clientFile.replace(/\.tsx?$/,``);return`export { pageRouter, registry } from ${JSON.stringify(t)};`}if(t===`\0ilha:loaders`){let t=e.loadersFile.replace(/\.tsx?$/,``);return`import ${JSON.stringify(t)};`}if(t.endsWith(`?client`)){let e=t.slice(0,-7);return`export { default } from ${JSON.stringify(e)};`}}function J(e,t){return async n=>{e.isUnderPagesDir(n)&&(await e.regen(),await t())}}function Y(e,t){let r=n(e.pagesDir,{recursive:!0},(n,r)=>{r&&t(o(e.pagesDir,r))});return()=>r.close()}const X=u((e={})=>{let t=W(e);return{name:`ilha:pages`,async buildStart(){t.pagesDir||t.setPaths(process.cwd()),this.addWatchFile?.(t.pagesDir),await t.regen()},async watchChange(e){await G(t,e,e=>t.shouldRegenOnChange(e))},resolveId(e,n){return K(t,e,n)},load(e){return q(t,e)},vite:{config(e){let t=[`ilha`,`@ilha/store`,`@ilha/router`,`alien-signals`,...H(e.root?c(e.root):process.cwd())],n=e.ssr?.noExternal,r=n===!0?!0:[...new Set([...Array.isArray(n)?n:n==null?[]:[n],...t])];return{resolve:{dedupe:[...new Set([...e.resolve?.dedupe??[],...t])]},ssr:{noExternal:r},optimizeDeps:{...e.optimizeDeps,include:[...new Set([...e.optimizeDeps?.include??[],`ilha`,`ilha/jsx-runtime`,`ilha/jsx-dev-runtime`,`@ilha/store`,`alien-signals`])]}}},configResolved(e){t.setPaths(e.root)},configureServer(e){e.watcher.add(t.pagesDir);let n=J(t,async()=>{for(let t of z){let n=e.moduleGraph.getModuleById(t);n&&e.moduleGraph.invalidateModule(n)}e.hot.send({type:`full-reload`})});e.watcher.on(`add`,n),e.watcher.on(`addDir`,n),e.watcher.on(`unlink`,n),e.watcher.on(`change`,async e=>{t.shouldRegenOnChange(e)&&await n(e)})}},rspack(e){t.setPaths(e.options.context??process.cwd());let n=J(t,()=>{e.watching&&e.invalidate()}),r;e.hooks.watchRun.tap(`ilha:pages`,()=>{r?.(),r=Y(t,n)}),e.hooks.shutdown.tap(`ilha:pages`,()=>r?.())}}});export{X as t};
@@ -1,4 +0,0 @@
1
- import e,{ISLAND_MOUNT_INTERNAL as t,context as n,html as r,mount as i}from"ilha";import{addRoute as a,createRouter as o,findRoute as s}from"rou3";const c=typeof window<`u`&&typeof document<`u`,l={readLocation(){return c?{pathname:location.pathname,search:location.search,hash:location.hash}:{pathname:`/`,search:``,hash:``}},push(e){c&&history.pushState(null,``,e)},replace(e){c&&history.replaceState(null,``,e)},onChange(e){return c?(window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)):()=>{}},toLinkHref(e){return e},extractLogicalPath(e){let t=e.getAttribute(`href`);return!t||e.protocol&&!/^(http:|https:)$/.test(e.protocol)||t.startsWith(`#`)||e.hostname&&(e.hostname!==location.hostname||e.protocol!==location.protocol)?null:e.pathname+e.search+e.hash}};function u(e){let t=e.startsWith(`#`)?e.slice(1):e,n=t===``?`/`:t.startsWith(`/`)?t:`/`+t,r=new URL(n,`http://_`);return{pathname:r.pathname,search:r.search,hash:r.hash}}const d={readLocation(){return c?u(location.hash):{pathname:`/`,search:``,hash:``}},push(e){c&&history.pushState(null,``,e.startsWith(`#`)?e:`#`+e)},replace(e){c&&history.replaceState(null,``,e.startsWith(`#`)?e:`#`+e)},onChange(e){return c?(window.addEventListener(`popstate`,e),window.addEventListener(`hashchange`,e),()=>{window.removeEventListener(`popstate`,e),window.removeEventListener(`hashchange`,e)}):()=>{}},toLinkHref(e){return e.startsWith(`#`)?e:`#`+e},extractLogicalPath(e){let t=e.getAttribute(`href`);if(!t||e.protocol&&!/^(http:|https:)$/.test(e.protocol))return null;if(t.startsWith(`#`)){let e=t.slice(1);return e===``||!e.startsWith(`/`)?null:e}if(/^https?:\/\//i.test(t))try{let e=new URL(t);if(e.origin!==location.origin||!e.hash||e.hash===`#`)return null;let n=e.hash.slice(1);return n.startsWith(`/`)?n:null}catch{return null}return t}};let f=`history`,p=l;function m(e){f=e,p=e===`hash`?d:l}function h(){return f}function g(){return p}const _=typeof window<`u`&&typeof document<`u`;function v(e){return e}var y=class{__ilhaRedirect=!0;to;status;constructor(e,t=302){this.to=e,this.status=t}},b=class{__ilhaLoaderError=!0;status;message;constructor(e,t){this.status=e,this.message=t}};function x(e,t=302){throw new y(e,t)}function S(e,t){throw new b(e,t)}function C(e){return e.length===0?async()=>({}):e.length===1?e[0]:async t=>{let n=await Promise.all(e.map(e=>e(t)));return Object.assign({},...n)}}const w=Symbol.for(`ilha.router.wrapLayout.leaf`),ee=Symbol.for(`ilha.router.wrapLayout.handler`);function te(e){let t=e.match(/^<([a-zA-Z][\w-]*)\s[^>]*>([\s\S]*)<\/\1>\s*$/);return t?t[2]:e}function ne(e){let t=e.match(/^<([a-zA-Z][\w-]*)\s([^>]*)>/);return t?{tag:t[1],attrs:t[2]}:null}const re=/<(pre|script|style|textarea)\b/i;function ie(e,t,n){let r=RegExp(`<${n}\\b`,`gi`),i=RegExp(`</${n}>`,`gi`),a=1,o=t;for(;a>0&&o<e.length;){r.lastIndex=o,i.lastIndex=o;let t=r.exec(e),n=i.exec(e);if(!n)return null;if(t&&t.index<n.index)a+=1,o=t.index+t[0].length;else{if(--a,a===0)return n.index+n[0].length;o=n.index+n[0].length}}return o}function ae(e,t){let n=t;for(;n<e.length;){if(e.startsWith(`<!--`,n)){let t=e.indexOf(`-->`,n);if(t===-1)return null;n=t+3;continue}let t=e.slice(n).match(re);if(t&&t.index!=null&&t.index>=0){let r=n+t.index,i=!1,a=n;for(;a<r;){let t=e.indexOf(`<!--`,a);if(t===-1||t>=r)break;let o=e.indexOf(`-->`,t);if(o===-1)return null;if(r<o+3){n=o+3,i=!0;break}a=o+3}if(i)continue;let o=e.indexOf(`<div`,n),s=e.indexOf(`</div>`,n);if(!(o!==-1&&o<r||s!==-1&&s<r)){let i=t[1].toLowerCase(),a=ie(e,r+t[0].length,i);if(a===null)return null;n=a;continue}}let r=e.indexOf(`<div`,n),i=e.indexOf(`</div>`,n);return i===-1&&r===-1?null:r===-1||i!==-1&&i<r?{kind:`close`,index:i}:{kind:`open`,index:r}}return null}function oe(e){let t=[];for(let n of e.matchAll(/<div\s[^>]*data-ilha-slot="k:page"[^>]*>/g)){let r=n.index+n[0].length,i=1,a=r;for(;i>0;){let n=ae(e,a);if(!n)break;if(n.kind===`open`)i+=1,a=n.index+4;else{if(--i,i===0){t.push({openEnd:r,closeStart:n.index});break}a=n.index+6}}}return t}function T(e,t,n){let r=oe(e);if(r.length===0)return e;let i=n===`innermost`?r[r.length-1]:r[0];return e.slice(0,i.openEnd)+t+e.slice(i.closeStart)}function se(e,t){let n=e[ee];if(!n)return e.toString(t);let r=e[w]??e;return n(Object.assign(r.key(`page`),{toString:()=>``})).toString(t)}async function ce(e,t,n,r){let i=te(await t.hydratable(n,r));return T(T(se(e,n),``,`innermost`),i,`innermost`)}function le(e,n){let r=n[w]??n,i=r===n?null:n,a=e(Object.assign(n.key(`page`),{toString:n.toString.bind(n)}));a[w]=r,a[ee]=e;function o(e){let t=[...e.querySelectorAll(`[data-ilha-slot="k:page"]`)].filter(t=>{let n=t.closest(`[data-ilha]`);return n===null||n===e});return t.length===0?e:t[t.length-1]}function s(e,t){let n=e=>{delete e._skipOnMount,t.setAttribute(`data-ilha-state`,JSON.stringify(e))};if(t.hasAttribute(`data-ilha-state`)){let r=e.getAttribute(`data-ilha-state`);if(r)try{n(JSON.parse(r));return}catch{}let i=t.getAttribute(`data-ilha-state`);if(i)try{let e=JSON.parse(i);delete e._skipOnMount,t.setAttribute(`data-ilha-state`,JSON.stringify(e))}catch{}return}let r=e.getAttribute(`data-ilha-state`);if(r)try{n(JSON.parse(r));return}catch{}t.childNodes.length>0&&t.setAttribute(`data-ilha-state`,`{}`)}function c(e){let n=e[t];if(typeof n!=`function`)return;e[t]=(e,t)=>{let r=e.closest(`[data-ilha]`);return r&&r!==e&&s(r,e),n(e,t)};let r=e.mount.bind(e);e.mount=(e,t)=>{let n=e.closest(`[data-ilha]`);return n&&n!==e&&s(n,e),r(e,t)}}c(r);let l=a.mount.bind(a),u=a[t];function d(e){s(e,o(e))}return a.mount=(e,t)=>(d(e),l(e,t)),a[t]=(e,t)=>(d(e),typeof u==`function`?u(e,t):{unmount:l(e,t),updateProps:()=>{}}),a.hydratable=async(e,t)=>{if(!t?.name)throw Error(`wrapLayout: hydratable requires options.name`);let n=e??{},o=await r.hydratable(n,t),s=ne(o);if(!s)return o;let c=te(o);i&&(c=await ce(i,r,n,t));let l=T(T(se(a,n),``,`first`),c,`first`);return`<${s.tag} ${s.attrs}>${l}</${s.tag}>`},a}function ue(n,r){let i=e.render(()=>{try{return r.toString()}catch(e){let t={path:k(),params:A(),search:j(),hash:M()};return n({message:e.message,status:e.status,stack:e.stack},t).toString()}});return i.mount=(e,t)=>{try{return r.mount(e,t)}catch(r){let i={path:k(),params:A(),search:j(),hash:M()},a=n({message:r.message,status:r.status,stack:r.stack},i);return e.innerHTML=a.toString(),a.mount(e,t)}},i[t]=(e,i)=>{try{let n=r[t];return typeof n==`function`?n(e,i):{unmount:r.mount(e,i),updateProps:()=>{}}}catch(t){let r={path:k(),params:A(),search:j(),hash:M()},a=n({message:t.message,status:t.status,stack:t.stack},r);return e.innerHTML=a.toString(),{unmount:a.mount(e,i),updateProps:()=>{}}}},i.hydratable=async(e,t)=>{if(!t?.name)throw Error(`wrapError: hydratable requires options.name`);return r.hydratable(e??{},t)},i}function de(e){return e}function fe(e){let t=new Map;for(let[n,r]of Object.entries(e))t.has(r)||t.set(r,n);return t}const pe=`/__ilha/loader`,E=new Map;async function D(e,t){let n=E.get(e);if(n){E.delete(e);try{return await n}catch{}}let r=`${pe}?path=${encodeURIComponent(e)}`;try{let e=await fetch(r,{signal:t,headers:{accept:`application/json`}});if(!e.ok){try{let t=await e.json();if(t&&typeof t==`object`&&`kind`in t)return t}catch{}return{kind:`error`,status:e.status,message:e.statusText}}return await e.json()}catch(e){if(e?.name===`AbortError`)throw e;return{kind:`error`,status:0,message:e?.message??`network error`}}}function O(e){if(!_||E.has(e))return;let t=e.split(`?`)[0]??``;if(!s(F,`GET`,t)?.data?.hasLoader)return;let n=D(e).catch(e=>({kind:`error`,status:0,message:e?.message??`prefetch failed`}));E.set(e,n)}async function me(e,t,n,r,i,a){if(!e)return t.innerHTML=`<div data-router-empty></div>`,()=>{};let o=!!s(F,`GET`,n.split(`?`)[0]??``)?.data?.hasLoader,c={},l=o?await D(n,r):{kind:`data`,data:{}};if(l.kind===`redirect`)return V(l.to,{replace:!0}),()=>{};if(l.kind===`error`){let e=String(l.message).replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`);return t.innerHTML=`<div data-router-view data-router-error="${l.status}">${e}</div>`,()=>{}}if(l.kind===`not-found`)return t.innerHTML=`<div data-router-empty></div>`,()=>{};c=l.data;let u={entries:[]};if(!i){console.warn(`[ilha-router] No registry provided for client-side navigation. Island will not be interactive.`);let n=await X(u,()=>e.toString(c));return Y(u.entries),t.innerHTML=`<div data-router-view>${n}</div>`,()=>{}}let d=a?.get(e)??Object.entries(i).find(([,t])=>t===e)?.[0];if(!d){console.warn(`[ilha-router] Island not found in registry for client-side navigation.`);let n=await X(u,()=>e.toString(c));return Y(u.entries),t.innerHTML=`<div data-router-view>${n}</div>`,()=>{}}let f=await X(u,()=>e.hydratable(c,{name:d,as:`div`,snapshot:!0}));Y(u.entries),t.innerHTML=`<div data-router-view>${f}</div>`;let p=t.querySelector(`[data-ilha="${d}"]`);return p?e.mount(p):()=>{}}const k=n(`router.path`,``),A=n(`router.params`,{}),j=n(`router.search`,``),M=n(`router.hash`,``);function he(){return{path:k,params:A,search:j,hash:M}}const N=n(`router.active`,null);let P=[],F=o(),I=new Map,L=new Map;function R(e){let t={};if(e)for(let[n,r]of Object.entries(e))t[n]=decodeURIComponent(r);return t}function ge(e){let t=typeof e==`string`?new URL(e,`http://localhost`):e,n=s(F,`GET`,t.pathname);k(t.pathname),A(R(n?.params)),j(t.search),M(t.hash),N(n?.data?.island??null)}function z(){let e=g().readLocation(),t=s(F,`GET`,e.pathname);k(e.pathname),A(R(t?.params)),j(e.search),M(e.hash),N(t?.data?.island??null)}function B(){_&&z()}function V(e,t={}){if(!_)return;let n=g(),r=n.readLocation();e!==r.pathname+r.search+r.hash&&(t.replace?n.replace(e):n.push(e),z())}function H(e=document,t={}){if(!_)return()=>{};let n=t.prefetch!==!1;function r(e,t){let n=e.getAttribute(`target`)===`_blank`,r=!!t&&(t.ctrlKey||t.metaKey||t.shiftKey),i=e.hasAttribute(`data-no-intercept`);return n||r||i?null:g().extractLogicalPath(e)}let i=e=>{if(e.defaultPrevented)return;let t=e.target.closest(`a`);if(!t)return;let n=r(t,e);n!==null&&(e.preventDefault(),V(n))},a=e=>{let t=e.target.closest(`a`);if(!t)return;let n=t.getAttribute(`data-prefetch`);if(n===null||n===`false`)return;let i=r(t);i!==null&&O(i.split(`#`)[0]??i)};return e.addEventListener(`click`,i),n&&e.addEventListener(`mouseover`,a,{passive:!0}),()=>{e.removeEventListener(`click`,i),n&&e.removeEventListener(`mouseover`,a)}}const U=e.render(()=>{let e=N();return e?`<div data-router-view>${e.toString()}</div>`:`<div data-router-empty></div>`}),_e=e.state(`href`,``).state(`label`,``).on(`[data-link]@click`,({state:e,event:t})=>{t.preventDefault(),V(e.href())}).on(`[data-link]@mouseenter`,({state:e})=>{let t=e.href();if(t){if(/^https?:\/\//i.test(t))try{let e=new URL(t);if(e.origin!==location.origin)return;O(e.pathname+e.search);return}catch{return}O(t)}}).render(({state:e})=>r`<a data-link data-prefetch href="${()=>g().toLinkHref(e.href())}"
2
- >${e.label}</a
3
- >`);function ve(e){let t=s(F,`GET`,k());return t?I.get(t.data.island)===e:!1}const W=`data-ilha-head`,G=`data-ilha-router-html`,ye=`data-ilha-router-body`;let K=null,q=null,be=null;async function xe(){return q||(be||=import(`node:async_hooks`).then(({AsyncLocalStorage:e})=>(q=new e,q)),be)}function Se(){return _?K:q?.getStore()??null}function Ce(e){let t=Se();if(!t){_||console.warn(`[ilha-router] head() called outside an SSR render window — ignored.`);return}t.entries.push(e)}function J(e){return typeof CSS<`u`&&typeof CSS.escape==`function`?CSS.escape(e):e.replace(/\\/g,`\\\\`).replace(/"/g,`\\"`)}function we(e){return`charset`in e?`meta[charset][${W}]`:`name`in e?`meta[name="${J(e.name)}"][${W}]`:`property`in e?`meta[property="${J(e.property)}"][${W}]`:`http-equiv`in e?`meta[http-equiv="${J(e[`http-equiv`])}"][${W}]`:null}function Te(e){return e.rel&&e.href?`link[rel="${J(e.rel)}"][href="${J(e.href)}"][${W}]`:null}function Y(e){if(!_)return;let t,n,r=[],i=[],a={},o={};for(let s of e)s.title!==void 0&&(t=s.title),s.titleTemplate!==void 0&&(n=s.titleTemplate),s.meta&&r.push(...s.meta),s.link&&i.push(...s.link),s.htmlAttrs&&(a={...a,...s.htmlAttrs}),s.bodyAttrs&&(o={...o,...s.bodyAttrs});let s=ke(t,n);s!==void 0&&(document.title=s);let c=Q(r,Oe),l=Q(i,e=>`${e.rel??``}:${e.href??``}`),u=new Set;for(let e of c){let t=we(e);if(!t)continue;let n=document.querySelector(t);n||(n=document.createElement(`meta`),n.setAttribute(W,``),document.head.appendChild(n));for(let[t,r]of Object.entries(e))n.setAttribute(t,r);u.add(n)}for(let e of l){let t=Te(e),n=t?document.querySelector(t):null;n||(n=document.createElement(`link`),n.setAttribute(W,``),document.head.appendChild(n));for(let[t,r]of Object.entries(e))n.setAttribute(t,r);u.add(n)}for(let e of[...document.head.querySelectorAll(`[${W}]`)])u.has(e)||e.remove();let d=document.documentElement,f=(d.getAttribute(G)??``).split(/\s+/).filter(Boolean);for(let e of f)d.removeAttribute(e);let p=Object.keys(a);for(let[e,t]of Object.entries(a))d.setAttribute(e,t);p.length?d.setAttribute(G,p.join(` `)):d.removeAttribute(G);let m=document.body,h=(m.getAttribute(ye)??``).split(/\s+/).filter(Boolean);for(let e of h)m.removeAttribute(e);let g=Object.keys(o);for(let[e,t]of Object.entries(o))m.setAttribute(e,t);g.length?m.setAttribute(ye,g.join(` `)):m.removeAttribute(ye)}async function X(e,t){if(_){let n=K;K=e;try{return await t()}finally{K=n}}return await(await xe()).run(e,()=>Promise.resolve(t()))}const Ee={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`};function De(e){return String(e).replace(/[&<>"']/g,e=>Ee[e])}function Z(e){return Object.entries(e).map(([e,t])=>` ${e}="${De(t)}"`).join(``)}function Oe(e){return`charset`in e?`charset`:`name`in e?`name:${e.name}`:`property`in e?`property:${e.property}`:`http-equiv`in e?`http-equiv:${e[`http-equiv`]}`:JSON.stringify(e)}function Q(e,t){let n=new Map;for(let r of e)n.set(t(r),r);return[...n.values()]}function ke(e,t){return t===void 0?e:typeof t==`function`?t(e):t.replace(/%s/g,e??``)}function $(e){let t,n,r=[],i=[],a=[],o={},s={};for(let c of e)c.title!==void 0&&(t=c.title),c.titleTemplate!==void 0&&(n=c.titleTemplate),c.meta&&r.push(...c.meta),c.link&&i.push(...c.link),c.script&&a.push(...c.script),c.htmlAttrs&&(o={...o,...c.htmlAttrs}),c.bodyAttrs&&(s={...s,...c.bodyAttrs});let c=ke(t,n),l=[];c!==void 0&&l.push(`<title>${De(c)}</title>`);for(let e of Q(r,Oe))l.push(`<meta${Z({...e,[W]:``})} />`);for(let e of Q(i,e=>`${e.rel??``}:${e.href??``}`))l.push(`<link${Z({...e,[W]:``})} />`);for(let e of a){let{children:t,...n}=e;l.push(`<script${Z(n)}>${t??``}<\/script>`)}return{headTags:l.join(`
4
- `),htmlAttrs:Z(o),bodyAttrs:Z(s)}}function Ae(e){return typeof e==`string`?new URL(e,`http://localhost`):e}function je(e){try{return new Request(e.toString())}catch{return{url:e.toString(),headers:new Headers}}}async function Me(e,t,n,r,i,a){let o=[],s=a??(e=>o.push(e));try{let a={kind:`data`,data:await e({params:n,request:r,url:t,signal:i,head:s})??{}};return o.length>0&&(a.head=$(o)),a}catch(e){return e instanceof y?{kind:`redirect`,to:e.to,status:e.status}:e instanceof b?{kind:`error`,status:e.status,message:e.message}:{kind:`error`,status:e?.status??500,message:e?.message??`Loader failed`}}}function Ne(t={}){let n=t.mode??`spa`,r=t.interceptLinks!==!1;P=[],F=o(),I=new Map,L=new Map;let c=null,l=null,u={route(e,t,n){let r=!!n,i={island:t,loader:n,hasLoader:r};return P.push({pattern:e,island:t,loader:n,hasLoader:r}),a(F,`GET`,e,i),L.set(e,i),I.has(t)||I.set(t,e),u},attachLoader(e,t){let n=L.get(e);if(!n)return console.warn(`[ilha-router] attachLoader("${e}", …): pattern was never registered via .route(). The loader will be ignored.`),u;n.loader=t,n.hasLoader=!0;let r=P.find(t=>t.pattern===e);return r&&(r.loader=t,r.hasLoader=!0),u},markLoader(e){let t=L.get(e);if(!t)return console.warn(`[ilha-router] markLoader("${e}"): pattern was never registered via .route(). The loader marker will be ignored.`),u;t.hasLoader=!0;let n=P.find(t=>t.pattern===e);return n&&(n.hasLoader=!0),u},routes(){return P.map(e=>({...e}))},prime:B,hydrateStatic(e,t={}){if(!_)return()=>{};let n=t.root??document.body;B();let{unmount:r}=i(e,{root:n});return r},mount(t,{hydrate:i=!1,registry:a,interceptLinks:o}={}){if(!_)return console.warn(`[ilha-router] mount() called in a non-browser environment`),()=>{};let u=typeof t==`string`?document.querySelector(t):t;if(!u)return console.warn(`[ilha-router] No element found for selector "${t}"`),()=>{};if(z(),n===`static`)return console.warn(`[ilha-router] router.mount() called in static mode. Use router.hydrateStatic(registry) instead.`),()=>{};let d=!0;c=g().onChange(()=>{d&&z()}),l=o??r?H(document):null;let f=null,p=null;if(i){h()===`hash`&&console.warn("[ilha-router] mount({ hydrate: true }) was called in hash mode. SSR + hydration assumes the server can render the active route, but in hash mode the server only ever sees the document URL. Use plain SPA mode (`mount(target)` without `hydrate: true`) for hash-mode apps.");let t=u.querySelector(`[data-router-view]`)??u,n=N(),r=a?fe(a):void 0,i=0,o=e.render(()=>{let e=N();if(e!==n){let o=++i;p?.abort(),p=new AbortController;let s=p.signal;queueMicrotask(async()=>{if(o===i){f?.();try{let n=g().readLocation();f=await me(e,t,n.pathname+n.search,s,a,r)}catch(e){if(e?.name===`AbortError`)return;throw e}n=e}})}return``}),m=document.createElement(`div`);m.style.display=`none`,u.appendChild(m);let _=o.mount(m);return(async()=>{let e=N();if(!e)return;let t=g().readLocation(),n=t.pathname+t.search,r=s(F,`GET`,t.pathname)?.data?.hasLoader?await D(n):{kind:`data`,data:{}};if(r.kind===`redirect`||r.kind===`error`)return;let i=r.kind===`data`?r.data:{},a={entries:[]};await X(a,()=>e.toString(i)),d&&Y(a.entries)})(),()=>{d=!1,++i,p?.abort(),_(),m.remove(),f?.(),l?.(),c?.(),l=null,c=null}}let m=null,v=null,y=0;f=U.mount(u);async function b(e,t){if(m?.(),m=null,v=e,!e)return;let n=u?.querySelector(`[data-router-view]`);if(!n)return;let r=g().readLocation(),i=s(F,`GET`,r.pathname)?.data?.hasLoader?await D(r.pathname+r.search,t):{kind:`data`,data:{}};if(t.aborted)return;if(i.kind===`redirect`){V(i.to,{replace:!0});return}let a=i.kind===`data`?i.data:{},o={entries:[]},c=await X(o,()=>e.toString(a));Y(o.entries),n.innerHTML=c,m=e.mount(n,a)}p=new AbortController,b(N(),p.signal);let x=e.render(()=>{let e=N();if(e!==v){let t=++y;p?.abort(),p=new AbortController;let n=p.signal;queueMicrotask(()=>{t===y&&b(e,n)})}return``}),S=document.createElement(`div`);S.style.display=`none`,u.appendChild(S);let C=x.mount(S);return()=>{d=!1,++y,p?.abort(),m?.(),C(),S.remove(),f?.(),l?.(),c?.(),l=null,c=null}},render(e){return ge(e),U.toString()},async renderHydratable(e,t,n={},r){let i=await this.renderResponse(e,t,n,r);return i.kind===`html`||i.kind===`error`?i.html:`<meta http-equiv="refresh" content="0; url=${i.to}">`},async renderResponse(e,t,n={},r){let{baseHead:i,...a}=n,o=Ae(e);ge(o);let c=s(F,`GET`,o.pathname),l=c?.data?.island??null;if(!l)return{kind:`html`,html:`<div data-router-empty></div>`,status:404,head:i?$([i]):void 0};let u={entries:i?[i]:[]},d={};if(c?.data?.loader){let e=r??je(o),t=new AbortController,n=await Me(c.data.loader,o,A(),e,t.signal,e=>u.entries.push(e));if(n.kind===`redirect`)return{kind:`redirect`,to:n.to,status:n.status};if(n.kind===`error`){let e=String(n.message).replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`),t=`<div data-router-view data-router-error="${n.status}">${e}</div>`;return{kind:`error`,status:n.status,message:n.message,html:t,head:$(u.entries)}}d=n.data}let f=fe(t).get(l);return f?{kind:`html`,html:`<div data-router-view>${await X(u,()=>l.hydratable(d,{name:f,as:`div`,snapshot:!0,...a}))}</div>`,head:$(u.entries)}:(console.warn(`[ilha-router] renderHydratable: active island for "${k()}" is not in the registry. Falling back to plain SSR — the island will not be interactive on the client.`),{kind:`html`,html:`<div data-router-view>${await X(u,()=>l.toString(d))}</div>`,head:$(u.entries)})},async runLoader(e,t){let n=Ae(e),r=s(F,`GET`,n.pathname);if(!r?.data?.island)return{kind:`not-found`};if(!r.data.loader)return{kind:`data`,data:{}};let i=R(r.params),a=t??je(n),o=new AbortController,c={entries:[]};return Me(r.data.loader,n,i,a,o.signal,e=>c.entries.push(e)).then(e=>e.kind!==`data`||c.entries.length===0?e:{...e,head:$(c.entries)})},hydrate(e,t={}){if(!_)return console.warn(`[ilha-router] hydrate() called in a non-browser environment`),()=>{};let n=t.root??document.body,r=t.target??n;B();let{unmount:a}=i(e,{root:n}),o=this.mount(r,{hydrate:!0,registry:e,interceptLinks:t.interceptLinks});return()=>{a(),o()}}};return u}var Pe={router:Ne,navigate:V,useRoute:he,isActive:ve,enableLinkInterception:H,prime:B,prefetch:O,RouterView:U,RouterLink:_e,loader:v,redirect:x,error:S,composeLoaders:C,head:Ce};export{Pe as C,h as D,le as E,m as O,$ as S,ue as T,M as _,U as a,j as b,H as c,ve as d,v as f,x as g,B as h,_e as i,S as l,O as m,b as n,C as o,V as p,y as r,de as s,pe as t,Ce as u,A as v,he as w,Ne as x,k as y};