alchemy 0.47.0 → 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 (52) hide show
  1. package/lib/alchemy.d.ts +9 -3
  2. package/lib/alchemy.d.ts.map +1 -1
  3. package/lib/alchemy.js +19 -39
  4. package/lib/alchemy.js.map +1 -1
  5. package/lib/cloudflare/astro.d.ts.map +1 -1
  6. package/lib/cloudflare/astro.js +3 -0
  7. package/lib/cloudflare/astro.js.map +1 -1
  8. package/lib/cloudflare/container.js +1 -1
  9. package/lib/cloudflare/container.js.map +1 -1
  10. package/lib/cloudflare/nuxt.d.ts.map +1 -1
  11. package/lib/cloudflare/nuxt.js +3 -0
  12. package/lib/cloudflare/nuxt.js.map +1 -1
  13. package/lib/cloudflare/pipeline.d.ts.map +1 -1
  14. package/lib/cloudflare/pipeline.js +1 -3
  15. package/lib/cloudflare/pipeline.js.map +1 -1
  16. package/lib/cloudflare/react-router.d.ts.map +1 -1
  17. package/lib/cloudflare/react-router.js +3 -0
  18. package/lib/cloudflare/react-router.js.map +1 -1
  19. package/lib/cloudflare/vite.d.ts.map +1 -1
  20. package/lib/cloudflare/vite.js +0 -1
  21. package/lib/cloudflare/vite.js.map +1 -1
  22. package/lib/cloudflare/website.d.ts +0 -1
  23. package/lib/cloudflare/website.d.ts.map +1 -1
  24. package/lib/cloudflare/website.js +13 -7
  25. package/lib/cloudflare/website.js.map +1 -1
  26. package/lib/cloudflare/worker.d.ts +7 -2
  27. package/lib/cloudflare/worker.d.ts.map +1 -1
  28. package/lib/cloudflare/worker.js +87 -70
  29. package/lib/cloudflare/worker.js.map +1 -1
  30. package/lib/scope.d.ts +14 -2
  31. package/lib/scope.d.ts.map +1 -1
  32. package/lib/scope.js +8 -3
  33. package/lib/scope.js.map +1 -1
  34. package/lib/util/safe-fetch.d.ts.map +1 -1
  35. package/lib/util/safe-fetch.js +1 -0
  36. package/lib/util/safe-fetch.js.map +1 -1
  37. package/package.json +3 -2
  38. package/src/alchemy.ts +28 -49
  39. package/src/cloudflare/astro.ts +3 -0
  40. package/src/cloudflare/container.ts +1 -1
  41. package/src/cloudflare/nuxt.ts +3 -0
  42. package/src/cloudflare/pipeline.ts +1 -3
  43. package/src/cloudflare/react-router.ts +3 -0
  44. package/src/cloudflare/vite.ts +0 -1
  45. package/src/cloudflare/website.ts +14 -8
  46. package/src/cloudflare/worker.ts +107 -102
  47. package/src/scope.ts +21 -4
  48. package/src/util/safe-fetch.ts +1 -0
  49. package/templates/react-router/package.json +1 -1
  50. package/templates/react-router/tsconfig.cloudflare.json +5 -1
  51. package/templates/react-router/tsconfig.json +0 -3
  52. package/templates/react-router/tsconfig.node.json +0 -4
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
  *
@@ -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
  }
@@ -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", {
@@ -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
@@ -353,7 +348,6 @@ export interface BaseWorkerProps<
353
348
  * the worker will be emulated locally and available at a randomly selected port.
354
349
  */
355
350
  dev?:
356
- | boolean
357
351
  | {
358
352
  /**
359
353
  * Port to use for local development
@@ -365,11 +359,16 @@ export interface BaseWorkerProps<
365
359
  * @default false
366
360
  */
367
361
  remote?: boolean;
362
+ /** @internal */
363
+ command?: undefined;
368
364
  }
369
365
  | {
370
366
  command: string;
371
- url: string;
372
367
  cwd?: string;
368
+ /** @internal */
369
+ port?: undefined;
370
+ /** @internal */
371
+ remote?: undefined;
373
372
  };
374
373
 
