@jskit-ai/shell-web 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.
- package/package.descriptor.mjs +165 -0
- package/package.json +23 -0
- package/src/client/components/ShellErrorHost.vue +208 -0
- package/src/client/components/ShellLayout.vue +191 -0
- package/src/client/components/ShellOutlet.vue +95 -0
- package/src/client/components/useShellLayout.js +93 -0
- package/src/client/error/index.js +2 -0
- package/src/client/error/inject.js +142 -0
- package/src/client/error/normalize.js +75 -0
- package/src/client/error/policy.js +50 -0
- package/src/client/error/presenters.js +89 -0
- package/src/client/error/runtime.js +418 -0
- package/src/client/error/store.js +176 -0
- package/src/client/error/tokens.js +14 -0
- package/src/client/index.js +17 -0
- package/src/client/navigation/linkResolver.js +117 -0
- package/src/client/placement/debug.js +52 -0
- package/src/client/placement/index.js +26 -0
- package/src/client/placement/inject.js +104 -0
- package/src/client/placement/pathname.js +14 -0
- package/src/client/placement/registry.js +41 -0
- package/src/client/placement/runtime.js +435 -0
- package/src/client/placement/surfaceContext.js +290 -0
- package/src/client/placement/tokens.js +29 -0
- package/src/client/placement/validators.js +210 -0
- package/src/client/providers/ShellWebClientProvider.js +352 -0
- package/templates/src/App.vue +11 -0
- package/templates/src/components/ShellLayout.vue +247 -0
- package/templates/src/error.js +13 -0
- package/templates/src/pages/console/index.vue +24 -0
- package/templates/src/pages/console.vue +20 -0
- package/templates/src/pages/home/index.vue +54 -0
- package/templates/src/pages/home.vue +20 -0
- package/templates/src/placement.js +12 -0
- package/test/errorRuntime.test.js +191 -0
- package/test/errorStore.test.js +26 -0
- package/test/linkResolver.test.js +112 -0
- package/test/placementRegistry.test.js +45 -0
- package/test/placementRuntime.test.js +374 -0
- package/test/provider.test.js +163 -0
- package/test/surfaceContext.test.js +184 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WEB_PLACEMENT_CONTEXT_CONTRIBUTOR_TAG,
|
|
3
|
+
WEB_PLACEMENT_SURFACE_ANY
|
|
4
|
+
} from "./tokens.js";
|
|
5
|
+
import { DEFAULT_DEBUG_DEPTH, explodePayload } from "./debug.js";
|
|
6
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
|
+
import {
|
|
8
|
+
isRenderableComponent,
|
|
9
|
+
normalizePlacementDefinition,
|
|
10
|
+
normalizePlacementHost,
|
|
11
|
+
normalizePlacementPosition,
|
|
12
|
+
normalizeSurface
|
|
13
|
+
} from "./validators.js";
|
|
14
|
+
|
|
15
|
+
function ensureArray(value) {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
if (value === undefined || value === null) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return [value];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PLACEMENT_DEBUG_PREFIX = "[placement-debug]";
|
|
26
|
+
const PLACEMENT_DEBUG_FLAG = "__JSKIT_PLACEMENT_DEBUG__";
|
|
27
|
+
const NOOP = () => {};
|
|
28
|
+
|
|
29
|
+
function isPlacementDebugEnabled() {
|
|
30
|
+
if (typeof globalThis !== "object" || !globalThis) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return globalThis[PLACEMENT_DEBUG_FLAG] === true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function debugLog(message, payload = null) {
|
|
38
|
+
if (!isPlacementDebugEnabled()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (payload === null || payload === undefined) {
|
|
43
|
+
console.log(`${PLACEMENT_DEBUG_PREFIX} ${message}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const terminalPayload = explodePayload(payload, DEFAULT_DEBUG_DEPTH);
|
|
47
|
+
const rendered = JSON.stringify(terminalPayload, null, 2);
|
|
48
|
+
console.log(`${PLACEMENT_DEBUG_PREFIX} ${message}\n${rendered}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createRuntimeLogger(logger) {
|
|
52
|
+
const runtimeLogger = isRecord(logger) ? logger : {};
|
|
53
|
+
let warn = NOOP;
|
|
54
|
+
let error = NOOP;
|
|
55
|
+
|
|
56
|
+
if (typeof runtimeLogger.warn === "function") {
|
|
57
|
+
warn = runtimeLogger.warn.bind(runtimeLogger);
|
|
58
|
+
}
|
|
59
|
+
if (typeof runtimeLogger.error === "function") {
|
|
60
|
+
error = runtimeLogger.error.bind(runtimeLogger);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
warn,
|
|
65
|
+
error
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizePlacementList(placements, context = {}) {
|
|
70
|
+
const normalized = [];
|
|
71
|
+
|
|
72
|
+
for (const candidate of ensureArray(placements)) {
|
|
73
|
+
const placement = normalizePlacementDefinition(candidate, {
|
|
74
|
+
strict: false,
|
|
75
|
+
source: "app placement"
|
|
76
|
+
});
|
|
77
|
+
if (!placement || placement.enabled === false) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
normalized.push(placement);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const byId = new Map();
|
|
84
|
+
for (const placement of normalized) {
|
|
85
|
+
if (byId.has(placement.id)) {
|
|
86
|
+
throw new Error(`Duplicate placement id "${placement.id}" in ${context.source || "placement list"}.`);
|
|
87
|
+
}
|
|
88
|
+
byId.set(placement.id, placement);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [...byId.values()].sort((left, right) => {
|
|
92
|
+
const orderCompare = left.order - right.order;
|
|
93
|
+
if (orderCompare !== 0) {
|
|
94
|
+
return orderCompare;
|
|
95
|
+
}
|
|
96
|
+
return left.id.localeCompare(right.id);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function matchesSurface(placementSurfaces, requestedSurface) {
|
|
101
|
+
if (requestedSurface === WEB_PLACEMENT_SURFACE_ANY) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
const surfaces = Array.isArray(placementSurfaces) ? placementSurfaces : [WEB_PLACEMENT_SURFACE_ANY];
|
|
105
|
+
return surfaces.includes(WEB_PLACEMENT_SURFACE_ANY) || surfaces.includes(requestedSurface);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveContextContributors(app, baseContext = {}, logger) {
|
|
109
|
+
const contributors = app.resolveTag(WEB_PLACEMENT_CONTEXT_CONTRIBUTOR_TAG);
|
|
110
|
+
let merged = {};
|
|
111
|
+
|
|
112
|
+
for (const contributor of ensureArray(contributors)) {
|
|
113
|
+
try {
|
|
114
|
+
const resolved = typeof contributor === "function" ? contributor(Object.freeze({ ...baseContext })) : contributor;
|
|
115
|
+
if (isRecord(resolved)) {
|
|
116
|
+
merged = {
|
|
117
|
+
...merged,
|
|
118
|
+
...resolved
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.warn(
|
|
123
|
+
{
|
|
124
|
+
contributor,
|
|
125
|
+
error: String(error?.message || error || "unknown error")
|
|
126
|
+
},
|
|
127
|
+
"Failed to evaluate web placement context contributor."
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return merged;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolvePlacementComponent(
|
|
136
|
+
app,
|
|
137
|
+
placement,
|
|
138
|
+
logger,
|
|
139
|
+
missingTokens,
|
|
140
|
+
invalidComponentTokens,
|
|
141
|
+
failedTokens
|
|
142
|
+
) {
|
|
143
|
+
const componentToken = String(placement.componentToken || "").trim();
|
|
144
|
+
if (!componentToken) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (invalidComponentTokens.has(componentToken) || failedTokens.has(componentToken)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!app.has(componentToken)) {
|
|
153
|
+
if (!missingTokens.has(componentToken)) {
|
|
154
|
+
missingTokens.add(componentToken);
|
|
155
|
+
logger.warn(
|
|
156
|
+
{
|
|
157
|
+
placementId: placement.id,
|
|
158
|
+
componentToken
|
|
159
|
+
},
|
|
160
|
+
"Skipping placement because component token is not bound."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let component = null;
|
|
167
|
+
try {
|
|
168
|
+
component = app.make(componentToken);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (!failedTokens.has(componentToken)) {
|
|
171
|
+
failedTokens.add(componentToken);
|
|
172
|
+
logger.error(
|
|
173
|
+
{
|
|
174
|
+
placementId: placement.id,
|
|
175
|
+
componentToken,
|
|
176
|
+
error: String(error?.message || error || "unknown error")
|
|
177
|
+
},
|
|
178
|
+
"Skipping placement because component token resolution threw."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!isRenderableComponent(component)) {
|
|
185
|
+
if (!invalidComponentTokens.has(componentToken)) {
|
|
186
|
+
invalidComponentTokens.add(componentToken);
|
|
187
|
+
logger.warn(
|
|
188
|
+
{
|
|
189
|
+
placementId: placement.id,
|
|
190
|
+
componentToken
|
|
191
|
+
},
|
|
192
|
+
"Skipping placement because component token did not resolve to a Vue component."
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return component;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function shouldIncludePlacement(placement, placementContext, logger) {
|
|
202
|
+
if (typeof placement.when !== "function") {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
return placement.when(Object.freeze({ ...placementContext })) === true;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
logger.warn(
|
|
210
|
+
{
|
|
211
|
+
placementId: placement.id,
|
|
212
|
+
error: String(error?.message || error || "unknown error")
|
|
213
|
+
},
|
|
214
|
+
"Placement when() predicate failed; placement was skipped."
|
|
215
|
+
);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function createWebPlacementRuntime({ app, logger = null } = {}) {
|
|
221
|
+
if (!app || typeof app.resolveTag !== "function" || typeof app.make !== "function" || typeof app.has !== "function") {
|
|
222
|
+
throw new Error("createWebPlacementRuntime requires app.resolveTag(), app.has(), and app.make().");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const runtimeLogger = createRuntimeLogger(logger);
|
|
226
|
+
const missingTokens = new Set();
|
|
227
|
+
const invalidComponentTokens = new Set();
|
|
228
|
+
const failedTokens = new Set();
|
|
229
|
+
const listeners = new Set();
|
|
230
|
+
let placementDefinitions = Object.freeze([]);
|
|
231
|
+
let sharedContext = Object.freeze({});
|
|
232
|
+
let revision = 0;
|
|
233
|
+
|
|
234
|
+
function notify(event = {}) {
|
|
235
|
+
revision += 1;
|
|
236
|
+
debugLog("notify", {
|
|
237
|
+
revision,
|
|
238
|
+
event
|
|
239
|
+
});
|
|
240
|
+
for (const listener of listeners) {
|
|
241
|
+
try {
|
|
242
|
+
listener(
|
|
243
|
+
Object.freeze({
|
|
244
|
+
revision,
|
|
245
|
+
...event
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
runtimeLogger.warn(
|
|
250
|
+
{
|
|
251
|
+
error: String(error?.message || error || "unknown error")
|
|
252
|
+
},
|
|
253
|
+
"Web placement runtime listener threw during notification."
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function replacePlacements(entries = [], { source = "app placement registry" } = {}) {
|
|
260
|
+
missingTokens.clear();
|
|
261
|
+
invalidComponentTokens.clear();
|
|
262
|
+
failedTokens.clear();
|
|
263
|
+
placementDefinitions = Object.freeze(normalizePlacementList(entries, { source }));
|
|
264
|
+
debugLog("replacePlacements", {
|
|
265
|
+
source,
|
|
266
|
+
count: placementDefinitions.length,
|
|
267
|
+
ids: placementDefinitions.map((entry) => entry.id)
|
|
268
|
+
});
|
|
269
|
+
notify({
|
|
270
|
+
type: "placements.replaced",
|
|
271
|
+
source
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function getContext() {
|
|
276
|
+
return sharedContext;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function setContext(value = {}, { replace = false, source = "placement-context" } = {}) {
|
|
280
|
+
const next = isRecord(value) ? { ...value } : {};
|
|
281
|
+
|
|
282
|
+
let nextContext = next;
|
|
283
|
+
if (!replace) {
|
|
284
|
+
nextContext = {
|
|
285
|
+
...sharedContext,
|
|
286
|
+
...next
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
sharedContext = Object.freeze(nextContext);
|
|
291
|
+
debugLog("setContext", {
|
|
292
|
+
replace,
|
|
293
|
+
source,
|
|
294
|
+
keys: Object.keys(sharedContext)
|
|
295
|
+
});
|
|
296
|
+
notify({
|
|
297
|
+
type: "context.updated",
|
|
298
|
+
source
|
|
299
|
+
});
|
|
300
|
+
return sharedContext;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function subscribe(listener) {
|
|
304
|
+
if (typeof listener !== "function") {
|
|
305
|
+
return () => {};
|
|
306
|
+
}
|
|
307
|
+
listeners.add(listener);
|
|
308
|
+
return () => {
|
|
309
|
+
listeners.delete(listener);
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getRevision() {
|
|
314
|
+
return revision;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function getPlacements({ surface = WEB_PLACEMENT_SURFACE_ANY, host = "", position = "", context = {} } = {}) {
|
|
318
|
+
const normalizedHost = normalizePlacementHost(host, { strict: false });
|
|
319
|
+
const normalizedPosition = normalizePlacementPosition(position, { strict: false });
|
|
320
|
+
if (!normalizedHost || !normalizedPosition) {
|
|
321
|
+
return Object.freeze([]);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const normalizedSurface = normalizeSurface(surface);
|
|
325
|
+
const baseContext = isRecord(context) ? { ...context } : {};
|
|
326
|
+
const contextFromRuntime = isRecord(sharedContext) ? sharedContext : {};
|
|
327
|
+
const contextFromContributors = resolveContextContributors(
|
|
328
|
+
app,
|
|
329
|
+
{
|
|
330
|
+
app,
|
|
331
|
+
surface: normalizedSurface,
|
|
332
|
+
host: normalizedHost,
|
|
333
|
+
position: normalizedPosition,
|
|
334
|
+
context: {
|
|
335
|
+
...contextFromRuntime,
|
|
336
|
+
...baseContext
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
runtimeLogger
|
|
340
|
+
);
|
|
341
|
+
const placementContext = {
|
|
342
|
+
...contextFromContributors,
|
|
343
|
+
...contextFromRuntime,
|
|
344
|
+
...baseContext,
|
|
345
|
+
app,
|
|
346
|
+
surface: normalizedSurface,
|
|
347
|
+
host: normalizedHost,
|
|
348
|
+
position: normalizedPosition
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
debugLog("getPlacements:start", {
|
|
352
|
+
surface: normalizedSurface,
|
|
353
|
+
host: normalizedHost,
|
|
354
|
+
position: normalizedPosition,
|
|
355
|
+
contextKeys: Object.keys(baseContext),
|
|
356
|
+
sharedContextKeys: Object.keys(contextFromRuntime),
|
|
357
|
+
placementCount: placementDefinitions.length
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const matches = [];
|
|
361
|
+
for (const placement of placementDefinitions) {
|
|
362
|
+
if (placement.host !== normalizedHost || placement.position !== normalizedPosition) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const placementSurfaces = Array.isArray(placement.surfaces)
|
|
366
|
+
? placement.surfaces
|
|
367
|
+
: [WEB_PLACEMENT_SURFACE_ANY];
|
|
368
|
+
|
|
369
|
+
if (!matchesSurface(placementSurfaces, normalizedSurface)) {
|
|
370
|
+
debugLog("getPlacements:skip-surfaces", {
|
|
371
|
+
placementId: placement.id,
|
|
372
|
+
placementSurfaces,
|
|
373
|
+
requestedSurface: normalizedSurface
|
|
374
|
+
});
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (!shouldIncludePlacement(placement, placementContext, runtimeLogger)) {
|
|
378
|
+
debugLog("getPlacements:skip-when", {
|
|
379
|
+
placementId: placement.id
|
|
380
|
+
});
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const component = resolvePlacementComponent(
|
|
385
|
+
app,
|
|
386
|
+
placement,
|
|
387
|
+
runtimeLogger,
|
|
388
|
+
missingTokens,
|
|
389
|
+
invalidComponentTokens,
|
|
390
|
+
failedTokens
|
|
391
|
+
);
|
|
392
|
+
if (!component) {
|
|
393
|
+
debugLog("getPlacements:skip-component", {
|
|
394
|
+
placementId: placement.id,
|
|
395
|
+
componentToken: placement.componentToken
|
|
396
|
+
});
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
debugLog("getPlacements:include", {
|
|
401
|
+
placementId: placement.id,
|
|
402
|
+
componentToken: placement.componentToken,
|
|
403
|
+
placementSurfaces,
|
|
404
|
+
order: placement.order
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
matches.push(
|
|
408
|
+
Object.freeze({
|
|
409
|
+
...placement,
|
|
410
|
+
component
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
debugLog("getPlacements:done", {
|
|
416
|
+
surface: normalizedSurface,
|
|
417
|
+
host: normalizedHost,
|
|
418
|
+
position: normalizedPosition,
|
|
419
|
+
resultIds: matches.map((entry) => entry.id)
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return Object.freeze(matches);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return Object.freeze({
|
|
426
|
+
replacePlacements,
|
|
427
|
+
getPlacements,
|
|
428
|
+
getContext,
|
|
429
|
+
setContext,
|
|
430
|
+
subscribe,
|
|
431
|
+
getRevision
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export { createWebPlacementRuntime };
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSurfacePathHelpers,
|
|
3
|
+
deriveSurfaceRouteBaseFromPagesRoot,
|
|
4
|
+
normalizeSurfaceId,
|
|
5
|
+
normalizeSurfacePagesRoot
|
|
6
|
+
} from "@jskit-ai/kernel/shared/surface";
|
|
7
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
8
|
+
import { normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
|
|
9
|
+
import { isExternalLinkTarget, splitPathQueryHash } from "@jskit-ai/kernel/shared/support/linkPath";
|
|
10
|
+
|
|
11
|
+
const EMPTY_SURFACE_CONFIG = Object.freeze({
|
|
12
|
+
tenancyMode: "",
|
|
13
|
+
defaultSurfaceId: "",
|
|
14
|
+
enabledSurfaceIds: Object.freeze([]),
|
|
15
|
+
surfacesById: Object.freeze({})
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function normalizeSurfaceIdList(value) {
|
|
19
|
+
if (!Array.isArray(value)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const normalized = [];
|
|
25
|
+
for (const candidate of value) {
|
|
26
|
+
const surfaceId = normalizeSurfaceId(candidate);
|
|
27
|
+
if (!surfaceId || seen.has(surfaceId)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
seen.add(surfaceId);
|
|
31
|
+
normalized.push(surfaceId);
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeSurfaceConfig(surfaceConfig = {}) {
|
|
37
|
+
const source = isRecord(surfaceConfig) ? surfaceConfig : {};
|
|
38
|
+
const enabledSurfaceIds = normalizeSurfaceIdList(source.enabledSurfaceIds);
|
|
39
|
+
const enabledSet = new Set(enabledSurfaceIds);
|
|
40
|
+
const rawSurfacesById = isRecord(source.surfacesById) ? source.surfacesById : {};
|
|
41
|
+
const normalizedSurfacesById = {};
|
|
42
|
+
|
|
43
|
+
for (const [rawSurfaceId, rawDefinition] of Object.entries(rawSurfacesById)) {
|
|
44
|
+
const definition = isRecord(rawDefinition) ? rawDefinition : {};
|
|
45
|
+
const surfaceId = normalizeSurfaceId(definition.id || rawSurfaceId);
|
|
46
|
+
if (!surfaceId) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pagesRoot = normalizeSurfacePagesRoot(definition.pagesRoot);
|
|
51
|
+
const routeBase = String(
|
|
52
|
+
definition.routeBase || deriveSurfaceRouteBaseFromPagesRoot(pagesRoot)
|
|
53
|
+
).trim() || "/";
|
|
54
|
+
const enabled = enabledSet.size > 0 ? enabledSet.has(surfaceId) : definition.enabled !== false;
|
|
55
|
+
normalizedSurfacesById[surfaceId] = Object.freeze({
|
|
56
|
+
...definition,
|
|
57
|
+
id: surfaceId,
|
|
58
|
+
pagesRoot,
|
|
59
|
+
routeBase,
|
|
60
|
+
enabled
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const derivedEnabledSurfaceIds =
|
|
65
|
+
enabledSurfaceIds.length > 0
|
|
66
|
+
? enabledSurfaceIds.filter((surfaceId) => Boolean(normalizedSurfacesById[surfaceId]))
|
|
67
|
+
: Object.values(normalizedSurfacesById)
|
|
68
|
+
.filter((definition) => definition.enabled)
|
|
69
|
+
.map((definition) => definition.id);
|
|
70
|
+
const defaultSurfaceId = normalizeSurfaceId(source.defaultSurfaceId);
|
|
71
|
+
const resolvedDefaultSurfaceId = normalizedSurfacesById[defaultSurfaceId]
|
|
72
|
+
? defaultSurfaceId
|
|
73
|
+
: derivedEnabledSurfaceIds[0] || Object.keys(normalizedSurfacesById)[0] || "";
|
|
74
|
+
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
tenancyMode: String(source.tenancyMode || "").trim().toLowerCase(),
|
|
77
|
+
defaultSurfaceId: resolvedDefaultSurfaceId,
|
|
78
|
+
enabledSurfaceIds: Object.freeze([...derivedEnabledSurfaceIds]),
|
|
79
|
+
surfacesById: Object.freeze({
|
|
80
|
+
...normalizedSurfacesById
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildSurfaceConfigContext(surfaceRuntime = null, { tenancyMode = "" } = {}) {
|
|
86
|
+
const normalizedTenancyMode = String(tenancyMode || "").trim().toLowerCase();
|
|
87
|
+
if (!isRecord(surfaceRuntime)) {
|
|
88
|
+
return normalizeSurfaceConfig({
|
|
89
|
+
tenancyMode: normalizedTenancyMode
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const surfaceDefinitions =
|
|
94
|
+
typeof surfaceRuntime.listSurfaceDefinitions === "function" ? surfaceRuntime.listSurfaceDefinitions() : [];
|
|
95
|
+
const surfacesById = {};
|
|
96
|
+
for (const definition of Array.isArray(surfaceDefinitions) ? surfaceDefinitions : []) {
|
|
97
|
+
if (!isRecord(definition)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const surfaceId = normalizeSurfaceId(definition.id);
|
|
102
|
+
if (!surfaceId) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
surfacesById[surfaceId] = definition;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return normalizeSurfaceConfig({
|
|
109
|
+
tenancyMode: normalizedTenancyMode,
|
|
110
|
+
defaultSurfaceId: surfaceRuntime.DEFAULT_SURFACE_ID,
|
|
111
|
+
enabledSurfaceIds:
|
|
112
|
+
typeof surfaceRuntime.listEnabledSurfaceIds === "function" ? surfaceRuntime.listEnabledSurfaceIds() : [],
|
|
113
|
+
surfacesById
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readPlacementSurfaceConfig(contextValue = null) {
|
|
118
|
+
const contextRecord = isRecord(contextValue) ? contextValue : {};
|
|
119
|
+
return normalizeSurfaceConfig(contextRecord.surfaceConfig);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveSurfaceDefinitionFromPlacementContext(contextValue = null, surfaceId = "") {
|
|
123
|
+
const surfaceConfig = readPlacementSurfaceConfig(contextValue);
|
|
124
|
+
const normalizedSurfaceId = normalizeSurfaceId(surfaceId);
|
|
125
|
+
if (!normalizedSurfaceId) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return surfaceConfig.surfacesById[normalizedSurfaceId] || null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function joinSurfacePath(surfacePrefix = "", pathname = "") {
|
|
132
|
+
const normalizedPrefix = normalizePathname(surfacePrefix || "/");
|
|
133
|
+
const rawPathname = String(pathname || "").trim();
|
|
134
|
+
if (!rawPathname) {
|
|
135
|
+
return normalizedPrefix;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const withLeadingSlash = rawPathname.startsWith("/") ? rawPathname : `/${rawPathname}`;
|
|
139
|
+
const normalizedPathname = withLeadingSlash.replace(/\/{2,}/g, "/");
|
|
140
|
+
const joined = normalizedPrefix === "/" ? normalizedPathname : `${normalizedPrefix}${normalizedPathname}`;
|
|
141
|
+
const compacted = joined.replace(/\/{2,}/g, "/");
|
|
142
|
+
if (!compacted) {
|
|
143
|
+
return "/";
|
|
144
|
+
}
|
|
145
|
+
return compacted === "/" ? compacted : compacted.replace(/\/+$/, "") || "/";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveSurfaceRootPathFromPlacementContext(contextValue = null, surfaceId = "") {
|
|
149
|
+
const surfaceDefinition = resolveSurfaceDefinitionFromPlacementContext(contextValue, surfaceId);
|
|
150
|
+
return joinSurfacePath(surfaceDefinition?.routeBase, "");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveSurfacePathFromPlacementContext(contextValue = null, surfaceId = "", pathname = "") {
|
|
154
|
+
const surfaceDefinition = resolveSurfaceDefinitionFromPlacementContext(contextValue, surfaceId);
|
|
155
|
+
return joinSurfacePath(surfaceDefinition?.routeBase, pathname);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeSurfaceOrigin(originValue = "") {
|
|
159
|
+
const rawOrigin = String(originValue || "").trim();
|
|
160
|
+
if (!rawOrigin) {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const parsed = new URL(rawOrigin);
|
|
166
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
return parsed.origin;
|
|
170
|
+
} catch {
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resolveRuntimeOrigin(currentOrigin = "") {
|
|
176
|
+
const normalizedCurrentOrigin = normalizeSurfaceOrigin(currentOrigin);
|
|
177
|
+
if (normalizedCurrentOrigin) {
|
|
178
|
+
return normalizedCurrentOrigin;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof window === "object" && window?.location?.origin) {
|
|
182
|
+
return normalizeSurfaceOrigin(window.location.origin);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveSurfaceNavigationTargetFromPlacementContext(
|
|
189
|
+
contextValue = null,
|
|
190
|
+
{
|
|
191
|
+
path = "/",
|
|
192
|
+
surfaceId = "",
|
|
193
|
+
currentOrigin = ""
|
|
194
|
+
} = {}
|
|
195
|
+
) {
|
|
196
|
+
const rawPath = String(path || "").trim() || "/";
|
|
197
|
+
if (isExternalLinkTarget(rawPath)) {
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
href: rawPath,
|
|
200
|
+
sameOrigin: false,
|
|
201
|
+
surfaceId: normalizeSurfaceId(surfaceId),
|
|
202
|
+
external: true
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { pathname, search, hash } = splitPathQueryHash(rawPath);
|
|
207
|
+
const normalizedPathname = normalizePathname(pathname || "/");
|
|
208
|
+
const normalizedPath = `${normalizedPathname}${search}${hash}`;
|
|
209
|
+
const resolvedSurfaceId =
|
|
210
|
+
normalizeSurfaceId(surfaceId) ||
|
|
211
|
+
resolveSurfaceIdFromPlacementPathname(contextValue, normalizedPathname) ||
|
|
212
|
+
"";
|
|
213
|
+
const surfaceDefinition = resolvedSurfaceId
|
|
214
|
+
? resolveSurfaceDefinitionFromPlacementContext(contextValue, resolvedSurfaceId)
|
|
215
|
+
: null;
|
|
216
|
+
const targetOrigin = normalizeSurfaceOrigin(surfaceDefinition?.origin || "");
|
|
217
|
+
const runtimeOrigin = resolveRuntimeOrigin(currentOrigin);
|
|
218
|
+
|
|
219
|
+
if (!targetOrigin) {
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
href: normalizedPath,
|
|
222
|
+
sameOrigin: true,
|
|
223
|
+
surfaceId: resolvedSurfaceId,
|
|
224
|
+
external: false
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (runtimeOrigin && targetOrigin === runtimeOrigin) {
|
|
229
|
+
return Object.freeze({
|
|
230
|
+
href: normalizedPath,
|
|
231
|
+
sameOrigin: true,
|
|
232
|
+
surfaceId: resolvedSurfaceId,
|
|
233
|
+
external: false
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return Object.freeze({
|
|
238
|
+
href: `${targetOrigin}${normalizedPath}`,
|
|
239
|
+
sameOrigin: false,
|
|
240
|
+
surfaceId: resolvedSurfaceId,
|
|
241
|
+
external: false
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function createPlacementSurfacePathHelpers(surfaceConfig = EMPTY_SURFACE_CONFIG) {
|
|
246
|
+
const surfacesById = isRecord(surfaceConfig.surfacesById) ? surfaceConfig.surfacesById : {};
|
|
247
|
+
const defaultSurfaceId = normalizeSurfaceId(surfaceConfig.defaultSurfaceId);
|
|
248
|
+
const fallbackSurfaceId = Object.keys(surfacesById)[0] || "";
|
|
249
|
+
const resolvedDefaultSurfaceId = defaultSurfaceId && surfacesById[defaultSurfaceId] ? defaultSurfaceId : fallbackSurfaceId;
|
|
250
|
+
if (!resolvedDefaultSurfaceId) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return createSurfacePathHelpers({
|
|
255
|
+
defaultSurfaceId: resolvedDefaultSurfaceId,
|
|
256
|
+
normalizeSurfaceId,
|
|
257
|
+
resolveSurfaceRouteBase(surfaceId) {
|
|
258
|
+
const normalizedSurfaceId = normalizeSurfaceId(surfaceId);
|
|
259
|
+
return String(surfacesById[normalizedSurfaceId]?.routeBase || "/").trim() || "/";
|
|
260
|
+
},
|
|
261
|
+
listSurfaceDefinitions() {
|
|
262
|
+
return Object.values(surfacesById);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveSurfaceIdFromPlacementPathname(contextValue = null, pathname = "") {
|
|
268
|
+
const surfaceConfig = readPlacementSurfaceConfig(contextValue);
|
|
269
|
+
const normalizedPathname =
|
|
270
|
+
normalizePathname(pathname) ||
|
|
271
|
+
(typeof window === "object" && window?.location?.pathname ? normalizePathname(window.location.pathname) : "/");
|
|
272
|
+
const pathHelpers = createPlacementSurfacePathHelpers(surfaceConfig);
|
|
273
|
+
if (!pathHelpers) {
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
return pathHelpers.resolveSurfaceFromPathname(normalizedPathname);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export {
|
|
280
|
+
EMPTY_SURFACE_CONFIG,
|
|
281
|
+
buildSurfaceConfigContext,
|
|
282
|
+
readPlacementSurfaceConfig,
|
|
283
|
+
resolveSurfaceDefinitionFromPlacementContext,
|
|
284
|
+
joinSurfacePath,
|
|
285
|
+
resolveSurfaceIdFromPlacementPathname,
|
|
286
|
+
resolveSurfaceRootPathFromPlacementContext,
|
|
287
|
+
resolveSurfacePathFromPlacementContext,
|
|
288
|
+
normalizeSurfaceOrigin,
|
|
289
|
+
resolveSurfaceNavigationTargetFromPlacementContext
|
|
290
|
+
};
|