@objectstack/lint 11.9.0 → 12.0.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.js CHANGED
@@ -371,6 +371,60 @@ function validateStackExpressions(stack) {
371
371
  return issues;
372
372
  }
373
373
 
374
+ // src/validate-list-view-mode.ts
375
+ var LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
376
+ var FORBIDDEN_FIELDS = ["userFilters", "quickFilters"];
377
+ function asArray3(v) {
378
+ if (Array.isArray(v)) return v;
379
+ if (v && typeof v === "object") {
380
+ return Object.entries(v).map(([name, def]) => ({
381
+ name,
382
+ ...def
383
+ }));
384
+ }
385
+ return [];
386
+ }
387
+ function scanView(view, where, path, out) {
388
+ if (!view || typeof view !== "object") return;
389
+ const rec = view;
390
+ for (const field of FORBIDDEN_FIELDS) {
391
+ if (rec[field] == null) continue;
392
+ out.push({
393
+ severity: "error",
394
+ rule: LIST_VIEW_FILTERS_IN_VIEWS_MODE,
395
+ where,
396
+ path: `${path}.${field}`,
397
+ 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.`,
398
+ hint: `Move \`${field}\` to a page list (InterfaceListPage, "filters" mode), or remove it. See ADR-0053.`
399
+ });
400
+ }
401
+ }
402
+ function scanListViews(listViews, wherePrefix, pathPrefix, out) {
403
+ if (!listViews || typeof listViews !== "object") return;
404
+ for (const [name, view] of Object.entries(listViews)) {
405
+ scanView(
406
+ view,
407
+ `${wherePrefix} \u203A listViews.${name}`,
408
+ `${pathPrefix}.listViews.${name}`,
409
+ out
410
+ );
411
+ }
412
+ }
413
+ function validateListViewMode(stack) {
414
+ const out = [];
415
+ asArray3(stack.objects).forEach((obj, i) => {
416
+ const label = typeof obj.name === "string" ? `object "${obj.name}"` : `objects[${i}]`;
417
+ scanListViews(obj.listViews, label, `objects[${i}]`, out);
418
+ });
419
+ asArray3(stack.views).forEach((view, i) => {
420
+ const named = typeof view.objectName === "string" ? view.objectName : typeof view.name === "string" ? view.name : void 0;
421
+ const label = named ? `view "${named}"` : `views[${i}]`;
422
+ scanView(view.list, `${label} \u203A list`, `views[${i}].list`, out);
423
+ scanListViews(view.listViews, label, `views[${i}]`, out);
424
+ });
425
+ return out;
426
+ }
427
+
374
428
  // src/validate-responsive-styles.ts
375
429
  var STYLE_NODE_MISSING_ID = "style-node-missing-id";
376
430
  var STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
@@ -574,7 +628,7 @@ function looksLikeTailwind(className) {
574
628
  return false;
575
629
  });
576
630
  }
