Ich veröffentlichte ein NPM-Paket!!! Publishing Mermaid Enhancements als npm-Paket (Deutsch (German))

Ich veröffentlichte ein NPM-Paket!!! Publishing Mermaid Enhancements als npm-Paket

Friday, 07 November 2025

//

14 minute read

Einleitung

Ich. ein .net Kerl schließlich gepflückt den Mut, meine Zehen in die Welt der npm Pakete tauchen! Mermaid.js ist obskur genug und seltsam genug, dass ich tatsächlich etwas Nützliches liefern könnte! So genießen.

Nachdem ich einige wirklich nützliche Verbesserungen für Mermaid.js Diagramme erstellt hatte (interaktive Pan/Zoom, Fullscreen Lightbox, Export in PNG/SVG und automatische Themenumschaltung), entschied ich, dass es Zeit war, sie richtig zu verpacken und mit der Community zu teilen. @mostlylucid/mermaid-enhancements als produktionsfertiges npm-Paket.

HINWEIS: Arbeite noch an der Veröffentlichung. Bleib dran (mein erstes npm-Paket nimmt also ein bisschen)

npm-Version npm-Downloads Lizenz: Unlizenzierung TypScript Bundle-Größe

Warum es packen?

Ich habe diese Verbesserungen in meinem Blog für eine Weile jetzt verwendet, und sie haben sich wesentlich für die Arbeit mit komplexen Mermaid-Diagramme. Die Funktionen umfassen:

  • Interaktiver Pan & Zoom - Navigieren Sie große Diagramme reibungslos
  • Vollbild-Lightbox - Immersives Seherlebnis
  • Nach PNG/SVG exportieren - Hochwertige Diagramm-Downloads
  • Automatisches Umschalten des Themes - Nahtlose Licht-/Dunkelmodus-Unterstützung
  • Responsives Design - Funktioniert auf Handy und Desktop

Da ich den gleichen Code zwischen den Projekten kopierte, machte es Sinn, ein richtiges npm-Paket zu erstellen, das jeder verwenden konnte.

Projektstruktur

Ich habe eine professionelle Paketstruktur mit TypeScript-Unterstützung eingerichtet:

mostlylucid-mermaid/
├── src/
│   ├── index.ts              # Main entry point
│   ├── enhancements.ts       # Pan/zoom/export functionality
│   ├── theme-switcher.ts     # Theme switching logic
│   ├── types.ts              # TypeScript type definitions
│   └── styles.css            # Complete styling
├── examples/
│   └── demo.html             # Full-featured demo
├── dist/                     # Built output (generated)
├── package.json
├── tsconfig.json
├── README.md
├── QUICKSTART.md
├── PUBLISHING.md
└── LICENSE

Das Paket verwendet TypeScript für die Typsicherheit und bessere Entwicklererfahrung, kompiliert aber auf JavaScript für maximale Kompatibilität.

Die Architektur

So passen die Komponenten zusammen:

So sieht es aus!

So sehen Sie, es ist ziemlich kompakt und hat einige nützliche Funktionen jenseits nur statische Diagramme. Es ärgerte mich immer, wie MASSIVE sie in der Seite waren, so schien dies eine vernünftige Annäherung an die Reduzierung Größe unter Beibehaltung Nützlichkeit.

graph TD
    A[User Initializes] --> B[init Function]
    B --> C[initMermaid]
    B --> D[enhanceMermaidDiagrams]

    C --> E[Theme Detection]
    C --> F[Event Listeners]
    C --> G[Mermaid Rendering]

    E --> E1[Global State]
    E --> E2[LocalStorage]
    E --> E3[DOM Class]
    E --> E4[OS Preference]

    F --> F1[Custom Events]
    F --> F2[Media Query]

    G --> H[Apply Enhancements]
    D --> H

    H --> I[Wrap Diagrams]
    H --> J[Init Pan/Zoom]
    H --> K[Add Controls]

    I --> L[Interactive Diagram]
    J --> L
    K --> L

    L --> M[User Interactions]
    M --> M1[Zoom In/Out]
    M --> M2[Pan]
    M --> M3[Fullscreen]
    M --> M4[Export PNG/SVG]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style L stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style M stroke:#7c3aed,stroke-width:3px,color:#8b5cf6

