@jskit-ai/kernel 0.1.65 → 0.1.67

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.
@@ -8,13 +8,15 @@ import {
8
8
  describeShellOutletTargets,
9
9
  discoverShellOutletTargetsFromVueSource,
10
10
  findShellOutletTargetById,
11
+ normalizePlacementTopologyDefinition,
12
+ normalizeSemanticPlacementId,
11
13
  normalizeShellOutletTargetId,
12
14
  normalizeShellOutletTargetRecord
13
15
  } from "../../shared/support/shellLayoutTargets.js";
14
- import { loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
15
16
 
16
17
  const VUE_DISCOVERY_IGNORED_ERROR_CODES = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"]);
17
18
  const LOCK_FILE_RELATIVE_PATH = ".jskit/lock.json";
19
+ const PLACEMENT_TOPOLOGY_RELATIVE_PATH = "src/placementTopology.js";
18
20
  const ROUTE_TAG_PATTERN = /<route\b([^>]*)>([\s\S]*?)<\/route>/g;
19
21
  const ATTRIBUTE_PATTERN = /([:@]?[A-Za-z_][A-Za-z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
20
22
 
@@ -68,7 +70,13 @@ function normalizeAppRouteOutletTarget({
68
70
  });
69
71
  }
70
72
 
71
- function discoverRouteMetaOutletTargetsFromVueSource(source = "", { context = "shell layout" } = {}) {
73
+ function discoverRouteMetaOutletTargetsFromVueSource(
74
+ source = "",
75
+ {
76
+ context = "shell layout",
77
+ enforceSingleDefault = true
78
+ } = {}
79
+ ) {
72
80
  const sourceText = String(source || "");
73
81
  const resolvedContext = normalizeText(context) || "shell layout";
74
82
  const targetById = new Map();
@@ -111,12 +119,14 @@ function discoverRouteMetaOutletTargetsFromVueSource(source = "", { context = "s
111
119
  throw new Error(`${resolvedContext} contains duplicate route meta placement target "${normalizedTarget.id}".`);
112
120
  }
113
121
  if (normalizedTarget.default === true) {
114
- if (defaultTargetId && defaultTargetId !== normalizedTarget.id) {
122
+ if (enforceSingleDefault === true && defaultTargetId && defaultTargetId !== normalizedTarget.id) {
115
123
  throw new Error(
116
124
  `${resolvedContext} defines multiple default route meta placement targets: "${defaultTargetId}" and "${normalizedTarget.id}".`
117
125
  );
118
126
  }
119
- defaultTargetId = normalizedTarget.id;
127
+ if (!defaultTargetId) {
128
+ defaultTargetId = normalizedTarget.id;
129
+ }
120
130
  }
121
131
  targetById.set(normalizedTarget.id, normalizedTarget);
122
132
  }
@@ -218,46 +228,6 @@ function normalizePackageOutletTarget({
218
228
  });
219
229
  }
220
230
 
221
- async function loadOutletDefaultOverrides(appRoot = "") {
222
- const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
223
- context: "discoverShellOutletTargetsFromApp"
224
- });
225
- let appConfig = {};
226
- try {
227
- appConfig = normalizeObject(
228
- await loadAppConfigFromModuleUrl({
229
- moduleUrl: pathToFileURL(path.join(resolvedAppRoot, "config", "public.js")).href
230
- })
231
- );
232
- } catch {
233
- return {};
234
- }
235
- return normalizeObject(normalizeObject(appConfig.ui).outletDefaults);
236
- }
237
-
238
- function applyOutletDefaultOverrides(target = {}, outletDefaultOverrides = {}) {
239
- const targetRecord = normalizeObject(target);
240
- const outletTargetId = normalizeShellOutletTargetId(targetRecord.id);
241
- if (!outletTargetId) {
242
- return targetRecord;
243
- }
244
-
245
- const overrideRecord = outletDefaultOverrides?.[outletTargetId];
246
- const normalizedOverrideToken =
247
- typeof overrideRecord === "string"
248
- ? normalizeText(overrideRecord)
249
- : normalizeText(normalizeObject(overrideRecord).linkComponentToken) ||
250
- normalizeText(normalizeObject(overrideRecord)["link-component-token"]);
251
- if (!normalizedOverrideToken) {
252
- return targetRecord;
253
- }
254
-
255
- return Object.freeze({
256
- ...targetRecord,
257
- defaultLinkComponentToken: normalizedOverrideToken
258
- });
259
- }
260
-
261
231
  async function collectInstalledPackageOutletTargets(appRoot) {
262
232
  const installedPackageStates = await readInstalledPackageStates(appRoot);
263
233
  const packageIds = Object.keys(installedPackageStates).sort((left, right) => left.localeCompare(right));
@@ -290,11 +260,185 @@ async function collectInstalledPackageOutletTargets(appRoot) {
290
260
  return targets;
291
261
  }
292
262
 
293
- async function discoverShellOutletTargetsFromApp({ appRoot, sourceRoot = "src" } = {}) {
263
+ function withTopologySource(placement = {}, sourcePath = "") {
264
+ return Object.freeze({
265
+ ...placement,
266
+ sourcePath
267
+ });
268
+ }
269
+
270
+ async function loadAppPlacementTopology(appRoot) {
271
+ const topologyPath = path.resolve(appRoot, PLACEMENT_TOPOLOGY_RELATIVE_PATH);
272
+ try {
273
+ await readFile(topologyPath, "utf8");
274
+ } catch (error) {
275
+ const errorCode = normalizeText(error?.code).toUpperCase();
276
+ if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
277
+ return [];
278
+ }
279
+ throw error;
280
+ }
281
+
282
+ let moduleNamespace = null;
283
+ try {
284
+ moduleNamespace = await import(pathToFileURL(topologyPath).href);
285
+ } catch (error) {
286
+ throw new Error(
287
+ `Could not load ${PLACEMENT_TOPOLOGY_RELATIVE_PATH}: ${String(error?.message || error || "unknown error")}`
288
+ );
289
+ }
290
+
291
+ const exported = moduleNamespace?.default;
292
+ const resolved = typeof exported === "function" ? exported() : exported;
293
+ const normalized = normalizePlacementTopologyDefinition(resolved, {
294
+ context: PLACEMENT_TOPOLOGY_RELATIVE_PATH
295
+ });
296
+ return normalized.placements.map((placement) => withTopologySource(placement, PLACEMENT_TOPOLOGY_RELATIVE_PATH));
297
+ }
298
+
299
+ async function collectInstalledPackagePlacementTopology(appRoot) {
300
+ const installedPackageStates = await readInstalledPackageStates(appRoot);
301
+ const packageIds = Object.keys(installedPackageStates).sort((left, right) => left.localeCompare(right));
302
+ const placements = [];
303
+
304
+ for (const packageId of packageIds) {
305
+ const installedPackageState = normalizeObject(installedPackageStates[packageId]);
306
+ const descriptorRecord = await loadInstalledPackageDescriptor({
307
+ appRoot,
308
+ packageId,
309
+ installedPackageState
310
+ });
311
+ const descriptor = normalizeObject(descriptorRecord.descriptor);
312
+ const metadata = normalizeObject(descriptor.metadata);
313
+ const ui = normalizeObject(metadata.ui);
314
+ const placementsMeta = normalizeObject(ui.placements);
315
+ const topology = placementsMeta.topology;
316
+ if (!topology) {
317
+ continue;
318
+ }
319
+
320
+ const normalized = normalizePlacementTopologyDefinition(topology, {
321
+ context: `package:${packageId}:metadata.ui.placements.topology`
322
+ });
323
+ for (const placement of normalized.placements) {
324
+ placements.push(
325
+ withTopologySource(
326
+ placement,
327
+ `package:${packageId}${descriptorRecord.descriptorPath ? `:${toPosixPath(descriptorRecord.descriptorPath)}` : ""}`
328
+ )
329
+ );
330
+ }
331
+ }
332
+
333
+ return placements;
334
+ }
335
+
336
+ async function discoverPlacementTopologyFromApp({ appRoot } = {}) {
294
337
  const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
295
- context: "discoverShellOutletTargetsFromApp"
338
+ context: "discoverPlacementTopologyFromApp"
339
+ });
340
+ const appPlacements = await loadAppPlacementTopology(resolvedAppRoot);
341
+ const packagePlacements = await collectInstalledPackagePlacementTopology(resolvedAppRoot);
342
+ const placementByKey = new Map();
343
+
344
+ for (const placement of [...packagePlacements, ...appPlacements]) {
345
+ const key = `${placement.id}::${placement.owner || ""}`;
346
+ placementByKey.set(key, placement);
347
+ }
348
+
349
+ return Object.freeze({
350
+ placements: Object.freeze(
351
+ [...placementByKey.values()].sort((left, right) => {
352
+ const idCompare = left.id.localeCompare(right.id);
353
+ if (idCompare !== 0) {
354
+ return idCompare;
355
+ }
356
+ return String(left.owner || "").localeCompare(String(right.owner || ""));
357
+ })
358
+ )
359
+ });
360
+ }
361
+
362
+ function findSemanticPlacementById(placements = [], { id = "", owner = "", surface = "" } = {}) {
363
+ const normalizedId = normalizeSemanticPlacementId(id);
364
+ const normalizedOwner = normalizeText(owner).toLowerCase();
365
+ const normalizedSurface = normalizeText(surface).toLowerCase();
366
+ if (!normalizedId) {
367
+ return null;
368
+ }
369
+
370
+ return (Array.isArray(placements) ? placements : []).find((placement) => {
371
+ if (placement.id !== normalizedId) {
372
+ return false;
373
+ }
374
+ if (normalizedOwner && placement.owner !== normalizedOwner) {
375
+ return false;
376
+ }
377
+ if (!normalizedOwner && placement.owner) {
378
+ return false;
379
+ }
380
+ const surfaces = Array.isArray(placement.surfaces) ? placement.surfaces : ["*"];
381
+ return !normalizedSurface || surfaces.includes("*") || surfaces.includes(normalizedSurface);
382
+ }) || null;
383
+ }
384
+
385
+ async function resolveSemanticPlacementTargetFromApp({
386
+ appRoot,
387
+ placement = "",
388
+ owner = "",
389
+ surface = "",
390
+ context = "ui-generator"
391
+ } = {}) {
392
+ const resolvedContext = normalizeText(context) || "ui-generator";
393
+ const topology = await discoverPlacementTopologyFromApp({ appRoot });
394
+ const placements = Array.isArray(topology.placements) ? topology.placements : [];
395
+ if (placements.length < 1) {
396
+ throw new Error(`${resolvedContext} could not find semantic placement topology in ${PLACEMENT_TOPOLOGY_RELATIVE_PATH}.`);
397
+ }
398
+
399
+ const requestedPlacement = normalizeText(placement);
400
+ if (requestedPlacement) {
401
+ const requestedId = normalizeSemanticPlacementId(requestedPlacement);
402
+ if (!requestedId) {
403
+ throw new Error(`${resolvedContext} option "placement" must be a semantic target in "area.slot" format.`);
404
+ }
405
+ const match = findSemanticPlacementById(placements, {
406
+ id: requestedId,
407
+ owner,
408
+ surface
409
+ }) || (owner
410
+ ? findSemanticPlacementById(placements, {
411
+ id: requestedId,
412
+ owner: "",
413
+ surface
414
+ })
415
+ : null);
416
+ if (!match) {
417
+ throw new Error(`${resolvedContext} semantic placement "${requestedId}" is not declared in app placement topology.`);
418
+ }
419
+ return match;
420
+ }
421
+
422
+ const defaultPlacement =
423
+ placements.find((entry) => entry.default === true && (!surface || entry.surfaces.includes("*") || entry.surfaces.includes(surface))) ||
424
+ placements.find((entry) => !entry.owner && (!surface || entry.surfaces.includes("*") || entry.surfaces.includes(surface))) ||
425
+ null;
426
+ if (defaultPlacement) {
427
+ return defaultPlacement;
428
+ }
429
+
430
+ throw new Error(`${resolvedContext} could not resolve a default semantic placement target.`);
431
+ }
432
+
433
+ async function collectAppSourceShellOutletTargets({
434
+ appRoot,
435
+ sourceRoot = "src",
436
+ enforceSingleDefault = true,
437
+ context = "discoverShellOutletTargetsFromApp"
438
+ } = {}) {
439
+ const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
440
+ context
296
441
  });
