wrangler 2.0.3 → 2.0.7
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/wrangler.js +2 -2
- package/package.json +5 -3
- package/pages/functions/buildPlugin.ts +13 -0
- package/pages/functions/buildWorker.ts +13 -0
- package/src/__tests__/configuration.test.ts +217 -29
- package/src/__tests__/dev.test.tsx +71 -9
- package/src/__tests__/index.test.ts +30 -16
- package/src/__tests__/init.test.ts +61 -20
- package/src/__tests__/kv.test.ts +109 -103
- package/src/__tests__/pages.test.ts +363 -33
- package/src/__tests__/parse.test.ts +5 -1
- package/src/__tests__/publish.test.ts +486 -72
- package/src/__tests__/r2.test.ts +47 -24
- package/src/__tests__/secret.test.ts +35 -0
- package/src/abort.d.ts +3 -0
- package/src/bundle.ts +32 -1
- package/src/cfetch/index.ts +4 -2
- package/src/cfetch/internal.ts +11 -9
- package/src/config/environment.ts +40 -14
- package/src/config/index.ts +162 -0
- package/src/config/validation.ts +126 -37
- package/src/create-worker-preview.ts +17 -7
- package/src/create-worker-upload-form.ts +22 -8
- package/src/dev/dev.tsx +5 -4
- package/src/dev/local.tsx +6 -0
- package/src/dev/remote.tsx +15 -1
- package/src/durable.ts +102 -0
- package/src/index.tsx +185 -98
- package/src/inspect.ts +39 -0
- package/src/kv.ts +111 -24
- package/src/open-in-browser.ts +5 -12
- package/src/pages.tsx +206 -65
- package/src/parse.ts +21 -4
- package/src/proxy.ts +38 -22
- package/src/publish.ts +227 -113
- package/src/sites.tsx +13 -16
- package/src/worker.ts +8 -0
- package/templates/new-worker.ts +16 -1
- package/wrangler-dist/cli.js +32273 -19295
package/src/proxy.ts
CHANGED
|
@@ -2,11 +2,13 @@ import { createServer as createHttpServer } from "node:http";
|
|
|
2
2
|
import { connect } from "node:http2";
|
|
3
3
|
import { createServer as createHttpsServer } from "node:https";
|
|
4
4
|
import WebSocket from "faye-websocket";
|
|
5
|
+
import { createHttpTerminator } from "http-terminator";
|
|
5
6
|
import { useEffect, useRef, useState } from "react";
|
|
6
7
|
import serveStatic from "serve-static";
|
|
7
8
|
import { getHttpsOptions } from "./https-options";
|
|
8
9
|
import { logger } from "./logger";
|
|
9
10
|
import type { CfPreviewToken } from "./create-worker-preview";
|
|
11
|
+
import type { HttpTerminator } from "http-terminator";
|
|
10
12
|
import type {
|
|
11
13
|
IncomingHttpHeaders,
|
|
12
14
|
RequestListener,
|
|
@@ -67,6 +69,11 @@ function rewriteRemoteHostToLocalHostInHeaders(
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
type PreviewProxy = {
|
|
73
|
+
server: HttpServer | HttpsServer;
|
|
74
|
+
terminator: HttpTerminator;
|
|
75
|
+
};
|
|
76
|
+
|
|
70
77
|
export function usePreviewServer({
|
|
71
78
|
previewToken,
|
|
72
79
|
publicRoot,
|
|
@@ -81,21 +88,26 @@ export function usePreviewServer({
|
|
|
81
88
|
ip: string;
|
|
82
89
|
}) {
|
|
83
90
|
/** Creates an HTTP/1 proxy that sends requests over HTTP/2. */
|
|
84
|
-
const [
|
|
91
|
+
const [proxy, setProxy] = useState<PreviewProxy>();
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
94
|
* Create the instance of the local proxy server that will pass on
|
|
88
95
|
* requests to the preview worker.
|
|
89
96
|
*/
|
|
90
97
|
useEffect(() => {
|
|
91
|
-
if (
|
|
98
|
+
if (proxy === undefined) {
|
|
92
99
|
createProxyServer(localProtocol)
|
|
93
|
-
.then((
|
|
100
|
+
.then((server) => {
|
|
101
|
+
setProxy({
|
|
102
|
+
server,
|
|
103
|
+
terminator: createHttpTerminator({ server }),
|
|
104
|
+
});
|
|
105
|
+
})
|
|
94
106
|
.catch(async (err) => {
|
|
95
107
|
logger.error("Failed to create proxy server:", err);
|
|
96
108
|
});
|
|
97
109
|
}
|
|
98
|
-
}, [
|
|
110
|
+
}, [proxy, localProtocol]);
|
|
99
111
|
|
|
100
112
|
/**
|
|
101
113
|
* When we're not connected / getting a fresh token on changes,
|
|
@@ -123,7 +135,7 @@ export function usePreviewServer({
|
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
useEffect(() => {
|
|
126
|
-
if (
|
|
138
|
+
if (proxy === undefined) {
|
|
127
139
|
return;
|
|
128
140
|
}
|
|
129
141
|
|
|
@@ -138,8 +150,8 @@ export function usePreviewServer({
|
|
|
138
150
|
// store the stream in a buffer so we can replay it later
|
|
139
151
|
streamBufferRef.current.push({ stream, headers });
|
|
140
152
|
};
|
|
141
|
-
|
|
142
|
-
cleanupListeners.push(() =>
|
|
153
|
+
proxy.server.on("stream", bufferStream);
|
|
154
|
+
cleanupListeners.push(() => proxy.server.off("stream", bufferStream));
|
|
143
155
|
|
|
144
156
|
const bufferRequestResponse = (
|
|
145
157
|
request: IncomingMessage,
|
|
@@ -149,9 +161,9 @@ export function usePreviewServer({
|
|
|
149
161
|
requestResponseBufferRef.current.push({ request, response });
|
|
150
162
|
};
|
|
151
163
|
|
|
152
|
-
|
|
164
|
+
proxy.server.on("request", bufferRequestResponse);
|
|
153
165
|
cleanupListeners.push(() =>
|
|
154
|
-
|
|
166
|
+
proxy.server.off("request", bufferRequestResponse)
|
|
155
167
|
);
|
|
156
168
|
return () => {
|
|
157
169
|
cleanupListeners.forEach((cleanup) => cleanup());
|
|
@@ -179,8 +191,8 @@ export function usePreviewServer({
|
|
|
179
191
|
port,
|
|
180
192
|
localProtocol
|
|
181
193
|
);
|
|
182
|
-
|
|
183
|
-
cleanupListeners.push(() =>
|
|
194
|
+
proxy.server.on("stream", handleStream);
|
|
195
|
+
cleanupListeners.push(() => proxy.server.off("stream", handleStream));
|
|
184
196
|
|
|
185
197
|
// flush and replay buffered streams
|
|
186
198
|
streamBufferRef.current.forEach((buffer) =>
|
|
@@ -236,9 +248,9 @@ export function usePreviewServer({
|
|
|
236
248
|
? createHandleAssetsRequest(assetPath, handleRequest)
|
|
237
249
|
: handleRequest;
|
|
238
250
|
|
|
239
|
-
|
|
251
|
+
proxy.server.on("request", actualHandleRequest);
|
|
240
252
|
cleanupListeners.push(() =>
|
|
241
|
-
|
|
253
|
+
proxy.server.off("request", actualHandleRequest)
|
|
242
254
|
);
|
|
243
255
|
|
|
244
256
|
// flush and replay buffered requests
|
|
@@ -270,8 +282,8 @@ export function usePreviewServer({
|
|
|
270
282
|
remoteWebsocketClient.close();
|
|
271
283
|
});
|
|
272
284
|
};
|
|
273
|
-
|
|
274
|
-
cleanupListeners.push(() =>
|
|
285
|
+
proxy.server.on("upgrade", handleUpgrade);
|
|
286
|
+
cleanupListeners.push(() => proxy.server.off("upgrade", handleUpgrade));
|
|
275
287
|
|
|
276
288
|
return () => {
|
|
277
289
|
cleanupListeners.forEach((cleanup) => cleanup());
|
|
@@ -281,7 +293,7 @@ export function usePreviewServer({
|
|
|
281
293
|
publicRoot,
|
|
282
294
|
port,
|
|
283
295
|
localProtocol,
|
|
284
|
-
|
|
296
|
+
proxy,
|
|
285
297
|
// We use a state value as a sigil to trigger reconnecting the server.
|
|
286
298
|
// It's not used inside the effect, so react-hooks/exhaustive-deps
|
|
287
299
|
// doesn't complain if it's not included in the dependency array.
|
|
@@ -293,7 +305,7 @@ export function usePreviewServer({
|
|
|
293
305
|
// containing component is mounted/unmounted.
|
|
294
306
|
useEffect(() => {
|
|
295
307
|
const abortController = new AbortController();
|
|
296
|
-
if (
|
|
308
|
+
if (proxy === undefined) {
|
|
297
309
|
return;
|
|
298
310
|
}
|
|
299
311
|
|
|
@@ -303,8 +315,10 @@ export function usePreviewServer({
|
|
|
303
315
|
abortSignal: abortController.signal,
|
|
304
316
|
})
|
|
305
317
|
.then(() => {
|
|
306
|
-
|
|
307
|
-
|
|
318
|
+
proxy.server.on("listening", () => {
|
|
319
|
+
logger.log(`⬣ Listening at ${localProtocol}://${ip}:${port}`);
|
|
320
|
+
});
|
|
321
|
+
proxy.server.listen(port, ip);
|
|
308
322
|
})
|
|
309
323
|
.catch((err) => {
|
|
310
324
|
if ((err as { code: string }).code !== "ABORT_ERR") {
|
|
@@ -313,10 +327,12 @@ export function usePreviewServer({
|
|
|
313
327
|
});
|
|
314
328
|
|
|
315
329
|
return () => {
|
|
316
|
-
proxyServer.close();
|
|
317
330
|
abortController.abort();
|
|
331
|
+
// Running `proxy.server.close()` does not close open connections, preventing the process from exiting.
|
|
332
|
+
// So we use this `terminator` to close all the connections and force the server to shutdown.
|
|
333
|
+
proxy.terminator.terminate();
|
|
318
334
|
};
|
|
319
|
-
}, [port, ip,
|
|
335
|
+
}, [port, ip, proxy, localProtocol]);
|
|
320
336
|
}
|
|
321
337
|
|
|
322
338
|
function createHandleAssetsRequest(
|
|
@@ -404,7 +420,7 @@ export async function waitForPortToBeAvailable(
|
|
|
404
420
|
): Promise<void> {
|
|
405
421
|
return new Promise((resolve, reject) => {
|
|
406
422
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
407
|
-
|
|
423
|
+
options.abortSignal.addEventListener("abort", () => {
|
|
408
424
|
const abortError = new Error("waitForPortToBeAvailable() aborted");
|
|
409
425
|
(abortError as Error & { code: string }).code = "ABORT_ERR";
|
|
410
426
|
doReject(abortError);
|
package/src/publish.ts
CHANGED
|
@@ -5,17 +5,26 @@ import { URLSearchParams } from "node:url";
|
|
|
5
5
|
import tmp from "tmp-promise";
|
|
6
6
|
import { bundleWorker } from "./bundle";
|
|
7
7
|
import { fetchResult } from "./cfetch";
|
|
8
|
+
import { printBindings } from "./config";
|
|
8
9
|
import { createWorkerUploadForm } from "./create-worker-upload-form";
|
|
10
|
+
import { confirm } from "./dialogs";
|
|
11
|
+
import { getMigrationsToUpload } from "./durable";
|
|
9
12
|
import { logger } from "./logger";
|
|
10
13
|
import { syncAssets } from "./sites";
|
|
11
14
|
import type { Config } from "./config";
|
|
15
|
+
import type {
|
|
16
|
+
Route,
|
|
17
|
+
ZoneIdRoute,
|
|
18
|
+
ZoneNameRoute,
|
|
19
|
+
CustomDomainRoute,
|
|
20
|
+
} from "./config/environment";
|
|
12
21
|
import type { Entry } from "./entry";
|
|
13
22
|
import type { AssetPaths } from "./sites";
|
|
14
23
|
import type { CfWorkerInit } from "./worker";
|
|
15
24
|
|
|
16
25
|
type Props = {
|
|
17
26
|
config: Config;
|
|
18
|
-
accountId: string;
|
|
27
|
+
accountId: string | undefined;
|
|
19
28
|
entry: Entry;
|
|
20
29
|
rules: Config["rules"];
|
|
21
30
|
name: string | undefined;
|
|
@@ -36,10 +45,178 @@ type Props = {
|
|
|
36
45
|
dryRun: boolean | undefined;
|
|
37
46
|
};
|
|
38
47
|
|
|
48
|
+
type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute;
|
|
49
|
+
|
|
39
50
|
function sleep(ms: number) {
|
|
40
51
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
function renderRoute(route: Route): string {
|
|
55
|
+
let result = "";
|
|
56
|
+
if (typeof route === "string") {
|
|
57
|
+
result = route;
|
|
58
|
+
} else {
|
|
59
|
+
result = route.pattern;
|
|
60
|
+
const isCustomDomain = Boolean(
|
|
61
|
+
"custom_domain" in route && route.custom_domain
|
|
62
|
+
);
|
|
63
|
+
if (isCustomDomain && "zone_id" in route) {
|
|
64
|
+
result += ` (custom domain - zone id: ${route.zone_id})`;
|
|
65
|
+
} else if (isCustomDomain && "zone_name" in route) {
|
|
66
|
+
result += ` (custom domain - zone name: ${route.zone_name})`;
|
|
67
|
+
} else if (isCustomDomain) {
|
|
68
|
+
result += ` (custom domain)`;
|
|
69
|
+
} else if ("zone_id" in route) {
|
|
70
|
+
result += ` (zone id: ${route.zone_id})`;
|
|
71
|
+
} else if ("zone_name" in route) {
|
|
72
|
+
result += ` (zone name: ${route.zone_name})`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// this function takes a string with quotes in it
|
|
79
|
+
// (i.e. `hello "world", if that really is your name`)
|
|
80
|
+
// and peels out the first instance of a substring
|
|
81
|
+
// bounded by quotes (so, in the example above, `world`)
|
|
82
|
+
//
|
|
83
|
+
// this is useful because the /domains api will return
|
|
84
|
+
// which domains conflicted in an error message, bounded
|
|
85
|
+
// by a string, which we can use to provide helpful
|
|
86
|
+
// messages to a user
|
|
87
|
+
function getQuoteBoundedSubstring(content: string) {
|
|
88
|
+
const matches = content.split('"');
|
|
89
|
+
return matches[1] ?? "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isOriginConflictError(
|
|
93
|
+
e: unknown
|
|
94
|
+
): e is { code: 100116; message: string; notes: Array<{ text: string }> } {
|
|
95
|
+
return (
|
|
96
|
+
typeof e === "object" &&
|
|
97
|
+
e !== null &&
|
|
98
|
+
(e as { code: number }).code === 100116
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isDNSConflictError(
|
|
103
|
+
e: unknown
|
|
104
|
+
): e is { code: 100117; message: string; notes: Array<{ text: string }> } {
|
|
105
|
+
return (
|
|
106
|
+
typeof e === "object" &&
|
|
107
|
+
e !== null &&
|
|
108
|
+
(e as { code: number }).code === 100117
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// empty error class to throw and then explicitly catch via `instanceof`
|
|
113
|
+
class CustomDomainOverrideRejected extends Error {}
|
|
114
|
+
|
|
115
|
+
// publishing to custom domains involves a few more steps than just updating
|
|
116
|
+
// the routing table, and thus the api implementing it is fairly defensive -
|
|
117
|
+
// it will error eagerly on conflicts against existing domains or existing
|
|
118
|
+
// managed DNS records
|
|
119
|
+
//
|
|
120
|
+
// however, you can pass params to override the errors. we start on the
|
|
121
|
+
// defensive path, and if one of these errors occur, we prompt the user
|
|
122
|
+
// for confirmation that they do indeed want to override the conflicts, and
|
|
123
|
+
// then retry the request with the right override added
|
|
124
|
+
//
|
|
125
|
+
// if a user does not confirm that they want to override, we skip publishing
|
|
126
|
+
// to these custom domains, but continue on through the rest of the
|
|
127
|
+
// publish stage
|
|
128
|
+
function publishCustomDomains(
|
|
129
|
+
workerUrl: string,
|
|
130
|
+
domains: Array<RouteObject>
|
|
131
|
+
): Promise<string[]> {
|
|
132
|
+
const config = {
|
|
133
|
+
override_scope: true,
|
|
134
|
+
override_existing_origin: false,
|
|
135
|
+
override_existing_dns_record: false,
|
|
136
|
+
};
|
|
137
|
+
const origins = domains.map((domainRoute) => {
|
|
138
|
+
return {
|
|
139
|
+
hostname: domainRoute.pattern,
|
|
140
|
+
zone_id: "zone_id" in domainRoute ? domainRoute.zone_id : undefined,
|
|
141
|
+
zone_name: "zone_name" in domainRoute ? domainRoute.zone_name : undefined,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!process.stdout.isTTY) {
|
|
146
|
+
// running in non-interactive mode.
|
|
147
|
+
// existing origins / dns records are not indicative of errors,
|
|
148
|
+
// so we aggressively update rather than aggressively fail
|
|
149
|
+
config.override_existing_origin = true;
|
|
150
|
+
config.override_existing_dns_record = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Mixing promise chains with async/await is funky, but it allows us to keep related
|
|
154
|
+
// logic synchronous-looking (i.e. prompting for confirmation and then re-requesting)
|
|
155
|
+
// while retaining the flexibility of promise chain fall-throughs. We can group error
|
|
156
|
+
// handling logic in dedicated catch calls, and all we have to do is re-throw an
|
|
157
|
+
// error and it will pass down to the next catch call
|
|
158
|
+
return fetchResult(`${workerUrl}/domains`, {
|
|
159
|
+
method: "PUT",
|
|
160
|
+
body: JSON.stringify({ ...config, origins }),
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
.catch(async (err) => {
|
|
166
|
+
if (isOriginConflictError(err)) {
|
|
167
|
+
const conflictingOrigins = getQuoteBoundedSubstring(err.notes[0].text);
|
|
168
|
+
const shouldContinue = await confirm(
|
|
169
|
+
`Custom Domains already exist for these domains: "${conflictingOrigins}"\nUpdate them to point to this script instead?`
|
|
170
|
+
);
|
|
171
|
+
if (!shouldContinue) {
|
|
172
|
+
throw new CustomDomainOverrideRejected();
|
|
173
|
+
}
|
|
174
|
+
config.override_existing_origin = true;
|
|
175
|
+
await fetchResult(`${workerUrl}/domains`, {
|
|
176
|
+
method: "PUT",
|
|
177
|
+
body: JSON.stringify({ ...config, origins }),
|
|
178
|
+
headers: {
|
|
179
|
+
"Content-Type": "application/json",
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.catch(async (err) => {
|
|
187
|
+
if (isDNSConflictError(err)) {
|
|
188
|
+
const conflictingOrigins = getQuoteBoundedSubstring(err.notes[0].text);
|
|
189
|
+
const shouldContinue = await confirm(
|
|
190
|
+
`You already have conflicting DNS records for these domains: "${conflictingOrigins}"\nUpdate them to point to this script instead?`
|
|
191
|
+
);
|
|
192
|
+
if (!shouldContinue) {
|
|
193
|
+
throw new CustomDomainOverrideRejected();
|
|
194
|
+
}
|
|
195
|
+
config.override_existing_dns_record = true;
|
|
196
|
+
await fetchResult(`${workerUrl}/domains`, {
|
|
197
|
+
method: "PUT",
|
|
198
|
+
body: JSON.stringify({ ...config, origins }),
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
.then(() => domains.map((domain) => renderRoute(domain)))
|
|
208
|
+
.catch((err) => {
|
|
209
|
+
if (err instanceof CustomDomainOverrideRejected) {
|
|
210
|
+
return [
|
|
211
|
+
domains.length > 1
|
|
212
|
+
? `Publishing to ${domains.length} Custom Domains was skipped, fix conflicts and try again`
|
|
213
|
+
: `Publishing to Custom Domain "${domains[0].pattern}" was skipped, fix conflict and try again`,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
throw err;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
43
220
|
export default async function publish(props: Props): Promise<void> {
|
|
44
221
|
// TODO: warn if git/hg has uncommitted changes
|
|
45
222
|
const { config, accountId } = props;
|
|
@@ -52,6 +229,25 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
52
229
|
const triggers = props.triggers || config.triggers?.crons;
|
|
53
230
|
const routes =
|
|
54
231
|
props.routes ?? config.routes ?? (config.route ? [config.route] : []) ?? [];
|
|
232
|
+
const routesOnly: Array<Route> = [];
|
|
233
|
+
const customDomainsOnly: Array<RouteObject> = [];
|
|
234
|
+
for (const route of routes) {
|
|
235
|
+
if (typeof route !== "string" && route.custom_domain) {
|
|
236
|
+
if (route.pattern.includes("*")) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Cannot use "${route.pattern}" as a Custom Domain; wildcard operators (*) are not allowed`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (route.pattern.includes("/")) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Cannot use "${route.pattern}" as a Custom Domain; paths are not allowed`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
customDomainsOnly.push(route);
|
|
247
|
+
} else {
|
|
248
|
+
routesOnly.push(route);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
55
251
|
|
|
56
252
|
// deployToWorkersDev defaults to true only if there aren't any routes defined
|
|
57
253
|
const deployToWorkersDev = config.workers_dev ?? routes.length === 0;
|
|
@@ -143,103 +339,19 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
143
339
|
}
|
|
144
340
|
);
|
|
145
341
|
|
|
146
|
-
// Some validation of durable objects + migrations
|
|
147
|
-
if (config.durable_objects.bindings.length > 0) {
|
|
148
|
-
// intrinsic [durable_objects] implies [migrations]
|
|
149
|
-
const exportedDurableObjects = config.durable_objects.bindings.filter(
|
|
150
|
-
(binding) => !binding.script_name
|
|
151
|
-
);
|
|
152
|
-
if (exportedDurableObjects.length > 0 && config.migrations.length === 0) {
|
|
153
|
-
logger.warn(
|
|
154
|
-
`In wrangler.toml, you have configured [durable_objects] exported by this Worker (${exportedDurableObjects.map(
|
|
155
|
-
(durable) => durable.class_name
|
|
156
|
-
)}), but no [migrations] for them. This may not work as expected until you add a [migrations] section to your wrangler.toml. Refer to https://developers.cloudflare.com/workers/learning/using-durable-objects/#durable-object-migrations-in-wranglertoml for more details.`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
342
|
const content = readFileSync(resolvedEntryPointPath, {
|
|
162
343
|
encoding: "utf-8",
|
|
163
344
|
});
|
|
164
345
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const scriptData = await fetchResult<{
|
|
175
|
-
script: ScriptData;
|
|
176
|
-
}>(
|
|
177
|
-
`/accounts/${accountId}/workers/services/${scriptName}/environments/${props.env}`
|
|
178
|
-
);
|
|
179
|
-
script = scriptData.script;
|
|
180
|
-
} else {
|
|
181
|
-
const scriptData = await fetchResult<{
|
|
182
|
-
default_environment: {
|
|
183
|
-
script: ScriptData;
|
|
184
|
-
};
|
|
185
|
-
}>(`/accounts/${accountId}/workers/services/${scriptName}`);
|
|
186
|
-
script = scriptData.default_environment.script;
|
|
187
|
-
}
|
|
188
|
-
} catch (err) {
|
|
189
|
-
if (
|
|
190
|
-
![
|
|
191
|
-
10090, // corresponds to workers.api.error.service_not_found, so the script wasn't previously published at all
|
|
192
|
-
10092, // workers.api.error.environment_not_found, so the script wasn't published to this environment yet
|
|
193
|
-
].includes((err as { code: number }).code)
|
|
194
|
-
) {
|
|
195
|
-
throw err;
|
|
196
|
-
}
|
|
197
|
-
// else it's a 404, no script found, and we can proceed
|
|
198
|
-
}
|
|
199
|
-
} else {
|
|
200
|
-
const scripts = await fetchResult<ScriptData[]>(
|
|
201
|
-
`/accounts/${accountId}/workers/scripts`
|
|
202
|
-
);
|
|
203
|
-
script = scripts.find(({ id }) => id === scriptName);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (script?.migration_tag) {
|
|
207
|
-
// was already published once
|
|
208
|
-
const scriptMigrationTag = script.migration_tag;
|
|
209
|
-
const foundIndex = config.migrations.findIndex(
|
|
210
|
-
(migration) => migration.tag === scriptMigrationTag
|
|
211
|
-
);
|
|
212
|
-
if (foundIndex === -1) {
|
|
213
|
-
logger.warn(
|
|
214
|
-
`The published script ${scriptName} has a migration tag "${script.migration_tag}, which was not found in wrangler.toml. You may have already deleted it. Applying all available migrations to the script...`
|
|
215
|
-
);
|
|
216
|
-
migrations = {
|
|
217
|
-
old_tag: script.migration_tag,
|
|
218
|
-
new_tag: config.migrations[config.migrations.length - 1].tag,
|
|
219
|
-
steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
|
|
220
|
-
};
|
|
221
|
-
} else {
|
|
222
|
-
if (foundIndex !== config.migrations.length - 1) {
|
|
223
|
-
// there are new migrations to send up
|
|
224
|
-
migrations = {
|
|
225
|
-
old_tag: script.migration_tag,
|
|
226
|
-
new_tag: config.migrations[config.migrations.length - 1].tag,
|
|
227
|
-
steps: config.migrations
|
|
228
|
-
.slice(foundIndex + 1)
|
|
229
|
-
.map(({ tag: _tag, ...rest }) => rest),
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
// else, we're up to date, no migrations to send
|
|
233
|
-
}
|
|
234
|
-
} else {
|
|
235
|
-
// first time publishing durable objects to this script,
|
|
236
|
-
// so we send all the migrations
|
|
237
|
-
migrations = {
|
|
238
|
-
new_tag: config.migrations[config.migrations.length - 1].tag,
|
|
239
|
-
steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
}
|
|
346
|
+
// durable object migrations
|
|
347
|
+
const migrations = !props.dryRun
|
|
348
|
+
? await getMigrationsToUpload(scriptName, {
|
|
349
|
+
accountId,
|
|
350
|
+
config,
|
|
351
|
+
legacyEnv: props.legacyEnv,
|
|
352
|
+
env: props.env,
|
|
353
|
+
})
|
|
354
|
+
: undefined;
|
|
243
355
|
|
|
244
356
|
const assets = await syncAssets(
|
|
245
357
|
accountId,
|
|
@@ -271,6 +383,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
271
383
|
data_blobs: config.data_blobs,
|
|
272
384
|
durable_objects: config.durable_objects,
|
|
273
385
|
r2_buckets: config.r2_buckets,
|
|
386
|
+
services: config.services,
|
|
274
387
|
unsafe: config.unsafe?.bindings,
|
|
275
388
|
};
|
|
276
389
|
|
|
@@ -298,6 +411,13 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
298
411
|
usage_model: config.usage_model,
|
|
299
412
|
};
|
|
300
413
|
|
|
414
|
+
const withoutStaticAssets = {
|
|
415
|
+
...bindings,
|
|
416
|
+
kv_namespaces: config.kv_namespaces,
|
|
417
|
+
text_blobs: config.text_blobs,
|
|
418
|
+
};
|
|
419
|
+
printBindings(withoutStaticAssets);
|
|
420
|
+
|
|
301
421
|
if (!props.dryRun) {
|
|
302
422
|
// Upload the script so it has time to propagate.
|
|
303
423
|
// We can also now tell whether available_on_subdomain is set
|
|
@@ -324,6 +444,7 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
324
444
|
logger.log(`--dry-run: exiting now.`);
|
|
325
445
|
return;
|
|
326
446
|
}
|
|
447
|
+
assert(accountId, "Missing accountId");
|
|
327
448
|
|
|
328
449
|
const uploadMs = Date.now() - start;
|
|
329
450
|
const deployments: Promise<string[]>[] = [];
|
|
@@ -374,13 +495,13 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
374
495
|
logger.log("Uploaded", workerName, formatTime(uploadMs));
|
|
375
496
|
|
|
376
497
|
// Update routing table for the script.
|
|
377
|
-
if (
|
|
498
|
+
if (routesOnly.length > 0) {
|
|
378
499
|
deployments.push(
|
|
379
500
|
fetchResult(`${workerUrl}/routes`, {
|
|
380
501
|
// Note: PUT will delete previous routes on this script.
|
|
381
502
|
method: "PUT",
|
|
382
503
|
body: JSON.stringify(
|
|
383
|
-
|
|
504
|
+
routesOnly.map((route) =>
|
|
384
505
|
typeof route !== "object" ? { pattern: route } : route
|
|
385
506
|
)
|
|
386
507
|
),
|
|
@@ -388,29 +509,22 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
388
509
|
"Content-Type": "application/json",
|
|
389
510
|
},
|
|
390
511
|
}).then(() => {
|
|
391
|
-
if (
|
|
392
|
-
return
|
|
512
|
+
if (routesOnly.length > 10) {
|
|
513
|
+
return routesOnly
|
|
393
514
|
.slice(0, 9)
|
|
394
|
-
.map((route) =>
|
|
395
|
-
|
|
396
|
-
? route
|
|
397
|
-
: "zone_id" in route
|
|
398
|
-
? `${route.pattern} (zone id: ${route.zone_id})`
|
|
399
|
-
: `${route.pattern} (zone name: ${route.zone_name})`
|
|
400
|
-
)
|
|
401
|
-
.concat([`...and ${routes.length - 10} more routes`]);
|
|
515
|
+
.map((route) => renderRoute(route))
|
|
516
|
+
.concat([`...and ${routesOnly.length - 10} more routes`]);
|
|
402
517
|
}
|
|
403
|
-
return
|
|
404
|
-
typeof route === "string"
|
|
405
|
-
? route
|
|
406
|
-
: "zone_id" in route
|
|
407
|
-
? `${route.pattern} (zone id: ${route.zone_id})`
|
|
408
|
-
: `${route.pattern} (zone name: ${route.zone_name})`
|
|
409
|
-
);
|
|
518
|
+
return routesOnly.map((route) => renderRoute(route));
|
|
410
519
|
})
|
|
411
520
|
);
|
|
412
521
|
}
|
|
413
522
|
|
|
523
|
+
// Update custom domains for the script
|
|
524
|
+
if (customDomainsOnly.length > 0) {
|
|
525
|
+
deployments.push(publishCustomDomains(workerUrl, customDomainsOnly));
|
|
526
|
+
}
|
|
527
|
+
|
|
414
528
|
// Configure any schedules for the script.
|
|
415
529
|
// TODO: rename this to `schedules`?
|
|
416
530
|
if (triggers && triggers.length) {
|
package/src/sites.tsx
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
1
2
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import ignore from "ignore";
|
|
4
5
|
import xxhash from "xxhash-wasm";
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
createKVNamespace,
|
|
8
|
+
listKVNamespaceKeys,
|
|
9
|
+
listKVNamespaces,
|
|
10
|
+
putKVBulkKeyValue,
|
|
11
|
+
deleteKVBulkKeyValue,
|
|
11
12
|
} from "./kv";
|
|
12
13
|
import { logger } from "./logger";
|
|
13
14
|
import type { Config } from "./config";
|
|
@@ -75,14 +76,14 @@ async function createKVNamespaceIfNotAlreadyExisting(
|
|
|
75
76
|
) {
|
|
76
77
|
// check if it already exists
|
|
77
78
|
// TODO: this is super inefficient, should be made better
|
|
78
|
-
const namespaces = await
|
|
79
|
+
const namespaces = await listKVNamespaces(accountId);
|
|
79
80
|
const found = namespaces.find((x) => x.title === title);
|
|
80
81
|
if (found) {
|
|
81
82
|
return { created: false, id: found.id };
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
// else we make the namespace
|
|
85
|
-
const id = await
|
|
86
|
+
const id = await createKVNamespace(accountId, title);
|
|
86
87
|
logger.log(`🌀 Created namespace for Workers Site "${title}"`);
|
|
87
88
|
|
|
88
89
|
return {
|
|
@@ -103,7 +104,7 @@ async function createKVNamespaceIfNotAlreadyExisting(
|
|
|
103
104
|
* asset in the KV namespace.
|
|
104
105
|
*/
|
|
105
106
|
export async function syncAssets(
|
|
106
|
-
accountId: string,
|
|
107
|
+
accountId: string | undefined,
|
|
107
108
|
scriptName: string,
|
|
108
109
|
siteAssets: AssetPaths | undefined,
|
|
109
110
|
preview: boolean,
|
|
@@ -120,6 +121,7 @@ export async function syncAssets(
|
|
|
120
121
|
logger.log("(Note: doing a dry run, not uploading or deleting anything.)");
|
|
121
122
|
return { manifest: undefined, namespace: undefined };
|
|
122
123
|
}
|
|
124
|
+
assert(accountId, "Missing accountId");
|
|
123
125
|
|
|
124
126
|
const title = `__${scriptName}-workers_sites_assets${
|
|
125
127
|
preview ? "_preview" : ""
|
|
@@ -131,7 +133,7 @@ export async function syncAssets(
|
|
|
131
133
|
);
|
|
132
134
|
|
|
133
135
|
// let's get all the keys in this namespace
|
|
134
|
-
const namespaceKeysResponse = await
|
|
136
|
+
const namespaceKeysResponse = await listKVNamespaceKeys(accountId, namespace);
|
|
135
137
|
const namespaceKeys = new Set(namespaceKeysResponse.map((x) => x.name));
|
|
136
138
|
|
|
137
139
|
const manifest: Record<string, string> = {};
|
|
@@ -185,14 +187,9 @@ export async function syncAssets(
|
|
|
185
187
|
|
|
186
188
|
await Promise.all([
|
|
187
189
|
// upload all the new assets
|
|
188
|
-
|
|
190
|
+
putKVBulkKeyValue(accountId, namespace, toUpload),
|
|
189
191
|
// delete all the unused assets
|
|
190
|
-
|
|
191
|
-
accountId,
|
|
192
|
-
namespace,
|
|
193
|
-
Array.from(namespaceKeys),
|
|
194
|
-
() => {}
|
|
195
|
-
),
|
|
192
|
+
deleteKVBulkKeyValue(accountId, namespace, Array.from(namespaceKeys)),
|
|
196
193
|
]);
|
|
197
194
|
|
|
198
195
|
logger.log("↗️ Done syncing assets");
|