mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
Takes Angular Material off the renderer's startup path: renderer.initialBytes 1,596,201 -> 1,344,067 bytes (-252,134, 15.8%) at the branch point; merged with master, where #1928 lands the form-field defaults per component, the branch measures 1,345,807 (-188,797, 12.3% against master's baseline). The bundler keeps each module in one chunk, so one eager import of any export carries the module's whole code; each item removes the last eager importer of one Material module. - EPG progress and update panels: eager host + `@defer (when visible)` view with an `@error` notice; the startup spinner is static markup copied from mat-spinner. - LazyMatDialog: the confirm dialog service, player service, parental lock prompt and shell actions resolve MatDialog on first use. - The theme emits tokens for the 21 imported components only, through a `$material-components` list and meta.apply; tools/nx/check-material-theme-scope.mjs (CI) keeps it in step with the imports in both directions. - No provideAnimations(): Material 22 animates with CSS and reads prefers-reduced-motion itself; apps/electron-backend-e2e/src/material-motion.e2e.ts proves dialogs, selects, menus, snack-bars and tooltips animate in and out and stop under reduced motion. Its renderer had deferred every DOM removal to the end of the tick, so the catalog list now tracks the grid offset from scroll events and the player settings panel decides "focus was inside" in a microtask, both with specs that detach the view first. - The snack-bar stays, and with it button, the icon-button chunk and the CDK overlay (about 190 KB): root services inject it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
215 lines
7.3 KiB
JavaScript
215 lines
7.3 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, stripComments } 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';
|
|
|
|
/**
|
|
* Runtime imports of a Material entry point: a static import that is not
|
|
* type-only (a `type` specifier list leaves nothing in the bundle) and a
|
|
* dynamic `import()` such as the one LazyMatDialog makes. A type-only
|
|
* import renders no component and so needs no theme.
|
|
*/
|
|
const STATIC_IMPORT =
|
|
/(?<![\w$.])import\s+(?!type\s)[^;]*?from\s+['"]@angular\/material\/([a-z-]+)['"]/g;
|
|
const DYNAMIC_IMPORT =
|
|
/\bimport\(\s*['"]@angular\/material\/([a-z-]+)['"]\s*\)/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 at runtime, statically or
|
|
* dynamically, with one importer each.
|
|
*/
|
|
export function usedEntryPoints(sources) {
|
|
const importers = new Map();
|
|
for (const { file, source } of sources) {
|
|
const code = stripComments(source);
|
|
for (const pattern of [STATIC_IMPORT, DYNAMIC_IMPORT]) {
|
|
for (const match of code.matchAll(pattern)) {
|
|
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.`
|
|
);
|
|
}
|
|
}
|