@jskit-ai/ui-generator 0.1.15 → 0.1.17

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.
@@ -3,6 +3,7 @@ import {
3
3
  resolveRequiredAppRoot,
4
4
  toPosixPath
5
5
  } from "@jskit-ai/kernel/server/support";
6
+ import { normalizeShellOutletTargetId } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
6
7
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
7
8
  import { toCamelCase, toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
8
9
 
@@ -44,32 +45,11 @@ function requireSinglePositionalTargetFile(args = [], { context = "ui-generator"
44
45
  return positionalArgs[0];
45
46
  }
46
47
 
47
- function normalizeExplicitOutletTargetId(value = "") {
48
- const normalizedValue = normalizeText(value);
49
- if (!normalizedValue) {
50
- return "";
51
- }
52
-
53
- const separatorIndex = normalizedValue.indexOf(":");
54
- if (separatorIndex <= 0 || separatorIndex >= normalizedValue.length - 1) {
55
- return "";
56
- }
57
-
58
- const host = normalizeText(normalizedValue.slice(0, separatorIndex));
59
- const position = normalizeText(normalizedValue.slice(separatorIndex + 1));
60
- if (!host || !position) {
61
- return "";
62
- }
63
-
64
- return `${host}:${position}`;
65
- }
66
-
67
48
  function resolveOutletTargetId(
68
49
  rawTarget = "",
69
50
  {
70
51
  context = "ui-generator",
71
- optionName = "target",
72
- defaultPosition = ""
52
+ optionName = "target"
73
53
  } = {}
74
54
  ) {
75
55
  const normalizedTarget = normalizeText(rawTarget);
@@ -77,18 +57,13 @@ function resolveOutletTargetId(
77
57
  throw new Error(`${context} requires --${optionName}.`);
78
58
  }
79
59
 
80
- const targetId = normalizedTarget.includes(":")
81
- ? normalizeExplicitOutletTargetId(normalizedTarget)
82
- : normalizeExplicitOutletTargetId(`${normalizedTarget}:${normalizeText(defaultPosition)}`);
60
+ const targetId = normalizeShellOutletTargetId(normalizedTarget);
83
61
  if (!targetId) {
84
- throw new Error(`${context} option "${optionName}" must be "host" or "host:position".`);
62
+ throw new Error(`${context} option "${optionName}" must be a target in "host:position" format.`);
85
63
  }
86
64
 
87
- const separatorIndex = targetId.indexOf(":");
88
65
  return Object.freeze({
89
- id: targetId,
90
- host: targetId.slice(0, separatorIndex),
91
- position: targetId.slice(separatorIndex + 1)
66
+ id: targetId
92
67
  });
93
68
  }
94
69
 
@@ -295,7 +270,6 @@ export {
295
270
  toPascalCase,
296
271
  requireOption,
297
272
  requireSinglePositionalTargetFile,
298
- normalizeExplicitOutletTargetId,
299
273
  resolveOutletTargetId,
300
274
  rejectUnexpectedOptions,
301
275
  resolvePathWithinApp,
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import test from "node:test";
6
+ import { readLocalLinkItemComponentSource } from "@jskit-ai/shell-web/server/support/localLinkItemScaffolds";
6
7
  import { runGeneratorSubcommand } from "../src/server/subcommands/addSubpages.js";
7
8
 
8
9
  async function withTempApp(run) {
@@ -105,14 +106,21 @@ test("ui-generator add-subpages derives the default target from an index-route p
105
106
 
106
107
  assert.deepEqual(result.touchedFiles, [
107
108
  "packages/main/src/client/providers/MainClientProvider.js",
109
+ "src/components/menus/TabLinkItem.vue",
108
110
  "src/components/SectionContainerShell.vue",
109
- "src/components/TabLinkItem.vue",
110
111
  `src/pages/${targetFile}`
111
112
  ]);
112
113
 
113
114
  const pageSource = await readPageFile(appRoot, targetFile);
114
- assert.match(pageSource, /<ShellOutlet host="practice" position="sub-pages" \/>/);
115
+ assert.match(
116
+ pageSource,
117
+ /<ShellOutlet target="practice:sub-pages" default-link-component-token="local\.main\.ui\.tab-link-item" \/>/
118
+ );
115
119
  assert.match(pageSource, /<RouterView \/>/);
120
+ assert.equal(
121
+ await readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
122
+ await readLocalLinkItemComponentSource("local.main.ui.tab-link-item")
123
+ );
116
124
  });
117
125
  });
118
126
 
@@ -131,7 +139,10 @@ test("ui-generator add-subpages derives the default target from a dynamic file-r
131
139
  });
