alchemy 0.60.2 → 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.
Files changed (174) hide show
  1. package/README.md +1 -2
  2. package/bin/alchemy.js +1480 -1498
  3. package/bin/commands/init.ts +94 -25
  4. package/bin/services/execute-alchemy.ts +32 -23
  5. package/lib/alchemy.d.ts +2 -2
  6. package/lib/alchemy.d.ts.map +1 -1
  7. package/lib/alchemy.js +1 -0
  8. package/lib/alchemy.js.map +1 -1
  9. package/lib/aws/client-props.d.ts +149 -0
  10. package/lib/aws/client-props.d.ts.map +1 -0
  11. package/lib/aws/client-props.js +1 -0
  12. package/lib/aws/client-props.js.map +1 -0
  13. package/lib/aws/credentials.d.ts +68 -5
  14. package/lib/aws/credentials.d.ts.map +1 -1
  15. package/lib/aws/credentials.js +148 -0
  16. package/lib/aws/credentials.js.map +1 -1
  17. package/lib/aws/ec2/internet-gateway-attachment.d.ts +57 -3
  18. package/lib/aws/ec2/internet-gateway-attachment.d.ts.map +1 -1
  19. package/lib/aws/ec2/internet-gateway-attachment.js +57 -3
  20. package/lib/aws/ec2/internet-gateway-attachment.js.map +1 -1
  21. package/lib/aws/ec2/internet-gateway.d.ts +68 -40
  22. package/lib/aws/ec2/internet-gateway.d.ts.map +1 -1
  23. package/lib/aws/ec2/internet-gateway.js +68 -40
  24. package/lib/aws/ec2/internet-gateway.js.map +1 -1
  25. package/lib/aws/ec2/nat-gateway.d.ts +99 -18
  26. package/lib/aws/ec2/nat-gateway.d.ts.map +1 -1
  27. package/lib/aws/ec2/nat-gateway.js +113 -37
  28. package/lib/aws/ec2/nat-gateway.js.map +1 -1
  29. package/lib/aws/ec2/route-table-association.d.ts +1 -1
  30. package/lib/aws/ec2/route-table-association.js +1 -1
  31. package/lib/aws/ec2/route-table.d.ts +64 -1
  32. package/lib/aws/ec2/route-table.d.ts.map +1 -1
  33. package/lib/aws/ec2/route-table.js +64 -1
  34. package/lib/aws/ec2/route-table.js.map +1 -1
  35. package/lib/aws/ec2/route.d.ts +60 -1
  36. package/lib/aws/ec2/route.d.ts.map +1 -1
  37. package/lib/aws/ec2/route.js +60 -1
  38. package/lib/aws/ec2/route.js.map +1 -1
  39. package/lib/aws/ec2/security-group.d.ts +96 -3
  40. package/lib/aws/ec2/security-group.d.ts.map +1 -1
  41. package/lib/aws/ec2/security-group.js +96 -3
  42. package/lib/aws/ec2/security-group.js.map +1 -1
  43. package/lib/aws/ec2/subnet.d.ts +61 -3
  44. package/lib/aws/ec2/subnet.d.ts.map +1 -1
  45. package/lib/aws/ec2/subnet.js +61 -3
  46. package/lib/aws/ec2/subnet.js.map +1 -1
  47. package/lib/aws/ec2/utils.d.ts +10 -2
  48. package/lib/aws/ec2/utils.d.ts.map +1 -1
  49. package/lib/aws/ec2/utils.js +68 -7
  50. package/lib/aws/ec2/utils.js.map +1 -1
  51. package/lib/aws/ec2/vpc.d.ts +50 -2
  52. package/lib/aws/ec2/vpc.d.ts.map +1 -1
  53. package/lib/aws/ec2/vpc.js +50 -2
  54. package/lib/aws/ec2/vpc.js.map +1 -1
  55. package/lib/aws/index.d.ts +2 -0
  56. package/lib/aws/index.d.ts.map +1 -1
  57. package/lib/aws/index.js +2 -0
  58. package/lib/aws/index.js.map +1 -1
  59. package/lib/cloudflare/api-response.js +2 -2
  60. package/lib/cloudflare/api-response.js.map +1 -1
  61. package/lib/cloudflare/api.d.ts +19 -0
  62. package/lib/cloudflare/api.d.ts.map +1 -1
  63. package/lib/cloudflare/api.js +4 -0
  64. package/lib/cloudflare/api.js.map +1 -1
  65. package/lib/cloudflare/assets.d.ts.map +1 -1
  66. package/lib/cloudflare/assets.js +3 -4
  67. package/lib/cloudflare/assets.js.map +1 -1
  68. package/lib/cloudflare/bucket.d.ts +5 -3
  69. package/lib/cloudflare/bucket.d.ts.map +1 -1
  70. package/lib/cloudflare/bucket.js +28 -20
  71. package/lib/cloudflare/bucket.js.map +1 -1
  72. package/lib/cloudflare/bundle/plugin-compat-warning.js +7 -7
  73. package/lib/cloudflare/bundle/plugin-compat-warning.js.map +1 -1
  74. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  75. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  76. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  77. package/lib/cloudflare/d1-migrations.d.ts.map +1 -1
  78. package/lib/cloudflare/d1-migrations.js +3 -2
  79. package/lib/cloudflare/d1-migrations.js.map +1 -1
  80. package/lib/cloudflare/index.d.ts +2 -1
  81. package/lib/cloudflare/index.d.ts.map +1 -1
  82. package/lib/cloudflare/index.js +2 -1
  83. package/lib/cloudflare/index.js.map +1 -1
  84. package/lib/cloudflare/kv-namespace.d.ts +2 -2
  85. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  86. package/lib/cloudflare/kv-namespace.js.map +1 -1
  87. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  88. package/lib/cloudflare/miniflare/build-worker-options.js +16 -3
  89. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  90. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  91. package/lib/cloudflare/miniflare/miniflare-controller.js +3 -0
  92. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  93. package/lib/cloudflare/oauth.js +3 -3
  94. package/lib/cloudflare/oauth.js.map +1 -1
  95. package/lib/cloudflare/quick-tunnel.d.ts +17 -0
  96. package/lib/cloudflare/quick-tunnel.d.ts.map +1 -0
  97. package/lib/cloudflare/quick-tunnel.js +26 -0
  98. package/lib/cloudflare/quick-tunnel.js.map +1 -0
  99. package/lib/cloudflare/sveltekit/plugin.d.ts +2 -2
  100. package/lib/cloudflare/sveltekit/plugin.d.ts.map +1 -1
  101. package/lib/cloudflare/sveltekit/plugin.js +5 -4
  102. package/lib/cloudflare/sveltekit/plugin.js.map +1 -1
  103. package/lib/cloudflare/website.d.ts.map +1 -1
  104. package/lib/cloudflare/website.js +26 -97
  105. package/lib/cloudflare/website.js.map +1 -1
  106. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  107. package/lib/cloudflare/worker-bundle.js +11 -9
  108. package/lib/cloudflare/worker-bundle.js.map +1 -1
  109. package/lib/cloudflare/worker-tail.d.ts.map +1 -1
  110. package/lib/cloudflare/worker-tail.js +6 -6
  111. package/lib/cloudflare/worker-tail.js.map +1 -1
  112. package/lib/resource.d.ts +1 -0
  113. package/lib/resource.d.ts.map +1 -1
  114. package/lib/resource.js +13 -2
  115. package/lib/resource.js.map +1 -1
  116. package/lib/scope.d.ts +38 -1
  117. package/lib/scope.d.ts.map +1 -1
  118. package/lib/scope.js +44 -19
  119. package/lib/scope.js.map +1 -1
  120. package/lib/state/cloudflare-state-store.d.ts.map +1 -1
  121. package/lib/state/cloudflare-state-store.js +2 -1
  122. package/lib/state/cloudflare-state-store.js.map +1 -1
  123. package/lib/util/idempotent-spawn.d.ts +24 -0
  124. package/lib/util/idempotent-spawn.d.ts.map +1 -0
  125. package/lib/util/idempotent-spawn.js +237 -0
  126. package/lib/util/idempotent-spawn.js.map +1 -0
  127. package/lib/util/index.d.ts +2 -0
  128. package/lib/util/index.d.ts.map +1 -0
  129. package/lib/util/index.js +2 -0
  130. package/lib/util/index.js.map +1 -0
  131. package/lib/util/safe-fetch.d.ts +24 -0
  132. package/lib/util/safe-fetch.d.ts.map +1 -1
  133. package/lib/util/safe-fetch.js +72 -0
  134. package/lib/util/safe-fetch.js.map +1 -1
  135. package/package.json +9 -5
  136. package/src/alchemy.ts +3 -2
  137. package/src/aws/client-props.ts +156 -0
  138. package/src/aws/credentials.ts +165 -4
  139. package/src/aws/ec2/internet-gateway-attachment.ts +59 -4
  140. package/src/aws/ec2/internet-gateway.ts +70 -41
  141. package/src/aws/ec2/nat-gateway.ts +123 -33
  142. package/src/aws/ec2/route-table-association.ts +1 -1
  143. package/src/aws/ec2/route-table.ts +66 -2
  144. package/src/aws/ec2/route.ts +62 -2
  145. package/src/aws/ec2/security-group.ts +98 -4
  146. package/src/aws/ec2/subnet.ts +63 -4
  147. package/src/aws/ec2/utils.ts +73 -7
  148. package/src/aws/ec2/vpc.ts +52 -3
  149. package/src/aws/index.ts +2 -0
  150. package/src/cloudflare/api-response.ts +2 -2
  151. package/src/cloudflare/api.ts +23 -0
  152. package/src/cloudflare/assets.ts +3 -4
  153. package/src/cloudflare/bucket.ts +39 -29
  154. package/src/cloudflare/bundle/plugin-compat-warning.ts +7 -7
  155. package/src/cloudflare/cloudflare-env-proxy.ts +2 -2
  156. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  157. package/src/cloudflare/d1-migrations.ts +4 -5
  158. package/src/cloudflare/index.ts +2 -1
  159. package/src/cloudflare/kv-namespace.ts +26 -27
  160. package/src/cloudflare/miniflare/build-worker-options.ts +16 -3
  161. package/src/cloudflare/miniflare/miniflare-controller.ts +4 -0
  162. package/src/cloudflare/oauth.ts +3 -3
  163. package/src/cloudflare/quick-tunnel.ts +30 -0
  164. package/src/cloudflare/sveltekit/plugin.ts +5 -5
  165. package/src/cloudflare/website.ts +30 -111
  166. package/src/cloudflare/worker-bundle.ts +11 -9
  167. package/src/cloudflare/worker-tail.ts +6 -8
  168. package/src/resource.ts +19 -2
  169. package/src/scope.ts +111 -20
  170. package/src/state/cloudflare-state-store.ts +2 -1
  171. package/src/util/idempotent-spawn.ts +303 -0
  172. package/src/util/index.ts +1 -0
  173. package/src/util/safe-fetch.ts +114 -0
  174. package/templates/rwsdk/package.json +1 -1
