Skip to content

Commit 441f671

Browse files
bug #841 Refactor PackageJsonSynchronizer to prevent unintentional duplicate dependencies (codedmonkey)
This PR was squashed before being merged into the 1.x branch. Discussion ---------- Refactor PackageJsonSynchronizer to prevent unintentional duplicate dependencies Fixes #840 Changes how `PackageJsonSynchronizer` handles dependency resolving. If a dependency is already defined under the `dependencies` section of package.json, Flex won't add it again under `devDependencies`. If multiple UX bundles require the same dependency, no action is performed. Because the way `PackageJsonSynchronizer` was written, I had to change the order of the steps in the synchronization process. While it used to update the package.json for each UX dependency individually, it now resolves a list of dependencies and updates package.json at the end. I also changed how it handles incompatible peer dependencies, no action is performed by the synchronizer if multiple ux package require incompatible peer dependencies. Commits ------- 815c96f Refactor PackageJsonSynchronizer to prevent unintentional duplicate dependencies
2 parents d1ca6ac + 815c96f commit 441f671

File tree

3 files changed

+123
-89
lines changed

3 files changed

+123
-89
lines changed

src/PackageJsonSynchronizer.php

Lines changed: 76 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313

1414
use Composer\Json\JsonFile;
1515
use Composer\Json\JsonManipulator;
16-
use Composer\Semver\Constraint\ConstraintInterface;
17-
use Composer\Semver\Intervals;
18-
use Composer\Semver\VersionParser;
1916
use Seld\JsonLint\ParsingException;
2017

