@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.
- package/client/moduleBootstrap.js +4 -0
- package/client/moduleBootstrap.test.js +4 -0
- package/client/shellBootstrap.js +2 -0
- package/client/shellBootstrap.test.js +3 -0
- package/client/vite/clientBootstrapPlugin.js +1 -0
- package/client/vite/clientBootstrapPlugin.test.js +3 -3
- package/package.json +2 -1
- package/server/http/lib/httpRuntime.js +3 -1
- package/server/http/lib/kernel.test.js +4 -0
- package/server/runtime/fastifyBootstrap.js +61 -0
- package/server/runtime/fastifyBootstrap.test.js +47 -1
- package/server/support/appConfigFiles.js +3 -2
- package/server/support/appConfigFiles.test.js +15 -0
- package/server/support/index.js +3 -0
- package/server/support/pageTargets.js +98 -63
- package/server/support/pageTargets.test.js +246 -15
- package/server/support/shellOutlets.js +245 -56
- package/server/support/shellOutlets.test.js +31 -67
- package/shared/support/generatedUiContract.js +542 -0
- package/shared/support/generatedUiContract.test.js +208 -0
- package/shared/support/shellLayoutTargets.js +230 -7
- package/shared/support/shellLayoutTargets.test.js +20 -2
|
@@ -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
|
|
250
|
-
assert.equal(details.componentToken, "
|
|
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, "
|
|
318
|
-
assert.equal(details.
|
|
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, "
|
|
357
|
-
assert.equal(details.
|
|
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
|
|
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
|
|
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, "
|
|
425
|
-
assert.equal(details.
|
|
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
|
});
|