132
140
 
133
141
  const pageSource = await readPageFile(appRoot, targetFile);
134
- assert.match(pageSource, /<ShellOutlet host="contacts-contact-id" position="sub-pages" \/>/);
142
+ assert.match(
143
+ pageSource,
144
+ /<ShellOutlet target="contacts-contact-id:sub-pages" default-link-component-token="local\.main\.ui\.tab-link-item" \/>/
145
+ );
135
146
  });
136
147
  });
137
148
 
@@ -150,28 +161,31 @@ test("ui-generator add-subpages derives the default target from a nested route p
150
161
  });
151
162
 
152
163
  const pageSource = await readPageFile(appRoot, targetFile);
153
- assert.match(pageSource, /<ShellOutlet host="catalog-products" position="sub-pages" \/>/);
164
+ assert.match(
165
+ pageSource,
166
+ /<ShellOutlet target="catalog-products:sub-pages" default-link-component-token="local\.main\.ui\.tab-link-item" \/>/
167
+ );
154
168
  });
155
169
  });
156
170
 
157
- test("ui-generator add-subpages supports explicit target host shorthand", async () => {
171
+ test("ui-generator add-subpages rejects explicit target shorthand without a position", async () => {
158
172
  await withTempApp(async (appRoot) => {
159
173
  await writeAppFixture(appRoot);
160
174
 
161
175
  const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
162
176
  await writePageFile(appRoot, targetFile);
163
177
 
164
- await runGeneratorSubcommand({
165
- appRoot,
166
- subcommand: "add-subpages",
167
- args: [targetFile],
168
- options: {
169
- target: "practice-hub"
170
- }
171
- });
172
-
173
- const pageSource = await readPageFile(appRoot, targetFile);
174
- assert.match(pageSource, /<ShellOutlet host="practice-hub" position="sub-pages" \/>/);
178
+ await assert.rejects(
179
+ runGeneratorSubcommand({
180
+ appRoot,
181
+ subcommand: "add-subpages",
182
+ args: [targetFile],
183
+ options: {
184
+ target: "practice-hub"
185
+ }
186
+ }),
187
+ /option "target" must be a target in "host:position" format/
188
+ );
175
189
  });
176
190
  });
177
191
 
@@ -192,7 +206,10 @@ test("ui-generator add-subpages supports explicit target host:position", async (
192
206
  });
193
207
 
194
208
  const pageSource = await readPageFile(appRoot, targetFile);
195
- assert.match(pageSource, /<ShellOutlet host="practice-hub" position="secondary-tabs" \/>/);
209
+ assert.match(
210
+ pageSource,
211
+ /<ShellOutlet target="practice-hub:secondary-tabs" default-link-component-token="local\.main\.ui\.tab-link-item" \/>/
212
+ );
196
213
  });
197
214
  });
198
215
 
@@ -209,8 +226,9 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
209
226
  customSectionShellSource,
210
227
  "utf8"
211
228
  );
229
+ await mkdir(path.join(appRoot, "src", "components", "menus"), { recursive: true });
212
230
  await writeFile(
213
- path.join(appRoot, "src", "components", "TabLinkItem.vue"),
231
+ path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"),
214
232
  customTabLinkSource,
215
233
  "utf8"
216
234
  );
@@ -233,7 +251,7 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
233
251
  customSectionShellSource
234
252
  );
235
253
  assert.equal(
236
- await readFile(path.join(appRoot, "src", "components", "TabLinkItem.vue"), "utf8"),
254
+ await readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
237
255
  customTabLinkSource
238
256
  );
239
257
  });
@@ -299,7 +317,7 @@ test("ui-generator add-subpages validates target format", async () => {
299
317
  target: "practice:"
300
318
  }
301
319
  }),
302
- /option "target" must be "host" or "host:position"/
320
+ /option "target" must be a target in "host:position" format/
303
321
  );
304
322
  });
305
323
  });
