├── .gitattributes ├── .gitignore ├── .php_cs ├── Makefile ├── CONTRIBUTING.md ├── src ├── Exception │ ├── NpmNotFoundException.php │ └── NpmCommandFailedException.php ├── NpmVendorFinder.php ├── NpmBridgeFactory.php ├── NpmBridgePlugin.php ├── NpmClient.php └── NpmBridge.php ├── phpunit.xml ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── composer.json ├── CHANGELOG.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | /test/ export-ignore 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.makefiles/ 2 | /artifacts/ 3 | /composer.lock 4 | /vendor/ 5 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setCacheFile(__DIR__ . '/artifacts/lint/php-cs-fixer/cache'); 5 | 6 | return $config; 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Powered by https://makefiles.dev/ 2 | 3 | -include .makefiles/Makefile 4 | -include .makefiles/pkg/php/v1/Makefile 5 | 6 | .makefiles/%: 7 | @curl -sfL https://makefiles.dev/v1 | bash /dev/stdin "$@" 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | As a guideline, please follow this process when contributing: 4 | 5 | 1. [Fork the repository] 6 | 2. [Create a branch] 7 | 3. Make your changes 8 | 4. Use `make precommit` to run tests and code style checks 9 | 5. [Squash commits] if necessary 10 | 6. [Create a pull request] 11 | 12 | [create a branch]: https://help.github.com/articles/about-branches 13 | [create a pull request]: https://help.github.com/articles/creating-a-pull-request 14 | [fork the repository]: https://help.github.com/articles/fork-a-repo 15 | [squash commits]: https://help.github.com/articles/about-git-rebase 16 | -------------------------------------------------------------------------------- /src/Exception/NpmNotFoundException.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | test/suite 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | src 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: 0 14 * * 0 # Sunday 2PM UTC = Monday 12AM AEST 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | php: ['7.2', '7.3', '7.4'] 14 | name: PHP ${{ matrix.php }} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v1 18 | - name: Set up PHP 19 | uses: shivammathur/setup-php@master 20 | with: 21 | php-version: ${{ matrix.php }} 22 | extensions: mbstring 23 | coverage: pcov 24 | - name: Check PHP version 25 | run: php -v 26 | - name: Install dependencies 27 | run: make vendor 28 | - name: Make 29 | run: make ci 30 | - name: Upload coverage reports 31 | if: success() 32 | run: | 33 | test ! -z "$TOKEN" 34 | bash <(curl -s https://codecov.io/bash) -t "$TOKEN" -B "${{ github.ref }}" 35 | env: 36 | TOKEN: "${{ secrets.CODECOV_TOKEN }}" 37 | -------------------------------------------------------------------------------- /src/Exception/NpmCommandFailedException.php: -------------------------------------------------------------------------------- 1 | command = $command; 21 | 22 | parent::__construct( 23 | sprintf('Execution of %s failed.', var_export($command, true)), 24 | 0, 25 | $cause 26 | ); 27 | } 28 | 29 | /** 30 | * Get the executed command. 31 | * 32 | * @return string The command. 33 | */ 34 | public function command(): string 35 | { 36 | return $this->command; 37 | } 38 | 39 | private $command; 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2020 Erin Millard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/NpmVendorFinder.php: -------------------------------------------------------------------------------- 1 | The list of NPM bridge enabled vendor packages. 20 | */ 21 | public function find(Composer $composer, NpmBridge $bridge): array 22 | { 23 | $packages = $composer->getRepositoryManager()->getLocalRepository() 24 | ->getPackages(); 25 | 26 | $dependantPackages = []; 27 | 28 | foreach ($packages as $package) { 29 | if ($bridge->isDependantPackage($package, false)) { 30 | $dependantPackages[$package->getName()] = $package; 31 | } 32 | } 33 | 34 | return array_values($dependantPackages); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eloquent/composer-npm-bridge", 3 | "description": "NPM integration for Composer packages.", 4 | "keywords": ["composer", "npm", "package", "integration", "bridge", "plugin", "composer-plugin"], 5 | "homepage": "https://github.com/eloquent/composer-npm-bridge", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Erin Millard", 10 | "email": "ezzatron@gmail.com", 11 | "homepage": "http://ezzatron.com/" 12 | } 13 | ], 14 | "type": "composer-plugin", 15 | "require": { 16 | "php": ">=7.2", 17 | "composer-plugin-api": "^2" 18 | }, 19 | "require-dev": { 20 | "composer/composer": "^2", 21 | "eloquent/code-style": "^1", 22 | "eloquent/phony-phpunit": "^6", 23 | "friendsofphp/php-cs-fixer": "^2", 24 | "errors/exceptions": "^0.2", 25 | "phpunit/phpunit": "^8" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Eloquent\\Composer\\NpmBridge\\": "src" 30 | } 31 | }, 32 | "extra": { 33 | "class": "Eloquent\\Composer\\NpmBridge\\NpmBridgePlugin" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/NpmBridgeFactory.php: -------------------------------------------------------------------------------- 1 | vendorFinder = $vendorFinder; 38 | $this->client = $client; 39 | } 40 | 41 | /** 42 | * Construct a new Composer NPM bridge plugin. 43 | * 44 | * @param IOInterface $io The i/o interface to use. 45 | */ 46 | public function createBridge(IOInterface $io): NpmBridge 47 | { 48 | return new NpmBridge($io, $this->vendorFinder, $this->client); 49 | } 50 | 51 | private $vendorFinder; 52 | private $client; 53 | } 54 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Composer NPM bridge changelog 2 | 3 | ## 5.0.1 (2022-06-12) 4 | 5 | - **[FIXED]** Fixed NPM warnings about using `--omit=dev` ([#25] - thanks 6 | [@NiklasBr]). 7 | 8 | [#25]: https://github.com/eloquent/composer-npm-bridge/pull/25 9 | [@niklasbr]: https://github.com/NiklasBr 10 | 11 | ## 5.0.0 (2020-11-23) 12 | 13 | - **[BC BREAK]** Dropped support for PHP <7.2. 14 | - **[BC BREAK]** Dropped support for Composer <2. 15 | - **[IMPROVED]** Added support for Composer 2 ([#22], [#24] - thanks 16 | [@bryonbean]). 17 | 18 | [#22]: https://github.com/eloquent/composer-npm-bridge/issues/22 19 | [#24]: https://github.com/eloquent/composer-npm-bridge/pull/24 20 | [@bryonbean]: https://github.com/bryonbean 21 | 22 | ## 4.1.0 (2018-12-06) 23 | 24 | - **[IMPROVED]** The NPM bridge can now be completely disabled by setting the 25 | `COMPOSER_NPM_BRIDGE_DISABLE` environment variable to a non-empty value 26 | ([#18], [#19] - thanks [@driskell]). 27 | - **[IMPROVED]** Custom timeouts can now be set on a per-package basis by 28 | setting the `extra.npm-bridge.timeout` option in `composer.json` 29 | ([#13], [#19] - thanks [@driskell]). 30 | - **[IMPROVED]** Packages can now choose whether to allow the `npm` executable 31 | to be absent at install time by setting the `extra.npm-bridge.optional` 32 | option in `composer.json` ([#19] - thanks [@driskell]). 33 | 34 | [#13]: https://github.com/eloquent/composer-npm-bridge/issues/13 35 | [#18]: https://github.com/eloquent/composer-npm-bridge/issues/18 36 | [#19]: https://github.com/eloquent/composer-npm-bridge/pull/19 37 | [@driskell]: https://github.com/driskell 38 | 39 | ## 4.0.1 (2017-09-19) 40 | 41 | - **[FIXED]** Fixed "class not found" errors when the plugin is removed by 42 | Composer ([#5], [#17] - thanks [@garex]). 43 | 44 | [#5]: https://github.com/eloquent/composer-npm-bridge/issues/5 45 | [#17]: https://github.com/eloquent/composer-npm-bridge/pull/17 46 | [@garex]: https://github.com/garex 47 | 48 | ## 4.0.0 (2017-07-12) 49 | 50 | - **[BC BREAK]** With the introduction of NPM's [`package-lock.json`], _Composer 51 | NPM bridge_ no longer manages shrinkwrap files. 52 | - **[BC BREAK]** Dropped support for PHP 5. 53 | 54 | [`package-lock.json`]: https://docs.npmjs.com/files/package-lock.json 55 | 56 | ## 3.0.1 (2016-02-22) 57 | 58 | - **[FIXED]** Fixed bug where Isolator was unable to be autoloaded ([#11]). 59 | 60 | [#11]: https://github.com/eloquent/composer-npm-bridge/issues/11 61 | 62 | ## 3.0.0 (2016-02-12) 63 | 64 | - **[BC BREAK]** Stripped down implementation, many public methods removed. 65 | - **[FIXED]** Updated Composer API version constraint ([#10]). 66 | 67 | [#10]: https://github.com/eloquent/composer-npm-bridge/issues/10 68 | 69 | ## 2.1.1 (2014-09-08) 70 | 71 | - **[IMPROVED]** Support for custom installation paths as determined by Composer 72 | plugins. 73 | 74 | ## 2.1.0 (2014-02-12) 75 | 76 | - **[IMPROVED]** Composer dev/production modes are now honored when installing 77 | NPM dependencies. 78 | - **[IMPROVED]** Can now be utilized as a dev-only dependency of the root 79 | package. 80 | 81 | ## 2.0.0 (2014-01-29) 82 | 83 | - **[BC BREAK]** Completely re-written as a Composer plugin. 84 | - **[IMPROVED]** Functions without custom Composer script configuration. 85 | 86 | ## 1.0.1 (2013-03-04) 87 | 88 | - **[NEW]** [Archer] integration. 89 | - **[NEW]** Implemented changelog. 90 | 91 | [archer]: https://github.com/IcecaveStudios/archer 92 | -------------------------------------------------------------------------------- /src/NpmBridgePlugin.php: -------------------------------------------------------------------------------- 1 | bridgeFactory = $bridgeFactory; 30 | } 31 | 32 | /** 33 | * Activate the plugin. 34 | * 35 | * @param Composer $composer The main Composer object. 36 | * @param IOInterface $io The i/o interface to use. 37 | */ 38 | public function activate(Composer $composer, IOInterface $io) 39 | { 40 | // preload classes to prevent errors when removing the plugin 41 | class_exists(NpmBridge::class); 42 | class_exists(NpmBridgeFactory::class); 43 | class_exists(NpmClient::class); 44 | class_exists(NpmBridge::class); 45 | class_exists(NpmVendorFinder::class); 46 | } 47 | 48 | /** 49 | * Get the event subscriber configuration for this plugin. 50 | * 51 | * @return array The events to listen to, and their associated handlers. 52 | */ 53 | public static function getSubscribedEvents(): array 54 | { 55 | // Issue #18 - disable if ENV set 56 | if (!empty(getenv('COMPOSER_NPM_BRIDGE_DISABLE'))) { 57 | return []; 58 | } 59 | 60 | // Increased priority to ensure we run before custom installers which are usually default priority 61 | return [ 62 | ScriptEvents::POST_INSTALL_CMD => ['onPostInstallCmd', 1], 63 | ScriptEvents::POST_UPDATE_CMD => ['onPostUpdateCmd', 1], 64 | ]; 65 | } 66 | 67 | /** 68 | * Handle post install command events. 69 | * 70 | * @param Event $event The event to handle. 71 | * 72 | * @throws NpmNotFoundException If the npm executable cannot be located. 73 | * @throws NpmCommandFailedException If the operation fails. 74 | */ 75 | public function onPostInstallCmd(Event $event) 76 | { 77 | $this->bridgeFactory->createBridge($event->getIO()) 78 | ->install($event->getComposer(), $event->isDevMode()); 79 | } 80 | 81 | /** 82 | * Handle post update command events. 83 | * 84 | * @param Event $event The event to handle. 85 | * 86 | * @throws NpmNotFoundException If the npm executable cannot be located. 87 | * @throws NpmCommandFailedException If the operation fails. 88 | */ 89 | public function onPostUpdateCmd(Event $event) 90 | { 91 | $this->bridgeFactory->createBridge($event->getIO()) 92 | ->update($event->getComposer()); 93 | } 94 | 95 | private $bridgeFactory; 96 | 97 | /** 98 | * Remove any hooks from Composer 99 | * 100 | * This will be called when a plugin is deactivated before being 101 | * uninstalled, but also before it gets upgraded to a new version 102 | * so the old one can be deactivated and the new one activated. 103 | * 104 | * @param Composer $composer 105 | * @param IOInterface $io 106 | */ 107 | public function deactivate(Composer $composer, IOInterface $io) 108 | { 109 | } 110 | 111 | /** 112 | * Prepare the plugin to be uninstalled 113 | * 114 | * This will be called after deactivate. 115 | * 116 | * @param Composer $composer 117 | * @param IOInterface $io 118 | */ 119 | public function uninstall(Composer $composer, IOInterface $io) 120 | { 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > # No longer maintained 2 | > 3 | > This package is no longer maintained. See [this statement] for more info. 4 | > 5 | > [this statement]: https://gist.github.com/ezzatron/713a548735febe3d76f8ca831bc895c0 6 | 7 | # Composer NPM bridge 8 | 9 | *NPM integration for Composer packages.* 10 | 11 | [![Current version image][version-image]][current version] 12 | [![Current build status image][build-image]][current build status] 13 | [![Current coverage status image][coverage-image]][current coverage status] 14 | 15 | [build-image]: http://img.shields.io/travis/eloquent/composer-npm-bridge/develop.svg?style=flat-square "Current build status for the develop branch" 16 | [coverage-image]: https://img.shields.io/codecov/c/github/eloquent/composer-npm-bridge/develop.svg?style=flat-square "Current test coverage for the develop branch" 17 | [current build status]: https://travis-ci.org/eloquent/composer-npm-bridge 18 | [current coverage status]: https://codecov.io/github/eloquent/composer-npm-bridge 19 | [current version]: https://packagist.org/packages/eloquent/composer-npm-bridge 20 | [version-image]: https://img.shields.io/packagist/v/eloquent/composer-npm-bridge.svg?style=flat-square "This project uses semantic versioning" 21 | 22 | ## Installation 23 | 24 | - Available as [Composer] package [eloquent/composer-npm-bridge]. 25 | 26 | [composer]: http://getcomposer.org/ 27 | [eloquent/composer-npm-bridge]: https://packagist.org/packages/eloquent/composer-npm-bridge 28 | 29 | ## Requirements 30 | 31 | - The `npm` executable must be available in PATH. 32 | 33 | ## Usage 34 | 35 | To utilize the *Composer NPM bridge*, simply add `eloquent/composer-npm-bridge` 36 | to the `require` section of the project's Composer configuration: 37 | 38 | composer require eloquent/composer-npm-bridge 39 | 40 | NPM dependencies are specified via a [package.json] configuration file in the 41 | root directory of the Composer package. Source control should be configured to 42 | ignore NPM's `node_modules` directory, similar to Composer's `vendor` directory. 43 | 44 | [package.json]: https://npmjs.org/doc/json.html 45 | 46 | ## How does it work? 47 | 48 | The *Composer NPM bridge* is a Composer plugin that automatically installs and 49 | updates [NPM] packages whenever the corresponding Composer command is executed. 50 | To detect compatible packages, the bridge inspects Composer package 51 | configuration information to find packages that directly require the 52 | `eloquent/composer-npm-bridge` Composer package itself. 53 | 54 | In addition to normal operation, `composer install` will [install] NPM 55 | dependencies for all Composer packages using the bridge. This includes the root 56 | package, as well as Composer dependencies. Similarly, `composer update` will 57 | [install] NPM dependencies for all Composer dependencies using the bridge. It 58 | will also [update] the NPM dependencies for the root project. 59 | 60 | NPM dependencies will be installed exactly as if `npm install` were run from the 61 | root directory of the package. This applies even if the package is installed as 62 | a dependency. 63 | 64 | [install]: https://npmjs.org/doc/install.html 65 | [npm]: https://npmjs.org/ 66 | [update]: https://npmjs.org/doc/update.html 67 | 68 | ## Configuration 69 | 70 | The following configuration can be added to `composer.json` under the 71 | `extra.npm-bridge` section to customize the behavior on a per-package basis. 72 | Values in the root package will not currently impact any dependency packages 73 | that also use *Composer NPM bridge* - each package must define its own options. 74 | 75 | Key | Type | Default | Description 76 | ---------|------|---------|--------------------------------------------------- 77 | timeout | int | `300` | Specify a custom timeout for the installation (in seconds). 78 | optional | bool | `false` | Skip instead of throwing an exception if `npm` is not found when processing the package. 79 | 80 | ```json5 81 | { 82 | // ... 83 | 84 | "extra": { 85 | "npm-bridge": { 86 | "timeout": 9000, 87 | "optional": true 88 | }, 89 | 90 | // ... 91 | } 92 | } 93 | ``` 94 | 95 | *Composer NPM bridge* can be completely disabled by setting the 96 | `COMPOSER_NPM_BRIDGE_DISABLE` environment variable to a non-empty value: 97 | 98 | ```shell 99 | COMPOSER_NPM_BRIDGE_DISABLE=1 composer install 100 | ``` 101 | 102 | ## Caveats 103 | 104 | Because NPM dependencies are installed underneath the root directory of the 105 | Composer package, Composer may complain about working copy changes when the 106 | package is installed as a dependency. Source control should be configured to 107 | ignore the `node_modules` directory in order to avoid this. 108 | -------------------------------------------------------------------------------- /src/NpmClient.php: -------------------------------------------------------------------------------- 1 | processExecutor = $processExecutor; 46 | $this->executableFinder = $executableFinder; 47 | $this->getcwd = $getcwd; 48 | $this->chdir = $chdir; 49 | 50 | $this->isNpmPathChecked = false; 51 | $this->getTimeout = [$processExecutorClass, 'getTimeout']; 52 | $this->setTimeout = [$processExecutorClass, 'setTimeout']; 53 | } 54 | 55 | /** 56 | * Install NPM dependencies for the project at the supplied path. 57 | * 58 | * @param string|null $path The path to the NPM project, or null to use the current working directory. 59 | * @param bool $isDevMode True if dev dependencies should be included. 60 | * @param int|null $timeout The process timeout, in seconds. 61 | * 62 | * @throws NpmNotFoundException If the npm executable cannot be located. 63 | * @throws NpmCommandFailedException If the operation fails. 64 | */ 65 | public function install(string $path = null, bool $isDevMode = true, int $timeout = null) 66 | { 67 | if ($isDevMode) { 68 | $arguments = ['install']; 69 | } else { 70 | $arguments = ['install', '--omit=dev']; 71 | } 72 | 73 | if ($timeout === null) { 74 | $timeout = self::DEFAULT_TIMEOUT; 75 | } 76 | 77 | $this->executeNpm($arguments, $path, $timeout); 78 | } 79 | 80 | /** 81 | * Update NPM dependencies for the project at the supplied path. 82 | * 83 | * @param string|null $path The path to the NPM project, or null to use the current working directory. 84 | * @param int $timeout The process timeout, in seconds. 85 | * 86 | * @throws NpmNotFoundException If the npm executable cannot be located. 87 | * @throws NpmCommandFailedException If the operation fails. 88 | */ 89 | public function update(string $path = null, int $timeout = null) 90 | { 91 | if ($timeout === null) { 92 | $timeout = self::DEFAULT_TIMEOUT; 93 | } 94 | 95 | $this->executeNpm(['update'], $path, $timeout); 96 | } 97 | 98 | /** 99 | * Check if the npm executable is available. 100 | * 101 | * @return bool True if available. 102 | */ 103 | public function isAvailable() 104 | { 105 | return null !== $this->npmPath(); 106 | } 107 | 108 | private function executeNpm($arguments, $workingDirectoryPath, $timeout) 109 | { 110 | $npmPath = $this->npmPath(); 111 | 112 | if (null === $npmPath) { 113 | throw new NpmNotFoundException(); 114 | } 115 | 116 | array_unshift($arguments, $npmPath); 117 | $command = implode(' ', array_map('escapeshellarg', $arguments)); 118 | 119 | if (null !== $workingDirectoryPath) { 120 | $previousWorkingDirectoryPath = call_user_func($this->getcwd); 121 | call_user_func($this->chdir, $workingDirectoryPath); 122 | } 123 | 124 | $oldTimeout = call_user_func($this->getTimeout); 125 | call_user_func($this->setTimeout, $timeout); 126 | 127 | $exitCode = $this->processExecutor->execute($command); 128 | 129 | call_user_func($this->setTimeout, $oldTimeout); 130 | 131 | if (null !== $workingDirectoryPath) { 132 | call_user_func($this->chdir, $previousWorkingDirectoryPath); 133 | } 134 | 135 | if (0 !== $exitCode) { 136 | throw new NpmCommandFailedException($command); 137 | } 138 | } 139 | 140 | private function npmPath() 141 | { 142 | if (!$this->npmPathChecked) { 143 | $this->npmPath = $this->executableFinder->find('npm'); 144 | $this->npmPathChecked = true; 145 | } 146 | 147 | return $this->npmPath; 148 | } 149 | 150 | private $processExecutor; 151 | private $executableFinder; 152 | private $getcwd; 153 | private $chdir; 154 | private $npmPath; 155 | private $npmPathChecked; 156 | private $getTimeout; 157 | private $setTimeout; 158 | } 159 | -------------------------------------------------------------------------------- /src/NpmBridge.php: -------------------------------------------------------------------------------- 1 | io = $io; 35 | $this->vendorFinder = $vendorFinder; 36 | $this->client = $client; 37 | } 38 | 39 | /** 40 | * Install NPM dependencies for a Composer project and its dependencies. 41 | * 42 | * @param Composer $composer The main Composer object. 43 | * @param bool $isDevMode True if dev mode is enabled. 44 | * 45 | * @throws NpmNotFoundException If the npm executable cannot be located. 46 | * @throws NpmCommandFailedException If the operation fails. 47 | */ 48 | public function install(Composer $composer, bool $isDevMode = true) 49 | { 50 | $this->io->write( 51 | 'Installing NPM dependencies for root project' 52 | ); 53 | 54 | $package = $composer->getPackage(); 55 | 56 | if ($this->isDependantPackage($package, $isDevMode)) { 57 | $isNpmAvailable = $this->client->isAvailable(); 58 | $extra = $package->getExtra(); 59 | 60 | if (!$isNpmAvailable && $this->isPackageOptional($extra)) { 61 | $this->io->write('Skipping as NPM is unavailable'); 62 | } else { 63 | $this->client 64 | ->install(null, $isDevMode, $this->packageTimeout($extra)); 65 | } 66 | } else { 67 | $this->io->write('Nothing to install'); 68 | } 69 | 70 | $this->installForVendors($composer); 71 | } 72 | 73 | /** 74 | * Update NPM dependencies for a Composer project and its dependencies. 75 | * 76 | * @param Composer $composer The main Composer object. 77 | * 78 | * @throws NpmNotFoundException If the npm executable cannot be located. 79 | * @throws NpmCommandFailedException If the operation fails. 80 | */ 81 | public function update(Composer $composer) 82 | { 83 | $this->io->write( 84 | 'Updating NPM dependencies for root project' 85 | ); 86 | 87 | $package = $composer->getPackage(); 88 | 89 | if ($this->isDependantPackage($package, true)) { 90 | $timeout = $this->packageTimeout($package->getExtra()); 91 | 92 | $this->client->update(null, $timeout); 93 | $this->client->install(null, true, $timeout); 94 | } else { 95 | $this->io->write('Nothing to update'); 96 | } 97 | 98 | $this->installForVendors($composer); 99 | } 100 | 101 | /** 102 | * Returns true if the supplied package requires the Composer NPM bridge. 103 | * 104 | * @param PackageInterface $package The package to inspect. 105 | * @param bool $includeDevDependencies True if the dev dependencies should also be inspected. 106 | * 107 | * @return bool True if the package requires the bridge. 108 | */ 109 | public function isDependantPackage( 110 | PackageInterface $package, 111 | bool $includeDevDependencies = false 112 | ): bool { 113 | foreach ($package->getRequires() as $link) { 114 | if ('eloquent/composer-npm-bridge' === $link->getTarget()) { 115 | return true; 116 | } 117 | } 118 | 119 | if ($includeDevDependencies) { 120 | foreach ($package->getDevRequires() as $link) { 121 | if ('eloquent/composer-npm-bridge' === $link->getTarget()) { 122 | return true; 123 | } 124 | } 125 | } 126 | 127 | return false; 128 | } 129 | 130 | private function installForVendors($composer) 131 | { 132 | $this->io->write( 133 | 'Installing NPM dependencies for Composer dependencies' 134 | ); 135 | 136 | $packages = $this->vendorFinder->find($composer, $this); 137 | 138 | if (count($packages) > 0) { 139 | $isNpmAvailable = $this->client->isAvailable(); 140 | $installationManager = $composer->getInstallationManager(); 141 | 142 | foreach ($packages as $package) { 143 | $extra = $package->getExtra(); 144 | 145 | if (!$isNpmAvailable && $this->isPackageOptional($extra)) { 146 | $this->io->write( 147 | sprintf( 148 | 'Skipping optional NPM dependencies for %s as NPM' . 149 | ' is unavailable', 150 | $package->getPrettyName() 151 | ) 152 | ); 153 | 154 | continue; 155 | } 156 | 157 | $this->io->write( 158 | sprintf( 159 | 'Installing NPM dependencies for %s', 160 | $package->getPrettyName() 161 | ) 162 | ); 163 | 164 | $this->client->install( 165 | $installationManager->getInstallPath($package), 166 | false, 167 | $this->packageTimeout($extra) 168 | ); 169 | } 170 | } else { 171 | $this->io->write('Nothing to install'); 172 | } 173 | } 174 | 175 | private function packageTimeout(array $extra) { 176 | if (isset($extra[self::EXTRA_KEY][self::EXTRA_KEY_TIMEOUT])) { 177 | return intval($extra[self::EXTRA_KEY][self::EXTRA_KEY_TIMEOUT]); 178 | } 179 | 180 | return null; 181 | } 182 | 183 | private function isPackageOptional(array $extra) 184 | { 185 | return !empty($extra[self::EXTRA_KEY][self::EXTRA_KEY_OPTIONAL]); 186 | } 187 | 188 | private $io; 189 | private $vendorFinder; 190 | private $client; 191 | } 192 | --------------------------------------------------------------------------------