tests / feature (8.4) (push) Successful in 2m0s
tests / feature (8.5) (push) Successful in 2m0s
tests / browser (chrome, chromium) (push) Failing after 8m3s
tests / browser (firefox, firefox) (push) Failing after 12m58s
tests / browser (safari, webkit) (push) Failing after 13m8s
Tailwind left the stack in 2.0.0, but the package still carried about 330 mentions of it. What the guard's Tailwind detection protected — a class that compiles to nothing — is now protected by a check that does not care where a dead class came from. DesignGuard: about 500 lines of Tailwind tables, scales, palettes and "2.0.0 replacement" hints give way to one check — a class a view or PHP file writes that neither the application's stylesheets nor the package's own declare. It catches a utility of any framework, a typo and a class whose rules were deleted alike, so it also found two classes ReStride draws nothing with. A stylesheet has to be in reach for it: the `.css` files among the scanned paths, or what the `missingStylesheets()` entry imports. The class reader no longer mistakes an array index for a class list (`$block['base']`), and it reads the array a class helper is given, where it read nothing before. The package's own three Tailwind self-guards go with it. Only their one unique check stays, as a test of its own: every `matchMedia` width in resources/js is an M3 breakpoint. The pagination views are `material.blade.php` and `simple-material.blade.php`; only Laravel's and Livewire's default theme names ever made them `tailwind`. The provider sets `Paginator`'s default views and switches `livewire.pagination_theme` to `material` when it is still Livewire's own default, so no application can forget the config; a theme an application chose, and a component's own `$paginationTheme` or `paginationView()`, still win. The rest is prose: the layer-order guidance for an application that still builds Tailwind, the Tailwind wording in the README, the Boost guidelines and the development skill, and about 25 "this used to be a Tailwind utility" comments, along with every "plan step NN" pointer into a gitignored folder. The reset keeps its credit, and NOTICE now carries it too. Feature suite 1159 passed, Chrome browser suite 299 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
124 lines
4.9 KiB
PHP
124 lines
4.9 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
|
|
|
/**
|
|
* The texts Boost copies into an application: the two guidelines every session reads, and the two
|
|
* skills. A fenced Blade, HTML, PHP or JS block goes through the design guard whole, so none may
|
|
* write a class no stylesheet declares; in prose, only an inline span that reads as an `md-*`
|
|
* class list is checked (`boostSpanIsClassList()`), so a name from another vocabulary — a prop, a
|
|
* role, a config key — is never held to that standard.
|
|
*/
|
|
const BOOST_TEXTS = [
|
|
'resources/boost/guidelines/core.blade.php',
|
|
'resources/boost/guidelines/material-3.blade.php',
|
|
'resources/boost/skills/livewire-material-development/SKILL.md',
|
|
'resources/boost/skills/material-3-design/SKILL.md',
|
|
];
|
|
|
|
/**
|
|
* A text's code, line for line, as the design guard reads an application: fenced Blade, HTML, PHP
|
|
* and JS into a view, fenced CSS and an inline rule into a stylesheet, and every inline span that
|
|
* reads as a class list (`md-ink-variant md-truncate`) as a `class` attribute on its own line.
|
|
* Prose, props, attributes and custom properties are left out.
|
|
*
|
|
* @return array{view: string, css: string}
|
|
*/
|
|
function boostTextAsCode(string $text): array
|
|
{
|
|
$view = [];
|
|
$css = [];
|
|
$fence = null;
|
|
|
|
foreach (explode("\n", $text) as $line) {
|
|
$viewLine = '';
|
|
$cssLine = '';
|
|
|
|
if (preg_match('/^```\s*([a-z]*)/', $line, $open) === 1) {
|
|
$fence = $fence === null ? ($open[1] ?: 'text') : null;
|
|
} elseif ($fence !== null) {
|
|
if ($fence === 'css') {
|
|
$cssLine = $line;
|
|
} elseif (in_array($fence, ['blade', 'html', 'php', 'js'], true)) {
|
|
$viewLine = $line;
|
|
}
|
|
} else {
|
|
preg_match_all('/`([^`]+)`/', $line, $spans);
|
|
|
|
// A span that is a rule of its own (`.sport-run-label { … }`) declares the class
|
|
// the example beside it writes, so it goes on the stylesheet side of the pair.
|
|
[$rules, $spans] = collect($spans[1])->partition(fn (string $span): bool => preg_match('/^\s*\.[\w-]+[^{]*\{[^}]*\}\s*$/', $span) === 1);
|
|
|
|
$cssLine = $rules->implode(' ');
|
|
|
|
$viewLine = $spans
|
|
->map(fn (string $span): ?string => match (true) {
|
|
str_contains($span, '<') => $span,
|
|
boostSpanIsClassList($span) => '<div class="'.$span.'"></div>',
|
|
default => null,
|
|
})
|
|
->filter()
|
|
->implode(' ');
|
|
}
|
|
|
|
$view[] = $viewLine;
|
|
$css[] = $cssLine;
|
|
}
|
|
|
|
return ['view' => implode("\n", $view), 'css' => implode("\n", $css)];
|
|
}
|
|
|
|
/**
|
|
* Whether an inline span reads as a class list: every token in it, and there has to be at least
|
|
* one, has to be an `md-*` class — the only vocabulary this check holds prose to. A single bare
|
|
* word (`outline`, `link`, `hidden`), a property or prop name (`border-color`,
|
|
* `placeholder-value`), a colour role (`outline-variant`), a CSS function (`var(…)`) and a shell
|
|
* command (`php artisan …`) all fail that test on their own, with no list of names to keep here.
|
|
*/
|
|
function boostSpanIsClassList(string $span): bool
|
|
{
|
|
$tokens = preg_split('/\s+/', trim($span));
|
|
|
|
foreach ($tokens as $token) {
|
|
// Every class the package declares is `md-*`, so that prefix is what tells a class list
|
|
// from the prop values, slot names and config keys the prose lists the same way
|
|
// (`top-start`, `rail-header`, `livewire-material.profiles`). A class of an application's
|
|
// own that an example names is nobody's to declare here.
|
|
if (preg_match('/^(?:[a-z][\w-]*:)*md-[a-z\d-]+$/', $token) !== 1) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
it('teaches only classes and variants the stylesheets define', function () {
|
|
$directory = sys_get_temp_dir().'/livewire-material-boost-'.bin2hex(random_bytes(4));
|
|
$files = [];
|
|
|
|
foreach (BOOST_TEXTS as $index => $path) {
|
|
$code = boostTextAsCode(File::get(__DIR__.'/../../'.$path));
|
|
|
|
File::ensureDirectoryExists($directory);
|
|
File::put($files["{$directory}/text-{$index}.blade.php"] = "{$directory}/text-{$index}.blade.php", $code['view']);
|
|
File::put($files["{$directory}/text-{$index}.css"] = "{$directory}/text-{$index}.css", $code['css']);
|
|
}
|
|
|
|
try {
|
|
$violations = DesignGuard::scan(realpath($directory))->violations();
|
|
} finally {
|
|
File::deleteDirectory($directory);
|
|
}
|
|
|
|
$named = array_map(function (string $violation) use ($directory): string {
|
|
return (string) preg_replace_callback(
|
|
'#^'.preg_quote(realpath(dirname($directory)) ?: dirname($directory), '#').'/[^/]+/text-(\d+)\.(?:blade\.php|css)#',
|
|
fn (array $match): string => BOOST_TEXTS[(int) $match[1]],
|
|
$violation,
|
|
);
|
|
}, $violations);
|
|
|
|
expect($named)->toBe([]);
|
|
});
|