alchemy 2.0.0-beta.23 → 2.0.0-beta.25

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 (138) hide show
  1. package/bin/alchemy.js +12574 -2451
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +2 -0
  4. package/bin/cli.js +18 -0
  5. package/bin/commands/bootstrap.ts +88 -4
  6. package/bin/commands/deploy.ts +54 -20
  7. package/bin/commands/dev.ts +45 -0
  8. package/bin/exec.ts +22 -0
  9. package/lib/AdoptPolicy.d.ts +2 -2
  10. package/lib/AdoptPolicy.d.ts.map +1 -1
  11. package/lib/AdoptPolicy.js.map +1 -1
  12. package/lib/AlchemyContext.d.ts +1 -0
  13. package/lib/AlchemyContext.d.ts.map +1 -1
  14. package/lib/AlchemyContext.js +1 -0
  15. package/lib/AlchemyContext.js.map +1 -1
  16. package/lib/Build/Command.d.ts.map +1 -1
  17. package/lib/Build/Command.js +8 -1
  18. package/lib/Build/Command.js.map +1 -1
  19. package/lib/Bundle/Bundle.d.ts +21 -9
  20. package/lib/Bundle/Bundle.d.ts.map +1 -1
  21. package/lib/Bundle/Bundle.js +29 -15
  22. package/lib/Bundle/Bundle.js.map +1 -1
  23. package/lib/Cloudflare/Local/Sidecar.d.ts +27 -0
  24. package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -0
  25. package/lib/Cloudflare/Local/Sidecar.js +24 -0
  26. package/lib/Cloudflare/Local/Sidecar.js.map +1 -0
  27. package/lib/Cloudflare/Local/SidecarHandlers.d.ts +5 -0
  28. package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -0
  29. package/lib/Cloudflare/Local/SidecarHandlers.js +119 -0
  30. package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -0
  31. package/lib/Cloudflare/Local/SidecarServer.d.ts +2 -0
  32. package/lib/Cloudflare/Local/SidecarServer.d.ts.map +1 -0
  33. package/lib/Cloudflare/Local/SidecarServer.js +27 -0
  34. package/lib/Cloudflare/Local/SidecarServer.js.map +1 -0
  35. package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
  36. package/lib/Cloudflare/SecretsStore/Secret.js +55 -7
  37. package/lib/Cloudflare/SecretsStore/Secret.js.map +1 -1
  38. package/lib/Cloudflare/SecretsStore/SecretsStore.d.ts.map +1 -1
  39. package/lib/Cloudflare/SecretsStore/SecretsStore.js +35 -11
  40. package/lib/Cloudflare/SecretsStore/SecretsStore.js.map +1 -1
  41. package/lib/Cloudflare/StateStore/State.d.ts +35 -0
  42. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  43. package/lib/Cloudflare/StateStore/State.js +187 -45
  44. package/lib/Cloudflare/StateStore/State.js.map +1 -1
  45. package/lib/Cloudflare/StateStore/index.d.ts +1 -1
  46. package/lib/Cloudflare/StateStore/index.d.ts.map +1 -1
  47. package/lib/Cloudflare/StateStore/index.js +1 -1
  48. package/lib/Cloudflare/StateStore/index.js.map +1 -1
  49. package/lib/Cloudflare/Workers/Compatibility.d.ts +6 -0
  50. package/lib/Cloudflare/Workers/Compatibility.d.ts.map +1 -0
  51. package/lib/Cloudflare/Workers/Compatibility.js +10 -0
  52. package/lib/Cloudflare/Workers/Compatibility.js.map +1 -0
  53. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +7 -0
  54. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -0
  55. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +81 -0
  56. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -0
  57. package/lib/Cloudflare/Workers/Worker.d.ts +3 -1
  58. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  59. package/lib/Cloudflare/Workers/Worker.js +89 -31
  60. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  61. package/lib/Cloudflare/Workers/WorkerBundle.d.ts +31 -0
  62. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -0
  63. package/lib/Cloudflare/Workers/WorkerBundle.js +174 -0
  64. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -0
  65. package/lib/Cloudflare/Workers/WorkerName.d.ts +3 -0
  66. package/lib/Cloudflare/Workers/WorkerName.d.ts.map +1 -0
  67. package/lib/Cloudflare/Workers/WorkerName.js +9 -0
  68. package/lib/Cloudflare/Workers/WorkerName.js.map +1 -0
  69. package/lib/Cloudflare/Workers/index.d.ts +1 -0
  70. package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
  71. package/lib/Cloudflare/Workers/index.js +1 -0
  72. package/lib/Cloudflare/Workers/index.js.map +1 -1
  73. package/lib/Platform.d.ts +1 -1
  74. package/lib/Platform.d.ts.map +1 -1
  75. package/lib/Sidecar/Lock.d.ts +24 -0
  76. package/lib/Sidecar/Lock.d.ts.map +1 -0
  77. package/lib/Sidecar/Lock.js +102 -0
  78. package/lib/Sidecar/Lock.js.map +1 -0
  79. package/lib/Sidecar/RpcClient.d.ts +15 -0
  80. package/lib/Sidecar/RpcClient.d.ts.map +1 -0
  81. package/lib/Sidecar/RpcClient.js +75 -0
  82. package/lib/Sidecar/RpcClient.js.map +1 -0
  83. package/lib/Sidecar/RpcHandler.d.ts +48 -0
  84. package/lib/Sidecar/RpcHandler.d.ts.map +1 -0
  85. package/lib/Sidecar/RpcHandler.js +64 -0
  86. package/lib/Sidecar/RpcHandler.js.map +1 -0
  87. package/lib/Sidecar/RpcPaths.d.ts +14 -0
  88. package/lib/Sidecar/RpcPaths.d.ts.map +1 -0
  89. package/lib/Sidecar/RpcPaths.js +22 -0
  90. package/lib/Sidecar/RpcPaths.js.map +1 -0
  91. package/lib/Sidecar/RpcServer.d.ts +10 -0
  92. package/lib/Sidecar/RpcServer.d.ts.map +1 -0
  93. package/lib/Sidecar/RpcServer.js +50 -0
  94. package/lib/Sidecar/RpcServer.js.map +1 -0
  95. package/lib/Sidecar/RpcTransport.d.ts +22 -0
  96. package/lib/Sidecar/RpcTransport.d.ts.map +1 -0
  97. package/lib/Sidecar/RpcTransport.js +81 -0
  98. package/lib/Sidecar/RpcTransport.js.map +1 -0
  99. package/lib/Telemetry/Metrics.d.ts +15 -0
  100. package/lib/Telemetry/Metrics.d.ts.map +1 -1
  101. package/lib/Telemetry/Metrics.js +20 -0
  102. package/lib/Telemetry/Metrics.js.map +1 -1
  103. package/lib/Util/PlatformServices.d.ts +3 -0
  104. package/lib/Util/PlatformServices.d.ts.map +1 -1
  105. package/lib/Util/PlatformServices.js +13 -0
  106. package/lib/Util/PlatformServices.js.map +1 -1
  107. package/lib/Util/layer-scoped.d.ts +16 -0
  108. package/lib/Util/layer-scoped.d.ts.map +1 -0
  109. package/lib/Util/layer-scoped.js +14 -0
  110. package/lib/Util/layer-scoped.js.map +1 -0
  111. package/package.json +4 -2
  112. package/src/AdoptPolicy.ts +9 -9
  113. package/src/AlchemyContext.ts +2 -0
  114. package/src/Build/Command.ts +8 -1
  115. package/src/Bundle/Bundle.ts +65 -25
  116. package/src/Cloudflare/Local/Sidecar.ts +44 -0
  117. package/src/Cloudflare/Local/SidecarHandlers.ts +182 -0
  118. package/src/Cloudflare/Local/SidecarServer.ts +57 -0
  119. package/src/Cloudflare/SecretsStore/Secret.ts +78 -7
  120. package/src/Cloudflare/SecretsStore/SecretsStore.ts +47 -12
  121. package/src/Cloudflare/StateStore/State.ts +281 -64
  122. package/src/Cloudflare/StateStore/index.ts +1 -1
  123. package/src/Cloudflare/Workers/Compatibility.ts +12 -0
  124. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +93 -0
  125. package/src/Cloudflare/Workers/Worker.ts +151 -55
  126. package/src/Cloudflare/Workers/WorkerBundle.ts +234 -0
  127. package/src/Cloudflare/Workers/WorkerName.ts +10 -0
  128. package/src/Cloudflare/Workers/index.ts +1 -0
  129. package/src/Platform.ts +1 -1
  130. package/src/Sidecar/Lock.ts +190 -0
  131. package/src/Sidecar/RpcClient.ts +141 -0
  132. package/src/Sidecar/RpcHandler.ts +169 -0
  133. package/src/Sidecar/RpcPaths.ts +29 -0
  134. package/src/Sidecar/RpcServer.ts +80 -0
  135. package/src/Sidecar/RpcTransport.ts +85 -0
  136. package/src/Telemetry/Metrics.ts +38 -0
  137. package/src/Util/PlatformServices.ts +19 -0
  138. package/src/Util/layer-scoped.ts +25 -0
