@jskit-ai/kernel 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +24 -0
  2. package/_testable/index.js +4 -0
  3. package/client/appConfig.js +33 -0
  4. package/client/componentInteraction.js +51 -0
  5. package/client/componentInteraction.test.js +111 -0
  6. package/client/descriptorSections.js +75 -0
  7. package/client/index.d.ts +70 -0
  8. package/client/index.js +3 -0
  9. package/client/logging.js +38 -0
  10. package/client/moduleBootstrap.js +670 -0
  11. package/client/moduleBootstrap.test.js +403 -0
  12. package/client/shellBootstrap.js +233 -0
  13. package/client/shellBootstrap.test.js +185 -0
  14. package/client/shellRouting.js +321 -0
  15. package/client/shellRouting.test.js +113 -0
  16. package/client/vite/clientBootstrapPlugin.js +259 -0
  17. package/client/vite/clientBootstrapPlugin.test.js +563 -0
  18. package/client/vite/index.js +3 -0
  19. package/internal/node/fileSystem.js +21 -0
  20. package/internal/node/installedPackageDescriptor.js +104 -0
  21. package/package.json +43 -0
  22. package/server/actions/ActionRuntimeServiceProvider.js +309 -0
  23. package/server/actions/ActionRuntimeServiceProvider.test.js +551 -0
  24. package/server/actions/index.js +8 -0
  25. package/server/container/ContainerCoreServiceProvider.js +27 -0
  26. package/server/container/index.js +10 -0
  27. package/server/exportPolicy.test.js +68 -0
  28. package/server/http/HttpFastifyServiceProvider.js +25 -0
  29. package/server/http/_testable/index.js +2 -0
  30. package/server/http/index.js +1 -0
  31. package/server/http/lib/controller.js +183 -0
  32. package/server/http/lib/controller.test.js +143 -0
  33. package/server/http/lib/errors.js +12 -0
  34. package/server/http/lib/httpRuntime.js +82 -0
  35. package/server/http/lib/index.js +18 -0
  36. package/server/http/lib/kernel.js +15 -0
  37. package/server/http/lib/kernel.test.js +880 -0
  38. package/server/http/lib/middlewareRuntime.js +149 -0
  39. package/server/http/lib/requestActionExecutor.js +258 -0
  40. package/server/http/lib/requestScope.js +59 -0
  41. package/server/http/lib/routeRegistration.js +165 -0
  42. package/server/http/lib/routeSupport.js +45 -0
  43. package/server/http/lib/routeValidator.js +469 -0
  44. package/server/http/lib/routeValidator.test.js +474 -0
  45. package/server/http/lib/router.js +206 -0
  46. package/server/kernel/KernelCoreServiceProvider.js +27 -0
  47. package/server/kernel/index.js +10 -0
  48. package/server/platform/PlatformServerRuntimeServiceProvider.js +30 -0
  49. package/server/platform/index.js +5 -0
  50. package/server/platform/providerRuntime/descriptorCatalog.js +170 -0
  51. package/server/platform/providerRuntime/helpers.js +45 -0
  52. package/server/platform/providerRuntime/lockfile.js +27 -0
  53. package/server/platform/providerRuntime/providerLoader.js +283 -0
  54. package/server/platform/providerRuntime.js +142 -0
  55. package/server/platform/providerRuntime.test.js +217 -0
  56. package/server/platform/runtime.js +40 -0
  57. package/server/platform/surfaceRuntime.js +150 -0
  58. package/server/platform/surfaceRuntime.test.js +136 -0
  59. package/server/registries/actionSurfaceSourceRegistry.js +150 -0
  60. package/server/registries/bootstrapPayloadContributorRegistry.js +41 -0
  61. package/server/registries/domainEventListenerRegistry.js +61 -0
  62. package/server/registries/index.js +36 -0
  63. package/server/registries/primitives.js +63 -0
  64. package/server/registries/routeVisibilityResolverRegistry.js +87 -0
  65. package/server/registries/serviceRegistrationRegistry.js +431 -0
  66. package/server/runtime/ServerRuntimeCoreServiceProvider.js +65 -0
  67. package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +53 -0
  68. package/server/runtime/apiRoutePolicyParity.test.js +109 -0
  69. package/server/runtime/apiRouteRegistration.js +65 -0
  70. package/server/runtime/bootBootstrapRoutes.js +46 -0
  71. package/server/runtime/bootBootstrapRoutes.test.js +79 -0
  72. package/server/runtime/bootstrapContributors.test.js +114 -0
  73. package/server/runtime/canonicalJson.js +74 -0
  74. package/server/runtime/composition.js +142 -0
  75. package/server/runtime/domainEvents.test.js +114 -0
  76. package/server/runtime/domainRules.js +50 -0
  77. package/server/runtime/domainRules.test.js +87 -0
  78. package/server/runtime/entityChangeEvents.js +182 -0
  79. package/server/runtime/entityChangeEvents.test.js +211 -0
  80. package/server/runtime/errors.js +68 -0
  81. package/server/runtime/errors.test.js +73 -0
  82. package/server/runtime/fastifyBootstrap.js +372 -0
  83. package/server/runtime/fastifyBootstrap.test.js +194 -0
  84. package/server/runtime/index.js +6 -0
  85. package/server/runtime/integers.js +13 -0
  86. package/server/runtime/moduleConfig.js +269 -0
  87. package/server/runtime/moduleConfig.test.js +141 -0
  88. package/server/runtime/pagination.js +13 -0
  89. package/server/runtime/realtimeNormalization.js +21 -0
  90. package/server/runtime/requestUrl.js +38 -0
  91. package/server/runtime/routeUtils.js +20 -0
  92. package/server/runtime/runtimeAssembly.js +113 -0
  93. package/server/runtime/runtimeKernel.js +55 -0
  94. package/server/runtime/securityAudit.js +269 -0
  95. package/server/runtime/securityAudit.test.js +41 -0
  96. package/server/runtime/serviceAuthorization.js +113 -0
  97. package/server/runtime/serviceAuthorization.test.js +100 -0
  98. package/server/runtime/serviceRegistration.test.js +197 -0
  99. package/server/support/SupportCoreServiceProvider.js +25 -0
  100. package/server/support/appConfig.js +37 -0
  101. package/server/support/appConfig.test.js +94 -0
  102. package/server/support/defaultMissingHandler.js +7 -0
  103. package/server/support/index.js +2 -0
  104. package/server/support/routePolicyConfig.js +51 -0
  105. package/server/support/symlinkSafeRequire.js +78 -0
  106. package/server/support/symlinkSafeRequire.test.js +27 -0
  107. package/server/surface/SurfaceRoutingServiceProvider.js +27 -0
  108. package/server/surface/index.js +19 -0
  109. package/shared/actions/actionContributorHelpers.js +34 -0
  110. package/shared/actions/actionContributorHelpers.test.js +16 -0
  111. package/shared/actions/actionDefinitions.js +488 -0
  112. package/shared/actions/actionDefinitions.test.js +212 -0
  113. package/shared/actions/audit.js +7 -0
  114. package/shared/actions/executionContext.js +97 -0
  115. package/shared/actions/executionContext.test.js +66 -0
  116. package/shared/actions/idempotency.js +62 -0
  117. package/shared/actions/index.js +2 -0
  118. package/shared/actions/observability.js +10 -0
  119. package/shared/actions/pipeline.js +287 -0
  120. package/shared/actions/policies.js +342 -0
  121. package/shared/actions/policies.test.js +233 -0
  122. package/shared/actions/registry.js +187 -0
  123. package/shared/actions/registry.test.js +381 -0
  124. package/shared/actions/requestMeta.js +36 -0
  125. package/shared/actions/textNormalization.js +3 -0
  126. package/shared/actions/withActionDefaults.js +34 -0
  127. package/shared/index.js +2 -0
  128. package/shared/runtime/application.js +323 -0
  129. package/shared/runtime/container.js +261 -0
  130. package/shared/runtime/containerErrors.js +22 -0
  131. package/shared/runtime/index.js +18 -0
  132. package/shared/runtime/kernelErrors.js +20 -0
  133. package/shared/runtime/serviceProvider.js +13 -0
  134. package/shared/support/formatDateTime.js +10 -0
  135. package/shared/support/formatDateTime.test.js +15 -0
  136. package/shared/support/index.js +14 -0
  137. package/shared/support/linkPath.js +67 -0
  138. package/shared/support/linkPath.test.js +35 -0
  139. package/shared/support/normalize.js +116 -0
  140. package/shared/support/normalize.test.js +48 -0
  141. package/shared/support/packageDescriptor.test.js +121 -0
  142. package/shared/support/permissions.js +50 -0
  143. package/shared/support/pickOwnProperties.js +17 -0
  144. package/shared/support/pickOwnProperties.test.js +25 -0
  145. package/shared/support/policies.js +11 -0
  146. package/shared/support/queryPath.js +33 -0
  147. package/shared/support/queryPath.test.js +19 -0
  148. package/shared/support/queryResilience.js +34 -0
  149. package/shared/support/queryResilience.test.js +33 -0
  150. package/shared/support/returnToPath.js +153 -0
  151. package/shared/support/returnToPath.test.js +123 -0
  152. package/shared/support/sorting.js +15 -0
  153. package/shared/support/tokens.js +23 -0
  154. package/shared/support/tokens.test.js +17 -0
  155. package/shared/support/visibility.js +56 -0
  156. package/shared/support/visibility.test.js +45 -0
  157. package/shared/surface/apiPaths.js +84 -0
  158. package/shared/surface/escapeRegExp.js +5 -0
  159. package/shared/surface/index.js +6 -0
  160. package/shared/surface/paths.js +273 -0
  161. package/shared/surface/registry.js +135 -0
  162. package/shared/surface/registry.test.js +44 -0
  163. package/shared/surface/runtime.js +357 -0
  164. package/shared/surface/runtime.test.js +319 -0
  165. package/shared/validators/createCursorListValidator.js +42 -0
  166. package/shared/validators/createCursorListValidator.test.js +34 -0
  167. package/shared/validators/cursorPaginationQueryValidator.js +31 -0
  168. package/shared/validators/cursorPaginationQueryValidator.test.js +21 -0
  169. package/shared/validators/index.js +12 -0
  170. package/shared/validators/inputNormalization.js +13 -0
  171. package/shared/validators/mergeObjectSchemas.js +31 -0
  172. package/shared/validators/mergeObjectSchemas.test.js +67 -0
  173. package/shared/validators/mergeValidators.js +89 -0
  174. package/shared/validators/mergeValidators.test.js +116 -0
  175. package/shared/validators/nestValidator.js +53 -0
  176. package/shared/validators/nestValidator.test.js +60 -0
  177. package/shared/validators/recordIdParamsValidator.js +36 -0
  178. package/shared/validators/recordIdParamsValidator.test.js +20 -0
  179. package/shared/validators/resourceRequiredMetadata.js +41 -0
  180. package/shared/validators/resourceRequiredMetadata.test.js +49 -0
  181. package/test/barrelExposure.test.js +106 -0
  182. package/test/dynamicImportPolicy.test.js +89 -0
  183. package/test/exportsContract.test.js +168 -0
  184. package/test/routeInputContractGuard.test.js +78 -0
  185. package/test/surfaceIndependence.test.js +109 -0
