@objectstack/lint 11.10.0 → 12.1.0

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/dist/index.cjs CHANGED
@@ -24,6 +24,9 @@ __export(index_exports, {
24
24
  CHART_FIELD_UNKNOWN: () => CHART_FIELD_UNKNOWN,
25
25
  FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
26
26
  FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
27
+ FORM_COLSPAN_ABSOLUTE: () => FORM_COLSPAN_ABSOLUTE,
28
+ FORM_FIELD_UNKNOWN: () => FORM_FIELD_UNKNOWN,
29
+ LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
27
30
  MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
28
31
  PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
29
32
  SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -38,7 +41,9 @@ __export(index_exports, {
38
41
  WIDGET_DATASET_UNKNOWN: () => WIDGET_DATASET_UNKNOWN,
39
42
  WIDGET_DIMENSION_UNKNOWN: () => WIDGET_DIMENSION_UNKNOWN,
40
43
  WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
44
+ validateFormLayout: () => validateFormLayout,
41
45
  validateJsxPages: () => validateJsxPages,
46
+ validateListViewMode: () => validateListViewMode,
42
47
  validatePageSourceStyling: () => validatePageSourceStyling,
43
48
  validateReactPageProps: () => validateReactPageProps,
44
49
  validateReactPages: () => validateReactPages,
@@ -423,6 +428,60 @@ function validateStackExpressions(stack) {
423
428
  return issues;
424
429
  }
425
430
 
431
+ // src/validate-list-view-mode.ts
432
+ var LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
433
+ var FORBIDDEN_FIELDS = ["userFilters", "quickFilters"];
434
+ function asArray3(v) {
435
+ if (Array.isArray(v)) return v;
436
+ if (v && typeof v === "object") {
437
+ return Object.entries(v).map(([name, def]) => ({
438
+ name,
439
+ ...def
440
+ }));
441
+ }
442
+ return [];
443
+ }
444
+ function scanView(view, where, path, out) {
445
+ if (!view || typeof view !== "object") return;
446
+ const rec = view;
447
+ for (const field of FORBIDDEN_FIELDS) {
448
+ if (rec[field] == null) continue;
449
+ out.push({
450
+ severity: "error",
451
+ rule: LIST_VIEW_FILTERS_IN_VIEWS_MODE,
452
+ where,
453
+ path: `${path}.${field}`,
454
+ message: `\`${field}\` is a page filters-mode control and is ignored on an object list view ("views" mode) \u2014 the ViewTabBar is the only nav control here.`,
455
+ hint: `Move \`${field}\` to a page list (InterfaceListPage, "filters" mode), or remove it. See ADR-0053.`
456
+ });
457
+ }
458
+ }
459
+ function scanListViews(listViews, wherePrefix, pathPrefix, out) {
460
+ if (!listViews || typeof listViews !== "object") return;
461
+ for (const [name, view] of Object.entries(listViews)) {
462
+ scanView(
463
+ view,
464
+ `${wherePrefix} \u203A listViews.${name}`,
465
+ `${pathPrefix}.listViews.${name}`,
466
+ out
467
+ );
468
+ }
469
+ }
470
+ function validateListViewMode(stack) {
471
+ const out = [];
472
+ asArray3(stack.objects).forEach((obj, i) => {
473
+ const label = typeof obj.name === "string" ? `object "${obj.name}"` : `objects[${i}]`;
474
+ scanListViews(obj.listViews, label, `objects[${i}]`, out);
475
+ });
476
+ asArray3(stack.views).forEach((view, i) => {
477
+ const named = typeof view.objectName === "string" ? view.objectName : typeof view.name === "string" ? view.name : void 0;
478
+ const label = named ? `view "${named}"` : `views[${i}]`;
479
+ scanView(view.list, `${label} \u203A list`, `views[${i}].list`, out);
480
+ scanListViews(view.listViews, label, `views[${i}]`, out);
481
+ });
482
+ return out;
483
+ }
484
+
426
485
  // src/validate-responsive-styles.ts
427
486
  var STYLE_NODE_MISSING_ID = "style-node-missing-id";
428
487
  var STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
@@ -626,7 +685,7 @@ function looksLikeTailwind(className) {
626
685
  return false;
627
686
  });
628
687
  }
629
- function asArray3(v) {
688
+ function asArray4(v) {
630
689
  if (Array.isArray(v)) return v;
631
690
  if (v && typeof v === "object") {
632
691
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -719,13 +778,13 @@ function checkNode(node, pageName, path, findings) {
719
778
  }
720
779
  function validateResponsiveStyles(stack) {
721
780
  const findings = [];
722
- const pages = asArray3(stack.pages);
781
+ const pages = asArray4(stack.pages);
723
782
  for (let p = 0; p < pages.length; p++) {
724
783
  const page = pages[p];
725
784
  const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
726
- const regions = asArray3(page.regions);
785
+ const regions = asArray4(page.regions);
727
786
  for (let r = 0; r < regions.length; r++) {
728
- const components = asArray3(regions[r].components);
787
+ const components = asArray4(regions[r].components);
729
788
  for (let c = 0; c < components.length; c++) {
730
789
  checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
731
790
  }
@@ -736,10 +795,10 @@ function validateResponsiveStyles(stack) {
736
795
 
737
796
  // src/validate-jsx-pages.ts
738
797
  var import_sdui_parser = require("@objectstack/sdui-parser");
739
- var asArray4 = (v) => Array.isArray(v) ? v : [];
798
+ var asArray5 = (v) => Array.isArray(v) ? v : [];
740
799
  function validateJsxPages(stack, opts = {}) {
741
800
  const findings = [];
742
- const pages = asArray4(stack.pages);
801
+ const pages = asArray5(stack.pages);
743
802
  for (let p = 0; p < pages.length; p++) {
744
803
  const page = pages[p];
745
804
  if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
@@ -787,10 +846,10 @@ function loadSucraseTransform() {
787
846
  }
788
847
  return cachedTransform;
789
848
  }
790
- var asArray5 = (v) => Array.isArray(v) ? v : [];
849
+ var asArray6 = (v) => Array.isArray(v) ? v : [];
791
850
  function validateReactPages(stack) {
792
851
  const findings = [];
793
- const pages = asArray5(stack.pages);
852
+ const pages = asArray6(stack.pages);
794
853
  for (let p = 0; p < pages.length; p++) {
795
854
  const page = pages[p];
796
855
  if (!page || page.kind !== "react") continue;
@@ -842,7 +901,7 @@ function loadTypeScript() {
842
901
  }
843
902
  return cachedTs;
844
903
  }
845
- var asArray6 = (v) => Array.isArray(v) ? v : [];
904
+ var asArray7 = (v) => Array.isArray(v) ? v : [];
846
905
  var BLOCKS = new Map(
847
906
  import_ui.REACT_BLOCKS.map((b) => [
848
907
  b.tag,
@@ -881,7 +940,7 @@ function nearestKnown(prop, known) {
881
940
  }
882
941
  function validateReactPageProps(stack) {
883
942
  const findings = [];
884
- const pages = asArray6(stack.pages);
943
+ const pages = asArray7(stack.pages);
885
944
  for (let p = 0; p < pages.length; p++) {
886
945
  const page = pages[p];
887
946
  if (!page || page.kind !== "react") continue;
@@ -949,11 +1008,11 @@ function validateReactPageProps(stack) {
949
1008
 
950
1009
  // src/validate-page-source-styling.ts
951
1010
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
952
- var asArray7 = (v) => Array.isArray(v) ? v : [];
1011
+ var asArray8 = (v) => Array.isArray(v) ? v : [];
953
1012
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
954
1013
  function validatePageSourceStyling(stack) {
955
1014
  const findings = [];
956
- const pages = asArray7(stack.pages);
1015
+ const pages = asArray8(stack.pages);
957
1016
  for (let p = 0; p < pages.length; p++) {
958
1017
  const page = pages[p];
959
1018
  if (!page) continue;
@@ -982,7 +1041,7 @@ function validatePageSourceStyling(stack) {
982
1041
  var import_data2 = require("@objectstack/spec/data");
983
1042
  var TITLE_FORMAT_RETIRED = "title-format-retired";
984
1043
  var TITLE_UNRESOLVABLE = "title-unresolvable";
985
- function asArray8(v) {
1044
+ function asArray9(v) {
986
1045
  if (Array.isArray(v)) return v;
987
1046
  if (v && typeof v === "object") {
988
1047
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -991,7 +1050,7 @@ function asArray8(v) {
991
1050
  }
992
1051
  function validateRecordTitle(stack) {
993
1052
  const findings = [];
994
- const objects = asArray8(stack.objects);
1053
+ const objects = asArray9(stack.objects);
995
1054
  for (let i = 0; i < objects.length; i++) {
996
1055
  const obj = objects[i];
997
1056
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -1026,7 +1085,7 @@ function validateRecordTitle(stack) {
1026
1085
  var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
1027
1086
  var FIELD_GROUP_EMPTY = "field-group-empty";
1028
1087
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
1029
- function asArray9(v) {
1088
+ function asArray10(v) {
1030
1089
  if (Array.isArray(v)) return v;
1031
1090
  if (v && typeof v === "object") {
1032
1091
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1035,7 +1094,7 @@ function asArray9(v) {
1035
1094
  }
1036
1095
  function validateSemanticRoles(stack) {
1037
1096
  const findings = [];
1038
- const objects = asArray9(stack.objects);
1097
+ const objects = asArray10(stack.objects);
1039
1098
  for (let i = 0; i < objects.length; i++) {
1040
1099
  const obj = objects[i];
1041
1100
  if (!obj || typeof obj !== "object") continue;
@@ -1101,12 +1160,94 @@ function validateSemanticRoles(stack) {
1101
1160
  }
1102
1161
  return findings;
1103
1162
  }
1163
+
1164
+ // src/validate-form-layout.ts
1165
+ var FORM_FIELD_UNKNOWN = "form-field-unknown";
1166
+ var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
1167
+ function asArray11(v) {
1168
+ if (Array.isArray(v)) return v;
1169
+ if (v && typeof v === "object") {
1170
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1171
+ }
1172
+ return [];
1173
+ }
1174
+ function fieldNameOf(entry) {
1175
+ if (typeof entry === "string") return entry.length > 0 ? entry : null;
1176
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
1177
+ const f = entry.field;
1178
+ return typeof f === "string" && f.length > 0 ? f : null;
1179
+ }
1180
+ return null;
1181
+ }
1182
+ function boundObject(view) {
1183
+ const data = view.data;
1184
+ if (data && typeof data === "object" && typeof data.object === "string") {
1185
+ return data.object;
1186
+ }
1187
+ return typeof view.objectName === "string" ? view.objectName : void 0;
1188
+ }
1189
+ function validateFormLayout(stack) {
1190
+ const findings = [];
1191
+ const objectFields = /* @__PURE__ */ new Map();
1192
+ for (const obj of asArray11(stack.objects)) {
1193
+ const name = typeof obj.name === "string" ? obj.name : void 0;
1194
+ if (!name) continue;
1195
+ const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
1196
+ objectFields.set(name, new Set(fields));
1197
+ }
1198
+ const views = asArray11(stack.views);
1199
+ for (let i = 0; i < views.length; i++) {
1200
+ const view = views[i];
1201
+ if (!view || typeof view !== "object") continue;
1202
+ const sections = Array.isArray(view.sections) ? view.sections : null;
1203
+ if (!sections) continue;
1204
+ const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
1205
+ const objName = boundObject(view);
1206
+ const known = objName ? objectFields.get(objName) : void 0;
1207
+ const where = `view "${viewName}"`;
1208
+ const base = `views[${i}]`;
1209
+ for (let s = 0; s < sections.length; s++) {
1210
+ const sec = sections[s];
1211
+ const secFields = sec && typeof sec === "object" && Array.isArray(sec.fields) ? sec.fields : [];
1212
+ for (let f = 0; f < secFields.length; f++) {
1213
+ const entry = secFields[f];
1214
+ const fname = fieldNameOf(entry);
1215
+ const fpath = `${base}.sections[${s}].fields[${f}]`;
1216
+ if (fname && known && !known.has(fname)) {
1217
+ findings.push({
1218
+ severity: "warning",
1219
+ rule: FORM_FIELD_UNKNOWN,
1220
+ where,
1221
+ path: fpath,
1222
+ message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
1223
+ hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
1224
+ });
1225
+ }
1226
+ const colSpan = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.colSpan : void 0;
1227
+ if (colSpan != null) {
1228
+ findings.push({
1229
+ severity: "warning",
1230
+ rule: FORM_COLSPAN_ABSOLUTE,
1231
+ where,
1232
+ path: `${fpath}.colSpan`,
1233
+ message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
1234
+ hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
1235
+ });
1236
+ }
1237
+ }
1238
+ }
1239
+ }
1240
+ return findings;
1241
+ }
1104
1242
  // Annotate the CommonJS export names for ESM import in node:
1105
1243
  0 && (module.exports = {
1106
1244
  CHART_CONFIG_MISSING,
1107
1245
  CHART_FIELD_UNKNOWN,
1108
1246
  FIELD_GROUP_EMPTY,
1109
1247
  FIELD_GROUP_UNDECLARED,
1248
+ FORM_COLSPAN_ABSOLUTE,
1249
+ FORM_FIELD_UNKNOWN,
1250
+ LIST_VIEW_FILTERS_IN_VIEWS_MODE,
1110
1251
  MEASURE_AGGREGATE_INCOHERENT,
1111
1252
  PAGE_SOURCE_CLASSNAME,
1112
1253
  SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -1121,7 +1262,9 @@ function validateSemanticRoles(stack) {
1121
1262
  WIDGET_DATASET_UNKNOWN,
1122
1263
  WIDGET_DIMENSION_UNKNOWN,
1123
1264
  WIDGET_MEASURE_UNKNOWN,
1265
+ validateFormLayout,
1124
1266
  validateJsxPages,
1267
+ validateListViewMode,
1125
1268
  validatePageSourceStyling,
1126
1269
  validateReactPageProps,
1127
1270
  validateReactPages,