@stealthscale/sdk-host 0.1.0 → 0.4.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/README.md +274 -59
- package/dist/app.d.ts +79 -0
- package/dist/app.js +3 -0
- package/dist/{create-routes-CkgZV-dU.js → create-routes-D4iCBYZe.js} +1244 -468
- package/dist/cyclonedx/bom.json +1 -1
- package/dist/deploy.d.ts +98 -0
- package/dist/deploy.js +254 -0
- package/dist/document-CeEtZo2E.js +38 -0
- package/dist/frame-HZM1zS7g.js +2500 -0
- package/dist/index.d.ts +36 -97
- package/dist/index.js +2 -2
- package/dist/options-DBsArZKT.d.ts +91 -0
- package/dist/{page-7ScZC_LI.js → page-DzeycJUh.js} +2 -2
- package/dist/releases.d.ts +131 -0
- package/dist/releases.js +404 -0
- package/dist/render-CEa8Jhmo.js +238 -0
- package/dist/render-DRndY-TD.d.ts +54 -0
- package/dist/standalone/app.d.ts +3 -0
- package/dist/standalone/app.js +214 -343
- package/dist/standalone.js +1 -1
- package/locales/en/host.json +46 -0
- package/package.json +60 -49
package/dist/releases.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { t as renderProduct } from "./render-CEa8Jhmo.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-HZM1zS7g.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 };
|