Julkaisin NPM-paketin!! (Suomi (Finnish))

Julkaisin NPM-paketin!!

Friday, 07 November 2025

//

13 minute read

Johdanto

Minä..net-tyyppi nappasi vihdoin rohkeuden upottaa varpaani npm-pakettien maailmaan! Merenneito.js on sen verran hämärä ja outo, että voisin itse asiassa tuoda jotain hyödyllistä! Joten nauti.

Sen jälkeen, kun olen rakentanut todella hyödyllisiä parannuksia Mermaid.js-kaavioihin (interaktiivinen pan/zoom, kokonäytön valolaatikko, vienti PNG/SVG:hen ja automaattinen teemavaihto), päätin, että on aika pakata ne kunnolla ja jakaa ne yhteisön kanssa. Tämä viesti kävelee läpi siitä, miten loin. @mostlylucid/mermaid-enhancements tuotantovalmiina npm-pakettina.

HUOMAUTUS: Työstän edelleen julkaisua. Pysy kanavalla (ensimmäinen npm-pakettini niin ottaa vähän)

npm-versio npm-lataukset Lisenssi: Luvaton TypeScript Bundlen koko

Miksi pakata se?

Olen käyttänyt näitä parannuksia blogissani jo jonkin aikaa, ja niistä on tullut välttämättömiä monimutkaisten merenneitokaavioiden kanssa työskentelyssä. Ominaisuuksiin kuuluu:

  • Vuorovaikutteinen Pan & Zoom - Navigoi suuret diagrammit sujuvasti
  • Koko näytön Lightbox - Uppoamaton katselukokemus
  • Vie PNG/SVG:hen - Laadukkaat kaaviolataukset
  • Automaattinen teemavaihto - Saumaton valo/pimeä tilatuki
  • Vastuullinen suunnittelu - Toimii mobiililla ja työpöydällä

Koska kopioin samaa koodia projektien välillä, oli järkevää luoda kunnon npm-paketti, jota kuka tahansa voisi käyttää.

Projektin rakenne

Perustin ammattimaisen pakettirakenteen, jossa on TypeScript-tuki:

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

Paketti käyttää TypeScriptiä tyyppiturvaan ja parempaan kehittäjäkokemukseen, mutta koostaa JavaScriptiin maksimaalisen yhteensopivuuden.

Arkkitehtuuri

Näin osat sopivat yhteen:

Tältä se näyttää!

Joten näet, että se on aika kompakti ja siinä on jotain hyödyllistä toiminnallisuutta paitsi vain staattisia kaavioita. Se aina ärsytti minua, kuinka MASSIVE ne olivat sivulla, joten tämä tuntui järkevältä lähestymistavalta koon muuttamiseen säilyttäen samalla hyödyllisyyden.

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

Keskeinen toteutus

Tyypin määritelmät

Ensin määritin kattavat TypeScript-tyypit:

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

Pääsisääntulopaikka

Pääsisäänkäynti on täysin yksinkertainen:

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

Parannuslogiikka käärii jokaisen kaavion hallintalaitteilla ja alustaa svg-pan-zoomin:

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

Ohjauspainikkeet luodaan dynaamisesti:

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

Vie Toiminnallisuus

Viennin toteutus kloonaa SVG:n, säilyttää viewBoxin ja käyttää html-to-imagea:

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

Teema vaihtuu

Teemanvaihtaja käsittelee useita havaintomenetelmiä:

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

Pakkauksen asetukset

package.json

Paketti.json määrittelee useita hakupisteitä eri käyttötapauksiin:

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

TypeScriptin asetukset

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

Käytä esimerkkejä

Peruskäyttö

Yksinkertaisin tapa käyttää pakettia:

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

await init();

Teema vaihtuu

Kohteet, joissa on valo/pimeä tila:

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

Reaktio kotoutumiseen

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

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

Demosivu

Loin kattavan demosivun, jossa näkyvät kaikki ominaisuudet:

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

Julkaisuprosessi

Tässä on julkaisutyönkulku:

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

Askel kerrallaan

  1. Rakenna paketti:
