vitek-plugin 0.1.0-beta → 0.1.2-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/README.md +19 -863
- package/dist/adapters/vite/dev-server.d.ts +2 -3
- package/dist/adapters/vite/dev-server.d.ts.map +1 -1
- package/dist/adapters/vite/dev-server.js +11 -187
- package/dist/build/build-api-bundle.d.ts +16 -0
- package/dist/build/build-api-bundle.d.ts.map +1 -0
- package/dist/build/build-api-bundle.js +96 -0
- package/dist/cli/serve.d.ts +7 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +84 -0
- package/dist/core/normalize/normalize-path.d.ts +1 -0
- package/dist/core/normalize/normalize-path.d.ts.map +1 -1
- package/dist/core/normalize/normalize-path.js +7 -1
- package/dist/core/server/request-handler.d.ts +23 -0
- package/dist/core/server/request-handler.d.ts.map +1 -0
- package/dist/core/server/request-handler.js +143 -0
- package/dist/core/types/generate.d.ts.map +1 -1
- package/dist/core/types/generate.js +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +60 -8
- package/package.json +23 -9
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Adapter for integration with Vite development server
|
|
3
3
|
* Thin layer that connects core → Vite
|
|
4
4
|
*/
|
|
5
|
-
import type {
|
|
6
|
-
import type { IncomingMessage, ServerResponse } from 'http';
|
|
5
|
+
import type { ViteDevServer } from 'vite';
|
|
7
6
|
import type { VitekLogger } from './logger.js';
|
|
8
7
|
export interface ViteDevServerOptions {
|
|
9
8
|
root: string;
|
|
@@ -16,7 +15,7 @@ export interface ViteDevServerOptions {
|
|
|
16
15
|
* Creates middleware for Vite development server
|
|
17
16
|
*/
|
|
18
17
|
export declare function createViteDevServerMiddleware(options: ViteDevServerOptions): {
|
|
19
|
-
middleware: (req: IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => Promise<void>;
|
|
18
|
+
middleware: (req: import("http").IncomingMessage, res: import("http").ServerResponse, next: import("vite").Connect.NextFunction) => Promise<void>;
|
|
20
19
|
cleanup: () => void;
|
|
21
20
|
reload: () => Promise<void>;
|
|
22
21
|
};
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../../src/adapters/vite/dev-server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAoB1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/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;AAsPD;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,oBAAoB;;;;EAe1E"}
|
|
@@ -14,21 +14,17 @@ function isTypeScriptProject(root) {
|
|
|
14
14
|
}
|
|
15
15
|
import { watchApiDirectory } from '../../core/file-system/watch-api-dir.js';
|
|
16
16
|
import { createRoute } from '../../core/routing/route-parser.js';
|
|
17
|
-
import {
|
|
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';
|
|
17
|
+
import { createRequestHandler } from '../../core/server/request-handler.js';
|
|
21
18
|
import { routesToSchema } from '../../core/types/schema.js';
|
|
22
19
|
import { generateTypesFile, generateServicesFile } from '../../core/types/generate.js';
|
|
23
20
|
import { API_BASE_PATH, GENERATED_TYPES_FILE, GENERATED_SERVICES_FILE } from '../../shared/constants.js';
|
|
24
|
-
import { HttpError } from '../../shared/errors.js';
|
|
25
21
|
/**
|
|
26
22
|
* Development server state
|
|
27
23
|
*/
|
|
28
24
|
class DevServerState {
|
|
29
25
|
options;
|
|
30
26
|
routes = [];
|
|
31
|
-
middlewares = [];
|
|
27
|
+
middlewares = [];
|
|
32
28
|
watcher = null;
|
|
33
29
|
constructor(options) {
|
|
34
30
|
this.options = options;
|
|
@@ -47,16 +43,12 @@ class DevServerState {
|
|
|
47
43
|
if (showReloadLog) {
|
|
48
44
|
this.options.logger.info('Reloading API routes...');
|
|
49
45
|
}
|
|
50
|
-
// Scan directory
|
|
51
46
|
const scanResult = scanApiDirectory(this.options.apiDir);
|
|
52
|
-
|
|
53
|
-
this.middlewares = [];
|
|
47
|
+
this.middlewares.length = 0;
|
|
54
48
|
for (const middlewareInfo of scanResult.middlewares) {
|
|
55
49
|
try {
|
|
56
|
-
// Convert absolute path to relative path to Vite root (format /src/api/posts/middleware.ts)
|
|
57
50
|
const relativePath = path.relative(this.options.root, middlewareInfo.path);
|
|
58
51
|
const vitePath = `/${relativePath.replace(/\\/g, '/')}`;
|
|
59
|
-
// Use Vite's ssrLoadModule to process TypeScript
|
|
60
52
|
const middlewareModule = await this.options.viteServer.ssrLoadModule(vitePath);
|
|
61
53
|
const middleware = middlewareModule.default || middlewareModule.middleware;
|
|
62
54
|
let middlewareArray = [];
|
|
@@ -79,23 +71,18 @@ class DevServerState {
|
|
|
79
71
|
}
|
|
80
72
|
const totalMiddlewareCount = this.middlewares.reduce((sum, m) => sum + m.middleware.length, 0);
|
|
81
73
|
this.options.logger.middlewareLoaded(totalMiddlewareCount);
|
|
82
|
-
|
|
83
|
-
this.routes = [];
|
|
74
|
+
this.routes.length = 0;
|
|
84
75
|
for (const parsedRoute of scanResult.routes) {
|
|
85
76
|
try {
|
|
86
|
-
// Convert absolute path to relative path to Vite root (format /src/api/users/[id].get.ts)
|
|
87
77
|
const relativePath = path.relative(this.options.root, parsedRoute.file);
|
|
88
78
|
const vitePath = `/${relativePath.replace(/\\/g, '/')}`;
|
|
89
|
-
// Use Vite's ssrLoadModule to process TypeScript
|
|
90
79
|
const handlerModule = await this.options.viteServer.ssrLoadModule(vitePath);
|
|
91
80
|
const handler = handlerModule.default || handlerModule.handler || handlerModule[parsedRoute.method];
|
|
92
81
|
if (typeof handler !== 'function') {
|
|
93
82
|
this.options.logger.warn(`Route file ${parsedRoute.file} does not export a handler function`);
|
|
94
83
|
continue;
|
|
95
84
|
}
|
|
96
|
-
// Extract bodyType from file (looking for export type Body or export interface Body)
|
|
97
85
|
const bodyType = extractBodyTypeFromFile(parsedRoute.file);
|
|
98
|
-
// Extract queryType from file (looking for export type Query or export interface Query)
|
|
99
86
|
const queryType = extractQueryTypeFromFile(parsedRoute.file);
|
|
100
87
|
const route = createRoute(parsedRoute, handler, bodyType, queryType);
|
|
101
88
|
this.routes.push(route);
|
|
@@ -104,13 +91,11 @@ class DevServerState {
|
|
|
104
91
|
this.options.logger.error(`Failed to load route ${parsedRoute.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
105
92
|
}
|
|
106
93
|
}
|
|
107
|
-
// Log registered routes (consolidated)
|
|
108
94
|
const routesInfo = this.routes.map(r => ({
|
|
109
95
|
method: r.method,
|
|
110
96
|
pattern: r.pattern,
|
|
111
97
|
}));
|
|
112
98
|
this.options.logger.routesRegistered(routesInfo, API_BASE_PATH);
|
|
113
|
-
// Generate types
|
|
114
99
|
await this.generateTypes();
|
|
115
100
|
}
|
|
116
101
|
/**
|
|
@@ -132,14 +117,12 @@ class DevServerState {
|
|
|
132
117
|
try {
|
|
133
118
|
const schema = routesToSchema(this.routes);
|
|
134
119
|
const isTypeScript = isTypeScriptProject(this.options.root);
|
|
135
|
-
// Generate api.types.ts only if it's a TypeScript project
|
|
136
120
|
if (isTypeScript) {
|
|
137
121
|
const typesPath = path.join(this.options.root, 'src', GENERATED_TYPES_FILE);
|
|
138
122
|
await generateTypesFile(typesPath, schema, API_BASE_PATH);
|
|
139
123
|
const relativeTypesPath = path.relative(this.options.root, typesPath);
|
|
140
124
|
this.options.logger.typesGenerated(`./${relativeTypesPath.replace(/\\/g, '/')}`);
|
|
141
125
|
}
|
|
142
|
-
// Generate api.services.ts or api.services.js depending on the project
|
|
143
126
|
const servicesFileName = isTypeScript ? GENERATED_SERVICES_FILE : 'api.services.js';
|
|
144
127
|
const servicesPath = path.join(this.options.root, 'src', servicesFileName);
|
|
145
128
|
await generateServicesFile(servicesPath, schema, API_BASE_PATH, isTypeScript);
|
|
@@ -177,26 +160,17 @@ function extractQueryTypeFromFile(filePath) {
|
|
|
177
160
|
return extractTypeFromFile(filePath, 'Query');
|
|
178
161
|
}
|
|
179
162
|
/**
|
|
180
|
-
*
|
|
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.
|
|
163
|
+
* Extracts a type (Body or Query) from a route file via regex. AST-based extraction could be used in a future version.
|
|
188
164
|
*/
|
|
189
165
|
function extractTypeFromFile(filePath, typeName) {
|
|
190
166
|
try {
|
|
191
167
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
192
|
-
// Look for export type {TypeName} = ...
|
|
193
168
|
const typeStart = content.indexOf(`export type ${typeName}`);
|
|
194
169
|
if (typeStart !== -1) {
|
|
195
170
|
const afterStart = content.substring(typeStart);
|
|
196
171
|
const equalsIndex = afterStart.indexOf('=');
|
|
197
172
|
if (equalsIndex !== -1) {
|
|
198
173
|
const afterEquals = afterStart.substring(equalsIndex + 1).trimStart();
|
|
199
|
-
// If it starts with {, need to count braces to find the correct closing
|
|
200
174
|
if (afterEquals.startsWith('{')) {
|
|
201
175
|
let braceCount = 0;
|
|
202
176
|
let i = 0;
|
|
@@ -219,8 +193,6 @@ function extractTypeFromFile(filePath, typeName) {
|
|
|
219
193
|
}
|
|
220
194
|
}
|
|
221
195
|
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
196
|
const semicolonIndex = afterEquals.indexOf(';');
|
|
225
197
|
if (semicolonIndex !== -1) {
|
|
226
198
|
return afterEquals.substring(0, semicolonIndex).trim();
|
|
@@ -228,7 +200,6 @@ function extractTypeFromFile(filePath, typeName) {
|
|
|
228
200
|
}
|
|
229
201
|
}
|
|
230
202
|
}
|
|
231
|
-
// Look for export interface {TypeName} { ... }
|
|
232
203
|
const interfaceStart = content.indexOf(`export interface ${typeName}`);
|
|
233
204
|
if (interfaceStart !== -1) {
|
|
234
205
|
const afterStart = content.substring(interfaceStart);
|
|
@@ -257,8 +228,7 @@ function extractTypeFromFile(filePath, typeName) {
|
|
|
257
228
|
}
|
|
258
229
|
return undefined;
|
|
259
230
|
}
|
|
260
|
-
catch
|
|
261
|
-
// If unable to read the file, return undefined
|
|
231
|
+
catch {
|
|
262
232
|
return undefined;
|
|
263
233
|
}
|
|
264
234
|
}
|
|
@@ -267,161 +237,15 @@ function extractTypeFromFile(filePath, typeName) {
|
|
|
267
237
|
*/
|
|
268
238
|
export function createViteDevServerMiddleware(options) {
|
|
269
239
|
const state = new DevServerState(options);
|
|
270
|
-
// Initialize when middleware is created
|
|
271
240
|
state.initialize().catch(error => {
|
|
272
241
|
options.logger.error(`Failed to initialize Vitek: ${error instanceof Error ? error.message : String(error)}`);
|
|
273
242
|
});
|
|
274
243
|
return {
|
|
275
|
-
middleware:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
},
|
|
244
|
+
middleware: createRequestHandler({
|
|
245
|
+
routes: state.routes,
|
|
246
|
+
middlewares: state.middlewares,
|
|
247
|
+
logger: options.logger,
|
|
248
|
+
}),
|
|
425
249
|
cleanup: () => state.cleanup(),
|
|
426
250
|
reload: () => state.reload(),
|
|
427
251
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the API bundle for preview/production
|
|
3
|
+
* Scans apiDir, generates an entry module, bundles with esbuild
|
|
4
|
+
*/
|
|
5
|
+
export interface BuildApiBundleOptions {
|
|
6
|
+
root: string;
|
|
7
|
+
apiDir: string;
|
|
8
|
+
outDir: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Builds the API bundle and writes it to outDir/vitek-api.mjs
|
|
12
|
+
* Returns the path to the written file, or null if skipped (no apiDir, no routes, or error)
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildApiBundle(options: BuildApiBundleOptions): Promise<string | null>;
|
|
15
|
+
export declare function getApiBundleFilename(): string;
|
|
16
|
+
//# sourceMappingURL=build-api-bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-api-bundle.d.ts","sourceRoot":"","sources":["../../src/build/build-api-bundle.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAgDD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgD3F;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the API bundle for preview/production
|
|
3
|
+
* Scans apiDir, generates an entry module, bundles with esbuild
|
|
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
|
+
import { patternToRegex } from '../core/normalize/normalize-path.js';
|
|
9
|
+
const VITEK_API_BUNDLE_FILENAME = 'vitek-api.mjs';
|
|
10
|
+
/**
|
|
11
|
+
* Generates the virtual entry file content that imports all route handlers and middlewares
|
|
12
|
+
* and exports { routes, middlewares } in the shape expected by createRequestHandler
|
|
13
|
+
* entryDir: directory of the generated entry file (for relative imports)
|
|
14
|
+
*/
|
|
15
|
+
function generateEntryContent(scanResult, entryDir) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
scanResult.routes.forEach((parsed, i) => {
|
|
18
|
+
const rel = path.relative(entryDir, parsed.file).replace(/\\/g, '/');
|
|
19
|
+
const importPath = rel.startsWith('.') ? rel : `./${rel}`;
|
|
20
|
+
lines.push(`import handler_${i} from ${JSON.stringify(importPath)};`);
|
|
21
|
+
});
|
|
22
|
+
scanResult.middlewares.forEach((mw, i) => {
|
|
23
|
+
const rel = path.relative(entryDir, mw.path).replace(/\\/g, '/');
|
|
24
|
+
const importPath = rel.startsWith('.') ? rel : `./${rel}`;
|
|
25
|
+
lines.push(`import mw_${i} from ${JSON.stringify(importPath)};`);
|
|
26
|
+
});
|
|
27
|
+
const routeEntries = scanResult.routes.map((parsed, i) => {
|
|
28
|
+
const regex = patternToRegex(parsed.pattern);
|
|
29
|
+
const regexSource = regex.source;
|
|
30
|
+
return ` { pattern: ${JSON.stringify(parsed.pattern)}, method: ${JSON.stringify(parsed.method)}, params: ${JSON.stringify(parsed.params)}, file: ${JSON.stringify(parsed.file)}, regex: new RegExp(${JSON.stringify(regexSource)}), handler: (() => { const m = handler_${i}; return typeof m === 'function' ? m : (m.default ?? m.handler ?? m[${JSON.stringify(parsed.method)}]); })() }`;
|
|
31
|
+
});
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push('const routes = [');
|
|
34
|
+
lines.push(routeEntries.join(',\n'));
|
|
35
|
+
lines.push('];');
|
|
36
|
+
const mwEntries = scanResult.middlewares.map((mw, i) => {
|
|
37
|
+
return ` { basePattern: ${JSON.stringify(mw.basePattern)}, middleware: (() => { const m = mw_${i}; const fn = typeof m === 'function' || Array.isArray(m) ? m : (m.default ?? m.middleware); return Array.isArray(fn) ? fn : [fn]; })() }`;
|
|
38
|
+
});
|
|
39
|
+
lines.push('');
|
|
40
|
+
lines.push('const middlewares = [');
|
|
41
|
+
lines.push(mwEntries.join(',\n'));
|
|
42
|
+
lines.push('];');
|
|
43
|
+
lines.push('');
|
|
44
|
+
lines.push('export { routes, middlewares };');
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Builds the API bundle and writes it to outDir/vitek-api.mjs
|
|
49
|
+
* Returns the path to the written file, or null if skipped (no apiDir, no routes, or error)
|
|
50
|
+
*/
|
|
51
|
+
export async function buildApiBundle(options) {
|
|
52
|
+
const { root, apiDir, outDir } = options;
|
|
53
|
+
if (!fs.existsSync(apiDir)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const scanResult = scanApiDirectory(apiDir);
|
|
57
|
+
if (scanResult.routes.length === 0 && scanResult.middlewares.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const tmpDir = path.join(outDir, '.vitek-tmp');
|
|
61
|
+
if (!fs.existsSync(tmpDir)) {
|
|
62
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
const tmpEntry = path.join(tmpDir, 'vitek-api-entry.mts');
|
|
65
|
+
const entryContent = generateEntryContent(scanResult, path.dirname(tmpEntry));
|
|
66
|
+
fs.writeFileSync(tmpEntry, entryContent, 'utf-8');
|
|
67
|
+
const outFile = path.join(outDir, VITEK_API_BUNDLE_FILENAME);
|
|
68
|
+
const esbuild = (await import('esbuild').catch(() => null));
|
|
69
|
+
if (!esbuild) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await esbuild.build({
|
|
74
|
+
entryPoints: [tmpEntry],
|
|
75
|
+
bundle: true,
|
|
76
|
+
format: 'esm',
|
|
77
|
+
platform: 'node',
|
|
78
|
+
outfile: outFile,
|
|
79
|
+
external: ['vitek-plugin'],
|
|
80
|
+
});
|
|
81
|
+
return outFile;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
try {
|
|
85
|
+
fs.unlinkSync(tmpEntry);
|
|
86
|
+
if (fs.readdirSync(tmpDir).length === 0) {
|
|
87
|
+
fs.rmdirSync(tmpDir);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function getApiBundleFilename() {
|
|
95
|
+
return VITEK_API_BUNDLE_FILENAME;
|
|
96
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production server (vitek-serve): serves static assets and the API from dist/
|
|
3
|
+
* Usage: vitek-serve [--dir=dist] [--port=3000] [--host=0.0.0.0]
|
|
4
|
+
* Also accepts space-separated: --dir dist --port 3000 --host 0.0.0.0
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=serve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/cli/serve.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production server (vitek-serve): serves static assets and the API from dist/
|
|
3
|
+
* Usage: vitek-serve [--dir=dist] [--port=3000] [--host=0.0.0.0]
|
|
4
|
+
* Also accepts space-separated: --dir dist --port 3000 --host 0.0.0.0
|
|
5
|
+
*/
|
|
6
|
+
import * as http from 'http';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import { pathToFileURL } from 'url';
|
|
10
|
+
import connect from 'connect';
|
|
11
|
+
import serveStatic from 'serve-static';
|
|
12
|
+
import { createRequestHandler } from '../core/server/request-handler.js';
|
|
13
|
+
import { API_BASE_PATH } from '../shared/constants.js';
|
|
14
|
+
import { getApiBundleFilename } from '../build/build-api-bundle.js';
|
|
15
|
+
function parseArgs() {
|
|
16
|
+
let dir = 'dist';
|
|
17
|
+
let port = 3000;
|
|
18
|
+
let host = '0.0.0.0';
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const arg = argv[i];
|
|
22
|
+
if (arg.startsWith('--dir='))
|
|
23
|
+
dir = arg.slice(6);
|
|
24
|
+
else if (arg === '--dir' && argv[i + 1])
|
|
25
|
+
dir = argv[++i];
|
|
26
|
+
else if (arg.startsWith('--port='))
|
|
27
|
+
port = parseInt(arg.slice(7), 10);
|
|
28
|
+
else if (arg === '--port' && argv[i + 1])
|
|
29
|
+
port = parseInt(argv[++i], 10);
|
|
30
|
+
else if (arg.startsWith('--host='))
|
|
31
|
+
host = arg.slice(7);
|
|
32
|
+
else if (arg === '--host' && argv[i + 1])
|
|
33
|
+
host = argv[++i];
|
|
34
|
+
}
|
|
35
|
+
return { dir, port, host };
|
|
36
|
+
}
|
|
37
|
+
async function main() {
|
|
38
|
+
const { dir, port, host } = parseArgs();
|
|
39
|
+
const distDir = path.resolve(process.cwd(), dir);
|
|
40
|
+
if (!fs.existsSync(distDir) || !fs.statSync(distDir).isDirectory()) {
|
|
41
|
+
console.error(`[vitek-serve] Directory not found or not a directory: ${distDir}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const app = connect();
|
|
45
|
+
const bundlePath = path.join(distDir, getApiBundleFilename());
|
|
46
|
+
if (fs.existsSync(bundlePath)) {
|
|
47
|
+
try {
|
|
48
|
+
const bundleUrl = pathToFileURL(bundlePath).href;
|
|
49
|
+
const mod = await import(bundleUrl);
|
|
50
|
+
const apiHandler = createRequestHandler({
|
|
51
|
+
routes: mod.routes,
|
|
52
|
+
middlewares: mod.middlewares,
|
|
53
|
+
});
|
|
54
|
+
app.use(apiHandler);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.warn('[vitek-serve] Failed to load API bundle; serving static files only:', err instanceof Error ? err.message : String(err));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log('[vitek-serve] No API bundle found; serving static files only.');
|
|
62
|
+
}
|
|
63
|
+
app.use(serveStatic(distDir, { fallthrough: true }));
|
|
64
|
+
app.use((req, res, next) => {
|
|
65
|
+
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
66
|
+
return next();
|
|
67
|
+
if (req.url?.startsWith(API_BASE_PATH))
|
|
68
|
+
return next();
|
|
69
|
+
const indexHtml = path.join(distDir, 'index.html');
|
|
70
|
+
if (!fs.existsSync(indexHtml))
|
|
71
|
+
return next();
|
|
72
|
+
res.setHeader('Content-Type', 'text/html');
|
|
73
|
+
fs.createReadStream(indexHtml).pipe(res);
|
|
74
|
+
});
|
|
75
|
+
const server = http.createServer(app);
|
|
76
|
+
server.listen(port, host, () => {
|
|
77
|
+
const base = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
|
|
78
|
+
console.log(`[vitek-serve] Ready at ${base}`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
main().catch((err) => {
|
|
82
|
+
console.error('[vitek-serve]', err);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - users/[id].get.ts -> users/:id
|
|
10
10
|
* - posts/[...ids].get.ts -> posts/*ids
|
|
11
11
|
* - health.get.ts -> health
|
|
12
|
+
* - execute/index.post.ts -> execute (index treated as directory index)
|
|
12
13
|
*/
|
|
13
14
|
export declare function normalizeRoutePath(filePath: string): string;
|
|
14
15
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-path.d.ts","sourceRoot":"","sources":["../../../src/core/normalize/normalize-path.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH
|
|
1
|
+
{"version":3,"file":"normalize-path.d.ts","sourceRoot":"","sources":["../../../src/core/normalize/normalize-path.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiC3D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAUlE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BtD"}
|
|
@@ -10,6 +10,7 @@ import { PARAM_PATTERN } from '../../shared/constants.js';
|
|
|
10
10
|
* - users/[id].get.ts -> users/:id
|
|
11
11
|
* - posts/[...ids].get.ts -> posts/*ids
|
|
12
12
|
* - health.get.ts -> health
|
|
13
|
+
* - execute/index.post.ts -> execute (index treated as directory index)
|
|
13
14
|
*/
|
|
14
15
|
export function normalizeRoutePath(filePath) {
|
|
15
16
|
// Remove extensions (.ts, .js) and HTTP method
|
|
@@ -31,7 +32,12 @@ export function normalizeRoutePath(filePath) {
|
|
|
31
32
|
}
|
|
32
33
|
return `:${paramName}`;
|
|
33
34
|
});
|
|
34
|
-
|
|
35
|
+
if (path === 'index') {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
if (path.endsWith('/index')) {
|
|
39
|
+
path = path.slice(0, -6);
|
|
40
|
+
}
|
|
35
41
|
return path;
|
|
36
42
|
}
|
|
37
43
|
/**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared request handler for API routes
|
|
3
|
+
* Used by both dev server and preview/production server
|
|
4
|
+
*/
|
|
5
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
6
|
+
import type { Connect } from 'vite';
|
|
7
|
+
import type { Route } from '../routing/route-types.js';
|
|
8
|
+
import type { LoadedMiddleware } from '../middleware/get-applicable-middlewares.js';
|
|
9
|
+
export interface RequestHandlerOptions {
|
|
10
|
+
routes: Route[];
|
|
11
|
+
middlewares: LoadedMiddleware[];
|
|
12
|
+
logger?: {
|
|
13
|
+
routeMatched?(pattern: string, method: string): void;
|
|
14
|
+
request?(method: string, path: string, statusCode: number, duration?: number): void;
|
|
15
|
+
warn?(message: string, data?: Record<string, unknown>): void;
|
|
16
|
+
error?(message: string, data?: Record<string, unknown>): void;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a Connect-style middleware that handles /api/* requests using the given routes and middlewares.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createRequestHandler(options: RequestHandlerOptions): (req: IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => Promise<void>;
|
|
23
|
+
//# sourceMappingURL=request-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-handler.d.ts","sourceRoot":"","sources":["../../../src/core/server/request-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAOpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AAEpF,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE;QACP,YAAY,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACrD,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACpF,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QAC7D,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC/D,CAAC;CACH;AAID;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,CAAC,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CA+I7J"}
|