wrangler 0.0.0-e2a26a0 → 0.0.0-e322475

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