alchemy 0.46.1 → 0.48.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 (78) hide show
  1. package/bin/alchemy.mjs +560 -98
  2. package/lib/alchemy.d.ts +9 -3
  3. package/lib/alchemy.d.ts.map +1 -1
  4. package/lib/alchemy.js +19 -39
  5. package/lib/alchemy.js.map +1 -1
  6. package/lib/build-date.d.ts +1 -1
  7. package/lib/build-date.js +1 -1
  8. package/lib/cloudflare/astro.d.ts.map +1 -1
  9. package/lib/cloudflare/astro.js +3 -0
  10. package/lib/cloudflare/astro.js.map +1 -1
  11. package/lib/cloudflare/container.js +1 -1
  12. package/lib/cloudflare/container.js.map +1 -1
  13. package/lib/cloudflare/nuxt.d.ts.map +1 -1
  14. package/lib/cloudflare/nuxt.js +3 -0
  15. package/lib/cloudflare/nuxt.js.map +1 -1
  16. package/lib/cloudflare/oauth.d.ts.map +1 -1
  17. package/lib/cloudflare/oauth.js +4 -3
  18. package/lib/cloudflare/oauth.js.map +1 -1
  19. package/lib/cloudflare/pipeline.d.ts.map +1 -1
  20. package/lib/cloudflare/pipeline.js +1 -3
  21. package/lib/cloudflare/pipeline.js.map +1 -1
  22. package/lib/cloudflare/react-router.d.ts.map +1 -1
  23. package/lib/cloudflare/react-router.js +3 -0
  24. package/lib/cloudflare/react-router.js.map +1 -1
  25. package/lib/cloudflare/vite.d.ts.map +1 -1
  26. package/lib/cloudflare/vite.js +0 -1
  27. package/lib/cloudflare/vite.js.map +1 -1
  28. package/lib/cloudflare/website.d.ts +0 -1
  29. package/lib/cloudflare/website.d.ts.map +1 -1
  30. package/lib/cloudflare/website.js +13 -7
  31. package/lib/cloudflare/website.js.map +1 -1
  32. package/lib/cloudflare/worker-metadata.d.ts +6 -0
  33. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  34. package/lib/cloudflare/worker-metadata.js +1 -0
  35. package/lib/cloudflare/worker-metadata.js.map +1 -1
  36. package/lib/cloudflare/worker.d.ts +33 -3
  37. package/lib/cloudflare/worker.d.ts.map +1 -1
  38. package/lib/cloudflare/worker.js +91 -70
  39. package/lib/cloudflare/worker.js.map +1 -1
  40. package/lib/cloudflare/zone.d.ts +1 -1
  41. package/lib/cloudflare/zone.d.ts.map +1 -1
  42. package/lib/cloudflare/zone.js +38 -56
  43. package/lib/cloudflare/zone.js.map +1 -1
  44. package/lib/scope.d.ts +14 -2
  45. package/lib/scope.d.ts.map +1 -1
  46. package/lib/scope.js +8 -3
  47. package/lib/scope.js.map +1 -1
  48. package/lib/util/safe-fetch.d.ts.map +1 -1
  49. package/lib/util/safe-fetch.js +1 -0
  50. package/lib/util/safe-fetch.js.map +1 -1
  51. package/lib/util/telemetry/constants.js +2 -2
  52. package/lib/util/telemetry/constants.js.map +1 -1
  53. package/package.json +4 -2
  54. package/src/alchemy.ts +28 -49
  55. package/src/build-date.ts +1 -1
  56. package/src/cloudflare/astro.ts +3 -0
  57. package/src/cloudflare/container.ts +1 -1
  58. package/src/cloudflare/nuxt.ts +3 -0
  59. package/src/cloudflare/oauth.ts +5 -4
  60. package/src/cloudflare/pipeline.ts +1 -3
  61. package/src/cloudflare/react-router.ts +3 -0
  62. package/src/cloudflare/vite.ts +0 -1
  63. package/src/cloudflare/website.ts +14 -8
  64. package/src/cloudflare/worker-metadata.ts +7 -0
  65. package/src/cloudflare/worker.ts +138 -102
  66. package/src/cloudflare/zone.ts +50 -70
  67. package/src/scope.ts +21 -4
  68. package/src/util/safe-fetch.ts +1 -0
  69. package/src/util/telemetry/constants.ts +2 -2
  70. package/templates/react-router/package.json +1 -1
  71. package/templates/react-router/tsconfig.cloudflare.json +5 -1
  72. package/templates/react-router/tsconfig.json +0 -3
  73. package/templates/react-router/tsconfig.node.json +0 -4
  74. package/lib/util/xdg-paths.d.ts +0 -9
  75. package/lib/util/xdg-paths.d.ts.map +0 -1
  76. package/lib/util/xdg-paths.js +0 -41
  77. package/lib/util/xdg-paths.js.map +0 -1
  78. package/src/util/xdg-paths.ts +0 -48
