diff --git a/test/build-test.ts b/test/build-test.ts index 0a82f8f43..28106c9c6 100644 --- a/test/build-test.ts +++ b/test/build-test.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import {existsSync, readdirSync, statSync} from "node:fs"; -import {mkdir, mkdtemp, readFile, rm, writeFile} from "node:fs/promises"; +import {mkdir, mkdtemp, open, readFile, rename, rm, unlink, writeFile} from "node:fs/promises"; import os from "node:os"; import {join, normalize, relative} from "node:path/posix"; import {PassThrough} from "node:stream"; @@ -15,6 +15,16 @@ const silentEffects = { output: {write() {}} }; +function getHashNormalizer() { + const hashes = new Map(); + let nextHashId = 0; + return (key: string) => { + let hash = hashes.get(key); + if (!hash) hashes.set(key, (hash = String(++nextHashId).padStart(8, "0"))); + return hash; + }; +} + describe("build", () => { before(() => setCurrentDate(new Date("2024-01-10T16:00:00"))); mockJsDelivr(); @@ -40,15 +50,37 @@ describe("build", () => { const expectedDir = join(outputRoot, outname); const generate = !existsSync(expectedDir) && process.env.CI !== "true"; const outputDir = generate ? expectedDir : actualDir; + const normalizeHash = getHashNormalizer(); await rm(actualDir, {recursive: true, force: true}); if (generate) console.warn(`! generating ${expectedDir}`); const config = {...(await readConfig(undefined, path)), output: outputDir}; await build({config}, new TestEffects(outputDir, join(config.root, ".observablehq", "cache"))); - // For non-public tests (most of them), we don’t want to test the contents - // of the _observablehq files because they change often. - if (!name.endsWith("-public")) await rm(join(outputDir, "_observablehq"), {recursive: true, force: true}); + // Replace any hashed files in _observablehq with empty files, and + // renumber the hashes so they are sequential. This way we don’t have to + // update the test snapshots whenever Framework’s client code changes. We + // make an exception for minisearch.json because to test the content. + for (const path of findFiles(join(actualDir, "_observablehq"))) { + const match = /^((.+)\.[0-9a-f]{8})\.(\w+)$/.exec(path); + if (!match) throw new Error(`no hash found: ${path}`); + const [, key, name, ext] = match; + const oldPath = join(actualDir, "_observablehq", path); + const newPath = join(actualDir, "_observablehq", `${name}.${normalizeHash(key)}.${ext}`); + if (/^minisearch\.[0-9a-f]{8}\.json$/.test(path)) { + await rename(oldPath, newPath); + } else { + await unlink(oldPath); + await (await open(newPath, "w")).close(); + } + } + + // Replace any reference to re-numbered files in _observablehq. + for (const path of findFiles(actualDir)) { + const actual = await readFile(join(actualDir, path), "utf8"); + const normalized = actual.replace(/\/_observablehq\/((.+)\.[0-9a-f]{8})\.(\w+)\b/g, (match, key, name, ext) => `/_observablehq/${name}.${normalizeHash(key)}.${ext}`); // prettier-ignore + if (normalized !== actual) await writeFile(join(actualDir, path), normalized); + } if (generate) return; diff --git a/test/deploy-test.ts b/test/deploy-test.ts index f686b4a60..fd4d3c116 100644 --- a/test/deploy-test.ts +++ b/test/deploy-test.ts @@ -1006,10 +1006,10 @@ describe("deploy", () => { .handleGetProject(DEPLOY_CONFIG) .handlePostDeploy({projectId: DEPLOY_CONFIG.projectId, deployId}) .expectFileUpload({deployId, path: "index.html", action: "upload"}) - .expectFileUpload({deployId, path: "_observablehq/theme-air,near-midnight.e68849dc.css", action: "skip"}) - .expectFileUpload({deployId, path: "_observablehq/client.e153837f.js", action: "skip"}) - .expectFileUpload({deployId, path: "_observablehq/runtime.4d065c03.js", action: "skip"}) - .expectFileUpload({deployId, path: "_observablehq/stdlib.11a0ff13.js", action: "skip"}) + .expectFileUpload({deployId, path: "_observablehq/client.00000001.js", action: "skip"}) + .expectFileUpload({deployId, path: "_observablehq/runtime.00000002.js", action: "skip"}) + .expectFileUpload({deployId, path: "_observablehq/stdlib.00000003.js", action: "skip"}) + .expectFileUpload({deployId, path: "_observablehq/theme-air,near-midnight.00000004.css", action: "skip"}) .handlePostDeployUploaded({deployId}) .handleGetDeploy({deployId, deployStatus: "uploaded"}) .start(); diff --git a/test/mocks/observableApi.ts b/test/mocks/observableApi.ts index 70a8089e2..f4c0b4145 100644 --- a/test/mocks/observableApi.ts +++ b/test/mocks/observableApi.ts @@ -284,10 +284,10 @@ class ObservableApiMock { expectStandardFiles(options: Omit) { return this.expectFileUpload({...options, path: "index.html"}) - .expectFileUpload({...options, path: "_observablehq/theme-air,near-midnight.e68849dc.css"}) - .expectFileUpload({...options, path: "_observablehq/client.e153837f.js"}) - .expectFileUpload({...options, path: "_observablehq/runtime.4d065c03.js"}) - .expectFileUpload({...options, path: "_observablehq/stdlib.11a0ff13.js"}); + .expectFileUpload({...options, path: "_observablehq/client.00000001.js"}) + .expectFileUpload({...options, path: "_observablehq/runtime.00000002.js"}) + .expectFileUpload({...options, path: "_observablehq/stdlib.00000003.js"}) + .expectFileUpload({...options, path: "_observablehq/theme-air,near-midnight.00000004.css"}); } /** Register a file that is expected to be uploaded. Also includes that file in diff --git a/test/output/build/404/404.html b/test/output/build/404/404.html index 38f39cc46..60a652760 100644 --- a/test/output/build/404/404.html +++ b/test/output/build/404/404.html @@ -5,12 +5,12 @@ Page not found - + - - - - + + + +