@ilha/router 0.6.6 → 0.6.8

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.
@@ -0,0 +1,21 @@
1
+ export type PagesMode = "spa" | "static";
2
+ export interface GenerateOptions {
3
+ /** Client navigation mode. Default: `spa`. */
4
+ mode?: PagesMode;
5
+ /**
6
+ * Whether to install client-side link interception. Only meaningful in `spa`
7
+ * mode. Default: `true`.
8
+ */
9
+ interceptLinks?: boolean;
10
+ }
11
+ /** Paths for all generated files derived from the base output directory. */
12
+ export interface GeneratedPaths {
13
+ /** Server module: raw imports, full route graph. `ilha:pages/server` */
14
+ serverFile: string;
15
+ /** Client module: ?client imports, browser-optimised. `ilha:pages/client` */
16
+ clientFile: string;
17
+ /** Server-only loaders side-effect module. `ilha:loaders` */
18
+ loadersFile: string;
19
+ }
20
+ export declare function resolveGeneratedPaths(outDir: string): GeneratedPaths;
21
+ export declare function generate(pagesDir: string, outDir: string, options?: GenerateOptions): Promise<void>;
package/dist/hash.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ export type HistoryMode = "history" | "hash";
2
+ export interface LogicalLocation {
3
+ pathname: string;
4
+ search: string;
5
+ hash: string;
6
+ }
7
+ export interface HistoryAdapter {
8
+ /** Read the current logical URL (the one routes are matched against). */
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;
14
+ /** Subscribe to logical-URL changes. Returns a cleanup function. */
15
+ onChange(handler: () => void): () => void;
16
+ /**
17
+ * Convert a logical href (what the user writes, e.g. "/users/42") into
18
+ * the actual DOM href attribute (e.g. "#/users/42" in hash mode).
19
+ */
20
+ toLinkHref(logicalPath: string): string;
21
+ /**
22
+ * Extract a logical path from an `<a>` element. Returns null when the
23
+ * link is not an in-app navigation target (external, anchor-only, etc).
24
+ * The caller still applies modifier-key / target=_blank checks.
25
+ */
26
+ extractLogicalPath(anchor: HTMLAnchorElement): string | null;
27
+ }
28
+ /**
29
+ * Set the router's history mode. Call this once at app entry, before
30
+ * mounting any router. Defaults to "history" (HTML5 History API).
31
+ *
32
+ * Use "hash" when the document is loaded over file:// (Electron, Tauri, etc.)
33
+ * or any time there's no server able to serve a SPA fallback at arbitrary
34
+ * pathnames.
35
+ *
36
+ * Switching modes mid-session is supported but not common — listeners
37
+ * registered before the switch will keep using their original adapter
38
+ * until they're re-attached (typically by unmounting and remounting
39
+ * the router).
40
+ */
41
+ export declare function setHistoryMode(mode: HistoryMode): void;
42
+ export declare function getHistoryMode(): HistoryMode;
43
+ /** Internal — used by index.ts. Not part of the public API. */
44
+ export declare function getAdapter(): HistoryAdapter;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,341 @@
1
- import { A as loader, B as useRoute, C as RouterView, D as enableLinkInterception, E as defineLayout, F as routeHash, G as setHistoryMode, H as wrapLayout, I as routeParams, L as routePath, M as prefetch, N as prime, O as error, P as redirect, R as routeSearch, S as RouterOptions, T as composeLoaders, U as HistoryMode, V as wrapError, W as getHistoryMode, _ as RouteRecord, a as InferLoader, b as RouterLink, c as LinkInterceptionOptions, d as LoaderError, f as MergeLoaders, g as RenderResponse, h as Redirect, i as HydrateOptions, j as navigate, k as isActive, l as Loader, m as NavigateOptions, n as ErrorHandler, o as LOADER_ENDPOINT, p as MountOptions, r as HydratableRenderOptions, s as LayoutHandler, t as AppError, u as LoaderContext, v as RouteSnapshot, w as _default, x as RouterMode, y as RouterBuilder, z as router } from "./index-DZA_1KrG.js";
2
- export { AppError, ErrorHandler, HistoryMode, HydratableRenderOptions, HydrateOptions, InferLoader, LOADER_ENDPOINT, LayoutHandler, LinkInterceptionOptions, Loader, LoaderContext, LoaderError, MergeLoaders, MountOptions, NavigateOptions, Redirect, RenderResponse, RouteRecord, RouteSnapshot, RouterBuilder, RouterLink, RouterMode, RouterOptions, RouterView, composeLoaders, _default as default, defineLayout, enableLinkInterception, error, getHistoryMode, isActive, loader, navigate, prefetch, prime, redirect, routeHash, routeParams, routePath, routeSearch, router, setHistoryMode, useRoute, wrapError, wrapLayout };
1
+ import type { Island, HydratableOptions } from "ilha";
2
+ export { setHistoryMode, getHistoryMode } from "./hash";
3
+ export type { HistoryMode } from "./hash";
4
+ export interface RouteRecord {
5
+ pattern: string;
6
+ island: Island<any, any>;
7
+ /** Merged loader chain (layouts outer→inner, then page) — `undefined` if no loaders. */
8
+ loader?: Loader<any>;
9
+ /** True when the route has a server-side loader, even if the client only has a marker. */
10
+ hasLoader?: boolean;
11
+ }
12
+ export interface RouteSnapshot {
13
+ path: string;
14
+ params: Record<string, string>;
15
+ search: string;
16
+ hash: string;
17
+ }
18
+ export interface AppError {
19
+ message: string;
20
+ status?: number;
21
+ stack?: string;
22
+ }
23
+ export type LayoutHandler = (children: Island<any, any>) => Island<any, any>;
24
+ export type ErrorHandler = (error: AppError, route: RouteSnapshot) => Island<any, any>;
25
+ /**
26
+ * Serializable description of `<head>` (and html/body attributes) contributed
27
+ * by a loader or a render-time `head()` call. Deliberately a plain POJO — Tier
28
+ * 1 head management is SSR-only, so there is no reactive wrapper. Dedup keys
29
+ * mirror unhead so a later move to a runtime head manager stays a drop-in.
30
+ */
31
+ export interface HeadInput {
32
+ title?: string;
33
+ /** Wrap the resolved title. The last template in merge order wins. */
34
+ titleTemplate?: string | ((title?: string) => string);
35
+ meta?: Array<Record<string, string>>;
36
+ link?: Array<Record<string, string>>;
37
+ /**
38
+ * Inline script bodies are emitted raw in SSR (`serializeHead`). Must be trusted
39
+ * app code and must not contain a literal `</script>` sequence.
40
+ */
41
+ script?: Array<Record<string, string> & {
42
+ children?: string;
43
+ }>;
44
+ htmlAttrs?: Record<string, string>;
45
+ bodyAttrs?: Record<string, string>;
46
+ }
47
+ /** Serialized head fragments ready to inject into a document shell. */
48
+ export interface SerializedHead {
49
+ /** Markup for inside `<head>` (title, meta, link, script). */
50
+ headTags: string;
51
+ /** Attribute string for the `<html>` tag (leading space included). */
52
+ htmlAttrs: string;
53
+ /** Attribute string for the `<body>` tag (leading space included). */
54
+ bodyAttrs: string;
55
+ }
56
+ export interface LoaderContext {
57
+ params: Record<string, string>;
58
+ request: Request;
59
+ url: URL;
60
+ signal: AbortSignal;
61
+ /** Contribute `<head>` data for this route. Safe to call multiple times. */
62
+ head: (input: HeadInput) => void;
63
+ }
64
+ export type Loader<T> = (ctx: LoaderContext) => Promise<T> | T;
65
+ /**
66
+ * Identity function for declaring a loader. Exists purely as a type anchor and
67
+ * a marker for the Vite plugin to detect by export name.
68
+ */
69
+ export declare function loader<T>(fn: Loader<T>): Loader<T>;
70
+ /** Extract the return type of a loader. */
71
+ export type InferLoader<L> = L extends Loader<infer T> ? Awaited<T> : never;
72
+ /**
73
+ * Merge multiple loader return types into a single object type.
74
+ * Later loaders override earlier ones on key collision — matching runtime merge.
75
+ *
76
+ * @example
77
+ * type PageInput = MergeLoaders<[typeof rootLayoutLoad, typeof sectionLayoutLoad, typeof pageLoad]>;
78
+ */
79
+ export type MergeLoaders<Ls extends readonly Loader<any>[]> = Ls extends readonly [
80
+ infer First extends Loader<any>,
81
+ ...infer Rest extends readonly Loader<any>[]
82
+ ] ? Rest extends readonly [] ? InferLoader<First> : Omit<InferLoader<First>, keyof MergeLoaders<Rest>> & MergeLoaders<Rest> : {};
83
+ export declare class Redirect {
84
+ readonly __ilhaRedirect: true;
85
+ readonly to: string;
86
+ readonly status: number;
87
+ constructor(to: string, status?: number);
88
+ }
89
+ export declare class LoaderError {
90
+ readonly __ilhaLoaderError: true;
91
+ readonly status: number;
92
+ readonly message: string;
93
+ constructor(status: number, message: string);
94
+ }
95
+ export declare function redirect(to: string, status?: number): never;
96
+ export declare function error(status: number, message: string): never;
97
+ /**
98
+ * Compose a list of loaders into a single loader. Later loaders win on key
99
+ * collision (page loader overrides layout loader for the same key). All loaders
100
+ * run concurrently within a chain since they share the same abort signal and
101
+ * request — re-fetching is cheap with a request-scoped cache (future work).
102
+ *
103
+ * For v1 we run them in parallel via `Promise.all`. If a loader throws a
104
+ * `Redirect` or `LoaderError`, the composed loader re-throws it unchanged.
105
+ */
106
+ export declare function composeLoaders<Ls extends readonly Loader<any>[]>(loaders: Ls): Loader<MergeLoaders<Ls>>;
107
+ export declare function wrapLayout(layout: LayoutHandler, page: Island<any, any>): Island<any, any>;
108
+ export declare function wrapError(handler: ErrorHandler, page: Island<any, any>): Island<any, any>;
109
+ export declare function defineLayout(layout: LayoutHandler): LayoutHandler;
110
+ export interface NavigateOptions {
111
+ replace?: boolean;
112
+ }
113
+ export type RouterMode = "spa" | "static";
114
+ export interface RouterOptions {
115
+ /**
116
+ * Client navigation mode.
117
+ * - `spa` — full route graph, SSR/hydration, client-side navigation.
118
+ * - `static` — no route graph bundled; hydrate islands on the current
119
+ * pre-rendered page only.
120
+ * Default: `spa`.
121
+ */
122
+ mode?: RouterMode;
123
+ /**
124
+ * When `true` (default), internal `<a>` clicks are intercepted and handled
125
+ * by the client router. Set to `false` for MPA-style behavior where links
126
+ * perform full document navigations.
127
+ * Only meaningful in `spa` mode; ignored in `static` mode.
128
+ * Default: `true`.
129
+ */
130
+ interceptLinks?: boolean;
131
+ }
132
+ export interface HydratableRenderOptions extends Partial<Omit<HydratableOptions, "name">> {
133
+ /**
134
+ * Base `<head>` data merged before loader and render-time contributions, so
135
+ * route-level head overrides it. Used by host entries (e.g. `IlhaHandler`)
136
+ * to supply app-wide title/meta/scripts.
137
+ */
138
+ baseHead?: HeadInput;
139
+ }
140
+ export interface HydrateOptions {
141
+ root?: Element;
142
+ target?: string | Element;
143
+ /**
144
+ * When `true` (default), internal `<a>` clicks are intercepted for
145
+ * client-side navigation. Set to `false` for MPA-style full-page navigations.
146
+ */
147
+ interceptLinks?: boolean;
148
+ }
149
+ export interface MountOptions {
150
+ hydrate?: boolean;
151
+ registry?: Record<string, Island<any, any>>;
152
+ /**
153
+ * When `true` (default), internal `<a>` clicks are intercepted for
154
+ * client-side navigation. Set to `false` for MPA-style full-page navigations.
155
+ */
156
+ interceptLinks?: boolean;
157
+ }
158
+ /** Response envelope returned by `renderResponse` — lets the host app handle redirects. */
159
+ export type RenderResponse = {
160
+ kind: "html";
161
+ html: string;
162
+ status?: number;
163
+ head?: SerializedHead;
164
+ } | {
165
+ kind: "redirect";
166
+ to: string;
167
+ status: number;
168
+ } | {
169
+ kind: "error";
170
+ status: number;
171
+ message: string;
172
+ html: string;
173
+ head?: SerializedHead;
174
+ };
175
+ export interface RouterBuilder {
176
+ /**
177
+ * Register a route. The optional `loader` is the merged loader chain
178
+ * (layout loaders outer→inner followed by the page loader) produced by
179
+ * the FS-routing codegen.
180
+ */
181
+ route(pattern: string, island: Island<any, any>, loader?: Loader<any>): RouterBuilder;
182
+ /**
183
+ * Attach (or replace) a loader on an already-registered route pattern.
184
+ * Used by the `ilha:loaders` virtual module to wire server-only loaders
185
+ * onto the client-safe `pageRouter` at SSR time. No-op if the pattern
186
+ * was never registered via `.route()`.
187
+ */
188
+ attachLoader(pattern: string, loader: Loader<any>): RouterBuilder;
189
+ /**
190
+ * Mark an already-registered route as having a server-side loader without
191
+ * importing that loader into the client bundle. Used by FS-routing codegen
192
+ * so SPA navigation knows to call the loader endpoint.
193
+ */
194
+ markLoader(pattern: string): RouterBuilder;
195
+ /**
196
+ * Return a snapshot of every registered route in match order. Useful for
197
+ * prerenderers that need to discover the filesystem routes exposed by
198
+ * `pageRouter` without reaching into router internals.
199
+ */
200
+ routes(): RouteRecord[];
201
+ prime(): void;
202
+ mount(target: string | Element, options?: MountOptions): () => void;
203
+ render(url: string | URL): string;
204
+ renderHydratable(url: string | URL, registry: Record<string, Island<any, any>>, options?: HydratableRenderOptions, request?: Request): Promise<string>;
205
+ /**
206
+ * Like `renderHydratable` but surfaces loader redirects and errors as
207
+ * structured responses instead of baking them into HTML. Prefer this from
208
+ * host server code so you can emit proper 302 / 4xx responses.
209
+ */
210
+ renderResponse(url: string | URL, registry: Record<string, Island<any, any>>, options?: HydratableRenderOptions, request?: Request): Promise<RenderResponse>;
211
+ /**
212
+ * Run the loader chain for a given URL without rendering. Backs the
213
+ * `/__ilha/loader` endpoint that the host server handler (e.g. `IlhaHandler`)
214
+ * serves as JSON for client-side navigation. Returns the raw loader result, a
215
+ * redirect sentinel, or an error sentinel.
216
+ */
217
+ runLoader(url: string | URL, request?: Request): Promise<{
218
+ kind: "data";
219
+ data: Record<string, unknown>;
220
+ head?: SerializedHead;
221
+ } | {
222
+ kind: "redirect";
223
+ to: string;
224
+ status: number;
225
+ } | {
226
+ kind: "error";
227
+ status: number;
228
+ message: string;
229
+ } | {
230
+ kind: "not-found";
231
+ }>;
232
+ /**
233
+ * Hydrate the application - combines prime(), mount(), and router.mount() into one call.
234
+ * @param registry - The island registry from ilha:registry
235
+ * @param options - Optional root element (defaults to document.body) and router target (defaults to root)
236
+ * @returns Cleanup function
237
+ */
238
+ hydrate(registry: Record<string, Island<any, any>>, options?: HydrateOptions): () => void;
239
+ /**
240
+ * Hydrate islands on the current pre-rendered page without mounting a route
241
+ * view or enabling client navigation. Intended for `static` mode: each page
242
+ * is a self-contained HTML file; only interactive islands need activation.
243
+ */
244
+ hydrateStatic(registry: Record<string, Island<any, any>>, options?: {
245
+ root?: Element;
246
+ }): () => void;
247
+ }
248
+ /** Path of the loader endpoint served by the Vite plugin / production adapter. */
249
+ export declare const LOADER_ENDPOINT = "/__ilha/loader";
250
+ /**
251
+ * Prefetch loader data for a given path. Safe to call repeatedly — a single
252
+ * inflight request is reused until it either resolves (and is consumed by
253
+ * navigation) or is superseded by another prefetch.
254
+ */
255
+ 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
+ };
272
+ 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
+ };
289
+ };
290
+ /**
291
+ * Prime route context signals from the current `location` so that islands
292
+ * hydrated by `ilha.mount()` see the correct route values on their first
293
+ * render — preventing a mismatch morph that would destroy hydrated bindings.
294
+ */
295
+ export declare function prime(): void;
296
+ export declare function navigate(to: string, opts?: NavigateOptions): void;
297
+ export interface LinkInterceptionOptions {
298
+ /**
299
+ * Prefetch loader data on `mouseenter` for eligible links. Links opt in via
300
+ * the `data-prefetch` attribute (set `data-prefetch="false"` to opt out a
301
+ * specific link even when the framework is configured to prefetch by default).
302
+ * Default: `true` — prefetches on hover for any link with `data-prefetch`.
303
+ */
304
+ prefetch?: boolean;
305
+ }
306
+ export declare function enableLinkInterception(root?: Element | Document, options?: LinkInterceptionOptions): () => void;
307
+ export declare const RouterView: Island<Record<string, unknown>, Record<never, never>>;
308
+ 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;
310
+ /**
311
+ * Contribute `<head>` data from inside an island's `.render()` body or a
312
+ * layout. During SSR this collects into the active render window; on the
313
+ * client, entries are collected when the router re-renders a route inside
314
+ * `withHeadStore` and then applied to `document`. Prefer a loader's `ctx.head`
315
+ * for data that depends on the request.
316
+ */
317
+ export declare function head(input: HeadInput): void;
318
+ /**
319
+ * Merge head entries in contribution order (loader first as the base, then
320
+ * render-time outer→inner layouts, then the page) and serialize. Later entries
321
+ * win on collision; the last `titleTemplate` wraps the resolved title.
322
+ */
323
+ export declare function serializeHead(entries: HeadInput[]): SerializedHead;
324
+ export declare function router(options?: RouterOptions): RouterBuilder;
325
+ declare const _default: {
326
+ router: typeof router;
327
+ navigate: typeof navigate;
328
+ useRoute: typeof useRoute;
329
+ isActive: typeof isActive;
330
+ enableLinkInterception: typeof enableLinkInterception;
331
+ prime: typeof prime;
332
+ prefetch: typeof prefetch;
333
+ RouterView: Island<Record<string, unknown>, Record<never, never>>;
334
+ RouterLink: Island<Record<string, unknown>, Omit<Omit<Record<never, never>, K> & Record<"href", string>, "label"> & Record<"label", string>>;
335
+ loader: typeof loader;
336
+ redirect: typeof redirect;
337
+ error: typeof error;
338
+ composeLoaders: typeof composeLoaders;
339
+ head: typeof head;
340
+ };
341
+ export default _default;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{C as e,E as t,S as n,T as r,_ as i,a,b as o,c as s,d as c,f as l,g as u,h as d,i as f,l as p,m,n as h,o as g,p as _,r as v,s as y,t as b,u as x,v as S,w as C,x as w,y as T}from"./src-Cprpo919.js";export{b as LOADER_ENDPOINT,h as LoaderError,v as Redirect,f as RouterLink,a as RouterView,g as composeLoaders,w as default,y as defineLayout,s as enableLinkInterception,p as error,r as getHistoryMode,x as isActive,c as loader,l as navigate,_ as prefetch,m as prime,d as redirect,u as routeHash,i as routeParams,S as routePath,T as routeSearch,o as router,t as setHistoryMode,n as useRoute,e as wrapError,C as wrapLayout};
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};
@@ -0,0 +1,6 @@
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};
@@ -0,0 +1,56 @@
1
+ import type { PagesMode } from "./codegen";
2
+ export declare const VIRTUAL_PAGES_SERVER = "ilha:pages/server";
3
+ export declare const VIRTUAL_PAGES_CLIENT = "ilha:pages/client";
4
+ export declare const VIRTUAL_LOADERS = "ilha:loaders";
5
+ export declare const RESOLVED_PAGES_SERVER = "\0ilha:pages/server";
6
+ export declare const RESOLVED_PAGES_CLIENT = "\0ilha:pages/client";
7
+ export declare const RESOLVED_LOADERS = "\0ilha:loaders";
8
+ export declare const RESOLVED_VIRTUAL_IDS: readonly ["\0ilha:pages/server", "\0ilha:pages/client", "\0ilha:loaders"];
9
+ /** Query suffix used on page/layout imports in the client file. */
10
+ export declare const CLIENT_QUERY = "?client";
11
+ export interface IlhaPagesOptions {
12
+ /** Directory containing page files. Default: `src/pages` */
13
+ dir?: string;
14
+ /** Output directory for generated files. Default: `.ilha` */
15
+ outDir?: string;
16
+ /**
17
+ * File-system router navigation mode.
18
+ * - `spa` — full client route graph with SSR/hydration and client navigation.
19
+ * - `static` — island registry only; no route graph bundled into the client.
20
+ * Default: `spa`.
21
+ */
22
+ mode?: PagesMode;
23
+ /**
24
+ * When `false`, internal `<a>` clicks are not intercepted — browser performs
25
+ * full document navigations. Only meaningful in `spa` mode.
26
+ * Default: `true`.
27
+ */
28
+ interceptLinks?: boolean;
29
+ }
30
+ export declare function resolvePluginPaths(root: string, options: IlhaPagesOptions): {
31
+ pagesDir: string;
32
+ outDir: string;
33
+ serverFile: string;
34
+ clientFile: string;
35
+ loadersFile: string;
36
+ };
37
+ export interface PagesPluginState {
38
+ pagesDir: string;
39
+ outDir: string;
40
+ serverFile: string;
41
+ clientFile: string;
42
+ loadersFile: string;
43
+ setPaths(root: string): void;
44
+ regen(): Promise<void>;
45
+ shouldRegenOnChange(file: string): boolean;
46
+ isUnderPagesDir(file: string): boolean;
47
+ }
48
+ export declare function createPagesPluginState(options: IlhaPagesOptions): PagesPluginState;
49
+ 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;
51
+ export declare function loadPagesModule(state: PagesPluginState, id: string): string | undefined;
52
+ type InvalidateModules = () => void | Promise<void>;
53
+ export declare function createStructuralInvalidate(state: PagesPluginState, invalidate: InvalidateModules): (file: string) => Promise<void>;
54
+ export declare function setupRspackPagesWatcher(state: PagesPluginState, structuralInvalidate: (file: string) => void | Promise<void>): () => void;
55
+ export declare const ilhaPages: import("unplugin").UnpluginInstance<IlhaPagesOptions | undefined, boolean>;
56
+ export {};
@@ -1,9 +1,5 @@
1
- import { H as wrapLayout, V as wrapError, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot } from "./index-DZA_1KrG.js";
2
- import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-rauddckN.js";
3
- import * as _$unplugin from "unplugin";
4
-
5
- //#region src/rolldown.d.ts
1
+ export { wrapLayout, wrapError, type LayoutHandler, type ErrorHandler, type RouteSnapshot, type AppError, } from "./index";
2
+ export { ilhaPages, type IlhaPagesOptions } from "./plugin";
3
+ import { type IlhaPagesOptions } from "./plugin";
6
4
  /** Rolldown plugin — use via `@ilha/router/rolldown`. */
