Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal to fix Tailwind CSS v4 support #86

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
8 changes: 7 additions & 1 deletion doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ Tailwind CSS for Symfony!

.. caution::

This bundle does not yet support Tailwind CSS 4.0.
This bundle support Tailwind CSS 4.0 but PostCSS is not supported by the standalone binary.
Please specify a lower Tailwind binary version if you want to use PostCSS.

This bundle makes it easy to use `Tailwind CSS <https://tailwindcss.com/>`_ with
Symfony's `AssetMapper Component <https://symfony.com/doc/current/frontend/asset_mapper.html>`_
Expand Down Expand Up @@ -246,6 +247,11 @@ set ``binary_version`` option:
Using a PostCSS config file
------------------------

.. warning::

Tailwind CSS standalone binary v4.0.0 or higher doesn't support PostCSS anymore.
If you want to use PostCSS, please specify a previous binary version.

If you want to use additional PostCSS plugins, you can specify the
PostCSS config file to use, set ``postcss_config_file`` option or
pass the ``--postcss`` option to the ``tailwind:build`` command.
Expand Down
47 changes: 34 additions & 13 deletions src/Command/TailwindInitCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,40 @@ private function addTailwindDirectives(SymfonyStyle $io): void
{
$inputFile = $this->tailwindBuilder->getInputCssPaths()[0];
$contents = is_file($inputFile) ? file_get_contents($inputFile) : '';
if (str_contains($contents, '@tailwind base')) {
$io->note(\sprintf('Tailwind directives already exist in "%s"', $inputFile));

return;
$versionEqualsOrGreaterThan4 = $this->tailwindBuilder->isBinaryVersionEqualOrGreaterThan4();
$tailwindEqualsOrGreaterThan4Directive = '@import "tailwindcss";';

if ($versionEqualsOrGreaterThan4) {
if (str_contains($contents, $tailwindEqualsOrGreaterThan4Directive)) {
$io->note(\sprintf('New Tailwind 4 or higher directive already exist in "%s"', $inputFile));

return;
}
if (str_contains($contents, '@tailwind base')) {
$io->note(\sprintf('Removing old Tailwind directives from "%s"', $inputFile));
$oldDirectives = '@tailwind base;'.\PHP_EOL.'@tailwind components;'.\PHP_EOL.'@tailwind utilities;'.\PHP_EOL.\PHP_EOL;
$contents = str_replace($oldDirectives, '', $contents);
}
$io->note(\sprintf('Adding Tailwind 4 or higher directive to "%s"', $inputFile));
file_put_contents($inputFile, $tailwindEqualsOrGreaterThan4Directive.\PHP_EOL.\PHP_EOL.$contents);
} else {
if (str_contains($contents, '@tailwind base')) {
$io->note(\sprintf('Tailwind directives already exist in "%s"', $inputFile));

return;
}
if (str_contains($contents, $tailwindEqualsOrGreaterThan4Directive)) {
$io->note(\sprintf('Removing Tailwind 4 or higher directive from "%s"', $inputFile));
$contents = str_replace($tailwindEqualsOrGreaterThan4Directive.\PHP_EOL.\PHP_EOL, '', $contents);
}
$io->note(\sprintf('Adding Tailwind directives to "%s"', $inputFile));
$tailwindDirectives = <<<EOF
@tailwind base;
@tailwind components;
@tailwind utilities;
EOF;

file_put_contents($inputFile, $tailwindDirectives.\PHP_EOL.$contents);
}

$io->note(\sprintf('Adding Tailwind directives to "%s"', $inputFile));
$tailwindDirectives = <<<EOF
@tailwind base;
@tailwind components;
@tailwind utilities;
EOF;

file_put_contents($inputFile, $tailwindDirectives."\n\n".$contents);
}
}
2 changes: 1 addition & 1 deletion src/DependencyInjection/TailwindExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->scalarNode('binary_version')
->info('Tailwind CLI version to download - null means the latest version')
->defaultValue('v3.4.17')
->defaultNull()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be kept. I think we need to enforce a version going forward so we don't unexpectedly upgrade.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. The fact that future versions will not support PostCSS, it's better to enforce the last 3.X.X version to let developer choose to upgrade or not.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put back the default binary version to v3.4.17 and added info in the doc.
I pushed the exception in the case of using PostCSS with v4 or higher.

->end()
->scalarNode('postcss_config_file')
->info('Path to PostCSS config file which is passed to the Tailwind CLI')
Expand Down
2 changes: 1 addition & 1 deletion src/TailwindBinary.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ private function downloadExecutable(): void
chmod($targetPath, 0777);
}

