vde-open 0.1.4 → 0.1.6

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.
@@ -1,21 +1,23 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DC62tzP2.js";
2
- import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-__laOomT.js";
3
- import { a as analyzeDocument, c as classifyReference, d as hasHiddenSegment, f as isValidLogicalPath, i as ParseLimitError, l as dirnameOfLogicalPath, n as scanReferences, o as assetTypeOf, p as roleAllowed, r as extractTitle, s as classifyLink, u as encodeLogicalPath } from "./render-BNZz0CjQ.js";
2
+ import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-B7DqaWh6.js";
3
+ import { C as roleAllowed, _ as dirnameOfLogicalPath, a as ParseLimitError, g as classifyReference, h as classifyLink, m as assetTypeOf, o as analyzeDocument, v as encodeLogicalPath, x as isValidLogicalPath, y as hasHiddenSegment } from "./rendering-rules-DiE2jI7I.js";
4
+ import { n as scanReferences, r as extractTitle } from "./render-BJnuEfHt.js";
4
5
  import { n as measureHeap, t as collectGarbage } from "./heap-C3qHm7SE.js";
5
- import { constants, existsSync, stat, unwatchFile, watch, watchFile } from "node:fs";
6
- import { appendFile, chmod, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, rmdir, stat as stat$1, writeFile } from "node:fs/promises";
6
+ import { accessSync, constants, existsSync, stat, statSync, unwatchFile, watch, watchFile } from "node:fs";
7
+ import { appendFile, chmod, link, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rename, rm, rmdir, stat as stat$1, writeFile } from "node:fs/promises";
7
8
  import * as sp from "node:path";
8
- import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
9
+ import { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
9
10
  import { EventEmitter } from "node:events";
10
- import { execFile } from "node:child_process";
11
+ import { execFile, spawn } from "node:child_process";
11
12
  import { promisify } from "node:util";
12
13
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
13
14
  import * as nativeFs from "fs";
14
- import { readdir as readdir$1, readdirSync, realpath as realpath$1, realpathSync, stat as stat$2, statSync } from "fs";
15
+ import { readdir as readdir$1, readdirSync, realpath as realpath$1, realpathSync, stat as stat$2, statSync as statSync$1 } from "fs";
15
16
  import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, normalize, posix as posix$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
16
17
  import { fileURLToPath } from "url";
17
18
  import { createRequire } from "module";
18
- import { fileURLToPath as fileURLToPath$1 } from "node:url";
19
+ import { tmpdir, type } from "node:os";
20
+ import { fileURLToPath as fileURLToPath$1, pathToFileURL } from "node:url";
19
21
  import { Worker } from "node:worker_threads";
20
22
  import { performance } from "node:perf_hooks";
21
23
  import { getHeapStatistics } from "node:v8";
@@ -23,7 +25,6 @@ import { STATUS_CODES, ServerResponse, createServer } from "node:http";
23
25
  import { Http2ServerRequest, constants as constants$1 } from "node:http2";
24
26
  import { Readable } from "node:stream";
25
27
  import { createServer as createServer$1 } from "node:net";
26
- import { type } from "node:os";
27
28
  //#region ../../packages/shared/src/bridge.ts
28
29
  const BRIDGE_HELLO = "vde-bridge-hello";
29
30
  const BRIDGE_PORT = "vde-bridge-port";
@@ -154,7 +155,7 @@ function parseStrictJson(text) {
154
155
  }
155
156
  //#endregion
156
157
  //#region package.json
157
- var version = "0.1.4";
158
+ var version = "0.1.6";
158
159
  //#endregion
159
160
  //#region src/documents/cursor.ts
160
161
  function invalidCursor(reason) {
@@ -3354,7 +3355,7 @@ function getOptions(options) {
3354
3355
  realpath: opts.fs.realpath || realpath$1,
3355
3356
  realpathSync: opts.fs.realpathSync || realpathSync,
3356
3357
  stat: opts.fs.stat || stat$2,
3357
- statSync: opts.fs.statSync || statSync
3358
+ statSync: opts.fs.statSync || statSync$1
3358
3359
  });
3359
3360
  if (opts.debug) log("globbing with options:", opts);
3360
3361
  return opts;
@@ -5445,6 +5446,454 @@ var DocumentService = class {
5445
5446
  }
5446
5447
  };
5447
5448
  //#endregion
