Zum Inhalt springen
aviral gupta

// 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

Ende des Moduls

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
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Modul B2: Welche Aussagen stimmen? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  2. 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!"));
    
  3. Ü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 "
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Befehl oder Feld zu, was Sie damit bekommen.

  5. Ü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"]
  6. 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";
    
  7. 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.js

Ausgabe

$ 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
  1. Hinweis 1

    Prüfen Sie zuerst den Typ: if (typeof text !== "string") throw new TypeError("slugify expects a string, got " + typeof text);

  2. Hinweis 2

    .replace(/[^a-z0-9]+/g, "-") macht aus jeder Folge anderer Zeichen einen Bindestrich; .replace(/^-+|-+$/g, "") entfernt Bindestriche an den Enden.

  3. 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.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.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
  1. Hinweis 1

    Prüfen Sie zuerst die Argumente: typeof text !== "string" bedeutet TypeError; !Number.isInteger(max) || max < 1 bedeutet RangeError.

  2. Hinweis 2

    Ist text.length <= max, geben Sie text unverändert zurück.

  3. 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.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.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
  1. Hinweis 1

    export * reicht jeden benannten Export eines Moduls weiter, ELLIPSIS eingeschlossen.

  2. Hinweis 2

    Nennen Sie, was Sie weiterreichen: export {slugify} from "./slug.js";

  3. 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.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Hä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 function

Warum, 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.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Ein Paket, eine Einstiegsdatei

textkit ist ein Ordner mit einer package.json. "name" und "version" braucht es zum Packen: Ohne sie bricht npm pack mit npm error Invalid package, must have name and version ab. "private": true schützt vor versehentlichem Veröffentlichen, und "type": "module" macht seine .js-Dateien zu ES-Modulen, egal was die package.json der App sagt. "exports": "./index.js" nennt die Einstiegsdatei; das Ziel muss mit ./ beginnen. index.js ist das öffentliche Gesicht: Sie exportiert slugify und truncate aus src unter ihrem Namen weiter. Mit "exports" ist jeder andere Pfad geschlossen: import "textkit/src/slug.js" bricht mit ERR_PACKAGE_PATH_NOT_EXPORTED ab. "main" ist das ältere Feld: Es nennt den Einstieg, schließt aber nichts, und sind beide gesetzt, gewinnt "exports".

Tests, die das Paket wie eine App nutzen

Mit "scripts": {"test": "node --test"} führt npm test jede Datei aus, die zu Mustern wie **/*.test.js und **/test/**/*.js passt; eine .spec.js-Datei gehört nicht dazu. Scheitert ein Test, endet node --test mit Exit-Code 1. Die Testdatei importiert aus "textkit", nicht aus "../src/slug.js": Ein Paket mit "exports" kann sich selbst unter seinem Namen importieren und bekommt genau, was seine Einstiegsdatei exportiert. So sehen die Tests, was eine App sieht, und eine Funktion, deren Export der Einstieg vergisst, lässt sie scheitern. Die Funktionen sind rein, Strings hinein, Strings heraus, mit einem TypeError für alles andere; daher laufen und testen sie überall, auch im Browser.

Aus einem Ordner oder aus einem Tarball installieren

In einem zweiten Projekt speichert npm install ../textkit den Eintrag "textkit": "file:../textkit" und macht node_modules/textkit zu einem Link auf diesen Ordner: Jede Änderung ist sofort da, praktisch, solange Sie an beiden arbeiten. npm pack, in textkit ausgeführt, schreibt textkit-1.0.0.tgz mit dem, was veröffentlicht würde: immer package.json, dazu, was "files" nennt, hier index.js und src; test/ bleibt draußen. npm install ../textkit/textkit-1.0.0.tgz speichert "file:../textkit/textkit-1.0.0.tgz" und kopiert diese Dateien: eine Momentaufnahme. Nach einer Korrektur packen und installieren Sie erneut. Beides geht offline, und npm ci installiert den Tarball aus package-lock.json neu.

Quellen

Zuletzt geprüft am 3. Oktober 2026