@jskit-ai/kernel 0.1.244 → 0.1.246

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 (101) hide show
  1. package/client/appConfig.js +2 -1
  2. package/client/asyncModuleRecovery.js +2 -4
  3. package/client/index.d.ts +18 -76
  4. package/client/index.js +7 -2
  5. package/client/logging.js +8 -2
  6. package/client/moduleBootstrap.js +41 -33
  7. package/client/moduleBootstrap.test.js +35 -0
  8. package/client/shellBootstrap.js +38 -10
  9. package/client/shellBootstrap.test.js +85 -6
  10. package/client/shellRouting.js +16 -61
  11. package/client/shellRouting.test.js +30 -1
  12. package/client/vite/clientBootstrapPlugin.js +0 -6
  13. package/client/vite/clientBootstrapPlugin.test.js +2 -3
  14. package/internal/node/installedPackages.js +14 -58
  15. package/package.json +2 -4
  16. package/server/actions/actionCatalogue.js +13 -34
  17. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  18. package/{shared → server}/actions/actionDefinitions.js +24 -131
  19. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  20. package/server/actions/actionEvents.js +97 -2
  21. package/server/actions/actionEvents.test.js +79 -0
  22. package/{shared → server}/actions/executionContext.js +3 -13
  23. package/server/actions/idempotency.js +24 -0
  24. package/server/actions/pipeline.js +87 -0
  25. package/{shared → server}/actions/policies.js +20 -33
  26. package/{shared → server}/actions/policies.test.js +6 -6
  27. package/{shared → server}/actions/registry.js +3 -22
  28. package/{shared → server}/actions/registry.test.js +70 -32
  29. package/server/actions/requestMeta.js +17 -0
  30. package/server/http/capabilityHttpRuntime.js +1 -1
  31. package/server/http/capabilityHttpRuntime.test.js +56 -0
  32. package/server/http/errorHandling.js +291 -0
  33. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  34. package/server/http/lib/routeRegistration.js +36 -150
  35. package/server/http/lib/routeTransport.js +2 -0
  36. package/server/http/lib/routeValidator.js +5 -47
  37. package/server/http/lib/routeValidator.test.js +15 -17
  38. package/server/http/lib/router.js +10 -94
  39. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  40. package/server/platform/providerRuntime/helpers.js +1 -34
  41. package/server/runtime/integers.js +1 -5
  42. package/server/runtime/serviceAuthorization.js +4 -9
  43. package/server/support/appConfig.js +2 -1
  44. package/server/support/appConfigFiles.js +1 -38
  45. package/server/support/appConfigFiles.test.js +3 -39
  46. package/server/support/index.js +1 -20
  47. package/shared/capabilities/defineProvider.js +1 -1
  48. package/shared/capabilities/index.js +1 -1
  49. package/shared/index.js +1 -1
  50. package/shared/support/index.js +1 -4
  51. package/shared/support/mobileConfig.js +99 -0
  52. package/shared/support/mobileConfig.test.js +113 -0
  53. package/shared/support/normalize.js +0 -108
  54. package/shared/support/normalize.test.js +0 -134
  55. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  56. package/shared/support/returnToPath.js +16 -20
  57. package/shared/support/returnToPath.test.js +5 -33
  58. package/shared/support/shellLayoutTargets.js +2 -140
  59. package/shared/support/shellLayoutTargets.test.js +1 -110
  60. package/shared/support/sorting.js +1 -9
  61. package/shared/surface/apiPaths.js +1 -82
  62. package/shared/surface/paths.js +38 -171
  63. package/shared/surface/registry.js +2 -1
  64. package/shared/surface/runtime.js +18 -109
  65. package/shared/surface/runtime.test.js +57 -0
  66. package/shared/validators/index.js +1 -4
  67. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  68. package/shared/validators/recordIdParamsValidator.js +0 -6
  69. package/shared/validators/schemaDefinitions.js +4 -30
  70. package/shared/validators/schemaPayloadValidation.js +2 -6
  71. package/test/barrelExposure.test.js +5 -8
  72. package/test/exportsContract.test.js +1 -3
  73. package/client/mobileLaunchRouting.js +0 -231
  74. package/client/mobileLaunchRouting.test.js +0 -230
  75. package/server/http/lib/controller.js +0 -183
  76. package/server/http/lib/controller.test.js +0 -143
  77. package/server/http/lib/index.js +0 -5
  78. package/server/runtime/canonicalJson.js +0 -74
  79. package/server/runtime/entityChangeEvents.js +0 -307
  80. package/server/runtime/entityChangeEvents.test.js +0 -274
  81. package/server/runtime/fastifyBootstrap.js +0 -553
  82. package/server/runtime/moduleConfig.js +0 -223
  83. package/server/runtime/moduleConfig.test.js +0 -165
  84. package/server/runtime/pagination.js +0 -13
  85. package/server/runtime/realtimeNormalization.js +0 -21
  86. package/server/runtime/routeUtils.js +0 -20
  87. package/server/runtime/securityAudit.js +0 -269
  88. package/server/runtime/securityAudit.test.js +0 -41
  89. package/server/support/pageTargets.js +0 -772
  90. package/server/support/pageTargets.test.js +0 -659
  91. package/server/support/shellOutlets.js +0 -576
  92. package/server/support/shellOutlets.test.js +0 -336
  93. package/server/surface/index.js +0 -18
  94. package/shared/actions/audit.js +0 -7
  95. package/shared/actions/idempotency.js +0 -62
  96. package/shared/actions/observability.js +0 -10
  97. package/shared/actions/pipeline.js +0 -309
  98. package/shared/actions/requestMeta.js +0 -36
  99. package/shared/actions/textNormalization.js +0 -3
  100. package/shared/actions/withActionDefaults.js +0 -34
  101. /package/{shared → server}/actions/executionContext.test.js +0 -0
