Files
workinf_Blender_Wasm/tools/web/check-deployment-cache.mjs
mes123456 7c16b279ae
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M7 workflows and release operations
2026-08-15 17:43:53 -04:00

162 lines
7.3 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
createDeploymentHttpServer,
listenDeploymentHttpServer,
loadDeploymentContract,
} from "./deployment-http-server.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const sourceDist = path.join(repoRoot, "web/dist");
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "blender-deployment-cache-"));
const root = path.join(workspace, "dist");
const contract = loadDeploymentContract();
let server;
function sha256(bytes) {
return crypto.createHash("sha256").update(bytes).digest("hex");
}
function route(id) {
const value = contract.responseHeaders.routes.find((candidate) => candidate.id === id);
assert.ok(value, `deployment contract route is missing: ${id}`);
return value;
}
async function responseBytes(response) {
return Buffer.from(await response.arrayBuffer());
}
async function expectCache(origin, url, expected) {
const response = await fetch(`${origin}${url}`, { cache: "no-store" });
assert.equal(response.status, 200, `cache probe failed url=${url} status=${response.status}`);
assert.equal(
response.headers.get("cache-control"),
expected,
`Cache-Control mismatch url=${url}`,
);
assert.match(response.headers.get("etag"), /^"sha256-[a-f0-9]{64}"$/);
return response;
}
function contentHashedAssets() {
const assetRoot = path.join(root, "assets");
const names = fs.readdirSync(assetRoot).sort();
const cases = [
["entry JS", names.filter((name) => name.endsWith(".js") && !name.includes(".worker-"))],
["CSS", names.filter((name) => name.endsWith(".css"))],
["Worker", names.filter((name) => name.includes(".worker-") && name.endsWith(".js"))],
];
for (const [role, roleNames] of cases) {
assert.ok(roleNames.length > 0, `production build has no content-hashed ${role} asset`);
for (const name of roleNames) {
assert.match(name, /-[A-Za-z0-9_-]{8}\.(?:js|css)$/, `${role} asset is not content hashed: ${name}`);
}
}
return cases.flatMap(([, namesForRole]) => namesForRole).map((name) => `/assets/${name}`);
}
try {
assert.ok(fs.statSync(path.join(sourceDist, "index.html")).size > 0, "web/dist is missing; run the production build first");
fs.cpSync(sourceDist, root, { recursive: true });
const manifestPath = path.join(root, "engine-manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
assert.equal(manifest.schemaVersion, 2);
assert.match(manifest.releaseId, /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/);
server = createDeploymentHttpServer({ root, contract });
const origin = await listenDeploymentHttpServer(server);
const noCache = route("entry-document").cacheControl;
const manifestNoCache = route("runtime-manifest").cacheControl;
const immutable = route("content-hashed-assets").cacheControl;
const engineNoCache = route("stable-engine-assets").cacheControl;
const entry = await expectCache(origin, "/", noCache);
const entryEtag = entry.headers.get("etag");
assert.deepEqual(await responseBytes(entry), fs.readFileSync(path.join(root, "index.html")));
await expectCache(origin, "/index.html", noCache);
const unchangedEntry = await fetch(`${origin}/`, { headers: { "If-None-Match": entryEtag } });
assert.equal(unchangedEntry.status, 304);
assert.equal(unchangedEntry.headers.get("cache-control"), noCache);
const nextEntryBytes = Buffer.concat([
fs.readFileSync(path.join(root, "index.html")),
Buffer.from("\n<!-- next release -->\n"),
]);
fs.writeFileSync(path.join(root, "index.html"), nextEntryBytes);
const changedEntry = await fetch(`${origin}/`, { headers: { "If-None-Match": entryEtag } });
assert.equal(changedEntry.status, 200);
assert.equal(changedEntry.headers.get("cache-control"), noCache);
assert.notEqual(changedEntry.headers.get("etag"), entryEtag);
assert.deepEqual(await responseBytes(changedEntry), nextEntryBytes);
const firstManifest = await expectCache(origin, "/engine-manifest.json", manifestNoCache);
const firstManifestEtag = firstManifest.headers.get("etag");
assert.deepEqual(await responseBytes(firstManifest), fs.readFileSync(manifestPath));
const unchangedManifest = await fetch(`${origin}/engine-manifest.json`, {
headers: { "If-None-Match": firstManifestEtag },
});
assert.equal(unchangedManifest.status, 304);
assert.equal(unchangedManifest.headers.get("cache-control"), manifestNoCache);
assert.equal((await unchangedManifest.arrayBuffer()).byteLength, 0);
const assetUrls = contentHashedAssets();
for (const url of assetUrls) {
const response = await expectCache(origin, url, immutable);
const bytes = await responseBytes(response);
assert.equal(response.headers.get("etag"), `"sha256-${sha256(bytes)}"`);
}
const uniqueResources = [...new Map(
manifest.variants.flatMap((variant) => Object.values(variant.resources))
.map((resource) => [resource.url, resource]),
).values()];
for (const resource of uniqueResources) {
const response = await expectCache(origin, resource.url, engineNoCache);
assert.equal(sha256(await responseBytes(response)), resource.sha256);
}
const nextManifest = { ...manifest, releaseId: `${manifest.releaseId}.next` };
fs.writeFileSync(manifestPath, `${JSON.stringify(nextManifest, null, 2)}\n`);
const changedManifest = await fetch(`${origin}/engine-manifest.json`, {
headers: { "If-None-Match": firstManifestEtag },
});
assert.equal(changedManifest.status, 200);
assert.equal(changedManifest.headers.get("cache-control"), manifestNoCache);
assert.notEqual(changedManifest.headers.get("etag"), firstManifestEtag);
assert.equal((await changedManifest.json()).releaseId, nextManifest.releaseId);
const stableResource = uniqueResources.find((resource) => resource.url.endsWith(".js"));
assert.ok(stableResource, "engine manifest has no stable JS resource");
const stablePath = path.join(root, stableResource.url.replace(/^\/+/, ""));
const stableFirst = await fetch(`${origin}${stableResource.url}`, { cache: "no-store" });
const stableFirstEtag = stableFirst.headers.get("etag");
const stableUnchanged = await fetch(`${origin}${stableResource.url}`, {
headers: { "If-None-Match": stableFirstEtag },
});
assert.equal(stableUnchanged.status, 304);
assert.equal(stableUnchanged.headers.get("cache-control"), engineNoCache);
const replacement = Buffer.from("export default () => { throw new Error('next release'); };\n");
fs.writeFileSync(stablePath, replacement);
const stableChanged = await fetch(`${origin}${stableResource.url}`, {
headers: { "If-None-Match": stableFirstEtag },
});
assert.equal(stableChanged.status, 200);
assert.equal(stableChanged.headers.get("cache-control"), engineNoCache);
assert.notEqual(stableChanged.headers.get("etag"), stableFirstEtag);
assert.equal(sha256(await responseBytes(stableChanged)), sha256(replacement));
assert.notEqual(sha256(replacement), stableResource.sha256);
process.stdout.write(
`deployment-cache-ok noCache=3 immutable=${assetUrls.length} stableEngine=${uniqueResources.length} revalidation=304/200\n`,
);
}
finally {
if (server?.listening) await new Promise((resolve) => server.close(resolve));
fs.rmSync(workspace, { recursive: true, force: true });
}