Startseite / Artikel / Wie Deno 2.x leise das Problem der Node-Kompatibilität sowie die Belastung durch Tools gelöst hat

Wie Deno 2.x leise das Problem der Node-Kompatibilität sowie die Belastung durch Tools gelöst hat

Dieser Artikel geht die Veröffentlichungen von Deno 2.0 bis 2.9 durch und zeigt, wie die Kompatibilität mit npm, Berechtigungssätze sowie integrierte Werkzeuge die Hindernisse beseitigten, die Entwickler einst dazu brachten, Deno aufzugeben.

2160 Wörter

Diese Testversion wurde vor fünf Jahren erstellt.

Letzten Monat benötigte ein schnelles Nebenprojekt einen kleinen HTTP-Server. Ein neuer Verzeichnisinhalt, deno init, und zehn Minuten später gab es bereits einen funktionsfähigen Server mit TypeScript, Tests, Formatierung und Linting – alles bereits integriert. Kein tsconfig-Datei zu schreiben. Keine Prettier-Einstellungen. Keine eslint-Konfiguration. Kein jest.config.ts. Auch keine package.json-Datei in Sicht.

Die Versionsprüfung zeigte 2.9.3 an.

Es stellte sich heraus, dass Deno zwischen Oktober 2024 und Juli 2026 zehn kleine Updates veröffentlicht hatte, wobei jedes davon stillschweigend eines der genauen Probleme behob, die ursprünglich zur Aufgabe des Tools geführt hatten. Damals wurde nichts davon bemerkt, weil das Tool bereits als „gute Idee, aber noch nicht für echte Arbeit geeignet“ abgelegt worden war und es keinen Grund gab, erneut nachzusehen.

Hier ist eine Übersicht darüber, was tatsächlich geändert wurde – jeweils ein altes Problem nach dem anderen.

Klage 1: npm-Pakete waren zweite Klasse

Dies war das Problem für viele Entwickler, einschließlich mir. Unter Deno 1.x funktionierte alles, was nicht auf deno.land/x veröffentlicht wurde oder keine korrekten ES-Module-URLs hatte, einfach nicht. Die Trennung zwischen der Deno-Welt und der npm-Welt schien dauerhaft zu bestehen.

Deno 2.0, das im Oktober 2024 veröffentlicht wurde, schloss diese Lücke vollständig. Jetzt können Sie direkt jedes Paket aus dem Katalog von npm mit mehr als zwei Millionen Paketen verwenden, indem Sie den npm:-Spezifikator nutzen:

import express from "npm:express@5";
import { PrismaClient } from "npm:@prisma/client";

Falls Sie lieber weiterhin package.json verwenden möchten, wird das ebenfalls unterstützt. Deno analysiert dieses File, holt die Abhängigkeiten aus dem npm-Registry und kann sogar ein lokales node_modules-Verzeichnis erstellen, wenn Sie diese Einstellung aktivieren. Private Registries funktionieren ebenfalls über .npmrc, genauso wie in Node.

Die Statistik, die den Beweis wirklich geliefert hat: Über 75 Prozent von Nodes eigenem Testsuite laufen jetzt erfolgreich unter Deno. Das ist kein oberflächlicher Workaround – es weist auf echte, überprüfte Kompatibilität hin.

Der entscheidende Moment war,当 Deno auf ein bereits vorhandenes Express-Projekt angewendet wurde. Es musste keine einzige Import-Anweisung geändert werden. Eine deno.json-Datei mit dem Wert "nodeModulesDir": "auto" wurde hinzugefügt, deno install wurde ausgeführt, und die Installation dauerte etwa 900 Millisekunden – im Vergleich zu über 3 Sekunden mit npm unter denselben Bedingungen (getestet mit einem sauberen Cache in einem Projekt, das React, Vite, Babel und ESLint verwendet). Der Server funktionierte bereits beim ersten Versuch einwandfrei.

Kritikpunkt 2: Das Berechtigungssystem war in der Praxis lästig

