@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
|
@@ -9,6 +9,9 @@ import { toCamelCase, toSnakeCase } from "@jskit-ai/kernel/shared/support/string
|
|
|
9
9
|
const DEFAULT_COMPONENT_DIRECTORY = "src/components";
|
|
10
10
|
const MAIN_CLIENT_PROVIDER_FILE = "packages/main/src/client/providers/MainClientProvider.js";
|
|
11
11
|
const PLACEMENT_FILE = "src/placement.js";
|
|
12
|
+
const SCRIPT_TAG_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
13
|
+
const SCRIPT_SETUP_ATTRIBUTE_PATTERN = /\bsetup\b/i;
|
|
14
|
+
const ATTRIBUTE_PATTERN = /([:@]?[A-Za-z_][A-Za-z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
|
|
12
15
|
|
|
13
16
|
function toKebabCase(value = "") {
|
|
14
17
|
return toSnakeCase(value).replaceAll("_", "-");
|
|
@@ -32,6 +35,85 @@ function requireOption(options = {}, optionName = "", { context = "ui-generator"
|
|
|
32
35
|
return optionValue;
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
function requireSinglePositionalTargetFile(args = [], { context = "ui-generator" } = {}) {
|
|
39
|
+
const positionalArgs = Array.isArray(args) ? args.map((value) => normalizeText(value)).filter(Boolean) : [];
|
|
40
|
+
if (positionalArgs.length !== 1) {
|
|
41
|
+
throw new Error(`${context} requires exactly one <target-file> positional argument.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return positionalArgs[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeExplicitOutletTargetId(value = "") {
|
|
48
|
+
const normalizedValue = normalizeText(value);
|
|
49
|
+
if (!normalizedValue) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const separatorIndex = normalizedValue.indexOf(":");
|
|
54
|
+
if (separatorIndex <= 0 || separatorIndex >= normalizedValue.length - 1) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const host = normalizeText(normalizedValue.slice(0, separatorIndex));
|
|
59
|
+
const position = normalizeText(normalizedValue.slice(separatorIndex + 1));
|
|
60
|
+
if (!host || !position) {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `${host}:${position}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveOutletTargetId(
|
|
68
|
+
rawTarget = "",
|
|
69
|
+
{
|
|
70
|
+
context = "ui-generator",
|
|
71
|
+
optionName = "target",
|
|
72
|
+
defaultPosition = ""
|
|
73
|
+
} = {}
|
|
74
|
+
) {
|
|
75
|
+
const normalizedTarget = normalizeText(rawTarget);
|
|
76
|
+
if (!normalizedTarget) {
|
|
77
|
+
throw new Error(`${context} requires --${optionName}.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const targetId = normalizedTarget.includes(":")
|
|
81
|
+
? normalizeExplicitOutletTargetId(normalizedTarget)
|
|
82
|
+
: normalizeExplicitOutletTargetId(`${normalizedTarget}:${normalizeText(defaultPosition)}`);
|
|
83
|
+
if (!targetId) {
|
|
84
|
+
throw new Error(`${context} option "${optionName}" must be "host" or "host:position".`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const separatorIndex = targetId.indexOf(":");
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
id: targetId,
|
|
90
|
+
host: targetId.slice(0, separatorIndex),
|
|
91
|
+
position: targetId.slice(separatorIndex + 1)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function rejectUnexpectedOptions(options = {}, allowedOptionNames = [], { context = "ui-generator" } = {}) {
|
|
96
|
+
const allowedOptionNameSet = new Set(
|
|
97
|
+
(Array.isArray(allowedOptionNames) ? allowedOptionNames : [])
|
|
98
|
+
.map((optionName) => normalizeText(optionName))
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const unexpectedOptions = Object.keys(options || {})
|
|
103
|
+
.map((optionName) => normalizeText(optionName))
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.filter((optionName) => !allowedOptionNameSet.has(optionName))
|
|
106
|
+
.sort((left, right) => left.localeCompare(right));
|
|
107
|
+
|
|
108
|
+
if (unexpectedOptions.length < 1) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${context} received unsupported option${unexpectedOptions.length > 1 ? "s" : ""}: ${unexpectedOptions.map((optionName) => `--${optionName}`).join(", ")}.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
35
117
|
function resolvePathWithinApp(appRoot, targetPath, { context = "ui-generator" } = {}) {
|
|
36
118
|
const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
|
|
37
119
|
context
|
|
@@ -150,6 +232,61 @@ function insertBeforeClassDeclaration(source = "", line = "", { className = "",
|
|
|
150
232
|
};
|
|
151
233
|
}
|
|
152
234
|
|
|
235
|
+
function findScriptBlock(source = "") {
|
|
236
|
+
const sourceText = String(source || "");
|
|
237
|
+
let firstMatch = null;
|
|
238
|
+
|
|
239
|
+
for (const match of sourceText.matchAll(SCRIPT_TAG_PATTERN)) {
|
|
240
|
+
if (!firstMatch) {
|
|
241
|
+
firstMatch = match;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const attributesSource = String(match[1] || "");
|
|
245
|
+
if (SCRIPT_SETUP_ATTRIBUTE_PATTERN.test(attributesSource)) {
|
|
246
|
+
return Object.freeze({
|
|
247
|
+
index: match.index,
|
|
248
|
+
source: String(match[0] || ""),
|
|
249
|
+
attributesSource,
|
|
250
|
+
content: String(match[2] || "")
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!firstMatch) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return Object.freeze({
|
|
260
|
+
index: firstMatch.index,
|
|
261
|
+
source: String(firstMatch[0] || ""),
|
|
262
|
+
attributesSource: String(firstMatch[1] || ""),
|
|
263
|
+
content: String(firstMatch[2] || "")
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseTagAttributes(attributesSource = "") {
|
|
268
|
+
const attributes = {};
|
|
269
|
+
const source = String(attributesSource || "");
|
|
270
|
+
for (const match of source.matchAll(ATTRIBUTE_PATTERN)) {
|
|
271
|
+
const attributeName = normalizeText(match[1]);
|
|
272
|
+
if (!attributeName) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const hasValue = match[2] != null || match[3] != null;
|
|
276
|
+
attributes[attributeName] = hasValue ? String(match[2] ?? match[3] ?? "") : true;
|
|
277
|
+
}
|
|
278
|
+
return attributes;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function indentBlock(source = "", indent = "") {
|
|
282
|
+
const sourceText = String(source || "");
|
|
283
|
+
const indentation = String(indent || "");
|
|
284
|
+
return sourceText
|
|
285
|
+
.split("\n")
|
|
286
|
+
.map((line) => `${indentation}${line}`)
|
|
287
|
+
.join("\n");
|
|
288
|
+
}
|
|
289
|
+
|
|
153
290
|
export {
|
|
154
291
|
DEFAULT_COMPONENT_DIRECTORY,
|
|
155
292
|
MAIN_CLIENT_PROVIDER_FILE,
|
|
@@ -157,9 +294,16 @@ export {
|
|
|
157
294
|
toKebabCase,
|
|
158
295
|
toPascalCase,
|
|
159
296
|
requireOption,
|
|
297
|
+
requireSinglePositionalTargetFile,
|
|
298
|
+
normalizeExplicitOutletTargetId,
|
|
299
|
+
resolveOutletTargetId,
|
|
300
|
+
rejectUnexpectedOptions,
|
|
160
301
|
resolvePathWithinApp,
|
|
161
302
|
ensureTrailingNewline,
|
|
162
303
|
appendBlockIfMarkerMissing,
|
|
163
304
|
insertImportIfMissing,
|
|
164
|
-
insertBeforeClassDeclaration
|
|
305
|
+
insertBeforeClassDeclaration,
|
|
306
|
+
findScriptBlock,
|
|
307
|
+
parseTagAttributes,
|
|
308
|
+
indentBlock
|
|
165
309
|
};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { runGeneratorSubcommand } from "../src/server/subcommands/addSubpages.js";
|
|
7
|
+
|
|
8
|
+
async function withTempApp(run) {
|
|
9
|
+
const appRoot = await mkdtemp(path.join(tmpdir(), "ui-generator-add-subpages-"));
|
|
10
|
+
try {
|
|
11
|
+
return await run(appRoot);
|
|
12
|
+
} finally {
|
|
13
|
+
await rm(appRoot, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function writeAppFixture(appRoot) {
|
|
18
|
+
await mkdir(path.join(appRoot, "config"), { recursive: true });
|
|
19
|
+
await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
|
|
20
|
+
await mkdir(path.join(appRoot, "src"), { recursive: true });
|
|
21
|
+
await mkdir(path.join(appRoot, "packages", "main", "src", "client", "providers"), { recursive: true });
|
|
22
|
+
|
|
23
|
+
await writeFile(
|
|
24
|
+
path.join(appRoot, "config", "public.js"),
|
|
25
|
+
`export const config = {
|
|
26
|
+
surfaceDefinitions: {
|
|
27
|
+
admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true, requiresAuth: true, requiresWorkspace: true }
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
`,
|
|
31
|
+
"utf8"
|
|
32
|
+
);
|
|
33
|
+
await writeFile(
|
|
34
|
+
path.join(appRoot, "src", "placement.js"),
|
|
35
|
+
`function addPlacement() {}
|
|
36
|
+
|
|
37
|
+
export { addPlacement };
|
|
38
|
+
export default function getPlacements() {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
`,
|
|
42
|
+
"utf8"
|
|
43
|
+
);
|
|
44
|
+
await writeFile(
|
|
45
|
+
path.join(appRoot, "packages", "main", "src", "client", "providers", "MainClientProvider.js"),
|
|
46
|
+
`const mainClientComponents = [];
|
|
47
|
+
|
|
48
|
+
function registerMainClientComponent(componentToken, resolveComponent) {
|
|
49
|
+
const token = String(componentToken || "").trim();
|
|
50
|
+
if (!token || typeof resolveComponent !== "function") {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
mainClientComponents.push(
|
|
54
|
+
Object.freeze({
|
|
55
|
+
token,
|
|
56
|
+
resolveComponent
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class MainClientProvider {}
|
|
62
|
+
|
|
63
|
+
export { MainClientProvider, registerMainClientComponent };
|
|
64
|
+
`,
|
|
65
|
+
"utf8"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function writePageFile(appRoot, targetFile, source = "<template><section /></template>\n") {
|
|
70
|
+
const targetPath = path.join(appRoot, "src/pages", targetFile);
|
|
71
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
72
|
+
await writeFile(targetPath, source, "utf8");
|
|
73
|
+
return targetPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function readPageFile(appRoot, targetFile) {
|
|
77
|
+
return readFile(path.join(appRoot, "src/pages", targetFile), "utf8");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
test("ui-generator add-subpages derives the default target from an index-route page path", async () => {
|
|
81
|
+
await withTempApp(async (appRoot) => {
|
|
82
|
+
await writeAppFixture(appRoot);
|
|
83
|
+
|
|
84
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
85
|
+
await writePageFile(
|
|
86
|
+
appRoot,
|
|
87
|
+
targetFile,
|
|
88
|
+
`<template>
|
|
89
|
+
<section class="pa-4">
|
|
90
|
+
<h1 class="text-h5 mb-2">Practice</h1>
|
|
91
|
+
</section>
|
|
92
|
+
</template>
|
|
93
|
+
`
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const result = await runGeneratorSubcommand({
|
|
97
|
+
appRoot,
|
|
98
|
+
subcommand: "add-subpages",
|
|
99
|
+
args: [targetFile],
|
|
100
|
+
options: {
|
|
101
|
+
title: "Practice",
|
|
102
|
+
subtitle: "Manage practice modules."
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(result.touchedFiles, [
|
|
107
|
+
"packages/main/src/client/providers/MainClientProvider.js",
|
|
108
|
+
"src/components/SectionContainerShell.vue",
|
|
109
|
+
"src/components/TabLinkItem.vue",
|
|
110
|
+
`src/pages/${targetFile}`
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const pageSource = await readPageFile(appRoot, targetFile);
|
|
114
|
+
assert.match(pageSource, /<ShellOutlet host="practice" position="sub-pages" \/>/);
|
|
115
|
+
assert.match(pageSource, /<RouterView \/>/);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("ui-generator add-subpages derives the default target from a dynamic file-route page path", async () => {
|
|
120
|
+
await withTempApp(async (appRoot) => {
|
|
121
|
+
await writeAppFixture(appRoot);
|
|
122
|
+
|
|
123
|
+
const targetFile = "w/[workspaceSlug]/admin/contacts/[contactId].vue";
|
|
124
|
+
await writePageFile(appRoot, targetFile);
|
|
125
|
+
|
|
126
|
+
await runGeneratorSubcommand({
|
|
127
|
+
appRoot,
|
|
128
|
+
subcommand: "add-subpages",
|
|
129
|
+
args: [targetFile],
|
|
130
|
+
options: {}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const pageSource = await readPageFile(appRoot, targetFile);
|
|
134
|
+
assert.match(pageSource, /<ShellOutlet host="contacts-contact-id" position="sub-pages" \/>/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("ui-generator add-subpages derives the default target from a nested route path", async () => {
|
|
139
|
+
await withTempApp(async (appRoot) => {
|
|
140
|
+
await writeAppFixture(appRoot);
|
|
141
|
+
|
|
142
|
+
const targetFile = "w/[workspaceSlug]/admin/catalog/products/index.vue";
|
|
143
|
+
await writePageFile(appRoot, targetFile);
|
|
144
|
+
|
|
145
|
+
await runGeneratorSubcommand({
|
|
146
|
+
appRoot,
|
|
147
|
+
subcommand: "add-subpages",
|
|
148
|
+
args: [targetFile],
|
|
149
|
+
options: {}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const pageSource = await readPageFile(appRoot, targetFile);
|
|
153
|
+
assert.match(pageSource, /<ShellOutlet host="catalog-products" position="sub-pages" \/>/);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("ui-generator add-subpages supports explicit target host shorthand", async () => {
|
|
158
|
+
await withTempApp(async (appRoot) => {
|
|
159
|
+
await writeAppFixture(appRoot);
|
|
160
|
+
|
|
161
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
162
|
+
await writePageFile(appRoot, targetFile);
|
|
163
|
+
|
|
164
|
+
await runGeneratorSubcommand({
|
|
165
|
+
appRoot,
|
|
166
|
+
subcommand: "add-subpages",
|
|
167
|
+
args: [targetFile],
|
|
168
|
+
options: {
|
|
169
|
+
target: "practice-hub"
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const pageSource = await readPageFile(appRoot, targetFile);
|
|
174
|
+
assert.match(pageSource, /<ShellOutlet host="practice-hub" position="sub-pages" \/>/);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("ui-generator add-subpages supports explicit target host:position", async () => {
|
|
179
|
+
await withTempApp(async (appRoot) => {
|
|
180
|
+
await writeAppFixture(appRoot);
|
|
181
|
+
|
|
182
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
183
|
+
await writePageFile(appRoot, targetFile);
|
|
184
|
+
|
|
185
|
+
await runGeneratorSubcommand({
|
|
186
|
+
appRoot,
|
|
187
|
+
subcommand: "add-subpages",
|
|
188
|
+
args: [targetFile],
|
|
189
|
+
options: {
|
|
190
|
+
target: "practice-hub:secondary-tabs"
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const pageSource = await readPageFile(appRoot, targetFile);
|
|
195
|
+
assert.match(pageSource, /<ShellOutlet host="practice-hub" position="secondary-tabs" \/>/);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("ui-generator add-subpages does not rewrite existing scaffold support components", async () => {
|
|
200
|
+
await withTempApp(async (appRoot) => {
|
|
201
|
+
await writeAppFixture(appRoot);
|
|
202
|
+
|
|
203
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
204
|
+
await writePageFile(appRoot, targetFile);
|
|
205
|
+
const customSectionShellSource = `<template><section class="custom-shell"><slot /></section></template>\n`;
|
|
206
|
+
const customTabLinkSource = `<template><button class="custom-tab-link"><slot /></button></template>\n`;
|
|
207
|
+
await writeFile(
|
|
208
|
+
path.join(appRoot, "src", "components", "SectionContainerShell.vue"),
|
|
209
|
+
customSectionShellSource,
|
|
210
|
+
"utf8"
|
|
211
|
+
);
|
|
212
|
+
await writeFile(
|
|
213
|
+
path.join(appRoot, "src", "components", "TabLinkItem.vue"),
|
|
214
|
+
customTabLinkSource,
|
|
215
|
+
"utf8"
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const result = await runGeneratorSubcommand({
|
|
219
|
+
appRoot,
|
|
220
|
+
subcommand: "add-subpages",
|
|
221
|
+
args: [targetFile],
|
|
222
|
+
options: {
|
|
223
|
+
title: "Practice"
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
assert.deepEqual(result.touchedFiles, [
|
|
228
|
+
"packages/main/src/client/providers/MainClientProvider.js",
|
|
229
|
+
`src/pages/${targetFile}`
|
|
230
|
+
]);
|
|
231
|
+
assert.equal(
|
|
232
|
+
await readFile(path.join(appRoot, "src", "components", "SectionContainerShell.vue"), "utf8"),
|
|
233
|
+
customSectionShellSource
|
|
234
|
+
);
|
|
235
|
+
assert.equal(
|
|
236
|
+
await readFile(path.join(appRoot, "src", "components", "TabLinkItem.vue"), "utf8"),
|
|
237
|
+
customTabLinkSource
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("ui-generator add-subpages fails if subpages are already enabled", async () => {
|
|
243
|
+
await withTempApp(async (appRoot) => {
|
|
244
|
+
await writeAppFixture(appRoot);
|
|
245
|
+
|
|
246
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
247
|
+
await writePageFile(appRoot, targetFile);
|
|
248
|
+
|
|
249
|
+
await runGeneratorSubcommand({
|
|
250
|
+
appRoot,
|
|
251
|
+
subcommand: "add-subpages",
|
|
252
|
+
args: [targetFile],
|
|
253
|
+
options: {}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await assert.rejects(
|
|
257
|
+
runGeneratorSubcommand({
|
|
258
|
+
appRoot,
|
|
259
|
+
subcommand: "add-subpages",
|
|
260
|
+
args: [targetFile],
|
|
261
|
+
options: {}
|
|
262
|
+
}),
|
|
263
|
+
/found existing RouterView.*Subpages are already enabled/
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("ui-generator add-subpages rejects files outside src/pages", async () => {
|
|
269
|
+
await withTempApp(async (appRoot) => {
|
|
270
|
+
await writeAppFixture(appRoot);
|
|
271
|
+
await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
|
|
272
|
+
await writeFile(path.join(appRoot, "src", "components", "Panel.vue"), "<template><div /></template>\n", "utf8");
|
|
273
|
+
|
|
274
|
+
await assert.rejects(
|
|
275
|
+
runGeneratorSubcommand({
|
|
276
|
+
appRoot,
|
|
277
|
+
subcommand: "add-subpages",
|
|
278
|
+
args: ["components/Panel.vue"],
|
|
279
|
+
options: {}
|
|
280
|
+
}),
|
|
281
|
+
/must be relative to src\/pages\/ and resolve to a configured surface/
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("ui-generator add-subpages validates target format", async () => {
|
|
287
|
+
await withTempApp(async (appRoot) => {
|
|
288
|
+
await writeAppFixture(appRoot);
|
|
289
|
+
|
|
290
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
291
|
+
await writePageFile(appRoot, targetFile);
|
|
292
|
+
|
|
293
|
+
await assert.rejects(
|
|
294
|
+
runGeneratorSubcommand({
|
|
295
|
+
appRoot,
|
|
296
|
+
subcommand: "add-subpages",
|
|
297
|
+
args: [targetFile],
|
|
298
|
+
options: {
|
|
299
|
+
target: "practice:"
|
|
300
|
+
}
|
|
301
|
+
}),
|
|
302
|
+
/option "target" must be "host" or "host:position"/
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("ui-generator add-subpages rejects target files with a src/pages prefix", async () => {
|
|
308
|
+
await withTempApp(async (appRoot) => {
|
|
309
|
+
await writeAppFixture(appRoot);
|
|
310
|
+
|
|
311
|
+
await assert.rejects(
|
|
312
|
+
runGeneratorSubcommand({
|
|
313
|
+
appRoot,
|
|
314
|
+
subcommand: "add-subpages",
|
|
315
|
+
args: ["src/pages/w/[workspaceSlug]/admin/practice/index.vue"],
|
|
316
|
+
options: {}
|
|
317
|
+
}),
|
|
318
|
+
/must be relative to src\/pages\/, without the src\/pages\/ prefix/
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
});
|