@objectstack/lint 10.2.0 → 10.3.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.d.cts CHANGED
@@ -67,7 +67,7 @@ interface WidgetBindingFinding {
67
67
  /** How to fix (or deliberately suppress) it. */
68
68
  hint: string;
69
69
  }
70
- type AnyRec$1 = Record<string, unknown>;
70
+ type AnyRec$2 = Record<string, unknown>;
71
71
  /**
72
72
  * Validate every dashboard widget's dataset binding. Returns the list of
73
73
  * findings (empty = clean). Caller decides how to surface them: `error`
@@ -75,7 +75,7 @@ type AnyRec$1 = Record<string, unknown>;
75
75
  * should fail validate/build; `warning` findings are advisory and must
76
76
  * never fail the build on their own.
77
77
  */
78
- declare function validateWidgetBindings(stack: AnyRec$1): WidgetBindingFinding[];
78
+ declare function validateWidgetBindings(stack: AnyRec$2): WidgetBindingFinding[];
79
79
 
80
80
  interface ExprIssue {
81
81
  where: string;
@@ -88,11 +88,36 @@ interface ExprIssue {
88
88
  */
89
89
  severity?: 'error' | 'warning';
90
90
  }
91
- type AnyRec = Record<string, unknown>;
91
+ type AnyRec$1 = Record<string, unknown>;
92
92
  /**
93
93
  * Validate every predicate in the stack. Returns the list of issues (empty =
94
94
  * clean). Caller decides how to surface / whether to fail the build.
95
95
  */
96
- declare function validateStackExpressions(stack: AnyRec): ExprIssue[];
96
+ declare function validateStackExpressions(stack: AnyRec$1): ExprIssue[];
97
+
98
+ type StyleSeverity = 'error' | 'warning';
99
+ interface StyleFinding {
100
+ severity: StyleSeverity;
101
+ rule: string;
102
+ /** Human-readable location, e.g. `page "pricing" › node "plan_solo"`. */
103
+ where: string;
104
+ /** Config path, e.g. `pages[0].regions[0].components[1]`. */
105
+ path: string;
106
+ message: string;
107
+ hint: string;
108
+ }
109
+ declare const STYLE_NODE_MISSING_ID = "style-node-missing-id";
110
+ declare const STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
111
+ declare const STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
112
+ declare const STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
113
+ declare const STYLE_UNKNOWN_TOKEN = "style-unknown-token";
114
+ type AnyRec = Record<string, unknown>;
115
+ /**
116
+ * Validate every page's component tree for SDUI styling correctness (ADR-0065).
117
+ * Returns findings (empty = clean). `error` findings describe styles that are
118
+ * silently dropped and should fail validate/build; `warning` findings are
119
+ * advisory (typos, drift, footguns).
120
+ */
121
+ declare function validateResponsiveStyles(stack: AnyRec): StyleFinding[];
97
122
 
98
- export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, MEASURE_AGGREGATE_INCOHERENT, TABLE_COUNT_ONLY, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateStackExpressions, validateWidgetBindings };
123
+ export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, MEASURE_AGGREGATE_INCOHERENT, 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, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateResponsiveStyles, validateStackExpressions, validateWidgetBindings };
package/dist/index.d.ts CHANGED
@@ -67,7 +67,7 @@ interface WidgetBindingFinding {
67
67
  /** How to fix (or deliberately suppress) it. */
68
68
  hint: string;
69
69
  }
70
- type AnyRec$1 = Record<string, unknown>;
70
+ type AnyRec$2 = Record<string, unknown>;
71
71
  /**
72
72
  * Validate every dashboard widget's dataset binding. Returns the list of
73
73
  * findings (empty = clean). Caller decides how to surface them: `error`
@@ -75,7 +75,7 @@ type AnyRec$1 = Record<string, unknown>;
75
75
  * should fail validate/build; `warning` findings are advisory and must
76
76
  * never fail the build on their own.
77
77
  */
