* * 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\ImportMap\Resolver\PackageResolverInterface; use Symfony\Component\AssetMapper\MappedAsset; /** * @author Kévin Dunglas * @author Ryan Weaver * * @final */ class ImportMapManager { public function __construct( private readonly AssetMapperInterface $assetMapper, private readonly ImportMapConfigReader $importMapConfigReader, private readonly RemotePackageDownloader $packageDownloader, private readonly PackageResolverInterface $resolver, ) { } /** * Adds or updates packages. * * @param PackageRequireOptions[] $packages * * @return ImportMapEntry[] */ public function require(array $packages): array { return $this->updateImportMapConfig(false, $packages, [], []); } /** * Removes packages. * * @param string[] $packages */ public function remove(array $packages): void { $this->updateImportMapConfig(false, [], $packages, []); } /** * Updates either all existing packages or the specified ones to the latest version. * * @return ImportMapEntry[] */ public function update(array $packages = []): array { return $this->updateImportMapConfig(true, [], [], $packages); } /** * @internal */ public static function parsePackageName(string $packageName): ?array { // https://regex101.com/r/z1nj7P/1 $regex = '/((?P@?[^=@\n]+))(?:@(?P[^=\s\n]+))?(?:=(?P[^\s\n]+))?/'; if (!preg_match($regex, $packageName, $matches)) { return null; } if (isset($matches['version']) && '' === $matches['version']) { unset($matches['version']); } return $matches; } /** * @param PackageRequireOptions[] $packagesToRequire * @param string[] $packagesToRemove * * @return ImportMapEntry[] */ private function updateImportMapConfig(bool $update, array $packagesToRequire, array $packagesToRemove, array $packagesToUpdate): array { $currentEntries = $this->importMapConfigReader->getEntries(); foreach ($packagesToRemove as $packageName) { if (!$currentEntries->has($packageName)) { throw new \InvalidArgumentException(sprintf('Package "%s" listed for removal was not found in "importmap.php".', $packageName)); } $this->cleanupPackageFiles($currentEntries->get($packageName)); $currentEntries->remove($packageName); } if ($update) { foreach ($currentEntries as $entry) { $importName = $entry->importName; if (!$entry->isRemotePackage() || ($packagesToUpdate && !\in_array($importName, $packagesToUpdate, true))) { continue; } $packagesToRequire[] = new PackageRequireOptions( $entry->packageModuleSpecifier, null, $importName, ); // remove it: then it will be re-added $this->cleanupPackageFiles($entry); $currentEntries->remove($importName); } } $newEntries = $this->requirePackages($packagesToRequire, $currentEntries); $this->importMapConfigReader->writeEntries($currentEntries); $this->packageDownloader->downloadPackages(); return $newEntries; } /** * Gets information about (and optionally downloads) the packages & updates the entries. * * Returns an array of the entries that were added. * * @param PackageRequireOptions[] $packagesToRequire */ private function requirePackages(array $packagesToRequire, ImportMapEntries $importMapEntries): array { if (!$packagesToRequire) { return []; } $addedEntries = []; // handle local packages foreach ($packagesToRequire as $key => $requireOptions) { if (null === $requireOptions->path) { continue; } $path = $requireOptions->path; if (!$asset = $this->findAsset($path)) { throw new \LogicException(sprintf('The path "%s" of the package "%s" cannot be found: either pass the logical name of the asset or a relative path starting with "./".', $requireOptions->path, $requireOptions->importName)); } // convert to a relative path (or fallback to the logical path) $path = $asset->logicalPath; if (null !== $relativePath = $this->importMapConfigReader->convertFilesystemPathToPath($asset->sourcePath)) { $path = $relativePath; } $newEntry = ImportMapEntry::createLocal( $requireOptions->importName, self::getImportMapTypeFromFilename($requireOptions->path), $path, $requireOptions->entrypoint, ); $importMapEntries->add($newEntry); $addedEntries[] = $newEntry; unset($packagesToRequire[$key]); } if (!$packagesToRequire) { return $addedEntries; } $resolvedPackages = $this->resolver->resolvePackages($packagesToRequire); foreach ($resolvedPackages as $resolvedPackage) { $newEntry = $this->importMapConfigReader->createRemoteEntry( $resolvedPackage->requireOptions->importName, $resolvedPackage->type, $resolvedPackage->version, $resolvedPackage->requireOptions->packageModuleSpecifier, $resolvedPackage->requireOptions->entrypoint, ); $importMapEntries->add($newEntry); $addedEntries[] = $newEntry; } return $addedEntries; } private function cleanupPackageFiles(ImportMapEntry $entry): void { $asset = $this->findAsset($entry->path); if ($asset && is_file($asset->sourcePath)) { @unlink($asset->sourcePath); } } private static function getImportMapTypeFromFilename(string $path): ImportMapType { return str_ends_with($path, '.css') ? ImportMapType::CSS : ImportMapType::JS; } /** * 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; } return $this->assetMapper->getAssetFromSourcePath($this->importMapConfigReader->convertPathToFilesystemPath($path)); } }