wrangler 0.0.0-beta.6 → 0.0.0-bfbb0be

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