577
- function asArray3(v) {
631
+ function asArray4(v) {
578
632
  if (Array.isArray(v)) return v;
579
633
  if (v && typeof v === "object") {
580
634
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -667,13 +721,13 @@ function checkNode(node, pageName, path, findings) {
667
721
  }
668
722
  function validateResponsiveStyles(stack) {
669
723
  const findings = [];
670
- const pages = asArray3(stack.pages);
724
+ const pages = asArray4(stack.pages);
671
725
  for (let p = 0; p < pages.length; p++) {
672
726
  const page = pages[p];
673
727
  const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
674
- const regions = asArray3(page.regions);
728
+ const regions = asArray4(page.regions);
675
729
  for (let r = 0; r < regions.length; r++) {
676
- const components = asArray3(regions[r].components);
730
+ const components = asArray4(regions[r].components);
677
731
  for (let c = 0; c < components.length; c++) {
678
732
  checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
679
733
  }
@@ -684,10 +738,10 @@ function validateResponsiveStyles(stack) {
684
738
 
685
739
  // src/validate-jsx-pages.ts
686
740
  import { parseJsx, compile } from "@objectstack/sdui-parser";
687
- var asArray4 = (v) => Array.isArray(v) ? v : [];
741
+ var asArray5 = (v) => Array.isArray(v) ? v : [];
688
742
  function validateJsxPages(stack, opts = {}) {
689
743
  const findings = [];
690
- const pages = asArray4(stack.pages);
744
+ const pages = asArray5(stack.pages);
691
745
  for (let p = 0; p < pages.length; p++) {
692
746
  const page = pages[p];
693
747
  if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
@@ -720,11 +774,24 @@ function validateJsxPages(stack, opts = {}) {
720
774
  }
721
775
 
722
776
  // src/validate-react-pages.ts
723
- import { transform } from "sucrase";
724
- var asArray5 = (v) => Array.isArray(v) ? v : [];
777
+ import { createRequire } from "module";
778
+ var cachedTransform = null;
779
+ function loadSucraseTransform() {
780
+ if (cachedTransform) return cachedTransform;
781
+ const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
782
+ try {
783
+ cachedTransform = createRequire(anchor)("sucrase").transform;
784
+ } catch (err) {
785
+ throw new Error(
786
+ `@objectstack/lint: validating a kind:'react' page requires the "sucrase" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "sucrase" in the image; it is only loaded when a react-source page is validated.`
787
+ );
788
+ }
789
+ return cachedTransform;
790
+ }
791
+ var asArray6 = (v) => Array.isArray(v) ? v : [];
725
792
  function validateReactPages(stack) {
726
793
  const findings = [];
727
- const pages = asArray5(stack.pages);
794
+ const pages = asArray6(stack.pages);
728
795
  for (let p = 0; p < pages.length; p++) {
729
796
  const page = pages[p];
730
797
  if (!page || page.kind !== "react") continue;
@@ -741,6 +808,7 @@ function validateReactPages(stack) {
741
808
  });
742
809
  continue;
743
810
  }
811
+ const transform = loadSucraseTransform();
744
812
  try {
745
813
  transform(source, { transforms: ["jsx", "typescript"], production: true });
746
814
  } catch (err) {
@@ -759,9 +827,22 @@ function validateReactPages(stack) {
759
827
  }
760
828
 
761
829
  // src/validate-react-page-props.ts
762
- import ts from "typescript";
830
+ import { createRequire as createRequire2 } from "module";
763
831
  import { REACT_BLOCKS } from "@objectstack/spec/ui";
764
- var asArray6 = (v) => Array.isArray(v) ? v : [];
832
+ var cachedTs = null;
833
+ function loadTypeScript() {
834
+ if (cachedTs) return cachedTs;
835
+ const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
836
+ try {
837
+ cachedTs = createRequire2(anchor)("typescript");
838
+ } catch (err) {
839
+ throw new Error(
840
+ `@objectstack/lint: validating a kind:'react' page requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a react-source page is validated.`
841
+ );
842
+ }
843
+ return cachedTs;
844
+ }
845
+ var asArray7 = (v) => Array.isArray(v) ? v : [];
765
846
  var BLOCKS = new Map(
766
847
  REACT_BLOCKS.map((b) => [
767
848
  b.tag,
@@ -800,32 +881,33 @@ function nearestKnown(prop, known) {
800
881
  }
801
882
  function validateReactPageProps(stack) {
802
883
  const findings = [];
803
- const pages = asArray6(stack.pages);
884
+ const pages = asArray7(stack.pages);
804
885
  for (let p = 0; p < pages.length; p++) {
805
886
  const page = pages[p];
806
887
  if (!page || page.kind !== "react") continue;
807
888
  const source = page.source;
808
889
  if (typeof source !== "string" || source.trim() === "") continue;
809
890
  const name = String(page.name ?? `#${p}`);
891
+ const tsc = loadTypeScript();
810
892
  let sf;
811
893
  try {
812
- sf = ts.createSourceFile("page.tsx", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
894
+ sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
813
895
  } catch {
814
896
  continue;
815
897
  }
816
898
  const visit = (node) => {
817
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
899
+ if (tsc.isJsxOpeningElement(node) || tsc.isJsxSelfClosingElement(node)) {
818
900
  const tag = node.tagName.getText(sf);
819
901
  const block = BLOCKS.get(tag);
820
902
  if (block) {
821
903
  let hasSpread = false;
822
904
  const used = /* @__PURE__ */ new Set();
823
905
  for (const a of node.attributes.properties) {
824
- if (ts.isJsxSpreadAttribute(a)) {
906
+ if (tsc.isJsxSpreadAttribute(a)) {
825
907
  hasSpread = true;
826
908
  continue;
827
909
  }
828
- if (ts.isJsxAttribute(a)) used.add(a.name.getText(sf));
910
+ if (tsc.isJsxAttribute(a)) used.add(a.name.getText(sf));
829
911
  }
830
912
  const where = `page "${name}" \u203A <${tag}>`;
831
913
  const path = `pages[${p}].source`;
@@ -858,7 +940,7 @@ function validateReactPageProps(stack) {
858
940
  }
859
941
  }
860
942
  }
861
- ts.forEachChild(node, visit);
943
+ tsc.forEachChild(node, visit);
862
944
  };
863
945
  visit(sf);
864
946
  }
@@ -867,11 +949,11 @@ function validateReactPageProps(stack) {
867
949
 
868
950
  // src/validate-page-source-styling.ts
869
951
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
870
- var asArray7 = (v) => Array.isArray(v) ? v : [];
952
+ var asArray8 = (v) => Array.isArray(v) ? v : [];
871
953
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
872
954
  function validatePageSourceStyling(stack) {
873
955
  const findings = [];
874
- const pages = asArray7(stack.pages);
956
+ const pages = asArray8(stack.pages);
875
957
  for (let p = 0; p < pages.length; p++) {
876
958
  const page = pages[p];
877
959
  if (!page) continue;
@@ -900,7 +982,7 @@ function validatePageSourceStyling(stack) {
900
982
  import { objectTitleCompleteness } from "@objectstack/spec/data";
901
983
  var TITLE_FORMAT_RETIRED = "title-format-retired";
902
984
  var TITLE_UNRESOLVABLE = "title-unresolvable";
903
- function asArray8(v) {
985
+ function asArray9(v) {
904
986
  if (Array.isArray(v)) return v;
905
987
  if (v && typeof v === "object") {
906
988
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -909,7 +991,7 @@ function asArray8(v) {
909
991
  }
910
992
  function validateRecordTitle(stack) {
911
993
  const findings = [];
912
- const objects = asArray8(stack.objects);
994
+ const objects = asArray9(stack.objects);
913
995
  for (let i = 0; i < objects.length; i++) {
914
996
  const obj = objects[i];
915
997
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -944,7 +1026,7 @@ function validateRecordTitle(stack) {
944
1026
  var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
945
1027
  var FIELD_GROUP_EMPTY = "field-group-empty";
946
1028
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
947
- function asArray9(v) {
1029
+ function asArray10(v) {
948
1030
  if (Array.isArray(v)) return v;
949
1031
  if (v && typeof v === "object") {
950
1032
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -953,7 +1035,7 @@ function asArray9(v) {
953
1035
  }
954
1036
  function validateSemanticRoles(stack) {
955
1037
  const findings = [];
956
- const objects = asArray9(stack.objects);
1038
+ const objects = asArray10(stack.objects);
957
1039
  for (let i = 0; i < objects.length; i++) {
958
1040
  const obj = objects[i];
959
1041
  if (!obj || typeof obj !== "object") continue;
@@ -1019,11 +1101,93 @@ function validateSemanticRoles(stack) {
1019
1101
  }
1020
1102
  return findings;
1021
1103
  }
1104
+
1105
+ // src/validate-form-layout.ts
1106
+ var FORM_FIELD_UNKNOWN = "form-field-unknown";
1107
+ var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
1108
+ function asArray11(v) {
1109
+ if (Array.isArray(v)) return v;
1110
+ if (v && typeof v === "object") {
1111
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1112
+ }
1113
+ return [];
1114
+ }
1115
+ function fieldNameOf(entry) {
1116
+ if (typeof entry === "string") return entry.length > 0 ? entry : null;
1117
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
1118
+ const f = entry.field;
1119
+ return typeof f === "string" && f.length > 0 ? f : null;
1120
+ }
1121
+ return null;
1122
+ }
1123
+ function boundObject(view) {
1124
+ const data = view.data;
1125
+ if (data && typeof data === "object" && typeof data.object === "string") {
1126
+ return data.object;
1127
+ }
1128
+ return typeof view.objectName === "string" ? view.objectName : void 0;
1129
+ }
1130
+ function validateFormLayout(stack) {
1131
+ const findings = [];
1132
+ const objectFields = /* @__PURE__ */ new Map();
1133
+ for (const obj of asArray11(stack.objects)) {
1134
+ const name = typeof obj.name === "string" ? obj.name : void 0;
1135
+ if (!name) continue;
1136
+ const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
1137
+ objectFields.set(name, new Set(fields));
1138
+ }
1139
+ const views = asArray11(stack.views);
1140
+ for (let i = 0; i < views.length; i++) {
1141
+ const view = views[i];
1142
+ if (!view || typeof view !== "object") continue;
1143
+ const sections = Array.isArray(view.sections) ? view.sections : null;
1144
+ if (!sections) continue;
1145
+ const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
1146
+ const objName = boundObject(view);
1147
+ const known = objName ? objectFields.get(objName) : void 0;
1148
+ const where = `view "${viewName}"`;
1149
+ const base = `views[${i}]`;
1150
+ for (let s = 0; s < sections.length; s++) {
1151
+ const sec = sections[s];
1152
+ const secFields = sec && typeof sec === "object" && Array.isArray(sec.fields) ? sec.fields : [];
1153
+ for (let f = 0; f < secFields.length; f++) {
1154
+ const entry = secFields[f];
1155
+ const fname = fieldNameOf(entry);
1156
+ const fpath = `${base}.sections[${s}].fields[${f}]`;
1157
+ if (fname && known && !known.has(fname)) {
1158
+ findings.push({
1159
+ severity: "warning",
1160
+ rule: FORM_FIELD_UNKNOWN,
1161
+ where,
1162
+ path: fpath,
1163
+ message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
1164
+ hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
1165
+ });
1166
+ }
1167
+ const colSpan = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.colSpan : void 0;
1168
+ if (colSpan != null) {
1169
+ findings.push({
1170
+ severity: "warning",
1171
+ rule: FORM_COLSPAN_ABSOLUTE,
1172
+ where,
1173
+ path: `${fpath}.colSpan`,
1174
+ 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`,
1175
+ hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
1176
+ });
1177
+ }
1178
+ }
1179
+ }
1180
+ }
1181
+ return findings;
1182
+ }
1022
1183
  export {
1023
1184
  CHART_CONFIG_MISSING,
1024
1185
  CHART_FIELD_UNKNOWN,
1025
1186
  FIELD_GROUP_EMPTY,
1026
1187
  FIELD_GROUP_UNDECLARED,
1188
+ FORM_COLSPAN_ABSOLUTE,
1189
+ FORM_FIELD_UNKNOWN,
1190
+ LIST_VIEW_FILTERS_IN_VIEWS_MODE,
1027
1191
  MEASURE_AGGREGATE_INCOHERENT,
1028
1192
  PAGE_SOURCE_CLASSNAME,
1029
1193
  SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -1038,7 +1202,9 @@ export {
1038
1202
  WIDGET_DATASET_UNKNOWN,
1039
1203
  WIDGET_DIMENSION_UNKNOWN,
1040
1204
  WIDGET_MEASURE_UNKNOWN,
1205
+ validateFormLayout,
1041
1206
  validateJsxPages,
1207
+ validateListViewMode,
1042
1208
  validatePageSourceStyling,
1043
1209
  validateReactPageProps,
1044
1210
  validateReactPages,