Files
iptvnator/tools/nx/component-templates.mjs
T
4grayandClaude Fable 5.1 ced7386dfa perf(web): render the radio play control as an icon button, not a fab (#1938)
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>
2026-10-14 19:51:34 +02:00

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);
}