npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aurorah/epub-studio

v1.2.1

Published

EPUB editor with reader, code view, TOC management, and multi-platform export — built on wMCP (Web Module Connection Protocol)

Readme

@aurorah/epub-studio

EPUB editor with reader, Monaco code view, TOC management, image management, and multi-platform export -- built on wMCP (Web Module Connection Protocol).

Current package version: 1.1.4.

v1.1.4 Notes

  • Requires @aurorah/wmcp >= 1.1.0 for the new readiness contract.
  • mount() now opts in to wMCP's readiness gating: host->module events received before the studio's first paint are buffered by wMCP (FIFO) and replayed once client._setReady() is called (after two chained requestAnimationFrames, when the deferred Reader / Editor / CodeEditor / Monaco-worker / Fonts init has drained).
  • Hosts may subscribe to the protocol-level wmcp:ready event (host.on("wmcp:ready", ...)) to coordinate post-mount UI such as loading spinners.
  • Hosts that previously worked around this race with a manual await yieldForStudioInit() (or similar setTimeout / requestAnimationFrame waits before emitting epub:openFile) can drop those workarounds.
  • Fixes the host-emit-before-module-ready race tracked in cursor_button_switch_delay_issue.md -- TOC / 파일구조 used to populate while the viewer / editor / code panes stayed empty until a refresh, with about a 20% success rate on cache-warm CDN fetches.

v1.1.1 Notes

  • Monaco code editor is bundled with an inline editor worker and uses Prettier plus @prettier/plugin-xml for formatting.
  • The Next.js demo dynamically imports @aurorah/epub-studio inside the client effect while keeping the stylesheet import static.
  • Host integrations can enable translateButton; clicking it emits epub:translate.
  • The wMCP manifest includes optional host requirements for log:write and export:pdf.

Prerequisites

  • Node.js >= 18
  • pnpm

Project Structure

v1.1/
  src/
    code-editor.ts             # Monaco HTML editor, formatting, linting, search, shortcuts, fullscreen
    color-picker.ts            # Shared color picker for reader colors and selected editor text/background
    cursor-sync-indicator.ts   # Maps editor/code cursor position to content blocks for sync indicators
    dev.ts                     # Standalone Vite dev entry that mounts EPUB Studio directly
    dom.ts                     # Static HTML template builder for the full EPUB Studio UI
    editable-select.ts         # Reusable editable select/dropdown helper
    editor-history.ts          # Undo/redo history tracking for WYSIWYG editor changes
    editor-selection.ts        # Selection save/restore helpers for toolbar and popup interactions
    editor.ts                  # WYSIWYG chapter editor, metadata editing, links, images, TOC actions
    engine.ts                  # EPUB ZIP/OPF/spine/TOC parser and open/drop file loading
    exporter.ts                # EPUB rebuild/export logic and platform presets
    font-size-selection-input.ts # Selection-aware font-size input for editor formatting
    fonts.ts                   # Reader font catalog, font loading, and font select rendering
    format-toolbar.ts          # Editor toolbar wiring and formatting command dispatch
    format.ts                  # Pure formatting helpers and format action definitions
    image-library.ts           # Pure image path normalization, discovery, and asset helpers
    image-manager.ts           # Image panel UI, image import/remove/preview, cover/chapter image handling
    index.ts                   # Public mount API, config handling, module initialization, wMCP wiring
    link.ts                    # Internal link picker, link preview, link context menu, link editing
    notify.ts                  # Toasts, button tooltips, and lightweight notification helpers
    reader.ts                  # Reader view rendering, pagination, search, theme, font, writing mode
    shortcuts.ts               # Platform-aware shortcut labels and toolbar titles
    state.ts                   # Legacy state compatibility exports
    store.ts                   # AppState, reducer/store, preferences, and legacy bridge
    styles/
      index.css                # EPUB Studio styles, themes, layout, panels, modals, editor, reader
    structure-edit.ts          # Structure tree edit operations: rename, move, promote/demote, TOC visibility
    structure-rewrite.ts       # Pure HTML/nav rewrite helpers for structure edits and ID/reference updates
    structure-undo.ts          # Snapshot/restore stack for structure editing undo
    structure.ts               # Chapter/TOC flat/tree rendering, selection, expansion, drag/drop structure UI
    ui.ts                      # Sidebar/panel tabs, mode switching, modal handling, chapter/TOC list rendering
    utils.ts                   # Shared DOM, HTML, file, status, preference, and download helpers
    viewer-width.ts            # Reader content width controls and persistence
    viewer-zoom.ts             # Reader zoom controls, presets, keyboard shortcuts, persistence
    vite-env.d.ts              # Vite and asset module type declarations
  tests/
    engine.test.ts             # EPUB parsing behavior tests
    exporter.test.ts           # EPUB export/rebuild behavior tests
    image-library.test.ts      # Image helper and path normalization tests
    state.test.ts              # Store/state behavior tests
    wmcp.test.ts               # wMCP manifest/integration contract tests
  demo/
    nextjs/
      src/
        app/
          api/
            epub/
              route.ts         # Demo API route for EPUB upload/export flows
          epub/
            actions.ts         # Demo server actions for save/export/audit logging
            page.tsx           # Client EPUB Studio mount page with dynamic package import
          globals.css          # Demo app global CSS
          layout.tsx           # Demo app root layout
          page.tsx             # Demo app home page
        wmcp/
          epub-studio.ts       # Demo wMCP host setup, overrides, host listeners, close handling

