wrangler 0.0.0-bf67bf2 → 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 +76 -36
  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 +2092 -1029
  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 +661 -272
  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 +100313 -110298
  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 -158
  136. package/src/api/inspect.ts +0 -441
  137. package/src/api/preview.ts +0 -123
  138. package/src/api/worker.ts +0 -157
  139. package/src/cfetch.ts +0 -72
  140. package/src/config.ts +0 -124
  141. package/src/dev.tsx +0 -736
  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,123 +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
- "compatibility_date",
63
- "compatibility_flags",
64
- "zone_id",
65
- "routes",
66
- "route",
67
- "jsx_factory",
68
- "jsx_fragment",
69
- "polyfill_node",
70
- "site",
71
- "triggers",
72
- "usage_model",
73
- ];
74
-
75
- Object.keys(config.env || {}).forEach((env) => {
76
- inheritedFields.forEach((field) => {
77
- if (config[field] !== undefined && config.env[env][field] === undefined) {
78
- config.env[env][field] = config[field]; // TODO: - shallow copy?
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);
79
185
  }
80
186
  });
81
- });
82
187
 
83
- const mirroredFields = ["vars", "kv_namespaces", "durable_objects"];
84
- Object.keys(config.env || {}).forEach((env) => {
85
- mirroredFields.forEach((field) => {
86
- // if it exists on top level, it should exist on env defns
87
- Object.keys(config[field] || {}).forEach((fieldKey) => {
88
- if (!(fieldKey in config.env[env][field])) {
89
- console.error(
90
- `In your configuration, "${field}.${fieldKey}" exists at a top level, but not on "env.${env}". This is not what you probably want, since the field "${field}" is not inherited by environments. Please add "${field}.${fieldKey}" to "env.${env}".`
91
- );
92
- }
93
- });
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(""));
94
192
  });
95
- });
96
193
 
97
- // todo: validate, add defaults
98
- // let's just do some basics for now
99
-
100
- // @ts-expect-error we're being sneaky here for now
101
- config.__path__ = path;
102
-
103
- 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
+ });
104
199
  }
105
200
 
106
201
  // a helper to demand one of a set of options
107
202
  // via https://github.com/yargs/yargs/issues/1093#issuecomment-491299261
108
203
  function demandOneOfOption(...options: string[]) {
109
- return function (argv: yargs.Arguments) {
204
+ return function (argv: Yargs.Arguments) {
110
205
  const count = options.filter((option) => argv[option]).length;
111
206
  const lastOption = options.pop();
112
207
 
113
208
  if (count === 0) {
114
- throw new Error(
209
+ throw new CommandLineArgsError(
115
210
  `Exactly one of the arguments ${options.join(
116
211
  ", "
117
212
  )} and ${lastOption} is required`
118
213
  );
119
214
  } else if (count > 1) {
120
- throw new Error(
215
+ throw new CommandLineArgsError(
121
216
  `Arguments ${options.join(
122
217
  ", "
123
218
  )} and ${lastOption} are mutually exclusive`
@@ -128,30 +223,65 @@ function demandOneOfOption(...options: string[]) {
128
223
  };
129
224
  }
130
225
 
131
- export async function main(argv: string[]): Promise<void> {
132
- const yargs = makeCLI(hideBin(process.argv))
133
- .command(
134
- // the default is to simply print the help menu
135
- ["*"],
136
- false,
137
- () => {},
138
- () => {
139
- yargs.showHelp("log");
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);
140
248
  }
141
- )
249
+ throw error;
250
+ })
142
251
  .scriptName("wrangler")
143
252
  .wrap(null);
144
253
 
145
- // you will note that we use the form for all commands where we use the builder function
146
- // to define options and subcommands. Further we return the result of this builder even
147
- // tho it's not completely necessary. The reason is that it's required for type inference
148
- // of the args in the handle function.I wish we could enforce this pattern, but this
149
- // comment will have to do for now.
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
+ );
150
275
 
151
- // 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. 🤷‍♂.)
152
282
 
153
283
  // [DEPRECATED] generate
154
- yargs.command(
284
+ wrangler.command(
155
285
  // we definitely want to move away from us cloning github templates
156
286
  // we can do something better here, let's see
157
287
  "generate [name] [template]",
@@ -163,587 +293,1347 @@ export async function main(argv: string[]): Promise<void> {
163
293
  default: "worker",
164
294
  })
165
295
  .positional("template", {
166
- describe: "a link to a GitHub template",
296
+ describe: "The URL of a GitHub template",
167
297
  default: "https://github.com/cloudflare/worker-template",
168
298
  });
169
299
  },
170
- () => {
300
+ (generateArgs) => {
171
301
  // "👯 [DEPRECATED]. Scaffold a Cloudflare Workers project from a public GitHub repository.",
172
- console.error(
173
- "`wrangler generate` has been deprecated, please refer to TODO://some/path for alternatives"
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."
174
309
  );
175
310
  }
176
311
  );
177
312
 
178
313
  // init
179
- yargs.command(
314
+ wrangler.command(
180
315
  "init [name]",
181
316
  "📥 Create a wrangler.toml configuration file",
182
317
  (yargs) => {
183
- return yargs.positional("name", {
184
- describe: "The name of your worker.",
185
- type: "string",
186
- });
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
+ });
187
340
  },
188
- async () => {
189
- const destination = path.join(process.cwd(), "wrangler.toml");
190
- if (fs.existsSync(destination)) {
191
- 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
+ }
192
405
  } else {
406
+ await mkdir(creationDirectory, { recursive: true });
193
407
  const compatibilityDate = new Date().toISOString().substring(0, 10);
408
+
194
409
  try {
195
410
  await writeFile(
196
- destination,
197
- `compatibility_date = "${compatibilityDate}"` + "\n"
411
+ wranglerTomlDestination,
412
+ TOML.stringify({
413
+ name: workerName,
414
+ compatibility_date: compatibilityDate,
415
+ }) + "\n"
198
416
  );
199
- console.log(`✨ Succesfully created wrangler.toml`);
200
- // TODO: suggest next steps?
417
+
418
+ logger.log(
419
+ `✨ Created ${path.relative(
420
+ process.cwd(),
421
+ wranglerTomlDestination
422
+ )}`
423
+ );
424
+ justCreatedWranglerToml = true;
201
425
  } catch (err) {
202
- console.error(`Failed to create wrangler.toml`);
203
- console.error(err);
204
- 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
+ );
205
458
  }
206
459
  }
207
460
 
208
- // if no package.json, ask, and if yes, create one
209
- 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;
210
468
 
211
469
  if (!pathToPackageJson) {
212
- if (
213
- await confirm("No package.json found. Would you like to create one?")
214
- ) {
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) {
215
478
  await writeFile(
216
- path.join(process.cwd(), "package.json"),
479
+ path.join(creationDirectory, "./package.json"),
217
480
  JSON.stringify(
218
481
  {
219
- name: "worker",
220
- version: "0.0.1",
482
+ name: workerName,
483
+ version: "0.0.0",
484
+ devDependencies: {
485
+ wrangler: wranglerVersion,
486
+ },
487
+ private: true,
221
488
  },
222
489
  null,
223
490
  " "
224
491
  ) + "\n"
225
492
  );
226
- console.log(`✨ Created package.json`);
227
- 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
+ );
228
499
  } else {
229
500
  return;
230
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
+ }
231
527
  }
232
528
 
233
- // if workers-types doesn't exist as a dependency
234
- // offer to install it
235
- // and make a tsconfig?
236
- let pathToTSConfig = await findUp("tsconfig.json");
529
+ let isTypescriptProject = false;
530
+ let pathToTSConfig = await findPath(
531
+ isolatedInit,
532
+ creationDirectory,
533
+ "tsconfig.json"
534
+ );
237
535
  if (!pathToTSConfig) {
238
- 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) {
239
652
  await writeFile(
240
- path.join(process.cwd(), "tsconfig.json"),
653
+ packagePath,
241
654
  JSON.stringify(
242
655
  {
243
- compilerOptions: {
244
- target: "ES2020",
245
- module: "CommonJS",
246
- lib: ["ES2020"],
247
- 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}`,
248
665
  },
249
666
  },
250
667
  null,
251
- " "
668
+ 2
252
669
  ) + "\n"
253
670
  );
254
- await execa("npm", [
255
- "install",
256
- "@cloudflare/workers-types",
257
- "--save-dev",
258
- ]);
259
- console.log(
260
- `✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies`
671
+ instructions.push(
672
+ `\nTo start developing your Worker, run \`${
673
+ isNamedWorker ? `cd ${args.name} && ` : ""
674
+ }npm start\``
261
675
  );
262
- 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
+ }
263
817
  }
264
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));
265
834
  }
266
835
  );
267
836
 
268
837
  // build
