Eine Zeichnung ist eine Datei, kein neues Ding – das ist die ganze Entscheidung dahinter. .excalidraw ist JSON; damit erbt sie Ordner, Gruppenordner, Papierkorb, Versionen, Freigabelinks, interne Freigaben und die Suche. Keine neue Tabelle, kein zweites Rechtemodell. Gespeichert wird über denselben Upload wie alles andere, wie beim Text-Editor in der Vorschau. Drei Sorgen, alle nachgemessen statt gehofft: Das Hauptbündel wächst von 484,11 auf 485,22 kB – das Zeichenbrett ist ein eigenes Stück (584 kB + 142 kB CSS) und wird nachgeladen wie der PDF-Betrachter. Wer nie zeichnet, holt es nie. Die Schriften: Die Richtlinie ist default-src 'self' ohne font-src, Excalidraw hätte seine handgezeichneten Schriften von aussen geladen und still eine Ersatzschrift gezeigt. Sie werden jetzt beim Bauen kopiert und über window.EXCALIDRAW_ASSET_PATH gefunden – gesetzt im Modul, nicht per Inline-Skript, das wäre von derselben Richtlinie blockiert. Xiaolai bleibt draussen: 13 der 14 MB für die chinesische Schrift. Die Reihenfolge der Dateiarten: .excalidraw wird vor code und text geprüft, sonst öffnete eine Zeichnung im Texteditor als Zahlenkolonne (json steht in codeExt). Kein gleichzeitiges Arbeiten. Die Komponente könnte es, aber den Transport bringt sie nicht mit – das wäre ein zweiter Dienst neben Aether. Solange es ihn nicht gibt, überschreibt der Zweite den Ersten. Zwei Dinge fangen das ab: Vor dem Speichern wird geprüft, ob sich die Fassung geändert hat, und dann gefragt statt überschrieben. Und Zeichnungen behalten ihre Fassungen von der ersten Minute an. Der zweite Punkt ist erst durch den Selbsttest aufgefallen: Versionierung ist sonst bewusst aus (Migration 000006), damit der Speicher nicht vollläuft. Für Zeichnungen stimmt die Rechnung nicht – sie werden in sich immer wieder bearbeitet und sind ein paar Kilobyte gross. Ohne sie wäre die Warnung im Editor ein Versprechen ohne Deckung gewesen. Neuer Selbsttest mit 12 Prüfungen: Er prüft die These nach, auf der alles beruht – wenn eine Zeichnung wirklich eine Datei ist, muss sie können, was Dateien können. Nebenbei: db-tests.sh liess je Lauf ein anonymes Postgres-Volume liegen (rund 49 MB). `docker rm -v` nimmt es jetzt mit; zehn verwaiste entfernt. Geprüft: go build/vet sauber, alle 15 Go-Pakete grün, 255 Prüfungen der acht Selbsttest-Suiten grün, 48 Vitest-Tests, tsc sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
186 lines
7.8 KiB
TypeScript
186 lines
7.8 KiB
TypeScript
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/<token> funktionieren also
|
||
// weiterhin offline.
|
||
//
|
||
// Ausgenommen sind Downloads und WebDAV: Ein Klick auf „Download"
|
||
// oeffnet eine Navigation zu /api/v1/files/<id>/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',
|
||
},
|
||
})
|