@ilha/router 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -666,6 +666,11 @@ interface HydrateOptions {
666
666
  }
667
667
 
668
668
  type HistoryMode = "history" | "hash";
669
+ type RouterMode = "spa" | "mpa";
670
+
671
+ interface RouterOptions {
672
+ mode?: RouterMode; // default: "spa"
673
+ }
669
674
 
670
675
  // Helper — returns fn as-is with LayoutHandler type enforced
671
676
  function defineLayout(fn: LayoutHandler): LayoutHandler;
@@ -927,6 +932,18 @@ pageRouter.hydrate(registry);
927
932
  pages({
928
933
  dir: "src/pages", // pages directory (default: "src/pages")
929
934
  generated: ".ilha/routes.ts", // generated file output (default: ".ilha/routes.ts")
935
+ mode: "spa", // "spa" | "mpa" (default: "spa")
936
+ });
937
+ ```
938
+
939
+ Use `mode: "mpa"` when you want filesystem-routed pages to behave like a multi-page app: the current page can still be SSR-rendered and hydrated, but in-app links are not intercepted by the router, so navigation is handled by the browser as a full document request.
940
+
941
+ ```ts
942
+ // vite.config.ts
943
+ import { pages } from "@ilha/router/vite";
944
+
945
+ export default defineConfig({
946
+ plugins: [pages({ mode: "mpa" })],
930
947
  });
