wrangler 0.0.0-e95e5a0 → 0.0.0-e978986

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 (52) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +3 -1
  3. package/bin/wrangler.js +36 -4
  4. package/package.json +11 -7
  5. package/pages/functions/buildPlugin.ts +13 -0
  6. package/pages/functions/buildWorker.ts +14 -1
  7. package/pages/functions/template-plugin.ts +3 -2
  8. package/src/__tests__/configuration.test.ts +336 -87
  9. package/src/__tests__/dev.test.tsx +243 -25
  10. package/src/__tests__/helpers/mock-dialogs.ts +41 -1
  11. package/src/__tests__/index.test.ts +79 -72
  12. package/src/__tests__/init.test.ts +182 -131
  13. package/src/__tests__/jest.setup.ts +8 -0
  14. package/src/__tests__/kv.test.ts +173 -146
  15. package/src/__tests__/package-manager.test.ts +154 -7
  16. package/src/__tests__/pages.test.ts +381 -53
  17. package/src/__tests__/parse.test.ts +8 -0
  18. package/src/__tests__/publish.test.ts +682 -89
  19. package/src/__tests__/r2.test.ts +55 -36
  20. package/src/__tests__/secret.test.ts +39 -4
  21. package/src/abort.d.ts +3 -0
  22. package/src/bundle.ts +33 -2
  23. package/src/cfetch/index.ts +21 -4
  24. package/src/cfetch/internal.ts +17 -9
  25. package/src/config/config.ts +1 -1
  26. package/src/config/environment.ts +40 -14
  27. package/src/config/index.ts +162 -0
  28. package/src/config/validation.ts +180 -65
  29. package/src/create-worker-preview.ts +17 -7
  30. package/src/create-worker-upload-form.ts +22 -8
  31. package/src/dev/dev.tsx +7 -6
  32. package/src/dev/local.tsx +6 -0
  33. package/src/dev/remote.tsx +15 -1
  34. package/src/dialogs.tsx +48 -0
  35. package/src/durable.ts +102 -0
  36. package/src/entry.ts +64 -9
  37. package/src/index.tsx +357 -177
  38. package/src/inspect.ts +39 -0
  39. package/src/kv.ts +114 -25
  40. package/src/open-in-browser.ts +5 -12
  41. package/src/package-manager.ts +50 -3
  42. package/src/pages.tsx +271 -110
  43. package/src/parse.ts +23 -4
  44. package/src/proxy.ts +38 -22
  45. package/src/publish.ts +227 -113
  46. package/src/sites.tsx +19 -17
  47. package/src/user.tsx +11 -1
  48. package/src/worker.ts +8 -0
  49. package/templates/new-worker-scheduled.js +17 -0
  50. package/templates/new-worker-scheduled.ts +32 -0
  51. package/templates/new-worker.ts +16 -1
  52. package/wrangler-dist/cli.js +36017 -22687
package/src/parse.ts CHANGED
@@ -62,8 +62,6 @@ export class ParseError extends Error implements Message {
62
62
  }
63
63
  }
64
64
 
65
- /* eslint-disable @typescript-eslint/no-explicit-any */
66
-
67
65
  const TOML_ERROR_NAME = "TomlError";
68
66
  const TOML_ERROR_SUFFIX = " at row ";
69
67
 
@@ -77,7 +75,9 @@ type TomlError = Error & {
77
75
  */
78
76
  export function parseTOML(input: string, file?: string): TOML.JsonMap | never {
79
77
  try {
80
- return TOML.parse(input);
78
+ // Normalize CRLF to LF to avoid hitting https://github.com/iarna/iarna-toml/issues/33.
79
+ const normalizedInput = input.replace(/\r\n/g, "\n");
80
+ return TOML.parse(normalizedInput);
81
81
  } catch (err) {
82
82
  const { name, message, line, col } = err as TomlError;
83
83
  if (name !== TOML_ERROR_NAME) {
@@ -98,10 +98,29 @@ export function parseTOML(input: string, file?: string): TOML.JsonMap | never {
98
98
 
99
99
  const JSON_ERROR_SUFFIX = " in JSON at position ";
100
100
 
101
+ /**
102
+ * A minimal type describing a package.json file.
103
+ */
104
+ export type PackageJSON = {
105
+ devDependencies?: Record<string, unknown>;
106
+ dependencies?: Record<string, unknown>;
107
+ scripts?: Record<string, unknown>;
108
+ };
109
+
110
+ /**
111
+ * A typed version of `parseJSON()`.
112
+ */
113
+ export function parsePackageJSON<T extends PackageJSON = PackageJSON>(
114
+ input: string,
115
+ file?: string
116
+ ): T {
117
+ return parseJSON<T>(input, file);
118
+ }
119
+
101
120
  /**
102
121
  * A wrapper around `JSON.parse` that throws a `ParseError`.
103
122
  */
104
- export function parseJSON(input: string, file?: string): any {
123
+ export function parseJSON<T>(input: string, file?: string): T {
105
124
  try {
106
125
  return JSON.parse(input);
107
126
  } catch (err) {
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) {