@uxf/router 11.114.0 → 11.120.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/README.md +71 -49
- package/client.d.ts +1 -0
- package/client.js +23 -0
- package/create-client-router.d.ts +44 -0
- package/create-client-router.js +100 -0
- package/{router.d.ts → create-router.d.ts} +21 -31
- package/{router.js → create-router.js} +8 -80
- package/{router.test.js → create-router.test.js} +12 -2
- package/index.d.ts +1 -1
- package/index.js +3 -1
- package/merge-route-matchers.d.ts +1 -1
- package/package.json +1 -1
- package/routes-check/routes-check.js +29 -4
- package/routes-check/routes-check.test.js +2 -0
- package/sitemap-generator.test.js +2 -2
- /package/{router.test.d.ts → create-router.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -6,61 +6,64 @@
|
|
|
6
6
|
yarn add @uxf/router
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- Create `routes.ts` and `index.ts` inside `routes` directory:
|
|
9
|
+
There are two factories:
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
- `createRouter` from `@uxf/router` is the default. It is server-safe (no React hooks, no client-only imports) and can be used anywhere, including React Server Components. It returns `routeToUrl`, `route`, `routes`, `getRouteInfo`, `createRouteMatcher` and `createSitemapGenerator`.
|
|
12
|
+
- `createClientRouter` from `@uxf/router/client` is a superset that adds the React hooks (`useQueryParams`, `useQueryParamsStatic`, `usePageParams`, `useRouteInfo`). It imports `next/navigation` and `next/router`, so it must only be used in client components.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
Define the routes once as plain data, then build each router from it:
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
16
|
+
```ts
|
|
17
|
+
// routes/routes.ts – plain route definitions, importable anywhere
|
|
18
|
+
|
|
19
|
+
import { number, object, optional, string } from "superstruct";
|
|
20
|
+
|
|
21
|
+
export const routes = {
|
|
22
|
+
index: {
|
|
23
|
+
path: "/",
|
|
24
|
+
},
|
|
25
|
+
"admin/index": {
|
|
26
|
+
path: "/admin",
|
|
27
|
+
schema: object({
|
|
28
|
+
param1: optional(number()),
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
"blog/detail": {
|
|
32
|
+
path: "/blog/[id]",
|
|
33
|
+
schema: object({
|
|
34
|
+
id: number(),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
"localized-route": {
|
|
38
|
+
path: {
|
|
39
|
+
en: "/en/home",
|
|
40
|
+
cs: "/cs/domu",
|
|
33
41
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
baseUrl: "https://www.uxf.cz"
|
|
47
|
-
} as const
|
|
48
|
-
);
|
|
42
|
+
schema: object({
|
|
43
|
+
term: optional(string()),
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
export const routerOptions = {
|
|
49
|
+
locales: ["cs", "en"],
|
|
50
|
+
baseUrl: "https://www.uxf.cz",
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
export type RouteList = typeof routes;
|
|
49
54
|
```
|
|
50
55
|
|
|
51
56
|
```ts
|
|
52
|
-
// routes/index.ts
|
|
57
|
+
// routes/index.ts – server-safe entry (`@app-routes`), usable in RSC
|
|
53
58
|
|
|
54
|
-
import
|
|
55
|
-
import { UxfGetServerSideProps, UxfGetStaticProps, ExtractSchema } from "@uxf/router";
|
|
59
|
+
import { createRouter, ExtractSchema, UxfGetServerSideProps, UxfGetStaticProps } from "@uxf/router";
|
|
56
60
|
import { PreviewData as NextPreviewData } from "next/types";
|
|
61
|
+
import { RouteList, routerOptions, routes } from "./routes";
|
|
57
62
|
|
|
58
|
-
export const {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
useQueryParamsStatic
|
|
63
|
-
} = router;
|
|
63
|
+
export const { route, routeToUrl, getRouteInfo, createRouteMatcher, createSitemapGenerator } = createRouter(
|
|
64
|
+
routes,
|
|
65
|
+
routerOptions,
|
|
66
|
+
);
|
|
64
67
|
|
|
65
68
|
export type GetRouteSchema<K extends keyof RouteList> = ExtractSchema<RouteList[K]>;
|
|
66
69
|
|
|
@@ -68,13 +71,27 @@ export type GetStaticProps<
|
|
|
68
71
|
Route extends keyof RouteList,
|
|
69
72
|
Props extends { [key: string]: any } = { [key: string]: any },
|
|
70
73
|
PreviewData extends NextPreviewData = NextPreviewData,
|
|
71
|
-
|
|
74
|
+
> = UxfGetStaticProps<RouteList, Route, Props, PreviewData>;
|
|
72
75
|
|
|
73
76
|
export type GetServerSideProps<
|
|
74
77
|
Route extends keyof RouteList,
|
|
75
78
|
Props extends { [key: string]: any } = { [key: string]: any },
|
|
76
79
|
PreviewData extends NextPreviewData = NextPreviewData,
|
|
77
|
-
|
|
80
|
+
> = UxfGetServerSideProps<RouteList, Route, Props, PreviewData>;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// routes/client.ts – client entry (`@app-routes/client`), hooks for client components
|
|
85
|
+
|
|
86
|
+
"use client";
|
|
87
|
+
|
|
88
|
+
import { createClientRouter } from "@uxf/router/client";
|
|
89
|
+
import { routerOptions, routes } from "./routes";
|
|
90
|
+
|
|
91
|
+
export const { useQueryParams, useQueryParamsStatic, usePageParams, useRouteInfo } = createClientRouter(
|
|
92
|
+
routes,
|
|
93
|
+
routerOptions,
|
|
94
|
+
);
|
|
78
95
|
```
|
|
79
96
|
|
|
80
97
|
Add configuration to `tsconfig.json`
|
|
@@ -86,6 +103,9 @@ Add configuration to `tsconfig.json`
|
|
|
86
103
|
"paths": {
|
|
87
104
|
"@app-routes": [
|
|
88
105
|
"routes"
|
|
106
|
+
],
|
|
107
|
+
"@app-routes/client": [
|
|
108
|
+
"routes/client"
|
|
89
109
|
]
|
|
90
110
|
}
|
|
91
111
|
}
|
|
@@ -94,9 +114,11 @@ Add configuration to `tsconfig.json`
|
|
|
94
114
|
|
|
95
115
|
## useQueryParams
|
|
96
116
|
|
|
117
|
+
Hooks live in the client entry (`@app-routes/client`):
|
|
118
|
+
|
|
97
119
|
```tsx
|
|
98
|
-
import { useQueryParams } from "@app-routes";
|
|
99
|
-
import { queryParamToNumber } from "
|
|
120
|
+
import { useQueryParams } from "@app-routes/client";
|
|
121
|
+
import { queryParamToNumber } from "@uxf/router";
|
|
100
122
|
|
|
101
123
|
// can be used on SSR pages
|
|
102
124
|
const [query, { push, replace }] = useQueryParams("route-name");
|
package/client.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./create-client-router";
|
package/client.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
15
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
// Client entry point for `@uxf/router/client`.
|
|
19
|
+
//
|
|
20
|
+
// Exposes `createClientRouter` – a superset of `createRouter` (`@uxf/router`) that adds the React
|
|
21
|
+
// hooks. It imports client-only modules (`next/navigation`, `next/router`), so it must not be
|
|
22
|
+
// imported into React Server Components. For server-safe usage import from `@uxf/router` instead.
|
|
23
|
+
__exportStar(require("./create-client-router"), exports);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Params } from "next/dist/server/request/params";
|
|
2
|
+
import { Infer } from "superstruct";
|
|
3
|
+
import { RouteInfo, Router, RouterOptions } from "./create-router";
|
|
4
|
+
import { QueryParams, RoutesDefinition } from "./types";
|
|
5
|
+
export interface TransitionOptions {
|
|
6
|
+
shallow?: boolean;
|
|
7
|
+
locale?: string | false;
|
|
8
|
+
scroll?: boolean;
|
|
9
|
+
unstable_skipClientCache?: boolean;
|
|
10
|
+
}
|
|
11
|
+
type QueryParamsResult<Nullable extends boolean, T extends keyof RouteList, Locales extends string[], RouteList extends RoutesDefinition<Locales>> = [
|
|
12
|
+
Nullable extends true ? Infer<NonNullable<RouteList[T]["schema"]>> | null : Infer<NonNullable<RouteList[T]["schema"]>>,
|
|
13
|
+
{
|
|
14
|
+
push: (params: Infer<NonNullable<RouteList[T]["schema"]>>, options?: TransitionOptions) => Promise<boolean>;
|
|
15
|
+
replace: (params: Infer<NonNullable<RouteList[T]["schema"]>>, options?: TransitionOptions) => Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* The client router – a superset of `createRouter` (`@uxf/router`) that adds the React hooks.
|
|
20
|
+
* Lives in a separate client-only module (`@uxf/router/client`) because it imports `next/navigation`
|
|
21
|
+
* and `next/router`, which are not safe to pull into React Server Components.
|
|
22
|
+
*/
|
|
23
|
+
export type ClientRouter<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = Router<Locales, RouteList> & {
|
|
24
|
+
useRouteInfo: () => RouteInfo | null;
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated use useQueryParamsStatic or useQueryParams instead
|
|
27
|
+
*/
|
|
28
|
+
useQueryParamsDeprecated: <T extends keyof RouteList>() => QueryParams<RouteList, T>;
|
|
29
|
+
/**
|
|
30
|
+
* Returns path params merged with search params
|
|
31
|
+
*/
|
|
32
|
+
usePageParams: () => Params | null;
|
|
33
|
+
useQueryParams: <T extends keyof RouteList>(routeName: T) => QueryParamsResult<false, T, Locales, RouteList>;
|
|
34
|
+
useQueryParamsStatic: <T extends keyof RouteList>(routeName: T) => QueryParamsResult<true, T, Locales, RouteList>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Creates the client router – everything `createRouter` returns plus the React hooks
|
|
38
|
+
* (`useQueryParams`, `useQueryParamsStatic`, `usePageParams`, `useRouteInfo`).
|
|
39
|
+
*
|
|
40
|
+
* Use this in client components. For server-safe usage (including React Server Components) use
|
|
41
|
+
* `createRouter` from `@uxf/router` instead.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createClientRouter<Locales extends string[], RouteList extends RoutesDefinition<Locales>>(routes: RouteList, routerOptions: RouterOptions<Locales>): ClientRouter<Locales, RouteList>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.createClientRouter = createClientRouter;
|
|
5
|
+
const is_nil_1 = require("@uxf/core/utils/is-nil");
|
|
6
|
+
const is_not_nil_1 = require("@uxf/core/utils/is-not-nil");
|
|
7
|
+
const throw_error_1 = require("@uxf/core/utils/throw-error");
|
|
8
|
+
const navigation_1 = require("next/navigation");
|
|
9
|
+
const router_1 = require("next/router");
|
|
10
|
+
const superstruct_1 = require("superstruct");
|
|
11
|
+
const create_router_1 = require("./create-router");
|
|
12
|
+
/**
|
|
13
|
+
* Creates the client router – everything `createRouter` returns plus the React hooks
|
|
14
|
+
* (`useQueryParams`, `useQueryParamsStatic`, `usePageParams`, `useRouteInfo`).
|
|
15
|
+
*
|
|
16
|
+
* Use this in client components. For server-safe usage (including React Server Components) use
|
|
17
|
+
* `createRouter` from `@uxf/router` instead.
|
|
18
|
+
*/
|
|
19
|
+
function createClientRouter(routes, routerOptions) {
|
|
20
|
+
const base = (0, create_router_1.createRouter)(routes, routerOptions);
|
|
21
|
+
const { routeToUrl, getRouteInfo } = base;
|
|
22
|
+
const useRouteInfo = () => {
|
|
23
|
+
const pathname = (0, navigation_1.usePathname)();
|
|
24
|
+
return (0, is_not_nil_1.isNotNil)(pathname) ? getRouteInfo(pathname) : null;
|
|
25
|
+
};
|
|
26
|
+
const usePageParams = () => {
|
|
27
|
+
const searchParams = (0, navigation_1.useSearchParams)();
|
|
28
|
+
const params = (0, navigation_1.useParams)();
|
|
29
|
+
if ((0, is_nil_1.isNil)(searchParams) || (0, is_nil_1.isNil)(params)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// Collect all query params, handling duplicate keys as arrays
|
|
33
|
+
const queryParams = Array.from(searchParams.entries()).reduce((acc, [key, value]) => {
|
|
34
|
+
const existing = acc[key];
|
|
35
|
+
if (existing === undefined) {
|
|
36
|
+
// First occurrence - set as single value
|
|
37
|
+
acc[key] = value;
|
|
38
|
+
}
|
|
39
|
+
else if (Array.isArray(existing)) {
|
|
40
|
+
// Already an array - append
|
|
41
|
+
existing.push(value);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Second occurrence - convert to array
|
|
45
|
+
acc[key] = [existing, value];
|
|
46
|
+
}
|
|
47
|
+
return acc;
|
|
48
|
+
}, {});
|
|
49
|
+
return { ...params, ...queryParams };
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
...base,
|
|
53
|
+
useRouteInfo,
|
|
54
|
+
usePageParams,
|
|
55
|
+
useQueryParamsDeprecated: () => (0, router_1.useRouter)().query,
|
|
56
|
+
useQueryParams(routeName) {
|
|
57
|
+
var _a;
|
|
58
|
+
const router = (0, navigation_1.useRouter)();
|
|
59
|
+
const pageParams = usePageParams();
|
|
60
|
+
if ((0, is_nil_1.isNil)(pageParams)) {
|
|
61
|
+
throw new Error("Router is not ready. Use useQueryParamsStatic instead of useQueryParams.");
|
|
62
|
+
}
|
|
63
|
+
const schema = (_a = routes[routeName].schema) !== null && _a !== void 0 ? _a : (0, throw_error_1.throwError)(`Route '${String(routeName)}' has no schema.`);
|
|
64
|
+
return [
|
|
65
|
+
(0, superstruct_1.mask)(pageParams, schema),
|
|
66
|
+
{
|
|
67
|
+
push: (params, options) => {
|
|
68
|
+
router.push(routeToUrl(routeName, params, {}), options);
|
|
69
|
+
return Promise.resolve(true); // remove me
|
|
70
|
+
},
|
|
71
|
+
replace: (params, options) => {
|
|
72
|
+
router.replace(routeToUrl(routeName, params, {}), options);
|
|
73
|
+
return Promise.resolve(true); // remove me
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
},
|
|
78
|
+
useQueryParamsStatic(routeName) {
|
|
79
|
+
const router = (0, navigation_1.useRouter)();
|
|
80
|
+
const pageParams = usePageParams();
|
|
81
|
+
const schema = routes[routeName].schema;
|
|
82
|
+
if (!schema) {
|
|
83
|
+
throw new Error(`Route '${String(routeName)}' has no schema.`);
|
|
84
|
+
}
|
|
85
|
+
return [
|
|
86
|
+
(0, is_not_nil_1.isNotNil)(pageParams) ? (0, superstruct_1.mask)(pageParams, schema) : null,
|
|
87
|
+
{
|
|
88
|
+
push: (params) => {
|
|
89
|
+
router.push(routeToUrl(routeName, params, {}));
|
|
90
|
+
return Promise.resolve(true); // remove me
|
|
91
|
+
},
|
|
92
|
+
replace: (params) => {
|
|
93
|
+
router.replace(routeToUrl(routeName, params, {}));
|
|
94
|
+
return Promise.resolve(true); // remove me
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Infer, Struct } from "superstruct";
|
|
1
|
+
import type { LinkProps } from "next/link";
|
|
2
|
+
import type { Struct } from "superstruct";
|
|
4
3
|
import { SitemapGeneratorOptions, SitemapGeneratorType, SitemapRouteResolvers } from "./sitemap-generator";
|
|
5
|
-
import {
|
|
6
|
-
export interface TransitionOptions {
|
|
7
|
-
shallow?: boolean;
|
|
8
|
-
locale?: string | false;
|
|
9
|
-
scroll?: boolean;
|
|
10
|
-
unstable_skipClientCache?: boolean;
|
|
11
|
-
}
|
|
4
|
+
import { RouteDefinition, RoutesDefinition } from "./types";
|
|
12
5
|
export type ExtractSchema<T> = T extends {
|
|
13
6
|
schema: Struct<infer U, any>;
|
|
14
7
|
} ? U : null;
|
|
@@ -26,39 +19,28 @@ export type FunctionParametersGeneratorWithPartialParams<Locales extends string[
|
|
|
26
19
|
}[keyof RouteList];
|
|
27
20
|
type RouteFunction<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = (...args: FunctionParametersGenerator<Locales, RouteList>) => LinkProps["href"];
|
|
28
21
|
type RouteToUrlFunction<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = (...args: FunctionParametersGenerator<Locales, RouteList>) => string;
|
|
29
|
-
type QueryParamsResult<Nullable extends boolean, T extends keyof RouteList, Locales extends string[], RouteList extends RoutesDefinition<Locales>> = [
|
|
30
|
-
Nullable extends true ? Infer<NonNullable<RouteList[T]["schema"]>> | null : Infer<NonNullable<RouteList[T]["schema"]>>,
|
|
31
|
-
{
|
|
32
|
-
push: (params: Infer<NonNullable<RouteList[T]["schema"]>>, options?: TransitionOptions) => Promise<boolean>;
|
|
33
|
-
replace: (params: Infer<NonNullable<RouteList[T]["schema"]>>, options?: TransitionOptions) => Promise<boolean>;
|
|
34
|
-
}
|
|
35
|
-
];
|
|
36
22
|
export type RouteMatcher = (pathname: string, pathParams?: Record<string, string | string[]>) => boolean;
|
|
37
|
-
type RouteInfo = {
|
|
23
|
+
export type RouteInfo = {
|
|
38
24
|
pathname: string;
|
|
39
25
|
routeName: string;
|
|
40
26
|
routeDefinition: RouteDefinition<any>;
|
|
41
27
|
};
|
|
42
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The router. Contains only pure URL/route helpers – no React hooks and no client-only imports
|
|
30
|
+
* (`next/navigation`, `next/router`), so it is safe to import into React Server Components.
|
|
31
|
+
*
|
|
32
|
+
* For the React hooks (`useQueryParams`, `usePageParams`, …) use `createClientRouter`
|
|
33
|
+
* from `@uxf/router/client`, which is a superset of this router.
|
|
34
|
+
*/
|
|
35
|
+
export type Router<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = {
|
|
43
36
|
route: RouteFunction<Locales, RouteList>;
|
|
44
37
|
routeToUrl: RouteToUrlFunction<Locales, RouteList>;
|
|
45
38
|
createSitemapGenerator: (resolvers: SitemapRouteResolvers<Locales, RouteList>, options?: SitemapGeneratorOptions) => SitemapGeneratorType;
|
|
46
39
|
routes: RouteList;
|
|
47
40
|
getRouteInfo: (pathname: string) => RouteInfo | null;
|
|
48
|
-
useRouteInfo: () => RouteInfo | null;
|
|
49
|
-
/**
|
|
50
|
-
* @deprecated use useQueryParamsStatic or useQueryParams instead
|
|
51
|
-
*/
|
|
52
|
-
useQueryParamsDeprecated: <T extends keyof RouteList>() => QueryParams<RouteList, T>;
|
|
53
|
-
/**
|
|
54
|
-
* Returns path params merged with search params
|
|
55
|
-
*/
|
|
56
|
-
usePageParams: () => Params | null;
|
|
57
|
-
useQueryParams: <T extends keyof RouteList>(routeName: T) => QueryParamsResult<false, T, Locales, RouteList>;
|
|
58
|
-
useQueryParamsStatic: <T extends keyof RouteList>(routeName: T) => QueryParamsResult<true, T, Locales, RouteList>;
|
|
59
41
|
createRouteMatcher: (...args: FunctionParametersGeneratorWithPartialParams<Locales, RouteList>) => RouteMatcher;
|
|
60
42
|
};
|
|
61
|
-
type RouterOptions<L extends string[]> = {
|
|
43
|
+
export type RouterOptions<L extends string[]> = {
|
|
62
44
|
baseUrl?: string;
|
|
63
45
|
locales?: L;
|
|
64
46
|
};
|
|
@@ -66,5 +48,13 @@ export type ExtendedRouteDefinition<Locales extends string[]> = RouteDefinition<
|
|
|
66
48
|
regex: RegExp[];
|
|
67
49
|
};
|
|
68
50
|
export type ExtendedRoutesDefinition<Locales extends string[]> = Record<string, ExtendedRouteDefinition<Locales>>;
|
|
51
|
+
/**
|
|
52
|
+
* Creates the router – URL building, route matching and sitemap generation.
|
|
53
|
+
*
|
|
54
|
+
* This is the server-safe default: it contains no React hooks and no client-only imports, so it can
|
|
55
|
+
* be used anywhere, including React Server Components. If you need the React hooks (`useQueryParams`,
|
|
56
|
+
* `usePageParams`, …) use `createClientRouter` from `@uxf/router/client` instead – it returns
|
|
57
|
+
* everything this router does plus the hooks.
|
|
58
|
+
*/
|
|
69
59
|
export declare function createRouter<Locales extends string[], RouteList extends RoutesDefinition<Locales>>(routes: RouteList, routerOptions: RouterOptions<Locales>): Router<Locales, RouteList>;
|
|
70
60
|
export {};
|
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createRouter = createRouter;
|
|
4
4
|
const empty_object_1 = require("@uxf/core/constants/empty-object");
|
|
5
|
-
const is_nil_1 = require("@uxf/core/utils/is-nil");
|
|
6
|
-
const is_not_nil_1 = require("@uxf/core/utils/is-not-nil");
|
|
7
5
|
const qs_1 = require("@uxf/core/utils/qs");
|
|
8
6
|
const throw_error_1 = require("@uxf/core/utils/throw-error");
|
|
9
|
-
const navigation_1 = require("next/navigation");
|
|
10
|
-
const router_1 = require("next/router");
|
|
11
|
-
const superstruct_1 = require("superstruct");
|
|
12
7
|
const sitemap_generator_1 = require("./sitemap-generator");
|
|
13
8
|
const path_to_regex_1 = require("./utils/path-to-regex");
|
|
14
9
|
/**
|
|
@@ -19,6 +14,14 @@ const path_to_regex_1 = require("./utils/path-to-regex");
|
|
|
19
14
|
function decodeArgs(args) {
|
|
20
15
|
return { route: args[0], params: args[1], options: args[2] };
|
|
21
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates the router – URL building, route matching and sitemap generation.
|
|
19
|
+
*
|
|
20
|
+
* This is the server-safe default: it contains no React hooks and no client-only imports, so it can
|
|
21
|
+
* be used anywhere, including React Server Components. If you need the React hooks (`useQueryParams`,
|
|
22
|
+
* `usePageParams`, …) use `createClientRouter` from `@uxf/router/client` instead – it returns
|
|
23
|
+
* everything this router does plus the hooks.
|
|
24
|
+
*/
|
|
22
25
|
function createRouter(routes, routerOptions) {
|
|
23
26
|
const routesWithRegex = Object.entries(routes).reduce((acc, [routeName, routeDefinition]) => ({
|
|
24
27
|
...acc,
|
|
@@ -94,35 +97,6 @@ function createRouter(routes, routerOptions) {
|
|
|
94
97
|
routeDefinition: entry[1],
|
|
95
98
|
};
|
|
96
99
|
};
|
|
97
|
-
const useRouteInfo = () => {
|
|
98
|
-
const pathname = (0, navigation_1.usePathname)();
|
|
99
|
-
return (0, is_not_nil_1.isNotNil)(pathname) ? getRouteInfo(pathname) : null;
|
|
100
|
-
};
|
|
101
|
-
const usePageParams = () => {
|
|
102
|
-
const searchParams = (0, navigation_1.useSearchParams)();
|
|
103
|
-
const params = (0, navigation_1.useParams)();
|
|
104
|
-
if ((0, is_nil_1.isNil)(searchParams) || (0, is_nil_1.isNil)(params)) {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
// Collect all query params, handling duplicate keys as arrays
|
|
108
|
-
const queryParams = Array.from(searchParams.entries()).reduce((acc, [key, value]) => {
|
|
109
|
-
const existing = acc[key];
|
|
110
|
-
if (existing === undefined) {
|
|
111
|
-
// First occurrence - set as single value
|
|
112
|
-
acc[key] = value;
|
|
113
|
-
}
|
|
114
|
-
else if (Array.isArray(existing)) {
|
|
115
|
-
// Already an array - append
|
|
116
|
-
existing.push(value);
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
// Second occurrence - convert to array
|
|
120
|
-
acc[key] = [existing, value];
|
|
121
|
-
}
|
|
122
|
-
return acc;
|
|
123
|
-
}, {});
|
|
124
|
-
return { ...params, ...queryParams };
|
|
125
|
-
};
|
|
126
100
|
return {
|
|
127
101
|
route(...args) {
|
|
128
102
|
var _a;
|
|
@@ -140,52 +114,6 @@ function createRouter(routes, routerOptions) {
|
|
|
140
114
|
},
|
|
141
115
|
routes,
|
|
142
116
|
getRouteInfo,
|
|
143
|
-
useRouteInfo,
|
|
144
|
-
usePageParams,
|
|
145
|
-
useQueryParamsDeprecated: () => (0, router_1.useRouter)().query,
|
|
146
|
-
useQueryParams(routeName) {
|
|
147
|
-
var _a;
|
|
148
|
-
const router = (0, navigation_1.useRouter)();
|
|
149
|
-
const pageParams = usePageParams();
|
|
150
|
-
if ((0, is_nil_1.isNil)(pageParams)) {
|
|
151
|
-
throw new Error("Router is not ready. Use useQueryParamsStatic instead of useQueryParams.");
|
|
152
|
-
}
|
|
153
|
-
const schema = (_a = routes[routeName].schema) !== null && _a !== void 0 ? _a : (0, throw_error_1.throwError)(`Route '${String(routeName)}' has no schema.`);
|
|
154
|
-
return [
|
|
155
|
-
(0, superstruct_1.mask)(pageParams, schema),
|
|
156
|
-
{
|
|
157
|
-
push: (params, options) => {
|
|
158
|
-
router.push(routeToUrl(routeName, params, {}), options);
|
|
159
|
-
return Promise.resolve(true); // remove me
|
|
160
|
-
},
|
|
161
|
-
replace: (params, options) => {
|
|
162
|
-
router.replace(routeToUrl(routeName, params, {}), options);
|
|
163
|
-
return Promise.resolve(true); // remove me
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
];
|
|
167
|
-
},
|
|
168
|
-
useQueryParamsStatic(routeName) {
|
|
169
|
-
const router = (0, navigation_1.useRouter)();
|
|
170
|
-
const pageParams = usePageParams();
|
|
171
|
-
const schema = routes[routeName].schema;
|
|
172
|
-
if (!schema) {
|
|
173
|
-
throw new Error(`Route '${String(routeName)}' has no schema.`);
|
|
174
|
-
}
|
|
175
|
-
return [
|
|
176
|
-
(0, is_not_nil_1.isNotNil)(pageParams) ? (0, superstruct_1.mask)(pageParams, schema) : null,
|
|
177
|
-
{
|
|
178
|
-
push: (params) => {
|
|
179
|
-
router.push(routeToUrl(routeName, params, {}));
|
|
180
|
-
return Promise.resolve(true); // remove me
|
|
181
|
-
},
|
|
182
|
-
replace: (params) => {
|
|
183
|
-
router.replace(routeToUrl(routeName, params, {}));
|
|
184
|
-
return Promise.resolve(true); // remove me
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
];
|
|
188
|
-
},
|
|
189
117
|
createRouteMatcher(...args) {
|
|
190
118
|
const [requiredRouteName, requiredParams] = args;
|
|
191
119
|
return (pathname, pageParams) => {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fs_1 = require("fs");
|
|
4
|
+
const path_1 = require("path");
|
|
3
5
|
const superstruct_1 = require("superstruct");
|
|
6
|
+
const create_router_1 = require("./create-router");
|
|
4
7
|
const merge_route_matchers_1 = require("./merge-route-matchers");
|
|
5
|
-
const router_1 = require("./router");
|
|
6
8
|
const superstruct_2 = require("./superstruct");
|
|
7
|
-
const { routeToUrl, createRouteMatcher } = (0,
|
|
9
|
+
const { routeToUrl, createRouteMatcher } = (0, create_router_1.createRouter)({
|
|
8
10
|
index: {
|
|
9
11
|
path: "/",
|
|
10
12
|
schema: (0, superstruct_1.object)({
|
|
@@ -168,3 +170,11 @@ test("merge route matchers", () => {
|
|
|
168
170
|
param2: "any",
|
|
169
171
|
})).toBe(false);
|
|
170
172
|
});
|
|
173
|
+
// Guards the RSC-safety invariant: the router must not pull in any client-only modules.
|
|
174
|
+
test("create-router has no client-only imports", () => {
|
|
175
|
+
const source = (0, fs_1.readFileSync)((0, path_1.join)(__dirname, "create-router.ts"), "utf8");
|
|
176
|
+
expect(source).not.toMatch(/from "next\/navigation"/);
|
|
177
|
+
expect(source).not.toMatch(/from "next\/router"/);
|
|
178
|
+
expect(source).not.toMatch(/from "react"/);
|
|
179
|
+
expect(source).not.toMatch(/from "react-dom"/);
|
|
180
|
+
});
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// Root entry point for `@uxf/router`. RSC-safe: contains no React hooks and no client-only imports.
|
|
3
|
+
// For the React hooks use `createClientRouter` from `@uxf/router/client`.
|
|
2
4
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
5
|
if (k2 === undefined) k2 = k;
|
|
4
6
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -14,7 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
16
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
17
|
};
|
|
16
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
__exportStar(require("./create-router"), exports);
|
|
17
20
|
__exportStar(require("./helper"), exports);
|
|
18
21
|
__exportStar(require("./merge-route-matchers"), exports);
|
|
19
|
-
__exportStar(require("./router"), exports);
|
|
20
22
|
__exportStar(require("./types"), exports);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { RouteMatcher } from "./router";
|
|
1
|
+
import { RouteMatcher } from "./create-router";
|
|
2
2
|
export declare function mergeRouteMatchers(routeMatchers: RouteMatcher[]): RouteMatcher;
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.routesCheck = routesCheck;
|
|
5
5
|
const throw_error_1 = require("@uxf/core/utils/throw-error");
|
|
6
|
+
const fs_1 = require("fs");
|
|
6
7
|
const path_1 = require("path");
|
|
7
8
|
const process_1 = require("process");
|
|
8
9
|
const true_case_path_1 = require("true-case-path");
|
|
@@ -19,6 +20,33 @@ function fileExists(path) {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
function findAppRoute(dir, parts) {
|
|
24
|
+
if (parts.length === 0) {
|
|
25
|
+
if (fileExists((0, path_1.join)(dir, "page.ts")) || fileExists((0, path_1.join)(dir, "route.ts"))) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
if (findAppRoute((0, path_1.join)(dir, parts[0]), parts.slice(1))) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
entries = (0, fs_1.readdirSync)(dir);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (entry.startsWith("(") && entry.endsWith(")")) {
|
|
43
|
+
if (findAppRoute((0, path_1.join)(dir, entry), parts)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
22
50
|
let hasError = false;
|
|
23
51
|
function routesCheck(routes, options) {
|
|
24
52
|
Object.entries(routes).forEach(([routeName, routeDefinition]) => {
|
|
@@ -40,10 +68,7 @@ function routesCheck(routes, options) {
|
|
|
40
68
|
if (fileExists(pagePath)) {
|
|
41
69
|
return;
|
|
42
70
|
}
|
|
43
|
-
if (
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (fileExists((0, path_1.join)(options.appDir, ...pathParts, "route.ts"))) {
|
|
71
|
+
if (findAppRoute(options.appDir, pathParts)) {
|
|
47
72
|
return;
|
|
48
73
|
}
|
|
49
74
|
throw new Error("Invalid");
|
|
@@ -13,6 +13,8 @@ describe("routesCheck", () => {
|
|
|
13
13
|
app1: { path: "/app-directory" },
|
|
14
14
|
app2: { path: "/app-directory/[param]" },
|
|
15
15
|
app3: { path: "/api" },
|
|
16
|
+
app4: { path: "/grouped-route" }, // page lives inside a (group) folder
|
|
17
|
+
app5: { path: "/nested/deep" }, // page lives inside nested (group) folders
|
|
16
18
|
}, {
|
|
17
19
|
shouldProcessExit: false,
|
|
18
20
|
appDir: (0, path_1.join)(__dirname, "__test__", "app"),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const superstruct_1 = require("superstruct");
|
|
4
|
-
const
|
|
5
|
-
const { createSitemapGenerator, routeToUrl } = (0,
|
|
4
|
+
const create_router_1 = require("./create-router");
|
|
5
|
+
const { createSitemapGenerator, routeToUrl } = (0, create_router_1.createRouter)({
|
|
6
6
|
index: {
|
|
7
7
|
path: "/",
|
|
8
8
|
schema: (0, superstruct_1.object)({
|
|
File without changes
|