wrangler 0.0.0-e22f9d7 → 0.0.0-e2b7352

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