Jag publicerade ett NPM-paket!!! Publishing Mermaid förbättringar som en npm-paket (Svenska (Swedish))

Jag publicerade ett NPM-paket!!! Publishing Mermaid förbättringar som en npm-paket

Friday, 07 November 2025

//

14 minute read

Inledning

Jag, en nätkille, tog äntligen mod till sig för att doppa mina tår i npm-paketens värld! Mermaid.js är obskyr nog och udda nog att jag faktiskt kunde leverera något användbart! Så njut.

Efter att ha byggt några riktigt användbara förbättringar för Mermaid.js diagram (interaktiv pan/zoom, helskärmsljusbox, export till PNG/SVG, och automatisk temabyte), bestämde jag mig för att det var dags att paketera dem ordentligt och dela dem med samhället. Detta inlägg går igenom hur jag skapade @mostlylucid/mermaid-enhancements som ett produktionsklart npm-paket.

OBS: Jobbar fortfarande på utgivningen. Håll ögonen öppna (mitt första npm paket så tar lite)

npm version npm- nerladdningar Licens: Olicensierad Typskript Bundelstorlek

Varför paketera den?

Jag har använt dessa förbättringar över min blogg ett tag nu, och de har blivit nödvändiga för att arbeta med komplexa Mermaid diagram. Funktionerna inkluderar:

  • Interaktiv panorering och zoom - Navigera stora diagram smidigt
  • Fullskärmsljusbox - Fördjupande visningsupplevelse
  • Export till PNG/SVG - Högkvalitativa diagramnedladdningar
  • Automatisk temabyte - Sömlös ljus-/mörklägesstöd
  • Responsiv design - Fungerar på mobil och skrivbord

Eftersom jag kopierade samma kod mellan projekt, var det vettigt att skapa ett riktigt npm-paket som vem som helst kunde använda.

Projektstruktur

Jag satte upp en professionell paketstruktur med TypeScript-stöd:

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

Paketet använder TypeScript för typsäkerhet och bättre utvecklarupplevelse, men kompilerar ner till JavaScript för maximal kompatibilitet.

Arkitekturen

Så här passar komponenterna ihop:

Så här ser det ut!

Så du ser att det är ganska kompakt och har några användbara funktioner utöver bara statiska diagram. Det irriterade mig alltid hur MASSAIV de var på sidan så detta verkade en förnuftig strategi för att minska storlek samtidigt behålla användbarhet.

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

Kärnan i genomförandet

Typdefinitioner

Först definierade jag omfattande typskripttyper:

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

Huvudinmatningspunkt

Huvudingången är helt enkel:

// 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,
};

Genomförande av pan/zoom

Förbättringslogiken sveper in varje diagram med kontroller och initierar 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;
    }
}

Styrknapparna skapas dynamiskt:

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);
}

Exportfunktionalitet

Exportgenomförandet klonar SVG, bevarar visningsrutan och använder 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);
    }
}

Temabyte

Temaomkopplaren hanterar flera detektionsmetoder:

// 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');
}

Paketinställning

paket.json

Paketet.json definierar flera ingångar för olika användningsfall:

{
  "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"
  }
}

Inställning av typskript

{
  "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"]
}

Exempel på användning

Grundläggande användning

Det enklaste sättet att använda förpackningen:

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

await init();

Med temabyte

För platser med ljus/mörkt läge:

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);
}

Reagera integration

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>
    );
}

Integrering av meningar

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

Demosidan

Jag skapade en omfattande demosida som visar alla funktioner:

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

Utgivningsprocess

Här är publiceringsarbetsflödet:

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

Steg för steg

  1. Bygg paketet:
npm run build:all  # Compiles TypeScript and minifies
  1. Provning lokalt:
npm run dev  # Opens demo at localhost:3000
  1. Versionsgupp:
npm version patch  # or minor, or major
  1. Publicera:
npm login
npm publish --access public

Paketet finns nu tillgängligt via:

  • Nu är det dags. npm install @mostlylucid/mermaid-enhancements
  • CDN för icke-pkg: https://unpkg.com/@mostlylucid/mermaid-enhancements
  • Det här är Jsdelivr CDN: https://cdn.jsdelivr.net/npm/@mostlylucid/mermaid-enhancements

Strategi för minigrering

Jag lade till ett minifikationsskript för att minska buntstorleken:

// 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')
);

Resultat:

  • Regelbunden: 23.46 KB
  • Minifierad: 9,78 KB (58,3% minskning!)

Dokumentation

Jag skapade omfattande dokumentation:

  • README.MUND - Fullständig API-referens, exempel, felsökning
  • KVICKSTART. M.D. - 5 minuters start guide
  • FÖRKUNNANDE. md - npm publiceringsanvisningar
  • FÖRÄNDRINGSLOGG. md - Versionshistorik

Styling

CSS är fullt responsiv och stöder mörkt läge:

/* 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);
}

Viktiga lärdomar

1. TypeScript är värt det

Även för ett litet bibliotek fångade TypeScript flera fel under utvecklingen och ger utmärkt IDE-stöd för användare.

2. Flera ingångspunkter materia

Stöd till båda import och require, plus att erbjuda en minified version, gör paketet mer mångsidig:

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

3. Demo är viktigt

Demosidan hjälpte mig att fånga buggar och fungerar som levande dokumentation. Användare kan se exakt hur det fungerar.

4. Rengöring är viktigt

Ge alltid städfunktioner för minneshantering:

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. Detektering av temabehoven

Olika platser hanterar teman på olika sätt, så jag implementerade flera detektionsmetoder:

  1. Globalt tillstånd (window.__themeState)
  2. Lokal lagring (localStorage.theme)
  3. DOM-klass (document.documentElement.classList)
  4. OS- preferens (prefers-color-scheme)

Prestandaöverväganden

Paketet är optimerat för prestanda:

  • Delegering av evenemang - En enda händelse lyssnare för alla kontrollknappar
  • Lazy initialisering - Pan/zoom initieras endast vid behov
  • Rensning av instanser - Rätt minneshantering
  • Minimala utbyten - Diagram bara re-rendera om temaförändring
  • Effektiva DOM-operationer - Använder requestAnimationFrame för smidiga animeringar

Webbläsarstöd

Testad och arbeta på:

  • Chrome/Edge (senast)
  • Firefox (senaste)
  • Safari (senaste)
  • Mobilwebbläsare (iOS Safari, Chrome Mobile)

Framtida förbättringar

Idéer för framtida versioner:

  • Konfigurerbara styrknappsikoner (stöd för teckensnitt Awesome, materialikoner)
  • Tangentbordsgenvägar (t.ex. Ctrl+Plus för zoom)
  • Rör gester på mobilen (rosa till zoom)
  • Kopiera diagram källkodsknapp
  • Dela diagram URL-funktion
  • Diagramanteckningar
  • Flera exportformat (PDF, JPEG)

Slutsatser

Packa Mermaid förbättringar som en npm modul var en stor lärande upplevelse. Paketet är nu:

  • Typsäkert med typskript
  • Väldokumenterad
  • Lätt att använda
  • Produktionsfärdig
  • Ramar-agnostiker
  • Fullt testad

Om du använder Mermaid.js i dina projekt, ge det ett försök! Interaktiva pan / Zoom och export funktioner gör arbete med komplexa diagram så mycket bättre.

Resurser

Finding related posts...
logo

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