wrangler 0.0.0-e3cab74 → 0.0.0-e452a35

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 (102) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +27 -14
  3. package/bin/wrangler.js +49 -5
  4. package/miniflare-dist/index.mjs +4886 -0
  5. package/package.json +42 -28
  6. package/pages/functions/buildPlugin.ts +105 -0
  7. package/pages/functions/buildWorker.ts +93 -4
  8. package/pages/functions/filepath-routing.test.ts +136 -25
  9. package/pages/functions/filepath-routing.ts +14 -3
  10. package/pages/functions/routes.ts +6 -1
  11. package/pages/functions/template-plugin.ts +147 -0
  12. package/pages/functions/template-worker.ts +21 -8
  13. package/src/__tests__/config-cache.test.ts +36 -0
  14. package/src/__tests__/configuration.test.ts +1514 -367
  15. package/src/__tests__/dev.test.tsx +870 -84
  16. package/src/__tests__/guess-worker-format.test.ts +37 -18
  17. package/src/__tests__/helpers/mock-account-id.ts +22 -4
  18. package/src/__tests__/helpers/mock-cfetch.ts +8 -4
  19. package/src/__tests__/helpers/mock-console.ts +35 -10
  20. package/src/__tests__/helpers/mock-dialogs.ts +26 -1
  21. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  22. package/src/__tests__/helpers/mock-istty.ts +27 -0
  23. package/src/__tests__/helpers/mock-kv.ts +9 -16
  24. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  25. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  26. package/src/__tests__/helpers/run-in-tmp.ts +10 -0
  27. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  28. package/src/__tests__/https-options.test.ts +130 -0
  29. package/src/__tests__/index.test.ts +137 -747
  30. package/src/__tests__/init.test.ts +1557 -0
  31. package/src/__tests__/jest.setup.ts +28 -8
  32. package/src/__tests__/kv.test.ts +598 -258
  33. package/src/__tests__/logger.test.ts +115 -0
  34. package/src/__tests__/pages.test.ts +657 -0
  35. package/src/__tests__/parse.test.ts +8 -0
  36. package/src/__tests__/publish.test.ts +4003 -941
  37. package/src/__tests__/r2.test.ts +69 -34
  38. package/src/__tests__/route.test.ts +45 -0
  39. package/src/__tests__/secret.test.ts +228 -73
  40. package/src/__tests__/tail.test.ts +146 -20
  41. package/src/__tests__/user.test.ts +131 -0
  42. package/src/__tests__/whoami.test.tsx +26 -7
  43. package/src/abort.d.ts +3 -0
  44. package/src/bundle.ts +74 -7
  45. package/src/cfetch/index.ts +21 -4
  46. package/src/cfetch/internal.ts +20 -14
  47. package/src/cli.ts +5 -11
  48. package/src/config/README.md +7 -7
  49. package/src/config/config.ts +69 -100
  50. package/src/config/environment.ts +119 -4
  51. package/src/config/index.ts +177 -4
  52. package/src/config/validation-helpers.ts +228 -23
  53. package/src/config/validation.ts +687 -174
  54. package/src/config-cache.ts +45 -0
  55. package/src/{api/preview.ts → create-worker-preview.ts} +74 -36
  56. package/src/{api/form_data.ts → create-worker-upload-form.ts} +55 -18
  57. package/src/dev/dev-vars.ts +36 -0
  58. package/src/dev/dev.tsx +422 -0
  59. package/src/dev/local.tsx +318 -0
  60. package/src/dev/remote.tsx +227 -0
  61. package/src/dev/use-esbuild.ts +118 -0
  62. package/src/dialogs.tsx +16 -6
  63. package/src/durable.ts +102 -0
  64. package/src/entry.ts +279 -14
  65. package/src/environment-variables.ts +3 -1
  66. package/src/errors.ts +11 -0
  67. package/src/https-options.ts +123 -0
  68. package/src/index.tsx +1268 -806
  69. package/src/inspect.ts +64 -11
  70. package/src/intl-polyfill.d.ts +139 -0
  71. package/src/{kv.tsx → kv.ts} +195 -62
  72. package/src/logger.ts +66 -0
  73. package/src/miniflare-cli/README.md +30 -0
  74. package/src/miniflare-cli/enum-keys.ts +17 -0
  75. package/src/miniflare-cli/index.ts +41 -0
  76. package/src/miniflare-cli/request-context.ts +40 -0
  77. package/src/module-collection.ts +94 -6
  78. package/src/open-in-browser.ts +9 -3
  79. package/src/package-manager.ts +9 -8
  80. package/src/pages.tsx +1057 -75
  81. package/src/parse.ts +24 -5
  82. package/src/proxy.ts +143 -54
  83. package/src/publish.ts +444 -229
  84. package/src/selfsigned.d.ts +29 -0
  85. package/src/sites.tsx +98 -44
  86. package/src/tail/index.ts +2 -2
  87. package/src/tail/printing.ts +13 -8
  88. package/src/update-check.ts +19 -0
  89. package/src/user.tsx +263 -109
  90. package/src/whoami.tsx +2 -1
  91. package/src/{api/worker.ts → worker.ts} +24 -30
  92. package/templates/gitignore +171 -0
  93. package/templates/new-worker.ts +16 -1
  94. package/templates/static-asset-facade.js +1 -1
  95. package/wrangler-dist/cli.js +55345 -69246
  96. package/src/__tests__/helpers/mock-user.ts +0 -27
  97. package/src/__tests__/logout.test.ts +0 -50
  98. package/src/__tests__/sentry.test.ts +0 -146
  99. package/src/dev.tsx +0 -961
  100. package/src/guess-worker-format.ts +0 -68
  101. package/src/reporting.ts +0 -115
  102. package/wrangler-dist/cli.js.map +0 -7
