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.
- package/bin/exec.js +5 -26
- package/bin/exec.js.map +1 -1
- package/lib/Auth/Lock.d.ts +4 -3
- package/lib/Auth/Lock.d.ts.map +1 -1
- package/lib/Auth/Lock.js +6 -5
- package/lib/Auth/Lock.js.map +1 -1
- package/lib/Axiom/Chart.d.ts +52 -2
- package/lib/Axiom/Chart.d.ts.map +1 -1
- package/lib/Bundle/Bundle.d.ts +7 -4
- package/lib/Bundle/Bundle.d.ts.map +1 -1
- package/lib/Bundle/Bundle.js +37 -14
- package/lib/Bundle/Bundle.js.map +1 -1
- package/lib/Cloudflare/AiGateway/AiGateway.d.ts +11 -11
- package/lib/Cloudflare/AiGateway/AiGateway.d.ts.map +1 -1
- package/lib/Cloudflare/AiGateway/AiGateway.js +28 -63
- package/lib/Cloudflare/AiGateway/AiGateway.js.map +1 -1
- package/lib/Cloudflare/Bridge.d.ts +3 -0
- package/lib/Cloudflare/Bridge.d.ts.map +1 -0
- package/lib/Cloudflare/Bridge.js +3 -0
- package/lib/Cloudflare/Bridge.js.map +1 -0
- package/lib/Cloudflare/Container/StartContainer.d.ts +2 -2
- package/lib/Cloudflare/Container/StartContainer.d.ts.map +1 -1
- package/lib/Cloudflare/Container/StartContainer.js +3 -3
- package/lib/Cloudflare/Container/StartContainer.js.map +1 -1
- package/lib/Cloudflare/Local/Sidecar.d.ts +45 -21
- package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -1
- package/lib/Cloudflare/Local/Sidecar.js +30 -6
- package/lib/Cloudflare/Local/Sidecar.js.map +1 -1
- package/lib/Cloudflare/Local/SidecarHandlers.d.ts +5 -2
- package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -1
- package/lib/Cloudflare/Local/SidecarHandlers.js +323 -70
- package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -1
- package/lib/Cloudflare/Local/SidecarServer.js +9 -5
- package/lib/Cloudflare/Local/SidecarServer.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +1 -1
- package/lib/Cloudflare/StateStore/Api.js +1 -1
- package/lib/Cloudflare/StateStore/Store.d.ts +1 -1
- package/lib/Cloudflare/Workers/HttpServer.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/HttpServer.js +3 -25
- package/lib/Cloudflare/Workers/HttpServer.js.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +1 -2
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +16 -97
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
- package/lib/Cloudflare/Workers/Vite.d.ts +9 -0
- package/lib/Cloudflare/Workers/Vite.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/Vite.js +95 -0
- package/lib/Cloudflare/Workers/Vite.js.map +1 -0
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +9 -95
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts +1 -2
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.js +5 -4
- package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
- package/lib/Diff.d.ts +15 -1
- package/lib/Diff.d.ts.map +1 -1
- package/lib/Diff.js +13 -7
- package/lib/Diff.js.map +1 -1
- package/lib/Http.d.ts.map +1 -1
- package/lib/Http.js +12 -7
- package/lib/Http.js.map +1 -1
- package/lib/Sidecar/Lock.d.ts +4 -5
- package/lib/Sidecar/Lock.d.ts.map +1 -1
- package/lib/Sidecar/Lock.js +22 -85
- package/lib/Sidecar/Lock.js.map +1 -1
- package/lib/Sidecar/RpcHandler.d.ts +4 -1
- package/lib/Sidecar/RpcHandler.d.ts.map +1 -1
- package/lib/Sidecar/RpcHandler.js +21 -2
- package/lib/Sidecar/RpcHandler.js.map +1 -1
- package/lib/Util/PlatformServices.d.ts.map +1 -1
- package/lib/Util/PlatformServices.js +0 -2
- package/lib/Util/PlatformServices.js.map +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +17 -12
- package/src/AWS/EC2/hosted.ts +1 -1
- package/src/AWS/ECS/Task.ts +1 -1
- package/src/AWS/Lambda/Function.ts +1 -1
- package/src/Auth/Lock.ts +6 -5
- package/src/Axiom/Chart.ts +56 -2
- package/src/Bundle/Bundle.ts +41 -17
- package/src/Cloudflare/AiGateway/AiGateway.ts +43 -76
- package/src/Cloudflare/Bridge.ts +2 -0
- package/src/Cloudflare/Container/ContainerApplication.ts +1 -1
- package/src/Cloudflare/Container/StartContainer.ts +10 -3
- package/src/Cloudflare/Local/Sidecar.ts +50 -21
- package/src/Cloudflare/Local/SidecarHandlers.ts +395 -91
- package/src/Cloudflare/Local/SidecarServer.ts +12 -4
- package/src/Cloudflare/StateStore/Api.ts +1 -1
- package/src/Cloudflare/Workers/HttpServer.ts +5 -37
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +18 -111
- package/src/Cloudflare/Workers/Vite.ts +114 -0
- package/src/Cloudflare/Workers/Worker.ts +27 -109
- package/src/Cloudflare/Workers/WorkerBundle.ts +7 -7
- package/src/Diff.ts +29 -8
- package/src/Http.ts +14 -10
- package/src/Sidecar/Lock.ts +30 -154
- package/src/Sidecar/RpcHandler.ts +67 -40
- 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
|
|
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
|
-
|
|
90
|
-
|
|
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 {
|
|
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: (
|
|
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 }) =>
|
|
127
|
-
|
|
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
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
-
?
|
|
68
|
-
|
|
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
|
|
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
|
|
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 = (
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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:
|
|
77
|
+
statusText: "Internal Server Error",
|
|
75
78
|
}),
|
|
79
|
+
),
|
|
76
80
|
);
|
|
77
81
|
},
|
|
78
82
|
);
|