olova 2.0.61 → 2.0.63
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/CHANGELOG.md +5 -0
- package/README.md +42 -61
- package/dist/compiler.d.ts +44 -0
- package/dist/compiler.js +2139 -0
- package/dist/compiler.js.map +1 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +859 -0
- package/dist/core.js.map +1 -0
- package/dist/global.d.ts +15 -0
- package/dist/global.js +226 -0
- package/dist/global.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2302 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +89 -0
- package/dist/runtime.js +633 -0
- package/dist/runtime.js.map +1 -0
- package/dist/signals-core-BdfWh1Yt.d.ts +43 -0
- package/dist/vite.d.ts +5 -0
- package/dist/vite.js +2302 -0
- package/dist/vite.js.map +1 -0
- package/package.json +83 -65
- package/dist/chunk-D7SIC5TC.js +0 -367
- package/dist/chunk-D7SIC5TC.js.map +0 -1
- package/dist/entry-server.cjs +0 -120
- package/dist/entry-server.cjs.map +0 -1
- package/dist/entry-server.js +0 -115
- package/dist/entry-server.js.map +0 -1
- package/dist/entry-worker.cjs +0 -133
- package/dist/entry-worker.cjs.map +0 -1
- package/dist/entry-worker.js +0 -127
- package/dist/entry-worker.js.map +0 -1
- package/dist/main.cjs +0 -18
- package/dist/main.cjs.map +0 -1
- package/dist/main.js +0 -16
- package/dist/main.js.map +0 -1
- package/dist/olova.cjs +0 -1680
- package/dist/olova.cjs.map +0 -1
- package/dist/olova.d.cts +0 -72
- package/dist/olova.d.ts +0 -72
- package/dist/olova.js +0 -1321
- package/dist/olova.js.map +0 -1
- package/dist/performance.cjs +0 -386
- package/dist/performance.cjs.map +0 -1
- package/dist/performance.js +0 -3
- package/dist/performance.js.map +0 -1
- package/dist/router.cjs +0 -646
- package/dist/router.cjs.map +0 -1
- package/dist/router.d.cts +0 -113
- package/dist/router.d.ts +0 -113
- package/dist/router.js +0 -632
- package/dist/router.js.map +0 -1
- package/main.tsx +0 -76
- package/olova.ts +0 -619
- package/src/entry-server.tsx +0 -165
- package/src/entry-worker.tsx +0 -201
- package/src/generator/index.ts +0 -409
- package/src/hydration/flight.ts +0 -320
- package/src/hydration/index.ts +0 -12
- package/src/hydration/types.ts +0 -225
- package/src/logger.ts +0 -182
- package/src/main.tsx +0 -24
- package/src/performance.ts +0 -488
- package/src/plugin/index.ts +0 -204
- package/src/router/ErrorBoundary.tsx +0 -145
- package/src/router/Link.tsx +0 -117
- package/src/router/OlovaRouter.tsx +0 -354
- package/src/router/Outlet.tsx +0 -8
- package/src/router/context.ts +0 -117
- package/src/router/index.ts +0 -29
- package/src/router/matching.ts +0 -63
- package/src/router/router.tsx +0 -23
- package/src/router/search-params.ts +0 -29
- package/src/scanner/index.ts +0 -114
- package/src/types/index.ts +0 -190
- package/src/utils/export.ts +0 -85
- package/src/utils/index.ts +0 -4
- package/src/utils/naming.ts +0 -54
- package/src/utils/path.ts +0 -45
- package/tsup.config.ts +0 -35
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
import { Suspense, useCallback, useEffect, useMemo, useRef, useState, useTransition, type ComponentType, type ReactNode } from 'react';
|
|
2
|
-
import type { LayoutRoute, Metadata, NavigateOptions, NotFoundPageConfig, Route, SearchParams, SetSearchParamsOptions } from '../types';
|
|
3
|
-
import { OutletContext, RouterContext } from './context';
|
|
4
|
-
import { ErrorBoundary, RouteErrorBoundary } from './ErrorBoundary';
|
|
5
|
-
import { findNotFoundPage, matchLayoutScope, matchRoute } from './matching';
|
|
6
|
-
import { buildSearchString, parseSearchParams } from './search-params';
|
|
7
|
-
|
|
8
|
-
function normalizePath(path: string): string {
|
|
9
|
-
if (path === '/') return path;
|
|
10
|
-
return path.replace(/\/+$/, '') || '/';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface OlovaRouterProps {
|
|
14
|
-
routes: Route[];
|
|
15
|
-
layouts?: LayoutRoute[];
|
|
16
|
-
notFoundPages?: NotFoundPageConfig[];
|
|
17
|
-
notFound?: ReactNode;
|
|
18
|
-
loadingFallback?: ReactNode;
|
|
19
|
-
initialPath?: string;
|
|
20
|
-
onNavigate?: (from: string, to: string) => void;
|
|
21
|
-
onBeforeNavigate?: (from: string, to: string) => boolean | void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function OlovaRouter({
|
|
25
|
-
routes,
|
|
26
|
-
layouts = [],
|
|
27
|
-
notFoundPages = [],
|
|
28
|
-
notFound = <div>404 - Not Found</div>,
|
|
29
|
-
loadingFallback = <div>Loading...</div>,
|
|
30
|
-
initialPath,
|
|
31
|
-
onNavigate,
|
|
32
|
-
onBeforeNavigate,
|
|
33
|
-
}: OlovaRouterProps) {
|
|
34
|
-
const isSSR = typeof window === 'undefined';
|
|
35
|
-
const [currentPath, setCurrentPath] = useState(() => normalizePath(initialPath || (isSSR ? '/' : window.location.pathname)));
|
|
36
|
-
const [searchParams, setSearchParamsState] = useState<SearchParams>(() =>
|
|
37
|
-
isSSR ? {} : parseSearchParams(window.location.search)
|
|
38
|
-
);
|
|
39
|
-
const [isPending, startTransition] = useTransition();
|
|
40
|
-
const prevPathRef = useRef(currentPath);
|
|
41
|
-
|
|
42
|
-
const [defaultTitle] = useState(() => isSSR ? '' : document.title);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (isSSR) return;
|
|
46
|
-
const onPopState = () => {
|
|
47
|
-
const newPath = normalizePath(window.location.pathname);
|
|
48
|
-
startTransition(() => {
|
|
49
|
-
setCurrentPath(newPath);
|
|
50
|
-
setSearchParamsState(parseSearchParams(window.location.search));
|
|
51
|
-
});
|
|
52
|
-
};
|
|
53
|
-
window.addEventListener('popstate', onPopState);
|
|
54
|
-
return () => window.removeEventListener('popstate', onPopState);
|
|
55
|
-
}, [isSSR]);
|
|
56
|
-
|
|
57
|
-
const performNavigation = useCallback((path: string, options: NavigateOptions = {}, historyMethod: 'push' | 'replace' = 'push') => {
|
|
58
|
-
if (isSSR) return;
|
|
59
|
-
const { scroll = true } = options;
|
|
60
|
-
|
|
61
|
-
const normalizedPath = normalizePath(path.split('?')[0].split('#')[0]);
|
|
62
|
-
const hash = path.includes('#') ? '#' + path.split('#')[1] : '';
|
|
63
|
-
const search = path.includes('?') ? '?' + path.split('?')[1].split('#')[0] : '';
|
|
64
|
-
const fullUrl = normalizedPath + search + hash;
|
|
65
|
-
|
|
66
|
-
if (onBeforeNavigate) {
|
|
67
|
-
const result = onBeforeNavigate(currentPath, normalizedPath);
|
|
68
|
-
if (result === false) return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const prevPath = currentPath;
|
|
72
|
-
|
|
73
|
-
if (historyMethod === 'replace') {
|
|
74
|
-
window.history.replaceState({}, '', fullUrl);
|
|
75
|
-
} else {
|
|
76
|
-
window.history.pushState({}, '', fullUrl);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
startTransition(() => {
|
|
80
|
-
setCurrentPath(normalizedPath);
|
|
81
|
-
setSearchParamsState(parseSearchParams(search));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
if (scroll) {
|
|
85
|
-
if (hash) {
|
|
86
|
-
const el = document.getElementById(hash.slice(1));
|
|
87
|
-
if (el) {
|
|
88
|
-
el.scrollIntoView({ behavior: 'smooth' });
|
|
89
|
-
} else {
|
|
90
|
-
window.scrollTo(0, 0);
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
window.scrollTo(0, 0);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (onNavigate) {
|
|
98
|
-
onNavigate(prevPath, normalizedPath);
|
|
99
|
-
}
|
|
100
|
-
}, [isSSR, currentPath, onBeforeNavigate, onNavigate]);
|
|
101
|
-
|
|
102
|
-
const navigate = useCallback((path: string, options?: NavigateOptions) => {
|
|
103
|
-
performNavigation(path, options, options?.replace ? 'replace' : 'push');
|
|
104
|
-
}, [performNavigation]);
|
|
105
|
-
|
|
106
|
-
const push = useCallback((path: string, options?: NavigateOptions) => {
|
|
107
|
-
performNavigation(path, options, 'push');
|
|
108
|
-
}, [performNavigation]);
|
|
109
|
-
|
|
110
|
-
const replace = useCallback((path: string, options?: NavigateOptions) => {
|
|
111
|
-
performNavigation(path, options, 'replace');
|
|
112
|
-
}, [performNavigation]);
|
|
113
|
-
|
|
114
|
-
const back = useCallback(() => {
|
|
115
|
-
if (!isSSR) window.history.back();
|
|
116
|
-
}, [isSSR]);
|
|
117
|
-
|
|
118
|
-
const forward = useCallback(() => {
|
|
119
|
-
if (!isSSR) window.history.forward();
|
|
120
|
-
}, [isSSR]);
|
|
121
|
-
|
|
122
|
-
const refresh = useCallback(() => {
|
|
123
|
-
if (!isSSR) {
|
|
124
|
-
startTransition(() => {
|
|
125
|
-
setCurrentPath(prev => prev);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}, [isSSR]);
|
|
129
|
-
|
|
130
|
-
const prefetch = useCallback((path: string) => {
|
|
131
|
-
// Trigger lazy component import by finding the matching route
|
|
132
|
-
const normalizedPath = normalizePath(path.split('?')[0]);
|
|
133
|
-
const pathParts = normalizedPath.split('/').filter(Boolean);
|
|
134
|
-
for (const route of routes) {
|
|
135
|
-
const segments = route.path.split('/').filter(Boolean);
|
|
136
|
-
const result = matchRoute(segments, pathParts);
|
|
137
|
-
if (result.match && route.component) {
|
|
138
|
-
// Accessing the component triggers React.lazy to load the chunk
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}, [routes]);
|
|
143
|
-
|
|
144
|
-
const setSearchParams = useCallback((
|
|
145
|
-
newParams: Record<string, string | string[] | null>,
|
|
146
|
-
options: SetSearchParamsOptions = {}
|
|
147
|
-
) => {
|
|
148
|
-
if (isSSR) return;
|
|
149
|
-
const { replace: shouldReplace = false, merge = false } = options;
|
|
150
|
-
|
|
151
|
-
let finalParams: Record<string, string | string[] | null>;
|
|
152
|
-
|
|
153
|
-
if (merge) {
|
|
154
|
-
finalParams = { ...searchParams, ...newParams };
|
|
155
|
-
for (const key of Object.keys(finalParams)) {
|
|
156
|
-
if (finalParams[key] === null) {
|
|
157
|
-
delete finalParams[key];
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
finalParams = newParams;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const searchString = buildSearchString(finalParams as Record<string, string | string[] | null>);
|
|
165
|
-
const newUrl = currentPath + searchString;
|
|
166
|
-
|
|
167
|
-
if (shouldReplace) {
|
|
168
|
-
window.history.replaceState({}, '', newUrl);
|
|
169
|
-
} else {
|
|
170
|
-
window.history.pushState({}, '', newUrl);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
setSearchParamsState(parseSearchParams(searchString));
|
|
174
|
-
}, [isSSR, searchParams, currentPath]);
|
|
175
|
-
|
|
176
|
-
const sortedRoutes = useMemo(() => {
|
|
177
|
-
return [...routes]
|
|
178
|
-
.sort((a, b) => {
|
|
179
|
-
const aHasCatchAll = a.path.includes('*');
|
|
180
|
-
const bHasCatchAll = b.path.includes('*');
|
|
181
|
-
const aHasDynamic = a.path.includes(':');
|
|
182
|
-
const bHasDynamic = b.path.includes(':');
|
|
183
|
-
|
|
184
|
-
if (aHasCatchAll && !bHasCatchAll) return 1;
|
|
185
|
-
if (!aHasCatchAll && bHasCatchAll) return -1;
|
|
186
|
-
if (aHasDynamic && !bHasDynamic) return 1;
|
|
187
|
-
if (!aHasDynamic && bHasDynamic) return -1;
|
|
188
|
-
return b.path.length - a.path.length;
|
|
189
|
-
})
|
|
190
|
-
.map(route => ({
|
|
191
|
-
...route,
|
|
192
|
-
segments: route.path.split('/').filter(Boolean)
|
|
193
|
-
}));
|
|
194
|
-
}, [routes]);
|
|
195
|
-
|
|
196
|
-
const applyMetadata = useCallback((metadata: Metadata | undefined) => {
|
|
197
|
-
if (isSSR) return;
|
|
198
|
-
if (metadata?.title) {
|
|
199
|
-
document.title = metadata.title;
|
|
200
|
-
} else {
|
|
201
|
-
document.title = defaultTitle;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const setMetaTag = (name: string, content: string | undefined, attr = 'name') => {
|
|
205
|
-
let meta = document.querySelector(`meta[${attr}="${name}"]`);
|
|
206
|
-
if (content) {
|
|
207
|
-
if (!meta) {
|
|
208
|
-
meta = document.createElement('meta');
|
|
209
|
-
meta.setAttribute(attr, name);
|
|
210
|
-
document.head.appendChild(meta);
|
|
211
|
-
}
|
|
212
|
-
meta.setAttribute('content', content);
|
|
213
|
-
} else if (meta) {
|
|
214
|
-
document.head.removeChild(meta);
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
setMetaTag('description', metadata?.description);
|
|
219
|
-
setMetaTag('keywords', metadata?.keywords
|
|
220
|
-
? (Array.isArray(metadata.keywords) ? metadata.keywords.join(', ') : metadata.keywords)
|
|
221
|
-
: undefined
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
// Open Graph tags
|
|
225
|
-
if (metadata?.openGraph) {
|
|
226
|
-
setMetaTag('og:title', metadata.openGraph.title || metadata.title, 'property');
|
|
227
|
-
setMetaTag('og:description', metadata.openGraph.description || metadata.description, 'property');
|
|
228
|
-
setMetaTag('og:image', metadata.openGraph.image, 'property');
|
|
229
|
-
setMetaTag('og:url', metadata.openGraph.url, 'property');
|
|
230
|
-
setMetaTag('og:type', metadata.openGraph.type || 'website', 'property');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Canonical URL
|
|
234
|
-
let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null;
|
|
235
|
-
if (metadata?.openGraph?.url) {
|
|
236
|
-
if (!canonical) {
|
|
237
|
-
canonical = document.createElement('link');
|
|
238
|
-
canonical.setAttribute('rel', 'canonical');
|
|
239
|
-
document.head.appendChild(canonical);
|
|
240
|
-
}
|
|
241
|
-
canonical.setAttribute('href', metadata.openGraph.url);
|
|
242
|
-
}
|
|
243
|
-
}, [defaultTitle, isSSR]);
|
|
244
|
-
|
|
245
|
-
const { currentRoute, MatchedComponent, params } = useMemo(() => {
|
|
246
|
-
const pathParts = currentPath.split('/').filter(Boolean);
|
|
247
|
-
|
|
248
|
-
for (const route of sortedRoutes) {
|
|
249
|
-
if (route.path === '/' && currentPath === '/') {
|
|
250
|
-
return {
|
|
251
|
-
currentRoute: route,
|
|
252
|
-
MatchedComponent: route.component || null,
|
|
253
|
-
params: {} as Record<string, string>
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const result = matchRoute(route.segments, pathParts);
|
|
258
|
-
if (result.match) {
|
|
259
|
-
return {
|
|
260
|
-
currentRoute: route,
|
|
261
|
-
MatchedComponent: route.component || null,
|
|
262
|
-
params: result.params
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return {
|
|
267
|
-
currentRoute: null as Route | null,
|
|
268
|
-
MatchedComponent: null as ComponentType | null,
|
|
269
|
-
params: {} as Record<string, string>
|
|
270
|
-
};
|
|
271
|
-
}, [sortedRoutes, currentPath]);
|
|
272
|
-
|
|
273
|
-
// Track path changes for onNavigate callback
|
|
274
|
-
useEffect(() => {
|
|
275
|
-
prevPathRef.current = currentPath;
|
|
276
|
-
}, [currentPath]);
|
|
277
|
-
|
|
278
|
-
useEffect(() => {
|
|
279
|
-
if (isSSR) return;
|
|
280
|
-
applyMetadata(currentRoute?.metadata);
|
|
281
|
-
}, [currentRoute, applyMetadata, isSSR]);
|
|
282
|
-
|
|
283
|
-
const matchingLayouts = useMemo(() => {
|
|
284
|
-
return layouts
|
|
285
|
-
.filter(layout => matchLayoutScope(layout.path, currentPath))
|
|
286
|
-
.sort((a, b) => a.path.length - b.path.length);
|
|
287
|
-
}, [layouts, currentPath]);
|
|
288
|
-
|
|
289
|
-
const FinalComponent = useMemo(() => {
|
|
290
|
-
if (MatchedComponent) return MatchedComponent;
|
|
291
|
-
return findNotFoundPage(currentPath, notFoundPages);
|
|
292
|
-
}, [MatchedComponent, currentPath, notFoundPages]);
|
|
293
|
-
|
|
294
|
-
const content = useMemo(() => {
|
|
295
|
-
const LoadingComponent = currentRoute?.loading;
|
|
296
|
-
const ErrorComponent = currentRoute?.error;
|
|
297
|
-
|
|
298
|
-
let result: ReactNode = FinalComponent ? <FinalComponent /> : notFound;
|
|
299
|
-
|
|
300
|
-
// Only wrap with Suspense if route has explicit loading component
|
|
301
|
-
// (eager imports don't need Suspense fallback)
|
|
302
|
-
if (LoadingComponent) {
|
|
303
|
-
result = (
|
|
304
|
-
<Suspense fallback={<LoadingComponent />}>
|
|
305
|
-
{result}
|
|
306
|
-
</Suspense>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Wrap with per-route error boundary (error.tsx)
|
|
311
|
-
if (ErrorComponent) {
|
|
312
|
-
result = (
|
|
313
|
-
<RouteErrorBoundary fallbackComponent={ErrorComponent} routePath={currentPath}>
|
|
314
|
-
{result}
|
|
315
|
-
</RouteErrorBoundary>
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
for (let i = matchingLayouts.length - 1; i >= 0; i--) {
|
|
320
|
-
const Layout = matchingLayouts[i].layout;
|
|
321
|
-
if (!Layout) continue;
|
|
322
|
-
|
|
323
|
-
const wrapped = result;
|
|
324
|
-
result = (
|
|
325
|
-
<OutletContext.Provider value={{ content: wrapped }}>
|
|
326
|
-
<Layout />
|
|
327
|
-
</OutletContext.Provider>
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return result;
|
|
332
|
-
}, [FinalComponent, matchingLayouts, notFound, loadingFallback, currentRoute, currentPath]);
|
|
333
|
-
|
|
334
|
-
return (
|
|
335
|
-
<ErrorBoundary>
|
|
336
|
-
<RouterContext.Provider value={{
|
|
337
|
-
currentPath,
|
|
338
|
-
params,
|
|
339
|
-
searchParams,
|
|
340
|
-
navigate,
|
|
341
|
-
push,
|
|
342
|
-
replace,
|
|
343
|
-
back,
|
|
344
|
-
forward,
|
|
345
|
-
refresh,
|
|
346
|
-
setSearchParams,
|
|
347
|
-
isNavigating: isPending,
|
|
348
|
-
prefetch,
|
|
349
|
-
}}>
|
|
350
|
-
{content}
|
|
351
|
-
</RouterContext.Provider>
|
|
352
|
-
</ErrorBoundary>
|
|
353
|
-
);
|
|
354
|
-
}
|
package/src/router/Outlet.tsx
DELETED
package/src/router/context.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useMemo, useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import type { RouterContextType, OutletContextType, NavigateOptions } from '../types';
|
|
3
|
-
|
|
4
|
-
export const RouterContext = createContext<RouterContextType | null>(null);
|
|
5
|
-
|
|
6
|
-
export const OutletContext = createContext<OutletContextType | null>(null);
|
|
7
|
-
|
|
8
|
-
const ssrRouter: RouterContextType = {
|
|
9
|
-
currentPath: '/',
|
|
10
|
-
params: {},
|
|
11
|
-
searchParams: {},
|
|
12
|
-
navigate: () => {},
|
|
13
|
-
push: () => {},
|
|
14
|
-
replace: () => {},
|
|
15
|
-
back: () => {},
|
|
16
|
-
forward: () => {},
|
|
17
|
-
refresh: () => {},
|
|
18
|
-
setSearchParams: () => {},
|
|
19
|
-
isNavigating: false,
|
|
20
|
-
prefetch: () => {},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export function useRouter() {
|
|
24
|
-
const context = useContext(RouterContext);
|
|
25
|
-
if (!context) {
|
|
26
|
-
if (typeof window === 'undefined') return ssrRouter;
|
|
27
|
-
throw new Error('useRouter must be used within OlovaRouter');
|
|
28
|
-
}
|
|
29
|
-
return context;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function useParams<T extends Record<string, string> = Record<string, string>>(): T {
|
|
33
|
-
const context = useContext(RouterContext);
|
|
34
|
-
return (context?.params || {}) as T;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function useSearchParams(): URLSearchParams {
|
|
38
|
-
const context = useContext(RouterContext);
|
|
39
|
-
const searchParams = context?.searchParams || {};
|
|
40
|
-
|
|
41
|
-
return useMemo(() => {
|
|
42
|
-
const params = new URLSearchParams();
|
|
43
|
-
Object.entries(searchParams).forEach(([key, value]) => {
|
|
44
|
-
if (Array.isArray(value)) {
|
|
45
|
-
value.forEach(v => params.append(key, v));
|
|
46
|
-
} else {
|
|
47
|
-
params.set(key, value);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
return params;
|
|
51
|
-
}, [searchParams]);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function usePathname(): string {
|
|
55
|
-
const context = useContext(RouterContext);
|
|
56
|
-
if (!context) {
|
|
57
|
-
if (typeof window === 'undefined') return '/';
|
|
58
|
-
throw new Error('usePathname must be used within OlovaRouter');
|
|
59
|
-
}
|
|
60
|
-
return context.currentPath;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function useSelectedLayoutSegment(): string | null {
|
|
64
|
-
const context = useContext(RouterContext);
|
|
65
|
-
if (!context) return null;
|
|
66
|
-
const segments = context.currentPath.split('/').filter(Boolean);
|
|
67
|
-
return segments[0] || null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function useSelectedLayoutSegments(): string[] {
|
|
71
|
-
const context = useContext(RouterContext);
|
|
72
|
-
if (!context) return [];
|
|
73
|
-
return context.currentPath.split('/').filter(Boolean);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function useIsNavigating(): boolean {
|
|
77
|
-
const context = useContext(RouterContext);
|
|
78
|
-
return context?.isNavigating ?? false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function useNavigationEvent(callback: (from: string, to: string) => void) {
|
|
82
|
-
const context = useContext(RouterContext);
|
|
83
|
-
const currentPath = context?.currentPath;
|
|
84
|
-
const prevPath = useRef(currentPath || '/');
|
|
85
|
-
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
if (!currentPath) return;
|
|
88
|
-
if (prevPath.current !== currentPath) {
|
|
89
|
-
callback(prevPath.current, currentPath);
|
|
90
|
-
prevPath.current = currentPath;
|
|
91
|
-
}
|
|
92
|
-
}, [currentPath, callback]);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function redirect(path: string, options?: NavigateOptions): never {
|
|
96
|
-
if (typeof window !== 'undefined') {
|
|
97
|
-
if (options?.replace) {
|
|
98
|
-
window.history.replaceState({}, '', path);
|
|
99
|
-
} else {
|
|
100
|
-
window.history.pushState({}, '', path);
|
|
101
|
-
}
|
|
102
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
103
|
-
}
|
|
104
|
-
throw new Error(`REDIRECT:${path}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function useRedirect() {
|
|
108
|
-
const { navigate, replace } = useRouter();
|
|
109
|
-
|
|
110
|
-
return useCallback((path: string, options?: NavigateOptions) => {
|
|
111
|
-
if (options?.replace) {
|
|
112
|
-
replace(path, options);
|
|
113
|
-
} else {
|
|
114
|
-
navigate(path, options);
|
|
115
|
-
}
|
|
116
|
-
}, [navigate, replace]);
|
|
117
|
-
}
|
package/src/router/index.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
redirect,
|
|
3
|
-
useIsNavigating,
|
|
4
|
-
useNavigationEvent,
|
|
5
|
-
useParams,
|
|
6
|
-
usePathname,
|
|
7
|
-
useRedirect,
|
|
8
|
-
useRouter,
|
|
9
|
-
useSearchParams,
|
|
10
|
-
useSelectedLayoutSegment,
|
|
11
|
-
useSelectedLayoutSegments,
|
|
12
|
-
} from './context';
|
|
13
|
-
export { ErrorBoundary, RouteErrorBoundary } from './ErrorBoundary';
|
|
14
|
-
export { createLink, type ResolveRoutePath } from './Link';
|
|
15
|
-
export { OlovaRouter } from './OlovaRouter';
|
|
16
|
-
export { Outlet } from './Outlet';
|
|
17
|
-
|
|
18
|
-
export type {
|
|
19
|
-
LayoutRoute,
|
|
20
|
-
Metadata,
|
|
21
|
-
NavigateOptions,
|
|
22
|
-
NotFoundPageConfig,
|
|
23
|
-
OutletContextType,
|
|
24
|
-
Route,
|
|
25
|
-
RouterContextType,
|
|
26
|
-
SearchParams,
|
|
27
|
-
SetSearchParamsOptions,
|
|
28
|
-
} from '../types';
|
|
29
|
-
|
package/src/router/matching.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type { ComponentType } from 'react';
|
|
2
|
-
import type { NotFoundPageConfig } from '../types';
|
|
3
|
-
|
|
4
|
-
export function matchRoute(patternParts: string[], pathParts: string[]) {
|
|
5
|
-
const params: Record<string, string> = {};
|
|
6
|
-
|
|
7
|
-
// Filter out empty parts (handles trailing slashes)
|
|
8
|
-
const filteredPathParts = pathParts.filter(Boolean);
|
|
9
|
-
const filteredPatternParts = patternParts.filter(Boolean);
|
|
10
|
-
|
|
11
|
-
for (let i = 0; i < filteredPatternParts.length; i++) {
|
|
12
|
-
const patternPart = filteredPatternParts[i];
|
|
13
|
-
const pathPart = filteredPathParts[i];
|
|
14
|
-
|
|
15
|
-
if (patternPart === '*') {
|
|
16
|
-
params['*'] = filteredPathParts.slice(i).join('/');
|
|
17
|
-
return { match: true, params };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (pathPart === undefined) {
|
|
21
|
-
return { match: false, params: {} };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (patternPart.startsWith(':')) {
|
|
25
|
-
params[patternPart.slice(1)] = decodeURIComponent(pathPart);
|
|
26
|
-
} else if (patternPart !== pathPart) {
|
|
27
|
-
return { match: false, params: {} };
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (filteredPathParts.length > filteredPatternParts.length) {
|
|
32
|
-
return { match: false, params: {} };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return { match: true, params };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function matchLayoutScope(layoutPath: string, pathname: string): boolean {
|
|
39
|
-
if (layoutPath === '/') return true;
|
|
40
|
-
const normalizedPathname = pathname.replace(/\/+$/, '') || '/';
|
|
41
|
-
const normalizedLayout = layoutPath.replace(/\/+$/, '') || '/';
|
|
42
|
-
return normalizedPathname === normalizedLayout || normalizedPathname.startsWith(normalizedLayout + '/');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function findNotFoundPage(path: string, notFoundPages: NotFoundPageConfig[]): ComponentType | null {
|
|
46
|
-
if (!notFoundPages || notFoundPages.length === 0) return null;
|
|
47
|
-
|
|
48
|
-
const normalizedPath = path.replace(/\/+$/, '') || '/';
|
|
49
|
-
|
|
50
|
-
const sorted = [...notFoundPages].sort((a, b) =>
|
|
51
|
-
b.pathPrefix.length - a.pathPrefix.length
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
for (const nf of sorted) {
|
|
55
|
-
if (nf.pathPrefix === '') {
|
|
56
|
-
return nf.component;
|
|
57
|
-
}
|
|
58
|
-
if (normalizedPath === nf.pathPrefix || normalizedPath.startsWith(nf.pathPrefix + '/')) {
|
|
59
|
-
return nf.component;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
package/src/router/router.tsx
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
OlovaRouter,
|
|
3
|
-
Outlet,
|
|
4
|
-
createLink,
|
|
5
|
-
redirect,
|
|
6
|
-
useIsNavigating,
|
|
7
|
-
useNavigationEvent,
|
|
8
|
-
useParams,
|
|
9
|
-
usePathname,
|
|
10
|
-
useRedirect,
|
|
11
|
-
useRouter,
|
|
12
|
-
useSearchParams,
|
|
13
|
-
useSelectedLayoutSegment,
|
|
14
|
-
useSelectedLayoutSegments,
|
|
15
|
-
type LayoutRoute,
|
|
16
|
-
type Metadata,
|
|
17
|
-
type NavigateOptions,
|
|
18
|
-
type NotFoundPageConfig,
|
|
19
|
-
type Route,
|
|
20
|
-
type SearchParams,
|
|
21
|
-
type SetSearchParamsOptions,
|
|
22
|
-
} from './index';
|
|
23
|
-
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { SearchParams } from '../types';
|
|
2
|
-
|
|
3
|
-
export function parseSearchParams(search: string): SearchParams {
|
|
4
|
-
const params: SearchParams = {};
|
|
5
|
-
const urlParams = new URLSearchParams(search);
|
|
6
|
-
|
|
7
|
-
for (const key of urlParams.keys()) {
|
|
8
|
-
const values = urlParams.getAll(key);
|
|
9
|
-
params[key] = values.length === 1 ? values[0] : values;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return params;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function buildSearchString(params: Record<string, string | string[] | null>): string {
|
|
16
|
-
const urlParams = new URLSearchParams();
|
|
17
|
-
|
|
18
|
-
for (const [key, value] of Object.entries(params)) {
|
|
19
|
-
if (value === null || value === undefined) continue;
|
|
20
|
-
if (Array.isArray(value)) {
|
|
21
|
-
value.forEach(v => urlParams.append(key, v));
|
|
22
|
-
} else {
|
|
23
|
-
urlParams.set(key, value);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const str = urlParams.toString();
|
|
28
|
-
return str ? `?${str}` : '';
|
|
29
|
-
}
|