wrangler 0.0.0-e043479 → 0.0.0-e0a0509

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 (96) hide show
  1. package/README.md +27 -14
  2. package/bin/wrangler.js +13 -1
  3. package/miniflare-dist/index.mjs +4886 -0
  4. package/package.json +38 -25
  5. package/pages/functions/buildPlugin.ts +92 -0
  6. package/pages/functions/buildWorker.ts +80 -4
  7. package/pages/functions/filepath-routing.test.ts +136 -25
  8. package/pages/functions/filepath-routing.ts +14 -3
  9. package/pages/functions/routes.ts +6 -1
  10. package/pages/functions/template-plugin.ts +146 -0
  11. package/pages/functions/template-worker.ts +21 -8
  12. package/src/__tests__/config-cache.test.ts +36 -0
  13. package/src/__tests__/configuration.test.ts +1294 -355
  14. package/src/__tests__/dev.test.tsx +592 -12
  15. package/src/__tests__/guess-worker-format.test.ts +37 -18
  16. package/src/__tests__/helpers/mock-account-id.ts +4 -0
  17. package/src/__tests__/helpers/mock-cfetch.ts +8 -4
  18. package/src/__tests__/helpers/mock-console.ts +20 -3
  19. package/src/__tests__/helpers/mock-dialogs.ts +26 -1
  20. package/src/__tests__/helpers/mock-http-server.ts +46 -0
  21. package/src/__tests__/helpers/mock-istty.ts +27 -0
  22. package/src/__tests__/helpers/mock-kv.ts +9 -16
  23. package/src/__tests__/helpers/mock-oauth-flow.ts +255 -0
  24. package/src/__tests__/helpers/run-in-tmp.ts +10 -0
  25. package/src/__tests__/helpers/write-worker-source.ts +31 -0
  26. package/src/__tests__/https-options.test.ts +130 -0
  27. package/src/__tests__/index.test.ts +129 -753
  28. package/src/__tests__/init.test.ts +1516 -0
  29. package/src/__tests__/jest.setup.ts +13 -5
  30. package/src/__tests__/kv.test.ts +208 -174
  31. package/src/__tests__/logger.test.ts +115 -0
  32. package/src/__tests__/pages.test.ts +327 -0
  33. package/src/__tests__/parse.test.ts +4 -0
  34. package/src/__tests__/publish.test.ts +3313 -505
  35. package/src/__tests__/r2.test.ts +38 -26
  36. package/src/__tests__/route.test.ts +45 -0
  37. package/src/__tests__/secret.test.ts +55 -34
  38. package/src/__tests__/tail.test.ts +146 -20
  39. package/src/__tests__/user.test.ts +131 -0
  40. package/src/__tests__/whoami.test.tsx +26 -7
  41. package/src/bundle.ts +43 -7
  42. package/src/cfetch/internal.ts +9 -5
  43. package/src/cli.ts +5 -11
  44. package/src/config/README.md +7 -7
  45. package/src/config/config.ts +69 -105
  46. package/src/config/environment.ts +93 -4
  47. package/src/config/index.ts +15 -4
  48. package/src/config/validation-helpers.ts +228 -23
  49. package/src/config/validation.ts +559 -154
  50. package/src/config-cache.ts +45 -0
  51. package/src/{api/preview.ts → create-worker-preview.ts} +41 -13
  52. package/src/{api/form_data.ts → create-worker-upload-form.ts} +33 -10
  53. package/src/dev/dev-vars.ts +36 -0
  54. package/src/dev/dev.tsx +418 -0
  55. package/src/dev/local.tsx +312 -0
  56. package/src/dev/remote.tsx +213 -0
  57. package/src/dev/use-esbuild.ts +118 -0
  58. package/src/dialogs.tsx +16 -6
  59. package/src/entry.ts +222 -12
  60. package/src/environment-variables.ts +3 -1
  61. package/src/errors.ts +11 -0
  62. package/src/https-options.ts +123 -0
  63. package/src/index.tsx +878 -482
  64. package/src/inspect.ts +25 -11
  65. package/src/intl-polyfill.d.ts +139 -0
  66. package/src/{kv.tsx → kv.ts} +7 -27
  67. package/src/logger.ts +66 -0
  68. package/src/miniflare-cli/README.md +30 -0
  69. package/src/miniflare-cli/enum-keys.ts +17 -0
  70. package/src/miniflare-cli/index.ts +41 -0
  71. package/src/miniflare-cli/request-context.ts +40 -0
  72. package/src/module-collection.ts +94 -6
  73. package/src/open-in-browser.ts +18 -5
  74. package/src/package-manager.ts +9 -8
  75. package/src/pages.tsx +892 -62
  76. package/src/parse.ts +4 -2
  77. package/src/proxy.ts +128 -55
  78. package/src/publish.ts +305 -199
  79. package/src/selfsigned.d.ts +29 -0
  80. package/src/sites.tsx +88 -33
  81. package/src/tail/index.ts +2 -2
  82. package/src/tail/printing.ts +13 -8
  83. package/src/update-check.ts +19 -0
  84. package/src/user.tsx +244 -104
  85. package/src/whoami.tsx +2 -1
  86. package/src/{api/worker.ts → worker.ts} +15 -29
  87. package/templates/gitignore +171 -0
  88. package/templates/static-asset-facade.js +1 -1
  89. package/wrangler-dist/cli.js +23568 -50489
  90. package/src/__tests__/helpers/mock-user.ts +0 -27
  91. package/src/__tests__/logout.test.ts +0 -50
  92. package/src/__tests__/sentry.test.ts +0 -146
  93. package/src/dev.tsx +0 -960
  94. package/src/guess-worker-format.ts +0 -68
  95. package/src/reporting.ts +0 -115
  96. package/wrangler-dist/cli.js.map +0 -7
package/src/pages.tsx CHANGED
@@ -2,36 +2,102 @@
2
2
 
3
3
  import { execSync, spawn } from "node:child_process";
4
4
  import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { readdir, readFile, stat } from "node:fs/promises";
