@flight-framework/router 0.0.1 → 0.0.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 +151 -22
- package/dist/index.d.ts +267 -16
- package/dist/index.js +399 -55
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# @flight-framework/router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Universal client-side routing primitives for Flight Framework. Zero external dependencies. Works with any UI framework.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
7
|
+
- Multiple prefetch strategies (none, intent, render, viewport)
|
|
8
|
+
- SSR-safe implementation
|
|
9
|
+
- Framework-agnostic with React adapters
|
|
10
|
+
- TypeScript-first design
|
|
11
|
+
- IntersectionObserver for viewport prefetching
|
|
12
|
+
- Backwards compatible API
|
|
11
13
|
|
|
12
14
|
## Installation
|
|
13
15
|
|
|
@@ -15,14 +17,13 @@ Agnostic client-side routing primitives for Flight Framework.
|
|
|
15
17
|
npm install @flight-framework/router
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
##
|
|
20
|
+
## Quick Start
|
|
19
21
|
|
|
20
22
|
### With React
|
|
21
23
|
|
|
22
24
|
```tsx
|
|
23
25
|
import { RouterProvider, Link, useRouter } from '@flight-framework/router';
|
|
24
26
|
|
|
25
|
-
// Wrap your app
|
|
26
27
|
function App() {
|
|
27
28
|
return (
|
|
28
29
|
<RouterProvider initialPath={url}>
|
|
@@ -32,21 +33,19 @@ function App() {
|
|
|
32
33
|
);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
// Use Link for navigation
|
|
36
36
|
function Navigation() {
|
|
37
37
|
return (
|
|
38
38
|
<nav>
|
|
39
39
|
<Link href="/">Home</Link>
|
|
40
|
-
<Link href="/docs" prefetch>Docs</Link>
|
|
40
|
+
<Link href="/docs" prefetch="intent">Docs</Link>
|
|
41
41
|
<Link href="/about">About</Link>
|
|
42
42
|
</nav>
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Use hooks to access router state
|
|
47
46
|
function Content() {
|
|
48
47
|
const { path, navigate } = useRouter();
|
|
49
|
-
|
|
48
|
+
|
|
50
49
|
return (
|
|
51
50
|
<main>
|
|
52
51
|
<p>Current path: {path}</p>
|
|
@@ -58,7 +57,89 @@ function Content() {
|
|
|
58
57
|
}
|
|
59
58
|
```
|
|
60
59
|
|
|
61
|
-
|
|
60
|
+
## Prefetch Strategies
|
|
61
|
+
|
|
62
|
+
Flight Router supports multiple prefetch strategies to optimize navigation performance:
|
|
63
|
+
|
|
64
|
+
| Strategy | Behavior | Best For |
|
|
65
|
+
|----------|----------|----------|
|
|
66
|
+
| `'none'` | No prefetching (default) | Low-priority links |
|
|
67
|
+
| `'intent'` | Prefetch on hover/focus | Most navigation links |
|
|
68
|
+
| `'render'` | Prefetch immediately | Critical paths (checkout) |
|
|
69
|
+
| `'viewport'` | Prefetch when visible | Mobile, infinite scroll |
|
|
70
|
+
|
|
71
|
+
### Usage Examples
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// No prefetching (default)
|
|
75
|
+
<Link href="/docs">Docs</Link>
|
|
76
|
+
<Link href="/docs" prefetch="none">Docs</Link>
|
|
77
|
+
|
|
78
|
+
// Prefetch on hover/focus (recommended for most cases)
|
|
79
|
+
<Link href="/docs" prefetch="intent">Docs</Link>
|
|
80
|
+
<Link href="/docs" prefetch>Docs</Link> // boolean true = "intent"
|
|
81
|
+
|
|
82
|
+
// Prefetch immediately when link renders
|
|
83
|
+
<Link href="/checkout" prefetch="render">Checkout</Link>
|
|
84
|
+
|
|
85
|
+
// Prefetch when link enters viewport (good for mobile)
|
|
86
|
+
<Link href="/products" prefetch="viewport">Products</Link>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Programmatic Prefetching
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import {
|
|
93
|
+
prefetch,
|
|
94
|
+
prefetchAll,
|
|
95
|
+
prefetchWhenIdle,
|
|
96
|
+
isPrefetched,
|
|
97
|
+
} from '@flight-framework/router';
|
|
98
|
+
|
|
99
|
+
// Basic prefetch
|
|
100
|
+
prefetch('/docs');
|
|
101
|
+
|
|
102
|
+
// High priority prefetch
|
|
103
|
+
prefetch('/checkout', { priority: 'high' });
|
|
104
|
+
|
|
105
|
+
// Prefetch with data loaders
|
|
106
|
+
prefetch('/products', { includeData: true });
|
|
107
|
+
|
|
108
|
+
// Prefetch multiple pages
|
|
109
|
+
prefetchAll(['/page1', '/page2', '/page3']);
|
|
110
|
+
|
|
111
|
+
// Prefetch when browser is idle
|
|
112
|
+
prefetchWhenIdle('/dashboard');
|
|
113
|
+
|
|
114
|
+
// Check if already prefetched
|
|
115
|
+
if (!isPrefetched('/docs')) {
|
|
116
|
+
prefetch('/docs');
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## PrefetchPageLinks Component
|
|
121
|
+
|
|
122
|
+
For proactive prefetching based on user behavior (search results, autocomplete):
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { PrefetchPageLinks } from '@flight-framework/router';
|
|
126
|
+
|
|
127
|
+
function SearchResults({ results }) {
|
|
128
|
+
return (
|
|
129
|
+
<>
|
|
130
|
+
{results.map(result => (
|
|
131
|
+
<div key={result.id}>
|
|
132
|
+
{/* Prefetch as soon as result renders */}
|
|
133
|
+
<PrefetchPageLinks page={result.url} />
|
|
134
|
+
<Link href={result.url}>{result.title}</Link>
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Hooks
|
|
62
143
|
|
|
63
144
|
```tsx
|
|
64
145
|
// Get current path and navigation functions
|
|
@@ -74,9 +155,9 @@ const [searchParams, setSearchParams] = useSearchParams();
|
|
|
74
155
|
const pathname = usePathname();
|
|
75
156
|
```
|
|
76
157
|
|
|
77
|
-
|
|
158
|
+
## Programmatic Navigation
|
|
78
159
|
|
|
79
|
-
```
|
|
160
|
+
```typescript
|
|
80
161
|
import { navigate, prefetch } from '@flight-framework/router';
|
|
81
162
|
|
|
82
163
|
// Navigate to a path
|
|
@@ -90,14 +171,11 @@ navigate('/next-page', { scroll: false });
|
|
|
90
171
|
|
|
91
172
|
// Pass state data
|
|
92
173
|
navigate('/dashboard', { state: { from: '/login' } });
|
|
93
|
-
|
|
94
|
-
// Prefetch a route
|
|
95
|
-
prefetch('/heavy-page');
|
|
96
174
|
```
|
|
97
175
|
|
|
98
|
-
|
|
176
|
+
## Route Matching
|
|
99
177
|
|
|
100
|
-
```
|
|
178
|
+
```typescript
|
|
101
179
|
import { matchRoute, parseParams, generatePath } from '@flight-framework/router';
|
|
102
180
|
|
|
103
181
|
// Check if a path matches a pattern
|
|
@@ -113,16 +191,45 @@ const path = generatePath('/docs/:slug', { slug: 'api-routes' });
|
|
|
113
191
|
// '/docs/api-routes'
|
|
114
192
|
```
|
|
115
193
|
|
|
116
|
-
|
|
194
|
+
## Link Props
|
|
117
195
|
|
|
118
196
|
| Prop | Type | Default | Description |
|
|
119
197
|
|------|------|---------|-------------|
|
|
120
198
|
| `href` | `string` | required | Target URL path |
|
|
121
|
-
| `prefetch` | `boolean` | `
|
|
199
|
+
| `prefetch` | `boolean \| PrefetchStrategy` | `'none'` | Prefetch strategy |
|
|
122
200
|
| `replace` | `boolean` | `false` | Replace history entry |
|
|
123
201
|
| `scroll` | `boolean` | `true` | Scroll to top on navigate |
|
|
124
202
|
| `target` | `string` | - | Link target (_blank, etc) |
|
|
125
203
|
| `className` | `string` | - | CSS class |
|
|
204
|
+
| `aria-label` | `string` | - | Accessibility label |
|
|
205
|
+
|
|
206
|
+
## Vue Integration
|
|
207
|
+
|
|
208
|
+
```vue
|
|
209
|
+
<script setup>
|
|
210
|
+
import { useLinkProps } from '@flight-framework/router';
|
|
211
|
+
|
|
212
|
+
const docsLink = useLinkProps('/docs', { prefetch: 'intent' });
|
|
213
|
+
</script>
|
|
214
|
+
|
|
215
|
+
<template>
|
|
216
|
+
<a v-bind="docsLink">Documentation</a>
|
|
217
|
+
</template>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Vanilla JavaScript
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { createLink, prefetch } from '@flight-framework/router';
|
|
224
|
+
|
|
225
|
+
const link = createLink({
|
|
226
|
+
href: '/docs',
|
|
227
|
+
children: 'Documentation',
|
|
228
|
+
prefetch: 'intent',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
document.body.appendChild(link);
|
|
232
|
+
```
|
|
126
233
|
|
|
127
234
|
## SSR Support
|
|
128
235
|
|
|
@@ -139,6 +246,28 @@ export function render(url: string) {
|
|
|
139
246
|
}
|
|
140
247
|
```
|
|
141
248
|
|
|
249
|
+
## Build Integration
|
|
250
|
+
|
|
251
|
+
For optimal prefetching, Flight generates a route manifest during build that maps routes to their JS modules. This enables `prefetch` to preload the correct chunks.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// flight.config.ts
|
|
255
|
+
export default defineConfig({
|
|
256
|
+
router: {
|
|
257
|
+
// Generates __FLIGHT_MANIFEST__ at build time
|
|
258
|
+
generateManifest: true,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Browser Support
|
|
264
|
+
|
|
265
|
+
| Feature | Chrome | Firefox | Safari | Edge |
|
|
266
|
+
|---------|--------|---------|--------|------|
|
|
267
|
+
| Prefetch | All | All | All | All |
|
|
268
|
+
| Viewport (IntersectionObserver) | 51+ | 55+ | 12.1+ | 15+ |
|
|
269
|
+
| fetchPriority | 101+ | No | No | 101+ |
|
|
270
|
+
|
|
142
271
|
## License
|
|
143
272
|
|
|
144
273
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,30 @@ interface RouterProviderProps {
|
|
|
27
27
|
/** Base path for the router (e.g., '/app') */
|
|
28
28
|
basePath?: string;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Prefetch strategy for Link component
|
|
32
|
+
*
|
|
33
|
+
* - 'none': No prefetching (default)
|
|
34
|
+
* - 'intent': Prefetch on hover or focus (recommended for most cases)
|
|
35
|
+
* - 'render': Prefetch immediately when link renders
|
|
36
|
+
* - 'viewport': Prefetch when link enters the viewport (good for mobile)
|
|
37
|
+
*/
|
|
38
|
+
type PrefetchStrategy = 'none' | 'intent' | 'render' | 'viewport';
|
|
39
|
+
/**
|
|
40
|
+
* Priority level for prefetch requests
|
|
41
|
+
*/
|
|
42
|
+
type PrefetchPriority = 'high' | 'low' | 'auto';
|
|
43
|
+
/**
|
|
44
|
+
* Options for programmatic prefetching
|
|
45
|
+
*/
|
|
46
|
+
interface PrefetchOptions {
|
|
47
|
+
/** Priority of the prefetch request */
|
|
48
|
+
priority?: PrefetchPriority;
|
|
49
|
+
/** Include data prefetch (loaders) */
|
|
50
|
+
includeData?: boolean;
|
|
51
|
+
/** Include module prefetch (JS chunks) */
|
|
52
|
+
includeModules?: boolean;
|
|
53
|
+
}
|
|
30
54
|
/**
|
|
31
55
|
* Props for Link component
|
|
32
56
|
*/
|
|
@@ -41,8 +65,17 @@ interface LinkProps {
|
|
|
41
65
|
target?: string;
|
|
42
66
|
/** Link relationship */
|
|
43
67
|
rel?: string;
|
|
44
|
-
/**
|
|
45
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Prefetch strategy for the target page.
|
|
70
|
+
*
|
|
71
|
+
* - `true` or `'intent'`: Prefetch on hover/focus
|
|
72
|
+
* - `'render'`: Prefetch when link renders
|
|
73
|
+
* - `'viewport'`: Prefetch when link enters viewport
|
|
74
|
+
* - `false` or `'none'`: No prefetching (default)
|
|
75
|
+
*
|
|
76
|
+
* @default 'none'
|
|
77
|
+
*/
|
|
78
|
+
prefetch?: boolean | PrefetchStrategy;
|
|
46
79
|
/** Replace current history entry instead of pushing */
|
|
47
80
|
replace?: boolean;
|
|
48
81
|
/** Scroll to top after navigation */
|
|
@@ -71,6 +104,51 @@ type RouteParams<T extends string = string> = Record<T, string>;
|
|
|
71
104
|
* Search params as key-value pairs
|
|
72
105
|
*/
|
|
73
106
|
type SearchParams = Record<string, string | string[]>;
|
|
107
|
+
/**
|
|
108
|
+
* Route definition for automatic route generation
|
|
109
|
+
*/
|
|
110
|
+
interface RouteDefinition {
|
|
111
|
+
/** Route path pattern (e.g., '/docs/:slug') */
|
|
112
|
+
path: string;
|
|
113
|
+
/** Dynamic import for component */
|
|
114
|
+
component: () => Promise<{
|
|
115
|
+
default: unknown;
|
|
116
|
+
}>;
|
|
117
|
+
/** Dynamic imports for layouts (in order, root first) */
|
|
118
|
+
layouts?: Array<() => Promise<{
|
|
119
|
+
default: unknown;
|
|
120
|
+
}>>;
|
|
121
|
+
/** Dynamic import for loader function */
|
|
122
|
+
loader?: () => Promise<{
|
|
123
|
+
loader: LoaderFunction;
|
|
124
|
+
}>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Loader function signature
|
|
128
|
+
*/
|
|
129
|
+
type LoaderFunction = (context: LoaderContext) => Promise<unknown> | unknown;
|
|
130
|
+
/**
|
|
131
|
+
* Context passed to loader functions
|
|
132
|
+
*/
|
|
133
|
+
interface LoaderContext {
|
|
134
|
+
/** Route parameters */
|
|
135
|
+
params: RouteParams;
|
|
136
|
+
/** Request object (available on server) */
|
|
137
|
+
request?: Request;
|
|
138
|
+
/** URL search params */
|
|
139
|
+
searchParams: URLSearchParams;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Route match result
|
|
143
|
+
*/
|
|
144
|
+
interface RouteMatch {
|
|
145
|
+
/** Matched route definition */
|
|
146
|
+
route: RouteDefinition;
|
|
147
|
+
/** Extracted parameters */
|
|
148
|
+
params: RouteParams;
|
|
149
|
+
/** Matched path */
|
|
150
|
+
pathname: string;
|
|
151
|
+
}
|
|
74
152
|
|
|
75
153
|
/**
|
|
76
154
|
* Router Context and Provider
|
|
@@ -88,13 +166,101 @@ declare let RouterProvider: unknown;
|
|
|
88
166
|
declare let useRouter: () => RouterContextValue;
|
|
89
167
|
|
|
90
168
|
/**
|
|
91
|
-
* Link Component
|
|
169
|
+
* @flight-framework/router - Link Component
|
|
170
|
+
*
|
|
171
|
+
* Universal client-side navigation link with advanced prefetching support.
|
|
172
|
+
* Works with React, Vue, Svelte, Solid, and vanilla JavaScript.
|
|
92
173
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
174
|
+
* Features:
|
|
175
|
+
* - Multiple prefetch strategies (none, intent, render, viewport)
|
|
176
|
+
* - SSR-safe implementation
|
|
177
|
+
* - Accessibility compliant
|
|
178
|
+
* - Framework-agnostic core with React adapter
|
|
95
179
|
*/
|
|
96
180
|
|
|
181
|
+
/**
|
|
182
|
+
* @deprecated Use `prefetch` from '@flight-framework/router' instead.
|
|
183
|
+
*/
|
|
184
|
+
declare function prefetchRoute(href: string): void;
|
|
97
185
|
declare let Link: unknown;
|
|
186
|
+
/**
|
|
187
|
+
* Create a link element with SPA navigation support (vanilla JS).
|
|
188
|
+
*
|
|
189
|
+
* For Vue, Svelte, Solid, and other frameworks, use this function
|
|
190
|
+
* or implement framework-specific wrappers.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* const link = createLink({
|
|
195
|
+
* href: '/docs',
|
|
196
|
+
* children: 'Documentation',
|
|
197
|
+
* prefetch: 'intent',
|
|
198
|
+
* });
|
|
199
|
+
* document.body.appendChild(link);
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
declare function createLink(props: LinkProps): HTMLAnchorElement;
|
|
203
|
+
/**
|
|
204
|
+
* Get link props for Vue template usage.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```vue
|
|
208
|
+
* <script setup>
|
|
209
|
+
* import { useLinkProps } from '@flight-framework/router';
|
|
210
|
+
* const linkProps = useLinkProps('/docs', { prefetch: 'intent' });
|
|
211
|
+
* </script>
|
|
212
|
+
*
|
|
213
|
+
* <template>
|
|
214
|
+
* <a v-bind="linkProps">Documentation</a>
|
|
215
|
+
* </template>
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
declare function useLinkProps(href: string, options?: Partial<Omit<LinkProps, 'href' | 'children'>>): {
|
|
219
|
+
href: string;
|
|
220
|
+
onClick: (e: MouseEvent) => void;
|
|
221
|
+
onMouseenter?: () => void;
|
|
222
|
+
onFocus?: () => void;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @flight-framework/router - PrefetchPageLinks Component
|
|
227
|
+
*
|
|
228
|
+
* Renders prefetch link tags for a page. Useful for proactively
|
|
229
|
+
* prefetching pages based on user behavior (e.g., search results).
|
|
230
|
+
*
|
|
231
|
+
* This is the Flight equivalent of React Router's PrefetchPageLinks.
|
|
232
|
+
*/
|
|
233
|
+
|
|
234
|
+
declare let PrefetchPageLinks: unknown;
|
|
235
|
+
/**
|
|
236
|
+
* Prefetch multiple pages at once.
|
|
237
|
+
*
|
|
238
|
+
* Useful for prefetching a list of search results or related pages.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* // Prefetch top 5 search results
|
|
243
|
+
* prefetchPages([
|
|
244
|
+
* '/products/1',
|
|
245
|
+
* '/products/2',
|
|
246
|
+
* '/products/3',
|
|
247
|
+
* ]);
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
declare function prefetchPages(pages: string[], options?: PrefetchOptions): void;
|
|
251
|
+
/**
|
|
252
|
+
* Prefetch a page when idle.
|
|
253
|
+
*
|
|
254
|
+
* Uses requestIdleCallback if available, otherwise falls back
|
|
255
|
+
* to setTimeout.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* // Prefetch after initial render settles
|
|
260
|
+
* prefetchWhenIdle('/dashboard');
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
declare function prefetchWhenIdle(page: string, options?: PrefetchOptions): void;
|
|
98
264
|
|
|
99
265
|
/**
|
|
100
266
|
* Router Hooks
|
|
@@ -124,16 +290,6 @@ declare let usePathname: () => string;
|
|
|
124
290
|
* ```
|
|
125
291
|
*/
|
|
126
292
|
declare function navigate(to: string, options?: NavigateOptions): void;
|
|
127
|
-
/**
|
|
128
|
-
* Prefetch a route's assets
|
|
129
|
-
* Creates a prefetch link for the target URL
|
|
130
|
-
*
|
|
131
|
-
* @example
|
|
132
|
-
* ```ts
|
|
133
|
-
* prefetch('/docs'); // Prefetch when user hovers
|
|
134
|
-
* ```
|
|
135
|
-
*/
|
|
136
|
-
declare function prefetch(href: string): void;
|
|
137
293
|
/**
|
|
138
294
|
* Match a pathname against a route pattern
|
|
139
295
|
*
|
|
@@ -157,5 +313,100 @@ declare function matchRoute(pathname: string, pattern: string): {
|
|
|
157
313
|
* ```
|
|
158
314
|
*/
|
|
159
315
|
declare function parseParams(pathname: string, pattern: string): RouteParams;
|
|
316
|
+
/**
|
|
317
|
+
* Find the best matching route from a list of route definitions
|
|
318
|
+
*/
|
|
319
|
+
declare function findRoute(pathname: string, routes: RouteDefinition[]): RouteMatch | null;
|
|
320
|
+
/**
|
|
321
|
+
* Generate a URL from a route pattern and params
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* generatePath('/docs/:slug', { slug: 'routing' });
|
|
326
|
+
* // Returns: '/docs/routing'
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
declare function generatePath(pattern: string, params?: RouteParams): string;
|
|
330
|
+
/**
|
|
331
|
+
* Check if current path matches a pattern
|
|
332
|
+
*/
|
|
333
|
+
declare function isActive(pattern: string): boolean;
|
|
334
|
+
/**
|
|
335
|
+
* Redirect to a new URL (full page navigation)
|
|
336
|
+
* Use this for external redirects or when you need to break out of SPA
|
|
337
|
+
*/
|
|
338
|
+
declare function redirect(url: string): never;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @flight-framework/router - Prefetch Utilities
|
|
342
|
+
*
|
|
343
|
+
* Universal prefetch system for Flight Framework.
|
|
344
|
+
* Works across all UI frameworks and runtimes.
|
|
345
|
+
*
|
|
346
|
+
* Features:
|
|
347
|
+
* - Multiple prefetch strategies (intent, render, viewport)
|
|
348
|
+
* - Priority-based prefetching
|
|
349
|
+
* - Module and data prefetching
|
|
350
|
+
* - IntersectionObserver for viewport detection
|
|
351
|
+
* - SSR-safe implementation
|
|
352
|
+
*/
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Prefetch a URL with the specified options.
|
|
356
|
+
*
|
|
357
|
+
* This is the main programmatic prefetch API for Flight Framework.
|
|
358
|
+
* It creates appropriate link elements for prefetching resources.
|
|
359
|
+
*
|
|
360
|
+
* @param href - The URL to prefetch
|
|
361
|
+
* @param options - Prefetch configuration options
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```typescript
|
|
365
|
+
* // Basic prefetch
|
|
366
|
+
* prefetch('/docs');
|
|
367
|
+
*
|
|
368
|
+
* // High priority prefetch (for critical navigation paths)
|
|
369
|
+
* prefetch('/checkout', { priority: 'high' });
|
|
370
|
+
*
|
|
371
|
+
* // Prefetch with data loaders
|
|
372
|
+
* prefetch('/products', { includeData: true });
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
declare function prefetch(href: string, options?: PrefetchOptions): void;
|
|
376
|
+
/**
|
|
377
|
+
* Prefetch multiple URLs at once.
|
|
378
|
+
*
|
|
379
|
+
* @param hrefs - Array of URLs to prefetch
|
|
380
|
+
* @param options - Prefetch configuration options
|
|
381
|
+
*/
|
|
382
|
+
declare function prefetchAll(hrefs: string[], options?: PrefetchOptions): void;
|
|
383
|
+
/**
|
|
384
|
+
* Check if a URL has been prefetched.
|
|
385
|
+
*
|
|
386
|
+
* @param href - The URL to check
|
|
387
|
+
* @returns True if the URL has been prefetched
|
|
388
|
+
*/
|
|
389
|
+
declare function isPrefetched(href: string): boolean;
|
|
390
|
+
/**
|
|
391
|
+
* Clear all prefetch state (useful for testing or memory management).
|
|
392
|
+
*/
|
|
393
|
+
declare function clearPrefetchCache(): void;
|
|
394
|
+
/**
|
|
395
|
+
* Observe an element for viewport entry and trigger prefetch.
|
|
396
|
+
*
|
|
397
|
+
* @param element - The link element to observe
|
|
398
|
+
* @param href - The URL to prefetch when visible
|
|
399
|
+
* @returns Cleanup function to stop observing
|
|
400
|
+
*/
|
|
401
|
+
declare function observeForPrefetch(element: Element, href: string): () => void;
|
|
402
|
+
/**
|
|
403
|
+
* Setup intent-based prefetching for an element.
|
|
404
|
+
* Prefetches on mouseenter or focus.
|
|
405
|
+
*
|
|
406
|
+
* @param element - The link element
|
|
407
|
+
* @param href - The URL to prefetch
|
|
408
|
+
* @returns Cleanup function to remove listeners
|
|
409
|
+
*/
|
|
410
|
+
declare function setupIntentPrefetch(element: HTMLElement, href: string): () => void;
|
|
160
411
|
|
|
161
|
-
export { Link, type LinkProps, type NavigateOptions, type RouteParams, RouterContext, type RouterContextValue, RouterProvider, type RouterProviderProps, type SearchParams, matchRoute, navigate, parseParams, prefetch, useParams, usePathname, useRouter, useSearchParams };
|
|
412
|
+
export { Link, type LinkProps, type LoaderContext, type LoaderFunction, type NavigateOptions, type PrefetchOptions, PrefetchPageLinks, type PrefetchPriority, type PrefetchStrategy, type RouteDefinition, type RouteMatch, type RouteParams, RouterContext, type RouterContextValue, RouterProvider, type RouterProviderProps, type SearchParams, clearPrefetchCache, createLink, findRoute, generatePath, isActive, isPrefetched, matchRoute, navigate, observeForPrefetch, parseParams, prefetch, prefetchAll, prefetchPages, prefetchRoute, prefetchWhenIdle, redirect, setupIntentPrefetch, useLinkProps, useParams, usePathname, useRouter, useSearchParams };
|
package/dist/index.js
CHANGED
|
@@ -104,26 +104,192 @@ if (typeof globalThis !== "undefined") {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// src/
|
|
107
|
+
// src/prefetch.ts
|
|
108
108
|
var isBrowser2 = typeof window !== "undefined";
|
|
109
|
+
var supportsIntersectionObserver = isBrowser2 && "IntersectionObserver" in window;
|
|
110
|
+
var prefetchedUrls = /* @__PURE__ */ new Set();
|
|
111
|
+
var prefetchingUrls = /* @__PURE__ */ new Set();
|
|
112
|
+
var viewportObservers = /* @__PURE__ */ new Map();
|
|
113
|
+
function prefetch(href, options = {}) {
|
|
114
|
+
if (!isBrowser2) return;
|
|
115
|
+
const {
|
|
116
|
+
priority = "auto",
|
|
117
|
+
includeModules = true,
|
|
118
|
+
includeData = false
|
|
119
|
+
} = options;
|
|
120
|
+
const url = normalizeUrl(href);
|
|
121
|
+
if (prefetchedUrls.has(url) || prefetchingUrls.has(url)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
prefetchingUrls.add(url);
|
|
125
|
+
createPrefetchLink(url, "document", priority);
|
|
126
|
+
if (includeModules) {
|
|
127
|
+
prefetchModules(url, priority);
|
|
128
|
+
}
|
|
129
|
+
if (includeData) {
|
|
130
|
+
prefetchData(url, priority);
|
|
131
|
+
}
|
|
132
|
+
prefetchedUrls.add(url);
|
|
133
|
+
prefetchingUrls.delete(url);
|
|
134
|
+
}
|
|
135
|
+
function prefetchAll(hrefs, options = {}) {
|
|
136
|
+
for (const href of hrefs) {
|
|
137
|
+
prefetch(href, options);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function isPrefetched(href) {
|
|
141
|
+
return prefetchedUrls.has(normalizeUrl(href));
|
|
142
|
+
}
|
|
143
|
+
function clearPrefetchCache() {
|
|
144
|
+
prefetchedUrls.clear();
|
|
145
|
+
prefetchingUrls.clear();
|
|
146
|
+
}
|
|
147
|
+
function createPrefetchLink(href, as, priority) {
|
|
148
|
+
if (!isBrowser2) return null;
|
|
149
|
+
const existing = document.querySelector(
|
|
150
|
+
`link[rel="prefetch"][href="${href}"], link[rel="modulepreload"][href="${href}"]`
|
|
151
|
+
);
|
|
152
|
+
if (existing) return existing;
|
|
153
|
+
const link = document.createElement("link");
|
|
154
|
+
if (as === "script") {
|
|
155
|
+
link.rel = "modulepreload";
|
|
156
|
+
} else {
|
|
157
|
+
link.rel = "prefetch";
|
|
158
|
+
link.as = as;
|
|
159
|
+
}
|
|
160
|
+
link.href = href;
|
|
161
|
+
if (priority !== "auto" && "fetchPriority" in link) {
|
|
162
|
+
link.fetchPriority = priority;
|
|
163
|
+
}
|
|
164
|
+
if (priority === "low" && "requestIdleCallback" in window) {
|
|
165
|
+
window.requestIdleCallback(() => {
|
|
166
|
+
document.head.appendChild(link);
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
document.head.appendChild(link);
|
|
170
|
+
}
|
|
171
|
+
return link;
|
|
172
|
+
}
|
|
173
|
+
function prefetchModules(href, priority) {
|
|
174
|
+
const manifest = window.__FLIGHT_MANIFEST__;
|
|
175
|
+
if (!manifest?.routes) return;
|
|
176
|
+
const routeModules = manifest.routes[href];
|
|
177
|
+
if (!routeModules) return;
|
|
178
|
+
for (const module of routeModules) {
|
|
179
|
+
createPrefetchLink(module, "script", priority);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function prefetchData(href, priority) {
|
|
183
|
+
const dataUrl = `/_flight/data${href === "/" ? "/index" : href}.json`;
|
|
184
|
+
createPrefetchLink(dataUrl, "fetch", priority);
|
|
185
|
+
}
|
|
186
|
+
var sharedObserver = null;
|
|
187
|
+
var observerCallbacks = /* @__PURE__ */ new Map();
|
|
188
|
+
function getViewportObserver() {
|
|
189
|
+
if (!supportsIntersectionObserver) return null;
|
|
190
|
+
if (!sharedObserver) {
|
|
191
|
+
sharedObserver = new IntersectionObserver(
|
|
192
|
+
(entries) => {
|
|
193
|
+
for (const entry of entries) {
|
|
194
|
+
if (entry.isIntersecting) {
|
|
195
|
+
const callback = observerCallbacks.get(entry.target);
|
|
196
|
+
if (callback) {
|
|
197
|
+
callback();
|
|
198
|
+
sharedObserver?.unobserve(entry.target);
|
|
199
|
+
observerCallbacks.delete(entry.target);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
// Start prefetching when link is 25% visible or within 100px of viewport
|
|
206
|
+
rootMargin: "100px",
|
|
207
|
+
threshold: 0.25
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return sharedObserver;
|
|
212
|
+
}
|
|
213
|
+
function observeForPrefetch(element, href) {
|
|
214
|
+
if (!supportsIntersectionObserver) {
|
|
215
|
+
return () => {
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const observer = getViewportObserver();
|
|
219
|
+
if (!observer) return () => {
|
|
220
|
+
};
|
|
221
|
+
const callback = () => {
|
|
222
|
+
prefetch(href, { priority: "low" });
|
|
223
|
+
};
|
|
224
|
+
observerCallbacks.set(element, callback);
|
|
225
|
+
observer.observe(element);
|
|
226
|
+
const cleanup = () => {
|
|
227
|
+
observer.unobserve(element);
|
|
228
|
+
observerCallbacks.delete(element);
|
|
229
|
+
viewportObservers.delete(element);
|
|
230
|
+
};
|
|
231
|
+
viewportObservers.set(element, cleanup);
|
|
232
|
+
return cleanup;
|
|
233
|
+
}
|
|
234
|
+
function setupIntentPrefetch(element, href) {
|
|
235
|
+
if (!isBrowser2) return () => {
|
|
236
|
+
};
|
|
237
|
+
let prefetchTriggered = false;
|
|
238
|
+
const handleIntent = () => {
|
|
239
|
+
if (!prefetchTriggered) {
|
|
240
|
+
prefetchTriggered = true;
|
|
241
|
+
prefetch(href, { priority: "auto" });
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
element.addEventListener("mouseenter", handleIntent, { passive: true });
|
|
245
|
+
element.addEventListener("focus", handleIntent, { passive: true });
|
|
246
|
+
element.addEventListener("touchstart", handleIntent, { passive: true });
|
|
247
|
+
return () => {
|
|
248
|
+
element.removeEventListener("mouseenter", handleIntent);
|
|
249
|
+
element.removeEventListener("focus", handleIntent);
|
|
250
|
+
element.removeEventListener("touchstart", handleIntent);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function normalizeUrl(href) {
|
|
254
|
+
if (isBrowser2 && !href.startsWith("http")) {
|
|
255
|
+
try {
|
|
256
|
+
const url = new URL(href, window.location.origin);
|
|
257
|
+
return url.pathname + url.search;
|
|
258
|
+
} catch {
|
|
259
|
+
return href;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return href;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/link.ts
|
|
266
|
+
var isBrowser3 = typeof window !== "undefined";
|
|
109
267
|
function handleLinkClick(href, options, event) {
|
|
110
268
|
if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
|
|
111
269
|
return;
|
|
112
270
|
}
|
|
271
|
+
if (event && event.button !== 0) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
113
274
|
event?.preventDefault();
|
|
114
275
|
const { navigate: navigate2 } = getRouterContext();
|
|
115
276
|
navigate2(href, options);
|
|
116
277
|
}
|
|
117
278
|
function isExternalUrl(href) {
|
|
118
279
|
if (!href) return false;
|
|
119
|
-
return href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:");
|
|
280
|
+
return href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("javascript:") || href.startsWith("#");
|
|
281
|
+
}
|
|
282
|
+
function normalizePrefetchStrategy(prefetchProp) {
|
|
283
|
+
if (prefetchProp === true) {
|
|
284
|
+
return "intent";
|
|
285
|
+
}
|
|
286
|
+
if (prefetchProp === false || prefetchProp === void 0) {
|
|
287
|
+
return "none";
|
|
288
|
+
}
|
|
289
|
+
return prefetchProp;
|
|
120
290
|
}
|
|
121
291
|
function prefetchRoute(href) {
|
|
122
|
-
|
|
123
|
-
const link = document.createElement("link");
|
|
124
|
-
link.rel = "prefetch";
|
|
125
|
-
link.href = href;
|
|
126
|
-
document.head.appendChild(link);
|
|
292
|
+
prefetch(href);
|
|
127
293
|
}
|
|
128
294
|
var Link = null;
|
|
129
295
|
if (typeof globalThis !== "undefined") {
|
|
@@ -137,58 +303,196 @@ if (typeof globalThis !== "undefined") {
|
|
|
137
303
|
className,
|
|
138
304
|
target,
|
|
139
305
|
rel,
|
|
140
|
-
prefetch:
|
|
306
|
+
prefetch: prefetchProp = "none",
|
|
141
307
|
replace = false,
|
|
142
308
|
scroll = true,
|
|
143
309
|
onClick,
|
|
310
|
+
"aria-label": ariaLabel,
|
|
144
311
|
...props
|
|
145
312
|
}) {
|
|
146
313
|
const linkRef = useRef(null);
|
|
147
314
|
const isExternal = isExternalUrl(href);
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
315
|
+
const prefetchStrategy = normalizePrefetchStrategy(prefetchProp);
|
|
316
|
+
const handleClick = useCallback(
|
|
317
|
+
(e) => {
|
|
318
|
+
if (onClick) {
|
|
319
|
+
onClick(e);
|
|
320
|
+
if (e.defaultPrevented) return;
|
|
321
|
+
}
|
|
322
|
+
if (isExternal || target === "_blank") return;
|
|
323
|
+
handleLinkClick(href, { replace, scroll }, e);
|
|
324
|
+
},
|
|
325
|
+
[href, isExternal, target, replace, scroll, onClick]
|
|
326
|
+
);
|
|
156
327
|
useEffect(() => {
|
|
157
|
-
if (
|
|
328
|
+
if (isExternal || !isBrowser3 || prefetchStrategy === "none") {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
158
331
|
const link = linkRef.current;
|
|
159
332
|
if (!link) return;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
link.removeEventListener("focus", doPrefetch);
|
|
172
|
-
};
|
|
173
|
-
}, [href, prefetch2, isExternal]);
|
|
333
|
+
switch (prefetchStrategy) {
|
|
334
|
+
case "render":
|
|
335
|
+
prefetch(href, { priority: "low" });
|
|
336
|
+
break;
|
|
337
|
+
case "viewport":
|
|
338
|
+
return observeForPrefetch(link, href);
|
|
339
|
+
case "intent":
|
|
340
|
+
default:
|
|
341
|
+
return setupIntentPrefetch(link, href);
|
|
342
|
+
}
|
|
343
|
+
}, [href, prefetchStrategy, isExternal]);
|
|
174
344
|
const computedRel = isExternal && target === "_blank" ? rel || "noopener noreferrer" : rel;
|
|
175
|
-
return React.createElement(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
345
|
+
return React.createElement(
|
|
346
|
+
"a",
|
|
347
|
+
{
|
|
348
|
+
ref: linkRef,
|
|
349
|
+
href,
|
|
350
|
+
className,
|
|
351
|
+
target,
|
|
352
|
+
rel: computedRel,
|
|
353
|
+
"aria-label": ariaLabel,
|
|
354
|
+
onClick: handleClick,
|
|
355
|
+
...props
|
|
356
|
+
},
|
|
357
|
+
children
|
|
358
|
+
);
|
|
184
359
|
};
|
|
185
360
|
}
|
|
186
361
|
} catch {
|
|
187
362
|
}
|
|
188
363
|
}
|
|
364
|
+
function createLink(props) {
|
|
365
|
+
const {
|
|
366
|
+
href,
|
|
367
|
+
children,
|
|
368
|
+
className,
|
|
369
|
+
target,
|
|
370
|
+
rel,
|
|
371
|
+
prefetch: prefetchProp = "none",
|
|
372
|
+
replace = false,
|
|
373
|
+
scroll = true
|
|
374
|
+
} = props;
|
|
375
|
+
const anchor = document.createElement("a");
|
|
376
|
+
anchor.href = href;
|
|
377
|
+
anchor.className = className || "";
|
|
378
|
+
if (target) anchor.target = target;
|
|
379
|
+
if (rel) anchor.rel = rel;
|
|
380
|
+
if (typeof children === "string") {
|
|
381
|
+
anchor.textContent = children;
|
|
382
|
+
}
|
|
383
|
+
const isExternal = isExternalUrl(href);
|
|
384
|
+
const prefetchStrategy = normalizePrefetchStrategy(prefetchProp);
|
|
385
|
+
if (!isExternal && target !== "_blank") {
|
|
386
|
+
anchor.addEventListener("click", (e) => {
|
|
387
|
+
handleLinkClick(href, { replace, scroll }, e);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (!isExternal && prefetchStrategy !== "none") {
|
|
391
|
+
switch (prefetchStrategy) {
|
|
392
|
+
case "render":
|
|
393
|
+
prefetch(href, { priority: "low" });
|
|
394
|
+
break;
|
|
395
|
+
case "viewport":
|
|
396
|
+
observeForPrefetch(anchor, href);
|
|
397
|
+
break;
|
|
398
|
+
case "intent":
|
|
399
|
+
default:
|
|
400
|
+
setupIntentPrefetch(anchor, href);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return anchor;
|
|
405
|
+
}
|
|
406
|
+
function useLinkProps(href, options = {}) {
|
|
407
|
+
const { replace = false, scroll = true, prefetch: prefetchProp = "none" } = options;
|
|
408
|
+
const isExternal = isExternalUrl(href);
|
|
409
|
+
const prefetchStrategy = normalizePrefetchStrategy(prefetchProp);
|
|
410
|
+
const result = {
|
|
411
|
+
href,
|
|
412
|
+
onClick: (e) => {
|
|
413
|
+
if (!isExternal) {
|
|
414
|
+
handleLinkClick(href, { replace, scroll }, e);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
if (prefetchStrategy === "intent" && !isExternal) {
|
|
419
|
+
let prefetched = false;
|
|
420
|
+
const doPrefetch = () => {
|
|
421
|
+
if (!prefetched) {
|
|
422
|
+
prefetched = true;
|
|
423
|
+
prefetch(href);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
result.onMouseenter = doPrefetch;
|
|
427
|
+
result.onFocus = doPrefetch;
|
|
428
|
+
}
|
|
429
|
+
if (prefetchStrategy === "render" && !isExternal && isBrowser3) {
|
|
430
|
+
prefetch(href, { priority: "low" });
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/prefetch-links.ts
|
|
436
|
+
var isBrowser4 = typeof window !== "undefined";
|
|
437
|
+
var PrefetchPageLinks = null;
|
|
438
|
+
if (typeof globalThis !== "undefined") {
|
|
439
|
+
try {
|
|
440
|
+
const React = globalThis.React;
|
|
441
|
+
if (React?.createElement && "useEffect" in React) {
|
|
442
|
+
const { useEffect, useState } = React;
|
|
443
|
+
PrefetchPageLinks = function FlightPrefetchPageLinks({
|
|
444
|
+
page,
|
|
445
|
+
options = {}
|
|
446
|
+
}) {
|
|
447
|
+
const [shouldRender, setShouldRender] = useState(false);
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (!isBrowser4) return;
|
|
450
|
+
if (isPrefetched(page)) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
prefetch(page, {
|
|
454
|
+
priority: "low",
|
|
455
|
+
includeModules: true,
|
|
456
|
+
...options
|
|
457
|
+
});
|
|
458
|
+
setShouldRender(false);
|
|
459
|
+
}, [page, options]);
|
|
460
|
+
return null;
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function prefetchPages(pages, options = {}) {
|
|
467
|
+
if (!isBrowser4) return;
|
|
468
|
+
for (const page of pages) {
|
|
469
|
+
if (!isPrefetched(page)) {
|
|
470
|
+
prefetch(page, {
|
|
471
|
+
priority: "low",
|
|
472
|
+
...options
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function prefetchWhenIdle(page, options = {}) {
|
|
478
|
+
if (!isBrowser4) return;
|
|
479
|
+
const doPrefetch = () => {
|
|
480
|
+
if (!isPrefetched(page)) {
|
|
481
|
+
prefetch(page, {
|
|
482
|
+
priority: "low",
|
|
483
|
+
...options
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
if ("requestIdleCallback" in window) {
|
|
488
|
+
window.requestIdleCallback(doPrefetch, { timeout: 3e3 });
|
|
489
|
+
} else {
|
|
490
|
+
setTimeout(doPrefetch, 100);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
189
493
|
|
|
190
494
|
// src/hooks.ts
|
|
191
|
-
var
|
|
495
|
+
var isBrowser5 = typeof window !== "undefined";
|
|
192
496
|
var useParams = () => ({});
|
|
193
497
|
var useSearchParams = () => [new URLSearchParams(), () => {
|
|
194
498
|
}];
|
|
@@ -210,10 +514,10 @@ if (typeof globalThis !== "undefined") {
|
|
|
210
514
|
};
|
|
211
515
|
useSearchParams = function useFlightSearchParams() {
|
|
212
516
|
const [searchParams, setSearchParamsState] = useState(
|
|
213
|
-
() =>
|
|
517
|
+
() => isBrowser5 ? new URLSearchParams(window.location.search) : new URLSearchParams()
|
|
214
518
|
);
|
|
215
519
|
useEffect(() => {
|
|
216
|
-
if (!
|
|
520
|
+
if (!isBrowser5) return;
|
|
217
521
|
const handleChange = () => {
|
|
218
522
|
setSearchParamsState(new URLSearchParams(window.location.search));
|
|
219
523
|
};
|
|
@@ -221,7 +525,7 @@ if (typeof globalThis !== "undefined") {
|
|
|
221
525
|
return () => window.removeEventListener("popstate", handleChange);
|
|
222
526
|
}, []);
|
|
223
527
|
const setSearchParams = useCallback((newParams) => {
|
|
224
|
-
if (!
|
|
528
|
+
if (!isBrowser5) return;
|
|
225
529
|
let params;
|
|
226
530
|
if (newParams instanceof URLSearchParams) {
|
|
227
531
|
params = newParams;
|
|
@@ -245,7 +549,7 @@ if (typeof globalThis !== "undefined") {
|
|
|
245
549
|
const { path } = getRouterContext();
|
|
246
550
|
const [pathname, setPathname] = useState(path);
|
|
247
551
|
useEffect(() => {
|
|
248
|
-
if (!
|
|
552
|
+
if (!isBrowser5) return;
|
|
249
553
|
const handleChange = () => {
|
|
250
554
|
setPathname(window.location.pathname);
|
|
251
555
|
};
|
|
@@ -260,20 +564,11 @@ if (typeof globalThis !== "undefined") {
|
|
|
260
564
|
}
|
|
261
565
|
|
|
262
566
|
// src/navigate.ts
|
|
263
|
-
var
|
|
567
|
+
var isBrowser6 = typeof window !== "undefined";
|
|
264
568
|
function navigate(to, options = {}) {
|
|
265
569
|
const { navigate: routerNavigate } = getRouterContext();
|
|
266
570
|
routerNavigate(to, options);
|
|
267
571
|
}
|
|
268
|
-
function prefetch(href) {
|
|
269
|
-
if (!isBrowser4) return;
|
|
270
|
-
const existing = document.querySelector(`link[rel="prefetch"][href="${href}"]`);
|
|
271
|
-
if (existing) return;
|
|
272
|
-
const link = document.createElement("link");
|
|
273
|
-
link.rel = "prefetch";
|
|
274
|
-
link.href = href;
|
|
275
|
-
document.head.appendChild(link);
|
|
276
|
-
}
|
|
277
572
|
function patternToRegex(pattern) {
|
|
278
573
|
const paramNames = [];
|
|
279
574
|
let regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\[\.\.\.(\w+)\\\]/g, (_, name) => {
|
|
@@ -308,14 +603,63 @@ function parseParams(pathname, pattern) {
|
|
|
308
603
|
const { params } = matchRoute(pathname, pattern);
|
|
309
604
|
return params;
|
|
310
605
|
}
|
|
606
|
+
function findRoute(pathname, routes) {
|
|
607
|
+
for (const route of routes) {
|
|
608
|
+
const { matched, params } = matchRoute(pathname, route.path);
|
|
609
|
+
if (matched) {
|
|
610
|
+
return {
|
|
611
|
+
route,
|
|
612
|
+
params,
|
|
613
|
+
pathname
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
function generatePath(pattern, params = {}) {
|
|
620
|
+
let path = pattern;
|
|
621
|
+
path = path.replace(/\[(\w+)\]/g, (_, name) => {
|
|
622
|
+
return params[name] || "";
|
|
623
|
+
});
|
|
624
|
+
path = path.replace(/:(\w+)/g, (_, name) => {
|
|
625
|
+
return params[name] || "";
|
|
626
|
+
});
|
|
627
|
+
return path;
|
|
628
|
+
}
|
|
629
|
+
function isActive(pattern) {
|
|
630
|
+
const { path } = getRouterContext();
|
|
631
|
+
const { matched } = matchRoute(path, pattern);
|
|
632
|
+
return matched;
|
|
633
|
+
}
|
|
634
|
+
function redirect(url) {
|
|
635
|
+
if (isBrowser6) {
|
|
636
|
+
window.location.href = url;
|
|
637
|
+
}
|
|
638
|
+
throw new Error(`Redirect to: ${url}`);
|
|
639
|
+
}
|
|
311
640
|
export {
|
|
312
641
|
Link,
|
|
642
|
+
PrefetchPageLinks,
|
|
313
643
|
RouterContext,
|
|
314
644
|
RouterProvider,
|
|
645
|
+
clearPrefetchCache,
|
|
646
|
+
createLink,
|
|
647
|
+
findRoute,
|
|
648
|
+
generatePath,
|
|
649
|
+
isActive,
|
|
650
|
+
isPrefetched,
|
|
315
651
|
matchRoute,
|
|
316
652
|
navigate,
|
|
653
|
+
observeForPrefetch,
|
|
317
654
|
parseParams,
|
|
318
655
|
prefetch,
|
|
656
|
+
prefetchAll,
|
|
657
|
+
prefetchPages,
|
|
658
|
+
prefetchRoute,
|
|
659
|
+
prefetchWhenIdle,
|
|
660
|
+
redirect,
|
|
661
|
+
setupIntentPrefetch,
|
|
662
|
+
useLinkProps,
|
|
319
663
|
useParams,
|
|
320
664
|
usePathname,
|
|
321
665
|
useRouter,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flight-framework/router",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Agnostic client-side routing primitives for Flight Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,4 +45,4 @@
|
|
|
45
45
|
"optional": true
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|