Getting Started

1. Install dependencies

cd v1.1
pnpm install

2. Dev mode (standalone, no wMCP host)

pnpm run dev

Opens at http://localhost:5173. Uses src/dev.ts which calls mount() directly.

3. Build the library

pnpm run build

Outputs to dist/:

  • epub-studio.es.js (ESM)
  • epub-studio.cjs.js (CJS)
  • epub-studio.css
  • index.d.ts

4. Run the Next.js demo (with wMCP host)

cd demo/nextjs
pnpm install
pnpm run dev

Opens at http://localhost:3000/epub.

After rebuilding the library (pnpm run build in v1.1/), run pnpm install in demo/nextjs/ to re-link the updated dist, then restart the Next.js dev server.

Usage

Standalone (no host)

import { mount } from "@aurorah/epub-studio";
import "@aurorah/epub-studio/style.css";

const instance = mount(document.getElementById("root")!, {
  theme: "dark",
  locale: "ko",
});

// Later: instance.destroy();

With wMCP host

import { mount } from "@aurorah/epub-studio";
import "@aurorah/epub-studio/style.css";
import { WmcpHost } from "@aurorah/wmcp";

const instance = mount(container, {
  theme: "dark",
  locale: "ko",
  openButton: false,
  translateButton: true,
  closeButton: true,
});

const host = new WmcpHost(instance.wmcpClient);

// Override capabilities to route through server APIs
host.override("epub:save", async (params, defaultSave) => {
  const blob = await defaultSave(params);
  return await saveToServer(blob);
});

// Listen for module events
host.on("epub:loaded", (data) => console.log(data));
host.on("epub:modified", (data) => console.log(data));
host.on("epub:translate", (data) => console.log(data));
host.on("epub:close", () => {
  host.destroy();
  instance.destroy();
});

// IMPORTANT: gate `epub:openFile` on `wmcp:ready`.
//
// `mount()` returns synchronously but several studio subsystems (Reader
// pane sizing, Monaco editor worker, Fonts, CodeEditor) only stabilize
// after the browser paints once. Emitting `epub:openFile` during that
// gap causes a race where TOC / 파일구조 populates but the
// viewer / editor / code panes silently stay on the placeholder.
//
// `@aurorah/[email protected]` (with `@aurorah/[email protected]`) emits the
// protocol-level `wmcp:ready` event once all subsystems have laid out.
// Do the fetch + `host.emit("epub:openFile", ...)` inside this listener
// to guarantee the studio is fully wired before the EPUB lands.
host.on("wmcp:ready", async () => {
  const blob = await fetchEpubBlob();
  host.emit("epub:openFile", { fileData: blob, fileName: "book.epub" });
});

// Other host->module commands can also be sent at any time; wMCP buffers
// them until `wmcp:ready` and replays FIFO.
host.emit("epub:setTheme", { theme: "sepia" });

