@jskit-ai/ui-generator 0.1.48 → 0.1.49
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/package.descriptor.mjs +40 -23
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +8 -1
- package/src/server/subcommands/addSubpages.js +77 -2
- package/src/server/subcommands/element.js +25 -7
- package/src/server/subcommands/outlet.js +139 -5
- package/src/server/subcommands/page.js +7 -2
- package/src/server/subcommands/pageSupport.js +1 -1
- package/src/server/subcommands/support.js +2 -0
- package/test/addSubpagesSubcommand.test.js +31 -7
- package/test/buildTemplateContext.test.js +196 -34
- package/test/elementSubcommand.test.js +76 -11
- package/test/outletSubcommand.test.js +47 -14
- package/test/pageSubcommand.test.js +100 -12
|
@@ -8,6 +8,23 @@ import { runGeneratorSubcommand } from "../src/server/subcommands/outlet.js";
|
|
|
8
8
|
async function withTempApp(run) {
|
|
9
9
|
const appRoot = await mkdtemp(path.join(tmpdir(), "ui-generator-outlet-"));
|
|
10
10
|
try {
|
|
11
|
+
await mkdir(path.join(appRoot, "src"), { recursive: true });
|
|
12
|
+
await writeFile(
|
|
13
|
+
path.join(appRoot, "src", "placementTopology.js"),
|
|
14
|
+
`const placements = [];
|
|
15
|
+
|
|
16
|
+
function addPlacementTopology(value = {}) {
|
|
17
|
+
placements.push(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { addPlacementTopology };
|
|
21
|
+
|
|
22
|
+
export default function getPlacementTopology() {
|
|
23
|
+
return { placements };
|
|
24
|
+
}
|
|
25
|
+
`,
|
|
26
|
+
"utf8"
|
|
27
|
+
);
|
|
11
28
|
return await run(appRoot);
|
|
12
29
|
} finally {
|
|
13
30
|
await rm(appRoot, { recursive: true, force: true });
|
|
@@ -40,24 +57,32 @@ import { computed } from "vue";
|
|
|
40
57
|
subcommand: "outlet",
|
|
41
58
|
args: [targetFile],
|
|
42
59
|
options: {
|
|
43
|
-
target: "contact-view:sub-pages"
|
|
60
|
+
target: "contact-view:sub-pages",
|
|
61
|
+
placement: "page.section-nav"
|
|
44
62
|
}
|
|
45
63
|
});
|
|
46
64
|
|
|
47
|
-
assert.deepEqual(result.touchedFiles, [targetFile]);
|
|
65
|
+
assert.deepEqual(result.touchedFiles, [targetFile, "src/placementTopology.js"]);
|
|
48
66
|
|
|
49
67
|
const output = await readFile(targetPath, "utf8");
|
|
50
68
|
assert.match(output, /import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
|
|
51
69
|
assert.match(output, /<ShellOutlet target="contact-view:sub-pages" \/>/);
|
|
52
70
|
assert.doesNotMatch(output, /RouterView/);
|
|
53
71
|
assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
|
|
72
|
+
const topologySource = await readFile(path.join(appRoot, "src", "placementTopology.js"), "utf8");
|
|
73
|
+
assert.match(topologySource, /id: "page\.section-nav"/);
|
|
74
|
+
assert.match(topologySource, /owner: "contact-view"/);
|
|
75
|
+
assert.match(topologySource, /compact: \{/);
|
|
76
|
+
assert.match(topologySource, /medium: \{/);
|
|
77
|
+
assert.match(topologySource, /expanded: \{/);
|
|
54
78
|
|
|
55
79
|
const rerun = await runGeneratorSubcommand({
|
|
56
80
|
appRoot,
|
|
57
81
|
subcommand: "outlet",
|
|
58
82
|
args: [targetFile],
|
|
59
83
|
options: {
|
|
60
|
-
target: "contact-view:sub-pages"
|
|
84
|
+
target: "contact-view:sub-pages",
|
|
85
|
+
placement: "page.section-nav"
|
|
61
86
|
}
|
|
62
87
|
});
|
|
63
88
|
|
|
@@ -91,7 +116,8 @@ import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
|
|
|
91
116
|
subcommand: "outlet",
|
|
92
117
|
args: [targetFile],
|
|
93
118
|
options: {
|
|
94
|
-
target: "contact-view:sub-pages"
|
|
119
|
+
target: "contact-view:sub-pages",
|
|
120
|
+
placement: "page.section-nav"
|
|
95
121
|
}
|
|
96
122
|
});
|
|
97
123
|
|
|
@@ -124,7 +150,8 @@ test("ui-generator outlet creates script setup when missing", async () => {
|
|
|
124
150
|
subcommand: "outlet",
|
|
125
151
|
args: [targetFile],
|
|
126
152
|
options: {
|
|
127
|
-
target: "contact-view:sub-pages"
|
|
153
|
+
target: "contact-view:sub-pages",
|
|
154
|
+
placement: "page.section-nav"
|
|
128
155
|
}
|
|
129
156
|
});
|
|
130
157
|
|
|
@@ -161,7 +188,8 @@ test("ui-generator outlet inserts generated script after existing route block",
|
|
|
161
188
|
subcommand: "outlet",
|
|
162
189
|
args: [targetFile],
|
|
163
190
|
options: {
|
|
164
|
-
target: "contact-view:sub-pages"
|
|
191
|
+
target: "contact-view:sub-pages",
|
|
192
|
+
placement: "page.section-nav"
|
|
165
193
|
}
|
|
166
194
|
});
|
|
167
195
|
|
|
@@ -203,7 +231,8 @@ test("ui-generator outlet keeps indentation when injected into nested template b
|
|
|
203
231
|
subcommand: "outlet",
|
|
204
232
|
args: [targetFile],
|
|
205
233
|
options: {
|
|
206
|
-
target: "contact-view:sub-pages"
|
|
234
|
+
target: "contact-view:sub-pages",
|
|
235
|
+
placement: "page.section-nav"
|
|
207
236
|
}
|
|
208
237
|
});
|
|
209
238
|
|
|
@@ -229,6 +258,7 @@ test("ui-generator outlet rejects unsupported options", async () => {
|
|
|
229
258
|
args: [targetFile],
|
|
230
259
|
options: {
|
|
231
260
|
target: "contact-view:sub-pages",
|
|
261
|
+
placement: "page.section-nav",
|
|
232
262
|
bogus: "routed"
|
|
233
263
|
}
|
|
234
264
|
}),
|
|
@@ -250,7 +280,8 @@ test("ui-generator outlet supports explicit target host:position", async () => {
|
|
|
250
280
|
subcommand: "outlet",
|
|
251
281
|
args: [targetFile],
|
|
252
282
|
options: {
|
|
253
|
-
target: "customer-view:summary-actions"
|
|
283
|
+
target: "customer-view:summary-actions",
|
|
284
|
+
placement: "page.actions"
|
|
254
285
|
}
|
|
255
286
|
});
|
|
256
287
|
|
|
@@ -272,9 +303,10 @@ test("ui-generator outlet rejects non-vue target files without changing them", a
|
|
|
272
303
|
runGeneratorSubcommand({
|
|
273
304
|
appRoot,
|
|
274
305
|
subcommand: "outlet",
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
target: "vet-view:sub-pages"
|
|
306
|
+
args: [targetFile],
|
|
307
|
+
options: {
|
|
308
|
+
target: "vet-view:sub-pages",
|
|
309
|
+
placement: "page.section-nav"
|
|
278
310
|
}
|
|
279
311
|
}),
|
|
280
312
|
/ui-generator outlet target file must be an existing Vue SFC \(\.vue\): src\/pages\/w\/\[workspaceSlug\]\/admin\/practice\/vets\/_components\/VetAddEditFormFields\.js\./
|
|
@@ -297,9 +329,10 @@ test("ui-generator outlet validates target format", async () => {
|
|
|
297
329
|
runGeneratorSubcommand({
|
|
298
330
|
appRoot,
|
|
299
331
|
subcommand: "outlet",
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
target: "customer-view:"
|
|
332
|
+
args: [targetFile],
|
|
333
|
+
options: {
|
|
334
|
+
target: "customer-view:",
|
|
335
|
+
placement: "page.actions"
|
|
303
336
|
}
|
|
304
337
|
}),
|
|
305
338
|
/ui-generator outlet option "target" must be a target in "host:position" format\./
|
|
@@ -18,6 +18,67 @@ function toPagePath(targetFile = "") {
|
|
|
18
18
|
return path.join("src/pages", targetFile);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function renderTopologyVariant(outlet, { linkRenderer = "" } = {}) {
|
|
22
|
+
const rendererLines = linkRenderer
|
|
23
|
+
? `,
|
|
24
|
+
renderers: {
|
|
25
|
+
link: "${linkRenderer}"
|
|
26
|
+
}`
|
|
27
|
+
: "";
|
|
28
|
+
return `{
|
|
29
|
+
outlet: "${outlet}"${rendererLines}
|
|
30
|
+
}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderTopologyEntry({
|
|
34
|
+
id = "",
|
|
35
|
+
owner = "",
|
|
36
|
+
surfaces = ["*"],
|
|
37
|
+
defaultPlacement = false,
|
|
38
|
+
outlet = "",
|
|
39
|
+
linkRenderer = ""
|
|
40
|
+
} = {}) {
|
|
41
|
+
const ownerLine = owner ? ` owner: "${owner}",\n` : "";
|
|
42
|
+
const defaultLine = defaultPlacement ? " default: true,\n" : "";
|
|
43
|
+
return ` {
|
|
44
|
+
id: "${id}",
|
|
45
|
+
${ownerLine} surfaces: ${JSON.stringify(surfaces)},
|
|
46
|
+
${defaultLine} variants: {
|
|
47
|
+
compact: ${renderTopologyVariant(outlet, { linkRenderer })},
|
|
48
|
+
medium: ${renderTopologyVariant(outlet, { linkRenderer })},
|
|
49
|
+
expanded: ${renderTopologyVariant(outlet, { linkRenderer })}
|
|
50
|
+
}
|
|
51
|
+
}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function writePlacementTopology(appRoot, entries = []) {
|
|
55
|
+
const defaultEntries = [
|
|
56
|
+
renderTopologyEntry({
|
|
57
|
+
id: "shell.primary-nav",
|
|
58
|
+
surfaces: ["*"],
|
|
59
|
+
defaultPlacement: true,
|
|
60
|
+
outlet: "shell-layout:primary-menu",
|
|
61
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
62
|
+
}),
|
|
63
|
+
renderTopologyEntry({
|
|
64
|
+
id: "shell.status",
|
|
65
|
+
surfaces: ["*"],
|
|
66
|
+
outlet: "shell-layout:top-right",
|
|
67
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
68
|
+
})
|
|
69
|
+
];
|
|
70
|
+
await writeFile(
|
|
71
|
+
path.join(appRoot, "src", "placementTopology.js"),
|
|
72
|
+
`export default {
|
|
73
|
+
placements: [
|
|
74
|
+
${[...defaultEntries, ...entries].join(",\n")}
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
`,
|
|
78
|
+
"utf8"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
21
82
|
async function writeAppFixture(appRoot, { configSource = "" } = {}) {
|
|
22
83
|
await mkdir(path.join(appRoot, "config"), { recursive: true });
|
|
23
84
|
await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
|
|
@@ -41,7 +102,6 @@ async function writeAppFixture(appRoot, { configSource = "" } = {}) {
|
|
|
41
102
|
<ShellOutlet
|
|
42
103
|
target="shell-layout:primary-menu"
|
|
43
104
|
default
|
|
44
|
-
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
45
105
|
/>
|
|
46
106
|
<ShellOutlet target="shell-layout:top-right" />
|
|
47
107
|
</div>
|
|
@@ -60,6 +120,7 @@ export default function getPlacements() {
|
|
|
60
120
|
`,
|
|
61
121
|
"utf8"
|
|
62
122
|
);
|
|
123
|
+
await writePlacementTopology(appRoot);
|
|
63
124
|
}
|
|
64
125
|
|
|
65
126
|
test("ui-generator page subcommand creates an index page from an explicit target file", async () => {
|
|
@@ -153,15 +214,15 @@ test("ui-generator page subcommand supports link placement options", async () =>
|
|
|
153
214
|
subcommand: "page",
|
|
154
215
|
args: [targetFile],
|
|
155
216
|
options: {
|
|
156
|
-
"link-placement": "shell
|
|
157
|
-
"link-component-token": "local.main.ui.tab-link-item",
|
|
217
|
+
"link-placement": "shell.status",
|
|
158
218
|
"link-to": "./notes"
|
|
159
219
|
}
|
|
160
220
|
});
|
|
161
221
|
|
|
162
222
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
163
|
-
assert.match(placementSource, /target: "shell
|
|
164
|
-
assert.match(placementSource, /
|
|
223
|
+
assert.match(placementSource, /target: "shell\.status"/);
|
|
224
|
+
assert.match(placementSource, /kind: "link"/);
|
|
225
|
+
assert.doesNotMatch(placementSource, /componentToken: "local\.main\.ui\.tab-link-item"/);
|
|
165
226
|
assert.match(placementSource, /icon: "mdi-view-list-outline"/);
|
|
166
227
|
assert.match(placementSource, /to: "\.\/notes"/);
|
|
167
228
|
});
|
|
@@ -170,6 +231,15 @@ test("ui-generator page subcommand supports link placement options", async () =>
|
|
|
170
231
|
test("ui-generator page subcommand infers subpage link placement, tab token, and link-to from the nearest parent host", async () => {
|
|
171
232
|
await withTempApp(async (appRoot) => {
|
|
172
233
|
await writeAppFixture(appRoot);
|
|
234
|
+
await writePlacementTopology(appRoot, [
|
|
235
|
+
renderTopologyEntry({
|
|
236
|
+
id: "page.section-nav",
|
|
237
|
+
owner: "contact-view",
|
|
238
|
+
surfaces: ["admin"],
|
|
239
|
+
outlet: "contact-view:sub-pages",
|
|
240
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
241
|
+
})
|
|
242
|
+
]);
|
|
173
243
|
|
|
174
244
|
const parentFile = "w/[workspaceSlug]/admin/contacts/[contactId].vue";
|
|
175
245
|
await mkdir(path.dirname(path.join(appRoot, toPagePath(parentFile))), { recursive: true });
|
|
@@ -196,8 +266,9 @@ test("ui-generator page subcommand infers subpage link placement, tab token, and
|
|
|
196
266
|
});
|
|
197
267
|
|
|
198
268
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
199
|
-
assert.match(placementSource, /target: "
|
|
200
|
-
assert.match(placementSource, /
|
|
269
|
+
assert.match(placementSource, /target: "page\.section-nav"/);
|
|
270
|
+
assert.match(placementSource, /owner: "contact-view"/);
|
|
271
|
+
assert.doesNotMatch(placementSource, /componentToken: "local\.main\.ui\.surface-aware-menu-link-item"/);
|
|
201
272
|
assert.match(placementSource, /icon: "mdi-view-list-outline"/);
|
|
202
273
|
assert.match(placementSource, /to: "\.\/notes"/);
|
|
203
274
|
});
|
|
@@ -206,6 +277,22 @@ test("ui-generator page subcommand infers subpage link placement, tab token, and
|
|
|
206
277
|
test("ui-generator page subcommand prefers the nearest index-route parent host", async () => {
|
|
207
278
|
await withTempApp(async (appRoot) => {
|
|
208
279
|
await writeAppFixture(appRoot);
|
|
280
|
+
await writePlacementTopology(appRoot, [
|
|
281
|
+
renderTopologyEntry({
|
|
282
|
+
id: "page.section-nav",
|
|
283
|
+
owner: "catalog",
|
|
284
|
+
surfaces: ["admin"],
|
|
285
|
+
outlet: "catalog:sub-pages",
|
|
286
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
287
|
+
}),
|
|
288
|
+
renderTopologyEntry({
|
|
289
|
+
id: "page.section-nav",
|
|
290
|
+
owner: "catalog-products",
|
|
291
|
+
surfaces: ["admin"],
|
|
292
|
+
outlet: "catalog-products:sub-pages",
|
|
293
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
294
|
+
})
|
|
295
|
+
]);
|
|
209
296
|
|
|
210
297
|
await mkdir(path.join(appRoot, "src/pages/w/[workspaceSlug]/admin/catalog/index/products"), {
|
|
211
298
|
recursive: true
|
|
@@ -247,8 +334,9 @@ test("ui-generator page subcommand prefers the nearest index-route parent host",
|
|
|
247
334
|
});
|
|
248
335
|
|
|
249
336
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
250
|
-
assert.match(placementSource, /target: "
|
|
251
|
-
assert.match(placementSource, /
|
|
337
|
+
assert.match(placementSource, /target: "page\.section-nav"/);
|
|
338
|
+
assert.match(placementSource, /owner: "catalog-products"/);
|
|
339
|
+
assert.doesNotMatch(placementSource, /componentToken: "local\.main\.ui\.surface-aware-menu-link-item"/);
|
|
252
340
|
assert.match(placementSource, /to: "\.\/variants"/);
|
|
253
341
|
});
|
|
254
342
|
});
|
|
@@ -423,7 +511,7 @@ test("ui-generator page subcommand rejects invalid link placement before creatin
|
|
|
423
511
|
"link-placement": "missing:target"
|
|
424
512
|
}
|
|
425
513
|
}),
|
|
426
|
-
/ui-generator page option "placement" target "
|
|
514
|
+
/ui-generator page option "placement" must be a semantic target in "area.slot" format/
|
|
427
515
|
);
|
|
428
516
|
|
|
429
517
|
await assert.rejects(readFile(path.join(appRoot, toPagePath(targetFile)), "utf8"), /ENOENT/);
|
|
@@ -455,10 +543,10 @@ test("ui-generator page subcommand rejects invalid link placement before overwri
|
|
|
455
543
|
args: [targetFile],
|
|
456
544
|
options: {
|
|
457
545
|
force: "true",
|
|
458
|
-
"link-placement": "missing
|
|
546
|
+
"link-placement": "missing.target"
|
|
459
547
|
}
|
|
460
548
|
}),
|
|
461
|
-
/ui-generator page
|
|
549
|
+
/ui-generator page semantic placement "missing.target" is not declared/
|
|
462
550
|
);
|
|
463
551
|
|
|
464
552
|
const pageSource = await readFile(targetPath, "utf8");
|