alchemy 2.0.0-beta.39 → 2.0.0-beta.40

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 (99) hide show
  1. package/bin/exec.js +5 -26
  2. package/bin/exec.js.map +1 -1
  3. package/lib/Auth/Lock.d.ts +4 -3
  4. package/lib/Auth/Lock.d.ts.map +1 -1
  5. package/lib/Auth/Lock.js +6 -5
  6. package/lib/Auth/Lock.js.map +1 -1
  7. package/lib/Axiom/Chart.d.ts +52 -2
  8. package/lib/Axiom/Chart.d.ts.map +1 -1
  9. package/lib/Bundle/Bundle.d.ts +7 -4
  10. package/lib/Bundle/Bundle.d.ts.map +1 -1
  11. package/lib/Bundle/Bundle.js +37 -14
  12. package/lib/Bundle/Bundle.js.map +1 -1
  13. package/lib/Cloudflare/AiGateway/AiGateway.d.ts +11 -11
  14. package/lib/Cloudflare/AiGateway/AiGateway.d.ts.map +1 -1
  15. package/lib/Cloudflare/AiGateway/AiGateway.js +28 -63
  16. package/lib/Cloudflare/AiGateway/AiGateway.js.map +1 -1
  17. package/lib/Cloudflare/Bridge.d.ts +3 -0
  18. package/lib/Cloudflare/Bridge.d.ts.map +1 -0
  19. package/lib/Cloudflare/Bridge.js +3 -0
  20. package/lib/Cloudflare/Bridge.js.map +1 -0
  21. package/lib/Cloudflare/Container/StartContainer.d.ts +2 -2
  22. package/lib/Cloudflare/Container/StartContainer.d.ts.map +1 -1
  23. package/lib/Cloudflare/Container/StartContainer.js +3 -3
  24. package/lib/Cloudflare/Container/StartContainer.js.map +1 -1
  25. package/lib/Cloudflare/Local/Sidecar.d.ts +45 -21
  26. package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -1
  27. package/lib/Cloudflare/Local/Sidecar.js +30 -6
  28. package/lib/Cloudflare/Local/Sidecar.js.map +1 -1
  29. package/lib/Cloudflare/Local/SidecarHandlers.d.ts +5 -2
  30. package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -1
  31. package/lib/Cloudflare/Local/SidecarHandlers.js +323 -70
  32. package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -1
  33. package/lib/Cloudflare/Local/SidecarServer.js +9 -5
  34. package/lib/Cloudflare/Local/SidecarServer.js.map +1 -1
  35. package/lib/Cloudflare/StateStore/Api.d.ts +1 -1
  36. package/lib/Cloudflare/StateStore/Api.js +1 -1
  37. package/lib/Cloudflare/StateStore/Store.d.ts +1 -1
  38. package/lib/Cloudflare/Workers/HttpServer.d.ts.map +1 -1
  39. package/lib/Cloudflare/Workers/HttpServer.js +3 -25
  40. package/lib/Cloudflare/Workers/HttpServer.js.map +1 -1
  41. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +1 -2
  42. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  43. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +16 -97
  44. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  45. package/lib/Cloudflare/Workers/Vite.d.ts +9 -0
  46. package/lib/Cloudflare/Workers/Vite.d.ts.map +1 -0
  47. package/lib/Cloudflare/Workers/Vite.js +95 -0
  48. package/lib/Cloudflare/Workers/Vite.js.map +1 -0
  49. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  50. package/lib/Cloudflare/Workers/Worker.js +9 -95
  51. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  52. package/lib/Cloudflare/Workers/WorkerBundle.d.ts +1 -2
  53. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
  54. package/lib/Cloudflare/Workers/WorkerBundle.js +5 -4
  55. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
  56. package/lib/Diff.d.ts +15 -1
  57. package/lib/Diff.d.ts.map +1 -1
  58. package/lib/Diff.js +13 -7
  59. package/lib/Diff.js.map +1 -1
  60. package/lib/Http.d.ts.map +1 -1
  61. package/lib/Http.js +12 -7
  62. package/lib/Http.js.map +1 -1
  63. package/lib/Sidecar/Lock.d.ts +4 -5
  64. package/lib/Sidecar/Lock.d.ts.map +1 -1
  65. package/lib/Sidecar/Lock.js +22 -85
  66. package/lib/Sidecar/Lock.js.map +1 -1
  67. package/lib/Sidecar/RpcHandler.d.ts +4 -1
  68. package/lib/Sidecar/RpcHandler.d.ts.map +1 -1
  69. package/lib/Sidecar/RpcHandler.js +21 -2
  70. package/lib/Sidecar/RpcHandler.js.map +1 -1
  71. package/lib/Util/PlatformServices.d.ts.map +1 -1
  72. package/lib/Util/PlatformServices.js +0 -2
  73. package/lib/Util/PlatformServices.js.map +1 -1
  74. package/lib/tsconfig.test.tsbuildinfo +1 -1
  75. package/package.json +17 -12
  76. package/src/AWS/EC2/hosted.ts +1 -1
  77. package/src/AWS/ECS/Task.ts +1 -1
  78. package/src/AWS/Lambda/Function.ts +1 -1
  79. package/src/Auth/Lock.ts +6 -5
  80. package/src/Axiom/Chart.ts +56 -2
  81. package/src/Bundle/Bundle.ts +41 -17
  82. package/src/Cloudflare/AiGateway/AiGateway.ts +43 -76
  83. package/src/Cloudflare/Bridge.ts +2 -0
  84. package/src/Cloudflare/Container/ContainerApplication.ts +1 -1
  85. package/src/Cloudflare/Container/StartContainer.ts +10 -3
  86. package/src/Cloudflare/Local/Sidecar.ts +50 -21
  87. package/src/Cloudflare/Local/SidecarHandlers.ts +395 -91
  88. package/src/Cloudflare/Local/SidecarServer.ts +12 -4
  89. package/src/Cloudflare/StateStore/Api.ts +1 -1
  90. package/src/Cloudflare/Workers/HttpServer.ts +5 -37
  91. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +18 -111
  92. package/src/Cloudflare/Workers/Vite.ts +114 -0
  93. package/src/Cloudflare/Workers/Worker.ts +27 -109
  94. package/src/Cloudflare/Workers/WorkerBundle.ts +7 -7
  95. package/src/Diff.ts +29 -8
  96. package/src/Http.ts +14 -10
  97. package/src/Sidecar/Lock.ts +30 -154
  98. package/src/Sidecar/RpcHandler.ts +67 -40
  99. package/src/Util/PlatformServices.ts +0 -2
