2525use Symfony \Component \Console \Input \ArrayInput ;
2626use Symfony \Component \Console \Input \InputInterface ;
2727use Symfony \Component \Console \Input \InputOption ;
28- use Symfony \Component \Console \Output \BufferedOutput ;
2928use Symfony \Component \Console \Output \OutputInterface ;
3029use Symfony \Component \Console \Question \ConfirmationQuestion ;
3130use Symfony \Component \Console \Style \SymfonyStyle ;
@@ -103,6 +102,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
103102 if (!empty ($ extensionUpgrades )) {
104103 $ extensionRows = [];
105104 $ unresolvedRows = [];
105+ $ unresolvedNames = [];
106106
107107 foreach ($ extensionUpgrades as $ upgrade ) {
108108 if ($ upgrade ['newConstraint ' ]) {
@@ -128,18 +128,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
128128 $ upgrade ['currentVersion ' ],
129129 $ upgrade ['coreConstraint ' ],
130130 ];
131+ $ unresolvedNames [] = $ upgrade ['name ' ];
131132 }
132133 }
133134
134135 if (!empty ($ extensionRows )) {
135- $ io ->section ('Extensions to upgrade ' );
136- $ io ->table (['Extension ' , 'Installed version ' , 'Requires typo3/cms-core ' , 'New version ' , 'New constraint ' ], $ extensionRows );
136+ $ io ->section ('Packages to upgrade ' );
137+ $ io ->table (['Package ' , 'Installed version ' , 'TYPO3 constraint ' , 'New version ' , 'New constraint ' ], $ extensionRows );
137138 }
138139
139140 if (!empty ($ unresolvedRows )) {
140- $ io ->section ('Extensions without a compatible version ' );
141- $ io ->table (['Extension ' , 'Installed version ' , 'Requires typo3/cms-core ' ], $ unresolvedRows );
142- $ io ->warning ('The extensions above have no published version compatible with ' . $ targetVersion . '. The update may fail . ' );
141+ $ io ->section ('Packages without a compatible version ' );
142+ $ io ->table (['Package ' , 'Installed version ' , 'TYPO3 constraint ' ], $ unresolvedRows );
143+ $ io ->warning ('The packages above have no published version compatible with ' . $ targetVersion . '. ' );
143144 }
144145 }
145146
@@ -148,6 +149,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
148149 return Command::SUCCESS ;
149150 }
150151
152+ // Offer to remove unresolvable packages before proceeding
153+ if (!empty ($ unresolvedNames )) {
154+ $ removeQuestion = new ConfirmationQuestion (
155+ 'Remove unresolvable packages ( ' . implode (', ' , $ unresolvedNames ) . ') from composer.json before updating? [Y/n] ' ,
156+ true
157+ );
158+ if ($ io ->askQuestion ($ removeQuestion )) {
159+ foreach ($ unresolvedNames as $ name ) {
160+ unset($ updatedData ['require ' ][$ name ], $ updatedData ['require-dev ' ][$ name ]);
161+ $ packagesToUpdate [] = $ name ;
162+ }
163+ }
164+ }
165+
151166 $ io ->writeln ('' );
152167 $ io ->writeln ('This will run: <info>composer update ' . implode (' ' , $ packagesToUpdate ) . ' -W</info> ' );
153168 $ io ->writeln ('' );
@@ -164,28 +179,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
164179 $ application = new Application ();
165180 $ application ->setAutoExit (false );
166181
167- $ bufferedOutput = new BufferedOutput ($ output ->getVerbosity (), $ output ->isDecorated ());
168182 $ arrayInput = new ArrayInput ([
169183 'command ' => 'update ' ,
170184 'packages ' => $ packagesToUpdate ,
171185 '-W ' => true ,
172186 ]);
173- $ exitCode = $ application ->run ($ arrayInput , $ bufferedOutput );
187+ $ exitCode = $ application ->run ($ arrayInput , $ output );
174188
175189 if ($ exitCode ) {
176190 $ jsonFile ->write ($ originalData );
177191 $ io ->error ('Failed to update TYPO3 packages. composer.json has been reverted. ' );
178-
179- if ($ output ->isVerbose ()) {
180- $ io ->section ('Composer output ' );
181- $ output ->write ($ bufferedOutput ->fetch ());
182- } else {
183- $ io ->note ('Run with -v to see the full composer output. ' );
184- }
185192 return Command::FAILURE ;
186193 }
187194
188- $ output ->write ($ bufferedOutput ->fetch ());
189195 $ io ->success ('TYPO3 core and extensions updated successfully. ' );
190196 return Command::SUCCESS ;
191197 }
@@ -199,18 +205,20 @@ private function isTypo3Package(string $name): bool
199205 'stable ' => BasePackage::STABILITY_STABLE ,
200206 ];
201207
202- private const ALL_STABILITIES = [
208+ private const PRE_RELEASE_STABILITIES = [
203209 'stable ' => BasePackage::STABILITY_STABLE ,
204210 'RC ' => BasePackage::STABILITY_RC ,
205211 'beta ' => BasePackage::STABILITY_BETA ,
206212 'alpha ' => BasePackage::STABILITY_ALPHA ,
207- 'dev ' => BasePackage::STABILITY_DEV ,
208213 ];
209214
210215 /**
211- * Find installed extensions incompatible with the target TYPO3 version
216+ * Find installed packages incompatible with the target TYPO3 version
212217 * and look up their latest compatible version from remote repositories.
213218 *
219+ * Checks all packages that require any typo3/cms-* package (excluding
220+ * the typo3/cms-* packages themselves, which are handled by the core update).
221+ *
214222 * @return array<int, array{name: string, currentVersion: string, coreConstraint: string, newConstraint: ?string, newDisplayVersion: ?string, stable: bool}>
215223 */
216224 private function findExtensionUpgrades (string $ targetVersion ): array
@@ -223,34 +231,50 @@ private function findExtensionUpgrades(string $targetVersion): array
223231 $ upgrades = [];
224232
225233 foreach ($ installedRepository ->getPackages () as $ package ) {
226- if ($ package ->getType () !== 'typo3-cms-extension ' ) {
234+ // Skip typo3/cms-* packages — they are handled by the core update
235+ if ($ this ->isTypo3Package ($ package ->getName ())) {
227236 continue ;
228237 }
229238
239+ // Check all typo3/cms-* requirements of this package
240+ $ incompatibleLinks = [];
230241 /** @var Link $link */
231242 foreach ($ package ->getRequires () as $ link ) {
232- if ($ link ->getTarget () === 'typo3/cms-core ' && !$ targetConstraint ->matches ($ link ->getConstraint ())) {
233- // Try stable first, fall back to pre-release versions
234- $ compatibleVersion = $ this ->findLatestCompatibleVersion ($ package , $ remoteRepositories , $ targetVersion , self ::STABLE_ONLY );
235- $ stable = true ;
236-
237- if (!$ compatibleVersion ) {
238- $ compatibleVersion = $ this ->findLatestCompatibleVersion ($ package , $ remoteRepositories , $ targetVersion , self ::ALL_STABILITIES );
239- $ stable = false ;
240- }
243+ if ($ this ->isTypo3Package ($ link ->getTarget ()) && !$ targetConstraint ->matches ($ link ->getConstraint ())) {
244+ $ incompatibleLinks [] = $ link ;
245+ }
246+ }
241247
242- [$ newConstraint , $ newDisplayVersion ] = $ this ->buildVersionStrings ($ compatibleVersion , $ stable );
248+ if (empty ($ incompatibleLinks )) {
249+ continue ;
250+ }
243251
244- $ upgrades [] = [
245- 'name ' => $ package ->getName (),
246- 'currentVersion ' => $ package ->getPrettyVersion (),
247- 'coreConstraint ' => $ link ->getPrettyConstraint (),
248- 'newConstraint ' => $ newConstraint ,
249- 'newDisplayVersion ' => $ newDisplayVersion ,
250- 'stable ' => $ stable ,
251- ];
252- }
252+ // Build a human-readable summary of the incompatible constraints
253+ $ constraintParts = [];
254+ foreach ($ incompatibleLinks as $ link ) {
255+ $ constraintParts [] = $ link ->getTarget () . ': ' . $ link ->getPrettyConstraint ();
256+ }
257+ $ coreConstraint = implode (', ' , $ constraintParts );
258+
259+ // Try stable first, fall back to pre-release versions
260+ $ compatibleVersion = $ this ->findLatestCompatibleVersion ($ package , $ remoteRepositories , $ targetVersion , self ::STABLE_ONLY );
261+ $ stable = true ;
262+
263+ if (!$ compatibleVersion ) {
264+ $ compatibleVersion = $ this ->findLatestCompatibleVersion ($ package , $ remoteRepositories , $ targetVersion , self ::PRE_RELEASE_STABILITIES );
265+ $ stable = false ;
253266 }
267+
268+ [$ newConstraint , $ newDisplayVersion ] = $ this ->buildVersionStrings ($ compatibleVersion , $ stable );
269+
270+ $ upgrades [] = [
271+ 'name ' => $ package ->getName (),
272+ 'currentVersion ' => $ package ->getPrettyVersion (),
273+ 'coreConstraint ' => $ coreConstraint ,
274+ 'newConstraint ' => $ newConstraint ,
275+ 'newDisplayVersion ' => $ newDisplayVersion ,
276+ 'stable ' => $ stable ,
277+ ];
254278 }
255279
256280 return $ upgrades ;
@@ -273,16 +297,6 @@ private function buildVersionStrings(?PackageInterface $package, bool $stable):
273297 return ['^ ' . $ prettyVersion , $ prettyVersion ];
274298 }
275299
276- // Dev packages: use the branch alias (dev-main, dev-master, etc.)
277- if ($ package ->isDev ()) {
278- $ sourceRef = $ package ->getSourceReference ();
279- $ displayVersion = $ prettyVersion ;
280- if ($ sourceRef ) {
281- $ displayVersion .= ' ( ' . substr ($ sourceRef , 0 , 7 ) . ') ' ;
282- }
283- return [$ prettyVersion , $ displayVersion ];
284- }
285-
286300 // Pre-release (RC, beta, alpha): use exact version
287301 return [$ prettyVersion , $ prettyVersion ];
288302 }
@@ -305,12 +319,21 @@ private function findLatestCompatibleVersion(PackageInterface $package, Composit
305319 );
306320
307321 foreach ($ results ['packages ' ] as $ candidate ) {
322+ $ compatible = true ;
323+ $ hasTypo3Requirement = false ;
308324 /** @var Link $link */
309325 foreach ($ candidate ->getRequires () as $ link ) {
310- if ($ link ->getTarget () === 'typo3/cms-core ' && $ targetConstraint ->matches ($ link ->getConstraint ())) {
311- return $ candidate ;
326+ if ($ this ->isTypo3Package ($ link ->getTarget ())) {
327+ $ hasTypo3Requirement = true ;
328+ if (!$ targetConstraint ->matches ($ link ->getConstraint ())) {
329+ $ compatible = false ;
330+ break ;
331+ }
312332 }
313333 }
334+ if ($ hasTypo3Requirement && $ compatible ) {
335+ return $ candidate ;
336+ }
314337 }
315338
316339 return null ;
0 commit comments