7
- declare function pages(options?: IlhaPagesOptions): _$unplugin.RolldownPlugin<any> | _$unplugin.RolldownPlugin<any>[];
8
- //#endregion
9
- export { type AppError, type ErrorHandler, type IlhaPagesOptions, type LayoutHandler, type RouteSnapshot, ilhaPages, pages, wrapError, wrapLayout };
5
+ export declare function pages(options?: IlhaPagesOptions): import("unplugin").RolldownPlugin<any> | import("unplugin").RolldownPlugin<any>[];
package/dist/rolldown.js CHANGED
@@ -1 +1 @@
1
- import{C as e,w as t}from"./src-Cprpo919.js";import{t as n}from"./plugin-OpojKenS.js";function r(e={}){return n.rolldown(e)}export{n as ilhaPages,r as pages,e as wrapError,t as wrapLayout};
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};
package/dist/rspack.d.ts CHANGED
@@ -1,8 +1,5 @@
1
- import { H as wrapLayout, V as wrapError, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot } from "./index-DZA_1KrG.js";
2
- import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-rauddckN.js";
3
-
4
- //#region src/rspack.d.ts
1
+ export { wrapLayout, wrapError, type LayoutHandler, type ErrorHandler, type RouteSnapshot, type AppError, } from "./index";
2
+ export { ilhaPages, type IlhaPagesOptions } from "./plugin";
3
+ import { type IlhaPagesOptions } from "./plugin";
5
4
  /** Rspack plugin — use via `@ilha/router/rspack`. */
6
- declare function pages(options?: IlhaPagesOptions): RspackPluginInstance;
7
- //#endregion
8
- export { type AppError, type ErrorHandler, type IlhaPagesOptions, type LayoutHandler, type RouteSnapshot, ilhaPages, pages, wrapError, wrapLayout };
5
+ export declare function pages(options?: IlhaPagesOptions): RspackPluginInstance;
package/dist/rspack.js CHANGED
@@ -1 +1 @@
1
- import{C as e,w as t}from"./src-Cprpo919.js";import{t as n}from"./plugin-OpojKenS.js";function r(e={}){return n.rspack(e)}export{n as ilhaPages,r as pages,e as wrapError,t as wrapLayout};
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};
@@ -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){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};