5
6
  import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
+ import { dirname, join, sep } from "node:path";
8
+ import { cwd } from "node:process";
7
9
  import { URL } from "node:url";
10
+ import { hash } from "blake3-wasm";
8
11
  import { watch } from "chokidar";
12
+ import { render, Text } from "ink";
13
+ import SelectInput from "ink-select-input";
14
+ import Spinner from "ink-spinner";
15
+ import Table from "ink-table";
9
16
  import { getType } from "mime";
17
+ import prettyBytes from "pretty-bytes";
18
+ import React from "react";
19
+ import { format as timeagoFormat } from "timeago.js";
20
+ import { File, FormData } from "undici";
21
+ import { buildPlugin } from "../pages/functions/buildPlugin";
10
22
  import { buildWorker } from "../pages/functions/buildWorker";
11
23
  import { generateConfigFromFileTree } from "../pages/functions/filepath-routing";
12
24
  import { writeRoutesModule } from "../pages/functions/routes";
25
+ import { fetchResult } from "./cfetch";
26
+ import { getConfigCache, saveToConfigCache } from "./config-cache";
27
+ import { prompt } from "./dialogs";
28
+ import { FatalError } from "./errors";
29
+ import { logger } from "./logger";
30
+ import { getRequestContextCheckOptions } from "./miniflare-cli/request-context";
13
31
  import openInBrowser from "./open-in-browser";
14
32
  import { toUrlPath } from "./paths";
33
+ import { requireAuth } from "./user";
15
34
  import type { Config } from "../pages/functions/routes";
16
35
  import type { Headers, Request, fetch } from "@miniflare/core";
17
36
  import type { BuildResult } from "esbuild";
18
37
  import type { MiniflareOptions } from "miniflare";
19
- import type { BuilderCallback } from "yargs";
38
+ import type { BuilderCallback, CommandModule } from "yargs";
39
+
40
+ export type Project = {
41
+ name: string;
42
+ subdomain: string;
43
+ domains: Array<string>;
44
+ source?: {
45
+ type: string;
46
+ };
47
+ latest_deployment?: {
48
+ modified_on: string;
49
+ };
50
+ created_on: string;
51
+ production_branch: string;
52
+ };
53
+
54
+ export type Deployment = {
55
+ id: string;
56
+ environment: string;
57
+ deployment_trigger: {
58
+ metadata: {
59
+ commit_hash: string;
60
+ branch: string;
61
+ };
62
+ };
63
+ url: string;
64
+ latest_stage: {
65
+ status: string;
66
+ ended_on: string;
67
+ };
68
+ project_name: string;
69
+ };
70
+
71
+ interface PagesConfigCache {
72
+ account_id?: string;
73
+ project_name?: string;
74
+ }
75
+
76
+ const PAGES_CONFIG_CACHE_FILENAME = "pages.json";
20
77
 
21
78
  // Defer importing miniflare until we really need it. This takes ~0.5s
22
79
  // and also modifies some `stream/web` and `undici` prototypes, so we
23
80
  // don't want to do this if pages commands aren't being called.
24
81
 
