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

321 lines
9.0 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 { randomUUID } from "node:crypto";
import {
cp,
mkdir,
mkdtemp,
readFile,
rm,
stat,
writeFile
} 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 validFontPackRoot = path.resolve(
process.env.MD_TO_PDF_FONT_PACK_DIR?.trim() ||
path.join(repositoryRoot, "output", "font-packs", "root")
);
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
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 < 45; 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 inspectFontPackMount(containerId, expectedRoot) {
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, "容器缺少字体包挂载");
assert(mount.RW === false, "字体包挂载不是只读");
assert(
path.resolve(mount.Source) === path.resolve(expectedRoot),
`字体包挂载源不匹配:${mount.Source}`
);
}
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());
}
async function runScenario({ name, root, expected }) {
const port = await freePort();
const projectName = `mdtp-fp3c-${name}-${randomUUID().slice(0, 8)}`;
const environment = {
...process.env,
MD_TO_PDF_IMAGE: image,
MD_TO_PDF_PORT: String(port),
MD_TO_PDF_FONT_PACK_DIR: root,
MD_TO_PDF_THEME_DIR: path.join(path.dirname(root), "empty-themes"),
MD_TO_PDF_APP_VERSION: "0.6.0"
};
await mkdir(environment.MD_TO_PDF_THEME_DIR, { recursive: true });
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, `${name} 场景未创建容器`);
inspectFontPackMount(containerId, root);
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",
`${name} 场景 DOCX capability 无效:${JSON.stringify(capability)}`
);
const cssResponse = await fetch(
`${origin}/api/themes/gov-red-standard/css`
);
const css = await cssResponse.text();
assert(cssResponse.ok, `${name} 场景主题 CSS 请求失败`);
const marker = "md-to-pdf-font-packs:mdtp-serif-sc@1.0.0";
assert(
css.includes(marker) === expected.fontPackAvailable,
`${name} 场景字体包标记状态不符合预期`
);
if (expected.fontPackAvailable) {
const assetPath = css.match(
/(\/api\/font-packs\/[^"'()\s]+\/web\?v=[a-f0-9]+)/u
)?.[1];
assert(assetPath, "有效字体包 CSS 缺少受控 WOFF2 地址");
const assetResponse = await fetch(`${origin}${assetPath}`);
assert(assetResponse.ok, "有效字体包 WOFF2 请求失败");
assert(
assetResponse.headers.get("content-type")?.includes("font/woff2"),
"有效字体包 WOFF2 MIME 无效"
);
assert(
assetResponse.headers.get("cache-control")?.includes("immutable"),
"有效字体包 WOFF2 缓存策略无效"
);
assert(
(await assetResponse.arrayBuffer()).byteLength > 1024 * 1024,
"有效字体包 WOFF2 内容异常"
);
}
if (expected.exportDocx) {
const docx = await exportDocx(origin);
const entries = unzipSync(docx);
const fontParts = Object.keys(entries).filter((entry) =>
entry.startsWith("word/fonts/")
);
assert(
fontParts.length >= expected.minimumFontParts,
`${name} 场景 DOCX 字体部件不足:${fontParts.length}`
);
if (expected.fontPackAvailable) {
const fontTable = strFromU8(entries["word/fontTable.xml"]);
assert(
fontTable.includes("MdTP Serif SC"),
"有效字体包未进入 DOCX 字体表"
);
}
}
return {
name,
fontPackAvailable: expected.fontPackAvailable,
docxExported: expected.exportDocx,
readOnlyMount: true
};
} finally {
run(
"docker",
[...compose, "down", "--remove-orphans", "--timeout", "5"],
{ env: environment }
);
}
}
const validRootStatus = await stat(validFontPackRoot).catch(
() => undefined
);
assert(
validRootStatus?.isDirectory(),
`未找到有效字体包根目录:${validFontPackRoot}。请先运行 npm run build:font-pack。`
);
run("docker", ["image", "inspect", image], { capture: true });
run("docker", [
"run",
"--rm",
"--entrypoint",
"sh",
image,
"-c",
"test ! -e /app/.local/font-packs"
]);
const temporaryRoot = await mkdtemp(
path.join(os.tmpdir(), "mdtp-docker-font-pack-")
);
try {
const emptyRoot = path.join(temporaryRoot, "empty-font-packs");
const damagedRoot = path.join(temporaryRoot, "damaged-font-packs");
await mkdir(emptyRoot, { recursive: true });
await cp(validFontPackRoot, damagedRoot, { recursive: true });
const manifestPath = path.join(
damagedRoot,
"mdtp-serif-sc",
"1.0.0",
"font-pack.json"
);
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const damagedAssetPath = path.join(
path.dirname(manifestPath),
manifest.faces[0].web.path
);
const damagedAsset = await readFile(damagedAssetPath);
damagedAsset[0] ^= 0xff;
await writeFile(damagedAssetPath, damagedAsset);
const results = [];
results.push(
await runScenario({
name: "empty",
root: emptyRoot,
expected: {
fontPackAvailable: false,
exportDocx: false,
minimumFontParts: 0
}
})
);
results.push(
await runScenario({
name: "valid",
root: validFontPackRoot,
expected: {
fontPackAvailable: true,
exportDocx: true,
minimumFontParts: 2
}
})
);
results.push(
await runScenario({
name: "damaged",
root: damagedRoot,
expected: {
fontPackAvailable: false,
exportDocx: true,
minimumFontParts: 0
}
})
);
process.stdout.write(`${JSON.stringify({ image, results }, null, 2)}\n`);
} finally {
await rm(temporaryRoot, { recursive: true, force: true });
}