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