Neu veröffentlicht: E-Commerce mit Power Pages, Stripe & Analytics

· David Göschel · Webentwicklung  · 6 minuten Lesezeit

Warum module Node16 die Vite-Extension kaputt macht und wie es zu beheben ist

Neun TypeScript-Fehler auf einmal, alle über fehlende .js-Extensions in Importen. Der Grund: Ein tsconfig.json-Setting, das für Node.js korrekt ist, aber mit Vite als Bundler inkompatibel ist. Ich erkläre warum und zeige die richtige Konfiguration.

Neun TypeScript-Fehler auf einmal, alle über fehlende .js-Extensions in Importen. Der Grund: Ein tsconfig.json-Setting, das für Node.js korrekt ist, aber mit Vite als Bundler inkompatibel ist. Ich erkläre warum und zeige die richtige Konfiguration.

Inhalt

Der Fehler, der keine Erklärung zu haben scheint

Du hast TypeScript-Code geschrieben, der syntaktisch korrekt ist. Die Imports sehen so aus:

import { API_ENDPOINT } from '@/config';
import { buildPayload } from '@/background/payload-builder';

Dann führst du npm run build aus und bekommst das:

error TS2835: Relative import paths need explicit file extensions in EcmaScript imports
when '--moduleResolution' is 'node16' or 'nodenext'.
Did you mean './payload-builder.js'?

Nicht einen Fehler. Neun davon. In verschiedenen Dateien. Alle dasselbe.

Und das Absurde: Der Code hat vorher funktioniert. Du hast nichts geändert.

Was wirklich passiert

Das Problem liegt in der tsconfig.json und dem Zusammenspiel von zwei Settings, die kombiniert zu Fehlern führen:

{
  "compilerOptions": {
    "module": "Node16",
    "moduleResolution": "node16"
  }
}

Was "module": "Node16" bedeutet: TypeScript behandelt deine Dateien als natives ECMAScript-Module für Node.js 16+. In diesem Modus gelten die Node.js-ESM-Regeln und die besagen: Relative Imports brauchen explizite Dateiendungen.

// correct for Node.js ESM:
import { foo } from './foo.js'; // .js, not .ts!

Das ist korrekt für Node.js-Projekte. Es ist fundamental falsch für Vite.

Warum Vite anders ist: Vite ist ein Bundler. Er verarbeitet TypeScript-Dateien, bevor sie in den Browser oder als Extension-Bundle kommen. Vite versteht .ts-Imports direkt und braucht keine .js-Extension, weil er selbst auflöst, was welche Datei ist.

Wenn TypeScript-Compiler (tsc) und Vite gleichzeitig im Spiel sind, entsteht ein Konflikt: tsc prüft nach Node.js-ESM-Regeln (.js erforderlich), Vite erwartet Bundler-Semantik (.ts oder gar keine Extension).

Die Lösung

Für Vite-basierte Projekte (Chrome Extensions, Frontend-Apps, alles mit @crxjs) ist die korrekte tsconfig.json:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2020", "DOM"],
    "strict": true,
    "noEmit": true,
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

Die vier entscheidenden Änderungen:

SettingVorherNachherWarum
moduleNode16ESNextKein Node.js-ESM-Enforcement
moduleResolutionnode16bundlerVite-kompatible Auflösung
noEmit(fehlend)truetsc nur für Type-Checking, Vite baut
allowImportingTsExtensions(fehlend)true.ts in Imports erlaubt

noEmit: true ist der wichtigste Schalter.

Mit noEmit: true kompiliert tsc keinen JavaScript-Output. Es prüft nur die Typen. Das ist für Vite-Projekte der richtige Ansatz: Vite übernimmt das Bauen, TypeScript übernimmt das Type-Checking. Beide tun, was sie am besten können. Eine weitere Konsequenz dieses Build-Modells: import.meta.env.VITE_*-Variablen werden von Vite zur Build-Zeit direkt in das Bundle eingebettet. Das hat praktische Auswirkungen auf Docker-Builds, die ich im Artikel über Vite Build-Time-Umgebungsvariablen in Docker detailliert beschreibe.

Warum das Backend eine andere tsconfig hat

