@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/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
+ }