@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.cjs CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // src/index.ts
@@ -34,6 +24,9 @@ __export(index_exports, {
34
24
  CHART_FIELD_UNKNOWN: () => CHART_FIELD_UNKNOWN,
35
25
  FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
36
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,
37
30
  MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
38
31
  PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
39
32
  SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -48,7 +41,9 @@ __export(index_exports, {
48
41
  WIDGET_DATASET_UNKNOWN: () => WIDGET_DATASET_UNKNOWN,
49
42
  WIDGET_DIMENSION_UNKNOWN: () => WIDGET_DIMENSION_UNKNOWN,
50
43
  WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
44
+ validateFormLayout: () => validateFormLayout,
51
45
  validateJsxPages: () => validateJsxPages,
46
+ validateListViewMode: () => validateListViewMode,
52
47
  validatePageSourceStyling: () => validatePageSourceStyling,
53
48
  validateReactPageProps: () => validateReactPageProps,
54
49
  validateReactPages: () => validateReactPages,
@@ -433,6 +428,60 @@ function validateStackExpressions(stack) {
433
428
  return issues;
434
429
  }
435
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
+
436
485
  // src/validate-responsive-styles.ts
437
486
  var STYLE_NODE_MISSING_ID = "style-node-missing-id";
438
487
  var STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
@@ -636,7 +685,7 @@ function looksLikeTailwind(className) {
636
685
  return false;
637
686
  });
638
687
  }
639
- function asArray3(v) {
688
+ function asArray4(v) {
640
689
  if (Array.isArray(v)) return v;
641
690
  if (v && typeof v === "object") {
642
691
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -729,13 +778,13 @@ function checkNode(node, pageName, path, findings) {
729
778
  }
730
779
  function validateResponsiveStyles(stack) {
731
780
  const findings = [];
732
- const pages = asArray3(stack.pages);
781
+ const pages = asArray4(stack.pages);
733
782
  for (let p = 0; p < pages.length; p++) {
734
783
  const page = pages[p];
735
784
  const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
736
- const regions = asArray3(page.regions);
785
+ const regions = asArray4(page.regions);
737
786
  for (let r = 0; r < regions.length; r++) {
738
- const components = asArray3(regions[r].components);
787
+ const components = asArray4(regions[r].components);
739
788
  for (let c = 0; c < components.length; c++) {
740
789
  checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
741
790
  }
@@ -746,10 +795,10 @@ function validateResponsiveStyles(stack) {
746
795
 
747
796
  // src/validate-jsx-pages.ts
748
797
  var import_sdui_parser = require("@objectstack/sdui-parser");
749
- var asArray4 = (v) => Array.isArray(v) ? v : [];
798
+ var asArray5 = (v) => Array.isArray(v) ? v : [];
750
799
  function validateJsxPages(stack, opts = {}) {
751
800
  const findings = [];
752
- const pages = asArray4(stack.pages);
801
+ const pages = asArray5(stack.pages);
753
802
  for (let p = 0; p < pages.length; p++) {
754
803
  const page = pages[p];
755
804
  if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
@@ -782,11 +831,25 @@ function validateJsxPages(stack, opts = {}) {
782
831
  }
783
832
 
784
833
  // src/validate-react-pages.ts
785
- var import_sucrase = require("sucrase");
786
- var asArray5 = (v) => Array.isArray(v) ? v : [];
834
+ var import_node_module = require("module");
835
+ var import_meta = {};
836
+ var cachedTransform = null;
837
+ function loadSucraseTransform() {
838
+ if (cachedTransform) return cachedTransform;
839
+ const anchor = typeof import_meta !== "undefined" && import_meta.url ? import_meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
840
+ try {
841
+ cachedTransform = (0, import_node_module.createRequire)(anchor)("sucrase").transform;
842
+ } catch (err) {
843
+ throw new Error(
844
+ `@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.`
845
+ );
846
+ }
847
+ return cachedTransform;
848
+ }
849
+ var asArray6 = (v) => Array.isArray(v) ? v : [];
787
850
  function validateReactPages(stack) {
788
851
  const findings = [];
789
- const pages = asArray5(stack.pages);
852
+ const pages = asArray6(stack.pages);
790
853
  for (let p = 0; p < pages.length; p++) {
791
854
  const page = pages[p];
792
855
  if (!page || page.kind !== "react") continue;
@@ -803,8 +866,9 @@ function validateReactPages(stack) {
803
866
  });
804
867
  continue;
805
868
  }
869
+ const transform = loadSucraseTransform();
806
870
  try {
807
- (0, import_sucrase.transform)(source, { transforms: ["jsx", "typescript"], production: true });
871
+ transform(source, { transforms: ["jsx", "typescript"], production: true });
808
872
  } catch (err) {
809
873
  const message = err instanceof Error ? err.message : String(err);
810
874
  findings.push({
@@ -821,9 +885,23 @@ function validateReactPages(stack) {
821
885
  }
822
886
 
823
887
  // src/validate-react-page-props.ts
824
- var import_typescript = __toESM(require("typescript"), 1);
888
+ var import_node_module2 = require("module");
825
889
  var import_ui = require("@objectstack/spec/ui");
826
- var asArray6 = (v) => Array.isArray(v) ? v : [];
890
+ var import_meta2 = {};
891
+ var cachedTs = null;
892
+ function loadTypeScript() {
893
+ if (cachedTs) return cachedTs;
894
+ const anchor = typeof import_meta2 !== "undefined" && import_meta2.url ? import_meta2.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
895
+ try {
896
+ cachedTs = (0, import_node_module2.createRequire)(anchor)("typescript");
897
+ } catch (err) {
898
+ throw new Error(
899
+ `@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.`
900
+ );
901
+ }
902
+ return cachedTs;
903
+ }
904
+ var asArray7 = (v) => Array.isArray(v) ? v : [];
827
905
  var BLOCKS = new Map(
828
906
  import_ui.REACT_BLOCKS.map((b) => [
829
907
  b.tag,
@@ -862,32 +940,33 @@ function nearestKnown(prop, known) {
862
940
  }
863
941
  function validateReactPageProps(stack) {
864
942
  const findings = [];
865
- const pages = asArray6(stack.pages);
943
+ const pages = asArray7(stack.pages);
866
944
  for (let p = 0; p < pages.length; p++) {
867
945
  const page = pages[p];
868
946
  if (!page || page.kind !== "react") continue;
869
947
  const source = page.source;
870
948
  if (typeof source !== "string" || source.trim() === "") continue;
871
949
  const name = String(page.name ?? `#${p}`);
950
+ const tsc = loadTypeScript();
872
951
  let sf;
873
952
  try {
874
- sf = import_typescript.default.createSourceFile("page.tsx", source, import_typescript.default.ScriptTarget.Latest, true, import_typescript.default.ScriptKind.TSX);
953
+ sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
875
954
  } catch {
876
955
  continue;
877
956
  }
878
957
  const visit = (node) => {
879
- if (import_typescript.default.isJsxOpeningElement(node) || import_typescript.default.isJsxSelfClosingElement(node)) {
958
+ if (tsc.isJsxOpeningElement(node) || tsc.isJsxSelfClosingElement(node)) {
880
959
  const tag = node.tagName.getText(sf);
881
960
  const block = BLOCKS.get(tag);
882
961
  if (block) {
883
962
  let hasSpread = false;
884
963
  const used = /* @__PURE__ */ new Set();
885
964
  for (const a of node.attributes.properties) {
886
- if (import_typescript.default.isJsxSpreadAttribute(a)) {
965
+ if (tsc.isJsxSpreadAttribute(a)) {
887
966
  hasSpread = true;
888
967
  continue;
889
968
  }
890
- if (import_typescript.default.isJsxAttribute(a)) used.add(a.name.getText(sf));
969
+ if (tsc.isJsxAttribute(a)) used.add(a.name.getText(sf));
891
970
  }
892
971
  const where = `page "${name}" \u203A <${tag}>`;
893
972
  const path = `pages[${p}].source`;
@@ -920,7 +999,7 @@ function validateReactPageProps(stack) {
920
999
  }
921
1000
  }
922
1001
  }
923
- import_typescript.default.forEachChild(node, visit);
1002
+ tsc.forEachChild(node, visit);
924
1003
  };
925
1004
  visit(sf);
926
1005
  }
@@ -929,11 +1008,11 @@ function validateReactPageProps(stack) {
929
1008
 
930
1009
  // src/validate-page-source-styling.ts
931
1010
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
932
- var asArray7 = (v) => Array.isArray(v) ? v : [];
1011
+ var asArray8 = (v) => Array.isArray(v) ? v : [];
933
1012
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
934
1013
  function validatePageSourceStyling(stack) {
935
1014
  const findings = [];
936
- const pages = asArray7(stack.pages);
1015
+ const pages = asArray8(stack.pages);
937
1016
  for (let p = 0; p < pages.length; p++) {
938
1017
  const page = pages[p];
939
1018
  if (!page) continue;
@@ -962,7 +1041,7 @@ function validatePageSourceStyling(stack) {
962
1041
  var import_data2 = require("@objectstack/spec/data");
963
1042
  var TITLE_FORMAT_RETIRED = "title-format-retired";
964
1043
  var TITLE_UNRESOLVABLE = "title-unresolvable";
965
- function asArray8(v) {
1044
+ function asArray9(v) {
966
1045
  if (Array.isArray(v)) return v;
967
1046
  if (v && typeof v === "object") {
968
1047
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -971,7 +1050,7 @@ function asArray8(v) {
971
1050
  }
972
1051
  function validateRecordTitle(stack) {
973
1052
  const findings = [];
974
- const objects = asArray8(stack.objects);
1053
+ const objects = asArray9(stack.objects);
975
1054
  for (let i = 0; i < objects.length; i++) {
976
1055
  const obj = objects[i];
977
1056
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -1006,7 +1085,7 @@ function validateRecordTitle(stack) {
1006
1085
  var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
1007
1086
  var FIELD_GROUP_EMPTY = "field-group-empty";
1008
1087
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
1009
- function asArray9(v) {
1088
+ function asArray10(v) {
1010
1089
  if (Array.isArray(v)) return v;
1011
1090
  if (v && typeof v === "object") {
1012
1091
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1015,7 +1094,7 @@ function asArray9(v) {
1015
1094
  }
1016
1095
  function validateSemanticRoles(stack) {
1017
1096
  const findings = [];
1018
- const objects = asArray9(stack.objects);
1097
+ const objects = asArray10(stack.objects);
1019
1098
  for (let i = 0; i < objects.length; i++) {
1020
1099
  const obj = objects[i];
1021
1100
  if (!obj || typeof obj !== "object") continue;
@@ -1081,12 +1160,94 @@ function validateSemanticRoles(stack) {
1081
1160
  }
1082
1161
  return findings;
1083
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
+ }
1084
1242
  // Annotate the CommonJS export names for ESM import in node:
1085
1243
  0 && (module.exports = {
1086
1244
  CHART_CONFIG_MISSING,
1087
1245
  CHART_FIELD_UNKNOWN,
1088
1246
  FIELD_GROUP_EMPTY,
1089
1247
  FIELD_GROUP_UNDECLARED,
1248
+ FORM_COLSPAN_ABSOLUTE,
1249
+ FORM_FIELD_UNKNOWN,
1250
+ LIST_VIEW_FILTERS_IN_VIEWS_MODE,
1090
1251
  MEASURE_AGGREGATE_INCOHERENT,
1091
1252
  PAGE_SOURCE_CLASSNAME,
1092
1253
  SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -1101,7 +1262,9 @@ function validateSemanticRoles(stack) {
1101
1262
  WIDGET_DATASET_UNKNOWN,
1102
1263
  WIDGET_DIMENSION_UNKNOWN,
1103
1264
  WIDGET_MEASURE_UNKNOWN,
1265
+ validateFormLayout,
1104
1266
  validateJsxPages,
1267
+ validateListViewMode,
1105
1268
  validatePageSourceStyling,
1106
1269
  validateReactPageProps,
1107
1270
  validateReactPages,