@jskit-ai/ui-generator 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -123
- package/package.descriptor.mjs +182 -82
- package/package.json +2 -2
- package/src/server/buildTemplateContext.js +31 -136
- package/src/server/subcommands/addSubpages.js +73 -0
- package/src/server/subcommands/element.js +30 -15
- package/src/server/subcommands/outlet.js +26 -126
- package/src/server/subcommands/page.js +142 -0
- package/src/server/subcommands/pageSupport.js +552 -0
- package/src/server/subcommands/support.js +145 -1
- package/test/addSubpagesSubcommand.test.js +321 -0
- package/test/buildTemplateContext.test.js +426 -65
- package/test/elementSubcommand.test.js +79 -6
- package/test/outletSubcommand.test.js +92 -29
- package/test/packageDescriptor.test.js +10 -0
- package/test/pageSubcommand.test.js +352 -0
- package/src/server/subcommands/container.js +0 -644
- package/templates/src/pages/admin/ui-generator/Page.vue +0 -6
- package/test/containerSubcommand.test.js +0 -307
package/package.descriptor.mjs
CHANGED
|
@@ -1,96 +1,91 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/ui-generator",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.15",
|
|
5
5
|
kind: "generator",
|
|
6
|
-
description: "
|
|
6
|
+
description: "Create non-CRUD pages, reusable UI elements, and subpage hosts.",
|
|
7
7
|
options: {
|
|
8
8
|
name: {
|
|
9
|
-
required:
|
|
9
|
+
required: false,
|
|
10
10
|
inputType: "text",
|
|
11
11
|
defaultValue: "",
|
|
12
|
-
promptLabel: "
|
|
13
|
-
promptHint:
|
|
12
|
+
promptLabel: "Display label",
|
|
13
|
+
promptHint:
|
|
14
|
+
"Display label used for generated page links and named UI elements. For page, if omitted, it is derived from the target file path."
|
|
14
15
|
},
|
|
15
16
|
surface: {
|
|
16
|
-
required:
|
|
17
|
+
required: false,
|
|
17
18
|
inputType: "text",
|
|
19
|
+
validationType: "enabled-surface-id",
|
|
18
20
|
defaultFromConfig: "surfaceDefaultId",
|
|
19
21
|
promptLabel: "Target surface",
|
|
20
|
-
promptHint: "
|
|
22
|
+
promptHint: "Used by the placed-element subcommand. Must match an enabled surface id."
|
|
21
23
|
},
|
|
22
24
|
path: {
|
|
23
25
|
required: false,
|
|
24
26
|
inputType: "text",
|
|
25
27
|
defaultValue: "src/components",
|
|
26
28
|
promptLabel: "Component path",
|
|
27
|
-
promptHint: "Component directory relative to app root (used by element
|
|
29
|
+
promptHint: "Component directory relative to app root (used by placed-element and add-subpages support scaffold)."
|
|
28
30
|
},
|
|
29
|
-
|
|
31
|
+
force: {
|
|
30
32
|
required: false,
|
|
31
|
-
inputType: "
|
|
33
|
+
inputType: "flag",
|
|
32
34
|
defaultValue: "",
|
|
33
|
-
promptLabel: "
|
|
34
|
-
promptHint: "
|
|
35
|
-
},
|
|
36
|
-
"route-path": {
|
|
37
|
-
required: false,
|
|
38
|
-
inputType: "text",
|
|
39
|
-
defaultValue: "",
|
|
40
|
-
promptLabel: "Route path",
|
|
41
|
-
promptHint:
|
|
42
|
-
"Optional explicit container route path (example: contact-tools or contacts/[contactId]). Defaults to --name."
|
|
35
|
+
promptLabel: "Force overwrite",
|
|
36
|
+
promptHint: "Overwrite the generated file if it already exists."
|
|
43
37
|
},
|
|
44
38
|
placement: {
|
|
45
39
|
required: false,
|
|
46
40
|
inputType: "text",
|
|
47
41
|
defaultValue: "",
|
|
48
42
|
promptLabel: "Placement target",
|
|
49
|
-
promptHint: "Optional host:position target (defaults to
|
|
43
|
+
promptHint: "Optional host:position target for placed-element placement (defaults to shell-layout:top-right)."
|
|
50
44
|
},
|
|
51
|
-
"placement
|
|
45
|
+
"link-placement": {
|
|
52
46
|
required: false,
|
|
53
47
|
inputType: "text",
|
|
54
48
|
defaultValue: "",
|
|
55
|
-
promptLabel: "
|
|
56
|
-
promptHint:
|
|
57
|
-
"Optional component token override for generated menu placement (example: local.main.ui.tab-link-item)."
|
|
49
|
+
promptLabel: "Link placement",
|
|
50
|
+
promptHint: "Optional host:position target for the generated page link placement."
|
|
58
51
|
},
|
|
59
|
-
"
|
|
52
|
+
"link-component-token": {
|
|
60
53
|
required: false,
|
|
61
54
|
inputType: "text",
|
|
62
55
|
defaultValue: "",
|
|
63
|
-
promptLabel: "
|
|
56
|
+
promptLabel: "Link component token",
|
|
64
57
|
promptHint:
|
|
65
|
-
"Optional
|
|
58
|
+
"Optional component token override for the generated page link placement (example: local.main.ui.tab-link-item)."
|
|
66
59
|
},
|
|
67
|
-
|
|
60
|
+
"link-to": {
|
|
68
61
|
required: false,
|
|
69
62
|
inputType: "text",
|
|
70
63
|
defaultValue: "",
|
|
71
|
-
promptLabel: "
|
|
72
|
-
promptHint:
|
|
64
|
+
promptLabel: "Link to",
|
|
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."
|
|
73
67
|
},
|
|
74
|
-
|
|
68
|
+
target: {
|
|
75
69
|
required: false,
|
|
76
70
|
inputType: "text",
|
|
77
71
|
defaultValue: "",
|
|
78
|
-
promptLabel: "Outlet
|
|
79
|
-
promptHint:
|
|
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."
|
|
80
75
|
},
|
|
81
|
-
|
|
76
|
+
title: {
|
|
82
77
|
required: false,
|
|
83
78
|
inputType: "text",
|
|
84
|
-
defaultValue: "
|
|
85
|
-
promptLabel: "
|
|
86
|
-
promptHint: "
|
|
79
|
+
defaultValue: "",
|
|
80
|
+
promptLabel: "Section title",
|
|
81
|
+
promptHint: "Optional SectionContainerShell title override for add-subpages."
|
|
87
82
|
},
|
|
88
|
-
|
|
83
|
+
subtitle: {
|
|
89
84
|
required: false,
|
|
90
85
|
inputType: "text",
|
|
91
|
-
defaultValue: "
|
|
92
|
-
promptLabel: "
|
|
93
|
-
promptHint: "
|
|
86
|
+
defaultValue: "",
|
|
87
|
+
promptLabel: "Section subtitle",
|
|
88
|
+
promptHint: "Optional SectionContainerShell subtitle override for add-subpages."
|
|
94
89
|
}
|
|
95
90
|
},
|
|
96
91
|
dependsOn: [],
|
|
@@ -110,27 +105,157 @@ export default Object.freeze({
|
|
|
110
105
|
generatorPrimarySubcommand: "page",
|
|
111
106
|
generatorSubcommands: {
|
|
112
107
|
page: {
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
entrypoint: "src/server/subcommands/page.js",
|
|
109
|
+
export: "runGeneratorSubcommand",
|
|
110
|
+
description: "Create a route page at an explicit target file and add a link placement entry for it.",
|
|
111
|
+
longDescription: [
|
|
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."
|
|
115
|
+
],
|
|
116
|
+
positionalArgs: [
|
|
117
|
+
{
|
|
118
|
+
name: "target-file",
|
|
119
|
+
required: true,
|
|
120
|
+
descriptionKey: "page-target-file"
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
optionNames: ["name", "link-placement", "link-component-token", "link-to", "force"],
|
|
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/...",
|
|
127
|
+
"If the target page file already exists, rerun with --force to overwrite it."
|
|
128
|
+
],
|
|
129
|
+
examples: [
|
|
130
|
+
{
|
|
131
|
+
label: "Common usage",
|
|
132
|
+
lines: [
|
|
133
|
+
"npx jskit generate ui-generator page \\",
|
|
134
|
+
" admin/reports/index.vue \\",
|
|
135
|
+
" --name \"Reports\""
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
label: "More advanced usage",
|
|
140
|
+
lines: [
|
|
141
|
+
"npx jskit generate ui-generator page \\",
|
|
142
|
+
" admin/customers/[customerId]/index/notes/index.vue \\",
|
|
143
|
+
" --name \"Notes\" \\",
|
|
144
|
+
" --force"
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
]
|
|
115
148
|
},
|
|
116
|
-
element: {
|
|
149
|
+
"placed-element": {
|
|
117
150
|
entrypoint: "src/server/subcommands/element.js",
|
|
118
151
|
export: "runGeneratorSubcommand",
|
|
119
|
-
description: "
|
|
120
|
-
optionNames: ["name", "surface", "path", "placement"]
|
|
152
|
+
description: "Create a Vue component file under the chosen component directory (default: src/components) and add a placement entry that renders it.",
|
|
153
|
+
optionNames: ["name", "surface", "path", "placement", "force"],
|
|
154
|
+
requiredOptionNames: ["name", "surface"],
|
|
155
|
+
notes: [
|
|
156
|
+
"If --placement is omitted, the placed element is added at shell-layout:top-right.",
|
|
157
|
+
"If the component file already exists, rerun with --force to overwrite it."
|
|
158
|
+
],
|
|
159
|
+
examples: [
|
|
160
|
+
{
|
|
161
|
+
label: "Common usage",
|
|
162
|
+
lines: [
|
|
163
|
+
"npx jskit generate ui-generator placed-element \\",
|
|
164
|
+
" --name \"Alerts Widget\" \\",
|
|
165
|
+
" --surface admin"
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
label: "More advanced usage",
|
|
170
|
+
lines: [
|
|
171
|
+
"npx jskit generate ui-generator placed-element \\",
|
|
172
|
+
" --name \"Ops Panel\" \\",
|
|
173
|
+
" --surface admin \\",
|
|
174
|
+
" --path src/widgets \\",
|
|
175
|
+
" --placement shell-layout:top-right \\",
|
|
176
|
+
" --force"
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
]
|
|
121
180
|
},
|
|
122
|
-
|
|
123
|
-
entrypoint: "src/server/subcommands/
|
|
181
|
+
"add-subpages": {
|
|
182
|
+
entrypoint: "src/server/subcommands/addSubpages.js",
|
|
124
183
|
export: "runGeneratorSubcommand",
|
|
125
|
-
description: "
|
|
126
|
-
|
|
184
|
+
description: "Upgrade an existing page into a routed subpage host with SectionContainerShell, ShellOutlet, and RouterView.",
|
|
185
|
+
positionalArgs: [
|
|
186
|
+
{
|
|
187
|
+
name: "target-file",
|
|
188
|
+
required: true,
|
|
189
|
+
descriptionKey: "existing-page-target-file"
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
optionNames: ["target", "path", "title", "subtitle"],
|
|
193
|
+
notes: [
|
|
194
|
+
"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/..."
|
|
196
|
+
],
|
|
197
|
+
examples: [
|
|
198
|
+
{
|
|
199
|
+
label: "Common usage",
|
|
200
|
+
lines: [
|
|
201
|
+
"npx jskit generate ui-generator add-subpages \\",
|
|
202
|
+
" admin/customers/[customerId]/index.vue \\",
|
|
203
|
+
" --title \"Customer\" \\",
|
|
204
|
+
" --subtitle \"View and manage this customer.\""
|
|
205
|
+
]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: "More advanced usage",
|
|
209
|
+
lines: [
|
|
210
|
+
"npx jskit generate ui-generator add-subpages \\",
|
|
211
|
+
" admin/contacts/[contactId]/index.vue \\",
|
|
212
|
+
" --target contact-view:summary-tabs \\",
|
|
213
|
+
" --path src/components/admin \\",
|
|
214
|
+
" --title \"Contact\" \\",
|
|
215
|
+
" --subtitle \"Manage contact modules.\""
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
]
|
|
127
219
|
},
|
|
128
220
|
outlet: {
|
|
129
221
|
entrypoint: "src/server/subcommands/outlet.js",
|
|
130
222
|
export: "runGeneratorSubcommand",
|
|
131
|
-
description: "Inject a ShellOutlet block into an existing Vue page/component.",
|
|
132
|
-
|
|
133
|
-
|
|
223
|
+
description: "Inject a generic ShellOutlet block into an existing Vue page/component.",
|
|
224
|
+
longDescription: [
|
|
225
|
+
"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`."
|
|
228
|
+
],
|
|
229
|
+
positionalArgs: [
|
|
230
|
+
{
|
|
231
|
+
name: "target-file",
|
|
232
|
+
required: true,
|
|
233
|
+
descriptionKey: "existing-vue-sfc-target-file"
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
optionNames: ["target"],
|
|
237
|
+
requiredOptionNames: ["target"],
|
|
238
|
+
notes: [
|
|
239
|
+
"Use --target host or --target host:position. If only host is given, position defaults to sub-pages."
|
|
240
|
+
],
|
|
241
|
+
examples: [
|
|
242
|
+
{
|
|
243
|
+
label: "Common usage",
|
|
244
|
+
lines: [
|
|
245
|
+
"npx jskit generate ui-generator outlet \\",
|
|
246
|
+
" src/components/ContactSummaryCard.vue \\",
|
|
247
|
+
" --target contact-view"
|
|
248
|
+
]
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
label: "More advanced usage",
|
|
252
|
+
lines: [
|
|
253
|
+
"npx jskit generate ui-generator outlet \\",
|
|
254
|
+
" src/pages/admin/customers/[customerId]/index.vue \\",
|
|
255
|
+
" --target customer-view:summary-actions"
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
]
|
|
134
259
|
}
|
|
135
260
|
},
|
|
136
261
|
apiSummary: {
|
|
@@ -149,7 +274,7 @@ export default Object.freeze({
|
|
|
149
274
|
mutations: {
|
|
150
275
|
dependencies: {
|
|
151
276
|
runtime: {
|
|
152
|
-
"@jskit-ai/users-web": "0.1.
|
|
277
|
+
"@jskit-ai/users-web": "0.1.47"
|
|
153
278
|
},
|
|
154
279
|
dev: {}
|
|
155
280
|
},
|
|
@@ -157,32 +282,7 @@ export default Object.freeze({
|
|
|
157
282
|
scripts: {}
|
|
158
283
|
},
|
|
159
284
|
procfile: {},
|
|
160
|
-
files: [
|
|
161
|
-
|
|
162
|
-
from: "templates/src/pages/admin/ui-generator/Page.vue",
|
|
163
|
-
toSurface: "${option:surface|lower}",
|
|
164
|
-
toSurfacePath: "${option:directory-prefix|pathprefix}${option:name|path}/index.vue",
|
|
165
|
-
reason: "Install generated UI page scaffold.",
|
|
166
|
-
category: "ui-generator",
|
|
167
|
-
id: "ui-generator-page-${option:name|snake}"
|
|
168
|
-
}
|
|
169
|
-
],
|
|
170
|
-
text: [
|
|
171
|
-
{
|
|
172
|
-
op: "append-text",
|
|
173
|
-
file: "src/placement.js",
|
|
174
|
-
position: "bottom",
|
|
175
|
-
skipIfContains: "jskit:ui-generator.page.menu:${option:surface|lower}:${option:directory-prefix|path}:${option:name|path}",
|
|
176
|
-
value:
|
|
177
|
-
"\n// jskit:ui-generator.page.menu:${option:surface|lower}:${option:directory-prefix|path}:${option:name|path}\n{\n addPlacement({\n id: \"__JSKIT_UI_MENU_PLACEMENT_ID__\",\n host: \"__JSKIT_UI_MENU_PLACEMENT_HOST__\",\n position: \"__JSKIT_UI_MENU_PLACEMENT_POSITION__\",\n surfaces: [\"${option:surface|lower}\"],\n order: 155,\n componentToken: \"__JSKIT_UI_MENU_COMPONENT_TOKEN__\",\n props: {\n label: \"${option:name|trim}\",\n surface: \"${option:surface|lower}\",\n workspaceSuffix: \"__JSKIT_UI_MENU_WORKSPACE_SUFFIX__\",\n nonWorkspaceSuffix: \"__JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__\",\n__JSKIT_UI_MENU_TO_PROP_LINE__ },\n when: ({ auth }) => Boolean(auth?.authenticated)\n });\n}\n",
|
|
178
|
-
reason: "Append generated UI page menu placement.",
|
|
179
|
-
category: "ui-generator",
|
|
180
|
-
id: "ui-generator-page-placement-menu-${option:name|snake}",
|
|
181
|
-
templateContext: {
|
|
182
|
-
entrypoint: "src/server/buildTemplateContext.js",
|
|
183
|
-
export: "buildUiPageTemplateContext"
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
]
|
|
285
|
+
files: [],
|
|
286
|
+
text: []
|
|
187
287
|
}
|
|
188
288
|
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/ui-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@jskit-ai/kernel": "0.1.
|
|
9
|
+
"@jskit-ai/kernel": "0.1.32"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
"./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
|
|
@@ -1,148 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
resolvePageLinkTargetDetails,
|
|
3
|
+
resolvePageTargetDetails
|
|
4
|
+
} from "@jskit-ai/kernel/server/support";
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function splitTextIntoWords(value = "") {
|
|
8
|
-
const normalized = String(value || "")
|
|
9
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
10
|
-
.replace(/[^A-Za-z0-9]+/g, " ")
|
|
11
|
-
.trim();
|
|
12
|
-
if (!normalized) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
return normalized
|
|
16
|
-
.split(/\s+/)
|
|
17
|
-
.map((entry) => entry.toLowerCase())
|
|
18
|
-
.filter(Boolean);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function wordsToKebab(words = []) {
|
|
22
|
-
return (Array.isArray(words) ? words : [])
|
|
23
|
-
.map((entry) => String(entry || "").toLowerCase())
|
|
24
|
-
.filter(Boolean)
|
|
25
|
-
.join("-");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function normalizePathValue(value = "") {
|
|
29
|
-
return String(value || "")
|
|
30
|
-
.replaceAll("\\", "/")
|
|
31
|
-
.split("/")
|
|
32
|
-
.map((segment) => {
|
|
33
|
-
const normalizedSegment = normalizeText(segment);
|
|
34
|
-
if (!normalizedSegment) {
|
|
35
|
-
return "";
|
|
36
|
-
}
|
|
37
|
-
if (/^\[[^\]]+\]$/.test(normalizedSegment)) {
|
|
38
|
-
return normalizedSegment;
|
|
39
|
-
}
|
|
40
|
-
const routeGroupMatch = /^\(([^()]+)\)$/.exec(normalizedSegment);
|
|
41
|
-
if (routeGroupMatch) {
|
|
42
|
-
const routeGroupName = wordsToKebab(splitTextIntoWords(routeGroupMatch[1]));
|
|
43
|
-
return routeGroupName ? `(${routeGroupName})` : "";
|
|
44
|
-
}
|
|
45
|
-
return wordsToKebab(splitTextIntoWords(normalizedSegment));
|
|
46
|
-
})
|
|
47
|
-
.filter(Boolean)
|
|
48
|
-
.join("/");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function splitPathSegments(value = "") {
|
|
52
|
-
return normalizePathValue(value)
|
|
53
|
-
.split("/")
|
|
54
|
-
.map((entry) => normalizeText(entry))
|
|
55
|
-
.filter(Boolean);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isRouteGroupSegment(value = "") {
|
|
59
|
-
const source = normalizeText(value);
|
|
60
|
-
return source.startsWith("(") && source.endsWith(")");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isNestedChildrenRouteGroupSegment(value = "") {
|
|
64
|
-
const source = normalizeText(value);
|
|
65
|
-
if (!isRouteGroupSegment(source)) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
const groupName = source.slice(1, -1).trim().toLowerCase();
|
|
69
|
-
return NESTED_CHILDREN_GROUPS.has(groupName);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function resolvePlacementUrlSuffix(options = {}) {
|
|
73
|
-
const routeSegments = [
|
|
74
|
-
...splitPathSegments(options?.["directory-prefix"]),
|
|
75
|
-
...splitPathSegments(options?.name)
|
|
76
|
-
].filter((segment) => !isRouteGroupSegment(segment));
|
|
77
|
-
if (routeSegments.length < 1) {
|
|
78
|
-
return "/";
|
|
79
|
-
}
|
|
80
|
-
return `/${routeSegments.join("/")}`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function resolveMenuComponentToken(options = {}) {
|
|
84
|
-
const explicitToken = normalizeText(options?.["placement-component-token"]);
|
|
85
|
-
if (explicitToken) {
|
|
86
|
-
return explicitToken;
|
|
87
|
-
}
|
|
88
|
-
return DEFAULT_MENU_COMPONENT_TOKEN;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function resolveAutoRelativePlacementTo(options = {}) {
|
|
92
|
-
const explicitPlacementTo = normalizeText(options?.["placement-to"]);
|
|
93
|
-
if (explicitPlacementTo) {
|
|
94
|
-
return explicitPlacementTo;
|
|
95
|
-
}
|
|
96
|
-
const directorySegments = splitPathSegments(options?.["directory-prefix"]);
|
|
97
|
-
const hasNestedChildrenGroup = directorySegments.some((segment) => isNestedChildrenRouteGroupSegment(segment));
|
|
98
|
-
if (!hasNestedChildrenGroup) {
|
|
6
|
+
function resolveLinkToPropLine(linkTo = "") {
|
|
7
|
+
if (!linkTo) {
|
|
99
8
|
return "";
|
|
100
9
|
}
|
|
101
|
-
|
|
102
|
-
if (!pagePath) {
|
|
103
|
-
return "";
|
|
104
|
-
}
|
|
105
|
-
return `./${pagePath}`;
|
|
10
|
+
return ` to: ${JSON.stringify(linkTo)},\n`;
|
|
106
11
|
}
|
|
107
12
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function normalizePlacementIdSegment(value = "") {
|
|
117
|
-
return wordsToKebab(splitTextIntoWords(value));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function resolveMenuPlacementId(options = {}) {
|
|
121
|
-
const idSegments = [
|
|
122
|
-
...splitPathSegments(options?.["directory-prefix"]),
|
|
123
|
-
...splitPathSegments(options?.name)
|
|
124
|
-
]
|
|
125
|
-
.map((segment) => normalizePlacementIdSegment(segment))
|
|
126
|
-
.filter(Boolean);
|
|
127
|
-
|
|
128
|
-
return `ui-generator.page.${idSegments.join(".")}.menu`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function buildUiPageTemplateContext({ appRoot, options } = {}) {
|
|
132
|
-
const placementTarget = await resolveShellOutletPlacementTargetFromApp({
|
|
13
|
+
async function buildUiPageTemplateContext({
|
|
14
|
+
appRoot,
|
|
15
|
+
targetFile = "",
|
|
16
|
+
options = {}
|
|
17
|
+
} = {}) {
|
|
18
|
+
const pageTarget = await resolvePageTargetDetails({
|
|
133
19
|
appRoot,
|
|
134
|
-
|
|
135
|
-
|
|
20
|
+
targetFile,
|
|
21
|
+
context: "ui-generator page"
|
|
22
|
+
});
|
|
23
|
+
const linkTarget = await resolvePageLinkTargetDetails({
|
|
24
|
+
appRoot: pageTarget.appRoot,
|
|
25
|
+
pageTarget,
|
|
26
|
+
targetFile,
|
|
27
|
+
context: "ui-generator page",
|
|
28
|
+
placement: options?.["link-placement"],
|
|
29
|
+
componentToken: options?.["link-component-token"],
|
|
30
|
+
linkTo: options?.["link-to"]
|
|
136
31
|
});
|
|
137
32
|
|
|
138
33
|
return {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 || ""),
|
|
37
|
+
__JSKIT_UI_LINK_COMPONENT_TOKEN__: String(linkTarget.componentToken || ""),
|
|
38
|
+
__JSKIT_UI_LINK_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
|
|
39
|
+
__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
|
|
40
|
+
__JSKIT_UI_LINK_TO_PROP_LINE__: resolveLinkToPropLine(linkTarget.linkTo)
|
|
146
41
|
};
|
|
147
42
|
}
|
|
148
43
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_COMPONENT_DIRECTORY,
|
|
4
|
+
DEFAULT_SUBPAGES_POSITION,
|
|
5
|
+
deriveDefaultSubpagesHost,
|
|
6
|
+
resolvePageTargetDetails,
|
|
7
|
+
upgradePageFileToSubpages
|
|
8
|
+
} from "./pageSupport.js";
|
|
9
|
+
import {
|
|
10
|
+
requireSinglePositionalTargetFile,
|
|
11
|
+
resolveOutletTargetId,
|
|
12
|
+
rejectUnexpectedOptions
|
|
13
|
+
} from "./support.js";
|
|
14
|
+
|
|
15
|
+
function resolveSubpagesOutletTarget(options = {}, pageTarget = {}) {
|
|
16
|
+
const rawTarget = normalizeText(options?.target);
|
|
17
|
+
return resolveOutletTargetId(rawTarget || deriveDefaultSubpagesHost(pageTarget), {
|
|
18
|
+
context: "ui-generator add-subpages",
|
|
19
|
+
optionName: "target",
|
|
20
|
+
defaultPosition: DEFAULT_SUBPAGES_POSITION
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function runGeneratorSubcommand({
|
|
25
|
+
appRoot,
|
|
26
|
+
subcommand = "",
|
|
27
|
+
args = [],
|
|
28
|
+
options = {},
|
|
29
|
+
dryRun = false
|
|
30
|
+
} = {}) {
|
|
31
|
+
const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
|
|
32
|
+
if (normalizedSubcommand !== "add-subpages") {
|
|
33
|
+
throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
34
|
+
}
|
|
35
|
+
const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator add-subpages" });
|
|
36
|
+
rejectUnexpectedOptions(
|
|
37
|
+
options,
|
|
38
|
+
["target", "path", "title", "subtitle"],
|
|
39
|
+
{ context: "ui-generator add-subpages" }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const componentDirectory = normalizeText(options?.path) || DEFAULT_COMPONENT_DIRECTORY;
|
|
43
|
+
const title = normalizeText(options?.title);
|
|
44
|
+
const subtitle = normalizeText(options?.subtitle);
|
|
45
|
+
const pageTarget = await resolvePageTargetDetails({
|
|
46
|
+
appRoot,
|
|
47
|
+
targetFile,
|
|
48
|
+
context: "ui-generator add-subpages"
|
|
49
|
+
});
|
|
50
|
+
const outletTarget = resolveSubpagesOutletTarget(options, pageTarget);
|
|
51
|
+
|
|
52
|
+
const result = await upgradePageFileToSubpages({
|
|
53
|
+
appRoot,
|
|
54
|
+
targetFile,
|
|
55
|
+
host: outletTarget.host,
|
|
56
|
+
position: outletTarget.position,
|
|
57
|
+
title,
|
|
58
|
+
subtitle,
|
|
59
|
+
componentDirectory,
|
|
60
|
+
preserveExistingContent: true,
|
|
61
|
+
dryRun
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
touchedFiles: result.touchedFiles,
|
|
66
|
+
summary:
|
|
67
|
+
result.touchedFiles.length > 0
|
|
68
|
+
? `Enabled subpages in ${result.targetFile} for "${pageTarget.routeUrlSuffix}" using outlet target "${outletTarget.id}".`
|
|
69
|
+
: `Subpages are already enabled in ${result.targetFile}.`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { runGeneratorSubcommand };
|