@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.
- package/dist/docs/ApiComponentsPage.tsx +10 -9
- package/dist/docs/ExamplesPage.tsx +445 -0
- package/dist/docs/GettingStartedPage.tsx +10 -33
- package/dist/docs/LearnNavigationApiPage.tsx +1 -4
- package/dist/docs/LearnNestedRoutesPage.tsx +8 -4
- package/dist/docs/LearnRscPage.tsx +75 -105
- package/dist/docs/LearnSsgPage.tsx +133 -0
- package/dist/docs/{LearnSsrPage.tsx → LearnSsrBasicPage.tsx} +38 -94
- package/dist/docs/LearnSsrWithLoadersPage.tsx +141 -0
- package/dist/docs/LearnTypeSafetyPage.tsx +16 -20
- package/dist/docs/index.md +4 -1
- package/dist/index.d.mts +36 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +36 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -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 — including loader data — 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
|
+
— 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
|
+
— 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 {
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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>
|
package/dist/docs/index.md
CHANGED
|
@@ -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—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
|
-
- [
|
|
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 — 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 — including loader data — 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
|
-
*
|
|
135
|
+
* SSR configuration for the router.
|
|
112
136
|
*
|
|
113
|
-
* By default, during SSR only pathless routes match.
|
|
114
|
-
* the router uses
|
|
115
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
154
|
+
ssr?: SSRConfig;
|
|
127
155
|
};
|
|
128
156
|
declare function Router({
|
|
129
157
|
routes: inputRoutes,
|
|
130
158
|
onNavigate,
|
|
131
159
|
fallback,
|
|
132
|
-
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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;;;;;
|
|
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)
|
|
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",
|
|
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,
|
|
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
|
|
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
|
-
|
|
578
|
+
ssr
|
|
551
579
|
]);
|
|
552
580
|
}
|
|
553
581
|
/**
|