import { defineConfig, type Plugin } from 'vite';
import { fileURLToPath, URL } from 'node:url';
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
/**
* Programmversion - eine Quelle, nicht zwei.
*
* Sie stand doppelt: in package.json, aus der electron-builder den Dateinamen
* des Installationspakets bildet, und noch einmal als Konstante in
* src/main.ts, aus der die Statusleiste und der Ausdruck sie nehmen. Zwei
* Angaben derselben Sache laufen auseinander - und dann traegt eine Unterlage,
* die in ein Verwaltungsverfahren geht, eine andere Versionsangabe als das
* Paket, mit dem sie erstellt wurde.
*/
const VERSION: string = (
JSON.parse(readFileSync(fileURLToPath(new URL('./package.json', import.meta.url)), 'utf8')) as {
version: string;
}
).version;
/**
* Inhaltssicherheitsrichtlinie.
*
* Sie wird nur in den Auslieferungsstand eingesetzt: Im Entwicklungsbetrieb
* benoetigt Vite eine WebSocket-Verbindung und eingebettete Skripte fuer das
* Neuladen. Der ausgelieferte Stand laedt dagegen nichts aus dem Netz nach -
* `default-src 'none'` verbietet es ausdruecklich.
*
* Der Altbestand hatte gar keine Richtlinie und lud Leaflet, jsPDF und
* html2canvas bei jedem Start von fremden Servern, obwohl die Programm-
* beschreibung ausdruecklich vollstaendige Offline-Faehigkeit zusicherte.
*/
const CONTENT_SECURITY_POLICY = [
"default-src 'none'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"font-src 'self' data:",
"connect-src 'self' blob:",
"base-uri 'none'",
"form-action 'none'",
"object-src 'none'",
// frame-ancestors wirkt nur als Antwortkopf und wird in einem Meta-Element
// ignoriert; es steht deshalb ausschliesslich in electron/main.ts.
].join('; ');
function contentSecurityPolicy(): Plugin {
return {
name: 'lsa-inhaltssicherheitsrichtlinie',
apply: 'build',
transformIndexHtml(html) {
return html.replace(
'',
``,
);
},
};
}
const WURZEL = fileURLToPath(new URL('.', import.meta.url));
/** Lizenzdatei, Lizenzhinweis, COPYING - dieselbe Regel wie in scripts/lizenztexte.mjs. */
const LIZENZDATEI = /^(licen[sc]e|copying)/i;
const TRENNLINIE = '='.repeat(76);
const UNTERSTRICH = '-'.repeat(76);
/** Einmal je Bibliothek gelesen, in mehreren Ausgabebrocken gebraucht. */
const abschnitte = new Map();
/**
* Der Lizenzabschnitt einer Bibliothek - Kopf und Text, woertlich aus dem
* installierten Paket.
*
* WARUM DER GANZE TEXT UND NICHT NUR DIE URHEBERZEILE: Der MIT-Text verlangt
* beides, Urheber- UND Genehmigungsvermerk. Die Urheberzeile allein liesse
* sich nicht verlaesslich herausloesen - jspdf schreibt "Copyright" auf eine
* eigene Zeile und die beiden Rechteinhaber darunter, dompurify liefert
* Apache-2.0 und MPL-2.0 in einer Datei, und die zwoelf MIT-Pakete fuehren den
* Genehmigungsvermerk in drei verschiedenen Wortlauten - zehn gleichlautend,
* rgbcolor und svg-pathdata je eigen. Woertlich ist ohne Faustregel und
* deshalb ohne stille Luecke.
*
* WARUM ES LIEBER ABBRICHT: Aus demselben Grund wie scripts/lizenztexte.mjs.
* Ein Banner, in dem eine Bibliothek fehlt, sieht nach Erfuellung aus und ist
* keine.
*/
function lizenzabschnitt(name: string): string {
const schon = abschnitte.get(name);
if (schon !== undefined) return schon;
const ablage = join(WURZEL, 'node_modules', name);
const paket = JSON.parse(readFileSync(join(ablage, 'package.json'), 'utf8')) as {
version?: string;
license?: string;
};
const dateien = readdirSync(ablage)
.filter((d) => LIZENZDATEI.test(d))
.sort();
if (dateien.length === 0) {
throw new Error(
`Keine Lizenzdatei in node_modules/${name} - abgebrochen, nichts gebaut. Ein Banner ohne diesen Text sieht nach Erfuellung aus und ist keiner.`,
);
}
const zeilen = [
TRENNLINIE,
`${name} ${String(paket.version)} – ${String(paket.license ?? '(keine Angabe im Paket)')}`,
TRENNLINIE,
];
for (const datei of dateien) {
// Zeilenenden vereinheitlichen wie in scripts/lizenztexte.mjs: Die Pakete
// bringen beides mit, und die Ausgabedatei fuehrt sonst gemischte.
const text = readFileSync(join(ablage, datei), 'utf8').replace(/\r\n/g, '\n').trim();
if (text === '') {
throw new Error(`node_modules/${name}/${datei} ist leer - abgebrochen, nichts gebaut.`);
}
if (text.includes('*/')) {
// Ein Lizenztext mit dieser Zeichenfolge schloesse den Kommentar mitten
// im Banner; was danach kaeme, waere Kode. Lieber gar nicht bauen.
throw new Error(
`node_modules/${name}/${datei} enthaelt die Zeichenfolge, die einen Blockkommentar schliesst - abgebrochen, nichts gebaut.`,
);
}
zeilen.push('', `(node_modules/${name}/${datei})`, UNTERSTRICH, '', text);
}
const abschnitt = zeilen.join('\n');
abschnitte.set(name, abschnitt);
return abschnitt;
}
/**
* Welche Bibliotheken hat Rollup in diesen Ausgabebrocken uebersetzt?
*
* Alles unterhalb von node_modules/ ist ein Fremdbestandteil; ein Bereichsname
* (@babel/runtime) besteht aus zwei Abschnitten, jeder andere aus einem - genau
* dieselbe Ableitung, die scripts/lizenztexte.mjs nach dem Bau aus den
* Quellkarten zieht.
*
* AUSGENOMMEN SIND KENNUNGEN MIT `?`. Das sind die Zwischenstuecke, die Rollups
* CommonJS-Uebersetzung selbst erzeugt (`?commonjs-es-import` und Verwandte).
* Ihr Kode stammt nicht aus dem Paket, und die Quellkarten fuehren sie nicht.
* Ohne diesen Filter truege index-*.js einen Vermerk fuer jspdf-autotable,
* dessen Kode dort nicht steht, sondern in pdf-*.js - ein Vermerk am falschen
* Ort ist keine Erfuellung, sondern eine falsche Angabe.
*/
function fremdbestandteile(moduleIds: readonly string[]): string[] {
const namen = new Set();
for (const roh of moduleIds) {
const id = roh.replace(/\\/g, '/');
if (id.includes('?')) continue;
const stelle = id.lastIndexOf('node_modules/');
if (stelle < 0) continue;
const teile = id.slice(stelle + 'node_modules/'.length).split('/');
namen.add(
String(teile[0]).startsWith('@')
? `${String(teile[0])}/${String(teile[1])}`
: String(teile[0]),
);
}
return [...namen].sort();
}
/**
* Der Banner eines Ausgabebrockens - die Lizenztexte der Bibliotheken, deren
* Kode wirklich darin steht.
*
* WARUM GESCHRIEBEN UND NICHT GESUCHT: Vier von dreizehn Bibliotheken trugen
* ihren Urhebervermerk in der Ausgabedatei, neun nicht - sechs bringen
* ueberhaupt keinen mit, zwei verlieren ihn an esbuild, und den von jspdf legt
* schon Rollup ab. An der Verkuerzung liegt das nicht; die Messung dazu steht
* unten neben `target`. Der Vermerk wird deshalb hineingeschrieben.
*
* WARUM EIN LEGALER KOMMENTAR (`/*!`): Rollup setzt den Banner, BEVOR die
* renderChunk-Haken laufen - und durch einen davon schickt Vite esbuild.
* esbuild gibt nur legale Kommentare wieder und verwirft jeden anderen; ein
* gewoehnlicher Banner ueberlebte den Bau nicht. Nachgemessen: mit `/*!` steht
* er in allen vier Ausgabedateien, in einem gewoehnlichen Kommentar in keiner.
*
* DIE VIER MIT EIGENEM VERMERK BEKOMMEN IHN TROTZDEM. Sie auszunehmen hiesse,
* die verkuerzte Ausgabe nach dem Namen des Rechteinhabers zu durchsuchen -
* eine Faustregel, die mit der naechsten Paketfassung still ausfaellt. Der
* doppelte Vermerk ist nicht billig: Nimmt man die vier heraus, schrumpft die
* Ausgabe um 33 213 der 48 219 Byte, die der Banner kostet - 28 541 davon
* allein dompurify, dessen Ausgabedatei im Betrieb nie geladen wird
* (gemessen am 07.09.2026 an zwei Bauten). Er kann dafuer nicht ausfallen.
*/
function lizenzbanner(namen: readonly string[]): string {
if (namen.length === 0) return '';
return [
'/*!',
`LSA-Planer Professional ${VERSION} – Lizenztexte der Bibliotheken in dieser Datei`,
'',
'Vite hat den Code der folgenden Bibliotheken in diese Datei übersetzt. Ihre',
'Lizenzen verlangen, dass Urheber- und Genehmigungsvermerk mit jeder Kopie',
'gehen; sie stehen deshalb hier, wörtlich so, wie sie dem jeweiligen Paket',
'beiliegen. Dieselben Texte liegen als DRITTANBIETER-LIZENZEN.txt neben der',
'Programmdatei, das Verzeichnis dazu als FREMDBESTANDTEILE.md.',
'',
'Das ist keine Rechtsauskunft.',
'',
`Es folgen ${namen.length} Texte.`,
'',
...namen.map((name) => `${lizenzabschnitt(name)}\n`),
'*/',
].join('\n');
}
// Der Renderer wird als reine Datei-Anwendung ausgeliefert (file://) und muss
// deshalb ausschliesslich relative Pfade verwenden. Es werden keinerlei Inhalte
// aus dem Netz nachgeladen - alle Bibliotheken und Schriften sind gebuendelt.
export default defineConfig({
plugins: [contentSecurityPolicy()],
define: {
__APP_VERSION__: JSON.stringify(VERSION),
},
root: 'src',
base: './',
publicDir: fileURLToPath(new URL('./public', import.meta.url)),
build: {
outDir: fileURLToPath(new URL('./build/renderer', import.meta.url)),
emptyOutDir: true,
/*
* `minify` steht bewusst nicht hier - die Vorgabe 'esbuild' gilt.
*
* Nachgemessen am 07.09.2026, weil die Vermutung nahelag, die Verkuerzung
* entferne die Urhebervermerke der gebuendelten Bibliotheken: Sie tut es
* nicht. esbuild gibt ohnehin nur "legal comments" wieder (`/*!`, `//!`,
* `@license`, `@preserve`) und verwirft jeden anderen Kommentar auch bei
* `minify: false`; der Vermerk von jspdf ist ein solcher legaler Kommentar
* und faellt trotzdem schon beim Zusammenfassen durch Rollup weg. Ein Bau
* ohne Verkuerzung waechst von 1 446 254 auf 2 659 030 Byte und bringt
* keinen einzigen Vermerk zurueck; erst zusaetzlich `target: 'esnext'` -
* dann laeuft esbuild gar nicht - holt zwei von neun zurueck und gibt den
* Zielbrowser auf. Alle drei Zahlen sind ohne Banner gemessen und an
* einem frueheren Stand des Quelltexts desselben Tages; ausgeliefert
* werden mit Banner 1 495 988 Byte.
*
* Daraus folgt nicht, dass sich nichts tun laesst, sondern nur, dass es
* ueber eine Einstellung nicht geht: Die Vermerke werden seit 5.14.0
* hineingeschrieben, siehe `lizenzbanner` weiter oben.
* docs/fremdbestandteile.md fuehrt beide Messungen.
*/
target: 'chrome128',
assetsInlineLimit: 0,
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
pdf: ['jspdf', 'jspdf-autotable'],
},
banner: (chunk) => lizenzbanner(fremdbestandteile(chunk.moduleIds)),
},
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@shared': fileURLToPath(new URL('./shared', import.meta.url)),
},
},
server: {
port: 5273,
strictPort: true,
},
});