@uxf/router 11.72.1 → 11.72.3
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/package.json +1 -1
- package/router.d.ts +3 -7
- package/router.js +3 -1
- package/routes-check/routes-check.d.ts +10 -0
- package/routes-check/routes-check.js +65 -0
- package/routes-check/routes-check.test.js +30 -0
- package/sitemap-generator.d.ts +21 -11
- package/sitemap-generator.js +22 -27
- package/sitemap-generator.test.js +15 -9
- package/routes-check.d.ts +0 -2
- package/routes-check.js +0 -117
- package/routes-check.test.js +0 -130
- /package/{routes-check.test.d.ts → routes-check/routes-check.test.d.ts} +0 -0
package/package.json
CHANGED
package/router.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LinkProps } from "next/link";
|
|
2
2
|
import { Infer, Struct } from "superstruct";
|
|
3
|
-
import { SitemapGeneratorOptions, SitemapGeneratorType } from "./sitemap-generator";
|
|
3
|
+
import { SitemapGeneratorOptions, SitemapGeneratorType, SitemapRouteResolvers } from "./sitemap-generator";
|
|
4
4
|
import { QueryParams, RoutesDefinition } from "./types";
|
|
5
5
|
type ExtractSchema<T> = T extends {
|
|
6
6
|
schema: Struct<infer U, any>;
|
|
@@ -12,11 +12,7 @@ type LocaleOptions<Locales extends string[]> = {
|
|
|
12
12
|
locale: Locales[number];
|
|
13
13
|
};
|
|
14
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>];
|
|
15
|
+
[K in keyof RouteList]: RouteList[K]["path"] extends string ? "schema" extends keyof RouteList[K] ? [K, ExtractSchema<RouteList[K]>, Options?] : [K, null?, Options?] : "schema" extends keyof RouteList[K] ? [K, ExtractSchema<RouteList[K]>, Options & LocaleOptions<Locales>] : [K, null, Options & LocaleOptions<Locales>];
|
|
20
16
|
}[keyof RouteList];
|
|
21
17
|
type RouteFunction<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = (...args: FunctionParametersGenerator<Locales, RouteList>) => LinkProps["href"];
|
|
22
18
|
type RouteToUrlFunction<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = (...args: FunctionParametersGenerator<Locales, RouteList>) => string;
|
|
@@ -30,7 +26,7 @@ type QueryParamsResult<Nullable extends boolean, T extends keyof RouteList, Loca
|
|
|
30
26
|
type Router<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = {
|
|
31
27
|
route: RouteFunction<Locales, RouteList>;
|
|
32
28
|
routeToUrl: RouteToUrlFunction<Locales, RouteList>;
|
|
33
|
-
createSitemapGenerator: (options?: SitemapGeneratorOptions) => SitemapGeneratorType
|
|
29
|
+
createSitemapGenerator: (resolvers: SitemapRouteResolvers<Locales, RouteList>, options?: SitemapGeneratorOptions) => SitemapGeneratorType;
|
|
34
30
|
routes: RouteList;
|
|
35
31
|
useActiveRoute: () => ActiveRoute<Locales, RouteList>;
|
|
36
32
|
/**
|
package/router.js
CHANGED
|
@@ -97,7 +97,9 @@ function createRouter(routes, routerOptions) {
|
|
|
97
97
|
locale: ((_a = router.locale) !== null && _a !== void 0 ? _a : null),
|
|
98
98
|
};
|
|
99
99
|
},
|
|
100
|
-
createSitemapGenerator
|
|
100
|
+
createSitemapGenerator(resolvers, options) {
|
|
101
|
+
return new sitemap_generator_1.SitemapGenerator(resolvers, options);
|
|
102
|
+
},
|
|
101
103
|
routes,
|
|
102
104
|
useQueryParamsDeprecated: () => (0, router_1.useRouter)().query,
|
|
103
105
|
useQueryParams(routeName) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node-script
|
|
2
|
+
import { RoutesDefinition } from "../types";
|
|
3
|
+
interface Options {
|
|
4
|
+
appDir: string;
|
|
5
|
+
pagesDir: string;
|
|
6
|
+
shouldProcessExit?: boolean;
|
|
7
|
+
defaultLocale?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function routesCheck(routes: RoutesDefinition<any>, options: Options): void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node-script
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.routesCheck = routesCheck;
|
|
5
|
+
const throw_error_1 = require("@uxf/core/utils/throw-error");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const process_1 = require("process");
|
|
8
|
+
const true_case_path_1 = require("true-case-path");
|
|
9
|
+
function fileExists(path) {
|
|
10
|
+
try {
|
|
11
|
+
return Boolean((0, true_case_path_1.trueCasePathSync)(path));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
try {
|
|
15
|
+
return Boolean((0, true_case_path_1.trueCasePathSync)(path + "x"));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
let hasError = false;
|
|
23
|
+
function routesCheck(routes, options) {
|
|
24
|
+
Object.entries(routes).forEach(([routeName, routeDefinition]) => {
|
|
25
|
+
var _a;
|
|
26
|
+
const pathname = typeof routeDefinition.path === "string"
|
|
27
|
+
? routeDefinition.path
|
|
28
|
+
: routeDefinition.path[(_a = options.defaultLocale) !== null && _a !== void 0 ? _a : (0, throw_error_1.throwError)("Default locale must be provided")];
|
|
29
|
+
const pathParts = pathname.substr(1).split("/");
|
|
30
|
+
try {
|
|
31
|
+
if (fileExists((0, path_1.join)(options.pagesDir, ...pathParts, "index.ts"))) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// TODO @vejvis - proč tu je tahle podmínka?
|
|
35
|
+
const lastPart = pathParts.splice(-1);
|
|
36
|
+
const pagePath = pathParts.length > 0
|
|
37
|
+
? (0, path_1.join)(options.pagesDir, ...pathParts, `${lastPart[0] || "index"}.ts`)
|
|
38
|
+
: (0, path_1.join)(options.pagesDir, `${lastPart[0] || "index"}.tsx`);
|
|
39
|
+
if (fileExists(pagePath)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (fileExists((0, path_1.join)(options.appDir, ...pathname.substr(1).split("/"), "page.ts"))) {
|
|
43
|
+
// app directory
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
throw new Error("Invalid");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
process_1.stdout.write(`❌ ${routeName}\n`);
|
|
50
|
+
hasError = true;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (hasError) {
|
|
54
|
+
if (options.shouldProcessExit) {
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
throw new Error("At least one invalid route found");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (options.shouldProcessExit) {
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
process_1.stdout.write("All routes checked.\n");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const path_1 = require("path");
|
|
4
|
+
const routes_check_1 = require("./routes-check");
|
|
5
|
+
describe("routesCheck", () => {
|
|
6
|
+
it("should be valid", () => {
|
|
7
|
+
expect(() => (0, routes_check_1.routesCheck)({
|
|
8
|
+
route1: { path: "/" }, // index
|
|
9
|
+
route2: { path: "/product" }, // index file
|
|
10
|
+
route3: { path: "/product/edit" }, // file with specific name
|
|
11
|
+
route4: { path: "/product/[id]" }, // path parameter, file with specific name
|
|
12
|
+
route5: { path: "/product-2/[id]" }, // path parameter, index file
|
|
13
|
+
app1: { path: "/app-directory" },
|
|
14
|
+
app2: { path: "/app-directory/[param]" },
|
|
15
|
+
}, {
|
|
16
|
+
shouldProcessExit: false,
|
|
17
|
+
appDir: (0, path_1.join)(__dirname, "__test__", "app"),
|
|
18
|
+
pagesDir: (0, path_1.join)(__dirname, "__test__", "pages"),
|
|
19
|
+
})).not.toThrow();
|
|
20
|
+
});
|
|
21
|
+
it("should fail", () => {
|
|
22
|
+
expect(() => (0, routes_check_1.routesCheck)({
|
|
23
|
+
unknown: { path: "/unknown-route" }, // index
|
|
24
|
+
}, {
|
|
25
|
+
shouldProcessExit: false,
|
|
26
|
+
appDir: (0, path_1.join)(__dirname, "__test__", "app"),
|
|
27
|
+
pagesDir: (0, path_1.join)(__dirname, "__test__", "pages"),
|
|
28
|
+
})).toThrow();
|
|
29
|
+
});
|
|
30
|
+
});
|
package/sitemap-generator.d.ts
CHANGED
|
@@ -7,22 +7,32 @@ export type SitemapItem = {
|
|
|
7
7
|
changefreq?: ChangeFrequency;
|
|
8
8
|
};
|
|
9
9
|
type RouteResolverResult = null | undefined | SitemapItem | Array<SitemapItem | null | undefined>;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Object-based approach for sitemap generation with compile-time exhaustiveness checking.
|
|
12
|
+
*
|
|
13
|
+
* Instead of builder pattern, this requires all routes to be specified in a single object.
|
|
14
|
+
* Routes that shouldn't be in sitemap are set to null, others have RouteResolver functions.
|
|
15
|
+
*
|
|
16
|
+
* This approach is much faster for TypeScript compiler as it avoids recursive type operations.
|
|
17
|
+
*/
|
|
18
|
+
export type SitemapRouteResolvers<Locales extends string[], RouteList extends RoutesDefinition<Locales>> = {
|
|
19
|
+
[K in keyof RouteList]: ((routeName: K) => Promise<RouteResolverResult>) | null;
|
|
14
20
|
};
|
|
15
|
-
export type SitemapGeneratorType
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
exhaustive: keyof RouteList extends never ? () => SitemapGeneratorType<Locales, RouteList> : MissingRoutesError<keyof RouteList>;
|
|
19
|
-
toXml: keyof RouteList extends never ? () => Promise<string> : MissingRoutesError<keyof RouteList>;
|
|
20
|
-
toJson: keyof RouteList extends never ? () => Promise<any> : MissingRoutesError<keyof RouteList>;
|
|
21
|
+
export type SitemapGeneratorType = {
|
|
22
|
+
toXml(): Promise<string>;
|
|
23
|
+
toJson(): Promise<string>;
|
|
21
24
|
};
|
|
22
25
|
export type SitemapGeneratorOptions = {
|
|
23
26
|
baseUrl?: string;
|
|
24
27
|
defaultPriority?: number;
|
|
25
28
|
defaultChangeFreq?: ChangeFrequency;
|
|
26
29
|
};
|
|
27
|
-
export declare
|
|
30
|
+
export declare class SitemapGenerator<Locales extends string[], RouteList extends RoutesDefinition<Locales>> {
|
|
31
|
+
private routeResolvers;
|
|
32
|
+
private options;
|
|
33
|
+
constructor(resolvers: SitemapRouteResolvers<Locales, RouteList>, options?: SitemapGeneratorOptions);
|
|
34
|
+
private getSitemapItems;
|
|
35
|
+
toXml(): Promise<string>;
|
|
36
|
+
toJson(): Promise<string>;
|
|
37
|
+
}
|
|
28
38
|
export {};
|
package/sitemap-generator.js
CHANGED
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.SitemapGenerator = void 0;
|
|
4
4
|
const object_to_xml_1 = require("./utils/object-to-xml");
|
|
5
5
|
function filterNullish(val) {
|
|
6
6
|
return val.filter((item) => item !== null && item !== undefined);
|
|
7
7
|
}
|
|
8
8
|
class SitemapGenerator {
|
|
9
|
-
constructor(options) {
|
|
10
|
-
this.routeResolvers =
|
|
11
|
-
this.options = null;
|
|
12
|
-
this.exhaustive = () => this;
|
|
13
|
-
this.toXml = () => {
|
|
14
|
-
return this.getSitemapItems().then((sitemapItems) => {
|
|
15
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
16
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
|
17
|
-
${sitemapItems.map((item) => ` <url>\n${(0, object_to_xml_1.objectToXml)(item)}\n </url>`).join("\n")}
|
|
18
|
-
</urlset>`;
|
|
19
|
-
});
|
|
20
|
-
};
|
|
21
|
-
this.toJson = () => this.getSitemapItems().then(JSON.stringify);
|
|
22
|
-
this.options = options !== null && options !== void 0 ? options : null;
|
|
9
|
+
constructor(resolvers, options) {
|
|
10
|
+
this.routeResolvers = resolvers;
|
|
11
|
+
this.options = options !== null && options !== void 0 ? options : {};
|
|
23
12
|
}
|
|
24
13
|
async getSitemapItems() {
|
|
25
14
|
const routes = Object.keys(this.routeResolvers);
|
|
26
|
-
const
|
|
15
|
+
const resolversWithRoutes = routes
|
|
16
|
+
.map((route) => ({ route, resolver: this.routeResolvers[route] }))
|
|
17
|
+
.filter(({ resolver }) => resolver !== null);
|
|
18
|
+
const allSitemapItems = await Promise.all(resolversWithRoutes.map(({ route, resolver }) => resolver(route))).then((resolverResults) => {
|
|
27
19
|
const sitemapItems = [];
|
|
28
20
|
resolverResults.forEach((result) => {
|
|
29
21
|
if (!result) {
|
|
@@ -38,22 +30,25 @@ ${sitemapItems.map((item) => ` <url>\n${(0, object_to_xml_1.objectToXml)(item
|
|
|
38
30
|
return sitemapItems;
|
|
39
31
|
});
|
|
40
32
|
return allSitemapItems.map((sitemapItem) => {
|
|
41
|
-
var _a
|
|
33
|
+
var _a;
|
|
42
34
|
return ({
|
|
43
35
|
lastmod: sitemapItem.lastmod,
|
|
44
|
-
loc: ((
|
|
45
|
-
changefreq: sitemapItem.changefreq !== undefined ? sitemapItem.changefreq :
|
|
46
|
-
priority: sitemapItem.priority !== undefined ? sitemapItem.priority :
|
|
36
|
+
loc: ((_a = this.options.baseUrl) !== null && _a !== void 0 ? _a : "") + sitemapItem.loc,
|
|
37
|
+
changefreq: sitemapItem.changefreq !== undefined ? sitemapItem.changefreq : this.options.defaultChangeFreq,
|
|
38
|
+
priority: sitemapItem.priority !== undefined ? sitemapItem.priority : this.options.defaultPriority,
|
|
47
39
|
});
|
|
48
40
|
});
|
|
49
41
|
}
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
|
|
42
|
+
toXml() {
|
|
43
|
+
return this.getSitemapItems().then((sitemapItems) => {
|
|
44
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
45
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
|
46
|
+
${sitemapItems.map((item) => ` <url>\n${(0, object_to_xml_1.objectToXml)(item)}\n </url>`).join("\n")}
|
|
47
|
+
</urlset>`;
|
|
48
|
+
});
|
|
53
49
|
}
|
|
54
|
-
|
|
55
|
-
return this;
|
|
50
|
+
toJson() {
|
|
51
|
+
return this.getSitemapItems().then(JSON.stringify);
|
|
56
52
|
}
|
|
57
53
|
}
|
|
58
|
-
|
|
59
|
-
exports.createSitemapGenerator = createSitemapGenerator;
|
|
54
|
+
exports.SitemapGenerator = SitemapGenerator;
|
|
@@ -9,6 +9,9 @@ const { createSitemapGenerator, routeToUrl } = (0, router_1.createRouter)({
|
|
|
9
9
|
queryParam: (0, superstruct_1.optional)((0, superstruct_1.string)()),
|
|
10
10
|
}),
|
|
11
11
|
},
|
|
12
|
+
skippedRoute: {
|
|
13
|
+
path: "/skipped",
|
|
14
|
+
},
|
|
12
15
|
catchAllSegments: {
|
|
13
16
|
path: "/catch-all/[...pathParams]",
|
|
14
17
|
schema: (0, superstruct_1.object)({
|
|
@@ -26,18 +29,21 @@ const { createSitemapGenerator, routeToUrl } = (0, router_1.createRouter)({
|
|
|
26
29
|
}, { baseUrl: "https://www.uxf.cz" });
|
|
27
30
|
const BASE_URL = "www.uxf.cz";
|
|
28
31
|
test("routeToUrl", async () => {
|
|
29
|
-
const xml = await createSitemapGenerator({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const xml = await createSitemapGenerator({
|
|
33
|
+
index: async (route) => ({ loc: routeToUrl(route, { queryParam: "test" }) }),
|
|
34
|
+
catchAllSegments: async (route) => ({ loc: routeToUrl(route, { pathParams: ["param-1"] }) }),
|
|
35
|
+
optionalCatchAllSegments: async (route) => ({
|
|
36
|
+
loc: routeToUrl(route, { pathParams: ["param-1", "param-2"] }),
|
|
37
|
+
priority: 10,
|
|
38
|
+
}),
|
|
39
|
+
skippedRoute: null,
|
|
40
|
+
}, {
|
|
41
|
+
baseUrl: BASE_URL,
|
|
42
|
+
}).toXml();
|
|
37
43
|
expect(xml).toBe(`<?xml version="1.0" encoding="UTF-8"?>
|
|
38
44
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
|
39
45
|
<url>
|
|
40
|
-
<loc>${BASE_URL}
|
|
46
|
+
<loc>${BASE_URL}/?queryParam=test</loc>
|
|
41
47
|
</url>
|
|
42
48
|
<url>
|
|
43
49
|
<loc>${BASE_URL}/catch-all/param-1</loc>
|
package/routes-check.d.ts
DELETED
package/routes-check.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
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
|
-
}
|
package/routes-check.test.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
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
|
-
});
|
|
File without changes
|