Files

231 lines
7.1 KiB
TypeScript
Raw Permalink Normal View History

import { readFileSync } from 'node:fs';
import { connect } from 'node:net';
import type { AddressInfo } from 'node:net';
import { enhancedImages } from '@sveltejs/enhanced-img';
2026-05-02 08:26:18 +12:00
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<string, string>;
};
const appName = packageJson.name;
const appVersion = packageJson.version;
const viteVersion = packageJson.devDependencies?.vite ?? 'unknown';
const svelteKitVersion = packageJson.devDependencies?.['@sveltejs/kit'] ?? 'unknown';
const submitProxyTarget = 'http://localhost:8000';
const proxyRoutes = {
'/api/submit': '/submit',
'/api/onboarding-submit': '/onboarding-submit',
'/api/contract-submit': '/contract-submit',
} as const;
const submitProxyPath = '/api/submit';
const submitProxyDestination = `${submitProxyTarget}${proxyRoutes[submitProxyPath]}`;
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;
2026-05-02 08:26:18 +12:00
}
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('/onboarding-submit') || msg.includes('/contract-submit'))
) {
return;
2026-05-02 08:26:18 +12:00
}
baseError(msg, options);
};
return logger;
}
function goodwalkDevServerPlugin(logger: ReturnType<typeof createLogger>): 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 createMailProxy = (apiPath: keyof typeof proxyRoutes): ProxyOptions => ({
target: submitProxyTarget,
rewrite: (path) => path.replace(apiPath, proxyRoutes[apiPath]),
configure(proxy) {
proxy.on('error', (error: NodeJS.ErrnoException, req, res) => {
const { code, hint } = formatProxyError(error);
const requestPath = req.url ?? apiPath;
const requestMethod = req.method ?? 'POST';
const targetPath = `${submitProxyTarget}${proxyRoutes[apiPath]}`;
logger.error(
[
'',
'[goodwalk] Mail proxy failed',
`[goodwalk] Request: ${requestMethod} ${requestPath}`,
`[goodwalk] Target: ${targetPath}`,
`[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), enhancedImages(), svelteTesting({ autoCleanup: false }), sveltekit()],
server: {
proxy: {
'/api/submit': createMailProxy('/api/submit'),
'/api/onboarding-submit': createMailProxy('/api/onboarding-submit'),
'/api/contract-submit': createMailProxy('/api/contract-submit'),
'/api/auth': {
target: submitProxyTarget,
rewrite: (path) => path.replace('/api/auth', '/auth'),
},
'/api/save-draft': {
target: submitProxyTarget,
rewrite: (path) => path.replace('/api/save-draft', '/auth/save-draft'),
},
}
},
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
include: ['src/**/*.test.ts']
}
};
2026-05-02 08:26:18 +12:00
});