@monimesl/dot-simulator 0.1.0-alpha.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.
@@ -0,0 +1,493 @@
1
+ import { createRequire } from "node:module";
2
+ import { HostError, NavigationStack, REACT_HOST_PATH, isReactCompatible, isVersionAtLeast, majorVersion, newestReactOfMajor } from "@monimesl/dot-core";
3
+ import { readFile, readdir, stat } from "node:fs/promises";
4
+ import { createServer } from "node:http";
5
+ import { dirname, extname, join, resolve, sep } from "node:path";
6
+ //#region src/compatibility.ts
7
+ var IncompatibleDotError = class extends Error {
8
+ name = "IncompatibleDotError";
9
+ reason;
10
+ constructor(reason, message) {
11
+ super(message);
12
+ this.reason = reason;
13
+ }
14
+ };
15
+ function checkCompatibility(manifest, { dashVersion, minRuntimeVersion, reactVersions }) {
16
+ if (!isVersionAtLeast(manifest.runtime, minRuntimeVersion)) throw new IncompatibleDotError("runtimeTooOld", `This dot was built with Dot runtime ${manifest.runtime}, but this host runs ${minRuntimeVersion} or newer. Rebuild it with a newer @monimesl/dot-runtime.`);
17
+ const minDashVersion = manifest.dash?.minVersion;
18
+ if (minDashVersion !== void 0 && !isVersionAtLeast(dashVersion, minDashVersion)) throw new IncompatibleDotError("dashTooOld", `This dot needs Dash ${minDashVersion} or newer, and this is Dash ${dashVersion}.`);
19
+ if (reactVersions !== void 0 && !reactVersions.some((version) => isReactCompatible(version, manifest.react))) throw new IncompatibleDotError("reactUnavailable", `This dot needs React ${manifest.react} or a newer ${majorVersion(manifest.react)}.x, which this host doesn't ship.`);
20
+ }
21
+ //#endregion
22
+ //#region src/host-calls.ts
23
+ const UI_METHOD_PREFIX = "dash.ui.";
24
+ var HostCallRunner = class {
25
+ #handlers;
26
+ #permissions;
27
+ #answer;
28
+ #running = /* @__PURE__ */ new Map();
29
+ constructor(handlers, permissions, answer) {
30
+ this.#handlers = handlers;
31
+ this.#permissions = permissions;
32
+ this.#answer = answer;
33
+ }
34
+ run({ id, method, params, timeout }) {
35
+ const entry = this.#handlers[method];
36
+ if (!entry) {
37
+ this.#answer({
38
+ id,
39
+ error: {
40
+ code: "unsupported",
41
+ message: `This host has no "${method}"`
42
+ }
43
+ });
44
+ return;
45
+ }
46
+ const call = {
47
+ method,
48
+ controller: new AbortController()
49
+ };
50
+ if (timeout !== void 0) call.timer = setTimeout(() => {
51
+ this.#fail(id, new HostError("timeout", `"${method}" took longer than ${timeout}ms`));
52
+ }, timeout);
53
+ this.#running.set(id, call);
54
+ this.#invoke(id, entry, {
55
+ method,
56
+ params,
57
+ signal: call.controller.signal
58
+ });
59
+ }
60
+ cancel(id) {
61
+ this.#end(id)?.controller.abort();
62
+ }
63
+ dismissUI() {
64
+ for (const [id, call] of this.#running) if (call.method.startsWith(UI_METHOD_PREFIX)) this.#fail(id, new HostError("cancelled", "The page it was shown on is no longer on top"));
65
+ }
66
+ async #invoke(id, entry, call) {
67
+ try {
68
+ if (typeof entry === "function") {
69
+ this.#succeed(id, await entry(call));
70
+ return;
71
+ }
72
+ await this.#permissions.check(call.method, entry.permission, call.signal);
73
+ this.#succeed(id, await entry.handle(call));
74
+ } catch (error) {
75
+ this.#fail(id, error);
76
+ }
77
+ }
78
+ #succeed(id, value) {
79
+ if (this.#end(id)) this.#answer({
80
+ id,
81
+ value
82
+ });
83
+ }
84
+ #fail(id, error) {
85
+ const call = this.#end(id);
86
+ if (!call) return;
87
+ call.controller.abort();
88
+ this.#answer({
89
+ id,
90
+ error: describeError(error)
91
+ });
92
+ }
93
+ #end(id) {
94
+ const call = this.#running.get(id);
95
+ if (call) {
96
+ this.#running.delete(id);
97
+ clearTimeout(call.timer);
98
+ }
99
+ return call;
100
+ }
101
+ };
102
+ function describeError(error) {
103
+ if (error instanceof HostError) return {
104
+ code: error.code,
105
+ message: error.message
106
+ };
107
+ return {
108
+ code: "failed",
109
+ message: error instanceof Error ? error.message : String(error)
110
+ };
111
+ }
112
+ //#endregion
113
+ //#region src/permissions.ts
114
+ var PermissionGate = class {
115
+ #dotName;
116
+ #declared;
117
+ #ask;
118
+ #answers = /* @__PURE__ */ new Map();
119
+ constructor(dotName, declared, ask) {
120
+ this.#dotName = dotName;
121
+ this.#declared = declared;
122
+ this.#ask = ask;
123
+ }
124
+ async check(method, permission, signal) {
125
+ const reason = this.#declared[permission];
126
+ if (reason === void 0) throw new HostError("denied", `"${method}" needs the "${permission}" permission. Add it to dot.json's permissions, with the reason Dash shows the person.`);
127
+ let allowed = this.#answers.get(permission);
128
+ if (allowed === void 0) {
129
+ allowed = await this.#ask({
130
+ dotName: this.#dotName,
131
+ permission,
132
+ reason
133
+ }, signal);
134
+ this.#answers.set(permission, allowed);
135
+ }
136
+ if (!allowed) throw new HostError("denied", `The person did not allow "${permission}"`);
137
+ }
138
+ };
139
+ //#endregion
140
+ //#region src/host.ts
141
+ function startHost({ logic, ui, manifest, dashVersion, minRuntimeVersion, systemContext, handlers = {}, askPermission, observe }) {
142
+ checkCompatibility(manifest, {
143
+ dashVersion,
144
+ minRuntimeVersion
145
+ });
146
+ const stack = new NavigationStack();
147
+ let context = systemContext;
148
+ const calls = new HostCallRunner(handlers, new PermissionGate(manifest.name, manifest.permissions ?? {}, askPermission), (result) => {
149
+ const message = {
150
+ type: "hostResult",
151
+ payload: result
152
+ };
153
+ observe?.("fromHost", message);
154
+ logic.post(message);
155
+ });
156
+ const broadcast = (message) => {
157
+ observe?.("fromHost", message);
158
+ ui.post(message);
159
+ logic.post(message);
160
+ };
161
+ const syncStack = () => {
162
+ const message = {
163
+ type: "syncStack",
164
+ payload: {
165
+ pages: stack.pages,
166
+ limit: stack.limit
167
+ }
168
+ };
169
+ broadcast(message);
170
+ };
171
+ const syncSystemContext = () => {
172
+ broadcast({
173
+ type: "syncSystemContext",
174
+ payload: context
175
+ });
176
+ };
177
+ const navigate = (navigation) => {
178
+ if (navigation.action !== "back" && !manifest.pages.includes(navigation.page)) return false;
179
+ const previousTop = stack.top;
180
+ if (!stack.apply(navigation)) return false;
181
+ syncStack();
182
+ if (stack.top !== previousTop) calls.dismissUI();
183
+ return true;
184
+ };
185
+ const navigateFromPage = ({ from, ...navigation }) => {
186
+ if (from === stack.top?.instanceId) navigate(navigation);
187
+ };
188
+ const hostMessageHandlers = /* @__PURE__ */ new Map([
189
+ ["navigate", (payload) => navigateFromPage(payload)],
190
+ ["hostCall", (payload) => calls.run(payload)],
191
+ ["hostCancel", (payload) => calls.cancel(payload.id)]
192
+ ]);
193
+ const stopService = logic.listen((raw) => {
194
+ const message = raw;
195
+ const handleHostMessage = hostMessageHandlers.get(message.type);
196
+ if (handleHostMessage) {
197
+ observe?.("toHost", message);
198
+ handleHostMessage(message.payload);
199
+ return;
200
+ }
201
+ observe?.("toWebview", message);
202
+ ui.post(message);
203
+ });
204
+ const stopWebview = ui.listen((raw) => {
205
+ const message = raw;
206
+ observe?.("toService", message);
207
+ logic.post(message);
208
+ if (message.type === "connect") {
209
+ syncSystemContext();
210
+ navigate({
211
+ action: "reset",
212
+ page: manifest.entry,
213
+ options: {}
214
+ });
215
+ }
216
+ });
217
+ return {
218
+ back: () => navigate({
219
+ action: "back",
220
+ delta: 1
221
+ }),
222
+ setSystemContext: (next) => {
223
+ context = next;
224
+ syncSystemContext();
225
+ },
226
+ stop: () => {
227
+ stopService();
228
+ stopWebview();
229
+ }
230
+ };
231
+ }
232
+ //#endregion
233
+ //#region src/versions.ts
234
+ const SIMULATED_DASH_VERSION = "1.0.0";
235
+ const MIN_RUNTIME_VERSION = "0.1.0-alpha.1";
236
+ const SIMULATOR_VERSIONS = {
237
+ dashVersion: SIMULATED_DASH_VERSION,
238
+ minRuntimeVersion: MIN_RUNTIME_VERSION
239
+ };
240
+ //#endregion
241
+ //#region src/paths.ts
242
+ const CLIENT_PATH = "/__dot/";
243
+ const EVENTS_PATH = `${CLIENT_PATH}events`;
244
+ const WEBVIEW_URL = `/dot/ui/index.html`;
245
+ //#endregion
246
+ //#region src/page.ts
247
+ const escapeHtml = (text) => text.replace(/[&<>"]/g, (char) => `&#${char.charCodeAt(0)};`);
248
+ function simulatorPage(appName) {
249
+ return `<!doctype html>
250
+ <html lang="en">
251
+ <head>
252
+ <meta charset="UTF-8" />
253
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
254
+ <title>${escapeHtml(appName)} · Dot simulator</title>
255
+ <style>${STYLES}</style>
256
+ </head>
257
+ <body>
258
+ <header>
259
+ <strong>${escapeHtml(appName)}</strong>
260
+ <span class="subtle">Dot simulator</span>
261
+ <select id="scheme" aria-label="Device appearance">
262
+ <option value="light">Light</option>
263
+ <option value="dark">Dark</option>
264
+ </select>
265
+ <button id="back" type="button" title="The system back button">◀ Back</button>
266
+ <button id="reload" type="button">Reload</button>
267
+ </header>
268
+ <main>
269
+ <section class="stage">
270
+ <pre id="error" hidden></pre>
271
+ <div class="phone" data-scheme="light">
272
+ <div class="status-bar">
273
+ <span class="clock">9:41</span>
274
+ <span class="island"></span>
275
+ <span class="indicators">${STATUS_ICONS}</span>
276
+ </div>
277
+ <iframe id="screen" title="${escapeHtml(appName)}"></iframe>
278
+ <span class="home-indicator"></span>
279
+ </div>
280
+ </section>
281
+ <aside>
282
+ <div class="panel-head">
283
+ <span>Messages</span>
284
+ <button id="clear" type="button">Clear</button>
285
+ </div>
286
+ <ol id="log"></ol>
287
+ </aside>
288
+ </main>
289
+ <script src="${CLIENT_PATH}simulator.js"><\/script>
290
+ </body>
291
+ </html>
292
+ `;
293
+ }
294
+ const STATUS_ICONS = `<svg class="torch" width="10" height="14" viewBox="0 0 10 14" aria-label="Torch on"><path d="M0 0h10v3L7.5 6v7a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1V6L0 3Z" fill="#ffd60a"/></svg><svg width="18" height="12" viewBox="0 0 18 12" fill="currentColor" aria-hidden="true"><rect x="0" y="8" width="3" height="4" rx="1"/><rect x="5" y="5.5" width="3" height="6.5" rx="1"/><rect x="10" y="3" width="3" height="9" rx="1"/><rect x="15" y="0" width="3" height="12" rx="1"/></svg><svg width="16" height="12" viewBox="0 0 16 12" fill="currentColor" aria-hidden="true"><path d="M8 2.2c2.3 0 4.4.9 6 2.4l1.2-1.3A10.4 10.4 0 0 0 8 .4 10.4 10.4 0 0 0 .8 3.3L2 4.6a8.6 8.6 0 0 1 6-2.4Zm0 3.6c1.3 0 2.5.5 3.5 1.3l1.2-1.3A6.9 6.9 0 0 0 8 4a6.9 6.9 0 0 0-4.7 1.8l1.2 1.3A5.1 5.1 0 0 1 8 5.8Zm0 3.6c-.5 0-1 .2-1.3.5L8 11.4l1.3-1.5c-.3-.3-.8-.5-1.3-.5Z"/></svg><svg width="27" height="13" viewBox="0 0 27 13" aria-hidden="true"><rect x="0.5" y="0.5" width="23" height="12" rx="3.5" fill="none" stroke="currentColor" opacity="0.4"/><rect x="2" y="2" width="20" height="9" rx="2" fill="currentColor"/><path d="M25 4.5v4c.8-.3 1.3-1.1 1.3-2s-.5-1.7-1.3-2Z" fill="currentColor" opacity="0.4"/></svg>`;
295
+ const STYLES = `
296
+ :root {
297
+ color-scheme: light dark;
298
+ --bg: #eef0f3; --panel: #ffffff; --border: #d6d9de; --text: #1f2328; --subtle: #6b7280;
299
+ --to-service: #2563eb; --to-webview: #059669; --danger: #dc2626;
300
+ font: 13px/1.4 system-ui, sans-serif;
301
+ }
302
+ @media (prefers-color-scheme: dark) {
303
+ :root {
304
+ --bg: #0d1117; --panel: #161b22; --border: #30363d; --text: #e6edf3; --subtle: #8b949e;
305
+ --to-service: #58a6ff; --to-webview: #3fb950; --danger: #f85149;
306
+ }
307
+ }
308
+ * { box-sizing: border-box; }
309
+ body { margin: 0; height: 100vh; display: flex; flex-direction: column; background: var(--bg); color: var(--text); }
310
+ header { display: flex; align-items: center; gap: 12px; padding: 8px 16px; background: var(--panel); border-bottom: 1px solid var(--border); }
311
+ #scheme { margin-left: auto; }
312
+ button, select { font: inherit; color: inherit; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; cursor: pointer; }
313
+ .subtle { color: var(--subtle); }
314
+ main { flex: 1; display: flex; min-height: 0; }
315
+ .stage { flex: 1; overflow: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 24px 16px; }
316
+ .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; }
317
+ .phone[data-scheme="dark"] { --screen: #121212; --screen-text: #fff; --dialog: #2c2c2e; --dialog-line: #48484a; }
318
+ .phone { --dialog: #f7f7f7; --dialog-line: #d1d1d6; }
319
+ .torch { display: none; }
320
+ .phone[data-torch="on"] .torch { display: block; }
321
+ .dialog-layer { position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center; background: rgb(0 0 0 / 0.3); }
322
+ .dialog { width: 270px; border-radius: 14px; overflow: hidden; background: var(--dialog); color: var(--screen-text); text-align: center; font: 13px/1.35 system-ui, sans-serif; }
323
+ .dialog-body { display: flex; flex-direction: column; gap: 4px; padding: 18px 16px 16px; }
324
+ .dialog-title { font-size: 17px; font-weight: 600; }
325
+ .dialog-actions { display: flex; border-top: 1px solid var(--dialog-line); }
326
+ .dialog-actions.stacked { flex-direction: column; }
327
+ .dialog-actions button { flex: 1; margin: 0; padding: 11px; border: 0; border-radius: 0; background: transparent; color: #0a84ff; font-size: 17px; }
328
+ .dialog-actions button + button { border-left: 1px solid var(--dialog-line); }
329
+ .dialog-actions.stacked button + button { border-left: 0; border-top: 1px solid var(--dialog-line); }
330
+ .dialog-actions .cancel { font-weight: 600; }
331
+ .dialog-actions .destructive { color: #ff3b30; }
332
+ .status-bar { flex: none; position: relative; height: 54px; display: flex; align-items: center; justify-content: space-between; padding: 0 30px 0 50px; color: var(--screen-text); font: 600 17px/1 system-ui, sans-serif; }
333
+ .island { position: absolute; top: 11px; left: 50%; width: 125px; height: 37px; margin-left: -62.5px; border-radius: 19px; background: #000; }
334
+ .indicators { display: flex; align-items: center; gap: 6px; }
335
+ #screen { flex: 1; display: block; width: 100%; border: 0; background: var(--screen); }
336
+ .home-indicator { position: absolute; bottom: 8px; left: 50%; width: 134px; height: 5px; margin-left: -67px; border-radius: 3px; background: var(--screen-text); pointer-events: none; }
337
+ #error { max-width: 640px; width: 100%; margin: 0; padding: 12px; white-space: pre-wrap; overflow-wrap: anywhere; color: #fff; background: var(--danger); border-radius: 8px; }
338
+ aside { width: 380px; display: flex; flex-direction: column; background: var(--panel); border-left: 1px solid var(--border); }
339
+ .panel-head { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--border); font-weight: 600; }
340
+ #log { flex: 1; overflow: auto; margin: 0; padding: 0; list-style: none; font: 12px/1.4 ui-monospace, monospace; }
341
+ #log li { border-bottom: 1px solid var(--border); padding: 4px 12px; }
342
+ #log summary { display: flex; gap: 8px; cursor: pointer; }
343
+ #log pre { margin: 4px 0; white-space: pre-wrap; word-break: break-all; color: var(--subtle); }
344
+ #log .time { color: var(--subtle); }
345
+ #log .toService .arrow { color: var(--to-service); }
346
+ #log .toWebview .arrow { color: var(--to-webview); }
347
+ #log .toHost .arrow, #log .fromHost .arrow { color: var(--subtle); }
348
+ #log .type { font-weight: 600; }
349
+ #log .note { padding: 2px 12px; text-align: center; color: var(--subtle); background: var(--bg); }
350
+ #log .failed .type { color: var(--danger); }
351
+ @media (max-width: 900px) { main { flex-direction: column; } aside { width: auto; height: 40vh; border-left: 0; border-top: 1px solid var(--border); } }
352
+ `;
353
+ //#endregion
354
+ //#region src/server.ts
355
+ const DEFAULT_PORT = 3680;
356
+ const PORT_ATTEMPTS = 5;
357
+ const MIME_TYPES = {
358
+ ".html": "text/html; charset=utf-8",
359
+ ".js": "text/javascript; charset=utf-8",
360
+ ".map": "application/json; charset=utf-8",
361
+ ".json": "application/json; charset=utf-8",
362
+ ".css": "text/css; charset=utf-8",
363
+ ".svg": "image/svg+xml",
364
+ ".png": "image/png",
365
+ ".jpg": "image/jpeg",
366
+ ".jpeg": "image/jpeg",
367
+ ".gif": "image/gif",
368
+ ".webp": "image/webp",
369
+ ".woff2": "font/woff2"
370
+ };
371
+ async function startSimulator({ dotDir, reactHostDir, name = "Dot", port = DEFAULT_PORT, host = "localhost", clientDir = defaultClientDir() }) {
372
+ const clients = /* @__PURE__ */ new Set();
373
+ let buildError = null;
374
+ const broadcast = (event, data = "") => {
375
+ for (const client of clients) client.write(serverEvent(event, data));
376
+ };
377
+ const server = createServer((request, response) => {
378
+ const { pathname } = new URL(request.url ?? "/", "http://localhost");
379
+ if (pathname === "/") {
380
+ send(response, 200, simulatorPage(name), ".html");
381
+ return;
382
+ }
383
+ if (pathname === "/favicon.ico") {
384
+ send(response, 204, "");
385
+ return;
386
+ }
387
+ if (pathname === EVENTS_PATH) {
388
+ response.writeHead(200, {
389
+ "Content-Type": "text/event-stream",
390
+ "Cache-Control": "no-store",
391
+ Connection: "keep-alive"
392
+ });
393
+ response.write(": connected\n\n");
394
+ if (buildError !== null) response.write(serverEvent("build-error", buildError));
395
+ clients.add(response);
396
+ request.on("close", () => clients.delete(response));
397
+ return;
398
+ }
399
+ if (pathname.startsWith(REACT_HOST_PATH) && reactHostDir !== void 0) {
400
+ serveReact(response, reactHostDir, pathname.slice(REACT_HOST_PATH.length));
401
+ return;
402
+ }
403
+ if (pathname.startsWith("/__dot/")) {
404
+ serveFile(response, clientDir, pathname.slice(7));
405
+ return;
406
+ }
407
+ if (pathname.startsWith("/dot/")) {
408
+ const injectBridge = pathname === WEBVIEW_URL || `${pathname}index.html` === WEBVIEW_URL;
409
+ serveFile(response, dotDir, pathname.slice(5), injectBridge);
410
+ return;
411
+ }
412
+ send(response, 404, "Not found");
413
+ });
414
+ await listen(server, port, host);
415
+ return {
416
+ url: `http://${host}:${server.address().port}/`,
417
+ reload: () => {
418
+ buildError = null;
419
+ broadcast("reload");
420
+ },
421
+ error: (message) => {
422
+ buildError = message;
423
+ broadcast("build-error", message);
424
+ },
425
+ close: () => new Promise((resolvePromise) => {
426
+ for (const client of clients) client.end();
427
+ server.close(() => resolvePromise());
428
+ })
429
+ };
430
+ }
431
+ function serverEvent(event, data) {
432
+ return `event: ${event}\n${data.split("\n").map((line) => `data: ${line}`).join("\n")}\n\n`;
433
+ }
434
+ async function listen(server, port, host) {
435
+ for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt++) if (!await new Promise((resolvePromise, reject) => {
436
+ const onError = (error) => {
437
+ if (error.code === "EADDRINUSE") resolvePromise(true);
438
+ else reject(error);
439
+ };
440
+ server.once("error", onError);
441
+ server.listen(port + attempt, host, () => {
442
+ server.off("error", onError);
443
+ resolvePromise(false);
444
+ });
445
+ })) return;
446
+ const last = port + PORT_ATTEMPTS - 1;
447
+ throw new Error(`Ports ${port} to ${last} are all in use. Pass another with --port.`);
448
+ }
449
+ async function serveReact(response, reactHostDir, path) {
450
+ const [major = "", ...file] = path.split("/");
451
+ const versions = await readdir(reactHostDir).catch(() => []);
452
+ const version = newestReactOfMajor(versions, Number(major));
453
+ if (version === void 0) {
454
+ send(response, 404, "Not found");
455
+ return;
456
+ }
457
+ await serveFile(response, join(reactHostDir, version), file.join("/"));
458
+ }
459
+ async function serveFile(response, root, path, injectBridge = false) {
460
+ const base = resolve(root);
461
+ let file = resolve(base, decodeURIComponent(path));
462
+ if (file !== base && !file.startsWith(base + sep)) {
463
+ send(response, 404, "Not found");
464
+ return;
465
+ }
466
+ try {
467
+ if ((await stat(file)).isDirectory()) file = join(file, "index.html");
468
+ const body = await readFile(file);
469
+ if (!injectBridge) {
470
+ send(response, 200, body, extname(file));
471
+ return;
472
+ }
473
+ const bridge = `<script src="${CLIENT_PATH}webview-bridge.js"><\/script>`;
474
+ send(response, 200, body.toString("utf8").replace("<head>", `<head>\n ${bridge}`), ".html");
475
+ } catch {
476
+ send(response, 404, "Not found");
477
+ }
478
+ }
479
+ function send(response, status, body, extension = ".txt") {
480
+ response.writeHead(status, {
481
+ "Content-Type": MIME_TYPES[extension] ?? "text/plain; charset=utf-8",
482
+ "Cache-Control": "no-store"
483
+ });
484
+ response.end(body);
485
+ }
486
+ function defaultClientDir() {
487
+ const manifest = createRequire(import.meta.url).resolve("@monimesl/dot-simulator/package.json");
488
+ return join(dirname(manifest), "dist/client");
489
+ }
490
+ //#endregion
491
+ export { DEFAULT_PORT, IncompatibleDotError, MIN_RUNTIME_VERSION, SIMULATED_DASH_VERSION, SIMULATOR_VERSIONS, checkCompatibility, startHost, startSimulator };
492
+
493
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/compatibility.ts","../../src/host-calls.ts","../../src/permissions.ts","../../src/host.ts","../../src/versions.ts","../../src/paths.ts","../../src/page.ts","../../src/server.ts"],"sourcesContent":["import { isReactCompatible, isVersionAtLeast, majorVersion } from '@monimesl/dot-core';\n\nimport type { HostManifest } from './manifest';\n\nexport type IncompatibilityReason = 'runtimeTooOld' | 'dashTooOld' | 'reactUnavailable';\n\n// The two versions a host knows about itself.\nexport interface HostVersions {\n // The Dash version this host is, checked against the dot's `dash.minVersion`.\n readonly dashVersion: string;\n // The oldest @monimesl/dot-runtime this host runs. Dots built with an older one must be rebuilt.\n readonly minRuntimeVersion: string;\n // The React versions the host ships. Left out by a host that serves the React the dot was built\n // with, as the simulator does.\n readonly reactVersions?: readonly string[] | undefined;\n}\n\n// Why a host refuses to open a dot: it was built with a runtime older than the host runs, it needs\n// a newer Dash than the host is, or it imports a React the host doesn't ship.\nexport class IncompatibleDotError extends Error {\n override readonly name = 'IncompatibleDotError';\n readonly reason: IncompatibilityReason;\n\n constructor(reason: IncompatibilityReason, message: string) {\n super(message);\n this.reason = reason;\n }\n}\n\n// Throws an IncompatibleDotError when a host with these versions cannot run the dot.\nexport function checkCompatibility(\n manifest: HostManifest,\n { dashVersion, minRuntimeVersion, reactVersions }: HostVersions,\n): void {\n if (!isVersionAtLeast(manifest.runtime, minRuntimeVersion)) {\n throw new IncompatibleDotError(\n 'runtimeTooOld',\n `This dot was built with Dot runtime ${manifest.runtime}, but this host runs ${minRuntimeVersion} or newer. ` +\n 'Rebuild it with a newer @monimesl/dot-runtime.',\n );\n }\n const minDashVersion = manifest.dash?.minVersion;\n if (minDashVersion !== undefined && !isVersionAtLeast(dashVersion, minDashVersion)) {\n throw new IncompatibleDotError(\n 'dashTooOld',\n `This dot needs Dash ${minDashVersion} or newer, and this is Dash ${dashVersion}.`,\n );\n }\n if (\n reactVersions !== undefined &&\n !reactVersions.some((version) => isReactCompatible(version, manifest.react))\n ) {\n throw new IncompatibleDotError(\n 'reactUnavailable',\n `This dot needs React ${manifest.react} or a newer ${majorVersion(manifest.react)}.x, which this host doesn't ship.`,\n );\n }\n}\n","import type {\n DotPermission,\n HostErrorCode,\n HostResult,\n LogicLayerMessages,\n} from '@monimesl/dot-core';\nimport { HostError } from '@monimesl/dot-core';\n\nimport type { PermissionGate } from './permissions';\n\nexport interface HostCall {\n readonly method: string;\n readonly params: unknown;\n // Aborted when the call ends early: cancelled, out of time, or dismissed with its page.\n readonly signal: AbortSignal;\n}\n\n// Runs one host method. Throw a `HostError` to fail with its code; anything else fails as `failed`.\nexport type HostHandler = (call: HostCall) => unknown;\n\n// A method that needs a permission: it runs only if the dot declared it and the person allowed it.\nexport interface PermissionedHandler {\n readonly permission: DotPermission;\n readonly handle: HostHandler;\n}\n\nexport type HostHandlers = Readonly<Record<string, HostHandler | PermissionedHandler>>;\n\nconst UI_METHOD_PREFIX = 'dash.ui.';\n\ninterface RunningCall {\n readonly method: string;\n readonly controller: AbortController;\n timer?: ReturnType<typeof setTimeout>;\n}\n\n// Runs the service's host calls with `handlers`, and answers each call exactly once.\nexport class HostCallRunner {\n readonly #handlers: HostHandlers;\n readonly #permissions: PermissionGate;\n readonly #answer: (result: HostResult) => void;\n readonly #running = new Map<number, RunningCall>();\n\n constructor(\n handlers: HostHandlers,\n permissions: PermissionGate,\n answer: (result: HostResult) => void,\n ) {\n this.#handlers = handlers;\n this.#permissions = permissions;\n this.#answer = answer;\n }\n\n run({ id, method, params, timeout }: LogicLayerMessages['hostCall']): void {\n const entry = this.#handlers[method];\n if (!entry) {\n this.#answer({ id, error: { code: 'unsupported', message: `This host has no \"${method}\"` } });\n return;\n }\n const call: RunningCall = { method, controller: new AbortController() };\n if (timeout !== undefined) {\n call.timer = setTimeout(() => {\n this.#fail(id, new HostError('timeout', `\"${method}\" took longer than ${timeout}ms`));\n }, timeout);\n }\n this.#running.set(id, call);\n void this.#invoke(id, entry, { method, params, signal: call.controller.signal });\n }\n\n // The service stopped waiting, so the call ends without an answer.\n cancel(id: number): void {\n this.#end(id)?.controller.abort();\n }\n\n // Dialogs belong to the page on top: when it changes, the ones still open are dismissed.\n dismissUI(): void {\n for (const [id, call] of this.#running) {\n if (call.method.startsWith(UI_METHOD_PREFIX)) {\n this.#fail(id, new HostError('cancelled', 'The page it was shown on is no longer on top'));\n }\n }\n }\n\n async #invoke(\n id: number,\n entry: HostHandler | PermissionedHandler,\n call: HostCall,\n ): Promise<void> {\n try {\n if (typeof entry === 'function') {\n this.#succeed(id, await entry(call));\n return;\n }\n await this.#permissions.check(call.method, entry.permission, call.signal);\n this.#succeed(id, await entry.handle(call));\n } catch (error) {\n this.#fail(id, error);\n }\n }\n\n #succeed(id: number, value: unknown): void {\n if (this.#end(id)) {\n this.#answer({ id, value });\n }\n }\n\n #fail(id: number, error: unknown): void {\n const call = this.#end(id);\n if (!call) {\n return;\n }\n call.controller.abort();\n this.#answer({ id, error: describeError(error) });\n }\n\n #end(id: number): RunningCall | undefined {\n const call = this.#running.get(id);\n if (call) {\n this.#running.delete(id);\n clearTimeout(call.timer);\n }\n return call;\n }\n}\n\nfunction describeError(error: unknown): { code: HostErrorCode; message: string } {\n if (error instanceof HostError) {\n return { code: error.code, message: error.message };\n }\n return { code: 'failed', message: error instanceof Error ? error.message : String(error) };\n}\n","import type { DotPermission } from '@monimesl/dot-core';\nimport { HostError } from '@monimesl/dot-core';\n\n// A dot asking for a permission it declared, the first time a call needs it.\nexport interface PermissionRequest {\n readonly dotName: string;\n readonly permission: DotPermission;\n // Why the dot needs it, from its dot.json.\n readonly reason: string;\n}\n\n// Each permission a dot declared in its dot.json, with the reason Dash shows the person.\nexport type DeclaredPermissions = Readonly<Partial<Record<string, string>>>;\n\n// Asks the person, and resolves with whether they allowed it. `signal` aborts when the call that\n// needs the permission ends first; the host then takes its prompt down and rejects.\nexport type AskPermission = (request: PermissionRequest, signal: AbortSignal) => Promise<boolean>;\n\n// Lets a call that needs a permission through only if the dot declared it and the person allowed\n// it. The person is asked once per run; the answer, yes or no, is kept.\nexport class PermissionGate {\n readonly #dotName: string;\n readonly #declared: DeclaredPermissions;\n readonly #ask: AskPermission;\n readonly #answers = new Map<DotPermission, boolean>();\n\n constructor(dotName: string, declared: DeclaredPermissions, ask: AskPermission) {\n this.#dotName = dotName;\n this.#declared = declared;\n this.#ask = ask;\n }\n\n // Rejects with a `denied` HostError when the call may not run.\n async check(method: string, permission: DotPermission, signal: AbortSignal): Promise<void> {\n const reason = this.#declared[permission];\n if (reason === undefined) {\n throw new HostError(\n 'denied',\n `\"${method}\" needs the \"${permission}\" permission. Add it to dot.json's permissions, with the reason Dash shows the person.`,\n );\n }\n let allowed = this.#answers.get(permission);\n if (allowed === undefined) {\n allowed = await this.#ask({ dotName: this.#dotName, permission, reason }, signal);\n this.#answers.set(permission, allowed);\n }\n if (!allowed) {\n throw new HostError('denied', `The person did not allow \"${permission}\"`);\n }\n }\n}\n","import type {\n Envelope,\n HostLayerMessages,\n HostResult,\n LogicLayerMessages,\n Navigation,\n SystemContext,\n Transport,\n} from '@monimesl/dot-core';\nimport { NavigationStack } from '@monimesl/dot-core';\n\nimport type { HostVersions } from './compatibility';\nimport { checkCompatibility } from './compatibility';\nimport type { HostHandlers } from './host-calls';\nimport { HostCallRunner } from './host-calls';\nimport type { HostManifest } from './manifest';\nimport type { AskPermission } from './permissions';\nimport { PermissionGate } from './permissions';\n\nexport type Direction = 'toService' | 'toWebview' | 'toHost' | 'fromHost';\n\nexport interface HostOptions extends HostVersions {\n ui: Transport;\n logic: Transport;\n manifest: HostManifest;\n systemContext: SystemContext;\n // The methods dots may call, such as `dash.ui.alert`; any other is answered `unsupported`.\n handlers?: HostHandlers;\n // Asks the person for a permission the dot declared, the first time a method needs it.\n askPermission: AskPermission;\n observe?: (direction: Direction, message: Envelope) => void;\n}\n\nexport interface Host {\n stop(): void;\n // false on the first page, where a device closes the dot instead.\n back(): boolean;\n setSystemContext(context: SystemContext): void;\n}\n\n// The host contract in JavaScript; native hosts implement the same in Swift or Kotlin.\n// Throws an IncompatibleDotError, before either layer hears anything, if it cannot run the dot.\nexport function startHost({\n logic,\n ui,\n manifest,\n dashVersion,\n minRuntimeVersion,\n systemContext,\n handlers = {},\n askPermission,\n observe,\n}: HostOptions): Host {\n checkCompatibility(manifest, { dashVersion, minRuntimeVersion });\n const stack = new NavigationStack();\n let context = systemContext;\n const permissions = new PermissionGate(manifest.name, manifest.permissions ?? {}, askPermission);\n const calls = new HostCallRunner(handlers, permissions, (result) => {\n const message: Envelope<'hostResult', HostResult> = { type: 'hostResult', payload: result };\n observe?.('fromHost', message);\n logic.post(message);\n });\n const broadcast = (message: Envelope) => {\n observe?.('fromHost', message);\n ui.post(message);\n logic.post(message);\n };\n const syncStack = () => {\n const message: Envelope<'syncStack', HostLayerMessages['syncStack']> = {\n type: 'syncStack',\n payload: { pages: stack.pages, limit: stack.limit },\n };\n broadcast(message);\n };\n const syncSystemContext = () => {\n const message: Envelope<'syncSystemContext', HostLayerMessages['syncSystemContext']> = {\n type: 'syncSystemContext',\n payload: context,\n };\n broadcast(message);\n };\n const navigate = (navigation: Navigation): boolean => {\n if (navigation.action !== 'back' && !manifest.pages.includes(navigation.page)) {\n return false;\n }\n const previousTop = stack.top;\n if (!stack.apply(navigation)) {\n return false;\n }\n syncStack();\n if (stack.top !== previousTop) {\n calls.dismissUI();\n }\n return true;\n };\n\n // Only the page on top may navigate; a request from a page below it is dropped.\n const navigateFromPage = ({ from, ...navigation }: LogicLayerMessages['navigate']) => {\n if (from === stack.top?.instanceId) {\n navigate(navigation);\n }\n };\n // Service messages meant for the host itself; every other one is relayed to the webview.\n const hostMessageHandlers = new Map<string, (payload: unknown) => void>([\n ['navigate', (payload) => navigateFromPage(payload as LogicLayerMessages['navigate'])],\n ['hostCall', (payload) => calls.run(payload as LogicLayerMessages['hostCall'])],\n ['hostCancel', (payload) => calls.cancel((payload as LogicLayerMessages['hostCancel']).id)],\n ]);\n\n const stopService = logic.listen((raw) => {\n const message = raw as Envelope;\n const handleHostMessage = hostMessageHandlers.get(message.type);\n if (handleHostMessage) {\n observe?.('toHost', message);\n handleHostMessage(message.payload);\n return;\n }\n observe?.('toWebview', message);\n ui.post(message);\n });\n const stopWebview = ui.listen((raw) => {\n const message = raw as Envelope;\n observe?.('toService', message);\n logic.post(message);\n if (message.type === 'connect') {\n syncSystemContext();\n navigate({ action: 'reset', page: manifest.entry, options: {} });\n }\n });\n return {\n back: () => navigate({ action: 'back', delta: 1 }),\n setSystemContext: (next) => {\n context = next;\n syncSystemContext();\n },\n stop: () => {\n stopService();\n stopWebview();\n },\n };\n}\n","import type { HostVersions } from './compatibility';\n\n// The Dash version the simulator acts as, for dots that set `dash.minVersion`.\nexport const SIMULATED_DASH_VERSION = '1.0.0';\n\n// The oldest @monimesl/dot-runtime whose bundles the simulator's bridge runs. Raise it when the\n// messages between the layers and the host change in a way older runtimes don't understand.\nexport const MIN_RUNTIME_VERSION = '0.1.0-alpha.1';\n\nexport const SIMULATOR_VERSIONS: HostVersions = {\n dashVersion: SIMULATED_DASH_VERSION,\n minRuntimeVersion: MIN_RUNTIME_VERSION,\n};\n","export const CLIENT_PATH = '/__dot/';\nexport const EVENTS_PATH = `${CLIENT_PATH}events`;\n\nexport const DOT_PATH = '/dot/';\nexport const WEBVIEW_URL = `${DOT_PATH}ui/index.html`;\nexport const SERVICE_URL = `${DOT_PATH}logic/dot.js`;\nexport const MANIFEST_URL = `${DOT_PATH}manifest.json`;\n","import { CLIENT_PATH } from './paths';\n\nconst escapeHtml = (text: string) => text.replace(/[&<>\"]/g, (char) => `&#${char.charCodeAt(0)};`);\n\nexport function simulatorPage(appName: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${escapeHtml(appName)} · Dot simulator</title>\n <style>${STYLES}</style>\n </head>\n <body>\n <header>\n <strong>${escapeHtml(appName)}</strong>\n <span class=\"subtle\">Dot simulator</span>\n <select id=\"scheme\" aria-label=\"Device appearance\">\n <option value=\"light\">Light</option>\n <option value=\"dark\">Dark</option>\n </select>\n <button id=\"back\" type=\"button\" title=\"The system back button\">◀ Back</button>\n <button id=\"reload\" type=\"button\">Reload</button>\n </header>\n <main>\n <section class=\"stage\">\n <pre id=\"error\" hidden></pre>\n <div class=\"phone\" data-scheme=\"light\">\n <div class=\"status-bar\">\n <span class=\"clock\">9:41</span>\n <span class=\"island\"></span>\n <span class=\"indicators\">${STATUS_ICONS}</span>\n </div>\n <iframe id=\"screen\" title=\"${escapeHtml(appName)}\"></iframe>\n <span class=\"home-indicator\"></span>\n </div>\n </section>\n <aside>\n <div class=\"panel-head\">\n <span>Messages</span>\n <button id=\"clear\" type=\"button\">Clear</button>\n </div>\n <ol id=\"log\"></ol>\n </aside>\n </main>\n <script src=\"${CLIENT_PATH}simulator.js\"></script>\n </body>\n</html>\n`;\n}\n\nconst TORCH_ICON = `<svg class=\"torch\" width=\"10\" height=\"14\" viewBox=\"0 0 10 14\" aria-label=\"Torch on\"><path d=\"M0 0h10v3L7.5 6v7a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1V6L0 3Z\" fill=\"#ffd60a\"/></svg>`;\n\nconst STATUS_ICONS = `${TORCH_ICON}<svg width=\"18\" height=\"12\" viewBox=\"0 0 18 12\" fill=\"currentColor\" aria-hidden=\"true\"><rect x=\"0\" y=\"8\" width=\"3\" height=\"4\" rx=\"1\"/><rect x=\"5\" y=\"5.5\" width=\"3\" height=\"6.5\" rx=\"1\"/><rect x=\"10\" y=\"3\" width=\"3\" height=\"9\" rx=\"1\"/><rect x=\"15\" y=\"0\" width=\"3\" height=\"12\" rx=\"1\"/></svg><svg width=\"16\" height=\"12\" viewBox=\"0 0 16 12\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M8 2.2c2.3 0 4.4.9 6 2.4l1.2-1.3A10.4 10.4 0 0 0 8 .4 10.4 10.4 0 0 0 .8 3.3L2 4.6a8.6 8.6 0 0 1 6-2.4Zm0 3.6c1.3 0 2.5.5 3.5 1.3l1.2-1.3A6.9 6.9 0 0 0 8 4a6.9 6.9 0 0 0-4.7 1.8l1.2 1.3A5.1 5.1 0 0 1 8 5.8Zm0 3.6c-.5 0-1 .2-1.3.5L8 11.4l1.3-1.5c-.3-.3-.8-.5-1.3-.5Z\"/></svg><svg width=\"27\" height=\"13\" viewBox=\"0 0 27 13\" aria-hidden=\"true\"><rect x=\"0.5\" y=\"0.5\" width=\"23\" height=\"12\" rx=\"3.5\" fill=\"none\" stroke=\"currentColor\" opacity=\"0.4\"/><rect x=\"2\" y=\"2\" width=\"20\" height=\"9\" rx=\"2\" fill=\"currentColor\"/><path d=\"M25 4.5v4c.8-.3 1.3-1.1 1.3-2s-.5-1.7-1.3-2Z\" fill=\"currentColor\" opacity=\"0.4\"/></svg>`;\n\nconst STYLES = `\n :root {\n color-scheme: light dark;\n --bg: #eef0f3; --panel: #ffffff; --border: #d6d9de; --text: #1f2328; --subtle: #6b7280;\n --to-service: #2563eb; --to-webview: #059669; --danger: #dc2626;\n font: 13px/1.4 system-ui, sans-serif;\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --bg: #0d1117; --panel: #161b22; --border: #30363d; --text: #e6edf3; --subtle: #8b949e;\n --to-service: #58a6ff; --to-webview: #3fb950; --danger: #f85149;\n }\n }\n * { box-sizing: border-box; }\n body { margin: 0; height: 100vh; display: flex; flex-direction: column; background: var(--bg); color: var(--text); }\n header { display: flex; align-items: center; gap: 12px; padding: 8px 16px; background: var(--panel); border-bottom: 1px solid var(--border); }\n #scheme { margin-left: auto; }\n button, select { font: inherit; color: inherit; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; cursor: pointer; }\n .subtle { color: var(--subtle); }\n main { flex: 1; display: flex; min-height: 0; }\n .stage { flex: 1; overflow: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 24px 16px; }\n .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; }\n .phone[data-scheme=\"dark\"] { --screen: #121212; --screen-text: #fff; --dialog: #2c2c2e; --dialog-line: #48484a; }\n .phone { --dialog: #f7f7f7; --dialog-line: #d1d1d6; }\n .torch { display: none; }\n .phone[data-torch=\"on\"] .torch { display: block; }\n .dialog-layer { position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center; background: rgb(0 0 0 / 0.3); }\n .dialog { width: 270px; border-radius: 14px; overflow: hidden; background: var(--dialog); color: var(--screen-text); text-align: center; font: 13px/1.35 system-ui, sans-serif; }\n .dialog-body { display: flex; flex-direction: column; gap: 4px; padding: 18px 16px 16px; }\n .dialog-title { font-size: 17px; font-weight: 600; }\n .dialog-actions { display: flex; border-top: 1px solid var(--dialog-line); }\n .dialog-actions.stacked { flex-direction: column; }\n .dialog-actions button { flex: 1; margin: 0; padding: 11px; border: 0; border-radius: 0; background: transparent; color: #0a84ff; font-size: 17px; }\n .dialog-actions button + button { border-left: 1px solid var(--dialog-line); }\n .dialog-actions.stacked button + button { border-left: 0; border-top: 1px solid var(--dialog-line); }\n .dialog-actions .cancel { font-weight: 600; }\n .dialog-actions .destructive { color: #ff3b30; }\n .status-bar { flex: none; position: relative; height: 54px; display: flex; align-items: center; justify-content: space-between; padding: 0 30px 0 50px; color: var(--screen-text); font: 600 17px/1 system-ui, sans-serif; }\n .island { position: absolute; top: 11px; left: 50%; width: 125px; height: 37px; margin-left: -62.5px; border-radius: 19px; background: #000; }\n .indicators { display: flex; align-items: center; gap: 6px; }\n #screen { flex: 1; display: block; width: 100%; border: 0; background: var(--screen); }\n .home-indicator { position: absolute; bottom: 8px; left: 50%; width: 134px; height: 5px; margin-left: -67px; border-radius: 3px; background: var(--screen-text); pointer-events: none; }\n #error { max-width: 640px; width: 100%; margin: 0; padding: 12px; white-space: pre-wrap; overflow-wrap: anywhere; color: #fff; background: var(--danger); border-radius: 8px; }\n aside { width: 380px; display: flex; flex-direction: column; background: var(--panel); border-left: 1px solid var(--border); }\n .panel-head { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--border); font-weight: 600; }\n #log { flex: 1; overflow: auto; margin: 0; padding: 0; list-style: none; font: 12px/1.4 ui-monospace, monospace; }\n #log li { border-bottom: 1px solid var(--border); padding: 4px 12px; }\n #log summary { display: flex; gap: 8px; cursor: pointer; }\n #log pre { margin: 4px 0; white-space: pre-wrap; word-break: break-all; color: var(--subtle); }\n #log .time { color: var(--subtle); }\n #log .toService .arrow { color: var(--to-service); }\n #log .toWebview .arrow { color: var(--to-webview); }\n #log .toHost .arrow, #log .fromHost .arrow { color: var(--subtle); }\n #log .type { font-weight: 600; }\n #log .note { padding: 2px 12px; text-align: center; color: var(--subtle); background: var(--bg); }\n #log .failed .type { color: var(--danger); }\n @media (max-width: 900px) { main { flex-direction: column; } aside { width: auto; height: 40vh; border-left: 0; border-top: 1px solid var(--border); } }\n`;\n","import { readdir, readFile, stat } from 'node:fs/promises';\nimport type { Server, ServerResponse } from 'node:http';\nimport { createServer } from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport { createRequire } from 'node:module';\nimport { dirname, extname, join, resolve, sep } from 'node:path';\n\nimport { REACT_HOST_PATH, newestReactOfMajor } from '@monimesl/dot-core';\n\nimport { simulatorPage } from './page';\nimport { DOT_PATH, CLIENT_PATH, EVENTS_PATH, WEBVIEW_URL } from './paths';\n\nexport interface SimulatorOptions {\n dotDir: string;\n // The React files hosts serve, a folder per version, from the dot's renderer. Without it, a dot\n // cannot load React.\n reactHostDir?: string | undefined;\n name?: string;\n port?: number | undefined;\n host?: string;\n clientDir?: string;\n}\n\nexport interface Simulator {\n readonly url: string;\n reload(): void;\n error(message: string): void;\n close(): Promise<void>;\n}\n\nexport const DEFAULT_PORT = 3680;\n\nconst PORT_ATTEMPTS = 5;\n\nconst MIME_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.woff2': 'font/woff2',\n};\n\nexport async function startSimulator({\n dotDir,\n reactHostDir,\n name = 'Dot',\n port = DEFAULT_PORT,\n host = 'localhost',\n clientDir = defaultClientDir(),\n}: SimulatorOptions): Promise<Simulator> {\n const clients = new Set<ServerResponse>();\n let buildError: string | null = null;\n const broadcast = (event: string, data = '') => {\n for (const client of clients) {\n client.write(serverEvent(event, data));\n }\n };\n\n const server = createServer((request, response) => {\n const { pathname } = new URL(request.url ?? '/', 'http://localhost');\n if (pathname === '/') {\n send(response, 200, simulatorPage(name), '.html');\n return;\n }\n if (pathname === '/favicon.ico') {\n send(response, 204, '');\n return;\n }\n if (pathname === EVENTS_PATH) {\n response.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-store',\n Connection: 'keep-alive',\n });\n response.write(': connected\\n\\n');\n if (buildError !== null) {\n response.write(serverEvent('build-error', buildError));\n }\n clients.add(response);\n request.on('close', () => clients.delete(response));\n return;\n }\n // As a native host serves the React it ships.\n if (pathname.startsWith(REACT_HOST_PATH) && reactHostDir !== undefined) {\n void serveReact(response, reactHostDir, pathname.slice(REACT_HOST_PATH.length));\n return;\n }\n if (pathname.startsWith(CLIENT_PATH)) {\n void serveFile(response, clientDir, pathname.slice(CLIENT_PATH.length));\n return;\n }\n if (pathname.startsWith(DOT_PATH)) {\n // As a native host injects its bridge into the WebView before the dot runs.\n const injectBridge = pathname === WEBVIEW_URL || `${pathname}index.html` === WEBVIEW_URL;\n void serveFile(response, dotDir, pathname.slice(DOT_PATH.length), injectBridge);\n return;\n }\n send(response, 404, 'Not found');\n });\n\n await listen(server, port, host);\n\n return {\n url: `http://${host}:${(server.address() as AddressInfo).port}/`,\n reload: () => {\n buildError = null;\n broadcast('reload');\n },\n error: (message) => {\n buildError = message;\n broadcast('build-error', message);\n },\n close: () =>\n new Promise((resolvePromise) => {\n for (const client of clients) {\n client.end();\n }\n server.close(() => resolvePromise());\n }),\n };\n}\n\nfunction serverEvent(event: string, data: string): string {\n const lines = data.split('\\n').map((line) => `data: ${line}`);\n return `event: ${event}\\n${lines.join('\\n')}\\n\\n`;\n}\n\nasync function listen(server: Server, port: number, host: string): Promise<void> {\n for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt++) {\n const taken = await new Promise<boolean>((resolvePromise, reject) => {\n const onError = (error: NodeJS.ErrnoException) => {\n if (error.code === 'EADDRINUSE') {\n resolvePromise(true);\n } else {\n reject(error);\n }\n };\n server.once('error', onError);\n server.listen(port + attempt, host, () => {\n server.off('error', onError);\n resolvePromise(false);\n });\n });\n if (!taken) {\n return;\n }\n }\n const last = port + PORT_ATTEMPTS - 1;\n throw new Error(`Ports ${port} to ${last} are all in use. Pass another with --port.`);\n}\n\n// `<major>/<file>`, from the newest version of that major in `reactHostDir`.\nasync function serveReact(\n response: ServerResponse,\n reactHostDir: string,\n path: string,\n): Promise<void> {\n const [major = '', ...file] = path.split('/');\n const versions = await readdir(reactHostDir).catch(() => []);\n const version = newestReactOfMajor(versions, Number(major));\n if (version === undefined) {\n send(response, 404, 'Not found');\n return;\n }\n await serveFile(response, join(reactHostDir, version), file.join('/'));\n}\n\nasync function serveFile(\n response: ServerResponse,\n root: string,\n path: string,\n injectBridge = false,\n): Promise<void> {\n const base = resolve(root);\n let file = resolve(base, decodeURIComponent(path));\n if (file !== base && !file.startsWith(base + sep)) {\n send(response, 404, 'Not found');\n return;\n }\n try {\n if ((await stat(file)).isDirectory()) {\n file = join(file, 'index.html');\n }\n const body = await readFile(file);\n if (!injectBridge) {\n send(response, 200, body, extname(file));\n return;\n }\n const bridge = `<script src=\"${CLIENT_PATH}webview-bridge.js\"></script>`;\n send(response, 200, body.toString('utf8').replace('<head>', `<head>\\n ${bridge}`), '.html');\n } catch {\n send(response, 404, 'Not found');\n }\n}\n\nfunction send(\n response: ServerResponse,\n status: number,\n body: string | Buffer,\n extension = '.txt',\n): void {\n response.writeHead(status, {\n 'Content-Type': MIME_TYPES[extension] ?? 'text/plain; charset=utf-8',\n 'Cache-Control': 'no-store',\n });\n response.end(body);\n}\n\nfunction defaultClientDir(): string {\n const manifest = createRequire(import.meta.url).resolve('@monimesl/dot-simulator/package.json');\n return join(dirname(manifest), 'dist/client');\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@monimesl/dot-simulator",
3
+ "version": "0.1.0-alpha.2",
4
+ "description": "Browser host for Dot mini apps: runs the service in a Worker and the webview in a device frame",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/monime-lab/dot.git",
9
+ "directory": "packages/hosts/simulator"
10
+ },
11
+ "sideEffects": false,
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "@monimesl/dot-source": "./src/index.ts",
18
+ "types": "./dist/node/index.d.ts",
19
+ "import": "./dist/node/index.js",
20
+ "default": "./dist/node/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "main": "./dist/node/index.js",
25
+ "types": "./dist/node/index.d.ts",
26
+ "dependencies": {
27
+ "@monimesl/dot-core": "0.1.0-alpha.2"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.20.4",
31
+ "publint": "^0.3.24",
32
+ "rimraf": "^6.1.3",
33
+ "tsdown": "^0.23.0",
34
+ "typescript": "^5.9.3",
35
+ "vitest": "^5.0.1",
36
+ "@monimesl/dot-dash": "0.1.0-alpha.2"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "rimraf dist && tsdown",
43
+ "dev": "tsdown --watch",
44
+ "typecheck": "tsc --noEmit -p tsconfig.json",
45
+ "test": "vitest run",
46
+ "clean": "rimraf dist *.tsbuildinfo"
47
+ }
48
+ }