Skip to content
This repository was archived by the owner on Jul 8, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 54 additions & 5 deletions src/NpmBridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -47,8 +51,12 @@ public function install(Composer $composer, bool $isDevMode = true)
'<info>Installing NPM dependencies for root project</info>'
);

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');
}
Expand All @@ -70,9 +78,13 @@ public function update(Composer $composer)
'<info>Updating NPM dependencies for root project</info>'
);

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');
}
Expand Down Expand Up @@ -119,13 +131,18 @@ private function installForVendors($composer)

if (count($packages) > 0) {
foreach ($packages as $package) {
if ($this->shouldSkipPackage($package)) {
continue;
}

$this->io->write(
sprintf(
'<info>Installing NPM dependencies for %s</info>',
$package->getPrettyName()
)
);

$this->configureClient($package);
$this->client->install(
$composer->getInstallationManager()
->getInstallPath($package),
Expand All @@ -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(
'<info>Skipping optional NPM dependencies for %s as npm is unavailable</info>',
$package->getPrettyName()
)
);

return true;
}

return false;
}

private $io;
private $vendorFinder;
private $client;
Expand Down
10 changes: 8 additions & 2 deletions src/NpmBridgePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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],
];
}

Expand Down
34 changes: 34 additions & 0 deletions src/NpmClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public function __construct(
$this->executableFinder = $executableFinder;
$this->getcwd = $getcwd;
$this->chdir = $chdir;
$this->timeout = null;
}

/**
Expand Down Expand Up @@ -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());
Expand All @@ -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);
Expand Down
11 changes: 9 additions & 2 deletions test/suite/NpmBridgePluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
Expand Down
57 changes: 53 additions & 4 deletions test/suite/NpmBridgeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -16,13 +17,17 @@ 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();

$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');
Expand All @@ -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());
Expand All @@ -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('<info>Installing NPM dependencies for root project</info>'),
$this->client->valid->calledWith(),
$this->client->install->calledWith(null, true),
$this->io->write->calledWith('<info>Installing NPM dependencies for Composer dependencies</info>'),
$this->client->valid->calledWith(),
$this->io->write->calledWith('<info>Installing NPM dependencies for vendorA/packageA</info>'),
$this->client->install->calledWith('/path/to/install/a', false),
$this->client->valid->calledWith(),
$this->io->write->calledWith('<info>Installing NPM dependencies for vendorB/packageB</info>'),
$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('<info>Installing NPM dependencies for vendorC/packageC</info>'),
$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('<info>Installing NPM dependencies for root project</info>'),
$this->client->valid->calledWith(),
$this->client->install->calledWith(null, false),
$this->io->write->calledWith('<info>Installing NPM dependencies for Composer dependencies</info>'),
$this->client->valid->calledWith(),
$this->io->write->calledWith('<info>Installing NPM dependencies for vendorA/packageA</info>'),
$this->client->install->calledWith('/path/to/install/a', false),
$this->client->valid->calledWith(),
$this->io->write->calledWith('<info>Installing NPM dependencies for vendorB/packageB</info>'),
$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('<info>Installing NPM dependencies for vendorC/packageC</info>'),
$this->client->setTimeout->calledWith(900),
$this->client->install->calledWith('/path/to/install/c', false)
);
}

Expand All @@ -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);
}

Expand All @@ -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('<info>Installing NPM dependencies for root project</info>'),
$this->io->write->calledWith('Nothing to install'),
$this->io->write->calledWith('<info>Installing NPM dependencies for Composer dependencies</info>'),
$this->client->valid->calledWith(),
$this->io->write->calledWith('<info>Skipping optional NPM dependencies for vendorC/packageC as npm is unavailable</info>'),
$this->client->valid->calledWith(),
$this->io->write->calledWith('<info>Installing NPM dependencies for vendorA/packageA</info>'),
$this->client->install->calledWith('/path/to/install/a', false)
);
}

public function testUpdate()
{
$this->rootPackage->setRequires([$this->linkRoot1, $this->linkRoot2, $this->linkRoot3]);
Expand All @@ -110,11 +156,14 @@ public function testUpdate()

Phony::inOrder(
$this->io->write->calledWith('<info>Updating NPM dependencies for root project</info>'),
$this->client->valid->calledWith(),
$this->client->update->calledWith(),
$this->client->install->calledWith(null, true),
$this->io->write->calledWith('<info>Installing NPM dependencies for Composer dependencies</info>'),
$this->client->valid->calledWith(),
$this->io->write->calledWith('<info>Installing NPM dependencies for vendorA/packageA</info>'),
$this->client->install->calledWith('/path/to/install/a', false),
$this->client->valid->calledWith(),
$this->io->write->calledWith('<info>Installing NPM dependencies for vendorB/packageB</info>'),
$this->client->install->calledWith('/path/to/install/b', false)
);
Expand Down
Loading