@fictjs/router 0.2.2 → 0.3.0
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/index.cjs +2373 -3
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1136 -2
- package/dist/index.d.ts +1136 -2
- package/dist/index.js +2305 -3
- package/dist/index.js.map +1 -0
- package/package.json +33 -7
- package/src/components.tsx +926 -0
- package/src/context.ts +404 -0
- package/src/data.ts +545 -0
- package/src/history.ts +659 -0
- package/src/index.ts +217 -0
- package/src/lazy.tsx +242 -0
- package/src/link.tsx +601 -0
- package/src/scroll.ts +245 -0
- package/src/types.ts +447 -0
- package/src/utils.ts +570 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview @fictjs/router - Reactive router for Fict
|
|
3
|
+
*
|
|
4
|
+
* A full-featured router for Fict applications with:
|
|
5
|
+
* - Fine-grained reactivity integration
|
|
6
|
+
* - Nested routes and layouts
|
|
7
|
+
* - Type-safe route parameters
|
|
8
|
+
* - Data loading and preloading
|
|
9
|
+
* - Route guards (beforeLeave)
|
|
10
|
+
* - Multiple history modes (browser, hash, memory)
|
|
11
|
+
* - Server-side rendering support
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { Router, Route, Link, useParams } from '@fictjs/router'
|
|
16
|
+
*
|
|
17
|
+
* function App() {
|
|
18
|
+
* return (
|
|
19
|
+
* <Router>
|
|
20
|
+
* <Route path="/" component={Home} />
|
|
21
|
+
* <Route path="/users/:id" component={UserProfile} />
|
|
22
|
+
* <Route path="/about" component={About} />
|
|
23
|
+
* </Router>
|
|
24
|
+
* )
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* function UserProfile() {
|
|
28
|
+
* const params = useParams()
|
|
29
|
+
* return <div>User ID: {params().id}</div>
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @packageDocumentation
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Components
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
Router,
|
|
42
|
+
HashRouter,
|
|
43
|
+
MemoryRouter,
|
|
44
|
+
StaticRouter,
|
|
45
|
+
Routes,
|
|
46
|
+
Route,
|
|
47
|
+
Outlet,
|
|
48
|
+
Navigate,
|
|
49
|
+
Redirect,
|
|
50
|
+
createRoutes,
|
|
51
|
+
createRouter,
|
|
52
|
+
} from './components'
|
|
53
|
+
|
|
54
|
+
export { Link, NavLink, Form } from './link'
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Hooks
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
useRouter,
|
|
62
|
+
useRoute,
|
|
63
|
+
useNavigate,
|
|
64
|
+
useLocation,
|
|
65
|
+
useParams,
|
|
66
|
+
useSearchParams,
|
|
67
|
+
useMatches,
|
|
68
|
+
useIsRouting,
|
|
69
|
+
usePendingLocation,
|
|
70
|
+
useRouteData,
|
|
71
|
+
useRouteError,
|
|
72
|
+
useResolvedPath,
|
|
73
|
+
useMatch,
|
|
74
|
+
useHref,
|
|
75
|
+
useIsActive,
|
|
76
|
+
useBeforeLeave,
|
|
77
|
+
} from './context'
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// History
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
createBrowserHistory,
|
|
85
|
+
createHashHistory,
|
|
86
|
+
createMemoryHistory,
|
|
87
|
+
createStaticHistory,
|
|
88
|
+
} from './history'
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Data Loading
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
query,
|
|
96
|
+
revalidate,
|
|
97
|
+
action,
|
|
98
|
+
getAction,
|
|
99
|
+
useSubmission,
|
|
100
|
+
useSubmissions,
|
|
101
|
+
submitAction,
|
|
102
|
+
preloadQuery,
|
|
103
|
+
createPreload,
|
|
104
|
+
createResource,
|
|
105
|
+
cleanupDataUtilities,
|
|
106
|
+
} from './data'
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Utilities
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
normalizePath,
|
|
114
|
+
joinPaths,
|
|
115
|
+
resolvePath,
|
|
116
|
+
createLocation,
|
|
117
|
+
parseURL,
|
|
118
|
+
createURL,
|
|
119
|
+
parseSearchParams,
|
|
120
|
+
stringifySearchParams,
|
|
121
|
+
locationsAreEqual,
|
|
122
|
+
stripBasePath,
|
|
123
|
+
prependBasePath,
|
|
124
|
+
matchRoutes,
|
|
125
|
+
compileRoute,
|
|
126
|
+
createBranches,
|
|
127
|
+
scoreRoute,
|
|
128
|
+
isServer,
|
|
129
|
+
isBrowser,
|
|
130
|
+
} from './utils'
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Scroll Restoration
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
export {
|
|
137
|
+
createScrollRestoration,
|
|
138
|
+
getScrollRestoration,
|
|
139
|
+
configureScrollRestoration,
|
|
140
|
+
scrollToTop,
|
|
141
|
+
scrollToHash,
|
|
142
|
+
saveScrollPosition,
|
|
143
|
+
restoreScrollPosition,
|
|
144
|
+
clearScrollPosition,
|
|
145
|
+
clearAllScrollPositions,
|
|
146
|
+
} from './scroll'
|
|
147
|
+
|
|
148
|
+
export type { ScrollRestorationOptions } from './scroll'
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Lazy Loading
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
export { lazy, preloadLazy, isLazyComponent, lazyRoute, createLazyRoutes } from './lazy'
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Types
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
export type {
|
|
161
|
+
// Location types
|
|
162
|
+
Location,
|
|
163
|
+
To,
|
|
164
|
+
NavigationIntent,
|
|
165
|
+
|
|
166
|
+
// Parameter types
|
|
167
|
+
Params,
|
|
168
|
+
SearchParams,
|
|
169
|
+
MatchFilter,
|
|
170
|
+
MatchFilters,
|
|
171
|
+
|
|
172
|
+
// Route definition types
|
|
173
|
+
RouteComponentProps,
|
|
174
|
+
PreloadArgs,
|
|
175
|
+
PreloadFunction,
|
|
176
|
+
RouteDefinition,
|
|
177
|
+
RouteProps,
|
|
178
|
+
|
|
179
|
+
// Match types
|
|
180
|
+
RouteMatch,
|
|
181
|
+
CompiledRoute,
|
|
182
|
+
RouteBranch,
|
|
183
|
+
|
|
184
|
+
// Navigation types
|
|
185
|
+
NavigateOptions,
|
|
186
|
+
NavigateFunction,
|
|
187
|
+
Navigation,
|
|
188
|
+
|
|
189
|
+
// Context types
|
|
190
|
+
RouterContextValue,
|
|
191
|
+
RouteContextValue,
|
|
192
|
+
|
|
193
|
+
// History types
|
|
194
|
+
History,
|
|
195
|
+
HistoryAction,
|
|
196
|
+
HistoryListener,
|
|
197
|
+
Blocker,
|
|
198
|
+
|
|
199
|
+
// BeforeLeave types
|
|
200
|
+
BeforeLeaveEventArgs,
|
|
201
|
+
BeforeLeaveHandler,
|
|
202
|
+
|
|
203
|
+
// Data loading types
|
|
204
|
+
Submission,
|
|
205
|
+
ActionFunction,
|
|
206
|
+
Action,
|
|
207
|
+
QueryFunction,
|
|
208
|
+
QueryCacheEntry,
|
|
209
|
+
|
|
210
|
+
// Router options
|
|
211
|
+
RouterOptions,
|
|
212
|
+
MemoryRouterOptions,
|
|
213
|
+
HashRouterOptions,
|
|
214
|
+
} from './types'
|
|
215
|
+
|
|
216
|
+
export type { LinkProps, NavLinkProps, NavLinkRenderProps, FormProps } from './link'
|
|
217
|
+
export type { Resource } from './data'
|
package/src/lazy.tsx
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Lazy loading utilities for @fictjs/router
|
|
3
|
+
*
|
|
4
|
+
* This module provides code splitting support via lazy loading of route components.
|
|
5
|
+
* Works with Fict's Suspense for loading states.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type FictNode, type Component } from '@fictjs/runtime'
|
|
9
|
+
import { createSignal } from '@fictjs/runtime/advanced'
|
|
10
|
+
|
|
11
|
+
import type { RouteComponentProps, RouteDefinition } from './types'
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Lazy Component
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* State for lazy-loaded component
|
|
19
|
+
*/
|
|
20
|
+
interface LazyState<T> {
|
|
21
|
+
component: T | null
|
|
22
|
+
error: unknown
|
|
23
|
+
loading: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a lazy-loaded component
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* // Create a lazy component
|
|
32
|
+
* const LazyUserProfile = lazy(() => import('./pages/UserProfile'))
|
|
33
|
+
*
|
|
34
|
+
* // Use in routes
|
|
35
|
+
* <Route path="/users/:id" component={LazyUserProfile} />
|
|
36
|
+
*
|
|
37
|
+
* // Or with Suspense fallback
|
|
38
|
+
* <Suspense fallback={<Loading />}>
|
|
39
|
+
* <LazyUserProfile />
|
|
40
|
+
* </Suspense>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function lazy<T extends Component<any>>(
|
|
44
|
+
loader: () => Promise<{ default: T } | T>,
|
|
45
|
+
): Component<any> {
|
|
46
|
+
let cachedComponent: T | null = null
|
|
47
|
+
let loadPromise: Promise<T> | null = null
|
|
48
|
+
|
|
49
|
+
// Create a wrapper component that handles lazy loading
|
|
50
|
+
const LazyComponent: Component<any> = props => {
|
|
51
|
+
const state = createSignal<LazyState<T>>({
|
|
52
|
+
component: cachedComponent,
|
|
53
|
+
error: null,
|
|
54
|
+
loading: !cachedComponent,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// If already cached, render immediately
|
|
58
|
+
if (cachedComponent) {
|
|
59
|
+
const CachedComponent = cachedComponent
|
|
60
|
+
return <CachedComponent {...props} />
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Start loading if not already in progress
|
|
64
|
+
if (!loadPromise) {
|
|
65
|
+
loadPromise = loader().then(module => {
|
|
66
|
+
// Handle both { default: Component } and direct Component exports
|
|
67
|
+
const component = 'default' in module ? module.default : module
|
|
68
|
+
cachedComponent = component
|
|
69
|
+
return component
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Load the component
|
|
74
|
+
loadPromise
|
|
75
|
+
.then(component => {
|
|
76
|
+
state({ component, error: null, loading: false })
|
|
77
|
+
})
|
|
78
|
+
.catch(error => {
|
|
79
|
+
state({ component: null, error, loading: false })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Render based on state
|
|
83
|
+
const currentState = state()
|
|
84
|
+
|
|
85
|
+
if (currentState.error) {
|
|
86
|
+
throw currentState.error
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (currentState.loading || !currentState.component) {
|
|
90
|
+
// Return null while loading - Suspense will handle the fallback
|
|
91
|
+
// For this to work properly with Suspense, we need to throw a promise
|
|
92
|
+
throw loadPromise
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const LoadedComponent = currentState.component
|
|
96
|
+
return <LoadedComponent {...props} />
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Mark as lazy for identification
|
|
100
|
+
;(LazyComponent as any).__lazy = true
|
|
101
|
+
;(LazyComponent as any).__preload = () => {
|
|
102
|
+
if (!loadPromise) {
|
|
103
|
+
loadPromise = loader().then(module => {
|
|
104
|
+
const component = 'default' in module ? module.default : module
|
|
105
|
+
cachedComponent = component
|
|
106
|
+
return component
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
return loadPromise
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return LazyComponent
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Preload a lazy component
|
|
117
|
+
* Useful for preloading on hover/focus
|
|
118
|
+
*/
|
|
119
|
+
export function preloadLazy(component: Component<any>): Promise<void> {
|
|
120
|
+
const lazyComp = component as any
|
|
121
|
+
if (lazyComp.__lazy && lazyComp.__preload) {
|
|
122
|
+
return lazyComp.__preload()
|
|
123
|
+
}
|
|
124
|
+
return Promise.resolve()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a component is a lazy component
|
|
129
|
+
*/
|
|
130
|
+
export function isLazyComponent(component: unknown): boolean {
|
|
131
|
+
return !!(component && typeof component === 'function' && (component as any).__lazy)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// Lazy Route
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a lazy route definition
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```tsx
|
|
143
|
+
* const routes = [
|
|
144
|
+
* lazyRoute({
|
|
145
|
+
* path: '/users/:id',
|
|
146
|
+
* component: () => import('./pages/UserProfile'),
|
|
147
|
+
* }),
|
|
148
|
+
* lazyRoute({
|
|
149
|
+
* path: '/settings',
|
|
150
|
+
* component: () => import('./pages/Settings'),
|
|
151
|
+
* loadingElement: <Loading />,
|
|
152
|
+
* errorElement: <Error />,
|
|
153
|
+
* }),
|
|
154
|
+
* ]
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function lazyRoute<P extends string = string>(config: {
|
|
158
|
+
path?: string
|
|
159
|
+
component: () => Promise<
|
|
160
|
+
{ default: Component<RouteComponentProps<P>> } | Component<RouteComponentProps<P>>
|
|
161
|
+
>
|
|
162
|
+
loadingElement?: FictNode
|
|
163
|
+
errorElement?: FictNode
|
|
164
|
+
preload?: RouteDefinition<P>['preload']
|
|
165
|
+
children?: RouteDefinition[]
|
|
166
|
+
index?: boolean
|
|
167
|
+
key?: string
|
|
168
|
+
}): RouteDefinition<P> {
|
|
169
|
+
const LazyComponent = lazy(config.component)
|
|
170
|
+
|
|
171
|
+
// Build the route definition, only including defined properties
|
|
172
|
+
const routeDef: RouteDefinition<P> = {
|
|
173
|
+
component: LazyComponent as Component<RouteComponentProps<P>>,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (config.path !== undefined) routeDef.path = config.path
|
|
177
|
+
if (config.loadingElement !== undefined) routeDef.loadingElement = config.loadingElement
|
|
178
|
+
if (config.errorElement !== undefined) routeDef.errorElement = config.errorElement
|
|
179
|
+
if (config.preload !== undefined) routeDef.preload = config.preload
|
|
180
|
+
if (config.children !== undefined) routeDef.children = config.children
|
|
181
|
+
if (config.index !== undefined) routeDef.index = config.index
|
|
182
|
+
if (config.key !== undefined) routeDef.key = config.key
|
|
183
|
+
|
|
184
|
+
return routeDef
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Lazy Loading Helpers
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create multiple lazy routes from a glob import pattern
|
|
193
|
+
* Useful for file-system based routing
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```tsx
|
|
197
|
+
* // In a Vite project
|
|
198
|
+
* const pages = import.meta.glob('./pages/*.tsx')
|
|
199
|
+
*
|
|
200
|
+
* const routes = createLazyRoutes(pages, {
|
|
201
|
+
* // Map file path to route path
|
|
202
|
+
* pathTransform: (filePath) => {
|
|
203
|
+
* // ./pages/UserProfile.tsx -> /user-profile
|
|
204
|
+
* return filePath
|
|
205
|
+
* .replace('./pages/', '/')
|
|
206
|
+
* .replace('.tsx', '')
|
|
207
|
+
* .toLowerCase()
|
|
208
|
+
* .replace(/([A-Z])/g, '-$1')
|
|
209
|
+
* },
|
|
210
|
+
* })
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function createLazyRoutes(
|
|
214
|
+
modules: Record<string, () => Promise<{ default: Component<any> }>>,
|
|
215
|
+
options: {
|
|
216
|
+
pathTransform?: (filePath: string) => string
|
|
217
|
+
loadingElement?: FictNode
|
|
218
|
+
errorElement?: FictNode
|
|
219
|
+
} = {},
|
|
220
|
+
): RouteDefinition[] {
|
|
221
|
+
const routes: RouteDefinition[] = []
|
|
222
|
+
|
|
223
|
+
for (const [filePath, loader] of Object.entries(modules)) {
|
|
224
|
+
const path = options.pathTransform
|
|
225
|
+
? options.pathTransform(filePath)
|
|
226
|
+
: filePath
|
|
227
|
+
.replace(/^\.\/pages/, '')
|
|
228
|
+
.replace(/\.(tsx?|jsx?)$/, '')
|
|
229
|
+
.toLowerCase()
|
|
230
|
+
|
|
231
|
+
routes.push(
|
|
232
|
+
lazyRoute({
|
|
233
|
+
path,
|
|
234
|
+
component: loader,
|
|
235
|
+
loadingElement: options.loadingElement,
|
|
236
|
+
errorElement: options.errorElement,
|
|
237
|
+
}),
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return routes
|
|
242
|
+
}
|