@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.
- package/client/appConfig.js +16 -2
- package/client/appConfig.test.js +71 -0
- package/client/index.d.ts +24 -0
- package/client/index.js +2 -1
- package/client/mobileLaunchRouting.js +231 -0
- package/client/mobileLaunchRouting.test.js +230 -0
- package/client/shellBootstrap.js +15 -0
- package/client/shellBootstrap.test.js +4 -0
- package/package.json +1 -1
- package/server/http/lib/kernel.test.js +71 -0
- package/server/http/lib/routeRegistration.js +4 -3
- package/server/http/lib/router.js +28 -10
- package/server/support/appConfig.js +93 -2
- package/server/support/appConfig.test.js +102 -1
- package/server/support/appConfigFiles.js +26 -8
- package/server/support/appConfigFiles.test.js +25 -1
- package/server/support/index.js +2 -2
- package/shared/support/normalize.js +92 -0
- package/shared/support/normalize.test.js +112 -0
- package/test/barrelExposure.test.js +4 -0
|
@@ -506,6 +506,77 @@ test("registerHttpRuntime passes app context so request scope is available", asy
|
|
|
506
506
|
assert.equal(fastify.setErrorHandlerCalls, 1);
|
|
507
507
|
});
|
|
508
508
|
|
|
509
|
+
test("createRouter preserves explicit internal route flags", () => {
|
|
510
|
+
const router = createRouter();
|
|
511
|
+
|
|
512
|
+
router.get(
|
|
513
|
+
"/internal-only",
|
|
514
|
+
{
|
|
515
|
+
internal: true
|
|
516
|
+
},
|
|
517
|
+
async (_request, reply) => {
|
|
518
|
+
reply.code(204).send();
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const [route] = router.list();
|
|
523
|
+
assert.equal(route?.internal, true);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("registerHttpRuntime skips internal routes from public HTTP registration", () => {
|
|
527
|
+
const app = createApplication();
|
|
528
|
+
const fastify = createFastifyStub();
|
|
529
|
+
const router = createRouter();
|
|
530
|
+
|
|
531
|
+
router.get(
|
|
532
|
+
"/internal-only",
|
|
533
|
+
{
|
|
534
|
+
internal: true
|
|
535
|
+
},
|
|
536
|
+
async (_request, reply) => {
|
|
537
|
+
reply.code(200).send({ hidden: true });
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
router.get("/public-route", async (_request, reply) => {
|
|
541
|
+
reply.code(200).send({ ok: true });
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
app.instance("jskit.fastify", fastify);
|
|
545
|
+
app.instance("jskit.http.router", router);
|
|
546
|
+
|
|
547
|
+
const registration = registerHttpRuntime(app);
|
|
548
|
+
assert.equal(registration.routeCount, 1);
|
|
549
|
+
assert.equal(fastify.routes.length, 1);
|
|
550
|
+
assert.equal(fastify.routes[0].url, "/public-route");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("registerHttpRuntime skips internal routes generated through router apiResource helpers", () => {
|
|
554
|
+
const app = createApplication();
|
|
555
|
+
const fastify = createFastifyStub();
|
|
556
|
+
const router = createRouter();
|
|
557
|
+
|
|
558
|
+
router.apiResource(
|
|
559
|
+
"widgets",
|
|
560
|
+
{
|
|
561
|
+
index: async (_request, reply) => reply.code(200).send({ ok: true }),
|
|
562
|
+
store: async (_request, reply) => reply.code(201).send({ ok: true }),
|
|
563
|
+
show: async (_request, reply) => reply.code(200).send({ ok: true }),
|
|
564
|
+
update: async (_request, reply) => reply.code(200).send({ ok: true }),
|
|
565
|
+
destroy: async (_request, reply) => reply.code(204).send()
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
internal: true
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
app.instance("jskit.fastify", fastify);
|
|
573
|
+
app.instance("jskit.http.router", router);
|
|
574
|
+
|
|
575
|
+
const registration = registerHttpRuntime(app);
|
|
576
|
+
assert.equal(registration.routeCount, 0);
|
|
577
|
+
assert.equal(fastify.routes.length, 0);
|
|
578
|
+
});
|
|
579
|
+
|
|
509
580
|
test("registerHttpRuntime installs API error handling once by default", () => {
|
|
510
581
|
const app = createApplication();
|
|
511
582
|
const fastify = createFastifyStub();
|
|
@@ -310,15 +310,16 @@ function registerRoutes(
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
const normalizedRoutes = normalizeArray(routes);
|
|
313
|
+
const publicRoutes = normalizedRoutes.filter((route) => route?.internal !== true);
|
|
313
314
|
const policyApplier = typeof applyRoutePolicy === "function" ? applyRoutePolicy : defaultApplyRoutePolicy;
|
|
314
315
|
const fallbackHandler = typeof missingHandler === "function" ? missingHandler : defaultMissingHandler;
|
|
315
316
|
const runtimeMiddlewareConfig = normalizeRuntimeMiddlewareConfig(middleware);
|
|
316
317
|
|
|
317
|
-
if (
|
|
318
|
+
if (publicRoutes.some((route) => routeRequiresJsonApiContentTypeParser(route))) {
|
|
318
319
|
registerJsonApiContentTypeParser(fastify);
|
|
319
320
|
}
|
|
320
321
|
|
|
321
|
-
for (const route of
|
|
322
|
+
for (const route of publicRoutes) {
|
|
322
323
|
const routeTransport = normalizeRouteTransport(route?.transport, {
|
|
323
324
|
context: `Route ${String(route?.method || "<unknown>")} ${String(route?.path || "<unknown>")} transport`,
|
|
324
325
|
ErrorType: RouteRegistrationError
|
|
@@ -407,7 +408,7 @@ function registerRoutes(
|
|
|
407
408
|
}
|
|
408
409
|
|
|
409
410
|
return {
|
|
410
|
-
routeCount:
|
|
411
|
+
routeCount: publicRoutes.length
|
|
411
412
|
};
|
|
412
413
|
}
|
|
413
414
|
|
|
@@ -35,6 +35,18 @@ function normalizeRouterMiddlewareStack(value, { context = "middleware" } = {})
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function normalizeRouteInternal(value, { method = "", path = "" } = {}) {
|
|
39
|
+
if (value == null) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value !== "boolean") {
|
|
43
|
+
throw new RouteDefinitionError(
|
|
44
|
+
`Route ${String(method || "<unknown>")} ${String(path || "<unknown>")} internal must be a boolean.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
function normalizeRouteInput(method, path, optionsOrHandler, maybeHandler) {
|
|
39
51
|
const options =
|
|
40
52
|
typeof optionsOrHandler === "function"
|
|
@@ -108,6 +120,10 @@ class HttpRouter {
|
|
|
108
120
|
auth: resolvedOptions.auth,
|
|
109
121
|
contextPolicy: resolvedOptions.contextPolicy,
|
|
110
122
|
surface: resolvedOptions.surface,
|
|
123
|
+
internal: normalizeRouteInternal(resolvedOptions.internal, {
|
|
124
|
+
method: input.method,
|
|
125
|
+
path: input.path
|
|
126
|
+
}),
|
|
111
127
|
visibility: resolvedOptions.visibility,
|
|
112
128
|
permission: resolvedOptions.permission,
|
|
113
129
|
ownerParam: resolvedOptions.ownerParam,
|
|
@@ -186,9 +202,11 @@ class HttpRouter {
|
|
|
186
202
|
const itemPath = `/${resourceName}/:${idParam}`;
|
|
187
203
|
|
|
188
204
|
const methods = normalizeObject(controller);
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
}
|
|
205
|
+
const routeOptions = {
|
|
206
|
+
...normalizeObject(options)
|
|
207
|
+
};
|
|
208
|
+
delete routeOptions.idParam;
|
|
209
|
+
delete routeOptions.apiOnly;
|
|
192
210
|
|
|
193
211
|
const requireMethod = (methodName) => {
|
|
194
212
|
const handler = methods[methodName];
|
|
@@ -198,15 +216,15 @@ class HttpRouter {
|
|
|
198
216
|
return handler;
|
|
199
217
|
};
|
|
200
218
|
|
|
201
|
-
this.get(basePath,
|
|
219
|
+
this.get(basePath, routeOptions, requireMethod("index"));
|
|
202
220
|
if (!options.apiOnly) {
|
|
203
|
-
this.get(`${basePath}/create`,
|
|
204
|
-
this.get(`${itemPath}/edit`,
|
|
221
|
+
this.get(`${basePath}/create`, routeOptions, requireMethod("create"));
|
|
222
|
+
this.get(`${itemPath}/edit`, routeOptions, requireMethod("edit"));
|
|
205
223
|
}
|
|
206
|
-
this.post(basePath,
|
|
207
|
-
this.get(itemPath,
|
|
208
|
-
this.put(itemPath,
|
|
209
|
-
this.delete(itemPath,
|
|
224
|
+
this.post(basePath, routeOptions, requireMethod("store"));
|
|
225
|
+
this.get(itemPath, routeOptions, requireMethod("show"));
|
|
226
|
+
this.put(itemPath, routeOptions, requireMethod("update"));
|
|
227
|
+
this.delete(itemPath, routeOptions, requireMethod("destroy"));
|
|
210
228
|
}
|
|
211
229
|
|
|
212
230
|
list() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeObject } from "../../shared/support/normalize.js";
|
|
1
|
+
import { normalizeMobileConfig, normalizeObject, normalizeText } from "../../shared/support/normalize.js";
|
|
2
2
|
import { normalizeSurfaceId } from "../../shared/surface/registry.js";
|
|
3
3
|
|
|
4
4
|
function resolveAppConfig(scope = null) {
|
|
@@ -34,4 +34,95 @@ function resolveDefaultSurfaceId(scope = null, { defaultSurfaceId = "" } = {}) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function resolveMobileConfig(source = null) {
|
|
38
|
+
const appConfig =
|
|
39
|
+
source && typeof source === "object" && typeof source.has === "function" && typeof source.make === "function"
|
|
40
|
+
? resolveAppConfig(source)
|
|
41
|
+
: normalizeObject(source);
|
|
42
|
+
|
|
43
|
+
return normalizeMobileConfig(appConfig.mobile);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveClientAssetMode(source = null) {
|
|
47
|
+
return resolveMobileConfig(source).assetMode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildWebCallbackUrl(appPublicUrl = "", callbackPath = "") {
|
|
51
|
+
const normalizedAppPublicUrl = normalizeText(appPublicUrl);
|
|
52
|
+
const normalizedCallbackPath = normalizeText(callbackPath);
|
|
53
|
+
if (!normalizedAppPublicUrl || !normalizedCallbackPath) {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const baseUrl = new URL(normalizedAppPublicUrl);
|
|
59
|
+
if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!baseUrl.pathname.endsWith("/")) {
|
|
64
|
+
baseUrl.pathname = `${baseUrl.pathname}/`;
|
|
65
|
+
}
|
|
66
|
+
baseUrl.search = "";
|
|
67
|
+
baseUrl.hash = "";
|
|
68
|
+
const relativeCallbackPath = normalizedCallbackPath.startsWith("/")
|
|
69
|
+
? normalizedCallbackPath.slice(1)
|
|
70
|
+
: normalizedCallbackPath;
|
|
71
|
+
return new URL(relativeCallbackPath, baseUrl).toString();
|
|
72
|
+
} catch {
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildMobileCallbackUrl(customScheme = "", callbackPath = "") {
|
|
78
|
+
const normalizedScheme = normalizeText(customScheme).toLowerCase();
|
|
79
|
+
const normalizedCallbackPath = normalizeText(callbackPath);
|
|
80
|
+
if (!normalizedScheme || !normalizedCallbackPath) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const suffix = normalizedCallbackPath.startsWith("/") ? normalizedCallbackPath.slice(1) : normalizedCallbackPath;
|
|
85
|
+
return suffix ? `${normalizedScheme}://${suffix}` : `${normalizedScheme}://`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildAppLinkCallbackUrls(appLinkDomains = [], callbackPath = "") {
|
|
89
|
+
const normalizedCallbackPath = normalizeText(callbackPath);
|
|
90
|
+
if (!normalizedCallbackPath) {
|
|
91
|
+
return Object.freeze([]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const urls = (Array.isArray(appLinkDomains) ? appLinkDomains : [])
|
|
95
|
+
.map((entry) => normalizeText(entry).toLowerCase())
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map((entry) => `https://${entry}${normalizedCallbackPath}`);
|
|
98
|
+
|
|
99
|
+
return Object.freeze([...new Set(urls)]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveMobileCallbackUrls(source = null, { appPublicUrl = "" } = {}) {
|
|
103
|
+
const mobileConfig = resolveMobileConfig(source);
|
|
104
|
+
const callbackPath = mobileConfig.auth.callbackPath;
|
|
105
|
+
const webCallbackUrl = buildWebCallbackUrl(appPublicUrl, callbackPath);
|
|
106
|
+
const mobileCallbackUrl = buildMobileCallbackUrl(mobileConfig.auth.customScheme, callbackPath);
|
|
107
|
+
const appLinkCallbackUrls = buildAppLinkCallbackUrls(mobileConfig.auth.appLinkDomains, callbackPath);
|
|
108
|
+
const callbackUrls = Object.freeze(
|
|
109
|
+
[...new Set([webCallbackUrl, mobileCallbackUrl, ...appLinkCallbackUrls].filter(Boolean))]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
callbackPath,
|
|
114
|
+
webCallbackUrl,
|
|
115
|
+
mobileCallbackUrl,
|
|
116
|
+
appLinkCallbackUrls,
|
|
117
|
+
callbackUrls
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export {
|
|
122
|
+
resolveAppConfig,
|
|
123
|
+
normalizeDefaultSurfaceId,
|
|
124
|
+
resolveDefaultSurfaceId,
|
|
125
|
+
resolveMobileConfig,
|
|
126
|
+
resolveClientAssetMode,
|
|
127
|
+
resolveMobileCallbackUrls
|
|
128
|
+
};
|
|
@@ -2,8 +2,11 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
resolveAppConfig,
|
|
5
|
+
resolveClientAssetMode,
|
|
5
6
|
normalizeDefaultSurfaceId,
|
|
6
|
-
resolveDefaultSurfaceId
|
|
7
|
+
resolveDefaultSurfaceId,
|
|
8
|
+
resolveMobileCallbackUrls,
|
|
9
|
+
resolveMobileConfig
|
|
7
10
|
} from "./appConfig.js";
|
|
8
11
|
|
|
9
12
|
test("resolveAppConfig returns normalized appConfig when scope exposes appConfig binding", () => {
|
|
@@ -92,3 +95,101 @@ test("resolveDefaultSurfaceId falls back to appConfig and then empty default", (
|
|
|
92
95
|
});
|
|
93
96
|
assert.equal(fromKernelFallback, "");
|
|
94
97
|
});
|
|
98
|
+
|
|
99
|
+
test("resolveMobileConfig reads the normalized mobile config from raw appConfig or scope", () => {
|
|
100
|
+
const fromRaw = resolveMobileConfig({
|
|
101
|
+
mobile: {
|
|
102
|
+
enabled: true,
|
|
103
|
+
strategy: "capacitor",
|
|
104
|
+
assetMode: "dev_server",
|
|
105
|
+
auth: {
|
|
106
|
+
customScheme: "convict"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.equal(fromRaw.enabled, true);
|
|
112
|
+
assert.equal(fromRaw.strategy, "capacitor");
|
|
113
|
+
assert.equal(fromRaw.assetMode, "dev_server");
|
|
114
|
+
assert.equal(fromRaw.auth.customScheme, "convict");
|
|
115
|
+
|
|
116
|
+
const fromScope = resolveMobileConfig({
|
|
117
|
+
has(token) {
|
|
118
|
+
return token === "appConfig";
|
|
119
|
+
},
|
|
120
|
+
make() {
|
|
121
|
+
return {
|
|
122
|
+
mobile: {
|
|
123
|
+
enabled: true,
|
|
124
|
+
strategy: "capacitor"
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
assert.equal(fromScope.enabled, true);
|
|
130
|
+
assert.equal(fromScope.strategy, "capacitor");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("resolveClientAssetMode returns the normalized mobile asset mode", () => {
|
|
134
|
+
assert.equal(
|
|
135
|
+
resolveClientAssetMode({
|
|
136
|
+
mobile: {
|
|
137
|
+
assetMode: "dev_server"
|
|
138
|
+
}
|
|
139
|
+
}),
|
|
140
|
+
"dev_server"
|
|
141
|
+
);
|
|
142
|
+
assert.equal(resolveClientAssetMode({}), "bundled");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("resolveMobileCallbackUrls resolves web, mobile, and app-link callback URLs", () => {
|
|
146
|
+
const callbackUrls = resolveMobileCallbackUrls(
|
|
147
|
+
{
|
|
148
|
+
mobile: {
|
|
149
|
+
auth: {
|
|
150
|
+
callbackPath: "/auth/login",
|
|
151
|
+
customScheme: "convict",
|
|
152
|
+
appLinkDomains: ["app.example.com", "APP.EXAMPLE.COM"]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
appPublicUrl: "https://example.com/app"
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(callbackUrls, {
|
|
162
|
+
callbackPath: "/auth/login",
|
|
163
|
+
webCallbackUrl: "https://example.com/app/auth/login",
|
|
164
|
+
mobileCallbackUrl: "convict://auth/login",
|
|
165
|
+
appLinkCallbackUrls: ["https://app.example.com/auth/login"],
|
|
166
|
+
callbackUrls: [
|
|
167
|
+
"https://example.com/app/auth/login",
|
|
168
|
+
"convict://auth/login",
|
|
169
|
+
"https://app.example.com/auth/login"
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("resolveMobileCallbackUrls omits invalid or unavailable callback targets", () => {
|
|
175
|
+
const callbackUrls = resolveMobileCallbackUrls(
|
|
176
|
+
{
|
|
177
|
+
mobile: {
|
|
178
|
+
auth: {
|
|
179
|
+
callbackPath: "/auth/login"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
appPublicUrl: "notaurl"
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
assert.deepEqual(callbackUrls, {
|
|
189
|
+
callbackPath: "/auth/login",
|
|
190
|
+
webCallbackUrl: "",
|
|
191
|
+
mobileCallbackUrl: "",
|
|
192
|
+
appLinkCallbackUrls: [],
|
|
193
|
+
callbackUrls: []
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -39,17 +39,20 @@ async function loadConfigModuleAtPath(absolutePath) {
|
|
|
39
39
|
return normalizeConfigObject(loadedModule?.config);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async function
|
|
43
|
-
|
|
42
|
+
async function loadAppConfigFromAppRoot({
|
|
43
|
+
appRoot = "",
|
|
44
44
|
publicConfigRelativePath = PUBLIC_CONFIG_RELATIVE_PATH,
|
|
45
45
|
serverConfigRelativePath = SERVER_CONFIG_RELATIVE_PATH
|
|
46
46
|
} = {}) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const normalizedAppRootInput = String(appRoot || "").trim();
|
|
48
|
+
if (!normalizedAppRootInput) {
|
|
49
|
+
throw new Error("loadAppConfigFromAppRoot requires appRoot.");
|
|
50
|
+
}
|
|
51
|
+
const normalizedAppRoot = path.resolve(normalizedAppRootInput);
|
|
52
|
+
|
|
50
53
|
const [publicConfig, serverConfig] = await Promise.all([
|
|
51
|
-
loadConfigModuleAtPath(path.join(
|
|
52
|
-
loadConfigModuleAtPath(path.join(
|
|
54
|
+
loadConfigModuleAtPath(path.join(normalizedAppRoot, publicConfigRelativePath)),
|
|
55
|
+
loadConfigModuleAtPath(path.join(normalizedAppRoot, serverConfigRelativePath))
|
|
53
56
|
]);
|
|
54
57
|
|
|
55
58
|
return Object.freeze({
|
|
@@ -58,4 +61,19 @@ async function loadAppConfigFromModuleUrl({
|
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
async function loadAppConfigFromModuleUrl({
|
|
65
|
+
moduleUrl = import.meta.url,
|
|
66
|
+
publicConfigRelativePath = PUBLIC_CONFIG_RELATIVE_PATH,
|
|
67
|
+
serverConfigRelativePath = SERVER_CONFIG_RELATIVE_PATH
|
|
68
|
+
} = {}) {
|
|
69
|
+
const appRoot = await resolveAppRootFromModuleUrl(moduleUrl, {
|
|
70
|
+
publicConfigRelativePath
|
|
71
|
+
});
|
|
72
|
+
return loadAppConfigFromAppRoot({
|
|
73
|
+
appRoot,
|
|
74
|
+
publicConfigRelativePath,
|
|
75
|
+
serverConfigRelativePath
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { loadAppConfigFromAppRoot, loadAppConfigFromModuleUrl };
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
|
|
7
|
+
import { loadAppConfigFromAppRoot, loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
|
|
8
8
|
|
|
9
9
|
async function createModuleUrlAt(absolutePath) {
|
|
10
10
|
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
@@ -30,6 +30,30 @@ test("loadAppConfigFromModuleUrl merges public and server config", async () => {
|
|
|
30
30
|
assert.equal(Object.isFrozen(loaded), true);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
test("loadAppConfigFromAppRoot merges public and server config", async () => {
|
|
34
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "kernel-app-config-"));
|
|
35
|
+
const appRoot = path.join(tempRoot, "app");
|
|
36
|
+
await mkdir(path.join(appRoot, "config"), { recursive: true });
|
|
37
|
+
await writeFile(path.join(appRoot, "config", "public.js"), "export const config = { a: 1, shared: 'public' };", "utf8");
|
|
38
|
+
await writeFile(path.join(appRoot, "config", "server.js"), "export const config = { b: 2, shared: 'server' };", "utf8");
|
|
39
|
+
|
|
40
|
+
const loaded = await loadAppConfigFromAppRoot({ appRoot });
|
|
41
|
+
|
|
42
|
+
assert.deepEqual(loaded, {
|
|
43
|
+
a: 1,
|
|
44
|
+
b: 2,
|
|
45
|
+
shared: "server"
|
|
46
|
+
});
|
|
47
|
+
assert.equal(Object.isFrozen(loaded), true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("loadAppConfigFromAppRoot requires an explicit appRoot", async () => {
|
|
51
|
+
await assert.rejects(
|
|
52
|
+
loadAppConfigFromAppRoot({ appRoot: "" }),
|
|
53
|
+
/requires appRoot/
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
33
57
|
test("loadAppConfigFromModuleUrl tolerates missing server config", async () => {
|
|
34
58
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "kernel-app-config-"));
|
|
35
59
|
const appRoot = path.join(tempRoot, "app");
|
package/server/support/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { symlinkSafeRequire } from "./symlinkSafeRequire.js";
|
|
2
|
-
export { resolveAppConfig } from "./appConfig.js";
|
|
3
|
-
export { loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
|
|
2
|
+
export { resolveAppConfig, resolveMobileConfig, resolveClientAssetMode, resolveMobileCallbackUrls } from "./appConfig.js";
|
|
3
|
+
export { loadAppConfigFromAppRoot, loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
|
|
4
4
|
export { importFreshModuleFromAbsolutePath } from "./importFreshModuleFromAbsolutePath.js";
|
|
5
5
|
export { resolveRequiredAppRoot, toPosixPath } from "./path.js";
|
|
6
6
|
export {
|
|
@@ -254,6 +254,94 @@ function normalizeOneOf(value, allowedValues = [], fallback = "") {
|
|
|
254
254
|
return supported[0] || "";
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
const MOBILE_ASSET_MODES = Object.freeze(["bundled", "dev_server"]);
|
|
258
|
+
const MOBILE_STRATEGIES = Object.freeze(["capacitor"]);
|
|
259
|
+
const DEFAULT_MOBILE_AUTH_CONFIG = Object.freeze({
|
|
260
|
+
callbackPath: "/auth/login",
|
|
261
|
+
customScheme: "",
|
|
262
|
+
appLinkDomains: Object.freeze([])
|
|
263
|
+
});
|
|
264
|
+
const DEFAULT_MOBILE_ANDROID_CONFIG = Object.freeze({
|
|
265
|
+
packageName: "",
|
|
266
|
+
minSdk: 26,
|
|
267
|
+
targetSdk: 35,
|
|
268
|
+
versionCode: 1,
|
|
269
|
+
versionName: "1.0.0"
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
function normalizeMobileStrategy(value = "", { fallback = "" } = {}) {
|
|
273
|
+
const normalized = normalizeLowerText(value);
|
|
274
|
+
return MOBILE_STRATEGIES.includes(normalized) ? normalized : normalizeLowerText(fallback);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeMobileAssetMode(value = "", { fallback = "bundled" } = {}) {
|
|
278
|
+
const normalized = normalizeLowerText(value);
|
|
279
|
+
if (!normalized) {
|
|
280
|
+
return normalizeLowerText(fallback, {
|
|
281
|
+
fallback: "bundled"
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (!MOBILE_ASSET_MODES.includes(normalized)) {
|
|
285
|
+
throw new TypeError('config.mobile.assetMode must be "bundled" or "dev_server".');
|
|
286
|
+
}
|
|
287
|
+
return normalized;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizeMobileCallbackPath(value = "", { fallback = "/auth/login" } = {}) {
|
|
291
|
+
const normalized = normalizeText(value, {
|
|
292
|
+
fallback
|
|
293
|
+
});
|
|
294
|
+
if (!normalized) {
|
|
295
|
+
return "";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function normalizeMobileConfig(source = {}) {
|
|
302
|
+
const mobile = normalizeObject(source);
|
|
303
|
+
const auth = normalizeObject(mobile.auth);
|
|
304
|
+
const android = normalizeObject(mobile.android);
|
|
305
|
+
|
|
306
|
+
return Object.freeze({
|
|
307
|
+
enabled: hasValue(mobile.enabled) ? normalizeBoolean(mobile.enabled) : false,
|
|
308
|
+
strategy: normalizeMobileStrategy(mobile.strategy),
|
|
309
|
+
appId: normalizeText(mobile.appId),
|
|
310
|
+
appName: normalizeText(mobile.appName),
|
|
311
|
+
assetMode: normalizeMobileAssetMode(mobile.assetMode),
|
|
312
|
+
devServerUrl: normalizeText(mobile.devServerUrl),
|
|
313
|
+
apiBaseUrl: normalizeText(mobile.apiBaseUrl),
|
|
314
|
+
auth: Object.freeze({
|
|
315
|
+
callbackPath: normalizeMobileCallbackPath(auth.callbackPath, {
|
|
316
|
+
fallback: DEFAULT_MOBILE_AUTH_CONFIG.callbackPath
|
|
317
|
+
}),
|
|
318
|
+
customScheme: normalizeLowerText(auth.customScheme),
|
|
319
|
+
appLinkDomains: Object.freeze([
|
|
320
|
+
...new Set(
|
|
321
|
+
normalizeUniqueTextList(auth.appLinkDomains, {
|
|
322
|
+
acceptSingle: true
|
|
323
|
+
}).map((entry) => normalizeLowerText(entry))
|
|
324
|
+
)
|
|
325
|
+
])
|
|
326
|
+
}),
|
|
327
|
+
android: Object.freeze({
|
|
328
|
+
packageName: normalizeText(android.packageName),
|
|
329
|
+
minSdk: normalizePositiveInteger(android.minSdk, {
|
|
330
|
+
fallback: DEFAULT_MOBILE_ANDROID_CONFIG.minSdk
|
|
331
|
+
}),
|
|
332
|
+
targetSdk: normalizePositiveInteger(android.targetSdk, {
|
|
333
|
+
fallback: DEFAULT_MOBILE_ANDROID_CONFIG.targetSdk
|
|
334
|
+
}),
|
|
335
|
+
versionCode: normalizePositiveInteger(android.versionCode, {
|
|
336
|
+
fallback: DEFAULT_MOBILE_ANDROID_CONFIG.versionCode
|
|
337
|
+
}),
|
|
338
|
+
versionName: normalizeText(android.versionName, {
|
|
339
|
+
fallback: DEFAULT_MOBILE_ANDROID_CONFIG.versionName
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
257
345
|
function ensureNonEmptyText(value, label = "value") {
|
|
258
346
|
const normalized = normalizeText(value);
|
|
259
347
|
if (!normalized) {
|
|
@@ -283,5 +371,9 @@ export {
|
|
|
283
371
|
normalizeRecordId,
|
|
284
372
|
normalizeOpaqueId,
|
|
285
373
|
normalizeOneOf,
|
|
374
|
+
normalizeMobileAssetMode,
|
|
375
|
+
normalizeMobileCallbackPath,
|
|
376
|
+
normalizeMobileConfig,
|
|
377
|
+
normalizeMobileStrategy,
|
|
286
378
|
ensureNonEmptyText
|
|
287
379
|
};
|