wrangler 0.0.0-e22f9d7 → 0.0.0-e273e09

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