Das Berechtigungskonzept machte theoretisch Sinn. In der Praxis bedeutete es jedoch, jedes Mal wieder etwas Ähnliches eingeben zu müssen:

deno run --allow-read=./data --allow-write=./data --allow-net=api.example.com --allow-env main.ts

Wenn man eine Flagge vergisst, stirbt der Prozess ab. Fügt man eine neue Abhängigkeit hinzu, die zufällig eine Umgebungsvariable liest, stirbt der Prozess erneut. Es fühlte sich eher wie die Konfiguration von Firewall-Regeln an als wie das Erstellen eines Nebenprojekts.

Deno 2.5, das im September 2025 veröffentlicht wurde, führte Berechtigungssets ein, die direkt im Konfigurationsfile definiert werden können. Man deklariert benannte Sets unter dem Schlüssel "permissions" in deno.json und ruft sie anschließend mit der Flagge -P (Abkürzung für --permission-set) auf:

{
  "permissions": {
    "default": {
      "read": ["./src", "./data"],
      "write": ["./data"],
      "net": ["api.example.com", "0.0.0.0:3000"],
      "env": true
    },
    "test": {
      "read": true,
      "write": ["./tmp"],
      "net": false
    }
  }
}

Dort zieht das Ausführen von deno run -P main.ts automatisch die „Standard“-Einstellungen aus deno.json im Arbeitsverzeichnis heran, oder man kann deno test -P=test ausführen, um eine separate Einstellungsgruppe ausschließlich für Tests zu verwenden. Testcode und Produktionscode erhalten unterschiedliche Berechtigungsrahmen, ohne dass dabei eine einzige Befehlszeilenflagge angepasst werden muss. Die zugrunde liegenden Sicherheitsgarantien haben sich nicht geändert – die damit verbundene Mühe ist einfach verschwunden.

Ebenfalls vorhanden ist die Umgebungsvariable DENO_AUDIT_PERMISSIONS, die ein JSONL-Log erzeugt, das jede während des Programmablaufs durchgeführte Berechtigungsprüfung aufzeichnet. Dadurch lässt sich genau erkennen, auf was Ihre Abhängigkeiten zugreifen wollen, ohne in deren Quellcode schauen zu müssen.

Kritikpunkt 3: Ich brauchte trotzdem eine Menge zusätzlicher Tools dazu

Der Vorteil und zugleich die Last von Node ist, dass fast jede Aufgabe einem separaten Paket übertragen wird. Benötigt man Formatierung? Dann gibt es prettier. Für Linting: ESLint sowie einige Plugins. Zum Testen: Jest oder Vitest, plus eine Konfigurationsdatei zur Handhabung von TypeScript-Transformationsregeln. Für Typüberprüfungen: tsc, das unabhängig von dem Programm konfiguriert wird, das den Code ausführt. Zum Bundeln: Man kann aus einem Dutzend Bundlern wählen, von denen jeder seinen eigenen Konfigurationsstil hat.

Deno integriert all das als eingebaute Unterbefehle. Diese existierten bereits vor der Veröffentlichung 2.0:

deno fmt          # formats TS, JS, JSON, HTML, CSS, YAML, SQL
deno lint         # built-in linter with quick fixes
deno test         # test runner with coverage, snapshots, sharding
deno check        # type checking
deno compile      # single binary, cross-platform, code signing
deno bench        # benchmarking
deno doc          # documentation generation

Ab Version 2.8 kamen sechs weitere Unterbefehle hinzu:

deno audit        # security audit of dependencies
deno audit fix    # auto-upgrade vulnerable packages to nearest patched version
deno why          # explain why a package is installed (traces dependency paths)
deno transpile    # strip types, emit .d.ts declarations
deno pack         # build an npm-publishable tarball from JSR/Deno code
deno ci           # reproducible install for CI (errors without lockfile)

Und mit 2.9 kamen weitere dazu:

deno desktop      # build native desktop apps via webview (experimental)
deno list         # show dependency tree (like npm ls)
deno link/unlink  # local package linking for development