package/src/publish.ts CHANGED
@@ -1,70 +1,268 @@
1
1
  import assert from "node:assert";
2
- import { readFileSync } from "node:fs";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { URLSearchParams } from "node:url";
5
- import { execaCommand } from "execa";
6
5
  import tmp from "tmp-promise";
7
- import { toFormData } from "./api/form_data";
8
6
  import { bundleWorker } from "./bundle";
9
7
  import { fetchResult } from "./cfetch";
10
- import { fileExists } from "./entry";
11
- import guessWorkerFormat from "./guess-worker-format";
8
+ import { printBindings } from "./config";
9
+ import { createWorkerUploadForm } from "./create-worker-upload-form";
10
+ import { confirm } from "./dialogs";
11
+ import { getMigrationsToUpload } from "./durable";
12
+ import { logger } from "./logger";
12
13
  import { syncAssets } from "./sites";
13
- import type { CfScriptFormat, CfWorkerInit } from "./api/worker";
14
14
  import type { Config } from "./config";
15
+ import type {
16
+ Route,
17
+ ZoneIdRoute,
18
+ ZoneNameRoute,
19
+ CustomDomainRoute,
20
+ } from "./config/environment";
21
+ import type { Entry } from "./entry";
15
22
  import type { AssetPaths } from "./sites";
23
+ import type { CfWorkerInit } from "./worker";
16
24
 
17
25
  type Props = {
18
26
  config: Config;
19
- format: CfScriptFormat | undefined;
20
- entry: { file: string; directory: string };
27
+ accountId: string | undefined;
28
+ entry: Entry;
21
29
  rules: Config["rules"];
22
30
  name: string | undefined;
23
31
  env: string | undefined;
24
32
  compatibilityDate: string | undefined;
25
33
  compatibilityFlags: string[] | undefined;
26
34
  assetPaths: AssetPaths | undefined;
27
- triggers: (string | number)[] | undefined;
28
- routes: (string | number)[] | undefined;
35
+ triggers: string[] | undefined;
36
+ routes: string[] | undefined;
29
37
  legacyEnv: boolean | undefined;
30
- jsxFactory: undefined | string;
31
- jsxFragment: undefined | string;
38
+ jsxFactory: string | undefined;
39
+ jsxFragment: string | undefined;
40
+ tsconfig: string | undefined;
32
41
  experimentalPublic: boolean;
42
+ minify: boolean | undefined;
43
+ nodeCompat: boolean | undefined;
44
+ outDir: string | undefined;
45
+ dryRun: boolean | undefined;
33
46
  };
34
47
 
48
+ type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute;
49
+
35
50
  function sleep(ms: number) {
36
51
  return new Promise((resolve) => setTimeout(resolve, ms));
37
52
  }