@@ -1,772 +0,0 @@
1
- import path from "node:path";
2
- import { readFile } from "node:fs/promises";
3
- import { pathToFileURL } from "node:url";
4
- import {
5
- normalizeSurfaceId,
6
- normalizeSurfacePagesRoot
7
- } from "../../shared/surface/index.js";
8
- import {
9
- normalizeObject,
10
- normalizeText
11
- } from "../../shared/support/normalize.js";
12
- import {
13
- discoverShellOutletTargetsFromVueSource,
14
- findShellOutletTargetById,
15
- normalizeShellOutletTargetId
16
- } from "../../shared/support/shellLayoutTargets.js";
17
- import {
18
- discoverPlacementTopologyFromApp,
19
- resolveSemanticPlacementTargetFromApp
20
- } from "./shellOutlets.js";
21
- import { resolveRequiredAppRoot, toPosixPath } from "./path.js";
22
- import { loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
23
-
24
- const DEFAULT_PAGE_LINK_COMPONENT_TOKEN = "local.main.ui.surface-aware-menu-link-item";
25
- const DEFAULT_SUBPAGE_LINK_COMPONENT_TOKEN = "local.main.ui.surface-aware-menu-link-item";
26
- const PAGE_ROOT_PREFIX = "src/pages/";
27
- const ROUTER_VIEW_TAG_PATTERN = /<RouterView\b/i;
28
- const SECTION_CONTAINER_SHELL_TAG_PATTERN = /<SectionContainerShell\b/i;
29
- const TABS_SLOT_PATTERN = /<template\b[^>]*(?:#tabs|v-slot:tabs)\b[^>]*>([\s\S]*?)<\/template>/i;
30
-
31
- function normalizeRelativeFilePath(value = "") {
32
- return String(value || "")
33
- .replaceAll("\\", "/")
34
- .replace(/^\.\/+/, "")
35
- .trim();
36
- }
37
-
38
- function validateVueTargetFile(relativePath = "", { context = "page target" } = {}) {
39
- const normalizedRelativePath = normalizeRelativeFilePath(relativePath);
40
- if (!normalizedRelativePath.endsWith(".vue")) {
41
- throw new Error(`${context} target file must be a .vue file: ${normalizedRelativePath || "<empty>"}.`);
42
- }
43
- return normalizedRelativePath;
44
- }
45
-
46
- function isAbsolutePathInput(value = "") {
47
- const normalizedValue = normalizeRelativeFilePath(value);
48
- if (!normalizedValue) {
49
- return false;
50
- }
51
-
52
- if (normalizedValue.startsWith("/")) {
53
- return true;
54
- }
55
-
56
- return /^[A-Za-z]:\//u.test(normalizedValue);
57
- }
58
-
59
- function resolvePagesRelativeAppPath(
60
- value = "",
61
- {
62
- context = "page target",
63
- label = "target path"
64
- } = {}
65
- ) {
66
- const normalizedValue = normalizeRelativeFilePath(value);
67
- if (!normalizedValue) {
68
- throw new Error(`${context} requires ${label}.`);
69
- }
70
- if (isAbsolutePathInput(normalizedValue)) {
71
- throw new Error(`${context} ${label} must be relative to src/pages/: ${normalizedValue}.`);
72
- }
73
- if (normalizedValue === "src/pages" || normalizedValue === PAGE_ROOT_PREFIX) {
74
- throw new Error(
75
- `${context} ${label} must include a path under src/pages/: ${normalizedValue}.`
76
- );
77
- }
78
- if (normalizedValue.startsWith(PAGE_ROOT_PREFIX)) {
79
- return normalizedValue;
80
- }
81
- if (normalizedValue.startsWith("src/")) {
82
- throw new Error(
83
- `${context} ${label} must be relative to src/pages/ or start with src/pages/: ${normalizedValue}.`
84
- );
85
- }
86
-
87
- return `${PAGE_ROOT_PREFIX}${normalizedValue}`;
88
- }
89
-
90
- function normalizePagesRelativeTargetFile(
91
- targetFile = "",
92
- {
93
- context = "page target",
94
- label = "target file"
95
- } = {}
96
- ) {
97
- return validateVueTargetFile(
98
- resolvePagesRelativeAppPath(targetFile, { context, label }),
99
- { context }
100
- );
101
- }
102
-
103
- function normalizePagesRelativeTargetRoot(
104
- targetRoot = "",
105
- {
106
- context = "page target",
107
- label = "target root"
108
- } = {}
109
- ) {
110
- const normalizedRelativePath = resolvePagesRelativeAppPath(targetRoot, {
111
- context,
112
- label
113
- });
114
- if (normalizedRelativePath.endsWith(".vue")) {
115
- throw new Error(`${context} ${label} must be a route directory, not a .vue file: ${normalizedRelativePath}.`);
116
- }
117
- return normalizedRelativePath;
118
- }
119
-
120
- function splitTextIntoWords(value = "") {
121
- const normalized = String(value || "")
122
- .replace(/^\[|\]$/g, "")
123
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
124
- .replace(/[^A-Za-z0-9]+/g, " ")
125
- .trim();
126
- if (!normalized) {
127
- return [];
128
- }
129
-
130
- return normalized
131
- .split(/\s+/)
132
- .map((entry) => entry.toLowerCase())
133
- .filter(Boolean);
134
- }
135
-
136
- function wordsToKebab(words = []) {
137
- return (Array.isArray(words) ? words : [])
138
- .map((entry) => String(entry || "").toLowerCase())
139
- .filter(Boolean)
140
- .join("-");
141
- }
142
-
143
- function toTitleCase(words = []) {
144
- return (Array.isArray(words) ? words : [])
145
- .map((word) => {
146
- const value = String(word || "");
147
- if (!value) {
148
- return "";
149
- }
150
- return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
151
- })
152
- .filter(Boolean)
153
- .join(" ");
154
- }
155
-
156
- function isRouteGroupSegment(value = "") {
157
- const normalizedValue = normalizeText(value);
158
- return normalizedValue.startsWith("(") && normalizedValue.endsWith(")");
159
- }
160
-
161
- function isIndexRouteSegment(value = "") {
162
- return normalizeText(value).toLowerCase() === "index";
163
- }
164
-
165
- function isPathlessRouteSegment(value = "") {
166
- return isRouteGroupSegment(value) || isIndexRouteSegment(value);
167
- }
168
-
169
- function normalizePlacementIdSegment(value = "") {
170
- return wordsToKebab(splitTextIntoWords(value));
171
- }
172
-
173
- function humanizePageSegment(value = "", fallback = "Page") {
174
- const words = splitTextIntoWords(value);
175
- if (words.length < 1) {
176
- return fallback;
177
- }
178
- return toTitleCase(words);
179
- }
180
-
181
- async function loadPublicConfig(appRoot = "", { context = "page target" } = {}) {
182
- const resolvedAppRoot = resolveRequiredAppRoot(appRoot, { context });
183
- try {
184
- const config = normalizeObject(
185
- await loadAppConfigFromModuleUrl({
186
- moduleUrl: pathToFileURL(path.join(resolvedAppRoot, "config", "public.js")).href
187
- })
188
- );
189
- if (Object.keys(config).length < 1) {
190
- throw new Error("requires exported config in config/public.js.");
191
- }
192
- return config;
193
- } catch (error) {
194
- throw new Error(
195
- `${context} could not load config/public.js: ${String(error?.message || error || "unknown error")}`
196
- );
197
- }
198
- }
199
-
200
- function normalizeSurfaceAccessPolicyId(value = "") {
201
- return normalizeText(value).toLowerCase();
202
- }
203
-
204
- function resolveSurfaceRequiresAuth(surfaceDefinition = {}, surfaceAccessPolicies = {}) {
205
- const normalizedDefinition = normalizeObject(surfaceDefinition);
206
- const normalizedPolicies = normalizeObject(surfaceAccessPolicies);
207
- const accessPolicyId = normalizeSurfaceAccessPolicyId(normalizedDefinition.accessPolicyId);
208
- const configuredPolicy = accessPolicyId
209
- ? normalizeObject(normalizedPolicies[accessPolicyId])
210
- : {};
211
-
212
- if (Object.hasOwn(configuredPolicy, "requireAuth")) {
213
- return configuredPolicy.requireAuth === true;
214
- }
215
-
216
- return normalizedDefinition.requiresAuth === true;
217
- }
218
-
219
- async function listSurfacePageRoots(appRoot = "", { context = "page target" } = {}) {
220
- const config = await loadPublicConfig(appRoot, { context });
221
- const surfaceDefinitions = normalizeObject(config.surfaceDefinitions);
222
- const surfaceAccessPolicies = normalizeObject(config.surfaceAccessPolicies);
223
-
224
- return Object.freeze(
225
- Object.entries(surfaceDefinitions)
226
- .map(([key, value]) => {
227
- const definition = normalizeObject(value);
228
- const surfaceId = normalizeSurfaceId(definition.id || key);
229
- if (!surfaceId || definition.enabled === false) {
230
- return null;
231
- }
232
-
233
- return Object.freeze({
234
- id: surfaceId,
235
- pagesRoot: normalizeSurfacePagesRoot(definition.pagesRoot),
236
- requiresAuth: resolveSurfaceRequiresAuth(definition, surfaceAccessPolicies)
237
- });
238
- })
239
- .filter(Boolean)
240
- );
241
- }
242
-
243
- function deriveSurfaceMatchesFromPageFile(relativePath = "", surfacePageRoots = []) {
244
- const normalizedRelativePath = normalizeRelativeFilePath(relativePath);
245
- if (!normalizedRelativePath.startsWith(PAGE_ROOT_PREFIX)) {
246
- return [];
247
- }
248
-
249
- const pagePathWithinPagesRoot = normalizedRelativePath.slice(PAGE_ROOT_PREFIX.length);
250
- return (Array.isArray(surfacePageRoots) ? surfacePageRoots : [])
251
- .map((surface) => {
252
- const pagesRoot = normalizeSurfacePagesRoot(surface?.pagesRoot);
253
- if (!pagesRoot) {
254
- return Object.freeze({
255
- surfaceId: normalizeSurfaceId(surface?.id),
256
- pagesRoot,
257
- requiresAuth: surface?.requiresAuth === true,
258
- surfaceRelativeFilePath: pagePathWithinPagesRoot
259
- });
260
- }
261
-
262
- const requiredPrefix = `${pagesRoot}/`;
263
- if (!pagePathWithinPagesRoot.startsWith(requiredPrefix)) {
264
- return null;
265
- }
266
-
267
- return Object.freeze({
268
- surfaceId: normalizeSurfaceId(surface?.id),
269
- pagesRoot,
270
- requiresAuth: surface?.requiresAuth === true,
271
- surfaceRelativeFilePath: pagePathWithinPagesRoot.slice(requiredPrefix.length)
272
- });
273
- })
274
- .filter(Boolean);
275
- }
276
-
277
- function compareSurfaceMatchSpecificity(leftMatch = {}, rightMatch = {}) {
278
- const leftPagesRoot = normalizeSurfacePagesRoot(leftMatch?.pagesRoot);
279
- const rightPagesRoot = normalizeSurfacePagesRoot(rightMatch?.pagesRoot);
280
- const leftSegmentCount = leftPagesRoot ? leftPagesRoot.split("/").filter(Boolean).length : 0;
281
- const rightSegmentCount = rightPagesRoot ? rightPagesRoot.split("/").filter(Boolean).length : 0;
282
-
283
- if (leftSegmentCount !== rightSegmentCount) {
284
- return rightSegmentCount - leftSegmentCount;
285
- }
286
- if (leftPagesRoot.length !== rightPagesRoot.length) {
287
- return rightPagesRoot.length - leftPagesRoot.length;
288
- }
289
- return leftPagesRoot.localeCompare(rightPagesRoot);
290
- }
291
-
292
- function resolveBestSurfaceMatchFromPageFile(relativePath = "", surfacePageRoots = [], { context = "page target" } = {}) {
293
- const matches = deriveSurfaceMatchesFromPageFile(relativePath, surfacePageRoots);
294
- if (matches.length < 1) {
295
- return null;
296
- }
297
-
298
- const sortedMatches = [...matches].sort(compareSurfaceMatchSpecificity);
299
- const bestMatch = sortedMatches[0];
300
- const bestPagesRoot = normalizeSurfacePagesRoot(bestMatch?.pagesRoot);
301
- const conflictingMatches = sortedMatches.filter(
302
- (match) => normalizeSurfacePagesRoot(match?.pagesRoot) === bestPagesRoot
303
- );
304
- if (conflictingMatches.length > 1) {
305
- const surfaceIds = conflictingMatches.map((match) => match.surfaceId).filter(Boolean).join(", ");
306
- const pagesRootLabel = bestPagesRoot || "/";
307
- throw new Error(
308
- `${context} target file is ambiguous because multiple surfaces share pagesRoot "${pagesRootLabel}" (${surfaceIds}): ${normalizeRelativeFilePath(relativePath)}.`
309
- );
310
- }
311
-
312
- return bestMatch;
313
- }
314
-
315
- function deriveRouteInfoFromSurfaceRelativeFile(surfaceRelativeFilePath = "", surfaceId = "") {
316
- const normalizedRelativeFilePath = validateVueTargetFile(surfaceRelativeFilePath, {
317
- context: "page target"
318
- });
319
- const withoutExtension = normalizedRelativeFilePath.slice(0, -".vue".length);
320
- const fileSegments = withoutExtension
321
- .split("/")
322
- .map((segment) => normalizeText(segment))
323
- .filter(Boolean);
324
-
325
- const routeSegments = [...fileSegments];
326
- if (routeSegments[routeSegments.length - 1] === "index") {
327
- routeSegments.pop();
328
- }
329
-
330
- const visibleRouteSegments = routeSegments.filter((segment) => !isPathlessRouteSegment(segment));
331
- const routeUrlSuffix = visibleRouteSegments.length > 0 ? `/${visibleRouteSegments.join("/")}` : "/";
332
- const surfacePlacementIdSegment = normalizePlacementIdSegment(surfaceId || "root") || "root";
333
- const placementIdSegments = visibleRouteSegments
334
- .map((segment) => normalizePlacementIdSegment(segment))
335
- .filter(Boolean);
336
- const pageLeafSegment = visibleRouteSegments[visibleRouteSegments.length - 1] || "";
337
- const defaultNameSource = pageLeafSegment || surfaceId || "page";
338
- const defaultName = humanizePageSegment(defaultNameSource, "Page");
339
-
340
- return Object.freeze({
341
- fileSegments,
342
- routeSegments,
343
- visibleRouteSegments,
344
- routeUrlSuffix,
345
- pageLeafSegment,
346
- defaultName,
347
- placementId:
348
- placementIdSegments.length > 0
349
- ? `ui-generator.page.${surfacePlacementIdSegment}.${placementIdSegments.join(".")}.link`
350
- : `ui-generator.page.${surfacePlacementIdSegment}.link`
351
- });
352
- }
353
-
354
- function buildRouteUrlSuffixFromVisibleSegments(segments = []) {
355
- const visibleSegments = (Array.isArray(segments) ? segments : [])
356
- .map((segment) => normalizeText(segment))
357
- .filter(Boolean);
358
- return visibleSegments.length > 0 ? `/${visibleSegments.join("/")}` : "/";
359
- }
360
-
361
- function buildAncestorRouteContexts(pageTarget = {}) {
362
- const routeSegments = Array.isArray(pageTarget?.routeSegments)
363
- ? pageTarget.routeSegments
364
- : [];
365
- const visibleRouteSegments = Array.isArray(pageTarget?.visibleRouteSegments)
366
- ? pageTarget.visibleRouteSegments
367
- : [];
368
- if (visibleRouteSegments.length < 2) {
369
- return [];
370
- }
371
-
372
- const ancestors = [];
373
-
374
- for (let visiblePrefixLength = visibleRouteSegments.length - 1; visiblePrefixLength >= 1; visiblePrefixLength -= 1) {
375
- const parentVisibleSegments = visibleRouteSegments.slice(0, visiblePrefixLength);
376
- const actualRouteSegments = [];
377
- let collectedVisibleSegments = 0;
378
-
379
- for (const segment of routeSegments) {
380
- actualRouteSegments.push(segment);
381
- if (!isPathlessRouteSegment(segment)) {
382
- collectedVisibleSegments += 1;
383
- }
384
- if (collectedVisibleSegments >= visiblePrefixLength) {
385
- break;
386
- }
387
- }
388
-
389
- if (collectedVisibleSegments !== visiblePrefixLength) {
390
- continue;
391
- }
392
-
393
- const nextRouteSegment = normalizeText(routeSegments[actualRouteSegments.length]);
394
- ancestors.push(
395
- Object.freeze({
396
- visibleRouteSegments: parentVisibleSegments,
397
- actualRouteSegments,
398
- childUsesIndexRouteOwner: isIndexRouteSegment(nextRouteSegment)
399
- })
400
- );
401
- }
402
-
403
- return ancestors;
404
- }
405
-
406
- function buildParentPageFileCandidates(pageTarget = {}, ancestorRoute = {}) {
407
- const surfacePagesRootSegments = normalizeRelativeFilePath(pageTarget?.surfacePagesRoot)
408
- .split("/")
409
- .map((segment) => normalizeText(segment))
410
- .filter(Boolean);
411
- const routeSegments = (Array.isArray(ancestorRoute?.actualRouteSegments) ? ancestorRoute.actualRouteSegments : [])
412
- .map((segment) => normalizeText(segment))
413
- .filter(Boolean);
414
- if (routeSegments.length < 1) {
415
- return [];
416
- }
417
-
418
- const baseSegments = ["src/pages", ...surfacePagesRootSegments, ...routeSegments];
419
- const fileRoutePath = `${baseSegments.join("/")}.vue`;
420
- const indexRoutePath = [...baseSegments, "index.vue"].join("/");
421
- const preferredCandidates = ancestorRoute?.childUsesIndexRouteOwner === true
422
- ? [indexRoutePath, fileRoutePath]
423
- : [fileRoutePath, indexRoutePath];
424
-
425
- return preferredCandidates.map((relativePath) =>
426
- Object.freeze({
427
- relativePath,
428
- pageShape: relativePath.endsWith("/index.vue") ? "index" : "file"
429
- })
430
- );
431
- }
432
-
433
- function resolveSubpagesHostTargetFromPageSource(source = "") {
434
- const sourceText = String(source || "");
435
- if (!ROUTER_VIEW_TAG_PATTERN.test(sourceText)) {
436
- return null;
437
- }
438
- const discoveredTargets = discoverShellOutletTargetsFromVueSource(sourceText, {
439
- context: "subpages host"
440
- });
441
- const targets = Array.isArray(discoveredTargets.targets) ? discoveredTargets.targets : [];
442
- if (targets.length !== 1) {
443
- return null;
444
- }
445
-
446
- const target = findShellOutletTargetById(targets, targets[0]?.id);
447
- if (!target) {
448
- return null;
449
- }
450
-
451
- let isSectionSubpagesHost = false;
452
- if (SECTION_CONTAINER_SHELL_TAG_PATTERN.test(sourceText)) {
453
- const tabsSlotMatch = TABS_SLOT_PATTERN.exec(sourceText);
454
- if (tabsSlotMatch) {
455
- const tabsTargets = discoverShellOutletTargetsFromVueSource(String(tabsSlotMatch[1] || ""), {
456
- context: "subpages host tabs slot"
457
- });
458
- const tabTargetIds = Array.isArray(tabsTargets.targets)
459
- ? tabsTargets.targets.map((entry) => normalizeShellOutletTargetId(entry?.id)).filter(Boolean)
460
- : [];
461
- if (tabTargetIds.length === 1 && tabTargetIds[0] === normalizeShellOutletTargetId(target.id)) {
462
- isSectionSubpagesHost = true;
463
- }
464
- }
465
- }
466
-
467
- return Object.freeze({
468
- id: target.id,
469
- isSectionSubpagesHost
470
- });
471
- }
472
-
473
- async function resolvePageTargetDetails({
474
- appRoot,
475
- targetFile = "",
476
- context = "page target"
477
- } = {}) {
478
- const resolvedAppRoot = resolveRequiredAppRoot(appRoot, { context });
479
- const normalizedRelativePath = normalizePagesRelativeTargetFile(targetFile, {
480
- context,
481
- label: "target file"
482
- });
483
-
484
- const surfacePageRoots = await listSurfacePageRoots(resolvedAppRoot, { context });
485
- const surfaceMatch = resolveBestSurfaceMatchFromPageFile(normalizedRelativePath, surfacePageRoots, { context });
486
- if (!surfaceMatch) {
487
- throw new Error(
488
- `${context} target file must be relative to src/pages/ and resolve to a configured surface: ${normalizeRelativeFilePath(targetFile)}.`
489
- );
490
- }
491
- const routeInfo = deriveRouteInfoFromSurfaceRelativeFile(surfaceMatch.surfaceRelativeFilePath, surfaceMatch.surfaceId);
492
- const absolutePath = path.resolve(resolvedAppRoot, normalizedRelativePath);
493
-
494
- return Object.freeze({
495
- appRoot: resolvedAppRoot,
496
- targetFilePath: Object.freeze({
497
- absolutePath,
498
- relativePath: normalizedRelativePath
499
- }),
500
- surfaceId: surfaceMatch.surfaceId,
501
- surfacePagesRoot: surfaceMatch.pagesRoot,
502
- surfaceRequiresAuth: surfaceMatch.requiresAuth === true,
503
- surfaceRelativeFilePath: surfaceMatch.surfaceRelativeFilePath,
504
- ...routeInfo
505
- });
506
- }
507
-
508
- function deriveDefaultSubpagesHost(pageTarget = {}) {
509
- const visibleRouteSegments = Array.isArray(pageTarget?.visibleRouteSegments)
510
- ? pageTarget.visibleRouteSegments
511
- : [];
512
- const hostSegments = visibleRouteSegments
513
- .map((segment) => normalizePlacementIdSegment(segment))
514
- .filter(Boolean);
515
-
516
- if (hostSegments.length > 0) {
517
- return hostSegments.join("-");
518
- }
519
-
520
- return normalizePlacementIdSegment(pageTarget?.surfaceId || "page") || "page";
521
- }
522
-
523
- async function resolveNearestParentSubpagesHost({
524
- appRoot,
525
- pageTarget = {},
526
- context = "page target"
527
- } = {}) {
528
- const resolvedAppRoot = resolveRequiredAppRoot(appRoot, { context });
529
- const ancestorRoutes = buildAncestorRouteContexts(pageTarget);
530
- if (ancestorRoutes.length < 1) {
531
- return null;
532
- }
533
-
534
- for (const ancestorRoute of ancestorRoutes) {
535
- const candidatePages = buildParentPageFileCandidates(pageTarget, ancestorRoute);
536
-
537
- for (const candidatePage of candidatePages) {
538
- const candidatePath = path.resolve(resolvedAppRoot, candidatePage.relativePath);
539
- let source = "";
540
- try {
541
- source = await readFile(candidatePath, "utf8");
542
- } catch {
543
- continue;
544
- }
545
-
546
- const target = resolveSubpagesHostTargetFromPageSource(source);
547
- if (!target) {
548
- continue;
549
- }
550
-
551
- return Object.freeze({
552
- ...target,
553
- pageFile: toPosixPath(path.relative(resolvedAppRoot, candidatePath)),
554
- pageShape: candidatePage.pageShape,
555
- visibleRouteSegments: ancestorRoute.visibleRouteSegments,
556
- routeUrlSuffix: buildRouteUrlSuffixFromVisibleSegments(ancestorRoute.visibleRouteSegments)
557
- });
558
- }
559
- }
560
-
561
- return null;
562
- }
563
-
564
- function normalizePlacementTargetId(target = {}) {
565
- return normalizeShellOutletTargetId(target?.id || target?.target || target);
566
- }
567
-
568
- function resolveConcreteTargetOwner(target = "") {
569
- const normalizedTarget = normalizeShellOutletTargetId(target);
570
- if (!normalizedTarget) {
571
- return "";
572
- }
573
- return normalizedTarget.slice(0, normalizedTarget.indexOf(":"));
574
- }
575
-
576
- function topologyPlacementTargetsConcreteOutlet(placement = {}, target = "") {
577
- const normalizedTarget = normalizeShellOutletTargetId(target);
578
- if (!normalizedTarget) {
579
- return false;
580
- }
581
- const variants = placement?.variants && typeof placement.variants === "object" ? placement.variants : {};
582
- return Object.values(variants).some((variant) => normalizeShellOutletTargetId(variant?.outlet) === normalizedTarget);
583
- }
584
-
585
- async function resolveSemanticPlacementTargetForConcreteOutlet({
586
- appRoot,
587
- concreteTarget = "",
588
- surface = "",
589
- context = "page target"
590
- } = {}) {
591
- const normalizedConcreteTarget = normalizeShellOutletTargetId(concreteTarget);
592
- if (!normalizedConcreteTarget) {
593
- return null;
594
- }
595
-
596
- const topology = await discoverPlacementTopologyFromApp({ appRoot });
597
- const owner = resolveConcreteTargetOwner(normalizedConcreteTarget);
598
- const normalizedSurface = normalizeSurfaceId(surface);
599
- const candidates = (Array.isArray(topology.placements) ? topology.placements : []).filter((placement) => {
600
- if (!topologyPlacementTargetsConcreteOutlet(placement, normalizedConcreteTarget)) {
601
- return false;
602
- }
603
- const surfaces = Array.isArray(placement.surfaces) ? placement.surfaces : ["*"];
604
- return !normalizedSurface || surfaces.includes("*") || surfaces.includes(normalizedSurface);
605
- });
606
-
607
- const exactOwnerMatches = candidates.filter((placement) => placement.owner && placement.owner === owner);
608
- const globalMatches = candidates.filter((placement) => !placement.owner);
609
- const selectedMatches = exactOwnerMatches.length > 0 ? exactOwnerMatches : globalMatches;
610
- if (selectedMatches.length < 1) {
611
- return null;
612
- }
613
- if (selectedMatches.length === 1) {
614
- return selectedMatches[0];
615
- }
616
-
617
- const targetLabels = selectedMatches.map((placement) =>
618
- placement.owner ? `${placement.id} [owner:${placement.owner}]` : placement.id
619
- );
620
- throw new Error(
621
- `${context} found multiple semantic placements mapped to concrete outlet "${normalizedConcreteTarget}": ` +
622
- `${targetLabels.join(", ")}. Pass --link-placement or make the topology mapping unambiguous.`
623
- );
624
- }
625
-
626
- function resolveRelativeLinkToFromParent(pageTarget = {}, parentHost = null) {
627
- const childSegments = Array.isArray(pageTarget?.visibleRouteSegments) ? pageTarget.visibleRouteSegments : [];
628
- const parentSegments = Array.isArray(parentHost?.visibleRouteSegments) ? parentHost.visibleRouteSegments : [];
629
- if (parentSegments.length < 1 || childSegments.length <= parentSegments.length) {
630
- return "";
631
- }
632
-
633
- const relativeSegments = childSegments.slice(parentSegments.length);
634
- if (relativeSegments.length < 1) {
635
- return "";
636
- }
637
-
638
- return `./${relativeSegments.join("/")}`;
639
- }
640
-
641
- function resolveRelativeLinkToFromNearestIndexOwner(pageTarget = {}) {
642
- const routeSegments = Array.isArray(pageTarget?.routeSegments) ? pageTarget.routeSegments : [];
643
- const deepestIndexOwnerIndex = routeSegments.findLastIndex((segment) => isIndexRouteSegment(segment));
644
- if (deepestIndexOwnerIndex < 0 || deepestIndexOwnerIndex >= routeSegments.length - 1) {
645
- return "";
646
- }
647
-
648
- const relativeSegments = routeSegments
649
- .slice(deepestIndexOwnerIndex + 1)
650
- .filter((segment) => !isPathlessRouteSegment(segment));
651
- if (relativeSegments.length < 1) {
652
- return "";
653
- }
654
-
655
- return `./${relativeSegments.join("/")}`;
656
- }
657
-
658
- function resolveInferredPageLinkTo({
659
- explicitLinkTo = "",
660
- pageTarget = {},
661
- parentHost = null,
662
- preservesRelativeSubpageLinks = false,
663
- suppressImplicitRelativeLinks = false
664
- } = {}) {
665
- const normalizedExplicitLinkTo = normalizeText(explicitLinkTo);
666
- if (normalizedExplicitLinkTo) {
667
- return normalizedExplicitLinkTo;
668
- }
669
- if (suppressImplicitRelativeLinks === true) {
670
- return "";
671
- }
672
-
673
- if (preservesRelativeSubpageLinks === true && parentHost?.isSectionSubpagesHost === true) {
674
- const inferredLinkTo = resolveRelativeLinkToFromParent(pageTarget, parentHost);
675
- if (inferredLinkTo) {
676
- return inferredLinkTo;
677
- }
678
- }
679
-
680
- const inferredLinkTo = resolveRelativeLinkToFromNearestIndexOwner(pageTarget);
681
- if (inferredLinkTo) {
682
- return inferredLinkTo;
683
- }
684
- return "";
685
- }
686
-
687
- function renderPageLinkWhenLine(pageTarget = {}) {
688
- if (pageTarget?.surfaceRequiresAuth !== true) {
689
- return "";
690
- }
691
-
692
- return " when: ({ auth }) => auth?.authenticated === true\n";
693
- }
694
-
695
- async function resolvePageLinkTargetDetails({
696
- appRoot,
697
- targetFile = "",
698
- pageTarget = null,
699
- placement = "",
700
- componentToken = "",
701
- linkTo = "",
702
- context = "page target"
703
- } = {}) {
704
- const resolvedPageTarget = pageTarget || await resolvePageTargetDetails({
705
- appRoot,
706
- targetFile,
707
- context
708
- });
709
- const parentHost = await resolveNearestParentSubpagesHost({
710
- appRoot: resolvedPageTarget.appRoot,
711
- pageTarget: resolvedPageTarget,
712
- context
713
- });
714
- const parentTargetId = normalizePlacementTargetId(parentHost);
715
- let placementTarget = null;
716
- const explicitPlacement = normalizeText(placement);
717
- if (explicitPlacement) {
718
- placementTarget = await resolveSemanticPlacementTargetFromApp({
719
- appRoot: resolvedPageTarget.appRoot,
720
- context,
721
- placement: explicitPlacement,
722
- owner: parentTargetId ? resolveConcreteTargetOwner(parentTargetId) : "",
723
- surface: resolvedPageTarget.surfaceId
724
- });
725
- } else if (parentTargetId) {
726
- placementTarget = await resolveSemanticPlacementTargetForConcreteOutlet({
727
- appRoot: resolvedPageTarget.appRoot,
728
- concreteTarget: parentTargetId,
729
- surface: resolvedPageTarget.surfaceId,
730
- context
731
- });
732
- }
733
- if (!placementTarget) {
734
- placementTarget = await resolveSemanticPlacementTargetFromApp({
735
- appRoot: resolvedPageTarget.appRoot,
736
- context,
737
- surface: resolvedPageTarget.surfaceId
738
- });
739
- }
740
- const preservesRelativeSubpageLinks =
741
- parentHost?.isSectionSubpagesHost === true &&
742
- Boolean(parentTargetId) &&
743
- topologyPlacementTargetsConcreteOutlet(placementTarget, parentTargetId);
744
-
745
- return Object.freeze({
746
- pageTarget: resolvedPageTarget,
747
- parentHost,
748
- placementTarget,
749
- componentToken: normalizeText(componentToken),
750
- whenLine: renderPageLinkWhenLine(resolvedPageTarget),
751
- linkTo: resolveInferredPageLinkTo({
752
- explicitLinkTo: linkTo,
753
- pageTarget: resolvedPageTarget,
754
- parentHost,
755
- preservesRelativeSubpageLinks,
756
- suppressImplicitRelativeLinks: preservesRelativeSubpageLinks !== true
757
- })
758
- });
759
- }
760
-
761
- export {
762
- DEFAULT_PAGE_LINK_COMPONENT_TOKEN,
763
- DEFAULT_SUBPAGE_LINK_COMPONENT_TOKEN,
764
- listSurfacePageRoots,
765
- normalizePagesRelativeTargetFile,
766
- normalizePagesRelativeTargetRoot,
767
- resolveBestSurfaceMatchFromPageFile,
768
- resolvePageTargetDetails,
769
- deriveDefaultSubpagesHost,
770
- resolveNearestParentSubpagesHost,
771
- resolvePageLinkTargetDetails
772
- };