diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php index 5d471ea623258..807cb77fc3a8d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php @@ -17,6 +17,7 @@ use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\AssetMapperRepository; use Symfony\Component\AssetMapper\Command\AssetMapperCompileCommand; +use Symfony\Component\AssetMapper\Command\DebugAssetMapperCommand; use Symfony\Component\AssetMapper\Command\ImportMapExportCommand; use Symfony\Component\AssetMapper\Command\ImportMapRemoveCommand; use Symfony\Component\AssetMapper\Command\ImportMapRequireCommand; @@ -70,6 +71,14 @@ ]) ->tag('console.command') + ->set('asset_mapper.command.debug', DebugAssetMapperCommand::class) + ->args([ + service('asset_mapper'), + service('asset_mapper.repository'), + param('kernel.project_dir'), + ]) + ->tag('console.command') + ->set('asset_mapper_compiler', AssetMapperCompiler::class) ->args([ tagged_iterator('asset_mapper.compiler'), diff --git a/src/Symfony/Component/AssetMapper/AssetMapperRepository.php b/src/Symfony/Component/AssetMapper/AssetMapperRepository.php index 87c165ccf23e3..b7440a9263844 100644 --- a/src/Symfony/Component/AssetMapper/AssetMapperRepository.php +++ b/src/Symfony/Component/AssetMapper/AssetMapperRepository.php @@ -55,7 +55,7 @@ public function find(string $logicalPath): ?string $file = rtrim($path, '/').'/'.$localLogicalPath; if (file_exists($file)) { - return $file; + return realpath($file); } } @@ -64,17 +64,24 @@ public function find(string $logicalPath): ?string public function findLogicalPath(string $filesystemPath): ?string { + if (!is_file($filesystemPath)) { + return null; + } + + $filesystemPath = realpath($filesystemPath); + foreach ($this->getDirectories() as $path => $namespace) { if (!str_starts_with($filesystemPath, $path)) { continue; } $logicalPath = substr($filesystemPath, \strlen($path)); + if ('' !== $namespace) { - $logicalPath = $namespace.'/'.$logicalPath; + $logicalPath = $namespace.'/'.ltrim($logicalPath, '/\\'); } - return ltrim($logicalPath, '/'); + return $this->normalizeLogicalPath($logicalPath); } return null; @@ -100,6 +107,7 @@ public function all(): array /** @var RecursiveDirectoryIterator $innerIterator */ $innerIterator = $iterator->getInnerIterator(); $logicalPath = ($namespace ? rtrim($namespace, '/').'/' : '').$innerIterator->getSubPathName(); + $logicalPath = $this->normalizeLogicalPath($logicalPath); $paths[$logicalPath] = $file->getPathname(); } } @@ -107,6 +115,14 @@ public function all(): array return $paths; } + /** + * @internal + */ + public function allDirectories(): array + { + return $this->getDirectories(); + } + private function getDirectories(): array { $filesystem = new Filesystem(); @@ -120,13 +136,13 @@ private function getDirectories(): array if (!file_exists($path)) { throw new \InvalidArgumentException(sprintf('The asset mapper directory "%s" does not exist.', $path)); } - $this->absolutePaths[$path] = $namespace; + $this->absolutePaths[realpath($path)] = $namespace; continue; } if (file_exists($this->projectRootDir.'/'.$path)) { - $this->absolutePaths[$this->projectRootDir.'/'.$path] = $namespace; + $this->absolutePaths[realpath($this->projectRootDir.'/'.$path)] = $namespace; continue; } @@ -136,4 +152,12 @@ private function getDirectories(): array return $this->absolutePaths; } + + /** + * Normalize slashes to / for logical paths. + */ + private function normalizeLogicalPath(string $logicalPath): string + { + return ltrim(str_replace('\\', '/', $logicalPath), '/\\'); + } } diff --git a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php index 6c08da1c7d68b..b6ed6c8dc9c65 100644 --- a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php +++ b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php @@ -31,7 +31,7 @@ * * @author Ryan Weaver */ -#[AsCommand(name: 'assetmap:compile', description: 'Compiles all mapped assets and writes them to the final public output directory.')] +#[AsCommand(name: 'asset-map:compile', description: 'Compiles all mapped assets and writes them to the final public output directory.')] final class AssetMapperCompileCommand extends Command { public function __construct( @@ -105,6 +105,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int )); } - return self::SUCCESS; + return 0; } } diff --git a/src/Symfony/Component/AssetMapper/Command/DebugAssetMapperCommand.php b/src/Symfony/Component/AssetMapper/Command/DebugAssetMapperCommand.php new file mode 100644 index 0000000000000..54b2e7e98038d --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Command/DebugAssetMapperCommand.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper\Command; + +use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\AssetMapperRepository; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Outputs all the assets in the asset mapper. + * + * @experimental + * + * @author Ryan Weaver + */ +#[AsCommand(name: 'debug:asset-map', description: 'Outputs all mapped assets.')] +final class DebugAssetMapperCommand extends Command +{ + private bool $didShortenPaths = false; + + public function __construct( + private readonly AssetMapperInterface $assetMapper, + private readonly AssetMapperRepository $assetMapperRepository, + private readonly string $projectDir, + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addOption('full', null, null, 'Whether to show the full paths') + ->setHelp(<<<'EOT' +The %command.name% command outputs all of the assets in +asset mapper for debugging purposes. +EOT + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $allAssets = $this->assetMapper->allAssets(); + + $pathRows = []; + foreach ($this->assetMapperRepository->allDirectories() as $path => $namespace) { + $path = $this->relativizePath($path); + if (!$input->getOption('full')) { + $path = $this->shortenPath($path); + } + + $pathRows[] = [$path, $namespace]; + } + $io->section('Asset Mapper Paths'); + $io->table(['Path', 'Namespace prefix'], $pathRows); + + $rows = []; + foreach ($allAssets as $asset) { + $logicalPath = $asset->logicalPath; + $sourcePath = $this->relativizePath($asset->getSourcePath()); + + if (!$input->getOption('full')) { + $logicalPath = $this->shortenPath($logicalPath); + $sourcePath = $this->shortenPath($sourcePath); + } + + $rows[] = [ + $logicalPath, + $sourcePath, + ]; + } + $io->section('Mapped Assets'); + $io->table(['Logical Path', 'Filesystem Path'], $rows); + + if ($this->didShortenPaths) { + $io->note('To see the full paths, re-run with the --full option.'); + } + + return 0; + } + + private function relativizePath(string $path): string + { + return str_replace($this->projectDir.'/', '', $path); + } + + private function shortenPath($path): string + { + $limit = 50; + + if (\strlen($path) <= $limit) { + return $path; + } + + $this->didShortenPaths = true; + $limit = floor(($limit - 3) / 2); + + return substr($path, 0, $limit).'...'.substr($path, -$limit); + } +} diff --git a/src/Symfony/Component/AssetMapper/Tests/AssetMapperRepositoryTest.php b/src/Symfony/Component/AssetMapper/Tests/AssetMapperRepositoryTest.php index 3118c9c812646..e92b419fddadb 100644 --- a/src/Symfony/Component/AssetMapper/Tests/AssetMapperRepositoryTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/AssetMapperRepositoryTest.php @@ -23,9 +23,9 @@ public function testFindWithAbsolutePaths() __DIR__.'/fixtures/dir2' => '', ], __DIR__); - $this->assertSame(__DIR__.'/fixtures/dir1/file1.css', $repository->find('file1.css')); - $this->assertSame(__DIR__.'/fixtures/dir2/file4.js', $repository->find('file4.js')); - $this->assertSame(__DIR__.'/fixtures/dir2/subdir/file5.js', $repository->find('subdir/file5.js')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir1/file1.css'), $repository->find('file1.css')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir2/file4.js'), $repository->find('file4.js')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir2/subdir/file5.js'), $repository->find('subdir/file5.js')); $this->assertNull($repository->find('file5.css')); } @@ -36,12 +36,22 @@ public function testFindWithRelativePaths() 'dir2' => '', ], __DIR__.'/fixtures'); - $this->assertSame(__DIR__.'/fixtures/dir1/file1.css', $repository->find('file1.css')); - $this->assertSame(__DIR__.'/fixtures/dir2/file4.js', $repository->find('file4.js')); - $this->assertSame(__DIR__.'/fixtures/dir2/subdir/file5.js', $repository->find('subdir/file5.js')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir1/file1.css'), $repository->find('file1.css')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir2/file4.js'), $repository->find('file4.js')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir2/subdir/file5.js'), $repository->find('subdir/file5.js')); $this->assertNull($repository->find('file5.css')); } + public function testFindWithMovingPaths() + { + $repository = new AssetMapperRepository([ + __DIR__.'/../Tests/fixtures/dir2' => '', + ], __DIR__); + + $this->assertSame(realpath(__DIR__.'/fixtures/dir2/file4.js'), $repository->find('file4.js')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir2/file4.js'), $repository->find('subdir/../file4.js')); + } + public function testFindWithNamespaces() { $repository = new AssetMapperRepository([ @@ -49,9 +59,9 @@ public function testFindWithNamespaces() 'dir2' => 'dir2_namespace', ], __DIR__.'/fixtures'); - $this->assertSame(__DIR__.'/fixtures/dir1/file1.css', $repository->find('dir1_namespace/file1.css')); - $this->assertSame(__DIR__.'/fixtures/dir2/file4.js', $repository->find('dir2_namespace/file4.js')); - $this->assertSame(__DIR__.'/fixtures/dir2/subdir/file5.js', $repository->find('dir2_namespace/subdir/file5.js')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir1/file1.css'), $repository->find('dir1_namespace/file1.css')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir2/file4.js'), $repository->find('dir2_namespace/file4.js')); + $this->assertSame(realpath(__DIR__.'/fixtures/dir2/subdir/file5.js'), $repository->find('dir2_namespace/subdir/file5.js')); // non-namespaced path does not work $this->assertNull($repository->find('file4.js')); } @@ -59,10 +69,12 @@ public function testFindWithNamespaces() public function testFindLogicalPath() { $repository = new AssetMapperRepository([ - 'dir1' => '', + 'dir1' => 'some_namespace', 'dir2' => '', ], __DIR__.'/fixtures'); $this->assertSame('subdir/file5.js', $repository->findLogicalPath(__DIR__.'/fixtures/dir2/subdir/file5.js')); + $this->assertSame('some_namespace/file2.js', $repository->findLogicalPath(__DIR__.'/fixtures/dir1/file2.js')); + $this->assertSame('some_namespace/file2.js', $repository->findLogicalPath(__DIR__.'/../Tests/fixtures/dir1/file2.js')); } public function testAll() @@ -83,8 +95,8 @@ public function testAll() 'already-abcdefVWXYZ0123456789.digested.css' => __DIR__.'/fixtures/dir2/already-abcdefVWXYZ0123456789.digested.css', 'file3.css' => __DIR__.'/fixtures/dir2/file3.css', 'file4.js' => __DIR__.'/fixtures/dir2/file4.js', - 'subdir'.\DIRECTORY_SEPARATOR.'file5.js' => __DIR__.'/fixtures/dir2/subdir/file5.js', - 'subdir'.\DIRECTORY_SEPARATOR.'file6.js' => __DIR__.'/fixtures/dir2/subdir/file6.js', + 'subdir/file5.js' => __DIR__.'/fixtures/dir2/subdir/file5.js', + 'subdir/file6.js' => __DIR__.'/fixtures/dir2/subdir/file6.js', 'test.gif.foo' => __DIR__.'/fixtures/dir3/test.gif.foo', ]); $this->assertEquals($expectedAllAssets, array_map('realpath', $actualAllAssets)); @@ -109,16 +121,10 @@ public function testAllWithNamespaces() 'dir3_namespace/test.gif.foo' => __DIR__.'/fixtures/dir3/test.gif.foo', ]; - $normalizedExpectedAllAssets = []; - foreach ($expectedAllAssets as $key => $val) { - $normalizedExpectedAllAssets[str_replace('/', \DIRECTORY_SEPARATOR, $key)] = realpath($val); - } + $normalizedExpectedAllAssets = array_map('realpath', $expectedAllAssets); $actualAssets = $repository->all(); - $normalizedActualAssets = []; - foreach ($actualAssets as $key => $val) { - $normalizedActualAssets[str_replace('/', \DIRECTORY_SEPARATOR, $key)] = realpath($val); - } + $normalizedActualAssets = array_map('realpath', $actualAssets); $this->assertEquals($normalizedExpectedAllAssets, $normalizedActualAssets); } diff --git a/src/Symfony/Component/AssetMapper/Tests/Command/AssetsMapperCompileCommandTest.php b/src/Symfony/Component/AssetMapper/Tests/Command/AssetsMapperCompileCommandTest.php index ea02d86491d29..810132c7bfe65 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Command/AssetsMapperCompileCommandTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Command/AssetsMapperCompileCommandTest.php @@ -40,7 +40,7 @@ public function testAssetsAreCompiled() { $application = new Application($this->kernel); - $command = $application->find('assetmap:compile'); + $command = $application->find('asset-map:compile'); $tester = new CommandTester($command); $res = $tester->execute([]); $this->assertSame(0, $res); @@ -59,6 +59,21 @@ public function testAssetsAreCompiled() $finder->in($targetBuildDir)->files(); $this->assertCount(9, $finder); $this->assertFileExists($targetBuildDir.'/manifest.json'); + + $expected = [ + 'file1.css', + 'file2.js', + 'file3.css', + 'subdir/file6.js', + 'subdir/file5.js', + 'file4.js', + 'already-abcdefVWXYZ0123456789.digested.css', + ]; + $actual = array_keys(json_decode(file_get_contents($targetBuildDir.'/manifest.json'), true)); + sort($expected); + sort($actual); + + $this->assertSame($expected, $actual); $this->assertFileExists($targetBuildDir.'/importmap.json'); } } diff --git a/src/Symfony/Component/AssetMapper/Tests/Command/DebugAssetsMapperCommandTest.php b/src/Symfony/Component/AssetMapper/Tests/Command/DebugAssetsMapperCommandTest.php new file mode 100644 index 0000000000000..8f375876078ff --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Tests/Command/DebugAssetsMapperCommandTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\AssetMapper\Tests\fixtures\AssetMapperTestAppKernel; +use Symfony\Component\Console\Tester\CommandTester; + +class DebugAssetsMapperCommandTest extends TestCase +{ + public function testCommandDumpsInformation() + { + $application = new Application(new AssetMapperTestAppKernel('test', true)); + + $command = $application->find('debug:asset-map'); + $tester = new CommandTester($command); + $res = $tester->execute([]); + $this->assertSame(0, $res); + + $this->assertStringContainsString('dir1', $tester->getDisplay()); + $this->assertStringContainsString('subdir/file6.js', $tester->getDisplay()); + $this->assertStringContainsString('dir2'.\DIRECTORY_SEPARATOR.'subdir'.\DIRECTORY_SEPARATOR.'file6.js', $tester->getDisplay()); + } +}