@@ -31,8 +31,12 @@ async function writeShellLayout(appRoot, source = "") {
31
31
  source ||
32
32
  `<template>
33
33
  <div>
34
- <ShellOutlet host="shell-layout" position="top-right" />
35
- <ShellOutlet host="shell-layout" position="primary-menu" default />
34
+ <ShellOutlet target="shell-layout:top-right" />
35
+ <ShellOutlet
36
+ target="shell-layout:primary-menu"
37
+ default
38
+ default-link-component-token="local.main.ui.surface-aware-menu-link-item"
39
+ />
36
40
  </div>
37
41
  </template>
38
42
  `
@@ -57,16 +61,44 @@ test("buildUiPageTemplateContext resolves link placement from default app ShellO
57
61
  targetFile: "admin/reports/index.vue",
58
62
  options: {}
59
63
  });
60
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "shell-layout");
61
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "primary-menu");
62
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "users.web.shell.surface-aware-menu-link-item");
64
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell-layout:primary-menu");
65
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
63
66
  assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
64
67
  assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/reports");
68
+ assert.equal(context.__JSKIT_UI_LINK_WHEN_LINE__, "");
65
69
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, "");
66
70
  assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.reports.link");
67
71
  });
68
72
  });
69
73
 
74
+ test("buildUiPageTemplateContext derives an auth guard from an authenticated surface policy", async () => {
75
+ await withTempApp(async (appRoot) => {
76
+ await writeConfig(
77
+ appRoot,
78
+ `export const config = {
79
+ surfaceAccessPolicies: {
80
+ authenticated: {
81
+ requireAuth: true
82
+ }
83
+ },
84
+ surfaceDefinitions: {
85
+ app: { id: "app", pagesRoot: "app", enabled: true, accessPolicyId: "authenticated" }
86
+ }
87
+ };
88
+ `
89
+ );
90
+ await writeShellLayout(appRoot);
91
+
92
+ const context = await buildUiPageTemplateContext({
93
+ appRoot,
94
+ targetFile: "app/reports/index.vue",
95
+ options: {}
96
+ });
97
+
98
+ assert.equal(context.__JSKIT_UI_LINK_WHEN_LINE__, " when: ({ auth }) => Boolean(auth?.authenticated)\n");
99
+ });
100
+ });
101
+
70
102
  test("buildUiPageTemplateContext supports explicit link placement override", async () => {
71
103
  await withTempApp(async (appRoot) => {
72
104
  await writeConfig(
@@ -87,8 +119,8 @@ test("buildUiPageTemplateContext supports explicit link placement override", asy
87
119
  "link-placement": "shell-layout:top-right"
88
120
  }
89
121
  });
90
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "top-right");
91
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "users.web.shell.surface-aware-menu-link-item");
122
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell-layout:top-right");
123
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
92
124
  });
93
125
  });
94
126
 
@@ -133,7 +165,11 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
133
165
  ui: {
134
166
  placements: {
135
167
  outlets: [
136
- { host: "workspace-tools", position: "primary-menu", source: "src/client/components/UsersWorkspaceToolsWidget.vue" }
168
+ {
169
+ target: "workspace-tools:primary-menu",
170
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
171
+ source: "src/client/components/UsersWorkspaceToolsWidget.vue"
172
+ }
137
173
  ]
138
174
  }
139
175
  }
@@ -149,8 +185,7 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
149
185
  "link-placement": "workspace-tools:primary-menu"
150
186
  }
151
187
  });
152
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "workspace-tools");
153
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "primary-menu");
188
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "workspace-tools:primary-menu");
154
189
  });
155
190
  });
156
191
 
@@ -226,7 +261,7 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
226
261
  `<template>
227
262
  <SectionContainerShell>
228
263
  <template #tabs>
229
- <ShellOutlet host="contact-view" position="sub-pages" />
264
+ <ShellOutlet target="contact-view:sub-pages" />
230
265
  </template>
231
266
  <RouterView />
232
267
  </SectionContainerShell>
@@ -240,8 +275,7 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
240
275
  options: {}
241
276
  });
242
277
 
243
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "contact-view");
244
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
278
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "contact-view:sub-pages");
245
279
  assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
246
280
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes\",\n");
247
281
  });
@@ -265,7 +299,7 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
265
299
  `<template>
266
300
  <SectionContainerShell>
267
301
  <template #tabs>
