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 | 4x 4x 4x 4x 13x 13x 13x 13x 13x 13x 11x 11x 28x 13x 13x 11x 14x 10x 10x 1x 1x 32x 12x 4x 16x 16x 11x 22x 11x 11x 5x 5x 6x 6x 6x 6x 6x 16x 16x 16x 16x 16x | /**
* 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.
*
* Two failure shapes share this rule:
*
* - **Scaffold absent**: no `themes/base.css` at all. The component
* hasn't started its theme-layer migration. This is the strongest
* failure mode: every other structural / hook-surface check is
* trivially passing or running on no data, which collectively
* reads as a misleading "all green" status. One unambiguous
* failure row here is the right shape — the component is *not*
* done.
* - **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');
}
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);
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);
};
|