olova-router 1.0.3 → 1.0.4
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.d.ts +1 -1
- package/dist/index.js +103 -36
- package/dist/router.d.ts +24 -4
- package/dist/router.js +50 -21
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
|
-
export { NotFoundPageConfig, OlovaRouter, SearchParams, SetSearchParamsOptions, createLink, useParams, useRouter, useSearchParams } from './router.js';
|
|
2
|
+
export { LayoutRoute, NotFoundPageConfig, OlovaRouter, Outlet, SearchParams, SetSearchParamsOptions, createLink, useParams, useRouter, useSearchParams } from './router.js';
|
|
3
3
|
import 'react/jsx-runtime';
|
|
4
4
|
import 'react';
|
|
5
5
|
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import { createContext, useContext, useState, useEffect } from 'react';
|
|
4
|
-
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
7
|
function parseSearchParams(search) {
|
|
@@ -27,6 +27,7 @@ function buildSearchString(params) {
|
|
|
27
27
|
return str ? `?${str}` : "";
|
|
28
28
|
}
|
|
29
29
|
var RouterContext = createContext(null);
|
|
30
|
+
var OutletContext = createContext(null);
|
|
30
31
|
function useRouter() {
|
|
31
32
|
const context = useContext(RouterContext);
|
|
32
33
|
if (!context) throw new Error("useRouter must be used within OlovaRouter");
|
|
@@ -41,6 +42,12 @@ function useSearchParams() {
|
|
|
41
42
|
if (!context) throw new Error("useSearchParams must be used within OlovaRouter");
|
|
42
43
|
return [context.searchParams, context.setSearchParams];
|
|
43
44
|
}
|
|
45
|
+
function Outlet() {
|
|
46
|
+
const context = useContext(OutletContext);
|
|
47
|
+
if (!context?.component) return null;
|
|
48
|
+
const Component = context.component;
|
|
49
|
+
return /* @__PURE__ */ jsx(Component, {});
|
|
50
|
+
}
|
|
44
51
|
function matchRoute(pattern, pathname) {
|
|
45
52
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
46
53
|
const pathParts = pathname.split("/").filter(Boolean);
|
|
@@ -66,6 +73,10 @@ function matchRoute(pattern, pathname) {
|
|
|
66
73
|
}
|
|
67
74
|
return { match: true, params };
|
|
68
75
|
}
|
|
76
|
+
function matchLayoutScope(layoutPath, pathname) {
|
|
77
|
+
if (layoutPath === "/") return true;
|
|
78
|
+
return pathname === layoutPath || pathname.startsWith(layoutPath + "/");
|
|
79
|
+
}
|
|
69
80
|
function findNotFoundPage(path2, notFoundPages) {
|
|
70
81
|
if (!notFoundPages || notFoundPages.length === 0) return null;
|
|
71
82
|
const sorted = [...notFoundPages].sort(
|
|
@@ -81,7 +92,7 @@ function findNotFoundPage(path2, notFoundPages) {
|
|
|
81
92
|
}
|
|
82
93
|
return null;
|
|
83
94
|
}
|
|
84
|
-
function OlovaRouter({ routes, notFoundPages = [], notFound = /* @__PURE__ */ jsx("div", { children: "404 - Not Found" }) }) {
|
|
95
|
+
function OlovaRouter({ routes, layouts = [], notFoundPages = [], notFound = /* @__PURE__ */ jsx("div", { children: "404 - Not Found" }) }) {
|
|
85
96
|
const [currentPath, setCurrentPath] = useState(window.location.pathname);
|
|
86
97
|
const [searchParams, setSearchParamsState] = useState(
|
|
87
98
|
() => parseSearchParams(window.location.search)
|
|
@@ -121,38 +132,56 @@ function OlovaRouter({ routes, notFoundPages = [], notFound = /* @__PURE__ */ js
|
|
|
121
132
|
}
|
|
122
133
|
setSearchParamsState(parseSearchParams(searchString));
|
|
123
134
|
};
|
|
124
|
-
|
|
135
|
+
const sortedRoutes = useMemo(() => {
|
|
136
|
+
return [...routes].sort((a, b) => {
|
|
137
|
+
const aHasCatchAll = a.path.includes("*");
|
|
138
|
+
const bHasCatchAll = b.path.includes("*");
|
|
139
|
+
const aHasDynamic = a.path.includes(":");
|
|
140
|
+
const bHasDynamic = b.path.includes(":");
|
|
141
|
+
if (aHasCatchAll && !bHasCatchAll) return 1;
|
|
142
|
+
if (!aHasCatchAll && bHasCatchAll) return -1;
|
|
143
|
+
if (aHasDynamic && !bHasDynamic) return 1;
|
|
144
|
+
if (!aHasDynamic && bHasDynamic) return -1;
|
|
145
|
+
return b.path.length - a.path.length;
|
|
146
|
+
});
|
|
147
|
+
}, [routes]);
|
|
148
|
+
const matchingLayouts = useMemo(() => {
|
|
149
|
+
return layouts.filter((layout) => matchLayoutScope(layout.path, currentPath)).sort((a, b) => a.path.length - b.path.length);
|
|
150
|
+
}, [layouts, currentPath]);
|
|
151
|
+
let MatchedComponent = null;
|
|
125
152
|
let params = {};
|
|
126
|
-
const sortedRoutes = [...routes].sort((a, b) => {
|
|
127
|
-
const aHasCatchAll = a.path.includes("*");
|
|
128
|
-
const bHasCatchAll = b.path.includes("*");
|
|
129
|
-
const aHasDynamic = a.path.includes(":");
|
|
130
|
-
const bHasDynamic = b.path.includes(":");
|
|
131
|
-
if (aHasCatchAll && !bHasCatchAll) return 1;
|
|
132
|
-
if (!aHasCatchAll && bHasCatchAll) return -1;
|
|
133
|
-
if (aHasDynamic && !bHasDynamic) return 1;
|
|
134
|
-
if (!aHasDynamic && bHasDynamic) return -1;
|
|
135
|
-
return b.path.length - a.path.length;
|
|
136
|
-
});
|
|
137
153
|
for (const route of sortedRoutes) {
|
|
138
154
|
if (route.path === "/" && currentPath === "/") {
|
|
139
|
-
|
|
155
|
+
MatchedComponent = route.component;
|
|
140
156
|
break;
|
|
141
157
|
}
|
|
142
158
|
const result = matchRoute(route.path, currentPath);
|
|
143
159
|
if (result.match) {
|
|
144
|
-
|
|
160
|
+
MatchedComponent = route.component;
|
|
145
161
|
params = result.params;
|
|
146
162
|
break;
|
|
147
163
|
}
|
|
148
164
|
}
|
|
149
|
-
if (!
|
|
165
|
+
if (!MatchedComponent) {
|
|
150
166
|
const NotFoundComponent = findNotFoundPage(currentPath, notFoundPages);
|
|
151
167
|
if (NotFoundComponent) {
|
|
152
|
-
|
|
168
|
+
MatchedComponent = NotFoundComponent;
|
|
153
169
|
}
|
|
154
170
|
}
|
|
155
|
-
|
|
171
|
+
const renderContent = () => {
|
|
172
|
+
const content = MatchedComponent ? /* @__PURE__ */ jsx(MatchedComponent, {}) : notFound;
|
|
173
|
+
if (matchingLayouts.length === 0) {
|
|
174
|
+
return content;
|
|
175
|
+
}
|
|
176
|
+
let result = content;
|
|
177
|
+
for (let i = matchingLayouts.length - 1; i >= 0; i--) {
|
|
178
|
+
const Layout = matchingLayouts[i].layout;
|
|
179
|
+
const wrapped = result;
|
|
180
|
+
result = /* @__PURE__ */ jsx(OutletContext.Provider, { value: { component: () => /* @__PURE__ */ jsx(Fragment, { children: wrapped }) }, children: /* @__PURE__ */ jsx(Layout, {}) });
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
};
|
|
184
|
+
return /* @__PURE__ */ jsx(RouterContext.Provider, { value: { currentPath, params, searchParams, navigate, setSearchParams }, children: renderContent() });
|
|
156
185
|
}
|
|
157
186
|
function createLink() {
|
|
158
187
|
return function Link({ href, children, className }) {
|
|
@@ -166,7 +195,7 @@ function createLink() {
|
|
|
166
195
|
|
|
167
196
|
// src/index.ts
|
|
168
197
|
function parseDynamicSegment(segment) {
|
|
169
|
-
if (segment === "$" || segment.match(/^\[
|
|
198
|
+
if (segment === "$" || segment.match(/^\[\.\.\.(.+)\]$/)) {
|
|
170
199
|
const paramName = segment === "$" ? "slug" : segment.match(/^\[\.\.\.(.+)\]$/)?.[1] || "slug";
|
|
171
200
|
return { isDynamic: true, paramName, isCatchAll: true };
|
|
172
201
|
}
|
|
@@ -224,18 +253,25 @@ function detectExportType(filePath) {
|
|
|
224
253
|
return { hasDefault: true, namedExport: null };
|
|
225
254
|
}
|
|
226
255
|
}
|
|
227
|
-
function scanDirectory(dir, rootDir, extensions, routes, notFoundPages, isRoot = false) {
|
|
256
|
+
function scanDirectory(dir, rootDir, extensions, routes, notFoundPages, layouts, isRoot = false) {
|
|
228
257
|
if (!fs.existsSync(dir)) return;
|
|
229
258
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
230
259
|
for (const entry of entries) {
|
|
231
260
|
const fullPath = path.join(dir, entry.name);
|
|
232
261
|
if (entry.isDirectory()) {
|
|
233
262
|
if (entry.name === "node_modules" || entry.name === "assets" || entry.name.startsWith("_")) continue;
|
|
234
|
-
scanDirectory(fullPath, rootDir, extensions, routes, notFoundPages, false);
|
|
263
|
+
scanDirectory(fullPath, rootDir, extensions, routes, notFoundPages, layouts, false);
|
|
235
264
|
} else if (entry.isFile()) {
|
|
236
265
|
const ext = path.extname(entry.name);
|
|
237
266
|
const baseName = path.basename(entry.name, ext);
|
|
238
|
-
if (baseName === "
|
|
267
|
+
if (baseName === "_layout" && extensions.includes(ext)) {
|
|
268
|
+
const relativePath = path.relative(rootDir, dir);
|
|
269
|
+
const { routePath } = pathToRoute(relativePath, path.sep);
|
|
270
|
+
layouts.push({
|
|
271
|
+
path: isRoot ? "/" : routePath,
|
|
272
|
+
filePath: fullPath
|
|
273
|
+
});
|
|
274
|
+
} else if (baseName === "404" && extensions.includes(ext)) {
|
|
239
275
|
const relativeParts = path.relative(rootDir, dir).split(path.sep).filter(Boolean);
|
|
240
276
|
const filteredParts = relativeParts.filter((p) => !isRouteGroup(p));
|
|
241
277
|
const pathPrefix = isRoot ? "" : "/" + filteredParts.join("/");
|
|
@@ -253,16 +289,18 @@ function scanDirectory(dir, rootDir, extensions, routes, notFoundPages, isRoot =
|
|
|
253
289
|
function scanRoutes(rootDir, extensions) {
|
|
254
290
|
const routes = [];
|
|
255
291
|
const notFoundPages = [];
|
|
292
|
+
const layouts = [];
|
|
256
293
|
const absoluteRoot = path.isAbsolute(rootDir) ? rootDir : path.resolve(rootDir);
|
|
257
294
|
if (!fs.existsSync(absoluteRoot)) {
|
|
258
295
|
throw new Error(`Olova Router: Root directory does not exist: ${absoluteRoot}`);
|
|
259
296
|
}
|
|
260
|
-
scanDirectory(absoluteRoot, absoluteRoot, extensions, routes, notFoundPages, true);
|
|
297
|
+
scanDirectory(absoluteRoot, absoluteRoot, extensions, routes, notFoundPages, layouts, true);
|
|
261
298
|
routes.sort((a, b) => a.isDynamic !== b.isDynamic ? a.isDynamic ? 1 : -1 : a.path.localeCompare(b.path));
|
|
262
299
|
notFoundPages.sort((a, b) => b.pathPrefix.length - a.pathPrefix.length);
|
|
263
|
-
|
|
300
|
+
layouts.sort((a, b) => a.path.length - b.path.length);
|
|
301
|
+
return { routes, notFoundPages, layouts };
|
|
264
302
|
}
|
|
265
|
-
function generateRouteTree(routes, notFoundPages, srcDir) {
|
|
303
|
+
function generateRouteTree(routes, notFoundPages, layouts, srcDir) {
|
|
266
304
|
const routeImports = routes.map((route, index) => {
|
|
267
305
|
const relativePath = "./" + path.relative(srcDir, route.component).replace(/\\/g, "/").replace(/\.tsx?$/, "");
|
|
268
306
|
if (route.hasDefault) {
|
|
@@ -283,18 +321,31 @@ function generateRouteTree(routes, notFoundPages, srcDir) {
|
|
|
283
321
|
return `import NotFound${index} from '${relativePath}';`;
|
|
284
322
|
}
|
|
285
323
|
}).join("\n");
|
|
324
|
+
const layoutImports = layouts.map((layout, index) => {
|
|
325
|
+
const relativePath = "./" + path.relative(srcDir, layout.filePath).replace(/\\/g, "/").replace(/\.tsx?$/, "");
|
|
326
|
+
if (layout.hasDefault) {
|
|
327
|
+
return `import Layout${index} from '${relativePath}';`;
|
|
328
|
+
} else if (layout.namedExport) {
|
|
329
|
+
return `import { ${layout.namedExport} as Layout${index} } from '${relativePath}';`;
|
|
330
|
+
} else {
|
|
331
|
+
return `import Layout${index} from '${relativePath}';`;
|
|
332
|
+
}
|
|
333
|
+
}).join("\n");
|
|
286
334
|
const routeObjects = routes.map((route, index) => {
|
|
287
335
|
return ` { path: '${route.path}', component: Route${index} }`;
|
|
288
336
|
}).join(",\n");
|
|
289
337
|
const notFoundObjects = notFoundPages.map((nf, index) => {
|
|
290
338
|
return ` { pathPrefix: '${nf.pathPrefix}', component: NotFound${index} }`;
|
|
291
339
|
}).join(",\n");
|
|
340
|
+
const layoutObjects = layouts.map((layout, index) => {
|
|
341
|
+
return ` { path: '${layout.path}', layout: Layout${index}, children: [] }`;
|
|
342
|
+
}).join(",\n");
|
|
292
343
|
const routePaths = routes.length > 0 ? routes.map((r) => `'${r.path}'`).join(" | ") : "never";
|
|
293
|
-
const allImports = [routeImports, notFoundImports].filter(Boolean).join("\n");
|
|
344
|
+
const allImports = [routeImports, notFoundImports, layoutImports].filter(Boolean).join("\n");
|
|
294
345
|
return `// Auto-generated by olova-router - DO NOT EDIT
|
|
295
346
|
// This file is auto-updated when you add/remove route folders
|
|
296
347
|
|
|
297
|
-
import { createLink, OlovaRouter, useRouter, useParams, useSearchParams } from 'olova-router/router';
|
|
348
|
+
import { createLink, OlovaRouter, useRouter, useParams, useSearchParams, Outlet } from 'olova-router/router';
|
|
298
349
|
${allImports}
|
|
299
350
|
|
|
300
351
|
export const routes = [
|
|
@@ -305,11 +356,15 @@ export const notFoundPages = [
|
|
|
305
356
|
${notFoundObjects || ""}
|
|
306
357
|
];
|
|
307
358
|
|
|
359
|
+
export const layouts = [
|
|
360
|
+
${layoutObjects || ""}
|
|
361
|
+
];
|
|
362
|
+
|
|
308
363
|
export type RoutePaths = ${routePaths};
|
|
309
364
|
|
|
310
365
|
export const Link = createLink<RoutePaths>();
|
|
311
|
-
export { OlovaRouter, useRouter, useParams, useSearchParams };
|
|
312
|
-
export type { NotFoundPageConfig, SearchParams, SetSearchParamsOptions } from 'olova-router/router';
|
|
366
|
+
export { OlovaRouter, useRouter, useParams, useSearchParams, Outlet };
|
|
367
|
+
export type { NotFoundPageConfig, SearchParams, SetSearchParamsOptions, LayoutRoute } from 'olova-router/router';
|
|
313
368
|
`;
|
|
314
369
|
}
|
|
315
370
|
function olovaRouter(options = {}) {
|
|
@@ -319,7 +374,7 @@ function olovaRouter(options = {}) {
|
|
|
319
374
|
let absoluteRootDir;
|
|
320
375
|
let watcher = null;
|
|
321
376
|
function generateRouteTreeFile() {
|
|
322
|
-
const { routes, notFoundPages } = scanRoutes(absoluteRootDir, extensions);
|
|
377
|
+
const { routes, notFoundPages, layouts } = scanRoutes(absoluteRootDir, extensions);
|
|
323
378
|
const routeConfigs = routes.map((r) => {
|
|
324
379
|
const exportInfo = detectExportType(r.filePath);
|
|
325
380
|
return {
|
|
@@ -339,7 +394,16 @@ function olovaRouter(options = {}) {
|
|
|
339
394
|
namedExport: exportInfo.namedExport
|
|
340
395
|
};
|
|
341
396
|
});
|
|
342
|
-
const
|
|
397
|
+
const layoutConfigs = layouts.map((l) => {
|
|
398
|
+
const exportInfo = detectExportType(l.filePath);
|
|
399
|
+
return {
|
|
400
|
+
path: l.path,
|
|
401
|
+
filePath: l.filePath.replace(/\\/g, "/"),
|
|
402
|
+
hasDefault: exportInfo.hasDefault,
|
|
403
|
+
namedExport: exportInfo.namedExport
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
const content = generateRouteTree(routeConfigs, notFoundConfigs, layoutConfigs, absoluteRootDir);
|
|
343
407
|
const treePath = path.resolve(absoluteRootDir, "route.tree.ts");
|
|
344
408
|
const existing = fs.existsSync(treePath) ? fs.readFileSync(treePath, "utf-8") : "";
|
|
345
409
|
if (existing !== content) {
|
|
@@ -349,14 +413,17 @@ function olovaRouter(options = {}) {
|
|
|
349
413
|
}
|
|
350
414
|
function startWatcher() {
|
|
351
415
|
if (watcher) return;
|
|
352
|
-
watcher = fs.watch(absoluteRootDir, { recursive: true }, (
|
|
416
|
+
watcher = fs.watch(absoluteRootDir, { recursive: true }, (eventType, filename) => {
|
|
353
417
|
if (!filename) return;
|
|
354
418
|
if (filename.includes("route.tree.ts")) return;
|
|
355
419
|
const isIndexFile = filename.endsWith("index.tsx") || filename.endsWith("index.ts");
|
|
356
420
|
const isAppFile = filename === "App.tsx" || filename === "App.ts";
|
|
357
421
|
const is404File = filename.endsWith("404.tsx") || filename.endsWith("404.ts");
|
|
422
|
+
const isLayoutFile = filename.endsWith("_layout.tsx") || filename.endsWith("_layout.ts");
|
|
358
423
|
const isDirectory = !filename.includes(".");
|
|
359
|
-
|
|
424
|
+
const isDynamicSegment = filename.includes("$") || filename.includes("[");
|
|
425
|
+
const isRenameEvent = eventType === "rename";
|
|
426
|
+
if (isIndexFile || isAppFile || is404File || isLayoutFile || isDirectory || isDynamicSegment || isRenameEvent) {
|
|
360
427
|
setTimeout(() => generateRouteTreeFile(), 100);
|
|
361
428
|
}
|
|
362
429
|
});
|
|
@@ -384,4 +451,4 @@ function olovaRouter(options = {}) {
|
|
|
384
451
|
}
|
|
385
452
|
var src_default = olovaRouter;
|
|
386
453
|
|
|
387
|
-
export { OlovaRouter, createLink, src_default as default, olovaRouter, useParams, useRouter, useSearchParams };
|
|
454
|
+
export { OlovaRouter, Outlet, createLink, src_default as default, olovaRouter, useParams, useRouter, useSearchParams };
|
package/dist/router.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ interface Route {
|
|
|
5
5
|
path: string;
|
|
6
6
|
component: ComponentType;
|
|
7
7
|
}
|
|
8
|
+
/** Layout route for nested layouts */
|
|
9
|
+
interface LayoutRoute {
|
|
10
|
+
path: string;
|
|
11
|
+
layout: ComponentType;
|
|
12
|
+
children: Route[];
|
|
13
|
+
}
|
|
8
14
|
/** 404 page configuration */
|
|
9
15
|
interface NotFoundPageConfig {
|
|
10
16
|
pathPrefix: string;
|
|
@@ -33,18 +39,32 @@ declare function useSearchParams(): [
|
|
|
33
39
|
SearchParams,
|
|
34
40
|
(params: Record<string, string | string[] | null>, options?: SetSearchParamsOptions) => void
|
|
35
41
|
];
|
|
42
|
+
/**
|
|
43
|
+
* Outlet component - renders the matched child route
|
|
44
|
+
* Use this in _layout.tsx to render nested routes
|
|
45
|
+
*/
|
|
46
|
+
declare function Outlet(): react_jsx_runtime.JSX.Element | null;
|
|
36
47
|
interface OlovaRouterProps {
|
|
37
48
|
routes: Route[];
|
|
49
|
+
layouts?: LayoutRoute[];
|
|
38
50
|
notFoundPages?: NotFoundPageConfig[];
|
|
39
51
|
notFound?: ReactNode;
|
|
40
52
|
}
|
|
41
53
|
/** Main router component - wrap your app with this */
|
|
42
|
-
declare function OlovaRouter({ routes, notFoundPages, notFound }: OlovaRouterProps): react_jsx_runtime.JSX.Element;
|
|
43
|
-
/**
|
|
54
|
+
declare function OlovaRouter({ routes, layouts, notFoundPages, notFound }: OlovaRouterProps): react_jsx_runtime.JSX.Element;
|
|
55
|
+
/**
|
|
56
|
+
* Type utilities for resolving dynamic route paths
|
|
57
|
+
* Transforms '/users/:id' into accepting '/users/anything'
|
|
58
|
+
*/
|
|
59
|
+
type ResolveSegment<S extends string> = S extends `:${string}` ? string : S extends '*' ? string : S;
|
|
60
|
+
type ResolvePathSegments<Path extends string> = Path extends `${infer Segment}/${infer Rest}` ? `${ResolveSegment<Segment>}/${ResolvePathSegments<Rest>}` : ResolveSegment<Path>;
|
|
61
|
+
type ResolveRoutePath<Path extends string> = Path extends `${infer Base}/*` ? `${ResolvePathSegments<Base>}/${string}` : ResolvePathSegments<Path>;
|
|
62
|
+
type ResolveRoutes<T extends string> = T extends string ? ResolveRoutePath<T> : never;
|
|
63
|
+
/** Creates a type-safe Link component that accepts resolved dynamic paths */
|
|
44
64
|
declare function createLink<T extends string>(): ({ href, children, className }: {
|
|
45
|
-
href: T
|
|
65
|
+
href: ResolveRoutes<T>;
|
|
46
66
|
children: ReactNode;
|
|
47
67
|
className?: string;
|
|
48
68
|
}) => react_jsx_runtime.JSX.Element;
|
|
49
69
|
|
|
50
|
-
export { type NotFoundPageConfig, OlovaRouter, type SearchParams, type SetSearchParamsOptions, createLink, useParams, useRouter, useSearchParams };
|
|
70
|
+
export { type LayoutRoute, type NotFoundPageConfig, OlovaRouter, Outlet, type ResolveRoutePath, type SearchParams, type SetSearchParamsOptions, createLink, useParams, useRouter, useSearchParams };
|
package/dist/router.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
-
import { jsx } from 'react/jsx-runtime';
|
|
1
|
+
import { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
3
3
|
|
|
4
4
|
// src/router.tsx
|
|
5
5
|
function parseSearchParams(search) {
|
|
@@ -25,6 +25,7 @@ function buildSearchString(params) {
|
|
|
25
25
|
return str ? `?${str}` : "";
|
|
26
26
|
}
|
|
27
27
|
var RouterContext = createContext(null);
|
|
28
|
+
var OutletContext = createContext(null);
|
|
28
29
|
function useRouter() {
|
|
29
30
|
const context = useContext(RouterContext);
|
|
30
31
|
if (!context) throw new Error("useRouter must be used within OlovaRouter");
|
|
@@ -39,6 +40,12 @@ function useSearchParams() {
|
|
|
39
40
|
if (!context) throw new Error("useSearchParams must be used within OlovaRouter");
|
|
40
41
|
return [context.searchParams, context.setSearchParams];
|
|
41
42
|
}
|
|
43
|
+
function Outlet() {
|
|
44
|
+
const context = useContext(OutletContext);
|
|
45
|
+
if (!context?.component) return null;
|
|
46
|
+
const Component = context.component;
|
|
47
|
+
return /* @__PURE__ */ jsx(Component, {});
|
|
48
|
+
}
|
|
42
49
|
function matchRoute(pattern, pathname) {
|
|
43
50
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
44
51
|
const pathParts = pathname.split("/").filter(Boolean);
|
|
@@ -64,6 +71,10 @@ function matchRoute(pattern, pathname) {
|
|
|
64
71
|
}
|
|
65
72
|
return { match: true, params };
|
|
66
73
|
}
|
|
74
|
+
function matchLayoutScope(layoutPath, pathname) {
|
|
75
|
+
if (layoutPath === "/") return true;
|
|
76
|
+
return pathname === layoutPath || pathname.startsWith(layoutPath + "/");
|
|
77
|
+
}
|
|
67
78
|
function findNotFoundPage(path, notFoundPages) {
|
|
68
79
|
if (!notFoundPages || notFoundPages.length === 0) return null;
|
|
69
80
|
const sorted = [...notFoundPages].sort(
|
|
@@ -79,7 +90,7 @@ function findNotFoundPage(path, notFoundPages) {
|
|
|
79
90
|
}
|
|
80
91
|
return null;
|
|
81
92
|
}
|
|
82
|
-
function OlovaRouter({ routes, notFoundPages = [], notFound = /* @__PURE__ */ jsx("div", { children: "404 - Not Found" }) }) {
|
|
93
|
+
function OlovaRouter({ routes, layouts = [], notFoundPages = [], notFound = /* @__PURE__ */ jsx("div", { children: "404 - Not Found" }) }) {
|
|
83
94
|
const [currentPath, setCurrentPath] = useState(window.location.pathname);
|
|
84
95
|
const [searchParams, setSearchParamsState] = useState(
|
|
85
96
|
() => parseSearchParams(window.location.search)
|
|
@@ -119,38 +130,56 @@ function OlovaRouter({ routes, notFoundPages = [], notFound = /* @__PURE__ */ js
|
|
|
119
130
|
}
|
|
120
131
|
setSearchParamsState(parseSearchParams(searchString));
|
|
121
132
|
};
|
|
122
|
-
|
|
133
|
+
const sortedRoutes = useMemo(() => {
|
|
134
|
+
return [...routes].sort((a, b) => {
|
|
135
|
+
const aHasCatchAll = a.path.includes("*");
|
|
136
|
+
const bHasCatchAll = b.path.includes("*");
|
|
137
|
+
const aHasDynamic = a.path.includes(":");
|
|
138
|
+
const bHasDynamic = b.path.includes(":");
|
|
139
|
+
if (aHasCatchAll && !bHasCatchAll) return 1;
|
|
140
|
+
if (!aHasCatchAll && bHasCatchAll) return -1;
|
|
141
|
+
if (aHasDynamic && !bHasDynamic) return 1;
|
|
142
|
+
if (!aHasDynamic && bHasDynamic) return -1;
|
|
143
|
+
return b.path.length - a.path.length;
|
|
144
|
+
});
|
|
145
|
+
}, [routes]);
|
|
146
|
+
const matchingLayouts = useMemo(() => {
|
|
147
|
+
return layouts.filter((layout) => matchLayoutScope(layout.path, currentPath)).sort((a, b) => a.path.length - b.path.length);
|
|
148
|
+
}, [layouts, currentPath]);
|
|
149
|
+
let MatchedComponent = null;
|
|
123
150
|
let params = {};
|
|
124
|
-
const sortedRoutes = [...routes].sort((a, b) => {
|
|
125
|
-
const aHasCatchAll = a.path.includes("*");
|
|
126
|
-
const bHasCatchAll = b.path.includes("*");
|
|
127
|
-
const aHasDynamic = a.path.includes(":");
|
|
128
|
-
const bHasDynamic = b.path.includes(":");
|
|
129
|
-
if (aHasCatchAll && !bHasCatchAll) return 1;
|
|
130
|
-
if (!aHasCatchAll && bHasCatchAll) return -1;
|
|
131
|
-
if (aHasDynamic && !bHasDynamic) return 1;
|
|
132
|
-
if (!aHasDynamic && bHasDynamic) return -1;
|
|
133
|
-
return b.path.length - a.path.length;
|
|
134
|
-
});
|
|
135
151
|
for (const route of sortedRoutes) {
|
|
136
152
|
if (route.path === "/" && currentPath === "/") {
|
|
137
|
-
|
|
153
|
+
MatchedComponent = route.component;
|
|
138
154
|
break;
|
|
139
155
|
}
|
|
140
156
|
const result = matchRoute(route.path, currentPath);
|
|
141
157
|
if (result.match) {
|
|
142
|
-
|
|
158
|
+
MatchedComponent = route.component;
|
|
143
159
|
params = result.params;
|
|
144
160
|
break;
|
|
145
161
|
}
|
|
146
162
|
}
|
|
147
|
-
if (!
|
|
163
|
+
if (!MatchedComponent) {
|
|
148
164
|
const NotFoundComponent = findNotFoundPage(currentPath, notFoundPages);
|
|
149
165
|
if (NotFoundComponent) {
|
|
150
|
-
|
|
166
|
+
MatchedComponent = NotFoundComponent;
|
|
151
167
|
}
|
|
152
168
|
}
|
|
153
|
-
|
|
169
|
+
const renderContent = () => {
|
|
170
|
+
const content = MatchedComponent ? /* @__PURE__ */ jsx(MatchedComponent, {}) : notFound;
|
|
171
|
+
if (matchingLayouts.length === 0) {
|
|
172
|
+
return content;
|
|
173
|
+
}
|
|
174
|
+
let result = content;
|
|
175
|
+
for (let i = matchingLayouts.length - 1; i >= 0; i--) {
|
|
176
|
+
const Layout = matchingLayouts[i].layout;
|
|
177
|
+
const wrapped = result;
|
|
178
|
+
result = /* @__PURE__ */ jsx(OutletContext.Provider, { value: { component: () => /* @__PURE__ */ jsx(Fragment, { children: wrapped }) }, children: /* @__PURE__ */ jsx(Layout, {}) });
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
};
|
|
182
|
+
return /* @__PURE__ */ jsx(RouterContext.Provider, { value: { currentPath, params, searchParams, navigate, setSearchParams }, children: renderContent() });
|
|
154
183
|
}
|
|
155
184
|
function createLink() {
|
|
156
185
|
return function Link({ href, children, className }) {
|
|
@@ -162,4 +191,4 @@ function createLink() {
|
|
|
162
191
|
};
|
|
163
192
|
}
|
|
164
193
|
|
|
165
|
-
export { OlovaRouter, createLink, useParams, useRouter, useSearchParams };
|
|
194
|
+
export { OlovaRouter, Outlet, createLink, useParams, useRouter, useSearchParams };
|