├── .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 |
--------------------------------------------------------------------------------