frond-js 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +247 -0
  3. package/README.md +211 -0
  4. package/dist/abort-BY8vBk0v.d.cts +99 -0
  5. package/dist/abort-BY8vBk0v.d.ts +99 -0
  6. package/dist/adapter-3G46J3CA.cjs +503 -0
  7. package/dist/adapter-3ONQWJVQ.js +501 -0
  8. package/dist/adapter-55QHWRSE.js +124 -0
  9. package/dist/adapter-DF34GBWJ.cjs +19 -0
  10. package/dist/adapter-EXTNILTC.cjs +126 -0
  11. package/dist/adapter-GOFC7TMC.js +284 -0
  12. package/dist/adapter-LRJTQ47I.cjs +286 -0
  13. package/dist/adapter-TWWZML4A.js +17 -0
  14. package/dist/adapter-ZM5FQTJT.js +1415 -0
  15. package/dist/adapter-ZRNSDQUV.cjs +1417 -0
  16. package/dist/chunk-2SUG7YFZ.cjs +108 -0
  17. package/dist/chunk-B2L2YVXD.js +89 -0
  18. package/dist/chunk-D4KWSEZD.js +393 -0
  19. package/dist/chunk-EZTIZO6R.cjs +430 -0
  20. package/dist/chunk-G7DLWGBW.cjs +103 -0
  21. package/dist/chunk-GTGLDLJD.cjs +479 -0
  22. package/dist/chunk-IIV6VIUJ.cjs +83 -0
  23. package/dist/chunk-JDTHZQUK.js +102 -0
  24. package/dist/chunk-JJVXT3AC.js +30 -0
  25. package/dist/chunk-LIXRYQL2.js +473 -0
  26. package/dist/chunk-NABYHI6X.cjs +400 -0
  27. package/dist/chunk-SJVOYNTF.js +425 -0
  28. package/dist/chunk-SLI2YL25.cjs +252 -0
  29. package/dist/chunk-U4264IQH.js +78 -0
  30. package/dist/chunk-UY2YRCFC.js +250 -0
  31. package/dist/chunk-WCZTTQ7Z.cjs +32 -0
  32. package/dist/core/index.cjs +162 -0
  33. package/dist/core/index.d.cts +321 -0
  34. package/dist/core/index.d.ts +321 -0
  35. package/dist/core/index.js +49 -0
  36. package/dist/default-DRLIJX73.js +1183 -0
  37. package/dist/default-UK52WOO5.cjs +1192 -0
  38. package/dist/formats/epub/index.cjs +29 -0
  39. package/dist/formats/epub/index.d.cts +286 -0
  40. package/dist/formats/epub/index.d.ts +286 -0
  41. package/dist/formats/epub/index.js +11 -0
  42. package/dist/index.cjs +655 -0
  43. package/dist/index.d.cts +335 -0
  44. package/dist/index.d.ts +335 -0
  45. package/dist/index.js +600 -0
  46. package/dist/render/index.cjs +2 -0
  47. package/dist/render/index.d.cts +116 -0
  48. package/dist/render/index.d.ts +116 -0
  49. package/dist/render/index.js +1 -0
  50. package/dist/types-B76GOMxj.d.ts +129 -0
  51. package/dist/types-B7mslPBY.d.cts +166 -0
  52. package/dist/types-B7mslPBY.d.ts +166 -0
  53. package/dist/types-BH88rUYt.d.cts +129 -0
  54. package/dist/types-C-5eHSRH.d.ts +379 -0
  55. package/dist/types-CPUqTEPW.d.cts +379 -0
  56. package/dist/types-DQYmArgv.d.cts +17 -0
  57. package/dist/types-DQYmArgv.d.ts +17 -0
  58. package/package.json +115 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 frond-js contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,247 @@
