@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.
@@ -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
- findScriptBlock,
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 = findScriptBlock(sourceText);
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
- const scriptSetupBlock = `<script setup>\n${shellOutletImport}\n</script>\n`;
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(options, ["target"], {
127
- context: "ui-generator outlet"
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 ? [targetFilePath.relativePath] : [],
167
- summary: changed
168
- ? `Injected outlet "${targetId}" into ${targetFilePath.relativePath}.`
169
- : `Outlet "${targetId}" is already present in ${targetFilePath.relativePath}.`
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 { buildUiPageTemplateContext } from "../buildTemplateContext.js";
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
- ` componentToken: "${context.__JSKIT_UI_LINK_COMPONENT_TOKEN__}",\n` +
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", "link-placement", "link-component-token", "link-to", "force"],
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 placementContext = await buildUiPageTemplateContext({
90
- appRoot: pageTarget.appRoot,
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 placementSource = await readFile(placementPath.absolutePath, "utf8");
98
- const placementMarker = `jskit:ui-generator.page.link:${pageTarget.surfaceId}:${pageTarget.routeUrlSuffix}`;
99
- const placementApplied = appendBlockIfMarkerMissing(
100
- placementSource,
101
- placementMarker,
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(pageFilePath, renderPlainPageSource(pageLabel), "utf8");
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.__JSKIT_UI_LINK_COMPONENT_TOKEN__ || "").trim()].filter(Boolean),
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}.`