mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
* perf(web): provide form-field defaults per component, not at the root app.config.ts provided MAT_FORM_FIELD_DEFAULT_OPTIONS in the root injector. Importing the token kept @angular/material/form-field in main.js, and Material 22.2's form field imports @angular/forms/signals, which would add @angular/forms to startup as well (the reason the Angular 22.2 dependency update exceeds the initial-bytes ratchet). Every form field renders in a lazy chunk, so the outline + dynamic defaults now come from provideFormFieldDefaults() (@iptvnator/ui/components/form-field-defaults) on each component that renders <mat-form-field>. tools/nx/check-form-field-defaults.mjs (ui:form-field-defaults:validate, run in CI) reports a component that renders a form field without the provider, and any use of the token outside the shared provider. The Xtream import form test asserts outlined, dynamically sized fields with no root provider; without the component provider they render as fill. renderer.initialBytes: 1,596,201 -> 1,534,604 bytes (-61,597) on a local production build; the baseline is lowered to the measured value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(web): cite #1928 as the initial-bytes baseline evidence Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ui): check form-field providers per component on the TypeScript AST The guard matched templates and providers with regular expressions over the whole file: a quoted inline template went unread, one component's provider covered another in the same file, an earlier array-valued provider hid the call, and provideFormFieldDefaults() in app.config or a route passed although it loads Material's form field on startup again. It now reads each @Component decorator's own template and providers array, and reports any provideFormFieldDefaults() call outside one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
201 lines
6.1 KiB
JavaScript
201 lines
6.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import {
|
|
DEFAULTS_FILE,
|
|
checkComponentSource,
|
|
listScannedFiles,
|
|
} from './check-form-field-defaults.mjs';
|
|
|
|
const repoRoot = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'../..'
|
|
);
|
|
const templates = {
|
|
'libs/a/field.component.html':
|
|
'<mat-form-field><input matInput /></mat-form-field>',
|
|
'libs/a/plain.component.html': '<button mat-button>Save</button>',
|
|
};
|
|
const readTemplate = async (file) => templates[file];
|
|
const check = async (file, source) =>
|
|
(await checkComponentSource(file, source, readTemplate)).map(
|
|
({ line, reason }) => ({ line, reason })
|
|
);
|
|
|
|
test('accepts a templateUrl form field with the provider', async () => {
|
|
const source = [
|
|
'@Component({',
|
|
" selector: 'app-field',",
|
|
' providers: [provideFormFieldDefaults()],',
|
|
" templateUrl: './field.component.html',",
|
|
'})',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(await check('libs/a/field.component.ts', source), []);
|
|
});
|
|
|
|
test('accepts the provider next to other providers', async () => {
|
|
const source = [
|
|
'@Component({',
|
|
' providers: [DatePipe, provideFormFieldDefaults()],',
|
|
" templateUrl: './field.component.html',",
|
|
'})',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(await check('libs/a/field.component.ts', source), []);
|
|
});
|
|
|
|
test('reports a templateUrl form field without the provider', async () => {
|
|
const source = [
|
|
'@Component({',
|
|
" selector: 'app-field',",
|
|
" templateUrl: './field.component.html',",
|
|
'})',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(await check('libs/a/field.component.ts', source), [
|
|
{
|
|
line: 1,
|
|
reason: 'renders <mat-form-field> without providers: [provideFormFieldDefaults()]',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('reports an inline template form field without the provider', async () => {
|
|
const source = [
|
|
'@Component({',
|
|
' template: `',
|
|
' <mat-form-field appearance="outline"></mat-form-field>',
|
|
' `,',
|
|
'})',
|
|
].join('\n');
|
|
|
|
assert.equal((await check('libs/a/inline.component.ts', source)).length, 1);
|
|
});
|
|
|
|
test('reads quoted inline templates', async () => {
|
|
const source = [
|
|
'@Component({',
|
|
" template: '<mat-form-field></mat-form-field>',",
|
|
'})',
|
|
'class Single {}',
|
|
'@Component({',
|
|
' template: "<mat-form-field></mat-form-field>",',
|
|
'})',
|
|
'class Double {}',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
(await check('libs/a/quoted.component.ts', source)).map(
|
|
({ line }) => line
|
|
),
|
|
[1, 5]
|
|
);
|
|
});
|
|
|
|
test('checks each component of a file on its own providers', async () => {
|
|
const source = [
|
|
'@Component({',
|
|
' providers: [provideFormFieldDefaults()],',
|
|
" templateUrl: './field.component.html',",
|
|
'})',
|
|
'class WithDefaults {}',
|
|
'@Component({',
|
|
" templateUrl: './field.component.html',",
|
|
'})',
|
|
'class WithoutDefaults {}',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(await check('libs/a/field.component.ts', source), [
|
|
{
|
|
line: 6,
|
|
reason: 'renders <mat-form-field> without providers: [provideFormFieldDefaults()]',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('finds the provider after an earlier array-valued provider', async () => {
|
|
const source = [
|
|
'@Component({',
|
|
' providers: [',
|
|
' { provide: LIST, useValue: [] },',
|
|
' provideFormFieldDefaults(),',
|
|
' ],',
|
|
" templateUrl: './field.component.html',",
|
|
'})',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(await check('libs/a/field.component.ts', source), []);
|
|
});
|
|
|
|
test('reports the provider outside a component', async () => {
|
|
const appConfig = [
|
|
'export const appConfig = {',
|
|
' providers: [provideRouter(routes), provideFormFieldDefaults()],',
|
|
'};',
|
|
].join('\n');
|
|
const routes = [
|
|
'export const routes = [',
|
|
" { path: 'settings', providers: [provideFormFieldDefaults()] },",
|
|
'];',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(await check('apps/web/src/app/app.config.ts', appConfig), [
|
|
{
|
|
line: 2,
|
|
reason: "calls provideFormFieldDefaults() outside a component's providers; a root or route provider loads Material's form field on startup",
|
|
},
|
|
]);
|
|
assert.equal(
|
|
(await check('apps/web/src/app/app.routes.ts', routes)).length,
|
|
1
|
|
);
|
|
});
|
|
|
|
test('ignores templates without a form field', async () => {
|
|
const source = [
|
|
'@Component({',
|
|
" templateUrl: './plain.component.html',",
|
|
'})',
|
|
'@Component({ template: `<mat-icon>add</mat-icon>` })',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(await check('libs/a/plain.component.ts', source), []);
|
|
});
|
|
|
|
test('reports the token outside the shared provider', async () => {
|
|
const source = [
|
|
"import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';",
|
|
'export const appConfig = {',
|
|
' providers: [{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {} }],',
|
|
'};',
|
|
].join('\n');
|
|
|
|
assert.deepEqual(
|
|
(await check('apps/web/src/app/app.config.ts', source)).map(
|
|
({ line }) => line
|
|
),
|
|
[1, 3]
|
|
);
|
|
assert.deepEqual(await check(DEFAULTS_FILE, source), []);
|
|
});
|
|
|
|
test('the shared provider supplies the outline, dynamic defaults', async () => {
|
|
const source = await readFile(path.join(repoRoot, DEFAULTS_FILE), 'utf8');
|
|
|
|
assert.match(source, /appearance: 'outline'/);
|
|
assert.match(source, /subscriptSizing: 'dynamic'/);
|
|
assert.match(source, /provide: MAT_FORM_FIELD_DEFAULT_OPTIONS/);
|
|
});
|
|
|
|
test('scans renderer TypeScript and skips specs', () => {
|
|
const files = listScannedFiles(repoRoot);
|
|
|
|
assert.ok(files.includes(DEFAULTS_FILE));
|
|
assert.ok(files.includes('apps/web/src/app/app.config.ts'));
|
|
assert.ok(files.every((file) => !/\.spec\.ts$/.test(file)));
|
|
});
|