268
- <ShellOutlet host="contact-view" position="sub-pages" />
302
+ <ShellOutlet target="contact-view:sub-pages" />
269
303
  </template>
270
304
  <RouterView />
271
305
  </SectionContainerShell>
@@ -279,8 +313,7 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
279
313
  options: {}
280
314
  });
281
315
 
282
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "contact-view");
283
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
316
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "contact-view:sub-pages");
284
317
  assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
285
318
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes/history\",\n");
286
319
  });
@@ -304,7 +337,7 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
304
337
  `<template>
305
338
  <SectionContainerShell>
306
339
  <template #tabs>
307
- <ShellOutlet host="catalog" position="sub-pages" />
340
+ <ShellOutlet target="catalog:sub-pages" />
308
341
  </template>
309
342
  <RouterView />
310
343
  </SectionContainerShell>
@@ -318,8 +351,7 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
318
351
  options: {}
319
352
  });
320
353
 
321
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "catalog");
322
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
354
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "catalog:sub-pages");
323
355
  assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
324
356
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./products\",\n");
325
357
  });
@@ -343,7 +375,7 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
343
375
  `<template>
344
376
  <SectionContainerShell>
345
377
  <template #tabs>
346
- <ShellOutlet host="catalog" position="sub-pages" />
378
+ <ShellOutlet target="catalog:sub-pages" />
347
379
  </template>
348
380
  <RouterView />
349
381
  </SectionContainerShell>
@@ -356,7 +388,7 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
356
388
  `<template>
357
389
  <SectionContainerShell>
358
390
  <template #tabs>
359
- <ShellOutlet host="catalog-products" position="sub-pages" />
391
+ <ShellOutlet target="catalog-products:sub-pages" />
360
392
  </template>
361
393
  <RouterView />
362
394
  </SectionContainerShell>
@@ -370,8 +402,7 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
370
402
  options: {}
371
403
  });
372
404
 
373
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "catalog-products");
374
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
405
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "catalog-products:sub-pages");
375
406
  assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
376
407
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./variants\",\n");
377
408
  });
@@ -395,7 +426,7 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
395
426
  `<template>
396
427
  <SectionContainerShell>
397
428
  <template #tabs>
398
- <ShellOutlet host="customer-view" position="sub-pages" />
429
+ <ShellOutlet target="customer-view:sub-pages" />
399
430
  </template>
400
431
  <RouterView />
401
432
  </SectionContainerShell>
@@ -409,8 +440,7 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
409
440
  options: {}
410
441
  });
411
442
 
412
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_HOST__, "customer-view");
413
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_POSITION__, "sub-pages");
443
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "customer-view:sub-pages");
414
444
  assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
415
445
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./pets\",\n");
416
446
  assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/customers/[customerId]/pets");
@@ -468,11 +498,35 @@ test("buildUiPageTemplateContext rejects target files with a leading src segment
468
498
  targetFile: "src/components/ReportsPanel.vue",
469
499
  options: {}
470
500
  }),
471
- /must be relative to src\/pages\/, without a leading src\/ segment/
501
+ /must be relative to src\/pages\/ or start with src\/pages\/:/
472
502
  );
473
503
  });
474
504
  });
475
505
 
506
+ test("buildUiPageTemplateContext accepts target files with a src/pages prefix", async () => {
507
+ await withTempApp(async (appRoot) => {
508
+ await writeConfig(
509
+ appRoot,
510
+ `export const config = {
511
+ surfaceDefinitions: {
512
+ admin: { id: "admin", pagesRoot: "admin", enabled: true }
513
+ }
514
+ };
515
+ `
516
+ );
517
+ await writeShellLayout(appRoot);
518
+
519
+ const context = await buildUiPageTemplateContext({
520
+ appRoot,
521
+ targetFile: "src/pages/admin/reports/index.vue",
522
+ options: {}
523
+ });
524
+
525
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_ID__, "ui-generator.page.admin.reports.link");
526
+ assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
527
+ });
528
+ });
529
+
476
530
  test("buildUiPageTemplateContext fails when the target file matches no surface", async () => {
477
531
  await withTempApp(async (appRoot) => {
478
532
  await writeConfig(
@@ -546,7 +600,7 @@ test("buildUiPageTemplateContext validates link placement format", async () => {
546
600
  "link-placement": "invalid-placement"
547
601
  }
548
602
  }),
549
- /option "placement" must be in "host:position" format/
603
+ /option "placement" must be a target in "host:position" format/
550
604
  );
551
605
  });
552
606
  });
