lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ vite.config.ts

10,7 KB Rohdatei
vite.config.ts — 268 Zeilen
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 });