Skip to content

Comments

✨ Add Node.js parsing-only entry point#4

Merged
williamchong merged 3 commits intolikecoin:masterfrom
williamchong:master
Feb 18, 2026
Merged

✨ Add Node.js parsing-only entry point#4
williamchong merged 3 commits intolikecoin:masterfrom
williamchong:master

Conversation

@williamchong
Copy link
Member

Add @likecoin/epub-ts/node subpath export that shims DOMParser, XMLSerializer, and document via linkedom, enabling EPUB parsing (metadata, spine, navigation, section rendering) in Node.js without a browser.

  • Add linkedom as optional peerDependency
  • Create src/node.ts with linkedom shims and parsing-safe re-exports
  • Add vite.config.node.ts for ESM + CJS node bundles
  • Guard window references in archive.ts, url.ts, replacements.ts
  • Replace window.decodeURIComponent with global decodeURIComponent
  • Add try/catch for CSS namespace selectors in querySelectorByType
  • Add getElementsByTagName fallback for parsers without NS support
  • Add 10 Node.js integration tests (191 total)

Add `@likecoin/epub-ts/node` subpath export that shims DOMParser,
XMLSerializer, and document via linkedom, enabling EPUB parsing
(metadata, spine, navigation, section rendering) in Node.js without
a browser.

- Add linkedom as optional peerDependency
- Create src/node.ts with linkedom shims and parsing-safe re-exports
- Add vite.config.node.ts for ESM + CJS node bundles
- Guard window references in archive.ts, url.ts, replacements.ts
- Replace window.decodeURIComponent with global decodeURIComponent
- Add try/catch for CSS namespace selectors in querySelectorByType
- Add getElementsByTagName fallback for parsers without NS support
- Add 10 Node.js integration tests (191 total)
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Node.js-focused, parsing-only entry point to epub.ts, allowing EPUB parsing (metadata/spine/navigation/section serialization) in non-browser environments by shimming required DOM globals via linkedom.

Changes:

  • Add @likecoin/epub-ts/node subpath export plus a dedicated Node build (vite.config.node.ts) producing ESM+CJS bundles.
  • Introduce src/node.ts to install DOMParser/XMLSerializer/document shims and re-export parsing-safe APIs.
  • Improve Node/SSR compatibility by guarding window usage, using global decodeURIComponent, and adding selector/namespace fallbacks; add Node integration tests.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
vite.config.node.ts Adds a second Vite library build to output Node-specific ESM/CJS bundles.
src/node.ts Node entry point: installs linkedom-based DOM shims and re-exports core classes/types.
package.json Adds ./node export and updates build script; declares linkedom as optional peer + dev dependency.
package-lock.json Locks linkedom and its transitive dependencies.
src/archive.ts Removes window.decodeURIComponent usage and guards URL API access for Node.
src/utils/url.ts Guards window.location usage to avoid crashing in Node/SSR.
src/utils/replacements.ts Guards window.location usage to avoid crashing in Node/SSR.
src/utils/core.ts Adds try/catch + fallback logic for environments lacking CSS namespace selector support.
src/packaging.ts Adds a non-namespace fallback for DC metadata lookup for parsers without NS support.
test/node.test.ts Adds Node environment integration tests for opening/parsing/rendering a fixture EPUB.
README.md Documents the new Node parsing-only entry point and basic usage.
PROJECT_STATUS.md Updates project status and artifact list to reflect Node entry point + added tests.
CHANGELOG.md Adds release notes for the Node entry point and related compatibility fixes.
AGENTS.md Documents src/node.ts as the Node entry point.

src/archive.ts Outdated
Comment on lines 152 to 153
const decodededUrl = decodeURIComponent(url.substr(1)); // Remove first slash
const entry = this.zip!.file(decodededUrl);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the new/modified variable name decodededUrl makes the code harder to read and search. Consider renaming to decodedUrl (and updating the subsequent uses) in this method.

Copilot uses AI. Check for mistakes.
src/archive.ts Outdated
Comment on lines 170 to 171
const decodededUrl = decodeURIComponent(url.substr(1)); // Remove first slash
const entry = this.zip!.file(decodededUrl);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the new/modified variable name decodededUrl makes the code harder to read and search. Consider renaming to decodedUrl (and updating the subsequent uses) in this method.

Copilot uses AI. Check for mistakes.
src/node.ts Outdated