@@ -1,12 +1,9 @@
1
1
  import type * as cf from "@cloudflare/workers-types";
2
2
  import * as Cause from "effect/Cause";
3
- import * as Context from "effect/Context";
4
3
  import * as Effect from "effect/Effect";
5
4
  import * as Option from "effect/Option";
6
5
  import type { Scope } from "effect/Scope";
7
6
  import type { HttpBodyError } from "effect/unstable/http/HttpBody";
8
- import * as EffectHttp from "effect/unstable/http/HttpEffect";
9
- import { ClientAbort } from "effect/unstable/http/HttpServerError";
10
7
  import * as HttpServerError from "effect/unstable/http/HttpServerError";
11
8
  import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
12
9
  import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
@@ -51,10 +48,6 @@ export const serveWebRequest = <Req = never>(
51
48
  Exclude<Req, HttpServerRequest.HttpServerRequest | Scope>
52
49
  > =>
53
50
  Effect.gen(function* () {
54
- const context =
55
- yield* Effect.context<
56
- Exclude<Req, HttpServerRequest.HttpServerRequest | Scope>
57
- >();
58
51
  const request = HttpServerRequest.fromWeb(
59
52
  webRequest as any as globalThis.Request,
60
53
  ).modify({
@@ -68,7 +61,9 @@ export const serveWebRequest = <Req = never>(
68
61
  }),
69
62
  });
70
63
 
71
- const safeHandler = handler.pipe(
64
+ const response = yield* handler.pipe(
65
+ Effect.provideService(HttpServerRequest.HttpServerRequest, request),
66
+ Effect.provideService(Request, webRequest as any),
72
67
  Effect.catchCause((cause) => {
73
68
  const message = Option.match(Cause.findErrorOption(cause), {
74
69
  onNone: () => "Internal Server Error",
@@ -86,34 +81,7 @@ export const serveWebRequest = <Req = never>(
86
81
  }),
87
82
  );
88
83
 
89
- const resolveSymbol = Symbol.for("@effect/platform/HttpApp/resolve");
90
- const httpApp = EffectHttp.toHandled(safeHandler, (request, response) => {
91
- response = EffectHttp.scopeTransferToStream(response);
92
- (request as any)[resolveSymbol](
93
- HttpServerResponse.toWeb(response, {
94
- withoutBody: request.method === "HEAD",
95
- context,
96
- }),
97
- );
98
- return Effect.void;
99
- });
100
-
101
- return yield* Effect.promise(() => {
102
- return new Promise<Response>((resolve) => {
103
- const contextMap = new Map<string, any>(context.mapUnsafe);
104
- contextMap.set(HttpServerRequest.HttpServerRequest.key, request);
105
- contextMap.set(Request.key, webRequest);
106
- (request as any)[resolveSymbol] = resolve;
107
- const fiber = Effect.runForkWith(Context.makeUnsafe(contextMap))(
108
- httpApp as any,
109
- );
110
- webRequest.signal?.addEventListener(
111
- "abort",
112
- () => {
113
- fiber.interruptUnsafe(undefined, ClientAbort.annotation);
114
- },
115
- { once: true },
116
- );
117
- });
84
+ return HttpServerResponse.toWeb(response, {
85
+ context: yield* Effect.context(),
118
86
  });
119
87
  }) as any;
@@ -1,130 +1,37 @@
1
- import type { HyperdriveOrigin } from "@distilled.cloud/cloudflare-runtime/Worker";
2
1
  import * as Effect from "effect/Effect";
3
- import * as Redacted from "effect/Redacted";
4
2
  import * as Provider from "../../Provider.ts";
5
- import type { ResourceBinding } from "../../Resource.ts";
6
3
  import { Stack } from "../../Stack.ts";
7
- import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
8
4
  import { Sidecar } from "../Local/Sidecar.ts";
9
- import { getCompatibility } from "./Compatibility.ts";
10
- import {
11
- getCronBindings,
12
- Worker,
13
- type WorkerBinding,
14
- type WorkerProps,
15
- } from "./Worker.ts";
16
- import { createWorkerName } from "./WorkerName.ts";
5
+ import { Worker } from "./Worker.ts";
17
6
 
18
7
  export const LocalWorkerProvider = () =>
19
8
  Provider.effect(
20
9
  Worker,
21
10
  Effect.gen(function* () {
22
- const { accountId } = yield* CloudflareEnvironment;
23
11
  const stack = yield* Stack;
24
12
  const sidecar = yield* Sidecar;
25
-
26
- const run = Effect.fn(function* (
27
- id: string,
28
- props: WorkerProps,
29
- bindings: ResourceBinding<Worker["Binding"]>[],
30
- ) {
31
- const name = yield* createWorkerName(id, props.name);
32
- const workerBindings: WorkerBinding[] = [];
33
- const durableObjectNamespaces: Record<string, string> = {};
34
- const hyperdrives: Record<string, Required<HyperdriveOrigin>> = {};
35
- for (const { sid, data } of bindings) {
36
- for (const binding of data.bindings ?? []) {
37
- workerBindings.push(binding);
38
- if (binding.type === "durable_object_namespace") {
39
- durableObjectNamespaces[binding.name] = sid;
40
- }
41
- }
42
- if (data.hyperdrives) {
43
- for (const [id, origin] of Object.entries(data.hyperdrives)) {
44
- hyperdrives[id] = {
45
- scheme: origin.scheme,
46
- host: origin.host,
47
- port: origin.port,
48
- user: origin.user,
49
- database: origin.database,
50
- password: Redacted.isRedacted(origin.password)
51
- ? Redacted.value(origin.password)
52
- : origin.password,
53
- sslmode: origin.sslmode,
54
- };
55
- }
56
- }
57
- }
58
- for (const [key, value] of Object.entries(props.env ?? {})) {
59
- if (value === undefined) continue;
60
- if (Redacted.isRedacted(value)) {
61
- workerBindings.push({
62
- type: "secret_text",
63
- name: key,
64
- text: Redacted.value(value),
65
- });
66
- } else if (typeof value === "string") {
67
- workerBindings.push({
68
- type: "plain_text",
69
- name: key,
70
- text: value,
71
- });
72
- } else {
73
- workerBindings.push({
74
- type: "json",
75
- name: key,
76
- json: value,
77
- });
78
- }
79
- }
80
- const result = yield* sidecar.serve({
81
- id,
82
- name,
83
- main: props.main,
84
- compatibility: getCompatibility(props),
85
- entry: props.isExternal
86
- ? {
87
- kind: "external",
88
- }
89
- : {
90
- kind: "effect",
91
- exports: (props.exports ?? {}) as any,
92
- },
93
- stack: { name: stack.name, stage: stack.stage },
94
- bindings: workerBindings,
95
- hyperdrives,
96
- durableObjectNamespaces: Object.entries(durableObjectNamespaces).map(
97
- ([className, namespaceId]) => ({
98
- className,
99
- uniqueKey: namespaceId,
100
- sql: true,
101
- }),
102
- ),
103
- userOptions: props.build,
104
- });
105
- return {
106
- workerId: name,
107
- workerName: name,
108
- logpush: undefined,
109
- url: result.address,
110
- tags: [],
111
- durableObjectNamespaces,
112
- domains: [],
113
- crons: Array.from(
114
- new Set([...getCronBindings(bindings), ...(props.crons ?? [])]),
115
- ),
116
- accountId,
117
- } satisfies Worker["Attributes"];
118
- });
119
-
120
13
  return {
121
- diff: () => Effect.succeed({ action: "update" }),
14
+ diff: ({ id, news, newBindings, instanceId }) =>
15
+ sidecar.diff({
16
+ id,
17
+ props: news as any,
18
+ bindings: newBindings as any,
19
+ stack,
20
+ instanceId,
21
+ }),
122
22
  // The local sidecar `serve` operation is itself a true upsert:
123
23
  // it tears down any existing process for the worker name and
124
24
  // starts a fresh one with the latest bindings, so observe and
125
25
  // sync collapse into a single sidecar call.
126
- reconcile: ({ id, news, bindings }) => run(id, news, bindings),
127
- delete: ({ output }) => sidecar.stop(output.workerName),
26
+ reconcile: ({ id, news, bindings, instanceId }) =>
27
+ sidecar.reconcile({
28
+ id,
29
+ props: news,
30
+ bindings,
31
+ stack,
32
+ instanceId,
33
+ }),
34
+ delete: ({ id }) => sidecar.delete(id),
128
35
  };
129
36
  }),
130
37
  );
@@ -0,0 +1,114 @@
1
+ import cloudflare, {
2
+ type CloudflareVitePluginOptions,
3
+ } from "@distilled.cloud/cloudflare-vite-plugin";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Redacted from "effect/Redacted";
6
+ import { createRequire } from "node:module";
7
+ import path from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ import type * as vite from "vite";
10
+
11
+ export const viteDev = (
12
+ rootDir: string = process.cwd(),
13
+ env: Record<string, unknown>,
14
+ pluginOptions: CloudflareVitePluginOptions,
15
+ ) =>
16
+ Effect.acquireRelease(
17
+ Effect.promise(async () => {
18
+ const vite = await loadVite(rootDir);
19
+ const devServer = await vite.createServer({
20
+ root: rootDir,
21
+ define: getDefine(env),
22
+ plugins: [cloudflare(pluginOptions)],
23
+ });
24
+ await devServer.listen();
25
+ return devServer;
26
+ }),
27
+ (devServer) =>
28
+ Effect.promise(async () => {
29
+ await devServer.close();
30
+ }),
31
+ );
32
+
33
+ export const viteBuild = (
34
+ rootDir: string = process.cwd(),
35
+ env: Record<string, unknown>,
36
+ pluginOptions: CloudflareVitePluginOptions,
37
+ ) =>
38
+ Effect.promise(async () => {
39
+ let serverBundle: vite.Rolldown.OutputBundle | undefined;
40
+ let assetsDirectory: string | undefined;
41
+ const vite = await loadVite(rootDir);
42
+ const builder = await vite.createBuilder(
43
+ {
44
+ root: rootDir,
45
+ define: getDefine(env),
46
+ plugins: [
47
+ cloudflare(pluginOptions),
48
+ {
49
+ name: "output:ssr",
50
+ applyToEnvironment(environment) {
51
+ return environment.name === "ssr";
52
+ },
53
+ generateBundle(_outputOptions, bundle) {
54
+ serverBundle = bundle;
55
+ },
56
+ },
57
+ {
58
+ name: "output:client",
59
+ applyToEnvironment(environment) {
60
+ return environment.name === "client";
61
+ },
62
+ generateBundle(outputOptions) {
63
+ assetsDirectory = outputOptions.dir;
64
+ },
65
+ },
66
+ ],
67
+ },
68
+ // This is the `useLegacyBuilder` option. The Vite CLI implementation uses `null` here.
69
+ // Originally we used `undefined` here, but this caused the static site build to fail.
70
+ // https://github.com/vitejs/vite/blob/a07a4bd052ac75f916391c999c408ad5f2867e61/packages/vite/src/node/cli.ts#L367
71
+ null,
72
+ );
73
+ await builder.buildApp();
74
+ return {
75
+ serverBundle,
76
+ assetsDirectory,
77
+ };
78
+ });
79
+
80
+ // Emulate `vite build` env semantics for `props.env`: only
81
+ // keys with Vite's default `VITE_` prefix are inlined into
82
+ // the bundle as `import.meta.env.*`. `Redacted` values are
83
+ // unwrapped — by prefixing with `VITE_` the user is opting
84
+ // them into the public bundle.
85
+ const getDefine = (env: Record<string, unknown>) =>
86
+ Object.fromEntries(
87
+ Object.entries(env).flatMap(([key, raw]) => {
88
+ if (!key.startsWith("VITE_")) return [];
89
+ const value = Redacted.isRedacted(raw) ? Redacted.value(raw) : raw;
90
+ return [[`import.meta.env.${key}`, JSON.stringify(value)] as const];
91
+ }),
92
+ );
93
+
94
+ type ViteModule = typeof import("vite");
95
+
96
+ /**
97
+ * Dynamically load Vite from the project root. Falls back to the bundled
98
+ * copy if the project doesn't have its own Vite installation.
99
+ */
100
+ async function loadVite(
101
+ projectRoot: string = process.cwd(),
102
+ ): Promise<ViteModule> {
103
+ try {
104
+ const require = createRequire(path.join(projectRoot, "package.json"));
105
+ const vitePath = require.resolve("vite");
106
+ // On Windows, absolute paths must be file:// URLs for ESM import().
107
+ const viteUrl = pathToFileURL(vitePath);
108
+ return await import(/* @vite-ignore */ viteUrl.href);
109
+ } catch {
110
+ // Fallback: try to import vite from the global node_modules (works for non-linked installs)
111
+ // The fallback is a bare specifier and works as-is.
112
+ return await import("vite");
113
+ }
114
+ }
@@ -1,5 +1,4 @@
1
1
  import type * as cf from "@cloudflare/workers-types";
2
- import cloudflareVite from "@distilled.cloud/cloudflare-vite-plugin";
3
2
  import * as workers from "@distilled.cloud/cloudflare/workers";
4
3
  import * as zones from "@distilled.cloud/cloudflare/zones";
5
4
  import * as Cause from "effect/Cause";
@@ -14,12 +13,9 @@ import * as Redacted from "effect/Redacted";
14
13
  import * as Schedule from "effect/Schedule";
15
14
  import * as Scope from "effect/Scope";
16
15
  import * as Stream from "effect/Stream";
17
- import { createRequire } from "node:module";
18
- import path from "node:path";
19
- import { pathToFileURL } from "node:url";
20
- import type * as vite from "vite";
21
16
  import { Unowned } from "../../AdoptPolicy.ts";
22
17
  import { AlchemyContext } from "../../AlchemyContext.ts";
18
+ import * as Artifacts from "../../Artifacts.ts";
23
19
  import * as Binding from "../../Binding.ts";
24
20
  import { hashDirectory, type MemoOptions } from "../../Build/Memo.ts";
25
21
  import * as Bundle from "../../Bundle/Bundle.ts";
@@ -91,6 +87,7 @@ import {
91
87
  type RpcErrorEnvelope,
92
88
  type RpcStreamEnvelope,
93
89
  } from "./Rpc.ts";
90
+ import * as Vite from "./Vite.ts";
94
91
  import { WorkerBundle } from "./WorkerBundle.ts";
95
92
  import { createWorkerName } from "./WorkerName.ts";
96
93
 
@@ -1592,92 +1589,35 @@ export const LiveWorkerProvider = () =>
1592
1589
  });
1593
1590
 
1594
1591
  const prepareBundle = (id: string, props: WorkerProps) =>
1595
- bundler.build({
1596
- id,
1597
- main: props.main,
1598
- compatibility: getCompatibility(props),
1599
- entry: props.isExternal
1600
- ? {
1601
- kind: "external",
1602
- }
1603
- : {
1604
- kind: "effect",
1605
- exports: (props.exports ?? {}) as any,
1606
- },
1607
- stack: { name: stack.name, stage: stack.stage },
1608
- userOptions: props.build,
1609
- });
1592
+ bundler
1593
+ .build({
1594
+ id,
1595
+ main: props.main,
1596
+ compatibility: getCompatibility(props),
1597
+ entry: props.isExternal
1598
+ ? {
1599
+ kind: "external",
1600
+ }
1601
+ : {
1602
+ kind: "effect",
1603
+ exports: (props.exports ?? {}) as any,
1604
+ },
1605
+ stack: { name: stack.name, stage: stack.stage },
1606
+ userOptions: props.build,
1607
+ })
1608
+ .pipe(Artifacts.cached("build"));
1610
1609
 
1611
1610
  const viteBuild = Effect.fnUntraced(function* (props: WorkerProps) {
1612
- let assetsDirectory: string | undefined;
1613
- let serverBundle: vite.Rolldown.OutputBundle | undefined;
1614
1611
  const compatibility = getCompatibility(props);
1612
+ const { assetsDirectory, serverBundle } = yield* Vite.viteBuild(
1613
+ props.vite?.rootDir,
1614
+ props.env ?? {},
1615
+ {
1616
+ compatibilityDate: compatibility.date,
1617
+ compatibilityFlags: compatibility.flags,
1618
+ },
1619
+ );
1615
1620
 
1616
- yield* Effect.promise(async () => {
1617
- const vite = await loadVite(props.vite?.rootDir);
1618
- const builder = await vite.createBuilder(
1619
- {
1620
- root: props.vite?.rootDir,
1621
- // Emulate `vite build` env semantics for `props.env`: only
1622
- // keys with Vite's default `VITE_` prefix are inlined into
1623
- // the bundle as `import.meta.env.*`. `Redacted` values are
1624
- // unwrapped — by prefixing with `VITE_` the user is opting
1625
- // them into the public bundle.
1626
- define: Object.fromEntries(
1627
- Object.entries(props.env ?? {}).flatMap(([key, raw]) => {
1628
- if (!key.startsWith("VITE_")) return [];
1629
- const value = Redacted.isRedacted(raw)
1630
- ? Redacted.value(raw)
1631
- : raw;
1632
- return [
1633
- [`import.meta.env.${key}`, JSON.stringify(value)] as const,
1634
- ];
1635
- }),
1636
- ),
1637
- // Declare the ssr environment so Vite 8+ creates it.
1638
- // The cloudflare-vite-plugin config hook merges its
1639
- // SSR-specific settings on top of this stub.
1640
- environments: {
1641
- ssr: {
1642
- build: {
1643
- // Prevent the SSR build from wiping the client
1644
- // build's output (both share the dist/ directory).
1645
- emptyOutDir: false,
1646
- },
1647
- },
1648
- },
1649
- plugins: [
1650
- cloudflareVite({
1651
- compatibilityDate: compatibility.date,
1652
- compatibilityFlags: compatibility.flags,
1653
- }),
1654
- {
1655
- name: "output:ssr",
1656
- applyToEnvironment(environment) {
1657
- return environment.name === "ssr";
1658
- },
1659
- generateBundle(_outputOptions, bundle) {
1660
- serverBundle = bundle;
1661
- },
1662
- },
1663
- {
1664
- name: "output:client",
1665
- applyToEnvironment(environment) {
1666
- return environment.name === "client";
1667
- },
1668
- generateBundle(outputOptions) {
1669
- assetsDirectory = outputOptions.dir;
1670
- },
1671
- },
1672
- ],
1673
- },
1674
- // This is the `useLegacyBuilder` option. The Vite CLI implementation uses `null` here.
1675
- // Originally we used `undefined` here, but this caused the static site build to fail.
1676
- // https://github.com/vitejs/vite/blob/a07a4bd052ac75f916391c999c408ad5f2867e61/packages/vite/src/node/cli.ts#L367
1677
- null,
1678
- );
1679
- await builder.buildApp();
1680
- });
1681
1621
  if (!assetsDirectory && !serverBundle) {
1682
1622
  return yield* Effect.die(
1683
1623
  new Error("Vite build produced neither server nor client output"),
@@ -2623,25 +2563,3 @@ const contentTypeFromExtension = (extension: string) => {
2623
2563
  return "application/octet-stream";
2624
2564
  }
2625
2565
  };
2626
-
2627
- type ViteModule = typeof import("vite");
2628
-
2629
- /**
2630
- * Dynamically load Vite from the project root. Falls back to the bundled
2631
- * copy if the project doesn't have its own Vite installation.
2632
- */
2633
- async function loadVite(
2634
- projectRoot: string = process.cwd(),
2635
- ): Promise<ViteModule> {
2636
- try {
2637
- const require = createRequire(path.join(projectRoot, "package.json"));
2638
- const vitePath = require.resolve("vite");
2639
- // On Windows, absolute paths must be file:// URLs for ESM import().
2640
- const viteUrl = pathToFileURL(vitePath);
2641
- return await import(/* @vite-ignore */ viteUrl.href);
2642
- } catch {
2643
- // Fallback: try to import vite from the global node_modules (works for non-linked installs)
2644
- // The fallback is a bare specifier and works as-is.
2645
- return await import("vite");
2646
- }
2647
- }
@@ -7,7 +7,6 @@ import * as Stream from "effect/Stream";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import type * as rolldown from "rolldown";
9
9
  import Sonda from "sonda/rolldown";
10
- import * as Artifacts from "../../Artifacts.ts";
11
10
  import * as Bundle from "../../Bundle/Bundle.ts";
12
11
  import { findCwdForBundle } from "../../Bundle/TempRoot.ts";
13
12
  import { Self } from "../../Self.ts";
@@ -64,9 +63,11 @@ export const WorkerBundle = Effect.gen(function* () {
64
63
  compatibilityFlags: options.compatibility.flags,
65
64
  }),
66
65
  options.entry.kind === "effect"
67
- ? virtualEntryPlugin(
68
- makeEffectVirtualEntry(options.entry.exports, options.stack),
69
- )
66
+ ? [
67
+ virtualEntryPlugin(
68
+ makeEffectVirtualEntry(options.entry.exports, options.stack),
69
+ ),
70
+ ]
70
71
  : undefined,
71
72
  ...(options.userOptions?.metafile ? [Sonda({ open: false })] : []),
72
73
  ],
@@ -116,7 +117,6 @@ export const WorkerBundle = Effect.gen(function* () {
116
117
  resolved.extraOptions,
117
118
  ),
118
119
  ),
119
- Artifacts.cached("build"),
120
120
  ),
121
121
  watch: flow(
122
122
  makeOptions,
@@ -160,12 +160,12 @@ import * as Stream from "effect/Stream";
160
160
 
161
161
  import { env, DurableObject, WorkerEntrypoint${hasWfClasses ? ", WorkflowEntrypoint" : ""} } from "cloudflare:workers";
162
162
  import { MinimumLogLevel } from "effect/References";
163
- import { NodeServices } from "@effect/platform-node";
163
+ import * as NodeServices from "@effect/platform-node/NodeServices";
164
164
  import { Stack } from "alchemy/Stack";
165
165
  import { WorkerEnvironment, makeDurableObjectBridge, makeWorkerBridge${hasWfClasses ? ", makeWorkflowBridge" : ""}, ExportedHandlerMethods } from "alchemy/Cloudflare";
166
166
  import { makeEntrypointLayer } from "alchemy/Runtime";
167
167
 
168
- import entrypoint from "${importPath}";
168
+ import entrypoint from ${JSON.stringify(importPath)};
169
169
 
170
170
  const tag = Context.Service("${Self.key}")
171
171
  const layer = makeEntrypointLayer(tag, entrypoint);
package/src/Diff.ts CHANGED
@@ -87,26 +87,47 @@ export const havePropsChanged = <Props extends object>(
87
87
  newProps: Props,
88
88
  ) =>
89
89
  Output.hasOutputs(newProps) ||
90
- JSON.stringify(canonicalize(oldProps ?? {})) !==
91
- JSON.stringify(canonicalize(newProps ?? {}));
90
+ JSON.stringify(canonicalize(oldProps ?? {}, false)) !==
91
+ JSON.stringify(canonicalize(newProps ?? {}, false));
92
+
93
+ export type DeepEqualOptions = {
94
+ /**
95
+ * When true, treat `null` and `undefined` as equivalent at any depth.
96
+ * Useful when comparing cloud-API responses (which often return `null`
97
+ * for unconfigured optional fields) against desired-state shapes built
98
+ * from `props?.x` (which leave the same fields `undefined`).
99
+ *
100
+ * @default false
101
+ */
102
+ stripNullish?: boolean;
103
+ };
92
104
 
93
105
  /**
94
106
  * Sort-keys deep equality for plain data (objects, arrays, primitives).
95
107
  * Use in provider `diff` handlers instead of ad-hoc `JSON.stringify` comparisons.
108
+ *
109
+ * By default, `null` and `undefined` are treated as distinct. Pass
110
+ * `{ stripNullish: true }` to opt into treating them as equivalent.
96
111
  */
97
- export const deepEqual = (a: unknown, b: unknown): boolean =>
98
- JSON.stringify(canonicalize(a ?? undefined)) ===
99
- JSON.stringify(canonicalize(b ?? undefined));
112
+ export const deepEqual = (
113
+ a: unknown,
114
+ b: unknown,
115
+ options?: DeepEqualOptions,
116
+ ): boolean =>
117
+ JSON.stringify(canonicalize(a, options?.stripNullish ?? false)) ===
118
+ JSON.stringify(canonicalize(b, options?.stripNullish ?? false));
100
119
 
101
- const canonicalize = (value: unknown): unknown => {
120
+ const canonicalize = (value: unknown, stripNullish: boolean): unknown => {
121
+ if (stripNullish && value == null) return undefined;
102
122
  if (Array.isArray(value)) {
103
- return value.map(canonicalize);
123
+ return value.map((v) => canonicalize(v, stripNullish));
104
124
  }
105
125
  if (value && typeof value === "object") {
106
126
  return Object.fromEntries(
107
127
  Object.entries(value)
128
+ .filter(([, nested]) => !stripNullish || nested != null)
108
129
  .sort(([a], [b]) => a.localeCompare(b))
109
- .map(([key, nested]) => [key, canonicalize(nested)]),
130
+ .map(([key, nested]) => [key, canonicalize(nested, stripNullish)]),
110
131
  );
111
132
  }
112
133
  return value;
package/src/Http.ts CHANGED
@@ -61,18 +61,22 @@ export const safeHttpEffect = <Req = never>(
61
61
  ),
62
62
  ) as any as HttpEffect<Req>,
63
63
  (cause) => {
64
- const message = Option.match(Cause.findErrorOption(cause), {
65
- onNone: () => "Internal Server Error",
66
- onSome: (error) => error.message ?? "Internal Server Error",
67
- });
68
-
69
- return Effect.map(
70
- Effect.all([Effect.logError(message), Effect.logError(cause)]),
71
- () =>
72
- HttpServerResponse.text(message, {
64
+ // ClientAbort interrupts are not real failures — the client closed
65
+ // the connection. Skip logging and respond with 499 if applicable.
66
+ if (Cause.hasInterruptsOnly(cause)) {
67
+ return Effect.succeed(HttpServerResponse.empty({ status: 499 }));
68
+ }
69
+ // Log the full cause server-side so operators can debug, but return
70
+ // a generic 500 to the client. Causes can contain sensitive data
71
+ // (prompt contents, API keys baked into error messages, internal
72
+ // file paths) and should never be echoed back to the network.
73
+ return Effect.logError("HTTP handler failed", cause).pipe(
74
+ Effect.as(
75
+ HttpServerResponse.text("Internal Server Error", {
73
76
  status: 500,
74
- statusText: message,
77
+ statusText: "Internal Server Error",
75
78
  }),
79
+ ),
76
80
  );
77
81
  },
78
82
  );