2.0.0 took Tailwind out of the stack, but the texts Boost copies into every application still taught its utilities: the core guideline said "built on Tailwind CSS", the always-on material-3 guideline wrote every rule as `bg-primary`, `type-*`, `rounded-corner-*`, `state-layer` and `medium:`, and both skills' tables and examples did the same. None of those classes exists in 2.0's stylesheets, so an agent following the guideline wrote markup that compiled to nothing. Found moving ReStride onto 2.0: its CLAUDE.md, and SealShare's, carry these texts. The guidelines and skills now teach what 2.0 has: component and layout component props (`gap="space200"`, `hide-from="medium"`), the `md-type-*`, `md-ink-*` and interaction classes, and `--md-sys-*` tokens in the application's own CSS, with breakpoints as range media queries. UPGRADE.md §1, §2 and §4 describe the finished move instead of the in-between state, and header comments that pointed at the removed tokens/utilities.css and tailwind.css, or called a component "still Tailwind", say what is true now. BoostVocabularyTest runs DesignGuard over the code in every shipped guideline and skill (fenced Blade and CSS, and each inline class list), so a text that teaches a class the stylesheets do not define fails the suite; it finds 268 violations in the texts as 2.0.0 shipped them. The guard's own table of what it reports is exempt. BoostResourcesTest now asks the design skill for each breakpoint's prop value and media query instead of the removed `medium:` variants. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 lines
4.8 KiB
PHP
138 lines
4.8 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use NoNameWeb\LivewireMaterial\Support\Stylesheets;
|
|
use NoNameWeb\LivewireMaterial\Testing\DesignGuard;
|
|
|
|
/**
|
|
* The texts Boost copies into an application: the two guidelines every session reads, and the two
|
|
* skills. Each teaches the vocabulary an agent then writes, so none may name a class or variant
|
|
* the package's stylesheets do not define.
|
|
*/
|
|
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',
|
|
];
|
|
|
|
/**
|
|
* The one section that names Tailwind's utilities on purpose: the design guard's table of what it
|
|
* reports, each beside its replacement.
|
|
*/
|
|
const BOOST_SECTIONS_NAMING_WHAT_FAILS = ['Testing the design'];
|
|
|
|
beforeEach(fn () => Stylesheets::resetCache());
|
|
|
|
/**
|
|
* 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 into a stylesheet, and every inline span that reads as a class
|
|
* list (`bg-primary`, `medium:hidden`, `md-ink-variant`) 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;
|
|
$skipping = false;
|
|
|
|
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 (! $skipping && $fence === 'css') {
|
|
$cssLine = $line;
|
|
} elseif (! $skipping && in_array($fence, ['blade', 'html', 'php', 'js'], true)) {
|
|
$viewLine = $line;
|
|
}
|
|
} else {
|
|
if (preg_match('/^## (.+)$/', $line, $heading) === 1) {
|
|
$skipping = in_array($heading[1], BOOST_SECTIONS_NAMING_WHAT_FAILS, true);
|
|
}
|
|
|
|
if (! $skipping) {
|
|
preg_match_all('/`([^`]+)`/', $line, $spans);
|
|
|
|
$viewLine = collect($spans[1])
|
|
->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. Not: a CSS declaration (`font: var(…)`), a
|
|
* command (`php artisan …`), or a single bare word, which is as often a role, prop or value
|
|
* (`outline`, `link`, `hidden`) as a utility.
|
|
*/
|
|
function boostSpanIsClassList(string $span): bool
|
|
{
|
|
$tokens = preg_split('/\s+/', trim($span));
|
|
|
|
if (count($tokens) === 1 && preg_match('/[-:]/', $tokens[0]) !== 1) {
|
|
return false;
|
|
}
|
|
|
|
if (preg_match('/^(?:php|npm|npx|composer|git)\b|\b(?:var|calc|color-mix)\(|^[a-z-]+:\s/', $span) === 1) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($tokens as $token) {
|
|
if (preg_match('/^(?:--|data-|aria-|wire:|x-|@|\$)/', $token) === 1 || str_contains($token, '::')) {
|
|
return false;
|
|
}
|
|
|
|
if (preg_match('/^[a-z0-9][a-z0-9:\/.\[\]()#%_-]*$/', $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([]);
|
|
});
|