Open lukepass opened 8 months ago
Hello,
I managed to make them work together with this trick:
<?php
namespace App\AssetMapper;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface;
use Symfony\Component\AssetMapper\MappedAsset;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
#[AsDecorator('sass.css_asset_compiler')]
readonly class CssCompiler implements AssetCompilerInterface
{
public function __construct(
private AssetCompilerInterface $sassCompiler,
#[Autowire('@tailwind.css_asset_compiler')]
private AssetCompilerInterface $tailwindCompiler
) {
}
public function supports(MappedAsset $asset): bool
{
return $this->sassCompiler->supports($asset);
}
public function compile(string $content, MappedAsset $asset, AssetMapperInterface $assetMapper): string
{
$content = $this->sassCompiler->compile($content, $asset, $assetMapper);
return $this->tailwindCompiler->compile($content, $asset, $assetMapper);
}
}
And in config:
symfonycasts_tailwind:
input_css: '%kernel.project_dir%/var/sass/app.output.css'
Entrypoint:
// assets/app.js
import './styles/app.scss';
Not sure it's the best way but it worked for me ^^
That's quite clever but unfortunately it didn't work. The compiled SASS still includes @tailwind directives and they didn't get compiled.
I had to remove the @
from the service name because it was throwing an error.
Do you know why it's not working? I tried adding an exception / dump in the decorator but it didn't get called.
I must correct myself. It "kinda" works but only when I run the command:
php bin/console asset-map:compile
And the first time it prints this error:
Built Tailwind CSS file does not exist: run "php bin/console tailwind:build" to
generate it
I had to manually run tailwind:build
. In development it still doesn't work.
Thanks!
You have to run the sass build and then the tailwind build. In development both can be run with the --watch
option in parallel so any change to app.scss
will trigger rebuild from tailwind. I made a little demo here ;)
Thanks, that worked; I can't understand the exact pipeline order but I can have SASS along with Tailwind. It would be nice to have this class already in the bundle!
Hi everyone ! I have the same issue. I have a repo with AssetMapper and SassBundle installed. I need to install the TailwindBundle, and I tried the @squrious's solution (from what I see, importants files are : the config in asset_mapper.yaml, the CssCompiler.php, and app.scss where you import tailwind rules).
Tailwind is loading correctly but my personal scss isn't recognize. I searched through the folder /var/tailwind and found that the css is build in the file app.output.built.css but Tailwind actually use another file : app.built.css (if I copy the content of the first one in the second one, it works).
How can I freely use TailwindBundle and SassBundle, am I missing something ?
#/assets/styles/app.scss
@tailwind base;
@tailwind components;
@tailwind utilities;
@import './variables';
body {
background-color: $primary;
}
#/config/packages/asset_mapper.yaml
symfonycasts_tailwind:
input_css: "%kernel.project_dir%/var/sass/app.output.css"
#/assets/app.js
import './bootstrap.js';
import './styles/app.scss';
import Duck from './modules/duck.js';
const duck = new Duck('Waddles');
duck.quack();
So I've spent a while trying to prep a Builder class that peformed both SASS & Tailwind build, inspired by @squrious 's code, but after a LOT of debugging I've come to discover that you don't need any of this. You just need to ensure that the SassBundle is listed before the TailwindBundle in the bundles.php
file, then set the input file for both bundles to the file you want to compile, and then Symfony AssetMapper handler the rest…
# bundles.config/php
<?php
return [
// …
// Other bundle orders do not matter, just `SymfonycastsSassBundle`
// must be before `SymfonycastsTailwindBundle`…
Symfonycasts\SassBundle\SymfonycastsSassBundle::class => ['all' => true],
Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle::class => ['all' => true],
// …
];
# config/packages/symfonycasts_sass.yaml
symfonycasts_sass:
root_sass: 'assets/styles/app.scss'
# config/packages/symfonycasts_tailwind:.yaml
symfonycasts_tailwind:
input_css: 'assets/styles/app.scss'
Then all you need to do is start watcher for both…
$ bin/console sass:build --watch
$ bin/console tailwind:build --watch
And for a Brucey Bonus, if you use Docker you can run these in a worker service like this…
# compose.override.yaml
services:
php:
# …
volumes:
# …
- sass_var:/app/var/sass # <== Asset mapped needs to be able to see the compiled CSS file in dev as it checks for the existance of the output file
- tailwind_var:/app/var/tailwind # <== This is where the `tailwind` compiled CSS file is written
depends_on:
# …
- tailwind
sass:
# …
volumes:
# …
- sass_var:/app/var/sass # <== This is where the compiled CSS files is written
command: ['bin/console', 'sass:build', '--watch' ]
tailwind:
# …
volumes:
# …
- sass_var:/app/var/sass # <== This is where the `sass` service outputs it's compiled CSS file
- tailwind_var:/app/var/tailwind # <== This is where the compiled css file is written. This is shared with the `php` services
depends_on:
# …
- sass
command: ['bin/console', 'tailwind:build', '--watch' ]
So it turns out my above solution doesn't work. This is because the builder and the compiler both use the symfonycasts_tailwind.input_css
config, but the builder needs to point to the SASS output file, and the compiler will only compile if the symfonycasts_tailwind.input_css
config value matches the original asset filename.
There were a coule of ways to 'fix' this…
TailwindSassAssetCompiler
that has a modifier support()
to match the original assert path;TailwindBuilder::$inputPath
argument to use the SassBuilder's output path (from SassBuilder::guessCssNameFromSassFile()
);I opted for the later, using a compiler pass that will only change the TailwindBuilder::$inputPath
arg if the SassBuilder service exists, and the input path for Tailwind matches one of the Sass files configured…
<?php
namespace App;
use App\DependencyInjection\CompilerPass\TailwindSassBuildCommandPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new TailwindSassBuildCommandPass());
}
}
<?php
declare(strict_types=1);
namespace App\DependencyInjection\CompilerPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfonycasts\SassBundle\SassBuilder;
use function in_array;
/**
* If we're Tailwind compiling a SASS output file, then the TailwindCssAssetCompiler needs to be configured with the
* original .scss file, but the builder needs to be configured with the .css file compiled by the SassBuilder, so we
* need to tweak the input passed to the builder.
*
* We do this by checking that the Sas builder is available and that the input for the tailwind builder is the same as
* one of the SASS input files. If both these facts are true, we modify the input file passed to the TailwindBuilder,
* so the assets compiled at runtime in dev work, and the `tailwind:build --watch` command also works for SASS files.
*/
final readonly class TailwindSassBuildCommandPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('sass.builder')) {
return;
}
$sassBuilderDef = $container->findDefinition('sass.builder');
$sassPaths = $sassBuilderDef->getArgument(0);
$cssPath = $sassBuilderDef->getArgument(1);
$tailWindBuilderDef = $container->findDefinition('tailwind.builder');
$tailwindInputFile = $tailWindBuilderDef->getArgument(1);
if (!in_array($tailwindInputFile, $sassPaths, true)) {
return;
}
$tailWindBuilderDef->setArgument(1, SassBuilder::guessCssNameFromSassFile($tailwindInputFile, $cssPath));
}
}
And the same config as above…
# config/packages/symfonycasts_sass.yaml
symfonycasts_sass:
root_sass: 'assets/styles/app.scss'
# config/packages/symfonycasts_tailwind:.yaml
symfonycasts_tailwind:
input_css: 'assets/styles/app.scss'
Ignore the above. I was working late. I now have a nice tidy solution that I'll create a Bundle for, but here's the idea…
Same source file for both Sass & Tailwind bundles (since we want both to process this file)…
# config/packages/symfonycasts_sass.yaml
symfonycasts_sass:
root_sass: 'assets/styles/app.scss'
# config/packages/symfonycasts_tailwind:.yaml
symfonycasts_tailwind:
input_css: 'assets/styles/app.scss'
Then we extend the TailwindBuilder with a Sass aware version. Note the $sassBuilder is 'optional' since it has to be added to the end of the list of args because the existing compiler passes for the Tailwind Bundle reference the arguments by number, not by name, so we can't screw them up.
<?php
declare(strict_types=1);
namespace CubicMushroom\TailwindSassBundle;
use Symfony\Contracts\Cache\CacheInterface;
use Symfonycasts\SassBundle\SassBuilder;
use Symfonycasts\TailwindBundle\TailwindBuilder;
use function explode;
use function realpath;
final class TailwindSassBuilder extends TailwindBuilder
{
public function __construct(
string $projectRootDir,
string $inputPath,
string $tailwindVarDir,
CacheInterface $cache,
?string $binaryPath = null,
?string $binaryVersion = null,
string $configPath = 'tailwind.config.js',
?SassBuilder $sassBuilder = null,
) {
if ($sassBuilder) {
foreach ($sassBuilder->getScssCssTargets() as $scssCssTarget) {
[$sassFile, $cssFile] = explode(':', $scssCssTarget);
if (realpath($sassFile) === realpath("$projectRootDir/$inputPath")) {
$inputPath = $cssFile;
break;
}
}
}
parent::__construct(
$projectRootDir,
$inputPath,
$tailwindVarDir,
$cache,
$binaryPath,
$binaryVersion,
$configPath,
);
}
}
And you need a compiler pass to override the sass.builder
service class and inject the SassBuilder instance…
The only problem with this is that calling SassBuilder::getScssCssTargets()
in the context of a web request, rather than a console process is that the working directory is not the same, so we need to make sure the file paths are absolute. You can do this in your config files, but why enforce that when you can add a compiler pass to udpate the configs…
<?php
declare(strict_types=1);
namespace CubicMushroom\TailwindSassBundle\DependencyInjection\CompilerPass;
use LogicException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Filesystem\Path;
/**
* This pass ensures that the SASS file paths are absolute.
*
* Since we want to call `Symfonycasts\SassBundle\SassBuilder::getScssCssTargets()` within the web context, in
* CubicMushroom\TailwindSassBundle\AssetMapper\TailwindSassAssetCompiler we need to ensure the SASS file paths are
* absolute, since the working directory is /app in the console and /app/public for web requests.
*
* IMPORTANT: This needs to be run during the 'optimize' pass, so that CSS file route parameters are resolved.
*/
final readonly class AbsoluteSassPathsPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('sass.builder')) {
throw new LogicException('The TailwindSassBundle requires the SymfonyCastsSassBundle to be installed.');
}
$sassBuilderDef = $container->findDefinition('sass.builder');
$sassPaths = $sassBuilderDef->getArgument(0);
$projectRootDir = $sassBuilderDef->getArgument(2);
$sassPaths = array_map(
fn(string $sassPath): string => Path::isAbsolute($sassPath)
? $sassPath
: Path::makeAbsolute($sassPath, $projectRootDir),
$sassPaths,
);
$sassBuilderDef->replaceArgument(0, $sassPaths);;
}
}
And to add your compiler passes just register them in your Kernel in the usual way, but not the need to add the AbsoluteSassPathsPass
one in the 'optimise' phase.
Since we now have a difference asset source to the TailwindBuilder input path, we just need to add a custom AssetCompilerInterface
instance…
<?php
declare(strict_types=1);
namespace CubicMushroom\TailwindSassBundle\AssetMapper;
use Exception;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface;
use Symfony\Component\AssetMapper\MappedAsset;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfonycasts\SassBundle\SassBuilder;
use Symfonycasts\TailwindBundle\TailwindBuilder;
/**
* Custom compiler that allows the Tailwind compile file to be returned if the asset source is a SASS file that the
* SassBuilder has mapped to the TailwindBuilder's input CSS file.
*/
class TailwindSassAssetCompiler implements AssetCompilerInterface
{
/**
* @var array<string, string>|null
*/
private ?array $sassFilesMap;
public function __construct(
#[Autowire('@tailwind.builder')]
private TailwindBuilder $tailwindBuilder,
#[Autowire('@sass.builder')]
private SassBuilder $sassBuilder,
) {
}
/**
* @throws Exception
*/
public function supports(MappedAsset $asset): bool
{
$sassFilesMap = $this->getSassFilesMap();
return ($sassFilesMap[$asset->sourcePath] ?? null) === $this->tailwindBuilder->getInputCssPath();
}
public function compile(string $content, MappedAsset $asset, AssetMapperInterface $assetMapper): string
{
$asset->addFileDependency($this->tailwindBuilder->getInternalOutputCssPath());
return $this->tailwindBuilder->getOutputCssContent();
}
/**
* @throws Exception
*/
private function getSassFilesMap(): array
{
if (!isset($this->sassFilesMap)) {
$this->sassFilesMap = [];
foreach ($this->sassBuilder->getScssCssTargets() as $scssCssTarget) {
[$sassFile, $cssFile] = explode(':', $scssCssTarget);
$this->sassFilesMap[$sassFile] = $cssFile;
}
}
return $this->sassFilesMap;
}
}
And finally, the AssetCompilerInterface
instances don't get auto-tagged, so you need to add the asset_mapper.compiler
tag when defining this in your services.{yaml,xml,php}
file…
<?php
declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use CubicMushroom\TailwindSassBundle\AssetMapper\TailwindSassAssetCompiler;
return function (ContainerConfigurator $container): void {
// default configuration for services in *this* file
$services = $container->services()
->defaults()
->autowire() // Automatically injects dependencies in your services.
->autoconfigure() // Automatically registers your services as commands, event subscribers, etc.
;
$services->set(TailwindSassAssetCompiler::class)
->tag(
'asset_mapper.compiler',
[
'priority' => 10,
],
);
};
Is there a chance that the previous version will be included in the base bundle?
Also please not that the version I was using in the first posts of this thread (the one using CssCompiler) doesn't work anymore in the latest version of this bundle.
Hello, I posted a related comment in another thread of the SassBundle:
https://github.com/SymfonyCasts/sass-bundle/issues/4
Since both bundles are involved, I will repost it here, thanks:
Hello, so as of today it's not possible to use SassBundle along with TailwindBundle correct? That's because they compete for the same file to build?
I also tried giving tailwind the SASS path:
But then I get this error:
That's because the tailwind parser doesn't know how to process the SASS file. I also tried giving tailwind the SASS output file:
But it does nothing.
How can I do to use Tailwind along with SASS and AssetMapper? Is it a bad practice?
Thanks.