1
+ # frond-js
2
+
3
+ > 中文版 → [README.md](./README.md)
4
+
5
+ A **zero-runtime-dependency** ebook reading engine: parses EPUB / MOBI(KF8) / FB2 / CBZ / PDF into a
6
+ single book model and paginates them in the browser. The `core` layer does not depend on the DOM, so
7
+ the same code runs in both Node and the browser.
8
+
9
+ ⭐ **It is an engine, not a reader app.** This library ships **no user interface** — no toolbar, no progress
10
+ bar, no swipe gestures, and it **does not take over the keyboard or mouse**: turn pages by calling
11
+ `reader.next()` / `reader.prev()` / `reader.goTo(i)`; which key that is bound to, whether gestures exist and
12
+ how focus moves are **the host's business**. That is what keeps hosts from inheriting UI they did not ask
13
+ for (see [docs/PRD.md](./docs/PRD.md) §1.2).
14
+
15
+ > ⚠️ This project is in the **`0.x`** stage and the API is not frozen. Versioning and SemVer
16
+ > commitments are documented in [docs/CHANGELOG.md](./docs/CHANGELOG.md) and
17
+ > [docs/API.md](./docs/API.md).
18
+
19
+ ## Supported formats
20
+
21
+ ⚠️ "Implemented" and "evidenced" are two different things. The table below deliberately keeps two
22
+ separate columns — **real-publication samples** and **cross-implementation / third-party checks**:
23
+
24
+ | Format | Extensions | Impl | Real-publication samples | Cross-implementation / third-party checks |
25
+ | ----------- | ------------------- | ---- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | EPUB | `.epub` | ✅ | ✅ **2** (z-library; ⚠️ both use **NCX** — the EPUB 3 **NAV** path has never seen a real book) | ✅ 10 EPUB 3 fixtures are produced by an **independent implementation** (the project's own writer is not used) and triple-checked with `unzip -t` / `7z l` / Python `zipfile`; CFI is compared character-by-character against [foliate-js](https://github.com/johnfactotum/foliate-js) |
27
+ | MOBI / AZW3 | `.mobi` / `.azw3` | ✅ | ✅ **6** (Project Gutenberg) + 1 (z-library) | ✅ Chapter-by-chapter comparison against PyPI `mobi` (a Python port of KindleUnpack) |
28
+ | FB2 | `.fb2` / `.fb2.zip` | ✅ | ✅ **2** + 1 (z-library) | ✅ Synthetic fixtures are produced by an independent Python script |
29
+ | CBZ | `.cbz` | ✅ | ✅ **1** (z-library comics) | ⚠️ Synthetic fixtures only, cross-checked with Python `zipfile` — **no cross-implementation oracle** |
30
+ | PDF | `.pdf` | ✅ | ✅ **1**(z-library · 87 MB · scanned + OCR) | ⚠️ Adapter over `pdfjs-dist`; fixture readings come from a **real PDF.js**; page-image timings still **do not extrapolate**. ⭐ **A real book exposed a limitation**: scanned pages (JPEG 2000 / JBIG2 images) can render as **entirely blank without any error** (see **FI-X37**) |
31
+
32
+ ⚠️ **Known gaps (not glossed over):**
33
+
34
+ - The 10 EPUB fixtures are called "real" inside this project, where "real" means "**a valid EPUB
35
+ produced by an independent implementation and verified by third-party tools**" — it does **not**
36
+ mean "downloaded from a publisher". ⚠️ As of 2026-09-19 **two actual published books have gone
37
+ through the EPUB path** (readings in
38
+ [`tests/fixtures/real-books/README.md`](./tests/fixtures/real-books/README.md)) — but both carry
39
+ their table of contents as **NCX**, so the **EPUB 3 NAV path still has no real-book evidence**
40
+ (FI-X21 is neither confirmed nor ruled out).
41
+ - MOBI's **HUFF/CDIC compression path** (`compression = 17480`) **still lacks a real-book sample**:
42
+ the implementation is done, but every committed real sample uses `compression = 2` (real
43
+ HUFF/CDIC files live in LGPL/GPL repositories and are, by convention, not committed here).
44
+ - The **per-chapter fingerprints of `kf8-nested.mobi` have not yet been compared** against an
45
+ independent implementation.
46
+ - **CBZ and PDF now each have one real published book** (2026-09-19 / 2026-09-22; readings in
47
+ [`tests/fixtures/real-books/README.md`](./tests/fixtures/real-books/README.md)); PDF page-image
48
+ timings still **do not extrapolate** (only one macOS Chromium has been measured).
49
+ - ⚠️ **Scanned PDFs need host-supplied wasm decoders**: when page content is JPEG 2000 / JBIG2, PDF.js
50
+ needs extra **wasm assets** to decode it. This library **neither bundles those bytes nor guesses where
51
+ they live** ⇒ **without that configuration such books render fully blank, with no error at all**
52
+ (`state` stays `ready`, paging works).
53
+ ✅ Since **W-3** there is a supported way to configure it: hand the directory URLs to `Reader`; they are
54
+ forwarded **verbatim** to PDF.js —
55
+
56
+ ```text
57
+ new Reader({
58
+ host,
59
+ // ⚠️ Each URL must be a string ending in `/`: without the trailing slash PDF.js throws,
60
+ // while a non-string is silently treated as "not provided".
61
+ pdf: { wasmUrl: '/vendor/pdfjs/wasm/' },
62
+ });
63
+ ```
64
+
65
+ (This is a **configuration fragment**, not one of the two copy-paste-runnable examples above — on
66
+ purpose: `tests/e2e/readme-examples.spec.ts` counts only `ts` fences and runs each of them.)
67
+
68
+ ⚠️ If a scanned book was opened **first** in the same page without the addresses, later books can still
69
+ come out blank ⇒ set the addresses **before opening any PDF** (the phenomenon is registered; its
70
+ mechanism is not). Registered as **FI-X37**; details and the real-book readings in
71
+ [`tests/fixtures/pdf/README.md`](./tests/fixtures/pdf/README.md), boundary §4.
72
+
73
+ ## Features
74
+
75
+ - **Compile-time layering**: four layers — `core` / `render` / `formats` / `reader` — with a
76
+ one-directional dependency graph enforced by ESLint `no-restricted-imports` and two tsconfigs.
77
+ `core` (including `formats`) must compile **without the `"dom"` lib** — that is the only way the
78
+ "one codebase, three targets" requirement can be verified automatically; trust alone always rots.
79
+ - **Zero runtime dependencies**: `core` has none; `pdfjs-dist` is an **optional peer**, needed only
80
+ if you use PDF.
81
+ - **Per-format code splitting**: format parsers are loaded on demand and **do not count against the
82
+ `core` size budget**.
83
+ - **Injectable IO**: implement `BookSource` (`size` / `read(range)` / `close()`) to plug in custom
84
+ storage (OPFS, remote ranges, in-memory caches, …). A reference implementation,
85
+ `createMemoryBookSource`, ships in the box.
86
+ - **Pluggable rendering**: implement `Renderer` to replace the whole rendering strategy
87
+ (paginated / scrolled / custom layout) without touching parsing or modeling.
88
+ - **Uniform cancellation**: every async API accepts `OperationOptions.signal`; aborting throws
89
+ `AbortError` and releases whatever was allocated.
90
+
91
+ ## Installation
92
+
93
+ ```bash
94
+ pnpm add frond-js
95
+ ```
96
+
97
+ Requires **Node ≥ 20**. For PDF support, also install the optional peer:
98
+
99
+ ```bash
100
+ pnpm add pdfjs-dist
101
+ ```
102
+
103
+ ## Runtime requirements
104
+
105
+ | Target | Requirement |
106
+ | --------------------------------------- | ---------------------------------------------------- |
107
+ | Browsers | Chrome / Edge ≥ 111, Firefox ≥ 115, Safari ≥ 16.4 |
108
+ | Node (headless parsing / modeling only) | ≥ 20 |
109
+ | **Development and testing** (this repo) | **Node ≥ 22.13** (`devDependencies` pins PDF.js 6.x) |
110
+
111
+ ⚠️ The table above lists **target runtimes**, **not** "verified on these versions": the matrix comes
112
+ from `docs/PROMPT.md` §4.1, and there is currently **no cross-browser version testing** —
113
+ `pnpm test:browser` runs on a single local Chromium.
114
+
115
+ ⚠️ Note the **Node version asymmetry**: consumers only need `≥ 20` (`package.json` `engines`), but
116
+ **developing and testing inside this repository** requires `≥ 22.13`.
117
+
118
+ Runtime feature dependencies: ES2022, `AbortSignal`, `CompressionStream` / `DecompressionStream`,
119
+ `DOMParser` (injected through an **optional adapter** on Node — the library does **not** hard-depend
120
+ on jsdom).
121
+
122
+ ## Quick start (browser)
123
+
124
+ ```ts
125
+ import { Reader } from 'frond-js';
126
+
127
+ const reader = new Reader({
128
+ host: { element: document.getElementById('viewer')!, ownerDocument: document },
129
+ });
130
+
131
+ const bytes = new Uint8Array(await (await fetch('/book.epub')).arrayBuffer());
132
+ await reader.open(bytes);
133
+ await reader.next();
134
+
135
+ console.log(reader.state); // 'ready'
136
+ ```
137
+
138
+ `open` accepts five input shapes: `ArrayBuffer` / `Uint8Array` / `Blob` / `string` (a URL) /
139
+ `BookSource`.
140
+
141
+ In **Node** there is no DOM, so only the `core`-side parsing entry point is usable:
142
+
143
+ ```ts
144
+ import { openBook } from 'frond-js/formats/epub';
145
+
146
+ const { book, toc } = await openBook(bytes, { source: 'book.epub' });
147
+ console.log(book.metadata.title, toc.length);
148
+ ```
149
+
150
+ > Neither snippet is invented:
151
+ > [tests/e2e/readme-examples.spec.ts](./tests/e2e/readme-examples.spec.ts) **extracts these two blocks
152
+ > verbatim and actually runs them** against the `dist/` build, on every `pnpm e2e` — break one token in an
153
+ > example and the corresponding test goes red. The host page
154
+ > [tests/e2e/fixtures/readme.html](./tests/e2e/fixtures/readme.html) only provides the DOM and an import
155
+ > map; it contains **no usage code of its own**.
156
+
157
+ ## Two constraints you must know about
158
+
159
+ **① The host element must have a definite height.** A host with `height: auto` collapses the
160
+ renderer's wrapper to zero (its children are all absolutely positioned), and pagination then
161
+ **silently** produces the wrong page count — no error, just wrong numbers.
162
+
163
+ ```css
164
+ #viewer {
165
+ width: 800px;
166
+ height: 600px;
167
+ }
168
+ ```
169
+
170
+ **② `toc` is only readable while `ready`.** Reading it in any other state throws `StateError` and
171
+ **does not return an empty array** — an empty array is indistinguishable from "this book genuinely
172
+ has no table of contents", so returning one would hand the caller false information.
173
+
174
+ ## Entry points
175
+
176
+ | Entry | Contents |
177
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
178
+ | `frond-js` | `Reader`, `READER_STATES` / `canTransition`, the error hierarchy, abort utilities, public types |
179
+ | `frond-js/core` | ZIP reading, XML parsing, archive-path resolution, the book model (incl. `TocItem`), CFI, injectable IO, errors, abort utilities |
180
+ | `frond-js/render` | `Renderer` / `RenderHost` / `RenderLocation` types (⚠️ currently **types only**) |
181
+ | `frond-js/formats/epub` | EPUB parser: `openBook` / `parseContainer` / `parseOpf` / `parseToc` / `EPUB_MEDIA_TYPE` (dynamic import) |
182
+
183
+ ⚠️ The parsers for the other formats (MOBI / FB2 / CBZ / PDF) are **not exported from any entry** —
184
+ `Reader` loads them automatically based on format detection. The full signature contract lives in
185
+ [docs/API.md](./docs/API.md) §1.
186
+
187
+ ## Development and verification
188
+
189
+ ```bash
190
+ pnpm install
191
+ pnpm verify # all ten gates must be green
192
+ ```
193
+
194
+ `pnpm verify` runs, in order:
195
+ `format:check → lint → typecheck → test → test:browser → build → size → release:check → api:check → docs:check → e2e`.
196
+
197
+ ⚠️ The English list above had drifted to **ten** rings and was missing `release:check` — corrected on 2026-09-19.
198
+
199
+ ⭐ **The first nine rings also run on Gitee Go for every push** (pipeline `frond-js-verify-L1`,
200
+ [build history](https://gitee.com/eltongu/frond-js/gitee_go/pipelines?codeRef=master&codeType=branch);
201
+ setup and pitfalls in [docs/CI.md](./docs/CI.md)). **Measured: 1 min 37 s.** The runtime is pinned by
202
+ `.nvmrc` and **installed + hard-asserted inside the job** — the runner default is not trusted (it ships
203
+ Node 16).
204
+
205
+ The two browser rings (`test:browser` / `e2e`) run in a second pipeline, `frond-js-verify-L2`,
206
+ **and are green** (`test:browser`: 1826 passed / 15.4 s; `e2e`: 4 passed / 29.9 s). ⚠️ It is **not**
207
+ auto-triggered — measured on this Gitee Go instance, **only a "match everything" filter works**
208
+ (`branches.prefix: ['']`); across six observations `precise`, non-empty `prefix` and `tags.*` all
209
+ produced **zero** builds, so fine-grained triggering is simply unavailable. The entry point is the
210
+ "Run" button on the pipeline page, plus an in-repo switch `.workflow/L2-ENABLE`
211
+ (present = run, absent = exit immediately).
212
+ ⚠️ **FI-X25 (handled via direction ③)**: that 2-CPU CI machine is ~4× slower than the dev machine,
213
+ and `e2e`'s "chapter switch p95 < 50 ms" measured **49.3 ms** on CI — only 1.4% of headroom, so it
214
+ would go red at random. ⇒ absolute millisecond budgets are **printed but not asserted** on CI
215
+ (`PERF_ABSOLUTE_BUDGET=0`, set on that one L2 line only); **local `pnpm verify` still asserts**.
216
+ ⚠️ The cost: CI no longer catches performance regressions — only the baseline device does.
217
+
218
+ The three test commands are **not interchangeable** (they run different things):
219
+
220
+ | Command | What it runs |
221
+ | ------------------- | --------------------------------------------------------------------------------- |
222
+ | `pnpm test` | Unit and integration tests over `src/`, in **Node** (the only source of coverage) |
223
+ | `pnpm test:browser` | **The same source**, executed in **Chromium** (verifies cross-target consistency) |
224
+ | `pnpm e2e` | The **`dist/` build output**, in a real browser (includes perf and size gates) |
225
+
226
+ ⚠️ If `pnpm install` fails during the **import phase** with `ERR_PNPM_CODEBUDDY_BROKER_DENY`, the
227
+ terminal has injected a Node filesystem-proxy shim; the workaround is in
228
+ [AGENTS.md](./AGENTS.md) §3.1.6.
229
+
230
+ ## Documentation
231
+
232
+ | Document | Contents |
233
+ | ---------------------------------------------- | ----------------------------------------------- |
234
+ | [docs/PRD.md](./docs/PRD.md) | Requirements and acceptance criteria |
235
+ | [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) | Layering, dependency direction, ADRs |
236
+ | [docs/API.md](./docs/API.md) | **The promised signature contract** (+ history) |
237
+ | [docs/CHANGELOG.md](./docs/CHANGELOG.md) | Changelog and version plan |
238
+ | [docs/MILESTONES.md](./docs/MILESTONES.md) | Milestones and hand-off cards |
239
+ | [docs/KNOWN-ISSUES.md](./docs/KNOWN-ISSUES.md) | Known-issue registry (including open decisions) |
240
+ | [docs/ATTRIBUTION.md](./docs/ATTRIBUTION.md) | Third-party sources and license registry |
241
+
242
+ ## License
243
+
244
+ [MIT](./LICENSE) · Copyright (c) 2026 frond-js contributors
245
+
246
+ Third-party sources and licenses — including the provenance and license status of the test fixtures
247
+ — are registered in [docs/ATTRIBUTION.md](./docs/ATTRIBUTION.md).
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # frond-js
2
+
3
+ > English version → [README.en.md](./README.en.md)
4
+
5
+ 一个**零运行时依赖**的电子书阅读引擎:把 EPUB / MOBI(KF8) / FB2 / CBZ / PDF 解析成统一的书模型,
6
+ 并在浏览器里分页渲染。`core` 侧不依赖 DOM,同一套代码可在 Node 与浏览器中运行。
7
+
8
+ ⭐ **它是引擎,不是阅读器。** 本库**不带任何界面** —— 没有工具栏、进度条、翻页手势,也**不接管键盘与鼠标**:
9
+ 翻页要调 `reader.next()` / `reader.prev()` / `reader.goTo(i)`,绑到哪个键、要不要做手势、焦点怎么走,
10
+ **都是宿主的事**。这样宿主才不会被迫接受一套它不想要的 UI(详见 [docs/PRD.md](./docs/PRD.md) §1.2)。
11
+
12
+ > ⚠️ 本项目处于 **`0.x`** 阶段,API 尚未冻结。版本号约定与 SemVer 承诺见
13
+ > [docs/CHANGELOG.md](./docs/CHANGELOG.md) 与 [docs/API.md](./docs/API.md)。
14
+
15
+ ## 支持格式
16
+
17
+ ⚠️ 「已实现」与「有证据」是两件事。下表刻意分开两列 —— **真实出版物样本** 与 **跨实现 / 第三方校验**:
18
+
19
+ | 格式 | 扩展名 | 实现 | 真实出版物样本 | 跨实现 / 第三方校验 |
20
+ | ----------- | ------------------- | ---- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
21
+ | EPUB | `.epub` | ✅ | ✅ **2 本**(z-library;⚠️ 目录均为 **NCX**,EPUB 3 **NAV 路径未跑过真书**) | ✅ 10 份 EPUB 3 fixture 由**独立实现**产出(不调用本项目写入器),经 `unzip -t` / `7z l` / Python `zipfile` 三重校验;CFI 与 [foliate-js](https://github.com/johnfactotum/foliate-js) 逐字符对拍 |
22
+ | MOBI / AZW3 | `.mobi` / `.azw3` | ✅ | ✅ **6 份**(Project Gutenberg)+ 1 本(z-library) | ✅ 与 PyPI `mobi`(KindleUnpack 的 Python 移植)逐章对拍 |
23
+ | FB2 | `.fb2` / `.fb2.zip` | ✅ | ✅ **2 份** + 1 本(z-library) | ✅ 合成件由独立 Python 脚本产出 |
24
+ | CBZ | `.cbz` | ✅ | ✅ **1 本**(z-library 漫画) | ⚠️ 仅合成件 + Python `zipfile` 交叉校验,**无跨实现对照** |
25
+ | PDF | `.pdf` | ✅ | ✅ **1**(z-library · 87 MB · 扫描 + OCR 件) | ⚠️ 适配 `pdfjs-dist`;夹具读数由**真 PDF.js** 跑出;页图渲染读数仍**不可外推**。⭐ **真书上暴露一条限制**:扫描版(JPEG 2000 / JBIG2 图像)在浏览器侧可能**整本出空白页而不报任何错**(见 **FI-X37**) |
26
+
27
+ ⚠️ **已知缺口(不粉饰)**:
28
+
29
+ - EPUB 的 10 份 fixture 在本项目里叫「真实」,其定义是「**由独立实现产出、经第三方工具校验的合法 EPUB**」。
30
+ ⚠️ 2026-09-19 起**已有 2 本真实出版物跑通 EPUB 链路**(读数见
31
+ [`tests/fixtures/real-books/README.md`](./tests/fixtures/real-books/README.md))—— 但两本的目录**都是 NCX**,
32
+ 所以 **EPUB 3 NAV 这条路径至今没有真书证据**(FI-X21 既未确认也未否掉)。
33
+ - MOBI 的 **HUFF/CDIC 压缩路径**(`compression = 17480`)**真书样本仍缺**:实现已完成,
34
+ 但入库的真实样本 `compression` 全是 `2`(真实 HUFF/CDIC 文件都在 LGPL/GPL 仓库,按约定不入库)。
35
+ - `kf8-nested.mobi` 的**逐章指纹尚未与独立实现对拍**。
36
+ - **CBZ 与 PDF 各已有 1 本真书样本**(2026-09-19 / 2026-09-22,读数见
37
+ [`tests/fixtures/real-books/README.md`](./tests/fixtures/real-books/README.md));但 PDF 的页图渲染读数
38
+ 仍**不可外推**到别的机器或别的文档(只在一台 macOS 的 Chromium 上跑过)。
39
+ - ⚠️ **扫描版 PDF 需要宿主提供 wasm 解码器**:页内容是 JPEG 2000 / JBIG2 图像时,PDF.js 要额外的
40
+ **wasm 资源**才解得动。本库**不打包这些字节、也不猜它们在哪** ⇒ **不给配置时这类书会整本渲染成空白,
41
+ 而不抛任何错误**(`state` 仍是 `ready`、翻页照常)。
42
+ ✅ 自 **W-3** 起有受支持的配法:把目录地址交给 `Reader`,**逐字**转发给 PDF.js ——
43
+
44
+ ```text
45
+ new Reader({
46
+ host,
47
+ // ⚠️ 每个地址必须是字符串且以 `/` 结尾:缺尾斜杠 PDF.js 会抛,非字符串则被静默当成「没给」。
48
+ pdf: { wasmUrl: '/vendor/pdfjs/wasm/' },
49
+ });
50
+ ```
51
+
52
+ (这是一段**配置片段**,不是上面那两段可复制运行的示例 —— 刻意的,`tests/e2e/readme-examples.spec.ts`
53
+ 只数 `ts` 围栏并逐段真跑。)
54
+
55
+ ⚠️ 同一页面里**先**打开过一本没给地址的扫描书,之后再给也仍可能是空白 ⇒ 地址要在打开任何 PDF **之前**配好
56
+ (这条现象已登记,机理未取证)。登记为 **FI-X37**;细则与真书读数见
57
+ [`tests/fixtures/pdf/README.md`](./tests/fixtures/pdf/README.md) 的 §四。
58
+
59
+ ## 特性
60
+
61
+ - **编译期分层**:`core` / `render` / `formats` / `reader` 四层,依赖方向单向,由 ESLint
62
+ `no-restricted-imports` 与两份 tsconfig 强制。`core`(含 `formats`)必须在**不含 `"dom"` lib**
63
+ 的条件下编译通过 —— 这是「三端一致」能被自动验证的唯一方式,靠自觉必然腐化。
64
+ - **零运行时依赖**:`core` 的运行时依赖为 0;`pdfjs-dist` 是**可选 peer**,只有用到 PDF 才需要装。
65
+ - **格式独立分包**:格式解析器按需动态加载,**不计入 `core` 的体积预算**。
66
+ - **可注入 IO**:实现 `BookSource`(`size` / `read(range)` / `close()`)即可接入自定义存储
67
+ (OPFS、远端分片、内存缓存……)。内置参考实现 `createMemoryBookSource`。
68
+ - **可插拔渲染**:实现 `Renderer` 可整体替换渲染策略(分页 / 滚动 / 自定义排版),而不触碰解析与建模。
69
+ - **统一的取消语义**:所有异步 API 接受 `OperationOptions.signal`;abort 后抛 `AbortError` 并释放已占资源。
70
+
71
+ ## 安装
72
+
73
+ ```bash
74
+ pnpm add frond-js
75
+ ```
76
+
77
+ 需要 **Node ≥ 20**。若要使用 PDF,再装可选 peer:
78
+
79
+ ```bash
80
+ pnpm add pdfjs-dist
81
+ ```
82
+
83
+ ## 运行时要求
84
+
85
+ | 目标 | 要求 |
86
+ | ------------------------------- | ----------------------------------------------------- |
87
+ | 浏览器 | Chrome / Edge ≥ 111、Firefox ≥ 115、Safari ≥ 16.4 |
88
+ | Node(仅 headless 解析 / 建模) | ≥ 20 |
89
+ | **开发与测试**(本仓库自身) | **Node ≥ 22.13**(`devDependencies` 钉了 PDF.js 6.x) |
90
+
91
+ ⚠️ 上表是**目标运行时**,**不是**「已在这些版本上验证过」:矩阵取自 `docs/PROMPT.md` §4.1,
92
+ 而目前**没有跨浏览器版本的实际测试** —— `pnpm test:browser` 只在本机的一份 Chromium 上跑。
93
+
94
+ ⚠️ 注意 **Node 版本的不对称**:消费者只需 `≥ 20`(`package.json` 的 `engines`),
95
+ 但**在本仓库里开发 / 测试**需要 `≥ 22.13`。
96
+
97
+ 运行时特性依赖:ES2022、`AbortSignal`、`CompressionStream` / `DecompressionStream`、`DOMParser`
98
+ (Node 侧通过**可选适配器注入**,本库**不硬依赖 jsdom**)。
99
+
100
+ ## 快速开始(浏览器)
101
+
102
+ ```ts
103
+ import { Reader } from 'frond-js';
104
+
105
+ const reader = new Reader({
106
+ host: { element: document.getElementById('viewer')!, ownerDocument: document },
107
+ });
108
+
109
+ const bytes = new Uint8Array(await (await fetch('/book.epub')).arrayBuffer());
110
+ await reader.open(bytes);
111
+ await reader.next();
112
+
113
+ console.log(reader.state); // 'ready'
114
+ ```
115
+
116
+ `open` 接受 `ArrayBuffer` / `Uint8Array` / `Blob` / `string`(URL)/ `BookSource` 五种输入形态。
117
+
118
+ 在 **Node** 里没有 DOM,只能用 `core` 侧的解析入口:
119
+
120
+ ```ts
121
+ import { openBook } from 'frond-js/formats/epub';
122
+
123
+ const { book, toc } = await openBook(bytes, { source: 'book.epub' });
124
+ console.log(book.metadata.title, toc.length);
125
+ ```
126
+
127
+ > 上面两段不是凭空写的:[tests/e2e/readme-examples.spec.ts](./tests/e2e/readme-examples.spec.ts)
128
+ > **把这两段原文抽出来真跑一遍**(被测对象是 `dist/` 构建产物,随 `pnpm e2e` 每次执行)
129
+ > ⇒ 示例改坏一个 token,对应那条用例就会红。宿主页面见
130
+ > [tests/e2e/fixtures/readme.html](./tests/e2e/fixtures/readme.html),它只提供 DOM 与 import map、**不含任何用法代码**。
131
+
132
+ ## 两个必须知道的约束
133
+
134
+ **① 宿主必须有确定高度。** 宿主元素的 `height: auto` 会让渲染层的 wrapper 塌成 0
135
+ (子元素全绝对定位),分页会**静默**得到错误页数 —— 不报错,只是页数是错的。
136
+
137
+ ```css
138
+ #viewer {
139
+ width: 800px;
140
+ height: 600px;
141
+ }
142
+ ```
143
+
144
+ **② `toc` 只在 `ready` 下可读。** 非 `ready` 时抛 `StateError`,**不返回空数组** ——
145
+ 空数组与「这本书确实没有目录」在类型上无法区分,返回它就是把假信息交给调用方。
146
+
147
+ ## 入口一览
148
+
149
+ | 入口 | 内容 |
150
+ | ----------------------- | ------------------------------------------------------------------------------------------------------- |
151
+ | `frond-js` | `Reader`、`READER_STATES` / `canTransition`、错误体系、abort 工具、公共类型 |
152
+ | `frond-js/core` | ZIP 读取、XML 解析、归档路径解析、书模型(含 `TocItem`)、CFI、可注入 IO、错误体系、abort 工具 |
153
+ | `frond-js/render` | `Renderer` / `RenderHost` / `RenderLocation` 类型(⚠️ 当前**只导出类型**) |
154
+ | `frond-js/formats/epub` | EPUB 解析器:`openBook` / `parseContainer` / `parseOpf` / `parseToc` / `EPUB_MEDIA_TYPE`(动态 import) |
155
+
156
+ ⚠️ 其余格式(MOBI / FB2 / CBZ / PDF)的解析器**不从任何入口导出** —— 它们由 `Reader` 在打开时
157
+ 按探测结果自动加载。完整签名契约见 [docs/API.md](./docs/API.md) §1。
158
+
159
+ ## 开发与验证
160
+
161
+ ```bash
162
+ pnpm install
163
+ pnpm verify # 十一环全绿才算过
164
+ ```
165
+
166
+ `pnpm verify` 依次跑:
167
+ `format:check → lint → typecheck → test → test:browser → build → size → release:check → api:check → docs:check → e2e`。
168
+
169
+ ⭐ **前九环由 Gitee Go 在每次 push 时代跑**(流水线 `frond-js-verify-L1`,
170
+ [构建历史](https://gitee.com/eltongu/frond-js/gitee_go/pipelines?codeRef=master&codeType=branch);
171
+ 开通与踩坑见 [docs/CI.md](./docs/CI.md))。**实测 1 分 37 秒**。运行时由 `.nvmrc` 钉住并在 CI 里
172
+ **自装 + 硬断言** —— 不信运行器给的默认值(它给的是 Node 16)。
173
+
174
+ 两个浏览器环(`test:browser` / `e2e`)由第二条流水线 `frond-js-verify-L2` 跑,**已全绿**
175
+ (`test:browser` 1826 passed / 15.4 秒;`e2e` 4 passed / 29.9 秒)。⚠️ 它**不是**自动触发 ——
176
+ 实测这个 Gitee Go 实例上**只有「匹配一切」的过滤规则生效**(`branches.prefix: ['']`),
177
+ `precise`、非空 `prefix`、以及 `tags.*` 合计六次观测**全部零构建** ⇒ 精细触发拿不到。
178
+ 入口固定为流水线页的「运行」按钮,外加一道仓内开关 `.workflow/L2-ENABLE`(在才跑,不在就秒退)。
179
+ ⚠️ **FI-X25(已按方向③处置)**:CI 那台 2 CPU 的机器比本机慢约 4 倍,
180
+ `e2e` 的「切章 p95 < 50 ms」在 CI 上实测 **49.3 ms** —— 只剩 1.4% 余量,会随机红。
181
+ ⇒ 绝对毫秒预算在 CI 上**只打印、不判红**(`PERF_ABSOLUTE_BUDGET=0`,只有 L2 那一行设);
182
+ **本地 `pnpm verify` 照旧断言**。⚠️ 代价:CI 不再拦性能退化,只在基准设备上拦。
183
+
184
+ 三条测试命令**不可互换**(它们跑的东西不同):
185
+
186
+ | 命令 | 跑什么 |
187
+ | ------------------- | ---------------------------------------------------------- |
188
+ | `pnpm test` | `src/` 的单元与集成测试,**Node** 环境(唯一的覆盖率来源) |
189
+ | `pnpm test:browser` | **同一套源码**,在 **Chromium** 里跑(验证跨端一致性) |
190
+ | `pnpm e2e` | **`dist/` 构建产物**,在真实浏览器里跑(含性能与体积门禁) |
191
+
192
+ ⚠️ 若 `pnpm install` 在**导入阶段**报 `ERR_PNPM_CODEBUDDY_BROKER_DENY`,那是终端注入了 Node
193
+ 文件系统代理 shim,处方见 [AGENTS.md](./AGENTS.md) §3.1.6。
194
+
195
+ ## 文档
196
+
197
+ | 文档 | 内容 |
198
+ | ---------------------------------------------- | ------------------------------------ |
199
+ | [docs/PRD.md](./docs/PRD.md) | 需求与验收标准 |
200
+ | [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) | 分层、依赖方向、关键设计决策(ADR) |
201
+ | [docs/API.md](./docs/API.md) | **对外承诺的签名契约**(含变更记录) |
202
+ | [docs/CHANGELOG.md](./docs/CHANGELOG.md) | 变更日志与版本规划 |
203
+ | [docs/MILESTONES.md](./docs/MILESTONES.md) | 阶段里程碑与交接卡 |
204
+ | [docs/KNOWN-ISSUES.md](./docs/KNOWN-ISSUES.md) | 已知问题登记(含未拍板项) |
205
+ | [docs/ATTRIBUTION.md](./docs/ATTRIBUTION.md) | 第三方来源与许可登记 |
206
+
207
+ ## 许可
208
+
209
+ [MIT](./LICENSE) · Copyright (c) 2026 frond-js contributors
210
+
211
+ 第三方来源与许可登记见 [docs/ATTRIBUTION.md](./docs/ATTRIBUTION.md)(含测试夹具的来源与许可状态)。
@@ -0,0 +1,99 @@
1
+ /**
2
+ * frond-js 统一错误体系。
3
+ *
4
+ * 设计约束(ARCHITECTURE §6 / REQ-CORE-003):
5
+ * - 禁止抛出裸 `Error` 或字符串
6
+ * - 每个错误携带稳定的 `code`,下游可据此分支,不依赖 message 文案
7
+ * - `cause` 保留原始错误,不丢失调用栈
8
+ */
9
+ /** 稳定错误码。属于公共契约,变更需走 SemVer。 */
10
+ type FrondErrorCode = 'ERR_PARSE' | 'ERR_FORMAT' | 'ERR_NETWORK' | 'ERR_SECURITY' | 'ERR_ABORT' | 'ERR_STATE';
11
+ interface FrondErrorOptions {
12
+ /** 原始错误,用于保留调用栈。 */
13
+ readonly cause?: unknown;
14
+ }
15
+ /**
16
+ * 所有 frond-js 错误的基类。
17
+ *
18
+ * 不要直接实例化本类;请使用具体的子类。
19
+ */
20
+ declare class FrondError extends Error {
21
+ /** 稳定错误码,用于程序化分支。 */
22
+ readonly code: FrondErrorCode;
23
+ constructor(code: FrondErrorCode, message: string, options?: FrondErrorOptions);
24
+ }
25
+ /** 解析失败:ZIP 损坏、XML 非法、结构缺失。 */
26
+ declare class ParseError extends FrondError {
27
+ constructor(message: string, options?: FrondErrorOptions);
28
+ }
29
+ /** 格式不支持或不符合规范。 */
30
+ declare class FormatError extends FrondError {
31
+ constructor(message: string, options?: FrondErrorOptions);
32
+ }
33
+ /** 资源加载失败。 */
34
+ declare class NetworkError extends FrondError {
35
+ constructor(message: string, options?: FrondErrorOptions);
36
+ }
37
+ /** 触发安全策略(例如 CSP 拦截、脚本执行被拒)。 */
38
+ declare class SecurityError extends FrondError {
39
+ constructor(message: string, options?: FrondErrorOptions);
40
+ }
41
+ /**
42
+ * 操作被取消。
43
+ *
44
+ * `name` 恒为 `'AbortError'`,与 `DOMException` 的语义对齐(H7)。
45
+ */
46
+ declare class AbortError extends FrondError {
47
+ constructor(message?: string, options?: FrondErrorOptions);
48
+ }
49
+ /** 非法状态迁移(H5:禁止隐式标志位)。 */
50
+ declare class StateError extends FrondError {
51
+ constructor(message: string, options?: FrondErrorOptions);
52
+ }
53
+ /** 判断任意值是否为 frond-js 错误。 */
54
+ declare function isFrondError(value: unknown): value is FrondError;
55
+ /**
56
+ * 判断任意值是否为取消错误。
57
+ *
58
+ * 同时识别 frond-js 的 {@link AbortError} 与宿主环境的 `DOMException`
59
+ * (`name === 'AbortError'`),因为 `AbortSignal.reason` 可能来自外部。
60
+ */
61
+ declare function isAbortError(value: unknown): boolean;
62
+
63
+ /**
64
+ * 取消与资源清理工具(H6 / H7)。
65
+ *
66
+ * 约定:
67
+ * - 所有异步 API 接受 `AbortSignal`
68
+ * - abort 后抛出 {@link AbortError},**不得吞掉**
69
+ * - abort 必须释放资源(`revokeObjectURL` / `worker.terminate()` / iframe 清理 / 事件解绑)
70
+ */
71
+ /** 创建一个携带原始原因的 {@link AbortError}。 */
72
+ declare function createAbortError(reason?: unknown): AbortError;
73
+ /**
74
+ * 若 signal 已 abort,立即抛出 {@link AbortError}。
75
+ *
76
+ * 用作异步流程中的检查点,避免在已取消的操作上继续分配资源。
77
+ */
78
+ declare function throwIfAborted(signal?: AbortSignal): void;
79
+ /**
80
+ * 合并多个 signal:任一 abort 即整体 abort。
81
+ *
82
+ * 全部为 `undefined` 时返回 `undefined`,便于直接透传给下游。
83
+ */
84
+ declare function linkSignals(...signals: (AbortSignal | undefined)[]): AbortSignal | undefined;
85
+ /**
86
+ * 注册 abort 回调,返回解绑函数。
87
+ *
88
+ * 若 signal 已 abort,回调会被**同步立即执行**。
89
+ * 解绑函数幂等,可在资源清理路径中无条件调用。
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const detach = onAbort(signal, () => URL.revokeObjectURL(url));
94
+ * // ... 正常结束时同样要调用 detach(),避免回调悬挂
95
+ * ```
96
+ */
97
+ declare function onAbort(signal: AbortSignal | undefined, handler: () => void): () => void;
98
+
99
+ export { AbortError as A, FormatError as F, NetworkError as N, ParseError as P, SecurityError as S, FrondError as a, type FrondErrorCode as b, type FrondErrorOptions as c, StateError as d, createAbortError as e, isFrondError as f, isAbortError as i, linkSignals as l, onAbort as o, throwIfAborted as t };