@velajs/vela 0.9.0 → 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.
Files changed (41) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/application.d.ts +12 -0
  3. package/dist/application.js +22 -0
  4. package/dist/constants.d.ts +29 -26
  5. package/dist/constants.js +26 -29
  6. package/dist/http/middleware-consumer.d.ts +11 -10
  7. package/dist/http/middleware-consumer.js +10 -11
  8. package/dist/http/route.manager.js +1 -6
  9. package/dist/index.d.ts +4 -4
  10. package/dist/index.js +2 -2
  11. package/dist/openapi/decorators.d.ts +15 -1
  12. package/dist/openapi/decorators.js +24 -0
  13. package/dist/openapi/document.js +99 -18
  14. package/dist/openapi/index.d.ts +3 -2
  15. package/dist/openapi/index.js +2 -1
  16. package/dist/openapi/scalar-ui.d.ts +1 -0
  17. package/dist/openapi/scalar-ui.js +18 -0
  18. package/dist/openapi/types.d.ts +18 -0
  19. package/dist/pipeline/component.manager.d.ts +1 -0
  20. package/dist/pipeline/component.manager.js +10 -34
  21. package/dist/registry/metadata.registry.d.ts +2 -2
  22. package/dist/registry/metadata.registry.js +13 -6
  23. package/dist/schedule/cron-matcher.d.ts +2 -0
  24. package/dist/schedule/cron-matcher.js +62 -0
  25. package/dist/schedule/index.d.ts +4 -3
  26. package/dist/schedule/index.js +2 -2
  27. package/dist/schedule/schedule.module.d.ts +2 -6
  28. package/dist/schedule/schedule.module.js +4 -42
  29. package/dist/schedule/schedule.tokens.d.ts +0 -3
  30. package/dist/schedule/schedule.tokens.js +0 -2
  31. package/dist/schedule/schedule.types.d.ts +0 -3
  32. package/dist/schedule-node/index.d.ts +2 -0
  33. package/dist/schedule-node/index.js +2 -0
  34. package/dist/schedule-node/schedule-node.module.d.ts +4 -0
  35. package/dist/schedule-node/schedule-node.module.js +18 -0
  36. package/dist/{schedule → schedule-node}/schedule.executor.d.ts +2 -5
  37. package/dist/schedule-node/schedule.executor.js +91 -0
  38. package/dist/services/logger.d.ts +9 -8
  39. package/dist/services/logger.js +14 -15
  40. package/package.json +5 -1
  41. package/dist/schedule/schedule.executor.js +0 -169
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 (2026-04-28)
4
+
5
+ Edge-runtime audit and AI-drift cleanup.
6
+
7
+ ### Breaking changes
8
+
9
+ - **Schedule module split.** `ScheduleExecutor`, `SCHEDULE_MODULE_OPTIONS`, and `ScheduleModuleOptions` are no longer exported from `@velajs/vela`. The `setInterval`-based timer executor moved to a new opt-in sub-export at `@velajs/vela/schedule-node`. Consumers on Node or Bun should now do:
10
+
11
+ ```ts
12
+ import { ScheduleNodeModule } from '@velajs/vela/schedule-node';
13
+
14
+ @Module({ imports: [ScheduleNodeModule.forRoot()], providers: [JobsService] })
15
+ class AppModule {}
16
+ ```
17
+
18
+ `ScheduleModule.forRoot()` is now metadata-only — `enableTimers` is no longer accepted (drop the option entirely). `ScheduleModule.forRootAsync()` was removed (no options to async-resolve).
19
+
20
+ Edge runtimes without `setInterval` (Cloudflare Workers, etc.) should continue to use platform cron triggers — `@velajs/cloudflare` ≥ 0.2.0 dispatches core `@Cron` jobs via its `scheduled()` handler.
21
+
22
+ - **TypeScript enums replaced with `as const` objects** for `HttpMethod`, `ParamType`, `Scope`, `RequestMethod`, and `LogLevel`. Value access (`HttpMethod.GET`) keeps working; type-position usages (`: HttpMethod`) keep working via same-name type aliases. Code that imported the enum *type* with structural assumptions about enum runtime shape may need adjustment.
23
+
24
+ ### Fixes
25
+
26
+ - `@Head()` is now HEAD-only. Previously it registered as a plain GET handler, so `GET` requests would also hit `@Head()`-decorated methods.
27
+ - Handler-level guards / pipes / interceptors / filters now key off the controller constructor instead of `${className}:${method}`, fixing a metadata collision when two controllers shared a class name (across feature modules or after minification).
28
+ - Removed an obsolete narration comment in `route.manager.ts`.
29
+
30
+ ### New
31
+
32
+ - `@velajs/vela/schedule-node` — opt-in entry for Node/Bun cron and interval execution.
33
+ - `parseCron(expression)` and `CronMatcher` exported from the core for use by platform cron adapters.
34
+ - Edge-runtime audit test (`src/__tests__/edge-runtime-audit.test.ts`) — fails CI if any file under `src/` (excluding `schedule-node/`) references forbidden APIs (`node:*`, `Buffer`, `process.*`, `__dirname/__filename`, `fs/path/os/child_process`, `setInterval`, `Bun.serve`).
35
+
3
36
  ## 0.1.0 (2026-02-19)
