wrangler 0.0.0-ece06ea → 0.0.0-ed4f2ef

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 (109) hide show
  1. package/README.md +3 -3
  2. package/bin/wrangler.js +12 -3
  3. package/miniflare-dist/index.mjs +79 -0
  4. package/miniflare-dist/index.mjs.map +7 -0
  5. package/package.json +49 -22
  6. package/pages/functions/buildWorker.ts +62 -0
  7. package/pages/functions/filepath-routing.test.ts +223 -0
  8. package/pages/functions/filepath-routing.ts +181 -0
  9. package/pages/functions/identifiers.ts +78 -0
  10. package/pages/functions/routes.ts +151 -0
  11. package/pages/functions/template-worker.ts +130 -0
  12. package/src/__tests__/configuration.test.ts +2874 -0
  13. package/src/__tests__/dev.test.tsx +588 -0
  14. package/src/__tests__/guess-worker-format.test.ts +83 -0
  15. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  16. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  17. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  18. package/src/__tests__/helpers/mock-account-id.ts +48 -0
  19. package/src/__tests__/helpers/mock-bin.ts +36 -0
  20. package/src/__tests__/helpers/mock-cfetch.ts +207 -0
  21. package/src/__tests__/helpers/mock-console.ts +70 -0
  22. package/src/__tests__/helpers/mock-dialogs.ts +65 -0
  23. package/src/__tests__/helpers/mock-kv.ts +33 -0
  24. package/src/__tests__/helpers/mock-oauth-flow.ts +178 -0
  25. package/src/__tests__/helpers/mock-stdin.ts +103 -0
  26. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  27. package/src/__tests__/helpers/run-in-tmp.ts +32 -0
  28. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  29. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  30. package/src/__tests__/https-options.test.ts +127 -0
  31. package/src/__tests__/index.test.ts +953 -119
  32. package/src/__tests__/jest.setup.ts +74 -0
  33. package/src/__tests__/kv.test.ts +1380 -0
  34. package/src/__tests__/package-manager.test.ts +206 -0
  35. package/src/__tests__/pages.test.ts +180 -0
  36. package/src/__tests__/parse.test.ts +322 -0
  37. package/src/__tests__/publish.test.ts +3946 -0
  38. package/src/__tests__/r2.test.ts +198 -0
  39. package/src/__tests__/route.test.ts +45 -0
  40. package/src/__tests__/secret.test.ts +425 -0
  41. package/src/__tests__/sentry.test.ts +146 -0
  42. package/src/__tests__/tail.test.ts +638 -0
  43. package/src/__tests__/user.test.ts +83 -0
  44. package/src/__tests__/whoami.test.tsx +95 -0
  45. package/src/bundle.ts +155 -0
  46. package/src/cfetch/index.ts +104 -0
  47. package/src/cfetch/internal.ts +139 -0
  48. package/src/cli.ts +18 -7
  49. package/src/config/README.md +107 -0
  50. package/src/config/config.ts +229 -0
  51. package/src/config/diagnostics.ts +80 -0
  52. package/src/config/environment.ts +371 -0
  53. package/src/config/index.ts +62 -0
  54. package/src/config/validation-helpers.ts +557 -0
  55. package/src/config/validation.ts +1240 -0
  56. package/src/create-worker-preview.ts +173 -0
  57. package/src/create-worker-upload-form.ts +240 -0
  58. package/src/dev/dev.tsx +413 -0
  59. package/src/dev/local.tsx +287 -0
  60. package/src/dev/remote.tsx +209 -0
  61. package/src/dev/use-esbuild.ts +109 -0
  62. package/src/dialogs.tsx +6 -14
  63. package/src/entry.ts +256 -0
  64. package/src/environment-variables.ts +33 -0
  65. package/src/errors.ts +11 -0
  66. package/src/https-options.ts +122 -0
  67. package/src/index.tsx +1503 -959
  68. package/src/inspect.ts +624 -0
  69. package/src/intl-polyfill.d.ts +139 -0
  70. package/src/kv.ts +319 -0
  71. package/src/miniflare-cli/README.md +30 -0
  72. package/src/miniflare-cli/enum-keys.ts +17 -0
  73. package/src/miniflare-cli/index.ts +38 -0
  74. package/src/module-collection.ts +257 -0
  75. package/src/open-in-browser.ts +25 -0
  76. package/src/package-manager.ts +126 -0
  77. package/src/pages.tsx +1074 -212
  78. package/src/parse.ts +183 -0
  79. package/src/paths.ts +26 -0
  80. package/src/proxy.ts +455 -0
  81. package/src/publish.ts +326 -295
  82. package/src/r2.ts +50 -0
  83. package/src/reporting.ts +131 -0
  84. package/src/selfsigned.d.ts +29 -0
  85. package/src/sites.tsx +224 -59
  86. package/src/tail/filters.ts +279 -0
  87. package/src/tail/index.ts +299 -0
  88. package/src/tail/printing.ts +62 -0
  89. package/src/user.tsx +340 -203
  90. package/src/whoami.tsx +69 -0
  91. package/src/worker.ts +172 -0
  92. package/templates/new-worker.js +15 -0
  93. package/templates/new-worker.ts +15 -0
  94. package/{static-asset-facade.js → templates/static-asset-facade.js} +1 -1
  95. package/templates/tsconfig.json +105 -0
  96. package/wrangler-dist/cli.js +136988 -131174
  97. package/wrangler-dist/cli.js.map +3 -3
  98. package/src/__tests__/fixtures/init/.gitkeep +0 -0
  99. package/src/__tests__/mock-cfetch.js +0 -46
  100. package/src/api/form_data.ts +0 -158
  101. package/src/api/inspect.ts +0 -441
  102. package/src/api/preview.ts +0 -123
  103. package/src/api/worker.ts +0 -161
  104. package/src/cfetch.ts +0 -72
  105. package/src/config.ts +0 -124
  106. package/src/dev.tsx +0 -736
  107. package/src/kv.tsx +0 -211
  108. package/src/tail.tsx +0 -71
  109. package/src/util/fetch.ts +0 -74
package/src/index.tsx CHANGED
@@ -1,23 +1,24 @@
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 { findUp } from "find-up";
8
+ import getPort from "get-port";
2
9
  import { render } from "ink";
3
- import { Dev } from "./dev";
4
- import { readFile } from "node:fs/promises";
10
+ import React from "react";
11
+ import onExit from "signal-exit";
12
+ import supportsColor from "supports-color";
5
13
  import makeCLI from "yargs";
6
- import { hideBin } from "yargs/helpers";
7
- import type yargs from "yargs";
8
- import { findUp } from "find-up";
9
- import TOML from "@iarna/toml";
10
- import type { Config } from "./config";
11
- import { confirm, prompt } from "./dialogs";
12
14
  import { version as wranglerVersion } from "../package.json";
