1/**
2 * `assets.base` on the precomputed-hash path.
3 *
4 * `Command.Build` / `StaticSite` deploys pass a caller-supplied asset hash
5 * so unchanged builds skip the directory walk and keep the manifest
6 * already on Cloudflare. Changing `base` re-keys the manifest without
7 * changing the build output, so the caller-supplied hash stays identical —
8 * the skip decision must incorporate the prefix or a stale root-keyed
9 * manifest survives the deploy.
10 */
11import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
12import * as Cloudflare from "@/Cloudflare/index.ts";
13import * as Test from "@/Test/Alchemy";
14import { expect } from "alchemy-test";
15import * as Effect from "effect/Effect";
16import * as FileSystem from "effect/FileSystem";
17import * as Path from "effect/Path";
18import { MinimumLogLevel } from "effect/References";
19import { expectUrlAbsent, expectUrlContains } from "../Utils/Http.ts";
20import { waitForWorkerToBeDeleted } from "../Utils/Worker.ts";
21
22const { test } = Test.make({ providers: Cloudflare.providers() });
23
24const logLevel = Effect.provideService(
25 MinimumLogLevel,
26 process.env.DEBUG ? "Debug" : "Info",
27);
28
29const marker = "assets-base-rekey-marker";
30
31test.provider(
32 "assets: changing base with an unchanged precomputed hash re-keys the manifest",
33 (stack) =>
34 Effect.gen(function* () {
35 const { accountId } = yield* yield* CloudflareEnvironment;
36 const fs = yield* FileSystem.FileSystem;
37 const path = yield* Path.Path;
38
39 yield* stack.destroy();
40
41 const directory = yield* fs.makeTempDirectory({
42 prefix: "alchemy-assets-base-",
43 });
44 yield* fs.writeFileString(
45 path.join(directory, "index.html"),
46 `<!doctype html><html><body>${marker}</body></html>`,
47 );
48
49 // The hash stands in for a build-input hash: identical across both
50 // deploys because the build output is identical — only `base` flips.
51 const hash = "assets-base-rekey-v1";
52
53 let workerName: string | undefined;
54
55 yield* Effect.gen(function* () {
56 const first = yield* stack.deploy(
57 Effect.gen(function* () {
58 return yield* Cloudflare.Worker("AssetsBaseWorker", {
59 workersDev: true,
60 assets: { directory, hash },
61 });
62 }),
63 );
64 workerName = first.workerName;
65 expect(first.url).toBeDefined();
66
67 yield* expectUrlContains(`${first.url!}/`, marker, {
68 timeout: "120 seconds",
69 label: "root-keyed manifest",
70 });
71
72 const second = yield* stack.deploy(
73 Effect.gen(function* () {
74 return yield* Cloudflare.Worker("AssetsBaseWorker", {
75 workersDev: true,
76 assets: { directory, hash, base: "/docs" },
77 });
78 }),
79 );
80
81 // The prefix must reach the served manifest even though the
82 // caller-supplied hash did not change.
83 yield* expectUrlContains(`${second.url!}/docs/`, marker, {
84 timeout: "60 seconds",
85 label: "base-keyed manifest",
86 });
87 yield* expectUrlAbsent(`${second.url!}/`, marker, {
88 timeout: "60 seconds",
89 label: "root no longer serves the shell",
90 });
91 }).pipe(
92 Effect.ensuring(
93 Effect.gen(function* () {
94 yield* stack.destroy().pipe(Effect.ignore);
95 if (workerName) {
96 yield* waitForWorkerToBeDeleted(workerName, accountId).pipe(
97 Effect.ignore,
98 );
99 }
100 }),
101 ),
102 );
103 }).pipe(logLevel),
104 {
105 tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"],
106 timeout: 300_000,
107 },
108);