vitek-plugin 0.1.0-beta
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/LICENSE +21 -0
- package/README.md +908 -0
- package/dist/adapters/vite/dev-server.d.ts +23 -0
- package/dist/adapters/vite/dev-server.d.ts.map +1 -0
- package/dist/adapters/vite/dev-server.js +428 -0
- package/dist/adapters/vite/logger.d.ts +33 -0
- package/dist/adapters/vite/logger.d.ts.map +1 -0
- package/dist/adapters/vite/logger.js +112 -0
- package/dist/core/context/create-context.d.ts +39 -0
- package/dist/core/context/create-context.d.ts.map +1 -0
- package/dist/core/context/create-context.js +34 -0
- package/dist/core/file-system/scan-api-dir.d.ts +26 -0
- package/dist/core/file-system/scan-api-dir.d.ts.map +1 -0
- package/dist/core/file-system/scan-api-dir.js +83 -0
- package/dist/core/file-system/watch-api-dir.d.ts +18 -0
- package/dist/core/file-system/watch-api-dir.d.ts.map +1 -0
- package/dist/core/file-system/watch-api-dir.js +40 -0
- package/dist/core/middleware/compose.d.ts +12 -0
- package/dist/core/middleware/compose.d.ts.map +1 -0
- package/dist/core/middleware/compose.js +27 -0
- package/dist/core/middleware/get-applicable-middlewares.d.ts +17 -0
- package/dist/core/middleware/get-applicable-middlewares.d.ts.map +1 -0
- package/dist/core/middleware/get-applicable-middlewares.js +47 -0
- package/dist/core/middleware/load-global.d.ts +13 -0
- package/dist/core/middleware/load-global.d.ts.map +1 -0
- package/dist/core/middleware/load-global.js +37 -0
- package/dist/core/normalize/normalize-path.d.ts +25 -0
- package/dist/core/normalize/normalize-path.d.ts.map +1 -0
- package/dist/core/normalize/normalize-path.js +76 -0
- package/dist/core/routing/route-matcher.d.ts +10 -0
- package/dist/core/routing/route-matcher.d.ts.map +1 -0
- package/dist/core/routing/route-matcher.js +32 -0
- package/dist/core/routing/route-parser.d.ts +28 -0
- package/dist/core/routing/route-parser.d.ts.map +1 -0
- package/dist/core/routing/route-parser.js +52 -0
- package/dist/core/routing/route-types.d.ts +43 -0
- package/dist/core/routing/route-types.d.ts.map +1 -0
- package/dist/core/routing/route-types.js +5 -0
- package/dist/core/types/extract-ast.d.ts +18 -0
- package/dist/core/types/extract-ast.d.ts.map +1 -0
- package/dist/core/types/extract-ast.js +26 -0
- package/dist/core/types/generate.d.ts +22 -0
- package/dist/core/types/generate.d.ts.map +1 -0
- package/dist/core/types/generate.js +576 -0
- package/dist/core/types/schema.d.ts +21 -0
- package/dist/core/types/schema.d.ts.map +1 -0
- package/dist/core/types/schema.js +17 -0
- package/dist/core/validation/types.d.ts +27 -0
- package/dist/core/validation/types.d.ts.map +1 -0
- package/dist/core/validation/types.js +5 -0
- package/dist/core/validation/validator.d.ts +22 -0
- package/dist/core/validation/validator.d.ts.map +1 -0
- package/dist/core/validation/validator.js +131 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/plugin.d.ts +27 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +54 -0
- package/dist/shared/constants.d.ts +13 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +12 -0
- package/dist/shared/errors.d.ts +75 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +118 -0
- package/dist/shared/response-helpers.d.ts +61 -0
- package/dist/shared/response-helpers.d.ts.map +1 -0
- package/dist/shared/response-helpers.js +100 -0
- package/dist/shared/utils.d.ts +17 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +27 -0
- package/package.json +48 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter for integration with Vite development server
|
|
3
|
+
* Thin layer that connects core → Vite
|
|
4
|
+
*/
|
|
5
|
+
import type { Connect, ViteDevServer } from 'vite';
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
7
|
+
import type { VitekLogger } from './logger.js';
|
|
8
|
+
export interface ViteDevServerOptions {
|
|
9
|
+
root: string;
|
|
10
|
+
apiDir: string;
|
|
11
|
+
logger: VitekLogger;
|
|
12
|
+
viteServer: ViteDevServer;
|
|
13
|
+
enableValidation?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Creates middleware for Vite development server
|
|
17
|
+
*/
|
|
18
|
+
export declare function createViteDevServerMiddleware(options: ViteDevServerOptions): {
|
|
19
|
+
middleware: (req: IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => Promise<void>;
|
|
20
|
+
cleanup: () => void;
|
|
21
|
+
reload: () => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=dev-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../../src/adapters/vite/dev-server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAsB5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,aAAa,CAAC;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAoRD;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,oBAAoB;sBAUhE,eAAe,OACf,cAAc,QACb,OAAO,CAAC,YAAY;;;EAoL/B"}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter for integration with Vite development server
|
|
3
|
+
* Thin layer that connects core → Vite
|
|
4
|
+
*/
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { scanApiDirectory } from '../../core/file-system/scan-api-dir.js';
|
|
8
|
+
/**
|
|
9
|
+
* Detects if the project uses TypeScript by checking if tsconfig.json exists
|
|
10
|
+
*/
|
|
11
|
+
function isTypeScriptProject(root) {
|
|
12
|
+
const tsconfigPath = path.join(root, 'tsconfig.json');
|
|
13
|
+
return fs.existsSync(tsconfigPath);
|
|
14
|
+
}
|
|
15
|
+
import { watchApiDirectory } from '../../core/file-system/watch-api-dir.js';
|
|
16
|
+
import { createRoute } from '../../core/routing/route-parser.js';
|
|
17
|
+
import { matchRoute } from '../../core/routing/route-matcher.js';
|
|
18
|
+
import { compose } from '../../core/middleware/compose.js';
|
|
19
|
+
import { getApplicableMiddlewares } from '../../core/middleware/get-applicable-middlewares.js';
|
|
20
|
+
import { createContext, isVitekResponse } from '../../core/context/create-context.js';
|
|
21
|
+
import { routesToSchema } from '../../core/types/schema.js';
|
|
22
|
+
import { generateTypesFile, generateServicesFile } from '../../core/types/generate.js';
|
|
23
|
+
import { API_BASE_PATH, GENERATED_TYPES_FILE, GENERATED_SERVICES_FILE } from '../../shared/constants.js';
|
|
24
|
+
import { HttpError } from '../../shared/errors.js';
|
|
25
|
+
/**
|
|
26
|
+
* Development server state
|
|
27
|
+
*/
|
|
28
|
+
class DevServerState {
|
|
29
|
+
options;
|
|
30
|
+
routes = [];
|
|
31
|
+
middlewares = []; // Loaded hierarchical middlewares
|
|
32
|
+
watcher = null;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Initializes the server: scan, load routes and middleware
|
|
38
|
+
*/
|
|
39
|
+
async initialize() {
|
|
40
|
+
await this.reload(false); // Don't show "Reloading" on initialization
|
|
41
|
+
this.setupWatcher();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Reloads routes and middleware
|
|
45
|
+
*/
|
|
46
|
+
async reload(showReloadLog = true) {
|
|
47
|
+
if (showReloadLog) {
|
|
48
|
+
this.options.logger.info('Reloading API routes...');
|
|
49
|
+
}
|
|
50
|
+
// Scan directory
|
|
51
|
+
const scanResult = scanApiDirectory(this.options.apiDir);
|
|
52
|
+
// Load hierarchical middlewares
|
|
53
|
+
this.middlewares = [];
|
|
54
|
+
for (const middlewareInfo of scanResult.middlewares) {
|
|
55
|
+
try {
|
|
56
|
+
// Convert absolute path to relative path to Vite root (format /src/api/posts/middleware.ts)
|
|
57
|
+
const relativePath = path.relative(this.options.root, middlewareInfo.path);
|
|
58
|
+
const vitePath = `/${relativePath.replace(/\\/g, '/')}`;
|
|
59
|
+
// Use Vite's ssrLoadModule to process TypeScript
|
|
60
|
+
const middlewareModule = await this.options.viteServer.ssrLoadModule(vitePath);
|
|
61
|
+
const middleware = middlewareModule.default || middlewareModule.middleware;
|
|
62
|
+
let middlewareArray = [];
|
|
63
|
+
if (Array.isArray(middleware)) {
|
|
64
|
+
middlewareArray = middleware;
|
|
65
|
+
}
|
|
66
|
+
else if (typeof middleware === 'function') {
|
|
67
|
+
middlewareArray = [middleware];
|
|
68
|
+
}
|
|
69
|
+
if (middlewareArray.length > 0) {
|
|
70
|
+
this.middlewares.push({
|
|
71
|
+
middleware: middlewareArray,
|
|
72
|
+
basePattern: middlewareInfo.basePattern,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
this.options.logger.warn(`Failed to load middleware ${middlewareInfo.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const totalMiddlewareCount = this.middlewares.reduce((sum, m) => sum + m.middleware.length, 0);
|
|
81
|
+
this.options.logger.middlewareLoaded(totalMiddlewareCount);
|
|
82
|
+
// Load routes
|
|
83
|
+
this.routes = [];
|
|
84
|
+
for (const parsedRoute of scanResult.routes) {
|
|
85
|
+
try {
|
|
86
|
+
// Convert absolute path to relative path to Vite root (format /src/api/users/[id].get.ts)
|
|
87
|
+
const relativePath = path.relative(this.options.root, parsedRoute.file);
|
|
88
|
+
const vitePath = `/${relativePath.replace(/\\/g, '/')}`;
|
|
89
|
+
// Use Vite's ssrLoadModule to process TypeScript
|
|
90
|
+
const handlerModule = await this.options.viteServer.ssrLoadModule(vitePath);
|
|
91
|
+
const handler = handlerModule.default || handlerModule.handler || handlerModule[parsedRoute.method];
|
|
92
|
+
if (typeof handler !== 'function') {
|
|
93
|
+
this.options.logger.warn(`Route file ${parsedRoute.file} does not export a handler function`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Extract bodyType from file (looking for export type Body or export interface Body)
|
|
97
|
+
const bodyType = extractBodyTypeFromFile(parsedRoute.file);
|
|
98
|
+
// Extract queryType from file (looking for export type Query or export interface Query)
|
|
99
|
+
const queryType = extractQueryTypeFromFile(parsedRoute.file);
|
|
100
|
+
const route = createRoute(parsedRoute, handler, bodyType, queryType);
|
|
101
|
+
this.routes.push(route);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
this.options.logger.error(`Failed to load route ${parsedRoute.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Log registered routes (consolidated)
|
|
108
|
+
const routesInfo = this.routes.map(r => ({
|
|
109
|
+
method: r.method,
|
|
110
|
+
pattern: r.pattern,
|
|
111
|
+
}));
|
|
112
|
+
this.options.logger.routesRegistered(routesInfo, API_BASE_PATH);
|
|
113
|
+
// Generate types
|
|
114
|
+
await this.generateTypes();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Sets up watcher to reload when files change
|
|
118
|
+
*/
|
|
119
|
+
setupWatcher() {
|
|
120
|
+
if (this.watcher) {
|
|
121
|
+
this.watcher.close();
|
|
122
|
+
}
|
|
123
|
+
this.watcher = watchApiDirectory(this.options.apiDir, async (event, filePath) => {
|
|
124
|
+
this.options.logger.info(`API file ${event}: ${filePath}`);
|
|
125
|
+
await this.reload();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Generates types and services files
|
|
130
|
+
*/
|
|
131
|
+
async generateTypes() {
|
|
132
|
+
try {
|
|
133
|
+
const schema = routesToSchema(this.routes);
|
|
134
|
+
const isTypeScript = isTypeScriptProject(this.options.root);
|
|
135
|
+
// Generate api.types.ts only if it's a TypeScript project
|
|
136
|
+
if (isTypeScript) {
|
|
137
|
+
const typesPath = path.join(this.options.root, 'src', GENERATED_TYPES_FILE);
|
|
138
|
+
await generateTypesFile(typesPath, schema, API_BASE_PATH);
|
|
139
|
+
const relativeTypesPath = path.relative(this.options.root, typesPath);
|
|
140
|
+
this.options.logger.typesGenerated(`./${relativeTypesPath.replace(/\\/g, '/')}`);
|
|
141
|
+
}
|
|
142
|
+
// Generate api.services.ts or api.services.js depending on the project
|
|
143
|
+
const servicesFileName = isTypeScript ? GENERATED_SERVICES_FILE : 'api.services.js';
|
|
144
|
+
const servicesPath = path.join(this.options.root, 'src', servicesFileName);
|
|
145
|
+
await generateServicesFile(servicesPath, schema, API_BASE_PATH, isTypeScript);
|
|
146
|
+
const relativeServicesPath = path.relative(this.options.root, servicesPath);
|
|
147
|
+
this.options.logger.servicesGenerated(`./${relativeServicesPath.replace(/\\/g, '/')}`);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
this.options.logger.error(`Failed to generate types: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Cleans up resources
|
|
155
|
+
*/
|
|
156
|
+
cleanup() {
|
|
157
|
+
if (this.watcher) {
|
|
158
|
+
this.watcher.close();
|
|
159
|
+
this.watcher = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Extracts the body type from a route file
|
|
165
|
+
* Looks for export type Body = ... or export interface Body { ... }
|
|
166
|
+
* Returns the complete type definition as a string
|
|
167
|
+
*/
|
|
168
|
+
function extractBodyTypeFromFile(filePath) {
|
|
169
|
+
return extractTypeFromFile(filePath, 'Body');
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Extracts the query type from a route file
|
|
173
|
+
* Looks for export type Query = ... or export interface Query { ... }
|
|
174
|
+
* Returns the complete type definition as a string
|
|
175
|
+
*/
|
|
176
|
+
function extractQueryTypeFromFile(filePath) {
|
|
177
|
+
return extractTypeFromFile(filePath, 'Query');
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Helper function to extract a type from a file
|
|
181
|
+
* Uses regex-based extraction (synchronous)
|
|
182
|
+
*
|
|
183
|
+
* Note: AST-based extraction using ts-morph would be more robust but requires:
|
|
184
|
+
* 1. Making this function async
|
|
185
|
+
* 2. Adding ts-morph as optional dependency
|
|
186
|
+
* 3. Updating call sites to handle async
|
|
187
|
+
* This can be implemented in a future version when needed.
|
|
188
|
+
*/
|
|
189
|
+
function extractTypeFromFile(filePath, typeName) {
|
|
190
|
+
try {
|
|
191
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
192
|
+
// Look for export type {TypeName} = ...
|
|
193
|
+
const typeStart = content.indexOf(`export type ${typeName}`);
|
|
194
|
+
if (typeStart !== -1) {
|
|
195
|
+
const afterStart = content.substring(typeStart);
|
|
196
|
+
const equalsIndex = afterStart.indexOf('=');
|
|
197
|
+
if (equalsIndex !== -1) {
|
|
198
|
+
const afterEquals = afterStart.substring(equalsIndex + 1).trimStart();
|
|
199
|
+
// If it starts with {, need to count braces to find the correct closing
|
|
200
|
+
if (afterEquals.startsWith('{')) {
|
|
201
|
+
let braceCount = 0;
|
|
202
|
+
let i = 0;
|
|
203
|
+
let foundClose = false;
|
|
204
|
+
for (; i < afterEquals.length; i++) {
|
|
205
|
+
if (afterEquals[i] === '{') {
|
|
206
|
+
braceCount++;
|
|
207
|
+
}
|
|
208
|
+
else if (afterEquals[i] === '}') {
|
|
209
|
+
braceCount--;
|
|
210
|
+
if (braceCount === 0) {
|
|
211
|
+
foundClose = true;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (foundClose) {
|
|
217
|
+
const typeBody = afterEquals.substring(0, i + 1).trim();
|
|
218
|
+
return typeBody;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// If it doesn't start with {, it's a simple type alias (e.g., string, number, etc)
|
|
223
|
+
// Get until the first ; (but may have line breaks)
|
|
224
|
+
const semicolonIndex = afterEquals.indexOf(';');
|
|
225
|
+
if (semicolonIndex !== -1) {
|
|
226
|
+
return afterEquals.substring(0, semicolonIndex).trim();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Look for export interface {TypeName} { ... }
|
|
232
|
+
const interfaceStart = content.indexOf(`export interface ${typeName}`);
|
|
233
|
+
if (interfaceStart !== -1) {
|
|
234
|
+
const afterStart = content.substring(interfaceStart);
|
|
235
|
+
const openBrace = afterStart.indexOf('{');
|
|
236
|
+
if (openBrace !== -1) {
|
|
237
|
+
let braceCount = 0;
|
|
238
|
+
let i = openBrace;
|
|
239
|
+
let foundClose = false;
|
|
240
|
+
for (; i < afterStart.length; i++) {
|
|
241
|
+
if (afterStart[i] === '{') {
|
|
242
|
+
braceCount++;
|
|
243
|
+
}
|
|
244
|
+
else if (afterStart[i] === '}') {
|
|
245
|
+
braceCount--;
|
|
246
|
+
if (braceCount === 0) {
|
|
247
|
+
foundClose = true;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (foundClose) {
|
|
253
|
+
const interfaceBody = afterStart.substring(openBrace + 1, i).trim();
|
|
254
|
+
return `{ ${interfaceBody} }`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
// If unable to read the file, return undefined
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Creates middleware for Vite development server
|
|
267
|
+
*/
|
|
268
|
+
export function createViteDevServerMiddleware(options) {
|
|
269
|
+
const state = new DevServerState(options);
|
|
270
|
+
// Initialize when middleware is created
|
|
271
|
+
state.initialize().catch(error => {
|
|
272
|
+
options.logger.error(`Failed to initialize Vitek: ${error instanceof Error ? error.message : String(error)}`);
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
middleware: async (req, res, next) => {
|
|
276
|
+
// Only process requests for /api/*
|
|
277
|
+
if (!req.url?.startsWith(API_BASE_PATH)) {
|
|
278
|
+
return next();
|
|
279
|
+
}
|
|
280
|
+
const startTime = Date.now();
|
|
281
|
+
const requestMethod = req.method?.toLowerCase() || 'get';
|
|
282
|
+
const requestPath = req.url.split('?')[0]; // Path without query string
|
|
283
|
+
try {
|
|
284
|
+
// Parse URL to separate path from query string
|
|
285
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
286
|
+
// Remove /api from path for matching
|
|
287
|
+
const routePath = url.pathname.replace(API_BASE_PATH, '') || '/';
|
|
288
|
+
const method = requestMethod;
|
|
289
|
+
// Try to match with a route
|
|
290
|
+
const match = matchRoute(state.routes, routePath, method);
|
|
291
|
+
if (!match) {
|
|
292
|
+
const duration = Date.now() - startTime;
|
|
293
|
+
res.statusCode = 404;
|
|
294
|
+
res.setHeader('Content-Type', 'application/json');
|
|
295
|
+
res.end(JSON.stringify({ error: 'Route not found' }));
|
|
296
|
+
options.logger.request(requestMethod, requestPath, 404, duration);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Log route match if enabled
|
|
300
|
+
options.logger.routeMatched(match.route.pattern, method);
|
|
301
|
+
// Parse query string (url was already created above)
|
|
302
|
+
const query = {};
|
|
303
|
+
url.searchParams.forEach((value, key) => {
|
|
304
|
+
if (query[key]) {
|
|
305
|
+
const existing = query[key];
|
|
306
|
+
query[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
query[key] = value;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
// Parse body if present
|
|
313
|
+
let body;
|
|
314
|
+
if (['post', 'put', 'patch'].includes(method)) {
|
|
315
|
+
body = await new Promise((resolve) => {
|
|
316
|
+
const chunks = [];
|
|
317
|
+
req.on('data', (chunk) => {
|
|
318
|
+
chunks.push(chunk);
|
|
319
|
+
});
|
|
320
|
+
req.on('end', () => {
|
|
321
|
+
const rawBody = Buffer.concat(chunks).toString();
|
|
322
|
+
if (!rawBody) {
|
|
323
|
+
resolve(undefined);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
resolve(JSON.parse(rawBody));
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
resolve(rawBody);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// Create context
|
|
336
|
+
const context = createContext({
|
|
337
|
+
url: req.url,
|
|
338
|
+
method,
|
|
339
|
+
headers: req.headers,
|
|
340
|
+
body,
|
|
341
|
+
}, match.params, query);
|
|
342
|
+
// Note: Validation is opt-in and can be done manually in handlers using validateBody/validateQuery
|
|
343
|
+
// Automatic validation based on TypeScript types would require AST parsing (Phase 2.2)
|
|
344
|
+
// For now, validation is available as helper functions that handlers can use
|
|
345
|
+
// Get applicable middlewares for this route
|
|
346
|
+
const applicableMiddlewares = getApplicableMiddlewares(state.middlewares, match.route.pattern);
|
|
347
|
+
// Compose middlewares + handler
|
|
348
|
+
const composed = compose(applicableMiddlewares);
|
|
349
|
+
const handler = async () => {
|
|
350
|
+
const result = await match.route.handler(context);
|
|
351
|
+
// Handle VitekResponse format (with status and headers)
|
|
352
|
+
if (isVitekResponse(result)) {
|
|
353
|
+
const response = result;
|
|
354
|
+
const statusCode = response.status || 200;
|
|
355
|
+
// Set headers
|
|
356
|
+
if (response.headers) {
|
|
357
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
358
|
+
res.setHeader(key, value);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Set default Content-Type if not specified and body exists
|
|
362
|
+
if (!response.headers || !response.headers['Content-Type']) {
|
|
363
|
+
if (response.body !== undefined) {
|
|
364
|
+
res.setHeader('Content-Type', 'application/json');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
res.statusCode = statusCode;
|
|
368
|
+
// Handle different response types
|
|
369
|
+
if (response.body === undefined) {
|
|
370
|
+
res.end();
|
|
371
|
+
}
|
|
372
|
+
else if (typeof response.body === 'string') {
|
|
373
|
+
res.end(response.body);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
res.end(JSON.stringify(response.body));
|
|
377
|
+
}
|
|
378
|
+
// Log request/response
|
|
379
|
+
const duration = Date.now() - startTime;
|
|
380
|
+
options.logger.request(requestMethod, requestPath, statusCode, duration);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// Backward compatibility: plain object/primitive → JSON response with status 200
|
|
384
|
+
res.setHeader('Content-Type', 'application/json');
|
|
385
|
+
res.statusCode = 200;
|
|
386
|
+
res.end(JSON.stringify(result));
|
|
387
|
+
// Log request/response
|
|
388
|
+
const duration = Date.now() - startTime;
|
|
389
|
+
options.logger.request(requestMethod, requestPath, 200, duration);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
await composed(context, handler);
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
const duration = Date.now() - startTime;
|
|
396
|
+
// Handle HTTP errors with proper status codes
|
|
397
|
+
if (error instanceof HttpError) {
|
|
398
|
+
const httpError = error;
|
|
399
|
+
options.logger.warn(`HTTP Error ${httpError.statusCode}: ${httpError.message}`);
|
|
400
|
+
res.statusCode = httpError.statusCode;
|
|
401
|
+
res.setHeader('Content-Type', 'application/json');
|
|
402
|
+
res.end(JSON.stringify({
|
|
403
|
+
error: httpError.name,
|
|
404
|
+
message: httpError.message,
|
|
405
|
+
code: httpError.code,
|
|
406
|
+
}));
|
|
407
|
+
// Log request/response
|
|
408
|
+
options.logger.request(requestMethod, requestPath, httpError.statusCode, duration);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
// Generic error handling (backward compatible)
|
|
412
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
413
|
+
options.logger.error(`Error handling request: ${errorMessage}`);
|
|
414
|
+
res.statusCode = 500;
|
|
415
|
+
res.setHeader('Content-Type', 'application/json');
|
|
416
|
+
res.end(JSON.stringify({
|
|
417
|
+
error: 'Internal server error',
|
|
418
|
+
message: errorMessage,
|
|
419
|
+
}));
|
|
420
|
+
// Log request/response
|
|
421
|
+
options.logger.request(requestMethod, requestPath, 500, duration);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
cleanup: () => state.cleanup(),
|
|
426
|
+
reload: () => state.reload(),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger adapter for Vite
|
|
3
|
+
* Translates core events into Vite logs
|
|
4
|
+
*/
|
|
5
|
+
import type { Logger } from 'vite';
|
|
6
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
7
|
+
export interface LoggingOptions {
|
|
8
|
+
level?: LogLevel;
|
|
9
|
+
enableRequestLogging?: boolean;
|
|
10
|
+
enableRouteLogging?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface VitekLogger {
|
|
13
|
+
debug(message: string, data?: Record<string, any>): void;
|
|
14
|
+
info(message: string, data?: Record<string, any>): void;
|
|
15
|
+
warn(message: string, data?: Record<string, any>): void;
|
|
16
|
+
error(message: string, data?: Record<string, any>): void;
|
|
17
|
+
routesRegistered(routes: Array<{
|
|
18
|
+
method: string;
|
|
19
|
+
pattern: string;
|
|
20
|
+
}>, apiBasePath: string): void;
|
|
21
|
+
routeMatched(pattern: string, method: string): void;
|
|
22
|
+
middlewareLoaded(count: number): void;
|
|
23
|
+
typesGenerated(outputPath: string): void;
|
|
24
|
+
servicesGenerated(outputPath: string): void;
|
|
25
|
+
request(method: string, path: string, statusCode: number, duration?: number): void;
|
|
26
|
+
response(method: string, path: string, statusCode: number, duration?: number): void;
|
|
27
|
+
getOptions(): LoggingOptions;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Creates a logger that uses Vite's logger
|
|
31
|
+
*/
|
|
32
|
+
export declare function createViteLogger(viteLogger: Logger, options?: LoggingOptions): VitekLogger;
|
|
33
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/adapters/vite/logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAoBD,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACzD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACxD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACzD,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAChG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACpD,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnF,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpF,UAAU,IAAI,cAAc,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,WAAW,CAyH1F"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger adapter for Vite
|
|
3
|
+
* Translates core events into Vite logs
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Formats the [vitek] tag with green color and bold
|
|
7
|
+
* Uses ANSI codes: \x1b[1m = bold, \x1b[32m = green, \x1b[0m = reset
|
|
8
|
+
*/
|
|
9
|
+
function formatTag(text) {
|
|
10
|
+
return `\x1b[1m\x1b[32m${text}\x1b[0m`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a log level should be logged based on the configured level
|
|
14
|
+
*/
|
|
15
|
+
function shouldLog(level, configuredLevel) {
|
|
16
|
+
const levels = ['debug', 'info', 'warn', 'error'];
|
|
17
|
+
const levelIndex = levels.indexOf(level);
|
|
18
|
+
const configuredIndex = levels.indexOf(configuredLevel);
|
|
19
|
+
return levelIndex >= configuredIndex;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates a logger that uses Vite's logger
|
|
23
|
+
*/
|
|
24
|
+
export function createViteLogger(viteLogger, options) {
|
|
25
|
+
const tag = formatTag('[vitek]');
|
|
26
|
+
const logLevel = options?.level || 'info';
|
|
27
|
+
const enableRequestLogging = options?.enableRequestLogging || false;
|
|
28
|
+
const enableRouteLogging = options?.enableRouteLogging !== false; // Default true
|
|
29
|
+
const formatData = (data) => {
|
|
30
|
+
if (!data || Object.keys(data).length === 0) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
return ' ' + JSON.stringify(data);
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
debug(message, data) {
|
|
37
|
+
if (shouldLog('debug', logLevel)) {
|
|
38
|
+
viteLogger.info(`${tag} [DEBUG] ${message}${formatData(data)}`, { timestamp: true });
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
info(message, data) {
|
|
42
|
+
if (shouldLog('info', logLevel)) {
|
|
43
|
+
viteLogger.info(`${tag} ${message}${formatData(data)}`, { timestamp: true });
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
warn(message, data) {
|
|
47
|
+
if (shouldLog('warn', logLevel)) {
|
|
48
|
+
viteLogger.warn(`${tag} ${message}${formatData(data)}`, { timestamp: true });
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
error(message, data) {
|
|
52
|
+
if (shouldLog('error', logLevel)) {
|
|
53
|
+
viteLogger.error(`${tag} ${message}${formatData(data)}`, { timestamp: true });
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
routesRegistered(routes, apiBasePath) {
|
|
57
|
+
if (routes.length === 0) {
|
|
58
|
+
if (shouldLog('info', logLevel)) {
|
|
59
|
+
viteLogger.info(`${tag} No routes registered`, { timestamp: true });
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (shouldLog('info', logLevel)) {
|
|
64
|
+
const routesList = routes
|
|
65
|
+
.map(r => {
|
|
66
|
+
const pattern = r.pattern === '' ? '/' : `/${r.pattern}`;
|
|
67
|
+
return ` ${r.method.toUpperCase()} ${apiBasePath}${pattern}`;
|
|
68
|
+
})
|
|
69
|
+
.join('\n');
|
|
70
|
+
viteLogger.info(`${tag} Registered routes:\n${routesList}`, { timestamp: true });
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
routeMatched(pattern, method) {
|
|
74
|
+
if (enableRouteLogging && shouldLog('debug', logLevel)) {
|
|
75
|
+
viteLogger.info(`${tag} [ROUTE] ${method.toUpperCase()} ${pattern}`, { timestamp: true });
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
middlewareLoaded(count) {
|
|
79
|
+
if (shouldLog('info', logLevel)) {
|
|
80
|
+
viteLogger.info(`${tag} Loaded ${count} global middleware(s)`, { timestamp: true });
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
typesGenerated(outputPath) {
|
|
84
|
+
if (shouldLog('info', logLevel)) {
|
|
85
|
+
viteLogger.info(`${tag} Generated types: ${outputPath}`, { timestamp: true });
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
servicesGenerated(outputPath) {
|
|
89
|
+
if (shouldLog('info', logLevel)) {
|
|
90
|
+
viteLogger.info(`${tag} Generated services: ${outputPath}`, { timestamp: true });
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
request(method, path, statusCode, duration) {
|
|
94
|
+
if (enableRequestLogging && shouldLog('info', logLevel)) {
|
|
95
|
+
const durationStr = duration !== undefined ? ` (${duration}ms)` : '';
|
|
96
|
+
const statusColor = statusCode >= 500 ? '\x1b[31m' : statusCode >= 400 ? '\x1b[33m' : '\x1b[32m';
|
|
97
|
+
viteLogger.info(`${tag} [REQUEST] ${method.toUpperCase()} ${path} ${statusColor}${statusCode}\x1b[0m${durationStr}`, { timestamp: true });
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
response(method, path, statusCode, duration) {
|
|
101
|
+
// Alias for request (for consistency)
|
|
102
|
+
this.request(method, path, statusCode, duration);
|
|
103
|
+
},
|
|
104
|
+
getOptions() {
|
|
105
|
+
return {
|
|
106
|
+
level: logLevel,
|
|
107
|
+
enableRequestLogging,
|
|
108
|
+
enableRouteLogging,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context creation for route handlers
|
|
3
|
+
* Core logic - runtime agnostic
|
|
4
|
+
*/
|
|
5
|
+
export interface VitekContext {
|
|
6
|
+
url: string;
|
|
7
|
+
method: string;
|
|
8
|
+
path: string;
|
|
9
|
+
query: Record<string, string | string[]>;
|
|
10
|
+
params: Record<string, string>;
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
body?: any;
|
|
13
|
+
}
|
|
14
|
+
export interface VitekRequest {
|
|
15
|
+
url: string;
|
|
16
|
+
method: string;
|
|
17
|
+
headers: Record<string, string>;
|
|
18
|
+
body?: any;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Response object that allows control over status code and headers
|
|
22
|
+
* If a handler returns a plain object, it will be treated as status 200 with JSON content-type
|
|
23
|
+
*/
|
|
24
|
+
export interface VitekResponse {
|
|
25
|
+
status?: number;
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
body?: any;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Type guard to check if a value is a VitekResponse
|
|
31
|
+
* A VitekResponse is identified by having 'status' (number) or 'headers' (object) properties
|
|
32
|
+
* Plain objects without these properties are treated as regular JSON responses (backward compatibility)
|
|
33
|
+
*/
|
|
34
|
+
export declare function isVitekResponse(value: any): value is VitekResponse;
|
|
35
|
+
/**
|
|
36
|
+
* Creates a context from a request and extracted parameters
|
|
37
|
+
*/
|
|
38
|
+
export declare function createContext(request: VitekRequest, params?: Record<string, string>, query?: Record<string, string | string[]>): VitekContext;
|
|
39
|
+
//# sourceMappingURL=create-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-context.d.ts","sourceRoot":"","sources":["../../../src/core/context/create-context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IACzC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,GAAG,GAAG,KAAK,IAAI,aAAa,CAWlE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,YAAY,EACrB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACnC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAM,GAC5C,YAAY,CAYd"}
|