13
- import {
14
- login,
15
- logout,
16
- listScopes,
17
- initialise as initialiseUserConfig,
18
- loginOrRefreshIfRequired,
19
- getAccountId,
20
- } from "./user";
15
+ import { fetchResult } from "./cfetch";
16
+ import { findWranglerToml, readConfig } from "./config";
17
+ import { createWorkerUploadForm } from "./create-worker-upload-form";
18
+ import Dev from "./dev/dev";
19
+ import { confirm, prompt } from "./dialogs";
20
+ import { getEntry } from "./entry";
21
+ import { DeprecationError } from "./errors";
21
22
  import {
22
23
  getNamespaceId,
23
24
  listNamespaces,
@@ -25,99 +26,160 @@ import {
25
26
  putKeyValue,
26
27
  putBulkKeyValue,
27
28
  deleteBulkKeyValue,
29
+ createNamespace,
30
+ isValidNamespaceBinding,
31
+ getKeyValue,
32
+ isKeyValue,
33
+ unexpectedKeyValueProps,
28
34
  } from "./kv";
35
+ import { getPackageManager } from "./package-manager";
36
+ import { pages, pagesBetaWarning } from "./pages";
37
+ import { formatMessage, ParseError, parseJSON, readFileSync } from "./parse";
38
+ import publish from "./publish";
39
+ import { createR2Bucket, deleteR2Bucket, listR2Buckets } from "./r2";
40
+ import { getAssetPaths } from "./sites";
41
+ import {
42
+ createTail,
43
+ jsonPrintLogs,
44
+ prettyPrintLogs,
45
+ translateCLICommandToFilterMessage,
46
+ } from "./tail";
47
+ import {
48
+ login,
49
+ logout,
50
+ listScopes,
51
+ validateScopeKeys,
52
+ requireAuth,
53
+ } from "./user";
54
+ import { whoami } from "./whoami";
29
55
 
30
- import { pages } from "./pages";
56
+ import type { Config } from "./config";
57
+ import type { TailCLIFilters } from "./tail";
58
+ import type { RawData } from "ws";
59
+ import type { CommandModule } from "yargs";
60
+ import type Yargs from "yargs";
31
61
 
32
- import cfetch from "./cfetch";
62
+ type ConfigPath = string | undefined;
33
63
 
34
- import publish from "./publish";
35
- import path from "path/posix";
36
- import { writeFile } from "node:fs/promises";
37
- import { toFormData } from "./api/form_data";
64
+ const resetColor = "\x1b[0m";
65
+ const fgGreenColor = "\x1b[32m";
38
66
 
39
- import { createTail } from "./tail";
40
- import onExit from "signal-exit";
41
- import { setTimeout } from "node:timers/promises";
42
- import * as fs from "node:fs";
43
- import { execa } from "execa";
67
+ function getRules(config: Config): Config["rules"] {
68
+ const rules = config.rules ?? config.build?.upload?.rules ?? [];
44
69
 
45
- async function readConfig(path?: string): Promise<Config> {
46
- const config: Config = {};
47
- if (!path) {
48
- path = await findUp("wrangler.toml");
49
- // TODO - terminate this early instead of going all the way to the root
70
+ if (config.rules && config.build?.upload?.rules) {
71
+ throw new Error(
72
+ `You cannot configure both [rules] and [build.upload.rules] in your wrangler.toml. Delete the \`build.upload\` section.`
73
+ );
50
74
  }
51
75
 
52
- if (path) {
53
- const tml: string = await readFile(path, "utf-8");
54
- const parsed = TOML.parse(tml) as Config;
55
- Object.assign(config, parsed);
76
+ if (config.build?.upload?.rules) {
77
+ console.warn(
78
+ `Deprecation notice: The \`build.upload.rules\` config field is no longer used, the rules should be specified via the \`rules\` config field. Delete the \`build.upload\` field from the configuration file, and add this:
79
+
80
+ ${TOML.stringify({ rules: config.build.upload.rules })}`
81
+ );
56
82
  }
83
+ return rules;
84
+ }
57
85
 
58
- const inheritedFields = [
59
- "name",
60
- "account_id",
61
- "workers_dev",
62
- "compatibility_date",
63
- "compatibility_flags",
64
- "zone_id",
65
- "routes",
66
- "route",
67
- "jsx_factory",
68
- "jsx_fragment",
69
- "polyfill_node",
70
- "site",
71
- "triggers",
72
- "usage_model",
73
- ];
74
-
75
- Object.keys(config.env || {}).forEach((env) => {
76
- inheritedFields.forEach((field) => {
77
- if (config[field] !== undefined && config.env[env][field] === undefined) {
78
- config.env[env][field] = config[field]; // TODO: - shallow copy?
86
+ function printWranglerBanner() {
87
+ // Let's not print this in tests
88
+ if (typeof jest !== "undefined") {
89
+ return;
90
+ }
91
+ const text = ` ⛅️ wrangler ${wranglerVersion} `;
92
+
93
+ console.log(
94
+ text +
95
+ "\n" +
96
+ (supportsColor.stdout
97
+ ? chalk.hex("#FF8800")("-".repeat(text.length))
98
+ : "-".repeat(text.length))
99
+ );
100
+ }
101
+
102
+ function isLegacyEnv(args: unknown, config: Config): boolean {
103
+ return config.legacy_env;
104
+ }
105
+
106
+ function getScriptName(
107
+ args: { name: string | undefined; env: string | undefined },
108
+ config: Config
109
+ ): string | undefined {
110
+ if (args.name && isLegacyEnv(args, config) && args.env) {
111
+ throw new CommandLineArgsError(
112
+ "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:" +
113
+ `
114
+ [env.${args.env}]
115
+ name = "${args.name}"
116
+ `
117
+ );
118
+ }
119
+
120
+ return args.name ?? config.name;
121
+ }
122
+
123
+ /**
124
+ * Alternative to the getScriptName() because special Legacy cases allowed "name", and "env" together in Wrangler1
125
+ */
126
+ function getLegacyScriptName(
127
+ args: { name: string | undefined; env: string | undefined },
128
+ config: Config
129
+ ) {
130
+ return args.name && args.env && isLegacyEnv(args, config)
131
+ ? `${args.name}-${args.env}`
132
+ : args.name ?? config.name;
133
+ }
134
+
135
+ /**
136
+ * Get a promise to the streamed input from stdin.
137
+ *
138
+ * This function can be used to grab the incoming stream of data from, say,
139
+ * piping the output of another process into the wrangler process.
140
+ */
141
+ function readFromStdin(): Promise<string> {
142
+ return new Promise((resolve, reject) => {
143
+ const stdin = process.stdin;
144
+ const chunks: string[] = [];
145
+
146
+ // When there is data ready to be read, the `readable` event will be triggered.
147
+ // In the handler for `readable` we call `read()` over and over until all the available data has been read.
148
+ stdin.on("readable", () => {
149
+ let chunk;
150
+ while (null !== (chunk = stdin.read())) {
151
+ chunks.push(chunk);
79
152
  }
80
153
  });
81
- });
82
154
 
83
- const mirroredFields = ["vars", "kv_namespaces", "durable_objects"];
84
- Object.keys(config.env || {}).forEach((env) => {
85
- mirroredFields.forEach((field) => {
86
- // if it exists on top level, it should exist on env defns
87
- Object.keys(config[field] || {}).forEach((fieldKey) => {
88
- if (!(fieldKey in config.env[env][field])) {
89
- console.error(
90
- `In your configuration, "${field}.${fieldKey}" exists at a top level, but not on "env.${env}". This is not what you probably want, since the field "${field}" is not inherited by environments. Please add "${field}.${fieldKey}" to "env.${env}".`
91
- );
92
- }
93
- });
155
+ // When the streamed data is complete the `end` event will be triggered.
156
+ // In the handler for `end` we join the chunks together and resolve the promise.
157
+ stdin.on("end", () => {
158
+ resolve(chunks.join(""));
94
159
  });
95
- });
96
160
 
97
- // todo: validate, add defaults
98
- // let's just do some basics for now
99
-
100
- // @ts-expect-error we're being sneaky here for now
101
- config.__path__ = path;
102
-
103
- return config;
161
+ // If there is an `error` event then the handler will reject the promise.
162
+ stdin.on("error", (err) => {
163
+ reject(err);
164
+ });
165
+ });
104
166
  }
105
167
 
106
168
  // a helper to demand one of a set of options
107
169
  // via https://github.com/yargs/yargs/issues/1093#issuecomment-491299261
108
170
  function demandOneOfOption(...options: string[]) {
109
- return function (argv: yargs.Arguments) {
171
+ return function (argv: Yargs.Arguments) {
110
172
  const count = options.filter((option) => argv[option]).length;
111
173
  const lastOption = options.pop();
112
174
 
113
175
  if (count === 0) {
114
- throw new Error(
176
+ throw new CommandLineArgsError(
115
177
  `Exactly one of the arguments ${options.join(
116
178
  ", "
117
179
  )} and ${lastOption} is required`
118
180
  );
119
181
  } else if (count > 1) {
120
- throw new Error(
182
+ throw new CommandLineArgsError(
121
183
  `Arguments ${options.join(
122
184
  ", "
123
185
  )} and ${lastOption} are mutually exclusive`
@@ -128,30 +190,56 @@ function demandOneOfOption(...options: string[]) {
128
190
  };
129
191
  }
130
192
 
193
+ class CommandLineArgsError extends Error {}
194
+
131
195
  export async function main(argv: string[]): Promise<void> {
132
- const yargs = makeCLI(hideBin(process.argv))
133
- .command(
134
- // the default is to simply print the help menu
135
- ["*"],
136
- false,
137
- () => {},
138
- () => {
139
- yargs.showHelp("log");
196
+ const wrangler = makeCLI(argv)
197
+ .strict()
198
+ // We handle errors ourselves in a try-catch around `yargs.parse`.
199
+ // If you want the "help info" to be displayed then throw an instance of `CommandLineArgsError`.
200
+ // Otherwise we just log the error that was thrown without any "help info".
201
+ .showHelpOnFail(false)
202
+ .fail((msg, error) => {
203
+ if (!error) {
204
+ // If there is only a `msg` then this came from yargs own validation, so wrap in a `CommandLineArgsError`.
205
+ error = new CommandLineArgsError(msg);
140
206
  }
141
- )
207
+ throw error;
208
+ })
142
209
  .scriptName("wrangler")
143
210
  .wrap(null);
144
211
 
145
- // you will note that we use the form for all commands where we use the builder function
146
- // to define options and subcommands. Further we return the result of this builder even
147
- // tho it's not completely necessary. The reason is that it's required for type inference
148
- // of the args in the handle function.I wish we could enforce this pattern, but this
149
- // comment will have to do for now.
212
+ // Default help command that supports the subcommands
213
+ const subHelp: CommandModule = {
214
+ command: ["*"],
215
+ handler: async (args) => {
216
+ setImmediate(() =>
217
+ wrangler.parse([...args._.map((a) => `${a}`), "--help"])
218
+ );
219
+ },
220
+ };
221
+ wrangler.command(
222
+ ["*"],
223
+ false,
224
+ () => {},
225
+ (args) => {
226
+ if (args._.length > 0) {
227
+ throw new CommandLineArgsError(`Unknown command: ${args._}.`);
228
+ } else {
229
+ wrangler.showHelp("log");
230
+ }
231
+ }
232
+ );
150
233
 
151
- // also annoying that choices[] doesn't get inferred as an enum. bleh.
234
+ // You will note that we use the form for all commands where we use the builder function
235
+ // to define options and subcommands.
236
+ // Further we return the result of this builder even though it's not completely necessary.
237
+ // The reason is that it's required for type inference of the args in the handle function.
238
+ // I wish we could enforce this pattern, but this comment will have to do for now.
239
+ // (It's also annoying that choices[] doesn't get inferred as an enum. 🤷‍♂.)
152
240
 
153
241
  // [DEPRECATED] generate
154
- yargs.command(
242
+ wrangler.command(
155
243
  // we definitely want to move away from us cloning github templates
156
244
  // we can do something better here, let's see
157
245
  "generate [name] [template]",
@@ -169,104 +257,332 @@ export async function main(argv: string[]): Promise<void> {
169
257
  },
170
258
  () => {
171
259
  // "👯 [DEPRECATED]. Scaffold a Cloudflare Workers project from a public GitHub repository.",
172
- console.error(
173
- "`wrangler generate` has been deprecated, please refer to TODO://some/path for alternatives"
260
+ throw new DeprecationError(
261
+ "`wrangler generate` has been deprecated, please refer to https://github.com/cloudflare/wrangler2/blob/main/docs/deprecations.md#generate for alternatives"
174
262
  );
175
263
  }
176
264
  );
177
265
 
178
266
  // init
179
- yargs.command(
267
+ wrangler.command(
180
268
  "init [name]",
181
269
  "📥 Create a wrangler.toml configuration file",
182
270
  (yargs) => {
183
- return yargs.positional("name", {
184
- describe: "The name of your worker.",
185
- type: "string",
186
- });
271
+ return yargs
272
+ .positional("name", {
273
+ describe: "The name of your worker",
274
+ type: "string",
275
+ })
276
+ .option("type", {
277
+ describe: "The type of worker to create",
278
+ type: "string",
279
+ choices: ["rust", "javascript", "webpack"],
280
+ deprecated: true,
281
+ })
282
+ .option("yes", {
283
+ describe: 'Answer "yes" to any prompts for new projects',
284
+ type: "boolean",
285
+ alias: "y",
286
+ });
187
287
  },
188
- async () => {
189
- const destination = path.join(process.cwd(), "wrangler.toml");
190
- if (fs.existsSync(destination)) {
191
- console.error(`${destination} already exists.`);
288
+ async (args) => {
289
+ printWranglerBanner();
290
+ if (args.type) {
291
+ let message = "The --type option is no longer supported.";
292
+ if (args.type === "webpack") {
293
+ message +=
294
+ "\nIf you wish to use webpack then you will need to create a custom build.";
295
+ // TODO: Add a link to docs
296
+ }
297
+ throw new CommandLineArgsError(message);
298
+ }
299
+
300
+ // TODO: make sure args.name is a valid identifier for a worker name
301
+
302
+ const creationDirectory = path.join(process.cwd(), args.name ?? "");
303
+
304
+ const packageManager = await getPackageManager(creationDirectory);
305
+
306
+ // TODO: ask which directory to make the worker in (defaults to args.name)
307
+ // TODO: if args.name isn't provided, ask what to name the worker
308
+
309
+ const wranglerTomlDestination = path.join(
310
+ creationDirectory,
311
+ "./wrangler.toml"
312
+ );
313
+ let justCreatedWranglerToml = false;
314
+ const workerName =
315
+ args.name || path.basename(path.resolve(process.cwd()));
316
+ if (fs.existsSync(wranglerTomlDestination)) {
317
+ console.warn(`${wranglerTomlDestination} file already exists!`);
318
+ const shouldContinue = await confirm(
319
+ "Do you want to continue initializing this project?"
320
+ );
321
+ if (!shouldContinue) {
322
+ return;
323
+ }
192
324
  } else {
325
+ await mkdir(creationDirectory, { recursive: true });
193
326
  const compatibilityDate = new Date().toISOString().substring(0, 10);
194
327
  try {
195
328
  await writeFile(
196
- destination,
197
- `compatibility_date = "${compatibilityDate}"` + "\n"
329
+ wranglerTomlDestination,
330
+ TOML.stringify({
331
+ name: workerName,
332
+ compatibility_date: compatibilityDate,
333
+ }) + "\n"
198
334
  );
199
- console.log(`✨ Succesfully created wrangler.toml`);
200
- // TODO: suggest next steps?
335
+ console.log(`✨ Successfully created wrangler.toml`);
336
+ justCreatedWranglerToml = true;
201
337
  } catch (err) {
202
- console.error(`Failed to create wrangler.toml`);
203
- console.error(err);
204
- throw err;
338
+ throw new Error(
339
+ `Failed to create wrangler.toml.\n${(err as Error).message ?? err}`
340
+ );
205
341
  }
206
342
  }
207
343
 
208
- // if no package.json, ask, and if yes, create one
209
344
  let pathToPackageJson = await findUp("package.json");
210
-
345
+ let shouldCreatePackageJson = false;
346
+ const yesFlag = args.yes ?? false;
211
347
  if (!pathToPackageJson) {
212
- if (
213
- await confirm("No package.json found. Would you like to create one?")
214
- ) {
348
+ // If no package.json exists, ask to create one
349
+ shouldCreatePackageJson =
350
+ yesFlag ||
351
+ (await confirm(
352
+ "No package.json found. Would you like to create one?"
353
+ ));
354
+
355
+ if (shouldCreatePackageJson) {
215
356
  await writeFile(
216
- path.join(process.cwd(), "package.json"),
357
+ path.join(creationDirectory, "./package.json"),
217
358
  JSON.stringify(
218
359
  {
219
- name: "worker",
220
- version: "0.0.1",
360
+ name: workerName,
361
+ version: "0.0.0",
362
+ devDependencies: {
363
+ wrangler: wranglerVersion,
364
+ },
365
+ private: true,
221
366
  },
222
367
  null,
223
368
  " "
224
369
  ) + "\n"
225
370
  );
371
+ await packageManager.install();
226
372
  console.log(`✨ Created package.json`);
227
- pathToPackageJson = path.join(process.cwd(), "package.json");
373
+ pathToPackageJson = path.join(creationDirectory, "package.json");
228
374
  } else {
229
375
  return;
230
376
  }
377
+ } else {
378
+ // If package.json exists and wrangler isn't installed,
379
+ // then ask to add wrangler to devDependencies
380
+ const packageJson = parseJSON(
381
+ readFileSync(pathToPackageJson),
382
+ pathToPackageJson
383
+ );
384
+ if (
385
+ !(
386
+ packageJson.devDependencies?.wrangler ||
387
+ packageJson.dependencies?.wrangler
388
+ )
389
+ ) {
390
+ const shouldInstall =
391
+ yesFlag ||
392
+ (await confirm(
393
+ "Would you like to install wrangler into your package.json?"
394
+ ));
395
+ if (shouldInstall) {
396
+ await packageManager.addDevDeps(`wrangler@${wranglerVersion}`);
397
+ console.log(`✨ Installed wrangler`);
398
+ }
399
+ }
231
400
  }
232
401
 
233
- // if workers-types doesn't exist as a dependency
234
- // offer to install it
235
- // and make a tsconfig?
402
+ let isTypescriptProject = false;
236
403
  let pathToTSConfig = await findUp("tsconfig.json");
237
404
  if (!pathToTSConfig) {
238
- if (await confirm("Would you like to use typescript?")) {
405
+ // If there's no tsconfig, offer to create one
406
+ // and install @cloudflare/workers-types
407
+ if (yesFlag || (await confirm("Would you like to use TypeScript?"))) {
408
+ isTypescriptProject = true;
239
409
  await writeFile(
240
- path.join(process.cwd(), "tsconfig.json"),
410
+ path.join(creationDirectory, "./tsconfig.json"),
411
+ readFileSync(path.join(__dirname, "../templates/tsconfig.json"))
412
+ );
413
+ await packageManager.addDevDeps(
414
+ "@cloudflare/workers-types",
415
+ "typescript"
416
+ );
417
+
418
+ console.log(
419
+ `✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies`
420
+ );
421
+ pathToTSConfig = path.join(creationDirectory, "tsconfig.json");
422
+ }
423
+ } else {
424
+ isTypescriptProject = true;
425
+ // If there's a tsconfig, check if @cloudflare/workers-types
426
+ // is already installed, and offer to install it if not
427
+ const packageJson = parseJSON(
428
+ readFileSync(pathToPackageJson),
429
+ pathToPackageJson
430
+ );
431
+ if (
432
+ !(
433
+ packageJson.devDependencies?.["@cloudflare/workers-types"] ||
434
+ packageJson.dependencies?.["@cloudflare/workers-types"]
435
+ )
436
+ ) {
437
+ const shouldInstall = await confirm(
438
+ "Would you like to install the type definitions for Workers into your package.json?"
439
+ );
440
+ if (shouldInstall) {
441
+ await packageManager.addDevDeps("@cloudflare/workers-types");
442
+ // We don't update the tsconfig.json because
443
+ // it could be complicated in existing projects
444
+ // and we don't want to break them. Instead, we simply
445
+ // tell the user that they need to update their tsconfig.json
446
+ console.log(
447
+ `✨ Installed @cloudflare/workers-types.\nPlease add "@cloudflare/workers-types" to compilerOptions.types in your tsconfig.json`
448
+ );
449
+ }
450
+ }
451
+ }
452
+
453
+ const packageJsonContent = parseJSON(
454
+ readFileSync(pathToPackageJson),
455
+ pathToPackageJson
456
+ );
457
+ const shouldWritePackageJsonScripts =
458
+ !packageJsonContent.scripts?.start &&
459
+ !packageJsonContent.scripts?.publish &&
460
+ shouldCreatePackageJson;
461
+
462
+ async function writePackageJsonScriptsAndUpdateWranglerToml(
463
+ isWritingScripts: boolean,
464
+ isCreatingWranglerToml: boolean,
465
+ packagePath: string,
466
+ scriptPath: string
467
+ ) {
468
+ if (isCreatingWranglerToml) {
469
+ // rewrite wrangler.toml with main = "path/to/script"
470
+ const parsedWranglerToml = TOML.parse(
471
+ readFileSync(wranglerTomlDestination)
472
+ );
473
+ fs.writeFileSync(
474
+ wranglerTomlDestination,
475
+ TOML.stringify({
476
+ name: parsedWranglerToml.name,
477
+ main: scriptPath,
478
+ compatibility_date: parsedWranglerToml.compatibility_date,
479
+ })
480
+ );
481
+ }
482
+ const isNamedWorker =
483
+ isCreatingWranglerToml && path.dirname(packagePath) !== process.cwd();
484
+
485
+ if (isWritingScripts) {
486
+ await writeFile(
487
+ packagePath,
241
488
  JSON.stringify(
242
489
  {
243
- compilerOptions: {
244
- target: "ES2020",
245
- module: "CommonJS",
246
- lib: ["ES2020"],
247
- types: ["@cloudflare/workers-types"],
490
+ ...packageJsonContent,
491
+ scripts: {
492
+ ...packageJsonContent.scripts,
493
+ start: isCreatingWranglerToml
494
+ ? `wrangler dev`
495
+ : `wrangler dev ${scriptPath}`,
496
+ publish: isCreatingWranglerToml
497
+ ? `wrangler publish`
498
+ : `wrangler publish ${scriptPath}`,
248
499
  },
249
500
  },
250
501
  null,
251
- " "
502
+ 2
252
503
  ) + "\n"
253
504
  );
254
- await execa("npm", [
255
- "install",
256
- "@cloudflare/workers-types",
257
- "--save-dev",
258
- ]);
259
505
  console.log(
260
- `✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies`
506
+ `\nTo start developing your Worker, run \`${
507
+ isNamedWorker ? `cd ${args.name} && ` : ""
508
+ }npm start\``
509
+ );
510
+ console.log(
511
+ `To publish your Worker to the Internet, run \`npm run publish\``
512
+ );
513
+ } else {
514
+ console.log(
515
+ `\nTo start developing your Worker, run \`npx wrangler dev\`${
516
+ isCreatingWranglerToml ? "" : ` ${scriptPath}`
517
+ }`
518
+ );
519
+ console.log(
520
+ `To publish your Worker to the Internet, run \`npx wrangler publish\`${
521
+ isCreatingWranglerToml ? "" : ` ${scriptPath}`
522
+ }`
523
+ );
524
+ }
525
+ }
526
+
527
+ if (isTypescriptProject) {
528
+ if (!fs.existsSync(path.join(creationDirectory, "./src/index.ts"))) {
529
+ let shouldCreateSource = false;
530
+
531
+ shouldCreateSource =
532
+ yesFlag ||
533
+ (await confirm(
534
+ `Would you like to create a Worker at src/index.ts?`
535
+ ));
536
+
537
+ if (shouldCreateSource) {
538
+ await mkdir(path.join(creationDirectory, "./src"), {
539
+ recursive: true,
540
+ });
541
+ await writeFile(
542
+ path.join(creationDirectory, "./src/index.ts"),
543
+ readFileSync(path.join(__dirname, "../templates/new-worker.ts"))
544
+ );
545
+
546
+ console.log(`✨ Created src/index.ts`);
547
+
548
+ await writePackageJsonScriptsAndUpdateWranglerToml(
549
+ shouldWritePackageJsonScripts,
550
+ justCreatedWranglerToml,
551
+ pathToPackageJson,
552
+ "src/index.ts"
553
+ );
554
+ }
555
+ }
556
+ } else {
557
+ if (!fs.existsSync("./src/index.js")) {
558
+ const shouldCreateSource = await confirm(
559
+ `Would you like to create a Worker at src/index.js?`
261
560
  );
262
- pathToTSConfig = path.join(process.cwd(), "tsconfig.json");
561
+ if (shouldCreateSource) {
562
+ await mkdir(path.join(creationDirectory, "./src"), {
563
+ recursive: true,
564
+ });
565
+ await writeFile(
566
+ path.join(path.join(creationDirectory, "./src/index.js")),
567
+ readFileSync(path.join(__dirname, "../templates/new-worker.js"))
568
+ );
569
+
570
+ console.log(`✨ Created src/index.js`);
571
+
572
+ await writePackageJsonScriptsAndUpdateWranglerToml(
573
+ shouldWritePackageJsonScripts,
574
+ justCreatedWranglerToml,
575
+ pathToPackageJson,
576
+ "src/index.js"
577
+ );
578
+ }
263
579
  }
264
580
  }
265
581
  }
266
582
  );
267
583
 
268
584
  // build
269
- yargs.command(
585
+ wrangler.command(
270
586
  "build",
271
587
  false,
272
588
  (yargs) => {
@@ -276,100 +592,37 @@ export async function main(argv: string[]): Promise<void> {
276
592
  },
277
593
  () => {
278
594
  // "[DEPRECATED] 🦀 Build your project (if applicable)",
279
- console.error(
280
- "`wrangler build` has been deprecated, please refer to TODO://some/path for alternatives"
595
+ throw new DeprecationError(
596
+ "`wrangler build` has been deprecated, please refer to https://github.com/cloudflare/wrangler2/blob/main/docs/deprecations.md#build for alternatives"
281
597
  );
282
598
  }
283
599
  );
284
600
 
285
- // login
286
- yargs.command(
287
- // this needs scopes as an option?
288
- "login",
289
- false, // we don't need to show this in the menu
290
- // "🔓 Login to Cloudflare",
291
- (yargs) => {
292
- // TODO: This needs some copy editing
293
- // I mean, this entire app does, but this too.
294
- return yargs
295
- .option("scopes-list", {
296
- describe: "list all the available OAuth scopes with descriptions.",
297
- })
298
- .option("scopes", {
299
- describe: "allows to choose your set of OAuth scopes.",
300
- array: true,
301
- type: "string",
302
- });
303
-
304
- // TODO: scopes
305
- },
306
- async (args) => {
307
- if (args["scopes-list"]) {
308
- listScopes();
309
- return;
310
- }
311
- if (args.scopes) {
312
- if (args.scopes.length === 0) {
313
- // don't allow no scopes to be passed, that would be weird
314
- listScopes();
315
- return;
316
- }
317
- await login({ scopes: args.scopes });
318
- return;
319
- }
320
- await login();
321
-
322
- // TODO: would be nice if it optionally saved login
323
- // creds inside node_modules/.cache or something
324
- // this way you could have multiple users on a single machine
325
- }
326
- );
327
-
328
- // logout
329
- yargs.command(
330
- // this needs scopes as an option?
331
- "logout",
332
- false, // we don't need to show this in the menu
333
- // "🚪 Logout from Cloudflare",
334
- () => {},
335
- async () => {
336
- await logout();
337
- }
338
- );
339
-
340
- // whoami
341
- yargs.command(
342
- "whoami",
343
- false, // we don't need to show this the menu
344
- // "🕵️ Retrieve your user info and test your auth config",
345
- () => {},
346
- (args) => {
347
- console.log(":whoami", args);
348
- }
349
- );
350
-
351
601
  // config
352
- yargs.command(
602
+ wrangler.command(
353
603
  "config",
354
604
  false,
355
605
  () => {},
356
606
  () => {
357
607
  // "🕵️ Authenticate Wrangler with a Cloudflare API Token",
358
- console.error(
359
- "`wrangler config` has been deprecated, please refer to TODO://some/path for alternatives"
608
+ throw new DeprecationError(
609
+ "`wrangler config` has been deprecated, please refer to https://github.com/cloudflare/wrangler2/blob/main/docs/deprecations.md#config for alternatives"
360
610
  );
361
611
  }
362
612
  );
363
613
 
364
614
  // dev
365
- yargs.command(
366
- "dev <filename>",
615
+ wrangler.command(
616
+ "dev [script]",
367
617
  "👂 Start a local server for developing your worker",
368
618
  (yargs) => {
369
619
  return yargs
370
- .positional("filename", { describe: "entry point", type: "string" })
620
+ .positional("script", {
621
+ describe: "The path to an entry point for your worker",
622
+ type: "string",
623
+ })
371
624
  .option("name", {
372
- describe: "name of the script",
625
+ describe: "Name of the worker",
373
626
  type: "string",
374
627
  })
375
628
  .option("format", {
@@ -379,17 +632,38 @@ export async function main(argv: string[]): Promise<void> {
379
632
  .option("env", {
380
633
  describe: "Perform on a specific environment",
381
634
  type: "string",
382
- // TODO: get choices for the toml file?
635
+ alias: "e",
636
+ })
637
+ .option("compatibility-date", {
638
+ describe: "Date to use for compatibility checks",
639
+ type: "string",
640
+ })
641
+ .option("compatibility-flags", {
642
+ describe: "Flags to use for compatibility checks",
643
+ type: "array",
644
+ alias: "compatibility-flag",
645
+ })
646
+ .option("latest", {
647
+ describe: "Use the latest version of the worker runtime",
648
+ type: "boolean",
649
+ default: true,
383
650
  })
384
651
  .option("ip", {
385
- describe: "IP address to listen on",
652
+ describe: "IP address to listen on, defaults to `localhost`",
386
653
  type: "string",
387
- default: "127.0.0.1",
388
654
  })
389
655
  .option("port", {
390
- describe: "Port to listen on, defaults to 8787",
656
+ describe: "Port to listen on",
657
+ type: "number",
658
+ })
659
+ .option("inspector-port", {
660
+ describe: "Port for devtools to connect to",
391
661
  type: "number",
392
- default: 8787,
662
+ })
663
+ .option("routes", {
664
+ describe: "Routes to upload",
665
+ alias: "route",
666
+ type: "array",
393
667
  })
394
668
  .option("host", {
395
669
  type: "string",
@@ -397,11 +671,10 @@ export async function main(argv: string[]): Promise<void> {
397
671
  "Host to forward requests to, defaults to the zone of project",
398
672
  })
399
673
  .option("local-protocol", {
400
- default: "http",
401
674
  describe: "Protocol to listen to requests on, defaults to http.",
402
- choices: ["http", "https"],
675
+ choices: ["http", "https"] as const,
403
676
  })
404
- .option("public", {
677
+ .option("experimental-public", {
405
678
  describe: "Static assets to be served",
406
679
  type: "string",
407
680
  })
@@ -409,11 +682,22 @@ export async function main(argv: string[]): Promise<void> {
409
682
  describe: "Root folder of static assets for Workers Sites",
410
683
  type: "string",
411
684
  })
685
+ .option("site-include", {
686
+ describe:
687
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.",
688
+ type: "string",
689
+ array: true,
690
+ })
691
+ .option("site-exclude", {
692
+ describe:
693
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.",
694
+ type: "string",
695
+ array: true,
696
+ })
412
697
  .option("upstream-protocol", {
413
- default: "https",
414
698
  describe:
415
699
  "Protocol to forward requests to host on, defaults to https.",
416
- choices: ["http", "https"],
700
+ choices: ["http", "https"] as const,
417
701
  })
418
702
  .option("jsx-factory", {
419
703
  describe: "The function that is called for each JSX element",
@@ -423,89 +707,209 @@ export async function main(argv: string[]): Promise<void> {
423
707
  describe: "The function that is called for each JSX fragment",
424
708
  type: "string",
425
709
  })
426
- .option("polyfill-node", {
427
- describe: "Try to polyfill node builtins",
710
+ .option("tsconfig", {
711
+ describe: "Path to a custom tsconfig.json file",
712
+ type: "string",
713
+ })
714
+ .option("local", {
715
+ alias: "l",
716
+ describe: "Run on my machine",
717
+ type: "boolean",
718
+ default: false, // I bet this will a point of contention. We'll revisit it.
719
+ })
720
+ .option("experimental-enable-local-persistence", {
721
+ describe: "Enable persistence for this session (only for local mode)",
428
722
  type: "boolean",
429
723
  });
430
724
  },
431
725
  async (args) => {
432
- const { filename, format } = args;
433
- const config = args.config as Config;
726
+ printWranglerBanner();
727
+ const configPath =
728
+ (args.config as ConfigPath) ||
729
+ (args.script && findWranglerToml(path.dirname(args.script)));
730
+ const config = readConfig(configPath, args);
731
+ const entry = await getEntry(args, config, "dev");
732
+
733
+ if (args["experimental-public"]) {
734
+ console.warn(
735
+ "🚨 The --experimental-public field is experimental and will change in the future."
736
+ );
737
+ }
434
738
 
435
- // -- snip, extract --
739
+ if (args.public) {
740
+ throw new Error(
741
+ "🚨 The --public field has been renamed to --experimental-public, and will change behaviour in the future."
742
+ );
743
+ }
436
744
 
437
- if (!args.local) {
438
- const loggedIn = await loginOrRefreshIfRequired();
439
- if (!loggedIn) {
440
- // didn't login, let's just quit
441
- console.log("Did not login, quitting...");
442
- return;
443
- }
444
- if (!config.account_id) {
445
- config.account_id = await getAccountId();
446
- if (!config.account_id) {
447
- console.error("No account id found, quitting...");
448
- return;
449
- }
450
- }
745
+ const upstreamProtocol =
746
+ (args["upstream-protocol"] as "http" | "https") ||
747
+ config.dev.upstream_protocol;
748
+ if (upstreamProtocol === "http") {
749
+ console.warn(
750
+ "Setting upstream-protocol to http is not currently implemented.\n" +
751
+ "If this is required in your project, please add your use case to the following issue:\n" +
752
+ "https://github.com/cloudflare/wrangler2/issues/583."
753
+ );
451
754
  }
452
755
 
453
- // -- snip, end --
756
+ const accountId = !args.local ? await requireAuth(config) : undefined;
454
757
 
455
- const envRootObj = args.env ? config.env[args.env] || {} : config;
758
+ // TODO: if worker_dev = false and no routes, then error (only for dev)
456
759
 
457
- const polyfillNode = args["polyfill-node"] ?? config.polyfill_node;
458
- if (polyfillNode) {
459
- console.warn(
460
- "Using polyfills for node builtin modules. These are not meant to be completely reliable, and have serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
760
+ /**
761
+ * Given something that resembles a URL,
762
+ * try to extract a host from it
763
+ */
764
+ function getHost(urlLike: string): string | undefined {
765
+ if (
766
+ !(urlLike.startsWith("http://") || urlLike.startsWith("https://"))
767
+ ) {
768
+ urlLike = "http://" + urlLike;
769
+ }
770
+ return new URL(urlLike).host;
771
+ }
772
+
773
+ /**
774
+ * Given something that resembles a host,
775
+ * try to infer a zone id from it
776
+ */
777
+ async function getZoneId(host: string): Promise<string | undefined> {
778
+ const zones = await fetchResult<{ id: string }[]>(
779
+ `/zones`,
780
+ {},
781
+ new URLSearchParams({ name: host })
461
782
  );
783
+ return zones[0]?.id;
784
+ }
785
+
786
+ // When we're given a host (in one of the above ways), we do 2 things:
787
+ // - We try to extract a host from it
788
+ // - We try to get a zone id from the host
789
+ //
790
+ // So it turns out it's particularly hard to get a 'valid' domain
791
+ // from a string, so we don't even try to validate TLDs, etc.
792
+ // Once we get something that looks like w.x.y.z-ish, we then try to
793
+ // get a zone id for it, by lopping off subdomains until we get a hit
794
+ // from the API. That's it!
795
+
796
+ let zone: { host: string; id: string } | undefined;
797
+
798
+ if (!args.local) {
799
+ const hostLike =
800
+ args.host ||
801
+ config.dev.host ||
802
+ (args.routes && args.routes[0]) ||
803
+ config.route ||
804
+ (config.routes && config.routes[0]);
805
+ const host =
806
+ typeof hostLike === "string" ? getHost(hostLike) : undefined;
807
+ let zoneId: string | undefined;
808
+ const hostPieces =
809
+ typeof host === "string" ? host.split(".") : undefined;
810
+ while (hostPieces && hostPieces.length > 1) {
811
+ zoneId = await getZoneId(hostPieces.join("."));
812
+ if (zoneId) break;
813
+ hostPieces.shift();
814
+ }
815
+ if (host && !zoneId) {
816
+ throw new Error(`Could not find zone for ${hostLike}`);
817
+ }
818
+ zone =
819
+ typeof zoneId === "string" && typeof host === "string"
820
+ ? {
821
+ host,
822
+ id: zoneId,
823
+ }
824
+ : undefined;
462
825
  }
463
826
 
464
- render(
827
+ const { waitUntilExit } = render(
465
828
  <Dev
466
- name={args.name || config.name}
467
- entry={filename}
468
- format={format}
829
+ name={getScriptName(args, config)}
830
+ entry={entry}
831
+ env={args.env}
832
+ zone={zone}
833
+ rules={getRules(config)}
834
+ legacyEnv={isLegacyEnv(args, config)}
835
+ build={config.build || {}}
469
836
  initialMode={args.local ? "local" : "remote"}
470
- jsxFactory={args["jsx-factory"] || envRootObj?.jsx_factory}
471
- jsxFragment={args["jsx-fragment"] || envRootObj?.jsx_fragment}
472
- accountId={config.account_id}
473
- site={args.site || config.site?.bucket}
474
- port={args.port || config.dev?.port}
475
- polyfillNode={polyfillNode}
476
- public={args.public}
477
- compatibilityDate={config.compatibility_date}
478
- compatibilityFlags={config.compatibility_flags}
837
+ jsxFactory={args["jsx-factory"] || config.jsx_factory}
838
+ jsxFragment={args["jsx-fragment"] || config.jsx_fragment}
839
+ tsconfig={args.tsconfig ?? config.tsconfig}
840
+ upstreamProtocol={upstreamProtocol}
841
+ localProtocol={
842
+ // The typings are not quite clever enough to handle element accesses, only property accesses,
843
+ // so we need to cast here.
844
+ (args["local-protocol"] as "http" | "https") ||
845
+ config.dev.local_protocol
846
+ }
847
+ enableLocalPersistence={
848
+ args["experimental-enable-local-persistence"] || false
849
+ }
850
+ accountId={accountId}
851
+ assetPaths={getAssetPaths(
852
+ config,
853
+ args.site,
854
+ args.siteInclude,
855
+ args.siteExclude
856
+ )}
857
+ port={
858
+ args.port || config.dev?.port || (await getPort({ port: 8787 }))
859
+ }
860
+ ip={args.ip || config.dev.ip}
861
+ inspectorPort={
862
+ args["inspector-port"] ?? (await getPort({ port: 9229 }))
863
+ }
864
+ public={args["experimental-public"]}
865
+ compatibilityDate={getDevCompatibilityDate(
866
+ config,
867
+ args["compatibility-date"]
868
+ )}
869
+ compatibilityFlags={
870
+ (args["compatibility-flags"] as string[]) ||
871
+ config.compatibility_flags
872
+ }
479
873
  usageModel={config.usage_model}
480
- variables={{
481
- ...(envRootObj?.vars || {}),
482
- ...(envRootObj?.kv_namespaces || []).reduce(
483
- (obj, { binding, preview_id }) => {
874
+ bindings={{
875
+ kv_namespaces: config.kv_namespaces?.map(
876
+ ({ binding, preview_id, id: _id }) => {
877
+ // In `dev`, we make folks use a separate kv namespace called
878
+ // `preview_id` instead of `id` so that they don't
879
+ // break production data. So here we check that a `preview_id`
880
+ // has actually been configured.
881
+ // This whole block of code will be obsoleted in the future
882
+ // when we have copy-on-write for previews on edge workers.
484
883
  if (!preview_id) {
485
884
  // TODO: This error has to be a _lot_ better, ideally just asking
486
885
  // to create a preview namespace for the user automatically
487
886
  throw new Error(
488
- "kv namespaces need a preview id during dev mode"
489
- );
887
+ `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`
888
+ ); // Ugh, I really don't like this message very much
490
889
  }
491
- return { ...obj, [binding]: { namespaceId: preview_id } };
492
- },
493
- {}
494
- ),
495
- ...(envRootObj?.durable_objects?.bindings || []).reduce(
496
- (obj, { name, class_name, script_name }) => {
497
- return { ...obj, [name]: { class_name, script_name } };
498
- },
499
- {}
890
+ return {
891
+ binding,
892
+ id: preview_id,
893
+ };
894
+ }
500
895
  ),
896
+ vars: config.vars,
897
+ wasm_modules: config.wasm_modules,
898
+ text_blobs: config.text_blobs,
899
+ data_blobs: config.data_blobs,
900
+ durable_objects: config.durable_objects,
901
+ r2_buckets: config.r2_buckets,
902
+ unsafe: config.unsafe?.bindings,
501
903
  }}
904
+ crons={config.triggers.crons}
502
905
  />
503
906
  );
907
+ await waitUntilExit();
504
908
  }
505
909
  );
506
910
 
507
911
  // publish
508
- yargs.command(
912
+ wrangler.command(
509
913
  "publish [script]",
510
914
  "🆙 Publish your Worker to Cloudflare.",
511
915
  (yargs) => {
@@ -513,16 +917,35 @@ export async function main(argv: string[]): Promise<void> {
513
917
  .option("env", {
514
918
  type: "string",
515
919
  describe: "Perform on a specific environment",
920
+ alias: "e",
516
921
  })
517
922
  .positional("script", {
518
- describe: "script to upload",
923
+ describe: "The path to an entry point for your worker",
519
924
  type: "string",
520
925
  })
521
926
  .option("name", {
522
- describe: "name to use when uploading",
927
+ describe: "Name of the worker",
523
928
  type: "string",
524
929
  })
525
- .option("public", {
930
+ .option("format", {
931
+ choices: ["modules", "service-worker"] as const,
932
+ describe: "Choose an entry type",
933
+ })
934
+ .option("compatibility-date", {
935
+ describe: "Date to use for compatibility checks",
936
+ type: "string",
937
+ })
938
+ .option("compatibility-flags", {
939
+ describe: "Flags to use for compatibility checks",
940
+ type: "array",
941
+ alias: "compatibility-flag",
942
+ })
943
+ .option("latest", {
944
+ describe: "Use the latest version of the worker runtime",
945
+ type: "boolean",
946
+ default: false,
947
+ })
948
+ .option("experimental-public", {
526
949
  describe: "Static assets to be served",
527
950
  type: "string",
528
951
  })
@@ -530,22 +953,28 @@ export async function main(argv: string[]): Promise<void> {
530
953
  describe: "Root folder of static assets for Workers Sites",
531
954
  type: "string",
532
955
  })
956
+ .option("site-include", {
957
+ describe:
958
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.",
959
+ type: "string",
960
+ array: true,
961
+ })
962
+ .option("site-exclude", {
963
+ describe:
964
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.",
965
+ type: "string",
966
+ array: true,
967
+ })
533
968
  .option("triggers", {
534
969
  describe: "cron schedules to attach",
535
970
  alias: ["schedule", "schedules"],
536
971
  type: "array",
537
972
  })
538
973
  .option("routes", {
539
- describe: "routes to upload",
974
+ describe: "Routes to upload",
540
975
  alias: "route",
541
976
  type: "array",
542
977
  })
543
- .option("services", {
544
- describe: "experimental support for services",
545
- type: "boolean",
546
- default: "false",
547
- hidden: true,
548
- })
549
978
  .option("jsx-factory", {
550
979
  describe: "The function that is called for each JSX element",
551
980
  type: "string",
@@ -554,80 +983,88 @@ export async function main(argv: string[]): Promise<void> {
554
983
  describe: "The function that is called for each JSX fragment",
555
984
  type: "string",
556
985
  })
557
- .option("polyfill-node", {
558
- describe: "Try to polyfill node builtins",
559
- type: "boolean",
986
+ .option("tsconfig", {
987
+ describe: "Path to a custom tsconfig.json file",
988
+ type: "string",
560
989
  });
561
990
  },
562
991
  async (args) => {
563
- if (args.local) {
564
- console.error("🚫 Local publishing is not yet supported");
565
- return;
992
+ printWranglerBanner();
993
+ if (args["experimental-public"]) {
994
+ console.warn(
995
+ "🚨 The --experimental-public field is experimental and will change in the future."
996
+ );
566
997
  }
567
- const config = args.config as Config;
568
-
569
- // -- snip, extract --
570
- if (!args.local) {
571
- const loggedIn = await loginOrRefreshIfRequired();
572
- if (!loggedIn) {
573
- // didn't login, let's just quit
574
- console.log("Did not login, quitting...");
575
- return;
576
- }
577
- if (!config.account_id) {
578
- config.account_id = await getAccountId();
579
- if (!config.account_id) {
580
- console.error("No account id found, quitting...");
581
- return;
582
- }
583
- }
998
+ if (args.public) {
999
+ throw new Error(
1000
+ "🚨 The --public field has been renamed to --experimental-public, and will change behaviour in the future."
1001
+ );
584
1002
  }
585
1003
 
586
- // -- snip, end --
1004
+ const configPath =
1005
+ (args.config as ConfigPath) ||
1006
+ (args.script && findWranglerToml(path.dirname(args.script)));
1007
+ const config = readConfig(configPath, args);
1008
+ const entry = await getEntry(args, config, "publish");
587
1009
 
588
- const polyfillNode = args["polyfill-node"] ?? config.polyfill_node;
589
- if (polyfillNode) {
1010
+ if (args.latest) {
590
1011
  console.warn(
591
- "Using polyfills for node builtin modules. These are not meant to be completely reliable, and have serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
1012
+ "⚠️ 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"
592
1013
  );
593
1014
  }
594
1015
 
1016
+ const accountId = await requireAuth(config);
1017
+
1018
+ const assetPaths = getAssetPaths(
1019
+ config,
1020
+ args["experimental-public"] || args.site,
1021
+ args.siteInclude,
1022
+ args.siteExclude
1023
+ );
595
1024
  await publish({
596
- config: args.config as Config,
597
- name: args.name,
598
- script: args.script,
1025
+ config,
1026
+ accountId,
1027
+ name: getScriptName(args, config),
1028
+ rules: getRules(config),
1029
+ entry,
599
1030
  env: args.env,
1031
+ compatibilityDate: args.latest
1032
+ ? new Date().toISOString().substring(0, 10)
1033
+ : args["compatibility-date"],
1034
+ compatibilityFlags: args["compatibility-flags"] as string[],
600
1035
  triggers: args.triggers,
601
1036
  jsxFactory: args["jsx-factory"],
602
1037
  jsxFragment: args["jsx-fragment"],
603
- polyfillNode: args["polyfill-node"],
1038
+ tsconfig: args.tsconfig,
604
1039
  routes: args.routes,
605
- public: args.public,
606
- site: args.site,
1040
+ assetPaths,
1041
+ legacyEnv: isLegacyEnv(args, config),
1042
+ experimentalPublic: args["experimental-public"] !== undefined,
607
1043
  });
608
1044
  }
609
1045
  );
610
1046
 
611
1047
  // tail
612
- yargs.command(
1048
+ wrangler.command(
613
1049
  "tail [name]",
614
- "🦚 Starts a log tailing session for a deployed Worker.",
1050
+ "🦚 Starts a log tailing session for a published Worker.",
615
1051
  (yargs) => {
616
1052
  return (
617
1053
  yargs
618
1054
  .positional("name", {
619
- describe: "name of the worker",
1055
+ describe: "Name of the worker",
620
1056
  type: "string",
621
1057
  })
622
1058
  // TODO: auto-detect if this should be json or pretty based on atty
623
1059
  .option("format", {
624
- default: "json",
1060
+ default: "pretty",
625
1061
  choices: ["json", "pretty"],
626
1062
  describe: "The format of log entries",
627
1063
  })
628
1064
  .option("status", {
629
1065
  choices: ["ok", "error", "canceled"],
630
1066
  describe: "Filter by invocation status",
1067
+ array: true,
631
1068
  })
632
1069
  .option("header", {
633
1070
  type: "string",
@@ -636,6 +1073,7 @@ export async function main(argv: string[]): Promise<void> {
636
1073
  .option("method", {
637
1074
  type: "string",
638
1075
  describe: "Filter by HTTP method",
1076
+ array: true,
639
1077
  })
640
1078
  .option("sampling-rate", {
641
1079
  type: "number",
@@ -645,102 +1083,116 @@ export async function main(argv: string[]): Promise<void> {
645
1083
  type: "string",
646
1084
  describe: "Filter by a text match in console.log messages",
647
1085
  })
1086
+ .option("ip", {
1087
+ type: "string",
1088
+ describe:
1089
+ 'Filter by the IP address the request originates from. Use "self" to filter for your own IP',
1090
+ array: true,
1091
+ })
648
1092
  .option("env", {
649
1093
  type: "string",
650
1094
  describe: "Perform on a specific environment",
1095
+ alias: "e",
1096
+ })
1097
+ .option("debug", {
1098
+ type: "boolean",
1099
+ hidden: true,
1100
+ default: false,
1101
+ describe:
1102
+ "If a log would have been filtered out, send it through anyway alongside the filter which would have blocked it.",
651
1103
  })
652
1104
  );
653
- // TODO: filter by client ip, which can be 'self' or an ip address
654
1105
  },
655
1106
  async (args) => {
656
- const config = args.config as Config;
657
-
658
- if (!(args.name || config.name)) {
659
- console.error("Missing script name");
660
- return;
1107
+ if (args.format === "pretty") {
1108
+ printWranglerBanner();
661
1109
  }
662
- const scriptName = `${args.name || config.name}${
663
- args.env ? `-${args.env}` : ""
664
- }`;
1110
+ const config = readConfig(args.config as ConfigPath, args);
665
1111
 
666
- // -- snip, extract --
1112
+ const scriptName = getLegacyScriptName(args, config);
667
1113
 
668
- if (!args.local) {
669
- const loggedIn = await loginOrRefreshIfRequired();
670
- if (!loggedIn) {
671
- // didn't login, let's just quit
672
- console.log("Did not login, quitting...");
673
- return;
674
- }
675
- if (!config.account_id) {
676
- config.account_id = await getAccountId();
677
- if (!config.account_id) {
678
- console.error("No account id found, quitting...");
679
- return;
680
- }
681
- }
1114
+ if (!scriptName) {
1115
+ throw new Error("Missing script name");
682
1116
  }
683
1117
 
684
- // -- snip, end --
685
-
686
- const accountId = config.account_id;
1118
+ const accountId = await requireAuth(config);
687
1119
 
688
- const filters = {
689
- status: args.status as "ok" | "error" | "canceled",
1120
+ const cliFilters: TailCLIFilters = {
1121
+ status: args.status as ("ok" | "error" | "canceled")[],
690
1122
  header: args.header,
691
1123
  method: args.method,
692
- "sampling-rate": args["sampling-rate"],
1124
+ samplingRate: args["sampling-rate"],
693
1125
  search: args.search,
1126
+ clientIp: args.ip,
694
1127
  };
695
1128
 
696
- const { tail, expiration, /* sendHeartbeat, */ deleteTail } =
697
- await createTail(accountId, scriptName, filters);
1129
+ const filters = translateCLICommandToFilterMessage(
1130
+ cliFilters,
1131
+ args.debug
1132
+ );
1133
+
1134
+ const { tail, expiration, deleteTail } = await createTail(
1135
+ accountId,
1136
+ scriptName,
1137
+ filters,
1138
+ !isLegacyEnv(args, config) ? args.env : undefined
1139
+ );
1140
+
1141
+ const scriptDisplayName = `${scriptName}${
1142
+ args.env && !isLegacyEnv(args, config) ? ` (${args.env})` : ""
1143
+ }`;
698
1144
 
699
1145
  console.log(
700
1146
  `successfully created tail, expires at ${expiration.toLocaleString()}`
701
1147
  );
702
1148
 
703
- onExit(() => {
1149
+ onExit(async () => {
704
1150
  tail.terminate();
705
- deleteTail();
1151
+ await deleteTail();
706
1152
  });
707
1153
 
708
- tail.on("message", (data) => {
709
- console.log(JSON.stringify(JSON.parse(data.toString()), null, " "));
710
- });
1154
+ const printLog: (data: RawData) => void =
1155
+ args.format === "pretty" ? prettyPrintLogs : jsonPrintLogs;
1156
+
1157
+ tail.on("message", printLog);
711
1158
 
712
1159
  while (tail.readyState !== tail.OPEN) {
713
1160
  switch (tail.readyState) {
714
1161
  case tail.CONNECTING:
715
- await setTimeout(1000);
1162
+ await setTimeout(100);
716
1163
  break;
717
1164
  case tail.CLOSING:
718
- await setTimeout(1000);
1165
+ await setTimeout(100);
719
1166
  break;
720
1167
  case tail.CLOSED:
721
- process.exit(1);
1168
+ throw new Error(
1169
+ `Connection to ${scriptDisplayName} closed unexpectedly.`
1170
+ );
722
1171
  }
723
1172
  }
724
1173
 
725
- console.log(`Connected to ${scriptName}, waiting for logs...`);
1174
+ console.log(`Connected to ${scriptDisplayName}, waiting for logs...`);
1175
+
1176
+ tail.on("close", async () => {
1177
+ tail.terminate();
1178
+ await deleteTail();
1179
+ });
726
1180
  }
727
1181
  );
728
1182
 
729
1183
  // preview
730
- yargs.command(
1184
+ wrangler.command(
731
1185
  "preview [method] [body]",
732
1186
  false,
733
1187
  (yargs) => {
734
1188
  return yargs
735
1189
  .positional("method", {
1190
+ type: "string",
736
1191
  describe: "Type of request to preview your worker",
737
- choices: ["GET", "POST"],
738
- default: ["GET"],
739
1192
  })
740
1193
  .positional("body", {
741
1194
  type: "string",
742
1195
  describe: "Body string to post to your preview worker request.",
743
- default: "Null",
744
1196
  })
745
1197
  .option("env", {
746
1198
  type: "string",
@@ -752,24 +1204,102 @@ export async function main(argv: string[]): Promise<void> {
752
1204
  type: "boolean",
753
1205
  });
754
1206
  },
755
- () => {
756
- // "🔬 [DEPRECATED] Preview your code temporarily on cloudflareworkers.com"
757
- console.error(
758
- "`wrangler preview` has been deprecated, please refer to TODO://some/path for alternatives"
1207
+ async (args) => {
1208
+ if (args.method || args.body) {
1209
+ throw new DeprecationError(
1210
+ "The `wrangler preview` command has been deprecated.\n" +
1211
+ "Try using `wrangler dev` to to try out a worker during development.\n"
1212
+ );
1213
+ }
1214
+
1215
+ // Delegate to `wrangler dev`
1216
+ console.warn(
1217
+ "***************************************************\n" +
1218
+ "The `wrangler preview` command has been deprecated.\n" +
1219
+ "Attempting to run `wrangler dev` instead.\n" +
1220
+ "***************************************************\n"
1221
+ );
1222
+
1223
+ const config = readConfig(args.config as ConfigPath, args);
1224
+ const entry = await getEntry({}, config, "dev");
1225
+
1226
+ const accountId = await requireAuth(config);
1227
+
1228
+ const { waitUntilExit } = render(
1229
+ <Dev
1230
+ name={config.name}
1231
+ entry={entry}
1232
+ rules={getRules(config)}
1233
+ env={args.env}
1234
+ zone={undefined}
1235
+ legacyEnv={isLegacyEnv(args, config)}
1236
+ build={config.build || {}}
1237
+ initialMode={args.local ? "local" : "remote"}
1238
+ jsxFactory={config.jsx_factory}
1239
+ jsxFragment={config.jsx_fragment}
1240
+ tsconfig={config.tsconfig}
1241
+ upstreamProtocol={config.dev.upstream_protocol}
1242
+ localProtocol={config.dev.local_protocol}
1243
+ enableLocalPersistence={false}
1244
+ accountId={accountId}
1245
+ assetPaths={undefined}
1246
+ port={config.dev?.port}
1247
+ ip={config.dev.ip}
1248
+ public={undefined}
1249
+ compatibilityDate={getDevCompatibilityDate(config)}
1250
+ compatibilityFlags={
1251
+ (args["compatibility-flags"] as string[]) ||
1252
+ config.compatibility_flags
1253
+ }
1254
+ usageModel={config.usage_model}
1255
+ bindings={{
1256
+ kv_namespaces: config.kv_namespaces?.map(
1257
+ ({ binding, preview_id, id: _id }) => {
1258
+ // In `dev`, we make folks use a separate kv namespace called
1259
+ // `preview_id` instead of `id` so that they don't
1260
+ // break production data. So here we check that a `preview_id`
1261
+ // has actually been configured.
1262
+ // This whole block of code will be obsoleted in the future
1263
+ // when we have copy-on-write for previews on edge workers.
1264
+ if (!preview_id) {
1265
+ // TODO: This error has to be a _lot_ better, ideally just asking
1266
+ // to create a preview namespace for the user automatically
1267
+ throw new Error(
1268
+ `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`
1269
+ ); // Ugh, I really don't like this message very much
1270
+ }
1271
+ return {
1272
+ binding,
1273
+ id: preview_id,
1274
+ };
1275
+ }
1276
+ ),
1277
+ vars: config.vars,
1278
+ wasm_modules: config.wasm_modules,
1279
+ text_blobs: config.text_blobs,
1280
+ data_blobs: config.data_blobs,
1281
+ durable_objects: config.durable_objects,
1282
+ r2_buckets: config.r2_buckets,
1283
+ unsafe: config.unsafe?.bindings,
1284
+ }}
1285
+ crons={config.triggers.crons}
1286
+ inspectorPort={await getPort({ port: 9229 })}
1287
+ />
759
1288
  );
1289
+ await waitUntilExit();
760
1290
  }
761
1291
  );
762
1292
 
763
- // route
764
- yargs.command(
1293
+ // [DEPRECATED] route
1294
+ wrangler.command(
765
1295
  "route",
766
1296
  false, // I think we want to hide this command
767
1297
  // "➡️ List or delete worker routes",
768
- (yargs) => {
769
- return yargs
1298
+ (routeYargs) => {
1299
+ return routeYargs
770
1300
  .command(
771
1301
  "list",
772
- "List a route associated with a zone",
1302
+ "List the routes associated with a zone",
773
1303
  (yargs) => {
774
1304
  return yargs
775
1305
  .option("env", {
@@ -778,27 +1308,28 @@ export async function main(argv: string[]): Promise<void> {
778
1308
  })
779
1309
  .option("zone", {
780
1310
  type: "string",
781
- describe: "zone id",
1311
+ describe: "Zone id",
782
1312
  })
783
1313
  .positional("zone", {
784
- describe: "zone id",
1314
+ describe: "Zone id",
785
1315
  type: "string",
786
1316
  });
787
1317
  },
788
- async (args) => {
789
- console.log(":route list", args);
790
- // TODO: use environment (current wrangler doesn't do so?)
791
- const zone = args.zone || (args.config as Config).zone_id;
792
- if (!zone) {
793
- console.error("missing zone id");
794
- return;
795
- }
796
-
797
- console.log(await cfetch(`/zones/${zone}/workers/routes`));
1318
+ () => {
1319
+ // "👯 [DEPRECATED]. Use wrangler.toml to manage routes.
1320
+ const deprecationNotice =
1321
+ "`wrangler route list` has been deprecated.";
1322
+ const futureRoutes =
1323
+ "Refer to wrangler.toml for a list of routes the worker will be deployed to upon publishing.";
1324
+ const presentRoutes =
1325
+ "Refer to the Cloudflare Dashboard to see the routes this worker is currently running on.";
1326
+ throw new DeprecationError(
1327
+ `${deprecationNotice}\n${futureRoutes}\n${presentRoutes}`
1328
+ );
798
1329
  }
799
1330
  )
800
1331
  .command(
801
- "delete <id>",
1332
+ "delete [id]",
802
1333
  "Delete a route associated with a zone",
803
1334
  (yargs) => {
804
1335
  return yargs
@@ -815,26 +1346,27 @@ export async function main(argv: string[]): Promise<void> {
815
1346
  describe: "Perform on a specific environment",
816
1347
  });
817
1348
  },
818
- async (args) => {
819
- console.log(":route delete", args);
820
- // TODO: use environment (current wrangler doesn't do so?)
821
- const zone = args.zone || (args.config as Config).zone_id;
822
- if (!zone) {
823
- throw new Error("missing zone id");
824
- }
825
-
826
- console.log(
827
- await cfetch(`/zones/${zone}/workers/routes/${args.id}`, {
828
- method: "DELETE",
829
- })
830
- );
1349
+ () => {
1350
+ // "👯 [DEPRECATED]. Use wrangler.toml to manage routes.
1351
+ const deprecationNotice =
1352
+ "`wrangler route delete` has been deprecated.";
1353
+ const shouldDo =
1354
+ "Remove the unwanted route(s) from wrangler.toml and run `wrangler publish` to remove your worker from those routes.";
1355
+ throw new DeprecationError(`${deprecationNotice}\n${shouldDo}`);
831
1356
  }
832
1357
  );
1358
+ },
1359
+ () => {
1360
+ // "👯 [DEPRECATED]. Use wrangler.toml to manage routes.
1361
+ const deprecationNotice = "`wrangler route` has been deprecated.";
1362
+ const shouldDo =
1363
+ "Please use wrangler.toml and/or `wrangler publish --routes` to modify routes";
1364
+ throw new DeprecationError(`${deprecationNotice}\n${shouldDo}`);
833
1365
  }
834
1366
  );
835
1367
 
836
1368
  // subdomain
837
- yargs.command(
1369
+ wrangler.command(
838
1370
  "subdomain [name]",
839
1371
  false,
840
1372
  // "👷 Create or change your workers.dev subdomain.",
@@ -842,182 +1374,190 @@ export async function main(argv: string[]): Promise<void> {
842
1374
  return yargs.positional("name", { type: "string" });
843
1375
  },
844
1376
  () => {
845
- console.error(
846
- "`wrangler subdomain` has been deprecated, please refer to TODO://some/path for alternatives"
1377
+ throw new DeprecationError(
1378
+ "`wrangler subdomain` has been deprecated, please refer to https://github.com/cloudflare/wrangler2/blob/main/docs/deprecations.md#subdomain for alternatives"
847
1379
  );
848
1380
  }
849
1381
  );
850
1382
 
851
1383
  // secret
852
- yargs.command(
1384
+ wrangler.command(
853
1385
  "secret",
854
1386
  "🤫 Generate a secret that can be referenced in the worker script",
855
- (yargs) => {
856
- return yargs
1387
+ (secretYargs) => {
1388
+ return secretYargs
1389
+ .command(subHelp)
857
1390
  .command(
858
1391
  "put <key>",
859
1392
  "Create or update a secret variable for a script",
860
1393
  (yargs) => {
861
1394
  return yargs
862
1395
  .positional("key", {
863
- describe: "The variable name to be accessible in the script.",
1396
+ describe: "The variable name to be accessible in the script",
864
1397
  type: "string",
865
1398
  })
866
1399
  .option("name", {
867
- describe: "name of the script",
1400
+ describe: "Name of the worker",
868
1401
  type: "string",
869
1402
  })
870
1403
  .option("env", {
871
1404
  type: "string",
872
1405
  describe:
873
- "Binds the secret to the script of the specific environment.",
1406
+ "Binds the secret to the Worker of the specific environment",
1407
+ alias: "e",
874
1408
  });
875
1409
  },
876
1410
  async (args) => {
877
- if (args.local) {
878
- console.error("--local not implemented for this command yet");
879
- return;
880
- }
881
- const config = args.config as Config;
1411
+ printWranglerBanner();
1412
+ const config = readConfig(args.config as ConfigPath, args);
882
1413
 
883
- // TODO: use environment (how does current wrangler do it?)
884
- const scriptName = args.name || config.name;
1414
+ const scriptName = getLegacyScriptName(args, config);
885
1415
  if (!scriptName) {
886
- console.error("Missing script name");
887
- return;
1416
+ throw new Error("Missing script name");
888
1417
  }
889
1418
 
890
- // -- snip, extract --
1419
+ const isInteractive = process.stdin.isTTY;
1420
+ const accountId = await requireAuth(config, isInteractive);
891
1421
 
892
- if (!args.local) {
893
- const loggedIn = await loginOrRefreshIfRequired();
894
- if (!loggedIn) {
895
- // didn't login, let's just quit
896
- console.log("Did not login, quitting...");
897
- return;
898
- }
899
- if (!config.account_id) {
900
- config.account_id = await getAccountId();
901
- if (!config.account_id) {
902
- console.error("No account id found, quitting...");
903
- return;
904
- }
905
- }
906
- }
907
-
908
- // -- snip, end --
1422
+ const secretValue = isInteractive
1423
+ ? await prompt("Enter a secret value:", "password")
1424
+ : await readFromStdin();
909
1425
 
910
- const secretValue = await prompt(
911
- "Enter a secret value:",
912
- "password"
1426
+ console.log(
1427
+ `🌀 Creating the secret for script ${scriptName} ${
1428
+ args.env && !isLegacyEnv(args, config) ? `(${args.env})` : ""
1429
+ }`
913
1430
  );
1431
+
914
1432
  async function submitSecret() {
915
- return await cfetch(
916
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets/`,
1433
+ const url =
1434
+ !args.env || isLegacyEnv(args, config)
1435
+ ? `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`
1436
+ : `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1437
+
1438
+ return await fetchResult(url, {
1439
+ method: "PUT",
1440
+ headers: { "Content-Type": "application/json" },
1441
+ body: JSON.stringify({
1442
+ name: args.key,
1443
+ text: secretValue,
1444
+ type: "secret_text",
1445
+ }),
1446
+ });
1447
+ }
1448
+
1449
+ const createDraftWorker = async () => {
1450
+ // TODO: log a warning
1451
+ await fetchResult(
1452
+ !args["legacy-env"] && args.env
1453
+ ? `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}`
1454
+ : `/accounts/${accountId}/workers/scripts/${scriptName}`,
917
1455
  {
918
1456
  method: "PUT",
919
- headers: { "Content-Type": "application/json" },
920
- body: JSON.stringify({
921
- name: args.key,
922
- text: secretValue,
923
- type: "secret_text",
1457
+ body: createWorkerUploadForm({
1458
+ name: scriptName,
1459
+ main: {
1460
+ name: scriptName,
1461
+ content: `export default { fetch() {} }`,
1462
+ type: "esm",
1463
+ },
1464
+ bindings: {
1465
+ kv_namespaces: [],
1466
+ vars: {},
1467
+ durable_objects: { bindings: [] },
1468
+ r2_buckets: [],
1469
+ wasm_modules: {},
1470
+ text_blobs: {},
1471
+ data_blobs: {},
1472
+ unsafe: [],
1473
+ },
1474
+ modules: [],
1475
+ migrations: undefined,
1476
+ compatibility_date: undefined,
1477
+ compatibility_flags: undefined,
1478
+ usage_model: undefined,
924
1479
  }),
925
1480
  }
926
1481
  );
1482
+ };
1483
+
1484
+ function isMissingWorkerError(e: unknown): e is { code: 10007 } {
1485
+ return (
1486
+ typeof e === "object" &&
1487
+ e !== null &&
1488
+ (e as { code: 10007 }).code === 10007
1489
+ );
927
1490
  }
928
1491
 
929
1492
  try {
930
- console.log(await submitSecret());
1493
+ await submitSecret();
931
1494
  } catch (e) {
932
- if (e.code === 10007) {
933
- // upload a draft worker
934
- await cfetch(
935
- `/accounts/${config.account_id}/workers/scripts/${scriptName}`,
936
- {
937
- method: "PUT",
938
- // @ts-expect-error TODO: fix this error!
939
- body: toFormData({
940
- main: {
941
- name: scriptName,
942
- content: `export default { fetch() {} }`,
943
- type: "esm",
944
- },
945
- variables: {},
946
- modules: [],
947
- }),
948
- }
949
- );
950
-
951
- // and then try again
952
- console.log(await submitSecret());
1495
+ if (isMissingWorkerError(e)) {
1496
+ // create a draft worker and try again
1497
+ await createDraftWorker();
1498
+ await submitSecret();
953
1499
  // TODO: delete the draft worker if this failed too?
1500
+ } else {
1501
+ throw e;
954
1502
  }
955
1503
  }
1504
+
1505
+ console.log(`✨ Success! Uploaded secret ${args.key}`);
956
1506
  }
957
1507
  )
958
1508
  .command(
959
1509
  "delete <key>",
960
1510
  "Delete a secret variable from a script",
961
1511
  (yargs) => {
1512
+ printWranglerBanner();
962
1513
  return yargs
963
1514
  .positional("key", {
964
- describe: "The variable name to be accessible in the script.",
1515
+ describe: "The variable name to be accessible in the script",
965
1516
  type: "string",
966
1517
  })
967
1518
  .option("name", {
968
- describe: "name of the script",
1519
+ describe: "Name of the worker",
969
1520
  type: "string",
970
1521
  })
971
1522
  .option("env", {
972
1523
  type: "string",
973
1524
  describe:
974
- "Binds the secret to the script of the specific environment.",
1525
+ "Binds the secret to the Worker of the specific environment",
1526
+ alias: "e",
975
1527
  });
976
1528
  },
977
1529
  async (args) => {
978
- if (args.local) {
979
- console.error("--local not implemented for this command yet");
980
- return;
981
- }
982
- const config = args.config as Config;
1530
+ const config = readConfig(args.config as ConfigPath, args);
983
1531
 
984
- // TODO: use environment (how does current wrangler do it?)
985
- const scriptName = args.name || config.name;
1532
+ const scriptName = getLegacyScriptName(args, config);
986
1533
  if (!scriptName) {
987
1534
  throw new Error("Missing script name");
988
1535
  }
989
1536
 
990
- // -- snip, extract --
991
-
992
- if (!args.local) {
993
- const loggedIn = await loginOrRefreshIfRequired();
994
- if (!loggedIn) {
995
- // didn't login, let's just quit
996
- console.log("Did not login, quitting...");
997
- return;
998
- }
999
- if (!config.account_id) {
1000
- config.account_id = await getAccountId();
1001
- if (!config.account_id) {
1002
- console.error("No account id found, quitting...");
1003
- return;
1004
- }
1005
- }
1006
- }
1007
-
1008
- // -- snip, end --
1537
+ const accountId = await requireAuth(config);
1009
1538
 
1010
- if (await confirm("Are you sure you want to delete this secret?")) {
1539
+ if (
1540
+ await confirm(
1541
+ `Are you sure you want to permanently delete the variable ${
1542
+ args.key
1543
+ } on the script ${scriptName}${
1544
+ args.env && !isLegacyEnv(args, config) ? ` (${args.env})` : ""
1545
+ }?`
1546
+ )
1547
+ ) {
1011
1548
  console.log(
1012
- `Deleting the secret ${args.key} on script ${scriptName}.`
1549
+ `🌀 Deleting the secret ${args.key} on script ${scriptName}${
1550
+ args.env && !isLegacyEnv(args, config) ? ` (${args.env})` : ""
1551
+ }`
1013
1552
  );
1014
1553
 
1015
- console.log(
1016
- await cfetch(
1017
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets/${args.key}`,
1018
- { method: "DELETE" }
1019
- )
1020
- );
1554
+ const url =
1555
+ !args.env || isLegacyEnv(args, config)
1556
+ ? `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`
1557
+ : `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1558
+
1559
+ await fetchResult(`${url}/${args.key}`, { method: "DELETE" });
1560
+ console.log(`✨ Success! Deleted secret ${args.key}`);
1021
1561
  }
1022
1562
  }
1023
1563
  )
@@ -1027,54 +1567,32 @@ export async function main(argv: string[]): Promise<void> {
1027
1567
  (yargs) => {
1028
1568
  return yargs
1029
1569
  .option("name", {
1030
- describe: "name of the script",
1570
+ describe: "Name of the worker",
1031
1571
  type: "string",
1032
1572
  })
1033
1573
  .option("env", {
1034
1574
  type: "string",
1035
1575
  describe:
1036
- "Binds the secret to the script of the specific environment.",
1576
+ "Binds the secret to the Worker of the specific environment.",
1577
+ alias: "e",
1037
1578
  });
1038
1579
  },
1039
1580
  async (args) => {
1040
- if (args.local) {
1041
- console.error("--local not implemented for this command yet");
1042
- return;
1043
- }
1044
- const config = args.config as Config;
1581
+ const config = readConfig(args.config as ConfigPath, args);
1045
1582
 
1046
- // TODO: use environment (how does current wrangler do it?)
1047
- const scriptName = args.name || config.name;
1583
+ const scriptName = getLegacyScriptName(args, config);
1048
1584
  if (!scriptName) {
1049
- console.error("Missing script name");
1050
- return;
1051
- }
1052
-
1053
- // -- snip, extract --
1054
-
1055
- if (!args.local) {
1056
- const loggedIn = await loginOrRefreshIfRequired();
1057
- if (!loggedIn) {
1058
- // didn't login, let's just quit
1059
- console.log("Did not login, quitting...");
1060
- return;
1061
- }
1062
- if (!config.account_id) {
1063
- config.account_id = await getAccountId();
1064
- if (!config.account_id) {
1065
- console.error("No account id found, quitting...");
1066
- return;
1067
- }
1068
- }
1585
+ throw new Error("Missing script name");
1069
1586
  }
1070
1587
 
1071
- // -- snip, end --
1588
+ const accountId = await requireAuth(config);
1072
1589
 
1073
- console.log(
1074
- await cfetch(
1075
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1076
- )
1077
- );
1590
+ const url =
1591
+ !args.env || isLegacyEnv(args, config)
1592
+ ? `/accounts/${accountId}/workers/scripts/${scriptName}/secrets`
1593
+ : `/accounts/${accountId}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1594
+
1595
+ console.log(JSON.stringify(await fetchResult(url), null, " "));
1078
1596
  }
1079
1597
  );
1080
1598
  }
@@ -1082,11 +1600,12 @@ export async function main(argv: string[]): Promise<void> {
1082
1600
 
1083
1601
  // kv
1084
1602
  // :namespace
1085
- yargs.command(
1603
+ wrangler.command(
1086
1604
  "kv:namespace",
1087
1605
  "🗂️ Interact with your Workers KV Namespaces",
1088
- (yargs) => {
1089
- return yargs
1606
+ (namespaceYargs) => {
1607
+ return namespaceYargs
1608
+ .command(subHelp)
1090
1609
  .command(
1091
1610
  "create <namespace>",
1092
1611
  "Create a new namespace",
@@ -1095,10 +1614,12 @@ export async function main(argv: string[]): Promise<void> {
1095
1614
  .positional("namespace", {
1096
1615
  describe: "The name of the new namespace",
1097
1616
  type: "string",
1617
+ demandOption: true,
1098
1618
  })
1099
1619
  .option("env", {
1100
1620
  type: "string",
1101
1621
  describe: "Perform on a specific environment",
1622
+ alias: "e",
1102
1623
  })
1103
1624
  .option("preview", {
1104
1625
  type: "boolean",
@@ -1106,88 +1627,44 @@ export async function main(argv: string[]): Promise<void> {
1106
1627
  });
1107
1628
  },
1108
1629
  async (args) => {
1109
- if (args._.length !== 2) {
1110
- throw new Error(
1111
- `Did you forget to add quotes around "${
1112
- args.namespace
1113
- } ${args._.slice(2).join(" ")}"?`
1630
+ printWranglerBanner();
1631
+
1632
+ if (!isValidNamespaceBinding(args.namespace)) {
1633
+ throw new CommandLineArgsError(
1634
+ `The namespace binding name "${args.namespace}" is invalid. It can only have alphanumeric and _ characters, and cannot begin with a number.`
1114
1635
  );
1115
1636
  }
1116
- const config = args.config as Config;
1637
+
1638
+ const config = readConfig(args.config as ConfigPath, args);
1117
1639
  if (!config.name) {
1118
1640
  console.warn(
1119
1641
  "No configured name present, using `worker` as a prefix for the title"
1120
1642
  );
1121
1643
  }
1122
1644
 
1123
- const title = `${config.name || "worker"}${
1124
- args.env ? `-${args.env}` : ""
1125
- }-${args.namespace}${args.preview ? "_preview" : ""}`;
1126
-
1127
- if (/[\W]+/.test(args.namespace)) {
1128
- throw new Error("invalid binding name, needs to be js friendly");
1129
- }
1130
-
1131
- if (args.local) {
1132
- const { Miniflare } = await import("miniflare");
1133
- const mf = new Miniflare({
1134
- kvPersist: (args.kvPersist as string) || true,
1135
- // TODO: these options shouldn't be required
1136
- script: ` `, // has to be a string with at least one char
1137
- });
1138
- mf.getKVNamespace(title); // this should "create" the namespace
1139
- console.log(`✨ Success! Created KV namespace ${title}`);
1140
- return;
1141
- }
1142
-
1143
- // -- snip, extract --
1645
+ const name = config.name || "worker";
1646
+ const environment = args.env ? `-${args.env}` : "";
1647
+ const preview = args.preview ? "_preview" : "";
1648
+ const title = `${name}${environment}-${args.namespace}${preview}`;
1144
1649
 
1145
- if (!args.local) {
1146
- const loggedIn = await loginOrRefreshIfRequired();
1147
- if (!loggedIn) {
1148
- // didn't login, let's just quit
1149
- console.log("Did not login, quitting...");
1150
- return;
1151
- }
1152
- if (!config.account_id) {
1153
- config.account_id = await getAccountId();
1154
- if (!config.account_id) {
1155
- console.error("No account id found, quitting...");
1156
- return;
1157
- }
1158
- }
1159
- }
1160
-
1161
- // -- snip, end --
1650
+ const accountId = await requireAuth(config);
1162
1651
 
1163
1652
  // TODO: generate a binding name stripping non alphanumeric chars
1164
1653
 
1165
1654
  console.log(`🌀 Creating namespace with title "${title}"`);
1166
-
1167
- const response = await cfetch<{ id: string }>(
1168
- `/accounts/${config.account_id}/storage/kv/namespaces`,
1169
- {
1170
- method: "POST",
1171
- headers: {
1172
- "Content-Type": "application/json",
1173
- },
1174
- body: JSON.stringify({
1175
- title,
1176
- }),
1177
- }
1178
- );
1655
+ const namespaceId = await createNamespace(accountId, title);
1179
1656
 
1180
1657
  console.log("✨ Success!");
1658
+ const envString = args.env ? ` under [env.${args.env}]` : "";
1659
+ const previewString = args.preview ? "preview_" : "";
1181
1660
  console.log(
1182
- `Add the following to your configuration file in your kv_namespaces array${
1183
- args.env ? ` under [env.${args.env}]` : ""
1184
- }:`
1661
+ `Add the following to your configuration file in your kv_namespaces array${envString}:`
1185
1662
  );
1186
1663
  console.log(
1187
- `{ binding = "${args.namespace}", ${
1188
- args.preview ? "preview_" : ""
1189
- }id = "${response.id}" }`
1664
+ `{ binding = "${args.namespace}", ${previewString}id = "${namespaceId}" }`
1190
1665
  );
1666
+
1667
+ // TODO: automatically write this block to the wrangler.toml config file??
1191
1668
  }
1192
1669
  )
1193
1670
  .command(
@@ -1195,41 +1672,14 @@ export async function main(argv: string[]): Promise<void> {
1195
1672
  "Outputs a list of all KV namespaces associated with your account id.",
1196
1673
  {},
1197
1674
  async (args) => {
1198
- if (args.local) {
1199
- console.error(`local mode is not yet supported for this command`);
1200
- return;
1201
- }
1202
-
1203
- const config = args.config as Config;
1675
+ const config = readConfig(args.config as ConfigPath, args);
1204
1676
 
1205
- // -- snip, extract --
1206
-
1207
- if (!args.local) {
1208
- const loggedIn = await loginOrRefreshIfRequired();
1209
- if (!loggedIn) {
1210
- // didn't login, let's just quit
1211
- console.log("Did not login, quitting...");
1212
- return;
1213
- }
1214
- if (!config.account_id) {
1215
- config.account_id = await getAccountId();
1216
- if (!config.account_id) {
1217
- console.error("No account id found, quitting...");
1218
- return;
1219
- }
1220
- }
1221
- }
1222
-
1223
- // -- snip, end --
1677
+ const accountId = await requireAuth(config);
1224
1678
 
1225
1679
  // TODO: we should show bindings if they exist for given ids
1226
1680
 
1227
1681
  console.log(
1228
- JSON.stringify(
1229
- await listNamespaces(config.account_id),
1230
- null,
1231
- " "
1232
- )
1682
+ JSON.stringify(await listNamespaces(accountId), null, " ")
1233
1683
  );
1234
1684
  }
1235
1685
  )
@@ -1250,6 +1700,7 @@ export async function main(argv: string[]): Promise<void> {
1250
1700
  .option("env", {
1251
1701
  type: "string",
1252
1702
  describe: "Perform on a specific environment",
1703
+ alias: "e",
1253
1704
  })
1254
1705
  .option("preview", {
1255
1706
  type: "boolean",
@@ -1257,47 +1708,22 @@ export async function main(argv: string[]): Promise<void> {
1257
1708
  });
1258
1709
  },
1259
1710
  async (args) => {
1260
- if (args.local) {
1261
- console.error(`local mode is not yet supported for this command`);
1262
- return;
1263
- }
1264
- const config = args.config as Config;
1265
-
1266
- const id =
1267
- args["namespace-id"] ||
1268
- (args.env
1269
- ? config.env[args.env] || {}
1270
- : config
1271
- ).kv_namespaces.find(
1272
- (namespace) => namespace.binding === args.binding
1273
- )[args.preview ? "preview_id" : "id"];
1274
- if (!id) {
1275
- throw new Error("Are you sure? id not found");
1276
- }
1277
-
1278
- // -- snip, extract --
1279
-
1280
- if (!args.local) {
1281
- const loggedIn = await loginOrRefreshIfRequired();
1282
- if (!loggedIn) {
1283
- // didn't login, let's just quit
1284
- console.log("Did not login, quitting...");
1285
- return;
1286
- }
1711
+ printWranglerBanner();
1712
+ const config = readConfig(args.config as ConfigPath, args);
1287
1713
 
1288
- if (!config.account_id) {
1289
- config.account_id = await getAccountId();
1290
- if (!config.account_id) {
1291
- console.error("No account id found, quitting...");
1292
- return;
1293
- }
1294
- }
1714
+ let id;
1715
+ try {
1716
+ id = getNamespaceId(args, config);
1717
+ } catch (e) {
1718
+ throw new CommandLineArgsError(
1719
+ "Not able to delete namespace.\n" + ((e as Error).message ?? e)
1720
+ );
1295
1721
  }
1296
1722
 
1297
- // -- snip, end --
1723
+ const accountId = await requireAuth(config);
1298
1724
 
1299
- await cfetch<{ id: string }>(
1300
- `/accounts/${config.account_id}/storage/kv/namespaces/${id}`,
1725
+ await fetchResult<{ id: string }>(
1726
+ `/accounts/${accountId}/storage/kv/namespaces/${id}`,
1301
1727
  { method: "DELETE" }
1302
1728
  );
1303
1729
 
@@ -1324,36 +1750,39 @@ export async function main(argv: string[]): Promise<void> {
1324
1750
  );
1325
1751
 
1326
1752
  // :key
1327
- yargs.command(
1753
+ wrangler.command(
1328
1754
  "kv:key",
1329
1755
  "🔑 Individually manage Workers KV key-value pairs",
1330
- (yargs) => {
1331
- return yargs
1756
+ (kvKeyYargs) => {
1757
+ return kvKeyYargs
1758
+ .command(subHelp)
1332
1759
  .command(
1333
- "put <key> <value>",
1760
+ "put <key> [value]",
1334
1761
  "Writes a single key/value pair to the given namespace.",
1335
1762
  (yargs) => {
1336
1763
  return yargs
1337
1764
  .positional("key", {
1338
1765
  type: "string",
1339
- describe: "The key to write to.",
1766
+ describe: "The key to write to",
1767
+ demandOption: true,
1340
1768
  })
1341
1769
  .positional("value", {
1342
1770
  type: "string",
1343
- describe: "The value to write.",
1771
+ describe: "The value to write",
1344
1772
  })
1345
1773
  .option("binding", {
1346
1774
  type: "string",
1347
- describe: "The binding of the namespace to write to.",
1775
+ describe: "The binding of the namespace to write to",
1348
1776
  })
1349
1777
  .option("namespace-id", {
1350
1778
  type: "string",
1351
- describe: "The id of the namespace to write to.",
1779
+ describe: "The id of the namespace to write to",
1352
1780
  })
1353
1781
  .check(demandOneOfOption("binding", "namespace-id"))
1354
1782
  .option("env", {
1355
1783
  type: "string",
1356
1784
  describe: "Perform on a specific environment",
1785
+ alias: "e",
1357
1786
  })
1358
1787
  .option("preview", {
1359
1788
  type: "boolean",
@@ -1361,7 +1790,7 @@ export async function main(argv: string[]): Promise<void> {
1361
1790
  })
1362
1791
  .option("ttl", {
1363
1792
  type: "number",
1364
- describe: "Time for which the entries should be visible.",
1793
+ describe: "Time for which the entries should be visible",
1365
1794
  })
1366
1795
  .option("expiration", {
1367
1796
  type: "number",
@@ -1370,50 +1799,35 @@ export async function main(argv: string[]): Promise<void> {
1370
1799
  })
1371
1800
  .option("path", {
1372
1801
  type: "string",
1373
- describe: "Read value from the file at a given path.",
1374
- });
1802
+ describe: "Read value from the file at a given path",
1803
+ })
1804
+ .check(demandOneOfOption("value", "path"));
1375
1805
  },
1376
1806
  async ({ key, ttl, expiration, ...args }) => {
1377
- const namespaceId = getNamespaceId(args);
1807
+ printWranglerBanner();
1808
+ const config = readConfig(args.config as ConfigPath, args);
1809
+ const namespaceId = getNamespaceId(args, config);
1810
+ // One of `args.path` and `args.value` must be defined
1378
1811
  const value = args.path
1379
- ? await readFile(args.path, "utf-8")
1380
- : args.value;
1381
- const config = args.config as Config;
1382
-
1383
- console.log(`writing ${key}=${value} to namespace ${namespaceId}`);
1384
- if (args.local) {
1385
- const { Miniflare } = await import("miniflare");
1386
- const mf = new Miniflare({
1387
- kvPersist: (args.kvPersist as string) || true,
1388
- // TODO: these options shouldn't be required
1389
- script: ` `, // has to be a string with at least one char
1390
- });
1391
- const ns = await mf.getKVNamespace(namespaceId);
1392
- await ns.put(key, value, { expiration, expirationTtl: ttl });
1393
- return;
1394
- }
1395
- // -- snip, extract --
1396
-
1397
- if (!args.local) {
1398
- const loggedIn = await loginOrRefreshIfRequired();
1399
- if (!loggedIn) {
1400
- // didn't login, let's just quit
1401
- console.log("Did not login, quitting...");
1402
- return;
1403
- }
1812
+ ? readFileSync(args.path)
1813
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1814
+ args.value!;
1404
1815
 
1405
- if (!config.account_id) {
1406
- config.account_id = await getAccountId();
1407
- if (!config.account_id) {
1408
- console.error("No account id found, quitting...");
1409
- return;
1410
- }
1411
- }
1816
+ if (args.path) {
1817
+ console.log(
1818
+ `writing the contents of ${args.path} to the key "${key}" on namespace ${namespaceId}`
1819
+ );
1820
+ } else {
1821
+ console.log(
1822
+ `writing the value "${value}" to key "${key}" on namespace ${namespaceId}`
1823
+ );
1412
1824
  }
1413
1825
 
1414
- // -- snip, end --
1826
+ const accountId = await requireAuth(config);
1415
1827
 
1416
- await putKeyValue(config.account_id, namespaceId, key, value, {
1828
+ await putKeyValue(accountId, namespaceId, {
1829
+ key,
1830
+ value,
1417
1831
  expiration,
1418
1832
  expiration_ttl: ttl,
1419
1833
  });
@@ -1436,6 +1850,13 @@ export async function main(argv: string[]): Promise<void> {
1436
1850
  .option("env", {
1437
1851
  type: "string",
1438
1852
  describe: "Perform on a specific environment",
1853
+ alias: "e",
1854
+ })
1855
+ .option("preview", {
1856
+ type: "boolean",
1857
+ // In the case of listing keys we will default to non-preview mode
1858
+ default: false,
1859
+ describe: "Interact with a preview namespace",
1439
1860
  })
1440
1861
  .option("prefix", {
1441
1862
  type: "string",
@@ -1444,47 +1865,17 @@ export async function main(argv: string[]): Promise<void> {
1444
1865
  },
1445
1866
  async ({ prefix, ...args }) => {
1446
1867
  // TODO: support for limit+cursor (pagination)
1868
+ const config = readConfig(args.config as ConfigPath, args);
1869
+ const namespaceId = getNamespaceId(args, config);
1447
1870
 
1448
- const namespaceId = getNamespaceId(args);
1449
- const config = args.config as Config;
1450
-
1451
- if (args.local) {
1452
- const { Miniflare } = await import("miniflare");
1453
- const mf = new Miniflare({
1454
- kvPersist: (args.kvPersist as string) || true,
1455
- // TODO: these options shouldn't be required
1456
- script: ` `, // has to be a string with at least one char
1457
- });
1458
- const ns = await mf.getKVNamespace(namespaceId);
1459
- const listResponse = await ns.list({ prefix });
1460
- console.log(JSON.stringify(listResponse.keys, null, " ")); // TODO: paginate, collate
1461
- return;
1462
- }
1463
-
1464
- // -- snip, extract --
1465
-
1466
- if (!args.local) {
1467
- const loggedIn = await loginOrRefreshIfRequired();
1468
- if (!loggedIn) {
1469
- // didn't login, let's just quit
1470
- console.log("Did not login, quitting...");
1471
- return;
1472
- }
1473
-
1474
- if (!config.account_id) {
1475
- config.account_id = await getAccountId();
1476
- if (!config.account_id) {
1477
- console.error("No account id found, quitting...");
1478
- return;
1479
- }
1480
- }
1481
- }
1482
-
1483
- // -- snip, end --
1871
+ const accountId = await requireAuth(config);
1484
1872
 
1485
- console.log(
1486
- await listNamespaceKeys(config.account_id, namespaceId, prefix)
1873
+ const results = await listNamespaceKeys(
1874
+ accountId,
1875
+ namespaceId,
1876
+ prefix
1487
1877
  );
1878
+ console.log(JSON.stringify(results, undefined, 2));
1488
1879
  }
1489
1880
  )
1490
1881
  .command(
@@ -1495,6 +1886,7 @@ export async function main(argv: string[]): Promise<void> {
1495
1886
  .positional("key", {
1496
1887
  describe: "The key value to get.",
1497
1888
  type: "string",
1889
+ demandOption: true,
1498
1890
  })
1499
1891
  .option("binding", {
1500
1892
  type: "string",
@@ -1508,58 +1900,26 @@ export async function main(argv: string[]): Promise<void> {
1508
1900
  .option("env", {
1509
1901
  type: "string",
1510
1902
  describe: "Perform on a specific environment",
1903
+ alias: "e",
1904
+ })
1905
+ .option("preview", {
1906
+ type: "boolean",
1907
+ describe: "Interact with a preview namespace",
1511
1908
  })
1512
1909
  .option("preview", {
1513
1910
  type: "boolean",
1911
+ // In the case of getting key values we will default to non-preview mode
1912
+ default: false,
1514
1913
  describe: "Interact with a preview namespace",
1515
1914
  });
1516
1915
  },
1517
1916
  async ({ key, ...args }) => {
1518
- const namespaceId = getNamespaceId(args);
1519
- const config = args.config as Config;
1520
-
1521
- if (args.local) {
1522
- const { Miniflare } = await import("miniflare");
1523
- const mf = new Miniflare({
1524
- kvPersist: (args.kvPersist as string) || true,
1525
- // TODO: these options shouldn't be required
1526
- script: ` `, // has to be a string with at least one char
1527
- });
1528
- const ns = await mf.getKVNamespace(namespaceId);
1529
- console.log(await ns.get(key));
1530
- return;
1531
- }
1532
-
1533
- // -- snip, extract --
1534
-
1535
- if (!args.local) {
1536
- const loggedIn = await loginOrRefreshIfRequired();
1537
- if (!loggedIn) {
1538
- // didn't login, let's just quit
1539
- console.log("Did not login, quitting...");
1540
- return;
1541
- }
1542
-
1543
- if (!config.account_id) {
1544
- config.account_id = await getAccountId();
1545
- if (!config.account_id) {
1546
- console.error("No account id found, quitting...");
1547
- return;
1548
- }
1549
- }
1550
- }
1917
+ const config = readConfig(args.config as ConfigPath, args);
1918
+ const namespaceId = getNamespaceId(args, config);
1551
1919
 
1552
- // -- snip, end --
1920
+ const accountId = await requireAuth(config);
1553
1921
 
1554
- // annoyingly, the API for this one doesn't return the
1555
- // data in the 'standard' format. goddamit.
1556
- // That's why we have the fallthrough response in cfetch.
1557
- // Oh well.
1558
- console.log(
1559
- await cfetch(
1560
- `/accounts/${config.account_id}/storage/kv/namespaces/${namespaceId}/values/${key}`
1561
- )
1562
- );
1922
+ console.log(await getKeyValue(accountId, namespaceId, key));
1563
1923
  }
1564
1924
  )
1565
1925
  .command(
@@ -1570,6 +1930,7 @@ export async function main(argv: string[]): Promise<void> {
1570
1930
  .positional("key", {
1571
1931
  describe: "The key value to delete",
1572
1932
  type: "string",
1933
+ demandOption: true,
1573
1934
  })
1574
1935
  .option("binding", {
1575
1936
  type: "string",
@@ -1583,6 +1944,7 @@ export async function main(argv: string[]): Promise<void> {
1583
1944
  .option("env", {
1584
1945
  type: "string",
1585
1946
  describe: "Perform on a specific environment",
1947
+ alias: "e",
1586
1948
  })
1587
1949
  .option("preview", {
1588
1950
  type: "boolean",
@@ -1590,45 +1952,18 @@ export async function main(argv: string[]): Promise<void> {
1590
1952
  });
1591
1953
  },
1592
1954
  async ({ key, ...args }) => {
1593
- const namespaceId = getNamespaceId(args);
1594
-
1595
- if (args.local) {
1596
- const { Miniflare } = await import("miniflare");
1597
- const mf = new Miniflare({
1598
- kvPersist: (args.kvPersist as string) || true,
1599
- // TODO: these options shouldn't be required
1600
- script: ` `, // has to be a string with at least one char
1601
- });
1602
- const ns = await mf.getKVNamespace(namespaceId);
1603
- console.log(await ns.delete(key));
1604
- return;
1605
- }
1606
-
1607
- const config = args.config as Config;
1608
-
1609
- // -- snip, extract --
1955
+ printWranglerBanner();
1956
+ const config = readConfig(args.config as ConfigPath, args);
1957
+ const namespaceId = getNamespaceId(args, config);
1610
1958
 
1611
- if (!args.local) {
1612
- const loggedIn = await loginOrRefreshIfRequired();
1613
- if (!loggedIn) {
1614
- // didn't login, let's just quit
1615
- console.log("Did not login, quitting...");
1616
- return;
1617
- }
1618
-
1619
- if (!config.account_id) {
1620
- config.account_id = await getAccountId();
1621
- if (!config.account_id) {
1622
- console.error("No account id found, quitting...");
1623
- return;
1624
- }
1625
- }
1626
- }
1959
+ console.log(
1960
+ `deleting the key "${key}" on namespace ${namespaceId}`
1961
+ );
1627
1962
 
1628
- // -- snip, end --
1963
+ const accountId = await requireAuth(config);
1629
1964
 
1630
- await cfetch(
1631
- `/accounts/${config.account_id}/storage/kv/namespaces/${namespaceId}/values/${key}`,
1965
+ await fetchResult(
1966
+ `/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`,
1632
1967
  { method: "DELETE" }
1633
1968
  );
1634
1969
  }
@@ -1637,11 +1972,12 @@ export async function main(argv: string[]): Promise<void> {
1637
1972
  );
1638
1973
 
1639
1974
  // :bulk
1640
- yargs.command(
1975
+ wrangler.command(
1641
1976
  "kv:bulk",
1642
1977
  "💪 Interact with multiple Workers KV key-value pairs at once",
1643
- (yargs) => {
1644
- return yargs
1978
+ (kvBulkYargs) => {
1979
+ return kvBulkYargs
1980
+ .command(subHelp)
1645
1981
  .command(
1646
1982
  "put <filename>",
1647
1983
  "Upload multiple key-value pairs to a namespace",
@@ -1650,19 +1986,21 @@ export async function main(argv: string[]): Promise<void> {
1650
1986
  .positional("filename", {
1651
1987
  describe: `The JSON file of key-value pairs to upload, in form [{"key":..., "value":...}"...]`,
1652
1988
  type: "string",
1989
+ demandOption: true,
1653
1990
  })
1654
1991
  .option("binding", {
1655
1992
  type: "string",
1656
- describe: "The name of the namespace to put to",
1993
+ describe: "The name of the namespace to insert values into",
1657
1994
  })
1658
1995
  .option("namespace-id", {
1659
1996
  type: "string",
1660
- describe: "The id of the namespace to put to",
1997
+ describe: "The id of the namespace to insert values into",
1661
1998
  })
1662
1999
  .check(demandOneOfOption("binding", "namespace-id"))
1663
2000
  .option("env", {
1664
2001
  type: "string",
1665
2002
  describe: "Perform on a specific environment",
2003
+ alias: "e",
1666
2004
  })
1667
2005
  .option("preview", {
1668
2006
  type: "boolean",
@@ -1670,78 +2008,91 @@ export async function main(argv: string[]): Promise<void> {
1670
2008
  });
1671
2009
  },
1672
2010
  async ({ filename, ...args }) => {
2011
+ printWranglerBanner();
1673
2012
  // The simplest implementation I could think of.
1674
2013
  // This could be made more efficient with a streaming parser/uploader
1675
2014
  // but we'll do that in the future if needed.
1676
2015
 
1677
- const namespaceId = getNamespaceId(args);
1678
- const config = args.config as Config;
1679
- const content = await readFile(filename, "utf-8");
1680
- let parsedContent;
1681
- try {
1682
- parsedContent = JSON.parse(content);
1683
- } catch (err) {
1684
- console.error(`could not parse json from ${filename}`);
1685
- throw err;
1686
- }
1687
-
1688
- if (args.local) {
1689
- const { Miniflare } = await import("miniflare");
1690
- const mf = new Miniflare({
1691
- kvPersist: (args.kvPersist as string) || true,
1692
- // TODO: these options shouldn't be required
1693
- script: ` `, // has to be a string with at least one char
1694
- });
1695
- const ns = await mf.getKVNamespace(namespaceId);
1696
- for (const {
1697
- key,
1698
- value,
1699
- expiration,
1700
- expiration_ttl,
1701
- } of parsedContent) {
1702
- await ns.put(key, value, {
1703
- expiration,
1704
- expirationTtl: expiration_ttl,
1705
- });
1706
- }
2016
+ const config = readConfig(args.config as ConfigPath, args);
2017
+ const namespaceId = getNamespaceId(args, config);
2018
+ const content = parseJSON(readFileSync(filename), filename);
1707
2019
 
1708
- return;
2020
+ if (!Array.isArray(content)) {
2021
+ throw new Error(
2022
+ `Unexpected JSON input from "${filename}".\n` +
2023
+ `Expected an array of key-value objects but got type "${typeof content}".`
2024
+ );
1709
2025
  }
1710
2026
 
1711
- // -- snip, extract --
1712
-
1713
- if (!args.local) {
1714
- const loggedIn = await loginOrRefreshIfRequired();
1715
- if (!loggedIn) {
1716
- // didn't login, let's just quit
1717
- console.log("Did not login, quitting...");
1718
- return;
1719
- }
1720
-
1721
- if (!config.account_id) {
1722
- config.account_id = await getAccountId();
1723
- if (!config.account_id) {
1724
- console.error("No account id found, quitting...");
1725
- return;
2027
+ const errors: string[] = [];
2028
+ const warnings: string[] = [];
2029
+ for (let i = 0; i < content.length; i++) {
2030
+ const keyValue = content[i];
2031
+ if (typeof keyValue !== "object") {
2032
+ errors.push(
2033
+ `The item at index ${i} is type: "${typeof keyValue}" - ${JSON.stringify(
2034
+ keyValue
2035
+ )}`
2036
+ );
2037
+ } else if (!isKeyValue(keyValue)) {
2038
+ errors.push(
2039
+ `The item at index ${i} is ${JSON.stringify(keyValue)}`
2040
+ );
2041
+ } else {
2042
+ const props = unexpectedKeyValueProps(keyValue);
2043
+ if (props.length > 0) {
2044
+ warnings.push(
2045
+ `The item at index ${i} contains unexpected properties: ${JSON.stringify(
2046
+ props
2047
+ )}.`
2048
+ );
1726
2049
  }
1727
2050
  }
1728
2051
  }
2052
+ if (warnings.length > 0) {
2053
+ console.warn(
2054
+ `Unexpected key-value properties in "${filename}".\n` +
2055
+ warnings.join("\n")
2056
+ );
2057
+ }
2058
+ if (errors.length > 0) {
2059
+ throw new Error(
2060
+ `Unexpected JSON input from "${filename}".\n` +
2061
+ `Each item in the array should be an object that matches:\n\n` +
2062
+ `interface KeyValue {\n` +
2063
+ ` key: string;\n` +
2064
+ ` value: string;\n` +
2065
+ ` expiration?: number;\n` +
2066
+ ` expiration_ttl?: number;\n` +
2067
+ ` metadata?: object;\n` +
2068
+ ` base64?: boolean;\n` +
2069
+ `}\n\n` +
2070
+ errors.join("\n")
2071
+ );
2072
+ }
1729
2073
 
1730
- // -- snip, end --
1731
-
1732
- console.log(
1733
- await putBulkKeyValue(config.account_id, namespaceId, content)
2074
+ const accountId = await requireAuth(config);
2075
+ await putBulkKeyValue(
2076
+ accountId,
2077
+ namespaceId,
2078
+ content,
2079
+ (index, total) => {
2080
+ console.log(`Uploaded ${index} of ${total}.`);
2081
+ }
1734
2082
  );
2083
+
2084
+ console.log("Success!");
1735
2085
  }
1736
2086
  )
1737
2087
  .command(
1738
2088
  "delete <filename>",
1739
- "Upload multiple key-value pairs to a namespace",
2089
+ "Delete multiple key-value pairs from a namespace",
1740
2090
  (yargs) => {
1741
2091
  return yargs
1742
2092
  .positional("filename", {
1743
- describe: `The JSON file of key-value pairs to upload, in form ["key1", "key2", ...]`,
2093
+ describe: `The JSON file of keys to delete, in the form ["key1", "key2", ...]`,
1744
2094
  type: "string",
2095
+ demandOption: true,
1745
2096
  })
1746
2097
  .option("binding", {
1747
2098
  type: "string",
@@ -1755,91 +2106,284 @@ export async function main(argv: string[]): Promise<void> {
1755
2106
  .option("env", {
1756
2107
  type: "string",
1757
2108
  describe: "Perform on a specific environment",
2109
+ alias: "e",
1758
2110
  })
1759
2111
  .option("preview", {
1760
2112
  type: "boolean",
1761
2113
  describe: "Interact with a preview namespace",
2114
+ })
2115
+ .option("force", {
2116
+ type: "boolean",
2117
+ alias: "f",
2118
+ describe: "Do not ask for confirmation before deleting",
1762
2119
  });
1763
2120
  },
1764
2121
  async ({ filename, ...args }) => {
1765
- const namespaceId = getNamespaceId(args);
1766
- const config = args.config as Config;
1767
- const content = await readFile(filename, "utf-8");
1768
- let parsedContent;
1769
- try {
1770
- parsedContent = JSON.parse(content);
1771
- } catch (err) {
1772
- console.error(`could not parse json from ${filename}`);
1773
- throw err;
1774
- }
2122
+ printWranglerBanner();
2123
+ const config = readConfig(args.config as ConfigPath, args);
2124
+ const namespaceId = getNamespaceId(args, config);
1775
2125
 
1776
- if (args.local) {
1777
- const { Miniflare } = await import("miniflare");
1778
- const mf = new Miniflare({
1779
- kvPersist: (args.kvPersist as string) || true,
1780
- // TODO: these options shouldn't be required
1781
- script: ` `, // has to be a string with at least one char
1782
- });
1783
- const ns = await mf.getKVNamespace(namespaceId);
1784
- for (const key of parsedContent) {
1785
- await ns.delete(key);
2126
+ if (!args.force) {
2127
+ const result = await confirm(
2128
+ `Are you sure you want to delete all the keys read from "${filename}" from kv-namespace with id "${namespaceId}"?`
2129
+ );
2130
+ if (!result) {
2131
+ console.log(`Not deleting keys read from "${filename}".`);
2132
+ return;
1786
2133
  }
1787
-
1788
- return;
1789
2134
  }
1790
2135
 
1791
- // -- snip, extract --
2136
+ const content = parseJSON(
2137
+ readFileSync(filename),
2138
+ filename
2139
+ ) as string[];
1792
2140
 
1793
- if (!args.local) {
1794
- const loggedIn = await loginOrRefreshIfRequired();
1795
- if (!loggedIn) {
1796
- // didn't login, let's just quit
1797
- console.log("Did not login, quitting...");
1798
- return;
1799
- }
2141
+ if (!Array.isArray(content)) {
2142
+ throw new Error(
2143
+ `Unexpected JSON input from "${filename}".\n` +
2144
+ `Expected an array of strings but got:\n${content}`
2145
+ );
2146
+ }
1800
2147
 
1801
- if (!config.account_id) {
1802
- config.account_id = await getAccountId();
1803
- if (!config.account_id) {
1804
- console.error("No account id found, quitting...");
1805
- return;
1806
- }
2148
+ const errors: string[] = [];
2149
+ for (let i = 0; i < content.length; i++) {
2150
+ const key = content[i];
2151
+ if (typeof key !== "string") {
2152
+ errors.push(
2153
+ `The item at index ${i} is type: "${typeof key}" - ${JSON.stringify(
2154
+ key
2155
+ )}`
2156
+ );
1807
2157
  }
1808
2158
  }
2159
+ if (errors.length > 0) {
2160
+ throw new Error(
2161
+ `Unexpected JSON input from "${filename}".\n` +
2162
+ `Expected an array of strings.\n` +
2163
+ errors.join("\n")
2164
+ );
2165
+ }
1809
2166
 
1810
- // -- snip, end --
2167
+ const accountId = await requireAuth(config);
1811
2168
 
1812
- console.log(
1813
- await deleteBulkKeyValue(config.account_id, namespaceId, content)
2169
+ await deleteBulkKeyValue(
2170
+ accountId,
2171
+ namespaceId,
2172
+ content,
2173
+ (index, total) => {
2174
+ console.log(`Deleted ${index} of ${total}.`);
2175
+ }
1814
2176
  );
2177
+
2178
+ console.log("Success!");
1815
2179
  }
1816
2180
  );
1817
2181
  }
1818
2182
  );
1819
2183
 
1820
- yargs.command("pages", "⚡️ Configure Cloudflare Pages", pages);
2184
+ wrangler.command(
2185
+ "pages",
2186
+ "⚡️ Configure Cloudflare Pages",
2187
+ async (pagesYargs) => {
2188
+ await pages(pagesYargs.command(subHelp).epilogue(pagesBetaWarning));
2189
+ }
2190
+ );
2191
+
2192
+ wrangler.command("r2", "📦 Interact with an R2 store", (r2Yargs) => {
2193
+ return r2Yargs
2194
+ .command(subHelp)
2195
+ .command("bucket", "Manage R2 buckets", (r2BucketYargs) => {
2196
+ r2BucketYargs.command(
2197
+ "create <name>",
2198
+ "Create a new R2 bucket",
2199
+ (yargs) => {
2200
+ return yargs.positional("name", {
2201
+ describe: "The name of the new bucket",
2202
+ type: "string",
2203
+ demandOption: true,
2204
+ });
2205
+ },
2206
+ async (args) => {
2207
+ printWranglerBanner();
2208
+
2209
+ const config = readConfig(args.config as ConfigPath, args);
2210
+
2211
+ const accountId = await requireAuth(config);
2212
+
2213
+ console.log(`Creating bucket ${args.name}.`);
2214
+ await createR2Bucket(accountId, args.name);
2215
+ console.log(`Created bucket ${args.name}.`);
2216
+ }
2217
+ );
2218
+
2219
+ r2BucketYargs.command("list", "List R2 buckets", {}, async (args) => {
2220
+ const config = readConfig(args.config as ConfigPath, args);
2221
+
2222
+ const accountId = await requireAuth(config);
2223
+
2224
+ console.log(JSON.stringify(await listR2Buckets(accountId), null, 2));
2225
+ });
2226
+
2227
+ r2BucketYargs.command(
2228
+ "delete <name>",
2229
+ "Delete an R2 bucket",
2230
+ (yargs) => {
2231
+ return yargs.positional("name", {
2232
+ describe: "The name of the bucket to delete",
2233
+ type: "string",
2234
+ demandOption: true,
2235
+ });
2236
+ },
2237
+ async (args) => {
2238
+ printWranglerBanner();
2239
+
2240
+ const config = readConfig(args.config as ConfigPath, args);
2241
+
2242
+ const accountId = await requireAuth(config);
2243
+
2244
+ console.log(`Deleting bucket ${args.name}.`);
2245
+ await deleteR2Bucket(accountId, args.name);
2246
+ console.log(`Deleted bucket ${args.name}.`);
2247
+ }
2248
+ );
2249
+ return r2BucketYargs;
2250
+ });
2251
+ });
2252
+
2253
+ /**
2254
+ * User Group: login, logout, and whoami
2255
+ * TODO: group commands into User group similar to .group() for flags in yargs
2256
+ */
2257
+ // login
2258
+ wrangler.command(
2259
+ // this needs scopes as an option?
2260
+ "login",
2261
+ "🔓 Login to Cloudflare",
2262
+ (yargs) => {
2263
+ // TODO: This needs some copy editing
2264
+ // I mean, this entire app does, but this too.
2265
+ return yargs
2266
+ .option("scopes-list", {
2267
+ describe: "List all the available OAuth scopes with descriptions",
2268
+ })
2269
+ .option("scopes", {
2270
+ describe: "Pick the set of applicable OAuth scopes when logging in",
2271
+ array: true,
2272
+ type: "string",
2273
+ });
2274
+
2275
+ // TODO: scopes
2276
+ },
2277
+ async (args) => {
2278
+ printWranglerBanner();
2279
+ if (args["scopes-list"]) {
2280
+ listScopes();
2281
+ return;
2282
+ }
2283
+ if (args.scopes) {
2284
+ if (args.scopes.length === 0) {
2285
+ // don't allow no scopes to be passed, that would be weird
2286
+ listScopes();
2287
+ return;
2288
+ }
2289
+ if (!validateScopeKeys(args.scopes)) {
2290
+ throw new CommandLineArgsError(
2291
+ `One of ${args.scopes} is not a valid authentication scope. Run "wrangler login --list-scopes" to see the valid scopes.`
2292
+ );
2293
+ }
2294
+ await login({ scopes: args.scopes });
2295
+ return;
2296
+ }
2297
+ await login();
2298
+
2299
+ // TODO: would be nice if it optionally saved login
2300
+ // credentials inside node_modules/.cache or something
2301
+ // this way you could have multiple users on a single machine
2302
+ }
2303
+ );
2304
+
2305
+ // logout
2306
+ wrangler.command(
2307
+ // this needs scopes as an option?
2308
+ "logout",
2309
+ "🚪 Logout from Cloudflare",
2310
+ () => {},
2311
+ async () => {
2312
+ printWranglerBanner();
2313
+ await logout();
2314
+ }
2315
+ );
2316
+
2317
+ // whoami
2318
+ wrangler.command(
2319
+ "whoami",
2320
+ "🕵️ Retrieve your user info and test your auth config",
2321
+ () => {},
2322
+ async () => {
2323
+ printWranglerBanner();
2324
+ await whoami();
2325
+ }
2326
+ );
1821
2327
 
1822
- yargs
2328
+ wrangler
2329
+ .option("legacy-env", {
2330
+ type: "boolean",
2331
+ describe: "Use legacy environments",
2332
+ })
1823
2333
  .option("config", {
2334
+ alias: "c",
1824
2335
  describe: "Path to .toml configuration file",
1825
2336
  type: "string",
1826
- async coerce(arg) {
1827
- return await readConfig(arg);
1828
- },
1829
- })
1830
- .option("local", {
1831
- describe: "Run on my machine",
1832
- type: "boolean",
1833
- default: false, // I bet this will a point of contention. We'll revisit it.
1834
2337
  });
1835
2338
 
1836
- yargs.group(["config", "help", "version"], "Flags:");
1837
- yargs.version(wranglerVersion);
1838
- await initialiseUserConfig();
1839
-
1840
- await yargs.parse(argv, (err, argv, output) => {
1841
- if (output) {
1842
- console.log(output);
2339
+ wrangler.group(["config", "help", "version", "legacy-env"], "Flags:");
2340
+ wrangler.help().alias("h", "help");
2341
+ wrangler.version(wranglerVersion).alias("v", "version");
2342
+ wrangler.exitProcess(false);
2343
+
2344
+ try {
2345
+ await wrangler.parse();
2346
+ } catch (e) {
2347
+ if (e instanceof CommandLineArgsError) {
2348
+ wrangler.showHelp("error");
2349
+ console.error(""); // Just adds a bit of space
2350
+ console.error(e.message);
2351
+ } else if (e instanceof ParseError) {
2352
+ console.error("");
2353
+ e.notes.push({
2354
+ text: "\nIf you think this is a bug, please open an issue at: https://github.com/cloudflare/wrangler2/issues/new",
2355
+ });
2356
+ console.error(formatMessage(e));
2357
+ } else {
2358
+ console.error(e instanceof Error ? e.message : e);
2359
+ console.error(""); // Just adds a bit of space
2360
+ console.error(
2361
+ `${fgGreenColor}%s${resetColor}`,
2362
+ "If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
2363
+ );
1843
2364
  }
1844
- });
2365
+ throw e;
2366
+ }
2367
+ }
2368
+
2369
+ function getDevCompatibilityDate(
2370
+ config: Config,
2371
+ compatibilityDate = config.compatibility_date
2372
+ ) {
2373
+ const currentDate = new Date().toISOString().substring(0, 10);
2374
+ if (config.configPath !== undefined && compatibilityDate === undefined) {
2375
+ console.warn(
2376
+ `No compatibility_date was specified. Using today's date: ${currentDate}.\n` +
2377
+ "Add one to your wrangler.toml file:\n" +
2378
+ "```\n" +
2379
+ `compatibility_date = "${currentDate}"\n` +
2380
+ "```\n" +
2381
+ "or pass it in your terminal:\n" +
2382
+ "```\n" +
2383
+ `--compatibility-date=${currentDate}\n` +
2384
+ "```\n" +
2385
+ "See https://developers.cloudflare.com/workers/platform/compatibility-dates for more information."
2386
+ );
2387
+ }
2388
+ return compatibilityDate ?? currentDate;
1845
2389
  }