Skip to content

Commit 42dfb9a

Browse files
weaverryannicolas-grekas
authored andcommitted
[AssetMapper] Warn of missing or incompat dependencies
1 parent 1a72bd5 commit 42dfb9a

15 files changed

+795
-37
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"require": {
3636
"php": ">=8.1",
3737
"composer-runtime-api": ">=2.1",
38+
"composer/semver": "^3.0",
3839
"ext-xml": "*",
3940
"friendsofphp/proxy-manager-lts": "^1.0.2",
4041
"doctrine/event-manager": "^1.2|^2",

src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
3535
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
3636
use Symfony\Component\AssetMapper\ImportMap\ImportMapUpdateChecker;
37+
use Symfony\Component\AssetMapper\ImportMap\ImportMapVersionChecker;
3738
use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader;
3839
use Symfony\Component\AssetMapper\ImportMap\RemotePackageStorage;
3940
use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver;
@@ -171,6 +172,12 @@
171172
service('asset_mapper.importmap.resolver'),
172173
])
173174

175+
->set('asset_mapper.importmap.version_checker', ImportMapVersionChecker::class)
176+
->args([
177+
service('asset_mapper.importmap.config_reader'),
178+
service('asset_mapper.importmap.remote_package_downloader'),
179+
])
180+
174181
->set('asset_mapper.importmap.resolver', JsDelivrEsmResolver::class)
175182
->args([service('http_client')])
176183

@@ -199,6 +206,7 @@
199206
->args([
200207
service('asset_mapper.importmap.manager'),
201208
param('kernel.project_dir'),
209+
service('asset_mapper.importmap.version_checker'),
202210
])
203211
->tag('console.command')
204212

@@ -207,7 +215,10 @@
207215
->tag('console.command')
208216

209217
->set('asset_mapper.importmap.command.update', ImportMapUpdateCommand::class)
210-
->args([service('asset_mapper.importmap.manager')])
218+
->args([
219+
service('asset_mapper.importmap.manager'),
220+
service('asset_mapper.importmap.version_checker'),
221+
])
211222
->tag('console.command')
212223

213224
->set('asset_mapper.importmap.command.install', ImportMapInstallCommand::class)

src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
1515
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
16+
use Symfony\Component\AssetMapper\ImportMap\ImportMapVersionChecker;
1617
use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions;
1718
use Symfony\Component\Console\Attribute\AsCommand;
1819
use Symfony\Component\Console\Command\Command;
@@ -28,9 +29,12 @@
2829
#[AsCommand(name: 'importmap:require', description: 'Require JavaScript packages')]
2930
final class ImportMapRequireCommand extends Command
3031
{
32+
use VersionProblemCommandTrait;
33+
3134
public function __construct(
3235
private readonly ImportMapManager $importMapManager,
3336
private readonly string $projectDir,
37+
private readonly ImportMapVersionChecker $importMapVersionChecker,
3438
) {
3539
parent::__construct();
3640
}
@@ -108,6 +112,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
108112
}
109113

