Aufwärmen · Aufgabe 1 von 7
// B2.5 · ca. 38 Min. · Einstieg
Build: ein Hilfspaket mit eigenen Tests
Nach diesem Build machen Sie aus Funktionen ein getestetes ES-Modul-Paket und nutzen es in einem anderen Projekt, aus einem Ordner oder Tarball.
Lektion 5 von 5 in B2 npm und Pakete
Danach können Sie
- package.json mit name, version, "type", "exports" und "test"-Skript anlegen, dazu eine Einstiegsdatei mit benannten Exporten
- Reine Hilfsfunktionen schreiben und das Paket über seinen Namen mit npm test testen
- Das Paket aus einem Ordner oder einem npm-pack-Tarball in ein zweites Projekt installieren und vorhersagen, was Sie jeweils bekommen
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es voraus, bevor Sie weiterlesen. textkit ist in der App installiert, und seine package.json hat "exports": "./index.js". Die Datei src/slug.js liegt in node_modules/textkit. Was tut node main.js?
// node_modules/textkit/package.json // {"name": "textkit", "type": "module", "exports": "./index.js", ...} // main.js import {slugify} from "textkit/src/slug.js"; console.log(slugify("Tea & Cake!"));Üben · Aufgabe 3 von 7
Setzen Sie das Flag ein, damit npm test die Testdateien von textkit mit dem eingebauten Test-Runner ausführt.
"scripts": { "test": "node ____" }"test": "node "Üben · Aufgabe 4 von 7
Ordnen Sie jedem Befehl oder Feld zu, was Sie damit bekommen.
Üben · Aufgabe 5 von 7
textkit enthält index.js, package.json, src/slug.js, src/truncate.js und test/textkit.test.js. Seine package.json hat "files": ["index.js", "src"]. Welche Dateien packt npm pack in den Tarball?
"files": ["index.js", "src"]Denksport · Aufgabe 6 von 7
Knobelaufgabe. test/textkit.test.js liegt im Ordner textkit und importiert {slugify, truncate} aus "textkit". textkit ist nirgends installiert, und es gibt keinen Ordner node_modules. Sie führen node --test in textkit aus. Was passiert?
// textkit/test/textkit.test.js import {slugify, truncate} from "textkit";Anwenden · Aufgabe 7 von 7
Mini-Aufgabe, offline. Bauen Sie textkit: package.json wie in der Lektion, eine index.js, die slugify und truncate weiterexportiert, Ihre Schritte 1 und 2 in src und test/textkit.test.js mit Import aus "textkit". Führen Sie npm test aus, bis es besteht, dann npm pack. Legen Sie daneben app mit "type": "module" an, installieren Sie ../textkit/textkit-1.0.0.tgz und geben Sie slugify("Tea & Cake!") und truncate("A long title", 7) aus. Löschen Sie node_modules, führen Sie npm ci aus und starten Sie es erneut.
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Selbst programmieren
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Von einem Ordner voller Funktionen zum installierten Paket
Dieses Programm spielt Ihr Terminal und braucht kein Netzwerk. Es kopiert die Paketdateien neben sich in einen Ordner textkit (index.js nach oben, slug.js und truncate.js nach src, den Test nach test), führt dort npm test und npm pack aus, installiert den Tarball in ein zweites Projekt, app, und startet app. Sie sehen jeden Befehl und sein Ergebnis.
main.js
// Plays the part of your terminal: builds the textkit package, tests and packs
// it, then installs the tarball into a second project and runs that. Offline.
import {execSync} from "node:child_process";
import {existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync} from "node:fs";
import {tmpdir} from "node:os";
import {dirname, join} from "node:path";
const root = mkdtempSync(join(tmpdir(), "build-"));
const put = (file, text) => {
mkdirSync(dirname(join(root, file)), {recursive: true});
writeFileSync(join(root, file), text);
};
const here = (name) => readFileSync(join(import.meta.dirname, name), "utf8");
const sh = (command, dir) => execSync(command, {cwd: join(root, dir), encoding: "utf8"});
// The package: package.json, one entry file, the code in src, the tests in test.
const pkg = {
name: "textkit",
version: "1.0.0",
private: true,
type: "module",
exports: "./index.js",
files: ["index.js", "src"],
scripts: {test: "node --test"}
};
put("textkit/package.json", JSON.stringify(pkg, null, 2));
put("textkit/index.js", here("index.js"));
put("textkit/src/slug.js", here("slug.js"));
put("textkit/src/truncate.js", here("truncate.js"));
put("textkit/test/textkit.test.js", here("textkit.test.js"));
console.log("$ npm test");
const report = sh("npm test --silent", "textkit");
console.log(report.split("\n").filter((line) => /^ℹ (tests|pass|fail) /.test(line)).join("\n"));
console.log("$ npm pack");
console.log(sh("npm pack --silent", "textkit").trim());
// The second project installs the tarball and imports the package by name.
put("app/package.json", JSON.stringify({name: "app", version: "1.0.0", private: true, type: "module"}, null, 2));
put("app/main.js", 'import {slugify, truncate} from "textkit";\nconsole.log(slugify("Crème Brûlée & Tea!"));\nconsole.log(truncate("A long title for a short line", 13));\n');
console.log("$ npm install ../textkit/textkit-1.0.0.tgz");
sh("npm install --silent --no-audit --no-fund ../textkit/textkit-1.0.0.tgz", "app");
console.log("dependencies:", JSON.parse(readFileSync(join(root, "app/package.json"), "utf8")).dependencies);
console.log("test/ installed:", existsSync(join(root, "app/node_modules/textkit/test")));
console.log("$ node main.js");
console.log(sh("node main.js", "app").trim());
index.js
export {slugify} from "./src/slug.js";
export {truncate} from "./src/truncate.js";
slug.js
// Turns a title into a URL slug: "Tea & Cake!" -> "tea-cake".
export function slugify(text) {
if (typeof text !== "string") throw new TypeError("slugify expects a string, got " + typeof text);
return text
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
truncate.js
// Shortens text to at most max characters, ending with an ellipsis: "A long…".
export const ELLIPSIS = "…";
export function truncate(text, max) {
if (typeof text !== "string") throw new TypeError("truncate expects a string, got " + typeof text);
if (!Number.isInteger(max) || max < 1) throw new RangeError("max must be a whole number from 1, got " + max);
if (text.length <= max) return text;
return text.slice(0, max - 1).trimEnd() + ELLIPSIS;
}
textkit.test.js
import {test} from "node:test";
import assert from "node:assert/strict";
import {slugify, truncate} from "textkit";
test("slugify makes a URL slug", () => {
assert.equal(slugify("Tea & Cake!"), "tea-cake", "slugify('Tea & Cake!') should be tea-cake");
});
test("truncate keeps short text and shortens long text", () => {
assert.equal(truncate("Tea", 5), "Tea", "short text should stay as it is");
assert.equal(truncate("A long title", 7), "A long…", "long text should end with …");
});
test("slugify refuses what is not a string", () => {
assert.throws(() => slugify(42), TypeError, "slugify(42) should throw a TypeError");
});
Ausführen mit
node main.jsAusgabe
$ npm test
ℹ tests 3
ℹ pass 3
ℹ fail 0
$ npm pack
textkit-1.0.0.tgz
$ npm install ../textkit/textkit-1.0.0.tgz
dependencies: { textkit: 'file:../textkit/textkit-1.0.0.tgz' }
test/ installed: false
$ node main.js
creme-brulee-tea
A long title…- Die Tests importieren aus "textkit" und bestehen ohne Installation: Das Paket importiert sich selbst unter seinem Namen, was "exports" erlaubt.
- npm pack gab nur den Dateinamen aus; gepackt hat es index.js, package.json und src, wie "files" es sagt.
- test/ ist nicht in der App: Der Tarball enthält nur, was "files" nennt, dazu package.json.
- Die App importiert den bloßen Namen "textkit", und "exports" leitet ihn zu index.js.
Übungen
Übung 1 von 3
Schritt 1: slugify
Schreiben Sie slugify(text), das einen Titel in einen URL-Slug verwandelt: Kleinbuchstaben, jede Folge anderer Zeichen als a–z und 0–9 durch einen Bindestrich ersetzt, kein Bindestrich am Anfang oder Ende. Akzente fallen weg: "Crème Brûlée" wird zu "creme-brulee". Ist text kein String, werfen Sie einen TypeError. Die Funktion nutzt keine Node.js-API, läuft also auch im Browser, und wird zu src/slug.js von textkit.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
Prüfen Sie zuerst den Typ: if (typeof text !== "string") throw new TypeError("slugify expects a string, got " + typeof text);
Hinweis 2
.replace(/[^a-z0-9]+/g, "-") macht aus jeder Folge anderer Zeichen einen Bindestrich; .replace(/^-+|-+$/g, "") entfernt Bindestriche an den Enden.
Hinweis 3
text.normalize("NFD") zerlegt é in e und ein Akzentzeichen; .replace(/[\u0300-\u036f]/g, "") entfernt dann die Zeichen. Tun Sie beides vor den anderen Ersetzungen.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// Turns a title into a URL slug: "Tea & Cake!" -> "tea-cake".
export function slugify(text) {
if (typeof text !== "string") throw new TypeError("slugify expects a string, got " + typeof text);
return text
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
console.log(slugify("Tea & Cake!"));
Auf dem eigenen Computer ausführen
Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// Turns a title into a URL slug: "Tea & Cake!" -> "tea-cake".
export function slugify(text) {
return text.toLowerCase();
}
console.log(slugify("Tea & Cake!"));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {slugify} from './main.js';
test('Tea & Cake! wird zu tea-cake', () => {
assert.equal(slugify('Tea & Cake!'), 'tea-cake', `slugify('Tea & Cake!') lieferte ${JSON.stringify(slugify('Tea & Cake!'))}`);
});
test('Folgen von Leer- und Satzzeichen werden ein Bindestrich, keiner an den Enden', () => {
const got = slugify(' Hello, World ');
assert.equal(got, 'hello-world', `slugify(' Hello, World ') lieferte ${JSON.stringify(got)}`);
});
test('Ziffern bleiben', () => {
assert.equal(slugify('Top 10 Tips'), 'top-10-tips', `slugify('Top 10 Tips') lieferte ${JSON.stringify(slugify('Top 10 Tips'))}`);
});
test('Akzente fallen weg: Crème Brûlée wird zu creme-brulee', () => {
const got = slugify('Crème Brûlée');
assert.equal(got, 'creme-brulee', `slugify('Crème Brûlée') lieferte ${JSON.stringify(got)}`);
});
test('ein Wert, der kein String ist, wirft einen TypeError', () => {
assert.throws(() => slugify(42), TypeError, 'slugify(42) sollte einen TypeError werfen');
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenÜbung 2 von 3
Schritt 2: truncate
Schreiben Sie truncate(text, max). Text mit höchstens max Zeichen kommt unverändert zurück. Längerer Text wird auf max - 1 Zeichen gekürzt, Leerzeichen am Ende werden entfernt und ELLIPSIS (…) angehängt, sodass das Ergebnis nie länger als max ist: truncate("A long title", 7) ist "A long…". Werfen Sie einen TypeError, wenn text kein String ist, und einen RangeError, wenn max keine ganze Zahl ab 1 ist. Daraus wird src/truncate.js von textkit; es läuft auch im Browser.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
Prüfen Sie zuerst die Argumente: typeof text !== "string" bedeutet TypeError; !Number.isInteger(max) || max < 1 bedeutet RangeError.
Hinweis 2
Ist text.length <= max, geben Sie text unverändert zurück.
Hinweis 3
Sonst lässt text.slice(0, max - 1) Platz für die Auslassungspunkte; .trimEnd() entfernt ein Leerzeichen davor.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// Shortens text to at most max characters, ending with an ellipsis: "A long…".
export const ELLIPSIS = "…";
export function truncate(text, max) {
if (typeof text !== "string") throw new TypeError("truncate expects a string, got " + typeof text);
if (!Number.isInteger(max) || max < 1) throw new RangeError("max must be a whole number from 1, got " + max);
if (text.length <= max) return text;
return text.slice(0, max - 1).trimEnd() + ELLIPSIS;
}
console.log(truncate("A long title", 7));
Auf dem eigenen Computer ausführen
Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// Shortens text to at most max characters, ending with an ellipsis: "A long…".
export const ELLIPSIS = "…";
export function truncate(text, max) {
return text;
}
console.log(truncate("A long title", 7));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {truncate} from './main.js';
test('Text, der passt, kommt unverändert zurück', () => {
assert.equal(truncate('Tea', 5), 'Tea', `truncate('Tea', 5) lieferte ${JSON.stringify(truncate('Tea', 5))}`);
assert.equal(truncate('Hello', 5), 'Hello', `truncate('Hello', 5) lieferte ${JSON.stringify(truncate('Hello', 5))}`);
});
test('langer Text endet mit …: truncate("A long title", 7) ist "A long…"', () => {
const got = truncate('A long title', 7);
assert.equal(got, 'A long…', `truncate('A long title', 7) lieferte ${JSON.stringify(got)}`);
});
test('kein Leerzeichen vor dem …: truncate("Hello world", 7) ist "Hello…"', () => {
const got = truncate('Hello world', 7);
assert.equal(got, 'Hello…', `truncate('Hello world', 7) lieferte ${JSON.stringify(got)}`);
});
test('das Ergebnis ist nie länger als max', () => {
for (let max = 1; max <= 12; max++) {
const got = truncate('A long title here', max);
assert.ok(got.length <= max, `truncate('A long title here', ${max}) lieferte ${JSON.stringify(got)}, ${got.length} Zeichen`);
}
});
test('ein max, das keine ganze Zahl ab 1 ist, wirft einen RangeError', () => {
assert.throws(() => truncate('Tea', 0), RangeError, 'truncate("Tea", 0) sollte einen RangeError werfen');
assert.throws(() => truncate('Tea', 2.5), RangeError, 'truncate("Tea", 2.5) sollte einen RangeError werfen');
});
test('text, der kein String ist, wirft einen TypeError', () => {
assert.throws(() => truncate(null, 5), TypeError, 'truncate(null, 5) sollte einen TypeError werfen');
});
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenÜbung 3 von 3
Schritt 3: das öffentliche Gesicht
main.js spielt index.js, die Einstiegsdatei von textkit; slug.js und truncate.js liegen hier daneben (im Paket in src). Der Starter exportiert mit export * alles weiter, also sickert ELLIPSIS, ein Detail von truncate.js, als Teil des Pakets nach außen. Exportieren Sie genau slugify und truncate unter ihrem Namen weiter und sonst nichts: kein ELLIPSIS, kein Default-Export. Was der Einstieg exportiert, darauf kann sich jede App verlassen.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
export * reicht jeden benannten Export eines Moduls weiter, ELLIPSIS eingeschlossen.
Hinweis 2
Nennen Sie, was Sie weiterreichen: export {slugify} from "./slug.js";
Hinweis 3
Eine Zeile pro Modul: export {truncate} from "./truncate.js"; und kein export default.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// The entry file of textkit: what it exports is the package.
export {slugify} from "./slug.js";
export {truncate} from "./truncate.js";
Auf dem eigenen Computer ausführen
Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.js
// The entry file of textkit: what it exports is the package.
export * from "./slug.js";
export * from "./truncate.js";
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import * as textkit from './main.js';
test('exportiert slugify und truncate und sonst nichts', () => {
const names = Object.keys(textkit).sort();
assert.deepEqual(names, ['slugify', 'truncate'], `der Einstieg exportiert ${JSON.stringify(names)}`);
});
test('die Funktionen arbeiten über die Einstiegsdatei', () => {
assert.equal(textkit.slugify('Tea & Cake!'), 'tea-cake', 'slugify über den Einstieg sollte tea-cake liefern');
assert.equal(textkit.truncate('A long title', 7), 'A long…', 'truncate über den Einstieg sollte A long… liefern');
});
test('hat keinen Default-Export', () => {
assert.equal('default' in textkit, false, 'der Einstieg sollte nur benannte Exporte haben');
});
slug.js
// Turns a title into a URL slug: "Tea & Cake!" -> "tea-cake".
export function slugify(text) {
if (typeof text !== "string") throw new TypeError("slugify expects a string, got " + typeof text);
return text
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
truncate.js
// Shortens text to at most max characters, ending with an ellipsis: "A long…".
export const ELLIPSIS = "…";
export function truncate(text, max) {
if (typeof text !== "string") throw new TypeError("truncate expects a string, got " + typeof text);
if (!Number.isInteger(max) || max < 1) throw new RangeError("max must be a whole number from 1, got " + max);
if (text.length <= max) return text;
return text.slice(0, max - 1).trimEnd() + ELLIPSIS;
}
package.json
{
"type": "module"
}
package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.
Programm ausführen:
node main.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenHäufige Fehler
Einen Default importieren, den das Paket nicht hat
// index.js stands in for the entry file of textkit: named exports only.
import slugify from "./index.js";
console.log(slugify("Tea & Cake!"));
Was Node.js ausgibt
SyntaxError: The requested module './index.js' does not provide an export named 'default'Warum, und die Lösung
textkit hat nur benannte Exporte, also fragt ein Import ohne geschweifte Klammern nach einem Default-Export, den es nicht gibt; beim installierten Paket nennt die Meldung 'textkit'. Nutzen Sie Klammern mit dem genauen Namen: import {slugify} from "textkit". Benannte Exporte sorgen auch dafür, dass jede App denselben Namen verwendet; das erleichtert Suchen und Umbenennen.
Einen Namen weiterexportieren, den das Modul nicht exportiert
// The entry file passes on a helper that slug.js keeps to itself.
export {slugify, clean} from "./slug.js";
Was Node.js ausgibt
SyntaxError: The requested module './slug.js' does not provide an export named 'clean'Warum, und die Lösung
Eine Einstiegsdatei kann nur weiterreichen, was ein Modul exportiert, und clean ist eine private Funktion von slug.js. Node.js prüft jeden Import und Re-Export, bevor eine Zeile läuft, also lässt sich das ganze Paket nicht laden. Lassen Sie clean privat und streichen Sie es aus dem Einstieg, oder exportieren Sie es zuerst aus slug.js, aber nur, wenn Apps sich wirklich darauf stützen sollen.
Eine Hilfsfunktion, die ihrer Eingabe vertraut
// An early slugify without a type check. A page number arrives as a number.
function slugify(text) {
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-");
}
console.log(slugify(42));
Was Node.js ausgibt
TypeError: text.toLowerCase is not a functionWarum, und die Lösung
Eine Zahl hat kein toLowerCase, also kommt der Fehler tief aus dem Paket und nennt eine Variable, die der Aufrufer nie gesehen hat. Prüfen Sie am Anfang jeder exportierten Funktion den Typ und werfen Sie einen TypeError, der sagt, was erwartet war: "slugify expects a string, got number". Ein Test mit assert.throws hält dieses Versprechen fest.
JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.