Drop-in replacement for epubjs — same API, fully typed, 1 runtime dependency, actively maintained.
A complete TypeScript rewrite of epubjs v0.3.93 with strict mode, modern tooling, and ongoing bug fixes — without breaking your existing code.
- Same API, zero migration cost — change one import line and everything works
- Full TypeScript strict mode — generated
.d.tsfrom source, so autocomplete matches runtime - 1 runtime dependency (
jszip) — smaller bundle, simpler supply chain - 970+ tests across 40 files — Vitest with robust coverage
- Vite build — ESM, CJS, and UMD outputs out of the box
- Node.js support —
@likecoin/epub-ts/nodeparses EPUBs server-side withlinkedom - Active maintenance — 20+ bug fixes and counting
Note: Built at 3ook.com and provided as-is. Forked from epubjs v0.3.93 by Fred Chasen / FuturePress.
Head-to-head numbers on two Project Gutenberg fixtures: a small book
(Alice in Wonderland #11,
185 KB) and a large book
(War and Peace #2600,
1.7 MB). Apple M4 Pro, macOS 26.4, headless Chrome 146, Node 20,
median of 10–15 iterations. Run the bench yourself with
npm run bench; see bench/README.md for full
methodology and caveats.
| Metric | epubjs 0.3.93 | @likecoin/epub-ts | Δ |
|---|---|---|---|
| Bundle size (gzip, KB) | 132.8 | 57.5 | −56.7% |
| Alice (185 KB) | |||
| Cold parse (ms) | 2.2 | 2.2 | ≈ 0 |
| First display (ms) | 96.8 | 83.4 | −13.8% |
locations.generate(1000) (ms) |
1760.3 | 10.4 | −99.4% |
currentLocation() (ms / call) |
0.106 | 0.060 | −43.8% |
| War and Peace (1.7 MB) | |||
| Cold parse (ms) | 11.7 | 7.9 | −33.1% |
| First display (ms) | 90.1 | 91.9 | ≈ 0 |
locations.generate(1000) (ms) |
42903.3 | 158.9 | −99.6% |
currentLocation() (ms / call) |
0.196 | 0.153 | −22.1% |
43 seconds → 159 ms on locations.generate() for a 1.7 MB book:
this is the single biggest user-visible win, and it's what the
0.4.9 locations optimization was
aimed at. Both libraries produce the same output count
(169 locations for Alice, 429 for War and Peace) — the delta is real
work, not a short-circuit.
currentLocation() — the pagination / CFI-range query called on every
page turn — exercises the
0.6.0 canvas text measurement and
0.6.1 Mapping.findStart/findEnd binary search
and is roughly 2× faster as a result. next-page timings are omitted
from the table because both libraries converge on a frame-paced
~33 ms, which doesn't differentiate them.
npm install @likecoin/epub-tsChange one line — everything else stays the same:
- import ePub from "epubjs";
+ import ePub from "@likecoin/epub-ts";import ePub from "@likecoin/epub-ts";
const book = ePub("/path/to/book.epub");
const rendition = book.renderTo("viewer", { width: 600, height: 400 });
rendition.display();import ePub from "@likecoin/epub-ts";
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener("change", async (event) => {
const file = event.target.files[0];
const data = await file.arrayBuffer();
const book = ePub(data);
const rendition = book.renderTo("viewer", { width: 600, height: 400 });
rendition.display();
});For environments without a bundler — a plain HTML page, or a react-native-webview
loading local files — use the UMD build. It exposes a global ePub and expects
JSZip to be loaded first, mirroring the classic epubjs drop-in:
<script src="jszip.min.js"></script>
<script src="epub.umd.js"></script>
<script>
const book = ePub("book.epub");
const rendition = book.renderTo("viewer", { width: 600, height: 400 });
rendition.display();
</script>The bundle lives at dist/epub.umd.js — copy it locally, or load it from a CDN
via https://unpkg.com/@likecoin/epub-ts/dist/epub.umd.js. In a bundler, prefer
the standard entrypoint instead, which resolves to the ES module build:
import ePub from "@likecoin/epub-ts".
Loading a book from a file:// URL (e.g. a local .epub bundled into a WebView)
is supported: fetch can't read the file: scheme, so such requests fall back to
XMLHttpRequest. The host must grant file access (on Android WebView, enable
allowFileAccess / allowFileAccessFromFileURLs). Alternatively, read the file
yourself and hand epub.ts an ArrayBuffer — ePub(arrayBuffer) skips the network
entirely and works everywhere.
Extract metadata, table of contents, and chapter HTML server-side. Requires linkedom:
npm install linkedomimport { Book } from "@likecoin/epub-ts/node";
import { readFileSync } from "node:fs";
const data = readFileSync("book.epub");
const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
const book = new Book(arrayBuffer);
await book.opened;
console.log(book.packaging.metadata.title);
console.log(book.navigation.toc.map(item => item.label));
const section = book.spine.first();
const html = await section.render(book.archive.request.bind(book.archive));locations.locationFromHref() maps a navigation href to a generated location index, so a table of contents can show approximate page numbers:
await book.locations.generate(1000); // ~1000 characters per page
const toc = book.navigation.toc.map(item => {
const index = book.locations.locationFromHref(item.href);
return { ...item, page: index === -1 ? undefined : index + 1 };
});Notes:
- The index is 0-based, so add 1 for a page number. It returns
-1when the href cannot be resolved. - Page numbers depend on the
charsvalue passed togenerate()— they are estimates for navigation, not the publisher's printed pages. When a book ships a realpage-list, usebook.pageListinstead. - Entries are resolved per section, so two entries pointing into the same file (
chapter.xhtml#aandchapter.xhtml#b) share a page number. - Pre-paginated (fixed-layout) books resolve without calling
generate()— each section is exactly one page, so the spine index is returned. Once locations exist they take precedence, so the result is always an index intolocations.
import {
Book, EpubCFI, Rendition, Contents, Layout,
Section, Spine, Locations, Navigation, PageList,
Resources, Packaging, Archive, Store,
Annotations, Themes, Mapping,
} from "@likecoin/epub-ts";Full documentation: likecoin.github.io/epub.ts
| Class | What it does |
|---|---|
Book |
Load, parse, and manipulate an EPUB |
Rendition |
Render a book into a DOM element |
Contents |
Manage content inside an iframe |
EpubCFI |
Parse EPUB Canonical Fragment Identifiers |
Locations |
Generate and query reading positions |
Navigation |
Table of contents and landmarks |
Annotations |
Highlights, underlines, and marks |
| Environment | Import | Notes |
|---|---|---|
| Modern browsers | @likecoin/epub-ts |
Chrome 80+, Edge 80+, Firefox 74+, Safari 13.4+ (ES2020, Q1 2020) |
| Insecure contexts | @likecoin/epub-ts |
http:// intranet and file:// deployments supported — runtime APIs gated behind secure contexts (crypto.randomUUID) are feature-detected with fallbacks |
| Vite / webpack | @likecoin/epub-ts |
ESM or CJS |
<script> tag / WebView |
dist/epub.umd.js |
UMD global ePub; load JSZip first. Reads file:// books via XHR fallback |
| Node.js 18+ | @likecoin/epub-ts/node |
Parsing only (no rendering); requires linkedom peer dep |
git clone https://github.com/likecoin/epub.ts.git
cd epub.ts
npm install| Script | Description |
|---|---|
npm run build |
Vite library build → dist/ |
npm test |
Run tests (Vitest) |
npm run test:watch |
Run tests in watch mode |
npm run typecheck |
tsc --noEmit |
npm run lint |
ESLint |
npm run lint:fix |
ESLint with auto-fix |
npm run docs |
Generate API docs (HTML + Markdown) |
Requires Node.js 18+ and npm 9+.
EPUBs are untrusted input. Sections render in a sandboxed iframe with scripts disabled by default; see SECURITY.md for the threat model, the cost of allowScriptedContent: true, and how to report a vulnerability.
See PROJECT_STATUS.md for current status and what to work on.
For AI agents contributing to this project, see AGENTS.md.
BSD-2-Clause (same as epubjs)
- epubjs by Fred Chasen / FuturePress — the original library this is forked from
- jszip — ZIP file handling
Built by 3ook.com
3ook is a Web3 eBook platform where authors publish EPUB ebooks and readers collect them as digital assets.
- epubjs — Original EPUB reader library
- epubcheck-ts — TypeScript EPUB validator (also by 3ook.com)