@@ -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,
@@ -478,36 +478,40 @@ async function emptyBucket(
478
478
  bucketName: string,
479
479
  props: BucketProps,
480
480
  ) {
481
- let batch: Promise<unknown>[] = [];
482
- for await (const key of listObjects(api, bucketName, props)) {
483
- batch.push(
484
- extractCloudflareResult(
485
- `delete R2 object "${key}"`,
481
+ let cursor: string | undefined;
482
+ while (true) {
483
+ const result = await listObjects(api, bucketName, props, cursor);
484
+ if (result.keys.length) {
485
+ // Another undocumented API! But it lets us delete multiple objects at once instead of one by one.
486
+ await extractCloudflareResult(
487
+ `delete ${result.keys.length} objects from bucket "${bucketName}"`,
486
488
  api.delete(
487
- `/accounts/${api.accountId}/r2/buckets/${bucketName}/objects/${key}`,
488
- { headers: withJurisdiction(props) },
489
+ `/accounts/${api.accountId}/r2/buckets/${bucketName}/objects`,
490
+ {
491
+ headers: withJurisdiction(props),
492
+ method: "DELETE",
493
+ body: JSON.stringify(result.keys),
494
+ },
489
495
  ),
490
- ),
491
- );
492
- if (batch.length >= 10) {
493
- // this is an arbitary batch size, open to feedback
494
- await Promise.all(batch);
495
- batch = [];
496
+ );
497
+ if (result.cursor) {
498
+ cursor = result.cursor;
499
+ continue;
500
+ }
496
501
  }
502
+ break;
497
503
  }
498
- await Promise.all(batch);
499
504
  }
500
505
 
501
506
  /**
502
- * Returns an async iterable of all object keys in a bucket,
503
- * handling pagination automatically.
507
+ * Lists objects in a bucket.
504
508
  */
505
- export async function* listObjects(
509
+ export async function listObjects(
506
510
  api: CloudflareApi,
507
511
  bucketName: string,
508
512
  props: { jurisdiction?: string },
509
513
  cursor?: string,
510
- ): AsyncGenerator<string> {
514
+ ) {
511
515
  const params = new URLSearchParams({
512
516
  per_page: "1000",
513
517
  });
@@ -518,11 +522,6 @@ export async function* listObjects(
518
522
  `/accounts/${api.accountId}/r2/buckets/${bucketName}/objects?${params.toString()}`,
519
523
  { headers: withJurisdiction(props) },
520
524
  );
521
- if (!response.ok) {
522
- throw new Error(
523
- `Failed to list objects in bucket "${bucketName}": ${await extractCloudflareError(response)}`,
524
- );
525
- }
526
525
  const json: {
527
526
  result: { key: string }[];
528
527
  result_info?: {
@@ -530,13 +529,24 @@ export async function* listObjects(
530
529
  is_truncated: boolean;
531
530
  per_page: number;
532
531
  };
532
+ success: boolean;
533
+ errors: CloudflareApiErrorPayload[];
533
534
  } = await response.json();
534
- for (const object of json.result) {
535
- yield object.key;
536
- }
537
- if (json.result_info?.is_truncated && json.result_info.cursor) {
538
- yield* listObjects(api, bucketName, props, json.result_info.cursor);
535
+ if (!json.success) {
536
+ // 10006 indicates that the bucket does not exist, so there are no objects to list
537
+ if (json.errors.some((e) => e.code === 10006)) {
538
+ return { keys: [], cursor: undefined };
539
+ }
540
+ throw new CloudflareApiError(
541
+ `Failed to list objects in bucket "${bucketName}": ${json.errors.map((e) => `- [${e.code}] ${e.message}${e.documentation_url ? ` (${e.documentation_url})` : ""}`).join("\n")}`,
542
+ response,
543
+ json.errors,
544
+ );
539
545
  }
546
+ return {
547
+ keys: json.result.map((object) => object.key),
548
+ cursor: json.result_info?.cursor,
549
+ };
540
550
  }
541
551
 
542
552
  /**
@@ -1,7 +1,7 @@
1
1
  import type { Plugin, PluginBuild } from "esbuild";
2
- import kleur from "kleur";
3
2
  import module from "node:module";
4
3
  import path from "node:path";
4
+ import pc from "picocolors";
5
5
  import { logger } from "../../util/logger.ts";
6
6
 
7
7
  const NODE_REGEX = new RegExp(
@@ -54,13 +54,13 @@ export function esbuildPluginCompatWarning(mode: "als" | null): Plugin {
54
54
  if (packages.size > 0) {
55
55
  const formattedImports = Array.from(packages)
56
56
  .sort()
57
- .map((imp) => kleur.yellow(imp))
57
+ .map((imp) => pc.yellow(imp))
58
58
  .join(", ");
59
59
 
60
60
  logger.warn(
61
61
  [
62
- `Detected Node.js imports (${formattedImports}) but ${kleur.red("nodejs_compat")} compatibility flag is not set. `,
63
- `Add ${kleur.blue("nodejs_compat")} to your compatibility flags and ensure compatibilityDate >= 2024-09-23. Imported from:`,
62
+ `Detected Node.js imports (${formattedImports}) but ${pc.red("nodejs_compat")} compatibility flag is not set. `,
63
+ `Add ${pc.blue("nodejs_compat")} to your compatibility flags and ensure compatibilityDate >= 2024-09-23. Imported from:`,
64
64
  formatImporters(
65
65
  nodeImporters,
66
66
  build.initialOptions.absWorkingDir ?? process.cwd(),
@@ -72,8 +72,8 @@ export function esbuildPluginCompatWarning(mode: "als" | null): Plugin {
72
72
  if (alsImporters.size > 0 && !mode) {
73
73
  logger.warn(
74
74
  [
75
- `Detected import of ${kleur.yellow("node:async_hooks")} but ${kleur.red("nodejs_als")} compatibility flag is not set. `,
76
- `Add ${kleur.blue("nodejs_als")} or ${kleur.blue("nodejs_compat")} to your compatibility flags. Imported from:`,
75
+ `Detected import of ${pc.yellow("node:async_hooks")} but ${pc.red("nodejs_als")} compatibility flag is not set. `,
76
+ `Add ${pc.blue("nodejs_als")} or ${pc.blue("nodejs_compat")} to your compatibility flags. Imported from:`,
77
77
  formatImporters(
78
78
  alsImporters,
79
79
  build.initialOptions.absWorkingDir ?? process.cwd(),
@@ -89,6 +89,6 @@ export function esbuildPluginCompatWarning(mode: "als" | null): Plugin {
89
89
  function formatImporters(importers: Set<string>, cwd: string) {
90
90
  return Array.from(importers)
91
91
  .sort()
92
- .map((imp) => `- ${kleur.yellow(path.relative(cwd, imp))}`)
92
+ .map((imp) => `- ${pc.yellow(path.relative(cwd, imp))}`)
93
93
  .join("\n");
94
94
  }
@@ -41,10 +41,10 @@ export const getPlatformProxyOptions = (
41
41
  ...input,
42
42
  configPath: validateConfigPath(input.configPath ?? DEFAULT_CONFIG_PATH),
43
43
  persist,
44
- experimental: input.experimental ?? {
44
+ experimental: (input as any).experimental ?? {
45
45
  remoteBindings: true,
46
46
  },
47
- };
47
+ } as any;
48
48
  };
49
49
 
50
50
  const warned = new Set<string>();
@@ -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";
@@ -100,41 +100,40 @@ export function isKVNamespace(resource: Resource): resource is KVNamespace {
100
100
  /**
101
101
  * Output returned after KV Namespace creation/update
102
102
  */
103
- export interface KVNamespace
104
- extends Resource<"cloudflare::KVNamespace">,
105
- Omit<KVNamespaceProps, "delete" | "dev"> {
106
- type: "kv_namespace";
107
- /**
108
- * The ID of the namespace
109
- */
110
- namespaceId: string;
111
-
112
- /**
113
- * Time at which the namespace was created
114
- */
115
- createdAt: number;
103
+ export type KVNamespace = Resource<"cloudflare::KVNamespace"> &
104
+ Omit<KVNamespaceProps, "delete" | "dev"> & {
105
+ type: "kv_namespace";
106
+ /**
107
+ * The ID of the namespace
108
+ */
109
+ namespaceId: string;
116
110
 
117
- /**
118
- * Time at which the namespace was last modified
119
- */
120
- modifiedAt: number;
111
+ /**
112
+ * Time at which the namespace was created
113
+ */
114
+ createdAt: number;
121
115
 
122
- /**
123
- * Development mode properties
124
- * @internal
125
- */
126
- dev: {
127
116
  /**
128
- * The ID of the KV namespace in development mode
117
+ * Time at which the namespace was last modified
129
118
  */
130
- id: string;
119
+ modifiedAt: number;
131
120
 
132
121
  /**
133
- * Whether the KV namespace is running remotely
122
+ * Development mode properties
123
+ * @internal
134
124
  */
135
- remote: boolean;
125
+ dev: {
126
+ /**
127
+ * The ID of the KV namespace in development mode
128
+ */
129
+ id: string;
130
+
131
+ /**
132
+ * Whether the KV namespace is running remotely
133
+ */
134
+ remote: boolean;
135
+ };
136
136
  };
137
- }
138
137
 
139
138
  /**
140
139
  * A Cloudflare KV Namespace is a key-value store that can be used to store data for your application.
@@ -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);
@@ -1,4 +1,4 @@
1
- import kleur from "kleur";
1
+ import envPaths from "env-paths";
2
2
  import { err, ok, okAsync, ResultAsync } from "neverthrow";
3
3
  import assert from "node:assert";
4
4
  import crypto from "node:crypto";
@@ -6,7 +6,7 @@ import fs from "node:fs/promises";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
8
  import open from "open";
9
- import envPaths from "env-paths";
9
+ import pc from "picocolors";
10
10
  import { HTTPServer } from "../util/http.ts";
11
11
  import { memoize } from "../util/memoize.ts";
12
12
  import {
@@ -133,7 +133,7 @@ export const wranglerLogin = (
133
133
  [
134
134
  "Opening browser to authorize with Cloudflare...",
135
135
  "",
136
- kleur.gray(
136
+ pc.gray(
137
137
  "If you are not automatically redirected, please open the following URL in your browser:",
138
138
  ),
139
139
  challenge.url,
@@ -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,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, type Secret } from "../secret.ts";
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 runDevCommand(scope, {
308
- id,
309
- command: typeof dev === "string" ? dev : dev.command,
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
- ...env,
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
- }
@@ -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";
@@ -92,21 +93,22 @@ export namespace WorkerBundle {
92
93
  paths: string[],
93
94
  format: "esm" | "cjs",
94
95
  ): WorkerBundle.Module[] => {
95
- return paths.map((path) => {
96
- const ext = path.split(".").pop();
96
+ return paths.map((filePath) => {
97
+ const normalizedPath = path.normalize(filePath);
98
+ const ext = normalizedPath.split(".").pop();
97
99
  switch (ext) {
98
100
  case "js":
99
- return { type: format, path };
101
+ return { type: format, path: normalizedPath };
100
102
  case "mjs":
101
- return { type: "esm", path };
103
+ return { type: "esm", path: normalizedPath };
102
104
  case "cjs":
103
- return { type: "cjs", path };
105
+ return { type: "cjs", path: normalizedPath };
104
106
  case "wasm":
105
- return { type: "wasm", path };
107
+ return { type: "wasm", path: normalizedPath };
106
108
  case "map":
107
- return { type: "sourcemap", path };
109
+ return { type: "sourcemap", path: normalizedPath };
108
110
  default:
109
- return { type: "text", path };
111
+ return { type: "text", path: normalizedPath };
110
112
  }
111
113
  });
112
114
  };
@@ -222,7 +224,7 @@ export namespace WorkerBundleSource {
222
224
  const fileNames = new Set<string>();
223
225
  await Promise.all(
224
226
  this.globs.map(async (glob) => {
225
- for await (const file of fs.glob(glob, { cwd: this.root })) {
227
+ for await (const file of globIterate(glob, { cwd: this.root })) {
226
228
  fileNames.add(file);
227
229
  }
228
230
  }),
@@ -1,4 +1,4 @@
1
- import kleur from "kleur";
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 = kleur.blue(`[${this.id}]`);
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} ${kleur.gray(event.request.method)} ${url.pathname} ${kleur.dim(">")} ${status >= 200 && status < 300 ? kleur.green(status) : kleur.red(status)} ${kleur.gray(`(cpu: ${Math.round(data.cpuTime)}ms, wall: ${Math.round(data.wallTime)}ms)`)}`,
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} ${kleur.red(exception.name)}`;
120
+ const start = `${prefix} ${pc.red(exception.name)}`;
123
121
  logger.log(
124
- `${start} ${exception.message}\n${kleur.gray(exception.stack)}`,
122
+ `${start} ${exception.message}\n${pc.gray(exception.stack)}`,
125
123
  );
126
124
  }
127
125
  });