· Webentwicklung · 4 minuten Lesezeit
Warum "module": "Node16" deine Vite-Extension kaputt macht – und wie du es in 2 Minuten fixst
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 fundamental inkompatibel ist. Ich erkläre warum – und zeige die richtige Konfiguration.
Inhalt
- Der Fehler, der keine Erklärung zu haben scheint
- Was wirklich passiert
- Die Lösung
- Warum das Backend eine andere tsconfig hat
- Der Vollständigkeits-Check: Alle drei Projekte
- Das Muster hinter dem Fehler
- Warum ich das in einem eigenen Post dokumentiere
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 zusammen eine Falle bilden:
{
"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.
// So muss es in Node.js ESM aussehen:
import { foo } from "./foo.js"; // .js, nicht .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 – er 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:
| Setting | Vorher | Nachher | Warum |
|---|---|---|---|
module | Node16 | ESNext | Kein Node.js-ESM-Enforcement |
moduleResolution | node16 | bundler | Vite-kompatible Auflösung |
noEmit | (fehlend) | true | tsc 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.
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",outDirgesetzt
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”.
Der Vollständigkeits-Check: Alle drei Projekte
Nach dem Fix habe ich alle drei Projekte im Ökosystem überprüft:
# Extension (Vite + @crxjs)
cd extension && npm run build
# ✅ Keine Fehler – moduleResolution: bundler ist korrekt
# Backend (Node.js + Express)
cd backend && npm run build
# ✅ Keine Fehler – module: CommonJS ist korrekt
# Frontend (Vite + React)
cd frontend && npm run build
# ✅ Keine Fehler – moduleResolution: bundler war bereits korrektDrei 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:
- Eine
tsconfig.jsonvon einem Node.js-Projekt kopiert und in ein Vite-Projekt einfügt - Einer Anleitung folgt, die für Node.js-ESM-Projekte geschrieben ist, aber für Bundler-Projekte verwendet wird
- TypeScript-Templates nutzt, die nicht Vite-spezifisch sind
Das macht diesen Fehler besonders tückisch: 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 Lösung ist immer eine Frage: Was ist das Laufzeitmodell dieses Projekts?
- Browser (Vite als Bundler):
bundler - Node.js (kein Bundler):
nodeodernode16 - 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 man die falsche Lösung ausprobiert oder den Fehler nicht mit Vite in Verbindung bringt.
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.
Das ist kein exotisches TypeScript-Wissen. Es ist Grundlage.
Zurück zur Übersicht: Von der Screenshot-Extension zur KI-Memory-Engine


