新增能力:桌面端新增 macOS 打包支持,electron-builder 增加独立 mac 配置块, 产出 dmg,按 arm64、x64 分别单独构建、不产出 universal 包;Pandoc 运行时清单 新增 darwin/arm64、darwin/x64 两个官方发行项(含独立许可证文件来源),桌面 Pandoc 候选路径与 prepare-pandoc-runtime.mjs 均已泛化为按 --platform/--arch 参数选取目标,不再只认 Windows x64。AGENTS.md 不再硬编码 Windows 绝对路径与 强制 PowerShell 语法,改为按当前 Shell 与仓库实际位置自适应。 问题修复:修复解压内置 Pandoc 时未保留 Unix 可执行权限位的问题(unzipSync 不保留压缩包内权限,已补 chmod 0o755);修复根 package.json 中 build:web-runtime/dev/desktop:dev 三个脚本的构建顺序错误 (@md-to-pdf/application 被排在其依赖的 @md-to-pdf/preview-engine 之前,在 没有历史 dist 残留的全新环境中会导致类型解析失败);修正 packages/docx-engine/tests/pandoc-runtime.test.ts 与 apps/desktop/tests/markdown-file.test.ts 中依赖宿主 OS 或路径分隔符的测试 断言,避免这些用例只能在特定平台上通过。 验证结果:docx-engine 包 93 项测试、desktop 包 62 项测试(61 通过 + 1 项 Windows 专属用例按预期跳过)均通过;全项目类型检查、生产构建通过, git diff --check 无空白错误。跳过依赖 Git 忽略的真实客户文档语料(tmp/)的 test:docx-real-world-corpus 与 test:docx-release-gate-suites 后,其余全部 工作区测试均通过;这两步需要接入真实语料的机器上单独执行。本机 macOS (Apple Silicon)实测 npm run package:mac:arm64 全链路成功,内置 Pandoc 3.9.0.2 完成下载、哈希校验与真实 DOCX 冒烟转换;实际截图确认窗口、macOS 原生菜单栏、编辑器工具栏、中文字体与实时预览渲染正常。 兼容与部署:版本统一为 0.6.4。本版本仅完成开发基础设施与验证,未构建正式 发行文件:没有产出真实签名的 dmg、没有重新构建 Windows 安装包,也没有重新 构建 Docker 镜像;正式 macOS 发行产物与 Windows/Docker 同步验证留待 DOCX 发布门禁阶段完成后一并发布。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5N6pcbjV4jVfXrcH3NcLP
266 lines
8.6 KiB
TypeScript
266 lines
8.6 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
interface PackageManifest {
|
|
version?: string;
|
|
productName?: string;
|
|
scripts?: Record<string, string>;
|
|
}
|
|
|
|
interface ProductBrand {
|
|
appId: string;
|
|
englishName: string;
|
|
displayName: string;
|
|
windowTitle: string;
|
|
executableName: string;
|
|
artifactPrefix: string;
|
|
}
|
|
|
|
interface PandocRuntimeManifest {
|
|
version: string;
|
|
artifacts: Array<{
|
|
platform: string;
|
|
architecture: string;
|
|
sha256: string;
|
|
licenseFiles: string[];
|
|
files?: Record<string, { bytes: number; sha256: string }>;
|
|
}>;
|
|
}
|
|
|
|
const desktopRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
const projectRoot = fileURLToPath(new URL("../../../", import.meta.url));
|
|
|
|
function readManifest<T = PackageManifest>(path: string): T {
|
|
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
}
|
|
|
|
describe("桌面发行构建链", () => {
|
|
it("打包前按依赖顺序完整重建内嵌 Web", () => {
|
|
const rootManifest = readManifest(`${projectRoot}/package.json`);
|
|
const desktopManifest = readManifest(`${desktopRoot}/package.json`);
|
|
const webBuild = rootManifest.scripts?.["build:web-runtime"] ?? "";
|
|
const expectedWorkspaces = [
|
|
"@md-to-pdf/markdown-echarts",
|
|
"@md-to-pdf/core",
|
|
"@md-to-pdf/renderer",
|
|
"@md-to-pdf/preview-engine",
|
|
"@md-to-pdf/application",
|
|
"@md-to-pdf/web"
|
|
];
|
|
|
|
let previousIndex = -1;
|
|
for (const workspace of expectedWorkspaces) {
|
|
const workspaceIndex = webBuild.indexOf(workspace);
|
|
expect(workspaceIndex).toBeGreaterThan(previousIndex);
|
|
previousIndex = workspaceIndex;
|
|
}
|
|
|
|
expect(desktopManifest.scripts?.["build:embedded-web"]).toBe(
|
|
"npm --prefix ../.. run build:web-runtime"
|
|
);
|
|
expect(desktopManifest.scripts?.package).toMatch(
|
|
/^npm run build:embedded-web && /
|
|
);
|
|
expect(desktopManifest.scripts?.make).toMatch(
|
|
/^npm run build:embedded-web && /
|
|
);
|
|
expect(desktopManifest.scripts?.package).toContain(
|
|
"npm run prepare:pandoc"
|
|
);
|
|
expect(desktopManifest.scripts?.make).toContain(
|
|
"npm run prepare:pandoc"
|
|
);
|
|
expect(rootManifest.scripts?.["desktop:package"]).toBe(
|
|
"npm run build:font-pack && npm run package -w @md-to-pdf/desktop"
|
|
);
|
|
expect(rootManifest.scripts?.["desktop:make"]).toBe(
|
|
"npm run build:font-pack && node scripts/build-desktop-release.mjs"
|
|
);
|
|
expect(rootManifest.scripts?.["desktop:make:with-font-pack"]).toBeUndefined();
|
|
});
|
|
|
|
it("将固定 Pandoc 最小运行时作为 Desktop 独立资源打包", () => {
|
|
const desktopManifest = readManifest(`${desktopRoot}/package.json`);
|
|
const runtimeManifest = readManifest<PandocRuntimeManifest>(
|
|
`${projectRoot}/packages/docx-engine/src/pandoc-runtime.json`
|
|
);
|
|
const builderConfig = readFileSync(
|
|
`${desktopRoot}/electron-builder.config.cjs`,
|
|
"utf8"
|
|
);
|
|
const prepareScript = readFileSync(
|
|
`${desktopRoot}/scripts/prepare-pandoc-runtime.mjs`,
|
|
"utf8"
|
|
);
|
|
const windowsArtifact = runtimeManifest.artifacts.find(
|
|
(artifact) =>
|
|
artifact.platform === "win32" && artifact.architecture === "x64"
|
|
);
|
|
|
|
expect(runtimeManifest.version).toBe("3.9.0.2");
|
|
expect(windowsArtifact).toMatchObject({
|
|
sha256:
|
|
"C97542F2800F446E788D9F74237856D995421AD1BB3CC8324286840C5F272D3A",
|
|
licenseFiles: ["COPYING.rtf", "COPYRIGHT.txt"],
|
|
files: {
|
|
"pandoc.exe": {
|
|
bytes: 231056136,
|
|
sha256:
|
|
"E83F8354C0F507222B5684797B9C5AE766F03889785995D14AAC27816EC456BA"
|
|
}
|
|
}
|
|
});
|
|
expect(desktopManifest.scripts?.["prepare:pandoc"]).toBe(
|
|
"node scripts/prepare-pandoc-runtime.mjs"
|
|
);
|
|
expect(builderConfig).toContain(
|
|
"`.runtime/pandoc/${pandocRuntime.version}/windows-x86_64`"
|
|
);
|
|
expect(builderConfig).toContain('"pandoc.exe"');
|
|
expect(builderConfig).toContain('"COPYING.rtf"');
|
|
expect(builderConfig).toContain('"COPYRIGHT.txt"');
|
|
expect(prepareScript).toContain("sha256(downloaded)");
|
|
expect(prepareScript).toContain("smokeTestPandoc");
|
|
expect(prepareScript).toContain('process.argv.includes("--check")');
|
|
});
|
|
|
|
it("将内置主题示例作为独立资源复制到发行目录", () => {
|
|
const builderConfig = readFileSync(
|
|
`${desktopRoot}/electron-builder.config.cjs`,
|
|
"utf8"
|
|
);
|
|
|
|
expect(builderConfig).toContain('from: "../../samples"');
|
|
expect(builderConfig).toContain('to: "samples"');
|
|
});
|
|
|
|
it("保持安装身份并从统一配置生成 MorphDoc 发行名称", () => {
|
|
const brand = readManifest<ProductBrand>(
|
|
`${projectRoot}/product.json`
|
|
);
|
|
const desktopManifest = readManifest(
|
|
`${desktopRoot}/package.json`
|
|
);
|
|
const builderConfig = readFileSync(
|
|
`${desktopRoot}/electron-builder.config.cjs`,
|
|
"utf8"
|
|
);
|
|
const installerInclude = readFileSync(
|
|
`${desktopRoot}/build/installer.nsh`,
|
|
"utf8"
|
|
);
|
|
|
|
expect(brand).toMatchObject({
|
|
appId: "com.md-to-pdf.desktop",
|
|
englishName: "MorphDoc",
|
|
displayName: "墨呈",
|
|
windowTitle: "墨呈 · MorphDoc",
|
|
executableName: "MorphDoc",
|
|
artifactPrefix: "MorphDoc"
|
|
});
|
|
expect(desktopManifest).toMatchObject({
|
|
version: "0.6.4",
|
|
productName: "MorphDoc"
|
|
});
|
|
expect(builderConfig).toContain("appId: brand.appId");
|
|
expect(builderConfig).toContain("productName: brand.englishName");
|
|
expect(builderConfig).toContain("executableName: brand.executableName");
|
|
expect(builderConfig).toContain("shortcutName: brand.displayName");
|
|
expect(installerInclude).toContain(
|
|
'!define MORPHDOC_EXECUTABLE "MorphDoc.exe"'
|
|
);
|
|
expect(installerInclude).toContain(
|
|
'!define MORPHDOC_DISPLAY_NAME "墨呈"'
|
|
);
|
|
expect(installerInclude).toContain(
|
|
'!define INSTALL_REGISTRY_KEY "Software\\${APP_GUID}.MorphDoc"'
|
|
);
|
|
expect(installerInclude).toContain(
|
|
'DeleteRegKey SHCTX "${MORPHDOC_LEGACY_INSTALL_REGISTRY_KEY}"'
|
|
);
|
|
expect(installerInclude).toContain(
|
|
"Software\\Classes\\Applications\\${MORPHDOC_LEGACY_EXECUTABLE}"
|
|
);
|
|
});
|
|
|
|
it("在安装版与便携版中强制内置完整字体包集合", () => {
|
|
const builderConfig = readFileSync(
|
|
`${desktopRoot}/electron-builder.config.cjs`,
|
|
"utf8"
|
|
);
|
|
const desktopMain = readFileSync(
|
|
`${desktopRoot}/src/main.ts`,
|
|
"utf8"
|
|
);
|
|
const bundledBuild = readFileSync(
|
|
`${projectRoot}/scripts/build-desktop-release.mjs`,
|
|
"utf8"
|
|
);
|
|
const installerInclude = readFileSync(
|
|
`${desktopRoot}/build/installer.nsh`,
|
|
"utf8"
|
|
);
|
|
|
|
expect(builderConfig).toContain('../../output/font-packs/root');
|
|
expect(builderConfig).toContain('to: "font-packs"');
|
|
expect(desktopMain).toContain(
|
|
'path.join(process.resourcesPath, "font-packs")'
|
|
);
|
|
expect(desktopMain).toContain("getDesktopUserFontPackDirectory()");
|
|
expect(bundledBuild).not.toContain("createDesktopCompanionInclude");
|
|
expect(bundledBuild).toContain("已生成字体直接内置");
|
|
expect(installerInclude).not.toContain("FONT_PACK_COMPANION");
|
|
expect(installerInclude).not.toContain("FontPackCompanionPage");
|
|
expect(installerInclude).not.toContain("/WITHFONTPACK=");
|
|
});
|
|
|
|
it("将 DOCX Lua Filter 复制到桌面主进程资源目录", () => {
|
|
const buildScript = readFileSync(
|
|
`${desktopRoot}/scripts/build-main.mjs`,
|
|
"utf8"
|
|
);
|
|
|
|
expect(buildScript).toContain(
|
|
"packages/docx-engine/assets/docx-media-filter.lua"
|
|
);
|
|
expect(buildScript).toContain("dist/docx-media-filter.lua");
|
|
});
|
|
|
|
it("为 ESM 主进程中的 CommonJS 字体转换依赖保留 __dirname", () => {
|
|
const buildScript = readFileSync(
|
|
`${desktopRoot}/scripts/build-main.mjs`,
|
|
"utf8"
|
|
);
|
|
|
|
expect(buildScript).toContain(
|
|
'dirname as __mdToPdfDirname'
|
|
);
|
|
expect(buildScript).toContain(
|
|
'fileURLToPath as __mdToPdfFileURLToPath'
|
|
);
|
|
expect(buildScript).toContain(
|
|
"const __dirname = __mdToPdfDirname(__mdToPdfFileURLToPath(import.meta.url));"
|
|
);
|
|
});
|
|
|
|
it("在 Docker Web 构建前复制样例并保留到运行镜像", () => {
|
|
const dockerfile = readFileSync(
|
|
`${projectRoot}/deploy/Dockerfile`,
|
|
"utf8"
|
|
);
|
|
const samplesBuildCopyIndex = dockerfile.indexOf(
|
|
"COPY samples ./samples"
|
|
);
|
|
const webBuildIndex = dockerfile.indexOf(
|
|
"npm run build -w @md-to-pdf/web"
|
|
);
|
|
|
|
expect(samplesBuildCopyIndex).toBeGreaterThan(-1);
|
|
expect(webBuildIndex).toBeGreaterThan(samplesBuildCopyIndex);
|
|
expect(dockerfile).toContain(
|
|
"COPY --chown=pwuser:pwuser --from=build /app/samples ./samples"
|
|
);
|
|
});
|
|
});
|