5449
+ //#region src/export/browser.ts
5450
+ function executableFile(path) {
5451
+ try {
5452
+ accessSync(path, constants.X_OK);
5453
+ return statSync(path).isFile();
5454
+ } catch {
5455
+ return false;
5456
+ }
5457
+ }
5458
+ const MAC_APPS = [
5459
+ "Google Chrome.app/Contents/MacOS/Google Chrome",
5460
+ "Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
5461
+ "Chromium.app/Contents/MacOS/Chromium"
5462
+ ];
5463
+ const LINUX_COMMANDS = [
5464
+ "google-chrome",
5465
+ "google-chrome-stable",
5466
+ "chromium",
5467
+ "chromium-browser",
5468
+ "microsoft-edge",
5469
+ "microsoft-edge-stable"
5470
+ ];
5471
+ const WINDOWS_APPS = ["Google\\Chrome\\Application\\chrome.exe", "Microsoft\\Edge\\Application\\msedge.exe"];
5472
+ function candidates(lookup) {
5473
+ const { env, platform, homeDir } = lookup;
5474
+ if (platform === "darwin") return ["/Applications", join(homeDir, "Applications")].flatMap((dir) => MAC_APPS.map((app) => join(dir, app)));
5475
+ if (platform === "win32") {
5476
+ const roots = [
5477
+ env["ProgramFiles"],
5478
+ env["ProgramFiles(x86)"],
5479
+ env["LOCALAPPDATA"]
5480
+ ].filter((root) => root !== void 0 && root !== "");
5481
+ return WINDOWS_APPS.flatMap((app) => roots.map((root) => `${root}\\${app}`));
5482
+ }
5483
+ const dirs = (env["PATH"] ?? "").split(delimiter).filter((dir) => isAbsolute(dir));
5484
+ return LINUX_COMMANDS.flatMap((command) => dirs.map((dir) => join(dir, command)));
5485
+ }
5486
+ function findBrowser(lookup) {
5487
+ const isExecutable = lookup.isExecutable ?? executableFile;
5488
+ const explicit = lookup.env["VDE_OPEN_BROWSER"];
5489
+ if (explicit !== void 0 && explicit !== "") {
5490
+ if (isAbsolute(explicit) && isExecutable(explicit)) return explicit;
5491
+ throw new VdeError("E_BROWSER_NOT_FOUND", "VDE_OPEN_BROWSER is not the absolute path of an executable file.", { source: "VDE_OPEN_BROWSER" });
5492
+ }
5493
+ const found = candidates(lookup).find((path) => isExecutable(path));
5494
+ if (found !== void 0) return found;
5495
+ throw new VdeError("E_BROWSER_NOT_FOUND", "Exporting a PDF needs Google Chrome or Microsoft Edge. Set VDE_OPEN_BROWSER to use another Chromium-based browser.", { source: "search" });
5496
+ }
5497
+ //#endregion
5498
+ //#region src/export/cdp.ts
5499
+ const MAX_MESSAGE_BYTES = 16777216;
5500
+ var CdpError = class extends Error {
5501
+ method;
5502
+ constructor(method, message) {
5503
+ super(`${method}: ${message}`);
5504
+ this.name = "CdpError";
5505
+ this.method = method;
5506
+ }
5507
+ };
5508
+ function connectCdp(toBrowser, fromBrowser) {
5509
+ let nextId = 1;
5510
+ let failure = null;
5511
+ const pending = /* @__PURE__ */ new Map();
5512
+ const waiters = [];
5513
+ let chunks = [];
5514
+ let buffered = 0;
5515
+ const close = (error) => {
5516
+ if (failure !== null) return;
5517
+ failure = error;
5518
+ for (const waiter of pending.values()) waiter.reject(error);
5519
+ pending.clear();
5520
+ for (const waiter of waiters.splice(0)) waiter.reject(error);
5521
+ };
5522
+ const dispatch = (message) => {
5523
+ if (message.id !== void 0) {
5524
+ const waiter = pending.get(message.id);
5525
+ if (!waiter) return;
5526
+ pending.delete(message.id);
5527
+ if (message.error) waiter.reject(new CdpError(waiter.method, message.error.message ?? "failed"));
5528
+ else waiter.resolve(message.result);
5529
+ return;
5530
+ }
5531
+ const index = waiters.findIndex((waiter) => waiter.method === message.method && waiter.sessionId === message.sessionId && waiter.accept(message.params));
5532
+ if (index === -1) return;
5533
+ const [waiter] = waiters.splice(index, 1);
5534
+ waiter?.resolve(message.params);
5535
+ };
5536
+ fromBrowser.on("data", (chunk) => {
5537
+ if (failure !== null) return;
5538
+ let start = 0;
5539
+ let end = chunk.indexOf(0, start);
5540
+ try {
5541
+ while (end !== -1) {
5542
+ chunks.push(chunk.subarray(start, end));
5543
+ const text = Buffer.concat(chunks).toString("utf8");
5544
+ chunks = [];
5545
+ buffered = 0;
5546
+ dispatch(JSON.parse(text));
5547
+ start = end + 1;
5548
+ end = chunk.indexOf(0, start);
5549
+ }
5550
+ if (start < chunk.length) {
5551
+ chunks.push(chunk.subarray(start));
5552
+ buffered += chunk.length - start;
5553
+ if (buffered > MAX_MESSAGE_BYTES) throw new Error("message too large");
5554
+ }
5555
+ } catch {
5556
+ chunks = [];
5557
+ close(new CdpError("protocol", "the browser sent a message that cannot be read"));
5558
+ }
5559
+ });
5560
+ fromBrowser.on("error", () => void 0);
5561
+ toBrowser.on("error", () => void 0);
5562
+ return {
5563
+ send(method, params = {}, sessionId) {
5564
+ if (failure !== null) return Promise.reject(failure);
5565
+ const id = nextId;
5566
+ nextId += 1;
5567
+ return new Promise((resolve, reject) => {
5568
+ pending.set(id, {
5569
+ method,
5570
+ resolve,
5571
+ reject
5572
+ });
5573
+ toBrowser.write(`${JSON.stringify({
5574
+ id,
5575
+ method,
5576
+ params,
5577
+ ...sessionId === void 0 ? {} : { sessionId }
5578
+ })}\0`);
5579
+ });
5580
+ },
5581
+ waitFor(method, sessionId, accept) {
5582
+ if (failure !== null) return Promise.reject(failure);
5583
+ return new Promise((resolve, reject) => {
5584
+ waiters.push({
5585
+ method,
5586
+ sessionId,
5587
+ accept,
5588
+ resolve,
5589
+ reject
5590
+ });
5591
+ });
5592
+ },
5593
+ close
5594
+ };
5595
+ }
5596
+ //#endregion
5597
+ //#region src/export/pdf-service.ts
5598
+ const BROWSER_FLAGS = [
5599
+ "--headless",
5600
+ "--remote-debugging-pipe",
5601
+ "--no-first-run",
5602
+ "--no-default-browser-check",
5603
+ "--disable-background-networking",
5604
+ "--disable-component-update",
5605
+ "--disable-default-apps",
5606
+ "--disable-extensions",
5607
+ "--disable-sync",
5608
+ "--password-store=basic",
5609
+ "--use-mock-keychain"
5610
+ ];
5611
+ const MIN_BROWSER_VERSION = 131;
5612
+ const EXIT_GRACE_MS = 5e3;
5613
+ const MAX_WAITING = 4;
5614
+ const READ_CHUNK_BYTES = 1048576;
5615
+ const MAX_PAGE_BYTES = 268435456;
5616
+ const TEMP_PREFIX$1 = "vde-open-pdf-";
5617
+ const STALE_TEMP_MS = 36e5;
5618
+ const PDF_HEAD = Buffer.from("%PDF-");
5619
+ const PDF_TAIL = Buffer.from("%%EOF");
5620
+ function exportFailed(reason, details = {}) {
5621
+ return new VdeError("E_EXPORT_FAILED", "The browser could not print the PDF.", {
5622
+ reason,
5623
+ ...details
5624
+ });
5625
+ }
5626
+ const stopping = () => new VdeError("E_DAEMON_STOPPING", "The daemon is stopping.");
5627
+ const cancelled = () => new VdeError("E_INTERRUPTED", "The PDF export was cancelled.");
5628
+ function settledWithin(promise, ms) {
5629
+ return new Promise((resolve) => {
5630
+ const timer = setTimeout(() => resolve(false), ms);
5631
+ promise.then(() => {
5632
+ clearTimeout(timer);
5633
+ resolve(true);
5634
+ });
5635
+ });
5636
+ }
5637
+ async function printPage(cdp, url) {
5638
+ const { product } = await cdp.send("Browser.getVersion");
5639
+ if (Number(/\/(\d+)\./.exec(product)?.[1] ?? "0") < MIN_BROWSER_VERSION) throw new VdeError("E_BROWSER_NOT_FOUND", `Exporting a PDF needs Google Chrome or Microsoft Edge ${String(MIN_BROWSER_VERSION)} or later.`, {
5640
+ source: "version",
5641
+ version: product
5642
+ });
5643
+ const { targetId } = await cdp.send("Target.createTarget", { url: "about:blank" });
5644
+ const { sessionId } = await cdp.send("Target.attachToTarget", {
5645
+ targetId,
5646
+ flatten: true
5647
+ });
5648
+ await cdp.send("Page.enable", {}, sessionId);
5649
+ await cdp.send("Page.setLifecycleEventsEnabled", { enabled: true }, sessionId);
5650
+ const seen = /* @__PURE__ */ new Set();
5651
+ let loaderId = null;
5652
+ const loaded = cdp.waitFor("Page.lifecycleEvent", sessionId, (event) => {
5653
+ if (event.name !== "load") return false;
5654
+ if (loaderId === null) seen.add(event.loaderId);
5655
+ return event.loaderId === loaderId;
5656
+ });
5657
+ loaded.catch(() => void 0);
5658
+ const navigation = await cdp.send("Page.navigate", { url }, sessionId);
5659
+ if (navigation.errorText !== void 0 || navigation.loaderId === void 0) throw exportFailed("load");
5660
+ loaderId = navigation.loaderId;
5661
+ if (!seen.has(loaderId)) await loaded;
5662
+ const { stream } = await cdp.send("Page.printToPDF", {
5663
+ printBackground: true,
5664
+ preferCSSPageSize: true,
5665
+ displayHeaderFooter: false,
5666
+ generateDocumentOutline: true,
5667
+ generateTaggedPDF: true,
5668
+ transferMode: "ReturnAsStream"
5669
+ }, sessionId);
5670
+ const parts = [];
5671
+ for (;;) {
5672
+ const chunk = await cdp.send("IO.read", {
5673
+ handle: stream,
5674
+ size: READ_CHUNK_BYTES
5675
+ }, sessionId);
5676
+ parts.push(Buffer.from(chunk.data, chunk.base64Encoded === true ? "base64" : "latin1"));
5677
+ if (chunk.eof) break;
5678
+ }
5679
+ await cdp.send("IO.close", { handle: stream }, sessionId);
5680
+ return {
5681
+ product,
5682
+ pdf: Buffer.concat(parts)
5683
+ };
5684
+ }
5685
+ function isPdf(pdf) {
5686
+ let end = pdf.length;
5687
+ while (end > 0 && (pdf[end - 1] === 10 || pdf[end - 1] === 13 || pdf[end - 1] === 32)) end -= 1;
5688
+ return pdf.subarray(0, PDF_HEAD.length).equals(PDF_HEAD) && pdf.subarray(end - PDF_TAIL.length, end).equals(PDF_TAIL);
5689
+ }
5690
+ function createPdfService(options) {
5691
+ const { store, documents, printer } = options;
5692
+ const platform = options.platform ?? process.platform;
5693
+ const timeoutMs = options.timeoutMs ?? LIMITS.pdfExportTimeoutMs;
5694
+ const tempRoot = options.tempRoot ?? tmpdir();
5695
+ const maxPageBytes = options.maxPageBytes ?? MAX_PAGE_BYTES;
5696
+ let closed = false;
5697
+ let stopRunning = null;
5698
+ let queue = Promise.resolve();
5699
+ let waiting = 0;
5700
+ const kill = (child) => {
5701
+ if (child.pid === void 0) return;
5702
+ if (platform === "win32") {
5703
+ if (child.exitCode !== null || child.signalCode !== null) return;
5704
+ spawn("taskkill", [
5705
+ "/PID",
5706
+ String(child.pid),
5707
+ "/T",
5708
+ "/F"
5709
+ ], {
5710
+ stdio: "ignore",
5711
+ windowsHide: true
5712
+ }).once("error", () => child.kill());
5713
+ return;
5714
+ }
5715
+ try {
5716
+ process.kill(-child.pid, "SIGKILL");
5717
+ } catch {}
5718
+ };
5719
+ const writePage = async (path, html, slot, images) => {
5720
+ const file = await open(path, "wx", 384);
5721
+ let written = 0;
5722
+ const write = async (text) => {
5723
+ written += Buffer.byteLength(text);
5724
+ if (written > maxPageBytes) throw new VdeError("E_LIMIT_EXCEEDED", "The document is too large to print.", {
5725
+ reason: "page-size",
5726
+ limit: maxPageBytes
5727
+ });
5728
+ await file.write(text);
5729
+ };
5730
+ try {
5731
+ let last = 0;
5732
+ for (const match of html.matchAll(new RegExp(`${slot}-(\\d+)`, "g"))) {
5733
+ await write(html.slice(last, match.index));
5734
+ const image = images[Number(match[1])];
5735
+ if (image !== void 0) {
5736
+ await write(`data:${image.mime};base64,`);
5737
+ await write((await store.readBlob(image.sha256)).toString("base64"));
5738
+ }
5739
+ last = match.index + match[0].length;
5740
+ }
5741
+ await write(html.slice(last));
5742
+ } finally {
5743
+ await file.close();
5744
+ }
5745
+ };
5746
+ const uid = platform === "win32" ? null : process.getuid?.() ?? null;
5747
+ const sweepStale = async () => {
5748
+ const now = Date.now();
5749
+ for (const name of await readdir(tempRoot).catch(() => [])) {
5750
+ if (!name.startsWith(TEMP_PREFIX$1)) continue;
5751
+ const path = join(tempRoot, name);
5752
+ const info = await lstat(path).catch(() => null);
5753
+ if (info === null || !info.isDirectory() || now - info.mtimeMs < STALE_TEMP_MS) continue;
5754
+ if (uid !== null && info.uid !== uid) continue;
5755
+ await rm(path, {
5756
+ recursive: true,
5757
+ force: true
5758
+ }).catch(() => void 0);
5759
+ }
5760
+ };
5761
+ sweepStale();
5762
+ const print = async (browser, directory, url, signal) => {
5763
+ if (closed) throw stopping();
5764
+ if (signal?.aborted === true) throw cancelled();
5765
+ const child = spawn(browser, [
5766
+ ...BROWSER_FLAGS,
5767
+ `--user-data-dir=${join(directory, "profile")}`,
5768
+ "about:blank"
5769
+ ], {
5770
+ stdio: [
5771
+ "ignore",
5772
+ "ignore",
5773
+ "ignore",
5774
+ "pipe",
5775
+ "pipe"
5776
+ ],
5777
+ detached: platform !== "win32",
5778
+ windowsHide: true
5779
+ });
5780
+ const cdp = connectCdp(child.stdio[3], child.stdio[4]);
5781
+ let reason = null;
5782
+ const stop = (why) => {
5783
+ reason ??= why;
5784
+ cdp.close(why);
5785
+ kill(child);
5786
+ };
5787
+ const exited = new Promise((resolve) => {
5788
+ child.once("exit", () => {
5789
+ cdp.close(reason ?? exportFailed("exit"));
5790
+ resolve();
5791
+ });
5792
+ child.once("error", () => {
5793
+ stop(exportFailed("spawn"));
5794
+ resolve();
5795
+ });
5796
+ });
5797
+ const timer = setTimeout(() => stop(exportFailed("timeout", { timeoutMs })), timeoutMs);
5798
+ const onAbort = () => stop(cancelled());
5799
+ signal?.addEventListener("abort", onAbort, { once: true });
5800
+ stopRunning = stop;
5801
+ let printed = null;
5802
+ try {
5803
+ printed = await printPage(cdp, url);
5804
+ await cdp.send("Browser.close").catch(() => void 0);
5805
+ return printed;
5806
+ } catch (error) {
5807
+ if (reason !== null) throw reason;
5808
+ if (error instanceof VdeError) throw error;
5809
+ throw exportFailed("protocol");
5810
+ } finally {
5811
+ clearTimeout(timer);
5812
+ signal?.removeEventListener("abort", onAbort);
5813
+ stopRunning = null;
5814
+ if (printed === null) kill(child);
5815
+ if (!await settledWithin(exited, EXIT_GRACE_MS)) {
5816
+ kill(child);
5817
+ await settledWithin(exited, EXIT_GRACE_MS);
5818
+ }
5819
+ }
5820
+ };
5821
+ const run = async (documentId, revision, signal) => {
5822
+ const started = Date.now();
5823
+ const { record, entry } = documents.describeRevision(documentId, revision);
5824
+ if (entry.format !== "markdown") throw new VdeError("E_UNSUPPORTED_FORMAT", "Only Markdown documents can be exported as PDF.", { format: entry.format });
5825
+ const source = (await store.readBlob(entry.sourceSha256)).toString("utf8");
5826
+ const images = entry.assets.filter((asset) => asset.role === "image" || asset.role === "svg");
5827
+ const slot = randomBytes(16).toString("hex");
5828
+ const html = await printer.print({
5829
+ source,
5830
+ title: record.title,
5831
+ documentLogicalPath: entry.documentLogicalPath,
5832
+ images: images.map((asset) => asset.logicalPath),
5833
+ imageSlot: slot
5834
+ });
5835
+ const browser = options.findBrowser();
5836
+ const directory = await mkdtemp(join(tempRoot, TEMP_PREFIX$1));
5837
+ try {
5838
+ const page = join(directory, "document.html");
5839
+ await writePage(page, html, slot, images);
5840
+ const { product, pdf } = await print(browser, directory, pathToFileURL(page).href, signal);
5841
+ if (!isPdf(pdf)) throw exportFailed("output");
5842
+ options.onEvent?.("pdf.exported", {
5843
+ bytes: pdf.length,
5844
+ elapsedMs: Date.now() - started,
5845
+ browser: product.split("/", 1)[0] ?? ""
5846
+ });
5847
+ return pdf;
5848
+ } finally {
5849
+ await rm(directory, {
5850
+ recursive: true,
5851
+ force: true,
5852
+ maxRetries: 10,
5853
+ retryDelay: 100
5854
+ }).catch((error) => {
5855
+ options.onEvent?.("pdf.cleanup_failed", { code: error.code ?? "unknown" });
5856
+ });
5857
+ }
5858
+ };
5859
+ return {
5860
+ exportMarkdown(documentId, revision, signal) {
5861
+ if (closed) return Promise.reject(stopping());
5862
+ if (waiting >= MAX_WAITING) return Promise.reject(new VdeError("E_LIMIT_EXCEEDED", "Too many PDF exports are waiting.", {
5863
+ reason: "waiting",
5864
+ limit: MAX_WAITING
5865
+ }));
5866
+ let counted = true;
5867
+ const uncount = () => {
5868
+ if (!counted) return;
5869
+ counted = false;
5870
+ waiting -= 1;
5871
+ };
5872
+ waiting += 1;
5873
+ signal?.addEventListener("abort", uncount, { once: true });
5874
+ const result = queue.then(() => {
5875
+ signal?.removeEventListener("abort", uncount);
5876
+ uncount();
5877
+ if (closed) throw stopping();
5878
+ if (signal?.aborted === true) throw cancelled();
5879
+ return run(documentId, revision, signal);
5880
+ });
5881
+ queue = result.catch((error) => {
5882
+ if (error instanceof VdeError && error.code !== "E_INTERRUPTED") options.onEvent?.("pdf.failed", {
5883
+ code: error.code,
5884
+ reason: String(error.details["reason"] ?? error.details["source"] ?? "")
5885
+ });
5886
+ });
5887
+ return result;
5888
+ },
5889
+ async close() {
5890
+ closed = true;
5891
+ stopRunning?.(stopping());
5892
+ await queue;
5893
+ }
5894
+ };
5895
+ }
5896
+ //#endregion
5448
5897
  //#region src/feedback/service.ts
