@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.
@@ -40,10 +40,71 @@ async function writeShellLayout(appRoot, source = "") {
40
40
  <ShellOutlet
41
41
  target="shell-layout:primary-menu"
42
42
  default
43
- default-link-component-token="local.main.ui.surface-aware-menu-link-item"
44
43
  />
45
44
  </div>
46
45
  </template>
46
+ `
47
+ );
48
+ await writePlacementTopology(appRoot);
49
+ }
50
+
51
+ function renderTopologyVariant(outlet, { linkRenderer = "" } = {}) {
52
+ const rendererLines = linkRenderer
53
+ ? `,
54
+ renderers: {
55
+ link: "${linkRenderer}"
56
+ }`
57
+ : "";
58
+ return `{
59
+ outlet: "${outlet}"${rendererLines}
60
+ }`;
61
+ }
62
+
63
+ function renderTopologyEntry({
64
+ id = "",
65
+ owner = "",
66
+ surfaces = ["*"],
67
+ defaultPlacement = false,
68
+ outlet = "",
69
+ linkRenderer = ""
70
+ } = {}) {
71
+ const ownerLine = owner ? ` owner: "${owner}",\n` : "";
72
+ const defaultLine = defaultPlacement ? " default: true,\n" : "";
73
+ return ` {
74
+ id: "${id}",
75
+ ${ownerLine} surfaces: ${JSON.stringify(surfaces)},
76
+ ${defaultLine} variants: {
77
+ compact: ${renderTopologyVariant(outlet, { linkRenderer })},
78
+ medium: ${renderTopologyVariant(outlet, { linkRenderer })},
79
+ expanded: ${renderTopologyVariant(outlet, { linkRenderer })}
80
+ }
81
+ }`;
82
+ }
83
+
84
+ async function writePlacementTopology(appRoot, entries = []) {
85
+ const defaultEntries = [
86
+ renderTopologyEntry({
87
+ id: "shell.primary-nav",
88
+ surfaces: ["*"],
89
+ defaultPlacement: true,
90
+ outlet: "shell-layout:primary-menu",
91
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
92
+ }),
93
+ renderTopologyEntry({
94
+ id: "shell.status",
95
+ surfaces: ["*"],
96
+ outlet: "shell-layout:top-right",
97
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
98
+ })
99
+ ];
100
+ await writeFileInApp(
101
+ appRoot,
102
+ "src/placementTopology.js",
103
+ `export default {
104
+ placements: [
105
+ ${[...defaultEntries, ...entries].join(",\n")}
106
+ ]
107
+ };
47
108
  `
48
109
  );
49
110
  }
@@ -246,8 +307,8 @@ test("resolvePageLinkTargetDetails falls back to the app default placement targe
246
307
  });
247
308
 
248
309
  assert.equal(details.pageTarget.surfaceId, "admin");
249
- assert.equal(details.placementTarget.id, "shell-layout:primary-menu");
250
- assert.equal(details.componentToken, "local.main.ui.surface-aware-menu-link-item");
310
+ assert.equal(details.placementTarget.id, "shell.primary-nav");
311
+ assert.equal(details.componentToken, "");
251
312
  assert.equal(details.linkTo, "");
252
313
  assert.equal(details.whenLine, "");
253
314
  });
@@ -292,15 +353,21 @@ test("resolvePageLinkTargetDetails prefers an outlet-declared default link token
292
353
  };
293
354
  `
294
355
  );
