Retarget forbidColours() at where 2.0.0 writes a role
Plan step 41 review (the user, 2026-09-15): an application without Tailwind names a role through its --md-sys-color-* custom property (CSS, an inline style, a script), an md-ink-* class, or a component's color/tone prop, not a bg-tertiary utility. forbidColours() keeps its name and signature and reports each of those, the role's on- and container roles included; a Tailwind leftover stays family (i)'s single report. ReStride's forbidColours(['tertiary', 'primary-container']) keeps its meaning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
6dc996afdf
commit
d5972b8ddb
@@ -150,7 +150,7 @@ it('uses only what compiles', function () {
|
||||
});
|
||||
```
|
||||
|
||||
The guard fails on Tailwind palette colours, unknown Material Symbol names and Blade directives written inside component tags. It also fails on every Tailwind utility or variant still sitting in a view, PHP or JS file — a breakpoint prefix, a radius, shadow, type size/weight/leading/tracking, easing or duration, a flex/grid/spacing/sizing/display utility, a text-layout utility, an M3 role utility (`text-on-surface-variant`, `bg-primary`), a pseudo-class variant, or an arbitrary `[…]` value — none of which compile any more, since the application carries no Tailwind: every line names its 2.0.0 replacement, a layout component and prop (`flex gap-4` → `<x-row gap="space200">`), an `md-*` class (`truncate` → `md-truncate`), or a token for the application's own CSS (`rounded-lg` → `var(--md-sys-shape-corner-lg)`). A class the application's own stylesheets declare is exempt, and so is every `md-*` class.
|
||||
The guard fails on Tailwind palette colours, unknown Material Symbol names and Blade directives written inside component tags. `forbidColours([...])` names roles the application leaves out, and fails wherever one is still written: its `var(--md-sys-color-…)` in CSS or an inline `style`, its `md-ink-*` class, or a component's `color`/`tone` prop. It also fails on every Tailwind utility or variant still sitting in a view, PHP or JS file — a breakpoint prefix, a radius, shadow, type size/weight/leading/tracking, easing or duration, a flex/grid/spacing/sizing/display utility, a text-layout utility, an M3 role utility (`text-on-surface-variant`, `bg-primary`), a pseudo-class variant, or an arbitrary `[…]` value — none of which compile any more, since the application carries no Tailwind: every line names its 2.0.0 replacement, a layout component and prop (`flex gap-4` → `<x-row gap="space200">`), an `md-*` class (`truncate` → `md-truncate`), or a token for the application's own CSS (`rounded-lg` → `var(--md-sys-shape-corner-lg)`). A class the application's own stylesheets declare is exempt, and so is every `md-*` class.
|
||||
|
||||
`missingStylesheets($cssEntry)` checks the CSS entry's `@import` graph (followed through every package file's own imports) against the package tags a view actually renders — unprefixed, under the configured prefix, or `<x-livewire-material::…>` — and `->links()`; each missing one names the exact `@import` line to add, and a tag the application shadows with its own component of the same name is reported instead. It also turns on a check of the application's own CSS (the entry and what it imports outside the package, plus any `.css` file among the scanned paths), which reports a literal colour, radius, shadow, font size, weight, line height, letter spacing, easing, duration or off-scale media query with its token or breakpoint — a value inside `var(--md-sys-…)` or `calc()` is always fine, and the generated `material-scheme.css` is skipped.
|
||||
|
||||
|
||||
@@ -1117,7 +1117,7 @@ it('uses only what compiles', function () {
|
||||
});
|
||||
```
|
||||
|
||||
It fails on Tailwind palette colours, unknown symbol names and Blade directives written inside a component tag (where they do not compile), with `path:line` for each.
|
||||
It fails on Tailwind palette colours, unknown symbol names and Blade directives written inside a component tag (where they do not compile), with `path:line` for each. `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.
|
||||
|
||||
It also fails on every Tailwind utility or variant, which compile to nothing in a Tailwind-free application, each with its 2.0.0 replacement — a layout component and prop, an `md-*` class, or a token for the application's own CSS. A class the application's own stylesheets declare is exempt, and so is every `md-*` class:
|
||||
|
||||
|
||||
@@ -239,7 +239,11 @@ class DesignGuard
|
||||
|
||||
/**
|
||||
* Roles the application's own rules leave out, e.g. ['tertiary', 'primary-container'].
|
||||
* Their on-roles and containers are forbidden with them.
|
||||
* Their on-roles and containers are forbidden with them, wherever 2.0.0 lets an application
|
||||
* write one: the `--md-sys-color-*` custom property (a `var()` in its CSS, an inline `style`, a
|
||||
* script reading it), the `md-ink-*` class text.css has for it, and a component's `color` or
|
||||
* `tone` prop (`color="tertiary"`, `:tone="'tertiary'"`). A Tailwind `bg-tertiary` left behind
|
||||
* is family (i)'s to report, as every colour utility is.
|
||||
*
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
@@ -325,6 +329,10 @@ class DesignGuard
|
||||
$violations[] = "{$where}:{$line} Blade directive `{$directive}` inside a component tag, where it does not compile";
|
||||
}
|
||||
|
||||
foreach ($this->forbiddenRoleProps($contents) as [$line, $what]) {
|
||||
$violations[] = "{$where}:{$line} {$what}";
|
||||
}
|
||||
|
||||
if ($this->cssEntry !== null) {
|
||||
foreach ($this->missingStylesheetViolations($contents, $resolved) as [$line, $what]) {
|
||||
$violations[] = "{$where}:{$line} {$what}";
|
||||
@@ -341,7 +349,7 @@ class DesignGuard
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->offTheTokens($text) as $what) {
|
||||
foreach ([...$this->offTheTokens($text), ...$this->forbiddenRoles($text)] as $what) {
|
||||
$violations[] = "{$where}:{$line} {$what}";
|
||||
}
|
||||
|
||||
@@ -376,14 +384,77 @@ class DesignGuard
|
||||
|
||||
protected function colourPattern(): string
|
||||
{
|
||||
$names = [self::PALETTE];
|
||||
return '/(?<![\w-])'.self::UTILITY.'-'.self::PALETTE.'(?![\w-])/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Every place one line names a role `forbidColours()` left out: its `--md-sys-color-*` custom
|
||||
* property, or the `md-ink-*` class text.css draws it with.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function forbiddenRoles(string $text): array
|
||||
{
|
||||
$found = [];
|
||||
|
||||
foreach ($this->forbiddenColours as $role) {
|
||||
$role = preg_quote($role, '/');
|
||||
$names[] = "(?:on-)?{$role}(?:-container)?";
|
||||
$names = ['--md-sys-color-(?:on-)?'.preg_quote($role, '/').'(?:-container)?'];
|
||||
|
||||
foreach ([$role, "on-{$role}", "{$role}-container", "on-{$role}-container"] as $variant) {
|
||||
if (isset(self::INK_ROLE[$variant])) {
|
||||
$names[] = preg_quote(self::INK_ROLE[$variant], '/');
|
||||
}
|
||||
}
|
||||
|
||||
preg_match_all('/(?<![\w-])(?:'.implode('|', $names).')(?![\w-])/', $text, $matches);
|
||||
|
||||
foreach ($matches[0] as $written) {
|
||||
$found[] = "`{$written}`: role `{$role}` is not part of this application's palette";
|
||||
}
|
||||
}
|
||||
|
||||
return '/(?<![\w-])'.self::UTILITY.'-(?:'.implode('|', $names).')(?![\w-])/';
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every component `color` or `tone` prop in `$contents` naming a role `forbidColours()` left
|
||||
* out, literal (`color="tertiary"`) or any string a bound one can take
|
||||
* (`:tone="$failed ? 'error' : 'info'"`).
|
||||
*
|
||||
* @return list<array{0: int, 1: string}>
|
||||
*/
|
||||
protected function forbiddenRoleProps(string $contents): array
|
||||
{
|
||||
if ($this->forbiddenColours === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
preg_match_all('/<x-[\w.:-]+((?:[^>"]|"[^"]*")*)>/s', $contents, $tags, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
||||
|
||||
$found = [];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
preg_match_all('/\s(?<bound>:?)(?:color|tone)="(?<value>[^"]*)"/', $tag[1][0], $props, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
||||
|
||||
foreach ($props as $prop) {
|
||||
$values = $prop['bound'][0] === ':'
|
||||
? (preg_match_all("/'([^']*)'/", $prop['value'][0], $strings) ? $strings[1] : [])
|
||||
: [$prop['value'][0]];
|
||||
|
||||
foreach ($values as $value) {
|
||||
foreach ($this->forbiddenColours as $role) {
|
||||
if (preg_match('/^(?:on-)?'.preg_quote($role, '/').'(?:-container)?$/', $value) === 1) {
|
||||
$found[] = [
|
||||
substr_count(substr($contents, 0, $tag[1][1] + $prop[0][1]), "\n") + 1,
|
||||
'`'.trim($prop[0][0])."`: role `{$role}` is not part of this application's palette",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -592,9 +663,16 @@ class DesignGuard
|
||||
$violations = [];
|
||||
|
||||
foreach ($this->applicationStylesheets() as $file) {
|
||||
$css = $this->withoutTokenFunctions($this->maskedCss((string) file_get_contents($file)));
|
||||
$masked = $this->maskedCss((string) file_get_contents($file));
|
||||
$css = $this->withoutTokenFunctions($masked);
|
||||
$where = $this->relative($file);
|
||||
|
||||
foreach (explode("\n", $masked) as $index => $text) {
|
||||
foreach ($this->forbiddenRoles($text) as $what) {
|
||||
$violations[] = "{$where}:".($index + 1)." {$what}";
|
||||
}
|
||||
}
|
||||
|
||||
foreach ([
|
||||
...$this->literalColours($css),
|
||||
...$this->literalDeclarations($css),
|
||||
|
||||
@@ -278,12 +278,30 @@ it('reports an arbitrary [] value once, whatever utility it modifies, and strips
|
||||
]);
|
||||
});
|
||||
|
||||
it('bans the roles an application leaves out', function () {
|
||||
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/views'))
|
||||
->forbidColours(['tertiary'])
|
||||
it('bans the roles an application leaves out, wherever 2.0.0 writes one', function () {
|
||||
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/forbidden-colours'))
|
||||
->forbidColours(['tertiary', 'primary-container', 'error'])
|
||||
->violations());
|
||||
|
||||
expect($violations)->toContain('views/page.blade.php:6 colour the theme does not declare `text-tertiary`');
|
||||
expect($violations)->toBe([
|
||||
'forbidden-colours/page.blade.php:5 Tailwind colour utility `bg-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
||||
"forbidden-colours/page.blade.php:1 `color=\"tertiary\"`: role `tertiary` is not part of this application's palette",
|
||||
"forbidden-colours/page.blade.php:2 `:tone=\"'tertiary'\"`: role `tertiary` is not part of this application's palette",
|
||||
"forbidden-colours/page.blade.php:3 `:color=\"\$failed ? 'error' : 'info'\"`: role `error` is not part of this application's palette",
|
||||
"forbidden-colours/page.blade.php:4 `--md-sys-color-on-tertiary-container`: role `tertiary` is not part of this application's palette",
|
||||
"forbidden-colours/page.blade.php:4 `md-ink-error`: role `error` is not part of this application's palette",
|
||||
"forbidden-colours/page.blade.php:7 `--md-sys-color-primary-container`: role `primary-container` is not part of this application's palette",
|
||||
"forbidden-colours/app.css:3 `--md-sys-color-tertiary-container`: role `tertiary` is not part of this application's palette",
|
||||
"forbidden-colours/app.css:4 `--md-sys-color-on-primary-container`: role `primary-container` is not part of this application's palette",
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves every role alone until an application forbids one', function () {
|
||||
$violations = fixtureRelative(DesignGuard::scan(realpath(GUARD_FIXTURES.'/forbidden-colours'))->violations());
|
||||
|
||||
expect($violations)->toBe([
|
||||
'forbidden-colours/page.blade.php:5 Tailwind colour utility `bg-tertiary` compiles to nothing — use `var(--md-sys-color-tertiary)` in your own CSS',
|
||||
]);
|
||||
});
|
||||
|
||||
it('bans any further pattern', function () {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* var(--md-sys-color-tertiary) in a comment is ignored */
|
||||
.hero {
|
||||
background: var(--md-sys-color-tertiary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.fine {
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-button label="Save" color="tertiary" />
|
||||
<x-badge :tone="'tertiary'" />
|
||||
<x-alert :color="$failed ? 'error' : 'info'" />
|
||||
<p class="md-ink-error" style="border-color: var(--md-sys-color-on-tertiary-container)">Failed</p>
|
||||
<div class="bg-tertiary"></div>
|
||||
<x-button color="primary" label="Allowed" />
|
||||
<span style="background: var(--md-sys-color-primary-container)"></span>
|
||||
<span style="background: var(--md-sys-color-tertiary-fixed)">A fixed role is another role</span>
|
||||
Reference in New Issue
Block a user