@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
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
deriveDefaultSubpagesHost,
|
|
5
|
+
resolveNearestParentSubpagesHost,
|
|
6
|
+
resolvePageTargetDetails,
|
|
7
|
+
resolveRequiredAppRoot,
|
|
8
|
+
toPosixPath
|
|
9
|
+
} from "@jskit-ai/kernel/server/support";
|
|
10
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_COMPONENT_DIRECTORY,
|
|
13
|
+
MAIN_CLIENT_PROVIDER_FILE,
|
|
14
|
+
resolvePathWithinApp,
|
|
15
|
+
insertImportIfMissing,
|
|
16
|
+
insertBeforeClassDeclaration,
|
|
17
|
+
findScriptBlock,
|
|
18
|
+
indentBlock
|
|
19
|
+
} from "./support.js";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_SUBPAGES_POSITION = "sub-pages";
|
|
22
|
+
const SECTION_CONTAINER_SHELL_COMPONENT = "SectionContainerShell";
|
|
23
|
+
const TAB_LINK_COMPONENT = "TabLinkItem";
|
|
24
|
+
const TAB_LINK_COMPONENT_TOKEN = "local.main.ui.tab-link-item";
|
|
25
|
+
|
|
26
|
+
const ROUTE_TAG_PATTERN = /<route\b[^>]*>[\s\S]*?<\/route>\s*/gi;
|
|
27
|
+
const TEMPLATE_TOKEN_PATTERN = /<\/?template\b[^>]*>/gi;
|
|
28
|
+
const SHELL_OUTLET_TAG_PATTERN = /<ShellOutlet\b[^>]*\/?>\s*/gi;
|
|
29
|
+
const ROUTER_VIEW_TAG_PATTERN = /<RouterView\b/i;
|
|
30
|
+
const ROUTER_VIEW_LINE_PATTERN = /^\s*<RouterView(?:\s[^>]*)?\s*\/>\s*$/gm;
|
|
31
|
+
|
|
32
|
+
function trimEdgeBlankLines(source = "") {
|
|
33
|
+
return String(source || "")
|
|
34
|
+
.replace(/^\s*\n/, "")
|
|
35
|
+
.replace(/\n\s*$/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderPlainPageSource(pageTitle = "") {
|
|
39
|
+
return `<template>
|
|
40
|
+
<section class="pa-4">
|
|
41
|
+
<h1 class="text-h5 mb-2">${pageTitle}</h1>
|
|
42
|
+
<p class="text-body-2 text-medium-emphasis">Replace this scaffold with your page implementation.</p>
|
|
43
|
+
</section>
|
|
44
|
+
</template>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderSectionContainerShellSource() {
|
|
49
|
+
return `<script setup>
|
|
50
|
+
import { computed, useSlots } from "vue";
|
|
51
|
+
|
|
52
|
+
const props = defineProps({
|
|
53
|
+
title: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: ""
|
|
56
|
+
},
|
|
57
|
+
subtitle: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: ""
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const slots = useSlots();
|
|
64
|
+
const resolvedTitle = computed(() => String(props.title || "").trim());
|
|
65
|
+
const resolvedSubtitle = computed(() => String(props.subtitle || "").trim());
|
|
66
|
+
const hasHeading = computed(() => Boolean(resolvedTitle.value || resolvedSubtitle.value));
|
|
67
|
+
const hasTabs = computed(() => Boolean(slots.tabs));
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<section class="section-container-shell d-flex flex-column ga-4">
|
|
72
|
+
<v-card rounded="lg" elevation="1" border>
|
|
73
|
+
<v-card-item v-if="hasHeading">
|
|
74
|
+
<v-card-title v-if="resolvedTitle" class="px-0">{{ resolvedTitle }}</v-card-title>
|
|
75
|
+
<v-card-subtitle v-if="resolvedSubtitle" class="px-0">{{ resolvedSubtitle }}</v-card-subtitle>
|
|
76
|
+
</v-card-item>
|
|
77
|
+
<template v-if="hasTabs">
|
|
78
|
+
<v-divider v-if="hasHeading" />
|
|
79
|
+
<v-card-text class="section-container-shell__tabs">
|
|
80
|
+
<slot name="tabs" />
|
|
81
|
+
</v-card-text>
|
|
82
|
+
</template>
|
|
83
|
+
</v-card>
|
|
84
|
+
|
|
85
|
+
<slot />
|
|
86
|
+
</section>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<style scoped>
|
|
90
|
+
.section-container-shell__tabs {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 0.5rem;
|
|
94
|
+
overflow-x: auto;
|
|
95
|
+
padding: 0.75rem;
|
|
96
|
+
scrollbar-width: thin;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.section-container-shell__tabs :deep(.tab-link-item) {
|
|
100
|
+
flex: 0 0 auto;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@media (max-width: 640px) {
|
|
104
|
+
.section-container-shell__tabs {
|
|
105
|
+
gap: 0.375rem;
|
|
106
|
+
padding: 0.5rem;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
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
|
+
async function ensureSubpagesSupportScaffold({
|
|
197
|
+
appRoot,
|
|
198
|
+
componentDirectory = DEFAULT_COMPONENT_DIRECTORY,
|
|
199
|
+
dryRun = false
|
|
200
|
+
} = {}) {
|
|
201
|
+
const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
|
|
202
|
+
context: "ui-generator add-subpages"
|
|
203
|
+
});
|
|
204
|
+
const normalizedComponentDirectory = normalizeText(componentDirectory) || DEFAULT_COMPONENT_DIRECTORY;
|
|
205
|
+
const providerPath = resolvePathWithinApp(resolvedAppRoot, MAIN_CLIENT_PROVIDER_FILE, {
|
|
206
|
+
context: "ui-generator add-subpages"
|
|
207
|
+
});
|
|
208
|
+
const sectionContainerShellPath = resolvePathWithinApp(
|
|
209
|
+
resolvedAppRoot,
|
|
210
|
+
path.join(normalizedComponentDirectory, `${SECTION_CONTAINER_SHELL_COMPONENT}.vue`),
|
|
211
|
+
{ context: "ui-generator add-subpages" }
|
|
212
|
+
);
|
|
213
|
+
const tabLinkPath = resolvePathWithinApp(
|
|
214
|
+
resolvedAppRoot,
|
|
215
|
+
path.join(normalizedComponentDirectory, `${TAB_LINK_COMPONENT}.vue`),
|
|
216
|
+
{ context: "ui-generator add-subpages" }
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const touchedFiles = new Set();
|
|
220
|
+
for (const supportFile of [
|
|
221
|
+
{
|
|
222
|
+
path: sectionContainerShellPath,
|
|
223
|
+
desiredSource: renderSectionContainerShellSource()
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
path: tabLinkPath,
|
|
227
|
+
desiredSource: renderTabLinkItemSource()
|
|
228
|
+
}
|
|
229
|
+
]) {
|
|
230
|
+
let alreadyExists = true;
|
|
231
|
+
try {
|
|
232
|
+
await readFile(supportFile.path.absolutePath, "utf8");
|
|
233
|
+
} catch {
|
|
234
|
+
alreadyExists = false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (alreadyExists) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (dryRun !== true) {
|
|
242
|
+
await mkdir(path.dirname(supportFile.path.absolutePath), { recursive: true });
|
|
243
|
+
await writeFile(supportFile.path.absolutePath, supportFile.desiredSource, "utf8");
|
|
244
|
+
}
|
|
245
|
+
touchedFiles.add(supportFile.path.relativePath);
|
|
246
|
+
}
|
|
247
|
+
|
|
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
|
+
);
|
|
253
|
+
}
|
|
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
|
+
const providerImportApplied = insertImportIfMissing(providerSource, providerImportLine);
|
|
258
|
+
const providerRegisterApplied = insertBeforeClassDeclaration(
|
|
259
|
+
providerImportApplied.content,
|
|
260
|
+
providerRegisterLine,
|
|
261
|
+
{
|
|
262
|
+
className: "MainClientProvider",
|
|
263
|
+
contextFile: MAIN_CLIENT_PROVIDER_FILE
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
if (providerImportApplied.changed || providerRegisterApplied.changed) {
|
|
267
|
+
if (dryRun !== true) {
|
|
268
|
+
await writeFile(providerPath.absolutePath, providerRegisterApplied.content, "utf8");
|
|
269
|
+
}
|
|
270
|
+
touchedFiles.add(providerPath.relativePath);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return Object.freeze({
|
|
274
|
+
touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
|
|
275
|
+
sectionContainerComponentImportPath: `/${toPosixPath(path.join(normalizedComponentDirectory, `${SECTION_CONTAINER_SHELL_COMPONENT}.vue`))}`
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function findTemplateBlock(source = "") {
|
|
280
|
+
const sourceText = String(source || "");
|
|
281
|
+
let openIndex = -1;
|
|
282
|
+
let openTagSource = "";
|
|
283
|
+
let openAttributesSource = "";
|
|
284
|
+
let depth = 0;
|
|
285
|
+
|
|
286
|
+
for (const match of sourceText.matchAll(TEMPLATE_TOKEN_PATTERN)) {
|
|
287
|
+
const tokenSource = String(match[0] || "");
|
|
288
|
+
const tokenIndex = Number(match.index);
|
|
289
|
+
const isClosingToken = /^<\/template/i.test(tokenSource);
|
|
290
|
+
|
|
291
|
+
if (!isClosingToken) {
|
|
292
|
+
if (openIndex < 0) {
|
|
293
|
+
openIndex = tokenIndex;
|
|
294
|
+
openTagSource = tokenSource;
|
|
295
|
+
const openTagMatch = /^<template\b([^>]*)>$/i.exec(tokenSource);
|
|
296
|
+
openAttributesSource = String(openTagMatch?.[1] || "");
|
|
297
|
+
}
|
|
298
|
+
depth += 1;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (openIndex < 0) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
depth -= 1;
|
|
307
|
+
if (depth === 0) {
|
|
308
|
+
const closeIndex = tokenIndex;
|
|
309
|
+
const endIndex = closeIndex + tokenSource.length;
|
|
310
|
+
return Object.freeze({
|
|
311
|
+
index: openIndex,
|
|
312
|
+
source: sourceText.slice(openIndex, endIndex),
|
|
313
|
+
attributesSource: openAttributesSource,
|
|
314
|
+
content: sourceText.slice(openIndex + openTagSource.length, closeIndex)
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function unwrapSectionContainerShell(source = "") {
|
|
323
|
+
const trimmedSource = trimEdgeBlankLines(source);
|
|
324
|
+
const match = /^\s*<SectionContainerShell\b[^>]*>([\s\S]*?)<\/SectionContainerShell>\s*$/i.exec(trimmedSource);
|
|
325
|
+
if (!match) {
|
|
326
|
+
return trimmedSource;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let nextContent = String(match[1] || "");
|
|
330
|
+
nextContent = nextContent.replace(/^\s*<template\b[^>]*#tabs[^>]*>[\s\S]*?<\/template>\s*/i, "");
|
|
331
|
+
nextContent = nextContent.replace(ROUTER_VIEW_LINE_PATTERN, "");
|
|
332
|
+
nextContent = nextContent.replace(SHELL_OUTLET_TAG_PATTERN, "");
|
|
333
|
+
return trimEdgeBlankLines(nextContent);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function stripExistingSubpagesStructure(source = "") {
|
|
337
|
+
const nextContent = unwrapSectionContainerShell(source)
|
|
338
|
+
.replace(SHELL_OUTLET_TAG_PATTERN, "")
|
|
339
|
+
.replace(ROUTER_VIEW_LINE_PATTERN, "");
|
|
340
|
+
return trimEdgeBlankLines(nextContent);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function renderSectionContainerOpenTag({ title = "", subtitle = "" } = {}) {
|
|
344
|
+
const lines = [" <SectionContainerShell"];
|
|
345
|
+
if (normalizeText(title)) {
|
|
346
|
+
lines.push(` title=${JSON.stringify(normalizeText(title))}`);
|
|
347
|
+
}
|
|
348
|
+
if (normalizeText(subtitle)) {
|
|
349
|
+
lines.push(` subtitle=${JSON.stringify(normalizeText(subtitle))}`);
|
|
350
|
+
}
|
|
351
|
+
if (lines.length === 1) {
|
|
352
|
+
return " <SectionContainerShell>";
|
|
353
|
+
}
|
|
354
|
+
return `${lines.join("\n")}\n >`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function renderSubpagesTemplate({
|
|
358
|
+
bodyContent = "",
|
|
359
|
+
title = "",
|
|
360
|
+
subtitle = "",
|
|
361
|
+
host = "",
|
|
362
|
+
position = DEFAULT_SUBPAGES_POSITION
|
|
363
|
+
} = {}) {
|
|
364
|
+
const lines = [
|
|
365
|
+
"<template>",
|
|
366
|
+
renderSectionContainerOpenTag({ title, subtitle }),
|
|
367
|
+
" <template #tabs>",
|
|
368
|
+
` <ShellOutlet host="${host}" position="${position}" />`,
|
|
369
|
+
" </template>"
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const normalizedBodyContent = trimEdgeBlankLines(bodyContent);
|
|
373
|
+
if (normalizedBodyContent) {
|
|
374
|
+
lines.push("");
|
|
375
|
+
lines.push(indentBlock(normalizedBodyContent, " "));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
lines.push("");
|
|
379
|
+
lines.push(" <RouterView />");
|
|
380
|
+
lines.push(" </SectionContainerShell>");
|
|
381
|
+
lines.push("</template>");
|
|
382
|
+
return `${lines.join("\n")}\n`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function applySubpagesScriptImports(source = "", { sectionContainerComponentImportPath = "" } = {}) {
|
|
386
|
+
const sourceText = String(source || "");
|
|
387
|
+
const scriptBlock = findScriptBlock(sourceText);
|
|
388
|
+
|
|
389
|
+
const importLines = [
|
|
390
|
+
"import ShellOutlet from \"@jskit-ai/shell-web/client/components/ShellOutlet\";",
|
|
391
|
+
"import { RouterView } from \"vue-router\";",
|
|
392
|
+
`import SectionContainerShell from "${sectionContainerComponentImportPath}";`
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
if (!scriptBlock) {
|
|
396
|
+
const scriptSetupBlock = `<script setup>\n${importLines.join("\n")}\n</script>\n`;
|
|
397
|
+
let insertionIndex = 0;
|
|
398
|
+
for (const match of sourceText.matchAll(ROUTE_TAG_PATTERN)) {
|
|
399
|
+
insertionIndex = match.index + String(match[0] || "").length;
|
|
400
|
+
}
|
|
401
|
+
const separator = insertionIndex > 0 ? "\n" : "";
|
|
402
|
+
return {
|
|
403
|
+
changed: true,
|
|
404
|
+
content: `${sourceText.slice(0, insertionIndex)}${separator}${scriptSetupBlock}\n${sourceText.slice(insertionIndex)}`
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let nextScriptContent = scriptBlock.content;
|
|
409
|
+
let changed = false;
|
|
410
|
+
for (const importLine of importLines) {
|
|
411
|
+
const importApplied = insertImportIfMissing(nextScriptContent, importLine);
|
|
412
|
+
nextScriptContent = importApplied.content;
|
|
413
|
+
changed = changed || importApplied.changed;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!changed) {
|
|
417
|
+
return {
|
|
418
|
+
changed: false,
|
|
419
|
+
content: sourceText
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const nextScriptTag = `<script${scriptBlock.attributesSource}>${nextScriptContent}</script>`;
|
|
424
|
+
return {
|
|
425
|
+
changed: true,
|
|
426
|
+
content: `${sourceText.slice(0, scriptBlock.index)}${nextScriptTag}${sourceText.slice(scriptBlock.index + scriptBlock.source.length)}`
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function applySubpagesUpgradeToPageSource(
|
|
431
|
+
source = "",
|
|
432
|
+
{
|
|
433
|
+
host = "",
|
|
434
|
+
position = DEFAULT_SUBPAGES_POSITION,
|
|
435
|
+
title = "",
|
|
436
|
+
subtitle = "",
|
|
437
|
+
sectionContainerComponentImportPath = "/src/components/SectionContainerShell.vue",
|
|
438
|
+
preserveExistingContent = true
|
|
439
|
+
} = {}
|
|
440
|
+
) {
|
|
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.");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const sourceText = String(source || "");
|
|
448
|
+
const templateBlock = findTemplateBlock(sourceText);
|
|
449
|
+
const existingTemplateContent = templateBlock ? templateBlock.content : "";
|
|
450
|
+
const bodyContent = preserveExistingContent
|
|
451
|
+
? stripExistingSubpagesStructure(existingTemplateContent)
|
|
452
|
+
: "";
|
|
453
|
+
const replacementTemplate = renderSubpagesTemplate({
|
|
454
|
+
bodyContent,
|
|
455
|
+
title,
|
|
456
|
+
subtitle,
|
|
457
|
+
host: normalizedHost,
|
|
458
|
+
position: normalizedPosition
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const nextSource = templateBlock
|
|
462
|
+
? `${sourceText.slice(0, templateBlock.index)}${replacementTemplate}${sourceText.slice(templateBlock.index + templateBlock.source.length)}`
|
|
463
|
+
: `${sourceText}\n${replacementTemplate}`;
|
|
464
|
+
const scriptApplied = applySubpagesScriptImports(nextSource, {
|
|
465
|
+
sectionContainerComponentImportPath
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
changed: scriptApplied.content !== sourceText,
|
|
470
|
+
content: scriptApplied.content
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function hasExistingSubpagesRouting(source = "") {
|
|
475
|
+
return ROUTER_VIEW_TAG_PATTERN.test(String(source || ""));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function upgradePageFileToSubpages({
|
|
479
|
+
appRoot,
|
|
480
|
+
targetFile,
|
|
481
|
+
host = "",
|
|
482
|
+
position = DEFAULT_SUBPAGES_POSITION,
|
|
483
|
+
title = "",
|
|
484
|
+
subtitle = "",
|
|
485
|
+
componentDirectory = DEFAULT_COMPONENT_DIRECTORY,
|
|
486
|
+
preserveExistingContent = true,
|
|
487
|
+
dryRun = false
|
|
488
|
+
} = {}) {
|
|
489
|
+
const pageTarget = await resolvePageTargetDetails({
|
|
490
|
+
appRoot,
|
|
491
|
+
targetFile,
|
|
492
|
+
context: "ui-generator add-subpages"
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
let source = "";
|
|
496
|
+
try {
|
|
497
|
+
source = await readFile(pageTarget.targetFilePath.absolutePath, "utf8");
|
|
498
|
+
} catch {
|
|
499
|
+
throw new Error(`ui-generator add-subpages target file not found: ${pageTarget.targetFilePath.relativePath}.`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (hasExistingSubpagesRouting(source)) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
`ui-generator add-subpages found existing RouterView in ${pageTarget.targetFilePath.relativePath}. Subpages are already enabled.`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const supportScaffold = await ensureSubpagesSupportScaffold({
|
|
509
|
+
appRoot,
|
|
510
|
+
componentDirectory,
|
|
511
|
+
dryRun
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const upgradeApplied = applySubpagesUpgradeToPageSource(source, {
|
|
515
|
+
host,
|
|
516
|
+
position,
|
|
517
|
+
title,
|
|
518
|
+
subtitle,
|
|
519
|
+
sectionContainerComponentImportPath: supportScaffold.sectionContainerComponentImportPath,
|
|
520
|
+
preserveExistingContent
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const touchedFiles = new Set(supportScaffold.touchedFiles);
|
|
524
|
+
if (upgradeApplied.changed) {
|
|
525
|
+
if (dryRun !== true) {
|
|
526
|
+
await writeFile(pageTarget.targetFilePath.absolutePath, upgradeApplied.content, "utf8");
|
|
527
|
+
}
|
|
528
|
+
touchedFiles.add(pageTarget.targetFilePath.relativePath);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return Object.freeze({
|
|
532
|
+
touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
|
|
533
|
+
targetFile: pageTarget.targetFilePath.relativePath,
|
|
534
|
+
surfaceId: pageTarget.surfaceId,
|
|
535
|
+
routeUrlSuffix: pageTarget.routeUrlSuffix
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export {
|
|
540
|
+
DEFAULT_SUBPAGES_POSITION,
|
|
541
|
+
DEFAULT_COMPONENT_DIRECTORY,
|
|
542
|
+
SECTION_CONTAINER_SHELL_COMPONENT,
|
|
543
|
+
TAB_LINK_COMPONENT,
|
|
544
|
+
TAB_LINK_COMPONENT_TOKEN,
|
|
545
|
+
resolvePageTargetDetails,
|
|
546
|
+
resolveNearestParentSubpagesHost,
|
|
547
|
+
deriveDefaultSubpagesHost,
|
|
548
|
+
renderPlainPageSource,
|
|
549
|
+
ensureSubpagesSupportScaffold,
|
|
550
|
+
applySubpagesUpgradeToPageSource,
|
|
551
|
+
upgradePageFileToSubpages
|
|
552
|
+
};
|