Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[AssetMapper] Add outdated command #51845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Symfony\Component\AssetMapper\Command\DebugAssetMapperCommand;
use Symfony\Component\AssetMapper\Command\ImportMapAuditCommand;
use Symfony\Component\AssetMapper\Command\ImportMapInstallCommand;
use Symfony\Component\AssetMapper\Command\ImportMapOutdatedCommand;
use Symfony\Component\AssetMapper\Command\ImportMapRemoveCommand;
use Symfony\Component\AssetMapper\Command\ImportMapRequireCommand;
use Symfony\Component\AssetMapper\Command\ImportMapUpdateCommand;
Expand All @@ -32,6 +33,7 @@
use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
use Symfony\Component\AssetMapper\ImportMap\ImportMapUpdateChecker;
use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader;
use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver;
use Symfony\Component\AssetMapper\MapperAwareAssetPackage;
Expand Down Expand Up @@ -179,6 +181,11 @@
service('asset_mapper.importmap.config_reader'),
service('http_client'),
])
->set('asset_mapper.importmap.update_checker', ImportMapUpdateChecker::class)
->args([
service('asset_mapper.importmap.config_reader'),
service('http_client'),
])

->set('asset_mapper.importmap.command.require', ImportMapRequireCommand::class)
->args([
Expand All @@ -205,5 +212,9 @@
->set('asset_mapper.importmap.command.audit', ImportMapAuditCommand::class)
->args([service('asset_mapper.importmap.auditor')])
->tag('console.command')

->set('asset_mapper.importmap.command.outdated', ImportMapOutdatedCommand::class)
->args([service('asset_mapper.importmap.update_checker')])
->tag('console.command')
;
};
1 change: 1 addition & 0 deletions src/Symfony/Component/AssetMapper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ CHANGELOG
* Add a `importmap:install` command to download all missing downloaded packages
* Allow specifying packages to update for the `importmap:update` command
* Add a `importmap:audit` command to check for security vulnerability advisories in dependencies
* Add a `importmap:outdated` command to check for outdated packages

6.3
---
Expand Down
106 changes: 106 additions & 0 deletions src/Symfony/Component/AssetMapper/Command/ImportMapOutdatedCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\AssetMapper\Command;

use Symfony\Component\AssetMapper\ImportMap\ImportMapUpdateChecker;
use Symfony\Component\AssetMapper\ImportMap\PackageUpdateInfo;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'importmap:outdated', description: 'List outdated JavaScript packages and their latest versions')]
final class ImportMapOutdatedCommand extends Command
{
private const COLOR_MAPPING = [
'update-possible' => 'yellow',
'semver-safe-update' => 'red',
];

public function __construct(
private readonly ImportMapUpdateChecker $updateChecker,
) {
parent::__construct();
}

protected function configure(): void
{
$this
->addArgument(
name: 'packages',
mode: InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
description: 'A list of packages to check',
)
->addOption(
name: 'format',
mode: InputOption::VALUE_REQUIRED,
description: sprintf('The output format ("%s")', implode(', ', $this->getAvailableFormatOptions())),
default: 'txt',
)
->setHelp(<<<'EOT'
The <info>%command.name%</info> command will list the latest updates available for the 3rd party packages in <comment>importmap.php</comment>.
Versions showing in <fg=red>red</> are semver compatible versions and you should upgrading.
Versions showing in <fg=yellow>yellow</> are major updates that include backward compatibility breaks according to semver.

<info>php %command.full_name%</info>

Or specific packages only:

<info>php %command.full_name% <packages></info>
EOT
);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$packages = $input->getArgument('packages');
$packagesUpdateInfos = $this->updateChecker->getAvailableUpdates($packages);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be used in the update command too ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain more? Do you mean: in the update command, we would first use this function to determine which updates are possible and then loop over and make them? If so, that's a different way than we're doing it now - but I can't think of any big difference in final behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that was the idea, but i'm not talking about "right now" ... but in a near future, as we already said :)

$packagesUpdateInfos = array_filter($packagesUpdateInfos, fn ($packageUpdateInfo) => $packageUpdateInfo->hasUpdate());
if (0 === \count($packagesUpdateInfos)) {
return Command::SUCCESS;
}

$displayData = array_map(fn ($importName, $packageUpdateInfo) => [
'name' => $importName,
'current' => $packageUpdateInfo->currentVersion,
'latest' => $packageUpdateInfo->latestVersion,
'latest-status' => PackageUpdateInfo::UPDATE_TYPE_MAJOR === $packageUpdateInfo->updateType ? 'update-possible' : 'semver-safe-update',
], array_keys($packagesUpdateInfos), $packagesUpdateInfos);

if ('json' === $input->getOption('format')) {
$io->writeln(json_encode($displayData, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
} else {
$table = $io->createTable();
$table->setHeaders(['Package', 'Current', 'Latest']);
foreach ($displayData as $datum) {
$color = self::COLOR_MAPPING[$datum['latest-status']] ?? 'default';
$table->addRow([
sprintf('<fg=%s>%s</>', $color, $datum['name']),
$datum['current'],
sprintf('<fg=%s>%s</>', $color, $datum['latest']),
]);
}
$table->render();
}

return Command::FAILURE;
}

private function getAvailableFormatOptions(): array
{
return ['txt', 'json'];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,16 @@ public function getEntries(): ImportMapEntries
throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "version" option.', $importName));
}

[$packageName, $filePath] = self::splitPackageNameAndFilePath($importName);

$entries->add(new ImportMapEntry(
$importName,
path: $path,
version: $version,
type: $type,
isEntrypoint: $isEntry,
packageName: $packageName,
filePath: $filePath,
));
}

Expand Down Expand Up @@ -144,4 +148,18 @@ private function extractVersionFromLegacyUrl(string $url): ?string

return substr($url, $lastAt + 1, $nextSlash - $lastAt - 1);
}

