wrangler 0.0.0-e928f94 → 0.0.0-e978986

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