Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | 1x 4x 4x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 4x 4x 4x 4x 3x 3x 2x 1x 1x 7x 1x 1x | /**
* Production server (vitek-serve): serves static assets and the API from dist/
* Usage: vitek-serve [--dir=dist] [--port=3000] [--host=0.0.0.0]
* Also accepts space-separated: --dir dist --port 3000 --host 0.0.0.0
*/
import * as http from 'http';
import * as path from 'path';
import * as fs from 'fs';
import { pathToFileURL } from 'url';
import connect from 'connect';
import serveStatic from 'serve-static';
import { createRequestHandler } from '../core/server/request-handler.js';
import { createSocketHandler } from '../core/socket/socket-handler.js';
import { API_BASE_PATH, getSocketBasePath } from '../shared/constants.js';
import { getApiBundleFilename } from '../build/build-api-bundle.js';
import { getSocketsBundleFilename } from '../build/build-sockets-bundle.js';
import type { ApiClient, SocketEmitter, VitekApp } from '../core/shared/vitek-app.js';
const VITEK_SERVE_CONFIG_FILENAME = 'vitek.config.mjs';
export type BeforeApiRequestHook = import('../core/server/request-handler.js').BeforeApiRequestHook;
export interface OnServerStartContext {
api: ApiClient;
sockets: SocketEmitter;
server: http.Server;
}
export type OnServerStartHook = (ctx: OnServerStartContext) => void | Promise<void>;
export type OnServerShutdownHook = () => void | Promise<void>;
export interface ProductionConfig {
beforeApiRequest?: BeforeApiRequestHook[];
onError?: (err: Error, req: http.IncomingMessage, res: http.ServerResponse) => void | Promise<void>;
onServerStart?: OnServerStartHook;
onServerShutdown?: OnServerShutdownHook;
maxBodySize?: number;
}
/** Load beforeApiRequest, onError, onServerStart, onServerShutdown from dist/vitek.config.mjs if present. Throws if file exists but import fails. */
export async function loadProductionConfig(distDir: string): Promise<ProductionConfig> {
const configPath = path.join(distDir, VITEK_SERVE_CONFIG_FILENAME);
if (!fs.existsSync(configPath)) return {};
const configUrl = pathToFileURL(configPath).href;
const configMod = await import(configUrl) as {
beforeApiRequest?: BeforeApiRequestHook | BeforeApiRequestHook[];
onError?: (err: Error, req: http.IncomingMessage, res: http.ServerResponse) => void | Promise<void>;
onServerStart?: OnServerStartHook;
onServerShutdown?: OnServerShutdownHook;
maxBodySize?: number;
};
const result: ProductionConfig = {};
Eif (configMod.beforeApiRequest) {
result.beforeApiRequest = Array.isArray(configMod.beforeApiRequest) ? configMod.beforeApiRequest : [configMod.beforeApiRequest];
}
Eif (configMod.onError) result.onError = configMod.onError;
Eif (configMod.onServerStart) result.onServerStart = configMod.onServerStart;
Eif (configMod.onServerShutdown) result.onServerShutdown = configMod.onServerShutdown;
Iif (configMod.maxBodySize != null) result.maxBodySize = configMod.maxBodySize;
return result;
}
export function parseArgs(): { dir: string; port: number; host: string; cors: boolean; trustProxy: boolean } {
let dir = 'dist';
const portEnv = process.env.PORT;
const hostEnv = process.env.HOST;
let port = portEnv !== undefined && portEnv !== '' ? parseInt(portEnv, 10) : 3000;
if (Number.isNaN(port)) port = 3000;
let host = hostEnv !== undefined && hostEnv !== '' ? hostEnv : '0.0.0.0';
let cors = false;
let trustProxy = false;
const argv = process.argv.slice(2);
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
Iif (arg.startsWith('--dir=')) dir = arg.slice(6I);
else if (arg === '--dir' && argv[i + 1]) dir = argv[++iI];
else if (arg.startsWith('--port=')) port = parseInt(arg.slice(7), 10);
else if (arg === '--port' && argv[i + 1]) port = parseInt(argv[++i], 10I);
else if (arg.startsWith('--host=')) host = arg.slice(7);
else if (arg === '--host' && argv[i + 1]) host = argv[++i];
else if (arg === '--cors') cors = Etrue;
else if (arg === '--trust-proxy') trustProxy = true;
}
return { dir, port, host, cors, trustProxy };
}
export async function main(): Promise<void> {
const { dir, port, host, cors, trustProxy } = parseArgs();
const distDir = path.resolve(process.cwd(), dir);
if (!fs.existsSync(distDir) || !fs.statSync(distDir).isDirectory()) {
console.error(`[vitek-serve] Directory not found or not a directory: ${distDir}`);
process.exit(1);
}
const apiBaseUrl = `http://127.0.0.1:${port}${API_BASE_PATH}`;
const api: ApiClient = {
async fetch(path: string, fetchOptions?: { method?: string; body?: unknown }) {
const url = `${apiBaseUrl}/${path.replace(/^\//, '')}`;
const res = await fetch(url, {
method: fetchOptions?.method ?? 'GET',
headers:
fetchOptions?.body !== undefined
? { 'Content-Type': 'application/json' }
: undefined,
body:
fetchOptions?.body !== undefined
? JSON.stringify(fetchOptions.body)
: undefined,
});
const text = await res.text();
if (!text) return undefined;
try {
return JSON.parse(text);
} catch {
return text;
}
},
};
const noopSockets: SocketEmitter = { emit() {} };
const shared: VitekApp = { sockets: noopSockets, api };
const app = connect();
const bundlePath = path.join(distDir, getApiBundleFilename());
let productionConfig: Awaited<ReturnType<typeof loadProductionConfig>> = {};
try {
productionConfig = await loadProductionConfig(distDir);
} catch (err) {
console.warn('[vitek-serve] Failed to load vitek.config.mjs; continuing without production hooks:', err instanceof Error ? err.message : String(err));
}
const { beforeApiRequest, onError, onServerStart, onServerShutdown, maxBodySize } = productionConfig;
if (fs.existsSync(bundlePath)) {
try {
const bundleUrl = pathToFileURL(bundlePath).href;
const mod = await import(bundleUrl) as { routes: unknown[]; middlewares: unknown[] };
const apiHandler = createRequestHandler({
routes: mod.routes as Parameters<typeof createRequestHandler>[0]['routes'],
middlewares: mod.middlewares as Parameters<typeof createRequestHandler>[0]['middlewares'],
beforeApiRequest,
cors: cors ? true : undefined,
trustProxy,
maxBodySize,
onError,
shared,
});
app.use(apiHandler as (req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => void);
} catch (err) {
console.warn('[vitek-serve] Failed to load API bundle; serving static files only:', err instanceof Error ? err.message : String(err));
}
} else {
console.log('[vitek-serve] No API bundle found; serving static files only.');
}
app.use(serveStatic(distDir, { fallthrough: true }));
app.use((req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => {
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
if (req.url?.startsWith(API_BASE_PATH)) return next();
const indexHtml = path.join(distDir, 'index.html');
if (!fs.existsSync(indexHtml)) return next();
res.setHeader('Content-Type', 'text/html');
fs.createReadStream(indexHtml).pipe(res);
});
const server = http.createServer(app);
const socketBasePath = getSocketBasePath();
const socketsBundlePath = path.join(distDir, getSocketsBundleFilename());
if (fs.existsSync(socketsBundlePath)) {
try {
const socketsUrl = pathToFileURL(socketsBundlePath).href;
const mod = await import(socketsUrl) as { sockets: Parameters<typeof createSocketHandler>[0]['sockets'] };
const handler = createSocketHandler({
sockets: mod.sockets,
socketBasePath,
shared,
});
server.on('upgrade', handler);
} catch (err) {
console.warn('[vitek-serve] Failed to load sockets bundle:', err instanceof Error ? err.message : String(err));
}
}
if (onServerStart) {
try {
await Promise.resolve(onServerStart({ api: shared.api, sockets: shared.sockets, server }));
} catch (err) {
console.error('[vitek-serve] onServerStart failed:', err instanceof Error ? err.message : String(err));
process.exit(1);
}
}
let shuttingDown = false;
const shutdown = async () => {
if (shuttingDown) return;
shuttingDown = true;
if (onServerShutdown) {
try {
await Promise.resolve(onServerShutdown());
} catch (err) {
console.warn('[vitek-serve] onServerShutdown error:', err instanceof Error ? err.message : String(err));
}
}
server.close(() => process.exit(0));
setTimeout(() => process.exit(0), 5000);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
server.listen(port, host, () => {
const base = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
console.log(`[vitek-serve] Ready at ${base}`);
});
}
const isServeEntry = process.argv[1]?.endsWith('serve.js');
Iif (typeof process.env.VITEST === 'undefined' && isServeEntry) {
main().catch((err) => {
console.error('[vitek-serve]', err);
process.exit(1);
});
}
|