@honestjs/api-docs-plugin 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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/index.d.mts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +314 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Orkhan Karimov <karimovok1@gmail.com> (https://github.com/kerimovok)
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# API Docs Plugin
|
|
2
|
+
|
|
3
|
+
Serves OpenAPI JSON and Swagger UI for your HonestJS application. Always generates the spec from an artifact—pass `{ routes, schemas }` directly or a context key like `'rpc.artifact'` (e.g. from `@honestjs/rpc-plugin`).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @honestjs/api-docs-plugin
|
|
9
|
+
# or
|
|
10
|
+
yarn add @honestjs/api-docs-plugin
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @honestjs/api-docs-plugin
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage with RPC Plugin
|
|
16
|
+
|
|
17
|
+
The RPC plugin writes its artifact to the application context. Pass the context key to read it. Ensure RPC runs before ApiDocs in the plugins array:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { Application } from "honestjs"
|
|
21
|
+
import { RPCPlugin } from "@honestjs/rpc-plugin"
|
|
22
|
+
import { ApiDocsPlugin } from "@honestjs/api-docs-plugin"
|
|
23
|
+
import AppModule from "./app.module"
|
|
24
|
+
|
|
25
|
+
const { hono } = await Application.create(AppModule, {
|
|
26
|
+
plugins: [
|
|
27
|
+
new RPCPlugin(),
|
|
28
|
+
new ApiDocsPlugin({ artifact: "rpc.artifact" }),
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export default hono
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
If RPC uses custom `context.namespace` / `context.keys.artifact`, pass the resulting full key (for example, `custom.artifact`) to `artifact`.
|
|
36
|
+
|
|
37
|
+
By default:
|
|
38
|
+
|
|
39
|
+
- OpenAPI JSON: `/openapi.json`
|
|
40
|
+
- Swagger UI: `/docs`
|
|
41
|
+
|
|
42
|
+
## Manual Artifact
|
|
43
|
+
|
|
44
|
+
Pass the artifact object directly:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { ApiDocsPlugin } from "@honestjs/api-docs-plugin"
|
|
48
|
+
|
|
49
|
+
const artifact = {
|
|
50
|
+
routes: [
|
|
51
|
+
{
|
|
52
|
+
method: "GET",
|
|
53
|
+
handler: "list",
|
|
54
|
+
controller: "UsersController",
|
|
55
|
+
fullPath: "/users",
|
|
56
|
+
parameters: [],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
schemas: [],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
plugins: [new ApiDocsPlugin({ artifact })]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuration Options
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface ApiDocsPluginOptions {
|
|
69
|
+
// Required: artifact - direct object or context key (resolved by plugin via app.getContext().get)
|
|
70
|
+
artifact: OpenApiArtifactInput | string
|
|
71
|
+
|
|
72
|
+
// OpenAPI generation (when converting artifact to spec)
|
|
73
|
+
title?: string
|
|
74
|
+
version?: string
|
|
75
|
+
description?: string
|
|
76
|
+
servers?: readonly { url: string; description?: string }[]
|
|
77
|
+
|
|
78
|
+
// Serving
|
|
79
|
+
openApiRoute?: string // default: '/openapi.json'
|
|
80
|
+
uiRoute?: string // default: '/docs'
|
|
81
|
+
uiTitle?: string // default: 'API Docs'
|
|
82
|
+
reloadOnRequest?: boolean // default: false
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Programmatic API
|
|
87
|
+
|
|
88
|
+
For custom workflows, use the exported utilities:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import {
|
|
92
|
+
fromArtifactSync,
|
|
93
|
+
write,
|
|
94
|
+
type OpenApiArtifactInput,
|
|
95
|
+
type OpenApiDocument,
|
|
96
|
+
} from "@honestjs/api-docs-plugin"
|
|
97
|
+
|
|
98
|
+
const artifact: OpenApiArtifactInput = { routes: [...], schemas: [...] }
|
|
99
|
+
const spec: OpenApiDocument = fromArtifactSync(artifact, {
|
|
100
|
+
title: "My API",
|
|
101
|
+
version: "1.0.0",
|
|
102
|
+
})
|
|
103
|
+
await write(spec, "./generated/openapi.json")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { IPlugin, Application } from 'honestjs';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
4
|
+
export { OpenAPIV3 } from 'openapi-types';
|
|
5
|
+
|
|
6
|
+
interface OpenApiGenerationOptions {
|
|
7
|
+
readonly title?: string;
|
|
8
|
+
readonly version?: string;
|
|
9
|
+
readonly description?: string;
|
|
10
|
+
readonly servers?: readonly {
|
|
11
|
+
url: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
interface OpenApiArtifactInput {
|
|
16
|
+
readonly routes: readonly OpenApiRouteInput[];
|
|
17
|
+
readonly schemas: readonly OpenApiSchemaInput[];
|
|
18
|
+
}
|
|
19
|
+
interface OpenApiRouteInput {
|
|
20
|
+
readonly method: string;
|
|
21
|
+
readonly handler: string;
|
|
22
|
+
readonly controller: string;
|
|
23
|
+
readonly fullPath: string;
|
|
24
|
+
readonly path?: string;
|
|
25
|
+
readonly prefix?: string;
|
|
26
|
+
readonly version?: string;
|
|
27
|
+
readonly route?: string;
|
|
28
|
+
readonly returns?: string;
|
|
29
|
+
readonly parameters?: readonly OpenApiParameterInput[];
|
|
30
|
+
}
|
|
31
|
+
interface OpenApiParameterInput {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly data?: string;
|
|
34
|
+
readonly type: string;
|
|
35
|
+
readonly required?: boolean;
|
|
36
|
+
readonly decoratorType: string;
|
|
37
|
+
}
|
|
38
|
+
interface OpenApiSchemaInput {
|
|
39
|
+
readonly type: string;
|
|
40
|
+
readonly schema: Record<string, any>;
|
|
41
|
+
}
|
|
42
|
+
/** OpenAPI 3.x document type (openapi-types). */
|
|
43
|
+
type OpenApiDocument = OpenAPIV3.Document;
|
|
44
|
+
declare function fromArtifactSync(artifact: OpenApiArtifactInput, options?: OpenApiGenerationOptions): OpenApiDocument;
|
|
45
|
+
declare function fromArtifact(artifact: OpenApiArtifactInput, options?: OpenApiGenerationOptions): Promise<OpenApiDocument>;
|
|
46
|
+
declare function write(openapi: OpenApiDocument, outputPath: string): Promise<string>;
|
|
47
|
+
|
|
48
|
+
type ArtifactInput = OpenApiArtifactInput | string;
|
|
49
|
+
interface ApiDocsPluginOptions extends OpenApiGenerationOptions {
|
|
50
|
+
/** Artifact: direct object `{ routes, schemas }` or context key string (e.g. `'rpc.artifact'`) resolved via app.getContext().get(key). OpenAPI is always generated from the artifact. */
|
|
51
|
+
readonly artifact: ArtifactInput;
|
|
52
|
+
readonly openApiRoute?: string;
|
|
53
|
+
readonly uiRoute?: string;
|
|
54
|
+
readonly uiTitle?: string;
|
|
55
|
+
readonly reloadOnRequest?: boolean;
|
|
56
|
+
}
|
|
57
|
+
declare class ApiDocsPlugin implements IPlugin {
|
|
58
|
+
private readonly artifact;
|
|
59
|
+
private readonly openApiRoute;
|
|
60
|
+
private readonly uiRoute;
|
|
61
|
+
private readonly uiTitle;
|
|
62
|
+
private readonly reloadOnRequest;
|
|
63
|
+
private readonly genOptions;
|
|
64
|
+
private app;
|
|
65
|
+
private cachedSpec;
|
|
66
|
+
constructor(options: ApiDocsPluginOptions);
|
|
67
|
+
afterModulesRegistered: (app: Application, hono: Hono) => Promise<void>;
|
|
68
|
+
private normalizeRoute;
|
|
69
|
+
private resolveSpec;
|
|
70
|
+
private renderSwaggerUiHtml;
|
|
71
|
+
private escapeHtml;
|
|
72
|
+
private escapeJsString;
|
|
73
|
+
private toErrorMessage;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { ApiDocsPlugin, type ApiDocsPluginOptions, type ArtifactInput, type OpenApiArtifactInput, type OpenApiDocument, type OpenApiGenerationOptions, type OpenApiParameterInput, type OpenApiRouteInput, type OpenApiSchemaInput, fromArtifact, fromArtifactSync, write };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { IPlugin, Application } from 'honestjs';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
4
|
+
export { OpenAPIV3 } from 'openapi-types';
|
|
5
|
+
|
|
6
|
+
interface OpenApiGenerationOptions {
|
|
7
|
+
readonly title?: string;
|
|
8
|
+
readonly version?: string;
|
|
9
|
+
readonly description?: string;
|
|
10
|
+
readonly servers?: readonly {
|
|
11
|
+
url: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
interface OpenApiArtifactInput {
|
|
16
|
+
readonly routes: readonly OpenApiRouteInput[];
|
|
17
|
+
readonly schemas: readonly OpenApiSchemaInput[];
|
|
18
|
+
}
|
|
19
|
+
interface OpenApiRouteInput {
|
|
20
|
+
readonly method: string;
|
|
21
|
+
readonly handler: string;
|
|
22
|
+
readonly controller: string;
|
|
23
|
+
readonly fullPath: string;
|
|
24
|
+
readonly path?: string;
|
|
25
|
+
readonly prefix?: string;
|
|
26
|
+
readonly version?: string;
|
|
27
|
+
readonly route?: string;
|
|
28
|
+
readonly returns?: string;
|
|
29
|
+
readonly parameters?: readonly OpenApiParameterInput[];
|
|
30
|
+
}
|
|
31
|
+
interface OpenApiParameterInput {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly data?: string;
|
|
34
|
+
readonly type: string;
|
|
35
|
+
readonly required?: boolean;
|
|
36
|
+
readonly decoratorType: string;
|
|
37
|
+
}
|
|
38
|
+
interface OpenApiSchemaInput {
|
|
39
|
+
readonly type: string;
|
|
40
|
+
readonly schema: Record<string, any>;
|
|
41
|
+
}
|
|
42
|
+
/** OpenAPI 3.x document type (openapi-types). */
|
|
43
|
+
type OpenApiDocument = OpenAPIV3.Document;
|
|
44
|
+
declare function fromArtifactSync(artifact: OpenApiArtifactInput, options?: OpenApiGenerationOptions): OpenApiDocument;
|
|
45
|
+
declare function fromArtifact(artifact: OpenApiArtifactInput, options?: OpenApiGenerationOptions): Promise<OpenApiDocument>;
|
|
46
|
+
declare function write(openapi: OpenApiDocument, outputPath: string): Promise<string>;
|
|
47
|
+
|
|
48
|
+
type ArtifactInput = OpenApiArtifactInput | string;
|
|
49
|
+
interface ApiDocsPluginOptions extends OpenApiGenerationOptions {
|
|
50
|
+
/** Artifact: direct object `{ routes, schemas }` or context key string (e.g. `'rpc.artifact'`) resolved via app.getContext().get(key). OpenAPI is always generated from the artifact. */
|
|
51
|
+
readonly artifact: ArtifactInput;
|
|
52
|
+
readonly openApiRoute?: string;
|
|
53
|
+
readonly uiRoute?: string;
|
|
54
|
+
readonly uiTitle?: string;
|
|
55
|
+
readonly reloadOnRequest?: boolean;
|
|
56
|
+
}
|
|
57
|
+
declare class ApiDocsPlugin implements IPlugin {
|
|
58
|
+
private readonly artifact;
|
|
59
|
+
private readonly openApiRoute;
|
|
60
|
+
private readonly uiRoute;
|
|
61
|
+
private readonly uiTitle;
|
|
62
|
+
private readonly reloadOnRequest;
|
|
63
|
+
private readonly genOptions;
|
|
64
|
+
private app;
|
|
65
|
+
private cachedSpec;
|
|
66
|
+
constructor(options: ApiDocsPluginOptions);
|
|
67
|
+
afterModulesRegistered: (app: Application, hono: Hono) => Promise<void>;
|
|
68
|
+
private normalizeRoute;
|
|
69
|
+
private resolveSpec;
|
|
70
|
+
private renderSwaggerUiHtml;
|
|
71
|
+
private escapeHtml;
|
|
72
|
+
private escapeJsString;
|
|
73
|
+
private toErrorMessage;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { ApiDocsPlugin, type ApiDocsPluginOptions, type ArtifactInput, type OpenApiArtifactInput, type OpenApiDocument, type OpenApiGenerationOptions, type OpenApiParameterInput, type OpenApiRouteInput, type OpenApiSchemaInput, fromArtifact, fromArtifactSync, write };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ApiDocsPlugin: () => ApiDocsPlugin,
|
|
34
|
+
fromArtifact: () => fromArtifact,
|
|
35
|
+
fromArtifactSync: () => fromArtifactSync,
|
|
36
|
+
write: () => write
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/openapi.generator.ts
|
|
41
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
42
|
+
var import_path = __toESM(require("path"));
|
|
43
|
+
function resolveOptions(options = {}) {
|
|
44
|
+
return {
|
|
45
|
+
title: options.title ?? "API",
|
|
46
|
+
version: options.version ?? "1.0.0",
|
|
47
|
+
description: options.description ?? "",
|
|
48
|
+
servers: options.servers ?? []
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function fromArtifactSync(artifact, options = {}) {
|
|
52
|
+
const resolved = resolveOptions(options);
|
|
53
|
+
const schemaMap = buildSchemaMap(artifact.schemas);
|
|
54
|
+
const spec = {
|
|
55
|
+
openapi: "3.0.3",
|
|
56
|
+
info: {
|
|
57
|
+
title: resolved.title,
|
|
58
|
+
version: resolved.version,
|
|
59
|
+
description: resolved.description
|
|
60
|
+
},
|
|
61
|
+
paths: {},
|
|
62
|
+
components: {
|
|
63
|
+
schemas: schemaMap
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
if (resolved.servers.length > 0) {
|
|
67
|
+
spec.servers = resolved.servers.map((server) => ({ ...server }));
|
|
68
|
+
}
|
|
69
|
+
for (const route of artifact.routes) {
|
|
70
|
+
const routePath = route.fullPath || buildFallbackPath(route);
|
|
71
|
+
const openApiPath = toOpenApiPath(routePath);
|
|
72
|
+
const method = route.method.toLowerCase();
|
|
73
|
+
if (!spec.paths[openApiPath]) {
|
|
74
|
+
spec.paths[openApiPath] = {};
|
|
75
|
+
}
|
|
76
|
+
;
|
|
77
|
+
spec.paths[openApiPath][method] = buildOperation(
|
|
78
|
+
route,
|
|
79
|
+
schemaMap
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return spec;
|
|
83
|
+
}
|
|
84
|
+
async function fromArtifact(artifact, options = {}) {
|
|
85
|
+
return fromArtifactSync(artifact, options);
|
|
86
|
+
}
|
|
87
|
+
async function write(openapi, outputPath) {
|
|
88
|
+
const absolute = import_path.default.isAbsolute(outputPath) ? outputPath : import_path.default.resolve(process.cwd(), outputPath);
|
|
89
|
+
await import_promises.default.mkdir(import_path.default.dirname(absolute), { recursive: true });
|
|
90
|
+
await import_promises.default.writeFile(absolute, JSON.stringify(openapi, null, 2), "utf-8");
|
|
91
|
+
return absolute;
|
|
92
|
+
}
|
|
93
|
+
function buildOperation(route, schemaMap) {
|
|
94
|
+
const controllerName = route.controller.replace(/Controller$/, "");
|
|
95
|
+
const parameters = route.parameters ?? [];
|
|
96
|
+
const operation = {
|
|
97
|
+
operationId: route.handler,
|
|
98
|
+
tags: [controllerName],
|
|
99
|
+
responses: buildResponses(route.returns, schemaMap)
|
|
100
|
+
};
|
|
101
|
+
const openApiParameters = buildParameters(parameters);
|
|
102
|
+
if (openApiParameters.length > 0) {
|
|
103
|
+
operation.parameters = openApiParameters;
|
|
104
|
+
}
|
|
105
|
+
const requestBody = buildRequestBody(parameters, schemaMap);
|
|
106
|
+
if (requestBody) {
|
|
107
|
+
operation.requestBody = requestBody;
|
|
108
|
+
}
|
|
109
|
+
return operation;
|
|
110
|
+
}
|
|
111
|
+
function buildParameters(parameters) {
|
|
112
|
+
const result = [];
|
|
113
|
+
for (const param of parameters) {
|
|
114
|
+
if (param.decoratorType === "param") {
|
|
115
|
+
result.push({
|
|
116
|
+
name: param.data ?? param.name,
|
|
117
|
+
in: "path",
|
|
118
|
+
required: true,
|
|
119
|
+
schema: tsTypeToJsonSchema(param.type)
|
|
120
|
+
});
|
|
121
|
+
} else if (param.decoratorType === "query") {
|
|
122
|
+
result.push({
|
|
123
|
+
name: param.data ?? param.name,
|
|
124
|
+
in: "query",
|
|
125
|
+
required: param.required === true,
|
|
126
|
+
schema: tsTypeToJsonSchema(param.type)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
function buildRequestBody(parameters, schemaMap) {
|
|
133
|
+
const bodyParam = parameters.find((param) => param.decoratorType === "body");
|
|
134
|
+
if (!bodyParam) return null;
|
|
135
|
+
const typeName = extractBaseTypeName(bodyParam.type);
|
|
136
|
+
const schema = typeName && schemaMap[typeName] ? { $ref: `#/components/schemas/${typeName}` } : { type: "object" };
|
|
137
|
+
return {
|
|
138
|
+
required: true,
|
|
139
|
+
content: {
|
|
140
|
+
"application/json": { schema }
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function buildResponses(returns, schemaMap) {
|
|
145
|
+
const responseSchema = resolveResponseSchema(returns, schemaMap);
|
|
146
|
+
if (!responseSchema) {
|
|
147
|
+
return { "200": { description: "Successful response" } };
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
"200": {
|
|
151
|
+
description: "Successful response",
|
|
152
|
+
content: {
|
|
153
|
+
"application/json": { schema: responseSchema }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function resolveResponseSchema(returns, schemaMap) {
|
|
159
|
+
if (!returns) return null;
|
|
160
|
+
let innerType = returns;
|
|
161
|
+
const promiseMatch = returns.match(/^Promise<(.+)>$/);
|
|
162
|
+
if (promiseMatch) {
|
|
163
|
+
innerType = promiseMatch[1];
|
|
164
|
+
}
|
|
165
|
+
const isArray = innerType.endsWith("[]");
|
|
166
|
+
const baseType = isArray ? innerType.slice(0, -2) : innerType;
|
|
167
|
+
if (["string", "number", "boolean"].includes(baseType)) {
|
|
168
|
+
const primitiveSchema = tsTypeToJsonSchema(baseType);
|
|
169
|
+
return isArray ? { type: "array", items: primitiveSchema } : primitiveSchema;
|
|
170
|
+
}
|
|
171
|
+
if (["void", "any", "unknown"].includes(baseType)) return null;
|
|
172
|
+
if (schemaMap[baseType]) {
|
|
173
|
+
const ref = { $ref: `#/components/schemas/${baseType}` };
|
|
174
|
+
return isArray ? { type: "array", items: ref } : ref;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function buildSchemaMap(schemas) {
|
|
179
|
+
const result = {};
|
|
180
|
+
for (const schemaInfo of schemas) {
|
|
181
|
+
const definition = schemaInfo.schema?.definitions?.[schemaInfo.type];
|
|
182
|
+
if (definition) {
|
|
183
|
+
result[schemaInfo.type] = definition;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
function tsTypeToJsonSchema(tsType) {
|
|
189
|
+
switch (tsType) {
|
|
190
|
+
case "number":
|
|
191
|
+
return { type: "number" };
|
|
192
|
+
case "boolean":
|
|
193
|
+
return { type: "boolean" };
|
|
194
|
+
case "string":
|
|
195
|
+
default:
|
|
196
|
+
return { type: "string" };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function extractBaseTypeName(tsType) {
|
|
200
|
+
if (!tsType) return null;
|
|
201
|
+
let type = tsType;
|
|
202
|
+
const promiseMatch = type.match(/^Promise<(.+)>$/);
|
|
203
|
+
if (promiseMatch) type = promiseMatch[1];
|
|
204
|
+
type = type.replace(/\[\]$/, "");
|
|
205
|
+
const genericMatch = type.match(/^\w+<(\w+)>$/);
|
|
206
|
+
if (genericMatch) type = genericMatch[1];
|
|
207
|
+
if (["string", "number", "boolean", "any", "void", "unknown", "object"].includes(type)) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return type;
|
|
211
|
+
}
|
|
212
|
+
function toOpenApiPath(expressPath) {
|
|
213
|
+
return expressPath.replace(/:(\w+)/g, "{$1}");
|
|
214
|
+
}
|
|
215
|
+
function buildFallbackPath(route) {
|
|
216
|
+
const parts = [route.prefix, route.version, route.route, route.path].filter((part) => part !== void 0 && part !== null && part !== "").map((part) => String(part).replace(/^\/+|\/+$/g, "")).filter((part) => part.length > 0);
|
|
217
|
+
const joined = parts.join("/");
|
|
218
|
+
return `/${joined}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/api-docs.plugin.ts
|
|
222
|
+
var DEFAULT_OPENAPI_ROUTE = "/openapi.json";
|
|
223
|
+
var DEFAULT_UI_ROUTE = "/docs";
|
|
224
|
+
var DEFAULT_UI_TITLE = "API Docs";
|
|
225
|
+
function isContextKey(artifact) {
|
|
226
|
+
return typeof artifact === "string";
|
|
227
|
+
}
|
|
228
|
+
function isArtifact(value) {
|
|
229
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
230
|
+
const obj = value;
|
|
231
|
+
return Array.isArray(obj.routes) && Array.isArray(obj.schemas);
|
|
232
|
+
}
|
|
233
|
+
var ApiDocsPlugin = class {
|
|
234
|
+
artifact;
|
|
235
|
+
openApiRoute;
|
|
236
|
+
uiRoute;
|
|
237
|
+
uiTitle;
|
|
238
|
+
reloadOnRequest;
|
|
239
|
+
genOptions;
|
|
240
|
+
app = null;
|
|
241
|
+
cachedSpec = null;
|
|
242
|
+
constructor(options) {
|
|
243
|
+
this.artifact = options.artifact;
|
|
244
|
+
this.openApiRoute = this.normalizeRoute(options.openApiRoute ?? DEFAULT_OPENAPI_ROUTE);
|
|
245
|
+
this.uiRoute = this.normalizeRoute(options.uiRoute ?? DEFAULT_UI_ROUTE);
|
|
246
|
+
this.uiTitle = options.uiTitle ?? DEFAULT_UI_TITLE;
|
|
247
|
+
this.reloadOnRequest = options.reloadOnRequest ?? false;
|
|
248
|
+
this.genOptions = {
|
|
249
|
+
title: options.title,
|
|
250
|
+
version: options.version,
|
|
251
|
+
description: options.description,
|
|
252
|
+
servers: options.servers
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
afterModulesRegistered = async (app, hono) => {
|
|
256
|
+
this.app = app;
|
|
257
|
+
hono.get(this.openApiRoute, async (c) => {
|
|
258
|
+
try {
|
|
259
|
+
const spec = await this.resolveSpec();
|
|
260
|
+
return c.json(spec);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return c.json(
|
|
263
|
+
{
|
|
264
|
+
error: "Failed to load OpenAPI spec",
|
|
265
|
+
message: this.toErrorMessage(error)
|
|
266
|
+
},
|
|
267
|
+
500
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
hono.get(this.uiRoute, (c) => {
|
|
272
|
+
return c.html(this.renderSwaggerUiHtml());
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
normalizeRoute(input) {
|
|
276
|
+
const trimmed = input.trim();
|
|
277
|
+
if (!trimmed) return "/";
|
|
278
|
+
let normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
279
|
+
if (normalized.length > 1) {
|
|
280
|
+
normalized = normalized.replace(/\/+$/g, "");
|
|
281
|
+
}
|
|
282
|
+
return normalized || "/";
|
|
283
|
+
}
|
|
284
|
+
async resolveSpec() {
|
|
285
|
+
if (!this.reloadOnRequest && this.cachedSpec) {
|
|
286
|
+
return this.cachedSpec;
|
|
287
|
+
}
|
|
288
|
+
let artifact;
|
|
289
|
+
if (isContextKey(this.artifact)) {
|
|
290
|
+
if (!this.app) {
|
|
291
|
+
throw new Error("ApiDocsPlugin: app not available when resolving artifact from context");
|
|
292
|
+
}
|
|
293
|
+
const value = this.app.getContext().get(this.artifact);
|
|
294
|
+
if (value === void 0) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`ApiDocsPlugin: no artifact at context key '${this.artifact}'. Ensure RPC plugin (or another producer) runs before ApiDocs and writes to this key.`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (!isArtifact(value)) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`ApiDocsPlugin: value at '${this.artifact}' is not a valid artifact (expected object with routes and schemas)`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
artifact = value;
|
|
305
|
+
} else {
|
|
306
|
+
artifact = this.artifact;
|
|
307
|
+
}
|
|
308
|
+
const spec = fromArtifactSync(artifact, this.genOptions);
|
|
309
|
+
if (!this.reloadOnRequest) this.cachedSpec = spec;
|
|
310
|
+
return spec;
|
|
311
|
+
}
|
|
312
|
+
renderSwaggerUiHtml() {
|
|
313
|
+
const title = this.escapeHtml(this.uiTitle);
|
|
314
|
+
const openApiRoute = this.escapeJsString(this.openApiRoute);
|
|
315
|
+
return `<!doctype html>
|
|
316
|
+
<html lang="en">
|
|
317
|
+
<head>
|
|
318
|
+
<meta charset="utf-8" />
|
|
319
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
320
|
+
<title>${title}</title>
|
|
321
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
|
|
322
|
+
</head>
|
|
323
|
+
<body>
|
|
324
|
+
<div id="swagger-ui"></div>
|
|
325
|
+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
326
|
+
<script>
|
|
327
|
+
window.onload = function () {
|
|
328
|
+
window.ui = SwaggerUIBundle({
|
|
329
|
+
url: '${openApiRoute}',
|
|
330
|
+
dom_id: '#swagger-ui'
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
</script>
|
|
334
|
+
</body>
|
|
335
|
+
</html>`;
|
|
336
|
+
}
|
|
337
|
+
escapeHtml(value) {
|
|
338
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
339
|
+
}
|
|
340
|
+
escapeJsString(value) {
|
|
341
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
342
|
+
}
|
|
343
|
+
toErrorMessage(error) {
|
|
344
|
+
return error instanceof Error ? error.message : String(error);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
348
|
+
0 && (module.exports = {
|
|
349
|
+
ApiDocsPlugin,
|
|
350
|
+
fromArtifact,
|
|
351
|
+
fromArtifactSync,
|
|
352
|
+
write
|
|
353
|
+
});
|
|
354
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/openapi.generator.ts","../src/api-docs.plugin.ts"],"sourcesContent":["export { ApiDocsPlugin } from './api-docs.plugin'\nexport type { ApiDocsPluginOptions, ArtifactInput } from './api-docs.plugin'\nexport { fromArtifact, fromArtifactSync, write } from './openapi.generator'\nexport type {\n\tOpenApiArtifactInput,\n\tOpenApiDocument,\n\tOpenApiGenerationOptions,\n\tOpenApiParameterInput,\n\tOpenApiRouteInput,\n\tOpenApiSchemaInput\n} from './openapi.generator'\nexport type { OpenAPIV3 } from 'openapi-types'\n","import fs from 'fs/promises'\nimport path from 'path'\nimport type { OpenAPIV3 } from 'openapi-types'\n\nexport interface OpenApiGenerationOptions {\n\treadonly title?: string\n\treadonly version?: string\n\treadonly description?: string\n\treadonly servers?: readonly { url: string; description?: string }[]\n}\n\nexport interface OpenApiArtifactInput {\n\treadonly routes: readonly OpenApiRouteInput[]\n\treadonly schemas: readonly OpenApiSchemaInput[]\n}\n\nexport interface OpenApiRouteInput {\n\treadonly method: string\n\treadonly handler: string\n\treadonly controller: string\n\treadonly fullPath: string\n\treadonly path?: string\n\treadonly prefix?: string\n\treadonly version?: string\n\treadonly route?: string\n\treadonly returns?: string\n\treadonly parameters?: readonly OpenApiParameterInput[]\n}\n\nexport interface OpenApiParameterInput {\n\treadonly name: string\n\treadonly data?: string\n\treadonly type: string\n\treadonly required?: boolean\n\treadonly decoratorType: string\n}\n\nexport interface OpenApiSchemaInput {\n\treadonly type: string\n\treadonly schema: Record<string, any>\n}\n\n/** OpenAPI 3.x document type (openapi-types). */\nexport type OpenApiDocument = OpenAPIV3.Document\n\ninterface ResolvedOpenApiOptions {\n\treadonly title: string\n\treadonly version: string\n\treadonly description: string\n\treadonly servers: readonly { url: string; description?: string }[]\n}\n\nfunction resolveOptions(options: OpenApiGenerationOptions = {}): ResolvedOpenApiOptions {\n\treturn {\n\t\ttitle: options.title ?? 'API',\n\t\tversion: options.version ?? '1.0.0',\n\t\tdescription: options.description ?? '',\n\t\tservers: options.servers ?? []\n\t}\n}\n\nexport function fromArtifactSync(\n\tartifact: OpenApiArtifactInput,\n\toptions: OpenApiGenerationOptions = {}\n): OpenApiDocument {\n\tconst resolved = resolveOptions(options)\n\tconst schemaMap = buildSchemaMap(artifact.schemas)\n\tconst spec: OpenAPIV3.Document = {\n\t\topenapi: '3.0.3',\n\t\tinfo: {\n\t\t\ttitle: resolved.title,\n\t\t\tversion: resolved.version,\n\t\t\tdescription: resolved.description\n\t\t},\n\t\tpaths: {},\n\t\tcomponents: {\n\t\t\tschemas: schemaMap as OpenAPIV3.ComponentsObject['schemas']\n\t\t}\n\t}\n\n\tif (resolved.servers.length > 0) {\n\t\tspec.servers = resolved.servers.map((server) => ({ ...server }))\n\t}\n\n\tfor (const route of artifact.routes) {\n\t\tconst routePath = route.fullPath || buildFallbackPath(route)\n\t\tconst openApiPath = toOpenApiPath(routePath)\n\t\tconst method = route.method.toLowerCase() as keyof OpenAPIV3.PathItemObject\n\t\tif (!spec.paths[openApiPath]) {\n\t\t\tspec.paths[openApiPath] = {}\n\t\t}\n\t\t;(spec.paths[openApiPath] as Record<string, OpenAPIV3.OperationObject>)[method] = buildOperation(\n\t\t\troute,\n\t\t\tschemaMap\n\t\t)\n\t}\n\n\treturn spec\n}\n\nexport async function fromArtifact(\n\tartifact: OpenApiArtifactInput,\n\toptions: OpenApiGenerationOptions = {}\n): Promise<OpenApiDocument> {\n\treturn fromArtifactSync(artifact, options)\n}\n\nexport async function write(openapi: OpenApiDocument, outputPath: string): Promise<string> {\n\tconst absolute = path.isAbsolute(outputPath) ? outputPath : path.resolve(process.cwd(), outputPath)\n\tawait fs.mkdir(path.dirname(absolute), { recursive: true })\n\tawait fs.writeFile(absolute, JSON.stringify(openapi, null, 2), 'utf-8')\n\treturn absolute\n}\n\nfunction buildOperation(\n\troute: OpenApiRouteInput,\n\tschemaMap: Record<string, Record<string, any>>\n): OpenAPIV3.OperationObject {\n\tconst controllerName = route.controller.replace(/Controller$/, '')\n\tconst parameters = route.parameters ?? []\n\tconst operation: OpenAPIV3.OperationObject = {\n\t\toperationId: route.handler,\n\t\ttags: [controllerName],\n\t\tresponses: buildResponses(route.returns, schemaMap)\n\t}\n\n\tconst openApiParameters = buildParameters(parameters)\n\tif (openApiParameters.length > 0) {\n\t\toperation.parameters = openApiParameters\n\t}\n\n\tconst requestBody = buildRequestBody(parameters, schemaMap)\n\tif (requestBody) {\n\t\toperation.requestBody = requestBody\n\t}\n\n\treturn operation\n}\n\nfunction buildParameters(parameters: readonly OpenApiParameterInput[]): OpenAPIV3.ParameterObject[] {\n\tconst result: OpenAPIV3.ParameterObject[] = []\n\n\tfor (const param of parameters) {\n\t\tif (param.decoratorType === 'param') {\n\t\t\tresult.push({\n\t\t\t\tname: param.data ?? param.name,\n\t\t\t\tin: 'path',\n\t\t\t\trequired: true,\n\t\t\t\tschema: tsTypeToJsonSchema(param.type)\n\t\t\t})\n\t\t} else if (param.decoratorType === 'query') {\n\t\t\tresult.push({\n\t\t\t\tname: param.data ?? param.name,\n\t\t\t\tin: 'query',\n\t\t\t\trequired: param.required === true,\n\t\t\t\tschema: tsTypeToJsonSchema(param.type)\n\t\t\t})\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunction buildRequestBody(\n\tparameters: readonly OpenApiParameterInput[],\n\tschemaMap: Record<string, Record<string, any>>\n): OpenAPIV3.RequestBodyObject | null {\n\tconst bodyParam = parameters.find((param) => param.decoratorType === 'body')\n\tif (!bodyParam) return null\n\n\tconst typeName = extractBaseTypeName(bodyParam.type)\n\tconst schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject =\n\t\ttypeName && schemaMap[typeName]\n\t\t\t? { $ref: `#/components/schemas/${typeName}` }\n\t\t\t: { type: 'object' as const }\n\n\treturn {\n\t\trequired: true,\n\t\tcontent: {\n\t\t\t'application/json': { schema }\n\t\t}\n\t}\n}\n\nfunction buildResponses(\n\treturns: string | undefined,\n\tschemaMap: Record<string, Record<string, any>>\n): OpenAPIV3.ResponsesObject {\n\tconst responseSchema = resolveResponseSchema(returns, schemaMap)\n\n\tif (!responseSchema) {\n\t\treturn { '200': { description: 'Successful response' } }\n\t}\n\n\treturn {\n\t\t'200': {\n\t\t\tdescription: 'Successful response',\n\t\t\tcontent: {\n\t\t\t\t'application/json': { schema: responseSchema }\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction resolveResponseSchema(\n\treturns: string | undefined,\n\tschemaMap: Record<string, Record<string, any>>\n): Record<string, any> | null {\n\tif (!returns) return null\n\n\tlet innerType = returns\n\tconst promiseMatch = returns.match(/^Promise<(.+)>$/)\n\tif (promiseMatch) {\n\t\tinnerType = promiseMatch[1]\n\t}\n\n\tconst isArray = innerType.endsWith('[]')\n\tconst baseType = isArray ? innerType.slice(0, -2) : innerType\n\tif (['string', 'number', 'boolean'].includes(baseType)) {\n\t\tconst primitiveSchema = tsTypeToJsonSchema(baseType)\n\t\treturn isArray ? { type: 'array', items: primitiveSchema } : primitiveSchema\n\t}\n\tif (['void', 'any', 'unknown'].includes(baseType)) return null\n\tif (schemaMap[baseType]) {\n\t\tconst ref = { $ref: `#/components/schemas/${baseType}` }\n\t\treturn isArray ? { type: 'array', items: ref } : ref\n\t}\n\treturn null\n}\n\nfunction buildSchemaMap(schemas: readonly OpenApiSchemaInput[]): Record<string, Record<string, any>> {\n\tconst result: Record<string, Record<string, any>> = {}\n\tfor (const schemaInfo of schemas) {\n\t\tconst definition = schemaInfo.schema?.definitions?.[schemaInfo.type]\n\t\tif (definition) {\n\t\t\tresult[schemaInfo.type] = definition\n\t\t}\n\t}\n\treturn result\n}\n\nfunction tsTypeToJsonSchema(tsType: string): Record<string, unknown> {\n\tswitch (tsType) {\n\t\tcase 'number':\n\t\t\treturn { type: 'number' as const }\n\t\tcase 'boolean':\n\t\t\treturn { type: 'boolean' as const }\n\t\tcase 'string':\n\t\tdefault:\n\t\t\treturn { type: 'string' as const }\n\t}\n}\n\nfunction extractBaseTypeName(tsType: string): string | null {\n\tif (!tsType) return null\n\n\tlet type = tsType\n\tconst promiseMatch = type.match(/^Promise<(.+)>$/)\n\tif (promiseMatch) type = promiseMatch[1]\n\ttype = type.replace(/\\[\\]$/, '')\n\tconst genericMatch = type.match(/^\\w+<(\\w+)>$/)\n\tif (genericMatch) type = genericMatch[1]\n\tif (['string', 'number', 'boolean', 'any', 'void', 'unknown', 'object'].includes(type)) {\n\t\treturn null\n\t}\n\treturn type\n}\n\nfunction toOpenApiPath(expressPath: string): string {\n\treturn expressPath.replace(/:(\\w+)/g, '{$1}')\n}\n\nfunction buildFallbackPath(route: OpenApiRouteInput): string {\n\tconst parts = [route.prefix, route.version, route.route, route.path]\n\t\t.filter((part) => part !== undefined && part !== null && part !== '')\n\t\t.map((part) => String(part).replace(/^\\/+|\\/+$/g, ''))\n\t\t.filter((part) => part.length > 0)\n\tconst joined = parts.join('/')\n\treturn `/${joined}`\n}\n","import type { Application, IPlugin } from 'honestjs'\nimport type { Hono } from 'hono'\n\nimport { fromArtifactSync } from './openapi.generator'\nimport type { OpenApiArtifactInput, OpenApiGenerationOptions } from './openapi.generator'\n\nconst DEFAULT_OPENAPI_ROUTE = '/openapi.json'\nconst DEFAULT_UI_ROUTE = '/docs'\nconst DEFAULT_UI_TITLE = 'API Docs'\n\nexport type ArtifactInput = OpenApiArtifactInput | string\n\nfunction isContextKey(artifact: ArtifactInput): artifact is string {\n\treturn typeof artifact === 'string'\n}\n\nfunction isArtifact(value: unknown): value is OpenApiArtifactInput {\n\tif (!value || typeof value !== 'object' || Array.isArray(value)) return false\n\tconst obj = value as Record<string, unknown>\n\treturn Array.isArray(obj.routes) && Array.isArray(obj.schemas)\n}\n\nexport interface ApiDocsPluginOptions extends OpenApiGenerationOptions {\n\t/** Artifact: direct object `{ routes, schemas }` or context key string (e.g. `'rpc.artifact'`) resolved via app.getContext().get(key). OpenAPI is always generated from the artifact. */\n\treadonly artifact: ArtifactInput\n\treadonly openApiRoute?: string\n\treadonly uiRoute?: string\n\treadonly uiTitle?: string\n\treadonly reloadOnRequest?: boolean\n}\n\nexport class ApiDocsPlugin implements IPlugin {\n\tprivate readonly artifact: ArtifactInput\n\tprivate readonly openApiRoute: string\n\tprivate readonly uiRoute: string\n\tprivate readonly uiTitle: string\n\tprivate readonly reloadOnRequest: boolean\n\tprivate readonly genOptions: OpenApiGenerationOptions\n\n\tprivate app: Application | null = null\n\tprivate cachedSpec: Record<string, unknown> | null = null\n\n\tconstructor(options: ApiDocsPluginOptions) {\n\t\tthis.artifact = options.artifact\n\t\tthis.openApiRoute = this.normalizeRoute(options.openApiRoute ?? DEFAULT_OPENAPI_ROUTE)\n\t\tthis.uiRoute = this.normalizeRoute(options.uiRoute ?? DEFAULT_UI_ROUTE)\n\t\tthis.uiTitle = options.uiTitle ?? DEFAULT_UI_TITLE\n\t\tthis.reloadOnRequest = options.reloadOnRequest ?? false\n\t\tthis.genOptions = {\n\t\t\ttitle: options.title,\n\t\t\tversion: options.version,\n\t\t\tdescription: options.description,\n\t\t\tservers: options.servers\n\t\t}\n\t}\n\n\tafterModulesRegistered = async (app: Application, hono: Hono): Promise<void> => {\n\t\tthis.app = app\n\n\t\thono.get(this.openApiRoute, async (c) => {\n\t\t\ttry {\n\t\t\t\tconst spec = await this.resolveSpec()\n\t\t\t\treturn c.json(spec)\n\t\t\t} catch (error) {\n\t\t\t\treturn c.json(\n\t\t\t\t\t{\n\t\t\t\t\t\terror: 'Failed to load OpenAPI spec',\n\t\t\t\t\t\tmessage: this.toErrorMessage(error)\n\t\t\t\t\t},\n\t\t\t\t\t500\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\n\t\thono.get(this.uiRoute, (c) => {\n\t\t\treturn c.html(this.renderSwaggerUiHtml())\n\t\t})\n\t}\n\n\tprivate normalizeRoute(input: string): string {\n\t\tconst trimmed = input.trim()\n\t\tif (!trimmed) return '/'\n\t\tlet normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`\n\t\tif (normalized.length > 1) {\n\t\t\tnormalized = normalized.replace(/\\/+$/g, '')\n\t\t}\n\t\treturn normalized || '/'\n\t}\n\n\tprivate async resolveSpec(): Promise<Record<string, unknown>> {\n\t\tif (!this.reloadOnRequest && this.cachedSpec) {\n\t\t\treturn this.cachedSpec\n\t\t}\n\n\t\tlet artifact: OpenApiArtifactInput\n\n\t\tif (isContextKey(this.artifact)) {\n\t\t\tif (!this.app) {\n\t\t\t\tthrow new Error('ApiDocsPlugin: app not available when resolving artifact from context')\n\t\t\t}\n\t\t\tconst value = this.app.getContext().get<unknown>(this.artifact)\n\t\t\tif (value === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`ApiDocsPlugin: no artifact at context key '${this.artifact}'. Ensure RPC plugin (or another producer) runs before ApiDocs and writes to this key.`\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (!isArtifact(value)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`ApiDocsPlugin: value at '${this.artifact}' is not a valid artifact (expected object with routes and schemas)`\n\t\t\t\t)\n\t\t\t}\n\t\t\tartifact = value\n\t\t} else {\n\t\t\tartifact = this.artifact\n\t\t}\n\n\t\tconst spec = fromArtifactSync(artifact, this.genOptions) as unknown as Record<string, unknown>\n\t\tif (!this.reloadOnRequest) this.cachedSpec = spec\n\t\treturn spec\n\t}\n\n\tprivate renderSwaggerUiHtml(): string {\n\t\tconst title = this.escapeHtml(this.uiTitle)\n\t\tconst openApiRoute = this.escapeJsString(this.openApiRoute)\n\n\t\treturn `<!doctype html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\" />\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t<title>${title}</title>\n\t<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\" />\n</head>\n<body>\n\t<div id=\"swagger-ui\"></div>\n\t<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n\t<script>\n\t\twindow.onload = function () {\n\t\t\twindow.ui = SwaggerUIBundle({\n\t\t\t\turl: '${openApiRoute}',\n\t\t\t\tdom_id: '#swagger-ui'\n\t\t\t});\n\t\t};\n\t</script>\n</body>\n</html>`\n\t}\n\n\tprivate escapeHtml(value: string): string {\n\t\treturn value\n\t\t\t.replace(/&/g, '&')\n\t\t\t.replace(/</g, '<')\n\t\t\t.replace(/>/g, '>')\n\t\t\t.replace(/\"/g, '"')\n\t\t\t.replace(/'/g, ''')\n\t}\n\n\tprivate escapeJsString(value: string): string {\n\t\treturn value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\")\n\t}\n\n\tprivate toErrorMessage(error: unknown): string {\n\t\treturn error instanceof Error ? error.message : String(error)\n\t}\n}\n"],"mappings":"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","names":["path","fs"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// src/openapi.generator.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function resolveOptions(options = {}) {
|
|
5
|
+
return {
|
|
6
|
+
title: options.title ?? "API",
|
|
7
|
+
version: options.version ?? "1.0.0",
|
|
8
|
+
description: options.description ?? "",
|
|
9
|
+
servers: options.servers ?? []
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function fromArtifactSync(artifact, options = {}) {
|
|
13
|
+
const resolved = resolveOptions(options);
|
|
14
|
+
const schemaMap = buildSchemaMap(artifact.schemas);
|
|
15
|
+
const spec = {
|
|
16
|
+
openapi: "3.0.3",
|
|
17
|
+
info: {
|
|
18
|
+
title: resolved.title,
|
|
19
|
+
version: resolved.version,
|
|
20
|
+
description: resolved.description
|
|
21
|
+
},
|
|
22
|
+
paths: {},
|
|
23
|
+
components: {
|
|
24
|
+
schemas: schemaMap
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
if (resolved.servers.length > 0) {
|
|
28
|
+
spec.servers = resolved.servers.map((server) => ({ ...server }));
|
|
29
|
+
}
|
|
30
|
+
for (const route of artifact.routes) {
|
|
31
|
+
const routePath = route.fullPath || buildFallbackPath(route);
|
|
32
|
+
const openApiPath = toOpenApiPath(routePath);
|
|
33
|
+
const method = route.method.toLowerCase();
|
|
34
|
+
if (!spec.paths[openApiPath]) {
|
|
35
|
+
spec.paths[openApiPath] = {};
|
|
36
|
+
}
|
|
37
|
+
;
|
|
38
|
+
spec.paths[openApiPath][method] = buildOperation(
|
|
39
|
+
route,
|
|
40
|
+
schemaMap
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return spec;
|
|
44
|
+
}
|
|
45
|
+
async function fromArtifact(artifact, options = {}) {
|
|
46
|
+
return fromArtifactSync(artifact, options);
|
|
47
|
+
}
|
|
48
|
+
async function write(openapi, outputPath) {
|
|
49
|
+
const absolute = path.isAbsolute(outputPath) ? outputPath : path.resolve(process.cwd(), outputPath);
|
|
50
|
+
await fs.mkdir(path.dirname(absolute), { recursive: true });
|
|
51
|
+
await fs.writeFile(absolute, JSON.stringify(openapi, null, 2), "utf-8");
|
|
52
|
+
return absolute;
|
|
53
|
+
}
|
|
54
|
+
function buildOperation(route, schemaMap) {
|
|
55
|
+
const controllerName = route.controller.replace(/Controller$/, "");
|
|
56
|
+
const parameters = route.parameters ?? [];
|
|
57
|
+
const operation = {
|
|
58
|
+
operationId: route.handler,
|
|
59
|
+
tags: [controllerName],
|
|
60
|
+
responses: buildResponses(route.returns, schemaMap)
|
|
61
|
+
};
|
|
62
|
+
const openApiParameters = buildParameters(parameters);
|
|
63
|
+
if (openApiParameters.length > 0) {
|
|
64
|
+
operation.parameters = openApiParameters;
|
|
65
|
+
}
|
|
66
|
+
const requestBody = buildRequestBody(parameters, schemaMap);
|
|
67
|
+
if (requestBody) {
|
|
68
|
+
operation.requestBody = requestBody;
|
|
69
|
+
}
|
|
70
|
+
return operation;
|
|
71
|
+
}
|
|
72
|
+
function buildParameters(parameters) {
|
|
73
|
+
const result = [];
|
|
74
|
+
for (const param of parameters) {
|
|
75
|
+
if (param.decoratorType === "param") {
|
|
76
|
+
result.push({
|
|
77
|
+
name: param.data ?? param.name,
|
|
78
|
+
in: "path",
|
|
79
|
+
required: true,
|
|
80
|
+
schema: tsTypeToJsonSchema(param.type)
|
|
81
|
+
});
|
|
82
|
+
} else if (param.decoratorType === "query") {
|
|
83
|
+
result.push({
|
|
84
|
+
name: param.data ?? param.name,
|
|
85
|
+
in: "query",
|
|
86
|
+
required: param.required === true,
|
|
87
|
+
schema: tsTypeToJsonSchema(param.type)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
function buildRequestBody(parameters, schemaMap) {
|
|
94
|
+
const bodyParam = parameters.find((param) => param.decoratorType === "body");
|
|
95
|
+
if (!bodyParam) return null;
|
|
96
|
+
const typeName = extractBaseTypeName(bodyParam.type);
|
|
97
|
+
const schema = typeName && schemaMap[typeName] ? { $ref: `#/components/schemas/${typeName}` } : { type: "object" };
|
|
98
|
+
return {
|
|
99
|
+
required: true,
|
|
100
|
+
content: {
|
|
101
|
+
"application/json": { schema }
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function buildResponses(returns, schemaMap) {
|
|
106
|
+
const responseSchema = resolveResponseSchema(returns, schemaMap);
|
|
107
|
+
if (!responseSchema) {
|
|
108
|
+
return { "200": { description: "Successful response" } };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
"200": {
|
|
112
|
+
description: "Successful response",
|
|
113
|
+
content: {
|
|
114
|
+
"application/json": { schema: responseSchema }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function resolveResponseSchema(returns, schemaMap) {
|
|
120
|
+
if (!returns) return null;
|
|
121
|
+
let innerType = returns;
|
|
122
|
+
const promiseMatch = returns.match(/^Promise<(.+)>$/);
|
|
123
|
+
if (promiseMatch) {
|
|
124
|
+
innerType = promiseMatch[1];
|
|
125
|
+
}
|
|
126
|
+
const isArray = innerType.endsWith("[]");
|
|
127
|
+
const baseType = isArray ? innerType.slice(0, -2) : innerType;
|
|
128
|
+
if (["string", "number", "boolean"].includes(baseType)) {
|
|
129
|
+
const primitiveSchema = tsTypeToJsonSchema(baseType);
|
|
130
|
+
return isArray ? { type: "array", items: primitiveSchema } : primitiveSchema;
|
|
131
|
+
}
|
|
132
|
+
if (["void", "any", "unknown"].includes(baseType)) return null;
|
|
133
|
+
if (schemaMap[baseType]) {
|
|
134
|
+
const ref = { $ref: `#/components/schemas/${baseType}` };
|
|
135
|
+
return isArray ? { type: "array", items: ref } : ref;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function buildSchemaMap(schemas) {
|
|
140
|
+
const result = {};
|
|
141
|
+
for (const schemaInfo of schemas) {
|
|
142
|
+
const definition = schemaInfo.schema?.definitions?.[schemaInfo.type];
|
|
143
|
+
if (definition) {
|
|
144
|
+
result[schemaInfo.type] = definition;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
function tsTypeToJsonSchema(tsType) {
|
|
150
|
+
switch (tsType) {
|
|
151
|
+
case "number":
|
|
152
|
+
return { type: "number" };
|
|
153
|
+
case "boolean":
|
|
154
|
+
return { type: "boolean" };
|
|
155
|
+
case "string":
|
|
156
|
+
default:
|
|
157
|
+
return { type: "string" };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function extractBaseTypeName(tsType) {
|
|
161
|
+
if (!tsType) return null;
|
|
162
|
+
let type = tsType;
|
|
163
|
+
const promiseMatch = type.match(/^Promise<(.+)>$/);
|
|
164
|
+
if (promiseMatch) type = promiseMatch[1];
|
|
165
|
+
type = type.replace(/\[\]$/, "");
|
|
166
|
+
const genericMatch = type.match(/^\w+<(\w+)>$/);
|
|
167
|
+
if (genericMatch) type = genericMatch[1];
|
|
168
|
+
if (["string", "number", "boolean", "any", "void", "unknown", "object"].includes(type)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return type;
|
|
172
|
+
}
|
|
173
|
+
function toOpenApiPath(expressPath) {
|
|
174
|
+
return expressPath.replace(/:(\w+)/g, "{$1}");
|
|
175
|
+
}
|
|
176
|
+
function buildFallbackPath(route) {
|
|
177
|
+
const parts = [route.prefix, route.version, route.route, route.path].filter((part) => part !== void 0 && part !== null && part !== "").map((part) => String(part).replace(/^\/+|\/+$/g, "")).filter((part) => part.length > 0);
|
|
178
|
+
const joined = parts.join("/");
|
|
179
|
+
return `/${joined}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/api-docs.plugin.ts
|
|
183
|
+
var DEFAULT_OPENAPI_ROUTE = "/openapi.json";
|
|
184
|
+
var DEFAULT_UI_ROUTE = "/docs";
|
|
185
|
+
var DEFAULT_UI_TITLE = "API Docs";
|
|
186
|
+
function isContextKey(artifact) {
|
|
187
|
+
return typeof artifact === "string";
|
|
188
|
+
}
|
|
189
|
+
function isArtifact(value) {
|
|
190
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
191
|
+
const obj = value;
|
|
192
|
+
return Array.isArray(obj.routes) && Array.isArray(obj.schemas);
|
|
193
|
+
}
|
|
194
|
+
var ApiDocsPlugin = class {
|
|
195
|
+
artifact;
|
|
196
|
+
openApiRoute;
|
|
197
|
+
uiRoute;
|
|
198
|
+
uiTitle;
|
|
199
|
+
reloadOnRequest;
|
|
200
|
+
genOptions;
|
|
201
|
+
app = null;
|
|
202
|
+
cachedSpec = null;
|
|
203
|
+
constructor(options) {
|
|
204
|
+
this.artifact = options.artifact;
|
|
205
|
+
this.openApiRoute = this.normalizeRoute(options.openApiRoute ?? DEFAULT_OPENAPI_ROUTE);
|
|
206
|
+
this.uiRoute = this.normalizeRoute(options.uiRoute ?? DEFAULT_UI_ROUTE);
|
|
207
|
+
this.uiTitle = options.uiTitle ?? DEFAULT_UI_TITLE;
|
|
208
|
+
this.reloadOnRequest = options.reloadOnRequest ?? false;
|
|
209
|
+
this.genOptions = {
|
|
210
|
+
title: options.title,
|
|
211
|
+
version: options.version,
|
|
212
|
+
description: options.description,
|
|
213
|
+
servers: options.servers
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
afterModulesRegistered = async (app, hono) => {
|
|
217
|
+
this.app = app;
|
|
218
|
+
hono.get(this.openApiRoute, async (c) => {
|
|
219
|
+
try {
|
|
220
|
+
const spec = await this.resolveSpec();
|
|
221
|
+
return c.json(spec);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
return c.json(
|
|
224
|
+
{
|
|
225
|
+
error: "Failed to load OpenAPI spec",
|
|
226
|
+
message: this.toErrorMessage(error)
|
|
227
|
+
},
|
|
228
|
+
500
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
hono.get(this.uiRoute, (c) => {
|
|
233
|
+
return c.html(this.renderSwaggerUiHtml());
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
normalizeRoute(input) {
|
|
237
|
+
const trimmed = input.trim();
|
|
238
|
+
if (!trimmed) return "/";
|
|
239
|
+
let normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
240
|
+
if (normalized.length > 1) {
|
|
241
|
+
normalized = normalized.replace(/\/+$/g, "");
|
|
242
|
+
}
|
|
243
|
+
return normalized || "/";
|
|
244
|
+
}
|
|
245
|
+
async resolveSpec() {
|
|
246
|
+
if (!this.reloadOnRequest && this.cachedSpec) {
|
|
247
|
+
return this.cachedSpec;
|
|
248
|
+
}
|
|
249
|
+
let artifact;
|
|
250
|
+
if (isContextKey(this.artifact)) {
|
|
251
|
+
if (!this.app) {
|
|
252
|
+
throw new Error("ApiDocsPlugin: app not available when resolving artifact from context");
|
|
253
|
+
}
|
|
254
|
+
const value = this.app.getContext().get(this.artifact);
|
|
255
|
+
if (value === void 0) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`ApiDocsPlugin: no artifact at context key '${this.artifact}'. Ensure RPC plugin (or another producer) runs before ApiDocs and writes to this key.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (!isArtifact(value)) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`ApiDocsPlugin: value at '${this.artifact}' is not a valid artifact (expected object with routes and schemas)`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
artifact = value;
|
|
266
|
+
} else {
|
|
267
|
+
artifact = this.artifact;
|
|
268
|
+
}
|
|
269
|
+
const spec = fromArtifactSync(artifact, this.genOptions);
|
|
270
|
+
if (!this.reloadOnRequest) this.cachedSpec = spec;
|
|
271
|
+
return spec;
|
|
272
|
+
}
|
|
273
|
+
renderSwaggerUiHtml() {
|
|
274
|
+
const title = this.escapeHtml(this.uiTitle);
|
|
275
|
+
const openApiRoute = this.escapeJsString(this.openApiRoute);
|
|
276
|
+
return `<!doctype html>
|
|
277
|
+
<html lang="en">
|
|
278
|
+
<head>
|
|
279
|
+
<meta charset="utf-8" />
|
|
280
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
281
|
+
<title>${title}</title>
|
|
282
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
|
|
283
|
+
</head>
|
|
284
|
+
<body>
|
|
285
|
+
<div id="swagger-ui"></div>
|
|
286
|
+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
287
|
+
<script>
|
|
288
|
+
window.onload = function () {
|
|
289
|
+
window.ui = SwaggerUIBundle({
|
|
290
|
+
url: '${openApiRoute}',
|
|
291
|
+
dom_id: '#swagger-ui'
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
</script>
|
|
295
|
+
</body>
|
|
296
|
+
</html>`;
|
|
297
|
+
}
|
|
298
|
+
escapeHtml(value) {
|
|
299
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
300
|
+
}
|
|
301
|
+
escapeJsString(value) {
|
|
302
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
303
|
+
}
|
|
304
|
+
toErrorMessage(error) {
|
|
305
|
+
return error instanceof Error ? error.message : String(error);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
export {
|
|
309
|
+
ApiDocsPlugin,
|
|
310
|
+
fromArtifact,
|
|
311
|
+
fromArtifactSync,
|
|
312
|
+
write
|
|
313
|
+
};
|
|
314
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/openapi.generator.ts","../src/api-docs.plugin.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\nimport type { OpenAPIV3 } from 'openapi-types'\n\nexport interface OpenApiGenerationOptions {\n\treadonly title?: string\n\treadonly version?: string\n\treadonly description?: string\n\treadonly servers?: readonly { url: string; description?: string }[]\n}\n\nexport interface OpenApiArtifactInput {\n\treadonly routes: readonly OpenApiRouteInput[]\n\treadonly schemas: readonly OpenApiSchemaInput[]\n}\n\nexport interface OpenApiRouteInput {\n\treadonly method: string\n\treadonly handler: string\n\treadonly controller: string\n\treadonly fullPath: string\n\treadonly path?: string\n\treadonly prefix?: string\n\treadonly version?: string\n\treadonly route?: string\n\treadonly returns?: string\n\treadonly parameters?: readonly OpenApiParameterInput[]\n}\n\nexport interface OpenApiParameterInput {\n\treadonly name: string\n\treadonly data?: string\n\treadonly type: string\n\treadonly required?: boolean\n\treadonly decoratorType: string\n}\n\nexport interface OpenApiSchemaInput {\n\treadonly type: string\n\treadonly schema: Record<string, any>\n}\n\n/** OpenAPI 3.x document type (openapi-types). */\nexport type OpenApiDocument = OpenAPIV3.Document\n\ninterface ResolvedOpenApiOptions {\n\treadonly title: string\n\treadonly version: string\n\treadonly description: string\n\treadonly servers: readonly { url: string; description?: string }[]\n}\n\nfunction resolveOptions(options: OpenApiGenerationOptions = {}): ResolvedOpenApiOptions {\n\treturn {\n\t\ttitle: options.title ?? 'API',\n\t\tversion: options.version ?? '1.0.0',\n\t\tdescription: options.description ?? '',\n\t\tservers: options.servers ?? []\n\t}\n}\n\nexport function fromArtifactSync(\n\tartifact: OpenApiArtifactInput,\n\toptions: OpenApiGenerationOptions = {}\n): OpenApiDocument {\n\tconst resolved = resolveOptions(options)\n\tconst schemaMap = buildSchemaMap(artifact.schemas)\n\tconst spec: OpenAPIV3.Document = {\n\t\topenapi: '3.0.3',\n\t\tinfo: {\n\t\t\ttitle: resolved.title,\n\t\t\tversion: resolved.version,\n\t\t\tdescription: resolved.description\n\t\t},\n\t\tpaths: {},\n\t\tcomponents: {\n\t\t\tschemas: schemaMap as OpenAPIV3.ComponentsObject['schemas']\n\t\t}\n\t}\n\n\tif (resolved.servers.length > 0) {\n\t\tspec.servers = resolved.servers.map((server) => ({ ...server }))\n\t}\n\n\tfor (const route of artifact.routes) {\n\t\tconst routePath = route.fullPath || buildFallbackPath(route)\n\t\tconst openApiPath = toOpenApiPath(routePath)\n\t\tconst method = route.method.toLowerCase() as keyof OpenAPIV3.PathItemObject\n\t\tif (!spec.paths[openApiPath]) {\n\t\t\tspec.paths[openApiPath] = {}\n\t\t}\n\t\t;(spec.paths[openApiPath] as Record<string, OpenAPIV3.OperationObject>)[method] = buildOperation(\n\t\t\troute,\n\t\t\tschemaMap\n\t\t)\n\t}\n\n\treturn spec\n}\n\nexport async function fromArtifact(\n\tartifact: OpenApiArtifactInput,\n\toptions: OpenApiGenerationOptions = {}\n): Promise<OpenApiDocument> {\n\treturn fromArtifactSync(artifact, options)\n}\n\nexport async function write(openapi: OpenApiDocument, outputPath: string): Promise<string> {\n\tconst absolute = path.isAbsolute(outputPath) ? outputPath : path.resolve(process.cwd(), outputPath)\n\tawait fs.mkdir(path.dirname(absolute), { recursive: true })\n\tawait fs.writeFile(absolute, JSON.stringify(openapi, null, 2), 'utf-8')\n\treturn absolute\n}\n\nfunction buildOperation(\n\troute: OpenApiRouteInput,\n\tschemaMap: Record<string, Record<string, any>>\n): OpenAPIV3.OperationObject {\n\tconst controllerName = route.controller.replace(/Controller$/, '')\n\tconst parameters = route.parameters ?? []\n\tconst operation: OpenAPIV3.OperationObject = {\n\t\toperationId: route.handler,\n\t\ttags: [controllerName],\n\t\tresponses: buildResponses(route.returns, schemaMap)\n\t}\n\n\tconst openApiParameters = buildParameters(parameters)\n\tif (openApiParameters.length > 0) {\n\t\toperation.parameters = openApiParameters\n\t}\n\n\tconst requestBody = buildRequestBody(parameters, schemaMap)\n\tif (requestBody) {\n\t\toperation.requestBody = requestBody\n\t}\n\n\treturn operation\n}\n\nfunction buildParameters(parameters: readonly OpenApiParameterInput[]): OpenAPIV3.ParameterObject[] {\n\tconst result: OpenAPIV3.ParameterObject[] = []\n\n\tfor (const param of parameters) {\n\t\tif (param.decoratorType === 'param') {\n\t\t\tresult.push({\n\t\t\t\tname: param.data ?? param.name,\n\t\t\t\tin: 'path',\n\t\t\t\trequired: true,\n\t\t\t\tschema: tsTypeToJsonSchema(param.type)\n\t\t\t})\n\t\t} else if (param.decoratorType === 'query') {\n\t\t\tresult.push({\n\t\t\t\tname: param.data ?? param.name,\n\t\t\t\tin: 'query',\n\t\t\t\trequired: param.required === true,\n\t\t\t\tschema: tsTypeToJsonSchema(param.type)\n\t\t\t})\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunction buildRequestBody(\n\tparameters: readonly OpenApiParameterInput[],\n\tschemaMap: Record<string, Record<string, any>>\n): OpenAPIV3.RequestBodyObject | null {\n\tconst bodyParam = parameters.find((param) => param.decoratorType === 'body')\n\tif (!bodyParam) return null\n\n\tconst typeName = extractBaseTypeName(bodyParam.type)\n\tconst schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject =\n\t\ttypeName && schemaMap[typeName]\n\t\t\t? { $ref: `#/components/schemas/${typeName}` }\n\t\t\t: { type: 'object' as const }\n\n\treturn {\n\t\trequired: true,\n\t\tcontent: {\n\t\t\t'application/json': { schema }\n\t\t}\n\t}\n}\n\nfunction buildResponses(\n\treturns: string | undefined,\n\tschemaMap: Record<string, Record<string, any>>\n): OpenAPIV3.ResponsesObject {\n\tconst responseSchema = resolveResponseSchema(returns, schemaMap)\n\n\tif (!responseSchema) {\n\t\treturn { '200': { description: 'Successful response' } }\n\t}\n\n\treturn {\n\t\t'200': {\n\t\t\tdescription: 'Successful response',\n\t\t\tcontent: {\n\t\t\t\t'application/json': { schema: responseSchema }\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction resolveResponseSchema(\n\treturns: string | undefined,\n\tschemaMap: Record<string, Record<string, any>>\n): Record<string, any> | null {\n\tif (!returns) return null\n\n\tlet innerType = returns\n\tconst promiseMatch = returns.match(/^Promise<(.+)>$/)\n\tif (promiseMatch) {\n\t\tinnerType = promiseMatch[1]\n\t}\n\n\tconst isArray = innerType.endsWith('[]')\n\tconst baseType = isArray ? innerType.slice(0, -2) : innerType\n\tif (['string', 'number', 'boolean'].includes(baseType)) {\n\t\tconst primitiveSchema = tsTypeToJsonSchema(baseType)\n\t\treturn isArray ? { type: 'array', items: primitiveSchema } : primitiveSchema\n\t}\n\tif (['void', 'any', 'unknown'].includes(baseType)) return null\n\tif (schemaMap[baseType]) {\n\t\tconst ref = { $ref: `#/components/schemas/${baseType}` }\n\t\treturn isArray ? { type: 'array', items: ref } : ref\n\t}\n\treturn null\n}\n\nfunction buildSchemaMap(schemas: readonly OpenApiSchemaInput[]): Record<string, Record<string, any>> {\n\tconst result: Record<string, Record<string, any>> = {}\n\tfor (const schemaInfo of schemas) {\n\t\tconst definition = schemaInfo.schema?.definitions?.[schemaInfo.type]\n\t\tif (definition) {\n\t\t\tresult[schemaInfo.type] = definition\n\t\t}\n\t}\n\treturn result\n}\n\nfunction tsTypeToJsonSchema(tsType: string): Record<string, unknown> {\n\tswitch (tsType) {\n\t\tcase 'number':\n\t\t\treturn { type: 'number' as const }\n\t\tcase 'boolean':\n\t\t\treturn { type: 'boolean' as const }\n\t\tcase 'string':\n\t\tdefault:\n\t\t\treturn { type: 'string' as const }\n\t}\n}\n\nfunction extractBaseTypeName(tsType: string): string | null {\n\tif (!tsType) return null\n\n\tlet type = tsType\n\tconst promiseMatch = type.match(/^Promise<(.+)>$/)\n\tif (promiseMatch) type = promiseMatch[1]\n\ttype = type.replace(/\\[\\]$/, '')\n\tconst genericMatch = type.match(/^\\w+<(\\w+)>$/)\n\tif (genericMatch) type = genericMatch[1]\n\tif (['string', 'number', 'boolean', 'any', 'void', 'unknown', 'object'].includes(type)) {\n\t\treturn null\n\t}\n\treturn type\n}\n\nfunction toOpenApiPath(expressPath: string): string {\n\treturn expressPath.replace(/:(\\w+)/g, '{$1}')\n}\n\nfunction buildFallbackPath(route: OpenApiRouteInput): string {\n\tconst parts = [route.prefix, route.version, route.route, route.path]\n\t\t.filter((part) => part !== undefined && part !== null && part !== '')\n\t\t.map((part) => String(part).replace(/^\\/+|\\/+$/g, ''))\n\t\t.filter((part) => part.length > 0)\n\tconst joined = parts.join('/')\n\treturn `/${joined}`\n}\n","import type { Application, IPlugin } from 'honestjs'\nimport type { Hono } from 'hono'\n\nimport { fromArtifactSync } from './openapi.generator'\nimport type { OpenApiArtifactInput, OpenApiGenerationOptions } from './openapi.generator'\n\nconst DEFAULT_OPENAPI_ROUTE = '/openapi.json'\nconst DEFAULT_UI_ROUTE = '/docs'\nconst DEFAULT_UI_TITLE = 'API Docs'\n\nexport type ArtifactInput = OpenApiArtifactInput | string\n\nfunction isContextKey(artifact: ArtifactInput): artifact is string {\n\treturn typeof artifact === 'string'\n}\n\nfunction isArtifact(value: unknown): value is OpenApiArtifactInput {\n\tif (!value || typeof value !== 'object' || Array.isArray(value)) return false\n\tconst obj = value as Record<string, unknown>\n\treturn Array.isArray(obj.routes) && Array.isArray(obj.schemas)\n}\n\nexport interface ApiDocsPluginOptions extends OpenApiGenerationOptions {\n\t/** Artifact: direct object `{ routes, schemas }` or context key string (e.g. `'rpc.artifact'`) resolved via app.getContext().get(key). OpenAPI is always generated from the artifact. */\n\treadonly artifact: ArtifactInput\n\treadonly openApiRoute?: string\n\treadonly uiRoute?: string\n\treadonly uiTitle?: string\n\treadonly reloadOnRequest?: boolean\n}\n\nexport class ApiDocsPlugin implements IPlugin {\n\tprivate readonly artifact: ArtifactInput\n\tprivate readonly openApiRoute: string\n\tprivate readonly uiRoute: string\n\tprivate readonly uiTitle: string\n\tprivate readonly reloadOnRequest: boolean\n\tprivate readonly genOptions: OpenApiGenerationOptions\n\n\tprivate app: Application | null = null\n\tprivate cachedSpec: Record<string, unknown> | null = null\n\n\tconstructor(options: ApiDocsPluginOptions) {\n\t\tthis.artifact = options.artifact\n\t\tthis.openApiRoute = this.normalizeRoute(options.openApiRoute ?? DEFAULT_OPENAPI_ROUTE)\n\t\tthis.uiRoute = this.normalizeRoute(options.uiRoute ?? DEFAULT_UI_ROUTE)\n\t\tthis.uiTitle = options.uiTitle ?? DEFAULT_UI_TITLE\n\t\tthis.reloadOnRequest = options.reloadOnRequest ?? false\n\t\tthis.genOptions = {\n\t\t\ttitle: options.title,\n\t\t\tversion: options.version,\n\t\t\tdescription: options.description,\n\t\t\tservers: options.servers\n\t\t}\n\t}\n\n\tafterModulesRegistered = async (app: Application, hono: Hono): Promise<void> => {\n\t\tthis.app = app\n\n\t\thono.get(this.openApiRoute, async (c) => {\n\t\t\ttry {\n\t\t\t\tconst spec = await this.resolveSpec()\n\t\t\t\treturn c.json(spec)\n\t\t\t} catch (error) {\n\t\t\t\treturn c.json(\n\t\t\t\t\t{\n\t\t\t\t\t\terror: 'Failed to load OpenAPI spec',\n\t\t\t\t\t\tmessage: this.toErrorMessage(error)\n\t\t\t\t\t},\n\t\t\t\t\t500\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\n\t\thono.get(this.uiRoute, (c) => {\n\t\t\treturn c.html(this.renderSwaggerUiHtml())\n\t\t})\n\t}\n\n\tprivate normalizeRoute(input: string): string {\n\t\tconst trimmed = input.trim()\n\t\tif (!trimmed) return '/'\n\t\tlet normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`\n\t\tif (normalized.length > 1) {\n\t\t\tnormalized = normalized.replace(/\\/+$/g, '')\n\t\t}\n\t\treturn normalized || '/'\n\t}\n\n\tprivate async resolveSpec(): Promise<Record<string, unknown>> {\n\t\tif (!this.reloadOnRequest && this.cachedSpec) {\n\t\t\treturn this.cachedSpec\n\t\t}\n\n\t\tlet artifact: OpenApiArtifactInput\n\n\t\tif (isContextKey(this.artifact)) {\n\t\t\tif (!this.app) {\n\t\t\t\tthrow new Error('ApiDocsPlugin: app not available when resolving artifact from context')\n\t\t\t}\n\t\t\tconst value = this.app.getContext().get<unknown>(this.artifact)\n\t\t\tif (value === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`ApiDocsPlugin: no artifact at context key '${this.artifact}'. Ensure RPC plugin (or another producer) runs before ApiDocs and writes to this key.`\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (!isArtifact(value)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`ApiDocsPlugin: value at '${this.artifact}' is not a valid artifact (expected object with routes and schemas)`\n\t\t\t\t)\n\t\t\t}\n\t\t\tartifact = value\n\t\t} else {\n\t\t\tartifact = this.artifact\n\t\t}\n\n\t\tconst spec = fromArtifactSync(artifact, this.genOptions) as unknown as Record<string, unknown>\n\t\tif (!this.reloadOnRequest) this.cachedSpec = spec\n\t\treturn spec\n\t}\n\n\tprivate renderSwaggerUiHtml(): string {\n\t\tconst title = this.escapeHtml(this.uiTitle)\n\t\tconst openApiRoute = this.escapeJsString(this.openApiRoute)\n\n\t\treturn `<!doctype html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\" />\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t<title>${title}</title>\n\t<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\" />\n</head>\n<body>\n\t<div id=\"swagger-ui\"></div>\n\t<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n\t<script>\n\t\twindow.onload = function () {\n\t\t\twindow.ui = SwaggerUIBundle({\n\t\t\t\turl: '${openApiRoute}',\n\t\t\t\tdom_id: '#swagger-ui'\n\t\t\t});\n\t\t};\n\t</script>\n</body>\n</html>`\n\t}\n\n\tprivate escapeHtml(value: string): string {\n\t\treturn value\n\t\t\t.replace(/&/g, '&')\n\t\t\t.replace(/</g, '<')\n\t\t\t.replace(/>/g, '>')\n\t\t\t.replace(/\"/g, '"')\n\t\t\t.replace(/'/g, ''')\n\t}\n\n\tprivate escapeJsString(value: string): string {\n\t\treturn value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\")\n\t}\n\n\tprivate toErrorMessage(error: unknown): string {\n\t\treturn error instanceof Error ? error.message : String(error)\n\t}\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@honestjs/api-docs-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "API Docs plugin for HonestJS - generates OpenAPI from artifact object or context key and serves Swagger UI",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"honestjs",
|
|
20
|
+
"hono",
|
|
21
|
+
"swagger",
|
|
22
|
+
"openapi",
|
|
23
|
+
"api-docs"
|
|
24
|
+
],
|
|
25
|
+
"author": "Orkhan Karimov <karimovok1@gmail.com> (https://github.com/kerimovok)",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/honestjs/plugins.git",
|
|
30
|
+
"directory": "packages/api-docs-plugin"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"honestjs": "^0.1.9",
|
|
34
|
+
"hono": "^4.12.6",
|
|
35
|
+
"reflect-metadata": "^0.2.2",
|
|
36
|
+
"vitest": "^3.2.4"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"openapi-types": "^12.1.3"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"honestjs": "^0.1.9",
|
|
43
|
+
"hono": "^4.0.0",
|
|
44
|
+
"typescript": "^5.x"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"dev": "tsup --watch",
|
|
49
|
+
"clean": "rm -rf dist",
|
|
50
|
+
"type-check": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest"
|
|
53
|
+
}
|
|
54
|
+
}
|