@objectstack/lint 11.4.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.cjs +156 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -10
- package/dist/index.d.ts +35 -10
- package/dist/index.js +143 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
137
|
-
declare function validateJsxPages(stack: AnyRec$
|
|
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
|
|
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
|
|
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 =
|
|
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,
|