@openthink/ui-leaf 0.6.0 → 0.7.0
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/package.json +1 -1
- package/packages/cli/dist/cli.js +299 -61
- package/packages/cli/dist/cli.js.map +1 -1
- package/packages/cli/dist/index.js +212 -60
- package/packages/cli/dist/index.js.map +1 -1
|
@@ -33,13 +33,15 @@ var reactAliasPlugin = {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
|
+
var SESSION_ENDED_HTML = '<div style="font-family:sans-serif;padding:2em;color:#555"><p>Session ended \u2014 re-launch the CLI to continue.</p></div>';
|
|
37
|
+
var CLOSED_OVERLAY_HTML = '<div style="font-family:sans-serif;padding:2em;color:#555"><p>This view has closed.</p></div>';
|
|
36
38
|
var SHARED_BRIDGE = `
|
|
37
39
|
async function mutate(name: string, args?: unknown): Promise<unknown> {
|
|
38
40
|
const res = await fetch("/mutate", {
|
|
39
41
|
method: "POST",
|
|
40
42
|
headers: {
|
|
41
43
|
"Content-Type": "application/json",
|
|
42
|
-
...(token ? {
|
|
44
|
+
...(token ? { "X-UI-Leaf-Token": token } : {}),
|
|
43
45
|
},
|
|
44
46
|
body: JSON.stringify({ name, args }),
|
|
45
47
|
});
|
|
@@ -61,12 +63,64 @@ async function heartbeat(): Promise<void> {
|
|
|
61
63
|
try {
|
|
62
64
|
await fetch("/heartbeat", {
|
|
63
65
|
method: "POST",
|
|
64
|
-
headers: token ? {
|
|
66
|
+
headers: token ? { "X-UI-Leaf-Token": token } : {},
|
|
65
67
|
});
|
|
66
68
|
} catch { /* server may have shut down; ignore */ }
|
|
67
69
|
}
|
|
68
70
|
setInterval(heartbeat, 5000);
|
|
69
|
-
heartbeat()
|
|
71
|
+
heartbeat();
|
|
72
|
+
|
|
73
|
+
function subscribeEvents(onEvent: (ev: { type: string; [k: string]: unknown }) => void): void {
|
|
74
|
+
let delay = 250;
|
|
75
|
+
const budget = 30_000;
|
|
76
|
+
const started = Date.now();
|
|
77
|
+
let done = false;
|
|
78
|
+
|
|
79
|
+
async function connect(): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch("/events", {
|
|
82
|
+
headers: token ? { "X-UI-Leaf-Token": token } : {},
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok || !res.body) throw new Error("bad status " + res.status);
|
|
85
|
+
delay = 250;
|
|
86
|
+
const reader = res.body.getReader();
|
|
87
|
+
const dec = new TextDecoder("utf-8");
|
|
88
|
+
let buf = "";
|
|
89
|
+
while (true) {
|
|
90
|
+
const { done: streamDone, value } = await reader.read();
|
|
91
|
+
if (streamDone) break;
|
|
92
|
+
buf += dec.decode(value, { stream: true });
|
|
93
|
+
let idx: number;
|
|
94
|
+
while ((idx = buf.indexOf("\\n\\n")) !== -1) {
|
|
95
|
+
const chunk = buf.slice(0, idx);
|
|
96
|
+
buf = buf.slice(idx + 2);
|
|
97
|
+
for (const line of chunk.split("\\n")) {
|
|
98
|
+
if (line.startsWith("data:")) {
|
|
99
|
+
try {
|
|
100
|
+
const ev = JSON.parse(line.slice(5).trimStart()) as { type: string; [k: string]: unknown };
|
|
101
|
+
if (ev.type === "closing") done = true;
|
|
102
|
+
onEvent(ev);
|
|
103
|
+
} catch { /* skip malformed event */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (done) return;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
if (done) return;
|
|
111
|
+
}
|
|
112
|
+
if (done) return;
|
|
113
|
+
if (Date.now() - started > budget) {
|
|
114
|
+
onEvent({ type: "closing", reason: "error" });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await new Promise<void>((r) => setTimeout(r, delay));
|
|
118
|
+
delay = Math.min(delay * 2, 5_000);
|
|
119
|
+
void connect();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
void connect();
|
|
123
|
+
}`;
|
|
70
124
|
async function runBunBuild(entryPath) {
|
|
71
125
|
let buildOutput;
|
|
72
126
|
try {
|
|
@@ -99,21 +153,21 @@ async function runBunBuild(entryPath) {
|
|
|
99
153
|
return { js: await output.text() };
|
|
100
154
|
}
|
|
101
155
|
function assembleHtml(opts) {
|
|
102
|
-
const { js, title, csp, data,
|
|
156
|
+
const { js, title, csp, data, dataLoader } = opts;
|
|
103
157
|
const safeJs = js.replace(/<\/script>/gi, "<\\/script>");
|
|
104
158
|
const titleEscaped = title.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
105
159
|
const cspMeta = csp ? ` <meta http-equiv="Content-Security-Policy" content="${csp.replace(/&/g, "&").replace(/"/g, """)}" />
|
|
106
160
|
` : "";
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
161
|
+
const dataInit = dataLoader ? "window.__UI_LEAF__ = {};" : `window.__UI_LEAF__ = { data: JSON.parse(${escapeForScriptTag(JSON.stringify(JSON.stringify(data ?? null)))}) };`;
|
|
162
|
+
const bootstrapScript = `${dataInit}
|
|
163
|
+
(function(){var m=/[#&]token=([^&#]*)/.exec(window.location.hash);if(m){try{window.__UI_LEAF__.token=decodeURIComponent(m[1]);history.replaceState(null,"",window.location.pathname+window.location.search);}catch(e){window.__UI_LEAF__.sessionEnded=true;}}else{window.__UI_LEAF__.sessionEnded=true;}})();`;
|
|
110
164
|
return `<!doctype html>
|
|
111
165
|
<html lang="en">
|
|
112
166
|
<head>
|
|
113
167
|
<meta charset="utf-8" />
|
|
114
168
|
<title>${titleEscaped}</title>
|
|
115
169
|
${cspMeta} <!-- ui-leaf bootstrap -->
|
|
116
|
-
<script
|
|
170
|
+
<script>${bootstrapScript}</script>
|
|
117
171
|
</head>
|
|
118
172
|
<body>
|
|
119
173
|
<div id="root"></div>
|
|
@@ -128,9 +182,9 @@ async function compileView(opts) {
|
|
|
128
182
|
data,
|
|
129
183
|
title = "ui-leaf",
|
|
130
184
|
csp,
|
|
131
|
-
// allowedHosts
|
|
185
|
+
// allowedHosts and token have no compile-time effect; accepted for API symmetry.
|
|
132
186
|
allowedHosts: _allowedHosts,
|
|
133
|
-
token,
|
|
187
|
+
token: _token,
|
|
134
188
|
dataLoader = false
|
|
135
189
|
} = opts;
|
|
136
190
|
const viewsRootAbs = resolve(viewsRoot);
|
|
@@ -170,41 +224,75 @@ async function compileView(opts) {
|
|
|
170
224
|
const entryContent = dataLoader ? `import { createRoot } from "react-dom/client";
|
|
171
225
|
import View from ${JSON.stringify(viewAbs)};
|
|
172
226
|
|
|
173
|
-
const ctx = (globalThis as { __UI_LEAF__?: { token?: string } }).__UI_LEAF__ ?? {};
|
|
227
|
+
const ctx = (globalThis as { __UI_LEAF__?: { token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};
|
|
174
228
|
const token = ctx.token;
|
|
229
|
+
|
|
230
|
+
if (ctx.sessionEnded) {
|
|
231
|
+
const root = document.getElementById("root");
|
|
232
|
+
if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};
|
|
233
|
+
} else {
|
|
175
234
|
${SHARED_BRIDGE}
|
|
176
235
|
|
|
177
|
-
async function bootstrap(): Promise<void> {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
236
|
+
async function bootstrap(): Promise<void> {
|
|
237
|
+
const res = await fetch("/api/data", {
|
|
238
|
+
headers: token ? { "X-UI-Leaf-Token": token } : {},
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok) {
|
|
241
|
+
const text = await res.text().catch(() => "");
|
|
242
|
+
throw new Error("ui-leaf: /api/data fetch failed (" + res.status + "): " + text);
|
|
243
|
+
}
|
|
244
|
+
let currentData: unknown = await res.json();
|
|
245
|
+
const el = document.getElementById("root");
|
|
246
|
+
if (!el) throw new Error("ui-leaf: #root element missing");
|
|
247
|
+
const root = createRoot(el);
|
|
248
|
+
root.render(<View data={currentData} mutate={mutate} />);
|
|
249
|
+
subscribeEvents((ev) => {
|
|
250
|
+
if (ev.type === "data-updated") {
|
|
251
|
+
currentData = ev.data;
|
|
252
|
+
root.render(<View data={currentData} mutate={mutate} />);
|
|
253
|
+
} else if (ev.type === "view-swapped") {
|
|
254
|
+
window.location.reload();
|
|
255
|
+
} else if (ev.type === "closing") {
|
|
256
|
+
el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};
|
|
257
|
+
}
|
|
258
|
+
});
|
|
184
259
|
}
|
|
185
|
-
|
|
186
|
-
const el = document.getElementById("root");
|
|
187
|
-
if (!el) throw new Error("ui-leaf: #root element missing");
|
|
188
|
-
createRoot(el).render(<View data={data} mutate={mutate} />);
|
|
260
|
+
bootstrap();
|
|
189
261
|
}
|
|
190
|
-
bootstrap();
|
|
191
262
|
` : `import { createRoot } from "react-dom/client";
|
|
192
263
|
import View from ${JSON.stringify(viewAbs)};
|
|
193
264
|
|
|
194
|
-
const ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string } }).__UI_LEAF__ ?? {};
|
|
195
|
-
const data = ctx.data;
|
|
265
|
+
const ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};
|
|
196
266
|
const token = ctx.token;
|
|
267
|
+
|
|
268
|
+
if (ctx.sessionEnded) {
|
|
269
|
+
const root = document.getElementById("root");
|
|
270
|
+
if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};
|
|
271
|
+
} else {
|
|
272
|
+
let currentData: unknown = ctx.data;
|
|
197
273
|
${SHARED_BRIDGE}
|
|
198
274
|
|
|
199
|
-
const el = document.getElementById("root");
|
|
200
|
-
if (!el) throw new Error("ui-leaf: #root element missing");
|
|
201
|
-
|
|
275
|
+
const el = document.getElementById("root");
|
|
276
|
+
if (!el) throw new Error("ui-leaf: #root element missing");
|
|
277
|
+
const root = createRoot(el);
|
|
278
|
+
root.render(<View data={currentData} mutate={mutate} />);
|
|
279
|
+
subscribeEvents((ev) => {
|
|
280
|
+
if (ev.type === "data-updated") {
|
|
281
|
+
currentData = ev.data;
|
|
282
|
+
root.render(<View data={currentData} mutate={mutate} />);
|
|
283
|
+
} else if (ev.type === "view-swapped") {
|
|
284
|
+
window.location.reload();
|
|
285
|
+
} else if (ev.type === "closing") {
|
|
286
|
+
el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
202
290
|
`;
|
|
203
291
|
await writeFile(entryPath, entryContent);
|
|
204
292
|
const buildResult = await runBunBuild(entryPath);
|
|
205
293
|
if ("errors" in buildResult) return { html: "", errors: buildResult.errors };
|
|
206
294
|
return {
|
|
207
|
-
html: assembleHtml({ js: buildResult.js, title, csp, data,
|
|
295
|
+
html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader }),
|
|
208
296
|
errors: []
|
|
209
297
|
};
|
|
210
298
|
} finally {
|
|
@@ -212,7 +300,7 @@ createRoot(el).render(<View data={data} mutate={mutate} />);
|
|
|
212
300
|
}
|
|
213
301
|
}
|
|
214
302
|
async function compileSource(opts) {
|
|
215
|
-
const { source, data, title = "ui-leaf", csp, token } = opts;
|
|
303
|
+
const { source, data, title = "ui-leaf", csp, token: _token } = opts;
|
|
216
304
|
const tempDir = await mkdtemp(join(tmpdir(), "ui-leaf-src-"));
|
|
217
305
|
try {
|
|
218
306
|
const viewPath = join(tempDir, "view.tsx");
|
|
@@ -221,20 +309,37 @@ async function compileSource(opts) {
|
|
|
221
309
|
const entryContent = `import { createRoot } from "react-dom/client";
|
|
222
310
|
import View from ${JSON.stringify(viewPath)};
|
|
223
311
|
|
|
224
|
-
const ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string } }).__UI_LEAF__ ?? {};
|
|
225
|
-
const data = ctx.data;
|
|
312
|
+
const ctx = (globalThis as { __UI_LEAF__?: { data?: unknown; token?: string; sessionEnded?: boolean } }).__UI_LEAF__ ?? {};
|
|
226
313
|
const token = ctx.token;
|
|
314
|
+
|
|
315
|
+
if (ctx.sessionEnded) {
|
|
316
|
+
const root = document.getElementById("root");
|
|
317
|
+
if (root) root.innerHTML = ${JSON.stringify(SESSION_ENDED_HTML)};
|
|
318
|
+
} else {
|
|
319
|
+
let currentData: unknown = ctx.data;
|
|
227
320
|
${SHARED_BRIDGE}
|
|
228
321
|
|
|
229
|
-
const el = document.getElementById("root");
|
|
230
|
-
if (!el) throw new Error("ui-leaf: #root element missing");
|
|
231
|
-
|
|
322
|
+
const el = document.getElementById("root");
|
|
323
|
+
if (!el) throw new Error("ui-leaf: #root element missing");
|
|
324
|
+
const root = createRoot(el);
|
|
325
|
+
root.render(<View data={currentData} mutate={mutate} />);
|
|
326
|
+
subscribeEvents((ev) => {
|
|
327
|
+
if (ev.type === "data-updated") {
|
|
328
|
+
currentData = ev.data;
|
|
329
|
+
root.render(<View data={currentData} mutate={mutate} />);
|
|
330
|
+
} else if (ev.type === "view-swapped") {
|
|
331
|
+
window.location.reload();
|
|
332
|
+
} else if (ev.type === "closing") {
|
|
333
|
+
el.innerHTML = ${JSON.stringify(CLOSED_OVERLAY_HTML)};
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
232
337
|
`;
|
|
233
338
|
await writeFile(entryPath, entryContent);
|
|
234
339
|
const buildResult = await runBunBuild(entryPath);
|
|
235
340
|
if ("errors" in buildResult) return { html: "", errors: buildResult.errors };
|
|
236
341
|
return {
|
|
237
|
-
html: assembleHtml({ js: buildResult.js, title, csp, data,
|
|
342
|
+
html: assembleHtml({ js: buildResult.js, title, csp, data, dataLoader: false }),
|
|
238
343
|
errors: []
|
|
239
344
|
};
|
|
240
345
|
} finally {
|
|
@@ -343,8 +448,19 @@ async function startDevServer(opts) {
|
|
|
343
448
|
try {
|
|
344
449
|
let fireEvent2 = function(event) {
|
|
345
450
|
for (const fn of listeners.get(event)) fn();
|
|
451
|
+
}, broadcast2 = function(event) {
|
|
452
|
+
const frame = sseEncoder.encode(`data: ${JSON.stringify(event)}
|
|
453
|
+
|
|
454
|
+
`);
|
|
455
|
+
for (const controller of sseClients) {
|
|
456
|
+
try {
|
|
457
|
+
controller.enqueue(frame);
|
|
458
|
+
} catch {
|
|
459
|
+
sseClients.delete(controller);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
346
462
|
};
|
|
347
|
-
var fireEvent = fireEvent2;
|
|
463
|
+
var fireEvent = fireEvent2, broadcast = broadcast2;
|
|
348
464
|
if (view.includes("/") || view.includes("\\")) {
|
|
349
465
|
throw new Error(
|
|
350
466
|
`ui-leaf: view '${view}' must be a bare identifier with no path separators`
|
|
@@ -378,6 +494,8 @@ async function startDevServer(opts) {
|
|
|
378
494
|
["disconnected", /* @__PURE__ */ new Set()],
|
|
379
495
|
["reconnected", /* @__PURE__ */ new Set()]
|
|
380
496
|
]);
|
|
497
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
498
|
+
const sseEncoder = new TextEncoder();
|
|
381
499
|
let lastHeartbeatAt = Date.now();
|
|
382
500
|
let closeRequested = false;
|
|
383
501
|
let connectionState = "connecting";
|
|
@@ -416,10 +534,7 @@ async function startDevServer(opts) {
|
|
|
416
534
|
}
|
|
417
535
|
if (method === "POST" && path === "/heartbeat") {
|
|
418
536
|
if (!checkAuth(req, token)) {
|
|
419
|
-
return new Response(
|
|
420
|
-
status: 401,
|
|
421
|
-
headers: { ...headers, "Content-Type": "application/json" }
|
|
422
|
-
});
|
|
537
|
+
return new Response("", { status: 401, headers });
|
|
423
538
|
}
|
|
424
539
|
lastHeartbeatAt = Date.now();
|
|
425
540
|
if (connectionState === "disconnected") {
|
|
@@ -432,10 +547,7 @@ async function startDevServer(opts) {
|
|
|
432
547
|
}
|
|
433
548
|
if (method === "POST" && path === "/mutate") {
|
|
434
549
|
if (!checkAuth(req, token)) {
|
|
435
|
-
return new Response(
|
|
436
|
-
status: 401,
|
|
437
|
-
headers: { ...headers, "Content-Type": "application/json" }
|
|
438
|
-
});
|
|
550
|
+
return new Response("", { status: 401, headers });
|
|
439
551
|
}
|
|
440
552
|
return handleMutate(req, mutations, headers);
|
|
441
553
|
}
|
|
@@ -447,16 +559,44 @@ async function startDevServer(opts) {
|
|
|
447
559
|
});
|
|
448
560
|
}
|
|
449
561
|
if (!checkAuth(req, token)) {
|
|
450
|
-
return new Response(
|
|
451
|
-
status: 401,
|
|
452
|
-
headers: { ...headers, "Content-Type": "application/json" }
|
|
453
|
-
});
|
|
562
|
+
return new Response("", { status: 401, headers });
|
|
454
563
|
}
|
|
455
564
|
return new Response(JSON.stringify(viewState.data !== void 0 ? viewState.data : null), {
|
|
456
565
|
status: 200,
|
|
457
566
|
headers: { ...headers, "Content-Type": "application/json" }
|
|
458
567
|
});
|
|
459
568
|
}
|
|
569
|
+
if (method === "GET" && path === "/events") {
|
|
570
|
+
if (!checkAuth(req, token)) {
|
|
571
|
+
return new Response("", { status: 401, headers });
|
|
572
|
+
}
|
|
573
|
+
let sseController;
|
|
574
|
+
const stream = new ReadableStream({
|
|
575
|
+
start(controller) {
|
|
576
|
+
sseController = controller;
|
|
577
|
+
sseClients.add(controller);
|
|
578
|
+
controller.enqueue(sseEncoder.encode(": connected\n\n"));
|
|
579
|
+
req.signal?.addEventListener("abort", () => {
|
|
580
|
+
sseClients.delete(sseController);
|
|
581
|
+
try {
|
|
582
|
+
sseController.close();
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
},
|
|
587
|
+
cancel() {
|
|
588
|
+
sseClients.delete(sseController);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
return new Response(stream, {
|
|
592
|
+
status: 200,
|
|
593
|
+
headers: {
|
|
594
|
+
...headers,
|
|
595
|
+
"Content-Type": "text/event-stream",
|
|
596
|
+
"Cache-Control": "no-cache"
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
460
600
|
return new Response(JSON.stringify({ error: "not found" }), {
|
|
461
601
|
status: 404,
|
|
462
602
|
headers: { ...headers, "Content-Type": "application/json" }
|
|
@@ -468,7 +608,15 @@ async function startDevServer(opts) {
|
|
|
468
608
|
if (closeRequested) return;
|
|
469
609
|
closeRequested = true;
|
|
470
610
|
if (heartbeatWatcher) clearInterval(heartbeatWatcher);
|
|
471
|
-
|
|
611
|
+
broadcast2({ type: "closing", reason });
|
|
612
|
+
for (const controller of sseClients) {
|
|
613
|
+
try {
|
|
614
|
+
controller.close();
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
sseClients.clear();
|
|
619
|
+
await bunServer.stop();
|
|
472
620
|
if (restoreStdout) restoreStdout();
|
|
473
621
|
resolveClosed(reason);
|
|
474
622
|
};
|
|
@@ -481,12 +629,12 @@ async function startDevServer(opts) {
|
|
|
481
629
|
};
|
|
482
630
|
bunServer = (() => {
|
|
483
631
|
if (bunPort === 0) {
|
|
484
|
-
return Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: handler, error: serverErrorHandler });
|
|
632
|
+
return Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });
|
|
485
633
|
}
|
|
486
634
|
const MAX_PORT_ATTEMPTS = 10;
|
|
487
635
|
for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
|
|
488
636
|
try {
|
|
489
|
-
return Bun.serve({ hostname: "127.0.0.1", port: bunPort + i, fetch: handler, error: serverErrorHandler });
|
|
637
|
+
return Bun.serve({ hostname: "127.0.0.1", port: bunPort + i, fetch: handler, error: serverErrorHandler, idleTimeout: 0 });
|
|
490
638
|
} catch (err) {
|
|
491
639
|
const isAddrinuse = err instanceof Error && err.message.includes("EADDRINUSE");
|
|
492
640
|
if (!isAddrinuse || i === MAX_PORT_ATTEMPTS - 1) {
|
|
@@ -515,18 +663,19 @@ async function startDevServer(opts) {
|
|
|
515
663
|
}
|
|
516
664
|
}
|
|
517
665
|
}, _heartbeatCheckIntervalMs);
|
|
518
|
-
const
|
|
666
|
+
const openUrl = `${url}/#token=${token}`;
|
|
667
|
+
const doOpen = _opener ? () => _opener(openUrl) : async () => {
|
|
519
668
|
if (shell === "app") {
|
|
520
|
-
const launched = await openInAppMode(
|
|
669
|
+
const launched = await openInAppMode(openUrl);
|
|
521
670
|
if (!launched) {
|
|
522
671
|
process.stderr.write(
|
|
523
672
|
`ui-leaf: shell:"app" requested but no Chromium browser found; falling back to default browser tab.
|
|
524
673
|
`
|
|
525
674
|
);
|
|
526
|
-
await open(
|
|
675
|
+
await open(openUrl);
|
|
527
676
|
}
|
|
528
677
|
} else {
|
|
529
|
-
await open(
|
|
678
|
+
await open(openUrl);
|
|
530
679
|
}
|
|
531
680
|
};
|
|
532
681
|
if (openBrowser) {
|
|
@@ -545,6 +694,7 @@ async function startDevServer(opts) {
|
|
|
545
694
|
},
|
|
546
695
|
update(newData) {
|
|
547
696
|
viewState.data = newData;
|
|
697
|
+
broadcast2({ type: "data-updated", data: newData });
|
|
548
698
|
fireEvent2("data-updated");
|
|
549
699
|
},
|
|
550
700
|
async swapView(source) {
|
|
@@ -557,6 +707,7 @@ async function startDevServer(opts) {
|
|
|
557
707
|
});
|
|
558
708
|
if (r.errors.length > 0) return r.errors;
|
|
559
709
|
viewState.html = r.html;
|
|
710
|
+
broadcast2({ type: "view-swapped" });
|
|
560
711
|
fireEvent2("view-swapped");
|
|
561
712
|
return [];
|
|
562
713
|
},
|
|
@@ -571,6 +722,8 @@ async function startDevServer(opts) {
|
|
|
571
722
|
if (r.errors.length > 0) return r.errors;
|
|
572
723
|
viewState.data = newData;
|
|
573
724
|
viewState.html = r.html;
|
|
725
|
+
broadcast2({ type: "data-updated", data: newData });
|
|
726
|
+
broadcast2({ type: "view-swapped" });
|
|
574
727
|
fireEvent2("data-updated");
|
|
575
728
|
fireEvent2("view-swapped");
|
|
576
729
|
return [];
|
|
@@ -585,10 +738,9 @@ async function startDevServer(opts) {
|
|
|
585
738
|
}
|
|
586
739
|
}
|
|
587
740
|
function checkAuth(req, token) {
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
return timingSafeEqual(match[1], token);
|
|
741
|
+
const value = req.headers.get("x-ui-leaf-token") ?? "";
|
|
742
|
+
if (!value) return false;
|
|
743
|
+
return timingSafeEqual(value, token);
|
|
592
744
|
}
|
|
593
745
|
async function handleMutate(req, mutations, headers) {
|
|
594
746
|
const contentLength = req.headers.get("content-length");
|