@jskit-ai/ui-generator 0.1.48 → 0.1.50
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 +117 -24
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +44 -3
- package/src/server/subcommands/addSubpages.js +89 -2
- package/src/server/subcommands/element.js +42 -10
- package/src/server/subcommands/outlet.js +399 -20
- package/src/server/subcommands/page.js +49 -23
- package/src/server/subcommands/pageSupport.js +115 -37
- package/src/server/subcommands/support.js +128 -23
- package/test/addSubpagesSubcommand.test.js +163 -15
- package/test/buildTemplateContext.test.js +227 -34
- package/test/elementSubcommand.test.js +89 -12
- package/test/outletSubcommand.test.js +305 -14
- package/test/packageDescriptor.test.js +11 -0
- package/test/pageSubcommand.test.js +234 -17
|
@@ -1,20 +1,29 @@
|
|
|
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
|
+
appendTopologyBlockIfPlacementMissing,
|
|
4
11
|
requireSinglePositionalTargetFile,
|
|
5
12
|
rejectUnexpectedOptions,
|
|
6
13
|
resolveOutletTargetId,
|
|
7
14
|
resolvePathWithinApp,
|
|
8
15
|
ensureTrailingNewline,
|
|
9
16
|
insertImportIfMissing,
|
|
10
|
-
|
|
17
|
+
findScriptSetupBlock,
|
|
18
|
+
insertScriptSetupBlock,
|
|
11
19
|
parseTagAttributes,
|
|
12
20
|
indentBlock
|
|
13
21
|
} from "./support.js";
|
|
14
22
|
|
|
15
|
-
const ROUTE_TAG_PATTERN = /<route\b[^>]*>[\s\S]*?<\/route>\s*/gi;
|
|
16
23
|
const TEMPLATE_CLOSE_TAG_PATTERN = /<\/template>/gi;
|
|
17
24
|
const SHELL_OUTLET_TAG_PATTERN = /<ShellOutlet\b([^>]*)\/?>/gi;
|
|
25
|
+
const TOPOLOGY_LAYOUT_CLASSES = Object.freeze(["compact", "medium", "expanded"]);
|
|
26
|
+
const TOPOLOGY_KINDS = new Set(["component", "link"]);
|
|
18
27
|
|
|
19
28
|
function hasShellOutletTarget(source = "", { target = "" } = {}) {
|
|
20
29
|
const normalizedTarget = normalizeText(target);
|
|
@@ -35,21 +44,12 @@ function hasShellOutletTarget(source = "", { target = "" } = {}) {
|
|
|
35
44
|
|
|
36
45
|
function applyScriptImports(source = "") {
|
|
37
46
|
const sourceText = String(source || "");
|
|
38
|
-
const scriptBlock =
|
|
47
|
+
const scriptBlock = findScriptSetupBlock(sourceText);
|
|
39
48
|
|
|
40
49
|
const shellOutletImport = "import ShellOutlet from \"@jskit-ai/shell-web/client/components/ShellOutlet\";";
|
|
41
50
|
|
|
42
51
|
if (!scriptBlock) {
|
|
43
|
-
|
|
44
|
-
let insertionIndex = 0;
|
|
45
|
-
for (const match of sourceText.matchAll(ROUTE_TAG_PATTERN)) {
|
|
46
|
-
insertionIndex = match.index + String(match[0] || "").length;
|
|
47
|
-
}
|
|
48
|
-
const separator = insertionIndex > 0 ? "\n" : "";
|
|
49
|
-
return {
|
|
50
|
-
changed: true,
|
|
51
|
-
content: `${sourceText.slice(0, insertionIndex)}${separator}${scriptSetupBlock}\n${sourceText.slice(insertionIndex)}`
|
|
52
|
-
};
|
|
52
|
+
return insertScriptSetupBlock(sourceText, shellOutletImport);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
let nextScriptContent = scriptBlock.content;
|
|
@@ -75,6 +75,204 @@ function createOutletBlock({ target = "" } = {}) {
|
|
|
75
75
|
return `<ShellOutlet target="${target}" />`;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function resolveOutletOwner(target = "") {
|
|
79
|
+
const normalizedTarget = normalizeText(target);
|
|
80
|
+
const separatorIndex = normalizedTarget.indexOf(":");
|
|
81
|
+
if (separatorIndex <= 0) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
return normalizedTarget.slice(0, separatorIndex);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveSemanticPlacementOption(options = {}, { context = "ui-generator outlet" } = {}) {
|
|
88
|
+
const placementId = resolveOptionalSemanticPlacementOption(options, { context });
|
|
89
|
+
if (!placementId) {
|
|
90
|
+
throw new Error(`${context} requires --placement in semantic "area.slot" format.`);
|
|
91
|
+
}
|
|
92
|
+
return placementId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveOptionalSemanticPlacementOption(options = {}, { context = "ui-generator outlet" } = {}) {
|
|
96
|
+
const rawPlacement = normalizeText(options?.placement);
|
|
97
|
+
if (!rawPlacement) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
const placementId = normalizeSemanticPlacementId(rawPlacement);
|
|
101
|
+
if (!placementId) {
|
|
102
|
+
throw new Error(`${context} requires --placement in semantic "area.slot" format.`);
|
|
103
|
+
}
|
|
104
|
+
return placementId;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveVariantOwners(variantTargets = {}) {
|
|
108
|
+
return [
|
|
109
|
+
...new Set(
|
|
110
|
+
Object.values(variantTargets || {})
|
|
111
|
+
.map((target) => resolveOutletOwner(target))
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
)
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveSemanticPlacementOwner({
|
|
118
|
+
placementId = "",
|
|
119
|
+
targetId = "",
|
|
120
|
+
variantTargets = null,
|
|
121
|
+
owner = "",
|
|
122
|
+
context = "ui-generator outlet"
|
|
123
|
+
} = {}) {
|
|
124
|
+
const explicitOwner = normalizePlacementOwnerId(owner);
|
|
125
|
+
if (explicitOwner) {
|
|
126
|
+
return explicitOwner;
|
|
127
|
+
}
|
|
128
|
+
if (placementId.startsWith("page.") || placementId.startsWith("settings.")) {
|
|
129
|
+
const variantOwners = resolveVariantOwners(variantTargets);
|
|
130
|
+
if (variantOwners.length > 1) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${context} requires --owner because semantic placement "${placementId}" maps to multiple outlet hosts: ${variantOwners.join(", ")}.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (variantOwners.length === 1) {
|
|
136
|
+
return variantOwners[0];
|
|
137
|
+
}
|
|
138
|
+
return resolveOutletOwner(targetId);
|
|
139
|
+
}
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveTopologyKind(options = {}, { context = "ui-generator outlet", defaultKind = "", required = false } = {}) {
|
|
144
|
+
const rawKind = normalizeText(options?.kind).toLowerCase();
|
|
145
|
+
if (!rawKind) {
|
|
146
|
+
if (defaultKind) {
|
|
147
|
+
return defaultKind;
|
|
148
|
+
}
|
|
149
|
+
if (required) {
|
|
150
|
+
throw new Error(`${context} requires --kind component or --kind link.`);
|
|
151
|
+
}
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
if (!TOPOLOGY_KINDS.has(rawKind)) {
|
|
155
|
+
throw new Error(`${context} option "kind" must be one of: component, link.`);
|
|
156
|
+
}
|
|
157
|
+
return rawKind;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveTopologyVariantTargets(options = {}, { context = "ui-generator topology", fallbackTarget = "" } = {}) {
|
|
161
|
+
const fallback = normalizeText(options?.target) || normalizeText(fallbackTarget);
|
|
162
|
+
const rawTargets = {
|
|
163
|
+
compact: normalizeText(options?.["compact-target"]) || fallback,
|
|
164
|
+
medium: normalizeText(options?.["medium-target"]) || fallback,
|
|
165
|
+
expanded: normalizeText(options?.["expanded-target"]) || fallback
|
|
166
|
+
};
|
|
167
|
+
const missingLayouts = TOPOLOGY_LAYOUT_CLASSES.filter((layoutClass) => !rawTargets[layoutClass]);
|
|
168
|
+
if (missingLayouts.length > 0) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`${context} requires --target or all layout targets: --compact-target, --medium-target, --expanded-target. Missing: ${missingLayouts.join(", ")}.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return Object.freeze(
|
|
175
|
+
Object.fromEntries(
|
|
176
|
+
TOPOLOGY_LAYOUT_CLASSES.map((layoutClass) => {
|
|
177
|
+
const target = resolveOutletTargetId(rawTargets[layoutClass], {
|
|
178
|
+
context,
|
|
179
|
+
optionName: `${layoutClass}-target`
|
|
180
|
+
});
|
|
181
|
+
return [layoutClass, target.id];
|
|
182
|
+
})
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function hasTopologyOptions(options = {}) {
|
|
188
|
+
return [
|
|
189
|
+
"compact-target",
|
|
190
|
+
"medium-target",
|
|
191
|
+
"expanded-target",
|
|
192
|
+
"owner",
|
|
193
|
+
"surface",
|
|
194
|
+
"description",
|
|
195
|
+
"kind",
|
|
196
|
+
"link-renderer"
|
|
197
|
+
].some((optionName) => normalizeText(options?.[optionName]));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveTopologySurfaces(options = {}) {
|
|
201
|
+
const surface = normalizePlacementSurfaceId(options?.surface);
|
|
202
|
+
if (surface) {
|
|
203
|
+
return [surface];
|
|
204
|
+
}
|
|
205
|
+
return ["*"];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderTopologyOwnerLine(owner = "") {
|
|
209
|
+
if (!owner) {
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
212
|
+
return ` owner: "${owner}",\n`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderLinkRendererBlock(rendererToken = "") {
|
|
216
|
+
const normalizedRendererToken = normalizeText(rendererToken) || "local.main.ui.surface-aware-menu-link-item";
|
|
217
|
+
return (
|
|
218
|
+
" renderers: {\n" +
|
|
219
|
+
` link: "${normalizedRendererToken}"\n` +
|
|
220
|
+
" }\n"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function renderTopologyVariantBlock({ layoutClass = "", target = "", kind = "", rendererToken = "" } = {}) {
|
|
225
|
+
const rendererBlock = kind === "link" ? renderLinkRendererBlock(rendererToken) : "";
|
|
226
|
+
const outletSeparator = rendererBlock ? "," : "";
|
|
227
|
+
return (
|
|
228
|
+
` ${layoutClass}: {\n` +
|
|
229
|
+
` outlet: "${target}"${outletSeparator}\n` +
|
|
230
|
+
rendererBlock +
|
|
231
|
+
" }"
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderOutletTopologyBlock({
|
|
236
|
+
marker = "",
|
|
237
|
+
placementId = "",
|
|
238
|
+
owner = "",
|
|
239
|
+
surfaces = ["*"],
|
|
240
|
+
description = "",
|
|
241
|
+
target = "",
|
|
242
|
+
variantTargets = null,
|
|
243
|
+
kind = "link",
|
|
244
|
+
rendererToken = ""
|
|
245
|
+
} = {}) {
|
|
246
|
+
const descriptionLine = normalizeText(description)
|
|
247
|
+
? ` description: ${JSON.stringify(normalizeText(description))},\n`
|
|
248
|
+
: "";
|
|
249
|
+
const resolvedVariantTargets = variantTargets || Object.freeze({
|
|
250
|
+
compact: target,
|
|
251
|
+
medium: target,
|
|
252
|
+
expanded: target
|
|
253
|
+
});
|
|
254
|
+
const variantBlocks = TOPOLOGY_LAYOUT_CLASSES
|
|
255
|
+
.map((layoutClass) => renderTopologyVariantBlock({
|
|
256
|
+
layoutClass,
|
|
257
|
+
target: resolvedVariantTargets[layoutClass],
|
|
258
|
+
kind,
|
|
259
|
+
rendererToken
|
|
260
|
+
}))
|
|
261
|
+
.join(",\n");
|
|
262
|
+
return (
|
|
263
|
+
`// ${marker}\n` +
|
|
264
|
+
"addPlacementTopology({\n" +
|
|
265
|
+
` id: "${placementId}",\n` +
|
|
266
|
+
renderTopologyOwnerLine(owner) +
|
|
267
|
+
descriptionLine +
|
|
268
|
+
` surfaces: ${JSON.stringify(surfaces)},\n` +
|
|
269
|
+
" variants: {\n" +
|
|
270
|
+
`${variantBlocks}\n` +
|
|
271
|
+
" }\n" +
|
|
272
|
+
"});\n"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
78
276
|
function findLastTemplateCloseTag(source = "") {
|
|
79
277
|
const sourceText = String(source || "");
|
|
80
278
|
let lastMatch = null;
|
|
@@ -119,19 +317,43 @@ async function runGeneratorSubcommand({
|
|
|
119
317
|
dryRun = false
|
|
120
318
|
} = {}) {
|
|
121
319
|
const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
|
|
320
|
+
if (normalizedSubcommand === "topology") {
|
|
321
|
+
return runTopologySubcommand({
|
|
322
|
+
appRoot,
|
|
323
|
+
options,
|
|
324
|
+
dryRun
|
|
325
|
+
});
|
|
326
|
+
}
|
|
122
327
|
if (normalizedSubcommand !== "outlet") {
|
|
123
328
|
throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
124
329
|
}
|
|
125
330
|
const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator outlet" });
|
|
126
|
-
rejectUnexpectedOptions(
|
|
127
|
-
|
|
128
|
-
|
|
331
|
+
rejectUnexpectedOptions(
|
|
332
|
+
options,
|
|
333
|
+
[
|
|
334
|
+
"target",
|
|
335
|
+
"placement",
|
|
336
|
+
"owner",
|
|
337
|
+
"surface",
|
|
338
|
+
"description",
|
|
339
|
+
"link-renderer",
|
|
340
|
+
"kind",
|
|
341
|
+
"compact-target",
|
|
342
|
+
"medium-target",
|
|
343
|
+
"expanded-target"
|
|
344
|
+
],
|
|
345
|
+
{ context: "ui-generator outlet" }
|
|
346
|
+
);
|
|
129
347
|
|
|
130
348
|
const outletTarget = resolveOutletTargetId(options?.target, {
|
|
131
349
|
context: "ui-generator outlet",
|
|
132
350
|
optionName: "target"
|
|
133
351
|
});
|
|
134
352
|
const targetId = outletTarget.id;
|
|
353
|
+
const placementId = resolveOptionalSemanticPlacementOption(options);
|
|
354
|
+
if (!placementId && hasTopologyOptions(options)) {
|
|
355
|
+
throw new Error("ui-generator outlet requires --placement when topology options are provided.");
|
|
356
|
+
}
|
|
135
357
|
|
|
136
358
|
const targetFilePath = resolvePathWithinApp(appRoot, targetFile, {
|
|
137
359
|
context: "ui-generator outlet"
|
|
@@ -157,16 +379,173 @@ async function runGeneratorSubcommand({
|
|
|
157
379
|
});
|
|
158
380
|
const scriptApplied = applyScriptImports(templateApplied.content);
|
|
159
381
|
|
|
382
|
+
const topologyPath = resolvePathWithinApp(appRoot, PLACEMENT_TOPOLOGY_FILE, {
|
|
383
|
+
context: "ui-generator outlet"
|
|
384
|
+
});
|
|
385
|
+
const topologyApplied = placementId
|
|
386
|
+
? await prepareTopologyBlock({
|
|
387
|
+
appRoot,
|
|
388
|
+
topologyPath,
|
|
389
|
+
context: "ui-generator outlet",
|
|
390
|
+
placementId,
|
|
391
|
+
owner: options?.owner,
|
|
392
|
+
surfaces: resolveTopologySurfaces(options),
|
|
393
|
+
description: options?.description,
|
|
394
|
+
target: targetId,
|
|
395
|
+
variantTargets: resolveTopologyVariantTargets(options, {
|
|
396
|
+
context: "ui-generator outlet",
|
|
397
|
+
fallbackTarget: targetId
|
|
398
|
+
}),
|
|
399
|
+
kind: resolveTopologyKind(options, {
|
|
400
|
+
context: "ui-generator outlet",
|
|
401
|
+
defaultKind: "link"
|
|
402
|
+
}),
|
|
403
|
+
rendererToken: options?.["link-renderer"]
|
|
404
|
+
})
|
|
405
|
+
: { changed: false, relativePath: topologyPath.relativePath };
|
|
406
|
+
|
|
160
407
|
const changed = templateApplied.changed || scriptApplied.changed;
|
|
161
408
|
if (changed && dryRun !== true) {
|
|
162
409
|
await writeFile(targetFilePath.absolutePath, scriptApplied.content, "utf8");
|
|
163
410
|
}
|
|
411
|
+
if (topologyApplied.changed && dryRun !== true) {
|
|
412
|
+
await writeFile(topologyApplied.absolutePath, topologyApplied.content, "utf8");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const touchedFiles = new Set();
|
|
416
|
+
if (changed) {
|
|
417
|
+
touchedFiles.add(targetFilePath.relativePath);
|
|
418
|
+
}
|
|
419
|
+
if (topologyApplied.changed) {
|
|
420
|
+
touchedFiles.add(topologyApplied.relativePath);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const actionSummary = [
|
|
424
|
+
changed ? `Injected outlet "${targetId}"` : "",
|
|
425
|
+
topologyApplied.changed && placementId
|
|
426
|
+
? `mapped semantic placement "${placementId}"`
|
|
427
|
+
: ""
|
|
428
|
+
].filter(Boolean).join(" and ");
|
|
429
|
+
return {
|
|
430
|
+
touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
|
|
431
|
+
summary: touchedFiles.size > 0
|
|
432
|
+
? `${actionSummary || `Processed outlet "${targetId}"`}.`
|
|
433
|
+
: placementId
|
|
434
|
+
? `Outlet "${targetId}" and semantic placement "${placementId}" are already present.`
|
|
435
|
+
: `Outlet "${targetId}" is already present.`
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function prepareTopologyBlock({
|
|
440
|
+
appRoot,
|
|
441
|
+
topologyPath = null,
|
|
442
|
+
context = "ui-generator topology",
|
|
443
|
+
placementId = "",
|
|
444
|
+
owner = "",
|
|
445
|
+
surfaces = ["*"],
|
|
446
|
+
description = "",
|
|
447
|
+
target = "",
|
|
448
|
+
variantTargets = null,
|
|
449
|
+
kind = "",
|
|
450
|
+
rendererToken = ""
|
|
451
|
+
} = {}) {
|
|
452
|
+
const resolvedTopologyPath = topologyPath || resolvePathWithinApp(appRoot, PLACEMENT_TOPOLOGY_FILE, {
|
|
453
|
+
context
|
|
454
|
+
});
|
|
455
|
+
const resolvedOwner = resolveSemanticPlacementOwner({
|
|
456
|
+
placementId,
|
|
457
|
+
targetId: target,
|
|
458
|
+
variantTargets,
|
|
459
|
+
owner,
|
|
460
|
+
context
|
|
461
|
+
});
|
|
462
|
+
const topologyMarker = `jskit:ui-generator.topology:${placementId}:${resolvedOwner || "global"}`;
|
|
463
|
+
const topologySource = await readFile(resolvedTopologyPath.absolutePath, "utf8");
|
|
464
|
+
const topologyApplied = await appendTopologyBlockIfPlacementMissing({
|
|
465
|
+
topologyPath: resolvedTopologyPath,
|
|
466
|
+
source: topologySource,
|
|
467
|
+
marker: topologyMarker,
|
|
468
|
+
block: renderOutletTopologyBlock({
|
|
469
|
+
marker: topologyMarker,
|
|
470
|
+
placementId,
|
|
471
|
+
owner: resolvedOwner,
|
|
472
|
+
surfaces,
|
|
473
|
+
description,
|
|
474
|
+
target,
|
|
475
|
+
variantTargets,
|
|
476
|
+
kind,
|
|
477
|
+
rendererToken
|
|
478
|
+
}),
|
|
479
|
+
placementId,
|
|
480
|
+
owner: resolvedOwner,
|
|
481
|
+
variantTargets,
|
|
482
|
+
context
|
|
483
|
+
});
|
|
484
|
+
return {
|
|
485
|
+
changed: topologyApplied.changed,
|
|
486
|
+
content: topologyApplied.content,
|
|
487
|
+
absolutePath: resolvedTopologyPath.absolutePath,
|
|
488
|
+
relativePath: resolvedTopologyPath.relativePath,
|
|
489
|
+
owner: resolvedOwner
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function applyTopologyBlock(options = {}) {
|
|
494
|
+
const topologyApplied = await prepareTopologyBlock(options);
|
|
495
|
+
if (topologyApplied.changed && options?.dryRun !== true) {
|
|
496
|
+
await writeFile(topologyApplied.absolutePath, topologyApplied.content, "utf8");
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
changed: topologyApplied.changed,
|
|
500
|
+
relativePath: topologyApplied.relativePath,
|
|
501
|
+
owner: topologyApplied.owner
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function runTopologySubcommand({
|
|
506
|
+
appRoot,
|
|
507
|
+
options = {},
|
|
508
|
+
dryRun = false
|
|
509
|
+
} = {}) {
|
|
510
|
+
const context = "ui-generator topology";
|
|
511
|
+
rejectUnexpectedOptions(
|
|
512
|
+
options,
|
|
513
|
+
[
|
|
514
|
+
"target",
|
|
515
|
+
"compact-target",
|
|
516
|
+
"medium-target",
|
|
517
|
+
"expanded-target",
|
|
518
|
+
"placement",
|
|
519
|
+
"owner",
|
|
520
|
+
"surface",
|
|
521
|
+
"description",
|
|
522
|
+
"kind",
|
|
523
|
+
"link-renderer"
|
|
524
|
+
],
|
|
525
|
+
{ context }
|
|
526
|
+
);
|
|
527
|
+
const placementId = resolveSemanticPlacementOption(options, { context });
|
|
528
|
+
const variantTargets = resolveTopologyVariantTargets(options, { context });
|
|
529
|
+
const kind = resolveTopologyKind(options, { context, required: true });
|
|
530
|
+
const topologyApplied = await applyTopologyBlock({
|
|
531
|
+
appRoot,
|
|
532
|
+
context,
|
|
533
|
+
placementId,
|
|
534
|
+
owner: options?.owner,
|
|
535
|
+
surfaces: resolveTopologySurfaces(options),
|
|
536
|
+
description: options?.description,
|
|
537
|
+
target: variantTargets.compact,
|
|
538
|
+
variantTargets,
|
|
539
|
+
kind,
|
|
540
|
+
rendererToken: options?.["link-renderer"],
|
|
541
|
+
dryRun
|
|
542
|
+
});
|
|
164
543
|
|
|
165
544
|
return {
|
|
166
|
-
touchedFiles: changed ? [
|
|
167
|
-
summary: changed
|
|
168
|
-
? `
|
|
169
|
-
: `
|
|
545
|
+
touchedFiles: topologyApplied.changed ? [topologyApplied.relativePath] : [],
|
|
546
|
+
summary: topologyApplied.changed
|
|
547
|
+
? `Mapped semantic placement "${placementId}"${topologyApplied.owner ? ` for owner "${topologyApplied.owner}"` : ""}.`
|
|
548
|
+
: `Semantic placement "${placementId}"${topologyApplied.owner ? ` for owner "${topologyApplied.owner}"` : ""} is already present.`
|
|
170
549
|
};
|
|
171
550
|
}
|
|
172
551
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { normalizeBoolean, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
buildUiPageTemplateContext,
|
|
6
|
+
resolveNavigationInferenceRoutePath,
|
|
7
|
+
shouldCreateNavigationLink
|
|
8
|
+
} from "../buildTemplateContext.js";
|
|
5
9
|
import {
|
|
6
10
|
PLACEMENT_FILE,
|
|
7
11
|
requireSinglePositionalTargetFile,
|
|
@@ -20,15 +24,20 @@ function renderPageLinkPlacementBlock({
|
|
|
20
24
|
label = "",
|
|
21
25
|
surface = ""
|
|
22
26
|
} = {}) {
|
|
27
|
+
const componentTokenLine = context.__JSKIT_UI_LINK_COMPONENT_TOKEN__
|
|
28
|
+
? ` componentToken: "${context.__JSKIT_UI_LINK_COMPONENT_TOKEN__}",\n`
|
|
29
|
+
: "";
|
|
23
30
|
return (
|
|
24
31
|
`// ${marker}\n` +
|
|
25
32
|
"{\n" +
|
|
26
33
|
" addPlacement({\n" +
|
|
27
34
|
` id: "${context.__JSKIT_UI_LINK_PLACEMENT_ID__}",\n` +
|
|
28
35
|
` target: "${context.__JSKIT_UI_LINK_PLACEMENT_TARGET__}",\n` +
|
|
36
|
+
`${context.__JSKIT_UI_LINK_OWNER_LINE__}` +
|
|
37
|
+
` kind: "link",\n` +
|
|
29
38
|
` surfaces: ["${surface}"],\n` +
|
|
30
39
|
" order: 155,\n" +
|
|
31
|
-
|
|
40
|
+
componentTokenLine +
|
|
32
41
|
" props: {\n" +
|
|
33
42
|
` label: "${label}",\n` +
|
|
34
43
|
` icon: "${context.__JSKIT_UI_LINK_ICON__}",\n` +
|
|
@@ -56,7 +65,7 @@ async function runGeneratorSubcommand({
|
|
|
56
65
|
const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator page" });
|
|
57
66
|
rejectUnexpectedOptions(
|
|
58
67
|
options,
|
|
59
|
-
["name", "
|
|
68
|
+
["name", "navigation-role", "link-placement", "link-to", "force"],
|
|
60
69
|
{ context: "ui-generator page" }
|
|
61
70
|
);
|
|
62
71
|
|
|
@@ -71,6 +80,7 @@ async function runGeneratorSubcommand({
|
|
|
71
80
|
: false;
|
|
72
81
|
const pageFilePath = pageTarget.targetFilePath.absolutePath;
|
|
73
82
|
const pageRelativePath = pageTarget.targetFilePath.relativePath;
|
|
83
|
+
const navigationInferenceRoutePath = resolveNavigationInferenceRoutePath(pageTarget);
|
|
74
84
|
|
|
75
85
|
const touchedFiles = new Set();
|
|
76
86
|
let pageAlreadyExisted = true;
|
|
@@ -86,31 +96,47 @@ async function runGeneratorSubcommand({
|
|
|
86
96
|
);
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
targetFile,
|
|
92
|
-
options
|
|
93
|
-
});
|
|
94
|
-
const placementPath = resolvePathWithinApp(pageTarget.appRoot, PLACEMENT_FILE, {
|
|
95
|
-
context: "ui-generator page"
|
|
99
|
+
const shouldCreateLink = shouldCreateNavigationLink(options, {
|
|
100
|
+
routePath: navigationInferenceRoutePath
|
|
96
101
|
});
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
renderPageLinkPlacementBlock({
|
|
103
|
-
marker: placementMarker,
|
|
104
|
-
context: placementContext,
|
|
105
|
-
label: pageLabel,
|
|
106
|
-
surface: pageTarget.surfaceId
|
|
102
|
+
const placementContext = shouldCreateLink
|
|
103
|
+
? await buildUiPageTemplateContext({
|
|
104
|
+
appRoot: pageTarget.appRoot,
|
|
105
|
+
targetFile,
|
|
106
|
+
options
|
|
107
107
|
})
|
|
108
|
-
|
|
108
|
+
: null;
|
|
109
|
+
const placementPath = shouldCreateLink
|
|
110
|
+
? resolvePathWithinApp(pageTarget.appRoot, PLACEMENT_FILE, {
|
|
111
|
+
context: "ui-generator page"
|
|
112
|
+
})
|
|
113
|
+
: null;
|
|
114
|
+
const placementSource = placementPath ? await readFile(placementPath.absolutePath, "utf8") : "";
|
|
115
|
+
const placementMarker = `jskit:ui-generator.page.link:${pageTarget.surfaceId}:${pageTarget.routeUrlSuffix}`;
|
|
116
|
+
const placementApplied = placementContext
|
|
117
|
+
? appendBlockIfMarkerMissing(
|
|
118
|
+
placementSource,
|
|
119
|
+
placementMarker,
|
|
120
|
+
renderPageLinkPlacementBlock({
|
|
121
|
+
marker: placementMarker,
|
|
122
|
+
context: placementContext,
|
|
123
|
+
label: pageLabel,
|
|
124
|
+
surface: pageTarget.surfaceId
|
|
125
|
+
})
|
|
126
|
+
)
|
|
127
|
+
: { changed: false, content: placementSource };
|
|
109
128
|
|
|
110
129
|
if (!pageAlreadyExisted || forceOverwrite) {
|
|
111
130
|
if (dryRun !== true) {
|
|
112
131
|
await mkdir(path.dirname(pageFilePath), { recursive: true });
|
|
113
|
-
await writeFile(
|
|
132
|
+
await writeFile(
|
|
133
|
+
pageFilePath,
|
|
134
|
+
renderPlainPageSource(pageLabel, {
|
|
135
|
+
surfaceId: pageTarget.surfaceId,
|
|
136
|
+
routePath: navigationInferenceRoutePath
|
|
137
|
+
}),
|
|
138
|
+
"utf8"
|
|
139
|
+
);
|
|
114
140
|
}
|
|
115
141
|
touchedFiles.add(pageRelativePath);
|
|
116
142
|
}
|
|
@@ -124,7 +150,7 @@ async function runGeneratorSubcommand({
|
|
|
124
150
|
|
|
125
151
|
const touchedFileList = [...touchedFiles].sort((left, right) => left.localeCompare(right));
|
|
126
152
|
return {
|
|
127
|
-
placementComponentTokens: [String(placementContext
|
|
153
|
+
placementComponentTokens: [String(placementContext?.__JSKIT_UI_LINK_COMPONENT_TOKEN__ || "").trim()].filter(Boolean),
|
|
128
154
|
touchedFiles: touchedFileList,
|
|
129
155
|
summary: !pageAlreadyExisted
|
|
130
156
|
? `Generated UI page "${pageTarget.routeUrlSuffix}" at ${pageRelativePath}.`
|