@jskit-ai/kernel 0.1.244 → 0.1.246
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/client/appConfig.js +2 -1
- package/client/asyncModuleRecovery.js +2 -4
- package/client/index.d.ts +18 -76
- package/client/index.js +7 -2
- package/client/logging.js +8 -2
- package/client/moduleBootstrap.js +41 -33
- package/client/moduleBootstrap.test.js +35 -0
- package/client/shellBootstrap.js +38 -10
- package/client/shellBootstrap.test.js +85 -6
- package/client/shellRouting.js +16 -61
- package/client/shellRouting.test.js +30 -1
- package/client/vite/clientBootstrapPlugin.js +0 -6
- package/client/vite/clientBootstrapPlugin.test.js +2 -3
- package/internal/node/installedPackages.js +14 -58
- package/package.json +2 -4
- package/server/actions/actionCatalogue.js +13 -34
- package/{shared → server}/actions/actionContributorHelpers.js +0 -5
- package/{shared → server}/actions/actionDefinitions.js +24 -131
- package/{shared → server}/actions/actionDefinitions.test.js +75 -21
- package/server/actions/actionEvents.js +97 -2
- package/server/actions/actionEvents.test.js +79 -0
- package/{shared → server}/actions/executionContext.js +3 -13
- package/server/actions/idempotency.js +24 -0
- package/server/actions/pipeline.js +87 -0
- package/{shared → server}/actions/policies.js +20 -33
- package/{shared → server}/actions/policies.test.js +6 -6
- package/{shared → server}/actions/registry.js +3 -22
- package/{shared → server}/actions/registry.test.js +70 -32
- package/server/actions/requestMeta.js +17 -0
- package/server/http/capabilityHttpRuntime.js +1 -1
- package/server/http/capabilityHttpRuntime.test.js +56 -0
- package/server/http/errorHandling.js +291 -0
- package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
- package/server/http/lib/routeRegistration.js +36 -150
- package/server/http/lib/routeTransport.js +2 -0
- package/server/http/lib/routeValidator.js +5 -47
- package/server/http/lib/routeValidator.test.js +15 -17
- package/server/http/lib/router.js +10 -94
- package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
- package/server/platform/providerRuntime/helpers.js +1 -34
- package/server/runtime/integers.js +1 -5
- package/server/runtime/serviceAuthorization.js +4 -9
- package/server/support/appConfig.js +2 -1
- package/server/support/appConfigFiles.js +1 -38
- package/server/support/appConfigFiles.test.js +3 -39
- package/server/support/index.js +1 -20
- package/shared/capabilities/defineProvider.js +1 -1
- package/shared/capabilities/index.js +1 -1
- package/shared/index.js +1 -1
- package/shared/support/index.js +1 -4
- package/shared/support/mobileConfig.js +99 -0
- package/shared/support/mobileConfig.test.js +113 -0
- package/shared/support/normalize.js +0 -108
- package/shared/support/normalize.test.js +0 -134
- package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
- package/shared/support/returnToPath.js +16 -20
- package/shared/support/returnToPath.test.js +5 -33
- package/shared/support/shellLayoutTargets.js +2 -140
- package/shared/support/shellLayoutTargets.test.js +1 -110
- package/shared/support/sorting.js +1 -9
- package/shared/surface/apiPaths.js +1 -82
- package/shared/surface/paths.js +38 -171
- package/shared/surface/registry.js +2 -1
- package/shared/surface/runtime.js +18 -109
- package/shared/surface/runtime.test.js +57 -0
- package/shared/validators/index.js +1 -4
- package/shared/validators/jsonRestSchemaSupport.js +0 -9
- package/shared/validators/recordIdParamsValidator.js +0 -6
- package/shared/validators/schemaDefinitions.js +4 -30
- package/shared/validators/schemaPayloadValidation.js +2 -6
- package/test/barrelExposure.test.js +5 -8
- package/test/exportsContract.test.js +1 -3
- package/client/mobileLaunchRouting.js +0 -231
- package/client/mobileLaunchRouting.test.js +0 -230
- package/server/http/lib/controller.js +0 -183
- package/server/http/lib/controller.test.js +0 -143
- package/server/http/lib/index.js +0 -5
- package/server/runtime/canonicalJson.js +0 -74
- package/server/runtime/entityChangeEvents.js +0 -307
- package/server/runtime/entityChangeEvents.test.js +0 -274
- package/server/runtime/fastifyBootstrap.js +0 -553
- package/server/runtime/moduleConfig.js +0 -223
- package/server/runtime/moduleConfig.test.js +0 -165
- package/server/runtime/pagination.js +0 -13
- package/server/runtime/realtimeNormalization.js +0 -21
- package/server/runtime/routeUtils.js +0 -20
- package/server/runtime/securityAudit.js +0 -269
- package/server/runtime/securityAudit.test.js +0 -41
- package/server/support/pageTargets.js +0 -772
- package/server/support/pageTargets.test.js +0 -659
- package/server/support/shellOutlets.js +0 -576
- package/server/support/shellOutlets.test.js +0 -336
- package/server/surface/index.js +0 -18
- package/shared/actions/audit.js +0 -7
- package/shared/actions/idempotency.js +0 -62
- package/shared/actions/observability.js +0 -10
- package/shared/actions/pipeline.js +0 -309
- package/shared/actions/requestMeta.js +0 -36
- package/shared/actions/textNormalization.js +0 -3
- package/shared/actions/withActionDefaults.js +0 -34
- /package/{shared → server}/actions/executionContext.test.js +0 -0
package/client/appConfig.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isRecord
|
|
1
|
+
import { isRecord } from "../shared/support/normalize.js";
|
|
2
|
+
import { normalizeMobileConfig } from "../shared/support/mobileConfig.js";
|
|
2
3
|
|
|
3
4
|
const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
|
|
4
5
|
const EMPTY_CLIENT_APP_CONFIG = Object.freeze({});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isRecord } from "../shared/support/normalize.js";
|
|
2
|
+
|
|
1
3
|
const DYNAMIC_IMPORT_ERROR_PATTERNS = Object.freeze([
|
|
2
4
|
/ChunkLoadError/iu,
|
|
3
5
|
/Failed to fetch dynamically imported module/iu,
|
|
@@ -7,10 +9,6 @@ const DYNAMIC_IMPORT_ERROR_PATTERNS = Object.freeze([
|
|
|
7
9
|
/Unable to preload CSS/iu
|
|
8
10
|
]);
|
|
9
11
|
|
|
10
|
-
function isRecord(value) {
|
|
11
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
12
|
function errorText(error = null) {
|
|
15
13
|
return String(error?.message || error || "").trim();
|
|
16
14
|
}
|
package/client/index.d.ts
CHANGED
|
@@ -6,100 +6,40 @@ export type ClientLogger = {
|
|
|
6
6
|
isDebugEnabled?: boolean;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
attempt: number;
|
|
11
|
-
error: any;
|
|
12
|
-
label: string;
|
|
13
|
-
message: string;
|
|
14
|
-
retry: (() => any) | null;
|
|
15
|
-
stale: boolean;
|
|
16
|
-
visible: boolean;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function createAsyncModuleRecoveryState(options?: {
|
|
20
|
-
label?: string;
|
|
21
|
-
message?: string;
|
|
22
|
-
retry?: (() => any) | null;
|
|
23
|
-
}): AsyncModuleRecoveryState;
|
|
24
|
-
|
|
25
|
-
export function isDynamicImportError(error?: any): boolean;
|
|
26
|
-
|
|
27
|
-
export function dynamicImportErrorMessage(error?: any, options?: {
|
|
28
|
-
label?: string;
|
|
29
|
-
stale?: boolean;
|
|
30
|
-
}): string;
|
|
9
|
+
export * from "./asyncModuleRecovery";
|
|
31
10
|
|
|
32
|
-
export
|
|
33
|
-
state: AsyncModuleRecoveryState,
|
|
34
|
-
error?: any,
|
|
35
|
-
options?: {
|
|
36
|
-
label?: string;
|
|
37
|
-
message?: string;
|
|
38
|
-
retry?: (() => any) | null;
|
|
39
|
-
stale?: boolean;
|
|
40
|
-
}
|
|
41
|
-
): AsyncModuleRecoveryState;
|
|
42
|
-
|
|
43
|
-
export function dismissAsyncModuleRecovery(state: AsyncModuleRecoveryState): boolean;
|
|
44
|
-
|
|
45
|
-
export function guardedReloadApp(options?: {
|
|
46
|
-
browserWindow?: any;
|
|
47
|
-
fetchFn?: ((input: string, init?: Record<string, any>) => Promise<any>) | null;
|
|
48
|
-
state?: AsyncModuleRecoveryState | null;
|
|
49
|
-
label?: string;
|
|
50
|
-
message?: string;
|
|
51
|
-
}): Promise<boolean>;
|
|
52
|
-
|
|
53
|
-
export function installAsyncModuleRecoveryHandlers(options?: {
|
|
54
|
-
router?: any;
|
|
55
|
-
state: AsyncModuleRecoveryState;
|
|
56
|
-
label?: string;
|
|
57
|
-
onNotify?: (state: AsyncModuleRecoveryState) => void;
|
|
58
|
-
windowObject?: any;
|
|
59
|
-
}): Readonly<{
|
|
60
|
-
dispose: () => void;
|
|
61
|
-
}>;
|
|
11
|
+
export const DEFAULT_GUARD_EVALUATOR_KEY: "__JSKIT_WEB_SHELL_GUARD_EVALUATOR__";
|
|
62
12
|
|
|
63
13
|
export function getClientAppConfig(): Readonly<Record<string, any>>;
|
|
64
14
|
export function resolveMobileConfig(appConfig?: Record<string, any>): Readonly<Record<string, any>>;
|
|
65
15
|
export function resolveClientAssetMode(appConfig?: Record<string, any>): string;
|
|
66
|
-
export function normalizeIncomingAppUrl(
|
|
67
|
-
url?: string,
|
|
68
|
-
mobileConfig?: Record<string, any>,
|
|
69
|
-
options?: {
|
|
70
|
-
currentOrigin?: string;
|
|
71
|
-
allowedHttpOrigins?: string[];
|
|
72
|
-
}
|
|
73
|
-
): string;
|
|
74
|
-
export function registerMobileLaunchRouting(options?: {
|
|
75
|
-
router: any;
|
|
76
|
-
mobileConfig?: Record<string, any>;
|
|
77
|
-
getInitialLaunchUrl?: () => Promise<string> | string;
|
|
78
|
-
subscribeToLaunchUrls?: (handler: (url: string) => void) => (() => void) | void;
|
|
79
|
-
currentOrigin?: string;
|
|
80
|
-
allowedHttpOrigins?: string[];
|
|
81
|
-
logger?: ClientLogger;
|
|
82
|
-
}): Readonly<{
|
|
83
|
-
initialize: () => Promise<string>;
|
|
84
|
-
dispose: () => void;
|
|
85
|
-
applyIncomingUrl: (url?: string, reason?: string) => Promise<string>;
|
|
86
|
-
}>;
|
|
87
|
-
|
|
88
16
|
export function resolveClientBootstrapDebugEnabled(options?: {
|
|
89
17
|
env?: Record<string, any>;
|
|
90
18
|
debugEnabled?: boolean;
|
|
91
19
|
debugEnvKey?: string;
|
|
92
20
|
}): boolean;
|
|
93
21
|
|
|
22
|
+
export function shellScrollBehavior(to: any, from: any, savedPosition: any): any;
|
|
23
|
+
|
|
94
24
|
export function createShellRouter(options?: {
|
|
95
|
-
createRouter: (options: { history?: any; routes: any[] }) => any;
|
|
25
|
+
createRouter: (options: { history?: any; routes: any[]; scrollBehavior?: (to: any, from: any, savedPosition: any) => any }) => any;
|
|
96
26
|
history?: any;
|
|
97
27
|
routes?: any[];
|
|
98
28
|
surfaceRuntime: any;
|
|
99
29
|
surfaceMode?: string;
|
|
100
30
|
fallbackRoute?: any;
|
|
101
31
|
notFoundComponent?: any;
|
|
102
|
-
guard?:
|
|
32
|
+
guard?:
|
|
33
|
+
| false
|
|
34
|
+
| ((to: any) => any)
|
|
35
|
+
| {
|
|
36
|
+
webRootAllowed?: "yes" | "no";
|
|
37
|
+
guardEvaluatorKey?: string;
|
|
38
|
+
authenticatedPolicy?: string;
|
|
39
|
+
publicPolicy?: string;
|
|
40
|
+
};
|
|
41
|
+
/** Defaults to shellScrollBehavior; pass your own function, or false for none. */
|
|
42
|
+
scrollBehavior?: false | ((to: any, from: any, savedPosition: any) => any);
|
|
103
43
|
}): Readonly<{
|
|
104
44
|
router: any;
|
|
105
45
|
activeRoutes: readonly any[];
|
|
@@ -112,6 +52,7 @@ export function bootstrapClientShellApp(options?: {
|
|
|
112
52
|
appConfig?: Record<string, any>;
|
|
113
53
|
appPlugins?: any[];
|
|
114
54
|
pinia?: any;
|
|
55
|
+
queryClient?: any;
|
|
115
56
|
router: any;
|
|
116
57
|
bootClientModules: (context: any) => Promise<any>;
|
|
117
58
|
surfaceRuntime: any;
|
|
@@ -119,6 +60,7 @@ export function bootstrapClientShellApp(options?: {
|
|
|
119
60
|
env?: Record<string, any>;
|
|
120
61
|
fallbackRoute?: any;
|
|
121
62
|
logger?: ClientLogger;
|
|
63
|
+
/** Its isDebugEnabled decides whether bootstrap builds debug payloads; without it, a debug method means enabled. */
|
|
122
64
|
createBootstrapLogger?: (options: {
|
|
123
65
|
env?: Record<string, any>;
|
|
124
66
|
logger?: ClientLogger;
|
package/client/index.js
CHANGED
|
@@ -8,6 +8,11 @@ export {
|
|
|
8
8
|
isDynamicImportError,
|
|
9
9
|
notifyAsyncModuleLoadError
|
|
10
10
|
} from "./asyncModuleRecovery.js";
|
|
11
|
-
export {
|
|
12
|
-
|
|
11
|
+
export {
|
|
12
|
+
resolveClientBootstrapDebugEnabled,
|
|
13
|
+
createSurfaceShellRouter as createShellRouter,
|
|
14
|
+
shellScrollBehavior,
|
|
15
|
+
bootstrapClientShellApp
|
|
16
|
+
} from "./shellBootstrap.js";
|
|
17
|
+
export { DEFAULT_GUARD_EVALUATOR_KEY } from "./shellRouting.js";
|
|
13
18
|
export { createComponentInteractionEmitter } from "./componentInteraction.js";
|
package/client/logging.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { isRecord } from "../shared/support/normalize.js";
|
|
2
2
|
|
|
3
|
+
// isDebugEnabled tells callers whether debug output goes anywhere, so they
|
|
4
|
+
// can skip building debug payloads. A logger may state it; otherwise it is
|
|
5
|
+
// true exactly when the logger has its own debug method.
|
|
3
6
|
function createStructuredLogger(logger = console) {
|
|
4
7
|
if (isRecord(logger)) {
|
|
8
|
+
const hasDebug = typeof logger.debug === "function";
|
|
5
9
|
return Object.freeze({
|
|
6
10
|
info: typeof logger.info === "function" ? logger.info.bind(logger) : console.info.bind(console),
|
|
7
11
|
warn: typeof logger.warn === "function" ? logger.warn.bind(logger) : console.warn.bind(console),
|
|
8
12
|
error: typeof logger.error === "function" ? logger.error.bind(logger) : console.error.bind(console),
|
|
9
|
-
debug:
|
|
13
|
+
debug: hasDebug ? logger.debug.bind(logger) : () => {},
|
|
14
|
+
isDebugEnabled: typeof logger.isDebugEnabled === "boolean" ? logger.isDebugEnabled : hasDebug
|
|
10
15
|
});
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -14,7 +19,8 @@ function createStructuredLogger(logger = console) {
|
|
|
14
19
|
info: console.info.bind(console),
|
|
15
20
|
warn: console.warn.bind(console),
|
|
16
21
|
error: console.error.bind(console),
|
|
17
|
-
debug: () => {}
|
|
22
|
+
debug: () => {},
|
|
23
|
+
isDebugEnabled: false
|
|
18
24
|
});
|
|
19
25
|
}
|
|
20
26
|
|
|
@@ -136,16 +136,18 @@ function registerClientModuleRoutes({
|
|
|
136
136
|
surfaceMode
|
|
137
137
|
});
|
|
138
138
|
const log = createStructuredLogger(logger);
|
|
139
|
-
log.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
if (log.isDebugEnabled) {
|
|
140
|
+
log.debug(
|
|
141
|
+
{
|
|
142
|
+
packageId: normalizedPackageId,
|
|
143
|
+
source,
|
|
144
|
+
surfaceMode: String(surfaceMode || "").trim(),
|
|
145
|
+
declaredRoutes: normalizedRoutes.map(toRouteSnapshot),
|
|
146
|
+
activeRoutes: activeRoutes.map(toRouteSnapshot)
|
|
147
|
+
},
|
|
148
|
+
"Client route registration analysis."
|
|
149
|
+
);
|
|
150
|
+
}
|
|
149
151
|
|
|
150
152
|
const declaredPaths = normalizedRoutes.map((route) => route.path);
|
|
151
153
|
const activePaths = activeRoutes.map((route) => route.path);
|
|
@@ -167,14 +169,16 @@ function registerClientModuleRoutes({
|
|
|
167
169
|
|
|
168
170
|
seenRoutePaths.add(normalizedPath);
|
|
169
171
|
router.addRoute(toVueRouteRecord(route));
|
|
170
|
-
log.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
172
|
+
if (log.isDebugEnabled) {
|
|
173
|
+
log.debug(
|
|
174
|
+
{
|
|
175
|
+
packageId: normalizedPackageId,
|
|
176
|
+
source,
|
|
177
|
+
route: toRouteSnapshot(route)
|
|
178
|
+
},
|
|
179
|
+
"Added client route to router."
|
|
180
|
+
);
|
|
181
|
+
}
|
|
178
182
|
registeredCount += 1;
|
|
179
183
|
}
|
|
180
184
|
|
|
@@ -560,14 +564,16 @@ async function bootClientModules({
|
|
|
560
564
|
},
|
|
561
565
|
"Registered client module routes."
|
|
562
566
|
);
|
|
563
|
-
log.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
567
|
+
if (log.isDebugEnabled) {
|
|
568
|
+
log.debug(
|
|
569
|
+
{
|
|
570
|
+
packageId,
|
|
571
|
+
source,
|
|
572
|
+
routerRoutes: summarizeRouterRoutes(router)
|
|
573
|
+
},
|
|
574
|
+
"Router route table after client route registration."
|
|
575
|
+
);
|
|
576
|
+
}
|
|
571
577
|
routeResults.push(result);
|
|
572
578
|
return result;
|
|
573
579
|
};
|
|
@@ -599,13 +605,15 @@ async function bootClientModules({
|
|
|
599
605
|
},
|
|
600
606
|
"Booted JSKIT client modules."
|
|
601
607
|
);
|
|
602
|
-
log.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
608
|
+
if (log.isDebugEnabled) {
|
|
609
|
+
log.debug(
|
|
610
|
+
{
|
|
611
|
+
routerRoutes: summarizeRouterRoutes(router),
|
|
612
|
+
currentPath: typeof window !== "undefined" ? String(window.location?.pathname || "") : ""
|
|
613
|
+
},
|
|
614
|
+
"JSKIT client bootstrap final route table."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
609
617
|
}
|
|
610
618
|
|
|
611
619
|
return Object.freeze({
|
|
@@ -433,3 +433,38 @@ test("bootClientModules throws when packageMetadataClientProviders export is mis
|
|
|
433
433
|
/packageMetadata provider export "MissingProvider" must be a capability provider/
|
|
434
434
|
);
|
|
435
435
|
});
|
|
436
|
+
|
|
437
|
+
test("bootClientModules builds route debug payloads only when the logger's debug is enabled", async () => {
|
|
438
|
+
const surfaceRuntime = createSurfaceRuntimeFixture();
|
|
439
|
+
const clientModules = [
|
|
440
|
+
{
|
|
441
|
+
packageId: "@example/auth",
|
|
442
|
+
packageMetadataUiRoutes: [{ id: "auth.login", path: "/auth/login", scope: "global", autoRegister: false }],
|
|
443
|
+
module: {
|
|
444
|
+
clientRoutes: [{ id: "auth.login", name: "auth-login", path: "/auth/login", scope: "global", component: {} }]
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
for (const isDebugEnabled of [false, true]) {
|
|
450
|
+
const router = createRouterStub();
|
|
451
|
+
let routeTableReads = 0;
|
|
452
|
+
router.getRoutes = () => {
|
|
453
|
+
routeTableReads += 1;
|
|
454
|
+
return [...router.routes];
|
|
455
|
+
};
|
|
456
|
+
const messages = [];
|
|
457
|
+
await bootClientModules({
|
|
458
|
+
clientModules,
|
|
459
|
+
router,
|
|
460
|
+
surfaceRuntime,
|
|
461
|
+
surfaceMode: "all",
|
|
462
|
+
logger: { info() {}, warn() {}, error() {}, debug: (_payload, message) => messages.push(message), isDebugEnabled }
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
assert.equal(router.routes.length, 1);
|
|
466
|
+
assert.equal(routeTableReads > 0, isDebugEnabled);
|
|
467
|
+
assert.equal(messages.includes("Added client route to router."), isDebugEnabled);
|
|
468
|
+
assert.equal(messages.includes("JSKIT client bootstrap final route table."), isDebugEnabled);
|
|
469
|
+
}
|
|
470
|
+
});
|
package/client/shellBootstrap.js
CHANGED
|
@@ -56,6 +56,38 @@ function installAppPlugins(app, appPlugins = []) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function isSurfaceRootRecord(record) {
|
|
60
|
+
return Boolean(record?.meta?.jskit?.surface);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A page that hosts tabs or sub-views keeps its own record above the changing
|
|
64
|
+
// child, so the two locations share a non-leaf record below the surface root.
|
|
65
|
+
function staysOnSamePage(to, from) {
|
|
66
|
+
if (to.path === from.path) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const fromParents = new Set((from.matched || []).slice(0, -1));
|
|
70
|
+
return (to.matched || []).slice(0, -1).some((record) => fromParents.has(record) && !isSurfaceRootRecord(record));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* JSKIT's default router scroll behaviour: Back/Forward restore the saved
|
|
75
|
+
* position, a new screen opens at the top (or at its hash target), and tab,
|
|
76
|
+
* query or hash changes within the same page keep the current position.
|
|
77
|
+
*/
|
|
78
|
+
function shellScrollBehavior(to, from, savedPosition) {
|
|
79
|
+
if (savedPosition) {
|
|
80
|
+
return savedPosition;
|
|
81
|
+
}
|
|
82
|
+
if (staysOnSamePage(to, from)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (to.hash) {
|
|
86
|
+
return { el: to.hash, top: 0 };
|
|
87
|
+
}
|
|
88
|
+
return { top: 0 };
|
|
89
|
+
}
|
|
90
|
+
|
|
59
91
|
function createSurfaceShellRouter({
|
|
60
92
|
createRouter,
|
|
61
93
|
history,
|
|
@@ -64,7 +96,8 @@ function createSurfaceShellRouter({
|
|
|
64
96
|
surfaceMode,
|
|
65
97
|
fallbackRoute = null,
|
|
66
98
|
notFoundComponent = null,
|
|
67
|
-
guard = false
|
|
99
|
+
guard = false,
|
|
100
|
+
scrollBehavior = shellScrollBehavior
|
|
68
101
|
} = {}) {
|
|
69
102
|
if (typeof createRouter !== "function") {
|
|
70
103
|
throw new TypeError("createSurfaceShellRouter requires createRouter().");
|
|
@@ -79,7 +112,8 @@ function createSurfaceShellRouter({
|
|
|
79
112
|
});
|
|
80
113
|
const router = createRouter({
|
|
81
114
|
history,
|
|
82
|
-
routes: activeRoutes
|
|
115
|
+
routes: activeRoutes,
|
|
116
|
+
...(typeof scrollBehavior === "function" ? { scrollBehavior } : {})
|
|
83
117
|
});
|
|
84
118
|
|
|
85
119
|
if (guard !== false) {
|
|
@@ -170,14 +204,7 @@ async function bootstrapClientShellApp({
|
|
|
170
204
|
logger: bootstrapLogger
|
|
171
205
|
});
|
|
172
206
|
|
|
173
|
-
const isDebugEnabled =
|
|
174
|
-
typeof resolvedLogger?.isDebugEnabled === "boolean"
|
|
175
|
-
? resolvedLogger.isDebugEnabled
|
|
176
|
-
: resolveClientBootstrapDebugEnabled({
|
|
177
|
-
env,
|
|
178
|
-
debugEnabled,
|
|
179
|
-
debugEnvKey
|
|
180
|
-
});
|
|
207
|
+
const { isDebugEnabled } = bootstrapLogger;
|
|
181
208
|
if (isDebugEnabled) {
|
|
182
209
|
bootstrapLogger.info(
|
|
183
210
|
{
|
|
@@ -245,6 +272,7 @@ async function bootstrapClientShellApp({
|
|
|
245
272
|
}
|
|
246
273
|
|
|
247
274
|
export {
|
|
275
|
+
shellScrollBehavior,
|
|
248
276
|
resolveClientBootstrapDebugEnabled,
|
|
249
277
|
createClientBootstrapLogger,
|
|
250
278
|
createSurfaceShellRouter,
|
|
@@ -4,7 +4,8 @@ import { createSurfaceRuntime } from "../shared/surface/runtime.js";
|
|
|
4
4
|
import {
|
|
5
5
|
bootstrapClientShellApp,
|
|
6
6
|
createClientBootstrapLogger,
|
|
7
|
-
createSurfaceShellRouter
|
|
7
|
+
createSurfaceShellRouter,
|
|
8
|
+
shellScrollBehavior
|
|
8
9
|
} from "./shellBootstrap.js";
|
|
9
10
|
import { getClientAppConfig } from "./appConfig.js";
|
|
10
11
|
|
|
@@ -52,11 +53,6 @@ test("createSurfaceShellRouter builds active routes and installs a guard", () =>
|
|
|
52
53
|
surfaceMode: "app",
|
|
53
54
|
notFoundComponent: {},
|
|
54
55
|
guard: {
|
|
55
|
-
surfaceDefinitions: {
|
|
56
|
-
app: { id: "app", pagesRoot: "app", requiresAuth: false },
|
|
57
|
-
admin: { id: "admin", pagesRoot: "admin", requiresAuth: false }
|
|
58
|
-
},
|
|
59
|
-
defaultSurfaceId: "app",
|
|
60
56
|
webRootAllowed: "yes"
|
|
61
57
|
}
|
|
62
58
|
});
|
|
@@ -68,6 +64,57 @@ test("createSurfaceShellRouter builds active routes and installs a guard", () =>
|
|
|
68
64
|
assert.equal(activeRoutes.some((route) => route.path === "/admin/home"), false);
|
|
69
65
|
assert.equal(activeRoutes.some((route) => route.path === "/:pathMatch(.*)*"), true);
|
|
70
66
|
assert.equal(fallbackRoute.path, "/:pathMatch(.*)*");
|
|
67
|
+
assert.equal(routerState.options.scrollBehavior, shellScrollBehavior);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("createSurfaceShellRouter lets an app replace or disable the scroll behaviour", () => {
|
|
71
|
+
const surfaceRuntime = createSurfaceRuntimeFixture();
|
|
72
|
+
const custom = () => ({ top: 42 });
|
|
73
|
+
const seen = [];
|
|
74
|
+
for (const scrollBehavior of [custom, false]) {
|
|
75
|
+
createSurfaceShellRouter({
|
|
76
|
+
createRouter(options) {
|
|
77
|
+
seen.push(options);
|
|
78
|
+
return {};
|
|
79
|
+
},
|
|
80
|
+
routes: [],
|
|
81
|
+
surfaceRuntime,
|
|
82
|
+
surfaceMode: "app",
|
|
83
|
+
notFoundComponent: {},
|
|
84
|
+
scrollBehavior
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
assert.equal(seen[0].scrollBehavior, custom);
|
|
88
|
+
assert.equal(Object.hasOwn(seen[1], "scrollBehavior"), false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("shellScrollBehavior opens new screens at the top and keeps in-page changes still", () => {
|
|
92
|
+
const surface = { path: "/home", meta: { jskit: { surface: "home" } } };
|
|
93
|
+
const settings = { path: "/home/settings", meta: {} };
|
|
94
|
+
const general = { path: "/home/settings/general", meta: {} };
|
|
95
|
+
const profile = { path: "/home/settings/profile", meta: {} };
|
|
96
|
+
const contacts = { path: "/home/contacts", meta: {} };
|
|
97
|
+
const contact = { path: "/home/contacts/:id", meta: {} };
|
|
98
|
+
const at = (path, matched, extra = {}) => ({ path, matched, hash: "", ...extra });
|
|
99
|
+
|
|
100
|
+
const saved = { left: 0, top: 640 };
|
|
101
|
+
assert.equal(shellScrollBehavior(at("/home/contacts", [surface, contacts]), at("/home", [surface]), saved), saved);
|
|
102
|
+
assert.deepEqual(shellScrollBehavior(at("/home/contacts", [surface, contacts]), at("/home", [surface]), null), { top: 0 });
|
|
103
|
+
// Another record of the same screen type is still a new screen.
|
|
104
|
+
assert.deepEqual(
|
|
105
|
+
shellScrollBehavior(at("/home/contacts/2", [surface, contact]), at("/home/contacts/1", [surface, contact]), null),
|
|
106
|
+
{ top: 0 }
|
|
107
|
+
);
|
|
108
|
+
// Query, hash or tab changes within one page keep the position.
|
|
109
|
+
assert.equal(shellScrollBehavior(at("/home/contacts", [surface, contacts]), at("/home/contacts", [surface, contacts]), null), false);
|
|
110
|
+
assert.equal(
|
|
111
|
+
shellScrollBehavior(at("/home/settings/profile", [surface, settings, profile]), at("/home/settings/general", [surface, settings, general]), null),
|
|
112
|
+
false
|
|
113
|
+
);
|
|
114
|
+
assert.deepEqual(
|
|
115
|
+
shellScrollBehavior(at("/home/contacts", [surface, contacts], { hash: "#recent" }), at("/home", [surface]), null),
|
|
116
|
+
{ el: "#recent", top: 0 }
|
|
117
|
+
);
|
|
71
118
|
});
|
|
72
119
|
|
|
73
120
|
test("createClientBootstrapLogger enables debug from env flag", () => {
|
|
@@ -80,6 +127,38 @@ test("createClientBootstrapLogger enables debug from env flag", () => {
|
|
|
80
127
|
assert.equal(logger.isDebugEnabled, true);
|
|
81
128
|
});
|
|
82
129
|
|
|
130
|
+
test("bootstrapClientShellApp hands hooks and modules one logger that carries its debug flag", async () => {
|
|
131
|
+
for (const debugEnabled of [false, true]) {
|
|
132
|
+
let routeTableReads = 0;
|
|
133
|
+
const seenLoggers = [];
|
|
134
|
+
const router = {
|
|
135
|
+
addRoute() {},
|
|
136
|
+
getRoutes() {
|
|
137
|
+
routeTableReads += 1;
|
|
138
|
+
return [];
|
|
139
|
+
},
|
|
140
|
+
async isReady() {}
|
|
141
|
+
};
|
|
142
|
+
const result = await bootstrapClientShellApp({
|
|
143
|
+
createApp: () => ({ use() {}, mount() {} }),
|
|
144
|
+
router,
|
|
145
|
+
surfaceRuntime: createSurfaceRuntimeFixture(),
|
|
146
|
+
logger: { info() {}, warn() {}, error() {} },
|
|
147
|
+
debugEnabled,
|
|
148
|
+
bootClientModules: async ({ logger }) => {
|
|
149
|
+
seenLoggers.push(logger);
|
|
150
|
+
return {};
|
|
151
|
+
},
|
|
152
|
+
onAfterRouterReady: ({ logger }) => seenLoggers.push(logger)
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
assert.equal(result.debugEnabled, debugEnabled);
|
|
156
|
+
assert.equal(result.logger.isDebugEnabled, debugEnabled);
|
|
157
|
+
assert.deepEqual(seenLoggers, [result.logger, result.logger]);
|
|
158
|
+
assert.equal(routeTableReads > 0, debugEnabled);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
83
162
|
test("bootstrapClientShellApp boots modules, reinstalls fallback route, and mounts app", async () => {
|
|
84
163
|
const calls = [];
|
|
85
164
|
const logs = [];
|
package/client/shellRouting.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { deriveSurfaceRouteBaseFromPagesRoot } from "../shared/surface/index.js";
|
|
2
1
|
import { filterRoutesBySurface } from "../shared/surface/runtime.js";
|
|
3
2
|
import { isExternalLinkTarget } from "../shared/support/linkPath.js";
|
|
4
3
|
import { AUTH_POLICY_AUTHENTICATED, AUTH_POLICY_PUBLIC } from "../shared/support/policies.js";
|
|
@@ -101,20 +100,6 @@ function resolveSearchFromFullPath(fullPath) {
|
|
|
101
100
|
return hashStart < 0 ? rawFullPath.slice(queryStart) : rawFullPath.slice(queryStart, hashStart);
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
function resolveSurfaceDefinition(surfaceDefinitions, surfaceId) {
|
|
105
|
-
const normalizedSurfaceId = String(surfaceId || "")
|
|
106
|
-
.trim()
|
|
107
|
-
.toLowerCase();
|
|
108
|
-
if (!normalizedSurfaceId) {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
const definition = surfaceDefinitions[normalizedSurfaceId];
|
|
112
|
-
if (!definition || typeof definition !== "object") {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
return definition;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
103
|
function normalizeWebRootAllowed(value) {
|
|
119
104
|
const normalizedValue = String(value || "")
|
|
120
105
|
.trim()
|
|
@@ -125,27 +110,7 @@ function normalizeWebRootAllowed(value) {
|
|
|
125
110
|
return WEB_ROOT_ALLOW_YES;
|
|
126
111
|
}
|
|
127
112
|
|
|
128
|
-
function
|
|
129
|
-
const defaultSurface = resolveSurfaceDefinition(surfaceDefinitions, defaultSurfaceId);
|
|
130
|
-
return String(
|
|
131
|
-
defaultSurface?.routeBase || deriveSurfaceRouteBaseFromPagesRoot(defaultSurface?.pagesRoot || "")
|
|
132
|
-
).trim() || "/";
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function resolveSurfaceRequiresAuth({ pathname, surfaceRuntime, surfaceDefinitions }) {
|
|
136
|
-
const normalizedPathname = String(pathname || "/").trim() || "/";
|
|
137
|
-
const surfaceId = surfaceRuntime.resolveSurfaceFromPathname(normalizedPathname);
|
|
138
|
-
const surfaceDefinition = resolveSurfaceDefinition(surfaceDefinitions, surfaceId);
|
|
139
|
-
return Boolean(surfaceDefinition?.requiresAuth);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function resolveEffectiveRouteGuard({
|
|
143
|
-
to,
|
|
144
|
-
surfaceRuntime,
|
|
145
|
-
surfaceDefinitions,
|
|
146
|
-
authenticatedPolicy = AUTH_POLICY_AUTHENTICATED,
|
|
147
|
-
publicPolicy = AUTH_POLICY_PUBLIC
|
|
148
|
-
}) {
|
|
113
|
+
function resolveEffectiveRouteGuard({ to, surfaceRuntime, authenticatedPolicy, publicPolicy }) {
|
|
149
114
|
const routeGuard = resolveRouteGuard(to);
|
|
150
115
|
const routePolicy = normalizeGuardPolicy(routeGuard?.policy);
|
|
151
116
|
if (routePolicy) {
|
|
@@ -154,13 +119,8 @@ function resolveEffectiveRouteGuard({
|
|
|
154
119
|
};
|
|
155
120
|
}
|
|
156
121
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
pathname: to?.path || "/",
|
|
160
|
-
surfaceRuntime,
|
|
161
|
-
surfaceDefinitions
|
|
162
|
-
})
|
|
163
|
-
) {
|
|
122
|
+
const surfaceId = surfaceRuntime.resolveSurfaceFromPathname(String(to?.path || "/").trim() || "/");
|
|
123
|
+
if (surfaceRuntime.getSurfaceDefinition(surfaceId)?.requiresAuth) {
|
|
164
124
|
return {
|
|
165
125
|
policy: authenticatedPolicy
|
|
166
126
|
};
|
|
@@ -245,31 +205,27 @@ function evaluateShellGuard({ guard, to, guardEvaluatorKey }) {
|
|
|
245
205
|
|
|
246
206
|
function createShellBeforeEachGuard({
|
|
247
207
|
surfaceRuntime,
|
|
248
|
-
surfaceDefinitions,
|
|
249
|
-
defaultSurfaceId,
|
|
250
208
|
webRootAllowed = WEB_ROOT_ALLOW_YES,
|
|
251
209
|
guardEvaluatorKey = DEFAULT_GUARD_EVALUATOR_KEY,
|
|
252
210
|
authenticatedPolicy = AUTH_POLICY_AUTHENTICATED,
|
|
253
211
|
publicPolicy = AUTH_POLICY_PUBLIC
|
|
254
212
|
} = {}) {
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
213
|
+
if (
|
|
214
|
+
!surfaceRuntime ||
|
|
215
|
+
typeof surfaceRuntime.resolveSurfaceFromPathname !== "function" ||
|
|
216
|
+
typeof surfaceRuntime.getSurfaceDefinition !== "function"
|
|
217
|
+
) {
|
|
218
|
+
throw new TypeError(
|
|
219
|
+
"createShellBeforeEachGuard requires surfaceRuntime.resolveSurfaceFromPathname() and getSurfaceDefinition()."
|
|
220
|
+
);
|
|
260
221
|
}
|
|
261
222
|
|
|
223
|
+
const defaultSurfaceRootPath =
|
|
224
|
+
surfaceRuntime.getSurfaceDefinition(surfaceRuntime.DEFAULT_SURFACE_ID)?.routeBase || "/";
|
|
225
|
+
const redirectWebRoot = normalizeWebRootAllowed(webRootAllowed) === WEB_ROOT_ALLOW_NO && defaultSurfaceRootPath !== "/";
|
|
226
|
+
|
|
262
227
|
return (to) => {
|
|
263
|
-
|
|
264
|
-
const defaultSurfaceRootPath = resolveDefaultSurfaceRootPath({
|
|
265
|
-
surfaceDefinitions,
|
|
266
|
-
defaultSurfaceId
|
|
267
|
-
});
|
|
268
|
-
if (
|
|
269
|
-
normalizedWebRootAllowed === WEB_ROOT_ALLOW_NO &&
|
|
270
|
-
String(to?.path || "/").trim() === "/" &&
|
|
271
|
-
defaultSurfaceRootPath !== "/"
|
|
272
|
-
) {
|
|
228
|
+
if (redirectWebRoot && String(to?.path || "/").trim() === "/") {
|
|
273
229
|
const search = resolveSearchFromFullPath(to?.fullPath || "");
|
|
274
230
|
const hash = String(to?.hash || "").trim();
|
|
275
231
|
return `${defaultSurfaceRootPath}${search}${hash}`;
|
|
@@ -278,7 +234,6 @@ function createShellBeforeEachGuard({
|
|
|
278
234
|
const guard = resolveEffectiveRouteGuard({
|
|
279
235
|
to,
|
|
280
236
|
surfaceRuntime,
|
|
281
|
-
surfaceDefinitions,
|
|
282
237
|
authenticatedPolicy,
|
|
283
238
|
publicPolicy
|
|
284
239
|
});
|