269
- yargs.command(
838
+ wrangler.command(
270
839
  "build",
271
840
  false,
272
841
  (yargs) => {
273
842
  return yargs.option("env", {
274
843
  describe: "Perform on a specific environment",
844
+ type: "string",
275
845
  });
276
846
  },
277
- () => {
847
+ async (buildArgs) => {
278
848
  // "[DEPRECATED] 🦀 Build your project (if applicable)",
279
- console.error(
280
- "`wrangler build` has been deprecated, please refer to TODO://some/path for alternatives"
281
- );
282
- }
283
- );
284
849
 
285
- // login
286
- yargs.command(
287
- // this needs scopes as an option?
288
- "login",
289
- false, // we don't need to show this in the menu
290
- // "🔓 Login to Cloudflare",
291
- (yargs) => {
292
- // TODO: This needs some copy editing
293
- // I mean, this entire app does, but this too.
294
- return yargs
295
- .option("scopes-list", {
296
- describe: "list all the available OAuth scopes with descriptions.",
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
+ ],
297
863
  })
298
- .option("scopes", {
299
- describe: "allows to choose your set of OAuth scopes.",
300
- array: true,
301
- type: "string",
302
- });
303
-
304
- // TODO: scopes
305
- },
306
- async (args) => {
307
- if (args["scopes-list"]) {
308
- listScopes();
309
- return;
310
- }
311
- if (args.scopes) {
312
- if (args.scopes.length === 0) {
313
- // don't allow no scopes to be passed, that would be weird
314
- listScopes();
315
- return;
316
- }
317
- await login({ scopes: args.scopes });
318
- return;
319
- }
320
- await login();
321
-
322
- // TODO: would be nice if it optionally saved login
323
- // creds inside node_modules/.cache or something
324
- // this way you could have multiple users on a single machine
325
- }
326
- );
327
-
328
- // logout
329
- yargs.command(
330
- // this needs scopes as an option?
331
- "logout",
332
- false, // we don't need to show this in the menu
333
- // "🚪 Logout from Cloudflare",
334
- () => {},
335
- async () => {
336
- await logout();
337
- }
338
- );
864
+ );
339
865
 
340
- // whoami
341
- yargs.command(
342
- "whoami",
343
- false, // we don't need to show this the menu
344
- // "🕵️ Retrieve your user info and test your auth config",
345
- () => {},
346
- (args) => {
347
- console.log(":whoami", args);
866
+ await createCLIParser([
867
+ "publish",
868
+ "--dry-run",
869
+ "--outdir=dist",
870
+ ...(buildArgs.env ? ["--env", buildArgs.env] : []),
871
+ ]).parse();
348
872
  }
349
873
  );
350
874
 
351
875
  // config
