@jskit-ai/kernel 0.1.62 → 0.1.64

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.
@@ -1,4 +1,4 @@
1
- import { isRecord } from "../shared/support/normalize.js";
1
+ import { isRecord, normalizeMobileConfig } from "../shared/support/normalize.js";
2
2
 
3
3
  const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
4
4
  const EMPTY_CLIENT_APP_CONFIG = Object.freeze({});
@@ -30,4 +30,18 @@ function getClientAppConfig() {
30
30
  return isRecord(appConfig) ? appConfig : EMPTY_CLIENT_APP_CONFIG;
31
31
  }
32
32
 
33
- export { CLIENT_APP_CONFIG_GLOBAL_KEY, setClientAppConfig, getClientAppConfig };
33
+ function resolveMobileConfig(appConfig = getClientAppConfig()) {
34
+ return normalizeMobileConfig(isRecord(appConfig) ? appConfig.mobile : {});
35
+ }
36
+
37
+ function resolveClientAssetMode(appConfig = getClientAppConfig()) {
38
+ return resolveMobileConfig(appConfig).assetMode;
39
+ }
40
+
41
+ export {
42
+ CLIENT_APP_CONFIG_GLOBAL_KEY,
43
+ setClientAppConfig,
44
+ getClientAppConfig,
45
+ resolveMobileConfig,
46
+ resolveClientAssetMode
47
+ };
@@ -0,0 +1,71 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ CLIENT_APP_CONFIG_GLOBAL_KEY,
5
+ getClientAppConfig,
6
+ resolveClientAssetMode,
7
+ resolveMobileConfig,
8
+ setClientAppConfig
9
+ } from "./appConfig.js";
10
+
11
+ test("resolveMobileConfig and resolveClientAssetMode read normalized client mobile config", () => {
12
+ const previous = globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY];
13
+
14
+ try {
15
+ setClientAppConfig({
16
+ mobile: {
17
+ enabled: true,
18
+ strategy: "capacitor",
19
+ assetMode: "dev_server",
20
+ auth: {
21
+ customScheme: "convict"
22
+ }
23
+ }
24
+ });
25
+
26
+ assert.equal(getClientAppConfig().mobile.enabled, true);
27
+ assert.deepEqual(resolveMobileConfig(), {
28
+ enabled: true,
29
+ strategy: "capacitor",
30
+ appId: "",
31
+ appName: "",
32
+ assetMode: "dev_server",
33
+ devServerUrl: "",
34
+ apiBaseUrl: "",
35
+ auth: {
36
+ callbackPath: "/auth/login",
37
+ customScheme: "convict",
38
+ appLinkDomains: []
39
+ },
40
+ android: {
41
+ packageName: "",
42
+ minSdk: 26,
43
+ targetSdk: 35,
44
+ versionCode: 1,
45
+ versionName: "1.0.0"
46
+ }
47
+ });
48
+ assert.equal(resolveClientAssetMode(), "dev_server");
49
+ } finally {
50
+ if (previous === undefined) {
51
+ delete globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY];
52
+ } else {
53
+ globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY] = previous;
54
+ }
55
+ }
56
+ });
57
+
58
+ test("resolveMobileConfig accepts explicit appConfig values", () => {
59
+ const mobileConfig = resolveMobileConfig({
60
+ mobile: {
61
+ enabled: true,
62
+ appId: "com.example.app",
63
+ appName: "Example App"
64
+ }
65
+ });
66
+
67
+ assert.equal(mobileConfig.enabled, true);
68
+ assert.equal(mobileConfig.appId, "com.example.app");
69
+ assert.equal(mobileConfig.appName, "Example App");
70
+ assert.equal(resolveClientAssetMode({}), "bundled");
71
+ });
package/client/index.d.ts CHANGED
@@ -7,6 +7,29 @@ export type ClientLogger = {
7
7
  };
8
8
 
9
9
  export function getClientAppConfig(): Readonly<Record<string, any>>;
10
+ export function resolveMobileConfig(appConfig?: Record<string, any>): Readonly<Record<string, any>>;
11
+ export function resolveClientAssetMode(appConfig?: Record<string, any>): string;
12
+ export function normalizeIncomingAppUrl(
13
+ url?: string,
14
+ mobileConfig?: Record<string, any>,
15
+ options?: {
16
+ currentOrigin?: string;
17
+ allowedHttpOrigins?: string[];
18
+ }
19
+ ): string;
20
+ export function registerMobileLaunchRouting(options?: {
21
+ router: any;
22
+ mobileConfig?: Record<string, any>;
23
+ getInitialLaunchUrl?: () => Promise<string> | string;
24
+ subscribeToLaunchUrls?: (handler: (url: string) => void) => (() => void) | void;
25
+ currentOrigin?: string;
26
+ allowedHttpOrigins?: string[];
27
+ logger?: ClientLogger;
28
+ }): Readonly<{
29
+ initialize: () => Promise<string>;
30
+ dispose: () => void;
31
+ applyIncomingUrl: (url?: string, reason?: string) => Promise<string>;
32
+ }>;
10
33
 
11
34
  export function resolveClientBootstrapDebugEnabled(options?: {
12
35
  env?: Record<string, any>;
@@ -52,6 +75,7 @@ export function bootstrapClientShellApp(options?: {
52
75
  debugEnvKey?: string;
53
76
  debugMessage?: string;
54
77
  onAfterModulesBootstrapped?: (context: any) => void | Promise<void>;
78
+ onAfterRouterReady?: (context: any) => void | Promise<void>;
55
79
  mountSelector?: string;
56
80
  }): Promise<
57
81
  Readonly<{
package/client/index.js CHANGED
@@ -1,3 +1,4 @@
1
- export { getClientAppConfig } from "./appConfig.js";
1
+ export { getClientAppConfig, resolveMobileConfig, resolveClientAssetMode } from "./appConfig.js";
2
+ export { normalizeIncomingAppUrl, registerMobileLaunchRouting } from "./mobileLaunchRouting.js";
2
3
  export { resolveClientBootstrapDebugEnabled, createSurfaceShellRouter as createShellRouter, bootstrapClientShellApp } from "./shellBootstrap.js";
3
4
  export { createComponentInteractionEmitter } from "./componentInteraction.js";
@@ -0,0 +1,231 @@
1
+ import { normalizePathname } from "../shared/surface/paths.js";
2
+ import { normalizeText } from "../shared/support/normalize.js";
3
+ import { resolveMobileConfig } from "./appConfig.js";
4
+
5
+ function buildNormalizedRoutePath(pathname = "/", search = "", hash = "") {
6
+ const normalizedPathname = normalizePathname(pathname);
7
+ const normalizedSearch = String(search || "").trim().replace(/^\?+/, "");
8
+ const normalizedHash = String(hash || "").trim();
9
+
10
+ return `${normalizedPathname}${normalizedSearch ? `?${normalizedSearch}` : ""}${normalizedHash}`;
11
+ }
12
+
13
+ function normalizeResolvedRoutePath(value = "", fallback = "") {
14
+ const normalizedValue = normalizeText(value);
15
+ if (!normalizedValue) {
16
+ return fallback;
17
+ }
18
+
19
+ try {
20
+ const parsed = new URL(normalizedValue, "https://jskit.invalid");
21
+ return buildNormalizedRoutePath(parsed.pathname, parsed.search, parsed.hash);
22
+ } catch {
23
+ return fallback;
24
+ }
25
+ }
26
+
27
+ function normalizeAllowedHttpOrigins({ mobileConfig = {}, currentOrigin = "", allowedHttpOrigins = [] } = {}) {
28
+ const origins = new Set();
29
+
30
+ const maybeAddOrigin = (value = "") => {
31
+ const normalizedValue = normalizeText(value);
32
+ if (!normalizedValue) {
33
+ return;
34
+ }
35
+
36
+ try {
37
+ const parsed = new URL(normalizedValue);
38
+ const protocol = String(parsed.protocol || "").toLowerCase();
39
+ if (protocol !== "http:" && protocol !== "https:") {
40
+ return;
41
+ }
42
+ origins.add(String(parsed.origin || "").trim().toLowerCase());
43
+ } catch {}
44
+ };
45
+
46
+ maybeAddOrigin(currentOrigin);
47
+
48
+ const explicitOrigins = Array.isArray(allowedHttpOrigins) ? allowedHttpOrigins : [allowedHttpOrigins];
49
+ for (const entry of explicitOrigins) {
50
+ maybeAddOrigin(entry);
51
+ }
52
+
53
+ const appLinkDomains = Array.isArray(mobileConfig?.auth?.appLinkDomains) ? mobileConfig.auth.appLinkDomains : [];
54
+ for (const domain of appLinkDomains) {
55
+ const normalizedDomain = normalizeText(domain).toLowerCase();
56
+ if (!normalizedDomain) {
57
+ continue;
58
+ }
59
+ maybeAddOrigin(`https://${normalizedDomain}`);
60
+ }
61
+
62
+ return origins;
63
+ }
64
+
65
+ function normalizeCustomSchemeRoutePath(parsedUrl) {
66
+ const host = normalizeText(parsedUrl?.host);
67
+ const pathname = normalizePathname(parsedUrl?.pathname || "/");
68
+
69
+ if (!host) {
70
+ return pathname;
71
+ }
72
+
73
+ if (pathname === "/") {
74
+ return `/${host}`;
75
+ }
76
+
77
+ return normalizePathname(`/${host}${pathname}`);
78
+ }
79
+
80
+ function normalizeIncomingAppUrl(url = "", mobileConfig = {}, { currentOrigin = "", allowedHttpOrigins = [] } = {}) {
81
+ const normalizedUrl = normalizeText(url);
82
+ if (!normalizedUrl) {
83
+ return "";
84
+ }
85
+
86
+ if (normalizedUrl.startsWith("/")) {
87
+ try {
88
+ const parsed = new URL(normalizedUrl, "https://jskit.invalid");
89
+ return buildNormalizedRoutePath(parsed.pathname, parsed.search, parsed.hash);
90
+ } catch {
91
+ return "";
92
+ }
93
+ }
94
+
95
+ let parsedUrl;
96
+ try {
97
+ parsedUrl = new URL(normalizedUrl);
98
+ } catch {
99
+ return "";
100
+ }
101
+
102
+ const resolvedMobileConfig = resolveMobileConfig({
103
+ mobile: mobileConfig
104
+ });
105
+ const allowedOrigins = normalizeAllowedHttpOrigins({
106
+ mobileConfig: resolvedMobileConfig,
107
+ currentOrigin,
108
+ allowedHttpOrigins
109
+ });
110
+ const protocol = String(parsedUrl.protocol || "").toLowerCase();
111
+ const customScheme = normalizeText(resolvedMobileConfig.auth.customScheme).toLowerCase();
112
+
113
+ if (customScheme && protocol === `${customScheme}:`) {
114
+ return buildNormalizedRoutePath(
115
+ normalizeCustomSchemeRoutePath(parsedUrl),
116
+ parsedUrl.search,
117
+ parsedUrl.hash
118
+ );
119
+ }
120
+
121
+ if ((protocol === "http:" || protocol === "https:") && allowedOrigins.has(String(parsedUrl.origin || "").toLowerCase())) {
122
+ return buildNormalizedRoutePath(parsedUrl.pathname, parsedUrl.search, parsedUrl.hash);
123
+ }
124
+
125
+ return "";
126
+ }
127
+
128
+ function registerMobileLaunchRouting({
129
+ router,
130
+ mobileConfig = {},
131
+ getInitialLaunchUrl = async () => "",
132
+ subscribeToLaunchUrls = () => () => {},
133
+ resolveTargetPath = null,
134
+ currentOrigin = typeof window === "object" && window?.location ? String(window.location.origin || "") : "",
135
+ allowedHttpOrigins = [],
136
+ logger = null
137
+ } = {}) {
138
+ if (!router || typeof router.replace !== "function") {
139
+ throw new TypeError("registerMobileLaunchRouting requires router.replace().");
140
+ }
141
+
142
+ const resolvedMobileConfig = resolveMobileConfig({
143
+ mobile: mobileConfig
144
+ });
145
+ const runtimeLogger =
146
+ logger && typeof logger === "object"
147
+ ? logger
148
+ : {
149
+ info() {},
150
+ warn() {},
151
+ error() {}
152
+ };
153
+
154
+ async function applyIncomingUrl(url = "", reason = "manual") {
155
+ const normalizedTargetPath = normalizeIncomingAppUrl(url, resolvedMobileConfig, {
156
+ currentOrigin,
157
+ allowedHttpOrigins
158
+ });
159
+ if (!normalizedTargetPath) {
160
+ return "";
161
+ }
162
+
163
+ let resolvedTargetPath = normalizedTargetPath;
164
+ if (typeof resolveTargetPath === "function") {
165
+ const nextTargetPath = await resolveTargetPath(
166
+ Object.freeze({
167
+ originalUrl: String(url || ""),
168
+ normalizedTargetPath,
169
+ reason,
170
+ mobileConfig: resolvedMobileConfig,
171
+ router
172
+ })
173
+ );
174
+ resolvedTargetPath = normalizeResolvedRoutePath(nextTargetPath, normalizedTargetPath);
175
+ }
176
+
177
+ const currentFullPath = String(router.currentRoute?.value?.fullPath || "").trim();
178
+ if (currentFullPath === resolvedTargetPath) {
179
+ return resolvedTargetPath;
180
+ }
181
+
182
+ await router.replace(resolvedTargetPath);
183
+ if (typeof runtimeLogger.info === "function") {
184
+ runtimeLogger.info(
185
+ {
186
+ reason,
187
+ targetPath: resolvedTargetPath
188
+ },
189
+ "Mobile launch routing applied incoming app URL."
190
+ );
191
+ }
192
+ return resolvedTargetPath;
193
+ }
194
+
195
+ async function initialize() {
196
+ if (resolvedMobileConfig.enabled !== true) {
197
+ return "";
198
+ }
199
+
200
+ const initialLaunchUrl = await getInitialLaunchUrl();
201
+ return applyIncomingUrl(initialLaunchUrl, "initial-launch");
202
+ }
203
+
204
+ const unsubscribe =
205
+ resolvedMobileConfig.enabled === true
206
+ ? subscribeToLaunchUrls((nextUrl) => {
207
+ Promise.resolve(applyIncomingUrl(nextUrl, "launch-event")).catch((error) => {
208
+ if (typeof runtimeLogger.warn === "function") {
209
+ runtimeLogger.warn(
210
+ {
211
+ error: String(error?.message || error || "unknown error")
212
+ },
213
+ "Mobile launch routing failed to apply incoming app URL."
214
+ );
215
+ }
216
+ });
217
+ })
218
+ : () => {};
219
+
220
+ return Object.freeze({
221
+ initialize,
222
+ dispose() {
223
+ if (typeof unsubscribe === "function") {
224
+ unsubscribe();
225
+ }
226
+ },
227
+ applyIncomingUrl
228
+ });
229
+ }
230
+
231
+ export { normalizeIncomingAppUrl, registerMobileLaunchRouting };
@@ -0,0 +1,230 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { normalizeIncomingAppUrl, registerMobileLaunchRouting } from "./mobileLaunchRouting.js";
4
+
5
+ test("normalizeIncomingAppUrl normalizes custom-scheme auth callback routes into router paths", () => {
6
+ const normalized = normalizeIncomingAppUrl("convict://auth/login?code=abc&oauthProvider=google", {
7
+ enabled: true,
8
+ auth: {
9
+ customScheme: "convict"
10
+ }
11
+ });
12
+
13
+ assert.equal(normalized, "/auth/login?code=abc&oauthProvider=google");
14
+ });
15
+
16
+ test("normalizeIncomingAppUrl normalizes custom-scheme workspace routes", () => {
17
+ const normalized = normalizeIncomingAppUrl("convict://w/acme/workouts/2026-05-07?tab=today", {
18
+ enabled: true,
19
+ auth: {
20
+ customScheme: "convict"
21
+ }
22
+ });
23
+
24
+ assert.equal(normalized, "/w/acme/workouts/2026-05-07?tab=today");
25
+ });
26
+
27
+ test("normalizeIncomingAppUrl normalizes allowed HTTPS app links", () => {
28
+ const normalized = normalizeIncomingAppUrl("https://app.example.com/auth/login?code=abc", {
29
+ enabled: true,
30
+ auth: {
31
+ appLinkDomains: ["app.example.com"]
32
+ }
33
+ });
34
+
35
+ assert.equal(normalized, "/auth/login?code=abc");
36
+ });
37
+
38
+ test("normalizeIncomingAppUrl accepts same-origin HTTP URLs when explicitly allowed", () => {
39
+ const normalized = normalizeIncomingAppUrl(
40
+ "http://192.168.1.10:5173/w/acme",
41
+ {
42
+ enabled: true
43
+ },
44
+ {
45
+ allowedHttpOrigins: ["http://192.168.1.10:5173"]
46
+ }
47
+ );
48
+
49
+ assert.equal(normalized, "/w/acme");
50
+ });
51
+
52
+ test("normalizeIncomingAppUrl rejects unowned schemes and domains", () => {
53
+ assert.equal(
54
+ normalizeIncomingAppUrl("otherapp://auth/login?code=abc", {
55
+ enabled: true,
56
+ auth: {
57
+ customScheme: "convict"
58
+ }
59
+ }),
60
+ ""
61
+ );
62
+
63
+ assert.equal(
64
+ normalizeIncomingAppUrl("https://evil.example.com/auth/login?code=abc", {
65
+ enabled: true,
66
+ auth: {
67
+ appLinkDomains: ["app.example.com"]
68
+ }
69
+ }),
70
+ ""
71
+ );
72
+ });
73
+
74
+ test("registerMobileLaunchRouting initializes and applies the initial launch URL", async () => {
75
+ const replaceCalls = [];
76
+ const runtime = registerMobileLaunchRouting({
77
+ router: {
78
+ currentRoute: {
79
+ value: {
80
+ fullPath: "/home"
81
+ }
82
+ },
83
+ async replace(target) {
84
+ replaceCalls.push(target);
85
+ }
86
+ },
87
+ mobileConfig: {
88
+ enabled: true,
89
+ auth: {
90
+ customScheme: "convict"
91
+ }
92
+ },
93
+ getInitialLaunchUrl: async () => "convict://auth/login?code=abc"
94
+ });
95
+
96
+ const targetPath = await runtime.initialize();
97
+
98
+ assert.equal(targetPath, "/auth/login?code=abc");
99
+ assert.deepEqual(replaceCalls, ["/auth/login?code=abc"]);
100
+ });
101
+
102
+ test("registerMobileLaunchRouting subscribes to later launch URLs and routes them", async () => {
103
+ const replaceCalls = [];
104
+ let listener = null;
105
+ const runtime = registerMobileLaunchRouting({
106
+ router: {
107
+ currentRoute: {
108
+ value: {
109
+ fullPath: "/home"
110
+ }
111
+ },
112
+ async replace(target) {
113
+ replaceCalls.push(target);
114
+ }
115
+ },
116
+ mobileConfig: {
117
+ enabled: true,
118
+ auth: {
119
+ customScheme: "convict"
120
+ }
121
+ },
122
+ subscribeToLaunchUrls(handler) {
123
+ listener = handler;
124
+ return () => {
125
+ listener = null;
126
+ };
127
+ }
128
+ });
129
+
130
+ assert.equal(typeof listener, "function");
131
+ listener("convict://w/acme");
132
+ await Promise.resolve();
133
+ await Promise.resolve();
134
+ assert.deepEqual(replaceCalls, ["/w/acme"]);
135
+
136
+ runtime.dispose();
137
+ assert.equal(listener, null);
138
+ });
139
+
140
+ test("registerMobileLaunchRouting lets a resolver override the final route target", async () => {
141
+ const replaceCalls = [];
142
+ const runtime = registerMobileLaunchRouting({
143
+ router: {
144
+ currentRoute: {
145
+ value: {
146
+ fullPath: "/home"
147
+ }
148
+ },
149
+ async replace(target) {
150
+ replaceCalls.push(target);
151
+ }
152
+ },
153
+ mobileConfig: {
154
+ enabled: true,
155
+ auth: {
156
+ customScheme: "convict"
157
+ }
158
+ },
159
+ getInitialLaunchUrl: async () => "convict://auth/login?code=abc",
160
+ resolveTargetPath({ originalUrl, normalizedTargetPath, reason }) {
161
+ assert.equal(originalUrl, "convict://auth/login?code=abc");
162
+ assert.equal(normalizedTargetPath, "/auth/login?code=abc");
163
+ assert.equal(reason, "initial-launch");
164
+ return "/w/acme";
165
+ }
166
+ });
167
+
168
+ const targetPath = await runtime.initialize();
169
+
170
+ assert.equal(targetPath, "/w/acme");
171
+ assert.deepEqual(replaceCalls, ["/w/acme"]);
172
+ });
173
+
174
+ test("registerMobileLaunchRouting forwards unknown deep-link paths to the normal router", async () => {
175
+ const replaceCalls = [];
176
+ const runtime = registerMobileLaunchRouting({
177
+ router: {
178
+ currentRoute: {
179
+ value: {
180
+ fullPath: "/home"
181
+ }
182
+ },
183
+ async replace(target) {
184
+ replaceCalls.push(target);
185
+ }
186
+ },
187
+ mobileConfig: {
188
+ enabled: true,
189
+ auth: {
190
+ customScheme: "convict"
191
+ }
192
+ },
193
+ getInitialLaunchUrl: async () => "convict://w/acme/does-not-exist"
194
+ });
195
+
196
+ const targetPath = await runtime.initialize();
197
+
198
+ assert.equal(targetPath, "/w/acme/does-not-exist");
199
+ assert.deepEqual(replaceCalls, ["/w/acme/does-not-exist"]);
200
+ });
201
+
202
+ test("registerMobileLaunchRouting no-ops when mobile config is disabled", async () => {
203
+ const replaceCalls = [];
204
+ const runtime = registerMobileLaunchRouting({
205
+ router: {
206
+ currentRoute: {
207
+ value: {
208
+ fullPath: "/home"
209
+ }
210
+ },
211
+ async replace(target) {
212
+ replaceCalls.push(target);
213
+ }
214
+ },
215
+ mobileConfig: {
216
+ enabled: false,
217
+ auth: {
218
+ customScheme: "convict"
219
+ }
220
+ },
221
+ getInitialLaunchUrl: async () => "convict://auth/login?code=abc",
222
+ subscribeToLaunchUrls() {
223
+ throw new Error("subscribeToLaunchUrls should not be called when mobile is disabled");
224
+ }
225
+ });
226
+
227
+ const targetPath = await runtime.initialize();
228
+ assert.equal(targetPath, "");
229
+ assert.deepEqual(replaceCalls, []);
230
+ });
@@ -121,6 +121,7 @@ async function bootstrapClientShellApp({
121
121
  debugEnvKey = "VITE_JSKIT_CLIENT_DEBUG",
122
122
  debugMessage = "Client modules bootstrapped before router install.",
123
123
  onAfterModulesBootstrapped = null,
124
+ onAfterRouterReady = null,
124
125
  mountSelector = "#app"
125
126
  } = {}) {
126
127
  if (typeof createApp !== "function") {
@@ -216,6 +217,20 @@ async function bootstrapClientShellApp({
216
217
  if (typeof router.isReady === "function") {
217
218
  await router.isReady();
218
219
  }
220
+ if (typeof onAfterRouterReady === "function") {
221
+ await onAfterRouterReady(
222
+ Object.freeze({
223
+ app,
224
+ router,
225
+ clientBootstrap,
226
+ surfaceRuntime,
227
+ surfaceMode,
228
+ env: isRecord(env) ? { ...env } : {},
229
+ logger: bootstrapLogger,
230
+ debugEnabled: isDebugEnabled
231
+ })
232
+ );
233
+ }
219
234
  app.mount(mountSelector);
220
235
 
221
236
  return Object.freeze({
@@ -168,6 +168,9 @@ test("bootstrapClientShellApp boots modules, reinstalls fallback route, and moun
168
168
  },
169
169
  onAfterModulesBootstrapped(context) {
170
170
  calls.push(`after:${context.clientBootstrap.routeCount}`);
171
+ },
172
+ onAfterRouterReady(context) {
173
+ calls.push(`router-ready:${context.clientBootstrap.routeCount}`);
171
174
  }
172
175
  });
173
176
 
@@ -185,4 +188,5 @@ test("bootstrapClientShellApp boots modules, reinstalls fallback route, and moun
185
188
  assert.equal(calls.includes("add:not-found"), true);
186
189
  assert.equal(calls.includes("isReady"), true);
187
190
  assert.equal(calls.includes("after:3"), true);
191
+ assert.equal(calls.includes("router-ready:3"), true);
188
192
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/kernel",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "json-rest-schema": "1.x.x"