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.
@@ -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 { Connect, ViteDevServer } from 'vite';
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,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"}
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 { 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';
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 = []; // Loaded hierarchical 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
- // Load hierarchical middlewares
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
- // Load routes
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
- * 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.
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 (error) {
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: 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
- },
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;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA2B3D;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"}
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
- // Return without normalizing (without adding /), as it will be used in patternToRegex
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"}