4
37
 
5
38
  Initial release.
@@ -2,6 +2,7 @@ import type { Hono } from 'hono';
2
2
  import type { Container } from './container/container';
3
3
  import type { Token } from './container/types';
4
4
  import type { RouteManager } from './http/route.manager';
5
+ import type { MountOpenApiOptions } from './openapi/types';
5
6
  import type { FilterType, GuardType, InterceptorType, PipeType } from './registry/types';
6
7
  export declare class VelaApplication {
7
8
  private readonly container;
@@ -22,6 +23,17 @@ export declare class VelaApplication {
22
23
  useGlobalGuards(...guards: GuardType[]): this;
23
24
  useGlobalInterceptors(...interceptors: InterceptorType[]): this;
24
25
  useGlobalFilters(...filters: FilterType[]): this;
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.
30
+ *
31
+ * ```ts
32
+ * const doc = createOpenApiDocument(AppModule);
33
+ * app.mountOpenApi({ document: doc, ui: 'scalar' });
34
+ * ```
35
+ */
36
+ mountOpenApi(options: MountOpenApiOptions): this;
25
37
  callOnModuleInit(): Promise<void>;
26
38
  callOnApplicationBootstrap(): Promise<void>;
27
39
  close(signal?: string): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { hasBeforeApplicationShutdown, hasOnApplicationBootstrap, hasOnApplicationShutdown, hasOnModuleDestroy, hasOnModuleInit } from "./lifecycle/index.js";
2
+ import { renderScalarUi } from "./openapi/scalar-ui.js";
2
3
  export class VelaApplication {
3
4
  container;
4
5
  routeManager;
@@ -52,6 +53,27 @@ export class VelaApplication {
52
53
  this.routeManager.useGlobalFilters(...filters);
53
54
  return this;
54
55
  }
56
+ /**
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.
60
+ *
61
+ * ```ts
62
+ * const doc = createOpenApiDocument(AppModule);
63
+ * app.mountOpenApi({ document: doc, ui: 'scalar' });
64
+ * ```
65
+ */ mountOpenApi(options) {
66
+ const app = this.getApp();
67
+ const jsonPath = options.path ?? '/docs.json';
68
+ 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));
74
+ }
75
+ return this;
76
+ }
55
77
  // Lifecycle hooks