package/src/alchemy.ts CHANGED
@@ -20,50 +20,6 @@ import type { LoggerApi } from "./util/cli.ts";
20
20
  import { logger } from "./util/logger.ts";
21
21
  import { TelemetryClient } from "./util/telemetry/client.ts";
22
22
 
23
- /**
24
- * Parses CLI arguments to extract alchemy options
25
- */
26
- function parseCliArgs(): Partial<AlchemyOptions> {
27
- const args = process.argv.slice(2);
28
- const options: Partial<AlchemyOptions> = {};
29
-
30
- // Parse phase from CLI arguments
31
- if (args.includes("--destroy")) {
32
- options.phase = "destroy";
33
- } else if (args.includes("--read")) {
34
- options.phase = "read";
35
- }
36
-
37
- if (args.includes("--local") || args.includes("--dev")) {
38
- options.dev = "prefer-local";
39
- } else if (
40
- args.includes("--remote") ||
41
- args.includes("--watch") ||
42
- process.execArgv.includes("--watch")
43
- ) {
44
- options.dev = "prefer-remote";
45
- }
46
-
47
- // Parse quiet flag
48
- if (args.includes("--quiet")) {
49
- options.quiet = true;
50
- }
51
-
52
- // Parse stage argument (--stage my-stage)
53
- const stageIndex = args.indexOf("--stage");
54
- if (stageIndex !== -1 && stageIndex + 1 < args.length) {
55
- options.stage = args[stageIndex + 1];
56
- }
57
- options.stage ??= process.env.STAGE;
58
-
59
- // Get password from environment variables
60
- if (process.env.ALCHEMY_PASSWORD) {
61
- options.password = process.env.ALCHEMY_PASSWORD;
62
- }
63
-
64
- return options;
65
- }
66
-
67
23
  /**
68
24
  * Type alias for semantic highlighting of `alchemy` as a type keyword
69
25
  */
