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 126 127 128 129 130 131 132 133 | 4x 4x 4x 24x 4x 20x 16x 16x 16x 2x 2x 14x 14x 4x 21x 21x 16x 16x 16x 24x 24x 16x 16x 14x 2x 12x 16x 16x 16x 13x 13x 16x 6x 10x | /**
* Rule 2: layer placement.
*
* Each component file has a role (`base`, `theme-base`, `theme`, `aux`)
* and that role pins where its rules can write customer hooks:
*
* - `base` and `aux` files stay unlayered. They define the painting
* surface; layering them would put their declarations behind any
* unlayered customer override (which always wins under cascade-
* layer rules) and break the existing override pattern.
* - `theme` and `theme-base` files write hooks inside `@layer theme`,
* which sits below `@layer customization`. That ordering is what
* makes a customer's `--slds-c-button-color-background` reliably
* win against the theme's default value.
*
* Carveout — relay-routing in `theme-base` files: a `themes/base.css`
* file is allowed to write hooks UNLAYERED when the value uses the
* `revert-layer` fallback pattern, e.g.:
*
* .slds-dropdown-trigger .slds-button {
* --slds-c-button-color-background: var(--slds-c-dropdown-trigger-color-background, revert-layer);
* }
*
* This is the recipe-level "open the channel without changing defaults"
* pattern: the recipe owns a relay hook, routes it onto a composed
* component's canonical hook, and `revert-layer` rolls back into
* `@layer theme` when the relay isn't set so the composed component's
* own theme-layer paint wins. Layering the rule into `@layer theme`
* would put it in the SAME layer as the composed component's variant
* writes, where source order decides — and the recipe's write loads
* after, breaking the composed component's defaults. Keeping it
* unlayered (above `@layer theme`) is what lets `revert-layer` cascade
* correctly.
*
* The rule is enforced per *rule that writes a customer hook*, not
* per file: a file may legitimately mix unlayered structural rules
* with layered hook-write rules.
*/
import type { Rule } from 'postcss';
import type { ComplianceCheck, ComplianceRow, ComponentSourceFileRole, Offender } from '../../types.js';
import { isRelayRoutingRule } from '../helpers/relayRouting.js';
import { resolveHookPrefix } from '../helpers/resolveHookPrefix.js';
import { failRow, notRunYetRow, passRow, sourceFilesFor, visitHookWriteRules } from './internals.js';
const ID = 'structural-layer-placement';
const LABEL = 'Hook writes are in the expected `@layer`';
const CATEGORY = 'customer-reach' as const;
function expectedLayerFor(role: ComponentSourceFileRole): string | null {
switch (role) {
case 'base':
return null;
case 'theme':
case 'theme-base':
return 'theme';
case 'aux':
return null;
}
}
function enclosingLayerName(rule: Rule): string | null {
let node: Rule['parent'] = rule.parent;
while (node) {
if (node.type === 'atrule' && (node as { name?: string }).name === 'layer') {
const params = (node as { params?: string }).params?.trim() ?? '';
return params.split(/\s*,\s*/)[0] || null;
}
node = (node as Rule).parent;
}
return null;
}
export const layerPlacement: ComplianceCheck = (input): ComplianceRow => {
const files = sourceFilesFor(input);
if (!files) return notRunYetRow(ID, LABEL, CATEGORY);
const ownHookPrefix = resolveHookPrefix(input.componentName);
const offenders: Offender[] = [];
for (const file of files) {
const expected = expectedLayerFor(file.role);
visitHookWriteRules(file, ({ rule, hookDecls }) => {
const actual = enclosingLayerName(rule);
if (expected === actual) return;
// Relay-routing carveout: theme-base files may write hooks unlayered
// when every write forwards INTO an embedded component's hook
// FROM this component's own `--slds-c-{ownHookPrefix}-*` namespace
// with a `revert-layer` fallback. The unlayered placement is what
// lets `revert-layer` roll back into `@layer theme`; layering the
// rule would defeat the pattern.
if (file.role === 'theme-base' && actual === null && isRelayRoutingRule(hookDecls, ownHookPrefix))
return;
const got = actual ? `@layer ${actual}` : '(unlayered)';
const want = expected ? `@layer ${expected}` : '(unlayered)';
const fix = expected
? `Wrap the rule (or the surrounding section) in \`@layer ${expected} { … }\` so its hook writes sit below \`@layer customization\` and customer overrides take precedence.`
: `Lift the rule out of \`${got}\` and leave it unlayered; \`${file.role}\` files must stay unlayered so an unlayered customer override can win the cascade.`;
// Emit one offender per hook write so the hook is visible as the
// primary identity in the UI; ruling at rule-grain hid the hook
// (offender.left was empty) and made the row read as bare selector +
// location, which doesn't tell authors what they need to fix.
for (const decl of hookDecls) {
const line = decl.source?.start?.line ?? rule.source?.start?.line;
offenders.push({
selector: rule.selector,
hook: decl.prop,
location: line ? `${file.path}:${line}` : file.path,
note: `got ${got}, expected ${want} (file role: ${file.role})`,
fix,
});
}
});
}
if (offenders.length === 0) {
return passRow(
ID,
LABEL,
'Every rule that writes a customer hook is in the layer the architecture expects for its file role.',
CATEGORY,
);
}
return failRow(
ID,
LABEL,
`${offenders.length} rule${offenders.length === 1 ? '' : 's'} write customer hooks in the wrong layer. ` +
`Theme files must place hook writes inside \`@layer theme\`; base/aux files must keep them unlayered.`,
offenders,
);
};
|