78
- declare function validateWidgetBindings(stack: AnyRec$1): WidgetBindingFinding[];
78
+ declare function validateWidgetBindings(stack: AnyRec$2): WidgetBindingFinding[];
79
79
 
80
80
  interface ExprIssue {
81
81
  where: string;
@@ -88,11 +88,36 @@ interface ExprIssue {
88
88
  */
89
89
  severity?: 'error' | 'warning';
90
90
  }
91
- type AnyRec = Record<string, unknown>;
91
+ type AnyRec$1 = Record<string, unknown>;
92
92
  /**
93
93
  * Validate every predicate in the stack. Returns the list of issues (empty =
94
94
  * clean). Caller decides how to surface / whether to fail the build.
95
95
  */
96
- declare function validateStackExpressions(stack: AnyRec): ExprIssue[];
96
+ declare function validateStackExpressions(stack: AnyRec$1): ExprIssue[];
97
+
98
+ type StyleSeverity = 'error' | 'warning';
99
+ interface StyleFinding {
100
+ severity: StyleSeverity;
101
+ rule: string;
102
+ /** Human-readable location, e.g. `page "pricing" › node "plan_solo"`. */
103
+ where: string;
104
+ /** Config path, e.g. `pages[0].regions[0].components[1]`. */
105
+ path: string;
106
+ message: string;
107
+ hint: string;
108
+ }
109
+ declare const STYLE_NODE_MISSING_ID = "style-node-missing-id";
110
+ declare const STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
111
+ declare const STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
112
+ declare const STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
113
+ declare const STYLE_UNKNOWN_TOKEN = "style-unknown-token";
114
+ type AnyRec = Record<string, unknown>;
115
+ /**
116
+ * Validate every page's component tree for SDUI styling correctness (ADR-0065).
117
+ * Returns findings (empty = clean). `error` findings describe styles that are
118
+ * silently dropped and should fail validate/build; `warning` findings are
119
+ * advisory (typos, drift, footguns).
120
+ */
121
+ declare function validateResponsiveStyles(stack: AnyRec): StyleFinding[];
97
122
 
98
- export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, MEASURE_AGGREGATE_INCOHERENT, TABLE_COUNT_ONLY, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateStackExpressions, validateWidgetBindings };
123
+ export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, MEASURE_AGGREGATE_INCOHERENT, 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, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateResponsiveStyles, validateStackExpressions, validateWidgetBindings };
package/dist/index.js CHANGED
@@ -370,14 +370,331 @@ function validateStackExpressions(stack) {
370
370
  }
371
371
  return issues;
372
372
  }
