diff --git a/Makefile b/Makefile
index 34c6a1d..6de61c0 100644
--- a/Makefile
+++ b/Makefile
@@ -18,5 +18,5 @@ install:
.PHONY: test coverage open-coverage lint install
test/bin/php-cs-fixer:
- curl -sSL http://cs.sensiolabs.org/download/php-cs-fixer-v2.phar -o test/bin/php-cs-fixer
+ curl -sSL https://cs.sensiolabs.org/download/php-cs-fixer-v2.phar -o test/bin/php-cs-fixer
chmod +x test/bin/php-cs-fixer
diff --git a/README.md b/README.md
index 23600ae..43fab7b 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,31 @@ a dependency.
[npm]: https://npmjs.org/
[update]: https://npmjs.org/doc/update.html
+## Configuration
+
+The following configuration can be added to `composer.json` `extra/npm-bridge`
+to customize the behaviour on a per-package basis. Values in the root package
+will not currently impact any dependency packages that also use
+`composer-npm-bridge` - each package must define its own options.
+
+Key|Description
+-|-
+`timeout`|Specify a custom timeout for the installation. The default is 300 seconds.
+`optional`|Skip instead of throwing an exception if `npm` is not found when processing the package.
+
+ ...
+ "extra": {
+ "npm-bridge": {
+ "timeout": 9000,
+ "optional": true
+ },
+ ...
+ }
+
+To disable `composer-npm-bridge` entirely for a single run, set `COMPOSER_NPM_BRIDGE_DISABLE=1` on the environment.
+
+ COMPOSER_NPM_BRIDGE_DISABLE=1 composer install
+
## Caveats
Because NPM dependencies are installed underneath the root directory of the
diff --git a/src/NpmBridge.php b/src/NpmBridge.php
index a626d3f..afe5467 100644
--- a/src/NpmBridge.php
+++ b/src/NpmBridge.php
@@ -13,6 +13,10 @@
*/
class NpmBridge
{
+ const EXTRA_KEY = 'npm-bridge';
+ const EXTRA_KEY_OPTIONAL = 'optional';
+ const EXTRA_KEY_TIMEOUT = 'timeout';
+
/**
* Construct a new Composer NPM bridge plugin.
*
@@ -47,8 +51,12 @@ public function install(Composer $composer, bool $isDevMode = true)
'Installing NPM dependencies for root project'
);
- if ($this->isDependantPackage($composer->getPackage(), $isDevMode)) {
- $this->client->install(null, $isDevMode);
+ $package = $composer->getPackage();
+ if ($this->isDependantPackage($package, $isDevMode)) {
+ if (!$this->shouldSkipPackage($package)) {
+ $this->configureClient($package);
+ $this->client->install(null, $isDevMode);
+ }
} else {
$this->io->write('Nothing to install');
}
@@ -70,9 +78,13 @@ public function update(Composer $composer)
'Updating NPM dependencies for root project'
);
- if ($this->isDependantPackage($composer->getPackage(), true)) {
- $this->client->update();
- $this->client->install(null, true);
+ $package = $composer->getPackage();
+ if ($this->isDependantPackage($package, true)) {
+ if (!$this->shouldSkipPackage($package)) {
+ $this->configureClient($package);
+ $this->client->update();
+ $this->client->install(null, true);
+ }
} else {
$this->io->write('Nothing to update');
}
@@ -119,6 +131,10 @@ private function installForVendors($composer)
if (count($packages) > 0) {
foreach ($packages as $package) {
+ if ($this->shouldSkipPackage($package)) {
+ continue;
+ }
+
$this->io->write(
sprintf(
'Installing NPM dependencies for %s',
@@ -126,6 +142,7 @@ private function installForVendors($composer)
)
);
+ $this->configureClient($package);
$this->client->install(
$composer->getInstallationManager()
->getInstallPath($package),
@@ -137,6 +154,38 @@ private function installForVendors($composer)
}
}
+ private function configureClient(PackageInterface $package)
+ {
+ $extra = $package->getExtra();
+ // Issue #13 - npm can take a while, so allow a custom timeout
+ if (isset($extra[self::EXTRA_KEY][self::EXTRA_KEY_TIMEOUT])) {
+ $this->client->setTimeout(intval($extra[self::EXTRA_KEY][self::EXTRA_KEY_TIMEOUT]));
+ } else {
+ $this->client->setTimeout(null);
+ }
+ }
+
+ private function shouldSkipPackage(PackageInterface $package)
+ {
+ if ($this->client->valid()) {
+ return false;
+ }
+
+ $extra = $package->getExtra();
+ if (!empty($extra[self::EXTRA_KEY][self::EXTRA_KEY_OPTIONAL])) {
+ $this->io->write(
+ sprintf(
+ 'Skipping optional NPM dependencies for %s as npm is unavailable',
+ $package->getPrettyName()
+ )
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+
private $io;
private $vendorFinder;
private $client;
diff --git a/src/NpmBridgePlugin.php b/src/NpmBridgePlugin.php
index a9a9d75..c292273 100644
--- a/src/NpmBridgePlugin.php
+++ b/src/NpmBridgePlugin.php
@@ -52,9 +52,15 @@ class_exists(NpmVendorFinder::class);
*/
public static function getSubscribedEvents(): array
{
+ // Issue #18 - disable if ENV set
+ if (!empty(getenv('COMPOSER_NPM_BRIDGE_DISABLE'))) {
+ return [];
+ }
+
+ // Increased priority to ensure we run before custom installers which are usually default priority
return [
- ScriptEvents::POST_INSTALL_CMD => 'onPostInstallCmd',
- ScriptEvents::POST_UPDATE_CMD => 'onPostUpdateCmd',
+ ScriptEvents::POST_INSTALL_CMD => ['onPostInstallCmd', 1],
+ ScriptEvents::POST_UPDATE_CMD => ['onPostUpdateCmd', 1],
];
}
diff --git a/src/NpmClient.php b/src/NpmClient.php
index 199cdd3..e1ffcf9 100644
--- a/src/NpmClient.php
+++ b/src/NpmClient.php
@@ -43,6 +43,7 @@ public function __construct(
$this->executableFinder = $executableFinder;
$this->getcwd = $getcwd;
$this->chdir = $chdir;
+ $this->timeout = null;
}
/**
@@ -78,6 +79,32 @@ public function update(string $path = null)
$this->executeNpm(['update'], $path);
}
+ /**
+ * Set timeout. Null to use default
+ *
+ * @param int|null $timeout
+ */
+ public function setTimeout($timeout)
+ {
+ $this->timeout = $timeout;
+ }
+
+ /**
+ * Check is npm is available
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ try {
+ $this->npmPath();
+ } catch (NpmNotFoundException $err) {
+ return false;
+ }
+
+ return true;
+ }
+
private function executeNpm($arguments, $workingDirectoryPath)
{
array_unshift($arguments, $this->npmPath());
@@ -88,7 +115,14 @@ private function executeNpm($arguments, $workingDirectoryPath)
call_user_func($this->chdir, $workingDirectoryPath);
}
+ if (null !== $this->timeout) {
+ $oldTimeout = $this->processExecutor->getTimeout();
+ $this->processExecutor->setTimeout($this->timeout);
+ }
$exitCode = $this->processExecutor->execute($command);
+ if (null !== $this->timeout) {
+ $this->processExecutor->setTimeout($oldTimeout);
+ }
if (null !== $workingDirectoryPath) {
call_user_func($this->chdir, $previousWorkingDirectoryPath);
diff --git a/test/suite/NpmBridgePluginTest.php b/test/suite/NpmBridgePluginTest.php
index d411d82..9d129a9 100644
--- a/test/suite/NpmBridgePluginTest.php
+++ b/test/suite/NpmBridgePluginTest.php
@@ -47,10 +47,17 @@ public function testActivate()
public function testGetSubscribedEvents()
{
+ putenv('COMPOSER_NPM_BRIDGE_DISABLE=1');
+ $this->assertSame(
+ [],
+ $this->plugin->getSubscribedEvents()
+ );
+ putenv('COMPOSER_NPM_BRIDGE_DISABLE=');
+
$this->assertSame(
[
- ScriptEvents::POST_INSTALL_CMD => 'onPostInstallCmd',
- ScriptEvents::POST_UPDATE_CMD => 'onPostUpdateCmd',
+ ScriptEvents::POST_INSTALL_CMD => ['onPostInstallCmd', 1],
+ ScriptEvents::POST_UPDATE_CMD => ['onPostUpdateCmd', 1],
],
$this->plugin->getSubscribedEvents()
);
diff --git a/test/suite/NpmBridgeTest.php b/test/suite/NpmBridgeTest.php
index 7d5d84a..51d2265 100644
--- a/test/suite/NpmBridgeTest.php
+++ b/test/suite/NpmBridgeTest.php
@@ -6,6 +6,7 @@
use Composer\Package\Link;
use Composer\Package\Package;
use Composer\Package\RootPackage;
+use Eloquent\Composer\NpmBridge\Exception\NpmNotFoundException;
use Eloquent\Phony\Phpunit\Phony;
use PHPUnit\Framework\TestCase;
@@ -16,6 +17,7 @@ protected function setUp()
$this->io = Phony::mock('Composer\IO\IOInterface');
$this->vendorFinder = Phony::mock('Eloquent\Composer\NpmBridge\NpmVendorFinder');
$this->client = Phony::mock('Eloquent\Composer\NpmBridge\NpmClient');
+ $this->client->valid->returns(true);
$this->bridge = new NpmBridge($this->io->get(), $this->vendorFinder->get(), $this->client->get());
$this->composer = new Composer();
@@ -23,6 +25,9 @@ protected function setUp()
$this->rootPackage = new RootPackage('vendor/package', '1.0.0.0', '1.0.0');
$this->packageA = new Package('vendorA/packageA', '1.0.0.0', '1.0.0');
$this->packageB = new Package('vendorB/packageB', '1.0.0.0', '1.0.0');
+ $this->packageC = new Package('vendorC/packageC', '1.0.0.0', '1.0.0');
+
+ $this->packageC->setExtra([NpmBridge::EXTRA_KEY => [NpmBridge::EXTRA_KEY_OPTIONAL => true, NpmBridge::EXTRA_KEY_TIMEOUT => 900]]);
$this->linkRoot1 = new Link('vendor/package', 'vendorX/packageX');
$this->linkRoot2 = new Link('vendor/package', 'vendorY/packageY');
@@ -31,6 +36,7 @@ protected function setUp()
$this->installationManager = Phony::mock('Composer\Installer\InstallationManager');
$this->installationManager->getInstallPath->with($this->packageA)->returns('/path/to/install/a');
$this->installationManager->getInstallPath->with($this->packageB)->returns('/path/to/install/b');
+ $this->installationManager->getInstallPath->with($this->packageC)->returns('/path/to/install/c');
$this->composer->setPackage($this->rootPackage);
$this->composer->setInstallationManager($this->installationManager->get());
@@ -39,34 +45,48 @@ protected function setUp()
public function testInstall()
{
$this->rootPackage->setRequires([$this->linkRoot1, $this->linkRoot2, $this->linkRoot3]);
- $this->vendorFinder->find->with($this->composer, $this->bridge)->returns([$this->packageA, $this->packageB]);
+ $this->vendorFinder->find->with($this->composer, $this->bridge)->returns([$this->packageA, $this->packageB, $this->packageC]);
$this->bridge->install($this->composer);
Phony::inOrder(
$this->io->write->calledWith('Installing NPM dependencies for root project'),
+ $this->client->valid->calledWith(),
$this->client->install->calledWith(null, true),
$this->io->write->calledWith('Installing NPM dependencies for Composer dependencies'),
+ $this->client->valid->calledWith(),
$this->io->write->calledWith('Installing NPM dependencies for vendorA/packageA'),
$this->client->install->calledWith('/path/to/install/a', false),
+ $this->client->valid->calledWith(),
$this->io->write->calledWith('Installing NPM dependencies for vendorB/packageB'),
- $this->client->install->calledWith('/path/to/install/b', false)
+ $this->client->install->calledWith('/path/to/install/b', false),
+ $this->client->valid->calledWith(),
+ $this->io->write->calledWith('Installing NPM dependencies for vendorC/packageC'),
+ $this->client->setTimeout->calledWith(900),
+ $this->client->install->calledWith('/path/to/install/c', false)
);
}
public function testInstallProductionMode()
{
$this->rootPackage->setRequires([$this->linkRoot1, $this->linkRoot2, $this->linkRoot3]);
- $this->vendorFinder->find->with($this->composer, $this->bridge)->returns([$this->packageA, $this->packageB]);
+ $this->vendorFinder->find->with($this->composer, $this->bridge)->returns([$this->packageA, $this->packageB, $this->packageC]);
$this->bridge->install($this->composer, false);
Phony::inOrder(
$this->io->write->calledWith('Installing NPM dependencies for root project'),
+ $this->client->valid->calledWith(),
$this->client->install->calledWith(null, false),
$this->io->write->calledWith('Installing NPM dependencies for Composer dependencies'),
+ $this->client->valid->calledWith(),
$this->io->write->calledWith('Installing NPM dependencies for vendorA/packageA'),
$this->client->install->calledWith('/path/to/install/a', false),
+ $this->client->valid->calledWith(),
$this->io->write->calledWith('Installing NPM dependencies for vendorB/packageB'),
- $this->client->install->calledWith('/path/to/install/b', false)
+ $this->client->install->calledWith('/path/to/install/b', false),
+ $this->client->valid->calledWith(),
+ $this->io->write->calledWith('Installing NPM dependencies for vendorC/packageC'),
+ $this->client->setTimeout->calledWith(900),
+ $this->client->install->calledWith('/path/to/install/c', false)
);
}
@@ -76,6 +96,7 @@ public function testInstallRootDevDependenciesInDevMode()
$this->vendorFinder->find->with($this->composer, $this->bridge)->returns([]);
$this->bridge->install($this->composer, true);
+ $this->client->valid->calledWith();
$this->client->install->calledWith(null, true);
}
@@ -102,6 +123,31 @@ public function testInstallNothing()
);
}
+ public function testInstallOptional()
+ {
+ $this->client->valid->returns(false);
+ $this->client->install->with('/path/to/install/a')->throws(NpmNotFoundException::class);
+ $this->client->install->with('/path/to/install/c')->returns();
+
+ $this->rootPackage->setRequires([$this->linkRoot1, $this->linkRoot2]);
+ $this->vendorFinder->find->with($this->composer, $this->bridge)->returns([$this->packageC, $this->packageA]);
+ try {
+ $this->bridge->install($this->composer);
+ } catch (Exception $err) {
+ }
+
+ Phony::inOrder(
+ $this->io->write->calledWith('Installing NPM dependencies for root project'),
+ $this->io->write->calledWith('Nothing to install'),
+ $this->io->write->calledWith('Installing NPM dependencies for Composer dependencies'),
+ $this->client->valid->calledWith(),
+ $this->io->write->calledWith('Skipping optional NPM dependencies for vendorC/packageC as npm is unavailable'),
+ $this->client->valid->calledWith(),
+ $this->io->write->calledWith('Installing NPM dependencies for vendorA/packageA'),
+ $this->client->install->calledWith('/path/to/install/a', false)
+ );
+ }
+
public function testUpdate()
{
$this->rootPackage->setRequires([$this->linkRoot1, $this->linkRoot2, $this->linkRoot3]);
@@ -110,11 +156,14 @@ public function testUpdate()
Phony::inOrder(
$this->io->write->calledWith('Updating NPM dependencies for root project'),
+ $this->client->valid->calledWith(),
$this->client->update->calledWith(),
$this->client->install->calledWith(null, true),
$this->io->write->calledWith('Installing NPM dependencies for Composer dependencies'),
+ $this->client->valid->calledWith(),
$this->io->write->calledWith('Installing NPM dependencies for vendorA/packageA'),
$this->client->install->calledWith('/path/to/install/a', false),
+ $this->client->valid->calledWith(),
$this->io->write->calledWith('Installing NPM dependencies for vendorB/packageB'),
$this->client->install->calledWith('/path/to/install/b', false)
);
diff --git a/test/suite/NpmClientTest.php b/test/suite/NpmClientTest.php
index 501883e..2e3637e 100644
--- a/test/suite/NpmClientTest.php
+++ b/test/suite/NpmClientTest.php
@@ -17,23 +17,47 @@ protected function setUp()
new NpmClient($this->processExecutor->get(), $this->executableFinder->get(), $this->getcwd, $this->chdir);
$this->processExecutor->execute->returns(0);
+ Phony::onStatic($this->processExecutor)->getTimeout->returns(300);
+ Phony::onStatic($this->processExecutor)->setTimeout->returns();
$this->executableFinder->find->with('npm')->returns('/path/to/npm');
$this->getcwd->returns('/path/to/cwd');
}
public function testInstall()
{
- $this->assertNull($this->client->install('/path/to/project'));
+ $this->assertNull($this->client->install());
+ Phony::inOrder(
+ $this->executableFinder->find->calledWith('npm'),
+ $this->processExecutor->execute->calledWith("'/path/to/npm' 'install'")
+ );
+ $this->chdir->never()->returned();
+ $this->processExecutor->setTimeout->never()->returned();
+ }
+
+ public function testInstallWorking()
+ {
$this->assertNull($this->client->install('/path/to/project'));
Phony::inOrder(
$this->executableFinder->find->calledWith('npm'),
$this->chdir->calledWith('/path/to/project'),
$this->processExecutor->execute->calledWith("'/path/to/npm' 'install'"),
- $this->chdir->calledWith('/path/to/cwd'),
- $this->chdir->calledWith('/path/to/project'),
- $this->processExecutor->execute->calledWith("'/path/to/npm' 'install'"),
$this->chdir->calledWith('/path/to/cwd')
);
+ $this->processExecutor->setTimeout->never()->returned();
+ }
+
+ public function testInstallTimeout()
+ {
+ $this->assertNull($this->client->setTimeout(900));
+ $this->assertNull($this->client->install());
+ Phony::inOrder(
+ $this->executableFinder->find->calledWith('npm'),
+ Phony::onStatic($this->processExecutor)->getTimeout->calledWith(),
+ Phony::onStatic($this->processExecutor)->setTimeout->calledWith(900),
+ $this->processExecutor->execute->calledWith("'/path/to/npm' 'install'"),
+ Phony::onStatic($this->processExecutor)->setTimeout->calledWith(300)
+ );
+ $this->chdir->never()->returned();
}
public function testInstallProductionMode()
@@ -65,17 +89,39 @@ public function testInstallFailureCommandFailed()
public function testUpdate()
{
- $this->assertNull($this->client->update('/path/to/project'));
+ $this->assertNull($this->client->update());
+ Phony::inOrder(
+ $this->executableFinder->find->calledWith('npm'),
+ $this->processExecutor->execute->calledWith("'/path/to/npm' 'update'")
+ );
+ $this->chdir->never()->returned();
+ $this->processExecutor->setTimeout->never()->returned();
+ }
+
+ public function testUpdateWorking()
+ {
$this->assertNull($this->client->update('/path/to/project'));
Phony::inOrder(
$this->executableFinder->find->calledWith('npm'),
$this->chdir->calledWith('/path/to/project'),
$this->processExecutor->execute->calledWith("'/path/to/npm' 'update'"),
- $this->chdir->calledWith('/path/to/cwd'),
- $this->chdir->calledWith('/path/to/project'),
- $this->processExecutor->execute->calledWith("'/path/to/npm' 'update'"),
$this->chdir->calledWith('/path/to/cwd')
);
+ $this->processExecutor->setTimeout->never()->returned();
+ }
+
+ public function testUpdateTimeout()
+ {
+ $this->assertNull($this->client->setTimeout(900));
+ $this->assertNull($this->client->update());
+ Phony::inOrder(
+ $this->executableFinder->find->calledWith('npm'),
+ Phony::onStatic($this->processExecutor)->getTimeout->calledWith(),
+ Phony::onStatic($this->processExecutor)->setTimeout->calledWith(900),
+ $this->processExecutor->execute->calledWith("'/path/to/npm' 'update'"),
+ Phony::onStatic($this->processExecutor)->setTimeout->calledWith(300)
+ );
+ $this->chdir->never()->returned();
}
public function testUpdateFailureNpmNotFound()
@@ -93,4 +139,19 @@ public function testUpdateFailureCommandFailed()
$this->expectException('Eloquent\Composer\NpmBridge\Exception\NpmCommandFailedException');
$this->client->update('/path/to/project');
}
+
+ public function testValid()
+ {
+ $this->executableFinder->find->with('npm')->returns(null);
+ $this->assertSame(
+ false,
+ $this->client->valid()
+ );
+
+ $this->executableFinder->find->with('npm')->returns('/path/to/npm');
+ $this->assertSame(
+ true,
+ $this->client->valid()
+ );
+ }
}