FestinHegre/vendor/symfony/asset-mapper/ImportMap/ImportMapVersionChecker.php
2024-09-26 17:26:04 +02:00

179 lines
6.2 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 Composer\Semver\Semver;
use Symfony\Component\AssetMapper\Exception\RuntimeException;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ImportMapVersionChecker
{
private const PACKAGE_METADATA_PATTERN = 'https://registry.npmjs.org/%package%/%version%';
private HttpClientInterface $httpClient;
public function __construct(
private ImportMapConfigReader $importMapConfigReader,
private RemotePackageDownloader $packageDownloader,
?HttpClientInterface $httpClient = null,
) {
$this->httpClient = $httpClient ?? HttpClient::create();
}
/**
* @return PackageVersionProblem[]
*/
public function checkVersions(): array
{
$entries = $this->importMapConfigReader->getEntries();
$packages = [];
foreach ($entries as $entry) {
if (!$entry->isRemotePackage()) {
continue;
}
$dependencies = $this->packageDownloader->getDependencies($entry->importName);
if (!$dependencies) {
continue;
}
$packageName = $entry->getPackageName();
$url = str_replace(
['%package%', '%version%'],
[$packageName, $entry->version],
self::PACKAGE_METADATA_PATTERN
);
$packages[$packageName] = [
$this->httpClient->request('GET', $url),
$dependencies,
];
}
$errors = [];
$problems = [];
foreach ($packages as $packageName => [$response, $dependencies]) {
if (200 !== $response->getStatusCode()) {
$errors[] = [$packageName, $response];
continue;
}
$data = json_decode($response->getContent(), true);
// dependencies seem to be found in both places
$packageDependencies = array_merge(
$data['dependencies'] ?? [],
$data['peerDependencies'] ?? []
);
foreach ($dependencies as $dependencyName) {
// dependency is not in the import map
if (!$entries->has($dependencyName)) {
$dependencyVersionConstraint = $packageDependencies[$dependencyName] ?? 'unknown';
$problems[] = new PackageVersionProblem($packageName, $dependencyName, $dependencyVersionConstraint, null);
continue;
}
$dependencyPackageName = $entries->get($dependencyName)->getPackageName();
if (!isset($packageDependencies[$dependencyPackageName])) {
continue;
}
$dependencyVersionConstraint = $packageDependencies[$dependencyPackageName];
if (!$this->isVersionSatisfied($dependencyVersionConstraint, $entries->get($dependencyName)->version)) {
$problems[] = new PackageVersionProblem($packageName, $dependencyPackageName, $dependencyVersionConstraint, $entries->get($dependencyName)->version);
}
}
}
try {
($errors[0][1] ?? null)?->getHeaders();
} catch (HttpExceptionInterface $e) {
$response = $e->getResponse();
$packageNames = implode('", "', array_column($errors, 0));
throw new RuntimeException(sprintf('Error %d finding metadata for package "%s". Response: ', $response->getStatusCode(), $packageNames).$response->getContent(false), 0, $e);
}
return $problems;
}
/**
* Converts npm-specific version constraints to composer-style.
*
* @internal
*/
public static function convertNpmConstraint(string $versionConstraint): ?string
{
// special npm constraint that don't translate to composer
if (\in_array($versionConstraint, ['latest', 'next'])
|| preg_match('/^(git|http|file):/', $versionConstraint)
|| str_contains($versionConstraint, '/')
) {
// GitHub shorthand like user/repo
return null;
}
// remove whitespace around hyphens
$versionConstraint = preg_replace('/\s?-\s?/', '-', $versionConstraint);
$segments = explode(' ', $versionConstraint);
$processedSegments = [];
foreach ($segments as $segment) {
if (str_contains($segment, '-') && !preg_match('/-(alpha|beta|rc)\./', $segment)) {
// This is a range
[$start, $end] = explode('-', $segment);
$processedSegments[] = self::cleanVersionSegment(trim($start)).' - '.self::cleanVersionSegment(trim($end));
} elseif (preg_match('/^~(\d+\.\d+)$/', $segment, $matches)) {
// Handle the tilde when only major.minor specified
$baseVersion = $matches[1];
$processedSegments[] = '>='.$baseVersion.'.0';
$processedSegments[] = '<'.$baseVersion[0].'.'.($baseVersion[2] + 1).'.0';
} else {
$processedSegments[] = self::cleanVersionSegment($segment);
}
}
return implode(' ', $processedSegments);
}
private static function cleanVersionSegment(string $segment): string
{
return str_replace(['v', '.x'], ['', '.*'], $segment);
}
private function isVersionSatisfied(string $versionConstraint, ?string $version): bool
{
if (!$version) {
return false;
}
try {
$versionConstraint = self::convertNpmConstraint($versionConstraint);
// if version isn't parseable/convertible, assume it's not satisfied
if (null === $versionConstraint) {
return false;
}
return Semver::satisfies($version, $versionConstraint);
} catch (\UnexpectedValueException $e) {
return false;
}
}
}