wrangler 0.0.0-e1c615f → 0.0.0-e3cab74

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 (85) hide show
  1. package/bin/wrangler.js +2 -2
  2. package/package.json +28 -13
  3. package/pages/functions/buildWorker.ts +6 -1
  4. package/pages/functions/filepath-routing.test.ts +112 -28
  5. package/pages/functions/filepath-routing.ts +88 -131
  6. package/pages/functions/routes.ts +11 -18
  7. package/pages/functions/template-worker.ts +21 -28
  8. package/src/__tests__/configuration.test.ts +2185 -0
  9. package/src/__tests__/dev.test.tsx +56 -9
  10. package/src/__tests__/guess-worker-format.test.ts +66 -0
  11. package/src/__tests__/{clipboardy-mock.js → helpers/clipboardy-mock.js} +0 -0
  12. package/src/__tests__/helpers/cmd-shim.d.ts +11 -0
  13. package/src/__tests__/helpers/faye-websocket.d.ts +6 -0
  14. package/src/__tests__/helpers/mock-account-id.ts +30 -0
  15. package/src/__tests__/helpers/mock-bin.ts +36 -0
  16. package/src/__tests__/{mock-cfetch.ts → helpers/mock-cfetch.ts} +53 -17
  17. package/src/__tests__/helpers/mock-console.ts +62 -0
  18. package/src/__tests__/{mock-dialogs.ts → helpers/mock-dialogs.ts} +1 -1
  19. package/src/__tests__/helpers/mock-kv.ts +40 -0
  20. package/src/__tests__/helpers/mock-user.ts +27 -0
  21. package/src/__tests__/helpers/mock-web-socket.ts +29 -0
  22. package/src/__tests__/helpers/run-in-tmp.ts +29 -0
  23. package/src/__tests__/helpers/run-wrangler.ts +16 -0
  24. package/src/__tests__/helpers/write-wrangler-toml.ts +17 -0
  25. package/src/__tests__/index.test.ts +655 -76
  26. package/src/__tests__/jest.setup.ts +37 -2
  27. package/src/__tests__/kv.test.ts +441 -398
  28. package/src/__tests__/logout.test.ts +50 -0
  29. package/src/__tests__/package-manager.test.ts +206 -0
  30. package/src/__tests__/parse.test.ts +322 -0
  31. package/src/__tests__/publish.test.ts +2406 -0
  32. package/src/__tests__/r2.test.ts +198 -0
  33. package/src/__tests__/secret.test.ts +326 -0
  34. package/src/__tests__/sentry.test.ts +146 -0
  35. package/src/__tests__/tail.test.ts +564 -0
  36. package/src/__tests__/whoami.test.tsx +86 -0
  37. package/src/api/form_data.ts +112 -26
  38. package/src/api/preview.ts +34 -15
  39. package/src/api/worker.ts +58 -27
  40. package/src/bundle.ts +133 -0
  41. package/src/cfetch/index.ts +21 -19
  42. package/src/cfetch/internal.ts +85 -21
  43. package/src/cli.ts +10 -0
  44. package/src/config/README.md +107 -0
  45. package/src/config/config.ts +265 -0
  46. package/src/config/diagnostics.ts +80 -0
  47. package/src/config/environment.ts +305 -0
  48. package/src/config/index.ts +52 -0
  49. package/src/config/validation-helpers.ts +370 -0
  50. package/src/config/validation.ts +1017 -0
  51. package/src/dev.tsx +455 -368
  52. package/src/dialogs.tsx +2 -2
  53. package/src/entry.ts +57 -0
  54. package/src/environment-variables.ts +33 -0
  55. package/src/guess-worker-format.ts +68 -0
  56. package/src/index.tsx +1044 -646
  57. package/src/inspect.ts +609 -0
  58. package/src/kv.tsx +47 -39
  59. package/src/module-collection.ts +142 -36
  60. package/src/open-in-browser.ts +13 -0
  61. package/src/package-manager.ts +126 -0
  62. package/src/pages.tsx +139 -62
  63. package/src/parse.ts +183 -0
  64. package/src/paths.ts +26 -0
  65. package/src/proxy.ts +350 -77
  66. package/src/publish.ts +307 -289
  67. package/src/r2.ts +50 -0
  68. package/src/reporting.ts +115 -0
  69. package/src/sites.tsx +183 -56
  70. package/src/tail/filters.ts +279 -0
  71. package/src/tail/index.ts +299 -0
  72. package/src/tail/printing.ts +62 -0
  73. package/src/user.tsx +137 -110
  74. package/src/whoami.tsx +69 -0
  75. package/templates/new-worker.js +15 -0
  76. package/templates/new-worker.ts +15 -0
  77. package/{static-asset-facade.js → templates/static-asset-facade.js} +0 -0
  78. package/templates/tsconfig.json +105 -0
  79. package/wrangler-dist/cli.js +127861 -111486
  80. package/wrangler-dist/cli.js.map +3 -3
  81. package/src/__tests__/run-in-tmp.ts +0 -19
  82. package/src/__tests__/run-wrangler.ts +0 -32
  83. package/src/api/inspect.ts +0 -430
  84. package/src/config.ts +0 -131
  85. package/src/tail.tsx +0 -73
