@jskit-ai/ui-generator 0.1.49 → 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 +87 -11
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +36 -2
- package/src/server/subcommands/addSubpages.js +32 -20
- package/src/server/subcommands/element.js +17 -3
- package/src/server/subcommands/outlet.js +305 -60
- package/src/server/subcommands/page.js +43 -22
- package/src/server/subcommands/pageSupport.js +114 -36
- package/src/server/subcommands/support.js +126 -23
- package/test/addSubpagesSubcommand.test.js +132 -8
- package/test/buildTemplateContext.test.js +31 -0
- package/test/elementSubcommand.test.js +13 -1
- package/test/outletSubcommand.test.js +258 -0
- package/test/packageDescriptor.test.js +11 -0
- package/test/pageSubcommand.test.js +134 -5
|
@@ -7,21 +7,23 @@ import {
|
|
|
7
7
|
} from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
|
|
8
8
|
import {
|
|
9
9
|
PLACEMENT_TOPOLOGY_FILE,
|
|
10
|
-
|
|
10
|
+
appendTopologyBlockIfPlacementMissing,
|
|
11
11
|
requireSinglePositionalTargetFile,
|
|
12
12
|
rejectUnexpectedOptions,
|
|
13
13
|
resolveOutletTargetId,
|
|
14
14
|
resolvePathWithinApp,
|
|
15
15
|
ensureTrailingNewline,
|
|
16
16
|
insertImportIfMissing,
|
|
17
|
-
|
|
17
|
+
findScriptSetupBlock,
|
|
18
|
+
insertScriptSetupBlock,
|
|
18
19
|
parseTagAttributes,
|
|
19
20
|
indentBlock
|
|
20
21
|
} from "./support.js";
|
|
21
22
|
|
|
22
|
-
const ROUTE_TAG_PATTERN = /<route\b[^>]*>[\s\S]*?<\/route>\s*/gi;
|
|
23
23
|
const TEMPLATE_CLOSE_TAG_PATTERN = /<\/template>/gi;
|
|
24
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"]);
|
|
25
27
|
|
|
26
28
|
function hasShellOutletTarget(source = "", { target = "" } = {}) {
|
|
27
29
|
const normalizedTarget = normalizeText(target);
|
|
@@ -42,21 +44,12 @@ function hasShellOutletTarget(source = "", { target = "" } = {}) {
|
|
|
42
44
|
|
|
43
45
|
function applyScriptImports(source = "") {
|
|
44
46
|
const sourceText = String(source || "");
|
|
45
|
-
const scriptBlock =
|
|
47
|
+
const scriptBlock = findScriptSetupBlock(sourceText);
|
|
46
48
|
|
|
47
49
|
const shellOutletImport = "import ShellOutlet from \"@jskit-ai/shell-web/client/components/ShellOutlet\";";
|
|
48
50
|
|
|
49
51
|
if (!scriptBlock) {
|
|
50
|
-
|
|
51
|
-
let insertionIndex = 0;
|
|
52
|
-
for (const match of sourceText.matchAll(ROUTE_TAG_PATTERN)) {
|
|
53
|
-
insertionIndex = match.index + String(match[0] || "").length;
|
|
54
|
-
}
|
|
55
|
-
const separator = insertionIndex > 0 ? "\n" : "";
|
|
56
|
-
return {
|
|
57
|
-
changed: true,
|
|
58
|
-
content: `${sourceText.slice(0, insertionIndex)}${separator}${scriptSetupBlock}\n${sourceText.slice(insertionIndex)}`
|
|
59
|
-
};
|
|
52
|
+
return insertScriptSetupBlock(sourceText, shellOutletImport);
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
let nextScriptContent = scriptBlock.content;
|
|
@@ -91,25 +84,119 @@ function resolveOutletOwner(target = "") {
|
|
|
91
84
|
return normalizedTarget.slice(0, separatorIndex);
|
|
92
85
|
}
|
|
93
86
|
|
|
94
|
-
function resolveSemanticPlacementOption(options = {}) {
|
|
95
|
-
const placementId =
|
|
87
|
+
function resolveSemanticPlacementOption(options = {}, { context = "ui-generator outlet" } = {}) {
|
|
88
|
+
const placementId = resolveOptionalSemanticPlacementOption(options, { context });
|
|
96
89
|
if (!placementId) {
|
|
97
|
-
throw new Error(
|
|
90
|
+
throw new Error(`${context} requires --placement in semantic "area.slot" format.`);
|
|
98
91
|
}
|
|
99
92
|
return placementId;
|
|
100
93
|
}
|
|
101
94
|
|
|
102
|
-
function
|
|
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
|
+
} = {}) {
|
|
103
124
|
const explicitOwner = normalizePlacementOwnerId(owner);
|
|
104
125
|
if (explicitOwner) {
|
|
105
126
|
return explicitOwner;
|
|
106
127
|
}
|
|
107
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
|
+
}
|
|
108
138
|
return resolveOutletOwner(targetId);
|
|
109
139
|
}
|
|
110
140
|
return "";
|
|
111
141
|
}
|
|
112
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
|
+
|
|
113
200
|
function resolveTopologySurfaces(options = {}) {
|
|
114
201
|
const surface = normalizePlacementSurfaceId(options?.surface);
|
|
115
202
|
if (surface) {
|
|
@@ -134,6 +221,17 @@ function renderLinkRendererBlock(rendererToken = "") {
|
|
|
134
221
|
);
|
|
135
222
|
}
|
|
136
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
|
+
|
|
137
235
|
function renderOutletTopologyBlock({
|
|
138
236
|
marker = "",
|
|
139
237
|
placementId = "",
|
|
@@ -141,12 +239,26 @@ function renderOutletTopologyBlock({
|
|
|
141
239
|
surfaces = ["*"],
|
|
142
240
|
description = "",
|
|
143
241
|
target = "",
|
|
242
|
+
variantTargets = null,
|
|
243
|
+
kind = "link",
|
|
144
244
|
rendererToken = ""
|
|
145
245
|
} = {}) {
|
|
146
246
|
const descriptionLine = normalizeText(description)
|
|
147
247
|
? ` description: ${JSON.stringify(normalizeText(description))},\n`
|
|
148
248
|
: "";
|
|
149
|
-
const
|
|
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");
|
|
150
262
|
return (
|
|
151
263
|
`// ${marker}\n` +
|
|
152
264
|
"addPlacementTopology({\n" +
|
|
@@ -155,18 +267,7 @@ function renderOutletTopologyBlock({
|
|
|
155
267
|
descriptionLine +
|
|
156
268
|
` surfaces: ${JSON.stringify(surfaces)},\n` +
|
|
157
269
|
" variants: {\n" +
|
|
158
|
-
|
|
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" +
|
|
270
|
+
`${variantBlocks}\n` +
|
|
170
271
|
" }\n" +
|
|
171
272
|
"});\n"
|
|
172
273
|
);
|
|
@@ -216,26 +317,43 @@ async function runGeneratorSubcommand({
|
|
|
216
317
|
dryRun = false
|
|
217
318
|
} = {}) {
|
|
218
319
|
const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
|
|
320
|
+
if (normalizedSubcommand === "topology") {
|
|
321
|
+
return runTopologySubcommand({
|
|
322
|
+
appRoot,
|
|
323
|
+
options,
|
|
324
|
+
dryRun
|
|
325
|
+
});
|
|
326
|
+
}
|
|
219
327
|
if (normalizedSubcommand !== "outlet") {
|
|
220
328
|
throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
221
329
|
}
|
|
222
330
|
const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator outlet" });
|
|
223
|
-
rejectUnexpectedOptions(
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
);
|
|
226
347
|
|
|
227
348
|
const outletTarget = resolveOutletTargetId(options?.target, {
|
|
228
349
|
context: "ui-generator outlet",
|
|
229
350
|
optionName: "target"
|
|
230
351
|
});
|
|
231
352
|
const targetId = outletTarget.id;
|
|
232
|
-
const placementId =
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
owner: options?.owner
|
|
237
|
-
});
|
|
238
|
-
const surfaces = resolveTopologySurfaces(options);
|
|
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
|
+
}
|
|
239
357
|
|
|
240
358
|
const targetFilePath = resolvePathWithinApp(appRoot, targetFile, {
|
|
241
359
|
context: "ui-generator outlet"
|
|
@@ -261,31 +379,37 @@ async function runGeneratorSubcommand({
|
|
|
261
379
|
});
|
|
262
380
|
const scriptApplied = applyScriptImports(templateApplied.content);
|
|
263
381
|
|
|
264
|
-
const changed = templateApplied.changed || scriptApplied.changed;
|
|
265
|
-
if (changed && dryRun !== true) {
|
|
266
|
-
await writeFile(targetFilePath.absolutePath, scriptApplied.content, "utf8");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
382
|
const topologyPath = resolvePathWithinApp(appRoot, PLACEMENT_TOPOLOGY_FILE, {
|
|
270
383
|
context: "ui-generator outlet"
|
|
271
384
|
});
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
renderOutletTopologyBlock({
|
|
278
|
-
marker: topologyMarker,
|
|
385
|
+
const topologyApplied = placementId
|
|
386
|
+
? await prepareTopologyBlock({
|
|
387
|
+
appRoot,
|
|
388
|
+
topologyPath,
|
|
389
|
+
context: "ui-generator outlet",
|
|
279
390
|
placementId,
|
|
280
|
-
owner,
|
|
281
|
-
surfaces,
|
|
391
|
+
owner: options?.owner,
|
|
392
|
+
surfaces: resolveTopologySurfaces(options),
|
|
282
393
|
description: options?.description,
|
|
283
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
|
+
}),
|
|
284
403
|
rendererToken: options?.["link-renderer"]
|
|
285
404
|
})
|
|
286
|
-
|
|
405
|
+
: { changed: false, relativePath: topologyPath.relativePath };
|
|
406
|
+
|
|
407
|
+
const changed = templateApplied.changed || scriptApplied.changed;
|
|
408
|
+
if (changed && dryRun !== true) {
|
|
409
|
+
await writeFile(targetFilePath.absolutePath, scriptApplied.content, "utf8");
|
|
410
|
+
}
|
|
287
411
|
if (topologyApplied.changed && dryRun !== true) {
|
|
288
|
-
await writeFile(
|
|
412
|
+
await writeFile(topologyApplied.absolutePath, topologyApplied.content, "utf8");
|
|
289
413
|
}
|
|
290
414
|
|
|
291
415
|
const touchedFiles = new Set();
|
|
@@ -293,14 +417,135 @@ async function runGeneratorSubcommand({
|
|
|
293
417
|
touchedFiles.add(targetFilePath.relativePath);
|
|
294
418
|
}
|
|
295
419
|
if (topologyApplied.changed) {
|
|
296
|
-
touchedFiles.add(
|
|
420
|
+
touchedFiles.add(topologyApplied.relativePath);
|
|
297
421
|
}
|
|
298
422
|
|
|
423
|
+
const actionSummary = [
|
|
424
|
+
changed ? `Injected outlet "${targetId}"` : "",
|
|
425
|
+
topologyApplied.changed && placementId
|
|
426
|
+
? `mapped semantic placement "${placementId}"`
|
|
427
|
+
: ""
|
|
428
|
+
].filter(Boolean).join(" and ");
|
|
299
429
|
return {
|
|
300
430
|
touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
|
|
301
431
|
summary: touchedFiles.size > 0
|
|
302
|
-
?
|
|
303
|
-
:
|
|
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
|
+
});
|
|
543
|
+
|
|
544
|
+
return {
|
|
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.`
|
|
304
549
|
};
|
|
305
550
|
}
|
|
306
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,
|
|
@@ -61,7 +65,7 @@ async function runGeneratorSubcommand({
|
|
|
61
65
|
const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator page" });
|
|
62
66
|
rejectUnexpectedOptions(
|
|
63
67
|
options,
|
|
64
|
-
["name", "link-placement", "link-to", "force"],
|
|
68
|
+
["name", "navigation-role", "link-placement", "link-to", "force"],
|
|
65
69
|
{ context: "ui-generator page" }
|
|
66
70
|
);
|
|
67
71
|
|
|
@@ -76,6 +80,7 @@ async function runGeneratorSubcommand({
|
|
|
76
80
|
: false;
|
|
77
81
|
const pageFilePath = pageTarget.targetFilePath.absolutePath;
|
|
78
82
|
const pageRelativePath = pageTarget.targetFilePath.relativePath;
|
|
83
|
+
const navigationInferenceRoutePath = resolveNavigationInferenceRoutePath(pageTarget);
|
|
79
84
|
|
|
80
85
|
const touchedFiles = new Set();
|
|
81
86
|
let pageAlreadyExisted = true;
|
|
@@ -91,31 +96,47 @@ async function runGeneratorSubcommand({
|
|
|
91
96
|
);
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
targetFile,
|
|
97
|
-
options
|
|
98
|
-
});
|
|
99
|
-
const placementPath = resolvePathWithinApp(pageTarget.appRoot, PLACEMENT_FILE, {
|
|
100
|
-
context: "ui-generator page"
|
|
99
|
+
const shouldCreateLink = shouldCreateNavigationLink(options, {
|
|
100
|
+
routePath: navigationInferenceRoutePath
|
|
101
101
|
});
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
renderPageLinkPlacementBlock({
|
|
108
|
-
marker: placementMarker,
|
|
109
|
-
context: placementContext,
|
|
110
|
-
label: pageLabel,
|
|
111
|
-
surface: pageTarget.surfaceId
|
|
102
|
+
const placementContext = shouldCreateLink
|
|
103
|
+
? await buildUiPageTemplateContext({
|
|
104
|
+
appRoot: pageTarget.appRoot,
|
|
105
|
+
targetFile,
|
|
106
|
+
options
|
|
112
107
|
})
|
|
113
|
-
|
|
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 };
|
|
114
128
|
|
|
115
129
|
if (!pageAlreadyExisted || forceOverwrite) {
|
|
116
130
|
if (dryRun !== true) {
|
|
117
131
|
await mkdir(path.dirname(pageFilePath), { recursive: true });
|
|
118
|
-
await writeFile(
|
|
132
|
+
await writeFile(
|
|
133
|
+
pageFilePath,
|
|
134
|
+
renderPlainPageSource(pageLabel, {
|
|
135
|
+
surfaceId: pageTarget.surfaceId,
|
|
136
|
+
routePath: navigationInferenceRoutePath
|
|
137
|
+
}),
|
|
138
|
+
"utf8"
|
|
139
|
+
);
|
|
119
140
|
}
|
|
120
141
|
touchedFiles.add(pageRelativePath);
|
|
121
142
|
}
|
|
@@ -129,7 +150,7 @@ async function runGeneratorSubcommand({
|
|
|
129
150
|
|
|
130
151
|
const touchedFileList = [...touchedFiles].sort((left, right) => left.localeCompare(right));
|
|
131
152
|
return {
|
|
132
|
-
placementComponentTokens: [String(placementContext
|
|
153
|
+
placementComponentTokens: [String(placementContext?.__JSKIT_UI_LINK_COMPONENT_TOKEN__ || "").trim()].filter(Boolean),
|
|
133
154
|
touchedFiles: touchedFileList,
|
|
134
155
|
summary: !pageAlreadyExisted
|
|
135
156
|
? `Generated UI page "${pageTarget.routeUrlSuffix}" at ${pageRelativePath}.`
|