alchemy 0.39.1 → 0.40.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 (151) hide show
  1. package/lib/alchemy.d.ts +6 -0
  2. package/lib/alchemy.d.ts.map +1 -1
  3. package/lib/alchemy.js +5 -0
  4. package/lib/alchemy.js.map +1 -1
  5. package/lib/build-date.d.ts +1 -1
  6. package/lib/build-date.js +1 -1
  7. package/lib/cloudflare/bucket.d.ts +10 -0
  8. package/lib/cloudflare/bucket.d.ts.map +1 -1
  9. package/lib/cloudflare/bucket.js +1 -0
  10. package/lib/cloudflare/bucket.js.map +1 -1
  11. package/lib/cloudflare/bundle/bundle-worker-dev.d.ts +25 -0
  12. package/lib/cloudflare/bundle/bundle-worker-dev.d.ts.map +1 -0
  13. package/lib/cloudflare/bundle/bundle-worker-dev.js +85 -0
  14. package/lib/cloudflare/bundle/bundle-worker-dev.js.map +1 -0
  15. package/lib/cloudflare/bundle/bundle-worker.d.ts.map +1 -1
  16. package/lib/cloudflare/bundle/bundle-worker.js +0 -3
  17. package/lib/cloudflare/bundle/bundle-worker.js.map +1 -1
  18. package/lib/cloudflare/bundle/nodejs-compat-mode.d.ts +1 -2
  19. package/lib/cloudflare/bundle/nodejs-compat-mode.d.ts.map +1 -1
  20. package/lib/cloudflare/bundle/nodejs-compat-mode.js +6 -1
  21. package/lib/cloudflare/bundle/nodejs-compat-mode.js.map +1 -1
  22. package/lib/cloudflare/d1-database.d.ts +10 -0
  23. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  24. package/lib/cloudflare/d1-database.js +1 -0
  25. package/lib/cloudflare/d1-database.js.map +1 -1
  26. package/lib/cloudflare/do-state-store/internal.d.ts +1 -0
  27. package/lib/cloudflare/do-state-store/internal.d.ts.map +1 -1
  28. package/lib/cloudflare/do-state-store/internal.js +27 -29
  29. package/lib/cloudflare/do-state-store/internal.js.map +1 -1
  30. package/lib/cloudflare/do-state-store/store.d.ts.map +1 -1
  31. package/lib/cloudflare/do-state-store/store.js +7 -20
  32. package/lib/cloudflare/do-state-store/store.js.map +1 -1
  33. package/lib/cloudflare/images.d.ts +12 -0
  34. package/lib/cloudflare/images.d.ts.map +1 -1
  35. package/lib/cloudflare/images.js +4 -0
  36. package/lib/cloudflare/images.js.map +1 -1
  37. package/lib/cloudflare/kv-namespace.d.ts +10 -0
  38. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  39. package/lib/cloudflare/kv-namespace.js +1 -0
  40. package/lib/cloudflare/kv-namespace.js.map +1 -1
  41. package/lib/cloudflare/queue.d.ts +10 -0
  42. package/lib/cloudflare/queue.d.ts.map +1 -1
  43. package/lib/cloudflare/queue.js +1 -0
  44. package/lib/cloudflare/queue.js.map +1 -1
  45. package/lib/cloudflare/vite.d.ts.map +1 -1
  46. package/lib/cloudflare/vite.js +12 -0
  47. package/lib/cloudflare/vite.js.map +1 -1
  48. package/lib/cloudflare/website.d.ts +7 -0
  49. package/lib/cloudflare/website.d.ts.map +1 -1
  50. package/lib/cloudflare/website.js +16 -6
  51. package/lib/cloudflare/website.js.map +1 -1
  52. package/lib/cloudflare/worker/get-worker-template.d.ts +2 -0
  53. package/lib/cloudflare/worker/get-worker-template.d.ts.map +1 -0
  54. package/lib/cloudflare/worker/get-worker-template.js +12 -0
  55. package/lib/cloudflare/worker/get-worker-template.js.map +1 -0
  56. package/lib/cloudflare/worker/http-server.d.ts +14 -0
  57. package/lib/cloudflare/worker/http-server.d.ts.map +1 -0
  58. package/lib/cloudflare/worker/http-server.js +67 -0
  59. package/lib/cloudflare/worker/http-server.js.map +1 -0
  60. package/lib/cloudflare/worker/miniflare-worker-options.d.ts +13 -0
  61. package/lib/cloudflare/worker/miniflare-worker-options.d.ts.map +1 -0
  62. package/lib/cloudflare/worker/miniflare-worker-options.js +408 -0
  63. package/lib/cloudflare/worker/miniflare-worker-options.js.map +1 -0
  64. package/lib/cloudflare/worker/miniflare.d.ts +31 -0
  65. package/lib/cloudflare/worker/miniflare.d.ts.map +1 -0
  66. package/lib/cloudflare/worker/miniflare.js +150 -0
  67. package/lib/cloudflare/worker/miniflare.js.map +1 -0
  68. package/lib/cloudflare/worker/mixed-mode.d.ts +17 -0
  69. package/lib/cloudflare/worker/mixed-mode.d.ts.map +1 -0
  70. package/lib/cloudflare/worker/mixed-mode.js +145 -0
  71. package/lib/cloudflare/worker/mixed-mode.js.map +1 -0
  72. package/lib/cloudflare/worker/subdomain.d.ts +3 -0
  73. package/lib/cloudflare/worker/subdomain.d.ts.map +1 -0
  74. package/lib/cloudflare/worker/subdomain.js +10 -0
  75. package/lib/cloudflare/worker/subdomain.js.map +1 -0
  76. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  77. package/lib/cloudflare/worker-metadata.js +7 -3
  78. package/lib/cloudflare/worker-metadata.js.map +1 -1
  79. package/lib/cloudflare/worker.d.ts +23 -1
  80. package/lib/cloudflare/worker.d.ts.map +1 -1
  81. package/lib/cloudflare/worker.js +229 -8
  82. package/lib/cloudflare/worker.js.map +1 -1
  83. package/lib/cloudflare/workflow.d.ts +7 -0
  84. package/lib/cloudflare/workflow.d.ts.map +1 -1
  85. package/lib/cloudflare/workflow.js.map +1 -1
  86. package/lib/scope.d.ts +2 -0
  87. package/lib/scope.d.ts.map +1 -1
  88. package/lib/scope.js +6 -1
  89. package/lib/scope.js.map +1 -1
  90. package/lib/util/assert-never.d.ts +1 -1
  91. package/lib/util/assert-never.d.ts.map +1 -1
  92. package/lib/util/assert-never.js +2 -2
  93. package/lib/util/assert-never.js.map +1 -1
  94. package/lib/util/cli.d.ts +21 -7
  95. package/lib/util/cli.d.ts.map +1 -1
  96. package/lib/util/cli.js +8 -0
  97. package/lib/util/cli.js.map +1 -1
  98. package/lib/util/deferred-promise.d.ts +8 -0
  99. package/lib/util/deferred-promise.d.ts.map +1 -0
  100. package/lib/util/deferred-promise.js +16 -0
  101. package/lib/util/deferred-promise.js.map +1 -0
  102. package/lib/util/detect-package-manager.d.ts +3 -0
  103. package/lib/util/detect-package-manager.d.ts.map +1 -0
  104. package/lib/util/detect-package-manager.js +25 -0
  105. package/lib/util/detect-package-manager.js.map +1 -0
  106. package/lib/util/logger.d.ts.map +1 -1
  107. package/lib/util/logger.js +1 -0
  108. package/lib/util/logger.js.map +1 -1
  109. package/lib/util/memoize.d.ts +4 -0
  110. package/lib/util/memoize.d.ts.map +1 -0
  111. package/lib/util/memoize.js +17 -0
  112. package/lib/util/memoize.js.map +1 -0
  113. package/package.json +7 -5
  114. package/src/alchemy.ts +14 -0
  115. package/src/build-date.ts +1 -1
  116. package/src/cloudflare/bucket.ts +12 -0
  117. package/src/cloudflare/bundle/bundle-worker-dev.ts +126 -0
  118. package/src/cloudflare/bundle/bundle-worker.ts +0 -5
  119. package/src/cloudflare/bundle/nodejs-compat-mode.ts +12 -3
  120. package/src/cloudflare/d1-database.ts +11 -0
  121. package/src/cloudflare/do-state-store/internal.ts +30 -31
  122. package/src/cloudflare/do-state-store/store.ts +9 -26
  123. package/src/cloudflare/images.ts +14 -0
  124. package/src/cloudflare/kv-namespace.ts +12 -0
  125. package/src/cloudflare/queue.ts +12 -0
  126. package/src/cloudflare/vite.ts +12 -0
  127. package/src/cloudflare/website.ts +25 -6
  128. package/src/cloudflare/worker/get-worker-template.ts +14 -0
  129. package/src/cloudflare/worker/http-server.ts +80 -0
  130. package/src/cloudflare/worker/miniflare-worker-options.ts +531 -0
  131. package/src/cloudflare/worker/miniflare.ts +198 -0
  132. package/src/cloudflare/worker/mixed-mode.ts +220 -0
  133. package/src/cloudflare/worker/subdomain.ts +16 -0
  134. package/src/cloudflare/worker-metadata.ts +12 -4
  135. package/src/cloudflare/worker.ts +284 -18
  136. package/src/cloudflare/workflow.ts +7 -0
  137. package/src/scope.ts +11 -1
  138. package/src/util/assert-never.ts +2 -2
  139. package/src/util/cli.ts +19 -7
  140. package/src/util/deferred-promise.ts +19 -0
  141. package/src/util/detect-package-manager.ts +23 -0
  142. package/src/util/logger.ts +1 -0
  143. package/src/util/memoize.ts +23 -0
  144. package/workers/do-state-store.js +826 -0
  145. package/{src/cloudflare/do-state-store/worker.ts → workers/do-state-store.ts} +55 -23
  146. package/workers/mixed-mode-proxy-worker.js +39 -0
  147. package/workers/mixed-mode-proxy-worker.ts +46 -0
  148. package/lib/cloudflare/do-state-store/worker.d.ts +0 -27
  149. package/lib/cloudflare/do-state-store/worker.d.ts.map +0 -1
  150. package/lib/cloudflare/do-state-store/worker.js +0 -179
  151. package/lib/cloudflare/do-state-store/worker.js.map +0 -1