Kernumsetzung

Typdefinitionen

Zuerst habe ich umfassende TypeScript-Typen definiert:

// src/types.ts
export interface PanZoomInstance {
    zoom(scale: number): void;
    zoomIn(): void;
    zoomOut(): void;
    reset(): void;
    fit(): void;
    center(): void;
    resize(): void;
    destroy(): void;
    isPanEnabled(): boolean;
    enablePan(enabled: boolean): void;
}

export type ExportFormat = 'png' | 'svg';
export type Theme = 'dark' | 'default';
export type ControlAction = 'fullscreen' | 'zoomIn' | 'zoomOut' |
                           'reset' | 'pan' | 'exportPng' | 'exportSvg';

export interface EnhancementConfig {
    icons?: IconConfig;
    controls?: {
        fullscreen?: boolean;
        zoom?: boolean;
        pan?: boolean;
        export?: boolean;
    };
}

Haupteingangspunkt

Der Haupteingangspunkt ist ganz einfach:

// src/index.ts
export {
    enhanceMermaidDiagrams,
    cleanupMermaidEnhancements
} from './enhancements.js';

export {
    initMermaid
} from './theme-switcher.js';

export async function init() {
    await initMermaid();
}

export default {
    init,
    initMermaid,
    enhanceMermaidDiagrams,
};

Pan/Zoom-Umsetzung

Die Erweiterungslogik umhüllt jedes Diagramm mit Steuerungen und initialisiert svg-pan-zoom:

// src/enhancements.ts
import svgPanZoom from 'svg-pan-zoom';
import { toPng, toSvg } from 'html-to-image';

const panZoomInstances = new Map();

function initPanZoom(svgElement: SVGElement, diagramId: string) {
    // Clean up existing instance if present
    if (panZoomInstances.has(diagramId)) {
        try {
            panZoomInstances.get(diagramId).destroy();
        } catch (e) {
            console.warn('Failed to destroy existing pan-zoom instance:', e);
        }
        panZoomInstances.delete(diagramId);
    }

    try {
        const panZoomInstance = svgPanZoom(svgElement, {
            zoomEnabled: true,
            controlIconsEnabled: false,
            fit: true,
            center: true,
            minZoom: 0.1,
            maxZoom: 10,
            zoomScaleSensitivity: 0.3,
            dblClickZoomEnabled: true,
            mouseWheelZoomEnabled: true,
            preventMouseEventsDefault: true,
            contain: false
        });

        panZoomInstances.set(diagramId, panZoomInstance);
        return panZoomInstance;
    } catch (error) {
        console.error('Failed to initialize pan-zoom:', error);
        return null;
    }
}

Die Steuerknöpfe werden dynamisch erstellt:

function createControlButtons(container: HTMLElement, diagramId: string) {
    if (container.querySelector('.mermaid-controls')) {
        return;
    }

    const controlsDiv = document.createElement('div');
    controlsDiv.className = 'mermaid-controls';

    const buttons = [
        { icon: 'bx-fullscreen', title: 'Fullscreen', action: 'fullscreen' },
        { icon: 'bx-zoom-in', title: 'Zoom In', action: 'zoomIn' },
        { icon: 'bx-zoom-out', title: 'Zoom Out', action: 'zoomOut' },
        { icon: 'bx-reset', title: 'Reset View', action: 'reset' },
        { icon: 'bx-move', title: 'Pan', action: 'pan' },
        { icon: 'bx-image', title: 'Export as PNG', action: 'exportPng' },
        { icon: 'bx-code-alt', title: 'Export as SVG', action: 'exportSvg' }
    ];

    buttons.forEach(btn => {
        const button = document.createElement('button');
        button.className = `mermaid-control-btn bx ${btn.icon}`;
        button.setAttribute('title', btn.title);
        button.setAttribute('aria-label', btn.title);
        button.setAttribute('data-action', btn.action);
        button.setAttribute('data-diagram-id', diagramId);
        controlsDiv.appendChild(button);
    });

    container.appendChild(controlsDiv);
}

