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 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | 6x 6x 6x 6x 17x 17x 17x 17x 17x 17x 13x 13x 33x 17x 17x 13x 17x 18x 10x 10x 2x 2x 36x 14x 6x 19x 19x 14x 1x 13x 26x 13x 13x 6x 6x 7x 7x 7x 7x 7x 19x 19x 19x 19x 19x | /**
* Rule: every component must ship the full theme-layer scaffold —
* `themes/base.css` plus paint for both required themes
* (`themes/cosmos.css` and `themes/lightning-blue.css`).
*
* The customer surface is per-theme: a customer using `lightning-blue`
* doesn't see paint a `cosmos`-only theme file emits, and vice
* versa. A component missing any of the three reads as "themed" in
* one product surface and "default" / unstyled in others; the
* migration is at best half-done.
*
* Severity: `fail` (`customer-reach`). A missing required file
* walls a tier of paint from the customer's resolved cascade.
*
* A component with no `themes/` directory at all hasn't opted into
* the theme layer; there is nothing for this rule to gate, so it
* passes trivially. The rule only has teeth once a `themes/` folder
* exists, at which point two failure shapes share it:
*
* - **Scaffold started but base absent**: a `themes/` folder exists
* (e.g. it already ships a per-theme file) but there's no
* `themes/base.css`. The migration is under way but the wiring
* layer every theme reads from is missing.
* - **Partial scaffold**: `themes/base.css` exists but one or both
* required theme files are missing. The migration started but
* hasn't shipped paint for every product surface yet.
*/
import type { ComplianceCheck, ComplianceRow, ComponentSourceFile, Offender } from '../../types.js';
import { failRow, notRunYetRow, passRow, sourceFilesFor } from './internals.js';
const ID = 'structural-required-themes-present';
const LABEL = 'Required themes are present (cosmos + lightning-blue)';
const CATEGORY = 'customer-reach' as const;
/**
* Themes the design-system-2 contract guarantees coverage for.
* Adding a new required theme is a deliberate migration step:
* extend this list, regenerate per-component reports, and the rule
* surfaces every component that hasn't shipped paint for it yet.
*/
const REQUIRED_THEMES: ReadonlyArray<string> = ['cosmos', 'lightning-blue'];
/**
* Extract the theme name from a `themes/<theme>.css` path. Returns
* `null` for any other shape (`themes/base.css`, top-level files,
* unrecognised nesting). Path separator is platform-aware in the
* source loader, but emitted relative paths use `/` after
* normalisation; we tolerate both for resilience.
*/
function themeNameFromPath(filePath: string): string | null {
const normalised = filePath.replaceAll('\\', '/');
const m = /(?:^|\/)themes\/([^/]+)\.css$/.exec(normalised);
Iif (!m) return null;
const name = m[1];
Iif (name === 'base') return null;
return name;
}
function presentThemeNames(files: ComponentSourceFile[]): Set<string> {
const out = new Set<string>();
for (const file of files) {
if (file.role !== 'theme') continue;
const name = themeNameFromPath(file.path);
Eif (name) out.add(name);
}
return out;
}
function hasThemeBase(files: ComponentSourceFile[]): boolean {
return files.some((f) => f.role === 'theme-base');
}
/**
* True when the component ships anything under a `themes/` directory
* (`themes/base.css`, `themes/<theme>.css`, or any other nested file).
* Path separators are normalised so the check stays cross-platform.
* When this is `false` the component has no theme-layer scaffold at
* all, which is a valid state the rule skips rather than fails.
*/
function hasThemesFolder(files: ComponentSourceFile[]): boolean {
return files.some((f) => /(?:^|\/)themes\//.test(f.path.replaceAll('\\', '/')));
}
function offenderForTheme(missingTheme: string, componentName: string): Offender {
const filePath = `packages/design-system-2/src/slds2/${componentName}/themes/${missingTheme}.css`;
return {
note: `Required theme \`${missingTheme}\` is missing — no \`themes/${missingTheme}.css\` was found for this component.`,
fix:
`Create \`${filePath}\` and assign every public hook the base ` +
`contract reads. Customers running the \`${missingTheme}\` theme ` +
`will otherwise resolve to the base-layer fallback, which is not ` +
`the theme's intended paint.`,
};
}
function offenderForMissingBase(componentName: string): Offender {
const filePath = `packages/design-system-2/src/slds2/${componentName}/themes/base.css`;
return {
note: 'Theme-layer scaffold not started — no `themes/base.css` is on disk for this component.',
fix:
`Create \`${filePath}\` as the paint-only base contract that ` +
`reads canonical \`--slds-c-*\` hooks via \`var()\`. Then add the ` +
`required per-theme files (see the other offenders on this row).`,
};
}
function buildBackticked(items: ReadonlyArray<string>, joiner: string): string {
return items.map((t) => `\`${t}\``).join(joiner);
}
function pluralise(n: number, singular: string): string {
return n === 1 ? singular : `${singular}s`;
}
export const requiredThemesPresent: ComplianceCheck = (input): ComplianceRow => {
const files = sourceFilesFor(input);
if (!files) return notRunYetRow(ID, LABEL, CATEGORY);
// No `themes/` directory at all is a valid state: the component
// hasn't opted into the theme layer, so there is nothing for this
// rule to gate. Skip validation and pass trivially.
if (!hasThemesFolder(files)) {
return passRow(
ID,
LABEL,
'No `themes/` directory is present for this component, so there is no theme-layer scaffold to validate.',
CATEGORY,
);
}
const present = presentThemeNames(files);
const missingThemes = REQUIRED_THEMES.filter((t) => !present.has(t));
const baseMissing = !hasThemeBase(files);
if (!baseMissing && missingThemes.length === 0) {
const allRequired = buildBackticked(REQUIRED_THEMES, ', ');
return passRow(
ID,
LABEL,
`All required theme files are present: \`themes/base.css\`, ${allRequired}.`,
CATEGORY,
);
}
const offenders: Offender[] = [];
if (baseMissing) offenders.push(offenderForMissingBase(input.componentName));
for (const t of missingThemes) offenders.push(offenderForTheme(t, input.componentName));
const requiredList = buildBackticked(REQUIRED_THEMES, ' and ');
const baseSegment = baseMissing
? '`themes/base.css` is missing (the component has no theme-layer scaffold)'
: '`themes/base.css` is present';
const themesMissingLabel = buildBackticked(missingThemes, ', ');
const isAre = missingThemes.length === 1 ? 'is' : 'are';
const themesSegment =
missingThemes.length === 0
? ''
: ` and ${missingThemes.length} required ${pluralise(missingThemes.length, 'theme')} ${pluralise(missingThemes.length, 'file')} ${isAre} missing (${themesMissingLabel})`;
const detail =
`${baseSegment}${themesSegment}. Every SLDS2 component must ship ` +
`\`themes/base.css\` plus paint for ${requiredList} so the ` +
`customer's resolved cascade matches the active theme regardless of ` +
`which product surface they're running.`;
return failRow(ID, LABEL, detail, offenders);
};
|