38
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
+
39
220
  export default async function publish(props: Props): Promise<void> {
40
221
  // TODO: warn if git/hg has uncommitted changes
41
- const { config } = props;
42
-
43
- // TODO: should we automatically fallback to top level config if there is no matching environment??
44
- const envRootObj = (props.env && config.env[props.env]) || config;
222
+ const { config, accountId } = props;
45
223
 
46
224
  assert(
47
- envRootObj.compatibility_date || props.compatibilityDate,
48
- "A compatibility_date is required when publishing. Add one to your wrangler.toml file, or pass it in your terminal as --compatibility_date. See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information."
225
+ props.compatibilityDate || config.compatibility_date,
226
+ "A compatibility_date is required when publishing. Add one to your wrangler.toml file, or pass it in your terminal as --compatibility-date. See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information."
49
227
  );
50
228
 
51
- const triggers = props.triggers || envRootObj.triggers?.crons;
229
+ const triggers = props.triggers || config.triggers?.crons;
52
230
  const routes =
53
- props.routes ??
54
- envRootObj.routes ??
55
- (envRootObj.route ? [envRootObj.route] : []) ??
56
- [];
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
+ }
57
251
 
58
- const { account_id: accountId, workers_dev: deployToWorkersDev } = config;
252
+ // deployToWorkersDev defaults to true only if there aren't any routes defined
253
+ const deployToWorkersDev = config.workers_dev ?? routes.length === 0;
59
254
 
60
- if (accountId === undefined) {
61
- throw new Error("No account_id provided.");
62
- }
255
+ const jsxFactory = props.jsxFactory || config.jsx_factory;
256
+ const jsxFragment = props.jsxFragment || config.jsx_fragment;
63
257
 
64
- const jsxFactory = props.jsxFactory || envRootObj.jsx_factory;
65
- const jsxFragment = props.jsxFragment || envRootObj.jsx_fragment;
258
+ const minify = props.minify ?? config.minify;
66
259
 
67
- assert(config.account_id, "missing account id");
260
+ const nodeCompat = props.nodeCompat ?? config.node_compat;
261
+ if (nodeCompat) {
262
+ logger.warn(
263
+ "Enabling node.js compatibility mode for builtins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
264
+ );
265
+ }
68
266
 
69
267
  const scriptName = props.name;
70
268
  assert(
@@ -72,74 +270,72 @@ export default async function publish(props: Props): Promise<void> {
72
270
  'You need to provide a name when publishing a worker. Either pass it as a cli arg with `--name <name>` or in your config file as `name = "<name>"`'
73
271
  );
74
272
 
75
- if (config.site?.["entry-point"]) {
76
- console.warn(
77
- "Deprecation notice: The `site.entry-point` config field is no longer used.\n" +
78
- "The entry-point should be specified via the command line (e.g. `wrangler publish path/to/script`) or the `main` config field.\n" +
79
- "Please remove the `site.entry-point` field from the `wrangler.toml` file."
80
- );
81
- }
82
-
83
273
  assert(
84
274
  !config.site || config.site.bucket,
85
275
  "A [site] definition requires a `bucket` field with a path to the site's public directory."
86
276
  );
87
277
 
88
- const destination = await tmp.dir({ unsafeCleanup: true });
89
- try {
90
- const envName = props.env ?? "production";
91
-
92
- if (config.build.command) {
93
- // TODO: add a deprecation message here?
94
- console.log("running:", config.build.command);
95
- await execaCommand(config.build.command, {
96
- shell: true,
97
- stdout: "inherit",
98
- stderr: "inherit",
99
- timeout: 1000 * 30,
100
- ...(config.build.cwd && { cwd: config.build.cwd }),
101
- });
278
+ if (props.outDir) {
279
+ // we're using a custom output directory,
280
+ // so let's first ensure it exists
281
+ mkdirSync(props.outDir, { recursive: true });
282
+ // add a README
283
+ const readmePath = path.join(props.outDir, "README.md");
284
+ writeFileSync(
285
+ readmePath,
286
+ `This folder contains the built output assets for the worker "${scriptName}" generated at ${new Date().toISOString()}.`
287
+ );
288
+ }
102
289
 
103
- if (fileExists(props.entry.file) === false) {
104
- throw new Error(
105
- `Could not resolve "${path.relative(
106
- process.cwd(),
107
- props.entry.file
108
- )}".`
109
- );
110
- }
111
- }
290
+ const destination = props.outDir ?? (await tmp.dir({ unsafeCleanup: true }));
291
+ const envName = props.env ?? "production";
112
292
 
113
- const format = await guessWorkerFormat(props.entry, props.format);
293
+ const start = Date.now();
294
+ const notProd = !props.legacyEnv && props.env;
295
+ const workerName = notProd ? `${scriptName} (${envName})` : scriptName;
296
+ const workerUrl = notProd
297
+ ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}`
298
+ : `/accounts/${accountId}/workers/scripts/${scriptName}`;
114
299
 
115
- if (props.experimentalPublic && format === "service-worker") {
116
- // TODO: check config too
117
- throw new Error(
118
- "You cannot publish in the service worker format with a public directory."
119
- );
120
- }
300
+ let available_on_subdomain; // we'll set this later
121
301
 
122
- if (config.wasm_modules && format === "modules") {
123
- throw new Error(
124
- "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"
125
- );
126
- }
302
+ const { format } = props.entry;
127
303
 
128
- if (config.text_blobs && format === "modules") {
129
- throw new Error(
130
- "You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[build.upload.rules]` in your wrangler.toml"
131
- );
132
- }
304
+ if (props.experimentalPublic && format === "service-worker") {
305
+ throw new Error(
306
+ "You cannot publish in the service-worker format with a public directory."
307
+ );
308
+ }
309
+
310
+ if (config.wasm_modules && format === "modules") {
311
+ throw new Error(
312
+ "You cannot configure [wasm_modules] with an ES module worker. Instead, import the .wasm module directly in your code"
313
+ );
314
+ }
133
315
 
