@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 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,8 @@ __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,
38
51
  validateReactPages: () => validateReactPages,
39
52
  validateRecordTitle: () => validateRecordTitle,
40
53
  validateResponsiveStyles: () => validateResponsiveStyles,
@@ -803,11 +816,149 @@ function validateReactPages(stack) {
803
816
  return findings;
804
817
  }
805
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
+
806
957
  // src/validate-record-title.ts
807
958
  var import_data2 = require("@objectstack/spec/data");
808
959
  var TITLE_FORMAT_RETIRED = "title-format-retired";
809
960
  var TITLE_UNRESOLVABLE = "title-unresolvable";
810
- function asArray6(v) {
961
+ function asArray8(v) {
811
962
  if (Array.isArray(v)) return v;
812
963
  if (v && typeof v === "object") {
813
964
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -816,7 +967,7 @@ function asArray6(v) {
816
967
  }
817
968
  function validateRecordTitle(stack) {
818
969
  const findings = [];
819
- const objects = asArray6(stack.objects);
970
+ const objects = asArray8(stack.objects);
820
971
  for (let i = 0; i < objects.length; i++) {
821
972
  const obj = objects[i];
822
973
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -851,6 +1002,7 @@ function validateRecordTitle(stack) {
851
1002
  CHART_CONFIG_MISSING,
852
1003
  CHART_FIELD_UNKNOWN,
853
1004
  MEASURE_AGGREGATE_INCOHERENT,
1005
+ PAGE_SOURCE_CLASSNAME,
854
1006
  STYLE_CLASSNAME_TAILWIND,
855
1007
  STYLE_NODE_MISSING_ID,
856
1008
  STYLE_RESPONSIVE_NO_BASE,
@@ -863,6 +1015,8 @@ function validateRecordTitle(stack) {
863
1015
  WIDGET_DIMENSION_UNKNOWN,
864
1016
  WIDGET_MEASURE_UNKNOWN,
865
1017
  validateJsxPages,
1018
+ validatePageSourceStyling,
1019
+ validateReactPageProps,
866
1020
  validateReactPages,
867
1021
  validateRecordTitle,
868
1022
  validateResponsiveStyles,