alchemy 0.61.0 → 0.62.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/README.md +1 -2
- package/bin/alchemy.js +1406 -1464
- package/bin/commands/init.ts +29 -2
- package/bin/services/execute-alchemy.ts +1 -2
- package/lib/alchemy.d.ts +2 -2
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js.map +1 -1
- package/lib/aws/client-props.d.ts +149 -0
- package/lib/aws/client-props.d.ts.map +1 -0
- package/lib/aws/client-props.js +1 -0
- package/lib/aws/client-props.js.map +1 -0
- package/lib/aws/credentials.d.ts +68 -5
- package/lib/aws/credentials.d.ts.map +1 -1
- package/lib/aws/credentials.js +148 -0
- package/lib/aws/credentials.js.map +1 -1
- package/lib/aws/ec2/internet-gateway-attachment.d.ts +57 -3
- package/lib/aws/ec2/internet-gateway-attachment.d.ts.map +1 -1
- package/lib/aws/ec2/internet-gateway-attachment.js +57 -3
- package/lib/aws/ec2/internet-gateway-attachment.js.map +1 -1
- package/lib/aws/ec2/internet-gateway.d.ts +68 -40
- package/lib/aws/ec2/internet-gateway.d.ts.map +1 -1
- package/lib/aws/ec2/internet-gateway.js +68 -40
- package/lib/aws/ec2/internet-gateway.js.map +1 -1
- package/lib/aws/ec2/nat-gateway.d.ts +99 -18
- package/lib/aws/ec2/nat-gateway.d.ts.map +1 -1
- package/lib/aws/ec2/nat-gateway.js +113 -37
- package/lib/aws/ec2/nat-gateway.js.map +1 -1
- package/lib/aws/ec2/route-table-association.d.ts +1 -1
- package/lib/aws/ec2/route-table-association.js +1 -1
- package/lib/aws/ec2/route-table.d.ts +64 -1
- package/lib/aws/ec2/route-table.d.ts.map +1 -1
- package/lib/aws/ec2/route-table.js +64 -1
- package/lib/aws/ec2/route-table.js.map +1 -1
- package/lib/aws/ec2/route.d.ts +60 -1
- package/lib/aws/ec2/route.d.ts.map +1 -1
- package/lib/aws/ec2/route.js +60 -1
- package/lib/aws/ec2/route.js.map +1 -1
- package/lib/aws/ec2/security-group.d.ts +96 -3
- package/lib/aws/ec2/security-group.d.ts.map +1 -1
- package/lib/aws/ec2/security-group.js +96 -3
- package/lib/aws/ec2/security-group.js.map +1 -1
- package/lib/aws/ec2/subnet.d.ts +61 -3
- package/lib/aws/ec2/subnet.d.ts.map +1 -1
- package/lib/aws/ec2/subnet.js +61 -3
- package/lib/aws/ec2/subnet.js.map +1 -1
- package/lib/aws/ec2/utils.d.ts +10 -2
- package/lib/aws/ec2/utils.d.ts.map +1 -1
- package/lib/aws/ec2/utils.js +68 -7
- package/lib/aws/ec2/utils.js.map +1 -1
- package/lib/aws/ec2/vpc.d.ts +50 -2
- package/lib/aws/ec2/vpc.d.ts.map +1 -1
- package/lib/aws/ec2/vpc.js +50 -2
- package/lib/aws/ec2/vpc.js.map +1 -1
- package/lib/aws/index.d.ts +2 -0
- package/lib/aws/index.d.ts.map +1 -1
- package/lib/aws/index.js +2 -0
- package/lib/aws/index.js.map +1 -1
- package/lib/cloudflare/api-response.js +2 -2
- package/lib/cloudflare/api-response.js.map +1 -1
- package/lib/cloudflare/api.d.ts +19 -0
- package/lib/cloudflare/api.d.ts.map +1 -1
- package/lib/cloudflare/api.js +4 -0
- package/lib/cloudflare/api.js.map +1 -1
- package/lib/cloudflare/assets.d.ts.map +1 -1
- package/lib/cloudflare/assets.js +3 -4
- package/lib/cloudflare/assets.js.map +1 -1
- package/lib/cloudflare/bucket.d.ts +5 -3
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +22 -18
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/bundle/plugin-compat-warning.js +7 -7
- package/lib/cloudflare/bundle/plugin-compat-warning.js.map +1 -1
- package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
- package/lib/cloudflare/kv-namespace.d.ts +2 -2
- package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
- package/lib/cloudflare/kv-namespace.js.map +1 -1
- package/lib/cloudflare/oauth.js +3 -3
- package/lib/cloudflare/oauth.js.map +1 -1
- package/lib/cloudflare/quick-tunnel.d.ts +1 -1
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +26 -97
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/worker-bundle.js +9 -8
- package/lib/cloudflare/worker-bundle.js.map +1 -1
- package/lib/cloudflare/worker-tail.d.ts.map +1 -1
- package/lib/cloudflare/worker-tail.js +6 -6
- package/lib/cloudflare/worker-tail.js.map +1 -1
- package/lib/resource.d.ts +1 -0
- package/lib/resource.d.ts.map +1 -1
- package/lib/resource.js +13 -2
- package/lib/resource.js.map +1 -1
- package/lib/scope.d.ts +24 -23
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +25 -20
- package/lib/scope.js.map +1 -1
- package/lib/state/cloudflare-state-store.d.ts.map +1 -1
- package/lib/state/cloudflare-state-store.js +2 -1
- package/lib/state/cloudflare-state-store.js.map +1 -1
- package/lib/util/idempotent-spawn.d.ts +10 -8
- package/lib/util/idempotent-spawn.d.ts.map +1 -1
- package/lib/util/idempotent-spawn.js +97 -78
- package/lib/util/idempotent-spawn.js.map +1 -1
- package/lib/util/index.d.ts +2 -0
- package/lib/util/index.d.ts.map +1 -0
- package/lib/util/index.js +2 -0
- package/lib/util/index.js.map +1 -0
- package/lib/util/safe-fetch.d.ts +24 -0
- package/lib/util/safe-fetch.d.ts.map +1 -1
- package/lib/util/safe-fetch.js +72 -0
- package/lib/util/safe-fetch.js.map +1 -1
- package/package.json +8 -5
- package/src/alchemy.ts +2 -2
- package/src/aws/client-props.ts +156 -0
- package/src/aws/credentials.ts +165 -4
- package/src/aws/ec2/internet-gateway-attachment.ts +59 -4
- package/src/aws/ec2/internet-gateway.ts +70 -41
- package/src/aws/ec2/nat-gateway.ts +123 -33
- package/src/aws/ec2/route-table-association.ts +1 -1
- package/src/aws/ec2/route-table.ts +66 -2
- package/src/aws/ec2/route.ts +62 -2
- package/src/aws/ec2/security-group.ts +98 -4
- package/src/aws/ec2/subnet.ts +63 -4
- package/src/aws/ec2/utils.ts +73 -7
- package/src/aws/ec2/vpc.ts +52 -3
- package/src/aws/index.ts +2 -0
- package/src/cloudflare/api-response.ts +2 -2
- package/src/cloudflare/api.ts +23 -0
- package/src/cloudflare/assets.ts +3 -4
- package/src/cloudflare/bucket.ts +27 -25
- package/src/cloudflare/bundle/plugin-compat-warning.ts +7 -7
- package/src/cloudflare/cloudflare-env-proxy.ts +2 -2
- package/src/cloudflare/kv-namespace.ts +26 -27
- package/src/cloudflare/oauth.ts +3 -3
- package/src/cloudflare/website.ts +30 -111
- package/src/cloudflare/worker-bundle.ts +9 -8
- package/src/cloudflare/worker-tail.ts +6 -8
- package/src/resource.ts +19 -2
- package/src/scope.ts +73 -43
- package/src/state/cloudflare-state-store.ts +2 -1
- package/src/util/idempotent-spawn.ts +117 -100
- package/src/util/index.ts +1 -0
- package/src/util/safe-fetch.ts +114 -0
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { once } from "node:events";
|
|
4
2
|
import fs from "node:fs/promises";
|
|
5
3
|
import path from "node:path";
|
|
6
4
|
import { alchemy } from "../alchemy.ts";
|
|
7
5
|
import { Exec } from "../os/index.ts";
|
|
8
6
|
import { Scope } from "../scope.ts";
|
|
9
|
-
import { isSecret
|
|
7
|
+
import { isSecret } from "../secret.ts";
|
|
10
8
|
import { dedent } from "../util/dedent.ts";
|
|
11
|
-
import { DeferredPromise } from "../util/deferred-promise.ts";
|
|
12
|
-
import { exists } from "../util/exists.ts";
|
|
13
9
|
import { logger } from "../util/logger.ts";
|
|
14
10
|
import { Assets } from "./assets.ts";
|
|
15
11
|
import type { Bindings } from "./bindings.ts";
|
|
@@ -304,14 +300,38 @@ export async function Website<B extends Bindings>(
|
|
|
304
300
|
|
|
305
301
|
let url: string | undefined;
|
|
306
302
|
if (dev && scope.local) {
|
|
307
|
-
url = await
|
|
308
|
-
|
|
309
|
-
|
|
303
|
+
url = await scope.spawn(name, {
|
|
304
|
+
cmd: typeof dev === "string" ? dev : dev.command,
|
|
305
|
+
cwd: paths.cwd,
|
|
306
|
+
extract: (line) => {
|
|
307
|
+
const URL_REGEX =
|
|
308
|
+
/http:\/\/(localhost|0\.0\.0\.0|127\.0\.0\.1|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d+\/?/;
|
|
309
|
+
const match = line
|
|
310
|
+
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
|
|
311
|
+
.match(URL_REGEX);
|
|
312
|
+
if (match) {
|
|
313
|
+
return match[0];
|
|
314
|
+
}
|
|
315
|
+
},
|
|
310
316
|
env: {
|
|
311
|
-
...
|
|
317
|
+
...Object.fromEntries(
|
|
318
|
+
Object.entries(env ?? {}).flatMap(([key, value]) => {
|
|
319
|
+
if (isSecret(value)) {
|
|
320
|
+
return [[key, value.unencrypted]];
|
|
321
|
+
}
|
|
322
|
+
if (typeof value === "string") {
|
|
323
|
+
return [[key, value]];
|
|
324
|
+
}
|
|
325
|
+
return [];
|
|
326
|
+
}),
|
|
327
|
+
),
|
|
312
328
|
...(typeof dev === "object" ? dev.env : {}),
|
|
329
|
+
FORCE_COLOR: "1",
|
|
330
|
+
...process.env,
|
|
331
|
+
// NOTE: we must set this to ensure the user does not accidentally set `NODE_ENV=production`
|
|
332
|
+
// which breaks `vite dev` (it won't, for example, re-write `process.env.TSS_APP_BASE` in the `.js` client side bundle)
|
|
333
|
+
NODE_ENV: "development",
|
|
313
334
|
},
|
|
314
|
-
cwd: paths.cwd,
|
|
315
335
|
});
|
|
316
336
|
}
|
|
317
337
|
|
|
@@ -348,104 +368,3 @@ async function writeMiniflareSymlink(cwd: string) {
|
|
|
348
368
|
}
|
|
349
369
|
});
|
|
350
370
|
}
|
|
351
|
-
|
|
352
|
-
async function runDevCommand(
|
|
353
|
-
scope: Scope,
|
|
354
|
-
props: {
|
|
355
|
-
id: string;
|
|
356
|
-
command: string;
|
|
357
|
-
env: Record<string, string | Secret<string> | undefined>;
|
|
358
|
-
cwd?: string;
|
|
359
|
-
},
|
|
360
|
-
) {
|
|
361
|
-
const persistFile = path.join(process.cwd(), ".alchemy", `${props.id}.pid`);
|
|
362
|
-
if (await exists(persistFile)) {
|
|
363
|
-
const pid = Number.parseInt(await fs.readFile(persistFile, "utf8"));
|
|
364
|
-
try {
|
|
365
|
-
// Actually kill the process if it's alive
|
|
366
|
-
process.kill(pid, "SIGTERM");
|
|
367
|
-
} catch {
|
|
368
|
-
// ignore
|
|
369
|
-
}
|
|
370
|
-
try {
|
|
371
|
-
await fs.unlink(persistFile);
|
|
372
|
-
} catch {
|
|
373
|
-
// ignore
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const command = props.command.split(" ");
|
|
377
|
-
const [cmd, ...args] = command;
|
|
378
|
-
|
|
379
|
-
const childProcess = spawn(cmd, args, {
|
|
380
|
-
cwd: props.cwd,
|
|
381
|
-
shell: true,
|
|
382
|
-
env: {
|
|
383
|
-
FORCE_COLOR: "1",
|
|
384
|
-
...process.env,
|
|
385
|
-
...Object.fromEntries(
|
|
386
|
-
Object.entries(props.env ?? {}).flatMap(([key, value]) => {
|
|
387
|
-
if (isSecret(value)) {
|
|
388
|
-
return [[key, value.unencrypted]];
|
|
389
|
-
}
|
|
390
|
-
if (typeof value === "string") {
|
|
391
|
-
return [[key, value]];
|
|
392
|
-
}
|
|
393
|
-
return [];
|
|
394
|
-
}),
|
|
395
|
-
),
|
|
396
|
-
// NOTE: we must set this to ensure the user does not accidentally set `NODE_ENV=production`
|
|
397
|
-
// which breaks `vite dev` (it won't, for example, re-write `process.env.TSS_APP_BASE` in the `.js` client side bundle)
|
|
398
|
-
NODE_ENV: "development",
|
|
399
|
-
},
|
|
400
|
-
stdio: "pipe",
|
|
401
|
-
});
|
|
402
|
-
await once(childProcess, "spawn");
|
|
403
|
-
scope.onCleanup(async () => {
|
|
404
|
-
if (childProcess.exitCode === null && !childProcess.killed) {
|
|
405
|
-
childProcess.kill("SIGTERM");
|
|
406
|
-
await Promise.any([
|
|
407
|
-
once(childProcess, "exit"),
|
|
408
|
-
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
409
|
-
]);
|
|
410
|
-
if (!childProcess.killed) {
|
|
411
|
-
childProcess.kill("SIGKILL");
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
try {
|
|
415
|
-
await fs.unlink(persistFile);
|
|
416
|
-
} catch {
|
|
417
|
-
// ignore
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
if (childProcess.pid) {
|
|
421
|
-
await fs.mkdir(path.dirname(persistFile), { recursive: true });
|
|
422
|
-
await fs.writeFile(persistFile, childProcess.pid.toString());
|
|
423
|
-
}
|
|
424
|
-
const URL_REGEX =
|
|
425
|
-
/http:\/\/(?:(?:localhost|0\.0\.0\.0|127\.0\.0\.1)|(?:\d{1,3}\.){3}\d{1,3}):\d+(?:\/)?/;
|
|
426
|
-
const promise = new DeferredPromise<string>();
|
|
427
|
-
childProcess.stdout.on("data", (data) => {
|
|
428
|
-
if (promise.status === "pending") {
|
|
429
|
-
const match = data
|
|
430
|
-
.toString()
|
|
431
|
-
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
|
|
432
|
-
.match(URL_REGEX);
|
|
433
|
-
if (match) {
|
|
434
|
-
promise.resolve(match[0]);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
process.stdout.write(data);
|
|
438
|
-
});
|
|
439
|
-
childProcess.stderr.on("data", (data) => {
|
|
440
|
-
process.stderr.write(data);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
return await Promise.race([
|
|
444
|
-
promise.value,
|
|
445
|
-
once(childProcess, "exit").then(([code, signal]) => {
|
|
446
|
-
throw new Error(
|
|
447
|
-
`Dev command "${props.command}" for "${props.id}" failed to start (code: ${code}, signal: ${signal})`,
|
|
448
|
-
);
|
|
449
|
-
}),
|
|
450
|
-
]);
|
|
451
|
-
}
|
|
@@ -93,21 +93,22 @@ export namespace WorkerBundle {
|
|
|
93
93
|
paths: string[],
|
|
94
94
|
format: "esm" | "cjs",
|
|
95
95
|
): WorkerBundle.Module[] => {
|
|
96
|
-
return paths.map((
|
|
97
|
-
const
|
|
96
|
+
return paths.map((filePath) => {
|
|
97
|
+
const normalizedPath = path.normalize(filePath);
|
|
98
|
+
const ext = normalizedPath.split(".").pop();
|
|
98
99
|
switch (ext) {
|
|
99
100
|
case "js":
|
|
100
|
-
return { type: format, path };
|
|
101
|
+
return { type: format, path: normalizedPath };
|
|
101
102
|
case "mjs":
|
|
102
|
-
return { type: "esm", path };
|
|
103
|
+
return { type: "esm", path: normalizedPath };
|
|
103
104
|
case "cjs":
|
|
104
|
-
return { type: "cjs", path };
|
|
105
|
+
return { type: "cjs", path: normalizedPath };
|
|
105
106
|
case "wasm":
|
|
106
|
-
return { type: "wasm", path };
|
|
107
|
+
return { type: "wasm", path: normalizedPath };
|
|
107
108
|
case "map":
|
|
108
|
-
return { type: "sourcemap", path };
|
|
109
|
+
return { type: "sourcemap", path: normalizedPath };
|
|
109
110
|
default:
|
|
110
|
-
return { type: "text", path };
|
|
111
|
+
return { type: "text", path: normalizedPath };
|
|
111
112
|
}
|
|
112
113
|
});
|
|
113
114
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import pc from "picocolors";
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
3
|
import { logger } from "../util/logger.ts";
|
|
4
4
|
import type { CloudflareApiResponse } from "./api-response.ts";
|
|
@@ -102,7 +102,7 @@ class TailClient {
|
|
|
102
102
|
});
|
|
103
103
|
ws.on("message", (message) => {
|
|
104
104
|
const data: TailEventMessage = JSON.parse(message.toString());
|
|
105
|
-
const prefix =
|
|
105
|
+
const prefix = pc.blue(`[${this.id}]`);
|
|
106
106
|
if (data.event && "request" in data.event) {
|
|
107
107
|
// TODO: handle other event types
|
|
108
108
|
const event = data.event;
|
|
@@ -110,18 +110,16 @@ class TailClient {
|
|
|
110
110
|
const status = event.response?.status ?? 500;
|
|
111
111
|
// TODO: make this look nicer
|
|
112
112
|
logger.log(
|
|
113
|
-
`${prefix} ${
|
|
113
|
+
`${prefix} ${pc.gray(event.request.method)} ${url.pathname} ${pc.dim(">")} ${status >= 200 && status < 300 ? pc.green(status) : pc.red(status)} ${pc.gray(`(cpu: ${Math.round(data.cpuTime)}ms, wall: ${Math.round(data.wallTime)}ms)`)}`,
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
for (const log of data.logs) {
|
|
117
|
-
logger.log(
|
|
118
|
-
`${prefix} ${kleur.gray(log.level)} ${log.message.join(" ")}`,
|
|
119
|
-
);
|
|
117
|
+
logger.log(`${prefix} ${pc.gray(log.level)} ${log.message.join(" ")}`);
|
|
120
118
|
}
|
|
121
119
|
for (const exception of data.exceptions) {
|
|
122
|
-
const start = `${prefix} ${
|
|
120
|
+
const start = `${prefix} ${pc.red(exception.name)}`;
|
|
123
121
|
logger.log(
|
|
124
|
-
`${start} ${exception.message}\n${
|
|
122
|
+
`${start} ${exception.message}\n${pc.gray(exception.stack)}`,
|
|
125
123
|
);
|
|
126
124
|
}
|
|
127
125
|
});
|
package/src/resource.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Scope as _Scope, type Scope } from "./scope.ts";
|
|
|
5
5
|
|
|
6
6
|
declare global {
|
|
7
7
|
var ALCHEMY_PROVIDERS: Map<ResourceKind, Provider<string, any>>;
|
|
8
|
+
var ALCHEMY_HANDLERS: Map<ResourceKind, ResourceLifecycleHandler>;
|
|
8
9
|
var ALCHEMY_DYNAMIC_RESOURCE_RESOLVERS: DynamicResourceResolver[];
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -15,6 +16,12 @@ export const PROVIDERS: Map<
|
|
|
15
16
|
ResourceKind,
|
|
16
17
|
Provider<string, any>
|
|
17
18
|
>());
|
|
19
|
+
const HANDLERS: Map<ResourceKind, ResourceLifecycleHandler> =
|
|
20
|
+
(globalThis.ALCHEMY_HANDLERS ??= new Map<
|
|
21
|
+
ResourceKind,
|
|
22
|
+
ResourceLifecycleHandler
|
|
23
|
+
>());
|
|
24
|
+
|
|
18
25
|
const DYNAMIC_RESOURCE_RESOLVERS: DynamicResourceResolver[] =
|
|
19
26
|
(globalThis.ALCHEMY_DYNAMIC_RESOURCE_RESOLVERS ??= []);
|
|
20
27
|
|
|
@@ -132,10 +139,20 @@ export function Resource<
|
|
|
132
139
|
const Type extends ResourceKind,
|
|
133
140
|
F extends ResourceLifecycleHandler,
|
|
134
141
|
>(type: Type, ...args: [Partial<ProviderOptions>, F] | [F]): Handler<F> {
|
|
142
|
+
const [options, handler] = args.length === 2 ? args : [undefined, args[0]];
|
|
135
143
|
if (PROVIDERS.has(type)) {
|
|
136
|
-
|
|
144
|
+
// We want Alchemy to work in a PNPM monorepo environment unfortunately,
|
|
145
|
+
// global registries do not work because PNPM's symlinks result in multiple
|
|
146
|
+
// instances of alchemy being loaded even if the package version is the
|
|
147
|
+
// same (peers do not fix this).
|
|
148
|
+
// MITIGATION: to ensure that most cases of accidental double-registration
|
|
149
|
+
// are caught, we're going to check the handler's toString() to see if it's
|
|
150
|
+
// the same as the previous handler. if it is, we're going to overwrite it.
|
|
151
|
+
if (HANDLERS.get(type)!.toString() !== handler.toString()) {
|
|
152
|
+
throw new Error(`Resource ${type} already exists`);
|
|
153
|
+
}
|
|
137
154
|
}
|
|
138
|
-
|
|
155
|
+
HANDLERS.set(type, handler);
|
|
139
156
|
|
|
140
157
|
type Out = Awaited<ReturnType<F>>;
|
|
141
158
|
|
package/src/scope.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import kleur from "kleur";
|
|
2
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import util from "node:util";
|
|
4
|
+
import pc from "picocolors";
|
|
5
5
|
import type { Phase } from "./alchemy.ts";
|
|
6
6
|
import { destroy, destroyAll, DestroyStrategy } from "./destroy.ts";
|
|
7
7
|
import {
|
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
createLoggerInstance,
|
|
24
24
|
type LoggerApi,
|
|
25
25
|
} from "./util/cli.ts";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
idempotentSpawn,
|
|
28
|
+
type IdempotentSpawnOptions,
|
|
29
|
+
} from "./util/idempotent-spawn.ts";
|
|
27
30
|
import { logger } from "./util/logger.ts";
|
|
28
31
|
import { AsyncMutex } from "./util/mutex.ts";
|
|
29
32
|
import type { ITelemetryClient } from "./util/telemetry/client.ts";
|
|
@@ -34,7 +37,7 @@ export class RootScopeStateAttemptError extends Error {
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
export interface ScopeOptions {
|
|
40
|
+
export interface ScopeOptions extends ProviderCredentials {
|
|
38
41
|
stage?: string;
|
|
39
42
|
parent: Scope | undefined;
|
|
40
43
|
scopeName: string;
|
|
@@ -83,6 +86,29 @@ export interface ScopeOptions {
|
|
|
83
86
|
dotAlchemy?: string;
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Base interface for provider credentials that can be extended by each provider.
|
|
91
|
+
* This allows providers to add their own credential properties without modifying the core scope interface.
|
|
92
|
+
*
|
|
93
|
+
* Provider credentials cannot conflict with core ScopeOptions properties.
|
|
94
|
+
*
|
|
95
|
+
* Providers can extend this interface using module augmentation:
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // In aws/scope-extensions.ts
|
|
100
|
+
* declare module "../scope.ts" {
|
|
101
|
+
* interface ProviderCredentials {
|
|
102
|
+
* aws?: AwsClientProps;
|
|
103
|
+
* }
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export interface ProviderCredentials extends Record<string, unknown> {
|
|
108
|
+
// Provider credentials should not conflict with core scope properties
|
|
109
|
+
// TypeScript will enforce this at compile time when providers extend this interface
|
|
110
|
+
}
|
|
111
|
+
|
|
86
112
|
export type PendingDeletions = Array<{
|
|
87
113
|
resource: Resource<string>;
|
|
88
114
|
oldProps?: ResourceProps;
|
|
@@ -155,6 +181,9 @@ export class Scope {
|
|
|
155
181
|
public readonly telemetryClient: ITelemetryClient;
|
|
156
182
|
public readonly dataMutex: AsyncMutex;
|
|
157
183
|
|
|
184
|
+
// Provider credentials for scope-level credential overrides
|
|
185
|
+
public readonly providerCredentials: ProviderCredentials;
|
|
186
|
+
|
|
158
187
|
private isErrored = false;
|
|
159
188
|
private isSkipped = false;
|
|
160
189
|
private finalized = false;
|
|
@@ -172,9 +201,31 @@ export class Scope {
|
|
|
172
201
|
public readonly dotAlchemy: string;
|
|
173
202
|
|
|
174
203
|
constructor(options: ScopeOptions) {
|
|
175
|
-
|
|
204
|
+
// Extract core scope options first
|
|
205
|
+
const {
|
|
206
|
+
scopeName,
|
|
207
|
+
parent,
|
|
208
|
+
stage,
|
|
209
|
+
password,
|
|
210
|
+
stateStore,
|
|
211
|
+
quiet,
|
|
212
|
+
phase,
|
|
213
|
+
local,
|
|
214
|
+
watch,
|
|
215
|
+
force,
|
|
216
|
+
destroyStrategy,
|
|
217
|
+
telemetryClient,
|
|
218
|
+
logger,
|
|
219
|
+
dotAlchemy,
|
|
220
|
+
...providerCredentials
|
|
221
|
+
} = options;
|
|
222
|
+
|
|
223
|
+
this.scopeName = scopeName;
|
|
176
224
|
this.name = this.scopeName;
|
|
177
|
-
this.parent =
|
|
225
|
+
this.parent = parent ?? Scope.getScope();
|
|
226
|
+
|
|
227
|
+
// Store provider credentials (TypeScript ensures no conflicts with core options)
|
|
228
|
+
this.providerCredentials = providerCredentials as ProviderCredentials;
|
|
178
229
|
|
|
179
230
|
const isChild = this.parent !== undefined;
|
|
180
231
|
if (this.scopeName?.includes(":") && isChild) {
|
|
@@ -188,18 +239,18 @@ export class Scope {
|
|
|
188
239
|
this.parent?.dotAlchemy ??
|
|
189
240
|
path.join(process.cwd(), ".alchemy");
|
|
190
241
|
|
|
191
|
-
this.stage =
|
|
242
|
+
this.stage = stage ?? this.parent?.stage ?? DEFAULT_STAGE;
|
|
192
243
|
this.parent?.children.set(this.scopeName!, this);
|
|
193
|
-
this.quiet =
|
|
244
|
+
this.quiet = quiet ?? this.parent?.quiet ?? false;
|
|
194
245
|
if (this.parent && !this.scopeName) {
|
|
195
246
|
throw new Error("Scope name is required when creating a child scope");
|
|
196
247
|
}
|
|
197
|
-
this.password =
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
248
|
+
this.password = password ?? this.parent?.password;
|
|
249
|
+
const resolvedPhase = phase ?? this.parent?.phase;
|
|
250
|
+
if (resolvedPhase === undefined) {
|
|
200
251
|
throw new Error("Phase is required");
|
|
201
252
|
}
|
|
202
|
-
this.phase =
|
|
253
|
+
this.phase = resolvedPhase;
|
|
203
254
|
|
|
204
255
|
this.logger = this.quiet
|
|
205
256
|
? createDummyLogger()
|
|
@@ -209,14 +260,14 @@ export class Scope {
|
|
|
209
260
|
stage: this.stage,
|
|
210
261
|
appName: this.appName ?? "",
|
|
211
262
|
},
|
|
212
|
-
|
|
263
|
+
logger,
|
|
213
264
|
);
|
|
214
265
|
|
|
215
|
-
this.local =
|
|
216
|
-
this.watch =
|
|
217
|
-
this.force =
|
|
266
|
+
this.local = local ?? this.parent?.local ?? false;
|
|
267
|
+
this.watch = watch ?? this.parent?.watch ?? false;
|
|
268
|
+
this.force = force ?? this.parent?.force ?? false;
|
|
218
269
|
this.destroyStrategy =
|
|
219
|
-
|
|
270
|
+
destroyStrategy ?? this.parent?.destroyStrategy ?? "sequential";
|
|
220
271
|
if (this.local) {
|
|
221
272
|
this.logger.warnOnce(
|
|
222
273
|
"Development mode is in beta. Please report any issues to https://github.com/sam-goodwin/alchemy/issues.",
|
|
@@ -224,14 +275,13 @@ export class Scope {
|
|
|
224
275
|
}
|
|
225
276
|
|
|
226
277
|
this.stateStore =
|
|
227
|
-
|
|
228
|
-
this.telemetryClient =
|
|
229
|
-
options.telemetryClient ?? this.parent?.telemetryClient!;
|
|
278
|
+
stateStore ?? this.parent?.stateStore ?? defaultStateStore;
|
|
279
|
+
this.telemetryClient = telemetryClient ?? this.parent?.telemetryClient!;
|
|
230
280
|
this.state = new InstrumentedStateStore(
|
|
231
281
|
this.stateStore(this),
|
|
232
282
|
this.telemetryClient,
|
|
233
283
|
);
|
|
234
|
-
if (!
|
|
284
|
+
if (!telemetryClient && !this.parent?.telemetryClient) {
|
|
235
285
|
throw new Error("Telemetry client is required");
|
|
236
286
|
}
|
|
237
287
|
this.dataMutex = new AsyncMutex();
|
|
@@ -242,28 +292,7 @@ export class Scope {
|
|
|
242
292
|
>(
|
|
243
293
|
// TODO(sam): validate uniqueness? Ensure a flat .logs/${id}.log dir? Or nest in scope dirs?
|
|
244
294
|
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
|
-
},
|
|
295
|
+
options: Omit<IdempotentSpawnOptions, "log" | "stateFile">,
|
|
267
296
|
) {
|
|
268
297
|
const dotAlchemy = path.join(process.cwd(), ".alchemy");
|
|
269
298
|
const logsDir = path.join(dotAlchemy, "logs");
|
|
@@ -480,6 +509,7 @@ export class Scope {
|
|
|
480
509
|
if (!this.isErrored && !this.isSkipped) {
|
|
481
510
|
// TODO: need to detect if it is in error
|
|
482
511
|
const resourceIds = await this.state.list();
|
|
512
|
+
|
|
483
513
|
const aliveIds = new Set(this.resources.keys());
|
|
484
514
|
const orphanIds = Array.from(
|
|
485
515
|
resourceIds.filter((id) => !aliveIds.has(id)),
|
|
@@ -607,7 +637,7 @@ export class Scope {
|
|
|
607
637
|
*/
|
|
608
638
|
public async cleanup() {
|
|
609
639
|
if (this.parent || this.cleanups.length === 0) return;
|
|
610
|
-
this.logger.log(
|
|
640
|
+
this.logger.log(pc.gray("Exiting..."));
|
|
611
641
|
await Promise.allSettled(this.cleanups.map((cleanup) => cleanup()));
|
|
612
642
|
}
|
|
613
643
|
|
|
@@ -15,6 +15,7 @@ import type { Scope } from "../scope.ts";
|
|
|
15
15
|
import type { Secret } from "../secret.ts";
|
|
16
16
|
import { logger } from "../util/logger.ts";
|
|
17
17
|
import { memoize } from "../util/memoize.ts";
|
|
18
|
+
import { safeFetch } from "../util/safe-fetch.ts";
|
|
18
19
|
import { StateStoreProxy } from "./proxy.ts";
|
|
19
20
|
|
|
20
21
|
export interface CloudflareStateStoreOptions extends CloudflareApiOptions {
|
|
@@ -70,7 +71,7 @@ export class CloudflareStateStore extends StateStoreProxy {
|
|
|
70
71
|
params,
|
|
71
72
|
context: { chain: this.scope.chain },
|
|
72
73
|
};
|
|
73
|
-
const response = await
|
|
74
|
+
const response = await safeFetch(url, {
|
|
74
75
|
method: "POST",
|
|
75
76
|
headers: {
|
|
76
77
|
Authorization: `Bearer ${token}`,
|