@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.
@@ -7,21 +7,23 @@ import {
7
7
  } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
8
8
  import {
9
9
  PLACEMENT_TOPOLOGY_FILE,
10
- appendBlockIfMarkerMissing,
10
+ appendTopologyBlockIfPlacementMissing,
11
11
  requireSinglePositionalTargetFile,
12
12
  rejectUnexpectedOptions,
13
13
  resolveOutletTargetId,
14
14
  resolvePathWithinApp,
15
15
  ensureTrailingNewline,
16
16
  insertImportIfMissing,
17
- findScriptBlock,
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 = findScriptBlock(sourceText);
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
- const scriptSetupBlock = `<script setup>\n${shellOutletImport}\n</script>\n`;
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 = normalizeSemanticPlacementId(options?.placement);
87
+ function resolveSemanticPlacementOption(options = {}, { context = "ui-generator outlet" } = {}) {
88
+ const placementId = resolveOptionalSemanticPlacementOption(options, { context });
96
89
  if (!placementId) {
97
- throw new Error('ui-generator outlet requires --placement in semantic "area.slot" format.');
90
+ throw new Error(`${context} requires --placement in semantic "area.slot" format.`);
98
91
  }
99
92
  return placementId;
100
93
  }
101
94
 
102
- function resolveSemanticPlacementOwner({ placementId = "", targetId = "", owner = "" } = {}) {
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 rendererBlock = renderLinkRendererBlock(rendererToken);
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
- " 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" +
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(options, ["target", "placement", "owner", "surface", "description", "link-renderer"], {
224
- context: "ui-generator outlet"
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 = resolveSemanticPlacementOption(options);
233
- const owner = resolveSemanticPlacementOwner({
234
- placementId,
235
- targetId,
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 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,
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(topologyPath.absolutePath, topologyApplied.content, "utf8");
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(topologyPath.relativePath);
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
- ? `Injected outlet "${targetId}" and mapped semantic placement "${placementId}"${owner ? ` for owner "${owner}"` : ""}.`
303
- : `Outlet "${targetId}" and semantic placement "${placementId}" are already present.`
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 { 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,
@@ -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 placementContext = await buildUiPageTemplateContext({
95
- appRoot: pageTarget.appRoot,
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 placementSource = await readFile(placementPath.absolutePath, "utf8");
103
- const placementMarker = `jskit:ui-generator.page.link:${pageTarget.surfaceId}:${pageTarget.routeUrlSuffix}`;
104
- const placementApplied = appendBlockIfMarkerMissing(
105
- placementSource,
106
- placementMarker,
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(pageFilePath, renderPlainPageSource(pageLabel), "utf8");
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.__JSKIT_UI_LINK_COMPONENT_TOKEN__ || "").trim()].filter(Boolean),
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}.`