wrangler 0.0.0-e2a26a0 → 0.0.0-e3e3243

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