@storyteller-platform/epub 0.4.0 → 0.4.2

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.
package/dist/index.js CHANGED
@@ -1,18 +1,87 @@
1
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
2
+ var __typeError = (msg) => {
3
+ throw TypeError(msg);
4
+ };
5
+ var __using = (stack, value, async) => {
6
+ if (value != null) {
7
+ if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
8
+ var dispose, inner;
9
+ if (async) dispose = value[__knownSymbol("asyncDispose")];
10
+ if (dispose === void 0) {
11
+ dispose = value[__knownSymbol("dispose")];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") __typeError("Object not disposable");
15
+ if (inner) dispose = function() {
16
+ try {
17
+ inner.call(this);
18
+ } catch (e) {
19
+ return Promise.reject(e);
20
+ }
21
+ };
22
+ stack.push([async, dispose, value]);
23
+ } else if (async) {
24
+ stack.push([async]);
25
+ }
26
+ return value;
27
+ };
28
+ var __callDispose = (stack, error, hasError) => {
29
+ var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
30
+ return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
31
+ };
32
+ var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
33
+ var next = (it) => {
34
+ while (it = stack.pop()) {
35
+ try {
36
+ var result = it[1] && it[1].call(it[2]);
37
+ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
38
+ } catch (e) {
39
+ fail(e);
40
+ }
41
+ }
42
+ if (hasError) throw error;
43
+ };
44
+ return next();
45
+ };
1
46
  import { randomUUID } from "node:crypto";
2
- import { rmSync } from "node:fs";
3
- import { cp, writeFile } from "node:fs/promises";
47
+ import { createWriteStream, rmSync } from "node:fs";
48
+ import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
49
  import { tmpdir } from "node:os";
5
- import { PortablePath, npath, ppath } from "@yarnpkg/fslib";
6
- import { DEFAULT_COMPRESSION_LEVEL, ZipFS } from "@yarnpkg/libzip";
50
+ import * as util from "node:util";
7
51
  import { Mutex } from "async-mutex";
8
52
  import { XMLBuilder, XMLParser } from "fast-xml-parser";
9
53
  import memoize from "mem";
10
54
  import { lookup } from "mime-types";
11
55
  import { nanoid } from "nanoid";
