Skip to content

Commit 441599e

Browse files
committed
feat: interactive tailwind:init command
1 parent 43100c1 commit 441599e

File tree

6 files changed

+169
-17
lines changed

6 files changed

+169
-17
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"symfony/filesystem": "^6.3|^7.0",
2424
"symfony/framework-bundle": "^6.3|^7.0",
2525
"symfony/phpunit-bridge": "^6.3.9|^7.0",
26-
"phpunit/phpunit": "^9.6"
26+
"phpunit/phpunit": "^9.6",
27+
"symfony/yaml": "^6.3|^7.0"
2728
},
2829
"minimum-stability": "dev",
2930
"autoload": {

config/services.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Symfonycasts\TailwindBundle\Command\TailwindInitCommand;
77
use Symfonycasts\TailwindBundle\TailwindBuilder;
88

9+
use Symfonycasts\TailwindBundle\TailwindVersionFinder;
910
use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg;
1011
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
1112
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
@@ -23,6 +24,8 @@
2324
abstract_arg('path to PostCSS config file'),
2425
])
2526

27+
->set('tailwind.version_finder', TailwindVersionFinder::class)
28+
2629
->set('tailwind.command.build', TailwindBuildCommand::class)
2730
->args([
2831
service('tailwind.builder'),
@@ -31,7 +34,9 @@
3134

3235
->set('tailwind.command.init', TailwindInitCommand::class)
3336
->args([
34-
service('tailwind.builder'),
37+
service('tailwind.version_finder'),
38+
abstract_arg('path to source Tailwind CSS file'),
39+
param('kernel.project_dir'),
3540
])
3641
->tag('console.command')
3742

src/Command/TailwindInitCommand.php

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
use Symfony\Component\Console\Input\InputInterface;
1515
use Symfony\Component\Console\Output\OutputInterface;
1616
use Symfony\Component\Console\Style\SymfonyStyle;
17+
use Symfony\Component\Yaml\Yaml;
1718
use Symfonycasts\TailwindBundle\TailwindBuilder;
19+
use Symfonycasts\TailwindBundle\TailwindVersionFinder;
1820

1921
#[AsCommand(
2022
name: 'tailwind:init',
@@ -23,47 +25,72 @@
2325
class TailwindInitCommand extends Command
2426
{
2527
public function __construct(
26-
private TailwindBuilder $tailwindBuilder,
28+
private TailwindVersionFinder $versionFinder,
29+
private array $inputCss,
30+
private string $rootDir,
2731
) {
2832
parent::__construct();
2933
}
3034

31-
protected function configure(): void
32-
{
33-
}
34-
3535
protected function execute(InputInterface $input, OutputInterface $output): int
3636
{
3737
$io = new SymfonyStyle($input, $output);
38-
if (!$this->createTailwindConfig($io)) {
38+
39+
if (!$input->isInteractive()) {
40+
throw new \RuntimeException('tailwind:init command must be run interactively.');
41+
}
42+
43+
$bundleConfig = $this->bundleConfig();
44+
45+
if ($io->confirm('Are you managing your own Taildind CSS binary?', false)) {
46+
$binaryPath = $io->ask('Enter the path to your Tailwind CSS binary:', 'node_modules/.bin/tailwindcss');
47+
$bundleConfig['symfonycasts_tailwind']['binary'] = $binaryPath;
48+
} else {
49+
$majorVersion = $io->ask('Which major version do you wish to use?', '4');
50+
$latestVersion = $this->versionFinder->latestVersionFor($majorVersion);
51+
$bundleConfig['symfonycasts_tailwind']['binary_version'] = $latestVersion;
52+
}
53+
54+
file_put_contents($this->bundleConfigFile(), Yaml::dump($bundleConfig));
55+
56+
$builder = new TailwindBuilder(
57+
$this->rootDir,
58+
$this->inputCss,
59+
$this->rootDir.'/var/tailwind',
60+
binaryPath: $bundleConfig['symfonycasts_tailwind']['binary'] ?? null,
61+
binaryVersion: $bundleConfig['symfonycasts_tailwind']['binary_version'] ?? null,
62+
);
63+
64+
if (!$this->createTailwindConfig($io, $builder)) {
3965
return self::FAILURE;
4066
}
4167

42-
$this->addTailwindDirectives($io);
68+
$this->addTailwindDirectives($io, $builder);
4369

4470
$io->success('Tailwind CSS is ready to use!');
4571

4672
return self::SUCCESS;
4773
}
4874

49-
private function createTailwindConfig(SymfonyStyle $io): bool
75+
private function createTailwindConfig(SymfonyStyle $io, TailwindBuilder $builder): bool
5076
{
51-
if ($this->tailwindBuilder->createBinary()->isV4()) {
77+
if ($builder->createBinary()->isV4()) {
5278
$io->note('Tailwind v4 detected: skipping config file creation.');
5379

5480
return true;
5581
}
5682

57-
$configFile = $this->tailwindBuilder->getConfigFilePath();
83+
$configFile = $builder->getConfigFilePath();
84+
5885
if (file_exists($configFile)) {
5986
$io->note(\sprintf('Tailwind config file already exists in "%s"', $configFile));
6087

6188
return true;
6289
}
6390

64-
$this->tailwindBuilder->setOutput($io);
91+
$builder->setOutput($io);
6592

66-
$process = $this->tailwindBuilder->runInit();
93+
$process = $builder->runInit();
6794
$process->wait(function ($type, $buffer) use ($io) {
6895
$io->write($buffer);
6996
});
@@ -96,9 +123,9 @@ private function createTailwindConfig(SymfonyStyle $io): bool
96123
return true;
97124
}
98125

99-
private function addTailwindDirectives(SymfonyStyle $io): void
126+
private function addTailwindDirectives(SymfonyStyle $io, TailwindBuilder $builder): void
100127
{
101-
$inputFile = $this->tailwindBuilder->getInputCssPaths()[0];
128+
$inputFile = $builder->getInputCssPaths()[0];
102129
$contents = is_file($inputFile) ? file_get_contents($inputFile) : '';
103130
if (str_contains($contents, '@tailwind base') || str_contains($contents, '@import "tailwindcss"')) {
104131
$io->note(\sprintf('Tailwind directives already exist in "%s"', $inputFile));
@@ -113,12 +140,30 @@ private function addTailwindDirectives(SymfonyStyle $io): void
113140
@tailwind utilities;
114141
EOF;
115142

116-
if ($this->tailwindBuilder->createBinary()->isV4()) {
143+
if ($builder->createBinary()->isV4()) {
117144
$tailwindDirectives = <<<EOF
118145
@import "tailwindcss";
119146
EOF;
120147
}
121148

122149
file_put_contents($inputFile, $tailwindDirectives."\n\n".$contents);
123150
}
151+
152+
private function bundleConfigFile(): string
153+
{
154+
return $this->rootDir.'/config/packages/symfonycasts_tailwind.yaml';
155+
}
156+
157+
private function bundleConfig(): array
158+
{
159+
if (!class_exists(Yaml::class)) {
160+
throw new \RuntimeException('You are using a non-standard Symfony setup. You will need to initialize this bundle manually.');
161+
}
162+
163+
if (!file_exists($this->bundleConfigFile())) {
164+
return [];
165+
}
166+
167+
return Yaml::parseFile($this->bundleConfigFile());
168+
}
124169
}

src/DependencyInjection/TailwindExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ public function load(array $configs, ContainerBuilder $container): void
3434
->replaceArgument(5, $config['config_file'])
3535
->replaceArgument(6, $config['postcss_config_file'])
3636
;
37+
$container->findDefinition('tailwind.command.init')
38+
->replaceArgument(1, $config['input_css'])
39+
;
3740
}
3841

3942
public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface

src/TailwindVersionFinder.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the SymfonyCasts TailwindBundle package.
5+
* Copyright (c) SymfonyCasts <https://symfonycasts.com/>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfonycasts\TailwindBundle;
11+
12+
use Symfony\Component\HttpClient\HttpClient;
13+
use Symfony\Contracts\HttpClient\HttpClientInterface;
14+
15+
/**
16+
* Finds the latest Tailwind CSS version by major version.
17+
*
18+
* @author Kevin Bond <[email protected]>
19+
*/
20+
final class TailwindVersionFinder
21+
{
22+
private HttpClientInterface $httpClient;
23+
24+
public function latestVersionFor(int $majorVersion): string
25+
{
26+
foreach ($this->tags() as $tag) {
27+
if (str_starts_with($tag, "v$majorVersion.")) {
28+
return $tag;
29+
}
30+
}
31+
32+
throw new \RuntimeException(sprintf('Could not find a Tailwind CSS %d.x release.', $majorVersion));
33+
}
34+
35+
/**
36+
* @return string[]
37+
*/
38+
private function tags(int $page = 1): iterable
39+
{
40+
$releases = $this->httpClient()
41+
->request('GET', 'https://api.github.com/repos/tailwindlabs/tailwindcss/releases', [
42+
'query' => ['page' => $page],
43+
])
44+
->toArray()
45+
;
46+
47+
if (!$releases) {
48+
return;
49+
}
50+
51+
foreach ($releases as $release) {
52+
yield $release['tag_name'];
53+
}
54+
55+
yield from $this->tags(++$page);
56+
}
57+
58+
private function httpClient(): HttpClientInterface
59+
{
60+
return $this->httpClient ?? HttpClient::create();
61+
}
62+
}

tests/TailwindVersionFinderTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the SymfonyCasts TailwindBundle package.
5+
* Copyright (c) SymfonyCasts <https://symfonycasts.com/>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfonycasts\TailwindBundle\Tests;
11+
12+
use PHPUnit\Framework\TestCase;
13+
use Symfonycasts\TailwindBundle\TailwindVersionFinder;
14+
15+
class TailwindVersionFinderTest extends TestCase
16+
{
17+
/**
18+
* @dataProvider majorVersionProvider
19+
*/
20+
public function testGetLatestVersion(int $majorVersion): void
21+
{
22+
$versionDetector = new TailwindVersionFinder();
23+
$latestVersion = $versionDetector->latestVersionFor($majorVersion);
24+
25+
$this->assertStringStartsWith('v'.$majorVersion.'.', $latestVersion);
26+
}
27+
28+
public static function majorVersionProvider(): array
29+
{
30+
return [
31+
[2],
32+
[3],
33+
[4],
34+
];
35+
}
36+
}

0 commit comments

Comments
 (0)