1import * as Cloudflare from "@/Cloudflare/index.ts";
2import * as Test from "@/Test/Alchemy";
3import { describe } from "alchemy-test";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as pathe from "pathe";
8import { cloneFixture } from "../Utils/Fixture.ts";
9import {
10 expectUrlAbsent,
11 expectUrlContains,
12 expectUrlHeader,
13 expectUrlRedirect,
14} from "../Utils/Http.ts";
15
16const { test } = Test.make({ providers: Cloudflare.providers() });
17
18const fixtureDir = pathe.resolve(import.meta.dirname, "fixtures/assets-config");
19
20const workerScript = (marker: string) => `
21export default {
22 fetch: async () => new Response(${JSON.stringify(marker)}),
23};
24`;
25
26// Regression test for https://github.com/alchemy-run/alchemy/issues/927:
27// `_redirects` / `_headers` were read from the assets directory and folded
28// into the change-detection hash, but their contents were never sent to
29// Cloudflare, so the rules silently never applied.
30describe.concurrent(
31 "Cloudflare.Worker assets config files",
32 { tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"] },
33 () => {
34 test.provider(
35 "redirects and headers apply, update, and survive keep-assets deploys",
36 (stack) =>
37 Effect.gen(function* () {
38 const fs = yield* FileSystem.FileSystem;
39 const path = yield* Path.Path;
40
41 yield* stack.destroy();
42
43 const deploy = (
44 assets: string | { directory: string; hash: string },
45 marker: string,
46 ) =>
47 stack.deploy(
48 Effect.gen(function* () {
49 return yield* Cloudflare.Worker("AssetsConfigFiles", {
50 script: workerScript(marker),
51 assets,
52 workersDev: true,
53 compatibility: {
54 date: "2024-01-01",
55 },
56 });
57 }),
58 );
59
60 // 1. Fresh deploy: rules from the fixture's `_redirects` /
61 // `_headers` must reach Cloudflare and apply.
62 const worker = yield* deploy(fixtureDir, "worker-v1");
63 const url = worker.url!;
64 yield* expectUrlContains(`${url}/`, "alchemy-assets-config-index");
65 yield* expectUrlRedirect(`${url}/old-path`, "/index.html", {
66 status: 301,
67 label: "initial redirect",
68 });
69 yield* expectUrlHeader(
70 `${url}/`,
71 "x-alchemy-test",
72 "assets-config-header",
73 { label: "initial header" },
74 );
75 // The special files themselves stay excluded from serving.
76 yield* expectUrlAbsent(`${url}/_redirects`, "/old-path", {
77 timeout: "15 seconds",
78 label: "_redirects not served",
79 });
80
81 // 2. Update: editing only `_redirects` must deploy the new rules.
82 const dir = yield* cloneFixture(fixtureDir, {
83 prefix: "alchemy-assets-config-",
84 });
85 yield* fs.writeFileString(
86 path.join(dir, "_redirects"),
87 "/moved /index.html 302\n",
88 );
89 yield* deploy(dir, "worker-v2");
90 yield* expectUrlRedirect(`${url}/moved`, "/index.html", {
91 status: 302,
92 label: "updated redirect",
93 });
94 // The old rule is gone: `/old-path` now falls through to the
95 // user worker instead of redirecting.
96 yield* expectUrlContains(`${url}/old-path`, "worker-v2", {
97 label: "removed redirect falls through",
98 });
99
100 // 3. Precomputed-hash deploys: the first one uploads, the second
101 // hits the keep-assets skip path (hash matches stored state)
102 // which never walks the directory — it must still re-send the
103 // `_redirects` / `_headers` contents or the PUT wipes them.
104 const prebuilt = { directory: dir, hash: "assets-config-files-test" };
105 yield* deploy(prebuilt, "worker-v3");
106 yield* expectUrlContains(`${url}/some-worker-route`, "worker-v3");
107 yield* deploy(prebuilt, "worker-v4");
108 // Once the new script serves, the skip-path deploy has landed.
109 yield* expectUrlContains(`${url}/some-worker-route`, "worker-v4");
110 yield* expectUrlRedirect(`${url}/moved`, "/index.html", {
111 status: 302,
112 label: "redirect after keep-assets deploy",
113 });
114 yield* expectUrlHeader(
115 `${url}/`,
116 "x-alchemy-test",
117 "assets-config-header",
118 { label: "header after keep-assets deploy" },
119 );
120
121 yield* stack.destroy();
122 }),
123 { timeout: 360_000 },
124 );
125 },
126);