wrangler 2.0.12 → 2.0.16

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