@stealthscale/sdk-host 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,404 @@
1
+ import { t as renderProduct } from "./render-CGRJqVed.js";
2
+ import { n as deploymentOf } from "./document-CeEtZo2E.js";
3
+ import { MANIFEST_FILE, lineOf, preloadsOf, registerPreloader, resolveProduct, unmetRequirements } from "@stealthscale/sdk-core";
4
+ import { NONE } from "@stealthscale/provider-i18n";
5
+ //#region src/releases/catalogues.ts
6
+ /**
7
+ * Returns a release's words in one language, fetched with the integrity `plugin.json` lists.
8
+ *
9
+ * @param fetching - Fetches a URL with an integrity.
10
+ * @param base - The absolute URL of the release's directory.
11
+ * @param release - The release's description.
12
+ * @param language - The BCP 47 tag the catalogue's file is named after.
13
+ * @throws {@link Error} When the description lists no such file, the response is not a success,
14
+ * or its body is not an object of words.
15
+ */
16
+ async function wordsAt(fetching, base, release, language) {
17
+ const path = `locales/${language}.json`;
18
+ const integrity = release.files[path];
19
+ const url = new URL(path, base).href;
20
+ if (integrity === void 0) throw new Error(`plugin.json lists no ${path}.`);
21
+ const response = await fetching(url, { integrity });
22
+ if (!response.ok) throw new Error(`${url} responded with ${String(response.status)}.`);
23
+ const words = await response.json();
24
+ if (typeof words !== "object" || words === null || Array.isArray(words)) throw new Error(`${url} contains no object of words.`);
25
+ return words;
26
+ }
27
+ /**
28
+ * Returns a page's catalogues with the namespaces of loaded releases added.
29
+ *
30
+ * @remarks
31
+ * A release's words in the page's fallback language are inlined, as the build inlines the
32
+ * product's. The words in any other language the release lists are fetched when the i18n
33
+ * instance asks for them, with their integrity. Every other namespace loads as before.
34
+ * @param catalogues - The page's catalogues.
35
+ * @param releases - The loaded releases, each with its words in the fallback language.
36
+ * @param fetching - Fetches a URL with an integrity.
37
+ */
38
+ function cataloguesWith(catalogues, releases, fetching) {
39
+ const { bundled, defaults, fallback, languages, load, namespaces } = catalogues;
40
+ const byId = new Map(releases.map((one) => [one.release.id, one]));
41
+ const words = Object.fromEntries(releases.map((one) => [one.release.id, one.words]));
42
+ return {
43
+ bundled: {
44
+ ...bundled,
45
+ [fallback]: {
46
+ ...bundled[fallback],
47
+ ...words
48
+ }
49
+ },
50
+ defaults: {
51
+ ...defaults,
52
+ ...words
53
+ },
54
+ fallback,
55
+ languages: [.../* @__PURE__ */ new Set([...languages, ...releases.flatMap(({ release }) => release.languages)])],
56
+ load: (language, namespace) => {
57
+ const loaded = byId.get(namespace);
58
+ return loaded?.release.languages.includes(language) === true ? wordsAt(fetching, loaded.base, loaded.release, language) : load(language, namespace);
59
+ },
60
+ namespaces: [...namespaces, ...releases.map(({ release }) => release.id)]
61
+ };
62
+ }
63
+ //#endregion
64
+ //#region src/releases/load.ts
65
+ /**
66
+ * Returns the report of a refused release.
67
+ */
68
+ function refusal(plugin, reason, detail) {
69
+ return {
70
+ detail,
71
+ kind: "plugin-refused",
72
+ plugin,
73
+ reason
74
+ };
75
+ }
76
+ /**
77
+ * Returns the message of an error, or the value written as text.
78
+ */
79
+ function messageOf(error) {
80
+ return error instanceof Error ? error.message : String(error);
81
+ }
82
+ /**
83
+ * Returns the phrase that describes a requirement the page does not meet.
84
+ */
85
+ function unmetOf({ package: name, provided, range }) {
86
+ return provided === void 0 ? `requires ${name} ${range}, which the page does not provide` : `requires ${name} ${range}, and the page provides ${provided}`;
87
+ }
88
+ /**
89
+ * Returns the refusal of a release whose requirements or languages the page does not meet, and
90
+ * undefined for a release the page can load.
91
+ */
92
+ function checked(options, listed) {
93
+ const { id, languages, requires } = listed.release;
94
+ const { fallback, namespaces } = options.catalogues;
95
+ const unmet = unmetRequirements(requires, options.deployment.provided);
96
+ if (unmet.length > 0) return refusal(id, "requires", unmet.map((one) => unmetOf(one)).join("; "));
97
+ if (!languages.includes(fallback)) return refusal(id, "catalogue", `has no catalogue in the page's fallback language ${fallback}`);
98
+ return namespaces.includes(id) ? refusal(id, "catalogue", `names the namespace ${id}, which the page's catalogues contain`) : void 0;
99
+ }
100
+ /**
101
+ * Returns the manifest that a module exports as `manifest`, or undefined where the manifest's
102
+ * contract has no plugin id.
103
+ */
104
+ function manifestIn(module) {
105
+ const manifest = typeof module === "object" && module !== null ? Reflect.get(module, "manifest") : void 0;
106
+ const contract = typeof manifest === "object" && manifest !== null ? Reflect.get(manifest, "contract") : void 0;
107
+ return typeof (typeof contract === "object" && contract !== null ? Reflect.get(contract, "pluginId") : void 0) === "string" ? manifest : void 0;
108
+ }
109
+ /**
110
+ * Imports a release's manifest, and returns it or the release's refusal.
111
+ */
112
+ async function manifestOf(options, listed) {
113
+ const { id } = listed.release;
114
+ let module;
115
+ try {
116
+ module = await options.importing(new URL(MANIFEST_FILE, listed.base).href);
117
+ } catch (error) {
118
+ return refusal(id, "manifest", messageOf(error));
119
+ }
120
+ const manifest = manifestIn(module);
121
+ if (manifest === void 0) return refusal(id, "manifest", "manifest.js exports no manifest");
122
+ return manifest.contract.pluginId === id ? manifest : refusal(id, "manifest", `manifest.js states the plugin ${manifest.contract.pluginId}`);
123
+ }
124
+ /**
125
+ * Returns a release's words in the page's fallback language, or the release's refusal.
126
+ *
127
+ * @remarks
128
+ * The deployment step wrote the words into the document's deployment after it checked their
129
+ * integrity. The loader fetches the words only when the document lists the release without them.
130
+ */
131
+ async function wordsOf(options, listed) {
132
+ const { base, release, words } = listed;
133
+ if (words !== void 0) return { words };
134
+ try {
135
+ return { words: await wordsAt(options.fetching, base, release, options.catalogues.fallback) };
136
+ } catch (error) {
137
+ return { refused: refusal(release.id, "catalogue", messageOf(error)) };
138
+ }
139
+ }
140
+ /**
141
+ * Loads one release, and returns it or its refusal.
142
+ *
143
+ * @remarks
144
+ * The words and the manifest load in parallel. A release whose words and manifest both fail is
145
+ * refused for its words.
146
+ */
147
+ async function loadedOf(options, listed) {
148
+ const { base, release } = listed;
149
+ const refused = checked(options, listed);
150
+ if (refused !== void 0) return refused;
151
+ const [read, manifest] = await Promise.all([wordsOf(options, listed), manifestOf(options, listed)]);
152
+ if ("refused" in read) return read.refused;
153
+ return "kind" in manifest ? manifest : {
154
+ base,
155
+ installed: {
156
+ ...listed.options,
157
+ manifest
158
+ },
159
+ release,
160
+ words: read.words
161
+ };
162
+ }
163
+ /**
164
+ * Loads the releases a deployment lists, after checking each against the packages the page
165
+ * provides.
166
+ *
167
+ * @param options - The page's catalogues, the deployment, and how to fetch and to import.
168
+ * @returns The loaded releases and the refused ones, each in the deployment's order.
169
+ */
170
+ async function loadReleases(options) {
171
+ const outcomes = await Promise.all(options.deployment.releases.map((listed) => loadedOf(options, listed)));
172
+ return {
173
+ loaded: outcomes.flatMap((outcome) => "kind" in outcome ? [] : [outcome]),
174
+ refused: outcomes.flatMap((outcome) => "kind" in outcome ? [outcome] : [])
175
+ };
176
+ }
177
+ //#endregion
178
+ //#region src/releases/preload.ts
179
+ /**
180
+ * Writes the preloads of a release's module into a page's document when the release imports the
181
+ * module.
182
+ */
183
+ /**
184
+ * Returns the listed release whose directory contains a URL, or undefined. Where two bases nest,
185
+ * the function returns the release with the longer base.
186
+ */
187
+ function listedAt(deployment, url) {
188
+ return deployment.releases.filter(({ base }) => url.startsWith(base)).toSorted((one, two) => two.base.length - one.base.length)[0];
189
+ }
190
+ /**
191
+ * Returns a `link` element that preloads a module. The element has an `integrity` attribute when
192
+ * the module belongs to a release.
193
+ */
194
+ function linkOf(target, href, integrity) {
195
+ const link = target.createElement("link");
196
+ link.setAttribute("rel", "modulepreload");
197
+ link.setAttribute("crossorigin", "");
198
+ link.setAttribute("href", href);
199
+ if (integrity !== void 0) link.setAttribute("integrity", integrity);
200
+ return link;
201
+ }
202
+ /**
203
+ * Returns the preloader of a page. For a module of a listed release, the preloader appends a module
204
+ * preload for each module that `preloadsOf` returns. It appends each URL once.
205
+ *
206
+ * @remarks
207
+ * The preloader does not append a preload for a module that the document already preloads.
208
+ * @param target - The page's document.
209
+ * @param deployment - The deployment that the document states.
210
+ */
211
+ function preloaderOf(target, deployment) {
212
+ const written = new Set([...target.querySelectorAll("link[rel=\"modulepreload\"]")].map(({ href }) => href));
213
+ return (url) => {
214
+ const listed = listedAt(deployment, url);
215
+ const preloads = listed === void 0 ? [] : preloadsOf(deployment, listed, url.slice(listed.base.length));
216
+ for (const { href, integrity } of preloads) {
217
+ const absolute = new URL(href, target.baseURI).href;
218
+ if (!written.has(absolute)) {
219
+ written.add(absolute);
220
+ target.head.append(linkOf(target, absolute, integrity));
221
+ }
222
+ }
223
+ };
224
+ }
225
+ //#endregion
226
+ //#region src/releases/resolve.ts
227
+ /**
228
+ * Returns a web package for each plugin of a definition. The resolver reads only that a plugin has
229
+ * one.
230
+ */
231
+ function packagesOf(definition) {
232
+ return Object.fromEntries(definition.plugins.map(({ manifest }) => {
233
+ const { pluginId } = manifest.contract;
234
+ return [pluginId, {
235
+ directory: ".",
236
+ name: pluginId
237
+ }];
238
+ }));
239
+ }
240
+ /**
241
+ * Returns the definition's resolution with the plugins of releases after its own.
242
+ */
243
+ function resolutionWith(options, releases) {
244
+ const { catalogues, definition } = options;
245
+ const plugins = [...definition.plugins, ...releases.map(({ installed }) => installed)];
246
+ const words = Object.fromEntries(releases.map(({ release, words: own }) => [release.id, own]));
247
+ const extended = {
248
+ ...definition,
249
+ plugins
250
+ };
251
+ return resolveProduct(extended, packagesOf(extended), { catalogues: {
252
+ ...catalogues.defaults,
253
+ ...words
254
+ } });
255
+ }
256
+ /**
257
+ * Returns the product with the admitted releases' plugins, or the build's product where the page
258
+ * admitted none.
259
+ */
260
+ function productOf(options, admitted, resolved) {
261
+ const { deployment, product } = options;
262
+ if (resolved === void 0) return {
263
+ ...product,
264
+ deployment
265
+ };
266
+ const manifests = admitted.map(({ installed }) => [installed.manifest.contract.pluginId, installed.manifest]);
267
+ return {
268
+ ...resolved,
269
+ code: product.code,
270
+ deployment,
271
+ manifests: {
272
+ ...product.manifests,
273
+ ...Object.fromEntries(manifests)
274
+ }
275
+ };
276
+ }
277
+ /**
278
+ * Resolves a product with the releases its page loaded, one release at a time.
279
+ *
280
+ * @param options - The definition, the build's product, the loaded releases, the page's catalogues
281
+ * and the deployment's id.
282
+ * @returns The product with every admitted release, and each release the resolution refused.
283
+ */
284
+ function withReleases(options) {
285
+ const admitted = [];
286
+ const refused = [];
287
+ let resolved;
288
+ for (const loaded of options.loaded) {
289
+ const { problems, product } = resolutionWith(options, [...admitted, loaded]);
290
+ if (product === void 0) {
291
+ const detail = problems.map((problem) => lineOf(problem)).join("; ");
292
+ refused.push(refusal(loaded.release.id, "resolution", detail));
293
+ } else {
294
+ admitted.push(loaded);
295
+ resolved = product;
296
+ }
297
+ }
298
+ return {
299
+ admitted,
300
+ product: productOf(options, admitted, resolved),
301
+ refused
302
+ };
303
+ }
304
+ //#endregion
305
+ //#region src/releases/deployed.ts
306
+ /**
307
+ * Fetches a URL with the page's `fetch`.
308
+ */
309
+ const FETCHING = (url, init) => fetch(url, init);
310
+ /**
311
+ * Imports a module by its absolute URL, which the bundler leaves as written.
312
+ */
313
+ const IMPORTING = (url) => import(
314
+ /* @vite-ignore */
315
+ url
316
+ );
317
+ /**
318
+ * Loads the releases in a page's document and resolves the product with them.
319
+ *
320
+ * @remarks
321
+ * A document without a deployment renders the build's product. Where the page cannot read the
322
+ * deployment, the page refuses every release with the reason `deployment`, and the build's
323
+ * product renders with an empty list of releases. The product's `releases` contains the id of
324
+ * every release in the document, so the page's first render withholds the same plugins as the
325
+ * server's render. After the function reads a deployment, it registers the preloader of the
326
+ * document. The preloader writes the preloads of a release's module when the release imports that
327
+ * module.
328
+ * @param options - The page's document and catalogues, the definition, the build's product, and how
329
+ * to fetch and to import.
330
+ * @returns The catalogues and the product to render, and each release the page refused.
331
+ */
332
+ async function deployedProduct(options) {
333
+ const { catalogues, definition, fetching = FETCHING, importing = IMPORTING, product } = options;
334
+ const read = deploymentOf(options.document);
335
+ if (read.kind === "none") return {
336
+ catalogues,
337
+ product,
338
+ refused: []
339
+ };
340
+ if (read.kind === "malformed") {
341
+ const detail = read.faults.join("; ");
342
+ return {
343
+ catalogues,
344
+ product: {
345
+ ...product,
346
+ releases: []
347
+ },
348
+ refused: [{
349
+ detail,
350
+ kind: "plugin-refused",
351
+ reason: "deployment"
352
+ }]
353
+ };
354
+ }
355
+ const { deployment } = read;
356
+ registerPreloader(preloaderOf(options.document, deployment));
357
+ const { loaded, refused } = await loadReleases({
358
+ catalogues,
359
+ deployment,
360
+ fetching,
361
+ importing
362
+ });
363
+ const released = withReleases({
364
+ catalogues,
365
+ definition,
366
+ deployment: deployment.id,
367
+ loaded,
368
+ product
369
+ });
370
+ return {
371
+ catalogues: cataloguesWith(catalogues, released.admitted, fetching),
372
+ product: {
373
+ ...released.product,
374
+ releases: deployment.releases.map(({ release }) => release.id)
375
+ },
376
+ refused: [...refused, ...released.refused]
377
+ };
378
+ }
379
+ //#endregion
380
+ //#region src/app/render-deployed.ts
381
+ /**
382
+ * Loads the releases the document's deployment lists, and renders the product resolved with them
383
+ * once the host is ready.
384
+ *
385
+ * @remarks
386
+ * The host reports each release the page refused. A document without a deployment renders the
387
+ * build's product.
388
+ * @param options - The definition, and what `renderProduct` renders with.
389
+ * @returns The React root the product renders in, which `unmount` takes down.
390
+ */
391
+ async function renderDeployed({ catalogues = NONE, definition, product, ...options }) {
392
+ const deployed = await deployedProduct({
393
+ catalogues,
394
+ definition,
395
+ document,
396
+ product
397
+ });
398
+ return renderProduct({
399
+ ...options,
400
+ ...deployed
401
+ });
402
+ }
403
+ //#endregion
404
+ export { deployedProduct, renderDeployed };
@@ -0,0 +1,238 @@
1
+ import { c as HostProvider, f as onCaughtError, l as HostNotFound, p as createHost, r as listedRoutesOf, t as createHostRoutes } from "./create-routes-D4iCBYZe.js";
2
+ import { n as useSections, t as ProductFrame } from "./frame-alSrG57-.js";
3
+ import { NOBODY, constantSession, hostContract } from "@stealthscale/sdk-core";
4
+ import { conditionContextOf } from "@stealthscale/sdk-plugin";
5
+ import { localStore } from "@stealthscale/settings";
6
+ import { Navigate, RouterProvider, createAppRootRoute, createRoute, createRouter, notFound, redirect, routeHref, routeMap, routerOptions } from "@stealthscale/provider-router";
7
+ import { c } from "react/compiler-runtime";
8
+ import { jsx } from "react/jsx-runtime";
9
+ import { CheckIcon, ChevronDownIcon, CircleAlertIcon, MinusIcon, PlusIcon } from "lucide-react";
10
+ import { createRoot } from "react-dom/client";
11
+ import { Shell } from "@stealthscale/provider-shell";
12
+ //#region src/app/glyphs.tsx
13
+ /**
14
+ * Lists the glyphs the settings sections' forms render where a product states none.
15
+ */
16
+ /**
17
+ * The marks of the settings forms: a check in a checked box and beside a picked option, a select's
18
+ * chevron, a number field's steppers, and the mark before an error.
19
+ */
20
+ const GLYPHS = {
21
+ checkbox: /* @__PURE__ */ jsx(CheckIcon, {}),
22
+ error: /* @__PURE__ */ jsx(CircleAlertIcon, {}),
23
+ number: {
24
+ decrement: /* @__PURE__ */ jsx(MinusIcon, {}),
25
+ increment: /* @__PURE__ */ jsx(PlusIcon, {})
26
+ },
27
+ select: {
28
+ indicator: /* @__PURE__ */ jsx(ChevronDownIcon, {}),
29
+ selected: /* @__PURE__ */ jsx(CheckIcon, {})
30
+ }
31
+ };
32
+ //#endregion
33
+ //#region src/app/site-root.tsx
34
+ /**
35
+ * Opens the first section the main menu lists, in place of the site root.
36
+ *
37
+ * @remarks
38
+ * The router's `beforeLoad` has already sent a person with no section to the sign-in page or to
39
+ * the not-found page. The component reads the sections in the main menu's own order, which sorts
40
+ * entries without a rank by their text in the person's language, so the site root opens the
41
+ * section the main menu lists first. The navigation replaces the site root in the history, so
42
+ * Back does not return to it.
43
+ * @returns The router's navigation to the section, or nothing while the main menu lists none.
44
+ */
45
+ function SiteRoot() {
46
+ const $ = c(2);
47
+ const [first] = useSections().sections;
48
+ let t0;
49
+ if ($[0] !== first) {
50
+ t0 = first === void 0 ? null : /* @__PURE__ */ jsx(Navigate, {
51
+ replace: true,
52
+ to: first.href
53
+ });
54
+ $[0] = first;
55
+ $[1] = t0;
56
+ } else t0 = $[1];
57
+ return t0;
58
+ }
59
+ //#endregion
60
+ //#region src/app/tree.ts
61
+ /**
62
+ * Returns true where the main menu lists a page the person may open.
63
+ *
64
+ * @param product - The product's routes.
65
+ * @param stores - The stores a route's condition reads.
66
+ */
67
+ function hasSection(product, stores) {
68
+ return listedRoutesOf(product.routes, stores).some(({ navigation }) => {
69
+ return navigation?.menu === hostContract.menus.main.id;
70
+ });
71
+ }
72
+ /**
73
+ * Returns true where a plugin declares a route at the site root, which then takes the place of the
74
+ * host's.
75
+ */
76
+ function rootedAtSite(product) {
77
+ return product.routes.some(({ parent, path }) => {
78
+ return parent === void 0 && (path === "/" || path === "");
79
+ });
80
+ }
81
+ /**
82
+ * Returns a product's route tree.
83
+ *
84
+ * @remarks
85
+ * The site root opens the first section the person may open. Without one, it sends a person who
86
+ * is not signed in to the product's sign-in route, with the site root as the `redirect` search,
87
+ * and renders the not-found page otherwise. A plugin's route at `/` takes the site root's place.
88
+ * Every route renders the host's not-found page in the frame where its condition stops it.
89
+ * @param product - The product, whose routes the host compiles.
90
+ * @param frame - The component of the root route. The host's frame where left out.
91
+ * @param shell - The document around the frame. The frame renders alone where left out.
92
+ */
93
+ function productTreeOf(product, frame = ProductFrame, shell) {
94
+ const root = createAppRootRoute()({
95
+ component: frame,
96
+ notFoundComponent: HostNotFound,
97
+ ...shell === void 0 ? {} : { shellComponent: shell }
98
+ });
99
+ const site = createRoute({
100
+ beforeLoad: ({ context, location }) => {
101
+ const { host, routes } = context;
102
+ if (hasSection(product, host.stores)) return;
103
+ if (product.signIn !== void 0 && !conditionContextOf(host.stores).authenticated) throw redirect({
104
+ replace: true,
105
+ search: { redirect: location.href },
106
+ to: routeHref(routes, product.signIn)
107
+ });
108
+ throw notFound();
109
+ },
110
+ component: SiteRoot,
111
+ getParentRoute: () => root,
112
+ path: "/"
113
+ });
114
+ const routes = createHostRoutes(product)(root);
115
+ return root.addChildren(rootedAtSite(product) ? routes : [site, ...routes]);
116
+ }
117
+ //#endregion
118
+ //#region src/app/router.ts
119
+ /**
120
+ * Returns a product's router: the frame at the root, the site root, and the plugins' routes, with
121
+ * the host and its data client in the router's context.
122
+ *
123
+ * @param host - The host the router's context contains.
124
+ * @param product - The product, whose routes the host compiles.
125
+ * @param options - The base path, the frame, the history and the document around the frame.
126
+ */
127
+ function createProductRouter(host, product, options = {}) {
128
+ const tree = productTreeOf(product, options.frame, options.shell);
129
+ const { basepath, history } = options;
130
+ return createRouter({
131
+ ...routerOptions({
132
+ data: host.data,
133
+ host,
134
+ routes: routeMap(tree)
135
+ }),
136
+ ...basepath === void 0 ? {} : { basepath },
137
+ ...history === void 0 ? {} : { history },
138
+ routeTree: tree
139
+ });
140
+ }
141
+ //#endregion
142
+ //#region src/app/sources.ts
143
+ /**
144
+ * Returns the sources the product's definition names, or none where it names no module.
145
+ *
146
+ * @param product - The product, with the definition's importers.
147
+ */
148
+ function sourcesOf(product) {
149
+ const importer = product.code?.sources;
150
+ return importer === void 0 ? Promise.resolve({}) : importer();
151
+ }
152
+ /**
153
+ * Returns the product's own frame, or undefined where the product renders the host's.
154
+ *
155
+ * @param product - The product, with the definition's importers.
156
+ * @throws {@link Error} Where the frame's module exports another number of functions than one.
157
+ */
158
+ async function frameOf(product) {
159
+ const importer = product.code?.frame;
160
+ if (importer === void 0) return void 0;
161
+ const functions = Object.values(await importer()).filter((value) => typeof value === "function");
162
+ if (functions.length !== 1) throw new Error(`The module of the product's frame exports ${String(functions.length)} functions, and a frame's module exports one.`);
163
+ return functions[0];
164
+ }
165
+ //#endregion
166
+ //#region src/app/hosted.ts
167
+ /**
168
+ * Returns the host over the product's sources, and the router over the host.
169
+ *
170
+ * @remarks
171
+ * Without the sources' session the session is constant and nobody is signed in, and without their
172
+ * store the host keeps choices in the page's local storage. The router's root renders the
173
+ * product's own frame where the definition names one.
174
+ * @param product - The product, with the definition's importers.
175
+ * @param options - The base path, the glyphs and the refused releases.
176
+ */
177
+ async function hostedOf(product, options) {
178
+ const sources = await sourcesOf(product);
179
+ const store = sources.store ?? localStore();
180
+ const host = createHost({
181
+ ...sources,
182
+ glyphs: options.glyphs ?? GLYPHS,
183
+ product,
184
+ refused: options.refused,
185
+ session: sources.session ?? constantSession(NOBODY),
186
+ store
187
+ });
188
+ return {
189
+ host,
190
+ router: createProductRouter(host, product, {
191
+ basepath: options.basepath,
192
+ frame: await frameOf(product)
193
+ }),
194
+ store
195
+ };
196
+ }
197
+ //#endregion
198
+ //#region src/app/render.tsx
199
+ /**
200
+ * Renders a product into the page from its definition, for the entry a product's build generates.
201
+ */
202
+ /**
203
+ * Renders the product once the host is ready: the shell over the product's locales, themes and
204
+ * store, the host, and the router with the frame at its root.
205
+ *
206
+ * @remarks
207
+ * The root takes the host's `onCaughtError`, so a page that throws the router's not-found writes
208
+ * nothing to the console.
209
+ * @param options - The product, the catalogues, the themes, the glyphs, the refused releases, the
210
+ * base path and the element.
211
+ * @returns The React root the product renders in, which `unmount` takes down.
212
+ * @throws {@link Error} Where no element is given and the document has none with the id `root`.
213
+ */
214
+ async function renderProduct({ basepath, catalogues, element = document.querySelector("#root"), glyphs, product, refused, themes }) {
215
+ if (element === null) throw new Error("A product renders into the element with the id root, and the document has none.");
216
+ const { host, router, store } = await hostedOf(product, {
217
+ basepath,
218
+ glyphs,
219
+ refused
220
+ });
221
+ await host.ready();
222
+ const root = createRoot(element, { onCaughtError });
223
+ root.render(/* @__PURE__ */ jsx(Shell, {
224
+ app: product.productId,
225
+ catalogues,
226
+ locales: product.locales,
227
+ store,
228
+ themes,
229
+ children: /* @__PURE__ */ jsx(HostProvider, {
230
+ host,
231
+ router,
232
+ children: /* @__PURE__ */ jsx(RouterProvider, { router })
233
+ })
234
+ }));
235
+ return root;
236
+ }
237
+ //#endregion
238
+ export { createProductRouter as n, GLYPHS as r, renderProduct as t };
@@ -0,0 +1,54 @@
1
+ import { Product } from "@stealthscale/sdk-core";
2
+ import { PluginRefused } from "@stealthscale/sdk-plugin";
3
+ import { Catalogues } from "@stealthscale/provider-i18n";
4
+ import { FormGlyphs } from "@stealthscale/component-forms/form";
5
+ import { Root } from "react-dom/client";
6
+ //#region src/app/render.d.ts
7
+ /**
8
+ * Lists what a product renders with.
9
+ */
10
+ interface ProductRenderOptions {
11
+ /**
12
+ * The base the product is served at, such as `/docs/`. The site's root where left out.
13
+ */
14
+ readonly basepath?: string | undefined;
15
+ /**
16
+ * The catalogues, from `virtual:i18n`. Every key renders as itself where left out.
17
+ */
18
+ readonly catalogues?: Catalogues | undefined;
19
+ /**
20
+ * The element the product renders into. The element with the id `root` where left out.
21
+ */
22
+ readonly element?: Element | null | undefined;
23
+ /**
24
+ * Glyphs the settings sections' forms render. Lucide's where left out.
25
+ */
26
+ readonly glyphs?: FormGlyphs | undefined;
27
+ /**
28
+ * The product, from `virtual:product`.
29
+ */
30
+ readonly product: Product;
31
+ /**
32
+ * Releases the page did not load, which the host reports as it is created.
33
+ */
34
+ readonly refused?: readonly PluginRefused[] | undefined;
35
+ /**
36
+ * The themes the product offers, the first applied until a person picks another.
37
+ */
38
+ readonly themes?: readonly string[] | undefined;
39
+ }
40
+ /**
41
+ * Renders the product once the host is ready: the shell over the product's locales, themes and
42
+ * store, the host, and the router with the frame at its root.
43
+ *
44
+ * @remarks
45
+ * The root takes the host's `onCaughtError`, so a page that throws the router's not-found writes
46
+ * nothing to the console.
47
+ * @param options - The product, the catalogues, the themes, the glyphs, the refused releases, the
48
+ * base path and the element.
49
+ * @returns The React root the product renders in, which `unmount` takes down.
50
+ * @throws {@link Error} Where no element is given and the document has none with the id `root`.
51
+ */
52
+ declare function renderProduct({ basepath, catalogues, element, glyphs, product, refused, themes }: ProductRenderOptions): Promise<Root>;
53
+ //#endregion
54
+ export { renderProduct as n, ProductRenderOptions as t };