@rx-ted/packages-honest-plugins 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +45 -0
  3. package/api-doc/.turbo/turbo-build.log +1 -0
  4. package/api-doc/.turbo/turbo-test.log +11 -0
  5. package/api-doc/.turbo/turbo-typecheck.log +1 -0
  6. package/api-doc/CHANGELOG.md +22 -0
  7. package/api-doc/README.md +80 -0
  8. package/api-doc/dist/api-doc.plugin.d.ts +53 -0
  9. package/api-doc/dist/api-doc.plugin.js +452 -0
  10. package/api-doc/dist/index.d.ts +5 -0
  11. package/api-doc/dist/index.js +3 -0
  12. package/api-doc/dist/interfaces/renderer.interface.d.ts +7 -0
  13. package/api-doc/dist/interfaces/renderer.interface.js +1 -0
  14. package/api-doc/dist/renderers/scalar.renderer.d.ts +10 -0
  15. package/api-doc/dist/renderers/scalar.renderer.js +76 -0
  16. package/api-doc/dist/renderers/swagger.renderer.d.ts +10 -0
  17. package/api-doc/dist/renderers/swagger.renderer.js +83 -0
  18. package/api-doc/node_modules/.bin/esbuild +22 -0
  19. package/api-doc/node_modules/.bin/jiti +22 -0
  20. package/api-doc/node_modules/.bin/sass +22 -0
  21. package/api-doc/node_modules/.bin/tsx +22 -0
  22. package/api-doc/node_modules/.bin/vite +22 -0
  23. package/api-doc/node_modules/.bin/vitest +22 -0
  24. package/api-doc/node_modules/.bin/yaml +22 -0
  25. package/api-doc/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  26. package/api-doc/package.json +48 -0
  27. package/api-doc/src/api-doc.plugin.ts +554 -0
  28. package/api-doc/src/index.ts +5 -0
  29. package/api-doc/src/interfaces/renderer.interface.ts +5 -0
  30. package/api-doc/src/plugin.test.ts +740 -0
  31. package/api-doc/src/renderers/scalar.renderer.ts +82 -0
  32. package/api-doc/src/renderers/swagger.renderer.ts +89 -0
  33. package/api-doc/tsconfig.json +21 -0
  34. package/api-doc/vitest.config.ts +13 -0
  35. package/cache/.turbo/turbo-build.log +1 -0
  36. package/cache/.turbo/turbo-test.log +16 -0
  37. package/cache/CHANGELOG.md +23 -0
  38. package/cache/README.md +79 -0
  39. package/cache/dist/cache-service.d.ts +8 -0
  40. package/cache/dist/cache-service.js +21 -0
  41. package/cache/dist/cacheable.d.ts +2 -0
  42. package/cache/dist/cacheable.js +10 -0
  43. package/cache/dist/cloudflare/driver.d.ts +23 -0
  44. package/cache/dist/cloudflare/driver.js +102 -0
  45. package/cache/dist/cloudflare/index.d.ts +3 -0
  46. package/cache/dist/cloudflare/index.js +2 -0
  47. package/cache/dist/cloudflare/plugin.d.ts +19 -0
  48. package/cache/dist/cloudflare/plugin.js +58 -0
  49. package/cache/dist/index.d.ts +11 -0
  50. package/cache/dist/index.js +14 -0
  51. package/cache/dist/local/driver.d.ts +2 -0
  52. package/cache/dist/local/driver.js +98 -0
  53. package/cache/dist/local/index.d.ts +2 -0
  54. package/cache/dist/local/index.js +2 -0
  55. package/cache/dist/local/plugin.d.ts +14 -0
  56. package/cache/dist/local/plugin.js +24 -0
  57. package/cache/dist/redis/driver.d.ts +10 -0
  58. package/cache/dist/redis/driver.js +102 -0
  59. package/cache/dist/redis/index.d.ts +4 -0
  60. package/cache/dist/redis/index.js +2 -0
  61. package/cache/dist/redis/plugin.d.ts +25 -0
  62. package/cache/dist/redis/plugin.js +61 -0
  63. package/cache/dist/resolve.d.ts +24 -0
  64. package/cache/dist/resolve.js +114 -0
  65. package/cache/dist/types.d.ts +18 -0
  66. package/cache/dist/types.js +1 -0
  67. package/cache/node_modules/.bin/esbuild +22 -0
  68. package/cache/node_modules/.bin/jiti +22 -0
  69. package/cache/node_modules/.bin/sass +22 -0
  70. package/cache/node_modules/.bin/tsx +22 -0
  71. package/cache/node_modules/.bin/vite +22 -0
  72. package/cache/node_modules/.bin/vitest +22 -0
  73. package/cache/node_modules/.bin/yaml +22 -0
  74. package/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  75. package/cache/package.json +46 -0
  76. package/cache/src/cache-service.test.ts +16 -0
  77. package/cache/src/cache-service.ts +15 -0
  78. package/cache/src/cacheable.test.ts +78 -0
  79. package/cache/src/cacheable.ts +16 -0
  80. package/cache/src/cloudflare/driver.test.ts +188 -0
  81. package/cache/src/cloudflare/driver.ts +118 -0
  82. package/cache/src/cloudflare/index.ts +3 -0
  83. package/cache/src/cloudflare/plugin.ts +71 -0
  84. package/cache/src/index.test.ts +68 -0
  85. package/cache/src/index.ts +20 -0
  86. package/cache/src/local/driver.ts +117 -0
  87. package/cache/src/local/index.ts +2 -0
  88. package/cache/src/local/plugin.ts +34 -0
  89. package/cache/src/redis/driver.test.ts +71 -0
  90. package/cache/src/redis/driver.ts +124 -0
  91. package/cache/src/redis/index.ts +4 -0
  92. package/cache/src/redis/plugin.ts +84 -0
  93. package/cache/src/resolve.test.ts +257 -0
  94. package/cache/src/resolve.ts +140 -0
  95. package/cache/src/types.ts +14 -0
  96. package/cache/tsconfig.json +21 -0
  97. package/cache/vitest.config.ts +13 -0
  98. package/db/.turbo/turbo-build.log +1 -0
  99. package/db/.turbo/turbo-test.log +21 -0
  100. package/db/CHANGELOG.md +26 -0
  101. package/db/README.md +99 -0
  102. package/db/dist/constants.d.ts +1 -0
  103. package/db/dist/constants.js +1 -0
  104. package/db/dist/d1/index.d.ts +3 -0
  105. package/db/dist/d1/index.js +2 -0
  106. package/db/dist/d1/plugin.d.ts +18 -0
  107. package/db/dist/d1/plugin.js +104 -0
  108. package/db/dist/db-service.d.ts +7 -0
  109. package/db/dist/db-service.js +21 -0
  110. package/db/dist/index.d.ts +15 -0
  111. package/db/dist/index.js +20 -0
  112. package/db/dist/mysql/client.d.ts +36 -0
  113. package/db/dist/mysql/client.js +84 -0
  114. package/db/dist/mysql/driver.d.ts +18 -0
  115. package/db/dist/mysql/driver.js +107 -0
  116. package/db/dist/mysql/index.d.ts +11 -0
  117. package/db/dist/mysql/index.js +11 -0
  118. package/db/dist/mysql/plugin.d.ts +51 -0
  119. package/db/dist/mysql/plugin.js +94 -0
  120. package/db/dist/mysql/types.d.ts +38 -0
  121. package/db/dist/mysql/types.js +1 -0
  122. package/db/dist/postgres/index.d.ts +2 -0
  123. package/db/dist/postgres/index.js +3 -0
  124. package/db/dist/repository.d.ts +14 -0
  125. package/db/dist/repository.js +79 -0
  126. package/db/dist/resolve.d.ts +6 -0
  127. package/db/dist/resolve.js +81 -0
  128. package/db/dist/schema-builder/column-builder.d.ts +26 -0
  129. package/db/dist/schema-builder/column-builder.js +53 -0
  130. package/db/dist/schema-builder/compile-d1.d.ts +5 -0
  131. package/db/dist/schema-builder/compile-d1.js +67 -0
  132. package/db/dist/schema-builder/compile-mysql.d.ts +12 -0
  133. package/db/dist/schema-builder/compile-mysql.js +78 -0
  134. package/db/dist/schema-builder/index.d.ts +8 -0
  135. package/db/dist/schema-builder/index.js +22 -0
  136. package/db/dist/schema-builder/table-builder.d.ts +16 -0
  137. package/db/dist/schema-builder/table-builder.js +38 -0
  138. package/db/dist/schema-builder/types.d.ts +26 -0
  139. package/db/dist/schema-builder/types.js +1 -0
  140. package/db/dist/schema-builder/zod-bridge.d.ts +24 -0
  141. package/db/dist/schema-builder/zod-bridge.js +53 -0
  142. package/db/dist/sqlite/index.d.ts +4 -0
  143. package/db/dist/sqlite/index.js +2 -0
  144. package/db/dist/sqlite/plugin.d.ts +17 -0
  145. package/db/dist/sqlite/plugin.js +33 -0
  146. package/db/dist/types.d.ts +42 -0
  147. package/db/dist/types.js +1 -0
  148. package/db/node_modules/.bin/esbuild +22 -0
  149. package/db/node_modules/.bin/jiti +22 -0
  150. package/db/node_modules/.bin/sass +22 -0
  151. package/db/node_modules/.bin/tsx +22 -0
  152. package/db/node_modules/.bin/vite +22 -0
  153. package/db/node_modules/.bin/vitest +22 -0
  154. package/db/node_modules/.bin/yaml +22 -0
  155. package/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  156. package/db/package.json +59 -0
  157. package/db/src/constants.test.ts +8 -0
  158. package/db/src/constants.ts +1 -0
  159. package/db/src/d1/index.ts +13 -0
  160. package/db/src/d1/plugin.ts +135 -0
  161. package/db/src/db-service.ts +14 -0
  162. package/db/src/index.test.ts +63 -0
  163. package/db/src/index.ts +57 -0
  164. package/db/src/mysql/client.ts +112 -0
  165. package/db/src/mysql/driver.test.ts +190 -0
  166. package/db/src/mysql/driver.ts +116 -0
  167. package/db/src/mysql/index.ts +51 -0
  168. package/db/src/mysql/plugin.test.ts +228 -0
  169. package/db/src/mysql/plugin.ts +128 -0
  170. package/db/src/mysql/types.ts +32 -0
  171. package/db/src/postgres/index.ts +15 -0
  172. package/db/src/repository.test.ts +195 -0
  173. package/db/src/repository.ts +95 -0
  174. package/db/src/resolve.ts +105 -0
  175. package/db/src/schema-builder/column-builder.test.ts +55 -0
  176. package/db/src/schema-builder/column-builder.ts +87 -0
  177. package/db/src/schema-builder/compile-d1.test.ts +60 -0
  178. package/db/src/schema-builder/compile-d1.ts +70 -0
  179. package/db/src/schema-builder/compile-mysql.test.ts +64 -0
  180. package/db/src/schema-builder/compile-mysql.ts +102 -0
  181. package/db/src/schema-builder/index.test.ts +36 -0
  182. package/db/src/schema-builder/index.ts +41 -0
  183. package/db/src/schema-builder/table-builder.test.ts +55 -0
  184. package/db/src/schema-builder/table-builder.ts +50 -0
  185. package/db/src/schema-builder/types.test.ts +68 -0
  186. package/db/src/schema-builder/types.ts +30 -0
  187. package/db/src/schema-builder/zod-bridge.ts +89 -0
  188. package/db/src/sqlite/index.ts +14 -0
  189. package/db/src/sqlite/plugin.ts +46 -0
  190. package/db/src/types.ts +45 -0
  191. package/db/tsconfig.json +21 -0
  192. package/db/vitest.config.ts +13 -0
  193. package/mail/.turbo/turbo-build.log +1 -0
  194. package/mail/.turbo/turbo-test.log +12 -0
  195. package/mail/.turbo/turbo-typecheck.log +1 -0
  196. package/mail/CHANGELOG.md +25 -0
  197. package/mail/README.md +214 -0
  198. package/mail/dist/index.d.ts +4 -0
  199. package/mail/dist/index.js +2 -0
  200. package/mail/dist/plugin.d.ts +64 -0
  201. package/mail/dist/plugin.js +232 -0
  202. package/mail/dist/providers/brevo.provider.d.ts +15 -0
  203. package/mail/dist/providers/brevo.provider.js +75 -0
  204. package/mail/dist/providers/custom.provider.d.ts +9 -0
  205. package/mail/dist/providers/custom.provider.js +22 -0
  206. package/mail/dist/providers/resend.provider.d.ts +16 -0
  207. package/mail/dist/providers/resend.provider.js +77 -0
  208. package/mail/dist/providers/smtp.provider.d.ts +21 -0
  209. package/mail/dist/providers/smtp.provider.js +86 -0
  210. package/mail/dist/rate-limiter.d.ts +16 -0
  211. package/mail/dist/rate-limiter.js +49 -0
  212. package/mail/dist/types.d.ts +68 -0
  213. package/mail/dist/types.js +6 -0
  214. package/mail/node_modules/.bin/esbuild +22 -0
  215. package/mail/node_modules/.bin/jiti +22 -0
  216. package/mail/node_modules/.bin/sass +22 -0
  217. package/mail/node_modules/.bin/tsx +22 -0
  218. package/mail/node_modules/.bin/vite +22 -0
  219. package/mail/node_modules/.bin/vitest +22 -0
  220. package/mail/node_modules/.bin/yaml +22 -0
  221. package/mail/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  222. package/mail/package.json +52 -0
  223. package/mail/src/index.ts +4 -0
  224. package/mail/src/plugin.test.ts +219 -0
  225. package/mail/src/plugin.ts +313 -0
  226. package/mail/src/providers/brevo.provider.ts +78 -0
  227. package/mail/src/providers/custom.provider.ts +29 -0
  228. package/mail/src/providers/resend.provider.ts +74 -0
  229. package/mail/src/providers/smtp.provider.ts +85 -0
  230. package/mail/src/rate-limiter.test.ts +81 -0
  231. package/mail/src/rate-limiter.ts +54 -0
  232. package/mail/src/types.ts +81 -0
  233. package/mail/tsconfig.json +21 -0
  234. package/mail/vitest.config.ts +13 -0
  235. package/package.json +36 -0
  236. package/s3/.turbo/turbo-build.log +1 -0
  237. package/s3/.turbo/turbo-test.log +11 -0
  238. package/s3/CHANGELOG.md +20 -0
  239. package/s3/README.md +90 -0
  240. package/s3/dist/index.d.ts +6 -0
  241. package/s3/dist/index.js +4 -0
  242. package/s3/dist/s3-service.d.ts +7 -0
  243. package/s3/dist/s3-service.js +21 -0
  244. package/s3/dist/s3.driver.d.ts +6 -0
  245. package/s3/dist/s3.driver.js +221 -0
  246. package/s3/dist/s3.plugin.d.ts +24 -0
  247. package/s3/dist/s3.plugin.js +58 -0
  248. package/s3/dist/types.d.ts +30 -0
  249. package/s3/dist/types.js +1 -0
  250. package/s3/node_modules/.bin/esbuild +22 -0
  251. package/s3/node_modules/.bin/jiti +22 -0
  252. package/s3/node_modules/.bin/sass +22 -0
  253. package/s3/node_modules/.bin/tsx +22 -0
  254. package/s3/node_modules/.bin/vite +22 -0
  255. package/s3/node_modules/.bin/vitest +22 -0
  256. package/s3/node_modules/.bin/yaml +22 -0
  257. package/s3/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  258. package/s3/package.json +47 -0
  259. package/s3/src/index.test.ts +20 -0
  260. package/s3/src/index.ts +13 -0
  261. package/s3/src/s3-service.ts +14 -0
  262. package/s3/src/s3.driver.ts +259 -0
  263. package/s3/src/s3.plugin.ts +80 -0
  264. package/s3/src/types.ts +37 -0
  265. package/s3/tsconfig.json +21 -0
  266. package/s3/vitest.config.ts +13 -0