Wenn du dir das Backend-Projekt ansiehst, findest du dort eine andere Konfiguration:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "moduleResolution": "node",
    "outDir": "./dist",
    "strict": true
  }
}

Hier ist "module": "CommonJS" und das ist richtig.

Das Backend ist ein Node.js-Server. Er verwendet require() und CommonJS-Module. Kein Bundler, kein Vite, kein Browser-Kontext. TypeScript soll hier tatsächlich JavaScript ausgeben (outDir: ./dist), das Node.js direkt ausführen kann.

Die Regel lautet also:

  • Vite-Projekt (Extension, Frontend): "module": "ESNext", "moduleResolution": "bundler", noEmit: true
  • Node.js-Projekt (Backend, CLI): "module": "CommonJS", "moduleResolution": "node", outDir gesetzt

Dieselbe tsconfig für beide zu verwenden ist ein klassischer Anfängerfehler und ein Fehler, den man nicht auf Anhieb versteht, weil die Fehlermeldung nicht sagt “wrong module system”, sondern “missing .js extensions”.

Vollständigkeitsprüfung aller drei Projekte

Nach dem Fix habe ich alle drei Projekte im System überprüft:

# Extension (Vite + @crxjs)
cd extension && npm run build
# build successful: moduleResolution bundler is correct

# Backend (Node.js + Express)
cd backend && npm run build
# build successful: module CommonJS is correct

# Frontend (Vite + React)
cd frontend && npm run build
# build successful: moduleResolution bundler was already correct

Drei verschiedene Projekte, drei verschiedene Build-Kontexte, drei verschiedene tsconfig-Konfigurationen. Alle richtig. Alle für ihre jeweilige Laufzeitumgebung optimiert.

Das Muster hinter dem Fehler

Dieser Fehler tritt immer auf, wenn jemand:

  1. Eine tsconfig.json von einem Node.js-Projekt kopiert und in ein Vite-Projekt einfügt
  2. Einer Anleitung folgt, die für Node.js-ESM-Projekte geschrieben ist, aber für Bundler-Projekte verwendet wird
  3. TypeScript-Templates nutzt, die nicht Vite-spezifisch sind

Das macht diesen Fehler besonders schwer zu erkennen: Er hat eine oberflächlich plausible Lösung (.js zu allen Importen hinzufügen), die das eigentliche Problem nicht löst, sondern verschlimmert.

Die richtige Frage ist immer: Was ist das Laufzeitmodell dieses Projekts?

  • Browser (Vite als Bundler): bundler
  • Node.js (kein Bundler): node oder node16
  • Beides (Monorepo): Separate tsconfig-Dateien für jedes Projekt

Warum ich das in einem eigenen Post dokumentiere

Weil dieser Fehler Zeit kostet. Nicht fünf Minuten, sondern manchmal Stunden, wenn die falsche Lösung ausprobiert wird oder der Fehler nicht mit Vite in Verbindung gebracht wird.

Und weil ich in Kundenprojekten regelmäßig auf veraltete oder falsch konfigurierte TypeScript-Setups stoße. Power Pages Portale mit React-Integrationen haben oft komplexe Projekt-Setups mit mehreren tsconfig-Dateien (main, test, strict). Wer den Unterschied zwischen node, node16 und bundler als moduleResolution versteht, kann diese Probleme in Minuten lösen statt in Stunden.