56
+ import yauzl from "yauzl";
57
+ import { ZipFile } from "yazl";
58
+ import { dirname, join, resolve } from "@storyteller-platform/path";
59
+ function promisify(api) {
60
+ return function(arg, options) {
61
+ return new Promise(function(resolve2, reject) {
62
+ api(arg, options, function(err, response) {
63
+ if (err) {
64
+ reject(err);
65
+ return;
66
+ }
67
+ resolve2(response);
68
+ });
69
+ });
70
+ };
71
+ }
72
+ const unzipFromBuffer = promisify(
73
+ (arg, options, callback) => {
74
+ yauzl.fromBuffer(arg, options, callback);
75
+ }
76
+ );
77
+ const unzipFromPath = promisify(
78
+ (arg, options, callback) => {
79
+ yauzl.open(arg, options, callback);
80
+ }
81
+ );
12
82
  class Epub {
13
- constructor(zipFs, zipPath, inputPath) {
14
- this.zipFs = zipFs;
15
- this.zipPath = zipPath;
83
+ constructor(extractPath, inputPath) {
84
+ this.extractPath = extractPath;
16
85
  this.inputPath = inputPath;
17
86
  this.readXhtmlItemContents = memoize(
18
87
  this.readXhtmlItemContents.bind(this),
@@ -189,11 +258,10 @@ ${JSON.stringify(element, null, 2)}`
189
258
  creators,
190
259
  contributors
191
260
  }, additionalMetadata = []) {
192
- const tmp = npath.join(
261
+ const extractPath = join(
193
262
  tmpdir(),
194
- `storyteller-platform-epub-${randomUUID()}.epub`
263
+ `storyteller-platform-epub-${randomUUID()}`
195
264
  );
196
- const zipFs = new ZipFS(npath.toPortablePath(tmp), { create: true });
197
265
  const encoder = new TextEncoder();
198
266
  const container = encoder.encode(`<?xml version="1.0"?>
199
267
  <container>
@@ -202,11 +270,8 @@ ${JSON.stringify(element, null, 2)}`
202
270
  </rootfiles>
203
271
  </container>
204
272
  `);
205
- await zipFs.mkdirpPromise(ppath.join(PortablePath.root, "META-INF"));
206
- await zipFs.writeFilePromise(
207
- ppath.join(PortablePath.root, "META-INF", "container.xml"),
208
- container
209
- );
273
+ await mkdir(join(extractPath, "META-INF"), { recursive: true });
274
+ await writeFile(join(extractPath, "META-INF", "container.xml"), container);
210
275
  const packageDocument = encoder.encode(`<?xml version="1.0"?>
211
276
  <package unique-identifier="pub-id" dir="${language.textInfo.direction}" xml:lang=${language.toString()} version="3.0">
212
277
  <metadata>
@@ -217,12 +282,9 @@ ${JSON.stringify(element, null, 2)}`
217
282
  </spine>
218
283
  </package>
219
284
  `);
220
- await zipFs.mkdirpPromise(ppath.join(PortablePath.root, "OEBPS"));
221
- await zipFs.writeFilePromise(
222
- ppath.join(PortablePath.root, "OEBPS", "content.opf"),
223
- packageDocument
224
- );
225
- const epub = new this(zipFs, tmp, path);
285
+ await mkdir(join(extractPath, "OEBPS"));
286
+ await writeFile(join(extractPath, "OEBPS", "content.opf"), packageDocument);
287
+ const epub = new this(extractPath, path);
226
288
  const metadata = [
227
289
  {
228
290
  id: "pub-id",
@@ -260,35 +322,66 @@ ${JSON.stringify(element, null, 2)}`
260
322
  */
261
323
  // eslint-disable-next-line @typescript-eslint/require-await
262
324
  static async from(pathOrData) {
263
- const tmp = npath.join(
264
- tmpdir(),
265
- `storyteller-platform-epub-${randomUUID()}.epub`
266
- );
267
- if (typeof pathOrData !== "string") {
268
- await writeFile(tmp, pathOrData);
269
- } else {
270
- await cp(pathOrData, tmp);
325
+ var _stack = [];
326
+ try {
327
+ const extractPath = join(
328
+ tmpdir(),
329
+ `storyteller-platform-epub-${randomUUID()}.epub`
330
+ );
331
+ const zipfile = typeof pathOrData === "string" ? await unzipFromPath(pathOrData, { lazyEntries: true }) : await unzipFromBuffer(Buffer.from(pathOrData), { lazyEntries: true });
332
+ const stack = __using(_stack, new DisposableStack());
333
+ stack.defer(() => {
334
+ zipfile.close();
335
+ });
336
+ const { promise, resolve: resolve2 } = Promise.withResolvers();
337
+ zipfile.on("end", () => {
338
+ resolve2();
339
+ });
340
+ const openReadStream = util.promisify(zipfile.openReadStream.bind(zipfile));
341
+ zipfile.readEntry();
342
+ zipfile.on("entry", async (entry) => {
343
+ if (entry.fileName.endsWith("/")) {
344
+ zipfile.readEntry();
345
+ } else {
346
+ const writePath = join(extractPath, entry.fileName);
347
+ const readStream = await openReadStream(entry);
348
+ await mkdir(dirname(writePath), { recursive: true });
349
+ await new Promise((resolvePipe) => {
350
+ const writePath2 = join(extractPath, entry.fileName);
351
+ const writeStream = createWriteStream(writePath2);
352
+ writeStream.on("finish", () => {
353
+ resolvePipe();
354
+ });
355
+ readStream.pipe(writeStream);
356
+ }).finally(() => {
357
+ zipfile.readEntry();
358
+ });
359
+ }
360
+ });
361
+ await promise;
362
+ return new this(
363
+ extractPath,
364
+ typeof pathOrData === "string" ? pathOrData : void 0
365
+ );
366
+ } catch (_) {
367
+ var _error = _, _hasError = true;
368
+ } finally {
369
+ __callDispose(_stack, _error, _hasError);
271
370
  }
272
- const zipFs = new ZipFS(npath.toPortablePath(tmp));
273
- return new this(
274
- zipFs,
275
- tmp,
276
- typeof pathOrData === "string" ? pathOrData : void 0
277
- );
278
371
  }
279
372
  async removeEntry(href) {
280
373
  const rootfile = await this.getRootfile();
281
374
  const filename = this.resolveHref(rootfile, href);
282
- await this.zipFs.removePromise(filename);
375
+ await rm(filename);
283
376
  }
284
377
  async getFileData(path, encoding) {
285
- return await this.zipFs.readFilePromise(path, encoding);
378
+ return await readFile(path, encoding);
286
379
  }
287
380
  async getRootfile() {
288
381
  var _a;
289
382
  if (this.rootfile !== null) return this.rootfile;
290
383
  const containerString = await this.getFileData(
291
- ppath.join(PortablePath.root, "META-INF", "container.xml"),
384
+ join(this.extractPath, "META-INF", "container.xml"),
292
385
  "utf-8"
293
386
  );
294
387
  if (!containerString)
@@ -320,7 +413,7 @@ ${JSON.stringify(element, null, 2)}`
320
413
  throw new Error(
321
414
  "Failed to parse EPUB container.xml: Found no rootfile element"
322
415
  );
323
- this.rootfile = ppath.resolve(PortablePath.root, fullPath);
416
+ this.rootfile = resolve(this.extractPath, fullPath);
324
417
  return this.rootfile;
325
418
  }
326
419
  migratePackageDocument(packageDocument) {
@@ -803,7 +896,11 @@ ${JSON.stringify(element, null, 2)}`
803
896
  if (!primaryLanguage) return null;
804
897
  const locale = primaryLanguage.value;
805
898
  if (!locale || locale.toLowerCase() === "und") return null;
806
- return new Intl.Locale(locale);
899
+ try {
900
+ return new Intl.Locale(locale);
901
+ } catch {
902
+ return null;
903
+ }
807
904
  }
808
905
  /**
809
906
  * Update the Epub's language metadata entry.
@@ -1443,8 +1540,8 @@ ${JSON.stringify(element, null, 2)}`
1443
1540
  * Returns a Zip Entry path for an HREF
1444
1541
  */
1445
1542
  resolveHref(from, href) {
1446
- const startPath = ppath.dirname(from);
1447
- return ppath.resolve(PortablePath.root, startPath, href);
1543
+ const startPath = dirname(from);
1544
+ return resolve(this.extractPath, startPath, href);
1448
1545
  }
1449
1546
  async readItemContents(id, encoding) {
1450
1547
  const rootfile = await this.getRootfile();
@@ -1490,8 +1587,8 @@ ${JSON.stringify(element, null, 2)}`
1490
1587
  return Epub.getXhtmlTextContent(body);
1491
1588
  }
1492
1589
  async writeEntryContents(path, contents, encoding) {
1493
- await this.zipFs.mkdirpPromise(ppath.dirname(path));
1494
- await this.zipFs.writeFilePromise(path, contents, encoding);
1590
+ await mkdir(dirname(path), { recursive: true });
1591
+ await writeFile(path, contents, encoding);
1495
1592
  }
1496
1593
  async writeItemContents(id, contents, encoding) {
1497
1594
  const rootfile = await this.getRootfile();
@@ -1581,8 +1678,8 @@ ${JSON.stringify(element, null, 2)}`
1581
1678
  contents
1582
1679
  )
1583
1680
  ) : contents;
1584
- await this.zipFs.mkdirpPromise(ppath.dirname(filename));
1585
- await this.zipFs.writeFilePromise(filename, data);
1681
+ await mkdir(dirname(filename), { recursive: true });
1682
+ await writeFile(filename, data);
1586
1683
  }
1587
1684
  /**
1588
1685
  * Update the manifest entry for an existing item.
@@ -1733,11 +1830,10 @@ ${JSON.stringify(element, null, 2)}`
1733
1830
  });
1734
1831
  }
1735
1832
  discardAndClose() {
1736
- this.zipFs.discardAndClose();
1737
1833
  this.rootfile = null;
1738
1834
  this.manifest = null;
1739
1835
  this.spine = null;
1740
- rmSync(this.zipPath, { recursive: true, force: true });
1836
+ rmSync(this.extractPath, { recursive: true, force: true });
1741
1837
  }
1742
1838
  /**
1743
1839
  * Write the current contents of the Epub to a new
@@ -1748,32 +1844,62 @@ ${JSON.stringify(element, null, 2)}`
1748
1844
  * timestamp.
1749
1845
  */
1750
1846
  async saveAndClose() {
1751
- if (!this.inputPath) {
1752
- throw new Error("In-memory EPUB files cannot be saved to disk");
1753
- }
1754
- await this.replaceMetadata(
1755
- (entry) => entry.properties["property"] === "dcterms:modified",
1756
- {
1757
- type: "meta",
1758
- properties: { property: "dcterms:modified" },
1759
- // We need UTC with integer seconds, but toISOString gives UTC with ms
1760
- value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
1847
+ var _stack = [];
1848
+ try {
1849
+ if (!this.inputPath) {
1850
+ throw new Error("In-memory EPUB files cannot be saved to disk");
1761
1851
  }
1762
- );
1763
- this.zipFs.level = 0;
1764
- await this.zipFs.writeFilePromise(
1765
- ppath.join(PortablePath.root, "mimetype"),
1766
- "application/epub+zip",
1767
- "utf-8"
1768
- );
1769
- this.zipFs.level = DEFAULT_COMPRESSION_LEVEL;
1770
- this.zipFs.saveAndClose();
1771
- await cp(this.zipPath, this.inputPath, { force: true });
1852
+ await this.replaceMetadata(
1853
+ (entry) => entry.properties["property"] === "dcterms:modified",
1854
+ {
1855
+ type: "meta",
1856
+ properties: { property: "dcterms:modified" },
1857
+ // We need UTC with integer seconds, but toISOString gives UTC with ms
1858
+ value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
1859
+ }
1860
+ );
1861
+ const tmpArchivePath = join(
1862
+ tmpdir(),
1863
+ `storyteller-platform-epub-${randomUUID()}`
1864
+ );
1865
+ const { promise, resolve: resolve2 } = Promise.withResolvers();
1866
+ const zipfile = new ZipFile();
1867
+ const writeStream = createWriteStream(tmpArchivePath);
1868
+ writeStream.on("close", () => {
1869
+ resolve2();
1870
+ });
1871
+ const stack = __using(_stack, new AsyncDisposableStack(), true);
1872
+ stack.defer(async () => {
1873
+ writeStream.close();
1874
+ await rm(tmpArchivePath, { force: true });
1875
+ });
1876
+ zipfile.outputStream.pipe(writeStream);
1877
+ zipfile.addBuffer(Buffer.from("application/epub+zip"), "mimetype", {
1878
+ compress: false
1879
+ });
1880
+ const entries = await readdir(this.extractPath, {
1881
+ recursive: true,
1882
+ withFileTypes: true
1883
+ });
1884
+ for (const entry of entries) {
1885
+ if (entry.name === "mimetype" || entry.isDirectory()) continue;
1886
+ zipfile.addFile(
1887
+ join(entry.parentPath, entry.name),
1888
+ join(entry.parentPath, entry.name).replace(`${this.extractPath}/`, "")
1889
+ );
1890
+ }
1891
+ zipfile.end();
1892
+ await promise;
1893
+ await cp(tmpArchivePath, this.inputPath);
1894
+ } catch (_) {
1895
+ var _error = _, _hasError = true;
1896
+ } finally {
1897
+ var _promise = __callDispose(_stack, _error, _hasError);
1898
+ _promise && await _promise;
1899
+ }
1772
1900
  }
1773
1901
  [Symbol.dispose]() {
1774
- if (this.zipFs.ready) {
1775
- this.discardAndClose();
1776
- }
1902
+ this.discardAndClose();
1777
1903
  }
1778
1904
  }
1779
1905
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyteller-platform/epub",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -32,6 +32,7 @@
32
32
  "@tsconfig/strictest": "^2.0.5",
33
33
  "@types/mime-types": "^2",
34
34
  "@types/node": "^24.0.0",
35
+ "@types/yazl": "^3",
35
36
  "markdown-toc": "^1.2.0",
36
37
  "remark-toc": "^9.0.0",
37
38
  "tsup": "^8.5.0",
@@ -44,14 +45,15 @@
44
45
  "dependencies": {
45
46
  "@storyteller-platform/fs": "^0.1.3",
46
47
  "@storyteller-platform/path": "^0.1.1",
47
- "@yarnpkg/fslib": "^3.1.3",
48
- "@yarnpkg/libzip": "^3.2.2",
48
+ "@types/yauzl": "^2.10.3",
49
49
  "@zip.js/zip.js": "^2.0.0",
50
50
  "async-mutex": "^0.5.0",
51
51
  "fast-xml-parser": "^4.0.0",
52
52
  "mem": "^8.0.0",
53
53
  "mime-types": "^3.0.1",
54
- "nanoid": "^5.1.5"
54
+ "nanoid": "^5.1.5",
55
+ "yauzl": "^3.2.0",
56
+ "yazl": "^3.3.1"
55
57
  },
56
58
  "publishConfig": {
57
59
  "access": "public",