@objectstack/lint 11.3.0 → 11.5.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$4 = 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$4 = 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$4): 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$3 = 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$3): 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$2 = 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$2): StyleFinding[];
123
+ declare function validateResponsiveStyles(stack: AnyRec$5): StyleFinding[];
124
124
 
125
125
  type JsxPageSeverity = 'error' | 'warning';
126
126
  interface JsxPageFinding {
@@ -133,11 +133,48 @@ interface JsxPageFinding {
133
133
  message: string;
134
134
  hint: string;
135
135
  }
136
- type AnyRec$1 = Record<string, unknown>;
137
- declare function validateJsxPages(stack: AnyRec$1, 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
 
141
+ type ReactPageSeverity = 'error' | 'warning';
142
+ interface ReactPageFinding {
143
+ severity: ReactPageSeverity;
144
+ rule: string;
145
+ where: string;
146
+ path: string;
147
+ message: string;
148
+ hint: string;
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";
175
+ type AnyRec$1 = Record<string, unknown>;
176
+ declare function validatePageSourceStyling(stack: AnyRec$1): SourceStyleFinding[];
177
+
141
178
  /**
142
179
  * Build-time record-title diagnostics (ADR-0079).
143
180
  *
@@ -189,4 +226,4 @@ type AnyRec = Record<string, unknown>;
189
226
  */
190
227
  declare function validateRecordTitle(stack: AnyRec): RecordTitleFinding[];
191
228
 
192
- export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, type JsxPageFinding, type JsxPageSeverity, MEASURE_AGGREGATE_INCOHERENT, 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, 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
@@ -690,7 +690,7 @@ function validateJsxPages(stack, opts = {}) {
690
690
  const pages = asArray4(stack.pages);
691
691
  for (let p = 0; p < pages.length; p++) {
692
692
  const page = pages[p];
693
- if (!page || page.kind !== "jsx") continue;
693
+ if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
694
694
  const name = String(page.name ?? `#${p}`);
695
695
  const source = page.source;
696
696
  if (typeof source !== "string" || source.trim() === "") {
@@ -699,7 +699,7 @@ function validateJsxPages(stack, opts = {}) {
699
699
  rule: "jsx-page-empty-source",
700
700
  where: `page "${name}"`,
701
701
  path: `pages[${p}].source`,
702
- message: "kind:'jsx' page has no `source`.",
702
+ message: `kind:'${page.kind}' page has no \`source\`.`,
703
703
  hint: "Author the page as a constrained JSX/Tailwind string in `source`."
704
704
  });
705
705
  continue;
@@ -719,11 +719,188 @@ function validateJsxPages(stack, opts = {}) {
719
719
  return findings;
720
720
  }
721
721
 
722
+ // src/validate-react-pages.ts
723
+ import { transform } from "sucrase";
724
+ var asArray5 = (v) => Array.isArray(v) ? v : [];
725
+ function validateReactPages(stack) {
726
+ const findings = [];
727
+ const pages = asArray5(stack.pages);
728
+ for (let p = 0; p < pages.length; p++) {
729
+ const page = pages[p];
730
+ if (!page || page.kind !== "react") continue;
731
+ const name = String(page.name ?? `#${p}`);
732
+ const source = page.source;
733
+ if (typeof source !== "string" || source.trim() === "") {
734
+ findings.push({
735
+ severity: "error",
736
+ rule: "react-page-empty-source",
737
+ where: `page "${name}"`,
738
+ path: `pages[${p}].source`,
739
+ message: "kind:'react' page has no `source`.",
740
+ hint: "Author the page as a real React component string in `source`."
741
+ });
742
+ continue;
743
+ }
744
+ try {
745
+ transform(source, { transforms: ["jsx", "typescript"], production: true });
746
+ } catch (err) {
747
+ const message = err instanceof Error ? err.message : String(err);
748
+ findings.push({
749
+ severity: "error",
750
+ rule: "react-page-syntax",
751
+ where: `page "${name}"`,
752
+ path: `pages[${p}].source`,
753
+ message: `kind:'react' source has a syntax error: ${message.split("\n")[0]}`,
754
+ hint: "The source is transpiled (never executed) at build to catch syntax errors early \u2014 fix the JS/JSX."
755
+ });
756
+ }
757
+ }
758
+ return findings;
759
+ }
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
+
722
899
  // src/validate-record-title.ts
723
900
  import { objectTitleCompleteness } from "@objectstack/spec/data";
724
901
  var TITLE_FORMAT_RETIRED = "title-format-retired";
725
902
  var TITLE_UNRESOLVABLE = "title-unresolvable";
726
- function asArray5(v) {
903
+ function asArray8(v) {
727
904
  if (Array.isArray(v)) return v;
728
905
  if (v && typeof v === "object") {
729
906
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -732,7 +909,7 @@ function asArray5(v) {
732
909
  }
733
910
  function validateRecordTitle(stack) {
734
911
  const findings = [];
735
- const objects = asArray5(stack.objects);
912
+ const objects = asArray8(stack.objects);
736
913
  for (let i = 0; i < objects.length; i++) {
737
914
  const obj = objects[i];
738
915
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -766,6 +943,7 @@ export {
766
943
  CHART_CONFIG_MISSING,
767
944
  CHART_FIELD_UNKNOWN,
768
945
  MEASURE_AGGREGATE_INCOHERENT,
946
+ PAGE_SOURCE_CLASSNAME,
769
947
  STYLE_CLASSNAME_TAILWIND,
770
948
  STYLE_NODE_MISSING_ID,
771
949
  STYLE_RESPONSIVE_NO_BASE,
@@ -778,6 +956,9 @@ export {
778
956
  WIDGET_DIMENSION_UNKNOWN,
779
957
  WIDGET_MEASURE_UNKNOWN,
780
958
  validateJsxPages,
959
+ validatePageSourceStyling,
960
+ validateReactPageProps,
961
+ validateReactPages,
781
962
  validateRecordTitle,
782
963
  validateResponsiveStyles,
783
964
  validateStackExpressions,