@jskit-ai/kernel 0.1.66 → 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 +1 -0
- package/server/support/pageTargets.js +24 -8
- package/server/support/pageTargets.test.js +143 -0
- package/server/support/shellOutlets.js +68 -12
- package/server/support/shellOutlets.test.js +31 -0
- package/shared/support/generatedUiContract.js +542 -0
- package/shared/support/generatedUiContract.test.js +208 -0
- package/shared/support/shellLayoutTargets.js +11 -3
- package/shared/support/shellLayoutTargets.test.js +20 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
GENERATED_UI_NAVIGATION_ROLE_OPTION,
|
|
5
|
+
GENERATED_UI_NAVIGATION_ROLE_VALUES,
|
|
6
|
+
GENERATED_UI_SURFACE_PROFILES,
|
|
7
|
+
assertGeneratedUiSourceContract,
|
|
8
|
+
buildGeneratedUiScreenClassName,
|
|
9
|
+
collectGeneratedUiSourceContractIssues,
|
|
10
|
+
inferGeneratedUiNavigationRole,
|
|
11
|
+
isGeneratedUiNoLinkNavigationRole,
|
|
12
|
+
normalizeGeneratedUiNavigationRole,
|
|
13
|
+
resolveGeneratedUiSurfaceProfile,
|
|
14
|
+
resolveGeneratedUiNavigationRoleLinkPlacement,
|
|
15
|
+
shouldCreateGeneratedUiNavigationLink
|
|
16
|
+
} from "./generatedUiContract.js";
|
|
17
|
+
|
|
18
|
+
test("generated UI navigation role metadata is descriptor-ready", () => {
|
|
19
|
+
assert.deepEqual(
|
|
20
|
+
GENERATED_UI_NAVIGATION_ROLE_VALUES,
|
|
21
|
+
["primary", "secondary", "utility", "detail", "workflow", "none"]
|
|
22
|
+
);
|
|
23
|
+
assert.equal(GENERATED_UI_NAVIGATION_ROLE_OPTION.validationType, "enum");
|
|
24
|
+
assert.deepEqual(GENERATED_UI_NAVIGATION_ROLE_OPTION.allowedValues, GENERATED_UI_NAVIGATION_ROLE_VALUES);
|
|
25
|
+
assert.equal(GENERATED_UI_NAVIGATION_ROLE_OPTION.defaultValue, "");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("generated UI surface profiles map app, operator, and settings density", () => {
|
|
29
|
+
assert.deepEqual(Object.keys(GENERATED_UI_SURFACE_PROFILES), ["task", "operator", "settings"]);
|
|
30
|
+
assert.equal(resolveGeneratedUiSurfaceProfile("").id, "task");
|
|
31
|
+
assert.equal(resolveGeneratedUiSurfaceProfile("operator").id, "operator");
|
|
32
|
+
assert.equal(resolveGeneratedUiSurfaceProfile("operator").density, "compact");
|
|
33
|
+
assert.equal(resolveGeneratedUiSurfaceProfile("settings").id, "settings");
|
|
34
|
+
assert.equal(
|
|
35
|
+
buildGeneratedUiScreenClassName("generated-page-screen d-flex", {
|
|
36
|
+
surfaceProfile: "operator"
|
|
37
|
+
}),
|
|
38
|
+
"generated-ui-screen generated-ui-screen--operator generated-page-screen d-flex"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("normalizeGeneratedUiNavigationRole defaults and validates roles", () => {
|
|
43
|
+
assert.equal(normalizeGeneratedUiNavigationRole(""), "primary");
|
|
44
|
+
assert.equal(normalizeGeneratedUiNavigationRole(" Secondary "), "secondary");
|
|
45
|
+
assert.throws(
|
|
46
|
+
() => normalizeGeneratedUiNavigationRole("drawer"),
|
|
47
|
+
/navigation-role must be one of: primary, secondary, utility, detail, workflow, none/
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("generated UI navigation roles resolve semantic link placements", () => {
|
|
52
|
+
assert.equal(resolveGeneratedUiNavigationRoleLinkPlacement({}), "");
|
|
53
|
+
assert.equal(resolveGeneratedUiNavigationRoleLinkPlacement({ "navigation-role": "secondary" }), "shell.secondary-nav");
|
|
54
|
+
assert.equal(resolveGeneratedUiNavigationRoleLinkPlacement({ "navigation-role": "utility" }), "shell.global-actions");
|
|
55
|
+
assert.equal(
|
|
56
|
+
resolveGeneratedUiNavigationRoleLinkPlacement({
|
|
57
|
+
"navigation-role": "secondary",
|
|
58
|
+
"link-placement": "settings.sections"
|
|
59
|
+
}),
|
|
60
|
+
"settings.sections"
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("generated UI navigation role inference keeps detail and workflow routes out of primary nav", () => {
|
|
65
|
+
assert.equal(inferGeneratedUiNavigationRole({}, { routePath: "/reports" }), "primary");
|
|
66
|
+
assert.equal(inferGeneratedUiNavigationRole({}, { routePath: "/reports/[reportId]" }), "detail");
|
|
67
|
+
assert.equal(inferGeneratedUiNavigationRole({}, { routePath: "/reports/[reportId]/activity" }), "primary");
|
|
68
|
+
assert.equal(
|
|
69
|
+
inferGeneratedUiNavigationRole({}, {
|
|
70
|
+
dynamicRoutePolicy: "any",
|
|
71
|
+
routePath: "/reports/[reportId]/activity"
|
|
72
|
+
}),
|
|
73
|
+
"detail"
|
|
74
|
+
);
|
|
75
|
+
assert.equal(inferGeneratedUiNavigationRole({}, { routePath: "/reports/new" }), "workflow");
|
|
76
|
+
assert.equal(
|
|
77
|
+
inferGeneratedUiNavigationRole({ "navigation-role": "primary" }, { routePath: "/reports/[reportId]" }),
|
|
78
|
+
"primary"
|
|
79
|
+
);
|
|
80
|
+
assert.equal(
|
|
81
|
+
shouldCreateGeneratedUiNavigationLink({}, {
|
|
82
|
+
allowLinkTo: true,
|
|
83
|
+
routePath: "/reports/[reportId]"
|
|
84
|
+
}),
|
|
85
|
+
false
|
|
86
|
+
);
|
|
87
|
+
assert.equal(
|
|
88
|
+
shouldCreateGeneratedUiNavigationLink({ "navigation-role": "primary" }, {
|
|
89
|
+
allowLinkTo: true,
|
|
90
|
+
routePath: "/reports/[reportId]"
|
|
91
|
+
}),
|
|
92
|
+
true
|
|
93
|
+
);
|
|
94
|
+
assert.equal(
|
|
95
|
+
shouldCreateGeneratedUiNavigationLink({ "link-placement": "shell.secondary-nav" }, {
|
|
96
|
+
allowLinkTo: true,
|
|
97
|
+
routePath: "/reports/[reportId]"
|
|
98
|
+
}),
|
|
99
|
+
true
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("generated UI no-link roles reject conflicting link options", () => {
|
|
104
|
+
assert.equal(isGeneratedUiNoLinkNavigationRole("detail"), true);
|
|
105
|
+
assert.equal(shouldCreateGeneratedUiNavigationLink({ "navigation-role": "workflow" }), false);
|
|
106
|
+
assert.equal(shouldCreateGeneratedUiNavigationLink({ "navigation-role": "primary" }), true);
|
|
107
|
+
assert.throws(
|
|
108
|
+
() => shouldCreateGeneratedUiNavigationLink({
|
|
109
|
+
"navigation-role": "detail",
|
|
110
|
+
"link-placement": "shell.primary-nav"
|
|
111
|
+
}),
|
|
112
|
+
/navigation-role "detail" cannot be combined with --link-placement/
|
|
113
|
+
);
|
|
114
|
+
assert.throws(
|
|
115
|
+
() => shouldCreateGeneratedUiNavigationLink({
|
|
116
|
+
"navigation-role": "none",
|
|
117
|
+
"link-to": "./details"
|
|
118
|
+
}, {
|
|
119
|
+
allowLinkTo: true
|
|
120
|
+
}),
|
|
121
|
+
/navigation-role "none" cannot be combined with --link-placement or --link-to/
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("generated UI source contract flags placeholder copy and missing profile hooks", () => {
|
|
126
|
+
const issues = collectGeneratedUiSourceContractIssues(
|
|
127
|
+
`<template>
|
|
128
|
+
<section>
|
|
129
|
+
<v-card>Replace this content</v-card>
|
|
130
|
+
</section>
|
|
131
|
+
</template>`,
|
|
132
|
+
{
|
|
133
|
+
profile: "page"
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
assert.deepEqual(
|
|
138
|
+
issues.map((issue) => issue.id),
|
|
139
|
+
[
|
|
140
|
+
"replace-this-copy",
|
|
141
|
+
"vuetify-card-shell",
|
|
142
|
+
"shared-screen-class",
|
|
143
|
+
"page-screen-title",
|
|
144
|
+
"page-empty-state-sheet",
|
|
145
|
+
"page-responsive-title-type",
|
|
146
|
+
"page-compact-rules"
|
|
147
|
+
]
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("generated UI source contract accepts compact-first CRUD detail structure", () => {
|
|
152
|
+
assert.doesNotThrow(() => assertGeneratedUiSourceContract(
|
|
153
|
+
`<template>
|
|
154
|
+
<CrudAddEditScreen :screen="screen">
|
|
155
|
+
<template #fields="{ formState, addEdit, resolveFieldErrors }"></template>
|
|
156
|
+
</CrudAddEditScreen>
|
|
157
|
+
</template>
|
|
158
|
+
|
|
159
|
+
<script setup>
|
|
160
|
+
const screen = useCrudAddEditScreen({
|
|
161
|
+
title: "New Customer",
|
|
162
|
+
resource: uiResource
|
|
163
|
+
});
|
|
164
|
+
</script>`,
|
|
165
|
+
{
|
|
166
|
+
profile: "crud-detail",
|
|
167
|
+
sourceName: "NewElement.vue"
|
|
168
|
+
}
|
|
169
|
+
));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("generated UI source contract accepts compact-first CRUD list structure", () => {
|
|
173
|
+
assert.doesNotThrow(() => assertGeneratedUiSourceContract(
|
|
174
|
+
`<template>
|
|
175
|
+
<CrudListScreen
|
|
176
|
+
:screen="screen"
|
|
177
|
+
empty-title="__JSKIT_UI_LIST_EMPTY_TITLE__"
|
|
178
|
+
load-error-title="__JSKIT_UI_LIST_LOAD_ERROR_TITLE__"
|
|
179
|
+
>
|
|
180
|
+
<template #card-fields="{ record, records, formatListCardValue }"></template>
|
|
181
|
+
<template #table-header></template>
|
|
182
|
+
<template #table-row="{ record, records }"></template>
|
|
183
|
+
</CrudListScreen>
|
|
184
|
+
</template>
|
|
185
|
+
|
|
186
|
+
<script setup>
|
|
187
|
+
const screen = useCrudListScreen({
|
|
188
|
+
resource: uiResource,
|
|
189
|
+
listFilters,
|
|
190
|
+
listBulkActions
|
|
191
|
+
});
|
|
192
|
+
</script>`,
|
|
193
|
+
{
|
|
194
|
+
profile: "crud-list",
|
|
195
|
+
sourceName: "ListElement.vue"
|
|
196
|
+
}
|
|
197
|
+
));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("generated UI responsive smoke profile requires compact medium expanded checks", () => {
|
|
201
|
+
assert.throws(
|
|
202
|
+
() => assertGeneratedUiSourceContract("const width = 390; const selector = 'generated-ui-screen';", {
|
|
203
|
+
profile: "responsive-smoke",
|
|
204
|
+
sourceName: "tests/e2e/example.spec.ts"
|
|
205
|
+
}),
|
|
206
|
+
/missing:medium-viewport/
|
|
207
|
+
);
|
|
208
|
+
});
|
|
@@ -328,7 +328,13 @@ function normalizeShellOutletTargetRecord(
|
|
|
328
328
|
});
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
function discoverShellOutletTargetsFromVueSource(
|
|
331
|
+
function discoverShellOutletTargetsFromVueSource(
|
|
332
|
+
source = "",
|
|
333
|
+
{
|
|
334
|
+
context = "shell layout",
|
|
335
|
+
enforceSingleDefault = true
|
|
336
|
+
} = {}
|
|
337
|
+
) {
|
|
332
338
|
const sourceText = String(source || "");
|
|
333
339
|
const resolvedContext = normalizeText(context) || "shell layout";
|
|
334
340
|
const targetById = new Map();
|
|
@@ -347,12 +353,14 @@ function discoverShellOutletTargetsFromVueSource(source = "", { context = "shell
|
|
|
347
353
|
}
|
|
348
354
|
|
|
349
355
|
if (normalizedTarget.default) {
|
|
350
|
-
if (defaultTargetId) {
|
|
356
|
+
if (enforceSingleDefault === true && defaultTargetId) {
|
|
351
357
|
throw new Error(
|
|
352
358
|
`${resolvedContext} defines multiple default ShellOutlet targets: "${defaultTargetId}" and "${normalizedTarget.id}".`
|
|
353
359
|
);
|
|
354
360
|
}
|
|
355
|
-
defaultTargetId
|
|
361
|
+
if (!defaultTargetId) {
|
|
362
|
+
defaultTargetId = normalizedTarget.id;
|
|
363
|
+
}
|
|
356
364
|
}
|
|
357
365
|
|
|
358
366
|
targetById.set(normalizedTarget.id, normalizedTarget);
|
|
@@ -74,6 +74,26 @@ test("discoverShellOutletTargetsFromVueSource throws for multiple defaults", ()
|
|
|
74
74
|
);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
test("discoverShellOutletTargetsFromVueSource can collect source hints without enforcing one default", () => {
|
|
78
|
+
const source = `
|
|
79
|
+
<template>
|
|
80
|
+
<ShellOutlet target="shell-layout:primary-menu" default />
|
|
81
|
+
<ShellOutlet target="shell-layout:primary-bottom-nav" default />
|
|
82
|
+
</template>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const discovered = discoverShellOutletTargetsFromVueSource(source, {
|
|
86
|
+
context: "ShellLayout.vue",
|
|
87
|
+
enforceSingleDefault: false
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
assert.equal(discovered.defaultTargetId, "shell-layout:primary-menu");
|
|
91
|
+
assert.deepEqual(
|
|
92
|
+
discovered.targets.map((entry) => entry.id),
|
|
93
|
+
["shell-layout:primary-menu", "shell-layout:primary-bottom-nav"]
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
77
97
|
test("discoverShellOutletTargetsFromVueSource ignores disabled default markers", () => {
|
|
78
98
|
const source = `
|
|
79
99
|
<template>
|