@jskit-ai/crud-ui-generator 0.1.84 → 0.1.86

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.
@@ -3,7 +3,7 @@ import { GENERATED_UI_NAVIGATION_ROLE_OPTION } from "@jskit-ai/kernel/shared/sup
3
3
  export default Object.freeze({
4
4
  packageVersion: 1,
5
5
  packageId: "@jskit-ai/crud-ui-generator",
6
- version: "0.1.84",
6
+ version: "0.1.86",
7
7
  kind: "generator",
8
8
  description: "Generate CRUD route trees from resource validators at an explicit route root relative to src/pages/.",
9
9
  options: {
@@ -175,7 +175,7 @@ export default Object.freeze({
175
175
  mutations: {
176
176
  dependencies: {
177
177
  runtime: {
178
- "@jskit-ai/users-web": "0.1.116"
178
+ "@jskit-ai/users-web": "0.1.118"
179
179
  },
180
180
  dev: {}
181
181
  },
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-ui-generator",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/crud-core": "0.1.109",
10
- "@jskit-ai/kernel": "0.1.102",
11
- "@jskit-ai/resource-crud-core": "0.1.46"
9
+ "@jskit-ai/crud-core": "0.1.111",
10
+ "@jskit-ai/kernel": "0.1.104",
11
+ "@jskit-ai/resource-crud-core": "0.1.48"
12
12
  },
13
13
  "exports": {
14
14
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -30,7 +30,7 @@ import {
30
30
  buildViewColumns,
31
31
  buildFormColumns,
32
32
  resolveRecordIdFieldKey,
33
- renderObjectPushLines,
33
+ renderObjectArrayEntryLines,
34
34
  resolveRecordChangedEventName,
35
35
  resolveRecordIdExpression
36
36
  } from "./resourceSupport.js";
@@ -864,8 +864,8 @@ async function buildUiTemplateContext({ appRoot, options } = {}) {
864
864
  __JSKIT_UI_SHARED_FORM_SLOT_PROPS__: buildCrudFieldsSlotProps(sharedFormFields, { includeMode: true }),
865
865
  __JSKIT_UI_CREATE_FORM_FIELDS__: JSON.stringify(createFields),
866
866
  __JSKIT_UI_EDIT_FORM_FIELDS__: JSON.stringify(editFields),
867
- __JSKIT_UI_CREATE_FORM_FIELD_PUSH_LINES__: renderObjectPushLines("UI_CREATE_FORM_FIELDS", createFields),
868
- __JSKIT_UI_EDIT_FORM_FIELD_PUSH_LINES__: renderObjectPushLines("UI_EDIT_FORM_FIELDS", editFields),
867
+ __JSKIT_UI_CREATE_FORM_FIELD_ARRAY_ENTRIES__: renderObjectArrayEntryLines(createFields),
868
+ __JSKIT_UI_EDIT_FORM_FIELD_ARRAY_ENTRIES__: renderObjectArrayEntryLines(editFields),
869
869
  __JSKIT_UI_CREATE_LOOKUP_IMPORT_LINE__: buildLookupImportLine(createFields),
870
870
  __JSKIT_UI_EDIT_LOOKUP_IMPORT_LINE__: buildLookupImportLine(editFields),
871
871
  __JSKIT_UI_CREATE_LOOKUP_RUNTIME_SETUP__: buildLookupRuntimeSetup(createFields, {
@@ -18,6 +18,7 @@ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
18
18
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
19
19
 
20
20
  const JS_IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
21
+ const JS_IDENTIFIER_PROPERTY_PATTERN = /^(\s*)"([A-Za-z_$][A-Za-z0-9_$]*)":/gm;
21
22
  const JSON_REST_TRANSPORT_EXTENSION_KEY = "x-json-rest-schema";
22
23
 
23
24
  function requireOption(options, optionName, { context = "ui-generator" } = {}) {
@@ -1045,21 +1046,22 @@ function resolveRecordIdFieldKey(fields = []) {
1045
1046
  return normalizeText(preferred?.key) || "id";
1046
1047
  }
1047
1048
 
1048
- function renderObjectPushLines(arrayName, entries = []) {
1049
- const normalizedArrayName = normalizeText(arrayName);
1050
- if (!normalizedArrayName) {
1051
- return "";
1052
- }
1053
-
1049
+ function renderObjectArrayEntryLines(entries = [], { indent = " " } = {}) {
1050
+ const entryIndent = String(indent || "");
1054
1051
  const lines = [];
1055
1052
  for (const entry of Array.isArray(entries) ? entries : []) {
1056
1053
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1057
1054
  continue;
1058
1055
  }
1059
- lines.push(`${normalizedArrayName}.push(${JSON.stringify(entry, null, 2)});`);
1056
+ const entrySource = JSON.stringify(entry, null, 2)
1057
+ .replace(JS_IDENTIFIER_PROPERTY_PATTERN, "$1$2:")
1058
+ .split("\n")
1059
+ .map((line) => `${entryIndent}${line}`)
1060
+ .join("\n");
1061
+ lines.push(`${entrySource},`);
1060
1062
  }
1061
1063
 
1062
- return lines.join("\n\n");
1064
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
1063
1065
  }
1064
1066
 
1065
1067
  function resolveRecordIdExpression(fields = []) {
@@ -1088,7 +1090,7 @@ export {
1088
1090
  buildViewColumns,
1089
1091
  buildFormColumns,
1090
1092
  resolveRecordIdFieldKey,
1091
- renderObjectPushLines,
1093
+ renderObjectArrayEntryLines,
1092
1094
  resolveCrudRecordChangedEvent as resolveRecordChangedEventName,
1093
1095
  resolveRecordIdExpression
1094
1096
  };
@@ -16,7 +16,7 @@ import {
16
16
  buildListRowColumns,
17
17
  buildViewColumns,
18
18
  buildFormColumns,
19
- renderObjectPushLines
19
+ renderObjectArrayEntryLines
20
20
  } from "../resourceSupport.js";
21
21
 
22
22
  const SUPPORTED_OPERATIONS = new Set(["list", "view", "new", "edit"]);
@@ -217,6 +217,126 @@ function insertBeforeAnchor(source, { anchor = "", snippet = "" } = {}) {
217
217
  };
218
218
  }
219
219
 
220
+ function findMatchingDelimiter(sourceText = "", openIndex = -1, openChar = "[", closeChar = "]") {
221
+ const source = String(sourceText || "");
222
+ if (openIndex < 0 || source[openIndex] !== openChar) {
223
+ return -1;
224
+ }
225
+
226
+ let depth = 0;
227
+ let quote = "";
228
+ let inLineComment = false;
229
+ let inBlockComment = false;
230
+ for (let index = openIndex; index < source.length; index += 1) {
231
+ const char = source[index];
232
+ const nextChar = source[index + 1] || "";
233
+ const previousChar = source[index - 1] || "";
234
+
235
+ if (inLineComment) {
236
+ if (char === "\n") {
237
+ inLineComment = false;
238
+ }
239
+ continue;
240
+ }
241
+ if (inBlockComment) {
242
+ if (char === "*" && nextChar === "/") {
243
+ inBlockComment = false;
244
+ index += 1;
245
+ }
246
+ continue;
247
+ }
248
+ if (quote) {
249
+ if (char === "\\" && quote !== "`") {
250
+ index += 1;
251
+ continue;
252
+ }
253
+ if (char === quote && previousChar !== "\\") {
254
+ quote = "";
255
+ }
256
+ continue;
257
+ }
258
+
259
+ if (char === "/" && nextChar === "/") {
260
+ inLineComment = true;
261
+ index += 1;
262
+ continue;
263
+ }
264
+ if (char === "/" && nextChar === "*") {
265
+ inBlockComment = true;
266
+ index += 1;
267
+ continue;
268
+ }
269
+ if (char === "\"" || char === "'" || char === "`") {
270
+ quote = char;
271
+ continue;
272
+ }
273
+ if (char === openChar) {
274
+ depth += 1;
275
+ continue;
276
+ }
277
+ if (char === closeChar) {
278
+ depth -= 1;
279
+ if (depth === 0) {
280
+ return index;
281
+ }
282
+ }
283
+ }
284
+
285
+ return -1;
286
+ }
287
+
288
+ function resolveAnchorLineIndex(sourceText = "", anchor = "") {
289
+ const escapedAnchor = String(anchor || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
290
+ const anchorLinePattern = new RegExp(`^[ \\t]*${escapedAnchor}[ \\t]*$`, "m");
291
+ const anchorLineMatch = anchorLinePattern.exec(String(sourceText || ""));
292
+ return Number(anchorLineMatch?.index ?? -1);
293
+ }
294
+
295
+ function findArrayDeclarationBeforeIndex(sourceText = "", arrayName = "", index = -1) {
296
+ const source = String(sourceText || "");
297
+ const resolvedIndex = Number.isInteger(index) ? index : -1;
298
+ if (resolvedIndex < 0) {
299
+ return null;
300
+ }
301
+
302
+ const declarationPattern = new RegExp(`\\b(?:const|let)\\s+${arrayName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*\\[`, "g");
303
+ let match = null;
304
+ let selected = null;
305
+ while ((match = declarationPattern.exec(source)) != null) {
306
+ if (match.index > resolvedIndex) {
307
+ break;
308
+ }
309
+ selected = match;
310
+ }
311
+ if (!selected) {
312
+ return null;
313
+ }
314
+
315
+ const openIndex = source.indexOf("[", selected.index);
316
+ const closeIndex = findMatchingDelimiter(source, openIndex, "[", "]");
317
+ if (closeIndex < 0) {
318
+ return null;
319
+ }
320
+
321
+ return {
322
+ openIndex,
323
+ closeIndex
324
+ };
325
+ }
326
+
327
+ function insertFormFieldDefinition(source, insertion = {}) {
328
+ const anchorIndex = resolveAnchorLineIndex(source, insertion.anchor);
329
+ const declaration = findArrayDeclarationBeforeIndex(source, insertion.arrayName, anchorIndex);
330
+ if (!declaration || anchorIndex <= declaration.openIndex || anchorIndex >= declaration.closeIndex) {
331
+ throw new Error(
332
+ `crud-ui-generator field found legacy form-field marker layout for ${insertion.arrayName}. ` +
333
+ "Run `jskit app migrate-source-mutations` before adding more generated form fields."
334
+ );
335
+ }
336
+
337
+ return insertBeforeAnchor(source, insertion);
338
+ }
339
+
220
340
  function resolveAnchorScopeStart(source = "", { anchorIndex = -1, anchor = "" } = {}) {
221
341
  const sourceText = String(source || "");
222
342
  const resolvedAnchorIndex = Number.isInteger(anchorIndex) ? anchorIndex : -1;
@@ -284,7 +404,8 @@ function buildAnchorInsertions(operationName, field) {
284
404
  {
285
405
  targetKind: "form-fields",
286
406
  anchor: "// jskit:crud-ui-form-fields:new",
287
- snippet: renderObjectPushLines("UI_CREATE_FORM_FIELDS", [field])
407
+ arrayName: "UI_CREATE_FORM_FIELDS",
408
+ snippet: renderObjectArrayEntryLines([field])
288
409
  }
289
410
  ];
290
411
  }
@@ -298,7 +419,8 @@ function buildAnchorInsertions(operationName, field) {
298
419
  {
299
420
  targetKind: "form-fields",
300
421
  anchor: "// jskit:crud-ui-form-fields:edit",
301
- snippet: renderObjectPushLines("UI_EDIT_FORM_FIELDS", [field])
422
+ arrayName: "UI_EDIT_FORM_FIELDS",
423
+ snippet: renderObjectArrayEntryLines([field])
302
424
  }
303
425
  ];
304
426
  }
@@ -426,7 +548,9 @@ async function runGeneratorSubcommand({
426
548
  fileStates.set(targetFile.absolutePath, state);
427
549
  }
428
550
 
429
- const applied = insertBeforeAnchor(state.source, insertion);
551
+ const applied = insertion.targetKind === "form-fields"
552
+ ? insertFormFieldDefinition(state.source, insertion)
553
+ : insertBeforeAnchor(state.source, insertion);
430
554
  state.source = applied.content;
431
555
  state.changed = state.changed || applied.changed;
432
556
  changed = changed || applied.changed;
@@ -1,17 +1,17 @@
1
- const UI_CREATE_FORM_FIELDS = [];
1
+ const UI_CREATE_FORM_FIELDS = [
2
+ __JSKIT_UI_CREATE_FORM_FIELD_ARRAY_ENTRIES__ // jskit:crud-ui-form-fields:new
3
+ ];
2
4
 
3
5
  // @jskit-contract crud.ui.form-fields.__JSKIT_UI_RESOURCE_NAMESPACE__.new.v1
4
6
  void UI_CREATE_FORM_FIELDS;
5
- // jskit:crud-ui-form-fields:new
6
- __JSKIT_UI_CREATE_FORM_FIELD_PUSH_LINES__
7
7
  Object.freeze(UI_CREATE_FORM_FIELDS);
8
8
 
9
- const UI_EDIT_FORM_FIELDS = [];
9
+ const UI_EDIT_FORM_FIELDS = [
10
+ __JSKIT_UI_EDIT_FORM_FIELD_ARRAY_ENTRIES__ // jskit:crud-ui-form-fields:edit
11
+ ];
10
12
 
11
13
  // @jskit-contract crud.ui.form-fields.__JSKIT_UI_RESOURCE_NAMESPACE__.edit.v1
12
14
  void UI_EDIT_FORM_FIELDS;
13
- // jskit:crud-ui-form-fields:edit
14
- __JSKIT_UI_EDIT_FORM_FIELD_PUSH_LINES__
15
15
  Object.freeze(UI_EDIT_FORM_FIELDS);
16
16
 
17
17
  export { UI_CREATE_FORM_FIELDS, UI_EDIT_FORM_FIELDS };
@@ -27,12 +27,12 @@ const UI_LIST_URL = __JSKIT_UI_EDIT_PAGE_LIST_URL__;
27
27
  const UI_VIEW_URL = __JSKIT_UI_EDIT_PAGE_VIEW_URL__;
28
28
  const UI_CANCEL_URL = UI_VIEW_URL || UI_LIST_URL;
29
29
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
30
- const UI_EDIT_FORM_FIELDS = [];
30
+ const UI_EDIT_FORM_FIELDS = [
31
+ __JSKIT_UI_EDIT_FORM_FIELD_ARRAY_ENTRIES__ // jskit:crud-ui-form-fields:edit
32
+ ];
31
33
 
32
34
  // @jskit-contract crud.ui.form-fields.__JSKIT_UI_RESOURCE_NAMESPACE__.edit.v1
33
35
  void UI_EDIT_FORM_FIELDS;
34
- // jskit:crud-ui-form-fields:edit
35
- __JSKIT_UI_EDIT_FORM_FIELD_PUSH_LINES__
36
36
  Object.freeze(UI_EDIT_FORM_FIELDS);
37
37
 
38
38
  const route = useRoute();
@@ -23,12 +23,12 @@ const UI_CREATE_API_URL = "__JSKIT_UI_API_BASE_URL__";
23
23
  const UI_LIST_URL = __JSKIT_UI_NEW_PAGE_LIST_URL__;
24
24
  const UI_VIEW_URL = __JSKIT_UI_NEW_PAGE_VIEW_URL__;
25
25
  const UI_RECORD_CHANGED_EVENT = __JSKIT_UI_RECORD_CHANGED_EVENT__;
26
- const UI_CREATE_FORM_FIELDS = [];
26
+ const UI_CREATE_FORM_FIELDS = [
27
+ __JSKIT_UI_CREATE_FORM_FIELD_ARRAY_ENTRIES__ // jskit:crud-ui-form-fields:new
28
+ ];
27
29
 
28
30
  // @jskit-contract crud.ui.form-fields.__JSKIT_UI_RESOURCE_NAMESPACE__.new.v1
29
31
  void UI_CREATE_FORM_FIELDS;
30
- // jskit:crud-ui-form-fields:new
31
- __JSKIT_UI_CREATE_FORM_FIELD_PUSH_LINES__
32
32
  Object.freeze(UI_CREATE_FORM_FIELDS);
33
33
 
34
34
  __JSKIT_UI_CREATE_LOOKUP_RUNTIME_SETUP__
@@ -104,9 +104,10 @@ test("field patches edit screen using resource metadata and anchors", async () =
104
104
  </template>
105
105
  <script setup>
106
106
  import { resource as uiResource } from "/packages/contacts/src/shared/contactResource.js";
107
- const UI_EDIT_FORM_FIELDS = [];
108
- // jskit:crud-ui-form-fields:edit
109
- UI_EDIT_FORM_FIELDS.push({ key: "firstName", component: "text" });
107
+ const UI_EDIT_FORM_FIELDS = [
108
+ { key: "firstName", component: "text" },
109
+ // jskit:crud-ui-form-fields:edit
110
+ ];
110
111
  </script>
111
112
  `
112
113
  );
@@ -132,7 +133,8 @@ UI_EDIT_FORM_FIELDS.push({ key: "firstName", component: "text" });
132
133
  assert.match(editSource, /:search="fieldLookupSearch\('vetId'\)"/);
133
134
  assert.match(editSource, /@update:search="setFieldLookupSearch\('vetId', \$event\)"/);
134
135
  assert.match(editSource, /:loading="fieldLookupLoading\('vetId'\)"/);
135
- assert.match(editSource, /UI_EDIT_FORM_FIELDS\.push\(\{[\s\S]*"key": "vetId"/);
136
+ assert.doesNotMatch(editSource, /UI_EDIT_FORM_FIELDS\.push/);
137
+ assert.match(editSource, /const UI_EDIT_FORM_FIELDS = \[[\s\S]*key: "vetId"[\s\S]*\/\/ jskit:crud-ui-form-fields:edit/);
136
138
 
137
139
  const second = await runGeneratorSubcommand({
138
140
  appRoot,
@@ -178,9 +180,10 @@ import { resource as uiResource } from "/packages/contacts/src/shared/contactRes
178
180
  await writeAppFile(
179
181
  appRoot,
180
182
  addEditFieldsFile,
181
- `const UI_EDIT_FORM_FIELDS = [];
182
- // jskit:crud-ui-form-fields:edit
183
- UI_EDIT_FORM_FIELDS.push({ key: "firstName", component: "text" });
183
+ `const UI_EDIT_FORM_FIELDS = [
184
+ { key: "firstName", component: "text" },
185
+ // jskit:crud-ui-form-fields:edit
186
+ ];
184
187
  `
185
188
  );
186
189
 
@@ -200,7 +203,8 @@ UI_EDIT_FORM_FIELDS.push({ key: "firstName", component: "text" });
200
203
  );
201
204
 
202
205
  const addEditFieldsSource = await readFile(path.join(appRoot, addEditFieldsFile), "utf8");
203
- assert.match(addEditFieldsSource, /UI_EDIT_FORM_FIELDS\.push\(\{[\s\S]*"key": "vetId"/);
206
+ assert.doesNotMatch(addEditFieldsSource, /UI_EDIT_FORM_FIELDS\.push/);
207
+ assert.match(addEditFieldsSource, /const UI_EDIT_FORM_FIELDS = \[[\s\S]*key: "vetId"[\s\S]*\/\/ jskit:crud-ui-form-fields:edit/);
204
208
  });
205
209
  });
206
210
 
@@ -257,10 +261,12 @@ import { resource as uiResource } from "/packages/contacts/src/shared/contactRes
257
261
  await writeAppFile(
258
262
  appRoot,
259
263
  addEditFieldsFile,
260
- `const UI_CREATE_FORM_FIELDS = [];
261
- // jskit:crud-ui-form-fields:new
262
- const UI_EDIT_FORM_FIELDS = [];
263
- // jskit:crud-ui-form-fields:edit
264
+ `const UI_CREATE_FORM_FIELDS = [
265
+ // jskit:crud-ui-form-fields:new
266
+ ];
267
+ const UI_EDIT_FORM_FIELDS = [
268
+ // jskit:crud-ui-form-fields:edit
269
+ ];
264
270
  `
265
271
  );
266
272
 
@@ -281,8 +287,49 @@ const UI_EDIT_FORM_FIELDS = [];
281
287
  assert.equal((addEditFormSource.match(/fieldLookupItems\('vetId'/g) || []).length, 2);
282
288
 
283
289
  const addEditFieldsSource = await readFile(path.join(appRoot, addEditFieldsFile), "utf8");
284
- assert.match(addEditFieldsSource, /UI_CREATE_FORM_FIELDS\.push\(\{[\s\S]*"key": "vetId"/);
285
- assert.match(addEditFieldsSource, /UI_EDIT_FORM_FIELDS\.push\(\{[\s\S]*"key": "vetId"/);
290
+ assert.doesNotMatch(addEditFieldsSource, /UI_CREATE_FORM_FIELDS\.push/);
291
+ assert.doesNotMatch(addEditFieldsSource, /UI_EDIT_FORM_FIELDS\.push/);
292
+ assert.match(addEditFieldsSource, /const UI_CREATE_FORM_FIELDS = \[[\s\S]*key: "vetId"[\s\S]*\/\/ jskit:crud-ui-form-fields:new/);
293
+ assert.match(addEditFieldsSource, /const UI_EDIT_FORM_FIELDS = \[[\s\S]*key: "vetId"[\s\S]*\/\/ jskit:crud-ui-form-fields:edit/);
294
+ });
295
+ });
296
+
297
+ test("field rejects legacy form-field marker layouts instead of adding new push calls", async () => {
298
+ await withTempApp(async (appRoot) => {
299
+ const resourceFile = "packages/contacts/src/shared/contactResource.js";
300
+ const editFile = "src/pages/admin/crm/contacts/[recordId]/edit.vue";
301
+
302
+ await writeAppFile(appRoot, resourceFile, RESOURCE_SOURCE);
303
+ await writeAppFile(
304
+ appRoot,
305
+ editFile,
306
+ `<template>
307
+ <v-row>
308
+ <!-- jskit:crud-ui-fields:edit -->
309
+ </v-row>
310
+ </template>
311
+ <script setup>
312
+ import { resource as uiResource } from "/packages/contacts/src/shared/contactResource.js";
313
+ const UI_EDIT_FORM_FIELDS = [];
314
+ // jskit:crud-ui-form-fields:edit
315
+ UI_EDIT_FORM_FIELDS.push({ key: "firstName", component: "text" });
316
+ </script>
317
+ `
318
+ );
319
+
320
+ await assert.rejects(
321
+ () => runGeneratorSubcommand({
322
+ appRoot,
323
+ subcommand: "field",
324
+ args: ["vetId", "edit", editFile],
325
+ options: {}
326
+ }),
327
+ /Run `jskit app migrate-source-mutations` before adding more generated form fields/
328
+ );
329
+
330
+ const editSource = await readFile(path.join(appRoot, editFile), "utf8");
331
+ assert.match(editSource, /UI_EDIT_FORM_FIELDS\.push/);
332
+ assert.doesNotMatch(editSource, /key: "vetId"/);
286
333
  });
287
334
  });
288
335