Der am häufigsten verwendete Befehl ist deno compile. Durch das Erstellen einer kleinen CLI-Tool und Ausführen von deno compile --target x86_64-unknown-linux-gnu main.ts entsteht ein eigenständiges Binärdatei. Auf dem Zielrechner muss nichts weiter installiert werden – man kopiert die Datei einfach auf einen Server und führt sie aus.

Achtung: Die resultierende Binärdatei enthält V8 sowie den gesamten Deno-Laufzeitumfang, wodurch typische Anwendungen etwa 60–100 MB groß sind. Wenn die Größe ein Problem darstellt, kann deno compile --bundle (bislang in Version 2.8 noch instabil) durch aggressives Tree-Shaking die Größe von einfachen Skripten erheblich verringern. Der eigene Blog von Deno zeigte, dass ein lodash „Hello World“-Beispiel mit --bundle --minify nur 1,5 MB groß ist.

Kritikpunkt 4: Die Migration eines echten Projekts erschien unerreichbar

Das größte Hindernis bei der Änderung von Laufzeiten hat in der Regel nichts mit der Laufzeit selbst zu tun. Es handelt sich um die Lockdatei, die Struktur von node_modules, auf der Ihre vorhandenen Tools beruhen, sowie die über das gesamte Codebasis verstreuten require()-Aufrufe.

Deno 2.9 führte die sogenannte Lockfile-Seedung ein. Angenommen, ein Projekt verfolgt bereits seine Abhängigkeiten über eine der gängigen Lockdateien von Paketverwaltern – beispielsweise eine package-lock.json von npm, eine pnpm-lock.yaml, eine yarn.lock oder eine bun.lock – hat aber noch nie eine deno.lock erstellt. Wenn in diesem Projekt deno install ausgeführt wird, wird die fehlende deno.lock direkt aus der gefundenen Datei erstellt. Die bereinigten Versionen stimmen überein, genauso wie die Integritätshashes. Es gibt somit keinen Anlass zur Sorge bezüglich Abweichungen bei der Auflösung der Abhängigkeiten.

Für Fälle, in denen tatsächlich ein physischer node_modules-Ordner benötigt wird – beispielsweise für native Erweiterungen oder Tools, die das Dateisystem direkt durchsuchen – weist die Einstellung "nodeModulesDir": "auto" Deno an, einen solchen Ordner zu erstellen. Es gibt außerdem die Option "nodeModulesLinker": "hoisted" in deno.json (seit Version 2.8 verfügbar), die für ältere Tools gedacht ist, die auf der flachen Verzeichnisstruktur von npm statt auf pnpm-basiertem Symbolisieren basieren.

Der Node Shim ist eine praktische Ergänzung: Sobald man Deno installiert, setzt es automatisch ein Ersatz-Binärdatei für node auf den PATH, ohne dass eine manuelle Konfiguration erforderlich ist. Solange keine andere Anwendung bereits ein node-Ausführbares bereitstellt, fängt dieser Shim die Aufrufe der Node CLI ab, übersetzt die Argumente und leitet sie an Deno weiter. Dadurch funktionieren CI-Skripte, die weiterhin node dist/server.js aufrufen, unverändert. Wenn man nicht möchte, dass dies automatisch geschieht, kann man dieses Verhalten mit DENO_DISABLE_NODE_SHIM=1 deaktivieren.

Bare Specifier-Imports – also der Schreibweise import fs from „fs“, wobei dies auf node:fs verweist – wurden in Version 2.0 eingeführt und sind ab Version 2.9 vollständig stabil; sie funktionieren ohne weitere Flags oder Konfigurationen. Es ist daher nicht notwendig, die Import-Anweisungen erneut zu schreiben, um auf diese Funktion umzusteigen.

Der Migrationsversuch

Betrachten wir ein bescheidenes Hono-API-Projekt (Hono ist ein leichtgewichtiges HTTP-Framework, das mit Express vergleichbar ist), das etwa 15 Routen, eine über Drizzle ORM zugängliche Postgres-Datenbank sowie einen Prozessor für Hintergrundaufgaben besitzt. Es wurde ursprünglich unter Node 22 mit pnpm ausgeführt. Das gesamte Experiment wurde mit Deno 2.9.3 durchgeführt.

