@jskit-ai/ui-generator 0.1.17 → 0.1.19

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/ui-generator",
4
- version: "0.1.17",
4
+ version: "0.1.19",
5
5
  kind: "generator",
6
6
  description: "Create non-CRUD pages, reusable UI elements, and subpage hosts.",
7
7
  options: {
@@ -19,7 +19,7 @@ export default Object.freeze({
19
19
  validationType: "enabled-surface-id",
20
20
  defaultFromConfig: "surfaceDefaultId",
21
21
  promptLabel: "Target surface",
22
- promptHint: "Used by the placed-element subcommand. Must match an enabled surface id."
22
+ promptHint: "Optional. Used when JSKIT cannot infer the target surface from placement or app topology."
23
23
  },
24
24
  path: {
25
25
  required: false,
@@ -152,9 +152,11 @@ export default Object.freeze({
152
152
  export: "runGeneratorSubcommand",
153
153
  description: "Create a Vue component file under the chosen component directory (default: src/components) and add a placement entry that renders it.",
154
154
  optionNames: ["name", "surface", "path", "placement", "force"],
155
- requiredOptionNames: ["name", "surface"],
155
+ requiredOptionNames: ["name"],
156
156
  notes: [
157
157
  "If --placement is omitted, the placed element is added at shell-layout:top-right.",
158
+ "If the placement target belongs to a page-owned outlet, JSKIT infers the surface automatically.",
159
+ "If the target is shared and the app has multiple enabled surfaces, pass --surface explicitly.",
158
160
  "If the component file already exists, rerun with --force to overwrite it."
159
161
  ],
160
162
  examples: [
@@ -162,8 +164,7 @@ export default Object.freeze({
162
164
  label: "Common usage",
163
165
  lines: [
164
166
  "npx jskit generate ui-generator placed-element \\",
165
- " --name \"Alerts Widget\" \\",
166
- " --surface admin"
167
+ " --name \"Alerts Widget\""
167
168
  ]
168
169
  },
169
170
  {
@@ -277,7 +278,7 @@ export default Object.freeze({
277
278
  mutations: {
278
279
  dependencies: {
279
280
  runtime: {
280
- "@jskit-ai/users-web": "0.1.49"
281
+ "@jskit-ai/users-web": "0.1.51"
281
282
  },
282
283
  dev: {}
283
284
  },
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@jskit-ai/ui-generator",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/kernel": "0.1.34",
10
- "@jskit-ai/shell-web": "0.1.33"
9
+ "@jskit-ai/kernel": "0.1.36",
10
+ "@jskit-ai/shell-web": "0.1.35"
11
11
  },
12
12
  "exports": {
13
13
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -1,9 +1,12 @@
1
1
  import path from "node:path";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import {
4
+ listSurfacePageRoots,
5
+ resolveBestSurfaceMatchFromPageFile,
4
6
  resolveShellOutletPlacementTargetFromApp,
5
7
  toPosixPath
6
8
  } from "@jskit-ai/kernel/server/support";
9
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface";
7
10
  import { normalizeBoolean, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
8
11
  import {
9
12
  DEFAULT_COMPONENT_DIRECTORY,
@@ -31,6 +34,47 @@ function renderElementComponentSource(elementName = "") {
31
34
  `;
32
35
  }
33
36
 
37
+ async function resolvePlacedElementSurface({
38
+ appRoot,
39
+ placementTarget = {},
40
+ surface = "",
41
+ context = "ui-generator placed-element"
42
+ } = {}) {
43
+ const explicitSurface = normalizeSurfaceId(surface);
44
+ const inferredSurfaceMatch = await resolveBestSurfaceMatchFromPageFile(
45
+ String(placementTarget?.sourcePath || ""),
46
+ await listSurfacePageRoots(appRoot, { context }),
47
+ { context }
48
+ );
49
+ const inferredSurface = normalizeSurfaceId(inferredSurfaceMatch?.surfaceId);
50
+
51
+ if (explicitSurface) {
52
+ if (inferredSurface && explicitSurface !== inferredSurface) {
53
+ throw new Error(
54
+ `${context} target "${normalizeText(placementTarget?.id) || "<unknown>"}" belongs to surface "${inferredSurface}", ` +
55
+ `so --surface ${explicitSurface} is invalid.`
56
+ );
57
+ }
58
+ return explicitSurface;
59
+ }
60
+
61
+ if (inferredSurface) {
62
+ return inferredSurface;
63
+ }
64
+
65
+ const surfacePageRoots = await listSurfacePageRoots(appRoot, { context });
66
+ if (surfacePageRoots.length === 1) {
67
+ return normalizeSurfaceId(surfacePageRoots[0]?.id);
68
+ }
69
+
70
+ const targetId = normalizeText(placementTarget?.id) || "<unknown>";
71
+ const enabledSurfaceIds = surfacePageRoots.map((entry) => normalizeSurfaceId(entry?.id)).filter(Boolean);
72
+ throw new Error(
73
+ `${context} could not infer a surface for placement target "${targetId}". ` +
74
+ `Pass --surface explicitly. Enabled surfaces: ${enabledSurfaceIds.join(", ") || "<none>"}.`
75
+ );
76
+ }
77
+
34
78
  async function runGeneratorSubcommand({
35
79
  appRoot,
36
80
  subcommand = "",
@@ -50,7 +94,6 @@ async function runGeneratorSubcommand({
50
94
  });
51
95
 
52
96
  const name = requireOption(options, "name", { context: "ui-generator placed-element" });
53
- const surface = requireOption(options, "surface", { context: "ui-generator placed-element" }).toLowerCase();
54
97
  const componentDirectory = normalizeText(options.path) || DEFAULT_COMPONENT_DIRECTORY;
55
98
  const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
56
99
  ? normalizeBoolean(options.force)
@@ -77,6 +120,12 @@ async function runGeneratorSubcommand({
77
120
  context: "ui-generator",
78
121
  placement: options?.placement || DEFAULT_ELEMENT_PLACEMENT
79
122
  });
123
+ const surface = await resolvePlacedElementSurface({
124
+ appRoot,
125
+ placementTarget,
126
+ surface: options?.surface,
127
+ context: "ui-generator placed-element"
128
+ });
80
129
 
81
130
  const touchedFiles = new Set();
82
131
  const desiredComponentSource = renderElementComponentSource(name);
@@ -46,17 +46,8 @@ export default function getPlacements() {
46
46
  path.join(appRoot, "packages", "main", "src", "client", "providers", "MainClientProvider.js"),
47
47
  `const mainClientComponents = [];
48
48
 
49
- function registerMainClientComponent(componentToken, resolveComponent) {
50
- const token = String(componentToken || "").trim();
51
- if (!token || typeof resolveComponent !== "function") {
52
- return;
53
- }
54
- mainClientComponents.push(
55
- Object.freeze({
56
- token,
57
- resolveComponent
58
- })
59
- );
49
+ function registerMainClientComponent(token, resolveComponent) {
50
+ mainClientComponents.push({ token, resolveComponent });
60
51
  }
61
52
 
62
53
  class MainClientProvider {}
@@ -15,10 +15,27 @@ async function withTempApp(run) {
15
15
  }
16
16
 
17
17
  async function writeAppFixture(appRoot) {
18
+ await mkdir(path.join(appRoot, "config"), { recursive: true });
18
19
  await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
19
20
  await mkdir(path.join(appRoot, "src", "pages", "admin", "workspace", "settings"), { recursive: true });
20
21
  await mkdir(path.join(appRoot, "packages", "main", "src", "client", "providers"), { recursive: true });
21
22
 
23
+ await writeFile(
24
+ path.join(appRoot, "config", "public.js"),
25
+ `export const config = {
26
+ surfaceDefaultId: "admin",
27
+ surfaceDefinitions: {
28
+ admin: {
29
+ id: "admin",
30
+ pagesRoot: "admin/workspace",
31
+ enabled: true
32
+ }
33
+ }
34
+ };
35
+ `,
36
+ "utf8"
37
+ );
38
+
22
39
  await writeFile(
23
40
  path.join(appRoot, "src", "components", "ShellLayout.vue"),
24
41
  `<template>
@@ -59,17 +76,8 @@ export default function getPlacements() {
59
76
  path.join(appRoot, "packages", "main", "src", "client", "providers", "MainClientProvider.js"),
60
77
  `const mainClientComponents = [];
61
78
 
62
- function registerMainClientComponent(componentToken, resolveComponent) {
63
- const token = String(componentToken || "").trim();
64
- if (!token || typeof resolveComponent !== "function") {
65
- return;
66
- }
67
- mainClientComponents.push(
68
- Object.freeze({
69
- token,
70
- resolveComponent
71
- })
72
- );
79
+ function registerMainClientComponent(token, resolveComponent) {
80
+ mainClientComponents.push({ token, resolveComponent });
73
81
  }
74
82
 
75
83
  class MainClientProvider {}
@@ -132,6 +140,101 @@ test("ui-generator placed-element subcommand supports explicit placement overrid
132
140
  });
133
141
  });
134
142
 
143
+ test("ui-generator placed-element infers surface from a page-owned placement target", async () => {
144
+ await withTempApp(async (appRoot) => {
145
+ await writeAppFixture(appRoot);
146
+
147
+ await runGeneratorSubcommand({
148
+ appRoot,
149
+ subcommand: "placed-element",
150
+ options: {
151
+ name: "Ops Panel",
152
+ placement: "admin-settings:forms"
153
+ }
154
+ });
155
+
156
+ const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
157
+ assert.match(placementSource, /target: "admin-settings:forms"/);
158
+ assert.match(placementSource, /surfaces: \["admin"\]/);
159
+ });
160
+ });
161
+
162
+ test("ui-generator placed-element infers the only enabled surface for shared shell targets", async () => {
163
+ await withTempApp(async (appRoot) => {
164
+ await writeAppFixture(appRoot);
165
+
166
+ await runGeneratorSubcommand({
167
+ appRoot,
168
+ subcommand: "placed-element",
169
+ options: {
170
+ name: "Ops Panel"
171
+ }
172
+ });
173
+
174
+ const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
175
+ assert.match(placementSource, /target: "shell-layout:top-right"/);
176
+ assert.match(placementSource, /surfaces: \["admin"\]/);
177
+ });
178
+ });
179
+
180
+ test("ui-generator placed-element requires explicit surface when a shared shell target is ambiguous", async () => {
181
+ await withTempApp(async (appRoot) => {
182
+ await writeAppFixture(appRoot);
183
+ await writeFile(
184
+ path.join(appRoot, "config", "public.js"),
185
+ `export const config = {
186
+ surfaceDefaultId: "admin",
187
+ surfaceDefinitions: {
188
+ admin: {
189
+ id: "admin",
190
+ pagesRoot: "admin/workspace",
191
+ enabled: true
192
+ },
193
+ console: {
194
+ id: "console",
195
+ pagesRoot: "console",
196
+ enabled: true
197
+ }
198
+ }
199
+ };
200
+ `,
201
+ "utf8"
202
+ );
203
+
204
+ await assert.rejects(
205
+ () =>
206
+ runGeneratorSubcommand({
207
+ appRoot,
208
+ subcommand: "placed-element",
209
+ options: {
210
+ name: "Ops Panel"
211
+ }
212
+ }),
213
+ /could not infer a surface for placement target "shell-layout:top-right". Pass --surface explicitly/
214
+ );
215
+ });
216
+ });
217
+
218
+ test("ui-generator placed-element rejects explicit surfaces that conflict with page-owned targets", async () => {
219
+ await withTempApp(async (appRoot) => {
220
+ await writeAppFixture(appRoot);
221
+
222
+ await assert.rejects(
223
+ () =>
224
+ runGeneratorSubcommand({
225
+ appRoot,
226
+ subcommand: "placed-element",
227
+ options: {
228
+ name: "Ops Panel",
229
+ placement: "admin-settings:forms",
230
+ surface: "console"
231
+ }
232
+ }),
233
+ /target "admin-settings:forms" belongs to surface "admin", so --surface console is invalid/
234
+ );
235
+ });
236
+ });
237
+
135
238
  test("ui-generator placed-element subcommand refuses to overwrite an existing component without force", async () => {
136
239
  await withTempApp(async (appRoot) => {
137
240
  await writeAppFixture(appRoot);
@@ -6,5 +6,6 @@ test("ui-generator surface options validate against enabled surface ids", () =>
6
6
  assert.equal(descriptor.kind, "generator");
7
7
  assert.equal(descriptor.options?.surface?.validationType, "enabled-surface-id");
8
8
  assert.equal(descriptor.metadata?.generatorSubcommands?.["placed-element"]?.optionNames?.includes("surface"), true);
9
+ assert.equal(descriptor.metadata?.generatorSubcommands?.["placed-element"]?.requiredOptionNames?.includes("surface"), false);
9
10
  assert.equal(descriptor.metadata?.generatorSubcommands?.page?.optionNames?.includes("force"), true);
10
11
  });