@@ -22,6 +22,7 @@ import { pathToFileURL } from "node:url";
22
22
  import type * as rolldown from "rolldown";
23
23
  import Sonda from "sonda/rolldown";
24
24
  import type * as vite from "vite";
25
+ import { AlchemyContext } from "../../AlchemyContext.ts";
25
26
  import * as Artifacts from "../../Artifacts.ts";
26
27
  import * as Binding from "../../Binding.ts";
27
28
  import {
@@ -53,6 +54,7 @@ import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
53
54
  import { D1Database } from "../D1/D1Database.ts";
54
55
  import { fromCloudflareFetcher } from "../Fetcher.ts";
55
56
  import type { KVNamespace } from "../KV/KVNamespace.ts";
57
+ import { SidecarLive } from "../Local/Sidecar.ts";
56
58
  import { CloudflareLogs } from "../Logs.ts";
57
59
  import type { Providers } from "../Providers.ts";
58
60
  import type { Queue as CloudflareQueue } from "../Queue/Queue.ts";
@@ -72,6 +74,7 @@ import {
72
74
  type DurableObjectNamespaceLike,
73
75
  } from "./DurableObjectNamespace.ts";
74
76
  import { workersHttpHandler } from "./HttpServer.ts";
77
+ import { LocalWorkerProvider } from "./LocalWorkerProvider.ts";
75
78
  import { Request } from "./Request.ts";
76
79
  import { makeRpcStub } from "./Rpc.ts";
77
80
  import { isWorkflowExport } from "./Workflow.ts";
@@ -966,7 +969,30 @@ function getDurableObjectTagMap(tags: ReadonlyArray<string>) {
966
969
  );
967
970
  }
