wrangler 2.0.7 → 2.0.11

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.
package/src/pages.tsx CHANGED
@@ -821,6 +821,299 @@ interface CreateDeploymentArgs {
821
821
  commitDirty?: boolean;
822
822
  }
823
823
 
824
+ const upload = async ({
825
+ directory,
826
+ accountId,
827
+ projectName,
828
+ }: {
829
+ directory: string;
830
+ accountId: string;
831
+ projectName: string;
832
+ }) => {
833
+ type FileContainer = {
834
+ content: string;
835
+ contentType: string;
836
+ sizeInBytes: number;
837
+ hash: string;
838
+ };
839
+
840
+ const IGNORE_LIST = [
841
+ "_worker.js",
842
+ "_redirects",
843
+ "_headers",
844
+ ".DS_Store",
845
+ "node_modules",
846
+ ".git",
847
+ ];
848
+
849
+ const walk = async (
850
+ dir: string,
851
+ fileMap: Map<string, FileContainer> = new Map(),
852
+ depth = 0
853
+ ) => {
854
+ const files = await readdir(dir);
855
+
856
+ await Promise.all(
857
+ files.map(async (file) => {
858
+ const filepath = join(dir, file);
859
+ const filestat = await stat(filepath);
860
+
861
+ if (IGNORE_LIST.includes(file)) {
862
+ return;
863
+ }
864
+
865
+ if (filestat.isSymbolicLink()) {
866
+ return;
867
+ }
868
+
869
+ if (filestat.isDirectory()) {
870
+ fileMap = await walk(filepath, fileMap, depth + 1);
871
+ } else {
872
+ let name;
873
+ if (depth) {
874
+ name = filepath.split(sep).slice(1).join("/");
875
+ } else {
876
+ name = file;
877
+ }
878
+
879
+ // TODO: Move this to later so we don't hold as much in memory
880
+ const fileContent = await readFile(filepath);
881
+
882
+ const base64Content = fileContent.toString("base64");
883
+ const extension = extname(basename(name)).substring(1);
884
+
885
+ if (filestat.size > 25 * 1024 * 1024) {
886
+ throw new Error(
887
+ `Error: Pages only supports files up to ${prettyBytes(
888
+ 25 * 1024 * 1024
889
+ )} in size\n${name} is ${prettyBytes(filestat.size)} in size`
890
+ );
891
+ }
892
+
893
+ fileMap.set(name, {
894
+ content: base64Content,
895
+ contentType: getType(name) || "application/octet-stream",
896
+ sizeInBytes: filestat.size,
897
+ hash: hash(base64Content + extension)
898
+ .toString("hex")
899
+ .slice(0, 32),
900
+ });
901
+ }
902
+ })
903
+ );
904
+
905
+ return fileMap;
906
+ };
907
+
908
+ const fileMap = await walk(directory);
909
+
910
+ if (fileMap.size > 20000) {
911
+ throw new FatalError(
912
+ `Error: Pages only supports up to 20,000 files in a deployment. Ensure you have specified your build output directory correctly.`,
913
+ 1
914
+ );
915
+ }
916
+
917
+ const files = [...fileMap.values()];
918
+
919
+ async function fetchJwt(): Promise<string> {
920
+ return (
921
+ await fetchResult<{ jwt: string }>(
922
+ `/accounts/${accountId}/pages/projects/${projectName}/upload-token`
923
+ )
924
+ ).jwt;
925
+ }
926
+
927
+ let jwt = await fetchJwt();
928
+
929
+ const start = Date.now();
930
+
931
+ const missingHashes = await fetchResult<string[]>(
932
+ `/pages/assets/check-missing`,
933
+ {
934
+ method: "POST",
935
+ headers: {
936
+ "Content-Type": "application/json",
937
+ Authorization: `Bearer ${jwt}`,
938
+ },
939
+ body: JSON.stringify({
940
+ hashes: files.map(({ hash }) => hash),
941
+ }),
942
+ }
943
+ );
944
+
945
+ const sortedFiles = files
946
+ .filter((file) => missingHashes.includes(file.hash))
947
+ .sort((a, b) => b.sizeInBytes - a.sizeInBytes);
948
+
949
+ // Start with a few buckets so small projects still get
950
+ // the benefit of multiple upload streams
951
+ const buckets: {
952
+ files: FileContainer[];
953
+ remainingSize: number;
954
+ }[] = new Array(BULK_UPLOAD_CONCURRENCY).fill(null).map(() => ({
955
+ files: [],
956
+ remainingSize: MAX_BUCKET_SIZE,
957
+ }));
958
+
959
+ let bucketOffset = 0;
960
+ for (const file of sortedFiles) {
961
+ let inserted = false;
962
+
963
+ for (let i = 0; i < buckets.length; i++) {
964
+ // Start at a different bucket for each new file
965
+ const bucket = buckets[(i + bucketOffset) % buckets.length];
966
+ if (
967
+ bucket.remainingSize >= file.sizeInBytes &&
968
+ bucket.files.length < MAX_BUCKET_FILE_COUNT
969
+ ) {
970
+ bucket.files.push(file);
971
+ bucket.remainingSize -= file.sizeInBytes;
972
+ inserted = true;
973
+ break;
974
+ }
975
+ }
976
+
977
+ if (!inserted) {
978
+ buckets.push({
979
+ files: [file],
980
+ remainingSize: MAX_BUCKET_SIZE - file.sizeInBytes,
981
+ });
982
+ }
983
+ bucketOffset++;
984
+ }
985
+
986
+ let counter = fileMap.size - sortedFiles.length;
987
+ const { rerender, unmount } = render(
988
+ <Progress done={counter} total={fileMap.size} />
989
+ );
990
+
991
+ const queue = new PQueue({ concurrency: BULK_UPLOAD_CONCURRENCY });
992
+
993
+ for (const bucket of buckets) {
994
+ // Don't upload empty buckets (can happen for tiny projects)
995
+ if (bucket.files.length === 0) continue;
996
+
997
+ const payload: UploadPayloadFile[] = bucket.files.map((file) => ({
998
+ key: file.hash,
999
+ value: file.content,
1000
+ metadata: {
1001
+ contentType: file.contentType,
1002
+ },
1003
+ base64: true,
1004
+ }));
1005
+
1006
+ let attempts = 0;
1007
+ const doUpload = async (): Promise<void> => {
1008
+ try {
1009
+ return await fetchResult(`/pages/assets/upload`, {
1010
+ method: "POST",
1011
+ headers: {
1012
+ "Content-Type": "application/json",
1013
+ Authorization: `Bearer ${jwt}`,
1014
+ },
1015
+ body: JSON.stringify(payload),
1016
+ });
1017
+ } catch (e) {
1018
+ if (attempts < MAX_UPLOAD_ATTEMPTS) {
1019
+ // Linear backoff, 0 second first time, then 1 second etc.
1020
+ await new Promise((resolve) =>
1021
+ setTimeout(resolve, attempts++ * 1000)
1022
+ );
1023
+
1024
+ if ((e as { code: number }).code === 8000013) {
1025
+ // Looks like the JWT expired, fetch another one
1026
+ jwt = await fetchJwt();
1027
+ }
1028
+ return doUpload();
1029
+ } else {
1030
+ throw e;
1031
+ }
1032
+ }
1033
+ };
1034
+
1035
+ queue.add(() =>
1036
+ doUpload().then(
1037
+ () => {
1038
+ counter += bucket.files.length;
1039
+ rerender(<Progress done={counter} total={fileMap.size} />);
1040
+ },
1041
+ (error) => {
1042
+ return Promise.reject(
1043
+ new FatalError(
1044
+ "Failed to upload files. Please try again.",
1045
+ error.code || 1
1046
+ )
1047
+ );
1048
+ }
1049
+ )
1050
+ );
1051
+ }
1052
+
1053
+ await queue.onIdle();
1054
+
1055
+ unmount();
1056
+
1057
+ const uploadMs = Date.now() - start;
1058
+
1059
+ const skipped = fileMap.size - missingHashes.length;
1060
+ const skippedMessage = skipped > 0 ? `(${skipped} already uploaded) ` : "";
1061
+
1062
+ logger.log(
1063
+ `✨ Success! Uploaded ${
1064
+ sortedFiles.length
1065
+ } files ${skippedMessage}${formatTime(uploadMs)}\n`
1066
+ );
1067
+
1068
+ const doUpsertHashes = async (): Promise<void> => {
1069
+ try {
1070
+ return await fetchResult(`/pages/assets/upsert-hashes`, {
1071
+ method: "POST",
1072
+ headers: {
1073
+ "Content-Type": "application/json",
1074
+ Authorization: `Bearer ${jwt}`,
1075
+ },
1076
+ body: JSON.stringify({
1077
+ hashes: files.map(({ hash }) => hash),
1078
+ }),
1079
+ });
1080
+ } catch (e) {
1081
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1082
+
1083
+ if ((e as { code: number }).code === 8000013) {
1084
+ // Looks like the JWT expired, fetch another one
1085
+ jwt = await fetchJwt();
1086
+ }
1087
+
1088
+ return await fetchResult(`/pages/assets/upsert-hashes`, {
1089
+ method: "POST",
1090
+ headers: {
1091
+ "Content-Type": "application/json",
1092
+ Authorization: `Bearer ${jwt}`,
1093
+ },
1094
+ body: JSON.stringify({
1095
+ hashes: files.map(({ hash }) => hash),
1096
+ }),
1097
+ });
1098
+ }
1099
+ };
1100
+
1101
+ try {
1102
+ await doUpsertHashes();
1103
+ } catch {
1104
+ logger.warn(
1105
+ "Failed to update file hashes. Every upload appeared to succeed for this deployment, but you might need to re-upload for future deployments. This shouldn't have any impact other than slowing the upload speed of your next deployment."
1106
+ );
1107
+ }
1108
+
1109
+ return Object.fromEntries(
1110
+ [...fileMap.entries()].map(([fileName, file]) => [
1111
+ `/${fileName}`,
1112
+ file.hash,
1113
+ ])
1114
+ );
1115
+ };
1116
+
824
1117
  const createDeployment: CommandModule<
