alchemy 0.55.3 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alchemy.js +46 -246
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +7 -0
- package/lib/alchemy.js.map +1 -1
- package/lib/cloudflare/bundle/plugin-hot-reload.d.ts +1 -3
- package/lib/cloudflare/bundle/plugin-hot-reload.d.ts.map +1 -1
- package/lib/cloudflare/bundle/plugin-hot-reload.js +4 -1
- package/lib/cloudflare/bundle/plugin-hot-reload.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/compatibility-presets.d.ts +1 -1
- package/lib/cloudflare/compatibility-presets.js +1 -1
- package/lib/cloudflare/compatibility-presets.js.map +1 -1
- package/lib/cloudflare/local-dev-cloudflare-shim.d.ts +3 -1
- package/lib/cloudflare/local-dev-cloudflare-shim.d.ts.map +1 -1
- package/lib/cloudflare/local-dev-cloudflare-shim.js +10 -5
- package/lib/cloudflare/local-dev-cloudflare-shim.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts +22 -0
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -0
- package/lib/cloudflare/miniflare/build-worker-options.js +394 -0
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -0
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts +23 -0
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -0
- package/lib/cloudflare/miniflare/miniflare-controller.js +110 -0
- package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -0
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +5 -15
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts +1 -5
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.js +17 -22
- package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
- package/lib/cloudflare/website.js +4 -4
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-bundle.d.ts +2 -0
- package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/worker-bundle.js +19 -3
- package/lib/cloudflare/worker-bundle.js.map +1 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +76 -97
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/context.d.ts +11 -0
- package/lib/context.d.ts.map +1 -1
- package/lib/context.js +8 -0
- package/lib/context.js.map +1 -1
- package/lib/scope.d.ts +12 -0
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +25 -0
- package/lib/scope.js.map +1 -1
- package/package.json +3 -2
- package/src/alchemy.ts +7 -0
- package/src/cloudflare/bundle/plugin-hot-reload.ts +4 -3
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/compatibility-presets.ts +1 -1
- package/src/cloudflare/local-dev-cloudflare-shim.ts +11 -5
- package/src/cloudflare/miniflare/build-worker-options.ts +454 -0
- package/src/cloudflare/miniflare/miniflare-controller.ts +128 -0
- package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +6 -15
- package/src/cloudflare/miniflare/remote-binding-proxy.ts +18 -23
- package/src/cloudflare/website.ts +4 -4
- package/src/cloudflare/worker-bundle.ts +28 -3
- package/src/cloudflare/worker.ts +90 -125
- package/src/context.ts +19 -0
- package/src/scope.ts +27 -0
- package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts +0 -14
- package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts.map +0 -1
- package/lib/cloudflare/miniflare/miniflare-worker-options.js +0 -468
- package/lib/cloudflare/miniflare/miniflare-worker-options.js.map +0 -1
- package/lib/cloudflare/miniflare/miniflare.d.ts +0 -40
- package/lib/cloudflare/miniflare/miniflare.d.ts.map +0 -1
- package/lib/cloudflare/miniflare/miniflare.js +0 -142
- package/lib/cloudflare/miniflare/miniflare.js.map +0 -1
- package/src/cloudflare/miniflare/miniflare-worker-options.ts +0 -599
- package/src/cloudflare/miniflare/miniflare.ts +0 -183
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as miniflare from "miniflare";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { findOpenPort } from "../../util/find-open-port.ts";
|
|
5
|
+
import type { HTTPServer } from "../../util/http.ts";
|
|
6
|
+
import { logger } from "../../util/logger.ts";
|
|
7
|
+
import { AsyncMutex } from "../../util/mutex.ts";
|
|
8
|
+
import {
|
|
9
|
+
buildWorkerOptions,
|
|
10
|
+
type MiniflareWorkerInput,
|
|
11
|
+
} from "./build-worker-options.ts";
|
|
12
|
+
import { MiniflareWorkerProxy } from "./miniflare-worker-proxy.ts";
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
var ALCHEMY_MINIFLARE_CONTROLLER: MiniflareController | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MiniflareController {
|
|
19
|
+
abort = new AbortController();
|
|
20
|
+
miniflare: miniflare.Miniflare | undefined;
|
|
21
|
+
options = new Map<string, miniflare.WorkerOptions>();
|
|
22
|
+
localProxies = new Map<string, MiniflareWorkerProxy>();
|
|
23
|
+
remoteProxies = new Map<string, HTTPServer>();
|
|
24
|
+
mutex = new AsyncMutex();
|
|
25
|
+
|
|
26
|
+
static get singleton() {
|
|
27
|
+
globalThis.ALCHEMY_MINIFLARE_CONTROLLER ??= new MiniflareController();
|
|
28
|
+
return globalThis.ALCHEMY_MINIFLARE_CONTROLLER;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async add(input: MiniflareWorkerInput) {
|
|
32
|
+
const { watch, remoteProxy } = await buildWorkerOptions(input);
|
|
33
|
+
if (remoteProxy) {
|
|
34
|
+
this.remoteProxies.set(input.name, remoteProxy);
|
|
35
|
+
}
|
|
36
|
+
const watcher = watch(this.abort.signal);
|
|
37
|
+
const first = await watcher.next();
|
|
38
|
+
assert(first.value, "First value is undefined");
|
|
39
|
+
this.options.set(input.name, first.value);
|
|
40
|
+
const miniflare = await this.update();
|
|
41
|
+
const proxy = new MiniflareWorkerProxy({
|
|
42
|
+
name: input.name,
|
|
43
|
+
port: input.port ?? (await findOpenPort()),
|
|
44
|
+
miniflare,
|
|
45
|
+
});
|
|
46
|
+
this.localProxies.set(input.name, proxy);
|
|
47
|
+
void this.watch(input.id, watcher);
|
|
48
|
+
logger.task(input.id, {
|
|
49
|
+
message: `Ready at ${proxy.url}`,
|
|
50
|
+
status: "success",
|
|
51
|
+
resource: input.id,
|
|
52
|
+
prefix: "dev",
|
|
53
|
+
prefixColor: "cyanBright",
|
|
54
|
+
});
|
|
55
|
+
return proxy.url;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async watch(
|
|
59
|
+
id: string,
|
|
60
|
+
watcher: AsyncGenerator<miniflare.WorkerOptions>,
|
|
61
|
+
) {
|
|
62
|
+
for await (const options of watcher) {
|
|
63
|
+
this.options.set(options.name!, options);
|
|
64
|
+
await this.update();
|
|
65
|
+
logger.task(id, {
|
|
66
|
+
message: "Updated",
|
|
67
|
+
status: "success",
|
|
68
|
+
resource: id,
|
|
69
|
+
prefix: "dev",
|
|
70
|
+
prefixColor: "cyanBright",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async update() {
|
|
76
|
+
return await this.mutex.lock(async () => {
|
|
77
|
+
const options: miniflare.MiniflareOptions = {
|
|
78
|
+
workers: Array.from(this.options.values()),
|
|
79
|
+
defaultPersistRoot: path.join(process.cwd(), ".alchemy/miniflare"),
|
|
80
|
+
unsafeDevRegistryPath: miniflare.getDefaultDevRegistryPath(),
|
|
81
|
+
analyticsEngineDatasetsPersist: true,
|
|
82
|
+
cachePersist: true,
|
|
83
|
+
d1Persist: true,
|
|
84
|
+
durableObjectsPersist: true,
|
|
85
|
+
kvPersist: true,
|
|
86
|
+
r2Persist: true,
|
|
87
|
+
secretsStorePersist: true,
|
|
88
|
+
workflowsPersist: true,
|
|
89
|
+
log: process.env.DEBUG
|
|
90
|
+
? new miniflare.Log(miniflare.LogLevel.DEBUG)
|
|
91
|
+
: undefined,
|
|
92
|
+
};
|
|
93
|
+
return await this.setMiniflareOptions(options);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async setMiniflareOptions(options: miniflare.MiniflareOptions) {
|
|
98
|
+
try {
|
|
99
|
+
if (this.miniflare) {
|
|
100
|
+
await this.miniflare.setOptions(options);
|
|
101
|
+
} else {
|
|
102
|
+
this.miniflare = new miniflare.Miniflare(options);
|
|
103
|
+
await this.miniflare.ready;
|
|
104
|
+
}
|
|
105
|
+
return this.miniflare;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (
|
|
108
|
+
error instanceof miniflare.MiniflareCoreError &&
|
|
109
|
+
error.code === "ERR_MODULE_STRING_SCRIPT"
|
|
110
|
+
) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'Miniflare detected an external dependency that could not be resolved. This typically occurs when the "nodejs_compat" or "nodejs_als" compatibility flag is not enabled.',
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async dispose() {
|
|
121
|
+
this.abort.abort();
|
|
122
|
+
await Promise.all([
|
|
123
|
+
this.miniflare?.dispose(),
|
|
124
|
+
...this.localProxies.values().map((proxy) => proxy.close()),
|
|
125
|
+
...this.remoteProxies.values().map((proxy) => proxy.close()),
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -55,18 +55,10 @@ export class MiniflareWorkerProxy {
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
this.wss.handleUpgrade(req, socket, head, (client) => {
|
|
58
|
-
client.on("message", (event, binary) => {
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
server.close(code, reason);
|
|
63
|
-
});
|
|
64
|
-
server.on("message", (event, binary) => {
|
|
65
|
-
client.send(event, { binary });
|
|
66
|
-
});
|
|
67
|
-
server.on("close", (code, reason) => {
|
|
68
|
-
client.close(code, reason);
|
|
69
|
-
});
|
|
58
|
+
client.on("message", (event, binary) => server.send(event, { binary }));
|
|
59
|
+
client.on("close", (code, reason) => server.close(code, reason));
|
|
60
|
+
server.on("message", (event, binary) => client.send(event, { binary }));
|
|
61
|
+
server.on("close", (code, reason) => client.close(code, reason));
|
|
70
62
|
this.wss.emit("connection", client, req);
|
|
71
63
|
});
|
|
72
64
|
}
|
|
@@ -157,9 +149,8 @@ const writeMiniflareResponseToNode = (
|
|
|
157
149
|
out: http.ServerResponse,
|
|
158
150
|
) => {
|
|
159
151
|
out.statusCode = res.status;
|
|
160
|
-
res.headers.forEach((value, key) =>
|
|
161
|
-
|
|
162
|
-
});
|
|
152
|
+
res.headers.forEach((value, key) => out.setHeader(key, value));
|
|
153
|
+
|
|
163
154
|
if (res.body) {
|
|
164
155
|
Readable.fromWeb(res.body).pipe(out, { end: true });
|
|
165
156
|
} else {
|
|
@@ -32,7 +32,7 @@ export interface RemoteBindingProxy {
|
|
|
32
32
|
export async function createRemoteProxyWorker(input: {
|
|
33
33
|
name: string;
|
|
34
34
|
bindings: WorkerBindingSpec[];
|
|
35
|
-
}) {
|
|
35
|
+
}): Promise<RemoteBindingProxy> {
|
|
36
36
|
const api = await createCloudflareApi();
|
|
37
37
|
const script = await getInternalWorkerBundle("remote-binding-proxy");
|
|
38
38
|
const [token, subdomain] = await Promise.all([
|
|
@@ -42,9 +42,7 @@ export async function createRemoteProxyWorker(input: {
|
|
|
42
42
|
main_module: script.bundle.entrypoint,
|
|
43
43
|
compatibility_date: "2025-06-16",
|
|
44
44
|
bindings: input.bindings,
|
|
45
|
-
observability: {
|
|
46
|
-
enabled: false,
|
|
47
|
-
},
|
|
45
|
+
observability: { enabled: false },
|
|
48
46
|
},
|
|
49
47
|
bundle: script.bundle,
|
|
50
48
|
session: {
|
|
@@ -55,27 +53,24 @@ export async function createRemoteProxyWorker(input: {
|
|
|
55
53
|
import("../worker-subdomain.ts").then((m) => m.getAccountSubdomain(api)),
|
|
56
54
|
]);
|
|
57
55
|
|
|
58
|
-
const proxyURL =
|
|
56
|
+
const proxyURL = `https://${input.name}.${subdomain}.workers.dev`;
|
|
59
57
|
const server = new HTTPServer({
|
|
60
58
|
fetch: async (req) => {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
url.search = origin.search;
|
|
64
|
-
url.hash = origin.hash;
|
|
59
|
+
const url = new URL(req.url);
|
|
60
|
+
const targetUrl = new URL(url.pathname + url.search, proxyURL);
|
|
65
61
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
const headers = new Headers(req.headers);
|
|
63
|
+
headers.set("cf-workers-preview-token", token);
|
|
64
|
+
headers.set("host", new URL(proxyURL).hostname);
|
|
65
|
+
headers.delete("cf-connecting-ip");
|
|
70
66
|
|
|
71
|
-
const res = await fetch(
|
|
67
|
+
const res = await fetch(targetUrl, {
|
|
72
68
|
method: req.method,
|
|
73
|
-
headers
|
|
69
|
+
headers,
|
|
74
70
|
body: req.body,
|
|
75
71
|
redirect: "manual",
|
|
76
72
|
});
|
|
77
73
|
|
|
78
|
-
// Remove headers that are not supported by miniflare
|
|
79
74
|
const responseHeaders = new Headers(res.headers);
|
|
80
75
|
responseHeaders.delete("transfer-encoding");
|
|
81
76
|
responseHeaders.delete("content-encoding");
|
|
@@ -86,6 +81,7 @@ export async function createRemoteProxyWorker(input: {
|
|
|
86
81
|
});
|
|
87
82
|
},
|
|
88
83
|
});
|
|
84
|
+
|
|
89
85
|
await server.listen();
|
|
90
86
|
return {
|
|
91
87
|
server,
|
|
@@ -126,13 +122,12 @@ async function createWorkersPreviewToken(
|
|
|
126
122
|
}
|
|
127
123
|
|
|
128
124
|
async function prewarm(url: string, previewToken: string) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"cf-workers-preview-token": previewToken,
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
console.error(`Failed to prewarm worker: ${res.status} ${res.statusText}`);
|
|
125
|
+
try {
|
|
126
|
+
await fetch(url, {
|
|
127
|
+
headers: { "cf-workers-preview-token": previewToken },
|
|
128
|
+
});
|
|
129
|
+
} catch {
|
|
130
|
+
// Ignore prewarm errors
|
|
136
131
|
}
|
|
137
132
|
}
|
|
138
133
|
|
|
@@ -206,11 +206,11 @@ export default {
|
|
|
206
206
|
} as WorkerProps<any> & { name: string };
|
|
207
207
|
|
|
208
208
|
if (wrangler) {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
209
|
+
const wranglerAbsPath = path.resolve(cwd, wranglerJsonPath);
|
|
210
|
+
const wranglerAbsDir = path.dirname(wranglerAbsPath);
|
|
211
211
|
|
|
212
212
|
await WranglerJson("wrangler.jsonc", {
|
|
213
|
-
path:
|
|
213
|
+
path: path.relative(cwd, wranglerJsonPath),
|
|
214
214
|
worker: workerProps,
|
|
215
215
|
// @ts-expect-error - props.wrangler can be string | object, this is fine
|
|
216
216
|
main: props.wrangler?.main ?? props.main,
|
|
@@ -218,7 +218,7 @@ export default {
|
|
|
218
218
|
assets: {
|
|
219
219
|
binding: "ASSETS",
|
|
220
220
|
// path must be relative to the wrangler.jsonc file
|
|
221
|
-
directory: path.relative(
|
|
221
|
+
directory: path.relative(wranglerAbsDir, assetsDirPath),
|
|
222
222
|
},
|
|
223
223
|
transform: props.transform,
|
|
224
224
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import esbuild from "esbuild";
|
|
2
2
|
import { err, ok, type Result } from "neverthrow";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
-
import path from "
|
|
4
|
+
import path from "pathe";
|
|
5
|
+
import { logger } from "../util/logger.ts";
|
|
5
6
|
import { external, external_als } from "./bundle/externals.ts";
|
|
6
7
|
import { esbuildPluginAlias } from "./bundle/plugin-alias.ts";
|
|
7
8
|
import { esbuildPluginCompatWarning } from "./bundle/plugin-compat-warning.ts";
|
|
@@ -17,6 +18,7 @@ export interface WorkerBundle {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function normalizeWorkerBundle(props: {
|
|
21
|
+
id: string;
|
|
20
22
|
script: string | undefined;
|
|
21
23
|
entrypoint: string | undefined;
|
|
22
24
|
noBundle: boolean | undefined;
|
|
@@ -66,6 +68,7 @@ export function normalizeWorkerBundle(props: {
|
|
|
66
68
|
sourcemaps: props.sourceMap !== false,
|
|
67
69
|
})
|
|
68
70
|
: new WorkerBundleSource.ESBuild({
|
|
71
|
+
id: props.id,
|
|
69
72
|
entrypoint: props.entrypoint,
|
|
70
73
|
format: props.format ?? "esm",
|
|
71
74
|
nodeCompat,
|
|
@@ -238,6 +241,7 @@ export namespace WorkerBundleSource {
|
|
|
238
241
|
esbuild.BuildOptions,
|
|
239
242
|
"entryPoints" | "format" | "absWorkingDir" | "outdir"
|
|
240
243
|
> {
|
|
244
|
+
id: string;
|
|
241
245
|
entrypoint: string;
|
|
242
246
|
cwd: string;
|
|
243
247
|
outdir: string;
|
|
@@ -274,7 +278,20 @@ export namespace WorkerBundleSource {
|
|
|
274
278
|
|
|
275
279
|
async *watch(signal: AbortSignal): AsyncIterable<WorkerBundle> {
|
|
276
280
|
const wasm = createWasmPlugin();
|
|
277
|
-
|
|
281
|
+
let count = 0;
|
|
282
|
+
const hotReload = createHotReloadPlugin({
|
|
283
|
+
onBuildStart: () => {
|
|
284
|
+
if (count > 0) {
|
|
285
|
+
logger.task(this.props.id, {
|
|
286
|
+
message: "Rebuilding",
|
|
287
|
+
status: "pending",
|
|
288
|
+
resource: this.props.id,
|
|
289
|
+
prefix: "dev",
|
|
290
|
+
prefixColor: "cyanBright",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
});
|
|
278
295
|
const options = this.buildOptions([wasm.plugin, hotReload.plugin]);
|
|
279
296
|
|
|
280
297
|
const context = await esbuild.context(options);
|
|
@@ -282,6 +299,7 @@ export namespace WorkerBundleSource {
|
|
|
282
299
|
await context.watch();
|
|
283
300
|
|
|
284
301
|
for await (const result of hotReload.iterator) {
|
|
302
|
+
count++;
|
|
285
303
|
const { entrypoint, root, modules } = this.resolveBuildOutput(
|
|
286
304
|
result.metafile!,
|
|
287
305
|
);
|
|
@@ -301,7 +319,14 @@ export namespace WorkerBundleSource {
|
|
|
301
319
|
}
|
|
302
320
|
|
|
303
321
|
private buildOptions(additionalPlugins: esbuild.Plugin[]) {
|
|
304
|
-
const {
|
|
322
|
+
const {
|
|
323
|
+
id: _,
|
|
324
|
+
entrypoint,
|
|
325
|
+
nodeCompat,
|
|
326
|
+
cwd,
|
|
327
|
+
format,
|
|
328
|
+
...props
|
|
329
|
+
} = this.props;
|
|
305
330
|
return {
|
|
306
331
|
entryPoints: [entrypoint],
|
|
307
332
|
absWorkingDir: cwd,
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import kleur from "kleur";
|
|
2
1
|
import { spawn } from "node:child_process";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -9,6 +8,7 @@ import { Resource, ResourceKind } from "../resource.ts";
|
|
|
9
8
|
import { isSecret } from "../secret.ts";
|
|
10
9
|
import type { type } from "../type.ts";
|
|
11
10
|
import { DeferredPromise } from "../util/deferred-promise.ts";
|
|
11
|
+
import { exists } from "../util/exists.ts";
|
|
12
12
|
import { logger } from "../util/logger.ts";
|
|
13
13
|
import { withExponentialBackoff } from "../util/retry.ts";
|
|
14
14
|
import { CloudflareApiError, handleApiError } from "./api-error.ts";
|
|
@@ -38,8 +38,6 @@ import {
|
|
|
38
38
|
isDurableObjectNamespace,
|
|
39
39
|
} from "./durable-object-namespace.ts";
|
|
40
40
|
import { type EventSource, isQueueEventSource } from "./event-source.ts";
|
|
41
|
-
import type { MiniflareWorkerOptions } from "./miniflare/miniflare-worker-options.ts";
|
|
42
|
-
import { miniflareServer } from "./miniflare/miniflare.ts";
|
|
43
41
|
import {
|
|
44
42
|
QueueConsumer,
|
|
45
43
|
deleteQueueConsumer,
|
|
@@ -48,11 +46,7 @@ import {
|
|
|
48
46
|
import { isQueue } from "./queue.ts";
|
|
49
47
|
import { Route } from "./route.ts";
|
|
50
48
|
import { uploadAssets } from "./worker-assets.ts";
|
|
51
|
-
import {
|
|
52
|
-
WorkerBundle,
|
|
53
|
-
type WorkerBundleSource,
|
|
54
|
-
normalizeWorkerBundle,
|
|
55
|
-
} from "./worker-bundle.ts";
|
|
49
|
+
import { WorkerBundle, normalizeWorkerBundle } from "./worker-bundle.ts";
|
|
56
50
|
import {
|
|
57
51
|
type WorkerScriptMetadata,
|
|
58
52
|
bumpMigrationTagVersion,
|
|
@@ -65,7 +59,6 @@ import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts";
|
|
|
65
59
|
// Previous versions of `Worker` used the `Bundle` resource.
|
|
66
60
|
// This import is here to avoid errors when destroying the `Bundle` resource.
|
|
67
61
|
import "../esbuild/bundle.ts";
|
|
68
|
-
import { exists } from "../util/exists.ts";
|
|
69
62
|
|
|
70
63
|
/**
|
|
71
64
|
* Configuration options for static assets
|
|
@@ -736,6 +729,7 @@ const _Worker = Resource(
|
|
|
736
729
|
: props.namespace?.namespaceName;
|
|
737
730
|
|
|
738
731
|
const bundleSourceResult = normalizeWorkerBundle({
|
|
732
|
+
id,
|
|
739
733
|
entrypoint: props.entrypoint,
|
|
740
734
|
script: props.script,
|
|
741
735
|
format: props.format,
|
|
@@ -762,7 +756,7 @@ const _Worker = Resource(
|
|
|
762
756
|
}
|
|
763
757
|
|
|
764
758
|
if (props.dev?.command) {
|
|
765
|
-
const
|
|
759
|
+
const result = await createDevCommand({
|
|
766
760
|
id,
|
|
767
761
|
command: props.dev.command,
|
|
768
762
|
cwd: props.dev.cwd || props.cwd || process.cwd(),
|
|
@@ -780,18 +774,25 @@ const _Worker = Resource(
|
|
|
780
774
|
),
|
|
781
775
|
},
|
|
782
776
|
});
|
|
783
|
-
url =
|
|
777
|
+
url = result.url;
|
|
778
|
+
this.onCleanup(() => result.cleanup());
|
|
784
779
|
} else {
|
|
785
|
-
|
|
780
|
+
const { MiniflareController } = await import(
|
|
781
|
+
"./miniflare/miniflare-controller.ts"
|
|
782
|
+
);
|
|
783
|
+
const controller = MiniflareController.singleton;
|
|
784
|
+
url = await controller.add({
|
|
786
785
|
id,
|
|
787
|
-
workerName,
|
|
786
|
+
name: workerName,
|
|
788
787
|
compatibilityDate,
|
|
789
788
|
compatibilityFlags,
|
|
790
789
|
bindings: props.bindings,
|
|
790
|
+
eventSources: props.eventSources,
|
|
791
|
+
assets: props.assets,
|
|
791
792
|
bundle: bundleSourceResult.value,
|
|
792
793
|
port: props.dev?.port,
|
|
793
|
-
assets: props.assets,
|
|
794
794
|
});
|
|
795
|
+
this.onCleanup(() => controller.dispose());
|
|
795
796
|
}
|
|
796
797
|
|
|
797
798
|
return this({
|
|
@@ -926,7 +927,6 @@ const _Worker = Resource(
|
|
|
926
927
|
let putWorkerResult: PutWorkerResult;
|
|
927
928
|
if (watch) {
|
|
928
929
|
const controller = new AbortController();
|
|
929
|
-
cleanups.push(() => controller.abort());
|
|
930
930
|
const promise = new DeferredPromise<PutWorkerResult>();
|
|
931
931
|
const runWatch = async () => {
|
|
932
932
|
for await (const bundle of bundleSource.watch(controller.signal)) {
|
|
@@ -973,14 +973,12 @@ const _Worker = Resource(
|
|
|
973
973
|
}
|
|
974
974
|
};
|
|
975
975
|
void runWatch(); // this is not awaited because it's an ongoing process
|
|
976
|
+
this.onCleanup(() => controller.abort());
|
|
976
977
|
putWorkerResult = await promise.value;
|
|
977
|
-
await createTail(api, id, workerName)
|
|
978
|
-
.
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
.catch((error) => {
|
|
982
|
-
logger.error(`Failed to create tail for ${workerName}`, error);
|
|
983
|
-
});
|
|
978
|
+
const tail = await createTail(api, id, workerName).catch((error) => {
|
|
979
|
+
logger.error(`Failed to create tail for ${workerName}`, error);
|
|
980
|
+
});
|
|
981
|
+
this.onCleanup(() => tail?.close());
|
|
984
982
|
} else {
|
|
985
983
|
const scriptBundle = await bundleSource.create();
|
|
986
984
|
putWorkerResult = await putWorkerWithAssets(props, scriptBundle);
|
|
@@ -1102,22 +1100,6 @@ const _Worker = Resource(
|
|
|
1102
1100
|
},
|
|
1103
1101
|
);
|
|
1104
1102
|
|
|
1105
|
-
const cleanups: (() => any)[] = [];
|
|
1106
|
-
let exiting = false;
|
|
1107
|
-
|
|
1108
|
-
process.on("SIGINT", async () => {
|
|
1109
|
-
if (cleanups.length > 0) {
|
|
1110
|
-
if (!exiting) {
|
|
1111
|
-
exiting = true;
|
|
1112
|
-
logger.log(`\n${kleur.gray("Exiting...")}`);
|
|
1113
|
-
}
|
|
1114
|
-
// TODO: for some reason this runs twice...
|
|
1115
|
-
// and this whole thing feels hacky anyway
|
|
1116
|
-
await Promise.allSettled(cleanups.map((cleanup) => cleanup()));
|
|
1117
|
-
}
|
|
1118
|
-
process.exit(0);
|
|
1119
|
-
});
|
|
1120
|
-
|
|
1121
1103
|
const assertUnique = <T, Key extends keyof T>(
|
|
1122
1104
|
inputs: T[],
|
|
1123
1105
|
key: Key,
|
|
@@ -1337,56 +1319,12 @@ async function provisionSubdomain(
|
|
|
1337
1319
|
}
|
|
1338
1320
|
}
|
|
1339
1321
|
|
|
1340
|
-
async function createMiniflare(props: {
|
|
1341
|
-
id: string;
|
|
1342
|
-
workerName: string;
|
|
1343
|
-
compatibilityDate: string;
|
|
1344
|
-
compatibilityFlags: string[];
|
|
1345
|
-
bindings: Bindings | undefined;
|
|
1346
|
-
port: number | undefined;
|
|
1347
|
-
bundle: WorkerBundleSource;
|
|
1348
|
-
assets: AssetsConfig | undefined;
|
|
1349
|
-
}) {
|
|
1350
|
-
const sharedOptions: Omit<MiniflareWorkerOptions, "bundle"> = {
|
|
1351
|
-
name: props.workerName,
|
|
1352
|
-
compatibilityDate: props.compatibilityDate,
|
|
1353
|
-
compatibilityFlags: props.compatibilityFlags,
|
|
1354
|
-
bindings: props.bindings,
|
|
1355
|
-
port: props.port,
|
|
1356
|
-
assets: props.assets,
|
|
1357
|
-
};
|
|
1358
|
-
|
|
1359
|
-
const startPromise = new DeferredPromise<string>();
|
|
1360
|
-
const controller = new AbortController();
|
|
1361
|
-
cleanups.push(() => controller.abort());
|
|
1362
|
-
const run = async () => {
|
|
1363
|
-
for await (const bundle of props.bundle.watch(controller.signal)) {
|
|
1364
|
-
const server = await miniflareServer.push({
|
|
1365
|
-
...sharedOptions,
|
|
1366
|
-
bundle,
|
|
1367
|
-
});
|
|
1368
|
-
if (startPromise.status === "pending") {
|
|
1369
|
-
startPromise.resolve(server.url);
|
|
1370
|
-
}
|
|
1371
|
-
logger.task("", {
|
|
1372
|
-
message: `ready at ${server.url}`,
|
|
1373
|
-
status: "success",
|
|
1374
|
-
resource: props.id,
|
|
1375
|
-
prefix: "miniflare",
|
|
1376
|
-
prefixColor: "greenBright",
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
};
|
|
1380
|
-
void run();
|
|
1381
|
-
return await startPromise.value;
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
1322
|
async function createDevCommand(props: {
|
|
1385
1323
|
id: string;
|
|
1386
1324
|
command: string;
|
|
1387
1325
|
cwd: string;
|
|
1388
1326
|
env: Record<string, string | undefined>;
|
|
1389
|
-
})
|
|
1327
|
+
}) {
|
|
1390
1328
|
const persistFile = path.join(process.cwd(), ".alchemy", `${props.id}.pid`);
|
|
1391
1329
|
if (await exists(persistFile)) {
|
|
1392
1330
|
const pid = Number.parseInt(await fs.readFile(persistFile, "utf8"));
|
|
@@ -1404,7 +1342,9 @@ async function createDevCommand(props: {
|
|
|
1404
1342
|
}
|
|
1405
1343
|
const command = props.command.split(" ");
|
|
1406
1344
|
const [cmd, ...args] = command;
|
|
1407
|
-
|
|
1345
|
+
|
|
1346
|
+
const promise = new DeferredPromise<string>();
|
|
1347
|
+
const childProcess = spawn(cmd, args, {
|
|
1408
1348
|
cwd: props.cwd,
|
|
1409
1349
|
shell: true,
|
|
1410
1350
|
env: {
|
|
@@ -1420,56 +1360,81 @@ async function createDevCommand(props: {
|
|
|
1420
1360
|
},
|
|
1421
1361
|
stdio: ["inherit", "pipe", "pipe"],
|
|
1422
1362
|
});
|
|
1423
|
-
const { url } = await new Promise<{ url: string }>((resolve, reject) => {
|
|
1424
|
-
let urlFound = false;
|
|
1425
|
-
let stdout = "";
|
|
1426
|
-
let stderr = "";
|
|
1427
|
-
const urlRegex =
|
|
1428
|
-
/http:\/\/(?:(?:localhost|0\.0\.0\.0|127\.0\.0\.1)|(?:\d{1,3}\.){3}\d{1,3}):\d+(?:\/)?/;
|
|
1429
|
-
|
|
1430
|
-
const parseOutput = (data: string) => {
|
|
1431
|
-
if (!urlFound) {
|
|
1432
|
-
const match = data.match(urlRegex);
|
|
1433
|
-
if (match) {
|
|
1434
|
-
urlFound = true;
|
|
1435
|
-
resolve({ url: match[0] });
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
};
|
|
1439
1363
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
}
|
|
1364
|
+
// Clean up the pid file when the process exits
|
|
1365
|
+
childProcess.once("exit", async () => {
|
|
1366
|
+
try {
|
|
1367
|
+
await fs.unlink(persistFile);
|
|
1368
|
+
} catch {
|
|
1369
|
+
// ignore
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1447
1372
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1373
|
+
let urlFound = false;
|
|
1374
|
+
let stdout = "";
|
|
1375
|
+
let stderr = "";
|
|
1376
|
+
const urlRegex =
|
|
1377
|
+
/http:\/\/(?:(?:localhost|0\.0\.0\.0|127\.0\.0\.1)|(?:\d{1,3}\.){3}\d{1,3}):\d+(?:\/)?/;
|
|
1378
|
+
|
|
1379
|
+
const parseOutput = (data: string) => {
|
|
1380
|
+
if (!urlFound) {
|
|
1381
|
+
const match = data.match(urlRegex);
|
|
1382
|
+
if (match) {
|
|
1383
|
+
urlFound = true;
|
|
1384
|
+
promise.resolve(match[0]);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1454
1388
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1389
|
+
// Handle stdout - parse for URL and write through with colors preserved
|
|
1390
|
+
childProcess.stdout?.on("data", (data) => {
|
|
1391
|
+
parseOutput(
|
|
1392
|
+
(stdout += data.toString().replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")),
|
|
1393
|
+
);
|
|
1394
|
+
process.stdout.write(data);
|
|
1395
|
+
});
|
|
1458
1396
|
|
|
1459
|
-
|
|
1397
|
+
childProcess.stderr?.on("data", (data) => {
|
|
1398
|
+
parseOutput(
|
|
1399
|
+
(stderr += data.toString().replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")),
|
|
1400
|
+
);
|
|
1401
|
+
process.stderr.write(data);
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
childProcess.on("error", (error) => {
|
|
1405
|
+
promise.reject(error);
|
|
1406
|
+
});
|
|
1407
|
+
if (childProcess.pid) {
|
|
1408
|
+
await fs.mkdir(path.dirname(persistFile), { recursive: true });
|
|
1409
|
+
await fs.writeFile(persistFile, childProcess.pid.toString());
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
url: await promise.value,
|
|
1413
|
+
cleanup: async () => {
|
|
1460
1414
|
try {
|
|
1461
1415
|
await fs.unlink(persistFile);
|
|
1462
1416
|
} catch {
|
|
1463
1417
|
// ignore
|
|
1464
1418
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1419
|
+
if (!childProcess.killed) {
|
|
1420
|
+
childProcess.kill("SIGTERM");
|
|
1421
|
+
// Give it time to exit gracefully
|
|
1422
|
+
await new Promise<void>((resolve) => {
|
|
1423
|
+
const timeout = setTimeout(() => {
|
|
1424
|
+
if (!childProcess.killed) {
|
|
1425
|
+
childProcess.kill("SIGKILL");
|
|
1426
|
+
}
|
|
1427
|
+
resolve();
|
|
1428
|
+
}, 5000);
|
|
1429
|
+
|
|
1430
|
+
childProcess.once("exit", () => {
|
|
1431
|
+
clearTimeout(timeout);
|
|
1432
|
+
resolve();
|
|
1433
|
+
});
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
};
|
|
1473
1438
|
}
|
|
1474
1439
|
|
|
1475
1440
|
type PutWorkerOptions = Omit<WorkerProps, "entrypoint"> & {
|