931
948
  ```
932
949
 
@@ -24,6 +24,8 @@ interface RouteRecord {
24
24
  island: Island<any, any>;
25
25
  /** Merged loader chain (layouts outer→inner, then page) — `undefined` if no loaders. */
26
26
  loader?: Loader<any>;
27
+ /** True when the route has a server-side loader, even if the client only has a marker. */
28
+ hasLoader?: boolean;
27
29
  }
28
30
  interface RouteSnapshot {
29
31
  path: string;
@@ -90,6 +92,16 @@ declare function defineLayout(layout: LayoutHandler): LayoutHandler;
90
92
  interface NavigateOptions {
91
93
  replace?: boolean;
92
94
  }
95
+ type RouterMode = "spa" | "mpa";
96
+ interface RouterOptions {
97
+ /**
98
+ * Client navigation mode. `spa` intercepts in-app links and renders routes
99
+ * in-place. `mpa` leaves links to the browser, so each page navigation is a
100
+ * full document request while still allowing hydration of the current route.
101
+ * Default: `spa`.
102
+ */
103
+ mode?: RouterMode;
104
+ }
93
105
  interface HydratableRenderOptions extends Partial<Omit<HydratableOptions, "name">> {}
94
106
  interface HydrateOptions {
95
107
  root?: Element;
@@ -128,6 +140,12 @@ interface RouterBuilder {
128
140
  * was never registered via `.route()`.
129
141
  */
130
142
  attachLoader(pattern: string, loader: Loader<any>): RouterBuilder;
143
+ /**
144
+ * Mark an already-registered route as having a server-side loader without
145
+ * importing that loader into the client bundle. Used by FS-routing codegen
146
+ * so SPA navigation knows to call the loader endpoint.
147
+ */
148
+ markLoader(pattern: string): RouterBuilder;
131
149
  /**
132
150
  * Return a snapshot of every registered route in match order. Useful for
133
151
  * prerenderers that need to discover the filesystem routes exposed by
@@ -234,7 +252,7 @@ declare function enableLinkInterception(root?: Element | Document, options?: Lin
234
252
  declare const RouterView: Island<Record<string, unknown>, Record<never, never>>;
235
253
  declare const RouterLink: Island<Record<string, unknown>, Omit<Omit<Record<never, never>, K> & Record<"href", string>, "label"> & Record<"label", string>>;
236
254
  declare function isActive(pattern: string): boolean;
237
- declare function router(): RouterBuilder;
255
+ declare function router(options?: RouterOptions): RouterBuilder;
238
256
  declare const _default: {
239
257
  router: typeof router;
240
258
  navigate: typeof navigate;
@@ -251,4 +269,4 @@ declare const _default: {
251
269
  composeLoaders: typeof composeLoaders;
252
270
  };
253
271
  //#endregion
254
- export { prefetch as A, wrapLayout as B, composeLoaders as C, isActive as D, error as E, routePath as F, getHistoryMode as H, routeSearch as I, router as L, redirect as M, routeHash as N, loader as O, routeParams as P, useRoute as R, _default as S, enableLinkInterception as T, setHistoryMode as U, HistoryMode as V, RouteRecord as _, InferLoader as a, RouterLink as b, LinkInterceptionOptions as c, LoaderError as d, MergeLoaders as f, RenderResponse as g, Redirect as h, HydrateOptions as i, prime as j, navigate as k, Loader as l, NavigateOptions as m, ErrorHandler as n, LOADER_ENDPOINT as o, MountOptions as p, HydratableRenderOptions as r, LayoutHandler as s, AppError as t, LoaderContext as u, RouteSnapshot as v, defineLayout as w, RouterView as x, RouterBuilder as y, wrapError as z };
272
+ export { loader as A, useRoute as B, RouterView as C, enableLinkInterception as D, defineLayout as E, routeHash as F, setHistoryMode as G, wrapLayout as H, routeParams as I, routePath as L, prefetch as M, prime as N, error as O, redirect as P, routeSearch as R, RouterOptions as S, composeLoaders as T, HistoryMode as U, wrapError as V, getHistoryMode as W, RouteRecord as _, InferLoader as a, RouterLink as b, LinkInterceptionOptions as c, LoaderError as d, MergeLoaders as f, RenderResponse as g, Redirect as h, HydrateOptions as i, navigate as j, isActive as k, Loader as l, NavigateOptions as m, ErrorHandler as n, LOADER_ENDPOINT as o, MountOptions as p, HydratableRenderOptions as r, LayoutHandler as s, AppError as t, LoaderContext as u, RouteSnapshot as v, _default as w, RouterMode as x, RouterBuilder as y, router as z };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as prefetch, B as wrapLayout, C as composeLoaders, D as isActive, E as error, F as routePath, H as getHistoryMode, I as routeSearch, L as router, M as redirect, N as routeHash, O as loader, P as routeParams, R as useRoute, S as _default, T as enableLinkInterception, U as setHistoryMode, V as HistoryMode, _ 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 prime, k as navigate, 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 defineLayout, x as RouterView, y as RouterBuilder, z as wrapError } from "./index-CFBKeDvv.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, 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 { 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-CJT0h4u4.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 };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as wrapError, E as setHistoryMode, S as useRoute, T as getHistoryMode, _ as routeParams, a as RouterView, b as router, c as enableLinkInterception, d as loader, f as navigate, g as routeHash, h as redirect, i as RouterLink, l as error, m as prime, n as LoaderError, o as composeLoaders, p as prefetch, r as Redirect, s as defineLayout, t as LOADER_ENDPOINT, u as isActive, v as routePath, w as wrapLayout, x as src_default, y as routeSearch } from "./src-0cb8FYdn.js";
1
+ import { C as wrapError, E as setHistoryMode, S as useRoute, T as getHistoryMode, _ as routeParams, a as RouterView, b as router, c as enableLinkInterception, d as loader, f as navigate, g as routeHash, h as redirect, i as RouterLink, l as error, m as prime, n as LoaderError, o as composeLoaders, p as prefetch, r as Redirect, s as defineLayout, t as LOADER_ENDPOINT, u as isActive, v as routePath, w as wrapLayout, x as src_default, y as routeSearch } from "./src-BqAWh426.js";
2
2
  export { LOADER_ENDPOINT, LoaderError, Redirect, RouterLink, RouterView, composeLoaders, src_default as default, defineLayout, enableLinkInterception, error, getHistoryMode, isActive, loader, navigate, prefetch, prime, redirect, routeHash, routeParams, routePath, routeSearch, router, setHistoryMode, useRoute, wrapError, wrapLayout };
@@ -138,7 +138,8 @@ function validateEntries(entries, pagesDir) {
138
138
  else seenNames.set(entry.name, entry.file);
139
139
  }
140
140
  }
141
- async function generate(pagesDir, outFile) {
141
+ async function generate(pagesDir, outFile, options = {}) {
142
+ const mode = options.mode ?? "spa";
142
143
  const entries = sortEntries(await scanPages(pagesDir));
143
144
  validateEntries(entries, pagesDir);
144
145
  const rel = (abs) => {
@@ -161,7 +162,7 @@ async function generate(pagesDir, outFile) {
161
162
  const wrappedId = `_wrapped${i}`;
162
163
  wrappedIslandLines.push(`const ${wrappedId} = ${expr};`);
163
164
  registryLines.push(` ${JSON.stringify(entry.name)}: ${wrappedId}` + (i < entries.length - 1 ? "," : ""));
164
- routeLines.push(` .route(${JSON.stringify(entry.pattern)}, ${wrappedId})`);
165
+ routeLines.push(` .route(${JSON.stringify(entry.pattern)}, ${wrappedId})` + (entry.hasLoader || entry.loaderLayouts.length > 0 ? `.markLoader(${JSON.stringify(entry.pattern)})` : ""));
165
166
  }
166
167
  const code = [
167
168
  `// @generated by @ilha/router — do not edit`,