@@ -1,3 +1,11 @@
1
+ import { spawn } from "node:child_process";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from "node:fs";
1
9
  import path from "node:path";
2
10
  import { BUILD_DATE } from "../build-date.ts";
3
11
  import type { Context } from "../context.ts";
@@ -11,6 +19,7 @@ import { Secret, secret } from "../secret.ts";
11
19
  import { serializeScope } from "../serde.ts";
12
20
  import type { type } from "../type.ts";
13
21
  import { getContentType } from "../util/content-type.ts";
22
+ import { DeferredPromise } from "../util/deferred-promise.ts";
14
23
  import { logger } from "../util/logger.ts";
15
24
  import { withExponentialBackoff } from "../util/retry.ts";
16
25
  import { CloudflareApiError, handleApiError } from "./api-error.ts";
@@ -28,6 +37,7 @@ import {
28
37
  } from "./bindings.ts";
29
38
  import type { Bound } from "./bound.ts";
30
39
  import { isBucket } from "./bucket.ts";
40
+ import { createWorkerDevContext } from "./bundle/bundle-worker-dev.ts";
31
41
  import {
32
42
  type NoBundleResult,
33
43
  bundleWorkerScript,
@@ -61,6 +71,9 @@ import {
61
71
  prepareWorkerMetadata,
62
72
  } from "./worker-metadata.ts";
63
73
  import { WorkerStub, isWorkerStub } from "./worker-stub.ts";
74
+ import type { MiniflareWorkerOptions } from "./worker/miniflare-worker-options.ts";
75
+ import { miniflareServer } from "./worker/miniflare.ts";
76
+ import { getAccountSubdomain } from "./worker/subdomain.ts";
64
77
  import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts";
65
78
 
66
79
  /**
@@ -300,6 +313,32 @@ export interface BaseWorkerProps<
300
313
  * @example "pr-123"
301
314
  */
302
315
  version?: string;
316
+
317
+ /**
318
+ * Configuration for local development. By default, when Alchemy is running in development mode,
319
+ * the worker will be emulated locally and available at a randomly selected port.
320
+ */
321
+ dev?:
322
+ | boolean
323
+ | {
324
+ /**
325
+ * Port to use for local development
326
+ */
327
+ port?: number;
328
+ /**
329
+ * EXPERIMENTAL: Whether to run the worker remotely instead of locally.
330
+ *
331
+ * When this is enabled, hot reloading will not work.
332
+ *
333
+ * @default false
334
+ */
335
+ remote?: boolean;
336
+ }
337
+ | {
338
+ command: string;
339
+ url: string;
340
+ cwd?: string;
341
+ };
303
342
  }
