All files / packages/sds-customization-compliance/src/checks/structural no-root-component-hooks.ts

100% Statements 20/20
90% Branches 9/10
100% Functions 2/2
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77                                                            4x 4x 4x   4x 13x 13x 9x 3x     6x   6x 10x 6x 6x 2x 2x                   6x 4x               2x                  
/**
 * Rule 1: no component hooks at `:root`.
 *
 * Customer hooks (`--slds-c-*`) describe a *component's* surface and
 * are scoped to that component. Writing them at `:root` makes the
 * value escape the component scope and bleed into every other instance
 * of every component on the page. The architecture (RFC 1157) requires
 * that component hooks be written on:
 *
 *   - the customer-reachable selector (component root, host, or
 *     compound class chain on either)
 *   - inside `@layer customization`
 *
 * never `:root`.
 *
 * Note: global tokens (`--slds-g-*`, `--slds-s-*`) ARE allowed at
 * `:root`. This rule only flags `--slds-c-*` properties.
 */
 
import { classifySelector } from '../helpers/selectorIssues.js';
import type { ComplianceCheck, ComplianceRow, Offender } from '../../types.js';
import {
  declLocation,
  failRow,
  notRunYetRow,
  passRow,
  themeLayerFilesFor,
  visitHookWriteRules,
} from './internals.js';
 
const ID = 'structural-no-root-component-hooks';
const LABEL = 'No component hooks written at `:root`';
const CATEGORY = 'customer-reach' as const;
 
export const noRootComponentHooks: ComplianceCheck = (input): ComplianceRow => {
  const files = themeLayerFilesFor(input);
  if (!files) return notRunYetRow(ID, LABEL, CATEGORY);
  if (files.length === 0) {
    return passRow(ID, LABEL, 'Component has no theme or theme-base files; nothing to check.', CATEGORY);
  }
 
  const offenders: Offender[] = [];
 
  for (const file of files) {
    visitHookWriteRules(file, ({ rule, hookDecls }) => {
      const cls = classifySelector(rule.selector, input.componentName);
      if (!cls.isRoot) return;
      for (const decl of hookDecls) {
        offenders.push({
          selector: rule.selector,
          hook: decl.prop,
          location: declLocation(file, decl),
          fix: `Move \`${decl.prop}\` off \`:root\` and onto the component-root selector (e.g. \`.slds-${input.componentName}\`) so the value stays scoped to this component.`,
        });
      }
    });
  }
 
  if (offenders.length === 0) {
    return passRow(
      ID,
      LABEL,
      'No component hook (`--slds-c-*`) is written at `:root`. Component-scope writes stay inside the component.',
      CATEGORY,
    );
  }
 
  return failRow(
    ID,
    LABEL,
    `${offenders.length} component-hook write${offenders.length === 1 ? '' : 's'} at \`:root\`. ` +
      `These leak the value into every other component instance on the page; component hooks must be written ` +
      `on the customer-reachable selector inside \`@layer customization\` instead.`,
    offenders,
  );
};