Build the Workbench's package CSS apart from Tailwind

Plan step 37 review. @tailwindcss/vite inlines the imports of an entry
that uses Tailwind without Vite's skipDuplicates, so with all.css beside
@import 'tailwindcss' every shared component stylesheet repeated later in
the cascade (231 repeated rules; button.css's hover and disabled rules
thirteen times), and every browser test ran against an order no
application gets. workbench/resources/css/package.css now holds all.css
and the scheme in an entry Tailwind never touches, and app.css keeps
Tailwind for the showcase's classes. The built package CSS repeats no
rule, and the Workbench's CSS shrinks from 559 to 406 KB.

Both entries open with the same layer statement, properties first,
because Tailwind hoists that layer to the top of its output; the order
is the one the single entry had, whichever the page links first. The
showcase's Vite config, vite.config.js and the error page test's probe
manifest name the new entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-15 03:12:44 +02:00
co-authored by Claude Opus 5
parent eda16ba727
commit 3854f4321c
6 changed files with 55 additions and 28 deletions
+2
View File
@@ -97,6 +97,7 @@ it('lets the application\'s own error view win', function () {
it('loads the application\'s Vite entries', function () { it('loads the application\'s Vite entries', function () {
File::ensureDirectoryExists($this->temporary.'/build'); File::ensureDirectoryExists($this->temporary.'/build');
File::put($this->temporary.'/build/manifest.json', json_encode([ File::put($this->temporary.'/build/manifest.json', json_encode([
'workbench/resources/css/package.css' => ['file' => 'assets/package-probe.css', 'src' => 'workbench/resources/css/package.css', 'isEntry' => true],
'workbench/resources/css/app.css' => ['file' => 'assets/app-probe.css', 'src' => 'workbench/resources/css/app.css', 'isEntry' => true], 'workbench/resources/css/app.css' => ['file' => 'assets/app-probe.css', 'src' => 'workbench/resources/css/app.css', 'isEntry' => true],
'workbench/resources/js/app.js' => ['file' => 'assets/app-probe.js', 'src' => 'workbench/resources/js/app.js', 'isEntry' => true], 'workbench/resources/js/app.js' => ['file' => 'assets/app-probe.js', 'src' => 'workbench/resources/js/app.js', 'isEntry' => true],
])); ]));
@@ -106,6 +107,7 @@ it('loads the application\'s Vite entries', function () {
$this->get('/abort/404') $this->get('/abort/404')
->assertNotFound() ->assertNotFound()
->assertSee('build/assets/package-probe.css', false)
->assertSee('build/assets/app-probe.css', false) ->assertSee('build/assets/app-probe.css', false)
->assertSee('build/assets/app-probe.js', false) ->assertSee('build/assets/app-probe.js', false)
->assertDontSee('data-md-error-fallback', false); ->assertDontSee('data-md-error-fallback', false);
+28 -16
View File
@@ -630,23 +630,35 @@ it('draws the interaction classes as the utilities the Tailwind components still
->and(stylesheetBlock($interaction, '.md-touch-target'))->toContain('min-width: var(--md-sys-measurement-space600);'); ->and(stylesheetBlock($interaction, '.md-touch-target'))->toContain('min-width: var(--md-sys-measurement-space600);');
}); });
it('declares the material layers in the Workbench between Tailwind\'s preflight and its utilities', function () { it('builds the package in a Workbench entry of its own, with the material layers between Tailwind\'s preflight and its utilities', function () {
$package = stylesheetWithoutComments(File::get(__DIR__.'/../../workbench/resources/css/package.css'));
$app = stylesheetWithoutComments(File::get(__DIR__.'/../../workbench/resources/css/app.css')); $app = stylesheetWithoutComments(File::get(__DIR__.'/../../workbench/resources/css/app.css'));
// The first statement orders the layers: above preflight, whose `* { padding: 0 }` would // Both entries open by ordering the layers, so the order holds whichever the page links first:
// otherwise beat a rewritten component's padding, and below the utilities. // above preflight, whose `* { padding: 0 }` would otherwise beat a component's padding, and
expect(trim($app))->toStartWith('@layer theme, base, material, components, utilities;'); // below the utilities.
foreach (['package.css' => $package, 'app.css' => $app] as $name => $css) {
expect(trim($css))->toStartWith('@layer properties, theme, base, material, components, utilities;', $name);
}
$all = strpos($app, "@import '../../../resources/css/all.css';"); // The package's CSS never shares an entry with Tailwind: `@tailwindcss/vite` inlines the
$tailwind = strpos($app, "@import 'tailwindcss'"); // imports of an entry that uses Tailwind without deduplicating them, so every shared component
$components = strpos($app, "@import '../../../resources/css/tailwind.css';"); // stylesheet would repeat later in the cascade instead of landing once, as it does for an
// application (StylesheetsBundleTest.php pins that against a real Vite build).
expect($all)->toBeInt() expect($package)->toContain("@import '../../../resources/css/all.css';")
->and($tailwind)->toBeGreaterThan($all) ->toContain("@import './material-scheme.css';")
->and($components)->toBeGreaterThan($tailwind) ->not->toContain('tailwind')
// material.css declared the tokens a second time; components.css and layout.css are the ->not->toMatch('/@(?:source|theme|utility|variant|custom-variant|apply)\b/')
// interim lists all.css replaced. ->and($app)->toContain("@import 'tailwindcss'")
->and($app)->not->toContain('material.css') ->toContain("@import '../../../resources/css/tailwind.css';")
->not->toContain("resources/css/components.css'") ->not->toContain('all.css')
->not->toContain("resources/css/layout.css'"); ->not->toContain('foundation.css')
->not->toContain('/components/')
->not->toContain('material-scheme.css')
->and(File::get(__DIR__.'/../../vite.config.js'))->toContain("'workbench/resources/css/package.css', 'workbench/resources/css/app.css'")
->and(config('livewire-material.showcase.vite'))->toBe([
'workbench/resources/css/package.css',
'workbench/resources/css/app.css',
'workbench/resources/js/app.js',
]);
}); });
+1 -1
View File
@@ -7,7 +7,7 @@ import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
laravel({ laravel({
input: ['workbench/resources/css/app.css', 'workbench/resources/js/app.js'], input: ['workbench/resources/css/package.css', 'workbench/resources/css/app.css', 'workbench/resources/js/app.js'],
publicDirectory: 'workbench/public', publicDirectory: 'workbench/public',
refresh: ['resources/views/**', 'workbench/resources/views/**'], refresh: ['resources/views/**', 'workbench/resources/views/**'],
}), }),
@@ -19,6 +19,8 @@ class WorkbenchServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
config(['livewire-material.showcase.vite' => [ config(['livewire-material.showcase.vite' => [
// The package's CSS apart from Tailwind's, so Vite deduplicates its imports (package.css).
'workbench/resources/css/package.css',
'workbench/resources/css/app.css', 'workbench/resources/css/app.css',
'workbench/resources/js/app.js', 'workbench/resources/js/app.js',
]]); ]]);
+6 -11
View File
@@ -1,16 +1,11 @@
/* The Workbench builds the package as an application would, with explicit sources so the plans /* The Workbench's Tailwind entry, for the classes the showcase still writes, with explicit sources
in docs/ are not scanned for class names. The layer statement puts the `material` layers above so the plans in docs/ are not scanned for class names. tailwind.css brings the theme and token
Tailwind's preflight (`base`) and below its `components` and `utilities`: a rewritten component's utilities without declaring the tokens a second time. The package's own CSS is package.css, an
padding, margin and border would otherwise lose to preflight's `* { padding: 0 }`, which the entry of its own (its header says why); the layer statement is the same in both, so the
material reset already carries, while every utility still outranks the package. all.css brings `material` layers sit between Tailwind's preflight and its utilities whichever loads first. */
the foundation, every layout stylesheet and every component stylesheet in one import; tailwind.css @layer properties, theme, base, material, components, utilities;
brings the theme and token utilities the showcase still writes as classes, without declaring the
tokens a second time. */
@layer theme, base, material, components, utilities;
@import '../../../resources/css/all.css';
@import 'tailwindcss' source(none); @import 'tailwindcss' source(none);
@import '../../../resources/css/tailwind.css'; @import '../../../resources/css/tailwind.css';
@import './material-scheme.css';
@source '../../../resources/views'; @source '../../../resources/views';
@source '../../../src'; @source '../../../src';
+16
View File
@@ -0,0 +1,16 @@
/* The package's CSS for the Workbench, built the way an application without Tailwind builds it:
all.css (the foundation, every layout and every component stylesheet) and the Workbench's
scheme, in an entry Tailwind never touches. Vite's own CSS pipeline inlines the imports here and
keeps a stylesheet several components import once, at its first position — the order an
application gets. `@tailwindcss/vite` inlines the imports of an entry that uses Tailwind without
that deduplication, so all.css imported beside `tailwindcss` would repeat every shared rule later
in the cascade; app.css keeps Tailwind, for the showcase's classes, apart.
Both entries open with the same layer statement, so the order holds whichever the page links
first: the `material` layers above Tailwind's preflight (`base`), whose `* { padding: 0 }` would
otherwise beat a component's padding, and below its `components` and `utilities`. `properties`
is the layer Tailwind hoists to the top of its output (the `--tw-*` fallbacks for a browser
without `@property`); named first, it stays lowest, as it was when both shared one entry. */
@layer properties, theme, base, material, components, utilities;
@import '../../../resources/css/all.css';
@import './material-scheme.css';