@jskit-ai/ui-generator 0.1.16 → 0.1.18

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,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/ui-generator",
4
- version: "0.1.16",
4
+ version: "0.1.18",
5
5
  kind: "generator",
6
6
  description: "Create non-CRUD pages, reusable UI elements, and subpage hosts.",
7
7
  options: {
@@ -40,14 +40,14 @@ export default Object.freeze({
40
40
  inputType: "text",
41
41
  defaultValue: "",
42
42
  promptLabel: "Placement target",
43
- promptHint: "Optional host:position target for placed-element placement (defaults to shell-layout:top-right)."
43
+ promptHint: "Optional target for placed-element placement (format: host:position, default: shell-layout:top-right)."
44
44
  },
45
45
  "link-placement": {
46
46
  required: false,
47
47
  inputType: "text",
48
48
  defaultValue: "",
49
49
  promptLabel: "Link placement",
50
- promptHint: "Optional host:position target for the generated page link placement."
50
+ promptHint: "Optional target for the generated page link placement (format: host:position)."
51
51
  },
52
52
  "link-component-token": {
53
53
  required: false,
@@ -63,15 +63,14 @@ export default Object.freeze({
63
63
  defaultValue: "",
64
64
  promptLabel: "Link to",
65
65
  promptHint:
66
- "Optional explicit props.to value for the generated page link placement (example: ./notes). If omitted for pages under a detected parent subpages host, it is inferred from the page path."
66
+ "Optional explicit props.to value for the generated page link placement (example: ./notes). If omitted for pages under a detected parent subpages target, it is inferred from the page path."
67
67
  },
68
68
  target: {
69
69
  required: false,
70
70
  inputType: "text",
71
71
  defaultValue: "",
72
72
  promptLabel: "Outlet target",
73
- promptHint:
74
- "Used by add-subpages and outlet. Accepts host or host:position. If only host is given, position defaults to sub-pages."
73
+ promptHint: "Used by add-subpages and outlet. Must be a target in host:position format."
75
74
  },
76
75
  title: {
77
76
  required: false,
@@ -105,13 +104,14 @@ export default Object.freeze({
105
104
  generatorPrimarySubcommand: "page",
106
105
  generatorSubcommands: {
107
106
  page: {
107
+ requiresShellWeb: true,
108
108
  entrypoint: "src/server/subcommands/page.js",
109
109
  export: "runGeneratorSubcommand",
110
110
  description: "Create a route page at an explicit target file and add a link placement entry for it.",
111
111
  longDescription: [
112
112
  "This command always creates one route page file. By default, its page link is placed from the page path itself.",
113
- "If an ancestor page has already been enhanced with sub-pages, JSKIT treats that ancestor as the real host. In that case the new page is linked into the nearest parent sub-pages outlet instead of the shell menu.",
114
- "That means the generated link normally becomes a tab or child-page link under that ancestor host, and `props.to` is inferred relative to that host. If the host page is `index.vue`, child pages belong under `index/...` so the router keeps the parent page visible while the child route renders underneath it."
113
+ "If an ancestor page has already been enhanced with sub-pages, JSKIT treats that ancestor outlet as the real placement target. In that case the new page is linked into the nearest parent sub-pages outlet instead of the shell menu.",
114
+ "That means the generated link normally becomes a tab or child-page link under that ancestor target, and `props.to` is inferred relative to that target. If the outlet page is `index.vue`, child pages belong under `index/...` so the router keeps the parent page visible while the child route renders underneath it."
115
115
  ],
116
116
  positionalArgs: [
117
117
  {
@@ -122,8 +122,8 @@ export default Object.freeze({
122
122
  ],
123
123
  optionNames: ["name", "link-placement", "link-component-token", "link-to", "force"],
124
124
  notes: [
125
- "If a nearest parent subpages host is found, placement, link component token, and props.to are inferred automatically.",
126
- "If the parent host page is index.vue, child pages belong under index/...",
125
+ "If a nearest parent subpages target is found, placement, link component token, and props.to are inferred automatically.",
126
+ "If the parent target page is index.vue, child pages belong under index/...",
127
127
  "If the target page file already exists, rerun with --force to overwrite it."
128
128
  ],
129
129
  examples: [
@@ -147,6 +147,7 @@ export default Object.freeze({
147
147
  ]
148
148
  },
149
149
  "placed-element": {
150
+ requiresShellWeb: true,
150
151
  entrypoint: "src/server/subcommands/element.js",
151
152
  export: "runGeneratorSubcommand",
152
153
  description: "Create a Vue component file under the chosen component directory (default: src/components) and add a placement entry that renders it.",
@@ -179,6 +180,7 @@ export default Object.freeze({
179
180
  ]
180
181
  },
181
182
  "add-subpages": {
183
+ requiresShellWeb: true,
182
184
  entrypoint: "src/server/subcommands/addSubpages.js",
183
185
  export: "runGeneratorSubcommand",
184
186
  description: "Upgrade an existing page into a routed subpage host with SectionContainerShell, ShellOutlet, and RouterView.",
@@ -192,7 +194,7 @@ export default Object.freeze({
192
194
  optionNames: ["target", "path", "title", "subtitle"],
193
195
  notes: [
194
196
  "Use this when the page should render shared content plus child routes below it.",
195
- "If the host page is index.vue, create child pages under index/..."
197
+ "If the outlet page is index.vue, create child pages under index/..."
196
198
  ],
197
199
  examples: [
198
200
  {
@@ -218,13 +220,14 @@ export default Object.freeze({
218
220
  ]
219
221
  },
220
222
  outlet: {
223
+ requiresShellWeb: true,
221
224
  entrypoint: "src/server/subcommands/outlet.js",
222
225
  export: "runGeneratorSubcommand",
223
226
  description: "Inject a generic ShellOutlet block into an existing Vue page/component.",
224
227
  longDescription: [
225
228
  "A ShellOutlet creates a named placement target inside a Vue file. That target is what other parts of JSKIT render into later.",
226
- "After an outlet exists, `jskit list-placements` will discover it and show it as `host:position`. That makes the target visible to humans and to generators that need a placement destination.",
227
- "Commands that create placed UI, such as `ui-generator placed-element`, and commands that add page links can then target that outlet by writing placement entries that point at the same `host:position`."
229
+ "After an outlet exists, `jskit list-placements` will discover it and show its `target`. That makes the outlet visible to humans and to generators that need a placement destination.",
230
+ "Commands that create placed UI, such as `ui-generator placed-element`, and commands that add page links can then target that outlet by writing placement entries that point at the same target."
228
231
  ],
229
232
  positionalArgs: [
230
233
  {
@@ -236,7 +239,7 @@ export default Object.freeze({
236
239
  optionNames: ["target"],
237
240
  requiredOptionNames: ["target"],
238
241
  notes: [
239
- "Use --target host or --target host:position. If only host is given, position defaults to sub-pages."
242
+ "Use --target host:position."
240
243
  ],
241
244
  examples: [
242
245
  {
@@ -244,7 +247,7 @@ export default Object.freeze({
244
247
  lines: [
245
248
  "npx jskit generate ui-generator outlet \\",
246
249
  " src/components/ContactSummaryCard.vue \\",
247
- " --target contact-view"
250
+ " --target contact-view:sub-pages"
248
251
  ]
249
252
  },
250
253
  {
@@ -274,7 +277,7 @@ export default Object.freeze({
274
277
  mutations: {
275
278
  dependencies: {
276
279
  runtime: {
277
- "@jskit-ai/users-web": "0.1.48"
280
+ "@jskit-ai/users-web": "0.1.50"
278
281
  },
279
282
  dev: {}
280
283
  },
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@jskit-ai/ui-generator",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/kernel": "0.1.33"
9
+ "@jskit-ai/kernel": "0.1.35",
10
+ "@jskit-ai/shell-web": "0.1.34"
10
11
  },
11
12
  "exports": {
12
13
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -32,11 +32,11 @@ async function buildUiPageTemplateContext({
32
32
 
33
33
  return {
34
34
  __JSKIT_UI_LINK_PLACEMENT_ID__: pageTarget.placementId,
35
- __JSKIT_UI_LINK_PLACEMENT_HOST__: String(linkTarget.placementTarget?.host || ""),
36
- __JSKIT_UI_LINK_PLACEMENT_POSITION__: String(linkTarget.placementTarget?.position || ""),
35
+ __JSKIT_UI_LINK_PLACEMENT_TARGET__: String(linkTarget.placementTarget?.id || ""),
37
36
  __JSKIT_UI_LINK_COMPONENT_TOKEN__: String(linkTarget.componentToken || ""),
38
37
  __JSKIT_UI_LINK_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
39
38
  __JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
39
+ __JSKIT_UI_LINK_WHEN_LINE__: String(linkTarget.whenLine || ""),
40
40
  __JSKIT_UI_LINK_TO_PROP_LINE__: resolveLinkToPropLine(linkTarget.linkTo)
41
41
  };
42
42
  }
@@ -14,10 +14,10 @@ import {
14
14
 
15
15
  function resolveSubpagesOutletTarget(options = {}, pageTarget = {}) {
16
16
  const rawTarget = normalizeText(options?.target);
17
- return resolveOutletTargetId(rawTarget || deriveDefaultSubpagesHost(pageTarget), {
17
+ const defaultTarget = `${deriveDefaultSubpagesHost(pageTarget)}:${DEFAULT_SUBPAGES_POSITION}`;
18
+ return resolveOutletTargetId(rawTarget || defaultTarget, {
18
19
  context: "ui-generator add-subpages",
19
- optionName: "target",
20
- defaultPosition: DEFAULT_SUBPAGES_POSITION
20
+ optionName: "target"
21
21
  });
22
22
  }
23
23
 
@@ -52,8 +52,7 @@ async function runGeneratorSubcommand({
52
52
  const result = await upgradePageFileToSubpages({
53
53
  appRoot,
54
54
  targetFile,
55
- host: outletTarget.host,
56
- position: outletTarget.position,
55
+ target: outletTarget.id,
57
56
  title,
58
57
  subtitle,
59
58
  componentDirectory,
@@ -135,8 +135,7 @@ async function runGeneratorSubcommand({
135
135
  "{\n" +
136
136
  " addPlacement({\n" +
137
137
  ` id: "ui-generator.element.${elementNameKebab}",\n` +
138
- ` host: "${placementTarget.host}",\n` +
139
- ` position: "${placementTarget.position}",\n` +
138
+ ` target: "${placementTarget.id}",\n` +
140
139
  ` surfaces: ["${surface}"],\n` +
141
140
  " order: 155,\n" +
142
141
  ` componentToken: "${componentToken}"\n` +
@@ -12,25 +12,21 @@ import {
12
12
  indentBlock
13
13
  } from "./support.js";
14
14
 
15
- const DEFAULT_OUTLET_POSITION = "sub-pages";
16
-
17
15
  const ROUTE_TAG_PATTERN = /<route\b[^>]*>[\s\S]*?<\/route>\s*/gi;
18
16
  const TEMPLATE_CLOSE_TAG_PATTERN = /<\/template>/gi;
19
17
  const SHELL_OUTLET_TAG_PATTERN = /<ShellOutlet\b([^>]*)\/?>/gi;
20
18
 
21
- function hasShellOutletTarget(source = "", { host = "", position = "" } = {}) {
22
- const normalizedHost = normalizeText(host);
23
- const normalizedPosition = normalizeText(position);
24
- if (!normalizedHost || !normalizedPosition) {
19
+ function hasShellOutletTarget(source = "", { target = "" } = {}) {
20
+ const normalizedTarget = normalizeText(target);
21
+ if (!normalizedTarget) {
25
22
  return false;
26
23
  }
27
24
 
28
25
  const sourceText = String(source || "");
29
26
  for (const match of sourceText.matchAll(SHELL_OUTLET_TAG_PATTERN)) {
30
27
  const attributes = parseTagAttributes(match[1]);
31
- const outletHost = normalizeText(attributes.host);
32
- const outletPosition = normalizeText(attributes.position);
33
- if (outletHost === normalizedHost && outletPosition === normalizedPosition) {
28
+ const outletTarget = normalizeText(attributes.target);
29
+ if (outletTarget === normalizedTarget) {
34
30
  return true;
35
31
  }
36
32
  }
@@ -75,8 +71,8 @@ function applyScriptImports(source = "") {
75
71
  };
76
72
  }
77
73
 
78
- function createOutletBlock({ host = "", position = "" } = {}) {
79
- return `<ShellOutlet host=\"${host}\" position=\"${position}\" />`;
74
+ function createOutletBlock({ target = "" } = {}) {
75
+ return `<ShellOutlet target="${target}" />`;
80
76
  }
81
77
 
82
78
  function findLastTemplateCloseTag(source = "") {
@@ -88,9 +84,9 @@ function findLastTemplateCloseTag(source = "") {
88
84
  return lastMatch;
89
85
  }
90
86
 
91
- function applyOutletTemplateBlock(source = "", { host = "", position = "" } = {}) {
87
+ function applyOutletTemplateBlock(source = "", { target = "" } = {}) {
92
88
  const sourceText = String(source || "");
93
- const outletBlock = createOutletBlock({ host, position });
89
+ const outletBlock = createOutletBlock({ target });
94
90
 
95
91
  const templateTagMatch = findLastTemplateCloseTag(sourceText);
96
92
  if (!templateTagMatch) {
@@ -133,10 +129,9 @@ async function runGeneratorSubcommand({
133
129
 
134
130
  const outletTarget = resolveOutletTargetId(options?.target, {
135
131
  context: "ui-generator outlet",
136
- optionName: "target",
137
- defaultPosition: DEFAULT_OUTLET_POSITION
132
+ optionName: "target"
138
133
  });
139
- const { host, position } = outletTarget;
134
+ const targetId = outletTarget.id;
140
135
 
141
136
  const targetFilePath = resolvePathWithinApp(appRoot, targetFile, {
142
137
  context: "ui-generator outlet"
@@ -154,12 +149,11 @@ async function runGeneratorSubcommand({
154
149
  throw new Error(`ui-generator outlet target file not found: ${targetFilePath.relativePath}.`);
155
150
  }
156
151
 
157
- const hasTargetOutlet = hasShellOutletTarget(source, { host, position });
152
+ const hasTargetOutlet = hasShellOutletTarget(source, { target: targetId });
158
153
  const templateApplied = hasTargetOutlet
159
154
  ? { changed: false, content: source }
160
155
  : applyOutletTemplateBlock(source, {
161
- host,
162
- position
156
+ target: targetId
163
157
  });
164
158
  const scriptApplied = applyScriptImports(templateApplied.content);
165
159
 
@@ -171,8 +165,8 @@ async function runGeneratorSubcommand({
171
165
  return {
172
166
  touchedFiles: changed ? [targetFilePath.relativePath] : [],
173
167
  summary: changed
174
- ? `Injected outlet \"${host}:${position}\" into ${targetFilePath.relativePath}.`
175
- : `Outlet \"${host}:${position}\" is already present in ${targetFilePath.relativePath}.`
168
+ ? `Injected outlet "${targetId}" into ${targetFilePath.relativePath}.`
169
+ : `Outlet "${targetId}" is already present in ${targetFilePath.relativePath}.`
176
170
  };
177
171
  }
178
172
 
@@ -25,8 +25,7 @@ function renderPageLinkPlacementBlock({
25
25
  "{\n" +
26
26
  " addPlacement({\n" +
27
27
  ` id: "${context.__JSKIT_UI_LINK_PLACEMENT_ID__}",\n` +
28
- ` host: "${context.__JSKIT_UI_LINK_PLACEMENT_HOST__}",\n` +
29
- ` position: "${context.__JSKIT_UI_LINK_PLACEMENT_POSITION__}",\n` +
28
+ ` target: "${context.__JSKIT_UI_LINK_PLACEMENT_TARGET__}",\n` +
30
29
  ` surfaces: ["${surface}"],\n` +
31
30
  " order: 155,\n" +
32
31
  ` componentToken: "${context.__JSKIT_UI_LINK_COMPONENT_TOKEN__}",\n` +
@@ -36,7 +35,7 @@ function renderPageLinkPlacementBlock({
36
35
  ` workspaceSuffix: "${context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__}",\n` +
37
36
  ` nonWorkspaceSuffix: "${context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__}",\n` +
38
37
  `${context.__JSKIT_UI_LINK_TO_PROP_LINE__} },\n` +
39
- " when: ({ auth }) => Boolean(auth?.authenticated)\n" +
38
+ `${String(context.__JSKIT_UI_LINK_WHEN_LINE__ || "")}` +
40
39
  " });\n" +
41
40
  "}\n"
42
41
  );
@@ -124,6 +123,7 @@ async function runGeneratorSubcommand({
124
123
 
125
124
  const touchedFileList = [...touchedFiles].sort((left, right) => left.localeCompare(right));
126
125
  return {
126
+ placementComponentTokens: [String(placementContext.__JSKIT_UI_LINK_COMPONENT_TOKEN__ || "").trim()].filter(Boolean),
127
127
  touchedFiles: touchedFileList,
128
128
  summary: !pageAlreadyExisted
129
129
  ? `Generated UI page "${pageTarget.routeUrlSuffix}" at ${pageRelativePath}.`
@@ -7,6 +7,11 @@ import {
7
7
  resolveRequiredAppRoot,
8
8
  toPosixPath
9
9
  } from "@jskit-ai/kernel/server/support";
10
+ import { normalizeShellOutletTargetId } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
11
+ import {
12
+ findLocalLinkItemDefinition,
13
+ readLocalLinkItemComponentSource
14
+ } from "@jskit-ai/shell-web/server/support/localLinkItemScaffolds";
10
15
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
11
16
  import {
12
17
  DEFAULT_COMPONENT_DIRECTORY,
@@ -20,8 +25,15 @@ import {
20
25
 
21
26
  const DEFAULT_SUBPAGES_POSITION = "sub-pages";
22
27
  const SECTION_CONTAINER_SHELL_COMPONENT = "SectionContainerShell";
23
- const TAB_LINK_COMPONENT = "TabLinkItem";
24
28
  const TAB_LINK_COMPONENT_TOKEN = "local.main.ui.tab-link-item";
29
+ const DEFAULT_MENU_COMPONENT_DIRECTORY = path.join(DEFAULT_COMPONENT_DIRECTORY, "menus");
30
+ const TAB_LINK_COMPONENT_DEFINITION = findLocalLinkItemDefinition(TAB_LINK_COMPONENT_TOKEN);
31
+
32
+ if (!TAB_LINK_COMPONENT_DEFINITION) {
33
+ throw new Error(`ui-generator add-subpages could not resolve ${TAB_LINK_COMPONENT_TOKEN} scaffold definition.`);
34
+ }
35
+
36
+ const TAB_LINK_COMPONENT = TAB_LINK_COMPONENT_DEFINITION.componentName;
25
37
 
26
38
  const ROUTE_TAG_PATTERN = /<route\b[^>]*>[\s\S]*?<\/route>\s*/gi;
27
39
  const TEMPLATE_TOKEN_PATTERN = /<\/?template\b[^>]*>/gi;
@@ -110,89 +122,6 @@ const hasTabs = computed(() => Boolean(slots.tabs));
110
122
  `;
111
123
  }
112
124
 
113
- function renderTabLinkItemSource() {
114
- return `<script setup>
115
- import { computed } from "vue";
116
- import { useRoute } from "vue-router";
117
- import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
118
- import {
119
- normalizeMenuLinkPathname,
120
- resolveMenuLinkTarget
121
- } from "@jskit-ai/users-web/client/support/menuLinkTarget";
122
-
123
- const props = defineProps({
124
- label: {
125
- type: String,
126
- default: ""
127
- },
128
- to: {
129
- type: String,
130
- default: ""
131
- },
132
- surface: {
133
- type: String,
134
- default: ""
135
- },
136
- workspaceSuffix: {
137
- type: String,
138
- default: "/"
139
- },
140
- nonWorkspaceSuffix: {
141
- type: String,
142
- default: "/"
143
- },
144
- disabled: {
145
- type: Boolean,
146
- default: false
147
- }
148
- });
149
-
150
- const route = useRoute();
151
- const paths = usePaths();
152
-
153
- const resolvedTo = computed(() => {
154
- return resolveMenuLinkTarget({
155
- to: props.to,
156
- surface: props.surface,
157
- currentSurfaceId: paths.currentSurfaceId.value,
158
- placementContext: paths.placementContext.value,
159
- workspaceSuffix: props.workspaceSuffix,
160
- nonWorkspaceSuffix: props.nonWorkspaceSuffix,
161
- routeParams: route.params || {},
162
- resolvePagePath(relativePath, options = {}) {
163
- return paths.page(relativePath, options);
164
- }
165
- });
166
- });
167
-
168
- const isActive = computed(() => {
169
- const targetPathname = normalizeMenuLinkPathname(resolvedTo.value);
170
- const currentPathname = normalizeMenuLinkPathname(route.fullPath || route.path);
171
- if (!targetPathname || !currentPathname) {
172
- return false;
173
- }
174
- return currentPathname === targetPathname || currentPathname.startsWith(\`\${targetPathname}/\`);
175
- });
176
- </script>
177
-
178
- <template>
179
- <v-btn
180
- v-if="resolvedTo"
181
- class="tab-link-item text-none"
182
- :to="resolvedTo"
183
- rounded="pill"
184
- size="small"
185
- :variant="isActive ? 'flat' : 'tonal'"
186
- :color="isActive ? 'primary' : undefined"
187
- :disabled="props.disabled"
188
- :aria-current="isActive ? 'page' : undefined"
189
- >
190
- {{ props.label }}
191
- </v-btn>
192
- </template>
193
- `;
194
- }
195
-
196
125
  async function ensureSubpagesSupportScaffold({
197
126
  appRoot,
198
127
  componentDirectory = DEFAULT_COMPONENT_DIRECTORY,
@@ -202,6 +131,10 @@ async function ensureSubpagesSupportScaffold({
202
131
  context: "ui-generator add-subpages"
203
132
  });
204
133
  const normalizedComponentDirectory = normalizeText(componentDirectory) || DEFAULT_COMPONENT_DIRECTORY;
134
+ const normalizedTabLinkComponentDirectory =
135
+ normalizedComponentDirectory === DEFAULT_COMPONENT_DIRECTORY
136
+ ? DEFAULT_MENU_COMPONENT_DIRECTORY
137
+ : normalizedComponentDirectory;
205
138
  const providerPath = resolvePathWithinApp(resolvedAppRoot, MAIN_CLIENT_PROVIDER_FILE, {
206
139
  context: "ui-generator add-subpages"
207
140
  });
@@ -212,21 +145,34 @@ async function ensureSubpagesSupportScaffold({
212
145
  );
213
146
  const tabLinkPath = resolvePathWithinApp(
214
147
  resolvedAppRoot,
215
- path.join(normalizedComponentDirectory, `${TAB_LINK_COMPONENT}.vue`),
148
+ path.join(normalizedTabLinkComponentDirectory, `${TAB_LINK_COMPONENT}.vue`),
216
149
  { context: "ui-generator add-subpages" }
217
150
  );
218
151
 
152
+ const providerSource = await readFile(providerPath.absolutePath, "utf8");
153
+ if (!/\bregisterMainClientComponent\s*\(/.test(providerSource)) {
154
+ throw new Error(
155
+ `ui-generator add-subpages could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
156
+ );
157
+ }
158
+
159
+ const providerRegisterLine = `registerMainClientComponent("${TAB_LINK_COMPONENT_TOKEN}", () => ${TAB_LINK_COMPONENT});`;
160
+ const providerHasTabLinkRegistration = providerSource.includes(providerRegisterLine);
219
161
  const touchedFiles = new Set();
220
- for (const supportFile of [
162
+ const supportFiles = [
221
163
  {
222
164
  path: sectionContainerShellPath,
223
165
  desiredSource: renderSectionContainerShellSource()
224
- },
225
- {
226
- path: tabLinkPath,
227
- desiredSource: renderTabLinkItemSource()
228
166
  }
229
- ]) {
167
+ ];
168
+ if (!providerHasTabLinkRegistration) {
169
+ supportFiles.push({
170
+ path: tabLinkPath,
171
+ desiredSource: await readLocalLinkItemComponentSource(TAB_LINK_COMPONENT_DEFINITION)
172
+ });
173
+ }
174
+
175
+ for (const supportFile of supportFiles) {
230
176
  let alreadyExists = true;
231
177
  try {
232
178
  await readFile(supportFile.path.absolutePath, "utf8");
@@ -245,15 +191,13 @@ async function ensureSubpagesSupportScaffold({
245
191
  touchedFiles.add(supportFile.path.relativePath);
246
192
  }
247
193
 
248
- const providerSource = await readFile(providerPath.absolutePath, "utf8");
249
- if (!/\bregisterMainClientComponent\s*\(/.test(providerSource)) {
250
- throw new Error(
251
- `ui-generator add-subpages could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
252
- );
194
+ const providerImportLine = `import ${TAB_LINK_COMPONENT} from "/${toPosixPath(path.join(normalizedTabLinkComponentDirectory, `${TAB_LINK_COMPONENT}.vue`))}";`;
195
+ if (providerHasTabLinkRegistration) {
196
+ return Object.freeze({
197
+ touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
198
+ sectionContainerComponentImportPath: `/${toPosixPath(path.join(normalizedComponentDirectory, `${SECTION_CONTAINER_SHELL_COMPONENT}.vue`))}`
199
+ });
253
200
  }
254
-
255
- const providerImportLine = `import ${TAB_LINK_COMPONENT} from "/${toPosixPath(path.join(normalizedComponentDirectory, `${TAB_LINK_COMPONENT}.vue`))}";`;
256
- const providerRegisterLine = `registerMainClientComponent("${TAB_LINK_COMPONENT_TOKEN}", () => ${TAB_LINK_COMPONENT});`;
257
201
  const providerImportApplied = insertImportIfMissing(providerSource, providerImportLine);
258
202
  const providerRegisterApplied = insertBeforeClassDeclaration(
259
203
  providerImportApplied.content,
@@ -358,14 +302,18 @@ function renderSubpagesTemplate({
358
302
  bodyContent = "",
359
303
  title = "",
360
304
  subtitle = "",
361
- host = "",
362
- position = DEFAULT_SUBPAGES_POSITION
305
+ target = ""
363
306
  } = {}) {
307
+ const normalizedTarget = normalizeShellOutletTargetId(target);
308
+ if (!normalizedTarget) {
309
+ throw new Error("ui-generator add-subpages requires target in \"host:position\" format.");
310
+ }
311
+
364
312
  const lines = [
365
313
  "<template>",
366
314
  renderSectionContainerOpenTag({ title, subtitle }),
367
315
  " <template #tabs>",
368
- ` <ShellOutlet host="${host}" position="${position}" />`,
316
+ ` <ShellOutlet target="${normalizedTarget}" default-link-component-token="${TAB_LINK_COMPONENT_TOKEN}" />`,
369
317
  " </template>"
370
318
  ];
371
319
 
@@ -430,18 +378,16 @@ function applySubpagesScriptImports(source = "", { sectionContainerComponentImpo
430
378
  function applySubpagesUpgradeToPageSource(
431
379
  source = "",
432
380
  {
433
- host = "",
434
- position = DEFAULT_SUBPAGES_POSITION,
381
+ target = "",
435
382
  title = "",
436
383
  subtitle = "",
437
384
  sectionContainerComponentImportPath = "/src/components/SectionContainerShell.vue",
438
385
  preserveExistingContent = true
439
386
  } = {}
440
387
  ) {
441
- const normalizedHost = normalizeText(host);
442
- const normalizedPosition = normalizeText(position) || DEFAULT_SUBPAGES_POSITION;
443
- if (!normalizedHost) {
444
- throw new Error("ui-generator add-subpages requires a valid host.");
388
+ const normalizedTarget = normalizeShellOutletTargetId(target);
389
+ if (!normalizedTarget) {
390
+ throw new Error('ui-generator add-subpages requires target in "host:position" format.');
445
391
  }
446
392
 
447
393
  const sourceText = String(source || "");
@@ -454,8 +400,7 @@ function applySubpagesUpgradeToPageSource(
454
400
  bodyContent,
455
401
  title,
456
402
  subtitle,
457
- host: normalizedHost,
458
- position: normalizedPosition
403
+ target: normalizedTarget
459
404
  });
460
405
 
461
406
  const nextSource = templateBlock
@@ -478,8 +423,7 @@ function hasExistingSubpagesRouting(source = "") {
478
423
  async function upgradePageFileToSubpages({
479
424
  appRoot,
480
425
  targetFile,
481
- host = "",
482
- position = DEFAULT_SUBPAGES_POSITION,
426
+ target = "",
483
427
  title = "",
484
428
  subtitle = "",
485
429
  componentDirectory = DEFAULT_COMPONENT_DIRECTORY,
@@ -512,8 +456,7 @@ async function upgradePageFileToSubpages({
512
456
  });
513
457
 
514
458
  const upgradeApplied = applySubpagesUpgradeToPageSource(source, {
515
- host,
516
- position,
459
+ target,
517
460
  title,
518
461
  subtitle,
519
462
  sectionContainerComponentImportPath: supportScaffold.sectionContainerComponentImportPath,
@@ -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,