@jskit-ai/ui-generator 0.1.17 → 0.1.19
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/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/ui-generator",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.19",
|
|
5
5
|
kind: "generator",
|
|
6
6
|
description: "Create non-CRUD pages, reusable UI elements, and subpage hosts.",
|
|
7
7
|
options: {
|
|
@@ -19,7 +19,7 @@ export default Object.freeze({
|
|
|
19
19
|
validationType: "enabled-surface-id",
|
|
20
20
|
defaultFromConfig: "surfaceDefaultId",
|
|
21
21
|
promptLabel: "Target surface",
|
|
22
|
-
promptHint: "Used
|
|
22
|
+
promptHint: "Optional. Used when JSKIT cannot infer the target surface from placement or app topology."
|
|
23
23
|
},
|
|
24
24
|
path: {
|
|
25
25
|
required: false,
|
|
@@ -152,9 +152,11 @@ export default Object.freeze({
|
|
|
152
152
|
export: "runGeneratorSubcommand",
|
|
153
153
|
description: "Create a Vue component file under the chosen component directory (default: src/components) and add a placement entry that renders it.",
|
|
154
154
|
optionNames: ["name", "surface", "path", "placement", "force"],
|
|
155
|
-
requiredOptionNames: ["name"
|
|
155
|
+
requiredOptionNames: ["name"],
|
|
156
156
|
notes: [
|
|
157
157
|
"If --placement is omitted, the placed element is added at shell-layout:top-right.",
|
|
158
|
+
"If the placement target belongs to a page-owned outlet, JSKIT infers the surface automatically.",
|
|
159
|
+
"If the target is shared and the app has multiple enabled surfaces, pass --surface explicitly.",
|
|
158
160
|
"If the component file already exists, rerun with --force to overwrite it."
|
|
159
161
|
],
|
|
160
162
|
examples: [
|
|
@@ -162,8 +164,7 @@ export default Object.freeze({
|
|
|
162
164
|
label: "Common usage",
|
|
163
165
|
lines: [
|
|
164
166
|
"npx jskit generate ui-generator placed-element \\",
|
|
165
|
-
" --name \"Alerts Widget\"
|
|
166
|
-
" --surface admin"
|
|
167
|
+
" --name \"Alerts Widget\""
|
|
167
168
|
]
|
|
168
169
|
},
|
|
169
170
|
{
|
|
@@ -277,7 +278,7 @@ export default Object.freeze({
|
|
|
277
278
|
mutations: {
|
|
278
279
|
dependencies: {
|
|
279
280
|
runtime: {
|
|
280
|
-
"@jskit-ai/users-web": "0.1.
|
|
281
|
+
"@jskit-ai/users-web": "0.1.51"
|
|
281
282
|
},
|
|
282
283
|
dev: {}
|
|
283
284
|
},
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/ui-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@jskit-ai/kernel": "0.1.
|
|
10
|
-
"@jskit-ai/shell-web": "0.1.
|
|
9
|
+
"@jskit-ai/kernel": "0.1.36",
|
|
10
|
+
"@jskit-ai/shell-web": "0.1.35"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
"./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import {
|
|
4
|
+
listSurfacePageRoots,
|
|
5
|
+
resolveBestSurfaceMatchFromPageFile,
|
|
4
6
|
resolveShellOutletPlacementTargetFromApp,
|
|
5
7
|
toPosixPath
|
|
6
8
|
} from "@jskit-ai/kernel/server/support";
|
|
9
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface";
|
|
7
10
|
import { normalizeBoolean, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
8
11
|
import {
|
|
9
12
|
DEFAULT_COMPONENT_DIRECTORY,
|
|
@@ -31,6 +34,47 @@ function renderElementComponentSource(elementName = "") {
|
|
|
31
34
|
`;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
async function resolvePlacedElementSurface({
|
|
38
|
+
appRoot,
|
|
39
|
+
placementTarget = {},
|
|
40
|
+
surface = "",
|
|
41
|
+
context = "ui-generator placed-element"
|
|
42
|
+
} = {}) {
|
|
43
|
+
const explicitSurface = normalizeSurfaceId(surface);
|
|
44
|
+
const inferredSurfaceMatch = await resolveBestSurfaceMatchFromPageFile(
|
|
45
|
+
String(placementTarget?.sourcePath || ""),
|
|
46
|
+
await listSurfacePageRoots(appRoot, { context }),
|
|
47
|
+
{ context }
|
|
48
|
+
);
|
|
49
|
+
const inferredSurface = normalizeSurfaceId(inferredSurfaceMatch?.surfaceId);
|
|
50
|
+
|
|
51
|
+
if (explicitSurface) {
|
|
52
|
+
if (inferredSurface && explicitSurface !== inferredSurface) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`${context} target "${normalizeText(placementTarget?.id) || "<unknown>"}" belongs to surface "${inferredSurface}", ` +
|
|
55
|
+
`so --surface ${explicitSurface} is invalid.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return explicitSurface;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (inferredSurface) {
|
|
62
|
+
return inferredSurface;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const surfacePageRoots = await listSurfacePageRoots(appRoot, { context });
|
|
66
|
+
if (surfacePageRoots.length === 1) {
|
|
67
|
+
return normalizeSurfaceId(surfacePageRoots[0]?.id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const targetId = normalizeText(placementTarget?.id) || "<unknown>";
|
|
71
|
+
const enabledSurfaceIds = surfacePageRoots.map((entry) => normalizeSurfaceId(entry?.id)).filter(Boolean);
|
|
72
|
+
throw new Error(
|
|
73
|
+
`${context} could not infer a surface for placement target "${targetId}". ` +
|
|
74
|
+
`Pass --surface explicitly. Enabled surfaces: ${enabledSurfaceIds.join(", ") || "<none>"}.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
34
78
|
async function runGeneratorSubcommand({
|
|
35
79
|
appRoot,
|
|
36
80
|
subcommand = "",
|
|
@@ -50,7 +94,6 @@ async function runGeneratorSubcommand({
|
|
|
50
94
|
});
|
|
51
95
|
|
|
52
96
|
const name = requireOption(options, "name", { context: "ui-generator placed-element" });
|
|
53
|
-
const surface = requireOption(options, "surface", { context: "ui-generator placed-element" }).toLowerCase();
|
|
54
97
|
const componentDirectory = normalizeText(options.path) || DEFAULT_COMPONENT_DIRECTORY;
|
|
55
98
|
const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
|
|
56
99
|
? normalizeBoolean(options.force)
|
|
@@ -77,6 +120,12 @@ async function runGeneratorSubcommand({
|
|
|
77
120
|
context: "ui-generator",
|
|
78
121
|
placement: options?.placement || DEFAULT_ELEMENT_PLACEMENT
|
|
79
122
|
});
|
|
123
|
+
const surface = await resolvePlacedElementSurface({
|
|
124
|
+
appRoot,
|
|
125
|
+
placementTarget,
|
|
126
|
+
surface: options?.surface,
|
|
127
|
+
context: "ui-generator placed-element"
|
|
128
|
+
});
|
|
80
129
|
|
|
81
130
|
const touchedFiles = new Set();
|
|
82
131
|
const desiredComponentSource = renderElementComponentSource(name);
|
|
@@ -46,17 +46,8 @@ export default function getPlacements() {
|
|
|
46
46
|
path.join(appRoot, "packages", "main", "src", "client", "providers", "MainClientProvider.js"),
|
|
47
47
|
`const mainClientComponents = [];
|
|
48
48
|
|
|
49
|
-
function registerMainClientComponent(
|
|
50
|
-
|
|
51
|
-
if (!token || typeof resolveComponent !== "function") {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
mainClientComponents.push(
|
|
55
|
-
Object.freeze({
|
|
56
|
-
token,
|
|
57
|
-
resolveComponent
|
|
58
|
-
})
|
|
59
|
-
);
|
|
49
|
+
function registerMainClientComponent(token, resolveComponent) {
|
|
50
|
+
mainClientComponents.push({ token, resolveComponent });
|
|
60
51
|
}
|
|
61
52
|
|
|
62
53
|
class MainClientProvider {}
|
|
@@ -15,10 +15,27 @@ async function withTempApp(run) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
async function writeAppFixture(appRoot) {
|
|
18
|
+
await mkdir(path.join(appRoot, "config"), { recursive: true });
|
|
18
19
|
await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
|
|
19
20
|
await mkdir(path.join(appRoot, "src", "pages", "admin", "workspace", "settings"), { recursive: true });
|
|
20
21
|
await mkdir(path.join(appRoot, "packages", "main", "src", "client", "providers"), { recursive: true });
|
|
21
22
|
|
|
23
|
+
await writeFile(
|
|
24
|
+
path.join(appRoot, "config", "public.js"),
|
|
25
|
+
`export const config = {
|
|
26
|
+
surfaceDefaultId: "admin",
|
|
27
|
+
surfaceDefinitions: {
|
|
28
|
+
admin: {
|
|
29
|
+
id: "admin",
|
|
30
|
+
pagesRoot: "admin/workspace",
|
|
31
|
+
enabled: true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
`,
|
|
36
|
+
"utf8"
|
|
37
|
+
);
|
|
38
|
+
|
|
22
39
|
await writeFile(
|
|
23
40
|
path.join(appRoot, "src", "components", "ShellLayout.vue"),
|
|
24
41
|
`<template>
|
|
@@ -59,17 +76,8 @@ export default function getPlacements() {
|
|
|
59
76
|
path.join(appRoot, "packages", "main", "src", "client", "providers", "MainClientProvider.js"),
|
|
60
77
|
`const mainClientComponents = [];
|
|
61
78
|
|
|
62
|
-
function registerMainClientComponent(
|
|
63
|
-
|
|
64
|
-
if (!token || typeof resolveComponent !== "function") {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
mainClientComponents.push(
|
|
68
|
-
Object.freeze({
|
|
69
|
-
token,
|
|
70
|
-
resolveComponent
|
|
71
|
-
})
|
|
72
|
-
);
|
|
79
|
+
function registerMainClientComponent(token, resolveComponent) {
|
|
80
|
+
mainClientComponents.push({ token, resolveComponent });
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
class MainClientProvider {}
|
|
@@ -132,6 +140,101 @@ test("ui-generator placed-element subcommand supports explicit placement overrid
|
|
|
132
140
|
});
|
|
133
141
|
});
|
|
134
142
|
|
|
143
|
+
test("ui-generator placed-element infers surface from a page-owned placement target", async () => {
|
|
144
|
+
await withTempApp(async (appRoot) => {
|
|
145
|
+
await writeAppFixture(appRoot);
|
|
146
|
+
|
|
147
|
+
await runGeneratorSubcommand({
|
|
148
|
+
appRoot,
|
|
149
|
+
subcommand: "placed-element",
|
|
150
|
+
options: {
|
|
151
|
+
name: "Ops Panel",
|
|
152
|
+
placement: "admin-settings:forms"
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
157
|
+
assert.match(placementSource, /target: "admin-settings:forms"/);
|
|
158
|
+
assert.match(placementSource, /surfaces: \["admin"\]/);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("ui-generator placed-element infers the only enabled surface for shared shell targets", async () => {
|
|
163
|
+
await withTempApp(async (appRoot) => {
|
|
164
|
+
await writeAppFixture(appRoot);
|
|
165
|
+
|
|
166
|
+
await runGeneratorSubcommand({
|
|
167
|
+
appRoot,
|
|
168
|
+
subcommand: "placed-element",
|
|
169
|
+
options: {
|
|
170
|
+
name: "Ops Panel"
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
175
|
+
assert.match(placementSource, /target: "shell-layout:top-right"/);
|
|
176
|
+
assert.match(placementSource, /surfaces: \["admin"\]/);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("ui-generator placed-element requires explicit surface when a shared shell target is ambiguous", async () => {
|
|
181
|
+
await withTempApp(async (appRoot) => {
|
|
182
|
+
await writeAppFixture(appRoot);
|
|
183
|
+
await writeFile(
|
|
184
|
+
path.join(appRoot, "config", "public.js"),
|
|
185
|
+
`export const config = {
|
|
186
|
+
surfaceDefaultId: "admin",
|
|
187
|
+
surfaceDefinitions: {
|
|
188
|
+
admin: {
|
|
189
|
+
id: "admin",
|
|
190
|
+
pagesRoot: "admin/workspace",
|
|
191
|
+
enabled: true
|
|
192
|
+
},
|
|
193
|
+
console: {
|
|
194
|
+
id: "console",
|
|
195
|
+
pagesRoot: "console",
|
|
196
|
+
enabled: true
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
`,
|
|
201
|
+
"utf8"
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
await assert.rejects(
|
|
205
|
+
() =>
|
|
206
|
+
runGeneratorSubcommand({
|
|
207
|
+
appRoot,
|
|
208
|
+
subcommand: "placed-element",
|
|
209
|
+
options: {
|
|
210
|
+
name: "Ops Panel"
|
|
211
|
+
}
|
|
212
|
+
}),
|
|
213
|
+
/could not infer a surface for placement target "shell-layout:top-right". Pass --surface explicitly/
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("ui-generator placed-element rejects explicit surfaces that conflict with page-owned targets", async () => {
|
|
219
|
+
await withTempApp(async (appRoot) => {
|
|
220
|
+
await writeAppFixture(appRoot);
|
|
221
|
+
|
|
222
|
+
await assert.rejects(
|
|
223
|
+
() =>
|
|
224
|
+
runGeneratorSubcommand({
|
|
225
|
+
appRoot,
|
|
226
|
+
subcommand: "placed-element",
|
|
227
|
+
options: {
|
|
228
|
+
name: "Ops Panel",
|
|
229
|
+
placement: "admin-settings:forms",
|
|
230
|
+
surface: "console"
|
|
231
|
+
}
|
|
232
|
+
}),
|
|
233
|
+
/target "admin-settings:forms" belongs to surface "admin", so --surface console is invalid/
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
135
238
|
test("ui-generator placed-element subcommand refuses to overwrite an existing component without force", async () => {
|
|
136
239
|
await withTempApp(async (appRoot) => {
|
|
137
240
|
await writeAppFixture(appRoot);
|
|
@@ -6,5 +6,6 @@ test("ui-generator surface options validate against enabled surface ids", () =>
|
|
|
6
6
|
assert.equal(descriptor.kind, "generator");
|
|
7
7
|
assert.equal(descriptor.options?.surface?.validationType, "enabled-surface-id");
|
|
8
8
|
assert.equal(descriptor.metadata?.generatorSubcommands?.["placed-element"]?.optionNames?.includes("surface"), true);
|
|
9
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.["placed-element"]?.requiredOptionNames?.includes("surface"), false);
|
|
9
10
|
assert.equal(descriptor.metadata?.generatorSubcommands?.page?.optionNames?.includes("force"), true);
|
|
10
11
|
});
|