@@ -23,8 +23,12 @@ async function writeAppFixture(appRoot) {
23
23
  path.join(appRoot, "src", "components", "ShellLayout.vue"),
24
24
  `<template>
25
25
  <div>
26
- <ShellOutlet host="shell-layout" position="primary-menu" default />
27
- <ShellOutlet host="shell-layout" position="top-right" />
26
+ <ShellOutlet
27
+ target="shell-layout:primary-menu"
28
+ default
29
+ default-link-component-token="local.main.ui.surface-aware-menu-link-item"
30
+ />
31
+ <ShellOutlet target="shell-layout:top-right" />
28
32
  </div>
29
33
  </template>
30
34
  `,
@@ -34,7 +38,7 @@ async function writeAppFixture(appRoot) {
34
38
  path.join(appRoot, "src", "pages", "admin", "workspace", "settings", "index.vue"),
35
39
  `<template>
36
40
  <section>
37
- <ShellOutlet host="admin-settings" position="forms" />
41
+ <ShellOutlet target="admin-settings:forms" />
38
42
  </section>
39
43
  </template>
40
44
  `,
@@ -104,8 +108,7 @@ test("ui-generator placed-element subcommand creates component and outlet placem
104
108
 
105
109
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
106
110
  assert.match(placementSource, /id: "ui-generator\.element\.ops-panel"/);
107
- assert.match(placementSource, /host: "shell-layout"/);
108
- assert.match(placementSource, /position: "top-right"/);
111
+ assert.match(placementSource, /target: "shell-layout:top-right"/);
109
112
  assert.match(placementSource, /componentToken: "local\.main\.ui\.element\.ops-panel"/);
110
113
  });
111
114
  });
@@ -125,8 +128,7 @@ test("ui-generator placed-element subcommand supports explicit placement overrid
125
128
  });
126
129
 
127
130
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
128
- assert.match(placementSource, /host: "shell-layout"/);
129
- assert.match(placementSource, /position: "primary-menu"/);
131
+ assert.match(placementSource, /target: "shell-layout:primary-menu"/);
130
132
  });
131
133
  });
132
134
 
@@ -40,7 +40,7 @@ import { computed } from "vue";
40
40
  subcommand: "outlet",
41
41
  args: [targetFile],
42
42
  options: {
43
- target: "contact-view"
43
+ target: "contact-view:sub-pages"
44
44
  }
45
45
  });
46
46
 
@@ -48,7 +48,7 @@ import { computed } from "vue";
48
48
 
49
49
  const output = await readFile(targetPath, "utf8");
50
50
  assert.match(output, /import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
51
- assert.match(output, /<ShellOutlet host="contact-view" position="sub-pages" \/>/);
51
+ assert.match(output, /<ShellOutlet target="contact-view:sub-pages" \/>/);
52
52
  assert.doesNotMatch(output, /RouterView/);
53
53
  assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
54
54
 
@@ -57,7 +57,7 @@ import { computed } from "vue";
57
57
  subcommand: "outlet",
58
58
  args: [targetFile],
59
59
  options: {
60
- target: "contact-view"
60
+ target: "contact-view:sub-pages"
61
61
  }
62
62
  });
63
63
 
@@ -79,7 +79,7 @@ import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
79
79
 
80
80
  <template>
81
81
  <section>
82
- <ShellOutlet host="contact-view" position="sub-pages" />
82
+ <ShellOutlet target="contact-view:sub-pages" />
83
83
  </section>
84
84
  </template>
