@jskit-ai/shell-web 0.1.4

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.
Files changed (41) hide show
  1. package/package.descriptor.mjs +165 -0
  2. package/package.json +23 -0
  3. package/src/client/components/ShellErrorHost.vue +208 -0
  4. package/src/client/components/ShellLayout.vue +191 -0
  5. package/src/client/components/ShellOutlet.vue +95 -0
  6. package/src/client/components/useShellLayout.js +93 -0
  7. package/src/client/error/index.js +2 -0
  8. package/src/client/error/inject.js +142 -0
  9. package/src/client/error/normalize.js +75 -0
  10. package/src/client/error/policy.js +50 -0
  11. package/src/client/error/presenters.js +89 -0
  12. package/src/client/error/runtime.js +418 -0
  13. package/src/client/error/store.js +176 -0
  14. package/src/client/error/tokens.js +14 -0
  15. package/src/client/index.js +17 -0
  16. package/src/client/navigation/linkResolver.js +117 -0
  17. package/src/client/placement/debug.js +52 -0
  18. package/src/client/placement/index.js +26 -0
  19. package/src/client/placement/inject.js +104 -0
  20. package/src/client/placement/pathname.js +14 -0
  21. package/src/client/placement/registry.js +41 -0
  22. package/src/client/placement/runtime.js +435 -0
  23. package/src/client/placement/surfaceContext.js +290 -0
  24. package/src/client/placement/tokens.js +29 -0
  25. package/src/client/placement/validators.js +210 -0
  26. package/src/client/providers/ShellWebClientProvider.js +352 -0
  27. package/templates/src/App.vue +11 -0
  28. package/templates/src/components/ShellLayout.vue +247 -0
  29. package/templates/src/error.js +13 -0
  30. package/templates/src/pages/console/index.vue +24 -0
  31. package/templates/src/pages/console.vue +20 -0
  32. package/templates/src/pages/home/index.vue +54 -0
  33. package/templates/src/pages/home.vue +20 -0
  34. package/templates/src/placement.js +12 -0
  35. package/test/errorRuntime.test.js +191 -0
  36. package/test/errorStore.test.js +26 -0
  37. package/test/linkResolver.test.js +112 -0
  38. package/test/placementRegistry.test.js +45 -0
  39. package/test/placementRuntime.test.js +374 -0
  40. package/test/provider.test.js +163 -0
  41. package/test/surfaceContext.test.js +184 -0