356
+ await writePlacementTopology(appRoot, [
357
+ renderTopologyEntry({
358
+ id: "page.section-nav",
359
+ owner: "home-settings",
360
+ surfaces: ["home"],
361
+ outlet: "home-settings:primary-menu",
362
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
363
+ })
364
+ ]);
295
365
  await writeFileInApp(
296
366
  appRoot,
297
367
  "src/pages/home/settings.vue",
298
368
  `<template>
299
369
  <section>
300
- <ShellOutlet
301
- target="home-settings:primary-menu"
302
- default-link-component-token="local.main.ui.surface-aware-menu-link-item"
303
- />
370
+ <ShellOutlet target="home-settings:primary-menu" />
304
371
  <RouterView />
305
372
  </section>
306
373
  </template>
@@ -314,12 +381,156 @@ test("resolvePageLinkTargetDetails prefers an outlet-declared default link token
314
381
  });
315
382
 
316
383
  assert.equal(details.parentHost?.id, "home-settings:primary-menu");
317
- assert.equal(details.placementTarget.id, "home-settings:primary-menu");
318
- assert.equal(details.componentToken, "local.main.ui.surface-aware-menu-link-item");
384
+ assert.equal(details.placementTarget.id, "page.section-nav");
385
+ assert.equal(details.placementTarget.owner, "home-settings");
386
+ assert.equal(details.componentToken, "");
319
387
  assert.equal(details.linkTo, "");
320
388
  });
321
389
  });
322
390
 
391
+ test("resolvePageLinkTargetDetails infers owner-scoped placement for sibling file-route children", async () => {
392
+ await withTempApp(async (appRoot) => {
393
+ await writeConfig(
394
+ appRoot,
395
+ `export const config = {
396
+ surfaceDefinitions: {
397
+ home: { id: "home", pagesRoot: "home", enabled: true }
398
+ }
399
+ };
400
+ `
401
+ );
402
+ await writePlacementTopology(appRoot, [
403
+ renderTopologyEntry({
404
+ id: "page.section-nav",
405
+ owner: "home-settings",
406
+ surfaces: ["home"],
407
+ outlet: "home-settings:primary-menu",
408
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
409
+ })
410
+ ]);
411
+ await writeFileInApp(
412
+ appRoot,
413
+ "src/pages/home/settings.vue",
414
+ `<template>
415
+ <section>
416
+ <ShellOutlet target="home-settings:primary-menu" />
417
+ <RouterView />
418
+ </section>
419
+ </template>
420
+ `
421
+ );
422
+
423
+ const details = await resolvePageLinkTargetDetails({
424
+ appRoot,
425
+ targetFile: "home/settings/profile.vue",
426
+ context: "page target"
427
+ });
428
+
429
+ assert.equal(details.parentHost?.id, "home-settings:primary-menu");
430
+ assert.equal(details.placementTarget.id, "page.section-nav");
431
+ assert.equal(details.placementTarget.owner, "home-settings");
432
+ });
433
+ });
434
+
435
+ test("resolvePageLinkTargetDetails prefers owner-scoped topology over a global mapping for the same concrete outlet", async () => {
436
+ await withTempApp(async (appRoot) => {
437
+ await writeConfig(
438
+ appRoot,
439
+ `export const config = {
440
+ surfaceDefinitions: {
441
+ home: { id: "home", pagesRoot: "home", enabled: true }
442
+ }
443
+ };
444
+ `
445
+ );
446
+ await writePlacementTopology(appRoot, [
447
+ renderTopologyEntry({
448
+ id: "page.section-nav",
449
+ surfaces: ["home"],
450
+ outlet: "home-settings:primary-menu",
451
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
452
+ }),
453
+ renderTopologyEntry({
454
+ id: "page.section-nav",
455
+ owner: "home-settings",
456
+ surfaces: ["home"],
457
+ outlet: "home-settings:primary-menu",
458
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
459
+ })
460
+ ]);
461
+ await writeFileInApp(
462
+ appRoot,
463
+ "src/pages/home/settings.vue",
464
+ `<template>
465
+ <section>
466
+ <ShellOutlet target="home-settings:primary-menu" />
467
+ <RouterView />
468
+ </section>
469
+ </template>
470
+ `
471
+ );
472
+
473
+ const details = await resolvePageLinkTargetDetails({
474
+ appRoot,
475
+ targetFile: "home/settings/profile.vue",
476
+ context: "page target"
477
+ });
478
+
479
+ assert.equal(details.placementTarget.id, "page.section-nav");
480
+ assert.equal(details.placementTarget.owner, "home-settings");
481
+ });
482
+ });
483
+
484
+ test("resolvePageLinkTargetDetails rejects ambiguous semantic mappings for the same owner outlet", async () => {
485
+ await withTempApp(async (appRoot) => {
486
+ await writeConfig(
487
+ appRoot,
488
+ `export const config = {
489
+ surfaceDefinitions: {
490
+ home: { id: "home", pagesRoot: "home", enabled: true }
491
+ }
492
+ };
493
+ `
494
+ );
495
+ await writePlacementTopology(appRoot, [
496
+ renderTopologyEntry({
497
+ id: "page.section-nav",
498
+ owner: "home-settings",
499
+ surfaces: ["home"],
500
+ outlet: "home-settings:primary-menu",
501
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
502
+ }),
503
+ renderTopologyEntry({
504
+ id: "page.actions",
505
+ owner: "home-settings",
506
+ surfaces: ["home"],
507
+ outlet: "home-settings:primary-menu",
508
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
509
+ })
510
+ ]);
511
+ await writeFileInApp(
512
+ appRoot,
513
+ "src/pages/home/settings.vue",
514
+ `<template>
515
+ <section>
516
+ <ShellOutlet target="home-settings:primary-menu" />
517
+ <RouterView />
518
+ </section>
519
+ </template>
520
+ `
521
+ );
522
+
523
+ await assert.rejects(
524
+ resolvePageLinkTargetDetails({
525
+ appRoot,
526
+ targetFile: "home/settings/profile.vue",
527
+ context: "page target"
528
+ }),
529
+ /found multiple semantic placements mapped to concrete outlet "home-settings:primary-menu": page\.actions \[owner:home-settings\], page\.section-nav \[owner:home-settings\]/
530
+ );
531
+ });
532
+ });
533
+
323
534
  test("resolvePageLinkTargetDetails inherits a file-route parent subpages host", async () => {
324
535
  await withTempApp(async (appRoot) => {
325
536
  await writeConfig(
@@ -332,6 +543,15 @@ test("resolvePageLinkTargetDetails inherits a file-route parent subpages host",
332
543
  `
333
544
  );
