Plan step 39 (part 5). Tailwind no longer clears its own breakpoint scale anywhere in the stack, so the guard's rationale changes from "a leftover sm: compiles to nothing" to "a browser drops the invalid class or wrong-width rule just as silently" — the scans themselves already worked over plain files and needed no change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
114 lines
5.2 KiB
PHP
114 lines
5.2 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
/**
|
|
* Breakpoints are M3's window size classes and only those: compact below `medium`, then medium 600,
|
|
* expanded 840, large 1200 and extra-large 1600 (docs/reference/m3/foundations.md § Layout,
|
|
* foundations-supplement.md § Breakpoints). Tailwind is gone from the whole stack (plan step 39),
|
|
* so a leftover `sm:` class or a media query at one of its screen widths no longer compiles to
|
|
* nothing — a browser just as silently ignores the invalid class or the wrong-width rule, so the
|
|
* mistake is exactly as easy to miss. These two scans are the guard: one for a Tailwind-shaped
|
|
* prefix (a habit, or an example pasted from elsewhere), one for the media queries a stylesheet or
|
|
* a script writes by hand.
|
|
*
|
|
* @return list<SplFileInfo>
|
|
*/
|
|
function breakpointSources(): array
|
|
{
|
|
return collect(['views', 'css', 'js'])
|
|
->flatMap(fn (string $directory): array => File::allFiles(__DIR__.'/../../resources/'.$directory))
|
|
->filter(fn (SplFileInfo $file): bool => in_array($file->getExtension(), ['php', 'css', 'js'], true))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* Every line of a file that matches, as `path:line: the line`.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
function breakpointOffenders(SplFileInfo $file, string $pattern): array
|
|
{
|
|
$lines = preg_split('/\R/', $file->getContents());
|
|
|
|
return collect($lines)
|
|
->filter(fn (string $line): bool => preg_match($pattern, $line) === 1)
|
|
->map(fn (string $line, int $index): string => $file->getRelativePathname().':'.($index + 1).': '.trim($line))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
it('writes no Tailwind breakpoint prefix in any view, stylesheet or script', function () {
|
|
// A prefix, not a name: `--radius-corner-sm:` and slider.js's `sm: { corner: 8 }` are excluded
|
|
// by the character before and the one after, and `@md:` is a container query, a different
|
|
// thing — a component sized by the room it has, not by the window.
|
|
$pattern = '/(?<![\w@-])(?:max-)?(?:sm|md|lg|xl|2xl):[a-z\[(-]/';
|
|
|
|
$offenders = collect(breakpointSources())
|
|
->flatMap(fn (SplFileInfo $file): array => breakpointOffenders($file, $pattern))
|
|
->values();
|
|
|
|
expect($offenders->all())->toBe([]);
|
|
});
|
|
|
|
it('writes no media query at a Tailwind screen width', function () {
|
|
// 40rem/640px, 48rem/768px, 64rem/1024px, 80rem/1280px and the 39.99rem that stood for "below
|
|
// 640" — none of them is an M3 boundary. The M3 ones are 37.5, 52.5, 75 and 100rem, and a
|
|
// script asks resources/js/breakpoints.js for them instead of writing its own string.
|
|
$widths = '/(?<![\d.])(?:40|48|64|80|39\.99)rem|(?<![\d.])(?:640|768|1024|1280)px/';
|
|
|
|
$offenders = collect(breakpointSources())
|
|
->flatMap(function (SplFileInfo $file) use ($widths): array {
|
|
// Only the condition of a query, so `max-width: min(40rem, 70dvh)` — a real width, not
|
|
// a breakpoint — and a 640px cap on a bottom sheet are left alone.
|
|
preg_match_all('/@(?:media|container)\b[^{]*|matchMedia\(\s*[\'"`][^\'"`]*/', $file->getContents(), $matches);
|
|
|
|
return collect($matches[0])
|
|
->filter(fn (string $condition): bool => preg_match($widths, $condition) === 1)
|
|
->map(fn (string $condition): string => $file->getRelativePathname().': '.trim($condition))
|
|
->values()
|
|
->all();
|
|
})
|
|
->values();
|
|
|
|
expect($offenders->all())->toBe([]);
|
|
});
|
|
|
|
it('scans the showcase example heredocs too, where the package teaches its own markup', function () {
|
|
$sections = collect(breakpointSources())
|
|
->filter(fn (SplFileInfo $file): bool => str_contains($file->getRelativePathname(), 'showcase'.DIRECTORY_SEPARATOR.'sections'))
|
|
->filter(fn (SplFileInfo $file): bool => str_contains($file->getContents(), "<<<'BLADE'"));
|
|
|
|
// Every section still teaches its examples through a heredoc nested inside an @php block
|
|
// (plan step 38 rewrote what is inside them, not the mechanism), so the two scans above —
|
|
// which read a file's raw text rather than its parsed Blade or PHP — still reach them.
|
|
expect($sections)->not->toBeEmpty();
|
|
|
|
// The premise itself, proven on a fixture now that plan step 38 has cleared every Tailwind
|
|
// breakpoint prefix out of the showcase (its own goal, so production content can no longer
|
|
// carry one): a real file, shaped exactly like a showcase section (an outer heredoc nesting an
|
|
// inner one, `@php … <<<'BLADE' … BLADE, … @endphp`), still gives up a prefix hiding two
|
|
// heredocs deep to the same pattern and helper the first test above uses.
|
|
$directory = sys_get_temp_dir().'/breakpoint-heredoc-probe-'.uniqid();
|
|
File::ensureDirectoryExists($directory);
|
|
File::put($directory.'/probe.blade.php', <<<'OUTER'
|
|
@php
|
|
$examples = [
|
|
'Probe' => <<<'BLADE'
|
|
<div class="md:flex"></div>
|
|
BLADE,
|
|
];
|
|
@endphp
|
|
OUTER);
|
|
|
|
try {
|
|
$pattern = '/(?<![\w@-])(?:max-)?(?:sm|md|lg|xl|2xl):[a-z\[(-]/';
|
|
$probe = File::allFiles($directory)[0];
|
|
|
|
expect(breakpointOffenders($probe, $pattern))->not->toBeEmpty();
|
|
} finally {
|
|
File::deleteDirectory($directory);
|
|
}
|
|
});
|