wrangler 0.0.0-e151223 → 0.0.0-e1b8ac4

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