wrangler 0.0.0-e5d3690 → 0.0.0-e88a54e

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