5449
5898
  function sha256$1(text) {
5450
5899
  return createHash("sha256").update(text).digest("hex");
@@ -10806,6 +11255,14 @@ async function startManagementServer(deps, options = {}) {
10806
11255
  })));
10807
11256
  api.post("/documents/:id/focus", async (c) => ok(c, "documents.focus", await deps.documents.focus({ documentId: c.req.param("id") })));
10808
11257
  api.post("/documents/:id/refresh", async (c) => ok(c, "documents.refresh", await deps.documents.refresh({ documentId: c.req.param("id") })));
11258
+ api.post("/documents/:id/pdf", async (c) => {
11259
+ const body = strictObject({ revision: revisionSchema }).parse(await c.req.json());
11260
+ const pdf = await deps.pdf.exportMarkdown(c.req.param("id"), body.revision, c.req.raw.signal);
11261
+ return c.body(new Uint8Array(pdf), 200, {
11262
+ "Content-Type": "application/pdf",
11263
+ "Content-Disposition": "attachment"
11264
+ });
11265
+ });
10809
11266
  api.get("/search", async (c) => {
10810
11267
  const { query, mode, limit, maxBytes, cursor } = c.req.query();
10811
11268
  const documents = c.req.queries("document") ?? [];
@@ -13346,6 +13803,10 @@ function createParseService(options = {}) {
13346
13803
  op: "render",
13347
13804
  input
13348
13805
  }),
13806
+ print: (input) => request({
13807
+ op: "print",
13808
+ input
13809
+ }),
13349
13810
  async close() {
13350
13811
  closed = true;
13351
13812
  collector.close(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
@@ -13709,8 +14170,10 @@ async function startDaemon(options) {
13709
14170
  let preview = null;
13710
14171
  let watcher = null;
13711
14172
  let parse = null;
14173
+ let printer = null;
13712
14174
  let search = null;
13713
14175
  let feedback = null;
14176
+ let pdf = null;
13714
14177
  let announceStopping = () => void 0;
13715
14178
  let lockTimer = null;
13716
14179
  let heapWatch = null;
@@ -13753,9 +14216,11 @@ async function startDaemon(options) {
13753
14216
  await step("drain", () => ipc?.drain());
13754
14217
  announceStopping();
13755
14218
  await step("management", () => management?.close(), SHUTDOWN_CLOSE_TIMEOUT_MS);
14219
+ await step("pdf", () => pdf?.close(), SHUTDOWN_CLOSE_TIMEOUT_MS);
13756
14220
  await step("preview", () => preview?.close(), SHUTDOWN_CLOSE_TIMEOUT_MS);
13757
14221
  await step("store", () => store?.close());
13758
14222
  await step("parse", () => parse?.close(), SHUTDOWN_CLOSE_TIMEOUT_MS);
14223
+ await step("print", () => printer?.close(), SHUTDOWN_CLOSE_TIMEOUT_MS);
13759
14224
  await step("search", () => search?.close(), SHUTDOWN_CLOSE_TIMEOUT_MS);
13760
14225
  await step("ipc", () => ipc?.close());
13761
14226
  let ownsLock = false;
@@ -13855,6 +14320,16 @@ async function startDaemon(options) {
13855
14320
  })
13856
14321
  });
13857
14322
  pruneGrants = () => render.pruneClosed();
14323
+ printer = createParseService({ timeoutMs: LIMITS.printRenderTimeoutMs });
14324
+ const exporting = createPdfService({
14325
+ store: openedStore,
14326
+ documents,
14327
+ printer,
14328
+ findBrowser: () => findBrowser(environment),
14329
+ platform: environment.platform,
14330
+ onEvent: (event, fields) => logger.log(event, fields)
14331
+ });
14332
+ pdf = exporting;
13858
14333
  preview = await startPreviewServer({
13859
14334
  render,
13860
14335
  frameAncestors: () => [managementOrigin, devOrigin].filter((origin) => origin !== null),
@@ -13870,6 +14345,7 @@ async function startDaemon(options) {
13870
14345
  render,
13871
14346
  search: searching,
13872
14347
  feedback: answering,
14348
+ pdf: exporting,
13873
14349
  previewOrigin,
13874
14350
  webRoot: webRootPath(),
13875
14351
  devOrigin,
@@ -13907,7 +14383,8 @@ async function startDaemon(options) {
13907
14383
  const { collectGarbage } = diagnosticsParamsSchema.parse(params ?? {});
13908
14384
  const workers = {
13909
14385
  search: await search?.diagnostics(collectGarbage, stopRequested.signal) ?? null,
13910
- parse: await parse?.diagnostics(collectGarbage, stopRequested.signal) ?? null
14386
+ parse: await parse?.diagnostics(collectGarbage, stopRequested.signal) ?? null,
14387
+ print: await printer?.diagnostics(collectGarbage, stopRequested.signal) ?? null
13911
14388
  };
13912
14389
  const heapUsedBytes = measureHeap(collectGarbage);
13913
14390
  const activeResources = {};