Files
dashfront/vite.config.js
2026-09-23 09:35:46 +02:00

239 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig, searchForWorkspaceRoot } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const useHttps = process.env.VITE_HTTPS === '1';
const certDir = path.resolve(__dirname, 'certs');
const keyPath = path.join(certDir, 'key.pem');
const certPath = path.join(certDir, 'cert.pem');
function httpsOptions() {
if (!useHttps) return false;
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
throw new Error(
'Missing TLS certs. Run: npm run cert (or ./scripts/generate-cert.sh)',
);
}
return {
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
};
}
function truncate(text, max = 500) {
if (text == null || text === '') return text;
const s = typeof text === 'string' ? text : String(text);
if (s.length <= max) return s;
return `${s.slice(0, max)}… (${s.length} chars)`;
}
function isBinaryContentType(contentType = '') {
const type = contentType.split(';')[0].trim().toLowerCase();
if (!type) return false;
if (type.startsWith('image/')) return true;
if (type === 'application/octet-stream') return true;
return false;
}
function formatRequestBody(contentType, buffer) {
if (!buffer || buffer.length === 0) return '(empty)';
if (isBinaryContentType(contentType)) {
const type = contentType.split(';')[0].trim();
return `(binary ${type}, ${buffer.length} bytes)`;
}
return truncate(buffer.toString('utf8'));
}
function formatResponseBody(statusCode, contentType, buffer) {
if (statusCode === 304) return '(304 no body)';
if (statusCode === 204 || !buffer || buffer.length === 0) return '(empty)';
if (isBinaryContentType(contentType)) {
const type = contentType.split(';')[0].trim();
return `(binary ${type}, ${buffer.length} bytes)`;
}
return truncate(buffer.toString('utf8'));
}
function hasRequestBody(method) {
return method === 'POST' || method === 'PUT' || method === 'PATCH';
}
// dashgui is a local (symlinked) package with its own nested node_modules.
// Its nested react/@mui/@emotion copies would otherwise get bundled in,
// producing a duplicate React instance and "Cannot read properties of null
// (reading 'useContext')" at runtime. Dedupe forces these to resolve from
// dashfront's tree. (dashgui's own package.json "exports" map already points
// the dashgui/* subpaths at its src/, so no alias is needed.)
const sharedDeps = [
'react',
'react-dom',
'react/jsx-runtime',
'react/jsx-dev-runtime',
'react-router',
'react-router-dom',
'@emotion/react',
'@emotion/styled',
'@mui/material',
'@mui/icons-material',
];
const sharedResolve = {
dedupe: sharedDeps,
};
const sharedServer = {
// HTTP :4209 — HTTPS :4308 (self-signed; see npm run cert / dev:https)
port: useHttps ? 4308 : 4209,
https: httpsOptions(),
allowedHosts: true,
fs: {
allow: [
searchForWorkspaceRoot(process.cwd()),
path.resolve(__dirname, '..'),
],
},
proxy: {
'/api': {
target: 'http://localhost:4208',
changeOrigin: true,
cookieDomainRewrite: '',
configure(proxy) {
proxy.on('proxyReq', (_proxyReq, req) => {
if (!hasRequestBody(req.method)) return;
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const buffer = chunks.length ? Buffer.concat(chunks) : null;
req._proxyRequestBody = formatRequestBody(
req.headers['content-type'] ?? '',
buffer,
);
});
});
proxy.on('proxyRes', (proxyRes, req) => {
const raw = proxyRes.headers['set-cookie'];
let cookies = raw ? (Array.isArray(raw) ? raw : [raw]) : null;
// Backend sets Secure; browsers/WebViews on plain HTTP refuse it.
if (cookies && !useHttps) {
cookies = cookies.map((c) => c.replace(/;\s*Secure/gi, ''));
proxyRes.headers['set-cookie'] = cookies;
}
const responseChunks = [];
proxyRes.on('data', (chunk) => responseChunks.push(chunk));
proxyRes.on('end', () => {
const responseBuffer = responseChunks.length
? Buffer.concat(responseChunks)
: null;
const parts = [
'[vite-proxy]',
req.method,
req.url,
String(proxyRes.statusCode),
];
if (hasRequestBody(req.method)) {
parts.push(`body=${req._proxyRequestBody ?? '(empty)'}`);
}
if (cookies?.length) {
parts.push(`set-cookie=${cookies.map((c) => c.split(';')[0]).join(',')}`);
}
parts.push(formatResponseBody(
proxyRes.statusCode,
proxyRes.headers['content-type'] ?? '',
responseBuffer,
));
console.log(parts.join(' '));
});
});
},
},
},
};
export default defineConfig(({ command }) => ({
resolve: sharedResolve,
build: {
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
// Consolidate every lazy(() => import('./pages/...')) route
// module into ONE chunk so we don't ship ~30 tiny 0.1–0.4 kB
// files. Still loaded on first route navigation (not on the
// initial bundle). For per-section chunks instead, replace this
// with groups matching /\/src\/pages\/(artikel|lieferung|...)\//.
name: 'routes',
test: /\/src\/pages\//,
},
{
name: 'react-vendor',
test: /node_modules\/(react|react-dom|react-router|react-router-dom|scheduler)\//,
},
{
name: 'mui-vendor',
test: /node_modules\/(@mui|@emotion)\//,
},
{
name: 'jsquash-image',
test: /node_modules\/@jsquash\/(avif|resize)\//,
},
],
},
},
},
},
plugins: [
react(),
...(command === 'build'
? [
VitePWA({
registerType: 'autoUpdate',
injectRegister: 'auto',
manifest: {
id: '/',
name: 'Dashfront',
short_name: 'Dashfront',
start_url: '/',
scope: '/',
display: 'standalone',
background_color: '#c5ddd8',
theme_color: '#3b5bdb',
icons: [
{
src: '/icons/icon-192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/icons/icon-512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: '/icons/icon-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'],
globIgnores: ['**/*.wasm'],
navigateFallback: 'index.html',
navigateFallbackDenylist: [/^\/api/],
},
}),
]
: []),
],
server: sharedServer,
preview: sharedServer,
}));