Hash web manifest from rewritten content

This commit is contained in:
scamiv
2026-03-23 00:43:34 +01:00
parent fd19295fea
commit f3f41f68d0
2 changed files with 116 additions and 16 deletions
+45 -16
View File
@@ -43,6 +43,36 @@ function createContentHash(filePath: string): string {
return createHash("sha256").update(content).digest("hex").slice(0, 12);
}
function createStringHash(content: string): string {
return createHash("sha256").update(content).digest("hex").slice(0, 12);
}
function createHashedAssetUrl(relativePath: string, hash: string): string {
const parsed = path.posix.parse(toPosixPath(relativePath));
const hashedFileName = `${parsed.name}.${hash}${parsed.ext}`;
const hashedRelativePath = path.posix.join(
"_assets",
parsed.dir,
hashedFileName,
);
return `/${encodeAssetPath(hashedRelativePath)}`;
}
function renderWebManifest(
resourcesDir: string,
assetManifest: AssetManifest,
): string {
const sourcePath = path.join(resourcesDir, "manifest.json");
const manifest = JSON.parse(fs.readFileSync(sourcePath, "utf8")) as {
icons?: Array<{ src?: string }>;
};
manifest.icons = manifest.icons?.map((icon) => ({
...icon,
src: buildAssetUrl(icon.src ?? "", assetManifest),
}));
return `${JSON.stringify(manifest, null, 2)}\n`;
}
export function getResourcesDir(rootDir: string = process.cwd()): string {
return path.join(rootDir, "resources");
}
@@ -86,18 +116,21 @@ export function buildPublicAssetManifest(resourcesDir: string): AssetManifest {
const manifest: AssetManifest = {};
for (const relativePath of listHashedPublicAssetPaths(resourcesDir)) {
if (relativePath === "manifest.json") {
continue;
}
const absolutePath = path.join(resourcesDir, relativePath);
const parsed = path.posix.parse(toPosixPath(relativePath));
const hash = createContentHash(absolutePath);
const hashedFileName = `${parsed.name}.${hash}${parsed.ext}`;
const hashedRelativePath = path.posix.join(
"_assets",
parsed.dir,
hashedFileName,
);
manifest[relativePath] = `/${encodeAssetPath(hashedRelativePath)}`;
manifest[relativePath] = createHashedAssetUrl(relativePath, hash);
}
const renderedWebManifest = renderWebManifest(resourcesDir, manifest);
manifest["manifest.json"] = createHashedAssetUrl(
"manifest.json",
createStringHash(renderedWebManifest),
);
manifestCache.set(resourcesDir, manifest);
return manifest;
}
@@ -117,14 +150,10 @@ export function createHashedPublicAssetFiles(
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
if (relativePath === "manifest.json") {
const manifest = JSON.parse(fs.readFileSync(sourcePath, "utf8")) as {
icons?: Array<{ src?: string }>;
};
manifest.icons = manifest.icons?.map((icon) => ({
...icon,
src: buildAssetUrl(icon.src ?? "", assetManifest),
}));
fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`);
fs.writeFileSync(
outputPath,
renderWebManifest(resourcesDir, assetManifest),
);
continue;
}
+71
View File
@@ -0,0 +1,71 @@
import fs from "fs/promises";
import os from "os";
import path from "path";
import { afterEach, describe, expect, test } from "vitest";
import {
buildPublicAssetManifest,
clearPublicAssetManifestCache,
createHashedPublicAssetFiles,
} from "../../src/server/PublicAssetManifest";
describe("PublicAssetManifest", () => {
let tempDir: string | null = null;
afterEach(async () => {
clearPublicAssetManifestCache();
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
tempDir = null;
}
});
test("hashes manifest.json from its rewritten content", async () => {
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(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);
});
});