@jskit-ai/kernel 0.1.245 → 0.1.247

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 (102) hide show
  1. package/client/appConfig.js +2 -1
  2. package/client/asyncModuleRecovery.js +2 -4
  3. package/client/index.d.ts +13 -75
  4. package/client/index.js +1 -1
  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 +1 -8
  9. package/client/shellBootstrap.test.js +32 -5
  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 +19 -62
  15. package/internal/node/installedPackages.test.js +33 -0
  16. package/package.json +2 -4
  17. package/server/actions/actionCatalogue.js +13 -34
  18. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  19. package/{shared → server}/actions/actionDefinitions.js +24 -131
  20. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  21. package/server/actions/actionEvents.js +97 -2
  22. package/server/actions/actionEvents.test.js +79 -0
  23. package/{shared → server}/actions/executionContext.js +3 -13
  24. package/server/actions/idempotency.js +24 -0
  25. package/server/actions/pipeline.js +87 -0
  26. package/{shared → server}/actions/policies.js +20 -33
  27. package/{shared → server}/actions/policies.test.js +6 -6
  28. package/{shared → server}/actions/registry.js +3 -22
  29. package/{shared → server}/actions/registry.test.js +70 -32
  30. package/server/actions/requestMeta.js +17 -0
  31. package/server/http/capabilityHttpRuntime.js +1 -1
  32. package/server/http/capabilityHttpRuntime.test.js +56 -0
  33. package/server/http/errorHandling.js +291 -0
  34. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  35. package/server/http/lib/routeRegistration.js +36 -150
  36. package/server/http/lib/routeTransport.js +2 -0
  37. package/server/http/lib/routeValidator.js +5 -47
  38. package/server/http/lib/routeValidator.test.js +15 -17
  39. package/server/http/lib/router.js +10 -94
  40. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  41. package/server/platform/providerRuntime/helpers.js +1 -34
  42. package/server/runtime/integers.js +1 -5
  43. package/server/runtime/serviceAuthorization.js +4 -9
  44. package/server/support/appConfig.js +2 -1
  45. package/server/support/appConfigFiles.js +1 -38
  46. package/server/support/appConfigFiles.test.js +3 -39
  47. package/server/support/index.js +1 -20
  48. package/shared/capabilities/defineProvider.js +1 -1
  49. package/shared/capabilities/index.js +1 -1
  50. package/shared/index.js +1 -1
  51. package/shared/support/index.js +1 -4
  52. package/shared/support/mobileConfig.js +99 -0
  53. package/shared/support/mobileConfig.test.js +113 -0
  54. package/shared/support/normalize.js +0 -108
  55. package/shared/support/normalize.test.js +0 -134
  56. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  57. package/shared/support/returnToPath.js +1 -20
  58. package/shared/support/returnToPath.test.js +1 -36
  59. package/shared/support/shellLayoutTargets.js +2 -140
  60. package/shared/support/shellLayoutTargets.test.js +1 -110
  61. package/shared/support/sorting.js +1 -9
  62. package/shared/surface/apiPaths.js +1 -82
  63. package/shared/surface/paths.js +38 -171
  64. package/shared/surface/registry.js +2 -1
  65. package/shared/surface/runtime.js +18 -109
  66. package/shared/surface/runtime.test.js +57 -0
  67. package/shared/validators/index.js +1 -4
  68. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  69. package/shared/validators/recordIdParamsValidator.js +0 -6
  70. package/shared/validators/schemaDefinitions.js +4 -30
  71. package/shared/validators/schemaPayloadValidation.js +2 -6
  72. package/test/barrelExposure.test.js +3 -7
  73. package/test/exportsContract.test.js +1 -3
  74. package/client/mobileLaunchRouting.js +0 -231
  75. package/client/mobileLaunchRouting.test.js +0 -230
  76. package/server/http/lib/controller.js +0 -183
  77. package/server/http/lib/controller.test.js +0 -143
  78. package/server/http/lib/index.js +0 -5
  79. package/server/runtime/canonicalJson.js +0 -74
  80. package/server/runtime/entityChangeEvents.js +0 -307
  81. package/server/runtime/entityChangeEvents.test.js +0 -274
  82. package/server/runtime/fastifyBootstrap.js +0 -553
  83. package/server/runtime/moduleConfig.js +0 -223
  84. package/server/runtime/moduleConfig.test.js +0 -165
  85. package/server/runtime/pagination.js +0 -13
  86. package/server/runtime/realtimeNormalization.js +0 -21
  87. package/server/runtime/routeUtils.js +0 -20
  88. package/server/runtime/securityAudit.js +0 -269
  89. package/server/runtime/securityAudit.test.js +0 -41
  90. package/server/support/pageTargets.js +0 -772
  91. package/server/support/pageTargets.test.js +0 -659
  92. package/server/support/shellOutlets.js +0 -576
  93. package/server/support/shellOutlets.test.js +0 -336
  94. package/server/surface/index.js +0 -18
  95. package/shared/actions/audit.js +0 -7
  96. package/shared/actions/idempotency.js +0 -62
  97. package/shared/actions/observability.js +0 -10
  98. package/shared/actions/pipeline.js +0 -309
  99. package/shared/actions/requestMeta.js +0 -36
  100. package/shared/actions/textNormalization.js +0 -3
  101. package/shared/actions/withActionDefaults.js +0 -34
  102. /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
- };