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/index.tsx CHANGED
@@ -3,33 +3,51 @@ import { writeFile, mkdir } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { setTimeout } from "node:timers/promises";
5
5
  import TOML from "@iarna/toml";
6
+ import chalk from "chalk";
7
+ import { execa } from "execa";
6
8
  import { findUp } from "find-up";
7
9
  import getPort from "get-port";
8
10
  import { render } from "ink";
9
11
  import React from "react";
10
12
  import onExit from "signal-exit";
13
+ import supportsColor from "supports-color";
14
+ import { setGlobalDispatcher, ProxyAgent } from "undici";
11
15
  import makeCLI from "yargs";
12
16
  import { version as wranglerVersion } from "../package.json";
13
- import { toFormData } from "./api/form_data";
14
17
  import { fetchResult } from "./cfetch";
15
18
  import { findWranglerToml, readConfig } from "./config";
16
- import Dev from "./dev";
19
+ import { createWorkerUploadForm } from "./create-worker-upload-form";
20
+ import Dev from "./dev/dev";
21
+ import { getVarsForDev } from "./dev/dev-vars";
17
22
  import { confirm, prompt } from "./dialogs";
18
23
  import { getEntry } from "./entry";
24
+ import { DeprecationError } from "./errors";
19
25
  import {
20
- getNamespaceId,
21
- listNamespaces,
22
- listNamespaceKeys,
23
- putKeyValue,
24
- putBulkKeyValue,
25
- deleteBulkKeyValue,
26
- createNamespace,
27
- isValidNamespaceBinding,
28
- getKeyValue,
26
+ getKVNamespaceId,
27
+ listKVNamespaces,
28
+ listKVNamespaceKeys,
29
+ putKVKeyValue,
30
+ putKVBulkKeyValue,
31
+ deleteKVBulkKeyValue,
32
+ createKVNamespace,
33
+ isValidKVNamespaceBinding,
34
+ getKVKeyValue,
35
+ isKVKeyValue,
36
+ unexpectedKVKeyValueProps,
37
+ deleteKVNamespace,
38
+ deleteKVKeyValue,
29
39
  } from "./kv";
40
+ import { logger } from "./logger";
30
41
  import { getPackageManager } from "./package-manager";
31
- import { pages } from "./pages";
32
- import { formatMessage, ParseError, parseJSON, readFileSync } from "./parse";
42
+ import { pages, pagesBetaWarning } from "./pages";
43
+ import {
44
+ formatMessage,
45
+ ParseError,
46
+ parseJSON,
47
+ parsePackageJSON,
48
+ parseTOML,
49
+ readFileSync,
50
+ } from "./parse";
33
51
  import publish from "./publish";
34
52
  import { createR2Bucket, deleteR2Bucket, listR2Buckets } from "./r2";
35
53
  import { getAssetPaths } from "./sites";
@@ -39,26 +57,38 @@ import {
39
57
  prettyPrintLogs,
40
58
  translateCLICommandToFilterMessage,
41
59
  } from "./tail";
60
+ import { updateCheck } from "./update-check";
42
61
  import {
43
62
  login,
44
63
  logout,
45
64
  listScopes,
46
- initialise as initialiseUserConfig,
47
- loginOrRefreshIfRequired,
48
- getAccountId,
49
65
  validateScopeKeys,
66
+ requireAuth,
50
67
  } from "./user";
51
68
  import { whoami } from "./whoami";
52
69
 
53
70
  import type { Config } from "./config";
54
71
  import type { TailCLIFilters } from "./tail";
55
72
  import type { RawData } from "ws";
73
+ import type { CommandModule } from "yargs";
56
74
  import type Yargs from "yargs";
57
75
 
58
76
  type ConfigPath = string | undefined;
59
77
 
60
78
  const resetColor = "\x1b[0m";
61
79
  const fgGreenColor = "\x1b[32m";
80
+ const DEFAULT_LOCAL_PORT = 8787;
81
+
82
+ const proxy =
83
+ process.env.https_proxy ||
84
+ process.env.HTTPS_PROXY ||
85
+ process.env.http_proxy ||
86
+ process.env.HTTP_PROXY ||
87
+ undefined;
88
+
89
+ if (proxy) {
90
+ setGlobalDispatcher(new ProxyAgent(proxy));
91
+ }
62
92
 
63
93
  function getRules(config: Config): Config["rules"] {
64
94
  const rules = config.rules ?? config.build?.upload?.rules ?? [];
@@ -70,8 +100,8 @@ function getRules(config: Config): Config["rules"] {
70
100
  }
71
101
 
72
102
  if (config.build?.upload?.rules) {
73
- console.warn(
74
- `Deprecation notice: The \`build.upload.rules\` config field is no longer used, the rules should be specified via the \`rules\` config field. Delete the \`build.upload\` field from the configuration file, and add this:
103
+ logger.warn(
104
+ `Deprecation: The \`build.upload.rules\` config field is no longer used, the rules should be specified via the \`rules\` config field. Delete the \`build.upload\` field from the configuration file, and add this:
75
105
 
76
106
  ${TOML.stringify({ rules: config.build.upload.rules })}`
77
107
  );
@@ -79,25 +109,89 @@ ${TOML.stringify({ rules: config.build.upload.rules })}`
79
109
  return rules;
80
110
  }
81
111
 
82
- function isLegacyEnv(args: unknown, config: Config): boolean {
83
- return (
84
- (args as { "legacy-env": boolean | undefined })["legacy-env"] ??
85
- config.legacy_env
112
+ async function printWranglerBanner() {
113
+ // Let's not print this in tests
114
+ if (typeof jest !== "undefined") {
115
+ return;
116
+ }
117
+
118
+ const text = ` ⛅️ wrangler ${wranglerVersion} ${await updateCheck()}`;
119
+
120
+ logger.log(
121
+ text +
122
+ "\n" +
123
+ (supportsColor.stdout
124
+ ? chalk.hex("#FF8800")("-".repeat(text.length))
125
+ : "-".repeat(text.length))
86
126
  );
87
127
  }
88
128
 
129
+ function isLegacyEnv(config: Config): boolean {
130
+ // We only read from config here, because we've already accounted for
131
+ // args["legacy-env"] in https://github.com/cloudflare/wrangler2/blob/b24aeb5722370c2e04bce97a84a1fa1e55725d79/packages/wrangler/src/config/validation.ts#L94-L98
132
+ return config.legacy_env;
133
+ }
134
+
89
135
  function getScriptName(
90
136
  args: { name: string | undefined; env: string | undefined },
91
137
  config: Config
92
138
  ): string | undefined {
93
- const shortScriptName = args.name ?? config.name;
94
- if (!shortScriptName) {
95
- return;
139
+ if (args.name && isLegacyEnv(config) && args.env) {
140
+ throw new CommandLineArgsError(
141
+ "In legacy environment mode you cannot use --name and --env together. If you want to specify a Worker name for a specific environment you can add the following to your wrangler.toml config:" +
142
+ `
143
+ [env.${args.env}]
144
+ name = "${args.name}"
145
+ `
146
+ );
96
147
  }
97
148
 
98
- return isLegacyEnv(args, config)
99
- ? `${shortScriptName}${args.env ? `-${args.env}` : ""}`
100
- : shortScriptName;
149
+ return args.name ?? config.name;
150
+ }
151
+
152
+ /**
153
+ * Alternative to the getScriptName() because special Legacy cases allowed "name", and "env" together in Wrangler1
154
+ */
155
+ function getLegacyScriptName(
156
+ args: { name: string | undefined; env: string | undefined },
157
+ config: Config
158
+ ) {
159
+ return args.name && args.env && isLegacyEnv(config)
160
+ ? `${args.name}-${args.env}`
161
+ : args.name ?? config.name;
162
+ }
163
+
164
+ /**
165
+ * Get a promise to the streamed input from stdin.
166
+ *
167
+ * This function can be used to grab the incoming stream of data from, say,
168
+ * piping the output of another process into the wrangler process.
169
+ */
170
+ function readFromStdin(): Promise<string> {
171
+ return new Promise((resolve, reject) => {
172
+ const stdin = process.stdin;
173
+ const chunks: string[] = [];
174
+
175
+ // When there is data ready to be read, the `readable` event will be triggered.
176
+ // In the handler for `readable` we call `read()` over and over until all the available data has been read.
177
+ stdin.on("readable", () => {
178
+ let chunk;
179
+ while (null !== (chunk = stdin.read())) {
180
+ chunks.push(chunk);
181
+ }
182
+ });
183
+
184
+ // When the streamed data is complete the `end` event will be triggered.
185
+ // In the handler for `end` we join the chunks together and resolve the promise.
186
+ stdin.on("end", () => {
187
+ resolve(chunks.join(""));
188
+ });
189
+
190
+ // If there is an `error` event then the handler will reject the promise.
191
+ stdin.on("error", (err) => {
192
+ reject(err);
193
+ });
194
+ });
101
195
  }
102
196
 
103
197
  // a helper to demand one of a set of options
@@ -125,22 +219,27 @@ function demandOneOfOption(...options: string[]) {
125
219
  };
126
220
  }
127
221
 
128
- class CommandLineArgsError extends Error {}
129
- class DeprecationError extends Error {
130
- constructor(message: string) {
131
- super(`DEPRECATION WARNING:\n${message}`);
132
- }
222
+ /**
223
+ * Remove trailing white space from inputs.
224
+ * Matching Wrangler legacy behavior with handling inputs
225
+ */
226
+ function trimTrailingWhitespace(str: string) {
227
+ return str.trimEnd();
133
228
  }
134
229
 
135
- export async function main(argv: string[]): Promise<void> {
230
+ class CommandLineArgsError extends Error {}
231
+
232
+ function createCLIParser(argv: string[]) {
136
233
  const wrangler = makeCLI(argv)
234
+ .strict()
137
235
  // We handle errors ourselves in a try-catch around `yargs.parse`.
138
236
  // If you want the "help info" to be displayed then throw an instance of `CommandLineArgsError`.
139
237
  // Otherwise we just log the error that was thrown without any "help info".
140
238
  .showHelpOnFail(false)
141
239
  .fail((msg, error) => {
142
- if (!error) {
143
- // If there is only a `msg` then this came from yargs own validation, so wrap in a `CommandLineArgsError`.
240
+ if (!error || error.name === "YError") {
241
+ // If there is no error or the error is a "YError", then this came from yargs own validation
242
+ // Wrap it in a `CommandLineArgsError` so that we can handle it appropriately further up.
144
243
  error = new CommandLineArgsError(msg);
145
244
  }
146
245
  throw error;
@@ -148,7 +247,15 @@ export async function main(argv: string[]): Promise<void> {
148
247
  .scriptName("wrangler")
149
248
  .wrap(null);
150
249
 
151
- // the default is to simply print the help menu
250
+ // Default help command that supports the subcommands
251
+ const subHelp: CommandModule = {
252
+ command: ["*"],
253
+ handler: async (args) => {
254
+ setImmediate(() =>
255
+ wrangler.parse([...args._.map((a) => `${a}`), "--help"])
256
+ );
257
+ },
258
+ };
152
259
  wrangler.command(
153
260
  ["*"],
154
261
  false,
@@ -182,14 +289,19 @@ export async function main(argv: string[]): Promise<void> {
182
289
  default: "worker",
183
290
  })
184
291
  .positional("template", {
185
- describe: "a link to a GitHub template",
292
+ describe: "The URL of a GitHub template",
186
293
  default: "https://github.com/cloudflare/worker-template",
187
294
  });
188
295
  },
189
- () => {
296
+ (generateArgs) => {
190
297
  // "👯 [DEPRECATED]. Scaffold a Cloudflare Workers project from a public GitHub repository.",
191
298
  throw new DeprecationError(
192
- "`wrangler generate` has been deprecated, please refer to TODO://some/path for alternatives"
299
+ "`wrangler generate` has been deprecated.\n" +
300
+ "Try running `wrangler init` to generate a basic Worker, or cloning the template repository instead:\n\n" +
301
+ "```\n" +
302
+ `git clone ${generateArgs.template}\n` +
303
+ "```\n\n" +
304
+ "Please refer to https://developers.cloudflare.com/workers/wrangler/deprecations/#generate for more information."
193
305
  );
194
306
  }
195
307
  );
@@ -204,6 +316,18 @@ export async function main(argv: string[]): Promise<void> {
204
316
  describe: "The name of your worker",
205
317
  type: "string",
206
318
  })
319
+ .option("type", {
320
+ describe: "The type of worker to create",
321
+ type: "string",
322
+ choices: ["rust", "javascript", "webpack"],
323
+ hidden: true,
324
+ deprecated: true,
325
+ })
326
+ .option("site", {
327
+ hidden: true,
328
+ type: "boolean",
329
+ deprecated: true,
330
+ })
207
331
  .option("yes", {
208
332
  describe: 'Answer "yes" to any prompts for new projects',
209
333
  type: "boolean",
@@ -211,7 +335,8 @@ export async function main(argv: string[]): Promise<void> {
211
335
  });
212
336
  },
