feat: 完善字体包发行与 Docker 内置字体
This commit is contained in:
@@ -194,6 +194,12 @@ describe("桌面发行构建链", () => {
|
||||
expect(installerInclude).toContain("/WITHFONTPACK=");
|
||||
expect(companionBuild).toContain("createDesktopCompanionInclude");
|
||||
expect(companionBuild).toContain("MD_TO_PDF_NSIS_INCLUDE");
|
||||
expect(companionBuild).toContain(
|
||||
"MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe"
|
||||
);
|
||||
expect(companionBuild).toContain(
|
||||
"MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip"
|
||||
);
|
||||
});
|
||||
|
||||
it("将 DOCX Lua Filter 复制到桌面主进程资源目录", () => {
|
||||
|
||||
@@ -38,6 +38,7 @@ describe("Docker v0.6.0 部署契约", () => {
|
||||
expect(dockerfile).toContain(
|
||||
"/app/packages/docx-engine/assets ./packages/docx-engine/assets"
|
||||
);
|
||||
expect(dockerfile).toContain("COPY tsconfig.base.json product.json ./");
|
||||
});
|
||||
|
||||
it("固定校验 Pandoc 二进制、许可证、版权和最终版本", () => {
|
||||
@@ -61,7 +62,8 @@ describe("Docker v0.6.0 部署契约", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("只读挂载字体包并排除本地字体资产构建上下文", () => {
|
||||
it("将受校验字体包内置到镜像并取消运行时字体挂载", () => {
|
||||
const dockerfile = readProjectFile("deploy/Dockerfile");
|
||||
const compose = readProjectFile("deploy/compose.yaml");
|
||||
const dockerignore = readProjectFile(
|
||||
"deploy/Dockerfile.dockerignore"
|
||||
@@ -70,14 +72,16 @@ describe("Docker v0.6.0 部署契约", () => {
|
||||
expect(compose).toContain(
|
||||
"FONT_PACK_ROOT: /app/.local/font-packs"
|
||||
);
|
||||
expect(compose).toContain(
|
||||
"${MD_TO_PDF_FONT_PACK_DIR:-../.local/font-packs}:/app/.local/font-packs:ro"
|
||||
expect(compose).not.toContain("MD_TO_PDF_FONT_PACK_DIR");
|
||||
expect(dockerfile).toContain(
|
||||
"COPY --chown=pwuser:pwuser output/font-packs/root /app/.local/font-packs"
|
||||
);
|
||||
expect(dockerfile).toContain("FONT_PACK_ROOT=/app/.local/font-packs");
|
||||
expect(compose).toContain(
|
||||
'APP_VERSION: "${MD_TO_PDF_APP_VERSION:-0.6.0}"'
|
||||
);
|
||||
expect(dockerignore.split(/\r?\n/u)).toContain(".local");
|
||||
expect(dockerignore.split(/\r?\n/u)).toContain("output");
|
||||
expect(dockerignore).toContain("!output/font-packs/root/**");
|
||||
});
|
||||
|
||||
it("提供显式真实 Docker 字体包验收命令", () => {
|
||||
|
||||
+6
-2
@@ -21,7 +21,7 @@ COPY packages/preview-engine/package.json packages/preview-engine/package.json
|
||||
COPY packages/renderer/package.json packages/renderer/package.json
|
||||
RUN npm ci
|
||||
|
||||
COPY tsconfig.base.json ./
|
||||
COPY tsconfig.base.json product.json ./
|
||||
COPY apps ./apps
|
||||
COPY packages ./packages
|
||||
COPY logos ./logos
|
||||
@@ -151,6 +151,7 @@ COPY --chown=pwuser:pwuser --from=build /app/packages/preview-engine/dist ./pack
|
||||
COPY --chown=pwuser:pwuser --from=build /app/packages/renderer/dist ./packages/renderer/dist
|
||||
COPY --chown=pwuser:pwuser --from=build /app/samples ./samples
|
||||
COPY --chown=pwuser:pwuser themes ./themes
|
||||
COPY --chown=pwuser:pwuser output/font-packs/root /app/.local/font-packs
|
||||
COPY deploy/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --chown=pwuser:pwuser deploy/entrypoint.sh /app/deploy/entrypoint.sh
|
||||
|
||||
@@ -161,7 +162,10 @@ ENV HOST=127.0.0.1 \
|
||||
PDF_RENDER_ORIGIN=http://127.0.0.1:8080 \
|
||||
PDF_CONCURRENCY=1 \
|
||||
PDF_MAX_QUEUE=4 \
|
||||
PDF_TIMEOUT_MS=120000
|
||||
PDF_TIMEOUT_MS=120000 \
|
||||
FONT_PACK_ROOT=/app/.local/font-packs
|
||||
|
||||
RUN test -f /app/.local/font-packs/mdtp-serif-sc/1.0.0/font-pack.json
|
||||
|
||||
USER pwuser
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ coverage
|
||||
dist
|
||||
node_modules
|
||||
out
|
||||
output
|
||||
output/*
|
||||
!output/font-packs/
|
||||
output/font-packs/*
|
||||
!output/font-packs/root/
|
||||
!output/font-packs/root/**
|
||||
playwright-report
|
||||
test-results
|
||||
tmp
|
||||
|
||||
+14
-22
@@ -74,11 +74,11 @@ docker compose -f deploy\compose.yaml up -d
|
||||
3 套标书主题;挂载目录只用于额外的本机扩展主题,无需重复
|
||||
放置内置主题。内置主题与挂载主题 ID 重复时以内置版本为准。
|
||||
|
||||
## 可选字体包挂载
|
||||
## 内置字体包
|
||||
|
||||
Compose 默认将项目的 `.local/font-packs` 只读挂载到容器中的
|
||||
`/app/.local/font-packs`。目录为空或不存在字体包时,应用仍可正常启动,
|
||||
Preview、PDF 和 DOCX 会使用主题原有字体安全降级。
|
||||
v0.6.0 的发行镜像固定将受校验字体包内置到
|
||||
`/app/.local/font-packs`。Compose 不挂载宿主机字体目录,因此部署端
|
||||
无需额外复制字体,Preview、PDF 和 DOCX 可以离线使用同一套原生资产。
|
||||
|
||||
字体包使用固定目录结构:
|
||||
|
||||
@@ -91,36 +91,29 @@ Preview、PDF 和 DOCX 会使用主题原有字体安全降级。
|
||||
└── fonts/
|
||||
```
|
||||
|
||||
从仓库外准备好冻结字体源后,可以构建并挂载项目字体包:
|
||||
从源码构建镜像前,先从仓库外冻结源生成字体包,再构建镜像:
|
||||
|
||||
```powershell
|
||||
npm run build:font-pack
|
||||
$env:MD_TO_PDF_FONT_PACK_DIR = (Resolve-Path 'output\font-packs\root').Path
|
||||
docker compose -f deploy\compose.yaml up -d
|
||||
```
|
||||
|
||||
也可以将完整字体包根目录复制到 `.local/font-packs`,直接使用默认配置。
|
||||
字体包目录以 `:ro` 挂载,容器内非 root 用户不能修改宿主机资产;清单、
|
||||
许可、文件类型、路径边界、容量和 SHA-256 仍会由运行时注册器再次校验。
|
||||
字体包缺失、版本不兼容或资源损坏不会阻断 PDF/DOCX 主链,只会产生诊断
|
||||
并回退到主题字体。
|
||||
字体二进制仍不提交到 Git。Docker 构建上下文只放行本次生成的
|
||||
`output/font-packs/root`,镜像构建会在缺少清单时立即失败。运行时注册器
|
||||
仍会复核许可证、文件类型、路径边界、容量和 SHA-256。更换字体包内容时
|
||||
必须提升字体包 SemVer 并重新构建镜像,不能覆盖同一个版本。
|
||||
|
||||
可选字体包二进制位于被 Git 和 Docker 构建上下文忽略的目录,既不提交到
|
||||
仓库,也不打入应用镜像。更换字体包版本后重启容器即可重新发现;不要在
|
||||
同一个 `<pack-id>/<semver>` 目录中覆盖不同内容。
|
||||
|
||||
发布前可以对已经构建好的镜像执行真实字体包矩阵:
|
||||
发布前执行真实内置字体门禁:
|
||||
|
||||
```powershell
|
||||
$env:MD_TO_PDF_IMAGE = 'yixiong/md-to-pdf:v0.6.0'
|
||||
$env:MD_TO_PDF_FONT_PACK_DIR = (Resolve-Path 'output\font-packs\root').Path
|
||||
npm run verify:docker-font-pack
|
||||
```
|
||||
|
||||
该命令要求本机 Docker 引擎已经启动,镜像已经存在,并且字体包已通过
|
||||
`npm run build:font-pack` 生成。验收会依次挂载空目录、有效字体包和篡改
|
||||
字体包,检查只读挂载、健康状态、Pandoc capability、WOFF2 资源、DOCX
|
||||
字体部件和损坏后的安全降级;所有临时容器、网络和目录会在结束时清理。
|
||||
该命令要求本机 Docker 引擎已经启动,并且冻结字体源可供
|
||||
`npm run build:font-pack` 生成。验收会重建指定镜像,核对镜像内清单哈希、
|
||||
确认不存在字体运行时挂载,并检查健康状态、Pandoc capability、WOFF2
|
||||
资源和 DOCX 字体部件;所有临时容器、网络和目录会在结束时清理。
|
||||
|
||||
## 运行参数
|
||||
|
||||
@@ -131,7 +124,6 @@ npm run verify:docker-font-pack
|
||||
| `MD_TO_PDF_REUSE_PLAYWRIGHT_RUNTIME` | `0` | 是否复用基础镜像中的 Chromium |
|
||||
| `MD_TO_PDF_PORT` | `8080` | 宿主机监听端口 |
|
||||
| `MD_TO_PDF_THEME_DIR` | `../.local/themes` | 宿主机主题目录 |
|
||||
| `MD_TO_PDF_FONT_PACK_DIR` | `../.local/font-packs` | 宿主机可选字体包根目录 |
|
||||
| `MD_TO_PDF_APP_VERSION` | `0.6.0` | 字体包兼容性判断使用的应用版本 |
|
||||
| `PDF_CONCURRENCY` | `1` | 同时执行的 PDF 任务数 |
|
||||
| `PDF_MAX_QUEUE` | `4` | 等待队列上限 |
|
||||
|
||||
@@ -23,7 +23,6 @@ services:
|
||||
FONT_PACK_ROOT: /app/.local/font-packs
|
||||
volumes:
|
||||
- "${MD_TO_PDF_THEME_DIR:-../.local/themes}:/app/.local/themes:ro"
|
||||
- "${MD_TO_PDF_FONT_PACK_DIR:-../.local/font-packs}:/app/.local/font-packs:ro"
|
||||
shm_size: "1gb"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
+143
-206
@@ -1,14 +1,6 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
cp,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rm,
|
||||
stat,
|
||||
writeFile
|
||||
} from "node:fs/promises";
|
||||
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";
|
||||
@@ -20,9 +12,14 @@ 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")
|
||||
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) {
|
||||
@@ -31,6 +28,10 @@ function assert(condition, message) {
|
||||
}
|
||||
}
|
||||
|
||||
function sha256(content) {
|
||||
return createHash("sha256").update(content).digest("hex").toLowerCase();
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: repositoryRoot,
|
||||
@@ -58,9 +59,7 @@ async function freePort() {
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
const port =
|
||||
typeof address === "object" && address
|
||||
? address.port
|
||||
: undefined;
|
||||
typeof address === "object" && address ? address.port : undefined;
|
||||
server.close((error) =>
|
||||
error || !port
|
||||
? reject(error ?? new Error("无法分配端口"))
|
||||
@@ -72,7 +71,7 @@ async function freePort() {
|
||||
|
||||
async function waitForHealth(origin) {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < 45; attempt += 1) {
|
||||
for (let attempt = 0; attempt < 60; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(`${origin}/api/health`);
|
||||
if (response.ok && (await response.json()).status === "ok") {
|
||||
@@ -88,19 +87,14 @@ async function waitForHealth(origin) {
|
||||
);
|
||||
}
|
||||
|
||||
function inspectFontPackMount(containerId, expectedRoot) {
|
||||
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, "容器缺少字体包挂载");
|
||||
assert(mount.RW === false, "字体包挂载不是只读");
|
||||
assert(
|
||||
path.resolve(mount.Source) === path.resolve(expectedRoot),
|
||||
`字体包挂载源不匹配:${mount.Source}`
|
||||
);
|
||||
assert(!mount, "Docker 字体包必须来自镜像层,不能依赖运行时挂载");
|
||||
}
|
||||
|
||||
async function exportDocx(origin) {
|
||||
@@ -109,8 +103,8 @@ async function exportDocx(origin) {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
markdown:
|
||||
"---\ntitle: Docker 字体包验收\n---\n\n# Docker 字体包验收\n\n这是一段可编辑中文正文。",
|
||||
fileName: "Docker 字体包验收.md",
|
||||
"---\ntitle: Docker 内置字体包验收\n---\n\n# Docker 内置字体包验收\n\n这是一段可编辑中文正文。",
|
||||
fileName: "Docker 内置字体包验收.md",
|
||||
language: "zh-CN",
|
||||
resources: [],
|
||||
exportConfig: {
|
||||
@@ -127,194 +121,137 @@ async function exportDocx(origin) {
|
||||
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
|
||||
);
|
||||
const fontPackStatus = await stat(fontPackRoot).catch(() => undefined);
|
||||
assert(
|
||||
validRootStatus?.isDirectory(),
|
||||
`未找到有效字体包根目录:${validFontPackRoot}。请先运行 npm run build:font-pack。`
|
||||
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 });
|
||||
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-")
|
||||
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),
|
||||
"镜像内字体包清单与本次受校验构建产物不一致"
|
||||
);
|
||||
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
|
||||
}
|
||||
})
|
||||
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 }
|
||||
);
|
||||
results.push(
|
||||
await runScenario({
|
||||
name: "valid",
|
||||
root: validFontPackRoot,
|
||||
expected: {
|
||||
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,
|
||||
exportDocx: true,
|
||||
minimumFontParts: 2
|
||||
}
|
||||
})
|
||||
pandocVersion: capability.detectedVersion,
|
||||
docxFontParts: fontParts.length
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
);
|
||||
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 {
|
||||
run(
|
||||
"docker",
|
||||
[...compose, "down", "--remove-orphans", "--timeout", "5"],
|
||||
{ env: environment }
|
||||
);
|
||||
await rm(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
+15
-2
@@ -90,6 +90,17 @@ SHA-256,只提取 `pandoc.exe`、`COPYING.rtf`、`COPYRIGHT.txt` 并生成
|
||||
使用 `process.resourcesPath` 下的固定程序,无需用户安装 Pandoc,显式
|
||||
开发配置和系统 PATH 降级仍保持兼容。
|
||||
|
||||
`v0.6.0` 发布收口阶段 C 已完成字体发行链收口。字体包构建器新增确定性
|
||||
ZIP 附件,Windows 独立安装器和附件统一使用 `MorphDoc-FontPack` 名称;
|
||||
Desktop 组合发行在同目录放置主安装器、字体安装器和手动 ZIP,主安装器
|
||||
仅绑定字体安装器文件名与 SHA-256,检测成功时显示默认勾选项,本体及免
|
||||
安装包均不包含字体包目录。Docker 构建上下文只放行受校验字体包根目录,
|
||||
镜像固定内置到 `/app/.local/font-packs`,Compose 已取消字体运行时挂载。
|
||||
真实 Docker 门禁确认镜像内清单哈希一致、无字体卷、Pandoc 为 3.9.0.2、
|
||||
WOFF2 可用,生产 DOCX 含 5 个字体部件。当前镜像大小为 711,082,692
|
||||
字节;Desktop 主安装器为 140,279,447 字节,独立字体安装器为
|
||||
24,488,770 字节,字体 ZIP 为 29,186,293 字节。
|
||||
|
||||
`v0.6.0` 阶段 3 已完成 DOCX 共享协议:`packages/core` 统一定义请求
|
||||
Schema、资源、Pandoc capability、错误码、结果、诊断、耗时、MIME 和
|
||||
安全文件名;`packages/application` 新增 `prepareDocxExport()`,复用
|
||||
@@ -1416,8 +1427,8 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
||||
同版本不可变门禁和真实资产构建;
|
||||
- 阶段 12D-FP3-B:已完成独立用户级 NSIS 字体包、原子安装/升级/卸载、
|
||||
主安装器同目录哈希校验和显式静默开关;
|
||||
- 阶段 12D-FP3-C:已完成 Docker v0.6.0 DOCX/Pandoc 运行链、字体包只读
|
||||
可选挂载及空目录、有效包、损坏包三场景真实部署门禁;
|
||||
- 阶段 12D-FP3-C:已完成 Docker v0.6.0 DOCX/Pandoc 运行链和早期可选
|
||||
字体挂载门禁;发布收口阶段 C 已进一步改为镜像内置字体并移除运行时卷;
|
||||
- 阶段 12D-R4-2:已完成 14 套主题的标准结构基线、可选字体包、真实
|
||||
Server HTTP、Chromium PDF 与生产 DOCX 矩阵;
|
||||
- 阶段 12D-R4 媒体专项:已完成稳定 PNG 布局、严格 DrawingML、五媒体
|
||||
@@ -1429,6 +1440,8 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
||||
文件关联兼容和旧版用户数据迁移;
|
||||
- 发布收口阶段 B:已完成 Desktop 固定 Pandoc 最小运行时的清单、下载
|
||||
哈希门禁、许可证保留、打包接入、离线复验和真实 DOCX 冒烟;
|
||||
- 发布收口阶段 C:已完成确定性字体 ZIP、MorphDoc 字体安装器命名、
|
||||
Desktop 同目录可选附件、Docker 镜像内置字体和真实 DOCX 发行门禁;
|
||||
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
||||
|
||||
每个阶段验收通过后创建一个独立提交,再进入下一阶段。当前阶段不得混入
|
||||
|
||||
+29
-8
@@ -1,7 +1,8 @@
|
||||
# 可选字体包构建
|
||||
|
||||
本目录只保存字体包配方、许可证和来源说明,不保存字体二进制。主程序、
|
||||
Desktop 安装包和 Docker 主镜像不会因为这些配方增加字体体积。
|
||||
本目录只保存字体包配方、许可证和来源说明,不保存字体二进制。普通
|
||||
Desktop 主安装包不包含字体;组合 Desktop 发行把字体作为同目录可选附件,
|
||||
Docker 发行构建则把本次受校验的字体包写入镜像层。
|
||||
|
||||
## 源目录
|
||||
|
||||
@@ -59,7 +60,7 @@ npm run build:font-pack:windows
|
||||
|
||||
```text
|
||||
output/font-packs/windows/
|
||||
└── md-to-pdf-font-pack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe
|
||||
└── MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe
|
||||
```
|
||||
|
||||
安装器使用当前用户权限,将整个 `mdtp-serif-sc` ID 安装到:
|
||||
@@ -81,16 +82,36 @@ output/font-packs/windows/
|
||||
npm run desktop:make:with-font-pack
|
||||
```
|
||||
|
||||
该命令把主安装器和字体包安装器作为两个同目录文件发行。主安装器只编译
|
||||
字体包文件名与 SHA-256,不嵌入字体二进制;仅当同目录文件存在且哈希完全
|
||||
一致时,交互安装向导才显示默认勾选的字体包选项。字体包失败不回滚已安装
|
||||
的主程序。
|
||||
该命令把主安装器、字体包安装器和字体包 ZIP 作为同目录文件发行。主安装器
|
||||
只编译字体包安装器的文件名与 SHA-256,不嵌入字体二进制;仅当同目录文件
|
||||
存在且哈希完全一致时,交互安装向导才显示默认勾选的字体包选项。字体包
|
||||
失败不回滚已安装的主程序。
|
||||
|
||||
主安装器静默运行时默认不安装伴随字体包;只有显式传入以下参数才会安装:
|
||||
|
||||
```powershell
|
||||
md-to-pdf-<version>-x86_64-Setup.exe /S /WITHFONTPACK=1
|
||||
MorphDoc-<version>-x86_64-Setup.exe /S /WITHFONTPACK=1
|
||||
```
|
||||
|
||||
普通 `npm run make -w @md-to-pdf/desktop` 不启用伴随逻辑,避免本地残留的
|
||||
生成文件影响常规桌面构建。
|
||||
|
||||
## ZIP 发行附件
|
||||
|
||||
独立生成可手动部署的确定性 ZIP:
|
||||
|
||||
```powershell
|
||||
npm run build:font-pack:archive
|
||||
```
|
||||
|
||||
产物为 `output/font-packs/release/MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip`。
|
||||
归档顶层已经包含 `mdtp-serif-sc/1.0.0/`,用户可直接解压到
|
||||
`%LOCALAPPDATA%/md-to-pdf/font-packs`。ZIP 内保留清单、许可证、来源说明
|
||||
以及 Web/DOCX 两类字体资产;相同输入会生成逐字节一致的附件。
|
||||
|
||||
## Docker 内置发行
|
||||
|
||||
Docker 构建前必须先执行 `npm run build:font-pack`。构建上下文只放行
|
||||
`output/font-packs/root`,Dockerfile 将其复制到
|
||||
`/app/.local/font-packs`;Compose 不再挂载宿主机字体目录。字体因此随
|
||||
镜像离线分发,同时仍由运行时注册器校验版本、路径、容量和 SHA-256。
|
||||
|
||||
Generated
+1
@@ -10034,6 +10034,7 @@
|
||||
"dependencies": {
|
||||
"@md-to-pdf/docx-engine": "0.1.0",
|
||||
"@md-to-pdf/font-pack-registry": "0.1.0",
|
||||
"fflate": "0.8.3",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+2
-1
@@ -30,7 +30,8 @@
|
||||
"verify:font-pack": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/font-pack-builder && node packages/font-pack-builder/dist/cli.js verify",
|
||||
"verify:docker-font-pack": "npm run verify:font-pack && npm run build:web-runtime && npm run build -w @md-to-pdf/server && node deploy/verify-font-pack-compose.mjs",
|
||||
"build:font-pack:windows": "npm run verify:font-pack && node packages/font-pack-builder/dist/cli.js windows-installer",
|
||||
"desktop:make:with-font-pack": "npm run build:font-pack:windows && node scripts/build-desktop-with-font-pack.mjs",
|
||||
"build:font-pack:archive": "npm run verify:font-pack && node packages/font-pack-builder/dist/cli.js archive",
|
||||
"desktop:make:with-font-pack": "npm run build:font-pack:windows && npm run build:font-pack:archive && node scripts/build-desktop-with-font-pack.mjs",
|
||||
"theme:import-typora": "node scripts/import-typora-theme.mjs",
|
||||
"verify:docx-reference": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:pandoc -w @md-to-pdf/docx-engine",
|
||||
"verify:docx-conversion": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dependencies": {
|
||||
"@md-to-pdf/docx-engine": "0.1.0",
|
||||
"@md-to-pdf/font-pack-registry": "0.1.0",
|
||||
"fflate": "0.8.3",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { lstat, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, posix, resolve } from "node:path";
|
||||
import { zipSync } from "fflate";
|
||||
import { verifyFontPack } from "./builder.js";
|
||||
|
||||
const ARCHIVE_MTIME = new Date("1980-01-01T00:00:00.000Z");
|
||||
|
||||
export interface BuildFontPackArchiveOptions {
|
||||
packRoot: string;
|
||||
appVersion: string;
|
||||
packId: string;
|
||||
packVersion: string;
|
||||
outputDirectory: string;
|
||||
reportPath?: string;
|
||||
}
|
||||
|
||||
export interface FontPackArchiveReport {
|
||||
packId: string;
|
||||
packVersion: string;
|
||||
artifactPath: string;
|
||||
artifactName: string;
|
||||
bytes: number;
|
||||
sha256: string;
|
||||
entries: string[];
|
||||
}
|
||||
|
||||
function sha256(content: Uint8Array) {
|
||||
return createHash("sha256").update(content).digest("hex").toUpperCase();
|
||||
}
|
||||
|
||||
export function fontPackArchiveName(packId: string, version: string) {
|
||||
return `MorphDoc-FontPack-${packId}-${version}.zip`;
|
||||
}
|
||||
|
||||
async function collectFiles(directory: string, prefix: string) {
|
||||
const result: Array<{ name: string; content: Uint8Array }> = [];
|
||||
const visit = async (currentDirectory: string, relativeDirectory: string) => {
|
||||
const entries = await readdir(currentDirectory, { withFileTypes: true });
|
||||
entries.sort((left, right) => left.name.localeCompare(right.name, "en"));
|
||||
for (const entry of entries) {
|
||||
const entryPath = join(currentDirectory, entry.name);
|
||||
const status = await lstat(entryPath);
|
||||
if (status.isSymbolicLink()) {
|
||||
throw new Error(`字体包归档不允许符号链接:${entry.name}`);
|
||||
}
|
||||
const relativePath = posix.join(
|
||||
prefix,
|
||||
relativeDirectory.replaceAll("\\", "/"),
|
||||
entry.name
|
||||
);
|
||||
if (status.isDirectory()) {
|
||||
await visit(entryPath, posix.join(relativeDirectory, entry.name));
|
||||
} else if (status.isFile()) {
|
||||
result.push({ name: relativePath, content: await readFile(entryPath) });
|
||||
} else {
|
||||
throw new Error(`字体包归档只允许普通文件:${entry.name}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
await visit(directory, "");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function createDeterministicFontPackArchive(
|
||||
packDirectory: string,
|
||||
prefix: string
|
||||
) {
|
||||
const files = await collectFiles(resolve(packDirectory), prefix);
|
||||
const entries: Record<string, Uint8Array> = {};
|
||||
for (const file of files) {
|
||||
entries[file.name] = file.content;
|
||||
}
|
||||
return {
|
||||
content: zipSync(entries, { level: 9, mtime: ARCHIVE_MTIME }),
|
||||
entries: files.map((file) => file.name)
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildFontPackArchive(
|
||||
options: BuildFontPackArchiveOptions
|
||||
): Promise<FontPackArchiveReport> {
|
||||
const verified = await verifyFontPack({
|
||||
root: options.packRoot,
|
||||
appVersion: options.appVersion,
|
||||
packId: options.packId,
|
||||
packVersion: options.packVersion
|
||||
});
|
||||
const archive = await createDeterministicFontPackArchive(
|
||||
verified.outputDirectory,
|
||||
`${verified.packId}/${verified.packVersion}`
|
||||
);
|
||||
const outputDirectory = resolve(options.outputDirectory);
|
||||
await mkdir(outputDirectory, { recursive: true });
|
||||
const artifactName = fontPackArchiveName(
|
||||
verified.packId,
|
||||
verified.packVersion
|
||||
);
|
||||
const artifactPath = join(outputDirectory, artifactName);
|
||||
await writeFile(artifactPath, archive.content);
|
||||
const report: FontPackArchiveReport = {
|
||||
packId: verified.packId,
|
||||
packVersion: verified.packVersion,
|
||||
artifactPath,
|
||||
artifactName,
|
||||
bytes: archive.content.byteLength,
|
||||
sha256: sha256(archive.content),
|
||||
entries: archive.entries
|
||||
};
|
||||
if (options.reportPath) {
|
||||
const reportPath = resolve(options.reportPath);
|
||||
await mkdir(dirname(reportPath), { recursive: true });
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
}
|
||||
return report;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { resolve } from "node:path";
|
||||
import { buildFontPackArchive } from "./archive.js";
|
||||
import { buildFontPack, verifyFontPack } from "./builder.js";
|
||||
import { buildWindowsFontPackInstaller } from "./windows-installer.js";
|
||||
|
||||
@@ -86,7 +87,25 @@ async function main() {
|
||||
)
|
||||
});
|
||||
}
|
||||
throw new Error("命令必须是 build、verify 或 windows-installer");
|
||||
if (command === "archive") {
|
||||
return buildFontPackArchive({
|
||||
packRoot: resolve(option(values, "root", "output/font-packs/root")),
|
||||
appVersion: option(values, "app-version", "0.6.0"),
|
||||
packId: option(values, "pack-id", "mdtp-serif-sc"),
|
||||
packVersion: option(values, "pack-version", "1.0.0"),
|
||||
outputDirectory: resolve(
|
||||
option(values, "output", "output/font-packs/release")
|
||||
),
|
||||
reportPath: resolve(
|
||||
option(
|
||||
values,
|
||||
"report",
|
||||
"output/font-packs/archive-report.json"
|
||||
)
|
||||
)
|
||||
});
|
||||
}
|
||||
throw new Error("命令必须是 build、verify、archive 或 windows-installer");
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./archive.js";
|
||||
export * from "./builder.js";
|
||||
export * from "./recipe.js";
|
||||
export * from "./windows-installer.js";
|
||||
|
||||
@@ -55,7 +55,7 @@ function define(name: string, value: string) {
|
||||
}
|
||||
|
||||
export function windowsFontPackInstallerName(packId: string, version: string) {
|
||||
return `md-to-pdf-font-pack-${packId}-${version}-${WINDOWS_FONT_PACK_ARCHITECTURE}-Setup.exe`;
|
||||
return `MorphDoc-FontPack-${packId}-${version}-${WINDOWS_FONT_PACK_ARCHITECTURE}-Setup.exe`;
|
||||
}
|
||||
|
||||
export function createWindowsFontPackInstallerArguments(options: {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { unzipSync } from "fflate";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
createDeterministicFontPackArchive,
|
||||
fontPackArchiveName
|
||||
} from "../src/index.js";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories.splice(0).map((directory) =>
|
||||
rm(directory, { recursive: true, force: true })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
describe("字体包 ZIP 发行附件", () => {
|
||||
it("使用 MorphDoc 品牌和稳定文件名", () => {
|
||||
expect(fontPackArchiveName("mdtp-serif-sc", "1.0.0")).toBe(
|
||||
"MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip"
|
||||
);
|
||||
});
|
||||
|
||||
it("生成可复现且可直接解压到字体包根目录的归档", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "morphdoc-font-archive-"));
|
||||
temporaryDirectories.push(root);
|
||||
await mkdir(join(root, "fonts"), { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(join(root, "font-pack.json"), "{}\n", "utf8"),
|
||||
writeFile(join(root, "fonts", "Regular.ttf"), new Uint8Array([1, 2, 3]))
|
||||
]);
|
||||
|
||||
const first = await createDeterministicFontPackArchive(
|
||||
root,
|
||||
"mdtp-serif-sc/1.0.0"
|
||||
);
|
||||
const second = await createDeterministicFontPackArchive(
|
||||
root,
|
||||
"mdtp-serif-sc/1.0.0"
|
||||
);
|
||||
expect(first.content).toEqual(second.content);
|
||||
expect(first.entries).toEqual([
|
||||
"mdtp-serif-sc/1.0.0/font-pack.json",
|
||||
"mdtp-serif-sc/1.0.0/fonts/Regular.ttf"
|
||||
]);
|
||||
const entries = unzipSync(first.content);
|
||||
expect(new TextDecoder().decode(entries[first.entries[0]!])).toBe("{}\n");
|
||||
expect(entries[first.entries[1]!]).toEqual(new Uint8Array([1, 2, 3]));
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ describe("Windows 字体包安装器构建", () => {
|
||||
|
||||
it("生成稳定发行文件名和完整 NSIS 定义", () => {
|
||||
expect(windowsFontPackInstallerName("mdtp-serif-sc", "1.0.0")).toBe(
|
||||
"md-to-pdf-font-pack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe"
|
||||
"MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe"
|
||||
);
|
||||
const args = createWindowsFontPackInstallerArguments({
|
||||
packDirectory: "C:/pack",
|
||||
|
||||
@@ -10,7 +10,8 @@ const desktopManifest = JSON.parse(
|
||||
await readFile(join(desktopRoot, "package.json"), "utf8")
|
||||
);
|
||||
const fontInstallerName =
|
||||
"md-to-pdf-font-pack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe";
|
||||
"MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe";
|
||||
const fontArchiveName = "MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip";
|
||||
const fontInstallerPath = join(
|
||||
repositoryRoot,
|
||||
"output",
|
||||
@@ -24,6 +25,13 @@ const generatedInclude = join(
|
||||
"font-packs",
|
||||
"desktop-companion.nsh"
|
||||
);
|
||||
const fontArchivePath = join(
|
||||
repositoryRoot,
|
||||
"output",
|
||||
"font-packs",
|
||||
"release",
|
||||
fontArchiveName
|
||||
);
|
||||
|
||||
await createDesktopCompanionInclude({
|
||||
installerPath: fontInstallerPath,
|
||||
@@ -66,6 +74,7 @@ await new Promise((resolvePromise, reject) => {
|
||||
const desktopOutput = join(desktopRoot, "out", `v${desktopManifest.version}`);
|
||||
await mkdir(desktopOutput, { recursive: true });
|
||||
await copyFile(fontInstallerPath, join(desktopOutput, fontInstallerName));
|
||||
await copyFile(fontArchivePath, join(desktopOutput, fontArchiveName));
|
||||
process.stdout.write(
|
||||
`主安装器与字体包伴随安装器已生成:${desktopOutput}\n`
|
||||
`主安装器、字体包伴随安装器与 ZIP 附件已生成:${desktopOutput}\n`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user