@velajs/vela 0.8.0 → 0.10.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.
@@ -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){
package/dist/index.d.ts CHANGED
@@ -2,14 +2,14 @@ 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';
10
10
  export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, } from './http/index';
11
11
  export { Logger, LogLevel } from './services/index';
12
- export type { LoggerService } from './services/index';
12
+ export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './services/index';
13
13
  export { ConfigModule, ConfigService, CONFIG_OPTIONS } from './config/index';
14
14
  export type { ConfigModuleOptions } from './config/index';
15
15
  export { HttpModule, HttpService, HTTP_MODULE_OPTIONS, HttpRequestException } from './fetch/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
@@ -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
+ }
@@ -79,7 +79,25 @@ export interface ApiDocMetadata {
79
79
  deprecated?: boolean;
80
80
  tags?: string[];
81
81
  }
82
+ export interface ApiResponseOptions {
83
+ description: string;
84
+ /** Zod schema, DTO class (from createZodDto), or raw JSON Schema. */
85
+ schema?: unknown;
86
+ }
87
+ export interface ApiResponseEntry extends ApiResponseOptions {
88
+ status: number | string;
89
+ }
82
90
  export interface CreateOpenApiDocumentOptions {
83
91
  info?: Partial<OpenApiInfo>;
84
92
  globalPrefix?: string;
85
93
  }
