t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/Cloudflare/Website/Vite.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

1691 lines
60 KiB
TypeScript

import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
import * as Cloudflare from "@/Cloudflare/index.ts";
import { isLocalId } from "@/Cloudflare/LocalRuntime";
import * as Test from "@/Test/Alchemy";
import { initialCwd } from "@/Util/Node.ts";
import * as r2 from "@distilled.cloud/cloudflare/r2";
import { describe, expect } from "alchemy-test";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import { MinimumLogLevel } from "effect/References";
import * as Schedule from "effect/Schedule";
import * as HttpClient from "effect/http/HttpClient";
import * as HttpClientRequest from "effect/http/HttpClientRequest";
import * as pathe from "pathe";
import { cloneFixture } from "../Utils/Fixture.ts";
import { expectUrlContains } from "../Utils/Http.ts";
import {
expectWorkerExists,
waitForWorkerToBeDeleted,
} from "../Utils/Worker.ts";
import type { Counter as ViteDoCounter } from "./vite-do-fixture/src/worker.ts";
const { test } = Test.make({ providers: Cloudflare.providers() });
const { test: devTest } = Test.make({
providers: Cloudflare.providers(),
dev: true,
});
const logLevel = Effect.provideService(
MinimumLogLevel,
process.env.DEBUG ? "Debug" : "Info",
);
const fixtureDir = pathe.resolve(import.meta.dirname, "vite-fixture");
const spaFixtureDir = pathe.resolve(import.meta.dirname, "vite-spa-fixture");
const foldkitFixtureDir = pathe.resolve(import.meta.dirname, "foldkit-fixture");
const doFixtureDir = pathe.resolve(import.meta.dirname, "vite-do-fixture");
const containerFixtureDir = pathe.resolve(
import.meta.dirname,
"vite-container-fixture",
);
const workerFirstFixtureDir = pathe.resolve(
import.meta.dirname,
"worker-first-fixture",
);
const reactRouterRscFixtureDir = pathe.resolve(
import.meta.dirname,
"react-router-rsc-fixture",
);
const tanstackDevBindingsFixtureDir = pathe.resolve(
import.meta.dirname,
"tanstack-dev-bindings-fixture",
);
const viteChildFixtureDir = pathe.resolve(
import.meta.dirname,
"vite-child-fixture",
);
// Vite/Rollup's `vite:build-html` plugin chokes when the project root
// is outside the current working directory because it tries to express
// the emitted asset path relative to `cwd`. To keep the temp clone
// reachable via a sane relative path, allocate the temp dir *inside*
// the alchemy package's `.tmp/` so it sits under the same workspace
// root as `cwd`.
const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
// Suites are sequential by default; every test here deploys its own
// scratch stack with its own worker names and fixture clone, so they are
// independent and can run concurrently. Builds run in per-project child
// processes (`runViteBuildChild`), so concurrent builds can't race each
// other's working directory.
describe.concurrent(
"Vite",
{ tags: ["provider:cloudflare", "provider:cloudflare:website"] },
() => {
test.provider(
"Vite: editing a source file republishes the assets in a single deploy",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* stack.destroy();
const rootDir = yield* cloneFixture(fixtureDir, {
prefix: "alchemy-vite-fix-",
tempRoot,
entries: ["index.html", "package.json", "vite.config.ts", "src"],
});
const indexPath = path.join(rootDir, "index.html");
// Restrict the input memo to fixture sources so the test isn't
// re-hashing the whole monorepo on every deploy.
const memoInclude = [
"index.html",
"src/**",
"package.json",
"vite.config.ts",
];
const v1Marker = `vite-v1-${Date.now()}`;
yield* fs.writeFileString(indexPath, htmlPage(v1Marker));
const site1 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"FixVite",
viteProps(rootDir, memoInclude),
);
}),
);
expect(site1.url).toBeDefined();
expect(site1.hash?.input).toBeDefined();
yield* expectWorkerExists(site1.workerName, accountId);
yield* expectUrlContains(`${site1.url!}/`, v1Marker, {
timeout: "120 seconds",
label: "deploy1 v1 marker",
});
// ── deploy 2: edit fixture, redeploy once ──────────────────────────
const v2Marker = `vite-v2-${Date.now()}`;
yield* fs.writeFileString(indexPath, htmlPage(v2Marker));
const site2 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"FixVite",
viteProps(rootDir, memoInclude),
);
}),
);
expect(site2.hash?.input).toBeDefined();
expect(site2.hash?.input).not.toEqual(site1.hash?.input);
yield* expectUrlContains(`${site2.url!}/`, v2Marker, {
timeout: "60 seconds",
label: "deploy2 v2 marker",
});
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site1.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
// Regression test for https://github.com/alchemy-run/alchemy/issues/1016.
//
// `hashViteInput` used to pass a relative root as both the directory and its
// base, resolving `app` as `<cwd>/app/app`. The empty match produced a constant
// input hash, so source edits were incorrectly treated as no-ops.
test.provider(
"Vite: a source edit changes the input hash when rootDir is relative",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* stack.destroy();
const absoluteRoot = yield* cloneFixture(fixtureDir, {
prefix: "alchemy-vite-relative-root-",
tempRoot,
entries: ["index.html", "package.json", "vite.config.ts", "src"],
});
// Relative to the process's anchored cwd (what a relative rootDir
// means to the engine) — a live `process.cwd()` read can race a
// concurrent tool's transient chdir.
const rootDir = path.relative(initialCwd, absoluteRoot);
const indexPath = path.join(absoluteRoot, "index.html");
const memoInclude = [
"index.html",
"src/**",
"package.json",
"vite.config.ts",
];
expect(path.isAbsolute(rootDir)).toBe(false);
yield* fs.writeFileString(indexPath, htmlPage("relative-root-v1"));
const site1 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"ViteRelativeRoot",
viteProps(rootDir, memoInclude),
);
}),
);
expect(site1.hash?.input).toBeDefined();
yield* fs.writeFileString(indexPath, htmlPage("relative-root-v2"));
const site2 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"ViteRelativeRoot",
viteProps(rootDir, memoInclude),
);
}),
);
expect(site2.hash?.input).toBeDefined();
expect(site2.hash?.input).not.toEqual(site1.hash?.input);
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site1.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
// Regression test for https://github.com/alchemy-run/alchemy/issues/792.
//
// A pure client-only Vite project (no vite.config.ts, no plugins, no worker
// entry) resolves as `appType: "spa"`, so the Cloudflare Vite plugin declares
// no `builder.buildApp`. On Vite 8, `builder.buildApp()` then runs post-order
// `buildApp` hooks *before* the default environment builds — which used to make
// our build-output plugin resolve while the client output was still undefined,
// so the deploy died with "Vite build produced neither assets nor server
// output". Detecting completion in `writeBundle` (per environment) instead of a
// post-order `buildApp` hook fixes it; this test proves the SPA path deploys.
test.provider(
"Vite: client-only SPA (no config, no plugins) builds and serves assets",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* stack.destroy();
const rootDir = yield* cloneFixture(spaFixtureDir, {
prefix: "alchemy-vite-spa-",
tempRoot,
entries: ["index.html", "package.json", "src"],
});
const indexPath = path.join(rootDir, "index.html");
const memoInclude = ["index.html", "src/**", "package.json"];
const marker = `vite-spa-${Date.now()}`;
yield* fs.writeFileString(indexPath, htmlPage(marker));
const site = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"FixViteSpa",
viteProps(rootDir, memoInclude),
);
}),
);
expect(site.url).toBeDefined();
expect(site.hash?.input).toBeDefined();
yield* expectWorkerExists(site.workerName, accountId);
yield* expectUrlContains(`${site.url!}/`, marker, {
timeout: "120 seconds",
label: "spa marker",
});
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
// Foldkit (foldkit.dev) is an Effect-native Elm-architecture frontend
// framework. Its apps are plain client-only Vite projects (the Foldkit Vite
// plugin only adds HMR/devtools wiring), so `Cloudflare.Website.Vite` deploys
// them as-is. This pins that the Foldkit plugin composes with the injected
// Cloudflare Vite plugin and that deep links fall back to `index.html` via
// `single-page-application` not-found handling.
test.provider(
"Vite: Foldkit SPA deploys and serves with SPA fallback",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
yield* stack.destroy();
const rootDir = yield* cloneFixture(foldkitFixtureDir, {
prefix: "alchemy-vite-foldkit-",
tempRoot,
entries: ["index.html", "package.json", "vite.config.ts", "src"],
});
const memoInclude = [
"index.html",
"src/**",
"package.json",
"vite.config.ts",
];
const site = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite("FixViteFoldkit", {
...viteProps(rootDir, memoInclude),
assets: {
notFoundHandling: "single-page-application",
},
});
}),
);
expect(site.url).toBeDefined();
expect(site.hash?.input).toBeDefined();
yield* expectWorkerExists(site.workerName, accountId);
yield* expectUrlContains(`${site.url!}/`, "Foldkit Fixture", {
timeout: "120 seconds",
label: "foldkit index",
});
// Deep link falls back to index.html so client-side routing can boot.
yield* expectUrlContains(
`${site.url!}/counter/42`,
"Foldkit Fixture",
{
timeout: "60 seconds",
label: "foldkit spa fallback",
},
);
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
test.provider(
"Vite: class form deploys and serves the built assets",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* stack.destroy();
const rootDir = yield* cloneFixture(fixtureDir, {
prefix: "alchemy-vite-class-",
tempRoot,
entries: ["index.html", "package.json", "vite.config.ts", "src"],
});
const indexPath = path.join(rootDir, "index.html");
const memoInclude = [
"index.html",
"src/**",
"package.json",
"vite.config.ts",
];
const marker = `vite-class-${Date.now()}`;
yield* fs.writeFileString(indexPath, htmlPage(marker));
const site1 = yield* stack.deploy(
Effect.gen(function* () {
return yield* class FixVite extends Cloudflare.Website.Vite<FixVite>()(
"FixVite",
viteProps(rootDir, memoInclude),
) {};
}),
);
expect(site1.url).toBeDefined();
expect(site1.hash?.input).toBeDefined();
yield* expectWorkerExists(site1.workerName, accountId);
yield* expectUrlContains(`${site1.url!}/`, marker, {
timeout: "120 seconds",
label: "class form marker",
});
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site1.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
// ─────────────────────────────────────────────────────────────────────
// Path-relocation behavior for the vite path
//
// `Cloudflare.Website.Vite` stores a path-insensitive `hash.input` made from
// the memo'd input tree plus build-affecting Vite options. The diff is:
//
// `input !== output.hash?.input`
//
// — a pure content comparison that must be stable across rootDir
// moves. We delete the original rootDir between deploys to make the
// test fail loudly if anything still depends on the recorded path.
// ─────────────────────────────────────────────────────────────────────
test.provider(
"Vite: relocating rootDir (and deleting the old one) is a no-op when sources are identical",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* stack.destroy();
const memoInclude = [
"index.html",
"src/**",
"package.json",
"vite.config.ts",
];
const marker = `vite-relocate-${Date.now()}`;
const rootA = yield* cloneFixture(fixtureDir, {
prefix: "alchemy-vite-relocate-a-",
tempRoot,
entries: ["index.html", "package.json", "vite.config.ts", "src"],
});
yield* fs.writeFileString(
path.join(rootA, "index.html"),
htmlPage(marker),
);
const site1 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"ViteReloc",
viteProps(rootA, memoInclude),
);
}),
);
expect(site1.hash?.input).toBeDefined();
yield* expectUrlContains(`${site1.url!}/`, marker, {
timeout: "120 seconds",
label: "deploy1 marker",
});
// Drop rootA so a stale path comparison can't quietly succeed.
yield* fs.remove(rootA, { recursive: true });
const rootB = yield* cloneFixture(fixtureDir, {
prefix: "alchemy-vite-relocate-b-",
tempRoot,
entries: ["index.html", "package.json", "vite.config.ts", "src"],
});
yield* fs.writeFileString(
path.join(rootB, "index.html"),
htmlPage(marker),
);
const site2 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"ViteReloc",
viteProps(rootB, memoInclude),
);
}),
);
// Identical sources ⇒ identical input hash ⇒ diff says
// unchanged ⇒ no rebuild required for the apply to succeed.
expect(site2.hash?.input).toEqual(site1.hash?.input);
yield* expectUrlContains(`${site2.url!}/`, marker, {
timeout: "60 seconds",
label: "deploy2 marker",
});
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site1.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
// ─────────────────────────────────────────────────────────────────────
// Workspace-aware memoization
//
// A Vite project in a monorepo can bundle source from sibling workspace
// packages. Those directories are discovered from the module graph at
// build time, persisted in `hash.workspaces` (relative to the Vite
// root), and included in the `hash.input` computation — so editing only
// a sibling package busts the memo and triggers a rebuild.
// ─────────────────────────────────────────────────────────────────────
test.provider(
"Vite: edits in a sibling workspace package bust the build memo",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* stack.destroy();
// Hand-rolled two-package workspace: `app` is the Vite root and
// `shared` is a sibling package whose source the client bundle
// imports directly (escaping the Vite root).
yield* fs.makeDirectory(tempRoot, { recursive: true });
const parent = yield* fs.makeTempDirectory({
prefix: "alchemy-vite-ws-",
directory: tempRoot,
});
yield* Effect.addFinalizer(
Exit.match({
onSuccess: () =>
Effect.ignore(fs.remove(parent, { recursive: true })),
onFailure: () => Effect.void,
}),
);
const rootDir = path.join(parent, "app");
const sharedDir = path.join(parent, "shared");
const writeShared = (marker: string) =>
fs.writeFileString(
path.join(sharedDir, "src/message.ts"),
`export const message = ${JSON.stringify(marker)};\n`,
);
yield* fs.makeDirectory(path.join(rootDir, "src"), {
recursive: true,
});
yield* fs.makeDirectory(path.join(sharedDir, "src"), {
recursive: true,
});
yield* fs.writeFileString(
path.join(rootDir, "index.html"),
htmlPage("vite-workspace-fixture"),
);
yield* fs.writeFileString(
path.join(rootDir, "package.json"),
JSON.stringify({
name: "alchemy-vite-workspace-app",
version: "0.0.0",
private: true,
type: "module",
}),
);
// Same shape as vite-fixture/vite.config.ts: point the SSR build at
// the minimal worker entry so the cloudflare-vite-plugin can wrap it.
yield* fs.writeFileString(
path.join(rootDir, "vite.config.ts"),
`import { defineConfig } from "vite";
export default defineConfig({
environments: {
ssr: { build: { rollupOptions: { input: "./src/worker.ts" } } },
},
});
`,
);
yield* fs.writeFileString(
path.join(rootDir, "src/worker.ts"),
`type Env = { ASSETS: { fetch: (req: Request) => Promise<Response> } };
export default {
fetch(request: Request, env: Env): Promise<Response> {
return env.ASSETS.fetch(request);
},
};
`,
);
// The client entry imports across the Vite root boundary into the
// sibling package's source.
yield* fs.writeFileString(
path.join(rootDir, "src/main.ts"),
`import { message } from "../../shared/src/message.ts";
const el = document.getElementById("app");
if (el) {
el.textContent = message;
}
`,
);
yield* fs.writeFileString(
path.join(sharedDir, "package.json"),
JSON.stringify({
name: "alchemy-vite-workspace-shared",
version: "0.0.0",
private: true,
type: "module",
}),
);
const memoInclude = [
"index.html",
"src/**",
"package.json",
"vite.config.ts",
];
const marker1 = `vite-ws-1-${Date.now()}`;
yield* writeShared(marker1);
const site1 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"ViteWorkspace",
viteProps(rootDir, memoInclude),
);
}),
);
expect(site1.url).toBeDefined();
expect(site1.hash?.input).toBeDefined();
// The sibling package was discovered from the module graph and
// persisted relative to the Vite root.
expect(site1.hash?.additionalWorkspaces).toContain("../shared");
yield* expectWorkerExists(site1.workerName, accountId);
yield* expectBundleContains(site1.url!, marker1, {
label: "workspace marker v1 in client bundle",
});
// ── deploy 2: edit ONLY the sibling package ────────────────────────
const marker2 = `vite-ws-2-${Date.now()}`;
yield* writeShared(marker2);
const site2 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"ViteWorkspace",
viteProps(rootDir, memoInclude),
);
}),
);
// Nothing under `rootDir` changed — only the sibling workspace did.
// The workspace-aware input hash must still bust the memo.
expect(site2.hash?.input).toBeDefined();
expect(site2.hash?.input).not.toEqual(site1.hash?.input);
expect(site2.hash?.additionalWorkspaces).toContain("../shared");
yield* expectBundleContains(site2.url!, marker2, {
label: "workspace marker v2 in client bundle",
});
// ── deploy 3: no changes anywhere ⇒ memo hit ───────────────────────
const site3 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite(
"ViteWorkspace",
viteProps(rootDir, memoInclude),
);
}),
);
expect(site3.hash?.input).toEqual(site2.hash?.input);
expect(site3.hash?.additionalWorkspaces).toEqual(
site2.hash?.additionalWorkspaces,
);
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site1.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
test.provider(
"Vite: `env` props are inlined and env-only changes redeploy",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
yield* stack.destroy();
const rootDir = yield* cloneFixture(fixtureDir, {
prefix: "alchemy-vite-env-",
tempRoot,
entries: ["index.html", "package.json", "vite.config.ts", "src"],
});
const memoInclude = ["index.html", "src/**", "package.json"];
const marker1 = `vite-env-1-${Date.now()}`;
const site1 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite("FixViteEnv", {
...viteProps(rootDir, memoInclude),
env: { VITE_TEST_MARKER: marker1 },
});
}),
);
expect(site1.url).toBeDefined();
expect(site1.hash?.input).toBeDefined();
// Resolve the hashed bundle URL by reading the deployed HTML, then
// assert the marker that `main.ts` references via
// `import.meta.env.VITE_TEST_MARKER` was actually inlined into the
// served JS asset by `Cloudflare.Website.Vite`'s `env`-→-`define` plumbing.
yield* expectBundleContains(site1.url!, marker1, {
label: "VITE_TEST_MARKER v1 inlined into client bundle",
});
const marker2 = `vite-env-2-${Date.now()}`;
const site2 = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite("FixViteEnv", {
...viteProps(rootDir, memoInclude),
env: { VITE_TEST_MARKER: marker2 },
});
}),
);
expect(site2.hash?.input).toBeDefined();
yield* expectBundleContains(site2.url!, marker2, {
label: "VITE_TEST_MARKER v2 inlined into client bundle",
});
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site1.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
test.provider(
"Vite: worker entry can host a local Durable Object binding",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
yield* stack.destroy();
const rootDir = yield* cloneFixture(doFixtureDir, {
prefix: "alchemy-vite-do-",
tempRoot,
// Keep the fixture's real stack file available for local
// `alchemy dev` smoke tests. The live deploy below uses an inline
// stack so cleanup stays under the provider test harness.
entries: [
"alchemy.run.ts",
"index.html",
"package.json",
"vite.config.ts",
"src",
],
});
const memoInclude = [
"index.html",
"src/**",
"package.json",
"vite.config.ts",
];
const site = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite("ViteDo", {
...viteProps(rootDir, memoInclude),
compatibility: {
date: "2026-03-17",
flags: ["nodejs_compat"],
},
// The documented SPA + Worker-API pattern (vite.mdx): the SPA
// fallback owns unmatched paths while the glob pins the API
// namespace to the Worker regardless of request mode.
assets: {
notFoundHandling: "single-page-application",
runWorkerFirst: ["/api/*"],
},
env: {
Counter: Cloudflare.DurableObject<ViteDoCounter>("Counter", {
className: "Counter",
}),
},
});
}),
);
expect(site.url).toBeDefined();
yield* expectWorkerExists(site.workerName, accountId);
yield* expectUrlContains(`${site.url!}/`, "Vite DO fixture", {
timeout: "120 seconds",
label: "vite do fixture assets",
});
// A deep link matches no asset: the SPA fallback serves the shell
// (static routing applies it to every request mode, so a plain GET
// with no Sec-Fetch-Mode header gets the shell too).
yield* expectUrlContains(
`${site.url!}/deep/link`,
"Vite DO fixture",
{
timeout: "60 seconds",
label: "vite do spa deep link",
},
);
const reset = yield* fetchJsonReady<{ ok: boolean }>(
`${site.url!}/api/reset`,
);
expect(reset.ok).toBe(true);
const first = yield* fetchJsonReady<{ count: number }>(
`${site.url!}/api/count`,
);
expect(first.count).toBe(1);
const second = yield* fetchJsonReady<{ count: number }>(
`${site.url!}/api/count`,
);
expect(second.count).toBe(2);
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
test.provider(
"Vite: Container binding on env deploys a container-backed DO class",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
yield* stack.destroy();
const rootDir = yield* cloneFixture(containerFixtureDir, {
prefix: "alchemy-vite-container-",
tempRoot,
entries: ["index.html", "package.json", "vite.config.ts", "src"],
});
const memoInclude = [
"index.html",
"src/**",
"package.json",
"vite.config.ts",
];
// A `Cloudflare.Container` declaration on `env` is Effect-shaped —
// before #997 the Vite build's env resolution ran it as an inlined
// env Effect and the deploy died before the build started.
const site = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite("ViteContainer", {
...viteProps(rootDir, memoInclude),
compatibility: {
date: "2026-03-17",
flags: ["nodejs_compat"],
},
assets: {
runWorkerFirst: ["/api/*"],
},
env: {
ECHO: Cloudflare.Container("ViteEchoContainer", {
className: "EchoObject",
image: "mendhak/http-https-echo:latest",
observability: { logs: { enabled: true } },
}),
},
});
}),
);
expect(site.url).toBeDefined();
yield* expectWorkerExists(site.workerName, accountId);
yield* expectUrlContains(`${site.url!}/`, "Vite Container fixture", {
timeout: "120 seconds",
label: "vite container fixture assets",
});
// The echo image reflects the request as JSON ("method" only appears
// in a real echo response, never in an error page) — proof the request
// went Worker → DO class → container port 8080 and back.
const echo = yield* fetchContainerReady(
`${site.url!}/api/echo`,
"method",
);
expect(echo).toContain("method");
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site.workerName, accountId);
}).pipe(logLevel),
// Container image pull + push + rollout comfortably exceeds the plain
// Vite deploy budget (mirrors Container.test.ts).
{
tags: [
"provider:cloudflare:container",
"provider:cloudflare:worker",
"live",
],
timeout: 600_000,
},
);
// The documented interception pattern (solidstart.mdx hand-rolled SSR /
// static-site.mdx docs-site worker): the client build emits an index.html
// that would shadow `/` under assets-first routing, so the worker runs
// first, renders every page, and delegates real static files to the
// ASSETS binding itself.
test.provider(
"Vite: runWorkerFirst true intercepts asset paths and delegates to ASSETS",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
yield* stack.destroy();
const rootDir = yield* cloneFixture(workerFirstFixtureDir, {
prefix: "alchemy-vite-worker-first-",
tempRoot,
entries: [
"index.html",
"package.json",
"public",
"src",
"vite.config.ts",
"worker.ts",
],
});
const memoInclude = [
"index.html",
"public/**",
"src/**",
"package.json",
"vite.config.ts",
"worker.ts",
];
const site = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite("ViteWorkerFirst", {
...viteProps(rootDir, memoInclude),
main: "worker.ts",
assets: { runWorkerFirst: true },
});
}),
);
expect(site.url).toBeDefined();
yield* expectWorkerExists(site.workerName, accountId);
// `/` matches the built index.html asset, but the worker runs first
// and renders — the raw template must never serve.
yield* expectUrlContains(`${site.url!}/`, "worker-first-rendered:/", {
timeout: "120 seconds",
label: "worker-first render at /",
});
const home = yield* fetchTextReady(`${site.url!}/`);
expect(home).not.toContain("worker-first-raw-template");
// Any other route renders too — the worker sees every request.
yield* expectUrlContains(
`${site.url!}/some/page`,
"worker-first-rendered:/some/page",
{ timeout: "60 seconds", label: "worker-first render deep" },
);
// Real static files still serve, through the worker's own ASSETS
// delegation.
yield* expectUrlContains(
`${site.url!}/robots.txt`,
"worker-first-static-asset",
{ timeout: "60 seconds", label: "worker-first ASSETS delegation" },
);
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
test.provider(
"Vite: main overrides the worker entry from the Vite config",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
yield* stack.destroy();
const rootDir = yield* cloneFixture(doFixtureDir, {
prefix: "alchemy-vite-main-",
tempRoot,
entries: [
"alchemy.run.ts",
"index.html",
"package.json",
"vite.config.ts",
"src",
],
});
const memoInclude = [
"index.html",
"src/**",
"package.json",
"vite.config.ts",
];
// The fixture's vite.config.ts points the ssr environment at
// `src/worker.ts`. `main` must take precedence and deploy
// `src/worker-main.ts`, which re-exports the Durable Object and
// additionally answers `/api/entry`.
const site = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite("ViteMain", {
...viteProps(rootDir, memoInclude),
main: "src/worker-main.ts",
compatibility: {
date: "2026-03-17",
flags: ["nodejs_compat"],
},
assets: {
runWorkerFirst: ["/api/*"],
},
env: {
Counter: Cloudflare.DurableObject<ViteDoCounter>("Counter", {
className: "Counter",
}),
},
});
}),
);
expect(site.url).toBeDefined();
yield* expectWorkerExists(site.workerName, accountId);
const entry = yield* fetchJsonReady<{ entry: string }>(
`${site.url!}/api/entry`,
);
expect(entry.entry).toBe("worker-main");
const reset = yield* fetchJsonReady<{ ok: boolean }>(
`${site.url!}/api/reset`,
);
expect(reset.ok).toBe(true);
const first = yield* fetchJsonReady<{ count: number }>(
`${site.url!}/api/count`,
);
expect(first.count).toBe(1);
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
test.provider(
"Vite: React Router RSC deploys from a distilled manifest",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
yield* stack.destroy();
const rootDir = yield* cloneFixture(reactRouterRscFixtureDir, {
prefix: "alchemy-vite-rsc-",
tempRoot,
// Keep the fixture's stack file available for local `alchemy dev`
// smoke tests. The live deploy below uses an inline stack so cleanup
// stays under the provider test harness.
entries: [
"alchemy.run.ts",
"app",
"package.json",
"react-router-vite",
"tsconfig.json",
"vite.config.ts",
],
});
const memoInclude = [
"app/**",
"react-router-vite/**",
"package.json",
"tsconfig.json",
"vite.config.ts",
];
const compatibility = {
date: "2026-03-10",
flags: ["nodejs_compat"],
};
const viteEnvironments = { entry: "rsc", children: ["ssr"] };
const site = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite("ReactRouterRsc", {
...viteProps(rootDir, memoInclude),
// No `assets` config: the RSC build emits no index.html (HTML
// is server-rendered), so SSR routes fall through to the RSC
// handler while hydration assets serve from the asset layer.
// `runWorkerFirst: true` would route `/assets/*` into the RSC
// handler, which cannot serve them.
compatibility,
viteEnvironments,
});
}),
);
expect(site.url).toBeDefined();
yield* expectWorkerExists(site.workerName, accountId);
yield* expectUrlContains(`${site.url!}/`, "React Router Vite", {
timeout: "120 seconds",
label: "react router rsc home route",
});
yield* expectUrlContains(`${site.url!}/about`, "About", {
timeout: "60 seconds",
label: "react router rsc client route",
});
// The hydration bundle the SSR HTML references must serve from the
// asset layer — the routing gap that let `runWorkerFirst: true`
// masquerade as live-tested (the old test never fetched an asset).
const home = yield* fetchTextReady(`${site.url!}/`);
const assetPath = home.match(/\/assets\/[^"']+\.js/)?.[0];
expect(assetPath).toBeDefined();
const bundle = yield* fetchTextReady(`${site.url!}${assetPath}`);
expect(bundle.length).toBeGreaterThan(0);
expect(bundle).not.toContain("<html");
const render = yield* fetchJsonReady<{ ok: boolean; html: string }>(
`${site.url!}/worker-render`,
);
expect(render.ok).toBe(true);
expect(render.html).toContain(
"Worker render via the ssr environment.",
);
yield* stack.destroy();
yield* waitForWorkerToBeDeleted(site.workerName, accountId);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:worker", "live"], timeout: 360_000 },
);
devTest.provider(
"Vite dev: each app runs in a child rooted at its own cwd",
(stack) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* stack.destroy();
const entries = [
"index.html",
"package.json",
"vite.config.ts",
"worker.ts",
];
const clone = (prefix: string) =>
cloneFixture(viteChildFixtureDir, { prefix, tempRoot, entries });
const [rootA, rootB] = yield* Effect.all(
[clone("alchemy-vite-child-a-"), clone("alchemy-vite-child-b-")],
{ concurrency: "unbounded" },
);
yield* Effect.all([
fs.writeFileString(
path.join(rootA, "index.html"),
htmlPage("child-app-a"),
),
fs.writeFileString(
path.join(rootB, "index.html"),
htmlPage("child-app-b"),
),
]);
const deployed = yield* stack.deploy(
Effect.gen(function* () {
const props = (rootDir: string) => ({
...viteProps(rootDir, entries),
main: "worker.ts",
assets: {
notFoundHandling: "single-page-application" as const,
},
dev: { port: 0 },
});
const appA = yield* Cloudflare.Website.Vite(
"ViteChildA",
props(rootA),
);
const appB = yield* Cloudflare.Website.Vite(
"ViteChildB",
props(rootB),
);
const defaultPortWorker = yield* Cloudflare.Worker(
"ViteChildDefaultPortWorker",
{
script:
'export default { fetch: () => new Response("default-port-worker") };',
},
);
return { appA, appB, defaultPortWorker };
}),
);
expect(deployed.appA.url).not.toBe(deployed.appB.url);
yield* Effect.all(
[
expectUrlContains(
deployed.defaultPortWorker.url!,
"default-port-worker",
{
timeout: "30 seconds",
label: "default-port worker",
},
),
expectUrlContains(deployed.appA.url!, "child-app-a", {
timeout: "30 seconds",
label: "child A HTML",
}),
expectUrlContains(deployed.appB.url!, "child-app-b", {
timeout: "30 seconds",
label: "child B HTML",
}),
expectUrlContains(deployed.appA.url!, rootA, {
timeout: "30 seconds",
label: "child A cwd",
}),
expectUrlContains(deployed.appB.url!, rootB, {
timeout: "30 seconds",
label: "child B cwd",
}),
],
{ concurrency: "unbounded" },
);
}).pipe(
Effect.ensuring(
stack.destroy().pipe(Effect.orDie).pipe(Effect.ignore),
),
logLevel,
),
{ tags: ["provider:cloudflare:worker", "local"], timeout: 120_000 },
);
devTest.provider(
"Vite dev: client-only SPA serves the shell for deep links",
(stack) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* stack.destroy();
const rootDir = yield* cloneFixture(spaFixtureDir, {
prefix: "alchemy-vite-spa-dev-",
tempRoot,
entries: ["index.html", "package.json", "src"],
});
const marker = "vite-spa-dev-marker";
yield* fs.writeFileString(
path.join(rootDir, "index.html"),
htmlPage(marker),
);
const site = yield* stack.deploy(
Effect.gen(function* () {
return yield* Cloudflare.Website.Vite("ViteSpaDev", {
...viteProps(rootDir, ["index.html", "src/**", "package.json"]),
assets: {
notFoundHandling: "single-page-application" as const,
},
dev: { port: 0 },
});
}),
);
// Local identity: the url points at the alchemy dev proxy — no
// cloud Worker exists.
expect(site.url).toBeDefined();
expect(site.url).toMatch(/^http:\/\/localhost:\d+/);
// The SPA shell serves at the root...
yield* expectUrlContains(`${site.url!}/`, marker, {
timeout: "60 seconds",
label: "spa dev shell",
});
// ...and a deep link to an unregistered route falls back to the
// shell so client-side routing can boot.
yield* expectUrlContains(`${site.url!}/deep/link/route`, marker, {
timeout: "30 seconds",
label: "spa dev deep link fallback",
});
// The dev server serves the client module the shell references.
yield* expectUrlContains(`${site.url!}/src/main.ts`, "hydrated", {
timeout: "30 seconds",
label: "spa dev module asset",
});
}).pipe(
Effect.ensuring(
stack.destroy().pipe(Effect.orDie).pipe(Effect.ignore),
),
logLevel,
),
{ tags: ["local"], timeout: 180_000 },
);
devTest.provider(
"Vite dev: TanStack Start keeps Alchemy-managed R2 bindings",
(stack) =>
Effect.gen(function* () {
const { accountId } = yield* yield* CloudflareEnvironment;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const bucketNames = new Set<string>();
const cleanup = Effect.gen(function* () {
yield* stack.destroy();
for (const bucketName of bucketNames) {
yield* waitForBucketToBeDeleted(bucketName, accountId);
}
});
const body = Effect.gen(function* () {
yield* stack.destroy();
const rootDir = yield* cloneFixture(tanstackDevBindingsFixtureDir, {
prefix: "alchemy-tanstack-dev-bindings-",
tempRoot,
entries: [
"alchemy.run.ts",
"package.json",
"tsconfig.json",
"vite.config.ts",
"src",
],
});
const indexRoutePath = path.join(rootDir, "src/routes/index.tsx");
const memoInclude = [
"src/**",
"package.json",
"tsconfig.json",
"vite.config.ts",
"alchemy.run.ts",
];
const key = `dev-binding-${Date.now()}.txt`;
yield* fs.writeFileString(
indexRoutePath,
tanstackIndexRouteSource("hmr-marker-v1"),
);
const deploy = (bucketId: string, marker: string) =>
stack.deploy(
Effect.gen(function* () {
const bucket = yield* Cloudflare.R2.Bucket(bucketId, {
forceDestroy: true,
});
const worker = yield* Cloudflare.Website.Vite(
"TanStackDevBindings",
{
...viteProps(rootDir, memoInclude),
assets: {
runWorkerFirst: true,
},
dev: {
port: 0,
},
env: {
BUCKET: bucket,
DEV_MARKER: marker,
},
},
);
return { bucket, worker };
}),
);
const first = yield* deploy("DevBucketA", "dev-marker-v1");
bucketNames.add(first.bucket.bucketName);
expect(first.worker.url).toBeDefined();
const r2Url = (base: string) =>
joinUrl(base, `/api/r2?key=${encodeURIComponent(key)}`);
yield* expectUrlContains(
joinUrl(first.worker.url!, "/"),
"hmr-marker-v1",
{
timeout: "30 seconds",
label: "tanstack dev initial route",
},
);
const env1 = yield* fetchJsonReady<{ marker: string }>(
r2Url(first.worker.url!),
);
expect(env1.marker).toBe("dev-marker-v1");
const put1 = yield* putTextJsonReady<{ ok: boolean }>(
r2Url(first.worker.url!),
"from-a",
);
expect(put1.ok).toBe(true);
const get1 = yield* fetchJsonReady<{ value: string | null }>(
r2Url(first.worker.url!),
);
expect(get1.value).toBe("from-a");
// Change only a TanStack route file. The stack is not re-applied; the
// local Vite server should render the updated route through the same
// Alchemy proxy.
yield* fs.writeFileString(
indexRoutePath,
tanstackIndexRouteSource("hmr-marker-v2"),
);
yield* expectUrlContains(
joinUrl(first.worker.url!, "/"),
"hmr-marker-v2",
{
timeout: "30 seconds",
label: "tanstack dev updated route",
},
);
const second = yield* deploy("DevBucketB", "dev-marker-v2");
bucketNames.add(second.bucket.bucketName);
expect(second.worker.url).toBe(first.worker.url);
const env2 = yield* fetchJsonReady<{ marker: string }>(
r2Url(second.worker.url!),
);
expect(env2.marker).toBe("dev-marker-v2");
// The Worker was rebound to DevBucketB. The object written through
// DevBucketA should not be visible through the new binding.
const reboundRead = yield* fetchJsonReady<{ value: string | null }>(
r2Url(second.worker.url!),
);
expect(reboundRead.value).toBeNull();
const put2 = yield* putTextJsonReady<{ ok: boolean }>(
r2Url(second.worker.url!),
"from-b",
);
expect(put2.ok).toBe(true);
const get2 = yield* fetchJsonReady<{ value: string | null }>(
r2Url(second.worker.url!),
);
expect(get2.value).toBe("from-b");
});
const exit = yield* Effect.exit(body);
if (Exit.isSuccess(exit)) {
yield* cleanup;
return exit.value;
}
yield* cleanup.pipe(
Effect.tapError((error) =>
Effect.logError("Vite dev live test cleanup failed", error),
),
Effect.ignore,
);
return yield* Effect.failCause(exit.cause);
}).pipe(logLevel),
{ tags: ["provider:cloudflare:r2", "local"], timeout: 360_000 },
);
},
);
const freshConn = HttpClient.mapRequest(
HttpClientRequest.setHeader("connection", "close"),
);
// Both providers return `worker.url` as a bare origin these days, but an
// external dev server URL parsed from stdout can still carry a trailing
// slash. Join without producing a `//` path that the dev server's router
// won't match.
const joinUrl = (base: string, path: string) =>
`${base.replace(/\/+$/, "")}${path}`;
const fetchTextReady = (url: string) =>
Effect.gen(function* () {
const client = freshConn(yield* HttpClient.HttpClient);
return yield* client.get(url).pipe(
Effect.flatMap((res) =>
res.status === 200
? res.text
: Effect.fail(new Error(`Worker not ready: ${res.status}`)),
),
Effect.retry({
// Capped interval, ~90s total budget (workers.dev propagation).
schedule: Schedule.min([
Schedule.exponential("500 millis"),
Schedule.spaced("2 seconds"),
]),
times: 45,
}),
Effect.orDie,
);
});
const fetchJsonReady = <T>(url: string) =>
Effect.gen(function* () {
const client = freshConn(yield* HttpClient.HttpClient);
return yield* client.get(url).pipe(
Effect.flatMap((res) =>
res.status === 200
? Effect.flatMap(res.text, (body) =>
Effect.try({
try: () => JSON.parse(body) as T,
catch: () => new Error(`non-json body: ${body}`),
}),
)
: Effect.fail(new Error(`Worker not ready: ${res.status}`)),
),
Effect.retry({
// Capped interval, ~90s total budget (workers.dev / DO propagation).
schedule: Schedule.min([
Schedule.exponential("500 millis"),
Schedule.spaced("2 seconds"),
]),
times: 45,
}),
);
});
// Retry a container-backed route until it answers 200 with a body containing
// `expected` — the container's first request rides through image provisioning
// and instance cold start, which comfortably outlasts fetchJsonReady's budget
// (mirrors Container.test.ts's readiness poll).
const fetchContainerReady = (url: string, expected: string) =>
Effect.gen(function* () {
const client = freshConn(yield* HttpClient.HttpClient);
return yield* client.get(url).pipe(
Effect.flatMap((res) =>
res.text.pipe(
Effect.flatMap((body) =>
res.status === 200 && body.includes(expected)
? Effect.succeed(body)
: Effect.fail(
new Error(
`container not ready: ${res.status} ${body.slice(0, 200)}`,
),
),
),
),
),
Effect.timeout("10 seconds"),
Effect.retry({
// Cap exponential backoff at 3s — snappy fast path without the
// geometric blow-up dominating wall time on a slow rollout.
schedule: Schedule.min([
Schedule.exponential("500 millis"),
Schedule.spaced("3 seconds"),
]),
times: 60,
}),
);
});
const putTextJsonReady = <T>(url: string, body: string) =>
Effect.gen(function* () {
return yield* HttpClient.execute(
HttpClientRequest.put(url).pipe(
HttpClientRequest.bodyText(body, "text/plain"),
),
).pipe(
Effect.flatMap((res) =>
res.status === 200
? Effect.flatMap(res.text, (responseBody) =>
Effect.try({
try: () => JSON.parse(responseBody) as T,
catch: () => new Error(`non-json body: ${responseBody}`),
}),
)
: Effect.fail(new Error(`Worker not ready: ${res.status}`)),
),
Effect.retry({
// Capped interval, ~90s total budget (workers.dev / DO propagation).
schedule: Schedule.min([
Schedule.exponential("500 millis"),
Schedule.spaced("2 seconds"),
]),
times: 45,
}),
);
});
// Assert that the site's *current* client bundle contains `marker`.
//
// The bundle filename is content-addressed, so an env-only redeploy changes
// the bundle URL referenced by `index.html`. A PoP can keep serving the
// previous deployment's `index.html` for a while after the version flip
// (cache-busting query params don't help when the whole deployment is stale
// at that edge) — so discovering the bundle URL *once* and then polling that
// asset latches onto the old, immutable bundle and times out waiting for a
// marker that will never appear there. Instead, re-discover the bundle URL
// from `index.html` on every attempt so the assertion converges as soon as
// the new deployment propagates.
const expectBundleContains = (
siteUrl: string,
marker: string,
options: { label?: string } = {},
) =>
Effect.gen(function* () {
const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
yield* Effect.gen(function* () {
// Cache-bust the index fetch — the unique query string defeats the
// CDN cache key for caches that respect it.
const res = yield* client.get(`${siteUrl}/`, {
urlParams: { __alchemy_cb: String(Date.now()) },
headers: { "cache-control": "no-cache", pragma: "no-cache" },
});
const html = yield* res.text;
const match = html.match(
/<script[^>]+src="(\/assets\/[^"]+\.js)"[^>]*>/i,
);
if (!match) {
// Fresh deploys can briefly return Cloudflare's "There is
// nothing here yet" HTML page instead of the SPA index — retry.
return yield* Effect.fail(
new Error(
`Could not find /assets/*.js script tag in HTML: ${html.slice(0, 200)}`,
),
);
}
const bundleRes = yield* client.get(`${siteUrl}${match[1]}`, {
urlParams: { __alchemy_cb: String(Date.now()) },
headers: { "cache-control": "no-cache", pragma: "no-cache" },
});
const bundle = yield* bundleRes.text;
if (!bundle.includes(marker)) {
return yield* Effect.fail(
new Error(
`bundle ${match[1]} does not (yet) contain marker "${marker}"`,
),
);
}
}).pipe(
Effect.retry({
// ~2 minutes total: capped exponential sampling through edge
// propagation of both the fresh index.html and the new asset.
schedule: Schedule.max([
Schedule.min([
Schedule.exponential("500 millis", 1.5),
Schedule.spaced("5 seconds"),
]),
Schedule.recurs(30),
]),
}),
Effect.tapError((error) =>
Effect.logError(
`expectBundleContains(${options.label ?? marker}) failed`,
error,
),
),
);
});
const viteProps = (rootDir: string, memoInclude: string[]) => ({
rootDir,
workersDev: true,
compatibility: {
date: "2024-09-23",
flags: ["nodejs_compat"],
},
memo: { include: memoInclude },
});
const htmlPage = (marker: string) => `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>${marker}</title>
</head>
<body>
<div id="app">${marker}</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
`;
const tanstackIndexRouteSource = (marker: string) => `
/** @jsxImportSource react */
import { createFileRoute } from "@tanstack/react-router";
const marker = ${JSON.stringify(marker)};
export const Route = createFileRoute("/")({
component: Home,
});
function Home() {
return <main>{marker}</main>;
}
`;
const waitForBucketToBeDeleted = Effect.fn(function* (
bucketName: string,
accountId: string,
) {
// Dev-mode buckets are purely virtual (`dev:`-prefixed identity) — there
// is no cloud bucket to wait on, and the real API rejects the name.
if (isLocalId(bucketName)) return;
yield* r2
.getBucket({
accountId,
bucketName,
})
.pipe(
Effect.flatMap(() => Effect.fail(new BucketStillExists())),
Effect.retry({
while: (error): error is BucketStillExists =>
error instanceof BucketStillExists,
schedule: Schedule.max([
Schedule.min([
Schedule.exponential("200 millis"),
Schedule.spaced("2 seconds"),
]),
Schedule.recurs(20),
]),
}),
Effect.catchTag("NoSuchBucket", () => Effect.void),
);
});
class BucketStillExists extends Data.TaggedError("BucketStillExists") {}