375
374
  /**
@@ -1047,8 +1046,6 @@ export const _Worker = Resource(
1047
1046
  ? props.namespace
1048
1047
  : props.namespace?.namespaceName;
1049
1048
 
1050
- const dev = normalizeDev(this, props.dev);
1051
-
1052
1049
  const [bundle, error] = wrap(() =>
1053
1050
  normalizeWorkerBundle({
1054
1051
  entrypoint: props.entrypoint,
@@ -1067,34 +1064,45 @@ export const _Worker = Resource(
1067
1064
  }),
1068
1065
  );
1069
1066
 
1070
- 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) {
1071
1072
  let url: string | undefined;
1072
1073
  if (error) {
1073
1074
  throw error;
1074
1075
  }
1075
1076
 
1076
- switch (dev.type) {
1077
- case "command":
1078
- createDevCommand({
1079
- id,
1080
- command: dev.command,
1081
- cwd: dev.cwd ?? props.cwd ?? process.cwd(),
1082
- env: props.env ?? {},
1083
- });
1084
- url = dev.url;
1085
- break;
1086
- case "miniflare": {
1087
- url = await createMiniflare({
1088
- id,
1089
- workerName,
1090
- compatibilityDate,
1091
- compatibilityFlags,
1092
- bindings: props.bindings,
1093
- bundle,
1094
- port: dev.port,
1095
- });
1096
- break;
1097
- }
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
+ });
1098
1106
  }
1099
1107
 
1100
1108
  return this({
@@ -1218,7 +1226,7 @@ export const _Worker = Resource(
1218
1226
  }
1219
1227
 
1220
1228
  let putWorkerResult: PutWorkerResult;
1221
- if (dev.type === "remote") {
1229
+ if (watch) {
1222
1230
  // todo(john): clean this up and add log tail
1223
1231
  const controller = new AbortController();
1224
1232
  cleanups.push(() => controller.abort());
@@ -1436,57 +1444,6 @@ process.on("SIGINT", async () => {
1436
1444
  process.exit(0);
1437
1445
  });
1438
1446
 
1439
- type Dev =
1440
- | {
1441
- type: "none";
1442
- local: false;
1443
- }
1444
- | {
1445
- type: "miniflare";
1446
- port?: number;
1447
- local: true;
1448
- }
1449
- | {
1450
- type: "command";
1451
- command: string;
1452
- url: string;
1453
- cwd?: string;
1454
- local: true;
1455
- }
1456
- | {
1457
- type: "remote";
1458
- local: false;
1459
- };
1460
-
1461
- const normalizeDev = (ctx: Context<any>, dev: WorkerProps["dev"]): Dev => {
1462
- if (!ctx.scope.dev || ctx.phase === "delete" || dev === false) {
1463
- return {
1464
- type: "none",
1465
- local: false,
1466
- };
1467
- }
1468
- const devObj = dev === true ? {} : (dev ?? {});
1469
- if ("command" in devObj) {
1470
- // Commands are always local
1471
- return {
1472
- type: "command",
1473
- ...devObj,
1474
- local: true,
1475
- };
1476
- }
1477
- if (devObj.remote === false || ctx.scope.dev === "prefer-local") {
1478
- return {
1479
- type: "miniflare",
1480
- port: devObj.port,
1481
- local: true,
1482
- };
1483
- }
1484
- return {
1485
- type: "remote",
1486
- local: false,
1487
- };
1488
- };
1489
-
1490
1447
  const assertUnique = <T, Key extends keyof T>(
1491
1448
  inputs: T[],
1492
1449
  key: Key,
@@ -1858,15 +1815,15 @@ async function createMiniflare(props: {
1858
1815
  return await startPromise.value;
1859
1816
  }
1860
1817
 
1861
- function createDevCommand(props: {
1818
+ async function createDevCommand(props: {
1862
1819
  id: string;
1863
1820
  command: string;
1864
1821
  cwd: string;
1865
1822
  env: Record<string, string>;
1866
- }) {
1823
+ }): Promise<{ url: string }> {
1867
1824
  const persistFile = path.join(process.cwd(), ".alchemy", `${props.id}.pid`);
1868
- if (existsSync(persistFile)) {
1869
- const pid = Number.parseInt(readFileSync(persistFile, "utf8"));
1825
+ if (await exists(persistFile)) {
1826
+ const pid = Number.parseInt(await fs.readFile(persistFile, "utf8"));
1870
1827
  try {
1871
1828
  // Actually kill the process if it's alive
1872
1829
  process.kill(pid, "SIGTERM");
@@ -1874,12 +1831,20 @@ function createDevCommand(props: {
1874
1831
  // ignore
1875
1832
  }
1876
1833
  try {
1877
- unlinkSync(persistFile);
1834
+ await fs.unlink(persistFile);
1878
1835
  } catch {
1879
1836
  // ignore
1880
1837
  }
1881
1838
  }
1882
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
+ });
1883
1848
  const proc = spawn(command[0], command.slice(1), {
1884
1849
  cwd: props.cwd,
1885
1850
  env: {
@@ -1890,21 +1855,61 @@ function createDevCommand(props: {
1890
1855
  ".alchemy",
1891
1856
  "miniflare",
1892
1857
  ),
1858
+ // Force colors in the child process since we're piping output
1859
+ // FORCE_COLOR: "1",
1893
1860
  },
1894
- stdio: ["inherit", "inherit", "inherit"],
1861
+ stdio: ["inherit", "pipe", "pipe"],
1895
1862
  });
1896
- cleanups.push(() => {
1897
- try {
1898
- unlinkSync(persistFile);
1899
- } catch {
1900
- // ignore
1901
- }
1902
- 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
+ });
1903
1907
  });
1904
1908
  if (proc.pid) {
1905
- mkdirSync(path.dirname(persistFile), { recursive: true });
1906
- writeFileSync(persistFile, proc.pid.toString());
1909
+ await fs.mkdir(path.dirname(persistFile), { recursive: true });
1910
+ await fs.writeFile(persistFile, proc.pid.toString());
1907
1911
  }
1912
+ return { url };
1908
1913
  }
1909
1914
 
1910
1915
  type PutWorkerOptions = Omit<WorkerProps, "entrypoint"> & {
package/src/scope.ts CHANGED
@@ -38,7 +38,18 @@ export interface ScopeOptions {
38
38
  stateStore?: StateStoreType;
39
39
  quiet?: boolean;
40
40
  phase?: Phase;
41
- dev?: "prefer-local" | "prefer-remote";
41
+ /**
42
+ * Determines if resources should be simulated locally (where possible)
43
+ *
44
+ * @default - `true` if ran with `alchemy dev` or `bun ./alchemy.run.ts --dev`
45
+ */
46
+ local?: boolean;
47
+ /**
48
+ * Determines if local changes to resources should be reactively pushed to the local or remote environment.
49
+ *
50
+ * @default - `true` if ran with `alchemy dev`, `alchemy watch`, `bun --watch ./alchemy.run.ts`
51
+ */
52
+ watch?: boolean;
42
53
  telemetryClient?: ITelemetryClient;
