Files
OpenFrontIO/tests/server/PublicAssetManifest.test.ts
scamiv 7d5ad0d0ac fix: add derived asset rewriting for web manifests and BMFont XML
Hash raw public assets first, then render derived assets from the manifest, hash the rewritten content, and emit that rewritten output.

This replaces the manifest.json special case with a small derived-asset registry and adds BMFont XML rewriting so <page file=...> entries point to hashed relative atlas page paths in production.

Also move asset-manifest.mjs emission to the end of the Vite asset sync step and add regression tests for rewritten web manifest hashing, BMFont XML page rewrite, BMFont XML hash invalidation when page images change, nested relative BMFont page paths, and hard failure on missing derived asset references.
2026-04-05 14:10:41 +02:00

217 lines
6.6 KiB
TypeScript

import fs from "fs/promises";
import os from "os";
import path from "path";
import { afterEach, describe, expect, test } from "vitest";
import { normalizeAssetPath } from "../../src/core/AssetUrls";
import {
buildPublicAssetManifest,
clearPublicAssetManifestCache,
createHashedPublicAssetFiles,
} from "../../src/server/PublicAssetManifest";
describe("PublicAssetManifest", () => {
let tempDir: string | null = null;
type TempResources = {
resourcesDir: string;
outDir: string;
};
async function createTempResources(): Promise<TempResources> {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "public-assets-"));
const resourcesDir = path.join(tempDir, "resources");
const outDir = path.join(tempDir, "static");
await fs.mkdir(resourcesDir, { recursive: true });
await fs.writeFile(path.join(resourcesDir, "manifest.json"), "{}\n");
return { resourcesDir, outDir };
}
function getExpectedRelativeEmittedPath(
fromAssetHref: string,
targetAssetHref: string,
): string {
const fromDir = path.posix.dirname(normalizeAssetPath(fromAssetHref));
const targetPath = normalizeAssetPath(targetAssetHref);
return path.posix.relative(fromDir, targetPath);
}
async function writeBitmapFontFixture(
resourcesDir: string,
xmlRelativePath: string,
pageFilePath: string,
pageContent: string = "png-v1",
): Promise<void> {
const xmlPath = path.join(resourcesDir, xmlRelativePath);
const pagePath = path.join(path.dirname(xmlPath), pageFilePath);
const xmlPageFilePath = pageFilePath.split(path.sep).join(path.posix.sep);
await fs.mkdir(path.dirname(pagePath), { recursive: true });
await fs.writeFile(
xmlPath,
[
'<?xml version="1.0"?>',
"<font>",
` <pages><page id="0" file="${xmlPageFilePath}"/></pages>`,
"</font>",
"",
].join("\n"),
);
await fs.writeFile(pagePath, pageContent);
}
async function emitHashedAsset(
outDir: string,
assetHref: string,
): Promise<string> {
return fs.readFile(
path.join(outDir, normalizeAssetPath(assetHref)),
"utf8",
);
}
afterEach(async () => {
clearPublicAssetManifestCache();
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
tempDir = null;
}
});
test("hashes manifest.json from its rewritten content", async () => {
const { resourcesDir, outDir } = await createTempResources();
await fs.mkdir(path.join(resourcesDir, "icons"), { recursive: true });
await fs.writeFile(
path.join(resourcesDir, "manifest.json"),
JSON.stringify(
{
name: "OpenFront",
icons: [{ src: "icons/app-icon.png" }],
},
null,
2,
),
);
await fs.writeFile(
path.join(resourcesDir, "icons", "app-icon.png"),
"icon-v1",
"utf8",
);
const firstManifest = buildPublicAssetManifest(resourcesDir);
const firstManifestHref = firstManifest["manifest.json"];
const firstIconHref = firstManifest["icons/app-icon.png"];
createHashedPublicAssetFiles(resourcesDir, outDir, firstManifest);
const firstOutput = await fs.readFile(
path.join(outDir, firstManifestHref.slice(1)),
"utf8",
);
await fs.writeFile(
path.join(resourcesDir, "icons", "app-icon.png"),
"icon-v2",
"utf8",
);
clearPublicAssetManifestCache();
const secondManifest = buildPublicAssetManifest(resourcesDir);
const secondManifestHref = secondManifest["manifest.json"];
const secondIconHref = secondManifest["icons/app-icon.png"];
expect(firstIconHref).not.toBe(secondIconHref);
expect(firstManifestHref).not.toBe(secondManifestHref);
expect(firstOutput).toContain(firstIconHref);
expect(firstOutput).not.toContain(secondIconHref);
});
test("rewrites BMFont XML page filenames to hashed relative paths", async () => {
const { resourcesDir, outDir } = await createTempResources();
await writeBitmapFontFixture(
resourcesDir,
path.join("fonts", "test.xml"),
"test.png",
);
const assetManifest = buildPublicAssetManifest(resourcesDir);
createHashedPublicAssetFiles(resourcesDir, outDir, assetManifest);
const xmlHref = assetManifest["fonts/test.xml"];
const pngHref = assetManifest["fonts/test.png"];
const emittedXml = await emitHashedAsset(outDir, xmlHref);
expect(emittedXml).toContain(
getExpectedRelativeEmittedPath(xmlHref, pngHref),
);
expect(emittedXml).not.toContain('file="test.png"');
});
test("BMFont XML hash changes when a referenced page image changes", async () => {
const { resourcesDir } = await createTempResources();
await writeBitmapFontFixture(
resourcesDir,
path.join("fonts", "test.xml"),
"test.png",
);
const firstManifest = buildPublicAssetManifest(resourcesDir);
await fs.writeFile(path.join(resourcesDir, "fonts", "test.png"), "png-v2");
clearPublicAssetManifestCache();
const secondManifest = buildPublicAssetManifest(resourcesDir);
expect(firstManifest["fonts/test.png"]).not.toBe(
secondManifest["fonts/test.png"],
);
expect(firstManifest["fonts/test.xml"]).not.toBe(
secondManifest["fonts/test.xml"],
);
});
test("fails when BMFont XML references a missing page image", async () => {
const { resourcesDir } = await createTempResources();
await fs.mkdir(path.join(resourcesDir, "fonts"), { recursive: true });
await fs.writeFile(
path.join(resourcesDir, "fonts", "broken.xml"),
[
'<?xml version="1.0"?>',
"<font>",
' <pages><page id="0" file="missing.png"/></pages>',
"</font>",
"",
].join("\n"),
);
expect(() => buildPublicAssetManifest(resourcesDir)).toThrow(
/missing from the asset manifest/i,
);
});
test("rewrites nested BMFont page references to the correct relative hashed path", async () => {
const { resourcesDir, outDir } = await createTempResources();
await writeBitmapFontFixture(
resourcesDir,
path.join("fonts", "nested", "atlas.xml"),
path.join("pages", "p0.png"),
"nested-png",
);
const assetManifest = buildPublicAssetManifest(resourcesDir);
createHashedPublicAssetFiles(resourcesDir, outDir, assetManifest);
const xmlHref = assetManifest["fonts/nested/atlas.xml"];
const pngHref = assetManifest["fonts/nested/pages/p0.png"];
const emittedXml = await emitHashedAsset(outDir, xmlHref);
expect(emittedXml).toContain(
getExpectedRelativeEmittedPath(xmlHref, pngHref),
);
expect(emittedXml).not.toContain('file="pages/p0.png"');
});
});