@ilha/router 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,24 @@ 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;
131
154
  }
132
155
  export interface HydratableRenderOptions extends Partial<Omit<HydratableOptions, "name">> {
133
156
  /**
@@ -253,39 +276,15 @@ export declare const LOADER_ENDPOINT = "/__ilha/loader";
253
276
  * navigation) or is superseded by another prefetch.
254
277
  */
255
278
  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
- };
279
+ export declare function routePath(value?: string): string;
280
+ export declare function routeParams(value?: Record<string, string>): Record<string, string>;
281
+ export declare function routeSearch(value?: string): string;
282
+ export declare function routeHash(value?: string): string;
272
283
  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
- };
284
+ path: typeof routePath;
285
+ params: typeof routeParams;
286
+ search: typeof routeSearch;
287
+ hash: typeof routeHash;
289
288
  };
290
289
  /**
291
290
  * Prime route context signals from the current `location` so that islands
@@ -293,6 +292,27 @@ export declare function useRoute(): {
293
292
  * render — preventing a mismatch morph that would destroy hydrated bindings.
294
293
  */
295
294
  export declare function prime(): void;
295
+ export interface Navigation {
296
+ /** Logical URL (path + search + hash) being navigated away from. */
297
+ from: string;
298
+ /** Logical URL being navigated to. */
299
+ to: string;
300
+ /** `"push"`/`"replace"` for programmatic navigations, `"pop"` for history traversal. */
301
+ type: "push" | "replace" | "pop";
302
+ }
303
+ export type BeforeNavigateHook = (nav: Navigation & {
304
+ cancel(): void;
305
+ }) => void;
306
+ export type AfterNavigateHook = (nav: Navigation) => void;
307
+ /**
308
+ * Run before a programmatic navigation commits. Call `nav.cancel()` to keep
309
+ * the current URL (e.g. unsaved-changes guards). Not invoked for browser
310
+ * back/forward — the URL has already changed by the time `popstate` fires.
311
+ * Returns an unsubscribe function.
312
+ */
313
+ export declare function beforeNavigate(fn: BeforeNavigateHook): () => void;
314
+ /** Run after a navigation (push, replace, or pop) has committed. Returns an unsubscribe function. */
315
+ export declare function afterNavigate(fn: AfterNavigateHook): () => void;
296
316
  export declare function navigate(to: string, opts?: NavigateOptions): void;
297
317
  export interface LinkInterceptionOptions {
298
318
  /**
@@ -306,7 +326,15 @@ export interface LinkInterceptionOptions {
306
326
  export declare function enableLinkInterception(root?: Element | Document, options?: LinkInterceptionOptions): () => void;
307
327
  export declare const RouterView: Island<Record<string, unknown>, Record<never, never>>;
308
328
  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;
329
+ export interface IsActiveOptions {
330
+ /**
331
+ * When `false`, `isActive("/docs")` also matches nested paths like
332
+ * `/docs/getting-started` (prefix match on the current path).
333
+ * Default: `true` (the matched route's pattern must equal `pattern`).
334
+ */
335
+ exact?: boolean;
336
+ }
337
+ export declare function isActive(pattern: string, options?: IsActiveOptions): boolean;
310
338
  /**
311
339
  * Contribute `<head>` data from inside an island's `.render()` body or a
312
340
  * layout. During SSR this collects into the active render window; on the
@@ -330,6 +358,8 @@ declare const _default: {
330
358
  enableLinkInterception: typeof enableLinkInterception;
331
359
  prime: typeof prime;
332
360
  prefetch: typeof prefetch;
361
+ beforeNavigate: typeof beforeNavigate;
362
+ afterNavigate: typeof afterNavigate;
333
363
  RouterView: Island<Record<string, unknown>, Record<never, never>>;
334
364
  RouterLink: Island<Record<string, unknown>, Omit<Omit<Record<never, never>, K> & Record<"href", string>, "label"> & Record<"label", string>>;
335
365
  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-Cgv3m3sQ.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,O as i,S as a,T as o,_ as s,a as c,b as l,c as u,d,f,g as p,h as m,i as h,k as g,l as _,m as v,n as y,o as b,p as x,r as S,s as C,t as w,u as T,v as E,w as D,x as O,y as k}from"./src-BHVpXJIQ.js";export{w as LOADER_ENDPOINT,y as LoaderError,S as Redirect,h as RouterLink,c as RouterView,b as afterNavigate,C as beforeNavigate,u as composeLoaders,o as default,_ as defineLayout,T as enableLinkInterception,d as error,g as getHistoryMode,f as head,x as isActive,v as loader,m as navigate,p as prefetch,s as prime,E as redirect,k as routeHash,l as routeParams,O as routePath,a as routeSearch,t as router,D as serializeHead,e as setHistoryMode,r as useRoute,n as wrapError,i as wrapLayout};
@@ -1,6 +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;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(`
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,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 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,n.strict===!0),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
4
  `)}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
5
  `)}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
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(`
5
7
  `)}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};
8
+ `))}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,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 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 r=t.slice(0,-7),i=n?c(n.replace(/\?.*$/,``),`..`,r):c(r);return!e.pagesDir||!e.isUnderPagesDir(i)?void 0:i+`?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(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 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};
package/dist/plugin.d.ts CHANGED
@@ -26,6 +26,11 @@ export interface IlhaPagesOptions {
26
26
  * Default: `true`.
27
27
  */
