mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
m3-theme.scss included mat.all-component-themes and mat.all-component-colors, which emit the tokens of every Material component: 23,646 bytes of the initial stylesheet belonged to 17 components nothing imports (datepicker, chips, stepper, expansion, radio, tabs, badge, table, paginator, sidenav, bottom sheet, toolbar, timepicker, grid list, tree, autocomplete, sort). A $material-components list, in Material's order, now drives the *-theme and *-color mixins through meta.apply, so light and dark cannot drift apart. tools/nx/check-material-theme-scope.mjs (CI) compares the list with the @angular/material/<entry> imports of the runtime sources in both directions: a newly imported component must be themed, and a component whose last import went must leave the list. core (ripples, options, pseudo-checkboxes) always stays, and the button entry point keeps button, icon-button and fab together. The compiled stylesheet lost exactly 426 declarations, all tokens of those 17 components plus the dead .mat-drawer-inner-container rule in styles.scss (sidenav is never imported); nothing was added or changed. renderer.initialBytes: 1,432,237 -> 1,408,099 bytes (-24,138) on a local production build. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
200 lines
6.8 KiB
JavaScript
200 lines
6.8 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import { readFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { isScanned } from './check-theme-references.mjs';
|
|
|
|
/**
|
|
* `apps/web/src/m3-theme.scss` themes only the Material components the app
|
|
* imports, through its `$material-components` list; `all-component-themes`
|
|
* also emitted about 24 KB of tokens for components nothing uses. This guard
|
|
* keeps the list in step with the imports in both directions: a component a
|
|
* new `@angular/material/<entry>` import brings in must be themed, or it
|
|
* renders with no colours at all, and a component whose last import went
|
|
* must leave the list.
|
|
*/
|
|
|
|
/** Angular renderer sources that load the theme (the remote control has its own styles). */
|
|
const SCANNED_PATHSPECS = ['apps/web/*.ts', 'libs/*.ts'];
|
|
|
|
export const THEME_FILE = 'apps/web/src/m3-theme.scss';
|
|
|
|
const ENTRY_POINT_IMPORT = /from\s+['"]@angular\/material\/([a-z-]+)['"]/g;
|
|
|
|
/**
|
|
* The theme mixins (`mat.<name>-theme`, `mat.<name>-color`) each Material
|
|
* entry point needs. An entry point that is not listed has no themed
|
|
* component.
|
|
*/
|
|
export const THEMES_BY_ENTRY_POINT = new Map([
|
|
['autocomplete', ['autocomplete']],
|
|
['badge', ['badge']],
|
|
['bottom-sheet', ['bottom-sheet']],
|
|
['button', ['button', 'icon-button', 'fab']],
|
|
['button-toggle', ['button-toggle']],
|
|
['card', ['card']],
|
|
['checkbox', ['checkbox']],
|
|
['chips', ['chips']],
|
|
['core', ['core']],
|
|
['datepicker', ['datepicker']],
|
|
['dialog', ['dialog']],
|
|
['divider', ['divider']],
|
|
['expansion', ['expansion']],
|
|
['form-field', ['form-field']],
|
|
['grid-list', ['grid-list']],
|
|
['icon', ['icon']],
|
|
['input', ['input']],
|
|
['list', ['list']],
|
|
['menu', ['menu']],
|
|
['paginator', ['paginator']],
|
|
['progress-bar', ['progress-bar']],
|
|
['progress-spinner', ['progress-spinner']],
|
|
['radio', ['radio']],
|
|
['select', ['select']],
|
|
['sidenav', ['sidenav']],
|
|
['slide-toggle', ['slide-toggle']],
|
|
['slider', ['slider']],
|
|
['snack-bar', ['snack-bar']],
|
|
['sort', ['sort']],
|
|
['stepper', ['stepper']],
|
|
['table', ['table']],
|
|
['tabs', ['tabs']],
|
|
['timepicker', ['timepicker']],
|
|
['toolbar', ['toolbar']],
|
|
['tooltip', ['tooltip']],
|
|
['tree', ['tree']],
|
|
]);
|
|
|
|
/**
|
|
* Themed whatever is imported: `core` carries the ripple, option, optgroup
|
|
* and pseudo-checkbox tokens that select, menu and list render with.
|
|
*/
|
|
export const ALWAYS_THEMED = ['core'];
|
|
|
|
/** Material entry points the runtime sources import, with one importer each. */
|
|
export function usedEntryPoints(sources) {
|
|
const importers = new Map();
|
|
for (const { file, source } of sources) {
|
|
for (const match of source.matchAll(ENTRY_POINT_IMPORT)) {
|
|
if (!importers.has(match[1])) importers.set(match[1], file);
|
|
}
|
|
}
|
|
return importers;
|
|
}
|
|
|
|
/** The theme names the imported entry points need, each with a reason. */
|
|
export function requiredThemes(importers) {
|
|
const required = new Map(
|
|
ALWAYS_THEMED.map((name) => [name, 'always themed'])
|
|
);
|
|
for (const [entryPoint, file] of importers) {
|
|
for (const name of THEMES_BY_ENTRY_POINT.get(entryPoint) ?? []) {
|
|
if (!required.has(name)) {
|
|
required.set(
|
|
name,
|
|
`@angular/material/${entryPoint} in ${file}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return required;
|
|
}
|
|
|
|
/**
|
|
* The components the theme file themes: the `$material-components` list
|
|
* plus any `mat.<name>-theme` included directly. `all` means
|
|
* `all-component-themes` is still included, which themes everything.
|
|
*/
|
|
export function themedComponents(scss) {
|
|
const themed = new Set();
|
|
const list = scss.match(/\$material-components\s*:\s*\(([^)]*)\)/);
|
|
if (list) {
|
|
for (const item of list[1].split(',')) {
|
|
const name = item.replace(/\/\/.*$/gm, '').trim();
|
|
if (name) themed.add(name);
|
|
}
|
|
}
|
|
for (const match of scss.matchAll(/@include\s+mat\.([a-z-]+)-theme\(/g)) {
|
|
themed.add(match[1]);
|
|
}
|
|
return {
|
|
themed,
|
|
all: /@include\s+mat\.all-component-themes\(/.test(scss),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Themes that imports need but the file lacks, and themes the file carries
|
|
* that no import needs.
|
|
*/
|
|
export function compareThemeScope(required, { themed, all }) {
|
|
const unthemed = all
|
|
? []
|
|
: [...required].filter(([name]) => !themed.has(name));
|
|
const unused = [...themed].filter((name) => !required.has(name));
|
|
return { unthemed, unused };
|
|
}
|
|
|
|
/** Tracked renderer sources the guard reads, at any depth. */
|
|
export function listScannedFiles(rootDir) {
|
|
// No shell: `cmd.exe` treats single quotes as literal characters, so a
|
|
// POSIX-quoted pathspec reaches git intact on Windows and matches nothing.
|
|
return execFileSync('git', ['ls-files', ...SCANNED_PATHSPECS], {
|
|
cwd: rootDir,
|
|
encoding: 'utf8',
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
})
|
|
.trim()
|
|
.split('\n')
|
|
.filter(Boolean)
|
|
.filter(isScanned);
|
|
}
|
|
|
|
const isMain =
|
|
process.argv[1] &&
|
|
path.resolve(process.argv[1]) ===
|
|
path.resolve(fileURLToPath(import.meta.url));
|
|
|
|
if (isMain) {
|
|
const rootDir = process.cwd();
|
|
const files = listScannedFiles(rootDir);
|
|
const sources = await Promise.all(
|
|
files.map(async (file) => ({
|
|
file,
|
|
source: await readFile(path.join(rootDir, file), 'utf8'),
|
|
}))
|
|
);
|
|
const required = requiredThemes(usedEntryPoints(sources));
|
|
const scope = themedComponents(
|
|
await readFile(path.join(rootDir, THEME_FILE), 'utf8')
|
|
);
|
|
const { unthemed, unused } = compareThemeScope(required, scope);
|
|
|
|
if (unthemed.length > 0) {
|
|
console.error(
|
|
`Material components the app imports but ${THEME_FILE} does not theme; they render without colours. Add them to $material-components:`
|
|
);
|
|
for (const [name, reason] of unthemed) {
|
|
console.error(`- ${name} (${reason})`);
|
|
}
|
|
process.exitCode = 1;
|
|
}
|
|
if (unused.length > 0) {
|
|
console.error(
|
|
`Material components ${THEME_FILE} themes although no runtime source imports them; their tokens only add to the initial stylesheet. Remove them from $material-components:`
|
|
);
|
|
for (const name of unused) {
|
|
console.error(`- ${name}`);
|
|
}
|
|
process.exitCode = 1;
|
|
}
|
|
if (!process.exitCode) {
|
|
console.log(
|
|
scope.all
|
|
? `Checked ${files.length} renderer sources; ${THEME_FILE} themes every Material component through all-component-themes.`
|
|
: `Checked ${files.length} renderer sources; ${THEME_FILE} themes exactly the ${scope.themed.size} Material components they import.`
|
|
);
|
|
}
|
|
}
|