@monimesl/dot-simulator 0.1.0-alpha.7 → 0.1.0-alpha.9
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/README.md +0 -5
- package/dist/client/service-bridge.js +101 -17
- package/dist/client/service-bridge.js.map +1 -1
- package/dist/client/simulator.js +164 -112
- package/dist/client/simulator.js.map +1 -1
- package/dist/client/webview-bridge.js.map +1 -1
- package/dist/node/index.d.ts +7 -7
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +385 -97
- package/dist/node/index.js.map +1 -1
- package/package.json +3 -3
package/dist/node/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { HostError, NavigationStack, REACT_HOST_PATH, allowsHost, dotFilePathProblem, isReactCompatible, isVersionAtLeast,
|
|
2
|
+
import { HostError, NavigationStack, REACT_HOST_PATH, allowsHost, dotFilePathProblem, isReactCompatible, isVersionAtLeast, newestReactOfMajor } from "@monimesl/dot-core";
|
|
3
3
|
import { appendFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { STATUS_CODES, createServer } from "node:http";
|
|
6
6
|
import { basename, dirname, extname, join, resolve, sep } from "node:path";
|
|
7
7
|
import { TextDecoder } from "node:util";
|
|
8
|
-
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
9
|
//#region src/compatibility.ts
|
|
10
10
|
var IncompatibleDotError = class extends Error {
|
|
11
11
|
name = "IncompatibleDotError";
|
|
@@ -16,10 +16,10 @@ var IncompatibleDotError = class extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
18
|
function checkCompatibility(manifest, { dashVersion, minRuntimeVersion, reactVersions }) {
|
|
19
|
-
if (!isVersionAtLeast(manifest.runtime, minRuntimeVersion)) throw new IncompatibleDotError("runtimeTooOld", `This dot was built with Dot runtime ${manifest.runtime}
|
|
19
|
+
if (!isVersionAtLeast(manifest.runtime, minRuntimeVersion)) throw new IncompatibleDotError("runtimeTooOld", `This dot was built with an old Dot runtime, ${manifest.runtime}. Rebuild it with @monimesl/dot-runtime ${minRuntimeVersion} or newer.`);
|
|
20
20
|
const minDashVersion = manifest.dash?.minVersion;
|
|
21
|
-
if (minDashVersion !== void 0 && !isVersionAtLeast(dashVersion, minDashVersion)) throw new IncompatibleDotError("dashTooOld", `This dot needs Dash ${minDashVersion} or newer
|
|
22
|
-
if (reactVersions !== void 0 && !reactVersions.some((version) => isReactCompatible(version, manifest.react))) throw new IncompatibleDotError("reactUnavailable", `This dot needs React ${manifest.react}
|
|
21
|
+
if (minDashVersion !== void 0 && !isVersionAtLeast(dashVersion, minDashVersion)) throw new IncompatibleDotError("dashTooOld", `This dot needs Dash ${minDashVersion} or newer. This is Dash ${dashVersion}.`);
|
|
22
|
+
if (reactVersions !== void 0 && !reactVersions.some((version) => isReactCompatible(version, manifest.react))) throw new IncompatibleDotError("reactUnavailable", `This dot needs React ${manifest.react}, which this host doesn't have.`);
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/host-calls.ts
|
|
@@ -41,7 +41,7 @@ var HostCallRunner = class {
|
|
|
41
41
|
id,
|
|
42
42
|
error: {
|
|
43
43
|
code: "unsupported",
|
|
44
|
-
message: `This host
|
|
44
|
+
message: `This host doesn't support "${method}"`
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
return;
|
|
@@ -64,7 +64,7 @@ var HostCallRunner = class {
|
|
|
64
64
|
this.#end(id)?.controller.abort();
|
|
65
65
|
}
|
|
66
66
|
dismissUI() {
|
|
67
|
-
for (const [id, call] of this.#running) if (call.method.startsWith(UI_METHOD_PREFIX)) this.#fail(id, new HostError("cancelled", "
|
|
67
|
+
for (const [id, call] of this.#running) if (call.method.startsWith(UI_METHOD_PREFIX)) this.#fail(id, new HostError("cancelled", "Its page is no longer on screen"));
|
|
68
68
|
}
|
|
69
69
|
async #invoke(id, entry, call) {
|
|
70
70
|
try {
|
|
@@ -126,7 +126,7 @@ var PermissionGate = class {
|
|
|
126
126
|
}
|
|
127
127
|
async check(method, permission, signal) {
|
|
128
128
|
const reason = this.#declared[permission];
|
|
129
|
-
if (reason === void 0) throw new HostError("denied", `"${method}" needs the "${permission}" permission. Add it to dot.json
|
|
129
|
+
if (reason === void 0) throw new HostError("denied", `"${method}" needs the "${permission}" permission. Add it to permissions in dot.json, with a reason to show the person.`);
|
|
130
130
|
let allowed = this.#answers.get(permission);
|
|
131
131
|
if (allowed === void 0) {
|
|
132
132
|
allowed = await this.#ask({
|
|
@@ -136,7 +136,7 @@ var PermissionGate = class {
|
|
|
136
136
|
}, signal);
|
|
137
137
|
this.#answers.set(permission, allowed);
|
|
138
138
|
}
|
|
139
|
-
if (!allowed) throw new HostError("denied", `The person
|
|
139
|
+
if (!allowed) throw new HostError("denied", `The person didn't allow "${permission}"`);
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
142
|
//#endregion
|
|
@@ -286,7 +286,10 @@ const SIMULATOR_VERSIONS = {
|
|
|
286
286
|
const CLIENT_PATH = "/__dot/";
|
|
287
287
|
const EVENTS_PATH = `${CLIENT_PATH}events`;
|
|
288
288
|
const HTTP_PATH = `${CLIENT_PATH}network/http/`;
|
|
289
|
-
const FILE_PATH = `${CLIENT_PATH}file/`;
|
|
289
|
+
const FILE_PATH = `${CLIENT_PATH}storage/file/`;
|
|
290
|
+
const CACHE_PATH = `${CLIENT_PATH}storage/cache/`;
|
|
291
|
+
const SECRET_PATH = `${CLIENT_PATH}storage/secret/`;
|
|
292
|
+
const TEMPORARY_FILES_PATH = `${CLIENT_PATH}files/`;
|
|
290
293
|
const WEBVIEW_URL = `/dot/ui/index.html`;
|
|
291
294
|
//#endregion
|
|
292
295
|
//#region src/page.ts
|
|
@@ -304,14 +307,6 @@ function simulatorPage(appName) {
|
|
|
304
307
|
<header>
|
|
305
308
|
<strong>${escapeHtml(appName)}</strong>
|
|
306
309
|
<span class="subtle">Dot simulator</span>
|
|
307
|
-
<select id="platform" aria-label="Target platform">
|
|
308
|
-
<option value="ios">iOS</option>
|
|
309
|
-
<option value="android">Android</option>
|
|
310
|
-
</select>
|
|
311
|
-
<select id="input-mode" aria-label="Input mode" title="Choose whether the simulator emulates a touch pointer">
|
|
312
|
-
<option value="touch">Touch</option>
|
|
313
|
-
<option value="browser">Browser</option>
|
|
314
|
-
</select>
|
|
315
310
|
<select id="scheme" aria-label="Device appearance">
|
|
316
311
|
<option value="light">Light</option>
|
|
317
312
|
<option value="dark">Dark</option>
|
|
@@ -370,14 +365,13 @@ const STYLES = `
|
|
|
370
365
|
* { box-sizing: border-box; }
|
|
371
366
|
body { margin: 0; height: 100vh; display: flex; flex-direction: column; background: var(--bg); color: var(--text); }
|
|
372
367
|
header { display: flex; align-items: center; gap: 12px; padding: 8px 16px; background: var(--panel); border-bottom: 1px solid var(--border); }
|
|
373
|
-
#
|
|
368
|
+
#scheme { margin-left: auto; }
|
|
374
369
|
button, select { font: inherit; color: inherit; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; cursor: pointer; }
|
|
375
370
|
.subtle { color: var(--subtle); }
|
|
376
371
|
main { flex: 1; display: flex; min-height: 0; }
|
|
377
372
|
.stage { flex: 1; overflow: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 24px 16px; }
|
|
378
|
-
.phone { flex: none; position: relative; display: flex; flex-direction: column; width: 393px; height: 852px; box-sizing: content-box; border: 12px solid #0b0b0c; border-radius: 67px; overflow: hidden; background: var(--screen); box-shadow: 0 0 0 2px #3a3a3c, 0 18px 50px rgb(0 0 0 / 0.3); --screen: #f2f2f2; --screen-text: #000; }
|
|
373
|
+
.phone { flex: none; position: relative; display: flex; flex-direction: column; width: 393px; height: 852px; box-sizing: content-box; border: 12px solid #0b0b0c; border-radius: 67px; overflow: hidden; background: var(--screen); box-shadow: 0 0 0 2px #3a3a3c, 0 18px 50px rgb(0 0 0 / 0.3); --screen: #f2f2f2; --screen-text: #000; --dialog: #f7f7f7; --dialog-line: #d1d1d6; }
|
|
379
374
|
.phone[data-scheme="dark"] { --screen: #121212; --screen-text: #fff; --dialog: #2c2c2e; --dialog-line: #48484a; }
|
|
380
|
-
.phone { --dialog: #f7f7f7; --dialog-line: #d1d1d6; }
|
|
381
375
|
.torch { display: none; }
|
|
382
376
|
.phone[data-torch="on"] .torch { display: block; }
|
|
383
377
|
.dialog-layer { position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center; background: rgb(0 0 0 / 0.3); }
|
|
@@ -450,6 +444,7 @@ function contentTypeFor(name) {
|
|
|
450
444
|
//#region src/network.ts
|
|
451
445
|
const MAX_RESPONSE_BYTES = 5 * MEGABYTE;
|
|
452
446
|
const MAX_DOWNLOAD_BYTES = 100 * MEGABYTE;
|
|
447
|
+
const MAX_REDIRECTS = 10;
|
|
453
448
|
const METHODS = [
|
|
454
449
|
"GET",
|
|
455
450
|
"POST",
|
|
@@ -481,13 +476,18 @@ const HOST_HEADERS = [
|
|
|
481
476
|
"upgrade"
|
|
482
477
|
];
|
|
483
478
|
const CONTENT_TYPE = /^[\w.+-]+\/[\w.+*-]+\s*(;.*)?$/;
|
|
479
|
+
function runHttpCall(call, params, hosts, files, signal) {
|
|
480
|
+
if (call === "download") return downloadHttpFile(params, hosts, files, signal);
|
|
481
|
+
if (call === "upload") return uploadHttpFiles(params, hosts, files, signal);
|
|
482
|
+
return sendHttpRequest(params, hosts, signal);
|
|
483
|
+
}
|
|
484
484
|
async function sendHttpRequest(request, hosts, signal) {
|
|
485
|
-
return exchange(textRequest(request), hosts, signal,
|
|
485
|
+
return exchange(textRequest(request), hosts, signal, readResponse);
|
|
486
486
|
}
|
|
487
487
|
async function downloadHttpFile(request, hosts, files, signal) {
|
|
488
488
|
return exchange(textRequest(request), hosts, signal, async (response) => {
|
|
489
489
|
if (!response.ok) return {
|
|
490
|
-
...
|
|
490
|
+
...await readResponse(response),
|
|
491
491
|
ok: false
|
|
492
492
|
};
|
|
493
493
|
const file = await save$1(response, request.name, files);
|
|
@@ -502,7 +502,7 @@ async function downloadHttpFile(request, hosts, files, signal) {
|
|
|
502
502
|
});
|
|
503
503
|
}
|
|
504
504
|
async function uploadHttpFiles(request, hosts, files, signal) {
|
|
505
|
-
if (request.files.length === 0) throw invalid("An upload needs at least one file
|
|
505
|
+
if (request.files.length === 0) throw invalid("An upload needs at least one file. To send only text, use http.request");
|
|
506
506
|
const parts = request.files.map((upload) => {
|
|
507
507
|
if (!upload.field.trim()) throw invalid("Each file needs a field name");
|
|
508
508
|
const file = files.requireFile(upload.file);
|
|
@@ -529,19 +529,19 @@ async function uploadHttpFiles(request, hosts, files, signal) {
|
|
|
529
529
|
kind: "form",
|
|
530
530
|
form: makeForm
|
|
531
531
|
}
|
|
532
|
-
}, hosts, signal,
|
|
532
|
+
}, hosts, signal, readResponse);
|
|
533
533
|
}
|
|
534
534
|
async function exchange(request, hosts, signal, read) {
|
|
535
535
|
let outgoing = request;
|
|
536
|
-
for (let redirects = 0; redirects <=
|
|
537
|
-
if (!allowsHost(hosts, outgoing.url.hostname)) throw new HostError("denied", `"${outgoing.url.hostname}"
|
|
536
|
+
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
|
|
537
|
+
if (!allowsHost(hosts, outgoing.url.hostname)) throw new HostError("denied", `"${outgoing.url.hostname}" isn't in network.hosts in dot.json. Add it there to send requests to it.`);
|
|
538
538
|
const response = await send$1(outgoing, signal);
|
|
539
539
|
const location = response.headers.get("location");
|
|
540
540
|
if (!REDIRECT_STATUSES.includes(response.status) || location === null) return read(response);
|
|
541
541
|
await response.body?.cancel();
|
|
542
542
|
outgoing = redirected(outgoing, location, response.status);
|
|
543
543
|
}
|
|
544
|
-
throw new HostError("failed", `"${request.url.href}" redirected more than
|
|
544
|
+
throw new HostError("failed", `"${request.url.href}" redirected more than ${MAX_REDIRECTS} times`);
|
|
545
545
|
}
|
|
546
546
|
function textRequest(request) {
|
|
547
547
|
const method = parseMethod(request.method ?? "GET", METHODS);
|
|
@@ -601,7 +601,7 @@ async function send$1(request, signal) {
|
|
|
601
601
|
}
|
|
602
602
|
function redirected(request, location, status) {
|
|
603
603
|
const url = URL.parse(location, request.url.href);
|
|
604
|
-
if (!url || url.protocol !== "http:" && url.protocol !== "https:") throw new HostError("failed", `${request.url.hostname} redirected to "${location}", which
|
|
604
|
+
if (!url || url.protocol !== "http:" && url.protocol !== "https:") throw new HostError("failed", `${request.url.hostname} redirected to "${location}", which isn't a web address`);
|
|
605
605
|
const becomesGet = status === 303 && request.method !== "HEAD" || (status === 301 || status === 302) && request.method === "POST";
|
|
606
606
|
const headers = new Headers(request.headers);
|
|
607
607
|
if (url.origin !== request.url.origin) headers.delete("authorization");
|
|
@@ -621,6 +621,9 @@ function result(response, body) {
|
|
|
621
621
|
body
|
|
622
622
|
};
|
|
623
623
|
}
|
|
624
|
+
async function readResponse(response) {
|
|
625
|
+
return result(response, await readBody(response));
|
|
626
|
+
}
|
|
624
627
|
async function readBody(response) {
|
|
625
628
|
const chunks = [];
|
|
626
629
|
await readChunks(response, MAX_RESPONSE_BYTES, (chunk) => {
|
|
@@ -705,7 +708,7 @@ function parseUrl(text) {
|
|
|
705
708
|
}
|
|
706
709
|
function parseMethod(name, methods) {
|
|
707
710
|
const method = name.toUpperCase();
|
|
708
|
-
if (!methods.includes(method)) throw invalid(`"${name}" is not an HTTP method
|
|
711
|
+
if (!methods.includes(method)) throw invalid(`"${name}" is not an HTTP method. Use ${methods.join(", ")}`);
|
|
709
712
|
return method;
|
|
710
713
|
}
|
|
711
714
|
function invalid(message) {
|
|
@@ -715,15 +718,291 @@ function tooLarge(limit) {
|
|
|
715
718
|
return new HostError("failed", `The response is larger than ${megabytes(limit)} MB`);
|
|
716
719
|
}
|
|
717
720
|
//#endregion
|
|
721
|
+
//#region src/serial.ts
|
|
722
|
+
var Serial = class {
|
|
723
|
+
#queue = Promise.resolve();
|
|
724
|
+
run(work) {
|
|
725
|
+
const result = this.#queue.then(work);
|
|
726
|
+
this.#queue = result.catch(() => void 0);
|
|
727
|
+
return result;
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/cache.ts
|
|
732
|
+
const DEFAULT_LIMIT = 64 * MEGABYTE;
|
|
733
|
+
const MAX_ENTRY_BYTES = MEGABYTE;
|
|
734
|
+
const MAX_KEY_LENGTH = 512;
|
|
735
|
+
const PARTIAL = ".partial";
|
|
736
|
+
var DotCache = class {
|
|
737
|
+
root;
|
|
738
|
+
limit;
|
|
739
|
+
#now;
|
|
740
|
+
#lock = new Serial();
|
|
741
|
+
#index = null;
|
|
742
|
+
#used = 0;
|
|
743
|
+
constructor(root, limit = DEFAULT_LIMIT, now = Date.now) {
|
|
744
|
+
this.root = root;
|
|
745
|
+
this.limit = limit;
|
|
746
|
+
this.#now = now;
|
|
747
|
+
}
|
|
748
|
+
get(key) {
|
|
749
|
+
return this.#lock.run(async () => await this.#live(key) ? this.#readValue(key) : null);
|
|
750
|
+
}
|
|
751
|
+
exists(key) {
|
|
752
|
+
return this.#lock.run(async () => await this.#live(key) !== null);
|
|
753
|
+
}
|
|
754
|
+
set(key, value, ttl) {
|
|
755
|
+
return this.#lock.run(async () => {
|
|
756
|
+
checkKey(key);
|
|
757
|
+
if (value === null) throw new HostError("invalid", `"${key}" needs a value. To delete it, use remove`);
|
|
758
|
+
const entry = {
|
|
759
|
+
key,
|
|
760
|
+
value
|
|
761
|
+
};
|
|
762
|
+
if (ttl !== void 0 && ttl > 0) entry.expiresAt = this.#now() + Math.trunc(ttl * 1e3);
|
|
763
|
+
const bytes = Buffer.from(JSON.stringify(entry));
|
|
764
|
+
if (bytes.length > MAX_ENTRY_BYTES) throw new HostError("invalid", `"${key}" is ${megabytes(bytes.length)} MB. A cache value can be at most ${megabytes(MAX_ENTRY_BYTES)} MB`);
|
|
765
|
+
const index = await this.#loadIndex();
|
|
766
|
+
const addedBytes = bytes.length - (index.get(key)?.size ?? 0);
|
|
767
|
+
if (this.#used + addedBytes > this.limit) {
|
|
768
|
+
await this.#dropExpired();
|
|
769
|
+
if (this.#used + addedBytes > this.limit) throw new HostError("failed", `The cache is full: ${megabytes(this.#used)} of ${megabytes(this.limit)} MB used`);
|
|
770
|
+
}
|
|
771
|
+
await mkdir(this.root, { recursive: true });
|
|
772
|
+
const file = this.#fileFor(key);
|
|
773
|
+
await writeFile(`${file}${PARTIAL}`, bytes);
|
|
774
|
+
await rename(`${file}${PARTIAL}`, file);
|
|
775
|
+
this.#remember(index, key, {
|
|
776
|
+
size: bytes.length,
|
|
777
|
+
expiresAt: entry.expiresAt
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
remove(key) {
|
|
782
|
+
return this.#lock.run(async () => {
|
|
783
|
+
checkKey(key);
|
|
784
|
+
await this.#forget(key);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
keys() {
|
|
788
|
+
return this.#lock.run(async () => {
|
|
789
|
+
await this.#dropExpired();
|
|
790
|
+
return [...(await this.#loadIndex()).keys()].sort();
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
clear() {
|
|
794
|
+
return this.#lock.run(async () => {
|
|
795
|
+
await rm(this.root, {
|
|
796
|
+
recursive: true,
|
|
797
|
+
force: true
|
|
798
|
+
});
|
|
799
|
+
this.#index = /* @__PURE__ */ new Map();
|
|
800
|
+
this.#used = 0;
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
tidy() {
|
|
804
|
+
return this.#lock.run(async () => {
|
|
805
|
+
this.#index = null;
|
|
806
|
+
await this.#loadIndex();
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
usage() {
|
|
810
|
+
return this.#lock.run(async () => {
|
|
811
|
+
await this.#loadIndex();
|
|
812
|
+
return this.#used;
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
async #loadIndex() {
|
|
816
|
+
if (this.#index) return this.#index;
|
|
817
|
+
const index = /* @__PURE__ */ new Map();
|
|
818
|
+
this.#index = index;
|
|
819
|
+
this.#used = 0;
|
|
820
|
+
for (const name of await readdir(this.root).catch(() => [])) {
|
|
821
|
+
const file = join(this.root, name);
|
|
822
|
+
const entry = name.endsWith(PARTIAL) ? null : await entryAt(file);
|
|
823
|
+
if (!entry || this.#expired(entry.expiresAt)) {
|
|
824
|
+
await rm(file, { force: true });
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
this.#remember(index, entry.key, {
|
|
828
|
+
size: (await stat(file)).size,
|
|
829
|
+
expiresAt: entry.expiresAt
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
return index;
|
|
833
|
+
}
|
|
834
|
+
async #live(key) {
|
|
835
|
+
checkKey(key);
|
|
836
|
+
const indexed = (await this.#loadIndex()).get(key);
|
|
837
|
+
if (!indexed) return null;
|
|
838
|
+
if (this.#expired(indexed.expiresAt)) {
|
|
839
|
+
await this.#forget(key);
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
return indexed;
|
|
843
|
+
}
|
|
844
|
+
async #readValue(key) {
|
|
845
|
+
const entry = await entryAt(this.#fileFor(key));
|
|
846
|
+
if (!entry) await this.#forget(key);
|
|
847
|
+
return entry?.value ?? null;
|
|
848
|
+
}
|
|
849
|
+
#remember(index, key, indexed) {
|
|
850
|
+
this.#used += indexed.size - (index.get(key)?.size ?? 0);
|
|
851
|
+
index.set(key, indexed);
|
|
852
|
+
}
|
|
853
|
+
async #forget(key) {
|
|
854
|
+
await rm(this.#fileFor(key), { force: true });
|
|
855
|
+
const index = await this.#loadIndex();
|
|
856
|
+
this.#used -= index.get(key)?.size ?? 0;
|
|
857
|
+
index.delete(key);
|
|
858
|
+
}
|
|
859
|
+
async #dropExpired() {
|
|
860
|
+
for (const [key, indexed] of await this.#loadIndex()) if (this.#expired(indexed.expiresAt)) await this.#forget(key);
|
|
861
|
+
}
|
|
862
|
+
#expired(expiresAt) {
|
|
863
|
+
return expiresAt !== void 0 && expiresAt <= this.#now();
|
|
864
|
+
}
|
|
865
|
+
#fileFor(key) {
|
|
866
|
+
return join(this.root, `${createHash("sha256").update(key).digest("hex")}.json`);
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
function runCacheCall(operation, params, cache) {
|
|
870
|
+
switch (operation) {
|
|
871
|
+
case "get": return cache.get(params.key);
|
|
872
|
+
case "set": {
|
|
873
|
+
const { key, value, ttl } = params;
|
|
874
|
+
return cache.set(key, value ?? null, ttl);
|
|
875
|
+
}
|
|
876
|
+
case "remove": return cache.remove(params.key);
|
|
877
|
+
case "exists": return cache.exists(params.key);
|
|
878
|
+
case "keys": return cache.keys();
|
|
879
|
+
case "clear": return cache.clear();
|
|
880
|
+
default: return Promise.reject(new HostError("unsupported", `This host doesn't support "dash.storage.cache.${operation}"`));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function entryAt(file) {
|
|
884
|
+
if (!file.endsWith(".json")) return null;
|
|
885
|
+
try {
|
|
886
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function checkKey(key) {
|
|
892
|
+
if (key === "") throw new HostError("invalid", "A key is needed");
|
|
893
|
+
if (key.length > MAX_KEY_LENGTH) throw new HostError("invalid", `"${key.slice(0, 40)}…" is longer than ${MAX_KEY_LENGTH} characters`);
|
|
894
|
+
}
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/secrets.ts
|
|
897
|
+
const MAX_ENTRIES = 8;
|
|
898
|
+
const MAX_VALUE_BYTES = 4096;
|
|
899
|
+
var DotSecrets = class {
|
|
900
|
+
file;
|
|
901
|
+
#partial;
|
|
902
|
+
#lock = new Serial();
|
|
903
|
+
constructor(file) {
|
|
904
|
+
this.file = file;
|
|
905
|
+
this.#partial = `${file}.partial`;
|
|
906
|
+
}
|
|
907
|
+
get(key) {
|
|
908
|
+
return this.#lock.run(async () => {
|
|
909
|
+
checkKey(key);
|
|
910
|
+
return (await this.#read()).get(key) ?? null;
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
set(key, value) {
|
|
914
|
+
return this.#lock.run(async () => {
|
|
915
|
+
checkKey(key);
|
|
916
|
+
if (typeof value !== "string") throw new HostError("invalid", `"${key}" needs a text value`);
|
|
917
|
+
if (Buffer.byteLength(value) > MAX_VALUE_BYTES) throw new HostError("invalid", `"${key}" is too long: a secret can be at most 4 KB`);
|
|
918
|
+
const entries = await this.#read();
|
|
919
|
+
if (!entries.has(key) && entries.size >= MAX_ENTRIES) throw new HostError("failed", `A dot can only write a maximum of ${MAX_ENTRIES} secrets. Remove one first`);
|
|
920
|
+
await this.#write(entries.set(key, value));
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
remove(key) {
|
|
924
|
+
return this.#lock.run(async () => {
|
|
925
|
+
checkKey(key);
|
|
926
|
+
const entries = await this.#read();
|
|
927
|
+
if (entries.delete(key)) await this.#write(entries);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
clear() {
|
|
931
|
+
return this.#lock.run(() => this.#write(/* @__PURE__ */ new Map()));
|
|
932
|
+
}
|
|
933
|
+
tidy() {
|
|
934
|
+
return this.#lock.run(() => rm(this.#partial, { force: true }));
|
|
935
|
+
}
|
|
936
|
+
async #read() {
|
|
937
|
+
try {
|
|
938
|
+
return new Map(Object.entries(JSON.parse(await readFile(this.file, "utf8"))));
|
|
939
|
+
} catch {
|
|
940
|
+
return /* @__PURE__ */ new Map();
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async #write(entries) {
|
|
944
|
+
if (entries.size === 0) {
|
|
945
|
+
await rm(this.file, { force: true });
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
949
|
+
await writeFile(this.#partial, JSON.stringify(Object.fromEntries(entries)));
|
|
950
|
+
await rename(this.#partial, this.file);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
function runSecretCall(operation, params, secrets) {
|
|
954
|
+
switch (operation) {
|
|
955
|
+
case "get": return secrets.get(params.key);
|
|
956
|
+
case "set": {
|
|
957
|
+
const { key, value } = params;
|
|
958
|
+
return secrets.set(key, value);
|
|
959
|
+
}
|
|
960
|
+
case "remove": return secrets.remove(params.key);
|
|
961
|
+
case "clear": return secrets.clear();
|
|
962
|
+
default: return Promise.reject(new HostError("unsupported", `This host doesn't support "dash.storage.secret.${operation}"`));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
//#endregion
|
|
718
966
|
//#region src/files.ts
|
|
719
|
-
const DEFAULT_QUOTA =
|
|
967
|
+
const DEFAULT_QUOTA = 256 * MEGABYTE;
|
|
968
|
+
const CACHE_FOLDER = ".cache";
|
|
969
|
+
const SECRETS_FOLDER = ".secrets";
|
|
970
|
+
const STAGING_FOLDER = ".staging";
|
|
720
971
|
const MAX_READ_BYTES = 5 * MEGABYTE;
|
|
721
972
|
var DotStorage = class {
|
|
722
973
|
#root;
|
|
974
|
+
#staging;
|
|
723
975
|
quota;
|
|
976
|
+
cache;
|
|
977
|
+
secrets;
|
|
978
|
+
lock = new Serial();
|
|
724
979
|
constructor(root, quota = DEFAULT_QUOTA) {
|
|
725
980
|
this.#root = root;
|
|
981
|
+
this.#staging = join(dirname(root), STAGING_FOLDER, basename(root));
|
|
726
982
|
this.quota = quota;
|
|
983
|
+
this.cache = new DotCache(join(dirname(root), CACHE_FOLDER, basename(root)));
|
|
984
|
+
this.secrets = new DotSecrets(join(dirname(root), SECRETS_FOLDER, `${basename(root)}.json`));
|
|
985
|
+
}
|
|
986
|
+
async tidy() {
|
|
987
|
+
await rm(this.#staging, {
|
|
988
|
+
recursive: true,
|
|
989
|
+
force: true
|
|
990
|
+
});
|
|
991
|
+
await this.cache.tidy();
|
|
992
|
+
await this.secrets.tidy();
|
|
993
|
+
}
|
|
994
|
+
async replace(target, write) {
|
|
995
|
+
await mkdir(this.#staging, { recursive: true });
|
|
996
|
+
const staged = join(this.#staging, randomUUID());
|
|
997
|
+
try {
|
|
998
|
+
await write(staged);
|
|
999
|
+
await moveInto(staged, target);
|
|
1000
|
+
} finally {
|
|
1001
|
+
await rm(staged, {
|
|
1002
|
+
recursive: true,
|
|
1003
|
+
force: true
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
727
1006
|
}
|
|
728
1007
|
async resolve(path, { allowRoot = false } = {}) {
|
|
729
1008
|
const problem = dotFilePathProblem(path, { allowRoot });
|
|
@@ -735,12 +1014,6 @@ var DotStorage = class {
|
|
|
735
1014
|
async usage() {
|
|
736
1015
|
return sizeOf(this.#root);
|
|
737
1016
|
}
|
|
738
|
-
async clear() {
|
|
739
|
-
await rm(this.#root, {
|
|
740
|
-
recursive: true,
|
|
741
|
-
force: true
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
1017
|
async ensureRoot() {
|
|
745
1018
|
await mkdir(this.#root, { recursive: true });
|
|
746
1019
|
}
|
|
@@ -750,53 +1023,54 @@ var DotStorage = class {
|
|
|
750
1023
|
let existing = path;
|
|
751
1024
|
while (!await exists(existing)) existing = dirname(existing);
|
|
752
1025
|
const real = await realpath(existing);
|
|
753
|
-
if (real !== realRoot && !real.startsWith(`${realRoot}/`)) throw new HostError("denied", "That path
|
|
1026
|
+
if (real !== realRoot && !real.startsWith(`${realRoot}/`)) throw new HostError("denied", "That path goes outside the dot's folder");
|
|
754
1027
|
}
|
|
755
1028
|
};
|
|
756
1029
|
async function runFileCall(operation, params, storage, temporaryFiles) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1030
|
+
return storage.lock.run(async () => {
|
|
1031
|
+
switch (operation) {
|
|
1032
|
+
case "read": return read(params, storage);
|
|
1033
|
+
case "write": return write(params, storage);
|
|
1034
|
+
case "stat": {
|
|
1035
|
+
const { path } = params;
|
|
1036
|
+
return entry(path, await storage.resolve(path, { allowRoot: true }));
|
|
1037
|
+
}
|
|
1038
|
+
case "list": return list(params, storage);
|
|
1039
|
+
case "createFolder": return createFolder(params.path, storage);
|
|
1040
|
+
case "move": return transfer(params, storage, false);
|
|
1041
|
+
case "copy": return transfer(params, storage, true);
|
|
1042
|
+
case "remove": return remove(params, storage);
|
|
1043
|
+
case "save": return save(params, storage, temporaryFiles);
|
|
1044
|
+
case "open": return open$1(params.path, storage, temporaryFiles);
|
|
1045
|
+
case "usage": return {
|
|
1046
|
+
used: await storage.usage(),
|
|
1047
|
+
quota: storage.quota
|
|
1048
|
+
};
|
|
1049
|
+
default: throw new HostError("unsupported", `This host doesn't support "dash.storage.file.${operation}"`);
|
|
763
1050
|
}
|
|
764
|
-
|
|
765
|
-
case "createFolder": return createFolder(params.path, storage);
|
|
766
|
-
case "move": return transfer(params, storage, false);
|
|
767
|
-
case "copy": return transfer(params, storage, true);
|
|
768
|
-
case "remove": return remove(params, storage);
|
|
769
|
-
case "save": return save(params, storage, temporaryFiles);
|
|
770
|
-
case "open": return open$1(params.path, storage, temporaryFiles);
|
|
771
|
-
case "usage": return {
|
|
772
|
-
used: await storage.usage(),
|
|
773
|
-
quota: storage.quota
|
|
774
|
-
};
|
|
775
|
-
default: throw new HostError("unsupported", `This host has no "dash.file.${operation}"`);
|
|
776
|
-
}
|
|
1051
|
+
});
|
|
777
1052
|
}
|
|
778
1053
|
async function read({ path, as }, storage) {
|
|
779
|
-
const encoding =
|
|
1054
|
+
const encoding = bufferEncoding(as);
|
|
780
1055
|
const file = await storage.resolve(path);
|
|
781
1056
|
const info = await statOrNull(file);
|
|
782
1057
|
if (!info) throw failed(`There is no file at "${path}"`);
|
|
783
1058
|
if (info.isDirectory()) throw failed(`"${path}" is a folder`);
|
|
784
1059
|
if (info.size > MAX_READ_BYTES) throw failed(`"${path}" is larger than ${megabytes(MAX_READ_BYTES)} MB, too large to read at once`);
|
|
785
|
-
|
|
786
|
-
return encoding === "base64" ? bytes.toString("base64") : bytes.toString("utf8");
|
|
1060
|
+
return (await readFile(file)).toString(encoding);
|
|
787
1061
|
}
|
|
788
1062
|
async function write({ path, data, as, append = false }, storage) {
|
|
789
|
-
const encoding =
|
|
1063
|
+
const encoding = bufferEncoding(as);
|
|
790
1064
|
const file = await storage.resolve(path);
|
|
791
1065
|
const existing = await statOrNull(file);
|
|
792
1066
|
if (existing?.isDirectory()) throw failed(`"${path}" is a folder`);
|
|
793
1067
|
if (encoding === "base64" && !/^[A-Za-z0-9+/]*={0,2}$/.test(data)) throw new HostError("invalid", "The data isn't base64");
|
|
794
|
-
const bytes = Buffer.from(data, encoding
|
|
1068
|
+
const bytes = Buffer.from(data, encoding);
|
|
795
1069
|
const replacedBytes = append ? 0 : existing?.size ?? 0;
|
|
796
1070
|
await checkQuota(storage, bytes.length - replacedBytes);
|
|
797
1071
|
await createParent(storage, file, path);
|
|
798
1072
|
if (append) await appendFile(file, bytes);
|
|
799
|
-
else await
|
|
1073
|
+
else await storage.replace(file, (staged) => writeFile(staged, bytes));
|
|
800
1074
|
return written(path, file);
|
|
801
1075
|
}
|
|
802
1076
|
async function list({ path, recursive = false }, storage) {
|
|
@@ -822,17 +1096,17 @@ async function transfer({ from, to, overwrite = false }, storage, keepSource) {
|
|
|
822
1096
|
const source = await storage.resolve(from);
|
|
823
1097
|
const target = await storage.resolve(to);
|
|
824
1098
|
if (!await exists(source)) throw failed(`There is nothing at "${from}"`);
|
|
825
|
-
if (to === from || to.startsWith(`${from}/`)) throw new HostError("invalid", `"${
|
|
1099
|
+
if (to === from || to.startsWith(`${from}/`)) throw new HostError("invalid", `Can't put "${from}" inside itself, at "${to}"`);
|
|
826
1100
|
await makeRoom(storage, target, to, overwrite, keepSource ? await sizeOf(source) : 0);
|
|
827
|
-
if (keepSource) await cp(source,
|
|
828
|
-
else await
|
|
1101
|
+
if (keepSource) await storage.replace(target, (staged) => cp(source, staged, { recursive: true }));
|
|
1102
|
+
else await moveInto(source, target);
|
|
829
1103
|
return written(to, target);
|
|
830
1104
|
}
|
|
831
1105
|
async function remove({ path, recursive = false }, storage) {
|
|
832
1106
|
const target = await storage.resolve(path);
|
|
833
1107
|
const info = await statOrNull(target);
|
|
834
1108
|
if (!info) return;
|
|
835
|
-
if (info.isDirectory() && !recursive && (await readdir(target)).length > 0) throw failed(`"${path}" is a folder with files in it
|
|
1109
|
+
if (info.isDirectory() && !recursive && (await readdir(target)).length > 0) throw failed(`"${path}" is a folder with files in it. To remove them too, pass { recursive: true }`);
|
|
836
1110
|
await rm(target, {
|
|
837
1111
|
recursive: true,
|
|
838
1112
|
force: true
|
|
@@ -842,7 +1116,7 @@ async function save({ file: id, path, overwrite = false }, storage, temporaryFil
|
|
|
842
1116
|
const file = temporaryFiles.requireFile(id);
|
|
843
1117
|
const target = await storage.resolve(path);
|
|
844
1118
|
await makeRoom(storage, target, path, overwrite, file.size);
|
|
845
|
-
await cp(file.path,
|
|
1119
|
+
await storage.replace(target, (staged) => cp(file.path, staged));
|
|
846
1120
|
return written(path, target);
|
|
847
1121
|
}
|
|
848
1122
|
async function open$1(path, storage, temporaryFiles) {
|
|
@@ -888,24 +1162,28 @@ async function written(path, file) {
|
|
|
888
1162
|
return found;
|
|
889
1163
|
}
|
|
890
1164
|
async function makeRoom(storage, target, path, overwrite, addedBytes) {
|
|
891
|
-
if (await exists(target) && !overwrite) throw failed(`"${path}" already exists
|
|
1165
|
+
if (await exists(target) && !overwrite) throw failed(`"${path}" already exists. To replace it, pass { overwrite: true }`);
|
|
892
1166
|
await checkQuota(storage, addedBytes - await sizeOf(target));
|
|
893
|
-
await
|
|
1167
|
+
await createParent(storage, target, path);
|
|
1168
|
+
}
|
|
1169
|
+
async function moveInto(from, target) {
|
|
1170
|
+
const targetInfo = await statOrNull(target);
|
|
1171
|
+
if (targetInfo && (targetInfo.isDirectory() || (await statOrNull(from))?.isDirectory())) await rm(target, {
|
|
894
1172
|
recursive: true,
|
|
895
1173
|
force: true
|
|
896
1174
|
});
|
|
897
|
-
await
|
|
1175
|
+
await rename(from, target);
|
|
898
1176
|
}
|
|
899
1177
|
async function createParent(storage, file, path) {
|
|
900
1178
|
await storage.ensureRoot();
|
|
901
1179
|
const parent = dirname(file);
|
|
902
|
-
if ((await statOrNull(parent))?.isFile()) throw failed(`
|
|
1180
|
+
if ((await statOrNull(parent))?.isFile()) throw failed(`Part of "${path}" is a file, not a folder`);
|
|
903
1181
|
await mkdir(parent, { recursive: true });
|
|
904
1182
|
}
|
|
905
1183
|
async function checkQuota(storage, addedBytes) {
|
|
906
1184
|
if (addedBytes <= 0) return;
|
|
907
1185
|
const used = await storage.usage();
|
|
908
|
-
if (used + addedBytes > storage.quota) throw failed(`The dot's
|
|
1186
|
+
if (used + addedBytes > storage.quota) throw failed(`The dot's folder is full: ${megabytes(used)} of ${megabytes(storage.quota)} MB used, and this needs ${megabytes(addedBytes)} MB more`);
|
|
909
1187
|
}
|
|
910
1188
|
async function sizeOf(path) {
|
|
911
1189
|
const info = await lstat(path).catch(() => null);
|
|
@@ -921,16 +1199,16 @@ async function exists(path) {
|
|
|
921
1199
|
async function statOrNull(path) {
|
|
922
1200
|
return stat(path).catch(() => null);
|
|
923
1201
|
}
|
|
924
|
-
function
|
|
925
|
-
if (name === void 0 || name === "text"
|
|
926
|
-
|
|
1202
|
+
function bufferEncoding(name) {
|
|
1203
|
+
if (name === void 0 || name === "text") return "utf8";
|
|
1204
|
+
if (name === "base64") return "base64";
|
|
1205
|
+
throw new HostError("invalid", `"${String(name)}" is not an encoding. Use "text" or "base64"`);
|
|
927
1206
|
}
|
|
928
1207
|
function failed(message) {
|
|
929
1208
|
return new HostError("failed", message);
|
|
930
1209
|
}
|
|
931
1210
|
//#endregion
|
|
932
1211
|
//#region src/temporary-files.ts
|
|
933
|
-
const TEMPORARY_FILES_PATH = "/__dot/files/";
|
|
934
1212
|
var TemporaryFiles = class {
|
|
935
1213
|
#directory;
|
|
936
1214
|
#files = /* @__PURE__ */ new Map();
|
|
@@ -942,7 +1220,7 @@ var TemporaryFiles = class {
|
|
|
942
1220
|
}
|
|
943
1221
|
requireFile(id) {
|
|
944
1222
|
const file = this.#files.get(id);
|
|
945
|
-
if (!file) throw new HostError("invalid", `"${id}"
|
|
1223
|
+
if (!file) throw new HostError("invalid", `"${id}" isn't one of this dot's files`);
|
|
946
1224
|
return file;
|
|
947
1225
|
}
|
|
948
1226
|
async clear() {
|
|
@@ -1008,6 +1286,7 @@ async function startSimulator({ dotDir, reactHostDir, name = "Dot", port = DEFAU
|
|
|
1008
1286
|
const clients = /* @__PURE__ */ new Set();
|
|
1009
1287
|
const files = new TemporaryFiles(await mkdtemp(join(tmpdir(), "dot-simulator-files-")));
|
|
1010
1288
|
const storage = new DotStorage(storageDir ?? await mkdtemp(join(tmpdir(), "dot-simulator-storage-")));
|
|
1289
|
+
await storage.tidy();
|
|
1011
1290
|
let buildError = null;
|
|
1012
1291
|
const broadcast = (event, data = "") => {
|
|
1013
1292
|
for (const client of clients) client.write(serverEvent(event, data));
|
|
@@ -1035,7 +1314,8 @@ async function startSimulator({ dotDir, reactHostDir, name = "Dot", port = DEFAU
|
|
|
1035
1314
|
return;
|
|
1036
1315
|
}
|
|
1037
1316
|
if (pathname.startsWith(HTTP_PATH) && request.method === "POST") {
|
|
1038
|
-
|
|
1317
|
+
const call = pathname.slice(HTTP_PATH.length);
|
|
1318
|
+
answer(request, response, async (params, signal) => runHttpCall(call, params, await networkHosts(dotDir), files, signal));
|
|
1039
1319
|
return;
|
|
1040
1320
|
}
|
|
1041
1321
|
if (pathname.startsWith(FILE_PATH) && request.method === "POST") {
|
|
@@ -1043,8 +1323,18 @@ async function startSimulator({ dotDir, reactHostDir, name = "Dot", port = DEFAU
|
|
|
1043
1323
|
answer(request, response, (params) => runFileCall(operation, params, storage, files));
|
|
1044
1324
|
return;
|
|
1045
1325
|
}
|
|
1046
|
-
if (pathname.startsWith("
|
|
1047
|
-
|
|
1326
|
+
if (pathname.startsWith(CACHE_PATH) && request.method === "POST") {
|
|
1327
|
+
const operation = pathname.slice(CACHE_PATH.length);
|
|
1328
|
+
answer(request, response, (params) => runCacheCall(operation, params, storage.cache));
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (pathname.startsWith(SECRET_PATH) && request.method === "POST") {
|
|
1332
|
+
const operation = pathname.slice(SECRET_PATH.length);
|
|
1333
|
+
answer(request, response, (params) => runSecretCall(operation, params, storage.secrets));
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (pathname.startsWith(TEMPORARY_FILES_PATH)) {
|
|
1337
|
+
serveTemporaryFile(response, files, pathname.slice(TEMPORARY_FILES_PATH.length));
|
|
1048
1338
|
return;
|
|
1049
1339
|
}
|
|
1050
1340
|
if (pathname.startsWith(REACT_HOST_PATH) && reactHostDir !== void 0) {
|
|
@@ -1060,7 +1350,7 @@ async function startSimulator({ dotDir, reactHostDir, name = "Dot", port = DEFAU
|
|
|
1060
1350
|
serveFile(response, dotDir, pathname.slice(5), injectBridge);
|
|
1061
1351
|
return;
|
|
1062
1352
|
}
|
|
1063
|
-
|
|
1353
|
+
notFound(response);
|
|
1064
1354
|
});
|
|
1065
1355
|
await listen(server, port, host);
|
|
1066
1356
|
return {
|
|
@@ -1078,20 +1368,15 @@ async function startSimulator({ dotDir, reactHostDir, name = "Dot", port = DEFAU
|
|
|
1078
1368
|
buildError = message;
|
|
1079
1369
|
broadcast("build-error", message);
|
|
1080
1370
|
},
|
|
1081
|
-
close:
|
|
1371
|
+
close: async () => {
|
|
1082
1372
|
for (const client of clients) client.end();
|
|
1083
|
-
|
|
1084
|
-
|
|
1373
|
+
await new Promise((resolvePromise) => {
|
|
1374
|
+
server.close(() => resolvePromise());
|
|
1375
|
+
});
|
|
1376
|
+
await files.clear();
|
|
1377
|
+
}
|
|
1085
1378
|
};
|
|
1086
1379
|
}
|
|
1087
|
-
function relayHttpCall(request, response, call, dotDir, files) {
|
|
1088
|
-
return answer(request, response, async (params, signal) => {
|
|
1089
|
-
const hosts = await networkHosts(dotDir);
|
|
1090
|
-
if (call === "download") return downloadHttpFile(params, hosts, files, signal);
|
|
1091
|
-
if (call === "upload") return uploadHttpFiles(params, hosts, files, signal);
|
|
1092
|
-
return sendHttpRequest(params, hosts, signal);
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
1380
|
async function answer(request, response, run) {
|
|
1096
1381
|
const controller = new AbortController();
|
|
1097
1382
|
response.on("close", () => controller.abort());
|
|
@@ -1112,7 +1397,7 @@ async function answer(request, response, run) {
|
|
|
1112
1397
|
async function serveTemporaryFile(response, files, id) {
|
|
1113
1398
|
const file = files.file(id);
|
|
1114
1399
|
if (!file) {
|
|
1115
|
-
|
|
1400
|
+
notFound(response);
|
|
1116
1401
|
return;
|
|
1117
1402
|
}
|
|
1118
1403
|
response.writeHead(200, {
|
|
@@ -1147,7 +1432,7 @@ async function serveReact(response, reactHostDir, path) {
|
|
|
1147
1432
|
const versions = await readdir(reactHostDir).catch(() => []);
|
|
1148
1433
|
const version = newestReactOfMajor(versions, Number(major));
|
|
1149
1434
|
if (version === void 0) {
|
|
1150
|
-
|
|
1435
|
+
notFound(response);
|
|
1151
1436
|
return;
|
|
1152
1437
|
}
|
|
1153
1438
|
await serveFile(response, join(reactHostDir, version), file.join("/"));
|
|
@@ -1156,7 +1441,7 @@ async function serveFile(response, root, path, injectBridge = false) {
|
|
|
1156
1441
|
const base = resolve(root);
|
|
1157
1442
|
let file = resolve(base, decodeURIComponent(path));
|
|
1158
1443
|
if (file !== base && !file.startsWith(base + sep)) {
|
|
1159
|
-
|
|
1444
|
+
notFound(response);
|
|
1160
1445
|
return;
|
|
1161
1446
|
}
|
|
1162
1447
|
try {
|
|
@@ -1169,7 +1454,7 @@ async function serveFile(response, root, path, injectBridge = false) {
|
|
|
1169
1454
|
const bridge = `<script src="${CLIENT_PATH}webview-bridge.js"><\/script>`;
|
|
1170
1455
|
send(response, 200, body.toString("utf8").replace("<head>", `<head>\n ${bridge}`), ".html");
|
|
1171
1456
|
} catch {
|
|
1172
|
-
|
|
1457
|
+
notFound(response);
|
|
1173
1458
|
}
|
|
1174
1459
|
}
|
|
1175
1460
|
function send(response, status, body, extension = ".txt") {
|
|
@@ -1179,6 +1464,9 @@ function send(response, status, body, extension = ".txt") {
|
|
|
1179
1464
|
});
|
|
1180
1465
|
response.end(body);
|
|
1181
1466
|
}
|
|
1467
|
+
function notFound(response) {
|
|
1468
|
+
send(response, 404, "Not found");
|
|
1469
|
+
}
|
|
1182
1470
|
function defaultClientDir() {
|
|
1183
1471
|
const manifest = createRequire(import.meta.url).resolve("@monimesl/dot-simulator/package.json");
|
|
1184
1472
|
return join(dirname(manifest), "dist/client");
|