wrangler 0.0.0-de73fa2 → 0.0.0-e0b9366

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