@aletheia-ios/tools 0.2.0 → 0.2.2
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 +68 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1061,7 +1061,10 @@ const TRAVERSAL = /^(\.\.[/\\])+/;
|
|
|
1061
1061
|
/** How long after the last change to wait before rebuilding, so an editor's save burst is one build. */
|
|
1062
1062
|
const DEBOUNCE_MS = 300;
|
|
1063
1063
|
const OK = 200;
|
|
1064
|
+
const NOT_MODIFIED = 304;
|
|
1064
1065
|
const NOT_FOUND = 404;
|
|
1066
|
+
/** Base 16, so the validator stays short. */
|
|
1067
|
+
const HEX = 16;
|
|
1065
1068
|
/**
|
|
1066
1069
|
* Packs and indexes the whole repository, reporting failures instead of throwing.
|
|
1067
1070
|
*
|
|
@@ -1103,7 +1106,27 @@ async function resolveFile(dist, url) {
|
|
|
1103
1106
|
if (!(existsSync(file) && (await stat(file)).isFile())) return null;
|
|
1104
1107
|
return file;
|
|
1105
1108
|
}
|
|
1106
|
-
/**
|
|
1109
|
+
/**
|
|
1110
|
+
* A validator for the file as it is right now, from its size and modification time.
|
|
1111
|
+
*
|
|
1112
|
+
* Every rebuild rewrites the file, so the value changes and a conditional request gets the new
|
|
1113
|
+
* bytes. Between rebuilds it is stable, which is the only case that answers 304.
|
|
1114
|
+
*/
|
|
1115
|
+
function etagOf(size, mtimeMs) {
|
|
1116
|
+
return `"${size.toString(HEX)}-${Math.trunc(mtimeMs).toString(HEX)}"`;
|
|
1117
|
+
}
|
|
1118
|
+
/** Whether the request already holds this version. A client may offer several. */
|
|
1119
|
+
function offers(header, etag) {
|
|
1120
|
+
if (header === void 0) return false;
|
|
1121
|
+
return header.split(",").some((candidate) => candidate.trim() === etag);
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* A static server over `dist/`.
|
|
1125
|
+
*
|
|
1126
|
+
* Sends `cache-control: no-store` so nothing holds on to a dev build, alongside an `ETag` so a
|
|
1127
|
+
* client that asks can still be told nothing changed. The two are not in tension: `no-store`
|
|
1128
|
+
* governs whether a copy may be kept, the validator governs whether a fetch has to transfer.
|
|
1129
|
+
*/
|
|
1107
1130
|
function fileServer(dist) {
|
|
1108
1131
|
return createServer(async (request, response) => {
|
|
1109
1132
|
const file = await resolveFile(dist, request.url ?? "/");
|
|
@@ -1111,9 +1134,20 @@ function fileServer(dist) {
|
|
|
1111
1134
|
response.writeHead(NOT_FOUND).end();
|
|
1112
1135
|
return;
|
|
1113
1136
|
}
|
|
1137
|
+
const { size, mtimeMs } = await stat(file);
|
|
1138
|
+
const etag = etagOf(size, mtimeMs);
|
|
1139
|
+
if (offers(request.headers["if-none-match"], etag)) {
|
|
1140
|
+
response.writeHead(NOT_MODIFIED, {
|
|
1141
|
+
etag,
|
|
1142
|
+
"cache-control": "no-store"
|
|
1143
|
+
}).end();
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1114
1146
|
response.writeHead(OK, {
|
|
1115
1147
|
"content-type": TYPES[extname(file)] ?? "application/octet-stream",
|
|
1116
|
-
"cache-control": "no-store"
|
|
1148
|
+
"cache-control": "no-store",
|
|
1149
|
+
"content-length": size,
|
|
1150
|
+
etag
|
|
1117
1151
|
});
|
|
1118
1152
|
createReadStream(file).pipe(response);
|
|
1119
1153
|
});
|
|
@@ -1131,22 +1165,50 @@ function watchRepo(repo) {
|
|
|
1131
1165
|
return [watch(repo.packages, { recursive: true }, trigger), watch(repo.lists, { recursive: true }, trigger)];
|
|
1132
1166
|
}
|
|
1133
1167
|
/**
|
|
1168
|
+
* Binds the port and reports the one it got, since port 0 picks a free one.
|
|
1169
|
+
*
|
|
1170
|
+
* A busy port is the common way this fails and it arrives as an `error` event rather than a
|
|
1171
|
+
* rejection, so without this it surfaces as an unhandled event and a stack trace.
|
|
1172
|
+
*
|
|
1173
|
+
* @throws `CliError` naming the port when something already holds it.
|
|
1174
|
+
*/
|
|
1175
|
+
async function listen(server, port) {
|
|
1176
|
+
try {
|
|
1177
|
+
await new Promise((resolve, reject) => {
|
|
1178
|
+
server.once("error", reject);
|
|
1179
|
+
server.listen(port, "0.0.0.0", () => {
|
|
1180
|
+
server.removeListener("error", reject);
|
|
1181
|
+
resolve();
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
if (error.code === "EADDRINUSE") throw new CliError([`port ${port} is already in use - pass --port <n> to pick another`], { cause: error });
|
|
1186
|
+
throw error;
|
|
1187
|
+
}
|
|
1188
|
+
const address = server.address();
|
|
1189
|
+
return typeof address === "object" && address !== null ? address.port : port;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1134
1192
|
* The `serve` command: packs, indexes, serves `dist/` on the lan and rebuilds on change.
|
|
1135
1193
|
*
|
|
1136
1194
|
* Binds every interface and prints the lan URL of each list's `index.json`, which is what
|
|
1137
1195
|
* the app's developer list points at. Port 0 binds a free port. Resolves once listening;
|
|
1138
1196
|
* the returned `close` stops the watchers and the server.
|
|
1139
1197
|
*
|
|
1140
|
-
* @throws `CliError` when there is no `lists/` directory,
|
|
1198
|
+
* @throws `CliError` when there is no `lists/` directory, or when the port is taken.
|
|
1141
1199
|
*/
|
|
1142
1200
|
async function serve(repo, port) {
|
|
1143
1201
|
const lists = await loadLists(repo);
|
|
1144
1202
|
await rebuild(repo);
|
|
1145
1203
|
const server = fileServer(repo.dist);
|
|
1146
1204
|
const watchers = watchRepo(repo);
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1205
|
+
let bound;
|
|
1206
|
+
try {
|
|
1207
|
+
bound = await listen(server, port);
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
for (const watcher of watchers) watcher.close();
|
|
1210
|
+
throw error;
|
|
1211
|
+
}
|
|
1150
1212
|
const url = `http://${lanAddress()}:${bound}/`;
|
|
1151
1213
|
info(`serving ${repo.dist} on ${url}`);
|
|
1152
1214
|
for (const list of lists) {
|