wrangler 0.0.0-e2b7352 → 0.0.0-e2f9bb2

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 (86) hide show
  1. package/README.md +24 -13
  2. package/bin/wrangler.js +3 -0
  3. package/miniflare-dist/index.mjs +4828 -21
  4. package/package.json +29 -22
  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 +386 -81
  14. package/src/__tests__/dev.test.tsx +242 -21
  15. package/src/__tests__/guess-worker-format.test.ts +18 -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 +24 -0
  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 +25 -759
  26. package/src/__tests__/init.test.ts +1042 -0
  27. package/src/__tests__/jest.setup.ts +4 -4
  28. package/src/__tests__/kv.test.ts +130 -56
  29. package/src/__tests__/logger.test.ts +115 -0
  30. package/src/__tests__/pages.test.ts +290 -43
  31. package/src/__tests__/publish.test.ts +1699 -333
  32. package/src/__tests__/r2.test.ts +24 -8
  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 +97 -45
  36. package/src/__tests__/user.test.ts +131 -0
  37. package/src/__tests__/whoami.test.tsx +14 -9
  38. package/src/bundle.ts +15 -1
  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 +26 -3
  44. package/src/config/index.ts +2 -1
  45. package/src/config/validation-helpers.ts +26 -5
  46. package/src/config/validation.ts +233 -41
  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 +60 -34
  52. package/src/dev/local.tsx +101 -21
  53. package/src/dev/remote.tsx +23 -13
  54. package/src/dev/use-esbuild.ts +19 -5
  55. package/src/dialogs.tsx +16 -6
  56. package/src/entry.ts +38 -11
  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 +529 -303
  61. package/src/inspect.ts +17 -11
  62. package/src/logger.ts +66 -0
  63. package/src/miniflare-cli/index.ts +4 -0
  64. package/src/miniflare-cli/request-context.ts +40 -0
  65. package/src/module-collection.ts +7 -7
  66. package/src/open-in-browser.ts +5 -4
  67. package/src/package-manager.ts +9 -8
  68. package/src/pages.tsx +860 -55
  69. package/src/parse.ts +1 -1
  70. package/src/proxy.ts +25 -11
  71. package/src/publish.ts +260 -141
  72. package/src/sites.tsx +27 -14
  73. package/src/tail/index.ts +1 -1
  74. package/src/tail/printing.ts +13 -8
  75. package/src/update-check.ts +19 -0
  76. package/src/user.tsx +104 -38
  77. package/src/whoami.tsx +2 -1
  78. package/src/worker.ts +15 -6
  79. package/templates/gitignore +171 -0
  80. package/templates/static-asset-facade.js +1 -1
  81. package/wrangler-dist/cli.js +20948 -50169
  82. package/miniflare-dist/index.mjs.map +0 -7
  83. package/src/__tests__/logout.test.ts +0 -52
  84. package/src/__tests__/sentry.test.ts +0 -146
  85. package/src/reporting.ts +0 -131
  86. package/wrangler-dist/cli.js.map +0 -7
package/src/pages.tsx CHANGED
@@ -2,21 +2,78 @@
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
@@ -25,17 +82,20 @@ import type { BuilderCallback } from "yargs";
25
82
  export const pagesBetaWarning =
26
83
  "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose";
27
84
 
