feat: 内置 Desktop Pandoc 最小运行时
This commit is contained in:
+14
-2
@@ -43,7 +43,18 @@ npm run make -w @md-to-pdf/desktop
|
||||
|
||||
`package` 和 `make` 会先调用根级 `build:web-runtime`,完整重建
|
||||
Markdown ECharts、Core、Renderer、Application、Preview Engine 和 Web,
|
||||
然后才复制 `apps/web/dist`。不得绕过该链路直接调用 electron-builder。
|
||||
再按固定清单下载或复用 Pandoc `3.9.0.2` 缓存、校验官方 ZIP 的
|
||||
SHA-256,并执行版本与最小 DOCX 冒烟,最后才调用 electron-builder。
|
||||
不得绕过该链路直接调用 electron-builder。
|
||||
|
||||
Pandoc 构建缓存位于被 Git 忽略的 `apps/desktop/.runtime/`。发行目录只
|
||||
包含 `pandoc.exe`、`COPYING.rtf`、`COPYRIGHT.txt` 和生成的审计清单,
|
||||
不会携带官方 MSI、手册、下载缓存或用户级 Pandoc 配置。已准备运行时可用
|
||||
以下命令离线复验:
|
||||
|
||||
```powershell
|
||||
npm run verify:pandoc-runtime -w @md-to-pdf/desktop
|
||||
```
|
||||
|
||||
版本化目录包、NSIS `Setup.exe` 和 ZIP 输出到
|
||||
`apps/desktop/out/v0.6.0/`。该目录被 Git 忽略。公司内部分发以
|
||||
@@ -63,7 +74,8 @@ NSIS 使用标准辅助安装模式,支持选择当前用户/所有用户安
|
||||
和 `MorphDoc.exe`。NSIS appId 保持 `com.md-to-pdf.desktop`,确保
|
||||
v0.5.1 覆盖升级沿用原安装身份。
|
||||
|
||||
当前目录包只包含一套 Electron Chromium,不携带 Playwright Chromium。
|
||||
当前目录包只包含一套 Electron Chromium,不携带 Playwright Chromium;
|
||||
Pandoc 作为未修改的独立程序随 Desktop 内置,通过受控子进程执行。
|
||||
Web 静态资源作为只读资源放在 Electron `resources/dist` 下,生产环境通过
|
||||
`mdpdf://bundle/` 自定义协议加载。
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const path = require("node:path");
|
||||
const { version } = require("./package.json");
|
||||
const brand = require("../../product.json");
|
||||
const pandocRuntime = require(
|
||||
"../../packages/docx-engine/src/pandoc-runtime.json"
|
||||
);
|
||||
|
||||
const windowsIcon = path.resolve(
|
||||
__dirname,
|
||||
@@ -38,6 +41,16 @@ module.exports = {
|
||||
to: "samples",
|
||||
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"
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
"scripts": {
|
||||
"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",
|
||||
"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 build && electron-builder --dir --config electron-builder.config.cjs --x64",
|
||||
"make": "npm run build:embedded-web && npm run build && electron-builder --win nsis zip --config electron-builder.config.cjs --x64",
|
||||
"package": "npm run build:embedded-web && npm run prepare:pandoc && npm run build && electron-builder --dir --config electron-builder.config.cjs --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",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false",
|
||||
"verify:docx-theme-styles": "electron scripts/verify-docx-theme-styles.cjs"
|
||||
@@ -28,6 +30,7 @@
|
||||
"electron": "43.2.0",
|
||||
"electron-builder": "26.15.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"fflate": "0.8.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.10",
|
||||
"wait-on": "^9.0.1"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface MinimalPandocArtifact {
|
||||
executableRelativePath: string;
|
||||
licenseFiles: string[];
|
||||
}
|
||||
|
||||
export function sha256(content: Uint8Array): string;
|
||||
|
||||
export function extractRequiredFiles(
|
||||
archive: Uint8Array,
|
||||
artifact: MinimalPandocArtifact
|
||||
): Map<string, Uint8Array>;
|
||||
|
||||
export function verifyPandocVersion(
|
||||
executablePath: string,
|
||||
expectedVersion: string
|
||||
): void;
|
||||
|
||||
export function preparePandocRuntime(options?: {
|
||||
checkOnly?: boolean;
|
||||
}): Promise<string>;
|
||||
@@ -0,0 +1,359 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
access,
|
||||
mkdir,
|
||||
readFile,
|
||||
rename,
|
||||
rm,
|
||||
writeFile
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { unzipSync } from "fflate";
|
||||
|
||||
const projectRoot = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const desktopRoot = path.join(projectRoot, "apps", "desktop");
|
||||
const manifestPath = path.join(
|
||||
projectRoot,
|
||||
"packages",
|
||||
"docx-engine",
|
||||
"src",
|
||||
"pandoc-runtime.json"
|
||||
);
|
||||
const maximumArchiveBytes = 64 * 1024 * 1024;
|
||||
const maximumExtractedBytes = 256 * 1024 * 1024;
|
||||
const downloadTimeoutMs = 120_000;
|
||||
|
||||
function sha256(content) {
|
||||
return createHash("sha256").update(content).digest("hex").toUpperCase();
|
||||
}
|
||||
|
||||
function normalizedBaseName(fileName) {
|
||||
return fileName.replaceAll("\\", "/").split("/").filter(Boolean).at(-1);
|
||||
}
|
||||
|
||||
async function fileExists(filePath) {
|
||||
try {
|
||||
await access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function readRuntimeManifest() {
|
||||
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
||||
const artifact = manifest.artifacts?.find(
|
||||
(candidate) =>
|
||||
candidate.platform === "win32" && candidate.architecture === "x64"
|
||||
);
|
||||
if (!artifact || artifact.archiveType !== "zip") {
|
||||
throw new Error("Pandoc 清单缺少 Windows x64 ZIP 发行项");
|
||||
}
|
||||
const requiredNames = [
|
||||
normalizedBaseName(artifact.executableRelativePath),
|
||||
...artifact.licenseFiles.map(normalizedBaseName)
|
||||
];
|
||||
if (
|
||||
requiredNames.some(
|
||||
(fileName) =>
|
||||
!fileName ||
|
||||
!Number.isInteger(artifact.files?.[fileName]?.bytes) ||
|
||||
!/^[A-F0-9]{64}$/u.test(artifact.files?.[fileName]?.sha256 ?? "")
|
||||
)
|
||||
) {
|
||||
throw new Error("Pandoc 清单缺少最小运行时文件哈希或长度");
|
||||
}
|
||||
return { manifest, artifact };
|
||||
}
|
||||
|
||||
async function downloadArchive(downloadUrl, destination) {
|
||||
const response = await fetch(downloadUrl, {
|
||||
redirect: "follow",
|
||||
signal: AbortSignal.timeout(downloadTimeoutMs)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Pandoc 下载失败:HTTP ${response.status}`);
|
||||
}
|
||||
const declaredLength = Number(response.headers.get("content-length"));
|
||||
if (
|
||||
Number.isFinite(declaredLength) &&
|
||||
declaredLength > maximumArchiveBytes
|
||||
) {
|
||||
throw new Error("Pandoc 归档超过允许的下载大小");
|
||||
}
|
||||
const content = new Uint8Array(await response.arrayBuffer());
|
||||
if (content.byteLength > maximumArchiveBytes) {
|
||||
throw new Error("Pandoc 归档超过允许的下载大小");
|
||||
}
|
||||
await writeFile(destination, content);
|
||||
return content;
|
||||
}
|
||||
|
||||
async function loadVerifiedArchive(artifact, archivePath) {
|
||||
if (await fileExists(archivePath)) {
|
||||
const cached = new Uint8Array(await readFile(archivePath));
|
||||
if (sha256(cached) === artifact.sha256) {
|
||||
return cached;
|
||||
}
|
||||
await rm(archivePath, { force: true });
|
||||
}
|
||||
|
||||
const partialPath = `${archivePath}.${process.pid}.partial`;
|
||||
await rm(partialPath, { force: true });
|
||||
try {
|
||||
const downloaded = await downloadArchive(
|
||||
artifact.downloadUrl,
|
||||
partialPath
|
||||
);
|
||||
const actualHash = sha256(downloaded);
|
||||
if (actualHash !== artifact.sha256) {
|
||||
throw new Error(
|
||||
`Pandoc 归档 SHA-256 不匹配:期望 ${artifact.sha256},实际 ${actualHash}`
|
||||
);
|
||||
}
|
||||
await rename(partialPath, archivePath);
|
||||
return downloaded;
|
||||
} finally {
|
||||
await rm(partialPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function extractRequiredFiles(archive, artifact) {
|
||||
const requiredNames = new Set([
|
||||
normalizedBaseName(artifact.executableRelativePath),
|
||||
...artifact.licenseFiles.map(normalizedBaseName)
|
||||
]);
|
||||
let selectedBytes = 0;
|
||||
const entries = unzipSync(archive, {
|
||||
filter(file) {
|
||||
const selected = requiredNames.has(normalizedBaseName(file.name));
|
||||
if (selected) {
|
||||
selectedBytes += file.originalSize;
|
||||
if (selectedBytes > maximumExtractedBytes) {
|
||||
throw new Error("Pandoc 最小运行时超过允许的解压大小");
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
});
|
||||
const files = new Map();
|
||||
for (const [entryName, content] of Object.entries(entries)) {
|
||||
const baseName = normalizedBaseName(entryName);
|
||||
if (!baseName || !requiredNames.has(baseName)) {
|
||||
continue;
|
||||
}
|
||||
if (files.has(baseName)) {
|
||||
throw new Error(`Pandoc 归档包含重复文件:${baseName}`);
|
||||
}
|
||||
files.set(baseName, content);
|
||||
}
|
||||
for (const requiredName of requiredNames) {
|
||||
if (!files.has(requiredName)) {
|
||||
throw new Error(`Pandoc 归档缺少必须文件:${requiredName}`);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function verifyPandocVersion(executablePath, expectedVersion) {
|
||||
const result = spawnSync(executablePath, ["--version"], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024,
|
||||
windowsHide: true
|
||||
});
|
||||
if (result.error || result.status !== 0) {
|
||||
throw new Error(`内置 Pandoc 无法执行:${result.error?.message ?? result.stderr}`);
|
||||
}
|
||||
const firstLine = result.stdout.split(/\r?\n/u)[0]?.trim();
|
||||
if (firstLine !== `pandoc ${expectedVersion}`) {
|
||||
throw new Error(
|
||||
`内置 Pandoc 版本不匹配:期望 pandoc ${expectedVersion},实际 ${firstLine ?? "未知"}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function smokeTestPandoc(executablePath) {
|
||||
const smokeRoot = path.join(
|
||||
tmpdir(),
|
||||
`morphdoc-pandoc-smoke-${process.pid}-${Date.now()}`
|
||||
);
|
||||
const dataDirectory = path.join(smokeRoot, "data");
|
||||
const outputPath = path.join(smokeRoot, "smoke.docx");
|
||||
await mkdir(dataDirectory, { recursive: true });
|
||||
try {
|
||||
const result = spawnSync(
|
||||
executablePath,
|
||||
[
|
||||
"--from=gfm",
|
||||
"--to=docx",
|
||||
`--data-dir=${dataDirectory}`,
|
||||
`--output=${outputPath}`
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
input: "# 墨呈\n\nPandoc 运行时冒烟测试。\n",
|
||||
maxBuffer: 1024 * 1024,
|
||||
windowsHide: true
|
||||
}
|
||||
);
|
||||
if (result.error || result.status !== 0) {
|
||||
throw new Error(`内置 Pandoc DOCX 冒烟失败:${result.error?.message ?? result.stderr}`);
|
||||
}
|
||||
const output = await readFile(outputPath);
|
||||
if (output.byteLength < 1024 || output[0] !== 0x50 || output[1] !== 0x4b) {
|
||||
throw new Error("内置 Pandoc 生成的 DOCX 不是有效 ZIP 包");
|
||||
}
|
||||
} finally {
|
||||
await rm(smokeRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyPreparedRuntime(targetDirectory, manifest, artifact) {
|
||||
const auditPath = path.join(targetDirectory, "runtime-manifest.json");
|
||||
if (!(await fileExists(auditPath))) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const audit = JSON.parse(await readFile(auditPath, "utf8"));
|
||||
if (
|
||||
audit.version !== manifest.version ||
|
||||
audit.archiveSha256 !== artifact.sha256 ||
|
||||
audit.downloadUrl !== artifact.downloadUrl
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const expectedFiles = [
|
||||
normalizedBaseName(artifact.executableRelativePath),
|
||||
...artifact.licenseFiles.map(normalizedBaseName)
|
||||
];
|
||||
for (const fileName of expectedFiles) {
|
||||
const content = await readFile(path.join(targetDirectory, fileName));
|
||||
const expected = artifact.files[fileName];
|
||||
if (
|
||||
!expected ||
|
||||
content.byteLength !== expected.bytes ||
|
||||
sha256(content) !== expected.sha256 ||
|
||||
audit.files?.[fileName]?.sha256 !== expected.sha256
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const executablePath = path.join(
|
||||
targetDirectory,
|
||||
normalizedBaseName(artifact.executableRelativePath)
|
||||
);
|
||||
verifyPandocVersion(executablePath, manifest.version);
|
||||
await smokeTestPandoc(executablePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function preparePandocRuntime({ checkOnly = false } = {}) {
|
||||
const { manifest, artifact } = await readRuntimeManifest();
|
||||
const platformDirectory = "windows-x86_64";
|
||||
const runtimeRoot = path.join(desktopRoot, ".runtime", "pandoc");
|
||||
const targetDirectory = path.join(
|
||||
runtimeRoot,
|
||||
manifest.version,
|
||||
platformDirectory
|
||||
);
|
||||
if (await verifyPreparedRuntime(targetDirectory, manifest, artifact)) {
|
||||
console.log(`Pandoc ${manifest.version} 最小运行时已通过校验`);
|
||||
return targetDirectory;
|
||||
}
|
||||
if (checkOnly) {
|
||||
throw new Error("Pandoc 最小运行时缺失或未通过完整性校验");
|
||||
}
|
||||
|
||||
const downloadDirectory = path.join(runtimeRoot, "downloads");
|
||||
await mkdir(downloadDirectory, { recursive: true });
|
||||
const archiveName = new URL(artifact.downloadUrl).pathname.split("/").at(-1);
|
||||
if (!archiveName) {
|
||||
throw new Error("Pandoc 下载地址缺少归档文件名");
|
||||
}
|
||||
const archivePath = path.join(downloadDirectory, archiveName);
|
||||
const archive = await loadVerifiedArchive(artifact, archivePath);
|
||||
const files = extractRequiredFiles(archive, artifact);
|
||||
const temporaryDirectory = path.join(
|
||||
runtimeRoot,
|
||||
manifest.version,
|
||||
`.${platformDirectory}-${process.pid}-${Date.now()}`
|
||||
);
|
||||
await rm(temporaryDirectory, { recursive: true, force: true });
|
||||
await mkdir(temporaryDirectory, { recursive: true });
|
||||
try {
|
||||
const auditFiles = {};
|
||||
for (const [fileName, content] of files) {
|
||||
const expected = artifact.files[fileName];
|
||||
const actualHash = sha256(content);
|
||||
if (
|
||||
!expected ||
|
||||
content.byteLength !== expected.bytes ||
|
||||
actualHash !== expected.sha256
|
||||
) {
|
||||
throw new Error(`Pandoc 文件完整性校验失败:${fileName}`);
|
||||
}
|
||||
await writeFile(path.join(temporaryDirectory, fileName), content);
|
||||
auditFiles[fileName] = {
|
||||
bytes: content.byteLength,
|
||||
sha256: actualHash
|
||||
};
|
||||
}
|
||||
const executablePath = path.join(
|
||||
temporaryDirectory,
|
||||
normalizedBaseName(artifact.executableRelativePath)
|
||||
);
|
||||
verifyPandocVersion(executablePath, manifest.version);
|
||||
await smokeTestPandoc(executablePath);
|
||||
await writeFile(
|
||||
path.join(temporaryDirectory, "runtime-manifest.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: manifest.version,
|
||||
platform: "win32",
|
||||
architecture: "x64",
|
||||
license: manifest.license,
|
||||
projectUrl: manifest.projectUrl,
|
||||
sourceArchiveUrl: manifest.sourceArchiveUrl,
|
||||
downloadUrl: artifact.downloadUrl,
|
||||
archiveSha256: artifact.sha256,
|
||||
files: auditFiles
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`,
|
||||
"utf8"
|
||||
);
|
||||
await rm(targetDirectory, { recursive: true, force: true });
|
||||
await rename(temporaryDirectory, targetDirectory);
|
||||
} finally {
|
||||
await rm(temporaryDirectory, { recursive: true, force: true });
|
||||
}
|
||||
console.log(`Pandoc ${manifest.version} 最小运行时已准备:${targetDirectory}`);
|
||||
return targetDirectory;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
extractRequiredFiles,
|
||||
preparePandocRuntime,
|
||||
sha256,
|
||||
verifyPandocVersion
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { strToU8, zipSync } from "fflate";
|
||||
import {
|
||||
extractRequiredFiles,
|
||||
sha256
|
||||
} from "../scripts/prepare-pandoc-runtime.mjs";
|
||||
|
||||
const artifact = {
|
||||
executableRelativePath: "pandoc.exe",
|
||||
licenseFiles: ["COPYING.rtf", "COPYRIGHT.txt"]
|
||||
};
|
||||
|
||||
function archive(files: Record<string, string>) {
|
||||
return zipSync(
|
||||
Object.fromEntries(
|
||||
Object.entries(files).map(([name, content]) => [
|
||||
name,
|
||||
strToU8(content)
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
describe("Desktop Pandoc 最小运行时构建", () => {
|
||||
it("仅提取可执行文件和两份许可证文件", () => {
|
||||
const files = extractRequiredFiles(
|
||||
archive({
|
||||
"pandoc-3.9.0.2/pandoc.exe": "executable",
|
||||
"pandoc-3.9.0.2/COPYING.rtf": "license",
|
||||
"pandoc-3.9.0.2/COPYRIGHT.txt": "copyright",
|
||||
"pandoc-3.9.0.2/MANUAL.html": "manual"
|
||||
}),
|
||||
artifact
|
||||
);
|
||||
|
||||
expect([...files.keys()].sort()).toEqual([
|
||||
"COPYING.rtf",
|
||||
"COPYRIGHT.txt",
|
||||
"pandoc.exe"
|
||||
]);
|
||||
});
|
||||
|
||||
it("拒绝缺失许可证文件的归档", () => {
|
||||
expect(() =>
|
||||
extractRequiredFiles(
|
||||
archive({
|
||||
"pandoc.exe": "executable",
|
||||
"COPYING.rtf": "license"
|
||||
}),
|
||||
artifact
|
||||
)
|
||||
).toThrow("COPYRIGHT.txt");
|
||||
});
|
||||
|
||||
it("拒绝用不同目录伪造的重复文件", () => {
|
||||
expect(() =>
|
||||
extractRequiredFiles(
|
||||
archive({
|
||||
"official/pandoc.exe": "official",
|
||||
"shadow/pandoc.exe": "shadow",
|
||||
"official/COPYING.rtf": "license",
|
||||
"official/COPYRIGHT.txt": "copyright"
|
||||
}),
|
||||
artifact
|
||||
)
|
||||
).toThrow("重复文件:pandoc.exe");
|
||||
});
|
||||
|
||||
it("使用大写十六进制输出稳定 SHA-256", () => {
|
||||
const content = strToU8("MorphDoc");
|
||||
expect(sha256(content)).toBe(
|
||||
createHash("sha256").update(content).digest("hex").toUpperCase()
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,17 @@ interface ProductBrand {
|
||||
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));
|
||||
|
||||
@@ -54,11 +65,62 @@ describe("桌面发行构建链", () => {
|
||||
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 package -w @md-to-pdf/desktop"
|
||||
);
|
||||
});
|
||||
|
||||
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`,
|
||||
|
||||
Reference in New Issue
Block a user