import { cpSync, mkdirSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, join } from 'node:path' import { defineConfig, type Plugin } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import { VitePWA } from 'vite-plugin-pwa' /** * Die 14 Standardschriften von PDF.js nach public/ kopieren. * * Aeltere PDFs betten Helvetica, Times und Courier nicht ein, sondern setzen * sie voraus. Ohne diese Dateien malt PDF.js mit einer Ersatzschrift – lesbar, * aber Zeilen brechen an anderer Stelle als im Original. * * Kopiert statt eingecheckt: 800 KB fremde Binaerdateien gehoeren nicht in den * Quelltext, und die Fassung soll der Abhaengigkeit folgen. public/pdfjs steht * deshalb in .gitignore. */ /** * Excalidraws Schriften selbst ausliefern. * * Ohne das laedt die Komponente sie zur Laufzeit von einem fremden Ursprung – * und die Sicherheitsrichtlinie (`default-src 'self'`, kein `font-src`) * verbietet genau das. Die Zeichnung erschiene dann in einer Ersatzschrift, * und man sucht den Grund im Zeichenbrett statt in der Richtlinie. * * Gesteuert wird es ueber `window.EXCALIDRAW_ASSET_PATH` (siehe index.html). * * **Xiaolai bleibt draussen.** Die chinesische Schrift ist 13 der 14 MB; alles * andere zusammen sind 512 KB. Wer Chinesisch in eine Zeichnung tippt, bekommt * eine Ersatzschrift – das ist der Preis dafuer, das Abbild nicht um 13 MB * aufzublaehen. Wird sie gebraucht, hier die Zeile streichen. */ function excalidrawAssets(): Plugin { return { name: 'aether-excalidraw-assets', config() { // Direkter Pfad statt require.resolve: Das Paket gibt seine // package.json in `exports` nicht frei, die Aufloesung scheitert daran. const wurzel = join(import.meta.dirname, 'node_modules', '@excalidraw', 'excalidraw', 'dist', 'prod') const ziel = join(import.meta.dirname, 'public', 'excalidraw') mkdirSync(ziel, { recursive: true }) cpSync(join(wurzel, 'fonts'), join(ziel, 'fonts'), { recursive: true, filter: (quelle) => !quelle.includes('Xiaolai'), }) }, } } function pdfjsStandardFonts(): Plugin { return { name: 'aether-pdfjs-standard-fonts', // Nicht 'buildStart': Im Entwicklungsbetrieb liest der Server public/ // beim Start ein, das Kopieren muss vorher passiert sein. config() { const require = createRequire(import.meta.url) const from = join(dirname(require.resolve('pdfjs-dist/package.json')), 'standard_fonts') const to = join(import.meta.dirname, 'public', 'pdfjs', 'standard_fonts') mkdirSync(to, { recursive: true }) cpSync(from, to, { recursive: true }) }, } } export default defineConfig({ plugins: [ pdfjsStandardFonts(), excalidrawAssets(), react(), tailwindcss(), VitePWA({ registerType: 'autoUpdate', workbox: { // Der Service Worker beantwortet Navigationen — also den Aufruf der // Seite selbst — **Netz zuerst, Cache als Rueckfall**. // // Frueher lag index.html im Vorab-Cache und wurde von dort geliefert. // Das war schnell, hatte aber eine unscheinbare Folge: Ein Eintrag im // Cache ist eine ganze Antwort, **mitsamt ihren Kopfzeilen**. Der // Browser bekam also dauerhaft die CSP von damals. Eine Aenderung an // HONEYBAR_ORIGIN erreichte ihn nie — der Inhalt der Datei aendert sich // ja nicht, und der Vorab-Cache holt nur nach, was sich geaendert hat. // Sichtbar wurde das als Widerspruch, an dem nichts falsch aussah: Das // Skript laedt den neuen Ursprung, die CSP erlaubt den alten. // // precacheFallback haelt dabei, was navigateFallback vorher leistete: // Ist weder Netz noch Cache da, antwortet die vorab abgelegte // index.html — Adressen wie /admin oder /s/ funktionieren also // weiterhin offline. // // Ausgenommen sind Downloads und WebDAV: Ein Klick auf „Download" // oeffnet eine Navigation zu /api/v1/files//download. Faengt der // Worker die ab, liefert er die Anwendung statt der Datei — und es // passiert sichtbar nichts. Sie sollen am Worker vorbeigehen, nicht // von ihm beantwortet werden. // Der Vorab-Cache beantwortet sonst auch den Aufruf von „/" — er // rechnet index.html als Verzeichnisseite an. Genau daran scheiterte // der erste Anlauf: Die Regel unten kam nie zum Zug, weil die Seite // schon vorher aus dem Vorab-Cache beantwortet wurde, mitsamt alter // CSP. Als reiner Rueckfall (precacheFallback) bleibt index.html dort // liegen, nur eben nicht mehr als Antwort auf jede Navigation. directoryIndex: null, // vite-plugin-pwa setzt sonst von sich aus navigateFallback: 'index.html' // — also genau die Vorab-Cache-Antwort, die wir hier los sein wollen. // Die Sperrliste nimmt jede Adresse davon aus; zustaendig ist dann die // Regel darunter. navigateFallbackDenylist: [/./], runtimeCaching: [ { urlPattern: ({ request, url }) => request.mode === 'navigate' && !/^\/(api|dav|share|branding|wopi)\//.test(url.pathname), handler: 'NetworkFirst', options: { cacheName: 'aether-seiten', // Ohne Grenze haengt der Start am langsamsten Netz. networkTimeoutSeconds: 3, precacheFallback: { fallbackURL: 'index.html' }, }, }, ], // PDF.js bleibt aussen vor: Betrachter (435 KB), Arbeiter (1,3 MB) // und Standardschriften (800 KB) sind zusammen zweieinhalb Megabyte, // die nur braucht, wer ein PDF oeffnet. Vorab in den Cache zu legen // hiesse, das jeder Installation aufzuladen. // // Offline waere davon ohnehin nichts gewonnen: Die Dokumente selbst // liegen auf dem Server und werden nicht mitgecacht – ein Betrachter // ohne Datei nuetzt nichts. globIgnores: [ '**/pdfjs/standard_fonts/**', '**/pdf.worker*.mjs', '**/PdfViewer-*.js', '**/PdfViewer-*.css', ], maximumFileSizeToCacheInBytes: 3 * 1024 * 1024, }, // Das Manifest liefert der Server (/manifest.webmanifest): Name, Farbe // und Symbol stehen in der Datenbank und sollen dem Admin-Bereich // folgen. Gebaut waeren sie fest verdrahtet, und ein hochgeladenes // Symbol erreichte die installierte App nie. manifest: false, }), ], server: { host: true, // auf allen Interfaces lauschen (IPv4 + IPv6), nötig für VS-Code-Port-Forwarding proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, proxyTimeout: 60 * 60 * 1000, timeout: 60 * 60 * 1000, }, '/share': { target: 'http://localhost:8080', changeOrigin: true, }, // Logo, App-Symbol und Manifest kommen ebenfalls vom Backend – ohne // diese beiden Eintraege fehlen sie im Entwicklungsbetrieb. '/branding': { target: 'http://localhost:8080', changeOrigin: true, }, '/manifest.webmanifest': { target: 'http://localhost:8080', changeOrigin: true, }, }, }, test: { // **node, nicht jsdom.** Die Einstellung stand auf 'jsdom', ohne dass // jsdom je installiert war – `npm test` brach deshalb ab, bevor es // irgendetwas ausführte. Geprüft wird hier, was sich ohne Browser prüfen // lässt: Reihenfolgen, Zoomstufen, Suche im Text, Einheiten. // // Wer eine Komponente prüfen will, schreibt `// @vitest-environment jsdom` // an den Anfang seiner Datei und nimmt jsdom in die devDependencies auf. environment: 'node', globals: true, setupFiles: './src/test-setup.ts', // Excalidraw durch die Vite-Verarbeitung schicken statt es Node // aufloesen zu lassen: Es importiert `roughjs/bin/rough` ohne Endung, und // Nodes ESM-Aufloesung verlangt sie. Der Buendler kennt diese Schreibweise, // Vitest im Normalbetrieb nicht. server: { deps: { inline: [/@excalidraw/] } }, }, })