316
+ if (config.text_blobs && format === "modules") {
317
+ throw new Error(
318
+ "You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[rules]` in your wrangler.toml"
319
+ );
320
+ }
321
+
322
+ if (config.data_blobs && format === "modules") {
323
+ throw new Error(
324
+ "You cannot configure [data_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure `[rules]` in your wrangler.toml"
325
+ );
326
+ }
327
+ try {
134
328
  const { modules, resolvedEntryPointPath, bundleType } = await bundleWorker(
135
329
  props.entry,
136
- destination.path,
330
+ typeof destination === "string" ? destination : destination.path,
137
331
  {
138
332
  serveAssetsFromWorker: props.experimentalPublic,
139
333
  jsxFactory,
140
334
  jsxFragment,
141
- format,
142
335
  rules: props.rules,
336
+ tsconfig: props.tsconfig ?? config.tsconfig,
337
+ minify,
338
+ nodeCompat,
143
339
  }
144
340
  );
145
341
 
@@ -147,44 +343,15 @@ export default async function publish(props: Props): Promise<void> {
147
343
  encoding: "utf-8",
148
344
  });
149
345
 
150
- // if config.migrations
151
- // get current migration tag
152
- let migrations;
153
- if (config.migrations.length > 0) {
154
- const scripts = await fetchResult<
155
- { id: string; migration_tag: string }[]
156
- >(`/accounts/${accountId}/workers/scripts`);
157
- const script = scripts.find(({ id }) => id === scriptName);
158
- if (script?.migration_tag) {
159
- // was already published once
160
- const foundIndex = config.migrations.findIndex(
161
- (migration) => migration.tag === script.migration_tag
162
- );
163
- if (foundIndex === -1) {
164
- console.warn(
165
- `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...`
166
- );
167
- migrations = {
168
- old_tag: script.migration_tag,
169
- new_tag: config.migrations[config.migrations.length - 1].tag,
170
- steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
171
- };
172
- } else {
173
- migrations = {
174
- old_tag: script.migration_tag,
175
- new_tag: config.migrations[config.migrations.length - 1].tag,
176
- steps: config.migrations
177
- .slice(foundIndex + 1)
178
- .map(({ tag: _tag, ...rest }) => rest),
179
- };
180
- }
181
- } else {
182
- migrations = {
183
- new_tag: config.migrations[config.migrations.length - 1].tag,
184
- steps: config.migrations.map(({ tag: _tag, ...rest }) => rest),
185
- };
186
- }
187
- }
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;
188
355
 
