Ich veröffentlichte ein NPM-Paket!!! Publishing Mermaid Enhancements als npm-Paket
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)
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
- Bauen Sie das Paket:
npm run build:all # Compiles TypeScript and minifies
- Lokale Prüfung:
npm run dev # Opens demo at localhost:3000
- Versionsstoß:
npm version patch # or minor, or major
- 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:
- Globaler Staat (
window.__themeState) - Örtliche Lagerhaltung (
localStorage.theme) - DOM-Klasse (
document.documentElement.classList) - 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
requestAnimationFramefü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
- GitHub-Repository: meistlucidweb/meistlucid-mermaid
- npm-Paket: @Mostlylucid/Mermaid-Enhancements (es kommt bald)
- Demo: Live-Demo (es kommt bald)