wrangler 0.0.0-e043479 → 0.0.0-e09abbc

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