@@ -175,8 +131,25 @@ async function _alchemy(
175
131
  if (typeof args[0] === "string") {
176
132
  const [appName, options] = args as [string, AlchemyOptions?];
177
133
 
178
- // Parse CLI arguments and merge with provided options (explicit options take precedence)
179
- const cliOptions = parseCliArgs();
134
+ const cliArgs = process.argv.slice(2);
135
+ const cliOptions = {
136
+ phase: cliArgs.includes("--destroy")
137
+ ? "destroy"
138
+ : cliArgs.includes("--read")
139
+ ? "read"
140
+ : "up",
141
+ local: cliArgs.includes("--local") || cliArgs.includes("--dev"),
142
+ watch: cliArgs.includes("--watch"),
143
+ quiet: cliArgs.includes("--quiet"),
144
+ // Parse stage argument (--stage my-stage) functionally and inline as a property declaration
145
+ stage: (function parseStage() {
146
+ const i = cliArgs.indexOf("--stage");
147
+ return i !== -1 && i + 1 < cliArgs.length
148
+ ? cliArgs[i + 1]
149
+ : process.env.STAGE;
150
+ })(),
151
+ password: process.env.ALCHEMY_PASSWORD,
152
+ } satisfies Partial<AlchemyOptions>;
180
153
  const mergedOptions = {
181
154
  ...cliOptions,
182
155
  ...options,
@@ -348,11 +321,17 @@ export interface AlchemyOptions {
348
321
  */
349
322
  phase?: Phase;
350
323
  /**
351
- * Determines how Alchemy will run in development mode.
324
+ * Determines if resources should be simulated locally (where possible)
325
+ *
326
+ * @default - `true` if ran with `alchemy dev` or `bun ./alchemy.run.ts --dev`
327
+ */
328
+ local?: boolean;
329
+ /**
330
+ * Determines if local changes to resources should be reactively pushed to the local or remote environment.
352
331
  *
353
- * @default - `"prefer-local"` if `--dev` or `--local` is passed as a CLI argument, `"prefer-remote"` if `--remote` or `--watch` is passed as a CLI argument, `undefined` otherwise
332
+ * @default - `true` if ran with `alchemy dev`, `alchemy watch`, `bun --watch ./alchemy.run.ts`
354
333
  */
355
- dev?: "prefer-local" | "prefer-remote";
334
+ watch?: boolean;
356
335
  /**
357
336
  * Name to scope the resource state under (e.g. `.alchemy/{stage}/..`).
358
337
  *
package/src/build-date.ts CHANGED
@@ -5,4 +5,4 @@
5
5
  * The build date used as the default worker compatibility date.
6
6
  * This is set to the date when the package was built.
7
7
  */
8
- export const BUILD_DATE = "2025-07-15";
8
+ export const BUILD_DATE = "2025-07-16";
@@ -85,5 +85,8 @@ export async function Astro<B extends Bindings>(
85
85
  run_worker_first: false,
86
86
  },
87
87
  wrangler,
88
+ dev: props.dev ?? {
89
+ command: "astro dev",
90
+ },
88
91
  });
89
92
  }
@@ -191,7 +191,7 @@ export async function Container<T>(
191
191
  adopt: props.adopt,
192
192
  };
193
193
 
194
- const isDev = Scope.current.dev && !props.dev?.remote;
194
+ const isDev = Scope.current.local && !props.dev?.remote;
195
195
  if (isDev) {
196
196
  const image = await Image(id, {
197
197
  name: `cloudflare-dev/${name}`, // prefix used by Miniflare
@@ -64,5 +64,8 @@ export async function Nuxt<B extends Bindings>(
64
64
  compatibilityFlags: ["nodejs_compat", ...(props?.compatibilityFlags ?? [])],
65
65
  // Enable wrangler by default, common for Nuxt/Cloudflare deployments
66
66
  wrangler: props?.wrangler ?? true,
67
+ dev: props?.dev ?? {
68
+ command: "nuxt dev",
69
+ },
67
70
  });
68
71
  }
@@ -6,6 +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 xdgAppPaths from "xdg-app-paths";
9
10
  import { HTTPServer } from "../util/http-server.ts";
10
11
  import { memoize } from "../util/memoize.ts";
11
12
  import {
@@ -15,7 +16,6 @@ import {
15
16
  HTTPError,
16
17
  singleFlight,
17
18
  } from "../util/neverthrow.ts";
18
- import { createXdgAppPaths } from "../util/xdg-paths.ts";
19
19
 
20
20
  const CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
21
21
  const REDIRECT_URI = "http://localhost:8976/oauth/callback";
@@ -70,7 +70,7 @@ export const getRefreshedWranglerConfig = singleFlight(() =>
70
70
  );
71
71
 
72
72
  const getWranglerConfigPath = memoize(async () => {
73
- const xdgConfigDir = createXdgAppPaths(".wrangler").config();
73
+ const xdgConfigDir = xdgAppPaths(".wrangler").config();
74
74
  const legacyConfigDir = path.join(os.homedir(), ".wrangler");
75
75
  const configDir = (await fs
76
76
  .stat(legacyConfigDir)
@@ -92,12 +92,12 @@ const readWranglerConfig = () => {
92
92
  const config = toml.parse(text.replace(/\r\n/g, "\n"));
93
93
  assert(typeof config.oauth_token === "string");
94
94
  assert(typeof config.refresh_token === "string");
95
- assert(config.expiration_time instanceof Date);
95
+ assert(!!config.expiration_time);
96
96
  assert(Array.isArray(config.scopes));
97
97
  return {
98
98
  oauth_token: config.oauth_token,
99
99
  refresh_token: config.refresh_token,
100
- expiration_time: new Date(config.expiration_time),
100
+ expiration_time: new Date(config.expiration_time as any),
101
101
  scopes: config.scopes as string[],
102
102
  };
103
103
  }),
@@ -110,6 +110,7 @@ const writeWranglerConfig = (config: WranglerConfig) => {
110
110
  return ResultAsync.fromSafePromise(getWranglerConfigPath()).map(
111
111
  async (configPath) => {
112
112
  const toml = await import("@iarna/toml");
113
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
113
114
  await fs.writeFile(
114
115
  configPath,
115
116
  toml.stringify({
@@ -368,11 +368,9 @@ const PipelineResource = Resource("cloudflare::Pipeline", async function <
368
368
  let pipelineData: CloudflarePipelineResponse;
369
369
 
370
370
  if (this.phase === "create") {
371
- console.log(props);
372
371
  // Check if we should adopt an existing pipeline
373
372
  try {
374
373
  // Try to create pipeline first
375
- console.log("Creating new Cloudflare Pipeline:", pipelineName);
376
374
  pipelineData = await createPipeline(api, pipelineName, props);
377
375
  } catch (error) {
378
376
  // If creation fails with 409 (conflict), adopt existing pipeline
@@ -383,7 +381,7 @@ const PipelineResource = Resource("cloudflare::Pipeline", async function <
383
381
  error.message.includes("Pipeline with this name already exists")))
384
382
  ) {
385
383
  if (props.adopt) {
386
- console.log(
384
+ console.warn(
387
385
  "Pipeline already exists, adopting existing Cloudflare Pipeline:",
388
386
  pipelineName,
389
387
  );
@@ -41,5 +41,8 @@ export async function ReactRouter<B extends Bindings>(
41
41
  dist: props.assets.dist ?? defaultAssets,
42
42
  }
43
43
  : (props.assets ?? defaultAssets),
44
+ dev: props.dev ?? {
45
+ command: "react-router dev",
46
+ },
44
47
  });
45
48
  }
@@ -38,7 +38,6 @@ export async function Vite<B extends Bindings>(
38
38
  : (props.assets ?? defaultAssets),
39
39
  dev: props.dev ?? {
40
40
  command: devCommand,
41
- url: "http://localhost:5173",
42
41
  },
43
42
  });
44
43
  }
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { alchemy } from "../alchemy.ts";
3
3
  import { Exec } from "../os/exec.ts";
4
4
  import { Scope } from "../scope.ts";
5
+ import { detectPackageManager } from "../util/detect-package-manager.ts";
5
6
  import { Assets } from "./assets.ts";
6
7
  import type { Bindings } from "./bindings.ts";
7
8
  import {
@@ -100,7 +101,6 @@ export interface WebsiteProps<B extends Bindings>
100
101
  */
101
102
  dev?: {
102
103
  command: string;
103
- url: string;
104
104
  };
105
105
 
106
106
  /**
@@ -123,6 +123,15 @@ export interface WebsiteProps<B extends Bindings>
123
123
  };
124
124
  }
125
125
 
126
+ const packageManager = await detectPackageManager();
127
+ const devCommand = {
128
+ npm: "npx vite dev",
129
+ bun: "bun vite dev",
130
+ pnpm: "pnpm vite dev",
131
+ yarn: "yarn vite dev",
132
+ deno: "deno task dev",
133
+ }[packageManager];
134
+
126
135
  export type Website<B extends Bindings> = B extends { ASSETS: any }
127
136
  ? never
128
137
  : Worker<B & { ASSETS: Assets }>;
@@ -189,12 +198,9 @@ export default {
189
198
  };`,
190
199
  url: props.url ?? true,
191
200
  adopt: props.adopt ?? true,
192
- dev: props.dev
193
- ? {
194
- command: props.dev.command,
195
- url: props.dev.url,
196
- }
197
- : undefined,
201
+ dev: {
202
+ command: props.dev?.command ?? devCommand,
203
+ },
198
204
  } as WorkerProps<any> & { name: string };
199
205
 
200
206
  if (wrangler) {
@@ -216,7 +222,7 @@ export default {
216
222
  });
217
223
  }
218
224
 
219
- const isDev = scope.dev && !!props.dev;
225
+ const isDev = scope.local;
220
226
 
221
227
  if (props.command && !isDev) {
222
228
  await Exec("build", {
@@ -197,6 +197,9 @@ export interface WorkerMetadata {
197
197
  suspended: boolean;
198
198
  }[];
199
199
  containers?: { class_name: string }[];
200
+ placement?: {
201
+ mode: "smart";
202
+ };
200
203
  }
201
204
 
202
205
  export async function prepareWorkerMetadata(
@@ -213,6 +216,9 @@ export async function prepareWorkerMetadata(
213
216
  };
214
217
  tags?: string[];
215
218
  unstable_cacheWorkerSettings?: boolean;
219
+ placement?: {
220
+ mode: "smart";
221
+ };
216
222
  },
217
223
  ): Promise<WorkerMetadata> {
218
224
  const oldSettings = await (props.unstable_cacheWorkerSettings
@@ -336,6 +342,7 @@ export async function prepareWorkerMetadata(
336
342
  transferred_classes: [],
337
343
  new_sqlite_classes: [],
338
344
  },
345
+ placement: props.placement,
339
346
  };
340
347
 
341
348
  const assetUploadResult = props.assetUploadResult;
@@ -1,13 +1,7 @@
1
1
  import type esbuild from "esbuild";
2
2
  import kleur from "kleur";
3
3
  import { spawn } from "node:child_process";
4
- import {
5
- existsSync,
6
- mkdirSync,
7
- readFileSync,
8
- unlinkSync,
9
- writeFileSync,
10
- } from "node:fs";
4
+ import fs from "node:fs/promises";
11
5
  import { isDeepStrictEqual } from "node:util";
12
6
  import path from "pathe";
13
7
  import { BUILD_DATE } from "../build-date.ts";
@@ -18,7 +12,7 @@ import { getBindKey, tryGetBinding } from "../runtime/bind.ts";
18
12
  import { isRuntime } from "../runtime/global.ts";
19
13
  import { bootstrapPlugin } from "../runtime/plugin.ts";
20
14
  import { Scope } from "../scope.ts";
21
- import { Secret, secret } from "../secret.ts";
15
+ import { Secret, isSecret, secret } from "../secret.ts";
22
16
  import { serializeScope } from "../serde.ts";
23
17
  import type { type } from "../type.ts";
24
18
  import { DeferredPromise } from "../util/deferred-promise.ts";
@@ -92,6 +86,7 @@ import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts";
92
86
  // Previous versions of `Worker` used the `Bundle` resource.
93
87
  // This import is here to avoid errors when destroying the `Bundle` resource.
94
88
  import "../esbuild/bundle.ts";
89
+ import { exists } from "../util/exists.ts";
95
90
 
96
91
  /**
97
92
  * Configuration options for static assets
@@ -181,6 +176,8 @@ export interface BaseWorkerProps<
181
176
  * Environment variables to attach to the worker
182
177
  *
183
178
  * These will be converted to plain_text bindings
179
+ *
180
+ * @deprecated - use `bindings` instead
184
181
  */
185
182
  env?: {
186
183
  [key: string]: string;
@@ -351,7 +348,6 @@ export interface BaseWorkerProps<
351
348
  * the worker will be emulated locally and available at a randomly selected port.
352
349
  */
353
350
  dev?:
354
- | boolean
355
351
  | {
356
352
  /**
357
353
  * Port to use for local development
@@ -363,12 +359,35 @@ export interface BaseWorkerProps<
363
359
  * @default false
364
360
  */
365
361
  remote?: boolean;
362
+ /** @internal */
363
+ command?: undefined;
366
364
  }
367
365
  | {
368
366
  command: string;
369
- url: string;
370
367
  cwd?: string;
368
+ /** @internal */
369
+ port?: undefined;
370
+ /** @internal */
371
+ remote?: undefined;
371
372
  };
373
+
374
+ /**
375
+ * Smart placement configuration for the worker.
376
+ *
377
+ * Controls how Cloudflare places the worker across its network for optimal performance.
378
+ *
379
+ * When omitted, smart placement is disabled (default behavior).
380
+ */
381
+ placement?: {
382
+ /**
383
+ * The placement mode for the worker.
384
+ *
385
+ * - "smart": Automatically optimize placement based on performance metrics
386
+ *
387
+ * @default undefined (smart placement disabled)
388
+ */
389
+ mode: "smart";
390
+ };
372
391
  }
373
392
 
374
393
  export interface InlineWorkerProps<
@@ -563,6 +582,13 @@ export type Worker<
563
582
  * Version label for this worker deployment
564
583
  */
565
584
  version?: string;
585
+
586
+ /**
587
+ * Smart placement configuration for the worker
588
+ */
589
+ placement?: {
590
+ mode: "smart";
591
+ };
566
592
  };
567
593
 
568
594
  /**
@@ -1020,8 +1046,6 @@ export const _Worker = Resource(
1020
1046
  ? props.namespace
1021
1047
  : props.namespace?.namespaceName;
1022
1048
 
1023
- const dev = normalizeDev(this, props.dev);
1024
-
1025
1049
  const [bundle, error] = wrap(() =>
1026
1050
  normalizeWorkerBundle({
1027
1051
  entrypoint: props.entrypoint,
@@ -1040,34 +1064,45 @@ export const _Worker = Resource(
1040
1064
  }),
1041
1065
  );
1042
1066
 
1043
- if (dev.local) {
1067
+ // run locally if
1068
+ const local = this.scope.local && !props.dev?.remote;
1069
+ const watch = this.scope.watch;
1070
+
1071
+ if (local) {
1044
1072
  let url: string | undefined;
1045
1073
  if (error) {
1046
1074
  throw error;
1047
1075
  }
1048
1076
 
1049
- switch (dev.type) {
1050
- case "command":
1051
- createDevCommand({
1052
- id,
1053
- command: dev.command,
1054
- cwd: dev.cwd ?? props.cwd ?? process.cwd(),
1055
- env: props.env ?? {},
1056
- });
1057
- url = dev.url;
1058
- break;
1059
- case "miniflare": {
1060
- url = await createMiniflare({
1061
- id,
1062
- workerName,
1063
- compatibilityDate,
1064
- compatibilityFlags,
1065
- bindings: props.bindings,
1066
- bundle,
1067
- port: dev.port,
1068
- });
1069
- break;
1070
- }
1077
+ if (props.dev?.command) {
1078
+ const { url: commandUrl } = await createDevCommand({
1079
+ id,
1080
+ command: props.dev.command,
1081
+ cwd: props.dev.cwd || props.cwd || process.cwd(),
1082
+ env: {
1083
+ ...props.env,
1084
+ ...Object.fromEntries(
1085
+ Object.entries(props.bindings ?? {}).flatMap(([key, value]) =>
1086
+ typeof value === "string"
1087
+ ? [[key, value]]
1088
+ : isSecret(value)
1089
+ ? [[key, value.unencrypted]]
1090
+ : [],
1091
+ ),
1092
+ ),
1093
+ },
1094
+ });
1095
+ url = commandUrl;
1096
+ } else {
1097
+ url = await createMiniflare({
1098
+ id,
1099
+ workerName,
1100
+ compatibilityDate,
1101
+ compatibilityFlags,
1102
+ bindings: props.bindings,
1103
+ bundle,
1104
+ port: props.dev?.port,
1105
+ });
1071
1106
  }
1072
1107
 
1073
1108
  return this({
@@ -1091,6 +1126,8 @@ export const _Worker = Resource(
1091
1126
  assets: props.assets,
1092
1127
  // Include cron triggers in the output
1093
1128
  crons: props.crons,
1129
+ // Include placement configuration in the output
1130
+ placement: props.placement,
1094
1131
  // phantom property
1095
1132
  Env: undefined!,
1096
1133
  } as unknown as Worker<B>);
@@ -1189,7 +1226,7 @@ export const _Worker = Resource(
1189
1226
  }
1190
1227
 
1191
1228
  let putWorkerResult: PutWorkerResult;
1192
- if (dev.type === "remote") {
1229
+ if (watch) {
1193
1230
  // todo(john): clean this up and add log tail
1194
1231
  const controller = new AbortController();
1195
1232
  cleanups.push(() => controller.abort());
@@ -1383,6 +1420,8 @@ export const _Worker = Resource(
1383
1420
  namespace: props.namespace,
1384
1421
  // Include version information in the output
1385
1422
  version: props.version,
1423
+ // Include placement configuration in the output
1424
+ placement: props.placement,
1386
1425
  // phantom property
1387
1426
  Env: undefined!,
1388
1427
  } as unknown as Worker<B>);
@@ -1405,57 +1444,6 @@ process.on("SIGINT", async () => {
1405
1444
  process.exit(0);
1406
1445
  });
1407
1446
 
1408
- type Dev =
1409
- | {
1410
- type: "none";
1411
- local: false;
1412
- }
1413
- | {
1414
- type: "miniflare";
1415
- port?: number;
1416
- local: true;
1417
- }
1418
- | {
1419
- type: "command";
1420
- command: string;
1421
- url: string;
1422
- cwd?: string;
1423
- local: true;
1424
- }
1425
- | {
1426
- type: "remote";
1427
- local: false;
1428
- };
1429
-
1430
- const normalizeDev = (ctx: Context<any>, dev: WorkerProps["dev"]): Dev => {
1431
- if (!ctx.scope.dev || ctx.phase === "delete" || dev === false) {
1432
- return {
1433
- type: "none",
1434
- local: false,
1435
- };
1436
- }
1437
- const devObj = dev === true ? {} : (dev ?? {});
1438
- if ("command" in devObj) {
1439
- // Commands are always local
1440
- return {
1441
- type: "command",
1442
- ...devObj,
1443
- local: true,
1444
- };
1445
- }
1446
- if (devObj.remote === false || ctx.scope.dev === "prefer-local") {
1447
- return {
1448
- type: "miniflare",
1449
- port: devObj.port,
1450
- local: true,
1451
- };
1452
- }
1453
- return {
1454
- type: "remote",
1455
- local: false,
1456
- };
1457
- };
1458
-
1459
1447
  const assertUnique = <T, Key extends keyof T>(
1460
1448
  inputs: T[],
1461
1449
  key: Key,
@@ -1827,15 +1815,15 @@ async function createMiniflare(props: {
1827
1815
  return await startPromise.value;
1828
1816
  }
1829
1817
 
1830
- function createDevCommand(props: {
1818
+ async function createDevCommand(props: {
1831
1819
  id: string;
1832
1820
  command: string;
1833
1821
  cwd: string;
1834
1822
  env: Record<string, string>;
1835
- }) {
1823
+ }): Promise<{ url: string }> {
1836
1824
  const persistFile = path.join(process.cwd(), ".alchemy", `${props.id}.pid`);
1837
- if (existsSync(persistFile)) {
1838
- const pid = Number.parseInt(readFileSync(persistFile, "utf8"));
1825
+ if (await exists(persistFile)) {
1826
+ const pid = Number.parseInt(await fs.readFile(persistFile, "utf8"));
1839
1827
  try {
1840
1828
  // Actually kill the process if it's alive
1841
1829
  process.kill(pid, "SIGTERM");
@@ -1843,12 +1831,20 @@ function createDevCommand(props: {
1843
1831
  // ignore
1844
1832
  }
1845
1833
  try {
1846
- unlinkSync(persistFile);
1834
+ await fs.unlink(persistFile);
1847
1835
  } catch {
1848
1836
  // ignore
1849
1837
  }
1850
1838
  }
1851
1839
  const command = props.command.split(" ");
1840
+ console.log({
1841
+ command,
1842
+ args: command.slice(1),
1843
+ cwd: props.cwd,
1844
+ env: {
1845
+ ...props.env,
1846
+ },
1847
+ });
1852
1848
  const proc = spawn(command[0], command.slice(1), {
1853
1849
  cwd: props.cwd,
1854
1850
  env: {
@@ -1859,21 +1855,61 @@ function createDevCommand(props: {
1859
1855
  ".alchemy",
1860
1856
  "miniflare",
1861
1857
  ),
1858
+ // Force colors in the child process since we're piping output
1859
+ // FORCE_COLOR: "1",
1862
1860
  },
1863
- stdio: ["inherit", "inherit", "inherit"],
1861
+ stdio: ["inherit", "pipe", "pipe"],
1864
1862
  });
1865
- cleanups.push(() => {
1866
- try {
1867
- unlinkSync(persistFile);
1868
- } catch {
1869
- // ignore
1870
- }
1871
- proc.kill();
1863
+ const { url } = await new Promise<{ url: string }>((resolve, reject) => {
1864
+ let urlFound = false;
1865
+ let stdout = "";
1866
+ let stderr = "";
1867
+ const urlRegex =
1868
+ /http:\/\/(?:(?:localhost|0\.0\.0\.0|127\.0\.0\.1)|(?:\d{1,3}\.){3}\d{1,3}):\d+(?:\/)?/;
1869
+
1870
+ const parseOutput = (data: string) => {
1871
+ if (!urlFound) {
1872
+ const match = data.match(urlRegex);
1873
+ if (match) {
1874
+ urlFound = true;
1875
+ resolve({ url: match[0] });
1876
+ }
1877
+ }
1878
+ };
1879
+
1880
+ // Handle stdout - parse for URL and write through with colors preserved
1881
+ proc.stdout?.on("data", (data) => {
1882
+ parseOutput(
1883
+ (stdout += data.toString().replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")),
1884
+ );
1885
+ process.stdout.write(data);
1886
+ });
1887
+
1888
+ proc.stderr?.on("data", (data) => {
1889
+ parseOutput(
1890
+ (stderr += data.toString().replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")),
1891
+ );
1892
+ process.stderr.write(data);
1893
+ });
1894
+
1895
+ proc.on("error", (error) => {
1896
+ reject(error);
1897
+ });
1898
+
1899
+ cleanups.push(async () => {
1900
+ try {
1901
+ await fs.unlink(persistFile);
1902
+ } catch {
1903
+ // ignore
1904
+ }
1905
+ proc.kill();
1906
+ });
1872
1907
  });
1873
1908
  if (proc.pid) {
1874
- mkdirSync(path.dirname(persistFile), { recursive: true });
1875
- writeFileSync(persistFile, proc.pid.toString());
1909
+ await fs.mkdir(path.dirname(persistFile), { recursive: true });
1910
+ await fs.writeFile(persistFile, proc.pid.toString());
1876
1911
  }
1912
+ return { url };
1877
1913
  }
1878
1914
 
1879
1915
  type PutWorkerOptions = Omit<WorkerProps, "entrypoint"> & {