diff --git a/doc/index.rst b/doc/index.rst index 3069ed6..88643ab 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -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. + By default, the Tailwind CSS binary v3.4.17 is used and works with PostCSS. This bundle makes it easy to use `Tailwind CSS `_ with Symfony's `AssetMapper Component `_ @@ -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, let the default v3.4.17 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. diff --git a/src/Command/TailwindInitCommand.php b/src/Command/TailwindInitCommand.php index 5c3e182..4dec9b6 100644 --- a/src/Command/TailwindInitCommand.php +++ b/src/Command/TailwindInitCommand.php @@ -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 = <<note(\sprintf('Adding Tailwind directives to "%s"', $inputFile)); - $tailwindDirectives = << + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfonycasts\TailwindBundle\Exception; + +/** + * Base ExceptionInterface for the TailwindCSS Bundle. + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..72a09ed --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,17 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfonycasts\TailwindBundle\Exception; + +/** + * Base InvalidArgumentException for the TailwindCSS Bundle. + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/TailwindBinary.php b/src/TailwindBinary.php index cb2d2bf..98ed67e 100644 --- a/src/TailwindBinary.php +++ b/src/TailwindBinary.php @@ -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(); } diff --git a/src/TailwindBuilder.php b/src/TailwindBuilder.php index 7d3fdec..d9637f9 100644 --- a/src/TailwindBuilder.php +++ b/src/TailwindBuilder.php @@ -13,6 +13,7 @@ use Symfony\Component\Process\InputStream; use Symfony\Component\Process\Process; use Symfony\Contracts\Cache\CacheInterface; +use Symfonycasts\TailwindBundle\Exception\InvalidArgumentException; /** * Manages the process of executing Tailwind on the input file. @@ -52,7 +53,6 @@ public function runBuild( ?string $postCssConfigFile = null, ): Process { $binary = $this->createBinary(); - $inputPath = $this->validateInputFile($inputFile ?? $this->inputPaths[0]); if (!\in_array($inputPath, $this->inputPaths)) { throw new \InvalidArgumentException(\sprintf('The input CSS file "%s" is not one of the configured input files.', $inputPath)); @@ -70,7 +70,9 @@ public function runBuild( } $postCssConfigPath = $this->validatePostCssConfigFile($postCssConfigFile ?? $this->postCssConfigPath); - if ($postCssConfigPath) { + if ($this->isBinaryVersionEqualOrGreaterThan4($binary) && $postCssConfigPath) { + throw new InvalidArgumentException('Tailwind 4+ does not support a PostCSS config file.'); + } elseif ($postCssConfigPath) { $arguments[] = '--postcss'; $arguments[] = $postCssConfigPath; } @@ -140,6 +142,15 @@ public function getOutputCssContent(string $inputFile): string return file_get_contents($this->getInternalOutputCssPath($inputFile)); } + public function isBinaryVersionEqualOrGreaterThan4(?TailwindBinary $binary = null): bool + { + $binary = $binary ?? $this->createBinary(); + $binaryVersion = $binary->getVersion(); + + return str_starts_with($binaryVersion, 'v') + && version_compare(substr($binaryVersion, 1), '4') >= 0; + } + private function validateInputFile(string $inputPath): string { if (is_file($inputPath)) { diff --git a/tests/TailwindBuilderTest.php b/tests/TailwindBuilderTest.php index 0b2a44c..abde32e 100644 --- a/tests/TailwindBuilderTest.php +++ b/tests/TailwindBuilderTest.php @@ -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, @@ -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, @@ -80,42 +96,49 @@ 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!'); + } + $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', ); @@ -123,9 +146,9 @@ public function testIntegrationWithPostcss(): 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'); + $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.'); } } diff --git a/tests/fixtures/assets/styles/app-v4.css b/tests/fixtures/assets/styles/app-v4.css new file mode 100644 index 0000000..5875251 --- /dev/null +++ b/tests/fixtures/assets/styles/app-v4.css @@ -0,0 +1,5 @@ +@import "tailwindcss"; + +body { + background-color: #ef4444; +} diff --git a/tests/fixtures/assets/styles/app.css b/tests/fixtures/assets/styles/app.css index f270572..814b489 100644 --- a/tests/fixtures/assets/styles/app.css +++ b/tests/fixtures/assets/styles/app.css @@ -3,5 +3,5 @@ @tailwind utilities; body { - background-color: red; + background-color: #ef4444; } diff --git a/tests/fixtures/assets/styles/second-v4.css b/tests/fixtures/assets/styles/second-v4.css new file mode 100644 index 0000000..0e2982a --- /dev/null +++ b/tests/fixtures/assets/styles/second-v4.css @@ -0,0 +1,5 @@ +@import "tailwindcss"; + +body { + background-color: #3b82f6; +} diff --git a/tests/fixtures/assets/styles/second.css b/tests/fixtures/assets/styles/second.css index e16ba4d..ac52985 100644 --- a/tests/fixtures/assets/styles/second.css +++ b/tests/fixtures/assets/styles/second.css @@ -3,5 +3,5 @@ @tailwind utilities; body { - background-color: blue; + background-color: #3b82f6; }