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.
- package/bin/alchemy.js +77 -37
- package/bin/commands/init.ts +65 -23
- package/bin/services/execute-alchemy.ts +33 -23
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +1 -0
- package/lib/alchemy.js.map +1 -1
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +8 -4
- package/lib/cloudflare/bucket.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/d1-migrations.d.ts.map +1 -1
- package/lib/cloudflare/d1-migrations.js +3 -2
- package/lib/cloudflare/d1-migrations.js.map +1 -1
- package/lib/cloudflare/index.d.ts +2 -1
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +2 -1
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +16 -3
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.js +3 -0
- package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
- package/lib/cloudflare/quick-tunnel.d.ts +17 -0
- package/lib/cloudflare/quick-tunnel.d.ts.map +1 -0
- package/lib/cloudflare/quick-tunnel.js +26 -0
- package/lib/cloudflare/quick-tunnel.js.map +1 -0
- package/lib/cloudflare/sveltekit/plugin.d.ts +2 -2
- package/lib/cloudflare/sveltekit/plugin.d.ts.map +1 -1
- package/lib/cloudflare/sveltekit/plugin.js +5 -4
- package/lib/cloudflare/sveltekit/plugin.js.map +1 -1
- package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/worker-bundle.js +2 -1
- package/lib/cloudflare/worker-bundle.js.map +1 -1
- package/lib/scope.d.ts +36 -0
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +20 -0
- package/lib/scope.js.map +1 -1
- package/lib/util/idempotent-spawn.d.ts +22 -0
- package/lib/util/idempotent-spawn.d.ts.map +1 -0
- package/lib/util/idempotent-spawn.js +218 -0
- package/lib/util/idempotent-spawn.js.map +1 -0
- package/package.json +2 -1
- package/src/alchemy.ts +1 -0
- package/src/cloudflare/bucket.ts +14 -6
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/d1-migrations.ts +4 -5
- package/src/cloudflare/index.ts +2 -1
- package/src/cloudflare/miniflare/build-worker-options.ts +16 -3
- package/src/cloudflare/miniflare/miniflare-controller.ts +4 -0
- package/src/cloudflare/quick-tunnel.ts +30 -0
- package/src/cloudflare/sveltekit/plugin.ts +5 -5
- package/src/cloudflare/worker-bundle.ts +2 -1
- package/src/scope.ts +61 -0
- package/src/util/idempotent-spawn.ts +286 -0
- 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.
|
|
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
package/src/cloudflare/bucket.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|
|
163
|
-
|
|
164
|
-
|
|
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);
|
package/src/cloudflare/index.ts
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
5
|
-
const 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
|
-
|
|
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
|
|
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
|
*/
|