@flight-framework/router 0.0.1
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 +144 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.js +323 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @flight-framework/router
|
|
2
|
+
|
|
3
|
+
Agnostic client-side routing primitives for Flight Framework.
|
|
4
|
+
|
|
5
|
+
## Philosophy
|
|
6
|
+
|
|
7
|
+
- Zero external runtime dependencies
|
|
8
|
+
- Works with React, Vue, Svelte, or vanilla JS
|
|
9
|
+
- SSR-safe (works on server and client)
|
|
10
|
+
- No vendor lock-in
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @flight-framework/router
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### With React
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { RouterProvider, Link, useRouter } from '@flight-framework/router';
|
|
24
|
+
|
|
25
|
+
// Wrap your app
|
|
26
|
+
function App() {
|
|
27
|
+
return (
|
|
28
|
+
<RouterProvider initialPath={url}>
|
|
29
|
+
<Navigation />
|
|
30
|
+
<Content />
|
|
31
|
+
</RouterProvider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Use Link for navigation
|
|
36
|
+
function Navigation() {
|
|
37
|
+
return (
|
|
38
|
+
<nav>
|
|
39
|
+
<Link href="/">Home</Link>
|
|
40
|
+
<Link href="/docs" prefetch>Docs</Link>
|
|
41
|
+
<Link href="/about">About</Link>
|
|
42
|
+
</nav>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Use hooks to access router state
|
|
47
|
+
function Content() {
|
|
48
|
+
const { path, navigate } = useRouter();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<main>
|
|
52
|
+
<p>Current path: {path}</p>
|
|
53
|
+
<button onClick={() => navigate('/dashboard')}>
|
|
54
|
+
Go to Dashboard
|
|
55
|
+
</button>
|
|
56
|
+
</main>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Hooks
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// Get current path and navigation functions
|
|
65
|
+
const { path, searchParams, navigate, back, forward } = useRouter();
|
|
66
|
+
|
|
67
|
+
// Get route parameters (from dynamic routes like [slug])
|
|
68
|
+
const { slug } = useParams<{ slug: string }>();
|
|
69
|
+
|
|
70
|
+
// Get and set search params
|
|
71
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
72
|
+
|
|
73
|
+
// Get current pathname
|
|
74
|
+
const pathname = usePathname();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Programmatic Navigation
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { navigate, prefetch } from '@flight-framework/router';
|
|
81
|
+
|
|
82
|
+
// Navigate to a path
|
|
83
|
+
navigate('/docs');
|
|
84
|
+
|
|
85
|
+
// Replace history instead of push
|
|
86
|
+
navigate('/login', { replace: true });
|
|
87
|
+
|
|
88
|
+
// Navigate without scrolling to top
|
|
89
|
+
navigate('/next-page', { scroll: false });
|
|
90
|
+
|
|
91
|
+
// Pass state data
|
|
92
|
+
navigate('/dashboard', { state: { from: '/login' } });
|
|
93
|
+
|
|
94
|
+
// Prefetch a route
|
|
95
|
+
prefetch('/heavy-page');
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Route Matching
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { matchRoute, parseParams, generatePath } from '@flight-framework/router';
|
|
102
|
+
|
|
103
|
+
// Check if a path matches a pattern
|
|
104
|
+
const { matched, params } = matchRoute('/docs/routing', '/docs/:slug');
|
|
105
|
+
// matched: true, params: { slug: 'routing' }
|
|
106
|
+
|
|
107
|
+
// Parse params from a path
|
|
108
|
+
const params = parseParams('/blog/2024/my-post', '/blog/:year/:slug');
|
|
109
|
+
// { year: '2024', slug: 'my-post' }
|
|
110
|
+
|
|
111
|
+
// Generate a path from pattern and params
|
|
112
|
+
const path = generatePath('/docs/:slug', { slug: 'api-routes' });
|
|
113
|
+
// '/docs/api-routes'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Link Props
|
|
117
|
+
|
|
118
|
+
| Prop | Type | Default | Description |
|
|
119
|
+
|------|------|---------|-------------|
|
|
120
|
+
| `href` | `string` | required | Target URL path |
|
|
121
|
+
| `prefetch` | `boolean` | `false` | Prefetch target on hover |
|
|
122
|
+
| `replace` | `boolean` | `false` | Replace history entry |
|
|
123
|
+
| `scroll` | `boolean` | `true` | Scroll to top on navigate |
|
|
124
|
+
| `target` | `string` | - | Link target (_blank, etc) |
|
|
125
|
+
| `className` | `string` | - | CSS class |
|
|
126
|
+
|
|
127
|
+
## SSR Support
|
|
128
|
+
|
|
129
|
+
The router is SSR-safe. Pass the initial path from your server:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// entry-server.tsx
|
|
133
|
+
export function render(url: string) {
|
|
134
|
+
return renderToString(
|
|
135
|
+
<RouterProvider initialPath={url}>
|
|
136
|
+
<App />
|
|
137
|
+
</RouterProvider>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for @flight-framework/router
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Router context value provided to all components
|
|
6
|
+
*/
|
|
7
|
+
interface RouterContextValue {
|
|
8
|
+
/** Current pathname (e.g., '/docs/routing') */
|
|
9
|
+
path: string;
|
|
10
|
+
/** URL search params as object */
|
|
11
|
+
searchParams: URLSearchParams;
|
|
12
|
+
/** Navigate to a new path */
|
|
13
|
+
navigate: (to: string, options?: NavigateOptions) => void;
|
|
14
|
+
/** Go back in history */
|
|
15
|
+
back: () => void;
|
|
16
|
+
/** Go forward in history */
|
|
17
|
+
forward: () => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Props for RouterProvider component
|
|
21
|
+
*/
|
|
22
|
+
interface RouterProviderProps {
|
|
23
|
+
/** Child components */
|
|
24
|
+
children: unknown;
|
|
25
|
+
/** Initial path for SSR (server passes request URL) */
|
|
26
|
+
initialPath?: string;
|
|
27
|
+
/** Base path for the router (e.g., '/app') */
|
|
28
|
+
basePath?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Props for Link component
|
|
32
|
+
*/
|
|
33
|
+
interface LinkProps {
|
|
34
|
+
/** Target URL path */
|
|
35
|
+
href: string;
|
|
36
|
+
/** Child content */
|
|
37
|
+
children: unknown;
|
|
38
|
+
/** CSS class name */
|
|
39
|
+
className?: string;
|
|
40
|
+
/** Open in new tab */
|
|
41
|
+
target?: string;
|
|
42
|
+
/** Link relationship */
|
|
43
|
+
rel?: string;
|
|
44
|
+
/** Prefetch the target page */
|
|
45
|
+
prefetch?: boolean;
|
|
46
|
+
/** Replace current history entry instead of pushing */
|
|
47
|
+
replace?: boolean;
|
|
48
|
+
/** Scroll to top after navigation */
|
|
49
|
+
scroll?: boolean;
|
|
50
|
+
/** Aria label for accessibility */
|
|
51
|
+
'aria-label'?: string;
|
|
52
|
+
/** Click handler */
|
|
53
|
+
onClick?: (event: MouseEvent) => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Options for programmatic navigation
|
|
57
|
+
*/
|
|
58
|
+
interface NavigateOptions {
|
|
59
|
+
/** Replace current history entry instead of pushing */
|
|
60
|
+
replace?: boolean;
|
|
61
|
+
/** Scroll to top after navigation */
|
|
62
|
+
scroll?: boolean;
|
|
63
|
+
/** State data to pass to the new route */
|
|
64
|
+
state?: unknown;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Dynamic route parameters extracted from URL
|
|
68
|
+
*/
|
|
69
|
+
type RouteParams<T extends string = string> = Record<T, string>;
|
|
70
|
+
/**
|
|
71
|
+
* Search params as key-value pairs
|
|
72
|
+
*/
|
|
73
|
+
type SearchParams = Record<string, string | string[]>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Router Context and Provider
|
|
77
|
+
*
|
|
78
|
+
* Provides routing state to the component tree.
|
|
79
|
+
* SSR-safe: works on both server and client.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* React Context for router
|
|
84
|
+
* Only used if React is available
|
|
85
|
+
*/
|
|
86
|
+
declare let RouterContext: unknown;
|
|
87
|
+
declare let RouterProvider: unknown;
|
|
88
|
+
declare let useRouter: () => RouterContextValue;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Link Component
|
|
92
|
+
*
|
|
93
|
+
* Client-side navigation link with prefetching support.
|
|
94
|
+
* Works with React or as a vanilla function.
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
declare let Link: unknown;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Router Hooks
|
|
101
|
+
*
|
|
102
|
+
* React hooks for accessing router state.
|
|
103
|
+
* These require React to be available.
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
declare let useParams: <T extends RouteParams = RouteParams>() => T;
|
|
107
|
+
declare let useSearchParams: () => [URLSearchParams, (params: SearchParams | URLSearchParams) => void];
|
|
108
|
+
declare let usePathname: () => string;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Navigation Utilities
|
|
112
|
+
*
|
|
113
|
+
* Programmatic navigation and route matching utilities.
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Navigate programmatically to a new path
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* navigate('/docs');
|
|
122
|
+
* navigate('/login', { replace: true });
|
|
123
|
+
* navigate('/dashboard', { scroll: false, state: { from: '/home' } });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
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
|
+
/**
|
|
138
|
+
* Match a pathname against a route pattern
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* matchRoute('/docs/routing', '/docs/:slug');
|
|
143
|
+
* // Returns: { params: { slug: 'routing' }, matched: true }
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
declare function matchRoute(pathname: string, pattern: string): {
|
|
147
|
+
matched: boolean;
|
|
148
|
+
params: RouteParams;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Parse parameters from a matched route
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* parseParams('/blog/2024/my-post', '/blog/:year/:slug');
|
|
156
|
+
* // Returns: { year: '2024', slug: 'my-post' }
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
declare function parseParams(pathname: string, pattern: string): RouteParams;
|
|
160
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// src/context.ts
|
|
2
|
+
var isBrowser = typeof window !== "undefined";
|
|
3
|
+
var currentContext = {
|
|
4
|
+
path: "/",
|
|
5
|
+
searchParams: new URLSearchParams(),
|
|
6
|
+
navigate: () => {
|
|
7
|
+
},
|
|
8
|
+
back: () => {
|
|
9
|
+
},
|
|
10
|
+
forward: () => {
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var subscribers = /* @__PURE__ */ new Set();
|
|
14
|
+
function subscribe(callback) {
|
|
15
|
+
subscribers.add(callback);
|
|
16
|
+
return () => subscribers.delete(callback);
|
|
17
|
+
}
|
|
18
|
+
function getRouterContext() {
|
|
19
|
+
return currentContext;
|
|
20
|
+
}
|
|
21
|
+
function updateContext(updates) {
|
|
22
|
+
currentContext = { ...currentContext, ...updates };
|
|
23
|
+
subscribers.forEach((cb) => cb(currentContext));
|
|
24
|
+
}
|
|
25
|
+
function navigateTo(to, options = {}) {
|
|
26
|
+
if (!isBrowser) return;
|
|
27
|
+
const { replace = false, scroll = true, state } = options;
|
|
28
|
+
if (replace) {
|
|
29
|
+
window.history.replaceState(state ?? null, "", to);
|
|
30
|
+
} else {
|
|
31
|
+
window.history.pushState(state ?? null, "", to);
|
|
32
|
+
}
|
|
33
|
+
const url = new URL(to, window.location.origin);
|
|
34
|
+
updateContext({
|
|
35
|
+
path: url.pathname,
|
|
36
|
+
searchParams: url.searchParams
|
|
37
|
+
});
|
|
38
|
+
if (scroll) {
|
|
39
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
var RouterContext = null;
|
|
43
|
+
var RouterProvider = null;
|
|
44
|
+
var useRouter = getRouterContext;
|
|
45
|
+
if (typeof globalThis !== "undefined") {
|
|
46
|
+
try {
|
|
47
|
+
const React = globalThis.React;
|
|
48
|
+
if (React?.createContext) {
|
|
49
|
+
const { createContext, useState, useEffect, useContext } = React;
|
|
50
|
+
const ReactRouterContext = createContext(currentContext);
|
|
51
|
+
RouterContext = ReactRouterContext;
|
|
52
|
+
RouterProvider = function FlightRouterProvider({
|
|
53
|
+
children,
|
|
54
|
+
initialPath,
|
|
55
|
+
basePath = ""
|
|
56
|
+
}) {
|
|
57
|
+
const [routerState, setRouterState] = useState(() => {
|
|
58
|
+
const path = isBrowser ? window.location.pathname : initialPath || "/";
|
|
59
|
+
const searchParams = isBrowser ? new URLSearchParams(window.location.search) : new URLSearchParams();
|
|
60
|
+
return {
|
|
61
|
+
path: basePath && path.startsWith(basePath) ? path.slice(basePath.length) || "/" : path,
|
|
62
|
+
searchParams,
|
|
63
|
+
navigate: navigateTo,
|
|
64
|
+
back: () => isBrowser && window.history.back(),
|
|
65
|
+
forward: () => isBrowser && window.history.forward()
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!isBrowser) return;
|
|
70
|
+
const handlePopState = () => {
|
|
71
|
+
let path = window.location.pathname;
|
|
72
|
+
if (basePath && path.startsWith(basePath)) {
|
|
73
|
+
path = path.slice(basePath.length) || "/";
|
|
74
|
+
}
|
|
75
|
+
setRouterState((prev) => ({
|
|
76
|
+
...prev,
|
|
77
|
+
path,
|
|
78
|
+
searchParams: new URLSearchParams(window.location.search)
|
|
79
|
+
}));
|
|
80
|
+
};
|
|
81
|
+
window.addEventListener("popstate", handlePopState);
|
|
82
|
+
return () => window.removeEventListener("popstate", handlePopState);
|
|
83
|
+
}, [basePath]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
return subscribe((ctx) => {
|
|
86
|
+
setRouterState((prev) => ({
|
|
87
|
+
...prev,
|
|
88
|
+
path: ctx.path,
|
|
89
|
+
searchParams: ctx.searchParams
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
}, []);
|
|
93
|
+
return React.createElement(
|
|
94
|
+
ReactRouterContext.Provider,
|
|
95
|
+
{ value: routerState },
|
|
96
|
+
children
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
useRouter = function useFlightRouter() {
|
|
100
|
+
return useContext(ReactRouterContext);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/link.ts
|
|
108
|
+
var isBrowser2 = typeof window !== "undefined";
|
|
109
|
+
function handleLinkClick(href, options, event) {
|
|
110
|
+
if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
event?.preventDefault();
|
|
114
|
+
const { navigate: navigate2 } = getRouterContext();
|
|
115
|
+
navigate2(href, options);
|
|
116
|
+
}
|
|
117
|
+
function isExternalUrl(href) {
|
|
118
|
+
if (!href) return false;
|
|
119
|
+
return href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:");
|
|
120
|
+
}
|
|
121
|
+
function prefetchRoute(href) {
|
|
122
|
+
if (!isBrowser2) return;
|
|
123
|
+
const link = document.createElement("link");
|
|
124
|
+
link.rel = "prefetch";
|
|
125
|
+
link.href = href;
|
|
126
|
+
document.head.appendChild(link);
|
|
127
|
+
}
|
|
128
|
+
var Link = null;
|
|
129
|
+
if (typeof globalThis !== "undefined") {
|
|
130
|
+
try {
|
|
131
|
+
const React = globalThis.React;
|
|
132
|
+
if (React?.createElement) {
|
|
133
|
+
const { useCallback, useEffect, useRef } = React;
|
|
134
|
+
Link = function FlightLink({
|
|
135
|
+
href,
|
|
136
|
+
children,
|
|
137
|
+
className,
|
|
138
|
+
target,
|
|
139
|
+
rel,
|
|
140
|
+
prefetch: prefetch2 = false,
|
|
141
|
+
replace = false,
|
|
142
|
+
scroll = true,
|
|
143
|
+
onClick,
|
|
144
|
+
...props
|
|
145
|
+
}) {
|
|
146
|
+
const linkRef = useRef(null);
|
|
147
|
+
const isExternal = isExternalUrl(href);
|
|
148
|
+
const handleClick = useCallback((e) => {
|
|
149
|
+
if (onClick) {
|
|
150
|
+
onClick(e);
|
|
151
|
+
if (e.defaultPrevented) return;
|
|
152
|
+
}
|
|
153
|
+
if (isExternal || target === "_blank") return;
|
|
154
|
+
handleLinkClick(href, { replace, scroll }, e);
|
|
155
|
+
}, [href, isExternal, target, replace, scroll, onClick]);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!prefetch2 || isExternal || !isBrowser2) return;
|
|
158
|
+
const link = linkRef.current;
|
|
159
|
+
if (!link) return;
|
|
160
|
+
let prefetched = false;
|
|
161
|
+
const doPrefetch = () => {
|
|
162
|
+
if (!prefetched) {
|
|
163
|
+
prefetched = true;
|
|
164
|
+
prefetchRoute(href);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
link.addEventListener("mouseenter", doPrefetch);
|
|
168
|
+
link.addEventListener("focus", doPrefetch);
|
|
169
|
+
return () => {
|
|
170
|
+
link.removeEventListener("mouseenter", doPrefetch);
|
|
171
|
+
link.removeEventListener("focus", doPrefetch);
|
|
172
|
+
};
|
|
173
|
+
}, [href, prefetch2, isExternal]);
|
|
174
|
+
const computedRel = isExternal && target === "_blank" ? rel || "noopener noreferrer" : rel;
|
|
175
|
+
return React.createElement("a", {
|
|
176
|
+
ref: linkRef,
|
|
177
|
+
href,
|
|
178
|
+
className,
|
|
179
|
+
target,
|
|
180
|
+
rel: computedRel,
|
|
181
|
+
onClick: handleClick,
|
|
182
|
+
...props
|
|
183
|
+
}, children);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/hooks.ts
|
|
191
|
+
var isBrowser3 = typeof window !== "undefined";
|
|
192
|
+
var useParams = () => ({});
|
|
193
|
+
var useSearchParams = () => [new URLSearchParams(), () => {
|
|
194
|
+
}];
|
|
195
|
+
var usePathname = () => "/";
|
|
196
|
+
if (typeof globalThis !== "undefined") {
|
|
197
|
+
try {
|
|
198
|
+
const React = globalThis.React;
|
|
199
|
+
if (React?.useState) {
|
|
200
|
+
const { useState, useEffect, useCallback, useMemo } = React;
|
|
201
|
+
useParams = function useFlightParams() {
|
|
202
|
+
const [params, setParams] = useState({});
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const storedParams = window.__FLIGHT_PARAMS__;
|
|
205
|
+
if (storedParams) {
|
|
206
|
+
setParams(storedParams);
|
|
207
|
+
}
|
|
208
|
+
}, []);
|
|
209
|
+
return params;
|
|
210
|
+
};
|
|
211
|
+
useSearchParams = function useFlightSearchParams() {
|
|
212
|
+
const [searchParams, setSearchParamsState] = useState(
|
|
213
|
+
() => isBrowser3 ? new URLSearchParams(window.location.search) : new URLSearchParams()
|
|
214
|
+
);
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (!isBrowser3) return;
|
|
217
|
+
const handleChange = () => {
|
|
218
|
+
setSearchParamsState(new URLSearchParams(window.location.search));
|
|
219
|
+
};
|
|
220
|
+
window.addEventListener("popstate", handleChange);
|
|
221
|
+
return () => window.removeEventListener("popstate", handleChange);
|
|
222
|
+
}, []);
|
|
223
|
+
const setSearchParams = useCallback((newParams) => {
|
|
224
|
+
if (!isBrowser3) return;
|
|
225
|
+
let params;
|
|
226
|
+
if (newParams instanceof URLSearchParams) {
|
|
227
|
+
params = newParams;
|
|
228
|
+
} else {
|
|
229
|
+
params = new URLSearchParams();
|
|
230
|
+
Object.entries(newParams).forEach(([key, value]) => {
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
value.forEach((v) => params.append(key, v));
|
|
233
|
+
} else {
|
|
234
|
+
params.set(key, value);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
|
239
|
+
window.history.pushState(null, "", newUrl);
|
|
240
|
+
setSearchParamsState(params);
|
|
241
|
+
}, []);
|
|
242
|
+
return [searchParams, setSearchParams];
|
|
243
|
+
};
|
|
244
|
+
usePathname = function useFlightPathname() {
|
|
245
|
+
const { path } = getRouterContext();
|
|
246
|
+
const [pathname, setPathname] = useState(path);
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (!isBrowser3) return;
|
|
249
|
+
const handleChange = () => {
|
|
250
|
+
setPathname(window.location.pathname);
|
|
251
|
+
};
|
|
252
|
+
window.addEventListener("popstate", handleChange);
|
|
253
|
+
return () => window.removeEventListener("popstate", handleChange);
|
|
254
|
+
}, []);
|
|
255
|
+
return pathname;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/navigate.ts
|
|
263
|
+
var isBrowser4 = typeof window !== "undefined";
|
|
264
|
+
function navigate(to, options = {}) {
|
|
265
|
+
const { navigate: routerNavigate } = getRouterContext();
|
|
266
|
+
routerNavigate(to, options);
|
|
267
|
+
}
|
|
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
|
+
function patternToRegex(pattern) {
|
|
278
|
+
const paramNames = [];
|
|
279
|
+
let regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\[\.\.\.(\w+)\\\]/g, (_, name) => {
|
|
280
|
+
paramNames.push(name);
|
|
281
|
+
return "(.+)";
|
|
282
|
+
}).replace(/\\\[(\w+)\\\]/g, (_, name) => {
|
|
283
|
+
paramNames.push(name);
|
|
284
|
+
return "([^/]+)";
|
|
285
|
+
}).replace(/:(\w+)/g, (_, name) => {
|
|
286
|
+
paramNames.push(name);
|
|
287
|
+
return "([^/]+)";
|
|
288
|
+
});
|
|
289
|
+
regexStr = `^${regexStr}$`;
|
|
290
|
+
return {
|
|
291
|
+
regex: new RegExp(regexStr),
|
|
292
|
+
paramNames
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function matchRoute(pathname, pattern) {
|
|
296
|
+
const { regex, paramNames } = patternToRegex(pattern);
|
|
297
|
+
const match = pathname.match(regex);
|
|
298
|
+
if (!match) {
|
|
299
|
+
return { matched: false, params: {} };
|
|
300
|
+
}
|
|
301
|
+
const params = {};
|
|
302
|
+
paramNames.forEach((name, index) => {
|
|
303
|
+
params[name] = match[index + 1] || "";
|
|
304
|
+
});
|
|
305
|
+
return { matched: true, params };
|
|
306
|
+
}
|
|
307
|
+
function parseParams(pathname, pattern) {
|
|
308
|
+
const { params } = matchRoute(pathname, pattern);
|
|
309
|
+
return params;
|
|
310
|
+
}
|
|
311
|
+
export {
|
|
312
|
+
Link,
|
|
313
|
+
RouterContext,
|
|
314
|
+
RouterProvider,
|
|
315
|
+
matchRoute,
|
|
316
|
+
navigate,
|
|
317
|
+
parseParams,
|
|
318
|
+
prefetch,
|
|
319
|
+
useParams,
|
|
320
|
+
usePathname,
|
|
321
|
+
useRouter,
|
|
322
|
+
useSearchParams
|
|
323
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flight-framework/router",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Agnostic client-side routing primitives for Flight Framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
20
|
+
"dev": "tsup src/index.ts --format esm --dts --watch"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"flight",
|
|
24
|
+
"router",
|
|
25
|
+
"spa",
|
|
26
|
+
"navigation",
|
|
27
|
+
"ssr"
|
|
28
|
+
],
|
|
29
|
+
"author": "Flight Framework",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/EliosLT/Flight-framework",
|
|
34
|
+
"directory": "packages/router"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.3.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"react": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|