@funstack/router 0.0.8 → 0.0.9

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,141 @@
1
+ import { CodeBlock } from "../components/CodeBlock.js";
2
+
3
+ export function LearnSsrWithLoadersPage() {
4
+ return (
5
+ <div className="learn-content">
6
+ <h2>SSR with Loaders</h2>
7
+
8
+ <p className="page-intro">
9
+ When you have a server runtime that can execute loaders at request time,
10
+ set <code>runLoaders: true</code> in the <code>ssr</code> config to
11
+ produce fully rendered HTML &mdash; including loader data &mdash; on the
12
+ server.
13
+ </p>
14
+
15
+ <section>
16
+ <h3>
17
+ How <code>runLoaders</code> Works
18
+ </h3>
19
+ <p>
20
+ As described in the{" "}
21
+ <a href="/learn/ssr/static-site-generation">Static Site Generation</a>{" "}
22
+ guide, the <code>ssr</code> prop enables path-based route matching
23
+ during SSR. By default, routes with loaders are skipped. Setting{" "}
24
+ <code>runLoaders: true</code> changes this: those routes are matched
25
+ and their loaders execute during SSR.
26
+ </p>
27
+ <CodeBlock language="tsx">{`// Without runLoaders (default): loaders are skipped
28
+ <Router routes={routes} ssr={{ path: "/dashboard" }} />
29
+
30
+ // With runLoaders: loaders execute during SSR
31
+ <Router routes={routes} ssr={{ path: "/dashboard", runLoaders: true }} />`}</CodeBlock>
32
+ <p>
33
+ When loaders run during SSR, their results are passed to components as
34
+ the <code>data</code> prop. The server-rendered HTML includes the
35
+ loader content, so users see the full page immediately.
36
+ </p>
37
+ </section>
38
+
39
+ <section>
40
+ <h3>Example</h3>
41
+ <p>Consider a dashboard route with a loader that fetches user data:</p>
42
+ <CodeBlock language="tsx">{`const routes = [
43
+ route({
44
+ component: AppShell,
45
+ children: [
46
+ route({ path: "/", component: HomePage }),
47
+ route({
48
+ path: "/dashboard",
49
+ component: DashboardPage,
50
+ loader: dashboardLoader,
51
+ }),
52
+ ],
53
+ }),
54
+ ];
55
+
56
+ // Without runLoaders: DashboardPage is skipped during SSR.
57
+ // Users see only the app shell; dashboard content fills in after hydration.
58
+ <Router routes={routes} ssr={{ path: "/dashboard" }} />
59
+
60
+ // With runLoaders: dashboardLoader executes during SSR.
61
+ // DashboardPage renders with data — users see the full page immediately.
62
+ <Router routes={routes} ssr={{ path: "/dashboard", runLoaders: true }} />`}</CodeBlock>
63
+ </section>
64
+
65
+ <section>
66
+ <h3>Server Setup</h3>
67
+ <p>
68
+ Unlike{" "}
69
+ <a href="/learn/ssr/static-site-generation">static site generation</a>
70
+ , SSR with loaders requires a server runtime that can handle requests
71
+ and render pages dynamically. Your server needs to know the requested
72
+ pathname and pass it to the router:
73
+ </p>
74
+ <CodeBlock language="tsx">{`// App.tsx — receives the pathname from the server
75
+ export default function App({ pathname }: { pathname: string }) {
76
+ return (
77
+ <Router
78
+ routes={routes}
79
+ ssr={{ path: pathname, runLoaders: true }}
80
+ />
81
+ );
82
+ }`}</CodeBlock>
83
+ <p>
84
+ Because loaders run during SSR, they must be able to execute in the
85
+ server environment. If your loaders call browser-only APIs, you may
86
+ need to adjust them or use environment checks.
87
+ </p>
88
+ </section>
89
+
90
+ <section>
91
+ <h3>
92
+ Comparison with <code>ssr</code> (No Loaders)
93
+ </h3>
94
+ <p>
95
+ The choice between <code>ssr</code> alone and{" "}
96
+ <code>ssr + runLoaders</code> depends on your deployment model:
97
+ </p>
98
+ <ul>
99
+ <li>
100
+ <strong>
101
+ <code>{"ssr={{ path }}"}</code>
102
+ </strong>{" "}
103
+ &mdash; Ideal for static site generation. Pages without loaders are
104
+ fully pre-rendered. Pages with loaders show the app shell during SSR
105
+ and fill in data after hydration. No server runtime needed.
106
+ </li>
107
+ <li>
108
+ <strong>
109
+ <code>{"ssr={{ path, runLoaders: true }}"}</code>
110
+ </strong>{" "}
111
+ &mdash; Ideal for dynamic SSR with a server runtime. All matched
112
+ routes render during SSR, including those with loaders. Users see
113
+ the complete page immediately.
114
+ </li>
115
+ </ul>
116
+ </section>
117
+
118
+ <section>
119
+ <h3>Key Takeaways</h3>
120
+ <ul>
121
+ <li>
122
+ Set <code>runLoaders: true</code> to execute loaders during SSR for
123
+ fully rendered server output
124
+ </li>
125
+ <li>
126
+ Loader results are passed to components as the <code>data</code>{" "}
127
+ prop during SSR, just as they are on the client
128
+ </li>
129
+ <li>
130
+ This mode requires a server runtime that handles requests
131
+ dynamically
132
+ </li>
133
+ <li>
134
+ After hydration, the real URL from the Navigation API takes over and{" "}
135
+ <code>ssr</code> is ignored
136
+ </li>
137
+ </ul>
138
+ </section>
139
+ </div>
140
+ );
141
+ }
@@ -67,7 +67,7 @@ function UserPage({ params }: { params: { userId: string } }) {
67
67
  For explicit type annotations, use the{" "}
68
68
  <code>RouteComponentProps</code> type helper with your params type:
69
69
  </p>
70
- <CodeBlock language="tsx">{`import { route, RouteComponentProps } from "@funstack/router";
70
+ <CodeBlock language="tsx">{`import { route, type RouteComponentProps } from "@funstack/router";
71
71
 
72
72
  // Define component with explicit props type
73
73
  function UserPage({ params }: RouteComponentProps<{ userId: string }>) {
@@ -97,7 +97,7 @@ const userRoute = route({
97
97
  for routes with loaders.
98
98
  </p>
99
99
  <CodeBlock language="tsx">{`import { use, Suspense } from "react";
100
- import { route, RouteComponentPropsWithData } from "@funstack/router";
100
+ import { route, type RouteComponentPropsWithData } from "@funstack/router";
101
101
 
102
102
  interface User {
103
103
  id: string;
@@ -155,7 +155,7 @@ const userRoute = route({
155
155
  the current page). Use the <code>routeState</code> helper to define
156
156
  typed state for your routes.
157
157
  </p>
158
- <CodeBlock language="tsx">{`import { route, routeState, RouteComponentProps } from "@funstack/router";
158
+ <CodeBlock language="tsx">{`import { routeState, RouteComponentProps } from "@funstack/router";
159
159
 
160
160
  // Define the state shape
161
161
  interface ProductListState {
@@ -209,12 +209,10 @@ function ProductListPage({
209
209
  }
210
210
 
211
211
  // Use routeState to create a typed route
212
- const productListRoute = routeState<ProductListState>()(
213
- route({
214
- path: "/products",
215
- component: ProductListPage,
216
- })
217
- );`}</CodeBlock>
212
+ const productListRoute = routeState<ProductListState>()({
213
+ path: "/products",
214
+ component: ProductListPage,
215
+ });`}</CodeBlock>
218
216
  <p>
219
217
  The <code>routeState</code> helper adds four props to your component:
220
218
  </p>
@@ -312,20 +310,18 @@ function ProductListPage(props: Props) {
312
310
  }
313
311
 
314
312
  // Route definition with both loader and state
315
- const productListRoute = routeState<ProductListState>()(
316
- route({
317
- path: "/products",
318
- component: ProductListPage,
319
- loader: async (): Promise<Product[]> => {
320
- const response = await fetch("/api/products");
321
- return response.json();
322
- },
323
- })
324
- );`}</CodeBlock>
313
+ const productListRoute = routeState<ProductListState>()({
314
+ path: "/products",
315
+ component: ProductListPage,
316
+ loader: async (): Promise<Product[]> => {
317
+ const response = await fetch("/api/products");
318
+ return response.json();
319
+ },
320
+ });`}</CodeBlock>
325
321
  </section>
326
322
 
327
323
  <section>
328
- <h3>Approach 2: Hooks</h3>
324
+ <h3 id="hooks">Approach 2: Hooks</h3>
329
325
 
330
326
  <h4>When to Use Hooks</h4>
331
327
  <p>
@@ -2,6 +2,7 @@
2
2
 
3
3
  ## Available Documentation
4
4
 
5
+ - [Examples](./ExamplesPage.tsx)
5
6
  - [Getting Started](./GettingStartedPage.tsx)
6
7
 
7
8
  ### API Reference
@@ -16,6 +17,8 @@
16
17
  - [Navigation API](./LearnNavigationApiPage.tsx) - FUNSTACK Router is built on the Navigation API , a modern browser API that provides a unified way to handle navigation. This guide explains the key differences from the older History API and the benefits this brings to your application.
17
18
  - [Nested Routes](./LearnNestedRoutesPage.tsx) - Nested routes let you build complex page layouts where parts of the UI persist across navigation while other parts change. Think of a dashboard with a sidebar that stays in place while the main content area updates&mdash;that's nested routing in action.
18
19
  - [React Server Components](./LearnRscPage.tsx) - FUNSTACK Router is designed to work with React Server Components (RSC). The package provides a dedicated server entry point so that route definitions can live in server modules, keeping client bundle sizes small.
19
- - [Server-Side Rendering](./LearnSsrPage.tsx) - FUNSTACK Router supports server-side rendering with a two-stage model. During SSR, pathless (layout) routes without loaders render to produce an app shell, while path-based routes and loaders activate only after client hydration. You can optionally provide an ssrPathname prop to match path-based routes during SSR for richer server-rendered output.
20
+ - [Static Site Generation](./LearnSsgPage.tsx) - When your server or static site generator knows the URL being rendered, you can use the ssr prop to match path-based routes during SSR. This produces richer server-rendered HTML &mdash; users see page content immediately instead of just the app shell.
21
+ - [How SSR Works](./LearnSsrBasicPage.tsx) - FUNSTACK Router supports server-side rendering with a two-stage model. During SSR, pathless (layout) routes without loaders render to produce an app shell, while path-based routes and loaders activate only after client hydration.
22
+ - [SSR with Loaders](./LearnSsrWithLoadersPage.tsx) - When you have a server runtime that can execute loaders at request time, set runLoaders: true in the ssr config to produce fully rendered HTML &mdash; including loader data &mdash; on the server.
20
23
  - [Controlling Transitions](./LearnTransitionsPage.tsx) - FUNSTACK Router wraps navigations in React's startTransition, which means the old UI may stay visible while the new route loads. This page explains how this works and how to control it.
21
24
  - [Type Safety](./LearnTypeSafetyPage.tsx) - FUNSTACK Router provides first-class TypeScript support, allowing you to access route params, navigation state, and loader data with full type safety. This guide covers two approaches: receiving typed data through component props (recommended) and accessing it through hooks.
package/dist/index.d.mts CHANGED
@@ -90,6 +90,30 @@ type OnNavigateCallback = (event: NavigateEvent, info: OnNavigateInfo) => void;
90
90
  type FallbackMode = "none" | "static";
91
91
  //#endregion
92
92
  //#region src/Router.d.ts
93
+ /**
94
+ * SSR configuration for the router.
95
+ */
96
+ type SSRConfig = {
97
+ /**
98
+ * Pathname to use for route matching during SSR.
99
+ *
100
+ * The router uses this pathname to match path-based routes during SSR.
101
+ * Route params are extracted normally.
102
+ */
103
+ path: string;
104
+ /**
105
+ * Whether to run loaders during SSR.
106
+ *
107
+ * - When `false` or omitted, routes with loaders are skipped during SSR
108
+ * and the parent route renders as a shell.
109
+ * - When `true`, routes with loaders are matched and their loaders are
110
+ * executed during SSR. The loader results are passed to components as
111
+ * the `data` prop, so server-rendered HTML includes loader content.
112
+ *
113
+ * @default false
114
+ */
115
+ runLoaders?: boolean;
116
+ };
93
117
  type RouterProps = {
94
118
  routes: RouteDefinition[];
95
119
  /**
@@ -108,11 +132,11 @@ type RouterProps = {
108
132
  */
109
133
  fallback?: FallbackMode;
110
134
  /**
111
- * Pathname to use for route matching during SSR.
135
+ * SSR configuration for the router.
112
136
  *
113
- * By default, during SSR only pathless routes match. When this prop is provided,
114
- * the router uses this pathname to match path-based routes during SSR as well.
115
- * Loaders are not executed during SSR regardless of this setting.
137
+ * By default (no `ssr` prop), during SSR only pathless routes match.
138
+ * When provided, the router uses the given pathname to match path-based
139
+ * routes during SSR as well.
116
140
  *
117
141
  * This prop is only used when the location entry is not available (during SSR
118
142
  * or hydration). Once the client hydrates, the real URL from the Navigation API
@@ -120,16 +144,20 @@ type RouterProps = {
120
144
  *
121
145
  * @example
122
146
  * ```tsx
123
- * <Router routes={routes} ssrPathname="/about" />
147
+ * // SSG: match path-based routes, skip loaders
148
+ * <Router routes={routes} ssr={{ path: "/about" }} />
149
+ *
150
+ * // SSR with loaders: match path-based routes including those with loaders
151
+ * <Router routes={routes} ssr={{ path: "/about", runLoaders: true }} />
124
152
  * ```
125
153
  */
126
- ssrPathname?: string;
154
+ ssr?: SSRConfig;
127
155
  };
128
156
  declare function Router({
129
157
  routes: inputRoutes,
130
158
  onNavigate,
131
159
  fallback,
132
- ssrPathname
160
+ ssr
133
161
  }: RouterProps): ReactNode;
134
162
  //#endregion
135
163
  //#region src/Outlet.d.ts
@@ -275,5 +303,5 @@ declare function useRouteData<T extends TypefulOpaqueRouteDefinition<string, Rec
275
303
  */
276
304
  declare function useIsPending(): boolean;
277
305
  //#endregion
278
- export { type ActionArgs, type ExtractRouteData, type ExtractRouteId, type ExtractRouteParams, type ExtractRouteState, type FallbackMode, type LoaderArgs, type Location, type MatchedRoute, type NavigateOptions, type OnNavigateCallback, type OnNavigateInfo, type OpaqueRouteDefinition, Outlet, type PathParams, type RouteComponentProps, type RouteComponentPropsOf, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type TypefulOpaqueRouteDefinition, type UseBlockerOptions, route, routeState, useBlocker, useIsPending, useLocation, useNavigate, useRouteData, useRouteParams, useRouteState, useSearchParams };
306
+ export { type ActionArgs, type ExtractRouteData, type ExtractRouteId, type ExtractRouteParams, type ExtractRouteState, type FallbackMode, type LoaderArgs, type Location, type MatchedRoute, type NavigateOptions, type OnNavigateCallback, type OnNavigateInfo, type OpaqueRouteDefinition, Outlet, type PathParams, type RouteComponentProps, type RouteComponentPropsOf, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type SSRConfig, type TypefulOpaqueRouteDefinition, type UseBlockerOptions, route, routeState, useBlocker, useIsPending, useLocation, useNavigate, useRouteData, useRouteParams, useRouteState, useSearchParams };
279
307
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/hooks/useRouteParams.ts","../src/hooks/useRouteState.ts","../src/hooks/useRouteData.ts","../src/hooks/useIsPending.ts"],"mappings":";;;;cAGM,6BAAA;;AAFoE;;;;;;;KAgB9D,uBAAA;EAAA,CACT,6BAAA,UAyB0B;EAvB3B,IAAA,WA4Be;EA1Bf,QAAA,GAAW,uBAAA;EAgCc;;;;;;EAzBzB,KAAA;EAPA;;;;;EAaA,eAAA,YAM2B;EAA3B,MAAA,IAAU,IAAA,EAAM,UAAA,CAAW,MAAA,+BAE3B;EAAA,MAAA,IAAU,IAAA,EAAM,UAAA,CAAW,MAAA,wCAAA;EAE3B,SAAA,GACI,aAAA;IACE,IAAA;IACA,MAAA,GAAS,MAAA;IACT,KAAA;IACA,QAAA,IACE,KAAA,cAAmB,IAAA,2BAChB,OAAA;IACL,YAAA,IAAgB,KAAA,cAAmB,IAAA;IACnC,UAAA,SAAmB,OAAA;IACnB,cAAA;IACA,IAAA;EAAA,KAEF,SAAA;AAAA;;;AAmBN;KAAY,YAAA;sCAEV,KAAA,EAAO,uBAAA,EAAP;EAEA,MAAA,EAAQ,MAAA,kBAAR;EAEA,QAAA;AAAA;;;;KAcU,cAAA;EAMQ,4DAJlB,OAAA,WAAkB,YAAA,WAUR;EARV,YAAA;EAEA,QAAA,EAAU,QAAA;AAAA;;;;KAMA,eAAA;EAYA,uDAVV,OAAA;EAEA,KAAA,YASA;EAPA,IAAA;AAAA;;;AAmBF;KAbY,QAAA;EACV,QAAA;EACA,MAAA;EACA,IAAA;AAAA;;;;;AAqBF;;;KAXY,kBAAA,IACV,KAAA,EAAO,aAAA,EACP,IAAA,EAAM,cAAA;;;;;AC1GR;;KDmHY,YAAA;;;KCnHA,WAAA;EACV,MAAA,EAAQ,eAAA;ED5BoC;;;;AAc9C;;;ECsBE,UAAA,GAAa,kBAAA;EDjBF;;;;;;ECwBX,QAAA,GAAW,YAAA;EDQc;;;;;;;;;;;;;;;;ECSzB,WAAA;AAAA;AAAA,iBAYc,MAAA,CAAA;EACd,MAAA,EAAQ,WAAA;EACR,UAAA;EACA,QAAA;EACA;AAAA,GACC,WAAA,GAAc,SAAA;;;;;;AD/EyD;iBEM1D,MAAA,CAAA,GAAU,SAAA;;;;;;iBCAV,WAAA,CAAA,IAAgB,EAAA,UAAY,OAAA,GAAU,eAAA;;;;;;iBCAtC,WAAA,CAAA,GAAe,QAAA;;;KCJ1B,eAAA,IACH,MAAA,EACI,eAAA,GACA,MAAA,qBACE,IAAA,EAAM,eAAA,KAAoB,eAAA,GAAkB,MAAA;;;;iBAMpC,eAAA,CAAA,IAAoB,eAAA,EAAiB,eAAA;;;KCVzC,iBAAA;;;;ANF8D;EMOxE,WAAA;AAAA;;;ANSF;;;;;;;;;;;;;;;;;;;;;;;;;iBMqBgB,UAAA,CAAW,OAAA,EAAS,iBAAA;;;;;;ANrCsC;;;;;AAgB1E;;;;;;;;;;;;iBOSgB,cAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,kBAAA,CAAmB,CAAA;;;;;;APhC0C;;;;;AAgB1E;;;;;;;;;;;;;iBQUgB,aAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,iBAAA,CAAkB,CAAA;;;;;;ARjC2C;;;;;AAgB1E;;;;;;;;;;;;;;;;iBSagB,YAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,gBAAA,CAAiB,CAAA;;;;;;iBC/Bd,YAAA,CAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/hooks/useRouteParams.ts","../src/hooks/useRouteState.ts","../src/hooks/useRouteData.ts","../src/hooks/useIsPending.ts"],"mappings":";;;;cAGM,6BAAA;;AAFoE;;;;;;;KAgB9D,uBAAA;EAAA,CACT,6BAAA,UAyB0B;EAvB3B,IAAA,WA4Be;EA1Bf,QAAA,GAAW,uBAAA;EAgCc;;;;;;EAzBzB,KAAA;EAPA;;;;;EAaA,eAAA,YAM2B;EAA3B,MAAA,IAAU,IAAA,EAAM,UAAA,CAAW,MAAA,+BAE3B;EAAA,MAAA,IAAU,IAAA,EAAM,UAAA,CAAW,MAAA,wCAAA;EAE3B,SAAA,GACI,aAAA;IACE,IAAA;IACA,MAAA,GAAS,MAAA;IACT,KAAA;IACA,QAAA,IACE,KAAA,cAAmB,IAAA,2BAChB,OAAA;IACL,YAAA,IAAgB,KAAA,cAAmB,IAAA;IACnC,UAAA,SAAmB,OAAA;IACnB,cAAA;IACA,IAAA;EAAA,KAEF,SAAA;AAAA;;;AAmBN;KAAY,YAAA;sCAEV,KAAA,EAAO,uBAAA,EAAP;EAEA,MAAA,EAAQ,MAAA,kBAAR;EAEA,QAAA;AAAA;;;;KAcU,cAAA;EAMQ,4DAJlB,OAAA,WAAkB,YAAA,WAUR;EARV,YAAA;EAEA,QAAA,EAAU,QAAA;AAAA;;;;KAMA,eAAA;EAYA,uDAVV,OAAA;EAEA,KAAA,YASA;EAPA,IAAA;AAAA;;;AAmBF;KAbY,QAAA;EACV,QAAA;EACA,MAAA;EACA,IAAA;AAAA;;;;;AAqBF;;;KAXY,kBAAA,IACV,KAAA,EAAO,aAAA,EACP,IAAA,EAAM,cAAA;;;;;ACvGR;;KDgHY,YAAA;;;;AAhJ8D;;KCgC9D,SAAA;ED9BkC;;AAc9C;;;;ECuBE,IAAA;EDC2B;;;;;;;;;;;ECW3B,UAAA;AAAA;AAAA,KAGU,WAAA;EACV,MAAA,EAAQ,eAAA;ED3BR;;;;;;;ECmCA,UAAA,GAAa,kBAAA;EDrBc;;;;;;EC4B3B,QAAA,GAAW,YAAA;EDtBL;;;;;;;;;;;;;;;AA4BR;;;;;ECeE,GAAA,GAAM,SAAA;AAAA;AAAA,iBAYQ,MAAA,CAAA;EACd,MAAA,EAAQ,WAAA;EACR,UAAA;EACA,QAAA;EACA;AAAA,GACC,WAAA,GAAc,SAAA;;;;;;AD5GyD;iBEM1D,MAAA,CAAA,GAAU,SAAA;;;;;;iBCAV,WAAA,CAAA,IAAgB,EAAA,UAAY,OAAA,GAAU,eAAA;;;;;;iBCAtC,WAAA,CAAA,GAAe,QAAA;;;KCJ1B,eAAA,IACH,MAAA,EACI,eAAA,GACA,MAAA,qBACE,IAAA,EAAM,eAAA,KAAoB,eAAA,GAAkB,MAAA;;;;iBAMpC,eAAA,CAAA,IAAoB,eAAA,EAAiB,eAAA;;;KCVzC,iBAAA;;;;ANF8D;EMOxE,WAAA;AAAA;;;ANSF;;;;;;;;;;;;;;;;;;;;;;;;;iBMqBgB,UAAA,CAAW,OAAA,EAAS,iBAAA;;;;;;ANrCsC;;;;;AAgB1E;;;;;;;;;;;;iBOSgB,cAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,kBAAA,CAAmB,CAAA;;;;;;APhC0C;;;;;AAgB1E;;;;;;;;;;;;;iBQUgB,aAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,iBAAA,CAAkB,CAAA;;;;;;ARjC2C;;;;;AAgB1E;;;;;;;;;;;;;;;;iBSagB,YAAA,WACJ,4BAAA,SAER,MAAA,oCAAA,CAIF,KAAA,EAAO,CAAA,GAAI,gBAAA,CAAiB,CAAA;;;;;;iBC/Bd,YAAA,CAAA"}
package/dist/index.mjs CHANGED
@@ -61,6 +61,7 @@ function internalRoutes(routes) {
61
61
 
62
62
  //#endregion
63
63
  //#region src/core/matchRoutes.ts
64
+ const SKIPPED = Symbol("skipped");
64
65
  /**
65
66
  * Match a pathname against a route tree, returning the matched route stack.
66
67
  * Returns null if no match is found.
@@ -68,6 +69,7 @@ function internalRoutes(routes) {
68
69
  function matchRoutes(routes, pathname, options) {
69
70
  for (const route of routes) {
70
71
  const matched = matchRoute(route, pathname, options);
72
+ if (matched === SKIPPED) return null;
71
73
  if (matched) return matched;
72
74
  }
73
75
  return null;
@@ -78,7 +80,15 @@ function matchRoutes(routes, pathname, options) {
78
80
  function matchRoute(route, pathname, options) {
79
81
  const hasChildren = Boolean(route.children?.length);
80
82
  const skipLoaders = options?.skipLoaders ?? false;
81
- if ((pathname === null || skipLoaders) && route.loader) return null;
83
+ if ((pathname === null || skipLoaders) && route.loader) {
84
+ if (skipLoaders && pathname !== null) {
85
+ if (route.path === void 0) return SKIPPED;
86
+ const isExact = route.exact ?? !hasChildren;
87
+ const { matched } = matchPath(route.path, pathname, isExact);
88
+ if (matched) return SKIPPED;
89
+ }
90
+ return null;
91
+ }
82
92
  if (route.path === void 0) {
83
93
  const result = {
84
94
  route,
@@ -86,10 +96,19 @@ function matchRoute(route, pathname, options) {
86
96
  pathname: ""
87
97
  };
88
98
  if (hasChildren) {
99
+ let anySkipped = false;
89
100
  for (const child of route.children) {
90
101
  const childMatch = matchRoute(child, pathname, options);
102
+ if (childMatch === SKIPPED) {
103
+ anySkipped = true;
104
+ break;
105
+ }
91
106
  if (childMatch) return [result, ...childMatch];
92
107
  }
108
+ if (anySkipped) {
109
+ if (route.component) return [result];
110
+ return SKIPPED;
111
+ }
93
112
  if (route.component && route.requireChildren === false) return [result];
94
113
  if ((pathname === null || skipLoaders) && route.component) return [result];
95
114
  return null;
@@ -109,8 +128,13 @@ function matchRoute(route, pathname, options) {
109
128
  let remainingPathname = pathname.slice(consumedPathname.length);
110
129
  if (!remainingPathname.startsWith("/")) remainingPathname = "/" + remainingPathname;
111
130
  if (remainingPathname === "") remainingPathname = "/";
131
+ let anyChildSkipped = false;
112
132
  for (const child of route.children) {
113
133
  const childMatch = matchRoute(child, remainingPathname, options);
134
+ if (childMatch === SKIPPED) {
135
+ anyChildSkipped = true;
136
+ break;
137
+ }
114
138
  if (childMatch) return [result, ...childMatch.map((m) => ({
115
139
  ...m,
116
140
  params: {
@@ -119,6 +143,10 @@ function matchRoute(route, pathname, options) {
119
143
  }
120
144
  }))];
121
145
  }
146
+ if (anyChildSkipped) {
147
+ if (route.component) return [result];
148
+ return SKIPPED;
149
+ }
122
150
  if (route.component && route.requireChildren === false) return [result];
123
151
  if (skipLoaders && route.component) return [result];
124
152
  return null;
@@ -470,7 +498,7 @@ function createAdapter(fallback) {
470
498
  const serverSnapshotSymbol = Symbol();
471
499
  const noopSubscribe = () => () => {};
472
500
  const getServerSnapshot = () => serverSnapshotSymbol;
473
- function Router({ routes: inputRoutes, onNavigate, fallback = "none", ssrPathname }) {
501
+ function Router({ routes: inputRoutes, onNavigate, fallback = "none", ssr }) {
474
502
  const routes = internalRoutes(inputRoutes);
475
503
  const adapter = useMemo(() => createAdapter(fallback), [fallback]);
476
504
  const [blockerRegistry] = useState(() => createBlockerRegistry());
@@ -506,22 +534,22 @@ function Router({ routes: inputRoutes, onNavigate, fallback = "none", ssrPathnam
506
534
  }, [adapter]);
507
535
  return useMemo(() => {
508
536
  const matchedRoutesWithData = (() => {
509
- if (locationEntry === null) {
510
- const matched = matchRoutes(routes, ssrPathname ?? null, { skipLoaders: true });
537
+ if (locationEntry === null && !ssr?.runLoaders) {
538
+ const matched = matchRoutes(routes, ssr?.path ?? null, { skipLoaders: true });
511
539
  if (!matched) return null;
512
540
  return matched.map((m) => ({
513
541
  ...m,
514
542
  data: void 0
515
543
  }));
516
544
  }
517
- const { url, key } = locationEntry;
545
+ const url = locationEntry ? locationEntry.url : new URL(ssr.path, "http://localhost");
518
546
  const matched = matchRoutes(routes, url.pathname);
519
547
  if (!matched) return null;
520
- return executeLoaders(matched, key, createLoaderRequest(url), adapter.getIdleAbortSignal());
548
+ return executeLoaders(matched, locationEntry?.key ?? "ssr", createLoaderRequest(url), locationEntry ? adapter.getIdleAbortSignal() : new AbortController().signal);
521
549
  })();
522
550
  const routerContextValue = {
523
551
  locationEntry,
524
- url: locationEntry?.url ?? null,
552
+ url: locationEntry?.url ?? (ssr ? new URL(ssr.path, "http://localhost") : null),
525
553
  isPending,
526
554
  navigate,
527
555
  navigateAsync,
@@ -547,7 +575,7 @@ function Router({ routes: inputRoutes, onNavigate, fallback = "none", ssrPathnam
547
575
  routes,
548
576
  adapter,
549
577
  blockerRegistry,
550
- ssrPathname
578
+ ssr
551
579
  ]);
552
580
  }
553
581
  /**