@flight-framework/router 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,14 +2,40 @@
2
2
 
3
3
  Universal client-side routing primitives for Flight Framework. Zero external dependencies. Works with any UI framework.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Features](#features)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Prefetch Strategies](#prefetch-strategies)
11
+ - [Smart Prefetching](#smart-prefetching)
12
+ - [Intercepting Routes](#intercepting-routes)
13
+ - [Programmatic Prefetching](#programmatic-prefetching)
14
+ - [Prefetch Queue](#prefetch-queue)
15
+ - [React Hooks](#react-hooks)
16
+ - [Programmatic Navigation](#programmatic-navigation)
17
+ - [Route Matching](#route-matching)
18
+ - [Link Component](#link-component)
19
+ - [Framework Integration](#framework-integration)
20
+ - [SSR Support](#ssr-support)
21
+ - [Browser Support](#browser-support)
22
+ - [License](#license)
23
+
24
+ ---
25
+
5
26
  ## Features
6
27
 
7
- - Multiple prefetch strategies (none, intent, render, viewport)
28
+ - Multiple prefetch strategies (none, intent, render, viewport, idle)
29
+ - Network-aware smart prefetching (respects Save-Data and connection speed)
30
+ - Intercepting routes with modal support
31
+ - Prefetch queue with concurrency control
8
32
  - SSR-safe implementation
9
33
  - Framework-agnostic with React adapters
10
34
  - TypeScript-first design
11
35
  - IntersectionObserver for viewport prefetching
12
- - Backwards compatible API
36
+ - Zero external dependencies
37
+
38
+ ---
13
39
 
14
40
  ## Installation
15
41
 
@@ -17,6 +43,8 @@ Universal client-side routing primitives for Flight Framework. Zero external dep
17
43
  npm install @flight-framework/router
18
44
  ```
19
45
 
46
+ ---
47
+
20
48
  ## Quick Start
21
49
 
22
50
  ### With React
@@ -57,6 +85,8 @@ function Content() {
57
85
  }
58
86
  ```
59
87
 
88
+ ---
89
+
60
90
  ## Prefetch Strategies
61
91
 
62
92
  Flight Router supports multiple prefetch strategies to optimize navigation performance:
@@ -67,6 +97,7 @@ Flight Router supports multiple prefetch strategies to optimize navigation perfo
67
97
  | `'intent'` | Prefetch on hover/focus | Most navigation links |
68
98
  | `'render'` | Prefetch immediately | Critical paths (checkout) |
69
99
  | `'viewport'` | Prefetch when visible | Mobile, infinite scroll |
100
+ | `'idle'` | Prefetch when browser is idle | Background preloading |
70
101
 
71
102
  ### Usage Examples
72
103
 
@@ -84,8 +115,129 @@ Flight Router supports multiple prefetch strategies to optimize navigation perfo
84
115
 
85
116
  // Prefetch when link enters viewport (good for mobile)
86
117
  <Link href="/products" prefetch="viewport">Products</Link>
118
+
119
+ // Prefetch when browser is idle
120
+ <Link href="/settings" prefetch="idle">Settings</Link>
87
121
  ```
88
122
 
123
+ ---
124
+
125
+ ## Smart Prefetching
126
+
127
+ Network-aware prefetching that respects user preferences and connection quality.
128
+
129
+ ### Features
130
+
131
+ - Respects `Save-Data` header (users on limited data plans)
132
+ - Detects connection type (2G, 3G, 4G)
133
+ - Configurable minimum network requirements
134
+ - Automatic throttling on slow connections
135
+
136
+ ### API
137
+
138
+ ```typescript
139
+ import {
140
+ smartPrefetch,
141
+ shouldSkipPrefetch,
142
+ canPrefetchOnNetwork,
143
+ } from '@flight-framework/router';
144
+
145
+ // Respects network conditions automatically
146
+ smartPrefetch('/dashboard');
147
+
148
+ // Check if prefetching should be skipped
149
+ if (shouldSkipPrefetch()) {
150
+ console.log('Skipping due to network conditions');
151
+ }
152
+
153
+ // Custom network requirements
154
+ const canPrefetch = canPrefetchOnNetwork({
155
+ respectSaveData: true, // Skip if Save-Data enabled
156
+ respectSlowNetwork: true, // Skip on 2G/slow-2G
157
+ minEffectiveType: '3g', // Minimum 3G required
158
+ });
159
+ ```
160
+
161
+ ### Configuration
162
+
163
+ ```typescript
164
+ smartPrefetch('/page', {
165
+ respectSaveData: true, // Default: true
166
+ respectSlowNetwork: true, // Default: true
167
+ minEffectiveType: '3g', // Default: '3g'
168
+ priority: 'high', // Optional priority override
169
+ });
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Intercepting Routes
175
+
176
+ Render routes in modals while preserving URL state. Perfect for photo galleries, user profiles, and detail views.
177
+
178
+ ### React Integration
179
+
180
+ ```tsx
181
+ import {
182
+ InterceptedRouteProvider,
183
+ useInterceptedRoute,
184
+ } from '@flight-framework/router';
185
+
186
+ // Wrap your layout
187
+ function RootLayout({ children }) {
188
+ return (
189
+ <InterceptedRouteProvider>
190
+ {children}
191
+ <ModalRenderer />
192
+ </InterceptedRouteProvider>
193
+ );
194
+ }
195
+
196
+ // Render intercepted content in a modal
197
+ function ModalRenderer() {
198
+ const intercepted = useInterceptedRoute();
199
+
200
+ if (!intercepted) return null;
201
+
202
+ return (
203
+ <Dialog open onClose={intercepted.dismiss}>
204
+ <intercepted.Component />
205
+ <button onClick={intercepted.navigateToPage}>
206
+ View Full Page
207
+ </button>
208
+ </Dialog>
209
+ );
210
+ }
211
+ ```
212
+
213
+ ### Intercepted Route State
214
+
215
+ | Property | Type | Description |
216
+ |----------|------|-------------|
217
+ | `Component` | `() => unknown` | The component to render |
218
+ | `pathname` | `string` | The intercepted URL path |
219
+ | `params` | `Record<string, string>` | Route parameters |
220
+ | `dismiss()` | `() => void` | Go back (close modal) |
221
+ | `navigateToPage()` | `() => void` | Navigate to actual page |
222
+
223
+ ### Additional Hooks
224
+
225
+ ```typescript
226
+ import {
227
+ useInterceptedRoute,
228
+ useSetInterceptedRoute,
229
+ useIsIntercepting,
230
+ } from '@flight-framework/router';
231
+
232
+ // Check if currently intercepting
233
+ const isIntercepting = useIsIntercepting();
234
+
235
+ // Programmatically set intercepted state (for router integration)
236
+ const setIntercepted = useSetInterceptedRoute();
237
+ ```
238
+
239
+ ---
240
+
89
241
  ## Programmatic Prefetching
90
242
 
91
243
  ```typescript
@@ -94,6 +246,7 @@ import {
94
246
  prefetchAll,
95
247
  prefetchWhenIdle,
96
248
  isPrefetched,
249
+ clearPrefetchCache,
97
250
  } from '@flight-framework/router';
98
251
 
99
252
  // Basic prefetch
@@ -115,33 +268,50 @@ prefetchWhenIdle('/dashboard');
115
268
  if (!isPrefetched('/docs')) {
116
269
  prefetch('/docs');
117
270
  }
271
+
272
+ // Clear prefetch cache (useful for testing)
273
+ clearPrefetchCache();
118
274
  ```
119
275
 
120
- ## PrefetchPageLinks Component
276
+ ---
121
277
 
122
- For proactive prefetching based on user behavior (search results, autocomplete):
278
+ ## Prefetch Queue
123
279
 
124
- ```tsx
125
- import { PrefetchPageLinks } from '@flight-framework/router';
280
+ Control concurrent prefetch requests to avoid overwhelming the network:
126
281
 
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
- }
282
+ ```typescript
283
+ import { PrefetchQueue } from '@flight-framework/router';
284
+
285
+ // Create queue with max 3 concurrent prefetches
286
+ const queue = new PrefetchQueue(3);
287
+
288
+ // Add URLs to queue
289
+ queue.add('/page1');
290
+ queue.add('/page2', { priority: 'high' });
291
+ queue.addAll(['/page3', '/page4', '/page5']);
292
+
293
+ // Control queue
294
+ queue.pause(); // Stop processing
295
+ queue.resume(); // Continue processing
296
+ queue.clear(); // Remove pending items
297
+
298
+ // Check state
299
+ console.log(queue.pending); // Items waiting
300
+ console.log(queue.activeCount); // Currently prefetching
140
301
  ```
141
302
 
142
- ## Hooks
303
+ ---
304
+
305
+ ## React Hooks
143
306
 
144
307
  ```tsx
308
+ import {
309
+ useRouter,
310
+ useParams,
311
+ useSearchParams,
312
+ usePathname,
313
+ } from '@flight-framework/router';
314
+
145
315
  // Get current path and navigation functions
146
316
  const { path, searchParams, navigate, back, forward } = useRouter();
147
317
 
@@ -155,6 +325,8 @@ const [searchParams, setSearchParams] = useSearchParams();
155
325
  const pathname = usePathname();
156
326
  ```
157
327
 
328
+ ---
329
+
158
330
  ## Programmatic Navigation
159
331
 
160
332
  ```typescript
@@ -173,6 +345,8 @@ navigate('/next-page', { scroll: false });
173
345
  navigate('/dashboard', { state: { from: '/login' } });
174
346
  ```
175
347
 
348
+ ---
349
+
176
350
  ## Route Matching
177
351
 
178
352
  ```typescript
@@ -191,7 +365,11 @@ const path = generatePath('/docs/:slug', { slug: 'api-routes' });
191
365
  // '/docs/api-routes'
192
366
  ```
193
367
 
194
- ## Link Props
368
+ ---
369
+
370
+ ## Link Component
371
+
372
+ ### Props
195
373
 
196
374
  | Prop | Type | Default | Description |
197
375
  |------|------|---------|-------------|
@@ -203,7 +381,32 @@ const path = generatePath('/docs/:slug', { slug: 'api-routes' });
203
381
  | `className` | `string` | - | CSS class |
204
382
  | `aria-label` | `string` | - | Accessibility label |
205
383
 
206
- ## Vue Integration
384
+ ### PrefetchPageLinks Component
385
+
386
+ For proactive prefetching based on user behavior:
387
+
388
+ ```tsx
389
+ import { PrefetchPageLinks } from '@flight-framework/router';
390
+
391
+ function SearchResults({ results }) {
392
+ return (
393
+ <>
394
+ {results.map(result => (
395
+ <div key={result.id}>
396
+ <PrefetchPageLinks page={result.url} />
397
+ <Link href={result.url}>{result.title}</Link>
398
+ </div>
399
+ ))}
400
+ </>
401
+ );
402
+ }
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Framework Integration
408
+
409
+ ### Vue
207
410
 
208
411
  ```vue
209
412
  <script setup>
@@ -217,7 +420,7 @@ const docsLink = useLinkProps('/docs', { prefetch: 'intent' });
217
420
  </template>
218
421
  ```
219
422
 
220
- ## Vanilla JavaScript
423
+ ### Vanilla JavaScript
221
424
 
222
425
  ```typescript
223
426
  import { createLink, prefetch } from '@flight-framework/router';
@@ -231,6 +434,8 @@ const link = createLink({
231
434
  document.body.appendChild(link);
232
435
  ```
233
436
 
437
+ ---
438
+
234
439
  ## SSR Support
235
440
 
236
441
  The router is SSR-safe. Pass the initial path from your server:
@@ -246,19 +451,7 @@ export function render(url: string) {
246
451
  }
247
452
  ```
248
453
 
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
- ```
454
+ ---
262
455
 
263
456
  ## Browser Support
264
457
 
@@ -266,8 +459,30 @@ export default defineConfig({
266
459
  |---------|--------|---------|--------|------|
267
460
  | Prefetch | All | All | All | All |
268
461
  | Viewport (IntersectionObserver) | 51+ | 55+ | 12.1+ | 15+ |
462
+ | Network Information API | 61+ | No | No | 79+ |
463
+ | requestIdleCallback | 47+ | 55+ | No | 79+ |
269
464
  | fetchPriority | 101+ | No | No | 101+ |
270
465
 
466
+ Features gracefully degrade when APIs are unavailable.
467
+
468
+ ---
469
+
470
+ ## TypeScript
471
+
472
+ Full TypeScript support with exported types:
473
+
474
+ ```typescript
475
+ import type {
476
+ PrefetchStrategy,
477
+ PrefetchOptions,
478
+ SmartPrefetchOptions,
479
+ InterceptedRouteState,
480
+ InterceptedRouteContextValue,
481
+ } from '@flight-framework/router';
482
+ ```
483
+
484
+ ---
485
+
271
486
  ## License
272
487
 
273
488
  MIT
package/dist/index.d.ts CHANGED
@@ -266,9 +266,32 @@ declare function prefetchWhenIdle(page: string, options?: PrefetchOptions): void
266
266
  * Router Hooks
267
267
  *
268
268
  * React hooks for accessing router state.
269
- * These require React to be available.
269
+ * Uses useSyncExternalStore for concurrent-safe external state (2026 best practice).
270
270
  */
271
271
 
272
+ /**
273
+ * Subscribe to pathname changes - for useSyncExternalStore
274
+ */
275
+ declare function subscribeToPathname(callback: () => void): () => void;
276
+ /**
277
+ * Get current pathname snapshot - for useSyncExternalStore
278
+ */
279
+ declare function getPathnameSnapshot(): string;
280
+ /**
281
+ * Get server snapshot - for useSyncExternalStore SSR
282
+ */
283
+ declare function getPathnameServerSnapshot(): string;
284
+ /**
285
+ * Create usePathname hook with provided React instance
286
+ * This pattern ensures proper bundler support for React hooks
287
+ *
288
+ * @example
289
+ * import { useSyncExternalStore } from 'react';
290
+ * import { createUsePathname } from '@flight-framework/router';
291
+ *
292
+ * const usePathname = createUsePathname(useSyncExternalStore);
293
+ */
294
+ declare function createUsePathname(useSyncExternalStore: <T>(subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T) => T): () => string;
272
295
  declare let useParams: <T extends RouteParams = RouteParams>() => T;
273
296
  declare let useSearchParams: () => [URLSearchParams, (params: SearchParams | URLSearchParams) => void];
274
297
  declare let usePathname: () => string;
@@ -409,4 +432,4 @@ declare function observeForPrefetch(element: Element, href: string): () => void;
409
432
  */
410
433
  declare function setupIntentPrefetch(element: HTMLElement, href: string): () => void;
411
434
 
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 };
435
+ 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, createUsePathname, findRoute, generatePath, getPathnameServerSnapshot, getPathnameSnapshot, isActive, isPrefetched, matchRoute, navigate, observeForPrefetch, parseParams, prefetch, prefetchAll, prefetchPages, prefetchRoute, prefetchWhenIdle, redirect, setupIntentPrefetch, subscribeToPathname, useLinkProps, useParams, usePathname, useRouter, useSearchParams };
package/dist/index.js CHANGED
@@ -39,6 +39,63 @@ function navigateTo(to, options = {}) {
39
39
  window.scrollTo({ top: 0, left: 0, behavior: "instant" });
40
40
  }
41
41
  }
42
+ function initRouter(options = {}) {
43
+ const { initialPath, basePath = "" } = options;
44
+ let path;
45
+ let searchParams;
46
+ if (isBrowser) {
47
+ path = window.location.pathname;
48
+ searchParams = new URLSearchParams(window.location.search);
49
+ } else {
50
+ path = initialPath || "/";
51
+ searchParams = new URLSearchParams();
52
+ }
53
+ if (basePath && path.startsWith(basePath)) {
54
+ path = path.slice(basePath.length) || "/";
55
+ }
56
+ currentContext = {
57
+ path,
58
+ searchParams,
59
+ navigate: navigateTo,
60
+ back: () => isBrowser && window.history.back(),
61
+ forward: () => isBrowser && window.history.forward()
62
+ };
63
+ if (isBrowser) {
64
+ window.addEventListener("popstate", () => {
65
+ updateContext({
66
+ path: window.location.pathname,
67
+ searchParams: new URLSearchParams(window.location.search)
68
+ });
69
+ });
70
+ const originalPushState = history.pushState.bind(history);
71
+ const originalReplaceState = history.replaceState.bind(history);
72
+ history.pushState = function(state, unused, url) {
73
+ originalPushState(state, unused, url);
74
+ if (url) {
75
+ const newUrl = new URL(url.toString(), window.location.origin);
76
+ updateContext({
77
+ path: newUrl.pathname,
78
+ searchParams: newUrl.searchParams
79
+ });
80
+ }
81
+ };
82
+ history.replaceState = function(state, unused, url) {
83
+ originalReplaceState(state, unused, url);
84
+ if (url) {
85
+ const newUrl = new URL(url.toString(), window.location.origin);
86
+ updateContext({
87
+ path: newUrl.pathname,
88
+ searchParams: newUrl.searchParams
89
+ });
90
+ }
91
+ };
92
+ }
93
+ }
94
+ var initialized = false;
95
+ if (isBrowser && !initialized) {
96
+ initialized = true;
97
+ initRouter();
98
+ }
42
99
  var RouterContext = null;
43
100
  var RouterProvider = null;
44
101
  var useRouter = getRouterContext;
@@ -493,15 +550,65 @@ function prefetchWhenIdle(page, options = {}) {
493
550
 
494
551
  // src/hooks.ts
495
552
  var isBrowser5 = typeof window !== "undefined";
553
+ var pathSubscribers = /* @__PURE__ */ new Set();
554
+ function notifyPathChange() {
555
+ pathSubscribers.forEach((fn) => fn());
556
+ }
557
+ var historyIntercepted = false;
558
+ function interceptHistory() {
559
+ if (!isBrowser5 || historyIntercepted) return;
560
+ historyIntercepted = true;
561
+ const originalPushState = history.pushState.bind(history);
562
+ const originalReplaceState = history.replaceState.bind(history);
563
+ history.pushState = function(state, unused, url) {
564
+ originalPushState(state, unused, url);
565
+ notifyPathChange();
566
+ };
567
+ history.replaceState = function(state, unused, url) {
568
+ originalReplaceState(state, unused, url);
569
+ notifyPathChange();
570
+ };
571
+ window.addEventListener("popstate", notifyPathChange);
572
+ }
573
+ if (isBrowser5) {
574
+ interceptHistory();
575
+ }
576
+ function subscribeToPathname(callback) {
577
+ pathSubscribers.add(callback);
578
+ return () => pathSubscribers.delete(callback);
579
+ }
580
+ function getPathnameSnapshot() {
581
+ return isBrowser5 ? window.location.pathname : "/";
582
+ }
583
+ function getPathnameServerSnapshot() {
584
+ return "/";
585
+ }
586
+ function createUsePathname(useSyncExternalStore) {
587
+ return function usePathname2() {
588
+ return useSyncExternalStore(
589
+ subscribeToPathname,
590
+ getPathnameSnapshot,
591
+ getPathnameServerSnapshot
592
+ );
593
+ };
594
+ }
496
595
  var useParams = () => ({});
497
596
  var useSearchParams = () => [new URLSearchParams(), () => {
498
597
  }];
499
- var usePathname = () => "/";
598
+ var usePathname = () => {
599
+ if (isBrowser5) {
600
+ return window.location.pathname;
601
+ }
602
+ return "/";
603
+ };
500
604
  if (typeof globalThis !== "undefined") {
501
605
  try {
502
606
  const React = globalThis.React;
607
+ if (React?.useSyncExternalStore) {
608
+ usePathname = createUsePathname(React.useSyncExternalStore);
609
+ }
503
610
  if (React?.useState) {
504
- const { useState, useEffect, useCallback, useMemo } = React;
611
+ const { useState, useEffect, useCallback } = React;
505
612
  useParams = function useFlightParams() {
506
613
  const [params, setParams] = useState({});
507
614
  useEffect(() => {
@@ -545,19 +652,6 @@ if (typeof globalThis !== "undefined") {
545
652
  }, []);
546
653
  return [searchParams, setSearchParams];
547
654
  };
548
- usePathname = function useFlightPathname() {
549
- const { path } = getRouterContext();
550
- const [pathname, setPathname] = useState(path);
551
- useEffect(() => {
552
- if (!isBrowser5) return;
553
- const handleChange = () => {
554
- setPathname(window.location.pathname);
555
- };
556
- window.addEventListener("popstate", handleChange);
557
- return () => window.removeEventListener("popstate", handleChange);
558
- }, []);
559
- return pathname;
560
- };
561
655
  }
562
656
  } catch {
563
657
  }
@@ -644,8 +738,11 @@ export {
644
738
  RouterProvider,
645
739
  clearPrefetchCache,
646
740
  createLink,
741
+ createUsePathname,
647
742
  findRoute,
648
743
  generatePath,
744
+ getPathnameServerSnapshot,
745
+ getPathnameSnapshot,
649
746
  isActive,
650
747
  isPrefetched,
651
748
  matchRoute,
@@ -659,6 +756,7 @@ export {
659
756
  prefetchWhenIdle,
660
757
  redirect,
661
758
  setupIntentPrefetch,
759
+ subscribeToPathname,
662
760
  useLinkProps,
663
761
  useParams,
664
762
  usePathname,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flight-framework/router",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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
+ }