wrangler 0.0.0-fb43821 → 0.0.0-fb53fda

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