25
- const EXIT_CALLBACKS: (() => void)[] = [];
26
- const EXIT = (message?: string, code?: number) => {
27
- if (message) console.log(message);
28
- if (code) process.exitCode = code;
29
- EXIT_CALLBACKS.forEach((callback) => callback());
30
- process.exit(code);
82
+ export const pagesBetaWarning =
83
+ "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose";
84
+
85
+ const isInPagesCI = !!process.env.CF_PAGES;
86
+
87
+ const CLEANUP_CALLBACKS: (() => void)[] = [];
88
+ const CLEANUP = () => {
89
+ CLEANUP_CALLBACKS.forEach((callback) => callback());
90
+ RUNNING_BUILDERS.forEach((builder) => builder.stop?.());
31
91
  };
32
92
 
33
- process.on("SIGINT", () => EXIT());
34
- process.on("SIGTERM", () => EXIT());
93
+ process.on("SIGINT", () => {
94
+ CLEANUP();
95
+ process.exit();
96
+ });
97
+ process.on("SIGTERM", () => {
98
+ CLEANUP();
99
+ process.exit();
100
+ });
35
101
 
36
102
  function isWindows() {
37
103
  return process.platform === "win32";
@@ -91,7 +157,7 @@ function getPort(pid: number) {
91
157
  const match = matches[0];
92
158
  if (match) return parseInt(match[1]);
93
159
  } catch (thrown) {
94
- console.error(
160
+ logger.error(
95
161
  `Error scanning for ports of process with PID ${pid}: ${thrown}`
96
162
  );
97
163
  }
@@ -104,13 +170,15 @@ async function spawnProxyProcess({
104
170
  port?: number;
105
171
  command: (string | number)[];
106
172
  }): Promise<void | number> {
107
- if (command.length === 0)
108
- return EXIT(
173
+ if (command.length === 0) {
174
+ CLEANUP();
175
+ throw new FatalError(
109
176
  "Must specify a directory of static assets to serve or a command to run.",
110
177
  1
111
178
  );
179
+ }
112
180
 
113
- console.log(`Running ${command.join(" ")}...`);
181
+ logger.log(`Running ${command.join(" ")}...`);
114
182
  const proxy = spawn(
115
183
  command[0].toString(),
116
184
  command.slice(1).map((value) => value.toString()),
@@ -122,30 +190,30 @@ async function spawnProxyProcess({
122
190
  },
123
191
  }
124
192
  );
125
- EXIT_CALLBACKS.push(() => {
193
+ CLEANUP_CALLBACKS.push(() => {
126
194
  proxy.kill();
127
195
  });
128
196
 
129
197
  proxy.stdout.on("data", (data) => {
130
- console.log(`[proxy]: ${data}`);
198
+ logger.log(`[proxy]: ${data}`);
131
199
  });
132
200
 
133
201
  proxy.stderr.on("data", (data) => {
134
- console.error(`[proxy]: ${data}`);
202
+ logger.error(`[proxy]: ${data}`);
135
203
  });
136
204
 
137
205
  proxy.on("close", (code) => {
138
- console.error(`Proxy exited with status ${code}.`);
206
+ logger.error(`Proxy exited with status ${code}.`);
139
207
  });
140
208
 
141
209
  // Wait for proxy process to start...
142
210
  while (!proxy.pid) {}
143
211
 
144
212
  if (port === undefined) {
145
- console.log(
213
+ logger.log(
146
214
  `Sleeping ${SECONDS_TO_WAIT_FOR_PROXY} seconds to allow proxy process to start before attempting to automatically determine port...`
147
215
  );
148
- console.log("To skip, specify the proxy port with --proxy.");
216
+ logger.log("To skip, specify the proxy port with --proxy.");
149
217
  await sleep(SECONDS_TO_WAIT_FOR_PROXY * 1000);
150
218
 
151
219
  port = getPids(proxy.pid)
@@ -153,12 +221,13 @@ async function spawnProxyProcess({
153
221
  .filter((port) => port !== undefined)[0];
154
222
 
155
223
  if (port === undefined) {
156
- return EXIT(
224
+ CLEANUP();
225
+ throw new FatalError(
157
226
  "Could not automatically determine proxy port. Please specify the proxy port with --proxy.",
158
227
  1
159
228
  );
160
229
  } else {
161
- console.log(`Automatically determined the proxy port to be ${port}.`);
230
+ logger.log(`Automatically determined the proxy port to be ${port}.`);
162
231
  }
163
232
  }
164
233
 
@@ -425,12 +494,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
425
494
  }).on("change", (path) => {
426
495
  switch (path) {
427
496
  case headersFile: {
428
- console.log("_headers modified. Re-evaluating...");
497
+ logger.log("_headers modified. Re-evaluating...");
429
498
  headersMatcher = generateHeadersMatcher(headersFile);
430
499
  break;
431
500
  }
432
501
  case redirectsFile: {
433
- console.log("_redirects modified. Re-evaluating...");
502
+ logger.log("_redirects modified. Re-evaluating...");
434
503
  redirectsMatcher = generateRedirectsMatcher(redirectsFile);
435
504
  break;
436
505
  }
@@ -643,7 +712,7 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
643
712
  const RUNNING_BUILDERS: BuildResult[] = [];
644
713
 
645
714
  async function buildFunctions({
646
- scriptPath,
715
+ outfile,
647
716
  outputConfigPath,
648
717
  functionsDirectory,
649
718
  minify = false,
@@ -651,8 +720,10 @@ async function buildFunctions({
651
720
  fallbackService = "ASSETS",
652
721
  watch = false,
653
722
  onEnd,
723
+ plugin = false,
724
+ buildOutputDirectory,
654
725
  }: {
655
- scriptPath: string;
726
+ outfile: string;
656
727
  outputConfigPath?: string;
657
728
  functionsDirectory: string;
658
729
  minify?: boolean;
@@ -660,6 +731,8 @@ async function buildFunctions({
660
731
  fallbackService?: string;
661
732
  watch?: boolean;
662
733
  onEnd?: () => void;
734
+ plugin?: boolean;
735
+ buildOutputDirectory?: string;
663
736
  }) {
664
737
  RUNNING_BUILDERS.forEach(
665
738
  (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
@@ -686,19 +759,488 @@ async function buildFunctions({
686
759
  outfile: routesModule,
687
760
  });
688
761
 
689
- RUNNING_BUILDERS.push(
690
- await buildWorker({
691
- routesModule,
692
- outfile: scriptPath,
693
- minify,
694
- sourcemap,
695
- fallbackService,
696
- watch,
697
- onEnd,
698
- })
699
- );
762
+ if (plugin) {
763
+ RUNNING_BUILDERS.push(
764
+ await buildPlugin({
765
+ routesModule,
766
+ outfile,
767
+ minify,
768
+ sourcemap,
769
+ watch,
770
+ onEnd,
771
+ })
772
+ );
773
+ } else {
774
+ RUNNING_BUILDERS.push(
775
+ await buildWorker({
776
+ routesModule,
777
+ outfile,
778
+ minify,
779
+ sourcemap,
780
+ fallbackService,
781
+ watch,
782
+ onEnd,
783
+ buildOutputDirectory,
784
+ })
785
+ );
786
+ }
787
+ }
788
+
789
+ interface CreateDeploymentArgs {
790
+ directory: string;
791
+ projectName?: string;
792
+ branch?: string;
793
+ commitHash?: string;
794
+ commitMessage?: string;
795
+ commitDirty?: boolean;
700
796
  }
701
797
 
798
+ const createDeployment: CommandModule<
799
+ CreateDeploymentArgs,
800
+ CreateDeploymentArgs
801
+ > = {
802
+ describe: "🆙 Publish a directory of static assets as a Pages deployment",
803
+ builder: (yargs) => {
804
+ return yargs
805
+ .positional("directory", {
806
+ type: "string",
807
+ demandOption: true,
808
+ description: "The directory of static files to upload",
809
+ })
810
+ .options({
811
+ "project-name": {
812
+ type: "string",
813
+ description: "The name of the project you want to deploy to",
814
+ },
815
+ branch: {
816
+ type: "string",
817
+ description: "The name of the branch you want to deploy to",
818
+ },
819
+ "commit-hash": {
820
+ type: "string",
821
+ description: "The SHA to attach to this deployment",
822
+ },
823
+ "commit-message": {
824
+ type: "string",
825
+ description: "The commit message to attach to this deployment",
826
+ },
827
+ "commit-dirty": {
828
+ type: "boolean",
829
+ description:
830
+ "Whether or not the workspace should be considered dirty for this deployment",
831
+ },
832
+ })
833
+ .epilogue(pagesBetaWarning);
834
+ },
835
+ handler: async ({
836
+ directory,
837
+ projectName,
838
+ branch,
839
+ commitHash,
840
+ commitMessage,
841
+ commitDirty,
842
+ }) => {
843
+ if (!directory) {
844
+ throw new FatalError("Must specify a directory.", 1);
845
+ }
846
+
847
+ const config = getConfigCache<PagesConfigCache>(
848
+ PAGES_CONFIG_CACHE_FILENAME
849
+ );
850
+ const accountId = await requireAuth(config);
851
+
852
+ projectName ??= config.project_name;
853
+
854
+ const isInteractive = process.stdin.isTTY;
855
+ if (!projectName && isInteractive) {
856
+ const projects = (await listProjects({ accountId })).filter(
857
+ (project) => !project.source
858
+ );
859
+
860
+ let existingOrNew: "existing" | "new" = "new";
861
+
862
+ if (projects.length > 0) {
863
+ existingOrNew = await new Promise<"new" | "existing">((resolve) => {
864
+ const { unmount } = render(
865
+ <>
866
+ <Text>
867
+ No project selected. Would you like to create one or use an
868
+ existing project?
869
+ </Text>
870
+ <SelectInput
871
+ items={[
872
+ {
873
+ key: "new",
874
+ label: "Create a new project",
875
+ value: "new",
876
+ },
877
+ {
878
+ key: "existing",
879
+ label: "Use an existing project",
880
+ value: "existing",
881
+ },
882
+ ]}
883
+ onSelect={async (selected) => {
884
+ resolve(selected.value as "new" | "existing");
885
+ unmount();
886
+ }}
887
+ />
888
+ </>
889
+ );
890
+ });
891
+ }
892
+
893
+ switch (existingOrNew) {
894
+ case "existing": {
895
+ projectName = await new Promise((resolve) => {
896
+ const { unmount } = render(
897
+ <>
898
+ <Text>Select a project:</Text>
899
+ <SelectInput
900
+ items={projects.map((project) => ({
901
+ key: project.name,
902
+ label: project.name,
903
+ value: project,
904
+ }))}
905
+ onSelect={async (selected) => {
906
+ resolve(selected.value.name);
907
+ unmount();
908
+ }}
909
+ />
910
+ </>
911
+ );
912
+ });
913
+ break;
914
+ }
915
+ case "new": {
916
+ projectName = await prompt("Enter the name of your new project:");
917
+
918
+ if (!projectName) {
919
+ throw new FatalError("Must specify a project name.", 1);
920
+ }
921
+
922
+ let isGitDir = true;
923
+ try {
924
+ execSync(`git rev-parse --is-inside-work-tree`, {
925
+ stdio: "ignore",
926
+ });
927
+ } catch (err) {
928
+ isGitDir = false;
929
+ }
930
+
931
+ const productionBranch = await prompt(
932
+ "Enter the production branch name:",
933
+ "text",
934
+ isGitDir
935
+ ? execSync(`git rev-parse --abbrev-ref HEAD`).toString().trim()
936
+ : "production"
937
+ );
938
+
939
+ if (!productionBranch) {
940
+ throw new FatalError("Must specify a production branch.", 1);
941
+ }
942
+
943
+ await fetchResult<Project>(`/accounts/${accountId}/pages/projects`, {
944
+ method: "POST",
945
+ body: JSON.stringify({
946
+ name: projectName,
947
+ production_branch: productionBranch,
948
+ }),
949
+ });
950
+
951
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
952
+ account_id: accountId,
953
+ project_name: projectName,
954
+ });
955
+
956
+ logger.log(`✨ Successfully created the '${projectName}' project.`);
957
+ break;
958
+ }
959
+ }
960
+ }
961
+
962
+ if (!projectName) {
963
+ throw new FatalError("Must specify a project name.", 1);
964
+ }
965
+
966
+ // We infer git info by default is not passed in
967
+
968
+ let isGitDir = true;
969
+ try {
970
+ execSync(`git rev-parse --is-inside-work-tree`, {
971
+ stdio: "ignore",
972
+ });
973
+ } catch (err) {
974
+ isGitDir = false;
975
+ }
976
+
977
+ let isGitDirty = false;
978
+
979
+ if (isGitDir) {
980
+ try {
981
+ isGitDirty = Boolean(
982
+ execSync(`git status --porcelain`).toString().length
983
+ );
984
+
985
+ if (!branch) {
986
+ branch = execSync(`git rev-parse --abbrev-ref HEAD`)
987
+ .toString()
988
+ .trim();
989
+ }
990
+
991
+ if (!commitHash) {
992
+ commitHash = execSync(`git rev-parse HEAD`).toString().trim();
993
+ }
994
+
995
+ if (!commitMessage) {
996
+ commitMessage = execSync(`git show -s --format=%B ${commitHash}`)
997
+ .toString()
998
+ .trim();
999
+ }
1000
+ } catch (err) {}
1001
+
1002
+ if (isGitDirty && !commitDirty) {
1003
+ logger.warn(
1004
+ `Warning: Your working directory is a git repo and has uncommitted changes\nTo silense this warning, pass in --commit-dirty=true`
1005
+ );
1006
+ }
1007
+
1008
+ if (commitDirty === undefined) {
1009
+ commitDirty = isGitDirty;
1010
+ }
1011
+ }
1012
+
1013
+ let builtFunctions: string | undefined = undefined;
1014
+ const functionsDirectory = join(cwd(), "functions");
1015
+ if (existsSync(functionsDirectory)) {
1016
+ const outfile = join(tmpdir(), "./functionsWorker.js");
1017
+
1018
+ await new Promise((resolve) =>
1019
+ buildFunctions({
1020
+ outfile,
1021
+ functionsDirectory,
1022
+ onEnd: () => resolve(null),
1023
+ buildOutputDirectory: dirname(outfile),
1024
+ })
1025
+ );
1026
+
1027
+ builtFunctions = readFileSync(outfile, "utf-8");
1028
+ }
1029
+
1030
+ type File = {
1031
+ content: Buffer;
1032
+ metadata: Metadata;
1033
+ };
1034
+
1035
+ type Metadata = {
1036
+ sizeInBytes: number;
1037
+ hash: string;
1038
+ };
1039
+
1040
+ const IGNORE_LIST = [
1041
+ "_worker.js",
1042
+ "_redirects",
1043
+ "_headers",
1044
+ ".DS_Store",
1045
+ "node_modules",
1046
+ ];
1047
+
1048
+ const walk = async (
1049
+ dir: string,
1050
+ fileMap: Map<string, File> = new Map(),
1051
+ depth = 0
1052
+ ) => {
1053
+ const files = await readdir(dir);
1054
+
1055
+ await Promise.all(
1056
+ files.map(async (file) => {
1057
+ const filepath = join(dir, file);
1058
+ const filestat = await stat(filepath);
1059
+
1060
+ if (IGNORE_LIST.includes(file)) {
1061
+ return;
1062
+ }
1063
+
1064
+ if (filestat.isSymbolicLink()) {
1065
+ return;
1066
+ }
1067
+
1068
+ if (filestat.isDirectory()) {
1069
+ fileMap = await walk(filepath, fileMap, depth + 1);
1070
+ } else {
1071
+ let name;
1072
+ if (depth) {
1073
+ name = filepath.split(sep).slice(1).join("/");
1074
+ } else {
1075
+ name = file;
1076
+ }
1077
+
1078
+ // TODO: Move this to later so we don't hold as much in memory
1079
+ const fileContent = await readFile(filepath);
1080
+
1081
+ const base64Content = fileContent.toString("base64");
1082
+ const extension =
1083
+ name.split(".").length > 1 ? name.split(".").at(-1) || "" : "";
1084
+
1085
+ const content = base64Content + extension;
1086
+
1087
+ if (filestat.size > 25 * 1024 * 1024) {
1088
+ throw new Error(
1089
+ `Error: Pages only supports files up to ${prettyBytes(
1090
+ 25 * 1024 * 1024
1091
+ )} in size\n${name} is ${prettyBytes(filestat.size)} in size`
1092
+ );
1093
+ }
1094
+
1095
+ fileMap.set(name, {
1096
+ content: fileContent,
1097
+ metadata: {
1098
+ sizeInBytes: filestat.size,
1099
+ hash: hash(content).toString("hex").slice(0, 32),
1100
+ },
1101
+ });
1102
+ }
1103
+ })
1104
+ );
1105
+
1106
+ return fileMap;
1107
+ };
1108
+
1109
+ const fileMap = await walk(directory);
1110
+
1111
+ const start = Date.now();
1112
+
1113
+ const files: Array<Promise<void>> = [];
1114
+
1115
+ if (fileMap.size > 1000) {
1116
+ throw new Error(
1117
+ `Error: Pages only supports up to 1,000 files in a deployment at the moment.\nTry a smaller project perhaps?`
1118
+ );
1119
+ }
1120
+
1121
+ let counter = 0;
1122
+
1123
+ const { rerender, unmount } = render(
1124
+ <Progress done={counter} total={fileMap.size} />
1125
+ );
1126
+
1127
+ fileMap.forEach((file: File, name: string) => {
1128
+ const form = new FormData();
1129
+ form.append(
1130
+ "file",
1131
+ new File([new Uint8Array(file.content.buffer)], name)
1132
+ );
1133
+
1134
+ // TODO: Consider a retry
1135
+
1136
+ const promise = fetchResult<{ id: string }>(
1137
+ `/accounts/${accountId}/pages/projects/${projectName}/file`,
1138
+ {
1139
+ method: "POST",
1140
+ body: form,
1141
+ }
1142
+ ).then((response) => {
1143
+ counter++;
1144
+ rerender(<Progress done={counter} total={fileMap.size} />);
1145
+ if (response.id != file.metadata.hash) {
1146
+ throw new Error(
1147
+ `Looks like there was an issue uploading '${name}'. Try again perhaps?`
1148
+ );
1149
+ }
1150
+ });
1151
+
1152
+ files.push(promise);
1153
+ });
1154
+
1155
+ await Promise.all(files);
1156
+
1157
+ unmount();
1158
+
1159
+ const uploadMs = Date.now() - start;
1160
+
1161
+ logger.log(
1162
+ `✨ Success! Uploaded ${fileMap.size} files ${formatTime(uploadMs)}\n`
1163
+ );
1164
+
1165
+ const formData = new FormData();
1166
+
1167
+ formData.append(
1168
+ "manifest",
1169
+ JSON.stringify(
1170
+ Object.fromEntries(
1171
+ [...fileMap.entries()].map(([fileName, file]) => [
1172
+ `/${fileName}`,
1173
+ file.metadata.hash,
1174
+ ])
1175
+ )
1176
+ )
1177
+ );
1178
+
1179
+ if (branch) {
1180
+ formData.append("branch", branch);
1181
+ }
1182
+
1183
+ if (commitMessage) {
1184
+ formData.append("commit_message", commitMessage);
1185
+ }
1186
+
1187
+ if (commitHash) {
1188
+ formData.append("commit_hash", commitHash);
1189
+ }
1190
+
1191
+ if (commitDirty !== undefined) {
1192
+ formData.append("commit_dirty", commitDirty);
1193
+ }
1194
+
1195
+ let _headers: string | undefined,
1196
+ _redirects: string | undefined,
1197
+ _workerJS: string | undefined;
1198
+
1199
+ try {
1200
+ _headers = readFileSync(join(directory, "_headers"), "utf-8");
1201
+ } catch {}
1202
+
1203
+ try {
1204
+ _redirects = readFileSync(join(directory, "_redirects"), "utf-8");
1205
+ } catch {}
1206
+
1207
+ try {
1208
+ _workerJS = readFileSync(join(directory, "_worker.js"), "utf-8");
1209
+ } catch {}
1210
+
1211
+ if (_headers) {
1212
+ formData.append("_headers", new File([_headers], "_headers"));
1213
+ }
1214
+
1215
+ if (_redirects) {
1216
+ formData.append("_redirects", new File([_redirects], "_redirects"));
1217
+ }
1218
+
1219
+ if (builtFunctions) {
1220
+ formData.append("_worker.js", new File([builtFunctions], "_worker.js"));
1221
+ } else if (_workerJS) {
1222
+ formData.append("_worker.js", new File([_workerJS], "_worker.js"));
1223
+ }
1224
+
1225
+ const deploymentResponse = await fetchResult<Deployment>(
1226
+ `/accounts/${accountId}/pages/projects/${projectName}/deployments`,
1227
+ {
1228
+ method: "POST",
1229
+ body: formData,
1230
+ }
1231
+ );
1232
+
1233
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1234
+ account_id: accountId,
1235
+ project_name: projectName,
1236
+ });
1237
+
1238
+ logger.log(
1239
+ `✨ Deployment complete! Take a peek over at ${deploymentResponse.url}`
1240
+ );
1241
+ },
1242
+ };
1243
+
702
1244
  export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
703
1245
  return yargs
704
1246
  .command(
@@ -759,7 +1301,8 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
759
1301
  description: "Auto reload HTML pages when change is detected",
760
1302
  },
761
1303
  // TODO: Miniflare user options
762
- });
1304
+ })
1305
+ .epilogue(pagesBetaWarning);
763
1306
  },
764
1307
  async ({
765
1308
  local,
@@ -773,8 +1316,11 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
773
1316
  "live-reload": liveReload,
774
1317
  _: [_pages, _dev, ...remaining],
775
1318
  }) => {
1319
+ // Beta message for `wrangler pages <commands>` usage
1320
+ logger.log(pagesBetaWarning);
1321
+
776
1322
  if (!local) {
777
- console.error("Only local mode is supported at the moment.");
1323
+ logger.error("Only local mode is supported at the moment.");
778
1324
  return;
779
1325
  }
780
1326
 
@@ -801,33 +1347,37 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
801
1347
  );
802
1348
 
803
1349
  if (usingFunctions) {
804
- const scriptPath = join(tmpdir(), "./functionsWorker.js");
1350
+ const outfile = join(tmpdir(), "./functionsWorker.js");
805
1351
 
806
- console.log(`Compiling worker to "${scriptPath}"...`);
1352
+ logger.log(`Compiling worker to "${outfile}"...`);
807
1353
 
808
- await buildFunctions({
809
- scriptPath,
810
- functionsDirectory,
811
- sourcemap: true,
812
- watch: true,
813
- onEnd: () => scriptReadyResolve(),
814
- });
1354
+ try {
1355
+ await buildFunctions({
1356
+ outfile,
1357
+ functionsDirectory,
1358
+ sourcemap: true,
1359
+ watch: true,
1360
+ onEnd: () => scriptReadyResolve(),
1361
+ buildOutputDirectory: directory,
1362
+ });
1363
+ } catch {}
815
1364
 
816
1365
  watch([functionsDirectory], {
817
1366
  persistent: true,
818
1367
  ignoreInitial: true,
819
1368
  }).on("all", async () => {
820
1369
  await buildFunctions({
821
- scriptPath,
1370
+ outfile,
822
1371
  functionsDirectory,
823
1372
  sourcemap: true,
824
1373
  watch: true,
825
1374
  onEnd: () => scriptReadyResolve(),
1375
+ buildOutputDirectory: directory,
826
1376
  });
827
1377
  });
828
1378
 
829
1379
  miniflareArgs = {
830
- scriptPath,
1380
+ scriptPath: outfile,
831
1381
  };
832
1382
  } else {
833
1383
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -843,7 +1393,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
843
1393
  scriptPath,
844
1394
  };
845
1395
  } else {
846
- console.log("No functions. Shimming...");
1396
+ logger.log("No functions. Shimming...");
847
1397
  miniflareArgs = {
848
1398
  // TODO: The fact that these request/response hacks are necessary is ridiculous.
849
1399
  // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
@@ -875,6 +1425,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
875
1425
  ? await generateAssetsFetch(directory)
876
1426
  : invalidAssetsFetch;
877
1427
 
1428
+ const requestContextCheckOptions =
1429
+ await getRequestContextCheckOptions();
1430
+
878
1431
  const miniflare = new Miniflare({
879
1432
  port,
880
1433
  watch: true,
@@ -895,7 +1448,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
895
1448
  // User bindings
896
1449
  bindings: {
897
1450
  ...Object.fromEntries(
898
- bindings.map((binding) => binding.toString().split("="))
1451
+ bindings
1452
+ .map((binding) => binding.toString().split("="))
1453
+ .map(([key, ...values]) => [key, values.join("=")])
899
1454
  ),
900
1455
  },
901
1456
 
@@ -908,7 +1463,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
908
1463
  url.host = `localhost:${proxyPort}`;
909
1464
  return await fetch(url, request);
910
1465
  } catch (thrown) {
911
- console.error(`Could not proxy request: ${thrown}`);
1466
+ logger.error(`Could not proxy request: ${thrown}`);
912
1467
 
913
1468
  // TODO: Pretty error page
914
1469
  return new Response(
@@ -920,7 +1475,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
920
1475
  try {
921
1476
  return await assetsFetch(request);
922
1477
  } catch (thrown) {
923
- console.error(`Could not serve static asset: ${thrown}`);
1478
+ logger.error(`Could not serve static asset: ${thrown}`);
924
1479
 
925
1480
  // TODO: Pretty error page
926
1481
  return new Response(
@@ -937,13 +1492,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
937
1492
  cachePersist: true,
938
1493
  liveReload,
939
1494
 
1495
+ ...requestContextCheckOptions,
940
1496
  ...miniflareArgs,
941
1497
  });
942
1498
 
943
1499
  try {
944
1500
  // `startServer` might throw if user code contains errors
945
1501
  const server = await miniflare.startServer();
946
- console.log(`Serving at http://localhost:${port}/`);
1502
+ logger.log(`Serving at http://localhost:${port}/`);
947
1503
 
948
1504
  if (process.env.BROWSER !== "none") {
949
1505
  await openInBrowser(`http://localhost:${port}/`);
@@ -958,13 +1514,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
958
1514
  });
959
1515
  }
960
1516
 
961
- EXIT_CALLBACKS.push(() => {
1517
+ CLEANUP_CALLBACKS.push(() => {
962
1518
  server.close();
963
1519
  miniflare.dispose().catch((err) => miniflare.log.error(err));
964
1520
  });
965
1521
  } catch (e) {
966
1522
  miniflare.log.error(e as Error);
967
- EXIT("Could not start Miniflare.", 1);
1523
+ CLEANUP();
1524
+ throw new FatalError("Could not start Miniflare.", 1);
968
1525
  }
969
1526
  }
970
1527
  )
@@ -982,7 +1539,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
982
1539
  description: "The directory of Pages Functions",
983
1540
  })
984
1541
  .options({
985
- "script-path": {
1542
+ outfile: {
986
1543
  type: "string",
987
1544
  default: "_worker.js",
988
1545
  description: "The location of the output Worker script",
@@ -1014,28 +1571,260 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1014
1571
  description:
1015
1572
  "Watch for changes to the functions and automatically rebuild the Worker script",
1016
1573
  },
1017
- }),
1574
+ plugin: {
1575
+ type: "boolean",
1576
+ default: false,
1577
+ description: "Build a plugin rather than a Worker script",
1578
+ },
1579
+ })
1580
+ .epilogue(pagesBetaWarning),
1018
1581
  async ({
1019
1582
  directory,
1020
- "script-path": scriptPath,
1583
+ outfile,
1021
1584
  "output-config-path": outputConfigPath,
1022
1585
  minify,
1023
1586
  sourcemap,
1024
1587
  fallbackService,
1025
1588
  watch,
1589
+ plugin,
1026
1590
  }) => {
1591
+ if (!isInPagesCI) {
1592
+ // Beta message for `wrangler pages <commands>` usage
1593
+ logger.log(pagesBetaWarning);
1594
+ }
1595
+
1027
1596
  await buildFunctions({
1028
- scriptPath,
1597
+ outfile,
1029
1598
  outputConfigPath,
1030
1599
  functionsDirectory: directory,
1031
1600
  minify,
1032
1601
  sourcemap,
1033
1602
  fallbackService,
1034
1603
  watch,
1604
+ plugin,
1605
+ buildOutputDirectory: dirname(outfile),
1035
1606
  });
1036
1607
  }
1037
1608
  )
1038
- );
1609
+ )
1610
+ .command("project", "⚡️ Interact with your Pages projects", (yargs) =>
1611
+ yargs
1612
+ .command(
1613
+ "list",
1614
+ "List your Cloudflare Pages projects",
1615
+ (yargs) => yargs.epilogue(pagesBetaWarning),
1616
+ async () => {
1617
+ const config = getConfigCache<PagesConfigCache>(
1618
+ PAGES_CONFIG_CACHE_FILENAME
1619
+ );
1620
+
1621
+ const accountId = await requireAuth(config);
1622
+
1623
+ const projects: Array<Project> = await listProjects({ accountId });
1624
+
1625
+ const data = projects.map((project) => {
1626
+ return {
1627
+ "Project Name": project.name,
1628
+ "Project Domains": `${project.domains.join(", ")}`,
1629
+ "Git Provider": project.source ? "Yes" : "No",
1630
+ "Last Modified": project.latest_deployment
1631
+ ? timeagoFormat(project.latest_deployment.modified_on)
1632
+ : timeagoFormat(project.created_on),
1633
+ };
1634
+ });
1635
+
1636
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1637
+ account_id: accountId,
1638
+ });
1639
+
1640
+ render(<Table data={data}></Table>);
1641
+ }
1642
+ )
1643
+ .command(
1644
+ "create [project-name]",
1645
+ "Create a new Cloudflare Pages project",
1646
+ (yargs) =>
1647
+ yargs
1648
+ .positional("project-name", {
1649
+ type: "string",
1650
+ demandOption: true,
1651
+ description: "The name of your Pages project",
1652
+ })
1653
+ .options({
1654
+ "production-branch": {
1655
+ type: "string",
1656
+ description:
1657
+ "The name of the production branch of your project",
1658
+ },
1659
+ })
1660
+ .epilogue(pagesBetaWarning),
1661
+ async ({ productionBranch, projectName }) => {
1662
+ const config = getConfigCache<PagesConfigCache>(
1663
+ PAGES_CONFIG_CACHE_FILENAME
1664
+ );
1665
+ const accountId = await requireAuth(config);
1666
+
1667
+ const isInteractive = process.stdin.isTTY;
1668
+ if (!projectName && isInteractive) {
1669
+ projectName = await prompt("Enter the name of your new project:");
1670
+ }
1671
+
1672
+ if (!projectName) {
1673
+ throw new FatalError("Must specify a project name.", 1);
1674
+ }
1675
+
1676
+ if (!productionBranch && isInteractive) {
1677
+ let isGitDir = true;
1678
+ try {
1679
+ execSync(`git rev-parse --is-inside-work-tree`, {
1680
+ stdio: "ignore",
1681
+ });
1682
+ } catch (err) {
1683
+ isGitDir = false;
1684
+ }
1685
+
1686
+ productionBranch = await prompt(
1687
+ "Enter the production branch name:",
1688
+ "text",
1689
+ isGitDir
1690
+ ? execSync(`git rev-parse --abbrev-ref HEAD`)
1691
+ .toString()
1692
+ .trim()
1693
+ : "production"
1694
+ );
1695
+ }
1696
+
1697
+ if (!productionBranch) {
1698
+ throw new FatalError("Must specify a production branch.", 1);
1699
+ }
1700
+
1701
+ const { subdomain } = await fetchResult<Project>(
1702
+ `/accounts/${accountId}/pages/projects`,
1703
+ {
1704
+ method: "POST",
1705
+ body: JSON.stringify({
1706
+ name: projectName,
1707
+ production_branch: productionBranch,
1708
+ }),
1709
+ }
1710
+ );
1711
+
1712
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1713
+ account_id: accountId,
1714
+ project_name: projectName,
1715
+ });
1716
+
1717
+ logger.log(
1718
+ `✨ Successfully created the '${projectName}' project. It will be available at https://${subdomain}/ once you create your first deployment.`
1719
+ );
1720
+ logger.log(
1721
+ `To deploy a folder of assets, run 'wrangler pages publish [directory]'.`
1722
+ );
1723
+ }
1724
+ )
1725
+ .epilogue(pagesBetaWarning)
1726
+ )
1727
+ .command(
1728
+ "deployment",
1729
+ "🚀 Interact with the deployments of a project",
1730
+ (yargs) =>
1731
+ yargs
1732
+ .command(
1733
+ "list",
1734
+ "List deployments in your Cloudflare Pages project",
1735
+ (yargs) =>
1736
+ yargs
1737
+ .options({
1738
+ "project-name": {
1739
+ type: "string",
1740
+ description:
1741
+ "The name of the project you would like to list deployments for",
1742
+ },
1743
+ })
1744
+ .epilogue(pagesBetaWarning),
1745
+ async ({ projectName }) => {
1746
+ const config = getConfigCache<PagesConfigCache>(
1747
+ PAGES_CONFIG_CACHE_FILENAME
1748
+ );
1749
+ const accountId = await requireAuth(config);
1750
+
1751
+ projectName ??= config.project_name;
1752
+
1753
+ const isInteractive = process.stdin.isTTY;
1754
+ if (!projectName && isInteractive) {
1755
+ const projects = await listProjects({ accountId });
1756
+ projectName = await new Promise((resolve) => {
1757
+ const { unmount } = render(
1758
+ <>
1759
+ <Text>Select a project:</Text>
1760
+ <SelectInput
1761
+ items={projects.map((project) => ({
1762
+ key: project.name,
1763
+ label: project.name,
1764
+ value: project,
1765
+ }))}
1766
+ onSelect={async (selected) => {
1767
+ resolve(selected.value.name);
1768
+ unmount();
1769
+ }}
1770
+ />
1771
+ </>
1772
+ );
1773
+ });
1774
+ }
1775
+
1776
+ if (!projectName) {
1777
+ throw new FatalError("Must specify a project name.", 1);
1778
+ }
1779
+
1780
+ const deployments: Array<Deployment> = await fetchResult(
1781
+ `/accounts/${accountId}/pages/projects/${projectName}/deployments`
1782
+ );
1783
+
1784
+ const titleCase = (word: string) =>
1785
+ word.charAt(0).toUpperCase() + word.slice(1);
1786
+
1787
+ const shortSha = (sha: string) => sha.slice(0, 7);
1788
+
1789
+ const getStatus = (deployment: Deployment) => {
1790
+ // Return a pretty time since timestamp if successful otherwise the status
1791
+ if (deployment.latest_stage.status === `success`) {
1792
+ return timeagoFormat(deployment.latest_stage.ended_on);
1793
+ }
1794
+ return titleCase(deployment.latest_stage.status);
1795
+ };
1796
+
1797
+ const data = deployments.map((deployment) => {
1798
+ return {
1799
+ Environment: titleCase(deployment.environment),
1800
+ Branch: deployment.deployment_trigger.metadata.branch,
1801
+ Source: shortSha(
1802
+ deployment.deployment_trigger.metadata.commit_hash
1803
+ ),
1804
+ Deployment: deployment.url,
1805
+ Status: getStatus(deployment),
1806
+ // TODO: Use a url shortener
1807
+ Build: `https://dash.cloudflare.com/${accountId}/pages/view/${deployment.project_name}/${deployment.id}`,
1808
+ };
1809
+ });
1810
+
1811
+ saveToConfigCache<PagesConfigCache>(PAGES_CONFIG_CACHE_FILENAME, {
1812
+ account_id: accountId,
1813
+ });
1814
+
1815
+ render(<Table data={data}></Table>);
1816
+ }
1817
+ )
1818
+ .command({
1819
+ command: "create [directory]",
1820
+ ...createDeployment,
1821
+ } as CommandModule)
1822
+ .epilogue(pagesBetaWarning)
1823
+ )
1824
+ .command({
1825
+ command: "publish [directory]",
1826
+ ...createDeployment,
1827
+ } as CommandModule);
1039
1828
  };
