wrangler 0.0.0-e95e5a0 → 0.0.0-e978986

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 (52) hide show
  1. package/Cloudflare_CA.pem +18 -0
  2. package/README.md +3 -1
  3. package/bin/wrangler.js +36 -4
  4. package/package.json +11 -7
  5. package/pages/functions/buildPlugin.ts +13 -0
  6. package/pages/functions/buildWorker.ts +14 -1
  7. package/pages/functions/template-plugin.ts +3 -2
  8. package/src/__tests__/configuration.test.ts +336 -87
  9. package/src/__tests__/dev.test.tsx +243 -25
  10. package/src/__tests__/helpers/mock-dialogs.ts +41 -1
  11. package/src/__tests__/index.test.ts +79 -72
  12. package/src/__tests__/init.test.ts +182 -131
  13. package/src/__tests__/jest.setup.ts +8 -0
  14. package/src/__tests__/kv.test.ts +173 -146
  15. package/src/__tests__/package-manager.test.ts +154 -7
  16. package/src/__tests__/pages.test.ts +381 -53
  17. package/src/__tests__/parse.test.ts +8 -0
  18. package/src/__tests__/publish.test.ts +682 -89
  19. package/src/__tests__/r2.test.ts +55 -36
  20. package/src/__tests__/secret.test.ts +39 -4
  21. package/src/abort.d.ts +3 -0
  22. package/src/bundle.ts +33 -2
  23. package/src/cfetch/index.ts +21 -4
  24. package/src/cfetch/internal.ts +17 -9
  25. package/src/config/config.ts +1 -1
  26. package/src/config/environment.ts +40 -14
  27. package/src/config/index.ts +162 -0
  28. package/src/config/validation.ts +180 -65
  29. package/src/create-worker-preview.ts +17 -7
  30. package/src/create-worker-upload-form.ts +22 -8
  31. package/src/dev/dev.tsx +7 -6
  32. package/src/dev/local.tsx +6 -0
  33. package/src/dev/remote.tsx +15 -1
  34. package/src/dialogs.tsx +48 -0
  35. package/src/durable.ts +102 -0
  36. package/src/entry.ts +64 -9
  37. package/src/index.tsx +357 -177
  38. package/src/inspect.ts +39 -0
  39. package/src/kv.ts +114 -25
  40. package/src/open-in-browser.ts +5 -12
  41. package/src/package-manager.ts +50 -3
  42. package/src/pages.tsx +271 -110
  43. package/src/parse.ts +23 -4
  44. package/src/proxy.ts +38 -22
  45. package/src/publish.ts +227 -113
  46. package/src/sites.tsx +19 -17
  47. package/src/user.tsx +11 -1
  48. package/src/worker.ts +8 -0
  49. package/templates/new-worker-scheduled.js +17 -0
  50. package/templates/new-worker-scheduled.ts +32 -0
  51. package/templates/new-worker.ts +16 -1
  52. package/wrangler-dist/cli.js +36017 -22687
package/src/pages.tsx CHANGED
@@ -4,7 +4,7 @@ import { execSync, spawn } from "node:child_process";
4
4
  import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { readdir, readFile, stat } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
- import { dirname, join, sep } from "node:path";
7
+ import { dirname, join, sep, extname, basename } from "node:path";
8
8
  import { cwd } from "node:process";
9
9
  import { URL } from "node:url";
10
10
  import { hash } from "blake3-wasm";
@@ -14,6 +14,7 @@ import SelectInput from "ink-select-input";
14
14
  import Spinner from "ink-spinner";
15
15
  import Table from "ink-table";
16
16
  import { getType } from "mime";
17
+ import PQueue from "p-queue";
17
18
  import prettyBytes from "pretty-bytes";
18
19
  import React from "react";
19
20
  import { format as timeagoFormat } from "timeago.js";
@@ -32,7 +33,11 @@ import openInBrowser from "./open-in-browser";
32
33
  import { toUrlPath } from "./paths";
33
34
  import { requireAuth } from "./user";
34
35
  import type { Config } from "../pages/functions/routes";
