@stealthscale/sdk-host 0.1.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.
@@ -0,0 +1,4730 @@
1
+ import { n as translatorOf, r as useProductName, t as pluginWordsOf } from "./words-yI-WdYM3.js";
2
+ import { DataProvider, createDataClient, defineMutation, defineQuery, findRecords, mutateOperation, operationQuery, resetData } from "@stealthscale/provider-data";
3
+ import { HOST, NOBODY, evaluateWhen, hostContract, pluginOf, subjectOf } from "@stealthscale/sdk-core";
4
+ import { sampledTransport } from "@stealthscale/provider-data/testing";
5
+ import { Boundary, HostContext, PluginProvider, RouteDecorations, Slot, changesOf, conditionContextOf, decisionKey, decisionOf, isCommandEnabled, lazyOf, useCommands, useDocumentTitle, useNavigation, usePluginStatuses, useResolvedProduct, useSettings, useToaster } from "@stealthscale/sdk-plugin";
6
+ import { Alert, EmptyState, Toast } from "@stealthscale/component-feedback";
7
+ import { settingKey } from "@stealthscale/settings";
8
+ import { c } from "react/compiler-runtime";
9
+ import { Navigate, Outlet, RouterContextProvider, compileRoutes, createLink, declaredOf, notFound, redirect, routeHref, useDeclaredRoute, useNavigate, useRouteContext, useRouter } from "@stealthscale/provider-router";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ import { Component, Suspense, createElement, lazy, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
12
+ import { Button } from "@stealthscale/component-actions";
13
+ import { useTranslation } from "@stealthscale/provider-i18n";
14
+ import { normalizeHotkey, useHotkey, useHotkeys } from "@stealthscale/provider-hotkeys";
15
+ import { Command, Dialog } from "@stealthscale/component-modals";
16
+ import { loadNeeds } from "@stealthscale/provider-data/router";
17
+ import { Stack } from "@stealthscale/component-layout";
18
+ import { Link } from "@stealthscale/component-navigation";
19
+ import { Page, Section } from "@stealthscale/component-screen";
20
+ import { List, Text } from "@stealthscale/component-typography";
21
+ import { flushSync } from "react-dom";
22
+ import { Switch } from "@stealthscale/component-forms";
23
+ import { useAnnounce } from "@stealthscale/hooks";
24
+ import { useSchemaForm } from "@stealthscale/component-forms/form";
25
+ /**
26
+ * Returns the message of an emit by a party that may not emit an event.
27
+ */
28
+ function refused(event, by) {
29
+ return `${by === void 0 ? "The product" : `The plugin ${by}`} may not emit ${event.id}: only ${event.plugin} emits it.`;
30
+ }
31
+ /**
32
+ * Returns the event bus over the declared events.
33
+ *
34
+ * @remarks
35
+ * An emit of an event no installed plugin declares throws, and so does an emit by a party other
36
+ * than the declaring plugin, unless the event states `emit: "anyone"`. The product's own code
37
+ * emits as no plugin, so it emits an `anyone` event alone. A sticky event keeps its last payload
38
+ * and hands it to each new subscriber when it subscribes. A subscription to an event no installed
39
+ * plugin declares receives nothing.
40
+ */
41
+ function createEventBus({ events, report }) {
42
+ const declared = new Map(events.map((event) => [event.id, event]));
43
+ const target = new EventTarget();
44
+ const kept = /* @__PURE__ */ new Map();
45
+ let depth = 0;
46
+ return {
47
+ emit: (by, eventId, payload) => {
48
+ const event = declared.get(eventId);
49
+ if (event === void 0) throw new Error(`No installed plugin declares the event ${eventId}.`);
50
+ if (event.emit !== "anyone" && by !== event.plugin) throw new Error(refused(event, by));
51
+ if (depth >= 16) {
52
+ report({
53
+ kind: "event-chain-cut",
54
+ plugin: by,
55
+ target: eventId
56
+ });
57
+ return;
58
+ }
59
+ if (event.sticky) kept.set(eventId, payload);
60
+ depth += 1;
61
+ try {
62
+ target.dispatchEvent(new CustomEvent(eventId, { detail: payload }));
63
+ } finally {
64
+ depth -= 1;
65
+ }
66
+ },
67
+ subscribe: (by, eventId, handler) => {
68
+ /**
69
+ * Passes a payload to the handler, and reports what the handler throws.
70
+ */
71
+ const deliver = (payload) => {
72
+ try {
73
+ handler(payload);
74
+ } catch (error) {
75
+ report({
76
+ error,
77
+ kind: "event-handler-failed",
78
+ plugin: by,
79
+ target: eventId
80
+ });
81
+ }
82
+ };
83
+ /**
84
+ * Receives the event on the target.
85
+ */
86
+ const listener = (event) => {
87
+ deliver(event.detail);
88
+ };
89
+ target.addEventListener(eventId, listener);
90
+ if (kept.has(eventId)) deliver(kept.get(eventId));
91
+ return () => {
92
+ target.removeEventListener(eventId, listener);
93
+ };
94
+ }
95
+ };
96
+ }
97
+ //#endregion
98
+ //#region src/host/transport.ts
99
+ /**
100
+ * Returns a transport that serves each declared query and mutation with its sample's data.
101
+ */
102
+ function sampledFrom(product) {
103
+ return sampledTransport(Object.fromEntries([...product.queries, ...product.mutations].map(({ operation, sample }) => [operation.id, { data: sample.data }])));
104
+ }
105
+ /**
106
+ * Returns the error of an operation no installed plugin declares.
107
+ */
108
+ function undeclared(id) {
109
+ return /* @__PURE__ */ new Error(`No installed plugin declares the operation ${id}.`);
110
+ }
111
+ /**
112
+ * Returns true for the data of one event of the changes stream.
113
+ */
114
+ function isBatch(data) {
115
+ return typeof data === "object" && data !== null && "changes" in data && Array.isArray(data.changes);
116
+ }
117
+ /**
118
+ * Returns the transport the host's data client runs on.
119
+ *
120
+ * @remarks
121
+ * The transport refuses an operation no installed plugin declares, and every subscription but
122
+ * the product's changes stream. A declared mutation that resolves announces the changes its
123
+ * declarations name for its variables. Each batch of the stream is announced after the client
124
+ * received it.
125
+ */
126
+ function guardedTransport({ changed, changes, product, transport = sampledFrom(product) }) {
127
+ const declared = new Set([...product.queries, ...product.mutations].map(({ operation }) => operation.id));
128
+ return {
129
+ run: async (operation, variables, options) => {
130
+ if (!declared.has(operation.id)) throw undeclared(operation.id);
131
+ const data = await transport.run(operation, variables, options);
132
+ const made = product.mutations.filter((mutation) => mutation.operation.id === operation.id).flatMap((mutation) => changesOf(mutation.changes, variables));
133
+ if (made.length > 0) changed(made);
134
+ return data;
135
+ },
136
+ subscribe: (operation, variables, next, signal) => {
137
+ if (operation.id !== changes?.id) return Promise.reject(undeclared(operation.id));
138
+ return transport.subscribe(operation, variables, (data) => {
139
+ next(data);
140
+ if (isBatch(data)) changed(data.changes);
141
+ }, signal);
142
+ }
143
+ };
144
+ }
145
+ //#endregion
146
+ //#region src/host/data.ts
147
+ /**
148
+ * Creates the host's data client: its transport guarded to the declared operations, the decisions
149
+ * each query's data states primed into the access store, changes announced on the bus, and the
150
+ * person sent to sign in where the gateway refuses the session.
151
+ */
152
+ /**
153
+ * Returns the decisions one decision selector finds in a query's data.
154
+ *
155
+ * @remarks
156
+ * A record whose member is not a boolean states no decision, so `useAccess` asks the access
157
+ * source for it.
158
+ */
159
+ function foundBy(selector, type, data) {
160
+ return findRecords(data, selector).flatMap(({ id, record }) => {
161
+ const allowed = record[selector.field];
162
+ return typeof allowed === "boolean" ? [{
163
+ allowed,
164
+ permission: selector.permission,
165
+ resource: {
166
+ id,
167
+ type
168
+ }
169
+ }] : [];
170
+ });
171
+ }
172
+ /**
173
+ * Returns the decisions one arrival of a query's data states, through the decision selectors of
174
+ * every query declared for its operation.
175
+ *
176
+ * @remarks
177
+ * A selector whose permission states no resource kind finds nothing.
178
+ */
179
+ function decisionsIn(product, arrival) {
180
+ const kinds = new Map(product.permissions.map(({ id, resource }) => [id, resource]));
181
+ return product.queries.filter(({ operation }) => operation.id === arrival.operation).flatMap(({ decisions }) => decisions).flatMap((selector) => {
182
+ const type = kinds.get(selector.permission);
183
+ return type === void 0 ? [] : foundBy(selector, type, arrival.data);
184
+ });
185
+ }
186
+ /**
187
+ * Returns the data client of the page or the request.
188
+ *
189
+ * @remarks
190
+ * Each arrival of a declared query's data, from a fetch or from a server render the page
191
+ * hydrates, primes the decisions its selectors state. A refusal of the session reads the session
192
+ * again and invalidates the router, so the sign-in redirect applies.
193
+ */
194
+ function createHostData({ access, connection, data, events, product, session }) {
195
+ return createDataClient({
196
+ changes: data?.changes,
197
+ onData: (arrival) => {
198
+ const decisions = decisionsIn(product, arrival);
199
+ if (decisions.length > 0) access.prime(decisions);
200
+ },
201
+ onUnauthenticated: () => {
202
+ session.refresh();
203
+ connection()?.invalidate();
204
+ },
205
+ transport: guardedTransport({
206
+ changed: (changes) => {
207
+ events.emit(HOST, hostContract.events.recordsChanged.id, { changes });
208
+ },
209
+ changes: data?.changes,
210
+ product,
211
+ transport: data?.transport
212
+ })
213
+ });
214
+ }
215
+ //#endregion
216
+ //#region src/host/internals.ts
217
+ /**
218
+ * The internals of each host `createHost` created.
219
+ */
220
+ const INTERNALS = /* @__PURE__ */ new WeakMap();
221
+ /**
222
+ * Returns a connector with no connection.
223
+ *
224
+ * @remarks
225
+ * A disconnect leaves a later connection in place, so a `HostProvider` that unmounts after
226
+ * another mounted does not remove the newer one.
227
+ */
228
+ function connector() {
229
+ let kept;
230
+ return {
231
+ connect: (connection) => {
232
+ kept = connection;
233
+ return () => {
234
+ if (kept === connection) kept = void 0;
235
+ };
236
+ },
237
+ current: () => kept
238
+ };
239
+ }
240
+ /**
241
+ * Records a host's internals, for the host's own parts to read.
242
+ */
243
+ function remember(host, internals) {
244
+ INTERNALS.set(host, internals);
245
+ }
246
+ /**
247
+ * Returns a host's internals.
248
+ *
249
+ * @throws {@link Error} Where `createHost` did not create the host.
250
+ */
251
+ function internalsOf(host) {
252
+ const internals = INTERNALS.get(host);
253
+ if (internals === void 0) throw new Error("The host was not created by createHost, so it has no runtime to provide.");
254
+ return internals;
255
+ }
256
+ //#endregion
257
+ //#region src/host/manifests.ts
258
+ /**
259
+ * Checks that the manifests map code to every name the resolved product declares.
260
+ *
261
+ * @remarks
262
+ * The types of `definePlugin` refuse a manifest that lacks code, so the check covers an untyped
263
+ * caller, such as a product assembled by hand.
264
+ */
265
+ /**
266
+ * The words of each kind in the message of a missing entry.
267
+ */
268
+ const WORDS = {
269
+ command: "the command",
270
+ extension: "the extension",
271
+ route: "the route",
272
+ section: "the settings section"
273
+ };
274
+ /**
275
+ * Returns the names of a record a manifest may leave out.
276
+ */
277
+ function namesOf(record) {
278
+ return record === void 0 ? [] : Object.keys(record);
279
+ }
280
+ /**
281
+ * Returns the names of the settings sections whose component a manifest maps.
282
+ */
283
+ function componentsOf(settings) {
284
+ return settings === void 0 ? [] : Object.entries(settings).filter(([, entry]) => entry.component !== void 0).map(([name]) => name);
285
+ }
286
+ /**
287
+ * Returns the key of every entry the manifests map to code: `<kind>:<qualified id>`.
288
+ */
289
+ function mappedOf(manifests) {
290
+ const mapped = /* @__PURE__ */ new Set();
291
+ for (const [pluginId, { code }] of Object.entries(manifests)) {
292
+ const named = [
293
+ ["route", namesOf(code.routes)],
294
+ ["extension", namesOf(code.extensions)],
295
+ ["command", namesOf(code.commands)],
296
+ ["section", componentsOf(code.settings)]
297
+ ];
298
+ for (const [kind, names] of named) for (const name of names) mapped.add(`${kind}:${pluginId}/${name}`);
299
+ }
300
+ return mapped;
301
+ }
302
+ /**
303
+ * Throws where a manifest lacks the code of a route, an extension, a command, or a settings section
304
+ * that renders a component.
305
+ *
306
+ * @remarks
307
+ * The host's own routes take no manifest: the host renders them itself.
308
+ * @param product - The resolved product, with each installed plugin's manifest.
309
+ * @throws {@link Error} Naming every declaration whose code is missing.
310
+ */
311
+ function validateManifests(product) {
312
+ const mapped = mappedOf(product.manifests);
313
+ const missing = [
314
+ ...product.routes.map(({ id }) => ["route", id]),
315
+ ...product.extensions.map(({ id }) => ["extension", id]),
316
+ ...product.commands.map(({ id }) => ["command", id]),
317
+ ...product.settings.sections.filter(({ component }) => component).map(({ id }) => ["section", id])
318
+ ].filter(([kind, id]) => pluginOf(id) !== HOST && !mapped.has(`${kind}:${id}`));
319
+ if (missing.length > 0) throw new Error(missing.map(([kind, id]) => `No manifest maps ${WORDS[kind]} ${id} to code.`).join(" "));
320
+ }
321
+ //#endregion
322
+ //#region src/stores/store.ts
323
+ /**
324
+ * Returns a store that keeps one value and calls its listeners after each change.
325
+ *
326
+ * @remarks
327
+ * The listeners run over a copy of the set, so a listener that subscribes or unsubscribes while
328
+ * the others run changes the next notification and not this one. Setting the same object again
329
+ * calls no listener, because `useSyncExternalStore` compares snapshots by identity.
330
+ * @param initial - The value before the first change.
331
+ */
332
+ function writable(initial) {
333
+ let value = initial;
334
+ const listeners = /* @__PURE__ */ new Set();
335
+ return {
336
+ get: () => value,
337
+ set: (next) => {
338
+ if (Object.is(next, value)) return;
339
+ value = next;
340
+ for (const listener of [...listeners]) listener();
341
+ },
342
+ subscribe: (listener) => {
343
+ listeners.add(listener);
344
+ return () => {
345
+ listeners.delete(listener);
346
+ };
347
+ }
348
+ };
349
+ }
350
+ //#endregion
351
+ //#region src/stores/mounted.ts
352
+ /**
353
+ * Returns the mounted store, with no slot on screen.
354
+ *
355
+ * @remarks
356
+ * Each `mount` records one instance, in mount order, until the function it returns is called. A
357
+ * slot whose last instance unmounts leaves the store. A second call of that function changes
358
+ * nothing.
359
+ */
360
+ function createMountedStore() {
361
+ const instances = /* @__PURE__ */ new Map();
362
+ const state = writable(/* @__PURE__ */ new Map());
363
+ let next = 0;
364
+ /**
365
+ * Publishes every mounted instance, grouped by slot.
366
+ */
367
+ const publish = () => {
368
+ const grouped = /* @__PURE__ */ new Map();
369
+ for (const [slotId, slot] of instances.values()) grouped.set(slotId, [...grouped.get(slotId) ?? [], slot]);
370
+ state.set(grouped);
371
+ };
372
+ return {
373
+ get: state.get,
374
+ mount: (slotId, slot) => {
375
+ const instance = next;
376
+ next += 1;
377
+ instances.set(instance, [slotId, slot]);
378
+ publish();
379
+ return () => {
380
+ if (instances.delete(instance)) publish();
381
+ };
382
+ },
383
+ subscribe: state.subscribe
384
+ };
385
+ }
386
+ //#endregion
387
+ //#region src/stores/pages.ts
388
+ /**
389
+ * Returns the page store, with no contribution.
390
+ *
391
+ * @remarks
392
+ * A slot lists its contributions by `order`, then in the order they were placed. A contribution
393
+ * renders nothing until its content is filled, and keeps its place while its content changes. A
394
+ * `place` for a key already placed replaces the contribution, and the earlier `place`'s leave
395
+ * function then changes nothing.
396
+ */
397
+ function createPageStore() {
398
+ const placed = /* @__PURE__ */ new Map();
399
+ const state = writable(/* @__PURE__ */ new Map());
400
+ let sequence = 0;
401
+ /**
402
+ * Publishes every contribution, grouped by slot and ordered.
403
+ */
404
+ const publish = () => {
405
+ const ordered = [...placed].toSorted(([, one], [, other]) => one.order - other.order || one.sequence - other.sequence);
406
+ const grouped = /* @__PURE__ */ new Map();
407
+ for (const [key, { content, order, slotId }] of ordered) grouped.set(slotId, [...grouped.get(slotId) ?? [], {
408
+ content,
409
+ key,
410
+ order
411
+ }]);
412
+ state.set(grouped);
413
+ };
414
+ return {
415
+ fill: (key, content) => {
416
+ const contribution = placed.get(key);
417
+ if (contribution === void 0 || contribution.content === content) return;
418
+ placed.set(key, {
419
+ ...contribution,
420
+ content
421
+ });
422
+ publish();
423
+ },
424
+ get: state.get,
425
+ place: (slotId, key, order) => {
426
+ sequence += 1;
427
+ const mine = sequence;
428
+ placed.set(key, {
429
+ content: null,
430
+ order,
431
+ sequence: mine,
432
+ slotId
433
+ });
434
+ publish();
435
+ return () => {
436
+ if (placed.get(key)?.sequence !== mine) return;
437
+ placed.delete(key);
438
+ publish();
439
+ };
440
+ },
441
+ subscribe: state.subscribe
442
+ };
443
+ }
444
+ //#endregion
445
+ //#region src/stores/quarantine.ts
446
+ /**
447
+ * Returns the quarantine store, with no target quarantined.
448
+ *
449
+ * @remarks
450
+ * The count is per render, not per second, so a target rendered sixty times a minute and one
451
+ * rendered once an hour follow one rule. A render that commits returns its target's count to
452
+ * zero. A quarantine lasts until `retry` or a reload: no store keeps it, because a reload may
453
+ * load a release that fixed the fault.
454
+ */
455
+ function createQuarantineStore({ after, report }) {
456
+ const failures = /* @__PURE__ */ new Map();
457
+ const state = writable(/* @__PURE__ */ new Map());
458
+ return {
459
+ failed: (target, error) => {
460
+ const count = (failures.get(target) ?? 0) + 1;
461
+ failures.set(target, count);
462
+ report({
463
+ error,
464
+ kind: "render-failed",
465
+ target
466
+ });
467
+ if (count < after || state.get().has(target)) return;
468
+ state.set(new Map([...state.get(), [target, {
469
+ error,
470
+ target
471
+ }]]));
472
+ report({
473
+ error,
474
+ kind: "quarantined",
475
+ target
476
+ });
477
+ },
478
+ get: state.get,
479
+ rendered: (target) => {
480
+ failures.delete(target);
481
+ },
482
+ retry: (target) => {
483
+ failures.delete(target);
484
+ if (!state.get().has(target)) return;
485
+ state.set(new Map([...state.get()].filter(([quarantined]) => quarantined !== target)));
486
+ },
487
+ subscribe: state.subscribe
488
+ };
489
+ }
490
+ //#endregion
491
+ //#region src/stores/reports.ts
492
+ /**
493
+ * Returns the report store, empty.
494
+ */
495
+ function createReportStore() {
496
+ const state = writable([]);
497
+ return {
498
+ add: (entry) => {
499
+ state.set([...state.get(), entry].slice(-100));
500
+ },
501
+ get: state.get,
502
+ subscribe: state.subscribe
503
+ };
504
+ }
505
+ //#endregion
506
+ //#region src/host/page.ts
507
+ /**
508
+ * Failed renders in a row after which a target is quarantined, where the product states none.
509
+ */
510
+ const QUARANTINE_AFTER = 3;
511
+ /**
512
+ * The prefix of every line the host writes to the console.
513
+ */
514
+ const PREFIX = "[host]";
515
+ /**
516
+ * Writes an entry to the console: as an error where the entry has an error, else as a warning.
517
+ */
518
+ function toConsole(entry) {
519
+ if ("error" in entry) console.error(`${PREFIX} ${entry.kind}`, entry);
520
+ else console.warn(`${PREFIX} ${entry.kind}`, entry);
521
+ }
522
+ /**
523
+ * Returns the page's stores, empty, and the function that reports an entry into them.
524
+ */
525
+ function createPage({ quarantineAfter = QUARANTINE_AFTER, report: receiver = toConsole }) {
526
+ const reports = createReportStore();
527
+ /**
528
+ * Adds an entry to the reports store and passes it to the receiver.
529
+ */
530
+ const report = (entry) => {
531
+ reports.add(entry);
532
+ receiver(entry);
533
+ };
534
+ return {
535
+ report,
536
+ stores: {
537
+ mounted: createMountedStore(),
538
+ pages: createPageStore(),
539
+ quarantine: createQuarantineStore({
540
+ after: quarantineAfter,
541
+ report
542
+ }),
543
+ reports
544
+ }
545
+ };
546
+ }
547
+ //#endregion
548
+ //#region src/host/ready.ts
549
+ /**
550
+ * Returns the values of a record a manifest may leave out.
551
+ */
552
+ function valuesOf(record) {
553
+ return record === void 0 ? [] : Object.values(record);
554
+ }
555
+ /**
556
+ * Returns every importer a plugin's code lists: its pages, extensions, commands and sections, and
557
+ * their fallbacks.
558
+ */
559
+ function importersOf(code) {
560
+ return [
561
+ ...valuesOf(code.routes).flatMap((route) => typeof route === "function" ? [route] : [route.component, route.fallback]),
562
+ ...valuesOf(code.extensions).flatMap(({ component, fallback }) => [component, fallback]),
563
+ ...valuesOf(code.commands).map(({ run }) => run),
564
+ ...valuesOf(code.settings).map(({ component }) => component)
565
+ ].filter((importer) => importer !== void 0);
566
+ }
567
+ /**
568
+ * Imports every module of the plugins named, and resolves once every import settled.
569
+ *
570
+ * @remarks
571
+ * A failed import does not fail the caller. The page or the extension whose module failed reports
572
+ * the failure when it renders.
573
+ * @param manifests - Each installed plugin's manifest, by plugin id.
574
+ * @param pluginIds - Ids of the plugins whose modules to import.
575
+ */
576
+ async function importPlugins(manifests, pluginIds) {
577
+ const importers = Object.entries(manifests).filter(([pluginId]) => pluginIds.has(pluginId)).flatMap(([, manifest]) => importersOf(manifest.code));
578
+ await Promise.allSettled(importers.map((load) => load()));
579
+ }
580
+ /**
581
+ * Imports the modules of every eager plugin, and resolves once every import settled.
582
+ */
583
+ async function importEager(product) {
584
+ const eager = product.plugins.filter((plugin) => plugin.eager).map(({ id }) => id);
585
+ await importPlugins(product.manifests, new Set(eager));
586
+ }
587
+ /**
588
+ * Resolves once a promise settles, or once the milliseconds pass, whichever comes first.
589
+ */
590
+ function within(promise, milliseconds) {
591
+ return new Promise((resolve) => {
592
+ const timer = setTimeout(resolve, milliseconds);
593
+ /**
594
+ * Stops the timer and resolves.
595
+ */
596
+ const settle = () => {
597
+ clearTimeout(timer);
598
+ resolve();
599
+ };
600
+ promise.then(settle, settle);
601
+ });
602
+ }
603
+ /**
604
+ * Resolves once the eager imports settled and the flag source identified the session, or once the
605
+ * timeout passed for the identification.
606
+ *
607
+ * @param eager - The eager plugins' imports.
608
+ * @param identified - The flag source's identification of the first session.
609
+ * @param timeout - Milliseconds to wait at most for the identification.
610
+ */
611
+ async function readied(eager, identified, timeout) {
612
+ await Promise.all([eager, within(identified, timeout)]);
613
+ }
614
+ //#endregion
615
+ //#region src/commands/registry.ts
616
+ /**
617
+ * Returns the one function a command's module exports.
618
+ *
619
+ * @throws {@link Error} Where the module exports no function or more than one.
620
+ */
621
+ function functionOf(module, commandId) {
622
+ const found = Object.values(module).filter((value) => typeof value === "function");
623
+ if (found.length !== 1) throw new Error(`The module of ${commandId} exports ${String(found.length)} functions, and a command's module exports one.`);
624
+ return found[0];
625
+ }
626
+ /**
627
+ * Returns the function that runs a command by its qualified id with its arguments.
628
+ *
629
+ * @remarks
630
+ * A run rejects where no installed plugin declares the command, and where its plugin is off or
631
+ * its condition is false at the moment of the run, so a control rendered before a permission was
632
+ * revoked cannot run it. The module of each importer is imported once. An import that fails is
633
+ * forgotten, so the next run imports again. Each need is a function that runs the needed command
634
+ * through the same checks. The call runs inside the `stealth:command:<id>` performance measure.
635
+ * A run that is in progress when its plugin turns off completes.
636
+ */
637
+ function createCommandRegistry({ hostOf, matched, product, stores }) {
638
+ const imports = /* @__PURE__ */ new Map();
639
+ /**
640
+ * Returns the function of a command's module, imported once per importer.
641
+ */
642
+ const imported = (command) => {
643
+ const entry = (product.manifests[command.plugin]?.code.commands ?? {})[command.id.slice(command.plugin.length + 1)];
644
+ if (entry === void 0) return Promise.reject(/* @__PURE__ */ new Error(`No manifest maps the command ${command.id} to code.`));
645
+ const known = imports.get(entry.run);
646
+ if (known !== void 0) return known;
647
+ const loading = entry.run().then((module) => functionOf(module, command.id));
648
+ imports.set(entry.run, loading);
649
+ loading.catch(() => {
650
+ imports.delete(entry.run);
651
+ });
652
+ return loading;
653
+ };
654
+ /**
655
+ * Runs a command by its qualified id, and resolves with its result.
656
+ */
657
+ const run = async (commandId, args) => {
658
+ const command = product.commands.find(({ id }) => id === commandId);
659
+ if (command === void 0) throw new Error(`No installed plugin declares the command ${commandId}.`);
660
+ if (!isCommandEnabled(command, stores, matched())) throw new Error(`The command ${commandId} cannot run: its condition is false.`);
661
+ const call = await imported(command);
662
+ const needs = Object.fromEntries(Object.entries(command.needs).map(([name, needed]) => [name, (given) => run(needed, given)]));
663
+ const start = performance.now();
664
+ try {
665
+ return await call(args, needs, hostOf(command.plugin));
666
+ } finally {
667
+ performance.measure(`stealth:command:${commandId}`, { start });
668
+ }
669
+ };
670
+ return run;
671
+ }
672
+ //#endregion
673
+ //#region src/host/api.ts
674
+ /**
675
+ * Builds the API a command's function receives beside its arguments: the host as the command's
676
+ * plugin sees it at the time of the run.
677
+ */
678
+ /**
679
+ * The error of a navigation before `HostProvider` connected the router.
680
+ */
681
+ const UNCONNECTED = "The host is not connected to a router. HostProvider connects one when it renders.";
682
+ /**
683
+ * Returns the functions that run declared queries and mutations through the host's data client.
684
+ */
685
+ function dataOf(product, client) {
686
+ return {
687
+ mutate: (mutation, variables) => {
688
+ const declared = product.mutations.find(({ id }) => id === mutation.id);
689
+ if (declared === void 0) return Promise.reject(/* @__PURE__ */ new Error(`No installed plugin declares the mutation ${mutation.id}.`));
690
+ return mutateOperation(client, defineMutation(declared.operation.id), variables, { changes: (given) => changesOf(declared.changes, given) });
691
+ },
692
+ query: (query, variables) => {
693
+ const declared = product.queries.find(({ id }) => id === query.id);
694
+ if (declared === void 0) return Promise.reject(/* @__PURE__ */ new Error(`No installed plugin declares the query ${query.id}.`));
695
+ return client.query(operationQuery(defineQuery(declared.operation.id), variables, {
696
+ resources: declared.records,
697
+ staleTime: declared.staleTime
698
+ }));
699
+ }
700
+ };
701
+ }
702
+ /**
703
+ * Resolves with the decision on one resource once the stores state one, and asks the access source
704
+ * for it meanwhile.
705
+ *
706
+ * @remarks
707
+ * Every change of the stores that leaves the check pending asks for it again, so a check the host
708
+ * dropped on a change of subject is asked for again.
709
+ */
710
+ function decided(stores, check) {
711
+ return new Promise((resolve) => {
712
+ const stops = [];
713
+ /**
714
+ * Resolves with the decision where the stores state one, and asks for it otherwise.
715
+ */
716
+ const settle = () => {
717
+ const decision = decisionOf(stores.access.get(), stores.session.get(), check);
718
+ if (decision === "pending") {
719
+ stores.access.request(check);
720
+ return;
721
+ }
722
+ for (const stop of stops) stop();
723
+ resolve(decision === "allowed");
724
+ };
725
+ stops.push(stores.access.subscribe(settle), stores.session.subscribe(settle));
726
+ settle();
727
+ });
728
+ }
729
+ /**
730
+ * Resolves whether the person has a permission: on the resource where a resource id is given for a
731
+ * scoped permission, and for the tenant otherwise.
732
+ */
733
+ function allowed(stores, product, permission, resourceId) {
734
+ const type = product.permissions.find(({ id }) => id === permission)?.resource;
735
+ if (resourceId === void 0 || type === void 0) return Promise.resolve(stores.session.get().permissions.has(permission));
736
+ return decided(stores, {
737
+ permission,
738
+ resource: {
739
+ id: resourceId,
740
+ type
741
+ }
742
+ });
743
+ }
744
+ /**
745
+ * Returns the function that builds a plugin's `HostApi` at the time of a run.
746
+ *
747
+ * @remarks
748
+ * The session, the connection and the deepest matched route are read when the function is
749
+ * called, so each run sees the page as it is then. Before `HostProvider` connects the router,
750
+ * `matched` is undefined, `navigate` rejects, and `t` returns the key.
751
+ */
752
+ function hostApiOf({ connection, data, events, product, stores, toaster }) {
753
+ const hostData = dataOf(product, data);
754
+ return (pluginId) => {
755
+ const connected = connection();
756
+ return {
757
+ can: (permission, resourceId) => allowed(stores, product, permission.id, resourceId),
758
+ data: hostData,
759
+ emit: (event, payload) => {
760
+ events.emit(pluginId, event.id, payload);
761
+ },
762
+ flag: (flag) => stores.flags.read(flag.id) ?? flag.default ?? false,
763
+ matched: connected?.matched().at(-1),
764
+ navigate: (to, options) => connected === void 0 ? Promise.reject(/* @__PURE__ */ new Error(UNCONNECTED)) : connected.navigate(to, options),
765
+ pluginId,
766
+ session: stores.session.get().session,
767
+ t: (key, values) => connected?.translate(pluginId, key, values) ?? key,
768
+ toaster
769
+ };
770
+ };
771
+ }
772
+ //#endregion
773
+ //#region src/recovery/stale.ts
774
+ /**
775
+ * Records the version under the key in the tab's session storage, and returns true where it did.
776
+ *
777
+ * @returns False where the key contains the version already, or where the browser refuses the
778
+ * storage.
779
+ */
780
+ function recordReload(key, version) {
781
+ try {
782
+ if (window.sessionStorage.getItem(key) === version) return false;
783
+ window.sessionStorage.setItem(key, version);
784
+ return true;
785
+ } catch {
786
+ return false;
787
+ }
788
+ }
789
+ /**
790
+ * Returns the function that reloads the page after a module failed to import, once per build
791
+ * version, and returns true while the page reloads.
792
+ *
793
+ * @remarks
794
+ * The function records the product's version under `stealth.<productId>.reloaded` before it
795
+ * reloads. For a version recorded before, it returns false, so a chunk the new build lacks as
796
+ * well renders the error component instead of a second reload. A server has no page to reload,
797
+ * and a page whose storage the browser refuses cannot record the version, so both return false.
798
+ * @param product - The product: its id names the storage key, and its version is the build's.
799
+ */
800
+ function staleRecovery({ productId, version }) {
801
+ const key = `stealth.${productId}.reloaded`;
802
+ let reloading = false;
803
+ return () => {
804
+ if (reloading) return true;
805
+ if (typeof window === "undefined" || !recordReload(key, version)) return false;
806
+ reloading = true;
807
+ window.location.reload();
808
+ return true;
809
+ };
810
+ }
811
+ /**
812
+ * Listens for Vite's `vite:preloadError` event until the returned function is called, and cancels
813
+ * the event where the page reloads.
814
+ *
815
+ * @remarks
816
+ * Vite's preload helper dispatches the cancelable event on the window for a stylesheet that
817
+ * failed to preload and for a module that failed to import. A cancelled event makes the import
818
+ * resolve with no module, which the host's importers keep pending while the page reloads. An
819
+ * event the listener does not cancel rejects the import.
820
+ * @param recover - Reloads the page once per build version, and returns true while it reloads.
821
+ */
822
+ function listenForStaleChunks(recover) {
823
+ /**
824
+ * Reloads the page for the failed import, and cancels the event where it does.
825
+ */
826
+ const listener = (event) => {
827
+ if (recover()) event.preventDefault();
828
+ };
829
+ window.addEventListener("vite:preloadError", listener);
830
+ return () => {
831
+ window.removeEventListener("vite:preloadError", listener);
832
+ };
833
+ }
834
+ //#endregion
835
+ //#region src/recovery/instrument.ts
836
+ /**
837
+ * The instrumented form of each product the host received, so one product object shares one
838
+ * recovery and one measure per plugin.
839
+ */
840
+ const INSTRUMENTED = /* @__PURE__ */ new WeakMap();
841
+ /**
842
+ * Returns a promise that never settles, for an import whose page reloads.
843
+ */
844
+ function reloading() {
845
+ return new Promise(() => {});
846
+ }
847
+ /**
848
+ * Returns the module an import resolved with.
849
+ *
850
+ * @throws {@link Error} Where the import resolved with no module, as Vite's preload helper resolves
851
+ * an import whose `vite:preloadError` event a listener cancelled.
852
+ */
853
+ function moduleOf(module, pluginId) {
854
+ if (module === void 0) throw new Error(`An import of a module of ${pluginId} resolved with no module.`);
855
+ return module;
856
+ }
857
+ /**
858
+ * Imports a plugin's module through its importer, measures the plugin's first import, and reloads
859
+ * the page where the import fails.
860
+ *
861
+ * @returns The module, or a promise that never settles while the page reloads.
862
+ * @throws The import's error where the page does not reload.
863
+ */
864
+ async function imported(importer, pluginId, loading) {
865
+ const start = loading.startOf(pluginId);
866
+ let module;
867
+ try {
868
+ module = moduleOf(await importer(), pluginId);
869
+ } catch (error) {
870
+ if (loading.recover()) return reloading();
871
+ throw error;
872
+ }
873
+ if (start !== void 0) performance.measure(`stealth:load:${pluginId}`, { start });
874
+ return module;
875
+ }
876
+ /**
877
+ * Returns the importer wrapped, or undefined where a manifest states none.
878
+ */
879
+ function maybe(importer, wrap) {
880
+ return importer === void 0 ? void 0 : wrap(importer);
881
+ }
882
+ /**
883
+ * Returns a record with each value mapped, or undefined where a manifest leaves the record out.
884
+ */
885
+ function mapped(record, map) {
886
+ return record === void 0 ? void 0 : Object.fromEntries(Object.entries(record).map(([name, value]) => [name, map(value)]));
887
+ }
888
+ /**
889
+ * Returns a plugin's code with every importer wrapped: pages, fallbacks, extensions, commands and
890
+ * settings sections.
891
+ */
892
+ function codeOf(code, wrap) {
893
+ return {
894
+ commands: mapped(code.commands, (entry) => ({
895
+ ...entry,
896
+ run: wrap(entry.run)
897
+ })),
898
+ extensions: mapped(code.extensions, (entry) => ({
899
+ component: wrap(entry.component),
900
+ fallback: maybe(entry.fallback, wrap)
901
+ })),
902
+ routes: mapped(code.routes, (route) => typeof route === "function" ? wrap(route) : {
903
+ component: wrap(route.component),
904
+ fallback: maybe(route.fallback, wrap)
905
+ }),
906
+ settings: mapped(code.settings, (entry) => ({
907
+ ...entry,
908
+ component: maybe(entry.component, wrap)
909
+ }))
910
+ };
911
+ }
912
+ /**
913
+ * Returns what the importers of one product share.
914
+ */
915
+ function loadingOf(product) {
916
+ const started = /* @__PURE__ */ new Set();
917
+ return {
918
+ recover: staleRecovery(product),
919
+ startOf: (pluginId) => {
920
+ const first = !started.has(pluginId);
921
+ started.add(pluginId);
922
+ return first ? performance.now() : void 0;
923
+ }
924
+ };
925
+ }
926
+ /**
927
+ * Returns the product with every importer of its manifests wrapped, once per product object.
928
+ *
929
+ * @remarks
930
+ * A wrapped importer measures its plugin's first import as `stealth:load:<plugin id>`, from the
931
+ * moment the import starts until the module resolves. Where an import rejects or resolves with no
932
+ * module, the page reloads once per build version, and the import never settles. A second failure
933
+ * for the same version rejects, and the route's error component or the extension's boundary
934
+ * renders it. `createRuntime` and `createHostRoutes` both wrap the product they receive, so a
935
+ * product that passes one object to `createHost` and `createHostRoutes` shares one recovery and
936
+ * one measure per plugin.
937
+ * @param product - The product, with each installed plugin's manifest.
938
+ */
939
+ function instrumented(product) {
940
+ const known = INSTRUMENTED.get(product);
941
+ if (known !== void 0) return known;
942
+ const loading = loadingOf(product);
943
+ const manifests = Object.fromEntries(Object.entries(product.manifests).map(([pluginId, manifest]) => [pluginId, {
944
+ ...manifest,
945
+ code: codeOf(manifest.code, (importer) => () => imported(importer, pluginId, loading))
946
+ }]));
947
+ const result = {
948
+ product: {
949
+ ...product,
950
+ manifests
951
+ },
952
+ recover: loading.recover
953
+ };
954
+ INSTRUMENTED.set(product, result);
955
+ return result;
956
+ }
957
+ //#endregion
958
+ //#region src/host/runtime.ts
959
+ /**
960
+ * Builds the runtime the hooks of `sdk-plugin` read: the product, the stores, the bus, the toaster,
961
+ * the setting store, the report and the command registry.
962
+ */
963
+ /**
964
+ * Returns the runtime of one host, with the product's one toaster.
965
+ *
966
+ * @remarks
967
+ * The runtime's product is the given product with its manifests' importers wrapped by
968
+ * `instrumented`, so every import of a plugin's module, an extension's included, is measured and
969
+ * recovers from a chunk that no longer exists. The toaster raises toasts at the end of the
970
+ * window's bottom edge, five at a time, overlapping until a pointer rests on them. A command runs
971
+ * with the routes matched at the moment of its run, and with no route before `HostProvider`
972
+ * connects the router.
973
+ */
974
+ function createRuntime({ connection, data, events, product, report, settings, stores }) {
975
+ const { product: loaded, recover } = instrumented(product);
976
+ const toaster = Toast.createToaster({
977
+ max: 5,
978
+ overlap: true,
979
+ placement: "bottom-end"
980
+ });
981
+ return {
982
+ events,
983
+ product: loaded,
984
+ recover,
985
+ report,
986
+ run: createCommandRegistry({
987
+ hostOf: hostApiOf({
988
+ connection,
989
+ data,
990
+ events,
991
+ product: loaded,
992
+ stores,
993
+ toaster
994
+ }),
995
+ matched: () => {
996
+ const connected = connection();
997
+ return connected === void 0 ? void 0 : new Set(connected.matched());
998
+ },
999
+ product: loaded,
1000
+ stores
1001
+ }),
1002
+ settings,
1003
+ stores,
1004
+ toaster
1005
+ };
1006
+ }
1007
+ //#endregion
1008
+ //#region src/host/sequence.ts
1009
+ /**
1010
+ * Runs the host's side of a change of session: the `host/sessionChanged` event, the reset of the
1011
+ * previous subject's data and decisions, and the flag source's identification.
1012
+ */
1013
+ /**
1014
+ * Emits the session as the sticky `host/sessionChanged` now and after every change, and identifies
1015
+ * each session to the flag source.
1016
+ *
1017
+ * @remarks
1018
+ * The switch and placement stores move to a new subject's keys before the event, because they
1019
+ * subscribed to the session first. A change of subject then resets the data client and clears
1020
+ * the access store, so a subject renders no data and no decision of the subject before it. The
1021
+ * flags keep their values while the flag source identifies the new session.
1022
+ */
1023
+ function followSession({ access, data, events, flags, session }) {
1024
+ let subject = subjectOf(session.get().session);
1025
+ /**
1026
+ * Emits the session as the host.
1027
+ */
1028
+ const announce = (current) => {
1029
+ events.emit(HOST, hostContract.events.sessionChanged.id, current);
1030
+ };
1031
+ announce(session.get().session);
1032
+ return {
1033
+ identified: flags.identify(session.get().session),
1034
+ stop: session.subscribe(() => {
1035
+ const current = session.get().session;
1036
+ const next = subjectOf(current);
1037
+ announce(current);
1038
+ if (next !== subject) {
1039
+ subject = next;
1040
+ resetData(data);
1041
+ access.clear();
1042
+ }
1043
+ flags.identify(current);
1044
+ })
1045
+ };
1046
+ }
1047
+ //#endregion
1048
+ //#region src/stores/access.ts
1049
+ /**
1050
+ * Milliseconds the checks of a failed batch read as denied before the next read asks again.
1051
+ */
1052
+ const DENIED_FOR = 3e4;
1053
+ /**
1054
+ * Returns the start every decision key on one resource shares.
1055
+ */
1056
+ function resourcePrefix({ id, type }) {
1057
+ return `${JSON.stringify([type, id]).slice(0, -1)},`;
1058
+ }
1059
+ /**
1060
+ * Returns the store of decisions, empty.
1061
+ */
1062
+ function decisionsOf(source) {
1063
+ const state = writable({
1064
+ decisions: /* @__PURE__ */ new Map(),
1065
+ source
1066
+ });
1067
+ const timers = /* @__PURE__ */ new Set();
1068
+ /**
1069
+ * Writes decisions over the known ones.
1070
+ */
1071
+ const decide = (entries) => {
1072
+ state.set({
1073
+ ...state.get(),
1074
+ decisions: new Map([...state.get().decisions, ...entries])
1075
+ });
1076
+ };
1077
+ /**
1078
+ * Removes the decisions whose keys a predicate accepts.
1079
+ */
1080
+ const remove = (accepts) => {
1081
+ const kept = [...state.get().decisions].filter(([key]) => !accepts(key));
1082
+ if (kept.length !== state.get().decisions.size) state.set({
1083
+ ...state.get(),
1084
+ decisions: new Map(kept)
1085
+ });
1086
+ };
1087
+ return {
1088
+ cancel: () => {
1089
+ for (const timer of timers) clearTimeout(timer);
1090
+ timers.clear();
1091
+ },
1092
+ decide,
1093
+ deny: (keys) => {
1094
+ decide(keys.map((key) => [key, false]));
1095
+ const timer = setTimeout(() => {
1096
+ timers.delete(timer);
1097
+ remove((key) => keys.includes(key) && state.get().decisions.get(key) === false);
1098
+ }, DENIED_FOR);
1099
+ timers.add(timer);
1100
+ },
1101
+ remove,
1102
+ state
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Returns the batches sent to the source, each settled through one of the two callbacks.
1107
+ *
1108
+ * @param source - The product's access source.
1109
+ * @param decided - Receives a batch's keys and the decision of each.
1110
+ * @param failed - Receives a failed batch's keys and the error.
1111
+ */
1112
+ function batchesOf(source, decided, failed) {
1113
+ const queued = /* @__PURE__ */ new Map();
1114
+ const flying = /* @__PURE__ */ new Map();
1115
+ let generation = 0;
1116
+ /**
1117
+ * Sends the queued checks in one call, and nothing where none is queued.
1118
+ */
1119
+ const flush = async () => {
1120
+ const batch = [...queued];
1121
+ const keys = batch.map(([key]) => key);
1122
+ const started = generation;
1123
+ if (batch.length === 0) return;
1124
+ queued.clear();
1125
+ for (const [key, check] of batch) flying.set(key, check);
1126
+ try {
1127
+ const allowed = await source.check(batch.map(([, check]) => check));
1128
+ if (allowed.length !== keys.length) throw new Error(`The access source decided ${String(allowed.length)} of ${String(keys.length)} checks.`);
1129
+ if (started === generation) decided(keys, allowed);
1130
+ } catch (error) {
1131
+ if (started === generation) failed(keys, error);
1132
+ } finally {
1133
+ if (started === generation) for (const key of keys) flying.delete(key);
1134
+ }
1135
+ };
1136
+ /**
1137
+ * Queues a check, and sends the queue at the end of the task.
1138
+ */
1139
+ const queue = (key, check) => {
1140
+ if (queued.size === 0) queueMicrotask(() => {
1141
+ flush();
1142
+ });
1143
+ queued.set(key, check);
1144
+ };
1145
+ return {
1146
+ clear: () => {
1147
+ const waiting = [...queued, ...flying];
1148
+ generation += 1;
1149
+ queued.clear();
1150
+ flying.clear();
1151
+ for (const [key, check] of waiting) queue(key, check);
1152
+ },
1153
+ has: (key) => queued.has(key) || flying.has(key),
1154
+ queue
1155
+ };
1156
+ }
1157
+ /**
1158
+ * Returns the access store over the product's access source.
1159
+ *
1160
+ * @remarks
1161
+ * Every check requested in one task goes to `check` in one call, with a known decision, a queued
1162
+ * check and a check in flight left out. A batch that rejects, or resolves with a list of another
1163
+ * length, denies its checks for 30 seconds and reports `access-failed`. A change of subject, or a
1164
+ * notification from the source, clears the store. A batch still in flight then writes nothing,
1165
+ * and its checks go to the source again, because their readers keep reading them as pending and
1166
+ * would not ask again. A primed decision for a permission no installed plugin declares is
1167
+ * ignored.
1168
+ */
1169
+ function createAccessStore({ product, report, source }) {
1170
+ const declared = new Set(product.permissions.map(({ id }) => id));
1171
+ const { cancel, decide, deny, remove, state } = decisionsOf(source !== void 0);
1172
+ const batches = source === void 0 ? void 0 : batchesOf(source, (keys, allowed) => {
1173
+ decide(keys.map((key, index) => [key, allowed[index] === true]));
1174
+ }, (keys, error) => {
1175
+ deny(keys);
1176
+ report({
1177
+ error,
1178
+ kind: "access-failed"
1179
+ });
1180
+ });
1181
+ /**
1182
+ * Drops every decision, every queued check and every batch in flight.
1183
+ */
1184
+ const clear = () => {
1185
+ batches?.clear();
1186
+ cancel();
1187
+ remove(() => true);
1188
+ };
1189
+ const stop = source?.subscribe?.(clear);
1190
+ return {
1191
+ clear,
1192
+ dispose: () => {
1193
+ stop?.();
1194
+ cancel();
1195
+ },
1196
+ forget: (resource) => {
1197
+ const prefix = resource === void 0 ? "" : resourcePrefix(resource);
1198
+ remove((key) => key.startsWith(prefix));
1199
+ },
1200
+ get: state.get,
1201
+ hydrate: decide,
1202
+ prime: (decisions) => {
1203
+ decide(decisions.filter(({ permission }) => declared.has(permission)).map((decision) => [decisionKey(decision), decision.allowed]));
1204
+ },
1205
+ request: (check) => {
1206
+ const key = decisionKey(check);
1207
+ if (batches === void 0 || state.get().decisions.has(key) || batches.has(key)) return;
1208
+ batches.queue(key, check);
1209
+ },
1210
+ subscribe: state.subscribe
1211
+ };
1212
+ }
1213
+ //#endregion
1214
+ //#region src/stores/availability.ts
1215
+ /**
1216
+ * Computes whether each installed plugin is on, and why it is not, from its kill switch, its
1217
+ * switch, its condition and the plugins it requires.
1218
+ */
1219
+ /**
1220
+ * The availability of a plugin that passes every test.
1221
+ */
1222
+ const ON = { on: true };
1223
+ /**
1224
+ * Returns every installed plugin's availability, in install order.
1225
+ *
1226
+ * @remarks
1227
+ * A plugin is computed on first need, so it is computed after every plugin its condition names
1228
+ * and every plugin it requires, whatever the install order. A plugin on a ring of conditions and
1229
+ * requirements, which the build refuses, reads as off to the plugin that asks.
1230
+ */
1231
+ function availabilityOf({ availability, flags, plugins, session, switches }) {
1232
+ const installed = new Map(plugins.map((plugin) => [plugin.id, plugin]));
1233
+ const known = /* @__PURE__ */ new Map();
1234
+ const computing = /* @__PURE__ */ new Set();
1235
+ /**
1236
+ * Returns true where a plugin is installed, not being computed, and on.
1237
+ */
1238
+ const on = (pluginId) => {
1239
+ const plugin = installed.get(pluginId);
1240
+ return plugin !== void 0 && !computing.has(pluginId) && computed(plugin).on;
1241
+ };
1242
+ const context = {
1243
+ ...conditionContextOf({
1244
+ availability,
1245
+ flags,
1246
+ session
1247
+ }),
1248
+ on
1249
+ };
1250
+ /**
1251
+ * Returns a plugin's availability: the reason of the first test it fails, else on.
1252
+ */
1253
+ const tested = (plugin) => {
1254
+ if (flags.read(plugin.killSwitch) === false) return {
1255
+ on: false,
1256
+ reason: "unavailable"
1257
+ };
1258
+ if (!plugin.locked && switches[plugin.id] === false) return {
1259
+ on: false,
1260
+ reason: "off"
1261
+ };
1262
+ if (!evaluateWhen(plugin.when, context)) return {
1263
+ on: false,
1264
+ reason: "condition"
1265
+ };
1266
+ if (plugin.requires.some(({ optional, pluginId }) => optional !== true && !on(pluginId))) return {
1267
+ on: false,
1268
+ reason: "requirement"
1269
+ };
1270
+ return ON;
1271
+ };
1272
+ /**
1273
+ * Returns a plugin's availability, computed once per computation.
1274
+ */
1275
+ const computed = (plugin) => {
1276
+ const done = known.get(plugin.id);
1277
+ if (done !== void 0) return done;
1278
+ computing.add(plugin.id);
1279
+ const state = tested(plugin);
1280
+ computing.delete(plugin.id);
1281
+ known.set(plugin.id, state);
1282
+ return state;
1283
+ };
1284
+ return plugins.map((plugin) => [plugin.id, computed(plugin)]);
1285
+ }
1286
+ /**
1287
+ * Returns the availability store, computed now and again whenever the session, a flag or a switch
1288
+ * changes.
1289
+ *
1290
+ * @remarks
1291
+ * The tests run in this order, and the first a plugin fails gives its reason: its kill switch is
1292
+ * on (`unavailable`), it is locked or switched on (`off`), its condition is true (`condition`),
1293
+ * and every plugin it requires without `optional` is on (`requirement`). A computation that
1294
+ * changes no plugin's state publishes nothing. A plugin whose reason changes while it remains
1295
+ * off is published without a call to `changed`.
1296
+ */
1297
+ function createAvailabilityStore({ changed, flags, product, session, switches }) {
1298
+ const availability = writable({});
1299
+ /**
1300
+ * Returns every plugin's availability as the stores are now.
1301
+ */
1302
+ const compute = () => availabilityOf({
1303
+ availability,
1304
+ flags,
1305
+ plugins: product.plugins,
1306
+ session,
1307
+ switches: switches.get()
1308
+ });
1309
+ const first = compute();
1310
+ let states = new Map(first.map(([pluginId, state]) => [pluginId, state.on]));
1311
+ availability.set(Object.fromEntries(first));
1312
+ /**
1313
+ * Computes every plugin's availability again, publishes it where it changed, and passes each
1314
+ * plugin that turned on or off to `changed`.
1315
+ */
1316
+ const recompute = () => {
1317
+ const entries = compute();
1318
+ const next = Object.fromEntries(entries);
1319
+ if (JSON.stringify(next) === JSON.stringify(availability.get())) return;
1320
+ const before = states;
1321
+ states = new Map(entries.map(([pluginId, state]) => [pluginId, state.on]));
1322
+ availability.set(next);
1323
+ for (const [pluginId, state] of entries) if (before.get(pluginId) !== state.on) changed({
1324
+ ...state,
1325
+ pluginId
1326
+ });
1327
+ };
1328
+ const stops = [
1329
+ session.subscribe(recompute),
1330
+ flags.subscribe(recompute),
1331
+ switches.subscribe(recompute)
1332
+ ];
1333
+ return {
1334
+ dispose: () => {
1335
+ for (const stop of stops) stop();
1336
+ },
1337
+ get: availability.get,
1338
+ subscribe: availability.subscribe
1339
+ };
1340
+ }
1341
+ //#endregion
1342
+ //#region src/stores/flag-session.ts
1343
+ /**
1344
+ * Returns the record of the variants served, for no subject yet.
1345
+ *
1346
+ * @param queue - Receives `flag-exposed` for each variant the subject is served first.
1347
+ */
1348
+ function exposuresOf(queue) {
1349
+ const served = /* @__PURE__ */ new Set();
1350
+ let current;
1351
+ return {
1352
+ expose: (flag, value) => {
1353
+ if (typeof value !== "string") return;
1354
+ const key = JSON.stringify([flag.id, value]);
1355
+ if (served.has(key)) return;
1356
+ served.add(key);
1357
+ queue({
1358
+ flag: flag.id,
1359
+ kind: "flag-exposed",
1360
+ variant: value
1361
+ });
1362
+ },
1363
+ reset: (subject) => {
1364
+ if (subject === current) return;
1365
+ current = subject;
1366
+ served.clear();
1367
+ },
1368
+ serve: (subject, flag, value) => {
1369
+ if (subject === current) served.add(JSON.stringify([flag.id, value]));
1370
+ }
1371
+ };
1372
+ }
1373
+ /**
1374
+ * Returns whom the flag source evaluates for: every session for a source without `identify`, and no
1375
+ * session until the first `identify` resolves for a source with it.
1376
+ *
1377
+ * @param source - The product's flag source.
1378
+ * @param failed - Receives the error of an `identify` that rejected and was not overtaken.
1379
+ */
1380
+ function identityOf(source, failed) {
1381
+ let identified = source?.identify === void 0;
1382
+ let generation = 0;
1383
+ return {
1384
+ identify: async (session) => {
1385
+ if (source?.identify === void 0) return true;
1386
+ generation += 1;
1387
+ identified = false;
1388
+ const started = generation;
1389
+ try {
1390
+ await source.identify(session);
1391
+ } catch (error) {
1392
+ if (started === generation) failed(error);
1393
+ return false;
1394
+ }
1395
+ if (started !== generation) return false;
1396
+ identified = true;
1397
+ return true;
1398
+ },
1399
+ stop: () => {
1400
+ generation += 1;
1401
+ },
1402
+ trusted: () => identified ? source : void 0
1403
+ };
1404
+ }
1405
+ //#endregion
1406
+ //#region src/stores/flags.ts
1407
+ /**
1408
+ * Keeps the flags the page reads for the session, each evaluated on its first read, and the
1409
+ * overrides of this tab.
1410
+ *
1411
+ * @remarks
1412
+ * A flag takes the value of the first of these that states one fitting the flag: the tab's
1413
+ * override, the flag source, the product, the contract's default. A value that does not fit is
1414
+ * reported as `flag-ignored`, and the next one applies. The store evaluates a flag only when the
1415
+ * page reads it, so a flag service that records an exposure on evaluation records one only for a
1416
+ * variant the person saw.
1417
+ */
1418
+ /**
1419
+ * Returns true where a value fits a flag: a boolean for a release or an ops flag, one of its
1420
+ * variants for an experiment.
1421
+ */
1422
+ function fits(flag, value) {
1423
+ return flag.type === "boolean" ? typeof value === "boolean" : typeof value === "string" && flag.variants?.includes(value) === true;
1424
+ }
1425
+ /**
1426
+ * Returns the flag source's value for a flag, or undefined where the source states none or throws.
1427
+ * A throw queues `flags-failed`.
1428
+ */
1429
+ function stated(flag, source, queue) {
1430
+ if (source === void 0) return void 0;
1431
+ try {
1432
+ return source.evaluate({
1433
+ id: flag.id,
1434
+ type: flag.type
1435
+ });
1436
+ } catch (error) {
1437
+ queue({
1438
+ error,
1439
+ kind: "flags-failed"
1440
+ });
1441
+ return;
1442
+ }
1443
+ }
1444
+ /**
1445
+ * Returns the reading of the first of these that states a value fitting the flag: the flag source,
1446
+ * the product, the contract's default.
1447
+ *
1448
+ * @param flag - The declared flag.
1449
+ * @param source - The flag source, or undefined while it evaluates for no identified session.
1450
+ * @param queue - Receives `flags-failed` for a source that throws, and `flag-ignored` for a value
1451
+ * that does not fit.
1452
+ */
1453
+ function evaluate(flag, source, queue) {
1454
+ const value = stated(flag, source, queue);
1455
+ if (fits(flag, value)) return {
1456
+ origin: "source",
1457
+ value
1458
+ };
1459
+ if (value !== void 0) queue({
1460
+ flag: flag.id,
1461
+ kind: "flag-ignored",
1462
+ source: "source",
1463
+ value
1464
+ });
1465
+ if (fits(flag, flag.product)) return {
1466
+ origin: "product",
1467
+ value: flag.product
1468
+ };
1469
+ if (flag.product !== void 0) queue({
1470
+ flag: flag.id,
1471
+ kind: "flag-ignored",
1472
+ source: "product",
1473
+ value: flag.product
1474
+ });
1475
+ return {
1476
+ origin: "contract",
1477
+ value: flag.default
1478
+ };
1479
+ }
1480
+ /**
1481
+ * Returns true where two maps contain the same reading objects.
1482
+ */
1483
+ function same(shown, taken) {
1484
+ return shown.size === taken.size && [...taken].every(([id, reading]) => shown.get(id) === reading);
1485
+ }
1486
+ /**
1487
+ * Returns the store's state, empty, and the queue of its reports.
1488
+ */
1489
+ function publishedOf(report) {
1490
+ const state = writable({
1491
+ overrides: {},
1492
+ readings: /* @__PURE__ */ new Map()
1493
+ });
1494
+ const readings = /* @__PURE__ */ new Map();
1495
+ const pending = [];
1496
+ let overrides = {};
1497
+ let scheduled = false;
1498
+ /**
1499
+ * Publishes the readings and the overrides where they changed, then passes each queued report on.
1500
+ */
1501
+ const flush = () => {
1502
+ const shown = state.get();
1503
+ scheduled = false;
1504
+ if (shown.overrides !== overrides || !same(shown.readings, readings)) state.set({
1505
+ overrides,
1506
+ readings: new Map(readings)
1507
+ });
1508
+ for (const entry of pending.splice(0)) report(entry);
1509
+ };
1510
+ return {
1511
+ flush,
1512
+ queue: (entry) => {
1513
+ pending.push(entry);
1514
+ },
1515
+ readings,
1516
+ schedule: () => {
1517
+ if (scheduled) return;
1518
+ scheduled = true;
1519
+ queueMicrotask(flush);
1520
+ },
1521
+ setOverrides: (next) => {
1522
+ overrides = next;
1523
+ },
1524
+ state
1525
+ };
1526
+ }
1527
+ /**
1528
+ * Returns true where a parsed value is an object whose members can be read by name.
1529
+ */
1530
+ function isRecord$1(value) {
1531
+ return typeof value === "object" && value !== null;
1532
+ }
1533
+ /**
1534
+ * Returns the object stored text contains, or an empty object for no text, for text that is not
1535
+ * JSON, and for JSON that is not an object.
1536
+ */
1537
+ function parsed(text) {
1538
+ if (text === null) return {};
1539
+ try {
1540
+ const value = JSON.parse(text);
1541
+ return isRecord$1(value) ? value : {};
1542
+ } catch {
1543
+ return {};
1544
+ }
1545
+ }
1546
+ /**
1547
+ * Returns the overrides the tab's storage contains that fit a declared flag, and queues
1548
+ * `flag-ignored` for each that does not fit. An override for a flag no installed plugin declares is
1549
+ * left out without a report.
1550
+ */
1551
+ function storedOverrides(storage, key, declared, queue) {
1552
+ const kept = [];
1553
+ for (const [id, value] of Object.entries(parsed(storage?.getItem(key) ?? null))) {
1554
+ const flag = declared.get(id);
1555
+ if (flag === void 0) continue;
1556
+ if (fits(flag, value)) kept.push([id, value]);
1557
+ else queue({
1558
+ flag: id,
1559
+ kind: "flag-ignored",
1560
+ source: "override",
1561
+ value
1562
+ });
1563
+ }
1564
+ return Object.fromEntries(kept);
1565
+ }
1566
+ /**
1567
+ * Writes the overrides to the tab's storage, or removes the key where none is left.
1568
+ *
1569
+ * @returns False where the storage refused the write. The overrides then last until the page
1570
+ * reloads.
1571
+ */
1572
+ function written(storage, key, overrides) {
1573
+ try {
1574
+ if (Object.keys(overrides).length === 0) storage.removeItem(key);
1575
+ else storage.setItem(key, JSON.stringify(overrides));
1576
+ return true;
1577
+ } catch {
1578
+ return false;
1579
+ }
1580
+ }
1581
+ /**
1582
+ * Returns the overrides with a flag's override replaced, or removed where the value is undefined.
1583
+ */
1584
+ function withOverride(overrides, id, value) {
1585
+ const kept = Object.entries(overrides).filter(([flag]) => flag !== id);
1586
+ return Object.fromEntries(value === void 0 ? kept : [...kept, [id, value]]);
1587
+ }
1588
+ /**
1589
+ * Returns the store's `read`: the tab's override, else the reading the page took, else a first
1590
+ * evaluation, published at the end of the task.
1591
+ */
1592
+ function readerOf({ declared, exposures, identity, published }) {
1593
+ /**
1594
+ * Evaluates a flag the page reads for the first time, and publishes the reading at the end of
1595
+ * the task.
1596
+ */
1597
+ const first = (flag) => {
1598
+ const reading = evaluate(flag, identity.trusted(), published.queue);
1599
+ published.readings.set(flag.id, reading);
1600
+ exposures.expose(flag, reading.value);
1601
+ published.schedule();
1602
+ return reading.value;
1603
+ };
1604
+ return (id) => {
1605
+ const overridden = published.state.get().overrides[id];
1606
+ if (overridden !== void 0) return overridden;
1607
+ const known = published.readings.get(id);
1608
+ if (known !== void 0) return known.value;
1609
+ const flag = declared.get(id);
1610
+ return flag === void 0 ? void 0 : first(flag);
1611
+ };
1612
+ }
1613
+ /**
1614
+ * Returns the store's `override`, which does nothing without the tab's storage. An override drops
1615
+ * the flag's reading, so the page evaluates the flag again once the override is removed.
1616
+ */
1617
+ function overriderOf({ declared, published }, storage, key) {
1618
+ return (id, value) => {
1619
+ const flag = declared.get(id);
1620
+ if (storage === void 0 || flag === void 0) return;
1621
+ if (value !== void 0 && !fits(flag, value)) published.queue({
1622
+ flag: id,
1623
+ kind: "flag-ignored",
1624
+ source: "override",
1625
+ value
1626
+ });
1627
+ else {
1628
+ const next = withOverride(published.state.get().overrides, id, value);
1629
+ published.readings.delete(id);
1630
+ published.setOverrides(next);
1631
+ written(storage, key, next);
1632
+ }
1633
+ published.flush();
1634
+ };
1635
+ }
1636
+ /**
1637
+ * Returns the function that evaluates again every flag the page has read, publishes the readings
1638
+ * that changed, and reports the variants the subject is served for the first time.
1639
+ */
1640
+ function reevaluatorOf({ exposures, identity, published }, flags) {
1641
+ return () => {
1642
+ for (const flag of flags) {
1643
+ const reading = published.readings.get(flag.id);
1644
+ if (reading === void 0) continue;
1645
+ const next = evaluate(flag, identity.trusted(), published.queue);
1646
+ if (next.value !== reading.value || next.origin !== reading.origin) published.readings.set(flag.id, next);
1647
+ exposures.expose(flag, next.value);
1648
+ }
1649
+ published.flush();
1650
+ };
1651
+ }
1652
+ /**
1653
+ * Returns a store's hydration: the server's readings until `settle`, with the tab's overrides set
1654
+ * aside until then.
1655
+ *
1656
+ * @remarks
1657
+ * A reading for a flag the browser's build does not declare, or whose value does not fit the
1658
+ * flag, is left out, as a server and a browser of two builds may differ.
1659
+ * @param shared - The declared flags, the exposures and the state the store's parts share.
1660
+ * @param reevaluate - Evaluates again every flag the page has read.
1661
+ * @param waiting - Returns true while the flag source has not identified the session.
1662
+ */
1663
+ function hydrationOf(shared, reevaluate, waiting) {
1664
+ const { declared, exposures, published } = shared;
1665
+ let active = false;
1666
+ let aside = {};
1667
+ return {
1668
+ active: () => active,
1669
+ hydrate: (readings, subject) => {
1670
+ active = true;
1671
+ aside = published.state.get().overrides;
1672
+ published.setOverrides({});
1673
+ for (const [id, reading] of readings) {
1674
+ const flag = declared.get(id);
1675
+ if (flag === void 0 || !fits(flag, reading.value)) continue;
1676
+ published.readings.set(id, reading);
1677
+ exposures.serve(subject, flag, reading.value);
1678
+ }
1679
+ published.flush();
1680
+ },
1681
+ settle: () => {
1682
+ if (!active) return;
1683
+ active = false;
1684
+ published.setOverrides(aside);
1685
+ if (waiting()) published.flush();
1686
+ else reevaluate();
1687
+ }
1688
+ };
1689
+ }
1690
+ /**
1691
+ * Returns the flag store over the product's flags, its flag source and the tab's storage.
1692
+ *
1693
+ * @remarks
1694
+ * A first read evaluates the flag and returns its value at once, and publishes the reading at the
1695
+ * end of the task, because the hooks read a flag inside a `useSyncExternalStore` snapshot. The
1696
+ * source is asked only once it identified the session. Until then a flag takes the product's
1697
+ * value or its default. Each notification of the source and each identification evaluates again
1698
+ * the flags the page has read, and the previous values remain while `identify` is pending.
1699
+ * `flag-exposed` is reported the first time the session's subject is served each variant of an
1700
+ * experiment. An override counts no exposure. A store a server render hydrated reads the
1701
+ * server's readings until `settle`, and evaluates again only then.
1702
+ */
1703
+ function createFlagStore({ overrides: storage, product, report, source }) {
1704
+ const published = publishedOf(report);
1705
+ const declared = new Map(product.flags.map((flag) => [flag.id, flag]));
1706
+ const key = `stealth.${product.productId}.flag-overrides`;
1707
+ const shared = {
1708
+ declared,
1709
+ exposures: exposuresOf(published.queue),
1710
+ identity: identityOf(source, (error) => {
1711
+ published.queue({
1712
+ error,
1713
+ kind: "flags-failed"
1714
+ });
1715
+ published.flush();
1716
+ }),
1717
+ published
1718
+ };
1719
+ const reevaluate = reevaluatorOf(shared, product.flags);
1720
+ const hydration = hydrationOf(shared, reevaluate, () => source !== void 0 && shared.identity.trusted() === void 0);
1721
+ published.setOverrides(storedOverrides(storage, key, declared, published.queue));
1722
+ published.flush();
1723
+ const stop = source?.subscribe(() => {
1724
+ if (shared.identity.trusted() !== void 0 && !hydration.active()) reevaluate();
1725
+ });
1726
+ return {
1727
+ dispose: () => {
1728
+ stop?.();
1729
+ shared.identity.stop();
1730
+ },
1731
+ get: published.state.get,
1732
+ hydrate: hydration.hydrate,
1733
+ identify: async (session) => {
1734
+ shared.exposures.reset(subjectOf(session));
1735
+ if (await shared.identity.identify(session) && !hydration.active()) reevaluate();
1736
+ },
1737
+ override: overriderOf(shared, storage, key),
1738
+ read: readerOf(shared),
1739
+ settle: hydration.settle,
1740
+ snapshot: () => [...published.readings],
1741
+ subscribe: published.state.subscribe
1742
+ };
1743
+ }
1744
+ //#endregion
1745
+ //#region src/stores/subject.ts
1746
+ /**
1747
+ * Names the subject a person's stored choices belong to.
1748
+ */
1749
+ /**
1750
+ * Returns the subject a session's stored choices are kept under: its person and tenant, or
1751
+ * `anyone` for a session nobody signed in to.
1752
+ */
1753
+ function subjectFor({ session }) {
1754
+ return subjectOf(session) ?? "anyone";
1755
+ }
1756
+ //#endregion
1757
+ //#region src/stores/placements.ts
1758
+ /**
1759
+ * The version of the stored value this host writes and reads.
1760
+ */
1761
+ const VERSION = 1;
1762
+ /**
1763
+ * The members of a slot's placement.
1764
+ */
1765
+ const MEMBERS = [
1766
+ "add",
1767
+ "order",
1768
+ "remove"
1769
+ ];
1770
+ /**
1771
+ * The check of no stored value.
1772
+ */
1773
+ const NOTHING = {
1774
+ dropped: [],
1775
+ slots: {}
1776
+ };
1777
+ /**
1778
+ * Returns true for a plain object.
1779
+ */
1780
+ function isRecord(value) {
1781
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1782
+ }
1783
+ /**
1784
+ * Returns true for a list.
1785
+ */
1786
+ function isList(value) {
1787
+ return Array.isArray(value);
1788
+ }
1789
+ /**
1790
+ * Returns one slot's placement with the members and ids the installed plugins allow, and a reason
1791
+ * for each part it drops.
1792
+ */
1793
+ function placementOf(slot, stated, extensions) {
1794
+ if (!isRecord(stated)) return { dropped: [`${slot.id} is not a placement`] };
1795
+ const dropped = [];
1796
+ const placement = {};
1797
+ /**
1798
+ * Returns true for the id of an extension an installed plugin declares.
1799
+ */
1800
+ const declared = (id) => typeof id === "string" && extensions.has(id);
1801
+ for (const member of MEMBERS) {
1802
+ const listed = stated[member];
1803
+ const path = `${slot.id}.${member}`;
1804
+ if (listed === void 0) continue;
1805
+ if (member === "add" && !slot.region) dropped.push(`${path} places extensions in a slot that is not a region`);
1806
+ else if (isList(listed)) {
1807
+ for (const id of listed.filter((one) => !declared(one))) dropped.push(`${path} lists ${JSON.stringify(id)}, which no installed plugin declares`);
1808
+ placement[member] = listed.filter((one) => declared(one));
1809
+ } else dropped.push(`${path} is not a list`);
1810
+ }
1811
+ return {
1812
+ dropped,
1813
+ placement
1814
+ };
1815
+ }
1816
+ /**
1817
+ * Returns the placements of the slots an installed plugin declares, and a reason for each part it
1818
+ * drops.
1819
+ */
1820
+ function checkedOf(stated, slots, extensions) {
1821
+ const dropped = [];
1822
+ const kept = {};
1823
+ for (const [slotId, placement] of Object.entries(stated)) {
1824
+ const slot = slots[slotId];
1825
+ if (slot === void 0) {
1826
+ dropped.push(`${slotId} is not a slot an installed plugin declares`);
1827
+ continue;
1828
+ }
1829
+ const read = placementOf(slot, placement, extensions);
1830
+ dropped.push(...read.dropped);
1831
+ if (read.placement !== void 0) kept[slotId] = read.placement;
1832
+ }
1833
+ return {
1834
+ dropped,
1835
+ slots: kept
1836
+ };
1837
+ }
1838
+ /**
1839
+ * Returns the placements stored text contains, checked, or why the store drops the whole value.
1840
+ */
1841
+ function readOf(text, slots, extensions) {
1842
+ if (text === null) return NOTHING;
1843
+ let parsed;
1844
+ try {
1845
+ parsed = JSON.parse(text);
1846
+ } catch {
1847
+ return {
1848
+ dropped: ["the stored value is not JSON"],
1849
+ slots: {}
1850
+ };
1851
+ }
1852
+ if (!isRecord(parsed) || !isRecord(parsed["slots"])) return {
1853
+ dropped: ["the stored value has no slots"],
1854
+ slots: {}
1855
+ };
1856
+ const { version } = parsed;
1857
+ if (typeof version === "number" && version > VERSION) return NOTHING;
1858
+ return version === VERSION ? checkedOf(parsed["slots"], slots, extensions) : {
1859
+ dropped: [`the stored value is not version ${String(VERSION)}`],
1860
+ slots: {}
1861
+ };
1862
+ }
1863
+ /**
1864
+ * Returns the placement store over the setting store, for the session's subject.
1865
+ *
1866
+ * @remarks
1867
+ * The store checks a stored value once per text it reads, so one value is reported once. A change
1868
+ * of subject reads the new subject's key and follows it alone. `update` checks the change,
1869
+ * reports what it drops, and writes the rest over the placements of the slots it does not name.
1870
+ */
1871
+ function createPlacementStore({ product, report, session, store }) {
1872
+ const extensions = new Set(product.extensions.map(({ id }) => id));
1873
+ let subject = subjectFor(session.get());
1874
+ let text;
1875
+ /**
1876
+ * Returns the key of the placements for the current subject.
1877
+ */
1878
+ const keyOf = () => settingKey(product.productId, `${subject}.placements`);
1879
+ /**
1880
+ * Reports each reason a check dropped a part.
1881
+ */
1882
+ const reported = ({ dropped, slots }) => {
1883
+ for (const reason of dropped) report({
1884
+ key: keyOf(),
1885
+ kind: "setting-dropped",
1886
+ reason
1887
+ });
1888
+ return slots;
1889
+ };
1890
+ /**
1891
+ * Returns the stored placements where the stored text changed, else the current ones.
1892
+ */
1893
+ const read = (current) => {
1894
+ const stored = store.read(keyOf());
1895
+ if (stored === text) return current;
1896
+ text = stored;
1897
+ return reported(readOf(stored, product.slots, extensions));
1898
+ };
1899
+ const state = writable(read({}));
1900
+ /**
1901
+ * Reads the stored placements again, and publishes them where they changed.
1902
+ */
1903
+ const refresh = () => {
1904
+ state.set(read(state.get()));
1905
+ };
1906
+ let stop = store.subscribe(keyOf(), refresh);
1907
+ const stopSession = session.subscribe(() => {
1908
+ const next = subjectFor(session.get());
1909
+ if (next === subject) return;
1910
+ subject = next;
1911
+ stop();
1912
+ stop = store.subscribe(keyOf(), refresh);
1913
+ refresh();
1914
+ });
1915
+ return {
1916
+ dispose: () => {
1917
+ stopSession();
1918
+ stop();
1919
+ },
1920
+ get: state.get,
1921
+ reset: () => {
1922
+ store.clear(keyOf());
1923
+ refresh();
1924
+ },
1925
+ subscribe: state.subscribe,
1926
+ update: (change) => {
1927
+ const slots = reported(checkedOf({
1928
+ ...state.get(),
1929
+ ...change
1930
+ }, product.slots, extensions));
1931
+ store.write(keyOf(), JSON.stringify({
1932
+ slots,
1933
+ version: VERSION
1934
+ }));
1935
+ refresh();
1936
+ }
1937
+ };
1938
+ }
1939
+ //#endregion
1940
+ //#region src/stores/session.ts
1941
+ /**
1942
+ * Keeps the session a product's source reports, with its grants as sets of the ids the installed
1943
+ * plugins declare.
1944
+ */
1945
+ /**
1946
+ * Returns a session's state, with its permissions and entitlements reduced to the declared ids.
1947
+ */
1948
+ function stateOf(session, permissions, entitlements) {
1949
+ return {
1950
+ entitlements: new Set(session.entitlements.filter((id) => entitlements.has(id))),
1951
+ permissions: new Set(session.permissions.filter((id) => permissions.has(id))),
1952
+ session
1953
+ };
1954
+ }
1955
+ /**
1956
+ * Returns the session store, which reads the source now and again after each notification.
1957
+ *
1958
+ * @remarks
1959
+ * A permission or an entitlement no installed plugin declares is left out of the sets, so a
1960
+ * check for it is false. A notification after which `read` returns the same object changes
1961
+ * nothing. A `read` that throws keeps the last session and reports `session-failed`. Before the
1962
+ * first successful read the session is `NOBODY`.
1963
+ */
1964
+ function createSessionStore({ product, report, source }) {
1965
+ const permissions = new Set(product.permissions.map(({ id }) => id));
1966
+ const entitlements = new Set(product.entitlements.map(({ id }) => id));
1967
+ /**
1968
+ * Returns the source's session, or undefined after reporting a `read` that threw.
1969
+ */
1970
+ const read = () => {
1971
+ try {
1972
+ return source.read();
1973
+ } catch (error) {
1974
+ report({
1975
+ error,
1976
+ kind: "session-failed"
1977
+ });
1978
+ return;
1979
+ }
1980
+ };
1981
+ const store = writable(stateOf(read() ?? NOBODY, permissions, entitlements));
1982
+ /**
1983
+ * Reads the source, and publishes the session where it is another object.
1984
+ */
1985
+ const refresh = () => {
1986
+ const session = read();
1987
+ if (session !== void 0 && session !== store.get().session) store.set(stateOf(session, permissions, entitlements));
1988
+ };
1989
+ return {
1990
+ dispose: source.subscribe(refresh),
1991
+ get: store.get,
1992
+ refresh,
1993
+ subscribe: store.subscribe
1994
+ };
1995
+ }
1996
+ //#endregion
1997
+ //#region src/stores/switches.ts
1998
+ /**
1999
+ * Returns a switchable plugin's switch as stored, or the product's `enabled` where nothing is
2000
+ * stored. A stored value other than `on` and `off` is reported and reads as `enabled`.
2001
+ */
2002
+ function switchOf(plugin, key, store, report) {
2003
+ const stored = store.read(key);
2004
+ if (stored === "on") return true;
2005
+ if (stored === "off") return false;
2006
+ if (stored !== null) report({
2007
+ key,
2008
+ kind: "setting-dropped",
2009
+ reason: `the stored switch is ${JSON.stringify(stored)}, neither "on" nor "off"`
2010
+ });
2011
+ return plugin.enabled;
2012
+ }
2013
+ /**
2014
+ * Returns the switch store over the setting store, for the session's subject.
2015
+ *
2016
+ * @remarks
2017
+ * A change of subject reads every switch from the new subject's keys and follows those keys
2018
+ * alone. A write to a key the store follows, from this tab or another, reads that switch again.
2019
+ */
2020
+ function createSwitchStore({ product, report, session, store }) {
2021
+ const switchable = product.plugins.filter(({ locked }) => !locked);
2022
+ const locked = product.plugins.filter((plugin) => plugin.locked);
2023
+ let subject = subjectFor(session.get());
2024
+ /**
2025
+ * Returns the key of a plugin's switch for the current subject.
2026
+ */
2027
+ const keyOf = (pluginId) => settingKey(product.productId, `${subject}.plugin.${pluginId}`);
2028
+ /**
2029
+ * Reads every switch of the subject, and reports a value stored for a locked plugin.
2030
+ */
2031
+ const read = () => {
2032
+ for (const { id } of locked) if (store.read(keyOf(id)) !== null) report({
2033
+ key: keyOf(id),
2034
+ kind: "setting-dropped",
2035
+ reason: "the plugin is locked"
2036
+ });
2037
+ return Object.fromEntries(switchable.map((plugin) => [plugin.id, switchOf(plugin, keyOf(plugin.id), store, report)]));
2038
+ };
2039
+ const state = writable(read());
2040
+ /**
2041
+ * Reads one plugin's switch again, and publishes it where it changed.
2042
+ */
2043
+ const refresh = (plugin) => {
2044
+ const on = switchOf(plugin, keyOf(plugin.id), store, report);
2045
+ if (state.get()[plugin.id] !== on) state.set({
2046
+ ...state.get(),
2047
+ [plugin.id]: on
2048
+ });
2049
+ };
2050
+ /**
2051
+ * Follows the key of every switchable plugin for the current subject.
2052
+ */
2053
+ const follow = () => switchable.map((plugin) => store.subscribe(keyOf(plugin.id), () => {
2054
+ refresh(plugin);
2055
+ }));
2056
+ let stops = follow();
2057
+ const stopSession = session.subscribe(() => {
2058
+ const next = subjectFor(session.get());
2059
+ if (next === subject) return;
2060
+ subject = next;
2061
+ for (const stop of stops) stop();
2062
+ stops = follow();
2063
+ state.set(read());
2064
+ });
2065
+ return {
2066
+ dispose: () => {
2067
+ stopSession();
2068
+ for (const stop of stops) stop();
2069
+ },
2070
+ get: state.get,
2071
+ set: (pluginId, on) => {
2072
+ const plugin = switchable.find(({ id }) => id === pluginId);
2073
+ if (plugin === void 0) return;
2074
+ store.write(keyOf(pluginId), on ? "on" : "off");
2075
+ refresh(plugin);
2076
+ },
2077
+ subscribe: state.subscribe
2078
+ };
2079
+ }
2080
+ //#endregion
2081
+ //#region src/host/state.ts
2082
+ /**
2083
+ * Creates the host's stores that follow the session: the session, the decisions, the flags, the
2084
+ * switches, the availability and the placements.
2085
+ */
2086
+ /**
2087
+ * Returns the tab's session storage where overrides are on, or undefined.
2088
+ *
2089
+ * @remarks
2090
+ * Overrides are on outside a production build unless the product states otherwise. A server has
2091
+ * no tab, and a page whose storage the browser refuses keeps no override.
2092
+ * @param overrides - The product's choice, where it states one.
2093
+ */
2094
+ function overridesOf(overrides) {
2095
+ if (!(overrides ?? process.env.NODE_ENV !== "production") || typeof window === "undefined") return;
2096
+ try {
2097
+ return window.sessionStorage;
2098
+ } catch {
2099
+ return;
2100
+ }
2101
+ }
2102
+ /**
2103
+ * Returns the stores that follow the session, each created from the product's sources.
2104
+ *
2105
+ * @remarks
2106
+ * The availability store emits `host/pluginChanged` as the host for every plugin that turns on or
2107
+ * off.
2108
+ */
2109
+ function createState({ events, options, report }) {
2110
+ const { product, store } = options;
2111
+ const session = createSessionStore({
2112
+ product,
2113
+ report,
2114
+ source: options.session
2115
+ });
2116
+ const access = createAccessStore({
2117
+ product,
2118
+ report,
2119
+ source: options.access
2120
+ });
2121
+ const flags = createFlagStore({
2122
+ overrides: overridesOf(options.overrides),
2123
+ product,
2124
+ report,
2125
+ source: options.flags
2126
+ });
2127
+ const switches = createSwitchStore({
2128
+ product,
2129
+ report,
2130
+ session,
2131
+ store
2132
+ });
2133
+ const availability = createAvailabilityStore({
2134
+ changed: (change) => {
2135
+ events.emit(HOST, hostContract.events.pluginChanged.id, change);
2136
+ },
2137
+ flags,
2138
+ product,
2139
+ session,
2140
+ switches
2141
+ });
2142
+ const placements = createPlacementStore({
2143
+ product,
2144
+ report,
2145
+ session,
2146
+ store
2147
+ });
2148
+ return {
2149
+ dispose: () => {
2150
+ availability.dispose();
2151
+ placements.dispose();
2152
+ switches.dispose();
2153
+ flags.dispose();
2154
+ access.dispose();
2155
+ session.dispose();
2156
+ },
2157
+ stores: {
2158
+ access,
2159
+ availability,
2160
+ flags,
2161
+ placements,
2162
+ session
2163
+ },
2164
+ switches
2165
+ };
2166
+ }
2167
+ //#endregion
2168
+ //#region src/host/create-host.ts
2169
+ /**
2170
+ * Creates the host of one page, or of one request on a server.
2171
+ */
2172
+ /**
2173
+ * Milliseconds `ready` waits for the flag source's first identification, where the product states
2174
+ * none.
2175
+ */
2176
+ const FLAGS_TIMEOUT = 1e3;
2177
+ /**
2178
+ * Creates a host for one page, or for one request on a server.
2179
+ *
2180
+ * @remarks
2181
+ * The host subscribes to the session source, the setting store, the flag source and the access
2182
+ * source as it is created. It emits the session as `host/sessionChanged`, and starts the flag
2183
+ * source's identification and the eager plugins' imports. The imports go through the runtime's
2184
+ * product, whose importers measure each plugin's first import and recover from a chunk that no
2185
+ * longer exists. `dispose` ends the subscriptions and cancels the data client's fetches.
2186
+ * @throws {@link Error} Where the product's manifests lack code for a declared name.
2187
+ */
2188
+ function createHost(options) {
2189
+ const { product } = options;
2190
+ validateManifests(product);
2191
+ const page = createPage(options);
2192
+ const events = createEventBus({
2193
+ events: product.events,
2194
+ report: page.report
2195
+ });
2196
+ const state = createState({
2197
+ events,
2198
+ options,
2199
+ report: page.report
2200
+ });
2201
+ const { access, flags, session } = state.stores;
2202
+ const connection = connector();
2203
+ const data = createHostData({
2204
+ access,
2205
+ connection: connection.current,
2206
+ data: options.data,
2207
+ events,
2208
+ product,
2209
+ session
2210
+ });
2211
+ const sequence = followSession({
2212
+ access,
2213
+ data,
2214
+ events,
2215
+ flags,
2216
+ session
2217
+ });
2218
+ const stores = {
2219
+ ...state.stores,
2220
+ ...page.stores
2221
+ };
2222
+ const runtime = createRuntime({
2223
+ connection: connection.current,
2224
+ data,
2225
+ events,
2226
+ product,
2227
+ report: page.report,
2228
+ settings: options.store,
2229
+ stores
2230
+ });
2231
+ const eager = importEager(runtime.product);
2232
+ let ready;
2233
+ const host = {
2234
+ data,
2235
+ dispose: () => {
2236
+ sequence.stop();
2237
+ state.dispose();
2238
+ data.cancelQueries();
2239
+ },
2240
+ ready: () => {
2241
+ ready ??= readied(eager, sequence.identified, options.flagsTimeout ?? FLAGS_TIMEOUT);
2242
+ return ready;
2243
+ },
2244
+ retry: page.stores.quarantine.retry,
2245
+ stores
2246
+ };
2247
+ remember(host, {
2248
+ access,
2249
+ connect: connection.connect,
2250
+ flags,
2251
+ glyphs: options.glyphs,
2252
+ runtime,
2253
+ switches: state.switches
2254
+ });
2255
+ return host;
2256
+ }
2257
+ //#endregion
2258
+ //#region src/parts/content.tsx
2259
+ /**
2260
+ * Renders the root route's outlet inside the `content` region, so the region's extensions decorate
2261
+ * every page.
2262
+ *
2263
+ * @remarks
2264
+ * The frame renders it where the page belongs. A frame that renders `Outlet` in its place renders
2265
+ * pages without the region, and the extension statuses read the region as not mounted.
2266
+ */
2267
+ function HostContent() {
2268
+ const $ = c(1);
2269
+ let t0;
2270
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
2271
+ t0 = /* @__PURE__ */ jsx(Slot, {
2272
+ slot: hostContract.slots.content,
2273
+ children: /* @__PURE__ */ jsx(Outlet, {})
2274
+ });
2275
+ $[0] = t0;
2276
+ } else t0 = $[0];
2277
+ return t0;
2278
+ }
2279
+ //#endregion
2280
+ //#region src/routes/not-found.ts
2281
+ /**
2282
+ * Returns true for the data of a page whose plugin a switch or a kill switch stopped.
2283
+ */
2284
+ function isStopped(data) {
2285
+ return "plugin" in data && typeof data.plugin === "string" && "reason" in data && (data.reason === "off" || data.reason === "unavailable");
2286
+ }
2287
+ /**
2288
+ * Returns true for the data of a page the host quarantined.
2289
+ */
2290
+ function isQuarantined(data) {
2291
+ return "target" in data && typeof data.target === "string" && data.target.startsWith("route:") && "reason" in data && data.reason === "quarantined";
2292
+ }
2293
+ /**
2294
+ * Reads the data of a not-found error as the host's not-found data.
2295
+ *
2296
+ * @remarks
2297
+ * Every route of the tree can throw a not-found error, so data of another shape, from a route the
2298
+ * product wrote, reads as none.
2299
+ * @param data - The data the router passes the not-found component.
2300
+ * @returns The data the route evaluator threw, or nothing.
2301
+ */
2302
+ function notFoundDataOf(data) {
2303
+ if (typeof data !== "object" || data === null) return void 0;
2304
+ return isStopped(data) || isQuarantined(data) ? data : void 0;
2305
+ }
2306
+ //#endregion
2307
+ //#region src/parts/not-found.tsx
2308
+ /**
2309
+ * Renders the root's not-found page: for a page whose plugin a switch or a kill switch stopped, for
2310
+ * a page the host quarantined, and for an address no route matches.
2311
+ */
2312
+ /**
2313
+ * Returns the ref of the page's heading, which takes focus where the page replaced the page a
2314
+ * person was on without a navigation.
2315
+ *
2316
+ * @remarks
2317
+ * The router records an address as resolved only after the pages of its navigation rendered, so
2318
+ * the resolved address equals the address while the page first renders only where no navigation
2319
+ * led to it: a switch, a kill switch, a condition or a quarantine changed under the person.
2320
+ */
2321
+ function useHeading() {
2322
+ const $ = c(6);
2323
+ const heading = useRef(null);
2324
+ const router = useRouter();
2325
+ let t0;
2326
+ if ($[0] !== router.state.location.href || $[1] !== router.state.resolvedLocation?.href) {
2327
+ t0 = () => router.state.resolvedLocation?.href === router.state.location.href;
2328
+ $[0] = router.state.location.href;
2329
+ $[1] = router.state.resolvedLocation?.href;
2330
+ $[2] = t0;
2331
+ } else t0 = $[2];
2332
+ const [replaced] = useState(t0);
2333
+ let t1;
2334
+ let t2;
2335
+ if ($[3] !== replaced) {
2336
+ t1 = () => {
2337
+ if (replaced) heading.current?.focus();
2338
+ };
2339
+ t2 = [replaced];
2340
+ $[3] = replaced;
2341
+ $[4] = t1;
2342
+ $[5] = t2;
2343
+ } else {
2344
+ t1 = $[4];
2345
+ t2 = $[5];
2346
+ }
2347
+ useEffect(t1, t2);
2348
+ return heading;
2349
+ }
2350
+ /**
2351
+ * Renders the not-found page the router's data describes.
2352
+ *
2353
+ * @remarks
2354
+ * A page whose plugin a switch turned off names the plugin and offers a button that turns it on.
2355
+ * A page whose plugin a kill switch stopped names the plugin and offers nothing. A quarantined
2356
+ * page offers a retry that lifts the quarantine. Data of another shape, or none, renders a plain
2357
+ * not-found page, which states nothing about the address. The host runs the router's
2358
+ * `beforeLoad` again after a switch or a retry, and the page renders again in this one's place.
2359
+ */
2360
+ function HostNotFound(t0) {
2361
+ const $ = c(25);
2362
+ const { data } = t0;
2363
+ let t1;
2364
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
2365
+ t1 = { strict: false };
2366
+ $[0] = t1;
2367
+ } else t1 = $[0];
2368
+ const { host } = useRouteContext(t1);
2369
+ const { i18n, t } = useTranslation("host");
2370
+ const heading = useHeading();
2371
+ let T0;
2372
+ let T1;
2373
+ let found;
2374
+ let t2;
2375
+ let t3;
2376
+ let t4;
2377
+ if ($[1] !== data || $[2] !== heading || $[3] !== host || $[4] !== i18n || $[5] !== t) {
2378
+ found = notFoundDataOf(data);
2379
+ const reason = found?.reason ?? "plain";
2380
+ const plugin = found === void 0 || found.reason === "quarantined" ? "" : pluginWordsOf(i18n).t(found.plugin, "plugin.name");
2381
+ T1 = EmptyState.Root;
2382
+ T0 = EmptyState.Content;
2383
+ t2 = /* @__PURE__ */ jsx(EmptyState.Title, {
2384
+ as: "h1",
2385
+ ref: heading,
2386
+ tabIndex: -1,
2387
+ children: t(`notFound.${reason}.title`, { plugin })
2388
+ });
2389
+ t3 = /* @__PURE__ */ jsx(EmptyState.Description, { children: t(`notFound.${reason}.description`, { plugin }) });
2390
+ t4 = found?.reason === "off" ? /* @__PURE__ */ jsx(Button, {
2391
+ onClick: () => {
2392
+ internalsOf(host).switches.set(found.plugin, true);
2393
+ },
2394
+ children: t("notFound.off.action", { plugin })
2395
+ }) : null;
2396
+ $[1] = data;
2397
+ $[2] = heading;
2398
+ $[3] = host;
2399
+ $[4] = i18n;
2400
+ $[5] = t;
2401
+ $[6] = T0;
2402
+ $[7] = T1;
2403
+ $[8] = found;
2404
+ $[9] = t2;
2405
+ $[10] = t3;
2406
+ $[11] = t4;
2407
+ } else {
2408
+ T0 = $[6];
2409
+ T1 = $[7];
2410
+ found = $[8];
2411
+ t2 = $[9];
2412
+ t3 = $[10];
2413
+ t4 = $[11];
2414
+ }
2415
+ let t5;
2416
+ if ($[12] !== found || $[13] !== host || $[14] !== t) {
2417
+ t5 = found?.reason === "quarantined" ? /* @__PURE__ */ jsx(Button, {
2418
+ onClick: () => {
2419
+ host.retry(found.target);
2420
+ },
2421
+ children: t("notFound.quarantined.action")
2422
+ }) : null;
2423
+ $[12] = found;
2424
+ $[13] = host;
2425
+ $[14] = t;
2426
+ $[15] = t5;
2427
+ } else t5 = $[15];
2428
+ let t6;
2429
+ if ($[16] !== T0 || $[17] !== t2 || $[18] !== t3 || $[19] !== t4 || $[20] !== t5) {
2430
+ t6 = /* @__PURE__ */ jsxs(T0, { children: [
2431
+ t2,
2432
+ t3,
2433
+ t4,
2434
+ t5
2435
+ ] });
2436
+ $[16] = T0;
2437
+ $[17] = t2;
2438
+ $[18] = t3;
2439
+ $[19] = t4;
2440
+ $[20] = t5;
2441
+ $[21] = t6;
2442
+ } else t6 = $[21];
2443
+ let t7;
2444
+ if ($[22] !== T1 || $[23] !== t6) {
2445
+ t7 = /* @__PURE__ */ jsx(T1, { children: t6 });
2446
+ $[22] = T1;
2447
+ $[23] = t6;
2448
+ $[24] = t7;
2449
+ } else t7 = $[24];
2450
+ return t7;
2451
+ }
2452
+ //#endregion
2453
+ //#region src/parts/connection.ts
2454
+ /**
2455
+ * Returns the qualified ids of the declared routes the router matched, outermost first.
2456
+ *
2457
+ * @remarks
2458
+ * The root and every route the product wrote without a name state no id, so the list leaves them
2459
+ * out.
2460
+ */
2461
+ function matchedOf(router) {
2462
+ return router.state.matches.flatMap((match) => declaredOf(match)?.id ?? []);
2463
+ }
2464
+ /**
2465
+ * Returns the connection of a router and the catalogues' instance.
2466
+ *
2467
+ * @remarks
2468
+ * `navigate` resolves the reference through the route map the router's context contains, with
2469
+ * the parameters filled in, and passes the search beside the path, as `RouteLink` does. It
2470
+ * rejects where the map contains no such route or a parameter the path names is missing.
2471
+ * @param router - The router whose context contains the host.
2472
+ * @param i18n - The instance `useTranslation` returns.
2473
+ */
2474
+ function connectionOf(router, i18n) {
2475
+ const words = pluginWordsOf(i18n);
2476
+ const { routes } = router.options.context;
2477
+ return {
2478
+ invalidate: () => {
2479
+ router.invalidate();
2480
+ },
2481
+ matched: () => matchedOf(router),
2482
+ navigate: async (to, options) => {
2483
+ await router.navigate({
2484
+ ...options?.search === void 0 ? {} : { search: options.search },
2485
+ to: routeHref(routes, to, options?.params)
2486
+ });
2487
+ },
2488
+ translate: (namespace, key, values) => words.t(namespace, key, values)
2489
+ };
2490
+ }
2491
+ //#endregion
2492
+ //#region src/parts/connect.ts
2493
+ /**
2494
+ * Keeps a host in step with its router and with the page while `HostProvider` is mounted.
2495
+ */
2496
+ /**
2497
+ * Returns true where an override changed, or a reading the page took was replaced or dropped.
2498
+ *
2499
+ * @remarks
2500
+ * The flag store keeps a reading's object until its value or its origin changes. A first read
2501
+ * adds a reading with the value the reader already used, a route condition included, so a reading
2502
+ * added alone changes nothing a condition read.
2503
+ */
2504
+ function changedFlags(previous, next) {
2505
+ if (previous.overrides !== next.overrides) return true;
2506
+ for (const [id, reading] of previous.readings) if (next.readings.get(id) !== reading) return true;
2507
+ return false;
2508
+ }
2509
+ /**
2510
+ * Calls `invalidate` once per task after a change a route condition reads: the session, a plugin's
2511
+ * availability, a quarantine, an override, or the value of a flag the page read. Returns a
2512
+ * function that stops the calls.
2513
+ */
2514
+ function followStores(stores, invalidate) {
2515
+ let queued = false;
2516
+ let flags = stores.flags.get();
2517
+ const queue = () => {
2518
+ if (queued) return;
2519
+ queued = true;
2520
+ queueMicrotask(() => {
2521
+ queued = false;
2522
+ invalidate();
2523
+ });
2524
+ };
2525
+ const stops = [
2526
+ stores.availability.subscribe(queue),
2527
+ stores.quarantine.subscribe(queue),
2528
+ stores.session.subscribe(queue),
2529
+ stores.flags.subscribe(() => {
2530
+ const next = stores.flags.get();
2531
+ if (changedFlags(flags, next)) queue();
2532
+ flags = next;
2533
+ })
2534
+ ];
2535
+ return () => {
2536
+ for (const stop of stops) stop();
2537
+ };
2538
+ }
2539
+ /**
2540
+ * Emits `host/navigated` for the address the router resolved before the call, and after each
2541
+ * navigation it resolves. Returns a function that stops the emits.
2542
+ *
2543
+ * @remarks
2544
+ * A load that resolves the address already resolved, such as an invalidation, emits nothing.
2545
+ */
2546
+ function followNavigations(events, router) {
2547
+ const announce = (href) => {
2548
+ const navigated = {
2549
+ href,
2550
+ matched: matchedOf(router)
2551
+ };
2552
+ events.emit(HOST, hostContract.events.navigated.id, navigated);
2553
+ };
2554
+ const resolved = router.state.resolvedLocation;
2555
+ if (resolved !== void 0) announce(resolved.href);
2556
+ return router.subscribe("onResolved", ({ hrefChanged, toLocation }) => {
2557
+ if (hrefChanged) announce(toLocation.href);
2558
+ });
2559
+ }
2560
+ /**
2561
+ * Connects a host to its router until the caller unmounts.
2562
+ *
2563
+ * @remarks
2564
+ * The host's API and its commands read the router's matches, its navigation and the catalogues
2565
+ * through the connection. The router runs `beforeLoad` again once per task after a change a route
2566
+ * condition reads, so a session change, a switch, a kill switch or a quarantine costs one
2567
+ * invalidation. The host emits `host/navigated` after every navigation the router resolves.
2568
+ * Vite's `vite:preloadError` event reloads the page once per build version. The last effect ends
2569
+ * the hydration from a server render, once the stores' readers are subscribed, so what the
2570
+ * browser's flags change renders again.
2571
+ */
2572
+ function useConnected(host, router) {
2573
+ const $ = c(21);
2574
+ const { i18n } = useTranslation("host");
2575
+ let t0;
2576
+ if ($[0] !== host) {
2577
+ t0 = internalsOf(host);
2578
+ $[0] = host;
2579
+ $[1] = t0;
2580
+ } else t0 = $[1];
2581
+ const { connect, flags, runtime } = t0;
2582
+ const { stores } = host;
2583
+ let t1;
2584
+ let t2;
2585
+ if ($[2] !== connect || $[3] !== i18n || $[4] !== router) {
2586
+ t1 = () => connect(connectionOf(router, i18n));
2587
+ t2 = [
2588
+ connect,
2589
+ i18n,
2590
+ router
2591
+ ];
2592
+ $[2] = connect;
2593
+ $[3] = i18n;
2594
+ $[4] = router;
2595
+ $[5] = t1;
2596
+ $[6] = t2;
2597
+ } else {
2598
+ t1 = $[5];
2599
+ t2 = $[6];
2600
+ }
2601
+ useEffect(t1, t2);
2602
+ let t3;
2603
+ let t4;
2604
+ if ($[7] !== runtime.recover) {
2605
+ t3 = () => listenForStaleChunks(runtime.recover);
2606
+ t4 = [runtime.recover];
2607
+ $[7] = runtime.recover;
2608
+ $[8] = t3;
2609
+ $[9] = t4;
2610
+ } else {
2611
+ t3 = $[8];
2612
+ t4 = $[9];
2613
+ }
2614
+ useEffect(t3, t4);
2615
+ let t5;
2616
+ let t6;
2617
+ if ($[10] !== router || $[11] !== stores) {
2618
+ t5 = () => followStores(stores, () => {
2619
+ router.invalidate();
2620
+ });
2621
+ t6 = [router, stores];
2622
+ $[10] = router;
2623
+ $[11] = stores;
2624
+ $[12] = t5;
2625
+ $[13] = t6;
2626
+ } else {
2627
+ t5 = $[12];
2628
+ t6 = $[13];
2629
+ }
2630
+ useEffect(t5, t6);
2631
+ let t7;
2632
+ let t8;
2633
+ if ($[14] !== router || $[15] !== runtime.events) {
2634
+ t7 = () => followNavigations(runtime.events, router);
2635
+ t8 = [router, runtime.events];
2636
+ $[14] = router;
2637
+ $[15] = runtime.events;
2638
+ $[16] = t7;
2639
+ $[17] = t8;
2640
+ } else {
2641
+ t7 = $[16];
2642
+ t8 = $[17];
2643
+ }
2644
+ useEffect(t7, t8);
2645
+ let t10;
2646
+ let t9;
2647
+ if ($[18] !== flags) {
2648
+ t9 = () => {
2649
+ flags.settle();
2650
+ };
2651
+ t10 = [flags];
2652
+ $[18] = flags;
2653
+ $[19] = t10;
2654
+ $[20] = t9;
2655
+ } else {
2656
+ t10 = $[19];
2657
+ t9 = $[20];
2658
+ }
2659
+ useEffect(t9, t10);
2660
+ }
2661
+ //#endregion
2662
+ //#region src/parts/failed.tsx
2663
+ /**
2664
+ * Renders the failure page as the document's main landmark, with the product's name in its
2665
+ * heading.
2666
+ *
2667
+ * @remarks
2668
+ * The page replaces the frame, so it renders the landmark the frame's own main region was.
2669
+ */
2670
+ function HostFailed(t0) {
2671
+ const $ = c(17);
2672
+ const { product } = t0;
2673
+ const { t } = useTranslation("host");
2674
+ const name = useProductName(product);
2675
+ let t1;
2676
+ if ($[0] !== name || $[1] !== t) {
2677
+ t1 = t("failed.title", { product: name });
2678
+ $[0] = name;
2679
+ $[1] = t;
2680
+ $[2] = t1;
2681
+ } else t1 = $[2];
2682
+ let t2;
2683
+ if ($[3] !== t1) {
2684
+ t2 = /* @__PURE__ */ jsx(EmptyState.Title, {
2685
+ as: "h1",
2686
+ children: t1
2687
+ });
2688
+ $[3] = t1;
2689
+ $[4] = t2;
2690
+ } else t2 = $[4];
2691
+ let t3;
2692
+ if ($[5] !== t) {
2693
+ t3 = t("failed.description");
2694
+ $[5] = t;
2695
+ $[6] = t3;
2696
+ } else t3 = $[6];
2697
+ let t4;
2698
+ if ($[7] !== t3) {
2699
+ t4 = /* @__PURE__ */ jsx(EmptyState.Description, { children: t3 });
2700
+ $[7] = t3;
2701
+ $[8] = t4;
2702
+ } else t4 = $[8];
2703
+ let t5;
2704
+ if ($[9] !== t) {
2705
+ t5 = t("failed.reload");
2706
+ $[9] = t;
2707
+ $[10] = t5;
2708
+ } else t5 = $[10];
2709
+ let t6;
2710
+ if ($[11] !== t5) {
2711
+ t6 = /* @__PURE__ */ jsx(Button, {
2712
+ onClick: _temp$4,
2713
+ children: t5
2714
+ });
2715
+ $[11] = t5;
2716
+ $[12] = t6;
2717
+ } else t6 = $[12];
2718
+ let t7;
2719
+ if ($[13] !== t2 || $[14] !== t4 || $[15] !== t6) {
2720
+ t7 = /* @__PURE__ */ jsx(EmptyState.Root, {
2721
+ as: "main",
2722
+ children: /* @__PURE__ */ jsxs(EmptyState.Content, { children: [
2723
+ t2,
2724
+ t4,
2725
+ t6
2726
+ ] })
2727
+ });
2728
+ $[13] = t2;
2729
+ $[14] = t4;
2730
+ $[15] = t6;
2731
+ $[16] = t7;
2732
+ } else t7 = $[16];
2733
+ return t7;
2734
+ }
2735
+ function _temp$4() {
2736
+ window.location.reload();
2737
+ }
2738
+ //#endregion
2739
+ //#region src/parts/last-boundary.ts
2740
+ /**
2741
+ * Catches an error that escapes every plugin's boundary: in the product's frame, in a part of the
2742
+ * host or in a provider.
2743
+ */
2744
+ /**
2745
+ * Renders its children, and the host's failure page in their place once a render of them throws.
2746
+ *
2747
+ * @remarks
2748
+ * React catches a render error in a class component alone. The boundary never resets, because
2749
+ * the state of the providers above the failure is unknown, so a person reloads the page to leave
2750
+ * it. Each error is reported as `render-failed` with the target `host`.
2751
+ */
2752
+ var LastBoundary = class extends Component {
2753
+ /**
2754
+ * Returns the state after a render of the tree threw.
2755
+ */
2756
+ static getDerivedStateFromError() {
2757
+ return { failed: true };
2758
+ }
2759
+ /**
2760
+ * The state before any render threw.
2761
+ */
2762
+ state = { failed: false };
2763
+ /**
2764
+ * Reports the error a render threw.
2765
+ */
2766
+ componentDidCatch(error) {
2767
+ this.props.runtime.report({
2768
+ error,
2769
+ kind: "render-failed",
2770
+ target: "host"
2771
+ });
2772
+ }
2773
+ /**
2774
+ * Renders the tree, or the failure page after a render of it threw.
2775
+ */
2776
+ render() {
2777
+ return this.state.failed ? createElement(HostFailed, { product: this.props.runtime.product }) : this.props.children;
2778
+ }
2779
+ };
2780
+ //#endregion
2781
+ //#region src/parts/provider.tsx
2782
+ /**
2783
+ * Renders the host's contexts around a product's router: the runtime the hooks of `sdk-plugin`
2784
+ * read, the data client, the `root` region and the last boundary.
2785
+ */
2786
+ /**
2787
+ * Provides a host to the product's tree, once per render tree.
2788
+ *
2789
+ * @remarks
2790
+ * The `root` region renders around the router, so its extensions render outside every route and
2791
+ * read the router's matches through the router's context. The provider connects the host to the
2792
+ * router (`useConnected`) and titles the document with the product's name as it mounts and as
2793
+ * the language changes, before any page titles it. It renders the failure page in place of a
2794
+ * tree that throws outside the router.
2795
+ */
2796
+ function HostProvider(t0) {
2797
+ const $ = c(19);
2798
+ const { children, host, router } = t0;
2799
+ let t1;
2800
+ if ($[0] !== host) {
2801
+ t1 = internalsOf(host);
2802
+ $[0] = host;
2803
+ $[1] = t1;
2804
+ } else t1 = $[1];
2805
+ const { runtime } = t1;
2806
+ const name = useProductName(runtime.product);
2807
+ useConnected(host, router);
2808
+ let t2;
2809
+ let t3;
2810
+ if ($[2] !== name) {
2811
+ t2 = () => {
2812
+ document.title = name;
2813
+ };
2814
+ t3 = [name];
2815
+ $[2] = name;
2816
+ $[3] = t2;
2817
+ $[4] = t3;
2818
+ } else {
2819
+ t2 = $[3];
2820
+ t3 = $[4];
2821
+ }
2822
+ useLayoutEffect(t2, t3);
2823
+ let t4;
2824
+ if ($[5] !== children) {
2825
+ t4 = /* @__PURE__ */ jsx(Slot, {
2826
+ slot: hostContract.slots.root,
2827
+ children
2828
+ });
2829
+ $[5] = children;
2830
+ $[6] = t4;
2831
+ } else t4 = $[6];
2832
+ let t5;
2833
+ if ($[7] !== router || $[8] !== t4) {
2834
+ t5 = /* @__PURE__ */ jsx(RouterContextProvider, {
2835
+ router,
2836
+ children: t4
2837
+ });
2838
+ $[7] = router;
2839
+ $[8] = t4;
2840
+ $[9] = t5;
2841
+ } else t5 = $[9];
2842
+ let t6;
2843
+ if ($[10] !== host.data || $[11] !== t5) {
2844
+ t6 = /* @__PURE__ */ jsx(DataProvider, {
2845
+ client: host.data,
2846
+ children: t5
2847
+ });
2848
+ $[10] = host.data;
2849
+ $[11] = t5;
2850
+ $[12] = t6;
2851
+ } else t6 = $[12];
2852
+ let t7;
2853
+ if ($[13] !== runtime || $[14] !== t6) {
2854
+ t7 = /* @__PURE__ */ jsx(HostContext, {
2855
+ value: runtime,
2856
+ children: t6
2857
+ });
2858
+ $[13] = runtime;
2859
+ $[14] = t6;
2860
+ $[15] = t7;
2861
+ } else t7 = $[15];
2862
+ let t8;
2863
+ if ($[16] !== runtime || $[17] !== t7) {
2864
+ t8 = /* @__PURE__ */ jsx(LastBoundary, {
2865
+ runtime,
2866
+ children: t7
2867
+ });
2868
+ $[16] = runtime;
2869
+ $[17] = t7;
2870
+ $[18] = t8;
2871
+ } else t8 = $[18];
2872
+ return t8;
2873
+ }
2874
+ //#endregion
2875
+ //#region src/commands/run.ts
2876
+ /**
2877
+ * Runs a command without arguments, and reports a rejection as `command-failed` under the command's
2878
+ * plugin and as an error toast.
2879
+ *
2880
+ * @remarks
2881
+ * A key press and the palette have no caller to receive a rejection, so the host reports it.
2882
+ * @param status - The command, as `useCommands` returns it.
2883
+ * @param runtime - The host's report and toaster.
2884
+ * @param title - The error toast's title, translated.
2885
+ */
2886
+ function runReporting(status, runtime, title) {
2887
+ status.run().catch((error) => {
2888
+ runtime.report({
2889
+ error,
2890
+ kind: "command-failed",
2891
+ plugin: status.command.plugin,
2892
+ target: status.command.id
2893
+ });
2894
+ runtime.toaster.create({
2895
+ title,
2896
+ type: "error"
2897
+ });
2898
+ });
2899
+ }
2900
+ //#endregion
2901
+ //#region src/commands/keys.ts
2902
+ /**
2903
+ * Returns the commands that bind keys, grouped by chord in the form the operating system reads,
2904
+ * each group in install order.
2905
+ */
2906
+ function chordsOf(commands) {
2907
+ const chords = /* @__PURE__ */ new Map();
2908
+ for (const status of commands) {
2909
+ const { keys } = status.command;
2910
+ if (keys === void 0) continue;
2911
+ const chord = normalizeHotkey(keys);
2912
+ chords.set(chord, [...chords.get(chord) ?? [], status]);
2913
+ }
2914
+ return chords;
2915
+ }
2916
+ /**
2917
+ * Binds every chord the product's commands bind, and renders nothing.
2918
+ *
2919
+ * @remarks
2920
+ * One registration per chord, because the hotkeys library runs every handler registered for a
2921
+ * chord. A press runs the first command of the chord, in install order, whose `enabled` is true,
2922
+ * without arguments, and runs nothing where none is. A chord with `Mod` runs while a text field
2923
+ * has focus, and a single key or a `Shift` or `Alt` chord does not, the library's defaults. A
2924
+ * command that rejects is reported as `command-failed` and raises an error toast.
2925
+ */
2926
+ function CommandKeys() {
2927
+ const $ = c(1);
2928
+ let t0;
2929
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
2930
+ t0 = { strict: false };
2931
+ $[0] = t0;
2932
+ } else t0 = $[0];
2933
+ const { host } = useRouteContext(t0);
2934
+ const { runtime } = internalsOf(host);
2935
+ const commands = useCommands();
2936
+ const { t } = useTranslation("host");
2937
+ useHotkeys([...chordsOf(commands)].map((t1) => {
2938
+ const [hotkey, bound] = t1;
2939
+ return {
2940
+ callback: () => {
2941
+ const first = bound.find(_temp$3);
2942
+ if (first !== void 0) runReporting(first, runtime, t("commands.failed", { label: first.label }));
2943
+ },
2944
+ hotkey
2945
+ };
2946
+ }));
2947
+ return null;
2948
+ }
2949
+ function _temp$3(t0) {
2950
+ const { enabled } = t0;
2951
+ return enabled;
2952
+ }
2953
+ //#endregion
2954
+ //#region src/commands/palette.tsx
2955
+ /**
2956
+ * Renders the command palette: the commands a key could run and the main menu's pages, in a dialog
2957
+ * `Mod+K` opens.
2958
+ */
2959
+ /**
2960
+ * The chord that opens the palette, which the build refuses to a command.
2961
+ */
2962
+ const PALETTE = "Mod+K";
2963
+ /**
2964
+ * Returns true for a command the palette lists: it is enabled, takes no arguments and resolves
2965
+ * with no result.
2966
+ */
2967
+ function isListed({ command, enabled }) {
2968
+ return enabled && !command.takesArguments && !command.returnsResult;
2969
+ }
2970
+ /**
2971
+ * Returns the rows the palette lists: each command a key could run under its plugin's name, then
2972
+ * each page of the main menu under `Go to`.
2973
+ *
2974
+ * @remarks
2975
+ * A command matches its catalogue's `keywords.<label>` beside its label, where the catalogue
2976
+ * states it.
2977
+ */
2978
+ function useChoices(runtime) {
2979
+ const $ = c(10);
2980
+ const commands = useCommands();
2981
+ const entries = useNavigation();
2982
+ const navigate = useNavigate();
2983
+ const { i18n, t } = useTranslation("host");
2984
+ let t0;
2985
+ if ($[0] !== commands || $[1] !== entries || $[2] !== i18n || $[3] !== navigate || $[4] !== runtime || $[5] !== t) {
2986
+ const words = pluginWordsOf(i18n);
2987
+ let t1;
2988
+ if ($[7] !== navigate || $[8] !== t) {
2989
+ t1 = (t2) => {
2990
+ const { href, label: label_0, routeId } = t2;
2991
+ return {
2992
+ action: {
2993
+ group: t("palette.goTo"),
2994
+ label: label_0,
2995
+ value: `route:${routeId}`
2996
+ },
2997
+ run: () => {
2998
+ navigate({ to: href });
2999
+ }
3000
+ };
3001
+ };
3002
+ $[7] = navigate;
3003
+ $[8] = t;
3004
+ $[9] = t1;
3005
+ } else t1 = $[9];
3006
+ t0 = [...commands.filter(_temp$2).map((status_0) => {
3007
+ const { id, label, plugin } = status_0.command;
3008
+ const keywords = `keywords.${label}`;
3009
+ return {
3010
+ action: {
3011
+ group: words.t(plugin, "plugin.name"),
3012
+ keywords: words.exists(plugin, keywords) ? words.t(plugin, keywords) : void 0,
3013
+ label: status_0.label,
3014
+ shortcut: status_0.keys,
3015
+ value: id
3016
+ },
3017
+ run: () => {
3018
+ runReporting(status_0, runtime, t("commands.failed", { label: status_0.label }));
3019
+ }
3020
+ };
3021
+ }), ...entries.map(t1)];
3022
+ $[0] = commands;
3023
+ $[1] = entries;
3024
+ $[2] = i18n;
3025
+ $[3] = navigate;
3026
+ $[4] = runtime;
3027
+ $[5] = t;
3028
+ $[6] = t0;
3029
+ } else t0 = $[6];
3030
+ return t0;
3031
+ }
3032
+ function _temp$2(status) {
3033
+ return isListed(status);
3034
+ }
3035
+ /**
3036
+ * Renders the palette in a dialog that `Mod+K` opens and closes.
3037
+ *
3038
+ * @remarks
3039
+ * Choosing a row closes the dialog, then runs the command or navigates to the page. The dialog
3040
+ * renders where the palette renders, the `overlay` region at the end of the frame, so it needs no
3041
+ * portal and renders on a server while closed.
3042
+ */
3043
+ function CommandPalette() {
3044
+ const $ = c(38);
3045
+ const [open, setOpen] = useState(false);
3046
+ let t0;
3047
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3048
+ t0 = { strict: false };
3049
+ $[0] = t0;
3050
+ } else t0 = $[0];
3051
+ const { host } = useRouteContext(t0);
3052
+ let t1;
3053
+ if ($[1] !== host) {
3054
+ t1 = internalsOf(host);
3055
+ $[1] = host;
3056
+ $[2] = t1;
3057
+ } else t1 = $[2];
3058
+ const choices = useChoices(t1.runtime);
3059
+ const { t } = useTranslation("host");
3060
+ let t2;
3061
+ if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
3062
+ t2 = () => {
3063
+ setOpen(_temp2);
3064
+ };
3065
+ $[3] = t2;
3066
+ } else t2 = $[3];
3067
+ useHotkey(PALETTE, t2);
3068
+ let t3;
3069
+ if ($[4] !== t) {
3070
+ t3 = t("palette.label");
3071
+ $[4] = t;
3072
+ $[5] = t3;
3073
+ } else t3 = $[5];
3074
+ let t4;
3075
+ if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
3076
+ t4 = (t5) => {
3077
+ const { open: next } = t5;
3078
+ setOpen(next);
3079
+ };
3080
+ $[6] = t4;
3081
+ } else t4 = $[6];
3082
+ let t5;
3083
+ if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
3084
+ t5 = /* @__PURE__ */ jsx(Dialog.Backdrop, {});
3085
+ $[7] = t5;
3086
+ } else t5 = $[7];
3087
+ let t6;
3088
+ if ($[8] !== choices) {
3089
+ t6 = choices.map(_temp3);
3090
+ $[8] = choices;
3091
+ $[9] = t6;
3092
+ } else t6 = $[9];
3093
+ let t7;
3094
+ if ($[10] !== t) {
3095
+ t7 = t("palette.commands");
3096
+ $[10] = t;
3097
+ $[11] = t7;
3098
+ } else t7 = $[11];
3099
+ let t8;
3100
+ if ($[12] !== t) {
3101
+ t8 = (matches) => t("palette.count", { count: matches });
3102
+ $[12] = t;
3103
+ $[13] = t8;
3104
+ } else t8 = $[13];
3105
+ let t9;
3106
+ if ($[14] !== choices) {
3107
+ t9 = (value) => {
3108
+ setOpen(false);
3109
+ for (const choice of choices) if (choice.action.value === value) choice.run();
3110
+ };
3111
+ $[14] = choices;
3112
+ $[15] = t9;
3113
+ } else t9 = $[15];
3114
+ let t10;
3115
+ if ($[16] !== t) {
3116
+ t10 = t("palette.search");
3117
+ $[16] = t;
3118
+ $[17] = t10;
3119
+ } else t10 = $[17];
3120
+ let t11;
3121
+ if ($[18] !== t) {
3122
+ t11 = t("palette.search");
3123
+ $[18] = t;
3124
+ $[19] = t11;
3125
+ } else t11 = $[19];
3126
+ let t12;
3127
+ if ($[20] !== t10 || $[21] !== t11) {
3128
+ t12 = /* @__PURE__ */ jsx(Command.Input, {
3129
+ "aria-label": t10,
3130
+ placeholder: t11
3131
+ });
3132
+ $[20] = t10;
3133
+ $[21] = t11;
3134
+ $[22] = t12;
3135
+ } else t12 = $[22];
3136
+ let t13;
3137
+ if ($[23] !== t) {
3138
+ t13 = t("palette.none");
3139
+ $[23] = t;
3140
+ $[24] = t13;
3141
+ } else t13 = $[24];
3142
+ let t14;
3143
+ if ($[25] !== t13) {
3144
+ t14 = /* @__PURE__ */ jsx(Command.List, { children: /* @__PURE__ */ jsx(Command.Empty, { children: t13 }) });
3145
+ $[25] = t13;
3146
+ $[26] = t14;
3147
+ } else t14 = $[26];
3148
+ let t15;
3149
+ if ($[27] !== t12 || $[28] !== t14 || $[29] !== t6 || $[30] !== t7 || $[31] !== t8 || $[32] !== t9) {
3150
+ t15 = /* @__PURE__ */ jsx(Dialog.Positioner, { children: /* @__PURE__ */ jsx(Dialog.Content, { children: /* @__PURE__ */ jsxs(Command.Root, {
3151
+ actions: t6,
3152
+ "aria-label": t7,
3153
+ count: t8,
3154
+ onRun: t9,
3155
+ children: [t12, t14]
3156
+ }) }) });
3157
+ $[27] = t12;
3158
+ $[28] = t14;
3159
+ $[29] = t6;
3160
+ $[30] = t7;
3161
+ $[31] = t8;
3162
+ $[32] = t9;
3163
+ $[33] = t15;
3164
+ } else t15 = $[33];
3165
+ let t16;
3166
+ if ($[34] !== open || $[35] !== t15 || $[36] !== t3) {
3167
+ t16 = /* @__PURE__ */ jsxs(Dialog.Root, {
3168
+ "aria-label": t3,
3169
+ onOpenChange: t4,
3170
+ open,
3171
+ placement: "top",
3172
+ scrollBehavior: "inside",
3173
+ size: "lg",
3174
+ variant: "plain",
3175
+ children: [t5, t15]
3176
+ });
3177
+ $[34] = open;
3178
+ $[35] = t15;
3179
+ $[36] = t3;
3180
+ $[37] = t16;
3181
+ } else t16 = $[37];
3182
+ return t16;
3183
+ }
3184
+ function _temp3(t0) {
3185
+ const { action } = t0;
3186
+ return action;
3187
+ }
3188
+ function _temp2(shown) {
3189
+ return !shown;
3190
+ }
3191
+ //#endregion
3192
+ //#region src/parts/toasts.tsx
3193
+ /**
3194
+ * Renders the toast region with each toast's title, description and action.
3195
+ *
3196
+ * @remarks
3197
+ * The host renders no glyph of its own, so a toast has no mark and no close button. A toast
3198
+ * closes after its type's duration and on Escape, and the region pauses every toast while a
3199
+ * pointer rests on it or focus is inside it. Alt+T moves focus to the region.
3200
+ */
3201
+ function HostToasts() {
3202
+ const $ = c(8);
3203
+ let t0;
3204
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3205
+ t0 = { strict: false };
3206
+ $[0] = t0;
3207
+ } else t0 = $[0];
3208
+ const { host } = useRouteContext(t0);
3209
+ const { t } = useTranslation("host");
3210
+ let t1;
3211
+ if ($[1] !== t) {
3212
+ t1 = t("toasts.label");
3213
+ $[1] = t;
3214
+ $[2] = t1;
3215
+ } else t1 = $[2];
3216
+ let t2;
3217
+ if ($[3] !== host) {
3218
+ t2 = internalsOf(host);
3219
+ $[3] = host;
3220
+ $[4] = t2;
3221
+ } else t2 = $[4];
3222
+ let t3;
3223
+ if ($[5] !== t1 || $[6] !== t2.runtime.toaster) {
3224
+ t3 = /* @__PURE__ */ jsx(Toast.Region, {
3225
+ label: t1,
3226
+ toaster: t2.runtime.toaster,
3227
+ children: _temp$1
3228
+ });
3229
+ $[5] = t1;
3230
+ $[6] = t2.runtime.toaster;
3231
+ $[7] = t3;
3232
+ } else t3 = $[7];
3233
+ return t3;
3234
+ }
3235
+ function _temp$1(toast) {
3236
+ return /* @__PURE__ */ jsxs(Toast.Root, { children: [/* @__PURE__ */ jsxs(Toast.Content, { children: [/* @__PURE__ */ jsx(Toast.Title, { children: toast.title }), /* @__PURE__ */ jsx(Toast.Description, { children: toast.description })] }), toast.action === void 0 ? null : /* @__PURE__ */ jsx(Toast.ActionTrigger, { children: toast.action.label })] });
3237
+ }
3238
+ //#endregion
3239
+ //#region src/parts/root.tsx
3240
+ /**
3241
+ * Renders the product's frame inside the `layout` region, the `overlay` region after it, and the
3242
+ * keys of the product's commands.
3243
+ *
3244
+ * @remarks
3245
+ * The router renders the root route's component inside a catch boundary of its own, so the
3246
+ * component renders the host's last boundary around the frame, and an error in the frame
3247
+ * renders the failure page and is reported with the target `host`.
3248
+ */
3249
+ function HostRoot(t0) {
3250
+ const $ = c(10);
3251
+ const { children } = t0;
3252
+ let t1;
3253
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3254
+ t1 = { strict: false };
3255
+ $[0] = t1;
3256
+ } else t1 = $[0];
3257
+ const { host } = useRouteContext(t1);
3258
+ let t2;
3259
+ if ($[1] !== host) {
3260
+ t2 = internalsOf(host);
3261
+ $[1] = host;
3262
+ $[2] = t2;
3263
+ } else t2 = $[2];
3264
+ let t3;
3265
+ if ($[3] !== children) {
3266
+ t3 = /* @__PURE__ */ jsx(Slot, {
3267
+ slot: hostContract.slots.layout,
3268
+ children
3269
+ });
3270
+ $[3] = children;
3271
+ $[4] = t3;
3272
+ } else t3 = $[4];
3273
+ let t4;
3274
+ let t5;
3275
+ if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
3276
+ t4 = /* @__PURE__ */ jsxs(Slot, {
3277
+ slot: hostContract.slots.overlay,
3278
+ children: [/* @__PURE__ */ jsx(CommandPalette, {}), /* @__PURE__ */ jsx(HostToasts, {})]
3279
+ });
3280
+ t5 = /* @__PURE__ */ jsx(CommandKeys, {});
3281
+ $[5] = t4;
3282
+ $[6] = t5;
3283
+ } else {
3284
+ t4 = $[5];
3285
+ t5 = $[6];
3286
+ }
3287
+ let t6;
3288
+ if ($[7] !== t2.runtime || $[8] !== t3) {
3289
+ t6 = /* @__PURE__ */ jsxs(LastBoundary, {
3290
+ runtime: t2.runtime,
3291
+ children: [
3292
+ t3,
3293
+ t4,
3294
+ t5
3295
+ ]
3296
+ });
3297
+ $[7] = t2.runtime;
3298
+ $[8] = t3;
3299
+ $[9] = t6;
3300
+ } else t6 = $[9];
3301
+ return t6;
3302
+ }
3303
+ //#endregion
3304
+ //#region src/routes/mapping.ts
3305
+ /**
3306
+ * Returns what the manifest of a route's plugin maps the route to.
3307
+ *
3308
+ * @remarks
3309
+ * The manifest's code states a page as its importer alone, or as an entry with a fallback. A
3310
+ * route whose plugin has no manifest maps to nothing.
3311
+ * @param product - The product, with each installed plugin's manifest.
3312
+ * @param routeId - Qualified id of the route.
3313
+ */
3314
+ function mappingOf(product, routeId) {
3315
+ const plugin = pluginOf(routeId);
3316
+ const name = routeId.slice(plugin.length + 1);
3317
+ const manifest = product.manifests[plugin];
3318
+ const code = (manifest?.code.routes)?.[name];
3319
+ const marker = manifest?.contract.routes[name];
3320
+ const entry = typeof code === "function" ? { component: code } : code;
3321
+ return {
3322
+ component: entry?.component,
3323
+ fallback: entry?.fallback,
3324
+ search: marker?.search
3325
+ };
3326
+ }
3327
+ /**
3328
+ * Returns the one function a page's module exports, as the component the host renders.
3329
+ *
3330
+ * @param module - The module the importer resolved with.
3331
+ * @param routeId - Qualified id of the route, which the error names.
3332
+ * @throws {@link Error} Where the module exports no function or more than one.
3333
+ */
3334
+ function componentOf(module, routeId) {
3335
+ const found = Object.values(module).filter((value) => typeof value === "function");
3336
+ if (found.length !== 1) throw new Error(`The module of ${routeId} exports ${String(found.length)} functions, and a page's module exports one.`);
3337
+ return found[0];
3338
+ }
3339
+ //#endregion
3340
+ //#region src/routes/error.tsx
3341
+ /**
3342
+ * Renders a plugin page that threw: its manifest's fallback where it states one, else the host's
3343
+ * error page, and counts each error towards the page's quarantine.
3344
+ */
3345
+ /**
3346
+ * Marks a route error component that has counted no error yet.
3347
+ */
3348
+ const UNCOUNTED = Symbol("uncounted");
3349
+ /**
3350
+ * Returns the error component of a plugin's route.
3351
+ *
3352
+ * @remarks
3353
+ * The router renders the component for an error that the page or its loader throws. Each error
3354
+ * counts one failure towards the route's quarantine, once however often the component renders
3355
+ * it. The fallback renders in its plugin's scope, without props.
3356
+ * @param routeId - Qualified id of the route.
3357
+ * @param fallback - Imports the component the manifest renders in the page's place, where it
3358
+ * states one.
3359
+ */
3360
+ function routeErrorOf(routeId, fallback) {
3361
+ const target = `route:${routeId}`;
3362
+ const Fallback = fallback === void 0 ? void 0 : lazy(async () => ({ default: componentOf(await fallback(), routeId) }));
3363
+ /**
3364
+ * Renders the fallback or the host's error page, and counts the error.
3365
+ */
3366
+ return function RouteError({ error }) {
3367
+ const { host } = useRouteContext({ strict: false });
3368
+ const { quarantine } = host.stores;
3369
+ const { t } = useTranslation("host");
3370
+ const counted = useRef(UNCOUNTED);
3371
+ useEffect(() => {
3372
+ if (counted.current === error) return;
3373
+ counted.current = error;
3374
+ quarantine.failed(target, error);
3375
+ }, [error, quarantine]);
3376
+ if (Fallback !== void 0) return /* @__PURE__ */ jsx(PluginProvider, {
3377
+ pluginId: pluginOf(routeId),
3378
+ children: /* @__PURE__ */ jsx(Suspense, {
3379
+ fallback: null,
3380
+ children: /* @__PURE__ */ jsx(Fallback, {})
3381
+ })
3382
+ });
3383
+ return /* @__PURE__ */ jsx(EmptyState.Root, { children: /* @__PURE__ */ jsxs(EmptyState.Content, { children: [/* @__PURE__ */ jsx(EmptyState.Title, {
3384
+ as: "h1",
3385
+ children: t("page.failed.title")
3386
+ }), /* @__PURE__ */ jsx(EmptyState.Description, { children: t("page.failed.description") })] }) });
3387
+ };
3388
+ }
3389
+ //#endregion
3390
+ //#region src/routes/evaluate.ts
3391
+ /**
3392
+ * Evaluates a plugin route's condition before the route loads, against the host in the router's
3393
+ * context.
3394
+ */
3395
+ /**
3396
+ * Returns the plugin whose state stops a plugin, with that plugin's reason: the plugin itself where
3397
+ * a switch or its kill switch stopped it, else the first plugin it requires without `optional` that
3398
+ * a switch or a kill switch stopped. Returns undefined where a condition stops the plugin.
3399
+ *
3400
+ * @remarks
3401
+ * A plugin the walk has passed reads as stopped by a condition, so a ring of requirements, which
3402
+ * the build refuses, ends the walk.
3403
+ * @param pluginId - Id of the plugin that is not on.
3404
+ * @param plugins - The installed plugins, with the plugins each requires.
3405
+ * @param availability - Every installed plugin's availability, by plugin id.
3406
+ * @param seen - Ids of the plugins the walk has passed.
3407
+ */
3408
+ function stoppedBy(pluginId, plugins, availability, seen = /* @__PURE__ */ new Set()) {
3409
+ const reason = availability[pluginId]?.reason;
3410
+ if (reason === "off" || reason === "unavailable") return {
3411
+ plugin: pluginId,
3412
+ reason
3413
+ };
3414
+ if (reason !== "requirement" || seen.has(pluginId)) return void 0;
3415
+ const walked = /* @__PURE__ */ new Set([...seen, pluginId]);
3416
+ return plugins.filter(({ id }) => id === pluginId).flatMap(({ requires }) => requires).filter(({ optional }) => optional !== true).map((required) => stoppedBy(required.pluginId, plugins, availability, walked)).find((found) => found !== void 0);
3417
+ }
3418
+ /**
3419
+ * Returns true where a condition requires a signed-in person at its top level or in a top-level
3420
+ * `allOf`.
3421
+ */
3422
+ function requiresSignIn(when) {
3423
+ return when.authenticated === true || (when.allOf?.some((one) => one.authenticated === true) ?? false);
3424
+ }
3425
+ /**
3426
+ * Returns the evaluator of the product's plugin routes, which reads the host in the router's
3427
+ * context.
3428
+ *
3429
+ * @remarks
3430
+ * The evaluator throws the router's not-found error where the route's plugin is not on, with the
3431
+ * plugin and its reason as data where a switch or a kill switch stopped it, and where the route
3432
+ * is quarantined, with the target as data. Where the condition is false for a person who is not
3433
+ * signed in, the product states a sign-in route, and the condition requires sign-in at its top
3434
+ * level or in a top-level `allOf`, it throws the router's redirect to the sign-in route, with the
3435
+ * address the navigation enters as the `redirect` search. Otherwise it returns the condition.
3436
+ * @param product - The installed plugins and the sign-in route.
3437
+ */
3438
+ function evaluatorOf(product) {
3439
+ return ({ pluginId, routeId, when }, { host, routes }, location) => {
3440
+ const context = conditionContextOf(host.stores);
3441
+ const target = `route:${routeId}`;
3442
+ if (!context.on(pluginId)) {
3443
+ const data = stoppedBy(pluginId, product.plugins, host.stores.availability.get());
3444
+ throw data === void 0 ? notFound() : notFound({ data });
3445
+ }
3446
+ if (host.stores.quarantine.get().has(target)) throw notFound({ data: {
3447
+ reason: "quarantined",
3448
+ target
3449
+ } });
3450
+ if (when === void 0 || evaluateWhen(when, context)) return true;
3451
+ if (product.signIn !== void 0 && !context.authenticated && requiresSignIn(when)) throw redirect({
3452
+ search: { redirect: location?.href },
3453
+ to: routeHref(routes, product.signIn)
3454
+ });
3455
+ return false;
3456
+ };
3457
+ }
3458
+ //#endregion
3459
+ //#region src/routes/loader.ts
3460
+ /**
3461
+ * Builds the loader of a route from the queries the route's declaration reads.
3462
+ */
3463
+ /**
3464
+ * Returns the queries a page reads, in the form the data foundation's loader takes.
3465
+ */
3466
+ function needsOf(product, route) {
3467
+ return route.data.flatMap(({ query, variables }) => product.queries.filter(({ id }) => id === query).map((declared) => ({
3468
+ operation: defineQuery(declared.operation.id),
3469
+ resources: declared.records,
3470
+ variables
3471
+ })));
3472
+ }
3473
+ /**
3474
+ * Returns the loader of a route that reads data, or undefined for a route that reads none.
3475
+ *
3476
+ * @remarks
3477
+ * The loader loads each query through the data client in the router's context, with the variables
3478
+ * the route's parameters and search contain.
3479
+ * @param product - The product, whose queries the route's data names.
3480
+ * @param route - The route as the build resolved it.
3481
+ */
3482
+ function loaderOf(product, route) {
3483
+ return route.data.length === 0 ? void 0 : loadNeeds(needsOf(product, route));
3484
+ }
3485
+ //#endregion
3486
+ //#region src/routes/page.ts
3487
+ /**
3488
+ * Loads a plugin's page and renders it as its route's component: in the plugin's scope, with the
3489
+ * extensions placed around the route, and with the route's count of failed renders returned to zero
3490
+ * after it commits.
3491
+ */
3492
+ /**
3493
+ * Returns the route component of a page: a plugin's page, the settings frame or a settings page.
3494
+ *
3495
+ * @remarks
3496
+ * The component reads the host from the router's context. A render of the page that throws
3497
+ * commits nothing, so the route's count of failed renders returns to zero only after a render
3498
+ * that commits.
3499
+ * @param pluginId - Id of the plugin that declares the route, or the settings page, whose scope
3500
+ * the page renders in.
3501
+ * @param routeId - The route's qualified id, under which its failed renders and the extensions
3502
+ * around it are recorded.
3503
+ * @param page - The component the route renders: a plugin's page, the settings frame or a settings
3504
+ * page.
3505
+ */
3506
+ function hostPageOf(pluginId, routeId, page) {
3507
+ const target = `route:${routeId}`;
3508
+ /**
3509
+ * Renders the page in its plugin's scope, with the extensions placed around its route.
3510
+ */
3511
+ return function HostPage() {
3512
+ const { host } = useRouteContext({ strict: false });
3513
+ const { quarantine } = host.stores;
3514
+ useEffect(() => {
3515
+ quarantine.rendered(target);
3516
+ });
3517
+ return createElement(PluginProvider, { pluginId }, createElement(RouteDecorations, { routeId }, createElement(page)));
3518
+ };
3519
+ }
3520
+ /**
3521
+ * Imports a plugin's page and, in parallel, every module of the plugins whose extensions load with
3522
+ * it, and returns the module of the page's route component.
3523
+ *
3524
+ * @remarks
3525
+ * A module of the other plugins that fails to import leaves the page to render, and its extension
3526
+ * reports the failure when it renders.
3527
+ * @param product - The product, with each installed plugin's manifest.
3528
+ * @param route - The route as the build resolved it, with the plugins that load with its page.
3529
+ * @returns The module whose default export is the route component.
3530
+ * @throws {@link Error} Where no installed manifest maps the route to code, or where the page's
3531
+ * module exports no function or more than one.
3532
+ */
3533
+ async function loadPage(product, route) {
3534
+ const { component } = mappingOf(product, route.id);
3535
+ if (component === void 0) throw new Error(`No manifest maps the route ${route.id} to code.`);
3536
+ const [module] = await Promise.all([component(), importPlugins(product.manifests, new Set(route.loads))]);
3537
+ return { default: hostPageOf(route.plugin, route.id, componentOf(module, route.id)) };
3538
+ }
3539
+ //#endregion
3540
+ //#region src/settings/placement.ts
3541
+ /**
3542
+ * Places a product's settings sections on the settings pages the settings menu lists: each on its
3543
+ * target page, else on the first listed page of its plugin.
3544
+ *
3545
+ * @remarks
3546
+ * A page the menu lists is a page the host renders: its plugin is on and its condition is true.
3547
+ * A section whose target page is not listed and whose plugin has no listed page renders nowhere,
3548
+ * and the host reports it as unplaced.
3549
+ */
3550
+ /**
3551
+ * Rank of a section without an `order`, which follows every ranked section.
3552
+ */
3553
+ const UNRANKED = Number.MAX_SAFE_INTEGER;
3554
+ /**
3555
+ * Returns the id of the route the host compiles for a settings page: `host/settings/<page id>`.
3556
+ *
3557
+ * @param pageId - Qualified id of the settings page.
3558
+ */
3559
+ function pageRouteOf(pageId) {
3560
+ return `${hostContract.routes.settings.id}/${pageId}`;
3561
+ }
3562
+ /**
3563
+ * Returns the id of the page a section renders on: its target where the menu lists it, else the
3564
+ * first listed page of its plugin, else undefined.
3565
+ *
3566
+ * @param section - A section, with its target page and its plugin.
3567
+ * @param listed - Ids of the routes the settings menu lists, in menu order.
3568
+ * @param pages - Every settings page.
3569
+ */
3570
+ function placedOn(section, listed, pages) {
3571
+ if (listed.includes(pageRouteOf(section.target))) return section.target;
3572
+ return listed.map((routeId) => pages.find((page) => pageRouteOf(page.id) === routeId)).find((page) => page?.plugin === section.plugin)?.id;
3573
+ }
3574
+ /**
3575
+ * Returns the sections that render on a page, ranked by `order`, the unranked after the ranked in
3576
+ * install order.
3577
+ *
3578
+ * @param pageId - Qualified id of the page.
3579
+ * @param sections - The sections that render, in install order.
3580
+ * @param listed - Ids of the routes the settings menu lists, in menu order.
3581
+ * @param pages - Every settings page.
3582
+ */
3583
+ function sectionsOn(pageId, sections, listed, pages) {
3584
+ return sections.filter((section) => placedOn(section, listed, pages) === pageId).toSorted((one, other) => (one.order ?? UNRANKED) - (other.order ?? UNRANKED));
3585
+ }
3586
+ /**
3587
+ * Returns the sections that render on no page.
3588
+ *
3589
+ * @param sections - The sections that render, in install order.
3590
+ * @param listed - Ids of the routes the settings menu lists, in menu order.
3591
+ * @param pages - Every settings page.
3592
+ */
3593
+ function unplacedOf(sections, listed, pages) {
3594
+ return sections.filter((section) => placedOn(section, listed, pages) === void 0);
3595
+ }
3596
+ //#endregion
3597
+ //#region src/settings/use-sections.ts
3598
+ /**
3599
+ * Reads the settings sections that render, and the settings pages the settings menu lists, in a
3600
+ * part under the settings route.
3601
+ */
3602
+ /**
3603
+ * Returns the ids of the sections whose plugin is on and whose condition is true, joined by spaces,
3604
+ * which no qualified id contains.
3605
+ */
3606
+ function shownOf(sections, stores) {
3607
+ const context = conditionContextOf(stores);
3608
+ return sections.filter(({ plugin, when }) => context.on(plugin) && evaluateWhen(when, context)).map(({ id }) => id).join(" ");
3609
+ }
3610
+ /**
3611
+ * Returns the sections that render and the pages the settings menu lists, and renders again when a
3612
+ * plugin turns on or off, a flag changes or the session changes.
3613
+ */
3614
+ function useSections() {
3615
+ const $ = c(22);
3616
+ let t0;
3617
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3618
+ t0 = { strict: false };
3619
+ $[0] = t0;
3620
+ } else t0 = $[0];
3621
+ const { host } = useRouteContext(t0);
3622
+ const { availability, flags, session } = host.stores;
3623
+ const { settings } = useResolvedProduct();
3624
+ const entries = useNavigation(hostContract.menus.settings);
3625
+ let t1;
3626
+ if ($[1] !== availability || $[2] !== flags || $[3] !== session) {
3627
+ t1 = (changed) => {
3628
+ const stops = [
3629
+ availability,
3630
+ flags,
3631
+ session
3632
+ ].map((store) => store.subscribe(changed));
3633
+ return () => {
3634
+ for (const stop of stops) stop();
3635
+ };
3636
+ };
3637
+ $[1] = availability;
3638
+ $[2] = flags;
3639
+ $[3] = session;
3640
+ $[4] = t1;
3641
+ } else t1 = $[4];
3642
+ const subscribe = t1;
3643
+ let t2;
3644
+ if ($[5] !== availability || $[6] !== flags || $[7] !== session || $[8] !== settings.sections) {
3645
+ t2 = () => shownOf(settings.sections, {
3646
+ availability,
3647
+ flags,
3648
+ session
3649
+ });
3650
+ $[5] = availability;
3651
+ $[6] = flags;
3652
+ $[7] = session;
3653
+ $[8] = settings.sections;
3654
+ $[9] = t2;
3655
+ } else t2 = $[9];
3656
+ const read = t2;
3657
+ const t3 = useSyncExternalStore(subscribe, read, read);
3658
+ let t4;
3659
+ if ($[10] !== t3) {
3660
+ t4 = new Set(t3.split(" "));
3661
+ $[10] = t3;
3662
+ $[11] = t4;
3663
+ } else t4 = $[11];
3664
+ const shown = t4;
3665
+ let t5;
3666
+ if ($[12] !== entries) {
3667
+ t5 = entries.map(_temp);
3668
+ $[12] = entries;
3669
+ $[13] = t5;
3670
+ } else t5 = $[13];
3671
+ let t6;
3672
+ if ($[14] !== settings.sections || $[15] !== shown) {
3673
+ let t7;
3674
+ if ($[17] !== shown) {
3675
+ t7 = (t8) => {
3676
+ const { id } = t8;
3677
+ return shown.has(id);
3678
+ };
3679
+ $[17] = shown;
3680
+ $[18] = t7;
3681
+ } else t7 = $[18];
3682
+ t6 = settings.sections.filter(t7);
3683
+ $[14] = settings.sections;
3684
+ $[15] = shown;
3685
+ $[16] = t6;
3686
+ } else t6 = $[16];
3687
+ let t7;
3688
+ if ($[19] !== t5 || $[20] !== t6) {
3689
+ t7 = {
3690
+ listed: t5,
3691
+ shown: t6
3692
+ };
3693
+ $[19] = t5;
3694
+ $[20] = t6;
3695
+ $[21] = t7;
3696
+ } else t7 = $[21];
3697
+ return t7;
3698
+ }
3699
+ function _temp(t0) {
3700
+ const { routeId } = t0;
3701
+ return routeId;
3702
+ }
3703
+ //#endregion
3704
+ //#region src/settings/unplaced.ts
3705
+ /**
3706
+ * Reports the settings sections that render on no settings page.
3707
+ */
3708
+ /**
3709
+ * Reports each section that renders on no page as `unplaced`, with its target page as the slot,
3710
+ * once while the caller is mounted.
3711
+ *
3712
+ * @remarks
3713
+ * A section is unplaced where its plugin is on, its condition is true, its target page is not
3714
+ * listed and its plugin has no listed page. A section that becomes unplaced after a change is
3715
+ * reported then.
3716
+ */
3717
+ function useUnplacedReports() {
3718
+ const $ = c(11);
3719
+ let t0;
3720
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3721
+ t0 = { strict: false };
3722
+ $[0] = t0;
3723
+ } else t0 = $[0];
3724
+ const { host } = useRouteContext(t0);
3725
+ let t1;
3726
+ if ($[1] !== host) {
3727
+ t1 = internalsOf(host);
3728
+ $[1] = host;
3729
+ $[2] = t1;
3730
+ } else t1 = $[2];
3731
+ const { report } = t1.runtime;
3732
+ const { settings } = useResolvedProduct();
3733
+ const { listed, shown } = useSections();
3734
+ let t2;
3735
+ if ($[3] !== listed || $[4] !== settings.pages || $[5] !== shown) {
3736
+ t2 = unplacedOf(shown, listed, settings.pages);
3737
+ $[3] = listed;
3738
+ $[4] = settings.pages;
3739
+ $[5] = shown;
3740
+ $[6] = t2;
3741
+ } else t2 = $[6];
3742
+ const unplaced = t2;
3743
+ let t3;
3744
+ if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
3745
+ t3 = /* @__PURE__ */ new Set();
3746
+ $[7] = t3;
3747
+ } else t3 = $[7];
3748
+ const reported = useRef(t3);
3749
+ let t4;
3750
+ if ($[8] !== report || $[9] !== unplaced) {
3751
+ t4 = () => {
3752
+ for (const { id, target } of unplaced) if (!reported.current.has(id)) {
3753
+ reported.current.add(id);
3754
+ report({
3755
+ kind: "unplaced",
3756
+ slot: target,
3757
+ target: id
3758
+ });
3759
+ }
3760
+ };
3761
+ $[8] = report;
3762
+ $[9] = unplaced;
3763
+ $[10] = t4;
3764
+ } else t4 = $[10];
3765
+ useEffect(t4);
3766
+ }
3767
+ //#endregion
3768
+ //#region src/settings/frame.tsx
3769
+ /**
3770
+ * Renders the navigation package's link as the router's link.
3771
+ */
3772
+ const MenuLink = createLink(Link);
3773
+ /**
3774
+ * Renders the settings frame around the settings page the router matched.
3775
+ *
3776
+ * @returns The page, with its title, the settings menu and the matched page in its body.
3777
+ */
3778
+ function SettingsFrame() {
3779
+ const $ = c(20);
3780
+ const { t } = useTranslation("host");
3781
+ const entries = useNavigation(hostContract.menus.settings);
3782
+ const current = useDeclaredRoute()?.id;
3783
+ useUnplacedReports();
3784
+ let t0;
3785
+ if ($[0] !== t) {
3786
+ t0 = t("settings.title");
3787
+ $[0] = t;
3788
+ $[1] = t0;
3789
+ } else t0 = $[1];
3790
+ let t1;
3791
+ if ($[2] !== t0) {
3792
+ t1 = /* @__PURE__ */ jsx(Page.Header, { children: /* @__PURE__ */ jsx(Page.Title, { children: t0 }) });
3793
+ $[2] = t0;
3794
+ $[3] = t1;
3795
+ } else t1 = $[3];
3796
+ let t2;
3797
+ if ($[4] !== t) {
3798
+ t2 = t("settings.menu");
3799
+ $[4] = t;
3800
+ $[5] = t2;
3801
+ } else t2 = $[5];
3802
+ let t3;
3803
+ if ($[6] !== current || $[7] !== entries) {
3804
+ let t4;
3805
+ if ($[9] !== current) {
3806
+ t4 = (t5) => {
3807
+ const { href, label, routeId } = t5;
3808
+ return /* @__PURE__ */ jsx(MenuLink, {
3809
+ inherit: routeId !== current,
3810
+ to: href,
3811
+ variant: "plain",
3812
+ children: label
3813
+ }, routeId);
3814
+ };
3815
+ $[9] = current;
3816
+ $[10] = t4;
3817
+ } else t4 = $[10];
3818
+ t3 = entries.map(t4);
3819
+ $[6] = current;
3820
+ $[7] = entries;
3821
+ $[8] = t3;
3822
+ } else t3 = $[8];
3823
+ let t4;
3824
+ if ($[11] !== t3) {
3825
+ t4 = /* @__PURE__ */ jsx(Stack, {
3826
+ direction: "row",
3827
+ gap: "lg",
3828
+ wrap: true,
3829
+ children: t3
3830
+ });
3831
+ $[11] = t3;
3832
+ $[12] = t4;
3833
+ } else t4 = $[12];
3834
+ let t5;
3835
+ if ($[13] !== t2 || $[14] !== t4) {
3836
+ t5 = /* @__PURE__ */ jsx(Page.Nav, {
3837
+ "aria-label": t2,
3838
+ children: t4
3839
+ });
3840
+ $[13] = t2;
3841
+ $[14] = t4;
3842
+ $[15] = t5;
3843
+ } else t5 = $[15];
3844
+ let t6;
3845
+ if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
3846
+ t6 = /* @__PURE__ */ jsx(Page.Body, { children: /* @__PURE__ */ jsx(Outlet, {}) });
3847
+ $[16] = t6;
3848
+ } else t6 = $[16];
3849
+ let t7;
3850
+ if ($[17] !== t1 || $[18] !== t5) {
3851
+ t7 = /* @__PURE__ */ jsxs(Page.Root, { children: [
3852
+ t1,
3853
+ t5,
3854
+ t6
3855
+ ] });
3856
+ $[17] = t1;
3857
+ $[18] = t5;
3858
+ $[19] = t7;
3859
+ } else t7 = $[19];
3860
+ return t7;
3861
+ }
3862
+ //#endregion
3863
+ //#region src/settings/index-page.tsx
3864
+ /**
3865
+ * Replaces the settings route's own address with the first page of the settings menu.
3866
+ *
3867
+ * @remarks
3868
+ * The menu always lists the host's Plugins page: the host is always on, and the page's condition
3869
+ * is the product's, which the settings route has already passed. The first entry sorts by rank,
3870
+ * then by translated title, so the redirect runs in the page and not before the route loads, and
3871
+ * a server renders the frame without a page.
3872
+ * @returns The router's redirect to the first page.
3873
+ */
3874
+ function SettingsIndex() {
3875
+ const $ = c(2);
3876
+ const first = useNavigation(hostContract.menus.settings)[0];
3877
+ let t0;
3878
+ if ($[0] !== first.href) {
3879
+ t0 = /* @__PURE__ */ jsx(Navigate, {
3880
+ replace: true,
3881
+ to: first.href
3882
+ });
3883
+ $[0] = first.href;
3884
+ $[1] = t0;
3885
+ } else t0 = $[1];
3886
+ return t0;
3887
+ }
3888
+ //#endregion
3889
+ //#region src/settings/dependents.ts
3890
+ /**
3891
+ * Returns the plugins that are on and require a plugin without `optional`, directly or through
3892
+ * another plugin that does, in install order.
3893
+ *
3894
+ * @remarks
3895
+ * A plugin is not on while a plugin it requires without `optional` is not on, so switching the
3896
+ * plugin off turns these off with it. The set of plugins turned off grows until no plugin joins
3897
+ * it, so the walk ends on a ring of requirements too.
3898
+ * @param plugins - The installed plugins, with the plugins each requires.
3899
+ * @param pluginId - Id of the plugin being switched off.
3900
+ * @param on - Returns true where a plugin is on.
3901
+ */
3902
+ function dependentsOf(plugins, pluginId, on) {
3903
+ const off = /* @__PURE__ */ new Set([pluginId]);
3904
+ let grown = true;
3905
+ while (grown) {
3906
+ grown = false;
3907
+ for (const plugin of plugins) if (plugin.requires.some((required) => required.optional !== true && off.has(required.pluginId)) && !off.has(plugin.id) && on(plugin.id)) {
3908
+ off.add(plugin.id);
3909
+ grown = true;
3910
+ }
3911
+ }
3912
+ return plugins.filter((plugin) => plugin.id !== pluginId && off.has(plugin.id));
3913
+ }
3914
+ //#endregion
3915
+ //#region src/settings/confirmation.tsx
3916
+ /**
3917
+ * Asks a person to confirm a switch that turns other plugins off with the plugin they switch off.
3918
+ */
3919
+ /**
3920
+ * Renders a warning that names the plugins that turn off with the plugin, with a button that keeps
3921
+ * it on and a button that turns it off.
3922
+ *
3923
+ * @remarks
3924
+ * The warning mounts with its words, which a screen reader does not reliably announce from a new
3925
+ * live region, so the confirmation announces them through the announcer's region, politely,
3926
+ * without moving focus off the switch. The plugins are listed in the person's language.
3927
+ * @returns The warning.
3928
+ */
3929
+ function Confirmation(t0) {
3930
+ const $ = c(41);
3931
+ const { dependents, name, onSettle } = t0;
3932
+ const { i18n, t } = useTranslation("host");
3933
+ const announce = useAnnounce();
3934
+ let t1;
3935
+ let title;
3936
+ if ($[0] !== dependents || $[1] !== i18n.language || $[2] !== name || $[3] !== t) {
3937
+ const plugins = new Intl.ListFormat(i18n.language, { type: "conjunction" }).format(dependents);
3938
+ let t2;
3939
+ if ($[6] !== name || $[7] !== t) {
3940
+ t2 = t("plugins.confirm.title", { plugin: name });
3941
+ $[6] = name;
3942
+ $[7] = t;
3943
+ $[8] = t2;
3944
+ } else t2 = $[8];
3945
+ title = t2;
3946
+ t1 = t("plugins.confirm.description", {
3947
+ count: dependents.length,
3948
+ plugins
3949
+ });
3950
+ $[0] = dependents;
3951
+ $[1] = i18n.language;
3952
+ $[2] = name;
3953
+ $[3] = t;
3954
+ $[4] = t1;
3955
+ $[5] = title;
3956
+ } else {
3957
+ t1 = $[4];
3958
+ title = $[5];
3959
+ }
3960
+ const description = t1;
3961
+ let t2;
3962
+ let t3;
3963
+ if ($[9] !== announce || $[10] !== description || $[11] !== title) {
3964
+ t2 = () => {
3965
+ announce(`${title} ${description}`);
3966
+ };
3967
+ t3 = [
3968
+ announce,
3969
+ description,
3970
+ title
3971
+ ];
3972
+ $[9] = announce;
3973
+ $[10] = description;
3974
+ $[11] = title;
3975
+ $[12] = t2;
3976
+ $[13] = t3;
3977
+ } else {
3978
+ t2 = $[12];
3979
+ t3 = $[13];
3980
+ }
3981
+ useEffect(t2, t3);
3982
+ let t4;
3983
+ if ($[14] !== title) {
3984
+ t4 = /* @__PURE__ */ jsx(Alert.Title, { children: title });
3985
+ $[14] = title;
3986
+ $[15] = t4;
3987
+ } else t4 = $[15];
3988
+ let t5;
3989
+ if ($[16] !== description) {
3990
+ t5 = /* @__PURE__ */ jsx(Alert.Description, { children: description });
3991
+ $[16] = description;
3992
+ $[17] = t5;
3993
+ } else t5 = $[17];
3994
+ let t6;
3995
+ if ($[18] !== t4 || $[19] !== t5) {
3996
+ t6 = /* @__PURE__ */ jsxs(Alert.Content, { children: [t4, t5] });
3997
+ $[18] = t4;
3998
+ $[19] = t5;
3999
+ $[20] = t6;
4000
+ } else t6 = $[20];
4001
+ let t7;
4002
+ if ($[21] !== onSettle) {
4003
+ t7 = () => {
4004
+ onSettle(false);
4005
+ };
4006
+ $[21] = onSettle;
4007
+ $[22] = t7;
4008
+ } else t7 = $[22];
4009
+ let t8;
4010
+ if ($[23] !== t) {
4011
+ t8 = t("plugins.confirm.keep");
4012
+ $[23] = t;
4013
+ $[24] = t8;
4014
+ } else t8 = $[24];
4015
+ let t9;
4016
+ if ($[25] !== t7 || $[26] !== t8) {
4017
+ t9 = /* @__PURE__ */ jsx(Button, {
4018
+ onClick: t7,
4019
+ size: "sm",
4020
+ variant: "outline",
4021
+ children: t8
4022
+ });
4023
+ $[25] = t7;
4024
+ $[26] = t8;
4025
+ $[27] = t9;
4026
+ } else t9 = $[27];
4027
+ let t10;
4028
+ if ($[28] !== onSettle) {
4029
+ t10 = () => {
4030
+ onSettle(true);
4031
+ };
4032
+ $[28] = onSettle;
4033
+ $[29] = t10;
4034
+ } else t10 = $[29];
4035
+ let t11;
4036
+ if ($[30] !== t) {
4037
+ t11 = t("plugins.confirm.action");
4038
+ $[30] = t;
4039
+ $[31] = t11;
4040
+ } else t11 = $[31];
4041
+ let t12;
4042
+ if ($[32] !== t10 || $[33] !== t11) {
4043
+ t12 = /* @__PURE__ */ jsx(Button, {
4044
+ onClick: t10,
4045
+ palette: "error",
4046
+ size: "sm",
4047
+ children: t11
4048
+ });
4049
+ $[32] = t10;
4050
+ $[33] = t11;
4051
+ $[34] = t12;
4052
+ } else t12 = $[34];
4053
+ let t13;
4054
+ if ($[35] !== t12 || $[36] !== t9) {
4055
+ t13 = /* @__PURE__ */ jsxs(Alert.Aside, { children: [t9, t12] });
4056
+ $[35] = t12;
4057
+ $[36] = t9;
4058
+ $[37] = t13;
4059
+ } else t13 = $[37];
4060
+ let t14;
4061
+ if ($[38] !== t13 || $[39] !== t6) {
4062
+ t14 = /* @__PURE__ */ jsxs(Alert.Root, {
4063
+ live: "off",
4064
+ status: "warning",
4065
+ children: [t6, t13]
4066
+ });
4067
+ $[38] = t13;
4068
+ $[39] = t6;
4069
+ $[40] = t14;
4070
+ } else t14 = $[40];
4071
+ return t14;
4072
+ }
4073
+ //#endregion
4074
+ //#region src/settings/plugin-row.tsx
4075
+ /**
4076
+ * Renders one installed plugin on the Plugins page: its name, its description, its state and the
4077
+ * switch a person turns it on and off with.
4078
+ */
4079
+ /**
4080
+ * Keys of the words that state why a plugin is not on, by the reason, where its switch does not
4081
+ * state it.
4082
+ */
4083
+ const REASONS = {
4084
+ condition: "plugins.reason.condition",
4085
+ requirement: "plugins.reason.requirement",
4086
+ unavailable: "plugins.reason.unavailable"
4087
+ };
4088
+ /**
4089
+ * Returns the key of the words that state why a plugin is always on or not on, or undefined where
4090
+ * its switch states it.
4091
+ */
4092
+ function noteOf(status) {
4093
+ if (!status.switchable) return "plugins.always";
4094
+ return status.reason === void 0 || status.reason === "off" ? void 0 : REASONS[status.reason];
4095
+ }
4096
+ /**
4097
+ * Renders the plugin's switch, which is disabled and on for a locked plugin, and the confirmation
4098
+ * where switching it off turns other plugins off.
4099
+ *
4100
+ * @remarks
4101
+ * The switch is on until the person confirms. Once they settle, the confirmation closes and
4102
+ * focus returns to the switch, because the button that had focus is gone.
4103
+ * @param props - The plugin's state, the person's switch, the plugins that turn off with it and the
4104
+ * callback that switches it.
4105
+ * @returns The list's item.
4106
+ */
4107
+ function PluginRow(t0) {
4108
+ const $ = c(6);
4109
+ const { dependents, onSwitch, status, switched } = t0;
4110
+ const { i18n, t } = useTranslation("host");
4111
+ const words = pluginWordsOf(i18n);
4112
+ const [pending, setPending] = useState(false);
4113
+ const input = useId();
4114
+ const name = words.t(status.id, "plugin.name");
4115
+ const note = noteOf(status);
4116
+ let t1;
4117
+ if ($[0] !== input) {
4118
+ t1 = { hiddenInput: input };
4119
+ $[0] = input;
4120
+ $[1] = t1;
4121
+ } else t1 = $[1];
4122
+ let t2;
4123
+ if ($[2] !== dependents || $[3] !== onSwitch) {
4124
+ t2 = (t3) => {
4125
+ const { checked } = t3;
4126
+ if (!checked && dependents.length > 0) setPending(true);
4127
+ else onSwitch(checked);
4128
+ };
4129
+ $[2] = dependents;
4130
+ $[3] = onSwitch;
4131
+ $[4] = t2;
4132
+ } else t2 = $[4];
4133
+ let t3;
4134
+ if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
4135
+ t3 = /* @__PURE__ */ jsx(Switch.Control, { children: /* @__PURE__ */ jsx(Switch.Thumb, {}) });
4136
+ $[5] = t3;
4137
+ } else t3 = $[5];
4138
+ return /* @__PURE__ */ jsxs(List.Item, { children: [/* @__PURE__ */ jsxs(Switch.Root, {
4139
+ checked: switched,
4140
+ disabled: !status.switchable,
4141
+ ids: t1,
4142
+ onCheckedChange: t2,
4143
+ spread: true,
4144
+ children: [/* @__PURE__ */ jsx(Switch.Label, { children: /* @__PURE__ */ jsxs(Stack, {
4145
+ as: "span",
4146
+ gap: "xs",
4147
+ children: [
4148
+ /* @__PURE__ */ jsx(Text, {
4149
+ as: "span",
4150
+ size: "sm",
4151
+ children: name
4152
+ }),
4153
+ /* @__PURE__ */ jsx(Text, {
4154
+ as: "span",
4155
+ size: "sm",
4156
+ tone: "muted",
4157
+ children: words.t(status.id, "plugin.description")
4158
+ }),
4159
+ note === void 0 ? null : /* @__PURE__ */ jsx(Text, {
4160
+ as: "span",
4161
+ size: "sm",
4162
+ tone: "muted",
4163
+ children: t(note)
4164
+ })
4165
+ ]
4166
+ }) }), t3]
4167
+ }), pending ? /* @__PURE__ */ jsx(Confirmation, {
4168
+ dependents: dependents.map((t4) => {
4169
+ const { id } = t4;
4170
+ return words.t(id, "plugin.name");
4171
+ }),
4172
+ name,
4173
+ onSettle: (off) => {
4174
+ flushSync(() => {
4175
+ setPending(false);
4176
+ });
4177
+ if (off) onSwitch(false);
4178
+ document.querySelector(`[id="${input}"]`).focus();
4179
+ }
4180
+ }) : null] });
4181
+ }
4182
+ //#endregion
4183
+ //#region src/settings/plugin-list.tsx
4184
+ /**
4185
+ * Renders the list of installed plugins on the host's Plugins page.
4186
+ */
4187
+ /**
4188
+ * Renders a section with a row per installed plugin, in install order, and writes a switch to the
4189
+ * host's switch store.
4190
+ *
4191
+ * @remarks
4192
+ * A switch writes under the person and the tenant of the session, so the choice applies to them
4193
+ * alone. The host evaluates every plugin again after a switch, and the pages, menus, commands and
4194
+ * settings of a plugin that turns off leave the page without a reload.
4195
+ * @returns The section.
4196
+ */
4197
+ function PluginList() {
4198
+ const $ = c(24);
4199
+ let t0;
4200
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
4201
+ t0 = { strict: false };
4202
+ $[0] = t0;
4203
+ } else t0 = $[0];
4204
+ const { host } = useRouteContext(t0);
4205
+ const { switches } = internalsOf(host);
4206
+ const { t } = useTranslation("host");
4207
+ const { plugins } = useResolvedProduct();
4208
+ const statuses = usePluginStatuses();
4209
+ const switched = useSyncExternalStore(switches.subscribe, switches.get, switches.get);
4210
+ let t1;
4211
+ if ($[1] !== statuses) {
4212
+ t1 = (pluginId) => statuses.some((status) => status.id === pluginId && status.on);
4213
+ $[1] = statuses;
4214
+ $[2] = t1;
4215
+ } else t1 = $[2];
4216
+ const on = t1;
4217
+ const t2 = Section;
4218
+ const t3 = true;
4219
+ let t4;
4220
+ if ($[3] !== t) {
4221
+ t4 = t("plugins.title");
4222
+ $[3] = t;
4223
+ $[4] = t4;
4224
+ } else t4 = $[4];
4225
+ let t5;
4226
+ if ($[5] !== t4) {
4227
+ t5 = /* @__PURE__ */ jsx(Section.Title, { children: t4 });
4228
+ $[5] = t4;
4229
+ $[6] = t5;
4230
+ } else t5 = $[6];
4231
+ let t6;
4232
+ if ($[7] !== t) {
4233
+ t6 = t("plugins.about");
4234
+ $[7] = t;
4235
+ $[8] = t6;
4236
+ } else t6 = $[8];
4237
+ let t7;
4238
+ if ($[9] !== t6) {
4239
+ t7 = /* @__PURE__ */ jsx(Section.Description, { children: t6 });
4240
+ $[9] = t6;
4241
+ $[10] = t7;
4242
+ } else t7 = $[10];
4243
+ let t8;
4244
+ if ($[11] !== t5 || $[12] !== t7) {
4245
+ t8 = /* @__PURE__ */ jsxs(Section.Header, { children: [t5, t7] });
4246
+ $[11] = t5;
4247
+ $[12] = t7;
4248
+ $[13] = t8;
4249
+ } else t8 = $[13];
4250
+ const t9 = Section;
4251
+ const t10 = List;
4252
+ const t11 = "lg";
4253
+ const t12 = "plain";
4254
+ const t13 = statuses.map((status_0) => /* @__PURE__ */ jsx(PluginRow, {
4255
+ dependents: dependentsOf(plugins, status_0.id, on),
4256
+ onSwitch: (checked) => {
4257
+ switches.set(status_0.id, checked);
4258
+ },
4259
+ status: status_0,
4260
+ switched: switched[status_0.id] ?? true
4261
+ }, status_0.id));
4262
+ let t14;
4263
+ if ($[14] !== t10.Root || $[15] !== t13) {
4264
+ t14 = /* @__PURE__ */ jsx(t10.Root, {
4265
+ gap: t11,
4266
+ variant: t12,
4267
+ children: t13
4268
+ });
4269
+ $[14] = t10.Root;
4270
+ $[15] = t13;
4271
+ $[16] = t14;
4272
+ } else t14 = $[16];
4273
+ let t15;
4274
+ if ($[17] !== t14 || $[18] !== t9.Body) {
4275
+ t15 = /* @__PURE__ */ jsx(t9.Body, { children: t14 });
4276
+ $[17] = t14;
4277
+ $[18] = t9.Body;
4278
+ $[19] = t15;
4279
+ } else t15 = $[19];
4280
+ let t16;
4281
+ if ($[20] !== t15 || $[21] !== t2.Root || $[22] !== t8) {
4282
+ t16 = /* @__PURE__ */ jsxs(t2.Root, {
4283
+ annotated: t3,
4284
+ children: [t8, t15]
4285
+ });
4286
+ $[20] = t15;
4287
+ $[21] = t2.Root;
4288
+ $[22] = t8;
4289
+ $[23] = t16;
4290
+ } else t16 = $[23];
4291
+ return t16;
4292
+ }
4293
+ //#endregion
4294
+ //#region src/settings/component-section.ts
4295
+ /**
4296
+ * Renders a settings section's component from its plugin's manifest, loaded on its first render.
4297
+ */
4298
+ /**
4299
+ * Renders the component the section's manifest maps it to, with `{ sectionId }` as its props.
4300
+ *
4301
+ * @returns The component, which suspends while its module loads.
4302
+ */
4303
+ function ComponentSection(t0) {
4304
+ const $ = c(9);
4305
+ const { section } = t0;
4306
+ let t1;
4307
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
4308
+ t1 = { strict: false };
4309
+ $[0] = t1;
4310
+ } else t1 = $[0];
4311
+ const { host } = useRouteContext(t1);
4312
+ let t2;
4313
+ if ($[1] !== section.id || $[2] !== section.plugin.length) {
4314
+ t2 = section.id.slice(section.plugin.length + 1);
4315
+ $[1] = section.id;
4316
+ $[2] = section.plugin.length;
4317
+ $[3] = t2;
4318
+ } else t2 = $[3];
4319
+ const name = t2;
4320
+ let t3;
4321
+ if ($[4] !== host || $[5] !== name || $[6] !== section.id || $[7] !== section.plugin) {
4322
+ const importer = (internalsOf(host).runtime.product.manifests[section.plugin]?.code)?.settings?.[name]?.component;
4323
+ const props = { sectionId: section.id };
4324
+ t3 = createElement(lazyOf(importer, section.id), props);
4325
+ $[4] = host;
4326
+ $[5] = name;
4327
+ $[6] = section.id;
4328
+ $[7] = section.plugin;
4329
+ $[8] = t3;
4330
+ } else t3 = $[8];
4331
+ return t3;
4332
+ }
4333
+ //#endregion
4334
+ //#region src/settings/section-failed.tsx
4335
+ /**
4336
+ * Renders the alert, which states that the section could not be shown.
4337
+ *
4338
+ * @returns The alert, in the error status.
4339
+ */
4340
+ function SectionFailed() {
4341
+ const $ = c(11);
4342
+ const { t } = useTranslation("host");
4343
+ let t0;
4344
+ if ($[0] !== t) {
4345
+ t0 = t("settings.failed.title");
4346
+ $[0] = t;
4347
+ $[1] = t0;
4348
+ } else t0 = $[1];
4349
+ let t1;
4350
+ if ($[2] !== t0) {
4351
+ t1 = /* @__PURE__ */ jsx(Alert.Title, { children: t0 });
4352
+ $[2] = t0;
4353
+ $[3] = t1;
4354
+ } else t1 = $[3];
4355
+ let t2;
4356
+ if ($[4] !== t) {
4357
+ t2 = t("settings.failed.description");
4358
+ $[4] = t;
4359
+ $[5] = t2;
4360
+ } else t2 = $[5];
4361
+ let t3;
4362
+ if ($[6] !== t2) {
4363
+ t3 = /* @__PURE__ */ jsx(Alert.Description, { children: t2 });
4364
+ $[6] = t2;
4365
+ $[7] = t3;
4366
+ } else t3 = $[7];
4367
+ let t4;
4368
+ if ($[8] !== t1 || $[9] !== t3) {
4369
+ t4 = /* @__PURE__ */ jsx(Alert.Root, {
4370
+ status: "error",
4371
+ children: /* @__PURE__ */ jsxs(Alert.Content, { children: [t1, t3] })
4372
+ });
4373
+ $[8] = t1;
4374
+ $[9] = t3;
4375
+ $[10] = t4;
4376
+ } else t4 = $[10];
4377
+ return t4;
4378
+ }
4379
+ //#endregion
4380
+ //#region src/settings/guarded.tsx
4381
+ /**
4382
+ * Renders a settings section's content in its plugin's scope, inside a boundary of its own.
4383
+ *
4384
+ * @remarks
4385
+ * A render of the content that throws counts one failure towards the quarantine of
4386
+ * `section:<id>` and shows an alert in the content's place, so the other sections of the page
4387
+ * keep running. A quarantined section shows the alert without rendering.
4388
+ */
4389
+ /**
4390
+ * Renders the content, or the alert once it failed or while it is quarantined.
4391
+ *
4392
+ * @returns The content in its plugin's scope, suspended while it loads.
4393
+ */
4394
+ function Guarded(t0) {
4395
+ const $ = c(1);
4396
+ const { children, section } = t0;
4397
+ let t1;
4398
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
4399
+ t1 = { strict: false };
4400
+ $[0] = t1;
4401
+ } else t1 = $[0];
4402
+ const { host } = useRouteContext(t1);
4403
+ const { quarantine } = host.stores;
4404
+ const target = `section:${section.id}`;
4405
+ const read = () => quarantine.get().has(target);
4406
+ return useSyncExternalStore(quarantine.subscribe, read, read) ? /* @__PURE__ */ jsx(SectionFailed, {}) : /* @__PURE__ */ jsx(PluginProvider, {
4407
+ pluginId: section.plugin,
4408
+ children: /* @__PURE__ */ jsx(Suspense, {
4409
+ fallback: null,
4410
+ children: /* @__PURE__ */ jsx(Boundary, {
4411
+ fallback: /* @__PURE__ */ jsx(SectionFailed, {}),
4412
+ onError: (error) => {
4413
+ quarantine.failed(target, error);
4414
+ },
4415
+ onRendered: () => {
4416
+ quarantine.rendered(target);
4417
+ },
4418
+ resetKey: section,
4419
+ children
4420
+ })
4421
+ })
4422
+ });
4423
+ }
4424
+ //#endregion
4425
+ //#region src/settings/schema-section.tsx
4426
+ /**
4427
+ * Renders the section's form, with the host's glyphs and a Save button.
4428
+ *
4429
+ * @remarks
4430
+ * The form's id is `settings.<section name>`, so each word of the form is a key of the plugin's
4431
+ * catalogue under `settings.<section name>`. A valid submission writes the values over the stored
4432
+ * ones and raises a success toast, and the form keeps the values. An invalid one focuses the
4433
+ * first refused field.
4434
+ * @returns The form.
4435
+ */
4436
+ function SchemaSection(t0) {
4437
+ const $ = c(38);
4438
+ const { schema, section } = t0;
4439
+ let t1;
4440
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
4441
+ t1 = { strict: false };
4442
+ $[0] = t1;
4443
+ } else t1 = $[0];
4444
+ const { host } = useRouteContext(t1);
4445
+ const { i18n, t } = useTranslation("host");
4446
+ const toaster = useToaster();
4447
+ let t2;
4448
+ if ($[1] !== section.id) {
4449
+ t2 = {
4450
+ id: section.id,
4451
+ kind: "settingsSection"
4452
+ };
4453
+ $[1] = section.id;
4454
+ $[2] = t2;
4455
+ } else t2 = $[2];
4456
+ const { update, values } = useSettings(t2);
4457
+ let t3;
4458
+ if ($[3] !== section.id || $[4] !== section.plugin.length) {
4459
+ t3 = section.id.slice(section.plugin.length + 1);
4460
+ $[3] = section.id;
4461
+ $[4] = section.plugin.length;
4462
+ $[5] = t3;
4463
+ } else t3 = $[5];
4464
+ const t4 = `settings.${t3}`;
4465
+ let t5;
4466
+ if ($[6] !== t || $[7] !== toaster || $[8] !== update) {
4467
+ t5 = (t6) => {
4468
+ const { value } = t6;
4469
+ update(value);
4470
+ toaster.create({
4471
+ title: t("settings.saved"),
4472
+ type: "success"
4473
+ });
4474
+ };
4475
+ $[6] = t;
4476
+ $[7] = toaster;
4477
+ $[8] = update;
4478
+ $[9] = t5;
4479
+ } else t5 = $[9];
4480
+ let t6;
4481
+ if ($[10] !== schema) {
4482
+ t6 = { ...schema };
4483
+ $[10] = schema;
4484
+ $[11] = t6;
4485
+ } else t6 = $[11];
4486
+ let t7;
4487
+ if ($[12] !== i18n || $[13] !== section.plugin) {
4488
+ t7 = translatorOf(i18n, section.plugin);
4489
+ $[12] = i18n;
4490
+ $[13] = section.plugin;
4491
+ $[14] = t7;
4492
+ } else t7 = $[14];
4493
+ let t8;
4494
+ if ($[15] !== t4 || $[16] !== t5 || $[17] !== t6 || $[18] !== t7 || $[19] !== values) {
4495
+ t8 = {
4496
+ id: t4,
4497
+ onSubmit: t5,
4498
+ schema: t6,
4499
+ translate: t7,
4500
+ values
4501
+ };
4502
+ $[15] = t4;
4503
+ $[16] = t5;
4504
+ $[17] = t6;
4505
+ $[18] = t7;
4506
+ $[19] = values;
4507
+ $[20] = t8;
4508
+ } else t8 = $[20];
4509
+ const form = useSchemaForm(t8);
4510
+ const T0 = form.AppForm;
4511
+ const T1 = form.Form;
4512
+ let t9;
4513
+ if ($[21] !== host) {
4514
+ t9 = internalsOf(host);
4515
+ $[21] = host;
4516
+ $[22] = t9;
4517
+ } else t9 = $[22];
4518
+ const t10 = t9.glyphs;
4519
+ let t11;
4520
+ if ($[23] !== form.Fields) {
4521
+ t11 = /* @__PURE__ */ jsx(form.Fields, {});
4522
+ $[23] = form.Fields;
4523
+ $[24] = t11;
4524
+ } else t11 = $[24];
4525
+ const T2 = form.Submit;
4526
+ let t12;
4527
+ if ($[25] !== t) {
4528
+ t12 = t("settings.save");
4529
+ $[25] = t;
4530
+ $[26] = t12;
4531
+ } else t12 = $[26];
4532
+ let t13;
4533
+ if ($[27] !== form.Submit || $[28] !== t12) {
4534
+ t13 = /* @__PURE__ */ jsx(T2, { children: t12 });
4535
+ $[27] = form.Submit;
4536
+ $[28] = t12;
4537
+ $[29] = t13;
4538
+ } else t13 = $[29];
4539
+ let t14;
4540
+ if ($[30] !== form.Form || $[31] !== t11 || $[32] !== t13 || $[33] !== t9.glyphs) {
4541
+ t14 = /* @__PURE__ */ jsxs(T1, {
4542
+ glyphs: t10,
4543
+ children: [t11, t13]
4544
+ });
4545
+ $[30] = form.Form;
4546
+ $[31] = t11;
4547
+ $[32] = t13;
4548
+ $[33] = t9.glyphs;
4549
+ $[34] = t14;
4550
+ } else t14 = $[34];
4551
+ let t15;
4552
+ if ($[35] !== form.AppForm || $[36] !== t14) {
4553
+ t15 = /* @__PURE__ */ jsx(T0, { children: t14 });
4554
+ $[35] = form.AppForm;
4555
+ $[36] = t14;
4556
+ $[37] = t15;
4557
+ } else t15 = $[37];
4558
+ return t15;
4559
+ }
4560
+ //#endregion
4561
+ //#region src/settings/section.tsx
4562
+ /**
4563
+ * Renders the section's heading, translated in its plugin's catalogue, beside its content.
4564
+ *
4565
+ * @returns A `section` element named by its heading.
4566
+ */
4567
+ function SettingsSection(t0) {
4568
+ const $ = c(14);
4569
+ const { section } = t0;
4570
+ const { i18n } = useTranslation("host");
4571
+ let t1;
4572
+ if ($[0] !== i18n || $[1] !== section.label || $[2] !== section.plugin) {
4573
+ t1 = pluginWordsOf(i18n).t(section.plugin, section.label);
4574
+ $[0] = i18n;
4575
+ $[1] = section.label;
4576
+ $[2] = section.plugin;
4577
+ $[3] = t1;
4578
+ } else t1 = $[3];
4579
+ let t2;
4580
+ if ($[4] !== t1) {
4581
+ t2 = /* @__PURE__ */ jsx(Section.Header, { children: /* @__PURE__ */ jsx(Section.Title, { children: t1 }) });
4582
+ $[4] = t1;
4583
+ $[5] = t2;
4584
+ } else t2 = $[5];
4585
+ let t3;
4586
+ if ($[6] !== section) {
4587
+ t3 = section.schema === void 0 ? /* @__PURE__ */ jsx(ComponentSection, { section }) : /* @__PURE__ */ jsx(SchemaSection, {
4588
+ schema: section.schema,
4589
+ section
4590
+ });
4591
+ $[6] = section;
4592
+ $[7] = t3;
4593
+ } else t3 = $[7];
4594
+ let t4;
4595
+ if ($[8] !== section || $[9] !== t3) {
4596
+ t4 = /* @__PURE__ */ jsx(Section.Body, { children: /* @__PURE__ */ jsx(Guarded, {
4597
+ section,
4598
+ children: t3
4599
+ }) });
4600
+ $[8] = section;
4601
+ $[9] = t3;
4602
+ $[10] = t4;
4603
+ } else t4 = $[10];
4604
+ let t5;
4605
+ if ($[11] !== t2 || $[12] !== t4) {
4606
+ t5 = /* @__PURE__ */ jsxs(Section.Root, {
4607
+ annotated: true,
4608
+ children: [t2, t4]
4609
+ });
4610
+ $[11] = t2;
4611
+ $[12] = t4;
4612
+ $[13] = t5;
4613
+ } else t5 = $[13];
4614
+ return t5;
4615
+ }
4616
+ //#endregion
4617
+ //#region src/settings/page.tsx
4618
+ /**
4619
+ * Returns the component of a settings page's route.
4620
+ *
4621
+ * @remarks
4622
+ * The page renders, in order, the sections whose target it is and the sections of its plugin
4623
+ * whose target page is not rendered, where their plugin is on and their condition is true. A page
4624
+ * that renders no section and is not the Plugins page states that it has nothing to set.
4625
+ */
4626
+ function settingsPageOf(page) {
4627
+ const plugins = page.id === hostContract.settings.pages.plugins.id;
4628
+ /**
4629
+ * Renders the page's content below the settings menu.
4630
+ */
4631
+ return function SettingsPage() {
4632
+ const { i18n, t } = useTranslation("host");
4633
+ const { settings } = useResolvedProduct();
4634
+ const { listed, shown } = useSections();
4635
+ const sections = sectionsOn(page.id, shown, listed, settings.pages);
4636
+ useDocumentTitle(pluginWordsOf(i18n).t(page.plugin, page.label));
4637
+ if (!plugins && sections.length === 0) return /* @__PURE__ */ jsx(EmptyState.Root, { children: /* @__PURE__ */ jsx(EmptyState.Content, { children: /* @__PURE__ */ jsx(EmptyState.Title, { children: t("settings.empty") }) }) });
4638
+ return /* @__PURE__ */ jsxs(Fragment, { children: [plugins ? /* @__PURE__ */ jsx(PluginList, {}) : null, sections.map((section) => /* @__PURE__ */ jsx(SettingsSection, { section }, section.id))] });
4639
+ };
4640
+ }
4641
+ //#endregion
4642
+ //#region src/settings/routes.ts
4643
+ /**
4644
+ * Id of the route at the settings route's own address, which opens the first settings page.
4645
+ */
4646
+ const SETTINGS_INDEX = `${hostContract.routes.settings.id}/index`;
4647
+ /**
4648
+ * Returns the declaration of the settings route's index: a child at `/` without a condition, which
4649
+ * the settings route's own condition guards.
4650
+ */
4651
+ function settingsIndexOf() {
4652
+ return {
4653
+ component: SettingsIndex,
4654
+ id: SETTINGS_INDEX,
4655
+ parent: hostContract.routes.settings.id,
4656
+ path: "/"
4657
+ };
4658
+ }
4659
+ /**
4660
+ * Returns the component of a route the host renders itself, or undefined for a plugin's route.
4661
+ *
4662
+ * @remarks
4663
+ * The settings frame and each settings page render as a plugin's page does: in the scope of the
4664
+ * route's plugin, with the extensions placed around the route, and with the route's count of
4665
+ * failed renders returned to zero after a render that commits. A settings page loads with the
4666
+ * modules of the plugins in its `loads`.
4667
+ * @param product - The product, with each installed plugin's manifest.
4668
+ * @param route - The route as the build resolved it.
4669
+ */
4670
+ function hostComponentOf(product, route) {
4671
+ if (route.id === hostContract.routes.settings.id) return hostPageOf(HOST, route.id, SettingsFrame);
4672
+ const page = product.settings.pages.find(({ id }) => pageRouteOf(id) === route.id);
4673
+ return page === void 0 ? void 0 : { load: async () => {
4674
+ await importPlugins(product.manifests, new Set(route.loads));
4675
+ return { default: hostPageOf(route.plugin, route.id, settingsPageOf(page)) };
4676
+ } };
4677
+ }
4678
+ //#endregion
4679
+ //#region src/routes/create-routes.ts
4680
+ /**
4681
+ * Compiles a product's routes under the product's root route: the plugins' pages, the settings
4682
+ * route with its index, and a route per settings page.
4683
+ */
4684
+ /**
4685
+ * Returns a route as the router's compiler takes it, with the host's own component for the
4686
+ * settings route and the settings pages.
4687
+ */
4688
+ function declarationOf(product, route) {
4689
+ return {
4690
+ component: hostComponentOf(product, route) ?? { load: () => loadPage(product, route) },
4691
+ id: route.id,
4692
+ loader: loaderOf(product, route),
4693
+ navigation: route.navigation,
4694
+ parent: route.parent,
4695
+ path: route.path,
4696
+ sample: route.sample,
4697
+ search: mappingOf(product, route.id).search,
4698
+ when: {
4699
+ pluginId: route.plugin,
4700
+ routeId: route.id,
4701
+ when: route.when
4702
+ }
4703
+ };
4704
+ }
4705
+ /**
4706
+ * Returns a function of the product's root route that compiles every route of the product under
4707
+ * it.
4708
+ *
4709
+ * @remarks
4710
+ * The routes compile in one call, so the compiler sees every path at once. Each route loads its
4711
+ * page with the modules of the plugins in its `loads`, loads its `data` through the data client
4712
+ * in the router's context, validates its search with its contract's validator, and evaluates its
4713
+ * condition against the host in the router's context. The settings route renders the settings
4714
+ * frame, its index opens the first settings page, and each settings page renders its sections.
4715
+ * The pages import through the manifests' importers as `instrumented` wraps them.
4716
+ * @param product - The product, with each installed plugin's manifest.
4717
+ * @returns The function that compiles the routes under the root, for the root's `addChildren`.
4718
+ */
4719
+ function createHostRoutes(product) {
4720
+ const loaded = instrumented(product).product;
4721
+ const declarations = [...loaded.routes.map((route) => declarationOf(loaded, route)), settingsIndexOf()];
4722
+ const evaluate = evaluatorOf(product);
4723
+ return (root) => compileRoutes(declarations, {
4724
+ errorComponent: ({ id }) => routeErrorOf(id, mappingOf(loaded, id).fallback),
4725
+ evaluate,
4726
+ parent: root
4727
+ });
4728
+ }
4729
+ //#endregion
4730
+ export { HostContent as a, sampledFrom as c, HostNotFound as i, HostRoot as n, createHost as o, HostProvider as r, internalsOf as s, createHostRoutes as t };