import { readFileSync } from 'node:fs'; import { connect } from 'node:net'; import type { AddressInfo } from 'node:net'; import { sveltekit } from '@sveltejs/kit/vite'; import { svelteTesting } from '@testing-library/svelte/vite'; import { defineConfig } from 'vitest/config'; import { createLogger, type Plugin, type ProxyOptions } from 'vite'; const packageJson = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as { name: string; version: string; devDependencies?: Record; }; const appName = packageJson.name; const appVersion = packageJson.version; const viteVersion = packageJson.devDependencies?.vite ?? 'unknown'; const svelteKitVersion = packageJson.devDependencies?.['@sveltejs/kit'] ?? 'unknown'; const submitProxyPath = '/api/submit'; const submitProxyTarget = 'http://localhost:8000'; const submitProxyDestination = `${submitProxyTarget}/submit`; function resolvePort(target: URL) { if (target.port) { return Number(target.port); } return target.protocol === 'https:' ? 443 : 80; } function probeTcpPort(target: string, timeoutMs = 1200) { const url = new URL(target); return new Promise<{ ok: true } | { ok: false; error: NodeJS.ErrnoException }>((resolve) => { const socket = connect({ host: url.hostname, port: resolvePort(url) }); let settled = false; const finish = (result: { ok: true } | { ok: false; error: NodeJS.ErrnoException }) => { if (settled) { return; } settled = true; socket.destroy(); resolve(result); }; socket.once('connect', () => finish({ ok: true })); socket.once('error', (error: NodeJS.ErrnoException) => finish({ ok: false, error })); socket.setTimeout(timeoutMs, () => finish({ ok: false, error: Object.assign(new Error(`Timed out after ${timeoutMs}ms`), { code: 'ETIMEDOUT' }) }) ); }); } function formatProxyError(error: NodeJS.ErrnoException) { const code = error.code ?? 'UNKNOWN'; const hint = code === 'ECONNREFUSED' ? `Mail API is not reachable at ${submitProxyTarget}. Start the Python API before submitting the form.` : code === 'ETIMEDOUT' ? `Mail API at ${submitProxyTarget} did not respond in time.` : 'The booking request could not be forwarded to the Mail API.'; return { code, hint }; } function createGoodwalkLogger() { const logger = createLogger(); const baseError = logger.error.bind(logger); logger.error = (msg, options) => { if ( typeof msg === 'string' && msg.includes('[vite] http proxy error') && (msg.includes('/submit') || msg.includes(submitProxyPath)) ) { return; } baseError(msg, options); }; return logger; } function goodwalkDevServerPlugin(logger: ReturnType): Plugin { let printedStartup = false; const printStartup = async (serverHost: string | undefined, address: AddressInfo | string | null) => { if (printedStartup) { return; } printedStartup = true; const localUrls = server.resolvedUrls?.local ?? []; const networkUrls = server.resolvedUrls?.network ?? []; const reachable = await probeTcpPort(submitProxyTarget); const backendStatus = reachable.ok ? `ready at ${submitProxyTarget}` : `${formatProxyError(reachable.error).code} at ${submitProxyTarget}`; logger.info(''); logger.info(`[goodwalk] ${appName} dev server ready`); logger.info(`[goodwalk] Version ${appVersion} | Node ${process.version} | Vite ${viteVersion} | SvelteKit ${svelteKitVersion}`); if (localUrls.length) { logger.info(`[goodwalk] Local: ${localUrls.join(', ')}`); } if (networkUrls.length) { logger.info(`[goodwalk] Network: ${networkUrls.join(', ')}`); } if (!localUrls.length && address && typeof address !== 'string') { const host = serverHost && serverHost !== '0.0.0.0' ? serverHost : 'localhost'; logger.info(`[goodwalk] Local: http://${host}:${address.port}/`); } logger.info(`[goodwalk] Booking proxy: ${submitProxyPath} -> ${submitProxyDestination}`); if (reachable.ok) { logger.info(`[goodwalk] Mail API: ${backendStatus}`); } else { logger.warn(`[goodwalk] Mail API: ${backendStatus}`); logger.warn(`[goodwalk] Hint: ${formatProxyError(reachable.error).hint}`); } logger.info(''); }; let server: import('vite').ViteDevServer; return { name: 'goodwalk-dev-server-logging', configureServer(devServer) { server = devServer; devServer.httpServer?.once('listening', () => { const address = devServer.httpServer?.address() ?? null; const configuredHost = typeof devServer.config.server.host === 'string' ? devServer.config.server.host : undefined; void printStartup(configuredHost, address); }); } }; } export default defineConfig(() => { const logger = createGoodwalkLogger(); const submitProxy: ProxyOptions = { target: submitProxyTarget, rewrite: (path) => path.replace(/^\/api\/submit/, '/submit'), configure(proxy) { proxy.on('error', (error: NodeJS.ErrnoException, req, res) => { const { code, hint } = formatProxyError(error); const requestPath = req.url ?? submitProxyPath; const requestMethod = req.method ?? 'POST'; logger.error( [ '', '[goodwalk] Booking proxy failed', `[goodwalk] Request: ${requestMethod} ${requestPath}`, `[goodwalk] Target: ${submitProxyDestination}`, `[goodwalk] Code: ${code}`, `[goodwalk] Message: ${error.message}`, `[goodwalk] Hint: ${hint}`, '' ].join('\n') ); if (res && 'writeHead' in res && !res.headersSent) { res.writeHead(502, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ error: 'Mail API unavailable', detail: hint }) ); } }); } }; return { customLogger: logger, plugins: [goodwalkDevServerPlugin(logger), svelteTesting({ autoCleanup: false }), sveltekit()], server: { proxy: { [submitProxyPath]: submitProxy } }, test: { environment: 'jsdom', setupFiles: ['./vitest.setup.ts'], include: ['src/**/*.test.ts'] } }; });