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.
- package/.env +1 -0
- package/bun.lock +530 -0
- package/package.json +18 -0
- package/src/configs/Logger.ts +211 -0
- package/src/configs/meta.ts +12 -0
- package/src/index.ts +35 -0
- package/src/openapi.ts +104 -0
- package/src/server.ts +94 -0
- package/src/template/ui.html +381 -0
- package/src/types.ts +25 -0
- package/src/ui.ts +26 -0
- package/tsconfig.json +20 -0
|
@@ -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
|
+
|