feat: 完善字体包发行与 Docker 内置字体
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user