@velajs/vela 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -2,12 +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
7
  export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from './container/index';
6
8
  export type { Type, Token, InjectableOptions, ProviderOptions, } from './container/index';
7
9
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from './constants';
8
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';
9
11
  export { Logger, LogLevel } from './services/index';
10
- export type { LoggerService } from './services/index';
12
+ export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './services/index';
11
13
  export { ConfigModule, ConfigService, CONFIG_OPTIONS } from './config/index';
12
14
  export type { ConfigModuleOptions } from './config/index';
13
15
  export { HttpModule, HttpService, HTTP_MODULE_OPTIONS, HttpRequestException } from './fetch/index';
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ export { defineMetadata, getMetadata } from "./metadata.js";
3
3
  // Factory & Application
4
4
  export { VelaFactory, createApplication } from "./factory.js";
5
5
  export { VelaApplication } from "./application.js";
6
+ // OpenAPI
7
+ export { createOpenApiDocument, ApiDoc, ApiTags, zodToJsonSchema } from "./openapi/index.js";
6
8
  // DI Container
7
9
  export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from "./container/index.js";
8
10
  // Constants
@@ -0,0 +1,20 @@
1
+ import type { ApiDocMetadata } from './types';
2
+ export declare const API_DOC_METADATA = "vela:openapi:doc";
3
+ export declare const API_TAGS_METADATA = "vela:openapi:tags";
4
+ /**
5
+ * Attach OpenAPI documentation to a route handler (or controller).
6
+ *
7
+ * ```ts
8
+ * @Get('/:id')
9
+ * @ApiDoc({ summary: 'Get a user', description: '...', operationId: 'getUser' })
10
+ * findOne(@Param('id') id: string) { ... }
11
+ * ```
12
+ */
13
+ export declare function ApiDoc(metadata: ApiDocMetadata): MethodDecorator & ClassDecorator;
14
+ /**
15
+ * Attach OpenAPI tags to a controller or handler. Handler-level tags merge
16
+ * with controller-level tags; duplicates are de-duplicated in the output.
17
+ */
18
+ export declare function ApiTags(...tags: string[]): MethodDecorator & ClassDecorator;
19
+ export declare function getApiDoc(target: object, propertyKey?: string | symbol): ApiDocMetadata | undefined;
20
+ export declare function getApiTags(target: object, propertyKey?: string | symbol): string[] | undefined;
@@ -0,0 +1,37 @@
1
+ export const API_DOC_METADATA = 'vela:openapi:doc';
2
+ export const API_TAGS_METADATA = 'vela:openapi:tags';
3
+ /**
4
+ * Attach OpenAPI documentation to a route handler (or controller).
5
+ *
6
+ * ```ts
7
+ * @Get('/:id')
8
+ * @ApiDoc({ summary: 'Get a user', description: '...', operationId: 'getUser' })
9
+ * findOne(@Param('id') id: string) { ... }
10
+ * ```
11
+ */ export function ApiDoc(metadata) {
12
+ return (target, propertyKey)=>{
13
+ if (propertyKey !== undefined) {
14
+ Reflect.defineMetadata(API_DOC_METADATA, metadata, target, propertyKey);
15
+ } else {
16
+ Reflect.defineMetadata(API_DOC_METADATA, metadata, target);
17
+ }
18
+ };
19
+ }
20
+ /**
21
+ * Attach OpenAPI tags to a controller or handler. Handler-level tags merge
22
+ * with controller-level tags; duplicates are de-duplicated in the output.
23
+ */ export function ApiTags(...tags) {
24
+ return (target, propertyKey)=>{
25
+ if (propertyKey !== undefined) {
26
+ Reflect.defineMetadata(API_TAGS_METADATA, tags, target, propertyKey);
27
+ } else {
28
+ Reflect.defineMetadata(API_TAGS_METADATA, tags, target);
29
+ }
30
+ };
31
+ }
32
+ export function getApiDoc(target, propertyKey) {
33
+ return propertyKey !== undefined ? Reflect.getMetadata(API_DOC_METADATA, target, propertyKey) : Reflect.getMetadata(API_DOC_METADATA, target);
34
+ }
35
+ export function getApiTags(target, propertyKey) {
36
+ return propertyKey !== undefined ? Reflect.getMetadata(API_TAGS_METADATA, target, propertyKey) : Reflect.getMetadata(API_TAGS_METADATA, target);
37
+ }
@@ -0,0 +1,3 @@
1
+ import type { Type } from '../container/types';
2
+ import type { CreateOpenApiDocumentOptions, OpenApiDocument } from './types';
3
+ export declare function createOpenApiDocument(rootModule: Type, options?: CreateOpenApiDocumentOptions): OpenApiDocument;
@@ -0,0 +1,187 @@
1
+ import { getModuleMetadata } from "../module/decorators.js";
2
+ import { ForwardRef } from "../container/types.js";
3
+ import { MetadataRegistry } from "../registry/metadata.registry.js";
4
+ import { ParamType } from "../constants.js";
5
+ import { getApiDoc, getApiTags } from "./decorators.js";
6
+ import { isOptional, zodToJsonSchema } from "./zod-to-json-schema.js";
7
+ function isDynamicModuleLike(v) {
8
+ return !!v && typeof v === 'object' && 'module' in v && typeof v.module === 'function';
9
+ }
10
+ function collectControllers(rootModule) {
11
+ const visited = new Set();
12
+ const controllers = new Set();
13
+ function visit(entry) {
14
+ const unwrapped = entry instanceof ForwardRef ? entry.factory() : entry;
15
+ const moduleClass = isDynamicModuleLike(unwrapped) ? unwrapped.module : unwrapped;
16
+ const extraControllers = isDynamicModuleLike(unwrapped) ? unwrapped.controllers ?? [] : [];
17
+ const extraImports = isDynamicModuleLike(unwrapped) ? unwrapped.imports ?? [] : [];
18
+ if (typeof moduleClass !== 'function' || visited.has(moduleClass)) return;
19
+ visited.add(moduleClass);
20
+ const metadata = getModuleMetadata(moduleClass);
21
+ if (metadata) {
22
+ for (const controller of metadata.controllers)controllers.add(controller);
23
+ for (const imp of metadata.imports)visit(imp);
24
+ }
25
+ for (const controller of extraControllers)controllers.add(controller);
26
+ for (const imp of extraImports)visit(imp);
27
+ }
28
+ visit(rootModule);
29
+ return [
30
+ ...controllers
31
+ ];
32
+ }
33
+ function normalizePath(path) {
34
+ // Hono/Nest style `:id` → OpenAPI `{id}`
35
+ return path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
36
+ }
37
+ function joinPath(a, b) {
38
+ const left = a.endsWith('/') ? a.slice(0, -1) : a;
39
+ const right = b && !b.startsWith('/') ? `/${b}` : b;
40
+ const joined = `${left}${right}`;
41
+ return joined || '/';
42
+ }
43
+ function getParamSchema(param, paramtypes) {
44
+ const metatype = paramtypes?.[param.index];
45
+ if (metatype?.schema) {
46
+ return zodToJsonSchema(metatype.schema);
47
+ }
48
+ return undefined;
49
+ }
50
+ function isParamOptional(param, paramtypes) {
51
+ const metatype = paramtypes?.[param.index];
52
+ if (metatype?.schema) {
53
+ return isOptional(metatype.schema);
54
+ }
55
+ return true;
56
+ }
57
+ function buildOperation(controller, route, pathString) {
58
+ const handlerName = route.handlerName;
59
+ const paramMetadata = MetadataRegistry.getParameters(controller).get(handlerName) ?? [];
60
+ const paramtypes = Reflect.getMetadata('design:paramtypes', controller.prototype, handlerName);
61
+ const parameters = [];
62
+ let requestBody;
63
+ // Path params declared in the URL but not listed as @Param() get a string default.
64
+ const declaredPathParams = new Set();
65
+ const pathParamNames = [
66
+ ...pathString.matchAll(/\{([^}]+)\}/g)
67
+ ].map((m)=>m[1]);
68
+ for (const param of paramMetadata){
69
+ if (param.type === ParamType.PARAM && param.name) {
70
+ declaredPathParams.add(param.name);
71
+ parameters.push({
72
+ name: param.name,
73
+ in: 'path',
74
+ required: true,
75
+ schema: getParamSchema(param, paramtypes) ?? {
76
+ type: 'string'
77
+ }
78
+ });
79
+ } else if (param.type === ParamType.QUERY && param.name) {
80
+ parameters.push({
81
+ name: param.name,
82
+ in: 'query',
83
+ required: !isParamOptional(param, paramtypes),
84
+ schema: getParamSchema(param, paramtypes) ?? {
85
+ type: 'string'
86
+ }
87
+ });
88
+ } else if (param.type === ParamType.HEADERS && param.name) {
89
+ parameters.push({
90
+ name: param.name,
91
+ in: 'header',
92
+ required: !isParamOptional(param, paramtypes),
93
+ schema: getParamSchema(param, paramtypes) ?? {
94
+ type: 'string'
95
+ }
96
+ });
97
+ } else if (param.type === ParamType.BODY) {
98
+ const schema = getParamSchema(param, paramtypes);
99
+ if (schema) {
100
+ requestBody = {
101
+ required: !isParamOptional(param, paramtypes),
102
+ content: {
103
+ 'application/json': {
104
+ schema
105
+ }
106
+ }
107
+ };
108
+ }
109
+ }
110
+ }
111
+ for (const name of pathParamNames){
112
+ if (!declaredPathParams.has(name)) {
113
+ parameters.push({
114
+ name,
115
+ in: 'path',
116
+ required: true,
117
+ schema: {
118
+ type: 'string'
119
+ }
120
+ });
121
+ }
122
+ }
123
+ const docMeta = getApiDoc(controller.prototype, handlerName);
124
+ const controllerTags = getApiTags(controller) ?? [];
125
+ const handlerTags = getApiTags(controller.prototype, handlerName) ?? [];
126
+ const docTags = docMeta?.tags ?? [];
127
+ const mergedTags = [
128
+ ...new Set([
129
+ ...controllerTags,
130
+ ...handlerTags,
131
+ ...docTags
132
+ ])
133
+ ];
134
+ const operation = {
135
+ responses: {
136
+ '200': {
137
+ description: 'OK'
138
+ }
139
+ }
140
+ };
141
+ if (parameters.length > 0) operation.parameters = parameters;
142
+ if (requestBody) operation.requestBody = requestBody;
143
+ if (docMeta?.summary) operation.summary = docMeta.summary;
144
+ if (docMeta?.description) operation.description = docMeta.description;
145
+ if (docMeta?.operationId) operation.operationId = docMeta.operationId;
146
+ if (docMeta?.deprecated) operation.deprecated = docMeta.deprecated;
147
+ if (mergedTags.length > 0) operation.tags = mergedTags;
148
+ return operation;
149
+ }
150
+ const VERB_WHITELIST = new Set([
151
+ 'get',
152
+ 'post',
153
+ 'put',
154
+ 'patch',
155
+ 'delete',
156
+ 'options',
157
+ 'head'
158
+ ]);
159
+ export function createOpenApiDocument(rootModule, options = {}) {
160
+ const paths = {};
161
+ const globalPrefix = options.globalPrefix ?? '';
162
+ for (const controller of collectControllers(rootModule)){
163
+ const controllerPath = MetadataRegistry.getControllerPath(controller);
164
+ const routes = MetadataRegistry.getRoutes(controller);
165
+ for (const route of routes){
166
+ const method = route.method.toLowerCase();
167
+ if (!VERB_WHITELIST.has(method)) continue;
168
+ const rawPath = joinPath(joinPath(globalPrefix, controllerPath), route.path);
169
+ const pathString = normalizePath(rawPath);
170
+ const operation = buildOperation(controller, route, pathString);
171
+ const pathItem = paths[pathString] ?? {};
172
+ pathItem[method] = operation;
173
+ paths[pathString] = pathItem;
174
+ }
175
+ }
176
+ return {
177
+ openapi: '3.1.0',
178
+ info: {
179
+ title: options.info?.title ?? 'Vela API',
180
+ version: options.info?.version ?? '1.0.0',
181
+ ...options.info?.description ? {
182
+ description: options.info.description
183
+ } : {}
184
+ },
185
+ paths
186
+ };
187
+ }
@@ -0,0 +1,4 @@
1
+ export { createOpenApiDocument } from './document';
2
+ export { ApiDoc, ApiTags, API_DOC_METADATA, API_TAGS_METADATA } from './decorators';
3
+ export { zodToJsonSchema } from './zod-to-json-schema';
4
+ export type { ApiDocMetadata, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, } from './types';
@@ -0,0 +1,3 @@
1
+ export { createOpenApiDocument } from "./document.js";
2
+ export { ApiDoc, ApiTags, API_DOC_METADATA, API_TAGS_METADATA } from "./decorators.js";
3
+ export { zodToJsonSchema } from "./zod-to-json-schema.js";
@@ -0,0 +1,85 @@
1
+ export interface OpenApiInfo {
2
+ title: string;
3
+ version: string;
4
+ description?: string;
5
+ }
6
+ export interface JsonSchema {
7
+ type?: string | string[];
8
+ format?: string;
9
+ enum?: Array<string | number | boolean | null>;
10
+ const?: unknown;
11
+ description?: string;
12
+ nullable?: boolean;
13
+ default?: unknown;
14
+ items?: JsonSchema;
15
+ properties?: Record<string, JsonSchema>;
16
+ required?: string[];
17
+ additionalProperties?: boolean | JsonSchema;
18
+ oneOf?: JsonSchema[];
19
+ anyOf?: JsonSchema[];
20
+ allOf?: JsonSchema[];
21
+ minimum?: number;
22
+ maximum?: number;
23
+ minLength?: number;
24
+ maxLength?: number;
25
+ pattern?: string;
26
+ $ref?: string;
27
+ [key: string]: unknown;
28
+ }
29
+ export interface OpenApiParameter {
30
+ name: string;
31
+ in: 'path' | 'query' | 'header' | 'cookie';
32
+ required?: boolean;
33
+ description?: string;
34
+ schema?: JsonSchema;
35
+ }
36
+ export interface OpenApiRequestBody {
37
+ description?: string;
38
+ required?: boolean;
39
+ content?: Record<string, {
40
+ schema: JsonSchema;
41
+ }>;
42
+ }
43
+ export interface OpenApiResponse {
44
+ description: string;
45
+ content?: Record<string, {
46
+ schema: JsonSchema;
47
+ }>;
48
+ }
49
+ export interface OpenApiOperation {
50
+ summary?: string;
51
+ description?: string;
52
+ operationId?: string;
53
+ deprecated?: boolean;
54
+ tags?: string[];
55
+ parameters?: OpenApiParameter[];
56
+ requestBody?: OpenApiRequestBody;
57
+ responses: Record<string, OpenApiResponse>;
58
+ }
59
+ export type HttpVerb = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'head';
60
+ export type OpenApiPathItem = {
61
+ [verb in HttpVerb]?: OpenApiOperation;
62
+ };
63
+ export interface OpenApiDocument {
64
+ openapi: '3.1.0';
65
+ info: OpenApiInfo;
66
+ paths: Record<string, OpenApiPathItem>;
67
+ components?: {
68
+ schemas?: Record<string, JsonSchema>;
69
+ };
70
+ tags?: Array<{
71
+ name: string;
72
+ description?: string;
73
+ }>;
74
+ }
75
+ export interface ApiDocMetadata {
76
+ summary?: string;
77
+ description?: string;
78
+ operationId?: string;
79
+ deprecated?: boolean;
80
+ tags?: string[];
81
+ }
82
+ export interface CreateOpenApiDocumentOptions {
83
+ info?: Partial<OpenApiInfo>;
84
+ globalPrefix?: string;
85
+ }
@@ -0,0 +1,4 @@
1
+ // Minimal OpenAPI 3.1 subset Vela emits. Kept intentionally loose (index
2
+ // signatures on schemas, open tags array) so users can extend without
3
+ // fighting the types. Covers what createOpenApiDocument actually produces.
4
+ export { };
@@ -0,0 +1,4 @@
1
+ import type { JsonSchema } from './types';
2
+ export declare function zodToJsonSchema(schema: unknown): JsonSchema;
3
+ export declare function isOptional(schema: unknown): boolean;
4
+ export declare function isNullable(schema: unknown): boolean;
@@ -0,0 +1,57 @@
1
+ // Zod v4 exposes `schema.toJSONSchema()` on every schema instance — an
2
+ // authoritative, zero-cost converter. We delegate to it when present and
3
+ // strip the `$schema` meta so the result embeds cleanly into OpenAPI
4
+ // components. For non-Zod schemas (anything that doesn't expose the
5
+ // method) we return an empty (permissive) schema.
6
+ function stripOpenApiIncompatible(schema) {
7
+ const out = {
8
+ ...schema
9
+ };
10
+ delete out.$schema;
11
+ // Zod v4 emits `additionalProperties: false` by default; users rarely
12
+ // want that constraint in their published OpenAPI doc, so leave it in
13
+ // — it's a faithful reflection of the schema. No change here.
14
+ if (Array.isArray(out.properties)) {
15
+ // shouldn't happen, but guard
16
+ } else if (out.properties && typeof out.properties === 'object') {
17
+ const props = out.properties;
18
+ const cleaned = {};
19
+ for (const [key, val] of Object.entries(props)){
20
+ cleaned[key] = stripOpenApiIncompatible(val);
21
+ }
22
+ out.properties = cleaned;
23
+ }
24
+ if (out.items && typeof out.items === 'object') {
25
+ out.items = stripOpenApiIncompatible(out.items);
26
+ }
27
+ return out;
28
+ }
29
+ export function zodToJsonSchema(schema) {
30
+ if (!schema || typeof schema !== 'object') return {};
31
+ const maybe = schema.toJSONSchema;
32
+ if (typeof maybe === 'function') {
33
+ try {
34
+ const result = maybe.call(schema);
35
+ if (result && typeof result === 'object') {
36
+ return stripOpenApiIncompatible(result);
37
+ }
38
+ } catch {
39
+ // Zod may refuse to convert certain schemas; fall through.
40
+ }
41
+ }
42
+ return {};
43
+ }
44
+ // Zod v4 and v3 both name constructors `ZodOptional` / `ZodDefault` /
45
+ // `ZodNullable`. Checking the constructor name is cheap and avoids
46
+ // depending on internal `_def` shape.
47
+ function ctorName(schema) {
48
+ if (!schema || typeof schema !== 'object') return undefined;
49
+ return schema.constructor?.name;
50
+ }
51
+ export function isOptional(schema) {
52
+ const name = ctorName(schema);
53
+ return name === 'ZodOptional' || name === 'ZodDefault';
54
+ }
55
+ export function isNullable(schema) {
56
+ return ctorName(schema) === 'ZodNullable';
57
+ }
@@ -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.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",