2118
/**
@@ -40,14 +37,17 @@ public function shouldSynchronize(): bool
4037

4138
public function synchronize(array $phpPackages): bool
4239
{
43-
// Remove all links and add again only the existing packages
4440
try {
45-
$didAddLink = $this->removePackageJsonLinks((new JsonFile($this->rootDir.'/package.json'))->read());
41+
JsonFile::parseJson(file_get_contents($this->rootDir.'/package.json'));
4642
} catch (ParsingException $e) {
4743
// if package.json is invalid (possible during a recipe upgrade), we can't update the file
4844
return false;
4945
}
5046

47+
$didChangePackageJson = $this->removeObsoletePackageJsonLinks();
48+
49+
$dependencies = [];
50+
5151
foreach ($phpPackages as $k => $phpPackage) {
5252
if (\is_string($phpPackage)) {
5353
// support for smooth upgrades from older flex versions
@@ -56,22 +56,29 @@ public function synchronize(array $phpPackages): bool
5656
'keywords' => ['symfony-ux'],
5757
];
5858
}
59-
$didAddLink = $this->addPackageJsonLink($phpPackage) || $didAddLink;
59+
60+
foreach ($this->resolvePackageDependencies($phpPackage) as $dependency => $constraint) {
61+
$dependencies[$dependency][$phpPackage['name']] = $constraint;
62+
}
6063
}
6164

62-
$this->registerPeerDependencies($phpPackages);
65+
$didChangePackageJson = $this->registerDependencies($dependencies) || $didChangePackageJson;
6366

6467
// Register controllers and entrypoints in controllers.json
6568
$this->registerWebpackResources($phpPackages);
6669

67-
return $didAddLink;
70+
return $didChangePackageJson;
6871
}
6972

70-
private function removePackageJsonLinks(array $packageJson): bool
73+
private function removeObsoletePackageJsonLinks(): bool
7174
{
72-
$didRemoveLink = false;
73-
$jsDependencies = $packageJson['dependencies'] ?? [];
74-
$jsDevDependencies = $packageJson['devDependencies'] ?? [];
75+
$didChangePackageJson = false;
76+
77+
$manipulator = new JsonManipulator(file_get_contents($this->rootDir.'/package.json'));
78+
$content = json_decode($manipulator->getContents(), true);
79+
80+
$jsDependencies = $content['dependencies'] ?? [];
81+
$jsDevDependencies = $content['devDependencies'] ?? [];
7582

7683
foreach (['dependencies' => $jsDependencies, 'devDependencies' => $jsDevDependencies] as $key => $packages) {
7784
foreach ($packages as $name => $version) {
@@ -82,39 +89,78 @@ private function removePackageJsonLinks(array $packageJson): bool
8289
continue;
8390
}
8491

85-
$manipulator = new JsonManipulator(file_get_contents($this->rootDir.'/package.json'));
86-
$manipulator->removeSubNode('devDependencies', $name);
87-
file_put_contents($this->rootDir.'/package.json', $manipulator->getContents());
88-
$didRemoveLink = true;
92+
$manipulator->removeSubNode($key, $name);
93+
$didChangePackageJson = true;
8994
}
9095
}
9196

92-
return $didRemoveLink;
97+
file_put_contents($this->rootDir.'/package.json', $manipulator->getContents());
98+
99+
return $didChangePackageJson;
93100
}
94101

95-
private function addPackageJsonLink(array $phpPackage): bool
102+
private function resolvePackageDependencies($phpPackage): array
96103
{
104+
$dependencies = [];
105+
97106
if (!$packageJson = $this->resolvePackageJson($phpPackage)) {
98-
return false;
107+
return $dependencies;
99108
}
100109

101-
$manipulator = new JsonManipulator(file_get_contents($this->rootDir.'/package.json'));
102-
$manipulator->addSubNode('devDependencies', '@'.$phpPackage['name'], 'file:'.substr($packageJson->getPath(), 1 + \strlen($this->rootDir), -13));
110+
$dependencies['@'.$phpPackage['name']] = 'file:'.substr($packageJson->getPath(), 1 + \strlen($this->rootDir), -13);
111+
112+
foreach ($packageJson->read()['peerDependencies'] ?? [] as $peerDependency => $constraint) {
113+
$dependencies[$peerDependency] = $constraint;
114+
}
115+
116+
return $dependencies;
117+
}
103118

119+
private function registerDependencies(array $flexDependencies): bool
120+
{
121+
$didChangePackageJson = false;
122+
123+
$manipulator = new JsonManipulator(file_get_contents($this->rootDir.'/package.json'));
104124
$content = json_decode($manipulator->getContents(), true);
105125

106-
$devDependencies = $content['devDependencies'];
107-
uksort($devDependencies, 'strnatcmp');
108-
$manipulator->addMainKey('devDependencies', $devDependencies);
126+
foreach ($flexDependencies as $dependency => $constraints) {
127+
if (1 !== \count($constraints) && 1 !== \count(array_count_values($constraints))) {
128+
// If the flex packages have a colliding peer dependency, leave the resolution to the user
129+
continue;
130+
}
131+
132+
$constraint = array_shift($constraints);
109133

110-
$newContents = $manipulator->getContents();
111-
if ($newContents === file_get_contents($this->rootDir.'/package.json')) {
112-
return false;
134+
$parentNode = isset($content['dependencies'][$dependency]) ? 'dependencies' : 'devDependencies';
135+
if (!isset($content[$parentNode][$dependency])) {
136+
$content['devDependencies'][$dependency] = $constraint;
137+
$didChangePackageJson = true;
138+
} elseif ($constraint !== $content[$parentNode][$dependency]) {
139+
$content[$parentNode][$dependency] = $constraint;
140+
$didChangePackageJson = true;
141+
}
113142
}
114143

115-
file_put_contents($this->rootDir.'/package.json', $newContents);
144+
if ($didChangePackageJson) {
145+
if (isset($content['dependencies'])) {
146+
$manipulator->addMainKey('dependencies', $content['dependencies']);
147+
}
148+
149+
if (isset($content['devDependencies'])) {
150+
$devDependencies = $content['devDependencies'];
151+
uksort($devDependencies, 'strnatcmp');
152+
$manipulator->addMainKey('devDependencies', $devDependencies);
153+
}
154+
155+
$newContents = $manipulator->getContents();
156+
if ($newContents === file_get_contents($this->rootDir.'/package.json')) {
157+
return false;
158+
}
159+
160+
file_put_contents($this->rootDir.'/package.json', $manipulator->getContents());
161+
}
116162

117-
return true;
163+
return $didChangePackageJson;
118164
}
119165

120166
private function registerWebpackResources(array $phpPackages)
@@ -151,7 +197,7 @@ private function registerWebpackResources(array $phpPackages)
151197
continue;
152198
}
153199

154-
// Otherwise, the package exists: merge new config with uer config
200+
// Otherwise, the package exists: merge new config with user config
155201
$previousConfig = $previousControllersJson['controllers'][$name][$controllerName];
156202

157203
$config = [];
@@ -180,39 +226,6 @@ private function registerWebpackResources(array $phpPackages)
180226
file_put_contents($controllersJsonPath, json_encode($newControllersJson, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)."\n");
181227
}
182228

183-
public function registerPeerDependencies(array $phpPackages)
184-
{
185-
$peerDependencies = [];
186-
187-
foreach ($phpPackages as $phpPackage) {
188-
if (!$packageJson = $this->resolvePackageJson($phpPackage)) {
189-
continue;
190-
}
191-
192-
$versionParser = new VersionParser();
193-
194-
foreach ($packageJson->read()['peerDependencies'] ?? [] as $peerDependency => $constraint) {
195-
$peerDependencies[$peerDependency][$constraint] = $versionParser->parseConstraints($constraint);
196-
}
197-
}
198-
199-
if (!$peerDependencies) {
200-
return;
201-
}
202-
203-
$manipulator = new JsonManipulator(file_get_contents($this->rootDir.'/package.json'));
204-
$content = json_decode($manipulator->getContents(), true);
205-
$devDependencies = $content['devDependencies'] ?? [];
206-
207-
foreach ($peerDependencies as $peerDependency => $constraints) {
208-
$devDependencies[$peerDependency] = $this->compactConstraints($constraints);
209-
}
210-
uksort($devDependencies, 'strnatcmp');
211-
$manipulator->addMainKey('devDependencies', $devDependencies);
212-
213-
file_put_contents($this->rootDir.'/package.json', $manipulator->getContents());
214-
}
215-
216229
private function resolvePackageJson(array $phpPackage): ?JsonFile
217230
{
218231
$packageDir = $this->rootDir.'/'.$this->vendorDir.'/'.$phpPackage['name'];
@@ -233,28 +246,4 @@ private function resolvePackageJson(array $phpPackage): ?JsonFile
233246

234247
return null;
235248
}
236-
237-
/**
238-
* @param ConstraintInterface[] $constraints
239-
*/
240-
private function compactConstraints(array $constraints): string
241-
{
242-
if (method_exists(Intervals::class, 'isSubsetOf')) {
243-
foreach ($constraints as $k1 => $constraint1) {
244-
foreach ($constraints as $k2 => $constraint2) {
245-
if ($k1 !== $k2 && Intervals::isSubsetOf($constraint1, $constraint2)) {
246-
unset($constraints[$k2]);
247-
}
248-
}
249-
}
250-
}
251-
252-
uksort($constraints, 'strnatcmp');
253-
254-
foreach ($constraints as $k => $constraint) {
255-
$constraints[$k] = \count($constraints) > 1 && false !== strpos($k, '|') ? '('.$k.')' : $k;
256-
}
257-
258-
return implode(',', $constraints);
259-
}
260249
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "symfony/fixture",
3+
"dependencies": {
4+
"@hotcookies": "^1.1|^2",
5+
"@hotdogs": "^2",
6+
"@symfony/existing-package": "file:vendor/symfony/existing-package/Resources/assets"
7+
},
8+
"devDependencies": {
9+
"@symfony/stimulus-bridge": "^1.0.0",
10+
"stimulus": "^1.1.1"
11+
},
12+
"browserslist": [
13+
"defaults"
14+
]
15+
}

