@jskit-ai/ui-generator 0.1.14 → 0.1.16
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 +31 -126
- package/src/server/subcommands/page.js +134 -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 +118 -29
- package/test/packageDescriptor.test.js +10 -0
- package/test/pageSubcommand.test.js +415 -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
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
resolveShellOutletPlacementTargetFromApp,
|
|
5
5
|
toPosixPath
|
|
6
6
|
} from "@jskit-ai/kernel/server/support";
|
|
7
|
-
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
|
+
import { normalizeBoolean, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_COMPONENT_DIRECTORY,
|
|
10
10
|
MAIN_CLIENT_PROVIDER_FILE,
|
|
@@ -12,12 +12,15 @@ import {
|
|
|
12
12
|
toKebabCase,
|
|
13
13
|
toPascalCase,
|
|
14
14
|
requireOption,
|
|
15
|
+
rejectUnexpectedOptions,
|
|
15
16
|
resolvePathWithinApp,
|
|
16
17
|
appendBlockIfMarkerMissing,
|
|
17
18
|
insertImportIfMissing,
|
|
18
19
|
insertBeforeClassDeclaration
|
|
19
20
|
} from "./support.js";
|
|
20
21
|
|
|
22
|
+
const DEFAULT_ELEMENT_PLACEMENT = "shell-layout:top-right";
|
|
23
|
+
|
|
21
24
|
function renderElementComponentSource(elementName = "") {
|
|
22
25
|
return `<template>
|
|
23
26
|
<section class="pa-4">
|
|
@@ -36,40 +39,47 @@ async function runGeneratorSubcommand({
|
|
|
36
39
|
dryRun = false
|
|
37
40
|
} = {}) {
|
|
38
41
|
const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
|
|
39
|
-
if (normalizedSubcommand !== "element") {
|
|
42
|
+
if (normalizedSubcommand !== "placed-element") {
|
|
40
43
|
throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
41
44
|
}
|
|
42
45
|
if (Array.isArray(args) && args.length > 0) {
|
|
43
|
-
throw new Error("ui-generator element does not accept positional arguments.");
|
|
46
|
+
throw new Error("ui-generator placed-element does not accept positional arguments.");
|
|
44
47
|
}
|
|
48
|
+
rejectUnexpectedOptions(options, ["name", "surface", "path", "placement", "force"], {
|
|
49
|
+
context: "ui-generator placed-element"
|
|
50
|
+
});
|
|
45
51
|
|
|
46
|
-
const name = requireOption(options, "name", { context: "ui-generator element" });
|
|
47
|
-
const surface = requireOption(options, "surface", { context: "ui-generator element" }).toLowerCase();
|
|
52
|
+
const name = requireOption(options, "name", { context: "ui-generator placed-element" });
|
|
53
|
+
const surface = requireOption(options, "surface", { context: "ui-generator placed-element" }).toLowerCase();
|
|
48
54
|
const componentDirectory = normalizeText(options.path) || DEFAULT_COMPONENT_DIRECTORY;
|
|
55
|
+
const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
|
|
56
|
+
? normalizeBoolean(options.force)
|
|
57
|
+
: false;
|
|
49
58
|
const elementNamePascal = toPascalCase(name);
|
|
50
59
|
const elementNameKebab = toKebabCase(name);
|
|
51
60
|
|
|
52
61
|
if (!elementNamePascal || !elementNameKebab) {
|
|
53
|
-
throw new Error("ui-generator element requires a valid --name.");
|
|
62
|
+
throw new Error("ui-generator placed-element requires a valid --name.");
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
const componentPath = resolvePathWithinApp(appRoot, path.join(componentDirectory, `${elementNamePascal}Element.vue`), {
|
|
57
|
-
context: "ui-generator element"
|
|
66
|
+
context: "ui-generator placed-element"
|
|
58
67
|
});
|
|
59
68
|
const providerPath = resolvePathWithinApp(appRoot, MAIN_CLIENT_PROVIDER_FILE, {
|
|
60
|
-
context: "ui-generator element"
|
|
69
|
+
context: "ui-generator placed-element"
|
|
61
70
|
});
|
|
62
71
|
const placementPath = resolvePathWithinApp(appRoot, PLACEMENT_FILE, {
|
|
63
|
-
context: "ui-generator element"
|
|
72
|
+
context: "ui-generator placed-element"
|
|
64
73
|
});
|
|
65
74
|
const componentToken = `local.main.ui.element.${elementNameKebab}`;
|
|
66
75
|
const placementTarget = await resolveShellOutletPlacementTargetFromApp({
|
|
67
76
|
appRoot,
|
|
68
77
|
context: "ui-generator",
|
|
69
|
-
placement: options?.placement
|
|
78
|
+
placement: options?.placement || DEFAULT_ELEMENT_PLACEMENT
|
|
70
79
|
});
|
|
71
80
|
|
|
72
81
|
const touchedFiles = new Set();
|
|
82
|
+
const desiredComponentSource = renderElementComponentSource(name);
|
|
73
83
|
|
|
74
84
|
let componentSource = "";
|
|
75
85
|
try {
|
|
@@ -77,10 +87,15 @@ async function runGeneratorSubcommand({
|
|
|
77
87
|
} catch {
|
|
78
88
|
componentSource = "";
|
|
79
89
|
}
|
|
80
|
-
if (!
|
|
90
|
+
if (componentSource && !forceOverwrite) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`ui-generator placed-element will not overwrite existing component file ${componentPath.relativePath}. Re-run with --force to overwrite it.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!componentSource || (forceOverwrite && componentSource !== desiredComponentSource)) {
|
|
81
96
|
if (dryRun !== true) {
|
|
82
97
|
await mkdir(path.dirname(componentPath.absolutePath), { recursive: true });
|
|
83
|
-
await writeFile(componentPath.absolutePath,
|
|
98
|
+
await writeFile(componentPath.absolutePath, desiredComponentSource, "utf8");
|
|
84
99
|
}
|
|
85
100
|
touchedFiles.add(componentPath.relativePath);
|
|
86
101
|
}
|
|
@@ -88,7 +103,7 @@ async function runGeneratorSubcommand({
|
|
|
88
103
|
const providerSource = await readFile(providerPath.absolutePath, "utf8");
|
|
89
104
|
if (!/\bregisterMainClientComponent\s*\(/.test(providerSource)) {
|
|
90
105
|
throw new Error(
|
|
91
|
-
`ui-generator element could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
|
|
106
|
+
`ui-generator placed-element could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
|
|
92
107
|
);
|
|
93
108
|
}
|
|
94
109
|
|
|
@@ -140,8 +155,8 @@ async function runGeneratorSubcommand({
|
|
|
140
155
|
touchedFiles: touchedFileList,
|
|
141
156
|
summary:
|
|
142
157
|
touchedFileList.length > 0
|
|
143
|
-
? `Generated UI element "${elementNameKebab}" and placement token "${componentToken}".`
|
|
144
|
-
: `UI element "${elementNameKebab}" is already up to date.`
|
|
158
|
+
? `Generated placed UI element "${elementNameKebab}" and placement token "${componentToken}".`
|
|
159
|
+
: `Placed UI element "${elementNameKebab}" is already up to date.`
|
|
145
160
|
};
|
|
146
161
|
}
|
|
147
162
|
|
|
@@ -1,91 +1,22 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
requireSinglePositionalTargetFile,
|
|
5
|
+
rejectUnexpectedOptions,
|
|
6
|
+
resolveOutletTargetId,
|
|
5
7
|
resolvePathWithinApp,
|
|
6
8
|
ensureTrailingNewline,
|
|
7
|
-
insertImportIfMissing
|
|
9
|
+
insertImportIfMissing,
|
|
10
|
+
findScriptBlock,
|
|
11
|
+
parseTagAttributes,
|
|
12
|
+
indentBlock
|
|
8
13
|
} from "./support.js";
|
|
9
14
|
|
|
10
15
|
const DEFAULT_OUTLET_POSITION = "sub-pages";
|
|
11
|
-
const MODE_ROUTED = "routed";
|
|
12
|
-
const MODE_OUTLET_ONLY = "outlet-only";
|
|
13
16
|
|
|
14
|
-
const SCRIPT_TAG_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
15
17
|
const ROUTE_TAG_PATTERN = /<route\b[^>]*>[\s\S]*?<\/route>\s*/gi;
|
|
16
18
|
const TEMPLATE_CLOSE_TAG_PATTERN = /<\/template>/gi;
|
|
17
|
-
const ROUTER_VIEW_TAG_PATTERN = /<RouterView(?:\s|\/|>)/;
|
|
18
19
|
const SHELL_OUTLET_TAG_PATTERN = /<ShellOutlet\b([^>]*)\/?>/gi;
|
|
19
|
-
const ATTRIBUTE_PATTERN = /([:@]?[A-Za-z_][A-Za-z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
|
|
20
|
-
const SCRIPT_SETUP_ATTRIBUTE_PATTERN = /\bsetup\b/i;
|
|
21
|
-
|
|
22
|
-
function resolveOutletMode(rawMode = "") {
|
|
23
|
-
const normalized = String(rawMode || "").trim().toLowerCase();
|
|
24
|
-
if (!normalized || normalized === MODE_ROUTED) {
|
|
25
|
-
return MODE_ROUTED;
|
|
26
|
-
}
|
|
27
|
-
if (normalized === MODE_OUTLET_ONLY || normalized === "outlet") {
|
|
28
|
-
return MODE_OUTLET_ONLY;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
throw new Error(`ui-generator outlet received unsupported --mode value: ${rawMode}. Use routed or outlet-only.`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function findScriptBlock(source = "") {
|
|
35
|
-
const sourceText = String(source || "");
|
|
36
|
-
let firstMatch = null;
|
|
37
|
-
|
|
38
|
-
for (const match of sourceText.matchAll(SCRIPT_TAG_PATTERN)) {
|
|
39
|
-
if (!firstMatch) {
|
|
40
|
-
firstMatch = match;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const attributesSource = String(match[1] || "");
|
|
44
|
-
if (SCRIPT_SETUP_ATTRIBUTE_PATTERN.test(attributesSource)) {
|
|
45
|
-
return Object.freeze({
|
|
46
|
-
index: match.index,
|
|
47
|
-
source: String(match[0] || ""),
|
|
48
|
-
attributesSource,
|
|
49
|
-
content: String(match[2] || "")
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!firstMatch) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return Object.freeze({
|
|
59
|
-
index: firstMatch.index,
|
|
60
|
-
source: String(firstMatch[0] || ""),
|
|
61
|
-
attributesSource: String(firstMatch[1] || ""),
|
|
62
|
-
content: String(firstMatch[2] || "")
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function hasImportFromModule(source = "", modulePath = "") {
|
|
67
|
-
const normalizedModulePath = String(modulePath || "").trim();
|
|
68
|
-
if (!normalizedModulePath) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const sourceText = String(source || "");
|
|
73
|
-
return sourceText.includes(`from "${normalizedModulePath}"`) || sourceText.includes(`from '${normalizedModulePath}'`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function parseTagAttributes(attributesSource = "") {
|
|
77
|
-
const attributes = {};
|
|
78
|
-
const source = String(attributesSource || "");
|
|
79
|
-
for (const match of source.matchAll(ATTRIBUTE_PATTERN)) {
|
|
80
|
-
const attributeName = normalizeText(match[1]);
|
|
81
|
-
if (!attributeName) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const hasValue = match[2] != null || match[3] != null;
|
|
85
|
-
attributes[attributeName] = hasValue ? String(match[2] ?? match[3] ?? "") : true;
|
|
86
|
-
}
|
|
87
|
-
return attributes;
|
|
88
|
-
}
|
|
89
20
|
|
|
90
21
|
function hasShellOutletTarget(source = "", { host = "", position = "" } = {}) {
|
|
91
22
|
const normalizedHost = normalizeText(host);
|
|
@@ -106,19 +37,14 @@ function hasShellOutletTarget(source = "", { host = "", position = "" } = {}) {
|
|
|
106
37
|
return false;
|
|
107
38
|
}
|
|
108
39
|
|
|
109
|
-
function applyScriptImports(source = ""
|
|
40
|
+
function applyScriptImports(source = "") {
|
|
110
41
|
const sourceText = String(source || "");
|
|
111
42
|
const scriptBlock = findScriptBlock(sourceText);
|
|
112
43
|
|
|
113
44
|
const shellOutletImport = "import ShellOutlet from \"@jskit-ai/shell-web/client/components/ShellOutlet\";";
|
|
114
|
-
const routerViewImport = "import { RouterView } from \"vue-router\";";
|
|
115
45
|
|
|
116
46
|
if (!scriptBlock) {
|
|
117
|
-
const
|
|
118
|
-
if (includeRouterViewImport) {
|
|
119
|
-
importLines.push(routerViewImport);
|
|
120
|
-
}
|
|
121
|
-
const scriptSetupBlock = `<script setup>\n${importLines.join("\n")}\n</script>\n`;
|
|
47
|
+
const scriptSetupBlock = `<script setup>\n${shellOutletImport}\n</script>\n`;
|
|
122
48
|
let insertionIndex = 0;
|
|
123
49
|
for (const match of sourceText.matchAll(ROUTE_TAG_PATTERN)) {
|
|
124
50
|
insertionIndex = match.index + String(match[0] || "").length;
|
|
@@ -133,15 +59,7 @@ function applyScriptImports(source = "", { includeRouterViewImport = false } = {
|
|
|
133
59
|
let nextScriptContent = scriptBlock.content;
|
|
134
60
|
const shellImportApplied = insertImportIfMissing(nextScriptContent, shellOutletImport);
|
|
135
61
|
nextScriptContent = shellImportApplied.content;
|
|
136
|
-
|
|
137
|
-
let routerImportChanged = false;
|
|
138
|
-
if (includeRouterViewImport && !hasImportFromModule(nextScriptContent, "vue-router")) {
|
|
139
|
-
const routerImportApplied = insertImportIfMissing(nextScriptContent, routerViewImport);
|
|
140
|
-
nextScriptContent = routerImportApplied.content;
|
|
141
|
-
routerImportChanged = routerImportApplied.changed;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!shellImportApplied.changed && !routerImportChanged) {
|
|
62
|
+
if (!shellImportApplied.changed) {
|
|
145
63
|
return {
|
|
146
64
|
changed: false,
|
|
147
65
|
content: sourceText
|
|
@@ -157,23 +75,8 @@ function applyScriptImports(source = "", { includeRouterViewImport = false } = {
|
|
|
157
75
|
};
|
|
158
76
|
}
|
|
159
77
|
|
|
160
|
-
function createOutletBlock({ host = "", position = ""
|
|
161
|
-
|
|
162
|
-
`<ShellOutlet host=\"${host}\" position=\"${position}\" />`
|
|
163
|
-
];
|
|
164
|
-
if (includeRouterView) {
|
|
165
|
-
lines.push("<RouterView />");
|
|
166
|
-
}
|
|
167
|
-
return lines.join("\n");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function indentBlock(source = "", indent = "") {
|
|
171
|
-
const sourceText = String(source || "");
|
|
172
|
-
const indentation = String(indent || "");
|
|
173
|
-
return sourceText
|
|
174
|
-
.split("\n")
|
|
175
|
-
.map((line) => `${indentation}${line}`)
|
|
176
|
-
.join("\n");
|
|
78
|
+
function createOutletBlock({ host = "", position = "" } = {}) {
|
|
79
|
+
return `<ShellOutlet host=\"${host}\" position=\"${position}\" />`;
|
|
177
80
|
}
|
|
178
81
|
|
|
179
82
|
function findLastTemplateCloseTag(source = "") {
|
|
@@ -185,9 +88,9 @@ function findLastTemplateCloseTag(source = "") {
|
|
|
185
88
|
return lastMatch;
|
|
186
89
|
}
|
|
187
90
|
|
|
188
|
-
function applyOutletTemplateBlock(source = "", { host = "", position = ""
|
|
91
|
+
function applyOutletTemplateBlock(source = "", { host = "", position = "" } = {}) {
|
|
189
92
|
const sourceText = String(source || "");
|
|
190
|
-
const outletBlock = createOutletBlock({ host, position
|
|
93
|
+
const outletBlock = createOutletBlock({ host, position });
|
|
191
94
|
|
|
192
95
|
const templateTagMatch = findLastTemplateCloseTag(sourceText);
|
|
193
96
|
if (!templateTagMatch) {
|
|
@@ -223,18 +126,26 @@ async function runGeneratorSubcommand({
|
|
|
223
126
|
if (normalizedSubcommand !== "outlet") {
|
|
224
127
|
throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
225
128
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
129
|
+
const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator outlet" });
|
|
130
|
+
rejectUnexpectedOptions(options, ["target"], {
|
|
131
|
+
context: "ui-generator outlet"
|
|
132
|
+
});
|
|
229
133
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
134
|
+
const outletTarget = resolveOutletTargetId(options?.target, {
|
|
135
|
+
context: "ui-generator outlet",
|
|
136
|
+
optionName: "target",
|
|
137
|
+
defaultPosition: DEFAULT_OUTLET_POSITION
|
|
138
|
+
});
|
|
139
|
+
const { host, position } = outletTarget;
|
|
234
140
|
|
|
235
141
|
const targetFilePath = resolvePathWithinApp(appRoot, targetFile, {
|
|
236
142
|
context: "ui-generator outlet"
|
|
237
143
|
});
|
|
144
|
+
if (!targetFilePath.relativePath.toLowerCase().endsWith(".vue")) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`ui-generator outlet target file must be an existing Vue SFC (.vue): ${targetFilePath.relativePath}.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
238
149
|
|
|
239
150
|
let source = "";
|
|
240
151
|
try {
|
|
@@ -244,19 +155,13 @@ async function runGeneratorSubcommand({
|
|
|
244
155
|
}
|
|
245
156
|
|
|
246
157
|
const hasTargetOutlet = hasShellOutletTarget(source, { host, position });
|
|
247
|
-
const hasRouterView = ROUTER_VIEW_TAG_PATTERN.test(source);
|
|
248
|
-
const shouldInsertRouterView = mode === MODE_ROUTED && !hasRouterView && !hasTargetOutlet;
|
|
249
|
-
|
|
250
158
|
const templateApplied = hasTargetOutlet
|
|
251
159
|
? { changed: false, content: source }
|
|
252
160
|
: applyOutletTemplateBlock(source, {
|
|
253
161
|
host,
|
|
254
|
-
position
|
|
255
|
-
includeRouterView: shouldInsertRouterView
|
|
162
|
+
position
|
|
256
163
|
});
|
|
257
|
-
const scriptApplied = applyScriptImports(templateApplied.content
|
|
258
|
-
includeRouterViewImport: shouldInsertRouterView
|
|
259
|
-
});
|
|
164
|
+
const scriptApplied = applyScriptImports(templateApplied.content);
|
|
260
165
|
|
|
261
166
|
const changed = templateApplied.changed || scriptApplied.changed;
|
|
262
167
|
if (changed && dryRun !== true) {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { normalizeBoolean, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import { buildUiPageTemplateContext } from "../buildTemplateContext.js";
|
|
5
|
+
import {
|
|
6
|
+
PLACEMENT_FILE,
|
|
7
|
+
requireSinglePositionalTargetFile,
|
|
8
|
+
rejectUnexpectedOptions,
|
|
9
|
+
resolvePathWithinApp,
|
|
10
|
+
appendBlockIfMarkerMissing
|
|
11
|
+
} from "./support.js";
|
|
12
|
+
import {
|
|
13
|
+
resolvePageTargetDetails,
|
|
14
|
+
renderPlainPageSource
|
|
15
|
+
} from "./pageSupport.js";
|
|
16
|
+
|
|
17
|
+
function renderPageLinkPlacementBlock({
|
|
18
|
+
marker = "",
|
|
19
|
+
context = {},
|
|
20
|
+
label = "",
|
|
21
|
+
surface = ""
|
|
22
|
+
} = {}) {
|
|
23
|
+
return (
|
|
24
|
+
`// ${marker}\n` +
|
|
25
|
+
"{\n" +
|
|
26
|
+
" addPlacement({\n" +
|
|
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` +
|
|
30
|
+
` surfaces: ["${surface}"],\n` +
|
|
31
|
+
" order: 155,\n" +
|
|
32
|
+
` componentToken: "${context.__JSKIT_UI_LINK_COMPONENT_TOKEN__}",\n` +
|
|
33
|
+
" props: {\n" +
|
|
34
|
+
` label: "${label}",\n` +
|
|
35
|
+
` surface: "${surface}",\n` +
|
|
36
|
+
` workspaceSuffix: "${context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__}",\n` +
|
|
37
|
+
` nonWorkspaceSuffix: "${context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__}",\n` +
|
|
38
|
+
`${context.__JSKIT_UI_LINK_TO_PROP_LINE__} },\n` +
|
|
39
|
+
" when: ({ auth }) => Boolean(auth?.authenticated)\n" +
|
|
40
|
+
" });\n" +
|
|
41
|
+
"}\n"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runGeneratorSubcommand({
|
|
46
|
+
appRoot,
|
|
47
|
+
subcommand = "",
|
|
48
|
+
args = [],
|
|
49
|
+
options = {},
|
|
50
|
+
dryRun = false
|
|
51
|
+
} = {}) {
|
|
52
|
+
const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
|
|
53
|
+
if (normalizedSubcommand !== "page") {
|
|
54
|
+
throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
55
|
+
}
|
|
56
|
+
const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator page" });
|
|
57
|
+
rejectUnexpectedOptions(
|
|
58
|
+
options,
|
|
59
|
+
["name", "link-placement", "link-component-token", "link-to", "force"],
|
|
60
|
+
{ context: "ui-generator page" }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const pageTarget = await resolvePageTargetDetails({
|
|
64
|
+
appRoot,
|
|
65
|
+
targetFile,
|
|
66
|
+
context: "ui-generator page"
|
|
67
|
+
});
|
|
68
|
+
const pageLabel = normalizeText(options?.name) || pageTarget.defaultName;
|
|
69
|
+
const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
|
|
70
|
+
? normalizeBoolean(options.force)
|
|
71
|
+
: false;
|
|
72
|
+
const pageFilePath = pageTarget.targetFilePath.absolutePath;
|
|
73
|
+
const pageRelativePath = pageTarget.targetFilePath.relativePath;
|
|
74
|
+
|
|
75
|
+
const touchedFiles = new Set();
|
|
76
|
+
let pageAlreadyExisted = true;
|
|
77
|
+
try {
|
|
78
|
+
await readFile(pageFilePath, "utf8");
|
|
79
|
+
} catch {
|
|
80
|
+
pageAlreadyExisted = false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (pageAlreadyExisted && !forceOverwrite) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`ui-generator page will not overwrite existing page ${pageRelativePath}. Re-run with --force to overwrite it.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const placementContext = await buildUiPageTemplateContext({
|
|
90
|
+
appRoot: pageTarget.appRoot,
|
|
91
|
+
targetFile,
|
|
92
|
+
options
|
|
93
|
+
});
|
|
94
|
+
const placementPath = resolvePathWithinApp(pageTarget.appRoot, PLACEMENT_FILE, {
|
|
95
|
+
context: "ui-generator page"
|
|
96
|
+
});
|
|
97
|
+
const placementSource = await readFile(placementPath.absolutePath, "utf8");
|
|
98
|
+
const placementMarker = `jskit:ui-generator.page.link:${pageTarget.surfaceId}:${pageTarget.routeUrlSuffix}`;
|
|
99
|
+
const placementApplied = appendBlockIfMarkerMissing(
|
|
100
|
+
placementSource,
|
|
101
|
+
placementMarker,
|
|
102
|
+
renderPageLinkPlacementBlock({
|
|
103
|
+
marker: placementMarker,
|
|
104
|
+
context: placementContext,
|
|
105
|
+
label: pageLabel,
|
|
106
|
+
surface: pageTarget.surfaceId
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!pageAlreadyExisted || forceOverwrite) {
|
|
111
|
+
if (dryRun !== true) {
|
|
112
|
+
await mkdir(path.dirname(pageFilePath), { recursive: true });
|
|
113
|
+
await writeFile(pageFilePath, renderPlainPageSource(pageLabel), "utf8");
|
|
114
|
+
}
|
|
115
|
+
touchedFiles.add(pageRelativePath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (placementApplied.changed) {
|
|
119
|
+
if (dryRun !== true) {
|
|
120
|
+
await writeFile(placementPath.absolutePath, placementApplied.content, "utf8");
|
|
121
|
+
}
|
|
122
|
+
touchedFiles.add(placementPath.relativePath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const touchedFileList = [...touchedFiles].sort((left, right) => left.localeCompare(right));
|
|
126
|
+
return {
|
|
127
|
+
touchedFiles: touchedFileList,
|
|
128
|
+
summary: !pageAlreadyExisted
|
|
129
|
+
? `Generated UI page "${pageTarget.routeUrlSuffix}" at ${pageRelativePath}.`
|
|
130
|
+
: `Regenerated UI page "${pageTarget.routeUrlSuffix}" at ${pageRelativePath}.`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { runGeneratorSubcommand };
|