if (typeof globalThis.document === "undefined") {
const { document } = parseHTML("<!DOCTYPE html><html><head></head><body></body></html>");
(globalThis as any).document = document;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(globalThis as any).document = document introduces an any escape hatch in a public entry point. Prefer a typed assignment (e.g., a narrow type assertion on globalThis or a declare global augmentation for document) so this stays type-safe under strict mode.

Suggested change
(globalThis as any).document = document;
(globalThis as typeof globalThis & { document: Document }).document = document as Document;

Copilot uses AI. Check for mistakes.
README.md Outdated
import { readFileSync } from "node:fs";

const data = readFileSync("book.epub");
const book = new Book(data.buffer);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Node.js README example passes data.buffer directly from a Node Buffer. For many buffers this includes extra bytes (because of byteOffset/byteLength), which can corrupt the EPUB input. Convert to an exact ArrayBuffer slice (like the Node test does) before constructing Book.

Suggested change
const book = new Book(data.buffer);
const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
const book = new Book(arrayBuffer);

Copilot uses AI. Check for mistakes.
src/archive.ts Outdated
Comment on lines 133 to 134
const decodededUrl = decodeURIComponent(url.substr(1)); // Remove first slash
const entry = this.zip!.file(decodededUrl);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the new/modified variable name decodededUrl makes the code harder to read and search. Consider renaming to decodedUrl (and updating the subsequent uses) in this method.

Copilot uses AI. Check for mistakes.
- Fix typo: decodededUrl → decodedUrl in archive.ts (6 occurrences)
- Remove `any` escape: use typed assertion for globalThis.document in node.ts
- Fix README example: slice Buffer to exact ArrayBuffer to avoid corruption
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.

README.md Outdated

### Node.js (parsing only)

Parse EPUB metadata, spine, navigation, and section content without a browser. Requires [`linkedom`](https://github.com/nicoleahmed/linkedom) as a peer dependency.
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linkedom GitHub URL is incorrect. It should point to https://github.com/WebReflection/linkedom (the official repository by WebReflection), not https://github.com/nicoleahmed/linkedom which does not exist.

Suggested change
Parse EPUB metadata, spine, navigation, and section content without a browser. Requires [`linkedom`](https://github.com/nicoleahmed/linkedom) as a peer dependency.
Parse EPUB metadata, spine, navigation, and section content without a browser. Requires [`linkedom`](https://github.com/WebReflection/linkedom) as a peer dependency.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.

Comment on lines +15 to +30
if (typeof globalThis.DOMParser === "undefined") {
globalThis.DOMParser = LinkedomDOMParser as unknown as typeof globalThis.DOMParser;
}

if (typeof globalThis.XMLSerializer === "undefined") {
globalThis.XMLSerializer = class XMLSerializer {
serializeToString(node: Node): string {
return (node as unknown as { toString(): string }).toString();
}
} as unknown as typeof globalThis.XMLSerializer;
}

if (typeof globalThis.document === "undefined") {
const { document } = parseHTML("<!DOCTYPE html><html><head></head><body></body></html>");
(globalThis as typeof globalThis & { document: Document }).document = document as Document;
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The src/node.ts module modifies globalThis (adding DOMParser, XMLSerializer, and document), which are side effects. However, package.json declares "sideEffects": false, which tells bundlers that no modules have side effects.

In practice, this works because:

  • Users must import from this module to use it
  • The globalThis modifications run at module initialization
  • The package builds to single-file bundles

However, this is semantically inaccurate. For correctness, consider updating package.json to "sideEffects": ["dist/epub.node.js", "dist/epub.node.cjs"] to explicitly mark the node bundles as having side effects. This would prevent potential edge cases where advanced tree-shaking might skip the module initialization.

Copilot uses AI. Check for mistakes.

console.log(book.packaging.metadata.title);
console.log(book.navigation.toc.map(item => item.label));

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README example assumes book.archive exists, but if the book failed to open or if there's an issue, book.archive could be undefined. Consider adding a null check or wrapping in a try-catch for better error handling in the documentation example.

For example:

if (!book.archive) {
  throw new Error("Failed to open EPUB archive");
}
const section = book.spine.first();
const html = await section.render(book.archive.request.bind(book.archive));

This would make the example more robust and help users understand potential failure modes.

Suggested change
if (!book.archive) {
throw new Error("Failed to open EPUB archive");
}

Copilot uses AI. Check for mistakes.
@williamchong williamchong merged commit 2626c36 into likecoin:master Feb 18, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant