Skip to content

[AssetMapper] Fixing incorrect exception & adding allowing more realistic error mode #50393

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

Merged
merged 1 commit into from
May 23, 2023
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -870,9 +870,10 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $
->info('The public path where the assets will be written to (and served from when "server" is true)')
->defaultValue('/assets/')
->end()
->booleanNode('strict_mode')
->info('If true, an exception will be thrown if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import \'./non-existent.js\'"')
->defaultValue(true)
->enumNode('missing_import_mode')
->values(['strict', 'warn', 'ignore'])
->info('Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import \'./non-existent.js\'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is.')
->defaultValue('warn')
->end()
->arrayNode('extensions')
->info('Key-value pair of file extensions set to their mime type.')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1305,10 +1305,10 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde
}

$container->getDefinition('asset_mapper.compiler.css_asset_url_compiler')
->setArgument(0, $config['strict_mode']);
->setArgument(0, $config['missing_import_mode']);

$container->getDefinition('asset_mapper.compiler.javascript_import_path_compiler')
->setArgument(0, $config['strict_mode']);
->setArgument(0, $config['missing_import_mode']);

$container
->getDefinition('asset_mapper.importmap.manager')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@

->set('asset_mapper.compiler.css_asset_url_compiler', CssAssetUrlCompiler::class)
->args([
abstract_arg('strict mode'),
abstract_arg('missing import mode'),
service('logger'),
])
->tag('asset_mapper.compiler')

Expand All @@ -123,7 +124,8 @@

->set('asset_mapper.compiler.javascript_import_path_compiler', JavaScriptImportPathCompiler::class)
->args([
abstract_arg('strict mode'),
abstract_arg('missing import mode'),
service('logger'),
])
->tag('asset_mapper.compiler')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
<xsd:attribute name="enabled" type="xsd:boolean" />
<xsd:attribute name="server" type="xsd:boolean" />
<xsd:attribute name="public-prefix" type="xsd:string" />
<xsd:attribute name="strict-mode" type="xsd:boolean" />
<xsd:attribute name="missing-import-mode" type="missing-import-mode" />
<xsd:attribute name="importmap-path" type="xsd:string" />
<xsd:attribute name="importmap-polyfill" type="xsd:string" />
<xsd:attribute name="vendor-dir" type="xsd:string" />
Expand All @@ -216,6 +216,14 @@
<xsd:attribute name="key" type="xsd:string" use="required" />
</xsd:complexType>

<xsd:simpleType name="missing-import-mode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="strict" />
<xsd:enumeration value="warn" />
<xsd:enumeration value="ignore" />
</xsd:restriction>
</xsd:simpleType>

<xsd:complexType name="translator">
<xsd:sequence>
<xsd:element name="fallback" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public function testAssetMapperCanBeEnabled()
'excluded_patterns' => [],
'server' => true,
'public_prefix' => '/assets/',
'strict_mode' => true,
'missing_import_mode' => 'warn',
'extensions' => [],
'importmap_path' => '%kernel.project_dir%/importmap.php',
'importmap_polyfill' => null,
Expand Down Expand Up @@ -619,7 +619,7 @@ protected static function getBundleDefaultConfig()
'excluded_patterns' => [],
'server' => true,
'public_prefix' => '/assets/',
'strict_mode' => true,
'missing_import_mode' => 'warn',
'extensions' => [],
'importmap_path' => '%kernel.project_dir%/importmap.php',
'importmap_polyfill' => null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
enabled="true"
server="true"
public-prefix="/assets_path/"
strict-mode="true"
missing-import-mode="strict"
importmap-path="%kernel.project_dir%/importmap.php"
importmap-polyfill="https://cdn.example.com/polyfill.js"
vendor-dir="%kernel.project_dir%/assets/vendor"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,8 @@ public function testAssetMapper()

$definition = $container->getDefinition('asset_mapper.repository');
$this->assertSame(['assets/' => '', 'assets2/' => 'my_namespace'], $definition->getArgument(0));

