@ilha/router 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ import { HydratableOptions, Island } from "ilha";
4
4
  interface RouteRecord {
5
5
  pattern: string;
6
6
  island: Island<any, any>;
7
+ /** Merged loader chain (layouts outer→inner, then page) — `undefined` if no loaders. */
8
+ loader?: Loader<any>;
7
9
  }
8
10
  interface RouteSnapshot {
9
11
  path: string;
@@ -18,8 +20,55 @@ interface AppError {
18
20
  }
19
21
  type LayoutHandler = (children: Island<any, any>) => Island<any, any>;
20
22
  type ErrorHandler = (error: AppError, route: RouteSnapshot) => Island<any, any>;
23
+ interface LoaderContext {
24
+ params: Record<string, string>;
25
+ request: Request;
26
+ url: URL;
27
+ signal: AbortSignal;
28
+ }
29
+ type Loader<T> = (ctx: LoaderContext) => Promise<T> | T;
30
+ /**
31
+ * Identity function for declaring a loader. Exists purely as a type anchor and
32
+ * a marker for the Vite plugin to detect by export name.
33
+ */
34
+ declare function loader<T>(fn: Loader<T>): Loader<T>;
35
+ /** Extract the return type of a loader. */
36
+ type InferLoader<L> = L extends Loader<infer T> ? Awaited<T> : never;
37
+ /**
38
+ * Merge multiple loader return types into a single object type.
39
+ * Later loaders override earlier ones on key collision — matching runtime merge.
40
+ *
41
+ * @example
42
+ * type PageInput = MergeLoaders<[typeof rootLayoutLoad, typeof sectionLayoutLoad, typeof pageLoad]>;
43
+ */
44
+ type MergeLoaders<Ls extends readonly Loader<any>[]> = Ls extends readonly [infer First extends Loader<any>, ...infer Rest extends readonly Loader<any>[]] ? Rest extends readonly [] ? InferLoader<First> : Omit<InferLoader<First>, keyof MergeLoaders<Rest>> & MergeLoaders<Rest> : {};
45
+ declare class Redirect {
46
+ readonly __ilhaRedirect: true;
47
+ readonly to: string;
48
+ readonly status: number;
49
+ constructor(to: string, status?: number);
50
+ }
51
+ declare class LoaderError {
52
+ readonly __ilhaLoaderError: true;
53
+ readonly status: number;
54
+ readonly message: string;
55
+ constructor(status: number, message: string);
56
+ }
57
+ declare function redirect(to: string, status?: number): never;
58
+ declare function error(status: number, message: string): never;
59
+ /**
60
+ * Compose a list of loaders into a single loader. Later loaders win on key
61
+ * collision (page loader overrides layout loader for the same key). All loaders
62
+ * run concurrently within a chain since they share the same abort signal and
63
+ * request — re-fetching is cheap with a request-scoped cache (future work).
64
+ *
65
+ * For v1 we run them in parallel via `Promise.all`. If a loader throws a
66
+ * `Redirect` or `LoaderError`, the composed loader re-throws it unchanged.
67
+ */
68
+ declare function composeLoaders<Ls extends readonly Loader<any>[]>(loaders: Ls): Loader<MergeLoaders<Ls>>;
21
69
  declare function wrapLayout(layout: LayoutHandler, page: Island<any, any>): Island<any, any>;
22
70
  declare function wrapError(handler: ErrorHandler, page: Island<any, any>): Island<any, any>;
71
+ declare function defineLayout(layout: LayoutHandler): LayoutHandler;
23
72
  interface NavigateOptions {
24
73
  replace?: boolean;
25
74
  }
@@ -32,12 +81,65 @@ interface MountOptions {
32
81
  hydrate?: boolean;
33
82
  registry?: Record<string, Island<any, any>>;
34
83
  }
84
+ /** Response envelope returned by `renderResponse` — lets the host app handle redirects. */
85
+ type RenderResponse = {
86
+ kind: "html";
87
+ html: string;
88
+ status?: number;
89
+ } | {
90
+ kind: "redirect";
91
+ to: string;
92
+ status: number;
93
+ } | {
94
+ kind: "error";
95
+ status: number;
96
+ message: string;
97
+ html: string;
98
+ };
35
99
  interface RouterBuilder {
36
- route(pattern: string, island: Island<any, any>): RouterBuilder;
100
+ /**
101
+ * Register a route. The optional `loader` is the merged loader chain
102
+ * (layout loaders outer→inner followed by the page loader) produced by
103
+ * the FS-routing codegen.
104
+ */
105
+ route(pattern: string, island: Island<any, any>, loader?: Loader<any>): RouterBuilder;
106
+ /**
107
+ * Attach (or replace) a loader on an already-registered route pattern.
108
+ * Used by the `ilha:loaders` virtual module to wire server-only loaders
109
+ * onto the client-safe `pageRouter` at SSR time. No-op if the pattern
110
+ * was never registered via `.route()`.
111
+ */
112
+ attachLoader(pattern: string, loader: Loader<any>): RouterBuilder;
37
113
  prime(): void;
38
114
  mount(target: string | Element, options?: MountOptions): () => void;
39
115
  render(url: string | URL): string;
40
- renderHydratable(url: string | URL, registry: Record<string, Island<any, any>>, options?: HydratableRenderOptions): Promise<string>;
116
+ renderHydratable(url: string | URL, registry: Record<string, Island<any, any>>, options?: HydratableRenderOptions, request?: Request): Promise<string>;
117
+ /**
118
+ * Like `renderHydratable` but surfaces loader redirects and errors as
119
+ * structured responses instead of baking them into HTML. Prefer this from
120
+ * host server code so you can emit proper 302 / 4xx responses.
121
+ */
122
+ renderResponse(url: string | URL, registry: Record<string, Island<any, any>>, options?: HydratableRenderOptions, request?: Request): Promise<RenderResponse>;
123
+ /**
124
+ * Run the loader chain for a given URL without rendering. Used by the
125
+ * `/__ilha/loader` endpoint the Vite plugin exposes for client-side
126
+ * navigation. Returns the raw loader result, a redirect sentinel, or an
127
+ * error sentinel.
128
+ */
129
+ runLoader(url: string | URL, request?: Request): Promise<{
130
+ kind: "data";
131
+ data: Record<string, unknown>;
132
+ } | {
133
+ kind: "redirect";
134
+ to: string;
135
+ status: number;
136
+ } | {
137
+ kind: "error";
138
+ status: number;
139
+ message: string;
140
+ } | {
141
+ kind: "not-found";
142
+ }>;
41
143
  /**
42
144
  * Hydrate the application - combines prime(), mount(), and router.mount() into one call.
43
145
  * @param registry - The island registry from ilha:registry
@@ -46,6 +148,14 @@ interface RouterBuilder {
46
148
  */
47
149
  hydrate(registry: Record<string, Island<any, any>>, options?: HydrateOptions): () => void;
48
150
  }
151
+ /** Path of the loader endpoint served by the Vite plugin / production adapter. */
152
+ declare const LOADER_ENDPOINT = "/__ilha/loader";
153
+ /**
154
+ * Prefetch loader data for a given path. Safe to call repeatedly — a single
155
+ * inflight request is reused until it either resolves (and is consumed by
156
+ * navigation) or is superseded by another prefetch.
157
+ */
158
+ declare function prefetch(pathWithSearch: string): void;
49
159
  declare const routePath: {
50
160
  (): string;
51
161
  (value: string): void;
@@ -84,23 +194,19 @@ declare function useRoute(): {
84
194
  * Prime route context signals from the current `location` so that islands
85
195
  * hydrated by `ilha.mount()` see the correct route values on their first
86
196
  * render — preventing a mismatch morph that would destroy hydrated bindings.
87
- *
88
- * Call this **before** `ilha.mount()` and **after** all routes have been
89
- * registered (i.e. after the `router().route(…).route(…)` chain).
90
- *
91
- * ```ts
92
- * import { mount } from "ilha";
93
- * import { pageRouter } from "ilha:pages";
94
- * import { registry } from "ilha:registry";
95
- *
96
- * pageRouter.prime(); // ← sync signals first
97
- * mount(registry, { root: … }); // ← then hydrate islands
98
- * pageRouter.mount("#app", { hydrate: true });
99
- * ```
100
197
  */
101
198
  declare function prime(): void;
102
199
  declare function navigate(to: string, opts?: NavigateOptions): void;
103
- declare function enableLinkInterception(root?: Element | Document): () => void;
200
+ interface LinkInterceptionOptions {
201
+ /**
202
+ * Prefetch loader data on `mouseenter` for eligible links. Links opt in via
203
+ * the `data-prefetch` attribute (set `data-prefetch="false"` to opt out a
204
+ * specific link even when the framework is configured to prefetch by default).
205
+ * Default: `true` — prefetches on hover for any link with `data-prefetch`.
206
+ */
207
+ prefetch?: boolean;
208
+ }
209
+ declare function enableLinkInterception(root?: Element | Document, options?: LinkInterceptionOptions): () => void;
104
210
  declare const RouterView: Island<Record<string, unknown>, Record<string, never>>;
105
211
  declare const RouterLink: Island<Record<string, unknown>, {
106
212
  [x: string]: never;
@@ -115,11 +221,16 @@ declare const _default: {
115
221
  isActive: typeof isActive;
116
222
  enableLinkInterception: typeof enableLinkInterception;
117
223
  prime: typeof prime;
224
+ prefetch: typeof prefetch;
118
225
  RouterView: Island<Record<string, unknown>, Record<string, never>>;
119
226
  RouterLink: Island<Record<string, unknown>, {
120
227
  [x: string]: never;
121
228
  href: string;
122
229
  } & Record<"label", string>>;
230
+ loader: typeof loader;
231
+ redirect: typeof redirect;
232
+ error: typeof error;
233
+ composeLoaders: typeof composeLoaders;
123
234
  };
124
235
  //#endregion
125
- export { AppError, ErrorHandler, HydratableRenderOptions, HydrateOptions, LayoutHandler, MountOptions, NavigateOptions, RouteRecord, RouteSnapshot, RouterBuilder, RouterLink, RouterView, _default as default, enableLinkInterception, isActive, navigate, prime, routeHash, routeParams, routePath, routeSearch, router, useRoute, wrapError, wrapLayout };
236
+ export { AppError, ErrorHandler, HydratableRenderOptions, HydrateOptions, InferLoader, LOADER_ENDPOINT, LayoutHandler, LinkInterceptionOptions, Loader, LoaderContext, LoaderError, MergeLoaders, MountOptions, NavigateOptions, Redirect, RenderResponse, RouteRecord, RouteSnapshot, RouterBuilder, RouterLink, RouterView, composeLoaders, _default as default, defineLayout, enableLinkInterception, error, isActive, loader, navigate, prefetch, prime, redirect, routeHash, routeParams, routePath, routeSearch, router, useRoute, wrapError, wrapLayout };