43
54
  logger?: LoggerApi;
44
55
  }
@@ -107,7 +118,8 @@ export class Scope {
107
118
  public readonly stateStore: StateStoreType;
108
119
  public readonly quiet: boolean;
109
120
  public readonly phase: Phase;
110
- public readonly dev?: "prefer-local" | "prefer-remote";
121
+ public readonly local: boolean;
122
+ public readonly watch: boolean;
111
123
  public readonly logger: LoggerApi;
112
124
  public readonly telemetryClient: ITelemetryClient;
113
125
  public readonly dataMutex: AsyncMutex;
@@ -162,9 +174,10 @@ export class Scope {
162
174
  options.logger,
163
175
  );
164
176
 
165
- this.dev = options.dev ?? this.parent?.dev;
177
+ this.local = options.local ?? this.parent?.local ?? false;
178
+ this.watch = options.watch ?? this.parent?.watch ?? false;
166
179
 
167
- if (this.dev) {
180
+ if (this.local) {
168
181
  this.logger.warnOnce(
169
182
  "Development mode is in beta. Please report any issues to https://github.com/sam-goodwin/alchemy/issues.",
170
183
  );
@@ -415,6 +428,10 @@ export class Scope {
415
428
  await this.rootTelemetryClient?.finalize()?.catch((error) => {
416
429
  this.logger.warn("Telemetry finalization failed:", error);
417
430
  });
431
+
432
+ if (!this.parent && process.env.ALCHEMY_TEST_KILL_ON_FINALIZE) {
433
+ process.exit(0);
434
+ }
418
435
  }
419
436
 
420
437
  public async destroyPendingDeletions() {
@@ -20,6 +20,7 @@ export async function safeFetch(
20
20
  err?.code === "UND_ERR_SOCKET" ||
21
21
  err?.code === "ECONNRESET" ||
22
22
  err?.code === "UND_ERR_CONNECT_TIMEOUT" ||
23
+ err?.code === "EPIPE" ||
23
24
  err?.name === "FetchError";
24
25
 
25
26
  if (!shouldRetry || attempt === retries) {
@@ -3,7 +3,7 @@
3
3
  "name": "@alchemy.run/react-router-template",
4
4
  "private": true,
5
5
  "scripts": {
6
- "build": "vite build",
6
+ "build": "react-router build",
7
7
  "deploy": "alchemy deploy",
8
8
  "destroy": "alchemy destroy",
9
9
  "dev": "alchemy dev",
@@ -11,7 +11,11 @@
11
11
  "composite": true,
12
12
  "strict": true,
13
13
  "lib": ["DOM", "DOM.Iterable", "ES2022"],
14
- "types": ["vite/client"],
14
+ "types": [
15
+ "vite/client",
16
+ "@cloudflare/workers-types",
17
+ "./types/env.d.ts"
18
+ ],
15
19
  "target": "ES2022",
16
20
  "module": "ES2022",
17
21
  "moduleResolution": "bundler",
@@ -10,8 +10,5 @@
10
10
  "skipLibCheck": true,
11
11
  "strict": true,
12
12
  "noEmit": true,
13
- "types": [
14
- "./worker-configuration.d.ts"
15
- ]
16
13
  }
17
14
  }
@@ -8,10 +8,6 @@
8
8
  "compilerOptions": {
9
9
  "composite": true,
10
10
  "strict": true,
11
- "types": [
12
- "@cloudflare/workers-types",
13
- "./types/env.d.ts"
14
- ],
15
11
  "lib": ["ES2022"],
16
12
  "target": "ES2022",
17
13
  "module": "ES2022",