297
- const outletDefaultOverrides = await loadOutletDefaultOverrides(resolvedAppRoot);
298
442
 
299
443
  const sourceDirectory = path.resolve(resolvedAppRoot, String(sourceRoot || "src"));
300
444
  const targetById = new Map();
@@ -311,12 +455,14 @@ async function discoverShellOutletTargetsFromApp({ appRoot, sourceRoot = "src" }
311
455
 
312
456
  const discoveredShellOutlets = source.includes("<ShellOutlet")
313
457
  ? discoverShellOutletTargetsFromVueSource(source, {
314
- context: relativePath
458
+ context: relativePath,
459
+ enforceSingleDefault
315
460
  })
316
461
  : { targets: [], defaultTargetId: "" };
317
462
  const discoveredRouteMetaOutlets = source.includes("<route")
318
463
  ? discoverRouteMetaOutletTargetsFromVueSource(source, {
319
- context: relativePath
464
+ context: relativePath,
465
+ enforceSingleDefault
320
466
  })
321
467
  : { targets: [], defaultTargetId: "" };
322
468
  const discoveredTargets = [
@@ -342,18 +488,37 @@ async function discoverShellOutletTargetsFromApp({ appRoot, sourceRoot = "src" }
342
488
  normalizeShellOutletTargetId(discoveredRouteMetaOutlets.defaultTargetId)
343
489
  ].filter(Boolean);
344
490
  for (const discoveredDefaultTargetId of discoveredDefaultTargetIds) {
345
- if (defaultTargetId && discoveredDefaultTargetId !== defaultTargetId) {
491
+ if (enforceSingleDefault === true && defaultTargetId && discoveredDefaultTargetId !== defaultTargetId) {
346
492
  throw new Error(
347
493
  `Multiple default ShellOutlet targets found in app source: "${defaultTargetId}" (${defaultTargetSource}) and ` +
348
494
  `"${discoveredDefaultTargetId}" (${relativePath}).`
349
495
  );
350
496
  }
351
497
 
352
- defaultTargetId = discoveredDefaultTargetId;
353
- defaultTargetSource = relativePath;
498
+ if (!defaultTargetId) {
499
+ defaultTargetId = discoveredDefaultTargetId;
500
+ defaultTargetSource = relativePath;
501
+ }
354
502
  }
355
503
  }
356
504
 
505
+ return Object.freeze({
506
+ resolvedAppRoot,
507
+ targetById,
508
+ defaultTargetId
509
+ });
510
+ }
511
+
512
+ async function discoverShellOutletTargetsFromApp({ appRoot, sourceRoot = "src" } = {}) {
513
+ const discoveredAppTargets = await collectAppSourceShellOutletTargets({
514
+ appRoot,
515
+ sourceRoot,
516
+ enforceSingleDefault: true,
517
+ context: "discoverShellOutletTargetsFromApp"
518
+ });
519
+ const resolvedAppRoot = discoveredAppTargets.resolvedAppRoot;
520
+ const targetById = new Map(discoveredAppTargets.targetById);
521
+
357
522
  const packageTargets = await collectInstalledPackageOutletTargets(resolvedAppRoot);
358
523
  for (const target of packageTargets) {
359
524
  if (!targetById.has(target.id)) {
@@ -363,15 +528,36 @@ async function discoverShellOutletTargetsFromApp({ appRoot, sourceRoot = "src" }
363
528
 
364
529
  const targets = [...targetById.values()].sort((left, right) => left.id.localeCompare(right.id));
365
530
  const normalizedTargets = targets.map((target) =>
366
- applyOutletDefaultOverrides({
531
+ Object.freeze({
367
532
  ...target,
368
- default: target.id === defaultTargetId
369
- }, outletDefaultOverrides)
533
+ default: target.id === discoveredAppTargets.defaultTargetId
534
+ })
370
535
  );
371
536
 
372
537
  return Object.freeze({
373
538
  targets: Object.freeze(normalizedTargets),
374
- defaultTargetId
539
+ defaultTargetId: discoveredAppTargets.defaultTargetId
540
+ });
541
+ }
542
+
543
+ async function discoverShellOutletSourcePathsFromApp({ appRoot, sourceRoot = "src" } = {}) {
544
+ const discoveredAppTargets = await collectAppSourceShellOutletTargets({
545
+ appRoot,
546
+ sourceRoot,
547
+ enforceSingleDefault: false,
548
+ context: "discoverShellOutletSourcePathsFromApp"
549
+ });
550
+ const targetById = new Map(discoveredAppTargets.targetById);
551
+
552
+ const packageTargets = await collectInstalledPackageOutletTargets(discoveredAppTargets.resolvedAppRoot);
553
+ for (const target of packageTargets) {
554
+ if (!targetById.has(target.id)) {
555
+ targetById.set(target.id, target);
556
+ }
557
+ }
558
+
559
+ return Object.freeze({
560
+ targets: Object.freeze([...targetById.values()].sort((left, right) => left.id.localeCompare(right.id)))
375
561
  });
376
562
  }
377
563
 
@@ -418,6 +604,9 @@ async function resolveShellOutletPlacementTargetFromApp({ appRoot, placement = "
418
604
  }
419
605
 
420
606
  export {
607
+ discoverPlacementTopologyFromApp,
608
+ discoverShellOutletSourcePathsFromApp,
421
609
  discoverShellOutletTargetsFromApp,
610
+ resolveSemanticPlacementTargetFromApp,
422
611
  resolveShellOutletPlacementTargetFromApp
423
612
  };
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import test from "node:test";
6
6
  import {
7
+ discoverShellOutletSourcePathsFromApp,
7
8
  discoverShellOutletTargetsFromApp,
8
9
  resolveShellOutletPlacementTargetFromApp
9
10
  } from "./shellOutlets.js";
@@ -99,7 +100,6 @@ test("discoverShellOutletTargetsFromApp includes installed package placement out
99
100
  outlets: [
100
101
  {
101
102
  target: "admin-cog:primary-menu",
102
- defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
103
103
  source: "src/client/components/UsersWorkspaceToolsWidget.vue"
104
104
  }
105
105
  ]
@@ -118,7 +118,6 @@ test("discoverShellOutletTargetsFromApp includes installed package placement out
118
118
  assert.deepEqual(discovered.targets[0], {
119
119
  id: "admin-cog:primary-menu",
120
120
  default: false,
121
- defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
122
121
  sourcePath: "package:@example/users-web:src/client/components/UsersWorkspaceToolsWidget.vue",
123
122
  sourcePackageId: "@example/users-web"
124
123
  });
@@ -132,68 +131,6 @@ test("discoverShellOutletTargetsFromApp includes installed package placement out
132
131
  });
133
132
  });
134
133
 
135
- test("discoverShellOutletTargetsFromApp applies app config default-link overrides by target id", async () => {
136
- await withTempApp(async (appRoot) => {
137
- await writeFileInApp(
138
- appRoot,
139
- "config/public.js",
140
- `export const config = {
141
- ui: {
142
- outletDefaults: {
143
- "admin-cog:primary-menu": {
144
- linkComponentToken: "local.main.ui.surface-aware-menu-link-item"
145
- }
146
- }
147
- }
148
- };
149
- `
150
- );
151
- await writeFileInApp(
152
- appRoot,
153
- ".jskit/lock.json",
154
- `${JSON.stringify(
155
- {
156
- lockVersion: 1,
157
- installedPackages: {
158
- "@example/users-web": {
159
- packageId: "@example/users-web",
160
- source: {
161
- type: "npm-installed-package",
162
- descriptorPath: "node_modules/@example/users-web/package.descriptor.mjs"
163
- }
164
- }
165
- }
166
- },
167
- null,
168
- 2
169
- )}\n`
170
- );
171
- await writeFileInApp(
172
- appRoot,
173
- "node_modules/@example/users-web/package.descriptor.mjs",
174
- `export default {
175
- packageId: "@example/users-web",
176
- metadata: {
177
- ui: {
178
- placements: {
179
- outlets: [
180
- {
181
- target: "admin-cog:primary-menu",
182
- defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item"
183
- }
184
- ]
185
- }
186
- }
187
- }
188
- };
189
- `
190
- );
191
-
192
- const discovered = await discoverShellOutletTargetsFromApp({ appRoot });
193
- assert.equal(discovered.targets[0].defaultLinkComponentToken, "local.main.ui.surface-aware-menu-link-item");
194
- });
195
- });
196
-
197
134
  test("discoverShellOutletTargetsFromApp returns targets with sourcePath and default marker", async () => {
198
135
  await withTempApp(async (appRoot) => {
199
136
  await writeFileInApp(
@@ -223,13 +160,11 @@ test("discoverShellOutletTargetsFromApp returns targets with sourcePath and defa
223
160
  {
224
161
  id: "admin-toolbox:widgets",
225
162
  default: true,
226
- defaultLinkComponentToken: "",
227
163
  sourcePath: "src/pages/admin/toolbox/index.vue"
228
164
  },
229
165
  {
230
166
  id: "shell-layout:primary-menu",
231
167
  default: false,
232
- defaultLinkComponentToken: "",
233
168
  sourcePath: "src/components/ShellLayout.vue"
234
169
  }
235
170
  ]);
@@ -266,7 +201,6 @@ test("discoverShellOutletTargetsFromApp discovers route meta placement outlets",
266
201
  {
267
202
  id: "contact-tools:sub-pages",
268
203
  default: false,
269
- defaultLinkComponentToken: "",
270
204
  sourcePath: "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/contact-tools.vue"
271
205
  }
272
206
  ]);
@@ -363,6 +297,36 @@ test("resolveShellOutletPlacementTargetFromApp throws when multiple default outl
363
297
  });
364
298
  });
365
299
 
300
+ test("discoverShellOutletSourcePathsFromApp ignores default conflicts while keeping source paths", async () => {
301
+ await withTempApp(async (appRoot) => {
302
+ await writeFileInApp(
303
+ appRoot,
304
+ "src/components/ShellLayout.vue",
305
+ `<template>
306
+ <div>
307
+ <ShellOutlet target="shell-layout:primary-menu" default />
308
+ <ShellOutlet target="shell-layout:primary-bottom-nav" default />
309
+ </div>
310
+ </template>
311
+ `
312
+ );
313
+
314
+ const discovered = await discoverShellOutletSourcePathsFromApp({ appRoot });
315
+ assert.deepEqual(discovered.targets, [
316
+ {
317
+ id: "shell-layout:primary-bottom-nav",
318
+ default: true,
319
+ sourcePath: "src/components/ShellLayout.vue"
320
+ },
321
+ {
322
+ id: "shell-layout:primary-menu",
323
+ default: true,
324
+ sourcePath: "src/components/ShellLayout.vue"
325
+ }
326
+ ]);
327
+ });
328
+ });
329
+
366
330
  test("resolveShellOutletPlacementTargetFromApp requires appRoot", async () => {
367
331
  await assert.rejects(
368
332
  () =>