@uxf/router 11.64.3 → 11.72.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 CHANGED
@@ -14,27 +14,53 @@ yarn add @uxf/router
14
14
 
15
15
  import { createRouter } from "@uxf/router";
16
16
 
17
- interface RouteList {
18
- index: null;
19
- "admin/index": { param1?: number };
20
- "blog/detail": { id: number };
21
- }
22
-
23
- export default createRouter<RouteList>({
24
- index: "/",
25
- "admin/index": "/admin",
26
- "blog/detail": "/blog/[id]",
27
- });
17
+ export default createRouter(
18
+ {
19
+ index: {
20
+ path: "/"
21
+ },
22
+ "admin/index": {
23
+ path: "/admin",
24
+ schema: object({
25
+ param1: optional(number())
26
+ })
27
+ },
28
+ "blog/detail": {
29
+ path: "/blog/[id]",
30
+ schema: object({
31
+ id: number()
32
+ })
33
+ },
34
+ "localized-route": {
35
+ path: {
36
+ en: "/en/home",
37
+ cs: "/cs/domu"
38
+ },
39
+ schema: object({
40
+ term: optional(string()),
41
+ })
42
+ }
43
+ } as const,
44
+ {
45
+ locales: ["cs", "en"],
46
+ baseUrl: "https://www.uxf.cz"
47
+ } as const
48
+ );
28
49
  ```
29
50
 
30
51
  ```ts
31
52
  // routes/index.ts
32
53
 
33
- import router, { RouteList } from "./routes";
54
+ import router from "./routes";
34
55
  import { UxfGetServerSideProps, UxfGetStaticProps } from "@uxf/router";
35
56
  import { PreviewData as NextPreviewData } from "next/types";
36
57
 
37
- export const { route, routeToUrl, sitemapGenerator, useQueryParams } = router;
58
+ export const {
59
+ routeToUrl,
60
+ sitemapGenerator,
61
+ useQueryParams,
62
+ useQueryParamsStatic
63
+ } = router;
38
64
 
39
65
  export type GetStaticProps<
40
66
  Route extends keyof RouteList,
@@ -67,12 +93,15 @@ Add configuration to `tsconfig.json`
67
93
  ## useQueryParams
68
94
 
69
95
  ```tsx
70
- import {useQueryParams} from "@app-routes";
71
- import {queryParamToNumber} from "./helper";
96
+ import { useQueryParams } from "@app-routes";
97
+ import { queryParamToNumber } from "./helper";
72
98
 
73
- const params = useQueryParams<"route-name">()
99
+ // can be used on SSR pages
100
+ const [query, { push, replace }] = useQueryParams("route-name");
74
101
 
75
- const id = queryParamToNumber(params.id);
102
+ // must be used on static pages, because router is not ready on first render
103
+ // query is null if router is not ready
104
+ const [query, { push, replace }] = useQueryParamsStatic("route-name");
76
105
  ```
77
106
 
78
107
  ## Next Link
@@ -81,10 +110,10 @@ const id = queryParamToNumber(params.id);
81
110
  // pages/index.js
82
111
 
83
112
  import Link from "next/link";
84
- import { route } from "@app-routes";
113
+ import { routeToUrl } from "@app-routes";
85
114
 
86
115
  export default () => (
87
- <Link href={route("blog/detail", { id: 12 })}>
116
+ <Link href={routeToUrl("blog/detail", { id: 12 })}>
88
117
  Hello world
89
118
  </Link>
90
119
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/router",
3
- "version": "11.64.3",
3
+ "version": "11.72.0",
4
4
  "description": "UXF Router",
5
5
  "author": "UXFans <dev@uxf.cz>",
6
6
  "homepage": "https://gitlab.com/uxf-npm/router#readme",
@@ -23,7 +23,8 @@
23
23
  "url": "https://gitlab.com/uxf-npm/router/issues"
24
24
  },
25
25
  "dependencies": {
26
- "qs": "6.13.0"
26
+ "qs": "6.13.0",
27
+ "superstruct": "2.0.2"
27
28
  },