94
+ export interface MountOpenApiOptions {
95
+ /** Pre-built OpenAPI document to serve. */
96
+ document: OpenApiDocument;
97
+ /** Path for the JSON endpoint. Default `/docs.json`. */
98
+ 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`. */
102
+ uiPath?: string;
103
+ }
@@ -1,2 +1,2 @@
1
1
  export { Logger, LogLevel } from './logger';
2
- export type { LoggerService } from './logger';
2
+ export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './logger';
@@ -13,18 +13,46 @@ export interface LoggerService {
13
13
  debug?(message: unknown, ...optionalParams: unknown[]): void;
14
14
  verbose?(message: unknown, ...optionalParams: unknown[]): void;
15
15
  }
16
+ export type ContextProvider = () => Record<string, unknown>;
17
+ export type LoggerLevelName = 'LOG' | 'ERROR' | 'WARN' | 'DEBUG' | 'VERBOSE';
18
+ export type Writer = (level: LoggerLevelName, line: string, ...rest: unknown[]) => void;
16
19
  export declare class Logger implements LoggerService {
17
20
  static level: LogLevel;
18
21
  private static globalLogger;
22
+ private static globalContextProviders;
23
+ private static globalWriter;
19
24
  private context?;
25
+ private instanceContextProviders;
26
+ private instanceWriter?;
20
27
  static setLogLevel(level: LogLevel): void;
21
28
  static overrideLogger(logger: LoggerService | false): void;
29
+ /** Register a context provider run on every log line from every logger. */
30
+ static addContextProvider(provider: ContextProvider): void;
31
+ /** Remove all registered global context providers. */
32
+ static clearContextProviders(): void;
33
+ /** Replace the default console writer. Useful on edge runtimes that have
34
+ * a different output sink (e.g. `env.LOGS.writeDataPoint` on Workers). */
35
+ static setWriter(writer: Writer): void;
36
+ /** Restore the default console-based writer. */
37
+ static resetWriter(): void;
22
38
  constructor(context?: string);
23
39
  setContext(context: string): void;
40
+ /** Add a context provider that runs only for THIS logger instance.
41
+ * Children created via extend() inherit these providers by copy. */
42
+ addContextProvider(provider: ContextProvider): this;
43
+ /** Override the writer for this logger (and any extend() children). */
44
+ setWriter(writer: Writer): this;
45
+ /** Create a child logger with a nested namespace. Context becomes
46
+ * `parent:child`; instance-level providers and writers are inherited
47
+ * by copy so later mutations on either logger stay isolated. */
48
+ extend(namespace: string): Logger;
24
49
  log(message: unknown, ...optionalParams: unknown[]): void;
25
50
  error(message: unknown, ...optionalParams: unknown[]): void;
26
51
  warn(message: unknown, ...optionalParams: unknown[]): void;
27
52
  debug(message: unknown, ...optionalParams: unknown[]): void;
28
53
  verbose(message: unknown, ...optionalParams: unknown[]): void;
54
+ private emit;
29
55
  private formatMessage;
56
+ private collectContext;
57
+ private formatContextObj;
30
58
  }
@@ -17,16 +17,50 @@ export var LogLevel = /*#__PURE__*/ function(LogLevel) {
17
17
  LogLevel[LogLevel["SILENT"] = 5] = "SILENT";
18
18
  return LogLevel;
19
19
  }({});
20
+ const defaultWriter = (level, line, ...rest)=>{
21
+ if (level === 'ERROR') return console.error(line, ...rest);
22
+ if (level === 'WARN') return console.warn(line, ...rest);
23
+ if (level === 'DEBUG' || level === 'VERBOSE') return console.debug(line, ...rest);
24
+ return console.log(line, ...rest);
25
+ };
26
+ function formatValue(v) {
27
+ if (typeof v === 'string') {
28
+ return /[\s="\\]/.test(v) ? JSON.stringify(v) : v;
29
+ }
30
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
31
+ try {
32
+ return JSON.stringify(v);
33
+ } catch {
34
+ return String(v);
35
+ }
36
+ }
20
37
  export class Logger {
21
38
  static level = 2;
22
39
  static globalLogger;
40
+ static globalContextProviders = [];
41
+ static globalWriter = defaultWriter;
23
42
  context;
43
+ instanceContextProviders = [];
44
+ instanceWriter;
24
45
  static setLogLevel(level) {
25
46
  Logger.level = level;
26
47
  }
27
48
  static overrideLogger(logger) {
28
49
  Logger.globalLogger = logger;
29
50
  }
51
+ /** Register a context provider run on every log line from every logger. */ static addContextProvider(provider) {
52
+ Logger.globalContextProviders.push(provider);
53
+ }
54
+ /** Remove all registered global context providers. */ static clearContextProviders() {
55
+ Logger.globalContextProviders = [];
56
+ }
57
+ /** Replace the default console writer. Useful on edge runtimes that have
58
+ * a different output sink (e.g. `env.LOGS.writeDataPoint` on Workers). */ static setWriter(writer) {
59
+ Logger.globalWriter = writer;
60
+ }
61
+ /** Restore the default console-based writer. */ static resetWriter() {
62
+ Logger.globalWriter = defaultWriter;
63
+ }
30
64
  constructor(context){
31
65
  if (context) {
32
66
  this.context = context;
@@ -35,55 +69,86 @@ export class Logger {
35
69
  setContext(context) {
36
70
  this.context = context;
37
71
  }
72
+ /** Add a context provider that runs only for THIS logger instance.
73
+ * Children created via extend() inherit these providers by copy. */ addContextProvider(provider) {
74
+ this.instanceContextProviders.push(provider);
75
+ return this;
76
+ }
77
+ /** Override the writer for this logger (and any extend() children). */ setWriter(writer) {
78
+ this.instanceWriter = writer;
79
+ return this;
80
+ }
81
+ /** Create a child logger with a nested namespace. Context becomes
82
+ * `parent:child`; instance-level providers and writers are inherited
83
+ * by copy so later mutations on either logger stay isolated. */ extend(namespace) {
84
+ const newContext = this.context ? `${this.context}:${namespace}` : namespace;
85
+ const child = new Logger(newContext);
86
+ child.instanceContextProviders = [
87
+ ...this.instanceContextProviders
88
+ ];
89
+ if (this.instanceWriter) child.instanceWriter = this.instanceWriter;
90
+ return child;
91
+ }
38
92
  log(message, ...optionalParams) {
39
- if (Logger.level > 2) return;
40
- if (Logger.globalLogger === false) return;
41
- if (Logger.globalLogger) {
42
- Logger.globalLogger.log(message, ...optionalParams);
43
- return;
44
- }
45
- console.log(this.formatMessage('LOG', message), ...optionalParams);
93
+ this.emit('LOG', 2, message, optionalParams);
46
94
  }
47
95
  error(message, ...optionalParams) {
48
- if (Logger.level > 4) return;
49
- if (Logger.globalLogger === false) return;
50
- if (Logger.globalLogger) {
51
- Logger.globalLogger.error(message, ...optionalParams);
52
- return;
53
- }
54
- console.error(this.formatMessage('ERROR', message), ...optionalParams);
96
+ this.emit('ERROR', 4, message, optionalParams);
55
97
  }
56
98
  warn(message, ...optionalParams) {
57
- if (Logger.level > 3) return;
58
- if (Logger.globalLogger === false) return;
59
- if (Logger.globalLogger) {
60
- Logger.globalLogger.warn(message, ...optionalParams);
61
- return;
62
- }
63
- console.warn(this.formatMessage('WARN', message), ...optionalParams);
99
+ this.emit('WARN', 3, message, optionalParams);
64
100
  }
65
101
  debug(message, ...optionalParams) {
66
- if (Logger.level > 1) return;
67
- if (Logger.globalLogger === false) return;
68
- if (Logger.globalLogger) {
69
- Logger.globalLogger.debug?.(message, ...optionalParams);
70
- return;
71
- }
72
- console.debug(this.formatMessage('DEBUG', message), ...optionalParams);
102
+ this.emit('DEBUG', 1, message, optionalParams);
73
103
  }
74
104
  verbose(message, ...optionalParams) {
75
- if (Logger.level > 0) return;
105
+ this.emit('VERBOSE', 0, message, optionalParams);
106
+ }
107
+ emit(levelName, levelValue, message, rest) {
108
+ if (Logger.level > levelValue) return;
76
109
  if (Logger.globalLogger === false) return;
77
110
  if (Logger.globalLogger) {
78
- Logger.globalLogger.verbose?.(message, ...optionalParams);
111
+ const method = levelName.toLowerCase();
112
+ const fn = Logger.globalLogger[method];
113
+ if (fn) fn.call(Logger.globalLogger, message, ...rest);
79
114
  return;
80
115
  }
81
- console.debug(this.formatMessage('VERBOSE', message), ...optionalParams);
116
+ const line = this.formatMessage(levelName, message);
117
+ const writer = this.instanceWriter ?? Logger.globalWriter;
118
+ writer(levelName, line, ...rest);
82
119
  }
83
120
  formatMessage(level, message) {
84
121
  const timestamp = new Date().toISOString();
85
122
  const ctx = this.context ? ` [${this.context}]` : '';
86
- return `[Vela] ${timestamp} ${level}${ctx} ${message}`;
123
+ const contextStr = this.formatContextObj(this.collectContext());
124
+ return `[Vela] ${timestamp} ${level}${ctx} ${message}${contextStr}`;
125
+ }
126
+ collectContext() {
127
+ const result = {};
128
+ for (const provider of Logger.globalContextProviders){
129
+ try {
130
+ Object.assign(result, provider());
131
+ } catch {
132
+ // Provider threw; drop its contribution and keep going.
133
+ }
134
+ }
135
+ for (const provider of this.instanceContextProviders){
136
+ try {
137
+ Object.assign(result, provider());
138
+ } catch {
139
+ // Same — never let a provider break logging.
140
+ }
141
+ }
142
+ return result;
143
+ }
144
+ formatContextObj(obj) {
145
+ const parts = [];
146
+ for (const key of Object.keys(obj)){
147
+ const value = obj[key];
148
+ if (value === undefined || value === null) continue;
149
+ parts.push(`${key}=${formatValue(value)}`);
150
+ }
151
+ return parts.length > 0 ? ` ${parts.join(' ')}` : '';
87
152
  }
88
153
  }
89
154
  Logger = _ts_decorate([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",