diff --git a/AGENTS.md b/AGENTS.md index 9c23e29..6f2e9fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,34 +13,28 @@ ## 2. 会话启动检查 -新会话、上下文压缩后或对当前目录不确定时,在执行其他 Shell 命令前先运行: - -```powershell -Get-Location -``` - -确认目录为: +新会话、上下文压缩后或对当前目录不确定时,在执行其他 Shell 命令前先确认仓库根目录。不得假设固定绝对路径或盘符——协作环境可能是 Windows、macOS 或 Linux,克隆位置因机器而异。可用如下只读方式确认(按当前 Shell 语法改写等价命令): ```text -C:\Projects\md-to-pdf +git rev-parse --show-toplevel ``` 随后依次执行只读检查: -```powershell +```text git status --short git log --oneline -5 -Get-Content -LiteralPath 'docs\PROGRESS.md' -Encoding UTF8 +读取 docs/PROGRESS.md(显式使用 UTF-8 编码) ``` 当前工作区可能包含用户或上一个会话留下的未提交修改。禁止使用 `git reset --hard`、`git checkout --`、`git clean` 等方式丢弃修改,除非用户明确要求。 ## 3. Shell 与文件操作 -- 当前默认 Shell 为 PowerShell,使用 PowerShell 语法。 +- 不假设固定 Shell 类型(PowerShell、bash、zsh 等均可能);命令语法跟随当前会话实际使用的 Shell,禁止把某一种 Shell 的专属语法当作硬性要求写入产物、脚本或文档。 - 不要通过绝对路径 `cd` 前缀拼接命令;工具支持时优先使用 `workdir`。 - 子目录操作使用相对路径或直接设置 `workdir`。 -- PowerShell 读取、写入、追加或替换文本时显式指定 UTF-8 编码。 +- 读取、写入、追加或替换文本文件时显式指定 UTF-8 编码。 - 本地文件修改优先使用补丁工具,避免使用不透明的大段覆盖命令。 - 不执行破坏性删除;如确需删除,先确认精确目标并向用户说明。 @@ -150,19 +144,19 @@ docs/ 进度、架构和部署文档 安装依赖: -```powershell +```text npm install ``` 本地开发: -```powershell +```text npm run dev ``` 完整验证: -```powershell +```text npm test npm run typecheck npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md index a70e4a0..2ad6190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,52 @@ Gitea Release 页面正文的基础;构建产物、校验和及详细验收记 - 暂无。 +## [0.6.4] - 2026-08-26 + +### 新增 + +- 桌面端新增 macOS 打包能力:`electron-builder` 增加独立 `mac` 配置块,产出 + `dmg`,按 arm64、x64 分别单独构建,不产出 universal 包(内置完整 + Chromium/Electron 与固定版本 Pandoc,合并双架构会让单包体积翻倍)。 +- Pandoc 运行时清单新增 `darwin/arm64`、`darwin/x64` 两个官方发行项及独立 + 许可证文件来源(macOS 官方 zip 本身不随附许可证文本);桌面 Pandoc 候选 + 路径与 `prepare-pandoc-runtime.mjs` 均已泛化为按 `--platform`/`--arch` + 参数选取目标,不再只认 Windows x64。 + +### 修复 + +- 修复 `apps/desktop/scripts/prepare-pandoc-runtime.mjs` 解压内置 Pandoc 时 + 未保留 Unix 可执行权限位的问题(`unzipSync` 不保留压缩包内的权限信息), + macOS/Linux 上解压出的二进制此前无法直接执行。 +- 修复根 `package.json` 中 `build:web-runtime`、`dev`、`desktop:dev` 三个 + 脚本的构建顺序错误:`@md-to-pdf/application` 依赖 + `@md-to-pdf/preview-engine`,但被排在其构建之前,在没有历史 `dist/` + 残留的全新环境(如全新克隆 + 全新 `npm install`)中会导致类型解析失败。 +- 修正 `AGENTS.md` 中硬编码的 Windows 绝对路径与强制 PowerShell 语法,改为 + 按当前 Shell 与仓库实际位置自适应。 +- 修正测试用例 `packages/docx-engine/tests/pandoc-runtime.test.ts` 中硬编码 + 反斜杠路径分隔符的断言,避免该测试仅能在 Windows 宿主上通过。 + +### 验证 + +- `docx-engine` 包 15 个测试文件、93 项测试通过;类型检查通过。 +- 本机 macOS(Apple Silicon)实测:`npm run package:mac:arm64` 全链路成功, + 内置 darwin-arm64 版 Pandoc 3.9.0.2 完成下载、哈希校验与真实 DOCX 冒烟 + 转换;未打包应用(`--dir`)可正常启动,主进程/渲染进程/GPU 进程/网络 + 服务进程均稳定运行,AppleScript 可正常触发退出。实际截图确认窗口、 + macOS 原生菜单栏、Markdown 编辑器工具栏、中文字体与实时预览(标题、 + 任务列表、表格、行内公式、引用块)均渲染正常。 +- 补跑跳过真实语料依赖的全部工作区测试与全项目类型检查、生产构建均 + 通过;`test:docx-real-world-corpus` 与 `test:docx-release-gate-suites` + 依赖 Git 忽略的真实客户文档语料(`tmp/`),本机没有该目录,这两步 + 在当前环境下无法验证,留待接入真实语料的机器上执行。 + +### 兼容与部署 + +- 本版本仅完成开发基础设施与验证,未构建正式发行文件:没有产出真实签名 + 的 dmg、没有重新构建 Windows 安装包,也没有重新构建 Docker 镜像;正式 + macOS 发行产物、Windows/Docker 同步验证留待后续门禁阶段完成后一并发布。 + ## [0.6.2] - 2026-08-26 ### 修复 @@ -221,7 +267,8 @@ Gitea Release 页面正文的基础;构建产物、校验和及详细验收记 - 支持真实分页预览、固定 Chromium PDF 导出和 PDF 精确预览。 - 支持长文档分页、页码导航、滚动同步以及 AIO Docker 容器部署。 -[Unreleased]: https://gitea.rk-health.com/yixiong/MorphDoc/compare/v0.6.2...main +[Unreleased]: https://gitea.rk-health.com/yixiong/MorphDoc/compare/v0.6.4...main +[0.6.4]: https://gitea.rk-health.com/yixiong/MorphDoc/releases/tag/v0.6.4 [0.6.2]: https://gitea.rk-health.com/yixiong/MorphDoc/releases/tag/v0.6.2 [0.6.1]: https://gitea.rk-health.com/yixiong/MorphDoc/releases/tag/v0.6.1 [0.6.0]: https://gitea.rk-health.com/yixiong/MorphDoc/releases/tag/v0.6.0 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 9df0559..553b7d5 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -57,8 +57,8 @@ npm run verify:pandoc-runtime -w @md-to-pdf/desktop ``` 版本化目录包、NSIS `Setup.exe` 和 ZIP 输出到 -`apps/desktop/out/v0.6.2/`。该目录被 Git 忽略。公司内部分发以 -`MorphDoc-0.6.2-x86_64-Setup.exe` 为正式安装包,ZIP 作为免安装 +`apps/desktop/out/v0.6.4/`。该目录被 Git 忽略。公司内部分发以 +`MorphDoc-0.6.4-x86_64-Setup.exe` 为正式安装包,ZIP 作为免安装 辅助包;当前未配置代码签名,Windows 首次运行可能显示“未知发布者” 提示。 diff --git a/apps/desktop/electron-builder.config.cjs b/apps/desktop/electron-builder.config.cjs index 2443f47..9c852d2 100644 --- a/apps/desktop/electron-builder.config.cjs +++ b/apps/desktop/electron-builder.config.cjs @@ -9,9 +9,25 @@ const windowsIcon = path.resolve( __dirname, "../../logos/desktop/windows/app.ico" ); +const macIcon = path.resolve( + __dirname, + "../../logos/desktop/macos/app.icns" +); const nsisInclude = process.env.MD_TO_PDF_NSIS_INCLUDE?.trim() || "build/installer.nsh"; +// macOS 每次只构建一个架构的 dmg(arm64 或 x64),不产出 universal 包—— +// 项目内置完整 Chromium/Electron 运行时和固定版本 Pandoc,合并双架构会让单个 +// 安装包体积翻倍。目标架构通过环境变量显式声明,用于挑选对应的内置 Pandoc +// 资源目录;不依赖 electron-builder 的 `${arch}` 路径宏(该宏在 extraResources +// 场景下有已知的可靠性问题)。 +const macTargetArch = process.env.MD_TO_PDF_MAC_TARGET_ARCH?.trim() || ""; +if (macTargetArch && macTargetArch !== "arm64" && macTargetArch !== "x64") { + throw new Error( + `未知 macOS 目标架构:${macTargetArch}(仅支持 arm64 或 x64)` + ); +} + module.exports = { appId: brand.appId, productName: brand.englishName, @@ -46,22 +62,12 @@ module.exports = { to: "font-packs", filter: ["**/*"] }, - { - from: `.runtime/pandoc/${pandocRuntime.version}/windows-x86_64`, - to: `pandoc/${pandocRuntime.version}/windows-x86_64`, - filter: [ - "pandoc.exe", - "COPYING.rtf", - "COPYRIGHT.txt", - "runtime-manifest.json" - ] - }, { from: windowsIcon, to: "app.ico" }, { - from: "../../logos/desktop/macos/app.icns", + from: macIcon, to: "app.icns" } ], @@ -78,7 +84,19 @@ module.exports = { ], icon: windowsIcon, executableName: brand.executableName, - artifactName: `${brand.artifactPrefix}-${version}-x86_64.\${ext}` + artifactName: `${brand.artifactPrefix}-${version}-x86_64.\${ext}`, + extraResources: [ + { + from: `.runtime/pandoc/${pandocRuntime.version}/windows-x86_64`, + to: `pandoc/${pandocRuntime.version}/windows-x86_64`, + filter: [ + "pandoc.exe", + "COPYING.rtf", + "COPYRIGHT.txt", + "runtime-manifest.json" + ] + } + ] }, nsis: { oneClick: false, @@ -94,5 +112,36 @@ module.exports = { installerLanguages: ["zh_CN"], include: nsisInclude, artifactName: `${brand.artifactPrefix}-${version}-x86_64-Setup.\${ext}` + }, + mac: { + category: "public.app-category.productivity", + icon: macIcon, + executableName: brand.executableName, + // 项目当前没有 Apple Developer 证书,显式声明不签名,与 Windows 侧 + // 一贯记录在案的"未签名"状态保持一致;后续如需公证再补齐。 + identity: null, + target: [ + { + target: "dmg" + } + ], + extraResources: macTargetArch + ? [ + { + from: `.runtime/pandoc/${pandocRuntime.version}/darwin-${macTargetArch}`, + to: `pandoc/${pandocRuntime.version}/darwin-${macTargetArch}`, + filter: [ + "pandoc", + "COPYING.md", + "COPYRIGHT", + "runtime-manifest.json" + ] + } + ] + : [] + }, + dmg: { + title: brand.displayName, + artifactName: `${brand.artifactPrefix}-${version}-\${arch}.\${ext}` } }; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 75ffc2b..bda23d9 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@md-to-pdf/desktop", - "version": "0.6.2", + "version": "0.6.4", "private": true, "productName": "MorphDoc", "description": "墨呈桌面端:面向结构化 Markdown 的主题化文档创作与发布工具", @@ -11,13 +11,20 @@ "build": "npm run clean && npm run build:main && npm run build:preload", "build:embedded-web": "npm --prefix ../.. run build:web-runtime", "prepare:pandoc": "node scripts/prepare-pandoc-runtime.mjs", + "prepare:pandoc:mac-arm64": "node scripts/prepare-pandoc-runtime.mjs --platform=darwin --arch=arm64", + "prepare:pandoc:mac-x64": "node scripts/prepare-pandoc-runtime.mjs --platform=darwin --arch=x64", "verify:pandoc-runtime": "node scripts/prepare-pandoc-runtime.mjs --check", "build:main": "node scripts/build-main.mjs", "build:preload": "esbuild src/app-preload.ts src/pdf-preload.ts --bundle --platform=node --format=cjs --external:electron --outdir=dist --out-extension:.js=.cjs", "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", "dev": "npm run build && wait-on http://localhost:5173 && electron dist/main.js --web-url=http://localhost:5173", "package": "npm run build:embedded-web && npm run prepare:pandoc && npm run build && electron-builder --dir --config electron-builder.config.cjs --x64", + "package:mac:arm64": "npm run build:embedded-web && npm run prepare:pandoc:mac-arm64 && npm run build && MD_TO_PDF_MAC_TARGET_ARCH=arm64 electron-builder --dir --config electron-builder.config.cjs --mac --arm64", + "package:mac:x64": "npm run build:embedded-web && npm run prepare:pandoc:mac-x64 && npm run build && MD_TO_PDF_MAC_TARGET_ARCH=x64 electron-builder --dir --config electron-builder.config.cjs --mac --x64", "make": "npm run build:embedded-web && npm run prepare:pandoc && npm run build && electron-builder --win nsis zip --config electron-builder.config.cjs --x64", + "make:mac:arm64": "npm run build:embedded-web && npm run prepare:pandoc:mac-arm64 && npm run build && MD_TO_PDF_MAC_TARGET_ARCH=arm64 electron-builder --mac dmg --config electron-builder.config.cjs --arm64", + "make:mac:x64": "npm run build:embedded-web && npm run prepare:pandoc:mac-x64 && npm run build && MD_TO_PDF_MAC_TARGET_ARCH=x64 electron-builder --mac dmg --config electron-builder.config.cjs --x64", + "make:mac": "npm run make:mac:arm64 && npm run make:mac:x64", "test": "vitest run", "typecheck": "tsc -p tsconfig.json --noEmit --pretty false", "verify:docx-theme-styles": "electron scripts/verify-docx-theme-styles.cjs" diff --git a/apps/desktop/scripts/prepare-pandoc-runtime.mjs b/apps/desktop/scripts/prepare-pandoc-runtime.mjs index 6f4d55f..d54ebd2 100644 --- a/apps/desktop/scripts/prepare-pandoc-runtime.mjs +++ b/apps/desktop/scripts/prepare-pandoc-runtime.mjs @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; import { access, + chmod, mkdir, readFile, rename, @@ -43,14 +44,38 @@ async function fileExists(filePath) { } } -async function readRuntimeManifest() { +function platformDirectoryName(platform, architecture) { + if (platform === "win32" && architecture === "x64") { + return "windows-x86_64"; + } + if (platform === "darwin" && (architecture === "arm64" || architecture === "x64")) { + return `darwin-${architecture}`; + } + throw new Error(`不支持的 Pandoc 桌面内置目标:${platform}/${architecture}`); +} + +/** 归档下载得到的、需要从压缩包内部提取的文件名(不含通过 licenseSource 单独下载的许可证文件)。 */ +function archiveRequiredNames(artifact) { + const licenseSource = artifact.licenseSource ?? {}; + return [ + normalizedBaseName(artifact.executableRelativePath), + ...artifact.licenseFiles + .filter((fileName) => !licenseSource[fileName]) + .map(normalizedBaseName) + ]; +} + +async function readRuntimeManifest({ platform, architecture }) { const manifest = JSON.parse(await readFile(manifestPath, "utf8")); const artifact = manifest.artifacts?.find( (candidate) => - candidate.platform === "win32" && candidate.architecture === "x64" + candidate.platform === platform && candidate.architecture === architecture ); - if (!artifact || artifact.archiveType !== "zip") { - throw new Error("Pandoc 清单缺少 Windows x64 ZIP 发行项"); + if (!artifact) { + throw new Error(`Pandoc 清单缺少 ${platform}/${architecture} 发行项`); + } + if (artifact.archiveType !== "zip") { + throw new Error(`Pandoc 清单要求 ${platform}/${architecture} 为 ZIP 发行项`); } const requiredNames = [ normalizedBaseName(artifact.executableRelativePath), @@ -92,6 +117,33 @@ async function downloadArchive(downloadUrl, destination) { return content; } +async function downloadLicenseSourceFile(downloadUrl) { + const response = await fetch(downloadUrl, { + redirect: "follow", + signal: AbortSignal.timeout(downloadTimeoutMs) + }); + if (!response.ok) { + throw new Error(`Pandoc 许可证文件下载失败:HTTP ${response.status}`); + } + const content = new Uint8Array(await response.arrayBuffer()); + if (content.byteLength > maximumExtractedBytes) { + throw new Error("Pandoc 许可证文件超过允许的下载大小"); + } + return content; +} + +/** + * 部分平台(如 macOS)的官方归档不随附许可证文件,需要按清单声明的 + * 独立来源单独下载;返回的文件与归档内提取的文件合并后一并校验哈希。 + */ +async function loadLicenseSourceFiles(artifact) { + const files = new Map(); + for (const [fileName, source] of Object.entries(artifact.licenseSource ?? {})) { + files.set(fileName, await downloadLicenseSourceFile(source.downloadUrl)); + } + return files; +} + async function loadVerifiedArchive(artifact, archivePath) { if (await fileExists(archivePath)) { const cached = new Uint8Array(await readFile(archivePath)); @@ -122,10 +174,7 @@ async function loadVerifiedArchive(artifact, archivePath) { } function extractRequiredFiles(archive, artifact) { - const requiredNames = new Set([ - normalizedBaseName(artifact.executableRelativePath), - ...artifact.licenseFiles.map(normalizedBaseName) - ]); + const requiredNames = new Set(archiveRequiredNames(artifact)); let selectedBytes = 0; const entries = unzipSync(archive, { filter(file) { @@ -253,9 +302,13 @@ async function verifyPreparedRuntime(targetDirectory, manifest, artifact) { } } -async function preparePandocRuntime({ checkOnly = false } = {}) { - const { manifest, artifact } = await readRuntimeManifest(); - const platformDirectory = "windows-x86_64"; +async function preparePandocRuntime({ + checkOnly = false, + platform = process.platform, + architecture = process.arch +} = {}) { + const { manifest, artifact } = await readRuntimeManifest({ platform, architecture }); + const platformDirectory = platformDirectoryName(platform, architecture); const runtimeRoot = path.join(desktopRoot, ".runtime", "pandoc"); const targetDirectory = path.join( runtimeRoot, @@ -278,7 +331,10 @@ async function preparePandocRuntime({ checkOnly = false } = {}) { } const archivePath = path.join(downloadDirectory, archiveName); const archive = await loadVerifiedArchive(artifact, archivePath); - const files = extractRequiredFiles(archive, artifact); + const files = new Map([ + ...extractRequiredFiles(archive, artifact), + ...await loadLicenseSourceFiles(artifact) + ]); const temporaryDirectory = path.join( runtimeRoot, manifest.version, @@ -308,6 +364,10 @@ async function preparePandocRuntime({ checkOnly = false } = {}) { temporaryDirectory, normalizedBaseName(artifact.executableRelativePath) ); + if (platform !== "win32") { + // unzipSync 不保留压缩包内的可执行权限位,POSIX 平台需要手动补上。 + await chmod(executablePath, 0o755); + } verifyPandocVersion(executablePath, manifest.version); await smokeTestPandoc(executablePath); await writeFile( @@ -315,8 +375,8 @@ async function preparePandocRuntime({ checkOnly = false } = {}) { `${JSON.stringify( { version: manifest.version, - platform: "win32", - architecture: "x64", + platform, + architecture, license: manifest.license, projectUrl: manifest.projectUrl, sourceArchiveUrl: manifest.sourceArchiveUrl, @@ -338,17 +398,25 @@ async function preparePandocRuntime({ checkOnly = false } = {}) { return targetDirectory; } +function readCliFlag(name) { + const prefix = `--${name}=`; + const match = process.argv.find((argument) => argument.startsWith(prefix)); + return match ? match.slice(prefix.length) : undefined; +} + const isDirectInvocation = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (isDirectInvocation) { - preparePandocRuntime({ checkOnly: process.argv.includes("--check") }).catch( - (error) => { - console.error(error instanceof Error ? error.message : error); - process.exitCode = 1; - } - ); + preparePandocRuntime({ + checkOnly: process.argv.includes("--check"), + platform: readCliFlag("platform") ?? process.platform, + architecture: readCliFlag("arch") ?? process.arch + }).catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + }); } export { diff --git a/apps/desktop/tests/markdown-file.test.ts b/apps/desktop/tests/markdown-file.test.ts index 125f923..3e2cfdc 100644 --- a/apps/desktop/tests/markdown-file.test.ts +++ b/apps/desktop/tests/markdown-file.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; @@ -18,7 +18,11 @@ describe("桌面 Markdown 文件参数", () => { expect(isMarkdownFilePath("C:\\docs\\说明.txt")).toBe(false); }); - it("从启动参数中提取绝对 Markdown 路径", () => { + // findMarkdownFileArgument 依赖 Node 的 path 模块识别绝对路径,其行为 + // 随宿主 OS 而定:Windows 风格盘符路径只有在 Windows 宿主上才会被 + // 识别为绝对路径,这与真实部署一致(Windows 安装包只会在 Windows 上 + // 收到 Windows 风格的启动参数),因此该用例仅在 Windows 宿主上有意义。 + it.runIf(process.platform === "win32")("从启动参数中提取绝对 Markdown 路径", () => { expect( findMarkdownFileArgument( [ @@ -83,15 +87,19 @@ describe("桌面 Markdown 文件参数", () => { const filePath = path.join(directory, "快照.md"); try { await writeFile(filePath, "# 第一版", "utf8"); + // readMarkdownFileSnapshot 内部会 realpath 解析符号链接(例如 macOS + // 的 /var -> /private/var),返回的是规范化路径,因此断言也需要对 + // 同一路径做 realpath,而不是直接比较 mkdtemp 拼接出的原始路径。 + const canonicalFilePath = await realpath(filePath); const snapshot = await readMarkdownFileSnapshot(filePath, 100); expect(snapshot.document).toEqual({ markdown: "# 第一版", fileName: "快照.md" }); - expect(snapshot.filePath).toBe(filePath); + expect(snapshot.filePath).toBe(canonicalFilePath); expect(snapshot.documentKey).toBe( - createMarkdownDocumentKey(filePath) + createMarkdownDocumentKey(canonicalFilePath) ); expect(snapshot.contentHash).toBe( createMarkdownContentHash("# 第一版") diff --git a/apps/desktop/tests/release-build.test.ts b/apps/desktop/tests/release-build.test.ts index d400f64..ec11e15 100644 --- a/apps/desktop/tests/release-build.test.ts +++ b/apps/desktop/tests/release-build.test.ts @@ -44,8 +44,8 @@ describe("桌面发行构建链", () => { "@md-to-pdf/markdown-echarts", "@md-to-pdf/core", "@md-to-pdf/renderer", - "@md-to-pdf/application", "@md-to-pdf/preview-engine", + "@md-to-pdf/application", "@md-to-pdf/web" ]; @@ -160,7 +160,7 @@ describe("桌面发行构建链", () => { artifactPrefix: "MorphDoc" }); expect(desktopManifest).toMatchObject({ - version: "0.6.2", + version: "0.6.4", productName: "MorphDoc" }); expect(builderConfig).toContain("appId: brand.appId"); diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 4a8a48b..d397081 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -10,7 +10,7 @@ const app = buildApp({ ? { fontPacks: { roots: [fontPackRoot], - appVersion: process.env.APP_VERSION ?? "0.6.2" + appVersion: process.env.APP_VERSION ?? "0.6.4" } } : {}) diff --git a/apps/server/tests/docker-deployment.test.ts b/apps/server/tests/docker-deployment.test.ts index e032e80..24786c3 100644 --- a/apps/server/tests/docker-deployment.test.ts +++ b/apps/server/tests/docker-deployment.test.ts @@ -14,7 +14,7 @@ function readProjectFile(relativePath: string) { return readFileSync(`${projectRoot}/${relativePath}`, "utf8"); } -describe("Docker v0.6.2 部署契约", () => { +describe("Docker v0.6.4 部署契约", () => { it("完整构建并复制 DOCX 运行时工作区和 Lua Filter", () => { const dockerfile = readProjectFile("deploy/Dockerfile"); const workspaces = [ @@ -82,7 +82,7 @@ describe("Docker v0.6.2 部署契约", () => { ); expect(dockerfile).toContain("FONT_PACK_ROOT=/app/.local/font-packs"); expect(compose).toContain( - 'APP_VERSION: "${MD_TO_PDF_APP_VERSION:-0.6.2}"' + 'APP_VERSION: "${MD_TO_PDF_APP_VERSION:-0.6.4}"' ); expect(dockerignore.split(/\r?\n/u)).toContain(".local"); expect(dockerignore).toContain("!output/font-packs/root/**"); diff --git a/apps/web/tests/app-version.test.ts b/apps/web/tests/app-version.test.ts index 308a58e..d76c793 100644 --- a/apps/web/tests/app-version.test.ts +++ b/apps/web/tests/app-version.test.ts @@ -10,8 +10,8 @@ const webRoot = fileURLToPath(new URL("../", import.meta.url)); describe("应用版本", () => { it("从统一构建版本生成标题徽标", () => { - expect(APP_VERSION).toBe("0.6.2"); - expect(APP_VERSION_LABEL).toBe("v0.6.2"); + expect(APP_VERSION).toBe("0.6.4"); + expect(APP_VERSION_LABEL).toBe("v0.6.4"); }); it("允许浏览器加载同源 Web Manifest", () => { diff --git a/deploy/README.md b/deploy/README.md index 74b4d66..c8a2ea4 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -38,7 +38,7 @@ docker compose -f deploy\compose.yaml down 可以直接指定版本化镜像名称构建: ```powershell -$env:MD_TO_PDF_IMAGE = 'yixiong/md-to-pdf:v0.6.2' +$env:MD_TO_PDF_IMAGE = 'yixiong/md-to-pdf:v0.6.4' docker compose -f deploy\compose.yaml build ``` @@ -48,13 +48,13 @@ docker compose -f deploy\compose.yaml build 已经保留同版本系列的已验证镜像,可在内网或软件源较慢时复用其运行层: ```powershell -$env:MD_TO_PDF_IMAGE = 'yixiong/md-to-pdf:v0.6.2' +$env:MD_TO_PDF_IMAGE = 'yixiong/md-to-pdf:v0.6.4' $env:MD_TO_PDF_RUNTIME_BASE_IMAGE = 'yixiong/md-to-pdf:v0.6.0' $env:MD_TO_PDF_REUSE_PLAYWRIGHT_RUNTIME = '1' docker compose -f deploy\compose.yaml build ``` -复用模式会校验基础镜像中存在 Chromium,再覆盖 v0.6.2 应用代码和生产 +复用模式会校验基础镜像中存在 Chromium,再覆盖 v0.6.4 应用代码和生产 依赖。正式跨机器构建仍建议使用默认完整路径。 ## 主题挂载 @@ -76,7 +76,7 @@ docker compose -f deploy\compose.yaml up -d ## 内置字体包 -v0.6.2 的发行镜像固定将受校验字体包内置到 +v0.6.4 的发行镜像固定将受校验字体包内置到 `/app/.local/font-packs`。Compose 不挂载宿主机字体目录,因此部署端 无需额外复制字体,Preview、PDF 和 DOCX 可以离线使用同一套原生资产。 @@ -104,7 +104,7 @@ docker compose -f deploy\compose.yaml up -d 发布前执行真实内置字体门禁: ```powershell -$env:MD_TO_PDF_IMAGE = 'yixiong/md-to-pdf:v0.6.2' +$env:MD_TO_PDF_IMAGE = 'yixiong/md-to-pdf:v0.6.4' npm run verify:docker-font-pack ``` @@ -122,7 +122,7 @@ 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_APP_VERSION` | `0.6.2` | 字体包兼容性判断使用的应用版本 | +| `MD_TO_PDF_APP_VERSION` | `0.6.4` | 字体包兼容性判断使用的应用版本 | | `PDF_CONCURRENCY` | `1` | 同时执行的 PDF 任务数 | | `PDF_MAX_QUEUE` | `4` | 等待队列上限 | | `PDF_TIMEOUT_MS` | `120000` | 单次 PDF 生成超时 | diff --git a/deploy/compose.yaml b/deploy/compose.yaml index 20c16f4..6709126 100644 --- a/deploy/compose.yaml +++ b/deploy/compose.yaml @@ -19,7 +19,7 @@ services: PDF_CONCURRENCY: "${PDF_CONCURRENCY:-1}" PDF_MAX_QUEUE: "${PDF_MAX_QUEUE:-4}" PDF_TIMEOUT_MS: "${PDF_TIMEOUT_MS:-120000}" - APP_VERSION: "${MD_TO_PDF_APP_VERSION:-0.6.2}" + APP_VERSION: "${MD_TO_PDF_APP_VERSION:-0.6.4}" FONT_PACK_ROOT: /app/.local/font-packs volumes: - "${MD_TO_PDF_THEME_DIR:-../.local/themes}:/app/.local/themes:ro" diff --git a/deploy/verify-font-pack-compose.mjs b/deploy/verify-font-pack-compose.mjs index f2403bf..511a73a 100644 --- a/deploy/verify-font-pack-compose.mjs +++ b/deploy/verify-font-pack-compose.mjs @@ -175,7 +175,7 @@ const environment = { ...buildEnvironment, MD_TO_PDF_PORT: String(port), MD_TO_PDF_THEME_DIR: themeRoot, - MD_TO_PDF_APP_VERSION: "0.6.2" + MD_TO_PDF_APP_VERSION: "0.6.4" }; const compose = [ "compose", diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 442a563..90977fa 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,6 +2,34 @@ 最后更新:2026-08-26 +## v0.6.4 环境自适应与 macOS 打包 + +- `AGENTS.md` 不再硬编码 Windows 绝对路径与强制 PowerShell 语法;会话启动检查改为 + 动态确认仓库根目录,命令语法跟随当前会话实际 Shell。 +- 新增 macOS 桌面打包能力:`electron-builder` 增加独立 `mac` 配置块,产出 `dmg`, + 按 arm64、x64 分别单独构建、不产出 universal 包;Pandoc 运行时清单新增 + `darwin/arm64`、`darwin/x64` 两个官方发行项(含独立许可证文件来源,因为 macOS + 官方 zip 本身不随附许可证文本),`prepare-pandoc-runtime.mjs` 与桌面 Pandoc + 候选路径均已泛化为按 `--platform`/`--arch` 参数选取目标。 +- 顺手发现并修复两个此前从未在 macOS 全新环境下暴露过的真实 bug: + 解压内置 Pandoc 时未保留 Unix 可执行权限位(`unzipSync` 不保留压缩包内权限, + 已补 `chmod 0o755`);根 `package.json` 的 `build:web-runtime`/`dev`/`desktop:dev` + 把 `@md-to-pdf/application` 排在其依赖的 `@md-to-pdf/preview-engine` 之前构建, + 在没有历史 `dist/` 残留的全新环境中会导致类型解析失败,已调整构建顺序。 +- 本机 macOS(Apple Silicon)实测:`npm run package:mac:arm64` 全链路成功,内置 + darwin-arm64 版 Pandoc 3.9.0.2 完成下载、哈希校验与真实 DOCX 冒烟转换;未打包 + 应用(`--dir`)可正常启动,主/渲染/GPU/网络服务进程均稳定运行,可正常退出。 + 实际截图确认窗口、macOS 原生菜单栏、编辑器工具栏、中文字体与实时预览(标题、 + 任务列表、表格、行内公式、引用块)均渲染正常。 +- 跳过依赖真实语料的 `test:docx-real-world-corpus`、`test:docx-release-gate-suites` + 后,其余全部工作区测试、全项目类型检查和生产构建均通过;这两步依赖 Git 忽略、 + 需从可信来源单独提供的真实客户文档(`tmp/`),本机没有该目录,留待接入真实 + 语料的机器上执行。 +- 本版本仅完成开发基础设施与验证,未构建正式发行文件:没有产出真实签名的 + dmg、没有重新构建 Windows 安装包,也没有重新构建 Docker 镜像;下一步是统一 + DOCX 发布门禁的产出目录规范(版本号动态化,不再写死)并新增清理脚本,随后 + 评估四套 140 门禁矩阵的并行执行策略。 + ## v0.6.2 DOCX 发布门禁结论 - 原整批 560 已停用,有效门禁由四套相互独立的 140 组成,未启动、恢复或重建旧 560 流程。 diff --git a/package-lock.json b/package-lock.json index dafd4fb..9e96157 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "md-to-pdf", - "version": "0.6.2", + "version": "0.6.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "md-to-pdf", - "version": "0.6.2", + "version": "0.6.4", "license": "UNLICENSED", "workspaces": [ "apps/*", @@ -22,7 +22,7 @@ }, "apps/desktop": { "name": "@md-to-pdf/desktop", - "version": "0.6.2", + "version": "0.6.4", "devDependencies": { "@md-to-pdf/application": "0.4.1", "@md-to-pdf/docx-engine": "0.1.0", @@ -626,7 +626,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1056,7 +1055,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.7.tgz", "integrity": "sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", @@ -1310,6 +1308,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1331,6 +1330,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2019,18 +2019,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -3500,7 +3488,6 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3771,20 +3758,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4441,7 +4414,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", @@ -4910,7 +4882,8 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -4952,7 +4925,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -5375,7 +5347,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -5921,6 +5892,7 @@ "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", @@ -5934,6 +5906,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -5954,6 +5927,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -5969,6 +5943,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -5979,6 +5954,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -7470,7 +7446,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", @@ -7554,7 +7529,6 @@ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", "license": "MIT", - "peer": true, "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", @@ -8076,7 +8050,6 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8262,6 +8235,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -8279,6 +8253,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -8423,7 +8398,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8433,7 +8407,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9155,6 +9128,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -9180,6 +9154,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -9194,6 +9169,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -9201,34 +9177,6 @@ "rimraf": "bin.js" } }, - "node_modules/terser": { - "version": "5.49.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", - "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -9541,7 +9489,6 @@ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index f086630..293bde5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "md-to-pdf", - "version": "0.6.2", + "version": "0.6.4", "private": true, "license": "UNLICENSED", "description": "面向结构化 Markdown 的主题化文档创作与发布工具", @@ -10,8 +10,8 @@ ], "scripts": { "build": "npm run build:web-runtime && npm run build -w @md-to-pdf/font-pack-builder && npm run build -w @md-to-pdf/server && npm run build -w @md-to-pdf/desktop", - "build:web-runtime": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && 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/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/web", - "dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && 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/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n markdown-echarts,core,semantic-document,docx-theme-engine,font-pack-registry,docx-engine,renderer,application,preview-engine,server,web \"npm:dev:markdown-echarts\" \"npm:dev:core\" \"npm:dev:semantic-document\" \"npm:dev:docx-theme-engine\" \"npm:dev:font-pack-registry\" \"npm:dev:docx-engine\" \"npm:dev:renderer\" \"npm:dev:application\" \"npm:dev:preview-engine\" \"npm:dev:server\" \"npm:dev:web\"", + "build:web-runtime": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && 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/renderer && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/web", + "dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && 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/renderer && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/application && concurrently -k -n markdown-echarts,core,semantic-document,docx-theme-engine,font-pack-registry,docx-engine,renderer,application,preview-engine,server,web \"npm:dev:markdown-echarts\" \"npm:dev:core\" \"npm:dev:semantic-document\" \"npm:dev:docx-theme-engine\" \"npm:dev:font-pack-registry\" \"npm:dev:docx-engine\" \"npm:dev:renderer\" \"npm:dev:application\" \"npm:dev:preview-engine\" \"npm:dev:server\" \"npm:dev:web\"", "dev:markdown-echarts": "npm run dev -w @md-to-pdf/markdown-echarts", "dev:core": "npm run dev -w @md-to-pdf/core", "dev:semantic-document": "npm run dev -w @md-to-pdf/semantic-document", @@ -24,12 +24,17 @@ "dev:server": "npm run dev -w @md-to-pdf/server", "dev:web": "npm run dev -w @md-to-pdf/web", "dev:desktop": "npm run dev -w @md-to-pdf/desktop", - "desktop:dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && 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/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n web,desktop \"npm:dev:web\" \"npm:dev:desktop\"", + "desktop:dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && 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/renderer && npm run build -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/application && concurrently -k -n web,desktop \"npm:dev:web\" \"npm:dev:desktop\"", "desktop:package": "npm run build:font-pack && npm run package -w @md-to-pdf/desktop", + "desktop:package:mac:arm64": "npm run build:font-pack && npm run package:mac:arm64 -w @md-to-pdf/desktop", + "desktop:package:mac:x64": "npm run build:font-pack && npm run package:mac:x64 -w @md-to-pdf/desktop", "build: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 scripts/build-font-packs.mjs", "verify:font-pack": "npm run build:font-pack", "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", "desktop:make": "npm run build:font-pack && node scripts/build-desktop-release.mjs", + "desktop:make:mac:arm64": "npm run build:font-pack && npm run make:mac:arm64 -w @md-to-pdf/desktop", + "desktop:make:mac:x64": "npm run build:font-pack && npm run make:mac:x64 -w @md-to-pdf/desktop", + "desktop:make:mac": "npm run desktop:make:mac:arm64 && npm run desktop:make:mac:x64", "release:stage": "node scripts/stage-release.mjs", "test:release-stage": "node --test scripts/stage-release.test.mjs", "test:docx-layout-visual-matrix-cases": "node --test scripts/docx-layout-visual-matrix-cases.test.mjs", diff --git a/packages/docx-engine/src/pandoc-runtime-manifest.ts b/packages/docx-engine/src/pandoc-runtime-manifest.ts index 8a2d722..2af2988 100644 --- a/packages/docx-engine/src/pandoc-runtime-manifest.ts +++ b/packages/docx-engine/src/pandoc-runtime-manifest.ts @@ -2,13 +2,18 @@ import { DOCX_PANDOC_VERSION } from "@md-to-pdf/core"; import runtimeManifest from "./pandoc-runtime.json" with { type: "json" }; export interface PandocRuntimeArtifact { - platform: "win32" | "linux"; - architecture: "x64"; + platform: "win32" | "linux" | "darwin"; + architecture: "x64" | "arm64"; archiveType: "zip" | "tar.gz"; downloadUrl: string; sha256: string; executableRelativePath: string; licenseFiles: readonly string[]; + /** + * 部分平台的官方归档不随附许可证文件(如 macOS zip),需要单独声明 + * 每个许可证文件的独立下载来源;缺省时许可证文件从归档内部提取。 + */ + licenseSource?: Readonly>; files?: Readonly< Record >; diff --git a/packages/docx-engine/src/pandoc-runtime.json b/packages/docx-engine/src/pandoc-runtime.json index 8a84a96..af01dfe 100644 --- a/packages/docx-engine/src/pandoc-runtime.json +++ b/packages/docx-engine/src/pandoc-runtime.json @@ -35,6 +35,68 @@ "sha256": "A69ABFABABDA8A56969A254B09F9553A7BE89DDEC00D4E0FE9FD585D71A67508", "executableRelativePath": "bin/pandoc", "licenseFiles": ["COPYING.md", "COPYRIGHT"] + }, + { + "platform": "darwin", + "architecture": "arm64", + "archiveType": "zip", + "downloadUrl": "https://github.com/jgm/pandoc/releases/download/3.9.0.2/pandoc-3.9.0.2-arm64-macOS.zip", + "sha256": "6E9ECA844076BCBB599BBEEBBBA78A70F93B5307782B85C2C272872812C88875", + "executableRelativePath": "pandoc-3.9.0.2-arm64/bin/pandoc", + "licenseFiles": ["COPYING.md", "COPYRIGHT"], + "licenseSource": { + "COPYING.md": { + "downloadUrl": "https://raw.githubusercontent.com/jgm/pandoc/3.9.0.2/COPYING.md" + }, + "COPYRIGHT": { + "downloadUrl": "https://raw.githubusercontent.com/jgm/pandoc/3.9.0.2/COPYRIGHT" + } + }, + "files": { + "pandoc": { + "bytes": 187727808, + "sha256": "901A9D2B3D1484E008CBCE7A11739E8C9C22161C4A4222589ED2C53C96B8A55E" + }, + "COPYING.md": { + "bytes": 17787, + "sha256": "9D56CAC92294E206AF026A5502BEE0FED77200B08B51EC28AA63C9EFDA4DCFDD" + }, + "COPYRIGHT": { + "bytes": 9598, + "sha256": "842E33EF01625E93F85BEBB8BAC83AA570186B7AA77A09971257CC29F8F60740" + } + } + }, + { + "platform": "darwin", + "architecture": "x64", + "archiveType": "zip", + "downloadUrl": "https://github.com/jgm/pandoc/releases/download/3.9.0.2/pandoc-3.9.0.2-x86_64-macOS.zip", + "sha256": "B9FBCEABCCBC8F34AC021A50483FC32F8160568D0B4B2C22D81BB29E3054FD82", + "executableRelativePath": "pandoc-3.9.0.2-x86_64/bin/pandoc", + "licenseFiles": ["COPYING.md", "COPYRIGHT"], + "licenseSource": { + "COPYING.md": { + "downloadUrl": "https://raw.githubusercontent.com/jgm/pandoc/3.9.0.2/COPYING.md" + }, + "COPYRIGHT": { + "downloadUrl": "https://raw.githubusercontent.com/jgm/pandoc/3.9.0.2/COPYRIGHT" + } + }, + "files": { + "pandoc": { + "bytes": 119823904, + "sha256": "539A4D539386E62EA7DBD6F0B7C0F76DFADAA78D57E47472849B1C878F8A0A6B" + }, + "COPYING.md": { + "bytes": 17787, + "sha256": "9D56CAC92294E206AF026A5502BEE0FED77200B08B51EC28AA63C9EFDA4DCFDD" + }, + "COPYRIGHT": { + "bytes": 9598, + "sha256": "842E33EF01625E93F85BEBB8BAC83AA570186B7AA77A09971257CC29F8F60740" + } + } } ] } diff --git a/packages/docx-engine/src/pandoc-runtime.ts b/packages/docx-engine/src/pandoc-runtime.ts index c1e1737..ef6395e 100644 --- a/packages/docx-engine/src/pandoc-runtime.ts +++ b/packages/docx-engine/src/pandoc-runtime.ts @@ -140,6 +140,22 @@ function createCandidates(options: PandocRuntimeOptions) { exclusive: false }); } + if ( + platform === "darwin" && + (architecture === "arm64" || architecture === "x64") && + options.desktopResourcesPath + ) { + candidates.push({ + executablePath: path.join( + options.desktopResourcesPath, + "pandoc", + DOCX_PANDOC_VERSION, + `darwin-${architecture}`, + "pandoc" + ), + exclusive: false + }); + } if (platform === "linux" && architecture === "x64") { candidates.push({ executablePath: `/opt/pandoc/${DOCX_PANDOC_VERSION}/bin/pandoc`, diff --git a/packages/docx-engine/tests/pandoc-runtime.test.ts b/packages/docx-engine/tests/pandoc-runtime.test.ts index 2667afd..8cea8aa 100644 --- a/packages/docx-engine/tests/pandoc-runtime.test.ts +++ b/packages/docx-engine/tests/pandoc-runtime.test.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { zipSync } from "fflate"; import { @@ -118,7 +119,34 @@ describe("Pandoc 运行时探测", () => { executablePath: "pandoc.exe" }); expect(runner.mock.calls[0]?.[0]).toContain( - `pandoc\\${DOCX_PANDOC_VERSION}\\windows-x86_64\\pandoc.exe` + path.join("pandoc", DOCX_PANDOC_VERSION, "windows-x86_64", "pandoc.exe") + ); + }); + + it("macOS 内置路径不存在时回退到 PATH", async () => { + const runner = vi + .fn() + .mockResolvedValueOnce( + processResult({ + outcome: "not-found", + exitCode: null, + stdout: new Uint8Array() + }) + ) + .mockResolvedValueOnce(processResult()); + const resolution = await probePandocRuntime({ + platform: "darwin", + architecture: "arm64", + desktopResourcesPath: "/Applications/MorphDoc.app/Contents/Resources", + environment: {}, + runner + }); + expect(resolution).toMatchObject({ + capability: { status: "available" }, + executablePath: "pandoc" + }); + expect(runner.mock.calls[0]?.[0]).toContain( + path.join("pandoc", DOCX_PANDOC_VERSION, "darwin-arm64", "pandoc") ); }); diff --git a/scripts/build-font-packs.mjs b/scripts/build-font-packs.mjs index 35c11cb..40844b6 100644 --- a/scripts/build-font-packs.mjs +++ b/scripts/build-font-packs.mjs @@ -11,7 +11,7 @@ const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const bundlePath = join(repositoryRoot, "font-packs", "bundle.json"); const bundle = JSON.parse(await readFile(bundlePath, "utf8")); const outputRoot = join(repositoryRoot, "output", "font-packs", "root"); -const appVersion = process.env.npm_package_version || "0.6.2"; +const appVersion = process.env.npm_package_version || "0.6.4"; if ( bundle.schemaVersion !== 1 ||