352
- yargs.command(
876
+ wrangler.command(
353
877
  "config",
354
878
  false,
355
879
  () => {},
356
880
  () => {
357
881
  // "🕵️ Authenticate Wrangler with a Cloudflare API Token",
358
- console.error(
359
- "`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"
360
884
  );
361
885
  }
362
886
  );
363
887
 
364
888
  // dev
365
- yargs.command(
366
- "dev <filename>",
889
+ wrangler.command(
890
+ "dev [script]",
367
891
  "👂 Start a local server for developing your worker",
368
892
  (yargs) => {
369
893
  return yargs
370
- .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
+ })
371
898
  .option("name", {
372
- describe: "name of the script",
899
+ describe: "Name of the worker",
373
900
  type: "string",
901
+ requiresArg: true,
374
902
  })
375
903
  .option("format", {
376
904
  choices: ["modules", "service-worker"] as const,
377
905
  describe: "Choose an entry type",
906
+ deprecated: true,
378
907
  })
379
908
  .option("env", {
380
909
  describe: "Perform on a specific environment",
381
910
  type: "string",
382
- // 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,
383
930
  })
384
931
  .option("ip", {
385
- describe: "IP address to listen on",
932
+ describe: "IP address to listen on, defaults to `localhost`",
386
933
  type: "string",
387
- default: "127.0.0.1",
934
+ requiresArg: true,
388
935
  })
389
936
  .option("port", {
390
- describe: "Port to listen on, defaults to 8787",
937
+ describe: "Port to listen on",
391
938
  type: "number",
392
- 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,
393
950
  })
394
951
  .option("host", {
395
952
  type: "string",
953
+ requiresArg: true,
396
954
  describe:
397
955
  "Host to forward requests to, defaults to the zone of project",
398
956
  })
399
957
  .option("local-protocol", {
400
- default: "http",
401
958
  describe: "Protocol to listen to requests on, defaults to http.",
402
- 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,
403
967
  })
404
- .option("public", {
968
+ .option("assets", {
405
969
  describe: "Static assets to be served",
406
970
  type: "string",
971
+ requiresArg: true,
407
972
  })
408
973
  .option("site", {
409
974
  describe: "Root folder of static assets for Workers Sites",
410
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,
411
991
  })
412
992
  .option("upstream-protocol", {
413
- default: "https",
414
993
  describe:
415
994
  "Protocol to forward requests to host on, defaults to https.",
416
- choices: ["http", "https"],
995
+ choices: ["http", "https"] as const,
417
996
  })
418
997
  .option("jsx-factory", {
419
998
  describe: "The function that is called for each JSX element",
420
999
  type: "string",
1000
+ requiresArg: true,
421
1001
  })
422
1002
  .option("jsx-fragment", {
423
1003
  describe: "The function that is called for each JSX fragment",
424
1004
  type: "string",
1005
+ requiresArg: true,
425
1006
  })
426
- .option("polyfill-node", {
427
- describe: "Try to polyfill node builtins",
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",
428
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,
429
1039
  });
430
1040
  },
431
1041
  async (args) => {
432
- const { filename, format } = args;
433
- 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
+ }
1064
+
1065
+ const entry = await getEntry(args, config, "dev");
1066
+
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
+ );
1078
+ }
434
1079
 
435
- // -- snip, extract --
1080
+ if (args.inspect) {
1081
+ logger.warn(
1082
+ "Passing --inspect is unnecessary, now you can always connect to devtools."
1083
+ );
1084
+ }
436
1085
 
437
- if (!args.local) {
438
- const loggedIn = await loginOrRefreshIfRequired();
439
- if (!loggedIn) {
440
- // didn't login, let's just quit
441
- console.log("Did not login, quitting...");
442
- return;
1086
+ if (args["experimental-public"]) {
1087
+ throw new Error(
1088
+ "The --experimental-public field has been renamed to --assets"
1089
+ );
443
1090
  }
444
- if (!config.account_id) {
445
- config.account_id = await getAccountId();
446
- if (!config.account_id) {
447
- console.error("No account id found, quitting...");
448
- return;
449
- }
1091
+
1092
+ if (args.public) {
1093
+ throw new Error("The --public field has been renamed to --assets");
450
1094
  }
451
- }
452
1095
 
453
- // -- snip, end --
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
+ }
454
1101
 
455
- const envRootObj = args.env ? config.env[args.env] || {} : config;
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
+ }
456
1111
 
457
- const polyfillNode = args["polyfill-node"] ?? config.polyfill_node;
458
- if (polyfillNode) {
459
- console.warn(
460
- "Using polyfills for node builtin modules. These are not meant to be completely reliable, and have serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
461
- );
462
- }
1112
+ // TODO: if worker_dev = false and no routes, then error (only for dev)
463
1113
 
464
- render(
465
- <Dev
466
- name={args.name || config.name}
467
- entry={filename}
468
- format={format}
469
- initialMode={args.local ? "local" : "remote"}
470
- jsxFactory={args["jsx-factory"] || envRootObj?.jsx_factory}
471
- jsxFragment={args["jsx-fragment"] || envRootObj?.jsx_fragment}
472
- accountId={config.account_id}
473
- site={args.site || config.site?.bucket}
474
- port={args.port || config.dev?.port}
475
- polyfillNode={polyfillNode}
476
- public={args.public}
477
- compatibilityDate={config.compatibility_date}
478
- compatibilityFlags={config.compatibility_flags}
479
- usageModel={config.usage_model}
480
- variables={{
481
- ...(envRootObj?.vars || {}),
482
- ...(envRootObj?.kv_namespaces || []).reduce(
483
- (obj, { binding, preview_id }) => {
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.
484
1151
  if (!preview_id) {
485
1152
  // TODO: This error has to be a _lot_ better, ideally just asking
486
1153
  // to create a preview namespace for the user automatically
487
1154
  throw new Error(
488
- "kv namespaces need a preview id during dev mode"
489
- );
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
490
1157
  }
491
- return { ...obj, [binding]: { namespaceId: preview_id } };
492
- },
493
- {}
1158
+ return {
1159
+ binding,
1160
+ id: preview_id,
1161
+ };
1162
+ }
494
1163
  ),
495
- ...(envRootObj?.durable_objects?.bindings || []).reduce(
496
- (obj, { name, class_name, script_name }) => {
497
- return { ...obj, [name]: { class_name, script_name } };
498
- },
499
- {}
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`
1177
+ );
1178
+ }
1179
+ return {
1180
+ binding,
1181
+ bucket_name: preview_bucket_name,
1182
+ };
1183
+ }
500
1184
  ),
501
- }}
502
- />
503
- );
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
+ }
504
1274
  }
505
1275
  );
506
1276
 
507
1277
  // publish
508
- yargs.command(
1278
+ wrangler.command(
509
1279
  "publish [script]",
510
1280
  "🆙 Publish your Worker to Cloudflare.",
511
1281
  (yargs) => {
512
1282
  return yargs
513
1283
  .option("env", {
514
1284
  type: "string",
1285
+ requiresArg: true,
515
1286
  describe: "Perform on a specific environment",
1287
+ alias: "e",
516
1288
  })
517
1289
  .positional("script", {
518
- describe: "script to upload",
1290
+ describe: "The path to an entry point for your worker",
519
1291
  type: "string",
1292
+ requiresArg: true,
520
1293
  })
521
1294
  .option("name", {
522
- 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",
523
1328
  type: "string",
1329
+ requiresArg: true,
1330
+ deprecated: true,
1331
+ hidden: true,
524
1332
  })
525
- .option("public", {
1333
+ .option("assets", {
526
1334
  describe: "Static assets to be served",
527
1335
  type: "string",
1336
+ requiresArg: true,
528
1337
  })
529
1338
  .option("site", {
530
1339
  describe: "Root folder of static assets for Workers Sites",
531
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,
532
1356
  })
533
1357
  .option("triggers", {
534
1358
  describe: "cron schedules to attach",
535
1359
  alias: ["schedule", "schedules"],
536
- type: "array",
1360
+ type: "string",
1361
+ requiresArg: true,
1362
+ array: true,
537
1363
  })
538
1364
  .option("routes", {
539
- describe: "routes to upload",
1365
+ describe: "Routes to upload",
540
1366
  alias: "route",
541
- type: "array",
542
- })
543
- .option("services", {
544
- describe: "experimental support for services",
545
- type: "boolean",
546
- default: "false",
547
- hidden: true,
1367
+ type: "string",
1368
+ requiresArg: true,
1369
+ array: true,
548
1370
  })
549
1371
  .option("jsx-factory", {
550
1372
  describe: "The function that is called for each JSX element",
551
1373
  type: "string",
1374
+ requiresArg: true,
552
1375
  })
553
1376
  .option("jsx-fragment", {
554
1377
  describe: "The function that is called for each JSX fragment",
555
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",
556
1397
  })
557
- .option("polyfill-node", {
558
- describe: "Try to polyfill node builtins",
1398
+ .option("legacy-env", {
559
1399
  type: "boolean",
1400
+ describe: "Use legacy environments",
1401
+ hidden: true,
560
1402
  });
561
1403
  },
562
1404
  async (args) => {
563
- if (args.local) {
564
- console.error("🚫 Local publishing is not yet supported");
565
- 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");
566
1415
  }
567
- const config = args.config as Config;
568
-
569
- // -- snip, extract --
570
- if (!args.local) {
571
- const loggedIn = await loginOrRefreshIfRequired();
572
- if (!loggedIn) {
573
- // didn't login, let's just quit
574
- console.log("Did not login, quitting...");
575
- return;
576
- }
577
- if (!config.account_id) {
578
- config.account_id = await getAccountId();
579
- if (!config.account_id) {
580
- console.error("No account id found, quitting...");
581
- return;
582
- }
583
- }
1416
+ if (args["experimental-public"]) {
1417
+ throw new Error(
1418
+ "The --experimental-public field has been renamed to --assets"
1419
+ );
584
1420
  }
585
1421
 
586
- // -- snip, end --
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
+ }
587
1427
 
588
- const polyfillNode = args["polyfill-node"] ?? config.polyfill_node;
589
- if (polyfillNode) {
590
- console.warn(
591
- "Using polyfills for node builtin modules. These are not meant to be completely reliable, and have serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
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"
592
1431
  );
593
1432
  }
594
1433
 
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
+ );
1445
+
595
1446
  await publish({
596
- config: args.config as Config,
597
- name: args.name,
598
- script: args.script,
1447
+ config,
1448
+ accountId,
1449
+ name: getScriptName(args, config),
1450
+ rules: getRules(config),
1451
+ entry,
599
1452
  env: args.env,
1453
+ compatibilityDate: args.latest
1454
+ ? new Date().toISOString().substring(0, 10)
1455
+ : args["compatibility-date"],
1456
+ compatibilityFlags: args["compatibility-flags"],
600
1457
  triggers: args.triggers,
601
1458
  jsxFactory: args["jsx-factory"],
602
1459
  jsxFragment: args["jsx-fragment"],
603
- polyfillNode: args["polyfill-node"],
1460
+ tsconfig: args.tsconfig,
604
1461
  routes: args.routes,
605
- public: args.public,
606
- 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,
607
1469
  });
608
1470
  }
609
1471
  );
610
1472
 
611
1473
  // tail
612
- yargs.command(
1474
+ wrangler.command(
613
1475
  "tail [name]",
614
- "🦚 Starts a log tailing session for a deployed Worker.",
1476
+ "🦚 Starts a log tailing session for a published Worker.",
615
1477
  (yargs) => {
616
- return (
617
- yargs
618
- .positional("name", {
619
- describe: "name of the worker",
620
- type: "string",
621
- })
622
- // TODO: auto-detect if this should be json or pretty based on atty
623
- .option("format", {
624
- default: "json",
625
- choices: ["json", "pretty"],
626
- describe: "The format of log entries",
627
- })
628
- .option("status", {
629
- choices: ["ok", "error", "canceled"],
630
- describe: "Filter by invocation status",
631
- })
632
- .option("header", {
633
- type: "string",
634
- describe: "Filter by HTTP header",
635
- })
636
- .option("method", {
637
- type: "string",
638
- describe: "Filter by HTTP method",
639
- })
640
- .option("sampling-rate", {
641
- type: "number",
642
- describe: "Adds a percentage of requests to log sampling rate",
643
- })
644
- .option("search", {
645
- type: "string",
646
- describe: "Filter by a text match in console.log messages",
647
- })
648
- .option("env", {
649
- type: "string",
650
- describe: "Perform on a specific environment",
651
- })
652
- );
653
- // TODO: filter by client ip, which can be 'self' or an ip address
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
+ });
654
1538
  },
655
1539
  async (args) => {
656
- const config = args.config as Config;
657
-
658
- if (!(args.name || config.name)) {
659
- console.error("Missing script name");
660
- return;
1540
+ if (args.format === "pretty") {
1541
+ await printWranglerBanner();
661
1542
  }
662
- const scriptName = `${args.name || config.name}${
663
- args.env ? `-${args.env}` : ""
664
- }`;
1543
+ const config = readConfig(args.config as ConfigPath, args);
665
1544
 
666
- // -- snip, extract --
1545
+ const scriptName = getLegacyScriptName(args, config);
667
1546
 
668
- if (!args.local) {
669
- const loggedIn = await loginOrRefreshIfRequired();
670
- if (!loggedIn) {
671
- // didn't login, let's just quit
672
- console.log("Did not login, quitting...");
673
- return;
674
- }
675
- if (!config.account_id) {
676
- config.account_id = await getAccountId();
677
- if (!config.account_id) {
678
- console.error("No account id found, quitting...");
679
- return;
680
- }
681
- }
1547
+ if (!scriptName) {
1548
+ throw new Error("Missing script name");
682
1549
  }
683
1550
 
684
- // -- snip, end --
1551
+ const accountId = await requireAuth(config);
685
1552
 
686
- const accountId = config.account_id;
687
-
688
- const filters = {
689
- status: args.status as "ok" | "error" | "canceled",
1553
+ const cliFilters: TailCLIFilters = {
1554
+ status: args.status as ("ok" | "error" | "canceled")[] | undefined,
690
1555
  header: args.header,
691
1556
  method: args.method,
692
- "sampling-rate": args["sampling-rate"],
1557
+ samplingRate: args["sampling-rate"],
693
1558
  search: args.search,
1559
+ clientIp: args.ip,
694
1560
  };
695
1561
 
696
- const { tail, expiration, /* sendHeartbeat, */ deleteTail } =
697
- await createTail(accountId, scriptName, filters);
1562
+ const filters = translateCLICommandToFilterMessage(
1563
+ cliFilters,
1564
+ args.debug
1565
+ );
698
1566
 
699
- console.log(
700
- `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
701
1572
  );
702
1573
 
703
- 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 () => {
704
1585
  tail.terminate();
705
- deleteTail();
1586
+ await deleteTail();
706
1587
  });
707
1588
 
708
- tail.on("message", (data) => {
709
- console.log(JSON.stringify(JSON.parse(data.toString()), null, " "));
710
- });
1589
+ const printLog: (data: RawData) => void =
1590
+ args.format === "pretty" ? prettyPrintLogs : jsonPrintLogs;
1591
+
1592
+ tail.on("message", printLog);
711
1593
 
712
1594
  while (tail.readyState !== tail.OPEN) {
713
1595
  switch (tail.readyState) {
714
1596
  case tail.CONNECTING:
715
- await setTimeout(1000);
1597
+ await setTimeout(100);
716
1598
  break;
717
1599
  case tail.CLOSING:
718
- await setTimeout(1000);
1600
+ await setTimeout(100);
719
1601
  break;
720
1602
  case tail.CLOSED:
721
- process.exit(1);
1603
+ throw new Error(
1604
+ `Connection to ${scriptDisplayName} closed unexpectedly.`
1605
+ );
722
1606
  }
723
1607
  }
724
1608
 
725
- 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
+ });
726
1617
  }
