diff --git a/src/Installer.php b/src/Installer.php
index 90c6ff8..d57eb61 100644
--- a/src/Installer.php
+++ b/src/Installer.php
@@ -30,8 +30,6 @@ class Installer extends LibraryInstaller
const KNOWN_FRAMEWORKS = ['vue', 'react', 'svelte'];
- protected bool $skipUpdateCode = false;
-
const UPDATE_STRATEGY_MERGE = 'merge';
const UPDATE_STRATEGY_OVERWRITE = 'overwrite';
@@ -304,6 +302,42 @@ protected function isLocallyTracked(string $path): bool
return is_link($path) || is_dir($path.'/.git');
}
+ /**
+ * Returns true when a package has dist.type=path but its install path is NOT locally
+ * tracked (no symlink, no .git). This identifies a Packagist-installed module that was
+ * re-resolved by the modules/* path repository on a subsequent composer update.
+ * The files are already in place; no download should occur.
+ */
+ protected function isInstalledModuleResolvedAsPath(PackageInterface $package): bool
+ {
+ return $package->getDistType() === 'path'
+ && ! $this->isLocallyTracked($this->getInstallPath($package));
+ }
+
+ /**
+ * Returns the package to register in installed.json after a path-skip scenario.
+ * When initial has non-path dist info (Packagist zip/tar), returns a clone of target
+ * with initial's dist metadata copied over so the lock file retains the Packagist
+ * source rather than recording dist.type=path.
+ * When initial is also path (already-polluted lock), returns target unchanged.
+ */
+ protected function resolveRegistrationTarget(
+ PackageInterface $initial,
+ PackageInterface $target
+ ): PackageInterface {
+ if ($initial->getDistType() !== null && $initial->getDistType() !== 'path') {
+ $clone = clone $target;
+ $clone->setDistType($initial->getDistType());
+ $clone->setDistUrl($initial->getDistUrl());
+ $clone->setDistReference($initial->getDistReference());
+ $clone->setDistSha1Checksum($initial->getDistSha1Checksum());
+
+ return $clone;
+ }
+
+ return $target;
+ }
+
/**
* Returns the path to frontend.json. Extracted for testability.
*/
@@ -427,7 +461,6 @@ private function copyDirectory(string $source, string $dest): void
/**
* Proxy for parent::install() — extracted for testability.
- * Mirrors the invokeParentUpdateCode() pattern.
*/
protected function parentInstall(InstalledRepositoryInterface $repo, PackageInterface $package): PromiseInterface
{
@@ -462,53 +495,29 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa
return \React\Promise\resolve(null);
}
- $promise = $this->parentInstall($repo, $package);
+ // Module is already installed (re-resolved by modules/* path repo on a repeat run).
+ // Files are in place — just register and skip the download.
+ // Note: if dist.type=path but the dir is absent (fresh clone from a polluted lock),
+ // this guard does not fire and parentInstall will fail. Fix the lock with composer update.
+ if ($package->getDistType() === 'path'
+ && ! $this->isLocallyTracked($this->getInstallPath($package))
+ && is_dir($this->getInstallPath($package))
+ ) {
+ $this->io->write(" - Skipping install (module already present, resolved as path by modules/* repo): {$package->getPrettyName()}");
- return $promise->then(function () use ($package) {
- $this->removeExcludedDirectories($package);
- $this->copyFrameworkFiles($package);
- });
- }
-
- /**
- * Call parent::update() solely for its installed.json tracking side-effects.
- * Sets the skip flag so updateCode() is a no-op, then resets it regardless of outcome.
- */
- protected function delegateRepoTracking(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target): PromiseInterface
- {
- $this->skipUpdateCode = true;
-
- $resetFlag = function (): void {
- $this->skipUpdateCode = false;
- };
-
- return parent::update($repo, $initial, $target)->then(
- $resetFlag,
- function (\Throwable $e) use ($resetFlag): never {
- $resetFlag();
- throw $e;
+ if (! $repo->hasPackage($package)) {
+ $repo->addPackage(clone $package);
}
- );
- }
- /**
- * No-op hook for parent::update() — skipped when we have already handled the download ourselves.
- * Overriding this lets parent::update() run only for its repo-tracking side-effects.
- *
- * {@inheritDoc}
- */
- protected function updateCode(PackageInterface $initial, PackageInterface $target): PromiseInterface
- {
- if ($this->skipUpdateCode) {
return \React\Promise\resolve(null);
}
- return $this->invokeParentUpdateCode($initial, $target);
- }
+ $promise = $this->parentInstall($repo, $package);
- protected function invokeParentUpdateCode(PackageInterface $initial, PackageInterface $target): PromiseInterface
- {
- return parent::updateCode($initial, $target);
+ return $promise->then(function () use ($package) {
+ $this->removeExcludedDirectories($package);
+ $this->copyFrameworkFiles($package);
+ });
}
/**
@@ -564,6 +573,26 @@ public function update(InstalledRepositoryInterface $repo, PackageInterface $ini
return \React\Promise\resolve(null);
}
+
+ // Target was re-resolved by modules/* path repo (not a dev repo).
+ // Files are already in place at the same version — skip download, update tracking only.
+ // Copies initial's dist info onto the registered clone so the lock records zip/tar
+ // rather than dist.type=path, preventing future composer install failures on fresh clones.
+ if ($this->isInstalledModuleResolvedAsPath($target)) {
+ $this->io->write(" - Skipping download (module resolved as path by modules/* repo): {$target->getPrettyName()}");
+ $registrationTarget = $this->resolveRegistrationTarget($initial, $target);
+ $this->binaryInstaller->removeBinaries($initial);
+ $this->binaryInstaller->installBinaries($registrationTarget, $installPath);
+ if ($repo->hasPackage($initial)) {
+ $repo->removePackage($initial);
+ }
+ if (! $repo->hasPackage($registrationTarget)) {
+ $repo->addPackage(clone $registrationTarget);
+ }
+
+ return \React\Promise\resolve(null);
+ }
+
$stashPath = null;
$basePath = null;
@@ -609,23 +638,17 @@ function () use ($repo, $initial, $target, $installPath, $stashPath, $basePath)
(new Filesystem)->removeDirectory($stashPath);
}
- // When upgrading FROM a path package, parent::update() invokes
- // PathDownloader against the old path source which may not exist.
- // Track the repo manually — we have already placed the files.
- if ($initial->getDistType() === 'path') {
- if ($repo->hasPackage($initial)) {
- $repo->removePackage($initial);
- }
- if (! $repo->hasPackage($target)) {
- $repo->addPackage(clone $target);
- }
-
- return \React\Promise\resolve(null);
+ // Direct repo tracking — equivalent to parent::update() minus the download.
+ $this->binaryInstaller->removeBinaries($initial);
+ $this->binaryInstaller->installBinaries($target, $installPath);
+ if ($repo->hasPackage($initial)) {
+ $repo->removePackage($initial);
+ }
+ if (! $repo->hasPackage($target)) {
+ $repo->addPackage(clone $target);
}
- // Delegate repo tracking (installed.json) to parent — skip the download step
- // since we have already placed the files ourselves.
- return $this->delegateRepoTracking($repo, $initial, $target);
+ return \React\Promise\resolve(null);
},
function (\Throwable $e) use ($stashPath, $basePath, $initial) {
if ($stashPath !== null) {
diff --git a/tests/ModuleInstallerTest.php b/tests/ModuleInstallerTest.php
index 770d56d..c650218 100644
--- a/tests/ModuleInstallerTest.php
+++ b/tests/ModuleInstallerTest.php
@@ -5,6 +5,7 @@
namespace Tests;
use Composer\Composer;
+use Composer\Installer\BinaryInstaller;
use Composer\IO\IOInterface;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
@@ -29,6 +30,15 @@ public function __construct(?IOInterface $io = null, ?Composer $composer = null)
{
$this->io = $io;
$this->composer = $composer;
+ // Provide a no-op binaryInstaller since we bypass LibraryInstaller's constructor.
+ $this->binaryInstaller = new class extends BinaryInstaller
+ {
+ public function __construct() {}
+
+ public function removeBinaries(PackageInterface $package): void {}
+
+ public function installBinaries(PackageInterface $package, string $installPath, bool $warnOnOverwrite = true): void {}
+ };
}
public function callGetModuleName(PackageInterface $package): string
@@ -66,23 +76,14 @@ public function callIsPathRepository(PackageInterface $package): bool
return parent::isPathRepository($package);
}
- public function callUpdateCode(PackageInterface $initial, PackageInterface $target): PromiseInterface
- {
- return $this->updateCode($initial, $target);
- }
-
- public function setSkipUpdateCode(bool $value): void
+ public function callIsInstalledModuleResolvedAsPath(PackageInterface $package): bool
{
- $this->skipUpdateCode = $value;
+ return parent::isInstalledModuleResolvedAsPath($package);
}
- public bool $parentUpdateCodeInvoked = false;
-
- protected function invokeParentUpdateCode(PackageInterface $initial, PackageInterface $target): PromiseInterface
+ public function callResolveRegistrationTarget(PackageInterface $initial, PackageInterface $target): PackageInterface
{
- $this->parentUpdateCodeInvoked = true;
-
- return parent::invokeParentUpdateCode($initial, $target);
+ return parent::resolveRegistrationTarget($initial, $target);
}
// ---- Framework-aware file copying ----
@@ -133,8 +134,12 @@ protected function copyFrameworkFiles(PackageInterface $package): void
parent::copyFrameworkFiles($package);
}
+ public bool $parentInstallInvoked = false;
+
protected function parentInstall(InstalledRepositoryInterface $repo, PackageInterface $package): PromiseInterface
{
+ $this->parentInstallInvoked = true;
+
return \React\Promise\resolve(null);
}
@@ -152,15 +157,6 @@ protected function downloadFresh(PackageInterface $package, string $path): Promi
{
return \React\Promise\resolve(null);
}
-
- public bool $delegateRepoTrackingInvoked = false;
-
- protected function delegateRepoTracking(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target): PromiseInterface
- {
- $this->delegateRepoTrackingInvoked = true;
-
- return \React\Promise\resolve(null);
- }
}
final class ModuleInstallerTest extends TestCase
@@ -920,12 +916,11 @@ public function test_update_skips_base_download_and_delegates_repo_manually_when
$installer->update($repo, $initial, $target);
$this->assertFalse($installer->downloadBaseInvoked);
- $this->assertFalse($installer->delegateRepoTrackingInvoked);
(new Filesystem)->remove($baseDir);
}
- public function test_update_downloads_base_and_delegates_repo_to_parent_when_initial_is_not_path_type(): void
+ public function test_update_downloads_base_and_does_direct_repo_tracking_when_initial_is_not_path_type(): void
{
$baseDir = sys_get_temp_dir().'/update-zip-initial-'.uniqid('', true);
mkdir($baseDir.'/test-module', 0755, true);
@@ -941,37 +936,312 @@ public function test_update_downloads_base_and_delegates_repo_to_parent_when_ini
$target = new Package('saucebase/test-module', '2.0.0.0', '2.0.0');
$target->setDistType('zip');
- $repo = $this->createStub(InstalledRepositoryInterface::class);
+ $repo = $this->createMock(InstalledRepositoryInterface::class);
+ $repo->method('hasPackage')->willReturnCallback(fn ($p) => $p === $initial);
+ $repo->expects($this->once())->method('removePackage')->with($initial);
+ $repo->expects($this->once())->method('addPackage');
$installer = new TestableInstaller($this->createStub(IOInterface::class), $composer);
$installer->update($repo, $initial, $target);
$this->assertTrue($installer->downloadBaseInvoked);
- $this->assertTrue($installer->delegateRepoTrackingInvoked);
(new Filesystem)->remove($baseDir);
}
// -------------------------------------------------------------------------
- // updateCode() skip flag
+ // isInstalledModuleResolvedAsPath
// -------------------------------------------------------------------------
- public function test_update_code_returns_resolved_promise_when_skip_flag_is_set(): void
+ public function test_is_installed_module_resolved_as_path_returns_true_for_real_dir_with_path_dist(): void
{
- $io = $this->createStub(IOInterface::class);
- $installer = new TestableInstaller($io, null);
- $installer->setSkipUpdateCode(true);
+ $baseDir = sys_get_temp_dir().'/is-installed-real-'.uniqid('', true);
+ mkdir($baseDir.'/test-module', 0755, true);
- $initial = $this->createStub(PackageInterface::class);
- $target = $this->createStub(PackageInterface::class);
+ $installer = $this->makeInstallerWithModuleDir($baseDir);
+
+ $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $pkg->setDistType('path');
+
+ $this->assertTrue($installer->callIsInstalledModuleResolvedAsPath($pkg));
+
+ (new Filesystem)->remove($baseDir);
+ }
+
+ public function test_is_installed_module_resolved_as_path_returns_false_for_symlink(): void
+ {
+ $baseDir = sys_get_temp_dir().'/is-installed-symlink-'.uniqid('', true);
+ $target = sys_get_temp_dir().'/is-installed-symlink-src-'.uniqid('', true);
+ mkdir($target, 0755, true);
+ mkdir($baseDir, 0755, true);
+ symlink($target, $baseDir.'/test-module');
+
+ $installer = $this->makeInstallerWithModuleDir($baseDir);
+
+ $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $pkg->setDistType('path');
+
+ $this->assertFalse($installer->callIsInstalledModuleResolvedAsPath($pkg));
+
+ (new Filesystem)->remove($baseDir);
+ (new Filesystem)->remove($target);
+ }
+
+ public function test_is_installed_module_resolved_as_path_returns_false_for_git_dir(): void
+ {
+ $baseDir = sys_get_temp_dir().'/is-installed-git-'.uniqid('', true);
+ mkdir($baseDir.'/test-module/.git', 0755, true);
+
+ $installer = $this->makeInstallerWithModuleDir($baseDir);
+
+ $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $pkg->setDistType('path');
+
+ $this->assertFalse($installer->callIsInstalledModuleResolvedAsPath($pkg));
+
+ (new Filesystem)->remove($baseDir);
+ }
+
+ public function test_is_installed_module_resolved_as_path_returns_false_for_non_path_dist(): void
+ {
+ $baseDir = sys_get_temp_dir().'/is-installed-zip-'.uniqid('', true);
+ mkdir($baseDir.'/test-module', 0755, true);
+
+ $installer = $this->makeInstallerWithModuleDir($baseDir);
+
+ $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $pkg->setDistType('zip');
+
+ $this->assertFalse($installer->callIsInstalledModuleResolvedAsPath($pkg));
+
+ (new Filesystem)->remove($baseDir);
+ }
+
+ // -------------------------------------------------------------------------
+ // resolveRegistrationTarget
+ // -------------------------------------------------------------------------
+
+ public function test_resolve_registration_target_copies_non_path_dist_from_initial(): void
+ {
+ $installer = new TestableInstaller($this->createStub(IOInterface::class), null);
+
+ $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $initial->setDistType('zip');
+ $initial->setDistUrl('https://api.github.com/repos/saucebase-dev/blog/zipball/abc123');
+ $initial->setDistReference('abc123');
+
+ $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $target->setDistType('path');
+ $target->setDistUrl('modules/test-module');
+
+ $result = $installer->callResolveRegistrationTarget($initial, $target);
+
+ $this->assertSame('zip', $result->getDistType());
+ $this->assertSame($initial->getDistUrl(), $result->getDistUrl());
+ $this->assertSame($initial->getDistReference(), $result->getDistReference());
+ $this->assertNotSame($target, $result, 'should return a clone, not mutate the original');
+ }
+
+ public function test_resolve_registration_target_returns_target_unchanged_when_initial_is_also_path(): void
+ {
+ $installer = new TestableInstaller($this->createStub(IOInterface::class), null);
+
+ $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $initial->setDistType('path');
+
+ $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $target->setDistType('path');
+ $target->setDistUrl('modules/test-module');
+
+ $result = $installer->callResolveRegistrationTarget($initial, $target);
+
+ $this->assertSame($target, $result);
+ $this->assertSame('path', $result->getDistType());
+ }
+
+ // -------------------------------------------------------------------------
+ // update() — isInstalledModuleResolvedAsPath guard
+ // -------------------------------------------------------------------------
+
+ public function test_update_does_not_crash_when_target_is_installed_module_resolved_as_path(): void
+ {
+ $baseDir = sys_get_temp_dir().'/update-path-crash-'.uniqid('', true);
+ mkdir($baseDir.'/test-module', 0755, true);
+
+ $composer = $this->createStub(Composer::class);
+ $root = new RootPackage('root/app', '1.0.0.0', '1.0.0');
+ $root->setExtra(['module-dir' => $baseDir]);
+ $composer->method('getPackage')->willReturn($root);
+
+ $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $initial->setDistType('zip');
+
+ $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $target->setDistType('path');
+ $target->setDistUrl($baseDir.'/test-module');
+
+ $repo = $this->createStub(InstalledRepositoryInterface::class);
+ $repo->method('hasPackage')->willReturn(false);
+
+ $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer);
$resolved = false;
- $installer->callUpdateCode($initial, $target)->then(function () use (&$resolved) {
+ $installer->update($repo, $initial, $target)->then(function () use (&$resolved) {
$resolved = true;
});
- $this->assertTrue($resolved, 'updateCode should resolve immediately when skip flag is set');
- $this->assertFalse($installer->parentUpdateCodeInvoked, 'parent::updateCode() must not be called when skip flag is set');
+ $this->assertTrue($resolved);
+ $this->assertFalse($installer->downloadBaseInvoked, 'should not attempt to download base');
+
+ (new Filesystem)->remove($baseDir);
+ }
+
+ public function test_update_does_not_stash_module_dir_when_target_is_installed_module_resolved_as_path(): void
+ {
+ $baseDir = sys_get_temp_dir().'/update-path-nostash-'.uniqid('', true);
+ mkdir($baseDir.'/test-module', 0755, true);
+ file_put_contents($baseDir.'/test-module/file.php', 'createStub(Composer::class);
+ $root = new RootPackage('root/app', '1.0.0.0', '1.0.0');
+ $root->setExtra(['module-dir' => $baseDir]);
+ $composer->method('getPackage')->willReturn($root);
+
+ $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $initial->setDistType('zip');
+
+ $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $target->setDistType('path');
+
+ $repo = $this->createStub(InstalledRepositoryInterface::class);
+ $repo->method('hasPackage')->willReturn(false);
+
+ $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer);
+ $installer->update($repo, $initial, $target);
+
+ $this->assertDirectoryExists($baseDir.'/test-module', 'module dir must not be stashed or removed');
+ $this->assertFileExists($baseDir.'/test-module/file.php', 'user files must survive');
+
+ (new Filesystem)->remove($baseDir);
+ }
+
+ public function test_update_preserves_dist_info_from_initial_when_target_is_installed_module_resolved_as_path(): void
+ {
+ $baseDir = sys_get_temp_dir().'/update-path-distfix-'.uniqid('', true);
+ mkdir($baseDir.'/test-module', 0755, true);
+
+ $composer = $this->createStub(Composer::class);
+ $root = new RootPackage('root/app', '1.0.0.0', '1.0.0');
+ $root->setExtra(['module-dir' => $baseDir]);
+ $composer->method('getPackage')->willReturn($root);
+
+ $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $initial->setDistType('zip');
+ $initial->setDistUrl('https://api.github.com/repos/saucebase-dev/test/zipball/abc');
+ $initial->setDistReference('abc');
+
+ $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $target->setDistType('path');
+ $target->setDistUrl($baseDir.'/test-module');
+
+ $registered = null;
+ $repo = $this->createMock(InstalledRepositoryInterface::class);
+ $repo->method('hasPackage')->willReturn(false);
+ $repo->expects($this->once())->method('addPackage')->willReturnCallback(
+ function (PackageInterface $p) use (&$registered) {
+ $registered = $p;
+ }
+ );
+
+ $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer);
+ $installer->update($repo, $initial, $target);
+
+ $this->assertNotNull($registered);
+ $this->assertSame('zip', $registered->getDistType(), 'lock must record zip dist, not path');
+ $this->assertSame($initial->getDistUrl(), $registered->getDistUrl());
+
+ (new Filesystem)->remove($baseDir);
+ }
+
+ public function test_update_keeps_path_dist_when_initial_is_also_path_type(): void
+ {
+ $baseDir = sys_get_temp_dir().'/update-path-both-'.uniqid('', true);
+ mkdir($baseDir.'/test-module', 0755, true);
+
+ $composer = $this->createStub(Composer::class);
+ $root = new RootPackage('root/app', '1.0.0.0', '1.0.0');
+ $root->setExtra(['module-dir' => $baseDir]);
+ $composer->method('getPackage')->willReturn($root);
+
+ $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $initial->setDistType('path');
+
+ $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $target->setDistType('path');
+
+ $repo = $this->createMock(InstalledRepositoryInterface::class);
+ $repo->method('hasPackage')->willReturn(false);
+ $repo->expects($this->once())->method('addPackage');
+
+ $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer);
+
+ $resolved = false;
+ $installer->update($repo, $initial, $target)->then(function () use (&$resolved) {
+ $resolved = true;
+ });
+
+ $this->assertTrue($resolved, 'must not throw when both initial and target are path type');
+
+ (new Filesystem)->remove($baseDir);
+ }
+
+ // -------------------------------------------------------------------------
+ // install() — installed-module-as-path guard
+ // -------------------------------------------------------------------------
+
+ public function test_install_skips_download_when_module_dir_exists_with_path_dist(): void
+ {
+ $baseDir = sys_get_temp_dir().'/install-path-exists-'.uniqid('', true);
+ mkdir($baseDir.'/test-module', 0755, true);
+
+ $installer = $this->makeInstallerWithModuleDir($baseDir);
+
+ $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $pkg->setDistType('path');
+
+ $repo = $this->createMock(InstalledRepositoryInterface::class);
+ $repo->method('hasPackage')->willReturn(false);
+ $repo->expects($this->once())->method('addPackage');
+
+ $resolved = false;
+ $installer->install($repo, $pkg)->then(function () use (&$resolved) {
+ $resolved = true;
+ });
+
+ $this->assertTrue($resolved);
+ $this->assertFalse($installer->parentInstallInvoked, 'parentInstall must not be called when dir already exists');
+
+ (new Filesystem)->remove($baseDir);
+ }
+
+ public function test_install_falls_through_to_parent_when_module_dir_absent_with_path_dist(): void
+ {
+ $baseDir = sys_get_temp_dir().'/install-path-absent-'.uniqid('', true);
+ mkdir($baseDir, 0755, true); // base exists but test-module subdir does NOT
+
+ $installer = $this->makeInstallerWithModuleDir($baseDir);
+
+ $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0');
+ $pkg->setDistType('path');
+
+ $repo = $this->createStub(InstalledRepositoryInterface::class);
+ $repo->method('hasPackage')->willReturn(false);
+
+ $installer->install($repo, $pkg);
+
+ $this->assertTrue($installer->parentInstallInvoked, 'parentInstall must be called when dir is absent');
+
+ (new Filesystem)->remove($baseDir);
}
// -------------------------------------------------------------------------