wrangler 0.0.0-e3cab74 → 0.0.0-e3ce82b

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