@velajs/vela 1.8.3 → 1.8.5

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.
@@ -24,13 +24,20 @@ export declare class VelaApplication {
24
24
  useGlobalInterceptors(...interceptors: InterceptorType[]): this;
25
25
  useGlobalFilters(...filters: FilterType[]): this;
26
26
  /**
27
- * Serve a pre-built OpenAPI document (and optionally a Scalar UI) on
28
- * the underlying Hono app. Edge-safe: the UI HTML loads Scalar from a
29
- * CDN at runtime so nothing is bundled server-side.
27
+ * Serve a pre-built OpenAPI document and one or more interactive doc UIs
28
+ * (Swagger UI, Scalar, ReDoc) on the underlying Hono app, mirroring the
29
+ * hono-crud docs convention. Edge-safe: each UI is a tiny self-contained
30
+ * HTML shell that loads its renderer from a CDN at runtime so nothing is
31
+ * bundled server-side and no extra dependency is required.
32
+ *
33
+ * Defaults: spec at `/openapi.json`, Scalar at `/scalar`, Swagger UI at
34
+ * `/docs`, ReDoc at `/redoc`. The deprecated `path` / `uiPath` aliases are
35
+ * retained for migration from the previous single-UI shape.
30
36
  *
31
37
  * ```ts
32
38
  * const doc = createOpenApiDocument(AppModule);
33
- * app.mountOpenApi({ document: doc, ui: 'scalar' });
39
+ * app.mountOpenApi({ document: doc }); // Scalar @ /scalar
40
+ * app.mountOpenApi({ document: doc, ui: 'all' }); // swagger + scalar + redoc
34
41
  * ```
35
42
  */
36
43
  mountOpenApi(options: MountOpenApiOptions): this;
@@ -1,5 +1,7 @@
1
1
  import { hasBeforeApplicationShutdown, hasOnApplicationBootstrap, hasOnApplicationShutdown, hasOnModuleDestroy, hasOnModuleInit } from "./lifecycle/index.js";
2
2
  import { renderScalarUi } from "./openapi/scalar-ui.js";
3
+ import { renderSwaggerUi } from "./openapi/swagger-ui.js";
4
+ import { renderRedocUi } from "./openapi/redoc-ui.js";
3
5
  export class VelaApplication {
4
6
  container;
5
7
  routeManager;
@@ -54,23 +56,69 @@ export class VelaApplication {
54
56
  return this;
55
57
  }
56
58
  /**
57
- * Serve a pre-built OpenAPI document (and optionally a Scalar UI) on
58
- * the underlying Hono app. Edge-safe: the UI HTML loads Scalar from a
59
- * CDN at runtime so nothing is bundled server-side.
59
+ * Serve a pre-built OpenAPI document and one or more interactive doc UIs
60
+ * (Swagger UI, Scalar, ReDoc) on the underlying Hono app, mirroring the
61
+ * hono-crud docs convention. Edge-safe: each UI is a tiny self-contained
62
+ * HTML shell that loads its renderer from a CDN at runtime so nothing is
63
+ * bundled server-side and no extra dependency is required.
64
+ *
65
+ * Defaults: spec at `/openapi.json`, Scalar at `/scalar`, Swagger UI at
66
+ * `/docs`, ReDoc at `/redoc`. The deprecated `path` / `uiPath` aliases are
67
+ * retained for migration from the previous single-UI shape.
60
68
  *
61
69
  * ```ts
62
70
  * const doc = createOpenApiDocument(AppModule);
63
- * app.mountOpenApi({ document: doc, ui: 'scalar' });
71
+ * app.mountOpenApi({ document: doc }); // Scalar @ /scalar
72
+ * app.mountOpenApi({ document: doc, ui: 'all' }); // swagger + scalar + redoc
64
73
  * ```
65
74
  */ mountOpenApi(options) {
66
75
  const app = this.getApp();
67
- const jsonPath = options.path ?? '/docs.json';
68
76
  const doc = options.document;
69
- app.get(jsonPath, (c)=>c.json(doc));
70
- if (options.ui === 'scalar') {
71
- const uiPath = options.uiPath ?? '/docs';
72
- const html = renderScalarUi(jsonPath);
73
- app.get(uiPath, (c)=>c.html(html));
77
+ const title = options.title;
78
+ // Spec JSON: new default `/openapi.json`. The deprecated `path` alias is
79
+ // honored as an override only (does not change the default).
80
+ const specPath = options.specPath ?? options.path ?? '/openapi.json';
81
+ app.get(specPath, (c)=>c.json(doc));
82
+ // Normalize the requested UI set. Default is Scalar only.
83
+ const uiOption = options.ui;
84
+ let uis;
85
+ let singleStringUi = false;
86
+ if (uiOption === undefined) {
87
+ uis = [
88
+ 'scalar'
89
+ ];
90
+ } else if (uiOption === 'all') {
91
+ uis = [
92
+ 'swagger',
93
+ 'scalar',
94
+ 'redoc'
95
+ ];
96
+ } else if (Array.isArray(uiOption)) {
97
+ uis = uiOption;
98
+ } else {
99
+ uis = [
100
+ uiOption
101
+ ];
102
+ singleStringUi = true;
103
+ }
104
+ for (const ui of uis){
105
+ // Back-compat: the old `{ ui: 'scalar', uiPath }` form mapped a single
106
+ // string UI to `uiPath`. Honor that only when exactly one UI was
107
+ // requested as a string.
108
+ const legacyPath = singleStringUi && uis.length === 1 ? options.uiPath : undefined;
109
+ if (ui === 'swagger') {
110
+ const path = legacyPath ?? options.swaggerPath ?? '/docs';
111
+ const html = renderSwaggerUi(specPath, title);
112
+ app.get(path, (c)=>c.html(html));
113
+ } else if (ui === 'redoc') {
114
+ const path = legacyPath ?? options.redocPath ?? '/redoc';
115
+ const html = renderRedocUi(specPath, title);
116
+ app.get(path, (c)=>c.html(html));
117
+ } else {
118
+ const path = legacyPath ?? options.scalarPath ?? '/scalar';
119
+ const html = renderScalarUi(specPath, title);
120
+ app.get(path, (c)=>c.html(html));
121
+ }
74
122
  }
75
123
  return this;
76
124
  }
@@ -241,6 +241,34 @@ export function createOpenApiDocument(rootModule, options = {}) {
241
241
  }
242
242
  }
243
243
  }
244
+ // Aggregate the document-root `tags` array. NestJS declares tag
245
+ // groups/descriptions/order via `DocumentBuilder().addTag(name, desc)`
246
+ // while its scanner auto-collects operation tags; we wire both halves
247
+ // here. Walk every operation's `tags` in first-seen order (stable: paths
248
+ // insertion order, then verb order), then merge with `options.tags`
249
+ // (caller-declared tags first — preserving their order + descriptions —
250
+ // followed by any purely-collected tag not already declared, as `{ name }`).
251
+ const seenTagNames = new Set();
252
+ const collectedTagNames = [];
253
+ for (const pathItem of Object.values(paths)){
254
+ for (const verb of VERB_WHITELIST){
255
+ const operation = pathItem[verb];
256
+ if (!operation?.tags) continue;
257
+ for (const tagName of operation.tags){
258
+ if (seenTagNames.has(tagName)) continue;
259
+ seenTagNames.add(tagName);
260
+ collectedTagNames.push(tagName);
261
+ }
262
+ }
263
+ }
264
+ const declaredTags = options.tags ?? [];
265
+ const declaredTagNames = new Set(declaredTags.map((t)=>t.name));
266
+ const tags = [
267
+ ...declaredTags,
268
+ ...collectedTagNames.filter((name)=>!declaredTagNames.has(name)).map((name)=>({
269
+ name
270
+ }))
271
+ ];
244
272
  const document = {
245
273
  openapi: '3.1.0',
246
274
  info: {
@@ -258,5 +286,11 @@ export function createOpenApiDocument(rootModule, options = {}) {
258
286
  schemas
259
287
  };
260
288
  }
289
+ // Only attach `tags` when non-empty. Emitting `tags: []` on a tag-less
290
+ // spec is the exact symptom the consumer reported, so omit the key
291
+ // entirely in that case (mirrors the `components` handling above).
292
+ if (tags.length > 0) {
293
+ document.tags = tags;
294
+ }
261
295
  return document;
262
296
  }
@@ -2,4 +2,6 @@ export { createOpenApiDocument } from './document';
2
2
  export { ApiDoc, ApiTags, ApiResponse, API_DOC_METADATA, API_TAGS_METADATA, API_RESPONSES_METADATA, } from './decorators';
3
3
  export { zodToJsonSchema } from './zod-to-json-schema';
4
4
  export { renderScalarUi } from './scalar-ui';
5
- export type { ApiDocMetadata, ApiResponseEntry, ApiResponseOptions, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, MountOpenApiOptions, OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, } from './types';
5
+ export { renderSwaggerUi } from './swagger-ui';
6
+ export { renderRedocUi } from './redoc-ui';
7
+ export type { ApiDocMetadata, ApiResponseEntry, ApiResponseOptions, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, MountOpenApiOptions, OpenApiUi, OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, } from './types';
@@ -2,3 +2,5 @@ export { createOpenApiDocument } from "./document.js";
2
2
  export { ApiDoc, ApiTags, ApiResponse, API_DOC_METADATA, API_TAGS_METADATA, API_RESPONSES_METADATA } from "./decorators.js";
3
3
  export { zodToJsonSchema } from "./zod-to-json-schema.js";
4
4
  export { renderScalarUi } from "./scalar-ui.js";
5
+ export { renderSwaggerUi } from "./swagger-ui.js";
6
+ export { renderRedocUi } from "./redoc-ui.js";
@@ -0,0 +1 @@
1
+ export declare function renderRedocUi(specUrl: string, title?: string): string;
@@ -0,0 +1,19 @@
1
+ // Minimal HTML shell that bootstraps ReDoc from a CDN. Served as a static
2
+ // string so it works on every edge runtime with no Node or bundler deps —
3
+ // ReDoc itself is loaded from a CDN at runtime.
4
+ export function renderRedocUi(specUrl, title) {
5
+ const safeUrl = specUrl.replace(/"/g, '"');
6
+ const safeTitle = (title ?? 'API Reference').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
7
+ return `<!doctype html>
8
+ <html>
9
+ <head>
10
+ <title>${safeTitle}</title>
11
+ <meta charset="utf-8" />
12
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
13
+ </head>
14
+ <body>
15
+ <redoc spec-url="${safeUrl}"></redoc>
16
+ <script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"></script>
17
+ </body>
18
+ </html>`;
19
+ }
@@ -1 +1 @@
1
- export declare function renderScalarUi(jsonUrl: string): string;
1
+ export declare function renderScalarUi(jsonUrl: string, title?: string): string;
@@ -1,12 +1,13 @@
1
1
  // Minimal HTML shell that bootstraps Scalar's hosted API-reference UI.
2
2
  // Served as a static string so it works on every edge runtime with no
3
3
  // Node or bundler deps. Scalar itself is loaded from a CDN at runtime.
4
- export function renderScalarUi(jsonUrl) {
4
+ export function renderScalarUi(jsonUrl, title) {
5
5
  const safeUrl = jsonUrl.replace(/"/g, '&quot;');
6
+ const safeTitle = (title ?? 'API Reference').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
6
7
  return `<!doctype html>
7
8
  <html>
8
9
  <head>
9
- <title>API Reference</title>
10
+ <title>${safeTitle}</title>
10
11
  <meta charset="utf-8" />
11
12
  <meta name="viewport" content="width=device-width, initial-scale=1" />
12
13
  </head>
@@ -0,0 +1 @@
1
+ export declare function renderSwaggerUi(specUrl: string, title?: string): string;
@@ -0,0 +1,23 @@
1
+ // Minimal HTML shell that bootstraps Swagger UI from a CDN. Served as a
2
+ // static string so it works on every edge runtime with no Node or bundler
3
+ // deps — Swagger UI itself is loaded from a CDN at runtime.
4
+ export function renderSwaggerUi(specUrl, title) {
5
+ const safeUrl = specUrl.replace(/"/g, '&quot;');
6
+ const safeTitle = (title ?? 'API Reference').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
7
+ return `<!doctype html>
8
+ <html>
9
+ <head>
10
+ <title>${safeTitle}</title>
11
+ <meta charset="utf-8" />
12
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui.css" />
14
+ </head>
15
+ <body>
16
+ <div id="swagger-ui"></div>
17
+ <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-bundle.js"></script>
18
+ <script>
19
+ window.ui = SwaggerUIBundle({ url: "${safeUrl}", dom_id: '#swagger-ui' });
20
+ </script>
21
+ </body>
22
+ </html>`;
23
+ }
@@ -90,14 +90,39 @@ export interface ApiResponseEntry extends ApiResponseOptions {
90
90
  export interface CreateOpenApiDocumentOptions {
91
91
  info?: Partial<OpenApiInfo>;
92
92
  globalPrefix?: string;
93
+ /**
94
+ * Declare top-level tag groups with descriptions and an explicit order
95
+ * (mirrors NestJS `DocumentBuilder().addTag(name, description)`). Tags
96
+ * actually used by operations but NOT declared here are still emitted —
97
+ * appended after the declared ones in first-seen order. Declared tags
98
+ * with no operations are kept (lets you pre-declare ordering/description).
99
+ */
100
+ tags?: Array<{
101
+ name: string;
102
+ description?: string;
103
+ }>;
93
104
  }
105
+ export type OpenApiUi = 'swagger' | 'scalar' | 'redoc';
94
106
  export interface MountOpenApiOptions {
95
107
  /** Pre-built OpenAPI document to serve. */
96
108
  document: OpenApiDocument;
97
- /** Path for the JSON endpoint. Default `/docs.json`. */
109
+ /** OpenAPI JSON spec path. @default '/openapi.json' */
110
+ specPath?: string;
111
+ /**
112
+ * UI(s) to mount. 'all' mounts swagger + scalar + redoc.
113
+ * @default 'scalar'
114
+ */
115
+ ui?: OpenApiUi | OpenApiUi[] | 'all';
116
+ /** Swagger UI path. @default '/docs' */
117
+ swaggerPath?: string;
118
+ /** Scalar path. @default '/scalar' */
119
+ scalarPath?: string;
120
+ /** ReDoc path. @default '/redoc' */
121
+ redocPath?: string;
122
+ /** Page title for the UIs. */
123
+ title?: string;
124
+ /** @deprecated use `specPath`. Back-compat alias; if set, overrides specPath default. */
98
125
  path?: string;
99
- /** Opt-in UI renderer. Only `scalar` is bundled today. */
100
- ui?: 'scalar';
101
- /** Path the UI is served at when `ui` is set. Default `/docs`. */
126
+ /** @deprecated single-UI path override; applies to the single `ui` when a string. */
102
127
  uiPath?: string;
103
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",