373
+
374
+ // src/validate-responsive-styles.ts
375
+ var STYLE_NODE_MISSING_ID = "style-node-missing-id";
376
+ var STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
377
+ var STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
378
+ var STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
379
+ var STYLE_UNKNOWN_TOKEN = "style-unknown-token";
380
+ var BREAKPOINTS = ["large", "medium", "small", "xsmall"];
381
+ var KNOWN_TOKENS = /* @__PURE__ */ new Set([
382
+ // SDUI tokens
383
+ "space-1",
384
+ "space-2",
385
+ "space-3",
386
+ "space-4",
387
+ "space-5",
388
+ "space-6",
389
+ "space-8",
390
+ "space-10",
391
+ "space-12",
392
+ "radius",
393
+ "radius-sm",
394
+ "radius-md",
395
+ "radius-lg",
396
+ "radius-xl",
397
+ "shadow-sm",
398
+ "shadow-md",
399
+ "shadow-lg",
400
+ "surface",
401
+ "surface-sunken",
402
+ "text-strong",
403
+ "text-muted",
404
+ "brand",
405
+ "brand-foreground",
406
+ "hairline",
407
+ // Base theme tokens (shadcn) — usually wrapped as hsl(var(--x)).
408
+ "background",
409
+ "foreground",
410
+ "card",
411
+ "card-foreground",
412
+ "popover",
413
+ "popover-foreground",
414
+ "primary",
415
+ "primary-foreground",
416
+ "secondary",
417
+ "secondary-foreground",
418
+ "muted",
419
+ "muted-foreground",
420
+ "accent",
421
+ "accent-foreground",
422
+ "destructive",
423
+ "destructive-foreground",
424
+ "border",
425
+ "input",
426
+ "ring",
427
+ "success",
428
+ "success-foreground",
429
+ "warning",
430
+ "warning-foreground",
431
+ "chart-1",
432
+ "chart-2",
433
+ "chart-3",
434
+ "chart-4",
435
+ "chart-5"
436
+ ]);
437
+ var KNOWN_CSS_PROPERTIES = /* @__PURE__ */ new Set([
438
+ "display",
439
+ "position",
440
+ "top",
441
+ "right",
442
+ "bottom",
443
+ "left",
444
+ "inset",
445
+ "zIndex",
446
+ "overflow",
447
+ "overflowX",
448
+ "overflowY",
449
+ "visibility",
450
+ "boxSizing",
451
+ "float",
452
+ "clear",
453
+ "width",
454
+ "height",
455
+ "minWidth",
456
+ "minHeight",
457
+ "maxWidth",
458
+ "maxHeight",
459
+ "aspectRatio",
460
+ "margin",
461
+ "marginTop",
462
+ "marginRight",
463
+ "marginBottom",
464
+ "marginLeft",
465
+ "marginInline",
466
+ "marginBlock",
467
+ "padding",
468
+ "paddingTop",
469
+ "paddingRight",
470
+ "paddingBottom",
471
+ "paddingLeft",
472
+ "paddingInline",
473
+ "paddingBlock",
474
+ "flex",
475
+ "flexDirection",
476
+ "flexWrap",
477
+ "flexGrow",
478
+ "flexShrink",
479
+ "flexBasis",
480
+ "alignItems",
481
+ "alignContent",
482
+ "alignSelf",
483
+ "justifyContent",
484
+ "justifyItems",
485
+ "justifySelf",
486
+ "gap",
487
+ "rowGap",
488
+ "columnGap",
489
+ "order",
490
+ "placeItems",
491
+ "placeContent",
492
+ "grid",
493
+ "gridTemplate",
494
+ "gridTemplateColumns",
495
+ "gridTemplateRows",
496
+ "gridTemplateAreas",
497
+ "gridColumn",
498
+ "gridRow",
499
+ "gridArea",
500
+ "gridAutoFlow",
501
+ "gridAutoColumns",
502
+ "gridAutoRows",
503
+ "color",
504
+ "backgroundColor",
505
+ "background",
506
+ "backgroundImage",
507
+ "backgroundSize",
508
+ "backgroundPosition",
509
+ "backgroundRepeat",
510
+ "backgroundClip",
511
+ "opacity",
512
+ "mixBlendMode",
513
+ "fontSize",
514
+ "fontWeight",
515
+ "fontFamily",
516
+ "fontStyle",
517
+ "lineHeight",
518
+ "letterSpacing",
519
+ "textAlign",
520
+ "textTransform",
521
+ "textDecoration",
522
+ "textOverflow",
523
+ "whiteSpace",
524
+ "wordBreak",
525
+ "overflowWrap",
526
+ "fontVariantNumeric",
527
+ "verticalAlign",
528
+ "textShadow",
529
+ "border",
530
+ "borderTop",
531
+ "borderRight",
532
+ "borderBottom",
533
+ "borderLeft",
534
+ "borderColor",
535
+ "borderWidth",
536
+ "borderStyle",
537
+ "borderRadius",
538
+ "borderTopLeftRadius",
539
+ "borderTopRightRadius",
540
+ "borderBottomLeftRadius",
541
+ "borderBottomRightRadius",
542
+ "outline",
543
+ "outlineOffset",
544
+ "boxShadow",
545
+ "transform",
546
+ "transformOrigin",
547
+ "transition",
548
+ "transitionProperty",
549
+ "transitionDuration",
550
+ "transitionTimingFunction",
551
+ "transitionDelay",
552
+ "animation",
553
+ "filter",
554
+ "backdropFilter",
555
+ "willChange",
556
+ "cursor",
557
+ "pointerEvents",
558
+ "userSelect",
559
+ "objectFit",
560
+ "objectPosition",
561
+ "content"
562
+ ]);
563
+ var VAR_RE = /var\(\s*--([a-zA-Z0-9-]+)\s*[,)]/g;
564
+ var TW_VARIANT = /^(sm|md|lg|xl|2xl|hover|focus|active|disabled|dark|group-hover|peer-[a-z]+|first|last|odd|even):/;
565
+ var TW_STEM_VALUE = /^-?(p|m|px|py|pt|pb|pl|pr|mx|my|mt|mb|ml|mr|gap|gap-x|gap-y|space-x|space-y|w|h|min-w|max-w|min-h|max-h|size|text|leading|tracking|bg|border|rounded|shadow|ring|opacity|inset|top|bottom|left|right|z|order|col|row|grid-cols|grid-rows|basis)-(\d+(\.\d+)?|\d+\/\d+|px|full|auto|none|screen|min|max|fit|xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl)$/;
566
+ var TW_BARE = /^(flex|grid|block|inline|inline-block|inline-flex|hidden|contents|table|flow-root|grow|shrink|truncate|italic|underline|uppercase|lowercase|capitalize|antialiased|absolute|relative|fixed|sticky|static|isolate|flex-col|flex-row|flex-wrap|flex-nowrap|items-center|items-start|items-end|items-stretch|justify-center|justify-between|justify-around|justify-start|justify-end|text-center|text-left|text-right|font-bold|font-semibold|font-medium|font-normal|tabular-nums)$/;
567
+ function looksLikeTailwind(className) {
568
+ return className.split(/\s+/).some((tok) => {
569
+ if (!tok) return false;
570
+ if (TW_VARIANT.test(tok)) return true;
571
+ if (/\[[^\]]+\]/.test(tok)) return true;
572
+ if (TW_STEM_VALUE.test(tok)) return true;
573
+ if (TW_BARE.test(tok)) return true;
574
+ return false;
575
+ });
576
+ }
577
+ function asArray3(v) {
578
+ if (Array.isArray(v)) return v;
579
+ if (v && typeof v === "object") {
580
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
581
+ }
582
+ return [];
583
+ }
584
+ function childrenOf(node) {
585
+ const props = node.properties ?? {};
586
+ const out = [];
587
+ for (const c of [node.children, props.children, node.body, props.body]) {
588
+ if (Array.isArray(c)) out.push(...c.filter((x) => x && typeof x === "object"));
589
+ }
590
+ return out;
591
+ }
592
+ function checkNode(node, pageName, path, findings) {
593
+ const id = typeof node.id === "string" ? node.id : void 0;
594
+ const type = typeof node.type === "string" ? node.type : "node";
595
+ const where = `page "${pageName}" \u203A ${id ? `node "${id}"` : `<${type}>`}`;
596
+ const rs = node.responsiveStyles;
597
+ const hasRs = !!rs && typeof rs === "object" && BREAKPOINTS.some((b) => rs[b]);
598
+ if (hasRs && !id) {
599
+ findings.push({
600
+ severity: "error",
601
+ rule: STYLE_NODE_MISSING_ID,
602
+ where,
603
+ path,
604
+ message: `Node has responsiveStyles but no \`id\`; scoped CSS cannot be generated and the styles are silently dropped.`,
605
+ hint: `Add a stable \`id\` to this node.`
606
+ });
607
+ }
608
+ if (hasRs && !rs.large && BREAKPOINTS.slice(1).some((b) => rs[b])) {
609
+ findings.push({
610
+ severity: "warning",
611
+ rule: STYLE_RESPONSIVE_NO_BASE,
612
+ where,
613
+ path,
614
+ message: `responsiveStyles sets a smaller breakpoint but no \`large\` base; the node is unstyled at desktop width.`,
615
+ hint: `Put the unconditional/base styles under \`responsiveStyles.large\` (desktop-first).`
616
+ });
617
+ }
618
+ if (typeof node.className === "string" && node.className.trim() && looksLikeTailwind(node.className)) {
619
+ findings.push({
620
+ severity: "warning",
621
+ rule: STYLE_CLASSNAME_TAILWIND,
622
+ where,
623
+ path,
624
+ message: `\`className\` contains Tailwind-looking utilities ("${node.className.trim().slice(0, 60)}"); these are not compiled from metadata and will silently do nothing.`,
625
+ hint: `Style this node with \`responsiveStyles\` + design tokens instead of \`className\` (ADR-0065).`
626
+ });
627
+ }
628
+ if (rs && typeof rs === "object") {
629
+ for (const bp of BREAKPOINTS) {
630
+ const map = rs[bp];
631
+ if (!map || typeof map !== "object") continue;
632
+ for (const [prop, value] of Object.entries(map)) {
633
+ if (!prop.startsWith("--") && !KNOWN_CSS_PROPERTIES.has(prop)) {
634
+ findings.push({
635
+ severity: "warning",
636
+ rule: STYLE_UNKNOWN_CSS_PROPERTY,
637
+ where,
638
+ path: `${path}.responsiveStyles.${bp}`,
639
+ message: `Unknown CSS property "${prop}" (typo?); if unintended it will not apply.`,
640
+ hint: `Use a camelCase CSS property name (e.g. \`flexDirection\`, \`backgroundColor\`).`
641
+ });
642
+ }
643
+ if (typeof value === "string") {
644
+ let m;
645
+ VAR_RE.lastIndex = 0;
646
+ while (m = VAR_RE.exec(value)) {
647
+ const token = m[1];
648
+ if (!KNOWN_TOKENS.has(token) && !token.startsWith("tw-")) {
649
+ findings.push({
650
+ severity: "warning",
651
+ rule: STYLE_UNKNOWN_TOKEN,
652
+ where,
653
+ path: `${path}.responsiveStyles.${bp}.${prop}`,
654
+ message: `References unknown design token \`var(--${token})\` (typo?); it will not resolve.`,
655
+ hint: `Use a token from the ADR-0065 palette (e.g. \`var(--space-6)\`, \`var(--surface)\`, \`hsl(var(--primary))\`).`
656
+ });
657
+ }
658
+ }
659
+ }
660
+ }
661
+ }
662
+ }
663
+ const kids = childrenOf(node);
664
+ for (let i = 0; i < kids.length; i++) {
665
+ checkNode(kids[i], pageName, `${path}.children[${i}]`, findings);
666
+ }
667
+ }
668
+ function validateResponsiveStyles(stack) {
669
+ const findings = [];
670
+ const pages = asArray3(stack.pages);
671
+ for (let p = 0; p < pages.length; p++) {
672
+ const page = pages[p];
673
+ const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
674
+ const regions = asArray3(page.regions);
675
+ for (let r = 0; r < regions.length; r++) {
676
+ const components = asArray3(regions[r].components);
677
+ for (let c = 0; c < components.length; c++) {
678
+ checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
679
+ }
680
+ }
681
+ }
682
+ return findings;
683
+ }
373
684
  export {
374
685
  CHART_CONFIG_MISSING,
375
686
  CHART_FIELD_UNKNOWN,
376
687
  MEASURE_AGGREGATE_INCOHERENT,
688
+ STYLE_CLASSNAME_TAILWIND,
689
+ STYLE_NODE_MISSING_ID,
690
+ STYLE_RESPONSIVE_NO_BASE,
691
+ STYLE_UNKNOWN_CSS_PROPERTY,
692
+ STYLE_UNKNOWN_TOKEN,
377
693
  TABLE_COUNT_ONLY,
378
694
  WIDGET_DATASET_UNKNOWN,
379
695
  WIDGET_DIMENSION_UNKNOWN,
380
696
  WIDGET_MEASURE_UNKNOWN,
697
+ validateResponsiveStyles,
381
698
  validateStackExpressions,
382
699
  validateWidgetBindings
383
700
  };