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;
}