From da648b1af646a86991d6ad5538bf846679fec5ce Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Tue, 17 Oct 2023 16:30:35 -0400 Subject: [PATCH] [AssetMapper] Split ImportmapManager into 2 --- .../Resources/config/asset_mapper.php | 16 +- .../Command/AssetMapperCompileCommand.php | 16 +- .../Command/ImportMapRequireCommand.php | 1 - .../Compiler/JavaScriptImportPathCompiler.php | 6 +- .../ImportMap/ImportMapConfigReader.php | 7 + .../ImportMap/ImportMapGenerator.php | 255 ++++++ .../ImportMap/ImportMapManager.php | 219 ----- .../ImportMap/ImportMapRenderer.php | 4 +- .../JavaScriptImportPathCompilerTest.php | 14 +- .../Tests/Factory/MappedAssetFactoryTest.php | 4 +- .../ImportMap/ImportMapConfigReaderTest.php | 8 + .../ImportMap/ImportMapGeneratorTest.php | 769 ++++++++++++++++++ .../Tests/ImportMap/ImportMapManagerTest.php | 680 ---------------- .../Tests/ImportMap/ImportMapRendererTest.php | 38 +- 14 files changed, 1091 insertions(+), 946 deletions(-) create mode 100644 src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php create mode 100644 src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php index d31573031c656..046bd8c6c5fde 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php @@ -31,6 +31,7 @@ use Symfony\Component\AssetMapper\Factory\MappedAssetFactory; use Symfony\Component\AssetMapper\ImportMap\ImportMapAuditor; use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; +use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator; use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer; use Symfony\Component\AssetMapper\ImportMap\ImportMapUpdateChecker; @@ -101,7 +102,7 @@ ->args([ service('asset_mapper.public_assets_path_resolver'), service('asset_mapper'), - service('asset_mapper.importmap.manager'), + service('asset_mapper.importmap.generator'), service('filesystem'), param('kernel.project_dir'), abstract_arg('public directory name'), @@ -137,7 +138,7 @@ ->set('asset_mapper.compiler.javascript_import_path_compiler', JavaScriptImportPathCompiler::class) ->args([ - service('asset_mapper.importmap.manager'), + service('asset_mapper.importmap.config_reader'), abstract_arg('missing import mode'), service('logger'), ]) @@ -153,13 +154,19 @@ ->set('asset_mapper.importmap.manager', ImportMapManager::class) ->args([ service('asset_mapper'), - service('asset_mapper.public_assets_path_resolver'), service('asset_mapper.importmap.config_reader'), service('asset_mapper.importmap.remote_package_downloader'), service('asset_mapper.importmap.resolver'), ]) ->alias(ImportMapManager::class, 'asset_mapper.importmap.manager') + ->set('asset_mapper.importmap.generator', ImportMapGenerator::class) + ->args([ + service('asset_mapper'), + service('asset_mapper.public_assets_path_resolver'), + service('asset_mapper.importmap.config_reader'), + ]) + ->set('asset_mapper.importmap.remote_package_storage', RemotePackageStorage::class) ->args([ abstract_arg('vendor directory'), @@ -183,7 +190,7 @@ ->set('asset_mapper.importmap.renderer', ImportMapRenderer::class) ->args([ - service('asset_mapper.importmap.manager'), + service('asset_mapper.importmap.generator'), service('assets.packages')->nullOnInvalid(), param('kernel.charset'), abstract_arg('polyfill URL'), @@ -205,7 +212,6 @@ ->set('asset_mapper.importmap.command.require', ImportMapRequireCommand::class) ->args([ service('asset_mapper.importmap.manager'), - param('kernel.project_dir'), service('asset_mapper.importmap.version_checker'), ]) ->tag('console.command') diff --git a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php index a2fbc2d3e4709..ab5bf73916cfe 100644 --- a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php +++ b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php @@ -14,7 +14,7 @@ use Symfony\Component\AssetMapper\AssetMapper; use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent; -use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; +use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator; use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -38,7 +38,7 @@ final class AssetMapperCompileCommand extends Command public function __construct( private readonly PublicAssetsPathResolverInterface $publicAssetsPathResolver, private readonly AssetMapperInterface $assetMapper, - private readonly ImportMapManager $importMapManager, + private readonly ImportMapGenerator $importMapGenerator, private readonly Filesystem $filesystem, private readonly string $projectDir, private readonly string $publicDirName, @@ -81,12 +81,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $manifestPath = $outputDir.'/'.AssetMapper::MANIFEST_FILE_NAME; $files[] = $manifestPath; - $importMapPath = $outputDir.'/'.ImportMapManager::IMPORT_MAP_CACHE_FILENAME; + $importMapPath = $outputDir.'/'.ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME; $files[] = $importMapPath; $entrypointFilePaths = []; - foreach ($this->importMapManager->getEntrypointNames() as $entrypointName) { - $dumpedEntrypointPath = $outputDir.'/'.sprintf(ImportMapManager::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName); + foreach ($this->importMapGenerator->getEntrypointNames() as $entrypointName) { + $dumpedEntrypointPath = $outputDir.'/'.sprintf(ImportMapGenerator::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName); $files[] = $dumpedEntrypointPath; $entrypointFilePaths[$entrypointName] = $dumpedEntrypointPath; } @@ -105,12 +105,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->filesystem->dumpFile($manifestPath, json_encode($manifest, \JSON_PRETTY_PRINT)); $io->comment(sprintf('Manifest written to %s', $this->shortenPath($manifestPath))); - $this->filesystem->dumpFile($importMapPath, json_encode($this->importMapManager->getRawImportMapData(), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG)); + $this->filesystem->dumpFile($importMapPath, json_encode($this->importMapGenerator->getRawImportMapData(), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG)); $io->comment(sprintf('Import map data written to %s.', $this->shortenPath($importMapPath))); - $entrypointNames = $this->importMapManager->getEntrypointNames(); + $entrypointNames = $this->importMapGenerator->getEntrypointNames(); foreach ($entrypointFilePaths as $entrypointName => $path) { - $this->filesystem->dumpFile($path, json_encode($this->importMapManager->getEntrypointMetadata($entrypointName), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG)); + $this->filesystem->dumpFile($path, json_encode($this->importMapGenerator->findEagerEntrypointImports($entrypointName), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG)); } $styledEntrypointNames = array_map(fn (string $entrypointName) => sprintf('%s', $entrypointName), $entrypointNames); $io->comment(sprintf('Entrypoint metadata written for %d entrypoints (%s).', \count($entrypointNames), implode(', ', $styledEntrypointNames))); diff --git a/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php b/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php index a7402ee92020a..19b5dfbbe4ba6 100644 --- a/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php +++ b/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php @@ -33,7 +33,6 @@ final class ImportMapRequireCommand extends Command public function __construct( private readonly ImportMapManager $importMapManager, - private readonly string $projectDir, private readonly ImportMapVersionChecker $importMapVersionChecker, ) { parent::__construct(); diff --git a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php index f1f33d71eedc0..481cbb9b90a69 100644 --- a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php +++ b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php @@ -15,7 +15,7 @@ use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\Exception\CircularAssetsException; use Symfony\Component\AssetMapper\Exception\RuntimeException; -use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\AssetMapper\ImportMap\JavaScriptImport; use Symfony\Component\AssetMapper\MappedAsset; @@ -32,7 +32,7 @@ final class JavaScriptImportPathCompiler implements AssetCompilerInterface private const IMPORT_PATTERN = '/(?:import\s*(?:(?:\*\s*as\s+\w+|[\w\s{},*]+)\s*from\s*)?|\bimport\()\s*[\'"`](\.\/[^\'"`]+|(\.\.\/)*[^\'"`]+)[\'"`]\s*[;\)]?/m'; public function __construct( - private readonly ImportMapManager $importMapManager, + private readonly ImportMapConfigReader $importMapConfigReader, private readonly string $missingImportMode = self::MISSING_IMPORT_WARN, private readonly ?LoggerInterface $logger = null, ) { @@ -139,7 +139,7 @@ private function isCommentedOut(mixed $offsetStart, string $fullContent): bool private function findAssetForBareImport(string $importedModule, AssetMapperInterface $assetMapper): ?MappedAsset { - if (!$importMapEntry = $this->importMapManager->findRootImportMapEntry($importedModule)) { + if (!$importMapEntry = $this->importMapConfigReader->findRootImportMapEntry($importedModule)) { // don't warn on missing non-relative (bare) imports: these could be valid URLs return null; diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php index 8aaee7a3e1646..d282fe1bac747 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php @@ -130,6 +130,13 @@ public function writeEntries(ImportMapEntries $entries): void EOF); } + public function findRootImportMapEntry(string $moduleName): ?ImportMapEntry + { + $entries = $this->getEntries(); + + return $entries->has($moduleName) ? $entries->get($moduleName) : null; + } + public function createRemoteEntry(string $importName, ImportMapType $type, string $version, string $packageModuleSpecifier, bool $isEntrypoint): ImportMapEntry { $path = $this->remotePackageStorage->getDownloadPath($packageModuleSpecifier, $type); diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php new file mode 100644 index 0000000000000..bca5897c03c53 --- /dev/null +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php @@ -0,0 +1,255 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper\ImportMap; + +use Symfony\Component\AssetMapper\AssetMapperInterface; +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. + */ +class ImportMapGenerator +{ + public const IMPORT_MAP_CACHE_FILENAME = 'importmap.json'; + public const ENTRYPOINT_CACHE_FILENAME_PATTERN = 'entrypoint.%s.json'; + + public function __construct( + private readonly AssetMapperInterface $assetMapper, + private readonly PublicAssetsPathResolverInterface $assetsPathResolver, + private readonly ImportMapConfigReader $importMapConfigReader, + ) { + } + + /** + * @internal + */ + public function getEntrypointNames(): array + { + $rootEntries = $this->importMapConfigReader->getEntries(); + $entrypointNames = []; + foreach ($rootEntries as $entry) { + if ($entry->isEntrypoint) { + $entrypointNames[] = $entry->importName; + } + } + + return $entrypointNames; + } + + /** + * @param string[] $entrypointNames + * + * @return array + * + * @internal + */ + public function getImportMapData(array $entrypointNames): array + { + $rawImportMapData = $this->getRawImportMapData(); + $finalImportMapData = []; + foreach ($entrypointNames as $entrypointName) { + $entrypointImports = $this->findEagerEntrypointImports($entrypointName); + // Entrypoint modules must be preloaded before their dependencies + foreach ([$entrypointName, ...$entrypointImports] as $import) { + if (isset($finalImportMapData[$import])) { + continue; + } + + // Missing dependency - rely on browser or compilers to warn + if (!isset($rawImportMapData[$import])) { + continue; + } + + $finalImportMapData[$import] = $rawImportMapData[$import]; + $finalImportMapData[$import]['preload'] = true; + unset($rawImportMapData[$import]); + } + } + + return array_merge($finalImportMapData, $rawImportMapData); + } + + /** + * @internal + * + * @return 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); + } + + $allEntries = []; + foreach ($this->importMapConfigReader->getEntries() as $rootEntry) { + $allEntries[$rootEntry->importName] = $rootEntry; + $allEntries = $this->addImplicitEntries($rootEntry, $allEntries); + } + + $rawImportMapData = []; + foreach ($allEntries as $entry) { + $asset = $this->findAsset($entry->path); + if (!$asset) { + throw $this->createMissingImportMapAssetException($entry); + } + + $path = $asset->publicPath; + $data = ['path' => $path, 'type' => $entry->type->value]; + $rawImportMapData[$entry->importName] = $data; + } + + return $rawImportMapData; + } + + /** + * Given an importmap entry name, finds all the non-lazy module imports in its chain. + * + * @internal + * + * @return array The array of import names + */ + 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); + } + + $rootImportEntries = $this->importMapConfigReader->getEntries(); + if (!$rootImportEntries->has($entryName)) { + throw new \InvalidArgumentException(sprintf('The entrypoint "%s" does not exist in "importmap.php".', $entryName)); + } + + if (!$rootImportEntries->get($entryName)->isEntrypoint) { + throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is not an entry point in "importmap.php". Set "entrypoint" => true to make it available as an entrypoint.', $entryName)); + } + + if ($rootImportEntries->get($entryName)->isRemotePackage()) { + throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is a remote package and cannot be used as an entrypoint.', $entryName)); + } + + $asset = $this->findAsset($rootImportEntries->get($entryName)->path); + if (!$asset) { + throw new \InvalidArgumentException(sprintf('The path "%s" of the entrypoint "%s" mentioned in "importmap.php" cannot be found in any asset map paths.', $rootImportEntries->get($entryName)->path, $entryName)); + } + + return $this->findEagerImports($asset); + } + + /** + * Adds "implicit" entries to the importmap. + * + * This recursively searches the dependencies of the given entry + * (i.e. it looks for modules imported from other modules) + * and adds them to the importmap. + * + * @param array $currentImportEntries + * + * @return array + */ + private function addImplicitEntries(ImportMapEntry $entry, array $currentImportEntries): array + { + // only process import dependencies for JS files + if (ImportMapType::JS !== $entry->type) { + return $currentImportEntries; + } + + if (!$asset = $this->findAsset($entry->path)) { + // should only be possible at this point for root importmap.php entries + throw $this->createMissingImportMapAssetException($entry); + } + + foreach ($asset->getJavaScriptImports() as $javaScriptImport) { + $importName = $javaScriptImport->importName; + + if (isset($currentImportEntries[$importName])) { + // entry already exists + continue; + } + + // check if this import requires an automatic importmap entry + if ($javaScriptImport->addImplicitlyToImportMap && $javaScriptImport->asset) { + $nextEntry = ImportMapEntry::createLocal( + $importName, + ImportMapType::tryFrom($javaScriptImport->asset->publicExtension) ?: ImportMapType::JS, + $javaScriptImport->asset->logicalPath, + false, + ); + + $currentImportEntries[$importName] = $nextEntry; + } else { + $nextEntry = $this->importMapConfigReader->findRootImportMapEntry($importName); + } + + // unless there was some missing importmap entry, recurse + if ($nextEntry) { + $currentImportEntries = $this->addImplicitEntries($nextEntry, $currentImportEntries); + } + } + + 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. + */ + private function findAsset(string $path): ?MappedAsset + { + if ($asset = $this->assetMapper->getAsset($path)) { + return $asset; + } + + if (str_starts_with($path, '.')) { + $path = $this->importMapConfigReader->getRootDirectory().'/'.$path; + } + + return $this->assetMapper->getAssetFromSourcePath($path); + } + + private function findEagerImports(MappedAsset $asset): array + { + $dependencies = []; + foreach ($asset->getJavaScriptImports() as $javaScriptImport) { + if ($javaScriptImport->isLazy) { + continue; + } + + $dependencies[] = $javaScriptImport->importName; + + // the import is for a MappedAsset? Follow its imports! + if ($javaScriptImport->asset) { + $dependencies = array_merge($dependencies, $this->findEagerImports($javaScriptImport->asset)); + } + } + + return $dependencies; + } + + private function createMissingImportMapAssetException(ImportMapEntry $entry): \InvalidArgumentException + { + if ($entry->isRemotePackage()) { + throw new LogicException(sprintf('The "%s" vendor asset is missing. Try running the "importmap:install" command.', $entry->importName)); + } + + throw new LogicException(sprintf('The asset "%s" cannot be found in any asset map paths.', $entry->path)); + } +} diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php index fc6a27bcc2dc8..4d06087a5542e 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php @@ -12,10 +12,8 @@ namespace Symfony\Component\AssetMapper\ImportMap; use Symfony\Component\AssetMapper\AssetMapperInterface; -use Symfony\Component\AssetMapper\Exception\LogicException; use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; use Symfony\Component\AssetMapper\MappedAsset; -use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; /** * @author Kévin Dunglas @@ -25,12 +23,8 @@ */ class ImportMapManager { - public const IMPORT_MAP_CACHE_FILENAME = 'importmap.json'; - public const ENTRYPOINT_CACHE_FILENAME_PATTERN = 'entrypoint.%s.json'; - public function __construct( private readonly AssetMapperInterface $assetMapper, - private readonly PublicAssetsPathResolverInterface $assetsPathResolver, private readonly ImportMapConfigReader $importMapConfigReader, private readonly RemotePackageDownloader $packageDownloader, private readonly PackageResolverInterface $resolver, @@ -69,104 +63,6 @@ public function update(array $packages = []): array return $this->updateImportMapConfig(true, [], [], $packages); } - public function findRootImportMapEntry(string $moduleName): ?ImportMapEntry - { - $entries = $this->importMapConfigReader->getEntries(); - - return $entries->has($moduleName) ? $entries->get($moduleName) : null; - } - - /** - * @internal - * - * @param string[] $entrypointNames - * - * @return array - */ - public function getImportMapData(array $entrypointNames): array - { - $rawImportMapData = $this->getRawImportMapData(); - $finalImportMapData = []; - foreach ($entrypointNames as $entrypointName) { - $entrypointImports = $this->findEagerEntrypointImports($entrypointName); - // Entrypoint modules must be preloaded before their dependencies - foreach ([$entrypointName, ...$entrypointImports] as $import) { - if (isset($finalImportMapData[$import])) { - continue; - } - - // Missing dependency - rely on browser or compilers to warn - if (!isset($rawImportMapData[$import])) { - continue; - } - - $finalImportMapData[$import] = $rawImportMapData[$import]; - $finalImportMapData[$import]['preload'] = true; - unset($rawImportMapData[$import]); - } - } - - return array_merge($finalImportMapData, $rawImportMapData); - } - - /** - * @internal - */ - public function getEntrypointMetadata(string $entrypointName): array - { - return $this->findEagerEntrypointImports($entrypointName); - } - - /** - * @internal - */ - public function getEntrypointNames(): array - { - $rootEntries = $this->importMapConfigReader->getEntries(); - $entrypointNames = []; - foreach ($rootEntries as $entry) { - if ($entry->isEntrypoint) { - $entrypointNames[] = $entry->importName; - } - } - - return $entrypointNames; - } - - /** - * @internal - * - * @return 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); - } - - $rootEntries = $this->importMapConfigReader->getEntries(); - $allEntries = []; - foreach ($rootEntries as $rootEntry) { - $allEntries[$rootEntry->importName] = $rootEntry; - $allEntries = $this->addImplicitEntries($rootEntry, $allEntries, $rootEntries); - } - - $rawImportMapData = []; - foreach ($allEntries as $entry) { - $asset = $this->findAsset($entry->path); - if (!$asset) { - throw $this->createMissingImportMapAssetException($entry); - } - - $path = $asset->publicPath; - $data = ['path' => $path, 'type' => $entry->type->value]; - $rawImportMapData[$entry->importName] = $data; - } - - return $rawImportMapData; - } - /** * @internal */ @@ -303,112 +199,6 @@ private function cleanupPackageFiles(ImportMapEntry $entry): void } } - /** - * Adds "implicit" entries to the importmap. - * - * This recursively searches the dependencies of the given entry - * (i.e. it looks for modules imported from other modules) - * and adds them to the importmap. - * - * @param array $currentImportEntries - * - * @return array - */ - private function addImplicitEntries(ImportMapEntry $entry, array $currentImportEntries, ImportMapEntries $rootEntries): array - { - // only process import dependencies for JS files - if (ImportMapType::JS !== $entry->type) { - return $currentImportEntries; - } - - if (!$asset = $this->findAsset($entry->path)) { - // should only be possible at this point for root importmap.php entries - throw $this->createMissingImportMapAssetException($entry); - } - - foreach ($asset->getJavaScriptImports() as $javaScriptImport) { - $importName = $javaScriptImport->importName; - - if (isset($currentImportEntries[$importName])) { - // entry already exists - continue; - } - - // check if this import requires an automatic importmap entry - if ($javaScriptImport->addImplicitlyToImportMap && $javaScriptImport->asset) { - $nextEntry = ImportMapEntry::createLocal( - $importName, - ImportMapType::tryFrom($javaScriptImport->asset->publicExtension) ?: ImportMapType::JS, - $javaScriptImport->asset->logicalPath, - false, - ); - - $currentImportEntries[$importName] = $nextEntry; - } else { - $nextEntry = $this->findRootImportMapEntry($importName); - } - - // unless there was some missing importmap entry, recurse - if ($nextEntry) { - $currentImportEntries = $this->addImplicitEntries($nextEntry, $currentImportEntries, $rootEntries); - } - } - - return $currentImportEntries; - } - - /** - * Given an importmap entry name, finds all the non-lazy module imports in its chain. - * - * @return array The array of import names - */ - private 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); - } - - $rootImportEntries = $this->importMapConfigReader->getEntries(); - if (!$rootImportEntries->has($entryName)) { - throw new \InvalidArgumentException(sprintf('The entrypoint "%s" does not exist in "importmap.php".', $entryName)); - } - - if (!$rootImportEntries->get($entryName)->isEntrypoint) { - throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is not an entry point in "importmap.php". Set "entrypoint" => true to make it available as an entrypoint.', $entryName)); - } - - if ($rootImportEntries->get($entryName)->isRemotePackage()) { - throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is a remote package and cannot be used as an entrypoint.', $entryName)); - } - - $asset = $this->findAsset($rootImportEntries->get($entryName)->path); - if (!$asset) { - throw new \InvalidArgumentException(sprintf('The path "%s" of the entrypoint "%s" mentioned in "importmap.php" cannot be found in any asset map paths.', $rootImportEntries->get($entryName)->path, $entryName)); - } - - return $this->findEagerImports($asset); - } - - private function findEagerImports(MappedAsset $asset): array - { - $dependencies = []; - foreach ($asset->getJavaScriptImports() as $javaScriptImport) { - if ($javaScriptImport->isLazy) { - continue; - } - - $dependencies[] = $javaScriptImport->importName; - - // the import is for a MappedAsset? Follow its imports! - if ($javaScriptImport->asset) { - $dependencies = array_merge($dependencies, $this->findEagerImports($javaScriptImport->asset)); - } - } - - return $dependencies; - } - private static function getImportMapTypeFromFilename(string $path): ImportMapType { return str_ends_with($path, '.css') ? ImportMapType::CSS : ImportMapType::JS; @@ -429,13 +219,4 @@ private function findAsset(string $path): ?MappedAsset return $this->assetMapper->getAssetFromSourcePath($path); } - - private function createMissingImportMapAssetException(ImportMapEntry $entry): \InvalidArgumentException - { - if ($entry->isRemotePackage()) { - throw new LogicException(sprintf('The "%s" vendor asset is missing. Try running the "importmap:install" command.', $entry->importName)); - } - - throw new LogicException(sprintf('The asset "%s" cannot be found in any asset map paths.', $entry->path)); - } } diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php index ca5fe2e6b2888..839bac2b2ef37 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php @@ -30,7 +30,7 @@ class ImportMapRenderer private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL = 'https://ga.jspm.io/npm:es-module-shims@1.8.0/dist/es-module-shims.js'; public function __construct( - private readonly ImportMapManager $importMapManager, + private readonly ImportMapGenerator $importMapGenerator, private readonly ?Packages $assetPackages = null, private readonly string $charset = 'UTF-8', private readonly string|false $polyfillImportName = false, @@ -43,7 +43,7 @@ public function render(string|array $entryPoint, array $attributes = []): string { $entryPoint = (array) $entryPoint; - $importMapData = $this->importMapManager->getImportMapData($entryPoint); + $importMapData = $this->importMapGenerator->getImportMapData($entryPoint); $importMap = []; $modulePreloads = []; $cssLinks = []; diff --git a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php index bf4c79e25bc1c..ba3a7f7e419ff 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php @@ -18,8 +18,8 @@ use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler; use Symfony\Component\AssetMapper\Exception\CircularAssetsException; use Symfony\Component\AssetMapper\Exception\RuntimeException; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; -use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; use Symfony\Component\AssetMapper\ImportMap\ImportMapType; use Symfony\Component\AssetMapper\MappedAsset; @@ -32,8 +32,8 @@ public function testCompile(string $sourceLogicalName, string $input, array $exp { $asset = new MappedAsset($sourceLogicalName, 'anything', '/assets/'.$sourceLogicalName); - $importMapManager = $this->createMock(ImportMapManager::class); - $importMapManager->expects($this->any()) + $importMapConfigReader = $this->createMock(ImportMapConfigReader::class); + $importMapConfigReader->expects($this->any()) ->method('findRootImportMapEntry') ->willReturnCallback(function ($importName) { return match ($importName) { @@ -43,7 +43,7 @@ public function testCompile(string $sourceLogicalName, string $input, array $exp default => null, }; }); - $compiler = new JavaScriptImportPathCompiler($importMapManager); + $compiler = new JavaScriptImportPathCompiler($importMapConfigReader); // compile - and check that content doesn't change $this->assertSame($input, $compiler->compile($input, $asset, $this->createAssetMapper())); $actualImports = []; @@ -311,7 +311,7 @@ public function testImportPathsCanUpdate(string $sourceLogicalName, string $inpu ->method('getAsset') ->willReturn($importedAsset); - $compiler = new JavaScriptImportPathCompiler($this->createMock(ImportMapManager::class)); + $compiler = new JavaScriptImportPathCompiler($this->createMock(ImportMapConfigReader::class)); $this->assertSame($expectedOutput, $compiler->compile($input, $asset, $assetMapper)); } @@ -380,7 +380,7 @@ public function testMissingImportMode(string $sourceLogicalName, string $input, $logger = $this->createMock(LoggerInterface::class); $compiler = new JavaScriptImportPathCompiler( - $this->createMock(ImportMapManager::class), + $this->createMock(ImportMapConfigReader::class), AssetCompilerInterface::MISSING_IMPORT_STRICT, $logger ); @@ -430,7 +430,7 @@ public function testErrorMessageAvoidsCircularException() }); $asset = new MappedAsset('htmx.js', '/path/to/app.js'); - $compiler = new JavaScriptImportPathCompiler($this->createMock(ImportMapManager::class)); + $compiler = new JavaScriptImportPathCompiler($this->createMock(ImportMapConfigReader::class)); $content = '//** @type {import("./htmx").HtmxApi} */'; $compiled = $compiler->compile($content, $asset, $assetMapper); // To form a good exception message, the compiler will check for the diff --git a/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php b/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php index d4131ae39c377..fd211dbf1d14d 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php @@ -20,7 +20,7 @@ use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler; use Symfony\Component\AssetMapper\Exception\RuntimeException; use Symfony\Component\AssetMapper\Factory\MappedAssetFactory; -use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\AssetMapper\MappedAsset; use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; @@ -124,7 +124,7 @@ public function testCreateMappedAssetInVendor() private function createFactory(AssetCompilerInterface $extraCompiler = null): MappedAssetFactory { $compilers = [ - new JavaScriptImportPathCompiler($this->createMock(ImportMapManager::class)), + new JavaScriptImportPathCompiler($this->createMock(ImportMapConfigReader::class)), new CssAssetUrlCompiler(), ]; if ($extraCompiler) { diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php index 5cfbf76c5e70b..a70da9dc22aa2 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php @@ -114,4 +114,12 @@ public function testGetRootDirectory() $configReader = new ImportMapConfigReader(__DIR__.'/../fixtures/importmap.php', $this->createMock(RemotePackageStorage::class)); $this->assertSame(__DIR__.'/../fixtures', $configReader->getRootDirectory()); } + + public function testFindRootImportMapEntry() + { + $configReader = new ImportMapConfigReader(__DIR__.'/../fixtures/importmap.php', $this->createMock(RemotePackageStorage::class)); + $entry = $configReader->findRootImportMapEntry('file2'); + $this->assertSame('file2', $entry->importName); + $this->assertSame('file2.js', $entry->path); + } } diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php new file mode 100644 index 0000000000000..036d1449b2311 --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php @@ -0,0 +1,769 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper\Tests\ImportMap; + +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; +use Symfony\Component\AssetMapper\ImportMap\ImportMapEntries; +use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; +use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator; +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 ImportMapConfigReader&MockObject $configReader; + private ImportMapGenerator $importMapGenerator; + + 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); + } + if (!file_exists(__DIR__.'/../fixtures/importmaps_for_writing/assets')) { + $this->filesystem->mkdir(self::$writableRoot.'/assets'); + } + } + + protected function tearDown(): void + { + $this->filesystem->remove(self::$writableRoot); + } + + public function testGetEntrypointNames() + { + $manager = $this->createImportMapGenerator(); + $this->mockImportMap([ + ImportMapEntry::createLocal('entry1', ImportMapType::JS, path: '/any', isEntrypoint: true), + ImportMapEntry::createLocal('entry2', ImportMapType::JS, path: '/any', isEntrypoint: true), + ImportMapEntry::createLocal('not_entrypoint', ImportMapType::JS, path: '/any', isEntrypoint: false), + ]); + + $this->assertEquals(['entry1', 'entry2'], $manager->getEntrypointNames()); + } + + public function testGetImportMapData() + { + $manager = $this->createImportMapGenerator(); + $this->mockImportMap([ + self::createLocalEntry( + 'entry1', + path: 'entry1.js', + isEntrypoint: true, + ), + self::createLocalEntry( + 'entry2', + path: 'entry2.js', + isEntrypoint: true, + ), + self::createLocalEntry( + 'entry3', + path: 'entry3.js', + isEntrypoint: true, + ), + self::createLocalEntry( + 'normal_js_file', + path: 'normal_js_file.js', + ), + self::createLocalEntry( + 'css_in_importmap', + path: 'styles/css_in_importmap.css', + type: ImportMapType::CSS, + ), + self::createLocalEntry( + 'never_imported_css', + path: 'styles/never_imported_css.css', + type: ImportMapType::CSS, + ), + ]); + + $importedFile1 = new MappedAsset( + 'imported_file1.js', + publicPathWithoutDigest: '/assets/imported_file1.js', + publicPath: '/assets/imported_file1-d1g35t.js', + ); + $importedFile2 = new MappedAsset( + 'imported_file2.js', + publicPathWithoutDigest: '/assets/imported_file2.js', + publicPath: '/assets/imported_file2-d1g35t.js', + ); + $importedFile3 = new MappedAsset( + 'imported_file3.js', + publicPathWithoutDigest: '/assets/imported_file3.js', + publicPath: '/assets/imported_file3-d1g35t.js', + ); + $normalJsFile = new MappedAsset( + 'normal_js_file.js', + publicPathWithoutDigest: '/assets/normal_js_file.js', + publicPath: '/assets/normal_js_file-d1g35t.js', + ); + $importedCss1 = new MappedAsset( + 'styles/file1.css', + publicPathWithoutDigest: '/assets/styles/file1.css', + publicPath: '/assets/styles/file1-d1g35t.css', + ); + $importedCss2 = new MappedAsset( + 'styles/file2.css', + publicPathWithoutDigest: '/assets/styles/file2.css', + publicPath: '/assets/styles/file2-d1g35t.css', + ); + $importedCssInImportmap = new MappedAsset( + 'styles/css_in_importmap.css', + publicPathWithoutDigest: '/assets/styles/css_in_importmap.css', + publicPath: '/assets/styles/css_in_importmap-d1g35t.css', + ); + $neverImportedCss = new MappedAsset( + 'styles/never_imported_css.css', + publicPathWithoutDigest: '/assets/styles/never_imported_css.css', + publicPath: '/assets/styles/never_imported_css-d1g35t.css', + ); + $this->mockAssetMapper([ + new MappedAsset( + 'entry1.js', + publicPath: '/assets/entry1-d1g35t.js', + javaScriptImports: [ + new JavaScriptImport('/assets/imported_file1.js', isLazy: false, asset: $importedFile1, addImplicitlyToImportMap: true), + new JavaScriptImport('/assets/styles/file1.css', isLazy: false, asset: $importedCss1, addImplicitlyToImportMap: true), + new JavaScriptImport('normal_js_file', isLazy: false, asset: $normalJsFile), + ] + ), + new MappedAsset( + 'entry2.js', + publicPath: '/assets/entry2-d1g35t.js', + javaScriptImports: [ + new JavaScriptImport('/assets/imported_file2.js', isLazy: false, asset: $importedFile2, addImplicitlyToImportMap: true), + new JavaScriptImport('css_in_importmap', isLazy: false, asset: $importedCssInImportmap), + new JavaScriptImport('/assets/styles/file2.css', isLazy: false, asset: $importedCss2, addImplicitlyToImportMap: true), + ] + ), + new MappedAsset( + 'entry3.js', + publicPath: '/assets/entry3-d1g35t.js', + javaScriptImports: [ + new JavaScriptImport('/assets/imported_file3.js', isLazy: false, asset: $importedFile3), + ], + ), + $importedFile1, + $importedFile2, + // $importedFile3, + $normalJsFile, + $importedCss1, + $importedCss2, + $importedCssInImportmap, + $neverImportedCss, + ]); + + $actualImportMapData = $manager->getImportMapData(['entry2', 'entry1']); + + $this->assertEquals([ + 'entry1' => [ + 'path' => '/assets/entry1-d1g35t.js', + 'type' => 'js', + 'preload' => true, // Rendered entry points are preloaded + ], + '/assets/imported_file1.js' => [ + 'path' => '/assets/imported_file1-d1g35t.js', + 'type' => 'js', + 'preload' => true, + ], + 'entry2' => [ + 'path' => '/assets/entry2-d1g35t.js', + 'type' => 'js', + 'preload' => true, // Rendered entry points are preloaded + ], + '/assets/imported_file2.js' => [ + 'path' => '/assets/imported_file2-d1g35t.js', + 'type' => 'js', + 'preload' => true, + ], + 'normal_js_file' => [ + 'path' => '/assets/normal_js_file-d1g35t.js', + 'type' => 'js', + 'preload' => true, // preloaded as it's a non-lazy dependency of an entry + ], + '/assets/styles/file1.css' => [ + 'path' => '/assets/styles/file1-d1g35t.css', + 'type' => 'css', + 'preload' => true, + ], + '/assets/styles/file2.css' => [ + 'path' => '/assets/styles/file2-d1g35t.css', + 'type' => 'css', + 'preload' => true, + ], + 'css_in_importmap' => [ + 'path' => '/assets/styles/css_in_importmap-d1g35t.css', + 'type' => 'css', + 'preload' => true, + ], + 'entry3' => [ + 'path' => '/assets/entry3-d1g35t.js', + 'type' => 'js', // No preload (entry point not "rendered") + ], + 'never_imported_css' => [ + 'path' => '/assets/styles/never_imported_css-d1g35t.css', + 'type' => 'css', + ], + ], $actualImportMapData); + + // now check the order + $this->assertEquals([ + // entry2 & its dependencies + 'entry2', + '/assets/imported_file2.js', + 'css_in_importmap', // in the importmap, but brought earlier because it's a dependency of entry2 + '/assets/styles/file2.css', + + // entry1 & its dependencies + 'entry1', + '/assets/imported_file1.js', + '/assets/styles/file1.css', + 'normal_js_file', + + // importmap entries never imported + 'entry3', + 'never_imported_css', + ], array_keys($actualImportMapData)); + } + + /** + * @dataProvider getRawImportMapDataTests + */ + public function testGetRawImportMapData(array $importMapEntries, array $mappedAssets, array $expectedData) + { + $manager = $this->createImportMapGenerator(); + $this->mockImportMap($importMapEntries); + $this->mockAssetMapper($mappedAssets); + $this->configReader->expects($this->any()) + ->method('getRootDirectory') + ->willReturn('/fake/root'); + + $this->assertEquals($expectedData, $manager->getRawImportMapData()); + } + + public function getRawImportMapDataTests(): iterable + { + yield 'it returns remote downloaded entry' => [ + [ + self::createRemoteEntry( + '@hotwired/stimulus', + version: '1.2.3', + path: '/assets/vendor/stimulus.js' + ), + ], + [ + new MappedAsset( + 'vendor/@hotwired/stimulus.js', + '/assets/vendor/stimulus.js', + publicPath: '/assets/vendor/@hotwired/stimulus-d1g35t.js', + ), + ], + [ + '@hotwired/stimulus' => [ + 'path' => '/assets/vendor/@hotwired/stimulus-d1g35t.js', + 'type' => 'js', + ], + ], + ]; + + yield 'it returns basic local javascript file' => [ + [ + self::createLocalEntry( + 'app', + path: 'app.js' + ), + ], + [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app-d13g35t.js', + ), + ], + [ + 'app' => [ + 'path' => '/assets/app-d13g35t.js', + 'type' => 'js', + ], + ], + ]; + + yield 'it returns basic local css file' => [ + [ + self::createLocalEntry( + 'app.css', + path: 'styles/app.css', + type: ImportMapType::CSS, + ), + ], + [ + new MappedAsset( + 'styles/app.css', + publicPath: '/assets/styles/app-d13g35t.css', + ), + ], + [ + 'app.css' => [ + 'path' => '/assets/styles/app-d13g35t.css', + 'type' => 'css', + ], + ], + ]; + + $simpleAsset = new MappedAsset( + 'simple.js', + publicPathWithoutDigest: '/assets/simple.js', + publicPath: '/assets/simple-d1g3st.js', + ); + yield 'it adds dependency to the importmap' => [ + [ + self::createLocalEntry( + 'app', + path: 'app.js', + ), + ], + [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app-d1g3st.js', + javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)] + ), + $simpleAsset, + ], + [ + 'app' => [ + 'path' => '/assets/app-d1g3st.js', + 'type' => 'js', + ], + '/assets/simple.js' => [ + 'path' => '/assets/simple-d1g3st.js', + 'type' => 'js', + ], + ], + ]; + + yield 'it adds dependency to the importmap from a remote asset' => [ + [ + self::createRemoteEntry( + 'bootstrap', + version: '1.2.3', + path: '/assets/vendor/bootstrap.js' + ), + ], + [ + new MappedAsset( + 'app.js', + sourcePath: '/assets/vendor/bootstrap.js', + publicPath: '/assets/vendor/bootstrap-d1g3st.js', + javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)] + ), + $simpleAsset, + ], + [ + 'bootstrap' => [ + 'path' => '/assets/vendor/bootstrap-d1g3st.js', + 'type' => 'js', + ], + '/assets/simple.js' => [ + 'path' => '/assets/simple-d1g3st.js', + 'type' => 'js', + ], + ], + ]; + + $eagerImportsSimpleAsset = new MappedAsset( + 'imports_simple.js', + publicPathWithoutDigest: '/assets/imports_simple.js', + publicPath: '/assets/imports_simple-d1g3st.js', + javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)] + ); + yield 'it processes imports recursively' => [ + [ + self::createLocalEntry( + 'app', + path: 'app.js', + ), + ], + [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app-d1g3st.js', + javaScriptImports: [new JavaScriptImport('/assets/imports_simple.js', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: true)] + ), + $eagerImportsSimpleAsset, + $simpleAsset, + ], + [ + 'app' => [ + 'path' => '/assets/app-d1g3st.js', + 'type' => 'js', + ], + '/assets/imports_simple.js' => [ + 'path' => '/assets/imports_simple-d1g3st.js', + 'type' => 'js', + ], + '/assets/simple.js' => [ + 'path' => '/assets/simple-d1g3st.js', + 'type' => 'js', + ], + ], + ]; + + yield 'it process can skip adding one importmap entry but still add a child' => [ + [ + self::createLocalEntry( + 'app', + path: 'app.js', + ), + self::createLocalEntry( + 'imports_simple', + path: 'imports_simple.js', + ), + ], + [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app-d1g3st.js', + javaScriptImports: [new JavaScriptImport('imports_simple', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: false)] + ), + $eagerImportsSimpleAsset, + $simpleAsset, + ], + [ + 'app' => [ + 'path' => '/assets/app-d1g3st.js', + 'type' => 'js', + ], + '/assets/simple.js' => [ + 'path' => '/assets/simple-d1g3st.js', + 'type' => 'js', + ], + 'imports_simple' => [ + 'path' => '/assets/imports_simple-d1g3st.js', + 'type' => 'js', + ], + ], + ]; + + yield 'imports with a module name are not added to the importmap' => [ + [ + self::createLocalEntry( + 'app', + path: 'app.js', + ), + ], + [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app-d1g3st.js', + javaScriptImports: [new JavaScriptImport('simple', isLazy: false, asset: $simpleAsset)] + ), + $simpleAsset, + ], + [ + 'app' => [ + 'path' => '/assets/app-d1g3st.js', + 'type' => 'js', + ], + ], + ]; + + yield 'it does not process dependencies of CSS files' => [ + [ + self::createLocalEntry( + 'app.css', + path: 'app.css', + type: ImportMapType::CSS, + ), + ], + [ + new MappedAsset( + 'app.css', + publicPath: '/assets/app-d1g3st.css', + javaScriptImports: [new JavaScriptImport('/assets/simple.js', asset: $simpleAsset)] + ), + ], + [ + 'app.css' => [ + 'path' => '/assets/app-d1g3st.css', + 'type' => 'css', + ], + ], + ]; + + yield 'it handles a relative path file' => [ + [ + self::createLocalEntry( + 'app', + path: './assets/app.js', + ), + ], + [ + new MappedAsset( + 'app.js', + // /fake/root is the mocked root directory + '/fake/root/assets/app.js', + publicPath: '/assets/app-d1g3st.js', + ), + ], + [ + 'app' => [ + 'path' => '/assets/app-d1g3st.js', + 'type' => 'js', + ], + ], + ]; + + yield 'it handles an absolute path file' => [ + [ + self::createLocalEntry( + 'app', + path: '/some/path/assets/app.js', + ), + ], + [ + new MappedAsset( + 'app.js', + '/some/path/assets/app.js', + publicPath: '/assets/app-d1g3st.js', + ), + ], + [ + 'app' => [ + 'path' => '/assets/app-d1g3st.js', + 'type' => 'js', + ], + ], + ]; + } + + public function testGetRawImportDataUsesCacheFile() + { + $manager = $this->createImportMapGenerator(); + $importmapData = [ + 'app' => [ + 'path' => 'app.js', + 'entrypoint' => true, + ], + '@hotwired/stimulus' => [ + '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->assertEquals($importmapData, $manager->getRawImportMapData()); + } + + /** + * @dataProvider getEagerEntrypointImportsTests + */ + public function testFindEagerEntrypointImports(MappedAsset $entryAsset, array $expected) + { + $manager = $this->createImportMapGenerator(); + $this->mockAssetMapper([$entryAsset]); + // put the entry asset in the importmap + $this->mockImportMap([ + ImportMapEntry::createLocal('the_entrypoint_name', ImportMapType::JS, path: $entryAsset->logicalPath, isEntrypoint: true), + ]); + + $this->assertEquals($expected, $manager->findEagerEntrypointImports('the_entrypoint_name')); + } + + public function getEagerEntrypointImportsTests(): iterable + { + yield 'an entry with no dependencies' => [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app.js', + ), + [], + ]; + + $simpleAsset = new MappedAsset( + 'simple.js', + publicPathWithoutDigest: '/assets/simple.js', + ); + yield 'an entry with a non-lazy dependency is included' => [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app.js', + javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset)] + ), + ['/assets/simple.js'], // path is the key in the importmap + ]; + + yield 'an entry with a non-lazy dependency with module name is included' => [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app.js', + javaScriptImports: [new JavaScriptImport('simple', isLazy: false, asset: $simpleAsset)] + ), + ['simple'], // path is the key in the importmap + ]; + + yield 'an entry with a lazy dependency is not included' => [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app.js', + javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: true, asset: $simpleAsset)] + ), + [], + ]; + + $importsSimpleAsset = new MappedAsset( + 'imports_simple.js', + publicPathWithoutDigest: '/assets/imports_simple.js', + javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset)] + ); + yield 'an entry follows through dependencies recursively' => [ + new MappedAsset( + 'app.js', + publicPath: '/assets/app.js', + javaScriptImports: [new JavaScriptImport('/assets/imports_simple.js', isLazy: false, asset: $importsSimpleAsset)] + ), + ['/assets/imports_simple.js', '/assets/simple.js'], + ]; + } + + public function testFindEagerEntrypointImportsUsesCacheFile() + { + $manager = $this->createImportMapGenerator(); + $entrypointData = [ + '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->assertEquals($entrypointData, $manager->findEagerEntrypointImports('foo')); + } + + private function createImportMapGenerator(): ImportMapGenerator + { + $this->pathResolver = $this->createMock(PublicAssetsPathResolverInterface::class); + $this->assetMapper = $this->createMock(AssetMapperInterface::class); + $this->configReader = $this->createMock(ImportMapConfigReader::class); + + // mock this to behave like normal + $this->configReader->expects($this->any()) + ->method('createRemoteEntry') + ->willReturnCallback(function (string $importName, ImportMapType $type, string $version, string $packageModuleSpecifier, bool $isEntrypoint) { + $path = '/path/to/vendor/'.$packageModuleSpecifier.'.js'; + + return ImportMapEntry::createRemote($importName, $type, $path, $version, $packageModuleSpecifier, $isEntrypoint); + }); + + return $this->importMapGenerator = new ImportMapGenerator( + $this->assetMapper, + $this->pathResolver, + $this->configReader, + ); + } + + private function mockImportMap(array $importMapEntries): void + { + $this->configReader->expects($this->any()) + ->method('getEntries') + ->willReturn(new ImportMapEntries($importMapEntries)) + ; + } + + 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); + } + + private static function createRemoteEntry(string $importName, string $version, string $path = null, ImportMapType $type = ImportMapType::JS, string $packageSpecifier = null): ImportMapEntry + { + $packageSpecifier = $packageSpecifier ?? $importName; + $path = $path ?? '/vendor/any-path.js'; + + return ImportMapEntry::createRemote($importName, $type, path: $path, version: $version, packageModuleSpecifier: $packageSpecifier, isEntrypoint: false); + } + + /** + * @param MappedAsset[] $mappedAssets + */ + private function mockAssetMapper(array $mappedAssets): void + { + $this->assetMapper->expects($this->any()) + ->method('getAsset') + ->willReturnCallback(function (string $logicalPath) use ($mappedAssets) { + foreach ($mappedAssets as $asset) { + if ($asset->logicalPath === $logicalPath) { + return $asset; + } + } + + return null; + }) + ; + + $this->assetMapper->expects($this->any()) + ->method('getAssetFromSourcePath') + ->willReturnCallback(function (string $sourcePath) use ($mappedAssets) { + // collapse ../ in paths and ./ in paths to mimic the realpath AssetMapper uses + $unCollapsePath = function (string $path) { + $parts = explode('/', $path); + $newParts = []; + foreach ($parts as $part) { + if ('..' === $part) { + array_pop($newParts); + + continue; + } + + if ('.' !== $part) { + $newParts[] = $part; + } + } + + return implode('/', $newParts); + }; + + $sourcePath = $unCollapsePath($sourcePath); + + foreach ($mappedAssets as $asset) { + if (isset($asset->sourcePath) && $unCollapsePath($asset->sourcePath) === $sourcePath) { + return $asset; + } + } + + return null; + }) + ; + } +} diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php index 6c42c6df051e3..357350aef9c7b 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php @@ -19,19 +19,16 @@ use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; use Symfony\Component\AssetMapper\ImportMap\ImportMapType; -use Symfony\Component\AssetMapper\ImportMap\JavaScriptImport; use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions; use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader; use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; use Symfony\Component\AssetMapper\ImportMap\Resolver\ResolvedImportMapPackage; use Symfony\Component\AssetMapper\MappedAsset; -use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; use Symfony\Component\Filesystem\Filesystem; class ImportMapManagerTest extends TestCase { private AssetMapperInterface&MockObject $assetMapper; - private PublicAssetsPathResolverInterface&MockObject $pathResolver; private PackageResolverInterface&MockObject $packageResolver; private ImportMapConfigReader&MockObject $configReader; private RemotePackageDownloader&MockObject $remotePackageDownloader; @@ -56,628 +53,6 @@ protected function tearDown(): void $this->filesystem->remove(self::$writableRoot); } - /** - * @dataProvider getRawImportMapDataTests - */ - public function testGetRawImportMapData(array $importMapEntries, array $mappedAssets, array $expectedData) - { - $manager = $this->createImportMapManager(); - $this->mockImportMap($importMapEntries); - $this->mockAssetMapper($mappedAssets); - $this->configReader->expects($this->any()) - ->method('getRootDirectory') - ->willReturn('/fake/root'); - - $this->assertEquals($expectedData, $manager->getRawImportMapData()); - } - - public function getRawImportMapDataTests(): iterable - { - yield 'it returns remote downloaded entry' => [ - [ - self::createRemoteEntry( - '@hotwired/stimulus', - version: '1.2.3', - path: '/assets/vendor/stimulus.js' - ), - ], - [ - new MappedAsset( - 'vendor/@hotwired/stimulus.js', - '/assets/vendor/stimulus.js', - publicPath: '/assets/vendor/@hotwired/stimulus-d1g35t.js', - ), - ], - [ - '@hotwired/stimulus' => [ - 'path' => '/assets/vendor/@hotwired/stimulus-d1g35t.js', - 'type' => 'js', - ], - ], - ]; - - yield 'it returns basic local javascript file' => [ - [ - self::createLocalEntry( - 'app', - path: 'app.js' - ), - ], - [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app-d13g35t.js', - ), - ], - [ - 'app' => [ - 'path' => '/assets/app-d13g35t.js', - 'type' => 'js', - ], - ], - ]; - - yield 'it returns basic local css file' => [ - [ - self::createLocalEntry( - 'app.css', - path: 'styles/app.css', - type: ImportMapType::CSS, - ), - ], - [ - new MappedAsset( - 'styles/app.css', - publicPath: '/assets/styles/app-d13g35t.css', - ), - ], - [ - 'app.css' => [ - 'path' => '/assets/styles/app-d13g35t.css', - 'type' => 'css', - ], - ], - ]; - - $simpleAsset = new MappedAsset( - 'simple.js', - publicPathWithoutDigest: '/assets/simple.js', - publicPath: '/assets/simple-d1g3st.js', - ); - yield 'it adds dependency to the importmap' => [ - [ - self::createLocalEntry( - 'app', - path: 'app.js', - ), - ], - [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app-d1g3st.js', - javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)] - ), - $simpleAsset, - ], - [ - 'app' => [ - 'path' => '/assets/app-d1g3st.js', - 'type' => 'js', - ], - '/assets/simple.js' => [ - 'path' => '/assets/simple-d1g3st.js', - 'type' => 'js', - ], - ], - ]; - - yield 'it adds dependency to the importmap from a remote asset' => [ - [ - self::createRemoteEntry( - 'bootstrap', - version: '1.2.3', - path: '/assets/vendor/bootstrap.js' - ), - ], - [ - new MappedAsset( - 'app.js', - sourcePath: '/assets/vendor/bootstrap.js', - publicPath: '/assets/vendor/bootstrap-d1g3st.js', - javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)] - ), - $simpleAsset, - ], - [ - 'bootstrap' => [ - 'path' => '/assets/vendor/bootstrap-d1g3st.js', - 'type' => 'js', - ], - '/assets/simple.js' => [ - 'path' => '/assets/simple-d1g3st.js', - 'type' => 'js', - ], - ], - ]; - - $eagerImportsSimpleAsset = new MappedAsset( - 'imports_simple.js', - publicPathWithoutDigest: '/assets/imports_simple.js', - publicPath: '/assets/imports_simple-d1g3st.js', - javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)] - ); - yield 'it processes imports recursively' => [ - [ - self::createLocalEntry( - 'app', - path: 'app.js', - ), - ], - [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app-d1g3st.js', - javaScriptImports: [new JavaScriptImport('/assets/imports_simple.js', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: true)] - ), - $eagerImportsSimpleAsset, - $simpleAsset, - ], - [ - 'app' => [ - 'path' => '/assets/app-d1g3st.js', - 'type' => 'js', - ], - '/assets/imports_simple.js' => [ - 'path' => '/assets/imports_simple-d1g3st.js', - 'type' => 'js', - ], - '/assets/simple.js' => [ - 'path' => '/assets/simple-d1g3st.js', - 'type' => 'js', - ], - ], - ]; - - yield 'it process can skip adding one importmap entry but still add a child' => [ - [ - self::createLocalEntry( - 'app', - path: 'app.js', - ), - self::createLocalEntry( - 'imports_simple', - path: 'imports_simple.js', - ), - ], - [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app-d1g3st.js', - javaScriptImports: [new JavaScriptImport('imports_simple', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: false)] - ), - $eagerImportsSimpleAsset, - $simpleAsset, - ], - [ - 'app' => [ - 'path' => '/assets/app-d1g3st.js', - 'type' => 'js', - ], - '/assets/simple.js' => [ - 'path' => '/assets/simple-d1g3st.js', - 'type' => 'js', - ], - 'imports_simple' => [ - 'path' => '/assets/imports_simple-d1g3st.js', - 'type' => 'js', - ], - ], - ]; - - yield 'imports with a module name are not added to the importmap' => [ - [ - self::createLocalEntry( - 'app', - path: 'app.js', - ), - ], - [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app-d1g3st.js', - javaScriptImports: [new JavaScriptImport('simple', isLazy: false, asset: $simpleAsset)] - ), - $simpleAsset, - ], - [ - 'app' => [ - 'path' => '/assets/app-d1g3st.js', - 'type' => 'js', - ], - ], - ]; - - yield 'it does not process dependencies of CSS files' => [ - [ - self::createLocalEntry( - 'app.css', - path: 'app.css', - type: ImportMapType::CSS, - ), - ], - [ - new MappedAsset( - 'app.css', - publicPath: '/assets/app-d1g3st.css', - javaScriptImports: [new JavaScriptImport('/assets/simple.js', asset: $simpleAsset)] - ), - ], - [ - 'app.css' => [ - 'path' => '/assets/app-d1g3st.css', - 'type' => 'css', - ], - ], - ]; - - yield 'it handles a relative path file' => [ - [ - self::createLocalEntry( - 'app', - path: './assets/app.js', - ), - ], - [ - new MappedAsset( - 'app.js', - // /fake/root is the mocked root directory - '/fake/root/assets/app.js', - publicPath: '/assets/app-d1g3st.js', - ), - ], - [ - 'app' => [ - 'path' => '/assets/app-d1g3st.js', - 'type' => 'js', - ], - ], - ]; - - yield 'it handles an absolute path file' => [ - [ - self::createLocalEntry( - 'app', - path: '/some/path/assets/app.js', - ), - ], - [ - new MappedAsset( - 'app.js', - '/some/path/assets/app.js', - publicPath: '/assets/app-d1g3st.js', - ), - ], - [ - 'app' => [ - 'path' => '/assets/app-d1g3st.js', - 'type' => 'js', - ], - ], - ]; - } - - public function testGetRawImportDataUsesCacheFile() - { - $manager = $this->createImportMapManager(); - $importmapData = [ - 'app' => [ - 'path' => 'app.js', - 'entrypoint' => true, - ], - '@hotwired/stimulus' => [ - '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->assertEquals($importmapData, $manager->getRawImportMapData()); - } - - /** - * @dataProvider getEntrypointMetadataTests - */ - public function testGetEntrypointMetadata(MappedAsset $entryAsset, array $expected) - { - $manager = $this->createImportMapManager(); - $this->mockAssetMapper([$entryAsset]); - // put the entry asset in the importmap - $this->mockImportMap([ - ImportMapEntry::createLocal('the_entrypoint_name', ImportMapType::JS, path: $entryAsset->logicalPath, isEntrypoint: true), - ]); - - $this->assertEquals($expected, $manager->getEntrypointMetadata('the_entrypoint_name')); - } - - public function getEntrypointMetadataTests(): iterable - { - yield 'an entry with no dependencies' => [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app.js', - ), - [], - ]; - - $simpleAsset = new MappedAsset( - 'simple.js', - publicPathWithoutDigest: '/assets/simple.js', - ); - yield 'an entry with a non-lazy dependency is included' => [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app.js', - javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset)] - ), - ['/assets/simple.js'], // path is the key in the importmap - ]; - - yield 'an entry with a non-lazy dependency with module name is included' => [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app.js', - javaScriptImports: [new JavaScriptImport('simple', isLazy: false, asset: $simpleAsset)] - ), - ['simple'], // path is the key in the importmap - ]; - - yield 'an entry with a lazy dependency is not included' => [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app.js', - javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: true, asset: $simpleAsset)] - ), - [], - ]; - - $importsSimpleAsset = new MappedAsset( - 'imports_simple.js', - publicPathWithoutDigest: '/assets/imports_simple.js', - javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset)] - ); - yield 'an entry follows through dependencies recursively' => [ - new MappedAsset( - 'app.js', - publicPath: '/assets/app.js', - javaScriptImports: [new JavaScriptImport('/assets/imports_simple.js', isLazy: false, asset: $importsSimpleAsset)] - ), - ['/assets/imports_simple.js', '/assets/simple.js'], - ]; - } - - public function testGetEntrypointMetadataUsesCacheFile() - { - $manager = $this->createImportMapManager(); - $entrypointData = [ - '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->assertEquals($entrypointData, $manager->getEntrypointMetadata('foo')); - } - - public function testGetImportMapData() - { - $manager = $this->createImportMapManager(); - $this->mockImportMap([ - self::createLocalEntry( - 'entry1', - path: 'entry1.js', - isEntrypoint: true, - ), - self::createLocalEntry( - 'entry2', - path: 'entry2.js', - isEntrypoint: true, - ), - self::createLocalEntry( - 'entry3', - path: 'entry3.js', - isEntrypoint: true, - ), - self::createLocalEntry( - 'normal_js_file', - path: 'normal_js_file.js', - ), - self::createLocalEntry( - 'css_in_importmap', - path: 'styles/css_in_importmap.css', - type: ImportMapType::CSS, - ), - self::createLocalEntry( - 'never_imported_css', - path: 'styles/never_imported_css.css', - type: ImportMapType::CSS, - ), - ]); - - $importedFile1 = new MappedAsset( - 'imported_file1.js', - publicPathWithoutDigest: '/assets/imported_file1.js', - publicPath: '/assets/imported_file1-d1g35t.js', - ); - $importedFile2 = new MappedAsset( - 'imported_file2.js', - publicPathWithoutDigest: '/assets/imported_file2.js', - publicPath: '/assets/imported_file2-d1g35t.js', - ); - $importedFile3 = new MappedAsset( - 'imported_file3.js', - publicPathWithoutDigest: '/assets/imported_file3.js', - publicPath: '/assets/imported_file3-d1g35t.js', - ); - $normalJsFile = new MappedAsset( - 'normal_js_file.js', - publicPathWithoutDigest: '/assets/normal_js_file.js', - publicPath: '/assets/normal_js_file-d1g35t.js', - ); - $importedCss1 = new MappedAsset( - 'styles/file1.css', - publicPathWithoutDigest: '/assets/styles/file1.css', - publicPath: '/assets/styles/file1-d1g35t.css', - ); - $importedCss2 = new MappedAsset( - 'styles/file2.css', - publicPathWithoutDigest: '/assets/styles/file2.css', - publicPath: '/assets/styles/file2-d1g35t.css', - ); - $importedCssInImportmap = new MappedAsset( - 'styles/css_in_importmap.css', - publicPathWithoutDigest: '/assets/styles/css_in_importmap.css', - publicPath: '/assets/styles/css_in_importmap-d1g35t.css', - ); - $neverImportedCss = new MappedAsset( - 'styles/never_imported_css.css', - publicPathWithoutDigest: '/assets/styles/never_imported_css.css', - publicPath: '/assets/styles/never_imported_css-d1g35t.css', - ); - $this->mockAssetMapper([ - new MappedAsset( - 'entry1.js', - publicPath: '/assets/entry1-d1g35t.js', - javaScriptImports: [ - new JavaScriptImport('/assets/imported_file1.js', isLazy: false, asset: $importedFile1, addImplicitlyToImportMap: true), - new JavaScriptImport('/assets/styles/file1.css', isLazy: false, asset: $importedCss1, addImplicitlyToImportMap: true), - new JavaScriptImport('normal_js_file', isLazy: false, asset: $normalJsFile), - ] - ), - new MappedAsset( - 'entry2.js', - publicPath: '/assets/entry2-d1g35t.js', - javaScriptImports: [ - new JavaScriptImport('/assets/imported_file2.js', isLazy: false, asset: $importedFile2, addImplicitlyToImportMap: true), - new JavaScriptImport('css_in_importmap', isLazy: false, asset: $importedCssInImportmap), - new JavaScriptImport('/assets/styles/file2.css', isLazy: false, asset: $importedCss2, addImplicitlyToImportMap: true), - ] - ), - new MappedAsset( - 'entry3.js', - publicPath: '/assets/entry3-d1g35t.js', - javaScriptImports: [ - new JavaScriptImport('/assets/imported_file3.js', isLazy: false, asset: $importedFile3), - ], - ), - $importedFile1, - $importedFile2, - // $importedFile3, - $normalJsFile, - $importedCss1, - $importedCss2, - $importedCssInImportmap, - $neverImportedCss, - ]); - - $actualImportMapData = $manager->getImportMapData(['entry2', 'entry1']); - - $this->assertEquals([ - 'entry1' => [ - 'path' => '/assets/entry1-d1g35t.js', - 'type' => 'js', - 'preload' => true, // Rendered entry points are preloaded - ], - '/assets/imported_file1.js' => [ - 'path' => '/assets/imported_file1-d1g35t.js', - 'type' => 'js', - 'preload' => true, - ], - 'entry2' => [ - 'path' => '/assets/entry2-d1g35t.js', - 'type' => 'js', - 'preload' => true, // Rendered entry points are preloaded - ], - '/assets/imported_file2.js' => [ - 'path' => '/assets/imported_file2-d1g35t.js', - 'type' => 'js', - 'preload' => true, - ], - 'normal_js_file' => [ - 'path' => '/assets/normal_js_file-d1g35t.js', - 'type' => 'js', - 'preload' => true, // preloaded as it's a non-lazy dependency of an entry - ], - '/assets/styles/file1.css' => [ - 'path' => '/assets/styles/file1-d1g35t.css', - 'type' => 'css', - 'preload' => true, - ], - '/assets/styles/file2.css' => [ - 'path' => '/assets/styles/file2-d1g35t.css', - 'type' => 'css', - 'preload' => true, - ], - 'css_in_importmap' => [ - 'path' => '/assets/styles/css_in_importmap-d1g35t.css', - 'type' => 'css', - 'preload' => true, - ], - 'entry3' => [ - 'path' => '/assets/entry3-d1g35t.js', - 'type' => 'js', // No preload (entry point not "rendered") - ], - 'never_imported_css' => [ - 'path' => '/assets/styles/never_imported_css-d1g35t.css', - 'type' => 'css', - ], - ], $actualImportMapData); - - // now check the order - $this->assertEquals([ - // entry2 & its dependencies - 'entry2', - '/assets/imported_file2.js', - 'css_in_importmap', // in the importmap, but brought earlier because it's a dependency of entry2 - '/assets/styles/file2.css', - - // entry1 & its dependencies - 'entry1', - '/assets/imported_file1.js', - '/assets/styles/file1.css', - 'normal_js_file', - - // importmap entries never imported - 'entry3', - 'never_imported_css', - ], array_keys($actualImportMapData)); - } - - public function testFindRootImportMapEntry() - { - $manager = $this->createImportMapManager(); - $entry1 = ImportMapEntry::createLocal('entry1', ImportMapType::JS, '/any/path', isEntrypoint: true); - $this->mockImportMap([$entry1]); - - $this->assertSame($entry1, $manager->findRootImportMapEntry('entry1')); - $this->assertNull($manager->findRootImportMapEntry('entry2')); - } - - public function testGetEntrypointNames() - { - $manager = $this->createImportMapManager(); - $this->mockImportMap([ - ImportMapEntry::createLocal('entry1', ImportMapType::JS, path: '/any', isEntrypoint: true), - ImportMapEntry::createLocal('entry2', ImportMapType::JS, path: '/any', isEntrypoint: true), - ImportMapEntry::createLocal('not_entrypoint', ImportMapType::JS, path: '/any', isEntrypoint: false), - ]); - - $this->assertEquals(['entry1', 'entry2'], $manager->getEntrypointNames()); - } - /** * @dataProvider getRequirePackageTests */ @@ -987,7 +362,6 @@ public static function getPackageNameTests(): iterable private function createImportMapManager(): ImportMapManager { - $this->pathResolver = $this->createMock(PublicAssetsPathResolverInterface::class); $this->assetMapper = $this->createMock(AssetMapperInterface::class); $this->configReader = $this->createMock(ImportMapConfigReader::class); $this->packageResolver = $this->createMock(PackageResolverInterface::class); @@ -1004,7 +378,6 @@ private function createImportMapManager(): ImportMapManager return $this->importMapManager = new ImportMapManager( $this->assetMapper, - $this->pathResolver, $this->configReader, $this->remotePackageDownloader, $this->packageResolver, @@ -1028,59 +401,6 @@ private function mockImportMap(array $importMapEntries): void ; } - /** - * @param MappedAsset[] $mappedAssets - */ - private function mockAssetMapper(array $mappedAssets): void - { - $this->assetMapper->expects($this->any()) - ->method('getAsset') - ->willReturnCallback(function (string $logicalPath) use ($mappedAssets) { - foreach ($mappedAssets as $asset) { - if ($asset->logicalPath === $logicalPath) { - return $asset; - } - } - - return null; - }) - ; - - $this->assetMapper->expects($this->any()) - ->method('getAssetFromSourcePath') - ->willReturnCallback(function (string $sourcePath) use ($mappedAssets) { - // collapse ../ in paths and ./ in paths to mimic the realpath AssetMapper uses - $unCollapsePath = function (string $path) { - $parts = explode('/', $path); - $newParts = []; - foreach ($parts as $part) { - if ('..' === $part) { - array_pop($newParts); - - continue; - } - - if ('.' !== $part) { - $newParts[] = $part; - } - } - - return implode('/', $newParts); - }; - - $sourcePath = $unCollapsePath($sourcePath); - - foreach ($mappedAssets as $asset) { - if (isset($asset->sourcePath) && $unCollapsePath($asset->sourcePath) === $sourcePath) { - return $asset; - } - } - - return null; - }) - ; - } - private function writeFile(string $filename, string $content): void { $path = \dirname(self::$writableRoot.'/'.$filename); diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php index 3d729d8c8caf7..a0d90e0cc5c15 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Asset\Packages; -use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; +use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator; use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -23,8 +23,8 @@ class ImportMapRendererTest extends TestCase { public function testBasicRender() { - $importMapManager = $this->createMock(ImportMapManager::class); - $importMapManager->expects($this->once()) + $importMapGenerator = $this->createMock(ImportMapGenerator::class); + $importMapGenerator->expects($this->once()) ->method('getImportMapData') ->with(['app']) ->willReturn([ @@ -68,7 +68,7 @@ public function testBasicRender() return '/subdirectory/'.$path; }); - $renderer = new ImportMapRenderer($importMapManager, $assetPackages, polyfillImportName: 'es-module-shim'); + $renderer = new ImportMapRenderer($importMapGenerator, $assetPackages, polyfillImportName: 'es-module-shim'); $html = $renderer->render(['app']); $this->assertStringContainsString('", $renderer->render('application')); - $renderer = new ImportMapRenderer($this->createBasicImportMapManager()); + $renderer = new ImportMapRenderer($this->createBasicImportMapGenerator()); $this->assertStringContainsString("", $renderer->render("application's")); - $renderer = new ImportMapRenderer($this->createBasicImportMapManager()); + $renderer = new ImportMapRenderer($this->createBasicImportMapGenerator()); $html = $renderer->render(['foo', 'bar']); $this->assertStringContainsString("import 'foo';", $html); $this->assertStringContainsString("import 'bar';", $html); } - private function createBasicImportMapManager(): ImportMapManager + private function createBasicImportMapGenerator(): ImportMapGenerator { - $importMapManager = $this->createMock(ImportMapManager::class); - $importMapManager->expects($this->once()) + $importMapGenerator = $this->createMock(ImportMapGenerator::class); + $importMapGenerator->expects($this->once()) ->method('getImportMapData') ->willReturn([ 'app' => [ @@ -159,13 +159,13 @@ private function createBasicImportMapManager(): ImportMapManager ]) ; - return $importMapManager; + return $importMapGenerator; } public function testItAddsPreloadLinks() { - $importMapManager = $this->createMock(ImportMapManager::class); - $importMapManager->expects($this->once()) + $importMapGenerator = $this->createMock(ImportMapGenerator::class); + $importMapGenerator->expects($this->once()) ->method('getImportMapData') ->willReturn([ 'app_js_preload' => [ @@ -188,7 +188,7 @@ public function testItAddsPreloadLinks() $requestStack = new RequestStack(); $requestStack->push($request); - $renderer = new ImportMapRenderer($importMapManager, requestStack: $requestStack); + $renderer = new ImportMapRenderer($importMapGenerator, requestStack: $requestStack); $renderer->render(['app']); $linkProvider = $request->attributes->get('_links'); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy