wrangler 0.0.0-e88a54e → 0.0.0-e95e5a0

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