Skip to content

Commit 43100c1

Browse files
authored
Merge pull request #91 from kbond/tw4
Tailwind v4 Support
2 parents 81c9e6f + da6bb70 commit 43100c1

File tree

11 files changed

+192
-73
lines changed

11 files changed

+192
-73
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"symfony/console": "^5.4|^6.3|^7.0",
1717
"symfony/http-client": "^5.4|^6.3|^7.0",
1818
"symfony/process": "^5.4|^6.3|^7.0",
19-
"symfony/cache": "^6.3|^7.0"
19+
"symfony/cache": "^6.3|^7.0",
20+
"symfony/deprecation-contracts": "^2.2|^3.0"
2021
},
2122
"require-dev": {
2223
"symfony/filesystem": "^6.3|^7.0",

config/services.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,11 @@
1212

1313
return static function (ContainerConfigurator $container): void {
1414
$container->services()
15-
->set('cache.symfonycasts.tailwind_bundle')
16-
->parent('cache.system')
17-
->tag('cache.pool')
18-
1915
->set('tailwind.builder', TailwindBuilder::class)
2016
->args([
2117
param('kernel.project_dir'),
2218
abstract_arg('path to source Tailwind CSS file'),
2319
param('kernel.project_dir').'/var/tailwind',
24-
service('cache.symfonycasts.tailwind_bundle'),
2520
abstract_arg('path to tailwind binary'),
2621
abstract_arg('Tailwind binary version'),
2722
abstract_arg('path to Tailwind CSS config file'),

doc/index.rst

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
Tailwind CSS for Symfony!
22
=========================
33

4-
.. caution::
5-
6-
This bundle does not yet support Tailwind CSS 4.0.
7-
84
This bundle makes it easy to use `Tailwind CSS <https://tailwindcss.com/>`_ with
95
Symfony's `AssetMapper Component <https://symfony.com/doc/current/frontend/asset_mapper.html>`_
106
(no Node.js required!).
@@ -31,6 +27,10 @@ Install the bundle & initialize your app with two commands:
3127
Done! This will create a ``tailwind.config.js`` file and make sure your
3228
``assets/styles/app.css`` contains the Tailwind directives.
3329

30+
.. note::
31+
32+
If using Tailwind CSS v4+, ``tailwind.config.js`` is not created or used.
33+
3434
Usage
3535
-----
3636

@@ -69,6 +69,10 @@ command with ``--poll`` option.
6969
7070
$ php bin/console tailwind:build --watch --poll
7171
72+
.. caution::
73+
74+
The ``--poll`` option is not available in Tailwind CSS v4+.
75+
7276
Symfony CLI
7377
~~~~~~~~~~~
7478

@@ -130,6 +134,11 @@ The Tailwind binary that the bundle downloads already contains the "Official Plu
130134
This means you can use those simply by adding the line to the ``plugins`` key in
131135
``tailwind.config.js`` - e.g. ``require('@tailwindcss/typography')``.
132136

137+
.. note
138+
139+
In Tailwind CSS v4 you include plugins with the ``@plugin`` directive in your
140+
input CSS file - e.g. ``@plugin "@tailwindcss/typography";``.
141+
133142
For other plugins - like `Flowbite Datepicker <https://flowbite.com/docs/plugins/datepicker/>`_,
134143
you will need to follow that package's documentation to `require the package <https://flowbite.com/docs/getting-started/quickstart/#require-via-npm>`_
135144
with ``npm``:
@@ -179,6 +188,10 @@ It's possible to use multiple input files by providing an array:
179188
Another option is the ``config_file`` option, which defaults to ``tailwind.config.js``.
180189
This represents the Tailwind configuration file:
181190

191+
.. caution::
192+
193+
The ``config_file`` is ignored in Tailwind CSS v4+.
194+
182195
.. code-block:: yaml
183196
184197
# config/packages/symfonycasts_tailwind.yaml
@@ -231,11 +244,9 @@ To instruct the bundle to use that binary instead, set the ``binary`` option:
231244
binary: 'node_modules/.bin/tailwindcss'
232245
233246
Using a Different Binary Version
234-
------------------------
247+
--------------------------------
235248

236-
By default, the latest standalone Tailwind binary gets downloaded. However,
237-
if you want to use a different version, you can specify the version to use,
238-
set ``binary_version`` option:
249+
To use a different version, adjust the ``binary_version`` option:
239250

240251
.. code-block:: yaml
241252
@@ -244,7 +255,11 @@ set ``binary_version`` option:
244255
binary_version: 'v3.3.0'
245256
246257
Using a PostCSS config file
247-
------------------------
258+
---------------------------
259+
260+
.. caution::
261+
262+
PostCSS config is not available in Tailwind CSS v4+.
248263

249264
If you want to use additional PostCSS plugins, you can specify the
250265
PostCSS config file to use, set ``postcss_config_file`` option or

src/Command/TailwindInitCommand.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
4848

4949
private function createTailwindConfig(SymfonyStyle $io): bool
5050
{
51+
if ($this->tailwindBuilder->createBinary()->isV4()) {
52+
$io->note('Tailwind v4 detected: skipping config file creation.');
53+
54+
return true;
55+
}
56+
5157
$configFile = $this->tailwindBuilder->getConfigFilePath();
5258
if (file_exists($configFile)) {
5359
$io->note(\sprintf('Tailwind config file already exists in "%s"', $configFile));
@@ -94,7 +100,7 @@ private function addTailwindDirectives(SymfonyStyle $io): void
94100
{
95101
$inputFile = $this->tailwindBuilder->getInputCssPaths()[0];
96102
$contents = is_file($inputFile) ? file_get_contents($inputFile) : '';
97-
if (str_contains($contents, '@tailwind base')) {
103+
if (str_contains($contents, '@tailwind base') || str_contains($contents, '@import "tailwindcss"')) {
98104
$io->note(\sprintf('Tailwind directives already exist in "%s"', $inputFile));
99105

100106
return;
@@ -107,6 +113,12 @@ private function addTailwindDirectives(SymfonyStyle $io): void
107113
@tailwind utilities;
108114
EOF;
109115

116+
if ($this->tailwindBuilder->createBinary()->isV4()) {
117+
$tailwindDirectives = <<<EOF
118+
@import "tailwindcss";
119+
EOF;
120+
}
121+
110122
file_put_contents($inputFile, $tailwindDirectives."\n\n".$contents);
111123
}
112124
}

src/DependencyInjection/TailwindExtension.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ public function load(array $configs, ContainerBuilder $container): void
2929

3030
$container->findDefinition('tailwind.builder')
3131
->replaceArgument(1, $config['input_css'])
32-
->replaceArgument(4, $config['binary'])
33-
->replaceArgument(5, $config['binary_version'])
34-
->replaceArgument(6, $config['config_file'])
35-
->replaceArgument(7, $config['postcss_config_file'])
32+
->replaceArgument(3, $config['binary'])
33+
->replaceArgument(4, $config['binary_version'])
34+
->replaceArgument(5, $config['config_file'])
35+
->replaceArgument(6, $config['postcss_config_file'])
3636
;
3737
}
3838

@@ -70,7 +70,13 @@ public function getConfigTreeBuilder(): TreeBuilder
7070
->end()
7171
->scalarNode('binary_version')
7272
->info('Tailwind CLI version to download - null means the latest version')
73-
->defaultValue('v3.4.17')
73+
->defaultNull()
74+
->beforeNormalization()
75+
->ifString()
76+
->then(static function (string $version): string {
77+
return 'v'.ltrim($version, 'vV');
78+
})
79+
->end()
7480
->end()
7581
->scalarNode('postcss_config_file')
7682
->info('Path to PostCSS config file which is passed to the Tailwind CLI')

src/TailwindBinary.php

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@
99

1010
namespace Symfonycasts\TailwindBundle;
1111

12-
use Psr\Cache\CacheItemInterface;
1312
use Symfony\Component\Console\Style\SymfonyStyle;
1413
use Symfony\Component\HttpClient\HttpClient;
1514
use Symfony\Component\Process\Process;
16-
use Symfony\Contracts\Cache\CacheInterface;
1715
use Symfony\Contracts\HttpClient\HttpClientInterface;
1816

1917
/**
@@ -23,36 +21,87 @@
2321
*/
2422
class TailwindBinary
2523
{
24+
private const DEFAULT_VERSION = 'v3.4.17';
25+
2626
private HttpClientInterface $httpClient;
27-
private ?string $cachedVersion = null;
2827

2928
public function __construct(
3029
private string $binaryDownloadDir,
3130
private string $cwd,
3231
private ?string $binaryPath,
3332
private ?string $binaryVersion,
34-
private CacheInterface $cache,
3533
private ?SymfonyStyle $output = null,
3634
?HttpClientInterface $httpClient = null,
3735
) {
3836
$this->httpClient = $httpClient ?? HttpClient::create();
37+
38+
if (!$this->binaryVersion && !$this->binaryPath) {
39+
trigger_deprecation(
40+
'symfonycasts/tailwind-bundle',
41+
'0.8',
42+
'Not specifying a "binary" or "binary_version" is deprecated. %s is being used.',
43+
self::DEFAULT_VERSION,
44+
);
45+
46+
$this->binaryVersion = self::DEFAULT_VERSION;
47+
}
48+
49+
if ($this->binaryVersion && $this->binaryPath) {
50+
$this->binaryVersion = null;
51+
}
3952
}
4053

4154
public function createProcess(array $arguments = []): Process
4255
{
43-
if (null === $this->binaryPath) {
44-
$binary = $this->binaryDownloadDir.'/'.$this->getVersion().'/'.self::getBinaryName();
45-
if (!is_file($binary)) {
46-
$this->downloadExecutable();
47-
}
48-
} else {
49-
$binary = $this->binaryPath;
56+
// add binary to the front of the $arguments array
57+
array_unshift($arguments, $this->getBinaryPath());
58+
59+
return new Process($arguments, $this->cwd);
60+
}
61+
62+
public function getVersion(): string
63+
{
64+
if ($this->binaryVersion) {
65+
return $this->binaryVersion;
5066
}
5167

52-
// add $binary to the front of the $arguments array
53-
array_unshift($arguments, $binary);
68+
$process = $this->createProcess(['--help']);
69+
$process->run();
5470

55-
return new Process($arguments, $this->cwd);
71+
if (!$process->isSuccessful()) {
72+
throw new \RuntimeException('Could not determine the tailwindcss version.');
73+
}
74+
75+
if (!preg_match('#(v\d+\.\d+\.\d+)#', $process->getOutput(), $matches)) {
76+
throw new \RuntimeException('Could not determine the tailwindcss version.');
77+
}
78+
79+
return $this->binaryVersion = $matches[1];
80+
}
81+
82+
public function isV4(): bool
83+
{
84+
return version_compare($this->getRawVersion(), '4.0.0', '>=');
85+
}
86+
87+
public function getRawVersion(): string
88+
{
89+
return ltrim($this->getVersion(), 'v');
90+
}
91+
92+
private function getBinaryPath(): string
93+
{
94+
if ($this->binaryPath) {
95+
return $this->binaryPath;
96+
}
97+
98+
$this->binaryPath = $this->binaryDownloadDir.'/'.$this->getVersion().'/'.self::getBinaryName();
99+
100+
if (!is_file($this->binaryPath)) {
101+
$this->downloadExecutable();
102+
}
103+
104+
return $this->binaryPath;
56105
}
57106

58107
private function downloadExecutable(): void
@@ -93,25 +142,6 @@ private function downloadExecutable(): void
93142
chmod($targetPath, 0777);
94143
}
95144

96-
private function getVersion(): string
97-
{
98-
return $this->cachedVersion ??= $this->binaryVersion ?? $this->getLatestVersion();
99-
}
100-
101-
private function getLatestVersion(): string
102-
{
103-
return $this->cache->get('latestVersion', function (CacheItemInterface $item) {
104-
$item->expiresAfter(3600);
105-
try {
106-
$response = $this->httpClient->request('GET', 'https://api.github.com/repos/tailwindlabs/tailwindcss/releases/latest');
107-
108-
return $response->toArray()['name'] ?? throw new \Exception('Cannot get the latest version name from response JSON.');
109-
} catch (\Throwable $e) {
110-
throw new \Exception('Cannot determine latest Tailwind CLI binary version. Please specify a version in the configuration.', previous: $e);
111-
}
112-
});
113-
}
114-
115145
/**
116146
* @internal
117147
*/

src/TailwindBuilder.php

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Symfony\Component\Console\Style\SymfonyStyle;
1313
use Symfony\Component\Process\InputStream;
1414
use Symfony\Component\Process\Process;
15-
use Symfony\Contracts\Cache\CacheInterface;
1615

1716
/**
1817
* Manages the process of executing Tailwind on the input file.
@@ -25,12 +24,12 @@ class TailwindBuilder
2524
{
2625
private ?SymfonyStyle $output = null;
2726
private readonly array $inputPaths;
27+
private TailwindBinary $binary;
2828

2929
public function __construct(
3030
private readonly string $projectRootDir,
3131
array $inputPaths,
3232
private readonly string $tailwindVarDir,
33-
private CacheInterface $cache,
3433
private readonly ?string $binaryPath = null,
3534
private readonly ?string $binaryVersion = null,
3635
private readonly string $configPath = 'tailwind.config.js',
@@ -58,6 +57,10 @@ public function runBuild(
5857
throw new \InvalidArgumentException(\sprintf('The input CSS file "%s" is not one of the configured input files.', $inputPath));
5958
}
6059

60+
if ($poll && $binary->isV4()) {
61+
throw new \InvalidArgumentException('The --poll option is not supported in Tailwind CSS v4.0.0 and later.');
62+
}
63+
6164
$arguments = ['-c', $this->configPath, '-i', $inputPath, '-o', $this->getInternalOutputCssPath($inputPath)];
6265
if ($watch) {
6366
$arguments[] = '--watch';
@@ -69,7 +72,13 @@ public function runBuild(
6972
$arguments[] = '--minify';
7073
}
7174

72-
$postCssConfigPath = $this->validatePostCssConfigFile($postCssConfigFile ?? $this->postCssConfigPath);
75+
$postCssConfigFile ??= $this->postCssConfigPath;
76+
77+
if ($postCssConfigFile && $binary->isV4()) {
78+
throw new \InvalidArgumentException('Custom PostCSS configuration is not supported in Tailwind CSS v4.0.0 and later.');
79+
}
80+
81+
$postCssConfigPath = $this->validatePostCssConfigFile($postCssConfigFile);
7382
if ($postCssConfigPath) {
7483
$arguments[] = '--postcss';
7584
$arguments[] = $postCssConfigPath;
@@ -140,6 +149,11 @@ public function getOutputCssContent(string $inputFile): string
140149
return file_get_contents($this->getInternalOutputCssPath($inputFile));
141150
}
142151

152+
public function createBinary(): TailwindBinary
153+
{
154+
return $this->binary ??= new TailwindBinary($this->tailwindVarDir, $this->projectRootDir, $this->binaryPath, $this->binaryVersion, $this->output);
155+
}
156+
143157
private function validateInputFile(string $inputPath): string
144158
{
145159
if (is_file($inputPath)) {
@@ -169,9 +183,4 @@ private function validatePostCssConfigFile(?string $postCssConfigPath): ?string
169183

170184
throw new \InvalidArgumentException(\sprintf('The PostCSS config file "%s" does not exist.', $postCssConfigPath));
171185
}
172-
173-
private function createBinary(): TailwindBinary
174-
{
175-
return new TailwindBinary($this->tailwindVarDir, $this->projectRootDir, $this->binaryPath, $this->binaryVersion, $this->cache, $this->output);
176-
}
177186
}

0 commit comments

Comments
 (0)