Config

mount(container, config?) accepts EpubStudioConfig:

| Property | Type | Default | Description | | ----------------- | ----------------------------------------- | --------- | -------------------------------------------------------------- | | theme | "dark" \| "sepia" \| "white" \| "paper" | "sepia" | Initial color theme | | locale | string | "ko" | Locale hint | | readOnly | boolean | false | Disable editing | | openButton | boolean | false | Show an open-file button in the toolbar | | translateButton | boolean | false | Show a translate button; emits epub:translate via wMCP | | closeButton | boolean | true | Show a close button; emits epub:close event via wMCP | | useCustomFonts | boolean | false | If true, skip auto-loading Google Fonts; host provides fonts |

Code Editor

The code tab uses monaco-editor for HTML editing. It supports undo/redo, find/replace, go to line, folding, format, lint markers, word wrap, invisibles, autocomplete, fullscreen, and image-aware cursor sync.

Formatting is powered by prettier and @prettier/plugin-xml. The Vite library build externalizes @aurorah/wmcp, prettier, and @prettier/plugin-xml, and bundles Monaco's editor worker inline for browser compatibility.

Fonts

Architecture

Font rendering has two layers:

  1. CSS variables (--font, --r-font, --mono) -- defined in src/styles/index.css at :root. These declare font-family names only. Every UI element in the stylesheet references them via var().
  2. Font file loading -- the actual .woff2 files that the browser needs to render those named families. This is handled by injectFonts() in src/index.ts.
mount() called
  |
  |- useCustomFonts: false (default)
  |    |
  |    +-> injectFonts() appends a Google Fonts <link> to <head>
  |        Google Fonts serves @font-face rules that load .woff2 files
  |        matching the font-family names in --font, --r-font, --mono
  |
  |- useCustomFonts: true
       |
       +-> injectFonts() is skipped
           Host must load fonts itself (next/font, @font-face, CDN link)
           Host can also override --font, --r-font, --mono on :root
           to use entirely different font families

CSS variables

| Variable | Used for | Default value | | ---------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --font | UI -- header, sidebar, panels, buttons, inputs, modals | "Noto Sans KR", "Noto Sans JP", "Noto Sans SC", "Noto Sans TC", "Noto Sans Arabic", "Noto Sans Devanagari", "Noto Sans Thai", "Noto Sans Bengali", "Noto Sans", sans-serif | | --r-font | Reader -- EPUB content viewer, chapter text | "Noto Serif KR", "Noto Serif JP", "Noto Serif SC", "Noto Serif TC", "Noto Serif", "Batang", serif | | --mono | Code -- code editor, raw HTML view, metadata fields | "JetBrains Mono", monospace |

These variables are defined in src/styles/index.css and bundled into dist/epub-studio.css. The fallback chain ensures the browser picks the correct font per character (e.g. Korean glyphs from Noto Sans KR, Japanese from Noto Sans JP, Arabic from Noto Sans Arabic, etc.).

Default fonts loaded

| CSS Variable | Fonts loaded from Google Fonts | Coverage | | ------------ | ------------------------------------------------------------- | ---------------------------------------------- | | --font | Noto Sans + KR, JP, SC, TC, Arabic, Devanagari, Thai, Bengali | UI -- Latin, CJK, Arabic, Hindi, Thai, Bengali | | --r-font | Noto Serif + KR, JP, SC, TC | Reader -- Latin, CJK | | --mono | JetBrains Mono | Code editor |

Google Fonts only downloads glyphs actually used on the page (unicode-range subsetting), so the real network payload stays small.

Custom fonts

If your host app already loads fonts (e.g. via next/font, @font-face, or a different CDN), pass useCustomFonts: true to skip auto-injection:

const instance = mount(container, {
  useCustomFonts: true,
});

When useCustomFonts is true, you must ensure the font families referenced by --font, --r-font, and --mono are available in the document. You can either:

  • Load the same font families that the defaults reference (e.g. Noto Sans KR)
  • Override the CSS variables on :root or .epub-studio-root to point to your own fonts

Next.js examples

Default (auto-inject) -- no font setup needed:

// app/epub/page.tsx
"use client";
import { useEffect, useRef } from "react";
import "@aurorah/epub-studio/style.css";

