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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | 4x 4x 4x 4x 6x 6x 6x 10x 12x 11x 11x 11x 7x 7x 6x 6x 6x 2x 4x 18x 18x 21x 13x 3x 10x 10x 10x 6x 4x | /**
* Rule: `themes/base.css` only reads `--slds-c-*` component hooks.
*
* The RFC 1157 implementation guide is explicit: component base files
* are "structural CSS with `var(--slds-c-*)` declarations." A
* `var(--slds-s-*)` or `var(--slds-g-*)` reference inside `themes/base.css`
* (`role: 'theme-base'`) skips the component-hook layer and breaks the
* customization promise — the customer can override `--slds-c-foo` and
* see no effect because the property is reading the shared/global tier
* directly. Themes (`themes/cosmos.css`, `themes/lightning-blue.css`)
* are where shared/global tokens get consumed, by *writing* them to
* `--slds-c-*` hooks. The base file only reads those hooks.
*
* This rule is broader than `no-shared-to-global-fallback`: that one
* catches the specific `var(--slds-s-*, var(--slds-g-*))` antipattern
* anywhere; this one catches the full class of "base file references a
* non-component token" violations, which only applies inside
* `themes/base.css`.
*
* Carveouts:
* - Hook *writes* on `--slds-c-*` may take a `--slds-g-*` literal as a
* value (`--slds-c-icon-color-foreground: var(--slds-g-color-warning-1)`).
* The text-color modifiers in `icon/themes/base.css` are the
* canonical case: a class-to-global mapping that's theme-agnostic.
* Restrictions on those writes belong to the hook-shape rules; this
* check does not flag them.
*
* Legacy `<component>.css` (role: `'base'`) and aux files are out of
* scope. They are the existing, pre-theme-layer paint surface; the
* RFC's "base file" terminology refers specifically to the new
* `themes/base.css` family.
*/
import type { Declaration } from 'postcss';
import type { ComplianceCheck, ComplianceRow, ComponentSourceFile, Offender } from '../../types.js';
import {
declLocation,
failRow,
isHookWriteDecl,
notRunYetRow,
passRow,
sourceFilesFor,
} from './internals.js';
const ID = 'structural-theme-base-reads-c-hooks-only';
const LABEL = '`themes/base.css` only reads `--slds-c-*` hooks';
const CATEGORY = 'customer-reach' as const;
const NON_COMPONENT_TOKEN_RE = /var\(\s*(--slds-(?:s|g)-[A-Za-z0-9_-]+)/g;
function selectorOf(decl: Declaration): string {
const parent = decl.parent;
return parent?.type === 'rule' ? (parent as { selector: string }).selector : '<root>';
}
function tierOf(token: string): 'shared' | 'global' {
return token.startsWith('--slds-s-') ? 'shared' : 'global';
}
function collectOffenders(file: ComponentSourceFile, into: Offender[]): void {
file.root.walkDecls((decl) => {
// Carveout: hook-write declarations are allowed to take a literal
// `var(--slds-g-*)` or `var(--slds-s-*)` as the right-hand side.
// Theme-agnostic class-to-global mappings (e.g., `.slds-icon-text-warning`
// setting `--slds-c-icon-color-foreground: var(--slds-g-color-warning-1)`)
// legitimately live in `themes/base.css`.
if (isHookWriteDecl(decl)) return;
NON_COMPONENT_TOKEN_RE.lastIndex = 0;
const seen = new Set<string>();
for (let m = NON_COMPONENT_TOKEN_RE.exec(decl.value); m; m = NON_COMPONENT_TOKEN_RE.exec(decl.value)) {
const ref = m[1];
if (seen.has(ref)) continue;
seen.add(ref);
const tier = tierOf(ref);
into.push({
selector: selectorOf(decl),
hook: ref,
prop: decl.prop,
location: declLocation(file, decl),
note: `${tier === 'shared' ? 'Shared-tier' : 'Global-tier'} token read in \`${decl.prop}\``,
fix:
tier === 'shared'
? `Read this through a \`--slds-c-*\` component hook. Themes assign \`var(${ref})\` to the component hook; the base file reads \`var(--slds-c-${stripPrefix(ref)})\`.`
: `Read this through a \`--slds-c-*\` component hook. Themes assign \`var(${ref})\` to the component hook; the base file reads the hook.`,
});
}
});
}
function stripPrefix(token: string): string {
return token.replace(/^--slds-(?:s|g)-/, '');
}
export const themeBaseReadsCHooksOnly: ComplianceCheck = (input): ComplianceRow => {
const files = sourceFilesFor(input);
if (!files) return notRunYetRow(ID, LABEL, CATEGORY);
const themeBaseFiles = files.filter((f) => f.role === 'theme-base');
if (themeBaseFiles.length === 0) {
return passRow(ID, LABEL, 'Component has no `themes/base.css`; nothing to check.', CATEGORY);
}
const offenders: Offender[] = [];
for (const file of themeBaseFiles) collectOffenders(file, offenders);
if (offenders.length === 0) {
return passRow(
ID,
LABEL,
'`themes/base.css` reads only `--slds-c-*` component hooks. No shared- or global-tier token is consumed directly.',
CATEGORY,
);
}
return failRow(
ID,
LABEL,
`${offenders.length} non-\`--slds-c-*\` token reference${offenders.length === 1 ? '' : 's'} in \`themes/base.css\`. ` +
`Per RFC 1157, base files are structural CSS that reads only component hooks; shared- (\`--slds-s-*\`) and global-tier (\`--slds-g-*\`) tokens get consumed by themes that assign them to \`--slds-c-*\` hooks. ` +
`Reading a non-component token directly here bypasses the customization layer.`,
offenders,
);
};
|