@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.
Files changed (41) hide show
  1. package/package.descriptor.mjs +165 -0
  2. package/package.json +23 -0
  3. package/src/client/components/ShellErrorHost.vue +208 -0
  4. package/src/client/components/ShellLayout.vue +191 -0
  5. package/src/client/components/ShellOutlet.vue +95 -0
  6. package/src/client/components/useShellLayout.js +93 -0
  7. package/src/client/error/index.js +2 -0
  8. package/src/client/error/inject.js +142 -0
  9. package/src/client/error/normalize.js +75 -0
  10. package/src/client/error/policy.js +50 -0
  11. package/src/client/error/presenters.js +89 -0
  12. package/src/client/error/runtime.js +418 -0
  13. package/src/client/error/store.js +176 -0
  14. package/src/client/error/tokens.js +14 -0
  15. package/src/client/index.js +17 -0
  16. package/src/client/navigation/linkResolver.js +117 -0
  17. package/src/client/placement/debug.js +52 -0
  18. package/src/client/placement/index.js +26 -0
  19. package/src/client/placement/inject.js +104 -0
  20. package/src/client/placement/pathname.js +14 -0
  21. package/src/client/placement/registry.js +41 -0
  22. package/src/client/placement/runtime.js +435 -0
  23. package/src/client/placement/surfaceContext.js +290 -0
  24. package/src/client/placement/tokens.js +29 -0
  25. package/src/client/placement/validators.js +210 -0
  26. package/src/client/providers/ShellWebClientProvider.js +352 -0
  27. package/templates/src/App.vue +11 -0
  28. package/templates/src/components/ShellLayout.vue +247 -0
  29. package/templates/src/error.js +13 -0
  30. package/templates/src/pages/console/index.vue +24 -0
  31. package/templates/src/pages/console.vue +20 -0
  32. package/templates/src/pages/home/index.vue +54 -0
  33. package/templates/src/pages/home.vue +20 -0
  34. package/templates/src/placement.js +12 -0
  35. package/test/errorRuntime.test.js +191 -0
  36. package/test/errorStore.test.js +26 -0
  37. package/test/linkResolver.test.js +112 -0
  38. package/test/placementRegistry.test.js +45 -0
  39. package/test/placementRuntime.test.js +374 -0
  40. package/test/provider.test.js +163 -0
  41. 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
+ };