@aletheia-ios/tools 0.2.1 → 0.2.3
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/dist/cli.js +56 -8
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -748,23 +748,37 @@ async function create(repo, slug, name) {
|
|
|
748
748
|
}
|
|
749
749
|
//#endregion
|
|
750
750
|
//#region src/lib/zip.ts
|
|
751
|
+
/** The calendar fields stamped on every entry, as a zip records them. */
|
|
752
|
+
const EPOCH = {
|
|
753
|
+
year: 2e3,
|
|
754
|
+
month: 0,
|
|
755
|
+
day: 1
|
|
756
|
+
};
|
|
751
757
|
/**
|
|
752
758
|
* The modification time stamped on every entry.
|
|
753
759
|
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
760
|
+
* Built from the local-time constructor rather than a UTC instant, because a zip stores a DOS
|
|
761
|
+
* timestamp of the *local* calendar fields. A UTC instant renders as 00:00 on a runner in UTC
|
|
762
|
+
* and 11:00 on a machine in Sydney, which produced byte-different archives of identical files
|
|
763
|
+
* and broke the one guarantee this function exists to make. These arguments read back as
|
|
764
|
+
* 2000-01-01 00:00 from every timezone.
|
|
765
|
+
*
|
|
766
|
+
* Constructed per call so that construction and encoding always see the same timezone.
|
|
756
767
|
*/
|
|
757
|
-
|
|
768
|
+
function epoch() {
|
|
769
|
+
return new Date(EPOCH.year, EPOCH.month, EPOCH.day);
|
|
770
|
+
}
|
|
758
771
|
/**
|
|
759
772
|
* Zips files into bytes that depend only on their names and contents.
|
|
760
773
|
*
|
|
761
|
-
* Entries are written in sorted name order with
|
|
762
|
-
*
|
|
774
|
+
* Entries are written in sorted name order with a fixed mtime and maximum deflate, so the same
|
|
775
|
+
* input produces the same archive and the same sha256 on any machine, in any timezone.
|
|
763
776
|
*/
|
|
764
777
|
function deterministicZip(files) {
|
|
778
|
+
const mtime = epoch();
|
|
765
779
|
const entries = {};
|
|
766
780
|
for (const name of Object.keys(files).sort()) entries[name] = [files[name], {
|
|
767
|
-
mtime
|
|
781
|
+
mtime,
|
|
768
782
|
level: 9
|
|
769
783
|
}];
|
|
770
784
|
return zipSync(entries);
|
|
@@ -1061,7 +1075,10 @@ const TRAVERSAL = /^(\.\.[/\\])+/;
|
|
|
1061
1075
|
/** How long after the last change to wait before rebuilding, so an editor's save burst is one build. */
|
|
1062
1076
|
const DEBOUNCE_MS = 300;
|
|
1063
1077
|
const OK = 200;
|
|
1078
|
+
const NOT_MODIFIED = 304;
|
|
1064
1079
|
const NOT_FOUND = 404;
|
|
1080
|
+
/** Base 16, so the validator stays short. */
|
|
1081
|
+
const HEX = 16;
|
|
1065
1082
|
/**
|
|
1066
1083
|
* Packs and indexes the whole repository, reporting failures instead of throwing.
|
|
1067
1084
|
*
|
|
@@ -1103,7 +1120,27 @@ async function resolveFile(dist, url) {
|
|
|
1103
1120
|
if (!(existsSync(file) && (await stat(file)).isFile())) return null;
|
|
1104
1121
|
return file;
|
|
1105
1122
|
}
|
|
1106
|
-
/**
|
|
1123
|
+
/**
|
|
1124
|
+
* A validator for the file as it is right now, from its size and modification time.
|
|
1125
|
+
*
|
|
1126
|
+
* Every rebuild rewrites the file, so the value changes and a conditional request gets the new
|
|
1127
|
+
* bytes. Between rebuilds it is stable, which is the only case that answers 304.
|
|
1128
|
+
*/
|
|
1129
|
+
function etagOf(size, mtimeMs) {
|
|
1130
|
+
return `"${size.toString(HEX)}-${Math.trunc(mtimeMs).toString(HEX)}"`;
|
|
1131
|
+
}
|
|
1132
|
+
/** Whether the request already holds this version. A client may offer several. */
|
|
1133
|
+
function offers(header, etag) {
|
|
1134
|
+
if (header === void 0) return false;
|
|
1135
|
+
return header.split(",").some((candidate) => candidate.trim() === etag);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* A static server over `dist/`.
|
|
1139
|
+
*
|
|
1140
|
+
* Sends `cache-control: no-store` so nothing holds on to a dev build, alongside an `ETag` so a
|
|
1141
|
+
* client that asks can still be told nothing changed. The two are not in tension: `no-store`
|
|
1142
|
+
* governs whether a copy may be kept, the validator governs whether a fetch has to transfer.
|
|
1143
|
+
*/
|
|
1107
1144
|
function fileServer(dist) {
|
|
1108
1145
|
return createServer(async (request, response) => {
|
|
1109
1146
|
const file = await resolveFile(dist, request.url ?? "/");
|
|
@@ -1111,9 +1148,20 @@ function fileServer(dist) {
|
|
|
1111
1148
|
response.writeHead(NOT_FOUND).end();
|
|
1112
1149
|
return;
|
|
1113
1150
|
}
|
|
1151
|
+
const { size, mtimeMs } = await stat(file);
|
|
1152
|
+
const etag = etagOf(size, mtimeMs);
|
|
1153
|
+
if (offers(request.headers["if-none-match"], etag)) {
|
|
1154
|
+
response.writeHead(NOT_MODIFIED, {
|
|
1155
|
+
etag,
|
|
1156
|
+
"cache-control": "no-store"
|
|
1157
|
+
}).end();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1114
1160
|
response.writeHead(OK, {
|
|
1115
1161
|
"content-type": TYPES[extname(file)] ?? "application/octet-stream",
|
|
1116
|
-
"cache-control": "no-store"
|
|
1162
|
+
"cache-control": "no-store",
|
|
1163
|
+
"content-length": size,
|
|
1164
|
+
etag
|
|
1117
1165
|
});
|
|
1118
1166
|
createReadStream(file).pipe(response);
|
|
1119
1167
|
});
|