@@ -0,0 +1,357 @@
1
+ import { createSurfacePathHelpers } from "./paths.js";
2
+ import {
3
+ createSurfaceRegistry,
4
+ deriveSurfaceRouteBaseFromPagesRoot,
5
+ normalizeSurfaceId,
6
+ normalizeSurfacePagesRoot
7
+ } from "./registry.js";
8
+ import { isRecord } from "../support/normalize.js";
9
+
10
+ function uniqueSurfaceIds(ids) {
11
+ const seen = new Set();
12
+ const ordered = [];
13
+ for (const id of ids) {
14
+ const normalizedId = normalizeSurfaceId(id);
15
+ if (!normalizedId || seen.has(normalizedId)) {
16
+ continue;
17
+ }
18
+ seen.add(normalizedId);
19
+ ordered.push(normalizedId);
20
+ }
21
+ return ordered;
22
+ }
23
+
24
+ function resolveSurfaceIds({ surfaces = {} } = {}) {
25
+ const surfaceCandidates = [];
26
+ for (const [key, value] of Object.entries(isRecord(surfaces) ? surfaces : {})) {
27
+ const record = isRecord(value) ? value : {};
28
+ surfaceCandidates.push(record.id || key);
29
+ }
30
+
31
+ const fromSurfaces = uniqueSurfaceIds(surfaceCandidates);
32
+ if (fromSurfaces.length > 0) {
33
+ return fromSurfaces;
34
+ }
35
+
36
+ throw new Error("createSurfaceRuntime requires at least one surface id.");
37
+ }
38
+
39
+ function normalizeRoutePath(pathLike = "") {
40
+ const rawPath = String(pathLike || "").trim();
41
+ if (!rawPath || rawPath === "/") {
42
+ return "/";
43
+ }
44
+
45
+ const withLeadingSlash = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
46
+ const compacted = withLeadingSlash.replace(/\/{2,}/g, "/");
47
+ if (compacted === "/") {
48
+ return "/";
49
+ }
50
+ return compacted.replace(/\/+$/, "") || "/";
51
+ }
52
+
53
+ function resolveAbsoluteRoutePath(parentAbsolutePath = "/", routePath = "") {
54
+ const normalizedParentPath = normalizeRoutePath(parentAbsolutePath);
55
+ const rawRoutePath = String(routePath || "").trim();
56
+ if (!rawRoutePath) {
57
+ return normalizedParentPath;
58
+ }
59
+ if (rawRoutePath.startsWith("/")) {
60
+ return normalizeRoutePath(rawRoutePath);
61
+ }
62
+ if (normalizedParentPath === "/") {
63
+ return normalizeRoutePath(`/${rawRoutePath}`);
64
+ }
65
+ return normalizeRoutePath(`${normalizedParentPath}/${rawRoutePath}`);
66
+ }
67
+
68
+ function readRouteJskitMeta(route) {
69
+ if (!isRecord(route) || !isRecord(route.meta) || !isRecord(route.meta.jskit)) {
70
+ return {};
71
+ }
72
+ return route.meta.jskit;
73
+ }
74
+
75
+ function resolveRouteExplicitSurface(route) {
76
+ const metaSurface = readRouteJskitMeta(route).surface;
77
+ return normalizeSurfaceId(route?.surface || metaSurface);
78
+ }
79
+
80
+ function resolveOwnRouteScope(route) {
81
+ const metaScope = readRouteJskitMeta(route).scope;
82
+ const normalizedScope = String(route?.scope || metaScope || "")
83
+ .trim()
84
+ .toLowerCase();
85
+ if (!normalizedScope) {
86
+ return "";
87
+ }
88
+ return normalizedScope === "global" ? "global" : "surface";
89
+ }
90
+
91
+ function resolveRouteSurface(route, inheritedSurfaceId = "", { surfaceRuntime, absolutePath = "/" } = {}) {
92
+ const explicitSurface = resolveRouteExplicitSurface(route);
93
+ if (explicitSurface) {
94
+ return explicitSurface;
95
+ }
96
+
97
+ const normalizedInheritedSurface = normalizeSurfaceId(inheritedSurfaceId);
98
+ if (normalizedInheritedSurface) {
99
+ return normalizedInheritedSurface;
100
+ }
101
+
102
+ const routePath = String(route?.path || "").trim();
103
+ if (routePath && routePath.startsWith("/")) {
104
+ return surfaceRuntime.resolveSurfaceFromPathname(routePath);
105
+ }
106
+
107
+ if (absolutePath) {
108
+ return surfaceRuntime.resolveSurfaceFromPathname(absolutePath);
109
+ }
110
+
111
+ return surfaceRuntime.DEFAULT_SURFACE_ID;
112
+ }
113
+
114
+ function normalizeRoutesBySurfaceBoundaries(routeList = [], { surfaceRuntime } = {}) {
115
+ const normalizedRoutes = Array.isArray(routeList) ? routeList : [];
116
+ const hoistedRoutes = [];
117
+
118
+ function cloneRouteWithChildren(route, children) {
119
+ const nextRoute = {
120
+ ...route
121
+ };
122
+ if (children.length > 0) {
123
+ nextRoute.children = children;
124
+ } else if (Object.prototype.hasOwnProperty.call(nextRoute, "children")) {
125
+ delete nextRoute.children;
126
+ }
127
+ return nextRoute;
128
+ }
129
+
130
+ function cloneRouteAsHoisted(route, absolutePath, children) {
131
+ const nextRoute = cloneRouteWithChildren(route, children);
132
+ nextRoute.path = absolutePath;
133
+ return nextRoute;
134
+ }
135
+
136
+ function visitRoute(route, parentSurfaceId = "", parentAbsolutePath = "/") {
137
+ if (!isRecord(route)) {
138
+ return null;
139
+ }
140
+
141
+ const absolutePath = resolveAbsoluteRoutePath(parentAbsolutePath, route.path);
142
+ const explicitSurface = resolveRouteExplicitSurface(route);
143
+ const normalizedParentSurfaceId = normalizeSurfaceId(parentSurfaceId);
144
+ const resolvedSurface = resolveRouteSurface(route, normalizedParentSurfaceId, {
145
+ surfaceRuntime,
146
+ absolutePath
147
+ });
148
+ const surfaceBoundary =
149
+ Boolean(explicitSurface) &&
150
+ Boolean(normalizedParentSurfaceId) &&
151
+ explicitSurface !== normalizedParentSurfaceId;
152
+
153
+ const children = Array.isArray(route.children) ? route.children : [];
154
+ const filteredChildren = [];
155
+ for (const child of children) {
156
+ const normalizedChild = visitRoute(child, resolvedSurface, absolutePath);
157
+ if (normalizedChild) {
158
+ filteredChildren.push(normalizedChild);
159
+ }
160
+ }
161
+
162
+ const shouldCloneForChildren =
163
+ children.length !== filteredChildren.length ||
164
+ filteredChildren.some((child, index) => child !== children[index]);
165
+ const routeWithChildren = shouldCloneForChildren ? cloneRouteWithChildren(route, filteredChildren) : route;
166
+
167
+ if (!surfaceBoundary) {
168
+ return routeWithChildren;
169
+ }
170
+
171
+ hoistedRoutes.push(cloneRouteAsHoisted(routeWithChildren, absolutePath, filteredChildren));
172
+ return null;
173
+ }
174
+
175
+ const keptRoutes = [];
176
+ for (const route of normalizedRoutes) {
177
+ const normalizedRoute = visitRoute(route, "", "/");
178
+ if (normalizedRoute) {
179
+ keptRoutes.push(normalizedRoute);
180
+ }
181
+ }
182
+
183
+ return [...keptRoutes, ...hoistedRoutes];
184
+ }
185
+
186
+
187
+ function createSurfaceRuntime(options = {}) {
188
+ const allMode = normalizeSurfaceId(options?.allMode || "all") || "all";
189
+ const sourceSurfaces = isRecord(options?.surfaces) ? options.surfaces : {};
190
+ const surfaceIds = resolveSurfaceIds({
191
+ surfaces: sourceSurfaces
192
+ });
193
+
194
+ const normalizedSurfaces = {};
195
+ for (const surfaceId of surfaceIds) {
196
+ const source = isRecord(sourceSurfaces[surfaceId]) ? sourceSurfaces[surfaceId] : {};
197
+ if (!Object.prototype.hasOwnProperty.call(source, "pagesRoot")) {
198
+ throw new Error(`Surface "${surfaceId}" requires pagesRoot (use "" for root).`);
199
+ }
200
+ const pagesRoot = normalizeSurfacePagesRoot(source.pagesRoot);
201
+ normalizedSurfaces[surfaceId] = {
202
+ ...source,
203
+ id: surfaceId,
204
+ pagesRoot,
205
+ routeBase: deriveSurfaceRouteBaseFromPagesRoot(pagesRoot),
206
+ enabled: source.enabled !== false
207
+ };
208
+ }
209
+
210
+ const defaultSurfaceId = normalizeSurfaceId(options?.defaultSurfaceId) || surfaceIds[0];
211
+ const registry = createSurfaceRegistry({
212
+ surfaces: normalizedSurfaces,
213
+ defaultSurfaceId
214
+ });
215
+
216
+ const pathHelpers = createSurfacePathHelpers({
217
+ apiBasePath: String(options?.apiBasePath || "/api"),
218
+ defaultSurfaceId: registry.DEFAULT_SURFACE_ID,
219
+ normalizeSurfaceId: registry.normalizeSurfaceId,
220
+ resolveSurfaceRouteBase: registry.resolveSurfaceRouteBase,
221
+ listSurfaceDefinitions: registry.listSurfaceDefinitions,
222
+ routes: options?.routes
223
+ });
224
+
225
+ const enabledSurfaceIds = surfaceIds.filter((surfaceId) => normalizedSurfaces[surfaceId]?.enabled !== false);
226
+ const defaultSurfaceSource = isRecord(normalizedSurfaces[registry.DEFAULT_SURFACE_ID])
227
+ ? normalizedSurfaces[registry.DEFAULT_SURFACE_ID]
228
+ : {};
229
+ const defaultSurfaceDefinition = Object.freeze({
230
+ ...defaultSurfaceSource
231
+ });
232
+
233
+ function normalizeSurfaceMode(value) {
234
+ const normalized = normalizeSurfaceId(value);
235
+ if (!normalized || normalized === allMode) {
236
+ return allMode;
237
+ }
238
+
239
+ return registry.SURFACE_REGISTRY[normalized] ? normalized : allMode;
240
+ }
241
+
242
+ function listEnabledSurfaceIds() {
243
+ return [...enabledSurfaceIds];
244
+ }
245
+
246
+ function getSurfaceDefinition(surfaceId) {
247
+ const normalizedSurfaceId = normalizeSurfaceId(surfaceId);
248
+ const definition = normalizedSurfaces[normalizedSurfaceId];
249
+ if (!definition) {
250
+ return null;
251
+ }
252
+
253
+ return Object.freeze({
254
+ ...definition
255
+ });
256
+ }
257
+
258
+ function listSurfaceDefinitions({ enabledOnly = false } = {}) {
259
+ const ids = enabledOnly ? enabledSurfaceIds : surfaceIds;
260
+ const definitions = [];
261
+ for (const surfaceId of ids) {
262
+ const source = isRecord(normalizedSurfaces[surfaceId]) ? normalizedSurfaces[surfaceId] : {};
263
+ definitions.push(
264
+ Object.freeze({
265
+ ...source
266
+ })
267
+ );
268
+ }
269
+ return definitions;
270
+ }
271
+
272
+ function isSurfaceEnabled(surfaceId) {
273
+ const normalizedSurface = normalizeSurfaceMode(surfaceId);
274
+ if (normalizedSurface === allMode) {
275
+ return false;
276
+ }
277
+
278
+ return enabledSurfaceIds.includes(normalizedSurface);
279
+ }
280
+
281
+ return {
282
+ SURFACE_MODE_ALL: allMode,
283
+ SURFACE_IDS: [...surfaceIds],
284
+ DEFAULT_SURFACE_ID: registry.DEFAULT_SURFACE_ID,
285
+ DEFAULT_SURFACE: defaultSurfaceDefinition,
286
+ normalizeSurfaceMode,
287
+ resolveSurfaceFromPathname: pathHelpers.resolveSurfaceFromPathname,
288
+ getSurfaceDefinition,
289
+ listSurfaceDefinitions,
290
+ listEnabledSurfaceIds,
291
+ isSurfaceEnabled
292
+ };
293
+ }
294
+
295
+
296
+ function filterRoutesBySurface(routeList, { surfaceRuntime, surfaceMode } = {}) {
297
+ if (!surfaceRuntime || typeof surfaceRuntime !== "object") {
298
+ throw new Error("filterRoutesBySurface requires surfaceRuntime.");
299
+ }
300
+ if (typeof surfaceRuntime.normalizeSurfaceMode !== "function") {
301
+ throw new Error("filterRoutesBySurface requires surfaceRuntime.normalizeSurfaceMode().");
302
+ }
303
+ if (typeof surfaceRuntime.resolveSurfaceFromPathname !== "function") {
304
+ throw new Error("filterRoutesBySurface requires surfaceRuntime.resolveSurfaceFromPathname().");
305
+ }
306
+ if (typeof surfaceRuntime.listEnabledSurfaceIds !== "function") {
307
+ throw new Error("filterRoutesBySurface requires surfaceRuntime.listEnabledSurfaceIds().");
308
+ }
309
+
310
+ const normalizedRoutes = normalizeRoutesBySurfaceBoundaries(routeList, {
311
+ surfaceRuntime
312
+ });
313
+ const allMode = String(surfaceRuntime.SURFACE_MODE_ALL || "all").trim().toLowerCase() || "all";
314
+ const normalizedMode = surfaceRuntime.normalizeSurfaceMode(surfaceMode);
315
+ const enabledSurfaces = new Set(surfaceRuntime.listEnabledSurfaceIds());
316
+
317
+ function filterRouteNode(route, inheritedSurfaceId = "") {
318
+ if (!isRecord(route)) {
319
+ return null;
320
+ }
321
+
322
+ const resolvedSurface = resolveRouteSurface(route, inheritedSurfaceId, {
323
+ surfaceRuntime,
324
+ absolutePath: String(route.path || "/")
325
+ });
326
+ const isGlobalScope = resolveOwnRouteScope(route) === "global";
327
+ const isEnabledSurfaceRoute = enabledSurfaces.has(resolvedSurface);
328
+ const ownIncluded =
329
+ isGlobalScope ||
330
+ (isEnabledSurfaceRoute && (normalizedMode === allMode || resolvedSurface === normalizedMode));
331
+
332
+ const children = Array.isArray(route.children) ? route.children : [];
333
+ const filteredChildren = children
334
+ .map((child) => filterRouteNode(child, resolvedSurface))
335
+ .filter(Boolean);
336
+
337
+ if (!ownIncluded && filteredChildren.length < 1) {
338
+ return null;
339
+ }
340
+
341
+ if (children.length < 1) {
342
+ return route;
343
+ }
344
+
345
+ return {
346
+ ...route,
347
+ children: filteredChildren
348
+ };
349
+ }
350
+
351
+ return normalizedRoutes.map((route) => filterRouteNode(route)).filter(Boolean);
352
+ }
353
+
354
+ export {
355
+ createSurfaceRuntime,
356
+ filterRoutesBySurface
357
+ };
@@ -0,0 +1,319 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { createSurfaceRuntime, filterRoutesBySurface } from "./runtime.js";
5
+
6
+ test("createSurfaceRuntime resolves enabled surfaces and normalizes surface mode", () => {
7
+ const runtime = createSurfaceRuntime({
8
+ allMode: "all",
9
+ defaultSurfaceId: "app",
10
+ surfaces: {
11
+ app: { id: "app", pagesRoot: "", enabled: true },
12
+ admin: { id: "admin", pagesRoot: "admin", enabled: true },
13
+ console: { id: "console", pagesRoot: "console", enabled: false }
14
+ }
15
+ });
16
+
17
+ assert.equal(runtime.normalizeSurfaceMode("admin"), "admin");
18
+ assert.equal(runtime.normalizeSurfaceMode("unknown"), "all");
19
+ assert.deepEqual(runtime.listEnabledSurfaceIds(), ["app", "admin"]);
20
+ assert.equal(runtime.isSurfaceEnabled("console"), false);
21
+ assert.equal(runtime.isSurfaceEnabled("admin"), true);
22
+ assert.equal(runtime.DEFAULT_SURFACE_ID, "app");
23
+ assert.equal(runtime.getSurfaceDefinition("missing"), null);
24
+ assert.deepEqual(
25
+ runtime.listSurfaceDefinitions({ enabledOnly: true }).map((entry) => entry.id),
26
+ ["app", "admin"]
27
+ );
28
+ });
29
+
30
+ test("createSurfaceRuntime resolves pathname by derived surface route base", () => {
31
+ const runtime = createSurfaceRuntime({
32
+ defaultSurfaceId: "app",
33
+ surfaces: {
34
+ app: { id: "app", pagesRoot: "", enabled: true },
35
+ admin: { id: "admin", pagesRoot: "admin", enabled: true },
36
+ console: { id: "console", pagesRoot: "console", enabled: true }
37
+ }
38
+ });
39
+
40
+ assert.equal(runtime.resolveSurfaceFromPathname("/admin/users"), "admin");
41
+ assert.equal(runtime.resolveSurfaceFromPathname("/console"), "console");
42
+ assert.equal(runtime.resolveSurfaceFromPathname("/"), "app");
43
+ });
44
+
45
+ test("createSurfaceRuntime does not infer workspace slug URL semantics", () => {
46
+ const runtime = createSurfaceRuntime({
47
+ defaultSurfaceId: "app",
48
+ surfaces: {
49
+ app: { id: "app", pagesRoot: "w/[workspaceSlug]", enabled: true, requiresWorkspace: true },
50
+ admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true, requiresWorkspace: true },
51
+ console: { id: "console", pagesRoot: "console", enabled: true, requiresWorkspace: false }
52
+ }
53
+ });
54
+
55
+ assert.equal(runtime.resolveSurfaceFromPathname("/w/acme"), "app");
56
+ assert.equal(runtime.resolveSurfaceFromPathname("/w/acme/projects"), "app");
57
+ assert.equal(runtime.resolveSurfaceFromPathname("/w/acme/admin"), "admin");
58
+ assert.equal(runtime.resolveSurfaceFromPathname("/w/acme/admin/contacts"), "admin");
59
+ assert.equal(runtime.resolveSurfaceFromPathname("/console"), "console");
60
+ assert.equal(runtime.resolveSurfaceFromPathname("/admin/contacts"), "app");
61
+ });
62
+
63
+ test("filterRoutesBySurface keeps enabled routes for chosen mode", () => {
64
+ const runtime = createSurfaceRuntime({
65
+ defaultSurfaceId: "app",
66
+ surfaces: {
67
+ app: { id: "app", pagesRoot: "", enabled: true },
68
+ admin: { id: "admin", pagesRoot: "admin", enabled: true },
69
+ console: { id: "console", pagesRoot: "console", enabled: false }
70
+ }
71
+ });
72
+
73
+ const filteredAll = filterRoutesBySurface(
74
+ [{ path: "/" }, { path: "/admin" }, { path: "/console" }],
75
+ {
76
+ surfaceRuntime: runtime,
77
+ surfaceMode: "all"
78
+ }
79
+ );
80
+ assert.deepEqual(
81
+ filteredAll.map((route) => route.path),
82
+ ["/", "/admin"]
83
+ );
84
+
85
+ const filteredAdmin = filterRoutesBySurface(
86
+ [{ path: "/" }, { path: "/admin/users" }, { path: "/console" }],
87
+ {
88
+ surfaceRuntime: runtime,
89
+ surfaceMode: "admin"
90
+ }
91
+ );
92
+ assert.deepEqual(
93
+ filteredAdmin.map((route) => route.path),
94
+ ["/admin/users"]
95
+ );
96
+ });
97
+
98
+ test("filterRoutesBySurface always keeps global routes", () => {
99
+ const runtime = createSurfaceRuntime({
100
+ defaultSurfaceId: "app",
101
+ surfaces: {
102
+ app: { id: "app", pagesRoot: "", enabled: true },
103
+ admin: { id: "admin", pagesRoot: "admin", enabled: true }
104
+ }
105
+ });
106
+
107
+ const filteredAdmin = filterRoutesBySurface(
108
+ [
109
+ { path: "/auth/login", scope: "global" },
110
+ { path: "/admin/users" },
111
+ { path: "/app-only", surface: "app" }
112
+ ],
113
+ {
114
+ surfaceRuntime: runtime,
115
+ surfaceMode: "admin"
116
+ }
117
+ );
118
+
119
+ assert.deepEqual(
120
+ filteredAdmin.map((route) => route.path),
121
+ ["/auth/login", "/admin/users"]
122
+ );
123
+ });
124
+
125
+ test("filterRoutesBySurface keeps parent route when a nested descendant is global", () => {
126
+ const runtime = createSurfaceRuntime({
127
+ defaultSurfaceId: "app",
128
+ surfaces: {
129
+ app: { id: "app", pagesRoot: "", enabled: true },
130
+ admin: { id: "admin", pagesRoot: "admin", enabled: true }
131
+ }
132
+ });
133
+
134
+ const filteredAdmin = filterRoutesBySurface(
135
+ [
136
+ {
137
+ path: "/account",
138
+ children: [
139
+ {
140
+ path: "settings",
141
+ children: [
142
+ {
143
+ path: "",
144
+ component: {},
145
+ meta: {
146
+ jskit: {
147
+ scope: "global"
148
+ }
149
+ }
150
+ }
151
+ ]
152
+ }
153
+ ]
154
+ },
155
+ { path: "/admin/users" }
156
+ ],
157
+ {
158
+ surfaceRuntime: runtime,
159
+ surfaceMode: "admin"
160
+ }
161
+ );
162
+
163
+ assert.deepEqual(
164
+ filteredAdmin.map((route) => route.path),
165
+ ["/account", "/admin/users"]
166
+ );
167
+ });
168
+
169
+ test("filterRoutesBySurface resolves route surface from metadata before pathname", () => {
170
+ const runtime = createSurfaceRuntime({
171
+ defaultSurfaceId: "home",
172
+ surfaces: {
173
+ home: { id: "home", pagesRoot: "", enabled: true },
174
+ app: { id: "app", pagesRoot: "w/[workspaceSlug]", enabled: true },
175
+ admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true }
176
+ }
177
+ });
178
+
179
+ const filteredAdmin = filterRoutesBySurface(
180
+ [
181
+ {
182
+ path: "/w/:workspaceSlug",
183
+ component: {},
184
+ meta: {
185
+ jskit: {
186
+ surface: "app"
187
+ }
188
+ },
189
+ children: [{ path: "projects", component: {} }]
190
+ },
191
+ {
192
+ path: "/w/:workspaceSlug/admin",
193
+ component: {},
194
+ meta: {
195
+ jskit: {
196
+ surface: "admin"
197
+ }
198
+ },
199
+ children: [{ path: "users", component: {} }]
200
+ }
201
+ ],
202
+ {
203
+ surfaceRuntime: runtime,
204
+ surfaceMode: "admin"
205
+ }
206
+ );
207
+
208
+ assert.deepEqual(
209
+ filteredAdmin.map((route) => route.path),
210
+ ["/w/:workspaceSlug/admin"]
211
+ );
212
+ assert.equal(filteredAdmin[0].children?.length, 1);
213
+ assert.equal(filteredAdmin[0].children?.[0]?.path, "users");
214
+ });
215
+
216
+ test("filterRoutesBySurface lets children inherit nearest ancestor surface metadata", () => {
217
+ const runtime = createSurfaceRuntime({
218
+ defaultSurfaceId: "home",
219
+ surfaces: {
220
+ home: { id: "home", pagesRoot: "", enabled: true },
221
+ app: { id: "app", pagesRoot: "w/[workspaceSlug]", enabled: true },
222
+ admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true }
223
+ }
224
+ });
225
+
226
+ const filteredApp = filterRoutesBySurface(
227
+ [
228
+ {
229
+ path: "/w/:workspaceSlug",
230
+ component: {},
231
+ meta: {
232
+ jskit: {
233
+ surface: "app"
234
+ }
235
+ },
236
+ children: [
237
+ { path: "projects", component: {} },
238
+ {
239
+ path: "admin",
240
+ component: {},
241
+ meta: {
242
+ jskit: {
243
+ surface: "admin"
244
+ }
245
+ },
246
+ children: [{ path: "users", component: {} }]
247
+ }
248
+ ]
249
+ }
250
+ ],
251
+ {
252
+ surfaceRuntime: runtime,
253
+ surfaceMode: "app"
254
+ }
255
+ );
256
+
257
+ assert.deepEqual(filteredApp.map((route) => route.path), ["/w/:workspaceSlug"]);
258
+ assert.equal(filteredApp[0].children?.length, 1);
259
+ assert.equal(filteredApp[0].children?.[0]?.path, "projects");
260
+ });
261
+
262
+ test("filterRoutesBySurface hoists cross-surface nested route wrappers to top-level records", () => {
263
+ const runtime = createSurfaceRuntime({
264
+ defaultSurfaceId: "home",
265
+ surfaces: {
266
+ home: { id: "home", pagesRoot: "", enabled: true },
267
+ app: { id: "app", pagesRoot: "w/[workspaceSlug]", enabled: true },
268
+ admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true }
269
+ }
270
+ });
271
+
272
+ const filteredAll = filterRoutesBySurface(
273
+ [
274
+ {
275
+ path: "/w/:workspaceSlug",
276
+ component: {},
277
+ meta: {
278
+ jskit: {
279
+ surface: "app"
280
+ }
281
+ },
282
+ children: [
283
+ { path: "", component: {} },
284
+ { path: "projects", component: {} },
285
+ {
286
+ path: "admin",
287
+ component: {},
288
+ meta: {
289
+ jskit: {
290
+ surface: "admin"
291
+ }
292
+ },
293
+ children: [
294
+ { path: "", component: {} },
295
+ { path: "members", component: {} }
296
+ ]
297
+ }
298
+ ]
299
+ }
300
+ ],
301
+ {
302
+ surfaceRuntime: runtime,
303
+ surfaceMode: "all"
304
+ }
305
+ );
306
+
307
+ assert.deepEqual(
308
+ filteredAll.map((route) => route.path),
309
+ ["/w/:workspaceSlug", "/w/:workspaceSlug/admin"]
310
+ );
311
+ assert.deepEqual(
312
+ (filteredAll[0].children || []).map((route) => route.path),
313
+ ["", "projects"]
314
+ );
315
+ assert.deepEqual(
316
+ (filteredAll[1].children || []).map((route) => route.path),
317
+ ["", "members"]
318
+ );
319
+ });