@jskit-ai/ui-generator 0.1.15 → 0.1.17

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.
@@ -38,8 +38,12 @@ async function writeAppFixture(appRoot, { configSource = "" } = {}) {
38
38
  path.join(appRoot, "src", "components", "ShellLayout.vue"),
39
39
  `<template>
40
40
  <div>
41
- <ShellOutlet host="shell-layout" position="primary-menu" default />
42
- <ShellOutlet host="shell-layout" position="top-right" />
41
+ <ShellOutlet
42
+ target="shell-layout:primary-menu"
43
+ default
44
+ default-link-component-token="local.main.ui.surface-aware-menu-link-item"
45
+ />
46
+ <ShellOutlet target="shell-layout:top-right" />
43
47
  </div>
44
48
  </template>
45
49
  `,
@@ -73,6 +77,7 @@ test("ui-generator page subcommand creates an index page from an explicit target
73
77
  });
74
78
 
75
79
  assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
80
+ assert.equal(result.summary, 'Generated UI page "/practice" at src/pages/w/[workspaceSlug]/admin/practice/index.vue.');
76
81
 
77
82
  const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
78
83
  assert.match(pageSource, /<h1 class="text-h5 mb-2">Practice<\/h1>/);
@@ -108,6 +113,36 @@ test("ui-generator page subcommand creates a file route and derives label from t
108
113
  });
109
114
  });
110
115
 