727
1618
  );
728
1619
 
729
1620
  // preview
730
- yargs.command(
1621
+ wrangler.command(
731
1622
  "preview [method] [body]",
732
1623
  false,
733
1624
  (yargs) => {
734
1625
  return yargs
735
1626
  .positional("method", {
1627
+ type: "string",
736
1628
  describe: "Type of request to preview your worker",
737
- choices: ["GET", "POST"],
738
- default: ["GET"],
739
1629
  })
740
1630
  .positional("body", {
741
1631
  type: "string",
742
1632
  describe: "Body string to post to your preview worker request.",
743
- default: "Null",
744
1633
  })
745
1634
  .option("env", {
746
1635
  type: "string",
1636
+ requiresArg: true,
747
1637
  describe: "Perform on a specific environment",
748
1638
  })
749
1639
  .option("watch", {
@@ -752,53 +1642,151 @@ export async function main(argv: string[]): Promise<void> {
752
1642
  type: "boolean",
753
1643
  });
754
1644
  },
755
- () => {
756
- // "🔬 [DEPRECATED] Preview your code temporarily on cloudflareworkers.com"
757
- console.error(
758
- "`wrangler preview` has been deprecated, please refer to TODO://some/path for alternatives"
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
+ />
759
1743
  );
1744
+ await waitUntilExit();
760
1745
  }
761
1746
  );
762
1747
 
763
- // route
764
- yargs.command(
1748
+ // [DEPRECATED] route
1749
+ wrangler.command(
765
1750
  "route",
766
1751
  false, // I think we want to hide this command
767
1752
  // "➡️ List or delete worker routes",
768
- (yargs) => {
769
- return yargs
1753
+ (routeYargs) => {
1754
+ return routeYargs
770
1755
  .command(
771
1756
  "list",
772
- "List a route associated with a zone",
1757
+ "List the routes associated with a zone",
773
1758
  (yargs) => {
774
1759
  return yargs
775
1760
  .option("env", {
776
1761
  type: "string",
1762
+ requiresArg: true,
777
1763
  describe: "Perform on a specific environment",
778
1764
  })
779
1765
  .option("zone", {
780
1766
  type: "string",
781
- describe: "zone id",
1767
+ requiresArg: true,
1768
+ describe: "Zone id",
782
1769
  })
783
1770
  .positional("zone", {
784
- describe: "zone id",
1771
+ describe: "Zone id",
785
1772
  type: "string",
786
1773
  });
787
1774
  },
788
- async (args) => {
789
- console.log(":route list", args);
790
- // TODO: use environment (current wrangler doesn't do so?)
791
- const zone = args.zone || (args.config as Config).zone_id;
792
- if (!zone) {
793
- console.error("missing zone id");
794
- return;
795
- }
796
-
797
- console.log(await cfetch(`/zones/${zone}/workers/routes`));
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
+ );
798
1786
  }
799
1787
  )
800
1788
  .command(
801
- "delete <id>",
1789
+ "delete [id]",
802
1790
  "Delete a route associated with a zone",
803
1791
  (yargs) => {
804
1792
  return yargs
@@ -808,33 +1796,36 @@ export async function main(argv: string[]): Promise<void> {
808
1796
  })
809
1797
  .option("zone", {
810
1798
  type: "string",
1799
+ requiresArg: true,
811
1800
  describe: "zone id",
812
1801
  })
813
1802
  .option("env", {
814
1803
  type: "string",
1804
+ requiresArg: true,
815
1805
  describe: "Perform on a specific environment",
816
1806
  });
817
1807
  },
818
- async (args) => {
819
- console.log(":route delete", args);
820
- // TODO: use environment (current wrangler doesn't do so?)
821
- const zone = args.zone || (args.config as Config).zone_id;
822
- if (!zone) {
823
- throw new Error("missing zone id");
824
- }
825
-
826
- console.log(
827
- await cfetch(`/zones/${zone}/workers/routes/${args.id}`, {
828
- method: "DELETE",
829
- })
830
- );
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}`);
831
1815
  }
832
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}`);
833
1824
  }
834
1825
  );
835
1826
 
836
1827
  // subdomain
837
- yargs.command(
1828
+ wrangler.command(
838
1829
  "subdomain [name]",
839
1830
  false,
840
1831
  // "👷 Create or change your workers.dev subdomain.",
@@ -842,182 +1833,202 @@ export async function main(argv: string[]): Promise<void> {
842
1833
  return yargs.positional("name", { type: "string" });
843
1834
  },
844
1835
  () => {
845
- console.error(
846
- "`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"
847
1838
  );
848
1839
  }
849
1840
  );
850
1841
 
851
1842
  // secret
852
- yargs.command(
1843
+ wrangler.command(
853
1844
  "secret",
854
1845
  "🤫 Generate a secret that can be referenced in the worker script",
855
- (yargs) => {
856
- 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
+ })
857
1854
  .command(
858
1855
  "put <key>",
859
1856
  "Create or update a secret variable for a script",
860
1857
  (yargs) => {
861
1858
  return yargs
862
1859
  .positional("key", {
863
- describe: "The variable name to be accessible in the script.",
1860
+ describe: "The variable name to be accessible in the script",
864
1861
  type: "string",
865
1862
  })
866
1863
  .option("name", {
867
- describe: "name of the script",
1864
+ describe: "Name of the worker",
868
1865
  type: "string",
1866
+ requiresArg: true,
869
1867
  })
870
1868
  .option("env", {
871
1869
  type: "string",
1870
+ requiresArg: true,
872
1871
  describe:
873
- "Binds the secret to the script of the specific environment.",
1872
+ "Binds the secret to the Worker of the specific environment",
1873
+ alias: "e",
874
1874
  });
875
1875
  },
876
1876
  async (args) => {
877
- if (args.local) {
878
- console.error("--local not implemented for this command yet");
879
- return;
880
- }
881
- const config = args.config as Config;
1877
+ await printWranglerBanner();
1878
+ const config = readConfig(args.config as ConfigPath, args);
882
1879
 
883
- // TODO: use environment (how does current wrangler do it?)
884
- const scriptName = args.name || config.name;
1880
+ const scriptName = getLegacyScriptName(args, config);
885
1881
  if (!scriptName) {
886
- console.error("Missing script name");
887
- return;
1882
+ throw new Error("Missing script name");
888
1883
  }
889
1884
 
890
- // -- snip, extract --
891
-
892
- if (!args.local) {
893
- const loggedIn = await loginOrRefreshIfRequired();
894
- if (!loggedIn) {
895
- // didn't login, let's just quit
896
- console.log("Did not login, quitting...");
897
- return;
898
- }
899
- if (!config.account_id) {
900
- config.account_id = await getAccountId();
901
- if (!config.account_id) {
902
- console.error("No account id found, quitting...");
903
- return;
904
- }
905
- }
906
- }
1885
+ const accountId = await requireAuth(config);
907
1886
 
908
- // -- 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
+ );
909
1893
 
910
- const secretValue = await prompt(
911
- "Enter a secret value:",
912
- "password"
1894
+ logger.log(
1895
+ `🌀 Creating the secret for script ${scriptName} ${
1896
+ args.env && !isLegacyEnv(config) ? `(${args.env})` : ""
1897
+ }`
913
1898
  );
1899
+
914
1900
  async function submitSecret() {
915
- return await cfetch(
916
- `/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}`,
917
1923
  {
918
1924
  method: "PUT",
919
- headers: { "Content-Type": "application/json" },
920
- body: JSON.stringify({
921
- name: args.key,
922
- text: secretValue,
923
- 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,
924
1948
  }),
925
1949
  }
926
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
+ );
927
1959
  }
928
1960
 
929
1961
  try {
930
- console.log(await submitSecret());
1962
+ await submitSecret();
931
1963
  } catch (e) {
932
- if (e.code === 10007) {
933
- // upload a draft worker
934
- await cfetch(
935
- `/accounts/${config.account_id}/workers/scripts/${scriptName}`,
936
- {
937
- method: "PUT",
938
- // @ts-expect-error TODO: fix this error!
939
- body: toFormData({
940
- main: {
941
- name: scriptName,
942
- content: `export default { fetch() {} }`,
943
- type: "esm",
944
- },
945
- variables: {},
946
- modules: [],
947
- }),
948
- }
949
- );
950
-
951
- // and then try again
952
- console.log(await submitSecret());
1964
+ if (isMissingWorkerError(e)) {
1965
+ // create a draft worker and try again
1966
+ await createDraftWorker();
1967
+ await submitSecret();
953
1968
  // TODO: delete the draft worker if this failed too?
1969
+ } else {
1970
+ throw e;
954
1971
  }
955
1972
  }
1973
+
1974
+ logger.log(`✨ Success! Uploaded secret ${args.key}`);
956
1975
  }
957
1976
  )
958
1977
  .command(
959
1978
  "delete <key>",
960
1979
  "Delete a secret variable from a script",
961
- (yargs) => {
1980
+ async (yargs) => {
1981
+ await printWranglerBanner();
962
1982
  return yargs
963
1983
  .positional("key", {
964
- describe: "The variable name to be accessible in the script.",
1984
+ describe: "The variable name to be accessible in the script",
965
1985
  type: "string",
966
1986
  })
967
1987
  .option("name", {
968
- describe: "name of the script",
1988
+ describe: "Name of the worker",
969
1989
  type: "string",
1990
+ requiresArg: true,
970
1991
  })
971
1992
  .option("env", {
972
1993
  type: "string",
1994
+ requiresArg: true,
973
1995
  describe:
974
- "Binds the secret to the script of the specific environment.",
1996
+ "Binds the secret to the Worker of the specific environment",
1997
+ alias: "e",
975
1998
  });
976
1999
  },
977
2000
  async (args) => {
978
- if (args.local) {
979
- console.error("--local not implemented for this command yet");
980
- return;
981
- }
982
- const config = args.config as Config;
2001
+ const config = readConfig(args.config as ConfigPath, args);
983
2002
 
984
- // TODO: use environment (how does current wrangler do it?)
985
- const scriptName = args.name || config.name;
2003
+ const scriptName = getLegacyScriptName(args, config);
986
2004
  if (!scriptName) {
987
2005
  throw new Error("Missing script name");
988
2006
  }
989
2007
 
990
- // -- snip, extract --
991
-
992
- if (!args.local) {
993
- const loggedIn = await loginOrRefreshIfRequired();
994
- if (!loggedIn) {
995
- // didn't login, let's just quit
996
- console.log("Did not login, quitting...");
997
- return;
998
- }
999
- if (!config.account_id) {
1000
- config.account_id = await getAccountId();
1001
- if (!config.account_id) {
1002
- console.error("No account id found, quitting...");
1003
- return;
1004
- }
1005
- }
1006
- }
1007
-
1008
- // -- snip, end --
2008
+ const accountId = await requireAuth(config);
1009
2009
 
1010
- if (await confirm("Are you sure you want to delete this secret?")) {
1011
- console.log(
1012
- `Deleting the secret ${args.key} on script ${scriptName}.`
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
+ }`
1013
2023
  );
1014
2024
 
1015
- console.log(
1016
- await cfetch(
1017
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets/${args.key}`,
1018
- { method: "DELETE" }
1019
- )
1020
- );
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}`);
1021
2032
  }
1022
2033
  }
1023
2034
  )
@@ -1027,54 +2038,34 @@ export async function main(argv: string[]): Promise<void> {
1027
2038
  (yargs) => {
1028
2039
  return yargs
1029
2040
  .option("name", {
1030
- describe: "name of the script",
2041
+ describe: "Name of the worker",
1031
2042
  type: "string",
2043
+ requiresArg: true,
1032
2044
  })
1033
2045
  .option("env", {
1034
2046
  type: "string",
2047
+ requiresArg: true,
1035
2048
  describe:
1036
- "Binds the secret to the script of the specific environment.",
2049
+ "Binds the secret to the Worker of the specific environment.",
2050
+ alias: "e",
1037
2051
  });
1038
2052
  },
1039
- async (args) => {
1040
- if (args.local) {
1041
- console.error("--local not implemented for this command yet");
1042
- return;
1043
- }
1044
- const config = args.config as Config;
1045
-
1046
- // TODO: use environment (how does current wrangler do it?)
1047
- const scriptName = args.name || config.name;
1048
- if (!scriptName) {
1049
- console.error("Missing script name");
1050
- return;
1051
- }
1052
-
1053
- // -- snip, extract --
1054
-
1055
- if (!args.local) {
1056
- const loggedIn = await loginOrRefreshIfRequired();
1057
- if (!loggedIn) {
1058
- // didn't login, let's just quit
1059
- console.log("Did not login, quitting...");
1060
- return;
1061
- }
1062
- if (!config.account_id) {
1063
- config.account_id = await getAccountId();
1064
- if (!config.account_id) {
1065
- console.error("No account id found, quitting...");
1066
- return;
1067
- }
1068
- }
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");
1069
2059
  }
1070
2060
 
1071
- // -- snip, end --
2061
+ const accountId = await requireAuth(config);
1072
2062
 
1073
- console.log(
1074
- await cfetch(
1075
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1076
- )
1077
- );
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, " "));
1078
2069
  }
1079
2070
  );
1080
2071
  }
@@ -1082,11 +2073,12 @@ export async function main(argv: string[]): Promise<void> {
1082
2073
 
1083
2074
  // kv
1084
2075
  // :namespace
1085
- yargs.command(
2076
+ wrangler.command(
1086
2077
  "kv:namespace",
1087
2078
  "🗂️ Interact with your Workers KV Namespaces",
1088
- (yargs) => {
1089
- return yargs
2079
+ (namespaceYargs) => {
2080
+ return namespaceYargs
2081
+ .command(subHelp)
1090
2082
  .command(
1091
2083
  "create <namespace>",
1092
2084
  "Create a new namespace",
@@ -1095,10 +2087,13 @@ export async function main(argv: string[]): Promise<void> {
1095
2087
  .positional("namespace", {
1096
2088
  describe: "The name of the new namespace",
1097
2089
  type: "string",
2090
+ demandOption: true,
1098
2091
  })
1099
2092
  .option("env", {
1100
2093
  type: "string",
2094
+ requiresArg: true,
1101
2095
  describe: "Perform on a specific environment",
2096
+ alias: "e",
1102
2097
  })
1103
2098
  .option("preview", {
1104
2099
  type: "boolean",
@@ -1106,88 +2101,44 @@ export async function main(argv: string[]): Promise<void> {
1106
2101
  });
1107
2102
  },
1108
2103
  async (args) => {
1109
- if (args._.length !== 2) {
1110
- throw new Error(
1111
- `Did you forget to add quotes around "${
1112
- args.namespace
1113
- } ${args._.slice(2).join(" ")}"?`
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.`
1114
2109
  );
1115
2110
  }
1116
- const config = args.config as Config;
2111
+
2112
+ const config = readConfig(args.config as ConfigPath, args);
1117
2113
  if (!config.name) {
1118
- console.warn(
2114
+ logger.warn(
1119
2115
  "No configured name present, using `worker` as a prefix for the title"
1120
2116
  );
1121
2117
  }
1122
2118
 
1123
- const title = `${config.name || "worker"}${
1124
- args.env ? `-${args.env}` : ""
1125
- }-${args.namespace}${args.preview ? "_preview" : ""}`;
1126
-
1127
- if (/[\W]+/.test(args.namespace)) {
1128
- throw new Error("invalid binding name, needs to be js friendly");
1129
- }
1130
-
1131
- if (args.local) {
1132
- const { Miniflare } = await import("miniflare");
1133
- const mf = new Miniflare({
1134
- kvPersist: (args.kvPersist as string) || true,
1135
- // TODO: these options shouldn't be required
1136
- script: ` `, // has to be a string with at least one char
1137
- });
1138
- mf.getKVNamespace(title); // this should "create" the namespace
1139
- console.log(`✨ Success! Created KV namespace ${title}`);
1140
- return;
1141
- }
1142
-
1143
- // -- snip, extract --
1144
-
1145
- if (!args.local) {
1146
- const loggedIn = await loginOrRefreshIfRequired();
1147
- if (!loggedIn) {
1148
- // didn't login, let's just quit
1149
- console.log("Did not login, quitting...");
1150
- return;
1151
- }
1152
- if (!config.account_id) {
1153
- config.account_id = await getAccountId();
1154
- if (!config.account_id) {
1155
- console.error("No account id found, quitting...");
1156
- return;
1157
- }
1158
- }
1159
- }
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}`;
1160
2123
 
1161
- // -- snip, end --
2124
+ const accountId = await requireAuth(config);
1162
2125
 
1163
2126
  // TODO: generate a binding name stripping non alphanumeric chars
1164
2127
 
1165
- console.log(`🌀 Creating namespace with title "${title}"`);
2128
+ logger.log(`🌀 Creating namespace with title "${title}"`);
2129
+ const namespaceId = await createKVNamespace(accountId, title);
1166
2130
 
1167
- const response = await cfetch<{ id: string }>(
1168
- `/accounts/${config.account_id}/storage/kv/namespaces`,
1169
- {
1170
- method: "POST",
1171
- headers: {
1172
- "Content-Type": "application/json",
1173
- },
1174
- body: JSON.stringify({
1175
- title,
1176
- }),
1177
- }
1178
- );
1179
-
1180
- console.log("✨ Success!");
1181
- console.log(
1182
- `Add the following to your configuration file in your kv_namespaces array${
1183
- args.env ? ` under [env.${args.env}]` : ""
1184
- }:`
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}:`
1185
2136
  );
1186
- console.log(
1187
- `{ binding = "${args.namespace}", ${
1188
- args.preview ? "preview_" : ""
1189
- }id = "${response.id}" }`
2137
+ logger.log(
2138
+ `{ binding = "${args.namespace}", ${previewString}id = "${namespaceId}" }`
1190
2139
  );
2140
+
2141
+ // TODO: automatically write this block to the wrangler.toml config file??
1191
2142
  }
1192
2143
  )
1193
2144
  .command(
@@ -1195,41 +2146,14 @@ export async function main(argv: string[]): Promise<void> {
1195
2146
  "Outputs a list of all KV namespaces associated with your account id.",
1196
2147
  {},
1197
2148
  async (args) => {
1198
- if (args.local) {
1199
- console.error(`local mode is not yet supported for this command`);
1200
- return;
1201
- }
1202
-
1203
- const config = args.config as Config;
1204
-
1205
- // -- snip, extract --
1206
-
1207
- if (!args.local) {
1208
- const loggedIn = await loginOrRefreshIfRequired();
1209
- if (!loggedIn) {
1210
- // didn't login, let's just quit
1211
- console.log("Did not login, quitting...");
1212
- return;
1213
- }
1214
- if (!config.account_id) {
1215
- config.account_id = await getAccountId();
1216
- if (!config.account_id) {
1217
- console.error("No account id found, quitting...");
1218
- return;
1219
- }
1220
- }
1221
- }
2149
+ const config = readConfig(args.config as ConfigPath, args);
1222
2150
 
1223
- // -- snip, end --
2151
+ const accountId = await requireAuth(config);
1224
2152
 
1225
2153
  // TODO: we should show bindings if they exist for given ids
1226
2154
 
1227
- console.log(
1228
- JSON.stringify(
1229
- await listNamespaces(config.account_id),
1230
- null,
1231
- " "
1232
- )
2155
+ logger.log(
2156
+ JSON.stringify(await listKVNamespaces(accountId), null, " ")
1233
2157
  );
1234
2158
  }
1235
2159
  )
@@ -1240,16 +2164,20 @@ export async function main(argv: string[]): Promise<void> {
1240
2164
  return yargs
1241
2165
  .option("binding", {
1242
2166
  type: "string",
2167
+ requiresArg: true,
1243
2168
  describe: "The name of the namespace to delete",
1244
2169
  })
1245
2170
  .option("namespace-id", {
1246
2171
  type: "string",
2172
+ requiresArg: true,
1247
2173
  describe: "The id of the namespace to delete",
1248
2174
  })
1249
2175
  .check(demandOneOfOption("binding", "namespace-id"))
1250
2176
  .option("env", {
1251
2177
  type: "string",
2178
+ requiresArg: true,
1252
2179
  describe: "Perform on a specific environment",
2180
+ alias: "e",
1253
2181
  })
1254
2182
  .option("preview", {
1255
2183
  type: "boolean",
@@ -1257,49 +2185,21 @@ export async function main(argv: string[]): Promise<void> {
1257
2185
  });
1258
2186
  },
1259
2187
  async (args) => {
1260
- if (args.local) {
1261
- console.error(`local mode is not yet supported for this command`);
1262
- return;
1263
- }
1264
- const config = args.config as Config;
1265
-
1266
- const id =
1267
- args["namespace-id"] ||
1268
- (args.env
1269
- ? config.env[args.env] || {}
1270
- : config
1271
- ).kv_namespaces.find(
1272
- (namespace) => namespace.binding === args.binding
1273
- )[args.preview ? "preview_id" : "id"];
1274
- if (!id) {
1275
- throw new Error("Are you sure? id not found");
1276
- }
1277
-
1278
- // -- snip, extract --
1279
-
1280
- if (!args.local) {
1281
- const loggedIn = await loginOrRefreshIfRequired();
1282
- if (!loggedIn) {
1283
- // didn't login, let's just quit
1284
- console.log("Did not login, quitting...");
1285
- return;
1286
- }
2188
+ await printWranglerBanner();
2189
+ const config = readConfig(args.config as ConfigPath, args);
1287
2190
 
1288
- if (!config.account_id) {
1289
- config.account_id = await getAccountId();
1290
- if (!config.account_id) {
1291
- console.error("No account id found, quitting...");
1292
- return;
1293
- }
1294
- }
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
+ );
1295
2198
  }
1296
2199
 
1297
- // -- snip, end --
2200
+ const accountId = await requireAuth(config);
1298
2201
 
1299
- await cfetch<{ id: string }>(
1300
- `/accounts/${config.account_id}/storage/kv/namespaces/${id}`,
1301
- { method: "DELETE" }
1302
- );
2202
+ await deleteKVNamespace(accountId, id);
1303
2203
 
1304
2204
  // TODO: recommend they remove it from wrangler.toml
1305
2205
 
@@ -1324,36 +2224,42 @@ export async function main(argv: string[]): Promise<void> {
1324
2224
  );
1325
2225
 
1326
2226
  // :key
1327
- yargs.command(
2227
+ wrangler.command(
1328
2228
  "kv:key",
1329
2229
  "🔑 Individually manage Workers KV key-value pairs",
1330
- (yargs) => {
1331
- return yargs
2230
+ (kvKeyYargs) => {
2231
+ return kvKeyYargs
2232
+ .command(subHelp)
1332
2233
  .command(
1333
- "put <key> <value>",
2234
+ "put <key> [value]",
1334
2235
  "Writes a single key/value pair to the given namespace.",
1335
2236
  (yargs) => {
1336
2237
  return yargs
1337
2238
  .positional("key", {
1338
2239
  type: "string",
1339
- describe: "The key to write to.",
2240
+ describe: "The key to write to",
2241
+ demandOption: true,
1340
2242
  })
1341
2243
  .positional("value", {
1342
2244
  type: "string",
1343
- describe: "The value to write.",
2245
+ describe: "The value to write",
1344
2246
  })
1345
2247
  .option("binding", {
1346
2248
  type: "string",
1347
- describe: "The binding of the namespace to write to.",
2249
+ requiresArg: true,
2250
+ describe: "The binding of the namespace to write to",
1348
2251
  })
1349
2252
  .option("namespace-id", {
1350
2253
  type: "string",
1351
- describe: "The id of the namespace to write to.",
2254
+ requiresArg: true,
2255
+ describe: "The id of the namespace to write to",
1352
2256
  })
1353
2257
  .check(demandOneOfOption("binding", "namespace-id"))
1354
2258
  .option("env", {
1355
2259
  type: "string",
2260
+ requiresArg: true,
1356
2261
  describe: "Perform on a specific environment",
2262
+ alias: "e",
1357
2263
  })
1358
2264
  .option("preview", {
1359
2265
  type: "boolean",
@@ -1361,7 +2267,7 @@ export async function main(argv: string[]): Promise<void> {
1361
2267
  })
1362
2268
  .option("ttl", {
1363
2269
  type: "number",
1364
- describe: "Time for which the entries should be visible.",
2270
+ describe: "Time for which the entries should be visible",
1365
2271
  })
1366
2272
  .option("expiration", {
1367
2273
  type: "number",
@@ -1370,50 +2276,36 @@ export async function main(argv: string[]): Promise<void> {
1370
2276
  })
1371
2277
  .option("path", {
1372
2278
  type: "string",
1373
- describe: "Read value from the file at a given path.",
1374
- });
2279
+ requiresArg: true,
2280
+ describe: "Read value from the file at a given path",
2281
+ })
2282
+ .check(demandOneOfOption("value", "path"));
1375
2283
  },
1376
2284
  async ({ key, ttl, expiration, ...args }) => {
1377
- 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
1378
2289
  const value = args.path
1379
- ? await readFile(args.path, "utf-8")
1380
- : args.value;
1381
- const config = args.config as Config;
1382
-
1383
- console.log(`writing ${key}=${value} to namespace ${namespaceId}`);
1384
- if (args.local) {
1385
- const { Miniflare } = await import("miniflare");
1386
- const mf = new Miniflare({
1387
- kvPersist: (args.kvPersist as string) || true,
1388
- // TODO: these options shouldn't be required
1389
- script: ` `, // has to be a string with at least one char
1390
- });
1391
- const ns = await mf.getKVNamespace(namespaceId);
1392
- await ns.put(key, value, { expiration, expirationTtl: ttl });
1393
- return;
1394
- }
1395
- // -- snip, extract --
2290
+ ? readFileSyncToBuffer(args.path)
2291
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2292
+ args.value!;
1396
2293
 
1397
- if (!args.local) {
1398
- const loggedIn = await loginOrRefreshIfRequired();
1399
- if (!loggedIn) {
1400
- // didn't login, let's just quit
1401
- console.log("Did not login, quitting...");
1402
- return;
1403
- }
1404
-
1405
- if (!config.account_id) {
1406
- config.account_id = await getAccountId();
1407
- if (!config.account_id) {
1408
- console.error("No account id found, quitting...");
1409
- return;
1410
- }
1411
- }
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
+ );
1412
2302
  }
1413
2303
 
1414
- // -- snip, end --
2304
+ const accountId = await requireAuth(config);
1415
2305
 
1416
- await putKeyValue(config.account_id, namespaceId, key, value, {
2306
+ await putKVKeyValue(accountId, namespaceId, {
2307
+ key,
2308
+ value,
1417
2309
  expiration,
1418
2310
  expiration_ttl: ttl,
1419
2311
  });
@@ -1426,65 +2318,46 @@ export async function main(argv: string[]): Promise<void> {
1426
2318
  return yargs
1427
2319
  .option("binding", {
1428
2320
  type: "string",
2321
+ requiresArg: true,
1429
2322
  describe: "The name of the namespace to list",
1430
2323
  })
1431
2324
  .option("namespace-id", {
1432
2325
  type: "string",
2326
+ requiresArg: true,
1433
2327
  describe: "The id of the namespace to list",
1434
2328
  })
1435
2329
  .check(demandOneOfOption("binding", "namespace-id"))
1436
2330
  .option("env", {
1437
2331
  type: "string",
2332
+ requiresArg: true,
1438
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",
1439
2341
  })
1440
2342
  .option("prefix", {
1441
2343
  type: "string",
2344
+ requiresArg: true,
1442
2345
  describe: "A prefix to filter listed keys",
1443
2346
  });
1444
2347
  },
1445
2348
  async ({ prefix, ...args }) => {
1446
2349
  // TODO: support for limit+cursor (pagination)
2350
+ const config = readConfig(args.config as ConfigPath, args);
2351
+ const namespaceId = getKVNamespaceId(args, config);
1447
2352
 
1448
- const namespaceId = getNamespaceId(args);
1449
- const config = args.config as Config;
1450
-
1451
- if (args.local) {
1452
- const { Miniflare } = await import("miniflare");
1453
- const mf = new Miniflare({
1454
- kvPersist: (args.kvPersist as string) || true,
1455
- // TODO: these options shouldn't be required
1456
- script: ` `, // has to be a string with at least one char
1457
- });
1458
- const ns = await mf.getKVNamespace(namespaceId);
1459
- const listResponse = await ns.list({ prefix });
1460
- console.log(JSON.stringify(listResponse.keys, null, " ")); // TODO: paginate, collate
1461
- return;
1462
- }
1463
-
1464
- // -- snip, extract --
1465
-
1466
- if (!args.local) {
1467
- const loggedIn = await loginOrRefreshIfRequired();
1468
- if (!loggedIn) {
1469
- // didn't login, let's just quit
1470
- console.log("Did not login, quitting...");
1471
- return;
1472
- }
1473
-
1474
- if (!config.account_id) {
1475
- config.account_id = await getAccountId();
1476
- if (!config.account_id) {
1477
- console.error("No account id found, quitting...");
1478
- return;
1479
- }
1480
- }
1481
- }
1482
-
1483
- // -- snip, end --
2353
+ const accountId = await requireAuth(config);
1484
2354
 
1485
- console.log(
1486
- await listNamespaceKeys(config.account_id, namespaceId, prefix)
2355
+ const results = await listKVNamespaceKeys(
2356
+ accountId,
2357
+ namespaceId,
2358
+ prefix
1487
2359
  );
2360
+ logger.log(JSON.stringify(results, undefined, 2));
1488
2361
  }
1489
2362
  )
1490
2363
  .command(
@@ -1495,71 +2368,56 @@ export async function main(argv: string[]): Promise<void> {
1495
2368
  .positional("key", {
1496
2369
  describe: "The key value to get.",
1497
2370
  type: "string",
2371
+ demandOption: true,
1498
2372
  })
1499
2373
  .option("binding", {
1500
2374
  type: "string",
2375
+ requiresArg: true,
1501
2376
  describe: "The name of the namespace to get from",
1502
2377
  })
1503
2378
  .option("namespace-id", {
1504
2379
  type: "string",
2380
+ requiresArg: true,
1505
2381
  describe: "The id of the namespace to get from",
1506
2382
  })
1507
2383
  .check(demandOneOfOption("binding", "namespace-id"))
1508
2384
  .option("env", {
1509
2385
  type: "string",
2386
+ requiresArg: true,
1510
2387
  describe: "Perform on a specific environment",
2388
+ alias: "e",
2389
+ })
2390
+ .option("preview", {
2391
+ type: "boolean",
2392
+ describe: "Interact with a preview namespace",
1511
2393
  })
1512
2394
  .option("preview", {
1513
2395
  type: "boolean",
2396
+ // In the case of getting key values we will default to non-preview mode
2397
+ default: false,
1514
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",
1515
2404
  });
1516
2405
  },
1517
2406
  async ({ key, ...args }) => {
1518
- const namespaceId = getNamespaceId(args);
1519
- const config = args.config as Config;
1520
-
1521
- if (args.local) {
1522
- const { Miniflare } = await import("miniflare");
1523
- const mf = new Miniflare({
1524
- kvPersist: (args.kvPersist as string) || true,
1525
- // TODO: these options shouldn't be required
1526
- script: ` `, // has to be a string with at least one char
1527
- });
1528
- const ns = await mf.getKVNamespace(namespaceId);
1529
- console.log(await ns.get(key));
1530
- return;
1531
- }
1532
-
1533
- // -- snip, extract --
2407
+ const config = readConfig(args.config as ConfigPath, args);
2408
+ const namespaceId = getKVNamespaceId(args, config);
1534
2409
 
1535
- if (!args.local) {
1536
- const loggedIn = await loginOrRefreshIfRequired();
1537
- if (!loggedIn) {
1538
- // didn't login, let's just quit
1539
- console.log("Did not login, quitting...");
1540
- return;
1541
- }
2410
+ const accountId = await requireAuth(config);
2411
+ const bufferKVValue = Buffer.from(
2412
+ await getKVKeyValue(accountId, namespaceId, key)
2413
+ );
1542
2414
 
1543
- if (!config.account_id) {
1544
- config.account_id = await getAccountId();
1545
- if (!config.account_id) {
1546
- console.error("No account id found, quitting...");
1547
- return;
1548
- }
1549
- }
2415
+ if (args.text) {
2416
+ const decoder = new StringDecoder("utf8");
2417
+ logger.log(decoder.write(bufferKVValue));
2418
+ } else {
2419
+ process.stdout.write(bufferKVValue);
1550
2420
  }
1551
-
1552
- // -- snip, end --
1553
-
1554
- // annoyingly, the API for this one doesn't return the
1555
- // data in the 'standard' format. goddamit.
1556
- // That's why we have the fallthrough response in cfetch.
1557
- // Oh well.
1558
- console.log(
1559
- await cfetch(
1560
- `/accounts/${config.account_id}/storage/kv/namespaces/${namespaceId}/values/${key}`
1561
- )
1562
- );
1563
2421
  }
1564
2422
  )
1565
2423
  .command(
@@ -1570,19 +2428,24 @@ export async function main(argv: string[]): Promise<void> {
1570
2428
  .positional("key", {
1571
2429
  describe: "The key value to delete",
1572
2430
  type: "string",
2431
+ demandOption: true,
1573
2432
  })
1574
2433
  .option("binding", {
1575
2434
  type: "string",
2435
+ requiresArg: true,
1576
2436
  describe: "The name of the namespace to delete from",
1577
2437
  })
1578
2438
  .option("namespace-id", {
1579
2439
  type: "string",
2440
+ requiresArg: true,
1580
2441
  describe: "The id of the namespace to delete from",
1581
2442
  })
1582
2443
  .check(demandOneOfOption("binding", "namespace-id"))
1583
2444
  .option("env", {
1584
2445
  type: "string",
2446
+ requiresArg: true,
1585
2447
  describe: "Perform on a specific environment",
2448
+ alias: "e",
1586
2449
  })
1587
2450
  .option("preview", {
1588
2451
  type: "boolean",
@@ -1590,58 +2453,29 @@ export async function main(argv: string[]): Promise<void> {
1590
2453
  });
1591
2454
  },
1592
2455
  async ({ key, ...args }) => {
1593
- const namespaceId = getNamespaceId(args);
1594
-
1595
- if (args.local) {
1596
- const { Miniflare } = await import("miniflare");
1597
- const mf = new Miniflare({
1598
- kvPersist: (args.kvPersist as string) || true,
1599
- // TODO: these options shouldn't be required
1600
- script: ` `, // has to be a string with at least one char
1601
- });
1602
- const ns = await mf.getKVNamespace(namespaceId);
1603
- console.log(await ns.delete(key));
1604
- return;
1605
- }
1606
-
1607
- const config = args.config as Config;
2456
+ await printWranglerBanner();
2457
+ const config = readConfig(args.config as ConfigPath, args);
2458
+ const namespaceId = getKVNamespaceId(args, config);
1608
2459
 
1609
- // -- snip, extract --
1610
-
1611
- if (!args.local) {
1612
- const loggedIn = await loginOrRefreshIfRequired();
1613
- if (!loggedIn) {
1614
- // didn't login, let's just quit
1615
- console.log("Did not login, quitting...");
1616
- return;
1617
- }
1618
-
1619
- if (!config.account_id) {
1620
- config.account_id = await getAccountId();
1621
- if (!config.account_id) {
1622
- console.error("No account id found, quitting...");
1623
- return;
1624
- }
1625
- }
1626
- }
2460
+ logger.log(
2461
+ `Deleting the key "${key}" on namespace ${namespaceId}.`
2462
+ );
1627
2463
 
1628
- // -- snip, end --
2464
+ const accountId = await requireAuth(config);
1629
2465
 
1630
- await cfetch(
1631
- `/accounts/${config.account_id}/storage/kv/namespaces/${namespaceId}/values/${key}`,
1632
- { method: "DELETE" }
1633
- );
2466
+ await deleteKVKeyValue(accountId, namespaceId, key);
1634
2467
  }
1635
2468
  );
1636
2469
  }
1637
2470
  );
1638
2471
 
1639
2472
  // :bulk
1640
- yargs.command(
2473
+ wrangler.command(
1641
2474
  "kv:bulk",
1642
2475
  "💪 Interact with multiple Workers KV key-value pairs at once",
1643
- (yargs) => {
1644
- return yargs
2476
+ (kvBulkYargs) => {
2477
+ return kvBulkYargs
2478
+ .command(subHelp)
1645
2479
  .command(
1646
2480
  "put <filename>",
1647
2481
  "Upload multiple key-value pairs to a namespace",
@@ -1650,19 +2484,24 @@ export async function main(argv: string[]): Promise<void> {
1650
2484
  .positional("filename", {
1651
2485
  describe: `The JSON file of key-value pairs to upload, in form [{"key":..., "value":...}"...]`,
1652
2486
  type: "string",
2487
+ demandOption: true,
1653
2488
  })
1654
2489
  .option("binding", {
1655
2490
  type: "string",
1656
- describe: "The name of the namespace to put to",
2491
+ requiresArg: true,
2492
+ describe: "The name of the namespace to insert values into",
1657
2493
  })
1658
2494
  .option("namespace-id", {
1659
2495
  type: "string",
1660
- describe: "The id of the namespace to put to",
2496
+ requiresArg: true,
2497
+ describe: "The id of the namespace to insert values into",
1661
2498
  })
1662
2499
  .check(demandOneOfOption("binding", "namespace-id"))
1663
2500
  .option("env", {
1664
2501
  type: "string",
2502
+ requiresArg: true,
1665
2503
  describe: "Perform on a specific environment",
2504
+ alias: "e",
1666
2505
  })
1667
2506
  .option("preview", {
1668
2507
  type: "boolean",
@@ -1670,176 +2509,400 @@ export async function main(argv: string[]): Promise<void> {
1670
2509
  });
1671
2510
  },
1672
2511
  async ({ filename, ...args }) => {
2512
+ await printWranglerBanner();
1673
2513
  // The simplest implementation I could think of.
1674
2514
  // This could be made more efficient with a streaming parser/uploader
1675
2515
  // but we'll do that in the future if needed.
1676
2516
 
1677
- const namespaceId = getNamespaceId(args);
1678
- const config = args.config as Config;
1679
- const content = await readFile(filename, "utf-8");
1680
- let parsedContent;
1681
- try {
1682
- parsedContent = JSON.parse(content);
1683
- } catch (err) {
1684
- console.error(`could not parse json from ${filename}`);
1685
- throw err;
1686
- }
1687
-
1688
- if (args.local) {
1689
- const { Miniflare } = await import("miniflare");
1690
- const mf = new Miniflare({
1691
- kvPersist: (args.kvPersist as string) || true,
1692
- // TODO: these options shouldn't be required
1693
- script: ` `, // has to be a string with at least one char
1694
- });
1695
- const ns = await mf.getKVNamespace(namespaceId);
1696
- for (const {
1697
- key,
1698
- value,
1699
- expiration,
1700
- expiration_ttl,
1701
- } of parsedContent) {
1702
- await ns.put(key, value, {
1703
- expiration,
1704
- expirationTtl: expiration_ttl,
1705
- });
1706
- }
2517
+ const config = readConfig(args.config as ConfigPath, args);
2518
+ const namespaceId = getKVNamespaceId(args, config);
2519
+ const content = parseJSON(readFileSync(filename), filename);
1707
2520
 
1708
- 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
+ );
1709
2526
  }
1710
2527
 
1711
- // -- snip, extract --
1712
-
1713
- if (!args.local) {
1714
- const loggedIn = await loginOrRefreshIfRequired();
1715
- if (!loggedIn) {
1716
- // didn't login, let's just quit
1717
- console.log("Did not login, quitting...");
1718
- return;
1719
- }
1720
-
1721
- if (!config.account_id) {
1722
- config.account_id = await getAccountId();
1723
- if (!config.account_id) {
1724
- console.error("No account id found, quitting...");
1725
- return;
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
+ );
1726
2544
  }
1727
2545
  }
1728
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
+ }
1729
2568
 
1730
- // -- snip, end --
2569
+ const accountId = await requireAuth(config);
2570
+ await putKVBulkKeyValue(accountId, namespaceId, content);
1731
2571
 
1732
- console.log(
1733
- await putBulkKeyValue(config.account_id, namespaceId, content)
1734
- );
2572
+ logger.log("Success!");
1735
2573
  }
1736
2574
  )
1737
2575
  .command(
1738
2576
  "delete <filename>",
1739
- "Upload multiple key-value pairs to a namespace",
2577
+ "Delete multiple key-value pairs from a namespace",
1740
2578
  (yargs) => {
1741
2579
  return yargs
1742
2580
  .positional("filename", {
1743
- 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", ...]`,
1744
2582
  type: "string",
2583
+ demandOption: true,
1745
2584
  })
1746
2585
  .option("binding", {
1747
2586
  type: "string",
2587
+ requiresArg: true,
1748
2588
  describe: "The name of the namespace to delete from",
1749
2589
  })
1750
2590
  .option("namespace-id", {
1751
2591
  type: "string",
2592
+ requiresArg: true,
1752
2593
  describe: "The id of the namespace to delete from",
1753
2594
  })
1754
2595
  .check(demandOneOfOption("binding", "namespace-id"))
1755
2596
  .option("env", {
1756
2597
  type: "string",
2598
+ requiresArg: true,
1757
2599
  describe: "Perform on a specific environment",
2600
+ alias: "e",
1758
2601
  })
1759
2602
  .option("preview", {
1760
2603
  type: "boolean",
1761
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",
1762
2610
  });
1763
2611
  },
1764
2612
  async ({ filename, ...args }) => {
1765
- const namespaceId = getNamespaceId(args);
1766
- const config = args.config as Config;
1767
- const content = await readFile(filename, "utf-8");
1768
- let parsedContent;
1769
- try {
1770
- parsedContent = JSON.parse(content);
1771
- } catch (err) {
1772
- console.error(`could not parse json from ${filename}`);
1773
- throw err;
1774
- }
2613
+ await printWranglerBanner();
2614
+ const config = readConfig(args.config as ConfigPath, args);
2615
+ const namespaceId = getKVNamespaceId(args, config);
1775
2616
 
1776
- if (args.local) {
1777
- const { Miniflare } = await import("miniflare");
1778
- const mf = new Miniflare({
1779
- kvPersist: (args.kvPersist as string) || true,
1780
- // TODO: these options shouldn't be required
1781
- script: ` `, // has to be a string with at least one char
1782
- });
1783
- const ns = await mf.getKVNamespace(namespaceId);
1784
- for (const key of parsedContent) {
1785
- await ns.delete(key);
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;
1786
2624
  }
1787
-
1788
- return;
1789
2625
  }
1790
2626
 
1791
- // -- snip, extract --
2627
+ const content = parseJSON(
2628
+ readFileSync(filename),
2629
+ filename
2630
+ ) as string[];
1792
2631
 
1793
- if (!args.local) {
1794
- const loggedIn = await loginOrRefreshIfRequired();
1795
- if (!loggedIn) {
1796
- // didn't login, let's just quit
1797
- console.log("Did not login, quitting...");
1798
- return;
1799
- }
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
+ }
1800
2638
 
1801
- if (!config.account_id) {
1802
- config.account_id = await getAccountId();
1803
- if (!config.account_id) {
1804
- console.error("No account id found, quitting...");
1805
- return;
1806
- }
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
+ );
1807
2648
  }
1808
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
+ }
1809
2657
 
1810
- // -- snip, end --
2658
+ const accountId = await requireAuth(config);
1811
2659
 
1812
- console.log(
1813
- await deleteBulkKeyValue(config.account_id, namespaceId, content)
1814
- );
2660
+ await deleteKVBulkKeyValue(accountId, namespaceId, content);
2661
+
2662
+ logger.log("Success!");
1815
2663
  }
1816
2664
  );
1817
2665
  }
1818
2666
  );
1819
2667
 
1820
- 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
+ );
1821
2675
 
1822
- yargs
1823
- .option("config", {
1824
- describe: "Path to .toml configuration file",
1825
- type: "string",
1826
- async coerce(arg) {
1827
- return await readConfig(arg);
1828
- },
1829
- })
1830
- .option("local", {
1831
- describe: "Run on my machine",
1832
- type: "boolean",
1833
- default: false, // I bet this will a point of contention. We'll revisit it.
1834
- });
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
+ );
1835
2789
 
1836
- yargs.group(["config", "help", "version"], "Flags:");
1837
- yargs.version(wranglerVersion);
1838
- 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
+ );
1839
2801
 
1840
- await yargs.parse(argv, (err, argv, output) => {
1841
- if (output) {
1842
- 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();
1843
2810
  }
2811
+ );
2812
+
2813
+ wrangler.option("config", {
2814
+ alias: "c",
2815
+ describe: "Path to .toml configuration file",
2816
+ type: "string",
2817
+ requiresArg: true,
1844
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
+ }
1845
2908
  }