56
78
  async callOnModuleInit() {
57
79
  for (const instance of this.instances){
@@ -13,29 +13,32 @@ export declare const METADATA_KEYS: {
13
13
  readonly CATCH: "vela:catch";
14
14
  readonly CRUD: "vela:crud";
15
15
  };
16
- export declare enum HttpMethod {
17
- GET = "get",
18
- POST = "post",
19
- PUT = "put",
20
- PATCH = "patch",
21
- DELETE = "delete",
22
- OPTIONS = "options",
23
- HEAD = "head",
24
- ALL = "all"
25
- }
26
- export declare enum ParamType {
27
- BODY = "body",
28
- QUERY = "query",
29
- PARAM = "param",
30
- HEADERS = "headers",
31
- REQUEST = "request",
32
- RESPONSE = "response",
33
- IP = "ip",
34
- COOKIE = "cookie",
35
- RAW_BODY = "raw_body"
36
- }
37
- export declare enum Scope {
38
- SINGLETON = "singleton",
39
- TRANSIENT = "transient",
40
- REQUEST = "request"
41
- }
16
+ export declare const HttpMethod: {
17
+ readonly GET: "get";
18
+ readonly POST: "post";
19
+ readonly PUT: "put";
20
+ readonly PATCH: "patch";
21
+ readonly DELETE: "delete";
22
+ readonly OPTIONS: "options";
23
+ readonly HEAD: "head";
24
+ readonly ALL: "all";
25
+ };
26
+ export type HttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod];
27
+ export declare const ParamType: {
28
+ readonly BODY: "body";
29
+ readonly QUERY: "query";
30
+ readonly PARAM: "param";
31
+ readonly HEADERS: "headers";
32
+ readonly REQUEST: "request";
33
+ readonly RESPONSE: "response";
34
+ readonly IP: "ip";
35
+ readonly COOKIE: "cookie";
36
+ readonly RAW_BODY: "raw_body";
37
+ };
38
+ export type ParamType = (typeof ParamType)[keyof typeof ParamType];
39
+ export declare const Scope: {
40
+ readonly SINGLETON: "singleton";
41
+ readonly TRANSIENT: "transient";
42
+ readonly REQUEST: "request";
43
+ };
44
+ export type Scope = (typeof Scope)[keyof typeof Scope];
package/dist/constants.js CHANGED
@@ -18,32 +18,29 @@ export const METADATA_KEYS = {
18
18
  // CRUD
19
19
  CRUD: 'vela:crud'
20
20
  };
21
- export var HttpMethod = /*#__PURE__*/ function(HttpMethod) {
22
- HttpMethod["GET"] = "get";
23
- HttpMethod["POST"] = "post";
24
- HttpMethod["PUT"] = "put";
25
- HttpMethod["PATCH"] = "patch";
26
- HttpMethod["DELETE"] = "delete";
27
- HttpMethod["OPTIONS"] = "options";
28
- HttpMethod["HEAD"] = "head";
29
- HttpMethod["ALL"] = "all";
30
- return HttpMethod;
31
- }({});
32
- export var ParamType = /*#__PURE__*/ function(ParamType) {
33
- ParamType["BODY"] = "body";
34
- ParamType["QUERY"] = "query";
35
- ParamType["PARAM"] = "param";
36
- ParamType["HEADERS"] = "headers";
37
- ParamType["REQUEST"] = "request";
38
- ParamType["RESPONSE"] = "response";
39
- ParamType["IP"] = "ip";
40
- ParamType["COOKIE"] = "cookie";
41
- ParamType["RAW_BODY"] = "raw_body";
42
- return ParamType;
43
- }({});
44
- export var Scope = /*#__PURE__*/ function(Scope) {
45
- Scope["SINGLETON"] = "singleton";
46
- Scope["TRANSIENT"] = "transient";
47
- Scope["REQUEST"] = "request";
48
- return Scope;
49
- }({});
21
+ export const HttpMethod = {
22
+ GET: 'get',
23
+ POST: 'post',
24
+ PUT: 'put',
25
+ PATCH: 'patch',
26
+ DELETE: 'delete',
27
+ OPTIONS: 'options',
28
+ HEAD: 'head',
29
+ ALL: 'all'
30
+ };
31
+ export const ParamType = {
32
+ BODY: 'body',
33
+ QUERY: 'query',
34
+ PARAM: 'param',
35
+ HEADERS: 'headers',
36
+ REQUEST: 'request',
37
+ RESPONSE: 'response',
38
+ IP: 'ip',
39
+ COOKIE: 'cookie',
40
+ RAW_BODY: 'raw_body'
41
+ };
42
+ export const Scope = {
43
+ SINGLETON: 'singleton',
44
+ TRANSIENT: 'transient',
45
+ REQUEST: 'request'
46
+ };
@@ -1,15 +1,16 @@
1
1
  import type { NestMiddleware } from '../pipeline/types';
2
2
  import type { Type } from '../registry/types';
3
- export declare enum RequestMethod {
4
- GET = "GET",
5
- POST = "POST",
6
- PUT = "PUT",
7
- DELETE = "DELETE",
8
- PATCH = "PATCH",
9
- OPTIONS = "OPTIONS",
10
- HEAD = "HEAD",
11
- ALL = "ALL"
12
- }
3
+ export declare const RequestMethod: {
4
+ readonly GET: "GET";
5
+ readonly POST: "POST";
6
+ readonly PUT: "PUT";
7
+ readonly DELETE: "DELETE";
8
+ readonly PATCH: "PATCH";
9
+ readonly OPTIONS: "OPTIONS";
10
+ readonly HEAD: "HEAD";
11
+ readonly ALL: "ALL";
12
+ };
13
+ export type RequestMethod = (typeof RequestMethod)[keyof typeof RequestMethod];
13
14
  export interface RouteInfo {
14
15
  path: string;
15
16
  method?: RequestMethod;
@@ -1,15 +1,14 @@
1
1
  import { MetadataRegistry } from "../registry/metadata.registry.js";
2
- export var RequestMethod = /*#__PURE__*/ function(RequestMethod) {
3
- RequestMethod["GET"] = "GET";
4
- RequestMethod["POST"] = "POST";
5
- RequestMethod["PUT"] = "PUT";
6
- RequestMethod["DELETE"] = "DELETE";
7
- RequestMethod["PATCH"] = "PATCH";
8
- RequestMethod["OPTIONS"] = "OPTIONS";
9
- RequestMethod["HEAD"] = "HEAD";
10
- RequestMethod["ALL"] = "ALL";
11
- return RequestMethod;
12
- }({});
2
+ export const RequestMethod = {
3
+ GET: 'GET',
4
+ POST: 'POST',
5
+ PUT: 'PUT',
6
+ DELETE: 'DELETE',
7
+ PATCH: 'PATCH',
8
+ OPTIONS: 'OPTIONS',
9
+ HEAD: 'HEAD',
10
+ ALL: 'ALL'
11
+ };
13
12
  export class MiddlewareBuilder {
14
13
  definitions = [];
15
14
  apply(...middleware) {
@@ -50,7 +50,7 @@ export class RouteManager {
50
50
  ],
51
51
  [
52
52
  HttpMethod.HEAD,
53
- (app, p, h)=>app.get(p, h)
53
+ (app, p, h)=>app.get(p, (c, next)=>c.req.method === 'HEAD' ? h(c) : next())
54
54
  ],
55
55
  [
56
56
  HttpMethod.ALL,
@@ -175,11 +175,6 @@ export class RouteManager {
175
175
  instantiateMany(items, container) {
176
176
  return items.map((item)=>this.instantiate(item, container));
177
177
  }
178
- // Resolve middleware priority at build time. Supports three sources:
179
- // 1. static priority on the class constructor
180
- // 2. priority property on the instance
181
- // 3. for container tokens, resolve and inspect the instance (+ its ctor)
182
- // Falls back to 0 on any error or when no priority is set.
183
178
  getMiddlewarePriority(entry) {
184
179
  if (entry == null) return 0;
185
180
  if (typeof entry === 'function') {
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ import './metadata';
2
2
  export { defineMetadata, getMetadata } from './metadata';
3
3
  export { VelaFactory, createApplication } from './factory';
4
4
  export { VelaApplication } from './application';
5
- export { createOpenApiDocument, ApiDoc, ApiTags, zodToJsonSchema } from './openapi/index';
6
- export type { OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, ApiDocMetadata, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, } from './openapi/index';
5
+ export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema, } from './openapi/index';
6
+ export type { OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, ApiDocMetadata, ApiResponseEntry, ApiResponseOptions, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, } from './openapi/index';
7
7
  export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from './container/index';
8
8
  export type { Type, Token, InjectableOptions, ProviderOptions, } from './container/index';
9
9
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from './constants';
@@ -20,8 +20,8 @@ export { CacheModule, CacheService, CacheInterceptor, MemoryCacheStore, CacheKey
20
20
  export type { CacheModuleOptions, CacheStore, CacheEntry } from './cache/index';
21
21
  export { EventEmitterModule, EventEmitter, EventEmitterSubscriber, OnEvent, ON_EVENT_METADATA, } from './event-emitter/index';
22
22
  export type { EventHandler, OnEventMetadata } from './event-emitter/index';
23
- export { ScheduleModule, ScheduleRegistry, ScheduleExecutor, Cron, Interval, SCHEDULE_MODULE_OPTIONS, CRON_METADATA, INTERVAL_METADATA, } from './schedule/index';
24
- export type { RegisteredCronJob, RegisteredIntervalJob, CronMetadata, IntervalMetadata, ScheduleModuleOptions, } from './schedule/index';
23
+ export { ScheduleModule, ScheduleRegistry, Cron, Interval, parseCron, CRON_METADATA, INTERVAL_METADATA, } from './schedule/index';
24
+ export type { RegisteredCronJob, RegisteredIntervalJob, CronMetadata, IntervalMetadata, CronMatcher, } from './schedule/index';
25
25
  export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthIndicator, } from './health/index';
26
26
  export type { HealthCheckResult, HealthCheckStatus, HealthIndicatorResult, HealthIndicatorFunction, ResponseCheckCallback, HttpPingOptions, } from './health/index';
27
27
  export { ThrottlerModule, ThrottlerGuard, ThrottlerStorage, Throttle, SkipThrottle, THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA, } from './throttler/index';
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { defineMetadata, getMetadata } from "./metadata.js";
4
4
  export { VelaFactory, createApplication } from "./factory.js";
5
5
  export { VelaApplication } from "./application.js";
6
6
  // OpenAPI
7
- export { createOpenApiDocument, ApiDoc, ApiTags, zodToJsonSchema } from "./openapi/index.js";
7
+ export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema } from "./openapi/index.js";
8
8
  // DI Container
9
9
  export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from "./container/index.js";
10
10
  // Constants
@@ -24,7 +24,7 @@ export { CacheModule, CacheService, CacheInterceptor, MemoryCacheStore, CacheKey
24
24
  // Event Emitter
25
25
  export { EventEmitterModule, EventEmitter, EventEmitterSubscriber, OnEvent, ON_EVENT_METADATA } from "./event-emitter/index.js";
26
26
  // Schedule
27
- export { ScheduleModule, ScheduleRegistry, ScheduleExecutor, Cron, Interval, SCHEDULE_MODULE_OPTIONS, CRON_METADATA, INTERVAL_METADATA } from "./schedule/index.js";
27
+ export { ScheduleModule, ScheduleRegistry, Cron, Interval, parseCron, CRON_METADATA, INTERVAL_METADATA } from "./schedule/index.js";
28
28
  // Health
29
29
  export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthIndicator } from "./health/index.js";
30
30
  // Throttler
@@ -1,6 +1,7 @@
1
- import type { ApiDocMetadata } from './types';
1
+ import type { ApiDocMetadata, ApiResponseEntry, ApiResponseOptions } from './types';
2
2
  export declare const API_DOC_METADATA = "vela:openapi:doc";
3
3
  export declare const API_TAGS_METADATA = "vela:openapi:tags";
4
+ export declare const API_RESPONSES_METADATA = "vela:openapi:responses";
4
5
  /**
5
6
  * Attach OpenAPI documentation to a route handler (or controller).
6
7
  *
@@ -18,3 +19,16 @@ export declare function ApiDoc(metadata: ApiDocMetadata): MethodDecorator & Clas
18
19
  export declare function ApiTags(...tags: string[]): MethodDecorator & ClassDecorator;
19
20
  export declare function getApiDoc(target: object, propertyKey?: string | symbol): ApiDocMetadata | undefined;
20
21
  export declare function getApiTags(target: object, propertyKey?: string | symbol): string[] | undefined;
22
+ /**
23
+ * Document a response for a given status code. Stackable: apply multiple
24
+ * times on the same handler to declare different statuses.
25
+ *
26
+ * ```ts
27
+ * @Get('/:id')
28
+ * @ApiResponse(200, { description: 'Found', schema: UserDto })
29
+ * @ApiResponse(404, { description: 'Not found', schema: ErrorDto })
30
+ * findOne() { ... }
31
+ * ```
32
+ */
33
+ export declare function ApiResponse(status: number | string, options: ApiResponseOptions): MethodDecorator;
34
+ export declare function getApiResponses(target: object, propertyKey: string | symbol): ApiResponseEntry[] | undefined;
@@ -1,5 +1,6 @@
1
1
  export const API_DOC_METADATA = 'vela:openapi:doc';
2
2
  export const API_TAGS_METADATA = 'vela:openapi:tags';
3
+ export const API_RESPONSES_METADATA = 'vela:openapi:responses';
3
4
  /**
4
5
  * Attach OpenAPI documentation to a route handler (or controller).
5
6
  *
@@ -35,3 +36,26 @@ export function getApiDoc(target, propertyKey) {
35
36
  export function getApiTags(target, propertyKey) {
36
37
  return propertyKey !== undefined ? Reflect.getMetadata(API_TAGS_METADATA, target, propertyKey) : Reflect.getMetadata(API_TAGS_METADATA, target);
37
38
  }
39
+ /**
40
+ * Document a response for a given status code. Stackable: apply multiple
41
+ * times on the same handler to declare different statuses.
42
+ *
43
+ * ```ts
44
+ * @Get('/:id')
45
+ * @ApiResponse(200, { description: 'Found', schema: UserDto })
46
+ * @ApiResponse(404, { description: 'Not found', schema: ErrorDto })
47
+ * findOne() { ... }
48
+ * ```
49
+ */ export function ApiResponse(status, options) {
50
+ return (target, propertyKey)=>{
51
+ const existing = Reflect.getMetadata(API_RESPONSES_METADATA, target, propertyKey) ?? [];
52
+ existing.push({
53
+ status,
54
+ ...options
55
+ });
56
+ Reflect.defineMetadata(API_RESPONSES_METADATA, existing, target, propertyKey);
57
+ };
58
+ }
59
+ export function getApiResponses(target, propertyKey) {
60
+ return Reflect.getMetadata(API_RESPONSES_METADATA, target, propertyKey);
61
+ }
@@ -2,7 +2,7 @@ import { getModuleMetadata } from "../module/decorators.js";
2
2
  import { ForwardRef } from "../container/types.js";
3
3
  import { MetadataRegistry } from "../registry/metadata.registry.js";
4
4
  import { ParamType } from "../constants.js";
5
- import { getApiDoc, getApiTags } from "./decorators.js";
5
+ import { getApiDoc, getApiResponses, getApiTags } from "./decorators.js";
6
6
  import { isOptional, zodToJsonSchema } from "./zod-to-json-schema.js";
7
7
  function isDynamicModuleLike(v) {
8
8
  return !!v && typeof v === 'object' && 'module' in v && typeof v.module === 'function';
@@ -31,7 +31,6 @@ function collectControllers(rootModule) {
31
31
  ];
32
32
  }
33
33
  function normalizePath(path) {
34
- // Hono/Nest style `:id` → OpenAPI `{id}`
35
34
  return path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
36
35
  }
37
36
  function joinPath(a, b) {
@@ -40,10 +39,47 @@ function joinPath(a, b) {
40
39
  const joined = `${left}${right}`;
41
40
  return joined || '/';
42
41
  }
43
- function getParamSchema(param, paramtypes) {
42
+ /**
43
+ * Tracks DTO classes referenced during document generation and registers
44
+ * each under `components.schemas`. Handles name collisions by suffixing.
45
+ */ let ComponentsRegistry = class ComponentsRegistry {
46
+ schemas = new Map();
47
+ classToKey = new WeakMap();
48
+ ref(dtoClass) {
49
+ const existing = this.classToKey.get(dtoClass);
50
+ if (existing) return {
51
+ $ref: `#/components/schemas/${existing}`
52
+ };
53
+ const baseName = dtoClass.name && dtoClass.name !== '' ? dtoClass.name : 'Schema';
54
+ let key = baseName;
55
+ let counter = 2;
56
+ while(this.schemas.has(key)){
57
+ key = `${baseName}${counter++}`;
58
+ }
59
+ this.classToKey.set(dtoClass, key);
60
+ const staticSchema = dtoClass.schema;
61
+ this.schemas.set(key, zodToJsonSchema(staticSchema));
62
+ return {
63
+ $ref: `#/components/schemas/${key}`
64
+ };
65
+ }
66
+ build() {
67
+ if (this.schemas.size === 0) return undefined;
68
+ return Object.fromEntries(this.schemas);
69
+ }
70
+ };
71
+ function isDtoClass(value) {
72
+ return typeof value === 'function' && value.schema != null;
73
+ }
74
+ function getParamSchema(param, paramtypes, registry) {
44
75
  const metatype = paramtypes?.[param.index];
45
- if (metatype?.schema) {
46
- return zodToJsonSchema(metatype.schema);
76
+ if (!metatype) return undefined;
77
+ if (isDtoClass(metatype)) {
78
+ return registry.ref(metatype);
79
+ }
80
+ const maybeSchema = metatype.schema;
81
+ if (maybeSchema) {
82
+ return zodToJsonSchema(maybeSchema);
47
83
  }
48
84
  return undefined;
49
85
  }
@@ -54,13 +90,33 @@ function isParamOptional(param, paramtypes) {
54
90
  }
55
91
  return true;
56
92
  }
57
- function buildOperation(controller, route, pathString) {
93
+ function isLikelyJsonSchema(value) {
94
+ if (!value || typeof value !== 'object') return false;
95
+ const v = value;
96
+ if (typeof v.toJSONSchema === 'function') return false;
97
+ return 'type' in v || '$ref' in v || 'oneOf' in v || 'anyOf' in v || 'allOf' in v || 'enum' in v || 'const' in v;
98
+ }
99
+ function resolveResponseSchema(input, registry) {
100
+ if (input === undefined || input === null) return undefined;
101
+ if (isDtoClass(input)) {
102
+ return registry.ref(input);
103
+ }
104
+ if (isLikelyJsonSchema(input)) {
105
+ return {
106
+ ...input
107
+ };
108
+ }
109
+ if (typeof input === 'object' && typeof input.toJSONSchema === 'function') {
110
+ return zodToJsonSchema(input);
111
+ }
112
+ return undefined;
113
+ }
114
+ function buildOperation(controller, route, pathString, registry) {
58
115
  const handlerName = route.handlerName;
59
116
  const paramMetadata = MetadataRegistry.getParameters(controller).get(handlerName) ?? [];
60
117
  const paramtypes = Reflect.getMetadata('design:paramtypes', controller.prototype, handlerName);
61
118
  const parameters = [];
62
119
  let requestBody;
63
- // Path params declared in the URL but not listed as @Param() get a string default.
64
120
  const declaredPathParams = new Set();
65
121
  const pathParamNames = [
66
122
  ...pathString.matchAll(/\{([^}]+)\}/g)
@@ -72,7 +128,7 @@ function buildOperation(controller, route, pathString) {
72
128
  name: param.name,
73
129
  in: 'path',
74
130
  required: true,
75
- schema: getParamSchema(param, paramtypes) ?? {
131
+ schema: getParamSchema(param, paramtypes, registry) ?? {
76
132
  type: 'string'
77
133
  }
78
134
  });
@@ -81,7 +137,7 @@ function buildOperation(controller, route, pathString) {
81
137
  name: param.name,
82
138
  in: 'query',
83
139
  required: !isParamOptional(param, paramtypes),
84
- schema: getParamSchema(param, paramtypes) ?? {
140
+ schema: getParamSchema(param, paramtypes, registry) ?? {
85
141
  type: 'string'
86
142
  }
87
143
  });
@@ -90,12 +146,12 @@ function buildOperation(controller, route, pathString) {
90
146
  name: param.name,
91
147
  in: 'header',
92
148
  required: !isParamOptional(param, paramtypes),
93
- schema: getParamSchema(param, paramtypes) ?? {
149
+ schema: getParamSchema(param, paramtypes, registry) ?? {
94
150
  type: 'string'
95
151
  }
96
152
  });
97
153
  } else if (param.type === ParamType.BODY) {
98
- const schema = getParamSchema(param, paramtypes);
154
+ const schema = getParamSchema(param, paramtypes, registry);
99
155
  if (schema) {
100
156
  requestBody = {
101
157
  required: !isParamOptional(param, paramtypes),
@@ -131,12 +187,29 @@ function buildOperation(controller, route, pathString) {
131
187
  ...docTags
132
188
  ])
133
189
  ];
134
- const operation = {
135
- responses: {
136
- '200': {
137
- description: 'OK'
138
- }
190
+ const responses = {
191
+ '200': {
192
+ description: 'OK'
193
+ }
194
+ };
195
+ const apiResponses = getApiResponses(controller.prototype, handlerName) ?? [];
196
+ for (const entry of apiResponses){
197
+ const key = String(entry.status);
198
+ const resolved = {
199
+ description: entry.description
200
+ };
201
+ const schema = resolveResponseSchema(entry.schema, registry);
202
+ if (schema) {
203
+ resolved.content = {
204
+ 'application/json': {
205
+ schema
206
+ }
207
+ };
139
208
  }
209
+ responses[key] = resolved;
210
+ }
211
+ const operation = {
212
+ responses
140
213
  };
141
214
  if (parameters.length > 0) operation.parameters = parameters;
142
215
  if (requestBody) operation.requestBody = requestBody;
@@ -159,6 +232,7 @@ const VERB_WHITELIST = new Set([
159
232
  export function createOpenApiDocument(rootModule, options = {}) {
160
233
  const paths = {};
161
234
  const globalPrefix = options.globalPrefix ?? '';
235
+ const registry = new ComponentsRegistry();
162
236
  for (const controller of collectControllers(rootModule)){
163
237
  const controllerPath = MetadataRegistry.getControllerPath(controller);
164
238
  const routes = MetadataRegistry.getRoutes(controller);
@@ -167,13 +241,13 @@ export function createOpenApiDocument(rootModule, options = {}) {
167
241
  if (!VERB_WHITELIST.has(method)) continue;
168
242
  const rawPath = joinPath(joinPath(globalPrefix, controllerPath), route.path);
169
243
  const pathString = normalizePath(rawPath);
170
- const operation = buildOperation(controller, route, pathString);
244
+ const operation = buildOperation(controller, route, pathString, registry);
171
245
  const pathItem = paths[pathString] ?? {};
172
246
  pathItem[method] = operation;
173
247
  paths[pathString] = pathItem;
174
248
  }
175
249
  }
176
- return {
250
+ const document = {
177
251
  openapi: '3.1.0',
178
252
  info: {
179
253
  title: options.info?.title ?? 'Vela API',
@@ -184,4 +258,11 @@ export function createOpenApiDocument(rootModule, options = {}) {
184
258
  },
185
259
  paths
186
260
  };
261
+ const schemas = registry.build();
262
+ if (schemas) {
263
+ document.components = {
264
+ schemas
265
+ };
266
+ }
267
+ return document;
187
268
  }
@@ -1,4 +1,5 @@
1
1
  export { createOpenApiDocument } from './document';
2
- export { ApiDoc, ApiTags, API_DOC_METADATA, API_TAGS_METADATA } from './decorators';
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
- export type { ApiDocMetadata, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, } from './types';
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';
@@ -1,3 +1,4 @@
1
1
  export { createOpenApiDocument } from "./document.js";
2
- export { ApiDoc, ApiTags, API_DOC_METADATA, API_TAGS_METADATA } from "./decorators.js";
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
+ export { renderScalarUi } from "./scalar-ui.js";
@@ -0,0 +1 @@
1
+ export declare function renderScalarUi(jsonUrl: string): string;
@@ -0,0 +1,18 @@
1
+ // Minimal HTML shell that bootstraps Scalar's hosted API-reference UI.
2
+ // Served as a static string so it works on every edge runtime with no
3
+ // Node or bundler deps. Scalar itself is loaded from a CDN at runtime.
4
+ export function renderScalarUi(jsonUrl) {
5
+ const safeUrl = jsonUrl.replace(/"/g, '&quot;');
6
+ return `<!doctype html>
7
+ <html>
8
+ <head>
9
+ <title>API Reference</title>
10
+ <meta charset="utf-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
12
+ </head>
13
+ <body>
14
+ <script id="api-reference" data-url="${safeUrl}"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
16
+ </body>
17
+ </html>`;
18
+ }