Files
archivmail/features/PROJ-79-dependency-major-upgrades.md
T
sysops 4d25b11413 feat(PROJ-79): typescript 5 -> 6.0.3, PROJ-79 abgeschlossen
TS 7.0.2 getestet und verworfen: typescript-eslint@8.66.0 wirft explizit
"does not support TS 7.0" (Peer-Range <6.1.0, siehe typescript-eslint#10940).
tsc/lint/build auf 6.0.3 gruen.

Alle 5 Teilschritte von PROJ-79 bearbeitet - lucide-react, tailwindcss v4
+ tailwind-merge v3 und @types/node abgeschlossen, eslint 10 und
typescript 7 an Oekosystem-Blockern (Plugin-Support fehlt) dokumentiert
zurueckgestellt. Status auf Deployed.
2026-08-06 14:34:53 +02:00

20 KiB
Raw Blame History

id, title, status, created
id title status created
PROJ-79 Frontend-Dependency-Major-Upgrades (eslint, lucide-react, tailwindcss+tailwind-merge, typescript, @types/node) Deployed 2026-08-05

Problem

npm outdated zeigt mehrere Major-Version-Sprünge im Frontend, die nicht blind gezogen werden sollten (Build-/Runtime-Bruchrisiko):

Paket Aktuell Ziel Risiko
eslint 9.39.2 10.8.0 Config-Format
typescript 5.9.3 7.0.2 2 Majors auf einmal
tailwindcss 3.4.19 4.3.3 Komplett neue Config-Architektur
lucide-react 0.562.0 1.28.0 Icon-API-Änderungen
tailwind-merge 2.6.0 3.6.0 Hart an tailwindcss v4 gekoppelt
@types/node 20.19.28 26.1.2 Typen sollten zur echten Node-Version auf 131/132 passen

Wichtiger Befund während der Umsetzung: tailwind-merge v3 setzt laut offizieller README Tailwind CSS v4 voraus ("if you use Tailwind v3, use tailwind-merge v2.6.0"). Ein isoliertes Upgrade von tailwind-merge allein wäre eine offiziell unsupported Kombination — heute laut Messung (283.556 paarweise Klassen-Merges aus dem echten Codebestand verglichen, 0 Abweichungen) verhaltensneutral, aber *-opacity-*-Klassen (z.B. bg-opacity-50) würden künftig lautlos falsch gemerged statt einen Build-Fehler zu werfen.

Lösung (Vorschlag, priorisierte Reihenfolge)

  1. lucide-react 0.562.0 → 1.28.0 — unabhängig, grep -rn "from \"lucide-react\"" src/ für Impact-Fläche, danach Upgrade + tsc --noEmit (fängt umbenannte Icons als Compile-Fehler ab).
  2. eslint 9 → 10 + eslint-config-next (aktuell 16.1.1) nachziehen, Flat-Config-Breaking-Changes prüfen, npm run lint danach.
  3. @types/node 20 → 26 — vorher node -v auf 131 und 132 prüfen, Typen dürfen nicht vor der tatsächlichen Node-Laufzeit-Version liegen.
  4. tailwindcss v3→v4 + tailwind-merge v2→v3 zusammen (gekoppeltes Paar, nicht einzeln):
    • npx @tailwindcss/upgrade (offizielles Migrationstool)
    • tailwind-merge@3 im selben Schritt
    • shadcn/ui-Komponenten-Kompatibilität mit Tailwind 4 vorab prüfen
    • Lint-Regel gegen *-opacity-*-Klassen ergänzen (bg-/text-/border-/ring-/divide-/placeholder-opacity) — Absicherung gegen stummen Styling-Bruch, da diese Klassen in v4 vom Merge verschluckt werden ohne Fehler
    • Voller visueller Regressionstest (größter Einzelschritt im gesamten Upgrade-Batch)
  5. typescript 5 → 6 → 7 — stufenweise, nicht direkt springen, nach jedem Schritt tsc --noEmit komplett grün vor dem nächsten.

Jeder Schritt: eigener Commit, erst auf 132 build+visuell testen, dann 131.

Implementation Notes

  • 2026-08-05: tailwind-merge-Einzelupgrade gestoppt nach Entdeckung der v4-Kopplung (siehe Problem-Abschnitt). Kein Code geändert, git status war danach sauber. Reihenfolge oben entsprechend revidiert — Punkt 4 bündelt beide Pakete statt sie getrennt zu behandeln.

lucide-react Upgrade (Punkt 1) — 2026-08-05

  • lucide-react 0.562.0 → 1.28.0 (npm install lucide-react@^1). Geändert wurden nur package.json / package-lock.jsonkein Anwendungscode musste angepasst werden.
  • Impact-Fläche: 19 Dateien unter src/, alle Imports einzeilig, insgesamt 20 verschiedene Icons: Bookmark, BookmarkPlus, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, FileText, Info, Lock, MailPlus, Moon, MoreHorizontal, PanelLeft, Search, Server, Sun, Trash2, X.
  • Keine Icon-Umbenennungen nötig: alle 20 Namen existieren unverändert in den v1-Typdeklarationen (node_modules/lucide-react/dist/lucide-react.d.ts) — es handelt sich durchweg um kanonische Namen, keine veralteten Aliase, die beim 0.x→1.x-Sprung entfallen wären.
  • Verifikation: npx tsc --noEmit0 Fehler; npm run builderfolgreich, alle 14 Routen generiert.
  • Kein Live-Browser-Test durchgeführt (laut Spec-Auftrag nicht nötig, da fehlende Icon-Exporte vollständig als Compile-Fehler auftreten würden).
  • Offen: Verifikation auf 132, danach 131-Deploy.

npm audit fix + Browserslist-Refresh — 2026-08-05

Anlass: beim 131-Deploy meldete npm ci 26 Vulnerabilities (1 low, 6 moderate, 19 high) sowie eine 8 Monate alte caniuse-lite-Datenbank (Browserslist). Lokal (nach dem lucide-react-Upgrade) zeigte npm audit nur 6 Vulnerabilities (@babel/core low, brace-expansion/js-yaml/ next/postcss/sharp high) — Differenz vermutlich Lock-Datei-Drift zwischen Workstation und 131 zum Zeitpunkt des Checks.

  • npm audit fix (ohne --force, da alle 6 Funde fixAvailable: true ohne SemVer-Major-Flag waren) → 0 Vulnerabilities. next wanderte dabei minor von 16.2.9 auf 16.3.0 (im ^16.1.1-Range, kein Breaking Change).
  • npx update-browserslist-db@latestcaniuse-lite aktualisiert (1.0.30001763 → 1.0.30001806), keine Target-Browser-Änderung.
  • Verifikation: npx tsc --noEmit 0 Fehler, npm run build erfolgreich, alle 14 Routen generiert.
  • update.sh erweitert: nach npm ci läuft jetzt npm audit --audit-level=high als Warn-Gate (nicht blockierend, kein Auto-Fix während des laufenden Deploys — Breaking-Change-Risiko live auf Produktivsystem wäre inakzeptabel). Bei Fund: Warnung mit Verweis auf /tmp/npm-audit-report.txt und Hinweis, npm audit fix lokal auszuführen/zu testen/zu committen statt es automatisch im Deploy zu fahren.
  • Deployed auf 132 und 131 am 2026-08-05. Beide Deploys sauber: npm ci meldete auf beiden Servern 0 vulnerabilities (vorher 26 auf 131), keine Browserslist-Warnung mehr im update.sh-Output, das neue npm-audit-Warn-Gate schlug erwartungsgemäß nicht an. Backend ✓ läuft und Frontend ✓ läuft auf beiden Servern.

eslint Upgrade (Punkt 2) — 2026-08-05 — blockiert, nicht abgeschlossen

Ergebnis: eslint bleibt vorerst auf 9.x. Das Upgrade auf 10.8.0 wurde durchgeführt, getestet und wieder zurückgenommen — Ursache ist ein Ökosystem-Blocker (siehe unten), kein Fehler in unserem Code. Die dabei ohnehin fällige Flat-Config-Migration wurde behalten, weil sie einen bestehenden Defekt behebt.

Nebenbefund (wichtig): npm run lint war bereits vor dem Upgrade kaputt. Das Script stand auf next lint, aber Next.js 16 hat den next lint-Befehl entfernt. Der Aufruf scheiterte mit Invalid project directory provided, no such directory: .../lint — Linting fand seit dem Next-16-Upgrade also faktisch gar nicht mehr statt. Die unten gelisteten Findings sind daher nicht neu entstanden, sondern nur wieder sichtbar geworden.

Geändert:

  • .eslintrc.json ({ "extends": "next/core-web-vitals" }) gelöscht, ersetzt durch eslint.config.mjs (Flat Config, importiert eslint-config-next/core-web-vitals, ignoriert .next/, out/, build/, next-env.d.ts). ESLint 10 unterstützt das alte .eslintrc-Format nicht mehr (ESLINT_USE_FLAT_CONFIG=false entfällt), die Migration ist für das Upgrade also Pflicht und ohnehin vorzuziehen.
  • package.json: Script lint von next linteslint .
  • eslint-config-next 16.1.1 → 16.3.0 (zieht mit next 16.3.0 gleich, weiterhin exakt gepinnt). Peer-Range ist eslint >=9.0.0, deckt 10 also nominell ab.
  • eslint bleibt ^9 (konkretisiert auf ^9.39.5).

Blocker für ESLint 10: eslint-config-next deklariert zwar eslint >=9.0.0, seine Plugin-Abhängigkeiten sind aber noch nicht ESLint-10-fähig. Mit eslint@10.8.0 bricht der Lauf sofort hart ab:

TypeError: Error while loading rule 'react/display-name':
contextOrFilename.getFilename is not a function
  at .../eslint-plugin-react/lib/util/version.js

Ursache: ESLint 10 hat die deprecateten context-Member (u.a. context.getFilename()) entfernt. Stand der fünf von eslint-config-next 16.3.0 gezogenen Plugins:

Plugin Version peer eslint ESLint 10
eslint-plugin-react 7.37.5 (= latest) … || ^9.7 nein — crasht
eslint-plugin-jsx-a11y 6.10.2 (= latest) … || ^9 nein
eslint-plugin-import 2.32.0 (= latest) … || ^9 nein
eslint-plugin-react-hooks 7.1.1 … || ^10.0.0 ja
typescript-eslint 8.66.0 … || ^10.0.0 ja

Es existiert keine stabile Version von eslint-plugin-react / -jsx-a11y / -import mit ESLint-10-Support. Die einzigen Workarounds wären, die betroffenen Plugins aus der Config zu werfen (verliert echte Regel-Abdeckung) oder Versionen zu forcieren, die laut Peer-Range nicht passen — beides wurde bewusst nicht gemacht. Neuer Anlauf, sobald eslint-plugin-react ESLint 10 unterstützt.

Lint-Ergebnis nach Flat-Config-Migration (ESLint 9): 30 Findings (25 Fehler, 5 Warnungen) in 25 Dateien — alles Bestandscode, der durch das reparierte Lint-Script wieder sichtbar wird. Nicht im Rahmen dieses Schritts gefixt (Scope):

Regel Anzahl Art
react-hooks/set-state-in-effect (error) 19 setXLoading(true) direkt im Effect-Body, verteilt über fast alle src/hooks/* und Admin-Tabs
@next/next/no-html-link-for-pages (error) 4 <a href> statt <Link> (admin/login, forgot-password ×2, signup)
@next/next/no-location-assign-relative-destination (warn) 2 reset-password:70, verify:46
react-hooks/refs (error) 1 useSearch.ts:38 — Ref-Schreibzugriff während Render
react-hooks/purity (error) 1 components/ui/sidebar.tsx:665 (shadcn/ui-Datei, nicht manuell zu editieren)
react-hooks/exhaustive-deps (warn) 1 pop3/page.tsx:124
@next/next/no-img-element (warn) 1 settings/TotpSection.tsx:89
unused eslint-disable (warn) 1 mail/[id]/page.tsx:401

Der Großteil (set-state-in-effect, refs, purity) stammt aus den neuen React-Compiler-Regeln von eslint-plugin-react-hooks v7. Empfehlung: Folge-Ticket für die Hook-Findings (potenziell echte Cascading-Render-Performance-Themen, überschneidet sich thematisch mit PROJ-78), die 4 no-html-link-for-pages-Fehler sind separat klein und schnell.

  • Verifikation: npx tsc --noEmit0 Fehler; npm run builderfolgreich, alle 14 Routen generiert.

Lint-Findings gefixt — 2026-08-05

Alle 30 Findings (25 Fehler, 5 Warnungen) aus der obigen Tabelle sind behoben. npm run lint läuft jetzt mit 0 Fehlern / 0 Warnungen durch. Es wurden keine Regeln global deaktiviert und die Flat-Config nicht abgeschwächt.

react-hooks/set-state-in-effect (19×) — drei Fix-Muster, je nach Fall:

  1. Ableitung statt Effect-State (echte Struktur-Fixes):
    • src/hooks/useSystemInfo.ts: setSystemInfoLoading(true) entfernt — Effect läuft nur beim Mount (leere Deps), Initialwert ist bereits true.
    • src/app/verify/page.tsx, src/app/signup/page.tsx: der Fehlerzustand „kein Token“ / „kein Einladungslink“ folgt direkt aus der URL und wird jetzt als useState-Initialwert abgeleitet statt im Effect gesetzt.
    • src/app/search/page.tsx: Zurücksetzen der Auswahl bei neuen Ergebnissen als „State beim Rendern anpassen“ (prevResults-Vergleich) — das von React empfohlene Muster, keine veraltete Auswahl mehr sichtbar.
    • src/hooks/use-mobile.tsx: auf useSyncExternalStore umgestellt (Viewport = externer Store). Spart den Extra-Render, SSR-Snapshot false entspricht dem bisherigen undefined → !!undefined === false.
  2. Fetch-Logik in async-Funktion gekapselt + Cancel-Guard (src/hooks/useSavedSearches.ts, src/hooks/useSearch.ts): Loading-/ Ergebnis-States werden in einer inneren async-Funktion gesetzt; zusätzlich verhindert ein cancelled-Flag im Cleanup, dass eine veraltete Antwort noch State schreibt (echte Verbesserung, vorher nicht vorhanden).
  3. Ladeaufruf in async-Wrapper für die 11 Stellen, die lediglich eine bestehende load()/checkAuth()-Funktion im Effect anstoßen (useAuth, useAdminDashboard, useImapAccounts, pop3/page.tsx, TenantLDAPDialog, ArchivingRulesTab, DSGVOTab, QuotaTab, ReconciliationCard, RetentionTab, RoutingRulesTab, SMTPOutTab): void (async () => { await load(); })(); mit erklärendem Kommentar. Ablauf, Timing und Ladeanzeige bleiben identisch — die Regel greift nur auf synchron im Effect-Body erreichbare State-Updates zu.

react-hooks/refs (1×, useSearch.ts) — der Filter-Spiegel-Ref wurde während des Renders beschrieben. Die Synchronisation läuft jetzt in einem useEffect ohne Dep-Array (nach jedem Commit). doSearch liest den Ref ausschließlich in Event-Handlern/Effects, also immer nach dem Commit; der synchrone Schnellpfad in setQuery (Enter während Debounce) bleibt erhalten.

@next/next/no-html-link-for-pages (4×)<a href="/"><Link> aus next/link in admin/login, forgot-password (2×), signup, jeweils mit ergänztem Import.

react-hooks/exhaustive-deps (1×, pop3/page.tsx)pollingRefs.current wird im Effect in eine lokale Variable kopiert und die Cleanup-Funktion nutzt diese. Die Map-Instanz wird nie neu zugewiesen, Verhalten unverändert.

Unused eslint-disable (1×, mail/[id]/page.tsx) — ersatzlos entfernt, exhaustive-deps meldet dort nichts mehr.

Bewusst belassene eslint-disable-Kommentare (4 Stück, alle begründet):

Datei Regel Begründung
src/components/ui/sidebar.tsx:665 react-hooks/purity shadcn/ui-Komponente — laut CLAUDE.md keine Custom-Änderungen. Die zufällige Skeleton-Breite ist gewollt und durch useMemo pro Mount stabil.
src/components/settings/TotpSection.tsx @next/next/no-img-element QR-Code kommt als data:image/png;base64-URL vom Backend; next/image bringt keinen Nutzen (kein Netzwerk-Request, keine Optimierung möglich).
src/app/verify/page.tsx @next/next/no-location-assign-relative-destination Voller Reload nach Auth-Aktion ist beabsichtigt (.claude/rules/frontend.md: „Use window.location.href for post-login redirect“), damit Auth-/Client-Cache sauber neu initialisiert wird.
src/app/reset-password/page.tsx dito dito (nach Passwort-Reset).

Verifikation: npm run lint0 Probleme; npx tsc --noEmit0 Fehler; npm run builderfolgreich, alle 14 Routen generiert.

Lint-Fix + 30 Findings deployed auf 132 und 131 am 2026-08-05. Beide Deploys via update.sh (Backend + Frontend Build erfolgreich), Backend ✓ läuft / Frontend ✓ läuft auf beiden Servern bestätigt. Health-Check (/api/health{"status":"ok"}) und Kernrouten /, /search, /admin/login → alle HTTP 200 auf beiden Servern gegen den echten laufenden Dienst geprüft.

@types/node (Punkt 3) — 2026-08-06

node -v per SSH auf beiden Servern geprüft: v20.19.2 auf 131 und 132, identisch. Also kein Sprung auf ^26 (das würde Typen vor die tatsächlich laufende Node-Runtime setzen) — stattdessen nur Patch-Update innerhalb ^20: @types/node ^20^20.19.43. npx tsc --noEmit 0 Fehler, npm run build erfolgreich, alle Routen generiert. Deployed auf 132 und 131 am 2026-08-06 (Commit fa8c5dd), Backend ✓ / Frontend ✓ auf beiden Servern, Health-Check ok.

tailwindcss v4 + tailwind-merge v3 (Punkt 4) — 2026-08-06

  • npx @tailwindcss/upgrade@latest --force (offizielles Migrationstool) ausgeführt. Umbau: tailwind.config.ts entfernt, Theme (Farben, Border-Radius, Keyframes/Animation) nach src/app/globals.css als @theme-Block überführt (v4-Standard: CSS-first-Config). @tailwind base/components/utilities@import 'tailwindcss' + @custom-variant dark (&:is(.dark *)). PostCSS-Config auf @tailwindcss/postcss umgestellt, autoprefixer entfernt (in v4 eingebaut). 31 Komponenten-/Seiten-Dateien automatisch migriert (outline-hidden statt outline-none, u.a.).
  • tailwind-merge 2.6.0 → 3.x im selben Schritt (Pflicht-Kopplung, siehe Problem-Abschnitt).
  • Codemod-Bug gefunden + gefixt: das Migrationstool hat variant="outline" (Prop-Wert, kein CSS-Klassenname) blind textuell zu outline-solid umbenannt — Verwechslung mit der v4-Umbenennung der outline-Utility-Klasse. Betraf 4 Call-Sites (pagination.tsx:52, ServicesTab.tsx:100, DSGVOTab.tsx:46/56/71/480, DashboardTab.tsx:448), dort als tsc-Fehler sichtbar geworden ("outline-solid" nicht Teil des variant-Union-Typs von badgeVariants/buttonVariants). Manuell auf "outline" zurückgesetzt — badge.tsx/button.tsx selbst (die tatsächliche Variant-Definition) waren vom Bug nicht betroffen.
  • *-opacity-*-Lint-Regel ergänzt (eslint.config.mjs): no-restricted-syntax gegen bg-/text-/border-/ring-/divide-/placeholder-opacity-\d+-Literale, da diese Utilities in v4 nicht mehr existieren und von tailwind-merge v3 lautlos verschluckt werden statt einen Build-Fehler zu werfen (siehe Problem-Abschnitt). Gegen echten Bestand getestet: 0 Treffer (Codebase nutzt bereits nur reines opacity-*, keine *-opacity-*-Utilities). Funktionstest mit synthetischer Testdatei bestätigt: Regel schlägt korrekt an.
  • Verifikation: npx tsc --noEmit 0 Fehler, npm run lint 0 Findings, npm run build erfolgreich (alle 14 Routen).
  • Visueller Regressionstest: kein Browser-Tool in dieser Session verfügbar, daher HTTP/HTML-Sanity-Check als Ersatz: CSS-Link im <head> vorhanden, CSS-Bundle enthält vollständige v4-Struktur (@layer properties, 56 --tw-*-Custom-Properties), Response-Größen /, /admin/login, /search vor/nach praktisch identisch, shadcn-Klassen- muster im HTML unauffällig, keine Runtime-Errors in journalctl. Kein echter Pixel-Vergleich durchgeführt.
  • Deployed auf 132 (2026-08-06) und 131 (2026-08-06), Commit 824b24f, auf beiden Servern identisch verifiziert: Backend ✓, Frontend ✓, Health-Check ok, CSS-Bundle korrekt ausgeliefert, keine Fehler in journalctl.

typescript (Punkt 5) — 2026-08-06

  • typescript 5 → 6.0.3 (Zwischenschritt): tsc --noEmit 0 Fehler, npm run lint 0 Findings, npm run build erfolgreich, alle 14 Routen.
  • typescript 6.0.3 → 7.0.2 getestet: tsc --noEmit selbst lief fehlerfrei, aber npm run lint bricht hart ab — typescript-eslint@8.66.0 (Peer-Range >=4.8.4 <6.1.0) wirft explizit typescript-eslint does not support TS 7.0 und verweist auf TS-7-Announcement sowie typescript-eslint#10940 (offener Tracking-Issue für TS-≥7.1-Support). Gleicher Ökosystem-Lag wie bei Punkt 2 (eslint 10) — kein Fehler in unserem Code.
  • Ergebnis: typescript bleibt auf ^6.0.3, nicht auf 7.x. Rollback von 7.0.2 durchgeführt, danach erneut tsc/lint/build grün verifiziert.
  • Neuer Anlauf auf 7.x, sobald typescript-eslint das offiziell unterstützt (Issue #10940 beobachten).

Acceptance Criteria

  • lucide-react auf 1.x, tsc --noEmit + npm run build grün.
  • eslint auf 10.x + eslint-config-next kompatibel, npm run lint grün. Blockiert: eslint-plugin-react/-jsx-a11y/-import haben noch keine ESLint-10-fähige Version — eslint bleibt auf 9.x. Flat-Config- Migration + Reparatur des lint-Scripts (next lint existiert in Next 16 nicht mehr) sind erledigt; npm run lint läuft wieder und ist seit 2026-08-05 grün — alle 30 Bestands-Findings gefixt (Details in den Implementation Notes, Abschnitt „Lint-Findings gefixt“).
  • @types/node auf zur Server-Node-Version passende Major-Version. Node auf 131 + 132 verifiziert: v20.19.2 auf beiden (identisch). @types/node daher nicht auf ^26 gesprungen — bleibt auf ^20-Linie, Patch auf ^20.19.43 aktualisiert. tsc --noEmit + npm run build grün.
  • tailwindcss v4 + tailwind-merge v3 gemeinsam umgesetzt, Lint-Regel gegen *-opacity-*-Klassen aktiv. Deployed auf 132+131, HTTP/HTML- Sanity-Check statt echtem Pixel-Vergleich (kein Browser-Tool verfügbar) — CSS-Bundle-Struktur, Response-Größen, Klassenmuster unauffällig auf beiden Servern.
  • typescript schrittweise auf 7.x, tsc --noEmit bei jedem Zwischenschritt grün. Ziel nicht erreicht: 5→6.0.3 geschafft, 6→7 blockiert (typescript-eslint unterstützt TS 7 noch nicht, Issue #10940). typescript bleibt auf ^6.0.3.
  • Jeder Schritt einzeln auf 132 verifiziert vor 131-Deploy.