alchemy 0.47.0 → 0.48.1
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.mjs +63 -56
- package/lib/alchemy.d.ts +9 -3
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +19 -39
- package/lib/alchemy.js.map +1 -1
- package/lib/cloudflare/astro.d.ts.map +1 -1
- package/lib/cloudflare/astro.js +3 -0
- package/lib/cloudflare/astro.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +5 -0
- package/lib/cloudflare/compatibility-date.gen.d.ts.map +1 -0
- package/lib/cloudflare/compatibility-date.gen.js +7 -0
- package/lib/cloudflare/compatibility-date.gen.js.map +1 -0
- package/lib/cloudflare/container.js +1 -1
- package/lib/cloudflare/container.js.map +1 -1
- package/lib/cloudflare/d1-migrations.d.ts.map +1 -1
- package/lib/cloudflare/d1-migrations.js +6 -11
- package/lib/cloudflare/d1-migrations.js.map +1 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts +2 -2
- package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-options.js +11 -2
- package/lib/cloudflare/miniflare/miniflare-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +20 -0
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -0
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +80 -0
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -0
- package/lib/cloudflare/miniflare/miniflare.d.ts +6 -6
- package/lib/cloudflare/miniflare/miniflare.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare.js +25 -16
- package/lib/cloudflare/miniflare/miniflare.js.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts +10 -10
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.js +34 -44
- package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
- package/lib/cloudflare/nuxt.d.ts.map +1 -1
- package/lib/cloudflare/nuxt.js +3 -0
- package/lib/cloudflare/nuxt.js.map +1 -1
- package/lib/cloudflare/oauth.js +3 -3
- package/lib/cloudflare/oauth.js.map +1 -1
- package/lib/cloudflare/pipeline.d.ts.map +1 -1
- package/lib/cloudflare/pipeline.js +1 -3
- package/lib/cloudflare/pipeline.js.map +1 -1
- package/lib/cloudflare/react-router.d.ts.map +1 -1
- package/lib/cloudflare/react-router.js +3 -0
- package/lib/cloudflare/react-router.js.map +1 -1
- package/lib/cloudflare/vite.d.ts.map +1 -1
- package/lib/cloudflare/vite.js +0 -1
- package/lib/cloudflare/vite.js.map +1 -1
- package/lib/cloudflare/website.d.ts +0 -1
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +15 -8
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +8 -4
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +82 -72
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/scope.d.ts +14 -2
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +8 -3
- package/lib/scope.js.map +1 -1
- package/lib/state/cloudflare-state-store.d.ts.map +1 -1
- package/lib/state/cloudflare-state-store.js +2 -2
- package/lib/state/cloudflare-state-store.js.map +1 -1
- package/lib/util/http.d.ts +13 -0
- package/lib/util/http.d.ts.map +1 -0
- package/lib/util/http.js +78 -0
- package/lib/util/http.js.map +1 -0
- package/lib/util/safe-fetch.d.ts.map +1 -1
- package/lib/util/safe-fetch.js +1 -0
- package/lib/util/safe-fetch.js.map +1 -1
- package/package.json +5 -4
- package/src/alchemy.ts +28 -49
- package/src/cloudflare/astro.ts +3 -0
- package/src/cloudflare/compatibility-date.gen.ts +7 -0
- package/src/cloudflare/container.ts +1 -1
- package/src/cloudflare/d1-migrations.ts +8 -21
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/miniflare/miniflare-worker-options.ts +13 -1
- package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +97 -0
- package/src/cloudflare/miniflare/miniflare.ts +30 -21
- package/src/cloudflare/miniflare/remote-binding-proxy.ts +40 -52
- package/src/cloudflare/nuxt.ts +3 -0
- package/src/cloudflare/oauth.ts +3 -3
- package/src/cloudflare/pipeline.ts +1 -3
- package/src/cloudflare/react-router.ts +3 -0
- package/src/cloudflare/vite.ts +0 -1
- package/src/cloudflare/website.ts +16 -14
- package/src/cloudflare/worker.ts +104 -106
- package/src/scope.ts +21 -4
- package/src/state/cloudflare-state-store.ts +2 -2
- package/src/util/http.ts +94 -0
- package/src/util/safe-fetch.ts +1 -0
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/tsconfig.cloudflare.json +5 -1
- package/templates/react-router/tsconfig.json +0 -3
- package/templates/react-router/tsconfig.node.json +0 -4
- package/lib/build-date.d.ts +0 -6
- package/lib/build-date.d.ts.map +0 -1
- package/lib/build-date.js +0 -8
- package/lib/build-date.js.map +0 -1
- package/lib/util/http-server.d.ts +0 -14
- package/lib/util/http-server.d.ts.map +0 -1
- package/lib/util/http-server.js +0 -71
- package/lib/util/http-server.js.map +0 -1
- package/src/build-date.ts +0 -8
- package/src/util/http-server.ts +0 -84
package/src/alchemy.ts
CHANGED
|
@@ -20,50 +20,6 @@ import type { LoggerApi } from "./util/cli.ts";
|
|
|
20
20
|
import { logger } from "./util/logger.ts";
|
|
21
21
|
import { TelemetryClient } from "./util/telemetry/client.ts";
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
* Parses CLI arguments to extract alchemy options
|
|
25
|
-
*/
|
|
26
|
-
function parseCliArgs(): Partial<AlchemyOptions> {
|
|
27
|
-
const args = process.argv.slice(2);
|
|
28
|
-
const options: Partial<AlchemyOptions> = {};
|
|
29
|
-
|
|
30
|
-
// Parse phase from CLI arguments
|
|
31
|
-
if (args.includes("--destroy")) {
|
|
32
|
-
options.phase = "destroy";
|
|
33
|
-
} else if (args.includes("--read")) {
|
|
34
|
-
options.phase = "read";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (args.includes("--local") || args.includes("--dev")) {
|
|
38
|
-
options.dev = "prefer-local";
|
|
39
|
-
} else if (
|
|
40
|
-
args.includes("--remote") ||
|
|
41
|
-
args.includes("--watch") ||
|
|
42
|
-
process.execArgv.includes("--watch")
|
|
43
|
-
) {
|
|
44
|
-
options.dev = "prefer-remote";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Parse quiet flag
|
|
48
|
-
if (args.includes("--quiet")) {
|
|
49
|
-
options.quiet = true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Parse stage argument (--stage my-stage)
|
|
53
|
-
const stageIndex = args.indexOf("--stage");
|
|
54
|
-
if (stageIndex !== -1 && stageIndex + 1 < args.length) {
|
|
55
|
-
options.stage = args[stageIndex + 1];
|
|
56
|
-
}
|
|
57
|
-
options.stage ??= process.env.STAGE;
|
|
58
|
-
|
|
59
|
-
// Get password from environment variables
|
|
60
|
-
if (process.env.ALCHEMY_PASSWORD) {
|
|
61
|
-
options.password = process.env.ALCHEMY_PASSWORD;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return options;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
23
|
/**
|
|
68
24
|
* Type alias for semantic highlighting of `alchemy` as a type keyword
|
|
69
25
|
*/
|
|
@@ -175,8 +131,25 @@ async function _alchemy(
|
|
|
175
131
|
if (typeof args[0] === "string") {
|
|
176
132
|
const [appName, options] = args as [string, AlchemyOptions?];
|
|
177
133
|
|
|
178
|
-
|
|
179
|
-
const cliOptions =
|
|
134
|
+
const cliArgs = process.argv.slice(2);
|
|
135
|
+
const cliOptions = {
|
|
136
|
+
phase: cliArgs.includes("--destroy")
|
|
137
|
+
? "destroy"
|
|
138
|
+
: cliArgs.includes("--read")
|
|
139
|
+
? "read"
|
|
140
|
+
: "up",
|
|
141
|
+
local: cliArgs.includes("--local") || cliArgs.includes("--dev"),
|
|
142
|
+
watch: cliArgs.includes("--watch"),
|
|
143
|
+
quiet: cliArgs.includes("--quiet"),
|
|
144
|
+
// Parse stage argument (--stage my-stage) functionally and inline as a property declaration
|
|
145
|
+
stage: (function parseStage() {
|
|
146
|
+
const i = cliArgs.indexOf("--stage");
|
|
147
|
+
return i !== -1 && i + 1 < cliArgs.length
|
|
148
|
+
? cliArgs[i + 1]
|
|
149
|
+
: process.env.STAGE;
|
|
150
|
+
})(),
|
|
151
|
+
password: process.env.ALCHEMY_PASSWORD,
|
|
152
|
+
} satisfies Partial<AlchemyOptions>;
|
|
180
153
|
const mergedOptions = {
|
|
181
154
|
...cliOptions,
|
|
182
155
|
...options,
|
|
@@ -348,11 +321,17 @@ export interface AlchemyOptions {
|
|
|
348
321
|
*/
|
|
349
322
|
phase?: Phase;
|
|
350
323
|
/**
|
|
351
|
-
* Determines
|
|
324
|
+
* Determines if resources should be simulated locally (where possible)
|
|
325
|
+
*
|
|
326
|
+
* @default - `true` if ran with `alchemy dev` or `bun ./alchemy.run.ts --dev`
|
|
327
|
+
*/
|
|
328
|
+
local?: boolean;
|
|
329
|
+
/**
|
|
330
|
+
* Determines if local changes to resources should be reactively pushed to the local or remote environment.
|
|
352
331
|
*
|
|
353
|
-
* @default - `
|
|
332
|
+
* @default - `true` if ran with `alchemy dev`, `alchemy watch`, `bun --watch ./alchemy.run.ts`
|
|
354
333
|
*/
|
|
355
|
-
|
|
334
|
+
watch?: boolean;
|
|
356
335
|
/**
|
|
357
336
|
* Name to scope the resource state under (e.g. `.alchemy/{stage}/..`).
|
|
358
337
|
*
|
package/src/cloudflare/astro.ts
CHANGED
|
@@ -191,7 +191,7 @@ export async function Container<T>(
|
|
|
191
191
|
adopt: props.adopt,
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
-
const isDev = Scope.current.
|
|
194
|
+
const isDev = Scope.current.local && !props.dev?.remote;
|
|
195
195
|
if (isDev) {
|
|
196
196
|
const image = await Image(id, {
|
|
197
197
|
name: `cloudflare-dev/${name}`, // prefix used by Miniflare
|
|
@@ -319,9 +319,6 @@ export async function applyMigrations(
|
|
|
319
319
|
|
|
320
320
|
if (applied.has(migrationName)) continue;
|
|
321
321
|
|
|
322
|
-
// Run the migration SQL
|
|
323
|
-
await executeD1SQL(options, migration.sql);
|
|
324
|
-
|
|
325
322
|
// Generate a migration id: prefer sequential zero-padded numeric ids (e.g. 00014)
|
|
326
323
|
// to keep consistency with legacy/imported data. If we cannot produce a numeric id
|
|
327
324
|
// (e.g. existing IDs are not numeric), fall back to a unique timestamp-based ID.
|
|
@@ -334,26 +331,16 @@ export async function applyMigrations(
|
|
|
334
331
|
Date.now().toString() + Math.random().toString(36).substr(2, 9);
|
|
335
332
|
}
|
|
336
333
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
},
|
|
334
|
+
// Run and record the migration in a single request.
|
|
335
|
+
// D1 over HTTP doesn't support transactions, so this is the next best thing.
|
|
336
|
+
await executeD1SQL(
|
|
337
|
+
options,
|
|
338
|
+
[
|
|
339
|
+
migration.sql,
|
|
340
|
+
`INSERT INTO ${options.migrationsTable} (id, name, applied_at) VALUES ('${migrationId}', '${migrationName}', datetime('now'));`,
|
|
341
|
+
].join("\n"),
|
|
346
342
|
);
|
|
347
343
|
|
|
348
|
-
if (!response.ok) {
|
|
349
|
-
await handleApiError(
|
|
350
|
-
response,
|
|
351
|
-
"inserting migration record",
|
|
352
|
-
"D1 database",
|
|
353
|
-
options.databaseId,
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
344
|
if (!options.quiet) {
|
|
358
345
|
logger.log(`Applied migration: ${migrationName}`);
|
|
359
346
|
}
|
package/src/cloudflare/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./bound.ts";
|
|
|
12
12
|
export * from "./browser-rendering.ts";
|
|
13
13
|
export * from "./bucket.ts";
|
|
14
14
|
export * from "./certificate-pack.ts";
|
|
15
|
+
export * from "./compatibility-date.gen.ts";
|
|
15
16
|
export * from "./container.ts";
|
|
16
17
|
export * from "./custom-domain.ts";
|
|
17
18
|
export * from "./d1-clone.ts";
|
|
@@ -17,6 +17,7 @@ export type MiniflareWorkerOptions = Pick<
|
|
|
17
17
|
| "compatibilityDate"
|
|
18
18
|
| "compatibilityFlags"
|
|
19
19
|
| "format"
|
|
20
|
+
| "assets"
|
|
20
21
|
> & {
|
|
21
22
|
name: string;
|
|
22
23
|
bundle: WorkerBundle;
|
|
@@ -209,6 +210,7 @@ function buildRemoteBinding(
|
|
|
209
210
|
|
|
210
211
|
export async function buildMiniflareWorkerOptions({
|
|
211
212
|
name: workerName,
|
|
213
|
+
assets,
|
|
212
214
|
bundle,
|
|
213
215
|
bindings,
|
|
214
216
|
format,
|
|
@@ -232,7 +234,9 @@ export async function buildMiniflareWorkerOptions({
|
|
|
232
234
|
),
|
|
233
235
|
compatibilityDate,
|
|
234
236
|
compatibilityFlags,
|
|
235
|
-
|
|
237
|
+
// This was true for compatibility with the dev registry, but that doesn't work anyway,
|
|
238
|
+
// and if we don't set it to false, websocket connections fail. I'm confused.
|
|
239
|
+
unsafeDirectSockets: [{ proxy: false }],
|
|
236
240
|
containerEngine: {
|
|
237
241
|
localDocker: {
|
|
238
242
|
socketPath:
|
|
@@ -291,6 +295,14 @@ export async function buildMiniflareWorkerOptions({
|
|
|
291
295
|
options.assets = {
|
|
292
296
|
binding: name,
|
|
293
297
|
directory: binding.path,
|
|
298
|
+
routerConfig: {
|
|
299
|
+
invoke_user_worker_ahead_of_assets:
|
|
300
|
+
assets?.run_worker_first === true,
|
|
301
|
+
},
|
|
302
|
+
assetConfig: {
|
|
303
|
+
html_handling: assets?.html_handling,
|
|
304
|
+
not_found_handling: assets?.not_found_handling,
|
|
305
|
+
},
|
|
294
306
|
};
|
|
295
307
|
break;
|
|
296
308
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type http from "node:http";
|
|
2
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
3
|
+
import { HTTPServer } from "../../util/http.ts";
|
|
4
|
+
|
|
5
|
+
export interface MiniflareWorkerProxyOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Used to proxy websocket connections to the worker.
|
|
8
|
+
* (This is a hack; used because trying to interact with `response.webSocket` doesn't work on Bun.)
|
|
9
|
+
*/
|
|
10
|
+
getDirectURL: () => Promise<URL>;
|
|
11
|
+
/** Used to proxy HTTP requests to the worker. */
|
|
12
|
+
fetch: (request: Request) => Promise<Response>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class MiniflareWorkerProxy extends HTTPServer {
|
|
16
|
+
wsServer: WebSocketServer;
|
|
17
|
+
wsReconnectAttempts = new Map<string, number>();
|
|
18
|
+
|
|
19
|
+
constructor(private readonly options: MiniflareWorkerProxyOptions) {
|
|
20
|
+
super({
|
|
21
|
+
fetch: options.fetch,
|
|
22
|
+
});
|
|
23
|
+
this.wsServer = new WebSocketServer({ noServer: true });
|
|
24
|
+
this.httpServer.on("upgrade", async (req, socket, head) => {
|
|
25
|
+
this.wsServer.handleUpgrade(req, socket, head, async (client) => {
|
|
26
|
+
const id = crypto.randomUUID();
|
|
27
|
+
await this.handleUpgrade(id, client, req);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async handleUpgrade(
|
|
33
|
+
id: string,
|
|
34
|
+
client: WebSocket,
|
|
35
|
+
request: http.IncomingMessage,
|
|
36
|
+
) {
|
|
37
|
+
const attempt = this.wsReconnectAttempts.get(id) ?? 0;
|
|
38
|
+
this.wsReconnectAttempts.set(id, attempt + 1);
|
|
39
|
+
if (attempt > 3) {
|
|
40
|
+
client.close(1006, "Too many reconnect attempts");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (attempt > 0) {
|
|
44
|
+
const delay = attempt * 1000;
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const target = await this.options.getDirectURL();
|
|
49
|
+
const url = new URL(request.url ?? "/", target);
|
|
50
|
+
const headers = { ...request.headers };
|
|
51
|
+
// All of these headers are set by the WebSocket constructor,
|
|
52
|
+
// so if we don't delete them, the request will fail.
|
|
53
|
+
delete headers.host;
|
|
54
|
+
delete headers.connection;
|
|
55
|
+
delete headers.upgrade;
|
|
56
|
+
delete headers["sec-websocket-version"];
|
|
57
|
+
delete headers["sec-websocket-key"];
|
|
58
|
+
delete headers["sec-websocket-protocol"];
|
|
59
|
+
delete headers["sec-websocket-extensions"];
|
|
60
|
+
delete headers["sec-websocket-accept"];
|
|
61
|
+
const server = new WebSocket(url.toString(), {
|
|
62
|
+
protocol: request.headers["sec-websocket-protocol"],
|
|
63
|
+
key: request.headers["sec-websocket-key"],
|
|
64
|
+
headers,
|
|
65
|
+
});
|
|
66
|
+
server.on("open", () => {
|
|
67
|
+
// Reset the reconnect attempts when the connection is established successfully.
|
|
68
|
+
this.wsReconnectAttempts.set(id, 0);
|
|
69
|
+
});
|
|
70
|
+
server.on("message", (data, binary) => {
|
|
71
|
+
client.send(data, { binary });
|
|
72
|
+
});
|
|
73
|
+
server.on("close", async (code, reason) => {
|
|
74
|
+
if (code === 1006) {
|
|
75
|
+
// When the worker hot reloads, the websocket connection is closed with this code.
|
|
76
|
+
// Reconnecting allows the client to maintain the same connection.
|
|
77
|
+
await this.handleUpgrade(id, client, request);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
client.close(code, reason);
|
|
81
|
+
});
|
|
82
|
+
client.on("message", (data, binary) => {
|
|
83
|
+
server.send(data, { binary });
|
|
84
|
+
});
|
|
85
|
+
client.on("close", (code, reason) => {
|
|
86
|
+
this.wsReconnectAttempts.delete(id);
|
|
87
|
+
if (server.readyState === WebSocket.OPEN) {
|
|
88
|
+
server.close(code, reason);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async close() {
|
|
94
|
+
this.wsServer.close();
|
|
95
|
+
await super.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
} from "miniflare";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { findOpenPort } from "../../util/find-open-port.ts";
|
|
10
|
-
import { HTTPServer } from "../../util/http-server.ts";
|
|
11
10
|
import { logger } from "../../util/logger.ts";
|
|
12
11
|
import {
|
|
13
12
|
promiseWithResolvers,
|
|
@@ -18,6 +17,7 @@ import {
|
|
|
18
17
|
buildRemoteBindings,
|
|
19
18
|
type MiniflareWorkerOptions,
|
|
20
19
|
} from "./miniflare-worker-options.ts";
|
|
20
|
+
import { MiniflareWorkerProxy } from "./miniflare-worker-proxy.ts";
|
|
21
21
|
import {
|
|
22
22
|
createRemoteProxyWorker,
|
|
23
23
|
type RemoteBindingProxy,
|
|
@@ -26,12 +26,12 @@ import {
|
|
|
26
26
|
class MiniflareServer {
|
|
27
27
|
miniflare?: Miniflare;
|
|
28
28
|
workers = new Map<string, WorkerOptions>();
|
|
29
|
-
|
|
29
|
+
workerProxies = new Map<string, MiniflareWorkerProxy>();
|
|
30
30
|
remoteBindingProxies = new Map<string, RemoteBindingProxy>();
|
|
31
31
|
|
|
32
32
|
stream = new WritableStream<{
|
|
33
33
|
worker: MiniflareWorkerOptions;
|
|
34
|
-
promise: PromiseWithResolvers<
|
|
34
|
+
promise: PromiseWithResolvers<MiniflareWorkerProxy>;
|
|
35
35
|
}>({
|
|
36
36
|
write: async ({ worker, promise }) => {
|
|
37
37
|
try {
|
|
@@ -48,7 +48,7 @@ class MiniflareServer {
|
|
|
48
48
|
writer = this.stream.getWriter();
|
|
49
49
|
|
|
50
50
|
async push(worker: MiniflareWorkerOptions) {
|
|
51
|
-
const promise = promiseWithResolvers<
|
|
51
|
+
const promise = promiseWithResolvers<MiniflareWorkerProxy>();
|
|
52
52
|
const [, server] = await Promise.all([
|
|
53
53
|
this.writer.write({ worker, promise }),
|
|
54
54
|
promise.promise,
|
|
@@ -62,11 +62,10 @@ class MiniflareServer {
|
|
|
62
62
|
|
|
63
63
|
private async set(worker: MiniflareWorkerOptions) {
|
|
64
64
|
this.workers.set(
|
|
65
|
-
worker.name
|
|
65
|
+
worker.name,
|
|
66
66
|
await buildMiniflareWorkerOptions({
|
|
67
67
|
...worker,
|
|
68
|
-
remoteProxyConnectionString:
|
|
69
|
-
await this.maybeCreateMixedModeProxy(worker),
|
|
68
|
+
remoteProxyConnectionString: await this.maybeCreateRemoteProxy(worker),
|
|
70
69
|
}),
|
|
71
70
|
);
|
|
72
71
|
if (this.miniflare) {
|
|
@@ -90,33 +89,41 @@ class MiniflareServer {
|
|
|
90
89
|
this.miniflare = new Miniflare(await this.miniflareOptions());
|
|
91
90
|
await withErrorRewrite(this.miniflare.ready);
|
|
92
91
|
}
|
|
93
|
-
const existing = this.
|
|
92
|
+
const existing = this.workerProxies.get(worker.name);
|
|
94
93
|
if (existing) {
|
|
95
94
|
return existing;
|
|
96
95
|
}
|
|
97
|
-
const server = new
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
const server = new MiniflareWorkerProxy({
|
|
97
|
+
getDirectURL: async () => {
|
|
98
|
+
const url = await this.miniflare?.unsafeGetDirectURL(worker.name);
|
|
99
|
+
if (!url) {
|
|
100
|
+
throw new Error(`Worker "${worker.name}" is not running`);
|
|
101
|
+
}
|
|
102
|
+
return url;
|
|
103
|
+
},
|
|
104
|
+
fetch: this.createRequestHandler(worker.name),
|
|
100
105
|
});
|
|
101
|
-
this.
|
|
102
|
-
await server.
|
|
106
|
+
this.workerProxies.set(worker.name, server);
|
|
107
|
+
await server.listen(worker.port ?? (await findOpenPort()));
|
|
103
108
|
return server;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
private async dispose() {
|
|
107
112
|
await Promise.all([
|
|
108
113
|
this.miniflare?.dispose(),
|
|
109
|
-
...Array.from(this.
|
|
114
|
+
...Array.from(this.workerProxies.values()).map((server) =>
|
|
115
|
+
server.close(),
|
|
116
|
+
),
|
|
110
117
|
...Array.from(this.remoteBindingProxies.values()).map((proxy) =>
|
|
111
|
-
proxy.server.
|
|
118
|
+
proxy.server.close(),
|
|
112
119
|
),
|
|
113
120
|
]);
|
|
114
121
|
this.miniflare = undefined;
|
|
115
122
|
this.workers.clear();
|
|
116
|
-
this.
|
|
123
|
+
this.workerProxies.clear();
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
private async
|
|
126
|
+
private async maybeCreateRemoteProxy(
|
|
120
127
|
worker: MiniflareWorkerOptions,
|
|
121
128
|
): Promise<RemoteProxyConnectionString | undefined> {
|
|
122
129
|
const bindings = buildRemoteBindings(worker);
|
|
@@ -130,6 +137,8 @@ class MiniflareServer {
|
|
|
130
137
|
)
|
|
131
138
|
) {
|
|
132
139
|
return existing.connectionString;
|
|
140
|
+
} else if (existing) {
|
|
141
|
+
await existing.server.close();
|
|
133
142
|
}
|
|
134
143
|
const proxy = await createRemoteProxyWorker({
|
|
135
144
|
name: `mixed-mode-proxy-${crypto.randomUUID()}`,
|
|
@@ -150,8 +159,8 @@ class MiniflareServer {
|
|
|
150
159
|
},
|
|
151
160
|
);
|
|
152
161
|
}
|
|
153
|
-
const
|
|
154
|
-
if (!
|
|
162
|
+
const worker = await this.miniflare?.getWorker(name);
|
|
163
|
+
if (!worker) {
|
|
155
164
|
return new Response(
|
|
156
165
|
`[Alchemy] Cannot find worker "${name}". Please try again.`,
|
|
157
166
|
{
|
|
@@ -159,12 +168,12 @@ class MiniflareServer {
|
|
|
159
168
|
},
|
|
160
169
|
);
|
|
161
170
|
}
|
|
162
|
-
const res = await
|
|
171
|
+
const res = await worker.fetch(req.url, {
|
|
163
172
|
method: req.method,
|
|
164
173
|
headers: req.headers as any,
|
|
165
174
|
body: req.body as any,
|
|
166
|
-
redirect: "manual",
|
|
167
175
|
duplex: "half",
|
|
176
|
+
redirect: "manual",
|
|
168
177
|
});
|
|
169
178
|
return res as unknown as Response;
|
|
170
179
|
} catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RemoteProxyConnectionString } from "miniflare";
|
|
2
|
-
import { HTTPServer } from "../../util/http
|
|
2
|
+
import { HTTPServer } from "../../util/http.ts";
|
|
3
3
|
import { extractCloudflareResult } from "../api-response.ts";
|
|
4
4
|
import { createCloudflareApi, type CloudflareApi } from "../api.ts";
|
|
5
5
|
import type { WorkerBindingSpec } from "../bindings.ts";
|
|
@@ -22,6 +22,12 @@ interface WorkersPreviewSession {
|
|
|
22
22
|
token: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface RemoteBindingProxy {
|
|
26
|
+
server: HTTPServer;
|
|
27
|
+
bindings: WorkerBindingSpec[];
|
|
28
|
+
connectionString: RemoteProxyConnectionString;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export async function createRemoteProxyWorker(input: {
|
|
26
32
|
name: string;
|
|
27
33
|
bindings: WorkerBindingSpec[];
|
|
@@ -47,62 +53,44 @@ export async function createRemoteProxyWorker(input: {
|
|
|
47
53
|
}),
|
|
48
54
|
import("../worker-subdomain.ts").then((m) => m.getAccountSubdomain(api)),
|
|
49
55
|
]);
|
|
50
|
-
return new RemoteBindingProxy(
|
|
51
|
-
`https://${input.name}.${subdomain}.workers.dev`,
|
|
52
|
-
token,
|
|
53
|
-
input.bindings,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
server
|
|
57
|
+
const proxyURL = new URL(`https://${input.name}.${subdomain}.workers.dev`);
|
|
58
|
+
const server = new HTTPServer({
|
|
59
|
+
fetch: async (req) => {
|
|
60
|
+
const origin = new URL(req.url);
|
|
61
|
+
const url = new URL(origin.pathname, proxyURL.toString());
|
|
62
|
+
url.search = origin.search;
|
|
63
|
+
url.hash = origin.hash;
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
) {
|
|
65
|
-
this.server = new HTTPServer({
|
|
66
|
-
fetch: this.fetch.bind(this),
|
|
67
|
-
});
|
|
68
|
-
}
|
|
65
|
+
const requestHeaders = new Headers(req.headers);
|
|
66
|
+
requestHeaders.set("cf-workers-preview-token", token);
|
|
67
|
+
requestHeaders.set("host", proxyURL.hostname);
|
|
68
|
+
requestHeaders.delete("cf-connecting-ip");
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async fetch(req: Request) {
|
|
79
|
-
const origin = new URL(req.url);
|
|
80
|
-
const url = new URL(origin.pathname, this.url);
|
|
81
|
-
url.search = origin.search;
|
|
82
|
-
url.hash = origin.hash;
|
|
83
|
-
|
|
84
|
-
const headers = new Headers(req.headers);
|
|
85
|
-
headers.set("cf-workers-preview-token", this.token);
|
|
86
|
-
headers.set("host", new URL(this.url).hostname);
|
|
87
|
-
headers.delete("cf-connecting-ip");
|
|
88
|
-
|
|
89
|
-
const res = await fetch(url, {
|
|
90
|
-
method: req.method,
|
|
91
|
-
headers,
|
|
92
|
-
body: req.body,
|
|
93
|
-
redirect: "manual",
|
|
94
|
-
});
|
|
70
|
+
const res = await fetch(url, {
|
|
71
|
+
method: req.method,
|
|
72
|
+
headers: requestHeaders,
|
|
73
|
+
body: req.body,
|
|
74
|
+
redirect: "manual",
|
|
75
|
+
});
|
|
95
76
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
77
|
+
// Remove headers that are not supported by miniflare
|
|
78
|
+
const responseHeaders = new Headers(res.headers);
|
|
79
|
+
responseHeaders.delete("transfer-encoding");
|
|
80
|
+
responseHeaders.delete("content-encoding");
|
|
100
81
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
82
|
+
return new Response(res.body, {
|
|
83
|
+
status: res.status,
|
|
84
|
+
headers: responseHeaders,
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
await server.listen();
|
|
89
|
+
return {
|
|
90
|
+
server,
|
|
91
|
+
bindings: input.bindings,
|
|
92
|
+
connectionString: new URL(server.url) as RemoteProxyConnectionString,
|
|
93
|
+
};
|
|
106
94
|
}
|
|
107
95
|
|
|
108
96
|
async function createWorkersPreviewToken(
|
package/src/cloudflare/nuxt.ts
CHANGED
|
@@ -64,5 +64,8 @@ export async function Nuxt<B extends Bindings>(
|
|
|
64
64
|
compatibilityFlags: ["nodejs_compat", ...(props?.compatibilityFlags ?? [])],
|
|
65
65
|
// Enable wrangler by default, common for Nuxt/Cloudflare deployments
|
|
66
66
|
wrangler: props?.wrangler ?? true,
|
|
67
|
+
dev: props?.dev ?? {
|
|
68
|
+
command: "nuxt dev",
|
|
69
|
+
},
|
|
67
70
|
});
|
|
68
71
|
}
|
package/src/cloudflare/oauth.ts
CHANGED
|
@@ -7,7 +7,7 @@ import os from "node:os";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import open from "open";
|
|
9
9
|
import xdgAppPaths from "xdg-app-paths";
|
|
10
|
-
import { HTTPServer } from "../util/http
|
|
10
|
+
import { HTTPServer } from "../util/http.ts";
|
|
11
11
|
import { memoize } from "../util/memoize.ts";
|
|
12
12
|
import {
|
|
13
13
|
detached,
|
|
@@ -145,7 +145,6 @@ export const wranglerLogin = (
|
|
|
145
145
|
return Response.redirect(`${ALCHEMY_BASE_URL}/auth/${type}`);
|
|
146
146
|
};
|
|
147
147
|
const server = new HTTPServer({
|
|
148
|
-
port: 8976,
|
|
149
148
|
fetch: async (request) => {
|
|
150
149
|
const url = new URL(request.url);
|
|
151
150
|
if (url.pathname !== "/oauth/callback") {
|
|
@@ -190,6 +189,7 @@ export const wranglerLogin = (
|
|
|
190
189
|
return renderResponse("success");
|
|
191
190
|
},
|
|
192
191
|
});
|
|
192
|
+
server.listen(8976);
|
|
193
193
|
const timeout = setTimeout(
|
|
194
194
|
() => {
|
|
195
195
|
err(
|
|
@@ -204,7 +204,7 @@ export const wranglerLogin = (
|
|
|
204
204
|
);
|
|
205
205
|
return ensure(result, () => {
|
|
206
206
|
clearTimeout(timeout);
|
|
207
|
-
void server.
|
|
207
|
+
void server.close();
|
|
208
208
|
});
|
|
209
209
|
};
|
|
210
210
|
|
|
@@ -368,11 +368,9 @@ const PipelineResource = Resource("cloudflare::Pipeline", async function <
|
|
|
368
368
|
let pipelineData: CloudflarePipelineResponse;
|
|
369
369
|
|
|
370
370
|
if (this.phase === "create") {
|
|
371
|
-
console.log(props);
|
|
372
371
|
// Check if we should adopt an existing pipeline
|
|
373
372
|
try {
|
|
374
373
|
// Try to create pipeline first
|
|
375
|
-
console.log("Creating new Cloudflare Pipeline:", pipelineName);
|
|
376
374
|
pipelineData = await createPipeline(api, pipelineName, props);
|
|
377
375
|
} catch (error) {
|
|
378
376
|
// If creation fails with 409 (conflict), adopt existing pipeline
|
|
@@ -383,7 +381,7 @@ const PipelineResource = Resource("cloudflare::Pipeline", async function <
|
|
|
383
381
|
error.message.includes("Pipeline with this name already exists")))
|
|
384
382
|
) {
|
|
385
383
|
if (props.adopt) {
|
|
386
|
-
console.
|
|
384
|
+
console.warn(
|
|
387
385
|
"Pipeline already exists, adopting existing Cloudflare Pipeline:",
|
|
388
386
|
pipelineName,
|
|
389
387
|
);
|