@@ -174,7 +175,7 @@ async function generate(pagesDir, outFile) {
174
175
  ...registryLines,
175
176
  `};`,
176
177
  ``,
177
- `export const pageRouter = router()`,
178
+ `export const pageRouter = router(${mode === "mpa" ? `{ mode: "mpa" }` : ""})`,
178
179
  ...routeLines,
179
180
  ` ;`
180
181
  ].join("\n");
@@ -285,7 +286,7 @@ function createPagesPluginState(options) {
285
286
  };
286
287
  const regen = async () => {
287
288
  try {
288
- await generate(pagesDir, outFile);
289
+ await generate(pagesDir, outFile, { mode: options.mode });
289
290
  } catch (e) {
290
291
  console.error("[ilha:pages] codegen failed:", e);
291
292
  }
@@ -1,11 +1,20 @@
1
1
  import * as _$unplugin from "unplugin";
2
2
 
3
+ //#region src/codegen.d.ts
4
+ type PagesMode = "spa" | "mpa";
5
+ //#endregion
3
6
  //#region src/plugin.d.ts
4
7
  interface IlhaPagesOptions {
5
8
  /** Directory containing page files. Default: `src/pages` */
6
9
  dir?: string;
7
10
  /** Output path for the generated routes + registry file. Default: `.ilha/routes.ts` */
8
11
  generated?: string;
12
+ /**
13
+ * File-system router navigation mode. `spa` intercepts in-app links and
14
+ * renders routes client-side; `mpa` leaves links to the browser for full
15
+ * document navigations. Default: `spa`.
16
+ */
17
+ mode?: PagesMode;
9
18
  }
10
19
  declare const ilhaPages: _$unplugin.UnpluginInstance<IlhaPagesOptions | undefined, boolean>;
11
20
  //#endregion
@@ -1,5 +1,5 @@
1
- import { B as wrapLayout, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot, z as wrapError } from "./index-CFBKeDvv.js";
2
- import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-CnS_mBUE.js";
1
+ import { H as wrapLayout, V as wrapError, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot } from "./index-CJT0h4u4.js";
2
+ import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-Diq1AxSa.js";
3
3
  import * as _$unplugin from "unplugin";
4
4
 
5
5
  //#region src/rolldown.d.ts
package/dist/rolldown.js CHANGED
@@ -1,5 +1,5 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-0cb8FYdn.js";
2
- import { t as ilhaPages } from "./plugin-DAk44om0.js";
1
+ import { C as wrapError, w as wrapLayout } from "./src-BqAWh426.js";
2
+ import { t as ilhaPages } from "./plugin-BMk5Ey9X.js";
3
3
  //#region src/rolldown.ts
4
4
  /** Rolldown plugin — use via `@ilha/router/rolldown`. */
5
5
  function pages(options = {}) {
package/dist/rspack.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as wrapLayout, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot, z as wrapError } from "./index-CFBKeDvv.js";
2
- import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-CnS_mBUE.js";
1
+ import { H as wrapLayout, V as wrapError, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot } from "./index-CJT0h4u4.js";
2
+ import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-Diq1AxSa.js";
3
3
  import * as _$unplugin from "unplugin";
4
4
 
5
5
  //#region src/rspack.d.ts
package/dist/rspack.js CHANGED
@@ -1,5 +1,5 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-0cb8FYdn.js";
2
- import { t as ilhaPages } from "./plugin-DAk44om0.js";
1
+ import { C as wrapError, w as wrapLayout } from "./src-BqAWh426.js";
2
+ import { t as ilhaPages } from "./plugin-BMk5Ey9X.js";
3
3
  //#region src/rspack.ts
4
4
  /** Rspack plugin — use via `@ilha/router/rspack`. */
5
5
  function pages(options = {}) {
@@ -349,7 +349,7 @@ function prefetch(pathWithSearch) {
349
349
  if (!isBrowser) return;
350
350
  if (prefetchCache.has(pathWithSearch)) return;
351
351
  const pathOnly = pathWithSearch.split("?")[0] ?? "";
352
- if (!findRoute(_rou3, "GET", pathOnly)?.data?.loader) return;
352
+ if (!findRoute(_rou3, "GET", pathOnly)?.data?.hasLoader) return;
353
353
  const promise = fetchLoaderData(pathWithSearch).catch((e) => {
354
354
  return {
355
355
  kind: "error",
@@ -369,7 +369,7 @@ async function mountRouteWithHydration(island, host, pathWithSearch, signal, reg
369
369
  host.innerHTML = `<div data-router-empty></div>`;
370
370
  return () => {};
371
371
  }
372
- const hasLoader = !!findRoute(_rou3, "GET", pathWithSearch.split("?")[0] ?? "")?.data?.loader;
372
+ const hasLoader = !!findRoute(_rou3, "GET", pathWithSearch.split("?")[0] ?? "")?.data?.hasLoader;
373
373
  let props = {};
374
374
  const loaderResult = hasLoader ? await fetchLoaderData(pathWithSearch, signal) : {
375
375
  kind: "data",
@@ -577,7 +577,8 @@ async function executeLoader(loader, url, params, request, signal) {
577
577
  };
578
578
  }
579
579
  }
580
- function router() {
580
+ function router(options = {}) {
581
+ const mode = options.mode ?? "spa";
581
582
  _records = [];
582
583
  _rou3 = createRouter();
583
584
  _islandToPattern = /* @__PURE__ */ new Map();
@@ -586,14 +587,17 @@ function router() {
586
587
  let _linkCleanup = null;
587
588
  const builder = {
588
589
  route(pattern, island, loader) {
590
+ const hasLoader = !!loader;
589
591
  const data = {
590
592
  island,
591
- loader
593
+ loader,
594
+ hasLoader
592
595
  };
593
596
  _records.push({
594
597
  pattern,
595
598
  island,
596
- loader
599
+ loader,
600
+ hasLoader
597
601
  });
598
602
  addRoute(_rou3, "GET", pattern, data);
599
603
  _patternToData.set(pattern, data);
@@ -607,8 +611,23 @@ function router() {
607
611
  return builder;
608
612
  }
609
613
  data.loader = loader;
614
+ data.hasLoader = true;
610
615
  const rec = _records.find((r) => r.pattern === pattern);
611
- if (rec) rec.loader = loader;
616
+ if (rec) {
617
+ rec.loader = loader;
618
+ rec.hasLoader = true;
619
+ }
620
+ return builder;
621
+ },
622
+ markLoader(pattern) {
623
+ const data = _patternToData.get(pattern);
624
+ if (!data) {
625
+ console.warn(`[ilha-router] markLoader("${pattern}"): pattern was never registered via .route(). The loader marker will be ignored.`);
626
+ return builder;
627
+ }
628
+ data.hasLoader = true;
629
+ const rec = _records.find((r) => r.pattern === pattern);
630
+ if (rec) rec.hasLoader = true;
612
631
  return builder;
613
632
  },
614
633
  routes() {
@@ -628,7 +647,7 @@ function router() {
628
647
  syncRouteFromLocation();
629
648
  const popHandler = () => syncRouteFromLocation();
630
649
  _navChangeCleanup = getAdapter().onChange(popHandler);
631
- _linkCleanup = enableLinkInterception(document);
650
+ _linkCleanup = mode === "spa" ? enableLinkInterception(document) : null;
632
651
  let unmountView = null;
633
652
  let navAbort = null;
634
653
  if (hydrate) {
@@ -692,7 +711,7 @@ function router() {
692
711
  const viewHost = host?.querySelector("[data-router-view]");
693
712
  if (!viewHost) return;
694
713
  const loc = getAdapter().readLocation();
695
- const result = !!findRoute(_rou3, "GET", loc.pathname)?.data?.loader ? await fetchLoaderData(loc.pathname + loc.search, signal) : {
714
+ const result = !!findRoute(_rou3, "GET", loc.pathname)?.data?.hasLoader ? await fetchLoaderData(loc.pathname + loc.search, signal) : {
696
715
  kind: "data",
697
716
  data: {}
698
717
  };
package/dist/vite.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
- import { B as wrapLayout, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot, z as wrapError } from "./index-CFBKeDvv.js";
3
- import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-CnS_mBUE.js";
2
+ import { H as wrapLayout, V as wrapError, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot } from "./index-CJT0h4u4.js";
3
+ import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-Diq1AxSa.js";
4
4
  import * as fs from "node:fs";
5
5
  import * as http from "node:http";
6
6
  import { Agent, ClientRequest, ClientRequestArgs, OutgoingHttpHeaders } from "node:http";
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-0cb8FYdn.js";
2
- import { t as ilhaPages } from "./plugin-DAk44om0.js";
1
+ import { C as wrapError, w as wrapLayout } from "./src-BqAWh426.js";
2
+ import { t as ilhaPages } from "./plugin-BMk5Ey9X.js";
3
3
  //#region src/vite.ts
4
4
  /** Vite plugin — use via `@ilha/router/vite`. */
5
5
  function pages(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/router",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "license": "MIT",
6
6
  "author": "Ryuz <ryuzer@proton.me>",