@veloxts/core 0.7.0 → 0.7.2
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/CHANGELOG.md +12 -0
- package/dist/app.d.ts +4 -17
- package/dist/app.js +25 -67
- package/dist/context.js +0 -9
- package/dist/errors/fail.d.ts +13 -8
- package/dist/errors/fail.js +0 -3
- package/dist/errors/formatter.js +4 -5
- package/dist/errors/index.js +0 -3
- package/dist/errors.js +4 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -7
- package/dist/plugin.js +0 -2
- package/dist/plugins/request-logger.js +1 -5
- package/dist/plugins/static.js +0 -8
- package/dist/utils/banner.js +0 -6
- package/dist/utils/config.js +3 -4
- package/dist/utils/lifecycle.js +8 -9
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +59 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @veloxts/core
|
|
2
2
|
|
|
3
|
+
## 0.7.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- chore(auth,core,create,cli,client,orm,mcp,router,validation,web): simplify code for clarity and maintainability
|
|
8
|
+
|
|
9
|
+
## 0.7.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- security audit, bumps dependency packages
|
|
14
|
+
|
|
3
15
|
## 0.7.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/app.d.ts
CHANGED
|
@@ -79,20 +79,12 @@ export declare class VeloxApp {
|
|
|
79
79
|
*/
|
|
80
80
|
get address(): string | null;
|
|
81
81
|
/**
|
|
82
|
-
* Async initialization
|
|
83
|
-
*
|
|
84
|
-
* @internal - This method is public for factory access but should not be called directly.
|
|
85
|
-
* Use createVeloxApp() instead.
|
|
86
|
-
*/
|
|
87
|
-
initialize(): Promise<void>;
|
|
88
|
-
/**
|
|
89
|
-
* Sets up request context decorator
|
|
90
|
-
*
|
|
91
|
-
* Adds `request.context` property to all requests via onRequest hook
|
|
82
|
+
* Async initialization hook for factory function.
|
|
83
|
+
* Empty because Fastify's ready() must be called in start() after plugin registration.
|
|
92
84
|
*
|
|
93
85
|
* @internal
|
|
94
86
|
*/
|
|
95
|
-
|
|
87
|
+
initialize(): Promise<void>;
|
|
96
88
|
/**
|
|
97
89
|
* Sets up global error handling
|
|
98
90
|
*
|
|
@@ -101,12 +93,6 @@ export declare class VeloxApp {
|
|
|
101
93
|
* @internal
|
|
102
94
|
*/
|
|
103
95
|
private _setupErrorHandling;
|
|
104
|
-
/**
|
|
105
|
-
* Sets up graceful shutdown handlers for process signals
|
|
106
|
-
*
|
|
107
|
-
* @internal
|
|
108
|
-
*/
|
|
109
|
-
private _setupGracefulShutdown;
|
|
110
96
|
/**
|
|
111
97
|
* Registers a plugin with the application
|
|
112
98
|
*
|
|
@@ -293,5 +279,6 @@ export declare function veloxApp(config?: VeloxAppConfig): Promise<VeloxApp>;
|
|
|
293
279
|
*
|
|
294
280
|
* const app = await velox({ port: 3030 });
|
|
295
281
|
* ```
|
|
282
|
+
* @deprecated Use veloxApp() instead.
|
|
296
283
|
*/
|
|
297
284
|
export declare const velox: typeof veloxApp;
|
package/dist/app.js
CHANGED
|
@@ -13,6 +13,8 @@ import { registerStatic } from './plugins/static.js';
|
|
|
13
13
|
import { printBanner } from './utils/banner.js';
|
|
14
14
|
import { mergeConfig, validateConfig } from './utils/config.js';
|
|
15
15
|
import { LifecycleManager } from './utils/lifecycle.js';
|
|
16
|
+
import { createLogger } from './utils/logger.js';
|
|
17
|
+
const log = createLogger('core');
|
|
16
18
|
/**
|
|
17
19
|
* Main VeloxTS application instance
|
|
18
20
|
*
|
|
@@ -46,25 +48,18 @@ export class VeloxApp {
|
|
|
46
48
|
* @internal
|
|
47
49
|
*/
|
|
48
50
|
constructor(config) {
|
|
49
|
-
// Merge user config with defaults and validate
|
|
50
51
|
const merged = mergeConfig(config);
|
|
51
|
-
// Validate and freeze configuration
|
|
52
52
|
this._config = validateConfig(merged);
|
|
53
|
-
|
|
54
|
-
const fastifyOptions = {
|
|
53
|
+
this._server = fastify({
|
|
55
54
|
logger: this._config.logger,
|
|
56
55
|
...this._config.fastify,
|
|
57
|
-
};
|
|
58
|
-
this._server = fastify(fastifyOptions);
|
|
59
|
-
// Initialize lifecycle manager
|
|
56
|
+
});
|
|
60
57
|
this._lifecycle = new LifecycleManager();
|
|
61
|
-
|
|
62
|
-
this._setupContext();
|
|
63
|
-
// Set up error handling
|
|
58
|
+
setupContextHook(this._server);
|
|
64
59
|
this._setupErrorHandling();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
this._lifecycle.setupSignalHandlers(async () => {
|
|
61
|
+
await this.stop();
|
|
62
|
+
});
|
|
68
63
|
if (process.env.VELOX_REQUEST_LOGGING === 'true') {
|
|
69
64
|
this._server.register(requestLogger);
|
|
70
65
|
}
|
|
@@ -108,24 +103,13 @@ export class VeloxApp {
|
|
|
108
103
|
return this._address;
|
|
109
104
|
}
|
|
110
105
|
/**
|
|
111
|
-
* Async initialization
|
|
112
|
-
*
|
|
113
|
-
* @internal - This method is public for factory access but should not be called directly.
|
|
114
|
-
* Use createVeloxApp() instead.
|
|
115
|
-
*/
|
|
116
|
-
async initialize() {
|
|
117
|
-
// Keep empty - ready() must be called in start() after plugins are registered
|
|
118
|
-
// This is a Fastify constraint: plugins must be registered before ready()
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Sets up request context decorator
|
|
122
|
-
*
|
|
123
|
-
* Adds `request.context` property to all requests via onRequest hook
|
|
106
|
+
* Async initialization hook for factory function.
|
|
107
|
+
* Empty because Fastify's ready() must be called in start() after plugin registration.
|
|
124
108
|
*
|
|
125
109
|
* @internal
|
|
126
110
|
*/
|
|
127
|
-
|
|
128
|
-
|
|
111
|
+
async initialize() {
|
|
112
|
+
// Intentionally empty - ready() is called in start()
|
|
129
113
|
}
|
|
130
114
|
/**
|
|
131
115
|
* Sets up global error handling
|
|
@@ -137,8 +121,7 @@ export class VeloxApp {
|
|
|
137
121
|
_setupErrorHandling() {
|
|
138
122
|
this._server.setErrorHandler(async (error, request, reply) => {
|
|
139
123
|
try {
|
|
140
|
-
|
|
141
|
-
if (error instanceof Error && error.name === 'ZodError' && 'issues' in error) {
|
|
124
|
+
if (error.name === 'ZodError' && 'issues' in error) {
|
|
142
125
|
const zodError = error;
|
|
143
126
|
return reply.status(400).send({
|
|
144
127
|
error: 'ValidationError',
|
|
@@ -150,9 +133,7 @@ export class VeloxApp {
|
|
|
150
133
|
})),
|
|
151
134
|
});
|
|
152
135
|
}
|
|
153
|
-
|
|
154
|
-
if (error instanceof Error &&
|
|
155
|
-
error.name === 'PrismaClientKnownRequestError' &&
|
|
136
|
+
if (error.name === 'PrismaClientKnownRequestError' &&
|
|
156
137
|
'code' in error &&
|
|
157
138
|
error.code === 'P2002') {
|
|
158
139
|
const prismaError = error;
|
|
@@ -163,22 +144,21 @@ export class VeloxApp {
|
|
|
163
144
|
statusCode: 409,
|
|
164
145
|
});
|
|
165
146
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
147
|
+
let statusCode = 500;
|
|
148
|
+
if (isVeloxError(error)) {
|
|
149
|
+
statusCode = error.statusCode;
|
|
150
|
+
}
|
|
151
|
+
else if (typeof error === 'object' && error !== null && 'statusCode' in error) {
|
|
152
|
+
statusCode = error.statusCode;
|
|
153
|
+
}
|
|
172
154
|
if (statusCode >= 500) {
|
|
173
155
|
request.log.error(error);
|
|
174
156
|
}
|
|
175
|
-
// Handle VeloxError instances (fast path - most common case)
|
|
176
157
|
if (isVeloxError(error)) {
|
|
177
158
|
return reply.status(error.statusCode).send(error.toJSON());
|
|
178
159
|
}
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
const name = error instanceof Error ? error.name : 'Error';
|
|
160
|
+
const message = error.message ?? 'Internal Server Error';
|
|
161
|
+
const name = error.name ?? 'Error';
|
|
182
162
|
return reply.status(statusCode).send({
|
|
183
163
|
error: name,
|
|
184
164
|
message,
|
|
@@ -186,8 +166,7 @@ export class VeloxApp {
|
|
|
186
166
|
});
|
|
187
167
|
}
|
|
188
168
|
catch (handlerError) {
|
|
189
|
-
|
|
190
|
-
console.error('Critical error in error handler:', handlerError);
|
|
169
|
+
log.error('Critical error in error handler:', handlerError);
|
|
191
170
|
if (!reply.sent) {
|
|
192
171
|
return reply.status(500).send({
|
|
193
172
|
error: 'InternalServerError',
|
|
@@ -198,16 +177,6 @@ export class VeloxApp {
|
|
|
198
177
|
}
|
|
199
178
|
});
|
|
200
179
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Sets up graceful shutdown handlers for process signals
|
|
203
|
-
*
|
|
204
|
-
* @internal
|
|
205
|
-
*/
|
|
206
|
-
_setupGracefulShutdown() {
|
|
207
|
-
this._lifecycle.setupSignalHandlers(async () => {
|
|
208
|
-
await this.stop();
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
180
|
/**
|
|
212
181
|
* Registers a plugin with the application
|
|
213
182
|
*
|
|
@@ -237,17 +206,13 @@ export class VeloxApp {
|
|
|
237
206
|
* ```
|
|
238
207
|
*/
|
|
239
208
|
async register(plugin, options) {
|
|
240
|
-
// Handle VeloxPlugin objects (with name, version, register)
|
|
241
209
|
if (isVeloxPlugin(plugin)) {
|
|
242
|
-
// Validate plugin metadata
|
|
243
210
|
validatePluginMetadata(plugin);
|
|
244
|
-
// Wrap plugin with fastify-plugin for proper encapsulation
|
|
245
211
|
const wrappedPlugin = fp(plugin.register, {
|
|
246
212
|
name: plugin.name,
|
|
247
213
|
dependencies: plugin.dependencies,
|
|
248
214
|
fastify: '5.x',
|
|
249
215
|
});
|
|
250
|
-
// Register with Fastify
|
|
251
216
|
try {
|
|
252
217
|
await this._server.register(wrappedPlugin, options ?? {});
|
|
253
218
|
}
|
|
@@ -256,7 +221,6 @@ export class VeloxApp {
|
|
|
256
221
|
}
|
|
257
222
|
return;
|
|
258
223
|
}
|
|
259
|
-
// Handle FastifyPluginAsync functions (standard Fastify plugins)
|
|
260
224
|
if (isFastifyPlugin(plugin)) {
|
|
261
225
|
try {
|
|
262
226
|
await this._server.register(plugin, options ?? {});
|
|
@@ -266,7 +230,6 @@ export class VeloxApp {
|
|
|
266
230
|
}
|
|
267
231
|
return;
|
|
268
232
|
}
|
|
269
|
-
// Invalid plugin type
|
|
270
233
|
throw new VeloxError('Invalid plugin: must be a VeloxPlugin object or FastifyPluginAsync function', 500, 'INVALID_PLUGIN_TYPE');
|
|
271
234
|
}
|
|
272
235
|
/**
|
|
@@ -354,16 +317,13 @@ export class VeloxApp {
|
|
|
354
317
|
}
|
|
355
318
|
const startTime = performance.now();
|
|
356
319
|
try {
|
|
357
|
-
// Ensure Fastify is ready before listening (must be after plugin registration)
|
|
358
320
|
await this._server.ready();
|
|
359
|
-
// Start listening
|
|
360
321
|
const address = await this._server.listen({
|
|
361
322
|
port: this._config.port,
|
|
362
323
|
host: this._config.host,
|
|
363
324
|
});
|
|
364
325
|
this._isRunning = true;
|
|
365
326
|
this._address = address;
|
|
366
|
-
// Print startup banner unless silent
|
|
367
327
|
if (!options.silent) {
|
|
368
328
|
printBanner(this._server, {
|
|
369
329
|
address,
|
|
@@ -397,11 +357,8 @@ export class VeloxApp {
|
|
|
397
357
|
throw new VeloxError('Server is not running', 500, 'SERVER_NOT_RUNNING');
|
|
398
358
|
}
|
|
399
359
|
try {
|
|
400
|
-
// Execute shutdown handlers
|
|
401
360
|
await this._lifecycle.executeShutdownHandlers();
|
|
402
|
-
// Clean up signal handlers to prevent memory leaks in tests
|
|
403
361
|
this._lifecycle.cleanupSignalHandlers();
|
|
404
|
-
// Close server
|
|
405
362
|
await this._server.close();
|
|
406
363
|
this._isRunning = false;
|
|
407
364
|
this._address = null;
|
|
@@ -483,5 +440,6 @@ export async function veloxApp(config = {}) {
|
|
|
483
440
|
*
|
|
484
441
|
* const app = await velox({ port: 3030 });
|
|
485
442
|
* ```
|
|
443
|
+
* @deprecated Use veloxApp() instead.
|
|
486
444
|
*/
|
|
487
445
|
export const velox = veloxApp;
|
package/dist/context.js
CHANGED
|
@@ -38,17 +38,13 @@ export function createContext(request, reply) {
|
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
40
|
export function isContext(value) {
|
|
41
|
-
// Early return for non-objects
|
|
42
41
|
if (typeof value !== 'object' || value === null) {
|
|
43
42
|
return false;
|
|
44
43
|
}
|
|
45
|
-
// Check properties exist using 'in' operator
|
|
46
44
|
if (!('request' in value) || !('reply' in value)) {
|
|
47
45
|
return false;
|
|
48
46
|
}
|
|
49
|
-
// After 'in' checks, safely access properties
|
|
50
47
|
const ctx = value;
|
|
51
|
-
// Verify request and reply are non-null objects
|
|
52
48
|
return (typeof ctx.request === 'object' &&
|
|
53
49
|
ctx.request !== null &&
|
|
54
50
|
typeof ctx.reply === 'object' &&
|
|
@@ -65,12 +61,7 @@ export function isContext(value) {
|
|
|
65
61
|
* @internal
|
|
66
62
|
*/
|
|
67
63
|
export function setupContextHook(server) {
|
|
68
|
-
// Create context for each request via direct assignment
|
|
69
|
-
// TypeScript's declaration merging provides type safety
|
|
70
64
|
server.addHook('onRequest', async (request, reply) => {
|
|
71
|
-
// Direct assignment is ~100-400ns faster than Object.defineProperty
|
|
72
|
-
// We use a mutable type assertion here because this is the framework's
|
|
73
|
-
// initialization code - the readonly constraint is for user code safety
|
|
74
65
|
request.context = createContext(request, reply);
|
|
75
66
|
});
|
|
76
67
|
}
|
package/dist/errors/fail.d.ts
CHANGED
|
@@ -33,6 +33,17 @@ export type ErrorCode = keyof typeof ERROR_CATALOG;
|
|
|
33
33
|
* Variables for template interpolation
|
|
34
34
|
*/
|
|
35
35
|
export type InterpolationVars = Record<string, string | number | boolean | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* JSON representation of a VeloxFailure for API responses
|
|
38
|
+
*/
|
|
39
|
+
interface VeloxFailureJson {
|
|
40
|
+
error: string;
|
|
41
|
+
message: string;
|
|
42
|
+
statusCode: number;
|
|
43
|
+
code: string;
|
|
44
|
+
fix?: string;
|
|
45
|
+
docs?: string;
|
|
46
|
+
}
|
|
36
47
|
/**
|
|
37
48
|
* Fluent error builder that provides catalog-driven errors
|
|
38
49
|
* with optional customization.
|
|
@@ -111,14 +122,7 @@ export declare class VeloxFailure extends Error {
|
|
|
111
122
|
/**
|
|
112
123
|
* Convert to JSON for API responses
|
|
113
124
|
*/
|
|
114
|
-
toJSON():
|
|
115
|
-
error: string;
|
|
116
|
-
message: string;
|
|
117
|
-
statusCode: number;
|
|
118
|
-
code: string;
|
|
119
|
-
fix?: string;
|
|
120
|
-
docs?: string;
|
|
121
|
-
};
|
|
125
|
+
toJSON(): VeloxFailureJson;
|
|
122
126
|
/**
|
|
123
127
|
* Interpolate variables into a template string
|
|
124
128
|
*/
|
|
@@ -161,3 +165,4 @@ export declare function fail(code: string, vars?: InterpolationVars): VeloxFailu
|
|
|
161
165
|
* @returns true if error is a VeloxFailure
|
|
162
166
|
*/
|
|
163
167
|
export declare function isVeloxFailure(error: unknown): error is VeloxFailure;
|
|
168
|
+
export {};
|
package/dist/errors/fail.js
CHANGED
|
@@ -54,7 +54,6 @@ export class VeloxFailure extends Error {
|
|
|
54
54
|
this.code = code;
|
|
55
55
|
this.statusCode = entry.statusCode;
|
|
56
56
|
this.entry = entry;
|
|
57
|
-
// Maintains proper stack trace
|
|
58
57
|
if (Error.captureStackTrace) {
|
|
59
58
|
Error.captureStackTrace(this, VeloxFailure);
|
|
60
59
|
}
|
|
@@ -142,11 +141,9 @@ export class VeloxFailure extends Error {
|
|
|
142
141
|
statusCode: this.statusCode,
|
|
143
142
|
code: this.code,
|
|
144
143
|
};
|
|
145
|
-
// Include fix in development
|
|
146
144
|
if (process.env.NODE_ENV !== 'production' && this.suggestion) {
|
|
147
145
|
json.fix = this.suggestion;
|
|
148
146
|
}
|
|
149
|
-
// Always include docs URL
|
|
150
147
|
if (this.docsUrl) {
|
|
151
148
|
json.docs = this.docsUrl;
|
|
152
149
|
}
|
package/dist/errors/formatter.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module errors/formatter
|
|
8
8
|
*/
|
|
9
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
10
|
import { getErrorEntry } from './catalog.js';
|
|
11
|
+
const log = createLogger('core');
|
|
10
12
|
// ============================================================================
|
|
11
13
|
// ANSI Color Codes (for terminal output)
|
|
12
14
|
// ============================================================================
|
|
@@ -260,15 +262,12 @@ export function formatErrorForApi(error, catalogCode) {
|
|
|
260
262
|
statusCode: error.statusCode ?? entry?.statusCode ?? 500,
|
|
261
263
|
code: catalogCode ?? error.code,
|
|
262
264
|
};
|
|
263
|
-
// Include field errors for validation
|
|
264
265
|
if (error.fields) {
|
|
265
266
|
response.fields = error.fields;
|
|
266
267
|
}
|
|
267
|
-
// Include fix suggestion in development
|
|
268
268
|
if (process.env.NODE_ENV !== 'production' && entry?.fix) {
|
|
269
269
|
response.fix = entry.fix.suggestion;
|
|
270
270
|
}
|
|
271
|
-
// Include docs link
|
|
272
271
|
if (entry?.docsUrl) {
|
|
273
272
|
response.docs = entry.docsUrl;
|
|
274
273
|
}
|
|
@@ -298,7 +297,7 @@ export function formatErrorOneLine(error, catalogCode) {
|
|
|
298
297
|
* @param catalogCode - Optional catalog error code
|
|
299
298
|
*/
|
|
300
299
|
export function logError(error, catalogCode) {
|
|
301
|
-
|
|
300
|
+
log.error(formatError(error, catalogCode));
|
|
302
301
|
}
|
|
303
302
|
/**
|
|
304
303
|
* Log a warning with pretty formatting
|
|
@@ -314,7 +313,7 @@ export function logWarning(message, suggestion) {
|
|
|
314
313
|
lines.push(` ${color('→', 'gray')} ${suggestion}`);
|
|
315
314
|
}
|
|
316
315
|
lines.push('');
|
|
317
|
-
|
|
316
|
+
log.warn(lines.join('\n'));
|
|
318
317
|
}
|
|
319
318
|
/**
|
|
320
319
|
* Log a deprecation warning
|
package/dist/errors/index.js
CHANGED
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module errors
|
|
11
11
|
*/
|
|
12
|
-
// Re-export catalog
|
|
13
12
|
export { ERROR_CATALOG, ERROR_DOMAINS, getDocsUrl, getErrorEntry, getErrorsByDomain, isKnownErrorCode, } from './catalog.js';
|
|
14
|
-
// Re-export fail() - the elegant error creation API
|
|
15
13
|
export { fail, isVeloxFailure, VeloxFailure, } from './fail.js';
|
|
16
|
-
// Re-export formatter
|
|
17
14
|
export { extractErrorLocation, formatError, formatErrorForApi, formatErrorOneLine, logDeprecation, logError, logWarning, } from './formatter.js';
|
package/dist/errors.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Provides base error classes with HTTP status codes and discriminated unions
|
|
4
4
|
* @module errors
|
|
5
5
|
*/
|
|
6
|
-
// Import catalog for use in error classes
|
|
7
6
|
import { ERROR_CATALOG } from './errors/catalog.js';
|
|
8
7
|
import { formatError as _formatError } from './errors/formatter.js';
|
|
9
|
-
|
|
8
|
+
import { createLogger } from './utils/logger.js';
|
|
9
|
+
const log = createLogger('core');
|
|
10
10
|
export { ERROR_CATALOG, ERROR_DOMAINS, extractErrorLocation, fail, formatError, formatErrorForApi, formatErrorOneLine, getDocsUrl, getErrorEntry, getErrorsByDomain, isKnownErrorCode, isVeloxFailure, logDeprecation, logError, logWarning, VeloxFailure, } from './errors/index.js';
|
|
11
11
|
/**
|
|
12
12
|
* Type guard for validation error responses
|
|
@@ -79,7 +79,6 @@ export class VeloxError extends Error {
|
|
|
79
79
|
this.name = 'VeloxError';
|
|
80
80
|
this.statusCode = statusCode;
|
|
81
81
|
this.code = code;
|
|
82
|
-
// Look up catalog entry for enhanced error info
|
|
83
82
|
if (code != null && String(code).startsWith('VELOX-')) {
|
|
84
83
|
const entry = ERROR_CATALOG[code];
|
|
85
84
|
if (entry) {
|
|
@@ -87,7 +86,6 @@ export class VeloxError extends Error {
|
|
|
87
86
|
this.docsUrl = entry.docsUrl;
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
|
-
// Maintains proper stack trace for where error was thrown (V8 only)
|
|
91
89
|
if (Error.captureStackTrace) {
|
|
92
90
|
Error.captureStackTrace(this, VeloxError);
|
|
93
91
|
}
|
|
@@ -104,11 +102,9 @@ export class VeloxError extends Error {
|
|
|
104
102
|
statusCode: this.statusCode,
|
|
105
103
|
code: this.code,
|
|
106
104
|
};
|
|
107
|
-
// Include fix suggestion in development only
|
|
108
105
|
if (process.env.NODE_ENV !== 'production' && this.fix) {
|
|
109
106
|
response.fix = this.fix;
|
|
110
107
|
}
|
|
111
|
-
// Always include docs URL if available
|
|
112
108
|
if (this.docsUrl) {
|
|
113
109
|
response.docs = this.docsUrl;
|
|
114
110
|
}
|
|
@@ -126,7 +122,7 @@ export class VeloxError extends Error {
|
|
|
126
122
|
* Log this error with pretty formatting
|
|
127
123
|
*/
|
|
128
124
|
log() {
|
|
129
|
-
|
|
125
|
+
log.error(this.format());
|
|
130
126
|
}
|
|
131
127
|
}
|
|
132
128
|
/**
|
|
@@ -296,7 +292,7 @@ export class NotFoundError extends VeloxError {
|
|
|
296
292
|
* ```
|
|
297
293
|
*/
|
|
298
294
|
export function isVeloxError(error) {
|
|
299
|
-
return error
|
|
295
|
+
return error instanceof VeloxError;
|
|
300
296
|
}
|
|
301
297
|
/**
|
|
302
298
|
* Type guard to check if an error is a ValidationError
|
package/dist/index.d.ts
CHANGED
|
@@ -32,3 +32,5 @@ export type { AuthContextExtension, CombineContexts, ContextExtension, CoreConte
|
|
|
32
32
|
export type { CacheControl, StaticOptions } from './plugins/static.js';
|
|
33
33
|
export { registerStatic } from './plugins/static.js';
|
|
34
34
|
export { requestLogger } from './plugins/request-logger.js';
|
|
35
|
+
export type { Logger, LogLevel } from './utils/logger.js';
|
|
36
|
+
export { createLogger } from './utils/logger.js';
|
package/dist/index.js
CHANGED
|
@@ -15,19 +15,13 @@
|
|
|
15
15
|
* @module @veloxts/core
|
|
16
16
|
*/
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
|
-
// Read version from package.json dynamically
|
|
19
18
|
const require = createRequire(import.meta.url);
|
|
20
19
|
const packageJson = require('../package.json');
|
|
21
20
|
/** VeloxTS framework version */
|
|
22
21
|
export const VELOX_VERSION = packageJson.version ?? '0.0.0-unknown';
|
|
23
22
|
export { VeloxApp, velox, veloxApp } from './app.js';
|
|
24
23
|
export { createContext, isContext, setupContextHook, setupTestContext } from './context.js';
|
|
25
|
-
export { assertNever, ConfigurationError,
|
|
26
|
-
// Elegant error creation API
|
|
27
|
-
fail, isConfigurationError, isNotFoundError, isNotFoundErrorResponse, isValidationError, isValidationErrorResponse, isVeloxError, isVeloxFailure,
|
|
28
|
-
// Developer experience utilities
|
|
29
|
-
logDeprecation, logWarning, NotFoundError, ValidationError, VeloxError, VeloxFailure, } from './errors.js';
|
|
30
|
-
// Plugin system
|
|
24
|
+
export { assertNever, ConfigurationError, fail, isConfigurationError, isNotFoundError, isNotFoundErrorResponse, isValidationError, isValidationErrorResponse, isVeloxError, isVeloxFailure, logDeprecation, logWarning, NotFoundError, ValidationError, VeloxError, VeloxFailure, } from './errors.js';
|
|
31
25
|
export { definePlugin, isFastifyPlugin, isVeloxPlugin, validatePluginMetadata } from './plugin.js';
|
|
32
26
|
export { isValidHost, isValidPort } from './utils/config.js';
|
|
33
27
|
export { registerStatic } from './plugins/static.js';
|
|
@@ -35,3 +29,4 @@ export { registerStatic } from './plugins/static.js';
|
|
|
35
29
|
// Request Logging (Development)
|
|
36
30
|
// ============================================================================
|
|
37
31
|
export { requestLogger } from './plugins/request-logger.js';
|
|
32
|
+
export { createLogger } from './utils/logger.js';
|
package/dist/plugin.js
CHANGED
|
@@ -78,7 +78,6 @@ export function validatePluginMetadata(plugin) {
|
|
|
78
78
|
if (!plugin.register || typeof plugin.register !== 'function') {
|
|
79
79
|
throw new VeloxError(`Plugin "${plugin.name}" must have a register function`, 500, 'INVALID_PLUGIN_METADATA');
|
|
80
80
|
}
|
|
81
|
-
// Validate dependencies array if provided
|
|
82
81
|
if (plugin.dependencies !== undefined) {
|
|
83
82
|
if (!Array.isArray(plugin.dependencies)) {
|
|
84
83
|
throw new VeloxError(`Plugin "${plugin.name}" dependencies must be an array`, 500, 'INVALID_PLUGIN_METADATA');
|
|
@@ -110,7 +109,6 @@ export function isVeloxPlugin(value) {
|
|
|
110
109
|
if (typeof value !== 'object' || value === null) {
|
|
111
110
|
return false;
|
|
112
111
|
}
|
|
113
|
-
// Use 'in' operator for safe property access without type assertions
|
|
114
112
|
return ('name' in value &&
|
|
115
113
|
'version' in value &&
|
|
116
114
|
'register' in value &&
|
|
@@ -93,17 +93,13 @@ function formatDuration(ms) {
|
|
|
93
93
|
* @param server - Fastify instance
|
|
94
94
|
*/
|
|
95
95
|
async function requestLoggerPlugin(server) {
|
|
96
|
-
// Skip if request logging not enabled via environment
|
|
97
96
|
if (process.env.VELOX_REQUEST_LOGGING !== 'true') {
|
|
98
97
|
return;
|
|
99
98
|
}
|
|
100
|
-
// Track request start time
|
|
101
99
|
server.addHook('onRequest', async (request) => {
|
|
102
100
|
request._veloxStartTime = performance.now();
|
|
103
101
|
});
|
|
104
|
-
// Log on response with timing
|
|
105
102
|
server.addHook('onResponse', async (request, reply) => {
|
|
106
|
-
// Wrap in try-catch to ensure logging never breaks request handling
|
|
107
103
|
try {
|
|
108
104
|
const startTime = request._veloxStartTime;
|
|
109
105
|
const duration = startTime ? performance.now() - startTime : 0;
|
|
@@ -115,7 +111,7 @@ async function requestLoggerPlugin(server) {
|
|
|
115
111
|
console.log(`${COLORS.dim}${timestamp}${COLORS.reset} ${color}${method}${COLORS.reset} ${url} ${color}${status}${COLORS.reset} ${COLORS.dim}${formatDuration(duration)}${COLORS.reset}`);
|
|
116
112
|
}
|
|
117
113
|
catch {
|
|
118
|
-
//
|
|
114
|
+
// Logging failures must not affect request handling
|
|
119
115
|
}
|
|
120
116
|
});
|
|
121
117
|
}
|
package/dist/plugins/static.js
CHANGED
|
@@ -83,9 +83,7 @@ function buildCacheControl(options) {
|
|
|
83
83
|
*/
|
|
84
84
|
export async function registerStatic(server, path, options = {}) {
|
|
85
85
|
const { spa = false, prefix = '/', cache = {}, exclude = [], index = 'index.html' } = options;
|
|
86
|
-
// Resolve to absolute path
|
|
87
86
|
const absoluteRoot = resolve(process.cwd(), path);
|
|
88
|
-
// Dynamically import @fastify/static (optional peer dependency)
|
|
89
87
|
let fastifyStatic;
|
|
90
88
|
try {
|
|
91
89
|
const module = await import('@fastify/static');
|
|
@@ -94,9 +92,7 @@ export async function registerStatic(server, path, options = {}) {
|
|
|
94
92
|
catch {
|
|
95
93
|
throw new Error('To serve static files, please install @fastify/static:\n\n pnpm add @fastify/static');
|
|
96
94
|
}
|
|
97
|
-
// Build cache control header
|
|
98
95
|
const cacheControl = buildCacheControl(cache);
|
|
99
|
-
// Register static plugin
|
|
100
96
|
await server.register(fastifyStatic, {
|
|
101
97
|
root: absoluteRoot,
|
|
102
98
|
prefix,
|
|
@@ -106,10 +102,8 @@ export async function registerStatic(server, path, options = {}) {
|
|
|
106
102
|
res.setHeader('Cache-Control', cacheControl);
|
|
107
103
|
},
|
|
108
104
|
});
|
|
109
|
-
// SPA fallback: serve index.html for non-file routes
|
|
110
105
|
if (spa) {
|
|
111
106
|
server.setNotFoundHandler(async (request, reply) => {
|
|
112
|
-
// Skip excluded paths (API routes, etc.)
|
|
113
107
|
for (const excludePath of exclude) {
|
|
114
108
|
if (request.url.startsWith(excludePath)) {
|
|
115
109
|
return reply.status(404).send({
|
|
@@ -119,7 +113,6 @@ export async function registerStatic(server, path, options = {}) {
|
|
|
119
113
|
});
|
|
120
114
|
}
|
|
121
115
|
}
|
|
122
|
-
// Skip requests for files (have extensions)
|
|
123
116
|
if (/\.\w+$/.test(request.url)) {
|
|
124
117
|
return reply.status(404).send({
|
|
125
118
|
error: 'NotFound',
|
|
@@ -127,7 +120,6 @@ export async function registerStatic(server, path, options = {}) {
|
|
|
127
120
|
statusCode: 404,
|
|
128
121
|
});
|
|
129
122
|
}
|
|
130
|
-
// Serve index.html for SPA routes
|
|
131
123
|
return reply.sendFile(index, absoluteRoot);
|
|
132
124
|
});
|
|
133
125
|
}
|
package/dist/utils/banner.js
CHANGED
|
@@ -17,11 +17,9 @@ export function printBanner(server, options) {
|
|
|
17
17
|
const { address, env, startTime } = options;
|
|
18
18
|
const elapsed = Math.round(performance.now() - startTime);
|
|
19
19
|
if (env === 'production') {
|
|
20
|
-
// Production: single line, minimal, machine-parseable
|
|
21
20
|
console.log(` VeloxTS v${VELOX_VERSION} | ${env} | ${address}`);
|
|
22
21
|
return;
|
|
23
22
|
}
|
|
24
|
-
// Development: detailed banner with route information
|
|
25
23
|
const routes = collectRoutes(server);
|
|
26
24
|
console.log('');
|
|
27
25
|
console.log(` ${pc.bold('VeloxTS')} v${VELOX_VERSION}`);
|
|
@@ -62,11 +60,7 @@ function formatMethod(method) {
|
|
|
62
60
|
*/
|
|
63
61
|
function collectRoutes(server) {
|
|
64
62
|
const routes = [];
|
|
65
|
-
// Fastify exposes routes via printRoutes or we can iterate
|
|
66
|
-
// Using the internal routes map after ready()
|
|
67
63
|
const routesList = server.printRoutes({ commonPrefix: false });
|
|
68
|
-
// Parse the route tree output
|
|
69
|
-
// Format: "└── /api (GET, HEAD)\n └── /users (GET, POST, HEAD)"
|
|
70
64
|
const lines = routesList.split('\n');
|
|
71
65
|
for (const line of lines) {
|
|
72
66
|
const match = line.match(/([/][^\s(]*)\s*\(([^)]+)\)/);
|
package/dist/utils/config.js
CHANGED
|
@@ -38,7 +38,7 @@ export function isValidPort(port) {
|
|
|
38
38
|
* @returns true if host is valid (non-empty string), narrowing to ValidHost
|
|
39
39
|
*/
|
|
40
40
|
export function isValidHost(host) {
|
|
41
|
-
return
|
|
41
|
+
return host.length > 0;
|
|
42
42
|
}
|
|
43
43
|
// ============================================================================
|
|
44
44
|
// Configuration Functions
|
|
@@ -88,10 +88,9 @@ export function validateConfig(config) {
|
|
|
88
88
|
if (!isValidHost(config.host)) {
|
|
89
89
|
throw new Error('Host must be a non-empty string.');
|
|
90
90
|
}
|
|
91
|
-
// Return frozen config with branded types
|
|
92
91
|
return Object.freeze({
|
|
93
|
-
port: config.port,
|
|
94
|
-
host: config.host,
|
|
92
|
+
port: config.port,
|
|
93
|
+
host: config.host,
|
|
95
94
|
logger: config.logger,
|
|
96
95
|
fastify: config.fastify,
|
|
97
96
|
});
|
package/dist/utils/lifecycle.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Handles graceful shutdown and cleanup
|
|
4
4
|
* @module utils/lifecycle
|
|
5
5
|
*/
|
|
6
|
+
import { createLogger } from './logger.js';
|
|
7
|
+
const log = createLogger('core');
|
|
6
8
|
/**
|
|
7
9
|
* Manages graceful shutdown for the VeloxTS application
|
|
8
10
|
*
|
|
@@ -32,12 +34,10 @@ export class LifecycleManager {
|
|
|
32
34
|
* ```
|
|
33
35
|
*/
|
|
34
36
|
addShutdownHandler(handler) {
|
|
35
|
-
// Prevent memory leaks from unbounded growth
|
|
36
37
|
if (this.shutdownHandlers.size >= LifecycleManager.MAX_SHUTDOWN_HANDLERS) {
|
|
37
38
|
throw new Error(`Maximum number of shutdown handlers (${LifecycleManager.MAX_SHUTDOWN_HANDLERS}) exceeded. ` +
|
|
38
39
|
'This may indicate a memory leak.');
|
|
39
40
|
}
|
|
40
|
-
// Set automatically prevents duplicates
|
|
41
41
|
this.shutdownHandlers.add(handler);
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
@@ -68,7 +68,7 @@ export class LifecycleManager {
|
|
|
68
68
|
}
|
|
69
69
|
catch (error) {
|
|
70
70
|
// Log error but continue with other handlers
|
|
71
|
-
|
|
71
|
+
log.error('Error during shutdown handler execution:', error);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
this.isShuttingDown = false;
|
|
@@ -85,23 +85,22 @@ export class LifecycleManager {
|
|
|
85
85
|
*/
|
|
86
86
|
setupSignalHandlers(onShutdown) {
|
|
87
87
|
const signals = ['SIGINT', 'SIGTERM'];
|
|
88
|
-
|
|
88
|
+
for (const signal of signals) {
|
|
89
89
|
const handler = async () => {
|
|
90
|
-
|
|
90
|
+
log.info(`\nReceived ${signal}, initiating graceful shutdown...`);
|
|
91
91
|
try {
|
|
92
92
|
await onShutdown();
|
|
93
|
-
|
|
93
|
+
log.info('Graceful shutdown completed');
|
|
94
94
|
process.exit(0);
|
|
95
95
|
}
|
|
96
96
|
catch (error) {
|
|
97
|
-
|
|
97
|
+
log.error('Error during graceful shutdown:', error);
|
|
98
98
|
process.exit(1);
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
|
-
// Store handler reference so it can be removed later
|
|
102
101
|
this.signalHandlers.set(signal, handler);
|
|
103
102
|
process.once(signal, handler);
|
|
104
|
-
}
|
|
103
|
+
}
|
|
105
104
|
}
|
|
106
105
|
/**
|
|
107
106
|
* Removes all signal handlers
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Logger
|
|
3
|
+
*
|
|
4
|
+
* Minimal logging utility that respects VELOX_LOG_LEVEL environment variable.
|
|
5
|
+
* Replaces raw console.* calls in library code with namespaced, level-aware logging.
|
|
6
|
+
*/
|
|
7
|
+
/** Supported log levels, ordered by verbosity. */
|
|
8
|
+
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
|
|
9
|
+
/** Logger instance returned by createLogger. */
|
|
10
|
+
export interface Logger {
|
|
11
|
+
debug: (...args: unknown[]) => void;
|
|
12
|
+
info: (...args: unknown[]) => void;
|
|
13
|
+
warn: (...args: unknown[]) => void;
|
|
14
|
+
error: (...args: unknown[]) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create a namespaced logger that respects VELOX_LOG_LEVEL.
|
|
18
|
+
*
|
|
19
|
+
* @param namespace - Logger namespace (e.g. 'router', 'auth')
|
|
20
|
+
* @returns Logger with debug/info/warn/error methods
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { createLogger } from '@veloxts/core';
|
|
25
|
+
*
|
|
26
|
+
* const log = createLogger('router');
|
|
27
|
+
* log.info('Route registered:', method, path);
|
|
28
|
+
* log.warn('Deprecated route pattern detected');
|
|
29
|
+
* log.error('Failed to register route:', error);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function createLogger(namespace: string): Logger;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Logger
|
|
3
|
+
*
|
|
4
|
+
* Minimal logging utility that respects VELOX_LOG_LEVEL environment variable.
|
|
5
|
+
* Replaces raw console.* calls in library code with namespaced, level-aware logging.
|
|
6
|
+
*/
|
|
7
|
+
const LEVEL_PRIORITY = {
|
|
8
|
+
silent: 0,
|
|
9
|
+
error: 1,
|
|
10
|
+
warn: 2,
|
|
11
|
+
info: 3,
|
|
12
|
+
debug: 4,
|
|
13
|
+
};
|
|
14
|
+
function getLogLevel() {
|
|
15
|
+
const env = (process.env.VELOX_LOG_LEVEL ?? 'warn').toLowerCase();
|
|
16
|
+
if (env in LEVEL_PRIORITY)
|
|
17
|
+
return env;
|
|
18
|
+
return 'warn';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a namespaced logger that respects VELOX_LOG_LEVEL.
|
|
22
|
+
*
|
|
23
|
+
* @param namespace - Logger namespace (e.g. 'router', 'auth')
|
|
24
|
+
* @returns Logger with debug/info/warn/error methods
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { createLogger } from '@veloxts/core';
|
|
29
|
+
*
|
|
30
|
+
* const log = createLogger('router');
|
|
31
|
+
* log.info('Route registered:', method, path);
|
|
32
|
+
* log.warn('Deprecated route pattern detected');
|
|
33
|
+
* log.error('Failed to register route:', error);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createLogger(namespace) {
|
|
37
|
+
const prefix = `[@veloxts/${namespace}]`;
|
|
38
|
+
function shouldLog(level) {
|
|
39
|
+
return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[getLogLevel()];
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
debug: (...args) => {
|
|
43
|
+
if (shouldLog('debug'))
|
|
44
|
+
console.debug(prefix, ...args);
|
|
45
|
+
},
|
|
46
|
+
info: (...args) => {
|
|
47
|
+
if (shouldLog('info'))
|
|
48
|
+
console.info(prefix, ...args);
|
|
49
|
+
},
|
|
50
|
+
warn: (...args) => {
|
|
51
|
+
if (shouldLog('warn'))
|
|
52
|
+
console.warn(prefix, ...args);
|
|
53
|
+
},
|
|
54
|
+
error: (...args) => {
|
|
55
|
+
if (shouldLog('error'))
|
|
56
|
+
console.error(prefix, ...args);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veloxts/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Fastify wrapper and plugin system for VeloxTS framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@fastify/static": "9.0.0",
|
|
46
|
-
"@types/node": "25.
|
|
46
|
+
"@types/node": "25.2.3",
|
|
47
47
|
"@vitest/coverage-v8": "4.0.18",
|
|
48
48
|
"typescript": "5.9.3",
|
|
49
49
|
"vitest": "4.0.18"
|