28
- const EXIT_CALLBACKS: (() => void)[] = [];
29
- const EXIT = (message?: string, code?: number) => {
30
- if (message) console.log(message);
31
- if (code) process.exitCode = code;
32
- EXIT_CALLBACKS.forEach((callback) => callback());
85
+ const CLEANUP_CALLBACKS: (() => void)[] = [];
86
+ const CLEANUP = () => {
87
+ CLEANUP_CALLBACKS.forEach((callback) => callback());
33
88
  RUNNING_BUILDERS.forEach((builder) => builder.stop?.());
34
- process.exit(code);
35
89
  };
36
90
 
37
- process.on("SIGINT", () => EXIT());
38
- process.on("SIGTERM", () => EXIT());
91
+ process.on("SIGINT", () => {
92
+ CLEANUP();
93
+ process.exit();
94
+ });
95
+ process.on("SIGTERM", () => {
96
+ CLEANUP();
97
+ process.exit();
98
+ });
39
99
 
40
100
  function isWindows() {
41
101
  return process.platform === "win32";
@@ -95,7 +155,7 @@ function getPort(pid: number) {
95
155
  const match = matches[0];
96
156
  if (match) return parseInt(match[1]);
97
157
  } catch (thrown) {
98
- console.error(
158
+ logger.error(
99
159
  `Error scanning for ports of process with PID ${pid}: ${thrown}`
100
160
  );
101
161
  }
@@ -108,13 +168,15 @@ async function spawnProxyProcess({
108
168
  port?: number;
109
169
  command: (string | number)[];
110
170
  }): Promise<void | number> {
111
- if (command.length === 0)
112
- return EXIT(
171
+ if (command.length === 0) {
172
+ CLEANUP();
173
+ throw new FatalError(
113
174
  "Must specify a directory of static assets to serve or a command to run.",
114
175
  1
115
176
  );
177
+ }
116
178
 
117
- console.log(`Running ${command.join(" ")}...`);
179
+ logger.log(`Running ${command.join(" ")}...`);
118
180
  const proxy = spawn(
119
181
  command[0].toString(),
120
182
  command.slice(1).map((value) => value.toString()),
@@ -126,30 +188,30 @@ async function spawnProxyProcess({
126
188
  },
127
189
  }
128
190
  );
129
- EXIT_CALLBACKS.push(() => {
191
+ CLEANUP_CALLBACKS.push(() => {
130
192
  proxy.kill();
131
193
  });
132
194
 
133
195
  proxy.stdout.on("data", (data) => {
134
- console.log(`[proxy]: ${data}`);
196
+ logger.log(`[proxy]: ${data}`);
135
197
  });
136
198
 
137
199
  proxy.stderr.on("data", (data) => {
138
- console.error(`[proxy]: ${data}`);
200
+ logger.error(`[proxy]: ${data}`);
139
201
  });
140
202
 
141
203
  proxy.on("close", (code) => {
142
- console.error(`Proxy exited with status ${code}.`);
204
+ logger.error(`Proxy exited with status ${code}.`);
143
205
  });
144
206
 
145
207
  // Wait for proxy process to start...
146
208
  while (!proxy.pid) {}
147
209
 
148
210
  if (port === undefined) {
149
- console.log(
211
+ logger.log(
150
212
  `Sleeping ${SECONDS_TO_WAIT_FOR_PROXY} seconds to allow proxy process to start before attempting to automatically determine port...`
151
213
  );
152
- console.log("To skip, specify the proxy port with --proxy.");
214
+ logger.log("To skip, specify the proxy port with --proxy.");
153
215
  await sleep(SECONDS_TO_WAIT_FOR_PROXY * 1000);
154
216
 
155
217
  port = getPids(proxy.pid)
@@ -157,12 +219,13 @@ async function spawnProxyProcess({
157
219
  .filter((port) => port !== undefined)[0];
158
220
 
159
221
  if (port === undefined) {
160
- return EXIT(
222
+ CLEANUP();
223
+ throw new FatalError(
161
224
  "Could not automatically determine proxy port. Please specify the proxy port with --proxy.",
162
225
  1
163
226
  );
164
227
  } else {
165
- console.log(`Automatically determined the proxy port to be ${port}.`);
228
+ logger.log(`Automatically determined the proxy port to be ${port}.`);
166
229
  }
167
230
  }
168
231
 
@@ -429,12 +492,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
429
492
  }).on("change", (path) => {
430
493
  switch (path) {
431
494
  case headersFile: {
432
- console.log("_headers modified. Re-evaluating...");
495
+ logger.log("_headers modified. Re-evaluating...");
433
496
  headersMatcher = generateHeadersMatcher(headersFile);
434
497
  break;
435
498
  }
436
499
  case redirectsFile: {
437
- console.log("_redirects modified. Re-evaluating...");
500
+ logger.log("_redirects modified. Re-evaluating...");
438
501
  redirectsMatcher = generateRedirectsMatcher(redirectsFile);
439
502
  break;
440
503
  }
@@ -647,7 +710,7 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
647
710
  const RUNNING_BUILDERS: BuildResult[] = [];
648
711
 
649
712
  async function buildFunctions({
650
- scriptPath,
713
+ outfile,
651
714
  outputConfigPath,
652
715
  functionsDirectory,
653
716
  minify = false,
@@ -655,8 +718,10 @@ async function buildFunctions({
655
718
  fallbackService = "ASSETS",
656
719
  watch = false,
657
720
  onEnd,
721
+ plugin = false,
722
+ buildOutputDirectory,
658
723
  }: {
659
- scriptPath: string;
724
+ outfile: string;
660
725
  outputConfigPath?: string;
661
726
  functionsDirectory: string;
662
727
  minify?: boolean;
@@ -664,6 +729,8 @@ async function buildFunctions({
664
729
  fallbackService?: string;
665
730
  watch?: boolean;
666
731
  onEnd?: () => void;
732
+ plugin?: boolean;
733
+ buildOutputDirectory?: string;
667
734
  }) {
668
735
  RUNNING_BUILDERS.forEach(
669
736
  (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
@@ -690,19 +757,483 @@ async function buildFunctions({
690
757
  outfile: routesModule,
691
758
  });
692
759
 
693
- RUNNING_BUILDERS.push(
694
- await buildWorker({
695
- routesModule,
696
- outfile: scriptPath,
697
- minify,
698
- sourcemap,
699
- fallbackService,
700
- watch,
701
- onEnd,
702
- })
703
- );
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;
704
794
  }
705
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
+
706
1237
  export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
707
1238
  return yargs
708
1239
  .command(
@@ -779,10 +1310,10 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
779
1310
  _: [_pages, _dev, ...remaining],
780
1311
  }) => {
781
1312
  // Beta message for `wrangler pages <commands>` usage
782
- console.log(pagesBetaWarning);
1313
+ logger.log(pagesBetaWarning);
783
1314
 
784
1315
  if (!local) {
785
- console.error("Only local mode is supported at the moment.");
1316
+ logger.error("Only local mode is supported at the moment.");
786
1317
  return;
787
1318
  }
788
1319
 
@@ -809,17 +1340,18 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
809
1340
  );
810
1341
 
811
1342
  if (usingFunctions) {
812
- const scriptPath = join(tmpdir(), "./functionsWorker.js");
1343
+ const outfile = join(tmpdir(), "./functionsWorker.js");
813
1344
 
814
- console.log(`Compiling worker to "${scriptPath}"...`);
1345
+ logger.log(`Compiling worker to "${outfile}"...`);
815
1346
 
816
1347
  try {
817
1348
  await buildFunctions({
818
- scriptPath,
1349
+ outfile,
819
1350
  functionsDirectory,
820
1351
  sourcemap: true,
821
1352
  watch: true,
822
1353
  onEnd: () => scriptReadyResolve(),
1354
+ buildOutputDirectory: directory,
823
1355
  });
824
1356
  } catch {}
825
1357
 
@@ -828,16 +1360,17 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
828
1360
  ignoreInitial: true,
829
1361
  }).on("all", async () => {
830
1362
  await buildFunctions({
831
- scriptPath,
1363
+ outfile,
832
1364
  functionsDirectory,
833
1365
  sourcemap: true,
834
1366
  watch: true,
835
1367
  onEnd: () => scriptReadyResolve(),
1368
+ buildOutputDirectory: directory,
836
1369
  });
837
1370
  });
838
1371
 
839
1372
  miniflareArgs = {
840
- scriptPath,
1373
+ scriptPath: outfile,
841
1374
  };
842
1375
  } else {
843
1376
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -853,7 +1386,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
853
1386
  scriptPath,
854
1387
  };
855
1388
  } else {
856
- console.log("No functions. Shimming...");
1389
+ logger.log("No functions. Shimming...");
857
1390
  miniflareArgs = {
858
1391
  // TODO: The fact that these request/response hacks are necessary is ridiculous.
859
1392
  // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
@@ -885,6 +1418,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
885
1418
  ? await generateAssetsFetch(directory)
886
1419
  : invalidAssetsFetch;
887
1420
 
1421
+ const requestContextCheckOptions =
1422
+ await getRequestContextCheckOptions();
1423
+
888
1424
  const miniflare = new Miniflare({
889
1425
  port,
890
1426
  watch: true,
@@ -920,7 +1456,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
920
1456
  url.host = `localhost:${proxyPort}`;
921
1457
  return await fetch(url, request);
922
1458
  } catch (thrown) {
923
- console.error(`Could not proxy request: ${thrown}`);
1459
+ logger.error(`Could not proxy request: ${thrown}`);
924
1460
 
925
1461
  // TODO: Pretty error page
926
1462
  return new Response(
@@ -932,7 +1468,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
932
1468
  try {
933
1469
  return await assetsFetch(request);
934
1470
  } catch (thrown) {
935
- console.error(`Could not serve static asset: ${thrown}`);
1471
+ logger.error(`Could not serve static asset: ${thrown}`);
936
1472
 
937
1473
  // TODO: Pretty error page
938
1474
  return new Response(
@@ -949,13 +1485,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
949
1485
  cachePersist: true,
950
1486
  liveReload,
951
1487
 
1488
+ ...requestContextCheckOptions,
952
1489
  ...miniflareArgs,
953
1490
  });
954
1491
 
955
1492
  try {
956
1493
  // `startServer` might throw if user code contains errors
957
1494
  const server = await miniflare.startServer();
958
- console.log(`Serving at http://localhost:${port}/`);
1495
+ logger.log(`Serving at http://localhost:${port}/`);
959
1496
 
960
1497
  if (process.env.BROWSER !== "none") {
961
1498
  await openInBrowser(`http://localhost:${port}/`);
@@ -970,13 +1507,14 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
970
1507
  });
971
1508
  }
972
1509
 
973
- EXIT_CALLBACKS.push(() => {
1510
+ CLEANUP_CALLBACKS.push(() => {
974
1511
  server.close();
975
1512
  miniflare.dispose().catch((err) => miniflare.log.error(err));
976
1513
  });
977
1514
  } catch (e) {
978
1515
  miniflare.log.error(e as Error);
979
- EXIT("Could not start Miniflare.", 1);
1516
+ CLEANUP();
1517
+ throw new FatalError("Could not start Miniflare.", 1);
980
1518
  }
981
1519
  }
982
1520
  )
@@ -994,7 +1532,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
994
1532
  description: "The directory of Pages Functions",
995
1533
  })
996
1534
  .options({
997
- "script-path": {
1535
+ outfile: {
998
1536
  type: "string",
999
1537
  default: "_worker.js",
1000
1538
  description: "The location of the output Worker script",
@@ -1026,32 +1564,258 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1026
1564
  description:
1027
1565
  "Watch for changes to the functions and automatically rebuild the Worker script",
1028
1566
  },
1567
+ plugin: {
1568
+ type: "boolean",
1569
+ default: false,
1570
+ description: "Build a plugin rather than a Worker script",
1571
+ },
1029
1572
  })
1030
1573
  .epilogue(pagesBetaWarning),
1031
1574
  async ({
1032
1575
  directory,
1033
- "script-path": scriptPath,
1576
+ outfile,
1034
1577
  "output-config-path": outputConfigPath,
1035
1578
  minify,
1036
1579
  sourcemap,
1037
1580
  fallbackService,
1038
1581
  watch,
1582
+ plugin,
1039
1583
  }) => {
1040
1584
  // Beta message for `wrangler pages <commands>` usage
1041
- console.log(pagesBetaWarning);
1585
+ logger.log(pagesBetaWarning);
1042
1586
 
1043
1587
  await buildFunctions({
1044
- scriptPath,
1588
+ outfile,
1045
1589
  outputConfigPath,
1046
1590
  functionsDirectory: directory,
1047
1591
  minify,
1048
1592
  sourcemap,
1049
1593
  fallbackService,
1050
1594
  watch,
1595
+ plugin,
1596
+ buildOutputDirectory: dirname(outfile),
1051
1597
  });
1052
1598
  }
1053
1599
  )
1054
- );
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);
1055
1819
  };
1056
1820
 
1057
1821
  const invalidAssetsFetch: typeof fetch = () => {
@@ -1059,3 +1823,44 @@ const invalidAssetsFetch: typeof fetch = () => {
1059
1823
  "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
1060
1824
  );
1061
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
+ }