alchemy 0.60.2 → 0.61.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.
Files changed (57) hide show
  1. package/bin/alchemy.js +77 -37
  2. package/bin/commands/init.ts +65 -23
  3. package/bin/services/execute-alchemy.ts +33 -23
  4. package/lib/alchemy.d.ts.map +1 -1
  5. package/lib/alchemy.js +1 -0
  6. package/lib/alchemy.js.map +1 -1
  7. package/lib/cloudflare/bucket.d.ts.map +1 -1
  8. package/lib/cloudflare/bucket.js +8 -4
  9. package/lib/cloudflare/bucket.js.map +1 -1
  10. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  11. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  12. package/lib/cloudflare/d1-migrations.d.ts.map +1 -1
  13. package/lib/cloudflare/d1-migrations.js +3 -2
  14. package/lib/cloudflare/d1-migrations.js.map +1 -1
  15. package/lib/cloudflare/index.d.ts +2 -1
  16. package/lib/cloudflare/index.d.ts.map +1 -1
  17. package/lib/cloudflare/index.js +2 -1
  18. package/lib/cloudflare/index.js.map +1 -1
  19. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  20. package/lib/cloudflare/miniflare/build-worker-options.js +16 -3
  21. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  22. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  23. package/lib/cloudflare/miniflare/miniflare-controller.js +3 -0
  24. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  25. package/lib/cloudflare/quick-tunnel.d.ts +17 -0
  26. package/lib/cloudflare/quick-tunnel.d.ts.map +1 -0
  27. package/lib/cloudflare/quick-tunnel.js +26 -0
  28. package/lib/cloudflare/quick-tunnel.js.map +1 -0
  29. package/lib/cloudflare/sveltekit/plugin.d.ts +2 -2
  30. package/lib/cloudflare/sveltekit/plugin.d.ts.map +1 -1
  31. package/lib/cloudflare/sveltekit/plugin.js +5 -4
  32. package/lib/cloudflare/sveltekit/plugin.js.map +1 -1
  33. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  34. package/lib/cloudflare/worker-bundle.js +2 -1
  35. package/lib/cloudflare/worker-bundle.js.map +1 -1
  36. package/lib/scope.d.ts +36 -0
  37. package/lib/scope.d.ts.map +1 -1
  38. package/lib/scope.js +20 -0
  39. package/lib/scope.js.map +1 -1
  40. package/lib/util/idempotent-spawn.d.ts +22 -0
  41. package/lib/util/idempotent-spawn.d.ts.map +1 -0
  42. package/lib/util/idempotent-spawn.js +218 -0
  43. package/lib/util/idempotent-spawn.js.map +1 -0
  44. package/package.json +2 -1
  45. package/src/alchemy.ts +1 -0
  46. package/src/cloudflare/bucket.ts +14 -6
  47. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  48. package/src/cloudflare/d1-migrations.ts +4 -5
  49. package/src/cloudflare/index.ts +2 -1
  50. package/src/cloudflare/miniflare/build-worker-options.ts +16 -3
  51. package/src/cloudflare/miniflare/miniflare-controller.ts +4 -0
  52. package/src/cloudflare/quick-tunnel.ts +30 -0
  53. package/src/cloudflare/sveltekit/plugin.ts +5 -5
  54. package/src/cloudflare/worker-bundle.ts +2 -1
  55. package/src/scope.ts +61 -0
  56. package/src/util/idempotent-spawn.ts +286 -0
  57. package/templates/rwsdk/package.json +1 -1
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Idempotently ensure a long-lived child is running with stdout/stderr -> files,
3
+ * and mirror those logs to THIS process's console with persisted offsets.
4
+ *
5
+ * Use: await ensureLoggedChildAndMirror({ cmd: "vite dev" })
6
+ */
7
+ export declare function idempotentSpawn({ cmd, cwd, stateFile, overlapBytes, resume, log, processName, extract, isSameProcess, quiet, }: {
8
+ cmd: string;
9
+ cwd?: string;
10
+ log: string;
11
+ stateFile?: string;
12
+ overlapBytes?: number;
13
+ resume?: boolean;
14
+ isSameProcess?: (pid: number) => Promise<boolean>;
15
+ processName?: string;
16
+ extract?: (line: string) => string | undefined;
17
+ /**
18
+ * If true, the child's stdout and stderr will not be mirrored to this process's console.
19
+ */
20
+ quiet?: boolean;
21
+ }): Promise<string | undefined>;
22
+ //# sourceMappingURL=idempotent-spawn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotent-spawn.d.ts","sourceRoot":"","sources":["../../src/util/idempotent-spawn.ts"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,EACpC,GAAG,EACH,GAAG,EACH,SAAwB,EACxB,YAAgB,EAChB,MAAc,EACd,GAAe,EACf,WAAW,EACX,OAAO,EACP,aAAa,EACb,KAAa,GACd,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/C;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAwP9B"}
@@ -0,0 +1,218 @@
1
+ import find from "find-process";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import fsp from "node:fs/promises";
5
+ import path from "node:path";
6
+ /**
7
+ * Idempotently ensure a long-lived child is running with stdout/stderr -> files,
8
+ * and mirror those logs to THIS process's console with persisted offsets.
9
+ *
10
+ * Use: await ensureLoggedChildAndMirror({ cmd: "vite dev" })
11
+ */
12
+ export async function idempotentSpawn({ cmd, cwd, stateFile = "state.json", overlapBytes = 0, resume = false, log = "log.txt", processName, extract, isSameProcess, quiet = false, }) {
13
+ if (!processName && !isSameProcess) {
14
+ throw new Error("Either processName or isSameProcess must be provided to resume a process");
15
+ }
16
+ const outPath = log;
17
+ await Promise.all([
18
+ fsp.mkdir(path.dirname(stateFile), { recursive: true }),
19
+ fsp.mkdir(path.dirname(log), { recursive: true }),
20
+ ]);
21
+ const { promise: extracted, resolve: resolveExtracted } = Promise.withResolvers();
22
+ if (!extract) {
23
+ console.warn("No extract function provided, will not return a value. This is probably a bug.");
24
+ resolveExtracted(undefined);
25
+ }
26
+ // ------------------------ helpers ------------------------
27
+ function isPidAlive(pid) {
28
+ if (!pid || Number.isNaN(pid))
29
+ return false;
30
+ try {
31
+ process.kill(pid, 0);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ async function writeJsonAtomic(file, data) {
39
+ const dir = path.dirname(file);
40
+ const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
41
+ await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
42
+ await fsp.rename(tmp, file); // atomic on POSIX
43
+ }
44
+ async function readJson(file) {
45
+ try {
46
+ return JSON.parse(await fsp.readFile(file, "utf8"));
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ }
52
+ async function spawnLoggedChild() {
53
+ const out = await fsp.open(outPath, "a");
54
+ // shell:true, NO args — pass a single command string
55
+ const child = spawn(cmd, {
56
+ shell: true,
57
+ cwd,
58
+ stdio: ["ignore", out.fd, out.fd], // stdout/stderr -> files (OS-level)
59
+ env: process.env,
60
+ detached: false,
61
+ });
62
+ // Child now owns dup'd fds; close our handles.
63
+ await out.close();
64
+ // Persist PID into the shared state file
65
+ const stateAll = (await readJson(stateFile)) ?? {};
66
+ stateAll.pid = child.pid;
67
+ await writeJsonAtomic(stateFile, stateAll);
68
+ return child;
69
+ }
70
+ async function ensureChildRunning() {
71
+ const stateAll = await readJson(stateFile);
72
+ if (stateAll) {
73
+ const pid = Number.parseInt(stateAll.pid, 10);
74
+ if (isPidAlive(pid))
75
+ return pid;
76
+ if (await isSameProcess?.(pid))
77
+ return pid;
78
+ if (processName) {
79
+ const processes = await find("pid", pid);
80
+ if (processes.length > 1) {
81
+ console.warn(`Found multiple processes with PID ${pid}, using the first one`);
82
+ }
83
+ if (processes.length > 0) {
84
+ return processes[0].name.startsWith(processName);
85
+ }
86
+ }
87
+ }
88
+ // not running, let's clear pid and state
89
+ await Promise.all([
90
+ fsp.rm(stateFile).catch(() => { }),
91
+ fsp.rm(log).catch(() => { }),
92
+ ]);
93
+ return (await spawnLoggedChild()).pid;
94
+ }
95
+ // Follow a file from persisted offset and mirror to a sink (stdout/stderr)
96
+ async function followFilePersisted(logPath, { write = (buf) => process.stdout.write(buf), chunkSize = 64 * 1024, tickMs = 100, }) {
97
+ logPath = path.resolve(logPath);
98
+ let state = (await readJson(stateFile)) ?? {};
99
+ let fh = await fsp.open(logPath, "r");
100
+ let st = await fh.stat();
101
+ let ino = Number(st.ino ?? 0);
102
+ let dev = Number(st.dev ?? 0);
103
+ // Resume policy: if resume is enabled and same file, start at saved offset (with overlap); else start from end.
104
+ let offset = resume &&
105
+ state.ino === ino &&
106
+ state.dev === dev &&
107
+ typeof state.offset === "number"
108
+ ? Math.max(0, Math.min(st.size, state.offset - overlapBytes))
109
+ : st.size;
110
+ let closed = false;
111
+ async function persist() {
112
+ try {
113
+ const cur = await fh.stat();
114
+ state = {
115
+ ...state,
116
+ offset,
117
+ ino: Number(cur.ino ?? ino),
118
+ dev: Number(cur.dev ?? dev),
119
+ size: cur.size,
120
+ mtimeMs: cur.mtimeMs,
121
+ };
122
+ await writeJsonAtomic(stateFile, state);
123
+ }
124
+ catch {
125
+ // If the file vanished mid-rotation, we'll catch up on the next event.
126
+ }
127
+ }
128
+ async function drain() {
129
+ while (true) {
130
+ const cur = await fh.stat().catch(() => null);
131
+ if (!cur)
132
+ break;
133
+ if (cur.size < offset)
134
+ offset = 0; // truncated
135
+ if (offset > 0 && extract) {
136
+ // read from start to offset
137
+ const fullBuffer = Buffer.allocUnsafe(offset);
138
+ await fh.read({
139
+ position: 0,
140
+ buffer: fullBuffer,
141
+ length: offset,
142
+ });
143
+ const content = fullBuffer.toString("utf8");
144
+ const lines = content.split("\n");
145
+ for (const line of lines) {
146
+ const extracted = extract(line);
147
+ if (extracted) {
148
+ resolveExtracted(extracted);
149
+ break;
150
+ }
151
+ }
152
+ }
153
+ const toRead = Math.min(chunkSize, cur.size - offset);
154
+ if (toRead <= 0)
155
+ break;
156
+ const { bytesRead, buffer } = await fh.read({
157
+ position: offset,
158
+ buffer: Buffer.allocUnsafe(toRead),
159
+ length: toRead,
160
+ });
161
+ if (!bytesRead)
162
+ break;
163
+ offset += bytesRead;
164
+ write(buffer.subarray(0, bytesRead));
165
+ }
166
+ await persist();
167
+ }
168
+ await drain();
169
+ const watcher = fs.watch(logPath, async () => {
170
+ if (closed)
171
+ return;
172
+ try {
173
+ const cur = await fh.stat();
174
+ const curIno = Number(cur.ino ?? 0);
175
+ const curDev = Number(cur.dev ?? 0);
176
+ if (curIno !== ino || curDev !== dev) {
177
+ // Rotated/replaced: reopen and start at beginning of new file
178
+ await fh.close().catch(() => { });
179
+ fh = await fsp.open(logPath, "r");
180
+ const cur2 = await fh.stat();
181
+ ino = Number(cur2.ino ?? 0);
182
+ dev = Number(cur2.dev ?? 0);
183
+ offset = 0;
184
+ }
185
+ }
186
+ catch {
187
+ // File might briefly disappear during rotation
188
+ }
189
+ await drain();
190
+ });
191
+ // TODO(sam): do we need this?
192
+ // const tick = setInterval(() => {
193
+ // drain().catch(() => {});
194
+ // }, tickMs);
195
+ return async function stop() {
196
+ closed = true;
197
+ watcher.close();
198
+ // clearInterval(tick);
199
+ await fh.close().catch(() => { });
200
+ await persist();
201
+ };
202
+ }
203
+ // ------------------------ main flow ------------------------
204
+ await ensureChildRunning();
205
+ if (!quiet) {
206
+ const write = quiet
207
+ ? () => false
208
+ : (buf) => process.stdout.write(buf);
209
+ // Start followers (stdout/stderr) and mirror to this process
210
+ await followFilePersisted(outPath, {
211
+ stateKey: `${path.resolve(outPath)}::stdout`,
212
+ write,
213
+ });
214
+ }
215
+ // Return a stopper for this instance's mirroring (child keeps running)
216
+ return extracted;
217
+ }
218
+ //# sourceMappingURL=idempotent-spawn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotent-spawn.js","sourceRoot":"","sources":["../../src/util/idempotent-spawn.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.60.2",
3
+ "version": "0.61.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Sam Goodwin <sam@alchemy.run>",
6
6
  "homepage": "https://alchemy.run",
@@ -180,6 +180,7 @@
180
180
  "execa": "^9.6.0",
181
181
  "fast-json-patch": "^3.1.1",
182
182
  "fast-xml-parser": "^5.2.5",
183
+ "find-process": "^2.0.0",
183
184
  "glob": "^10.0.0",
184
185
  "jszip": "^3.0.0",
185
186
  "kleur": "^4.1.5",
package/src/alchemy.ts CHANGED
@@ -193,6 +193,7 @@ If this is a mistake, you can disable this check by setting the ALCHEMY_CI_STATE
193
193
  });
194
194
  onExit((code) => {
195
195
  root.cleanup().then(() => {
196
+ code = code === 130 ? 0 : (code ?? 0);
196
197
  process.exit(code);
197
198
  });
198
199
  return true;
@@ -5,8 +5,8 @@ import { Scope } from "../scope.ts";
5
5
  import { withExponentialBackoff } from "../util/retry.ts";
6
6
  import { CloudflareApiError } from "./api-error.ts";
7
7
  import {
8
- extractCloudflareError,
9
8
  extractCloudflareResult,
9
+ type CloudflareApiErrorPayload,
10
10
  } from "./api-response.ts";
11
11
  import {
12
12
  createCloudflareApi,
@@ -518,11 +518,6 @@ export async function* listObjects(
518
518
  `/accounts/${api.accountId}/r2/buckets/${bucketName}/objects?${params.toString()}`,
519
519
  { headers: withJurisdiction(props) },
520
520
  );
521
- if (!response.ok) {
522
- throw new Error(
523
- `Failed to list objects in bucket "${bucketName}": ${await extractCloudflareError(response)}`,
524
- );
525
- }
526
521
  const json: {
527
522
  result: { key: string }[];
528
523
  result_info?: {
@@ -530,7 +525,20 @@ export async function* listObjects(
530
525
  is_truncated: boolean;
531
526
  per_page: number;
532
527
  };
528
+ success: boolean;
529
+ errors: CloudflareApiErrorPayload[];
533
530
  } = await response.json();
531
+ if (!json.success) {
532
+ // 10006 indicates that the bucket does not exist, so there are no objects to list
533
+ if (json.errors.some((e) => e.code === 10006)) {
534
+ return;
535
+ }
536
+ throw new CloudflareApiError(
537
+ `Failed to list objects in bucket "${bucketName}": ${json.errors.map((e) => `- [${e.code}] ${e.message}${e.documentation_url ? ` (${e.documentation_url})` : ""}`).join("\n")}`,
538
+ response,
539
+ json.errors,
540
+ );
541
+ }
534
542
  for (const object of json.result) {
535
543
  yield object.key;
536
544
  }
@@ -4,4 +4,4 @@
4
4
  /**
5
5
  * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
6
6
  */
7
- export const DEFAULT_COMPATIBILITY_DATE = "2025-08-10";
7
+ export const DEFAULT_COMPATIBILITY_DATE = "2025-08-13";
@@ -1,3 +1,4 @@
1
+ import { glob } from "glob";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import { logger } from "../util/logger.ts";
@@ -159,11 +160,9 @@ async function migrateLegacySchema(
159
160
  export async function listMigrationsFiles(
160
161
  migrationsDir: string,
161
162
  ): Promise<Array<{ id: string; sql: string }>> {
162
- const entries = await Array.fromAsync(
163
- fs.glob("**/*.sql", {
164
- cwd: migrationsDir,
165
- }),
166
- );
163
+ const entries = await glob("**/*.sql", {
164
+ cwd: migrationsDir,
165
+ });
167
166
 
168
167
  const sqlFiles = entries.sort((a: string, b: string) => {
169
168
  const aNum = getPrefix(a);
@@ -39,13 +39,14 @@ export * from "./permission-groups.ts";
39
39
  export * from "./pipeline.ts";
40
40
  export * from "./queue-consumer.ts";
41
41
  export * from "./queue.ts";
42
+ export * from "./quick-tunnel.ts";
42
43
  export * from "./rate-limit.ts";
43
44
  export * from "./react-router/react-router.ts";
44
45
  export * from "./redirect-rule.ts";
45
46
  export * from "./redwood/redwood.ts";
46
47
  export * from "./route.ts";
47
- export * from "./ruleset.ts";
48
48
  export * from "./rule.ts";
49
+ export * from "./ruleset.ts";
49
50
  export * from "./secret-key.ts";
50
51
  export * from "./secret.ts";
51
52
  export * from "./secrets-store.ts";
@@ -69,9 +69,15 @@ export const buildWorkerOptions = async (
69
69
  name: input.name,
70
70
  compatibilityDate: input.compatibilityDate,
71
71
  compatibilityFlags: input.compatibilityFlags,
72
- // TODO: Setting `proxy: true` here causes the following error when connecting via a websocket:
73
- // workerd/io/worker.c++:2164: info: uncaught exception; source = Uncaught (in promise); stack = TypeError: Invalid URL string.
74
- unsafeDirectSockets: [{ proxy: false }],
72
+ unsafeDirectSockets: [
73
+ // This matches the Wrangler configuration by exposing the default handler (e.g. `export default { fetch }`).
74
+ // However, unlike Wrangler, we set `proxy: false` to avoid the following error when connecting via a websocket:
75
+ // workerd/io/worker.c++:2164: info: uncaught exception; source = Uncaught (in promise); stack = TypeError: Invalid URL string.
76
+ {
77
+ entrypoint: "default",
78
+ proxy: false,
79
+ },
80
+ ],
75
81
  containerEngine: {
76
82
  localDocker: {
77
83
  socketPath:
@@ -164,6 +170,13 @@ export const buildWorkerOptions = async (
164
170
  scriptName: binding.scriptName,
165
171
  useSQLite: binding.sqlite,
166
172
  };
173
+ // This matches Wrangler configuration for exposing durable objects as an entrypoint.
174
+ // See https://github.com/cloudflare/workers-sdk/blob/ae0c806087c203da6a3d7da450e8fabe0d81c987/packages/wrangler/src/dev/miniflare/index.ts#L1266
175
+ options.unsafeDirectSockets!.push({
176
+ entrypoint: binding.className,
177
+ serviceName: binding.scriptName,
178
+ proxy: true,
179
+ });
167
180
  break;
168
181
  }
169
182
  case "hyperdrive": {
@@ -82,6 +82,10 @@ export class MiniflareController {
82
82
  log: process.env.DEBUG
83
83
  ? new miniflare.Log(miniflare.LogLevel.DEBUG)
84
84
  : undefined,
85
+
86
+ // This is required to allow websites and other separate processes
87
+ // to detect Alchemy-managed Durable Objects via the Wrangler dev registry.
88
+ unsafeDevRegistryDurableObjectProxy: true,
85
89
  };
86
90
  for (const worker of this.options.values()) {
87
91
  options.workers.push(worker);
@@ -0,0 +1,30 @@
1
+ import type { Scope } from "../scope.ts";
2
+
3
+ /**
4
+ * Starts a Cloudflare Quick Tunnel to a local server.
5
+ *
6
+ * @returns the Local URL and the publicly accessible URL of the Quick Tunnel
7
+ */
8
+ export const quickTunnel = async (
9
+ scope: Scope,
10
+ localUrl = "http://localhost:8080",
11
+ ) => ({
12
+ /**
13
+ * The URL of the local server.
14
+ */
15
+ localUrl,
16
+ /**
17
+ * The URL of the publicly accessible Quick Tunnel.
18
+ */
19
+ tunnelUrl: await scope.spawn("tunnel", {
20
+ cmd: `cloudflared tunnel --url ${localUrl}`,
21
+ // used to check if a PID is a cloudflared process when the parent exits, for resumability
22
+ processName: "cloudflared",
23
+ extract: (line) => {
24
+ const match = line.match(/https:\/\/([^\s]+)\.trycloudflare\.com/);
25
+ if (match) {
26
+ return `https://${match[1]}.trycloudflare.com`;
27
+ }
28
+ },
29
+ }),
30
+ });
@@ -1,12 +1,12 @@
1
1
  import adapter, { type AdapterOptions } from "@sveltejs/adapter-cloudflare";
2
2
  import { getPlatformProxyOptions } from "../cloudflare-env-proxy.ts";
3
3
 
4
- const alchemy = (options?: AdapterOptions) => {
5
- const platformProxy = getPlatformProxyOptions(options?.platformProxy);
4
+ export default (options?: AdapterOptions) => {
5
+ const { platformProxy: proxyOptions, ...config } = options ?? {};
6
+ const platformProxy = getPlatformProxyOptions(proxyOptions);
6
7
  return adapter({
7
8
  platformProxy,
8
- ...options,
9
+ config: platformProxy.configPath,
10
+ ...config,
9
11
  });
10
12
  };
11
-
12
- export default alchemy;
@@ -1,4 +1,5 @@
1
1
  import esbuild from "esbuild";
2
+ import { globIterate } from "glob";
2
3
  import { err, ok, type Result } from "neverthrow";
3
4
  import fs from "node:fs/promises";
4
5
  import path from "pathe";
@@ -222,7 +223,7 @@ export namespace WorkerBundleSource {
222
223
  const fileNames = new Set<string>();
223
224
  await Promise.all(
224
225
  this.globs.map(async (glob) => {
225
- for await (const file of fs.glob(glob, { cwd: this.root })) {
226
+ for await (const file of globIterate(glob, { cwd: this.root })) {
226
227
  fileNames.add(file);
227
228
  }
228
229
  }),
package/src/scope.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import kleur from "kleur";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import path from "node:path";
3
4
  import util from "node:util";
4
5
  import type { Phase } from "./alchemy.ts";
5
6
  import { destroy, destroyAll, DestroyStrategy } from "./destroy.ts";
@@ -22,6 +23,7 @@ import {
22
23
  createLoggerInstance,
23
24
  type LoggerApi,
24
25
  } from "./util/cli.ts";
26
+ import { idempotentSpawn } from "./util/idempotent-spawn.ts";
25
27
  import { logger } from "./util/logger.ts";
26
28
  import { AsyncMutex } from "./util/mutex.ts";
27
29
  import type { ITelemetryClient } from "./util/telemetry/client.ts";
@@ -64,8 +66,21 @@ export interface ScopeOptions {
64
66
  * @default "sequential"
65
67
  */
66
68
  destroyStrategy?: DestroyStrategy;
69
+ /**
70
+ * The telemetry client to use for the scope.
71
+ *
72
+ */
67
73
  telemetryClient?: ITelemetryClient;
74
+ /**
75
+ * The logger to use for the scope.
76
+ */
68
77
  logger?: LoggerApi;
78
+ /**
79
+ * The path to the .alchemy directory.
80
+ *
81
+ * @default "./.alchemy"
82
+ */
83
+ dotAlchemy?: string;
69
84
  }
70
85
 
71
86
  export type PendingDeletions = Array<{
@@ -154,6 +169,8 @@ export class Scope {
154
169
  return this.scopeName;
155
170
  }
156
171
 
172
+ public readonly dotAlchemy: string;
173
+
157
174
  constructor(options: ScopeOptions) {
158
175
  this.scopeName = options.scopeName;
159
176
  this.name = this.scopeName;
@@ -166,6 +183,10 @@ export class Scope {
166
183
  `Scope name "${this.scopeName}" cannot contain double colons`,
167
184
  );
168
185
  }
186
+ this.dotAlchemy =
187
+ options.dotAlchemy ??
188
+ this.parent?.dotAlchemy ??
189
+ path.join(process.cwd(), ".alchemy");
169
190
 
170
191
  this.stage = options?.stage ?? this.parent?.stage ?? DEFAULT_STAGE;
171
192
  this.parent?.children.set(this.scopeName!, this);
@@ -216,6 +237,46 @@ export class Scope {
216
237
  this.dataMutex = new AsyncMutex();
217
238
  }
218
239
 
240
+ public async spawn<
241
+ E extends ((line: string) => string | undefined) | undefined,
242
+ >(
243
+ // TODO(sam): validate uniqueness? Ensure a flat .logs/${id}.log dir? Or nest in scope dirs?
244
+ id: string,
245
+ options: {
246
+ /**
247
+ * The command to run (e.g. `cloudflared tunnel --url http://localhost:8080`).
248
+ */
249
+ cmd: string;
250
+ /**
251
+ * Function executed on each line of the process's output (stdout and stderr) to extract a value.
252
+ */
253
+ extract?: E;
254
+ /**
255
+ * Name of the process - used to check if a PID is a cloudflared process when the parent exits, for resumability
256
+ */
257
+ processName?: string;
258
+ /**
259
+ * Function to check if a PID is the same process as the one that was spawned.
260
+ *
261
+ * Used to check if a PID is a cloudflared process when the parent exits, for resumability.
262
+ *
263
+ * One of {@link extract} or {@link processName} must be provided.
264
+ */
265
+ isSameProcess?: (pid: number) => Promise<boolean>;
266
+ },
267
+ ) {
268
+ const dotAlchemy = path.join(process.cwd(), ".alchemy");
269
+ const logsDir = path.join(dotAlchemy, "logs");
270
+ const pidsDir = path.join(dotAlchemy, "pids");
271
+
272
+ const extracted = await idempotentSpawn({
273
+ log: path.join(logsDir, `${id}.log`),
274
+ stateFile: path.join(pidsDir, `${id}.pid.json`),
275
+ ...options,
276
+ });
277
+ return extracted as E extends undefined ? undefined : string;
278
+ }
279
+
219
280
  /**
220
281
  * @internal
221
282
  */