wrangler 0.0.0-e0d2f35 → 0.0.0-e1b8ac4

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