825
1118
  CreateDeploymentArgs,
826
1119
  CreateDeploymentArgs
@@ -1053,237 +1346,11 @@ const createDeployment: CommandModule<
1053
1346
  builtFunctions = readFileSync(outfile, "utf-8");
1054
1347
  }
1055
1348
 
1056
- type FileContainer = {
1057
- content: string;
1058
- contentType: string;
1059
- sizeInBytes: number;
1060
- hash: string;
1061
- };
1062
-
1063
- const IGNORE_LIST = [
1064
- "_worker.js",
1065
- "_redirects",
1066
- "_headers",
1067
- ".DS_Store",
1068
- "node_modules",
1069
- ];
1070
-
1071
- const walk = async (
1072
- dir: string,
1073
- fileMap: Map<string, FileContainer> = new Map(),
1074
- depth = 0
1075
- ) => {
1076
- const files = await readdir(dir);
1077
-
1078
- await Promise.all(
1079
- files.map(async (file) => {
1080
- const filepath = join(dir, file);
1081
- const filestat = await stat(filepath);
1082
-
1083
- if (IGNORE_LIST.includes(file)) {
1084
- return;
1085
- }
1086
-
1087
- if (filestat.isSymbolicLink()) {
1088
- return;
1089
- }
1090
-
1091
- if (filestat.isDirectory()) {
1092
- fileMap = await walk(filepath, fileMap, depth + 1);
1093
- } else {
1094
- let name;
1095
- if (depth) {
1096
- name = filepath.split(sep).slice(1).join("/");
1097
- } else {
1098
- name = file;
1099
- }
1100
-
1101
- // TODO: Move this to later so we don't hold as much in memory
1102
- const fileContent = await readFile(filepath);
1103
-
1104
- const base64Content = fileContent.toString("base64");
1105
- const extension = extname(basename(name)).substring(1);
1106
-
1107
- if (filestat.size > 25 * 1024 * 1024) {
1108
- throw new Error(
1109
- `Error: Pages only supports files up to ${prettyBytes(
1110
- 25 * 1024 * 1024
1111
- )} in size\n${name} is ${prettyBytes(filestat.size)} in size`
1112
- );
1113
- }
1114
-
1115
- fileMap.set(name, {
1116
- content: base64Content,
1117
- contentType: getType(name) || "application/octet-stream",
1118
- sizeInBytes: filestat.size,
1119
- hash: hash(base64Content + extension)
1120
- .toString("hex")
1121
- .slice(0, 32),
1122
- });
1123
- }
1124
- })
1125
- );
1126
-
1127
- return fileMap;
1128
- };
1129
-
1130
- const fileMap = await walk(directory);
1131
-
1132
- if (fileMap.size > 20000) {
1133
- throw new FatalError(
1134
- `Error: Pages only supports up to 20,000 files in a deployment. Ensure you have specified your build output directory correctly.`,
1135
- 1
1136
- );
1137
- }
1138
-
1139
- const files = [...fileMap.values()];
1140
-
1141
- const { jwt } = await fetchResult<{ jwt: string }>(
1142
- `/accounts/${accountId}/pages/projects/${projectName}/upload-token`
1143
- );
1144
-
1145
- const start = Date.now();
1146
-
1147
- const missingHashes = await fetchResult<string[]>(
1148
- `/pages/assets/check-missing`,
1149
- {
1150
- method: "POST",
1151
- headers: {
1152
- "Content-Type": "application/json",
1153
- Authorization: `Bearer ${jwt}`,
1154
- },
1155
- body: JSON.stringify({
1156
- hashes: files.map(({ hash }) => hash),
1157
- }),
1158
- }
1159
- );
1160
-
1161
- const sortedFiles = files
1162
- .filter((file) => missingHashes.includes(file.hash))
1163
- .sort((a, b) => b.sizeInBytes - a.sizeInBytes);
1164
-
1165
- // Start with a few buckets so small projects still get
1166
- // the benefit of multiple upload streams
1167
- const buckets: {
1168
- files: FileContainer[];
1169
- remainingSize: number;
1170
- }[] = new Array(BULK_UPLOAD_CONCURRENCY).fill(null).map(() => ({
1171
- files: [],
1172
- remainingSize: MAX_BUCKET_SIZE,
1173
- }));
1174
-
1175
- let bucketOffset = 0;
1176
- for (const file of sortedFiles) {
1177
- let inserted = false;
1178
-
1179
- for (let i = 0; i < buckets.length; i++) {
1180
- // Start at a different bucket for each new file
1181
- const bucket = buckets[(i + bucketOffset) % buckets.length];
1182
- if (
1183
- bucket.remainingSize >= file.sizeInBytes &&
1184
- bucket.files.length < MAX_BUCKET_FILE_COUNT
1185
- ) {
1186
- bucket.files.push(file);
1187
- bucket.remainingSize -= file.sizeInBytes;
1188
- inserted = true;
1189
- break;
1190
- }
1191
- }
1192
-
1193
- if (!inserted) {
1194
- buckets.push({
1195
- files: [file],
1196
- remainingSize: MAX_BUCKET_SIZE - file.sizeInBytes,
1197
- });
1198
- }
1199
- bucketOffset++;
1200
- }
1201
-
1202
- let counter = fileMap.size - sortedFiles.length;
1203
- const { rerender, unmount } = render(
1204
- <Progress done={counter} total={fileMap.size} />
1205
- );
1206
-
1207
- const queue = new PQueue({ concurrency: BULK_UPLOAD_CONCURRENCY });
1208
-
1209
- for (const bucket of buckets) {
1210
- // Don't upload empty buckets (can happen for tiny projects)
1211
- if (bucket.files.length === 0) continue;
1212
-
1213
- const payload: UploadPayloadFile[] = bucket.files.map((file) => ({
1214
- key: file.hash,
1215
- value: file.content,
1216
- metadata: {
1217
- contentType: file.contentType,
1218
- },
1219
- base64: true,
1220
- }));
1221
-
1222
- let attempts = 0;
1223
- const doUpload = async (): Promise<void> => {
1224
- try {
1225
- return await fetchResult(`/pages/assets/upload`, {
1226
- method: "POST",
1227
- headers: {
1228
- "Content-Type": "application/json",
1229
- Authorization: `Bearer ${jwt}`,
1230
- },
1231
- body: JSON.stringify(payload),
1232
- });
1233
- } catch (e) {
1234
- if (attempts < MAX_UPLOAD_ATTEMPTS) {
1235
- // Linear backoff, 0 second first time, then 1 second etc.
1236
- await new Promise((resolve) =>
1237
- setTimeout(resolve, attempts++ * 1000)
1238
- );
1239
- return doUpload();
1240
- } else {
1241
- throw e;
1242
- }
1243
- }
1244
- };
1245
-
1246
- queue.add(() =>
1247
- doUpload().then(
1248
- () => {
1249
- counter += bucket.files.length;
1250
- rerender(<Progress done={counter} total={fileMap.size} />);
1251
- },
1252
- (error) => {
1253
- return Promise.reject(
1254
- new FatalError(
1255
- "Failed to upload files. Please try again.",
1256
- error.code || 1
1257
- )
1258
- );
1259
- }
1260
- )
1261
- );
1262
- }
1263
-
1264
- await queue.onIdle();
1265
-
1266
- unmount();
1267
-
1268
- const uploadMs = Date.now() - start;
1269
-
1270
- logger.log(
1271
- `✨ Success! Uploaded ${fileMap.size} files ${formatTime(uploadMs)}\n`
1272
- );
1349
+ const manifest = await upload({ directory, accountId, projectName });
1273
1350
 
1274
1351
  const formData = new FormData();
1275
1352
 
1276
- formData.append(
1277
- "manifest",
1278
- JSON.stringify(
1279
- Object.fromEntries(
1280
- [...fileMap.entries()].map(([fileName, file]) => [
1281
- `/${fileName}`,
1282
- file.hash,
1283
- ])
1284
- )
1285
- )
1286
- );
1353
+ formData.append("manifest", JSON.stringify(manifest));
1287
1354
 
1288
1355
  if (branch) {
1289
1356
  formData.append("branch", branch);
@@ -1522,6 +1589,9 @@ export const pages: BuilderCallback<unknown, unknown> = (yargs) => {
1522
1589
  } else {
1523
1590
  logger.log("No functions. Shimming...");
1524
1591
  miniflareArgs = {
1592
+ // cfFetch sets the `cf` object that a function could expect
1593
+ // If there are no functions, there's no reason to set this up (and not make that network call)
1594
+ cfFetch: false,
1525
1595
  // TODO: The fact that these request/response hacks are necessary is ridiculous.
1526
1596
  // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well)
1527
1597
  script: `