@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
|
@@ -1,644 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
resolveRequiredAppRoot,
|
|
6
|
-
resolveShellOutletPlacementTargetFromApp,
|
|
7
|
-
toPosixPath
|
|
8
|
-
} from "@jskit-ai/kernel/server/support";
|
|
9
|
-
import {
|
|
10
|
-
normalizeObject,
|
|
11
|
-
normalizeText
|
|
12
|
-
} from "@jskit-ai/kernel/shared/support/normalize";
|
|
13
|
-
import {
|
|
14
|
-
DEFAULT_COMPONENT_DIRECTORY,
|
|
15
|
-
MAIN_CLIENT_PROVIDER_FILE,
|
|
16
|
-
PLACEMENT_FILE,
|
|
17
|
-
toKebabCase,
|
|
18
|
-
requireOption,
|
|
19
|
-
resolvePathWithinApp,
|
|
20
|
-
appendBlockIfMarkerMissing,
|
|
21
|
-
insertImportIfMissing,
|
|
22
|
-
insertBeforeClassDeclaration
|
|
23
|
-
} from "./support.js";
|
|
24
|
-
|
|
25
|
-
const CONTAINER_OUTLET_POSITION = "sub-pages";
|
|
26
|
-
const SECTION_CONTAINER_SHELL_COMPONENT = "SectionContainerShell";
|
|
27
|
-
const TAB_LINK_COMPONENT = "TabLinkItem";
|
|
28
|
-
const TAB_LINK_COMPONENT_TOKEN = "local.main.ui.tab-link-item";
|
|
29
|
-
const ROUTE_TAG_PATTERN = /<route\b([^>]*)>([\s\S]*?)<\/route>/;
|
|
30
|
-
const ATTRIBUTE_PATTERN = /([:@]?[A-Za-z_][A-Za-z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
|
|
31
|
-
|
|
32
|
-
function isBracketRouteParamSegment(value = "") {
|
|
33
|
-
return value.startsWith("[") && value.endsWith("]");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function normalizeRoutePrefixSegment(value = "") {
|
|
37
|
-
const source = normalizeText(value);
|
|
38
|
-
if (!source) {
|
|
39
|
-
return "";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (isBracketRouteParamSegment(source)) {
|
|
43
|
-
return source;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return toKebabCase(source);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function normalizeRoutePrefix(value = "") {
|
|
50
|
-
const source = normalizeText(value).replaceAll("\\", "/");
|
|
51
|
-
if (!source) {
|
|
52
|
-
return "";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const parts = source
|
|
56
|
-
.split("/")
|
|
57
|
-
.map((entry) => normalizeRoutePrefixSegment(entry))
|
|
58
|
-
.filter(Boolean);
|
|
59
|
-
return parts.join("/");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function resolveContainerRoutePath({ name = "", routePath = "" } = {}) {
|
|
63
|
-
const rawRoutePath = normalizeText(routePath);
|
|
64
|
-
const normalizedRoutePath = normalizeRoutePrefix(routePath);
|
|
65
|
-
if (rawRoutePath && !normalizedRoutePath) {
|
|
66
|
-
throw new Error("ui-generator container requires a valid --route-path when provided.");
|
|
67
|
-
}
|
|
68
|
-
if (normalizedRoutePath) {
|
|
69
|
-
return normalizedRoutePath;
|
|
70
|
-
}
|
|
71
|
-
return toKebabCase(name);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function parseTagAttributes(attributesSource = "") {
|
|
75
|
-
const attributes = {};
|
|
76
|
-
const source = String(attributesSource || "");
|
|
77
|
-
for (const match of source.matchAll(ATTRIBUTE_PATTERN)) {
|
|
78
|
-
const attributeName = normalizeText(match[1]);
|
|
79
|
-
if (!attributeName) {
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const hasValue = match[2] != null || match[3] != null;
|
|
84
|
-
const attributeValue = hasValue ? String(match[2] ?? match[3] ?? "") : true;
|
|
85
|
-
attributes[attributeName] = attributeValue;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return attributes;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function createContainerRouteMeta({
|
|
92
|
-
surface = "",
|
|
93
|
-
containerHost = "",
|
|
94
|
-
containerPosition = CONTAINER_OUTLET_POSITION
|
|
95
|
-
} = {}) {
|
|
96
|
-
return {
|
|
97
|
-
meta: {
|
|
98
|
-
jskit: {
|
|
99
|
-
surface,
|
|
100
|
-
placements: {
|
|
101
|
-
outlets: [
|
|
102
|
-
{
|
|
103
|
-
host: containerHost,
|
|
104
|
-
position: containerPosition
|
|
105
|
-
}
|
|
106
|
-
]
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function renderContainerRouteMetaBlock(routeMeta = {}) {
|
|
114
|
-
return `<route lang="json">
|
|
115
|
-
${JSON.stringify(routeMeta, null, 2)}
|
|
116
|
-
</route>
|
|
117
|
-
`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function normalizeOutletTargetId(outlet = {}) {
|
|
121
|
-
const host = normalizeText(outlet?.host);
|
|
122
|
-
const position = normalizeText(outlet?.position);
|
|
123
|
-
if (!host || !position) {
|
|
124
|
-
return "";
|
|
125
|
-
}
|
|
126
|
-
return `${host}:${position}`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function ensureContainerRouteMetaOutlets(source = "", { surface = "", containerHost = "", containerPosition = "" } = {}) {
|
|
130
|
-
const sourceText = String(source || "");
|
|
131
|
-
const routeTagMatch = ROUTE_TAG_PATTERN.exec(sourceText);
|
|
132
|
-
const expectedTargetId = normalizeOutletTargetId({
|
|
133
|
-
host: containerHost,
|
|
134
|
-
position: containerPosition
|
|
135
|
-
});
|
|
136
|
-
if (!expectedTargetId) {
|
|
137
|
-
return {
|
|
138
|
-
changed: false,
|
|
139
|
-
content: sourceText
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!routeTagMatch) {
|
|
144
|
-
const appendedContent = `${sourceText.trimEnd()}\n\n${renderContainerRouteMetaBlock(createContainerRouteMeta({
|
|
145
|
-
surface,
|
|
146
|
-
containerHost,
|
|
147
|
-
containerPosition
|
|
148
|
-
}))}`;
|
|
149
|
-
return {
|
|
150
|
-
changed: appendedContent !== sourceText,
|
|
151
|
-
content: appendedContent
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const routeTagAttributes = parseTagAttributes(routeTagMatch[1]);
|
|
156
|
-
const routeTagLanguage = normalizeText(routeTagAttributes.lang).toLowerCase();
|
|
157
|
-
if (routeTagLanguage && routeTagLanguage !== "json") {
|
|
158
|
-
return {
|
|
159
|
-
changed: false,
|
|
160
|
-
content: sourceText
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
let routeMetaRecord = null;
|
|
165
|
-
try {
|
|
166
|
-
routeMetaRecord = JSON.parse(String(routeTagMatch[2] || "").trim());
|
|
167
|
-
} catch {
|
|
168
|
-
return {
|
|
169
|
-
changed: false,
|
|
170
|
-
content: sourceText
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const routeMeta = normalizeObject(routeMetaRecord);
|
|
175
|
-
const metadata = normalizeObject(routeMeta.meta);
|
|
176
|
-
const jskitMetadata = normalizeObject(metadata.jskit);
|
|
177
|
-
const placementsMetadata = normalizeObject(jskitMetadata.placements);
|
|
178
|
-
const outletRecords = Array.isArray(placementsMetadata.outlets) ? [...placementsMetadata.outlets] : [];
|
|
179
|
-
const knownTargetIds = new Set(outletRecords.map((entry) => normalizeOutletTargetId(entry)).filter(Boolean));
|
|
180
|
-
if (!knownTargetIds.has(expectedTargetId)) {
|
|
181
|
-
outletRecords.push({
|
|
182
|
-
host: containerHost,
|
|
183
|
-
position: containerPosition
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const normalizedSurface = normalizeText(jskitMetadata.surface) || normalizeText(surface);
|
|
188
|
-
const nextRouteMeta = {
|
|
189
|
-
...routeMeta,
|
|
190
|
-
meta: {
|
|
191
|
-
...metadata,
|
|
192
|
-
jskit: {
|
|
193
|
-
...jskitMetadata,
|
|
194
|
-
surface: normalizedSurface,
|
|
195
|
-
placements: {
|
|
196
|
-
...placementsMetadata,
|
|
197
|
-
outlets: outletRecords
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
const renderedRouteMeta = renderContainerRouteMetaBlock(nextRouteMeta);
|
|
203
|
-
const replacementContent =
|
|
204
|
-
`${sourceText.slice(0, routeTagMatch.index)}${renderedRouteMeta}${sourceText.slice(routeTagMatch.index + routeTagMatch[0].length)}`;
|
|
205
|
-
return {
|
|
206
|
-
changed: replacementContent !== sourceText,
|
|
207
|
-
content: replacementContent
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function loadPublicConfig(appRoot = "") {
|
|
212
|
-
const configPath = path.join(appRoot, "config", "public.js");
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
await readFile(configPath, "utf8");
|
|
216
|
-
} catch {
|
|
217
|
-
throw new Error("ui-generator container requires app config at config/public.js.");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
let moduleNamespace = null;
|
|
221
|
-
try {
|
|
222
|
-
moduleNamespace = await import(`${pathToFileURL(configPath).href}?t=${Date.now()}_${Math.random()}`);
|
|
223
|
-
} catch (error) {
|
|
224
|
-
throw new Error(
|
|
225
|
-
`ui-generator container could not load config/public.js: ${String(error?.message || error || "unknown error")}`
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const config = normalizeObject(
|
|
230
|
-
moduleNamespace?.config ||
|
|
231
|
-
moduleNamespace?.default?.config ||
|
|
232
|
-
moduleNamespace?.default
|
|
233
|
-
);
|
|
234
|
-
if (Object.keys(config).length < 1) {
|
|
235
|
-
throw new Error("ui-generator container requires exported config in config/public.js.");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return config;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function resolveSurfacePagesDirectory(appRoot = "", surfaceId = "") {
|
|
242
|
-
const config = await loadPublicConfig(appRoot);
|
|
243
|
-
const surfaceDefinitions = normalizeObject(config.surfaceDefinitions);
|
|
244
|
-
const normalizedSurfaceId = normalizeText(surfaceId).toLowerCase();
|
|
245
|
-
const surfaceDefinition = normalizeObject(surfaceDefinitions[normalizedSurfaceId]);
|
|
246
|
-
if (Object.keys(surfaceDefinition).length < 1) {
|
|
247
|
-
throw new Error(`ui-generator container surface "${normalizedSurfaceId}" is not defined in config/public.js.`);
|
|
248
|
-
}
|
|
249
|
-
if (surfaceDefinition.enabled === false) {
|
|
250
|
-
throw new Error(`ui-generator container surface "${normalizedSurfaceId}" is disabled in config/public.js.`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const pagesRoot = normalizeText(surfaceDefinition.pagesRoot);
|
|
254
|
-
if (!pagesRoot) {
|
|
255
|
-
return path.join(appRoot, "src", "pages");
|
|
256
|
-
}
|
|
257
|
-
return path.join(appRoot, "src", "pages", pagesRoot);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function renderSectionContainerShellSource() {
|
|
261
|
-
return `<script setup>
|
|
262
|
-
import { computed } from "vue";
|
|
263
|
-
import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
|
|
264
|
-
|
|
265
|
-
const props = defineProps({
|
|
266
|
-
title: {
|
|
267
|
-
type: String,
|
|
268
|
-
default: ""
|
|
269
|
-
},
|
|
270
|
-
subtitle: {
|
|
271
|
-
type: String,
|
|
272
|
-
default: ""
|
|
273
|
-
},
|
|
274
|
-
host: {
|
|
275
|
-
type: String,
|
|
276
|
-
default: ""
|
|
277
|
-
},
|
|
278
|
-
position: {
|
|
279
|
-
type: String,
|
|
280
|
-
default: "${CONTAINER_OUTLET_POSITION}"
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
const resolvedTitle = computed(() => String(props.title || "").trim() || "Section");
|
|
285
|
-
const resolvedSubtitle = computed(() => String(props.subtitle || "").trim());
|
|
286
|
-
</script>
|
|
287
|
-
|
|
288
|
-
<template>
|
|
289
|
-
<section class="section-container-shell d-flex flex-column ga-4">
|
|
290
|
-
<v-card rounded="lg" elevation="1" border>
|
|
291
|
-
<v-card-item>
|
|
292
|
-
<v-card-title class="px-0">{{ resolvedTitle }}</v-card-title>
|
|
293
|
-
<v-card-subtitle v-if="resolvedSubtitle" class="px-0">{{ resolvedSubtitle }}</v-card-subtitle>
|
|
294
|
-
</v-card-item>
|
|
295
|
-
<v-divider />
|
|
296
|
-
<v-card-text class="section-container-shell__tabs">
|
|
297
|
-
<ShellOutlet :host="props.host" :position="props.position" />
|
|
298
|
-
</v-card-text>
|
|
299
|
-
</v-card>
|
|
300
|
-
|
|
301
|
-
<slot />
|
|
302
|
-
</section>
|
|
303
|
-
</template>
|
|
304
|
-
|
|
305
|
-
<style scoped>
|
|
306
|
-
.section-container-shell__tabs {
|
|
307
|
-
display: flex;
|
|
308
|
-
align-items: center;
|
|
309
|
-
gap: 0.5rem;
|
|
310
|
-
overflow-x: auto;
|
|
311
|
-
padding: 0.75rem;
|
|
312
|
-
scrollbar-width: thin;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
.section-container-shell__tabs :deep(.tab-link-item) {
|
|
316
|
-
flex: 0 0 auto;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
@media (max-width: 640px) {
|
|
320
|
-
.section-container-shell__tabs {
|
|
321
|
-
gap: 0.375rem;
|
|
322
|
-
padding: 0.5rem;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
</style>
|
|
326
|
-
`;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function renderTabLinkItemSource() {
|
|
330
|
-
return `<script setup>
|
|
331
|
-
import { computed } from "vue";
|
|
332
|
-
import { useRoute } from "vue-router";
|
|
333
|
-
import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
|
|
334
|
-
import {
|
|
335
|
-
normalizeMenuLinkPathname,
|
|
336
|
-
resolveMenuLinkTarget
|
|
337
|
-
} from "@jskit-ai/users-web/client/support/menuLinkTarget";
|
|
338
|
-
|
|
339
|
-
const props = defineProps({
|
|
340
|
-
label: {
|
|
341
|
-
type: String,
|
|
342
|
-
default: ""
|
|
343
|
-
},
|
|
344
|
-
to: {
|
|
345
|
-
type: String,
|
|
346
|
-
default: ""
|
|
347
|
-
},
|
|
348
|
-
surface: {
|
|
349
|
-
type: String,
|
|
350
|
-
default: ""
|
|
351
|
-
},
|
|
352
|
-
workspaceSuffix: {
|
|
353
|
-
type: String,
|
|
354
|
-
default: "/"
|
|
355
|
-
},
|
|
356
|
-
nonWorkspaceSuffix: {
|
|
357
|
-
type: String,
|
|
358
|
-
default: "/"
|
|
359
|
-
},
|
|
360
|
-
disabled: {
|
|
361
|
-
type: Boolean,
|
|
362
|
-
default: false
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
const route = useRoute();
|
|
367
|
-
const paths = usePaths();
|
|
368
|
-
|
|
369
|
-
const resolvedTo = computed(() => {
|
|
370
|
-
return resolveMenuLinkTarget({
|
|
371
|
-
to: props.to,
|
|
372
|
-
surface: props.surface,
|
|
373
|
-
currentSurfaceId: paths.currentSurfaceId.value,
|
|
374
|
-
placementContext: paths.placementContext.value,
|
|
375
|
-
workspaceSuffix: props.workspaceSuffix,
|
|
376
|
-
nonWorkspaceSuffix: props.nonWorkspaceSuffix,
|
|
377
|
-
routeParams: route.params || {},
|
|
378
|
-
resolvePagePath(relativePath, options = {}) {
|
|
379
|
-
return paths.page(relativePath, options);
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
const isActive = computed(() => {
|
|
385
|
-
const targetPathname = normalizeMenuLinkPathname(resolvedTo.value);
|
|
386
|
-
const currentPathname = normalizeMenuLinkPathname(route.fullPath || route.path);
|
|
387
|
-
if (!targetPathname || !currentPathname) {
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
return currentPathname === targetPathname || currentPathname.startsWith(\`\${targetPathname}/\`);
|
|
391
|
-
});
|
|
392
|
-
</script>
|
|
393
|
-
|
|
394
|
-
<template>
|
|
395
|
-
<v-btn
|
|
396
|
-
v-if="resolvedTo"
|
|
397
|
-
class="tab-link-item text-none"
|
|
398
|
-
:to="resolvedTo"
|
|
399
|
-
rounded="pill"
|
|
400
|
-
size="small"
|
|
401
|
-
:variant="isActive ? 'flat' : 'tonal'"
|
|
402
|
-
:color="isActive ? 'primary' : undefined"
|
|
403
|
-
:disabled="props.disabled"
|
|
404
|
-
:aria-current="isActive ? 'page' : undefined"
|
|
405
|
-
>
|
|
406
|
-
{{ props.label }}
|
|
407
|
-
</v-btn>
|
|
408
|
-
</template>
|
|
409
|
-
`;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function renderContainerPageSource({
|
|
413
|
-
surface = "",
|
|
414
|
-
title = "",
|
|
415
|
-
subtitle = "",
|
|
416
|
-
containerHost = "",
|
|
417
|
-
containerPosition = CONTAINER_OUTLET_POSITION,
|
|
418
|
-
sectionContainerComponentImportPath = "/src/components/SectionContainerShell.vue"
|
|
419
|
-
} = {}) {
|
|
420
|
-
const routeMeta = createContainerRouteMeta({
|
|
421
|
-
surface,
|
|
422
|
-
containerHost,
|
|
423
|
-
containerPosition
|
|
424
|
-
});
|
|
425
|
-
return `<script setup>
|
|
426
|
-
import { RouterView } from "vue-router";
|
|
427
|
-
import SectionContainerShell from "${sectionContainerComponentImportPath}";
|
|
428
|
-
</script>
|
|
429
|
-
|
|
430
|
-
<template>
|
|
431
|
-
<SectionContainerShell
|
|
432
|
-
title="${title}"
|
|
433
|
-
subtitle="${subtitle}"
|
|
434
|
-
host="${containerHost}"
|
|
435
|
-
position="${containerPosition}"
|
|
436
|
-
>
|
|
437
|
-
<RouterView />
|
|
438
|
-
</SectionContainerShell>
|
|
439
|
-
</template>
|
|
440
|
-
|
|
441
|
-
${renderContainerRouteMetaBlock(routeMeta)}
|
|
442
|
-
`;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async function runGeneratorSubcommand({
|
|
446
|
-
appRoot,
|
|
447
|
-
subcommand = "",
|
|
448
|
-
args = [],
|
|
449
|
-
options = {},
|
|
450
|
-
dryRun = false
|
|
451
|
-
} = {}) {
|
|
452
|
-
const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
|
|
453
|
-
if (normalizedSubcommand !== "container") {
|
|
454
|
-
throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
455
|
-
}
|
|
456
|
-
if (Array.isArray(args) && args.length > 0) {
|
|
457
|
-
throw new Error("ui-generator container does not accept positional arguments.");
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
|
|
461
|
-
context: "ui-generator container"
|
|
462
|
-
});
|
|
463
|
-
const name = requireOption(options, "name", { context: "ui-generator container" });
|
|
464
|
-
const surface = requireOption(options, "surface", { context: "ui-generator container" }).toLowerCase();
|
|
465
|
-
const routePrefix = normalizeRoutePrefix(options?.["directory-prefix"]);
|
|
466
|
-
const componentDirectory = normalizeText(options?.path) || DEFAULT_COMPONENT_DIRECTORY;
|
|
467
|
-
const containerRoutePath = resolveContainerRoutePath({
|
|
468
|
-
name,
|
|
469
|
-
routePath: options?.["route-path"]
|
|
470
|
-
});
|
|
471
|
-
const containerSlug = toKebabCase(name);
|
|
472
|
-
if (!containerSlug || !containerRoutePath) {
|
|
473
|
-
throw new Error("ui-generator container requires a valid --name.");
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const routePath = routePrefix ? `${routePrefix}/${containerRoutePath}` : containerRoutePath;
|
|
477
|
-
const pagesDirectory = await resolveSurfacePagesDirectory(resolvedAppRoot, surface);
|
|
478
|
-
const containerFilePath = path.join(pagesDirectory, `${routePath}.vue`);
|
|
479
|
-
const containerRelativePath = toPosixPath(path.relative(resolvedAppRoot, containerFilePath));
|
|
480
|
-
const providerPath = resolvePathWithinApp(resolvedAppRoot, MAIN_CLIENT_PROVIDER_FILE, {
|
|
481
|
-
context: "ui-generator container"
|
|
482
|
-
});
|
|
483
|
-
const sectionContainerShellPath = resolvePathWithinApp(
|
|
484
|
-
resolvedAppRoot,
|
|
485
|
-
path.join(componentDirectory, `${SECTION_CONTAINER_SHELL_COMPONENT}.vue`),
|
|
486
|
-
{
|
|
487
|
-
context: "ui-generator container"
|
|
488
|
-
}
|
|
489
|
-
);
|
|
490
|
-
const sectionTabLinkPath = resolvePathWithinApp(
|
|
491
|
-
resolvedAppRoot,
|
|
492
|
-
path.join(componentDirectory, `${TAB_LINK_COMPONENT}.vue`),
|
|
493
|
-
{
|
|
494
|
-
context: "ui-generator container"
|
|
495
|
-
}
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
const placementOption = normalizeText(options?.placement);
|
|
499
|
-
const placementTarget = placementOption
|
|
500
|
-
? await resolveShellOutletPlacementTargetFromApp({
|
|
501
|
-
appRoot: resolvedAppRoot,
|
|
502
|
-
context: "ui-generator container",
|
|
503
|
-
placement: placementOption
|
|
504
|
-
})
|
|
505
|
-
: null;
|
|
506
|
-
|
|
507
|
-
const touchedFiles = new Set();
|
|
508
|
-
|
|
509
|
-
let existingSectionContainerSource = "";
|
|
510
|
-
try {
|
|
511
|
-
existingSectionContainerSource = await readFile(sectionContainerShellPath.absolutePath, "utf8");
|
|
512
|
-
} catch {
|
|
513
|
-
existingSectionContainerSource = "";
|
|
514
|
-
}
|
|
515
|
-
if (!existingSectionContainerSource) {
|
|
516
|
-
if (dryRun !== true) {
|
|
517
|
-
await mkdir(path.dirname(sectionContainerShellPath.absolutePath), { recursive: true });
|
|
518
|
-
await writeFile(sectionContainerShellPath.absolutePath, renderSectionContainerShellSource(), "utf8");
|
|
519
|
-
}
|
|
520
|
-
touchedFiles.add(sectionContainerShellPath.relativePath);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
let existingSectionTabLinkSource = "";
|
|
524
|
-
try {
|
|
525
|
-
existingSectionTabLinkSource = await readFile(sectionTabLinkPath.absolutePath, "utf8");
|
|
526
|
-
} catch {
|
|
527
|
-
existingSectionTabLinkSource = "";
|
|
528
|
-
}
|
|
529
|
-
if (!existingSectionTabLinkSource) {
|
|
530
|
-
if (dryRun !== true) {
|
|
531
|
-
await mkdir(path.dirname(sectionTabLinkPath.absolutePath), { recursive: true });
|
|
532
|
-
await writeFile(sectionTabLinkPath.absolutePath, renderTabLinkItemSource(), "utf8");
|
|
533
|
-
}
|
|
534
|
-
touchedFiles.add(sectionTabLinkPath.relativePath);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const providerSource = await readFile(providerPath.absolutePath, "utf8");
|
|
538
|
-
if (!/\bregisterMainClientComponent\s*\(/.test(providerSource)) {
|
|
539
|
-
throw new Error(
|
|
540
|
-
`ui-generator container could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const providerImportLine = `import ${TAB_LINK_COMPONENT} from "/${toPosixPath(path.join(componentDirectory, `${TAB_LINK_COMPONENT}.vue`))}";`;
|
|
545
|
-
const providerRegisterLine =
|
|
546
|
-
`registerMainClientComponent("${TAB_LINK_COMPONENT_TOKEN}", () => ${TAB_LINK_COMPONENT});`;
|
|
547
|
-
const providerImportApplied = insertImportIfMissing(providerSource, providerImportLine);
|
|
548
|
-
const providerRegisterApplied = insertBeforeClassDeclaration(
|
|
549
|
-
providerImportApplied.content,
|
|
550
|
-
providerRegisterLine,
|
|
551
|
-
{
|
|
552
|
-
className: "MainClientProvider",
|
|
553
|
-
contextFile: MAIN_CLIENT_PROVIDER_FILE
|
|
554
|
-
}
|
|
555
|
-
);
|
|
556
|
-
if (providerImportApplied.changed || providerRegisterApplied.changed) {
|
|
557
|
-
if (dryRun !== true) {
|
|
558
|
-
await writeFile(providerPath.absolutePath, providerRegisterApplied.content, "utf8");
|
|
559
|
-
}
|
|
560
|
-
touchedFiles.add(providerPath.relativePath);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
let existingContainerSource = "";
|
|
564
|
-
try {
|
|
565
|
-
existingContainerSource = await readFile(containerFilePath, "utf8");
|
|
566
|
-
} catch {
|
|
567
|
-
existingContainerSource = "";
|
|
568
|
-
}
|
|
569
|
-
if (!existingContainerSource) {
|
|
570
|
-
if (dryRun !== true) {
|
|
571
|
-
await mkdir(path.dirname(containerFilePath), { recursive: true });
|
|
572
|
-
await writeFile(
|
|
573
|
-
containerFilePath,
|
|
574
|
-
renderContainerPageSource({
|
|
575
|
-
surface,
|
|
576
|
-
title: name,
|
|
577
|
-
subtitle: `Manage ${toKebabCase(name).replaceAll("-", " ")} modules.`,
|
|
578
|
-
containerHost: containerSlug,
|
|
579
|
-
sectionContainerComponentImportPath: `/${toPosixPath(path.join(componentDirectory, `${SECTION_CONTAINER_SHELL_COMPONENT}.vue`))}`
|
|
580
|
-
}),
|
|
581
|
-
"utf8"
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
touchedFiles.add(containerRelativePath);
|
|
585
|
-
} else {
|
|
586
|
-
const routeMetaApplied = ensureContainerRouteMetaOutlets(existingContainerSource, {
|
|
587
|
-
surface,
|
|
588
|
-
containerHost: containerSlug,
|
|
589
|
-
containerPosition: CONTAINER_OUTLET_POSITION
|
|
590
|
-
});
|
|
591
|
-
if (routeMetaApplied.changed) {
|
|
592
|
-
if (dryRun !== true) {
|
|
593
|
-
await writeFile(containerFilePath, routeMetaApplied.content, "utf8");
|
|
594
|
-
}
|
|
595
|
-
touchedFiles.add(containerRelativePath);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (placementTarget) {
|
|
600
|
-
const placementPath = resolvePathWithinApp(resolvedAppRoot, PLACEMENT_FILE, {
|
|
601
|
-
context: "ui-generator container"
|
|
602
|
-
});
|
|
603
|
-
const placementSource = await readFile(placementPath.absolutePath, "utf8");
|
|
604
|
-
const placementIdSuffix = routePath.replaceAll("/", "-");
|
|
605
|
-
const placementMarker = `jskit:ui-generator.container.menu:${surface}:${routePath}`;
|
|
606
|
-
const placementBlock =
|
|
607
|
-
`// ${placementMarker}\n` +
|
|
608
|
-
"{\n" +
|
|
609
|
-
" addPlacement({\n" +
|
|
610
|
-
` id: "ui-generator.container.${placementIdSuffix}.menu",\n` +
|
|
611
|
-
` host: "${placementTarget.host}",\n` +
|
|
612
|
-
` position: "${placementTarget.position}",\n` +
|
|
613
|
-
` surfaces: ["${surface}"],\n` +
|
|
614
|
-
" order: 155,\n" +
|
|
615
|
-
' componentToken: "users.web.shell.surface-aware-menu-link-item",\n' +
|
|
616
|
-
" props: {\n" +
|
|
617
|
-
` label: "${name}",\n` +
|
|
618
|
-
` surface: "${surface}",\n` +
|
|
619
|
-
` workspaceSuffix: "/${routePath}",\n` +
|
|
620
|
-
` nonWorkspaceSuffix: "/${routePath}"\n` +
|
|
621
|
-
" },\n" +
|
|
622
|
-
" when: ({ auth }) => Boolean(auth?.authenticated)\n" +
|
|
623
|
-
" });\n" +
|
|
624
|
-
"}\n";
|
|
625
|
-
const placementApplied = appendBlockIfMarkerMissing(placementSource, placementMarker, placementBlock);
|
|
626
|
-
if (placementApplied.changed) {
|
|
627
|
-
if (dryRun !== true) {
|
|
628
|
-
await writeFile(placementPath.absolutePath, placementApplied.content, "utf8");
|
|
629
|
-
}
|
|
630
|
-
touchedFiles.add(placementPath.relativePath);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const touchedFileList = [...touchedFiles].sort((left, right) => left.localeCompare(right));
|
|
635
|
-
return {
|
|
636
|
-
touchedFiles: touchedFileList,
|
|
637
|
-
summary:
|
|
638
|
-
touchedFileList.length > 0
|
|
639
|
-
? `Generated UI container "${routePath}" with outlet "${containerSlug}:${CONTAINER_OUTLET_POSITION}".`
|
|
640
|
-
: `UI container "${routePath}" is already up to date.`
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
export { runGeneratorSubcommand };
|