Files
MorphDoc/deploy/verify-font-pack-compose.mjs
T

258 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { spawnSync } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, stat } from "node:fs/promises";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defaultExportConfig } from "@md-to-pdf/core";
import { strFromU8, unzipSync } from "fflate";
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const repositoryRoot = path.resolve(scriptDirectory, "..");
const composeFile = path.join(scriptDirectory, "compose.yaml");
const image = process.env.MD_TO_PDF_IMAGE?.trim() || "md-to-pdf:local";
const fontPackRoot = path.resolve(
path.join(repositoryRoot, "output", "font-packs", "root")
);
const manifestPath = path.join(
fontPackRoot,
"mdtp-serif-sc",
"1.0.0",
"font-pack.json"
);
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function sha256(content) {
return createHash("sha256").update(content).digest("hex").toLowerCase();
}
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: repositoryRoot,
encoding: "utf8",
stdio: options.capture ? "pipe" : "inherit",
env: options.env ?? process.env
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(
`${command} ${args.join(" ")} 失败(退出码 ${result.status}${
options.capture ? `\n${result.stderr || result.stdout}` : ""
}`
);
}
return options.capture ? result.stdout.trim() : "";
}
async function freePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
const port =
typeof address === "object" && address ? address.port : undefined;
server.close((error) =>
error || !port
? reject(error ?? new Error("无法分配端口"))
: resolve(port)
);
});
});
}
async function waitForHealth(origin) {
let lastError;
for (let attempt = 0; attempt < 60; attempt += 1) {
try {
const response = await fetch(`${origin}/api/health`);
if (response.ok && (await response.json()).status === "ok") {
return;
}
} catch (error) {
lastError = error;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(
`容器健康检查未就绪${lastError ? `${String(lastError)}` : ""}`
);
}
function assertNoFontPackMount(containerId) {
const inspection = JSON.parse(
run("docker", ["inspect", containerId], { capture: true })
)[0];
const mount = inspection.Mounts.find(
(item) => item.Destination === "/app/.local/font-packs"
);
assert(!mount, "Docker 字体包必须来自镜像层,不能依赖运行时挂载");
}
async function exportDocx(origin) {
const response = await fetch(`${origin}/api/docx`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
markdown:
"---\ntitle: Docker 内置字体包验收\n---\n\n# Docker 内置字体包验收\n\n这是一段可编辑中文正文。",
fileName: "Docker 内置字体包验收.md",
language: "zh-CN",
resources: [],
exportConfig: {
...defaultExportConfig,
themeId: "gov-red-standard"
}
})
});
if (!response.ok) {
throw new Error(
`DOCX 导出失败(${response.status}):${await response.text()}`
);
}
return new Uint8Array(await response.arrayBuffer());
}
const fontPackStatus = await stat(fontPackRoot).catch(() => undefined);
assert(
fontPackStatus?.isDirectory(),
`未找到有效字体包根目录:${fontPackRoot}。请先运行 npm run build:font-pack。`
);
const localManifest = await readFile(manifestPath);
const buildEnvironment = {
...process.env,
MD_TO_PDF_IMAGE: image
};
run(
"docker",
["compose", "--file", composeFile, "build", "app"],
{ env: buildEnvironment }
);
run("docker", ["image", "inspect", image], { capture: true });
const embeddedManifestHash = run(
"docker",
[
"run",
"--rm",
"--entrypoint",
"sha256sum",
image,
"/app/.local/font-packs/mdtp-serif-sc/1.0.0/font-pack.json"
],
{ capture: true }
).split(/\s+/u)[0];
assert(
embeddedManifestHash === sha256(localManifest),
"镜像内字体包清单与本次受校验构建产物不一致"
);
const port = await freePort();
const projectName = `morphdoc-font-pack-${randomUUID().slice(0, 8)}`;
const temporaryRoot = await mkdtemp(
path.join(os.tmpdir(), "morphdoc-docker-font-pack-")
);
const themeRoot = path.join(temporaryRoot, "empty-themes");
await mkdir(themeRoot, { recursive: true });
const environment = {
...buildEnvironment,
MD_TO_PDF_PORT: String(port),
MD_TO_PDF_THEME_DIR: themeRoot,
MD_TO_PDF_APP_VERSION: "0.6.0"
};
const compose = [
"compose",
"--project-name",
projectName,
"--file",
composeFile
];
try {
run(
"docker",
[...compose, "up", "--detach", "--no-build", "--pull", "never"],
{ env: environment }
);
const containerId = run(
"docker",
[...compose, "ps", "--quiet", "app"],
{ capture: true, env: environment }
);
assert(containerId, "未创建 Docker 字体包验收容器");
assertNoFontPackMount(containerId);
const origin = `http://127.0.0.1:${port}`;
await waitForHealth(origin);
const capabilityResponse = await fetch(`${origin}/api/docx/capability`);
const capability = await capabilityResponse.json();
assert(
capabilityResponse.ok &&
capability.status === "available" &&
capability.detectedVersion === "3.9.0.2",
`DOCX capability 无效:${JSON.stringify(capability)}`
);
const cssResponse = await fetch(
`${origin}/api/themes/gov-red-standard/css`
);
const css = await cssResponse.text();
assert(cssResponse.ok, "主题 CSS 请求失败");
assert(
css.includes("md-to-pdf-font-packs:mdtp-serif-sc@1.0.0"),
"Docker 内置字体包未进入主题 CSS"
);
const assetPath = css.match(
/(\/api\/font-packs\/[^"'()\s]+\/web\?v=[a-f0-9]+)/u
)?.[1];
assert(assetPath, "Docker 内置字体包 CSS 缺少受控 WOFF2 地址");
const assetResponse = await fetch(`${origin}${assetPath}`);
assert(assetResponse.ok, "Docker 内置字体包 WOFF2 请求失败");
assert(
assetResponse.headers.get("content-type")?.includes("font/woff2"),
"Docker 内置字体包 WOFF2 MIME 无效"
);
assert(
assetResponse.headers.get("cache-control")?.includes("immutable"),
"Docker 内置字体包 WOFF2 缓存策略无效"
);
const docx = await exportDocx(origin);
const entries = unzipSync(docx);
const fontParts = Object.keys(entries).filter((entry) =>
entry.startsWith("word/fonts/")
);
assert(fontParts.length >= 2, `DOCX 字体部件不足:${fontParts.length}`);
const fontTable = strFromU8(entries["word/fontTable.xml"]);
assert(fontTable.includes("MdTP Serif SC"), "字体包未进入 DOCX 字体表");
process.stdout.write(
`${JSON.stringify(
{
image,
embeddedManifestSha256: embeddedManifestHash,
runtimeFontPackMount: false,
fontPackAvailable: true,
pandocVersion: capability.detectedVersion,
docxFontParts: fontParts.length
},
null,
2
)}\n`
);
} finally {
run(
"docker",
[...compose, "down", "--remove-orphans", "--timeout", "5"],
{ env: environment }
);
await rm(temporaryRoot, { recursive: true, force: true });
}