diff --git a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php index 8ca018f0f13c4..5d011f9edc39a 100644 --- a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php +++ b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php @@ -15,6 +15,7 @@ use Psr\Log\NullLogger; use Symfony\Component\AssetMapper\AssetDependency; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\Exception\CircularAssetsException; use Symfony\Component\AssetMapper\Exception\RuntimeException; use Symfony\Component\AssetMapper\MappedAsset; @@ -57,8 +58,12 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac if (!$dependentAsset) { $message = sprintf('Unable to find asset "%s" imported from "%s".', $matches[1], $asset->sourcePath); - if (null !== $assetMapper->getAsset(sprintf('%s.js', $resolvedPath))) { - $message .= sprintf(' Try adding ".js" to the end of the import - i.e. "%s.js".', $matches[1]); + try { + if (null !== $assetMapper->getAsset(sprintf('%s.js', $resolvedPath))) { + $message .= sprintf(' Try adding ".js" to the end of the import - i.e. "%s.js".', $matches[1]); + } + } catch (CircularAssetsException $e) { + // avoid circular error if there is self-referencing import comments } $this->handleMissingImport($message); diff --git a/src/Symfony/Component/AssetMapper/Exception/CircularAssetsException.php b/src/Symfony/Component/AssetMapper/Exception/CircularAssetsException.php new file mode 100644 index 0000000000000..da412e63123ee --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Exception/CircularAssetsException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper\Exception; + +/** + * Thrown when a circular reference is detected while creating an asset. + */ +class CircularAssetsException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php b/src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php index b6fdb3debaa2d..4c19ab7677d51 100644 --- a/src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php +++ b/src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php @@ -12,6 +12,7 @@ namespace Symfony\Component\AssetMapper\Factory; use Symfony\Component\AssetMapper\AssetMapperCompiler; +use Symfony\Component\AssetMapper\Exception\CircularAssetsException; use Symfony\Component\AssetMapper\Exception\RuntimeException; use Symfony\Component\AssetMapper\MappedAsset; use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; @@ -36,7 +37,7 @@ public function __construct( public function createMappedAsset(string $logicalPath, string $sourcePath): ?MappedAsset { if (\in_array($logicalPath, $this->assetsBeingCreated, true)) { - throw new RuntimeException(sprintf('Circular reference detected while creating asset for "%s": "%s".', $logicalPath, implode(' -> ', $this->assetsBeingCreated).' -> '.$logicalPath)); + throw new CircularAssetsException(sprintf('Circular reference detected while creating asset for "%s": "%s".', $logicalPath, implode(' -> ', $this->assetsBeingCreated).' -> '.$logicalPath)); } if (!isset($this->assetsCache[$logicalPath])) { diff --git a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php index 4c04a70ba78b4..cf290e5ef0c90 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php @@ -16,6 +16,7 @@ use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface; use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler; +use Symfony\Component\AssetMapper\Exception\CircularAssetsException; use Symfony\Component\AssetMapper\Exception\RuntimeException; use Symfony\Component\AssetMapper\MappedAsset; @@ -277,6 +278,31 @@ public static function provideMissingImportModeTests(): iterable ]; } + public function testErrorMessageAvoidsCircularException() + { + $assetMapper = $this->createMock(AssetMapperInterface::class); + $assetMapper->expects($this->any()) + ->method('getAsset') + ->willReturnCallback(function ($logicalPath) { + if ('htmx' === $logicalPath) { + return null; + } + + if ('htmx.js' === $logicalPath) { + throw new CircularAssetsException(); + } + }); + + $asset = new MappedAsset('htmx.js', '/path/to/app.js'); + $compiler = new JavaScriptImportPathCompiler(); + $content = '//** @type {import("./htmx").HtmxApi} */'; + $compiled = $compiler->compile($content, $asset, $assetMapper); + // To form a good exception message, the compiler will check for the + // htmx.js asset, which will throw a CircularAssetsException. This + // should not be caught. + $this->assertSame($content, $compiled); + } + private function createAssetMapper(): AssetMapperInterface { $assetMapper = $this->createMock(AssetMapperInterface::class);