import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'electron-vite'; import type { Plugin } from 'vite'; import { CSP_PLATZHALTER, cspRichtlinie } from './src/shared/csp'; /** * Version aus der package.json, zur Bauzeit fest eingesetzt. * * `app.getVersion()` liefert nur im gepackten Zustand die Anwendungsversion. * Wird der gebaute Einstiegspunkt direkt gestartet – etwa in der Entwicklung * oder in den Oberflächentests – findet Electron keine package.json und gibt * ersatzweise die Electron-Version zurück. Das stand dann fälschlich als * „Programmversion" auf dem Startbildschirm. */ const paket = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')) as { version: string; }; /** Ein Git-Aufruf, dessen Fehlschlag den Bau nicht aufhält. */ function git(...argumente: readonly string[]): string | null { try { return execFileSync('git', argumente, { cwd: __dirname, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], }).trim(); } catch { /* Kein Git, kein Arbeitsbaum, kein Commit – etwa beim Bau aus einem Quellarchiv. Die Anwendung läuft ohne Baukennung genauso. */ return null; } } /** * Woraus dieser Bau entstanden ist. * * Die Versionsnummer allein reicht nicht: Zwischen zwei Veröffentlichungen * entstehen viele Bauten mit derselben Nummer, und wer eine Rückmeldung gibt * („bei mir tut X nicht“), kann sonst nicht sagen, welchen Stand er vor sich * hat. * * Genommen wird das **Commit-Datum**, nicht das Bau-Datum: Dasselbe Commit * ergibt so immer dieselbe Kennung, und der Bau bleibt wiederholbar. Ein `+` * verrät einen Arbeitsbaum mit ungesicherten Änderungen – dann stimmt der * Quellstand eben nicht mit dem Commit überein, und das muss man sehen. */ function baukennung(): { kennung: string; stand: string } { const kurz = git('rev-parse', '--short', 'HEAD'); if (kurz === null) { return { kennung: '', stand: '' }; } /* `--untracked-files=no`, und das mit Absicht: electron-vite legt beim Bauen eine transpilierte Kopie dieser Datei daneben (`electron.vite.config..mjs`) und räumt sie danach weg. Ohne den Schalter meldete `status` genau diese Datei – und jeder Bau truegänge ein `+`, auch aus einem tadellos sauberen Arbeitsbaum. Eine Marke, die immer leuchtet, sagt nichts mehr. Der Preis: Eine völlig neue, noch nicht aufgenommene Quelldatei fällt nicht auf. Der Fall ist selten und fällt anderswo laut auf – wer das Commit auscheckt, bekommt einen Bau, der gar nicht erst durchläuft. */ const schmutzig = (git('status', '--porcelain', '--untracked-files=no') ?? '').length > 0; const datum = git('log', '-1', '--format=%cI') ?? ''; return { kennung: schmutzig ? `${kurz}+` : kurz, stand: datum.slice(0, 10) }; } const bau = baukennung(); /** * Ersetzt den Platzhalter im `` * durch die Richtlinie aus `src/shared/csp.ts`. * * Im Dev-Modus braucht Vite `'unsafe-inline'` (HMR-Präambel, injizierte * Styles) – im Produktionsbuild gilt die strenge Variante. * * `'meta'` als Ziel, und das ist hier der ganze Punkt: Direktiven, die ein * Browser im ``-Element ignoriert (`frame-ancestors`), bleiben draußen. * Sonst schreibt Chromium bei jedem Start einen Konsolenfehler über eine * Direktive, die ohnehin nicht gilt. Den Kopf-Weg bedient * `src/main/sicherheit.ts`. */ function cspPlugin(): Plugin { return { name: 'lernapp-csp', transformIndexHtml: { order: 'pre', handler(html, ctx) { const modus = ctx.server ? 'development' : 'production'; return html.replaceAll(CSP_PLATZHALTER, cspRichtlinie(modus, 'meta')); }, }, }; } export default defineConfig({ main: { define: { __ANWENDUNGS_VERSION__: JSON.stringify(paket.version), __BAUKENNUNG__: JSON.stringify(bau.kennung), __BAUSTAND__: JSON.stringify(bau.stand), }, build: { // Native und CommonJS-Abhängigkeiten (better-sqlite3) dürfen nicht // gebündelt werden – sie bleiben in node_modules und landen über // electron-builder im Paket. `true` ist zwar der Standard, steht hier // aber bewusst explizit, weil das Gegenstück im Preload abweicht. externalizeDeps: true, rollupOptions: { input: { index: resolve(__dirname, 'src/main/index.ts') }, }, }, }, preload: { build: { // MUSS false sein: mit `sandbox: true` kann das Preload-Skript zur // Laufzeit nichts nachladen, also muss alles gebündelt sein. // Ausgabeformat bleibt CommonJS (package.json hat kein // "type": "module") – sandboxed Preloads unterstützen kein ESM. externalizeDeps: false, rollupOptions: { input: { index: resolve(__dirname, 'src/preload/index.ts') }, }, }, }, renderer: { root: resolve(__dirname, 'src/renderer'), resolve: { alias: { '@renderer': resolve(__dirname, 'src/renderer/src'), '@shared': resolve(__dirname, 'src/shared'), }, }, plugins: [react(), cspPlugin()], build: { rollupOptions: { input: { index: resolve(__dirname, 'src/renderer/index.html') }, }, }, }, });