npm run build:all  # Compiles TypeScript and minifies
  1. Testaa paikallisesti:
npm run dev  # Opens demo at localhost:3000
  1. Versiokuoppa:
npm version patch  # or minor, or major
  1. Julkaise:
npm login
npm publish --access public

Paketti on nyt saatavilla seuraavilla tavoilla:

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

Vähennysstrategia

Lisäsin nippukokoa pienentävän pienennysskriptin:

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

Tulokset:

  • Säännöllisesti: 23.46 KB
  • Vähennetty: 9,78 KB (58,3 prosentin vähennys!)

Dokumentaatio

Laadin kattavat asiakirjat:

  • README.Md - Täydellinen API-viittaus, esimerkkejä, vianetsintä
  • Quickstart.md - 5 minuutin aloitusopas
  • PUBLISHING.Md - npm julkaisuohjeet
  • MUUTOSLOG.md - Versiohistoria

Styling

CSS on täysin reagoiva ja tukee tummaa tilaa:

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

Opittuja asioita

1. TypeScript kannattaa

Pienellekin kirjastolle TypeScript kiinnitti useita vikoja kehityksen aikana ja tarjoaa erinomaista IDE-tukea käyttäjille.

2. Useilla rajanylityspaikoilla on merkitystä

Kummankin tukeminen import sekä require, sekä minimaalinen versio, tekee paketista monipuolisemman:

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

3. Demo on olennainen

Demosivu auttoi minua nappaamaan vikoja ja toimii elävänä dokumentointina. Käyttäjät näkevät tarkasti, miten se toimii.

4. Siivous on tärkeää

Toimita aina muistinhallintaan puhdistustoimintoja:

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. Teema Detection Needs Returns

Eri sivustot käsittelevät teemoja eri tavalla, joten käytin useita havaintomenetelmiä:

  1. Maailmanlaajuinen tila (window.__themeState)
  2. Paikallinen varastointi (localStorage.theme)
  3. DOM-luokka (document.documentElement.classList)
  4. Kokonaiselossaolosuositus (prefers-color-scheme)

Suorituskykyä koskevia huomioita

Paketti on optimoitu suorituskykyyn:

  • Tapahtumavaltuuskunta - Yhden tapahtuman kuuntelija kaikille ohjauspainikkeille
  • Laiska alustaminen - Pan/zoom vain tarvittaessa
  • Oikeuslaitoksen siivous - Oikea muistinhallinta
  • Pienet renderaattorit - Kaaviot uudelleen vain teemamuutoksesta
  • Tehokkaat DOM-operaatiot - Käytökset requestAnimationFrame Sileisiin animaatioihin

Selaintuki

Testattu ja työn alla:

  • Chrome/Edge (viimeisin)
  • Firefox (viimeisin)
  • Safari (viimeisin)
  • Mobiiliselaimet (iOS Safari, Chrome Mobile)

Tulevia parannuksia

Tulevien versioiden ideat:

  • Konfiguroitavat ohjauspainikkeiden kuvakkeet (tuki Font Awesome -kuvakkeille)
  • Näppäimistön pikanäppäimet (esim. Ctrl+Plus zoomaukseen)
  • Kosketa eleitä mobiilissa (vaalenne zoomata)
  • Kopioi kaavion lähdekoodin painike
  • Jaa kaavion URL-ominaisuus
  • Kaaviohuomautukset
  • Useita vientiformaatteja (PDF, JPEG)

Päätelmät

Merenneidon lisälaitteiden pakkaaminen npm-moduuliksi oli hieno oppimiskokemus. Paketti on nyt:

  • Tyyppiturvallinen TypeScriptillä
  • Hyvin dokumentoitu
  • Helppo käyttää
  • Tuotantovalmiit
  • Kehysagnostiikka
  • Täysin testattu

Jos käytät Mermaid.js:ia projekteissasi, kokeile! Vuorovaikutteiset pan/zoom- ja vientiominaisuudet tekevät monimutkaisten kaavioiden kanssa työskentelystä paljon parempaa.

Resurssit

Finding related posts...
logo

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