35
- import type { Headers, Request, fetch } from "@miniflare/core";
36
+ import type {
37
+ Headers as MiniflareHeaders,
38
+ Request as MiniflareRequest,
39
+ fetch as miniflareFetch,
40
+ } from "@miniflare/core";
36
41
  import type { BuildResult } from "esbuild";
37
42
  import type { MiniflareOptions } from "miniflare";
38
43
  import type { BuilderCallback, CommandModule } from "yargs";
@@ -68,12 +73,23 @@ export type Deployment = {
68
73
  project_name: string;
69
74
  };
70
75
 
76
+ export type UploadPayloadFile = {
77
+ key: string;
78
+ value: string;
79
+ metadata: { contentType: string };
80
+ base64: boolean;
81
+ };
82
+
71
83
  interface PagesConfigCache {
72
84
  account_id?: string;
73
85
  project_name?: string;
74
86
  }
75
87
 
76
88
  const PAGES_CONFIG_CACHE_FILENAME = "pages.json";
89
+ const MAX_BUCKET_SIZE = 50 * 1024 * 1024;
90
+ const MAX_BUCKET_FILE_COUNT = 5000;
91
+ const BULK_UPLOAD_CONCURRENCY = 3;
92
+ const MAX_UPLOAD_ATTEMPTS = 5;
77
93
 
78
94
  // Defer importing miniflare until we really need it. This takes ~0.5s
79
95
  // and also modifies some `stream/web` and `undici` prototypes, so we
@@ -82,6 +98,8 @@ const PAGES_CONFIG_CACHE_FILENAME = "pages.json";
82
98
  export const pagesBetaWarning =
83
99
  "🚧 'wrangler pages <command>' is a beta command. Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose";
84
100
 
101
+ const isInPagesCI = !!process.env.CF_PAGES;
102
+
85
103
  const CLEANUP_CALLBACKS: (() => void)[] = [];
