@objectstack/lint 11.4.0 → 11.6.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.d.ts CHANGED
@@ -69,7 +69,7 @@ interface WidgetBindingFinding {
69
69
  /** How to fix (or deliberately suppress) it. */
70
70
  hint: string;
71
71
  }
72
- type AnyRec$5 = Record<string, unknown>;
72
+ type AnyRec$7 = Record<string, unknown>;
73
73
  /**
74
74
  * Validate every dashboard widget's dataset binding. Returns the list of
75
75
  * findings (empty = clean). Caller decides how to surface them: `error`
@@ -77,7 +77,7 @@ type AnyRec$5 = Record<string, unknown>;
77
77
  * should fail validate/build; `warning` findings are advisory and must
78
78
  * never fail the build on their own.
79
79
  */
80
- declare function validateWidgetBindings(stack: AnyRec$5): WidgetBindingFinding[];
80
+ declare function validateWidgetBindings(stack: AnyRec$7): WidgetBindingFinding[];
81
81
 
82
82
  interface ExprIssue {
83
83
  where: string;
@@ -90,12 +90,12 @@ interface ExprIssue {
90
90
  */
91
91
  severity?: 'error' | 'warning';
92
92
  }
93
- type AnyRec$4 = Record<string, unknown>;
93
+ type AnyRec$6 = Record<string, unknown>;
94
94
  /**
95
95
  * Validate every predicate in the stack. Returns the list of issues (empty =
96
96
  * clean). Caller decides how to surface / whether to fail the build.
97
97
  */
98
- declare function validateStackExpressions(stack: AnyRec$4): ExprIssue[];
98
+ declare function validateStackExpressions(stack: AnyRec$6): ExprIssue[];
99
99
 
100
100
  type StyleSeverity = 'error' | 'warning';
101
101
  interface StyleFinding {
@@ -113,14 +113,14 @@ declare const STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
113
113
  declare const STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
114
114
  declare const STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
115
115
  declare const STYLE_UNKNOWN_TOKEN = "style-unknown-token";
116
- type AnyRec$3 = Record<string, unknown>;
116
+ type AnyRec$5 = Record<string, unknown>;
117
117
  /**
118
118
  * Validate every page's component tree for SDUI styling correctness (ADR-0065).
119
119
  * Returns findings (empty = clean). `error` findings describe styles that are
120
120
  * silently dropped and should fail validate/build; `warning` findings are
121
121
  * advisory (typos, drift, footguns).
122
122
  */
123
- declare function validateResponsiveStyles(stack: AnyRec$3): StyleFinding[];
123
+ declare function validateResponsiveStyles(stack: AnyRec$5): StyleFinding[];
124
124
 
125
125
  type JsxPageSeverity = 'error' | 'warning';
126
126
  interface JsxPageFinding {
@@ -133,8 +133,8 @@ interface JsxPageFinding {
133
133
  message: string;
134
134
  hint: string;
135
135
  }
136
- type AnyRec$2 = Record<string, unknown>;
137
- declare function validateJsxPages(stack: AnyRec$2, opts?: {
136
+ type AnyRec$4 = Record<string, unknown>;
137
+ declare function validateJsxPages(stack: AnyRec$4, opts?: {
138
138
  manifest?: Manifest;
139
139
  }): JsxPageFinding[];
140
140
 
@@ -147,8 +147,33 @@ interface ReactPageFinding {
147
147
  message: string;
148
148
  hint: string;
149
149
  }
150
+ type AnyRec$3 = Record<string, unknown>;
151
+ declare function validateReactPages(stack: AnyRec$3): ReactPageFinding[];
152
+
153
+ type ReactPropSeverity = 'error' | 'warning';
154
+ interface ReactPropFinding {
155
+ severity: ReactPropSeverity;
156
+ rule: string;
157
+ where: string;
158
+ path: string;
159
+ message: string;
160
+ hint: string;
161
+ }
162
+ type AnyRec$2 = Record<string, unknown>;
163
+ declare function validateReactPageProps(stack: AnyRec$2): ReactPropFinding[];
164
+
165
+ type SourceStyleSeverity = 'error' | 'warning';
166
+ interface SourceStyleFinding {
167
+ severity: SourceStyleSeverity;
168
+ rule: string;
169
+ where: string;
170
+ path: string;
171
+ message: string;
172
+ hint: string;
173
+ }
174
+ declare const PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
150
175
  type AnyRec$1 = Record<string, unknown>;
151
- declare function validateReactPages(stack: AnyRec$1): ReactPageFinding[];
176
+ declare function validatePageSourceStyling(stack: AnyRec$1): SourceStyleFinding[];
152
177
 
153
178
  /**
154
179
  * Build-time record-title diagnostics (ADR-0079).
@@ -201,4 +226,4 @@ type AnyRec = Record<string, unknown>;
201
226
  */
202
227
  declare function validateRecordTitle(stack: AnyRec): RecordTitleFinding[];
203
228
 
204
- export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, type JsxPageFinding, type JsxPageSeverity, MEASURE_AGGREGATE_INCOHERENT, type ReactPageFinding, type ReactPageSeverity, type RecordTitleFinding, type RecordTitleSeverity, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateJsxPages, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateStackExpressions, validateWidgetBindings };
229
+ export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, type JsxPageFinding, type JsxPageSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateJsxPages, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateStackExpressions, validateWidgetBindings };
package/dist/index.js CHANGED
@@ -758,11 +758,149 @@ function validateReactPages(stack) {
758
758
  return findings;
759
759
  }
760
760
 
761
+ // src/validate-react-page-props.ts
762
+ import ts from "typescript";
763
+ import { REACT_BLOCKS } from "@objectstack/spec/ui";
764
+ var asArray6 = (v) => Array.isArray(v) ? v : [];
765
+ var BLOCKS = new Map(
766
+ REACT_BLOCKS.map((b) => [
767
+ b.tag,
768
+ {
769
+ requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
770
+ knownProps: new Set(b.interactions.map((i) => i.name))
771
+ }
772
+ ])
773
+ );
774
+ function editDistance(a, b, cap = 2) {
775
+ if (Math.abs(a.length - b.length) > cap) return cap + 1;
776
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
777
+ for (let j = 1; j <= b.length; j++) {
778
+ let prev = dp[0];
779
+ dp[0] = j;
780
+ for (let i = 1; i <= a.length; i++) {
781
+ const tmp = dp[i];
782
+ dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
783
+ prev = tmp;
784
+ }
785
+ }
786
+ return dp[a.length];
787
+ }
788
+ function nearestKnown(prop, known) {
789
+ if (known.has(prop)) return null;
790
+ let best = null;
791
+ let bestD = 3;
792
+ for (const k of known) {
793
+ const d = editDistance(prop, k);
794
+ if (d < bestD) {
795
+ bestD = d;
796
+ best = k;
797
+ }
798
+ }
799
+ return bestD <= 2 ? best : null;
800
+ }
801
+ function validateReactPageProps(stack) {
802
+ const findings = [];
803
+ const pages = asArray6(stack.pages);
804
+ for (let p = 0; p < pages.length; p++) {
805
+ const page = pages[p];
806
+ if (!page || page.kind !== "react") continue;
807
+ const source = page.source;
808
+ if (typeof source !== "string" || source.trim() === "") continue;
809
+ const name = String(page.name ?? `#${p}`);
810
+ let sf;
811
+ try {
812
+ sf = ts.createSourceFile("page.tsx", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
813
+ } catch {
814
+ continue;
815
+ }
816
+ const visit = (node) => {
817
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
818
+ const tag = node.tagName.getText(sf);
819
+ const block = BLOCKS.get(tag);
820
+ if (block) {
821
+ let hasSpread = false;
822
+ const used = /* @__PURE__ */ new Set();
823
+ for (const a of node.attributes.properties) {
824
+ if (ts.isJsxSpreadAttribute(a)) {
825
+ hasSpread = true;
826
+ continue;
827
+ }
828
+ if (ts.isJsxAttribute(a)) used.add(a.name.getText(sf));
829
+ }
830
+ const where = `page "${name}" \u203A <${tag}>`;
831
+ const path = `pages[${p}].source`;
832
+ if (!hasSpread) {
833
+ for (const req of block.requiredBindings) {
834
+ if (!used.has(req)) {
835
+ findings.push({
836
+ severity: "error",
837
+ rule: "react-prop-missing-required",
838
+ where,
839
+ path,
840
+ message: `<${tag}> is missing the required prop "${req}".`,
841
+ hint: `Pass ${req}={\u2026}. See the react-tier component contract.`
842
+ });
843
+ }
844
+ }
845
+ }
846
+ for (const u of used) {
847
+ const near = nearestKnown(u, block.knownProps);
848
+ if (near) {
849
+ findings.push({
850
+ severity: "warning",
851
+ rule: "react-prop-typo",
852
+ where,
853
+ path,
854
+ message: `<${tag}> has prop "${u}" \u2014 did you mean "${near}"?`,
855
+ hint: "Likely a typo of a contract prop. Fix it or remove it."
856
+ });
857
+ }
858
+ }
859
+ }
860
+ }
861
+ ts.forEachChild(node, visit);
862
+ };
863
+ visit(sf);
864
+ }
865
+ return findings;
866
+ }
867
+
868
+ // src/validate-page-source-styling.ts
869
+ var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
870
+ var asArray7 = (v) => Array.isArray(v) ? v : [];
871
+ var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
872
+ function validatePageSourceStyling(stack) {
873
+ const findings = [];
874
+ const pages = asArray7(stack.pages);
875
+ for (let p = 0; p < pages.length; p++) {
876
+ const page = pages[p];
877
+ if (!page) continue;
878
+ const kind = page.kind;
879
+ if (kind !== "html" && kind !== "react" && kind !== "jsx") continue;
880
+ const source = page.source;
881
+ if (typeof source !== "string" || source.trim() === "") continue;
882
+ const name = String(page.name ?? `#${p}`);
883
+ CLASSNAME_ATTR.lastIndex = 0;
884
+ let count = 0;
885
+ while (CLASSNAME_ATTR.exec(source) !== null) count++;
886
+ if (count === 0) continue;
887
+ findings.push({
888
+ severity: "warning",
889
+ rule: PAGE_SOURCE_CLASSNAME,
890
+ where: `page "${name}"`,
891
+ path: `pages[${p}].source`,
892
+ message: `${count} \`className\` attribute${count > 1 ? "s" : ""} in ${String(kind)}-source page \u2014 Tailwind utilities in page source silently produce no CSS (the build never scans authored metadata; ADR-0065).`,
893
+ hint: kind === "react" ? `Style with inline style={{}} using hsl(var(--token)) theme colors (e.g. color:'hsl(var(--foreground))', background:'hsl(var(--card))'); render drawer/modal via <ObjectForm formType="drawer"|"modal"> instead of hand-rolled overlays.` : `Lay out with the components' structured props (<flex direction gap>, <grid columns>) and add CSS via a JSON style object style={{"color":"hsl(var(--foreground))"}}; do not use Tailwind className.`
894
+ });
895
+ }
896
+ return findings;
897
+ }
898
+
761
899
  // src/validate-record-title.ts
762
900
  import { objectTitleCompleteness } from "@objectstack/spec/data";
763
901
  var TITLE_FORMAT_RETIRED = "title-format-retired";
764
902
  var TITLE_UNRESOLVABLE = "title-unresolvable";
765
- function asArray6(v) {
903
+ function asArray8(v) {
766
904
  if (Array.isArray(v)) return v;
767
905
  if (v && typeof v === "object") {
768
906
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -771,7 +909,7 @@ function asArray6(v) {
771
909
  }
772
910
  function validateRecordTitle(stack) {
773
911
  const findings = [];
774
- const objects = asArray6(stack.objects);
912
+ const objects = asArray8(stack.objects);
775
913
  for (let i = 0; i < objects.length; i++) {
776
914
  const obj = objects[i];
777
915
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -805,6 +943,7 @@ export {
805
943
  CHART_CONFIG_MISSING,
806
944
  CHART_FIELD_UNKNOWN,
807
945
  MEASURE_AGGREGATE_INCOHERENT,
946
+ PAGE_SOURCE_CLASSNAME,
808
947
  STYLE_CLASSNAME_TAILWIND,
809
948
  STYLE_NODE_MISSING_ID,
810
949
  STYLE_RESPONSIVE_NO_BASE,
@@ -817,6 +956,8 @@ export {
817
956
  WIDGET_DIMENSION_UNKNOWN,
818
957
  WIDGET_MEASURE_UNKNOWN,
819
958
  validateJsxPages,
959
+ validatePageSourceStyling,
960
+ validateReactPageProps,
820
961
  validateReactPages,
821
962
  validateRecordTitle,
822
963
  validateResponsiveStyles,