wrangler 2.0.9 → 2.0.14
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/kv-asset-handler.js +1 -0
- package/package.json +4 -2
- package/src/__tests__/configuration.test.ts +255 -142
- package/src/__tests__/dev.test.tsx +27 -0
- package/src/__tests__/index.test.ts +2 -1
- package/src/__tests__/jest.setup.ts +30 -0
- package/src/__tests__/publish.test.ts +393 -160
- package/src/__tests__/user.test.ts +1 -0
- package/src/bundle.ts +9 -5
- package/src/config/environment.ts +1 -1
- package/src/config/validation-helpers.ts +10 -1
- package/src/config/validation.ts +22 -13
- package/src/dev/dev.tsx +29 -45
- package/src/dev/local.tsx +10 -7
- package/src/dev/remote.tsx +4 -1
- package/src/dev/use-esbuild.ts +1 -4
- package/src/generate-auth-url.ts +33 -0
- package/src/generate-random-state.ts +16 -0
- package/src/index.tsx +234 -179
- package/src/open-in-browser.ts +1 -3
- package/src/pages.tsx +295 -240
- package/src/parse.ts +2 -1
- package/src/proxy.ts +19 -6
- package/src/publish.ts +6 -1
- package/src/sites.tsx +49 -18
- package/src/user.tsx +12 -24
- package/templates/static-asset-facade.js +2 -6
- package/wrangler-dist/cli.js +73627 -73462
- package/vendor/@cloudflare/kv-asset-handler/CHANGELOG.md +0 -332
- package/vendor/@cloudflare/kv-asset-handler/LICENSE_APACHE +0 -176
- package/vendor/@cloudflare/kv-asset-handler/LICENSE_MIT +0 -25
- package/vendor/@cloudflare/kv-asset-handler/README.md +0 -245
- package/vendor/@cloudflare/kv-asset-handler/dist/index.d.ts +0 -32
- package/vendor/@cloudflare/kv-asset-handler/dist/index.js +0 -354
- package/vendor/@cloudflare/kv-asset-handler/dist/mocks.d.ts +0 -13
- package/vendor/@cloudflare/kv-asset-handler/dist/mocks.js +0 -148
- package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/getAssetFromKV.js +0 -436
- package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/mapRequestToAsset.js +0 -40
- package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.d.ts +0 -1
- package/vendor/@cloudflare/kv-asset-handler/dist/test/serveSinglePageApp.js +0 -42
- package/vendor/@cloudflare/kv-asset-handler/dist/types.d.ts +0 -26
- package/vendor/@cloudflare/kv-asset-handler/dist/types.js +0 -31
- package/vendor/@cloudflare/kv-asset-handler/package.json +0 -52
- package/vendor/@cloudflare/kv-asset-handler/src/index.ts +0 -296
- package/vendor/@cloudflare/kv-asset-handler/src/mocks.ts +0 -136
- package/vendor/@cloudflare/kv-asset-handler/src/test/getAssetFromKV.ts +0 -464
- package/vendor/@cloudflare/kv-asset-handler/src/test/mapRequestToAsset.ts +0 -33
- package/vendor/@cloudflare/kv-asset-handler/src/test/serveSinglePageApp.ts +0 -42
- package/vendor/@cloudflare/kv-asset-handler/src/types.ts +0 -39
- package/vendor/wrangler-mime/CHANGELOG.md +0 -289
- package/vendor/wrangler-mime/LICENSE +0 -21
- package/vendor/wrangler-mime/Mime.js +0 -97
- package/vendor/wrangler-mime/README.md +0 -187
- package/vendor/wrangler-mime/cli.js +0 -46
- package/vendor/wrangler-mime/index.js +0 -4
- package/vendor/wrangler-mime/lite.js +0 -4
- package/vendor/wrangler-mime/package.json +0 -52
- package/vendor/wrangler-mime/types/other.js +0 -1
- package/vendor/wrangler-mime/types/standard.js +0 -1
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,249 +1346,11 @@ const createDeployment: CommandModule<
|
|
|
1053
1346
|
builtFunctions = readFileSync(outfile, "utf-8");
|
|
1054
1347
|
}
|
|
1055
1348
|
|
|
1056
|
-
|
|
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
|
-
".git",
|
|
1070
|
-
];
|
|
1071
|
-
|
|
1072
|
-
const walk = async (
|
|
1073
|
-
dir: string,
|
|
1074
|
-
fileMap: Map<string, FileContainer> = new Map(),
|
|
1075
|
-
depth = 0
|
|
1076
|
-
) => {
|
|
1077
|
-
const files = await readdir(dir);
|
|
1078
|
-
|
|
1079
|
-
await Promise.all(
|
|
1080
|
-
files.map(async (file) => {
|
|
1081
|
-
const filepath = join(dir, file);
|
|
1082
|
-
const filestat = await stat(filepath);
|
|
1083
|
-
|
|
1084
|
-
if (IGNORE_LIST.includes(file)) {
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
if (filestat.isSymbolicLink()) {
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
if (filestat.isDirectory()) {
|
|
1093
|
-
fileMap = await walk(filepath, fileMap, depth + 1);
|
|
1094
|
-
} else {
|
|
1095
|
-
let name;
|
|
1096
|
-
if (depth) {
|
|
1097
|
-
name = filepath.split(sep).slice(1).join("/");
|
|
1098
|
-
} else {
|
|
1099
|
-
name = file;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// TODO: Move this to later so we don't hold as much in memory
|
|
1103
|
-
const fileContent = await readFile(filepath);
|
|
1104
|
-
|
|
1105
|
-
const base64Content = fileContent.toString("base64");
|
|
1106
|
-
const extension = extname(basename(name)).substring(1);
|
|
1107
|
-
|
|
1108
|
-
if (filestat.size > 25 * 1024 * 1024) {
|
|
1109
|
-
throw new Error(
|
|
1110
|
-
`Error: Pages only supports files up to ${prettyBytes(
|
|
1111
|
-
25 * 1024 * 1024
|
|
1112
|
-
)} in size\n${name} is ${prettyBytes(filestat.size)} in size`
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
fileMap.set(name, {
|
|
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),
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
})
|
|
1126
|
-
);
|
|
1127
|
-
|
|
1128
|
-
return fileMap;
|
|
1129
|
-
};
|
|
1130
|
-
|
|
1131
|
-
const fileMap = await walk(directory);
|
|
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
|
-
async function fetchJwt(): Promise<string> {
|
|
1143
|
-
return (
|
|
1144
|
-
await fetchResult<{ jwt: string }>(
|
|
1145
|
-
`/accounts/${accountId}/pages/projects/${projectName}/upload-token`
|
|
1146
|
-
)
|
|
1147
|
-
).jwt;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
let jwt = await fetchJwt();
|
|
1151
|
-
|
|
1152
|
-
const start = Date.now();
|
|
1153
|
-
|
|
1154
|
-
const missingHashes = await fetchResult<string[]>(
|
|
1155
|
-
`/pages/assets/check-missing`,
|
|
1156
|
-
{
|
|
1157
|
-
method: "POST",
|
|
1158
|
-
headers: {
|
|
1159
|
-
"Content-Type": "application/json",
|
|
1160
|
-
Authorization: `Bearer ${jwt}`,
|
|
1161
|
-
},
|
|
1162
|
-
body: JSON.stringify({
|
|
1163
|
-
hashes: files.map(({ hash }) => hash),
|
|
1164
|
-
}),
|
|
1165
|
-
}
|
|
1166
|
-
);
|
|
1167
|
-
|
|
1168
|
-
const sortedFiles = files
|
|
1169
|
-
.filter((file) => missingHashes.includes(file.hash))
|
|
1170
|
-
.sort((a, b) => b.sizeInBytes - a.sizeInBytes);
|
|
1171
|
-
|
|
1172
|
-
// Start with a few buckets so small projects still get
|
|
1173
|
-
// the benefit of multiple upload streams
|
|
1174
|
-
const buckets: {
|
|
1175
|
-
files: FileContainer[];
|
|
1176
|
-
remainingSize: number;
|
|
1177
|
-
}[] = new Array(BULK_UPLOAD_CONCURRENCY).fill(null).map(() => ({
|
|
1178
|
-
files: [],
|
|
1179
|
-
remainingSize: MAX_BUCKET_SIZE,
|
|
1180
|
-
}));
|
|
1181
|
-
|
|
1182
|
-
let bucketOffset = 0;
|
|
1183
|
-
for (const file of sortedFiles) {
|
|
1184
|
-
let inserted = false;
|
|
1185
|
-
|
|
1186
|
-
for (let i = 0; i < buckets.length; i++) {
|
|
1187
|
-
// Start at a different bucket for each new file
|
|
1188
|
-
const bucket = buckets[(i + bucketOffset) % buckets.length];
|
|
1189
|
-
if (
|
|
1190
|
-
bucket.remainingSize >= file.sizeInBytes &&
|
|
1191
|
-
bucket.files.length < MAX_BUCKET_FILE_COUNT
|
|
1192
|
-
) {
|
|
1193
|
-
bucket.files.push(file);
|
|
1194
|
-
bucket.remainingSize -= file.sizeInBytes;
|
|
1195
|
-
inserted = true;
|
|
1196
|
-
break;
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
if (!inserted) {
|
|
1201
|
-
buckets.push({
|
|
1202
|
-
files: [file],
|
|
1203
|
-
remainingSize: MAX_BUCKET_SIZE - file.sizeInBytes,
|
|
1204
|
-
});
|
|
1205
|
-
}
|
|
1206
|
-
bucketOffset++;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
let counter = fileMap.size - sortedFiles.length;
|
|
1210
|
-
const { rerender, unmount } = render(
|
|
1211
|
-
<Progress done={counter} total={fileMap.size} />
|
|
1212
|
-
);
|
|
1213
|
-
|
|
1214
|
-
const queue = new PQueue({ concurrency: BULK_UPLOAD_CONCURRENCY });
|
|
1215
|
-
|
|
1216
|
-
for (const bucket of buckets) {
|
|
1217
|
-
// Don't upload empty buckets (can happen for tiny projects)
|
|
1218
|
-
if (bucket.files.length === 0) continue;
|
|
1219
|
-
|
|
1220
|
-
const payload: UploadPayloadFile[] = bucket.files.map((file) => ({
|
|
1221
|
-
key: file.hash,
|
|
1222
|
-
value: file.content,
|
|
1223
|
-
metadata: {
|
|
1224
|
-
contentType: file.contentType,
|
|
1225
|
-
},
|
|
1226
|
-
base64: true,
|
|
1227
|
-
}));
|
|
1228
|
-
|
|
1229
|
-
let attempts = 0;
|
|
1230
|
-
const doUpload = async (): Promise<void> => {
|
|
1231
|
-
try {
|
|
1232
|
-
return await fetchResult(`/pages/assets/upload`, {
|
|
1233
|
-
method: "POST",
|
|
1234
|
-
headers: {
|
|
1235
|
-
"Content-Type": "application/json",
|
|
1236
|
-
Authorization: `Bearer ${jwt}`,
|
|
1237
|
-
},
|
|
1238
|
-
body: JSON.stringify(payload),
|
|
1239
|
-
});
|
|
1240
|
-
} catch (e) {
|
|
1241
|
-
if (attempts < MAX_UPLOAD_ATTEMPTS) {
|
|
1242
|
-
// Linear backoff, 0 second first time, then 1 second etc.
|
|
1243
|
-
await new Promise((resolve) =>
|
|
1244
|
-
setTimeout(resolve, attempts++ * 1000)
|
|
1245
|
-
);
|
|
1246
|
-
|
|
1247
|
-
if ((e as { code: number }).code === 8000013) {
|
|
1248
|
-
// Looks like the JWT expired, fetch another one
|
|
1249
|
-
jwt = await fetchJwt();
|
|
1250
|
-
}
|
|
1251
|
-
return doUpload();
|
|
1252
|
-
} else {
|
|
1253
|
-
throw e;
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
};
|
|
1257
|
-
|
|
1258
|
-
queue.add(() =>
|
|
1259
|
-
doUpload().then(
|
|
1260
|
-
() => {
|
|
1261
|
-
counter += bucket.files.length;
|
|
1262
|
-
rerender(<Progress done={counter} total={fileMap.size} />);
|
|
1263
|
-
},
|
|
1264
|
-
(error) => {
|
|
1265
|
-
return Promise.reject(
|
|
1266
|
-
new FatalError(
|
|
1267
|
-
"Failed to upload files. Please try again.",
|
|
1268
|
-
error.code || 1
|
|
1269
|
-
)
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
)
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
await queue.onIdle();
|
|
1277
|
-
|
|
1278
|
-
unmount();
|
|
1279
|
-
|
|
1280
|
-
const uploadMs = Date.now() - start;
|
|
1281
|
-
|
|
1282
|
-
logger.log(
|
|
1283
|
-
`✨ Success! Uploaded ${fileMap.size} files ${formatTime(uploadMs)}\n`
|
|
1284
|
-
);
|
|
1349
|
+
const manifest = await upload({ directory, accountId, projectName });
|
|
1285
1350
|
|
|
1286
1351
|
const formData = new FormData();
|
|
1287
1352
|
|
|
1288
|
-
formData.append(
|
|
1289
|
-
"manifest",
|
|
1290
|
-
JSON.stringify(
|
|
1291
|
-
Object.fromEntries(
|
|
1292
|
-
[...fileMap.entries()].map(([fileName, file]) => [
|
|
1293
|
-
`/${fileName}`,
|
|
1294
|
-
file.hash,
|
|
1295
|
-
])
|
|
1296
|
-
)
|
|
1297
|
-
)
|
|
1298
|
-
);
|
|
1353
|
+
formData.append("manifest", JSON.stringify(manifest));
|
|
1299
1354
|
|
|
1300
1355
|
if (branch) {
|
|
1301
1356
|
formData.append("branch", branch);
|
package/src/parse.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import TOML from "@iarna/toml";
|
|
4
4
|
import { formatMessagesSync } from "esbuild";
|
|
5
|
+
import { logger } from "./logger";
|
|
5
6
|
|
|
6
7
|
export type Message = {
|
|
7
8
|
text: string;
|
|
@@ -38,7 +39,7 @@ export function formatMessage(
|
|
|
38
39
|
const lines = formatMessagesSync([input], {
|
|
39
40
|
color,
|
|
40
41
|
kind: kind,
|
|
41
|
-
terminalWidth:
|
|
42
|
+
terminalWidth: logger.columns,
|
|
42
43
|
});
|
|
43
44
|
return lines.join("\n");
|
|
44
45
|
}
|
package/src/proxy.ts
CHANGED
|
@@ -100,7 +100,10 @@ export function usePreviewServer({
|
|
|
100
100
|
.then((server) => {
|
|
101
101
|
setProxy({
|
|
102
102
|
server,
|
|
103
|
-
terminator: createHttpTerminator({
|
|
103
|
+
terminator: createHttpTerminator({
|
|
104
|
+
server,
|
|
105
|
+
gracefulTerminationTimeout: 0,
|
|
106
|
+
}),
|
|
104
107
|
});
|
|
105
108
|
})
|
|
106
109
|
.catch(async (err) => {
|
|
@@ -330,7 +333,9 @@ export function usePreviewServer({
|
|
|
330
333
|
abortController.abort();
|
|
331
334
|
// Running `proxy.server.close()` does not close open connections, preventing the process from exiting.
|
|
332
335
|
// So we use this `terminator` to close all the connections and force the server to shutdown.
|
|
333
|
-
proxy.terminator
|
|
336
|
+
proxy.terminator
|
|
337
|
+
.terminate()
|
|
338
|
+
.catch(() => logger.error("Failed to terminate the proxy server."));
|
|
334
339
|
};
|
|
335
340
|
}, [port, ip, proxy, localProtocol]);
|
|
336
341
|
}
|
|
@@ -450,16 +455,24 @@ export async function waitForPortToBeAvailable(
|
|
|
450
455
|
// trying to make a server listen on that port, and retrying
|
|
451
456
|
// until it succeeds.
|
|
452
457
|
const server = createHttpServer();
|
|
458
|
+
const terminator = createHttpTerminator({
|
|
459
|
+
server,
|
|
460
|
+
gracefulTerminationTimeout: 0, // default 1000
|
|
461
|
+
});
|
|
462
|
+
|
|
453
463
|
server.on("error", (err) => {
|
|
454
464
|
// @ts-expect-error non standard property on Error
|
|
455
465
|
if (err.code !== "EADDRINUSE") {
|
|
456
466
|
doReject(err);
|
|
457
467
|
}
|
|
458
468
|
});
|
|
459
|
-
server.listen(port, () =>
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
469
|
+
server.listen(port, () =>
|
|
470
|
+
terminator
|
|
471
|
+
.terminate()
|
|
472
|
+
.then(doResolve, () =>
|
|
473
|
+
logger.error("Failed to terminate the port checker.")
|
|
474
|
+
)
|
|
475
|
+
);
|
|
463
476
|
}
|
|
464
477
|
});
|
|
465
478
|
}
|
package/src/publish.ts
CHANGED
|
@@ -430,7 +430,12 @@ export default async function publish(props: Props): Promise<void> {
|
|
|
430
430
|
method: "PUT",
|
|
431
431
|
body: createWorkerUploadForm(worker),
|
|
432
432
|
},
|
|
433
|
-
new URLSearchParams({
|
|
433
|
+
new URLSearchParams({
|
|
434
|
+
include_subdomain_availability: "true",
|
|
435
|
+
// pass excludeScript so the whole body of the
|
|
436
|
+
// script doesn't get included in the response
|
|
437
|
+
excludeScript: "true",
|
|
438
|
+
})
|
|
434
439
|
)
|
|
435
440
|
).available_on_subdomain;
|
|
436
441
|
}
|