@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.
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/dist/create-routes-CkgZV-dU.js +4730 -0
- package/dist/cyclonedx/bom.json +1 -0
- package/dist/index.d.ts +288 -0
- package/dist/index.js +81 -0
- package/dist/openfeature.d.ts +27 -0
- package/dist/openfeature.js +80 -0
- package/dist/page-7ScZC_LI.js +62 -0
- package/dist/section-CtcVFcBK.js +41 -0
- package/dist/standalone/app.d.ts +65 -0
- package/dist/standalone/app.js +1426 -0
- package/dist/standalone-XfUZYt9b.d.ts +137 -0
- package/dist/standalone.d.ts +2 -0
- package/dist/standalone.js +257 -0
- package/dist/words-yI-WdYM3.js +58 -0
- package/locales/en/host.json +135 -0
- package/package.json +139 -0
|
@@ -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 };
|