2026-05-02 11:24:11 +12:00
|
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
|
import { connect } from 'node:net';
|
|
|
|
|
import type { AddressInfo } from 'node:net';
|
2026-05-11 21:02:24 +12:00
|
|
|
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';
|
2026-05-02 11:24:11 +12:00
|
|
|
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';
|
2026-05-11 21:02:24 +12:00
|
|
|
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]}`;
|
2026-05-02 11:24:11 +12:00
|
|
|
|
|
|
|
|
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
|
|
|
}
|
2026-05-02 11:24:11 +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') &&
|
2026-05-11 21:02:24 +12:00
|
|
|
(msg.includes('/submit') || msg.includes('/onboarding-submit') || msg.includes('/contract-submit'))
|
2026-05-02 11:24:11 +12:00
|
|
|
) {
|
|
|
|
|
return;
|
2026-05-02 08:26:18 +12:00
|
|
|
}
|
2026-05-02 11:24:11 +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();
|
|
|
|
|
|
2026-05-11 21:02:24 +12:00
|
|
|
const createMailProxy = (apiPath: keyof typeof proxyRoutes): ProxyOptions => ({
|
2026-05-02 11:24:11 +12:00
|
|
|
target: submitProxyTarget,
|
2026-05-11 21:02:24 +12:00
|
|
|
rewrite: (path) => path.replace(apiPath, proxyRoutes[apiPath]),
|
2026-05-02 11:24:11 +12:00
|
|
|
configure(proxy) {
|
|
|
|
|
proxy.on('error', (error: NodeJS.ErrnoException, req, res) => {
|
|
|
|
|
const { code, hint } = formatProxyError(error);
|
2026-05-11 21:02:24 +12:00
|
|
|
const requestPath = req.url ?? apiPath;
|
2026-05-02 11:24:11 +12:00
|
|
|
const requestMethod = req.method ?? 'POST';
|
2026-05-11 21:02:24 +12:00
|
|
|
const targetPath = `${submitProxyTarget}${proxyRoutes[apiPath]}`;
|
2026-05-02 11:24:11 +12:00
|
|
|
|
|
|
|
|
logger.error(
|
|
|
|
|
[
|
|
|
|
|
'',
|
2026-05-11 21:02:24 +12:00
|
|
|
'[goodwalk] Mail proxy failed',
|
2026-05-02 11:24:11 +12:00
|
|
|
`[goodwalk] Request: ${requestMethod} ${requestPath}`,
|
2026-05-11 21:02:24 +12:00
|
|
|
`[goodwalk] Target: ${targetPath}`,
|
2026-05-02 11:24:11 +12:00
|
|
|
`[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
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-05-11 21:02:24 +12:00
|
|
|
});
|
2026-05-02 11:24:11 +12:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
customLogger: logger,
|
2026-05-11 21:02:24 +12:00
|
|
|
plugins: [goodwalkDevServerPlugin(logger), enhancedImages(), svelteTesting({ autoCleanup: false }), sveltekit()],
|
2026-05-02 11:24:11 +12:00
|
|
|
server: {
|
|
|
|
|
proxy: {
|
2026-05-11 21:02:24 +12:00
|
|
|
'/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'),
|
|
|
|
|
},
|
2026-05-02 11:24:11 +12:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
test: {
|
|
|
|
|
environment: 'jsdom',
|
|
|
|
|
setupFiles: ['./vitest.setup.ts'],
|
|
|
|
|
include: ['src/**/*.test.ts']
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-05-02 08:26:18 +12:00
|
|
|
});
|