@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.
- package/package.json +1 -1
- package/src/commands/wechat-export.ts +1 -1
- package/src/imgx/chrome-render.test.ts +17 -2
- package/src/imgx/chrome-render.ts +80 -26
- package/src/imgx/runtime.ts +0 -61
- package/src/wechat-preview/assets/browser-dist/editor-export.js +36 -36
- package/src/wechat-preview/assets/templates/export-shell.html +6 -0
- package/src/wechat-preview/browser/editor-export.ts +7 -14
- package/src/wechat-preview/index.ts +20 -18
- package/src/wechat-preview/server/types.ts +1 -0
- package/src/wechat-preview/wechat-export.integration.test.ts +52 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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 =
|
|
404
|
+
dumpedDom = await dumpReadyHtml({
|
|
405
405
|
chromePath,
|
|
406
406
|
html: shellHtml,
|
|
407
|
-
|
|
408
|
-
keepTempOnError: Boolean(input.debugDir),
|
|
407
|
+
timeoutMs,
|
|
409
408
|
});
|
|
410
409
|
} catch (error) {
|
|
411
|
-
if (error instanceof
|
|
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:
|
|
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(
|
|
426
|
-
|
|
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: "
|
|
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
|
-
"
|
|
456
|
-
`Wechat preview export
|
|
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: "
|
|
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
|
-
"
|
|
494
|
-
`Wechat preview export
|
|
495
|
+
"render_error",
|
|
496
|
+
`Wechat preview export returned an incomplete result (status=${result.status})`,
|
|
495
497
|
);
|
|
496
498
|
}
|
|
497
499
|
|
|
@@ -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, `\n\n`);
|
|
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
|
});
|