From 48a2d689e6e91c943d931d30e90555726ac6f7c7 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Wed, 4 Oct 2023 11:07:37 -0400 Subject: [PATCH] [AssetMapper] Allowing for files to be written to some non-local location --- .../FrameworkExtension.php | 23 +++--- .../Resources/config/asset_mapper.php | 23 ++++-- .../XmlFrameworkExtensionTest.php | 2 +- .../Component/AssetMapper/AssetMapper.php | 9 +-- .../Command/AssetMapperCompileCommand.php | 80 ++++++------------- .../CompiledAssetMapperConfigReader.php | 52 ++++++++++++ .../Event/PreAssetsCompileEvent.php | 9 +-- .../ImportMap/ImportMapGenerator.php | 21 ++--- .../Path/LocalPublicAssetsFilesystem.php | 43 ++++++++++ .../Path/PublicAssetsFilesystemInterface.php | 33 ++++++++ .../Path/PublicAssetsPathResolver.php | 7 -- .../PublicAssetsPathResolverInterface.php | 5 -- .../AssetMapper/Tests/AssetMapperTest.php | 16 ++-- .../Command/AssetMapperCompileCommandTest.php | 3 +- .../CompiledAssetMapperConfigReaderTest.php | 69 ++++++++++++++++ .../test_public/final-assets/manifest.json | 3 - .../ImportMap/ImportMapGeneratorTest.php | 39 +++++---- .../Path/LocalPublicAssetsFilesystemTest.php | 55 +++++++++++++ .../Path/PublicAssetsPathResolverTest.php | 21 ----- 19 files changed, 347 insertions(+), 166 deletions(-) create mode 100644 src/Symfony/Component/AssetMapper/CompiledAssetMapperConfigReader.php create mode 100644 src/Symfony/Component/AssetMapper/Path/LocalPublicAssetsFilesystem.php create mode 100644 src/Symfony/Component/AssetMapper/Path/PublicAssetsFilesystemInterface.php create mode 100644 src/Symfony/Component/AssetMapper/Tests/CompiledAssetMapperConfigReaderTest.php delete mode 100644 src/Symfony/Component/AssetMapper/Tests/Fixtures/test_public/final-assets/manifest.json create mode 100644 src/Symfony/Component/AssetMapper/Tests/Path/LocalPublicAssetsFilesystemTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d9be5d61fcd98..132904c303f1d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1353,13 +1353,17 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde ->setArgument(0, $paths) ->setArgument(2, $excludedPathPatterns); - $publicDirName = $this->getPublicDirectoryName($container); $container->getDefinition('asset_mapper.public_assets_path_resolver') - ->setArgument(1, $config['public_prefix']) - ->setArgument(2, $publicDirName); + ->setArgument(0, $config['public_prefix']); - $container->getDefinition('asset_mapper.command.compile') - ->setArgument(5, $publicDirName); + $publicDirectory = $this->getPublicDirectory($container); + $publicAssetsDirectory = rtrim($publicDirectory.'/'.ltrim($config['public_prefix'], '/'), '/'); + $container->getDefinition('asset_mapper.local_public_assets_filesystem') + ->setArgument(0, $publicDirectory) + ; + + $container->getDefinition('asset_mapper.compiled_asset_mapper_config_reader') + ->setArgument(0, $publicAssetsDirectory); if (!$config['server']) { $container->removeDefinition('asset_mapper.dev_server_subscriber'); @@ -3163,11 +3167,12 @@ private function writeConfigEnabled(string $path, bool $value, array &$config): $config['enabled'] = $value; } - private function getPublicDirectoryName(ContainerBuilder $container): string + private function getPublicDirectory(ContainerBuilder $container): string { - $defaultPublicDir = 'public'; + $projectDir = $container->getParameter('kernel.project_dir'); + $defaultPublicDir = $projectDir.'/public'; - $composerFilePath = $container->getParameter('kernel.project_dir').'/composer.json'; + $composerFilePath = $projectDir.'/composer.json'; if (!file_exists($composerFilePath)) { return $defaultPublicDir; @@ -3176,6 +3181,6 @@ private function getPublicDirectoryName(ContainerBuilder $container): string $container->addResource(new FileResource($composerFilePath)); $composerConfig = json_decode(file_get_contents($composerFilePath), true); - return $composerConfig['extra']['public-dir'] ?? $defaultPublicDir; + return isset($composerConfig['extra']['public-dir']) ? $projectDir.'/'.$composerConfig['extra']['public-dir'] : $defaultPublicDir; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php index 046bd8c6c5fde..9139a6c898fc9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php @@ -24,6 +24,7 @@ use Symfony\Component\AssetMapper\Command\ImportMapRemoveCommand; use Symfony\Component\AssetMapper\Command\ImportMapRequireCommand; use Symfony\Component\AssetMapper\Command\ImportMapUpdateCommand; +use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader; use Symfony\Component\AssetMapper\Compiler\CssAssetUrlCompiler; use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler; use Symfony\Component\AssetMapper\Compiler\SourceMappingUrlsCompiler; @@ -40,6 +41,7 @@ use Symfony\Component\AssetMapper\ImportMap\RemotePackageStorage; use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver; use Symfony\Component\AssetMapper\MapperAwareAssetPackage; +use Symfony\Component\AssetMapper\Path\LocalPublicAssetsFilesystem; use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolver; return static function (ContainerConfigurator $container) { @@ -48,7 +50,7 @@ ->args([ service('asset_mapper.repository'), service('asset_mapper.mapped_asset_factory'), - service('asset_mapper.public_assets_path_resolver'), + service('asset_mapper.compiled_asset_mapper_config_reader'), ]) ->alias(AssetMapperInterface::class, 'asset_mapper') @@ -76,9 +78,17 @@ ->set('asset_mapper.public_assets_path_resolver', PublicAssetsPathResolver::class) ->args([ - param('kernel.project_dir'), abstract_arg('asset public prefix'), - abstract_arg('public directory name'), + ]) + + ->set('asset_mapper.local_public_assets_filesystem', LocalPublicAssetsFilesystem::class) + ->args([ + abstract_arg('public directory'), + ]) + + ->set('asset_mapper.compiled_asset_mapper_config_reader', CompiledAssetMapperConfigReader::class) + ->args([ + abstract_arg('public assets directory'), ]) ->set('asset_mapper.asset_package', MapperAwareAssetPackage::class) @@ -100,12 +110,11 @@ ->set('asset_mapper.command.compile', AssetMapperCompileCommand::class) ->args([ - service('asset_mapper.public_assets_path_resolver'), + service('asset_mapper.compiled_asset_mapper_config_reader'), service('asset_mapper'), service('asset_mapper.importmap.generator'), - service('filesystem'), + service('asset_mapper.local_public_assets_filesystem'), param('kernel.project_dir'), - abstract_arg('public directory name'), param('kernel.debug'), service('event_dispatcher')->nullOnInvalid(), ]) @@ -163,7 +172,7 @@ ->set('asset_mapper.importmap.generator', ImportMapGenerator::class) ->args([ service('asset_mapper'), - service('asset_mapper.public_assets_path_resolver'), + service('asset_mapper.compiled_asset_mapper_config_reader'), service('asset_mapper.importmap.config_reader'), ]) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php index 23d9ecfef3ad3..822409f706bc3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php @@ -79,7 +79,7 @@ public function testAssetMapper() $container = $this->createContainerFromFile('asset_mapper'); $definition = $container->getDefinition('asset_mapper.public_assets_path_resolver'); - $this->assertSame('/assets_path/', $definition->getArgument(1)); + $this->assertSame('/assets_path/', $definition->getArgument(0)); $definition = $container->getDefinition('asset_mapper.dev_server_subscriber'); $this->assertSame(['zip' => 'application/zip'], $definition->getArgument(2)); diff --git a/src/Symfony/Component/AssetMapper/AssetMapper.php b/src/Symfony/Component/AssetMapper/AssetMapper.php index fc681cb4bf73e..4afcf6336368b 100644 --- a/src/Symfony/Component/AssetMapper/AssetMapper.php +++ b/src/Symfony/Component/AssetMapper/AssetMapper.php @@ -12,7 +12,6 @@ namespace Symfony\Component\AssetMapper; use Symfony\Component\AssetMapper\Factory\MappedAssetFactoryInterface; -use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; /** * Finds and returns assets in the pipeline. @@ -28,7 +27,7 @@ class AssetMapper implements AssetMapperInterface public function __construct( private readonly AssetMapperRepository $mapperRepository, private readonly MappedAssetFactoryInterface $mappedAssetFactory, - private readonly PublicAssetsPathResolverInterface $assetsPathResolver, + private readonly CompiledAssetMapperConfigReader $compiledConfigReader, ) { } @@ -78,12 +77,10 @@ public function getPublicPath(string $logicalPath): ?string private function loadManifest(): array { if (null === $this->manifestData) { - $path = $this->assetsPathResolver->getPublicFilesystemPath().'/'.self::MANIFEST_FILE_NAME; - - if (!is_file($path)) { + if (!$this->compiledConfigReader->configExists(self::MANIFEST_FILE_NAME)) { $this->manifestData = []; } else { - $this->manifestData = json_decode(file_get_contents($path), true); + $this->manifestData = $this->compiledConfigReader->loadConfig(self::MANIFEST_FILE_NAME); } } diff --git a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php index cc392a6b9960a..7bb8accd74abf 100644 --- a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php +++ b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php @@ -13,16 +13,15 @@ use Symfony\Component\AssetMapper\AssetMapper; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader; use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent; use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator; -use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; +use Symfony\Component\AssetMapper\Path\PublicAssetsFilesystemInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Filesystem\Filesystem; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -36,12 +35,11 @@ final class AssetMapperCompileCommand extends Command { public function __construct( - private readonly PublicAssetsPathResolverInterface $publicAssetsPathResolver, + private readonly CompiledAssetMapperConfigReader $compiledConfigReader, private readonly AssetMapperInterface $assetMapper, private readonly ImportMapGenerator $importMapGenerator, - private readonly Filesystem $filesystem, + private readonly PublicAssetsFilesystemInterface $assetsFilesystem, private readonly string $projectDir, - private readonly string $publicDirName, private readonly bool $isDebug, private readonly ?EventDispatcherInterface $eventDispatcher = null, ) { @@ -51,7 +49,6 @@ public function __construct( protected function configure(): void { $this - ->addOption('clean', null, null, 'Whether to clean the public directory before compiling assets') ->setHelp(<<<'EOT' The %command.name% command compiles and dumps all the assets in the asset mapper into the final public directory (usually public/assets). @@ -64,61 +61,36 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $publicDir = $this->projectDir.'/'.$this->publicDirName; - if (!is_dir($publicDir)) { - throw new InvalidArgumentException(sprintf('The public directory "%s" does not exist.', $publicDir)); - } - - $outputDir = $this->publicAssetsPathResolver->getPublicFilesystemPath(); - if ($input->getOption('clean')) { - $io->comment(sprintf('Cleaning %s', $outputDir)); - $this->filesystem->remove($outputDir); - $this->filesystem->mkdir($outputDir); - } - - // set up the file paths - $files = []; - $manifestPath = $outputDir.'/'.AssetMapper::MANIFEST_FILE_NAME; - $files[] = $manifestPath; - $importMapPath = $outputDir.'/'.ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME; - $files[] = $importMapPath; + $this->eventDispatcher?->dispatch(new PreAssetsCompileEvent($output)); - $entrypointFilePaths = []; + // remove existing config files + $this->compiledConfigReader->removeConfig(AssetMapper::MANIFEST_FILE_NAME); + $this->compiledConfigReader->removeConfig(ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME); + $entrypointFiles = []; foreach ($this->importMapGenerator->getEntrypointNames() as $entrypointName) { - $dumpedEntrypointPath = $outputDir.'/'.sprintf(ImportMapGenerator::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName); - $files[] = $dumpedEntrypointPath; - $entrypointFilePaths[$entrypointName] = $dumpedEntrypointPath; + $path = sprintf(ImportMapGenerator::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName); + $this->compiledConfigReader->removeConfig($path); + $entrypointFiles[$entrypointName] = $path; } - // remove existing files - foreach ($files as $file) { - if (is_file($file)) { - $this->filesystem->remove($file); - } - } - - $this->eventDispatcher?->dispatch(new PreAssetsCompileEvent($outputDir, $output)); - - // dump new files - $manifest = $this->createManifestAndWriteFiles($io, $publicDir); - $this->filesystem->dumpFile($manifestPath, json_encode($manifest, \JSON_PRETTY_PRINT)); + $manifest = $this->createManifestAndWriteFiles($io); + $manifestPath = $this->compiledConfigReader->saveConfig(AssetMapper::MANIFEST_FILE_NAME, $manifest); $io->comment(sprintf('Manifest written to %s', $this->shortenPath($manifestPath))); - $this->filesystem->dumpFile($importMapPath, json_encode($this->importMapGenerator->getRawImportMapData(), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG)); + $importMapPath = $this->compiledConfigReader->saveConfig(ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME, $this->importMapGenerator->getRawImportMapData()); $io->comment(sprintf('Import map data written to %s.', $this->shortenPath($importMapPath))); - $entrypointNames = $this->importMapGenerator->getEntrypointNames(); - foreach ($entrypointFilePaths as $entrypointName => $path) { - $this->filesystem->dumpFile($path, json_encode($this->importMapGenerator->findEagerEntrypointImports($entrypointName), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG)); + foreach ($entrypointFiles as $entrypointName => $path) { + $this->compiledConfigReader->saveConfig($path, $this->importMapGenerator->findEagerEntrypointImports($entrypointName)); } - $styledEntrypointNames = array_map(fn (string $entrypointName) => sprintf('%s', $entrypointName), $entrypointNames); - $io->comment(sprintf('Entrypoint metadata written for %d entrypoints (%s).', \count($entrypointNames), implode(', ', $styledEntrypointNames))); + $styledEntrypointNames = array_map(fn (string $entrypointName) => sprintf('%s', $entrypointName), array_keys($entrypointFiles)); + $io->comment(sprintf('Entrypoint metadata written for %d entrypoints (%s).', \count($entrypointFiles), implode(', ', $styledEntrypointNames))); if ($this->isDebug) { $io->warning(sprintf( - 'You are compiling assets in development. Symfony will not serve any changed assets until you delete the "%s" directory.', - $this->shortenPath($outputDir) + 'You are compiling assets in development. Symfony will not serve any changed assets until you delete the files in the "%s" directory.', + $this->shortenPath(\dirname($manifestPath)) )); } @@ -130,20 +102,18 @@ private function shortenPath(string $path): string return str_replace($this->projectDir.'/', '', $path); } - private function createManifestAndWriteFiles(SymfonyStyle $io, string $publicDir): array + private function createManifestAndWriteFiles(SymfonyStyle $io): array { $allAssets = $this->assetMapper->allAssets(); - $io->comment(sprintf('Compiling assets to %s%s', $publicDir, $this->publicAssetsPathResolver->resolvePublicPath(''))); + $io->comment(sprintf('Compiling and writing asset files to %s', $this->shortenPath($this->assetsFilesystem->getDestinationPath()))); $manifest = []; foreach ($allAssets as $asset) { - // $asset->getPublicPath() will start with a "/" - $targetPath = $publicDir.$asset->publicPath; if (null !== $asset->content) { // The original content has been modified by the AssetMapperCompiler - $this->filesystem->dumpFile($targetPath, $asset->content); + $this->assetsFilesystem->write($asset->publicPath, $asset->content); } else { - $this->filesystem->copy($asset->sourcePath, $targetPath, true); + $this->assetsFilesystem->copy($asset->sourcePath, $asset->publicPath); } $manifest[$asset->logicalPath] = $asset->publicPath; diff --git a/src/Symfony/Component/AssetMapper/CompiledAssetMapperConfigReader.php b/src/Symfony/Component/AssetMapper/CompiledAssetMapperConfigReader.php new file mode 100644 index 0000000000000..daa656805fe9d --- /dev/null +++ b/src/Symfony/Component/AssetMapper/CompiledAssetMapperConfigReader.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper; + +use Symfony\Component\Filesystem\Path; + +/** + * Reads and writes compiled configuration files for asset mapper. + */ +class CompiledAssetMapperConfigReader +{ + public function __construct(private readonly string $directory) + { + } + + public function configExists(string $filename): bool + { + return is_file(Path::join($this->directory, $filename)); + } + + public function loadConfig(string $filename): array + { + return json_decode(file_get_contents(Path::join($this->directory, $filename)), true, 512, \JSON_THROW_ON_ERROR); + } + + public function saveConfig(string $filename, array $data): string + { + $path = Path::join($this->directory, $filename); + @mkdir(\dirname($path), 0777, true); + file_put_contents($path, json_encode($data, \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR)); + + return $path; + } + + public function removeConfig(string $filename): void + { + $path = Path::join($this->directory, $filename); + + if (is_file($path)) { + unlink($path); + } + } +} diff --git a/src/Symfony/Component/AssetMapper/Event/PreAssetsCompileEvent.php b/src/Symfony/Component/AssetMapper/Event/PreAssetsCompileEvent.php index a55a2e8e6a77a..972e78ae9802e 100644 --- a/src/Symfony/Component/AssetMapper/Event/PreAssetsCompileEvent.php +++ b/src/Symfony/Component/AssetMapper/Event/PreAssetsCompileEvent.php @@ -21,20 +21,13 @@ */ class PreAssetsCompileEvent extends Event { - private string $outputDir; private OutputInterface $output; - public function __construct(string $outputDir, OutputInterface $output) + public function __construct(OutputInterface $output) { - $this->outputDir = $outputDir; $this->output = $output; } - public function getOutputDir(): string - { - return $this->outputDir; - } - public function getOutput(): OutputInterface { return $this->output; diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php index bca5897c03c53..f75593be85e52 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php @@ -12,9 +12,9 @@ namespace Symfony\Component\AssetMapper\ImportMap; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader; use Symfony\Component\AssetMapper\Exception\LogicException; use Symfony\Component\AssetMapper\MappedAsset; -use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; /** * Provides data needed to write the importmap & preloads. @@ -26,7 +26,7 @@ class ImportMapGenerator public function __construct( private readonly AssetMapperInterface $assetMapper, - private readonly PublicAssetsPathResolverInterface $assetsPathResolver, + private readonly CompiledAssetMapperConfigReader $compiledConfigReader, private readonly ImportMapConfigReader $importMapConfigReader, ) { } @@ -87,9 +87,8 @@ public function getImportMapData(array $entrypointNames): array */ public function getRawImportMapData(): array { - $dumpedImportMapPath = $this->assetsPathResolver->getPublicFilesystemPath().'/'.self::IMPORT_MAP_CACHE_FILENAME; - if (is_file($dumpedImportMapPath)) { - return json_decode(file_get_contents($dumpedImportMapPath), true, 512, \JSON_THROW_ON_ERROR); + if ($this->compiledConfigReader->configExists(self::IMPORT_MAP_CACHE_FILENAME)) { + return $this->compiledConfigReader->loadConfig(self::IMPORT_MAP_CACHE_FILENAME); } $allEntries = []; @@ -122,9 +121,8 @@ public function getRawImportMapData(): array */ public function findEagerEntrypointImports(string $entryName): array { - $dumpedEntrypointPath = $this->assetsPathResolver->getPublicFilesystemPath().'/'.sprintf(self::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entryName); - if (is_file($dumpedEntrypointPath)) { - return json_decode(file_get_contents($dumpedEntrypointPath), true, 512, \JSON_THROW_ON_ERROR); + if ($this->compiledConfigReader->configExists(sprintf(self::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entryName))) { + return $this->compiledConfigReader->loadConfig(sprintf(self::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entryName)); } $rootImportEntries = $this->importMapConfigReader->getEntries(); @@ -202,13 +200,6 @@ private function addImplicitEntries(ImportMapEntry $entry, array $currentImportE return $currentImportEntries; } - private function findRootImportMapEntry(string $moduleName): ?ImportMapEntry - { - $entries = $this->importMapConfigReader->getEntries(); - - return $entries->has($moduleName) ? $entries->get($moduleName) : null; - } - /** * Finds the MappedAsset allowing for a "logical path", relative or absolute filesystem path. */ diff --git a/src/Symfony/Component/AssetMapper/Path/LocalPublicAssetsFilesystem.php b/src/Symfony/Component/AssetMapper/Path/LocalPublicAssetsFilesystem.php new file mode 100644 index 0000000000000..c6302515927f7 --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Path/LocalPublicAssetsFilesystem.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper\Path; + +use Symfony\Component\Filesystem\Filesystem; + +class LocalPublicAssetsFilesystem implements PublicAssetsFilesystemInterface +{ + private Filesystem $filesystem; + + public function __construct(private readonly string $publicDir) + { + $this->filesystem = new Filesystem(); + } + + public function write(string $path, string $contents): void + { + $targetPath = $this->publicDir.'/'.ltrim($path, '/'); + + $this->filesystem->dumpFile($targetPath, $contents); + } + + public function copy(string $originPath, string $path): void + { + $targetPath = $this->publicDir.'/'.ltrim($path, '/'); + + $this->filesystem->copy($originPath, $targetPath, true); + } + + public function getDestinationPath(): string + { + return $this->publicDir; + } +} diff --git a/src/Symfony/Component/AssetMapper/Path/PublicAssetsFilesystemInterface.php b/src/Symfony/Component/AssetMapper/Path/PublicAssetsFilesystemInterface.php new file mode 100644 index 0000000000000..b33c126301215 --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Path/PublicAssetsFilesystemInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper\Path; + +/** + * Writes asset files to their public location. + */ +interface PublicAssetsFilesystemInterface +{ + /** + * Write the contents of a file to the public location. + */ + public function write(string $path, string $contents): void; + + /** + * Copy a local file to the public location. + */ + public function copy(string $originPath, string $path): void; + + /** + * A string representation of the public directory, used for feedback. + */ + public function getDestinationPath(): string; +} diff --git a/src/Symfony/Component/AssetMapper/Path/PublicAssetsPathResolver.php b/src/Symfony/Component/AssetMapper/Path/PublicAssetsPathResolver.php index c05c6c5ad3afc..fe839d591a99e 100644 --- a/src/Symfony/Component/AssetMapper/Path/PublicAssetsPathResolver.php +++ b/src/Symfony/Component/AssetMapper/Path/PublicAssetsPathResolver.php @@ -16,9 +16,7 @@ class PublicAssetsPathResolver implements PublicAssetsPathResolverInterface private readonly string $publicPrefix; public function __construct( - private readonly string $projectRootDir, string $publicPrefix = '/assets/', - private readonly string $publicDirName = 'public', ) { // ensure that the public prefix always ends with a single slash $this->publicPrefix = rtrim($publicPrefix, '/').'/'; @@ -28,9 +26,4 @@ public function resolvePublicPath(string $logicalPath): string { return $this->publicPrefix.ltrim($logicalPath, '/'); } - - public function getPublicFilesystemPath(): string - { - return rtrim(rtrim($this->projectRootDir, '/').'/'.$this->publicDirName.$this->publicPrefix, '/'); - } } diff --git a/src/Symfony/Component/AssetMapper/Path/PublicAssetsPathResolverInterface.php b/src/Symfony/Component/AssetMapper/Path/PublicAssetsPathResolverInterface.php index 802d1ce07ecff..5046fb8d034ba 100644 --- a/src/Symfony/Component/AssetMapper/Path/PublicAssetsPathResolverInterface.php +++ b/src/Symfony/Component/AssetMapper/Path/PublicAssetsPathResolverInterface.php @@ -17,9 +17,4 @@ interface PublicAssetsPathResolverInterface * The path that should be prefixed on all asset paths to point to the output location. */ public function resolvePublicPath(string $logicalPath): string; - - /** - * Returns the filesystem path to where assets are stored when compiled. - */ - public function getPublicFilesystemPath(): string; } diff --git a/src/Symfony/Component/AssetMapper/Tests/AssetMapperTest.php b/src/Symfony/Component/AssetMapper/Tests/AssetMapperTest.php index 8829089fb93f6..ef31a1c7fb714 100644 --- a/src/Symfony/Component/AssetMapper/Tests/AssetMapperTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/AssetMapperTest.php @@ -15,9 +15,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\AssetMapper\AssetMapper; use Symfony\Component\AssetMapper\AssetMapperRepository; +use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader; use Symfony\Component\AssetMapper\Factory\MappedAssetFactoryInterface; use Symfony\Component\AssetMapper\MappedAsset; -use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; class AssetMapperTest extends TestCase { @@ -90,17 +90,21 @@ private function createAssetMapper(): AssetMapper { $dirs = ['dir1' => '', 'dir2' => '', 'dir3' => '']; $repository = new AssetMapperRepository($dirs, __DIR__.'/Fixtures'); - $pathResolver = $this->createMock(PublicAssetsPathResolverInterface::class); - $pathResolver->expects($this->any()) - ->method('getPublicFilesystemPath') - ->willReturn(__DIR__.'/Fixtures/test_public/final-assets'); + $compiledConfigReader = $this->createMock(CompiledAssetMapperConfigReader::class); + $compiledConfigReader->expects($this->any()) + ->method('configExists') + ->with(AssetMapper::MANIFEST_FILE_NAME) + ->willReturn(true); + $compiledConfigReader->expects($this->any()) + ->method('loadConfig') + ->willReturn(['file4.js' => '/final-assets/file4.checksumfrommanifest.js']); $this->mappedAssetFactory = $this->createMock(MappedAssetFactoryInterface::class); return new AssetMapper( $repository, $this->mappedAssetFactory, - $pathResolver, + $compiledConfigReader, ); } } diff --git a/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php b/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php index 595f2a7509873..05283f33df5d7 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php @@ -51,7 +51,7 @@ public function testAssetsAreCompiled() $this->filesystem->mkdir($targetBuildDir); file_put_contents($targetBuildDir.'/manifest.json', '{}'); file_put_contents($targetBuildDir.'/importmap.json', '{}'); - file_put_contents($targetBuildDir.'/entrypoint.file6.json', '[]]'); + file_put_contents($targetBuildDir.'/entrypoint.file6.json', '[]'); $command = $application->find('asset-map:compile'); $tester = new CommandTester($command); @@ -119,7 +119,6 @@ public function testEventIsDispatched() $listenerCalled = false; $dispatcher->addListener(PreAssetsCompileEvent::class, function (PreAssetsCompileEvent $event) use (&$listenerCalled) { $listenerCalled = true; - $this->assertSame(realpath(__DIR__.'/../Fixtures').'/public/assets', $event->getOutputDir()); $this->assertInstanceOf(OutputInterface::class, $event->getOutput()); }); diff --git a/src/Symfony/Component/AssetMapper/Tests/CompiledAssetMapperConfigReaderTest.php b/src/Symfony/Component/AssetMapper/Tests/CompiledAssetMapperConfigReaderTest.php new file mode 100644 index 0000000000000..03ea5fce21f1f --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Tests/CompiledAssetMapperConfigReaderTest.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader; +use Symfony\Component\Filesystem\Filesystem; + +class CompiledAssetMapperConfigReaderTest extends TestCase +{ + private Filesystem $filesystem; + private string $writableRoot; + + protected function setUp(): void + { + $this->filesystem = new Filesystem(); + $this->writableRoot = __DIR__.'/../Fixtures/importmaps_for_writing'; + if (!file_exists(__DIR__.'/../Fixtures/importmaps_for_writing')) { + $this->filesystem->mkdir($this->writableRoot); + } + // realpath to help path comparisons in the tests + $this->writableRoot = realpath($this->writableRoot); + } + + protected function tearDown(): void + { + $this->filesystem->remove($this->writableRoot); + } + + public function testConfigExists() + { + $reader = new CompiledAssetMapperConfigReader($this->writableRoot); + $this->assertFalse($reader->configExists('foo.json')); + $this->filesystem->touch($this->writableRoot.'/foo.json'); + $this->assertTrue($reader->configExists('foo.json')); + } + + public function testLoadConfig() + { + $reader = new CompiledAssetMapperConfigReader($this->writableRoot); + $this->filesystem->dumpFile($this->writableRoot.'/foo.json', '{"foo": "bar"}'); + $this->assertEquals(['foo' => 'bar'], $reader->loadConfig('foo.json')); + } + + public function testSaveConfig() + { + $reader = new CompiledAssetMapperConfigReader($this->writableRoot); + $this->assertEquals($this->writableRoot.'/foo.json', $reader->saveConfig('foo.json', ['foo' => 'bar'])); + $this->assertEquals(['foo' => 'bar'], json_decode(file_get_contents($this->writableRoot.'/foo.json'), true)); + } + + public function testRemoveConfig() + { + $reader = new CompiledAssetMapperConfigReader($this->writableRoot); + $this->filesystem->touch($this->writableRoot.'/foo.json'); + $this->assertTrue($reader->configExists('foo.json')); + $reader->removeConfig('foo.json'); + $this->assertFalse($reader->configExists('foo.json')); + } +} diff --git a/src/Symfony/Component/AssetMapper/Tests/Fixtures/test_public/final-assets/manifest.json b/src/Symfony/Component/AssetMapper/Tests/Fixtures/test_public/final-assets/manifest.json deleted file mode 100644 index b32c6a99d4bef..0000000000000 --- a/src/Symfony/Component/AssetMapper/Tests/Fixtures/test_public/final-assets/manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "file4.js": "/final-assets/file4.checksumfrommanifest.js" -} diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php index b8f6e767ae6b0..6c8ab752d286d 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\CompiledAssetMapperConfigReader; use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\AssetMapper\ImportMap\ImportMapEntries; use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; @@ -21,13 +22,12 @@ use Symfony\Component\AssetMapper\ImportMap\ImportMapType; use Symfony\Component\AssetMapper\ImportMap\JavaScriptImport; use Symfony\Component\AssetMapper\MappedAsset; -use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; use Symfony\Component\Filesystem\Filesystem; class ImportMapGeneratorTest extends TestCase { private AssetMapperInterface&MockObject $assetMapper; - private PublicAssetsPathResolverInterface&MockObject $pathResolver; + private CompiledAssetMapperConfigReader&MockObject $compiledConfigReader; private ImportMapConfigReader&MockObject $configReader; private ImportMapGenerator $importMapGenerator; @@ -565,10 +565,13 @@ public function testGetRawImportDataUsesCacheFile() 'path' => 'https://anyurl.com/stimulus', ], ]; - $this->writeFile('public/assets/importmap.json', json_encode($importmapData)); - $this->pathResolver->expects($this->once()) - ->method('getPublicFilesystemPath') - ->willReturn(self::$writableRoot.'/public/assets'); + $this->compiledConfigReader->expects($this->once()) + ->method('configExists') + ->with('importmap.json') + ->willReturn(true); + $this->compiledConfigReader->expects($this->once()) + ->method('loadConfig') + ->willReturn($importmapData); $this->assertEquals($importmapData, $manager->getRawImportMapData()); } @@ -651,17 +654,20 @@ public function testFindEagerEntrypointImportsUsesCacheFile() 'app', '/assets/foo.js', ]; - $this->writeFile('public/assets/entrypoint.foo.json', json_encode($entrypointData)); - $this->pathResolver->expects($this->once()) - ->method('getPublicFilesystemPath') - ->willReturn(self::$writableRoot.'/public/assets'); + $this->compiledConfigReader->expects($this->once()) + ->method('configExists') + ->with('entrypoint.foo.json') + ->willReturn(true); + $this->compiledConfigReader->expects($this->once()) + ->method('loadConfig') + ->willReturn($entrypointData); $this->assertEquals($entrypointData, $manager->findEagerEntrypointImports('foo')); } private function createImportMapGenerator(): ImportMapGenerator { - $this->pathResolver = $this->createMock(PublicAssetsPathResolverInterface::class); + $this->compiledConfigReader = $this->createMock(CompiledAssetMapperConfigReader::class); $this->assetMapper = $this->createMock(AssetMapperInterface::class); $this->configReader = $this->createMock(ImportMapConfigReader::class); @@ -676,7 +682,7 @@ private function createImportMapGenerator(): ImportMapGenerator return $this->importMapGenerator = new ImportMapGenerator( $this->assetMapper, - $this->pathResolver, + $this->compiledConfigReader, $this->configReader, ); } @@ -689,15 +695,6 @@ private function mockImportMap(array $importMapEntries): void ; } - private function writeFile(string $filename, string $content): void - { - $path = \dirname(self::$writableRoot.'/'.$filename); - if (!is_dir($path)) { - mkdir($path, 0777, true); - } - file_put_contents(self::$writableRoot.'/'.$filename, $content); - } - private static function createLocalEntry(string $importName, string $path, ImportMapType $type = ImportMapType::JS, bool $isEntrypoint = false): ImportMapEntry { return ImportMapEntry::createLocal($importName, $type, path: $path, isEntrypoint: $isEntrypoint); diff --git a/src/Symfony/Component/AssetMapper/Tests/Path/LocalPublicAssetsFilesystemTest.php b/src/Symfony/Component/AssetMapper/Tests/Path/LocalPublicAssetsFilesystemTest.php new file mode 100644 index 0000000000000..4363ccbf577a8 --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Tests/Path/LocalPublicAssetsFilesystemTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper\Tests\Path; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AssetMapper\Path\LocalPublicAssetsFilesystem; +use Symfony\Component\Filesystem\Filesystem; + +class LocalPublicAssetsFilesystemTest extends TestCase +{ + private Filesystem $filesystem; + private static string $writableRoot = __DIR__.'/../Fixtures/importmaps_for_writing'; + + protected function setUp(): void + { + $this->filesystem = new Filesystem(); + if (!file_exists(__DIR__.'/../Fixtures/importmaps_for_writing')) { + $this->filesystem->mkdir(self::$writableRoot); + } + } + + protected function tearDown(): void + { + $this->filesystem->remove(self::$writableRoot); + } + + public function testWrite() + { + $filesystem = new LocalPublicAssetsFilesystem(self::$writableRoot); + $filesystem->write('foo/bar.js', 'foobar'); + $this->assertFileExists(self::$writableRoot.'/foo/bar.js'); + $this->assertSame('foobar', file_get_contents(self::$writableRoot.'/foo/bar.js')); + + // with a directory + $filesystem->write('foo/baz/bar.js', 'foobar'); + $this->assertFileExists(self::$writableRoot.'/foo/baz/bar.js'); + } + + public function testCopy() + { + $filesystem = new LocalPublicAssetsFilesystem(self::$writableRoot); + $filesystem->copy(__DIR__.'/../Fixtures/importmaps/assets/pizza/index.js', 'foo/bar.js'); + $this->assertFileExists(self::$writableRoot.'/foo/bar.js'); + $this->assertSame("console.log('pizza/index.js');", trim(file_get_contents(self::$writableRoot.'/foo/bar.js'))); + } +} diff --git a/src/Symfony/Component/AssetMapper/Tests/Path/PublicAssetsPathResolverTest.php b/src/Symfony/Component/AssetMapper/Tests/Path/PublicAssetsPathResolverTest.php index af2fa7f74f109..2144b98919527 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Path/PublicAssetsPathResolverTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Path/PublicAssetsPathResolverTest.php @@ -19,38 +19,17 @@ class PublicAssetsPathResolverTest extends TestCase public function testResolvePublicPath() { $resolver = new PublicAssetsPathResolver( - '/projectRootDir/', '/assets-prefix/', - 'publicDirName', ); $this->assertSame('/assets-prefix/', $resolver->resolvePublicPath('')); $this->assertSame('/assets-prefix/foo/bar', $resolver->resolvePublicPath('/foo/bar')); $this->assertSame('/assets-prefix/foo/bar', $resolver->resolvePublicPath('foo/bar')); $resolver = new PublicAssetsPathResolver( - '/projectRootDir/', '/assets-prefix', // The trailing slash should be added automatically - 'publicDirName', ); $this->assertSame('/assets-prefix/', $resolver->resolvePublicPath('')); $this->assertSame('/assets-prefix/foo/bar', $resolver->resolvePublicPath('/foo/bar')); $this->assertSame('/assets-prefix/foo/bar', $resolver->resolvePublicPath('foo/bar')); } - - public function testGetPublicFilesystemPath() - { - $resolver = new PublicAssetsPathResolver( - '/path/to/projectRootDir/', - '/assets-prefix', - 'publicDirName', - ); - $this->assertSame('/path/to/projectRootDir/publicDirName/assets-prefix', $resolver->getPublicFilesystemPath()); - - $resolver = new PublicAssetsPathResolver( - '/path/to/projectRootDir', - '/assets-prefix/', - 'publicDirName', - ); - $this->assertSame('/path/to/projectRootDir/publicDirName/assets-prefix', $resolver->getPublicFilesystemPath()); - } }