@@ -0,0 +1,554 @@
1
+ import { ENV_SYMBOL, type Env, type ILogger } from '@rx-ted/packages-core';
2
+ import type {
3
+ ApiDocOptions,
4
+ ApiDocSecurityScheme,
5
+ ApiDocSecurityRequirement,
6
+ ApiTagOptions,
7
+ Application,
8
+ HonestOptions,
9
+ IPlugin,
10
+ } from '@rx-ted/packages-honest';
11
+ import {
12
+ ComponentManager,
13
+ VERSION_NEUTRAL,
14
+ normalizePath,
15
+ resolvePluginLogger,
16
+ } from '@rx-ted/packages-honest';
17
+ import type { Context, Hono } from 'hono';
18
+ import type { IDocRenderer } from './interfaces/renderer.interface';
19
+ import { ScalarRenderer } from './renderers/scalar.renderer';
20
+ import { SwaggerRenderer } from './renderers/swagger.renderer';
21
+
22
+ type RendererName = 'scalar' | 'swagger';
23
+
24
+ export interface ApiDocPluginOptions {
25
+ specUrl?: string;
26
+ uiRoute?: string;
27
+ uiTitle?: string;
28
+ defaultRenderer?: RendererName;
29
+ renderers?: IDocRenderer[];
30
+ onRequest?: (c: Context) => void | Promise<void>;
31
+ securitySchemes?: Record<string, ApiDocSecurityScheme>;
32
+ security?: ApiDocSecurityRequirement[];
33
+ exclude?: string[];
34
+ excludeMethods?: string[];
35
+ excludeTags?: string[];
36
+ }
37
+
38
+ export function normalizeRoute(input: string): string {
39
+ const trimmed = input.trim();
40
+ if (!trimmed) return '/';
41
+ let normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
42
+ if (normalized.length > 1) {
43
+ normalized = normalized.replace(/\/+$/g, '');
44
+ }
45
+ return normalized || '/';
46
+ }
47
+
48
+ const DEFAULT_SECURITY_SCHEMES: Record<string, ApiDocSecurityScheme> = {
49
+ bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
50
+ basicAuth: { type: 'http', scheme: 'basic' },
51
+ apiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },
52
+ cookieAuth: { type: 'apiKey', in: 'cookie', name: 'token' },
53
+ };
54
+
55
+ export class ApiDocPlugin implements IPlugin {
56
+ readonly name = 'api-doc-plugin';
57
+ readonly version = '1.0.0';
58
+ logger?: ILogger;
59
+
60
+ private readonly specUrl: string = '/openapi.json';
61
+ private readonly uiRoute: string = '/docs';
62
+ private readonly uiTitle: string = 'API Documentation';
63
+ private readonly defaultRendererName: RendererName = 'scalar';
64
+ private readonly onRequest?: (c: Context) => void | Promise<void>;
65
+ private readonly securitySchemes: Record<string, ApiDocSecurityScheme> = {
66
+ ...DEFAULT_SECURITY_SCHEMES,
67
+ };
68
+ private readonly defaultSecurity?: ApiDocSecurityRequirement[];
69
+ private readonly exclude?: string[];
70
+ private readonly excludeMethods?: string[];
71
+ private readonly excludeTags?: string[];
72
+ private renderers: Map<string, IDocRenderer> = new Map();
73
+ private spec: Record<string, any> | null = null;
74
+ private routingPrefix?: string;
75
+ private routingVersion?: number | typeof VERSION_NEUTRAL | number[];
76
+ private app?: Application;
77
+
78
+ constructor(options: ApiDocPluginOptions = {}) {
79
+ if (options.specUrl !== undefined) this.specUrl = options.specUrl;
80
+ if (options.uiRoute !== undefined) this.uiRoute = normalizeRoute(options.uiRoute);
81
+ if (options.uiTitle !== undefined) this.uiTitle = options.uiTitle;
82
+ if (options.defaultRenderer !== undefined) this.defaultRendererName = options.defaultRenderer;
83
+ if (options.onRequest !== undefined) this.onRequest = options.onRequest;
84
+ if (options.securitySchemes !== undefined) this.securitySchemes = options.securitySchemes;
85
+ if (options.security !== undefined) this.defaultSecurity = options.security;
86
+ if (options.exclude !== undefined) this.exclude = options.exclude;
87
+ if (options.excludeMethods !== undefined) this.excludeMethods = options.excludeMethods;
88
+ if (options.excludeTags !== undefined) this.excludeTags = options.excludeTags;
89
+ if (options.renderers !== undefined) {
90
+ options.renderers.forEach((r) => this.renderers.set(r.name, r));
91
+ } else {
92
+ [new ScalarRenderer(), new SwaggerRenderer()].forEach((r) => this.renderers.set(r.name, r));
93
+ }
94
+ }
95
+
96
+ private applyPrefixToUrl(url: string): string {
97
+ if (!this.routingPrefix && !this.routingVersion) {
98
+ return url;
99
+ }
100
+
101
+ const prefix = this.routingPrefix ? normalizePath(`/${this.routingPrefix}`) : '';
102
+ let versionSegment = '';
103
+
104
+ if (
105
+ this.routingVersion !== undefined &&
106
+ this.routingVersion !== null &&
107
+ this.routingVersion !== VERSION_NEUTRAL &&
108
+ !Array.isArray(this.routingVersion)
109
+ ) {
110
+ versionSegment = `/v${this.routingVersion}`;
111
+ }
112
+
113
+ const base = `${prefix}${versionSegment}`;
114
+ if (!base) return url;
115
+
116
+ const urlPath = url.startsWith('/') ? url : `/${url}`;
117
+ return normalizePath(`${base}${urlPath}`);
118
+ }
119
+
120
+ setRoutingOptions(options?: HonestOptions['routing']): void {
121
+ if (options) {
122
+ this.routingPrefix = options.prefix;
123
+ this.routingVersion = options.version;
124
+ }
125
+ }
126
+
127
+ registerRenderer(renderer: IDocRenderer): void {
128
+ this.renderers.set(renderer.name, renderer);
129
+ }
130
+
131
+ getRenderer(name: string): IDocRenderer | undefined {
132
+ return this.renderers.get(name);
133
+ }
134
+
135
+ beforeModulesRegistered = async (app: Application, hono: Hono): Promise<void> => {
136
+ this.logger ??= resolvePluginLogger(this.name);
137
+
138
+ if (!ComponentManager.hasPlugin(ENV_SYMBOL)) {
139
+ this.logger.warn('ApiDoc disabled (env not configured)');
140
+ return;
141
+ }
142
+
143
+ const envInstance = ComponentManager.getPlugin<Env>(ENV_SYMBOL);
144
+ if (!envInstance.DEBUG) {
145
+ this.logger.warn('ApiDoc disabled in production');
146
+ return;
147
+ }
148
+
149
+ try {
150
+ const routingOptions = app.getRoutingOptions();
151
+ if (routingOptions) {
152
+ this.setRoutingOptions(routingOptions);
153
+ }
154
+ } catch {
155
+ // Ignore if options are not accessible
156
+ }
157
+
158
+ const specUrlWithPrefix = this.applyPrefixToUrl(this.specUrl);
159
+ const uiRouteWithPrefix = this.applyPrefixToUrl(this.uiRoute);
160
+
161
+ // Register callback to print doc URLs when server starts
162
+ if (app.registerOnStartCallback) {
163
+ app.registerOnStartCallback((port: number) => {
164
+ const base = `http://localhost:${port}`;
165
+ this.logger?.info(`Docs: ${base}${uiRouteWithPrefix}`);
166
+ this.logger?.info(`OpenAPI JSON: ${base}${specUrlWithPrefix}`);
167
+ });
168
+ }
169
+
170
+ hono.get(specUrlWithPrefix, async (c: Context) => {
171
+ await this.runHook(c);
172
+ if (!this.spec) {
173
+ return c.json(
174
+ { status: 500, code: 'ERROR', data: null, error: 'API spec not generated yet' },
175
+ 500,
176
+ );
177
+ }
178
+ return c.json({ status: 200, code: 'OK', data: this.spec });
179
+ });
180
+
181
+ hono.get(uiRouteWithPrefix, async (c: Context) => {
182
+ await this.runHook(c);
183
+ const renderer = this.renderers.get(this.defaultRendererName);
184
+ if (!renderer) {
185
+ return c.json({ error: `Default renderer '${this.defaultRendererName}' not found` }, 500);
186
+ }
187
+ return c.html(
188
+ renderer.renderHtml({
189
+ specUrl: specUrlWithPrefix,
190
+ uiTitle: this.uiTitle,
191
+ }),
192
+ );
193
+ });
194
+
195
+ hono.get(`${uiRouteWithPrefix}/:renderer`, async (c: Context) => {
196
+ await this.runHook(c);
197
+ const rendererName = c.req.param('renderer');
198
+ if (!rendererName)
199
+ return c.json(
200
+ {
201
+ error: `Renderer '${rendererName}' not found`,
202
+ available: Array.from(this.renderers.keys()),
203
+ },
204
+ 404,
205
+ );
206
+
207
+ const renderer = this.renderers.get(rendererName);
208
+
209
+ if (!renderer) {
210
+ return c.json(
211
+ {
212
+ error: `Renderer '${rendererName}' not found`,
213
+ available: Array.from(this.renderers.keys()),
214
+ },
215
+ 404,
216
+ );
217
+ }
218
+
219
+ return c.html(
220
+ renderer.renderHtml({
221
+ specUrl: specUrlWithPrefix,
222
+ uiTitle: this.uiTitle,
223
+ }),
224
+ );
225
+ });
226
+ };
227
+
228
+ afterModulesRegistered = async (app: Application, hono: Hono): Promise<void> => {
229
+ this.app = app;
230
+ this.spec = this.generateSpec();
231
+ };
232
+
233
+ private generateSpec(): Record<string, any> {
234
+ const paths: Record<string, any> = {};
235
+ const tags: Array<{ name: string; description: string }> = [];
236
+
237
+ if (!this.app) {
238
+ return {
239
+ openapi: '3.0.0',
240
+ info: {
241
+ title: this.uiTitle,
242
+ version: '1.0.0',
243
+ description: 'API Documentation',
244
+ },
245
+ paths: {},
246
+ };
247
+ }
248
+
249
+ const routes = this.app.getRoutes();
250
+ const controllerTags: Map<string | symbol, ApiTagOptions> = new Map();
251
+
252
+ // Collect all controller tags first
253
+ for (const route of routes) {
254
+ if (route.controllerClass) {
255
+ const tag = Reflect.getMetadata('api:controller:tag', route.controllerClass) as
256
+ | ApiTagOptions
257
+ | undefined;
258
+ if (tag && !controllerTags.has(route.controller)) {
259
+ controllerTags.set(route.controller, tag);
260
+ tags.push({
261
+ name: tag.name,
262
+ description: tag.description ?? `${tag.name} endpoints`,
263
+ });
264
+ }
265
+ }
266
+ }
267
+
268
+ for (const route of routes) {
269
+ let fullPath = route.fullPath;
270
+ if (!fullPath) continue;
271
+
272
+ // Skip excluded paths
273
+ if (this.exclude && this.isPathExcluded(fullPath, this.exclude)) {
274
+ continue;
275
+ }
276
+
277
+ // Skip excluded methods
278
+ if (this.excludeMethods?.map((m) => m.toLowerCase()).includes(route.method.toLowerCase())) {
279
+ continue;
280
+ }
281
+
282
+ // Convert Hono :param syntax to OpenAPI {param} syntax
283
+ fullPath = this.convertHonoPathToOpenAPI(fullPath);
284
+
285
+ const pathItem = paths[fullPath] ?? {};
286
+
287
+ const apiDoc = route.controllerClass
288
+ ? (Reflect.getMetadata(
289
+ 'api:method:apiDoc',
290
+ route.controllerClass.prototype,
291
+ route.handler,
292
+ ) as ApiDocOptions | undefined)
293
+ : undefined;
294
+
295
+ const controllerTag = controllerTags.get(route.controller);
296
+
297
+ // Skip excluded tags
298
+ const tagName = apiDoc?.tags ?? (controllerTag ? [controllerTag.name] : ['Default']);
299
+ if (this.excludeTags && tagName.some((tag: string) => this.excludeTags!.includes(tag))) {
300
+ continue;
301
+ }
302
+
303
+ const operation: any = {
304
+ ...(apiDoc?.summary && { summary: apiDoc.summary }),
305
+ ...(apiDoc?.description && { description: apiDoc.description }),
306
+ tags: tagName,
307
+ // Add security requirements
308
+ ...(apiDoc?.security
309
+ ? { security: apiDoc.security }
310
+ : this.defaultSecurity
311
+ ? { security: this.defaultSecurity }
312
+ : this.securitySchemes
313
+ ? { security: Object.keys(this.securitySchemes).map((n) => ({ [n]: [] })) }
314
+ : {}),
315
+ };
316
+
317
+ // Handle parameters (path params from Zod schema + query params)
318
+ const parameters: any[] = [];
319
+
320
+ // Add path parameters from apiDoc.request.params
321
+ if (apiDoc?.request?.params) {
322
+ const paramsSchema = this.toOpenAPISchema(apiDoc.request.params);
323
+ if (paramsSchema.properties) {
324
+ for (const [name, schema] of Object.entries(
325
+ paramsSchema.properties as Record<string, any>,
326
+ )) {
327
+ parameters.push({
328
+ name,
329
+ in: 'path',
330
+ required: true,
331
+ schema,
332
+ });
333
+ }
334
+ }
335
+ }
336
+
337
+ // Add query parameters from apiDoc.request.query
338
+ if (apiDoc?.request?.query) {
339
+ const querySchema = this.toOpenAPISchema(apiDoc.request.query);
340
+ if (querySchema.properties) {
341
+ for (const [name, schema] of Object.entries(
342
+ querySchema.properties as Record<string, any>,
343
+ )) {
344
+ parameters.push({
345
+ name,
346
+ in: 'query',
347
+ required: querySchema.required?.includes(name) ?? false,
348
+ schema,
349
+ });
350
+ }
351
+ }
352
+ }
353
+
354
+ if (parameters.length > 0) {
355
+ operation.parameters = parameters;
356
+ }
357
+
358
+ if (apiDoc?.request?.body) {
359
+ operation.requestBody = {
360
+ required: true,
361
+ content: {
362
+ 'application/json': {
363
+ schema: this.toOpenAPISchema(apiDoc.request.body),
364
+ },
365
+ },
366
+ };
367
+ }
368
+
369
+ if (apiDoc?.responses) {
370
+ operation.responses = {};
371
+ for (const [statusStr, response] of Object.entries(apiDoc.responses)) {
372
+ const status = parseInt(statusStr, 10);
373
+ operation.responses[status] = {
374
+ description: response.description ?? 'Success',
375
+ ...(response.schema && {
376
+ content: {
377
+ 'application/json': {
378
+ schema: this.toOpenAPISchema(response.schema),
379
+ },
380
+ },
381
+ }),
382
+ };
383
+ }
384
+ }
385
+
386
+ pathItem[route.method] = operation;
387
+ paths[fullPath] = pathItem;
388
+ }
389
+
390
+ return {
391
+ openapi: '3.0.0',
392
+ info: {
393
+ title: this.uiTitle,
394
+ version: '1.0.0',
395
+ description: 'API Documentation',
396
+ },
397
+ paths,
398
+ ...(tags.length > 0 && { tags }),
399
+ components: {
400
+ securitySchemes: this.securitySchemes,
401
+ },
402
+ };
403
+ }
404
+
405
+ private convertHonoPathToOpenAPI(honoPath: string): string {
406
+ // Convert Hono :param syntax to OpenAPI {param} syntax
407
+ return honoPath.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
408
+ }
409
+
410
+ private isPathExcluded(path: string, excludePatterns: string[]): boolean {
411
+ return excludePatterns.some((pattern) => {
412
+ // Convert pattern to regex (support wildcard *)
413
+ const regex = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`);
414
+ return regex.test(path);
415
+ });
416
+ }
417
+
418
+ private toOpenAPISchema(schema: any): any {
419
+ if (!schema) return { type: 'object' };
420
+ if (schema._def) {
421
+ return this.zodToOpenAPI(schema);
422
+ }
423
+ if (schema.type || schema.properties) {
424
+ return schema;
425
+ }
426
+ return { type: 'object' };
427
+ }
428
+
429
+ private zodToOpenAPI(schema: any): any {
430
+ if (!schema?._def) return { type: 'object' };
431
+
432
+ const def = schema._def;
433
+
434
+ // Handle Zod 4.x new structure (type, innerType, etc.)
435
+ // Zod 4.x uses def.type property instead of typeName
436
+ const zodType = def.type || def.typeName;
437
+
438
+ switch (zodType) {
439
+ case 'string':
440
+ case 'ZodString': {
441
+ const stringMeta: any = { type: 'string' };
442
+ // Zod 3.x: checks in def.checks
443
+ if (def.checks) {
444
+ for (const check of def.checks) {
445
+ if (check.kind === 'min') stringMeta.minLength = check.value;
446
+ if (check.kind === 'max') stringMeta.maxLength = check.value;
447
+ if (check.kind === 'regex') stringMeta.pattern = check.regex.source;
448
+ }
449
+ }
450
+ // Zod 4.x: instance properties
451
+ if (schema.minLength != null) stringMeta.minLength = schema.minLength;
452
+ if (schema.maxLength != null) stringMeta.maxLength = schema.maxLength;
453
+ return stringMeta;
454
+ }
455
+
456
+ case 'number':
457
+ case 'ZodNumber': {
458
+ const numberMeta: any = { type: 'number' };
459
+ // Zod 3.x: checks in def.checks
460
+ if (def.checks) {
461
+ for (const check of def.checks) {
462
+ if (check.kind === 'min') numberMeta.minimum = check.value;
463
+ if (check.kind === 'max') numberMeta.maximum = check.value;
464
+ }
465
+ }
466
+ // Zod 4.x: instance properties
467
+ if (schema.minValue != null && Number.isFinite(schema.minValue))
468
+ numberMeta.minimum = schema.minValue;
469
+ if (schema.maxValue != null && Number.isFinite(schema.maxValue))
470
+ numberMeta.maximum = schema.maxValue;
471
+ return numberMeta;
472
+ }
473
+
474
+ case 'boolean':
475
+ case 'ZodBoolean':
476
+ return { type: 'boolean' };
477
+
478
+ case 'array':
479
+ case 'ZodArray': {
480
+ // Zod 4.x: def.element, Zod 3.x: def.type (the schema, not a string)
481
+ const elementSchema =
482
+ def.element ||
483
+ (typeof def.type !== 'string' ? def.type : null) ||
484
+ (def.typeName ? def : null);
485
+ return {
486
+ type: 'array',
487
+ items: elementSchema ? this.zodToOpenAPI(elementSchema) : { type: 'object' },
488
+ };
489
+ }
490
+
491
+ case 'object':
492
+ case 'ZodObject': {
493
+ const properties: Record<string, any> = {};
494
+ const required: string[] = [];
495
+
496
+ // Zod 4.x: def.shape() or def.shape
497
+ // Zod 3.x: def.shape()
498
+ const shape = typeof def.shape === 'function' ? def.shape() : def.shape || {};
499
+
500
+ for (const [key, value] of Object.entries(shape as Record<string, any>)) {
501
+ const field = value as any;
502
+ properties[key] = this.zodToOpenAPI(field);
503
+
504
+ // Check if field is required (not optional)
505
+ const fieldDef = field?._def;
506
+ const fieldType = fieldDef?.type || fieldDef?.typeName;
507
+ if (fieldType !== 'optional' && fieldType !== 'ZodOptional') {
508
+ required.push(key);
509
+ }
510
+ }
511
+
512
+ return {
513
+ type: 'object',
514
+ properties,
515
+ ...(required.length > 0 && { required }),
516
+ };
517
+ }
518
+
519
+ case 'optional':
520
+ case 'ZodOptional':
521
+ // Zod 4.x: def.innerType, Zod 3.x: def.innerType or def.type
522
+ return this.zodToOpenAPI(def.innerType ?? def.type);
523
+
524
+ case 'enum':
525
+ case 'ZodEnum':
526
+ return {
527
+ type: 'string',
528
+ enum: def.values || (def.entries ? Object.values(def.entries) : []),
529
+ };
530
+
531
+ case 'null':
532
+ case 'ZodNull':
533
+ return { type: 'null' };
534
+
535
+ case 'union':
536
+ case 'ZodUnion':
537
+ if (def.options) {
538
+ return {
539
+ oneOf: def.options.map((opt: any) => this.zodToOpenAPI(opt)),
540
+ };
541
+ }
542
+ return { type: 'object' };
543
+
544
+ default:
545
+ return { type: 'object' };
546
+ }
547
+ }
548
+
549
+ private async runHook(c: Context): Promise<void> {
550
+ if (this.onRequest) {
551
+ await this.onRequest(c);
552
+ }
553
+ }
554
+ }
@@ -0,0 +1,5 @@
1
+ export { ApiDocPlugin } from './api-doc.plugin';
2
+ export type { ApiDocPluginOptions } from './api-doc.plugin';
3
+ export type { IDocRenderer } from './interfaces/renderer.interface';
4
+ export { ScalarRenderer } from './renderers/scalar.renderer';
5
+ export { SwaggerRenderer } from './renderers/swagger.renderer';
@@ -0,0 +1,5 @@
1
+ export interface IDocRenderer {
2
+ readonly name: string;
3
+
4
+ renderHtml(config: { specUrl: string; uiTitle: string }): string;
5
+ }