@visulima/api-platform 1.0.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +137 -0
  4. package/dist/chunk-2LATTLUM.mjs +166 -0
  5. package/dist/chunk-2LATTLUM.mjs.map +1 -0
  6. package/dist/chunk-4DRV4PCJ.js +51 -0
  7. package/dist/chunk-4DRV4PCJ.js.map +1 -0
  8. package/dist/chunk-5VRACIDE.mjs +10 -0
  9. package/dist/chunk-5VRACIDE.mjs.map +1 -0
  10. package/dist/chunk-GSWANBU5.mjs +51 -0
  11. package/dist/chunk-GSWANBU5.mjs.map +1 -0
  12. package/dist/chunk-J4EBGCNK.mjs +99 -0
  13. package/dist/chunk-J4EBGCNK.mjs.map +1 -0
  14. package/dist/chunk-JC4IRQUL.js +10 -0
  15. package/dist/chunk-JC4IRQUL.js.map +1 -0
  16. package/dist/chunk-S7GUPAL4.js +166 -0
  17. package/dist/chunk-S7GUPAL4.js.map +1 -0
  18. package/dist/chunk-T25VSNTF.js +99 -0
  19. package/dist/chunk-T25VSNTF.js.map +1 -0
  20. package/dist/index-3318b0da.d.ts +33 -0
  21. package/dist/index-browser.d.ts +2 -0
  22. package/dist/index-browser.js +8 -0
  23. package/dist/index-browser.js.map +1 -0
  24. package/dist/index-browser.mjs +8 -0
  25. package/dist/index-browser.mjs.map +1 -0
  26. package/dist/index-server.d.ts +87 -0
  27. package/dist/index-server.js +454 -0
  28. package/dist/index-server.js.map +1 -0
  29. package/dist/index-server.mjs +454 -0
  30. package/dist/index-server.mjs.map +1 -0
  31. package/dist/next/cli/index.d.ts +11 -0
  32. package/dist/next/cli/index.js +203 -0
  33. package/dist/next/cli/index.js.map +1 -0
  34. package/dist/next/cli/index.mjs +203 -0
  35. package/dist/next/cli/index.mjs.map +1 -0
  36. package/dist/next/index-browser.d.ts +4 -0
  37. package/dist/next/index-browser.js +12 -0
  38. package/dist/next/index-browser.js.map +1 -0
  39. package/dist/next/index-browser.mjs +12 -0
  40. package/dist/next/index-browser.mjs.map +1 -0
  41. package/dist/next/index-server.d.ts +129 -0
  42. package/dist/next/index-server.js +76 -0
  43. package/dist/next/index-server.js.map +1 -0
  44. package/dist/next/index-server.mjs +76 -0
  45. package/dist/next/index-server.mjs.map +1 -0
  46. package/dist/swagger-6ad3b021.d.ts +11 -0
  47. package/next/cli/package.json +9 -0
  48. package/next/package.json +10 -0
  49. package/package.json +207 -0
  50. package/src/connect/create-node-router.ts +44 -0
  51. package/src/connect/handler.ts +46 -0
  52. package/src/connect/middleware/cors-middleware.ts +10 -0
  53. package/src/connect/middleware/http-header-normalizer.ts +93 -0
  54. package/src/connect/middleware/rate-limiter-middleware.ts +43 -0
  55. package/src/connect/middleware/serializers-middleware.ts +121 -0
  56. package/src/connect/serializers/types.d.ts +1 -0
  57. package/src/connect/serializers/xml.ts +13 -0
  58. package/src/connect/serializers/yaml.ts +7 -0
  59. package/src/error-handler/jsonapi-error-handler.ts +46 -0
  60. package/src/error-handler/problem-error-handler.ts +44 -0
  61. package/src/error-handler/types.d.ts +14 -0
  62. package/src/error-handler/utils.ts +39 -0
  63. package/src/index-browser.tsx +1 -0
  64. package/src/index-server.ts +75 -0
  65. package/src/next/cli/index.ts +2 -0
  66. package/src/next/cli/list/api-route-file-parser.ts +74 -0
  67. package/src/next/cli/list/collect-api-route-files.ts +42 -0
  68. package/src/next/cli/list/list-command.ts +105 -0
  69. package/src/next/cli/list/routes-render.ts +62 -0
  70. package/src/next/cli/list/types.d.ts +1 -0
  71. package/src/next/index-browser.tsx +3 -0
  72. package/src/next/index-server.ts +6 -0
  73. package/src/next/routes/api/swagger.ts +23 -0
  74. package/src/next/routes/pages/swagger/get-static-properties-swagger.ts +32 -0
  75. package/src/next/routes/pages/swagger/redoc.tsx +35 -0
  76. package/src/next/routes/pages/swagger/swagger.tsx +39 -0
  77. package/src/next/webpack/with-open-api.ts +63 -0
  78. package/src/swagger/extend-swagger-spec.ts +167 -0
  79. package/src/swagger/swagger-handler.ts +83 -0
  80. package/src/utils.ts +37 -0
  81. package/src/zod/date-in-schema.ts +57 -0
  82. package/src/zod/date-out-schema.ts +41 -0
  83. package/src/zod/index.ts +9 -0
