Files
gw-svelte/vite.config.ts
T

214 lines
6.3 KiB
TypeScript

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<string, string>;
};
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<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 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']
}
};
});