Exportfunktionalität

Die Exportimplementierung klont das SVG, bewahrt die ViewBox und verwendet html-to-image:

async function exportDiagram(
    container: HTMLElement,
    format: ExportFormat,
    diagramId: string
) {
    try {
        const svgElement = container.querySelector('svg');
        if (!svgElement) {
            console.warn('No diagram found to export');
            return;
        }

        // Clone to avoid modifying the original
        const clonedSvg = svgElement.cloneNode(true) as SVGElement;

        // Get or calculate viewBox
        let viewBox = clonedSvg.getAttribute('viewBox');
        if (!viewBox) {
            const bbox = svgElement.getBBox();
            viewBox = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`;
            clonedSvg.setAttribute('viewBox', viewBox);
        }

        // Set explicit dimensions for proper export
        const [, , vbWidth, vbHeight] = viewBox.split(' ').map(Number);
        clonedSvg.setAttribute('width', vbWidth.toString());
        clonedSvg.setAttribute('height', vbHeight.toString());

        // Remove pan-zoom transforms
        clonedSvg.removeAttribute('style');
        clonedSvg.style.backgroundColor = 'transparent';
        clonedSvg.style.maxWidth = 'none';

        // Create temporary container
        const tempDiv = document.createElement('div');
        tempDiv.style.position = 'absolute';
        tempDiv.style.left = '-9999px';
        tempDiv.appendChild(clonedSvg);
        document.body.appendChild(tempDiv);

        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const filename = `mermaid-diagram-${timestamp}`;

        if (format === 'png') {
            const dataUrl = await toPng(clonedSvg, {
                backgroundColor: 'white',
                pixelRatio: 2 // Higher quality
            });
            downloadFile(dataUrl, `${filename}.png`);
        } else {
            const dataUrl = await toSvg(clonedSvg, {
                backgroundColor: 'transparent'
            });
            downloadFile(dataUrl, `${filename}.svg`);
        }

        document.body.removeChild(tempDiv);
        console.log(`Diagram exported as ${format.toUpperCase()}`);
    } catch (error) {
        console.error('Failed to export diagram:', error);
    }
}

Thema-Umschaltung

Der Themen-Switcher behandelt mehrere Erkennungsmethoden:

// src/theme-switcher.ts
export async function initMermaid() {
    // Normalize code fences
    normalizeMermaidCodeFences();

    const mermaidElements = document.querySelectorAll(elementSelector);
    if (mermaidElements.length === 0) return;

    await saveOriginalData();

    // Set up theme change handlers
    const handleDarkThemeSet = async () => {
        await resetProcessed();
        await loadMermaid('dark');
    };

    const handleLightThemeSet = async () => {
        await resetProcessed();
        await loadMermaid('default');
    };

    // Listen for custom theme events
    document.body.addEventListener('dark-theme-set', handleDarkThemeSet);
    document.body.addEventListener('light-theme-set', handleLightThemeSet);

    // OS theme change listener
    if (typeof window.matchMedia === 'function') {
        const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
        mediaQuery.addEventListener('change', async (e) => {
            await resetProcessed();
            await loadMermaid(e.matches ? 'dark' : 'default');
        });
    }

    // Detect current theme with fallbacks
    let isDarkMode = false;
    if (typeof window.__themeState !== 'undefined') {
        isDarkMode = window.__themeState === 'dark';
    } else if (localStorage.theme) {
        isDarkMode = localStorage.theme === 'dark';
    } else if (document.documentElement.classList.contains('dark')) {
        isDarkMode = true;
    } else if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
        isDarkMode = true;
    }

    await loadMermaid(isDarkMode ? 'dark' : 'default');
}

Konfiguration des Pakets

paket.json

Das Paket.json definiert mehrere Einstiegspunkte für verschiedene Anwendungsfälle:

{
  "name": "@mostlylucid/mermaid-enhancements",
  "version": "1.0.0",
  "description": "Enhance Mermaid.js diagrams with interactive pan/zoom, fullscreen lightbox, export to PNG/SVG, and automatic theme switching",
  "main": "dist/index.js",
  "module": "src/index.ts",
  "types": "src/types.ts",
  "exports": {
    ".": {
      "types": "./src/types.ts",
      "import": "./src/index.ts",
      "require": "./dist/index.js"
    },
    "./min": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.min.js",
      "require": "./dist/index.min.js"
    },
    "./styles.css": "./src/styles.css"
  },
  "unpkg": "dist/index.min.js",
  "jsdelivr": "dist/index.min.js",
  "scripts": {
    "build": "tsc",
    "minify": "node scripts/minify.js",
    "build:all": "npm run build && npm run minify",
    "prepublishOnly": "npm run build:all",
    "dev": "cd examples && npx http-server -p 3000 -o"
  },
  "peerDependencies": {
    "mermaid": "^10.0.0 || ^11.0.0"
  },
  "dependencies": {
    "html-to-image": "^1.11.11",
    "svg-pan-zoom": "^3.6.1"
  }
}

TypScript-Konfiguration

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "node"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "examples"]
}

Anwendungsbeispiele

Grundlegende Verwendung

Der einfachste Weg, um das Paket zu verwenden:

import mermaid from 'mermaid';
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';

await init();

Mit dem Thema Umschalten

Für Orte mit Licht/Dunkel-Modus:

import { init } from '@mostlylucid/mermaid-enhancements';

// Initialize
await init();

// When theme changes
function toggleTheme() {
    const isDark = document.body.classList.toggle('dark');
    document.documentElement.classList.toggle('dark', isDark);

    // Notify the enhancements
    const event = new Event(isDark ? 'dark-theme-set' : 'light-theme-set');
    document.body.dispatchEvent(event);
}

Integration reagieren

import { useEffect } from 'react';
import { init, cleanupMermaidEnhancements } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';

function MermaidDiagram({ chart }: { chart: string }) {
    useEffect(() => {
        init();
        return () => cleanupMermaidEnhancements();
    }, [chart]);

    return (
        <div className="mermaid">
            {chart}
        </div>
    );
}

VUE-Integration

<template>
    <div class="mermaid">{{ chart }}</div>
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue';
import { init, cleanupMermaidEnhancements } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';

const props = defineProps(['chart']);

onMounted(async () => {
    await init();
});

onUnmounted(() => {
    cleanupMermaidEnhancements();
});
</script>

Die Demoseite

Ich erstellte eine umfassende Demo-Seite mit allen Funktionen:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mermaid Enhancements Demo</title>

    <!-- Boxicons for control button icons -->
    <link href="https://unpkg.com/[email protected]/css/boxicons.min.css" rel="stylesheet">

    <!-- Mermaid Enhancements CSS -->
    <link rel="stylesheet" href="../src/styles.css">
</head>
<body>
    <!-- Your diagrams -->
    <div class="mermaid">
graph TD
    A[Start] --> B{Is it working?}
    B -->|Yes| C[Great!]
    B -->|No| D[Check setup]
    C --> E[Zoom & Pan]
    D --> F[Read docs]
    E --> G[Export to PNG/SVG]
    </div>

    <!-- Load Mermaid -->
    <script type="module">
        import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
        window.mermaid = mermaid;
    </script>

    <!-- Initialize enhancements -->
    <script type="module">
        import { init } from '../dist/index.js';
        await init();
    </script>
</body>
</html>

Veröffentlichungsverfahren

Hier ist der Veröffentlichungs-Workflow:

sequenceDiagram
    participant Dev as Developer
    participant Git as Git Repo
    participant NPM as npm Registry
    participant CDN as unpkg/jsdelivr
    participant User as End User

    Dev->>Dev: Write code
    Dev->>Dev: npm run build:all
    Dev->>Dev: Test locally

    Dev->>Git: git commit & push
    Dev->>Git: Create version tag

    Dev->>NPM: npm login
    Dev->>NPM: npm publish --access public

    NPM-->>CDN: Sync package

    User->>NPM: npm install
    User->>CDN: Import from CDN

    NPM-->>User: Deliver package
    CDN-->>User: Serve files

Schritt für Schritt

  1. Bauen Sie das Paket:
npm run build:all  # Compiles TypeScript and minifies
  1. Lokale Prüfung:
npm run dev  # Opens demo at localhost:3000
  1. Versionsstoß:
npm version patch  # or minor, or major
  1. Veröffentlichen:
npm login
npm publish --access public

Das Paket ist jetzt verfügbar über:

  • npm: npm install @mostlylucid/mermaid-enhancements
  • nicht pkg CDN: https://unpkg.com/@mostlylucid/mermaid-enhancements
  • jsdelivr CDN: https://cdn.jsdelivr.net/npm/@mostlylucid/mermaid-enhancements

Verminderungsstrategie

Ich habe ein Minification-Skript hinzugefügt, um die Bundle-Größe zu reduzieren:

// scripts/minify.js
const { minify } = require('terser');
const fs = require('fs');
const path = require('path');

async function minifyFile(inputPath, outputPath) {
    const code = fs.readFileSync(inputPath, 'utf8');

    const result = await minify(code, {
        compress: {
            dead_code: true,
            drop_console: false,
            drop_debugger: true,
            keep_classnames: true,
            keep_fnames: true,
        },
        mangle: {
            keep_classnames: true,
            keep_fnames: true,
        },
        format: {
            comments: false,
        },
    });

    fs.writeFileSync(outputPath, result.code);

    const originalSize = fs.statSync(inputPath).size;
    const minifiedSize = fs.statSync(outputPath).size;
    const reduction = ((1 - minifiedSize / originalSize) * 100).toFixed(1);

    console.log(`✓ ${path.basename(outputPath)}: ${originalSize} → ${minifiedSize} bytes (${reduction}% smaller)`);
}

// Minify main bundle
minifyFile(
    path.join(__dirname, '../dist/index.js'),
    path.join(__dirname, '../dist/index.min.js')
);

Ergebnisse:

  • Regelmäßig: 23.46 KB
  • Minifiziert: 9,78 KB (Verringerung um 58,3%!)

Dokumentation

Ich habe eine umfassende Dokumentation erstellt:

  • README.md - Vollständige API-Referenz, Beispiele, Fehlerbehebung
  • QUICKSTART.md - 5-Minuten-Anleitung
  • VERÖFFENTLICHUNG - npm Veröffentlichungsanweisungen
  • ÄNDERUNGSLOG.md - Versionshistorie

Styling

Das CSS ist voll responsiv und unterstützt den dunklen Modus:

/* Diagram wrapper */
.mermaid-wrapper {
    position: relative;
    border-radius: 0.5rem;
    overflow: hidden;
    width: 100%;
    margin: 1rem 0;
}

/* Control buttons */
.mermaid-controls {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    display: flex;
    gap: 0.25rem;
    z-index: 10;
    background: rgba(255, 255, 255, 0.9);
    border-radius: 0.5rem;
    padding: 0.25rem;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.dark .mermaid-controls {
    background: rgba(31, 41, 55, 0.95);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

/* Individual buttons */
.mermaid-control-btn {
    padding: 0.5rem;
    border-radius: 0.25rem;
    cursor: pointer;
    transition: all 0.2s;
    background: transparent;
    border: none;
    color: #4b5563;
    font-size: 1.25rem;
}

.mermaid-control-btn:hover {
    background: rgba(37, 99, 235, 0.1);
    color: #2563eb;
    transform: scale(1.1);
}

Gelernte Lektionen

1. TypeScript ist es wert

Auch für eine kleine Bibliothek, TypeScript gefangen mehrere Bugs während der Entwicklung und bietet ausgezeichnete IDE-Unterstützung für Benutzer.

2. Mehrere Eingangspunkte Materie

Unterstützung beider import und require, plus bietet eine verkleinerte Version, macht das Paket vielseitiger:

"exports": {
    ".": {
      "types": "./src/types.ts",
      "import": "./src/index.ts",
      "require": "./dist/index.js"
    },
    "./min": {
      "import": "./dist/index.min.js"
    }
}

3. Demo ist unerlässlich

Die Demo-Seite half mir, Bugs zu fangen und dient als lebendige Dokumentation. Benutzer können genau sehen, wie es funktioniert.

4. Reinigung ist wichtig

Bereinigungsfunktionen für die Speicherverwaltung immer bereitstellen:

export function cleanupMermaidEnhancements() {
    panZoomInstances.forEach((instance, id) => {
        try {
            instance.destroy();
        } catch (e) {
            console.warn(`Failed to destroy pan-zoom instance ${id}:`, e);
        }
    });
    panZoomInstances.clear();
}

5. Thema-Erkennung braucht Rückfall

Verschiedene Sites behandeln Themen unterschiedlich, so habe ich mehrere Detektionsmethoden implementiert:

  1. Globaler Staat (window.__themeState)
  2. Örtliche Lagerhaltung (localStorage.theme)
  3. DOM-Klasse (document.documentElement.classList)
  4. Vorliebe für Betriebssysteme (prefers-color-scheme)

Leistungserwägungen

Das Paket ist für die Leistung optimiert:

  • Delegation der Veranstaltung - Single Event Hörer für alle Steuerknöpfe
  • Faule Initialisierung - Pan/Zoom nur bei Bedarf initialisiert
  • Bereinigung der Instanzen - Richtige Speicherverwaltung
  • Minimale Re-Renders - Diagramme nur re-render auf Thema ändern
  • Effiziente DOM-Operationen - Verwendung requestAnimationFrame für reibungslose Animationen

Browser-Unterstützung

Getestet und bearbeitet an:

  • Chrom/Edge (zuletzt)
  • Firefox (letzte)
  • Safari (letzte)
  • Mobile Browser (iOS Safari, Chrome Mobile)

Künftige Verbesserungen

Ideen für zukünftige Versionen:

  • Konfigurierbare Steuerungsknopfsymbole (Unterstützung für Font Awesome, Material Icons)
  • Tastenkürzel (z.B. Strg+Plus zum Vergrößern)
  • Berühren Sie Gesten auf dem Handy (pinch to zoom)
  • Kopieren Diagramm-Quellcode-Taste
  • Diagramm-URL-Funktion teilen
  • Annotationen im Diagramm
  • Mehrere Exportformate (PDF, JPEG)

Schlußfolgerung

Das Verpacken der Mermaid-Erweiterungen als npm-Modul war ein tolles Lernerlebnis. Das Paket ist jetzt:

  • Typsicher mit TypeScript
  • Gut dokumentiert
  • Einfach zu bedienen
  • Produktionsfertig
  • Rahmenagnostisch
  • Vollständig getestet

Wenn Sie Mermaid.js in Ihren Projekten verwenden, versuchen Sie es! Die interaktiven Pan/Zoom- und Exportfunktionen machen das Arbeiten mit komplexen Diagrammen so viel besser.

Aufkommen

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.