85
85
  `,
@@ -91,12 +91,12 @@ import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
91
91
  subcommand: "outlet",
92
92
  args: [targetFile],
93
93
  options: {
94
- target: "contact-view"
94
+ target: "contact-view:sub-pages"
95
95
  }
96
96
  });
97
97
 
98
98
  const output = await readFile(targetPath, "utf8");
99
- assert.equal((output.match(/<ShellOutlet host="contact-view" position="sub-pages" \/>/g) || []).length, 1);
99
+ assert.equal((output.match(/<ShellOutlet target="contact-view:sub-pages" \/>/g) || []).length, 1);
100
100
  assert.equal(
101
101
  (output.match(/import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/g) || []).length,
102
102
  1
@@ -124,7 +124,7 @@ test("ui-generator outlet creates script setup when missing", async () => {
124
124
  subcommand: "outlet",
125
125
  args: [targetFile],
126
126
  options: {
127
- target: "contact-view"
127
+ target: "contact-view:sub-pages"
128
128
  }
129
129
  });
130
130
 
@@ -161,7 +161,7 @@ test("ui-generator outlet inserts generated script after existing route block",
161
161
  subcommand: "outlet",
162
162
  args: [targetFile],
163
163
  options: {
164
- target: "contact-view"
164
+ target: "contact-view:sub-pages"
165
165
  }
166
166
  });
167
167
 
@@ -203,12 +203,12 @@ test("ui-generator outlet keeps indentation when injected into nested template b
203
203
  subcommand: "outlet",
204
204
  args: [targetFile],
205
205
  options: {
206
- target: "contact-view"
206
+ target: "contact-view:sub-pages"
207
207
  }
208
208
  });
209
209
 
210
210
  const output = await readFile(targetPath, "utf8");
211
- assert.match(output, /\n\s{2}<\/section>\n\s{2}<ShellOutlet host="contact-view" position="sub-pages" \/>\n<\/template>/);
211
+ assert.match(output, /\n\s{2}<\/section>\n\s{2}<ShellOutlet target="contact-view:sub-pages" \/>\n<\/template>/);
212
212
  assert.match(output, /<template v-else-if="view\.isLoading">\n\s*<v-skeleton-loader type="heading, text@2, article" \/>\n\s*<\/template>/);
213
213
  assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
214
214
  });
@@ -226,11 +226,11 @@ test("ui-generator outlet rejects unsupported options", async () => {
226
226
  runGeneratorSubcommand({
227
227
  appRoot,
228
228
  subcommand: "outlet",
229
- args: [targetFile],
230
- options: {
231
- target: "contact-view",
232
- bogus: "routed"
233
- }
229
+ args: [targetFile],
230
+ options: {
231
+ target: "contact-view:sub-pages",
232
+ bogus: "routed"
233
+ }
234
234
  }),
235
235
  /ui-generator outlet received unsupported option: --bogus\./
236
236
  );
@@ -255,7 +255,33 @@ test("ui-generator outlet supports explicit target host:position", async () => {
255
255
  });
256
256
 
257
257
  const output = await readFile(targetPath, "utf8");
258
- assert.match(output, /<ShellOutlet host="customer-view" position="summary-actions" \/>/);
258
+ assert.match(output, /<ShellOutlet target="customer-view:summary-actions" \/>/);
259
+ });
260
+ });
261
+
262
+ test("ui-generator outlet rejects non-vue target files without changing them", async () => {
263
+ await withTempApp(async (appRoot) => {
264
+ const targetFile = "src/pages/w/[workspaceSlug]/admin/practice/vets/_components/VetAddEditFormFields.js";
265
+ const targetPath = path.join(appRoot, targetFile);
266
+ const originalSource = "export const fields = [];\n";
267
+
268
+ await mkdir(path.dirname(targetPath), { recursive: true });
269
+ await writeFile(targetPath, originalSource, "utf8");
270
+
271
+ await assert.rejects(
272
+ runGeneratorSubcommand({
273
+ appRoot,
274
+ subcommand: "outlet",
275
+ args: [targetFile],
276
+ options: {
277
+ target: "vet-view:sub-pages"
278
+ }
279
+ }),
280
+ /ui-generator outlet target file must be an existing Vue SFC \(\.vue\): src\/pages\/w\/\[workspaceSlug\]\/admin\/practice\/vets\/_components\/VetAddEditFormFields\.js\./
281
+ );
282
+
283
+ const output = await readFile(targetPath, "utf8");
284
+ assert.equal(output, originalSource);
259
285
  });
260
286
  });
261
287
 
@@ -276,7 +302,7 @@ test("ui-generator outlet validates target format", async () => {
276
302
  target: "customer-view:"
277
303
  }
278
304
  }),
279
- /ui-generator outlet option "target" must be "host" or "host:position"\./
305
+ /ui-generator outlet option "target" must be a target in "host:position" format\./
280
306
  );
281
307
  });
282
308
  });