28
28
  interceptLinks?: boolean;
29
+ /**
30
+ * Fail codegen on duplicate route patterns / registry name collisions
31
+ * instead of warning. Recommended for CI/production builds. Default: `false`.
32
+ */
33
+ strict?: boolean;
29
34
  }
30
35
  export declare function resolvePluginPaths(root: string, options: IlhaPagesOptions): {
31
36
  pagesDir: string;
@@ -47,7 +52,7 @@ export interface PagesPluginState {
47
52
  }
48
53
  export declare function createPagesPluginState(options: IlhaPagesOptions): PagesPluginState;
49
54
  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;
55
+ export declare function resolvePagesId(state: PagesPluginState, id: string, importer?: string): string | undefined;
51
56
  export declare function loadPagesModule(state: PagesPluginState, id: string): string | undefined;
52
57
  type InvalidateModules = () => void | Promise<void>;
53
58
  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-Cgv3m3sQ.js";import{t as n}from"./plugin-DBB11WaX.js";function r(e={}){return n.rolldown(e)}export{n as ilhaPages,r as pages,t as wrapError,e as wrapLayout};
1
+ import{D as e,O as t}from"./src-BHVpXJIQ.js";import{t as n}from"./plugin-nkvFqO15.js";function r(e={}){return n.rolldown(e)}export{n as ilhaPages,r as pages,e as wrapError,t as wrapLayout};
package/dist/rspack.js CHANGED
@@ -1 +1 @@
1
- import{E as e,T as t}from"./src-Cgv3m3sQ.js";import{t as n}from"./plugin-DBB11WaX.js";function r(e={}){return n.rspack(e)}export{n as ilhaPages,r as pages,t as wrapError,e as wrapLayout};
1
+ import{D as e,O as t}from"./src-BHVpXJIQ.js";import{t as n}from"./plugin-nkvFqO15.js";function r(e={}){return n.rspack(e)}export{n as ilhaPages,r as pages,e as wrapError,t 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;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=`${me}?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 D(e){if(!_)return;let t=T.get(e);if(t&&Date.now()<=t.expires)return;let n=e.split(`?`)[0]??``;if(!s(F,`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 he(e,t,n,r,i,a){if(!e){if(V){t.innerHTML=`<div data-router-view data-router-not-found>${V.toString()}</div>`;let e=t.firstElementChild;return e?V.mount(e):()=>{}}return t.innerHTML=`<div data-router-empty></div>`,()=>{}}let o=!!s(F,`GET`,n.split(`?`)[0]??``)?.data?.hasLoader,c={},l=o?await E(n,r):{kind:`data`,data:{}};if(l.kind===`redirect`)return Le(l.to),()=>{};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):()=>{}}let O=null,ge=null;async function _e(){return O||(ge||=import(`node:async_hooks`).then(({AsyncLocalStorage:e})=>(O=new e,O)),ge)}_||_e().catch(()=>{});function k(){return _?null:O?.getStore()??null}function ve(){return{path:``,params:{},search:``,hash:``,island:null}}const ye=n(`router.path`,``),be=n(`router.params`,{}),xe=n(`router.search`,``),Se=n(`router.hash`,``);function A(e){let t=k();return arguments.length>0?t?t.path=e:(ye(e),e):t?t.path:ye()}function j(e){let t=k();return arguments.length>0?t?t.params=e:(be(e),e):t?t.params:be()}function M(e){let t=k();return arguments.length>0?t?t.search=e:(xe(e),e):t?t.search:xe()}function N(e){let t=k();return arguments.length>0?t?t.hash=e:(Se(e),e):t?t.hash:Se()}function Ce(){return{path:A,params:j,search:M,hash:N}}const we=n(`router.active`,null);function P(e){let t=k();return arguments.length>0?t?t.island=e??null:(we(e??null),e??null):t?t.island:we()}let F=o();function Te(e){let t={};if(e)for(let[n,r]of Object.entries(e))t[n]=decodeURIComponent(r);return t}function Ee(e,t=F){let n=typeof e==`string`?new URL(e,`http://localhost`):e,r=s(t,`GET`,n.pathname);A(n.pathname),j(Te(r?.params)),M(n.search),N(n.hash),P(r?.data?.island??null)}function I(){let e=g().readLocation(),t=s(F,`GET`,e.pathname);A(e.pathname),j(Te(t?.params)),M(e.search),N(e.hash),P(t?.data?.island??null)}function L(){_&&I()}const De=new Set,Oe=new Set;function ke(e){return De.add(e),()=>De.delete(e)}function Ae(e){return Oe.add(e),()=>Oe.delete(e)}function je(e){for(let t of Oe)try{t(e)}catch(e){console.error(`[ilha-router] afterNavigate hook threw:`,e)}}const Me=new Map;let Ne=0,R=0;function z(){if(!_)return 0;let e=history.state;return typeof e?.__ilhaNavKey==`number`?e.__ilhaNavKey:0}function Pe(){Me.set(z(),{x:window.scrollX,y:window.scrollY})}function Fe(e){requestAnimationFrame(()=>{if(e&&e!==`#`){let t=document.getElementById(e.slice(1))??document.querySelector(`a[name="${J(e.slice(1))}"]`);if(t){t.scrollIntoView();return}}window.scrollTo(0,0)})}function Ie(){let e=Me.get(z());e&&requestAnimationFrame(()=>window.scrollTo(e.x,e.y))}function B(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 De)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:z()}):(Pe(),Ne=Math.max(Ne+1,z()+1),n.push(e,{__ilhaNavKey:Ne})),R=z(),I(),t.scroll!==!1&&Fe(n.readLocation().hash),je({from:i,to:e,type:a}))}function Le(e){if(/^https?:\/\//i.test(e)){try{let t=new URL(e);if(t.origin===location.origin){B(t.pathname+t.search+t.hash,{replace:!0});return}}catch{return}location.assign(e);return}B(e,{replace:!0})}function Re(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(),B(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 V=null;const H=e.render(()=>{let e=P();return e?`<div data-router-view>${e.toString()}</div>`:V?`<div data-router-view data-router-not-found>${V.toString()}</div>`:`<div data-router-empty></div>`}),ze=e.state(`href`,``).state(`label`,``).on(`[data-link]@click`,({state:e,event:t})=>{t.preventDefault(),B(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 Be(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(F,`GET`,A());return n?n.data.pattern===e:!1}const U=`data-ilha-head`,W=`data-ilha-router-html`,G=`data-ilha-router-body`;let K=null,q=null,Ve=null;async function He(){return q||(Ve||=import(`node:async_hooks`).then(({AsyncLocalStorage:e})=>(q=new e,q)),Ve)}function Ue(){return _?K:q?.getStore()??null}function We(e){let t=Ue();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 Ge(e){return`charset`in e?`meta[charset][${U}]`:`name`in e?`meta[name="${J(e.name)}"][${U}]`:`property`in e?`meta[property="${J(e.property)}"][${U}]`:`http-equiv`in e?`meta[http-equiv="${J(e[`http-equiv`])}"][${U}]`:null}function Ke(e){return e.rel&&e.href?`link[rel="${J(e.rel)}"][href="${J(e.href)}"][${U}]`: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=Xe(t,n);s!==void 0&&(document.title=s);let c=Q(r,Ye),l=Q(i,e=>`${e.rel??``}:${e.href??``}`),u=new Set;for(let e of c){let t=Ge(e);if(!t)continue;let n=document.querySelector(t);n||(n=document.createElement(`meta`),n.setAttribute(U,``),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=Ke(e),n=t?document.querySelector(t):null;n||(n=document.createElement(`link`),n.setAttribute(U,``),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(`[${U}]`)])u.has(e)||e.remove();let d=document.documentElement,f=(d.getAttribute(W)??``).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(W,p.join(` `)):d.removeAttribute(W);let m=document.body,h=(m.getAttribute(G)??``).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(G,g.join(` `)):m.removeAttribute(G)}async function X(e,t){if(_){let n=K;K=e;try{return await t()}finally{K=n}}return await(await He()).run(e,()=>Promise.resolve(t()))}const qe={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`};function Je(e){return String(e).replace(/[&<>"']/g,e=>qe[e])}function Z(e){return Object.entries(e).map(([e,t])=>` ${e}="${Je(t)}"`).join(``)}function Ye(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 Xe(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=Xe(t,n),l=[];c!==void 0&&l.push(`<title>${Je(c)}</title>`);for(let e of Q(r,Ye))l.push(`<meta${Z({...e,[U]:``})} />`);for(let e of Q(i,e=>`${e.rel??``}:${e.href??``}`))l.push(`<link${Z({...e,[U]:``})} />`);for(let e of a){let{children:t,...n}=e,r=(t??``).replace(/<\/script/gi,`<\\/script`);l.push(`<script${Z(n)}>${r}<\/script>`)}return{headTags:l.join(`
4
+ `),htmlAttrs:Z(o),bodyAttrs:Z(s)}}function Ze(e){return typeof e==`string`?new URL(e,`http://localhost`):e}function Qe(){try{return typeof process<`u`&&!!process.env&&process.env.NODE_ENV!==`production`}catch{return!1}}function $e(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 et(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 tt(e){try{return new Request(e.toString())}catch{return{url:e.toString(),headers:new Headers}}}async function nt(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:Qe()?e?.message??`Loader failed`:`Internal error`})}}function rt(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;F=d,V=p;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},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:L,hydrateStatic(e,t={}){if(!_)return()=>{};let n=t.root??document.body;L();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(I(),R=z(),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();Me.set(R,{x:window.scrollX,y:window.scrollY}),R=z(),I(),Ie(),je({from:e,to:A()+M()+N(),type:`pop`})}),v=o??r?Re(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=P(),r=a?pe(a):void 0,i=0,o=e.render(()=>{let e=P();if(e!==n){let o=++i;f?.abort(),f=new AbortController;let s=f.signal;queueMicrotask(async()=>{if(o===i){d?.(),d=null;try{let n=g().readLocation();d=await he(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}n=e}})}return``}),p=document.createElement(`div`);p.style.display=`none`,c.appendChild(p);let _=o.mount(p);return(async()=>{let e=P();if(!e)return;let t=g().readLocation(),n=t.pathname+t.search,r=s(F,`GET`,t.pathname)?.data?.hasLoader?await E(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)),l&&Y(a.entries)})(),()=>{l=!1,++i,f?.abort(),_(),p.remove(),d?.(),v?.(),m?.(),v=null,m=null,u!==null&&(history.scrollRestoration=u)}}let p=null,y=null,b=0;d=H.mount(c);async function x(e,t){if(p?.(),p=null,y=e,!e){let e=c?.querySelector(`[data-router-not-found]`);V&&e&&(p=V.mount(e));return}let n=c?.querySelector(`[data-router-view]`);if(!n)return;let r=g().readLocation(),i=s(F,`GET`,r.pathname)?.data?.hasLoader?await E(r.pathname+r.search,t):{kind:`data`,data:{}};if(t.aborted)return;if(i.kind===`redirect`){Le(i.to);return}let a=i.kind===`data`?i.data:{},o={entries:[]},l=await X(o,()=>e.toString(a));Y(o.entries),n.innerHTML=l,p=e.mount(n,a)}f=new AbortController,x(P(),f.signal).catch(e=>{e?.name!==`AbortError`&&console.error(`[ilha-router] initial mount failed:`,e)});let ee=e.render(()=>{let e=P();if(e!==y){let t=++b;f?.abort(),f=new AbortController;let n=f.signal;queueMicrotask(()=>{t===b&&x(e,n).catch(e=>{e?.name!==`AbortError`&&console.error(`[ilha-router] navigation failed:`,e)})})}return``}),S=document.createElement(`div`);S.style.display=`none`,c.appendChild(S);let C=ee.mount(S);return()=>{l=!1,++b,f?.abort(),p?.(),C(),S.remove(),d?.(),v?.(),m?.(),v=null,m=null,u!==null&&(history.scrollRestoration=u)}},render(e){let t=()=>(Ee(e,d),H.toString());return!_&&O?O.run(ve(),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=${Je(i.to)}">`},async renderResponse(e,t,n={},r){if(!_){let i=await _e();if(!i.getStore())return i.run(ve(),()=>b(e,t,n,r))}return b(e,t,n,r)},async runLoader(e,t){let n=Ze(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=Te(r.params),a=t??tt(n),o=et(t,l),u={entries:[]};try{let e=await nt(r.data.loader,n,i,a,o.signal,e=>u.entries.push(e));if(e.kind===`redirect`){let t=$e(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)}}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;L();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=Ze(e);Ee(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 X(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??tt(o),t=et(r,l),n;try{n=await nt(u.data.loader,o,j(),e,t.signal,e=>m.entries.push(e))}finally{t.done()}if(n.kind===`redirect`){let e=$e(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=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:$(m.entries)}}h=n.data}let g=pe(t).get(f);return g?{kind:`html`,html:`<div data-router-view>${await X(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 X(m,()=>f.toString(h))}</div>`,head:$(m.entries)})}return y}var it={router:rt,navigate:B,useRoute:Ce,isActive:Be,enableLinkInterception:Re,prime:L,prefetch:D,beforeNavigate:ke,afterNavigate:Ae,RouterView:H,RouterLink:ze,loader:v,redirect:x,error:ee,composeLoaders:S,head:We};export{m as A,rt as C,de as D,Ce as E,ue as O,M as S,it as T,L as _,H as a,j as b,S as c,ee as d,We as f,D as g,B as h,ze as i,h as k,fe as l,v as m,b as n,Ae as o,Be as p,y as r,ke as s,me as t,Re as u,x as v,$ as w,A as x,N 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-Cgv3m3sQ.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-BHVpXJIQ.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-Cgv3m3sQ.js";import{t as n}from"./plugin-DBB11WaX.js";function r(e={}){return n.vite(e)}export{n as ilhaPages,r as pages,t as wrapError,e as wrapLayout};
1
+ import{D as e,O as t}from"./src-BHVpXJIQ.js";import{t as n}from"./plugin-nkvFqO15.js";function r(e={}){return n.vite(e)}export{n as ilhaPages,r as pages,e as wrapError,t as wrapLayout};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/router",
3
- "version": "0.6.8",
3
+ "version": "0.7.0",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "keywords": [
6
6
  "frontend",
@@ -64,14 +64,14 @@
64
64
  "test": "bun test"
65
65
  },
66
66
  "dependencies": {
67
- "rou3": "0.8.1",
68
- "unplugin": "3.0.0"
67
+ "rou3": "0.9.0",
68
+ "unplugin": "3.3.0"
69
69
  },
70
70
  "devDependencies": {
71
- "ilha": "0.8.5",
72
- "vite": "^8.1.0"
71
+ "ilha": "0.9.0",
72
+ "vite": "^8.1.3"
73
73
  },
74
74
  "peerDependencies": {
75
- "ilha": ">=0.8.5"
75
+ "ilha": ">=0.9.0"
76
76
  }
77
77
  }
@@ -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};