alchemy 0.65.1 → 0.67.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.
- package/bin/alchemy.js +139 -79
- package/bin/commands/init.ts +6 -3
- package/bin/services/cdp-manager/server.ts +3 -2
- package/bin/services/execute-alchemy.ts +33 -3
- package/bin/services/find-workspace-root.ts +48 -0
- package/lib/alchemy.d.ts +20 -3
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +18 -5
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +51 -1
- package/lib/apply.js.map +1 -1
- package/lib/cloudflare/bindings.d.ts +17 -5
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bindings.js +3 -1
- package/lib/cloudflare/bindings.js.map +1 -1
- package/lib/cloudflare/bound.d.ts +4 -3
- package/lib/cloudflare/bound.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +1 -1
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/bundle/internal-worker-bundle.d.ts +1 -1
- package/lib/cloudflare/bundle/internal-worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/bundle/internal-worker-bundle.js.map +1 -1
- package/lib/cloudflare/cloudflare-env-proxy.js +3 -3
- package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/d1-database.d.ts.map +1 -1
- package/lib/cloudflare/d1-database.js +2 -1
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/cloudflare/d1-local-migrations.d.ts +1 -0
- package/lib/cloudflare/d1-local-migrations.d.ts.map +1 -1
- package/lib/cloudflare/d1-local-migrations.js +3 -2
- package/lib/cloudflare/d1-local-migrations.js.map +1 -1
- package/lib/cloudflare/hyperdrive-ref.d.ts +23 -0
- package/lib/cloudflare/hyperdrive-ref.d.ts.map +1 -0
- package/lib/cloudflare/hyperdrive-ref.js +52 -0
- package/lib/cloudflare/hyperdrive-ref.js.map +1 -0
- package/lib/cloudflare/index.d.ts +2 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +2 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/kv-namespace.js +1 -1
- package/lib/cloudflare/kv-namespace.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts +2 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +11 -6
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/delete.d.ts +3 -2
- package/lib/cloudflare/miniflare/delete.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/delete.js +13 -12
- package/lib/cloudflare/miniflare/delete.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts +3 -1
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.js +29 -13
- package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +24 -13
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +95 -79
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
- package/lib/cloudflare/miniflare/paths.d.ts +4 -4
- package/lib/cloudflare/miniflare/paths.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/paths.js +10 -4
- package/lib/cloudflare/miniflare/paths.js.map +1 -1
- package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts +2 -0
- package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts.map +1 -0
- package/lib/cloudflare/miniflare/symlink-miniflare-state.js +19 -0
- package/lib/cloudflare/miniflare/symlink-miniflare-state.js.map +1 -0
- package/lib/cloudflare/miniflare/tunnel.d.ts +18 -0
- package/lib/cloudflare/miniflare/tunnel.d.ts.map +1 -0
- package/lib/cloudflare/miniflare/tunnel.js +79 -0
- package/lib/cloudflare/miniflare/tunnel.js.map +1 -0
- package/lib/cloudflare/next.d.ts.map +1 -1
- package/lib/cloudflare/next.js +3 -0
- package/lib/cloudflare/next.js.map +1 -1
- package/lib/cloudflare/nuxt/plugin.d.ts.map +1 -1
- package/lib/cloudflare/nuxt/plugin.js +3 -3
- package/lib/cloudflare/nuxt/plugin.js.map +1 -1
- package/lib/cloudflare/quick-tunnel.d.ts +1 -1
- package/lib/cloudflare/redwood/plugin.js +2 -2
- package/lib/cloudflare/redwood/plugin.js.map +1 -1
- package/lib/cloudflare/secret-ref.d.ts +38 -0
- package/lib/cloudflare/secret-ref.d.ts.map +1 -0
- package/lib/cloudflare/secret-ref.js +38 -0
- package/lib/cloudflare/secret-ref.js.map +1 -0
- package/lib/cloudflare/vite/plugin.d.ts.map +1 -1
- package/lib/cloudflare/vite/plugin.js +6 -3
- package/lib/cloudflare/vite/plugin.js.map +1 -1
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +5 -16
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +7 -9
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +16 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +16 -2
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +2 -1
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +5 -6
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/llms.cloudflare.txt +1 -1
- package/lib/scope.d.ts +26 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +12 -5
- package/lib/scope.js.map +1 -1
- package/lib/state/file-system-state-store.d.ts.map +1 -1
- package/lib/state/file-system-state-store.js +1 -2
- package/lib/state/file-system-state-store.js.map +1 -1
- package/lib/state/sqlite-state-store.d.ts +11 -0
- package/lib/state/sqlite-state-store.d.ts.map +1 -1
- package/lib/state/sqlite-state-store.js +22 -14
- package/lib/state/sqlite-state-store.js.map +1 -1
- package/lib/stripe/customer.d.ts +0 -8
- package/lib/stripe/customer.d.ts.map +1 -1
- package/lib/stripe/customer.js +0 -6
- package/lib/stripe/customer.js.map +1 -1
- package/lib/stripe/price.d.ts +23 -23
- package/lib/stripe/price.d.ts.map +1 -1
- package/lib/stripe/price.js +6 -22
- package/lib/stripe/price.js.map +1 -1
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +1 -1
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/assert-never.js +1 -1
- package/lib/util/assert-never.js.map +1 -1
- package/lib/util/cli.d.ts.map +1 -1
- package/lib/util/cli.js +12 -13
- package/lib/util/cli.js.map +1 -1
- package/lib/util/idempotent-spawn.d.ts.map +1 -1
- package/lib/util/idempotent-spawn.js +5 -8
- package/lib/util/idempotent-spawn.js.map +1 -1
- package/lib/util/root-dir.d.ts +2 -0
- package/lib/util/root-dir.d.ts.map +1 -0
- package/lib/util/root-dir.js +2 -0
- package/lib/util/root-dir.js.map +1 -0
- package/package.json +4 -4
- package/src/alchemy.ts +43 -8
- package/src/apply.ts +58 -3
- package/src/cloudflare/bindings.ts +21 -4
- package/src/cloudflare/bound.ts +50 -52
- package/src/cloudflare/bucket.ts +1 -1
- package/src/cloudflare/bundle/internal-worker-bundle.ts +2 -1
- package/src/cloudflare/cloudflare-env-proxy.ts +5 -5
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/d1-database.ts +2 -1
- package/src/cloudflare/d1-local-migrations.ts +10 -6
- package/src/cloudflare/hyperdrive-ref.ts +98 -0
- package/src/cloudflare/index.ts +2 -0
- package/src/cloudflare/kv-namespace.ts +1 -1
- package/src/cloudflare/miniflare/build-worker-options.ts +21 -12
- package/src/cloudflare/miniflare/delete.ts +19 -11
- package/src/cloudflare/miniflare/miniflare-controller.ts +33 -14
- package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +122 -102
- package/src/cloudflare/miniflare/paths.ts +11 -7
- package/src/cloudflare/miniflare/symlink-miniflare-state.ts +19 -0
- package/src/cloudflare/miniflare/tunnel.ts +116 -0
- package/src/cloudflare/next.ts +3 -0
- package/src/cloudflare/nuxt/plugin.ts +12 -3
- package/src/cloudflare/redwood/plugin.ts +2 -2
- package/src/cloudflare/secret-ref.ts +56 -0
- package/src/cloudflare/vite/plugin.ts +8 -3
- package/src/cloudflare/website.ts +5 -19
- package/src/cloudflare/worker-metadata.ts +14 -12
- package/src/cloudflare/worker.ts +36 -3
- package/src/cloudflare/wrangler.json.ts +14 -11
- package/src/llms.cloudflare.txt +1 -1
- package/src/scope.ts +38 -6
- package/src/state/file-system-state-store.ts +1 -2
- package/src/state/sqlite-state-store.ts +41 -14
- package/src/stripe/customer.ts +0 -14
- package/src/stripe/price.ts +59 -56
- package/src/test/vitest.ts +4 -1
- package/src/util/assert-never.ts +1 -1
- package/src/util/cli.ts +15 -13
- package/src/util/idempotent-spawn.ts +5 -6
- package/src/util/root-dir.ts +1 -0
- package/templates/nextjs/alchemy.run.ts +2 -2
- package/templates/rwsdk/.cursor/rules/alchemy_cloudflare.mdc +1 -1
- package/templates/rwsdk/package.json +3 -3
- package/workers/cloudflare-state-store.js +1 -1
- package/workers/tunnel-proxy.js +110 -0
- package/workers/tunnel-proxy.ts +121 -0
|
@@ -1,126 +1,108 @@
|
|
|
1
1
|
import * as miniflare from "miniflare";
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import http from "node:http";
|
|
4
|
-
import type Stream from "node:stream";
|
|
5
4
|
import { Readable } from "node:stream";
|
|
6
5
|
import { WebSocket, WebSocketServer } from "ws";
|
|
7
|
-
import { logger } from "../../util/logger.ts";
|
|
8
6
|
|
|
9
|
-
interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
miniflare: miniflare.Miniflare;
|
|
7
|
+
export interface MiniflareWorkerProxy {
|
|
8
|
+
url: URL;
|
|
9
|
+
close: () => Promise<void>;
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
await this.handleRequest(req, res);
|
|
25
|
-
});
|
|
26
|
-
this.server.listen(this.options.port);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
get ready() {
|
|
30
|
-
if (!this.server.listening) {
|
|
31
|
-
return once(this.server, "listening");
|
|
32
|
-
}
|
|
33
|
-
return Promise.resolve();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
get url() {
|
|
37
|
-
return `http://localhost:${this.options.port}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async close() {
|
|
41
|
-
await Promise.all([
|
|
42
|
-
new Promise((resolve) => this.wss.close(resolve)),
|
|
43
|
-
new Promise((resolve) => this.server.close(resolve)),
|
|
44
|
-
]);
|
|
45
|
-
}
|
|
12
|
+
export async function createMiniflareWorkerProxy(options: {
|
|
13
|
+
port: number;
|
|
14
|
+
transformRequest?: (request: RequestInfo) => void;
|
|
15
|
+
getWorkerName: (request: miniflare.Request) => string;
|
|
16
|
+
miniflare: miniflare.Miniflare;
|
|
17
|
+
mode: "local" | "remote";
|
|
18
|
+
}) {
|
|
19
|
+
const server = http.createServer();
|
|
20
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
46
21
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
22
|
+
server.on("upgrade", async (req, socket, head) => {
|
|
23
|
+
try {
|
|
24
|
+
const server = await createServerWebSocket(req);
|
|
25
|
+
wss.handleUpgrade(req, socket, head, (client) => {
|
|
26
|
+
client.on("message", (event, binary) => server.send(event, { binary }));
|
|
27
|
+
client.on("close", (code, reason) => server.close(code, reason));
|
|
28
|
+
server.on("message", (event, binary) => client.send(event, { binary }));
|
|
29
|
+
server.on("close", (code, reason) => client.close(code, reason));
|
|
30
|
+
wss.emit("connection", client, req);
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(error);
|
|
54
34
|
socket.destroy();
|
|
55
|
-
return;
|
|
56
35
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
if (!target) {
|
|
71
|
-
logger.error(
|
|
72
|
-
`[Alchemy] Websocket connection failed: The worker "${this.options.name}" is not running.`,
|
|
36
|
+
});
|
|
37
|
+
server.on("request", async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const request = toMiniflareRequest(req);
|
|
40
|
+
const name = options.getWorkerName(request);
|
|
41
|
+
const worker = await options.miniflare.getWorker(name);
|
|
42
|
+
const response = await worker.fetch(request);
|
|
43
|
+
writeMiniflareResponseToNode(response, res);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(error);
|
|
46
|
+
const response = MiniflareWorkerProxyError.fromUnknown(error).toResponse(
|
|
47
|
+
options.mode,
|
|
73
48
|
);
|
|
74
|
-
|
|
49
|
+
writeMiniflareResponseToNode(response, res);
|
|
75
50
|
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const toMiniflareRequest = (req: http.IncomingMessage): miniflare.Request => {
|
|
54
|
+
const info = parseIncomingMessage(req);
|
|
55
|
+
options.transformRequest?.(info);
|
|
56
|
+
return new miniflare.Request(info.url, {
|
|
57
|
+
method: info.method,
|
|
58
|
+
headers: info.headers,
|
|
59
|
+
body: info.body,
|
|
60
|
+
redirect: info.redirect,
|
|
61
|
+
duplex: info.duplex,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const createServerWebSocket = async (req: http.IncomingMessage) => {
|
|
66
|
+
const name = options.getWorkerName(toMiniflareRequest(req));
|
|
67
|
+
const target = await options.miniflare.unsafeGetDirectURL(name);
|
|
76
68
|
const url = new URL(req.url ?? "/", target);
|
|
77
69
|
url.protocol = url.protocol.replace("http", "ws");
|
|
78
70
|
const protocols = req.headers["sec-websocket-protocol"]
|
|
79
71
|
?.split(",")
|
|
80
72
|
.map((p) => p.trim());
|
|
81
73
|
const server = new WebSocket(url, protocols);
|
|
82
|
-
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
once(server, "close", { signal: controller.signal }).then((args) => {
|
|
86
|
-
const [code, reason] = args as [number, string];
|
|
87
|
-
logger.error(
|
|
88
|
-
`[Alchemy] Websocket connection failed for worker "${this.options.name}": ${code} ${reason}`,
|
|
89
|
-
);
|
|
90
|
-
return null;
|
|
91
|
-
}),
|
|
92
|
-
]).finally(() => controller.abort());
|
|
93
|
-
}
|
|
74
|
+
await once(server, "open");
|
|
75
|
+
return server;
|
|
76
|
+
};
|
|
94
77
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
}
|
|
78
|
+
server.listen(options.port);
|
|
79
|
+
await once(server, "listening");
|
|
80
|
+
const url = new URL(`http://localhost:${options.port}`);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
url,
|
|
84
|
+
close: async () => {
|
|
85
|
+
await Promise.all([
|
|
86
|
+
new Promise((resolve) => server.close(resolve)),
|
|
87
|
+
new Promise((resolve) => wss.close(resolve)),
|
|
88
|
+
]);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface RequestInfo {
|
|
94
|
+
method: string;
|
|
95
|
+
url: URL;
|
|
96
|
+
headers: miniflare.Headers;
|
|
97
|
+
body: miniflare.BodyInit | undefined;
|
|
98
|
+
redirect: "manual";
|
|
99
|
+
duplex: "half" | undefined;
|
|
119
100
|
}
|
|
120
101
|
|
|
121
|
-
const
|
|
102
|
+
const parseIncomingMessage = (req: http.IncomingMessage): RequestInfo => {
|
|
122
103
|
const method = req.method ?? "GET";
|
|
123
|
-
const
|
|
104
|
+
const protocol = req.headers["x-forwarded-proto"] ?? "http";
|
|
105
|
+
const url = new URL(req.url ?? "/", `${protocol}://${req.headers.host}`);
|
|
124
106
|
const headers = new miniflare.Headers();
|
|
125
107
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
126
108
|
if (Array.isArray(value)) {
|
|
@@ -135,13 +117,14 @@ const toMiniflareRequest = (req: http.IncomingMessage) => {
|
|
|
135
117
|
["GET", "HEAD", "OPTIONS"].includes(method) || !req.readable
|
|
136
118
|
? undefined
|
|
137
119
|
: Readable.toWeb(req);
|
|
138
|
-
return
|
|
120
|
+
return {
|
|
139
121
|
method,
|
|
122
|
+
url,
|
|
140
123
|
headers,
|
|
141
124
|
body,
|
|
142
125
|
redirect: "manual",
|
|
143
126
|
duplex: body ? "half" : undefined,
|
|
144
|
-
}
|
|
127
|
+
};
|
|
145
128
|
};
|
|
146
129
|
|
|
147
130
|
const writeMiniflareResponseToNode = (
|
|
@@ -159,3 +142,40 @@ const writeMiniflareResponseToNode = (
|
|
|
159
142
|
out.end();
|
|
160
143
|
}
|
|
161
144
|
};
|
|
145
|
+
|
|
146
|
+
export class MiniflareWorkerProxyError extends Error {
|
|
147
|
+
constructor(
|
|
148
|
+
message: string,
|
|
149
|
+
readonly status: number,
|
|
150
|
+
options?: ErrorOptions,
|
|
151
|
+
) {
|
|
152
|
+
super(message, options);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
toResponse(mode: "local" | "remote"): miniflare.Response {
|
|
156
|
+
let text = `[Alchemy] ${this.message}`;
|
|
157
|
+
if (mode === "local" && this.cause) {
|
|
158
|
+
const cause =
|
|
159
|
+
this.cause instanceof Error
|
|
160
|
+
? (this.cause.stack ?? this.cause.message)
|
|
161
|
+
: String(this.cause);
|
|
162
|
+
text += `\n\n${cause}`;
|
|
163
|
+
}
|
|
164
|
+
return new miniflare.Response(text, {
|
|
165
|
+
status: this.status,
|
|
166
|
+
headers: {
|
|
167
|
+
"Content-Type": "text/plain",
|
|
168
|
+
"Alchemy-Error": this.message,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static fromUnknown(error: unknown): MiniflareWorkerProxyError {
|
|
174
|
+
if (error instanceof MiniflareWorkerProxyError) {
|
|
175
|
+
return error;
|
|
176
|
+
}
|
|
177
|
+
return new MiniflareWorkerProxyError("An unknown error occurred.", 500, {
|
|
178
|
+
cause: error,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { existsSync, readlinkSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ALCHEMY_ROOT } from "../../util/root-dir.ts";
|
|
3
5
|
|
|
4
6
|
const dynamicImportContext = new AsyncLocalStorage<boolean>();
|
|
5
7
|
|
|
@@ -10,13 +12,15 @@ export const withSkipPathValidation = <T>(callback: () => T) => {
|
|
|
10
12
|
return dynamicImportContext.run(true, callback);
|
|
11
13
|
};
|
|
12
14
|
|
|
13
|
-
export const
|
|
14
|
-
|
|
15
|
+
export const getDefaultConfigPath = (rootDir: string = process.cwd()) => {
|
|
16
|
+
return path.join(rootDir, ".alchemy", "local", "wrangler.jsonc");
|
|
17
|
+
};
|
|
15
18
|
|
|
16
|
-
export const
|
|
17
|
-
path
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
export const getDefaultPersistPath = (rootDir: string = ALCHEMY_ROOT) => {
|
|
20
|
+
return path.join(rootDir, ".alchemy", "miniflare", "v3");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const validateConfigPath = (path: string, throws = true) => {
|
|
20
24
|
if (!existsSync(path)) {
|
|
21
25
|
warnOrThrow(
|
|
22
26
|
`The Wrangler config path, "${path}", could not be found. Please run \`alchemy dev\` or \`alchemy deploy\` to create it.`,
|
|
@@ -31,7 +35,7 @@ const DEFAULT_VALIDATE_PERSIST =
|
|
|
31
35
|
!process.argv.some((arg) => ["build", "prepare", "typegen"].includes(arg));
|
|
32
36
|
|
|
33
37
|
export const validatePersistPath = (
|
|
34
|
-
path
|
|
38
|
+
path: string,
|
|
35
39
|
throws = DEFAULT_VALIDATE_PERSIST,
|
|
36
40
|
) => {
|
|
37
41
|
try {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getDefaultPersistPath } from "./paths.ts";
|
|
4
|
+
|
|
5
|
+
export async function writeMiniflareSymlink(rootDir: string, cwd: string) {
|
|
6
|
+
const target = path.resolve(getDefaultPersistPath(cwd));
|
|
7
|
+
await fs.mkdir(target, { recursive: true });
|
|
8
|
+
if (cwd === process.cwd()) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const persistPath = path.resolve(cwd, getDefaultPersistPath(rootDir));
|
|
12
|
+
console.log("link", target, persistPath);
|
|
13
|
+
await fs.mkdir(path.dirname(persistPath), { recursive: true });
|
|
14
|
+
await fs.symlink(target, persistPath).catch((e) => {
|
|
15
|
+
if (e.code !== "EEXIST") {
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type * as miniflare from "miniflare";
|
|
2
|
+
import { Scope } from "../../scope.ts";
|
|
3
|
+
import { findOpenPort } from "../../util/find-open-port.ts";
|
|
4
|
+
import type { CloudflareApi } from "../api.ts";
|
|
5
|
+
import { getInternalWorkerBundle } from "../bundle/internal-worker-bundle.ts";
|
|
6
|
+
import {
|
|
7
|
+
enableWorkerSubdomain,
|
|
8
|
+
getAccountSubdomain,
|
|
9
|
+
getWorkerSubdomain,
|
|
10
|
+
} from "../worker-subdomain.ts";
|
|
11
|
+
import { putWorker } from "../worker.ts";
|
|
12
|
+
import {
|
|
13
|
+
createMiniflareWorkerProxy,
|
|
14
|
+
MiniflareWorkerProxyError,
|
|
15
|
+
} from "./miniflare-worker-proxy.ts";
|
|
16
|
+
|
|
17
|
+
export interface Tunnel {
|
|
18
|
+
/**
|
|
19
|
+
* Enables tunneling for a local worker.
|
|
20
|
+
* Returns a `workers.dev` URL that can be used to access the worker.
|
|
21
|
+
*/
|
|
22
|
+
configureWorker: (input: {
|
|
23
|
+
api: CloudflareApi;
|
|
24
|
+
name: string;
|
|
25
|
+
}) => Promise<URL>;
|
|
26
|
+
/**
|
|
27
|
+
* Closes the tunnel.
|
|
28
|
+
*/
|
|
29
|
+
close: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createTunnel(
|
|
33
|
+
miniflare: miniflare.Miniflare,
|
|
34
|
+
): Promise<Tunnel> {
|
|
35
|
+
const workers = new Set<string>(); // used to avoid exposing workers that are not running with tunneling enabled
|
|
36
|
+
const proxy = await createMiniflareWorkerProxy({
|
|
37
|
+
port: await findOpenPort(9977), // alchemy auth uses 9976, so one above that
|
|
38
|
+
miniflare,
|
|
39
|
+
mode: "remote",
|
|
40
|
+
transformRequest: (request) => {
|
|
41
|
+
if (request.url.origin === remote.origin) {
|
|
42
|
+
request.url.protocol = proxy.url.protocol;
|
|
43
|
+
request.url.host = proxy.url.host;
|
|
44
|
+
request.url.port = proxy.url.port;
|
|
45
|
+
request.headers.set("host", proxy.url.host);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
getWorkerName: (request) => {
|
|
49
|
+
const name = request.headers.get("alchemy-worker-name");
|
|
50
|
+
if (!name) {
|
|
51
|
+
throw new MiniflareWorkerProxyError(
|
|
52
|
+
"Worker name is missing from request headers. This indicates a bug in Alchemy.",
|
|
53
|
+
530,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (!workers.has(name)) {
|
|
57
|
+
throw new MiniflareWorkerProxyError(
|
|
58
|
+
`The worker "${name}" is not running with tunneling enabled.`,
|
|
59
|
+
530,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return name;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const remoteUrlString = await Scope.current.spawn("tunnel", {
|
|
66
|
+
processName: `cloudflared-${proxy.url.port}`,
|
|
67
|
+
cmd: `cloudflared tunnel --url ${proxy.url.toString()}`,
|
|
68
|
+
quiet: !process.env.DEBUG,
|
|
69
|
+
extract: (line) => {
|
|
70
|
+
const match = line.match(/https:\/\/([^\s]+)\.trycloudflare\.com/);
|
|
71
|
+
if (match) {
|
|
72
|
+
return `https://${match[1]}.trycloudflare.com`;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const remote = new URL(remoteUrlString);
|
|
77
|
+
return {
|
|
78
|
+
configureWorker: async (input) => {
|
|
79
|
+
workers.add(input.name);
|
|
80
|
+
return await createTunnelProxyWorker({
|
|
81
|
+
api: input.api,
|
|
82
|
+
name: input.name,
|
|
83
|
+
host: remote.host,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
close: async () => {
|
|
87
|
+
await proxy.close();
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function createTunnelProxyWorker(input: {
|
|
93
|
+
api: CloudflareApi;
|
|
94
|
+
name: string;
|
|
95
|
+
host: string;
|
|
96
|
+
}) {
|
|
97
|
+
const script = await getInternalWorkerBundle("tunnel-proxy");
|
|
98
|
+
const [accountSubdomain, workerSubdomainStatus] = await Promise.all([
|
|
99
|
+
getAccountSubdomain(input.api),
|
|
100
|
+
getWorkerSubdomain(input.api, input.name),
|
|
101
|
+
putWorker(input.api, {
|
|
102
|
+
workerName: input.name,
|
|
103
|
+
scriptBundle: script.bundle,
|
|
104
|
+
compatibilityDate: "2025-09-01",
|
|
105
|
+
compatibilityFlags: [],
|
|
106
|
+
bindings: {
|
|
107
|
+
WORKER_NAME: input.name,
|
|
108
|
+
TUNNEL_HOST: input.host,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
if (!workerSubdomainStatus.enabled) {
|
|
113
|
+
await enableWorkerSubdomain(input.api, input.name);
|
|
114
|
+
}
|
|
115
|
+
return new URL(`https://${input.name}.${accountSubdomain}.workers.dev`);
|
|
116
|
+
}
|
package/src/cloudflare/next.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { CloudflareOptions } from "nitropack/presets/cloudflare/types";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getDefaultConfigPath,
|
|
4
|
+
getDefaultPersistPath,
|
|
5
|
+
validateConfigPath,
|
|
6
|
+
validatePersistPath,
|
|
7
|
+
} from "../miniflare/paths.ts";
|
|
3
8
|
|
|
4
9
|
const alchemy = (
|
|
5
10
|
options: Partial<CloudflareOptions> = {},
|
|
@@ -7,8 +12,12 @@ const alchemy = (
|
|
|
7
12
|
return {
|
|
8
13
|
nodeCompat: true,
|
|
9
14
|
dev: {
|
|
10
|
-
configPath: validateConfigPath(
|
|
11
|
-
|
|
15
|
+
configPath: validateConfigPath(
|
|
16
|
+
options.dev?.configPath ?? getDefaultConfigPath(),
|
|
17
|
+
),
|
|
18
|
+
persistDir: validatePersistPath(
|
|
19
|
+
options.dev?.persistDir ?? getDefaultPersistPath(),
|
|
20
|
+
),
|
|
12
21
|
...options.dev,
|
|
13
22
|
},
|
|
14
23
|
...options,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { redwood, type RedwoodPluginOptions } from "rwsdk/vite";
|
|
2
2
|
import type { PluginOption } from "vite";
|
|
3
|
-
import {
|
|
3
|
+
import { getDefaultConfigPath } from "../miniflare/paths.ts";
|
|
4
4
|
import alchemyVite from "../vite/plugin.ts";
|
|
5
5
|
|
|
6
6
|
const alchemyRedwood = (options?: RedwoodPluginOptions): PluginOption => {
|
|
7
|
-
const configPath = options?.configPath ??
|
|
7
|
+
const configPath = options?.configPath ?? getDefaultConfigPath();
|
|
8
8
|
return [
|
|
9
9
|
redwood({
|
|
10
10
|
...options,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createCloudflareApi, type CloudflareApiOptions } from "./api.ts";
|
|
2
|
+
import { findSecretsStoreByName, SecretsStore } from "./secrets-store.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Properties for referencing an existing Secret in a Secrets Store (without managing its value)
|
|
6
|
+
*/
|
|
7
|
+
export interface SecretRefProps extends CloudflareApiOptions {
|
|
8
|
+
name: string;
|
|
9
|
+
store?: SecretsStore;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type SecretRef = {
|
|
13
|
+
type: "secrets_store_secret";
|
|
14
|
+
name: string;
|
|
15
|
+
storeId: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SecretRef references an existing secret by name in a Secrets Store.
|
|
20
|
+
*
|
|
21
|
+
* It does not create or update the secret value – use {@link Secret} for that.
|
|
22
|
+
*
|
|
23
|
+
* Behavior:
|
|
24
|
+
* - Resolves the target secrets store ID (defaults to Cloudflare's `default_secrets_store`, creating/adopting if needed)
|
|
25
|
+
* - Exposes a binding of type `secrets_store_secret` usable in Worker bindings
|
|
26
|
+
* - Update with a different name will trigger resource replacement
|
|
27
|
+
* - Delete is a no-op in Cloudflare (resource removed from state only)
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Reference an existing secret in the default store and bind to a Worker
|
|
31
|
+
* const apiKeyRef = await SecretRef("api-key-ref", { name: "API_KEY" });
|
|
32
|
+
* const worker = await Worker("my-worker", {
|
|
33
|
+
* entrypoint: "./src/worker.ts",
|
|
34
|
+
* url: true,
|
|
35
|
+
* bindings: {
|
|
36
|
+
* API_KEY: apiKeyRef,
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
*/
|
|
40
|
+
export async function SecretRef(props: SecretRefProps): Promise<SecretRef> {
|
|
41
|
+
const api = await createCloudflareApi(props);
|
|
42
|
+
|
|
43
|
+
const storeId =
|
|
44
|
+
props.store?.id ??
|
|
45
|
+
(await findSecretsStoreByName(api, SecretsStore.Default))?.id;
|
|
46
|
+
|
|
47
|
+
if (!storeId) {
|
|
48
|
+
throw new Error(`Secrets store ${SecretsStore.Default} not found`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
type: "secrets_store_secret",
|
|
53
|
+
name: props.name,
|
|
54
|
+
storeId,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -2,7 +2,8 @@ import { cloudflare, type PluginConfig } from "@cloudflare/vite-plugin";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { PluginOption } from "vite";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
getDefaultConfigPath,
|
|
6
|
+
getDefaultPersistPath,
|
|
6
7
|
validateConfigPath,
|
|
7
8
|
validatePersistPath,
|
|
8
9
|
} from "../miniflare/paths.ts";
|
|
@@ -12,7 +13,8 @@ const alchemy = (config?: PluginConfig): PluginOption => {
|
|
|
12
13
|
path: validatePersistPath(
|
|
13
14
|
typeof config?.persistState === "object"
|
|
14
15
|
? config.persistState.path
|
|
15
|
-
:
|
|
16
|
+
: // persist path should default to the /.alchemy/miniflare/v3
|
|
17
|
+
getDefaultPersistPath(),
|
|
16
18
|
),
|
|
17
19
|
};
|
|
18
20
|
if (typeof persistState === "object" && persistState.path.endsWith("v3")) {
|
|
@@ -20,7 +22,10 @@ const alchemy = (config?: PluginConfig): PluginOption => {
|
|
|
20
22
|
}
|
|
21
23
|
return cloudflare({
|
|
22
24
|
...config,
|
|
23
|
-
configPath: validateConfigPath(
|
|
25
|
+
configPath: validateConfigPath(
|
|
26
|
+
// config path doesn't need to be in the root, it can be in the app dir
|
|
27
|
+
config?.configPath ?? getDefaultConfigPath(),
|
|
28
|
+
),
|
|
24
29
|
persistState,
|
|
25
30
|
experimental: config?.experimental ?? {
|
|
26
31
|
remoteBindings: true,
|
|
@@ -10,7 +10,7 @@ import { Assets } from "./assets.ts";
|
|
|
10
10
|
import type { Bindings } from "./bindings.ts";
|
|
11
11
|
import { DEFAULT_COMPATIBILITY_DATE } from "./compatibility-date.gen.ts";
|
|
12
12
|
import { unionCompatibilityFlags } from "./compatibility-presets.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { writeMiniflareSymlink } from "./miniflare/symlink-miniflare-state.ts";
|
|
14
14
|
import { type AssetsConfig, Worker, type WorkerProps } from "./worker.ts";
|
|
15
15
|
import { WranglerJson, type WranglerJsonSpec } from "./wrangler.json.ts";
|
|
16
16
|
|
|
@@ -267,7 +267,9 @@ export async function Website<B extends Bindings>(
|
|
|
267
267
|
await fs.writeFile(paths.entrypoint, content);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
if (Scope.current.local) {
|
|
271
|
+
await writeMiniflareSymlink(Scope.current.rootDir, paths.cwd);
|
|
272
|
+
}
|
|
271
273
|
|
|
272
274
|
await WranglerJson({
|
|
273
275
|
path: path.relative(paths.cwd, paths.wrangler.path),
|
|
@@ -293,6 +295,7 @@ export async function Website<B extends Bindings>(
|
|
|
293
295
|
...env,
|
|
294
296
|
...(typeof build === "object" ? build.env : {}),
|
|
295
297
|
NODE_ENV: "production",
|
|
298
|
+
ALCHEMY_ROOT: Scope.current.rootDir,
|
|
296
299
|
},
|
|
297
300
|
memoize: typeof build === "object" ? build.memoize : undefined,
|
|
298
301
|
});
|
|
@@ -350,20 +353,3 @@ export async function Website<B extends Bindings>(
|
|
|
350
353
|
dev: url ? { url } : undefined,
|
|
351
354
|
})) as Website<B>;
|
|
352
355
|
}
|
|
353
|
-
|
|
354
|
-
async function writeMiniflareSymlink(cwd: string) {
|
|
355
|
-
const target = path.resolve(DEFAULT_PERSIST_PATH);
|
|
356
|
-
await fs.mkdir(target, { recursive: true });
|
|
357
|
-
|
|
358
|
-
if (cwd === process.cwd()) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const persistPath = path.resolve(cwd, DEFAULT_PERSIST_PATH);
|
|
363
|
-
await fs.mkdir(path.dirname(persistPath), { recursive: true });
|
|
364
|
-
await fs.symlink(target, persistPath).catch((e) => {
|
|
365
|
-
if (e.code !== "EEXIST") {
|
|
366
|
-
throw e;
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
}
|