wrangler 0.0.0-e2f9bb2 → 0.0.0-e305903a

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