wrangler 0.0.0-e928f94 → 0.0.0-e95bea2

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