218 lines
7.0 KiB
PHP
218 lines
7.0 KiB
PHP
|
<?php
|
||
|
|
||
|
/*
|
||
|
* This file is part of the Symfony package.
|
||
|
*
|
||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||
|
*
|
||
|
* For the full copyright and license information, please view the LICENSE
|
||
|
* file that was distributed with this source code.
|
||
|
*/
|
||
|
|
||
|
namespace Symfony\Component\AssetMapper\ImportMap;
|
||
|
|
||
|
use Symfony\Component\AssetMapper\AssetMapperInterface;
|
||
|
use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface;
|
||
|
use Symfony\Component\AssetMapper\MappedAsset;
|
||
|
|
||
|
/**
|
||
|
* @author Kévin Dunglas <kevin@dunglas.dev>
|
||
|
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||
|
*
|
||
|
* @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<package>@?[^=@\n]+))(?:@(?P<version>[^=\s\n]+))?(?:=(?P<alias>[^\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));
|
||
|
}
|
||
|
}
|