@zzclub/pipeline 0.13.1 → 0.13.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.
@@ -12,11 +12,17 @@
12
12
  <script id="zzhub-wechat-export-result" type="application/json">{"status":"pending"}</script>
13
13
  </div>
14
14
  <script>
15
+ var renderFinished = false;
16
+ var resolveRender;
17
+ window.__zzhubRenderReady = new Promise(function (resolve) { resolveRender = resolve; });
15
18
  window.__ZZHUB_SET_RESULT__ = function (payload) {
19
+ if (renderFinished) return;
20
+ renderFinished = true;
16
21
  var el = document.getElementById("zzhub-wechat-export-result");
17
22
  if (el) {
18
23
  el.textContent = JSON.stringify(payload);
19
24
  }
25
+ resolveRender();
20
26
  };
21
27
  window.addEventListener("error", function (event) {
22
28
  window.__ZZHUB_SET_RESULT__({
@@ -13,24 +13,17 @@ interface BrowserPayload {
13
13
  customCss?: string;
14
14
  }
15
15
 
16
+ type BrowserResult =
17
+ | { status: "success"; html: string; semanticHtml?: string }
18
+ | { status: "error"; error: string };
19
+
16
20
  declare global {
17
21
  interface Window {
18
22
  __ZZHUB_WECHAT_PAYLOAD__?: BrowserPayload;
23
+ __ZZHUB_SET_RESULT__: (result: BrowserResult) => void;
19
24
  }
20
25
  }
21
26
 
22
- function setResult(
23
- result:
24
- | { status: "success"; html: string; semanticHtml?: string }
25
- | { status: "error"; error: string },
26
- ): void {
27
- const el = document.getElementById("zzhub-wechat-export-result");
28
- if (!el) {
29
- return;
30
- }
31
- el.textContent = JSON.stringify(result);
32
- }
33
-
34
27
  async function main(): Promise<void> {
35
28
  const payload = window.__ZZHUB_WECHAT_PAYLOAD__;
36
29
  if (!payload) {
@@ -59,10 +52,10 @@ async function main(): Promise<void> {
59
52
  theme: payload.exportTheme,
60
53
  });
61
54
  await editor.destroy();
62
- setResult({ status: "success", html, semanticHtml });
55
+ window.__ZZHUB_SET_RESULT__({ status: "success", html, semanticHtml });
63
56
  }
64
57
 
65
58
  void main().catch((error: unknown) => {
66
59
  const detail = error instanceof Error ? error.message : String(error);
67
- setResult({ status: "error", error: detail });
60
+ window.__ZZHUB_SET_RESULT__({ status: "error", error: detail });
68
61
  });
@@ -4,14 +4,13 @@ import { createRequire } from "module";
4
4
  import { dirname, isAbsolute, join, resolve } from "path";
5
5
  import { pathToFileURL } from "url";
6
6
  import {
7
- ChromeDumpError,
8
- dumpHtmlDom,
9
7
  ensureParentDir,
10
8
  escapeHtml,
11
9
  findChrome,
12
10
  readUtf8,
13
11
  renderTemplate,
14
12
  } from "../imgx/runtime";
13
+ import { ChromeRenderError, dumpReadyHtml } from "../imgx/chrome-render";
15
14
  import {
16
15
  PACKAGE_ROOT,
17
16
  TEMPLATE_PATH,
@@ -45,6 +44,7 @@ export type WechatExportErrorKind =
45
44
  export interface WechatExportDebugInfo {
46
45
  chrome_path?: string;
47
46
  virtual_time_budget_ms?: number;
47
+ timeout_ms?: number;
48
48
  bundle_stale?: boolean;
49
49
  bundle_rebuilt?: boolean;
50
50
  shell_path?: string;
@@ -94,7 +94,7 @@ export interface ExportMarkdownToWechatHtmlInput {
94
94
  editorVars?: Record<string, string>;
95
95
  exportTheme?: Record<string, string>;
96
96
  };
97
- /** Chrome virtual-time-budget in ms (default 15000). */
97
+ /** 浏览器真实等待上限,默认 15000 毫秒。 */
98
98
  timeoutMs?: number;
99
99
  /** Write intermediate artifacts for debugging. */
100
100
  debugDir?: string;
@@ -320,7 +320,7 @@ export async function exportMarkdownToWechatHtml(
320
320
  input: ExportMarkdownToWechatHtmlInput,
321
321
  ): Promise<ExportMarkdownToWechatHtmlResult> {
322
322
  const started = Date.now();
323
- const timeoutMs = input.timeoutMs ?? 15_000;
323
+ const timeoutMs = input.timeoutMs && Number.isFinite(input.timeoutMs) && input.timeoutMs > 0 ? input.timeoutMs : 15_000;
324
324
  const title = input.title ?? "Wechat Preview Export";
325
325
  let chromePath: string | undefined;
326
326
  let bundleRebuilt = false;
@@ -339,7 +339,7 @@ export async function exportMarkdownToWechatHtml(
339
339
  markdownPath: input.markdownPath,
340
340
  debug: {
341
341
  chrome_path: chromePath,
342
- virtual_time_budget_ms: timeoutMs,
342
+ timeout_ms: timeoutMs,
343
343
  bundle_stale: bundleStale,
344
344
  bundle_rebuilt: bundleRebuilt,
345
345
  debug_dir: input.debugDir,
@@ -401,29 +401,31 @@ export async function exportMarkdownToWechatHtml(
401
401
 
402
402
  let dumpedDom: string;
403
403
  try {
404
- dumpedDom = dumpHtmlDom({
404
+ dumpedDom = await dumpReadyHtml({
405
405
  chromePath,
406
406
  html: shellHtml,
407
- virtualTimeBudgetMs: timeoutMs,
408
- keepTempOnError: Boolean(input.debugDir),
407
+ timeoutMs,
409
408
  });
410
409
  } catch (error) {
411
- if (error instanceof ChromeDumpError) {
410
+ if (error instanceof ChromeRenderError) {
412
411
  if (input.debugDir) {
413
412
  await writeDebugArtifacts(input.debugDir, {
414
413
  "00-payload-markdown.md": markdown,
415
414
  "01-export-shell.html": shellHtml,
416
415
  "meta.json": JSON.stringify({
417
416
  status: "failed",
418
- kind: "chrome_failed",
417
+ kind: error.kind,
419
418
  error: error.message,
420
419
  stderr: error.stderr,
421
420
  duration_ms: Date.now() - started,
421
+ timeout_ms: timeoutMs,
422
422
  }, null, 2),
423
423
  });
424
424
  }
425
- throw makeError("chrome_failed", error.message, {
426
- shell_path: error.tempHtmlPath,
425
+ throw makeError(error.kind, error.kind === "timeout"
426
+ ? `Wechat preview export did not finish before timeout (${timeoutMs}ms real time)`
427
+ : error.message, {
428
+ shell_path: input.debugDir ? join(input.debugDir, "01-export-shell.html") : undefined,
427
429
  stderr_tail: stderrTail(error.stderr),
428
430
  debug_dir: input.debugDir,
429
431
  });
@@ -445,15 +447,15 @@ export async function exportMarkdownToWechatHtml(
445
447
  "02-dump-dom.html": dumpedDom,
446
448
  "meta.json": JSON.stringify({
447
449
  status: "failed",
448
- kind: "timeout",
450
+ kind: "render_error",
449
451
  error: error instanceof Error ? error.message : String(error),
450
452
  duration_ms: Date.now() - started,
451
453
  }, null, 2),
452
454
  });
453
455
  }
454
456
  throw makeError(
455
- "timeout",
456
- `Wechat preview export did not finish before timeout (${timeoutMs}ms): ${
457
+ "render_error",
458
+ `Wechat preview export returned an invalid result: ${
457
459
  error instanceof Error ? error.message : String(error)
458
460
  }`,
459
461
  );
@@ -483,15 +485,15 @@ export async function exportMarkdownToWechatHtml(
483
485
  "02-dump-dom.html": dumpedDom,
484
486
  "meta.json": JSON.stringify({
485
487
  status: "failed",
486
- kind: "timeout",
488
+ kind: "render_error",
487
489
  status_value: result.status,
488
490
  duration_ms: Date.now() - started,
489
491
  }, null, 2),
490
492
  });
491
493
  }
492
494
  throw makeError(
493
- "timeout",
494
- `Wechat preview export did not finish before timeout (status=${result.status}, budget=${timeoutMs}ms)`,
495
+ "render_error",
496
+ `Wechat preview export returned an incomplete result (status=${result.status})`,
495
497
  );
496
498
  }
497
499
 
@@ -11,6 +11,7 @@ export type PreviewEntryStatus = "success" | "failed";
11
11
  export interface PreviewEntryDebug {
12
12
  chrome_path?: string;
13
13
  virtual_time_budget_ms?: number;
14
+ timeout_ms?: number;
14
15
  bundle_stale?: boolean;
15
16
  bundle_rebuilt?: boolean;
16
17
  shell_path?: string;
@@ -5,8 +5,10 @@ import { dirname, join } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { PNG } from "pngjs";
7
7
  import { findChrome } from "../imgx/runtime";
8
+ import { dumpReadyHtml } from "../imgx/chrome-render";
9
+ import { TEMPLATE_PATH } from "../runtime-paths";
8
10
  import { replaceImageUrls } from "../providers/wechat";
9
- import { exportMarkdownToWechatHtml } from "./index";
11
+ import { exportMarkdownToWechatHtml, extractRenderResult } from "./index";
10
12
 
11
13
  const fixturePath = join(
12
14
  dirname(fileURLToPath(import.meta.url)),
@@ -165,4 +167,53 @@ describe("semantic WeChat HTML export", () => {
165
167
  message: expect.stringContaining("无法读取缩放图片的原始尺寸"),
166
168
  });
167
169
  }, 30_000);
170
+
171
+ test.skipIf(findChrome() === null)("waits for remote PNG and GIF responses before completing scaled HTML export", async () => {
172
+ tempDir = await mkdtemp(join(tmpdir(), "zzhub-wechat-remote-"));
173
+ const png = new PNG({ width: 200, height: 100 });
174
+ png.data.fill(255);
175
+ const pngBytes = PNG.sync.write(png);
176
+ const gif = Buffer.from("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", "base64");
177
+ const requested = Promise.withResolvers<void>();
178
+ const release = Promise.withResolvers<void>();
179
+ const server = Bun.serve({ hostname: "127.0.0.1", port: 0, async fetch(request) {
180
+ requested.resolve();
181
+ await release.promise;
182
+ const isGif = request.url.endsWith(".gif");
183
+ return new Response(isGif ? gif : pngBytes, { headers: { "content-type": isGif ? "image/gif" : "image/png", "cache-control": "no-store" } });
184
+ } });
185
+ const markdownPath = join(tempDir, "images.md");
186
+ await writeFile(markdownPath, `![0.50](http://127.0.0.1:${server.port}/photo.png "远程图注")\n\n![0.50](http://127.0.0.1:${server.port}/motion.gif "动图图注")`);
187
+ let finished = false;
188
+ const exporting = exportMarkdownToWechatHtml({ markdownPath, outPath: join(tempDir, "images.html"), account: "default" })
189
+ .then(result => { finished = true; return result; });
190
+ try {
191
+ await Promise.race([requested.promise, exporting]);
192
+ // 人为延迟网络响应,确认导出不会提前读到 pending。
193
+ await Bun.sleep(800);
194
+ expect(finished).toBe(false);
195
+ release.resolve();
196
+ const { html } = await exporting;
197
+ expect(html).toContain("width: 50%;");
198
+ expect(html).toContain("max-width: 100px;");
199
+ expect(html).toContain("max-width: 0.5px;");
200
+ expect(html).toContain("远程图注</p>");
201
+ expect(html).toContain("动图图注</p>");
202
+ expect(html).toContain("/motion.gif");
203
+ } finally {
204
+ release.resolve();
205
+ await exporting.catch(() => {});
206
+ await server.stop(true);
207
+ }
208
+ }, 30_000);
209
+
210
+ test.skipIf(findChrome() === null)("settles the export result when the browser module fails to load", async () => {
211
+ const html = (await readFile(TEMPLATE_PATH, "utf8"))
212
+ .replace("{{PAGE_TITLE}}", "加载失败")
213
+ .replace("{{CSS_LINK}}", "")
214
+ .replace("{{PAYLOAD_JSON}}", "{}")
215
+ .replace("{{SCRIPT_URL}}", "file:///missing/wechat-editor.js");
216
+ const result = extractRenderResult(await dumpReadyHtml({ chromePath: findChrome()!, html, timeoutMs: 5000 }));
217
+ expect(result).toEqual({ status: "error", error: "failed to load wechat preview bundle" });
218
+ }, 10_000);
168
219
  });