304
343
 
305
344
  export interface InlineWorkerProps<
@@ -922,9 +961,6 @@ export const _Worker = Resource(
922
961
  throw new Error("entrypoint must be provided when noBundle is true");
923
962
  }
924
963
 
925
- // Create Cloudflare API client with automatic account discovery
926
- const api = await createCloudflareApi(props);
927
-
928
964
  // Use the provided name
929
965
  const workerName = props.name ?? id;
930
966
 
@@ -932,6 +968,200 @@ export const _Worker = Resource(
932
968
  props.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE;
933
969
  const compatibilityFlags = props.compatibilityFlags ?? [];
934
970
 
971
+ if (this.scope.dev && this.phase !== "delete" && props.dev !== false) {
972
+ // Get current timestamp
973
+ const now = Date.now();
974
+
975
+ if (typeof props.dev === "object" && "command" in props.dev) {
976
+ upsertDevCommand({
977
+ id,
978
+ command: props.dev.command,
979
+ cwd: props.dev.cwd ?? process.cwd(),
980
+ env: props.env ?? {},
981
+ });
982
+ return this({
983
+ type: "service",
984
+ id,
985
+ entrypoint: props.entrypoint,
986
+ name: workerName,
987
+ compatibilityDate,
988
+ compatibilityFlags,
989
+ format: props.format || "esm", // Include format in the output
990
+ bindings: props.bindings ?? ({} as B),
991
+ env: props.env,
992
+ observability: props.observability,
993
+ createdAt: now,
994
+ updatedAt: now,
995
+ eventSources: props.eventSources,
996
+ url: props.dev.url,
997
+ dev: props.dev,
998
+ // Include assets configuration in the output
999
+ assets: props.assets,
1000
+ // Include cron triggers in the output
1001
+ crons: props.crons,
1002
+ // phantom property
1003
+ Env: undefined!,
1004
+ } as unknown as Worker<B>);
1005
+ }
1006
+
1007
+ const sharedOptions: Omit<MiniflareWorkerOptions, "script"> = {
1008
+ name: workerName,
1009
+ compatibilityDate,
1010
+ compatibilityFlags,
1011
+ bindings: props.bindings ?? ({} as B),
1012
+ port: typeof props.dev === "object" ? props.dev.port : undefined,
1013
+ };
1014
+
1015
+ let url: string;
1016
+
1017
+ // If entrypoint is provided, set up hot reloading with esbuild context
1018
+ if (props.entrypoint) {
1019
+ const startPromise = new DeferredPromise<string>();
1020
+
1021
+ await createWorkerDevContext(
1022
+ workerName,
1023
+ {
1024
+ ...props,
1025
+ entrypoint: props.entrypoint,
1026
+ compatibilityDate,
1027
+ compatibilityFlags,
1028
+ },
1029
+ {
1030
+ onBuildStart: () => {
1031
+ logger.task("miniflare-server", {
1032
+ message: `${startPromise.status === "pending" ? "Building" : "Rebuilding"}`,
1033
+ status: "pending",
1034
+ resource: id,
1035
+ prefix: "build",
1036
+ prefixColor: "cyanBright",
1037
+ });
1038
+ },
1039
+ onBuildEnd: async (newScript) => {
1040
+ try {
1041
+ // Hot reload callback - update the miniflare worker
1042
+ const server = await miniflareServer.push({
1043
+ ...sharedOptions,
1044
+ script: newScript,
1045
+ });
1046
+ if (startPromise.status === "pending") {
1047
+ logger.task("miniflare-server", {
1048
+ message: `Started Miniflare server at ${server.url}`,
1049
+ status: "success",
1050
+ resource: id,
1051
+ prefix: "ready",
1052
+ prefixColor: "cyanBright",
1053
+ });
1054
+ startPromise.resolve(server.url);
1055
+ } else {
1056
+ logger.task("miniflare-server", {
1057
+ message: `Updated Miniflare server at ${server.url}`,
1058
+ status: "success",
1059
+ resource: id,
1060
+ prefix: "reload",
1061
+ prefixColor: "cyanBright",
1062
+ });
1063
+ }
1064
+ } catch (error) {
1065
+ if (startPromise.status === "pending") {
1066
+ logger.error(error);
1067
+ logger.task("miniflare-server", {
1068
+ message: "Failed to start Miniflare server",
1069
+ status: "failure",
1070
+ resource: id,
1071
+ prefix: "error",
1072
+ prefixColor: "redBright",
1073
+ });
1074
+ startPromise.reject(
1075
+ new Error(
1076
+ `Failed to start Miniflare server for worker "${workerName}"`,
1077
+ { cause: error },
1078
+ ),
1079
+ );
1080
+ } else {
1081
+ logger.task("miniflare-server", {
1082
+ message: "Failed to update Miniflare server",
1083
+ status: "failure",
1084
+ resource: id,
1085
+ prefix: "error",
1086
+ prefixColor: "redBright",
1087
+ });
1088
+ logger.error(error);
1089
+ }
1090
+ }
1091
+ },
1092
+ onBuildError: (errors) => {
1093
+ if (startPromise.status === "pending") {
1094
+ logger.task("miniflare-server", {
1095
+ message: "Failed to build worker",
1096
+ status: "failure",
1097
+ resource: id,
1098
+ prefix: "error",
1099
+ prefixColor: "redBright",
1100
+ });
1101
+ startPromise.reject(errors);
1102
+ } else {
1103
+ logger.task("miniflare-server", {
1104
+ message: "Failed to rebuild worker",
1105
+ status: "failure",
1106
+ resource: id,
1107
+ prefix: "error",
1108
+ prefixColor: "redBright",
1109
+ });
1110
+ logger.error(errors);
1111
+ }
1112
+ },
1113
+ },
1114
+ );
1115
+
1116
+ url = await startPromise.value;
1117
+ } else {
1118
+ // Fallback to one-time bundling for inline scripts
1119
+ const scriptContent =
1120
+ props.script ??
1121
+ (await bundleWorkerScript({
1122
+ name: workerName,
1123
+ ...props,
1124
+ compatibilityDate,
1125
+ compatibilityFlags,
1126
+ }));
1127
+ const server = await miniflareServer.push({
1128
+ ...sharedOptions,
1129
+ script:
1130
+ typeof scriptContent === "string"
1131
+ ? scriptContent
1132
+ : scriptContent[props.entrypoint!].toString(),
1133
+ });
1134
+ url = server.url;
1135
+ }
1136
+
1137
+ return this({
1138
+ type: "service",
1139
+ id,
1140
+ entrypoint: props.entrypoint,
1141
+ name: workerName,
1142
+ compatibilityDate,
1143
+ compatibilityFlags,
1144
+ format: props.format || "esm", // Include format in the output
1145
+ bindings: props.bindings ?? ({} as B),
1146
+ env: props.env,
1147
+ observability: props.observability,
1148
+ createdAt: now,
1149
+ updatedAt: now,
1150
+ eventSources: props.eventSources,
1151
+ url,
1152
+ dev: props.dev,
1153
+ // Include assets configuration in the output
1154
+ assets: props.assets,
1155
+ // Include cron triggers in the output
1156
+ crons: props.crons,
1157
+ // phantom property
1158
+ Env: undefined!,
1159
+ } as unknown as Worker<B>);
1160
+ }
1161
+
1162
+ // Create Cloudflare API client with automatic account discovery
1163
+ const api = await createCloudflareApi(props);
1164
+
935
1165
  const uploadWorkerScript = async (props: WorkerProps<B>) => {
936
1166
  // Get the script content - either from props.script, or by bundling
937
1167
  const scriptBundle =
@@ -1229,6 +1459,7 @@ export const _Worker = Resource(
1229
1459
  updatedAt: now,
1230
1460
  eventSources: props.eventSources,
1231
1461
  url: workerUrl,
1462
+ dev: props.dev,
1232
1463
  // Include assets configuration in the output
1233
1464
  assets: props.assets,
1234
1465
  // Include cron triggers in the output
@@ -1288,6 +1519,55 @@ export async function deleteWorker(
1288
1519
  return;
1289
1520
  }
1290
1521
 
1522
+ function upsertDevCommand(props: {
1523
+ id: string;
1524
+ command: string;
1525
+ cwd: string;
1526
+ env: Record<string, string>;
1527
+ }) {
1528
+ const persistFile = path.join(process.cwd(), ".alchemy", `${props.id}.pid`);
1529
+ if (existsSync(persistFile)) {
1530
+ const pid = Number.parseInt(readFileSync(persistFile, "utf8"));
1531
+ try {
1532
+ // Actually kill the process if it's alive
1533
+ process.kill(pid, "SIGTERM");
1534
+ } catch {
1535
+ // ignore
1536
+ }
1537
+ try {
1538
+ unlinkSync(persistFile);
1539
+ } catch {
1540
+ // ignore
1541
+ }
1542
+ }
1543
+ const command = props.command.split(" ");
1544
+ const proc = spawn(command[0], command.slice(1), {
1545
+ env: {
1546
+ ...process.env,
1547
+ ...props.env,
1548
+ ALCHEMY_CLOUDFLARE_PERSIST_PATH: path.join(
1549
+ process.cwd(),
1550
+ ".alchemy",
1551
+ "miniflare",
1552
+ ),
1553
+ },
1554
+ stdio: ["inherit", "inherit", "inherit"],
1555
+ });
1556
+ process.on("SIGINT", () => {
1557
+ try {
1558
+ unlinkSync(persistFile);
1559
+ } catch {
1560
+ // ignore
1561
+ }
1562
+ proc.kill("SIGINT");
1563
+ process.exit(0);
1564
+ });
1565
+ if (proc.pid) {
1566
+ mkdirSync(path.dirname(persistFile), { recursive: true });
1567
+ writeFileSync(persistFile, proc.pid.toString());
1568
+ }
1569
+ }
1570
+
1291
1571
  type PutWorkerOptions = WorkerProps & {
1292
1572
  dispatchNamespace?: string;
1293
1573
  migrationTag?: string;
@@ -1564,21 +1844,7 @@ export async function configureURL<B extends Bindings>(
1564
1844
  );
1565
1845
 
1566
1846
  // Get the account's workers.dev subdomain
1567
- const subdomainResponse = await api.get(
1568
- `/accounts/${api.accountId}/workers/subdomain`,
1569
- );
1570
-
1571
- if (!subdomainResponse.ok) {
1572
- throw new Error(
1573
- `Could not fetch workers.dev subdomain: ${subdomainResponse.status} ${subdomainResponse.statusText}`,
1574
- );
1575
- }
1576
- const subdomainData: {
1577
- result: {
1578
- subdomain: string;
1579
- };
1580
- } = await subdomainResponse.json();
1581
- const subdomain = subdomainData.result?.subdomain;
1847
+ const subdomain = await getAccountSubdomain(api);
1582
1848
 
1583
1849
  if (subdomain) {
1584
1850
  workerUrl = `https://${workerName}.${subdomain}.workers.dev`;
@@ -25,6 +25,13 @@ export interface WorkflowProps {
25
25
  * @default - bound worker script
26
26
  */
27
27
  scriptName?: string;
28
+ dev?: {
29
+ /**
30
+ * Whether to run the workflow remotely instead of locally
31
+ * @default false
32
+ */
33
+ remote?: boolean;
34
+ };
28
35
  }
29
36
 
30
37
  export function isWorkflow(binding: Binding): binding is Workflow {
package/src/scope.ts CHANGED
@@ -36,6 +36,7 @@ export interface ScopeOptions {
36
36
  stateStore?: StateStoreType;
37
37
  quiet?: boolean;
38
38
  phase?: Phase;
39
+ dev?: boolean;
39
40
  telemetryClient?: ITelemetryClient;
40
41
  logger?: LoggerApi;
41
42
  }
@@ -100,6 +101,7 @@ export class Scope {
100
101
  public readonly stateStore: StateStoreType;
101
102
  public readonly quiet: boolean;
102
103
  public readonly phase: Phase;
104
+ public readonly dev?: boolean;
103
105
  public readonly logger: LoggerApi;
104
106
  public readonly telemetryClient: ITelemetryClient;
105
107
  public readonly dataMutex: AsyncMutex;
@@ -111,7 +113,7 @@ export class Scope {
111
113
  private deferred: (() => Promise<any>)[] = [];
112
114
 
113
115
  constructor(options: ScopeOptions) {
114
- this.appName = options.appName;
116
+ this.appName = options.appName ?? options.parent?.appName;
115
117
  this.scopeName = options.scopeName ?? null;
116
118
  if (this.scopeName?.includes(":")) {
117
119
  throw new Error(
@@ -143,6 +145,14 @@ export class Scope {
143
145
  options.logger,
144
146
  );
145
147
 
148
+ this.dev = options.dev ?? this.parent?.dev ?? false;
149
+
150
+ if (this.dev) {
151
+ this.logger.warnOnce(
152
+ "Local development mode is in beta. Please report any issues to https://github.com/sam-goodwin/alchemy/issues.",
153
+ );
154
+ }
155
+
146
156
  this.stateStore =
147
157
  options.stateStore ??
148
158
  this.parent?.stateStore ??
@@ -1,3 +1,3 @@
1
- export function assertNever(value: never): never {
2
- throw new Error(`Unexpected value: ${value}`);
1
+ export function assertNever(value: never, message?: string): never {
2
+ throw new Error(message ?? `Unexpected value: ${value}`);
3
3
  }
package/src/util/cli.ts CHANGED
@@ -2,6 +2,10 @@ import packageJson from "../../package.json" with { type: "json" };
2
2
  import type { Phase } from "../alchemy.ts";
3
3
  import { dedent } from "./dedent.ts";
4
4
 
5
+ declare global {
6
+ var _ALCHEMY_WARNINGS: Set<string> | undefined;
7
+ }
8
+
5
9
  // ANSI color codes
6
10
  const colors = {
7
11
  reset: "\x1b[0m",
@@ -28,27 +32,28 @@ const colorize = (text: string, color: ColorName): string => {
28
32
  return `${colors[color]}${text}${colors.reset}`;
29
33
  };
30
34
 
31
- export type Task = {
35
+ export interface Task {
32
36
  prefix?: string;
33
- prefixColor?: string;
37
+ prefixColor?: ColorName;
34
38
  resource?: string;
35
39
  message: string;
36
40
  status?: "pending" | "success" | "failure";
37
- };
41
+ }
38
42
 
39
- export type LoggerApi = {
43
+ export interface LoggerApi {
40
44
  log: (...args: unknown[]) => void;
41
45
  warn: (...args: unknown[]) => void;
46
+ warnOnce: (message: string) => void;
42
47
  error: (...args: unknown[]) => void;
43
48
  task: (id: string, data: Task) => void;
44
49
  exit: () => void;
45
- };
50
+ }
46
51
 
47
- type AlchemyInfo = {
52
+ interface AlchemyInfo {
48
53
  phase: Phase;
49
54
  stage: string;
50
55
  appName: string;
51
- };
56
+ }
52
57
 
53
58
  let loggerApi: LoggerApi | null = null;
54
59
  export const createLoggerInstance = (
@@ -67,6 +72,7 @@ export const createDummyLogger = (): LoggerApi => {
67
72
  log: () => {},
68
73
  error: () => {},
69
74
  warn: () => {},
75
+ warnOnce: () => {},
70
76
  task: () => {},
71
77
  exit: () => {},
72
78
  };
@@ -87,6 +93,12 @@ export const createFallbackLogger = (alchemyInfo: AlchemyInfo): LoggerApi => {
87
93
  console.error(colorize("ERROR", "redBright"), ...args),
88
94
  warn: (...args: unknown[]) =>
89
95
  console.warn(colorize("WARN", "yellowBright"), ...args),
96
+ warnOnce: (message: string) => {
97
+ globalThis._ALCHEMY_WARNINGS ??= new Set();
98
+ if (globalThis._ALCHEMY_WARNINGS.has(message)) return;
99
+ globalThis._ALCHEMY_WARNINGS.add(message);
100
+ console.warn(colorize("WARN", "yellowBright"), message);
101
+ },
90
102
  task: (_id: string, data: Task) => {
91
103
  const prefix = data.prefix ? `[${data.prefix}]` : "";
92
104
 
@@ -0,0 +1,19 @@
1
+ export class DeferredPromise<T> {
2
+ private promise = Promise.withResolvers<T>();
3
+
4
+ status: "pending" | "fulfilled" | "rejected" = "pending";
5
+
6
+ get value() {
7
+ return this.promise.promise;
8
+ }
9
+
10
+ resolve(value: T) {
11
+ this.status = "fulfilled";
12
+ this.promise.resolve(value);
13
+ }
14
+
15
+ reject(reason?: any) {
16
+ this.status = "rejected";
17
+ this.promise.reject(reason);
18
+ }
19
+ }
@@ -0,0 +1,23 @@
1
+ import fs from "node:fs";
2
+
3
+ export type PackageManager = "bun" | "pnpm" | "yarn" | "npm";
4
+
5
+ export function detectPackageManager(): PackageManager {
6
+ if (fs.existsSync("bun.lockb")) return "bun";
7
+ if (fs.existsSync("pnpm-lock.yaml")) return "pnpm";
8
+ if (fs.existsSync("yarn.lock")) return "yarn";
9
+
10
+ if (process.env.npm_execpath?.includes("bun")) {
11
+ return "bun";
12
+ }
13
+
14
+ const userAgent = process.env.npm_config_user_agent;
15
+ if (userAgent) {
16
+ if (userAgent.startsWith("bun")) return "bun";
17
+ if (userAgent.startsWith("pnpm")) return "pnpm";
18
+ if (userAgent.startsWith("yarn")) return "yarn";
19
+ if (userAgent.startsWith("npm")) return "npm";
20
+ }
21
+
22
+ return "npm";
23
+ }
@@ -9,6 +9,7 @@ export const logger = new Proxy({} as LoggerApi, {
9
9
  log: console.log,
10
10
  error: console.error,
11
11
  warn: console.warn,
12
+ warnOnce: console.warn,
12
13
  task: () => {},
13
14
  exit: () => {},
14
15
  } as LoggerApi);
@@ -0,0 +1,23 @@
1
+ type AsyncReturnType<T> = T extends (...args: any[]) => Promise<infer R>
2
+ ? R
3
+ : T;
4
+
5
+ export function memoize<F extends (...args: any[]) => Promise<any>>(
6
+ fn: F,
7
+ keyFn: (...args: Parameters<F>) => string,
8
+ ) {
9
+ const cache = new Map<string, Promise<AsyncReturnType<F>>>();
10
+ return async (...args: Parameters<F>): Promise<AsyncReturnType<F>> => {
11
+ const key = keyFn(...args);
12
+ const cached = cache.get(key);
13
+ if (cached) {
14
+ return await cached;
15
+ }
16
+ const promise = fn(...args).catch((error) => {
17
+ cache.delete(key);
18
+ throw error;
19
+ });
20
+ cache.set(key, promise);
21
+ return await promise;
22
+ };
23
+ }