$definition = $container->getDefinition('asset_mapper.compiler.css_asset_url_compiler');
$this->assertSame('strict', $definition->getArgument(0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
*/
interface AssetCompilerInterface
{
public const MISSING_IMPORT_STRICT = 'strict';
public const MISSING_IMPORT_WARN = 'warn';
public const MISSING_IMPORT_IGNORE = 'ignore';

public function supports(MappedAsset $asset): bool;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Symfony\Component\AssetMapper\Compiler;

use Symfony\Component\Asset\Exception\RuntimeException;
use Symfony\Component\AssetMapper\Exception\RuntimeException;

/**
* Helps resolve "../" and "./" in paths.
Expand Down
33 changes: 27 additions & 6 deletions src/Symfony/Component/AssetMapper/Compiler/CssAssetUrlCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@

namespace Symfony\Component\AssetMapper\Compiler;

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\AssetMapper\AssetDependency;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\Exception\RuntimeException;
use Symfony\Component\AssetMapper\MappedAsset;

/**
Expand All @@ -26,23 +29,32 @@ final class CssAssetUrlCompiler implements AssetCompilerInterface
{
use AssetCompilerPathResolverTrait;

private readonly LoggerInterface $logger;

// https://regex101.com/r/BOJ3vG/1
public const ASSET_URL_PATTERN = '/url\(\s*["\']?(?!(?:\/|\#|%23|data|http|\/\/))([^"\'\s?#)]+)([#?][^"\')]+)?\s*["\']?\)/';

public function __construct(private readonly bool $strictMode = true)
{
public function __construct(
private readonly string $missingImportMode = self::MISSING_IMPORT_WARN,
LoggerInterface $logger = null,
) {
$this->logger = $logger ?? new NullLogger();
}

public function compile(string $content, MappedAsset $asset, AssetMapperInterface $assetMapper): string
{
return preg_replace_callback(self::ASSET_URL_PATTERN, function ($matches) use ($asset, $assetMapper) {
$resolvedPath = $this->resolvePath(\dirname($asset->getLogicalPath()), $matches[1]);
try {
$resolvedPath = $this->resolvePath(\dirname($asset->getLogicalPath()), $matches[1]);
} catch (RuntimeException $e) {
$this->handleMissingImport(sprintf('Error processing import in "%s": "%s"', $asset->getSourcePath(), $e->getMessage()), $e);

return $matches[0];
}
$dependentAsset = $assetMapper->getAsset($resolvedPath);

if (null === $dependentAsset) {
if ($this->strictMode) {
throw new \RuntimeException(sprintf('Unable to find asset "%s" referenced in "%s".', $resolvedPath, $asset->getSourcePath()));
}
$this->handleMissingImport(sprintf('Unable to find asset "%s" referenced in "%s".', $matches[1], $asset->getSourcePath()));

// return original, unchanged path
return $matches[0];
Expand All @@ -59,4 +71,13 @@ public function supports(MappedAsset $asset): bool
{
return 'css' === $asset->getPublicExtension();
}

private function handleMissingImport(string $message, \Throwable $e = null): void
{
match ($this->missingImportMode) {
AssetCompilerInterface::MISSING_IMPORT_IGNORE => null,
AssetCompilerInterface::MISSING_IMPORT_WARN => $this->logger->warning($message),
AssetCompilerInterface::MISSING_IMPORT_STRICT => throw new RuntimeException($message, 0, $e),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@

namespace Symfony\Component\AssetMapper\Compiler;

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\AssetMapper\AssetDependency;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\Exception\RuntimeException;
use Symfony\Component\AssetMapper\MappedAsset;

/**
Expand All @@ -26,31 +29,44 @@ final class JavaScriptImportPathCompiler implements AssetCompilerInterface
{
use AssetCompilerPathResolverTrait;

private readonly LoggerInterface $logger;

// https://regex101.com/r/VFdR4H/1
private const IMPORT_PATTERN = '/(?:import\s+(?:(?:\*\s+as\s+\w+|[\w\s{},*]+)\s+from\s+)?|\bimport\()\s*[\'"`](\.\/[^\'"`]+|(\.\.\/)+[^\'"`]+)[\'"`]\s*[;\)]?/m';

public function __construct(private readonly bool $strictMode = true)
{
public function __construct(
private readonly string $missingImportMode = self::MISSING_IMPORT_WARN,
LoggerInterface $logger = null,
) {
$this->logger = $logger ?? new NullLogger();
}

public function compile(string $content, MappedAsset $asset, AssetMapperInterface $assetMapper): string
{
return preg_replace_callback(self::IMPORT_PATTERN, function ($matches) use ($asset, $assetMapper) {
$resolvedPath = $this->resolvePath(\dirname($asset->getLogicalPath()), $matches[1]);
try {
$resolvedPath = $this->resolvePath(\dirname($asset->getLogicalPath()), $matches[1]);
} catch (RuntimeException $e) {
$this->handleMissingImport(sprintf('Error processing import in "%s": "%s"', $asset->getSourcePath(), $e->getMessage()), $e);

return $matches[0];
}

$dependentAsset = $assetMapper->getAsset($resolvedPath);

if (!$dependentAsset && $this->strictMode) {
$message = sprintf('Unable to find asset "%s" imported from "%s".', $resolvedPath, $asset->getSourcePath());
if (!$dependentAsset) {
$message = sprintf('Unable to find asset "%s" imported from "%s".', $matches[1], $asset->getSourcePath());

if (null !== $assetMapper->getAsset(sprintf('%s.js', $resolvedPath))) {
$message .= sprintf(' Try adding ".js" to the end of the import - i.e. "%s.js".', $resolvedPath);
$message .= sprintf(' Try adding ".js" to the end of the import - i.e. "%s.js".', $matches[1]);
}

throw new \RuntimeException($message);
$this->handleMissingImport($message);

return $matches[0];
}

if ($dependentAsset && $this->supports($dependentAsset)) {
if ($this->supports($dependentAsset)) {
// If we found the path and it's a JavaScript file, list it as a dependency.
// This will cause the asset to be included in the importmap.
$isLazy = str_contains($matches[0], 'import(');
Expand Down Expand Up @@ -80,4 +96,20 @@ private function makeRelativeForJavaScript(string $path): string

return './'.$path;
}

private function handleMissingImport(string $message, \Throwable $e = null): void
{
switch ($this->missingImportMode) {
case AssetCompilerInterface::MISSING_IMPORT_IGNORE:
return;

case AssetCompilerInterface::MISSING_IMPORT_WARN:
$this->logger->warning($message);

return;

case AssetCompilerInterface::MISSING_IMPORT_STRICT:
throw new RuntimeException($message, 0, $e);
}
}
}
16 changes: 16 additions & 0 deletions src/Symfony/Component/AssetMapper/Exception/ExceptionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\AssetMapper\Exception;

interface ExceptionInterface extends \Throwable
{
}
16 changes: 16 additions & 0 deletions src/Symfony/Component/AssetMapper/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\AssetMapper\Exception;

class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
namespace Symfony\Component\AssetMapper\Tests\Compiler;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Asset\Exception\RuntimeException;
use Symfony\Component\AssetMapper\Compiler\AssetCompilerPathResolverTrait;
use Symfony\Component\AssetMapper\Exception\RuntimeException;

class AssetCompilerPathResolverTraitTest extends TestCase
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
namespace Symfony\Component\AssetMapper\Tests\Compiler;

use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\AssetMapper\AssetDependency;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface;
use Symfony\Component\AssetMapper\Compiler\CssAssetUrlCompiler;
use Symfony\Component\AssetMapper\MappedAsset;

Expand All @@ -24,8 +26,9 @@ class CssAssetUrlCompilerTest extends TestCase
*/
public function testCompile(string $sourceLogicalName, string $input, string $expectedOutput, array $expectedDependencies)
{
$compiler = new CssAssetUrlCompiler(false);
$compiler = new CssAssetUrlCompiler(AssetCompilerInterface::MISSING_IMPORT_IGNORE, $this->createMock(LoggerInterface::class));
$asset = new MappedAsset($sourceLogicalName);
$asset->setSourcePath('anything');
$asset->setPublicPathWithoutDigest('/assets/'.$sourceLogicalName);
$this->assertSame($expectedOutput, $compiler->compile($input, $asset, $this->createAssetMapper()));
$assetDependencyLogicalPaths = array_map(fn (AssetDependency $dependency) => $dependency->asset->getLogicalPath(), $asset->getDependencies());
Expand Down Expand Up @@ -117,7 +120,7 @@ public function testStrictMode(string $sourceLogicalName, string $input, ?string
$asset = new MappedAsset($sourceLogicalName);
$asset->setSourcePath('/path/to/styles.css');

$compiler = new CssAssetUrlCompiler(true);
$compiler = new CssAssetUrlCompiler(AssetCompilerInterface::MISSING_IMPORT_STRICT, $this->createMock(LoggerInterface::class));
$this->assertSame($input, $compiler->compile($input, $asset, $this->createAssetMapper()));
}

Expand Down
Loading