1import {
2 makeDeployTarget,
3 type DeployTarget,
4 type DeployTargetServer,
5} from "@alchemy.run/frontend-frameworks/core";
6import * as Miniflare from "../miniflare/miniflare.ts";
7import {
8 moduleTypeFromExtension,
9 type MiniflareModule,
10} from "../miniflare/miniflare-module.ts";
11import * as Effect from "effect/Effect";
12import { cast } from "effect/Function";
13import * as Path from "effect/Path";
14import type * as Options from "./Options.ts";
15
16/**
17 * The e2e harness's Cloudflare deploy target: a framework-core `DeployTarget`
18 * value whose `serve` boots miniflare over a `BuildOutput` (the
19 * preview-parity engine the harness has always used for `e2e preview` / the
20 * Playwright `live` mode). Serving built output is *architecturally* the
21 * target's concern — the implementation lives in the harness for now because
22 * miniflare is a harness dependency.
23 */
24export interface CloudflareTarget extends DeployTarget<Options.CloudflareTargetOptions> {
25 /** Always available on the harness's cloudflare target. */
26 readonly serve: NonNullable<DeployTarget["serve"]>;
27}
28
29/**
30 * Miniflare requires a script even for an assets-only deploy, so when a
31 * `BuildOutput` carries no server modules (and the preview config hand-rolls
32 * none), this stub stands in behind `routerConfig.has_user_worker: false`.
33 * Every real request must be answered by the asset layer — a request reaching
34 * the stub means the routing is misconfigured, so it 500s loudly.
35 */
36const ASSETS_ONLY_STUB: MiniflareModule = {
37 type: "ESModule",
38 path: "__assets-only-stub.js",
39 contents: `export default { fetch: () => new Response("assets-only build: no user worker — a request reached the stub, so asset routing is misconfigured", { status: 500 }) }`,
40};
41
42export const makeCloudflareTarget = (
43 config: Options.CloudflareTargetOptions,
44): CloudflareTarget =>
45 makeDeployTarget({
46 platform: "cloudflare",
47 config,
48 bundle: {
49 conditions: ["workerd", "worker", "module", "browser"],
50 external: ["cloudflare:"],
51 },
52 serve: (context) =>
53 Effect.gen(function* () {
54 const path = yield* Path.Path;
55 const preview = config.preview ?? {};
56 const build = context.output;
57 const modules = build.serverModules?.flatMap(
58 (module): MiniflareModule | Array<MiniflareModule> => {
59 const type = moduleTypeFromExtension(path.extname(module.name));
60 if (type === "SourceMap") {
61 return [];
62 }
63 return {
64 path: module.name,
65 type,
66 contents: module.content as
67 | string
68 | Uint8Array<ArrayBuffer>
69 | undefined,
70 };
71 },
72 );
73 // Assets-only build: no server modules in the BuildOutput and none
74 // hand-rolled in the preview config. The harness synthesizes the
75 // stub script miniflare requires and routes every request to the
76 // asset layer (`has_user_worker: false`) — fixtures don't hand-roll
77 // stubs.
78 const resolvedModules = modules ?? preview.modules;
79 const assetsOnly =
80 resolvedModules === undefined || resolvedModules.length === 0;
81 const instance = yield* Effect.acquireDisposable(
82 Effect.promise(
83 async () =>
84 await Miniflare.createMiniflare({
85 ...preview,
86 assets:
87 preview.assets && build.clientDirectory
88 ? {
89 ...preview.assets,
90 directory: build.clientDirectory,
91 ...(assetsOnly
92 ? {
93 routerConfig: {
94 ...preview.assets.routerConfig,
95 has_user_worker: false,
96 },
97 }
98 : undefined),
99 }
100 : undefined,
101 modules: assetsOnly ? [ASSETS_ONLY_STUB] : resolvedModules,
102 }),
103 ),
104 );
105 return {
106 url: instance.url.toString(),
107 // miniflare's Response type differs nominally from the DOM's; the
108 // harness has always cast across this boundary.
109 fetch: cast<
110 Miniflare.MiniflareInstance["fetch"],
111 DeployTargetServer["fetch"]
112 >(instance.fetch),
113 };
114 }),
115 });