@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.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
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
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -23,6 +33,7 @@ __export(index_exports, {
|
|
|
23
33
|
CHART_CONFIG_MISSING: () => CHART_CONFIG_MISSING,
|
|
24
34
|
CHART_FIELD_UNKNOWN: () => CHART_FIELD_UNKNOWN,
|
|
25
35
|
MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
|
|
36
|
+
PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
|
|
26
37
|
STYLE_CLASSNAME_TAILWIND: () => STYLE_CLASSNAME_TAILWIND,
|
|
27
38
|
STYLE_NODE_MISSING_ID: () => STYLE_NODE_MISSING_ID,
|
|
28
39
|
STYLE_RESPONSIVE_NO_BASE: () => STYLE_RESPONSIVE_NO_BASE,
|
|
@@ -35,6 +46,9 @@ __export(index_exports, {
|
|
|
35
46
|
WIDGET_DIMENSION_UNKNOWN: () => WIDGET_DIMENSION_UNKNOWN,
|
|
36
47
|
WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
|
|
37
48
|
validateJsxPages: () => validateJsxPages,
|
|
49
|
+
validatePageSourceStyling: () => validatePageSourceStyling,
|
|
50
|
+
validateReactPageProps: () => validateReactPageProps,
|
|
51
|
+
validateReactPages: () => validateReactPages,
|
|
38
52
|
validateRecordTitle: () => validateRecordTitle,
|
|
39
53
|
validateResponsiveStyles: () => validateResponsiveStyles,
|
|
40
54
|
validateStackExpressions: () => validateStackExpressions,
|
|
@@ -734,7 +748,7 @@ function validateJsxPages(stack, opts = {}) {
|
|
|
734
748
|
const pages = asArray4(stack.pages);
|
|
735
749
|
for (let p = 0; p < pages.length; p++) {
|
|
736
750
|
const page = pages[p];
|
|
737
|
-
if (!page || page.kind !== "jsx") continue;
|
|
751
|
+
if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
|
|
738
752
|
const name = String(page.name ?? `#${p}`);
|
|
739
753
|
const source = page.source;
|
|
740
754
|
if (typeof source !== "string" || source.trim() === "") {
|
|
@@ -743,7 +757,7 @@ function validateJsxPages(stack, opts = {}) {
|
|
|
743
757
|
rule: "jsx-page-empty-source",
|
|
744
758
|
where: `page "${name}"`,
|
|
745
759
|
path: `pages[${p}].source`,
|
|
746
|
-
message:
|
|
760
|
+
message: `kind:'${page.kind}' page has no \`source\`.`,
|
|
747
761
|
hint: "Author the page as a constrained JSX/Tailwind string in `source`."
|
|
748
762
|
});
|
|
749
763
|
continue;
|
|
@@ -763,11 +777,188 @@ function validateJsxPages(stack, opts = {}) {
|
|
|
763
777
|
return findings;
|
|
764
778
|
}
|
|
765
779
|
|
|
780
|
+
// src/validate-react-pages.ts
|
|
781
|
+
var import_sucrase = require("sucrase");
|
|
782
|
+
var asArray5 = (v) => Array.isArray(v) ? v : [];
|
|
783
|
+
function validateReactPages(stack) {
|
|
784
|
+
const findings = [];
|
|
785
|
+
const pages = asArray5(stack.pages);
|
|
786
|
+
for (let p = 0; p < pages.length; p++) {
|
|
787
|
+
const page = pages[p];
|
|
788
|
+
if (!page || page.kind !== "react") continue;
|
|
789
|
+
const name = String(page.name ?? `#${p}`);
|
|
790
|
+
const source = page.source;
|
|
791
|
+
if (typeof source !== "string" || source.trim() === "") {
|
|
792
|
+
findings.push({
|
|
793
|
+
severity: "error",
|
|
794
|
+
rule: "react-page-empty-source",
|
|
795
|
+
where: `page "${name}"`,
|
|
796
|
+
path: `pages[${p}].source`,
|
|
797
|
+
message: "kind:'react' page has no `source`.",
|
|
798
|
+
hint: "Author the page as a real React component string in `source`."
|
|
799
|
+
});
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
(0, import_sucrase.transform)(source, { transforms: ["jsx", "typescript"], production: true });
|
|
804
|
+
} catch (err) {
|
|
805
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
806
|
+
findings.push({
|
|
807
|
+
severity: "error",
|
|
808
|
+
rule: "react-page-syntax",
|
|
809
|
+
where: `page "${name}"`,
|
|
810
|
+
path: `pages[${p}].source`,
|
|
811
|
+
message: `kind:'react' source has a syntax error: ${message.split("\n")[0]}`,
|
|
812
|
+
hint: "The source is transpiled (never executed) at build to catch syntax errors early \u2014 fix the JS/JSX."
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return findings;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/validate-react-page-props.ts
|
|
820
|
+
var import_typescript = __toESM(require("typescript"), 1);
|
|
821
|
+
var import_ui = require("@objectstack/spec/ui");
|
|
822
|
+
var asArray6 = (v) => Array.isArray(v) ? v : [];
|
|
823
|
+
var BLOCKS = new Map(
|
|
824
|
+
import_ui.REACT_BLOCKS.map((b) => [
|
|
825
|
+
b.tag,
|
|
826
|
+
{
|
|
827
|
+
requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
|
|
828
|
+
knownProps: new Set(b.interactions.map((i) => i.name))
|
|
829
|
+
}
|
|
830
|
+
])
|
|
831
|
+
);
|
|
832
|
+
function editDistance(a, b, cap = 2) {
|
|
833
|
+
if (Math.abs(a.length - b.length) > cap) return cap + 1;
|
|
834
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
|
|
835
|
+
for (let j = 1; j <= b.length; j++) {
|
|
836
|
+
let prev = dp[0];
|
|
837
|
+
dp[0] = j;
|
|
838
|
+
for (let i = 1; i <= a.length; i++) {
|
|
839
|
+
const tmp = dp[i];
|
|
840
|
+
dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
841
|
+
prev = tmp;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return dp[a.length];
|
|
845
|
+
}
|
|
846
|
+
function nearestKnown(prop, known) {
|
|
847
|
+
if (known.has(prop)) return null;
|
|
848
|
+
let best = null;
|
|
849
|
+
let bestD = 3;
|
|
850
|
+
for (const k of known) {
|
|
851
|
+
const d = editDistance(prop, k);
|
|
852
|
+
if (d < bestD) {
|
|
853
|
+
bestD = d;
|
|
854
|
+
best = k;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return bestD <= 2 ? best : null;
|
|
858
|
+
}
|
|
859
|
+
function validateReactPageProps(stack) {
|
|
860
|
+
const findings = [];
|
|
861
|
+
const pages = asArray6(stack.pages);
|
|
862
|
+
for (let p = 0; p < pages.length; p++) {
|
|
863
|
+
const page = pages[p];
|
|
864
|
+
if (!page || page.kind !== "react") continue;
|
|
865
|
+
const source = page.source;
|
|
866
|
+
if (typeof source !== "string" || source.trim() === "") continue;
|
|
867
|
+
const name = String(page.name ?? `#${p}`);
|
|
868
|
+
let sf;
|
|
869
|
+
try {
|
|
870
|
+
sf = import_typescript.default.createSourceFile("page.tsx", source, import_typescript.default.ScriptTarget.Latest, true, import_typescript.default.ScriptKind.TSX);
|
|
871
|
+
} catch {
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const visit = (node) => {
|
|
875
|
+
if (import_typescript.default.isJsxOpeningElement(node) || import_typescript.default.isJsxSelfClosingElement(node)) {
|
|
876
|
+
const tag = node.tagName.getText(sf);
|
|
877
|
+
const block = BLOCKS.get(tag);
|
|
878
|
+
if (block) {
|
|
879
|
+
let hasSpread = false;
|
|
880
|
+
const used = /* @__PURE__ */ new Set();
|
|
881
|
+
for (const a of node.attributes.properties) {
|
|
882
|
+
if (import_typescript.default.isJsxSpreadAttribute(a)) {
|
|
883
|
+
hasSpread = true;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (import_typescript.default.isJsxAttribute(a)) used.add(a.name.getText(sf));
|
|
887
|
+
}
|
|
888
|
+
const where = `page "${name}" \u203A <${tag}>`;
|
|
889
|
+
const path = `pages[${p}].source`;
|
|
890
|
+
if (!hasSpread) {
|
|
891
|
+
for (const req of block.requiredBindings) {
|
|
892
|
+
if (!used.has(req)) {
|
|
893
|
+
findings.push({
|
|
894
|
+
severity: "error",
|
|
895
|
+
rule: "react-prop-missing-required",
|
|
896
|
+
where,
|
|
897
|
+
path,
|
|
898
|
+
message: `<${tag}> is missing the required prop "${req}".`,
|
|
899
|
+
hint: `Pass ${req}={\u2026}. See the react-tier component contract.`
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
for (const u of used) {
|
|
905
|
+
const near = nearestKnown(u, block.knownProps);
|
|
906
|
+
if (near) {
|
|
907
|
+
findings.push({
|
|
908
|
+
severity: "warning",
|
|
909
|
+
rule: "react-prop-typo",
|
|
910
|
+
where,
|
|
911
|
+
path,
|
|
912
|
+
message: `<${tag}> has prop "${u}" \u2014 did you mean "${near}"?`,
|
|
913
|
+
hint: "Likely a typo of a contract prop. Fix it or remove it."
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
import_typescript.default.forEachChild(node, visit);
|
|
920
|
+
};
|
|
921
|
+
visit(sf);
|
|
922
|
+
}
|
|
923
|
+
return findings;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// src/validate-page-source-styling.ts
|
|
927
|
+
var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
|
|
928
|
+
var asArray7 = (v) => Array.isArray(v) ? v : [];
|
|
929
|
+
var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
|
|
930
|
+
function validatePageSourceStyling(stack) {
|
|
931
|
+
const findings = [];
|
|
932
|
+
const pages = asArray7(stack.pages);
|
|
933
|
+
for (let p = 0; p < pages.length; p++) {
|
|
934
|
+
const page = pages[p];
|
|
935
|
+
if (!page) continue;
|
|
936
|
+
const kind = page.kind;
|
|
937
|
+
if (kind !== "html" && kind !== "react" && kind !== "jsx") continue;
|
|
938
|
+
const source = page.source;
|
|
939
|
+
if (typeof source !== "string" || source.trim() === "") continue;
|
|
940
|
+
const name = String(page.name ?? `#${p}`);
|
|
941
|
+
CLASSNAME_ATTR.lastIndex = 0;
|
|
942
|
+
let count = 0;
|
|
943
|
+
while (CLASSNAME_ATTR.exec(source) !== null) count++;
|
|
944
|
+
if (count === 0) continue;
|
|
945
|
+
findings.push({
|
|
946
|
+
severity: "warning",
|
|
947
|
+
rule: PAGE_SOURCE_CLASSNAME,
|
|
948
|
+
where: `page "${name}"`,
|
|
949
|
+
path: `pages[${p}].source`,
|
|
950
|
+
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).`,
|
|
951
|
+
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.`
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
return findings;
|
|
955
|
+
}
|
|
956
|
+
|
|
766
957
|
// src/validate-record-title.ts
|
|
767
958
|
var import_data2 = require("@objectstack/spec/data");
|
|
768
959
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
769
960
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
770
|
-
function
|
|
961
|
+
function asArray8(v) {
|
|
771
962
|
if (Array.isArray(v)) return v;
|
|
772
963
|
if (v && typeof v === "object") {
|
|
773
964
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -776,7 +967,7 @@ function asArray5(v) {
|
|
|
776
967
|
}
|
|
777
968
|
function validateRecordTitle(stack) {
|
|
778
969
|
const findings = [];
|
|
779
|
-
const objects =
|
|
970
|
+
const objects = asArray8(stack.objects);
|
|
780
971
|
for (let i = 0; i < objects.length; i++) {
|
|
781
972
|
const obj = objects[i];
|
|
782
973
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
@@ -811,6 +1002,7 @@ function validateRecordTitle(stack) {
|
|
|
811
1002
|
CHART_CONFIG_MISSING,
|
|
812
1003
|
CHART_FIELD_UNKNOWN,
|
|
813
1004
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
1005
|
+
PAGE_SOURCE_CLASSNAME,
|
|
814
1006
|
STYLE_CLASSNAME_TAILWIND,
|
|
815
1007
|
STYLE_NODE_MISSING_ID,
|
|
816
1008
|
STYLE_RESPONSIVE_NO_BASE,
|
|
@@ -823,6 +1015,9 @@ function validateRecordTitle(stack) {
|
|
|
823
1015
|
WIDGET_DIMENSION_UNKNOWN,
|
|
824
1016
|
WIDGET_MEASURE_UNKNOWN,
|
|
825
1017
|
validateJsxPages,
|
|
1018
|
+
validatePageSourceStyling,
|
|
1019
|
+
validateReactPageProps,
|
|
1020
|
+
validateReactPages,
|
|
826
1021
|
validateRecordTitle,
|
|
827
1022
|
validateResponsiveStyles,
|
|
828
1023
|
validateStackExpressions,
|