diff --git a/config/services.php b/config/services.php index 755843a..e351c9b 100644 --- a/config/services.php +++ b/config/services.php @@ -19,6 +19,7 @@ abstract_arg('path to css directory'), param('kernel.project_dir'), abstract_arg('path to binary'), + abstract_arg('binary version'), abstract_arg('sass options'), ]) diff --git a/doc/index.rst b/doc/index.rst index b353c64..758a4fa 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -227,6 +227,15 @@ You can configure most of the `Dart Sass CLI options replaceArgument(0, $config['root_sass']) ->replaceArgument(1, '%kernel.project_dir%/var/sass') ->replaceArgument(3, $config['binary']) - ->replaceArgument(4, $config['sass_options']) + ->replaceArgument(4, $config['binary_version']) + ->replaceArgument(5, $config['sass_options']) ; $container->findDefinition('sass.css_asset_compiler') @@ -87,6 +88,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('The Sass binary to use') ->defaultNull() ->end() + ->scalarNode('binary_version') + ->info('The Sass binary version to download - null means the latest version') + ->defaultNull() + ->end() ->arrayNode('sass_options') ->addDefaultsIfNotSet() ->children() diff --git a/src/SassBinary.php b/src/SassBinary.php index a21edd8..ec425a2 100644 --- a/src/SassBinary.php +++ b/src/SassBinary.php @@ -16,12 +16,13 @@ class SassBinary { - private const VERSION = '1.69.7'; private HttpClientInterface $httpClient; + private ?string $cachedVersion = null; public function __construct( private string $binaryDownloadDir, private ?string $binaryPath = null, + private ?string $binaryVersion = null, private ?SymfonyStyle $output = null, HttpClientInterface $httpClient = null ) { @@ -34,7 +35,7 @@ public function __construct( public function createProcess(array $args): Process { if (null === $this->binaryPath) { - $binary = $this->getDefaultBinaryPath(); + $binary = $this->getDefaultBinaryPath($this->getVersion()); if (!is_file($binary)) { $this->downloadExecutable(); } @@ -49,7 +50,7 @@ public function createProcess(array $args): Process public function downloadExecutable(): void { - $url = sprintf('https://github.com/sass/dart-sass/releases/download/%s/%s', self::VERSION, $this->getBinaryName()); + $url = sprintf('https://github.com/sass/dart-sass/releases/download/%s/%s', $this->getVersion(), $this->getBinaryName()); $isZip = str_ends_with($url, '.zip'); $this->output?->note('Downloading Sass binary from '.$url); @@ -75,6 +76,13 @@ public function downloadExecutable(): void }, ]); + if (404 === $response->getStatusCode()) { + if ($this->getLatestVersion() !== $this->getVersion()) { + throw new \Exception(sprintf('Cannot download Sass binary. Please verify version `%s` exists for your machine.', $this->getVersion())); + } + throw new \Exception(sprintf('Cannot download Sass binary. Response code: %d', $response->getStatusCode())); + } + $fileHandler = fopen($targetPath, 'w'); foreach ($this->httpClient->stream($response) as $chunk) { fwrite($fileHandler, $chunk->getContent()); @@ -86,11 +94,11 @@ public function downloadExecutable(): void if ($isZip) { if (!\extension_loaded('zip')) { - throw new \Exception('Cannot unzip the downloaded sass binary. Please install the "zip" PHP extension.'); + throw new \Exception('Cannot unzip the downloaded Sass binary. Please install the "zip" PHP extension.'); } $archive = new \ZipArchive(); $archive->open($targetPath); - $archive->extractTo($this->binaryDownloadDir); + $archive->extractTo($this->binaryDownloadDir.'/dart-sass'); $archive->close(); unlink($targetPath); @@ -98,7 +106,7 @@ public function downloadExecutable(): void } else { $archive = new \PharData($targetPath); $archive->decompress(); - $archive->extractTo($this->binaryDownloadDir); + $archive->extractTo($this->binaryDownloadDir.'/dart-sass'); // delete the .tar (the .tar.gz is deleted below) unlink(substr($targetPath, 0, -3)); @@ -106,7 +114,10 @@ public function downloadExecutable(): void unlink($targetPath); - $binaryPath = $this->getDefaultBinaryPath(); + // Rename the extracted directory to its version + rename($this->binaryDownloadDir.'/dart-sass/dart-sass', $this->binaryDownloadDir.'/dart-sass/'.$this->getVersion()); + + $binaryPath = $this->getDefaultBinaryPath($this->getVersion()); if (!is_file($binaryPath)) { throw new \Exception(sprintf('Could not find downloaded binary in "%s".', $binaryPath)); } @@ -156,11 +167,27 @@ public function getBinaryName(): string private function buildBinaryFileName(string $os, bool $isWindows = false): string { - return 'dart-sass-'.self::VERSION.'-'.$os.($isWindows ? '.zip' : '.tar.gz'); + return 'dart-sass-'.$this->getVersion().'-'.$os.($isWindows ? '.zip' : '.tar.gz'); + } + + private function getDefaultBinaryPath(string $version): string + { + return $this->binaryDownloadDir.'/dart-sass/'.$version.'/sass'; + } + + private function getVersion(): string + { + return $this->cachedVersion ??= $this->binaryVersion ?? $this->getLatestVersion(); } - private function getDefaultBinaryPath(): string + private function getLatestVersion(): string { - return $this->binaryDownloadDir.'/dart-sass/sass'; + try { + $response = $this->httpClient->request('GET', 'https://api.github.com/repos/sass/dart-sass/releases/latest'); + + return $response->toArray()['tag_name'] ?? throw new \Exception('Cannot get the latest version name from response JSON.'); + } catch (\Throwable $e) { + throw new \Exception('Cannot determine latest Dart Sass CLI binary version. Please specify a version in the configuration.', previous: $e); + } } } diff --git a/src/SassBuilder.php b/src/SassBuilder.php index f267d9d..ad2c7ef 100644 --- a/src/SassBuilder.php +++ b/src/SassBuilder.php @@ -53,6 +53,7 @@ public function __construct( private readonly string $cssPath, private readonly string $projectRootDir, private readonly ?string $binaryPath, + private readonly ?string $binaryVersion, bool|array $sassOptions = [], ) { if (\is_bool($sassOptions)) { @@ -174,7 +175,7 @@ public function setOutput(SymfonyStyle $output): void private function createBinary(): SassBinary { - return new SassBinary($this->projectRootDir.'/var', $this->binaryPath, $this->output); + return new SassBinary($this->projectRootDir.'/var', $this->binaryPath, $this->binaryVersion, $this->output); } /** diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 8f17645..97f45d5 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -13,6 +13,8 @@ use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\MappedAsset; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; +use Symfonycasts\SassBundle\SassBinary; class FunctionalTest extends KernelTestCase { @@ -34,6 +36,10 @@ protected function setUp(): void protected function tearDown(): void { unlink(__DIR__.'/fixtures/assets/app.css'); + if (file_exists(__DIR__.'/fixtures/var')) { + $filesystem = new Filesystem(); + $filesystem->remove(__DIR__.'/fixtures/var'); + } } public function testBuildCssIfUsed(): void @@ -53,4 +59,17 @@ public function testBuildCssIfUsed(): void $this->assertStringContainsString('color: red', $asset->content); } } + + public function testVersionDownloaded(): void + { + $testedVersion = '1.69.5'; // This should differ from the latest version which downloaded by default + $binary = new SassBinary(binaryDownloadDir: __DIR__.'/fixtures/var/version', binaryVersion: $testedVersion); + + $binary->downloadExecutable(); + $this->assertDirectoryExists(__DIR__.'/fixtures/var/version/dart-sass/1.69.5'); + + $sassVersionProcess = new Process([__DIR__.'/fixtures/var/version/dart-sass/1.69.5/sass', '--version']); + $sassVersionProcess->run(); + $this->assertSame(trim($sassVersionProcess->getOutput(), \PHP_EOL), $testedVersion); + } } diff --git a/tests/SassBuilderTest.php b/tests/SassBuilderTest.php index 097c3c6..eb558e5 100644 --- a/tests/SassBuilderTest.php +++ b/tests/SassBuilderTest.php @@ -38,6 +38,7 @@ public function testIntegration(): void __DIR__.'/fixtures/assets', __DIR__.'/fixtures', null, + null ); $process = $builder->runBuild(false); @@ -55,6 +56,7 @@ public function testSassDefaultOptions(): void __DIR__.'/fixtures/assets', __DIR__.'/fixtures', null, + null ); $process = $builder->runBuild(false); @@ -78,6 +80,7 @@ public function testEmbedSources(): void __DIR__.'/fixtures/assets', __DIR__.'/fixtures', null, + null, [ 'embed_sources' => true, 'embed_source_map' => true, @@ -104,6 +107,7 @@ public function testSassOptions(): void __DIR__.'/fixtures/assets', __DIR__.'/fixtures', null, + null, [ 'style' => 'compressed', 'source_map' => false, @@ -133,6 +137,7 @@ public function testSassBuilderConvertPhpOptions(array $phpOptions, array $expec __DIR__.'/fixtures/assets', __DIR__.'/fixtures', null, + null, $phpOptions, ); @@ -175,6 +180,7 @@ private function createBuilder(array|string $sassFiles, array $options = []): Sa __DIR__.'/fixtures/assets/dist', __DIR__.'/fixtures', null, + null, $options, ); }