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.
Files changed (185) hide show
  1. package/bin/alchemy.js +139 -79
  2. package/bin/commands/init.ts +6 -3
  3. package/bin/services/cdp-manager/server.ts +3 -2
  4. package/bin/services/execute-alchemy.ts +33 -3
  5. package/bin/services/find-workspace-root.ts +48 -0
  6. package/lib/alchemy.d.ts +20 -3
  7. package/lib/alchemy.d.ts.map +1 -1
  8. package/lib/alchemy.js +18 -5
  9. package/lib/alchemy.js.map +1 -1
  10. package/lib/apply.d.ts.map +1 -1
  11. package/lib/apply.js +51 -1
  12. package/lib/apply.js.map +1 -1
  13. package/lib/cloudflare/bindings.d.ts +17 -5
  14. package/lib/cloudflare/bindings.d.ts.map +1 -1
  15. package/lib/cloudflare/bindings.js +3 -1
  16. package/lib/cloudflare/bindings.js.map +1 -1
  17. package/lib/cloudflare/bound.d.ts +4 -3
  18. package/lib/cloudflare/bound.d.ts.map +1 -1
  19. package/lib/cloudflare/bucket.js +1 -1
  20. package/lib/cloudflare/bucket.js.map +1 -1
  21. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts +1 -1
  22. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts.map +1 -1
  23. package/lib/cloudflare/bundle/internal-worker-bundle.js.map +1 -1
  24. package/lib/cloudflare/cloudflare-env-proxy.js +3 -3
  25. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  26. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  27. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  28. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  29. package/lib/cloudflare/d1-database.js +2 -1
  30. package/lib/cloudflare/d1-database.js.map +1 -1
  31. package/lib/cloudflare/d1-local-migrations.d.ts +1 -0
  32. package/lib/cloudflare/d1-local-migrations.d.ts.map +1 -1
  33. package/lib/cloudflare/d1-local-migrations.js +3 -2
  34. package/lib/cloudflare/d1-local-migrations.js.map +1 -1
  35. package/lib/cloudflare/hyperdrive-ref.d.ts +23 -0
  36. package/lib/cloudflare/hyperdrive-ref.d.ts.map +1 -0
  37. package/lib/cloudflare/hyperdrive-ref.js +52 -0
  38. package/lib/cloudflare/hyperdrive-ref.js.map +1 -0
  39. package/lib/cloudflare/index.d.ts +2 -0
  40. package/lib/cloudflare/index.d.ts.map +1 -1
  41. package/lib/cloudflare/index.js +2 -0
  42. package/lib/cloudflare/index.js.map +1 -1
  43. package/lib/cloudflare/kv-namespace.js +1 -1
  44. package/lib/cloudflare/kv-namespace.js.map +1 -1
  45. package/lib/cloudflare/miniflare/build-worker-options.d.ts +2 -1
  46. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  47. package/lib/cloudflare/miniflare/build-worker-options.js +11 -6
  48. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  49. package/lib/cloudflare/miniflare/delete.d.ts +3 -2
  50. package/lib/cloudflare/miniflare/delete.d.ts.map +1 -1
  51. package/lib/cloudflare/miniflare/delete.js +13 -12
  52. package/lib/cloudflare/miniflare/delete.js.map +1 -1
  53. package/lib/cloudflare/miniflare/miniflare-controller.d.ts +3 -1
  54. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  55. package/lib/cloudflare/miniflare/miniflare-controller.js +29 -13
  56. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  57. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +24 -13
  58. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  59. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +95 -79
  60. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  61. package/lib/cloudflare/miniflare/paths.d.ts +4 -4
  62. package/lib/cloudflare/miniflare/paths.d.ts.map +1 -1
  63. package/lib/cloudflare/miniflare/paths.js +10 -4
  64. package/lib/cloudflare/miniflare/paths.js.map +1 -1
  65. package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts +2 -0
  66. package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts.map +1 -0
  67. package/lib/cloudflare/miniflare/symlink-miniflare-state.js +19 -0
  68. package/lib/cloudflare/miniflare/symlink-miniflare-state.js.map +1 -0
  69. package/lib/cloudflare/miniflare/tunnel.d.ts +18 -0
  70. package/lib/cloudflare/miniflare/tunnel.d.ts.map +1 -0
  71. package/lib/cloudflare/miniflare/tunnel.js +79 -0
  72. package/lib/cloudflare/miniflare/tunnel.js.map +1 -0
  73. package/lib/cloudflare/next.d.ts.map +1 -1
  74. package/lib/cloudflare/next.js +3 -0
  75. package/lib/cloudflare/next.js.map +1 -1
  76. package/lib/cloudflare/nuxt/plugin.d.ts.map +1 -1
  77. package/lib/cloudflare/nuxt/plugin.js +3 -3
  78. package/lib/cloudflare/nuxt/plugin.js.map +1 -1
  79. package/lib/cloudflare/quick-tunnel.d.ts +1 -1
  80. package/lib/cloudflare/redwood/plugin.js +2 -2
  81. package/lib/cloudflare/redwood/plugin.js.map +1 -1
  82. package/lib/cloudflare/secret-ref.d.ts +38 -0
  83. package/lib/cloudflare/secret-ref.d.ts.map +1 -0
  84. package/lib/cloudflare/secret-ref.js +38 -0
  85. package/lib/cloudflare/secret-ref.js.map +1 -0
  86. package/lib/cloudflare/vite/plugin.d.ts.map +1 -1
  87. package/lib/cloudflare/vite/plugin.js +6 -3
  88. package/lib/cloudflare/vite/plugin.js.map +1 -1
  89. package/lib/cloudflare/website.d.ts.map +1 -1
  90. package/lib/cloudflare/website.js +5 -16
  91. package/lib/cloudflare/website.js.map +1 -1
  92. package/lib/cloudflare/worker-metadata.d.ts +1 -1
  93. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  94. package/lib/cloudflare/worker-metadata.js +7 -9
  95. package/lib/cloudflare/worker-metadata.js.map +1 -1
  96. package/lib/cloudflare/worker.d.ts +16 -1
  97. package/lib/cloudflare/worker.d.ts.map +1 -1
  98. package/lib/cloudflare/worker.js +16 -2
  99. package/lib/cloudflare/worker.js.map +1 -1
  100. package/lib/cloudflare/wrangler.json.d.ts +2 -1
  101. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  102. package/lib/cloudflare/wrangler.json.js +5 -6
  103. package/lib/cloudflare/wrangler.json.js.map +1 -1
  104. package/lib/llms.cloudflare.txt +1 -1
  105. package/lib/scope.d.ts +26 -1
  106. package/lib/scope.d.ts.map +1 -1
  107. package/lib/scope.js +12 -5
  108. package/lib/scope.js.map +1 -1
  109. package/lib/state/file-system-state-store.d.ts.map +1 -1
  110. package/lib/state/file-system-state-store.js +1 -2
  111. package/lib/state/file-system-state-store.js.map +1 -1
  112. package/lib/state/sqlite-state-store.d.ts +11 -0
  113. package/lib/state/sqlite-state-store.d.ts.map +1 -1
  114. package/lib/state/sqlite-state-store.js +22 -14
  115. package/lib/state/sqlite-state-store.js.map +1 -1
  116. package/lib/stripe/customer.d.ts +0 -8
  117. package/lib/stripe/customer.d.ts.map +1 -1
  118. package/lib/stripe/customer.js +0 -6
  119. package/lib/stripe/customer.js.map +1 -1
  120. package/lib/stripe/price.d.ts +23 -23
  121. package/lib/stripe/price.d.ts.map +1 -1
  122. package/lib/stripe/price.js +6 -22
  123. package/lib/stripe/price.js.map +1 -1
  124. package/lib/test/vitest.d.ts.map +1 -1
  125. package/lib/test/vitest.js +1 -1
  126. package/lib/test/vitest.js.map +1 -1
  127. package/lib/util/assert-never.js +1 -1
  128. package/lib/util/assert-never.js.map +1 -1
  129. package/lib/util/cli.d.ts.map +1 -1
  130. package/lib/util/cli.js +12 -13
  131. package/lib/util/cli.js.map +1 -1
  132. package/lib/util/idempotent-spawn.d.ts.map +1 -1
  133. package/lib/util/idempotent-spawn.js +5 -8
  134. package/lib/util/idempotent-spawn.js.map +1 -1
  135. package/lib/util/root-dir.d.ts +2 -0
  136. package/lib/util/root-dir.d.ts.map +1 -0
  137. package/lib/util/root-dir.js +2 -0
  138. package/lib/util/root-dir.js.map +1 -0
  139. package/package.json +4 -4
  140. package/src/alchemy.ts +43 -8
  141. package/src/apply.ts +58 -3
  142. package/src/cloudflare/bindings.ts +21 -4
  143. package/src/cloudflare/bound.ts +50 -52
  144. package/src/cloudflare/bucket.ts +1 -1
  145. package/src/cloudflare/bundle/internal-worker-bundle.ts +2 -1
  146. package/src/cloudflare/cloudflare-env-proxy.ts +5 -5
  147. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  148. package/src/cloudflare/d1-database.ts +2 -1
  149. package/src/cloudflare/d1-local-migrations.ts +10 -6
  150. package/src/cloudflare/hyperdrive-ref.ts +98 -0
  151. package/src/cloudflare/index.ts +2 -0
  152. package/src/cloudflare/kv-namespace.ts +1 -1
  153. package/src/cloudflare/miniflare/build-worker-options.ts +21 -12
  154. package/src/cloudflare/miniflare/delete.ts +19 -11
  155. package/src/cloudflare/miniflare/miniflare-controller.ts +33 -14
  156. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +122 -102
  157. package/src/cloudflare/miniflare/paths.ts +11 -7
  158. package/src/cloudflare/miniflare/symlink-miniflare-state.ts +19 -0
  159. package/src/cloudflare/miniflare/tunnel.ts +116 -0
  160. package/src/cloudflare/next.ts +3 -0
  161. package/src/cloudflare/nuxt/plugin.ts +12 -3
  162. package/src/cloudflare/redwood/plugin.ts +2 -2
  163. package/src/cloudflare/secret-ref.ts +56 -0
  164. package/src/cloudflare/vite/plugin.ts +8 -3
  165. package/src/cloudflare/website.ts +5 -19
  166. package/src/cloudflare/worker-metadata.ts +14 -12
  167. package/src/cloudflare/worker.ts +36 -3
  168. package/src/cloudflare/wrangler.json.ts +14 -11
  169. package/src/llms.cloudflare.txt +1 -1
  170. package/src/scope.ts +38 -6
  171. package/src/state/file-system-state-store.ts +1 -2
  172. package/src/state/sqlite-state-store.ts +41 -14
  173. package/src/stripe/customer.ts +0 -14
  174. package/src/stripe/price.ts +59 -56
  175. package/src/test/vitest.ts +4 -1
  176. package/src/util/assert-never.ts +1 -1
  177. package/src/util/cli.ts +15 -13
  178. package/src/util/idempotent-spawn.ts +5 -6
  179. package/src/util/root-dir.ts +1 -0
  180. package/templates/nextjs/alchemy.run.ts +2 -2
  181. package/templates/rwsdk/.cursor/rules/alchemy_cloudflare.mdc +1 -1
  182. package/templates/rwsdk/package.json +3 -3
  183. package/workers/cloudflare-state-store.js +1 -1
  184. package/workers/tunnel-proxy.js +110 -0
  185. 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 MiniflareWorkerProxyOptions {
10
- name: string;
11
- port: number;
12
- miniflare: miniflare.Miniflare;
7
+ export interface MiniflareWorkerProxy {
8
+ url: URL;
9
+ close: () => Promise<void>;
13
10
  }
14
11
 
15
- export class MiniflareWorkerProxy {
16
- private server = http.createServer();
17
- private wss = new WebSocketServer({ noServer: true });
18
-
19
- constructor(private readonly options: MiniflareWorkerProxyOptions) {
20
- this.server.on("upgrade", async (req, socket, head) => {
21
- await this.handleUpgrade(req, socket, head);
22
- });
23
- this.server.on("request", async (req, res) => {
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
- private async handleUpgrade(
48
- req: http.IncomingMessage,
49
- socket: Stream.Duplex,
50
- head: Buffer,
51
- ) {
52
- const server = await this.createServerWebSocket(req);
53
- if (!server) {
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
- this.wss.handleUpgrade(req, socket, head, (client) => {
58
- client.on("message", (event, binary) => server.send(event, { binary }));
59
- client.on("close", (code, reason) => server.close(code, reason));
60
- server.on("message", (event, binary) => client.send(event, { binary }));
61
- server.on("close", (code, reason) => client.close(code, reason));
62
- this.wss.emit("connection", client, req);
63
- });
64
- }
65
-
66
- private async createServerWebSocket(req: http.IncomingMessage) {
67
- const target = await this.options.miniflare.unsafeGetDirectURL(
68
- this.options.name,
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
- return null;
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
- const controller = new AbortController();
83
- return await Promise.race([
84
- once(server, "open", { signal: controller.signal }).then(() => server),
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
- private async handleRequest(
96
- req: http.IncomingMessage,
97
- res: http.ServerResponse,
98
- ) {
99
- const worker = await this.options.miniflare.getWorker(this.options.name);
100
- if (!worker) {
101
- res.statusCode = 503;
102
- res.end(`[Alchemy] The worker "${this.options.name}" is not running.`);
103
- return;
104
- }
105
- try {
106
- const response = await worker.fetch(toMiniflareRequest(req));
107
- writeMiniflareResponseToNode(response, res);
108
- } catch (rawError) {
109
- const message =
110
- rawError instanceof Error
111
- ? (rawError.stack ?? rawError.message)
112
- : String(rawError);
113
- res.statusCode = 500;
114
- res.end(
115
- `[Alchemy] The worker "${this.options.name}" threw an error:\n\n${message}`,
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 toMiniflareRequest = (req: http.IncomingMessage) => {
102
+ const parseIncomingMessage = (req: http.IncomingMessage): RequestInfo => {
122
103
  const method = req.method ?? "GET";
123
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
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 new miniflare.Request(url, {
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 DEFAULT_CONFIG_PATH = ".alchemy/local/wrangler.jsonc";
14
- export const DEFAULT_PERSIST_PATH = ".alchemy/miniflare/v3";
15
+ export const getDefaultConfigPath = (rootDir: string = process.cwd()) => {
16
+ return path.join(rootDir, ".alchemy", "local", "wrangler.jsonc");
17
+ };
15
18
 
16
- export const validateConfigPath = (
17
- path = DEFAULT_CONFIG_PATH,
18
- throws = true,
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 = DEFAULT_PERSIST_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
+ }
@@ -33,6 +33,9 @@ export async function Nextjs<const B extends Bindings>(
33
33
  );
34
34
  }
35
35
  return await Website(id, {
36
+ bundle: {
37
+ minify: true,
38
+ },
36
39
  ...props,
37
40
  entrypoint: props.entrypoint ?? ".open-next/worker.js",
38
41
  wrangler: {
@@ -1,5 +1,10 @@
1
1
  import type { CloudflareOptions } from "nitropack/presets/cloudflare/types";
2
- import { validateConfigPath, validatePersistPath } from "../miniflare/paths.ts";
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(options.dev?.configPath),
11
- persistDir: validatePersistPath(options.dev?.persistDir),
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 { DEFAULT_CONFIG_PATH } from "../miniflare/paths.ts";
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 ?? DEFAULT_CONFIG_PATH;
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
- DEFAULT_PERSIST_PATH,
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
- : DEFAULT_PERSIST_PATH,
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(config?.configPath),
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 { DEFAULT_PERSIST_PATH } from "./miniflare/paths.ts";
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
- await writeMiniflareSymlink(paths.cwd);
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
- }