public static function splitPackageNameAndFilePath(string $packageName): array
{
$filePath = '';
$i = strpos($packageName, '/');

if ($i && (!str_starts_with($packageName, '@') || $i = strpos($packageName, '/', $i + 1))) {
// @vendor/package/filepath or package/filepath
$filePath = substr($packageName, $i);
$packageName = substr($packageName, 0, $i);
}

return [$packageName, $filePath];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public function __construct(
public readonly ?string $version = null,
public readonly ImportMapType $type = ImportMapType::JS,
public readonly bool $isEntrypoint = false,
public readonly ?string $packageName = null,
public readonly ?string $filePath = null,
) {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\Contracts\HttpClient\HttpClientInterface;

class ImportMapUpdateChecker
{
private const URL_PACKAGE_METADATA = 'https://registry.npmjs.org/%s';

public function __construct(
private readonly ImportMapConfigReader $importMapConfigReader,
private readonly HttpClientInterface $httpClient,
) {
}

/**
* @param string[] $packages
*
* @return PackageUpdateInfo[]
*/
public function getAvailableUpdates(array $packages = []): array
{
$entries = $this->importMapConfigReader->getEntries();
$updateInfos = [];
$responses = [];
foreach ($entries as $entry) {
if (null === $entry->packageName || null === $entry->version) {
continue;
}
if (\count($packages) && !\in_array($entry->packageName, $packages, true)) {
continue;
}

$responses[$entry->importName] = $this->httpClient->request('GET', sprintf(self::URL_PACKAGE_METADATA, $entry->packageName), ['headers' => ['Accept' => 'application/vnd.npm.install-v1+json']]);
}

foreach ($responses as $importName => $response) {
$entry = $entries->get($importName);
if (200 !== $response->getStatusCode()) {
throw new \RuntimeException(sprintf('Unable to get latest version for package "%s".', $entry->packageName));
}
$updateInfo = new PackageUpdateInfo($entry->packageName, $entry->version);
try {
$updateInfo->latestVersion = json_decode($response->getContent(), true)['dist-tags']['latest'];
$updateInfo->updateType = $this->getUpdateType($updateInfo->currentVersion, $updateInfo->latestVersion);
} catch (\Exception $e) {
throw new \RuntimeException(sprintf('Unable to get latest version for package "%s".', $entry->packageName), 0, $e);
}
$updateInfos[$importName] = $updateInfo;
}

return $updateInfos;
}

private function getVersionPart(string $version, int $part): ?string
{
return explode('.', $version)[$part] ?? $version;
}

private function getUpdateType(string $currentVersion, string $latestVersion): string
{
if (version_compare($currentVersion, $latestVersion, '>')) {
return PackageUpdateInfo::UPDATE_TYPE_DOWNGRADE;
}
if (version_compare($currentVersion, $latestVersion, '==')) {
return PackageUpdateInfo::UPDATE_TYPE_UP_TO_DATE;
}
if ($this->getVersionPart($currentVersion, 0) < $this->getVersionPart($latestVersion, 0)) {
return PackageUpdateInfo::UPDATE_TYPE_MAJOR;
}
if ($this->getVersionPart($currentVersion, 1) < $this->getVersionPart($latestVersion, 1)) {
return PackageUpdateInfo::UPDATE_TYPE_MINOR;
}
if ($this->getVersionPart($currentVersion, 2) < $this->getVersionPart($latestVersion, 2)) {
return PackageUpdateInfo::UPDATE_TYPE_PATCH;
}

throw new \LogicException(sprintf('Unable to determine update type for "%s" and "%s".', $currentVersion, $latestVersion));
}
}
34 changes: 34 additions & 0 deletions src/Symfony/Component/AssetMapper/ImportMap/PackageUpdateInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\AssetMapper\ImportMap;

class PackageUpdateInfo
{
public const UPDATE_TYPE_DOWNGRADE = 'downgrade';
public const UPDATE_TYPE_UP_TO_DATE = 'up-to-date';
public const UPDATE_TYPE_MAJOR = 'major';
public const UPDATE_TYPE_MINOR = 'minor';
public const UPDATE_TYPE_PATCH = 'patch';

public function __construct(
public readonly string $packageName,
public readonly string $currentVersion,
public ?string $latestVersion = null,
public ?string $updateType = null,
) {
}

public function hasUpdate(): bool
{
return !\in_array($this->updateType, [self::UPDATE_TYPE_DOWNGRADE, self::UPDATE_TYPE_DOWNGRADE, self::UPDATE_TYPE_UP_TO_DATE]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\AssetMapper\ImportMap\Resolver;

use Symfony\Component\AssetMapper\Exception\RuntimeException;
use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions;
Expand Down Expand Up @@ -56,7 +57,7 @@ public function resolvePackages(array $packagesToRequire): array
continue;
}

[$packageName, $filePath] = self::splitPackageNameAndFilePath($packageName);
[$packageName, $filePath] = ImportMapConfigReader::splitPackageNameAndFilePath($packageName);

$response = $this->httpClient->request('GET', sprintf($this->versionUrlPattern, $packageName, urlencode($constraint)));
$requiredPackages[] = [$options, $response, $packageName, $filePath, /* resolved version */ null];
Expand Down Expand Up @@ -159,9 +160,8 @@ public function downloadPackages(array $importMapEntries, callable $progressCall
$responses = [];

foreach ($importMapEntries as $package => $entry) {
[$packageName, $filePath] = self::splitPackageNameAndFilePath($entry->importName);
$pattern = ImportMapType::CSS === $entry->type ? $this->distUrlCssPattern : $this->distUrlPattern;
$url = sprintf($pattern, $packageName, $entry->version, $filePath);
$url = sprintf($pattern, $entry->packageName, $entry->version, $entry->filePath);

$responses[$package] = $this->httpClient->request('GET', $url);
}
Expand Down Expand Up @@ -218,20 +218,6 @@ private function fetchPackageRequirementsFromImports(string $content): array
return $dependencies;
}

private static function splitPackageNameAndFilePath(string $packageName): array
{
$filePath = '';
$i = strpos($packageName, '/');

if ($i && (!str_starts_with($packageName, '@') || $i = strpos($packageName, '/', $i + 1))) {
// @vendor/package/filepath or package/filepath
$filePath = substr($packageName, $i);
$packageName = substr($packageName, 0, $i);
}

return [$packageName, $filePath];
}

/**
* Parses the very specific import syntax used by jsDelivr.
*
Expand Down
Loading