116
+ test("ui-generator page subcommand omits the auth guard for public surface links", async () => {
117
+ await withTempApp(async (appRoot) => {
118
+ await writeAppFixture(appRoot, {
119
+ configSource: `export const config = {
120
+ surfaceAccessPolicies: {
121
+ public: {}
122
+ },
123
+ surfaceDefinitions: {
124
+ home: { id: "home", pagesRoot: "home", enabled: true, accessPolicyId: "public" }
125
+ }
126
+ };
127
+ `
128
+ });
129
+
130
+ const targetFile = "home/reports/index.vue";
131
+ await runGeneratorSubcommand({
132
+ appRoot,
133
+ subcommand: "page",
134
+ args: [targetFile],
135
+ options: {
136
+ name: "Reports"
137
+ }
138
+ });
139
+
140
+ const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
141
+ assert.match(placementSource, /id: "ui-generator\.page\.home\.reports\.link"/);
142
+ assert.doesNotMatch(placementSource, /when: \(\{ auth \}\) => Boolean\(auth\?\.authenticated\)/);
143
+ });
144
+ });
145
+
111
146
  test("ui-generator page subcommand supports link placement options", async () => {
112
147
  await withTempApp(async (appRoot) => {
113
148
  await writeAppFixture(appRoot);
@@ -125,8 +160,7 @@ test("ui-generator page subcommand supports link placement options", async () =>
125
160
  });
126
161
 
127
162
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
128
- assert.match(placementSource, /host: "shell-layout"/);
129
- assert.match(placementSource, /position: "top-right"/);
163
+ assert.match(placementSource, /target: "shell-layout:top-right"/);
130
164
  assert.match(placementSource, /componentToken: "local\.main\.ui\.tab-link-item"/);
131
165
  assert.match(placementSource, /to: "\.\/notes"/);
132
166
  });
@@ -143,7 +177,7 @@ test("ui-generator page subcommand infers subpage link placement, tab token, and
143
177
  `<template>
144
178
  <SectionContainerShell>
145
179
  <template #tabs>
146
- <ShellOutlet host="contact-view" position="sub-pages" />
180
+ <ShellOutlet target="contact-view:sub-pages" />
147
181
  </template>
148
182
  <RouterView />
149
183
  </SectionContainerShell>
@@ -161,8 +195,7 @@ test("ui-generator page subcommand infers subpage link placement, tab token, and
161
195
  });
162
196
 
163
197
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
164
- assert.match(placementSource, /host: "contact-view"/);
165
- assert.match(placementSource, /position: "sub-pages"/);
198
+ assert.match(placementSource, /target: "contact-view:sub-pages"/);
166
199
  assert.match(placementSource, /componentToken: "local\.main\.ui\.tab-link-item"/);
167
200
  assert.match(placementSource, /to: "\.\/notes"/);
168
201
  });
@@ -180,7 +213,7 @@ test("ui-generator page subcommand prefers the nearest index-route parent host",
180
213
  `<template>
181
214
  <SectionContainerShell>
182
215
  <template #tabs>
183
- <ShellOutlet host="catalog" position="sub-pages" />
216
+ <ShellOutlet target="catalog:sub-pages" />
184
217
  </template>
185
218
  <RouterView />
186
219
  </SectionContainerShell>
@@ -193,7 +226,7 @@ test("ui-generator page subcommand prefers the nearest index-route parent host",
193
226
  `<template>
194
227
  <SectionContainerShell>
195
228
  <template #tabs>
196
- <ShellOutlet host="catalog-products" position="sub-pages" />
229
+ <ShellOutlet target="catalog-products:sub-pages" />
197
230
  </template>
198
231
  <RouterView />
199
232
  </SectionContainerShell>
@@ -212,8 +245,7 @@ test("ui-generator page subcommand prefers the nearest index-route parent host",
212
245
  });
213
246
 
214
247
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
215
- assert.match(placementSource, /host: "catalog-products"/);
216
- assert.match(placementSource, /position: "sub-pages"/);
248
+ assert.match(placementSource, /target: "catalog-products:sub-pages"/);
217
249
  assert.match(placementSource, /componentToken: "local\.main\.ui\.tab-link-item"/);
218
250
  assert.match(placementSource, /to: "\.\/variants"/);
219
251
  });
@@ -265,7 +297,28 @@ test("ui-generator page subcommand rejects unsupported options", async () => {
265
297
  });
266
298
  });
267
299
 
268
- test("ui-generator page subcommand rejects target files with a src/pages prefix", async () => {
300
+ test("ui-generator page subcommand accepts target files with a src/pages prefix", async () => {
301
+ await withTempApp(async (appRoot) => {
302
+ await writeAppFixture(appRoot);
303
+
304
+ const targetFile = "src/pages/w/[workspaceSlug]/admin/practice/index.vue";
305
+ const result = await runGeneratorSubcommand({
306
+ appRoot,
307
+ subcommand: "page",
308
+ args: [targetFile],
309
+ options: {}
310
+ });
311
+
312
+ assert.deepEqual(result.touchedFiles, [targetFile, "src/placement.js"]);
313
+ const pageSource = await readFile(path.join(appRoot, targetFile), "utf8");
314
+ assert.match(pageSource, /Practice/);
315
+ const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
316
+ assert.match(placementSource, /id: "ui-generator\.page\.admin\.practice\.link"/);
317
+ assert.match(placementSource, /workspaceSuffix: "\/practice"/);
318
+ });
319
+ });
320
+
321
+ test("ui-generator page subcommand still rejects non-page src targets", async () => {
269
322
  await withTempApp(async (appRoot) => {
270
323
  await writeAppFixture(appRoot);
271
324
 
@@ -273,10 +326,10 @@ test("ui-generator page subcommand rejects target files with a src/pages prefix"
273
326
  runGeneratorSubcommand({
274
327
  appRoot,
275
328
  subcommand: "page",
276
- args: ["src/pages/w/[workspaceSlug]/admin/practice/index.vue"],
329
+ args: ["src/components/PracticePanel.vue"],
277
330
  options: {}
278
331
  }),
279
- /must be relative to src\/pages\/, without the src\/pages\/ prefix/
332
+ /must be relative to src\/pages\/ or start with src\/pages\/:/
280
333
  );
281
334
  });
282
335
  });
@@ -350,3 +403,65 @@ test("ui-generator page subcommand overwrites an existing page when --force is p
350
403
  assert.doesNotMatch(pageSource, /custom practice page/);
351
404
  });
352
405
  });
406
+
407
+ test("ui-generator page subcommand rejects invalid link placement before creating a new page", async () => {
408
+ await withTempApp(async (appRoot) => {
409
+ await writeAppFixture(appRoot);
410
+
411
+ const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
412
+ const placementPath = path.join(appRoot, "src", "placement.js");
413
+ const originalPlacementSource = await readFile(placementPath, "utf8");
414
+
415
+ await assert.rejects(
416
+ runGeneratorSubcommand({
417
+ appRoot,
418
+ subcommand: "page",
419
+ args: [targetFile],
420
+ options: {
421
+ "link-placement": "missing:target"
422
+ }
423
+ }),
424
+ /ui-generator page option "placement" target "missing:target" is not declared/
425
+ );
426
+
427
+ await assert.rejects(readFile(path.join(appRoot, toPagePath(targetFile)), "utf8"), /ENOENT/);
428
+ const placementSource = await readFile(placementPath, "utf8");
429
+ assert.equal(placementSource, originalPlacementSource);
430
+ });
431
+ });
432
+
433
+ test("ui-generator page subcommand rejects invalid link placement before overwriting an existing page", async () => {
434
+ await withTempApp(async (appRoot) => {
435
+ await writeAppFixture(appRoot);
436
+
437
+ const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
438
+ const targetPath = path.join(appRoot, toPagePath(targetFile));
439
+ const originalPageSource = `<template>
440
+ <div>custom practice page</div>
441
+ </template>
442
+ `;
443
+ const placementPath = path.join(appRoot, "src", "placement.js");
444
+ const originalPlacementSource = await readFile(placementPath, "utf8");
445
+
446
+ await mkdir(path.dirname(targetPath), { recursive: true });
447
+ await writeFile(targetPath, originalPageSource, "utf8");
448
+
449
+ await assert.rejects(
450
+ runGeneratorSubcommand({
451
+ appRoot,
452
+ subcommand: "page",
453
+ args: [targetFile],
454
+ options: {
455
+ force: "true",
456
+ "link-placement": "missing:target"
457
+ }
458
+ }),
459
+ /ui-generator page option "placement" target "missing:target" is not declared/
460
+ );
461
+
462
+ const pageSource = await readFile(targetPath, "utf8");
463
+ assert.equal(pageSource, originalPageSource);
464
+ const placementSource = await readFile(placementPath, "utf8");
465
+ assert.equal(placementSource, originalPlacementSource);
466
+ });
467
+ });
package/README.md DELETED
@@ -1,214 +0,0 @@
1
- # @jskit-ai/ui-generator
2
-
3
- Generate app-local UI pages, page links, placed elements, and routed subpage hosts for JSKIT apps.
4
-
5
- ## Quick Start
6
-
7
- List available placement targets in the current app:
8
-
9
- ```bash
10
- npx jskit list placements
11
- ```
12
-
13
- Create a normal page at an explicit file:
14
-
15
- ```bash
16
- npx jskit generate @jskit-ai/ui-generator page \
17
- admin/reports-dashboard/index.vue \
18
- --name "Reports Dashboard"
19
- ```
20
-
21
- Create a file-route page:
22
-
23
- ```bash
24
- npx jskit generate @jskit-ai/ui-generator page \
25
- admin/contacts/[contactId].vue \
26
- --name "Contact"
27
- ```
28
-
29
- Upgrade an existing page into a routed subpage host:
30
-
31
- ```bash
32
- npx jskit generate @jskit-ai/ui-generator add-subpages \
33
- admin/contacts/[contactId].vue \
34
- --title "Contact" \
35
- --subtitle "Manage contact modules."
36
- ```
37
-
38
- Create a reusable placed element:
39
-
40
- ```bash
41
- npx jskit generate @jskit-ai/ui-generator placed-element \
42
- --name "Alerts Widget" \
43
- --surface admin
44
- ```
45
-
46
- Inject a plain generic outlet into an existing Vue file:
47
-
48
- ```bash
49
- npx jskit generate @jskit-ai/ui-generator outlet \
50
- src/components/ContactSummaryCard.vue \
51
- --target contact-view:summary-actions
52
- ```
53
-
54
- ## Commands
55
-
56
- - `page <target-file>`: create a route page at that exact file relative to `src/pages/` and add a link placement entry for it.
57
- - `add-subpages <target-file>`: upgrade an existing page relative to `src/pages/` into the standard `SectionContainerShell + ShellOutlet + RouterView` host shape.
58
- - `placed-element`: create a reusable component and register a placement for it.
59
- - `outlet <target-file>`: inject a plain `ShellOutlet` into an existing Vue SFC.
60
-
61
- For `placed-element`, the default placement target is `shell-layout:top-right`.
62
- Use `--placement host:position` to override it.
63
-
64
- ## The Mental Model
65
-
66
- `page` and `add-subpages` operate on explicit page files relative to `src/pages/`. `outlet` still targets an explicit Vue file path relative to the app root.
67
-
68
- That means:
69
-
70
- - `catalog/index.vue` is obviously an index-route page
71
- - `catalog.vue` is obviously a file-route page
72
- - there is no extra route-shape flag to remember
73
- - the owning surface is derived from where the file lives
74
-
75
- This is the reference model for JSKIT page-producing generators.
76
-
77
- - `@jskit-ai/ui-generator page <target-file>` works from an explicit page file relative to `src/pages/`.
78
- - `@jskit-ai/crud-ui-generator crud <target-root>` works from an explicit route root relative to `src/pages/`.
79
- - `@jskit-ai/assistant page <target-file>` and `settings-page <target-file>` work from explicit page files relative to `src/pages/`.
80
-
81
- ## Page Links
82
-
83
- `page` creates a page file and appends a link placement block for it.
84
-
85
- These options control that generated page link:
86
-
87
- - `--link-placement`: where the page link renders
88
- - `--link-component-token`: how the page link is rendered
89
- - `--link-to`: explicit `props.to` override for the page link
90
-
91
- This is intentionally separate from `placed-element`, which still uses `--placement` because it places arbitrary UI, not a page link.
92
-
93
- ## Routed Subpages
94
-
95
- `add-subpages` is the only routed-subpages command.
96
-
97
- It upgrades an existing page so the page itself owns:
98
-
99
- - `SectionContainerShell`
100
- - `ShellOutlet host="..." position="sub-pages"`
101
- - `RouterView`
102
-
103
- `--target` controls that outlet target:
104
-
105
- - if omitted, the target is derived from the page path
106
- - `--target contact-view` means `contact-view:sub-pages`
107
- - `--target contact-view:secondary-tabs` uses an explicit custom position
108
-
109
- Derived target examples:
110
-
111
- - `src/pages/admin/catalog/index.vue` -> `catalog:sub-pages`
112
- - `src/pages/admin/catalog.vue` -> `catalog:sub-pages`
113
- - `src/pages/admin/contacts/[contactId].vue` -> `contacts-contact-id:sub-pages`
114
- - `src/pages/admin/catalog/products/index.vue` -> `catalog-products:sub-pages`
115
-
116
- If the page already contains a `RouterView`, `add-subpages` fails instead of trying to update an existing routed host.
117
-
118
- It also ensures the shared support scaffold exists:
119
-
120
- - `src/components/SectionContainerShell.vue`
121
- - `src/components/TabLinkItem.vue`
122
- - `packages/main/src/client/providers/MainClientProvider.js` registration for `local.main.ui.tab-link-item`
123
-
124
- ## Child Route Placement Rule
125
-
126
- Child routes attach differently depending on the parent page file shape.
127
-
128
- If the parent is a file route:
129
-
130
- - parent: `src/pages/admin/catalog.vue`
131
- - child pages go under: `src/pages/admin/catalog/...`
132
- - example child page: `src/pages/admin/catalog/products/index.vue`
133
-
134
- If the parent is an index route:
135
-
136
- - parent: `src/pages/admin/catalog/index.vue`
137
- - child pages go under: `src/pages/admin/catalog/index/...`
138
- - example child page: `src/pages/admin/catalog/index/products/index.vue`
139
-
140
- That `index/...` folder shape is the native file-based routing rule for nesting children under an `index.vue` page.
141
-
142
- ## Example: File Route Parent
143
-
144
- Create the parent page:
145
-
146
- ```bash
147
- npx jskit generate @jskit-ai/ui-generator page \
148
- admin/contacts/[contactId].vue \
149
- --name "Contact"
150
- ```
151
-
152
- Upgrade it to host subpages:
153
-
154
- ```bash
155
- npx jskit generate @jskit-ai/ui-generator add-subpages \
156
- admin/contacts/[contactId].vue \
157
- --title "Contact" \
158
- --subtitle "Manage contact modules."
159
- ```
160
-
161
- Generate a child page link inside that host:
162
-
163
- ```bash
164
- npx jskit generate @jskit-ai/ui-generator page \
165
- admin/contacts/[contactId]/notes/index.vue \
166
- --name "Notes"
167
- ```
168
-
169
- ## Example: Index Route Parent
170
-
171
- Create the parent page:
172
-
173
- ```bash
174
- npx jskit generate @jskit-ai/ui-generator page \
175
- admin/catalog/index.vue \
176
- --name "Catalog"
177
- ```
178
-
179
- Upgrade it to host subpages:
180
-
181
- ```bash
182
- npx jskit generate @jskit-ai/ui-generator add-subpages \
183
- admin/catalog/index.vue \
184
- --title "Catalog"
185
- ```
186
-
187
- Because the parent page is `index.vue`, nested child pages live under the matching `index/` folder.
188
-
189
- Generate a child page link inside that host:
190
-
191
- ```bash
192
- npx jskit generate @jskit-ai/ui-generator page \
193
- admin/catalog/index/products/index.vue \
194
- --name "Products"
195
- ```
196
-
197
- When `page` finds the nearest parent page upgraded with `add-subpages`, it reuses that parent’s real outlet target, defaults the link renderer to `local.main.ui.tab-link-item`, and derives `to` from the child route automatically.
198
-
199
- ## Generic Outlet Injection
200
-
201
- `outlet` is intentionally small.
202
-
203
- It only adds:
204
-
205
- - `import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";`
206
- - `<ShellOutlet host="..." position="..." />`
207
-
208
- It does not:
209
-
210
- - add `RouterView`
211
- - add `SectionContainerShell`
212
- - add routed subpage scaffolding
213
-
214
- Use `add-subpages` when the goal is routed child pages inside a page.