diff --git a/composer.json b/composer.json index f115f2e..ae79456 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "symfony/filesystem": "^6.3|^7.0", "symfony/framework-bundle": "^6.3|^7.0", "symfony/phpunit-bridge": "^6.3.9|^7.0", - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^9.6", + "symfony/yaml": "^6.3|^7.0" }, "minimum-stability": "dev", "autoload": { diff --git a/config/services.php b/config/services.php index adc62f2..872c9e4 100644 --- a/config/services.php +++ b/config/services.php @@ -6,6 +6,7 @@ use Symfonycasts\TailwindBundle\Command\TailwindInitCommand; use Symfonycasts\TailwindBundle\TailwindBuilder; +use Symfonycasts\TailwindBundle\TailwindVersionFinder; use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg; use function Symfony\Component\DependencyInjection\Loader\Configurator\param; use function Symfony\Component\DependencyInjection\Loader\Configurator\service; @@ -23,6 +24,8 @@ abstract_arg('path to PostCSS config file'), ]) + ->set('tailwind.version_finder', TailwindVersionFinder::class) + ->set('tailwind.command.build', TailwindBuildCommand::class) ->args([ service('tailwind.builder'), @@ -31,7 +34,9 @@ ->set('tailwind.command.init', TailwindInitCommand::class) ->args([ - service('tailwind.builder'), + service('tailwind.version_finder'), + abstract_arg('path to source Tailwind CSS file'), + param('kernel.project_dir'), ]) ->tag('console.command') diff --git a/src/Command/TailwindInitCommand.php b/src/Command/TailwindInitCommand.php index 7a7e991..ff837ba 100644 --- a/src/Command/TailwindInitCommand.php +++ b/src/Command/TailwindInitCommand.php @@ -14,7 +14,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Yaml\Yaml; use Symfonycasts\TailwindBundle\TailwindBuilder; +use Symfonycasts\TailwindBundle\TailwindVersionFinder; #[AsCommand( name: 'tailwind:init', @@ -23,47 +25,72 @@ class TailwindInitCommand extends Command { public function __construct( - private TailwindBuilder $tailwindBuilder, + private TailwindVersionFinder $versionFinder, + private array $inputCss, + private string $rootDir, ) { parent::__construct(); } - protected function configure(): void - { - } - protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - if (!$this->createTailwindConfig($io)) { + + if (!$input->isInteractive()) { + throw new \RuntimeException('tailwind:init command must be run interactively.'); + } + + $bundleConfig = $this->bundleConfig(); + + if ($io->confirm('Are you managing your own Taildind CSS binary?', false)) { + $binaryPath = $io->ask('Enter the path to your Tailwind CSS binary:', 'node_modules/.bin/tailwindcss'); + $bundleConfig['symfonycasts_tailwind']['binary'] = $binaryPath; + } else { + $majorVersion = $io->ask('Which major version do you wish to use?', '4'); + $latestVersion = $this->versionFinder->latestVersionFor($majorVersion); + $bundleConfig['symfonycasts_tailwind']['binary_version'] = $latestVersion; + } + + file_put_contents($this->bundleConfigFile(), Yaml::dump($bundleConfig)); + + $builder = new TailwindBuilder( + $this->rootDir, + $this->inputCss, + $this->rootDir.'/var/tailwind', + binaryPath: $bundleConfig['symfonycasts_tailwind']['binary'] ?? null, + binaryVersion: $bundleConfig['symfonycasts_tailwind']['binary_version'] ?? null, + ); + + if (!$this->createTailwindConfig($io, $builder)) { return self::FAILURE; } - $this->addTailwindDirectives($io); + $this->addTailwindDirectives($io, $builder); $io->success('Tailwind CSS is ready to use!'); return self::SUCCESS; } - private function createTailwindConfig(SymfonyStyle $io): bool + private function createTailwindConfig(SymfonyStyle $io, TailwindBuilder $builder): bool { - if ($this->tailwindBuilder->createBinary()->isV4()) { + if ($builder->createBinary()->isV4()) { $io->note('Tailwind v4 detected: skipping config file creation.'); return true; } - $configFile = $this->tailwindBuilder->getConfigFilePath(); + $configFile = $builder->getConfigFilePath(); + if (file_exists($configFile)) { $io->note(\sprintf('Tailwind config file already exists in "%s"', $configFile)); return true; } - $this->tailwindBuilder->setOutput($io); + $builder->setOutput($io); - $process = $this->tailwindBuilder->runInit(); + $process = $builder->runInit(); $process->wait(function ($type, $buffer) use ($io) { $io->write($buffer); }); @@ -96,9 +123,9 @@ private function createTailwindConfig(SymfonyStyle $io): bool return true; } - private function addTailwindDirectives(SymfonyStyle $io): void + private function addTailwindDirectives(SymfonyStyle $io, TailwindBuilder $builder): void { - $inputFile = $this->tailwindBuilder->getInputCssPaths()[0]; + $inputFile = $builder->getInputCssPaths()[0]; $contents = is_file($inputFile) ? file_get_contents($inputFile) : ''; if (str_contains($contents, '@tailwind base') || str_contains($contents, '@import "tailwindcss"')) { $io->note(\sprintf('Tailwind directives already exist in "%s"', $inputFile)); @@ -113,7 +140,7 @@ private function addTailwindDirectives(SymfonyStyle $io): void @tailwind utilities; EOF; - if ($this->tailwindBuilder->createBinary()->isV4()) { + if ($builder->createBinary()->isV4()) { $tailwindDirectives = <<rootDir.'/config/packages/symfonycasts_tailwind.yaml'; + } + + private function bundleConfig(): array + { + if (!class_exists(Yaml::class)) { + throw new \RuntimeException('You are using a non-standard Symfony setup. You will need to initialize this bundle manually.'); + } + + if (!file_exists($this->bundleConfigFile())) { + return []; + } + + return Yaml::parseFile($this->bundleConfigFile()); + } } diff --git a/src/DependencyInjection/TailwindExtension.php b/src/DependencyInjection/TailwindExtension.php index 1542eec..ee013b2 100644 --- a/src/DependencyInjection/TailwindExtension.php +++ b/src/DependencyInjection/TailwindExtension.php @@ -34,6 +34,9 @@ public function load(array $configs, ContainerBuilder $container): void ->replaceArgument(5, $config['config_file']) ->replaceArgument(6, $config['postcss_config_file']) ; + $container->findDefinition('tailwind.command.init') + ->replaceArgument(1, $config['input_css']) + ; } public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface diff --git a/src/TailwindVersionFinder.php b/src/TailwindVersionFinder.php new file mode 100644 index 0000000..f41fd35 --- /dev/null +++ b/src/TailwindVersionFinder.php @@ -0,0 +1,62 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfonycasts\TailwindBundle; + +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * Finds the latest Tailwind CSS version by major version. + * + * @author Kevin Bond + */ +final class TailwindVersionFinder +{ + private HttpClientInterface $httpClient; + + public function latestVersionFor(int $majorVersion): string + { + foreach ($this->tags() as $tag) { + if (str_starts_with($tag, "v$majorVersion.")) { + return $tag; + } + } + + throw new \RuntimeException(\sprintf('Could not find a Tailwind CSS %d.x release.', $majorVersion)); + } + + /** + * @return string[] + */ + private function tags(int $page = 1): iterable + { + $releases = $this->httpClient() + ->request('GET', 'https://api.github.com/repos/tailwindlabs/tailwindcss/releases', [ + 'query' => ['page' => $page], + ]) + ->toArray() + ; + + if (!$releases) { + return; + } + + foreach ($releases as $release) { + yield $release['tag_name']; + } + + yield from $this->tags(++$page); + } + + private function httpClient(): HttpClientInterface + { + return $this->httpClient ??= HttpClient::create(); + } +} diff --git a/tests/TailwindVersionFinderTest.php b/tests/TailwindVersionFinderTest.php new file mode 100644 index 0000000..1fa0caa --- /dev/null +++ b/tests/TailwindVersionFinderTest.php @@ -0,0 +1,36 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfonycasts\TailwindBundle\Tests; + +use PHPUnit\Framework\TestCase; +use Symfonycasts\TailwindBundle\TailwindVersionFinder; + +class TailwindVersionFinderTest extends TestCase +{ + /** + * @dataProvider majorVersionProvider + */ + public function testGetLatestVersion(int $majorVersion): void + { + $versionDetector = new TailwindVersionFinder(); + $latestVersion = $versionDetector->latestVersionFor($majorVersion); + + $this->assertStringStartsWith('v'.$majorVersion.'.', $latestVersion); + } + + public static function majorVersionProvider(): array + { + return [ + [2], + [3], + [4], + ]; + } +}