334
545
  await writeShellLayout(appRoot);
546
+ await writePlacementTopology(appRoot, [
547
+ renderTopologyEntry({
548
+ id: "page.section-nav",
549
+ owner: "contact-view",
550
+ surfaces: ["admin"],
551
+ outlet: "contact-view:sub-pages",
552
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
553
+ })
554
+ ]);
335
555
  await writeFileInApp(
336
556
  appRoot,
337
557
  "src/pages/admin/contacts/[contactId].vue",
@@ -353,8 +573,9 @@ test("resolvePageLinkTargetDetails inherits a file-route parent subpages host",
353
573
  });
354
574
 
355
575
  assert.equal(details.parentHost?.id, "contact-view:sub-pages");
356
- assert.equal(details.placementTarget.id, "contact-view:sub-pages");
357
- assert.equal(details.componentToken, "local.main.ui.surface-aware-menu-link-item");
576
+ assert.equal(details.placementTarget.id, "page.section-nav");
577
+ assert.equal(details.placementTarget.owner, "contact-view");
578
+ assert.equal(details.componentToken, "");
358
579
  assert.equal(details.linkTo, "./notes");
359
580
  });
360
581
  });
@@ -375,13 +596,13 @@ test("resolvePageLinkTargetDetails honors explicit placement and link overrides"
375
596
  const details = await resolvePageLinkTargetDetails({
376
597
  appRoot,
377
598
  targetFile: "admin/contacts/[contactId]/index/notes/index.vue",
378
- placement: "shell-layout:top-right",
599
+ placement: "shell.status",
379
600
  componentToken: "custom.link-item",
380
601
  linkTo: "./assistant-notes",
381
602
  context: "page target"
382
603
  });
383
604
 
384
- assert.equal(details.placementTarget.id, "shell-layout:top-right");
605
+ assert.equal(details.placementTarget.id, "shell.status");
385
606
  assert.equal(details.componentToken, "custom.link-item");
386
607
  assert.equal(details.linkTo, "./assistant-notes");
387
608
  });
@@ -399,6 +620,15 @@ test("resolvePageLinkTargetDetails inherits an index-route parent subpages host
399
620
  `
400
621
  );
401
622
  await writeShellLayout(appRoot);
623
+ await writePlacementTopology(appRoot, [
624
+ renderTopologyEntry({
625
+ id: "page.section-nav",
626
+ owner: "customer-view",
627
+ surfaces: ["admin"],
628
+ outlet: "customer-view:sub-pages",
629
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
630
+ })
631
+ ]);
402
632
  await writeFileInApp(
403
633
  appRoot,
404
634
  "src/pages/admin/customers/[customerId]/index.vue",
@@ -421,8 +651,9 @@ test("resolvePageLinkTargetDetails inherits an index-route parent subpages host
421
651
 
422
652
  assert.equal(details.parentHost?.id, "customer-view:sub-pages");
423
653
  assert.equal(details.parentHost?.pageFile, "src/pages/admin/customers/[customerId]/index.vue");
424
- assert.equal(details.placementTarget.id, "customer-view:sub-pages");
425
- assert.equal(details.componentToken, "local.main.ui.surface-aware-menu-link-item");
654
+ assert.equal(details.placementTarget.id, "page.section-nav");
655
+ assert.equal(details.placementTarget.owner, "customer-view");
656
+ assert.equal(details.componentToken, "");
426
657
  assert.equal(details.linkTo, "./pets");
427
658
  });
428
659
  });