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

@abfcode/spine

v0.13.0

Published

Fast, dependency-light EPUB toolkit for parsing, transforming, normalizing, and writing ebooks anywhere.

Readme

@abfcode/spine

npm CI license

A fast EPUB toolkit: parse, write, normalize, and ingest ebooks anywhere. One parse() call turns EPUB bytes into structured content blocks, real chapters, a normalized TOC, metadata, text chunks, and resolved anchors — built for readers, RAG pipelines, TTS, and search indexing, and built to survive everything from pristine EPUB3 to broken OPF soup.

npm install @abfcode/spine

Why this one

  • Runs anywhere — including where DOM parsers can't. Node, browsers, Web Workers, Electron (renderer and main), Bun, Deno, edge runtimes. Parsing runs on an in-tree streaming tokenizer (2460/2460 html5lib conformance), so there's no DOM anywhere. That's not a portability nicety: DOM-backed parsing needs jsdom in Node (5–7× slower — measured against spine's own earlier jsdom pipeline) and can't run in a Web Worker at all, because DOMParser doesn't exist there.
  • Fast at every scale. Metadata remains millisecond-scale (lazy zip — images you don't ask for are never inflated); Moby-Dick fully extracts in about 40ms on the current reference run; a 3,000-chapter / ~14MB-text web novel in about 480ms, scaling approximately linearly. Verified in-browser, not just in Node — see Performance.
  • One runtime dependency (fflate). Tokenizer, entities, zip directory, XML handling: all in-tree, purpose-built for ebooks.
  • Hard to break. Malformed XML recovers with warnings, missing containers/spines/TOCs fall back to archive scanning, DRM surfaces as a typed error instead of garbage, and a fuzz suite guarantees termination.
  • Built for real readers. Chapters cut at TOC targets (multi-chapter files and file-spanning chapters both come out right), footnote/epub:type semantics, RTL direction, pre-resolved links and image paths — and the block model is safe to render by construction: no innerHTML anywhere, so a hostile EPUB can't smuggle <script> into your UI.
  • Typed and verified, deeply. TypeScript-first, stable error codes, structured warnings, and 2,800+ tests across a dozen harnesses — unit, html5lib conformance, property-based laws, generative + mutational fuzzing, a hostile-input suite (zip bombs, ReDoS, entity expansion), golden corpora, differential-vs-jsdom, generated round-trips, warning-free epubcheck, a 3,000-chapter scale harness, and a wild corpus of 64 real books parsed end-to-end with committed diffable results. Mutation-tested (StrykerJS) and perf/memory-regression-tracked. See Verification.

Usage

import { parse } from '@abfcode/spine'

const book = parse(epubBytes) // Uint8Array

book.metadata.title           // 'Moby Dick; Or, The Whale'
book.toc                      // normalized table of contents
book.blocks(0)                // structured blocks for spine item 0

Writing and normalizing EPUBs

The experimental writer lives at a separate subpath, keeping parser-only bundles unchanged:

import { parse } from '@abfcode/spine'
import { writeEpub, bookToSpec } from '@abfcode/spine/write'

const bytes = writeEpub({
  metadata: { title: 'My Book', authors: ['A. Writer'] },
  chapters: [
    { title: 'One', content: { text: 'First paragraph.\n\nSecond paragraph.' } },
  ],
})

// Clean and rewrite an existing book:
const normalized = writeEpub(bookToSpec(parse(existingBytes)))

Output is deterministic by default. EPUB3+NCX, nav-only EPUB3, and EPUB2 are supported; the generated conformance matrix passes epubcheck with warnings treated as failures. The writer API remains experimental until 1.0. See the API reference.

Text ingestion (.txt / .md → EPUB)

Huge amounts of webnovel content circulate as giant .txt rips. The /text subpath turns them into shelf-ready books — for a library app's import flow, that means raw rips go straight onto the shelf:

import { textToEpub, textToBook } from '@abfcode/spine/text'

const epub = textToEpub(txtBytes, { metadata: { title: 'My Rip' } })
const book = textToBook(txtBytes) // or parse it in one step

Chapters come from an ordered heuristic ruleset (Chapter 1 / CHAPTER IV / Ch. 3 / markdown headings / CJK 第一章 / ALL-CAPS conventions...) with guards biased toward under-splitting — a marker-less wall stays one chapter, and every cut is observable through onWarning. Handles legacy encodings (gb18030, shift-jis, ...), multi-file rips, Gutenberg boilerplate, and poetry; output passes epubcheck warning-free. Full options in the API reference.

Chapters (RAG / TTS / export)

for (const ch of book.iterChapters()) {
  ch.title // 'CHAPTER 1. Loomings.' — cut at TOC targets, so multi-chapter
  ch.text  // files and file-spanning chapters both come out right
  ch.start // { spineIndex, blockIndex } — map text back to structure
}

Chunks (fixed-size text windows)

const chunks = book.chunks({ mode: 'size', maxChars: 2000 })
// each chunk: stable id, text, spineIndex, block range, anchors

Reader rendering

const blocks = book.blocks(spineIndex)
// Inline texts carry their own separator spaces: concatenate directly.
// See docs/API.md for the full Block/Inline model and whitespace contract.

Off the main thread

Blocks, chunks, and chapters are plain structured-clone-able objects, so worker parsing is just:

// worker.ts
import { parse } from '@abfcode/spine'
onmessage = (e) => {
  const book = parse(e.data)
  postMessage({ toc: book.toc, chapters: book.chapters() })
}

Error handling

import { isSpineError } from '@abfcode/spine'

try {
  const book = parse(bytes)
  console.warn(book.warnings) // non-fatal recoveries, if any
} catch (e) {
  if (isSpineError(e, 'drm_encrypted')) {
    // tell the user the book is DRM-protected
  }
}

Performance

Full pipeline (parse + every block + fixed-size chunks), median from a quiet WSL2 reference run on this development machine with Node 24.11.1. These are machine measurements, not cross-hardware guarantees:

| book | size | time | | --- | --- | --- | | Alice in Wonderland | 135 KB | 7.4 ms | | Frankenstein | 349 KB | 13.6 ms | | Moby-Dick | 711 KB | 40.1 ms | | Leaves of Grass (379 spine files) | 680 KB | 75.3 ms | | War and Peace | 1.8 MB | 137.2 ms | | Complete Works of Shakespeare | 2.8 MB | 407.2 ms | | Synthetic web novel (3,000 chapters, 13,971,369 text chars) | 2.8 MB | 480.5 ms |

Metadata + TOC only: 0.2–9.3 ms in that run. Scaling is approximately linear in book size — a 6,000-chapter novel takes ~2× the 3,000-chapter one. Run pnpm bench to reproduce. It generates the canonical scale EPUB plus a manifest containing its size and SHA-256, then records full sample statistics under the gitignored .bench-fixtures/ directory. pnpm verify:scale is the regression gate: it fails if doubling chapter count approaches quadratic growth.

The numbers hold in the primary deployment environment, not just Node: pnpm bench:browser runs the same files in a module Worker inside real headless Chromium and writes its own hash-linked JSON record. Across the current Chrome 149 / Node 24 run, Worker heavy-extraction times were ~1.03–1.57× Node; the canonical scale book was ~1.10× Node.

Documentation

Development

  • Prerequisites: Node 24.15 or newer and the exact pnpm version pinned in package.json. Corepack and GitHub Actions both read that pin; run pnpm run doctor to verify Node, pnpm, the temporary directory, Java, Chromium, Bun, and Deno without installing anything.
  • In WSL, Windows may leak TEMP/TMP paths into Node subprocesses. If Vitest or tsx cannot create a temporary directory or IPC socket, run with TMPDIR=/tmp (for example, TMPDIR=/tmp pnpm test).
  • pnpm test — the fast suite (PR-blocking, < ~30s): unit, property laws, golden corpora, html5lib conformance, generative fuzz, hostile-input suite, API-surface snapshot, differential-vs-jsdom, i18n
  • pnpm typecheck — type-check source, tests, and validation tooling; runs in CI alongside the build and fast suite.
  • UPDATE_GOLDEN=1 pnpm test — regenerate golden snapshots after an intentional change (review the diff!)
  • pnpm bench / pnpm bench:browser — corpus benchmarks (Node / worker)
  • pnpm build — emit dist/ (ESM + .d.ts)

Verification

The fast suite (pnpm test) plus the ReDoS audit and Node/Bun/Deno smoke run in CI on every change. The heavier lanes below are local, on-demand audit tools — each has a command and a committed baseline, so a maintainer runs them when touching the relevant area and reviews the result as a git diff:

| lane | command | what it guards | | --- | --- | --- | | Wild corpus | pnpm verify:corpus | 64 real books parsed end-to-end; committed diffable per-book results | | Splitter oracle | pnpm verify:splitter | text-splitter precision/recall vs the same books' EPUB chapters | | Mutational fuzz | pnpm verify:fuzz | mutated EPUB bytes (zip surgery) must only ever throw typed errors (also a nightly CI run) | | ReDoS audit | pnpm verify:redos | every source regex checked for catastrophic backtracking (in CI) | | Mutation testing | pnpm verify:mutation | StrykerJS grades the tests themselves; a ratcheted baseline | | Performance / memory | pnpm bench, pnpm bench:browser, pnpm verify:scale, pnpm verify:perf, pnpm verify:memory | shipped-dist Node/Worker timings, linear scaling, book-ratio perf, no-leak / lazy-zip / heap envelopes |

Baselines for the corpus, splitter, mutation score, and perf/memory are committed under corpus/, so a regression shows up as a reviewable diff.

Status & stability

0.x: breaking changes land at minor versions and are called out in the CHANGELOG. The compatibility promise itself is written down in the stability contract — in short: error codes, warning codes, the block model (including its JSON shape, with unknown-kind tolerance for union growth), chapter coordinates, href canonical form, and deterministic output are contract; message text and cross-version parse output are not (key persisted parses to the exported VERSION).

ESM-only, by design — every supported runtime speaks ESM natively. Type declarations ship with the package.

License

Apache-2.0