@@ -0,0 +1,121 @@
1
+ import type { NextHandler } from "@visulima/connect";
2
+ import accepts from "accepts";
3
+ import { header as headerCase } from "case";
4
+ import type { NextApiResponse } from "next";
5
+ import type { IncomingMessage, ServerResponse } from "node:http";
6
+
7
+ import type { Serializer } from "../serializers/types";
8
+ import xmlTransformer from "../serializers/xml";
9
+ import yamlTransformer from "../serializers/yaml";
10
+
11
+ function hasJsonStructure(string_: any): boolean {
12
+ if (typeof string_ !== "string") {
13
+ return false;
14
+ }
15
+
16
+ try {
17
+ const result = JSON.parse(string_);
18
+ const type = Object.prototype.toString.call(result);
19
+
20
+ return type === "[object Object]" || type === "[object Array]";
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ const contentTypeKey = "Content-Type";
27
+
28
+ // eslint-disable-next-line max-len
29
+ const serialize = <Request extends IncomingMessage, Response extends ServerResponse>(
30
+ serializers: Serializers,
31
+ request: Request,
32
+ response: Response | NextApiResponse,
33
+ data: any,
34
+ options: {
35
+ defaultContentType: string;
36
+ },
37
+ // eslint-disable-next-line radar/cognitive-complexity
38
+ ) => {
39
+ const contentType = response.getHeader(contentTypeKey) as string | undefined;
40
+
41
+ // skip serialization when Content-Type is already set
42
+ if (typeof contentType === "string") {
43
+ return data;
44
+ }
45
+
46
+ const accept = accepts(request);
47
+ const types: string[] = [...(accept.types() as string[]), options.defaultContentType];
48
+
49
+ let serializedData = data;
50
+
51
+ // eslint-disable-next-line no-restricted-syntax
52
+ types.every((type) => {
53
+ let breakTypes = false;
54
+
55
+ serializers.forEach(({ regex, serializer }) => {
56
+ if (!regex.test(type)) {
57
+ return;
58
+ }
59
+
60
+ response.setHeader(contentTypeKey, type);
61
+ serializedData = serializer(serializedData);
62
+ breakTypes = true;
63
+ });
64
+
65
+ if (!breakTypes) {
66
+ if (/yaml|yml/.test(type)) {
67
+ response.setHeader(contentTypeKey, type);
68
+
69
+ serializedData = yamlTransformer(hasJsonStructure(data) ? JSON.parse(data) : data);
70
+ } else if (/xml/.test(type)) {
71
+ response.setHeader(contentTypeKey, type);
72
+
73
+ serializedData = xmlTransformer({
74
+ [headerCase(`${request.url?.replace("/api/", "")}`.trim())]: hasJsonStructure(data) ? JSON.parse(data) : data,
75
+ });
76
+ }
77
+ }
78
+
79
+ return breakTypes;
80
+ });
81
+
82
+ // eslint-disable-next-line no-param-reassign
83
+ return serializedData;
84
+ };
85
+
86
+ // eslint-disable-next-line max-len
87
+ const serializersMiddleware = (serializers: Serializers = [], defaultContentType: string = "application/json; charset=utf-8") => async <Request extends IncomingMessage, Response extends ServerResponse>(request: Request, response: Response | NextApiResponse, next: NextHandler) => {
88
+ if (typeof (response as NextApiResponse)?.send === "function") {
89
+ const oldSend = (response as NextApiResponse).send;
90
+
91
+ (response as NextApiResponse).send = (data) => {
92
+ (response as NextApiResponse).send = oldSend;
93
+
94
+ // eslint-disable-next-line no-param-reassign
95
+ data = serialize<Request, Response>(serializers, request, response, data, { defaultContentType });
96
+
97
+ return (response as NextApiResponse).send(data);
98
+ };
99
+ } else {
100
+ const oldEnd = response.end;
101
+
102
+ // @ts-ignore
103
+ response.end = (data, ...arguments_) => {
104
+ response.end = oldEnd;
105
+
106
+ // eslint-disable-next-line no-param-reassign
107
+ data = serialize<Request, Response>(serializers, request, response, data, { defaultContentType });
108
+
109
+ // @ts-ignore
110
+ return response.end(data, ...arguments_);
111
+ };
112
+ }
113
+
114
+ return next();
115
+ };
116
+
117
+ export type Serializers = {
118
+ regex: RegExp;
119
+ serializer: Serializer;
120
+ }[];
121
+ export default serializersMiddleware;
@@ -0,0 +1 @@
1
+ export type Serializer = (data: any) => string | Buffer | Uint8Array;
@@ -0,0 +1,13 @@
1
+ import type { XmlElement } from "jstoxml";
2
+ import { toXML } from "jstoxml";
3
+
4
+ import type { Serializer } from "./types";
5
+
6
+ const config = {
7
+ header: true,
8
+ indent: " ",
9
+ };
10
+
11
+ const xmlTransformer: Serializer = (data?: XmlElement | XmlElement[]) => toXML(data, config);
12
+
13
+ export default xmlTransformer;
@@ -0,0 +1,7 @@
1
+ import { stringify } from "yaml";
2
+
3
+ import type { Serializer } from "./types";
4
+
5
+ const yamlTransformer: Serializer = (data) => stringify(data, { indent: 2 });
6
+
7
+ export default yamlTransformer;
@@ -0,0 +1,46 @@
1
+ import { HttpError } from "http-errors";
2
+ import { getReasonPhrase } from "http-status-codes";
3
+ import { ErrorSerializer, JapiError } from "ts-japi";
4
+
5
+ import type { ErrorHandler } from "./types";
6
+ import { addStatusCodeToResponse, sendJson, setErrorHeaders } from "./utils";
7
+
8
+ const defaultTitle = "An error occurred";
9
+
10
+ const jsonapiErrorHandler: ErrorHandler = (error: HttpError | JapiError | Error, _request, response) => {
11
+ addStatusCodeToResponse(response, error);
12
+
13
+ setErrorHeaders(response, error);
14
+
15
+ if (error instanceof JapiError || JapiError.isLikeJapiError(error)) {
16
+ const serializer = new ErrorSerializer();
17
+
18
+ sendJson(response, serializer.serialize(error));
19
+ } else if (error instanceof HttpError) {
20
+ const { statusCode, title, message } = error;
21
+
22
+ sendJson(response, {
23
+ errors: [
24
+ {
25
+ code: statusCode,
26
+ title: title || getReasonPhrase(statusCode) || defaultTitle,
27
+ detail: message,
28
+ },
29
+ ],
30
+ });
31
+ } else {
32
+ const { message } = error;
33
+
34
+ sendJson(response, {
35
+ errors: [
36
+ {
37
+ code: "500",
38
+ title: getReasonPhrase(response.statusCode) || defaultTitle,
39
+ detail: message,
40
+ },
41
+ ],
42
+ });
43
+ }
44
+ };
45
+
46
+ export default jsonapiErrorHandler;
@@ -0,0 +1,44 @@
1
+ import { HttpError } from "http-errors";
2
+ import { getReasonPhrase } from "http-status-codes";
3
+
4
+ import type { ErrorHandler } from "./types";
5
+ import { addStatusCodeToResponse, sendJson, setErrorHeaders } from "./utils";
6
+
7
+ const defaultType = "https://tools.ietf.org/html/rfc2616#section-10";
8
+ const defaultTitle = "An error occurred";
9
+ /**
10
+ * Normalizes errors according to the API Problem spec (RFC 7807).
11
+ *
12
+ * @see https://tools.ietf.org/html/rfc7807
13
+ */
14
+ const problemErrorHandler: ErrorHandler = (error: HttpError | Error, _request, response) => {
15
+ const { stack, message } = error;
16
+
17
+ if (error instanceof HttpError) {
18
+ const {
19
+ statusCode, expose, title, type,
20
+ } = error;
21
+
22
+ response.statusCode = statusCode;
23
+
24
+ setErrorHeaders(response, error);
25
+
26
+ sendJson(response, {
27
+ type: type || defaultType,
28
+ title: title || getReasonPhrase(statusCode) || defaultTitle,
29
+ details: message,
30
+ ...(expose ? { trace: stack } : {}),
31
+ });
32
+ } else {
33
+ addStatusCodeToResponse(response, error);
34
+
35
+ sendJson(response, {
36
+ type: defaultType,
37
+ title: getReasonPhrase(response.statusCode) || defaultTitle,
38
+ details: message,
39
+ ...((error as { expose: boolean } & Error).expose ? { trace: stack } : {}),
40
+ });
41
+ }
42
+ };
43
+
44
+ export default problemErrorHandler;
@@ -0,0 +1,14 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ export type ErrorHandler = <Request extends IncomingMessage, Response extends ServerResponse>(
4
+ error: any,
5
+ request: Request,
6
+ response: Response,
7
+ ) => any | Promise<any>;
8
+
9
+ export type ErrorHandlers = {
10
+ regex: RegExp;
11
+ handler: ErrorHandler;
12
+ }[];
13
+
14
+ export type ApiFormat = "jsonapi" | "problem";
@@ -0,0 +1,39 @@
1
+ import { StatusCodes } from "http-status-codes";
2
+ import type { ServerResponse } from "node:http";
3
+
4
+ export const setErrorHeaders = (response: ServerResponse, error: any) => {
5
+ const headers: { [key: string]: number | string | ReadonlyArray<string> } = error.headers || {};
6
+
7
+ Object.keys(headers).forEach((header: string) => {
8
+ response.setHeader(header, headers[header] as number | string | ReadonlyArray<string>);
9
+ });
10
+ };
11
+
12
+ /**
13
+ * Send `JSON` object
14
+ * @param response response object
15
+ * @param jsonBody of data
16
+ */
17
+ export const sendJson = (response: ServerResponse, jsonBody: any): void => {
18
+ // Set header to application/json
19
+ response.setHeader("content-type", "application/json; charset=utf-8");
20
+
21
+ response.end(JSON.stringify(jsonBody));
22
+ };
23
+
24
+ export const addStatusCodeToResponse = (response: ServerResponse, error: any): void => {
25
+ // respect err.statusCode
26
+ if (typeof error.statusCode !== "undefined") {
27
+ response.statusCode = error.statusCode;
28
+ }
29
+
30
+ // respect err.status
31
+ if (typeof error.status !== "undefined") {
32
+ response.statusCode = error.status;
33
+ }
34
+
35
+ // default status code to 500
36
+ if (response.statusCode < 400) {
37
+ response.statusCode = StatusCodes.INTERNAL_SERVER_ERROR;
38
+ }
39
+ };
@@ -0,0 +1 @@
1
+ export * as zod from "./zod";
@@ -0,0 +1,75 @@
1
+ export * from "./index-browser";
2
+
3
+ export {
4
+ default as createHttpError,
5
+ BadRequest,
6
+ Forbidden,
7
+ BadGateway,
8
+ BandwidthLimitExceeded,
9
+ Conflict,
10
+ ExpectationFailed,
11
+ FailedDependency,
12
+ GatewayTimeout,
13
+ Gone,
14
+ HTTPVersionNotSupported,
15
+ ImATeapot,
16
+ InsufficientStorage,
17
+ InternalServerError,
18
+ VariantAlsoNegotiates,
19
+ ProxyAuthenticationRequired,
20
+ NetworkAuthenticationRequire,
21
+ LengthRequired,
22
+ LoopDetected,
23
+ Locked,
24
+ MethodNotAllowed,
25
+ MisdirectedRequest,
26
+ NotAcceptable,
27
+ NotExtended,
28
+ NotFound,
29
+ NotImplemented,
30
+ PayloadTooLarge,
31
+ RequestHeaderFieldsTooLarge,
32
+ PaymentRequired,
33
+ PreconditionFailed,
34
+ PreconditionRequired,
35
+ RangeNotSatisfiable,
36
+ RequestTimeout,
37
+ ServiceUnavailable,
38
+ TooManyRequests,
39
+ Unauthorized,
40
+ UnorderedCollection,
41
+ UnprocessableEntity,
42
+ UnavailableForLegalReasons,
43
+ UnsupportedMediaType,
44
+ UpgradeRequired,
45
+ URITooLong,
46
+ } from "http-errors";
47
+
48
+ export { default as createNodeRouter } from "./connect/create-node-router";
49
+ export { onError, onNoMatch } from "./connect/handler";
50
+ export type {
51
+ EdgeRequestHandler,
52
+ ExpressRequestHandler,
53
+ NodeRequestHandler,
54
+ Route,
55
+ HandlerOptions,
56
+ NextHandler,
57
+ FunctionLike,
58
+ Nextable,
59
+ ValueOrPromise,
60
+ FindResult,
61
+ RouteShortcutMethod,
62
+ HttpMethod,
63
+ } from "@visulima/connect";
64
+ export {
65
+ createEdgeRouter, EdgeRouter, expressWrapper, NodeRouter, Router, withZod, sendJson,
66
+ } from "@visulima/connect";
67
+
68
+ export { default as rateLimiterMiddleware } from "./connect/middleware/rate-limiter-middleware";
69
+ export { default as corsMiddleware } from "./connect/middleware/cors-middleware";
70
+ export { default as serializersMiddleware } from "./connect/middleware/serializers-middleware";
71
+ export { default as httpHeaderNormalizerMiddleware } from "./connect/middleware/http-header-normalizer";
72
+
73
+ export { default as swaggerHandler } from "./swagger/swagger-handler";
74
+
75
+ export { dateIn, dateOut } from "./zod";
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line import/prefer-default-export
2
+ export { default as listCommand } from "./list/list-command";
@@ -0,0 +1,74 @@
1
+ import type { OpenApiObject } from "@visulima/jsdoc-open-api";
2
+ import { jsDocumentCommentsToOpenApi, parseFile, swaggerJsDocumentCommentsToOpenApi } from "@visulima/jsdoc-open-api";
3
+ import fs from "node:fs";
4
+ import process from "node:process";
5
+
6
+ import type { Route } from "./types";
7
+
8
+ const extensionRegex = /\.(js|ts|mjs|cjs)$/;
9
+
10
+ const apiRouteFileParser = (apiRouteFile: string, cwdPath: string, verbose: boolean = false): Route[] => {
11
+ let specs: OpenApiObject[] = [];
12
+
13
+ const parsedJsDocumentFile = parseFile(apiRouteFile, jsDocumentCommentsToOpenApi, verbose);
14
+
15
+ specs = [...specs, ...parsedJsDocumentFile.map((item) => item.spec)];
16
+
17
+ const parsedSwaggerJsDocumentFile = parseFile(apiRouteFile, swaggerJsDocumentCommentsToOpenApi, verbose);
18
+
19
+ specs = [...specs, ...parsedSwaggerJsDocumentFile.map((item) => item.spec)];
20
+
21
+ const routes: Route[] = [];
22
+
23
+ if (specs.length === 0) {
24
+ const apiRouteFileContent = fs.readFileSync(apiRouteFile, "utf8");
25
+
26
+ apiRouteFileContent.split(/\r?\n/).forEach((line) => {
27
+ const match = line.match(/[=aces|]+\s["'|](GET|POST|PUT|PATCH|HEAD|DELETE|OPTIONS)["'|]/);
28
+
29
+ if (match) {
30
+ let [, method] = match;
31
+
32
+ if (method === "GET") {
33
+ method = "GET|HEAD";
34
+ }
35
+
36
+ routes.push({
37
+ method: method as string,
38
+ path: apiRouteFile.replace(cwdPath, "").replace(extensionRegex, ""),
39
+ tags: [],
40
+ file: apiRouteFile.replace(`${process.cwd()}/`, ""),
41
+ });
42
+ }
43
+ });
44
+
45
+ if (routes.length === 0) {
46
+ routes.push({
47
+ method: "GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS",
48
+ path: apiRouteFile.replace(cwdPath, "").replace(extensionRegex, ""),
49
+ tags: [],
50
+ file: apiRouteFile.replace(`${process.cwd()}/`, ""),
51
+ });
52
+ }
53
+
54
+ return routes;
55
+ }
56
+
57
+ specs.forEach((spec) => {
58
+ const paths = Object.entries(spec.paths);
59
+
60
+ paths.forEach(([path, pathSpec]) => {
61
+ const methods = Object.entries(pathSpec);
62
+
63
+ methods.forEach(([method, methodSpec]) => {
64
+ routes.push({
65
+ path, method: method.toUpperCase(), tags: methodSpec.tags, file: apiRouteFile.replace(`${process.cwd()}/`, ""),
66
+ });
67
+ });
68
+ });
69
+ });
70
+
71
+ return routes;
72
+ };
73
+
74
+ export default apiRouteFileParser;
@@ -0,0 +1,42 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import { collect } from "@visulima/readdir";
3
+ import fs from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ const isDirectory = (path: string): boolean => {
7
+ try {
8
+ return fs.statSync(path).isDirectory();
9
+ } catch {
10
+ return false;
11
+ }
12
+ };
13
+
14
+ const collectApiRouteFiles = async (path: string = "", verbose: boolean = false): Promise<string[]> => {
15
+ let apiFolderPath = join(path, "pages/api");
16
+
17
+ // src/pages will be ignored if pages is present in the root directory
18
+ if (!isDirectory(apiFolderPath)) {
19
+ apiFolderPath = join(path, "src/pages/api");
20
+
21
+ if (!isDirectory(apiFolderPath)) {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ return collect(apiFolderPath, {
27
+ extensions: [".js", ".cjs", ".mjs", ".ts"],
28
+ includeDirs: false,
29
+ minimatchOptions: {
30
+ match: {
31
+ debug: verbose,
32
+ matchBase: true,
33
+ },
34
+ skip: {
35
+ debug: verbose,
36
+ matchBase: true,
37
+ },
38
+ },
39
+ });
40
+ };
41
+
42
+ export default collectApiRouteFiles;
@@ -0,0 +1,105 @@
1
+ import colors from "chalk";
2
+ import { join } from "node:path";
3
+ import process from "node:process";
4
+
5
+ import apiRouteFileParser from "./api-route-file-parser";
6
+ import collectApiRouteFiles from "./collect-api-route-files";
7
+ import routesRender from "./routes-render";
8
+ import type { Route } from "./types";
9
+
10
+ type RenderOptions = {
11
+ includePaths: string[];
12
+ excludePaths: string[];
13
+ methods: string[];
14
+ group: string;
15
+ };
16
+
17
+ const groupBy = (list: Route[], keyGetter: (item: Route) => keyof Route): Map<string, Route[]> => {
18
+ const map = new Map();
19
+
20
+ list.forEach((item) => {
21
+ const key = keyGetter(item);
22
+ const collection = map.get(key);
23
+
24
+ if (!collection) {
25
+ map.set(key, [item]);
26
+ } else {
27
+ collection.push(item);
28
+ }
29
+ });
30
+
31
+ return map;
32
+ };
33
+
34
+ const listCommand = async (
35
+ path: string = "",
36
+ options: Partial<
37
+ {
38
+ verbose: boolean;
39
+ } & RenderOptions
40
+ > = {},
41
+ // eslint-disable-next-line radar/cognitive-complexity
42
+ ) => {
43
+ const routePath = join(process.cwd(), path);
44
+
45
+ const apiRouteFiles = await collectApiRouteFiles(routePath, options.verbose || false);
46
+
47
+ if (apiRouteFiles.length === 0) {
48
+ // eslint-disable-next-line no-console
49
+ console.error(`No API routes found, in "${routePath}".`);
50
+ // eslint-disable-next-line unicorn/no-process-exit
51
+ process.exit(1);
52
+ }
53
+
54
+ let parsedApiRoutes = apiRouteFiles.flatMap((apiRouteFile) => apiRouteFileParser(apiRouteFile, routePath, options.verbose || false));
55
+
56
+ if (options.includePaths) {
57
+ parsedApiRoutes = options.includePaths.flatMap((ipath) => parsedApiRoutes.filter((route) => route.path.startsWith(ipath)));
58
+ }
59
+
60
+ if (options.excludePaths) {
61
+ parsedApiRoutes = options.excludePaths.flatMap((epath) => parsedApiRoutes.filter((route) => !route.path.startsWith(epath)));
62
+ }
63
+
64
+ if (typeof options.group !== "undefined") {
65
+ const groupedMap = groupBy(parsedApiRoutes, (route) => {
66
+ if (options.group === "path") {
67
+ return route.path.replace("/pages", "").split("/")[1];
68
+ }
69
+
70
+ return route.tags[0] || "unsorted";
71
+ });
72
+
73
+ let counter = 0;
74
+
75
+ groupedMap.forEach((routes, key) => {
76
+ if (counter > 0) {
77
+ // eslint-disable-next-line no-console
78
+ console.log();
79
+ }
80
+
81
+ const dotsCount = (process.stdout.columns - 16 - key.length) / 2;
82
+ const dots = dotsCount > 0 ? Array.from({ length: dotsCount }).fill(" ").join("") : "";
83
+ // eslint-disable-next-line no-console
84
+ console.log(dots + colors.bold.underline(key));
85
+
86
+ routesRender(routes, options).forEach((renderedRoute) => {
87
+ // eslint-disable-next-line no-console
88
+ console.log(renderedRoute);
89
+ });
90
+
91
+ counter += 1;
92
+ });
93
+ } else {
94
+ routesRender([], options).forEach((renderedRoute) => {
95
+ // eslint-disable-next-line no-console
96
+ console.log(renderedRoute);
97
+ });
98
+ }
99
+ // });
100
+
101
+ // eslint-disable-next-line no-console
102
+ console.log(`\n Listed ${colors.greenBright(String(apiRouteFiles.length))} HTTP ${apiRouteFiles.length === 1 ? "route" : "routes"}.\n`);
103
+ };
104
+
105
+ export default listCommand;
@@ -0,0 +1,62 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import colors from "chalk";
3
+
4
+ import type { Route } from "./types";
5
+
6
+ const renderRoute = (method: string, routePath: string): string => {
7
+ const colorMap = {
8
+ GET: colors.blue,
9
+ POST: colors.yellow,
10
+ PATCH: colors.yellow,
11
+ PUT: colors.yellow,
12
+ DELETE: colors.redBright,
13
+ OPTIONS: colors.hex("#6C7280"),
14
+ ANY: colors.redBright,
15
+ HEAD: colors.hex("#6C7280"),
16
+ };
17
+
18
+ let methodText: string;
19
+
20
+ if (method === "GET|HEAD") {
21
+ methodText = `${colors.blue("GET")}${colors.grey("|HEAD")}`;
22
+ } else {
23
+ const coloredMethod = colorMap[method as keyof typeof colorMap](method);
24
+
25
+ methodText = method === "GET" ? `${coloredMethod}${colors.grey("|HEAD")}` : coloredMethod;
26
+ }
27
+
28
+ const spacesCount = method === "GET" ? 6 : 14 - method.length;
29
+ const spaces = Array.from({ length: spacesCount }).fill(" ").join("");
30
+
31
+ const dotsCount = process.stdout.columns - 16 - routePath.length - 4;
32
+ const dots = dotsCount > 0 ? Array.from({ length: dotsCount }).fill(".").join("") : "";
33
+
34
+ const routeText = routePath
35
+ .split("/")
36
+ .map((segment) => {
37
+ const isDynamicSegment = ["[", ":"].includes(segment[0] || "");
38
+
39
+ return isDynamicSegment ? colors.yellowBright(segment) : segment;
40
+ })
41
+ .join("/");
42
+
43
+ return ` ${methodText}${spaces}${routeText}${colors.grey(dots)}`;
44
+ };
45
+
46
+ const routesRender = (routesMap: Route[], options: { methods?: string[]; } = {}) => routesMap
47
+ .map((route) => {
48
+ if (options.methods && options.methods.includes(route.method)) {
49
+ return;
50
+ }
51
+
52
+ if (route.method === "GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS") {
53
+ // eslint-disable-next-line no-param-reassign
54
+ route.method = "ANY";
55
+ }
56
+
57
+ // eslint-disable-next-line consistent-return
58
+ return renderRoute(route.method, route.path.replace("/pages", ""));
59
+ })
60
+ .filter(Boolean);
61
+
62
+ export default routesRender;
@@ -0,0 +1 @@
1
+ export type Route = { path: string; method: string; tags: any; file: string };
@@ -0,0 +1,3 @@
1
+ export { default as RedocPage } from "./routes/pages/swagger/redoc";
2
+ export { default as SwaggerPage } from "./routes/pages/swagger/swagger";
3
+ export { default as getSwaggerStaticProps } from "./routes/pages/swagger/get-static-properties-swagger";
@@ -0,0 +1,6 @@
1
+ export * from "./index-browser";
2
+
3
+ // eslint-disable-next-line import/prefer-default-export
4
+ export { default as withOpenApi } from "./webpack/with-open-api";
5
+
6
+ export { default as swaggerApiRoute } from "./routes/api/swagger";