@@ -0,0 +1,418 @@
1
+ import {
2
+ isRecord,
3
+ normalizeAction,
4
+ normalizeChannel,
5
+ normalizeNonNegativeInteger,
6
+ normalizeSeverity,
7
+ normalizeText
8
+ } from "./normalize.js";
9
+ import { createDefaultErrorPolicy } from "./policy.js";
10
+
11
+ function createRuntimeLogger(logger = null) {
12
+ const source = isRecord(logger) ? logger : null;
13
+ return Object.freeze({
14
+ warn: typeof source?.warn === "function" ? source.warn.bind(source) : () => {},
15
+ error: typeof source?.error === "function" ? source.error.bind(source) : () => {}
16
+ });
17
+ }
18
+
19
+ function normalizeErrorEvent(rawEvent = {}) {
20
+ const source = isRecord(rawEvent) ? rawEvent : { message: rawEvent };
21
+ const cause = source.cause !== undefined ? source.cause : source.error;
22
+
23
+ const statusCandidate = Number(
24
+ source.status || source.statusCode || cause?.status || cause?.statusCode || 0
25
+ );
26
+ const status = Number.isFinite(statusCandidate) ? Math.trunc(statusCandidate) : 0;
27
+
28
+ const fieldErrors = isRecord(source.fieldErrors)
29
+ ? Object.freeze({ ...source.fieldErrors })
30
+ : isRecord(source.details?.fieldErrors)
31
+ ? Object.freeze({ ...source.details.fieldErrors })
32
+ : null;
33
+
34
+ const details = isRecord(source.details) ? Object.freeze({ ...source.details }) : null;
35
+
36
+ const userMessage = normalizeText(source.userMessage);
37
+ const runtimeMessage = normalizeText(source.message || cause?.message);
38
+
39
+ return Object.freeze({
40
+ code: normalizeText(source.code || cause?.code),
41
+ status,
42
+ source: normalizeText(source.source, "app"),
43
+ message: normalizeText(userMessage || runtimeMessage, "Request failed."),
44
+ userMessage,
45
+ severity: normalizeSeverity(source.severity, "error"),
46
+ channel: normalizeChannel(source.channel),
47
+ presenterId: normalizeText(source.presenterId),
48
+ action: normalizeAction(source.action),
49
+ persist: typeof source.persist === "boolean" ? source.persist : null,
50
+ blocking: source.blocking === true,
51
+ dedupeKey: normalizeText(source.dedupeKey),
52
+ dedupeWindowMs: normalizeNonNegativeInteger(source.dedupeWindowMs, 0),
53
+ traceId: normalizeText(source.traceId),
54
+ fieldErrors,
55
+ details,
56
+ cause: cause || null,
57
+ timestamp: Number(Date.now())
58
+ });
59
+ }
60
+
61
+ function normalizePolicyDecision(policyDecision = {}, event = {}) {
62
+ const source = isRecord(policyDecision) ? policyDecision : {};
63
+ const channel = normalizeChannel(source.channel || event.channel, "snackbar") || "snackbar";
64
+
65
+ return Object.freeze({
66
+ channel,
67
+ presenterId: normalizeText(source.presenterId || event.presenterId),
68
+ message: normalizeText(source.message || event.userMessage || event.message, "Request failed."),
69
+ severity: normalizeSeverity(source.severity || event.severity, "error"),
70
+ action: normalizeAction(source.action || event.action),
71
+ persist:
72
+ typeof source.persist === "boolean"
73
+ ? source.persist
74
+ : typeof event.persist === "boolean"
75
+ ? event.persist
76
+ : channel !== "snackbar",
77
+ dedupeKey: normalizeText(source.dedupeKey || event.dedupeKey),
78
+ dedupeWindowMs: normalizeNonNegativeInteger(source.dedupeWindowMs, event.dedupeWindowMs || 0)
79
+ });
80
+ }
81
+
82
+ function normalizePresenter(candidate = {}) {
83
+ const source = isRecord(candidate) ? candidate : {};
84
+ const id = normalizeText(source.id);
85
+ if (!id) {
86
+ throw new Error("Error presenter requires id.");
87
+ }
88
+
89
+ if (typeof source.present !== "function") {
90
+ throw new Error(`Error presenter "${id}" requires present(payload).`);
91
+ }
92
+
93
+ return Object.freeze({
94
+ id,
95
+ supports: typeof source.supports === "function" ? source.supports.bind(source) : () => true,
96
+ present: source.present.bind(source),
97
+ dismiss: typeof source.dismiss === "function" ? source.dismiss.bind(source) : null
98
+ });
99
+ }
100
+
101
+ function createErrorRuntime({
102
+ presenters = [],
103
+ policy = null,
104
+ defaultPresenterId = "",
105
+ moduleDefaultPresenterId = "",
106
+ logger = null
107
+ } = {}) {
108
+ const runtimeLogger = createRuntimeLogger(logger);
109
+ const byPresenterId = new Map();
110
+ const listeners = new Set();
111
+ const dedupeWindowByKey = new Map();
112
+ let activePolicy = typeof policy === "function" ? policy : createDefaultErrorPolicy();
113
+ let activeAppDefaultPresenterId = normalizeText(defaultPresenterId);
114
+ const activeModuleDefaultPresenterId = normalizeText(moduleDefaultPresenterId);
115
+
116
+ function getPresenterIds() {
117
+ return Object.freeze([...byPresenterId.keys()].sort((left, right) => left.localeCompare(right)));
118
+ }
119
+
120
+ function assertPresenterExists(presenterId, label) {
121
+ if (!byPresenterId.has(presenterId)) {
122
+ throw new Error(`${label} presenter "${presenterId}" is not registered.`);
123
+ }
124
+ }
125
+
126
+ function resolveDefaultPresenterId() {
127
+ if (activeAppDefaultPresenterId) {
128
+ assertPresenterExists(activeAppDefaultPresenterId, "App default error");
129
+ return activeAppDefaultPresenterId;
130
+ }
131
+
132
+ if (activeModuleDefaultPresenterId) {
133
+ assertPresenterExists(activeModuleDefaultPresenterId, "Module default error");
134
+ return activeModuleDefaultPresenterId;
135
+ }
136
+
137
+ throw new Error("Error runtime requires app default presenter or module default presenter.");
138
+ }
139
+
140
+ function assertBootReady() {
141
+ resolveDefaultPresenterId();
142
+ }
143
+
144
+ function registerPresenter(presenter) {
145
+ const normalized = normalizePresenter(presenter);
146
+ if (byPresenterId.has(normalized.id)) {
147
+ throw new Error(`Error presenter "${normalized.id}" is already registered.`);
148
+ }
149
+
150
+ byPresenterId.set(normalized.id, normalized);
151
+ return normalized.id;
152
+ }
153
+
154
+ function registerPresenters(nextPresenters = []) {
155
+ const source = Array.isArray(nextPresenters) ? nextPresenters : [];
156
+ const ids = [];
157
+ for (const presenter of source) {
158
+ ids.push(registerPresenter(presenter));
159
+ }
160
+ return Object.freeze(ids);
161
+ }
162
+
163
+ function setPolicy(nextPolicy) {
164
+ if (typeof nextPolicy !== "function") {
165
+ throw new TypeError("Error policy must be a function.");
166
+ }
167
+ activePolicy = nextPolicy;
168
+ }
169
+
170
+ function setAppDefaultPresenterId(nextDefaultPresenterId = "") {
171
+ const normalized = normalizeText(nextDefaultPresenterId);
172
+ if (normalized) {
173
+ assertPresenterExists(normalized, "App default error");
174
+ }
175
+
176
+ activeAppDefaultPresenterId = normalized;
177
+ return activeAppDefaultPresenterId;
178
+ }
179
+
180
+ function subscribe(listener) {
181
+ if (typeof listener !== "function") {
182
+ return () => {};
183
+ }
184
+
185
+ listeners.add(listener);
186
+ return () => {
187
+ listeners.delete(listener);
188
+ };
189
+ }
190
+
191
+ function notify(event = {}) {
192
+ const payload = Object.freeze({ ...event });
193
+ for (const listener of listeners) {
194
+ try {
195
+ listener(payload);
196
+ } catch (error) {
197
+ runtimeLogger.warn(
198
+ {
199
+ error: String(error?.message || error || "unknown error")
200
+ },
201
+ "Error runtime subscriber failed."
202
+ );
203
+ }
204
+ }
205
+ }
206
+
207
+ function resolveChannelCompatiblePresenter(channel = "") {
208
+ for (const presenter of byPresenterId.values()) {
209
+ if (presenter.supports(channel)) {
210
+ return presenter;
211
+ }
212
+ }
213
+
214
+ return null;
215
+ }
216
+
217
+ function resolvePresenterForDecision(decision) {
218
+ const explicitPresenterId = normalizeText(decision.presenterId);
219
+ if (explicitPresenterId) {
220
+ assertPresenterExists(explicitPresenterId, "Policy-selected error");
221
+ return byPresenterId.get(explicitPresenterId);
222
+ }
223
+
224
+ const defaultPresenterId = resolveDefaultPresenterId();
225
+ const defaultPresenter = byPresenterId.get(defaultPresenterId);
226
+ if (defaultPresenter?.supports(decision.channel)) {
227
+ return defaultPresenter;
228
+ }
229
+
230
+ const compatiblePresenter = resolveChannelCompatiblePresenter(decision.channel);
231
+ if (!compatiblePresenter) {
232
+ throw new Error(`No error presenter supports channel "${decision.channel}".`);
233
+ }
234
+
235
+ return compatiblePresenter;
236
+ }
237
+
238
+ function shouldSkipByDedupe(decision) {
239
+ if (!decision.dedupeKey || decision.dedupeWindowMs < 1) {
240
+ return false;
241
+ }
242
+
243
+ const now = Number(Date.now());
244
+ const previousTimestamp = Number(dedupeWindowByKey.get(decision.dedupeKey) || 0);
245
+ if (now - previousTimestamp < decision.dedupeWindowMs) {
246
+ return true;
247
+ }
248
+
249
+ dedupeWindowByKey.set(decision.dedupeKey, now);
250
+ return false;
251
+ }
252
+
253
+ function report(rawEvent = {}, rawContext = {}) {
254
+ const event = normalizeErrorEvent(rawEvent);
255
+ const context = isRecord(rawContext) ? Object.freeze({ ...rawContext }) : Object.freeze({});
256
+
257
+ let policyDecision;
258
+ try {
259
+ policyDecision = activePolicy(
260
+ event,
261
+ Object.freeze({
262
+ context,
263
+ runtime: Object.freeze({
264
+ presenterIds: getPresenterIds(),
265
+ appDefaultPresenterId: activeAppDefaultPresenterId,
266
+ moduleDefaultPresenterId: activeModuleDefaultPresenterId
267
+ })
268
+ })
269
+ );
270
+ } catch (error) {
271
+ runtimeLogger.error(
272
+ {
273
+ source: event.source,
274
+ error: String(error?.message || error || "unknown error")
275
+ },
276
+ "Error policy threw while evaluating error event."
277
+ );
278
+ throw error;
279
+ }
280
+
281
+ const decision = normalizePolicyDecision(policyDecision, event);
282
+
283
+ if (decision.channel === "silent") {
284
+ const silentResult = Object.freeze({
285
+ event,
286
+ decision,
287
+ skipped: true,
288
+ reason: "silent"
289
+ });
290
+ notify({
291
+ type: "reported.silent",
292
+ result: silentResult
293
+ });
294
+ return silentResult;
295
+ }
296
+
297
+ if (shouldSkipByDedupe(decision)) {
298
+ const dedupedResult = Object.freeze({
299
+ event,
300
+ decision,
301
+ skipped: true,
302
+ reason: "dedupe"
303
+ });
304
+ notify({
305
+ type: "reported.deduped",
306
+ result: dedupedResult
307
+ });
308
+ return dedupedResult;
309
+ }
310
+
311
+ const presenter = resolvePresenterForDecision(decision);
312
+ if (!presenter.supports(decision.channel)) {
313
+ throw new Error(
314
+ `Error presenter "${presenter.id}" does not support channel "${decision.channel}".`
315
+ );
316
+ }
317
+
318
+ const payload = Object.freeze({
319
+ ...decision,
320
+ presenterId: presenter.id,
321
+ event,
322
+ context
323
+ });
324
+
325
+ const presentationId = normalizeText(presenter.present(payload));
326
+
327
+ const result = Object.freeze({
328
+ event,
329
+ decision: Object.freeze({
330
+ ...decision,
331
+ presenterId: presenter.id
332
+ }),
333
+ presentationId,
334
+ skipped: false
335
+ });
336
+
337
+ notify({
338
+ type: "reported",
339
+ result
340
+ });
341
+
342
+ return result;
343
+ }
344
+
345
+ function dismiss(presentationId = "", options = {}) {
346
+ const normalizedPresentationId = normalizeText(presentationId);
347
+ const source = isRecord(options) ? options : {};
348
+ const presenterId = normalizeText(source.presenterId);
349
+
350
+ if (presenterId) {
351
+ const presenter = byPresenterId.get(presenterId);
352
+ if (!presenter || typeof presenter.dismiss !== "function") {
353
+ return 0;
354
+ }
355
+ return Number(presenter.dismiss(normalizedPresentationId) || 0);
356
+ }
357
+
358
+ let total = 0;
359
+ for (const presenter of byPresenterId.values()) {
360
+ if (typeof presenter.dismiss !== "function") {
361
+ continue;
362
+ }
363
+ total += Number(presenter.dismiss(normalizedPresentationId) || 0);
364
+ }
365
+ return total;
366
+ }
367
+
368
+ function configure(options = {}) {
369
+ const source = isRecord(options) ? options : {};
370
+
371
+ if (Array.isArray(source.presenters) && source.presenters.length > 0) {
372
+ registerPresenters(source.presenters);
373
+ }
374
+
375
+ if (Object.prototype.hasOwnProperty.call(source, "policy")) {
376
+ setPolicy(source.policy);
377
+ }
378
+
379
+ if (Object.prototype.hasOwnProperty.call(source, "defaultPresenterId")) {
380
+ setAppDefaultPresenterId(source.defaultPresenterId);
381
+ }
382
+
383
+ assertBootReady();
384
+
385
+ return getSnapshot();
386
+ }
387
+
388
+ function getSnapshot() {
389
+ return Object.freeze({
390
+ presenterIds: getPresenterIds(),
391
+ appDefaultPresenterId: activeAppDefaultPresenterId,
392
+ moduleDefaultPresenterId: activeModuleDefaultPresenterId,
393
+ resolvedDefaultPresenterId: resolveDefaultPresenterId()
394
+ });
395
+ }
396
+
397
+ registerPresenters(presenters);
398
+ assertBootReady();
399
+
400
+ return Object.freeze({
401
+ report,
402
+ dismiss,
403
+ configure,
404
+ registerPresenter,
405
+ registerPresenters,
406
+ setPolicy,
407
+ setAppDefaultPresenterId,
408
+ assertBootReady,
409
+ getSnapshot,
410
+ subscribe,
411
+ normalizeErrorEvent
412
+ });
413
+ }
414
+
415
+ export {
416
+ createErrorRuntime,
417
+ normalizeErrorEvent
418
+ };
@@ -0,0 +1,176 @@
1
+ import {
2
+ normalizeAction,
3
+ normalizeSeverity,
4
+ normalizeText
5
+ } from "./normalize.js";
6
+
7
+ const PRESENTATION_CHANNELS = Object.freeze(["snackbar", "banner", "dialog"]);
8
+ const SINGLETON_CHANNELS = new Set(["banner"]);
9
+
10
+ function createEmptyChannelState() {
11
+ return {
12
+ snackbar: [],
13
+ banner: [],
14
+ dialog: []
15
+ };
16
+ }
17
+
18
+ function cloneEntry(entry = {}) {
19
+ return Object.freeze({
20
+ id: String(entry.id || "").trim(),
21
+ channel: String(entry.channel || "").trim(),
22
+ message: String(entry.message || "").trim(),
23
+ severity: String(entry.severity || "error").trim(),
24
+ persist: Boolean(entry.persist),
25
+ action: entry.action || null,
26
+ presenterId: String(entry.presenterId || "").trim(),
27
+ dedupeKey: String(entry.dedupeKey || "").trim(),
28
+ timestamp: Number(entry.timestamp || 0)
29
+ });
30
+ }
31
+
32
+ function createErrorPresentationStore({
33
+ now = () => Date.now()
34
+ } = {}) {
35
+ const listeners = new Set();
36
+ const channelState = createEmptyChannelState();
37
+ let sequence = 0;
38
+ let revision = 0;
39
+
40
+ function getChannelEntries(channel) {
41
+ if (!PRESENTATION_CHANNELS.includes(channel)) {
42
+ throw new Error(`Unknown presentation channel "${channel}".`);
43
+ }
44
+ return channelState[channel];
45
+ }
46
+
47
+ function getState() {
48
+ return Object.freeze({
49
+ revision,
50
+ channels: Object.freeze({
51
+ snackbar: Object.freeze(channelState.snackbar.map(cloneEntry)),
52
+ banner: Object.freeze(channelState.banner.map(cloneEntry)),
53
+ dialog: Object.freeze(channelState.dialog.map(cloneEntry))
54
+ })
55
+ });
56
+ }
57
+
58
+ function notify(event = {}) {
59
+ const snapshot = getState();
60
+ for (const listener of listeners) {
61
+ try {
62
+ listener(snapshot, Object.freeze({ ...event }));
63
+ } catch {
64
+ // Ignore store listener failures so one broken consumer does not break the runtime.
65
+ }
66
+ }
67
+ }
68
+
69
+ function subscribe(listener) {
70
+ if (typeof listener !== "function") {
71
+ return () => {};
72
+ }
73
+
74
+ listeners.add(listener);
75
+ return () => {
76
+ listeners.delete(listener);
77
+ };
78
+ }
79
+
80
+ function present(channel, payload = {}) {
81
+ const normalizedChannel = String(channel || "").trim();
82
+ const entries = getChannelEntries(normalizedChannel);
83
+ sequence += 1;
84
+
85
+ const entry = Object.freeze({
86
+ id: normalizeText(payload.id, `${normalizedChannel}-${sequence}`),
87
+ channel: normalizedChannel,
88
+ message: normalizeText(payload.message, "Request failed."),
89
+ severity: normalizeSeverity(payload.severity, "error"),
90
+ persist: typeof payload.persist === "boolean" ? payload.persist : normalizedChannel !== "snackbar",
91
+ action: normalizeAction(payload.action),
92
+ presenterId: normalizeText(payload.presenterId),
93
+ dedupeKey: normalizeText(payload.dedupeKey),
94
+ timestamp: Number(now())
95
+ });
96
+
97
+ if (SINGLETON_CHANNELS.has(normalizedChannel) && entries.length > 0) {
98
+ entries.splice(0, entries.length);
99
+ }
100
+
101
+ entries.push(entry);
102
+ revision += 1;
103
+
104
+ notify({
105
+ type: "presented",
106
+ channel: normalizedChannel,
107
+ id: entry.id
108
+ });
109
+
110
+ return entry.id;
111
+ }
112
+
113
+ function dismiss(channel, presentationId = "") {
114
+ const normalizedChannel = String(channel || "").trim();
115
+ const entries = getChannelEntries(normalizedChannel);
116
+ const normalizedPresentationId = normalizeText(presentationId);
117
+
118
+ if (!normalizedPresentationId) {
119
+ if (entries.length < 1) {
120
+ return 0;
121
+ }
122
+ const removed = entries.length;
123
+ entries.splice(0, entries.length);
124
+ revision += 1;
125
+ notify({
126
+ type: "dismissed",
127
+ channel: normalizedChannel,
128
+ id: "",
129
+ count: removed
130
+ });
131
+ return removed;
132
+ }
133
+
134
+ const index = entries.findIndex((entry) => entry.id === normalizedPresentationId);
135
+ if (index < 0) {
136
+ return 0;
137
+ }
138
+
139
+ entries.splice(index, 1);
140
+ revision += 1;
141
+ notify({
142
+ type: "dismissed",
143
+ channel: normalizedChannel,
144
+ id: normalizedPresentationId,
145
+ count: 1
146
+ });
147
+
148
+ return 1;
149
+ }
150
+
151
+ function clear(channel = "") {
152
+ const normalizedChannel = normalizeText(channel).toLowerCase();
153
+ if (!normalizedChannel) {
154
+ let removedCount = 0;
155
+ for (const candidateChannel of PRESENTATION_CHANNELS) {
156
+ removedCount += dismiss(candidateChannel, "");
157
+ }
158
+ return removedCount;
159
+ }
160
+
161
+ return dismiss(normalizedChannel, "");
162
+ }
163
+
164
+ return Object.freeze({
165
+ getState,
166
+ subscribe,
167
+ present,
168
+ dismiss,
169
+ clear
170
+ });
171
+ }
172
+
173
+ export {
174
+ PRESENTATION_CHANNELS,
175
+ createErrorPresentationStore
176
+ };
@@ -0,0 +1,14 @@
1
+ const SHELL_WEB_ERROR_RUNTIME_CLIENT_TOKEN = "runtime.web-error.client";
2
+ const SHELL_WEB_ERROR_PRESENTATION_STORE_CLIENT_TOKEN = "runtime.web-error.presentation-store.client";
3
+
4
+ const SHELL_WEB_ERROR_RUNTIME_INJECTION_KEY = Symbol.for("jskit.shell-web.runtime.web-error.client");
5
+ const SHELL_WEB_ERROR_PRESENTATION_STORE_INJECTION_KEY = Symbol.for(
6
+ "jskit.shell-web.runtime.web-error.presentation-store.client"
7
+ );
8
+
9
+ export {
10
+ SHELL_WEB_ERROR_RUNTIME_CLIENT_TOKEN,
11
+ SHELL_WEB_ERROR_PRESENTATION_STORE_CLIENT_TOKEN,
12
+ SHELL_WEB_ERROR_RUNTIME_INJECTION_KEY,
13
+ SHELL_WEB_ERROR_PRESENTATION_STORE_INJECTION_KEY
14
+ };
@@ -0,0 +1,17 @@
1
+ import {
2
+ ShellWebClientProvider,
3
+ SHELL_WEB_QUERY_CLIENT_TOKEN
4
+ } from "./providers/ShellWebClientProvider.js";
5
+
6
+ export {
7
+ ShellWebClientProvider,
8
+ SHELL_WEB_QUERY_CLIENT_TOKEN
9
+ } from "./providers/ShellWebClientProvider.js";
10
+
11
+ export { default as ShellLayout } from "./components/ShellLayout.vue";
12
+ export { default as ShellOutlet } from "./components/ShellOutlet.vue";
13
+ export { default as ShellErrorHost } from "./components/ShellErrorHost.vue";
14
+
15
+ const clientProviders = Object.freeze([ShellWebClientProvider]);
16
+
17
+ export { clientProviders };