1import {
2 layerRuntime,
3 registerHttpServer,
4} from "@alchemy.run/cloudflare-runtime/core";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as Match from "effect/Match";
8import * as Stdio from "effect/Stdio";
9import * as Stream from "effect/Stream";
10import * as FetchHttpClient from "effect/http/FetchHttpClient";
11import * as NodeV8 from "node:v8";
12import {
13 Artifacts,
14 createArtifactStore,
15 makeScopedArtifacts,
16} from "../../Artifacts.ts";
17import { CloudflareAuth } from "../Auth/AuthProvider.ts";
18import * as Credentials from "../Credentials.ts";
19import * as CloudflareEnvironment from "../CloudflareEnvironment.ts";
20import * as RpcServerEnvironment from "../../Local/RpcServerEnvironment.ts";
21import { PlatformServices, runMain } from "../../Util/PlatformServices.ts";
22import { materializeRuntimeBindings } from "./RuntimeBindings.ts";
23import { loadSource, SourceProviderError } from "./Source.ts";
24import * as Vite from "./Sources/Vite.ts";
25import {
26 type ViteChildConfig,
27 VITE_CHILD_READY_PREFIX,
28 VITE_CHILD_READY_SUFFIX,
29} from "./ViteChild.shared.ts";
30
31const readConfig = Effect.gen(function* () {
32 const stdio = yield* Stdio.Stdio;
33 const chunks = yield* Stream.runCollect(stdio.stdin);
34 const bytes = Buffer.concat(chunks);
35 // A cross-runtime spawn (bun engine → node child) sends JSON because
36 // bun's `v8.serialize` output is unreadable by real V8. Sniff the
37 // encoding by the JSON marker: only JSON starts with `{` — node's V8
38 // payloads start with 0xFF and bun's JSC serialization with its own
39 // binary header, so same-runtime spawns fall through to deserialize.
40 return (
41 bytes[0] === 0x7b // "{"
42 ? JSON.parse(bytes.toString("utf8"))
43 : NodeV8.deserialize(bytes)
44 ) as ViteChildConfig;
45});
46
47const program = Effect.scoped(
48 Effect.gen(function* () {
49 const config = yield* readConfig;
50 const credentials = Credentials.fromAuthProvider().pipe(
51 Layer.provideMerge(CloudflareEnvironment.fromProfile()),
52 Layer.provide(CloudflareAuth),
53 );
54 const runtimeContext = yield* Layer.unwrap(
55 Effect.gen(function* () {
56 const environment = yield* CloudflareEnvironment.CloudflareEnvironment;
57 return layerRuntime({
58 api: { accountId: Effect.map(environment, (env) => env.accountId) },
59 storage: { directory: config.storageDirectory },
60 });
61 }),
62 ).pipe(
63 Layer.provide(Layer.mergeAll(credentials, FetchHttpClient.layer)),
64 Layer.build,
65 );
66 // The non-runtime fields are consumed here; everything else in
67 // `config.worker` is the runtime worker shape `viteDev` expects, so new
68 // runtime fields flow through without being re-listed.
69 const {
70 compatibility,
71 main,
72 viteEnvironments,
73 hasAssets,
74 bindingDescriptors,
75 devRemote,
76 devAccess,
77 ...runtimeWorker
78 } = config.worker;
79 const bindings = yield* materializeRuntimeBindings(
80 {
81 name: config.worker.name,
82 env: config.env,
83 hasAssets,
84 bindingDescriptors,
85 devRemote,
86 devAccess,
87 },
88 {
89 accountId: config.accountId,
90 selfUrl: config.publicUrl,
91 stack: config.stack,
92 },
93 );
94 const source = config.source;
95 const viteHost = "127.0.0.1";
96 // `viteDev` resolves `port: 0` per the loaded Vite: a true OS-assigned
97 // random port on Vite >= 8.2.1 (vitejs/vite#23158), a probed ephemeral
98 // port on older Vite.
99 const vitePort = source ? undefined : 0;
100 const url = source
101 ? yield* loadSource(source.descriptor).pipe(
102 // `loadSource` is typed against the full `SourceServices` union
103 // (which includes the per-run Artifacts cache the live provider
104 // supplies); the dev child has no run-scoped cache, so hand the
105 // module a fresh one.
106 Effect.provideService(
107 Artifacts,
108 makeScopedArtifacts(createArtifactStore(), source.fqn),
109 ),
110 Effect.flatMap((provider) =>
111 provider.dev({
112 id: source.id,
113 fqn: source.fqn,
114 workerName: config.worker.name,
115 compatibility,
116 entry: { kind: "external" },
117 stack: config.stack,
118 env: config.env,
119 extraOptions: undefined,
120 assets: source.assets,
121 worker: {
122 bindings,
123 durableObjectNamespaces: config.worker.durableObjectNamespaces,
124 hyperdrives: config.worker.hyperdrives,
125 queueConsumers: Effect.succeed(config.worker.queueConsumers),
126 assets: config.worker.assets,
127 },
128 runtimeContext,
129 }),
130 ),
131 Effect.flatMap((handle) =>
132 Match.value(handle).pipe(
133 Match.when({ mode: "server" }, (handle) =>
134 (handle.serviceBinding === "http"
135 ? registerHttpServer(config.worker.name, handle.url).pipe(
136 Effect.provideContext(runtimeContext),
137 )
138 : Effect.void
139 ).pipe(Effect.as(handle.url.toString())),
140 ),
141 Match.when({ mode: "bundle" }, () =>
142 Effect.fail(
143 new SourceProviderError({
144 provider: source.descriptor.provider,
145 message:
146 "A source declared devMode 'server' but returned a bundle-mode dev handle.",
147 }),
148 ),
149 ),
150 Match.exhaustive,
151 ),
152 ),
153 )
154 : yield* Vite.viteDev(
155 config.rootDir,
156 config.env,
157 {
158 main,
159 compatibilityDate: compatibility.date,
160 compatibilityFlags: compatibility.flags,
161 viteEnvironments,
162 worker: { ...runtimeWorker, bindings },
163 context: runtimeContext,
164 },
165 {
166 host: viteHost,
167 port: vitePort,
168 strictPort: false,
169 },
170 ).pipe(Effect.map((server) => server.resolvedUrls?.local[0]));
171 if (!url) {
172 return yield* Effect.die("Dev server child started without a local URL");
173 }
174 yield* Effect.sync(() => {
175 process.stdout.write(
176 `${VITE_CHILD_READY_PREFIX}${url}${VITE_CHILD_READY_SUFFIX}\n`,
177 );
178 });
179 return yield* Effect.never;
180 }),
181);
182
183runMain(
184 program.pipe(
185 Effect.provide(
186 RpcServerEnvironment.fromEnv().pipe(Layer.provideMerge(PlatformServices)),
187 ),
188 ),
189);