968
971
 
972
+ const selectLayer = <
973
+ LayerLive extends Layer.Layer<any, any, any>,
974
+ LayerDev extends Layer.Layer<any, any, any>,
975
+ >(input: {
976
+ live: () => LayerLive;
977
+ dev: () => LayerDev;
978
+ }): Layer.Layer<
979
+ Layer.Success<LayerLive | LayerDev>,
980
+ Layer.Error<LayerLive | LayerDev>,
981
+ Layer.Services<LayerLive | LayerDev> | AlchemyContext
982
+ > =>
983
+ Layer.unwrap(
984
+ AlchemyContext.useSync((context) =>
985
+ context.dev ? input.dev() : input.live(),
986
+ ),
987
+ );
988
+
969
989
  export const WorkerProvider = () =>
990
+ selectLayer({
991
+ live: LiveWorkerProvider,
992
+ dev: () => Layer.provide(LocalWorkerProvider(), SidecarLive),
993
+ });
994
+
995
+ export const LiveWorkerProvider = () =>
970
996
  Provider.effect(
971
997
  Worker,
972
998
  Effect.gen(function* () {
@@ -1078,11 +1104,44 @@ export const WorkerProvider = () =>
1078
1104
  const reconcileDomains = (
1079
1105
  scriptName: string,
1080
1106
  desired: string[],
1081
- previous: Worker["Attributes"]["domains"],
1107
+ _previous: Worker["Attributes"]["domains"],
1082
1108
  ) =>
1083
1109
  Effect.gen(function* () {
1110
+ // Always query the live state of domains attached to *this*
1111
+ // Worker rather than trusting `_previous` from local state.
1112
+ // State may have been wiped, populated by another machine, or
1113
+ // simply be out of date. Without this we PUT domains that are
1114
+ // already registered to this same Worker and Cloudflare
1115
+ // returns a confusing "hostname already in use" error.
1116
+ const liveAll = yield* listDomains({
1117
+ accountId,
1118
+ service: scriptName,
1119
+ }).pipe(
1120
+ Effect.map((r) =>
1121
+ (r.result ?? []).flatMap((d) =>
1122
+ d.id && d.hostname && d.zoneId
1123
+ ? [
1124
+ {
1125
+ id: d.id,
1126
+ hostname: d.hostname,
1127
+ zoneId: d.zoneId,
1128
+ service: d.service ?? undefined,
1129
+ },
1130
+ ]
1131
+ : [],
1132
+ ),
1133
+ ),
1134
+ Effect.catch(() => Effect.succeed([])),
1135
+ );
1136
+
1084
1137
  const desiredSet = new Set(desired);
1085
- const toRemove = previous.filter((p) => !desiredSet.has(p.hostname));
1138
+ const liveByHostname = new Map(liveAll.map((d) => [d.hostname, d]));
1139
+
1140
+ // Detach what's no longer wanted. Use the live list so we
1141
+ // don't try to delete domains we no longer track.
1142
+ const toRemove = liveAll.filter(
1143
+ (d) => !desiredSet.has(d.hostname),
1144
+ );
1086
1145
  yield* Effect.all(
1087
1146
  toRemove.map((d) =>
1088
1147
  deleteDomain({ accountId, domainId: d.id }).pipe(
@@ -1095,26 +1154,62 @@ export const WorkerProvider = () =>
1095
1154
  if (desired.length === 0) return [];
1096
1155
 
1097
1156
  const zoneCache = new Map<string, string>();
1157
+
1158
+ // Attach `hostname` to this Worker. Skip the PUT entirely if
1159
+ // the hostname is already attached to *this* Worker — that's a
1160
+ // no-op for Cloudflare and avoids the "already in use" 409.
1161
+ // If it's attached to a *different* Worker, refuse with a
1162
+ // clear message rather than silently re-routing traffic.
1163
+ const attachDomain = Effect.fnUntraced(function* (hostname: string) {
1164
+ const live = liveByHostname.get(hostname);
1165
+ if (live) {
1166
+ return {
1167
+ hostname: live.hostname,
1168
+ id: live.id,
1169
+ zoneId: live.zoneId,
1170
+ };
1171
+ }
1172
+
1173
+ // Not attached to this Worker — but it could still belong
1174
+ // to another Worker. Check before we try to PUT so we can
1175
+ // emit a helpful error instead of the raw 409.
1176
+ const otherOwner = yield* listDomains({
1177
+ accountId,
1178
+ hostname,
1179
+ }).pipe(
1180
+ Effect.map((r) =>
1181
+ (r.result ?? []).find(
1182
+ (d) => d.hostname === hostname && d.service !== scriptName,
1183
+ ),
1184
+ ),
1185
+ Effect.catch(() => Effect.succeed(undefined)),
1186
+ );
1187
+ if (otherOwner?.id) {
1188
+ return yield* Effect.die(
1189
+ new Error(
1190
+ `Cannot attach hostname '${hostname}' to Worker '${scriptName}': ` +
1191
+ `it is already attached to Worker '${otherOwner.service ?? "<unknown>"}'. ` +
1192
+ `Detach it from that Worker first, or pick a different hostname.`,
1193
+ ),
1194
+ );
1195
+ }
1196
+
1197
+ const zoneId = yield* inferZoneIdForHostname(hostname, zoneCache);
1198
+ const res = yield* putDomain({
1199
+ accountId,
1200
+ hostname,
1201
+ service: scriptName,
1202
+ zoneId,
1203
+ });
1204
+ return {
1205
+ hostname,
1206
+ id: res.id ?? "",
1207
+ zoneId: res.zoneId ?? zoneId,
1208
+ };
1209
+ });
1210
+
1098
1211
  const applied = yield* Effect.all(
1099
- desired.map((hostname) =>
1100
- Effect.gen(function* () {
1101
- const zoneId = yield* inferZoneIdForHostname(
1102
- hostname,
1103
- zoneCache,
1104
- );
1105
- const res = yield* putDomain({
1106
- accountId,
1107
- hostname,
1108
- service: scriptName,
1109
- zoneId,
1110
- });
1111
- return {
1112
- hostname,
1113
- id: res.id ?? "",
1114
- zoneId: res.zoneId ?? zoneId,
1115
- };
1116
- }),
1117
- ),
1212
+ desired.map(attachDomain),
1118
1213
  { concurrency: "unbounded" },
1119
1214
  );
1120
1215
  return applied;
@@ -1205,21 +1300,15 @@ export const WorkerProvider = () =>
1205
1300
  return undefined;
1206
1301
  }
1207
1302
 
1208
- // Handle AssetsWithHash (from Build resource)
1209
- // Props are resolved by Plan, so Input<string> values are already strings at runtime
1210
1303
  if (
1211
1304
  typeof assets === "object" &&
1212
1305
  "path" in assets &&
1213
1306
  "hash" in assets
1214
1307
  ) {
1215
- const result = yield* readAssets({
1308
+ return yield* readAssets({
1216
1309
  directory: assets.path as string,
1217
1310
  config: assets.config,
1218
1311
  });
1219
- return {
1220
- ...result,
1221
- hash: assets.hash as string,
1222
- };
1223
1312
  }
1224
1313
 
1225
1314
  // Handle string path or AssetsProps
@@ -1547,31 +1636,30 @@ ${[
1547
1636
  let metadataAssets:
1548
1637
  | workers.PutScriptRequest["metadata"]["assets"]
1549
1638
  | undefined;
1550
- let keepAssets = false;
1639
+ const keepAssets = false;
1551
1640
  if (assets) {
1552
- if (output?.hash?.assets !== assets.hash) {
1553
- yield* Effect.logInfo(
1554
- `Cloudflare Worker ${olds ? "update" : "create"}: uploading assets for ${name}`,
1555
- );
1556
- const { jwt } = yield* uploadAssets(
1557
- accountId,
1558
- name,
1559
- assets,
1560
- session,
1561
- );
1562
- metadataAssets = {
1563
- jwt,
1564
- config: assets.config,
1565
- };
1566
- } else {
1567
- yield* Effect.logInfo(
1568
- `Cloudflare Worker update: reusing existing assets for ${name}`,
1569
- );
1570
- metadataAssets = {
1571
- config: assets.config,
1572
- };
1573
- keepAssets = true;
1574
- }
1641
+ // Always upload assets on every deploy. The "skip if hash
1642
+ // unchanged" optimization was the source of subtle bundle/
1643
+ // asset desync bugs (deploy succeeds but worker serves 404s
1644
+ // because the bundle references hashed asset filenames that
1645
+ // aren't in the still-on-Cloudflare manifest). The upload
1646
+ // session is content-addressed on Cloudflare's side — only
1647
+ // genuinely-new asset bytes are PUT; unchanged assets cost
1648
+ // a manifest check and nothing else, so the optimization
1649
+ // wasn't worth the failure mode.
1650
+ yield* Effect.logInfo(
1651
+ `Cloudflare Worker ${olds ? "update" : "create"}: uploading assets for ${name}`,
1652
+ );
1653
+ const { jwt } = yield* uploadAssets(
1654
+ accountId,
1655
+ name,
1656
+ assets,
1657
+ session,
1658
+ );
1659
+ metadataAssets = {
1660
+ jwt,
1661
+ config: assets.config,
1662
+ };
1575
1663
  metadataBindings.push({
1576
1664
  type: "assets",
1577
1665
  name: "ASSETS",
@@ -1868,11 +1956,19 @@ ${[
1868
1956
  const input = yield* hashDirectory(props.vite);
1869
1957
  return input !== output.hash?.input;
1870
1958
  }
1959
+ // Always recompute both hashes by walking `dist/` (assets) and
1960
+ // re-bundling the worker. The previous short-circuit
1961
+ // (`Effect.succeed(output.hash.assets)`) compared the cached
1962
+ // hash to itself and could never detect drift, and the
1963
+ // `props.assets.hash` form is the *input* (source) hash which
1964
+ // misses non-deterministic build output (e.g. Astro/Vite
1965
+ // shuffling content-hashed filenames between builds). The
1966
+ // result of either shortcut was deploying a new bundle on top
1967
+ // of stale assets and 404'ing in production. Re-walking is the
1968
+ // only correct comparison.
1871
1969
  const [assetsHash, bundleHash] = yield* Effect.all(
1872
1970
  [
1873
- "assets" in output && output.hash?.assets
1874
- ? Effect.succeed(output.hash.assets)
1875
- : prepareAssets(props.assets).pipe(Effect.map((a) => a?.hash)),
1971
+ prepareAssets(props.assets).pipe(Effect.map((a) => a?.hash)),
1876
1972
  prepareBundle(id, props).pipe(Effect.map((b) => b.hash)),
1877
1973
  ],
1878
1974
  { concurrency: "unbounded" },
@@ -0,0 +1,234 @@
1
+ import cloudflareRolldown from "@distilled.cloud/cloudflare-rolldown-plugin";
2
+ import * as Effect from "effect/Effect";
3
+ import * as FileSystem from "effect/FileSystem";
4
+ import { flow } from "effect/Function";
5
+ import type * as Path from "effect/Path";
6
+ import * as Stream from "effect/Stream";
7
+ import type * as rolldown from "rolldown";
8
+ import * as Artifacts from "../../Artifacts.ts";
9
+ import * as Bundle from "../../Bundle/Bundle.ts";
10
+ import { findCwdForBundle } from "../../Bundle/TempRoot.ts";
11
+ import { Self } from "../../Self.ts";
12
+ import {
13
+ isDurableObjectExport,
14
+ type DurableObjectExport,
15
+ } from "./DurableObjectNamespace.ts";
16
+ import { isWorkflowExport, type WorkflowExport } from "./Workflow.ts";
17
+
18
+ export interface WorkerBundleOptions {
19
+ id: string;
20
+ main: string;
21
+ compatibility: {
22
+ date: string;
23
+ flags: string[];
24
+ };
25
+ entry:
26
+ | {
27
+ kind: "external";
28
+ }
29
+ | {
30
+ kind: "effect";
31
+ exports: Record<string, DurableObjectExport | WorkflowExport>;
32
+ };
33
+ stack: { name: string; stage: string };
34
+ }
35
+
36
+ export const WorkerBundle = Effect.gen(function* () {
37
+ const fs = yield* FileSystem.FileSystem;
38
+ const context = yield* Effect.context<FileSystem.FileSystem | Path.Path>();
39
+ const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
40
+
41
+ const makeOptions = Effect.fnUntraced(function* (
42
+ options: WorkerBundleOptions,
43
+ ) {
44
+ const realMain = yield* fs.realPath(options.main).pipe(
45
+ Effect.mapError(
46
+ (cause) =>
47
+ new Bundle.BundleError({
48
+ message: `Failed to find real path for bundle: ${options.main}`,
49
+ cause,
50
+ }),
51
+ ),
52
+ );
53
+ const inputOptions: rolldown.InputOptions = {
54
+ input: realMain,
55
+ cwd: yield* findCwdForBundle(realMain).pipe(
56
+ Effect.mapError(
57
+ (cause) =>
58
+ new Bundle.BundleError({
59
+ message: `Failed to find cwd for bundle: ${realMain}`,
60
+ cause,
61
+ }),
62
+ ),
63
+ Effect.provide(context),
64
+ ),
65
+ plugins: [
66
+ cloudflareRolldown({
67
+ compatibilityDate: options.compatibility.date,
68
+ compatibilityFlags: options.compatibility.flags,
69
+ }),
70
+ options.entry.kind === "effect"
71
+ ? virtualEntryPlugin(
72
+ makeEffectVirtualEntry(options.entry.exports, options.stack),
73
+ )
74
+ : undefined,
75
+ ],
76
+ checks: {
77
+ // Suppress unresolved import warnings for unrelated AWS packages
78
+ unresolvedImport: false,
79
+ },
80
+ };
81
+ const outputOptions: rolldown.OutputOptions = {
82
+ format: "esm",
83
+ sourcemap: "hidden",
84
+ minify: true,
85
+ keepNames: true,
86
+ dir: `.alchemy/bundles/${options.id}`,
87
+ };
88
+ return { inputOptions, outputOptions };
89
+ });
90
+
91
+ return {
92
+ build: flow(
93
+ makeOptions,
94
+ Effect.flatMap((resolved) =>
95
+ Bundle.build(resolved.inputOptions, resolved.outputOptions),
96
+ ),
97
+ Artifacts.cached("build"),
98
+ ),
99
+ watch: flow(
100
+ makeOptions,
101
+ Stream.fromEffect,
102
+ Stream.flatMap((resolved) =>
103
+ Bundle.watch(resolved.inputOptions, resolved.outputOptions),
104
+ ),
105
+ ),
106
+ };
107
+ });
108
+
109
+ const makeEffectVirtualEntry = (
110
+ exports: Record<string, DurableObjectExport | WorkflowExport>,
111
+ stack: { name: string; stage: string },
112
+ ) => {
113
+ const durableObjectClasses: string[] = [];
114
+ const workflowClasses: string[] = [];
115
+ for (const [className, entry] of Object.entries(exports)) {
116
+ if (isDurableObjectExport(entry)) {
117
+ durableObjectClasses.push(className);
118
+ } else if (isWorkflowExport(entry)) {
119
+ workflowClasses.push(className);
120
+ }
121
+ }
122
+ return (importPath: string) => `import * as Config from "effect/Config";
123
+ import * as ConfigProvider from "effect/ConfigProvider";
124
+ import * as Console from "effect/Console";
125
+ import * as Effect from "effect/Effect";
126
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
127
+ import * as Layer from "effect/Layer";
128
+ import * as Logger from "effect/Logger";
129
+ import * as Context from "effect/Context";
130
+ import * as Stream from "effect/Stream";
131
+
132
+ import { env, DurableObject${workflowClasses.length > 0 ? ", WorkflowEntrypoint" : ""} } from "cloudflare:workers";
133
+ import { MinimumLogLevel } from "effect/References";
134
+ import { NodeServices } from "@effect/platform-node";
135
+ import { Stack } from "alchemy/Stack";
136
+ import { WorkerEnvironment, makeDurableObjectBridge${workflowClasses.length > 0 ? ", makeWorkflowBridge" : ""}, ExportedHandlerMethods } from "alchemy/Cloudflare";
137
+
138
+ import entry from "${importPath}";
139
+
140
+ const tag = Context.Service("${Self.key}")
141
+ const layer =
142
+ typeof entry?.build === "function"
143
+ ? entry
144
+ : Layer.effect(tag, typeof entry?.asEffect === "function" ? entry.asEffect() : entry);
145
+
146
+ const platform = Layer.mergeAll(
147
+ NodeServices.layer,
148
+ FetchHttpClient.layer,
149
+ // TODO(sam): wire this up to telemetry more directly
150
+ Logger.layer([Logger.consolePretty()]),
151
+ );
152
+
153
+ const stack = Layer.succeed(
154
+ Stack,
155
+ {
156
+ name: "${stack.name}",
157
+ stage: "${stack.stage}",
158
+ bindings: {},
159
+ resources: {}
160
+ }
161
+ );
162
+
163
+ const exportsEffect = tag.asEffect().pipe(
164
+ Effect.flatMap(func => func.ExecutionContext.exports),
165
+ Effect.map(exports => exports),
166
+ Effect.provide(
167
+ layer.pipe(
168
+ Layer.provideMerge(stack),
169
+ // TODO(sam): additional credentials?
170
+ Layer.provideMerge(platform),
171
+ Layer.provideMerge(
172
+ Layer.succeed(
173
+ ConfigProvider.ConfigProvider,
174
+ ConfigProvider.orElse(
175
+ ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
176
+ ConfigProvider.fromUnknown(env),
177
+ ),
178
+ )
179
+ ),
180
+ Layer.provideMerge(
181
+ Layer.succeed(
182
+ WorkerEnvironment,
183
+ env,
184
+ )
185
+ ),
186
+ Layer.provideMerge(
187
+ Layer.succeed(
188
+ MinimumLogLevel,
189
+ env.DEBUG ? "Debug" : "Info",
190
+ )
191
+ ),
192
+ )
193
+ ),
194
+ Effect.scoped
195
+ );
196
+
197
+ // TODO(sam): we could kick this off during module init, but any I/O will break deploy
198
+ // let exportsPromise = Effect.runPromise(exportsEffect);
199
+
200
+ // for now, we delay initializing the worker until the first request
201
+ let exportsPromise;
202
+
203
+ // don't initialize the workerEffect during module init because Cloudflare does not allow I/O during module init
204
+ // we cache it synchronously (??=) to guarnatee only one initialization ever happens
205
+ const getExports = () => (exportsPromise ??= Effect.runPromise(exportsEffect))
206
+ const getExport = (name) => getExports().then(exports => exports[name]?.make)
207
+ const worker = () => getExports().then(exports => exports.default)
208
+
209
+ export default Object.fromEntries(ExportedHandlerMethods.map(
210
+ method => [method, async (...args) => (await worker())[method](...args)])
211
+ ) satisfies Required<cf.ExportedHandler>;
212
+
213
+ // export class proxy stubs for Durable Objects and Workflows
214
+ ${[
215
+ ...(durableObjectClasses.length > 0
216
+ ? [
217
+ "const DurableObjectBridge = makeDurableObjectBridge(DurableObject, getExport);",
218
+ ...durableObjectClasses.map(
219
+ (id) =>
220
+ `export class ${id} extends DurableObjectBridge("${id}") {}`,
221
+ ),
222
+ ]
223
+ : []),
224
+ ...(workflowClasses.length > 0
225
+ ? [
226
+ "const WorkflowBridgeFn = makeWorkflowBridge(WorkflowEntrypoint, getExport);",
227
+ ...workflowClasses.map(
228
+ (id) => `export class ${id} extends WorkflowBridgeFn("${id}") {}`,
229
+ ),
230
+ ]
231
+ : []),
232
+ ].join("\n")}
233
+ `;
234
+ };
@@ -0,0 +1,10 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { createPhysicalName } from "../../PhysicalName.ts";
3
+
4
+ export const createWorkerName = (id: string, name: string | undefined) =>
5
+ name
6
+ ? Effect.succeed(name)
7
+ : createPhysicalName({
8
+ id,
9
+ maxLength: 54,
10
+ }).pipe(Effect.map((name) => name.toLowerCase()));
@@ -7,6 +7,7 @@ export * from "./DynamicWorkerLoader.ts";
7
7
  export * from "./Fetch.ts";
8
8
  export * from "./HttpServer.ts";
9
9
  export * from "./InferEnv.ts";
10
+ export * from "./LocalWorkerProvider.ts";
10
11
  export * from "./Request.ts";
11
12
  export * from "./Rpc.ts";
12
13
  export * from "./ScheduledEvents.ts";
package/src/Platform.ts CHANGED
@@ -180,7 +180,7 @@ export const Platform = <
180
180
  string,
181
181
  | {
182
182
  env?: Record<string, any>;
183
- exports?: string[];
183
+ exports?: string[] | Record<string, any>;
184
184
  }
185
185
  | undefined
186
186
  >,