export default function EpubPage() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let cleanup: (() => void) | undefined;
    let cancelled = false;

    async function init() {
      const { mount } = await import("@aurorah/epub-studio");
      if (cancelled || !ref.current) return;

      const instance = mount(ref.current, { theme: "dark" });
      cleanup = () => instance.destroy();
    }

    init();

    return () => {
      cancelled = true;
      cleanup?.();
    };
  }, []);

  return <div ref={ref} style={{ width: "100vw", height: "100vh" }} />;
}

With wMCP host integration:

Use this when the Next.js app should observe EPUB Studio events, override save/export behavior, or close/navigate from the host app.

// app/epub/page.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import "@aurorah/epub-studio/style.css";
import { setupHost } from "@/wmcp/epub-studio";

export default function EpubPage() {
  const containerRef = useRef<HTMLDivElement>(null);
  const mountedRef = useRef(false);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    setReady(true);
  }, []);

  useEffect(() => {
    if (!ready || !containerRef.current || mountedRef.current) return;
    mountedRef.current = true;

    let cleanup: (() => void) | undefined;
    let cancelled = false;

    async function init() {
      const { mount } = await import("@aurorah/epub-studio");
      if (cancelled || !containerRef.current) return;

      const instance = mount(containerRef.current, {
        theme: "dark",
        locale: "ko",
        openButton: true,
        translateButton: true,
        closeButton: true,
      });

      const host = setupHost(instance.wmcpClient, {
        onClose: () => {
          cleanup?.();
          window.location.href = "/";
        },
      });

      // Gate the EPUB fetch + open on `wmcp:ready`. See the "Studio readiness"
      // note below for why this matters — emitting `epub:openFile` before the
      // studio's first paint causes a race where TOC populates but the
      // viewer / editor / code panes stay empty.
      host.on("wmcp:ready", async () => {
        const blob = await fetchEpubBlob();
        host.emit("epub:openFile", { fileData: blob, fileName: "book.epub" });
      });

      cleanup = () => {
        host.destroy();
        instance.destroy();
      };
    }

    init();

    return () => {
      cancelled = true;
      cleanup?.();
      mountedRef.current = false;
    };
  }, [ready]);

  return <div ref={containerRef} id="epub-studio-root" style={{ width: "100vw", height: "100vh" }} />;
}
// src/wmcp/epub-studio.ts
import { WmcpHost } from "@aurorah/wmcp";
import type { WmcpClient } from "@aurorah/wmcp";
import { saveEpubToApi, exportEpubToApi, writeAuditLog } from "@/app/epub/actions";

export function setupHost(client: WmcpClient, opts?: { onClose?: () => void }): WmcpHost {
  const host = new WmcpHost(client);

  host.override("epub:open", async (params, defaultEpubOpen) => {
    const result = await defaultEpubOpen(params);
    await writeAuditLog({ action: "open", detail: { fileName: params.fileName } });
    return result;
  });

  host.override("epub:save", async (params, defaultEpubSave) => {
    const blob = await defaultEpubSave(params);
    return await saveEpubToApi(blob as Blob);
  });

  host.override("epub:export", async (params, defaultEpubExport) => {
    const blob = await defaultEpubExport(params);
    return await exportEpubToApi(params.platformId as string, blob as Blob);
  });

  host.connectDirect({
    "log:write": async (params) => writeAuditLog(params as { action: string; detail?: object }),
  });

  host.on("epub:loaded", (data) => console.log("[Host] EPUB loaded:", data));
  host.on("epub:modified", (data) => console.log("[Host] EPUB modified:", data));
  host.on("epub:translate", (data) => console.log("[Host] EPUB translate requested:", data));

  if (opts?.onClose) {
    host.on("epub:close", () => opts.onClose!());
  }

  return host;
}

Key points:

  • Keep import "@aurorah/epub-studio/style.css" static so Next.js includes the CSS.
  • Dynamically import mount() inside the client effect because EPUB Studio depends on browser APIs.
  • Use mountedRef and cancelled to avoid duplicate mounts and async mount after unmount.
  • setupHost(instance.wmcpClient) connects the module to the host app.
  • host.override() lets the host wrap default module behavior and then forward blobs to server actions.
  • host.connectDirect() exposes optional host requirements like log:write back to the module.
  • Always destroy both host and instance in cleanup.
  • Always do epub:openFile (and any other host-side EPUB load logic) inside host.on("wmcp:ready", ...) — see "Studio readiness" below.

