alchemy 0.55.2 → 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 +13920 -6724
- 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-alias.d.ts +3 -0
- package/lib/cloudflare/bundle/plugin-alias.d.ts.map +1 -0
- package/lib/cloudflare/bundle/plugin-alias.js +40 -0
- package/lib/cloudflare/bundle/plugin-alias.js.map +1 -0
- 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 +21 -4
- 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/lib/util/telemetry/client.d.ts +3 -4
- package/lib/util/telemetry/client.d.ts.map +1 -1
- package/lib/util/telemetry/client.js +19 -25
- package/lib/util/telemetry/client.js.map +1 -1
- package/lib/util/telemetry/types.d.ts +1 -1
- package/lib/util/telemetry/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/alchemy.ts +7 -0
- package/src/cloudflare/bundle/plugin-alias.ts +49 -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 +30 -4
- package/src/cloudflare/worker.ts +90 -125
- package/src/context.ts +19 -0
- package/src/scope.ts +27 -0
- package/src/util/telemetry/client.ts +23 -33
- package/src/util/telemetry/types.ts +3 -3
- 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
|
@@ -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,8 +1,10 @@
|
|
|
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";
|
|
7
|
+
import { esbuildPluginAlias } from "./bundle/plugin-alias.ts";
|
|
6
8
|
import { esbuildPluginCompatWarning } from "./bundle/plugin-compat-warning.ts";
|
|
7
9
|
import { createHotReloadPlugin } from "./bundle/plugin-hot-reload.ts";
|
|
8
10
|
import { esbuildPluginHybridNodeCompat } from "./bundle/plugin-hybrid-node-compat.ts";
|
|
@@ -16,6 +18,7 @@ export interface WorkerBundle {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function normalizeWorkerBundle(props: {
|
|
21
|
+
id: string;
|
|
19
22
|
script: string | undefined;
|
|
20
23
|
entrypoint: string | undefined;
|
|
21
24
|
noBundle: boolean | undefined;
|
|
@@ -65,6 +68,7 @@ export function normalizeWorkerBundle(props: {
|
|
|
65
68
|
sourcemaps: props.sourceMap !== false,
|
|
66
69
|
})
|
|
67
70
|
: new WorkerBundleSource.ESBuild({
|
|
71
|
+
id: props.id,
|
|
68
72
|
entrypoint: props.entrypoint,
|
|
69
73
|
format: props.format ?? "esm",
|
|
70
74
|
nodeCompat,
|
|
@@ -237,6 +241,7 @@ export namespace WorkerBundleSource {
|
|
|
237
241
|
esbuild.BuildOptions,
|
|
238
242
|
"entryPoints" | "format" | "absWorkingDir" | "outdir"
|
|
239
243
|
> {
|
|
244
|
+
id: string;
|
|
240
245
|
entrypoint: string;
|
|
241
246
|
cwd: string;
|
|
242
247
|
outdir: string;
|
|
@@ -273,7 +278,20 @@ export namespace WorkerBundleSource {
|
|
|
273
278
|
|
|
274
279
|
async *watch(signal: AbortSignal): AsyncIterable<WorkerBundle> {
|
|
275
280
|
const wasm = createWasmPlugin();
|
|
276
|
-
|
|
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
|
+
});
|
|
277
295
|
const options = this.buildOptions([wasm.plugin, hotReload.plugin]);
|
|
278
296
|
|
|
279
297
|
const context = await esbuild.context(options);
|
|
@@ -281,6 +299,7 @@ export namespace WorkerBundleSource {
|
|
|
281
299
|
await context.watch();
|
|
282
300
|
|
|
283
301
|
for await (const result of hotReload.iterator) {
|
|
302
|
+
count++;
|
|
284
303
|
const { entrypoint, root, modules } = this.resolveBuildOutput(
|
|
285
304
|
result.metafile!,
|
|
286
305
|
);
|
|
@@ -300,7 +319,14 @@ export namespace WorkerBundleSource {
|
|
|
300
319
|
}
|
|
301
320
|
|
|
302
321
|
private buildOptions(additionalPlugins: esbuild.Plugin[]) {
|
|
303
|
-
const {
|
|
322
|
+
const {
|
|
323
|
+
id: _,
|
|
324
|
+
entrypoint,
|
|
325
|
+
nodeCompat,
|
|
326
|
+
cwd,
|
|
327
|
+
format,
|
|
328
|
+
...props
|
|
329
|
+
} = this.props;
|
|
304
330
|
return {
|
|
305
331
|
entryPoints: [entrypoint],
|
|
306
332
|
absWorkingDir: cwd,
|
|
@@ -319,13 +345,13 @@ export namespace WorkerBundleSource {
|
|
|
319
345
|
...props.loader,
|
|
320
346
|
},
|
|
321
347
|
plugins: [
|
|
348
|
+
esbuildPluginAlias(props.alias ?? {}, this.props.cwd),
|
|
322
349
|
nodeCompat === "v2"
|
|
323
350
|
? esbuildPluginHybridNodeCompat()
|
|
324
351
|
: esbuildPluginCompatWarning(nodeCompat ?? null),
|
|
325
352
|
...(props.plugins ?? []),
|
|
326
353
|
...additionalPlugins,
|
|
327
354
|
],
|
|
328
|
-
alias: props.alias,
|
|
329
355
|
external: [
|
|
330
356
|
...(nodeCompat === "als" ? external_als : external),
|
|
331
357
|
...(props.external ?? []),
|
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"> & {
|
package/src/context.ts
CHANGED
|
@@ -67,6 +67,17 @@ export interface BaseContext<Out extends Resource> {
|
|
|
67
67
|
* "return undefined" so that `await MyResource()` always returns a value.
|
|
68
68
|
*/
|
|
69
69
|
destroy(): never;
|
|
70
|
+
/**
|
|
71
|
+
* Register a cleanup function that will be called when the process exits.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* const proc = spawn('my-command', ['arg1', 'arg2']);
|
|
75
|
+
* this.onCleanup(async () => {
|
|
76
|
+
* proc.kill();
|
|
77
|
+
* await waitForExit(proc);
|
|
78
|
+
* });
|
|
79
|
+
*/
|
|
80
|
+
onCleanup(fn: () => void | Promise<void>): void;
|
|
70
81
|
/**
|
|
71
82
|
* Create the Resource envelope (with Alchemy + User properties)
|
|
72
83
|
*/
|
|
@@ -153,6 +164,14 @@ export function context<
|
|
|
153
164
|
destroy: () => {
|
|
154
165
|
throw new DestroyedSignal();
|
|
155
166
|
},
|
|
167
|
+
onCleanup: (fn: () => void | Promise<void>) => {
|
|
168
|
+
// make the function idempotent so repeated calls don't cause the process to hang
|
|
169
|
+
let promise: Promise<void> | undefined;
|
|
170
|
+
scope.root.onCleanup(async () => {
|
|
171
|
+
promise ??= Promise.resolve(fn());
|
|
172
|
+
await promise;
|
|
173
|
+
});
|
|
174
|
+
},
|
|
156
175
|
create,
|
|
157
176
|
}) as unknown as Context<Out>;
|
|
158
177
|
}
|
package/src/scope.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import util from "node:util";
|
|
3
4
|
import type { Phase } from "./alchemy.ts";
|
|
@@ -99,6 +100,8 @@ export class Scope {
|
|
|
99
100
|
new AsyncLocalStorage<Scope>());
|
|
100
101
|
public static globals: Scope[] = (globalThis.__ALCHEMY_GLOBALS__ ??= []);
|
|
101
102
|
|
|
103
|
+
private static exitHandlerInstalled = false;
|
|
104
|
+
|
|
102
105
|
public static getScope(): Scope | undefined {
|
|
103
106
|
const scope = Scope.storage.getStore();
|
|
104
107
|
if (!scope) {
|
|
@@ -144,6 +147,7 @@ export class Scope {
|
|
|
144
147
|
private finalized = false;
|
|
145
148
|
private startedAt = performance.now();
|
|
146
149
|
private deferred: (() => Promise<any>)[] = [];
|
|
150
|
+
private cleanups: (() => Promise<void>)[] = [];
|
|
147
151
|
|
|
148
152
|
public get appName(): string {
|
|
149
153
|
if (this.parent) {
|
|
@@ -466,6 +470,7 @@ export class Scope {
|
|
|
466
470
|
});
|
|
467
471
|
|
|
468
472
|
if (!this.parent && process.env.ALCHEMY_TEST_KILL_ON_FINALIZE) {
|
|
473
|
+
await this.cleanup();
|
|
469
474
|
process.exit(0);
|
|
470
475
|
}
|
|
471
476
|
}
|
|
@@ -537,6 +542,28 @@ export class Scope {
|
|
|
537
542
|
return promise;
|
|
538
543
|
}
|
|
539
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Run all cleanup functions registered with `onCleanup`.
|
|
547
|
+
* This should only be called on the root scope.
|
|
548
|
+
*/
|
|
549
|
+
public async cleanup() {
|
|
550
|
+
if (this.parent) return;
|
|
551
|
+
this.logger.log(kleur.gray("Exiting..."));
|
|
552
|
+
await Promise.allSettled(this.cleanups.map((cleanup) => cleanup()));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Register a cleanup function that will be called when the process exits.
|
|
557
|
+
* This should only be called on the root scope.
|
|
558
|
+
*/
|
|
559
|
+
public onCleanup(fn: () => Promise<void>) {
|
|
560
|
+
if (this.parent) {
|
|
561
|
+
this.root.onCleanup(fn);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
this.cleanups.push(fn);
|
|
565
|
+
}
|
|
566
|
+
|
|
540
567
|
/**
|
|
541
568
|
* Returns a string representation of the scope.
|
|
542
569
|
*/
|