xypriss-swagger 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.
@@ -0,0 +1,211 @@
1
+ import { meta } from "./meta";
2
+
3
+ /**
4
+ * ANSI color codes for terminal output
5
+ */
6
+ const ANSI = {
7
+ reset: "\x1b[0m",
8
+ bold: "\x1b[1m",
9
+ dim: "\x1b[2m",
10
+
11
+ // Foreground colors
12
+ black: "\x1b[30m",
13
+ red: "\x1b[31m",
14
+ green: "\x1b[32m",
15
+ yellow: "\x1b[33m",
16
+ blue: "\x1b[34m",
17
+ magenta: "\x1b[35m",
18
+ cyan: "\x1b[36m",
19
+ white: "\x1b[37m",
20
+ gray: "\x1b[90m",
21
+
22
+ // Bright foreground
23
+ brightRed: "\x1b[91m",
24
+ brightGreen: "\x1b[92m",
25
+ brightYellow: "\x1b[93m",
26
+ brightBlue: "\x1b[94m",
27
+ brightMagenta: "\x1b[95m",
28
+ brightCyan: "\x1b[96m",
29
+ brightWhite: "\x1b[97m",
30
+
31
+ // Background colors
32
+ bgRed: "\x1b[41m",
33
+ bgGreen: "\x1b[42m",
34
+ bgYellow: "\x1b[43m",
35
+ bgBlue: "\x1b[44m",
36
+ bgMagenta: "\x1b[45m",
37
+ bgCyan: "\x1b[46m",
38
+ } as const;
39
+
40
+ type LogLevel =
41
+ | "info"
42
+ | "success"
43
+ | "warn"
44
+ | "error"
45
+ | "debug"
46
+ | "http"
47
+ | "swagger"
48
+ | "db"
49
+ | "auth";
50
+
51
+ interface LogConfig {
52
+ icon: string;
53
+ color: string;
54
+ label: string;
55
+ labelColor: string;
56
+ }
57
+
58
+ const LOG_CONFIGS: Record<LogLevel, LogConfig> = {
59
+ info: {
60
+ icon: "ℹ️ ",
61
+ color: ANSI.brightCyan,
62
+ label: "INFO",
63
+ labelColor: `${ANSI.bgCyan}${ANSI.black}`,
64
+ },
65
+ success: {
66
+ icon: "✅",
67
+ color: ANSI.brightGreen,
68
+ label: "OK",
69
+ labelColor: `${ANSI.bgGreen}${ANSI.black}`,
70
+ },
71
+ warn: {
72
+ icon: "⚠️ ",
73
+ color: ANSI.brightYellow,
74
+ label: "WARN",
75
+ labelColor: `${ANSI.bgYellow}${ANSI.black}`,
76
+ },
77
+ error: {
78
+ icon: "❌",
79
+ color: ANSI.brightRed,
80
+ label: "ERROR",
81
+ labelColor: `${ANSI.bgRed}${ANSI.white}`,
82
+ },
83
+ debug: {
84
+ icon: "🐛",
85
+ color: ANSI.gray,
86
+ label: "DEBUG",
87
+ labelColor: `${ANSI.bgMagenta}${ANSI.white}`,
88
+ },
89
+ http: {
90
+ icon: "🌐",
91
+ color: ANSI.brightBlue,
92
+ label: "HTTP",
93
+ labelColor: `${ANSI.bgBlue}${ANSI.white}`,
94
+ },
95
+ swagger: {
96
+ icon: "🛡️ ",
97
+ color: ANSI.brightMagenta,
98
+ label: "SWAGGER",
99
+ labelColor: `${ANSI.bgMagenta}${ANSI.white}`,
100
+ },
101
+ db: {
102
+ icon: "🗄️ ",
103
+ color: ANSI.yellow,
104
+ label: "DB",
105
+ labelColor: `${ANSI.bgYellow}${ANSI.black}`,
106
+ },
107
+ auth: {
108
+ icon: "🔐",
109
+ color: ANSI.brightYellow,
110
+ label: "AUTH",
111
+ labelColor: `${ANSI.bgYellow}${ANSI.black}`,
112
+ },
113
+ };
114
+
115
+ export class Logger {
116
+ private context: string;
117
+ private static showTimestamp = true;
118
+
119
+ constructor(context: string) {
120
+ this.context = context;
121
+ }
122
+
123
+ // ─── Static factory ───────────────────────────────────────────────────────
124
+
125
+ static for(context: string): Logger {
126
+ return new Logger(context);
127
+ }
128
+
129
+ static disableTimestamp(): void {
130
+ Logger.showTimestamp = false;
131
+ }
132
+
133
+ // ─── Core log method ──────────────────────────────────────────────────────
134
+
135
+ private log(level: LogLevel, ...args: unknown[]): void {
136
+ const cfg = LOG_CONFIGS[level];
137
+ const timestamp = Logger.showTimestamp
138
+ ? `${ANSI.dim}${ANSI.gray}${new Date().toISOString()}${ANSI.reset} `
139
+ : "";
140
+ const label = `${cfg.labelColor}${ANSI.bold} ${cfg.label} ${ANSI.reset}`;
141
+ const ctx = `${ANSI.dim}${ANSI.cyan}[${this.context}]${ANSI.reset}`;
142
+
143
+ // First arg gets the level color + icon, rest are passed raw so Node.js
144
+ // pretty-prints objects/arrays/Errors natively (no JSON.stringify needed).
145
+ const [first, ...rest] = args;
146
+ const prefix = `${timestamp}${label} ${ctx} ${cfg.color}${cfg.icon} ${String(first)}${ANSI.reset}`;
147
+
148
+ const consoleFn =
149
+ level === "error"
150
+ ? console.error
151
+ : level === "warn"
152
+ ? console.warn
153
+ : console.log;
154
+
155
+ rest.length > 0 ? consoleFn(prefix, ...rest) : consoleFn(prefix);
156
+ }
157
+
158
+ // ─── Public methods ───────────────────────────────────────────────────────
159
+
160
+ info(...args: unknown[]): void {
161
+ this.log("info", ...args);
162
+ }
163
+
164
+ success(...args: unknown[]): void {
165
+ this.log("success", ...args);
166
+ }
167
+
168
+ warn(...args: unknown[]): void {
169
+ this.log("warn", ...args);
170
+ }
171
+
172
+ error(...args: unknown[]): void {
173
+ this.log("error", ...args);
174
+ }
175
+
176
+ debug(...args: unknown[]): void {
177
+ this.log("debug", ...args);
178
+ }
179
+
180
+ http(...args: unknown[]): void {
181
+ this.log("http", ...args);
182
+ }
183
+
184
+ swagger(...args: unknown[]): void {
185
+ this.log("swagger", ...args);
186
+ }
187
+
188
+ db(...args: unknown[]): void {
189
+ this.log("db", ...args);
190
+ }
191
+
192
+ auth(...args: unknown[]): void {
193
+ this.log("auth", ...args);
194
+ }
195
+
196
+ // ─── Convenience: banner / separator ─────────────────────────────────────
197
+
198
+ banner(title: string): void {
199
+ const line = `${ANSI.cyan}${"─".repeat(60)}${ANSI.reset}`;
200
+ const centered = title
201
+ .padStart(30 + Math.floor(title.length / 2))
202
+ .padEnd(60);
203
+ console.log(`\n${line}`);
204
+ console.log(`${ANSI.bold}${ANSI.brightCyan}${centered}${ANSI.reset}`);
205
+ console.log(`${line}\n`);
206
+ }
207
+ }
208
+
209
+ // ─── Default singleton logger ──────────────────────────────────────────────
210
+
211
+ export const logger = Logger.for(meta.name);
@@ -0,0 +1,12 @@
1
+ import "xypriss";
2
+ import { ISwaggerJSONStructure } from "../types";
3
+
4
+ export const meta = __sys__.fs.readJsonSync(
5
+ __sys__.fs.join(__sys__.__root__, "package.json"),
6
+ ) as ISwaggerJSONStructure;
7
+
8
+ export const toPascalCase = (str: string, spliter = "-") =>
9
+ str
10
+ .split(spliter)
11
+ .map((n) => n[0].toUpperCase() + n.slice(1))
12
+ .join(" ");
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { logger, Logger } from "./configs/Logger";
2
+ import { meta } from "./configs/meta";
3
+ import { SwaggerServer } from "./server";
4
+ import { SwaggerConfig } from "./types";
5
+ import { Plugin } from "xypriss";
6
+
7
+ export function SwaggerPlugin(config: SwaggerConfig): any {
8
+ return Plugin.create({
9
+ name: meta.name,
10
+ version: meta.version,
11
+ description: meta.description,
12
+
13
+ onRegister(server) {
14
+ const log = Logger.for("Bootstrap");
15
+ log.info("Starting swagger plugin...");
16
+ },
17
+ onServerReady(server) {
18
+ logger.success("Swagger plugin is ready");
19
+
20
+ },
21
+ onServerStart(server) {
22
+ logger.success("Swagger plugin has started");
23
+
24
+ server.app.get("/swagger", (_req: any, res: any) => {
25
+ res.redirect(`http://localhost:${config.port}`);
26
+ });
27
+ },
28
+ onAuxiliaryServerDeploy(ops, server) {
29
+ SwaggerServer(config, ops, server);
30
+ },
31
+ });
32
+ }
33
+
34
+
35
+
package/src/openapi.ts ADDED
@@ -0,0 +1,104 @@
1
+ export interface OpenAPIConfig {
2
+ openapi: string;
3
+ info: {
4
+ title: string;
5
+ version: string;
6
+ description?: string;
7
+ };
8
+ paths: Record<string, any>;
9
+ components: {
10
+ securitySchemes?: Record<string, any>;
11
+ };
12
+ }
13
+
14
+ export function generateOpenAPI(registry: any[], config: any): OpenAPIConfig {
15
+ const doc: OpenAPIConfig = {
16
+ openapi: "3.0.0",
17
+ info: {
18
+ title:
19
+ __sys__.vars.__name__ ||
20
+ config.title ||
21
+ "XyPriss API Documentation",
22
+ version: config.version || "1.0.0",
23
+ description: config.description || "Generated by @xypriss/swagger",
24
+ },
25
+ paths: {},
26
+ components: {
27
+ securitySchemes: {
28
+ BearerAuth: {
29
+ type: "http",
30
+ scheme: "bearer",
31
+ },
32
+ },
33
+ },
34
+ };
35
+
36
+ for (const route of registry) {
37
+ if (!route.path || !route.method) continue;
38
+
39
+ // Convert Express-like path /users/:id to Swagger-like path /users/{id}
40
+ const openApiPath = route.path.replace(
41
+ /:([a-zA-Z0-9_]+)(\([^)]+\))?/g,
42
+ "{$1}",
43
+ );
44
+
45
+ if (!doc.paths[openApiPath]) {
46
+ doc.paths[openApiPath] = {};
47
+ }
48
+
49
+ const methodStr = (
50
+ Array.isArray(route.method) ? route.method[0] : route.method
51
+ ).toLowerCase();
52
+
53
+ // Base operation object
54
+ const operation: any = {
55
+ summary: route.meta?.summary || `${route.method} ${route.path}`,
56
+ description: route.meta?.description || "",
57
+ tags: route.meta?.tags || ["Default"],
58
+ parameters: [],
59
+ responses: {
60
+ "200": {
61
+ description: "Successful response",
62
+ },
63
+ },
64
+ };
65
+
66
+ // If guards are detected, optionally assume it requires Auth
67
+ if (route.hasGuards) {
68
+ operation.security = [{ BearerAuth: [] }];
69
+ }
70
+
71
+ // Add Path Parameters
72
+ if (route.paramNames && route.paramNames.length > 0) {
73
+ for (const param of route.paramNames) {
74
+ // Determine if there is a Regex constraint
75
+ const constraintMatch = route.path.match(
76
+ new RegExp(`:${param}\\\\(([^)]+)\\\\)`),
77
+ );
78
+ const pattern = constraintMatch
79
+ ? constraintMatch[1]
80
+ : undefined;
81
+
82
+ operation.parameters.push({
83
+ name: param,
84
+ in: "path",
85
+ required: true,
86
+ schema: {
87
+ type: "string", // fallback, precise type could depend on regex
88
+ pattern,
89
+ },
90
+ });
91
+ }
92
+ }
93
+
94
+ // Add additional meta (like requestBody, query params) if defined by user within meta.openapi
95
+ if (route.meta?.openapi) {
96
+ Object.assign(operation, route.meta.openapi);
97
+ }
98
+
99
+ doc.paths[openApiPath][methodStr] = operation;
100
+ }
101
+
102
+ return doc;
103
+ }
104
+
package/src/server.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { logger } from "./configs/Logger";
2
+ import { meta, toPascalCase } from "./configs/meta";
3
+ import { generateOpenAPI } from "./openapi";
4
+ import { SwaggerConfig } from "./types";
5
+ import { getSwaggerUIHtml } from "./ui";
6
+ import { Plugin } from "xypriss";
7
+
8
+ type auxis = NonNullable<
9
+ ReturnType<typeof Plugin.create>["onAuxiliaryServerDeploy"]
10
+ >;
11
+ export type OpsServerManager = Parameters<auxis>["0"] & {
12
+ getRouteRegistry?: () => any[];
13
+ };
14
+ export type XyPrissServer = Parameters<auxis>["1"];
15
+
16
+ export function SwaggerServer(
17
+ config: SwaggerConfig,
18
+ ops: OpsServerManager,
19
+ _XServer: XyPrissServer,
20
+ ) {
21
+ const docPath = config.path || "/docs";
22
+ const specPath = `${docPath}/swagger.json`;
23
+ // console.log("plugin root path: ", __sys__.__root__);
24
+ // console.log(
25
+ // "😇 env de HELLO depuis le plugin: ",
26
+ // __sys__.__env__.get("HELLO"),
27
+ // );
28
+ // console.log(
29
+ // "🤧 env de COMMON_VAR du root du project (devrait être undefined): ",
30
+ // __sys__.__env__.get("COMMON_VAR"),
31
+ // );
32
+ const workspaceSYS = __sys__.plugins.get(meta.name);
33
+
34
+ // console.log("workspaceFS: ", workspaceFS);
35
+
36
+ if (!workspaceSYS) {
37
+ throw new Error(
38
+ toPascalCase(meta.name, "-") +
39
+ " is not authorized in your xypriss.config.jsonc or xypriss.config.json. Please add ",
40
+ );
41
+ }
42
+
43
+ const port = config.port || 7070;
44
+ const server = ops.createAuxiliaryServer({
45
+ server: { port },
46
+ security: {
47
+ enabled: false,
48
+ },
49
+ });
50
+
51
+ // Serve the raw OpenAPI JSON specification
52
+ server.get(specPath, (_req, res) => {
53
+ try {
54
+ let registry: any[] = [];
55
+
56
+ if (ops.getRouteRegistry) {
57
+ registry = ops.getRouteRegistry();
58
+ }
59
+
60
+ const spec = generateOpenAPI(registry, config);
61
+ res.json(spec);
62
+ } catch (error) {
63
+ logger.error("Error generating OpenAPI spec:", error);
64
+ res.status(500).json({
65
+ error: "Failed to generate documentation",
66
+ });
67
+ }
68
+ });
69
+
70
+ // Serve the Swagger HTML Viewer
71
+ server.get(docPath, (_req, res) => {
72
+ const html = getSwaggerUIHtml(
73
+ specPath,
74
+ config.title || workspaceSYS?.vars?.__name__ || "API Documentation",
75
+ );
76
+ res.html(html);
77
+ });
78
+
79
+ // Redirect root path of the sub-server to docPath
80
+ server.redirect("/", docPath, 301);
81
+
82
+ // Boot the auxiliary server immediately
83
+ const url = `http://localhost:${port}${docPath}`;
84
+ server.start(() => {
85
+ logger.swagger(
86
+ `${toPascalCase(meta.name)} Server isolated on port ${port}`,
87
+ );
88
+ logger.http(`GET ${url}`);
89
+
90
+ });
91
+ }
92
+
93
+
94
+