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/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 [proxyServer, setProxyServer] = useState<HttpServer | HttpsServer>();
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 (proxyServer === undefined) {
98
+ if (proxy === undefined) {
92
99
  createProxyServer(localProtocol)
93
- .then((proxy) => setProxyServer(proxy))
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
- }, [proxyServer, localProtocol]);
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 (proxyServer === undefined) {
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
- proxyServer.on("stream", bufferStream);
142
- cleanupListeners.push(() => proxyServer.off("stream", bufferStream));
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
- proxyServer.on("request", bufferRequestResponse);
164
+ proxy.server.on("request", bufferRequestResponse);
153
165
  cleanupListeners.push(() =>
154
- proxyServer.off("request", bufferRequestResponse)
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
- proxyServer.on("stream", handleStream);
183
- cleanupListeners.push(() => proxyServer.off("stream", handleStream));
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
- proxyServer.on("request", actualHandleRequest);
251
+ proxy.server.on("request", actualHandleRequest);
240
252
  cleanupListeners.push(() =>
241
- proxyServer.off("request", actualHandleRequest)
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
- proxyServer.on("upgrade", handleUpgrade);
274
- cleanupListeners.push(() => proxyServer.off("upgrade", handleUpgrade));
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
- proxyServer,
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 (proxyServer === undefined) {
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
- proxyServer.listen(port, ip);
307
- logger.log(`⬣ Listening at ${localProtocol}://${ip}:${port}`);
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, proxyServer, localProtocol]);
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
- (options.abortSignal as any).addEventListener("abort", () => {
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
- // if config.migrations
166
- let migrations;
167
- if (config.migrations.length > 0) {
168
- // get current migration tag
169
- type ScriptData = { id: string; migration_tag?: string };
170
- let script: ScriptData | undefined;
171
- if (!props.legacyEnv) {
172
- try {
173
- if (props.env) {
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 (routes.length > 0) {
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
- routes.map((route) =>
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 (routes.length > 10) {
392
- return routes
512
+ if (routesOnly.length > 10) {
513
+ return routesOnly
393
514
  .slice(0, 9)
394
- .map((route) =>
395
- typeof route === "string"
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 routes.map((route) =>
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
- createNamespace,
7
- listNamespaceKeys,
8
- listNamespaces,
9
- putBulkKeyValue,
10
- deleteBulkKeyValue,
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 listNamespaces(accountId);
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 createNamespace(accountId, title);
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 listNamespaceKeys(accountId, namespace);
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
- putBulkKeyValue(accountId, namespace, toUpload, () => {}),
190
+ putKVBulkKeyValue(accountId, namespace, toUpload),
189
191
  // delete all the unused assets
190
- deleteBulkKeyValue(
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");