213
337
  async (args) => {
214
- if ("type" in args) {
338
+ await printWranglerBanner();
339
+ if (args.type) {
215
340
  let message = "The --type option is no longer supported.";
216
341
  if (args.type === "webpack") {
217
342
  message +=
@@ -221,9 +346,33 @@ export async function main(argv: string[]): Promise<void> {
221
346
  throw new CommandLineArgsError(message);
222
347
  }
223
348
 
224
- // TODO: make sure args.name is a valid identifier for a worker name
349
+ const devDepsToInstall: string[] = [];
350
+ const instructions: string[] = [];
351
+ let shouldRunPackageManagerInstall = false;
352
+ const creationDirectory = path.resolve(process.cwd(), args.name ?? "");
353
+
354
+ if (args.site) {
355
+ const gitDirectory =
356
+ creationDirectory !== process.cwd()
357
+ ? path.basename(creationDirectory)
358
+ : "my-site";
359
+ const message =
360
+ "The --site option is no longer supported.\n" +
361
+ "If you wish to create a brand new Worker Sites project then clone the `worker-sites-template` starter repository:\n\n" +
362
+ "```\n" +
363
+ `git clone --depth=1 --branch=wrangler2 https://github.com/cloudflare/worker-sites-template ${gitDirectory}\n` +
364
+ `cd ${gitDirectory}\n` +
365
+ "```\n\n" +
366
+ "Find out more about how to create and maintain Sites projects at https://developers.cloudflare.com/workers/platform/sites.\n" +
367
+ "Have you considered using Cloudflare Pages instead? See https://pages.cloudflare.com/.";
368
+ throw new CommandLineArgsError(message);
369
+ }
225
370
 
226
- const creationDirectory = path.join(process.cwd(), args.name ?? "");
371
+ // TODO: make sure args.name is a valid identifier for a worker name
372
+ const workerName = path
373
+ .basename(creationDirectory)
374
+ .toLowerCase()
375
+ .replaceAll(/[^a-z0-9\-_]/gm, "-");
227
376
 
228
377
  const packageManager = await getPackageManager(creationDirectory);
229
378
 
@@ -235,10 +384,14 @@ export async function main(argv: string[]): Promise<void> {
235
384
  "./wrangler.toml"
236
385
  );
237
386
  let justCreatedWranglerToml = false;
238
- const workerName =
239
- args.name || path.basename(path.resolve(process.cwd()));
387
+
240
388
  if (fs.existsSync(wranglerTomlDestination)) {
241
- console.warn(`${wranglerTomlDestination} file already exists!`);
389
+ logger.warn(
390
+ `${path.relative(
391
+ process.cwd(),
392
+ wranglerTomlDestination
393
+ )} already exists!`
394
+ );
242
395
  const shouldContinue = await confirm(
243
396
  "Do you want to continue initializing this project?"
244
397
  );
@@ -248,6 +401,7 @@ export async function main(argv: string[]): Promise<void> {
248
401
  } else {
249
402
  await mkdir(creationDirectory, { recursive: true });
250
403
  const compatibilityDate = new Date().toISOString().substring(0, 10);
404
+
251
405
  try {
252
406
  await writeFile(
253
407
  wranglerTomlDestination,
@@ -256,18 +410,63 @@ export async function main(argv: string[]): Promise<void> {
256
410
  compatibility_date: compatibilityDate,
257
411
  }) + "\n"
258
412
  );
259
- console.log(`✨ Successfully created wrangler.toml`);
413
+
414
+ logger.log(
415
+ `✨ Created ${path.relative(
416
+ process.cwd(),
417
+ wranglerTomlDestination
418
+ )}`
419
+ );
260
420
  justCreatedWranglerToml = true;
261
421
  } catch (err) {
262
422
  throw new Error(
263
- `Failed to create wrangler.toml.\n${(err as Error).message ?? err}`
423
+ `Failed to create ${path.relative(
424
+ process.cwd(),
425
+ wranglerTomlDestination
426
+ )}.\n${(err as Error).message ?? err}`
264
427
  );
265
428
  }
266
429
  }
267
430
 
268
- let pathToPackageJson = await findUp("package.json");
269
- let shouldCreatePackageJson = false;
270
431
  const yesFlag = args.yes ?? false;
432
+
433
+ const isInsideGitProject = Boolean(
434
+ await findUp(".git", { cwd: creationDirectory, type: "directory" })
435
+ );
436
+ let isGitInstalled;
437
+ try {
438
+ isGitInstalled = (await execa("git", ["--version"])).exitCode === 0;
439
+ } catch (err) {
440
+ isGitInstalled = false;
441
+ }
442
+ if (!isInsideGitProject && isGitInstalled) {
443
+ const shouldInitGit =
444
+ yesFlag ||
445
+ (await confirm("Would you like to use git to manage this Worker?"));
446
+ if (shouldInitGit) {
447
+ await execa("git", ["init", "--initial-branch=main"], {
448
+ cwd: creationDirectory,
449
+ });
450
+ await writeFile(
451
+ path.join(creationDirectory, ".gitignore"),
452
+ readFileSync(path.join(__dirname, "../templates/gitignore"))
453
+ );
454
+ logger.log(
455
+ args.name && args.name !== "."
456
+ ? `✨ Initialized git repository at ${path.relative(
457
+ process.cwd(),
458
+ creationDirectory
459
+ )}`
460
+ : `✨ Initialized git repository`
461
+ );
462
+ }
463
+ }
464
+
465
+ let pathToPackageJson = await findUp("package.json", {
466
+ cwd: creationDirectory,
467
+ });
468
+ let shouldCreatePackageJson = false;
469
+
271
470
  if (!pathToPackageJson) {
272
471
  // If no package.json exists, ask to create one
273
472
  shouldCreatePackageJson =
@@ -284,7 +483,7 @@ export async function main(argv: string[]): Promise<void> {
284
483
  name: workerName,
285
484
  version: "0.0.0",
286
485
  devDependencies: {
287
- wrangler: wranglerVersion,
486
+ wrangler: `^${wranglerVersion}`,
288
487
  },
289
488
  private: true,
290
489
  },
@@ -292,16 +491,19 @@ export async function main(argv: string[]): Promise<void> {
292
491
  " "
293
492
  ) + "\n"
294
493
  );
295
- await packageManager.install();
296
- console.log(`✨ Created package.json`);
494
+
495
+ shouldRunPackageManagerInstall = true;
297
496
  pathToPackageJson = path.join(creationDirectory, "package.json");
497
+ logger.log(
498
+ `✨ Created ${path.relative(process.cwd(), pathToPackageJson)}`
499
+ );
298
500
  } else {
299
501
  return;
300
502
  }
301
503
  } else {
302
504
  // If package.json exists and wrangler isn't installed,
303
505
  // then ask to add wrangler to devDependencies
304
- const packageJson = parseJSON(
506
+ const packageJson = parsePackageJSON(
305
507
  readFileSync(pathToPackageJson),
306
508
  pathToPackageJson
307
509
  );
@@ -314,17 +516,21 @@ export async function main(argv: string[]): Promise<void> {
314
516
  const shouldInstall =
315
517
  yesFlag ||
316
518
  (await confirm(
317
- "Would you like to install wrangler into your package.json?"
519
+ `Would you like to install wrangler into ${path.relative(
520
+ process.cwd(),
521
+ pathToPackageJson
522
+ )}?`
318
523
  ));
319
524
  if (shouldInstall) {
320
- await packageManager.addDevDeps(`wrangler@${wranglerVersion}`);
321
- console.log(`✨ Installed wrangler`);
525
+ devDepsToInstall.push(`wrangler@${wranglerVersion}`);
322
526
  }
323
527
  }
324
528
  }
325
529
 
326
530
  let isTypescriptProject = false;
327
- let pathToTSConfig = await findUp("tsconfig.json");
531
+ let pathToTSConfig = await findUp("tsconfig.json", {
532
+ cwd: creationDirectory,
533
+ });
328
534
  if (!pathToTSConfig) {
329
535
  // If there's no tsconfig, offer to create one
330
536
  // and install @cloudflare/workers-types
@@ -334,21 +540,18 @@ export async function main(argv: string[]): Promise<void> {
334
540
  path.join(creationDirectory, "./tsconfig.json"),
335
541
  readFileSync(path.join(__dirname, "../templates/tsconfig.json"))
336
542
  );
337
- await packageManager.addDevDeps(
338
- "@cloudflare/workers-types",
339
- "typescript"
340
- );
341
-
342
- console.log(
343
- `✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies`
344
- );
543
+ devDepsToInstall.push("@cloudflare/workers-types");
544
+ devDepsToInstall.push("typescript");
345
545
  pathToTSConfig = path.join(creationDirectory, "tsconfig.json");
546
+ logger.log(
547
+ `✨ Created ${path.relative(process.cwd(), pathToTSConfig)}`
548
+ );
346
549
  }
347
550
  } else {
348
551
  isTypescriptProject = true;
349
552
  // If there's a tsconfig, check if @cloudflare/workers-types
350
553
  // is already installed, and offer to install it if not
351
- const packageJson = parseJSON(
554
+ const packageJson = parsePackageJSON(
352
555
  readFileSync(pathToPackageJson),
353
556
  pathToPackageJson
354
557
  );
@@ -362,19 +565,22 @@ export async function main(argv: string[]): Promise<void> {
362
565
  "Would you like to install the type definitions for Workers into your package.json?"
363
566
  );
364
567
  if (shouldInstall) {
365
- await packageManager.addDevDeps("@cloudflare/workers-types");
568
+ devDepsToInstall.push("@cloudflare/workers-types");
366
569
  // We don't update the tsconfig.json because
367
570
  // it could be complicated in existing projects
368
571
  // and we don't want to break them. Instead, we simply
369
572
  // tell the user that they need to update their tsconfig.json
370
- console.log(
371
- `✨ Installed @cloudflare/workers-types.\nPlease add "@cloudflare/workers-types" to compilerOptions.types in your tsconfig.json`
573
+ instructions.push(
574
+ `🚨 Please add "@cloudflare/workers-types" to compilerOptions.types in ${path.relative(
575
+ process.cwd(),
576
+ pathToTSConfig
577
+ )}`
372
578
  );
373
579
  }
374
580
  }
375
581
  }
376
582
 
377
- const packageJsonContent = parseJSON(
583
+ const packageJsonContent = parsePackageJSON(
378
584
  readFileSync(pathToPackageJson),
379
585
  pathToPackageJson
380
586
  );
@@ -383,6 +589,41 @@ export async function main(argv: string[]): Promise<void> {
383
589
  !packageJsonContent.scripts?.publish &&
384
590
  shouldCreatePackageJson;
385
591
 
592
+ /*
593
+ * Passes the array of accumulated devDeps to install through to
594
+ * the package manager. Also generates a human-readable list
595
+ * of packages it installed.
596
+ * If there are no devDeps to install, optionally runs
597
+ * the package manager's install command.
598
+ */
599
+ async function installPackages(
600
+ shouldRunInstall: boolean,
601
+ depsToInstall: string[]
602
+ ) {
603
+ //lets install the devDeps they asked for
604
+ //and run their package manager's install command if needed
605
+ if (depsToInstall.length > 0) {
606
+ const formatter = new Intl.ListFormat("en", {
607
+ style: "long",
608
+ type: "conjunction",
609
+ });
610
+ await packageManager.addDevDeps(...depsToInstall);
611
+ const versionlessPackages = depsToInstall.map((dep) =>
612
+ dep === `wrangler@${wranglerVersion}` ? "wrangler" : dep
613
+ );
614
+
615
+ logger.log(
616
+ `✨ Installed ${formatter.format(
617
+ versionlessPackages
618
+ )} into devDependencies`
619
+ );
620
+ } else {
621
+ if (shouldRunInstall) {
622
+ await packageManager.install();
623
+ }
624
+ }
625
+ }
626
+
386
627
  async function writePackageJsonScriptsAndUpdateWranglerToml(
387
628
  isWritingScripts: boolean,
388
629
  isCreatingWranglerToml: boolean,
@@ -391,7 +632,7 @@ export async function main(argv: string[]): Promise<void> {
391
632
  ) {
392
633
  if (isCreatingWranglerToml) {
393
634
  // rewrite wrangler.toml with main = "path/to/script"
394
- const parsedWranglerToml = TOML.parse(
635
+ const parsedWranglerToml = parseTOML(
395
636
  readFileSync(wranglerTomlDestination)
396
637
  );
397
638
  fs.writeFileSync(
@@ -417,7 +658,7 @@ export async function main(argv: string[]): Promise<void> {
417
658
  start: isCreatingWranglerToml
418
659
  ? `wrangler dev`
419
660
  : `wrangler dev ${scriptPath}`,
420
- publish: isCreatingWranglerToml
661
+ deploy: isCreatingWranglerToml
421
662
  ? `wrangler publish`
422
663
  : `wrangler publish ${scriptPath}`,
423
664
  },
@@ -426,21 +667,21 @@ export async function main(argv: string[]): Promise<void> {
426
667
  2
427
668
  ) + "\n"
428
669
  );
429
- console.log(
670
+ instructions.push(
430
671
  `\nTo start developing your Worker, run \`${
431
672
  isNamedWorker ? `cd ${args.name} && ` : ""
432
673
  }npm start\``
433
674
  );
434
- console.log(
435
- `To publish your Worker to the Internet, run \`npm run publish\``
675
+ instructions.push(
676
+ `To publish your Worker to the Internet, run \`npm run deploy\``
436
677
  );
437
678
  } else {
438
- console.log(
679
+ instructions.push(
439
680
  `\nTo start developing your Worker, run \`npx wrangler dev\`${
440
681
  isCreatingWranglerToml ? "" : ` ${scriptPath}`
441
682
  }`
442
683
  );
443
- console.log(
684
+ instructions.push(
444
685
  `To publish your Worker to the Internet, run \`npx wrangler publish\`${
445
686
  isCreatingWranglerToml ? "" : ` ${scriptPath}`
446
687
  }`
@@ -450,12 +691,13 @@ export async function main(argv: string[]): Promise<void> {
450
691
 
451
692
  if (isTypescriptProject) {
452
693
  if (!fs.existsSync(path.join(creationDirectory, "./src/index.ts"))) {
453
- let shouldCreateSource = false;
454
-
455
- shouldCreateSource =
694
+ const shouldCreateSource =
456
695
  yesFlag ||
457
696
  (await confirm(
458
- `Would you like to create a Worker at src/index.ts?`
697
+ `Would you like to create a Worker at ${path.relative(
698
+ process.cwd(),
699
+ path.join(creationDirectory, "./src/index.ts")
700
+ )}?`
459
701
  ));
460
702
 
461
703
  if (shouldCreateSource) {
@@ -467,7 +709,12 @@ export async function main(argv: string[]): Promise<void> {
467
709
  readFileSync(path.join(__dirname, "../templates/new-worker.ts"))
468
710
  );
469
711
 
470
- console.log(`✨ Created src/index.ts`);
712
+ logger.log(
713
+ `✨ Created ${path.relative(
714
+ process.cwd(),
715
+ path.join(creationDirectory, "./src/index.ts")
716
+ )}`
717
+ );
471
718
 
472
719
  await writePackageJsonScriptsAndUpdateWranglerToml(
473
720
  shouldWritePackageJsonScripts,
@@ -478,20 +725,31 @@ export async function main(argv: string[]): Promise<void> {
478
725
  }
479
726
  }
480
727
  } else {
481
- if (!fs.existsSync("./src/index.js")) {
482
- const shouldCreateSource = await confirm(
483
- `Would you like to create a Worker at src/index.js?`
484
- );
728
+ if (!fs.existsSync(path.join(creationDirectory, "./src/index.js"))) {
729
+ const shouldCreateSource =
730
+ yesFlag ||
731
+ (await confirm(
732
+ `Would you like to create a Worker at ${path.relative(
733
+ process.cwd(),
734
+ path.join(creationDirectory, "./src/index.js")
735
+ )}?`
736
+ ));
737
+
485
738
  if (shouldCreateSource) {
486
739
  await mkdir(path.join(creationDirectory, "./src"), {
487
740
  recursive: true,
488
741
  });
489
742
  await writeFile(
490
- path.join(path.join(creationDirectory, "./src/index.js")),
743
+ path.join(creationDirectory, "./src/index.js"),
491
744
  readFileSync(path.join(__dirname, "../templates/new-worker.js"))
492
745
  );
493
746
 
494
- console.log(`✨ Created src/index.js`);
747
+ logger.log(
748
+ `✨ Created ${path.relative(
749
+ process.cwd(),
750
+ path.join(creationDirectory, "./src/index.js")
751
+ )}`
752
+ );
495
753
 
496
754
  await writePackageJsonScriptsAndUpdateWranglerToml(
497
755
  shouldWritePackageJsonScripts,
@@ -502,6 +760,21 @@ export async function main(argv: string[]): Promise<void> {
502
760
  }
503
761
  }
504
762
  }
763
+ // install packages as the final step of init
764
+ try {
765
+ await installPackages(shouldRunPackageManagerInstall, devDepsToInstall);
766
+ } catch (e) {
767
+ // fetching packages could fail due to loss of internet, etc
768
+ // we should let folks know we failed to fetch, but their
769
+ // workers project is still ready to go
770
+ logger.error(e instanceof Error ? e.message : e);
771
+ instructions.push(
772
+ "\n🚨 wrangler was unable to fetch your npm packages, but your project is ready to go"
773
+ );
774
+ }
775
+
776
+ // let users know what to do now
777
+ instructions.forEach((instruction) => logger.log(instruction));
505
778
  }
506
779
  );
507
780
 
@@ -512,83 +785,34 @@ export async function main(argv: string[]): Promise<void> {
512
785
  (yargs) => {
513
786
  return yargs.option("env", {
514
787
  describe: "Perform on a specific environment",
788
+ type: "string",
515
789
  });
516
790
  },
517
- () => {
791
+ async (buildArgs) => {
518
792
  // "[DEPRECATED] 🦀 Build your project (if applicable)",
519
- throw new DeprecationError(
520
- "`wrangler build` has been deprecated, please refer to TODO://some/path for alternatives"
521
- );
522
- }
523
- );
524
793
 
525
- // login
526
- wrangler.command(
527
- // this needs scopes as an option?
528
- "login",
529
- false, // we don't need to show this in the menu
530
- // "🔓 Login to Cloudflare",
531
- (yargs) => {
532
- // TODO: This needs some copy editing
533
- // I mean, this entire app does, but this too.
534
- return yargs
535
- .option("scopes-list", {
536
- describe: "List all the available OAuth scopes with descriptions",
794
+ const envFlag = buildArgs.env ? ` --env=${buildArgs.env}` : "";
795
+ logger.log(
796
+ formatMessage({
797
+ kind: "warning",
798
+ text: "Deprecation: `wrangler build` has been deprecated.",
799
+ notes: [
800
+ {
801
+ text: "Please refer to https://developers.cloudflare.com/workers/wrangler/migration/deprecations/#build for more information.",
802
+ },
803
+ {
804
+ text: `Attempting to run \`wrangler publish --dry-run --outdir=dist${envFlag}\` for you instead:`,
805
+ },
806
+ ],
537
807
  })
538
- .option("scopes", {
539
- describe: "Pick the set of applicable OAuth scopes when logging in",
540
- array: true,
541
- type: "string",
542
- });
543
-
544
- // TODO: scopes
545
- },
546
- async (args) => {
547
- if (args["scopes-list"]) {
548
- listScopes();
549
- return;
550
- }
551
- if (args.scopes) {
552
- if (args.scopes.length === 0) {
553
- // don't allow no scopes to be passed, that would be weird
554
- listScopes();
555
- return;
556
- }
557
- if (!validateScopeKeys(args.scopes)) {
558
- throw new CommandLineArgsError(
559
- `One of ${args.scopes} is not a valid authentication scope. Run "wrangler login --list-scopes" to see the valid scopes.`
560
- );
561
- }
562
- await login({ scopes: args.scopes });
563
- return;
564
- }
565
- await login();
566
-
567
- // TODO: would be nice if it optionally saved login
568
- // credentials inside node_modules/.cache or something
569
- // this way you could have multiple users on a single machine
570
- }
571
- );
572
-
573
- // logout
574
- wrangler.command(
575
- // this needs scopes as an option?
576
- "logout",
577
- false, // we don't need to show this in the menu
578
- // "🚪 Logout from Cloudflare",
579
- () => {},
580
- async () => {
581
- await logout();
582
- }
583
- );
808
+ );
584
809
 
585
- // whoami
586
- wrangler.command(
587
- "whoami",
588
- "🕵️ Retrieve your user info and test your auth config",
589
- () => {},
590
- async () => {
591
- await whoami();
810
+ await createCLIParser([
811
+ "publish",
812
+ "--dry-run",
813
+ "--outdir=dist",
814
+ ...(buildArgs.env ? ["--env", buildArgs.env] : []),
815
+ ]).parse();
592
816
  }
593
817
  );
594
818
 
@@ -600,7 +824,7 @@ export async function main(argv: string[]): Promise<void> {
600
824
  () => {
601
825
  // "🕵️ Authenticate Wrangler with a Cloudflare API Token",
602
826
  throw new DeprecationError(
603
- "`wrangler config` has been deprecated, please refer to TODO://some/path for alternatives"
827
+ "`wrangler config` has been deprecated, please refer to https://developers.cloudflare.com/workers/wrangler/migration/deprecations/#config for alternatives"
604
828
  );
605
829
  }
606
830
  );
@@ -618,23 +842,30 @@ export async function main(argv: string[]): Promise<void> {
618
842
  .option("name", {
619
843
  describe: "Name of the worker",
620
844
  type: "string",
845
+ requiresArg: true,
621
846
  })
622
847
  .option("format", {
623
848
  choices: ["modules", "service-worker"] as const,
624
849
  describe: "Choose an entry type",
850
+ deprecated: true,
625
851
  })
626
852
  .option("env", {
627
853
  describe: "Perform on a specific environment",
628
854
  type: "string",
855
+ requiresArg: true,
856
+ alias: "e",
629
857
  })
630
858
  .option("compatibility-date", {
631
859
  describe: "Date to use for compatibility checks",
632
860
  type: "string",
861
+ requiresArg: true,
633
862
  })
634
863
  .option("compatibility-flags", {
635
864
  describe: "Flags to use for compatibility checks",
636
- type: "array",
637
865
  alias: "compatibility-flag",
866
+ type: "string",
867
+ requiresArg: true,
868
+ array: true,
638
869
  })
639
870
  .option("latest", {
640
871
  describe: "Use the latest version of the worker runtime",
@@ -642,9 +873,9 @@ export async function main(argv: string[]): Promise<void> {
642
873
  default: true,
643
874
  })
644
875
  .option("ip", {
645
- describe: "IP address to listen on",
876
+ describe: "IP address to listen on, defaults to `localhost`",
646
877
  type: "string",
647
- default: "127.0.0.1",
878
+ requiresArg: true,
648
879
  })
649
880
  .option("port", {
650
881
  describe: "Port to listen on",
@@ -654,49 +885,66 @@ export async function main(argv: string[]): Promise<void> {
654
885
  describe: "Port for devtools to connect to",
655
886
  type: "number",
656
887
  })
888
+ .option("routes", {
889
+ describe: "Routes to upload",
890
+ alias: "route",
891
+ type: "string",
892
+ requiresArg: true,
893
+ array: true,
894
+ })
657
895
  .option("host", {
658
896
  type: "string",
897
+ requiresArg: true,
659
898
  describe:
660
899
  "Host to forward requests to, defaults to the zone of project",
661
900
  })
662
901
  .option("local-protocol", {
663
- default: "http",
664
902
  describe: "Protocol to listen to requests on, defaults to http.",
665
- choices: ["http", "https"],
903
+ choices: ["http", "https"] as const,
666
904
  })
667
905
  .option("experimental-public", {
668
906
  describe: "Static assets to be served",
669
907
  type: "string",
908
+ requiresArg: true,
670
909
  })
671
910
  .option("site", {
672
911
  describe: "Root folder of static assets for Workers Sites",
673
912
  type: "string",
913
+ requiresArg: true,
674
914
  })
675
915
  .option("site-include", {
676
916
  describe:
677
917
  "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.",
678
918
  type: "string",
919
+ requiresArg: true,
679
920
  array: true,
680
921
  })
681
922
  .option("site-exclude", {
682
923
  describe:
683
924
  "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.",
684
925
  type: "string",
926
+ requiresArg: true,
685
927
  array: true,
686
928
  })
687
929
  .option("upstream-protocol", {
688
- default: "https",
689
930
  describe:
690
931
  "Protocol to forward requests to host on, defaults to https.",
691
- choices: ["http", "https"],
932
+ choices: ["http", "https"] as const,
692
933
  })
693
934
  .option("jsx-factory", {
694
935
  describe: "The function that is called for each JSX element",
695
936
  type: "string",
937
+ requiresArg: true,
696
938
  })
697
939
  .option("jsx-fragment", {
698
940
  describe: "The function that is called for each JSX fragment",
699
941
  type: "string",
942
+ requiresArg: true,
943
+ })
944
+ .option("tsconfig", {
945
+ describe: "Path to a custom tsconfig.json file",
946
+ type: "string",
947
+ requiresArg: true,
700
948
  })
701
949
  .option("local", {
702
950
  alias: "l",
@@ -704,76 +952,194 @@ export async function main(argv: string[]): Promise<void> {
704
952
  type: "boolean",
705
953
  default: false, // I bet this will a point of contention. We'll revisit it.
706
954
  })
955
+ .option("minify", {
956
+ describe: "Minify the script",
957
+ type: "boolean",
958
+ })
959
+ .option("node-compat", {
960
+ describe: "Enable node.js compatibility",
961
+ type: "boolean",
962
+ })
707
963
  .option("experimental-enable-local-persistence", {
708
964
  describe: "Enable persistence for this session (only for local mode)",
709
965
  type: "boolean",
966
+ })
967
+ .option("inspect", {
968
+ describe: "Enable dev tools",
969
+ type: "boolean",
970
+ deprecated: true,
971
+ })
972
+ .option("legacy-env", {
973
+ type: "boolean",
974
+ describe: "Use legacy environments",
975
+ hidden: true,
710
976
  });
711
977
  },
712
978
  async (args) => {
713
- const config = readConfig(
979
+ await printWranglerBanner();
980
+ const configPath =
714
981
  (args.config as ConfigPath) ||
715
- (args.script && findWranglerToml(path.dirname(args.script)))
716
- );
717
- const entry = getEntry(config, "dev", args.script);
982
+ (args.script && findWranglerToml(path.dirname(args.script)));
983
+ const config = readConfig(configPath, args);
984
+ const entry = await getEntry(args, config, "dev");
985
+
986
+ if (config.services && config.services.length > 0) {
987
+ logger.warn(
988
+ `This worker is bound to live services: ${config.services
989
+ .map(
990
+ (service) =>
991
+ `${service.binding} (${service.service}${
992
+ service.environment ? `@${service.environment}` : ""
993
+ })`
994
+ )
995
+ .join(", ")}`
996
+ );
997
+ }
998
+
999
+ if (args.inspect) {
1000
+ logger.warn(
1001
+ "Passing --inspect is unnecessary, now you can always connect to devtools."
1002
+ );
1003
+ }
718
1004
 
719
1005
  if (args["experimental-public"]) {
720
- console.warn(
721
- "🚨 The --experimental-public field is experimental and will change in the future."
1006
+ logger.warn(
1007
+ "The --experimental-public field is experimental and will change in the future."
722
1008
  );
723
1009
  }
724
1010
 
725
1011
  if (args.public) {
726
1012
  throw new Error(
727
- "🚨 The --public field has been renamed to --experimental-public, and will change behaviour in the future."
1013
+ "The --public field has been renamed to --experimental-public, and will change behaviour in the future."
728
1014
  );
729
1015
  }
730
1016
 
731
- if (config.site?.["entry-point"]) {
732
- console.warn(
733
- "Deprecation notice: The `site.entry-point` config field is no longer used.\n" +
734
- "The entry-point is specified via the command line (e.g. `wrangler dev path/to/script`).\n" +
735
- "Please remove the `site.entry-point` field from the `wrangler.toml` file."
1017
+ const upstreamProtocol =
1018
+ args["upstream-protocol"] || config.dev.upstream_protocol;
1019
+ if (upstreamProtocol === "http") {
1020
+ logger.warn(
1021
+ "Setting upstream-protocol to http is not currently implemented.\n" +
1022
+ "If this is required in your project, please add your use case to the following issue:\n" +
1023
+ "https://github.com/cloudflare/wrangler2/issues/583."
1024
+ );
1025
+ }
1026
+
1027
+ const accountId = !args.local ? await requireAuth(config) : undefined;
1028
+
1029
+ // TODO: if worker_dev = false and no routes, then error (only for dev)
1030
+
1031
+ /**
1032
+ * Given something that resembles a URL,
1033
+ * try to extract a host from it
1034
+ */
1035
+ function getHost(urlLike: string): string | undefined {
1036
+ // strip leading * / *.
1037
+ urlLike = urlLike.replace(/^\*(\.)?/g, "");
1038
+
1039
+ if (
1040
+ !(urlLike.startsWith("http://") || urlLike.startsWith("https://"))
1041
+ ) {
1042
+ urlLike = "http://" + urlLike;
1043
+ }
1044
+ return new URL(urlLike).host;
1045
+ }
1046
+
1047
+ /**
1048
+ * Given something that resembles a host,
1049
+ * try to infer a zone id from it
1050
+ */
1051
+ async function getZoneId(host: string): Promise<string | undefined> {
1052
+ const zones = await fetchResult<{ id: string }[]>(
1053
+ `/zones`,
1054
+ {},
1055
+ new URLSearchParams({ name: host })
736
1056
  );
1057
+ return zones[0]?.id;
737
1058
  }
738
1059
 
1060
+ // When we're given a host (in one of the above ways), we do 2 things:
1061
+ // - We try to extract a host from it
1062
+ // - We try to get a zone id from the host
1063
+ //
1064
+ // So it turns out it's particularly hard to get a 'valid' domain
1065
+ // from a string, so we don't even try to validate TLDs, etc.
1066
+ // Once we get something that looks like w.x.y.z-ish, we then try to
1067
+ // get a zone id for it, by lopping off subdomains until we get a hit
1068
+ // from the API. That's it!
1069
+
1070
+ let zone: { host: string; id: string } | undefined;
1071
+
739
1072
  if (!args.local) {
740
- // -- snip, extract --
741
- const loggedIn = await loginOrRefreshIfRequired();
742
- if (!loggedIn) {
743
- // didn't login, let's just quit
744
- console.log("Did not login, quitting...");
745
- return;
1073
+ const hostLike =
1074
+ args.host ||
1075
+ config.dev.host ||
1076
+ (args.routes && args.routes[0]) ||
1077
+ config.route ||
1078
+ (config.routes && config.routes[0]);
1079
+
1080
+ let zoneId: string | undefined =
1081
+ typeof hostLike === "object" && "zone_id" in hostLike
1082
+ ? hostLike.zone_id
1083
+ : undefined;
1084
+
1085
+ const host =
1086
+ typeof hostLike === "string"
1087
+ ? getHost(hostLike)
1088
+ : typeof hostLike === "object"
1089
+ ? "zone_name" in hostLike
1090
+ ? getHost(hostLike.zone_name)
1091
+ : getHost(hostLike.pattern)
1092
+ : undefined;
1093
+
1094
+ const hostPieces =
1095
+ typeof host === "string" ? host.split(".") : undefined;
1096
+
1097
+ while (!zoneId && hostPieces && hostPieces.length > 1) {
1098
+ zoneId = await getZoneId(hostPieces.join("."));
1099
+ hostPieces.shift();
746
1100
  }
747
1101
 
748
- if (!config.account_id) {
749
- config.account_id = await getAccountId();
750
- if (!config.account_id) {
751
- throw new Error("No account id found, quitting...");
752
- }
1102
+ if (host && !zoneId) {
1103
+ throw new Error(`Could not find zone for ${hostLike}`);
753
1104
  }
754
- // -- snip, end --
1105
+
1106
+ zone =
1107
+ typeof zoneId === "string" && typeof host === "string"
1108
+ ? {
1109
+ host,
1110
+ id: zoneId,
1111
+ }
1112
+ : undefined;
755
1113
  }
756
1114
 
757
- const environments = config.env ?? {};
758
- const envRootObj = args.env ? environments[args.env] || {} : config;
1115
+ const nodeCompat = args.nodeCompat ?? config.node_compat;
1116
+ if (nodeCompat) {
1117
+ logger.warn(
1118
+ "Enabling node.js compatibility mode for built-ins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
1119
+ );
1120
+ }
759
1121
 
760
1122
  const { waitUntilExit } = render(
761
1123
  <Dev
762
1124
  name={getScriptName(args, config)}
763
1125
  entry={entry}
764
1126
  env={args.env}
765
- zone={undefined}
1127
+ zone={zone}
766
1128
  rules={getRules(config)}
767
- legacyEnv={isLegacyEnv(args, config)}
768
- buildCommand={config.build || {}}
769
- format={args.format || config.build?.upload?.format}
1129
+ legacyEnv={isLegacyEnv(config)}
1130
+ minify={args.minify ?? config.minify}
1131
+ nodeCompat={nodeCompat}
1132
+ build={config.build || {}}
770
1133
  initialMode={args.local ? "local" : "remote"}
771
- jsxFactory={args["jsx-factory"] || envRootObj?.jsx_factory}
772
- jsxFragment={args["jsx-fragment"] || envRootObj?.jsx_fragment}
1134
+ jsxFactory={args["jsx-factory"] || config.jsx_factory}
1135
+ jsxFragment={args["jsx-fragment"] || config.jsx_fragment}
1136
+ tsconfig={args.tsconfig ?? config.tsconfig}
1137
+ upstreamProtocol={upstreamProtocol}
1138
+ localProtocol={args["local-protocol"] || config.dev.local_protocol}
773
1139
  enableLocalPersistence={
774
1140
  args["experimental-enable-local-persistence"] || false
775
1141
  }
776
- accountId={config.account_id}
1142
+ accountId={accountId}
777
1143
  assetPaths={getAssetPaths(
778
1144
  config,
779
1145
  args.site,
@@ -781,24 +1147,25 @@ export async function main(argv: string[]): Promise<void> {
781
1147
  args.siteExclude
782
1148
  )}
783
1149
  port={
784
- args.port || config.dev?.port || (await getPort({ port: 8787 }))
1150
+ args.port ||
1151
+ config.dev.port ||
1152
+ (await getPort({ port: DEFAULT_LOCAL_PORT }))
785
1153
  }
1154
+ ip={args.ip || config.dev.ip}
786
1155
  inspectorPort={
787
1156
  args["inspector-port"] ?? (await getPort({ port: 9229 }))
788
1157
  }
789
1158
  public={args["experimental-public"]}
790
- compatibilityDate={
791
- args["compatibility-date"] ||
792
- config.compatibility_date ||
793
- new Date().toISOString().substring(0, 10)
794
- }
1159
+ compatibilityDate={getDevCompatibilityDate(
1160
+ config,
1161
+ args["compatibility-date"]
1162
+ )}
795
1163
  compatibilityFlags={
796
- (args["compatibility-flags"] as string[]) ||
797
- config.compatibility_flags
1164
+ args["compatibility-flags"] || config.compatibility_flags
798
1165
  }
799
- usageModel={envRootObj.usage_model}
1166
+ usageModel={config.usage_model}
800
1167
  bindings={{
801
- kv_namespaces: envRootObj.kv_namespaces?.map(
1168
+ kv_namespaces: config.kv_namespaces?.map(
802
1169
  ({ binding, preview_id, id: _id }) => {
803
1170
  // In `dev`, we make folks use a separate kv namespace called
804
1171
  // `preview_id` instead of `id` so that they don't
@@ -819,13 +1186,30 @@ export async function main(argv: string[]): Promise<void> {
819
1186
  };
820
1187
  }
821
1188
  ),
822
- vars: envRootObj.vars,
1189
+ vars: getVarsForDev(config),
823
1190
  wasm_modules: config.wasm_modules,
824
1191
  text_blobs: config.text_blobs,
825
- durable_objects: envRootObj.durable_objects,
826
- r2_buckets: envRootObj.r2_buckets,
827
- unsafe: envRootObj.unsafe?.bindings,
1192
+ data_blobs: config.data_blobs,
1193
+ durable_objects: config.durable_objects,
1194
+ r2_buckets: config.r2_buckets?.map(
1195
+ ({ binding, preview_bucket_name, bucket_name: _bucket_name }) => {
1196
+ // same idea as kv namespace preview id,
1197
+ // same copy-on-write TODO
1198
+ if (!preview_bucket_name) {
1199
+ throw new Error(
1200
+ `In development, you should use a separate r2 bucket than the one you'd use in production. Please create a new r2 bucket with "wrangler r2 bucket create <name>" and add its name as preview_bucket_name to the r2_buckets "${binding}" in your wrangler.toml`
1201
+ );
1202
+ }
1203
+ return {
1204
+ binding,
1205
+ bucket_name: preview_bucket_name,
1206
+ };
1207
+ }
1208
+ ),
1209
+ services: config.services,
1210
+ unsafe: config.unsafe?.bindings,
828
1211
  }}
1212
+ crons={config.triggers.crons}
829
1213
  />
830
1214
  );
831
1215
  await waitUntilExit();
@@ -840,28 +1224,41 @@ export async function main(argv: string[]): Promise<void> {
840
1224
  return yargs
841
1225
  .option("env", {
842
1226
  type: "string",
1227
+ requiresArg: true,
843
1228
  describe: "Perform on a specific environment",
1229
+ alias: "e",
844
1230
  })
845
1231
  .positional("script", {
846
1232
  describe: "The path to an entry point for your worker",
847
1233
  type: "string",
1234
+ requiresArg: true,
848
1235
  })
849
1236
  .option("name", {
850
1237
  describe: "Name of the worker",
851
1238
  type: "string",
1239
+ requiresArg: true,
1240
+ })
1241
+ .option("outdir", {
1242
+ describe: "Output directory for the bundled worker",
1243
+ type: "string",
1244
+ requiresArg: true,
852
1245
  })
853
1246
  .option("format", {
854
1247
  choices: ["modules", "service-worker"] as const,
855
1248
  describe: "Choose an entry type",
1249
+ deprecated: true,
856
1250
  })
857
1251
  .option("compatibility-date", {
858
1252
  describe: "Date to use for compatibility checks",
859
1253
  type: "string",
1254
+ requiresArg: true,
860
1255
  })
861
1256
  .option("compatibility-flags", {
862
1257
  describe: "Flags to use for compatibility checks",
863
- type: "array",
864
1258
  alias: "compatibility-flag",
1259
+ type: "string",
1260
+ requiresArg: true,
1261
+ array: true,
865
1262
  })
866
1263
  .option("latest", {
867
1264
  describe: "Use the latest version of the worker runtime",
@@ -871,81 +1268,100 @@ export async function main(argv: string[]): Promise<void> {
871
1268
  .option("experimental-public", {
872
1269
  describe: "Static assets to be served",
873
1270
  type: "string",
1271
+ requiresArg: true,
874
1272
  })
875
1273
  .option("site", {
876
1274
  describe: "Root folder of static assets for Workers Sites",
877
1275
  type: "string",
1276
+ requiresArg: true,
878
1277
  })
879
1278
  .option("site-include", {
880
1279
  describe:
881
1280
  "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.",
882
1281
  type: "string",
1282
+ requiresArg: true,
883
1283
  array: true,
884
1284
  })
885
1285
  .option("site-exclude", {
886
1286
  describe:
887
1287
  "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.",
888
1288
  type: "string",
1289
+ requiresArg: true,
889
1290
  array: true,
890
1291
  })
891
1292
  .option("triggers", {
892
1293
  describe: "cron schedules to attach",
893
1294
  alias: ["schedule", "schedules"],
894
- type: "array",
1295
+ type: "string",
1296
+ requiresArg: true,
1297
+ array: true,
895
1298
  })
896
1299
  .option("routes", {
897
1300
  describe: "Routes to upload",
898
1301
  alias: "route",
899
- type: "array",
1302
+ type: "string",
1303
+ requiresArg: true,
1304
+ array: true,
900
1305
  })
901
1306
  .option("jsx-factory", {
902
1307
  describe: "The function that is called for each JSX element",
903
1308
  type: "string",
1309
+ requiresArg: true,
904
1310
  })
905
1311
  .option("jsx-fragment", {
906
1312
  describe: "The function that is called for each JSX fragment",
907
1313
  type: "string",
1314
+ requiresArg: true,
1315
+ })
1316
+ .option("tsconfig", {
1317
+ describe: "Path to a custom tsconfig.json file",
1318
+ type: "string",
1319
+ requiresArg: true,
1320
+ })
1321
+ .option("minify", {
1322
+ describe: "Minify the script",
1323
+ type: "boolean",
1324
+ })
1325
+ .option("node-compat", {
1326
+ describe: "Enable node.js compatibility",
1327
+ type: "boolean",
1328
+ })
1329
+ .option("dry-run", {
1330
+ describe: "Don't actually publish",
1331
+ type: "boolean",
1332
+ })
1333
+ .option("legacy-env", {
1334
+ type: "boolean",
1335
+ describe: "Use legacy environments",
1336
+ hidden: true,
908
1337
  });
909
1338
  },
910
1339
  async (args) => {
1340
+ await printWranglerBanner();
911
1341
  if (args["experimental-public"]) {
912
- console.warn(
913
- "🚨 The --experimental-public field is experimental and will change in the future."
1342
+ logger.warn(
1343
+ "The --experimental-public field is experimental and will change in the future."
914
1344
  );
915
1345
  }
916
1346
  if (args.public) {
917
1347
  throw new Error(
918
- "🚨 The --public field has been renamed to --experimental-public, and will change behaviour in the future."
1348
+ "The --public field has been renamed to --experimental-public, and will change behaviour in the future."
919
1349
  );
920
1350
  }
921
1351
 
922
- const config = readConfig(
1352
+ const configPath =
923
1353
  (args.config as ConfigPath) ||
924
- (args.script && findWranglerToml(path.dirname(args.script)))
925
- );
926
- const entry = getEntry(config, "publish", args.script);
1354
+ (args.script && findWranglerToml(path.dirname(args.script)));
1355
+ const config = readConfig(configPath, args);
1356
+ const entry = await getEntry(args, config, "publish");
927
1357
 
928
1358
  if (args.latest) {
929
- console.warn(
930
- "⚠️ Using the latest version of the Workers runtime. To silence this warning, please choose a specific version of the runtime with --compatibility-date, or add a compatibility_date to your wrangler.toml.\n"
1359
+ logger.warn(
1360
+ "Using the latest version of the Workers runtime. To silence this warning, please choose a specific version of the runtime with --compatibility-date, or add a compatibility_date to your wrangler.toml.\n"
931
1361
  );
932
1362
  }
933
1363
 
934
- // -- snip, extract --
935
- const loggedIn = await loginOrRefreshIfRequired();
936
- if (!loggedIn) {
937
- // didn't login, let's just quit
938
- console.log("Did not login, quitting...");
939
- return;
940
- }
941
-
942
- if (!config.account_id) {
943
- config.account_id = await getAccountId();
944
- if (!config.account_id) {
945
- throw new Error("No account id found, quitting...");
946
- }
947
- }
948
- // -- snip, end --
1364
+ const accountId = args.dryRun ? undefined : await requireAuth(config);
949
1365
 
950
1366
  const assetPaths = getAssetPaths(
951
1367
  config,
@@ -953,24 +1369,30 @@ export async function main(argv: string[]): Promise<void> {
953
1369
  args.siteInclude,
954
1370
  args.siteExclude
955
1371
  );
1372
+
956
1373
  await publish({
957
1374
  config,
1375
+ accountId,
958
1376
  name: getScriptName(args, config),
959
1377
  rules: getRules(config),
960
- format: args.format || config.build?.upload?.format,
961
1378
  entry,
962
1379
  env: args.env,
963
1380
  compatibilityDate: args.latest
964
1381
  ? new Date().toISOString().substring(0, 10)
965
1382
  : args["compatibility-date"],
966
- compatibilityFlags: args["compatibility-flags"] as string[],
1383
+ compatibilityFlags: args["compatibility-flags"],
967
1384
  triggers: args.triggers,
968
1385
  jsxFactory: args["jsx-factory"],
969
1386
  jsxFragment: args["jsx-fragment"],
1387
+ tsconfig: args.tsconfig,
970
1388
  routes: args.routes,
971
1389
  assetPaths,
972
- legacyEnv: isLegacyEnv(args, config),
1390
+ legacyEnv: isLegacyEnv(config),
1391
+ minify: args.minify,
1392
+ nodeCompat: args.nodeCompat,
973
1393
  experimentalPublic: args["experimental-public"] !== undefined,
1394
+ outDir: args.outdir,
1395
+ dryRun: args.dryRun,
974
1396
  });
975
1397
  }
976
1398
  );
@@ -980,88 +1402,83 @@ export async function main(argv: string[]): Promise<void> {
980
1402
  "tail [name]",
981
1403
  "🦚 Starts a log tailing session for a published Worker.",
982
1404
  (yargs) => {
983
- return (
984
- yargs
985
- .positional("name", {
986
- describe: "Name of the worker",
987
- type: "string",
988
- })
989
- // TODO: auto-detect if this should be json or pretty based on atty
990
- .option("format", {
991
- default: "json",
992
- choices: ["json", "pretty"],
993
- describe: "The format of log entries",
994
- })
995
- .option("status", {
996
- choices: ["ok", "error", "canceled"],
997
- describe: "Filter by invocation status",
998
- array: true,
999
- })
1000
- .option("header", {
1001
- type: "string",
1002
- describe: "Filter by HTTP header",
1003
- })
1004
- .option("method", {
1005
- type: "string",
1006
- describe: "Filter by HTTP method",
1007
- array: true,
1008
- })
1009
- .option("sampling-rate", {
1010
- type: "number",
1011
- describe: "Adds a percentage of requests to log sampling rate",
1012
- })
1013
- .option("search", {
1014
- type: "string",
1015
- describe: "Filter by a text match in console.log messages",
1016
- })
1017
- .option("ip", {
1018
- type: "string",
1019
- describe:
1020
- 'Filter by the IP address the request originates from. Use "self" to filter for your own IP',
1021
- array: true,
1022
- })
1023
- .option("env", {
1024
- type: "string",
1025
- describe: "Perform on a specific environment",
1026
- })
1027
- .option("debug", {
1028
- type: "boolean",
1029
- hidden: true,
1030
- default: false,
1031
- describe:
1032
- "If a log would have been filtered out, send it through anyway alongside the filter which would have blocked it.",
1033
- })
1034
- );
1405
+ return yargs
1406
+ .positional("name", {
1407
+ describe: "Name of the worker",
1408
+ type: "string",
1409
+ })
1410
+ .option("format", {
1411
+ default: process.stdout.isTTY ? "pretty" : "json",
1412
+ choices: ["json", "pretty"],
1413
+ describe: "The format of log entries",
1414
+ })
1415
+ .option("status", {
1416
+ choices: ["ok", "error", "canceled"],
1417
+ describe: "Filter by invocation status",
1418
+ array: true,
1419
+ })
1420
+ .option("header", {
1421
+ type: "string",
1422
+ requiresArg: true,
1423
+ describe: "Filter by HTTP header",
1424
+ })
1425
+ .option("method", {
1426
+ type: "string",
1427
+ requiresArg: true,
1428
+ describe: "Filter by HTTP method",
1429
+ array: true,
1430
+ })
1431
+ .option("sampling-rate", {
1432
+ type: "number",
1433
+ describe: "Adds a percentage of requests to log sampling rate",
1434
+ })
1435
+ .option("search", {
1436
+ type: "string",
1437
+ requiresArg: true,
1438
+ describe: "Filter by a text match in console.log messages",
1439
+ })
1440
+ .option("ip", {
1441
+ type: "string",
1442
+ requiresArg: true,
1443
+ describe:
1444
+ 'Filter by the IP address the request originates from. Use "self" to filter for your own IP',
1445
+ array: true,
1446
+ })
1447
+ .option("env", {
1448
+ type: "string",
1449
+ requiresArg: true,
1450
+ describe: "Perform on a specific environment",
1451
+ alias: "e",
1452
+ })
1453
+ .option("debug", {
1454
+ type: "boolean",
1455
+ hidden: true,
1456
+ default: false,
1457
+ describe:
1458
+ "If a log would have been filtered out, send it through anyway alongside the filter which would have blocked it.",
1459
+ })
1460
+ .option("legacy-env", {
1461
+ type: "boolean",
1462
+ describe: "Use legacy environments",
1463
+ hidden: true,
1464
+ });
1035
1465
  },
1036
1466
  async (args) => {
1037
- const config = readConfig(args.config as ConfigPath);
1467
+ if (args.format === "pretty") {
1468
+ await printWranglerBanner();
1469
+ }
1470
+ const config = readConfig(args.config as ConfigPath, args);
1038
1471
 
1039
- const scriptName = getScriptName(args, config);
1472
+ const scriptName = getLegacyScriptName(args, config);
1040
1473
 
1041
1474
  if (!scriptName) {
1042
1475
  throw new Error("Missing script name");
1043
1476
  }
1044
1477
 
1045
- // -- snip, extract --
1046
- const loggedIn = await loginOrRefreshIfRequired();
1047
- if (!loggedIn) {
1048
- // didn't login, let's just quit
1049
- console.log("Did not login, quitting...");
1050
- return;
1051
- }
1052
-
1053
- if (!config.account_id) {
1054
- config.account_id = await getAccountId();
1055
- if (!config.account_id) {
1056
- throw new Error("No account id found, quitting...");
1057
- }
1058
- }
1059
- // -- snip, end --
1060
-
1061
- const accountId = config.account_id;
1478
+ const accountId = await requireAuth(config);
1062
1479
 
1063
1480
  const cliFilters: TailCLIFilters = {
1064
- status: args.status as ("ok" | "error" | "canceled")[],
1481
+ status: args.status as ("ok" | "error" | "canceled")[] | undefined,
1065
1482
  header: args.header,
1066
1483
  method: args.method,
1067
1484
  samplingRate: args["sampling-rate"],
@@ -1078,16 +1495,18 @@ export async function main(argv: string[]): Promise<void> {
1078
1495
  accountId,
1079
1496
  scriptName,
1080
1497
  filters,
1081
- !isLegacyEnv(args, config) ? args.env : undefined
1498
+ !isLegacyEnv(config) ? args.env : undefined
1082
1499
  );
1083
1500
 
1084
1501
  const scriptDisplayName = `${scriptName}${
1085
- args.env && !isLegacyEnv(args, config) ? ` (${args.env})` : ""
1502
+ args.env && !isLegacyEnv(config) ? ` (${args.env})` : ""
1086
1503
  }`;
1087
1504
 
1088
- console.log(
1089
- `successfully created tail, expires at ${expiration.toLocaleString()}`
1090
- );
1505
+ if (args.format === "pretty") {
1506
+ logger.log(
1507
+ `Successfully created tail, expires at ${expiration.toLocaleString()}`
1508
+ );
1509
+ }
1091
1510
 
1092
1511
  onExit(async () => {
1093
1512
  tail.terminate();
@@ -1114,7 +1533,9 @@ export async function main(argv: string[]): Promise<void> {
1114
1533
  }
1115
1534
  }
1116
1535
 
1117
- console.log(`Connected to ${scriptDisplayName}, waiting for logs...`);
1536
+ if (args.format === "pretty") {
1537
+ logger.log(`Connected to ${scriptDisplayName}, waiting for logs...`);
1538
+ }
1118
1539
 
1119
1540
  tail.on("close", async () => {
1120
1541
  tail.terminate();
@@ -1139,6 +1560,7 @@ export async function main(argv: string[]): Promise<void> {
1139
1560
  })
1140
1561
  .option("env", {
1141
1562
  type: "string",
1563
+ requiresArg: true,
1142
1564
  describe: "Perform on a specific environment",
1143
1565
  })
1144
1566
  .option("watch", {
@@ -1156,34 +1578,17 @@ export async function main(argv: string[]): Promise<void> {
1156
1578
  }
1157
1579
 
1158
1580
  // Delegate to `wrangler dev`
1159
- console.warn(
1581
+ logger.warn(
1160
1582
  "***************************************************\n" +
1161
1583
  "The `wrangler preview` command has been deprecated.\n" +
1162
1584
  "Attempting to run `wrangler dev` instead.\n" +
1163
1585
  "***************************************************\n"
1164
1586
  );
1165
1587
 
1166
- const config = readConfig(args.config as ConfigPath);
1167
- const entry = getEntry(config, "dev");
1168
-
1169
- // -- snip, extract --
1170
- const loggedIn = await loginOrRefreshIfRequired();
1171
- if (!loggedIn) {
1172
- // didn't login, let's just quit
1173
- console.log("Did not login, quitting...");
1174
- return;
1175
- }
1176
-
1177
- if (!config.account_id) {
1178
- config.account_id = await getAccountId();
1179
- if (!config.account_id) {
1180
- throw new Error("No account id found, quitting...");
1181
- }
1182
- }
1183
- // -- snip, end --
1588
+ const config = readConfig(args.config as ConfigPath, args);
1589
+ const entry = await getEntry({}, config, "dev");
1184
1590
 
1185
- const environments = config.env ?? {};
1186
- const envRootObj = args.env ? environments[args.env] || {} : config;
1591
+ const accountId = await requireAuth(config);
1187
1592
 
1188
1593
  const { waitUntilExit } = render(
1189
1594
  <Dev
@@ -1192,28 +1597,29 @@ export async function main(argv: string[]): Promise<void> {
1192
1597
  rules={getRules(config)}
1193
1598
  env={args.env}
1194
1599
  zone={undefined}
1195
- legacyEnv={isLegacyEnv(args, config)}
1196
- buildCommand={config.build || {}}
1197
- format={config.build?.upload?.format}
1600
+ legacyEnv={isLegacyEnv(config)}
1601
+ build={config.build || {}}
1602
+ minify={undefined}
1603
+ nodeCompat={config.node_compat}
1198
1604
  initialMode={args.local ? "local" : "remote"}
1199
- jsxFactory={envRootObj?.jsx_factory}
1200
- jsxFragment={envRootObj?.jsx_fragment}
1605
+ jsxFactory={config.jsx_factory}
1606
+ jsxFragment={config.jsx_fragment}
1607
+ tsconfig={config.tsconfig}
1608
+ upstreamProtocol={config.dev.upstream_protocol}
1609
+ localProtocol={config.dev.local_protocol}
1201
1610
  enableLocalPersistence={false}
1202
- accountId={config.account_id}
1611
+ accountId={accountId}
1203
1612
  assetPaths={undefined}
1204
- port={config.dev?.port}
1205
- public={undefined}
1206
- compatibilityDate={
1207
- config.compatibility_date ||
1208
- new Date().toISOString().substring(0, 10)
1209
- }
1210
- compatibilityFlags={
1211
- (args["compatibility-flags"] as string[]) ||
1212
- config.compatibility_flags
1613
+ port={
1614
+ config.dev.port || (await getPort({ port: DEFAULT_LOCAL_PORT }))
1213
1615
  }
1616
+ ip={config.dev.ip}
1617
+ public={undefined}
1618
+ compatibilityDate={getDevCompatibilityDate(config)}
1619
+ compatibilityFlags={config.compatibility_flags}
1214
1620
  usageModel={config.usage_model}
1215
1621
  bindings={{
1216
- kv_namespaces: envRootObj.kv_namespaces?.map(
1622
+ kv_namespaces: config.kv_namespaces?.map(
1217
1623
  ({ binding, preview_id, id: _id }) => {
1218
1624
  // In `dev`, we make folks use a separate kv namespace called
1219
1625
  // `preview_id` instead of `id` so that they don't
@@ -1234,13 +1640,30 @@ export async function main(argv: string[]): Promise<void> {
1234
1640
  };
1235
1641
  }
1236
1642
  ),
1237
- vars: envRootObj.vars,
1643
+ vars: config.vars,
1238
1644
  wasm_modules: config.wasm_modules,
1239
1645
  text_blobs: config.text_blobs,
1240
- durable_objects: envRootObj.durable_objects,
1241
- r2_buckets: envRootObj.r2_buckets,
1242
- unsafe: envRootObj.unsafe?.bindings,
1646
+ data_blobs: config.data_blobs,
1647
+ durable_objects: config.durable_objects,
1648
+ r2_buckets: config.r2_buckets?.map(
1649
+ ({ binding, preview_bucket_name, bucket_name: _bucket_name }) => {
1650
+ // same idea as kv namespace preview id,
1651
+ // same copy-on-write TODO
1652
+ if (!preview_bucket_name) {
1653
+ throw new Error(
1654
+ `In development, you should use a separate r2 bucket than the one you'd use in production. Please create a new r2 bucket with "wrangler r2 bucket create <name>" and add its name as preview_bucket_name to the r2_buckets "${binding}" in your wrangler.toml`
1655
+ );
1656
+ }
1657
+ return {
1658
+ binding,
1659
+ bucket_name: preview_bucket_name,
1660
+ };
1661
+ }
1662
+ ),
1663
+ services: config.services,
1664
+ unsafe: config.unsafe?.bindings,
1243
1665
  }}
1666
+ crons={config.triggers.crons}
1244
1667
  inspectorPort={await getPort({ port: 9229 })}
1245
1668
  />
1246
1669
  );
@@ -1248,7 +1671,7 @@ export async function main(argv: string[]): Promise<void> {
1248
1671
  }
1249
1672
  );
1250
1673
 
1251
- // route
1674
+ // [DEPRECATED] route
1252
1675
  wrangler.command(
1253
1676
  "route",
1254
1677
  false, // I think we want to hide this command
@@ -1262,10 +1685,12 @@ export async function main(argv: string[]): Promise<void> {
1262
1685
  return yargs
1263
1686
  .option("env", {
1264
1687
  type: "string",
1688
+ requiresArg: true,
1265
1689
  describe: "Perform on a specific environment",
1266
1690
  })
1267
1691
  .option("zone", {
1268
1692
  type: "string",
1693
+ requiresArg: true,
1269
1694
  describe: "Zone id",
1270
1695
  })
1271
1696
  .positional("zone", {
@@ -1273,21 +1698,21 @@ export async function main(argv: string[]): Promise<void> {
1273
1698
  type: "string",
1274
1699
  });
1275
1700
  },
1276
- async (args) => {
1277
- console.log(":route list", args);
1278
- // TODO: use environment (current wrangler doesn't do so?)
1279
- // TODO: don't get `zone_id` from config at all and require the command line arg `--zone`.
1280
- const config = readConfig(args.config as ConfigPath);
1281
- const zone = args.zone || config.zone_id;
1282
- if (!zone) {
1283
- throw new Error("missing zone id");
1284
- }
1285
-
1286
- console.log(await fetchResult(`/zones/${zone}/workers/routes`));
1701
+ () => {
1702
+ // "👯 [DEPRECATED]. Use wrangler.toml to manage routes.
1703
+ const deprecationNotice =
1704
+ "`wrangler route list` has been deprecated.";
1705
+ const futureRoutes =
1706
+ "Refer to wrangler.toml for a list of routes the worker will be deployed to upon publishing.";
1707
+ const presentRoutes =
1708
+ "Refer to the Cloudflare Dashboard to see the routes this worker is currently running on.";
1709
+ throw new DeprecationError(
1710
+ `${deprecationNotice}\n${futureRoutes}\n${presentRoutes}`
1711
+ );
1287
1712
  }
1288
1713
  )
1289
1714
  .command(
1290
- "delete <id>",
1715
+ "delete [id]",
1291
1716
  "Delete a route associated with a zone",
1292
1717
  (yargs) => {
1293
1718
  return yargs
@@ -1297,29 +1722,31 @@ export async function main(argv: string[]): Promise<void> {
1297
1722
  })
1298
1723
  .option("zone", {
1299
1724
  type: "string",
1725
+ requiresArg: true,
1300
1726
  describe: "zone id",
1301
1727
  })
1302
1728
  .option("env", {
1303
1729
  type: "string",
1730
+ requiresArg: true,
1304
1731
  describe: "Perform on a specific environment",
1305
1732
  });
1306
1733
  },
1307
- async (args) => {
1308
- console.log(":route delete", args);
1309
- // TODO: use environment (current wrangler doesn't do so?)
1310
- const config = readConfig(args.config as ConfigPath);
1311
- const zone = args.zone || config.zone_id;
1312
- if (!zone) {
1313
- throw new Error("missing zone id");
1314
- }
1315
-
1316
- console.log(
1317
- await fetchResult(`/zones/${zone}/workers/routes/${args.id}`, {
1318
- method: "DELETE",
1319
- })
1320
- );
1734
+ () => {
1735
+ // "👯 [DEPRECATED]. Use wrangler.toml to manage routes.
1736
+ const deprecationNotice =
1737
+ "`wrangler route delete` has been deprecated.";
1738
+ const shouldDo =
1739
+ "Remove the unwanted route(s) from wrangler.toml and run `wrangler publish` to remove your worker from those routes.";
1740
+ throw new DeprecationError(`${deprecationNotice}\n${shouldDo}`);
1321
1741
  }
1322
1742
  );
1743
+ },
1744
+ () => {
1745
+ // "👯 [DEPRECATED]. Use wrangler.toml to manage routes.
1746
+ const deprecationNotice = "`wrangler route` has been deprecated.";
1747
+ const shouldDo =
1748
+ "Please use wrangler.toml and/or `wrangler publish --routes` to modify routes";
1749
+ throw new DeprecationError(`${deprecationNotice}\n${shouldDo}`);
1323
1750
  }
1324
1751
  );
1325
1752
 
@@ -1333,7 +1760,7 @@ export async function main(argv: string[]): Promise<void> {
1333
1760
  },
1334
1761
  () => {
1335
1762
  throw new DeprecationError(
1336
- "`wrangler subdomain` has been deprecated, please refer to TODO://some/path for alternatives"
1763
+ "`wrangler subdomain` has been deprecated, please refer to https://developers.cloudflare.com/workers/wrangler/migration/deprecations/#subdomain for alternatives"
1337
1764
  );
1338
1765
  }
1339
1766
  );
@@ -1344,6 +1771,12 @@ export async function main(argv: string[]): Promise<void> {
1344
1771
  "🤫 Generate a secret that can be referenced in the worker script",
1345
1772
  (secretYargs) => {
1346
1773
  return secretYargs
1774
+ .command(subHelp)
1775
+ .option("legacy-env", {
1776
+ type: "boolean",
1777
+ describe: "Use legacy environments",
1778
+ hidden: true,
1779
+ })
1347
1780
  .command(
1348
1781
  "put <key>",
1349
1782
  "Create or update a secret variable for a script",
@@ -1356,53 +1789,45 @@ export async function main(argv: string[]): Promise<void> {
1356
1789
  .option("name", {
1357
1790
  describe: "Name of the worker",
1358
1791
  type: "string",
1792
+ requiresArg: true,
1359
1793
  })
1360
1794
  .option("env", {
1361
1795
  type: "string",
1796
+ requiresArg: true,
1362
1797
  describe:
1363
1798
  "Binds the secret to the Worker of the specific environment",
1799
+ alias: "e",
1364
1800
  });
1365
1801
  },
1366
1802
  async (args) => {
1367
- const config = readConfig(args.config as ConfigPath);
1803
+ await printWranglerBanner();
1804
+ const config = readConfig(args.config as ConfigPath, args);
1368
1805
 
1369
- const scriptName = getScriptName(args, config);
1806
+ const scriptName = getLegacyScriptName(args, config);
1370
1807
  if (!scriptName) {
1371
1808
  throw new Error("Missing script name");
1372
1809
  }
1373
1810
 
1374
- // -- snip, extract --
1375
- const loggedIn = await loginOrRefreshIfRequired();
1376
- if (!loggedIn) {
1377
- // didn't login, let's just quit
1378
- console.log("Did not login, quitting...");
1379
- return;
1380
- }
1381
-
1382
- if (!config.account_id) {
1383
- config.account_id = await getAccountId();
1384
- if (!config.account_id) {
1385
- throw new Error("No account id found, quitting...");
1386
- }
1387
- }
1388
- // -- snip, end --
1811
+ const accountId = await requireAuth(config);
1389
1812
 
1390
- const secretValue = await prompt(
1391
- "Enter a secret value:",
1392
- "password"
1813
+ const isInteractive = process.stdin.isTTY;
1814
+ const secretValue = trimTrailingWhitespace(
1815
+ isInteractive
1816
+ ? await prompt("Enter a secret value:", "password")
1817
+ : await readFromStdin()
1393
1818
  );
1394
1819
 
1395
- console.log(
1820
+ logger.log(
1396
1821
  `🌀 Creating the secret for script ${scriptName} ${
1397
- args.env && !isLegacyEnv(args, config) ? `(${args.env})` : ""
1822
+ args.env && !isLegacyEnv(config) ? `(${args.env})` : ""
1398
1823
  }`
1399
1824
  );
1400
1825
 
1401
1826
  async function submitSecret() {
1402
1827
  const url =
1403
- !args.env || isLegacyEnv(args, config)
1404
- ? `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1405
- : `/accounts/${config.account_id}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1828
+ !args.env || isLegacyEnv(config)
1829
+ ? `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`
1830
+ : `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1406
1831
 
1407
1832
  return await fetchResult(url, {
1408
1833
  method: "PUT",
@@ -1415,56 +1840,71 @@ export async function main(argv: string[]): Promise<void> {
1415
1840
  });
1416
1841
  }
1417
1842
 
1843
+ const createDraftWorker = async () => {
1844
+ // TODO: log a warning
1845
+ await fetchResult(
1846
+ !isLegacyEnv(config) && args.env
1847
+ ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}`
1848
+ : `/accounts/${accountId}/workers/scripts/${scriptName}`,
1849
+ {
1850
+ method: "PUT",
1851
+ body: createWorkerUploadForm({
1852
+ name: scriptName,
1853
+ main: {
1854
+ name: scriptName,
1855
+ content: `export default { fetch() {} }`,
1856
+ type: "esm",
1857
+ },
1858
+ bindings: {
1859
+ kv_namespaces: [],
1860
+ vars: {},
1861
+ durable_objects: { bindings: [] },
1862
+ r2_buckets: [],
1863
+ services: [],
1864
+ wasm_modules: {},
1865
+ text_blobs: {},
1866
+ data_blobs: {},
1867
+ unsafe: [],
1868
+ },
1869
+ modules: [],
1870
+ migrations: undefined,
1871
+ compatibility_date: undefined,
1872
+ compatibility_flags: undefined,
1873
+ usage_model: undefined,
1874
+ }),
1875
+ }
1876
+ );
1877
+ };
1878
+
1879
+ function isMissingWorkerError(e: unknown): e is { code: 10007 } {
1880
+ return (
1881
+ typeof e === "object" &&
1882
+ e !== null &&
1883
+ (e as { code: number }).code === 10007
1884
+ );
1885
+ }
1886
+
1418
1887
  try {
1419
1888
  await submitSecret();
1420
1889
  } catch (e) {
1421
- // @ts-expect-error non-standard property on Error
1422
- if (e.code === 10007) {
1423
- // upload a draft worker
1424
- // TODO: log a warning
1425
- await fetchResult(
1426
- !args["legacy-env"] && args.env
1427
- ? `/accounts/${config.account_id}/workers/services/${scriptName}/environments/${args.env}`
1428
- : `/accounts/${config.account_id}/workers/scripts/${scriptName}`,
1429
- {
1430
- method: "PUT",
1431
- body: toFormData({
1432
- name: scriptName,
1433
- main: {
1434
- name: scriptName,
1435
- content: `export default { fetch() {} }`,
1436
- type: "esm",
1437
- },
1438
- bindings: {
1439
- kv_namespaces: [],
1440
- vars: {},
1441
- durable_objects: { bindings: [] },
1442
- r2_buckets: [],
1443
- wasm_modules: {},
1444
- text_blobs: {},
1445
- unsafe: [],
1446
- },
1447
- modules: [],
1448
- migrations: undefined,
1449
- compatibility_date: undefined,
1450
- compatibility_flags: undefined,
1451
- usage_model: undefined,
1452
- }),
1453
- }
1454
- );
1455
-
1456
- // and then try again
1890
+ if (isMissingWorkerError(e)) {
1891
+ // create a draft worker and try again
1892
+ await createDraftWorker();
1457
1893
  await submitSecret();
1458
1894
  // TODO: delete the draft worker if this failed too?
1895
+ } else {
1896
+ throw e;
1459
1897
  }
1460
1898
  }
1461
- console.log(`✨ Success! Uploaded secret ${args.key}`);
1899
+
1900
+ logger.log(`✨ Success! Uploaded secret ${args.key}`);
1462
1901
  }
1463
1902
  )
1464
1903
  .command(
1465
1904
  "delete <key>",
1466
1905
  "Delete a secret variable from a script",
1467
- (yargs) => {
1906
+ async (yargs) => {
1907
+ await printWranglerBanner();
1468
1908
  return yargs
1469
1909
  .positional("key", {
1470
1910
  describe: "The variable name to be accessible in the script",
@@ -1473,59 +1913,48 @@ export async function main(argv: string[]): Promise<void> {
1473
1913
  .option("name", {
1474
1914
  describe: "Name of the worker",
1475
1915
  type: "string",
1916
+ requiresArg: true,
1476
1917
  })
1477
1918
  .option("env", {
1478
1919
  type: "string",
1920
+ requiresArg: true,
1479
1921
  describe:
1480
1922
  "Binds the secret to the Worker of the specific environment",
1923
+ alias: "e",
1481
1924
  });
1482
1925
  },
1483
1926
  async (args) => {
1484
- const config = readConfig(args.config as ConfigPath);
1927
+ const config = readConfig(args.config as ConfigPath, args);
1485
1928
 
1486
- const scriptName = getScriptName(args, config);
1929
+ const scriptName = getLegacyScriptName(args, config);
1487
1930
  if (!scriptName) {
1488
1931
  throw new Error("Missing script name");
1489
1932
  }
1490
1933
 
1491
- // -- snip, extract --
1492
- const loggedIn = await loginOrRefreshIfRequired();
1493
- if (!loggedIn) {
1494
- // didn't login, let's just quit
1495
- console.log("Did not login, quitting...");
1496
- return;
1497
- }
1498
-
1499
- if (!config.account_id) {
1500
- config.account_id = await getAccountId();
1501
- if (!config.account_id) {
1502
- throw new Error("No account id found, quitting...");
1503
- }
1504
- }
1505
- // -- snip, end --
1934
+ const accountId = await requireAuth(config);
1506
1935
 
1507
1936
  if (
1508
1937
  await confirm(
1509
1938
  `Are you sure you want to permanently delete the variable ${
1510
1939
  args.key
1511
1940
  } on the script ${scriptName}${
1512
- args.env && !isLegacyEnv(args, config) ? ` (${args.env})` : ""
1941
+ args.env && !isLegacyEnv(config) ? ` (${args.env})` : ""
1513
1942
  }?`
1514
1943
  )
1515
1944
  ) {
1516
- console.log(
1945
+ logger.log(
1517
1946
  `🌀 Deleting the secret ${args.key} on script ${scriptName}${
1518
- args.env && !isLegacyEnv(args, config) ? ` (${args.env})` : ""
1947
+ args.env && !isLegacyEnv(config) ? ` (${args.env})` : ""
1519
1948
  }`
1520
1949
  );
1521
1950
 
1522
1951
  const url =
1523
- !args.env || isLegacyEnv(args, config)
1524
- ? `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1525
- : `/accounts/${config.account_id}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1952
+ !args.env || isLegacyEnv(config)
1953
+ ? `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`
1954
+ : `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1526
1955
 
1527
1956
  await fetchResult(`${url}/${args.key}`, { method: "DELETE" });
1528
- console.log(`✨ Success! Deleted secret ${args.key}`);
1957
+ logger.log(`✨ Success! Deleted secret ${args.key}`);
1529
1958
  }
1530
1959
  }
1531
1960
  )
@@ -1537,43 +1966,32 @@ export async function main(argv: string[]): Promise<void> {
1537
1966
  .option("name", {
1538
1967
  describe: "Name of the worker",
1539
1968
  type: "string",
1969
+ requiresArg: true,
1540
1970
  })
1541
1971
  .option("env", {
1542
1972
  type: "string",
1973
+ requiresArg: true,
1543
1974
  describe:
1544
1975
  "Binds the secret to the Worker of the specific environment.",
1976
+ alias: "e",
1545
1977
  });
1546
1978
  },
1547
1979
  async (args) => {
1548
- const config = readConfig(args.config as ConfigPath);
1980
+ const config = readConfig(args.config as ConfigPath, args);
1549
1981
 
1550
- const scriptName = getScriptName(args, config);
1982
+ const scriptName = getLegacyScriptName(args, config);
1551
1983
  if (!scriptName) {
1552
1984
  throw new Error("Missing script name");
1553
1985
  }
1554
1986
 
1555
- // -- snip, extract --
1556
- const loggedIn = await loginOrRefreshIfRequired();
1557
- if (!loggedIn) {
1558
- // didn't login, let's just quit
1559
- console.log("Did not login, quitting...");
1560
- return;
1561
- }
1562
-
1563
- if (!config.account_id) {
1564
- config.account_id = await getAccountId();
1565
- if (!config.account_id) {
1566
- throw new Error("No account id found, quitting...");
1567
- }
1568
- }
1569
- // -- snip, end --
1987
+ const accountId = await requireAuth(config);
1570
1988
 
1571
1989
  const url =
1572
- !args.env || isLegacyEnv(args, config)
1573
- ? `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1574
- : `/accounts/${config.account_id}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1990
+ !args.env || isLegacyEnv(config)
1991
+ ? `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`
1992
+ : `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1575
1993
 
1576
- console.log(JSON.stringify(await fetchResult(url), null, " "));
1994
+ logger.log(JSON.stringify(await fetchResult(url), null, " "));
1577
1995
  }
1578
1996
  );
1579
1997
  }
@@ -1586,6 +2004,7 @@ export async function main(argv: string[]): Promise<void> {
1586
2004
  "🗂️ Interact with your Workers KV Namespaces",
1587
2005
  (namespaceYargs) => {
1588
2006
  return namespaceYargs
2007
+ .command(subHelp)
1589
2008
  .command(
1590
2009
  "create <namespace>",
1591
2010
  "Create a new namespace",
@@ -1598,7 +2017,9 @@ export async function main(argv: string[]): Promise<void> {
1598
2017
  })
1599
2018
  .option("env", {
1600
2019
  type: "string",
2020
+ requiresArg: true,
1601
2021
  describe: "Perform on a specific environment",
2022
+ alias: "e",
1602
2023
  })
1603
2024
  .option("preview", {
1604
2025
  type: "boolean",
@@ -1606,22 +2027,17 @@ export async function main(argv: string[]): Promise<void> {
1606
2027
  });
1607
2028
  },
1608
2029
  async (args) => {
1609
- if (args._.length > 2) {
1610
- const extraArgs = args._.slice(2).join(" ");
1611
- throw new CommandLineArgsError(
1612
- `Unexpected additional positional arguments "${extraArgs}".`
1613
- );
1614
- }
2030
+ await printWranglerBanner();
1615
2031
 
1616
- if (!isValidNamespaceBinding(args.namespace)) {
2032
+ if (!isValidKVNamespaceBinding(args.namespace)) {
1617
2033
  throw new CommandLineArgsError(
1618
2034
  `The namespace binding name "${args.namespace}" is invalid. It can only have alphanumeric and _ characters, and cannot begin with a number.`
1619
2035
  );
1620
2036
  }
1621
2037
 
1622
- const config = readConfig(args.config as ConfigPath);
2038
+ const config = readConfig(args.config as ConfigPath, args);
1623
2039
  if (!config.name) {
1624
- console.warn(
2040
+ logger.warn(
1625
2041
  "No configured name present, using `worker` as a prefix for the title"
1626
2042
  );
1627
2043
  }
@@ -1631,34 +2047,20 @@ export async function main(argv: string[]): Promise<void> {
1631
2047
  const preview = args.preview ? "_preview" : "";
1632
2048
  const title = `${name}${environment}-${args.namespace}${preview}`;
1633
2049
 
1634
- // -- snip, extract --
1635
- const loggedIn = await loginOrRefreshIfRequired();
1636
- if (!loggedIn) {
1637
- // didn't login, let's just quit
1638
- console.log("Did not login, quitting...");
1639
- return;
1640
- }
1641
-
1642
- if (!config.account_id) {
1643
- config.account_id = await getAccountId();
1644
- if (!config.account_id) {
1645
- throw new Error("No account id found, quitting...");
1646
- }
1647
- }
1648
- // -- snip, end --
2050
+ const accountId = await requireAuth(config);
1649
2051
 
1650
2052
  // TODO: generate a binding name stripping non alphanumeric chars
1651
2053
 
1652
- console.log(`🌀 Creating namespace with title "${title}"`);
1653
- const namespaceId = await createNamespace(config.account_id, title);
2054
+ logger.log(`🌀 Creating namespace with title "${title}"`);
2055
+ const namespaceId = await createKVNamespace(accountId, title);
1654
2056
 
1655
- console.log("✨ Success!");
2057
+ logger.log("✨ Success!");
1656
2058
  const envString = args.env ? ` under [env.${args.env}]` : "";
1657
2059
  const previewString = args.preview ? "preview_" : "";
1658
- console.log(
2060
+ logger.log(
1659
2061
  `Add the following to your configuration file in your kv_namespaces array${envString}:`
1660
2062
  );
1661
- console.log(
2063
+ logger.log(
1662
2064
  `{ binding = "${args.namespace}", ${previewString}id = "${namespaceId}" }`
1663
2065
  );
1664
2066
 
@@ -1670,32 +2072,14 @@ export async function main(argv: string[]): Promise<void> {
1670
2072
  "Outputs a list of all KV namespaces associated with your account id.",
1671
2073
  {},
1672
2074
  async (args) => {
1673
- const config = readConfig(args.config as ConfigPath);
1674
-
1675
- // -- snip, extract --
1676
- const loggedIn = await loginOrRefreshIfRequired();
1677
- if (!loggedIn) {
1678
- // didn't login, let's just quit
1679
- console.log("Did not login, quitting...");
1680
- return;
1681
- }
2075
+ const config = readConfig(args.config as ConfigPath, args);
1682
2076
 
1683
- if (!config.account_id) {
1684
- config.account_id = await getAccountId();
1685
- if (!config.account_id) {
1686
- throw new Error("No account id found, quitting...");
1687
- }
1688
- }
1689
- // -- snip, end --
2077
+ const accountId = await requireAuth(config);
1690
2078
 
1691
2079
  // TODO: we should show bindings if they exist for given ids
1692
2080
 
1693
- console.log(
1694
- JSON.stringify(
1695
- await listNamespaces(config.account_id),
1696
- null,
1697
- " "
1698
- )
2081
+ logger.log(
2082
+ JSON.stringify(await listKVNamespaces(accountId), null, " ")
1699
2083
  );
1700
2084
  }
1701
2085
  )
@@ -1706,16 +2090,20 @@ export async function main(argv: string[]): Promise<void> {
1706
2090
  return yargs
1707
2091
  .option("binding", {
1708
2092
  type: "string",
2093
+ requiresArg: true,
1709
2094
  describe: "The name of the namespace to delete",
1710
2095
  })
1711
2096
  .option("namespace-id", {
1712
2097
  type: "string",
2098
+ requiresArg: true,
1713
2099
  describe: "The id of the namespace to delete",
1714
2100
  })
1715
2101
  .check(demandOneOfOption("binding", "namespace-id"))
1716
2102
  .option("env", {
1717
2103
  type: "string",
2104
+ requiresArg: true,
1718
2105
  describe: "Perform on a specific environment",
2106
+ alias: "e",
1719
2107
  })
1720
2108
  .option("preview", {
1721
2109
  type: "boolean",
@@ -1723,37 +2111,21 @@ export async function main(argv: string[]): Promise<void> {
1723
2111
  });
1724
2112
  },
1725
2113
  async (args) => {
1726
- const config = readConfig(args.config as ConfigPath);
2114
+ await printWranglerBanner();
2115
+ const config = readConfig(args.config as ConfigPath, args);
1727
2116
 
1728
2117
  let id;
1729
2118
  try {
1730
- id = getNamespaceId(args, config);
2119
+ id = getKVNamespaceId(args, config);
1731
2120
  } catch (e) {
1732
2121
  throw new CommandLineArgsError(
1733
2122
  "Not able to delete namespace.\n" + ((e as Error).message ?? e)
1734
2123
  );
1735
2124
  }
1736
2125
 
1737
- // -- snip, extract --
1738
- const loggedIn = await loginOrRefreshIfRequired();
1739
- if (!loggedIn) {
1740
- // didn't login, let's just quit
1741
- console.log("Did not login, quitting...");
1742
- return;
1743
- }
1744
-
1745
- if (!config.account_id) {
1746
- config.account_id = await getAccountId();
1747
- if (!config.account_id) {
1748
- throw new Error("No account id found, quitting...");
1749
- }
1750
- }
1751
- // -- snip, end --
2126
+ const accountId = await requireAuth(config);
1752
2127
 
1753
- await fetchResult<{ id: string }>(
1754
- `/accounts/${config.account_id}/storage/kv/namespaces/${id}`,
1755
- { method: "DELETE" }
1756
- );
2128
+ await deleteKVNamespace(accountId, id);
1757
2129
 
1758
2130
  // TODO: recommend they remove it from wrangler.toml
1759
2131
 
@@ -1783,6 +2155,7 @@ export async function main(argv: string[]): Promise<void> {
1783
2155
  "🔑 Individually manage Workers KV key-value pairs",
1784
2156
  (kvKeyYargs) => {
1785
2157
  return kvKeyYargs
2158
+ .command(subHelp)
1786
2159
  .command(
1787
2160
  "put <key> [value]",
1788
2161
  "Writes a single key/value pair to the given namespace.",
@@ -1799,16 +2172,20 @@ export async function main(argv: string[]): Promise<void> {
1799
2172
  })
1800
2173
  .option("binding", {
1801
2174
  type: "string",
2175
+ requiresArg: true,
1802
2176
  describe: "The binding of the namespace to write to",
1803
2177
  })
1804
2178
  .option("namespace-id", {
1805
2179
  type: "string",
2180
+ requiresArg: true,
1806
2181
  describe: "The id of the namespace to write to",
1807
2182
  })
1808
2183
  .check(demandOneOfOption("binding", "namespace-id"))
1809
2184
  .option("env", {
1810
2185
  type: "string",
2186
+ requiresArg: true,
1811
2187
  describe: "Perform on a specific environment",
2188
+ alias: "e",
1812
2189
  })
1813
2190
  .option("preview", {
1814
2191
  type: "boolean",
@@ -1825,13 +2202,15 @@ export async function main(argv: string[]): Promise<void> {
1825
2202
  })
1826
2203
  .option("path", {
1827
2204
  type: "string",
2205
+ requiresArg: true,
1828
2206
  describe: "Read value from the file at a given path",
1829
2207
  })
1830
2208
  .check(demandOneOfOption("value", "path"));
1831
2209
  },
1832
2210
  async ({ key, ttl, expiration, ...args }) => {
1833
- const config = readConfig(args.config as ConfigPath);
1834
- const namespaceId = getNamespaceId(args, config);
2211
+ await printWranglerBanner();
2212
+ const config = readConfig(args.config as ConfigPath, args);
2213
+ const namespaceId = getKVNamespaceId(args, config);
1835
2214
  // One of `args.path` and `args.value` must be defined
1836
2215
  const value = args.path
1837
2216
  ? readFileSync(args.path)
@@ -1839,32 +2218,20 @@ export async function main(argv: string[]): Promise<void> {
1839
2218
  args.value!;
1840
2219
 
1841
2220
  if (args.path) {
1842
- console.log(
1843
- `writing the contents of ${args.path} to the key "${key}" on namespace ${namespaceId}`
2221
+ logger.log(
2222
+ `Writing the contents of ${args.path} to the key "${key}" on namespace ${namespaceId}.`
1844
2223
  );
1845
2224
  } else {
1846
- console.log(
1847
- `writing the value "${value}" to key "${key}" on namespace ${namespaceId}`
2225
+ logger.log(
2226
+ `Writing the value "${value}" to key "${key}" on namespace ${namespaceId}.`
1848
2227
  );
1849
2228
  }
1850
2229
 
1851
- // -- snip, extract --
1852
- const loggedIn = await loginOrRefreshIfRequired();
1853
- if (!loggedIn) {
1854
- // didn't login, let's just quit
1855
- console.log("Did not login, quitting...");
1856
- return;
1857
- }
1858
-
1859
- if (!config.account_id) {
1860
- config.account_id = await getAccountId();
1861
- if (!config.account_id) {
1862
- throw new Error("No account id found, quitting...");
1863
- }
1864
- }
1865
- // -- snip, end --
2230
+ const accountId = await requireAuth(config);
1866
2231
 
1867
- await putKeyValue(config.account_id, namespaceId, key, value, {
2232
+ await putKVKeyValue(accountId, namespaceId, {
2233
+ key,
2234
+ value,
1868
2235
  expiration,
1869
2236
  expiration_ttl: ttl,
1870
2237
  });
@@ -1877,16 +2244,20 @@ export async function main(argv: string[]): Promise<void> {
1877
2244
  return yargs
1878
2245
  .option("binding", {
1879
2246
  type: "string",
2247
+ requiresArg: true,
1880
2248
  describe: "The name of the namespace to list",
1881
2249
  })
1882
2250
  .option("namespace-id", {
1883
2251
  type: "string",
2252
+ requiresArg: true,
1884
2253
  describe: "The id of the namespace to list",
1885
2254
  })
1886
2255
  .check(demandOneOfOption("binding", "namespace-id"))
1887
2256
  .option("env", {
1888
2257
  type: "string",
2258
+ requiresArg: true,
1889
2259
  describe: "Perform on a specific environment",
2260
+ alias: "e",
1890
2261
  })
1891
2262
  .option("preview", {
1892
2263
  type: "boolean",
@@ -1896,36 +2267,23 @@ export async function main(argv: string[]): Promise<void> {
1896
2267
  })
1897
2268
  .option("prefix", {
1898
2269
  type: "string",
2270
+ requiresArg: true,
1899
2271
  describe: "A prefix to filter listed keys",
1900
2272
  });
1901
2273
  },
1902
2274
  async ({ prefix, ...args }) => {
1903
2275
  // TODO: support for limit+cursor (pagination)
1904
- const config = readConfig(args.config as ConfigPath);
1905
- const namespaceId = getNamespaceId(args, config);
1906
-
1907
- // -- snip, extract --
1908
- const loggedIn = await loginOrRefreshIfRequired();
1909
- if (!loggedIn) {
1910
- // didn't login, let's just quit
1911
- console.log("Did not login, quitting...");
1912
- return;
1913
- }
2276
+ const config = readConfig(args.config as ConfigPath, args);
2277
+ const namespaceId = getKVNamespaceId(args, config);
1914
2278
 
1915
- if (!config.account_id) {
1916
- config.account_id = await getAccountId();
1917
- if (!config.account_id) {
1918
- throw new Error("No account id found, quitting...");
1919
- }
1920
- }
1921
- // -- snip, end --
2279
+ const accountId = await requireAuth(config);
1922
2280
 
1923
- const results = await listNamespaceKeys(
1924
- config.account_id,
2281
+ const results = await listKVNamespaceKeys(
2282
+ accountId,
1925
2283
  namespaceId,
1926
2284
  prefix
1927
2285
  );
1928
- console.log(JSON.stringify(results, undefined, 2));
2286
+ logger.log(JSON.stringify(results, undefined, 2));
1929
2287
  }
1930
2288
  )
1931
2289
  .command(
@@ -1940,16 +2298,20 @@ export async function main(argv: string[]): Promise<void> {
1940
2298
  })
1941
2299
  .option("binding", {
1942
2300
  type: "string",
2301
+ requiresArg: true,
1943
2302
  describe: "The name of the namespace to get from",
1944
2303
  })
1945
2304
  .option("namespace-id", {
1946
2305
  type: "string",
2306
+ requiresArg: true,
1947
2307
  describe: "The id of the namespace to get from",
1948
2308
  })
1949
2309
  .check(demandOneOfOption("binding", "namespace-id"))
1950
2310
  .option("env", {
1951
2311
  type: "string",
2312
+ requiresArg: true,
1952
2313
  describe: "Perform on a specific environment",
2314
+ alias: "e",
1953
2315
  })
1954
2316
  .option("preview", {
1955
2317
  type: "boolean",
@@ -1963,26 +2325,12 @@ export async function main(argv: string[]): Promise<void> {
1963
2325
  });
1964
2326
  },
1965
2327
  async ({ key, ...args }) => {
1966
- const config = readConfig(args.config as ConfigPath);
1967
- const namespaceId = getNamespaceId(args, config);
1968
-
1969
- // -- snip, extract --
1970
- const loggedIn = await loginOrRefreshIfRequired();
1971
- if (!loggedIn) {
1972
- // didn't login, let's just quit
1973
- console.log("Did not login, quitting...");
1974
- return;
1975
- }
2328
+ const config = readConfig(args.config as ConfigPath, args);
2329
+ const namespaceId = getKVNamespaceId(args, config);
1976
2330
 
1977
- if (!config.account_id) {
1978
- config.account_id = await getAccountId();
1979
- if (!config.account_id) {
1980
- throw new Error("No account id found, quitting...");
1981
- }
1982
- }
1983
- // -- snip, end --
2331
+ const accountId = await requireAuth(config);
1984
2332
 
1985
- console.log(await getKeyValue(config.account_id, namespaceId, key));
2333
+ logger.log(await getKVKeyValue(accountId, namespaceId, key));
1986
2334
  }
1987
2335
  )
1988
2336
  .command(
@@ -1997,16 +2345,20 @@ export async function main(argv: string[]): Promise<void> {
1997
2345
  })
1998
2346
  .option("binding", {
1999
2347
  type: "string",
2348
+ requiresArg: true,
2000
2349
  describe: "The name of the namespace to delete from",
2001
2350
  })
2002
2351
  .option("namespace-id", {
2003
2352
  type: "string",
2353
+ requiresArg: true,
2004
2354
  describe: "The id of the namespace to delete from",
2005
2355
  })
2006
2356
  .check(demandOneOfOption("binding", "namespace-id"))
2007
2357
  .option("env", {
2008
2358
  type: "string",
2359
+ requiresArg: true,
2009
2360
  describe: "Perform on a specific environment",
2361
+ alias: "e",
2010
2362
  })
2011
2363
  .option("preview", {
2012
2364
  type: "boolean",
@@ -2014,33 +2366,17 @@ export async function main(argv: string[]): Promise<void> {
2014
2366
  });
2015
2367
  },
2016
2368
  async ({ key, ...args }) => {
2017
- const config = readConfig(args.config as ConfigPath);
2018
- const namespaceId = getNamespaceId(args, config);
2369
+ await printWranglerBanner();
2370
+ const config = readConfig(args.config as ConfigPath, args);
2371
+ const namespaceId = getKVNamespaceId(args, config);
2019
2372
 
2020
- console.log(
2021
- `deleting the key "${key}" on namespace ${namespaceId}`
2373
+ logger.log(
2374
+ `Deleting the key "${key}" on namespace ${namespaceId}.`
2022
2375
  );
2023
2376
 
2024
- // -- snip, extract --
2025
- const loggedIn = await loginOrRefreshIfRequired();
2026
- if (!loggedIn) {
2027
- // didn't login, let's just quit
2028
- console.log("Did not login, quitting...");
2029
- return;
2030
- }
2031
-
2032
- if (!config.account_id) {
2033
- config.account_id = await getAccountId();
2034
- if (!config.account_id) {
2035
- throw new Error("No account id found, quitting...");
2036
- }
2037
- }
2038
- // -- snip, end --
2377
+ const accountId = await requireAuth(config);
2039
2378
 
2040
- await fetchResult(
2041
- `/accounts/${config.account_id}/storage/kv/namespaces/${namespaceId}/values/${key}`,
2042
- { method: "DELETE" }
2043
- );
2379
+ await deleteKVKeyValue(accountId, namespaceId, key);
2044
2380
  }
2045
2381
  );
2046
2382
  }
@@ -2052,6 +2388,7 @@ export async function main(argv: string[]): Promise<void> {
2052
2388
  "💪 Interact with multiple Workers KV key-value pairs at once",
2053
2389
  (kvBulkYargs) => {
2054
2390
  return kvBulkYargs
2391
+ .command(subHelp)
2055
2392
  .command(
2056
2393
  "put <filename>",
2057
2394
  "Upload multiple key-value pairs to a namespace",
@@ -2064,16 +2401,20 @@ export async function main(argv: string[]): Promise<void> {
2064
2401
  })
2065
2402
  .option("binding", {
2066
2403
  type: "string",
2404
+ requiresArg: true,
2067
2405
  describe: "The name of the namespace to insert values into",
2068
2406
  })
2069
2407
  .option("namespace-id", {
2070
2408
  type: "string",
2409
+ requiresArg: true,
2071
2410
  describe: "The id of the namespace to insert values into",
2072
2411
  })
2073
2412
  .check(demandOneOfOption("binding", "namespace-id"))
2074
2413
  .option("env", {
2075
2414
  type: "string",
2415
+ requiresArg: true,
2076
2416
  describe: "Perform on a specific environment",
2417
+ alias: "e",
2077
2418
  })
2078
2419
  .option("preview", {
2079
2420
  type: "boolean",
@@ -2081,33 +2422,67 @@ export async function main(argv: string[]): Promise<void> {
2081
2422
  });
2082
2423
  },
2083
2424
  async ({ filename, ...args }) => {
2425
+ await printWranglerBanner();
2084
2426
  // The simplest implementation I could think of.
2085
2427
  // This could be made more efficient with a streaming parser/uploader
2086
2428
  // but we'll do that in the future if needed.
2087
2429
 
2088
- const config = readConfig(args.config as ConfigPath);
2089
- const namespaceId = getNamespaceId(args, config);
2430
+ const config = readConfig(args.config as ConfigPath, args);
2431
+ const namespaceId = getKVNamespaceId(args, config);
2090
2432
  const content = parseJSON(readFileSync(filename), filename);
2091
2433
 
2092
- // -- snip, extract --
2093
- const loggedIn = await loginOrRefreshIfRequired();
2094
- if (!loggedIn) {
2095
- // didn't login, let's just quit
2096
- console.log("Did not login, quitting...");
2097
- return;
2434
+ if (!Array.isArray(content)) {
2435
+ throw new Error(
2436
+ `Unexpected JSON input from "${filename}".\n` +
2437
+ `Expected an array of key-value objects but got type "${typeof content}".`
2438
+ );
2098
2439
  }
2099
2440
 
2100
- if (!config.account_id) {
2101
- config.account_id = await getAccountId();
2102
- if (!config.account_id) {
2103
- throw new Error("No account id found, quitting...");
2441
+ const errors: string[] = [];
2442
+ const warnings: string[] = [];
2443
+ for (let i = 0; i < content.length; i++) {
2444
+ const keyValue = content[i];
2445
+ if (!isKVKeyValue(keyValue)) {
2446
+ errors.push(
2447
+ `The item at index ${i} is ${JSON.stringify(keyValue)}`
2448
+ );
2449
+ } else {
2450
+ const props = unexpectedKVKeyValueProps(keyValue);
2451
+ if (props.length > 0) {
2452
+ warnings.push(
2453
+ `The item at index ${i} contains unexpected properties: ${JSON.stringify(
2454
+ props
2455
+ )}.`
2456
+ );
2457
+ }
2104
2458
  }
2105
2459
  }
2106
- // -- snip, end --
2460
+ if (warnings.length > 0) {
2461
+ logger.warn(
2462
+ `Unexpected key-value properties in "${filename}".\n` +
2463
+ warnings.join("\n")
2464
+ );
2465
+ }
2466
+ if (errors.length > 0) {
2467
+ throw new Error(
2468
+ `Unexpected JSON input from "${filename}".\n` +
2469
+ `Each item in the array should be an object that matches:\n\n` +
2470
+ `interface KeyValue {\n` +
2471
+ ` key: string;\n` +
2472
+ ` value: string;\n` +
2473
+ ` expiration?: number;\n` +
2474
+ ` expiration_ttl?: number;\n` +
2475
+ ` metadata?: object;\n` +
2476
+ ` base64?: boolean;\n` +
2477
+ `}\n\n` +
2478
+ errors.join("\n")
2479
+ );
2480
+ }
2107
2481
 
2108
- console.log(
2109
- await putBulkKeyValue(config.account_id, namespaceId, content)
2110
- );
2482
+ const accountId = await requireAuth(config);
2483
+ await putKVBulkKeyValue(accountId, namespaceId, content);
2484
+
2485
+ logger.log("Success!");
2111
2486
  }
2112
2487
  )
2113
2488
  .command(
@@ -2122,202 +2497,267 @@ export async function main(argv: string[]): Promise<void> {
2122
2497
  })
2123
2498
  .option("binding", {
2124
2499
  type: "string",
2500
+ requiresArg: true,
2125
2501
  describe: "The name of the namespace to delete from",
2126
2502
  })
2127
2503
  .option("namespace-id", {
2128
2504
  type: "string",
2505
+ requiresArg: true,
2129
2506
  describe: "The id of the namespace to delete from",
2130
2507
  })
2131
2508
  .check(demandOneOfOption("binding", "namespace-id"))
2132
2509
  .option("env", {
2133
2510
  type: "string",
2511
+ requiresArg: true,
2134
2512
  describe: "Perform on a specific environment",
2513
+ alias: "e",
2135
2514
  })
2136
2515
  .option("preview", {
2137
2516
  type: "boolean",
2138
2517
  describe: "Interact with a preview namespace",
2518
+ })
2519
+ .option("force", {
2520
+ type: "boolean",
2521
+ alias: "f",
2522
+ describe: "Do not ask for confirmation before deleting",
2139
2523
  });
2140
2524
  },
2141
2525
  async ({ filename, ...args }) => {
2142
- const config = readConfig(args.config as ConfigPath);
2143
- const namespaceId = getNamespaceId(args, config);
2144
- const content = parseJSON(readFileSync(filename), filename);
2526
+ await printWranglerBanner();
2527
+ const config = readConfig(args.config as ConfigPath, args);
2528
+ const namespaceId = getKVNamespaceId(args, config);
2529
+
2530
+ if (!args.force) {
2531
+ const result = await confirm(
2532
+ `Are you sure you want to delete all the keys read from "${filename}" from kv-namespace with id "${namespaceId}"?`
2533
+ );
2534
+ if (!result) {
2535
+ logger.log(`Not deleting keys read from "${filename}".`);
2536
+ return;
2537
+ }
2538
+ }
2539
+
2540
+ const content = parseJSON(
2541
+ readFileSync(filename),
2542
+ filename
2543
+ ) as string[];
2145
2544
 
2146
- // -- snip, extract --
2147
- const loggedIn = await loginOrRefreshIfRequired();
2148
- if (!loggedIn) {
2149
- // didn't login, let's just quit
2150
- console.log("Did not login, quitting...");
2151
- return;
2545
+ if (!Array.isArray(content)) {
2546
+ throw new Error(
2547
+ `Unexpected JSON input from "${filename}".\n` +
2548
+ `Expected an array of strings but got:\n${content}`
2549
+ );
2152
2550
  }
2153
2551
 
2154
- if (!config.account_id) {
2155
- config.account_id = await getAccountId();
2156
- if (!config.account_id) {
2157
- throw new Error("No account id found, quitting...");
2552
+ const errors: string[] = [];
2553
+ for (let i = 0; i < content.length; i++) {
2554
+ const key = content[i];
2555
+ if (typeof key !== "string") {
2556
+ errors.push(
2557
+ `The item at index ${i} is type: "${typeof key}" - ${JSON.stringify(
2558
+ key
2559
+ )}`
2560
+ );
2158
2561
  }
2159
2562
  }
2160
- // -- snip, end --
2563
+ if (errors.length > 0) {
2564
+ throw new Error(
2565
+ `Unexpected JSON input from "${filename}".\n` +
2566
+ `Expected an array of strings.\n` +
2567
+ errors.join("\n")
2568
+ );
2569
+ }
2161
2570
 
2162
- console.log(
2163
- await deleteBulkKeyValue(config.account_id, namespaceId, content)
2164
- );
2571
+ const accountId = await requireAuth(config);
2572
+
2573
+ await deleteKVBulkKeyValue(accountId, namespaceId, content);
2574
+
2575
+ logger.log("Success!");
2165
2576
  }
2166
2577
  );
2167
2578
  }
2168
2579
  );
2169
2580
 
2170
- wrangler.command("pages", "⚡️ Configure Cloudflare Pages", pages);
2581
+ wrangler.command(
2582
+ "pages",
2583
+ "⚡️ Configure Cloudflare Pages",
2584
+ async (pagesYargs) => {
2585
+ await pages(pagesYargs.command(subHelp).epilogue(pagesBetaWarning));
2586
+ }
2587
+ );
2171
2588
 
2172
2589
  wrangler.command("r2", "📦 Interact with an R2 store", (r2Yargs) => {
2173
- return r2Yargs.command("bucket", "Manage R2 buckets", (r2BucketYargs) => {
2174
- r2BucketYargs.command(
2175
- "create <name>",
2176
- "Create a new R2 bucket",
2177
- (yargs) => {
2178
- return yargs.positional("name", {
2179
- describe: "The name of the new bucket",
2180
- type: "string",
2181
- demandOption: true,
2182
- });
2183
- },
2184
- async (args) => {
2185
- // We expect three values in `_`: `r2`, `bucket`, `create`.
2186
- if (args._.length > 3) {
2187
- const extraArgs = args._.slice(3).join(" ");
2188
- throw new CommandLineArgsError(
2189
- `Unexpected additional positional arguments "${extraArgs}".`
2190
- );
2191
- }
2590
+ return r2Yargs
2591
+ .command(subHelp)
2592
+ .command("bucket", "Manage R2 buckets", (r2BucketYargs) => {
2593
+ r2BucketYargs.command(
2594
+ "create <name>",
2595
+ "Create a new R2 bucket",
2596
+ (yargs) => {
2597
+ return yargs.positional("name", {
2598
+ describe: "The name of the new bucket",
2599
+ type: "string",
2600
+ demandOption: true,
2601
+ });
2602
+ },
2603
+ async (args) => {
2604
+ await printWranglerBanner();
2192
2605
 
2193
- const config = readConfig(args.config as ConfigPath);
2606
+ const config = readConfig(args.config as ConfigPath, args);
2194
2607
 
2195
- // -- snip, extract --
2196
- const loggedIn = await loginOrRefreshIfRequired();
2197
- if (!loggedIn) {
2198
- // didn't login, let's just quit
2199
- console.log("Did not login, quitting...");
2200
- return;
2201
- }
2608
+ const accountId = await requireAuth(config);
2202
2609
 
2203
- if (!config.account_id) {
2204
- config.account_id = await getAccountId();
2205
- if (!config.account_id) {
2206
- throw new Error("No account id found, quitting...");
2207
- }
2610
+ logger.log(`Creating bucket ${args.name}.`);
2611
+ await createR2Bucket(accountId, args.name);
2612
+ logger.log(`Created bucket ${args.name}.`);
2208
2613
  }
2209
- // -- snip, end --
2614
+ );
2210
2615
 
2211
- console.log(`Creating bucket ${args.name}.`);
2212
- await createR2Bucket(config.account_id, args.name);
2213
- console.log(`Created bucket ${args.name}.`);
2214
- }
2215
- );
2616
+ r2BucketYargs.command("list", "List R2 buckets", {}, async (args) => {
2617
+ const config = readConfig(args.config as ConfigPath, args);
2216
2618
 
2217
- r2BucketYargs.command("list", "List R2 buckets", {}, async (args) => {
2218
- const config = readConfig(args.config as ConfigPath);
2619
+ const accountId = await requireAuth(config);
2219
2620
 
2220
- // -- snip, extract --
2221
- const loggedIn = await loginOrRefreshIfRequired();
2222
- if (!loggedIn) {
2223
- // didn't login, let's just quit
2224
- console.log("Did not login, quitting...");
2225
- return;
2226
- }
2621
+ logger.log(JSON.stringify(await listR2Buckets(accountId), null, 2));
2622
+ });
2227
2623
 
2228
- if (!config.account_id) {
2229
- config.account_id = await getAccountId();
2230
- if (!config.account_id) {
2231
- throw new Error("No account id found, quitting...");
2232
- }
2233
- }
2234
- // -- snip, end --
2624
+ r2BucketYargs.command(
2625
+ "delete <name>",
2626
+ "Delete an R2 bucket",
2627
+ (yargs) => {
2628
+ return yargs.positional("name", {
2629
+ describe: "The name of the bucket to delete",
2630
+ type: "string",
2631
+ demandOption: true,
2632
+ });
2633
+ },
2634
+ async (args) => {
2635
+ await printWranglerBanner();
2636
+
2637
+ const config = readConfig(args.config as ConfigPath, args);
2235
2638
 
2236
- console.log(
2237
- JSON.stringify(await listR2Buckets(config.account_id), null, 2)
2639
+ const accountId = await requireAuth(config);
2640
+
2641
+ logger.log(`Deleting bucket ${args.name}.`);
2642
+ await deleteR2Bucket(accountId, args.name);
2643
+ logger.log(`Deleted bucket ${args.name}.`);
2644
+ }
2238
2645
  );
2646
+ return r2BucketYargs;
2239
2647
  });
2648
+ });
2240
2649
 
2241
- r2BucketYargs.command(
2242
- "delete <name>",
2243
- "Delete an R2 bucket",
2244
- (yargs) => {
2245
- return yargs.positional("name", {
2246
- describe: "The name of the bucket to delete",
2247
- type: "string",
2248
- demandOption: true,
2249
- });
2250
- },
2251
- async (args) => {
2252
- // We expect three values in `_`: `r2`, `bucket`, `delete`.
2253
- if (args._.length > 3) {
2254
- const extraArgs = args._.slice(3).join(" ");
2255
- throw new CommandLineArgsError(
2256
- `Unexpected additional positional arguments "${extraArgs}".`
2257
- );
2258
- }
2650
+ /**
2651
+ * User Group: login, logout, and whoami
2652
+ * TODO: group commands into User group similar to .group() for flags in yargs
2653
+ */
2654
+ // login
2655
+ wrangler.command(
2656
+ // this needs scopes as an option?
2657
+ "login",
2658
+ "🔓 Login to Cloudflare",
2659
+ (yargs) => {
2660
+ // TODO: This needs some copy editing
2661
+ // I mean, this entire app does, but this too.
2662
+ return yargs
2663
+ .option("scopes-list", {
2664
+ describe: "List all the available OAuth scopes with descriptions",
2665
+ })
2666
+ .option("scopes", {
2667
+ describe: "Pick the set of applicable OAuth scopes when logging in",
2668
+ array: true,
2669
+ type: "string",
2670
+ requiresArg: true,
2671
+ });
2259
2672
 
2260
- const config = readConfig(args.config as ConfigPath);
2673
+ // TODO: scopes
2674
+ },
2675
+ async (args) => {
2676
+ await printWranglerBanner();
2677
+ if (args["scopes-list"]) {
2678
+ listScopes();
2679
+ return;
2680
+ }
2681
+ if (args.scopes) {
2682
+ if (args.scopes.length === 0) {
2683
+ // don't allow no scopes to be passed, that would be weird
2684
+ listScopes();
2685
+ return;
2686
+ }
2687
+ if (!validateScopeKeys(args.scopes)) {
2688
+ throw new CommandLineArgsError(
2689
+ `One of ${args.scopes} is not a valid authentication scope. Run "wrangler login --list-scopes" to see the valid scopes.`
2690
+ );
2691
+ }
2692
+ await login({ scopes: args.scopes });
2693
+ return;
2694
+ }
2695
+ await login();
2261
2696
 
2262
- // -- snip, extract --
2263
- const loggedIn = await loginOrRefreshIfRequired();
2264
- if (!loggedIn) {
2265
- // didn't login, let's just quit
2266
- console.log("Did not login, quitting...");
2267
- return;
2268
- }
2697
+ // TODO: would be nice if it optionally saved login
2698
+ // credentials inside node_modules/.cache or something
2699
+ // this way you could have multiple users on a single machine
2700
+ }
2701
+ );
2269
2702
 
2270
- if (!config.account_id) {
2271
- config.account_id = await getAccountId();
2272
- if (!config.account_id) {
2273
- throw new Error("No account id found, quitting...");
2274
- }
2275
- }
2276
- // -- snip, end --
2703
+ // logout
2704
+ wrangler.command(
2705
+ // this needs scopes as an option?
2706
+ "logout",
2707
+ "🚪 Logout from Cloudflare",
2708
+ () => {},
2709
+ async () => {
2710
+ await printWranglerBanner();
2711
+ await logout();
2712
+ }
2713
+ );
2277
2714
 
2278
- console.log(`Deleting bucket ${args.name}.`);
2279
- await deleteR2Bucket(config.account_id, args.name);
2280
- console.log(`Deleted bucket ${args.name}.`);
2281
- }
2282
- );
2283
- return r2BucketYargs;
2284
- });
2285
- });
2715
+ // whoami
2716
+ wrangler.command(
2717
+ "whoami",
2718
+ "🕵️ Retrieve your user info and test your auth config",
2719
+ () => {},
2720
+ async () => {
2721
+ await printWranglerBanner();
2722
+ await whoami();
2723
+ }
2724
+ );
2286
2725
 
2287
- wrangler
2288
- .option("legacy-env", {
2289
- type: "boolean",
2290
- describe: "Use legacy environments",
2291
- })
2292
- .option("config", {
2293
- alias: "c",
2294
- describe: "Path to .toml configuration file",
2295
- type: "string",
2296
- });
2726
+ wrangler.option("config", {
2727
+ alias: "c",
2728
+ describe: "Path to .toml configuration file",
2729
+ type: "string",
2730
+ requiresArg: true,
2731
+ });
2297
2732
 
2298
- wrangler.group(["config", "help", "version", "legacy-env"], "Flags:");
2733
+ wrangler.group(["config", "help", "version"], "Flags:");
2299
2734
  wrangler.help().alias("h", "help");
2300
2735
  wrangler.version(wranglerVersion).alias("v", "version");
2301
2736
  wrangler.exitProcess(false);
2302
2737
 
2738
+ return wrangler;
2739
+ }
2740
+
2741
+ export async function main(argv: string[]): Promise<void> {
2742
+ const wrangler = createCLIParser(argv);
2303
2743
  try {
2304
- await initialiseUserConfig();
2305
2744
  await wrangler.parse();
2306
2745
  } catch (e) {
2746
+ logger.log(""); // Just adds a bit of space
2307
2747
  if (e instanceof CommandLineArgsError) {
2308
- wrangler.showHelp("error");
2309
- console.error(""); // Just adds a bit of space
2310
- console.error(e.message);
2748
+ logger.error(e.message);
2749
+ // We are not able to ask the `wrangler` CLI parser to show help for a subcommand programmatically.
2750
+ // The workaround is to re-run the parsing with an additional `--help` flag, which will result in the correct help message being displayed.
2751
+ // The `wrangler` object is "frozen"; we cannot reuse that with different args, so we must create a new CLI parser to generate the help message.
2752
+ await createCLIParser([...argv, "--help"]).parse();
2311
2753
  } else if (e instanceof ParseError) {
2312
- console.error("");
2313
2754
  e.notes.push({
2314
2755
  text: "\nIf you think this is a bug, please open an issue at: https://github.com/cloudflare/wrangler2/issues/new",
2315
2756
  });
2316
- console.error(formatMessage(e));
2757
+ logger.log(formatMessage(e));
2317
2758
  } else {
2318
- console.error(e instanceof Error ? e.message : e);
2319
- console.error(""); // Just adds a bit of space
2320
- console.error(
2759
+ logger.error(e instanceof Error ? e.message : e);
2760
+ logger.log(
2321
2761
  `${fgGreenColor}%s${resetColor}`,
2322
2762
  "If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
2323
2763
  );
@@ -2325,3 +2765,25 @@ export async function main(argv: string[]): Promise<void> {
2325
2765
  throw e;
2326
2766
  }
2327
2767
  }
2768
+
2769
+ function getDevCompatibilityDate(
2770
+ config: Config,
2771
+ compatibilityDate = config.compatibility_date
2772
+ ) {
2773
+ const currentDate = new Date().toISOString().substring(0, 10);
2774
+ if (config.configPath !== undefined && compatibilityDate === undefined) {
2775
+ logger.warn(
2776
+ `No compatibility_date was specified. Using today's date: ${currentDate}.\n` +
2777
+ "Add one to your wrangler.toml file:\n" +
2778
+ "```\n" +
2779
+ `compatibility_date = "${currentDate}"\n` +
2780
+ "```\n" +
2781
+ "or pass it in your terminal:\n" +
2782
+ "```\n" +
2783
+ `--compatibility-date=${currentDate}\n` +
2784
+ "```\n" +
2785
+ "See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information."
2786
+ );
2787
+ }
2788
+ return compatibilityDate ?? currentDate;
2789
+ }