wrangler 0.0.0-ece06ea → 0.0.0-ed08df4

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