private function getVersion(): string
public function getVersion(): string
{
return $this->cachedVersion ??= $this->binaryVersion ?? $this->getLatestVersion();
}
Expand Down
8 changes: 8 additions & 0 deletions src/TailwindBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ public function getOutputCssContent(string $inputFile): string
return file_get_contents($this->getInternalOutputCssPath($inputFile));
}

public function isBinaryVersionEqualOrGreaterThan4(): bool
{
$binaryVersion = $this->createBinary()->getVersion();

return str_starts_with($binaryVersion, 'v')
&& version_compare(substr($binaryVersion, 1), '4') >= 0;
}

private function validateInputFile(string $inputPath): string
{
if (is_file($inputPath)) {
Expand Down
59 changes: 41 additions & 18 deletions tests/TailwindBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,25 @@ protected function tearDown(): void
}
}

protected function isTailwindBinaryEqualOrGreaterThan4(): bool
{
$versionBuilder = new TailwindBuilder(
'',
[],
'',
new ArrayAdapter(),
);

return $versionBuilder->isBinaryVersionEqualOrGreaterThan4();
}

public function testIntegrationWithDefaultOptions(): void
{
$cssFilesSuffix = $this->isTailwindBinaryEqualOrGreaterThan4() ? '-v4' : '';

$builder = new TailwindBuilder(
__DIR__.'/fixtures',
[__DIR__.'/fixtures/assets/styles/app.css'],
[__DIR__.'/fixtures/assets/styles/app'.$cssFilesSuffix.'.css'],
__DIR__.'/fixtures/var/tailwind',
new ArrayAdapter(),
null,
Expand All @@ -59,17 +73,19 @@ public function testIntegrationWithDefaultOptions(): void
$process->wait();

$this->assertTrue($process->isSuccessful());
$this->assertFileExists(__DIR__.'/fixtures/var/tailwind/app.built.css');
$this->assertFileExists(__DIR__.'/fixtures/var/tailwind/app'.$cssFilesSuffix.'.built.css');

$outputFileContents = file_get_contents(__DIR__.'/fixtures/var/tailwind/app.built.css');
$this->assertStringContainsString("body {\n background-color: red;\n}", $outputFileContents, 'The output file should contain non-minified CSS.');
$outputFileContents = file_get_contents(__DIR__.'/fixtures/var/tailwind/app'.$cssFilesSuffix.'.built.css');
$this->assertStringContainsString("body {\n background-color: #ef4444;\n}", $outputFileContents, 'The output file should contain non-minified CSS.');
}

public function testIntegrationWithMinify(): void
{
$cssFilesSuffix = $this->isTailwindBinaryEqualOrGreaterThan4() ? '-v4' : '';

$builder = new TailwindBuilder(
__DIR__.'/fixtures',
[__DIR__.'/fixtures/assets/styles/app.css'],
[__DIR__.'/fixtures/assets/styles/app'.$cssFilesSuffix.'.css'],
__DIR__.'/fixtures/var/tailwind',
new ArrayAdapter(),
null,
Expand All @@ -80,52 +96,59 @@ public function testIntegrationWithMinify(): void
$process->wait();

$this->assertTrue($process->isSuccessful());
$this->assertFileExists(__DIR__.'/fixtures/var/tailwind/app.built.css');
$this->assertFileExists(__DIR__.'/fixtures/var/tailwind/app'.$cssFilesSuffix.'.built.css');

$outputFileContents = file_get_contents(__DIR__.'/fixtures/var/tailwind/app.built.css');
$this->assertStringContainsString('body{background-color:red}', $outputFileContents, 'The output file should contain minified CSS.');
$outputFileContents = file_get_contents(__DIR__.'/fixtures/var/tailwind/app'.$cssFilesSuffix.'.built.css');
$this->assertStringContainsString('body{background-color:#ef4444}', $outputFileContents, 'The output file should contain minified CSS.');
}

public function testBuildProvidedInputFile(): void
{
$cssFilesSuffix = $this->isTailwindBinaryEqualOrGreaterThan4() ? '-v4' : '';

$builder = new TailwindBuilder(
__DIR__.'/fixtures',
[__DIR__.'/fixtures/assets/styles/app.css', __DIR__.'/fixtures/assets/styles/second.css'],
[__DIR__.'/fixtures/assets/styles/app'.$cssFilesSuffix.'.css', __DIR__.'/fixtures/assets/styles/second'.$cssFilesSuffix.'.css'],
__DIR__.'/fixtures/var/tailwind',
new ArrayAdapter(),
null,
'v3.4.17',
null,
__DIR__.'/fixtures/tailwind.config.js'
);
$process = $builder->runBuild(watch: false, poll: false, minify: true, inputFile: 'assets/styles/second.css');
$process = $builder->runBuild(watch: false, poll: false, minify: true, inputFile: 'assets/styles/second'.$cssFilesSuffix.'.css');
$process->wait();

$this->assertTrue($process->isSuccessful());
$this->assertFileExists(__DIR__.'/fixtures/var/tailwind/second.built.css');
$this->assertFileExists(__DIR__.'/fixtures/var/tailwind/second'.$cssFilesSuffix.'.built.css');

$outputFileContents = file_get_contents(__DIR__.'/fixtures/var/tailwind/second.built.css');
$this->assertStringContainsString('body{background-color:blue}', $outputFileContents, 'The output file should contain minified CSS.');
$outputFileContents = file_get_contents(__DIR__.'/fixtures/var/tailwind/second'.$cssFilesSuffix.'.built.css');
$this->assertStringContainsString('body{background-color:#3b82f6}', $outputFileContents, 'The output file should contain minified CSS.');
}

public function testIntegrationWithPostcss(): void
{
$cssFilesSuffix = $this->isTailwindBinaryEqualOrGreaterThan4() ? '-v4' : '';
if ($cssFilesSuffix) {
$this->markTestSkipped('Postcss seems not compatible with Tailwind CLI v4!');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, if so I think we should add a note about that in https://symfony.com/bundles/TailwindBundle/current/index.html#using-a-postcss-config-file . WDYT? IIRC Symfony docs have some special syntax for versions, probably we could leverage it to let it know that this does not work since v4 of tailwind

Copy link
Author

@nikomuse nikomuse Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I opened an issue on the TailwindCSS repo and I've got an answer :

"The CLI does not use postcss anymore so we don't have a plan to making this work.
If you rely on postcss I recommend using the postcss-cli directly with the @tailwindcss/postcss extension."

So unfortunately it will not be available in the future.
I'll check the SF docs formats and standards to update it.
Thanks.

Copy link
Author

@nikomuse nikomuse Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just put a warning block in the documentation as the deprecated one seems more appropriate for the bundle version that the binary version. What do you thing about it ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, we could raise and test an exception in case of using PostCSS with v4+ binary. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, throwing an excepting with a clear message explaining that it does not work in Tailwind v4 anymore sounds good to me. Though what happens in the Tailwind binary if when you continue using postcss? Does it also throw an error? Probably the best way would be to do the same to be consistent. But wait, if we just pass the same option to the Tailwind binary, won't it fail upstream? I mean, in theory, we should do nothing because it should fail anyway when the Tailwind binary will run, or am I missing something?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, actually nothing happens! You can add any fake argument to the binary while executing it, no error is thrown and the css is built correctly but without PostCSS processing...

}

$builder = new TailwindBuilder(
__DIR__.'/fixtures',
[__DIR__.'/fixtures/assets/styles/app.css'],
[__DIR__.'/fixtures/assets/styles/app'.$cssFilesSuffix.'.css'],
__DIR__.'/fixtures/var/tailwind',
new ArrayAdapter(),
null,
'v3.4.17',
null,
__DIR__.'/fixtures/tailwind.config.js',
__DIR__.'/fixtures/postcss.config.js',
);
$process = $builder->runBuild(watch: false, poll: false, minify: false);
$process->wait();

$this->assertTrue($process->isSuccessful());
$this->assertFileExists(__DIR__.'/fixtures/var/tailwind/app.built.css');
$this->assertFileExists(__DIR__.'/fixtures/var/tailwind/app'.$cssFilesSuffix.'.built.css');

$outputFileContents = file_get_contents(__DIR__.'/fixtures/var/tailwind/app.built.css');
$outputFileContents = file_get_contents(__DIR__.'/fixtures/var/tailwind/app'.$cssFilesSuffix.'.built.css');
$this->assertStringContainsString('.dummy {}', $outputFileContents, 'The output file should contain the dummy CSS added by the dummy plugin.');
}
}
5 changes: 5 additions & 0 deletions tests/fixtures/assets/styles/app-v4.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "tailwindcss";

body {
background-color: #ef4444;
}
2 changes: 1 addition & 1 deletion tests/fixtures/assets/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
@tailwind utilities;

body {
background-color: red;
background-color: #ef4444;
}
5 changes: 5 additions & 0 deletions tests/fixtures/assets/styles/second-v4.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "tailwindcss";

body {
background-color: #3b82f6;
}
2 changes: 1 addition & 1 deletion tests/fixtures/assets/styles/second.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
@tailwind utilities;

body {
background-color: blue;
background-color: #3b82f6;
}
Loading