Wie viele Player in der Javascript-Welt ist auch Deno auf die Zusammenarbeit mit NPM angewiesen. Es bietet jetzt aber mit JSR ein eigenes, gegenüber NPM verbessertes Repo an.
Die Typescript-Laufzeitumgebung Deno lädt Third-Party-Module im ESM-Format über das Import-Statement während der Kompilierzeit von einer beliebigen URL im Netz. Beispiele aus dem Content Delivery Netzwerk Esm.sh [1], dem Deno-Modul Cache (»deno.land/x« [2]) oder ab Version 2.84 aus dem Nodes Package Repository NPM [3] finden Sie in der Tabelle “Deno lädt ES-Module mit Import”.
Mit der Javascript Registry JSR [4] bringt das Deno-Projekt nun ein eigenes Package Repository für “modernes” Javascript und Typescript an den Start. Zur Erinnerung: Ein Package speichert meist mehrere Module zusammen mit einem Manifest und weiteren Dateien wie der Lizenz oder einer Dokumentation. Es obliegt dann der Laufzeitumgebung selbst oder deren Paketmanager, die Module aus dem Package der Anwendung lokal zur Verfügung zu stellen, beispielsweise wie unter Nodes im Ordner »node_modules/« im Wurzelverzeichnis des Projekts.
|
Hoster |
Beispiel |
Repository |
|
Esm.sh |
»import mod from ‘https://esm.sh/cowsay’« |
NPM |
|
»deno.land/x« |
»import mod from ‘https://deno.land/x/denosay« |
Deno |
|
NPM |
»import mod from ‘npm:cowsay’« |
NPM |
|
JSR |
»import mod from ‘jsr:denosay’« |
JSR |
Laut Aussage der Deno-Macher verhält sich JSR rückwärtskompatibel zu NPM, bringt jedoch einige Verbesserungen mit. Dieser Artikel vergleicht beide Repositories und zeigt, wie Sie JSR-Packages anhand des bestehenden Packages it-depends-on [5] unter Deno und JSR verwenden und erstellen. Die Module des Packages validieren eine Softwarearchitektur wie das Java-Package Archunit [6] im Stil eines Unit-Tests.
Listing 1 veranschaulicht einen Anwendungsfall: Die ersten beiden Zeilen importieren Module zum Unit-Testen aus »deno.land/x«. Die Aufrufe in den Zeilen 3 und 4 verwenden den Specifier »jsr:« dazu, die Rulesets aus dem Package unter JSR zu laden. Besagte Rulesets dienen in den Zeilen 5 bis 9 und 10 bis 13 dazu, die gewünschten Vorgaben durchzusetzen.
Listing 1
Unit-Test für Softwarearchitektur
import { describe, it } from "https://deno.land/std@0.196.0/testing/bdd.ts";
import { assert } from "https://deno.land/std@0.196.0/assert/mod.ts";
import * as layer from "jsr:@pamoller/it-depends-on/rules/layer.ts";
import * as software from "jsr:@pamoller/it-depends-on/rules/software.ts";
describe("test layer compliance", async () => {
it("common depends only on itself, jsr and npm", async () => {
assert(await layer.directoryDependsOn("./common"));
assert(await software.directoryDependsOn("./common", "jsr:*", "npm:*"));
});
it("rules is an exclusive client of common", async () => {
assert(await layer.directoryDependsOn("./rules", "./common"));
assert(await software.directoryDoesNotDependOn("./rules"));
});
});
Kleine Hausnummer
Der Vergleich von JSR mit NPM zeigt ein großes Ungleichgewicht zwischen den beiden Repositories. NPM, im Jahr 2010 gegründet, hostet mittlerweile 3 Millionen Packages, das 24 Jahre später auf der Bildfläche erschienene JSR lediglich 5000 Pakete.
Dennoch begnügen sich die Macher von Deno nicht damit, eine optimierte Laufzeit zu erreichen, sondern wollen auch ein verbessertes Package Repository bieten. Dazu haben sie, basierend auf Erfahrungswerten mit Node und NPM, eine Reihe von Maßnahmen ergriffen.
Damit Software-Stacks nicht mehr zusammenbrechen können wie 2016 nach der Löschung des NPM-Packages left-pad [7], lassen sich Pakete unter JSR nicht mehr entfernen (“immutable”). Die zweite Maßnahme: Im Sinn von Denos Vorliebe für Webstandards fokussiert JSR auf Typescript und ESM-konforme Packages, toleriert aber genauso Javascript-Code – zumindest, solange nicht CommonJS zum Einsatz kommt.
Drittens verlangt JSR eine gewisse Codequalität. Im Gegensatz zu Node genügt es bei JSR nicht, ein Paket zu deployen und dessen Qualität mit Download-Zahlen zu belegen. Abbildung 1 zeigt die Homepage von it-depends-on unter JSR. Der grüne Haken neben dem qualifizierten Paketnamen (bestehend aus dem Code-Space @pamoller und dem Paketnamen it-depends-on) belegt, dass der Code getestet und seine Herkunft beglaubigt wurde.
Maßnahme Nummer 4 bezieht sich auf Dokumentation. JSR geht hier weiter als NPM und extrahiert API-Dokumentation aus den Quellcodes. Außerdem berücksichtigt es zusätzliche Dokumentation im JSDoc-Format, sofern sie nach Maßgabe von JSR erstellt wurde. Der vorliegende JSR-Score berechnet zudem die Anzahl der Laufzeitumgebungen ein, unter denen das Paket läuft, und belohnt die Vermeidung sogenannter Slow Types (vorrangig Exporte aus Modulen ohne Typangabe).
JSR nutzen
Da JSR-Pakete ES-Module beheimaten, kann man sie prinzipiell überall dort einsetzen, wo solche Module Verwendung finden. Dazu zählen Deno, Node, Bun, Cloudflare Workers und aktuelle Webbrowser, aber auch Bundler wie Vite, Esbuild, Webpack und Rollup. Damit JSR nun Denos Segen in die weite Welt hinaustragen kann, hat das Deno-Projekt Erweiterungen für die gängigen Paketmanager Npm, Yarn, Bunx und Pnpm geschaffen. Damit lassen sich JSR-Pakete zumindest laden. Hängt jedoch der Code von der Laufzeitumgebung von Deno ab, wie der Ausdruck »Deno.wirteFileSync()«, bricht Node mit einem Fehler ab, da es das Objekt »Deno« nicht kennt.
Damit Deno mit NPM-Packages nicht dasselbe passiert, haben sich die Deno-Macher um Kompatibilität bemüht und zum Beispiel Polyfills für Nodes Built-in-Modul [8] in das Ökosystem von Deno integriert. Die Tabelle “Paketmanager” zeigt die wechselseitige Verwendung von JSR- und NPM-Packages für die gängigen Paketmanager.
Hinter den Kulissen lädt Npm das JSR-Package nicht aus dem eigenen Repository, sondern bemüht einen API-Endpunkt, der NPM-kompatible Archivdateien ausliefert. JSR erzeugt sie in einem mehrstufigen Verfahren: Zunächst transpiliert es Typescript nach Javascript, fügt dann Typdeklarationen hinzu und erstellt letztlich eine »package.json«.
|
Paketmanager |
NPM-Package |
JSR-Package |
|
Deno |
»deno add npm:confetti« |
»deno add @pamoller/it-depends-on« |
|
Npm |
»npm add confetti« |
»npx jsr add @pamoller/it-depends-on« |
|
Yarn |
»yarn add confetti« |
»yarn dlx jsr add @pamoller/it-depends-on« |
|
Bun |
»bunx add confetti« |
»bunx jsr add @pamoller/it-depends-on« |
|
Pnpm |
»pnpm add confetti« |
»pnpm dlx jsr add @pamoller/it-depends-on« |
Übrigens wirkt der Befehl »deno add @pamoller/it-depends-on« ähnlich wie der analoge Aufruf »npm install cowsay« unter Node. Die Softwareabhängigkeit tragen Sie in das Manifest des Projekts ein. Anders als unter Node heißt das Manifest jedoch nicht »package.json«, sondern »deno.json«.
Eleganterweise lässt sich der Schlüssel bei der Verwendung ohne den Specifier »jsr:« als Alias für die Referenz des JSR-Pakets nutzen. Außerdem unterstützt JSR Semantic Versioning [9]. Grundsätzlich können Sie dem Paketnamen überall einen Bezeichner nach dem Muster »@1.0.0« oder »@^1.0.1« zur Auswahl einer Version anhängen.
Selbstgemacht
Die Einsatztauglichkeit eines JSR-Packages unter Deno, Node, Bun und im Webbrowser hängt vom Geschick seines Entwicklers ab. Listing 2 bildet die Verzeichnisstruktur des Pakets it-depends-on ab. Das Verzeichnis ».github/« speichert die Beschreibung von Github-Aktionen (erste vier Zeilen), die Ursprung und Softwarequalität gegenüber JSR beglaubigen sollen. Der Ordner »rules/« (Zeilen 11 bis 13) nimmt die bereitgestellten Module »layer.ts«, »software.ts« sowie das Verzeichnis »common/« sowie Dateien mit Hilfscode (Zeilen 5 bis 10) auf. Die Zeilen 14 bis 16 enthalten die Softwaretests des Pakets, Zeile 17 mit »LICENSE« dessen Open-Source-Lizenz (MIT). In der »README.md« in der darauffolgenden Zeile lagert der übliche Softwareprolog. In Zeile 19 sehen Sie das Manifest »deno.json«, die letzten beiden Zeilen speichern beteiligte NPM-Packages.
Listing 2
Ordnerstruktur des JSR-Packages
|- .github
|- workflows
|- ci.yml
|- publish.yml
|- common
|- error.ts
|- file.ts
|- glob.ts
|- string.ts
|- walk.ts
|- rules
|- layer.ts
|- software.ts
|- test
|- test.layer.ts
|- test.software.ts
|- LICENSE
|- README.md
|- deno.json
|- package-lock.json
|- package.json
Listing 3 zeigt den Quellcode des Deno-Manifests für das JSR-Package it-depends-on (Zeile 19 von Listing 2). In der zweiten Zeile findet sich der Paketname, in der dritten die Version und in der vierten die Beschreibung. Das Feld »export« (Zeilen 5 bis 8) muss mindestens einen Entry Point für das Package angeben. Normalerweise wäre es das Modul »mod.ts«, analog zu »index.js« unter NPM. Im vorliegenden Fall dienen die beiden Module »rules/layer.ts« und »rules/software.ts« der Einfachheit halber als mögliche Einstiegspunkte. Der Code in den Zeilen 9 bis 12 speichert die Softwareabhängigkeiten in einer Map.
Listing 3
Manifest des JSR-Packages
{
"name": "@pamoller/it-depends-on",
"version": "0.1.8",
"description": "helps unit testing your architecture",
"exports": {
"./layer": "./rules/layer.ts",
"./software": "./rules/software.ts"
},
"imports": {
"@std/fs": "jsr:@std/fs@^0.229.3",
"@std/regexp": "jsr:@std/regexp@^0.224.1"
},
"tasks": {
"test": "deno test --doc --allow-all --parallel --coverage --trace-leaks --clean test/test.*.ts"
}
}
Intern arbeitet das Paket mit Denos Standardbibliothek, um mit dem Paket aus Zeile 10 auf das Dateisystem zuzugreifen und in der darauffolgenden Zeile reguläre Ausdrücke anzuwenden. Der Code in den Zeilen 13 bis 15 speichert abschließend Labels für Skriptaufrufe. So startet der Aufruf »deno task test« das Deno-Kommando aus Zeile 16. Die gleichnamige Tabelle zeigt die entsprechenden Felder aus dem Nodes-Manifest »package.json«.
|
JSR |
NPM |
Bedeutung |
|
»name« |
»name« |
Name |
|
»version« |
»version« |
Versionsnummer |
|
»description« |
»description« |
Beschreibung |
|
»imports« |
»dependencies« |
Abhängigkeiten |
|
»exports« |
»main« |
Entry Point |
|
»tasks« |
»scripts« |
Skriptaufrufe |
Codequalität
Das Beglaubigen der Codequalität übernimmt die Github-Aktion »ci.yml« (Listing 2, Zeile 3). Hinter Github-Aktionen verbergen sich Skripte, die im Stil einer Pipeline auf den Servern von Github abgearbeitet werden. Sie erledigen dabei regelmäßige Dienstaufgaben und protokollieren sie. Bei »ci.yml« aus Listing 4 handelt es sich um einen Continuous-Integration-Mechanismus: Bei jeder Codeaktualisierung des Main Branches, sei es ein Push- oder ein Pull-Request, werden die Softwaretests des Pakets aufgerufen (Listing 3 ab Zeile 15).
Vor dem Testdurchlauf erzeugt der Code in den Zeilen 10 bis 21 von Listing 4 jeweils eine virtuelle Testumgebung für Ubuntu, Windows und MacOS. Die Pipeline-Schritte ab Zeile 22 werden für jede virtuelle Umgebung ausgeführt: Zunächst klont der Aufruf in Zeile 23f das Repository, bevor die Kommandos in den Zeilen 25 bis 28 die jeweils letzte Version von Deno 1.x installieren.
Um das notwendige Nachinstallieren beteiligter NPM-Packages kümmert sich der Code in Zeile 29f, bevor schließlich die letzten beiden Zeilen die Softwaretests des Pakets ab Zeile 15 von Listing 3 durch Aufrufen des Deno-Task »test« starten. Die letzten Testdurchläufe von »it-depends-on« finden Sie unter Github [10]. Beim Hochladen des Pakets liest JSR die Protokolle aus Github und bewertet den letzten Durchlauf in seinem Score.
Listing 4
Continuous Integration mit Github
name: ci
permissions:
contents: write
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
deno:
- v1.x
os:
- ubuntu-latest
- windows-latest
- macOS-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: ${{ matrix.deno }}
- name: Install
run: npm install
- name: Run tests
run: deno task test
Herkunftsgarantie
Die Github-Aktion »publish.yml« aus der vierten Zeile von Listing 3 veröffentlicht und beglaubigt unter JSR den Ursprung des Software-Releases it-depends-on. Den Quellcode der Pipeline sehen Sie in Listing 5. Die Pipeline reagiert, anders als »ci.yml«, in den Zeilen 2 bis 6 auf das Erstellen eines neuen Tags der Form »v1.5.1«, wie es gewöhnlich der Code-Maintainer für ein neues Release auf dem Main Branch setzt.
Die If-Bedingung aus Zeile 9 erwies sich als notwendig, um den Pipeline-Start letztlich auf neue Tags einzuschränken. Der Aufruf in Zeile 10 wählt Ubuntu als virtuelle Umgebung des Deployments. Das Permission-Feld »contents: read« (Zeile 12) erlaubt ausschließlich das Lesen von Daten, während »id-token: write« in der darauffolgenden Zeile das Erstellen eines Tokens zur Authentifizierung gegenüber JSR im OIDC-Protokoll (Open ID Connect) gestattet.
Die Arbeitsschritte in den Zeilen 14 bis 19 checken das Repository in der virtuellen Umgebung aus und installieren die NPM-Packages aus »package.json«, bevor am Ende Zeile 19 das Package nach JSR schiebt und veröffentlicht.
Listing 5
Beglaubigung mit Github
name: Publish
on:
push:
branches: [ "master" ]
tags:
- v*
jobs:
publish:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Install
run: npm install
- name: Publish package
run: npx jsr publish --allow-dirty
Workaround
Wie bereits beschrieben, lassen sich JSR-Pakete zwar unter vielen Laufzeitumgebungen und Bundlern installieren, sind aber gegebenenfalls nicht lauffähig. Abbildung 2 veranschaulicht Nodes Probleme mit it-depends-on. Zunächst gelang es, das Package mit dem Kommando »npx jsr add @pamoller/it-depends-on« zu installieren.
Daraufhin landet es in Form eines NPM-Packages unter »node_modules/@pamoller/it-depends-on/«. Allerdings scheitert Node erwartungsgemäß beim Verwenden mit der Fehlermeldung »Cannot find name ‘Deno’«. Da unglücklicherweise alle Lese- und Schreiboperationen über die Laufzeitumgebung von Deno stattfinden müssen, braucht es einen Workaround.
Das Package könnte Nodes Built-in-Modul verwenden, doch das würde der Deno-Programmierung ein jähes Ende bereiten. Um das zu vermeiden, haben die Deno-Entwickler mit Dnt (Deno To Node Transform) [11] ein Tool geschaffen, das Deno-Code nach Node übersetzt. Intern ersetzt Dnt zunächst die Modul-Specifier »jsr:«, »npm:« und »node:«, bevor es den Polyfill node_deno_shims [12] hinzufügt, damit Aufrufe von Methoden in der Laufzeitumgebung emuliert werden können.
Nach dem Überprüfen von Datentypen, Erstellen von ESM-, CommonJS- und Typescript-Deklarationsdateien sowie Ableiten einer »package.json« steht zu guter Letzt das Testen des NPM-Pakets an. Zum Schluss lässt sich das Ergebnis nach NPM deployen. Aber auch für Dnt gilt: Beim Verwenden eines Deno-Standard-Moduls, das von Deno selbst abhängt, scheitert Dnt.
Fazit
Wie viele Player in der Javascript-Welt ist auch Deno auf die Zusammenarbeit mit NPM angewiesen. Es bietet aber jetzt mit JSR ein eigenes, gegenüber NPM verbessertes Repository an. Die JSR-Packages lassen sich zumindest unter allen gängigen Laufzeitumgebungen installieren und dank ESM-Formats zumeist auch verwenden.
Das Erstellen eines universell lauffähigen Deno-Packages gelingt aber nicht immer, da Denos Laufzeitumgebung dummerweise fest mit der Standardbibliothek verdrahtet ist. Das Kapseln der Laufzeitumgebung hinter einem Interface könnte Abhilfe schaffen. In der Browser-Welt gelang es bereits, viele Webstandards zu spezifizieren und zu implementieren. Das kann ein Superset für Deno und JSR aber allein nicht leisten. (csi)
Infos
- Esm.sh: https://esm.sh
- Deno.land/x: https://deno.land/x
- NPM: https://npmjs.com
- https://jsr.io
- it-depends-on: https://jsr.io/@pamoller/it-depends-on
- Archunit: https://www.archunit.org/
- Left-Pad-Desaster: https://www.theregister.com/2016/03/23/npm_left_pad_chaos/
- Polyfills für Nodes Built-in-Modul: https://docs.deno.com/api/node/
- Semantic Versioning: https://semver.org/
- Testdurchläufe von it-depends-on: https://github.com/pamoller/it-depends-on/actions
- Dnt: https://jsr.io/@deno/dnt
- Polyfill node_deno_shims: https://github.com/denoland/node_shims








