@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.cjs +199 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -9
- package/dist/index.d.ts +46 -9
- package/dist/index.js +185 -4
- package/dist/index.js.map +1 -1
- package/package.json +6 -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$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$
|
|
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,11 +133,48 @@ 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
|
|
|
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:
|
|
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
|
|
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 =
|
|
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,
|