86
104
  const CLEANUP = () => {
87
105
  CLEANUP_CALLBACKS.forEach((callback) => callback());
@@ -282,7 +300,7 @@ function generateRulesMatcher<T>(
282
300
  T
283
301
  ][];
284
302
 
285
- return ({ request }: { request: Request }) => {
303
+ return ({ request }: { request: MiniflareRequest }) => {
286
304
  const { pathname, host } = new URL(request.url);
287
305
 
288
306
  return compiledRules
@@ -355,7 +373,7 @@ function generateHeadersMatcher(headersFile: string) {
355
373
  )
356
374
  );
357
375
 
358
- return (request: Request) => {
376
+ return (request: MiniflareRequest) => {
359
377
  const matches = rulesMatcher({
360
378
  request,
361
379
  });
@@ -404,7 +422,7 @@ function generateRedirectsMatcher(redirectsFile: string) {
404
422
  })
405
423
  );
406
424
 
407
- return (request: Request) => {
425
+ return (request: MiniflareRequest) => {
408
426
  const match = rulesMatcher({
409
427
  request,
410
428
  })[0];
@@ -459,7 +477,9 @@ function hasFileExtension(pathname: string) {
459
477
  return /\/.+\.[a-z0-9]+$/i.test(pathname);
460
478
  }
461
479
 
462
- async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
480
+ async function generateAssetsFetch(
481
+ directory: string
482
+ ): Promise<typeof miniflareFetch> {
463
483
  // Defer importing miniflare until we really need it
464
484
  const { Headers, Request, Response } = await import("@miniflare/core");
465
485
 
@@ -508,12 +528,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
508
528
  return readFileSync(file);
509
529
  };
510
530
 
511
- const generateResponse = (request: Request) => {
531
+ const generateResponse = (request: MiniflareRequest) => {
512
532
  const url = new URL(request.url);
513
533
 
514
534
  const deconstructedResponse: {
515
535
  status: number;
516
- headers: Headers;
536
+ headers: MiniflareHeaders;
517
537
  body?: Buffer;
518
538
  } = {
519
539
  status: 200,
@@ -665,8 +685,12 @@ async function generateAssetsFetch(directory: string): Promise<typeof fetch> {
665
685
  };
666
686
 
667
687
  const attachHeaders = (
668
- request: Request,
669
- deconstructedResponse: { status: number; headers: Headers; body?: Buffer }
688
+ request: MiniflareRequest,
689
+ deconstructedResponse: {
690
+ status: number;
691
+ headers: MiniflareHeaders;
692
+ body?: Buffer;
693
+ }
670
694
  ) => {
671
695
  const headers = deconstructedResponse.headers;
672
696
  const newHeaders = new Headers({});
@@ -720,6 +744,7 @@ async function buildFunctions({
720
744
  onEnd,
721
745
  plugin = false,
722
746
  buildOutputDirectory,
747
+ nodeCompat,
723
748
  }: {
724
749
  outfile: string;
725
750
  outputConfigPath?: string;
@@ -731,12 +756,13 @@ async function buildFunctions({
731
756
  onEnd?: () => void;
732
757
  plugin?: boolean;
733
758
  buildOutputDirectory?: string;
759
+ nodeCompat?: boolean;
734
760
  }) {
735
761
  RUNNING_BUILDERS.forEach(
736
762
  (runningBuilder) => runningBuilder.stop && runningBuilder.stop()
737
763
  );
738
764
 
739
- const routesModule = join(tmpdir(), "./functionsRoutes.mjs");
765
+ const routesModule = join(tmpdir(), `./functionsRoutes-${Math.random()}.mjs`);
740
766
  const baseURL = toUrlPath("/");
741
767
 
742
768
  const config: Config = await generateConfigFromFileTree({
@@ -765,6 +791,7 @@ async function buildFunctions({
765
791
  minify,
766
792
  sourcemap,
767
793
  watch,
794
+ nodeCompat,
768
795
  onEnd,
769
796
  })
770
797
  );
@@ -779,6 +806,7 @@ async function buildFunctions({
779
806
  watch,
780
807
  onEnd,
781
808
  buildOutputDirectory,
809
+ nodeCompat,
782
810
  })
783
811
  );
784
812
  }
@@ -802,34 +830,30 @@ const createDeployment: CommandModule<
802
830
  return yargs
803
831
  .positional("directory", {
804
832
  type: "string",
805
- default: "functions",
806
- description: "The directory of Pages Functions",
833
+ demandOption: true,
834
+ description: "The directory of static files to upload",
807
835
  })
808
836
  .options({
809
837
  "project-name": {
810
838
  type: "string",
811
- description:
812
- "The name of the project you want to list deployments for",
839
+ description: "The name of the project you want to deploy to",
813
840
  },
814
841
  branch: {
815
842
  type: "string",
816
- description:
817
- "The branch of the project you want to list deployments for",
843
+ description: "The name of the branch you want to deploy to",
818
844
  },
819
845
  "commit-hash": {
820
846
  type: "string",
821
- description:
822
- "The branch of the project you want to list deployments for",
847
+ description: "The SHA to attach to this deployment",
823
848
  },
824
849
  "commit-message": {
825
850
  type: "string",
826
- description:
827
- "The branch of the project you want to list deployments for",
851
+ description: "The commit message to attach to this deployment",
828
852
  },
829
853
  "commit-dirty": {
830
854
  type: "boolean",
831
855
  description:
832
- "The branch of the project you want to list deployments for",
856
+ "Whether or not the workspace should be considered dirty for this deployment",
833
857
  },
834
858
  })
835
859
  .epilogue(pagesBetaWarning);
@@ -842,6 +866,10 @@ const createDeployment: CommandModule<
842
866
  commitMessage,
843
867
  commitDirty,
844
868
  }) => {
869
+ if (!directory) {
870
+ throw new FatalError("Must specify a directory.", 1);
871
+ }
872
+
845
873
  const config = getConfigCache<PagesConfigCache>(
846
874
  PAGES_CONFIG_CACHE_FILENAME
847
875
  );
@@ -851,40 +879,45 @@ const createDeployment: CommandModule<
851
879
 
852
880
  const isInteractive = process.stdin.isTTY;
853
881
  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
+ const projects = (await listProjects({ accountId })).filter(
883
+ (project) => !project.source
884
+ );
885
+
886
+ let existingOrNew: "existing" | "new" = "new";
887
+
888
+ if (projects.length > 0) {
889
+ existingOrNew = await new Promise<"new" | "existing">((resolve) => {
890
+ const { unmount } = render(
891
+ <>
892
+ <Text>
893
+ No project selected. Would you like to create one or use an
894
+ existing project?
895
+ </Text>
896
+ <SelectInput
897
+ items={[
898
+ {
899
+ key: "new",
900
+ label: "Create a new project",
901
+ value: "new",
902
+ },
903
+ {
904
+ key: "existing",
905
+ label: "Use an existing project",
906
+ value: "existing",
907
+ },
908
+ ]}
909
+ onSelect={async (selected) => {
910
+ resolve(selected.value as "new" | "existing");
911
+ unmount();
912
+ }}
913
+ />
914
+ </>
915
+ );
916
+ });
917
+ }
882
918
 
883
919
  switch (existingOrNew) {
884
920
  case "existing": {
885
- const projects = (await listProjects({ accountId })).filter(
886
- (project) => !project.source
887
- );
888
921
  projectName = await new Promise((resolve) => {
889
922
  const { unmount } = render(
890
923
  <>
@@ -994,7 +1027,7 @@ const createDeployment: CommandModule<
994
1027
 
995
1028
  if (isGitDirty && !commitDirty) {
996
1029
  logger.warn(
997
- `Warning: Your working directory is a git repo and has uncommitted changes\nTo silense this warning, pass in --commit-dirty=true`
1030
+ `Warning: Your working directory is a git repo and has uncommitted changes\nTo silence this warning, pass in --commit-dirty=true`
998
1031
  );
999
1032
  }
1000
1033
 
@@ -1006,7 +1039,7 @@ const createDeployment: CommandModule<
1006
1039
  let builtFunctions: string | undefined = undefined;
1007
1040
  const functionsDirectory = join(cwd(), "functions");
1008
1041
  if (existsSync(functionsDirectory)) {
1009
- const outfile = join(tmpdir(), "./functionsWorker.js");
1042
+ const outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`);
1010
1043
 
1011
1044
  await new Promise((resolve) =>
1012
1045
  buildFunctions({
@@ -1020,12 +1053,9 @@ const createDeployment: CommandModule<
1020
1053
  builtFunctions = readFileSync(outfile, "utf-8");
1021
1054
  }
1022
1055
 
1023
- type File = {
1024
- content: Buffer;
1025
- metadata: Metadata;
1026
- };
1027
-
1028
- type Metadata = {
1056
+ type FileContainer = {
1057
+ content: string;
1058
+ contentType: string;
1029
1059
  sizeInBytes: number;
1030
1060
  hash: string;
1031
1061
  };
@@ -1036,11 +1066,12 @@ const createDeployment: CommandModule<
1036
1066
  "_headers",
1037
1067
  ".DS_Store",
1038
1068
  "node_modules",
1069
+ ".git",
1039
1070
  ];
1040
1071
 
1041
1072
  const walk = async (
1042
1073
  dir: string,
1043
- fileMap: Map<string, File> = new Map(),
1074
+ fileMap: Map<string, FileContainer> = new Map(),
1044
1075
  depth = 0
1045
1076
  ) => {
1046
1077
  const files = await readdir(dir);
@@ -1072,10 +1103,7 @@ const createDeployment: CommandModule<
1072
1103
  const fileContent = await readFile(filepath);
1073
1104
 
1074
1105
  const base64Content = fileContent.toString("base64");
1075
- const extension =
1076
- name.split(".").length > 1 ? name.split(".").at(-1) || "" : "";
1077
-
1078
- const content = base64Content + extension;
1106
+ const extension = extname(basename(name)).substring(1);
1079
1107
 
1080
1108
  if (filestat.size > 25 * 1024 * 1024) {
1081
1109
  throw new Error(
@@ -1086,11 +1114,12 @@ const createDeployment: CommandModule<
1086
1114
  }
1087
1115
 
1088
1116
  fileMap.set(name, {
1089
- content: fileContent,
1090
- metadata: {
1091
- sizeInBytes: filestat.size,
1092
- hash: hash(content).toString("hex").slice(0, 32),
1093
- },
1117
+ content: base64Content,
1118
+ contentType: getType(name) || "application/octet-stream",
1119
+ sizeInBytes: filestat.size,
1120
+ hash: hash(base64Content + extension)
1121
+ .toString("hex")
1122
+ .slice(0, 32),
1094
1123
  });
1095
1124
  }
1096
1125
  })
@@ -1101,51 +1130,139 @@ const createDeployment: CommandModule<
1101
1130
 
1102
1131
  const fileMap = await walk(directory);
1103
1132
 
1133
+ if (fileMap.size > 20000) {
1134
+ throw new FatalError(
1135
+ `Error: Pages only supports up to 20,000 files in a deployment. Ensure you have specified your build output directory correctly.`,
1136
+ 1
1137
+ );
1138
+ }
1139
+
1140
+ const files = [...fileMap.values()];
1141
+
1142
+ const { jwt } = await fetchResult<{ jwt: string }>(
1143
+ `/accounts/${accountId}/pages/projects/${projectName}/upload-token`
1144
+ );
1145
+
1104
1146
  const start = Date.now();
1105
1147
 
1106
- const files: Array<Promise<void>> = [];
1148
+ const missingHashes = await fetchResult<string[]>(
1149
+ `/pages/assets/check-missing`,
1150
+ {
1151
+ method: "POST",
1152
+ headers: {
1153
+ "Content-Type": "application/json",
1154
+ Authorization: `Bearer ${jwt}`,
1155
+ },
1156
+ body: JSON.stringify({
1157
+ hashes: files.map(({ hash }) => hash),
1158
+ }),
1159
+ }
1160
+ );
1107
1161
 
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
- }
1162
+ const sortedFiles = files
1163
+ .filter((file) => missingHashes.includes(file.hash))
1164
+ .sort((a, b) => b.sizeInBytes - a.sizeInBytes);
1165
+
1166
+ // Start with a few buckets so small projects still get
1167
+ // the benefit of multiple upload streams
1168
+ const buckets: {
1169
+ files: FileContainer[];
1170
+ remainingSize: number;
1171
+ }[] = new Array(BULK_UPLOAD_CONCURRENCY).fill(null).map(() => ({
1172
+ files: [],
1173
+ remainingSize: MAX_BUCKET_SIZE,
1174
+ }));
1175
+
1176
+ let bucketOffset = 0;
1177
+ for (const file of sortedFiles) {
1178
+ let inserted = false;
1179
+
1180
+ for (let i = 0; i < buckets.length; i++) {
1181
+ // Start at a different bucket for each new file
1182
+ const bucket = buckets[(i + bucketOffset) % buckets.length];
1183
+ if (
1184
+ bucket.remainingSize >= file.sizeInBytes &&
1185
+ bucket.files.length < MAX_BUCKET_FILE_COUNT
1186
+ ) {
1187
+ bucket.files.push(file);
1188
+ bucket.remainingSize -= file.sizeInBytes;
1189
+ inserted = true;
1190
+ break;
1191
+ }
1192
+ }
1113
1193
 
1114
- let counter = 0;
1194
+ if (!inserted) {
1195
+ buckets.push({
1196
+ files: [file],
1197
+ remainingSize: MAX_BUCKET_SIZE - file.sizeInBytes,
1198
+ });
1199
+ }
1200
+ bucketOffset++;
1201
+ }
1115
1202
 
1203
+ let counter = fileMap.size - sortedFiles.length;
1116
1204
  const { rerender, unmount } = render(
1117
1205
  <Progress done={counter} total={fileMap.size} />
1118
1206
  );
1119
1207
 
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
- );
1208
+ const queue = new PQueue({ concurrency: BULK_UPLOAD_CONCURRENCY });
1126
1209
 
1127
- // TODO: Consider a retry
1210
+ for (const bucket of buckets) {
1211
+ // Don't upload empty buckets (can happen for tiny projects)
1212
+ if (bucket.files.length === 0) continue;
1128
1213
 
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
- );
1214
+ const payload: UploadPayloadFile[] = bucket.files.map((file) => ({
1215
+ key: file.hash,
1216
+ value: file.content,
1217
+ metadata: {
1218
+ contentType: file.contentType,
1219
+ },
1220
+ base64: true,
1221
+ }));
1222
+
1223
+ let attempts = 0;
1224
+ const doUpload = async (): Promise<void> => {
1225
+ try {
1226
+ return await fetchResult(`/pages/assets/upload`, {
1227
+ method: "POST",
1228
+ headers: {
1229
+ "Content-Type": "application/json",
1230
+ Authorization: `Bearer ${jwt}`,
1231
+ },
1232
+ body: JSON.stringify(payload),
1233
+ });
1234
+ } catch (e) {
1235
+ if (attempts < MAX_UPLOAD_ATTEMPTS) {
1236
+ // Linear backoff, 0 second first time, then 1 second etc.
1237
+ await new Promise((resolve) =>
1238
+ setTimeout(resolve, attempts++ * 1000)
1239
+ );
1240
+ return doUpload();
1241
+ } else {
1242
+ throw e;
1243
+ }
1142
1244
  }
1143
- });
1245
+ };
1144
1246
 
1145
- files.push(promise);
1146
- });
1247
+ queue.add(() =>
1248
+ doUpload().then(
1249
+ () => {
1250
+ counter += bucket.files.length;
1251
+ rerender(<Progress done={counter} total={fileMap.size} />);
1252
+ },
1253
+ (error) => {
1254
+ return Promise.reject(
1255
+ new FatalError(
1256
+ "Failed to upload files. Please try again.",
1257
+ error.code || 1
1258
+ )
1259
+ );
1260
+ }
1261
+ )
1262
+ );
1263
+ }
1147
1264
 
1148
- await Promise.all(files);
1265
+ await queue.onIdle();
1149
1266
 
1150
1267
  unmount();
1151
1268
 
@@ -1163,7 +1280,7 @@ const createDeployment: CommandModule<
1163
1280
  Object.fromEntries(
1164
1281
  [...fileMap.entries()].map(([fileName, file]) => [
1165
1282
  `/${fileName}`,
1166
- file.metadata.hash,
1283
+ file.hash,
1167
1284
  ])
1168
1285
  )
1169
1286
  )
@@ -1237,7 +1354,7 @@ const createDeployment: CommandModule<
1237
1354
  export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1238
1355
  return yargs
1239
1356
  .command(
1240
- "dev [directory] [-- command]",
1357
+ "dev [directory] [-- command..]",
1241
1358
  "🧑‍💻 Develop your full-stack Pages application locally",
1242
1359
  (yargs) => {
1243
1360
  return yargs
@@ -1293,6 +1410,12 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1293
1410
  default: false,
1294
1411
  description: "Auto reload HTML pages when change is detected",
1295
1412
  },
1413
+ "node-compat": {
1414
+ describe: "Enable node.js compatibility",
1415
+ default: false,
1416
+ type: "boolean",
1417
+ hidden: true,
1418
+ },
1296
1419
  // TODO: Miniflare user options
1297
1420
  })
1298
1421
  .epilogue(pagesBetaWarning);
@@ -1307,6 +1430,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1307
1430
  kv: kvs = [],
1308
1431
  do: durableObjects = [],
1309
1432
  "live-reload": liveReload,
1433
+ "node-compat": nodeCompat,
1310
1434
  _: [_pages, _dev, ...remaining],
1311
1435
  }) => {
1312
1436
  // Beta message for `wrangler pages <commands>` usage
@@ -1340,7 +1464,16 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1340
1464
  );
1341
1465
 
1342
1466
  if (usingFunctions) {
1343
- const outfile = join(tmpdir(), "./functionsWorker.js");
1467
+ const outfile = join(
1468
+ tmpdir(),
1469
+ `./functionsWorker-${Math.random()}.js`
1470
+ );
1471
+
1472
+ if (nodeCompat) {
1473
+ console.warn(
1474
+ "Enabling node.js compatibility mode for builtins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
1475
+ );
1476
+ }
1344
1477
 
1345
1478
  logger.log(`Compiling worker to "${outfile}"...`);
1346
1479
 
@@ -1352,6 +1485,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1352
1485
  watch: true,
1353
1486
  onEnd: () => scriptReadyResolve(),
1354
1487
  buildOutputDirectory: directory,
1488
+ nodeCompat,
1355
1489
  });
1356
1490
  } catch {}
1357
1491
 
@@ -1366,6 +1500,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1366
1500
  watch: true,
1367
1501
  onEnd: () => scriptReadyResolve(),
1368
1502
  buildOutputDirectory: directory,
1503
+ nodeCompat,
1369
1504
  });
1370
1505
  });
1371
1506
 
@@ -1388,6 +1523,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1388
1523
  } else {
1389
1524
  logger.log("No functions. Shimming...");
1390
1525
  miniflareArgs = {
1526
+ // cfFetch sets the `cf` object that a function could expect
1527
+ // If there are no functions, there's no reason to set this up (and not make that network call)
1528
+ cfFetch: false,
1391
1529
  // TODO: The fact that these request/response hacks are necessary is ridiculous.
1392
1530
  // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
1393
1531
  script: `
@@ -1449,7 +1587,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1449
1587
 
1450
1588
  // env.ASSETS.fetch
1451
1589
  serviceBindings: {
1452
- async ASSETS(request: Request) {
1590
+ async ASSETS(request: MiniflareRequest) {
1453
1591
  if (proxyPort) {
1454
1592
  try {
1455
1593
  const url = new URL(request.url);
@@ -1569,6 +1707,16 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1569
1707
  default: false,
1570
1708
  description: "Build a plugin rather than a Worker script",
1571
1709
  },
1710
+ "build-output-directory": {
1711
+ type: "string",
1712
+ description: "The directory to output static assets to",
1713
+ },
1714
+ "node-compat": {
1715
+ describe: "Enable node.js compatibility",
1716
+ default: false,
1717
+ type: "boolean",
1718
+ hidden: true,
1719
+ },
1572
1720
  })
1573
1721
  .epilogue(pagesBetaWarning),
1574
1722
  async ({
@@ -1580,9 +1728,21 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1580
1728
  fallbackService,
1581
1729
  watch,
1582
1730
  plugin,
1731
+ "build-output-directory": buildOutputDirectory,
1732
+ "node-compat": nodeCompat,
1583
1733
  }) => {
1584
- // Beta message for `wrangler pages <commands>` usage
1585
- logger.log(pagesBetaWarning);
1734
+ if (!isInPagesCI) {
1735
+ // Beta message for `wrangler pages <commands>` usage
1736
+ logger.log(pagesBetaWarning);
1737
+ }
1738
+
1739
+ if (nodeCompat) {
1740
+ console.warn(
1741
+ "Enabling node.js compatibility mode for builtins and globals. This is experimental and has serious tradeoffs. Please see https://github.com/ionic-team/rollup-plugin-node-polyfills/ for more details."
1742
+ );
1743
+ }
1744
+
1745
+ buildOutputDirectory ??= dirname(outfile);
1586
1746
 
1587
1747
  await buildFunctions({
1588
1748
  outfile,
@@ -1593,7 +1753,8 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1593
1753
  fallbackService,
1594
1754
  watch,
1595
1755
  plugin,
1596
- buildOutputDirectory: dirname(outfile),
1756
+ buildOutputDirectory,
1757
+ nodeCompat,
1597
1758
  });
1598
1759
  }
1599
1760
  )
@@ -1818,7 +1979,7 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1818
1979
  } as CommandModule);
1819
1980
  };
1820
1981
 
1821
- const invalidAssetsFetch: typeof fetch = () => {
1982
+ const invalidAssetsFetch: typeof miniflareFetch = () => {
1822
1983
  throw new Error(
1823
1984
  "Trying to fetch assets directly when there is no `directory` option specified, and not in `local` mode."
1824
1985
  );