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.
20 KiB
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)
lucide-react0.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).eslint9 → 10 +eslint-config-next(aktuell 16.1.1) nachziehen, Flat-Config-Breaking-Changes prüfen,npm run lintdanach.@types/node20 → 26 — vorhernode -vauf 131 und 132 prüfen, Typen dürfen nicht vor der tatsächlichen Node-Laufzeit-Version liegen.tailwindcssv3→v4 +tailwind-mergev2→v3 zusammen (gekoppeltes Paar, nicht einzeln):npx @tailwindcss/upgrade(offizielles Migrationstool)tailwind-merge@3im 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)
typescript5 → 6 → 7 — stufenweise, nicht direkt springen, nach jedem Schritttsc --noEmitkomplett 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 statuswar 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-react0.562.0 → 1.28.0 (npm install lucide-react@^1). Geändert wurden nurpackage.json/package-lock.json— kein 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 --noEmit→ 0 Fehler;npm run build→ erfolgreich, 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 FundefixAvailable: trueohne SemVer-Major-Flag waren) → 0 Vulnerabilities.nextwanderte dabei minor von 16.2.9 auf 16.3.0 (im^16.1.1-Range, kein Breaking Change).npx update-browserslist-db@latest→caniuse-liteaktualisiert (1.0.30001763 → 1.0.30001806), keine Target-Browser-Änderung.- Verifikation:
npx tsc --noEmit0 Fehler,npm run builderfolgreich, alle 14 Routen generiert. update.sherweitert: nachnpm ciläuft jetztnpm audit --audit-level=highals 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.txtund Hinweis,npm audit fixlokal 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 cimeldete 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 durcheslint.config.mjs(Flat Config, importierteslint-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=falseentfällt), die Migration ist für das Upgrade also Pflicht und ohnehin vorzuziehen.package.json: Scriptlintvonnext lint→eslint .eslint-config-next16.1.1 → 16.3.0 (zieht mitnext16.3.0 gleich, weiterhin exakt gepinnt). Peer-Range isteslint >=9.0.0, deckt 10 also nominell ab.eslintbleibt^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 --noEmit→ 0 Fehler;npm run build→ erfolgreich, 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:
- Ableitung statt Effect-State (echte Struktur-Fixes):
src/hooks/useSystemInfo.ts:setSystemInfoLoading(true)entfernt — Effect läuft nur beim Mount (leere Deps), Initialwert ist bereitstrue.src/app/verify/page.tsx,src/app/signup/page.tsx: der Fehlerzustand „kein Token“ / „kein Einladungslink“ folgt direkt aus der URL und wird jetzt alsuseState-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: aufuseSyncExternalStoreumgestellt (Viewport = externer Store). Spart den Extra-Render, SSR-Snapshotfalseentspricht dem bisherigenundefined → !!undefined === false.
- Fetch-Logik in async-Funktion gekapselt + Cancel-Guard
(
src/hooks/useSavedSearches.ts,src/hooks/useSearch.ts): Loading-/ Ergebnis-States werden in einer innerenasync-Funktion gesetzt; zusätzlich verhindert eincancelled-Flag im Cleanup, dass eine veraltete Antwort noch State schreibt (echte Verbesserung, vorher nicht vorhanden). - 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 lint → 0 Probleme; npx tsc --noEmit →
0 Fehler; npm run build → erfolgreich, 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.tsentfernt, Theme (Farben, Border-Radius, Keyframes/Animation) nachsrc/app/globals.cssals@theme-Block überführt (v4-Standard: CSS-first-Config).@tailwind base/components/utilities→@import 'tailwindcss'+@custom-variant dark (&:is(.dark *)). PostCSS-Config auf@tailwindcss/postcssumgestellt,autoprefixerentfernt (in v4 eingebaut). 31 Komponenten-/Seiten-Dateien automatisch migriert (outline-hiddenstattoutline-none, u.a.).tailwind-merge2.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 zuoutline-solidumbenannt — Verwechslung mit der v4-Umbenennung deroutline-Utility-Klasse. Betraf 4 Call-Sites (pagination.tsx:52,ServicesTab.tsx:100,DSGVOTab.tsx:46/56/71/480,DashboardTab.tsx:448), dort alstsc-Fehler sichtbar geworden ("outline-solid"nicht Teil desvariant-Union-Typs vonbadgeVariants/buttonVariants). Manuell auf"outline"zurückgesetzt —badge.tsx/button.tsxselbst (die tatsächliche Variant-Definition) waren vom Bug nicht betroffen. *-opacity-*-Lint-Regel ergänzt (eslint.config.mjs):no-restricted-syntaxgegenbg-/text-/border-/ring-/divide-/placeholder-opacity-\d+-Literale, da diese Utilities in v4 nicht mehr existieren und vontailwind-mergev3 lautlos verschluckt werden statt einen Build-Fehler zu werfen (siehe Problem-Abschnitt). Gegen echten Bestand getestet: 0 Treffer (Codebase nutzt bereits nur reinesopacity-*, keine*-opacity-*-Utilities). Funktionstest mit synthetischer Testdatei bestätigt: Regel schlägt korrekt an.- Verifikation:
npx tsc --noEmit0 Fehler,npm run lint0 Findings,npm run builderfolgreich (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,/searchvor/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
typescript5 → 6.0.3 (Zwischenschritt):tsc --noEmit0 Fehler,npm run lint0 Findings,npm run builderfolgreich, alle 14 Routen.typescript6.0.3 → 7.0.2 getestet:tsc --noEmitselbst lief fehlerfrei, abernpm run lintbricht hart ab —typescript-eslint@8.66.0(Peer-Range>=4.8.4 <6.1.0) wirft explizittypescript-eslint does not support TS 7.0und 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:
typescriptbleibt auf^6.0.3, nicht auf 7.x. Rollback von 7.0.2 durchgeführt, danach erneuttsc/lint/buildgrün verifiziert. - Neuer Anlauf auf 7.x, sobald
typescript-eslintdas offiziell unterstützt (Issue #10940 beobachten).
Acceptance Criteria
lucide-reactauf 1.x,tsc --noEmit+npm run buildgrün.eslintauf 10.x +eslint-config-nextkompatibel,npm run lintgrün. Blockiert:eslint-plugin-react/-jsx-a11y/-importhaben noch keine ESLint-10-fähige Version — eslint bleibt auf 9.x. Flat-Config- Migration + Reparatur deslint-Scripts (next lintexistiert in Next 16 nicht mehr) sind erledigt;npm run lintlä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/nodeauf zur Server-Node-Version passende Major-Version. Node auf 131 + 132 verifiziert: v20.19.2 auf beiden (identisch).@types/nodedaher nicht auf ^26 gesprungen — bleibt auf ^20-Linie, Patch auf^20.19.43aktualisiert.tsc --noEmit+npm run buildgrün.tailwindcssv4 +tailwind-mergev3 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.typescriptschrittweise auf 7.x,tsc --noEmitbei jedem Zwischenschritt grün. Ziel nicht erreicht: 5→6.0.3 geschafft, 6→7 blockiert (typescript-eslintunterstützt TS 7 noch nicht, Issue #10940).typescriptbleibt auf^6.0.3.- Jeder Schritt einzeln auf 132 verifiziert vor 131-Deploy.