110114
$newPackages = $this->importMapManager->require($packages);
115+
116+
$this->renderVersionProblems($this->importMapVersionChecker, $output);
117+
111118
if (1 === \count($newPackages)) {
112119
$newPackage = $newPackages[0];
113120
$message = sprintf('Package "%s" added to importmap.php', $newPackage->importName);

src/Symfony/Component/AssetMapper/Command/ImportMapUpdateCommand.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
1515
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
16+
use Symfony\Component\AssetMapper\ImportMap\ImportMapVersionChecker;
1617
use Symfony\Component\Console\Attribute\AsCommand;
1718
use Symfony\Component\Console\Command\Command;
1819
use Symfony\Component\Console\Input\InputArgument;
@@ -26,8 +27,11 @@
2627
#[AsCommand(name: 'importmap:update', description: 'Update JavaScript packages to their latest versions')]
2728
final class ImportMapUpdateCommand extends Command
2829
{
30+
use VersionProblemCommandTrait;
31+
2932
public function __construct(
30-
protected readonly ImportMapManager $importMapManager,
33+
private readonly ImportMapManager $importMapManager,
34+
private readonly ImportMapVersionChecker $importMapVersionChecker,
3135
) {
3236
parent::__construct();
3337
}
@@ -57,6 +61,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
5761
$io = new SymfonyStyle($input, $output);
5862
$updatedPackages = $this->importMapManager->update($packages);
5963

64+
$this->renderVersionProblems($this->importMapVersionChecker, $output);
65+
6066
if (0 < \count($packages)) {
6167
$io->success(sprintf(
6268
'Updated %s package%s in importmap.php.',
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\AssetMapper\Command;
13+
14+
use Symfony\Component\AssetMapper\ImportMap\ImportMapVersionChecker;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
17+
/**
18+
* @internal
19+
*/
20+
trait VersionProblemCommandTrait
21+
{
22+
private function renderVersionProblems(ImportMapVersionChecker $importMapVersionChecker, OutputInterface $output): void
23+
{
24+
$problems = $importMapVersionChecker->checkVersions();
25+
foreach ($problems as $problem) {
26+
if (null === $problem->installedVersion) {
27+
$output->writeln(sprintf('[warning] <info>%s</info> requires <info>%s</info> but it is not in the importmap.php. You may need to run "php bin/console importmap:require %s".', $problem->packageName, $problem->dependencyPackageName, $problem->dependencyPackageName));
28+
29+
continue;
30+
}
31+
32+
if (null === $problem->requiredVersionConstraint) {
33+
$output->writeln(sprintf('[warning] <info>%s</info> appears to import <info>%s</info> but this is not listed as a dependency of <info>%s</info>. This is odd and could be a misconfiguration of that package.', $problem->packageName, $problem->dependencyPackageName, $problem->packageName));
34+
35+
continue;
36+
}
37+
38+
$output->writeln(sprintf('[warning] <info>%s</info> requires <info>%s</info>@<comment>%s</comment> but version <comment>%s</comment> is installed.', $problem->packageName, $problem->dependencyPackageName, $problem->requiredVersionConstraint, $problem->installedVersion));
39+
}
40+
}
41+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\AssetMapper\ImportMap;
13+
14+
use Composer\Semver\Semver;
15+
use Symfony\Component\AssetMapper\Exception\RuntimeException;
16+
use Symfony\Component\HttpClient\HttpClient;
17+
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
18+
use Symfony\Contracts\HttpClient\HttpClientInterface;
19+
20+
class ImportMapVersionChecker
21+
{
22+
private const PACKAGE_METADATA_PATTERN = 'https://registry.npmjs.org/%package%/%version%';
23+
24+
private HttpClientInterface $httpClient;
25+
26+
public function __construct(
27+
private ImportMapConfigReader $importMapConfigReader,
28+
private RemotePackageDownloader $packageDownloader,
29+
HttpClientInterface $httpClient = null,
30+
) {
31+
$this->httpClient = $httpClient ?? HttpClient::create();
32+
}
33+
34+
/**
35+
* @return PackageVersionProblem[]
36+
*/
37+
public function checkVersions(): array
38+
{
39+
$entries = $this->importMapConfigReader->getEntries();
40+
41+
$packages = [];
42+
foreach ($entries as $entry) {
43+
if (!$entry->isRemotePackage()) {
44+
continue;
45+
}
46+
47+
$dependencies = $this->packageDownloader->getDependencies($entry->importName);
48+
if (!$dependencies) {
49+
continue;
50+
}
51+
52+
$packageName = $entry->getPackageName();
53+
54+
$url = str_replace(
55+
['%package%', '%version%'],
56+
[$packageName, $entry->version],
57+
self::PACKAGE_METADATA_PATTERN
58+
);
59+
$packages[$packageName] = [
60+
$this->httpClient->request('GET', $url),
61+
$dependencies,
62+
];
63+
}
64+
65+
$errors = [];
66+
$problems = [];
67+
foreach ($packages as $packageName => [$response, $dependencies]) {
68+
if (200 !== $response->getStatusCode()) {
69+
$errors[] = [$packageName, $response];
70+
continue;
71+
}
72+
73+
$data = json_decode($response->getContent(), true);
74+
// dependencies seem to be found in both places
75+
$packageDependencies = array_merge(
76+
$data['dependencies'] ?? [],
77+
$data['peerDependencies'] ?? []
78+
);
79+
80+
foreach ($dependencies as $dependencyName) {
81+
// dependency is not in the import map
82+
if (!$entries->has($dependencyName)) {
83+
$dependencyVersionConstraint = $packageDependencies[$dependencyName] ?? 'unknown';
84+
$problems[] = new PackageVersionProblem($packageName, $dependencyName, $dependencyVersionConstraint, null);
85+
86+
continue;
87+
}
88+
89+
$dependencyPackageName = $entries->get($dependencyName)->getPackageName();
90+
$dependencyVersionConstraint = $packageDependencies[$dependencyPackageName] ?? null;
91+
92+
if (null === $dependencyVersionConstraint) {
93+
$problems[] = new PackageVersionProblem($packageName, $dependencyPackageName, $dependencyVersionConstraint, $entries->get($dependencyName)->version);
94+
95+
continue;
96+
}
97+
98+
if (!$this->isVersionSatisfied($dependencyVersionConstraint, $entries->get($dependencyName)->version)) {
99+
$problems[] = new PackageVersionProblem($packageName, $dependencyPackageName, $dependencyVersionConstraint, $entries->get($dependencyName)->version);
100+
}
101+
}
102+
}
103+
104+
try {
105+
($errors[0][1] ?? null)?->getHeaders();
106+
} catch (HttpExceptionInterface $e) {
107+
$response = $e->getResponse();
108+
$packageNames = implode('", "', array_column($errors, 0));
109+
110+
throw new RuntimeException(sprintf('Error %d finding metadata for package "%s". Response: ', $response->getStatusCode(), $packageNames).$response->getContent(false), 0, $e);
111+
}
112+
113+
return $problems;
114+
}
115+
116+
/**
117+
* Converts npm-specific version constraints to composer-style.
118+
*
119+
* @internal
120+
*/
121+
public static function convertNpmConstraint(string $versionConstraint): ?string
122+
{
123+
// special npm constraint that don't translate to composer
124+
if (\in_array($versionConstraint, ['latest', 'next'])
125+
|| preg_match('/^(git|http|file):/', $versionConstraint)
126+
|| str_contains($versionConstraint, '/')
127+
) {
128+
// GitHub shorthand like user/repo
129+
return null;
130+
}
131+
132+
// remove whitespace around hyphens
133+
$versionConstraint = preg_replace('/\s?-\s?/', '-', $versionConstraint);
134+
$segments = explode(' ', $versionConstraint);
135+
$processedSegments = [];
136+
137+
foreach ($segments as $segment) {
138+
if (str_contains($segment, '-') && !preg_match('/-(alpha|beta|rc)\./', $segment)) {
139+
// This is a range
140+
[$start, $end] = explode('-', $segment);
141+
$processedSegments[] = '>='.self::cleanVersionSegment(trim($start)).' <='.self::cleanVersionSegment(trim($end));
142+
} elseif (preg_match('/^~(\d+\.\d+)$/', $segment, $matches)) {
143+
// Handle the tilde when only major.minor specified
144+
$baseVersion = $matches[1];
145+
$processedSegments[] = '>='.$baseVersion.'.0';
146+
$processedSegments[] = '<'.$baseVersion[0].'.'.($baseVersion[2] + 1).'.0';
147+
} else {
148+
$processedSegments[] = self::cleanVersionSegment($segment);
149+
}
150+
}
151+
152+
return implode(' ', $processedSegments);
153+
}
154+
155+
private static function cleanVersionSegment(string $segment): string
156+
{
157+
return str_replace(['v', '.x'], ['', '.*'], $segment);
158+
}
159+
160+
private function isVersionSatisfied(string $versionConstraint, ?string $version): bool
161+
{
162+
if (!$version) {
163+
return false;
164+
}
165+
166+
try {
167+
$versionConstraint = self::convertNpmConstraint($versionConstraint);
168+
169+
// if version isn't parseable/convertible, assume it's not satisfied
170+
if (null === $versionConstraint) {
171+
return false;
172+
}
173+
174+
return Semver::satisfies($version, $versionConstraint);
175+
} catch (\UnexpectedValueException $e) {
176+
return false;
177+
}
178+
}
179+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\AssetMapper\ImportMap;
13+
14+
final class PackageVersionProblem
15+
{
16+
public function __construct(
17+
public readonly string $packageName,
18+
public readonly string $dependencyPackageName,
19+
public readonly ?string $requiredVersionConstraint,
20+
public readonly ?string $installedVersion
21+
) {
22+
}
23+
}

0 commit comments

Comments
 (0)
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