mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
The audio player's play/pause control was the app's only mat-fab. It is now a mat-icon-button drawn as the same 56px primary circle (icon-button overrides: 56px state layer, 32px white icon, 50% shape, selection-colour fill, hover scale), so the eager button chunk no longer carries the fab directives and the theme no longer emits the fab tokens: renderer.initialBytes 1,349,048 -> 1,329,249 bytes (-19,799: -13,799 in button.mjs, -6,028 in the stylesheet), measured locally against the merge base. The fab also reset the keyboard focus ring with `outline: none`; the icon button keeps the app ring. - tools/nx/check-material-theme-scope.mjs: `button` brings button and icon-button; `fab` is required only while a runtime template renders mat-fab / mat-mini-fab (matFab / matMiniFab), found through the component templates the way check-form-field-defaults.mjs resolves them. That resolution moves to tools/nx/component-templates.mjs, shared by both. Templates know HTML comments only, so the scan strips just those: a `//` in text or an attribute value no longer hides a fab on the same line. - apps/web/src/m3-theme.scss drops `fab` from $material-components. - apps/web-e2e: the Stalker radio test checks the control's size, shape, fill, icon and keyboard focus ring. - journey-baselines.json: renderer.initialBytes 1,345,807 -> 1,329,249. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
92 lines
3.1 KiB
JavaScript
92 lines
3.1 KiB
JavaScript
import path from 'node:path';
|
|
import ts from 'typescript';
|
|
|
|
/**
|
|
* The templates the app renders, found from the `@Component` decorators of
|
|
* its TypeScript sources: an inline `template` literal, or the file a
|
|
* `templateUrl` names. A guard that asks "does the app render this
|
|
* element?" reads these, not every `.html` file: an orphaned template that
|
|
* no component names never reaches a user.
|
|
*/
|
|
|
|
/** The object literal of an `@Component({...})` decorator, if `node` is one. */
|
|
export function componentMetadata(node) {
|
|
if (
|
|
!ts.isDecorator(node) ||
|
|
!ts.isCallExpression(node.expression) ||
|
|
!ts.isIdentifier(node.expression.expression) ||
|
|
node.expression.expression.text !== 'Component'
|
|
) {
|
|
return undefined;
|
|
}
|
|
const [metadata] = node.expression.arguments;
|
|
return metadata && ts.isObjectLiteralExpression(metadata)
|
|
? metadata
|
|
: undefined;
|
|
}
|
|
|
|
/** The initializer of the `name` property of a metadata literal. */
|
|
export function property(metadata, name) {
|
|
return metadata.properties.find(
|
|
(entry) =>
|
|
ts.isPropertyAssignment(entry) &&
|
|
(ts.isIdentifier(entry.name) || ts.isStringLiteral(entry.name)) &&
|
|
entry.name.text === name
|
|
)?.initializer;
|
|
}
|
|
|
|
/** Literal text of a quoted, backticked or interpolated template. */
|
|
function literalText(node) {
|
|
if (ts.isStringLiteralLike(node)) return node.text;
|
|
if (ts.isTemplateExpression(node)) {
|
|
return [
|
|
node.head.text,
|
|
...node.templateSpans.map((span) => span.literal.text),
|
|
].join('');
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* The template text of one `@Component` metadata literal: its inline
|
|
* `template`, or else the file its `templateUrl` names, which `readTemplate`
|
|
* reads by a path relative to the component's `file`. Undefined when the
|
|
* component declares neither as a literal.
|
|
*/
|
|
export async function componentTemplate(metadata, file, readTemplate) {
|
|
const inline = property(metadata, 'template');
|
|
const template = inline ? literalText(inline) : undefined;
|
|
const url = property(metadata, 'templateUrl');
|
|
if (template !== undefined || !url || !ts.isStringLiteralLike(url)) {
|
|
return template;
|
|
}
|
|
return readTemplate(path.posix.join(path.posix.dirname(file), url.text));
|
|
}
|
|
|
|
/**
|
|
* The template text of every `@Component` in a TypeScript source, in
|
|
* declaration order. A source without a component decorator is not parsed.
|
|
*/
|
|
export async function componentTemplates(file, source, readTemplate) {
|
|
if (!source.includes('@Component')) return [];
|
|
const sourceFile = ts.createSourceFile(
|
|
file,
|
|
source,
|
|
ts.ScriptTarget.Latest,
|
|
true
|
|
);
|
|
const components = [];
|
|
const visit = (node) => {
|
|
const metadata = componentMetadata(node);
|
|
if (metadata) components.push(metadata);
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sourceFile);
|
|
const templates = await Promise.all(
|
|
components.map((metadata) =>
|
|
componentTemplate(metadata, file, readTemplate)
|
|
)
|
|
);
|
|
return templates.filter((template) => template !== undefined);
|
|
}
|