-
Notifications
You must be signed in to change notification settings - Fork 758
/
Copy pathindex.tsx
2048 lines (1913 loc) · 66.9 KB
/
index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import React from "react";
import { render } from "ink";
import Dev from "./dev";
import { readFile } from "node:fs/promises";
import makeCLI from "yargs";
import type Yargs from "yargs";
import { findUp } from "find-up";
import TOML from "@iarna/toml";
import { normaliseAndValidateEnvironmentsConfig } from "./config";
import type { Config } from "./config";
import { getAssetPaths } from "./sites";
import { confirm, prompt } from "./dialogs";
import { version as wranglerVersion } from "../package.json";
import {
login,
logout,
listScopes,
initialise as initialiseUserConfig,
loginOrRefreshIfRequired,
getAccountId,
validateScopeKeys,
} from "./user";
import {
getNamespaceId,
listNamespaces,
listNamespaceKeys,
putKeyValue,
putBulkKeyValue,
deleteBulkKeyValue,
createNamespace,
isValidNamespaceBinding,
} from "./kv";
import { pages } from "./pages";
import { fetchResult, fetchRaw } from "./cfetch";
import publish from "./publish";
import path from "path/posix";
import { writeFile } from "node:fs/promises";
import { toFormData } from "./api/form_data";
import { createTail } from "./tail";
import onExit from "signal-exit";
import { setTimeout } from "node:timers/promises";
import * as fs from "node:fs";
import { execa } from "execa";
import { whoami } from "./whoami";
const resetColor = "\x1b[0m";
const fgGreenColor = "\x1b[32m";
async function readConfig(configPath?: string): Promise<Config> {
const config: Config = {};
if (!configPath) {
configPath = await findUp("wrangler.toml");
// TODO - terminate this early instead of going all the way to the root
}
if (configPath) {
const tml: string = await readFile(configPath, "utf-8");
const parsed = TOML.parse(tml) as Config;
Object.assign(config, parsed);
}
normaliseAndValidateEnvironmentsConfig(config);
if ("experimental_services" in config) {
console.warn(
"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"
);
}
// todo: validate, add defaults
// let's just do some basics for now
// @ts-expect-error we're being sneaky here for now
config.__path__ = configPath;
return config;
}
// a helper to demand one of a set of options
// via https://github.com/yargs/yargs/issues/1093#issuecomment-491299261
function demandOneOfOption(...options: string[]) {
return function (argv: Yargs.Arguments) {
const count = options.filter((option) => argv[option]).length;
const lastOption = options.pop();
if (count === 0) {
throw new CommandLineArgsError(
`Exactly one of the arguments ${options.join(
", "
)} and ${lastOption} is required`
);
} else if (count > 1) {
throw new CommandLineArgsError(
`Arguments ${options.join(
", "
)} and ${lastOption} are mutually exclusive`
);
}
return true;
};
}
class CommandLineArgsError extends Error {}
class DeprecationError extends Error {}
class NotImplementedError extends Error {}
export async function main(argv: string[]): Promise<void> {
const wrangler = makeCLI(argv)
// We handle errors ourselves in a try-catch around `yargs.parse`.
// If you want the "help info" to be displayed then throw an instance of `CommandLineArgsError`.
// Otherwise we just log the error that was thrown without any "help info".
.showHelpOnFail(false)
.fail((msg, error) => {
if (!error) {
// If there is only a `msg` then this came from yargs own validation, so wrap in a `CommandLineArgsError`.
error = new CommandLineArgsError(msg);
}
throw error;
})
.scriptName("wrangler")
.wrap(null);
// the default is to simply print the help menu
wrangler.command(
["*"],
false,
() => {},
(args) => {
if (args._.length > 0) {
throw new CommandLineArgsError(`Unknown command: ${args._}.`);
} else {
wrangler.showHelp("log");
}
}
);
// You will note that we use the form for all commands where we use the builder function
// to define options and subcommands.
// Further we return the result of this builder even though it's not completely necessary.
// The reason is that it's required for type inference of the args in the handle function.
// I wish we could enforce this pattern, but this comment will have to do for now.
// (It's also annoying that choices[] doesn't get inferred as an enum. 🤷♂.)
// [DEPRECATED] generate
wrangler.command(
// we definitely want to move away from us cloning github templates
// we can do something better here, let's see
"generate [name] [template]",
false,
(yargs) => {
return yargs
.positional("name", {
describe: "Name of the Workers project",
default: "worker",
})
.positional("template", {
describe: "a link to a GitHub template",
default: "https://github.com/cloudflare/worker-template",
});
},
() => {
// "👯 [DEPRECATED]. Scaffold a Cloudflare Workers project from a public GitHub repository.",
throw new DeprecationError(
"`wrangler generate` has been deprecated, please refer to TODO://some/path for alternatives"
);
}
);
// init
wrangler.command(
"init [name]",
"📥 Create a wrangler.toml configuration file",
(yargs) => {
return yargs.positional("name", {
describe: "The name of your worker.",
type: "string",
});
},
async (args) => {
if ("type" in args) {
let message = "The --type option is no longer supported.";
if (args.type === "webpack") {
message +=
"\nIf you wish to use webpack then you will need to create a custom build.";
// TODO: Add a link to docs
}
throw new CommandLineArgsError(message);
}
const destination = path.join(process.cwd(), "wrangler.toml");
if (fs.existsSync(destination)) {
console.warn(`${destination} file already exists!`);
const shouldContinue = await confirm(
"Do you want to continue initializing this project?"
);
if (!shouldContinue) {
return;
}
} else {
const compatibilityDate = new Date().toISOString().substring(0, 10);
try {
await writeFile(
destination,
`compatibility_date = "${compatibilityDate}"` + "\n"
);
console.log(`✨ Successfully created wrangler.toml`);
// TODO: suggest next steps?
} catch (err) {
throw new Error(
`Failed to create wrangler.toml.\n${err.message ?? err}`
);
}
}
let pathToPackageJson = await findUp("package.json");
if (!pathToPackageJson) {
// If no package.json exists, ask to create one
const shouldCreatePackageJson = await confirm(
"No package.json found. Would you like to create one?"
);
if (shouldCreatePackageJson) {
await writeFile(
path.join(process.cwd(), "package.json"),
JSON.stringify(
{
name: "worker",
version: "0.0.1",
devDependencies: {
wrangler: wranglerVersion,
},
},
null,
" "
) + "\n"
);
await execa("npm", ["install"], {
stdio: "inherit",
});
console.log(`✨ Created package.json`);
pathToPackageJson = path.join(process.cwd(), "package.json");
} else {
return;
}
} else {
// If package.json exists and wrangler isn't installed,
// then ask to add wrangler to devDependencies
const packageJson = JSON.parse(
await readFile(pathToPackageJson, "utf-8")
);
if (
!(
packageJson.devDependencies?.wrangler ||
packageJson.dependencies?.wrangler
)
) {
const shouldInstall = await confirm(
"Would you like to install wrangler into your package.json?"
);
if (shouldInstall) {
await execa(
"npm",
["install", `wrangler@${wranglerVersion}`, "--save-dev"],
{
stdio: "inherit",
}
);
console.log(`✨ Installed wrangler`);
}
}
}
let pathToTSConfig = await findUp("tsconfig.json");
if (!pathToTSConfig) {
// If there's no tsconfig, offer to create one
// and install @cloudflare/workers-types
if (await confirm("Would you like to use typescript?")) {
await writeFile(
path.join(process.cwd(), "tsconfig.json"),
JSON.stringify(
{
compilerOptions: {
target: "esnext",
module: "esnext",
moduleResolution: "node",
esModuleInterop: true,
allowJs: true,
allowSyntheticDefaultImports: true,
isolatedModules: true,
noEmit: true,
lib: ["esnext"],
jsx: "react",
resolveJsonModule: true,
types: ["@cloudflare/workers-types"],
},
},
null,
" "
) + "\n"
);
await execa(
"npm",
["install", "@cloudflare/workers-types", "--save-dev"],
{ stdio: "inherit" }
);
console.log(
`✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies`
);
pathToTSConfig = path.join(process.cwd(), "tsconfig.json");
}
} else {
// If there's a tsconfig, check if @cloudflare/workers-types
// is already installed, and offer to install it if not
const packageJson = JSON.parse(
await readFile(pathToPackageJson, "utf-8")
);
if (
!(
packageJson.devDependencies?.["@cloudflare/workers-types"] ||
packageJson.dependencies?.["@cloudflare/workers-types"]
)
) {
const shouldInstall = await confirm(
"Would you like to install the type definitions for Workers into your package.json?"
);
if (shouldInstall) {
await execa(
"npm",
["install", "@cloudflare/workers-types", "--save-dev"],
{
stdio: "inherit",
}
);
// We don't update the tsconfig.json because
// it could be complicated in existing projects
// and we don't want to break them. Instead, we simply
// tell the user that they need to update their tsconfig.json
console.log(
`✨ Installed @cloudflare/workers-types.\nPlease add "@cloudflare/workers-types" to compilerOptions.types in your tsconfig.json`
);
} else {
return;
}
}
}
}
);
// build
wrangler.command(
"build",
false,
(yargs) => {
return yargs.option("env", {
describe: "Perform on a specific environment",
});
},
() => {
// "[DEPRECATED] 🦀 Build your project (if applicable)",
throw new DeprecationError(
"`wrangler build` has been deprecated, please refer to TODO://some/path for alternatives"
);
}
);
// login
wrangler.command(
// this needs scopes as an option?
"login",
false, // we don't need to show this in the menu
// "🔓 Login to Cloudflare",
(yargs) => {
// TODO: This needs some copy editing
// I mean, this entire app does, but this too.
return yargs
.option("scopes-list", {
describe: "list all the available OAuth scopes with descriptions.",
})
.option("scopes", {
describe: "allows to choose your set of OAuth scopes.",
array: true,
type: "string",
});
// TODO: scopes
},
async (args) => {
if (args["scopes-list"]) {
listScopes();
return;
}
if (args.scopes) {
if (args.scopes.length === 0) {
// don't allow no scopes to be passed, that would be weird
listScopes();
return;
}
if (!validateScopeKeys(args.scopes)) {
throw new CommandLineArgsError(
`One of ${args.scopes} is not a valid authentication scope. Run "wrangler login --list-scopes" to see the valid scopes.`
);
}
await login({ scopes: args.scopes });
return;
}
await login();
// TODO: would be nice if it optionally saved login
// credentials inside node_modules/.cache or something
// this way you could have multiple users on a single machine
}
);
// logout
wrangler.command(
// this needs scopes as an option?
"logout",
false, // we don't need to show this in the menu
// "🚪 Logout from Cloudflare",
() => {},
async () => {
await logout();
}
);
// whoami
wrangler.command(
"whoami",
"🕵️ Retrieve your user info and test your auth config",
() => {},
async () => {
await whoami();
}
);
// config
wrangler.command(
"config",
false,
() => {},
() => {
// "🕵️ Authenticate Wrangler with a Cloudflare API Token",
throw new DeprecationError(
"`wrangler config` has been deprecated, please refer to TODO://some/path for alternatives"
);
}
);
// dev
wrangler.command(
"dev <filename>",
"👂 Start a local server for developing your worker",
(yargs) => {
return yargs
.positional("filename", {
describe: "entry point",
type: "string",
demandOption: true,
})
.option("name", {
describe: "name of the script",
type: "string",
})
.option("format", {
choices: ["modules", "service-worker"] as const,
describe: "Choose an entry type",
})
.option("env", {
describe: "Perform on a specific environment",
type: "string",
// TODO: get choices for the toml file?
})
.option("compatibility-date", {
describe: "Date to use for compatibility checks",
type: "string",
})
.option("compatibility-flags", {
describe: "Flags to use for compatibility checks",
type: "array",
alias: "compatibility-flag",
})
.option("latest", {
describe: "Use the latest version of the worker runtime",
type: "boolean",
default: true,
})
.option("ip", {
describe: "IP address to listen on",
type: "string",
default: "127.0.0.1",
})
.option("port", {
describe: "Port to listen on, defaults to 8787",
type: "number",
default: 8787,
})
.option("host", {
type: "string",
describe:
"Host to forward requests to, defaults to the zone of project",
})
.option("local-protocol", {
default: "http",
describe: "Protocol to listen to requests on, defaults to http.",
choices: ["http", "https"],
})
.option("experimental-public", {
describe: "Static assets to be served",
type: "string",
})
.option("site", {
describe: "Root folder of static assets for Workers Sites",
type: "string",
})
.option("site-include", {
describe:
"Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.",
type: "string",
array: true,
})
.option("site-exclude", {
describe:
"Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.",
type: "string",
array: true,
})
.option("upstream-protocol", {
default: "https",
describe:
"Protocol to forward requests to host on, defaults to https.",
choices: ["http", "https"],
})
.option("jsx-factory", {
describe: "The function that is called for each JSX element",
type: "string",
})
.option("jsx-fragment", {
describe: "The function that is called for each JSX fragment",
type: "string",
});
},
async (args) => {
const { filename, format } = args;
const config = args.config as Config;
if (args["experimental-public"]) {
console.warn(
"🚨 The --experimental-public field is experimental and will change in the future."
);
}
if (args.public) {
throw new Error(
"🚨 The --public field has been renamed to --experimental-public, and will change behaviour in the future."
);
}
if (config.site?.["entry-point"]) {
console.warn(
"Deprecation notice: The `site.entry-point` config field is no longer used.\n" +
"The entry-point is specified via the command line (e.g. `wrangler dev path/to/script`).\n" +
"Please remove the `site.entry-point` field from the `wrangler.toml` file."
);
}
if (!args.local) {
// -- snip, extract --
const loggedIn = await loginOrRefreshIfRequired();
if (!loggedIn) {
// didn't login, let's just quit
console.log("Did not login, quitting...");
return;
}
if (!config.account_id) {
config.account_id = await getAccountId();
if (!config.account_id) {
throw new Error("No account id found, quitting...");
}
}
// -- snip, end --
}
const environments = config.env ?? {};
const envRootObj = args.env ? environments[args.env] || {} : config;
// TODO: this error shouldn't actually happen,
// but we haven't fixed it internally yet
if ("durable_objects" in envRootObj) {
if (!(args.name || config.name)) {
console.warn(
'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.'
);
}
// TODO: if not already published, publish a draft worker
}
const { waitUntilExit } = render(
<Dev
name={args.name || config.name}
entry={path.relative(process.cwd(), filename)}
buildCommand={config.build || {}}
format={format}
initialMode={args.local ? "local" : "remote"}
jsxFactory={args["jsx-factory"] || envRootObj?.jsx_factory}
jsxFragment={args["jsx-fragment"] || envRootObj?.jsx_fragment}
accountId={config.account_id}
assetPaths={getAssetPaths(
config,
args.site,
args.siteInclude,
args.siteExclude
)}
port={args.port || config.dev?.port}
public={args["experimental-public"]}
compatibilityDate={
args["compatibility-date"] ||
config.compatibility_date ||
new Date().toISOString().substring(0, 10)
}
compatibilityFlags={
(args["compatibility-flags"] as string[]) ||
config.compatibility_flags
}
usageModel={config.usage_model}
bindings={{
kv_namespaces: envRootObj.kv_namespaces?.map(
({ binding, preview_id, id: _id }) => {
// In `dev`, we make folks use a separate kv namespace called
// `preview_id` instead of `id` so that they don't
// break production data. So here we check that a `preview_id`
// has actually been configured.
// This whole block of code will be obsoleted in the future
// when we have copy-on-write for previews on edge workers.
if (!preview_id) {
// TODO: This error has to be a _lot_ better, ideally just asking
// to create a preview namespace for the user automatically
throw new Error(
`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`
); // Ugh, I really don't like this message very much
}
return {
binding,
id: preview_id,
};
}
),
vars: envRootObj.vars,
durable_objects: envRootObj.durable_objects,
services: envRootObj.experimental_services,
}}
/>
);
await waitUntilExit();
}
);
// publish
wrangler.command(
"publish [script]",
"🆙 Publish your Worker to Cloudflare.",
(yargs) => {
return yargs
.option("env", {
type: "string",
describe: "Perform on a specific environment",
})
.positional("script", {
describe: "script to upload",
type: "string",
})
.option("name", {
describe: "name to use when uploading",
type: "string",
})
.option("compatibility-date", {
describe: "Date to use for compatibility checks",
type: "string",
})
.option("compatibility-flags", {
describe: "Flags to use for compatibility checks",
type: "array",
alias: "compatibility-flag",
})
.option("latest", {
describe: "Use the latest version of the worker runtime",
type: "boolean",
default: false,
})
.option("experimental-public", {
describe: "Static assets to be served",
type: "string",
})
.option("site", {
describe: "Root folder of static assets for Workers Sites",
type: "string",
})
.option("site-include", {
describe:
"Array of .gitignore-style patterns that match file or directory names from the sites directory. Only matched items will be uploaded.",
type: "string",
array: true,
})
.option("site-exclude", {
describe:
"Array of .gitignore-style patterns that match file or directory names from the sites directory. Matched items will not be uploaded.",
type: "string",
array: true,
})
.option("triggers", {
describe: "cron schedules to attach",
alias: ["schedule", "schedules"],
type: "array",
})
.option("routes", {
describe: "routes to upload",
alias: "route",
type: "array",
})
.option("services", {
describe: "experimental support for services",
type: "boolean",
default: "false",
hidden: true,
})
.option("jsx-factory", {
describe: "The function that is called for each JSX element",
type: "string",
})
.option("jsx-fragment", {
describe: "The function that is called for each JSX fragment",
type: "string",
});
},
async (args) => {
if (args.local) {
throw new NotImplementedError(
"🚫 Local publishing is not yet supported"
);
}
if (args["experimental-public"]) {
console.warn(
"🚨 The --experimental-public field is experimental and will change in the future."
);
}
if (args.public) {
throw new Error(
"🚨 The --public field has been renamed to --experimental-public, and will change behaviour in the future."
);
}
const config = args.config as Config;
if (args.latest) {
console.warn(
"⚠️ 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"
);
}
if (!args.local) {
// -- snip, extract --
const loggedIn = await loginOrRefreshIfRequired();
if (!loggedIn) {
// didn't login, let's just quit
console.log("Did not login, quitting...");
return;
}
if (!config.account_id) {
config.account_id = await getAccountId();
if (!config.account_id) {
throw new Error("No account id found, quitting...");
}
}
// -- snip, end --
}
const assetPaths = getAssetPaths(
config,
args["experimental-public"] || args.site,
args.siteInclude,
args.siteExclude
);
await publish({
config: args.config as Config,
name: args.name,
script: args.script,
env: args.env,
compatibilityDate: args.latest
? new Date().toISOString().substring(0, 10)
: args["compatibility-date"],
compatibilityFlags: args["compatibility-flags"] as string[],
triggers: args.triggers,
jsxFactory: args["jsx-factory"],
jsxFragment: args["jsx-fragment"],
routes: args.routes,
assetPaths,
format: undefined, // TODO: add args for this
legacyEnv: undefined, // TODO: get this from somewhere... config?
experimentalPublic: args["experimental-public"] !== undefined,
});
}
);
// tail
wrangler.command(
"tail [name]",
"🦚 Starts a log tailing session for a deployed Worker.",
(yargs) => {
return (
yargs
.positional("name", {
describe: "name of the worker",
type: "string",
})
// TODO: auto-detect if this should be json or pretty based on atty
.option("format", {
default: "json",
choices: ["json", "pretty"],
describe: "The format of log entries",
})
.option("status", {
choices: ["ok", "error", "canceled"],
describe: "Filter by invocation status",
})
.option("header", {
type: "string",
describe: "Filter by HTTP header",
})
.option("method", {
type: "string",
describe: "Filter by HTTP method",
})
.option("sampling-rate", {
type: "number",
describe: "Adds a percentage of requests to log sampling rate",
})
.option("search", {
type: "string",
describe: "Filter by a text match in console.log messages",
})
.option("env", {
type: "string",
describe: "Perform on a specific environment",
})
);
// TODO: filter by client ip, which can be 'self' or an ip address
},
async (args) => {
if (args.local) {
throw new NotImplementedError(
`local mode is not yet supported for this command`
);
}
const config = args.config as Config;
if (!(args.name || config.name)) {
throw new Error("Missing script name");
}
const scriptName = `${args.name || config.name}${
args.env ? `-${args.env}` : ""
}`;
// -- snip, extract --
const loggedIn = await loginOrRefreshIfRequired();
if (!loggedIn) {
// didn't login, let's just quit
console.log("Did not login, quitting...");
return;
}
if (!config.account_id) {
config.account_id = await getAccountId();
if (!config.account_id) {
throw new Error("No account id found, quitting...");
}
}
// -- snip, end --
const accountId = config.account_id;
const filters = {
status: args.status as "ok" | "error" | "canceled",
header: args.header,
method: args.method,
"sampling-rate": args["sampling-rate"],
search: args.search,
};
const { tail, expiration, /* sendHeartbeat, */ deleteTail } =
await createTail(accountId, scriptName, filters);
console.log(
`successfully created tail, expires at ${expiration.toLocaleString()}`
);
onExit(async () => {
tail.terminate();
await deleteTail();
});
tail.on("message", (data) => {
console.log(JSON.stringify(JSON.parse(data.toString()), null, " "));
});
while (tail.readyState !== tail.OPEN) {
switch (tail.readyState) {
case tail.CONNECTING:
await setTimeout(1000);
break;
case tail.CLOSING:
await setTimeout(1000);
break;
case tail.CLOSED:
process.exit(1);
}
}
console.log(`Connected to ${scriptName}, waiting for logs...`);
}
);
// preview
wrangler.command(
"preview [method] [body]",
false,
(yargs) => {
return yargs
.positional("method", {
describe: "Type of request to preview your worker",
choices: ["GET", "POST"],
default: ["GET"],
})
.positional("body", {
type: "string",
describe: "Body string to post to your preview worker request.",
default: "Null",
})
.option("env", {
type: "string",
describe: "Perform on a specific environment",
})
.option("watch", {
default: true,
describe: "Enable live preview",
type: "boolean",
});
},
() => {
// "🔬 [DEPRECATED] Preview your code temporarily on https://cloudflareworkers.com"
throw new DeprecationError(
"`wrangler preview` has been deprecated, please refer to TODO://some/path for alternatives"
);
}
);
// route
wrangler.command(
"route",
false, // I think we want to hide this command
// "➡️ List or delete worker routes",
(routeYargs) => {
return routeYargs
.command(
"list",
"List a route associated with a zone",
(yargs) => {
return yargs
.option("env", {
type: "string",
describe: "Perform on a specific environment",
})
.option("zone", {
type: "string",
describe: "zone id",
})
.positional("zone", {
describe: "zone id",
type: "string",
});
},
async (args) => {
console.log(":route list", args);
// TODO: use environment (current wrangler doesn't do so?)
const zone = args.zone || (args.config as Config).zone_id;
if (!zone) {
throw new Error("missing zone id");
}
console.log(await fetchResult(`/zones/${zone}/workers/routes`));
}
)
.command(
"delete <id>",