lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ vite.config.ts
| 1 | import { defineConfig, type Plugin } from 'vite'; |
| 2 | import { fileURLToPath, URL } from 'node:url'; |
| 3 | import { readdirSync, readFileSync } from 'node:fs'; |
| 4 | import { join } from 'node:path'; |
| 5 | |
| 6 | /** |
| 7 | * Programmversion - eine Quelle, nicht zwei. |
| 8 | * |
| 9 | * Sie stand doppelt: in package.json, aus der electron-builder den Dateinamen |
| 10 | * des Installationspakets bildet, und noch einmal als Konstante in |
| 11 | * src/main.ts, aus der die Statusleiste und der Ausdruck sie nehmen. Zwei |
| 12 | * Angaben derselben Sache laufen auseinander - und dann traegt eine Unterlage, |
| 13 | * die in ein Verwaltungsverfahren geht, eine andere Versionsangabe als das |
| 14 | * Paket, mit dem sie erstellt wurde. |
| 15 | */ |
| 16 | const VERSION: string = ( |
| 17 | JSON.parse(readFileSync(fileURLToPath(new URL('./package.json', import.meta.url)), 'utf8')) as { |
| 18 | version: string; |
| 19 | } |
| 20 | ).version; |
| 21 | |
| 22 | /** |
| 23 | * Inhaltssicherheitsrichtlinie. |
| 24 | * |
| 25 | * Sie wird nur in den Auslieferungsstand eingesetzt: Im Entwicklungsbetrieb |
| 26 | * benoetigt Vite eine WebSocket-Verbindung und eingebettete Skripte fuer das |
| 27 | * Neuladen. Der ausgelieferte Stand laedt dagegen nichts aus dem Netz nach - |
| 28 | * `default-src 'none'` verbietet es ausdruecklich. |
| 29 | * |
| 30 | * Der Altbestand hatte gar keine Richtlinie und lud Leaflet, jsPDF und |
| 31 | * html2canvas bei jedem Start von fremden Servern, obwohl die Programm- |
| 32 | * beschreibung ausdruecklich vollstaendige Offline-Faehigkeit zusicherte. |
| 33 | */ |
| 34 | const CONTENT_SECURITY_POLICY = [ |
| 35 | "default-src 'none'", |
| 36 | "script-src 'self'", |
| 37 | "style-src 'self' 'unsafe-inline'", |
| 38 | "img-src 'self' data: blob:", |
| 39 | "font-src 'self' data:", |
| 40 | "connect-src 'self' blob:", |
| 41 | "base-uri 'none'", |
| 42 | "form-action 'none'", |
| 43 | "object-src 'none'", |
| 44 | // frame-ancestors wirkt nur als Antwortkopf und wird in einem Meta-Element |
| 45 | // ignoriert; es steht deshalb ausschliesslich in electron/main.ts. |
| 46 | ].join('; '); |
| 47 | |
| 48 | function contentSecurityPolicy(): Plugin { |
| 49 | return { |
| 50 | name: 'lsa-inhaltssicherheitsrichtlinie', |
| 51 | apply: 'build', |
| 52 | transformIndexHtml(html) { |
| 53 | return html.replace( |
| 54 | '<!--sicherheitsrichtlinie-->', |
| 55 | `<meta http-equiv="Content-Security-Policy" content="${CONTENT_SECURITY_POLICY}" />`, |
| 56 | ); |
| 57 | }, |
| 58 | }; |
| 59 | } |
| 60 | |
| 61 | const WURZEL = fileURLToPath(new URL('.', import.meta.url)); |
| 62 | |
| 63 | /** Lizenzdatei, Lizenzhinweis, COPYING - dieselbe Regel wie in scripts/lizenztexte.mjs. */ |
| 64 | const LIZENZDATEI = /^(licen[sc]e|copying)/i; |
| 65 | |
| 66 | const TRENNLINIE = '='.repeat(76); |
| 67 | const UNTERSTRICH = '-'.repeat(76); |
| 68 | |
| 69 | /** Einmal je Bibliothek gelesen, in mehreren Ausgabebrocken gebraucht. */ |
| 70 | const abschnitte = new Map<string, string>(); |
| 71 | |
| 72 | /** |
| 73 | * Der Lizenzabschnitt einer Bibliothek - Kopf und Text, woertlich aus dem |
| 74 | * installierten Paket. |
| 75 | * |
| 76 | * WARUM DER GANZE TEXT UND NICHT NUR DIE URHEBERZEILE: Der MIT-Text verlangt |
| 77 | * beides, Urheber- UND Genehmigungsvermerk. Die Urheberzeile allein liesse |
| 78 | * sich nicht verlaesslich herausloesen - jspdf schreibt "Copyright" auf eine |
| 79 | * eigene Zeile und die beiden Rechteinhaber darunter, dompurify liefert |
| 80 | * Apache-2.0 und MPL-2.0 in einer Datei, und die zwoelf MIT-Pakete fuehren den |
| 81 | * Genehmigungsvermerk in drei verschiedenen Wortlauten - zehn gleichlautend, |
| 82 | * rgbcolor und svg-pathdata je eigen. Woertlich ist ohne Faustregel und |
| 83 | * deshalb ohne stille Luecke. |
| 84 | * |
| 85 | * WARUM ES LIEBER ABBRICHT: Aus demselben Grund wie scripts/lizenztexte.mjs. |
| 86 | * Ein Banner, in dem eine Bibliothek fehlt, sieht nach Erfuellung aus und ist |
| 87 | * keine. |
| 88 | */ |
| 89 | function lizenzabschnitt(name: string): string { |
| 90 | const schon = abschnitte.get(name); |
| 91 | if (schon !== undefined) return schon; |
| 92 | |
| 93 | const ablage = join(WURZEL, 'node_modules', name); |
| 94 | const paket = JSON.parse(readFileSync(join(ablage, 'package.json'), 'utf8')) as { |
| 95 | version?: string; |
| 96 | license?: string; |
| 97 | }; |
| 98 | const dateien = readdirSync(ablage) |
| 99 | .filter((d) => LIZENZDATEI.test(d)) |
| 100 | .sort(); |
| 101 | if (dateien.length === 0) { |
| 102 | throw new Error( |
| 103 | `Keine Lizenzdatei in node_modules/${name} - abgebrochen, nichts gebaut. Ein Banner ohne diesen Text sieht nach Erfuellung aus und ist keiner.`, |
| 104 | ); |
| 105 | } |
| 106 | |
| 107 | const zeilen = [ |
| 108 | TRENNLINIE, |
| 109 | `${name} ${String(paket.version)} – ${String(paket.license ?? '(keine Angabe im Paket)')}`, |
| 110 | TRENNLINIE, |
| 111 | ]; |
| 112 | for (const datei of dateien) { |
| 113 | // Zeilenenden vereinheitlichen wie in scripts/lizenztexte.mjs: Die Pakete |
| 114 | // bringen beides mit, und die Ausgabedatei fuehrt sonst gemischte. |
| 115 | const text = readFileSync(join(ablage, datei), 'utf8').replace(/\r\n/g, '\n').trim(); |
| 116 | if (text === '') { |
| 117 | throw new Error(`node_modules/${name}/${datei} ist leer - abgebrochen, nichts gebaut.`); |
| 118 | } |
| 119 | if (text.includes('*/')) { |
| 120 | // Ein Lizenztext mit dieser Zeichenfolge schloesse den Kommentar mitten |
| 121 | // im Banner; was danach kaeme, waere Kode. Lieber gar nicht bauen. |
| 122 | throw new Error( |
| 123 | `node_modules/${name}/${datei} enthaelt die Zeichenfolge, die einen Blockkommentar schliesst - abgebrochen, nichts gebaut.`, |
| 124 | ); |
| 125 | } |
| 126 | zeilen.push('', `(node_modules/${name}/${datei})`, UNTERSTRICH, '', text); |
| 127 | } |
| 128 | |
| 129 | const abschnitt = zeilen.join('\n'); |
| 130 | abschnitte.set(name, abschnitt); |
| 131 | return abschnitt; |
| 132 | } |
| 133 | |
| 134 | /** |
| 135 | * Welche Bibliotheken hat Rollup in diesen Ausgabebrocken uebersetzt? |
| 136 | * |
| 137 | * Alles unterhalb von node_modules/ ist ein Fremdbestandteil; ein Bereichsname |
| 138 | * (@babel/runtime) besteht aus zwei Abschnitten, jeder andere aus einem - genau |
| 139 | * dieselbe Ableitung, die scripts/lizenztexte.mjs nach dem Bau aus den |
| 140 | * Quellkarten zieht. |
| 141 | * |
| 142 | * AUSGENOMMEN SIND KENNUNGEN MIT `?`. Das sind die Zwischenstuecke, die Rollups |
| 143 | * CommonJS-Uebersetzung selbst erzeugt (`?commonjs-es-import` und Verwandte). |
| 144 | * Ihr Kode stammt nicht aus dem Paket, und die Quellkarten fuehren sie nicht. |
| 145 | * Ohne diesen Filter truege index-*.js einen Vermerk fuer jspdf-autotable, |
| 146 | * dessen Kode dort nicht steht, sondern in pdf-*.js - ein Vermerk am falschen |
| 147 | * Ort ist keine Erfuellung, sondern eine falsche Angabe. |
| 148 | */ |
| 149 | function fremdbestandteile(moduleIds: readonly string[]): string[] { |
| 150 | const namen = new Set<string>(); |
| 151 | for (const roh of moduleIds) { |
| 152 | const id = roh.replace(/\\/g, '/'); |
| 153 | if (id.includes('?')) continue; |
| 154 | const stelle = id.lastIndexOf('node_modules/'); |
| 155 | if (stelle < 0) continue; |
| 156 | const teile = id.slice(stelle + 'node_modules/'.length).split('/'); |
| 157 | namen.add( |
| 158 | String(teile[0]).startsWith('@') |
| 159 | ? `${String(teile[0])}/${String(teile[1])}` |
| 160 | : String(teile[0]), |
| 161 | ); |
| 162 | } |
| 163 | return [...namen].sort(); |
| 164 | } |
| 165 | |
| 166 | /** |
| 167 | * Der Banner eines Ausgabebrockens - die Lizenztexte der Bibliotheken, deren |
| 168 | * Kode wirklich darin steht. |
| 169 | * |
| 170 | * WARUM GESCHRIEBEN UND NICHT GESUCHT: Vier von dreizehn Bibliotheken trugen |
| 171 | * ihren Urhebervermerk in der Ausgabedatei, neun nicht - sechs bringen |
| 172 | * ueberhaupt keinen mit, zwei verlieren ihn an esbuild, und den von jspdf legt |
| 173 | * schon Rollup ab. An der Verkuerzung liegt das nicht; die Messung dazu steht |
| 174 | * unten neben `target`. Der Vermerk wird deshalb hineingeschrieben. |
| 175 | * |
| 176 | * WARUM EIN LEGALER KOMMENTAR (`/*!`): Rollup setzt den Banner, BEVOR die |
| 177 | * renderChunk-Haken laufen - und durch einen davon schickt Vite esbuild. |
| 178 | * esbuild gibt nur legale Kommentare wieder und verwirft jeden anderen; ein |
| 179 | * gewoehnlicher Banner ueberlebte den Bau nicht. Nachgemessen: mit `/*!` steht |
| 180 | * er in allen vier Ausgabedateien, in einem gewoehnlichen Kommentar in keiner. |
| 181 | * |
| 182 | * DIE VIER MIT EIGENEM VERMERK BEKOMMEN IHN TROTZDEM. Sie auszunehmen hiesse, |
| 183 | * die verkuerzte Ausgabe nach dem Namen des Rechteinhabers zu durchsuchen - |
| 184 | * eine Faustregel, die mit der naechsten Paketfassung still ausfaellt. Der |
| 185 | * doppelte Vermerk ist nicht billig: Nimmt man die vier heraus, schrumpft die |
| 186 | * Ausgabe um 33 213 der 48 219 Byte, die der Banner kostet - 28 541 davon |
| 187 | * allein dompurify, dessen Ausgabedatei im Betrieb nie geladen wird |
| 188 | * (gemessen am 07.09.2026 an zwei Bauten). Er kann dafuer nicht ausfallen. |
| 189 | */ |
| 190 | function lizenzbanner(namen: readonly string[]): string { |
| 191 | if (namen.length === 0) return ''; |
| 192 | return [ |
| 193 | '/*!', |
| 194 | `LSA-Planer Professional ${VERSION} – Lizenztexte der Bibliotheken in dieser Datei`, |
| 195 | '', |
| 196 | 'Vite hat den Code der folgenden Bibliotheken in diese Datei übersetzt. Ihre', |
| 197 | 'Lizenzen verlangen, dass Urheber- und Genehmigungsvermerk mit jeder Kopie', |
| 198 | 'gehen; sie stehen deshalb hier, wörtlich so, wie sie dem jeweiligen Paket', |
| 199 | 'beiliegen. Dieselben Texte liegen als DRITTANBIETER-LIZENZEN.txt neben der', |
| 200 | 'Programmdatei, das Verzeichnis dazu als FREMDBESTANDTEILE.md.', |
| 201 | '', |
| 202 | 'Das ist keine Rechtsauskunft.', |
| 203 | '', |
| 204 | `Es folgen ${namen.length} Texte.`, |
| 205 | '', |
| 206 | ...namen.map((name) => `${lizenzabschnitt(name)}\n`), |
| 207 | '*/', |
| 208 | ].join('\n'); |
| 209 | } |
| 210 | |
| 211 | // Der Renderer wird als reine Datei-Anwendung ausgeliefert (file://) und muss |
| 212 | // deshalb ausschliesslich relative Pfade verwenden. Es werden keinerlei Inhalte |
| 213 | // aus dem Netz nachgeladen - alle Bibliotheken und Schriften sind gebuendelt. |
| 214 | export default defineConfig({ |
| 215 | plugins: [contentSecurityPolicy()], |
| 216 | define: { |
| 217 | __APP_VERSION__: JSON.stringify(VERSION), |
| 218 | }, |
| 219 | root: 'src', |
| 220 | base: './', |
| 221 | publicDir: fileURLToPath(new URL('./public', import.meta.url)), |
| 222 | build: { |
| 223 | outDir: fileURLToPath(new URL('./build/renderer', import.meta.url)), |
| 224 | emptyOutDir: true, |
| 225 | /* |
| 226 | * `minify` steht bewusst nicht hier - die Vorgabe 'esbuild' gilt. |
| 227 | * |
| 228 | * Nachgemessen am 07.09.2026, weil die Vermutung nahelag, die Verkuerzung |
| 229 | * entferne die Urhebervermerke der gebuendelten Bibliotheken: Sie tut es |
| 230 | * nicht. esbuild gibt ohnehin nur "legal comments" wieder (`/*!`, `//!`, |
| 231 | * `@license`, `@preserve`) und verwirft jeden anderen Kommentar auch bei |
| 232 | * `minify: false`; der Vermerk von jspdf ist ein solcher legaler Kommentar |
| 233 | * und faellt trotzdem schon beim Zusammenfassen durch Rollup weg. Ein Bau |
| 234 | * ohne Verkuerzung waechst von 1 446 254 auf 2 659 030 Byte und bringt |
| 235 | * keinen einzigen Vermerk zurueck; erst zusaetzlich `target: 'esnext'` - |
| 236 | * dann laeuft esbuild gar nicht - holt zwei von neun zurueck und gibt den |
| 237 | * Zielbrowser auf. Alle drei Zahlen sind ohne Banner gemessen und an |
| 238 | * einem frueheren Stand des Quelltexts desselben Tages; ausgeliefert |
| 239 | * werden mit Banner 1 495 988 Byte. |
| 240 | * |
| 241 | * Daraus folgt nicht, dass sich nichts tun laesst, sondern nur, dass es |
| 242 | * ueber eine Einstellung nicht geht: Die Vermerke werden seit 5.14.0 |
| 243 | * hineingeschrieben, siehe `lizenzbanner` weiter oben. |
| 244 | * docs/fremdbestandteile.md fuehrt beide Messungen. |
| 245 | */ |
| 246 | target: 'chrome128', |
| 247 | assetsInlineLimit: 0, |
| 248 | sourcemap: true, |
| 249 | rollupOptions: { |
| 250 | output: { |
| 251 | manualChunks: { |
| 252 | pdf: ['jspdf', 'jspdf-autotable'], |
| 253 | }, |
| 254 | banner: (chunk) => lizenzbanner(fremdbestandteile(chunk.moduleIds)), |
| 255 | }, |
| 256 | }, |
| 257 | }, |
| 258 | resolve: { |
| 259 | alias: { |
| 260 | '@': fileURLToPath(new URL('./src', import.meta.url)), |
| 261 | '@shared': fileURLToPath(new URL('./shared', import.meta.url)), |
| 262 | }, |
| 263 | }, |
| 264 | server: { |
| 265 | port: 5273, |
| 266 | strictPort: true, |
| 267 | }, |
| 268 | }); |