Alle Artikel der Serie

  1. Vision und Systemübersicht: Chrome Extension, RAG-Architektur, Projekthintergrund: Artikel lesen
  2. RAG-System Aufbau: Qdrant, Embeddings, Cosine-Ähnlichkeit in TypeScript: Artikel lesen
  3. AI Provider Abstraktion: Ollama vs. OpenAI, Interface-Design, kein Vendor-Lock-in: Artikel lesen
  4. Chrome Extension MV3: Drei isolierte Laufzeitkontexte, Message Passing, Strategy Pattern: Artikel lesen
  5. Docker Compose Strategie: Override-Pattern, von lokal zu Azure: Artikel lesen
  6. Ollama lokal vs. Docker: Die Entscheidung und ihre Konsequenzen: Artikel lesen
  7. Ollama Auto-Pull Entrypoint: Automatisiertes Modell-Setup beim Container-Start: Artikel lesen
  8. tsconfig und Vite: Node16 vs. bundler, warum Vite eigene Regeln hat: (dieser Artikel)
  9. Instagram Caption mit MutationObserver vollständig laden: Artikel lesen
  10. Chrome Extension Foundation mit Health-Dot und Retry-Queue: Artikel lesen
  11. Phase 2 Features: Shadow DOM Overlay, Tailwind v4, Duplicate Detection: Artikel lesen
  12. Race Condition bei der Plattformerkennung: Wie ein UI-Event die Instagram-Erkennung bricht: Artikel lesen
  13. PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen
  14. Instagram Karussell vollständig erfassen mit MutationObserver: Lazy-Loading, Observer-before-click, Timeout-Fallback: Artikel lesen
  15. Notiz und Tags beim Screenshot-Speichern: Artikel lesen
  16. Instagram Tastatur-Shortcuts blockieren Chrome Extension Eingaben: Artikel lesen
  17. Lowercase-Normalisierung und Duplikat-Erkennung im Tag-Input: Artikel lesen
  18. Zitadel Login V2 in Docker Compose: drei versteckte Fehler: Artikel lesen
  19. PKCE OAuth in einer Chrome MV3 Extension: Artikel lesen
  20. React Frontend mit react-oidc-context und Zitadel: Artikel lesen
  21. Vite Build-Time-Umgebungsvariablen in Docker: Artikel lesen
  22. Event-Driven Ingestion mit BullMQ und Redis: Artikel lesen
  23. MinIO statt Azurite: S3-kompatible Objektspeicherung lokal und auf Hetzner: Artikel lesen
  24. access_token, id_token und der Userinfo-Endpoint: was wohin gehört: Artikel lesen
  25. Qdrant Multi-Tenancy: Pro Nutzer eine eigene Collection: Artikel lesen
  26. Wenn Backend und Frontend unterschiedliche Typen kennen: Artikel lesen
  27. Zitadel Bootstrap entfernt: Host-Header-Bug und manuelles Setup: Artikel lesen
  28. Backend Code Review: sechs Probleme vor dem Launch behoben: Artikel lesen
  29. Traefik statt NGINX: Reverse Proxy für einen wachsenden Docker-Compose-Stack: Artikel lesen
  30. Zweischichtiges Rate Limiting: Traefik und express-rate-limit mit Redis: Artikel lesen
  31. DSGVO Art. 17 korrekt implementieren: Promise.allSettled und Export-Batching: Artikel lesen
  32. Embedding-Modell-Lock-in: Warum mxbai-embed-large eine Produktionsentscheidung für immer ist: Artikel lesen
  33. Docker Volumes in Produktion: Named Volumes, Bind Mounts und der Hetzner-Volume-Trick: Artikel lesen
  34. Zwei Sicherheitslücken vor dem Launch: Redis ohne Auth und ein offener Qdrant-Admin-Port: Artikel lesen
  35. Traefik als einziger Einstiegspunkt im Docker Compose Stack: Artikel lesen
  36. Zitadel hinter Traefik richtig verdrahten mit Issuer, JWKS und Login V2: Artikel lesen
  37. Frontend reparieren wenn der nginx Healthcheck an localhost scheitert: Artikel lesen
  38. Observability für meinen Docker Compose Stack mit Bull Board und Dozzle: Artikel lesen
  39. Qdrant Dashboard sicher öffnen mit lokalem Traefik und SSH Tunnel: Artikel lesen
  40. Diagnose: Warum mein Chunking trotz Tokenisierung noch scheiterte: Artikel lesen
  41. Entscheidung: Warum ich den Chunk auf 1500 Tokens gesetzt habe: Artikel lesen
  42. Implementierung: Wie ich den Embedding Workflow in mehrere saubere Schritte zerlegt habe: Artikel lesen
  43. Validierung: Wie ich Chunking, Speicherung und Suche wieder zusammenbringe: Artikel lesen

Du arbeitest an einem Projekt mit mehreren TypeScript-Konfigurationen und willst sicherstellen, dass jede für ihren Build-Kontext optimiert ist? Lass uns das gemeinsam einschätzen.

Zurück zum Blog

Ähnliche Beiträge

Alle Beiträge ansehen