@objectstack/lint 11.9.0 → 12.0.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 +197 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -19
- package/dist/index.d.ts +90 -19
- package/dist/index.js +189 -23
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -371,6 +371,60 @@ function validateStackExpressions(stack) {
|
|
|
371
371
|
return issues;
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
// src/validate-list-view-mode.ts
|
|
375
|
+
var LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
|
|
376
|
+
var FORBIDDEN_FIELDS = ["userFilters", "quickFilters"];
|
|
377
|
+
function asArray3(v) {
|
|
378
|
+
if (Array.isArray(v)) return v;
|
|
379
|
+
if (v && typeof v === "object") {
|
|
380
|
+
return Object.entries(v).map(([name, def]) => ({
|
|
381
|
+
name,
|
|
382
|
+
...def
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
function scanView(view, where, path, out) {
|
|
388
|
+
if (!view || typeof view !== "object") return;
|
|
389
|
+
const rec = view;
|
|
390
|
+
for (const field of FORBIDDEN_FIELDS) {
|
|
391
|
+
if (rec[field] == null) continue;
|
|
392
|
+
out.push({
|
|
393
|
+
severity: "error",
|
|
394
|
+
rule: LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
395
|
+
where,
|
|
396
|
+
path: `${path}.${field}`,
|
|
397
|
+
message: `\`${field}\` is a page filters-mode control and is ignored on an object list view ("views" mode) \u2014 the ViewTabBar is the only nav control here.`,
|
|
398
|
+
hint: `Move \`${field}\` to a page list (InterfaceListPage, "filters" mode), or remove it. See ADR-0053.`
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function scanListViews(listViews, wherePrefix, pathPrefix, out) {
|
|
403
|
+
if (!listViews || typeof listViews !== "object") return;
|
|
404
|
+
for (const [name, view] of Object.entries(listViews)) {
|
|
405
|
+
scanView(
|
|
406
|
+
view,
|
|
407
|
+
`${wherePrefix} \u203A listViews.${name}`,
|
|
408
|
+
`${pathPrefix}.listViews.${name}`,
|
|
409
|
+
out
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function validateListViewMode(stack) {
|
|
414
|
+
const out = [];
|
|
415
|
+
asArray3(stack.objects).forEach((obj, i) => {
|
|
416
|
+
const label = typeof obj.name === "string" ? `object "${obj.name}"` : `objects[${i}]`;
|
|
417
|
+
scanListViews(obj.listViews, label, `objects[${i}]`, out);
|
|
418
|
+
});
|
|
419
|
+
asArray3(stack.views).forEach((view, i) => {
|
|
420
|
+
const named = typeof view.objectName === "string" ? view.objectName : typeof view.name === "string" ? view.name : void 0;
|
|
421
|
+
const label = named ? `view "${named}"` : `views[${i}]`;
|
|
422
|
+
scanView(view.list, `${label} \u203A list`, `views[${i}].list`, out);
|
|
423
|
+
scanListViews(view.listViews, label, `views[${i}]`, out);
|
|
424
|
+
});
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
374
428
|
// src/validate-responsive-styles.ts
|
|
375
429
|
var STYLE_NODE_MISSING_ID = "style-node-missing-id";
|
|
376
430
|
var STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
|
|
@@ -574,7 +628,7 @@ function looksLikeTailwind(className) {
|
|
|
574
628
|
return false;
|
|
575
629
|
});
|
|
576
630
|
}
|
|
577
|
-
function
|
|
631
|
+
function asArray4(v) {
|
|
578
632
|
if (Array.isArray(v)) return v;
|
|
579
633
|
if (v && typeof v === "object") {
|
|
580
634
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -667,13 +721,13 @@ function checkNode(node, pageName, path, findings) {
|
|
|
667
721
|
}
|
|
668
722
|
function validateResponsiveStyles(stack) {
|
|
669
723
|
const findings = [];
|
|
670
|
-
const pages =
|
|
724
|
+
const pages = asArray4(stack.pages);
|
|
671
725
|
for (let p = 0; p < pages.length; p++) {
|
|
672
726
|
const page = pages[p];
|
|
673
727
|
const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
|
|
674
|
-
const regions =
|
|
728
|
+
const regions = asArray4(page.regions);
|
|
675
729
|
for (let r = 0; r < regions.length; r++) {
|
|
676
|
-
const components =
|
|
730
|
+
const components = asArray4(regions[r].components);
|
|
677
731
|
for (let c = 0; c < components.length; c++) {
|
|
678
732
|
checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
|
|
679
733
|
}
|
|
@@ -684,10 +738,10 @@ function validateResponsiveStyles(stack) {
|
|
|
684
738
|
|
|
685
739
|
// src/validate-jsx-pages.ts
|
|
686
740
|
import { parseJsx, compile } from "@objectstack/sdui-parser";
|
|
687
|
-
var
|
|
741
|
+
var asArray5 = (v) => Array.isArray(v) ? v : [];
|
|
688
742
|
function validateJsxPages(stack, opts = {}) {
|
|
689
743
|
const findings = [];
|
|
690
|
-
const pages =
|
|
744
|
+
const pages = asArray5(stack.pages);
|
|
691
745
|
for (let p = 0; p < pages.length; p++) {
|
|
692
746
|
const page = pages[p];
|
|
693
747
|
if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
|
|
@@ -720,11 +774,24 @@ function validateJsxPages(stack, opts = {}) {
|
|
|
720
774
|
}
|
|
721
775
|
|
|
722
776
|
// src/validate-react-pages.ts
|
|
723
|
-
import {
|
|
724
|
-
var
|
|
777
|
+
import { createRequire } from "module";
|
|
778
|
+
var cachedTransform = null;
|
|
779
|
+
function loadSucraseTransform() {
|
|
780
|
+
if (cachedTransform) return cachedTransform;
|
|
781
|
+
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
782
|
+
try {
|
|
783
|
+
cachedTransform = createRequire(anchor)("sucrase").transform;
|
|
784
|
+
} catch (err) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`@objectstack/lint: validating a kind:'react' page requires the "sucrase" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "sucrase" in the image; it is only loaded when a react-source page is validated.`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return cachedTransform;
|
|
790
|
+
}
|
|
791
|
+
var asArray6 = (v) => Array.isArray(v) ? v : [];
|
|
725
792
|
function validateReactPages(stack) {
|
|
726
793
|
const findings = [];
|
|
727
|
-
const pages =
|
|
794
|
+
const pages = asArray6(stack.pages);
|
|
728
795
|
for (let p = 0; p < pages.length; p++) {
|
|
729
796
|
const page = pages[p];
|
|
730
797
|
if (!page || page.kind !== "react") continue;
|
|
@@ -741,6 +808,7 @@ function validateReactPages(stack) {
|
|
|
741
808
|
});
|
|
742
809
|
continue;
|
|
743
810
|
}
|
|
811
|
+
const transform = loadSucraseTransform();
|
|
744
812
|
try {
|
|
745
813
|
transform(source, { transforms: ["jsx", "typescript"], production: true });
|
|
746
814
|
} catch (err) {
|
|
@@ -759,9 +827,22 @@ function validateReactPages(stack) {
|
|
|
759
827
|
}
|
|
760
828
|
|
|
761
829
|
// src/validate-react-page-props.ts
|
|
762
|
-
import
|
|
830
|
+
import { createRequire as createRequire2 } from "module";
|
|
763
831
|
import { REACT_BLOCKS } from "@objectstack/spec/ui";
|
|
764
|
-
var
|
|
832
|
+
var cachedTs = null;
|
|
833
|
+
function loadTypeScript() {
|
|
834
|
+
if (cachedTs) return cachedTs;
|
|
835
|
+
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
836
|
+
try {
|
|
837
|
+
cachedTs = createRequire2(anchor)("typescript");
|
|
838
|
+
} catch (err) {
|
|
839
|
+
throw new Error(
|
|
840
|
+
`@objectstack/lint: validating a kind:'react' page requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a react-source page is validated.`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
return cachedTs;
|
|
844
|
+
}
|
|
845
|
+
var asArray7 = (v) => Array.isArray(v) ? v : [];
|
|
765
846
|
var BLOCKS = new Map(
|
|
766
847
|
REACT_BLOCKS.map((b) => [
|
|
767
848
|
b.tag,
|
|
@@ -800,32 +881,33 @@ function nearestKnown(prop, known) {
|
|
|
800
881
|
}
|
|
801
882
|
function validateReactPageProps(stack) {
|
|
802
883
|
const findings = [];
|
|
803
|
-
const pages =
|
|
884
|
+
const pages = asArray7(stack.pages);
|
|
804
885
|
for (let p = 0; p < pages.length; p++) {
|
|
805
886
|
const page = pages[p];
|
|
806
887
|
if (!page || page.kind !== "react") continue;
|
|
807
888
|
const source = page.source;
|
|
808
889
|
if (typeof source !== "string" || source.trim() === "") continue;
|
|
809
890
|
const name = String(page.name ?? `#${p}`);
|
|
891
|
+
const tsc = loadTypeScript();
|
|
810
892
|
let sf;
|
|
811
893
|
try {
|
|
812
|
-
sf =
|
|
894
|
+
sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
|
|
813
895
|
} catch {
|
|
814
896
|
continue;
|
|
815
897
|
}
|
|
816
898
|
const visit = (node) => {
|
|
817
|
-
if (
|
|
899
|
+
if (tsc.isJsxOpeningElement(node) || tsc.isJsxSelfClosingElement(node)) {
|
|
818
900
|
const tag = node.tagName.getText(sf);
|
|
819
901
|
const block = BLOCKS.get(tag);
|
|
820
902
|
if (block) {
|
|
821
903
|
let hasSpread = false;
|
|
822
904
|
const used = /* @__PURE__ */ new Set();
|
|
823
905
|
for (const a of node.attributes.properties) {
|
|
824
|
-
if (
|
|
906
|
+
if (tsc.isJsxSpreadAttribute(a)) {
|
|
825
907
|
hasSpread = true;
|
|
826
908
|
continue;
|
|
827
909
|
}
|
|
828
|
-
if (
|
|
910
|
+
if (tsc.isJsxAttribute(a)) used.add(a.name.getText(sf));
|
|
829
911
|
}
|
|
830
912
|
const where = `page "${name}" \u203A <${tag}>`;
|
|
831
913
|
const path = `pages[${p}].source`;
|
|
@@ -858,7 +940,7 @@ function validateReactPageProps(stack) {
|
|
|
858
940
|
}
|
|
859
941
|
}
|
|
860
942
|
}
|
|
861
|
-
|
|
943
|
+
tsc.forEachChild(node, visit);
|
|
862
944
|
};
|
|
863
945
|
visit(sf);
|
|
864
946
|
}
|
|
@@ -867,11 +949,11 @@ function validateReactPageProps(stack) {
|
|
|
867
949
|
|
|
868
950
|
// src/validate-page-source-styling.ts
|
|
869
951
|
var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
|
|
870
|
-
var
|
|
952
|
+
var asArray8 = (v) => Array.isArray(v) ? v : [];
|
|
871
953
|
var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
|
|
872
954
|
function validatePageSourceStyling(stack) {
|
|
873
955
|
const findings = [];
|
|
874
|
-
const pages =
|
|
956
|
+
const pages = asArray8(stack.pages);
|
|
875
957
|
for (let p = 0; p < pages.length; p++) {
|
|
876
958
|
const page = pages[p];
|
|
877
959
|
if (!page) continue;
|
|
@@ -900,7 +982,7 @@ function validatePageSourceStyling(stack) {
|
|
|
900
982
|
import { objectTitleCompleteness } from "@objectstack/spec/data";
|
|
901
983
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
902
984
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
903
|
-
function
|
|
985
|
+
function asArray9(v) {
|
|
904
986
|
if (Array.isArray(v)) return v;
|
|
905
987
|
if (v && typeof v === "object") {
|
|
906
988
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -909,7 +991,7 @@ function asArray8(v) {
|
|
|
909
991
|
}
|
|
910
992
|
function validateRecordTitle(stack) {
|
|
911
993
|
const findings = [];
|
|
912
|
-
const objects =
|
|
994
|
+
const objects = asArray9(stack.objects);
|
|
913
995
|
for (let i = 0; i < objects.length; i++) {
|
|
914
996
|
const obj = objects[i];
|
|
915
997
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
@@ -944,7 +1026,7 @@ function validateRecordTitle(stack) {
|
|
|
944
1026
|
var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
945
1027
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
946
1028
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
947
|
-
function
|
|
1029
|
+
function asArray10(v) {
|
|
948
1030
|
if (Array.isArray(v)) return v;
|
|
949
1031
|
if (v && typeof v === "object") {
|
|
950
1032
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -953,7 +1035,7 @@ function asArray9(v) {
|
|
|
953
1035
|
}
|
|
954
1036
|
function validateSemanticRoles(stack) {
|
|
955
1037
|
const findings = [];
|
|
956
|
-
const objects =
|
|
1038
|
+
const objects = asArray10(stack.objects);
|
|
957
1039
|
for (let i = 0; i < objects.length; i++) {
|
|
958
1040
|
const obj = objects[i];
|
|
959
1041
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -1019,11 +1101,93 @@ function validateSemanticRoles(stack) {
|
|
|
1019
1101
|
}
|
|
1020
1102
|
return findings;
|
|
1021
1103
|
}
|
|
1104
|
+
|
|
1105
|
+
// src/validate-form-layout.ts
|
|
1106
|
+
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
1107
|
+
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
1108
|
+
function asArray11(v) {
|
|
1109
|
+
if (Array.isArray(v)) return v;
|
|
1110
|
+
if (v && typeof v === "object") {
|
|
1111
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
1112
|
+
}
|
|
1113
|
+
return [];
|
|
1114
|
+
}
|
|
1115
|
+
function fieldNameOf(entry) {
|
|
1116
|
+
if (typeof entry === "string") return entry.length > 0 ? entry : null;
|
|
1117
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
1118
|
+
const f = entry.field;
|
|
1119
|
+
return typeof f === "string" && f.length > 0 ? f : null;
|
|
1120
|
+
}
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
function boundObject(view) {
|
|
1124
|
+
const data = view.data;
|
|
1125
|
+
if (data && typeof data === "object" && typeof data.object === "string") {
|
|
1126
|
+
return data.object;
|
|
1127
|
+
}
|
|
1128
|
+
return typeof view.objectName === "string" ? view.objectName : void 0;
|
|
1129
|
+
}
|
|
1130
|
+
function validateFormLayout(stack) {
|
|
1131
|
+
const findings = [];
|
|
1132
|
+
const objectFields = /* @__PURE__ */ new Map();
|
|
1133
|
+
for (const obj of asArray11(stack.objects)) {
|
|
1134
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
1135
|
+
if (!name) continue;
|
|
1136
|
+
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
1137
|
+
objectFields.set(name, new Set(fields));
|
|
1138
|
+
}
|
|
1139
|
+
const views = asArray11(stack.views);
|
|
1140
|
+
for (let i = 0; i < views.length; i++) {
|
|
1141
|
+
const view = views[i];
|
|
1142
|
+
if (!view || typeof view !== "object") continue;
|
|
1143
|
+
const sections = Array.isArray(view.sections) ? view.sections : null;
|
|
1144
|
+
if (!sections) continue;
|
|
1145
|
+
const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
|
|
1146
|
+
const objName = boundObject(view);
|
|
1147
|
+
const known = objName ? objectFields.get(objName) : void 0;
|
|
1148
|
+
const where = `view "${viewName}"`;
|
|
1149
|
+
const base = `views[${i}]`;
|
|
1150
|
+
for (let s = 0; s < sections.length; s++) {
|
|
1151
|
+
const sec = sections[s];
|
|
1152
|
+
const secFields = sec && typeof sec === "object" && Array.isArray(sec.fields) ? sec.fields : [];
|
|
1153
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
1154
|
+
const entry = secFields[f];
|
|
1155
|
+
const fname = fieldNameOf(entry);
|
|
1156
|
+
const fpath = `${base}.sections[${s}].fields[${f}]`;
|
|
1157
|
+
if (fname && known && !known.has(fname)) {
|
|
1158
|
+
findings.push({
|
|
1159
|
+
severity: "warning",
|
|
1160
|
+
rule: FORM_FIELD_UNKNOWN,
|
|
1161
|
+
where,
|
|
1162
|
+
path: fpath,
|
|
1163
|
+
message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
|
|
1164
|
+
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
const colSpan = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.colSpan : void 0;
|
|
1168
|
+
if (colSpan != null) {
|
|
1169
|
+
findings.push({
|
|
1170
|
+
severity: "warning",
|
|
1171
|
+
rule: FORM_COLSPAN_ABSOLUTE,
|
|
1172
|
+
where,
|
|
1173
|
+
path: `${fpath}.colSpan`,
|
|
1174
|
+
message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
|
|
1175
|
+
hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return findings;
|
|
1182
|
+
}
|
|
1022
1183
|
export {
|
|
1023
1184
|
CHART_CONFIG_MISSING,
|
|
1024
1185
|
CHART_FIELD_UNKNOWN,
|
|
1025
1186
|
FIELD_GROUP_EMPTY,
|
|
1026
1187
|
FIELD_GROUP_UNDECLARED,
|
|
1188
|
+
FORM_COLSPAN_ABSOLUTE,
|
|
1189
|
+
FORM_FIELD_UNKNOWN,
|
|
1190
|
+
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
1027
1191
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
1028
1192
|
PAGE_SOURCE_CLASSNAME,
|
|
1029
1193
|
SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
@@ -1038,7 +1202,9 @@ export {
|
|
|
1038
1202
|
WIDGET_DATASET_UNKNOWN,
|
|
1039
1203
|
WIDGET_DIMENSION_UNKNOWN,
|
|
1040
1204
|
WIDGET_MEASURE_UNKNOWN,
|
|
1205
|
+
validateFormLayout,
|
|
1041
1206
|
validateJsxPages,
|
|
1207
|
+
validateListViewMode,
|
|
1042
1208
|
validatePageSourceStyling,
|
|
1043
1209
|
validateReactPageProps,
|
|
1044
1210
|
validateReactPages,
|