28
29
  "peerDependencies": {
29
30
  "next": ">= 12"
package/router.d.ts CHANGED
@@ -1,22 +1,52 @@
1
1
  import { LinkProps } from "next/link";
2
+ import { Infer, Struct } from "superstruct";
2
3
  import { SitemapGeneratorOptions, SitemapGeneratorType } from "./sitemap-generator";
3
- import { QueryParams } from "./types";
4
- export type FunctionParametersGenerator<RouteList> = {
5
- [K in keyof RouteList]: RouteList[K] extends null ? [K] : [K, RouteList[K]];
4
+ import { QueryParams, RoutesDefinition } from "./types";
5
+ type ExtractSchema<T> = T extends {
6
+ schema: Struct<infer U, any>;
7
+ } ? U : null;
8
+ type Options = {
9
+ shouldBeAbsolute?: boolean;
10
+ };
11
+ type LocaleOptions<Locales extends string[]> = {
12
+ locale: Locales[number];
13
+ };
14
+ export type FunctionParametersGenerator<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = {
15
+ [K in keyof RouteList]: RouteList[K]["path"] extends string ? RouteList[K] extends {
16
+ schema: undefined;
17
+ } ? [K, null?, Options?] : [K, ExtractSchema<RouteList[K]>?, Options?] : RouteList[K] extends {
18
+ schema: undefined;
19
+ } ? [K, null, Options & LocaleOptions<Locales>] : [K, ExtractSchema<RouteList[K]>, Options & LocaleOptions<Locales>];
6
20
  }[keyof RouteList];
7
- type RouteFunction<RouteList> = (...args: FunctionParametersGenerator<RouteList>) => LinkProps["href"];
8
- type RouteToUrlFunction<RouteList> = (...args: FunctionParametersGenerator<RouteList>) => string;
9
- type Router<RouteList> = {
10
- route: RouteFunction<RouteList>;
11
- routeToUrl: RouteToUrlFunction<RouteList>;
12
- createSitemapGenerator: (options?: SitemapGeneratorOptions) => SitemapGeneratorType<RouteList>;
13
- routes: {
14
- [key in keyof RouteList]: string;
15
- };
16
- useActiveRoute: () => keyof RouteList;
17
- useQueryParams: <T extends keyof RouteList>() => QueryParams<RouteList, T>;
21
+ type RouteFunction<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = (...args: FunctionParametersGenerator<Locales, RouteList>) => LinkProps["href"];
22
+ type RouteToUrlFunction<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = (...args: FunctionParametersGenerator<Locales, RouteList>) => string;
23
+ type QueryParamsResult<Nullable extends boolean, T extends keyof RouteList, Locales extends string[], RouteList extends RoutesDefinition<Locales>> = [
24
+ Nullable extends true ? Infer<NonNullable<RouteList[T]["schema"]>> | null : Infer<NonNullable<RouteList[T]["schema"]>>,
25
+ {
26
+ push: (params: Infer<NonNullable<RouteList[T]["schema"]>>) => Promise<boolean>;
27
+ replace: (params: Infer<NonNullable<RouteList[T]["schema"]>>) => Promise<boolean>;
28
+ }
29
+ ];
30
+ type Router<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = {
31
+ route: RouteFunction<Locales, RouteList>;
32
+ routeToUrl: RouteToUrlFunction<Locales, RouteList>;
33
+ createSitemapGenerator: (options?: SitemapGeneratorOptions) => SitemapGeneratorType<Locales, RouteList>;
34
+ routes: RouteList;
35
+ useActiveRoute: () => ActiveRoute<Locales, RouteList>;
36
+ /**
37
+ * @deprecated use useQueryParamsStatic or useQueryParams instead
38
+ */
39
+ useQueryParamsDeprecated: <T extends keyof RouteList>() => QueryParams<RouteList, T>;
40
+ useQueryParams: <T extends keyof RouteList>(routeName: T) => QueryParamsResult<false, T, Locales, RouteList>;
41
+ useQueryParamsStatic: <T extends keyof RouteList>(routeName: T) => QueryParamsResult<true, T, Locales, RouteList>;
42
+ };
43
+ type RouterOptions<L extends string[]> = {
44
+ baseUrl?: string;
45
+ locales?: L;
46
+ };
47
+ type ActiveRoute<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = {
48
+ route: keyof RouteList;
49
+ locale: Locales[number] | null;
18
50
  };
19
- export declare function createRouter<RouteList>(routes: {
20
- [key in keyof RouteList]: string;
21
- }): Router<RouteList>;
51
+ export declare function createRouter<Locales extends string[], RouteList extends RoutesDefinition<Locales>>(routes: RouteList, routerOptions: RouterOptions<Locales>): Router<Locales, RouteList>;
22
52
  export {};
package/router.js CHANGED
@@ -2,18 +2,38 @@
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 throw_error_1 = require("@uxf/core/utils/throw-error");
5
6
  const router_1 = require("next/router");
6
7
  const qs_1 = require("qs");
8
+ const superstruct_1 = require("superstruct");
7
9
  const sitemap_generator_1 = require("./sitemap-generator");
8
- function createRouter(routes) {
10
+ /**
11
+ * Arguments can be:
12
+ *
13
+ * [routeName, params, options]
14
+ */
15
+ function decodeArgs(args) {
16
+ return { route: args[0], params: args[1], options: args[2] };
17
+ }
18
+ function createRouter(routes, routerOptions) {
9
19
  return {
10
- route: (route, params = undefined) => ({
11
- pathname: routes[route],
12
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
13
- query: params !== null && params !== void 0 ? params : null,
14
- }),
15
- routeToUrl: (route, params = undefined) => {
16
- let pathname = routes[route];
20
+ route(...args) {
21
+ var _a;
22
+ const { route, params, options } = decodeArgs(args);
23
+ return {
24
+ pathname: typeof routes[route].path === "string"
25
+ ? routes[route].path
26
+ : routes[route].path[(_a = options === null || options === void 0 ? void 0 : options.locale) !== null && _a !== void 0 ? _a : (0, throw_error_1.throwError)("Locale is required")],
27
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
28
+ query: params !== null && params !== void 0 ? params : null,
29
+ };
30
+ },
31
+ routeToUrl(...args) {
32
+ var _a;
33
+ const { route, params, options } = decodeArgs(args);
34
+ let pathname = typeof routes[route].path === "string"
35
+ ? routes[route].path
36
+ : routes[route].path[(_a = options === null || options === void 0 ? void 0 : options.locale) !== null && _a !== void 0 ? _a : (0, throw_error_1.throwError)("Locale is required")];
17
37
  if (!pathname) {
18
38
  throw new Error(`Route '${String(route)}' not found.`);
19
39
  }
@@ -61,18 +81,55 @@ function createRouter(routes) {
61
81
  if (Object.keys(restParams).length > 0) {
62
82
  pathname = `${pathname}?${(0, qs_1.stringify)(restParams, { arrayFormat: "repeat" })}`;
63
83
  }
64
- return pathname;
84
+ return (options === null || options === void 0 ? void 0 : options.shouldBeAbsolute) ? `${routerOptions.baseUrl}${pathname}` : pathname;
65
85
  },
66
- useActiveRoute: () => {
86
+ useActiveRoute() {
87
+ var _a;
67
88
  const router = (0, router_1.useRouter)();
68
- const activeRoute = Object.keys(routes).find((route) => routes[route] === router.pathname);
89
+ const activeRoute = Object.keys(routes).find((route) => typeof routes[route].path === "string"
90
+ ? routes[route].path === router.pathname
91
+ : Object.values(routes[route].path).includes(router.pathname));
69
92
  if (!activeRoute) {
70
93
  throw new Error("Active route not found.");
71
94
  }
72
- return activeRoute;
95
+ return {
96
+ route: activeRoute,
97
+ locale: ((_a = router.locale) !== null && _a !== void 0 ? _a : null),
98
+ };
73
99
  },
74
100
  createSitemapGenerator: sitemap_generator_1.createSitemapGenerator,
75
101
  routes,
76
- useQueryParams: () => (0, router_1.useRouter)().query,
102
+ useQueryParamsDeprecated: () => (0, router_1.useRouter)().query,
103
+ useQueryParams(routeName) {
104
+ const router = (0, router_1.useRouter)();
105
+ const schema = routes[routeName].schema;
106
+ if (!schema) {
107
+ throw new Error(`Route '${String(routeName)}' has no schema.`);
108
+ }
109
+ if (router.isReady) {
110
+ throw new Error("Router is not ready. Use useQueryParamsStatic instead of useQueryParams.");
111
+ }
112
+ return [
113
+ (0, superstruct_1.mask)(router.query, schema),
114
+ {
115
+ push: (params) => router.push(this.routeToUrl(routeName, params, {})),
116
+ replace: (params) => router.replace(this.routeToUrl(routeName, params, {})),
117
+ },
118
+ ];
119
+ },
120
+ useQueryParamsStatic(routeName) {
121
+ const router = (0, router_1.useRouter)();
122
+ const schema = routes[routeName].schema;
123
+ if (!schema) {
124
+ throw new Error(`Route '${String(routeName)}' has no schema.`);
125
+ }
126
+ return [
127
+ router.isReady ? (0, superstruct_1.mask)(router.query, schema) : null,
128
+ {
129
+ push: (params) => router.push(this.routeToUrl(routeName, params, {})),
130
+ replace: (params) => router.replace(this.routeToUrl(routeName, params, {})),
131
+ },
132
+ ];
133
+ },
77
134
  };
78
135
  }
package/router.test.js CHANGED
@@ -1,20 +1,129 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const superstruct_1 = require("superstruct");
3
4
  const router_1 = require("./router");
5
+ const superstruct_2 = require("./superstruct");
4
6
  const { routeToUrl } = (0, router_1.createRouter)({
5
- index: "/",
6
- catchAllSegments: "/catch-all/[...pathParams]",
7
- optionalCatchAllSegments: "/catch-all-optional/[[...pathParams]]",
7
+ index: {
8
+ path: "/",
9
+ schema: (0, superstruct_1.object)({
10
+ queryParam: (0, superstruct_1.optional)((0, superstruct_1.string)()),
11
+ }),
12
+ },
13
+ catchAllSegments: {
14
+ path: "/catch-all/[...pathParams]",
15
+ schema: (0, superstruct_1.object)({
16
+ pathParams: (0, superstruct_2.array)((0, superstruct_1.string)()),
17
+ queryParam: (0, superstruct_1.optional)((0, superstruct_1.string)()),
18
+ }),
19
+ },
20
+ optionalCatchAllSegments: {
21
+ path: "/catch-all-optional/[[...pathParams]]",
22
+ schema: (0, superstruct_1.object)({
23
+ pathParams: (0, superstruct_1.optional)((0, superstruct_1.nullable)((0, superstruct_1.union)([(0, superstruct_2.array)((0, superstruct_1.string)()), (0, superstruct_1.string)()]))),
24
+ queryParam: (0, superstruct_1.optional)((0, superstruct_1.string)()),
25
+ }),
26
+ },
27
+ routeWithoutParams: {
28
+ path: "/route-without-params",
29
+ },
30
+ localizedRouteWithoutParams: {
31
+ path: {
32
+ cs: "/lokalizovany-odkaz",
33
+ en: "/localized-url",
34
+ },
35
+ },
36
+ localizedRouteWithParams: {
37
+ path: {
38
+ cs: "/lokalizovany-odkaz-s-parametry",
39
+ en: "/localized-url-with-parameters",
40
+ },
41
+ schema: (0, superstruct_1.object)({
42
+ test: (0, superstruct_1.string)(),
43
+ }),
44
+ },
45
+ }, {
46
+ locales: ["cs", "en"],
47
+ baseUrl: "https://www.uxf.cz",
8
48
  });
49
+ const DATA = [
50
+ {
51
+ actual: routeToUrl("index", {}),
52
+ expected: "/",
53
+ },
54
+ {
55
+ actual: routeToUrl("index", { queryParam: "value" }),
56
+ expected: "/?queryParam=value",
57
+ },
58
+ {
59
+ actual: routeToUrl("catchAllSegments", { pathParams: ["param1", "param2"] }),
60
+ expected: "/catch-all/param1/param2",
61
+ },
62
+ {
63
+ actual: routeToUrl("catchAllSegments", { pathParams: ["param1", "param2"], queryParam: "value" }),
64
+ expected: "/catch-all/param1/param2?queryParam=value",
65
+ },
66
+ {
67
+ actual: routeToUrl("optionalCatchAllSegments", { pathParams: ["param1", "param2"], queryParam: "value" }),
68
+ expected: "/catch-all-optional/param1/param2?queryParam=value",
69
+ },
70
+ {
71
+ actual: routeToUrl("optionalCatchAllSegments", { pathParams: [] }),
72
+ expected: "/catch-all-optional",
73
+ },
74
+ {
75
+ actual: routeToUrl("optionalCatchAllSegments", { pathParams: null }),
76
+ expected: "/catch-all-optional",
77
+ },
78
+ {
79
+ actual: routeToUrl("optionalCatchAllSegments", { pathParams: undefined }),
80
+ expected: "/catch-all-optional",
81
+ },
82
+ {
83
+ actual: routeToUrl("optionalCatchAllSegments", { pathParams: "" }),
84
+ expected: "/catch-all-optional",
85
+ },
86
+ {
87
+ actual: routeToUrl("optionalCatchAllSegments", { pathParams: "", queryParam: "value" }),
88
+ expected: "/catch-all-optional?queryParam=value",
89
+ },
90
+ {
91
+ actual: routeToUrl("routeWithoutParams"),
92
+ expected: "/route-without-params",
93
+ },
94
+ {
95
+ actual: routeToUrl("localizedRouteWithoutParams", null, { locale: "en" }),
96
+ expected: "/localized-url",
97
+ },
98
+ {
99
+ actual: routeToUrl("localizedRouteWithoutParams", null, { locale: "cs" }),
100
+ expected: "/lokalizovany-odkaz",
101
+ },
102
+ {
103
+ actual: routeToUrl("localizedRouteWithParams", { test: "value" }, { locale: "en" }),
104
+ expected: "/localized-url-with-parameters?test=value",
105
+ },
106
+ {
107
+ actual: routeToUrl("localizedRouteWithParams", { test: "value" }, { locale: "cs" }),
108
+ expected: "/lokalizovany-odkaz-s-parametry?test=value",
109
+ },
110
+ {
111
+ actual: routeToUrl("index", { queryParam: "value" }, { shouldBeAbsolute: true }),
112
+ expected: "https://www.uxf.cz/?queryParam=value",
113
+ },
114
+ {
115
+ actual: routeToUrl("routeWithoutParams", null, { shouldBeAbsolute: true }),
116
+ expected: "https://www.uxf.cz/route-without-params",
117
+ },
118
+ {
119
+ actual: routeToUrl("localizedRouteWithoutParams", null, { locale: "cs", shouldBeAbsolute: true }),
120
+ expected: "https://www.uxf.cz/lokalizovany-odkaz",
121
+ },
122
+ {
123
+ actual: routeToUrl("localizedRouteWithParams", { test: "value" }, { locale: "en", shouldBeAbsolute: true }),
124
+ expected: "https://www.uxf.cz/localized-url-with-parameters?test=value",
125
+ },
126
+ ];
9
127
  test("routeToUrl", () => {
10
- expect(routeToUrl("index", {})).toBe("/");
11
- expect(routeToUrl("index", { queryParam: "value" })).toBe("/?queryParam=value");
12
- expect(routeToUrl("catchAllSegments", { pathParams: ["param1", "param2"] })).toBe("/catch-all/param1/param2");
13
- expect(routeToUrl("catchAllSegments", { pathParams: ["param1", "param2"], queryParam: "value" })).toBe("/catch-all/param1/param2?queryParam=value");
14
- expect(routeToUrl("optionalCatchAllSegments", { pathParams: ["param1", "param2"], queryParam: "value" })).toBe("/catch-all-optional/param1/param2?queryParam=value");
15
- expect(routeToUrl("optionalCatchAllSegments", { pathParams: [] })).toBe("/catch-all-optional");
16
- expect(routeToUrl("optionalCatchAllSegments", { pathParams: null })).toBe("/catch-all-optional");
17
- expect(routeToUrl("optionalCatchAllSegments", { pathParams: undefined })).toBe("/catch-all-optional");
18
- expect(routeToUrl("optionalCatchAllSegments", { pathParams: "" })).toBe("/catch-all-optional");
19
- expect(routeToUrl("optionalCatchAllSegments", { pathParams: "", queryParam: "value" })).toBe("/catch-all-optional?queryParam=value");
128
+ DATA.map(({ actual, expected }) => expect(actual).toBe(expected));
20
129
  });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ts-node-script
2
+ export declare function routesCheck(routes: Record<string, string>, shouldProcessExit?: boolean): void;
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env ts-node-script
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.routesCheck = routesCheck;
8
+ const is_not_empty_1 = require("@uxf/core/utils/is-not-empty");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = require("path");
11
+ const true_case_path_1 = require("true-case-path");
12
+ let error = false;
13
+ function fileExists(path) {
14
+ try {
15
+ return Boolean((0, true_case_path_1.trueCasePathSync)(path));
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ function directoryExists(path) {
22
+ try {
23
+ const stats = fs_1.default.lstatSync(path);
24
+ return stats.isDirectory();
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Filters out folders used for organizational purposes in the app folder
32
+ * such as parallel routes (e.g., (group)) and dynamic routes ([param]).
33
+ */
34
+ function cleanPathSegments(pathSegments) {
35
+ return pathSegments.filter((segment) => !segment.startsWith("(") && !segment.endsWith(")"));
36
+ }
37
+ function normalizeDynamicSegment(segment) {
38
+ return (segment === null || segment === void 0 ? void 0 : segment.startsWith("[")) ? "index" : segment || "";
39
+ }
40
+ /**
41
+ * Checks for collisions in the path (in `baseDir`) * between the `segment.tsx` file and the `segment/` folder within one level.
42
+ *
43
+ * Example:
44
+ * - route `/payments/export` gives segments ["payments", "export"]
45
+ * - check if it exists at the same time `pages/payments.tsx` and `pages/payments/`.
46
+ */
47
+ function checkFolderFileCollision(baseDir, pathSegments) {
48
+ for (let i = 0; i < pathSegments.length - 1; i++) {
49
+ const segment = pathSegments[i];
50
+ const parentDir = (0, path_1.join)(baseDir, ...pathSegments.slice(0, i));
51
+ const filePath = (0, path_1.join)(parentDir, `${segment}.tsx`);
52
+ const folderPath = (0, path_1.join)(parentDir, segment);
53
+ if (fileExists(filePath) && directoryExists(folderPath)) {
54
+ // eslint-disable-next-line no-console
55
+ console.log(`Colission: For segment '${segment}' there is file and folder at the same time.\n` +
56
+ `Replace file ${segment}.tsx with ${segment}/index.tsx.`);
57
+ throw new Error("Route colission");
58
+ }
59
+ }
60
+ }
61
+ function routesCheck(routes, shouldProcessExit = true) {
62
+ Object.keys(routes).forEach((route) => {
63
+ const pathname = routes[route];
64
+ try {
65
+ const basePath = (0, path_1.join)(__dirname, "..", "..", "src", "pages");
66
+ const appBasePath = (0, path_1.join)(__dirname, "..", "..", "src", "app");
67
+ const rawPathSegments = pathname.substring(1).split("/");
68
+ const pathSegments = cleanPathSegments(rawPathSegments);
69
+ checkFolderFileCollision(basePath, pathSegments);
70
+ checkFolderFileCollision(appBasePath, pathSegments);
71
+ // Find the last segment (e.g. for /payments/export it will be "export")
72
+ const lastPart = pathSegments.splice(-1);
73
+ const normalizedLastPart = normalizeDynamicSegment(lastPart.at(0));
74
+ // --- Checks in `pages` folder ---
75
+ // 1) path like pages/product.tsx (if it's at "first level")
76
+ const firstLevelPagePath = (0, path_1.join)(basePath, `${normalizedLastPart}.tsx`);
77
+ /// 2) path type pages/product/index.tsx
78
+ const firstLevelFolderPath = (0, path_1.join)(basePath, normalizedLastPart, "index.tsx");
79
+ // 3) paths for nested pages (pages/payments/export.tsx etc.)
80
+ const nestedPaths = (0, path_1.join)(basePath, ...pathSegments, `${normalizedLastPart || "index"}.tsx`);
81
+ const pagePath = (0, is_not_empty_1.isNotEmpty)(pathSegments)
82
+ ? nestedPaths
83
+ : fileExists(firstLevelFolderPath)
84
+ ? firstLevelFolderPath
85
+ : firstLevelPagePath;
86
+ // --- Checks in `app folder ` ---
87
+ const appPath = (0, path_1.join)(appBasePath, ...pathSegments, normalizedLastPart, "page.tsx");
88
+ // Check if the path exists in either `pages` or `app`
89
+ if (fileExists(pagePath) || fileExists(appPath)) {
90
+ return; // Valid route
91
+ }
92
+ throw new Error("Missing file for route!");
93
+ }
94
+ catch (err) {
95
+ // eslint-disable-next-line no-console
96
+ console.log("❌ Invalid route: " + pathname);
97
+ if (err instanceof Error) {
98
+ // eslint-disable-next-line no-console
99
+ console.error(err.message);
100
+ }
101
+ error = true;
102
+ }
103
+ });
104
+ if (error) {
105
+ if (shouldProcessExit) {
106
+ process.exit(1);
107
+ }
108
+ throw new Error("At least one invalid route found");
109
+ }
110
+ else {
111
+ if (shouldProcessExit) {
112
+ process.exit(0);
113
+ }
114
+ // eslint-disable-next-line no-console
115
+ console.log("All routes checked.");
116
+ }
117
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = require("path");
8
+ const true_case_path_1 = require("true-case-path");
9
+ const routes_check_1 = require("./routes-check");
10
+ jest.mock("fs", () => {
11
+ // Access the actual fs module so we can spread it
12
+ const actualFs = jest.requireActual("fs");
13
+ return {
14
+ ...actualFs,
15
+ // We'll mock only the function(s) we need
16
+ lstatSync: jest.fn(),
17
+ };
18
+ });
19
+ jest.mock("true-case-path", () => ({
20
+ trueCasePathSync: jest.fn(),
21
+ }));
22
+ const mockTrueCasePathSync = true_case_path_1.trueCasePathSync;
23
+ /**
24
+ * Helper function to mock:
25
+ * 1) Which file paths "exist"
26
+ * 2) Which directory paths "exist"
27
+ */
28
+ function mockFileExists(knownPaths, knownDirs = []) {
29
+ // Mock the trueCasePathSync behavior (our fileExists check)
30
+ mockTrueCasePathSync.mockImplementation((inputPath) => {
31
+ if (knownPaths.includes(inputPath)) {
32
+ return inputPath; // indicates file exists
33
+ }
34
+ throw new Error(`Path not found: ${inputPath}`);
35
+ });
36
+ // Mock lstatSync for directory checks
37
+ fs_1.default.lstatSync.mockImplementation((inputPath) => {
38
+ if (knownDirs.includes(inputPath)) {
39
+ return { isDirectory: () => true };
40
+ }
41
+ // if it's in knownPaths but not in knownDirs => treat it as a file
42
+ if (knownPaths.includes(inputPath)) {
43
+ return { isDirectory: () => false };
44
+ }
45
+ // otherwise, it's not found => throw
46
+ throw new Error(`No such file or directory: ${inputPath}`);
47
+ });
48
+ }
49
+ const rootDirPages = (0, path_1.join)(__dirname, "..", "..", "src", "pages");
50
+ const rootDirApp = (0, path_1.join)(__dirname, "..", "..", "src", "app");
51
+ describe("routesCheck", () => {
52
+ beforeEach(() => {
53
+ jest.clearAllMocks();
54
+ });
55
+ it("should handle multiple valid routes at once (pages + app)", () => {
56
+ /**
57
+ * 1) /product => pages/product/index.tsx
58
+ * 2) /product/edit => pages/product/edit.tsx
59
+ * 3) /product/[id] => pages/product/[id].tsx
60
+ * 4) /dashboard => app/dashboard/page.tsx
61
+ * 5) /settings/privacy => app/settings/privacy/page.tsx
62
+ */
63
+ mockFileExists([
64
+ // pages
65
+ (0, path_1.join)(rootDirPages, "product", "index.tsx"),
66
+ (0, path_1.join)(rootDirPages, "product", "edit.tsx"),
67
+ (0, path_1.join)(rootDirPages, "product", "[id].tsx"),
68
+ // app
69
+ (0, path_1.join)(rootDirApp, "dashboard", "page.tsx"),
70
+ (0, path_1.join)(rootDirApp, "settings", "privacy", "page.tsx"),
71
+ ], [
72
+ // directories
73
+ (0, path_1.join)(rootDirPages, "product"),
74
+ (0, path_1.join)(rootDirApp, "dashboard"),
75
+ (0, path_1.join)(rootDirApp, "settings"),
76
+ (0, path_1.join)(rootDirApp, "settings", "privacy"),
77
+ ]);
78
+ const routes = {
79
+ product: "/product",
80
+ productEdit: "/product/edit",
81
+ productDetail: "/product/[id]",
82
+ dashboard: "/dashboard",
83
+ privacy: "/settings/privacy",
84
+ };
85
+ expect(() => (0, routes_check_1.routesCheck)(routes, false)).not.toThrow();
86
+ });
87
+ it("should fail when invalid routes are provided", () => {
88
+ /**
89
+ * We'll test multiple routes that do not exist in either pages or app:
90
+ * 1) /unknown
91
+ * 2) /product/unknown
92
+ */
93
+ // Intentionally not mocking any matching paths here
94
+ mockFileExists([]);
95
+ const routes = {
96
+ unknown: "/unknown",
97
+ productUnknown: "/product/unknown",
98
+ };
99
+ expect(() => (0, routes_check_1.routesCheck)(routes, false)).toThrow();
100
+ });
101
+ it("Pages folder: should detect collisions (file + folder) in multiple segments", () => {
102
+ /**
103
+ * Testing collision scenarios together:
104
+ * 1) /payments/export in pages => collision with payments.tsx + payments/ folder
105
+ */
106
+ mockFileExists([
107
+ // pages collisions
108
+ (0, path_1.join)(rootDirPages, "payments.tsx"),
109
+ (0, path_1.join)(rootDirPages, "payments", "export.tsx"),
110
+ ], [(0, path_1.join)(rootDirPages, "payments")]);
111
+ const routes = {
112
+ paymentsExport: "/payments/export",
113
+ };
114
+ expect(() => (0, routes_check_1.routesCheck)(routes, false)).toThrow();
115
+ });
116
+ it("App folder: should detect that page.tsx is not created for the route", () => {
117
+ /**
118
+ * Testing collision scenarios together:
119
+ * 1) /admin/analytics in app => collision with analytics.tsx + analytics/ folder
120
+ */
121
+ mockFileExists([
122
+ // app collisions
123
+ (0, path_1.join)(rootDirApp, "admin", "analytics.tsx"),
124
+ ], [(0, path_1.join)(rootDirApp, "admin"), (0, path_1.join)(rootDirApp, "admin", "analytics")]);
125
+ const routes = {
126
+ adminAnalytics: "/admin/analytics",
127
+ };
128
+ expect(() => (0, routes_check_1.routesCheck)(routes, false)).toThrow();
129
+ });
130
+ });
@@ -1,3 +1,4 @@
1
+ import { RoutesDefinition } from "./types";
1
2
  type ChangeFrequency = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
2
3
  export type SitemapItem = {
3
4
  loc: string;
@@ -11,10 +12,10 @@ type MissingRoutesError<Routes> = {
11
12
  __nonExhaustive: never;
12
13
  missingRoutes: Routes;
13
14
  };
14
- export type SitemapGeneratorType<RouteList> = {
15
- add: <Route extends keyof RouteList>(route: Route, resolver: RouteResolver<Route>) => SitemapGeneratorType<Omit<RouteList, Route>>;
16
- skip: <Route extends keyof RouteList>(route: Route) => SitemapGeneratorType<Omit<RouteList, Route>>;
17
- exhaustive: keyof RouteList extends never ? () => SitemapGeneratorType<RouteList> : MissingRoutesError<keyof RouteList>;
15
+ export type SitemapGeneratorType<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = {
16
+ add: <Route extends keyof RouteList>(route: Route, resolver: RouteResolver<Route>) => SitemapGeneratorType<Locales, Omit<RouteList, Route>>;
17
+ skip: <Route extends keyof RouteList>(route: Route) => SitemapGeneratorType<Locales, Omit<RouteList, Route>>;
18
+ exhaustive: keyof RouteList extends never ? () => SitemapGeneratorType<Locales, RouteList> : MissingRoutesError<keyof RouteList>;
18
19
  toXml: keyof RouteList extends never ? () => Promise<string> : MissingRoutesError<keyof RouteList>;
19
20
  toJson: keyof RouteList extends never ? () => Promise<any> : MissingRoutesError<keyof RouteList>;
20
21
  };
@@ -23,5 +24,5 @@ export type SitemapGeneratorOptions = {
23
24
  defaultPriority?: number;
24
25
  defaultChangeFreq?: ChangeFrequency;
25
26
  };
26
- export declare const createSitemapGenerator: <RouteList>(options?: SitemapGeneratorOptions) => SitemapGeneratorType<RouteList>;
27
+ export declare const createSitemapGenerator: <Locales extends string[], RouteList extends RoutesDefinition<Locales>>(options?: SitemapGeneratorOptions) => SitemapGeneratorType<Locales, RouteList>;
27
28
  export {};
@@ -1,11 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const superstruct_1 = require("superstruct");
3
4
  const router_1 = require("./router");
4
5
  const { createSitemapGenerator, routeToUrl } = (0, router_1.createRouter)({
5
- index: "/",
6
- catchAllSegments: "/catch-all/[...pathParams]",
7
- optionalCatchAllSegments: "/catch-all-optional/[[...pathParams]]",
8
- });
6
+ index: {
7
+ path: "/",
8
+ schema: (0, superstruct_1.object)({
9
+ queryParam: (0, superstruct_1.optional)((0, superstruct_1.string)()),
10
+ }),
11
+ },
12
+ catchAllSegments: {
13
+ path: "/catch-all/[...pathParams]",
14
+ schema: (0, superstruct_1.object)({
15
+ pathParams: (0, superstruct_1.array)(),
16
+ queryParam: (0, superstruct_1.optional)((0, superstruct_1.string)()),
17
+ }),
18
+ },
19
+ optionalCatchAllSegments: {
20
+ path: "/catch-all-optional/[[...pathParams]]",
21
+ schema: (0, superstruct_1.object)({
22
+ pathParams: (0, superstruct_1.optional)((0, superstruct_1.nullable)((0, superstruct_1.union)([(0, superstruct_1.array)(), (0, superstruct_1.string)()]))),
23
+ queryParam: (0, superstruct_1.optional)((0, superstruct_1.string)()),
24
+ }),
25
+ },
26
+ }, { baseUrl: "https://www.uxf.cz" });
9
27
  const BASE_URL = "www.uxf.cz";
10
28
  test("routeToUrl", async () => {
11
29
  const xml = await createSitemapGenerator({ baseUrl: BASE_URL })
@@ -0,0 +1,7 @@
1
+ import { Infer, Struct } from "superstruct";
2
+ /**
3
+ * NextJS používá následující zápis pole v url `?item=a&item=b&item=c`
4
+ *
5
+ * Není teda schopen rozeznat, zda parametr `?item=a` je pole o jednom prvku nebo pouze string
6
+ */
7
+ export declare function array<T extends Struct<any>>(inner: T): Struct<Infer<T>[], null>;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.array = array;
4
+ const superstruct_1 = require("superstruct");
5
+ /**
6
+ * NextJS používá následující zápis pole v url `?item=a&item=b&item=c`
7
+ *
8
+ * Není teda schopen rozeznat, zda parametr `?item=a` je pole o jednom prvku nebo pouze string
9
+ */
10
+ function array(inner) {
11
+ return (0, superstruct_1.coerce)((0, superstruct_1.array)(inner), (0, superstruct_1.union)([(0, superstruct_1.string)(), (0, superstruct_1.array)((0, superstruct_1.string)())]), (value) => {
12
+ return Array.isArray(value) ? value : [value];
13
+ });
14
+ }
@@ -0,0 +1 @@
1
+ export declare const boolean: () => import("superstruct").Struct<boolean, null>;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.boolean = void 0;
4
+ const superstruct_1 = require("superstruct");
5
+ const boolean = () => (0, superstruct_1.coerce)((0, superstruct_1.boolean)(), (0, superstruct_1.string)(), (value) => {
6
+ return value === "1" || value === "true" || value === "";
7
+ });
8
+ exports.boolean = boolean;
@@ -0,0 +1,4 @@
1
+ export { object, optional, string } from "superstruct";
2
+ export * from "./array";
3
+ export * from "./boolean";
4
+ export * from "./integer";
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.string = exports.optional = exports.object = void 0;
18
+ var superstruct_1 = require("superstruct");
19
+ Object.defineProperty(exports, "object", { enumerable: true, get: function () { return superstruct_1.object; } });
20
+ Object.defineProperty(exports, "optional", { enumerable: true, get: function () { return superstruct_1.optional; } });
21
+ Object.defineProperty(exports, "string", { enumerable: true, get: function () { return superstruct_1.string; } });
22
+ __exportStar(require("./array"), exports);
23
+ __exportStar(require("./boolean"), exports);
24
+ __exportStar(require("./integer"), exports);
@@ -0,0 +1 @@
1
+ export declare const integer: () => import("superstruct").Struct<number, null>;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.integer = void 0;
4
+ const superstruct_1 = require("superstruct");
5
+ const integer = () => (0, superstruct_1.coerce)((0, superstruct_1.integer)(), (0, superstruct_1.string)(), (value) => {
6
+ const parsed = parseInt(value, 10);
7
+ if (isNaN(parsed)) {
8
+ throw new Error("Invalid number string");
9
+ }
10
+ return parsed;
11
+ });
12
+ exports.integer = integer;
package/types.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import { GetServerSideProps, GetStaticProps, PreviewData as NextPreviewData } from "next";
2
+ import { Struct } from "superstruct";
3
+ export type RoutesDefinition<Locales extends string[]> = Record<string, {
4
+ path: string | Record<Locales[number], string>;
5
+ schema?: Struct<any, any> | null;
6
+ }>;
2
7
  export type QueryParams<RouteList, Route extends keyof RouteList> = RouteList[Route] extends null ? never : {
3
8
  [key in keyof RouteList[Route]]: string | string[] | undefined;
4
9
  };