1040
1829
 
1041
1830
  const invalidAssetsFetch: typeof fetch = () => {
@@ -1043,3 +1832,44 @@ const invalidAssetsFetch: typeof fetch = () => {
1043
1832
  "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
1044
1833
  );
1045
1834
  };
1835
+
1836
+ const listProjects = async ({
1837
+ accountId,
1838
+ }: {
1839
+ accountId: string;
1840
+ }): Promise<Array<Project>> => {
1841
+ const pageSize = 10;
1842
+ let page = 1;
1843
+ const results = [];
1844
+ while (results.length % pageSize === 0) {
1845
+ const json: Array<Project> = await fetchResult(
1846
+ `/accounts/${accountId}/pages/projects`,
1847
+ {},
1848
+ new URLSearchParams({
1849
+ per_page: pageSize.toString(),
1850
+ page: page.toString(),
1851
+ })
1852
+ );
1853
+ page++;
1854
+ results.push(...json);
1855
+ if (json.length < pageSize) {
1856
+ break;
1857
+ }
1858
+ }
1859
+ return results;
1860
+ };
1861
+
1862
+ function formatTime(duration: number) {
1863
+ return `(${(duration / 1000).toFixed(2)} sec)`;
1864
+ }
1865
+
1866
+ function Progress({ done, total }: { done: number; total: number }) {
1867
+ return (
1868
+ <>
1869
+ <Text>
1870
+ <Spinner type="earth" />
1871
+ {` Uploading... (${done}/${total})\n`}
1872
+ </Text>
1873
+ </>
1874
+ );
1875
+ }