tests/PackageJsonSynchronizerTest.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\Flex\Tests;
1313

14-
use Composer\Semver\Intervals;
1514
use PHPUnit\Framework\TestCase;
1615
use Symfony\Component\Filesystem\Filesystem;
1716
use Symfony\Flex\PackageJsonSynchronizer;
@@ -145,7 +144,6 @@ public function testSynchronizeNewPackage()
145144
'{
146145
"name": "symfony/fixture",
147146
"devDependencies": {
148-
"@hotcookies": "'.(method_exists(Intervals::class, 'isSubsetOf') ? '^1.1' : '^1.1,(^1.1|^2)').'",
149147
"@hotdogs": "^2",
150148
"@symfony/existing-package": "file:vendor/symfony/existing-package/Resources/assets",
151149
"@symfony/new-package": "file:vendor/symfony/new-package/assets",
@@ -215,4 +213,36 @@ public function testArrayFormattingHasNotChanged()
215213
trim(file_get_contents($this->tempDir.'/package.json'))
216214
);
217215
}
216+
217+
public function testExistingElevatedPackage()
218+
{
219+
(new Filesystem())->copy($this->tempDir.'/elevated_dependencies_package.json', $this->tempDir.'/package.json', true);
220+
221+
$this->synchronizer->synchronize([
222+
[
223+
'name' => 'symfony/existing-package',
224+
'keywords' => ['symfony-ux'],
225+
],
226+
]);
227+
228+
// Should keep existing package references and config
229+
$this->assertSame(
230+
[
231+
'name' => 'symfony/fixture',
232+
'dependencies' => [
233+
'@hotcookies' => '^1.1|^2',
234+
'@hotdogs' => '^2',
235+
'@symfony/existing-package' => 'file:vendor/symfony/existing-package/Resources/assets',
236+
],
237+
'devDependencies' => [
238+
'@symfony/stimulus-bridge' => '^1.0.0',
239+
'stimulus' => '^1.1.1',
240+
],
241+
'browserslist' => [
242+
'defaults',
243+
],
244+
],
245+
json_decode(file_get_contents($this->tempDir.'/package.json'), true)
246+
);
247+
}
218248
}

0 commit comments

Comments
 (0)