wrangler 0.0.0-e2a26a0 → 0.0.0-e3a458e

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