wrangler 0.0.0-e2a26a0 → 0.0.0-e3278fa

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