@jskit-ai/ui-generator 0.1.48 → 0.1.49
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 +40 -23
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +8 -1
- package/src/server/subcommands/addSubpages.js +77 -2
- package/src/server/subcommands/element.js +25 -7
- package/src/server/subcommands/outlet.js +139 -5
- package/src/server/subcommands/page.js +7 -2
- package/src/server/subcommands/pageSupport.js +1 -1
- package/src/server/subcommands/support.js +2 -0
- package/test/addSubpagesSubcommand.test.js +31 -7
- package/test/buildTemplateContext.test.js +196 -34
- package/test/elementSubcommand.test.js +76 -11
- package/test/outletSubcommand.test.js +47 -14
- package/test/pageSubcommand.test.js +100 -12
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.49",
|
|
5
5
|
kind: "generator",
|
|
6
6
|
description: "Create non-CRUD pages, reusable UI elements, and subpage hosts.",
|
|
7
7
|
options: {
|
|
@@ -40,22 +40,35 @@ export default Object.freeze({
|
|
|
40
40
|
inputType: "text",
|
|
41
41
|
defaultValue: "",
|
|
42
42
|
promptLabel: "Placement target",
|
|
43
|
-
promptHint: "
|
|
43
|
+
promptHint: "Semantic placement target for placed-element and outlet mapping (format: area.slot, default for placed-element: shell.status)."
|
|
44
44
|
},
|
|
45
|
-
|
|
45
|
+
owner: {
|
|
46
46
|
required: false,
|
|
47
47
|
inputType: "text",
|
|
48
48
|
defaultValue: "",
|
|
49
|
-
promptLabel: "
|
|
50
|
-
promptHint: "Optional
|
|
49
|
+
promptLabel: "Placement owner",
|
|
50
|
+
promptHint: "Optional owner id for semantic topology mappings. Page/settings placements default to the outlet host."
|
|
51
51
|
},
|
|
52
|
-
|
|
52
|
+
description: {
|
|
53
53
|
required: false,
|
|
54
54
|
inputType: "text",
|
|
55
55
|
defaultValue: "",
|
|
56
|
-
promptLabel: "
|
|
57
|
-
promptHint:
|
|
58
|
-
|
|
56
|
+
promptLabel: "Description",
|
|
57
|
+
promptHint: "Optional description for generated semantic topology mappings."
|
|
58
|
+
},
|
|
59
|
+
"link-renderer": {
|
|
60
|
+
required: false,
|
|
61
|
+
inputType: "text",
|
|
62
|
+
defaultValue: "local.main.ui.surface-aware-menu-link-item",
|
|
63
|
+
promptLabel: "Link renderer",
|
|
64
|
+
promptHint: "Default link renderer token for generated topology variants."
|
|
65
|
+
},
|
|
66
|
+
"link-placement": {
|
|
67
|
+
required: false,
|
|
68
|
+
inputType: "text",
|
|
69
|
+
defaultValue: "",
|
|
70
|
+
promptLabel: "Link placement",
|
|
71
|
+
promptHint: "Optional semantic target for the generated page link placement (format: area.slot)."
|
|
59
72
|
},
|
|
60
73
|
"link-to": {
|
|
61
74
|
required: false,
|
|
@@ -120,9 +133,9 @@ export default Object.freeze({
|
|
|
120
133
|
descriptionKey: "page-target-file"
|
|
121
134
|
}
|
|
122
135
|
],
|
|
123
|
-
optionNames: ["name", "link-placement", "link-
|
|
136
|
+
optionNames: ["name", "link-placement", "link-to", "force"],
|
|
124
137
|
notes: [
|
|
125
|
-
"If a nearest parent subpages target is found, placement
|
|
138
|
+
"If a nearest parent subpages target is found, semantic placement and props.to are inferred automatically.",
|
|
126
139
|
"If the parent target page is index.vue, child pages belong under index/...",
|
|
127
140
|
"If the target page file already exists, rerun with --force to overwrite it."
|
|
128
141
|
],
|
|
@@ -151,10 +164,10 @@ export default Object.freeze({
|
|
|
151
164
|
entrypoint: "src/server/subcommands/element.js",
|
|
152
165
|
export: "runGeneratorSubcommand",
|
|
153
166
|
description: "Create a Vue component file under the chosen component directory (default: src/components) and add a placement entry that renders it.",
|
|
154
|
-
optionNames: ["name", "surface", "path", "placement", "force"],
|
|
167
|
+
optionNames: ["name", "surface", "path", "placement", "owner", "force"],
|
|
155
168
|
requiredOptionNames: ["name"],
|
|
156
169
|
notes: [
|
|
157
|
-
"If --placement is omitted, the placed element is added at shell
|
|
170
|
+
"If --placement is omitted, the placed element is added at shell.status.",
|
|
158
171
|
"If the placement target belongs to a page-owned outlet, JSKIT infers the surface automatically.",
|
|
159
172
|
"If the target is shared and the app has multiple enabled surfaces, pass --surface explicitly.",
|
|
160
173
|
"If the component file already exists, rerun with --force to overwrite it."
|
|
@@ -174,7 +187,7 @@ export default Object.freeze({
|
|
|
174
187
|
" --name \"Ops Panel\" \\",
|
|
175
188
|
" --surface admin \\",
|
|
176
189
|
" --path src/widgets \\",
|
|
177
|
-
" --placement shell
|
|
190
|
+
" --placement shell.status \\",
|
|
178
191
|
" --force"
|
|
179
192
|
]
|
|
180
193
|
}
|
|
@@ -226,9 +239,9 @@ export default Object.freeze({
|
|
|
226
239
|
export: "runGeneratorSubcommand",
|
|
227
240
|
description: "Inject a generic ShellOutlet block into an existing Vue page/component.",
|
|
228
241
|
longDescription: [
|
|
229
|
-
"A ShellOutlet creates a
|
|
230
|
-
"
|
|
231
|
-
"
|
|
242
|
+
"A ShellOutlet creates a concrete placement recipient inside a Vue file.",
|
|
243
|
+
"The command also appends the semantic topology mapping for that outlet, so `jskit list-placements` exposes the public `area.slot` placement instead of leaving the concrete recipient unused.",
|
|
244
|
+
"Generated topology includes compact, medium, and expanded variants. By default all three point at the inserted outlet; hand-edit `src/placementTopology.js` if the adaptive layout uses different concrete outlets."
|
|
232
245
|
],
|
|
233
246
|
positionalArgs: [
|
|
234
247
|
{
|
|
@@ -237,10 +250,11 @@ export default Object.freeze({
|
|
|
237
250
|
descriptionKey: "existing-vue-sfc-target-file"
|
|
238
251
|
}
|
|
239
252
|
],
|
|
240
|
-
optionNames: ["target"],
|
|
241
|
-
requiredOptionNames: ["target"],
|
|
253
|
+
optionNames: ["target", "placement", "owner", "surface", "description", "link-renderer"],
|
|
254
|
+
requiredOptionNames: ["target", "placement"],
|
|
242
255
|
notes: [
|
|
243
|
-
"Use --target host:position."
|
|
256
|
+
"Use --target host:position for the concrete outlet and --placement area.slot for the public semantic placement.",
|
|
257
|
+
"The generated topology maps compact, medium, and expanded to the new concrete outlet."
|
|
244
258
|
],
|
|
245
259
|
examples: [
|
|
246
260
|
{
|
|
@@ -248,7 +262,8 @@ export default Object.freeze({
|
|
|
248
262
|
lines: [
|
|
249
263
|
"npx jskit generate ui-generator outlet \\",
|
|
250
264
|
" src/components/ContactSummaryCard.vue \\",
|
|
251
|
-
" --target contact-view:sub-pages"
|
|
265
|
+
" --target contact-view:sub-pages \\",
|
|
266
|
+
" --placement page.section-nav"
|
|
252
267
|
]
|
|
253
268
|
},
|
|
254
269
|
{
|
|
@@ -256,7 +271,9 @@ export default Object.freeze({
|
|
|
256
271
|
lines: [
|
|
257
272
|
"npx jskit generate ui-generator outlet \\",
|
|
258
273
|
" src/pages/admin/customers/[customerId]/index.vue \\",
|
|
259
|
-
" --target customer-view:summary-actions"
|
|
274
|
+
" --target customer-view:summary-actions \\",
|
|
275
|
+
" --placement page.actions \\",
|
|
276
|
+
" --surface admin"
|
|
260
277
|
]
|
|
261
278
|
}
|
|
262
279
|
]
|
|
@@ -278,7 +295,7 @@ export default Object.freeze({
|
|
|
278
295
|
mutations: {
|
|
279
296
|
dependencies: {
|
|
280
297
|
runtime: {
|
|
281
|
-
"@jskit-ai/users-web": "0.1.
|
|
298
|
+
"@jskit-ai/users-web": "0.1.81"
|
|
282
299
|
},
|
|
283
300
|
dev: {}
|
|
284
301
|
},
|
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.49",
|
|
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.66",
|
|
10
|
+
"@jskit-ai/shell-web": "0.1.65"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
"./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
|
|
@@ -12,6 +12,13 @@ function resolveLinkToPropLine(linkTo = "") {
|
|
|
12
12
|
return ` to: ${JSON.stringify(linkTo)},\n`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function resolveOwnerLine(owner = "") {
|
|
16
|
+
if (!owner) {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
return ` owner: ${JSON.stringify(owner)},\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
async function buildUiPageTemplateContext({
|
|
16
23
|
appRoot,
|
|
17
24
|
targetFile = "",
|
|
@@ -28,13 +35,13 @@ async function buildUiPageTemplateContext({
|
|
|
28
35
|
targetFile,
|
|
29
36
|
context: "ui-generator page",
|
|
30
37
|
placement: options?.["link-placement"],
|
|
31
|
-
componentToken: options?.["link-component-token"],
|
|
32
38
|
linkTo: options?.["link-to"]
|
|
33
39
|
});
|
|
34
40
|
|
|
35
41
|
return {
|
|
36
42
|
__JSKIT_UI_LINK_PLACEMENT_ID__: pageTarget.placementId,
|
|
37
43
|
__JSKIT_UI_LINK_PLACEMENT_TARGET__: String(linkTarget.placementTarget?.id || ""),
|
|
44
|
+
__JSKIT_UI_LINK_OWNER_LINE__: resolveOwnerLine(linkTarget.placementTarget?.owner || ""),
|
|
38
45
|
__JSKIT_UI_LINK_COMPONENT_TOKEN__: String(linkTarget.componentToken || ""),
|
|
39
46
|
__JSKIT_UI_LINK_ICON__: DEFAULT_GENERATED_LINK_ICON,
|
|
40
47
|
__JSKIT_UI_LINK_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
1
2
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
3
|
import {
|
|
3
4
|
DEFAULT_COMPONENT_DIRECTORY,
|
|
@@ -7,11 +8,60 @@ import {
|
|
|
7
8
|
upgradePageFileToSubpages
|
|
8
9
|
} from "./pageSupport.js";
|
|
9
10
|
import {
|
|
11
|
+
PLACEMENT_TOPOLOGY_FILE,
|
|
12
|
+
appendBlockIfMarkerMissing,
|
|
10
13
|
requireSinglePositionalTargetFile,
|
|
14
|
+
resolvePathWithinApp,
|
|
11
15
|
resolveOutletTargetId,
|
|
12
16
|
rejectUnexpectedOptions
|
|
13
17
|
} from "./support.js";
|
|
14
18
|
|
|
19
|
+
function resolveOutletOwner(target = "") {
|
|
20
|
+
const normalizedTarget = normalizeText(target);
|
|
21
|
+
const separatorIndex = normalizedTarget.indexOf(":");
|
|
22
|
+
if (separatorIndex <= 0) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
return normalizedTarget.slice(0, separatorIndex);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderSectionNavTopologyBlock({
|
|
29
|
+
marker = "",
|
|
30
|
+
owner = "",
|
|
31
|
+
surface = "",
|
|
32
|
+
target = ""
|
|
33
|
+
} = {}) {
|
|
34
|
+
return (
|
|
35
|
+
`// ${marker}\n` +
|
|
36
|
+
"addPlacementTopology({\n" +
|
|
37
|
+
` id: "page.section-nav",\n` +
|
|
38
|
+
` owner: "${owner}",\n` +
|
|
39
|
+
` description: "Navigation between child pages in this section.",\n` +
|
|
40
|
+
` surfaces: ["${surface}"],\n` +
|
|
41
|
+
" variants: {\n" +
|
|
42
|
+
" compact: {\n" +
|
|
43
|
+
` outlet: "${target}",\n` +
|
|
44
|
+
" renderers: {\n" +
|
|
45
|
+
` link: "local.main.ui.surface-aware-menu-link-item"\n` +
|
|
46
|
+
" }\n" +
|
|
47
|
+
" },\n" +
|
|
48
|
+
" medium: {\n" +
|
|
49
|
+
` outlet: "${target}",\n` +
|
|
50
|
+
" renderers: {\n" +
|
|
51
|
+
` link: "local.main.ui.surface-aware-menu-link-item"\n` +
|
|
52
|
+
" }\n" +
|
|
53
|
+
" },\n" +
|
|
54
|
+
" expanded: {\n" +
|
|
55
|
+
` outlet: "${target}",\n` +
|
|
56
|
+
" renderers: {\n" +
|
|
57
|
+
` link: "local.main.ui.surface-aware-menu-link-item"\n` +
|
|
58
|
+
" }\n" +
|
|
59
|
+
" }\n" +
|
|
60
|
+
" }\n" +
|
|
61
|
+
"});\n"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
15
65
|
function resolveSubpagesOutletTarget(options = {}, pageTarget = {}) {
|
|
16
66
|
const rawTarget = normalizeText(options?.target);
|
|
17
67
|
const defaultTarget = `${deriveDefaultSubpagesHost(pageTarget)}:${DEFAULT_SUBPAGES_POSITION}`;
|
|
@@ -60,10 +110,35 @@ async function runGeneratorSubcommand({
|
|
|
60
110
|
dryRun
|
|
61
111
|
});
|
|
62
112
|
|
|
113
|
+
const topologyPath = resolvePathWithinApp(appRoot, PLACEMENT_TOPOLOGY_FILE, {
|
|
114
|
+
context: "ui-generator add-subpages"
|
|
115
|
+
});
|
|
116
|
+
const owner = resolveOutletOwner(outletTarget.id);
|
|
117
|
+
const topologyMarker = `jskit:ui-generator.topology:page.section-nav:${owner}`;
|
|
118
|
+
const topologySource = await readFile(topologyPath.absolutePath, "utf8");
|
|
119
|
+
const topologyApplied = appendBlockIfMarkerMissing(
|
|
120
|
+
topologySource,
|
|
121
|
+
topologyMarker,
|
|
122
|
+
renderSectionNavTopologyBlock({
|
|
123
|
+
marker: topologyMarker,
|
|
124
|
+
owner,
|
|
125
|
+
surface: result.surfaceId,
|
|
126
|
+
target: outletTarget.id
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
const touchedFiles = new Set(result.touchedFiles);
|
|
130
|
+
if (topologyApplied.changed) {
|
|
131
|
+
if (dryRun !== true) {
|
|
132
|
+
await writeFile(topologyPath.absolutePath, topologyApplied.content, "utf8");
|
|
133
|
+
}
|
|
134
|
+
touchedFiles.add(topologyPath.relativePath);
|
|
135
|
+
}
|
|
136
|
+
const touchedFileList = [...touchedFiles].sort((left, right) => left.localeCompare(right));
|
|
137
|
+
|
|
63
138
|
return {
|
|
64
|
-
touchedFiles:
|
|
139
|
+
touchedFiles: touchedFileList,
|
|
65
140
|
summary:
|
|
66
|
-
|
|
141
|
+
touchedFileList.length > 0
|
|
67
142
|
? `Enabled subpages in ${result.targetFile} for "${pageTarget.routeUrlSuffix}" using outlet target "${outletTarget.id}".`
|
|
68
143
|
: `Subpages are already enabled in ${result.targetFile}.`
|
|
69
144
|
};
|
|
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import {
|
|
4
4
|
listSurfacePageRoots,
|
|
5
5
|
resolveBestSurfaceMatchFromPageFile,
|
|
6
|
-
|
|
6
|
+
resolveSemanticPlacementTargetFromApp,
|
|
7
7
|
toPosixPath
|
|
8
8
|
} from "@jskit-ai/kernel/server/support";
|
|
9
9
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface";
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
insertBeforeClassDeclaration
|
|
23
23
|
} from "./support.js";
|
|
24
24
|
|
|
25
|
-
const DEFAULT_ELEMENT_PLACEMENT = "shell
|
|
25
|
+
const DEFAULT_ELEMENT_PLACEMENT = "shell.status";
|
|
26
26
|
|
|
27
27
|
function renderElementComponentSource(elementName = "") {
|
|
28
28
|
return `<template>
|
|
@@ -41,14 +41,28 @@ async function resolvePlacedElementSurface({
|
|
|
41
41
|
context = "ui-generator placed-element"
|
|
42
42
|
} = {}) {
|
|
43
43
|
const explicitSurface = normalizeSurfaceId(surface);
|
|
44
|
+
const placementSurfaces = Array.isArray(placementTarget?.surfaces)
|
|
45
|
+
? placementTarget.surfaces.map((entry) => normalizeSurfaceId(entry)).filter(Boolean)
|
|
46
|
+
: [];
|
|
47
|
+
const isPlacementGlobal = placementSurfaces.length < 1 || placementSurfaces.includes("*");
|
|
48
|
+
const concreteSourcePath = normalizeText(placementTarget?.sourcePath).startsWith("src/")
|
|
49
|
+
? normalizeText(placementTarget?.sourcePath)
|
|
50
|
+
: "";
|
|
44
51
|
const inferredSurfaceMatch = await resolveBestSurfaceMatchFromPageFile(
|
|
45
|
-
|
|
52
|
+
concreteSourcePath,
|
|
46
53
|
await listSurfacePageRoots(appRoot, { context }),
|
|
47
54
|
{ context }
|
|
48
55
|
);
|
|
49
|
-
const inferredSurface =
|
|
56
|
+
const inferredSurface =
|
|
57
|
+
normalizeSurfaceId(inferredSurfaceMatch?.surfaceId) ||
|
|
58
|
+
(placementSurfaces.length === 1 && !isPlacementGlobal ? placementSurfaces[0] : "");
|
|
50
59
|
|
|
51
60
|
if (explicitSurface) {
|
|
61
|
+
if (!isPlacementGlobal && placementSurfaces.length > 0 && !placementSurfaces.includes(explicitSurface)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${context} target "${normalizeText(placementTarget?.id) || "<unknown>"}" is not available on surface "${explicitSurface}".`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
52
66
|
if (inferredSurface && explicitSurface !== inferredSurface) {
|
|
53
67
|
throw new Error(
|
|
54
68
|
`${context} target "${normalizeText(placementTarget?.id) || "<unknown>"}" belongs to surface "${inferredSurface}", ` +
|
|
@@ -89,7 +103,7 @@ async function runGeneratorSubcommand({
|
|
|
89
103
|
if (Array.isArray(args) && args.length > 0) {
|
|
90
104
|
throw new Error("ui-generator placed-element does not accept positional arguments.");
|
|
91
105
|
}
|
|
92
|
-
rejectUnexpectedOptions(options, ["name", "surface", "path", "placement", "force"], {
|
|
106
|
+
rejectUnexpectedOptions(options, ["name", "surface", "path", "placement", "owner", "force"], {
|
|
93
107
|
context: "ui-generator placed-element"
|
|
94
108
|
});
|
|
95
109
|
|
|
@@ -115,10 +129,11 @@ async function runGeneratorSubcommand({
|
|
|
115
129
|
context: "ui-generator placed-element"
|
|
116
130
|
});
|
|
117
131
|
const componentToken = `local.main.ui.element.${elementNameKebab}`;
|
|
118
|
-
const placementTarget = await
|
|
132
|
+
const placementTarget = await resolveSemanticPlacementTargetFromApp({
|
|
119
133
|
appRoot,
|
|
120
134
|
context: "ui-generator",
|
|
121
|
-
placement: options?.placement || DEFAULT_ELEMENT_PLACEMENT
|
|
135
|
+
placement: options?.placement || DEFAULT_ELEMENT_PLACEMENT,
|
|
136
|
+
owner: options?.owner
|
|
122
137
|
});
|
|
123
138
|
const surface = await resolvePlacedElementSurface({
|
|
124
139
|
appRoot,
|
|
@@ -179,12 +194,15 @@ async function runGeneratorSubcommand({
|
|
|
179
194
|
|
|
180
195
|
const placementSource = await readFile(placementPath.absolutePath, "utf8");
|
|
181
196
|
const placementMarker = `jskit:ui-generator.element:${surface}:${elementNameKebab}`;
|
|
197
|
+
const ownerLine = placementTarget.owner ? ` owner: "${placementTarget.owner}",\n` : "";
|
|
182
198
|
const placementBlock =
|
|
183
199
|
`// ${placementMarker}\n` +
|
|
184
200
|
"{\n" +
|
|
185
201
|
" addPlacement({\n" +
|
|
186
202
|
` id: "ui-generator.element.${elementNameKebab}",\n` +
|
|
187
203
|
` target: "${placementTarget.id}",\n` +
|
|
204
|
+
ownerLine +
|
|
205
|
+
` kind: "component",\n` +
|
|
188
206
|
` surfaces: ["${surface}"],\n` +
|
|
189
207
|
" order: 155,\n" +
|
|
190
208
|
` componentToken: "${componentToken}"\n` +
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import {
|
|
4
|
+
normalizePlacementOwnerId,
|
|
5
|
+
normalizePlacementSurfaceId,
|
|
6
|
+
normalizeSemanticPlacementId
|
|
7
|
+
} from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
|
|
8
|
+
import {
|
|
9
|
+
PLACEMENT_TOPOLOGY_FILE,
|
|
10
|
+
appendBlockIfMarkerMissing,
|
|
4
11
|
requireSinglePositionalTargetFile,
|
|
5
12
|
rejectUnexpectedOptions,
|
|
6
13
|
resolveOutletTargetId,
|
|
@@ -75,6 +82,96 @@ function createOutletBlock({ target = "" } = {}) {
|
|
|
75
82
|
return `<ShellOutlet target="${target}" />`;
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
function resolveOutletOwner(target = "") {
|
|
86
|
+
const normalizedTarget = normalizeText(target);
|
|
87
|
+
const separatorIndex = normalizedTarget.indexOf(":");
|
|
88
|
+
if (separatorIndex <= 0) {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
return normalizedTarget.slice(0, separatorIndex);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveSemanticPlacementOption(options = {}) {
|
|
95
|
+
const placementId = normalizeSemanticPlacementId(options?.placement);
|
|
96
|
+
if (!placementId) {
|
|
97
|
+
throw new Error('ui-generator outlet requires --placement in semantic "area.slot" format.');
|
|
98
|
+
}
|
|
99
|
+
return placementId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveSemanticPlacementOwner({ placementId = "", targetId = "", owner = "" } = {}) {
|
|
103
|
+
const explicitOwner = normalizePlacementOwnerId(owner);
|
|
104
|
+
if (explicitOwner) {
|
|
105
|
+
return explicitOwner;
|
|
106
|
+
}
|
|
107
|
+
if (placementId.startsWith("page.") || placementId.startsWith("settings.")) {
|
|
108
|
+
return resolveOutletOwner(targetId);
|
|
109
|
+
}
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveTopologySurfaces(options = {}) {
|
|
114
|
+
const surface = normalizePlacementSurfaceId(options?.surface);
|
|
115
|
+
if (surface) {
|
|
116
|
+
return [surface];
|
|
117
|
+
}
|
|
118
|
+
return ["*"];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderTopologyOwnerLine(owner = "") {
|
|
122
|
+
if (!owner) {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
return ` owner: "${owner}",\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderLinkRendererBlock(rendererToken = "") {
|
|
129
|
+
const normalizedRendererToken = normalizeText(rendererToken) || "local.main.ui.surface-aware-menu-link-item";
|
|
130
|
+
return (
|
|
131
|
+
" renderers: {\n" +
|
|
132
|
+
` link: "${normalizedRendererToken}"\n` +
|
|
133
|
+
" }\n"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderOutletTopologyBlock({
|
|
138
|
+
marker = "",
|
|
139
|
+
placementId = "",
|
|
140
|
+
owner = "",
|
|
141
|
+
surfaces = ["*"],
|
|
142
|
+
description = "",
|
|
143
|
+
target = "",
|
|
144
|
+
rendererToken = ""
|
|
145
|
+
} = {}) {
|
|
146
|
+
const descriptionLine = normalizeText(description)
|
|
147
|
+
? ` description: ${JSON.stringify(normalizeText(description))},\n`
|
|
148
|
+
: "";
|
|
149
|
+
const rendererBlock = renderLinkRendererBlock(rendererToken);
|
|
150
|
+
return (
|
|
151
|
+
`// ${marker}\n` +
|
|
152
|
+
"addPlacementTopology({\n" +
|
|
153
|
+
` id: "${placementId}",\n` +
|
|
154
|
+
renderTopologyOwnerLine(owner) +
|
|
155
|
+
descriptionLine +
|
|
156
|
+
` surfaces: ${JSON.stringify(surfaces)},\n` +
|
|
157
|
+
" variants: {\n" +
|
|
158
|
+
" compact: {\n" +
|
|
159
|
+
` outlet: "${target}",\n` +
|
|
160
|
+
rendererBlock +
|
|
161
|
+
" },\n" +
|
|
162
|
+
" medium: {\n" +
|
|
163
|
+
` outlet: "${target}",\n` +
|
|
164
|
+
rendererBlock +
|
|
165
|
+
" },\n" +
|
|
166
|
+
" expanded: {\n" +
|
|
167
|
+
` outlet: "${target}",\n` +
|
|
168
|
+
rendererBlock +
|
|
169
|
+
" }\n" +
|
|
170
|
+
" }\n" +
|
|
171
|
+
"});\n"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
78
175
|
function findLastTemplateCloseTag(source = "") {
|
|
79
176
|
const sourceText = String(source || "");
|
|
80
177
|
let lastMatch = null;
|
|
@@ -123,7 +220,7 @@ async function runGeneratorSubcommand({
|
|
|
123
220
|
throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
124
221
|
}
|
|
125
222
|
const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator outlet" });
|
|
126
|
-
rejectUnexpectedOptions(options, ["target"], {
|
|
223
|
+
rejectUnexpectedOptions(options, ["target", "placement", "owner", "surface", "description", "link-renderer"], {
|
|
127
224
|
context: "ui-generator outlet"
|
|
128
225
|
});
|
|
129
226
|
|
|
@@ -132,6 +229,13 @@ async function runGeneratorSubcommand({
|
|
|
132
229
|
optionName: "target"
|
|
133
230
|
});
|
|
134
231
|
const targetId = outletTarget.id;
|
|
232
|
+
const placementId = resolveSemanticPlacementOption(options);
|
|
233
|
+
const owner = resolveSemanticPlacementOwner({
|
|
234
|
+
placementId,
|
|
235
|
+
targetId,
|
|
236
|
+
owner: options?.owner
|
|
237
|
+
});
|
|
238
|
+
const surfaces = resolveTopologySurfaces(options);
|
|
135
239
|
|
|
136
240
|
const targetFilePath = resolvePathWithinApp(appRoot, targetFile, {
|
|
137
241
|
context: "ui-generator outlet"
|
|
@@ -162,11 +266,41 @@ async function runGeneratorSubcommand({
|
|
|
162
266
|
await writeFile(targetFilePath.absolutePath, scriptApplied.content, "utf8");
|
|
163
267
|
}
|
|
164
268
|
|
|
269
|
+
const topologyPath = resolvePathWithinApp(appRoot, PLACEMENT_TOPOLOGY_FILE, {
|
|
270
|
+
context: "ui-generator outlet"
|
|
271
|
+
});
|
|
272
|
+
const topologyMarker = `jskit:ui-generator.topology:${placementId}:${owner || "global"}`;
|
|
273
|
+
const topologySource = await readFile(topologyPath.absolutePath, "utf8");
|
|
274
|
+
const topologyApplied = appendBlockIfMarkerMissing(
|
|
275
|
+
topologySource,
|
|
276
|
+
topologyMarker,
|
|
277
|
+
renderOutletTopologyBlock({
|
|
278
|
+
marker: topologyMarker,
|
|
279
|
+
placementId,
|
|
280
|
+
owner,
|
|
281
|
+
surfaces,
|
|
282
|
+
description: options?.description,
|
|
283
|
+
target: targetId,
|
|
284
|
+
rendererToken: options?.["link-renderer"]
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
if (topologyApplied.changed && dryRun !== true) {
|
|
288
|
+
await writeFile(topologyPath.absolutePath, topologyApplied.content, "utf8");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const touchedFiles = new Set();
|
|
292
|
+
if (changed) {
|
|
293
|
+
touchedFiles.add(targetFilePath.relativePath);
|
|
294
|
+
}
|
|
295
|
+
if (topologyApplied.changed) {
|
|
296
|
+
touchedFiles.add(topologyPath.relativePath);
|
|
297
|
+
}
|
|
298
|
+
|
|
165
299
|
return {
|
|
166
|
-
touchedFiles:
|
|
167
|
-
summary:
|
|
168
|
-
? `Injected outlet "${targetId}"
|
|
169
|
-
: `Outlet "${targetId}"
|
|
300
|
+
touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
|
|
301
|
+
summary: touchedFiles.size > 0
|
|
302
|
+
? `Injected outlet "${targetId}" and mapped semantic placement "${placementId}"${owner ? ` for owner "${owner}"` : ""}.`
|
|
303
|
+
: `Outlet "${targetId}" and semantic placement "${placementId}" are already present.`
|
|
170
304
|
};
|
|
171
305
|
}
|
|
172
306
|
|
|
@@ -20,15 +20,20 @@ function renderPageLinkPlacementBlock({
|
|
|
20
20
|
label = "",
|
|
21
21
|
surface = ""
|
|
22
22
|
} = {}) {
|
|
23
|
+
const componentTokenLine = context.__JSKIT_UI_LINK_COMPONENT_TOKEN__
|
|
24
|
+
? ` componentToken: "${context.__JSKIT_UI_LINK_COMPONENT_TOKEN__}",\n`
|
|
25
|
+
: "";
|
|
23
26
|
return (
|
|
24
27
|
`// ${marker}\n` +
|
|
25
28
|
"{\n" +
|
|
26
29
|
" addPlacement({\n" +
|
|
27
30
|
` id: "${context.__JSKIT_UI_LINK_PLACEMENT_ID__}",\n` +
|
|
28
31
|
` target: "${context.__JSKIT_UI_LINK_PLACEMENT_TARGET__}",\n` +
|
|
32
|
+
`${context.__JSKIT_UI_LINK_OWNER_LINE__}` +
|
|
33
|
+
` kind: "link",\n` +
|
|
29
34
|
` surfaces: ["${surface}"],\n` +
|
|
30
35
|
" order: 155,\n" +
|
|
31
|
-
|
|
36
|
+
componentTokenLine +
|
|
32
37
|
" props: {\n" +
|
|
33
38
|
` label: "${label}",\n` +
|
|
34
39
|
` icon: "${context.__JSKIT_UI_LINK_ICON__}",\n` +
|
|
@@ -56,7 +61,7 @@ async function runGeneratorSubcommand({
|
|
|
56
61
|
const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator page" });
|
|
57
62
|
rejectUnexpectedOptions(
|
|
58
63
|
options,
|
|
59
|
-
["name", "link-placement", "link-
|
|
64
|
+
["name", "link-placement", "link-to", "force"],
|
|
60
65
|
{ context: "ui-generator page" }
|
|
61
66
|
);
|
|
62
67
|
|
|
@@ -313,7 +313,7 @@ function renderSubpagesTemplate({
|
|
|
313
313
|
"<template>",
|
|
314
314
|
renderSectionContainerOpenTag({ title, subtitle }),
|
|
315
315
|
" <template #tabs>",
|
|
316
|
-
` <ShellOutlet target="${normalizedTarget}"
|
|
316
|
+
` <ShellOutlet target="${normalizedTarget}" />`,
|
|
317
317
|
" </template>"
|
|
318
318
|
];
|
|
319
319
|
|
|
@@ -10,6 +10,7 @@ import { toCamelCase, toSnakeCase } from "@jskit-ai/kernel/shared/support/string
|
|
|
10
10
|
const DEFAULT_COMPONENT_DIRECTORY = "src/components";
|
|
11
11
|
const MAIN_CLIENT_PROVIDER_FILE = "packages/main/src/client/providers/MainClientProvider.js";
|
|
12
12
|
const PLACEMENT_FILE = "src/placement.js";
|
|
13
|
+
const PLACEMENT_TOPOLOGY_FILE = "src/placementTopology.js";
|
|
13
14
|
const SCRIPT_TAG_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
14
15
|
const SCRIPT_SETUP_ATTRIBUTE_PATTERN = /\bsetup\b/i;
|
|
15
16
|
const ATTRIBUTE_PATTERN = /([:@]?[A-Za-z_][A-Za-z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
|
|
@@ -266,6 +267,7 @@ export {
|
|
|
266
267
|
DEFAULT_COMPONENT_DIRECTORY,
|
|
267
268
|
MAIN_CLIENT_PROVIDER_FILE,
|
|
268
269
|
PLACEMENT_FILE,
|
|
270
|
+
PLACEMENT_TOPOLOGY_FILE,
|
|
269
271
|
toKebabCase,
|
|
270
272
|
toPascalCase,
|
|
271
273
|
requireOption,
|