@jskit-ai/shell-web 0.1.85 → 0.1.87
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/package.descriptor.mjs +6 -3
- package/package.json +3 -2
- package/src/client/asyncModuleRecovery/index.js +4 -0
- package/src/client/asyncModuleRecovery/inject.js +30 -0
- package/src/client/index.js +4 -0
- package/src/client/providers/ShellWebClientProvider.js +161 -17
- package/src/client/providers/appModuleLoadFailure.js +29 -0
- package/templates/expected-existing/src/pages/home/index.vue +8 -2
- package/templates/src/pages/home/index.vue +6 -2
- package/templates/src/pages/home/settings.vue +6 -2
- package/test/asyncModuleRecoveryInject.test.js +49 -0
- package/test/bootstrapClaimContract.test.js +2 -2
- package/test/provider.test.js +204 -24
- package/test/settingsPlacementContract.test.js +37 -0
package/package.descriptor.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/shell-web",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.87",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Web shell layout runtime with outlet-based placement contributions.",
|
|
7
7
|
dependsOn: [],
|
|
8
8
|
capabilities: {
|
|
9
9
|
provides: [
|
|
10
10
|
"runtime.web-placement",
|
|
11
|
-
"runtime.web-error"
|
|
11
|
+
"runtime.web-error",
|
|
12
|
+
"runtime.web-async-module-recovery"
|
|
12
13
|
],
|
|
13
14
|
requires: []
|
|
14
15
|
},
|
|
@@ -54,6 +55,8 @@ export default Object.freeze({
|
|
|
54
55
|
client: [
|
|
55
56
|
"runtime.web-placement.client",
|
|
56
57
|
"runtime.web-bootstrap.client",
|
|
58
|
+
"runtime.web-refresh.client",
|
|
59
|
+
"runtime.web-async-module-recovery.client",
|
|
57
60
|
"runtime.web-error.client",
|
|
58
61
|
"runtime.web-error.presentation-store.client"
|
|
59
62
|
]
|
|
@@ -291,7 +294,7 @@ export default Object.freeze({
|
|
|
291
294
|
dependencies: {
|
|
292
295
|
runtime: {
|
|
293
296
|
"@mdi/js": "^7.4.47",
|
|
294
|
-
"@jskit-ai/kernel": "0.1.
|
|
297
|
+
"@jskit-ai/kernel": "0.1.88"
|
|
295
298
|
},
|
|
296
299
|
dev: {}
|
|
297
300
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/shell-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.87",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
10
10
|
"./client/error": "./src/client/error/index.js",
|
|
11
11
|
"./client/placement": "./src/client/placement/index.js",
|
|
12
|
+
"./client/asyncModuleRecovery": "./src/client/asyncModuleRecovery/index.js",
|
|
12
13
|
"./client/bootstrap": "./src/client/bootstrap/index.js",
|
|
13
14
|
"./server/support/localLinkItemScaffolds": "./src/server/support/localLinkItemScaffolds.js",
|
|
14
15
|
"./client/navigation/linkResolver": "./src/client/navigation/linkResolver.js",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"@mdi/js": "^7.4.47",
|
|
29
|
-
"@jskit-ai/kernel": "0.1.
|
|
30
|
+
"@jskit-ai/kernel": "0.1.88"
|
|
30
31
|
},
|
|
31
32
|
"peerDependencies": {
|
|
32
33
|
"pinia": "^3.0.4",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasInjectionContext,
|
|
3
|
+
inject
|
|
4
|
+
} from "vue";
|
|
5
|
+
|
|
6
|
+
const SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY =
|
|
7
|
+
"jskit.shell-web.runtime.web-async-module-recovery.client";
|
|
8
|
+
|
|
9
|
+
function isShellAsyncModuleRecoveryRuntime(value) {
|
|
10
|
+
return Boolean(
|
|
11
|
+
value &&
|
|
12
|
+
typeof value.notify === "function" &&
|
|
13
|
+
typeof value.reload === "function"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function useShellAsyncModuleRecoveryRuntime() {
|
|
18
|
+
if (!hasInjectionContext()) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const runtime = inject(SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY, null);
|
|
23
|
+
return isShellAsyncModuleRecoveryRuntime(runtime) ? runtime : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY,
|
|
28
|
+
isShellAsyncModuleRecoveryRuntime,
|
|
29
|
+
useShellAsyncModuleRecoveryRuntime
|
|
30
|
+
};
|
package/src/client/index.js
CHANGED
|
@@ -17,6 +17,10 @@ export { default as ShellTabLinkItem } from "./components/ShellTabLinkItem.vue";
|
|
|
17
17
|
export { useShellLayoutState } from "./composables/useShellLayoutState.js";
|
|
18
18
|
export { useShellLayoutStore } from "./stores/useShellLayoutStore.js";
|
|
19
19
|
export { useShellErrorPresentationStore } from "./stores/useShellErrorPresentationStore.js";
|
|
20
|
+
export {
|
|
21
|
+
SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY,
|
|
22
|
+
useShellAsyncModuleRecoveryRuntime
|
|
23
|
+
} from "./asyncModuleRecovery/index.js";
|
|
20
24
|
export {
|
|
21
25
|
BOOTSTRAP_PAYLOAD_HANDLER_TAG,
|
|
22
26
|
registerBootstrapPayloadHandler,
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { getClientAppConfig } from "@jskit-ai/kernel/client";
|
|
2
|
+
import {
|
|
3
|
+
createAsyncModuleRecoveryState,
|
|
4
|
+
guardedReloadApp,
|
|
5
|
+
installAsyncModuleRecoveryHandlers,
|
|
6
|
+
isDynamicImportError,
|
|
7
|
+
notifyAsyncModuleLoadError
|
|
8
|
+
} from "@jskit-ai/kernel/client/asyncModuleRecovery";
|
|
9
|
+
import {
|
|
10
|
+
isMissingDynamicModule,
|
|
11
|
+
notifyDynamicImportFailure
|
|
12
|
+
} from "./appModuleLoadFailure.js";
|
|
13
|
+
import {
|
|
14
|
+
SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY
|
|
15
|
+
} from "../asyncModuleRecovery/inject.js";
|
|
2
16
|
import {
|
|
3
17
|
isRecord
|
|
4
18
|
} from "@jskit-ai/kernel/shared/support";
|
|
@@ -29,18 +43,10 @@ import { resolveBootstrapErrorStatusCode } from "../bootstrap/bootstrapErrorStat
|
|
|
29
43
|
const APP_PLACEMENT_MODULE_SPECIFIER = "/src/placement.js";
|
|
30
44
|
const APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER = "/src/placementTopology.js";
|
|
31
45
|
const APP_ERROR_MODULE_SPECIFIER = "/src/error.js";
|
|
46
|
+
const ASYNC_MODULE_RELOAD_FAILURE_MESSAGE =
|
|
47
|
+
"The app cannot reload because the app server is not reachable. Restart the server, then click Reload.";
|
|
32
48
|
|
|
33
|
-
function
|
|
34
|
-
const message = String(error?.message || error || "");
|
|
35
|
-
return (
|
|
36
|
-
message.includes(moduleSpecifier) &&
|
|
37
|
-
(message.includes("Cannot find module") ||
|
|
38
|
-
message.includes("Failed to fetch dynamically imported module") ||
|
|
39
|
-
message.includes("ERR_MODULE_NOT_FOUND"))
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function loadAppPlacementDefinitions(logger) {
|
|
49
|
+
async function loadAppPlacementDefinitions(logger, asyncModuleRecoveryRuntime = null) {
|
|
44
50
|
try {
|
|
45
51
|
const moduleNamespace = await import("/src/placement.js");
|
|
46
52
|
const exported = moduleNamespace?.default;
|
|
@@ -60,6 +66,9 @@ async function loadAppPlacementDefinitions(logger) {
|
|
|
60
66
|
if (isMissingDynamicModule(error, APP_PLACEMENT_MODULE_SPECIFIER)) {
|
|
61
67
|
return [];
|
|
62
68
|
}
|
|
69
|
+
notifyDynamicImportFailure(asyncModuleRecoveryRuntime, error, {
|
|
70
|
+
label: "Placement config"
|
|
71
|
+
});
|
|
63
72
|
|
|
64
73
|
logger.warn(
|
|
65
74
|
{
|
|
@@ -73,7 +82,7 @@ async function loadAppPlacementDefinitions(logger) {
|
|
|
73
82
|
return [];
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
async function loadAppPlacementTopology(logger) {
|
|
85
|
+
async function loadAppPlacementTopology(logger, asyncModuleRecoveryRuntime = null) {
|
|
77
86
|
try {
|
|
78
87
|
const moduleNamespace = await import("/src/placementTopology.js");
|
|
79
88
|
const exported = moduleNamespace?.default;
|
|
@@ -82,6 +91,9 @@ async function loadAppPlacementTopology(logger) {
|
|
|
82
91
|
if (isMissingDynamicModule(error, APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER)) {
|
|
83
92
|
return [];
|
|
84
93
|
}
|
|
94
|
+
notifyDynamicImportFailure(asyncModuleRecoveryRuntime, error, {
|
|
95
|
+
label: "Placement topology"
|
|
96
|
+
});
|
|
85
97
|
|
|
86
98
|
logger.warn(
|
|
87
99
|
{
|
|
@@ -127,7 +139,7 @@ function createErrorConfigToolkit(errorRuntime) {
|
|
|
127
139
|
});
|
|
128
140
|
}
|
|
129
141
|
|
|
130
|
-
async function loadAppErrorConfig(logger, errorRuntime) {
|
|
142
|
+
async function loadAppErrorConfig(logger, errorRuntime, asyncModuleRecoveryRuntime = null) {
|
|
131
143
|
try {
|
|
132
144
|
const moduleNamespace = await import("/src/error.js");
|
|
133
145
|
const exported = moduleNamespace?.default;
|
|
@@ -149,6 +161,9 @@ async function loadAppErrorConfig(logger, errorRuntime) {
|
|
|
149
161
|
if (isMissingDynamicModule(error, APP_ERROR_MODULE_SPECIFIER)) {
|
|
150
162
|
return {};
|
|
151
163
|
}
|
|
164
|
+
notifyDynamicImportFailure(asyncModuleRecoveryRuntime, error, {
|
|
165
|
+
label: "Error config"
|
|
166
|
+
});
|
|
152
167
|
|
|
153
168
|
logger.warn(
|
|
154
169
|
{
|
|
@@ -362,6 +377,10 @@ function installRouterErrorBridge(app, errorRuntime, logger) {
|
|
|
362
377
|
}
|
|
363
378
|
|
|
364
379
|
router.onError((error) => {
|
|
380
|
+
if (isDynamicImportError(error)) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
365
384
|
try {
|
|
366
385
|
errorRuntime.report({
|
|
367
386
|
source: "shell-web.router.on-error",
|
|
@@ -384,6 +403,121 @@ function installRouterErrorBridge(app, errorRuntime, logger) {
|
|
|
384
403
|
});
|
|
385
404
|
}
|
|
386
405
|
|
|
406
|
+
function createShellAsyncModuleRecoveryRuntime({
|
|
407
|
+
app,
|
|
408
|
+
logger = null
|
|
409
|
+
} = {}) {
|
|
410
|
+
if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
|
|
411
|
+
throw new Error("createShellAsyncModuleRecoveryRuntime requires application has()/make().");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const runtimeLogger = logger || createSharedProviderLogger(app);
|
|
415
|
+
const state = createAsyncModuleRecoveryState();
|
|
416
|
+
let installedRecovery = null;
|
|
417
|
+
|
|
418
|
+
function errorRuntime() {
|
|
419
|
+
if (!app.has("runtime.web-error.client")) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
const runtime = app.make("runtime.web-error.client");
|
|
423
|
+
return runtime && typeof runtime.report === "function" ? runtime : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function report(nextState = state) {
|
|
427
|
+
const runtime = errorRuntime();
|
|
428
|
+
if (!runtime) {
|
|
429
|
+
runtimeLogger.warn(
|
|
430
|
+
{
|
|
431
|
+
label: String(nextState?.label || "")
|
|
432
|
+
},
|
|
433
|
+
"Async module recovery could not report because the shell error runtime is unavailable."
|
|
434
|
+
);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
return runtime.report({
|
|
440
|
+
source: "shell-web.async-module-recovery",
|
|
441
|
+
message: String(nextState?.message || "A required app module could not load."),
|
|
442
|
+
cause: nextState?.error || null,
|
|
443
|
+
intent: "app-recoverable",
|
|
444
|
+
severity: "warning",
|
|
445
|
+
dedupeKey: `shell-web.async-module-recovery.${Number(nextState?.attempt || 0)}`,
|
|
446
|
+
action: {
|
|
447
|
+
label: "Reload",
|
|
448
|
+
dismissOnRun: true,
|
|
449
|
+
handler() {
|
|
450
|
+
return reload();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
} catch (error) {
|
|
455
|
+
runtimeLogger.error(
|
|
456
|
+
{
|
|
457
|
+
label: String(nextState?.label || ""),
|
|
458
|
+
error: String(error?.message || error || "unknown error")
|
|
459
|
+
},
|
|
460
|
+
"Async module recovery could not report through the shell error runtime."
|
|
461
|
+
);
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function reload() {
|
|
467
|
+
const reloaded = await guardedReloadApp({
|
|
468
|
+
state,
|
|
469
|
+
label: "App",
|
|
470
|
+
message: ASYNC_MODULE_RELOAD_FAILURE_MESSAGE
|
|
471
|
+
});
|
|
472
|
+
if (!reloaded) {
|
|
473
|
+
report(state);
|
|
474
|
+
}
|
|
475
|
+
return reloaded;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function notify(error = null, {
|
|
479
|
+
label = "App module",
|
|
480
|
+
message = "",
|
|
481
|
+
stale = isDynamicImportError(error)
|
|
482
|
+
} = {}) {
|
|
483
|
+
notifyAsyncModuleLoadError(state, error, {
|
|
484
|
+
label,
|
|
485
|
+
message,
|
|
486
|
+
retry: state.retry,
|
|
487
|
+
stale
|
|
488
|
+
});
|
|
489
|
+
return report(state);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function install() {
|
|
493
|
+
if (installedRecovery) {
|
|
494
|
+
return installedRecovery;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
installedRecovery = installAsyncModuleRecoveryHandlers({
|
|
498
|
+
state,
|
|
499
|
+
label: "App module",
|
|
500
|
+
router: app.has("jskit.client.router") ? app.make("jskit.client.router") : null,
|
|
501
|
+
onNotify: report
|
|
502
|
+
});
|
|
503
|
+
return installedRecovery;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function dispose() {
|
|
507
|
+
installedRecovery?.dispose?.();
|
|
508
|
+
installedRecovery = null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return Object.freeze({
|
|
512
|
+
dispose,
|
|
513
|
+
install,
|
|
514
|
+
notify,
|
|
515
|
+
reload,
|
|
516
|
+
report,
|
|
517
|
+
state
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
387
521
|
class ShellWebClientProvider {
|
|
388
522
|
static id = "shell.web.client";
|
|
389
523
|
|
|
@@ -437,6 +571,12 @@ class ShellWebClientProvider {
|
|
|
437
571
|
logger
|
|
438
572
|
})
|
|
439
573
|
);
|
|
574
|
+
app.singleton("runtime.web-async-module-recovery.client", (scope) =>
|
|
575
|
+
createShellAsyncModuleRecoveryRuntime({
|
|
576
|
+
app: scope,
|
|
577
|
+
logger
|
|
578
|
+
})
|
|
579
|
+
);
|
|
440
580
|
app.singleton("runtime.web-error.presentation-store.client", () => createErrorPresentationStore());
|
|
441
581
|
app.singleton("runtime.web-error.client", (scope) =>
|
|
442
582
|
createErrorRuntime({
|
|
@@ -456,13 +596,17 @@ class ShellWebClientProvider {
|
|
|
456
596
|
}
|
|
457
597
|
|
|
458
598
|
const logger = createSharedProviderLogger(isRecord(app) ? app : null);
|
|
599
|
+
const errorRuntime = app.make("runtime.web-error.client");
|
|
600
|
+
const asyncModuleRecoveryRuntime = app.make("runtime.web-async-module-recovery.client");
|
|
601
|
+
asyncModuleRecoveryRuntime.install();
|
|
602
|
+
|
|
459
603
|
const placementRuntime = app.make("runtime.web-placement.client");
|
|
460
604
|
if (placementRuntime && typeof placementRuntime.replacePlacements === "function") {
|
|
461
|
-
const placementTopology = await loadAppPlacementTopology(logger);
|
|
605
|
+
const placementTopology = await loadAppPlacementTopology(logger, asyncModuleRecoveryRuntime);
|
|
462
606
|
if (typeof placementRuntime.replacePlacementTopology === "function") {
|
|
463
607
|
placementRuntime.replacePlacementTopology(placementTopology, { source: APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER });
|
|
464
608
|
}
|
|
465
|
-
const placements = await loadAppPlacementDefinitions(logger);
|
|
609
|
+
const placements = await loadAppPlacementDefinitions(logger, asyncModuleRecoveryRuntime);
|
|
466
610
|
placementRuntime.replacePlacements(placements, { source: APP_PLACEMENT_MODULE_SPECIFIER });
|
|
467
611
|
const appConfig = getClientAppConfig();
|
|
468
612
|
const surfaceRuntime = app.has("jskit.client.surface.runtime")
|
|
@@ -486,8 +630,7 @@ class ShellWebClientProvider {
|
|
|
486
630
|
);
|
|
487
631
|
}
|
|
488
632
|
|
|
489
|
-
const
|
|
490
|
-
const errorConfig = await loadAppErrorConfig(logger, errorRuntime);
|
|
633
|
+
const errorConfig = await loadAppErrorConfig(logger, errorRuntime, asyncModuleRecoveryRuntime);
|
|
491
634
|
applyAppErrorConfig(errorRuntime, errorConfig);
|
|
492
635
|
|
|
493
636
|
const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
|
|
@@ -514,6 +657,7 @@ class ShellWebClientProvider {
|
|
|
514
657
|
vueApp.provide("jskit.shell-web.runtime.web-placement.client", placementRuntime);
|
|
515
658
|
vueApp.provide("jskit.shell-web.runtime.web-refresh.client", refreshRuntime);
|
|
516
659
|
vueApp.provide("jskit.shell-web.runtime.web-error.client", errorRuntime);
|
|
660
|
+
vueApp.provide(SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY, asyncModuleRecoveryRuntime);
|
|
517
661
|
vueApp.provide(
|
|
518
662
|
"jskit.shell-web.runtime.web-error.presentation-store.client",
|
|
519
663
|
errorPresentationStore
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDynamicImportError
|
|
3
|
+
} from "@jskit-ai/kernel/client/asyncModuleRecovery";
|
|
4
|
+
|
|
5
|
+
function isMissingDynamicModule(error, moduleSpecifier) {
|
|
6
|
+
const message = String(error?.message || error || "");
|
|
7
|
+
return (
|
|
8
|
+
message.includes(moduleSpecifier) &&
|
|
9
|
+
(message.includes("Cannot find module") ||
|
|
10
|
+
message.includes("ERR_MODULE_NOT_FOUND"))
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function notifyDynamicImportFailure(asyncModuleRecoveryRuntime, error, {
|
|
15
|
+
label = "App module"
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!isDynamicImportError(error) || typeof asyncModuleRecoveryRuntime?.notify !== "function") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
asyncModuleRecoveryRuntime.notify(error, {
|
|
22
|
+
label
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
isMissingDynamicModule,
|
|
28
|
+
notifyDynamicImportFailure
|
|
29
|
+
};
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
<style scoped>
|
|
21
21
|
.generated-ui-screen {
|
|
22
|
-
--generated-ui-screen-title-size:
|
|
22
|
+
--generated-ui-screen-title-size: 2rem;
|
|
23
23
|
--generated-ui-screen-panel-padding: 1rem;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
.home-start-screen__title {
|
|
32
32
|
font-size: var(--generated-ui-screen-title-size);
|
|
33
33
|
font-weight: 700;
|
|
34
|
-
letter-spacing:
|
|
34
|
+
letter-spacing: 0;
|
|
35
35
|
line-height: 1.08;
|
|
36
36
|
margin: 0 0 0.45rem;
|
|
37
37
|
}
|
|
@@ -39,4 +39,10 @@
|
|
|
39
39
|
.home-start-screen__panel {
|
|
40
40
|
padding: var(--generated-ui-screen-panel-padding);
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
@media (max-width: 640px) {
|
|
44
|
+
.generated-ui-screen {
|
|
45
|
+
--generated-ui-screen-title-size: 1.5rem;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
42
48
|
</style>
|
|
@@ -56,7 +56,7 @@ const health = computed(() => {
|
|
|
56
56
|
|
|
57
57
|
<style scoped>
|
|
58
58
|
.generated-ui-screen {
|
|
59
|
-
--generated-ui-screen-title-size:
|
|
59
|
+
--generated-ui-screen-title-size: 2rem;
|
|
60
60
|
--generated-ui-screen-panel-padding: 1rem;
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -70,7 +70,7 @@ const health = computed(() => {
|
|
|
70
70
|
.home-surface-screen__title {
|
|
71
71
|
font-size: var(--generated-ui-screen-title-size);
|
|
72
72
|
font-weight: 700;
|
|
73
|
-
letter-spacing:
|
|
73
|
+
letter-spacing: 0;
|
|
74
74
|
line-height: 1.1;
|
|
75
75
|
margin: 0 0 0.4rem;
|
|
76
76
|
}
|
|
@@ -90,6 +90,10 @@ const health = computed(() => {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
@media (max-width: 640px) {
|
|
93
|
+
.generated-ui-screen {
|
|
94
|
+
--generated-ui-screen-title-size: 1.5rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
93
97
|
.home-surface-screen__header {
|
|
94
98
|
flex-direction: column;
|
|
95
99
|
}
|
|
@@ -29,14 +29,14 @@ import { RouterView } from "vue-router";
|
|
|
29
29
|
|
|
30
30
|
<style scoped>
|
|
31
31
|
.generated-ui-screen {
|
|
32
|
-
--generated-ui-screen-title-size:
|
|
32
|
+
--generated-ui-screen-title-size: 1.85rem;
|
|
33
33
|
--generated-ui-screen-panel-padding: 1rem;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
.settings-shell__title {
|
|
37
37
|
font-size: var(--generated-ui-screen-title-size);
|
|
38
38
|
font-weight: 650;
|
|
39
|
-
letter-spacing:
|
|
39
|
+
letter-spacing: 0;
|
|
40
40
|
line-height: 1.15;
|
|
41
41
|
margin: 0 0 0.35rem;
|
|
42
42
|
}
|
|
@@ -81,6 +81,10 @@ import { RouterView } from "vue-router";
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
@media (max-width: 960px) {
|
|
84
|
+
.generated-ui-screen {
|
|
85
|
+
--generated-ui-screen-title-size: 1.35rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
84
88
|
.settings-shell__body {
|
|
85
89
|
grid-template-columns: 1fr;
|
|
86
90
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createApp } from "vue";
|
|
4
|
+
import {
|
|
5
|
+
SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY,
|
|
6
|
+
useShellAsyncModuleRecoveryRuntime
|
|
7
|
+
} from "../src/client/asyncModuleRecovery/index.js";
|
|
8
|
+
|
|
9
|
+
test("shell async module recovery runtime composable returns null outside Vue injection context", () => {
|
|
10
|
+
assert.equal(useShellAsyncModuleRecoveryRuntime(), null);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("shell async module recovery runtime composable resolves the provided public runtime", async () => {
|
|
14
|
+
const runtime = {
|
|
15
|
+
notify(error, options) {
|
|
16
|
+
return { error, options };
|
|
17
|
+
},
|
|
18
|
+
async reload() {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const app = createApp({
|
|
23
|
+
render() {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
app.provide(SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY, runtime);
|
|
28
|
+
|
|
29
|
+
assert.equal(
|
|
30
|
+
app.runWithContext(() => useShellAsyncModuleRecoveryRuntime()),
|
|
31
|
+
runtime
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("shell async module recovery runtime composable rejects incomplete runtimes", () => {
|
|
36
|
+
const app = createApp({
|
|
37
|
+
render() {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
app.provide(SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY, {
|
|
42
|
+
notify() {}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.equal(
|
|
46
|
+
app.runWithContext(() => useShellAsyncModuleRecoveryRuntime()),
|
|
47
|
+
null
|
|
48
|
+
);
|
|
49
|
+
});
|
|
@@ -7,7 +7,7 @@ import descriptor from "../package.descriptor.mjs";
|
|
|
7
7
|
|
|
8
8
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
10
|
-
const CREATE_APP_TEMPLATE_DIR = path.resolve(PACKAGE_DIR, "..", "..", "tooling", "create-app", "templates", "
|
|
10
|
+
const CREATE_APP_TEMPLATE_DIR = path.resolve(PACKAGE_DIR, "..", "..", "tooling", "create-app", "templates", "minimal-shell");
|
|
11
11
|
|
|
12
12
|
function findFileMutation(id) {
|
|
13
13
|
const files = descriptor?.mutations?.files;
|
|
@@ -48,7 +48,7 @@ test("shell-web claims starter shell files as app-owned scaffolds", () => {
|
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
test("shell-web expected-existing starter files stay aligned with create-app
|
|
51
|
+
test("shell-web expected-existing starter files stay aligned with create-app minimal-shell", async () => {
|
|
52
52
|
const comparedFiles = [
|
|
53
53
|
"src/App.vue",
|
|
54
54
|
"src/pages/home.vue",
|
package/test/provider.test.js
CHANGED
|
@@ -5,9 +5,31 @@ import {
|
|
|
5
5
|
ShellWebClientProvider,
|
|
6
6
|
resolveAppPlacementTopologyExport
|
|
7
7
|
} from "../src/client/providers/ShellWebClientProvider.js";
|
|
8
|
+
import {
|
|
9
|
+
isMissingDynamicModule
|
|
10
|
+
} from "../src/client/providers/appModuleLoadFailure.js";
|
|
11
|
+
import {
|
|
12
|
+
SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY
|
|
13
|
+
} from "../src/client/asyncModuleRecovery/index.js";
|
|
8
14
|
import { useShellErrorPresentationStore } from "../src/client/stores/useShellErrorPresentationStore.js";
|
|
9
15
|
const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
|
|
10
16
|
|
|
17
|
+
async function withTemporaryGlobal(name, value, callback) {
|
|
18
|
+
const hadPrevious = Object.prototype.hasOwnProperty.call(globalThis, name);
|
|
19
|
+
const previousValue = globalThis[name];
|
|
20
|
+
globalThis[name] = value;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return await callback();
|
|
24
|
+
} finally {
|
|
25
|
+
if (hadPrevious) {
|
|
26
|
+
globalThis[name] = previousValue;
|
|
27
|
+
} else {
|
|
28
|
+
delete globalThis[name];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
function setClientAppConfig(source = {}) {
|
|
12
34
|
const normalized =
|
|
13
35
|
source && typeof source === "object" && !Array.isArray(source) ? Object.freeze({ ...source }) : Object.freeze({});
|
|
@@ -17,7 +39,7 @@ function setClientAppConfig(source = {}) {
|
|
|
17
39
|
return normalized;
|
|
18
40
|
}
|
|
19
41
|
|
|
20
|
-
function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
|
|
42
|
+
function createAppDouble({ surfaceRuntime = null, queryClient = null, router = null } = {}) {
|
|
21
43
|
const singletons = new Map();
|
|
22
44
|
const singletonInstances = new Map();
|
|
23
45
|
const provided = [];
|
|
@@ -67,6 +89,9 @@ function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
|
|
|
67
89
|
if (token === "jskit.client.query-client") {
|
|
68
90
|
return Boolean(queryClient);
|
|
69
91
|
}
|
|
92
|
+
if (token === "jskit.client.router") {
|
|
93
|
+
return Boolean(router);
|
|
94
|
+
}
|
|
70
95
|
return singletons.has(token) || singletonInstances.has(token);
|
|
71
96
|
},
|
|
72
97
|
make(token) {
|
|
@@ -82,6 +107,9 @@ function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
|
|
|
82
107
|
if (token === "jskit.client.query-client" && queryClient) {
|
|
83
108
|
return queryClient;
|
|
84
109
|
}
|
|
110
|
+
if (token === "jskit.client.router" && router) {
|
|
111
|
+
return router;
|
|
112
|
+
}
|
|
85
113
|
if (singletonInstances.has(token)) {
|
|
86
114
|
return singletonInstances.get(token);
|
|
87
115
|
}
|
|
@@ -100,38 +128,45 @@ function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
|
|
|
100
128
|
}
|
|
101
129
|
|
|
102
130
|
async function withFetchStub(responsePayload, callback) {
|
|
103
|
-
|
|
104
|
-
globalThis.fetch = async () => ({
|
|
131
|
+
return withFetchImplementation(async () => ({
|
|
105
132
|
ok: true,
|
|
106
133
|
async json() {
|
|
107
134
|
return responsePayload;
|
|
108
135
|
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
return await callback();
|
|
113
|
-
} finally {
|
|
114
|
-
if (previousFetch === undefined) {
|
|
115
|
-
delete globalThis.fetch;
|
|
116
|
-
} else {
|
|
117
|
-
globalThis.fetch = previousFetch;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
136
|
+
}), callback);
|
|
120
137
|
}
|
|
121
138
|
|
|
122
139
|
async function withFetchImplementation(fetchImplementation, callback) {
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
return withTemporaryGlobal("fetch", fetchImplementation, callback);
|
|
141
|
+
}
|
|
125
142
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
async function withWindowDouble(windowObject, callback) {
|
|
144
|
+
return withTemporaryGlobal("window", windowObject, callback);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function createWindowDouble({ href = "https://example.test/home" } = {}) {
|
|
148
|
+
const listeners = new Map();
|
|
149
|
+
const reloadCalls = [];
|
|
150
|
+
const location = {
|
|
151
|
+
href,
|
|
152
|
+
reload() {
|
|
153
|
+
reloadCalls.push(location.href);
|
|
133
154
|
}
|
|
134
|
-
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
listeners,
|
|
159
|
+
reloadCalls,
|
|
160
|
+
location,
|
|
161
|
+
addEventListener(type, handler) {
|
|
162
|
+
listeners.set(type, handler);
|
|
163
|
+
},
|
|
164
|
+
removeEventListener(type, handler) {
|
|
165
|
+
if (listeners.get(type) === handler) {
|
|
166
|
+
listeners.delete(type);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
135
170
|
}
|
|
136
171
|
|
|
137
172
|
test("shell web client provider preserves append-only placement topology object exports", () => {
|
|
@@ -160,6 +195,23 @@ test("shell web client provider preserves append-only placement topology object
|
|
|
160
195
|
assert.deepEqual(warnings, []);
|
|
161
196
|
});
|
|
162
197
|
|
|
198
|
+
test("shell web boot import classifier does not swallow fetched dynamic module failures", () => {
|
|
199
|
+
assert.equal(
|
|
200
|
+
isMissingDynamicModule(
|
|
201
|
+
new Error("Cannot find module '/src/placement.js' imported from /src/main.js"),
|
|
202
|
+
"/src/placement.js"
|
|
203
|
+
),
|
|
204
|
+
true
|
|
205
|
+
);
|
|
206
|
+
assert.equal(
|
|
207
|
+
isMissingDynamicModule(
|
|
208
|
+
new Error("Failed to fetch dynamically imported module: http://localhost:5173/src/placement.js?t=123"),
|
|
209
|
+
"/src/placement.js"
|
|
210
|
+
),
|
|
211
|
+
false
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
163
215
|
test("shell web client provider binds runtime and injects it into Vue app", async () => {
|
|
164
216
|
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
165
217
|
const app = createAppDouble();
|
|
@@ -170,6 +222,7 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
170
222
|
assert.equal(app.singletons.has("runtime.web-error.client"), true);
|
|
171
223
|
assert.equal(app.singletons.has("runtime.web-error.presentation-store.client"), true);
|
|
172
224
|
assert.equal(app.singletons.has("runtime.web-refresh.client"), true);
|
|
225
|
+
assert.equal(app.singletons.has("runtime.web-async-module-recovery.client"), true);
|
|
173
226
|
|
|
174
227
|
await provider.boot(app);
|
|
175
228
|
assert.equal(app.plugins.length, 0);
|
|
@@ -179,6 +232,7 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
179
232
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-placement.client"), true);
|
|
180
233
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-refresh.client"), true);
|
|
181
234
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.client"), true);
|
|
235
|
+
assert.equal(providedByKey.has(SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY), true);
|
|
182
236
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.presentation-store.client"), true);
|
|
183
237
|
|
|
184
238
|
const placementRuntime = providedByKey.get("jskit.shell-web.runtime.web-placement.client");
|
|
@@ -194,6 +248,11 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
194
248
|
const refreshRuntime = providedByKey.get("jskit.shell-web.runtime.web-refresh.client");
|
|
195
249
|
assert.equal(typeof refreshRuntime.refresh, "function");
|
|
196
250
|
|
|
251
|
+
const asyncModuleRecoveryRuntime = providedByKey.get(SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY);
|
|
252
|
+
assert.equal(typeof asyncModuleRecoveryRuntime.install, "function");
|
|
253
|
+
assert.equal(typeof asyncModuleRecoveryRuntime.notify, "function");
|
|
254
|
+
assert.equal(typeof asyncModuleRecoveryRuntime.reload, "function");
|
|
255
|
+
|
|
197
256
|
const errorStore = providedByKey.get("jskit.shell-web.runtime.web-error.presentation-store.client");
|
|
198
257
|
assert.equal(typeof errorStore.getState, "function");
|
|
199
258
|
assert.equal(typeof errorStore.present, "function");
|
|
@@ -260,6 +319,127 @@ test("shell refresh runtime reports recoverable retry errors as banners", async
|
|
|
260
319
|
});
|
|
261
320
|
});
|
|
262
321
|
|
|
322
|
+
test("shell web client provider reports dynamic import failures through async module recovery", async () => {
|
|
323
|
+
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
324
|
+
const routerErrorHandlers = [];
|
|
325
|
+
const replacedPaths = [];
|
|
326
|
+
const router = {
|
|
327
|
+
onError(handler) {
|
|
328
|
+
routerErrorHandlers.push(handler);
|
|
329
|
+
return () => null;
|
|
330
|
+
},
|
|
331
|
+
replace(fullPath) {
|
|
332
|
+
replacedPaths.push(fullPath);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const app = createAppDouble({ router });
|
|
336
|
+
const provider = new ShellWebClientProvider();
|
|
337
|
+
provider.register(app);
|
|
338
|
+
await provider.boot(app);
|
|
339
|
+
|
|
340
|
+
assert.equal(routerErrorHandlers.length, 2);
|
|
341
|
+
|
|
342
|
+
const chunkError = new Error("Failed to fetch dynamically imported module: /assets/page.js");
|
|
343
|
+
for (const handler of routerErrorHandlers) {
|
|
344
|
+
handler(chunkError, {
|
|
345
|
+
fullPath: "/app/dashboard"
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
350
|
+
const state = errorStore.getState();
|
|
351
|
+
assert.equal(state.channels.banner.length, 1);
|
|
352
|
+
assert.equal(
|
|
353
|
+
state.channels.banner[0].message,
|
|
354
|
+
"Page did not download. The app may have been updated, or the network request failed."
|
|
355
|
+
);
|
|
356
|
+
assert.equal(state.channels.banner[0].severity, "warning");
|
|
357
|
+
assert.equal(state.channels.banner[0].action.label, "Reload");
|
|
358
|
+
assert.deepEqual(replacedPaths, []);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("shell web async module recovery reload action checks the document before reloading", async () => {
|
|
363
|
+
const windowObject = createWindowDouble();
|
|
364
|
+
await withWindowDouble(windowObject, async () => {
|
|
365
|
+
const fetchCalls = [];
|
|
366
|
+
await withFetchImplementation(async (input, init = {}) => {
|
|
367
|
+
fetchCalls.push({ input, init });
|
|
368
|
+
return {
|
|
369
|
+
ok: true,
|
|
370
|
+
status: 200,
|
|
371
|
+
async json() {
|
|
372
|
+
return { surfaceAccess: {} };
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}, async () => {
|
|
376
|
+
const routerErrorHandlers = [];
|
|
377
|
+
const router = {
|
|
378
|
+
onError(handler) {
|
|
379
|
+
routerErrorHandlers.push(handler);
|
|
380
|
+
return () => null;
|
|
381
|
+
},
|
|
382
|
+
replace() {}
|
|
383
|
+
};
|
|
384
|
+
const app = createAppDouble({ router });
|
|
385
|
+
const provider = new ShellWebClientProvider();
|
|
386
|
+
provider.register(app);
|
|
387
|
+
await provider.boot(app);
|
|
388
|
+
|
|
389
|
+
const chunkError = new Error("Failed to fetch dynamically imported module: /assets/page.js");
|
|
390
|
+
routerErrorHandlers[0](chunkError, {
|
|
391
|
+
fullPath: "/home/settings"
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
395
|
+
const banner = errorStore.getState().channels.banner[0];
|
|
396
|
+
await banner.action.handler();
|
|
397
|
+
|
|
398
|
+
assert.deepEqual(windowObject.reloadCalls, ["https://example.test/home"]);
|
|
399
|
+
const reloadFetch = fetchCalls.find((entry) => entry.input === "https://example.test/home");
|
|
400
|
+
assert.equal(reloadFetch?.init?.cache, "no-store");
|
|
401
|
+
assert.equal(reloadFetch?.init?.credentials, "same-origin");
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("shell web async module recovery keeps the page alive when reload check fails", async () => {
|
|
407
|
+
const windowObject = createWindowDouble();
|
|
408
|
+
await withWindowDouble(windowObject, async () => {
|
|
409
|
+
await withFetchImplementation(async (input) => {
|
|
410
|
+
if (input === "https://example.test/home") {
|
|
411
|
+
throw new TypeError("Failed to fetch");
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
status: 200,
|
|
416
|
+
async json() {
|
|
417
|
+
return { surfaceAccess: {} };
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}, async () => {
|
|
421
|
+
const app = createAppDouble();
|
|
422
|
+
const provider = new ShellWebClientProvider();
|
|
423
|
+
provider.register(app);
|
|
424
|
+
await provider.boot(app);
|
|
425
|
+
|
|
426
|
+
const recoveryRuntime = app.make("runtime.web-async-module-recovery.client");
|
|
427
|
+
const reloaded = await recoveryRuntime.reload();
|
|
428
|
+
|
|
429
|
+
assert.equal(reloaded, false);
|
|
430
|
+
assert.deepEqual(windowObject.reloadCalls, []);
|
|
431
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
432
|
+
const state = errorStore.getState();
|
|
433
|
+
assert.equal(state.channels.banner.length, 1);
|
|
434
|
+
assert.equal(
|
|
435
|
+
state.channels.banner[0].message,
|
|
436
|
+
"The app cannot reload because the app server is not reachable. Restart the server, then click Reload."
|
|
437
|
+
);
|
|
438
|
+
assert.equal(state.channels.banner[0].action.label, "Reload");
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
263
443
|
test("shell web client provider resolves surface config from client app config", async () => {
|
|
264
444
|
setClientAppConfig({
|
|
265
445
|
tenancyMode: "workspaces",
|
|
@@ -38,6 +38,11 @@ function readTopology(id = "", owner = "") {
|
|
|
38
38
|
: [];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function readClientContainerTokens() {
|
|
42
|
+
const tokens = descriptor?.metadata?.apiSummary?.containerTokens?.client;
|
|
43
|
+
return Array.isArray(tokens) ? tokens : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
function findFileMutation(id) {
|
|
42
47
|
const files = descriptor?.mutations?.files;
|
|
43
48
|
return Array.isArray(files)
|
|
@@ -143,6 +148,29 @@ test("shell-web installs generated adaptive shell Playwright smoke coverage", as
|
|
|
143
148
|
assert.equal(packageJson?.exports?.["./test/adaptiveShellSmoke"], "./src/test/adaptiveShellSmoke.js");
|
|
144
149
|
});
|
|
145
150
|
|
|
151
|
+
test("shell-web exports async module recovery runtime access as a public client API", async () => {
|
|
152
|
+
const packageJson = JSON.parse(await readFile(path.join(PACKAGE_DIR, "package.json"), "utf8"));
|
|
153
|
+
const clientIndex = await readFile(path.join(PACKAGE_DIR, "src", "client", "index.js"), "utf8");
|
|
154
|
+
const recoveryIndex = await readFile(
|
|
155
|
+
path.join(PACKAGE_DIR, "src", "client", "asyncModuleRecovery", "index.js"),
|
|
156
|
+
"utf8"
|
|
157
|
+
);
|
|
158
|
+
const providerSource = await readFile(
|
|
159
|
+
path.join(PACKAGE_DIR, "src", "client", "providers", "ShellWebClientProvider.js"),
|
|
160
|
+
"utf8"
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
assert.equal(
|
|
164
|
+
packageJson?.exports?.["./client/asyncModuleRecovery"],
|
|
165
|
+
"./src/client/asyncModuleRecovery/index.js"
|
|
166
|
+
);
|
|
167
|
+
assert.match(clientIndex, /useShellAsyncModuleRecoveryRuntime/);
|
|
168
|
+
assert.match(clientIndex, /SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY/);
|
|
169
|
+
assert.match(recoveryIndex, /useShellAsyncModuleRecoveryRuntime/);
|
|
170
|
+
assert.match(recoveryIndex, /SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY/);
|
|
171
|
+
assert.match(providerSource, /SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY/);
|
|
172
|
+
});
|
|
173
|
+
|
|
146
174
|
test("shell-web route transition keeps mobile route motion placement-driven", async () => {
|
|
147
175
|
const source = await readFile(
|
|
148
176
|
path.join(PACKAGE_DIR, "src", "client", "components", "ShellRouteTransition.vue"),
|
|
@@ -265,6 +293,15 @@ test("shell-web placement topology seeds global actions as a semantic shell plac
|
|
|
265
293
|
});
|
|
266
294
|
|
|
267
295
|
test("shell-web descriptor metadata advertises adaptive shell outlets, default links, and installs the scaffold page", () => {
|
|
296
|
+
assert.deepEqual(readClientContainerTokens(), [
|
|
297
|
+
"runtime.web-placement.client",
|
|
298
|
+
"runtime.web-bootstrap.client",
|
|
299
|
+
"runtime.web-refresh.client",
|
|
300
|
+
"runtime.web-async-module-recovery.client",
|
|
301
|
+
"runtime.web-error.client",
|
|
302
|
+
"runtime.web-error.presentation-store.client"
|
|
303
|
+
]);
|
|
304
|
+
|
|
268
305
|
assert.deepEqual(
|
|
269
306
|
readOutlets("shell-layout:primary-bottom-nav"),
|
|
270
307
|
[
|