Skip to content

feat: interactive tailwind:init command #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
7 changes: 6 additions & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'),
Expand All @@ -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')

Expand Down
75 changes: 60 additions & 15 deletions src/Command/TailwindInitCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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);
});
Expand Down Expand Up @@ -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));
Expand All @@ -113,12 +140,30 @@ private function addTailwindDirectives(SymfonyStyle $io): void
@tailwind utilities;
EOF;

if ($this->tailwindBuilder->createBinary()->isV4()) {
if ($builder->createBinary()->isV4()) {
$tailwindDirectives = <<<EOF
@import "tailwindcss";
EOF;
}

file_put_contents($inputFile, $tailwindDirectives."\n\n".$contents);
}

private function bundleConfigFile(): string
{
return $this->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());
}
}
3 changes: 3 additions & 0 deletions src/DependencyInjection/TailwindExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions src/TailwindVersionFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/*
* This file is part of the SymfonyCasts TailwindBundle package.
* Copyright (c) SymfonyCasts <https://symfonycasts.com/>
* 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 <[email protected]>
*/
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();
}
}
36 changes: 36 additions & 0 deletions tests/TailwindVersionFinderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the SymfonyCasts TailwindBundle package.
* Copyright (c) SymfonyCasts <https://symfonycasts.com/>
* 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],
];
}
}