Studio readiness — wmcp:ready

mount() returns synchronously, but the Reader pane sizing, Monaco editor worker, CodeEditor, and Fonts only stabilize after the browser paints once. Emitting epub:openFile during that gap causes a race where TOC / 파일구조 populates (sync wMCP listeners on the store) but the viewer / editor / code panes silently stay on the placeholder.

Starting in @aurorah/[email protected] (with @aurorah/[email protected]), the studio emits the protocol-level wmcp:ready event once all subsystems have laid out (after two chained requestAnimationFrames inside mount()). Hosts MUST run their EPUB fetch + open logic inside this listener:

host.on("wmcp:ready", async () => {
  const blob = await fetchEpubBlob();
  host.emit("epub:openFile", { fileData: blob, fileName: "book.epub" });
});

wMCP also buffers host->module events that arrive before the studio's first paint and replays them FIFO once readiness fires, so an early emit will not be lost. The wmcp:ready listener is still the recommended entry point because (1) it lets the host start its fetch lazily — only after the studio is actually mounted — and (2) any post-emit UI like a "loading EPUB" spinner can be coordinated against the same readiness signal.

Custom fonts with next/font (2 files needed):

Step 1 -- Load fonts in your root layout:

// app/layout.tsx
import { Noto_Sans_KR, Noto_Serif_KR } from "next/font/google";

const sans = Noto_Sans_KR({ subsets: ["latin"], variable: "--font" });
const serif = Noto_Serif_KR({ subsets: ["latin"], variable: "--r-font" });

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={`${sans.variable} ${serif.variable}`}>
      <body>{children}</body>
    </html>
  );
}

Step 2 -- Mount with useCustomFonts: true so the package uses the fonts from Step 1 instead of auto-injecting:

// app/epub/page.tsx
"use client";
import { useEffect, useRef } from "react";
import "@aurorah/epub-studio/style.css";

export default function EpubPage() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let cleanup: (() => void) | undefined;
    let cancelled = false;

    async function init() {
      const { mount } = await import("@aurorah/epub-studio");
      if (cancelled || !ref.current) return;

      const instance = mount(ref.current, {
        theme: "dark",
        useCustomFonts: true,
      });
      cleanup = () => instance.destroy();
    }

    init();

    return () => {
      cancelled = true;
      cleanup?.();
    };
  }, []);

  return <div ref={ref} style={{ width: "100vw", height: "100vh" }} />;
}

wMCP Contract

Defined in wmcp-manifest.json. Read it for the full protocol spec.

module:capabilities (module provides, host can override)

| Capability | Description | | ------------- | ------------------------------------ | | epub:open | Open and parse an EPUB file | | epub:save | Build an EPUB Blob from editor state | | epub:export | Export EPUB for a specific platform |

module:events (module emits, host listens)

| Event | Description | | ---------------- | ---------------------------------- | | epub:loaded | EPUB file was loaded and parsed | | epub:modified | Content was modified in the editor | | epub:translate | User clicked the translate button | | epub:close | User clicked the close button |

In addition, the protocol-level wmcp:ready event (emitted by @aurorah/wmcp >= 1.1.0) fires once after mount() when the studio is fully wired. Hosts MUST gate epub:openFile (and other initial host->module commands that depend on a stable studio) on this event — see Studio readiness.

module:listeners (host emits, module listens)

| Listener | Description | | --------------- | ------------------------------------- | | epub:openFile | Host requests opening a specific EPUB | | epub:setTheme | Host requests theme change |

host:requires (module needs from host, optional)

| Requirement | Description | | ------------ | ------------------------------------------------------ | | log:write | Write an audit log | | export:pdf | Render book HTML to PDF through a host-side PDF engine |

Export Platforms

The exporter supports: standard, epub2, ridi, kakao, naver, kindle, apple, syosetu.

Tests

pnpm test