So verlief die Umwandlung:

  1. Führen Sie von der Wurzel des Projekts aus deno install aus. Dies erzeugt innerhalb von weniger als zwei Sekunden eine deno.lock-Datei direkt aus der vorhandenen pnpm-lock.yaml-Datei.
  2. Fügen Sie innerhalb einer neu erstellten deno.json-Datei den Eintrag "nodeModulesDir": "auto" hinzu.
  3. Starten Sie die Anwendung mit deno run -A src/server.ts.

Es funktioniert einwandfrei. Jeder der 15 Routen reagiert korrekt, die auf Drizzle basierenden Abfragen laufen wie erwartet, und der Queue-Worker bearbeitet weiterhin Aufgaben im Hintergrund.

Drei Dinge sind jedoch kaputtgegangen:

  • Ein Testfile setzte auf jest.mock(), was im eingebauten Testlaufwerk von Deno keine Entsprechung hat. Der Austausch gegen einen manuellen Mock dauert weniger als fünf Minuten.
  • Eine Abhängigkeit verwendete innerhalb eines CommonJS-Files __dirname, doch Deno hatte es als ES-Modul geladen. Die Lösung bestand darin, "type": "commonjs" zur lokalen Übernahme dieses spezifischen Pakets hinzuzufügen.
  • Ein Worker-Skript las process.env.NODE_ENV ohne process explizit einzubinden. Dieser Teil funktionierte tatsächlich einwandfrei, da Deno seit Version 2.0 process als globale Variable bereitstellt – doch die Berechtigungsflaggen enthielten für dieses spezielle Skript nicht env: true, weshalb dies hinzugefügt werden musste.
  • Die gesamte Umwandlung dauerte von Anfang bis Ende etwa 25 Minuten. Die Startzeiten sanken aufgrund von Messungen mit hyperfine bei 50 Durchläufen von etwa 620 ms auf 320 ms. Der Leerlauf-Memoryverbrauch (RSS) verringerte sich von 142 MB auf 64 MB. Darüber hinaus konnten vier separate Konfigurationsdateien gelöscht werden: prettier, eslint, jest und tsconfig.

    Noch vorhandene Unzulänglichkeiten

    Der Titel dieses Artikels erwähnt „alles, was ich hasste“, und die zuvor aufgeführten Beschwerden waren authentische, häufige Probleme unter Node-Entwicklern. Dennoch gibt es einige neuere Einschränkungen, die erwähnt werden sollten:

    • Die Abdeckung der Node-API ist nicht vollständig. Eine Bestehensrate von 75 Prozent im Testumfang von Node bedeutet, dass ein Viertel der Tests weiterhin fehlschlägt. Dieser Mangel kann eine typische Arbeitslast möglicherweise nicht beeinflussen, doch alle, die auf ungewöhnliche Aspekte von node:vm, die programmierte Verwendung von node:inspector oder tiefere Funktionen von node:cluster angewiesen sind, sollten vorab das Kompatibilitätsdashboard unter node-test-viewer.deno.dev überprüfen.
  • Kompilierte native Module benötigen weiterhin ein lokales node_modules-Verzeichnis. Alle Pakete mit C++-Bindings – wie sharp, bcrypt, sqlite3 und ähnliche – erfordern sowohl die Einstellung nodeModulesDir als auch das Flag --allow-ffi. Es funktioniert zwar, stellt aber einen zusätzlichen Einrichtungsschritt dar, den man leicht versehentlich überspringen kann.
  • Skripte zur Installation werden blockiert, es sei denn, man gibt etwas anderes an. Pakete, die postinstall-Hooks ausführen – beispielsweise Kompilierungs Schritte mit node-gyp oder prisma generate – benötigen eine ausdrückliche Genehmigung über --allow-scripts=npm:Package-Name. Dies ist eine bewusste Sicherheitsentscheidung, kann aber ein Team mitten in der Migration überraschen.
  • Viele Tools gehen weiterhin davon aus, mit Node zu kommunizieren. Einige Hilfsprogramme überprüfen process.versions.node und passen ihr Verhalten entsprechend an, oder sie verwenden fest codierte Dateipfade wie /node_modules/.cache. Denos pnpm-ähnliche Symlink-Struktur für Module stört einige dieser Tools. Es gibt eine Option im Hoisted-Modus, um dies zu umgehen, doch es handelt sich dabei nur um einen Workaround, nicht um eine echte Lösung.
  • Niemandes dieser Probleme war ernst genug, um die oben beschriebene Migration zu blockieren. Bei anderen Projekten könnten sie jedoch problematisch sein, weshalb es sich lohnt, sie im Voraus zu überprüfen, anstatt sie mitten in der Migration zu entdecken.

    Warum diese Fixes übersehen wurden

    In 21 Monaten gab es zehn kleine Versionen, die jeweils stillschweigend drei bis fünf Problempunkte beseitigten. Es gab keine einzige umfassende Überarbeitung, kein „Deno 3.0“-Launch-Event. Das Team führte in Version 2.5 fein abgestimmte Berechtigungssets ein, brachte in Version 2.8 eine Installationsgeschwindigkeit auf NPM-Niveau und fügte in Version 2.9 die Erstellung von Lockfiles hinzu – alles wurde als routinemäßige Wartung betrachtet und nicht als Schlagzeilenmeldung.

    Vergleichen Sie das mit der Art und Weise, wie Framework-Übergänge normalerweise in den Fokus rücken: ein Blogbeitrag, eine Vortragsrede auf einer Konferenz, ein Migrationsleitfaden, eine Welle an Kommentaren in sozialen Medien, kontroverse Meinungen sowie Gegenargumente dazu. Deno umging diesen gesamten Zyklus öffentlicher Debatten und brachte die Korrekturen direkt heraus.

    Entwickler, die den Runtime frühzeitig abgelehnt haben, taten dies oft, weil sie sich einmal eine Meinung gebildet hatten und diese im Laufe der Weiterentwicklung des Tools nicht mehr überdachten. Das ist eine Nachlässigkeit, kein Mangel am Projekt selbst.

    Für alle, deren Eindruck von Deno vor der Version 2.0 entstanden ist, existiert das damals getestete Tool im Grunde nicht mehr. Was heute vorhanden ist, verhält sich weniger wie „ein interessanter TypeScript-Runtime, der nicht auf npm zugreifen kann“, und eher wie „Node ohne sein angesammeltes Ballastgut“. Die Frustrationen waren damals berechtigt. Sie wurden seitdem beseitigt. Es lohnt sich, noch einmal einen Blick darauf zu werfen.

    Verwandte Artikel

  • Die Brückenfunktion von TypeScript 6 auf dem Weg zu einem nativen TS 7-Compiler — Erfahren Sie, wie TypeScript 6 die Standardkonfigurationen, die Modulauflösung sowie die Import-Syntax aktualisiert, um Codebasen auf den schnelleren, auf Go basierenden TypeScript 7-Compiler vorzubereiten.
  • Die Veränderungen in JavaScript 2026: Runtimes, TypeScript 7 und Rust-Tooling — Eine Führung durch die Änderungen im JavaScript-Ecosystem im Jahr 2026 – Bun, Deno und Node.js im Wettbewerb, die auf Go basierende Überarbeitung von TypeScript sowie build-Tools, die von Rust angetrieben werden – mit Erklärungen dazu, was für Entwickler tatsächlich wichtig ist.
  • NestJS vs Node.js: Warum Struktur in großen Skalen vorteilhafter ist als Freiheit — Dieser Artikel erläutert, wie NestJS Architektur, Abhängigkeitsinjektion und Konventionen auf Node.js aufbaut, um Wartbarkeitsprobleme zu lösen, die reines Node.js oder Express nicht bewältigen können.