@forinda/kickjs-swagger 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Felix Orinda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,144 @@
1
+ import { Express } from 'express';
2
+ import { AppAdapter, Container } from '@forinda/kickjs-core';
3
+
4
+ /**
5
+ * Interface for converting validation library schemas to JSON Schema.
6
+ *
7
+ * KickJS ships with a Zod parser by default. To use a different validation
8
+ * library (Yup, Joi, Valibot, ArkType, etc.), implement this interface and
9
+ * pass it to the SwaggerAdapter.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import Joi from 'joi'
14
+ * import joiToJson from 'joi-to-json'
15
+ *
16
+ * const joiParser: SchemaParser = {
17
+ * name: 'joi',
18
+ * supports: (schema) => Joi.isSchema(schema),
19
+ * toJsonSchema: (schema) => joiToJson(schema),
20
+ * }
21
+ *
22
+ * new SwaggerAdapter({ schemaParser: joiParser })
23
+ * ```
24
+ */
25
+ interface SchemaParser {
26
+ /** Human-readable name for logging/debugging */
27
+ readonly name: string;
28
+ /**
29
+ * Return true if this parser can handle the given schema object.
30
+ * Called before `toJsonSchema` to allow graceful fallback.
31
+ */
32
+ supports(schema: unknown): boolean;
33
+ /**
34
+ * Convert a validation schema to a JSON Schema object.
35
+ * Should return a plain object conforming to JSON Schema draft-07 or later.
36
+ * Must not include the top-level `$schema` key — the builder adds it.
37
+ */
38
+ toJsonSchema(schema: unknown): Record<string, unknown>;
39
+ }
40
+ /**
41
+ * Default schema parser for Zod v4+.
42
+ * Uses Zod's built-in `.toJSONSchema()` instance method.
43
+ */
44
+ declare const zodSchemaParser: SchemaParser;
45
+
46
+ interface ApiOperationOptions {
47
+ summary?: string;
48
+ description?: string;
49
+ operationId?: string;
50
+ deprecated?: boolean;
51
+ }
52
+ interface ApiResponseOptions {
53
+ status: number;
54
+ description?: string;
55
+ schema?: any;
56
+ }
57
+ /** Attach operation metadata to a route handler */
58
+ declare function ApiOperation(options: ApiOperationOptions): MethodDecorator;
59
+ /** Document a response status. Can be stacked multiple times. */
60
+ declare function ApiResponse(options: ApiResponseOptions): MethodDecorator;
61
+ /** Apply OpenAPI tags at class or method level */
62
+ declare function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator;
63
+ /** Mark endpoint as requiring Bearer token auth */
64
+ declare function ApiBearerAuth(name?: string): ClassDecorator & MethodDecorator;
65
+ /** Exclude a controller or method from the OpenAPI spec */
66
+ declare function ApiExclude(): ClassDecorator & MethodDecorator;
67
+
68
+ interface OpenAPIInfo {
69
+ title: string;
70
+ version: string;
71
+ description?: string;
72
+ }
73
+ interface SwaggerOptions {
74
+ info?: Partial<OpenAPIInfo>;
75
+ servers?: {
76
+ url: string;
77
+ description?: string;
78
+ }[];
79
+ bearerAuth?: boolean;
80
+ /**
81
+ * Pluggable schema parser for converting validation schemas to JSON Schema.
82
+ * Defaults to `zodSchemaParser` which handles Zod v4+ schemas.
83
+ *
84
+ * Override this to use Yup, Joi, Valibot, ArkType, or any other library.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * new SwaggerAdapter({
89
+ * schemaParser: myYupParser,
90
+ * })
91
+ * ```
92
+ */
93
+ schemaParser?: SchemaParser;
94
+ }
95
+ /** Register a controller for OpenAPI introspection (called by Application during route mounting) */
96
+ declare function registerControllerForDocs(controllerClass: any, mountPath: string): void;
97
+ /** Clear all registered routes (for HMR) */
98
+ declare function clearRegisteredRoutes(): void;
99
+ /** Build a full OpenAPI 3.0.3 spec from registered controllers and their decorators */
100
+ declare function buildOpenAPISpec(options?: SwaggerOptions): any;
101
+
102
+ interface SwaggerAdapterOptions extends SwaggerOptions {
103
+ /** Path to serve Swagger UI (default: '/docs') */
104
+ docsPath?: string;
105
+ /** Path to serve ReDoc (default: '/redoc') */
106
+ redocPath?: string;
107
+ /** Path to serve the raw JSON spec (default: '/openapi.json') */
108
+ specPath?: string;
109
+ }
110
+ /**
111
+ * Swagger adapter — auto-generates OpenAPI spec from decorators and serves docs.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * bootstrap({
116
+ * modules,
117
+ * adapters: [
118
+ * new SwaggerAdapter({
119
+ * info: { title: 'My API', version: '1.0.0' },
120
+ * }),
121
+ * ],
122
+ * })
123
+ * ```
124
+ *
125
+ * Endpoints:
126
+ * GET /docs — Swagger UI
127
+ * GET /redoc — ReDoc
128
+ * GET /openapi.json — Raw OpenAPI 3.0.3 spec
129
+ */
130
+ declare class SwaggerAdapter implements AppAdapter {
131
+ private options;
132
+ name: string;
133
+ constructor(options?: SwaggerAdapterOptions);
134
+ /** Collect controller metadata as routes are mounted */
135
+ onRouteMount(controllerClass: any, mountPath: string): void;
136
+ beforeMount(app: Express, _container: Container): void;
137
+ }
138
+
139
+ /** Generate Swagger UI HTML that loads from CDN */
140
+ declare function swaggerUIHtml(specUrl: string, title?: string): string;
141
+ /** Generate ReDoc HTML that loads from CDN */
142
+ declare function redocHtml(specUrl: string, title?: string): string;
143
+
144
+ export { ApiBearerAuth, ApiExclude, ApiOperation, type ApiOperationOptions, ApiResponse, type ApiResponseOptions, ApiTags, type OpenAPIInfo, type SchemaParser, SwaggerAdapter, type SwaggerAdapterOptions, type SwaggerOptions, buildOpenAPISpec, clearRegisteredRoutes, redocHtml, registerControllerForDocs, swaggerUIHtml, zodSchemaParser };
package/dist/index.js ADDED
@@ -0,0 +1,442 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/schema-parser.ts
5
+ var zodSchemaParser = {
6
+ name: "zod",
7
+ supports(schema) {
8
+ return schema != null && typeof schema === "object" && typeof schema.safeParse === "function" && typeof schema.toJSONSchema === "function";
9
+ },
10
+ toJsonSchema(schema) {
11
+ const { $schema: _, ...rest } = schema.toJSONSchema();
12
+ return rest;
13
+ }
14
+ };
15
+
16
+ // src/decorators.ts
17
+ import "reflect-metadata";
18
+ var SWAGGER_KEYS = {
19
+ OPERATION: /* @__PURE__ */ Symbol("kick:swagger:operation"),
20
+ RESPONSES: /* @__PURE__ */ Symbol("kick:swagger:responses"),
21
+ TAGS: /* @__PURE__ */ Symbol("kick:swagger:tags"),
22
+ BEARER_AUTH: /* @__PURE__ */ Symbol("kick:swagger:bearer"),
23
+ EXCLUDE: /* @__PURE__ */ Symbol("kick:swagger:exclude")
24
+ };
25
+ function ApiOperation(options) {
26
+ return (target, propertyKey) => {
27
+ Reflect.defineMetadata(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey);
28
+ };
29
+ }
30
+ __name(ApiOperation, "ApiOperation");
31
+ function ApiResponse(options) {
32
+ return (target, propertyKey) => {
33
+ const existing = Reflect.getMetadata(SWAGGER_KEYS.RESPONSES, target.constructor, propertyKey) || [];
34
+ Reflect.defineMetadata(SWAGGER_KEYS.RESPONSES, [
35
+ ...existing,
36
+ options
37
+ ], target.constructor, propertyKey);
38
+ };
39
+ }
40
+ __name(ApiResponse, "ApiResponse");
41
+ function ApiTags(...tags) {
42
+ return (target, propertyKey) => {
43
+ if (propertyKey) {
44
+ Reflect.defineMetadata(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey);
45
+ } else {
46
+ Reflect.defineMetadata(SWAGGER_KEYS.TAGS, tags, target);
47
+ }
48
+ };
49
+ }
50
+ __name(ApiTags, "ApiTags");
51
+ function ApiBearerAuth(name = "BearerAuth") {
52
+ return (target, propertyKey) => {
53
+ if (propertyKey) {
54
+ Reflect.defineMetadata(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey);
55
+ } else {
56
+ Reflect.defineMetadata(SWAGGER_KEYS.BEARER_AUTH, name, target);
57
+ }
58
+ };
59
+ }
60
+ __name(ApiBearerAuth, "ApiBearerAuth");
61
+ function ApiExclude() {
62
+ return (target, propertyKey) => {
63
+ if (propertyKey) {
64
+ Reflect.defineMetadata(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey);
65
+ } else {
66
+ Reflect.defineMetadata(SWAGGER_KEYS.EXCLUDE, true, target);
67
+ }
68
+ };
69
+ }
70
+ __name(ApiExclude, "ApiExclude");
71
+
72
+ // src/openapi-builder.ts
73
+ import "reflect-metadata";
74
+ import { METADATA } from "@forinda/kickjs-core";
75
+ var registeredRoutes = [];
76
+ function registerControllerForDocs(controllerClass, mountPath) {
77
+ registeredRoutes.push({
78
+ controllerClass,
79
+ mountPath
80
+ });
81
+ }
82
+ __name(registerControllerForDocs, "registerControllerForDocs");
83
+ function clearRegisteredRoutes() {
84
+ registeredRoutes.length = 0;
85
+ }
86
+ __name(clearRegisteredRoutes, "clearRegisteredRoutes");
87
+ function buildOpenAPISpec(options = {}) {
88
+ const parser = options.schemaParser ?? zodSchemaParser;
89
+ const toJsonSchema = /* @__PURE__ */ __name((schema) => {
90
+ try {
91
+ if (!parser.supports(schema)) return null;
92
+ return parser.toJsonSchema(schema);
93
+ } catch {
94
+ return null;
95
+ }
96
+ }, "toJsonSchema");
97
+ const componentSchemas = {};
98
+ let schemaCounter = 0;
99
+ const registerSchema = /* @__PURE__ */ __name((jsonSchema, hint) => {
100
+ let name = jsonSchema.title || jsonSchema.label || hint || "";
101
+ if (!name) {
102
+ name = `Schema${++schemaCounter}`;
103
+ }
104
+ name = name.replace(/[^a-zA-Z0-9]/g, "");
105
+ if (!componentSchemas[name]) {
106
+ const clean = {
107
+ ...jsonSchema
108
+ };
109
+ delete clean.title;
110
+ delete clean.label;
111
+ delete clean.$schema;
112
+ componentSchemas[name] = clean;
113
+ }
114
+ return {
115
+ $ref: `#/components/schemas/${name}`
116
+ };
117
+ }, "registerSchema");
118
+ const spec = {
119
+ openapi: "3.0.3",
120
+ info: {
121
+ title: options.info?.title || "API",
122
+ version: options.info?.version || "1.0.0",
123
+ ...options.info?.description ? {
124
+ description: options.info.description
125
+ } : {}
126
+ },
127
+ paths: {},
128
+ components: {
129
+ schemas: {},
130
+ securitySchemes: {}
131
+ },
132
+ tags: []
133
+ };
134
+ if (options.servers) {
135
+ spec.servers = options.servers;
136
+ }
137
+ const allTags = /* @__PURE__ */ new Set();
138
+ const securitySchemes = {};
139
+ for (const { controllerClass, mountPath } of registeredRoutes) {
140
+ if (Reflect.getMetadata(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue;
141
+ const routes = Reflect.getMetadata(METADATA.ROUTES, controllerClass) || [];
142
+ const classTags = Reflect.getMetadata(SWAGGER_KEYS.TAGS, controllerClass) || [];
143
+ const classAuth = Reflect.getMetadata(SWAGGER_KEYS.BEARER_AUTH, controllerClass);
144
+ const controllerPath = Reflect.getMetadata(METADATA.CONTROLLER_PATH, controllerClass) || "/";
145
+ for (const route of routes) {
146
+ if (Reflect.getMetadata(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName)) continue;
147
+ let routePath = route.path === "/" ? "" : route.path;
148
+ let fullPath = mountPath + (controllerPath === "/" ? "" : controllerPath) + routePath;
149
+ if (!fullPath) fullPath = "/";
150
+ const openApiPath = fullPath.replace(/:([a-zA-Z_]+)/g, "{$1}");
151
+ const method = route.method.toLowerCase();
152
+ const operation = Reflect.getMetadata(SWAGGER_KEYS.OPERATION, controllerClass, route.handlerName) || {};
153
+ const responses = Reflect.getMetadata(SWAGGER_KEYS.RESPONSES, controllerClass, route.handlerName) || [];
154
+ const methodTags = Reflect.getMetadata(SWAGGER_KEYS.TAGS, controllerClass, route.handlerName) || [];
155
+ const methodAuth = Reflect.getMetadata(SWAGGER_KEYS.BEARER_AUTH, controllerClass, route.handlerName);
156
+ const tags = methodTags.length > 0 ? methodTags : classTags;
157
+ tags.forEach((t) => allTags.add(t));
158
+ const op = {
159
+ ...tags.length > 0 ? {
160
+ tags
161
+ } : {},
162
+ ...operation.summary ? {
163
+ summary: operation.summary
164
+ } : {},
165
+ ...operation.description ? {
166
+ description: operation.description
167
+ } : {},
168
+ ...operation.operationId ? {
169
+ operationId: operation.operationId
170
+ } : {},
171
+ ...operation.deprecated ? {
172
+ deprecated: true
173
+ } : {},
174
+ parameters: [],
175
+ responses: {}
176
+ };
177
+ const paramMatches = fullPath.match(/:([a-zA-Z_]+)/g) || [];
178
+ for (const match of paramMatches) {
179
+ const paramName = match.slice(1);
180
+ let schema = {
181
+ type: "string"
182
+ };
183
+ if (route.validation?.params) {
184
+ const jsonSchema = toJsonSchema(route.validation.params);
185
+ if (jsonSchema?.properties && typeof jsonSchema.properties === "object") {
186
+ const props = jsonSchema.properties;
187
+ if (props[paramName]) {
188
+ schema = props[paramName];
189
+ }
190
+ }
191
+ }
192
+ op.parameters.push({
193
+ name: paramName,
194
+ in: "path",
195
+ required: true,
196
+ schema
197
+ });
198
+ }
199
+ if (route.validation?.query) {
200
+ const jsonSchema = toJsonSchema(route.validation.query);
201
+ if (jsonSchema?.properties && typeof jsonSchema.properties === "object") {
202
+ const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : [];
203
+ for (const [name, propSchema] of Object.entries(jsonSchema.properties)) {
204
+ op.parameters.push({
205
+ name,
206
+ in: "query",
207
+ required: required.includes(name),
208
+ schema: propSchema
209
+ });
210
+ }
211
+ }
212
+ }
213
+ if (op.parameters.length === 0) delete op.parameters;
214
+ if (route.validation?.body && [
215
+ "post",
216
+ "put",
217
+ "patch"
218
+ ].includes(method)) {
219
+ const bodySchema = toJsonSchema(route.validation.body);
220
+ if (bodySchema) {
221
+ const ref = registerSchema(bodySchema, `${route.handlerName}Body`);
222
+ op.requestBody = {
223
+ required: true,
224
+ content: {
225
+ "application/json": {
226
+ schema: ref
227
+ }
228
+ }
229
+ };
230
+ }
231
+ }
232
+ const fileUpload = Reflect.getMetadata(METADATA.FILE_UPLOAD, controllerClass, route.handlerName);
233
+ if (fileUpload) {
234
+ const properties = {};
235
+ if (fileUpload.fieldName) {
236
+ properties[fileUpload.fieldName] = {
237
+ type: "string",
238
+ format: "binary"
239
+ };
240
+ }
241
+ op.requestBody = {
242
+ required: true,
243
+ content: {
244
+ "multipart/form-data": {
245
+ schema: {
246
+ type: "object",
247
+ properties
248
+ }
249
+ }
250
+ }
251
+ };
252
+ }
253
+ if (responses.length > 0) {
254
+ for (const resp of responses) {
255
+ op.responses[String(resp.status)] = {
256
+ description: resp.description || "",
257
+ ...resp.schema ? (() => {
258
+ const converted = typeof resp.schema === "function" || typeof resp.schema === "object" ? toJsonSchema(resp.schema) : null;
259
+ const finalSchema = converted ? registerSchema(converted, `${route.handlerName}Response${resp.status}`) : typeof resp.schema === "object" ? resp.schema : void 0;
260
+ return finalSchema ? {
261
+ content: {
262
+ "application/json": {
263
+ schema: finalSchema
264
+ }
265
+ }
266
+ } : {};
267
+ })() : {}
268
+ };
269
+ }
270
+ } else {
271
+ const defaultStatus = method === "post" ? "201" : method === "delete" ? "204" : "200";
272
+ op.responses[defaultStatus] = {
273
+ description: "Successful operation"
274
+ };
275
+ if (route.validation?.body) {
276
+ op.responses["422"] = {
277
+ description: "Validation error"
278
+ };
279
+ }
280
+ }
281
+ const authName = methodAuth || classAuth;
282
+ if (authName) {
283
+ op.security = [
284
+ {
285
+ [authName]: []
286
+ }
287
+ ];
288
+ securitySchemes[authName] = {
289
+ type: "http",
290
+ scheme: "bearer",
291
+ bearerFormat: "JWT"
292
+ };
293
+ }
294
+ if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {};
295
+ spec.paths[openApiPath][method] = op;
296
+ }
297
+ }
298
+ spec.tags = Array.from(allTags).map((name) => ({
299
+ name
300
+ }));
301
+ spec.components.securitySchemes = securitySchemes;
302
+ if (options.bearerAuth) {
303
+ if (!securitySchemes.BearerAuth) {
304
+ spec.components.securitySchemes.BearerAuth = {
305
+ type: "http",
306
+ scheme: "bearer",
307
+ bearerFormat: "JWT"
308
+ };
309
+ }
310
+ spec.security = [
311
+ {
312
+ BearerAuth: []
313
+ }
314
+ ];
315
+ }
316
+ spec.components.schemas = componentSchemas;
317
+ if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas;
318
+ if (Object.keys(spec.components.securitySchemes).length === 0) delete spec.components.securitySchemes;
319
+ if (Object.keys(spec.components).length === 0) delete spec.components;
320
+ return spec;
321
+ }
322
+ __name(buildOpenAPISpec, "buildOpenAPISpec");
323
+
324
+ // src/swagger.adapter.ts
325
+ import { Router } from "express";
326
+ import { Logger } from "@forinda/kickjs-core";
327
+
328
+ // src/ui.ts
329
+ function escapeHtml(str) {
330
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
331
+ }
332
+ __name(escapeHtml, "escapeHtml");
333
+ function swaggerUIHtml(specUrl, title = "API Docs") {
334
+ const safeTitle = escapeHtml(title);
335
+ const safeUrl = JSON.stringify(specUrl).replace(/</g, "\\u003c");
336
+ return `<!DOCTYPE html>
337
+ <html lang="en">
338
+ <head>
339
+ <meta charset="UTF-8">
340
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
341
+ <title>${safeTitle}</title>
342
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
343
+ </head>
344
+ <body>
345
+ <div id="swagger-ui"></div>
346
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
347
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
348
+ <script>
349
+ SwaggerUIBundle({
350
+ url: ${safeUrl},
351
+ dom_id: '#swagger-ui',
352
+ deepLinking: true,
353
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
354
+ plugins: [SwaggerUIBundle.plugins.DownloadUrl],
355
+ layout: 'StandaloneLayout',
356
+ });
357
+ </script>
358
+ </body>
359
+ </html>`;
360
+ }
361
+ __name(swaggerUIHtml, "swaggerUIHtml");
362
+ function redocHtml(specUrl, title = "API Docs") {
363
+ const safeTitle = escapeHtml(title);
364
+ const safeUrl = escapeHtml(specUrl);
365
+ return `<!DOCTYPE html>
366
+ <html lang="en">
367
+ <head>
368
+ <meta charset="UTF-8">
369
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
370
+ <title>${safeTitle}</title>
371
+ </head>
372
+ <body>
373
+ <redoc spec-url="${safeUrl}"></redoc>
374
+ <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
375
+ </body>
376
+ </html>`;
377
+ }
378
+ __name(redocHtml, "redocHtml");
379
+
380
+ // src/swagger.adapter.ts
381
+ var log = Logger.for("SwaggerAdapter");
382
+ var SwaggerAdapter = class {
383
+ static {
384
+ __name(this, "SwaggerAdapter");
385
+ }
386
+ options;
387
+ name = "SwaggerAdapter";
388
+ constructor(options = {}) {
389
+ this.options = options;
390
+ }
391
+ /** Collect controller metadata as routes are mounted */
392
+ onRouteMount(controllerClass, mountPath) {
393
+ registerControllerForDocs(controllerClass, mountPath);
394
+ }
395
+ beforeMount(app, _container) {
396
+ clearRegisteredRoutes();
397
+ const docsPath = this.options.docsPath ?? "/docs";
398
+ const redocPath = this.options.redocPath ?? "/redoc";
399
+ const specPath = this.options.specPath ?? "/openapi.json";
400
+ const docsRouter = Router();
401
+ docsRouter.use((_req, res, next) => {
402
+ res.setHeader("Content-Security-Policy", [
403
+ "default-src 'self'",
404
+ "script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net",
405
+ "style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com",
406
+ "font-src 'self' https://fonts.gstatic.com",
407
+ "img-src 'self' data: https://unpkg.com",
408
+ "connect-src 'self'"
409
+ ].join("; "));
410
+ next();
411
+ });
412
+ docsRouter.get(specPath, (_req, res) => {
413
+ const spec = buildOpenAPISpec(this.options);
414
+ res.json(spec);
415
+ });
416
+ docsRouter.get(docsPath, (_req, res) => {
417
+ res.type("html").send(swaggerUIHtml(specPath, this.options.info?.title));
418
+ });
419
+ docsRouter.get(redocPath, (_req, res) => {
420
+ res.type("html").send(redocHtml(specPath, this.options.info?.title));
421
+ });
422
+ app.use(docsRouter);
423
+ log.info(`Swagger UI: ${docsPath}`);
424
+ log.info(`ReDoc: ${redocPath}`);
425
+ log.info(`OpenAPI spec: ${specPath}`);
426
+ }
427
+ };
428
+ export {
429
+ ApiBearerAuth,
430
+ ApiExclude,
431
+ ApiOperation,
432
+ ApiResponse,
433
+ ApiTags,
434
+ SwaggerAdapter,
435
+ buildOpenAPISpec,
436
+ clearRegisteredRoutes,
437
+ redocHtml,
438
+ registerControllerForDocs,
439
+ swaggerUIHtml,
440
+ zodSchemaParser
441
+ };
442
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema-parser.ts","../src/decorators.ts","../src/openapi-builder.ts","../src/swagger.adapter.ts","../src/ui.ts"],"sourcesContent":["/**\n * Interface for converting validation library schemas to JSON Schema.\n *\n * KickJS ships with a Zod parser by default. To use a different validation\n * library (Yup, Joi, Valibot, ArkType, etc.), implement this interface and\n * pass it to the SwaggerAdapter.\n *\n * @example\n * ```ts\n * import Joi from 'joi'\n * import joiToJson from 'joi-to-json'\n *\n * const joiParser: SchemaParser = {\n * name: 'joi',\n * supports: (schema) => Joi.isSchema(schema),\n * toJsonSchema: (schema) => joiToJson(schema),\n * }\n *\n * new SwaggerAdapter({ schemaParser: joiParser })\n * ```\n */\nexport interface SchemaParser {\n /** Human-readable name for logging/debugging */\n readonly name: string\n\n /**\n * Return true if this parser can handle the given schema object.\n * Called before `toJsonSchema` to allow graceful fallback.\n */\n supports(schema: unknown): boolean\n\n /**\n * Convert a validation schema to a JSON Schema object.\n * Should return a plain object conforming to JSON Schema draft-07 or later.\n * Must not include the top-level `$schema` key — the builder adds it.\n */\n toJsonSchema(schema: unknown): Record<string, unknown>\n}\n\n/**\n * Default schema parser for Zod v4+.\n * Uses Zod's built-in `.toJSONSchema()` instance method.\n */\nexport const zodSchemaParser: SchemaParser = {\n name: 'zod',\n\n supports(schema: unknown): boolean {\n return (\n schema != null &&\n typeof schema === 'object' &&\n typeof (schema as any).safeParse === 'function' &&\n typeof (schema as any).toJSONSchema === 'function'\n )\n },\n\n toJsonSchema(schema: unknown): Record<string, unknown> {\n const { $schema: _, ...rest } = (schema as any).toJSONSchema() as Record<string, unknown>\n return rest\n },\n}\n","import 'reflect-metadata'\n\nconst SWAGGER_KEYS = {\n OPERATION: Symbol('kick:swagger:operation'),\n RESPONSES: Symbol('kick:swagger:responses'),\n TAGS: Symbol('kick:swagger:tags'),\n BEARER_AUTH: Symbol('kick:swagger:bearer'),\n EXCLUDE: Symbol('kick:swagger:exclude'),\n}\n\nexport { SWAGGER_KEYS }\n\nexport interface ApiOperationOptions {\n summary?: string\n description?: string\n operationId?: string\n deprecated?: boolean\n}\n\nexport interface ApiResponseOptions {\n status: number\n description?: string\n schema?: any\n}\n\n/** Attach operation metadata to a route handler */\nexport function ApiOperation(options: ApiOperationOptions): MethodDecorator {\n return (target, propertyKey) => {\n Reflect.defineMetadata(SWAGGER_KEYS.OPERATION, options, target.constructor, propertyKey)\n }\n}\n\n/** Document a response status. Can be stacked multiple times. */\nexport function ApiResponse(options: ApiResponseOptions): MethodDecorator {\n return (target, propertyKey) => {\n const existing: ApiResponseOptions[] =\n Reflect.getMetadata(SWAGGER_KEYS.RESPONSES, target.constructor, propertyKey) || []\n Reflect.defineMetadata(\n SWAGGER_KEYS.RESPONSES,\n [...existing, options],\n target.constructor,\n propertyKey,\n )\n }\n}\n\n/** Apply OpenAPI tags at class or method level */\nexport function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n Reflect.defineMetadata(SWAGGER_KEYS.TAGS, tags, target.constructor, propertyKey)\n } else {\n Reflect.defineMetadata(SWAGGER_KEYS.TAGS, tags, target)\n }\n }\n}\n\n/** Mark endpoint as requiring Bearer token auth */\nexport function ApiBearerAuth(name = 'BearerAuth'): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n Reflect.defineMetadata(SWAGGER_KEYS.BEARER_AUTH, name, target.constructor, propertyKey)\n } else {\n Reflect.defineMetadata(SWAGGER_KEYS.BEARER_AUTH, name, target)\n }\n }\n}\n\n/** Exclude a controller or method from the OpenAPI spec */\nexport function ApiExclude(): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n Reflect.defineMetadata(SWAGGER_KEYS.EXCLUDE, true, target.constructor, propertyKey)\n } else {\n Reflect.defineMetadata(SWAGGER_KEYS.EXCLUDE, true, target)\n }\n }\n}\n","import 'reflect-metadata'\nimport { METADATA, type RouteDefinition } from '@forinda/kickjs-core'\nimport { SWAGGER_KEYS, type ApiOperationOptions, type ApiResponseOptions } from './decorators'\nimport { zodSchemaParser, type SchemaParser } from './schema-parser'\n\nexport interface OpenAPIInfo {\n title: string\n version: string\n description?: string\n}\n\nexport interface SwaggerOptions {\n info?: Partial<OpenAPIInfo>\n servers?: { url: string; description?: string }[]\n bearerAuth?: boolean\n /**\n * Pluggable schema parser for converting validation schemas to JSON Schema.\n * Defaults to `zodSchemaParser` which handles Zod v4+ schemas.\n *\n * Override this to use Yup, Joi, Valibot, ArkType, or any other library.\n *\n * @example\n * ```ts\n * new SwaggerAdapter({\n * schemaParser: myYupParser,\n * })\n * ```\n */\n schemaParser?: SchemaParser\n}\n\ninterface RegisteredRoute {\n controllerClass: any\n mountPath: string\n}\n\nconst registeredRoutes: RegisteredRoute[] = []\n\n/** Register a controller for OpenAPI introspection (called by Application during route mounting) */\nexport function registerControllerForDocs(controllerClass: any, mountPath: string): void {\n registeredRoutes.push({ controllerClass, mountPath })\n}\n\n/** Clear all registered routes (for HMR) */\nexport function clearRegisteredRoutes(): void {\n registeredRoutes.length = 0\n}\n\n/** Build a full OpenAPI 3.0.3 spec from registered controllers and their decorators */\nexport function buildOpenAPISpec(options: SwaggerOptions = {}): any {\n const parser = options.schemaParser ?? zodSchemaParser\n\n /** Convert a validation schema to JSON Schema using the configured parser */\n const toJsonSchema = (schema: unknown): Record<string, unknown> | null => {\n try {\n if (!parser.supports(schema)) return null\n return parser.toJsonSchema(schema)\n } catch {\n return null\n }\n }\n\n const componentSchemas: Record<string, any> = {}\n let schemaCounter = 0\n\n /**\n * Register a schema in components.schemas and return a $ref pointer.\n * If the schema has a title/label, use that as the name. Otherwise generate one.\n */\n const registerSchema = (jsonSchema: Record<string, unknown>, hint?: string): any => {\n // Try to extract a name from the schema\n let name = (jsonSchema.title as string) || (jsonSchema.label as string) || hint || ''\n if (!name) {\n name = `Schema${++schemaCounter}`\n }\n // Sanitize name for OpenAPI (remove spaces, special chars)\n name = name.replace(/[^a-zA-Z0-9]/g, '')\n\n // Avoid duplicates — if already registered with same name, reuse\n if (!componentSchemas[name]) {\n const clean = { ...jsonSchema }\n delete clean.title\n delete clean.label\n delete clean.$schema\n componentSchemas[name] = clean\n }\n return { $ref: `#/components/schemas/${name}` }\n }\n\n const spec: any = {\n openapi: '3.0.3',\n info: {\n title: options.info?.title || 'API',\n version: options.info?.version || '1.0.0',\n ...(options.info?.description ? { description: options.info.description } : {}),\n },\n paths: {},\n components: { schemas: {}, securitySchemes: {} },\n tags: [],\n }\n\n if (options.servers) {\n spec.servers = options.servers\n }\n\n const allTags = new Set<string>()\n const securitySchemes: Record<string, any> = {}\n\n for (const { controllerClass, mountPath } of registeredRoutes) {\n // Skip excluded controllers\n if (Reflect.getMetadata(SWAGGER_KEYS.EXCLUDE, controllerClass)) continue\n\n const routes: RouteDefinition[] = Reflect.getMetadata(METADATA.ROUTES, controllerClass) || []\n const classTags: string[] = Reflect.getMetadata(SWAGGER_KEYS.TAGS, controllerClass) || []\n const classAuth: string | undefined = Reflect.getMetadata(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n )\n const controllerPath = Reflect.getMetadata(METADATA.CONTROLLER_PATH, controllerClass) || '/'\n\n for (const route of routes) {\n // Skip excluded methods\n if (Reflect.getMetadata(SWAGGER_KEYS.EXCLUDE, controllerClass, route.handlerName)) continue\n\n // Build the full path\n let routePath = route.path === '/' ? '' : route.path\n let fullPath = mountPath + (controllerPath === '/' ? '' : controllerPath) + routePath\n if (!fullPath) fullPath = '/'\n\n // Convert Express :param to OpenAPI {param}\n const openApiPath = fullPath.replace(/:([a-zA-Z_]+)/g, '{$1}')\n const method = route.method.toLowerCase()\n\n // Gather metadata\n const operation: ApiOperationOptions =\n Reflect.getMetadata(SWAGGER_KEYS.OPERATION, controllerClass, route.handlerName) || {}\n const responses: ApiResponseOptions[] =\n Reflect.getMetadata(SWAGGER_KEYS.RESPONSES, controllerClass, route.handlerName) || []\n const methodTags: string[] =\n Reflect.getMetadata(SWAGGER_KEYS.TAGS, controllerClass, route.handlerName) || []\n const methodAuth: string | undefined = Reflect.getMetadata(\n SWAGGER_KEYS.BEARER_AUTH,\n controllerClass,\n route.handlerName,\n )\n\n // Tags — method level overrides class level\n const tags = methodTags.length > 0 ? methodTags : classTags\n tags.forEach((t) => allTags.add(t))\n\n // Build operation object\n const op: any = {\n ...(tags.length > 0 ? { tags } : {}),\n ...(operation.summary ? { summary: operation.summary } : {}),\n ...(operation.description ? { description: operation.description } : {}),\n ...(operation.operationId ? { operationId: operation.operationId } : {}),\n ...(operation.deprecated ? { deprecated: true } : {}),\n parameters: [],\n responses: {},\n }\n\n // Path parameters\n const paramMatches = fullPath.match(/:([a-zA-Z_]+)/g) || []\n for (const match of paramMatches) {\n const paramName = match.slice(1)\n let schema: any = { type: 'string' }\n\n // Try to get type from params validation schema\n if (route.validation?.params) {\n const jsonSchema = toJsonSchema(route.validation.params)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const props = jsonSchema.properties as Record<string, any>\n if (props[paramName]) {\n schema = props[paramName]\n }\n }\n }\n\n op.parameters.push({\n name: paramName,\n in: 'path',\n required: true,\n schema,\n })\n }\n\n // Query parameters\n if (route.validation?.query) {\n const jsonSchema = toJsonSchema(route.validation.query)\n if (jsonSchema?.properties && typeof jsonSchema.properties === 'object') {\n const required = Array.isArray(jsonSchema.required) ? jsonSchema.required : []\n for (const [name, propSchema] of Object.entries(\n jsonSchema.properties as Record<string, any>,\n )) {\n op.parameters.push({\n name,\n in: 'query',\n required: required.includes(name),\n schema: propSchema,\n })\n }\n }\n }\n\n // Remove empty parameters array\n if (op.parameters.length === 0) delete op.parameters\n\n // Request body\n if (route.validation?.body && ['post', 'put', 'patch'].includes(method)) {\n const bodySchema = toJsonSchema(route.validation.body)\n if (bodySchema) {\n const ref = registerSchema(bodySchema, `${route.handlerName}Body`)\n op.requestBody = {\n required: true,\n content: { 'application/json': { schema: ref } },\n }\n }\n }\n\n // File upload detection\n const fileUpload = Reflect.getMetadata(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n if (fileUpload) {\n const properties: any = {}\n if (fileUpload.fieldName) {\n properties[fileUpload.fieldName] = {\n type: 'string',\n format: 'binary',\n }\n }\n op.requestBody = {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: { type: 'object', properties },\n },\n },\n }\n }\n\n // Responses\n if (responses.length > 0) {\n for (const resp of responses) {\n op.responses[String(resp.status)] = {\n description: resp.description || '',\n ...(resp.schema\n ? (() => {\n const converted =\n typeof resp.schema === 'function' || typeof resp.schema === 'object'\n ? toJsonSchema(resp.schema)\n : null\n const finalSchema = converted\n ? registerSchema(converted, `${route.handlerName}Response${resp.status}`)\n : typeof resp.schema === 'object'\n ? resp.schema\n : undefined\n return finalSchema\n ? { content: { 'application/json': { schema: finalSchema } } }\n : {}\n })()\n : {}),\n }\n }\n } else {\n // Auto-generate default responses\n const defaultStatus = method === 'post' ? '201' : method === 'delete' ? '204' : '200'\n op.responses[defaultStatus] = { description: 'Successful operation' }\n\n if (route.validation?.body) {\n op.responses['422'] = { description: 'Validation error' }\n }\n }\n\n // Security\n const authName = methodAuth || classAuth\n if (authName) {\n op.security = [{ [authName]: [] }]\n securitySchemes[authName] = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n\n // Mount\n if (!spec.paths[openApiPath]) spec.paths[openApiPath] = {}\n spec.paths[openApiPath][method] = op\n }\n }\n\n // Finalize\n spec.tags = Array.from(allTags).map((name) => ({ name }))\n spec.components.securitySchemes = securitySchemes\n\n if (options.bearerAuth) {\n if (!securitySchemes.BearerAuth) {\n spec.components.securitySchemes.BearerAuth = {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT',\n }\n }\n spec.security = [{ BearerAuth: [] }]\n }\n\n // Merge collected schemas into components\n spec.components.schemas = componentSchemas\n\n // Clean up empty components\n if (Object.keys(spec.components.schemas).length === 0) delete spec.components.schemas\n if (Object.keys(spec.components.securitySchemes).length === 0)\n delete spec.components.securitySchemes\n if (Object.keys(spec.components).length === 0) delete spec.components\n\n return spec\n}\n","import { Router } from 'express'\nimport type { Express } from 'express'\nimport { Logger, type AppAdapter, type Container } from '@forinda/kickjs-core'\nimport {\n buildOpenAPISpec,\n registerControllerForDocs,\n clearRegisteredRoutes,\n type SwaggerOptions,\n} from './openapi-builder'\nimport { swaggerUIHtml, redocHtml } from './ui'\n\nconst log = Logger.for('SwaggerAdapter')\n\nexport interface SwaggerAdapterOptions extends SwaggerOptions {\n /** Path to serve Swagger UI (default: '/docs') */\n docsPath?: string\n /** Path to serve ReDoc (default: '/redoc') */\n redocPath?: string\n /** Path to serve the raw JSON spec (default: '/openapi.json') */\n specPath?: string\n}\n\n/**\n * Swagger adapter — auto-generates OpenAPI spec from decorators and serves docs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * adapters: [\n * new SwaggerAdapter({\n * info: { title: 'My API', version: '1.0.0' },\n * }),\n * ],\n * })\n * ```\n *\n * Endpoints:\n * GET /docs — Swagger UI\n * GET /redoc — ReDoc\n * GET /openapi.json — Raw OpenAPI 3.0.3 spec\n */\nexport class SwaggerAdapter implements AppAdapter {\n name = 'SwaggerAdapter'\n\n constructor(private options: SwaggerAdapterOptions = {}) {}\n\n /** Collect controller metadata as routes are mounted */\n onRouteMount(controllerClass: any, mountPath: string): void {\n registerControllerForDocs(controllerClass, mountPath)\n }\n\n beforeMount(app: Express, _container: Container): void {\n // Clear previous registrations (supports HMR rebuild)\n clearRegisteredRoutes()\n const docsPath = this.options.docsPath ?? '/docs'\n const redocPath = this.options.redocPath ?? '/redoc'\n const specPath = this.options.specPath ?? '/openapi.json'\n\n // Use a sub-router with relaxed CSP so CDN scripts load\n const docsRouter = Router()\n\n docsRouter.use((_req, res, next) => {\n res.setHeader(\n 'Content-Security-Policy',\n [\n \"default-src 'self'\",\n \"script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.redoc.ly https://cdn.jsdelivr.net\",\n \"style-src 'self' 'unsafe-inline' https://unpkg.com https://fonts.googleapis.com\",\n \"font-src 'self' https://fonts.gstatic.com\",\n \"img-src 'self' data: https://unpkg.com\",\n \"connect-src 'self'\",\n ].join('; '),\n )\n next()\n })\n\n // Spec endpoint (JSON)\n docsRouter.get(specPath, (_req, res) => {\n const spec = buildOpenAPISpec(this.options)\n res.json(spec)\n })\n\n // Swagger UI\n docsRouter.get(docsPath, (_req, res) => {\n res.type('html').send(swaggerUIHtml(specPath, this.options.info?.title))\n })\n\n // ReDoc\n docsRouter.get(redocPath, (_req, res) => {\n res.type('html').send(redocHtml(specPath, this.options.info?.title))\n })\n\n app.use(docsRouter)\n\n log.info(`Swagger UI: ${docsPath}`)\n log.info(`ReDoc: ${redocPath}`)\n log.info(`OpenAPI spec: ${specPath}`)\n }\n}\n\n// Re-export for use by Application when mounting module routes\nexport { registerControllerForDocs, clearRegisteredRoutes }\n","/** Escape a string for safe HTML attribute/content interpolation */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\n/** Generate Swagger UI HTML that loads from CDN */\nexport function swaggerUIHtml(specUrl: string, title = 'API Docs'): string {\n const safeTitle = escapeHtml(title)\n const safeUrl = JSON.stringify(specUrl).replace(/</g, '\\\\u003c')\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\">\n</head>\n<body>\n <div id=\"swagger-ui\"></div>\n <script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n <script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js\"></script>\n <script>\n SwaggerUIBundle({\n url: ${safeUrl},\n dom_id: '#swagger-ui',\n deepLinking: true,\n presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],\n plugins: [SwaggerUIBundle.plugins.DownloadUrl],\n layout: 'StandaloneLayout',\n });\n </script>\n</body>\n</html>`\n}\n\n/** Generate ReDoc HTML that loads from CDN */\nexport function redocHtml(specUrl: string, title = 'API Docs'): string {\n const safeTitle = escapeHtml(title)\n const safeUrl = escapeHtml(specUrl)\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${safeTitle}</title>\n</head>\n<body>\n <redoc spec-url=\"${safeUrl}\"></redoc>\n <script src=\"https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js\"></script>\n</body>\n</html>`\n}\n"],"mappings":"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","names":["zodSchemaParser","name","supports","schema","safeParse","toJSONSchema","toJsonSchema","$schema","_","rest","SWAGGER_KEYS","OPERATION","Symbol","RESPONSES","TAGS","BEARER_AUTH","EXCLUDE","ApiOperation","options","target","propertyKey","Reflect","defineMetadata","SWAGGER_KEYS","OPERATION","ApiResponse","existing","getMetadata","RESPONSES","ApiTags","tags","TAGS","ApiBearerAuth","name","BEARER_AUTH","ApiExclude","EXCLUDE","METADATA","registeredRoutes","registerControllerForDocs","controllerClass","mountPath","push","clearRegisteredRoutes","length","buildOpenAPISpec","options","parser","schemaParser","zodSchemaParser","toJsonSchema","schema","supports","componentSchemas","schemaCounter","registerSchema","jsonSchema","hint","name","title","label","replace","clean","$schema","$ref","spec","openapi","info","version","description","paths","components","schemas","securitySchemes","tags","servers","allTags","Set","Reflect","getMetadata","SWAGGER_KEYS","EXCLUDE","routes","METADATA","ROUTES","classTags","TAGS","classAuth","BEARER_AUTH","controllerPath","CONTROLLER_PATH","route","handlerName","routePath","path","fullPath","openApiPath","method","toLowerCase","operation","OPERATION","responses","RESPONSES","methodTags","methodAuth","forEach","t","add","op","summary","operationId","deprecated","parameters","paramMatches","match","paramName","slice","type","validation","params","properties","props","in","required","query","Array","isArray","propSchema","Object","entries","includes","body","bodySchema","ref","requestBody","content","fileUpload","FILE_UPLOAD","fieldName","format","resp","String","status","converted","finalSchema","undefined","defaultStatus","authName","security","scheme","bearerFormat","from","map","bearerAuth","BearerAuth","keys","Router","Logger","escapeHtml","str","replace","swaggerUIHtml","specUrl","title","safeTitle","safeUrl","JSON","stringify","redocHtml","log","Logger","for","SwaggerAdapter","name","options","onRouteMount","controllerClass","mountPath","registerControllerForDocs","beforeMount","app","_container","clearRegisteredRoutes","docsPath","redocPath","specPath","docsRouter","Router","use","_req","res","next","setHeader","join","get","spec","buildOpenAPISpec","json","type","send","swaggerUIHtml","info","title","redocHtml"]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@forinda/kickjs-swagger",
3
+ "version": "0.3.0",
4
+ "description": "OpenAPI spec generation from decorators, Swagger UI and ReDoc serving for KickJS",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "dependencies": {
18
+ "reflect-metadata": "^0.2.2",
19
+ "@forinda/kickjs-core": "0.3.0"
20
+ },
21
+ "peerDependencies": {
22
+ "express": "^5.1.0",
23
+ "zod": ">=4.0.0"
24
+ },
25
+ "peerDependenciesMeta": {
26
+ "zod": {
27
+ "optional": true
28
+ }
29
+ },
30
+ "devDependencies": {
31
+ "@swc/core": "^1.7.28",
32
+ "@types/express": "^5.0.6",
33
+ "@types/node": "^24.5.2",
34
+ "express": "^5.1.0",
35
+ "tsup": "^8.5.0",
36
+ "typescript": "^5.9.2",
37
+ "vitest": "^3.2.4",
38
+ "zod": "^4.3.6"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "license": "MIT",
44
+ "author": "Felix Orinda",
45
+ "engines": {
46
+ "node": ">=20.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsup --watch",
51
+ "test": "vitest run",
52
+ "test:watch": "vitest",
53
+ "typecheck": "tsc --noEmit",
54
+ "clean": "rm -rf dist .turbo",
55
+ "lint": "tsc --noEmit"
56
+ }
57
+ }