package/src/index.tsx CHANGED
@@ -1,22 +1,21 @@
1
- import React from "react";
1
+ import * as fs from "node:fs";
2
+ import { writeFile, mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { setTimeout } from "node:timers/promises";
5
+ import TOML from "@iarna/toml";
6
+ import { findUp } from "find-up";
7
+ import getPort from "get-port";
2
8
  import { render } from "ink";
3
- import Dev from "./dev";
4
- import { readFile } from "node:fs/promises";
9
+ import React from "react";
10
+ import onExit from "signal-exit";
5
11
  import makeCLI from "yargs";
6
- import type Yargs from "yargs";
7
- import { findUp } from "find-up";
8
- import TOML from "@iarna/toml";
9
- import type { Config } from "./config";
10
- import { confirm, prompt } from "./dialogs";
11
12
  import { version as wranglerVersion } from "../package.json";
12
- import {
13
- login,
14
- logout,
15
- listScopes,
16
- initialise as initialiseUserConfig,
17
- loginOrRefreshIfRequired,
18
- getAccountId,
19
- } from "./user";
13
+ import { toFormData } from "./api/form_data";
14
+ import { fetchResult } from "./cfetch";
15
+ import { findWranglerToml, readConfig } from "./config";
16
+ import Dev from "./dev";
17
+ import { confirm, prompt } from "./dialogs";
18
+ import { getEntry } from "./entry";
20
19
  import {
21
20
  getNamespaceId,
22
21
  listNamespaces,
@@ -26,92 +25,79 @@ import {
26
25
  deleteBulkKeyValue,
27
26
  createNamespace,
28
27
  isValidNamespaceBinding,
28
+ getKeyValue,
29
29
  } from "./kv";
30
-
30
+ import { getPackageManager } from "./package-manager";
31
31
  import { pages } from "./pages";
32
-
33
- import { fetchResult, fetchRaw } from "./cfetch";
34
-
32
+ import { formatMessage, ParseError, parseJSON, readFileSync } from "./parse";
35
33
  import publish from "./publish";
36
- import path from "path/posix";
37
- import { writeFile } from "node:fs/promises";
38
- import { toFormData } from "./api/form_data";
34
+ import { createR2Bucket, deleteR2Bucket, listR2Buckets } from "./r2";
35
+ import { getAssetPaths } from "./sites";
36
+ import {
37
+ createTail,
38
+ jsonPrintLogs,
39
+ prettyPrintLogs,
40
+ translateCLICommandToFilterMessage,
41
+ } from "./tail";
42
+ import {
43
+ login,
44
+ logout,
45
+ listScopes,
46
+ initialise as initialiseUserConfig,
47
+ loginOrRefreshIfRequired,
48
+ getAccountId,
49
+ validateScopeKeys,
50
+ } from "./user";
51
+ import { whoami } from "./whoami";
39
52
 
40
- import { createTail } from "./tail";
41
- import onExit from "signal-exit";
42
- import { setTimeout } from "node:timers/promises";
43
- import * as fs from "node:fs";
44
- import { execa } from "execa";
53
+ import type { Config } from "./config";
54
+ import type { TailCLIFilters } from "./tail";
55
+ import type { RawData } from "ws";
56
+ import type Yargs from "yargs";
45
57
 
46
- async function readConfig(configPath?: string): Promise<Config> {
47
- const config: Config = {};
48
- if (!configPath) {
49
- configPath = await findUp("wrangler.toml");
50
- // TODO - terminate this early instead of going all the way to the root
51
- }
58
+ type ConfigPath = string | undefined;
52
59
 
53
- if (configPath) {
54
- const tml: string = await readFile(configPath, "utf-8");
55
- const parsed = TOML.parse(tml) as Config;
56
- Object.assign(config, parsed);
57
- }
60
+ const resetColor = "\x1b[0m";
61
+ const fgGreenColor = "\x1b[32m";
58
62
 
59
- const inheritedFields = [
60
- "name",
61
- "account_id",
62
- "workers_dev",
63
- "compatibility_date",
64
- "compatibility_flags",
65
- "zone_id",
66
- "routes",
67
- "route",
68
- "jsx_factory",
69
- "jsx_fragment",
70
- "site",
71
- "triggers",
72
- "usage_model",
73
- ];
74
-
75
- Object.keys(config.env || {}).forEach((env) => {
76
- inheritedFields.forEach((field) => {
77
- if (config[field] !== undefined && config.env[env][field] === undefined) {
78
- config.env[env][field] = config[field]; // TODO: - shallow copy?
79
- }
80
- });
81
- });
63
+ function getRules(config: Config): Config["rules"] {
64
+ const rules = config.rules ?? config.build?.upload?.rules ?? [];
82
65
 
83
- const mirroredFields = [
84
- "vars",
85
- "kv_namespaces",
86
- "durable_objects",
87
- "experimental_services",
88
- ];
89
- Object.keys(config.env || {}).forEach((env) => {
90
- mirroredFields.forEach((field) => {
91
- // if it exists on top level, it should exist on env definitions
92
- Object.keys(config[field] || {}).forEach((fieldKey) => {
93
- if (!(fieldKey in config.env[env][field])) {
94
- console.warn(
95
- `In your configuration, "${field}.${fieldKey}" exists at a top level, but not on "env.${env}". This is not what you probably want, since the field "${field}" is not inherited by environments. Please add "${field}.${fieldKey}" to "env.${env}".`
96
- );
97
- }
98
- });
99
- });
100
- });
66
+ if (config.rules && config.build?.upload?.rules) {
67
+ throw new Error(
68
+ `You cannot configure both [rules] and [build.upload.rules] in your wrangler.toml. Delete the \`build.upload\` section.`
69
+ );
70
+ }
101
71
 
102
- if ("experimental_services" in config) {
72
+ if (config.build?.upload?.rules) {
103
73
  console.warn(
104
- "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"
74
+ `Deprecation notice: The \`build.upload.rules\` config field is no longer used, the rules should be specified via the \`rules\` config field. Delete the \`build.upload\` field from the configuration file, and add this:
75
+
76
+ ${TOML.stringify({ rules: config.build.upload.rules })}`
105
77
  );
106
78
  }
79
+ return rules;
80
+ }
107
81
 
108
- // todo: validate, add defaults
109
- // let's just do some basics for now
82
+ function isLegacyEnv(args: unknown, config: Config): boolean {
83
+ return (
84
+ (args as { "legacy-env": boolean | undefined })["legacy-env"] ??
85
+ config.legacy_env
86
+ );
87
+ }
110
88
 
111
- // @ts-expect-error we're being sneaky here for now
112
- config.__path__ = configPath;
89
+ function getScriptName(
90
+ args: { name: string | undefined; env: string | undefined },
91
+ config: Config
92
+ ): string | undefined {
93
+ const shortScriptName = args.name ?? config.name;
94
+ if (!shortScriptName) {
95
+ return;
96
+ }
113
97
 
114
- return config;
98
+ return isLegacyEnv(args, config)
99
+ ? `${shortScriptName}${args.env ? `-${args.env}` : ""}`
100
+ : shortScriptName;
115
101
  }
116
102
 
117
103
  // a helper to demand one of a set of options
@@ -140,8 +126,11 @@ function demandOneOfOption(...options: string[]) {
140
126
  }
141
127
 
142
128
  class CommandLineArgsError extends Error {}
143
- class DeprecationError extends Error {}
144
- class NotImplementedError extends Error {}
129
+ class DeprecationError extends Error {
130
+ constructor(message: string) {
131
+ super(`DEPRECATION WARNING:\n${message}`);
132
+ }
133
+ }
145
134
 
146
135
  export async function main(argv: string[]): Promise<void> {
147
136
  const wrangler = makeCLI(argv)
@@ -210,10 +199,16 @@ export async function main(argv: string[]): Promise<void> {
210
199
  "init [name]",
211
200
  "📥 Create a wrangler.toml configuration file",
212
201
  (yargs) => {
213
- return yargs.positional("name", {
214
- describe: "The name of your worker.",
215
- type: "string",
216
- });
202
+ return yargs
203
+ .positional("name", {
204
+ describe: "The name of your worker",
205
+ type: "string",
206
+ })
207
+ .option("yes", {
208
+ describe: 'Answer "yes" to any prompts for new projects',
209
+ type: "boolean",
210
+ alias: "y",
211
+ });
217
212
  },
218
213
  async (args) => {
219
214
  if ("type" in args) {
@@ -226,94 +221,285 @@ export async function main(argv: string[]): Promise<void> {
226
221
  throw new CommandLineArgsError(message);
227
222
  }
228
223
 
229
- const destination = path.join(process.cwd(), "wrangler.toml");
230
- if (fs.existsSync(destination)) {
231
- console.warn(`${destination} file already exists!`);
232
- const result = await confirm(
224
+ // TODO: make sure args.name is a valid identifier for a worker name
225
+
226
+ const creationDirectory = path.join(process.cwd(), args.name ?? "");
227
+
228
+ const packageManager = await getPackageManager(creationDirectory);
229
+
230
+ // TODO: ask which directory to make the worker in (defaults to args.name)
231
+ // TODO: if args.name isn't provided, ask what to name the worker
232
+
233
+ const wranglerTomlDestination = path.join(
234
+ creationDirectory,
235
+ "./wrangler.toml"
236
+ );
237
+ let justCreatedWranglerToml = false;
238
+ const workerName =
239
+ args.name || path.basename(path.resolve(process.cwd()));
240
+ if (fs.existsSync(wranglerTomlDestination)) {
241
+ console.warn(`${wranglerTomlDestination} file already exists!`);
242
+ const shouldContinue = await confirm(
233
243
  "Do you want to continue initializing this project?"
234
244
  );
235
- if (!result) {
245
+ if (!shouldContinue) {
236
246
  return;
237
247
  }
248
+ } else {
249
+ await mkdir(creationDirectory, { recursive: true });
250
+ const compatibilityDate = new Date().toISOString().substring(0, 10);
251
+ try {
252
+ await writeFile(
253
+ wranglerTomlDestination,
254
+ TOML.stringify({
255
+ name: workerName,
256
+ compatibility_date: compatibilityDate,
257
+ }) + "\n"
258
+ );
259
+ console.log(`✨ Successfully created wrangler.toml`);
260
+ justCreatedWranglerToml = true;
261
+ } catch (err) {
262
+ throw new Error(
263
+ `Failed to create wrangler.toml.\n${(err as Error).message ?? err}`
264
+ );
265
+ }
238
266
  }
239
267
 
240
- const compatibilityDate = new Date().toISOString().substring(0, 10);
241
- try {
242
- await writeFile(
243
- destination,
244
- `compatibility_date = "${compatibilityDate}"` + "\n"
245
- );
246
- console.log(`✨ Successfully created wrangler.toml`);
247
- // TODO: suggest next steps?
248
- } catch (err) {
249
- throw new Error(
250
- `Failed to create wrangler.toml.\n${err.message ?? err}`
251
- );
252
- }
253
-
254
- // if no package.json, ask, and if yes, create one
255
268
  let pathToPackageJson = await findUp("package.json");
256
-
269
+ let shouldCreatePackageJson = false;
270
+ const yesFlag = args.yes ?? false;
257
271
  if (!pathToPackageJson) {
258
- if (
259
- await confirm("No package.json found. Would you like to create one?")
260
- ) {
272
+ // If no package.json exists, ask to create one
273
+ shouldCreatePackageJson =
274
+ yesFlag ||
275
+ (await confirm(
276
+ "No package.json found. Would you like to create one?"
277
+ ));
278
+
279
+ if (shouldCreatePackageJson) {
261
280
  await writeFile(
262
- path.join(process.cwd(), "package.json"),
281
+ path.join(creationDirectory, "./package.json"),
263
282
  JSON.stringify(
264
283
  {
265
- name: "worker",
266
- version: "0.0.1",
284
+ name: workerName,
285
+ version: "0.0.0",
286
+ devDependencies: {
287
+ wrangler: wranglerVersion,
288
+ },
289
+ private: true,
267
290
  },
268
291
  null,
269
292
  " "
270
293
  ) + "\n"
271
294
  );
295
+ await packageManager.install();
272
296
  console.log(`✨ Created package.json`);
273
- pathToPackageJson = path.join(process.cwd(), "package.json");
297
+ pathToPackageJson = path.join(creationDirectory, "package.json");
274
298
  } else {
275
299
  return;
276
300
  }
301
+ } else {
302
+ // If package.json exists and wrangler isn't installed,
303
+ // then ask to add wrangler to devDependencies
304
+ const packageJson = parseJSON(
305
+ readFileSync(pathToPackageJson),
306
+ pathToPackageJson
307
+ );
308
+ if (
309
+ !(
310
+ packageJson.devDependencies?.wrangler ||
311
+ packageJson.dependencies?.wrangler
312
+ )
313
+ ) {
314
+ const shouldInstall =
315
+ yesFlag ||
316
+ (await confirm(
317
+ "Would you like to install wrangler into your package.json?"
318
+ ));
319
+ if (shouldInstall) {
320
+ await packageManager.addDevDeps(`wrangler@${wranglerVersion}`);
321
+ console.log(`✨ Installed wrangler`);
322
+ }
323
+ }
277
324
  }
278
325
 
279
- // if workers-types doesn't exist as a dependency
280
- // offer to install it
281
- // and make a tsconfig?
326
+ let isTypescriptProject = false;
282
327
  let pathToTSConfig = await findUp("tsconfig.json");
283
328
  if (!pathToTSConfig) {
284
- if (await confirm("Would you like to use typescript?")) {
329
+ // If there's no tsconfig, offer to create one
330
+ // and install @cloudflare/workers-types
331
+ if (yesFlag || (await confirm("Would you like to use TypeScript?"))) {
332
+ isTypescriptProject = true;
285
333
  await writeFile(
286
- path.join(process.cwd(), "tsconfig.json"),
334
+ path.join(creationDirectory, "./tsconfig.json"),
335
+ readFileSync(path.join(__dirname, "../templates/tsconfig.json"))
336
+ );
337
+ await packageManager.addDevDeps(
338
+ "@cloudflare/workers-types",
339
+ "typescript"
340
+ );
341
+
342
+ console.log(
343
+ `✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies`
344
+ );
345
+ pathToTSConfig = path.join(creationDirectory, "tsconfig.json");
346
+ }
347
+ } else {
348
+ isTypescriptProject = true;
349
+ // If there's a tsconfig, check if @cloudflare/workers-types
350
+ // is already installed, and offer to install it if not
351
+ const packageJson = parseJSON(
352
+ readFileSync(pathToPackageJson),
353
+ pathToPackageJson
354
+ );
355
+ if (
356
+ !(
357
+ packageJson.devDependencies?.["@cloudflare/workers-types"] ||
358
+ packageJson.dependencies?.["@cloudflare/workers-types"]
359
+ )
360
+ ) {
361
+ const shouldInstall = await confirm(
362
+ "Would you like to install the type definitions for Workers into your package.json?"
363
+ );
364
+ if (shouldInstall) {
365
+ await packageManager.addDevDeps("@cloudflare/workers-types");
366
+ // We don't update the tsconfig.json because
367
+ // it could be complicated in existing projects
368
+ // and we don't want to break them. Instead, we simply
369
+ // tell the user that they need to update their tsconfig.json
370
+ console.log(
371
+ `✨ Installed @cloudflare/workers-types.\nPlease add "@cloudflare/workers-types" to compilerOptions.types in your tsconfig.json`
372
+ );
373
+ }
374
+ }
375
+ }
376
+
377
+ const packageJsonContent = parseJSON(
378
+ readFileSync(pathToPackageJson),
379
+ pathToPackageJson
380
+ );
381
+ const shouldWritePackageJsonScripts =
382
+ !packageJsonContent.scripts?.start &&
383
+ !packageJsonContent.scripts?.publish &&
384
+ shouldCreatePackageJson;
385
+
386
+ async function writePackageJsonScriptsAndUpdateWranglerToml(
387
+ isWritingScripts: boolean,
388
+ isCreatingWranglerToml: boolean,
389
+ packagePath: string,
390
+ scriptPath: string
391
+ ) {
392
+ if (isCreatingWranglerToml) {
393
+ // rewrite wrangler.toml with main = "path/to/script"
394
+ const parsedWranglerToml = TOML.parse(
395
+ readFileSync(wranglerTomlDestination)
396
+ );
397
+ fs.writeFileSync(
398
+ wranglerTomlDestination,
399
+ TOML.stringify({
400
+ name: parsedWranglerToml.name,
401
+ main: scriptPath,
402
+ compatibility_date: parsedWranglerToml.compatibility_date,
403
+ })
404
+ );
405
+ }
406
+ const isNamedWorker =
407
+ isCreatingWranglerToml && path.dirname(packagePath) !== process.cwd();
408
+
409
+ if (isWritingScripts) {
410
+ await writeFile(
411
+ packagePath,
287
412
  JSON.stringify(
288
413
  {
289
- compilerOptions: {
290
- target: "esnext",
291
- module: "esnext",
292
- moduleResolution: "node",
293
- esModuleInterop: true,
294
- allowJs: true,
295
- allowSyntheticDefaultImports: true,
296
- isolatedModules: true,
297
- noEmit: true,
298
- lib: ["esnext"],
299
- jsx: "react",
300
- resolveJsonModule: true,
301
- types: ["@cloudflare/workers-types"],
414
+ ...packageJsonContent,
415
+ scripts: {
416
+ ...packageJsonContent.scripts,
417
+ start: isCreatingWranglerToml
418
+ ? `wrangler dev`
419
+ : `wrangler dev ${scriptPath}`,
420
+ publish: isCreatingWranglerToml
421
+ ? `wrangler publish`
422
+ : `wrangler publish ${scriptPath}`,
302
423
  },
303
424
  },
304
425
  null,
305
- " "
426
+ 2
306
427
  ) + "\n"
307
428
  );
308
- await execa("npm", [
309
- "install",
310
- "@cloudflare/workers-types",
311
- "--save-dev",
312
- ]);
313
429
  console.log(
314
- `✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies`
430
+ `\nTo start developing your Worker, run \`${
431
+ isNamedWorker ? `cd ${args.name} && ` : ""
432
+ }npm start\``
433
+ );
434
+ console.log(
435
+ `To publish your Worker to the Internet, run \`npm run publish\``
436
+ );
437
+ } else {
438
+ console.log(
439
+ `\nTo start developing your Worker, run \`npx wrangler dev\`${
440
+ isCreatingWranglerToml ? "" : ` ${scriptPath}`
441
+ }`
315
442
  );
316
- pathToTSConfig = path.join(process.cwd(), "tsconfig.json");
443
+ console.log(
444
+ `To publish your Worker to the Internet, run \`npx wrangler publish\`${
445
+ isCreatingWranglerToml ? "" : ` ${scriptPath}`
446
+ }`
447
+ );
448
+ }
449
+ }
450
+
451
+ if (isTypescriptProject) {
452
+ if (!fs.existsSync(path.join(creationDirectory, "./src/index.ts"))) {
453
+ let shouldCreateSource = false;
454
+
455
+ shouldCreateSource =
456
+ yesFlag ||
457
+ (await confirm(
458
+ `Would you like to create a Worker at src/index.ts?`
459
+ ));
460
+
461
+ if (shouldCreateSource) {
462
+ await mkdir(path.join(creationDirectory, "./src"), {
463
+ recursive: true,
464
+ });
465
+ await writeFile(
466
+ path.join(creationDirectory, "./src/index.ts"),
467
+ readFileSync(path.join(__dirname, "../templates/new-worker.ts"))
468
+ );
469
+
470
+ console.log(`✨ Created src/index.ts`);
471
+
472
+ await writePackageJsonScriptsAndUpdateWranglerToml(
473
+ shouldWritePackageJsonScripts,
474
+ justCreatedWranglerToml,
475
+ pathToPackageJson,
476
+ "src/index.ts"
477
+ );
478
+ }
479
+ }
480
+ } else {
481
+ if (!fs.existsSync("./src/index.js")) {
482
+ const shouldCreateSource = await confirm(
483
+ `Would you like to create a Worker at src/index.js?`
484
+ );
485
+ if (shouldCreateSource) {
486
+ await mkdir(path.join(creationDirectory, "./src"), {
487
+ recursive: true,
488
+ });
489
+ await writeFile(
490
+ path.join(path.join(creationDirectory, "./src/index.js")),
491
+ readFileSync(path.join(__dirname, "../templates/new-worker.js"))
492
+ );
493
+
494
+ console.log(`✨ Created src/index.js`);
495
+
496
+ await writePackageJsonScriptsAndUpdateWranglerToml(
497
+ shouldWritePackageJsonScripts,
498
+ justCreatedWranglerToml,
499
+ pathToPackageJson,
500
+ "src/index.js"
501
+ );
502
+ }
317
503
  }
318
504
  }
319
505
  }
@@ -347,10 +533,10 @@ export async function main(argv: string[]): Promise<void> {
347
533
  // I mean, this entire app does, but this too.
348
534
  return yargs
349
535
  .option("scopes-list", {
350
- describe: "list all the available OAuth scopes with descriptions.",
536
+ describe: "List all the available OAuth scopes with descriptions",
351
537
  })
352
538
  .option("scopes", {
353
- describe: "allows to choose your set of OAuth scopes.",
539
+ describe: "Pick the set of applicable OAuth scopes when logging in",
354
540
  array: true,
355
541
  type: "string",
356
542
  });
@@ -368,6 +554,11 @@ export async function main(argv: string[]): Promise<void> {
368
554
  listScopes();
369
555
  return;
370
556
  }
557
+ if (!validateScopeKeys(args.scopes)) {
558
+ throw new CommandLineArgsError(
559
+ `One of ${args.scopes} is not a valid authentication scope. Run "wrangler login --list-scopes" to see the valid scopes.`
560
+ );
561
+ }
371
562
  await login({ scopes: args.scopes });
372
563
  return;
373
564
  }
@@ -394,11 +585,10 @@ export async function main(argv: string[]): Promise<void> {
394
585
  // whoami
395
586
  wrangler.command(
396
587
  "whoami",
397
- false, // we don't need to show this the menu
398
- // "🕵️ Retrieve your user info and test your auth config",
588
+ "🕵️ Retrieve your user info and test your auth config",
399
589
  () => {},
400
- (args) => {
401
- console.log(":whoami", args);
590
+ async () => {
591
+ await whoami();
402
592
  }
403
593
  );
404
594
 
@@ -417,13 +607,16 @@ export async function main(argv: string[]): Promise<void> {
417
607
 
418
608
  // dev
419
609
  wrangler.command(
420
- "dev <filename>",
610
+ "dev [script]",
421
611
  "👂 Start a local server for developing your worker",
422
612
  (yargs) => {
423
613
  return yargs
424
- .positional("filename", { describe: "entry point", type: "string" })
614
+ .positional("script", {
615
+ describe: "The path to an entry point for your worker",
616
+ type: "string",
617
+ })
425
618
  .option("name", {
426
- describe: "name of the script",
619
+ describe: "Name of the worker",
427
620
  type: "string",
428
621
  })
429
622
  .option("format", {
@@ -433,7 +626,20 @@ export async function main(argv: string[]): Promise<void> {
433
626
  .option("env", {
434
627
  describe: "Perform on a specific environment",
435
628
  type: "string",
436
- // TODO: get choices for the toml file?
629
+ })
630
+ .option("compatibility-date", {
631
+ describe: "Date to use for compatibility checks",
632
+ type: "string",
633
+ })
634
+ .option("compatibility-flags", {
635
+ describe: "Flags to use for compatibility checks",
636
+ type: "array",
637
+ alias: "compatibility-flag",
638
+ })
639
+ .option("latest", {
640
+ describe: "Use the latest version of the worker runtime",
641
+ type: "boolean",
642
+ default: true,
437
643
  })
438
644
  .option("ip", {
439
645
  describe: "IP address to listen on",
@@ -441,9 +647,12 @@ export async function main(argv: string[]): Promise<void> {
441
647
  default: "127.0.0.1",
442
648
  })
443
649
  .option("port", {
444
- describe: "Port to listen on, defaults to 8787",
650
+ describe: "Port to listen on",
651
+ type: "number",
652
+ })
653
+ .option("inspector-port", {
654
+ describe: "Port for devtools to connect to",
445
655
  type: "number",
446
- default: 8787,
447
656
  })
448
657
  .option("host", {
449
658
  type: "string",
@@ -463,6 +672,18 @@ export async function main(argv: string[]): Promise<void> {
463
672
  describe: "Root folder of static assets for Workers Sites",
464
673
  type: "string",
465
674
  })
675
+ .option("site-include", {
676
+ describe:
677
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.",
678
+ type: "string",
679
+ array: true,
680
+ })
681
+ .option("site-exclude", {
682
+ describe:
683
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.",
684
+ type: "string",
685
+ array: true,
686
+ })
466
687
  .option("upstream-protocol", {
467
688
  default: "https",
468
689
  describe:
@@ -476,11 +697,24 @@ export async function main(argv: string[]): Promise<void> {
476
697
  .option("jsx-fragment", {
477
698
  describe: "The function that is called for each JSX fragment",
478
699
  type: "string",
700
+ })
701
+ .option("local", {
702
+ alias: "l",
703
+ describe: "Run on my machine",
704
+ type: "boolean",
705
+ default: false, // I bet this will a point of contention. We'll revisit it.
706
+ })
707
+ .option("experimental-enable-local-persistence", {
708
+ describe: "Enable persistence for this session (only for local mode)",
709
+ type: "boolean",
479
710
  });
480
711
  },
481
712
  async (args) => {
482
- const { filename, format } = args;
483
- const config = args.config as Config;
713
+ const config = readConfig(
714
+ (args.config as ConfigPath) ||
715
+ (args.script && findWranglerToml(path.dirname(args.script)))
716
+ );
717
+ const entry = getEntry(config, "dev", args.script);
484
718
 
485
719
  if (args["experimental-public"]) {
486
720
  console.warn(
@@ -494,54 +728,75 @@ export async function main(argv: string[]): Promise<void> {
494
728
  );
495
729
  }
496
730
 
497
- // -- snip, extract --
731
+ if (config.site?.["entry-point"]) {
732
+ console.warn(
733
+ "Deprecation notice: The `site.entry-point` config field is no longer used.\n" +
734
+ "The entry-point is specified via the command line (e.g. `wrangler dev path/to/script`).\n" +
735
+ "Please remove the `site.entry-point` field from the `wrangler.toml` file."
736
+ );
737
+ }
498
738
 
499
739
  if (!args.local) {
740
+ // -- snip, extract --
500
741
  const loggedIn = await loginOrRefreshIfRequired();
501
742
  if (!loggedIn) {
502
743
  // didn't login, let's just quit
503
744
  console.log("Did not login, quitting...");
504
745
  return;
505
746
  }
747
+
506
748
  if (!config.account_id) {
507
749
  config.account_id = await getAccountId();
508
750
  if (!config.account_id) {
509
751
  throw new Error("No account id found, quitting...");
510
752
  }
511
753
  }
754
+ // -- snip, end --
512
755
  }
513
756
 
514
- // -- snip, end --
515
-
516
- const envRootObj = args.env ? config.env[args.env] || {} : config;
517
-
518
- // TODO: this error shouldn't actually happen,
519
- // but we haven't fixed it internally yet
520
- if ("durable_objects" in envRootObj) {
521
- if (!(args.name || config.name)) {
522
- console.warn(
523
- 'A worker with durable objects need 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.'
524
- );
525
- }
526
- // TODO: if not already published, publish a draft worker
527
- }
757
+ const environments = config.env ?? {};
758
+ const envRootObj = args.env ? environments[args.env] || {} : config;
528
759
 
529
760
  const { waitUntilExit } = render(
530
761
  <Dev
531
- name={args.name || config.name}
532
- entry={path.relative(process.cwd(), filename)}
762
+ name={getScriptName(args, config)}
763
+ entry={entry}
764
+ env={args.env}
765
+ zone={undefined}
766
+ rules={getRules(config)}
767
+ legacyEnv={isLegacyEnv(args, config)}
533
768
  buildCommand={config.build || {}}
534
- format={format}
769
+ format={args.format || config.build?.upload?.format}
535
770
  initialMode={args.local ? "local" : "remote"}
536
771
  jsxFactory={args["jsx-factory"] || envRootObj?.jsx_factory}
537
772
  jsxFragment={args["jsx-fragment"] || envRootObj?.jsx_fragment}
773
+ enableLocalPersistence={
774
+ args["experimental-enable-local-persistence"] || false
775
+ }
538
776
  accountId={config.account_id}
539
- site={args.site || config.site?.bucket}
540
- port={args.port || config.dev?.port}
777
+ assetPaths={getAssetPaths(
778
+ config,
779
+ args.site,
780
+ args.siteInclude,
781
+ args.siteExclude
782
+ )}
783
+ port={
784
+ args.port || config.dev?.port || (await getPort({ port: 8787 }))
785
+ }
786
+ inspectorPort={
787
+ args["inspector-port"] ?? (await getPort({ port: 9229 }))
788
+ }
541
789
  public={args["experimental-public"]}
542
- compatibilityDate={config.compatibility_date}
543
- compatibilityFlags={config.compatibility_flags}
544
- usageModel={config.usage_model}
790
+ compatibilityDate={
791
+ args["compatibility-date"] ||
792
+ config.compatibility_date ||
793
+ new Date().toISOString().substring(0, 10)
794
+ }
795
+ compatibilityFlags={
796
+ (args["compatibility-flags"] as string[]) ||
797
+ config.compatibility_flags
798
+ }
799
+ usageModel={envRootObj.usage_model}
545
800
  bindings={{
546
801
  kv_namespaces: envRootObj.kv_namespaces?.map(
547
802
  ({ binding, preview_id, id: _id }) => {
@@ -565,8 +820,11 @@ export async function main(argv: string[]): Promise<void> {
565
820
  }
566
821
  ),
567
822
  vars: envRootObj.vars,
823
+ wasm_modules: config.wasm_modules,
824
+ text_blobs: config.text_blobs,
568
825
  durable_objects: envRootObj.durable_objects,
569
- services: envRootObj.experimental_services,
826
+ r2_buckets: envRootObj.r2_buckets,
827
+ unsafe: envRootObj.unsafe?.bindings,
570
828
  }}
571
829
  />
572
830
  );
@@ -585,13 +843,31 @@ export async function main(argv: string[]): Promise<void> {
585
843
  describe: "Perform on a specific environment",
586
844
  })
587
845
  .positional("script", {
588
- describe: "script to upload",
846
+ describe: "The path to an entry point for your worker",
589
847
  type: "string",
590
848
  })
591
849
  .option("name", {
592
- describe: "name to use when uploading",
850
+ describe: "Name of the worker",
851
+ type: "string",
852
+ })
853
+ .option("format", {
854
+ choices: ["modules", "service-worker"] as const,
855
+ describe: "Choose an entry type",
856
+ })
857
+ .option("compatibility-date", {
858
+ describe: "Date to use for compatibility checks",
593
859
  type: "string",
594
860
  })
861
+ .option("compatibility-flags", {
862
+ describe: "Flags to use for compatibility checks",
863
+ type: "array",
864
+ alias: "compatibility-flag",
865
+ })
866
+ .option("latest", {
867
+ describe: "Use the latest version of the worker runtime",
868
+ type: "boolean",
869
+ default: false,
870
+ })
595
871
  .option("experimental-public", {
596
872
  describe: "Static assets to be served",
597
873
  type: "string",
@@ -600,22 +876,28 @@ export async function main(argv: string[]): Promise<void> {
600
876
  describe: "Root folder of static assets for Workers Sites",
601
877
  type: "string",
602
878
  })
879
+ .option("site-include", {
880
+ describe:
881
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.",
882
+ type: "string",
883
+ array: true,
884
+ })
885
+ .option("site-exclude", {
886
+ describe:
887
+ "Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.",
888
+ type: "string",
889
+ array: true,
890
+ })
603
891
  .option("triggers", {
604
892
  describe: "cron schedules to attach",
605
893
  alias: ["schedule", "schedules"],
606
894
  type: "array",
607
895
  })
608
896
  .option("routes", {
609
- describe: "routes to upload",
897
+ describe: "Routes to upload",
610
898
  alias: "route",
611
899
  type: "array",
612
900
  })
613
- .option("services", {
614
- describe: "experimental support for services",
615
- type: "boolean",
616
- default: "false",
617
- hidden: true,
618
- })
619
901
  .option("jsx-factory", {
620
902
  describe: "The function that is called for each JSX element",
621
903
  type: "string",
@@ -626,12 +908,6 @@ export async function main(argv: string[]): Promise<void> {
626
908
  });
627
909
  },
628
910
  async (args) => {
629
- if (args.local) {
630
- throw new NotImplementedError(
631
- "🚫 Local publishing is not yet supported"
632
- );
633
- }
634
-
635
911
  if (args["experimental-public"]) {
636
912
  console.warn(
637
913
  "🚨 The --experimental-public field is experimental and will change in the future."
@@ -643,37 +919,58 @@ export async function main(argv: string[]): Promise<void> {
643
919
  );
644
920
  }
645
921
 
646
- const config = args.config as Config;
922
+ const config = readConfig(
923
+ (args.config as ConfigPath) ||
924
+ (args.script && findWranglerToml(path.dirname(args.script)))
925
+ );
926
+ const entry = getEntry(config, "publish", args.script);
927
+
928
+ if (args.latest) {
929
+ console.warn(
930
+ "⚠️ 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"
931
+ );
932
+ }
647
933
 
648
934
  // -- snip, extract --
649
- if (!args.local) {
650
- const loggedIn = await loginOrRefreshIfRequired();
651
- if (!loggedIn) {
652
- // didn't login, let's just quit
653
- console.log("Did not login, quitting...");
654
- return;
655
- }
935
+ const loggedIn = await loginOrRefreshIfRequired();
936
+ if (!loggedIn) {
937
+ // didn't login, let's just quit
938
+ console.log("Did not login, quitting...");
939
+ return;
940
+ }
941
+
942
+ if (!config.account_id) {
943
+ config.account_id = await getAccountId();
656
944
  if (!config.account_id) {
657
- config.account_id = await getAccountId();
658
- if (!config.account_id) {
659
- throw new Error("No account id found, quitting...");
660
- }
945
+ throw new Error("No account id found, quitting...");
661
946
  }
662
947
  }
663
-
664
948
  // -- snip, end --
665
949
 
950
+ const assetPaths = getAssetPaths(
951
+ config,
952
+ args["experimental-public"] || args.site,
953
+ args.siteInclude,
954
+ args.siteExclude
955
+ );
666
956
  await publish({
667
- config: args.config as Config,
668
- name: args.name,
669
- script: args.script,
957
+ config,
958
+ name: getScriptName(args, config),
959
+ rules: getRules(config),
960
+ format: args.format || config.build?.upload?.format,
961
+ entry,
670
962
  env: args.env,
963
+ compatibilityDate: args.latest
964
+ ? new Date().toISOString().substring(0, 10)
965
+ : args["compatibility-date"],
966
+ compatibilityFlags: args["compatibility-flags"] as string[],
671
967
  triggers: args.triggers,
672
968
  jsxFactory: args["jsx-factory"],
673
969
  jsxFragment: args["jsx-fragment"],
674
970
  routes: args.routes,
675
- public: args["experimental-public"],
676
- site: args.site,
971
+ assetPaths,
972
+ legacyEnv: isLegacyEnv(args, config),
973
+ experimentalPublic: args["experimental-public"] !== undefined,
677
974
  });
678
975
  }
679
976
  );
@@ -681,12 +978,12 @@ export async function main(argv: string[]): Promise<void> {
681
978
  // tail
682
979
  wrangler.command(
683
980
  "tail [name]",
684
- "🦚 Starts a log tailing session for a deployed Worker.",
981
+ "🦚 Starts a log tailing session for a published Worker.",
685
982
  (yargs) => {
686
983
  return (
687
984
  yargs
688
985
  .positional("name", {
689
- describe: "name of the worker",
986
+ describe: "Name of the worker",
690
987
  type: "string",
691
988
  })
692
989
  // TODO: auto-detect if this should be json or pretty based on atty
@@ -698,6 +995,7 @@ export async function main(argv: string[]): Promise<void> {
698
995
  .option("status", {
699
996
  choices: ["ok", "error", "canceled"],
700
997
  describe: "Filter by invocation status",
998
+ array: true,
701
999
  })
702
1000
  .option("header", {
703
1001
  type: "string",
@@ -706,6 +1004,7 @@ export async function main(argv: string[]): Promise<void> {
706
1004
  .option("method", {
707
1005
  type: "string",
708
1006
  describe: "Filter by HTTP method",
1007
+ array: true,
709
1008
  })
710
1009
  .option("sampling-rate", {
711
1010
  type: "number",
@@ -715,54 +1014,76 @@ export async function main(argv: string[]): Promise<void> {
715
1014
  type: "string",
716
1015
  describe: "Filter by a text match in console.log messages",
717
1016
  })
1017
+ .option("ip", {
1018
+ type: "string",
1019
+ describe:
1020
+ 'Filter by the IP address the request originates from. Use "self" to filter for your own IP',
1021
+ array: true,
1022
+ })
718
1023
  .option("env", {
719
1024
  type: "string",
720
1025
  describe: "Perform on a specific environment",
721
1026
  })
1027
+ .option("debug", {
1028
+ type: "boolean",
1029
+ hidden: true,
1030
+ default: false,
1031
+ describe:
1032
+ "If a log would have been filtered out, send it through anyway alongside the filter which would have blocked it.",
1033
+ })
722
1034
  );
723
- // TODO: filter by client ip, which can be 'self' or an ip address
724
1035
  },
725
1036
  async (args) => {
726
- const config = args.config as Config;
1037
+ const config = readConfig(args.config as ConfigPath);
1038
+
1039
+ const scriptName = getScriptName(args, config);
727
1040
 
728
- if (!(args.name || config.name)) {
1041
+ if (!scriptName) {
729
1042
  throw new Error("Missing script name");
730
1043
  }
731
- const scriptName = `${args.name || config.name}${
732
- args.env ? `-${args.env}` : ""
733
- }`;
734
1044
 
735
1045
  // -- snip, extract --
1046
+ const loggedIn = await loginOrRefreshIfRequired();
1047
+ if (!loggedIn) {
1048
+ // didn't login, let's just quit
1049
+ console.log("Did not login, quitting...");
1050
+ return;
1051
+ }
736
1052
 
737
- if (!args.local) {
738
- const loggedIn = await loginOrRefreshIfRequired();
739
- if (!loggedIn) {
740
- // didn't login, let's just quit
741
- console.log("Did not login, quitting...");
742
- return;
743
- }
1053
+ if (!config.account_id) {
1054
+ config.account_id = await getAccountId();
744
1055
  if (!config.account_id) {
745
- config.account_id = await getAccountId();
746
- if (!config.account_id) {
747
- throw new Error("No account id found, quitting...");
748
- }
1056
+ throw new Error("No account id found, quitting...");
749
1057
  }
750
1058
  }
751
-
752
1059
  // -- snip, end --
753
1060
 
754
1061
  const accountId = config.account_id;
755
1062
 
756
- const filters = {
757
- status: args.status as "ok" | "error" | "canceled",
1063
+ const cliFilters: TailCLIFilters = {
1064
+ status: args.status as ("ok" | "error" | "canceled")[],
758
1065
  header: args.header,
759
1066
  method: args.method,
760
- "sampling-rate": args["sampling-rate"],
1067
+ samplingRate: args["sampling-rate"],
761
1068
  search: args.search,
1069
+ clientIp: args.ip,
762
1070
  };
763
1071
 
764
- const { tail, expiration, /* sendHeartbeat, */ deleteTail } =
765
- await createTail(accountId, scriptName, filters);
1072
+ const filters = translateCLICommandToFilterMessage(
1073
+ cliFilters,
1074
+ args.debug
1075
+ );
1076
+
1077
+ const { tail, expiration, deleteTail } = await createTail(
1078
+ accountId,
1079
+ scriptName,
1080
+ filters,
1081
+ !isLegacyEnv(args, config) ? args.env : undefined
1082
+ );
1083
+
1084
+ const scriptDisplayName = `${scriptName}${
1085
+ args.env && !isLegacyEnv(args, config) ? ` (${args.env})` : ""
1086
+ }`;
766
1087
 
767
1088
  console.log(
768
1089
  `successfully created tail, expires at ${expiration.toLocaleString()}`
@@ -773,24 +1094,32 @@ export async function main(argv: string[]): Promise<void> {
773
1094
  await deleteTail();
774
1095
  });
775
1096
 
776
- tail.on("message", (data) => {
777
- console.log(JSON.stringify(JSON.parse(data.toString()), null, " "));
778
- });
1097
+ const printLog: (data: RawData) => void =
1098
+ args.format === "pretty" ? prettyPrintLogs : jsonPrintLogs;
1099
+
1100
+ tail.on("message", printLog);
779
1101
 
780
1102
  while (tail.readyState !== tail.OPEN) {
781
1103
  switch (tail.readyState) {
782
1104
  case tail.CONNECTING:
783
- await setTimeout(1000);
1105
+ await setTimeout(100);
784
1106
  break;
785
1107
  case tail.CLOSING:
786
- await setTimeout(1000);
1108
+ await setTimeout(100);
787
1109
  break;
788
1110
  case tail.CLOSED:
789
- process.exit(1);
1111
+ throw new Error(
1112
+ `Connection to ${scriptDisplayName} closed unexpectedly.`
1113
+ );
790
1114
  }
791
1115
  }
792
1116
 
793
- console.log(`Connected to ${scriptName}, waiting for logs...`);
1117
+ console.log(`Connected to ${scriptDisplayName}, waiting for logs...`);
1118
+
1119
+ tail.on("close", async () => {
1120
+ tail.terminate();
1121
+ await deleteTail();
1122
+ });
794
1123
  }
795
1124
  );
796
1125
 
@@ -801,14 +1130,12 @@ export async function main(argv: string[]): Promise<void> {
801
1130
  (yargs) => {
802
1131
  return yargs
803
1132
  .positional("method", {
1133
+ type: "string",
804
1134
  describe: "Type of request to preview your worker",
805
- choices: ["GET", "POST"],
806
- default: ["GET"],
807
1135
  })
808
1136
  .positional("body", {
809
1137
  type: "string",
810
1138
  describe: "Body string to post to your preview worker request.",
811
- default: "Null",
812
1139
  })
813
1140
  .option("env", {
814
1141
  type: "string",
@@ -820,11 +1147,104 @@ export async function main(argv: string[]): Promise<void> {
820
1147
  type: "boolean",
821
1148
  });
822
1149
  },
823
- () => {
824
- // "🔬 [DEPRECATED] Preview your code temporarily on https://cloudflareworkers.com"
825
- throw new DeprecationError(
826
- "`wrangler preview` has been deprecated, please refer to TODO://some/path for alternatives"
1150
+ async (args) => {
1151
+ if (args.method || args.body) {
1152
+ throw new DeprecationError(
1153
+ "The `wrangler preview` command has been deprecated.\n" +
1154
+ "Try using `wrangler dev` to to try out a worker during development.\n"
1155
+ );
1156
+ }
1157
+
1158
+ // Delegate to `wrangler dev`
1159
+ console.warn(
1160
+ "***************************************************\n" +
1161
+ "The `wrangler preview` command has been deprecated.\n" +
1162
+ "Attempting to run `wrangler dev` instead.\n" +
1163
+ "***************************************************\n"
827
1164
  );
1165
+
1166
+ const config = readConfig(args.config as ConfigPath);
1167
+ const entry = getEntry(config, "dev");
1168
+
1169
+ // -- snip, extract --
1170
+ const loggedIn = await loginOrRefreshIfRequired();
1171
+ if (!loggedIn) {
1172
+ // didn't login, let's just quit
1173
+ console.log("Did not login, quitting...");
1174
+ return;
1175
+ }
1176
+
1177
+ if (!config.account_id) {
1178
+ config.account_id = await getAccountId();
1179
+ if (!config.account_id) {
1180
+ throw new Error("No account id found, quitting...");
1181
+ }
1182
+ }
1183
+ // -- snip, end --
1184
+
1185
+ const environments = config.env ?? {};
1186
+ const envRootObj = args.env ? environments[args.env] || {} : config;
1187
+
1188
+ const { waitUntilExit } = render(
1189
+ <Dev
1190
+ name={config.name}
1191
+ entry={entry}
1192
+ rules={getRules(config)}
1193
+ env={args.env}
1194
+ zone={undefined}
1195
+ legacyEnv={isLegacyEnv(args, config)}
1196
+ buildCommand={config.build || {}}
1197
+ format={config.build?.upload?.format}
1198
+ initialMode={args.local ? "local" : "remote"}
1199
+ jsxFactory={envRootObj?.jsx_factory}
1200
+ jsxFragment={envRootObj?.jsx_fragment}
1201
+ enableLocalPersistence={false}
1202
+ accountId={config.account_id}
1203
+ assetPaths={undefined}
1204
+ port={config.dev?.port}
1205
+ public={undefined}
1206
+ compatibilityDate={
1207
+ config.compatibility_date ||
1208
+ new Date().toISOString().substring(0, 10)
1209
+ }
1210
+ compatibilityFlags={
1211
+ (args["compatibility-flags"] as string[]) ||
1212
+ config.compatibility_flags
1213
+ }
1214
+ usageModel={config.usage_model}
1215
+ bindings={{
1216
+ kv_namespaces: envRootObj.kv_namespaces?.map(
1217
+ ({ binding, preview_id, id: _id }) => {
1218
+ // In `dev`, we make folks use a separate kv namespace called
1219
+ // `preview_id` instead of `id` so that they don't
1220
+ // break production data. So here we check that a `preview_id`
1221
+ // has actually been configured.
1222
+ // This whole block of code will be obsoleted in the future
1223
+ // when we have copy-on-write for previews on edge workers.
1224
+ if (!preview_id) {
1225
+ // TODO: This error has to be a _lot_ better, ideally just asking
1226
+ // to create a preview namespace for the user automatically
1227
+ throw new Error(
1228
+ `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`
1229
+ ); // Ugh, I really don't like this message very much
1230
+ }
1231
+ return {
1232
+ binding,
1233
+ id: preview_id,
1234
+ };
1235
+ }
1236
+ ),
1237
+ vars: envRootObj.vars,
1238
+ wasm_modules: config.wasm_modules,
1239
+ text_blobs: config.text_blobs,
1240
+ durable_objects: envRootObj.durable_objects,
1241
+ r2_buckets: envRootObj.r2_buckets,
1242
+ unsafe: envRootObj.unsafe?.bindings,
1243
+ }}
1244
+ inspectorPort={await getPort({ port: 9229 })}
1245
+ />
1246
+ );
1247
+ await waitUntilExit();
828
1248
  }
829
1249
  );
830
1250
 
@@ -837,7 +1257,7 @@ export async function main(argv: string[]): Promise<void> {
837
1257
  return routeYargs
838
1258
  .command(
839
1259
  "list",
840
- "List a route associated with a zone",
1260
+ "List the routes associated with a zone",
841
1261
  (yargs) => {
842
1262
  return yargs
843
1263
  .option("env", {
@@ -846,17 +1266,19 @@ export async function main(argv: string[]): Promise<void> {
846
1266
  })
847
1267
  .option("zone", {
848
1268
  type: "string",
849
- describe: "zone id",
1269
+ describe: "Zone id",
850
1270
  })
851
1271
  .positional("zone", {
852
- describe: "zone id",
1272
+ describe: "Zone id",
853
1273
  type: "string",
854
1274
  });
855
1275
  },
856
1276
  async (args) => {
857
1277
  console.log(":route list", args);
858
1278
  // TODO: use environment (current wrangler doesn't do so?)
859
- const zone = args.zone || (args.config as Config).zone_id;
1279
+ // TODO: don't get `zone_id` from config at all and require the command line arg `--zone`.
1280
+ const config = readConfig(args.config as ConfigPath);
1281
+ const zone = args.zone || config.zone_id;
860
1282
  if (!zone) {
861
1283
  throw new Error("missing zone id");
862
1284
  }
@@ -885,7 +1307,8 @@ export async function main(argv: string[]): Promise<void> {
885
1307
  async (args) => {
886
1308
  console.log(":route delete", args);
887
1309
  // TODO: use environment (current wrangler doesn't do so?)
888
- const zone = args.zone || (args.config as Config).zone_id;
1310
+ const config = readConfig(args.config as ConfigPath);
1311
+ const zone = args.zone || config.zone_id;
889
1312
  if (!zone) {
890
1313
  throw new Error("missing zone id");
891
1314
  }
@@ -927,82 +1350,86 @@ export async function main(argv: string[]): Promise<void> {
927
1350
  (yargs) => {
928
1351
  return yargs
929
1352
  .positional("key", {
930
- describe: "The variable name to be accessible in the script.",
1353
+ describe: "The variable name to be accessible in the script",
931
1354
  type: "string",
932
1355
  })
933
1356
  .option("name", {
934
- describe: "name of the script",
1357
+ describe: "Name of the worker",
935
1358
  type: "string",
936
1359
  })
937
1360
  .option("env", {
938
1361
  type: "string",
939
1362
  describe:
940
- "Binds the secret to the script of the specific environment.",
1363
+ "Binds the secret to the Worker of the specific environment",
941
1364
  });
942
1365
  },
943
1366
  async (args) => {
944
- if (args.local) {
945
- throw new NotImplementedError(
946
- "--local not implemented for this command yet"
947
- );
948
- }
949
- const config = args.config as Config;
1367
+ const config = readConfig(args.config as ConfigPath);
950
1368
 
951
- // TODO: use environment (how does current wrangler do it?)
952
- const scriptName = args.name || config.name;
1369
+ const scriptName = getScriptName(args, config);
953
1370
  if (!scriptName) {
954
1371
  throw new Error("Missing script name");
955
1372
  }
956
1373
 
957
1374
  // -- snip, extract --
1375
+ const loggedIn = await loginOrRefreshIfRequired();
1376
+ if (!loggedIn) {
1377
+ // didn't login, let's just quit
1378
+ console.log("Did not login, quitting...");
1379
+ return;
1380
+ }
958
1381
 
959
- if (!args.local) {
960
- const loggedIn = await loginOrRefreshIfRequired();
961
- if (!loggedIn) {
962
- // didn't login, let's just quit
963
- console.log("Did not login, quitting...");
964
- return;
965
- }
1382
+ if (!config.account_id) {
1383
+ config.account_id = await getAccountId();
966
1384
  if (!config.account_id) {
967
- config.account_id = await getAccountId();
968
- if (!config.account_id) {
969
- throw new Error("No account id found, quitting...");
970
- }
1385
+ throw new Error("No account id found, quitting...");
971
1386
  }
972
1387
  }
973
-
974
1388
  // -- snip, end --
975
1389
 
976
1390
  const secretValue = await prompt(
977
1391
  "Enter a secret value:",
978
1392
  "password"
979
1393
  );
1394
+
1395
+ console.log(
1396
+ `🌀 Creating the secret for script ${scriptName} ${
1397
+ args.env && !isLegacyEnv(args, config) ? `(${args.env})` : ""
1398
+ }`
1399
+ );
1400
+
980
1401
  async function submitSecret() {
981
- return await fetchResult(
982
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets/`,
983
- {
984
- method: "PUT",
985
- headers: { "Content-Type": "application/json" },
986
- body: JSON.stringify({
987
- name: args.key,
988
- text: secretValue,
989
- type: "secret_text",
990
- }),
991
- }
992
- );
1402
+ const url =
1403
+ !args.env || isLegacyEnv(args, config)
1404
+ ? `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1405
+ : `/accounts/${config.account_id}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1406
+
1407
+ return await fetchResult(url, {
1408
+ method: "PUT",
1409
+ headers: { "Content-Type": "application/json" },
1410
+ body: JSON.stringify({
1411
+ name: args.key,
1412
+ text: secretValue,
1413
+ type: "secret_text",
1414
+ }),
1415
+ });
993
1416
  }
994
1417
 
995
1418
  try {
996
- console.log(await submitSecret());
1419
+ await submitSecret();
997
1420
  } catch (e) {
1421
+ // @ts-expect-error non-standard property on Error
998
1422
  if (e.code === 10007) {
999
1423
  // upload a draft worker
1424
+ // TODO: log a warning
1000
1425
  await fetchResult(
1001
- `/accounts/${config.account_id}/workers/scripts/${scriptName}`,
1426
+ !args["legacy-env"] && args.env
1427
+ ? `/accounts/${config.account_id}/workers/services/${scriptName}/environments/${args.env}`
1428
+ : `/accounts/${config.account_id}/workers/scripts/${scriptName}`,
1002
1429
  {
1003
1430
  method: "PUT",
1004
- // @ts-expect-error TODO: fix this error!
1005
1431
  body: toFormData({
1432
+ name: scriptName,
1006
1433
  main: {
1007
1434
  name: scriptName,
1008
1435
  content: `export default { fetch() {} }`,
@@ -1012,17 +1439,26 @@ export async function main(argv: string[]): Promise<void> {
1012
1439
  kv_namespaces: [],
1013
1440
  vars: {},
1014
1441
  durable_objects: { bindings: [] },
1442
+ r2_buckets: [],
1443
+ wasm_modules: {},
1444
+ text_blobs: {},
1445
+ unsafe: [],
1015
1446
  },
1016
1447
  modules: [],
1448
+ migrations: undefined,
1449
+ compatibility_date: undefined,
1450
+ compatibility_flags: undefined,
1451
+ usage_model: undefined,
1017
1452
  }),
1018
1453
  }
1019
1454
  );
1020
1455
 
1021
1456
  // and then try again
1022
- console.log(await submitSecret());
1457
+ await submitSecret();
1023
1458
  // TODO: delete the draft worker if this failed too?
1024
1459
  }
1025
1460
  }
1461
+ console.log(`✨ Success! Uploaded secret ${args.key}`);
1026
1462
  }
1027
1463
  )
1028
1464
  .command(
@@ -1031,63 +1467,65 @@ export async function main(argv: string[]): Promise<void> {
1031
1467
  (yargs) => {
1032
1468
  return yargs
1033
1469
  .positional("key", {
1034
- describe: "The variable name to be accessible in the script.",
1470
+ describe: "The variable name to be accessible in the script",
1035
1471
  type: "string",
1036
1472
  })
1037
1473
  .option("name", {
1038
- describe: "name of the script",
1474
+ describe: "Name of the worker",
1039
1475
  type: "string",
1040
1476
  })
1041
1477
  .option("env", {
1042
1478
  type: "string",
1043
1479
  describe:
1044
- "Binds the secret to the script of the specific environment.",
1480
+ "Binds the secret to the Worker of the specific environment",
1045
1481
  });
1046
1482
  },
1047
1483
  async (args) => {
1048
- if (args.local) {
1049
- throw new NotImplementedError(
1050
- "--local not implemented for this command yet"
1051
- );
1052
- }
1053
- const config = args.config as Config;
1484
+ const config = readConfig(args.config as ConfigPath);
1054
1485
 
1055
- // TODO: use environment (how does current wrangler do it?)
1056
- const scriptName = args.name || config.name;
1486
+ const scriptName = getScriptName(args, config);
1057
1487
  if (!scriptName) {
1058
1488
  throw new Error("Missing script name");
1059
1489
  }
1060
1490
 
1061
1491
  // -- snip, extract --
1492
+ const loggedIn = await loginOrRefreshIfRequired();
1493
+ if (!loggedIn) {
1494
+ // didn't login, let's just quit
1495
+ console.log("Did not login, quitting...");
1496
+ return;
1497
+ }
1062
1498
 
1063
- if (!args.local) {
1064
- const loggedIn = await loginOrRefreshIfRequired();
1065
- if (!loggedIn) {
1066
- // didn't login, let's just quit
1067
- console.log("Did not login, quitting...");
1068
- return;
1069
- }
1499
+ if (!config.account_id) {
1500
+ config.account_id = await getAccountId();
1070
1501
  if (!config.account_id) {
1071
- config.account_id = await getAccountId();
1072
- if (!config.account_id) {
1073
- throw new Error("No account id found, quitting...");
1074
- }
1502
+ throw new Error("No account id found, quitting...");
1075
1503
  }
1076
1504
  }
1077
-
1078
1505
  // -- snip, end --
1079
1506
 
1080
- if (await confirm("Are you sure you want to delete this secret?")) {
1507
+ if (
1508
+ await confirm(
1509
+ `Are you sure you want to permanently delete the variable ${
1510
+ args.key
1511
+ } on the script ${scriptName}${
1512
+ args.env && !isLegacyEnv(args, config) ? ` (${args.env})` : ""
1513
+ }?`
1514
+ )
1515
+ ) {
1081
1516
  console.log(
1082
- `Deleting the secret ${args.key} on script ${scriptName}.`
1517
+ `🌀 Deleting the secret ${args.key} on script ${scriptName}${
1518
+ args.env && !isLegacyEnv(args, config) ? ` (${args.env})` : ""
1519
+ }`
1083
1520
  );
1084
1521
 
1085
- console.log(
1086
- await fetchResult(
1087
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets/${args.key}`,
1088
- { method: "DELETE" }
1089
- )
1090
- );
1522
+ const url =
1523
+ !args.env || isLegacyEnv(args, config)
1524
+ ? `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1525
+ : `/accounts/${config.account_id}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1526
+
1527
+ await fetchResult(`${url}/${args.key}`, { method: "DELETE" });
1528
+ console.log(`✨ Success! Deleted secret ${args.key}`);
1091
1529
  }
1092
1530
  }
1093
1531
  )
@@ -1097,53 +1535,45 @@ export async function main(argv: string[]): Promise<void> {
1097
1535
  (yargs) => {
1098
1536
  return yargs
1099
1537
  .option("name", {
1100
- describe: "name of the script",
1538
+ describe: "Name of the worker",
1101
1539
  type: "string",
1102
1540
  })
1103
1541
  .option("env", {
1104
1542
  type: "string",
1105
1543
  describe:
1106
- "Binds the secret to the script of the specific environment.",
1544
+ "Binds the secret to the Worker of the specific environment.",
1107
1545
  });
1108
1546
  },
1109
1547
  async (args) => {
1110
- if (args.local) {
1111
- throw new NotImplementedError(
1112
- "--local not implemented for this command yet"
1113
- );
1114
- }
1115
- const config = args.config as Config;
1548
+ const config = readConfig(args.config as ConfigPath);
1116
1549
 
1117
- // TODO: use environment (how does current wrangler do it?)
1118
- const scriptName = args.name || config.name;
1550
+ const scriptName = getScriptName(args, config);
1119
1551
  if (!scriptName) {
1120
1552
  throw new Error("Missing script name");
1121
1553
  }
1122
1554
 
1123
1555
  // -- snip, extract --
1556
+ const loggedIn = await loginOrRefreshIfRequired();
1557
+ if (!loggedIn) {
1558
+ // didn't login, let's just quit
1559
+ console.log("Did not login, quitting...");
1560
+ return;
1561
+ }
1124
1562
 
1125
- if (!args.local) {
1126
- const loggedIn = await loginOrRefreshIfRequired();
1127
- if (!loggedIn) {
1128
- // didn't login, let's just quit
1129
- console.log("Did not login, quitting...");
1130
- return;
1131
- }
1563
+ if (!config.account_id) {
1564
+ config.account_id = await getAccountId();
1132
1565
  if (!config.account_id) {
1133
- config.account_id = await getAccountId();
1134
- if (!config.account_id) {
1135
- throw new Error("No account id found, quitting...");
1136
- }
1566
+ throw new Error("No account id found, quitting...");
1137
1567
  }
1138
1568
  }
1139
-
1140
1569
  // -- snip, end --
1141
1570
 
1142
- console.log(
1143
- await fetchResult(
1144
- `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1145
- )
1146
- );
1571
+ const url =
1572
+ !args.env || isLegacyEnv(args, config)
1573
+ ? `/accounts/${config.account_id}/workers/scripts/${scriptName}/secrets`
1574
+ : `/accounts/${config.account_id}/workers/services/${scriptName}/environments/${args.env}/secrets`;
1575
+
1576
+ console.log(JSON.stringify(await fetchResult(url), null, " "));
1147
1577
  }
1148
1578
  );
1149
1579
  }
@@ -1164,6 +1594,7 @@ export async function main(argv: string[]): Promise<void> {
1164
1594
  .positional("namespace", {
1165
1595
  describe: "The name of the new namespace",
1166
1596
  type: "string",
1597
+ demandOption: true,
1167
1598
  })
1168
1599
  .option("env", {
1169
1600
  type: "string",
@@ -1188,7 +1619,7 @@ export async function main(argv: string[]): Promise<void> {
1188
1619
  );
1189
1620
  }
1190
1621
 
1191
- const config = args.config as Config;
1622
+ const config = readConfig(args.config as ConfigPath);
1192
1623
  if (!config.name) {
1193
1624
  console.warn(
1194
1625
  "No configured name present, using `worker` as a prefix for the title"
@@ -1200,35 +1631,20 @@ export async function main(argv: string[]): Promise<void> {
1200
1631
  const preview = args.preview ? "_preview" : "";
1201
1632
  const title = `${name}${environment}-${args.namespace}${preview}`;
1202
1633
 
1203
- if (args.local) {
1204
- const { Miniflare } = await import("miniflare");
1205
- const mf = new Miniflare({
1206
- kvPersist: (args.kvPersist as string) || true,
1207
- // TODO: these options shouldn't be required
1208
- script: ` `, // has to be a string with at least one char
1209
- });
1210
- await mf.getKVNamespace(title); // this should "create" the namespace
1211
- console.log(`✨ Success! Created KV namespace ${title}`);
1634
+ // -- snip, extract --
1635
+ const loggedIn = await loginOrRefreshIfRequired();
1636
+ if (!loggedIn) {
1637
+ // didn't login, let's just quit
1638
+ console.log("Did not login, quitting...");
1212
1639
  return;
1213
1640
  }
1214
1641
 
1215
- // -- snip, extract --
1216
-
1217
- if (!args.local) {
1218
- const loggedIn = await loginOrRefreshIfRequired();
1219
- if (!loggedIn) {
1220
- // didn't login, let's just quit
1221
- console.log("Did not login, quitting...");
1222
- return;
1223
- }
1642
+ if (!config.account_id) {
1643
+ config.account_id = await getAccountId();
1224
1644
  if (!config.account_id) {
1225
- config.account_id = await getAccountId();
1226
- if (!config.account_id) {
1227
- throw new Error("No account id found, quitting...");
1228
- }
1645
+ throw new Error("No account id found, quitting...");
1229
1646
  }
1230
1647
  }
1231
-
1232
1648
  // -- snip, end --
1233
1649
 
1234
1650
  // TODO: generate a binding name stripping non alphanumeric chars
@@ -1254,31 +1670,22 @@ export async function main(argv: string[]): Promise<void> {
1254
1670
  "Outputs a list of all KV namespaces associated with your account id.",
1255
1671
  {},
1256
1672
  async (args) => {
1257
- if (args.local) {
1258
- throw new NotImplementedError(
1259
- `local mode is not yet supported for this command`
1260
- );
1261
- }
1262
-
1263
- const config = args.config as Config;
1673
+ const config = readConfig(args.config as ConfigPath);
1264
1674
 
1265
1675
  // -- snip, extract --
1676
+ const loggedIn = await loginOrRefreshIfRequired();
1677
+ if (!loggedIn) {
1678
+ // didn't login, let's just quit
1679
+ console.log("Did not login, quitting...");
1680
+ return;
1681
+ }
1266
1682
 
1267
- if (!args.local) {
1268
- const loggedIn = await loginOrRefreshIfRequired();
1269
- if (!loggedIn) {
1270
- // didn't login, let's just quit
1271
- console.log("Did not login, quitting...");
1272
- return;
1273
- }
1683
+ if (!config.account_id) {
1684
+ config.account_id = await getAccountId();
1274
1685
  if (!config.account_id) {
1275
- config.account_id = await getAccountId();
1276
- if (!config.account_id) {
1277
- throw new Error("No account id found, quitting...");
1278
- }
1686
+ throw new Error("No account id found, quitting...");
1279
1687
  }
1280
1688
  }
1281
-
1282
1689
  // -- snip, end --
1283
1690
 
1284
1691
  // TODO: we should show bindings if they exist for given ids
@@ -1316,40 +1723,31 @@ export async function main(argv: string[]): Promise<void> {
1316
1723
  });
1317
1724
  },
1318
1725
  async (args) => {
1319
- if (args.local) {
1320
- throw new NotImplementedError(
1321
- `local mode is not yet supported for this command`
1322
- );
1323
- }
1324
- const config = args.config as Config;
1726
+ const config = readConfig(args.config as ConfigPath);
1325
1727
 
1326
1728
  let id;
1327
1729
  try {
1328
- id = getNamespaceId(args);
1730
+ id = getNamespaceId(args, config);
1329
1731
  } catch (e) {
1330
1732
  throw new CommandLineArgsError(
1331
- "Not able to delete namespace.\n" + e.message
1733
+ "Not able to delete namespace.\n" + ((e as Error).message ?? e)
1332
1734
  );
1333
1735
  }
1334
1736
 
1335
1737
  // -- snip, extract --
1738
+ const loggedIn = await loginOrRefreshIfRequired();
1739
+ if (!loggedIn) {
1740
+ // didn't login, let's just quit
1741
+ console.log("Did not login, quitting...");
1742
+ return;
1743
+ }
1336
1744
 
1337
- if (!args.local) {
1338
- const loggedIn = await loginOrRefreshIfRequired();
1339
- if (!loggedIn) {
1340
- // didn't login, let's just quit
1341
- console.log("Did not login, quitting...");
1342
- return;
1343
- }
1344
-
1745
+ if (!config.account_id) {
1746
+ config.account_id = await getAccountId();
1345
1747
  if (!config.account_id) {
1346
- config.account_id = await getAccountId();
1347
- if (!config.account_id) {
1348
- throw new Error("No account id found, quitting...");
1349
- }
1748
+ throw new Error("No account id found, quitting...");
1350
1749
  }
1351
1750
  }
1352
-
1353
1751
  // -- snip, end --
1354
1752
 
1355
1753
  await fetchResult<{ id: string }>(
@@ -1392,19 +1790,20 @@ export async function main(argv: string[]): Promise<void> {
1392
1790
  return yargs
1393
1791
  .positional("key", {
1394
1792
  type: "string",
1395
- describe: "The key to write to.",
1793
+ describe: "The key to write to",
1794
+ demandOption: true,
1396
1795
  })
1397
1796
  .positional("value", {
1398
1797
  type: "string",
1399
- describe: "The value to write.",
1798
+ describe: "The value to write",
1400
1799
  })
1401
1800
  .option("binding", {
1402
1801
  type: "string",
1403
- describe: "The binding of the namespace to write to.",
1802
+ describe: "The binding of the namespace to write to",
1404
1803
  })
1405
1804
  .option("namespace-id", {
1406
1805
  type: "string",
1407
- describe: "The id of the namespace to write to.",
1806
+ describe: "The id of the namespace to write to",
1408
1807
  })
1409
1808
  .check(demandOneOfOption("binding", "namespace-id"))
1410
1809
  .option("env", {
@@ -1417,7 +1816,7 @@ export async function main(argv: string[]): Promise<void> {
1417
1816
  })
1418
1817
  .option("ttl", {
1419
1818
  type: "number",
1420
- describe: "Time for which the entries should be visible.",
1819
+ describe: "Time for which the entries should be visible",
1421
1820
  })
1422
1821
  .option("expiration", {
1423
1822
  type: "number",
@@ -1426,16 +1825,18 @@ export async function main(argv: string[]): Promise<void> {
1426
1825
  })
1427
1826
  .option("path", {
1428
1827
  type: "string",
1429
- describe: "Read value from the file at a given path.",
1828
+ describe: "Read value from the file at a given path",
1430
1829
  })
1431
1830
  .check(demandOneOfOption("value", "path"));
1432
1831
  },
1433
1832
  async ({ key, ttl, expiration, ...args }) => {
1434
- const namespaceId = getNamespaceId(args);
1833
+ const config = readConfig(args.config as ConfigPath);
1834
+ const namespaceId = getNamespaceId(args, config);
1835
+ // One of `args.path` and `args.value` must be defined
1435
1836
  const value = args.path
1436
- ? await readFile(args.path, "utf-8")
1437
- : args.value;
1438
- const config = args.config as Config;
1837
+ ? readFileSync(args.path)
1838
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1839
+ args.value!;
1439
1840
 
1440
1841
  if (args.path) {
1441
1842
  console.log(
@@ -1447,35 +1848,20 @@ export async function main(argv: string[]): Promise<void> {
1447
1848
  );
1448
1849
  }
1449
1850
 
1450
- if (args.local) {
1451
- const { Miniflare } = await import("miniflare");
1452
- const mf = new Miniflare({
1453
- kvPersist: (args.kvPersist as string) || true,
1454
- // TODO: these options shouldn't be required
1455
- script: ` `, // has to be a string with at least one char
1456
- });
1457
- const ns = await mf.getKVNamespace(namespaceId);
1458
- await ns.put(key, value, { expiration, expirationTtl: ttl });
1851
+ // -- snip, extract --
1852
+ const loggedIn = await loginOrRefreshIfRequired();
1853
+ if (!loggedIn) {
1854
+ // didn't login, let's just quit
1855
+ console.log("Did not login, quitting...");
1459
1856
  return;
1460
1857
  }
1461
- // -- snip, extract --
1462
-
1463
- if (!args.local) {
1464
- const loggedIn = await loginOrRefreshIfRequired();
1465
- if (!loggedIn) {
1466
- // didn't login, let's just quit
1467
- console.log("Did not login, quitting...");
1468
- return;
1469
- }
1470
1858
 
1859
+ if (!config.account_id) {
1860
+ config.account_id = await getAccountId();
1471
1861
  if (!config.account_id) {
1472
- config.account_id = await getAccountId();
1473
- if (!config.account_id) {
1474
- throw new Error("No account id found, quitting...");
1475
- }
1862
+ throw new Error("No account id found, quitting...");
1476
1863
  }
1477
1864
  }
1478
-
1479
1865
  // -- snip, end --
1480
1866
 
1481
1867
  await putKeyValue(config.account_id, namespaceId, key, value, {
@@ -1515,46 +1901,31 @@ export async function main(argv: string[]): Promise<void> {
1515
1901
  },
1516
1902
  async ({ prefix, ...args }) => {
1517
1903
  // TODO: support for limit+cursor (pagination)
1904
+ const config = readConfig(args.config as ConfigPath);
1905
+ const namespaceId = getNamespaceId(args, config);
1518
1906
 
1519
- const namespaceId = getNamespaceId(args);
1520
- const config = args.config as Config;
1521
-
1522
- if (args.local) {
1523
- const { Miniflare } = await import("miniflare");
1524
- const mf = new Miniflare({
1525
- kvPersist: (args.kvPersist as string) || true,
1526
- // TODO: these options shouldn't be required
1527
- script: ` `, // has to be a string with at least one char
1528
- });
1529
- const ns = await mf.getKVNamespace(namespaceId);
1530
- const listResponse = await ns.list({ prefix });
1531
- console.log(JSON.stringify(listResponse.keys, null, " ")); // TODO: paginate, collate
1907
+ // -- snip, extract --
1908
+ const loggedIn = await loginOrRefreshIfRequired();
1909
+ if (!loggedIn) {
1910
+ // didn't login, let's just quit
1911
+ console.log("Did not login, quitting...");
1532
1912
  return;
1533
1913
  }
1534
1914
 
1535
- // -- snip, extract --
1536
-
1537
- if (!args.local) {
1538
- const loggedIn = await loginOrRefreshIfRequired();
1539
- if (!loggedIn) {
1540
- // didn't login, let's just quit
1541
- console.log("Did not login, quitting...");
1542
- return;
1543
- }
1544
-
1915
+ if (!config.account_id) {
1916
+ config.account_id = await getAccountId();
1545
1917
  if (!config.account_id) {
1546
- config.account_id = await getAccountId();
1547
- if (!config.account_id) {
1548
- throw new Error("No account id found, quitting...");
1549
- }
1918
+ throw new Error("No account id found, quitting...");
1550
1919
  }
1551
1920
  }
1552
-
1553
1921
  // -- snip, end --
1554
1922
 
1555
- console.log(
1556
- await listNamespaceKeys(config.account_id, namespaceId, prefix)
1923
+ const results = await listNamespaceKeys(
1924
+ config.account_id,
1925
+ namespaceId,
1926
+ prefix
1557
1927
  );
1928
+ console.log(JSON.stringify(results, undefined, 2));
1558
1929
  }
1559
1930
  )
1560
1931
  .command(
@@ -1565,6 +1936,7 @@ export async function main(argv: string[]): Promise<void> {
1565
1936
  .positional("key", {
1566
1937
  describe: "The key value to get.",
1567
1938
  type: "string",
1939
+ demandOption: true,
1568
1940
  })
1569
1941
  .option("binding", {
1570
1942
  type: "string",
@@ -1591,46 +1963,26 @@ export async function main(argv: string[]): Promise<void> {
1591
1963
  });
1592
1964
  },
1593
1965
  async ({ key, ...args }) => {
1594
- const namespaceId = getNamespaceId(args);
1595
- const config = args.config as Config;
1596
-
1597
- if (args.local) {
1598
- const { Miniflare } = await import("miniflare");
1599
- const mf = new Miniflare({
1600
- kvPersist: (args.kvPersist as string) || true,
1601
- // TODO: these options shouldn't be required
1602
- script: ` `, // has to be a string with at least one char
1603
- });
1604
- const ns = await mf.getKVNamespace(namespaceId);
1605
- console.log(await ns.get(key));
1606
- return;
1607
- }
1966
+ const config = readConfig(args.config as ConfigPath);
1967
+ const namespaceId = getNamespaceId(args, config);
1608
1968
 
1609
1969
  // -- snip, extract --
1970
+ const loggedIn = await loginOrRefreshIfRequired();
1971
+ if (!loggedIn) {
1972
+ // didn't login, let's just quit
1973
+ console.log("Did not login, quitting...");
1974
+ return;
1975
+ }
1610
1976
 
1611
- if (!args.local) {
1612
- const loggedIn = await loginOrRefreshIfRequired();
1613
- if (!loggedIn) {
1614
- // didn't login, let's just quit
1615
- console.log("Did not login, quitting...");
1616
- return;
1617
- }
1618
-
1977
+ if (!config.account_id) {
1978
+ config.account_id = await getAccountId();
1619
1979
  if (!config.account_id) {
1620
- config.account_id = await getAccountId();
1621
- if (!config.account_id) {
1622
- throw new Error("No account id found, quitting...");
1623
- }
1980
+ throw new Error("No account id found, quitting...");
1624
1981
  }
1625
1982
  }
1626
-
1627
1983
  // -- snip, end --
1628
1984
 
1629
- console.log(
1630
- await fetchRaw(
1631
- `/accounts/${config.account_id}/storage/kv/namespaces/${namespaceId}/values/${key}`
1632
- )
1633
- );
1985
+ console.log(await getKeyValue(config.account_id, namespaceId, key));
1634
1986
  }
1635
1987
  )
1636
1988
  .command(
@@ -1641,6 +1993,7 @@ export async function main(argv: string[]): Promise<void> {
1641
1993
  .positional("key", {
1642
1994
  describe: "The key value to delete",
1643
1995
  type: "string",
1996
+ demandOption: true,
1644
1997
  })
1645
1998
  .option("binding", {
1646
1999
  type: "string",
@@ -1661,44 +2014,27 @@ export async function main(argv: string[]): Promise<void> {
1661
2014
  });
1662
2015
  },
1663
2016
  async ({ key, ...args }) => {
1664
- const namespaceId = getNamespaceId(args);
2017
+ const config = readConfig(args.config as ConfigPath);
2018
+ const namespaceId = getNamespaceId(args, config);
1665
2019
 
1666
2020
  console.log(
1667
2021
  `deleting the key "${key}" on namespace ${namespaceId}`
1668
2022
  );
1669
2023
 
1670
- if (args.local) {
1671
- const { Miniflare } = await import("miniflare");
1672
- const mf = new Miniflare({
1673
- kvPersist: (args.kvPersist as string) || true,
1674
- // TODO: these options shouldn't be required
1675
- script: ` `, // has to be a string with at least one char
1676
- });
1677
- const ns = await mf.getKVNamespace(namespaceId);
1678
- console.log(await ns.delete(key));
2024
+ // -- snip, extract --
2025
+ const loggedIn = await loginOrRefreshIfRequired();
2026
+ if (!loggedIn) {
2027
+ // didn't login, let's just quit
2028
+ console.log("Did not login, quitting...");
1679
2029
  return;
1680
2030
  }
1681
2031
 
1682
- const config = args.config as Config;
1683
-
1684
- // -- snip, extract --
1685
-
1686
- if (!args.local) {
1687
- const loggedIn = await loginOrRefreshIfRequired();
1688
- if (!loggedIn) {
1689
- // didn't login, let's just quit
1690
- console.log("Did not login, quitting...");
1691
- return;
1692
- }
1693
-
2032
+ if (!config.account_id) {
2033
+ config.account_id = await getAccountId();
1694
2034
  if (!config.account_id) {
1695
- config.account_id = await getAccountId();
1696
- if (!config.account_id) {
1697
- throw new Error("No account id found, quitting...");
1698
- }
2035
+ throw new Error("No account id found, quitting...");
1699
2036
  }
1700
2037
  }
1701
-
1702
2038
  // -- snip, end --
1703
2039
 
1704
2040
  await fetchResult(
@@ -1724,14 +2060,15 @@ export async function main(argv: string[]): Promise<void> {
1724
2060
  .positional("filename", {
1725
2061
  describe: `The JSON file of key-value pairs to upload, in form [{"key":..., "value":...}"...]`,
1726
2062
  type: "string",
2063
+ demandOption: true,
1727
2064
  })
1728
2065
  .option("binding", {
1729
2066
  type: "string",
1730
- describe: "The name of the namespace to put to",
2067
+ describe: "The name of the namespace to insert values into",
1731
2068
  })
1732
2069
  .option("namespace-id", {
1733
2070
  type: "string",
1734
- describe: "The id of the namespace to put to",
2071
+ describe: "The id of the namespace to insert values into",
1735
2072
  })
1736
2073
  .check(demandOneOfOption("binding", "namespace-id"))
1737
2074
  .option("env", {
@@ -1748,59 +2085,24 @@ export async function main(argv: string[]): Promise<void> {
1748
2085
  // This could be made more efficient with a streaming parser/uploader
1749
2086
  // but we'll do that in the future if needed.
1750
2087
 
1751
- const namespaceId = getNamespaceId(args);
1752
- const config = args.config as Config;
1753
- const content = await readFile(filename, "utf-8");
1754
- let parsedContent;
1755
- try {
1756
- parsedContent = JSON.parse(content);
1757
- } catch (err) {
1758
- throw new Error(
1759
- `Could not parse json from ${filename}.\n${err.message ?? err}`
1760
- );
1761
- }
1762
-
1763
- if (args.local) {
1764
- const { Miniflare } = await import("miniflare");
1765
- const mf = new Miniflare({
1766
- kvPersist: (args.kvPersist as string) || true,
1767
- // TODO: these options shouldn't be required
1768
- script: ` `, // has to be a string with at least one char
1769
- });
1770
- const ns = await mf.getKVNamespace(namespaceId);
1771
- for (const {
1772
- key,
1773
- value,
1774
- expiration,
1775
- expiration_ttl,
1776
- } of parsedContent) {
1777
- await ns.put(key, value, {
1778
- expiration,
1779
- expirationTtl: expiration_ttl,
1780
- });
1781
- }
2088
+ const config = readConfig(args.config as ConfigPath);
2089
+ const namespaceId = getNamespaceId(args, config);
2090
+ const content = parseJSON(readFileSync(filename), filename);
1782
2091
 
2092
+ // -- snip, extract --
2093
+ const loggedIn = await loginOrRefreshIfRequired();
2094
+ if (!loggedIn) {
2095
+ // didn't login, let's just quit
2096
+ console.log("Did not login, quitting...");
1783
2097
  return;
1784
2098
  }
1785
2099
 
1786
- // -- snip, extract --
1787
-
1788
- if (!args.local) {
1789
- const loggedIn = await loginOrRefreshIfRequired();
1790
- if (!loggedIn) {
1791
- // didn't login, let's just quit
1792
- console.log("Did not login, quitting...");
1793
- return;
1794
- }
1795
-
2100
+ if (!config.account_id) {
2101
+ config.account_id = await getAccountId();
1796
2102
  if (!config.account_id) {
1797
- config.account_id = await getAccountId();
1798
- if (!config.account_id) {
1799
- throw new Error("No account id found, quitting...");
1800
- }
2103
+ throw new Error("No account id found, quitting...");
1801
2104
  }
1802
2105
  }
1803
-
1804
2106
  // -- snip, end --
1805
2107
 
1806
2108
  console.log(
@@ -1810,12 +2112,13 @@ export async function main(argv: string[]): Promise<void> {
1810
2112
  )
1811
2113
  .command(
1812
2114
  "delete <filename>",
1813
- "Upload multiple key-value pairs to a namespace",
2115
+ "Delete multiple key-value pairs from a namespace",
1814
2116
  (yargs) => {
1815
2117
  return yargs
1816
2118
  .positional("filename", {
1817
- describe: `The JSON file of key-value pairs to upload, in form ["key1", "key2", ...]`,
2119
+ describe: `The JSON file of keys to delete, in the form ["key1", "key2", ...]`,
1818
2120
  type: "string",
2121
+ demandOption: true,
1819
2122
  })
1820
2123
  .option("binding", {
1821
2124
  type: "string",
@@ -1836,51 +2139,24 @@ export async function main(argv: string[]): Promise<void> {
1836
2139
  });
1837
2140
  },
1838
2141
  async ({ filename, ...args }) => {
1839
- const namespaceId = getNamespaceId(args);
1840
- const config = args.config as Config;
1841
- const content = await readFile(filename, "utf-8");
1842
- let parsedContent;
1843
- try {
1844
- parsedContent = JSON.parse(content);
1845
- } catch (err) {
1846
- throw new Error(
1847
- `Could not parse json from ${filename}.\n${err.message ?? err}`
1848
- );
1849
- }
1850
-
1851
- if (args.local) {
1852
- const { Miniflare } = await import("miniflare");
1853
- const mf = new Miniflare({
1854
- kvPersist: (args.kvPersist as string) || true,
1855
- // TODO: these options shouldn't be required
1856
- script: ` `, // has to be a string with at least one char
1857
- });
1858
- const ns = await mf.getKVNamespace(namespaceId);
1859
- for (const key of parsedContent) {
1860
- await ns.delete(key);
1861
- }
2142
+ const config = readConfig(args.config as ConfigPath);
2143
+ const namespaceId = getNamespaceId(args, config);
2144
+ const content = parseJSON(readFileSync(filename), filename);
1862
2145
 
2146
+ // -- snip, extract --
2147
+ const loggedIn = await loginOrRefreshIfRequired();
2148
+ if (!loggedIn) {
2149
+ // didn't login, let's just quit
2150
+ console.log("Did not login, quitting...");
1863
2151
  return;
1864
2152
  }
1865
2153
 
1866
- // -- snip, extract --
1867
-
1868
- if (!args.local) {
1869
- const loggedIn = await loginOrRefreshIfRequired();
1870
- if (!loggedIn) {
1871
- // didn't login, let's just quit
1872
- console.log("Did not login, quitting...");
1873
- return;
1874
- }
1875
-
2154
+ if (!config.account_id) {
2155
+ config.account_id = await getAccountId();
1876
2156
  if (!config.account_id) {
1877
- config.account_id = await getAccountId();
1878
- if (!config.account_id) {
1879
- throw new Error("No account id found, quitting...");
1880
- }
2157
+ throw new Error("No account id found, quitting...");
1881
2158
  }
1882
2159
  }
1883
-
1884
2160
  // -- snip, end --
1885
2161
 
1886
2162
  console.log(
@@ -1893,37 +2169,159 @@ export async function main(argv: string[]): Promise<void> {
1893
2169
 
1894
2170
  wrangler.command("pages", "⚡️ Configure Cloudflare Pages", pages);
1895
2171
 
2172
+ wrangler.command("r2", "📦 Interact with an R2 store", (r2Yargs) => {
2173
+ return r2Yargs.command("bucket", "Manage R2 buckets", (r2BucketYargs) => {
2174
+ r2BucketYargs.command(
2175
+ "create <name>",
2176
+ "Create a new R2 bucket",
2177
+ (yargs) => {
2178
+ return yargs.positional("name", {
2179
+ describe: "The name of the new bucket",
2180
+ type: "string",
2181
+ demandOption: true,
2182
+ });
2183
+ },
2184
+ async (args) => {
2185
+ // We expect three values in `_`: `r2`, `bucket`, `create`.
2186
+ if (args._.length > 3) {
2187
+ const extraArgs = args._.slice(3).join(" ");
2188
+ throw new CommandLineArgsError(
2189
+ `Unexpected additional positional arguments "${extraArgs}".`
2190
+ );
2191
+ }
2192
+
2193
+ const config = readConfig(args.config as ConfigPath);
2194
+
2195
+ // -- snip, extract --
2196
+ const loggedIn = await loginOrRefreshIfRequired();
2197
+ if (!loggedIn) {
2198
+ // didn't login, let's just quit
2199
+ console.log("Did not login, quitting...");
2200
+ return;
2201
+ }
2202
+
2203
+ if (!config.account_id) {
2204
+ config.account_id = await getAccountId();
2205
+ if (!config.account_id) {
2206
+ throw new Error("No account id found, quitting...");
2207
+ }
2208
+ }
2209
+ // -- snip, end --
2210
+
2211
+ console.log(`Creating bucket ${args.name}.`);
2212
+ await createR2Bucket(config.account_id, args.name);
2213
+ console.log(`Created bucket ${args.name}.`);
2214
+ }
2215
+ );
2216
+
2217
+ r2BucketYargs.command("list", "List R2 buckets", {}, async (args) => {
2218
+ const config = readConfig(args.config as ConfigPath);
2219
+
2220
+ // -- snip, extract --
2221
+ const loggedIn = await loginOrRefreshIfRequired();
2222
+ if (!loggedIn) {
2223
+ // didn't login, let's just quit
2224
+ console.log("Did not login, quitting...");
2225
+ return;
2226
+ }
2227
+
2228
+ if (!config.account_id) {
2229
+ config.account_id = await getAccountId();
2230
+ if (!config.account_id) {
2231
+ throw new Error("No account id found, quitting...");
2232
+ }
2233
+ }
2234
+ // -- snip, end --
2235
+
2236
+ console.log(
2237
+ JSON.stringify(await listR2Buckets(config.account_id), null, 2)
2238
+ );
2239
+ });
2240
+
2241
+ r2BucketYargs.command(
2242
+ "delete <name>",
2243
+ "Delete an R2 bucket",
2244
+ (yargs) => {
2245
+ return yargs.positional("name", {
2246
+ describe: "The name of the bucket to delete",
2247
+ type: "string",
2248
+ demandOption: true,
2249
+ });
2250
+ },
2251
+ async (args) => {
2252
+ // We expect three values in `_`: `r2`, `bucket`, `delete`.
2253
+ if (args._.length > 3) {
2254
+ const extraArgs = args._.slice(3).join(" ");
2255
+ throw new CommandLineArgsError(
2256
+ `Unexpected additional positional arguments "${extraArgs}".`
2257
+ );
2258
+ }
2259
+
2260
+ const config = readConfig(args.config as ConfigPath);
2261
+
2262
+ // -- snip, extract --
2263
+ const loggedIn = await loginOrRefreshIfRequired();
2264
+ if (!loggedIn) {
2265
+ // didn't login, let's just quit
2266
+ console.log("Did not login, quitting...");
2267
+ return;
2268
+ }
2269
+
2270
+ if (!config.account_id) {
2271
+ config.account_id = await getAccountId();
2272
+ if (!config.account_id) {
2273
+ throw new Error("No account id found, quitting...");
2274
+ }
2275
+ }
2276
+ // -- snip, end --
2277
+
2278
+ console.log(`Deleting bucket ${args.name}.`);
2279
+ await deleteR2Bucket(config.account_id, args.name);
2280
+ console.log(`Deleted bucket ${args.name}.`);
2281
+ }
2282
+ );
2283
+ return r2BucketYargs;
2284
+ });
2285
+ });
2286
+
1896
2287
  wrangler
2288
+ .option("legacy-env", {
2289
+ type: "boolean",
2290
+ describe: "Use legacy environments",
2291
+ })
1897
2292
  .option("config", {
1898
2293
  alias: "c",
1899
2294
  describe: "Path to .toml configuration file",
1900
2295
  type: "string",
1901
- async coerce(arg) {
1902
- return await readConfig(arg);
1903
- },
1904
- })
1905
- .option("local", {
1906
- alias: "l",
1907
- describe: "Run on my machine",
1908
- type: "boolean",
1909
- default: false, // I bet this will a point of contention. We'll revisit it.
1910
2296
  });
1911
2297
 
1912
- wrangler.group(["config", "help", "version"], "Flags:");
2298
+ wrangler.group(["config", "help", "version", "legacy-env"], "Flags:");
1913
2299
  wrangler.help().alias("h", "help");
1914
2300
  wrangler.version(wranglerVersion).alias("v", "version");
1915
2301
  wrangler.exitProcess(false);
1916
2302
 
1917
- await initialiseUserConfig();
1918
-
1919
2303
  try {
2304
+ await initialiseUserConfig();
1920
2305
  await wrangler.parse();
1921
2306
  } catch (e) {
1922
2307
  if (e instanceof CommandLineArgsError) {
1923
2308
  wrangler.showHelp("error");
1924
2309
  console.error(""); // Just adds a bit of space
2310
+ console.error(e.message);
2311
+ } else if (e instanceof ParseError) {
2312
+ console.error("");
2313
+ e.notes.push({
2314
+ text: "\nIf you think this is a bug, please open an issue at: https://github.com/cloudflare/wrangler2/issues/new",
2315
+ });
2316
+ console.error(formatMessage(e));
2317
+ } else {
2318
+ console.error(e instanceof Error ? e.message : e);
2319
+ console.error(""); // Just adds a bit of space
2320
+ console.error(
2321
+ `${fgGreenColor}%s${resetColor}`,
2322
+ "If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
2323
+ );
1925
2324
  }
1926
- console.error(e.message);
1927
2325
  throw e;
1928
2326
  }
1929
2327
  }