Report the stylesheets an entry imports that no view needs any more
missingStylesheets() names an import the views need, but nothing named one they had stopped needing: when SealShare's last card left its views, card.css stayed in every page until a reviewer noticed by hand. unusedStylesheets($cssEntry) reads the entry's direct package imports and reports each one no scanned view needs, counting what a needed stylesheet imports itself and foundation.css always; an entry that imports all.css is left alone. Opt-in, beside missingStylesheets() (plan step 46). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ff5349759b
commit
05f115ab36
+2
-1
@@ -170,7 +170,8 @@ the contrast toggle (`<x-theme-toggle mode="contrast">`) and the Standard motion
|
||||
- **Retargeted:** `forbidColours([...])` keeps its signature and reports a left-out role where 2.0.0
|
||||
writes one: `var(--md-sys-color-…)` in CSS or an inline `style`, an `md-ink-*` class, a
|
||||
component's `color`/`tone` prop.
|
||||
- **New:** `missingStylesheets($cssEntry)` names each `@import` the views need; a `.css` file passed
|
||||
- **New:** `missingStylesheets($cssEntry)` names each `@import` the views need, and
|
||||
`unusedStylesheets($cssEntry)` each one they no longer do; a `.css` file passed
|
||||
to `scan()` is checked for literal values and off-scale media queries.
|
||||
|
||||
Browser tests that assert widths switch at 640/1024/1280 now
|
||||
|
||||
@@ -1118,6 +1118,7 @@ use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
||||
it('uses only what compiles', function () {
|
||||
expect(DesignGuard::scan([resource_path('views'), resource_path('js'), resource_path('css'), app_path()])
|
||||
->missingStylesheets(resource_path('css/app.css'))
|
||||
->unusedStylesheets(resource_path('css/app.css'))
|
||||
->forbidColours(['tertiary']) // roles this application's rules leave out
|
||||
->violations())->toBe([]);
|
||||
});
|
||||
@@ -1151,7 +1152,7 @@ It reads every path it is given and fails with `path:line` on unknown symbol nam
|
||||
|
||||
In the `.css` files it is given (`material-scheme.css` skipped) it fails on a literal colour, radius, shadow, font, font size, weight, line height, letter spacing, easing or duration, and on a media query width off 600/840/1200/1600px; a `var()`, or a `calc()`/`min()`/`max()`/`clamp()` built on one, is fine.
|
||||
|
||||
`missingStylesheets($cssEntry)` follows the entry's relative `@import`s through every package file's own imports (`split-button.css` counts `button.css` and `menu.css` too; `tailwindcss` or a font URL is skipped) and checks them against the package tags a scanned view renders (plain, prefixed or `<x-livewire-material::…>`) and `->links()`; each missing stylesheet names its `@import` line once, and a tag the application shadows with its own component is reported instead — the application's component wins in Blade. It reads imports only: leave `resource_path('css')` out of `scan()` until the stylesheets are on tokens. `forbidColours([...])` fails wherever a left-out role (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds a pattern of your own.
|
||||
`missingStylesheets($cssEntry)` follows the entry's relative `@import`s through every package file's own imports (`split-button.css` counts `button.css` and `menu.css` too; `tailwindcss` or a font URL is skipped) and checks them against the package tags a scanned view renders (plain, prefixed or `<x-livewire-material::…>`) and `->links()`; each missing stylesheet names its `@import` line once, and a tag the application shadows with its own component is reported instead — the application's component wins in Blade. It reads imports only: leave `resource_path('css')` out of `scan()` until the stylesheets are on tokens. `unusedStylesheets($cssEntry)` is the other way round: a package stylesheet the entry imports directly that no scanned view needs, even through a needed stylesheet's own imports, named at its `@import` line to remove (an entry importing `all.css` is left alone). `forbidColours([...])` fails wherever a left-out role (with its `on-` and container roles) is still written: `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. `forbid($pattern, $reason)` adds a pattern of your own.
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
+114
-2
@@ -31,7 +31,9 @@ use Symfony\Component\Finder\Finder;
|
||||
* `->links()` needing `pagination.css`; each names the missing `@import` line once, at its
|
||||
* first use. A tag the application shadows with its own component of the same name is
|
||||
* reported instead — the application's component wins in Blade, so the package's
|
||||
* stylesheet is moot.
|
||||
* stylesheet is moot. `unusedStylesheets($cssEntry)` reports the other way: a package
|
||||
* stylesheet the entry imports directly that no scanned view needs, not even through
|
||||
* another needed stylesheet's imports (an entry importing `all.css` is left alone).
|
||||
* (iii) every `.css` file among the scanned paths, outside the package and excluding the
|
||||
* generated `material-scheme.css`: a literal colour, radius, shadow, font size, weight,
|
||||
* line height, letter spacing, easing or duration, and a media query at a width other
|
||||
@@ -262,6 +264,8 @@ class DesignGuard
|
||||
|
||||
protected ?string $cssEntry = null;
|
||||
|
||||
protected ?string $unusedEntry = null;
|
||||
|
||||
/** @var array<string, true>|null */
|
||||
protected ?array $applicationClassesCache = null;
|
||||
|
||||
@@ -325,6 +329,20 @@ class DesignGuard
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check (ii) the other way round: each package stylesheet `$cssEntry` imports directly that no
|
||||
* scanned Blade view needs — no package tag it renders, no `->links()`, and not reached through
|
||||
* the imports of a stylesheet that is needed — named at its `@import` line, to remove. A
|
||||
* component whose last use left the views otherwise keeps its CSS in every page. `foundation.css`
|
||||
* is always needed; an entry that imports `all.css` has chosen everything and is not read.
|
||||
*/
|
||||
public function unusedStylesheets(string $cssEntry): static
|
||||
{
|
||||
$this->unusedEntry = $cssEntry;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
@@ -336,6 +354,7 @@ class DesignGuard
|
||||
: [];
|
||||
|
||||
$reported = [];
|
||||
$needed = [];
|
||||
|
||||
if ($this->cssEntry !== null && ! is_file($this->cssEntry)) {
|
||||
$violations[] = "{$this->cssEntry}:1 the CSS entry `missingStylesheets()` names does not exist";
|
||||
@@ -390,6 +409,10 @@ class DesignGuard
|
||||
$violations[] = "{$where}:{$line} {$what}";
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->unusedEntry !== null) {
|
||||
$needed = [...$needed, ...$this->neededStylesheets($contents)];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (explode("\n", $contents) as $index => $text) {
|
||||
@@ -413,9 +436,92 @@ class DesignGuard
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->unusedEntry !== null) {
|
||||
$violations = [...$violations, ...$this->unusedStylesheetViolations($needed)];
|
||||
}
|
||||
|
||||
return $this->sortViolations([...$violations, ...$this->applicationCssViolations()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The package stylesheets a Blade view's own tags and `->links()` calls need, before their
|
||||
* imports are followed.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function neededStylesheets(string $contents): array
|
||||
{
|
||||
$needed = [];
|
||||
|
||||
foreach ($this->packageTagUsages($contents) as [, $name, $spelling]) {
|
||||
if ($spelling === 'plain' && $this->shadowedByApplication($name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($stylesheet = static::packageStylesheetFor($name)) !== null) {
|
||||
$needed[] = $stylesheet;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->paginationUsages($contents) !== []) {
|
||||
$needed[] = static::packageCssRoot().'/components/pagination.css';
|
||||
}
|
||||
|
||||
return $needed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $needed
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function unusedStylesheetViolations(array $needed): array
|
||||
{
|
||||
$entry = (string) $this->unusedEntry;
|
||||
$real = realpath($entry);
|
||||
|
||||
if ($real === false || ! is_file($real)) {
|
||||
return ["{$entry}:1 the CSS entry `unusedStylesheets()` names does not exist"];
|
||||
}
|
||||
|
||||
$root = static::packageCssRoot();
|
||||
// Comments out, line count kept; the import strings themselves stay readable.
|
||||
$css = (string) preg_replace_callback('~/\*.*?\*/~s', fn (array $comment): string => str_repeat("\n", substr_count($comment[0], "\n")), (string) file_get_contents($real));
|
||||
$imports = [];
|
||||
|
||||
preg_match_all('/@import\s+(?:url\(\s*)?([\'"])([^\'"]+)\1/i', $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$file = realpath(dirname($real).'/'.$match[2][0]);
|
||||
|
||||
if ($file === false || ! static::isPackageFile($file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($file === $root.'/all.css') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$imports[] = [substr_count(substr($css, 0, $match[0][1]), "\n") + 1, $file];
|
||||
}
|
||||
|
||||
$reached = array_fill_keys(Stylesheets::resolvedFiles([...array_unique($needed), $root.'/foundation.css']), true);
|
||||
$violations = [];
|
||||
|
||||
foreach ($imports as [$line, $file]) {
|
||||
if (! isset($reached[$file])) {
|
||||
$violations[] = sprintf(
|
||||
'%s:%d `%s` is imported, but no scanned view renders a component that needs it — remove `@import \'%s\';`',
|
||||
$this->relative($real),
|
||||
$line,
|
||||
static::packageRelativeName($file),
|
||||
$this->importLineFrom($real, $file),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* `$violations` ("path:line message", `relative()`'s shape) sorted by path, then line
|
||||
* (numerically, so line 9 sits before line 10), then message — whichever check produced each
|
||||
@@ -1559,7 +1665,13 @@ class DesignGuard
|
||||
*/
|
||||
protected function importLine(string $file): string
|
||||
{
|
||||
$entryDir = dirname((string) (realpath($this->cssEntry) ?: $this->cssEntry));
|
||||
return $this->importLineFrom((string) $this->cssEntry, $file);
|
||||
}
|
||||
|
||||
/** `importLine()` for an entry other than `missingStylesheets()`'s. */
|
||||
protected function importLineFrom(string $entry, string $file): string
|
||||
{
|
||||
$entryDir = dirname((string) (realpath($entry) ?: $entry));
|
||||
$vendor = base_path('vendor/nonameweb/livewire-material/resources/css');
|
||||
|
||||
// A Composer path repository may symlink the package: the import still goes through vendor/.
|
||||
|
||||
@@ -420,6 +420,20 @@ it('counts what an imported package stylesheet already brings in, through its ow
|
||||
expect($violations)->toBe([]);
|
||||
});
|
||||
|
||||
it('names each stylesheet the entry imports that no scanned view needs, and leaves all.css alone', function () {
|
||||
// dependencies.blade.php renders <x-button>, <x-menu> and <x-split-button>: menu.css is needed
|
||||
// (and reached through split-button.css too); card.css and stack.css serve nothing on the page.
|
||||
$entry = realpath(GUARD_FIXTURES.'/stylesheets/unused.css');
|
||||
|
||||
expect(fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/dependencies.blade.php'))->unusedStylesheets($entry)->violations()))->toBe([
|
||||
"stylesheets/unused.css:4 `components/card.css` is imported, but no scanned view renders a component that needs it — remove `@import '../../../../resources/css/components/card.css';`",
|
||||
"stylesheets/unused.css:5 `layout/stack.css` is imported, but no scanned view renders a component that needs it — remove `@import '../../../../resources/css/layout/stack.css';`",
|
||||
])
|
||||
// missingStylesheets() alone never reports an unused import.
|
||||
->and(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/dependencies.blade.php'))->missingStylesheets($entry)->violations())->toBe([])
|
||||
->and(DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views/dependencies.blade.php'))->unusedStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/all.css'))->violations())->toBe([]);
|
||||
});
|
||||
|
||||
it('needs nothing more once the entry imports all.css', function () {
|
||||
$violations = DesignGuard::scan(realpath(GUARD_FIXTURES.'/stylesheets/views'))
|
||||
->missingStylesheets(realpath(GUARD_FIXTURES.'/stylesheets/all.css'))
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
@import '../../../../resources/css/foundation.css';
|
||||
@import '../../../../resources/css/components/split-button.css';
|
||||
@import '../../../../resources/css/components/menu.css';
|
||||
@import '../../../../resources/css/components/card.css';
|
||||
@import '../../../../resources/css/layout/stack.css';
|
||||
Reference in New Issue
Block a user