189
356
  const assets = await syncAssets(
190
357
  accountId,
@@ -194,16 +361,17 @@ export default async function publish(props: Props): Promise<void> {
194
361
  // concept of service environments for kv namespaces yet).
195
362
  scriptName + (!props.legacyEnv && props.env ? `-${props.env}` : ""),
196
363
  props.assetPaths,
197
- false
364
+ false,
365
+ props.dryRun
198
366
  );
199
367
 
200
368
  const bindings: CfWorkerInit["bindings"] = {
201
- kv_namespaces: (envRootObj.kv_namespaces || []).concat(
369
+ kv_namespaces: (config.kv_namespaces || []).concat(
202
370
  assets.namespace
203
371
  ? { binding: "__STATIC_CONTENT", id: assets.namespace }
204
372
  : []
205
373
  ),
206
- vars: envRootObj.vars,
374
+ vars: config.vars,
207
375
  wasm_modules: config.wasm_modules,
208
376
  text_blobs: {
209
377
  ...config.text_blobs,
@@ -212,9 +380,11 @@ export default async function publish(props: Props): Promise<void> {
212
380
  __STATIC_CONTENT_MANIFEST: "__STATIC_CONTENT_MANIFEST",
213
381
  }),
214
382
  },
215
- durable_objects: envRootObj.durable_objects,
216
- r2_buckets: envRootObj.r2_buckets,
217
- unsafe: envRootObj.unsafe?.bindings,
383
+ data_blobs: config.data_blobs,
384
+ durable_objects: config.durable_objects,
385
+ r2_buckets: config.r2_buckets,
386
+ services: config.services,
387
+ unsafe: config.unsafe?.bindings,
218
388
  };
219
389
 
220
390
  if (assets.manifest) {
@@ -235,67 +405,82 @@ export default async function publish(props: Props): Promise<void> {
235
405
  bindings,
236
406
  migrations,
237
407
  modules,
238
- compatibility_date: config.compatibility_date,
239
- compatibility_flags: config.compatibility_flags,
408
+ compatibility_date: props.compatibilityDate ?? config.compatibility_date,
409
+ compatibility_flags:
410
+ props.compatibilityFlags ?? config.compatibility_flags,
240
411
  usage_model: config.usage_model,
241
412
  };
242
413
 
243
- const start = Date.now();
244
- const notProd = !props.legacyEnv && props.env;
245
- const workerName = notProd ? `${scriptName} (${envName})` : scriptName;
246
- const workerUrl = notProd
247
- ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${envName}`
248
- : `/accounts/${accountId}/workers/scripts/${scriptName}`;
249
-
250
- // Upload the script so it has time to propagate.
251
- const { available_on_subdomain } = await fetchResult(
252
- workerUrl,
253
- {
254
- method: "PUT",
255
- body: toFormData(worker),
256
- },
257
- new URLSearchParams({ available_on_subdomain: "true" })
258
- );
259
-
260
- const uploadMs = Date.now() - start;
261
- console.log("Uploaded", workerName, formatTime(uploadMs));
262
- const deployments: Promise<string[]>[] = [];
263
-
264
- if (deployToWorkersDev) {
265
- // Deploy to a subdomain of `workers.dev`
266
- const userSubdomain = (
267
- await fetchResult<{ subdomain: string }>(
268
- `/accounts/${accountId}/workers/subdomain`
414
+ const withoutStaticAssets = {
415
+ ...bindings,
416
+ kv_namespaces: config.kv_namespaces,
417
+ text_blobs: config.text_blobs,
418
+ };
419
+ printBindings(withoutStaticAssets);
420
+
421
+ if (!props.dryRun) {
422
+ // Upload the script so it has time to propagate.
423
+ // We can also now tell whether available_on_subdomain is set
424
+ available_on_subdomain = (
425
+ await fetchResult<{ available_on_subdomain: boolean }>(
426
+ workerUrl,
427
+ {
428
+ method: "PUT",
429
+ body: createWorkerUploadForm(worker),
430
+ },
431
+ new URLSearchParams({ include_subdomain_availability: "true" })
269
432
  )
270
- ).subdomain;
271
- const scriptURL =
272
- props.legacyEnv || !props.env
273
- ? `${scriptName}.${userSubdomain}.workers.dev`
274
- : `${envName}.${scriptName}.${userSubdomain}.workers.dev`;
275
- if (!available_on_subdomain) {
276
- // Enable the `workers.dev` subdomain.
277
- deployments.push(
278
- fetchResult(`${workerUrl}/subdomain`, {
279
- method: "POST",
280
- body: JSON.stringify({ enabled: true }),
281
- headers: {
282
- "Content-Type": "application/json",
283
- },
433
+ ).available_on_subdomain;
434
+ }
435
+ } finally {
436
+ if (typeof destination !== "string") {
437
+ // this means we're using a temp dir,
438
+ // so let's clean up before we proceed
439
+ await destination.cleanup();
440
+ }
441
+ }
442
+
443
+ if (props.dryRun) {
444
+ logger.log(`--dry-run: exiting now.`);
445
+ return;
446
+ }
447
+ assert(accountId, "Missing accountId");
448
+
449
+ const uploadMs = Date.now() - start;
450
+ const deployments: Promise<string[]>[] = [];
451
+
452
+ if (deployToWorkersDev) {
453
+ // Deploy to a subdomain of `workers.dev`
454
+ const userSubdomain = await getSubdomain(accountId);
455
+ const scriptURL =
456
+ props.legacyEnv || !props.env
457
+ ? `${scriptName}.${userSubdomain}.workers.dev`
458
+ : `${envName}.${scriptName}.${userSubdomain}.workers.dev`;
459
+ if (!available_on_subdomain) {
460
+ // Enable the `workers.dev` subdomain.
461
+ deployments.push(
462
+ fetchResult(`${workerUrl}/subdomain`, {
463
+ method: "POST",
464
+ body: JSON.stringify({ enabled: true }),
465
+ headers: {
466
+ "Content-Type": "application/json",
467
+ },
468
+ })
469
+ .then(() => [scriptURL])
470
+ // Add a delay when the subdomain is first created.
471
+ // This is to prevent an issue where a negative cache-hit
472
+ // causes the subdomain to be unavailable for 30 seconds.
473
+ // This is a temporary measure until we fix this on the edge.
474
+ .then(async (url) => {
475
+ await sleep(3000);
476
+ return url;
284
477
  })
285
- .then(() => [scriptURL])
286
- // Add a delay when the subdomain is first created.
287
- // This is to prevent an issue where a negative cache-hit
288
- // causes the subdomain to be unavailable for 30 seconds.
289
- // This is a temporary measure until we fix this on the edge.
290
- .then(async (url) => {
291
- await sleep(3000);
292
- return url;
293
- })
294
- );
295
- } else {
296
- deployments.push(Promise.resolve([scriptURL]));
297
- }
478
+ );
298
479
  } else {
480
+ deployments.push(Promise.resolve([scriptURL]));
481
+ }
482
+ } else {
483
+ if (available_on_subdomain) {
299
484
  // Disable the workers.dev deployment
300
485
  await fetchResult(`${workerUrl}/subdomain`, {
301
486
  method: "POST",
@@ -305,64 +490,94 @@ export default async function publish(props: Props): Promise<void> {
305
490
  },
306
491
  });
307
492
  }
493
+ }
308
494
 
309
- // Update routing table for the script.
310
- if (routes.length > 0) {
311
- deployments.push(
312
- fetchResult(`${workerUrl}/routes`, {
313
- // TODO: PATCH will not delete previous routes on this script,
314
- // whereas PUT will. We need to decide on the default behaviour
315
- // and how to configure it.
316
- method: "PUT",
317
- body: JSON.stringify(routes.map((pattern) => ({ pattern }))),
318
- headers: {
319
- "Content-Type": "application/json",
320
- },
321
- }).then(() => {
322
- if (routes.length > 10) {
323
- return routes
324
- .slice(0, 9)
325
- .map(String)
326
- .concat([`...and ${routes.length - 10} more routes`]);
327
- }
328
- return routes.map(String);
329
- })
330
- );
331
- }
495
+ logger.log("Uploaded", workerName, formatTime(uploadMs));
332
496
 
333
- // Configure any schedules for the script.
334
- // TODO: rename this to `schedules`?
335
- if (triggers && triggers.length) {
336
- deployments.push(
337
- fetchResult(`${workerUrl}/schedules`, {
338
- // TODO: Unlike routes, this endpoint does not support PATCH.
339
- // So technically, this will override any previous schedules.
340
- // We should change the endpoint to support PATCH.
341
- method: "PUT",
342
- body: JSON.stringify(triggers.map((cron) => ({ cron }))),
343
- headers: {
344
- "Content-Type": "application/json",
345
- },
346
- }).then(() => triggers.map(String))
347
- );
348
- }
497
+ // Update routing table for the script.
498
+ if (routesOnly.length > 0) {
499
+ deployments.push(
500
+ fetchResult(`${workerUrl}/routes`, {
501
+ // Note: PUT will delete previous routes on this script.
502
+ method: "PUT",
503
+ body: JSON.stringify(
504
+ routesOnly.map((route) =>
505
+ typeof route !== "object" ? { pattern: route } : route
506
+ )
507
+ ),
508
+ headers: {
509
+ "Content-Type": "application/json",
510
+ },
511
+ }).then(() => {
512
+ if (routesOnly.length > 10) {
513
+ return routesOnly
514
+ .slice(0, 9)
515
+ .map((route) => renderRoute(route))
516
+ .concat([`...and ${routesOnly.length - 10} more routes`]);
517
+ }
518
+ return routesOnly.map((route) => renderRoute(route));
519
+ })
520
+ );
521
+ }
349
522
 
350
- const targets = await Promise.all(deployments);
351
- const deployMs = Date.now() - start - uploadMs;
523
+ // Update custom domains for the script
524
+ if (customDomainsOnly.length > 0) {
525
+ deployments.push(publishCustomDomains(workerUrl, customDomainsOnly));
526
+ }
352
527
 
353
- if (deployments.length > 0) {
354
- console.log("Published", workerName, formatTime(deployMs));
355
- for (const target of targets.flat()) {
356
- console.log(" ", target);
357
- }
358
- } else {
359
- console.log("No publish targets for", workerName, formatTime(deployMs));
528
+ // Configure any schedules for the script.
529
+ // TODO: rename this to `schedules`?
530
+ if (triggers && triggers.length) {
531
+ deployments.push(
532
+ fetchResult(`${workerUrl}/schedules`, {
533
+ // Note: PUT will override previous schedules on this script.
534
+ method: "PUT",
535
+ body: JSON.stringify(triggers.map((cron) => ({ cron }))),
536
+ headers: {
537
+ "Content-Type": "application/json",
538
+ },
539
+ }).then(() => triggers.map((trigger) => `schedule: ${trigger}`))
540
+ );
541
+ }
542
+
543
+ const targets = await Promise.all(deployments);
544
+ const deployMs = Date.now() - start - uploadMs;
545
+
546
+ if (deployments.length > 0) {
547
+ logger.log("Published", workerName, formatTime(deployMs));
548
+ for (const target of targets.flat()) {
549
+ logger.log(" ", target);
360
550
  }
361
- } finally {
362
- await destination.cleanup();
551
+ } else {
552
+ logger.log("No publish targets for", workerName, formatTime(deployMs));
363
553
  }
364
554
  }
365
555
 
366
556
  function formatTime(duration: number) {
367
557
  return `(${(duration / 1000).toFixed(2)} sec)`;
368
558
  }
559
+
560
+ async function getSubdomain(accountId: string): Promise<string> {
561
+ try {
562
+ const { subdomain } = await fetchResult(
563
+ `/accounts/${accountId}/workers/subdomain`
564
+ );
565
+ return subdomain;
566
+ } catch (e) {
567
+ const error = e as { code?: number };
568
+ if (typeof error === "object" && !!error && error.code === 10007) {
569
+ // 10007 error code: not found
570
+ // https://api.cloudflare.com/#worker-subdomain-get-subdomain
571
+
572
+ const errorMessage =
573
+ "Error: You need to register a workers.dev subdomain before publishing to workers.dev";
574
+ const solutionMessage =
575
+ "You can either publish your worker to one or more routes by specifying them in wrangler.toml, or register a workers.dev subdomain here:";
576
+ const onboardingLink = `https://dash.cloudflare.com/${accountId}/workers/onboarding`;
577
+
578
+ throw new Error(`${errorMessage}\n${solutionMessage}\n${onboardingLink}`);
579
+ } else {
580
+ throw e;
581
+ }
582
+ }
583
+ }