├── .editorconfig ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── ecs.php ├── resources ├── doc │ ├── config.md │ ├── events.md │ ├── faqs.md │ ├── index.md │ └── usage.md └── foxy.svg └── src ├── Asset ├── AbstractAssetManager.php ├── AssetManagerFinder.php ├── AssetManagerInterface.php ├── AssetPackage.php ├── AssetPackageInterface.php ├── BunManager.php ├── NpmManager.php ├── PnpmManager.php └── YarnManager.php ├── Config ├── Config.php └── ConfigBuilder.php ├── Converter ├── SemverConverter.php ├── SemverUtil.php └── VersionConverterInterface.php ├── Event ├── AbstractSolveEvent.php ├── GetAssetsEvent.php ├── PostSolveEvent.php └── PreSolveEvent.php ├── Exception ├── ExceptionInterface.php └── RuntimeException.php ├── Fallback ├── AssetFallback.php ├── ComposerFallback.php └── FallbackInterface.php ├── Foxy.php ├── FoxyEvents.php ├── Json ├── JsonFile.php └── JsonFormatter.php ├── Solver ├── Solver.php └── SolverInterface.php └── Util ├── AssetUtil.php ├── ComposerUtil.php ├── ConsoleUtil.php ├── LockerUtil.php └── PackageUtil.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | 13 | [*.php] 14 | ij_php_space_before_short_closure_left_parenthesis = false 15 | ij_php_space_after_type_cast = true 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | 20 | [*.yml] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | ## 0.1.3 Under development 5 | 6 | - Enh #87: Refactor `SemverUtil::class` stability normalization logic (@terabytesoftw) 7 | - Bug #104: Add support for PHP 8.4 (@terabytesoftw) 8 | - Enh #105: Update dependencies in `composer.lock` (@terabytesoftw) 9 | - Enh #106: Update composer dependencies for compatibility with newer versions (@terabytesoftw) 10 | 11 | ## 0.1.2 June 10, 2024 12 | 13 | - Bug #64: Update docs, `composer.lock` and change directory in `Solver.php` (@terabytesoftw) 14 | - Enh #63: Add the ability to specify a custom directory for `package.json` (@terabytesoftw) 15 | - Bug #69: Add `funding.yml` file (@terabytesoftw) 16 | 17 | ## 0.1.1 April 4, 2024 18 | 19 | - Enh #50: Add `BunManager` class to manage the `Bun` instances (@terabytesoftw) 20 | - Enh #52: Add file lock `yarn.lock` for `Bun` and update `README.md` (@terabytesoftw) 21 | 22 | ## 0.1.0 January 21, 2024 23 | 24 | - Initial release. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 by Wilmer Arámbula (https://github.com/terabytesoftw) All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Foxy 3 |

4 | 5 |

6 | 7 | php-version 8 | 9 | 10 | PHPUnit 11 | 12 | 13 | Codecov 14 | 15 | 16 | PSalm 17 | 18 | 19 | StyleCI 20 | 21 |

22 | 23 | Add to your `composer.json` file. 24 | 25 | Manager can be `bun`, `npm`, `yarn` or `pnpm`. For default, `npm` is used. 26 | 27 | ```json 28 | { 29 | "require": { 30 | "php-forge/foxy": "^0.1" 31 | }, 32 | "config": { 33 | "foxy": { 34 | "manager": "bun" 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | > **Important:** 41 | > 42 | > ⚠ This plugin is based on [Fxpio/Foxy](https://github.com/fxpio/foxy). 43 | > 44 | > Updates: 45 | > - PHP version to `8.1` or higher. 46 | > - Composer version to `2.0` or higher. 47 | > - Composer api version to `2.0` or higher. 48 | > - Add support for [Bun](https://bun.sh/). 49 | > - Remove deprecated methods. 50 | > - Add static analysis with [Psalm](https://psalm.dev). 51 | > - Add code quality with [StyleCI](https://github.styleci.io). 52 | 53 | 54 | Foxy is a Composer plugin to automate the validation, installation, updating and removing of PHP libraries 55 | asset dependencies (javaScript, stylesheets, etc.) defined in the NPM `package.json` file of the project and 56 | PHP libraries during the execution of Composer. It handles restoring the project state in case 57 | [Bun](https://bun.sh/) or [NPM](https://www.npmjs.com) or [Yarn](https://yarnpkg.com) or [PNpM](https://PNpM.io) terminates with an error. 58 | 59 | All features and tools are available: 60 | 61 | - [Babel](https://babeljs.io) 62 | - [Bun](https://github.com/oven-sh/bun) 63 | - [Grunt](https://gruntjs.com) 64 | - [Gulp](https://gulpjs.com) 65 | - [Npmrc](https://docs.npmjs.com/files/npmrc) 66 | - [Less](http://lesscss.org) 67 | - [Scss/Sass](http://sass-lang.com) 68 | - [TypeScript](https://www.typescriptlang.org) 69 | - [Yarnrc](https://yarnpkg.com/en/docs/yarnrc) 70 | - [Webpack](https://webpack.js.org), , 71 | 72 | It is certain that each language has its own dependency management system, and that it is highly recommended to use each 73 | package manager. NPM, Yarn or PNpM works very well when the asset dependencies are managed only in the PHP project, but 74 | when you create PHP libraries that using assets, there is no way to automatically add asset dependencies, and most 75 | importantly, no validation of versions can be done automatically. You must tell the developers the list of asset 76 | dependencies that using by your PHP library, and you must ask him to add manually the asset dependencies to its asset 77 | manager of his project. 78 | 79 | However, another solution exist - what many projects propose - you must add the assets in the folder of the PHP library 80 | (like `/assets`, `/Resources/public`). Of course, with this method, the code is duplicated, it pollutes the source code 81 | of the PHP library, no version management/validation is possible, and it is even less possible, to use all tools such as 82 | Babel, Scss, Less, etc ... 83 | 84 | Foxy focuses solely on automation of the validation, addition, updating and deleting of the dependencies in the 85 | definition file of the asset package, while restoring the project state, as well as PHP dependencies if Bun, NPM, Yarn 86 | or PNpM terminates with an error. 87 | 88 | #### It is Fast 89 | 90 | Foxy retrieves the list of all Composer dependencies to inject the asset dependencies in the file `package.json`, and 91 | leaves the execution of the analysis, validation and downloading of the libraries to Bun, NPM, Yarn or PNpM. 92 | 93 | Therefore, no VCS Repository of Composer is used for analyzing the asset dependencies, and you keep the performance 94 | of native package manager used. 95 | 96 | #### It is Reliable 97 | 98 | Foxy creates mock packages of the PHP libraries containing only the asset dependencies definition file in a local 99 | directory, and associates these packages in the asset dependencies definition file of the project. Given that Foxy does 100 | not manipulate any asset dependencies, and let alone the version constraints, this allows Bun, NPM, Yarn or PNpM to 101 | solve the asset dependencies without any intermediary. Moreover, the entire validation with the lock file and 102 | installation process is left to Bun, NPM, Yarn or PNpM. 103 | 104 | #### It is Secure 105 | 106 | Foxy restores the Composer lock file with all its PHP dependencies, as well as the asset dependencies definition file, 107 | in the previous state if Bun, NPM, Yarn or PNpM ends with an error. 108 | 109 | Features 110 | -------- 111 | 112 | - Compatible with [Yii Assets](https://github.com/yiisoft/assets) 113 | - Compatible with [Symfony Webpack Encore](http://symfony.com/doc/current/frontend.html) 114 | and [Laravel Mix](https://laravel.com/docs/master/mix) 115 | - Works with Node.js and Bun, NPM, Yarn or PNpM 116 | - Works with the asset dependencies defined in the `package.json` file for projects and PHP libraries 117 | - Works with the installation in the dependencies of the project or libraries (not in global mode) 118 | - Works with public or private repositories 119 | - Works with all features of Composer, NPM, Yarn and PNpM 120 | - Retains the native performance of Composer, NPM, Yarn and PNpM 121 | - Restores previous versions of PHP dependencies and the lock file if NPM, Yarn or PNpM terminates with an error 122 | - Validates the NPM, Yarn or PNpM version with a version range 123 | - Configuration of the plugin per project, globally or with the environment variables: 124 | - Enable/disable the plugin 125 | - Choose the asset manager: NPM, Yarn or PNpM (`npm` is used by default) 126 | - Lock the version of the asset manager with the Composer version range 127 | - Define the custom path of binary of the asset manager 128 | - Enable/disable the fallback for the asset package file of the project 129 | - Enable/disable the fallback for the Composer lock file and its dependencies 130 | - Enable/disable the running of asset manager to keep only the manipulation of the asset package file 131 | - Override the install command options for the asset manager 132 | - Override the update command options for the asset manager 133 | - Define the custom path of the mock package of PHP library 134 | - Enable/disable manually the asset packages for the PHP libraries 135 | - Works with the Composer commands: 136 | - `install` 137 | - `update` 138 | - `require` 139 | - `remove` 140 | 141 | Documentation 142 | ------------- 143 | 144 | - [Guide](resources/doc/index.md) 145 | - [FAQs](resources/doc/faqs.md) 146 | - [Release Notes](https://github.com/php-forge/foxy/releases) 147 | 148 | Installation 149 | ------------ 150 | 151 | Installation instructions are located in [the guide](resources/doc/index.md). 152 | 153 | License 154 | ------- 155 | 156 | Foxy is released under the MIT license. See the complete license in: 157 | 158 | [LICENSE](LICENSE) 159 | 160 | Reporting an issue or a feature request 161 | --------------------------------------- 162 | 163 | Issues and feature requests are tracked in the [Github issue tracker](https://github.com/php-forge/foxy/issues). 164 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-forge/foxy", 3 | "description": "Fast, reliable, and secure Bun/NPM/Yarn/pnpm bridge for Composer", 4 | "keywords": ["bun", "npm", "yarn", "composer", "bridge", "dependency manager", "package", "asset", "nodejs"], 5 | "homepage": "https://github.com/fxpio/foxy", 6 | "type": "composer-plugin", 7 | "license": "MIT", 8 | "funding": [ 9 | { 10 | "type": "github", 11 | "url": "https://github.com/sponsors/terabytesoftw" 12 | } 13 | ], 14 | "require": { 15 | "ext-ctype": "*", 16 | "ext-mbstring": "*", 17 | "php": "^8.1", 18 | "composer/composer": "^2.8", 19 | "composer-plugin-api": "^2.0", 20 | "composer/semver": "^3.4", 21 | "symfony/console": "^6.0|^7.0" 22 | }, 23 | "require-dev": { 24 | "maglnet/composer-require-checker": "^4.7", 25 | "php-forge/support": "^0.1", 26 | "phpunit/phpunit": "^10.5", 27 | "symplify/easy-coding-standard": "^12.5", 28 | "vimeo/psalm": "^5.26.1|^6.4.1" 29 | }, 30 | "config": { 31 | "preferred-install": { 32 | "*": "dist" 33 | }, 34 | "sort-packages": true 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Foxy\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Foxy\\Tests\\": "tests" 44 | } 45 | }, 46 | "extra": { 47 | "class": "Foxy\\Foxy", 48 | "branch-alias": { 49 | "dev-main": "1.0-dev" 50 | } 51 | }, 52 | "scripts": { 53 | "check-dependencies": "composer-require-checker", 54 | "easy-coding-standard": "ecs check", 55 | "psalm": "psalm", 56 | "test": "phpunit" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths( 14 | [ 15 | __DIR__ . '/src', 16 | __DIR__ . '/tests', 17 | ] 18 | ); 19 | 20 | // this way you add a single rule 21 | $ecsConfig->rules( 22 | [ 23 | OrderedClassElementsFixer::class, 24 | OrderedTraitsFixer::class, 25 | NoUnusedImportsFixer::class, 26 | ] 27 | ); 28 | 29 | // this way you can add sets - group of rules 30 | $ecsConfig->sets( 31 | [ 32 | // run and fix, one by one 33 | SetList::DOCBLOCK, 34 | SetList::NAMESPACES, 35 | SetList::COMMENTS, 36 | SetList::PSR_12, 37 | ] 38 | ); 39 | 40 | // this way configures a rule 41 | $ecsConfig->ruleWithConfiguration( 42 | ClassDefinitionFixer::class, 43 | [ 44 | 'space_before_parenthesis' => true, 45 | ], 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /resources/doc/config.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | ## Manipulate the configuration 5 | 6 | ### Define the config for one project 7 | 8 | All options can be added in the `composer.json` file of the project in the `config.foxy.*` section. 9 | 10 | **Example:** 11 | 12 | ```json 13 | { 14 | "name": "root/package", 15 | "config": { 16 | "foxy": { 17 | "enabled": false 18 | } 19 | } 20 | } 21 | ``` 22 | 23 | ### Define the config for all projects 24 | 25 | You can define the options in the `composer.json` file of each project, but you can also set an option 26 | for all projects. 27 | 28 | To do this, you simply need to add your options in the Composer global configuration, 29 | in the file of your choice: 30 | 31 | - `/composer.json` file 32 | - `/config.json` file 33 | 34 | > **Note:** 35 | > The `composer global config` command cannot be used, bacause Composer does not accept custom options. 36 | > But you can use the command `composer global config -e` to edit the global `composer.json` file with 37 | > your text editor. 38 | 39 | ### Define the config in a environment variable 40 | 41 | You can define each option (`config.foxy.*`) directly in the PHP environment variables. For 42 | this, all variables will start with `FOXY__` and uppercased, and each `-` will replaced by `_`. 43 | 44 | The accepted value types are: 45 | 46 | - string 47 | - boolean 48 | - integer 49 | - JSON array or object 50 | 51 | **Example:** 52 | ```json 53 | { 54 | "config": { 55 | "foxy": { 56 | "enabled": false 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | Can be overridden by `FOXY__ENABLED="false"` environment variable. 63 | 64 | **Example:** 65 | ```json 66 | { 67 | "config": { 68 | "foxy": { 69 | "enable-packages": { 70 | "foo/*": true 71 | } 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | Can be overridden by `FOXY__ENABLE_PACKAGES="{"foo/*": true}"` environment variable. 78 | 79 | ### Config priority order 80 | 81 | The config values are retrieved in priority in: 82 | 83 | 1. the environment variables starting with `FOXY__` 84 | 2. the project `composer.json` file 85 | 3. the global `/config.json` file 86 | 4. the global `/composer.json` file 87 | 88 | ### Define the config for multiple manager 89 | 90 | All keys starting with the prefix `manager-` can accept a unique value, but it will also can accept 91 | a map containing the value for each manager defined by her name. 92 | 93 | **Example:** 94 | ```json 95 | { 96 | "config": { 97 | "foxy": { 98 | "manager-version": { 99 | "npm": ">=5.0", 100 | "yarn": ">=1.0.0" 101 | } 102 | } 103 | } 104 | } 105 | ``` 106 | 107 | > **Note:** 108 | > 109 | > This format is available only for the configuration file, and so, not available for the 110 | > environment variables. 111 | 112 | ## Use the config options 113 | 114 | ### Enable/disable the plugin 115 | 116 | You can enable or disable the plugin with the option `config.foxy.enabled` [`boolean`, default: `true`]. 117 | 118 | **Example:** 119 | ```json 120 | { 121 | "config": { 122 | "foxy": { 123 | "enabled": false 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | ### Choose the asset manager 130 | 131 | You can choose the asset manager with the option `config.foxy.manager` [`string`, default: `npm`]. 132 | 133 | **Available values:** 134 | 135 | - `bun` 136 | - `npm` 137 | - `pnpm` 138 | - `yarn` 139 | 140 | 141 | **Example:** 142 | ```json 143 | { 144 | "config": { 145 | "foxy": { 146 | "manager": "yarn" 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | ### Lock the version of the asset manager with the Composer version range 153 | 154 | You can validate the version of the asset manager with the option 155 | `config.foxy.manager-version` [`string`, default: `null`]. 156 | 157 | **Example:** 158 | ```json 159 | { 160 | "config": { 161 | "foxy": { 162 | "manager-version": "^5.3.0" 163 | } 164 | } 165 | } 166 | ``` 167 | 168 | ### Define the custom path of binary of the asset manager 169 | 170 | You can define the custom path of the binary of the asset manager with the option 171 | `config.foxy.manager-bin` [`string`, default: `null`]. 172 | 173 | **Example:** 174 | ```json 175 | { 176 | "config": { 177 | "foxy": { 178 | "manager-bin": "/custom/path/of/asset/manager/binary" 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | ### Override the install and update command options for the asset manager 185 | 186 | You can add custom options for the asset manager binary for the install and update commands with the 187 | option `config.foxy.manager-options` [`string`, default: `null`]. 188 | 189 | **Example:** 190 | ```json 191 | { 192 | "config": { 193 | "foxy": { 194 | "manager": "yarn", 195 | "manager-options": "--production=true --modules-folder=./assets" 196 | } 197 | } 198 | } 199 | ``` 200 | 201 | > **Note:** 202 | > 203 | > It is rather recommended that you use the configuration files `.npmrc` for NPM, and `.yarnrc` for Yarn 204 | 205 | ### Override the install command options for the asset manager 206 | 207 | You can add custom options for the asset manager binary for the install command with the 208 | option `config.foxy.manager-install-options` [`string`, default: `null`]. 209 | 210 | **Example:** 211 | ```json 212 | { 213 | "config": { 214 | "foxy": { 215 | "manager": "npm", 216 | "manager-install-options": "--dry-run" 217 | } 218 | } 219 | } 220 | ``` 221 | 222 | > **Note:** 223 | > 224 | > For this example, the option allow you to keep only the manipulation of the asset package file, 225 | > and validate the dependencies without the installation of the dependencies 226 | 227 | ### Override the update command options for the asset manager 228 | 229 | You can add custom options for the asset manager binary for the update command with the 230 | option `config.foxy.manager-update-options` [`string`, default: `null`]. 231 | 232 | **Example:** 233 | ```json 234 | { 235 | "config": { 236 | "foxy": { 237 | "manager": "yarn", 238 | "manager-update-options": "--flat" 239 | } 240 | } 241 | } 242 | ``` 243 | 244 | ### Define the execution timeout of the asset manager 245 | 246 | You can define the execution timeout of the asset manager with the 247 | option `config.foxy.manager-timeout` [`int`, default: `PHP_INT_MAX`]. 248 | 249 | **Example:** 250 | ```json 251 | { 252 | "config": { 253 | "foxy": { 254 | "manager-timeout": 420 255 | } 256 | } 257 | } 258 | ``` 259 | 260 | ### Enable/disable the fallback for the asset package file of the project 261 | 262 | You can enable or disable the fallback of the asset package file with the option 263 | `config.foxy.fallback-asset` [`boolean`, default: `true`]. 264 | 265 | **Example:** 266 | ```json 267 | { 268 | "config": { 269 | "foxy": { 270 | "fallback-asset": false 271 | } 272 | } 273 | } 274 | ``` 275 | 276 | ### Enable/disable the fallback for the Composer lock file and its dependencies 277 | 278 | You can enable or disable the fallback of the Composer lock file and its dependencies with the option 279 | `config.foxy.fallback-composer` [`boolean`, default: `true`]. 280 | 281 | **Example:** 282 | ```json 283 | { 284 | "config": { 285 | "foxy": { 286 | "fallback-composer": false 287 | } 288 | } 289 | } 290 | ``` 291 | 292 | ### Enable/disable the running of asset manager 293 | 294 | You can enable or disable the running of the asset manager with the option 295 | `config.foxy.run-asset-manager` [`boolean`, default: `true`]. 296 | 297 | **Example:** 298 | ```json 299 | { 300 | "config": { 301 | "foxy": { 302 | "run-asset-manager": false 303 | } 304 | } 305 | } 306 | ``` 307 | 308 | > **Note:** 309 | > 310 | > This option allow you to keep only the manipulation of the asset package file, 311 | > without the execution of the asset manager 312 | 313 | ### Define the custom path of the mock package of PHP library 314 | 315 | You can define the custom path of the mock package of PHP library with the option 316 | `config.foxy.composer-asset-dir` [`string`, default: `null`]. 317 | 318 | **Example:** 319 | ```json 320 | { 321 | "config": { 322 | "foxy": { 323 | "composer-asset-dir": "./my/mock/asset/path/of/project" 324 | } 325 | } 326 | } 327 | ``` 328 | 329 | ### Enable/disable manually the PHP packages with an asset package definition 330 | 331 | By default, Foxy looks in the `composer.json` file of the PHP dependencies, if the mock package needs 332 | to be added into NPM or Yarn. However, some public PHP package already uses the `package.json` file 333 | to handle their asset dependencies, but Foxy is not enabled for this package. In this case, you can 334 | manually enable the PHP packages to be scanned in your project. 335 | 336 | Patterns can be written in Glob style or with regular expressions. In this case, the pattern must 337 | start and end with slash (`/`). 338 | 339 | You can define the patterns to enable or disable the packages with the option 340 | `config.foxy.enable-packages` [`array`, default: `array()`]. 341 | 342 | **Example:** 343 | ```json 344 | { 345 | "config": { 346 | "foxy": { 347 | "enable-packages": { 348 | "/^bar\/*/": true, 349 | "foo/*": true, 350 | "baz/test-*": false 351 | } 352 | } 353 | } 354 | } 355 | ``` 356 | 357 | If you do not deactivate any packages, you can use a simple array. 358 | 359 | **Example:** 360 | ```json 361 | { 362 | "config": { 363 | "foxy": { 364 | "enable-packages": [ 365 | "/^bar\/*/", 366 | "foo/*" 367 | ] 368 | } 369 | } 370 | } 371 | ``` 372 | 373 | ### Specify a custom directory for `package.json` file 374 | 375 | You can define the custom directory for `package.json` file with the option `root-package-json-dir` [`string`, default: `null`]. 376 | 377 | **Example:** 378 | ```json 379 | { 380 | "config": { 381 | "foxy": { 382 | "root-package-json-dir": "module/theme" 383 | } 384 | } 385 | } 386 | ``` 387 | -------------------------------------------------------------------------------- /resources/doc/events.md: -------------------------------------------------------------------------------- 1 | Events 2 | ====== 3 | 4 | Foxy triggers events with the event dispatcher system of Composer allowing you to extend Foxy's 5 | capabilities. To do this, you must create a Composer plugin requiring Foxy in addition to the 6 | requirements required for the creation of a Composer plugin. You can read the documentation of 7 | Composer: [Setting up and using plugin](https://getcomposer.org/doc/articles/plugins.md). 8 | 9 | ## Event names 10 | 11 | All event names are listed in constants of the `Foxy\FoxyEvents` class containing the name of 12 | the event class for each event. 13 | 14 | ### pre-solve 15 | 16 | The `foxy.pre-solve` event occurs before the `solve` action of asset packages and after the 17 | Composer's command events `post-install-cmd` and `post-update-cmd`. 18 | 19 | ### get-assets 20 | 21 | The `foxy.get-assets` event occurs before the `solve` action of asset packages and during the 22 | retrieves the map of the asset packages. It is in this event that you can add new asset packages 23 | in the map. 24 | 25 | ### post-solve 26 | 27 | The `foxy.post-solve` event occurs after the `solve` action of asset packages and before the 28 | execution of the Composer's fallback. 29 | -------------------------------------------------------------------------------- /resources/doc/faqs.md: -------------------------------------------------------------------------------- 1 | FAQs 2 | ==== 3 | 4 | What version required of Composer? 5 | ---------------------------------- 6 | 7 | See the documentation: [Installation](index.md#installation). 8 | 9 | Why this plugin? 10 | ---------------- 11 | 12 | It is certain that each language has its own dependency management system, and that it is highly recommended to use 13 | each package manager. NPM or Yarn works very well when the asset dependencies are managed only in the PHP project, 14 | but when you create PHP libraries that using assets, there is no way to automatically add asset dependencies, 15 | and most importantly, no validation of versions can be done automatically. You must tell the developers 16 | the list of asset dependencies that using by your PHP library, and you must ask him to add manually the asset 17 | dependencies to its asset manager of his project. 18 | 19 | However, another solution exist - what many projects propose - you must add the assets in the folder of the 20 | PHP library (like `/assets`, `/Resources/public`). Of course, with this method, the code is duplicated, it 21 | pollutes the source code of the PHP library, no version management/validation is possible, and it is even 22 | less possible, to use correctly all tools such as Babel, Scss, Less, etc ... 23 | 24 | Foxy focuses solely on automation of the validation, addition, updating and deleting of the dependencies in 25 | the definition file of the asset package, while restoring the project state, as well as PHP dependencies if 26 | NPM or Yarn terminates with an error. 27 | 28 | What is the difference between Foxy and Fxp Composer Asset Plugin? 29 | ------------------------------------------------------------------ 30 | 31 | When [Fxp Composer Asset Plugin](https://github.com/fxpio/composer-asset-plugin) has been created, 32 | it lacked some important functionality to NPM and Bower as a true lock file, the access to 33 | private repositories, the management of organizations (scope), and the assets was limited to a simple 34 | download of the packages. The solution was to use the SAT solver, the VCS Repositories and the 35 | Composer lock file to manage the asset dependencies of the PHP libraries. However, there are 3 major 36 | disadvantages to this approach: 37 | 38 | 1. The plugin must be installed in global mode 39 | 2. Nodejs must be used more and more to compile some libraries 40 | 3. The use of VCS Repositories coupled with the SAT Solver architecture of Composer is much less 41 | efficient than NPM, despite the optimizations of the plugin to avoid the imports 42 | 43 | Now, Bower has been depreciated, NPM has a true lock file (since 5.x), as well as the possibility 44 | of using the private repositories, Yarn arrived with his big performances, and more and more javascript 45 | library requires a compilation because they use Babel, Typescript, Sass, Less, etc... 46 | 47 | Nodejs filling its gaps, and becoming more and more required, a plugin could finally perform the reverse operation, 48 | retaining the benefits of Fxp Composer Asset Plugin and NPM. So, conversely, Foxy creates package mocks for NPM 49 | in local directory, containing only the `package.json` file from the PHP library, and adding the path of the 50 | mock package to the project's `package.json` file. The entire validation and installation process is left 51 | to NPM or Yarn. However, the plugin manages the fallback if there is an error of the asset manager. 52 | 53 | To conclude, given that there is not a backward compatibility, and that it is impossible to have a version 54 | of the plugin installed globally, and another version installed in the project - because Composer will 55 | install the plugin in the project, but will only use the plugin installed globally - the Fxp Composer Asset 56 | Plugin was become Foxy. 57 | 58 | How does the plugin work? 59 | ------------------------- 60 | 61 | Foxy creates the mocks of Composer packages for NPM in local directory, containing only the `package.json` 62 | file from the PHP library, and adding the package path to the `package.json` file of the project. 63 | 64 | The name of the Composer package is converted to a format compatible with NPM, using the NPM scope 65 | `@composer-asset` and replacing the separation slash `/` between the vendor and the package name by 66 | 2 dashes `--`, giving consequently, the following format `@composer-asset/--` 67 | (this scope is reserved by Foxy in the registry of NPM and in Github). 68 | 69 | NPM will install in the `node_modules/@composer-asset` folder an updated copy of each Composer package mock 70 | that is located by default in the folder `vendor/foxy/composer-asset`. 71 | 72 | For more details, the plugin work in this order: 73 | 74 | 1. Validation of the asset manager installation, then checking of the compatible asset manager version (optional) 75 | 2. Saving the status of project 76 | 3. Installing/updating of the PHP dependencies by Composer 77 | 4. Retrieving the entire list of installed packages 78 | 5. Retains only PHP dependencies with the `foxy/foxy` dependency in the `require` or `require-dev` section of 79 | the `composer.json` file and with the presence of the `package.json` file 80 | 6. Checking the lock file of asset manager 81 | 7. Comparing the difference between the installed asset dependencies and the new asset dependencies, to determine 82 | whether the dependency must be installed, updated, or removed 83 | 8. Creating, updating, or deleting of the mock asset libraries in local directory, containing only the 84 | `package.json` file of the PHP library, with a formatted name as: 85 | `@composer-asset/--` 86 | 9. Adding, updating, or deleting the mock asset library in the `package.json` file of the project 87 | 10. Running the install or update command of asset manager 88 | 11. Restoring the `package.json` file with the previous dependencies if the asset manager terminates with an error 89 | 12. Restoring the `composer.lock` file and all PHP dependencies if the asset manager terminates with an error 90 | 91 | Is Foxy useful if my asset dependencies are defined only in my project? 92 | ----------------------------------------------------------------------- 93 | 94 | Foxy is mainly focused on automating of the asset management of the PHP libraries, avoiding potentially conflicting 95 | manual management. 96 | 97 | Given that Foxy makes it possible to ensure that the entire management process is valid whether it is for Composer 98 | and NPM or Yarn, you can use Foxy even if all of your asset dependencies are only defined in the `package.json` file 99 | of your project. However, the value added by Foxy in this configuration will be low, and will be limited to the 100 | management of the fallback of the PHP dependencies if there is an error of the asset manager. 101 | 102 | NPM/Yarn does not find the mock of the Composer dependencies 103 | ------------------------------------------------------------ 104 | 105 | The advantage of Foxy, is that it allows you to keep the workflows of each tool. However, Foxy creates PHP 106 | package mocks for NPM, and in this case, Composer must be launched before NPM or Yarn. After, nothing prevents 107 | you to using all available commands of your favorite asset manager. 108 | 109 | Why Foxy does nothing with the '--dry-run' option? 110 | -------------------------------------------------- 111 | 112 | Foxy can work with Composer's `--dry-run` option, but chose to do nothing. Given that the PHP dependencies 113 | are not installed, updated or deleted, Foxy can not update the `package.json` file, and so, NPM can not 114 | check the new constraints, if any. To sum up, this amounts to running the commands 115 | `composer update --dry-run` followed by `npm update --dry-run`. 116 | 117 | However, with the Foxy's fallbacks, this behavior is automatically reproduced, but by downloading the PHP 118 | dependencies, and restoring the `package.json` file, the `composer.lock` file, and all the PHP dependencies 119 | if the asset manager finishes with an error. 120 | 121 | How to increase the PHP memory limit? 122 | ------------------------------------- 123 | 124 | See the official documentation of Composer: [Memory limits errors](https://getcomposer.org/doc/articles/troubleshooting.md#memory-limit-errors). 125 | -------------------------------------------------------------------------------- /resources/doc/index.md: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | 1. [Introduction](index.md#introduction) 5 | 2. [Required dependencies](index.md#required-dependencies) 6 | 3. [Installation](index.md#installation) 7 | 4. [Usage](usage.md) 8 | 5. [Configuration](config.md) 9 | 6. [Event](events.md) 10 | 7. [FAQs](faqs.md) 11 | 12 | ## Introduction 13 | 14 | Foxy is a Composer plug-in that aggregates npm-packages from Composer packages. 15 | 16 | This makes it possible (and automates the process of) installing and updating npm-packages that ship with your Composer packages, leveraging the native (`npm` or `yarn`) package manager to do the heavy lifting. 17 | 18 | For this approach to work well, you should think of an npm-package in a Composer package not just as an "artifact", but as an actual npm-package *embedded* in your Composer package. 19 | 20 | Importantly, you should name it and *version* it, independently of your Composer version number - like you would normally do with a stand-alone npm-package. 21 | 22 | Note that, for npm-packages with no version number, Foxy will default to the Composer version, as a fallback only: versioning your npm-package explicitly is much safer in terms of correctly versioning breaking/non-breaking changes to any client-side APIs exposed by the embedded npm-package. 23 | 24 | ## Required dependencies 25 | 26 | - [Nodejs](https://nodejs.org) 27 | - [NPM](https://www.npmjs.com) or [Yarn](https://yarnpkg.com) 28 | - [Git](https://git-scm.com) 29 | 30 | ## Installation 31 | 32 | See the [Release Notes](https://github.com/fxpio/foxy/releases) 33 | to know the Composer version required. 34 | 35 | ```shell 36 | composer require "foxy/foxy:^1.0.0" 37 | ``` 38 | 39 | Composer will install the plugin to your project's `vendor/foxy` directory. 40 | 41 | ## Next step 42 | 43 | You can read how to: 44 | 45 | - [Use this plugin](usage.md) 46 | - [Configure this plugin](config.md) 47 | - [Expand Foxy with Composer events](events.md) 48 | -------------------------------------------------------------------------------- /resources/doc/usage.md: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | To use Foxy, whether for a PHP library or a PHP project, you must add the Foxy dependency in 5 | the `require` section of the Composer file. 6 | 7 | **composer.json:** 8 | ```json 9 | { 10 | "require": { 11 | "php-forge/foxy": "^0.1" 12 | } 13 | } 14 | ``` 15 | 16 | And create the `package.json` file to add your asset dependencies. 17 | 18 | **package.json:** 19 | ```json 20 | { 21 | "dependencies": { 22 | "@foo/bar": "latest" 23 | } 24 | } 25 | ``` 26 | 27 | Composer will install Foxy automatically before installing the PHP dependencies, and Foxy will immediately 28 | deal with the asset dependencies. 29 | 30 | ## Use the plugin in PHP library 31 | 32 | ### With the Foxy dependency 33 | 34 | In the case if you use Foxy in a PHP library, you can render Foxy optional by adding its dependency in 35 | the `require-dev` section of the Composer file. 36 | 37 | **composer.json:** 38 | ```json 39 | { 40 | "require-dev": { 41 | "php-forge/foxy": "^0.1" 42 | } 43 | } 44 | ``` 45 | 46 | > **Note:** 47 | > 48 | > If no PHP dependency requires Foxy inevitably (always in `require-dev` section), you must add Foxy 49 | > in the required dependencies to the `composer.json` file of your project. 50 | 51 | ### With the Composer's extra option 52 | 53 | However, if you want enable the Foxy for your library, but without required dependencies or dev dependencies, 54 | you can use the extra option `extra.foxy` in your `composer.json` file: 55 | 56 | **composer.json:** 57 | ```json 58 | { 59 | "extra": { 60 | "foxy": true 61 | } 62 | } 63 | ``` 64 | 65 | > **Note:** 66 | > 67 | > Like for the activation with the Foxy dependencies, you must add Foxy in the required dependencies 68 | > to the `composer.json` file of your project. 69 | -------------------------------------------------------------------------------- /resources/foxy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/Asset/AbstractAssetManager.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Asset; 15 | 16 | use Composer\IO\IOInterface; 17 | use Composer\Package\RootPackageInterface; 18 | use Composer\Semver\VersionParser; 19 | use Composer\Util\Filesystem; 20 | use Composer\Util\Platform; 21 | use Composer\Util\ProcessExecutor; 22 | use Foxy\Config\Config; 23 | use Foxy\Converter\SemverConverter; 24 | use Foxy\Converter\VersionConverterInterface; 25 | use Foxy\Exception\RuntimeException; 26 | use Foxy\Fallback\FallbackInterface; 27 | use Foxy\Json\JsonFile; 28 | 29 | /** 30 | * Abstract Manager. 31 | * 32 | * @author François Pluchino 33 | */ 34 | abstract class AbstractAssetManager implements AssetManagerInterface 35 | { 36 | final public const NODE_MODULES_PATH = './node_modules'; 37 | protected bool $updatable = true; 38 | private null|string $version = ''; 39 | 40 | public function __construct( 41 | protected IOInterface $io, 42 | protected Config $config, 43 | protected ProcessExecutor $executor, 44 | protected Filesystem $fs, 45 | protected FallbackInterface|null $fallback = null, 46 | protected VersionConverterInterface|null $versionConverter = null 47 | ) { 48 | $this->versionConverter ??= new SemverConverter(); 49 | } 50 | 51 | public function isAvailable(): bool 52 | { 53 | return null !== $this->getVersion(); 54 | } 55 | 56 | public function getPackageName(): string 57 | { 58 | return 'package.json'; 59 | } 60 | 61 | public function hasLockFile(): bool 62 | { 63 | return file_exists($this->getLockPackageName()); 64 | } 65 | 66 | public function isInstalled(): bool 67 | { 68 | return is_dir(self::NODE_MODULES_PATH) && file_exists($this->getPackageName()); 69 | } 70 | 71 | public function setFallback(FallbackInterface $fallback): static 72 | { 73 | $this->fallback = $fallback; 74 | 75 | return $this; 76 | } 77 | 78 | public function setUpdatable($updatable): static 79 | { 80 | $this->updatable = $updatable; 81 | 82 | return $this; 83 | } 84 | 85 | public function isUpdatable(): bool 86 | { 87 | return $this->updatable && $this->isInstalled() && $this->isValidForUpdate(); 88 | } 89 | 90 | public function isValidForUpdate(): bool 91 | { 92 | return true; 93 | } 94 | 95 | public function validate(): void 96 | { 97 | $version = $this->getVersion(); 98 | /** @var string $constraintVersion */ 99 | $constraintVersion = $this->config->get('manager-version'); 100 | 101 | if (null === $version) { 102 | throw new RuntimeException(sprintf('The binary of "%s" must be installed', $this->getName())); 103 | } 104 | 105 | if ($constraintVersion) { 106 | $parser = new VersionParser(); 107 | $constraint = $parser->parseConstraints($constraintVersion); 108 | 109 | if (!$constraint->matches($parser->parseConstraints($version))) { 110 | throw new RuntimeException( 111 | sprintf( 112 | 'The installed %s version "%s" doesn\'t match with the constraint version "%s"', 113 | $this->getName(), 114 | $version, 115 | $constraintVersion 116 | ) 117 | ); 118 | } 119 | } 120 | } 121 | 122 | public function addDependencies(RootPackageInterface $rootPackage, array $dependencies): AssetPackageInterface 123 | { 124 | $assetPackage = new AssetPackage($rootPackage, new JsonFile($this->getPackageName(), null, $this->io)); 125 | $assetPackage->removeUnusedDependencies($dependencies); 126 | $alreadyInstalledDependencies = $assetPackage->addNewDependencies($dependencies); 127 | 128 | $this->actionWhenComposerDependenciesAreAlreadyInstalled($alreadyInstalledDependencies); 129 | $this->io->write('Merging Composer dependencies in the asset package'); 130 | 131 | return $assetPackage->write(); 132 | } 133 | 134 | public function run(): int 135 | { 136 | if (true !== $this->config->get('run-asset-manager')) { 137 | return 0; 138 | } 139 | 140 | $rootPackageDir = $this->config->get('root-package-json-dir'); 141 | 142 | if (is_string($rootPackageDir) && !empty($rootPackageDir)) { 143 | if (!is_dir($rootPackageDir)) { 144 | throw new RuntimeException(sprintf('The root package directory "%s" doesn\'t exist.', $rootPackageDir)); 145 | } 146 | chdir($rootPackageDir); 147 | } 148 | 149 | $updatable = $this->isUpdatable(); 150 | $info = sprintf('%s %s dependencies', $updatable ? 'Updating' : 'Installing', $this->getName()); 151 | $this->io->write($info); 152 | 153 | $timeout = ProcessExecutor::getTimeout(); 154 | 155 | /** @var int $managerTimeout */ 156 | $managerTimeout = $this->config->get('manager-timeout', PHP_INT_MAX); 157 | ProcessExecutor::setTimeout($managerTimeout); 158 | 159 | $cmd = $updatable ? $this->getUpdateCommand() : $this->getInstallCommand(); 160 | $res = $this->executor->execute($cmd); 161 | 162 | ProcessExecutor::setTimeout($timeout); 163 | 164 | if ($res > 0 && null !== $this->fallback) { 165 | $this->fallback->restore(); 166 | } 167 | 168 | return $res; 169 | } 170 | 171 | /** 172 | * Action when the composer dependencies are already installed. 173 | * 174 | * @param array $names the asset package name of composer dependencies. 175 | * 176 | * @psalm-param list $names 177 | */ 178 | protected function actionWhenComposerDependenciesAreAlreadyInstalled(array $names): void 179 | { 180 | // do nothing by default 181 | } 182 | 183 | /** 184 | * Build the command with binary and command options. 185 | * 186 | * @param string $defaultBin The default binary of command if option isn't defined. 187 | * @param string $action The command action to retrieve the options in config. 188 | * @param array|string $command The command. 189 | */ 190 | protected function buildCommand(string $defaultBin, string $action, array|string $command): string 191 | { 192 | $bin = $this->config->get('manager-bin', $defaultBin); 193 | $bin = Platform::isWindows() ? str_replace('/', '\\', (string) $bin) : $bin; 194 | $gOptions = trim((string) $this->config->get('manager-options', '')); 195 | $options = trim((string) $this->config->get('manager-' . $action . '-options', '')); 196 | 197 | /** @psalm-var string|string[] $command */ 198 | return (string) $bin . ' ' . implode(' ', (array) $command) 199 | . (empty($gOptions) ? '' : ' ' . $gOptions) 200 | . (empty($options) ? '' : ' ' . $options); 201 | } 202 | 203 | protected function getVersion(): string|null 204 | { 205 | if ($this->version === '' && $this->versionConverter !== null) { 206 | $this->executor->execute($this->getVersionCommand(), $version); 207 | $this->version = '' !== trim((string) $version) ? $this->versionConverter->convertVersion(trim((string) $version)) : null; 208 | } 209 | 210 | return $this->version; 211 | } 212 | 213 | /** 214 | * Get the command to retrieve the version. 215 | */ 216 | abstract protected function getVersionCommand(): string; 217 | 218 | /** 219 | * Get the command to install the asset dependencies. 220 | */ 221 | abstract protected function getInstallCommand(): string; 222 | 223 | /** 224 | * Get the command to update the asset dependencies. 225 | */ 226 | abstract protected function getUpdateCommand(): string; 227 | } 228 | -------------------------------------------------------------------------------- /src/Asset/AssetManagerFinder.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Asset; 15 | 16 | use Foxy\Exception\RuntimeException; 17 | 18 | /** 19 | * Asset Manager finder. 20 | * 21 | * @author François Pluchino 22 | */ 23 | final class AssetManagerFinder 24 | { 25 | /** 26 | * @psalm-var AssetManagerInterface[] 27 | */ 28 | private array $managers = []; 29 | 30 | /** 31 | * @psalm-param AssetManagerInterface[] $managers The asset managers 32 | */ 33 | public function __construct(array $managers = []) 34 | { 35 | foreach ($managers as $manager) { 36 | if ($manager instanceof AssetManagerInterface) { 37 | $this->addManager($manager); 38 | } 39 | } 40 | } 41 | 42 | public function addManager(AssetManagerInterface $manager): void 43 | { 44 | $this->managers[$manager->getName()] = $manager; 45 | } 46 | 47 | /** 48 | * Find the asset manager. 49 | * 50 | * @param string|null $manager The name of the asset manager 51 | * 52 | * @throws RuntimeException When the asset manager does not exist 53 | * @throws RuntimeException When the asset manager is not found 54 | */ 55 | public function findManager(string|null $manager = null): AssetManagerInterface 56 | { 57 | if (null !== $manager) { 58 | if (isset($this->managers[$manager])) { 59 | return $this->managers[$manager]; 60 | } 61 | 62 | throw new RuntimeException(sprintf('The asset manager "%s" doesn\'t exist', $manager)); 63 | } 64 | 65 | return $this->findAvailableManager(); 66 | } 67 | 68 | /** 69 | * Find the available asset manager. 70 | * 71 | * @throws RuntimeException When no asset manager is found 72 | */ 73 | private function findAvailableManager(): AssetManagerInterface 74 | { 75 | // find asset manager by lockfile 76 | foreach ($this->managers as $manager) { 77 | if ($manager->hasLockFile()) { 78 | return $manager; 79 | } 80 | } 81 | 82 | // find asset manager by availability 83 | foreach ($this->managers as $manager) { 84 | if ($manager->isAvailable()) { 85 | return $manager; 86 | } 87 | } 88 | 89 | throw new RuntimeException('No asset manager is found'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Asset/AssetManagerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Asset; 15 | 16 | use Composer\Package\RootPackageInterface; 17 | use Foxy\Exception\RuntimeException; 18 | use Foxy\Fallback\FallbackInterface; 19 | 20 | /** 21 | * Interface of asset manager. 22 | * 23 | * @author François Pluchino 24 | */ 25 | interface AssetManagerInterface 26 | { 27 | /** 28 | * Get the name of asset manager. 29 | */ 30 | public function getName(): string; 31 | 32 | /** 33 | * Check if the asset manager is available. 34 | */ 35 | public function isAvailable(): bool; 36 | 37 | /** 38 | * Get the filename of the asset package. 39 | */ 40 | public function getPackageName(): string; 41 | 42 | /** 43 | * Check if the lock file is present or not. 44 | */ 45 | public function hasLockFile(): bool; 46 | 47 | /** 48 | * Check if the asset dependencies are installed or not. 49 | */ 50 | public function isInstalled(): bool; 51 | 52 | /** 53 | * Set the fallback. 54 | * 55 | * @param FallbackInterface $fallback The fallback 56 | */ 57 | public function setFallback(FallbackInterface $fallback): self; 58 | 59 | /** 60 | * Define if the asset manager can be use the update command. 61 | * 62 | * @param bool $updatable The value 63 | */ 64 | public function setUpdatable(bool $updatable): self; 65 | 66 | /** 67 | * Check if the asset manager can be use the update command or not. 68 | */ 69 | public function isUpdatable(): bool; 70 | 71 | /** 72 | * Check if the asset package is valid for the update. 73 | */ 74 | public function isValidForUpdate(): bool; 75 | 76 | /** 77 | * Get the filename of the lock file. 78 | */ 79 | public function getLockPackageName(): string; 80 | 81 | /** 82 | * Validate the version of asset manager. 83 | * 84 | * @throws RuntimeException When the binary isn't installed 85 | * @throws RuntimeException When the version doesn't match 86 | */ 87 | public function validate(): void; 88 | 89 | /** 90 | * Add the asset dependencies in asset package file. 91 | * 92 | * @param RootPackageInterface $rootPackage The composer root package 93 | * @param array $dependencies The asset local dependencies 94 | */ 95 | public function addDependencies(RootPackageInterface $rootPackage, array $dependencies): AssetPackageInterface; 96 | 97 | /** 98 | * Run the asset manager to install/update the asset dependencies. 99 | */ 100 | public function run(): int; 101 | } 102 | -------------------------------------------------------------------------------- /src/Asset/AssetPackage.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Asset; 15 | 16 | use Composer\Json\JsonFile; 17 | use Composer\Package\RootPackageInterface; 18 | 19 | /** 20 | * Asset package. 21 | * 22 | * @author François Pluchino 23 | */ 24 | final class AssetPackage implements AssetPackageInterface 25 | { 26 | public const SECTION_DEPENDENCIES = 'dependencies'; 27 | public const SECTION_DEV_DEPENDENCIES = 'devDependencies'; 28 | public const COMPOSER_PREFIX = '@composer-asset/'; 29 | protected array $package = []; 30 | 31 | /** 32 | * @param RootPackageInterface $rootPackage The composer root package 33 | * @param JsonFile $jsonFile The json file 34 | */ 35 | public function __construct(RootPackageInterface $rootPackage, protected JsonFile $jsonFile) 36 | { 37 | $this->jsonFile = $jsonFile; 38 | 39 | if ($jsonFile->exists()) { 40 | $this->setPackage((array) $jsonFile->read()); 41 | } 42 | 43 | $this->injectRequiredKeys($rootPackage); 44 | } 45 | 46 | public function write(): self 47 | { 48 | $this->jsonFile->write($this->package); 49 | 50 | return $this; 51 | } 52 | 53 | public function setPackage(array $package): self 54 | { 55 | $this->package = $package; 56 | 57 | return $this; 58 | } 59 | 60 | public function getPackage(): array 61 | { 62 | return $this->package; 63 | } 64 | 65 | public function getInstalledDependencies(): array 66 | { 67 | $installedAssets = []; 68 | 69 | if (isset($this->package[self::SECTION_DEPENDENCIES]) && \is_array($this->package[self::SECTION_DEPENDENCIES])) { 70 | /** 71 | * @var string $dependency 72 | * @var string $version 73 | */ 74 | foreach ($this->package[self::SECTION_DEPENDENCIES] as $dependency => $version) { 75 | if (str_starts_with($dependency, self::COMPOSER_PREFIX)) { 76 | $installedAssets[$dependency] = $version; 77 | } 78 | } 79 | } 80 | 81 | return $installedAssets; 82 | } 83 | 84 | /** 85 | * Add new dependencies. 86 | * 87 | * @param array $dependencies The dependencies 88 | * 89 | * @return array The existing packages. 90 | * 91 | * @psalm-return list The existing packages. 92 | * 93 | * @psalm-suppress MixedArrayAssignment 94 | */ 95 | public function addNewDependencies(array $dependencies): array 96 | { 97 | $installedAssets = $this->getInstalledDependencies(); 98 | $existingPackages = []; 99 | 100 | /** 101 | * @var string $name 102 | * @var string $path 103 | */ 104 | foreach ($dependencies as $name => $path) { 105 | if (isset($installedAssets[$name])) { 106 | $existingPackages[] = $name; 107 | } else { 108 | $this->package[self::SECTION_DEPENDENCIES][$name] = 'file:./' . \dirname($path); 109 | } 110 | } 111 | 112 | $this->orderPackages(self::SECTION_DEPENDENCIES); 113 | $this->orderPackages(self::SECTION_DEV_DEPENDENCIES); 114 | 115 | return $existingPackages; 116 | } 117 | 118 | /** 119 | * @psalm-suppress MixedArrayAccess 120 | */ 121 | public function removeUnusedDependencies(array $dependencies): self 122 | { 123 | $installedAssets = $this->getInstalledDependencies(); 124 | $removeDependencies = array_diff_key($installedAssets, $dependencies); 125 | 126 | foreach ($removeDependencies as $dependency => $version) { 127 | unset($this->package[self::SECTION_DEPENDENCIES][$dependency]); 128 | } 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Inject the required keys for asset package defined in root composer package. 135 | * 136 | * @param RootPackageInterface $rootPackage The composer root package 137 | */ 138 | protected function injectRequiredKeys(RootPackageInterface $rootPackage): void 139 | { 140 | if (!isset($this->package['license']) && \count($rootPackage->getLicense()) > 0) { 141 | $license = current($rootPackage->getLicense()); 142 | 143 | if ('proprietary' === $license) { 144 | if (!isset($this->package['private'])) { 145 | $this->package['private'] = true; 146 | } 147 | } else { 148 | $this->package['license'] = $license; 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * Order the packages section. 155 | * 156 | * @param string $section The package section 157 | */ 158 | protected function orderPackages(string $section): void 159 | { 160 | if (isset($this->package[$section]) && \is_array($this->package[$section])) { 161 | ksort($this->package[$section], SORT_STRING); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Asset/AssetPackageInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Asset; 15 | 16 | /** 17 | * Interface of asset package. 18 | * 19 | * @author François Pluchino 20 | */ 21 | interface AssetPackageInterface 22 | { 23 | /** 24 | * Write the asset package in file. 25 | */ 26 | public function write(): self; 27 | 28 | /** 29 | * Set the asset package. 30 | * 31 | * @param array $package The asset package 32 | */ 33 | public function setPackage(array $package): self; 34 | 35 | /** 36 | * Get the asset package. 37 | */ 38 | public function getPackage(): array; 39 | 40 | /** 41 | * Get the installed asset dependencies. 42 | * 43 | * @return array The installed asset dependencies 44 | */ 45 | public function getInstalledDependencies(): array; 46 | 47 | /** 48 | * Add the new asset dependencies and return the names of already installed asset dependencies. 49 | * 50 | * @param array $dependencies The asset dependencies 51 | * 52 | * @return array The asset package name of the already asset dependencies 53 | */ 54 | public function addNewDependencies(array $dependencies): array; 55 | 56 | /** 57 | * Remove the unused asset dependencies. 58 | * 59 | * @param array $dependencies All asset dependencies 60 | */ 61 | public function removeUnusedDependencies(array $dependencies): self; 62 | } 63 | -------------------------------------------------------------------------------- /src/Asset/BunManager.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Asset; 15 | 16 | use Composer\Util\Platform; 17 | 18 | /** 19 | * Pnpm Manager. 20 | * 21 | * @author Wilmer Arambula (terabytesfotw@gmail.com) 22 | */ 23 | final class BunManager extends AbstractAssetManager 24 | { 25 | public function getName(): string 26 | { 27 | return 'bun'; 28 | } 29 | 30 | public function getLockPackageName(): string 31 | { 32 | return 'yarn.lock'; 33 | } 34 | 35 | public function isInstalled(): bool 36 | { 37 | return parent::isInstalled() && file_exists($this->getLockPackageName()); 38 | } 39 | 40 | protected function getVersionCommand(): string 41 | { 42 | $command = Platform::isWindows() ? 'bun.exe' : 'bun'; 43 | 44 | return $this->buildCommand($command, 'version', '--version'); 45 | } 46 | 47 | protected function getInstallCommand(): string 48 | { 49 | $command = Platform::isWindows() ? 'bun.exe' : 'bun'; 50 | 51 | return $this->buildCommand($command, 'install', 'install -y'); 52 | } 53 | 54 | protected function getUpdateCommand(): string 55 | { 56 | $command = Platform::isWindows() ? 'bun.exe' : 'bun'; 57 | 58 | return $this->buildCommand($command, 'update', 'update -y'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Asset/NpmManager.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Asset; 15 | 16 | /** 17 | * NPM Manager. 18 | * 19 | * @author François Pluchino 20 | */ 21 | final class NpmManager extends AbstractAssetManager 22 | { 23 | public function getName(): string 24 | { 25 | return 'npm'; 26 | } 27 | 28 | public function getLockPackageName(): string 29 | { 30 | return 'package-lock.json'; 31 | } 32 | 33 | protected function getVersionCommand(): string 34 | { 35 | return $this->buildCommand('npm', 'version', '--version'); 36 | } 37 | 38 | protected function getInstallCommand(): string 39 | { 40 | return $this->buildCommand('npm', 'install', 'install'); 41 | } 42 | 43 | protected function getUpdateCommand(): string 44 | { 45 | return $this->buildCommand('npm', 'update', 'update'); 46 | } 47 | 48 | protected function actionWhenComposerDependenciesAreAlreadyInstalled(array $names): void 49 | { 50 | foreach ($names as $name) { 51 | $this->fs->remove(self::NODE_MODULES_PATH . '/' . $name); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Asset/PnpmManager.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Asset; 15 | 16 | /** 17 | * Pnpm Manager. 18 | * 19 | * @author Steffen Dietz 20 | */ 21 | final class PnpmManager extends AbstractAssetManager 22 | { 23 | public function getName(): string 24 | { 25 | return 'pnpm'; 26 | } 27 | 28 | public function getLockPackageName(): string 29 | { 30 | return 'pnpm-lock.yaml'; 31 | } 32 | 33 | public function isInstalled(): bool 34 | { 35 | return parent::isInstalled() && file_exists($this->getLockPackageName()); 36 | } 37 | 38 | protected function getVersionCommand(): string 39 | { 40 | return $this->buildCommand('pnpm', 'version', '--version'); 41 | } 42 | 43 | protected function getInstallCommand(): string 44 | { 45 | return $this->buildCommand('pnpm', 'install', 'install'); 46 | } 47 | 48 | protected function getUpdateCommand(): string 49 | { 50 | return $this->buildCommand('pnpm', 'update', 'update'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Asset/YarnManager.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Asset; 15 | 16 | use Composer\Semver\VersionParser; 17 | 18 | /** 19 | * Yarn Manager. 20 | * 21 | * @author François Pluchino 22 | */ 23 | final class YarnManager extends AbstractAssetManager 24 | { 25 | public function getName(): string 26 | { 27 | return 'yarn'; 28 | } 29 | 30 | public function getLockPackageName(): string 31 | { 32 | return 'yarn.lock'; 33 | } 34 | 35 | public function isInstalled(): bool 36 | { 37 | return parent::isInstalled() && file_exists($this->getLockPackageName()); 38 | } 39 | 40 | public function isValidForUpdate(): bool 41 | { 42 | if ($this->isYarnNext()) { 43 | return true; 44 | } 45 | 46 | $cmd = $this->buildCommand('yarn', 'check', $this->mergeInteractiveCommand(['check'])); 47 | 48 | return 0 === $this->executor->execute($cmd); 49 | } 50 | 51 | protected function getVersionCommand(): string 52 | { 53 | return $this->buildCommand('yarn', 'version', '--version'); 54 | } 55 | 56 | protected function getInstallCommand(): string 57 | { 58 | return $this->buildCommand('yarn', 'install', $this->mergeInteractiveCommand(['install'])); 59 | } 60 | 61 | protected function getUpdateCommand(): string 62 | { 63 | $commandName = $this->isYarnNext() ? 'up' : 'upgrade'; 64 | 65 | return $this->buildCommand('yarn', 'update', $this->mergeInteractiveCommand([$commandName])); 66 | } 67 | 68 | private function isYarnNext(): bool 69 | { 70 | $version = $this->getVersion(); 71 | $parser = new VersionParser(); 72 | $constraint = $parser->parseConstraints('>=2.0.0'); 73 | 74 | return $version !== null ? $constraint->matches($parser->parseConstraints($version)) : false; 75 | } 76 | 77 | private function mergeInteractiveCommand(array $command): array 78 | { 79 | if (!$this->isYarnNext()) { 80 | $command[] = '--non-interactive'; 81 | } 82 | 83 | return $command; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Config/Config.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Config; 15 | 16 | use Foxy\Exception\RuntimeException; 17 | 18 | /** 19 | * Helper of package config. 20 | * 21 | * @author François Pluchino 22 | */ 23 | class Config 24 | { 25 | private array $cacheEnv = []; 26 | 27 | /** 28 | * @param array $config The config. 29 | * @param array $defaults The default values. 30 | */ 31 | public function __construct(private array $config, private array $defaults = []) 32 | { 33 | $this->config = $config; 34 | $this->defaults = $defaults; 35 | } 36 | 37 | /** 38 | * Get the array config value. 39 | * 40 | * @param string $key The config key. 41 | * @param array $default The default value. 42 | */ 43 | public function getArray(string $key, array $default = []): array 44 | { 45 | $value = $this->get($key, null); 46 | 47 | return is_array($value) ? $value : $default; 48 | } 49 | 50 | /** 51 | * Get the config value. 52 | * 53 | * @param string $key The config key. 54 | * @param mixed $default The default value. 55 | */ 56 | public function get(string $key, mixed $default = null): mixed 57 | { 58 | if (\array_key_exists($key, $this->cacheEnv)) { 59 | return $this->cacheEnv[$key]; 60 | } 61 | 62 | $envKey = $this->convertEnvKey($key); 63 | $envValue = getenv($envKey); 64 | 65 | if (false !== $envValue) { 66 | return $this->cacheEnv[$key] = $this->convertEnvValue($envValue, $envKey); 67 | } 68 | 69 | $defaultValue = $this->getDefaultValue($key, $default); 70 | 71 | return \array_key_exists($key, $this->config) 72 | ? $this->getByManager($key, $this->config[$key], $defaultValue) 73 | : $defaultValue; 74 | } 75 | 76 | /** 77 | * Convert the config key into environment variable. 78 | * 79 | * @param string $key The config key. 80 | */ 81 | private function convertEnvKey(string $key): string 82 | { 83 | return 'FOXY__' . strtoupper(str_replace('-', '_', $key)); 84 | } 85 | 86 | /** 87 | * Convert the value of environment variable into php variable. 88 | * 89 | * @param string $value The value of environment variable. 90 | * @param string $environmentVariable The environment variable name. 91 | */ 92 | private function convertEnvValue(string $value, string $environmentVariable): array|bool|int|string 93 | { 94 | $value = trim(trim(trim($value, '\''), '"')); 95 | 96 | if ($this->isBoolean($value)) { 97 | $value = $this->convertBoolean($value); 98 | } elseif ($this->isInteger($value)) { 99 | $value = $this->convertInteger($value); 100 | } elseif ($this->isJson($value)) { 101 | $value = $this->convertJson($value, $environmentVariable); 102 | } 103 | 104 | return $value; 105 | } 106 | 107 | /** 108 | * Check if the value of environment variable is a boolean. 109 | * 110 | * @param string $value The value of environment variable. 111 | */ 112 | private function isBoolean(string $value): bool 113 | { 114 | $value = strtolower($value); 115 | 116 | return \in_array($value, ['true', 'false', '1', '0', 'yes', 'no', 'y', 'n'], true); 117 | } 118 | 119 | /** 120 | * Convert the value of environment variable into a boolean. 121 | * 122 | * @param string $value The value of environment variable. 123 | */ 124 | private function convertBoolean(string $value): bool 125 | { 126 | return \in_array($value, ['true', '1', 'yes', 'y'], true); 127 | } 128 | 129 | /** 130 | * Check if the value of environment variable is a integer. 131 | * 132 | * @param string $value The value of environment variable. 133 | */ 134 | private function isInteger(string $value): bool 135 | { 136 | return ctype_digit(trim($value, '-')); 137 | } 138 | 139 | /** 140 | * Convert the value of environment variable into a integer. 141 | * 142 | * @param string $value The value of environment variable. 143 | */ 144 | private function convertInteger(string $value): int 145 | { 146 | return (int) $value; 147 | } 148 | 149 | /** 150 | * Check if the value of environment variable is a string JSON. 151 | * 152 | * @param string $value The value of environment variable. 153 | */ 154 | private function isJson(string $value): bool 155 | { 156 | return str_starts_with($value, '{') || str_starts_with($value, '['); 157 | } 158 | 159 | /** 160 | * Convert the value of environment variable into a json array. 161 | * 162 | * @param string $value The value of environment variable. 163 | * @param string $environmentVariable The environment variable name. 164 | */ 165 | private function convertJson(string $value, string $environmentVariable): array 166 | { 167 | $value = json_decode($value, true); 168 | 169 | if (json_last_error()) { 170 | throw new RuntimeException( 171 | sprintf('The "%s" environment variable isn\'t a valid JSON', $environmentVariable) 172 | ); 173 | } 174 | 175 | return is_array($value) ? $value : []; 176 | } 177 | 178 | /** 179 | * Get the configured default value or custom default value. 180 | * 181 | * @param string $key The config key. 182 | * @param mixed $default The default value. 183 | */ 184 | private function getDefaultValue(string $key, mixed $default = null): mixed 185 | { 186 | $value = null === $default && \array_key_exists($key, $this->defaults) 187 | ? $this->defaults[$key] 188 | : $default; 189 | 190 | return $this->getByManager($key, $value, $default); 191 | } 192 | 193 | /** 194 | * Get the value defined by the manager name in the key. 195 | * 196 | * @param string $key The config key. 197 | * @param mixed $value The value. 198 | * @param mixed $default The default value. 199 | */ 200 | private function getByManager(string $key, mixed $value, mixed $default = null): mixed 201 | { 202 | if (str_starts_with($key, 'manager-') && \is_array($value)) { 203 | /** @var int|string $manager */ 204 | $manager = $this->get('manager', ''); 205 | 206 | $value = \array_key_exists($manager, $value) 207 | ? $value[$manager] 208 | : $default; 209 | } 210 | 211 | return $value; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/Config/ConfigBuilder.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Config; 15 | 16 | use Composer\Composer; 17 | use Composer\IO\IOInterface; 18 | use Composer\Json\JsonFile; 19 | 20 | /** 21 | * Plugin Config builder. 22 | * 23 | * @author François Pluchino 24 | */ 25 | abstract class ConfigBuilder 26 | { 27 | /** 28 | * Build the config of plugin. 29 | * 30 | * @param Composer $composer The composer. 31 | * @param array $defaults The default values. 32 | * @param IOInterface|null $io The composer input/output. 33 | */ 34 | public static function build(Composer $composer, array $defaults = [], IOInterface|null $io = null): Config 35 | { 36 | $config = self::getConfigBase($composer, $io); 37 | 38 | return new Config($config, $defaults); 39 | } 40 | 41 | /** 42 | * Get the base of data. 43 | * 44 | * @param Composer $composer The composer. 45 | * @param IOInterface|null $io The composer input/output. 46 | */ 47 | private static function getConfigBase(Composer $composer, IOInterface|null $io = null): array 48 | { 49 | $globalPackageConfig = self::getGlobalConfig($composer, 'composer', $io); 50 | $globalConfig = self::getGlobalConfig($composer, 'config', $io); 51 | $packageConfig = $composer->getPackage()->getConfig(); 52 | $packageConfig = isset($packageConfig['foxy']) && \is_array($packageConfig['foxy']) 53 | ? $packageConfig['foxy'] 54 | : []; 55 | 56 | return array_merge($globalPackageConfig, $globalConfig, $packageConfig); 57 | } 58 | 59 | /** 60 | * Get the data of the global config. 61 | * 62 | * @param Composer $composer The composer. 63 | * @param string $filename The filename. 64 | * @param IOInterface|null $io The composer input/output. 65 | */ 66 | private static function getGlobalConfig(Composer $composer, string $filename, IOInterface|null $io = null): array 67 | { 68 | $home = self::getComposerHome($composer); 69 | $file = new JsonFile($home . '/' . $filename . '.json'); 70 | $config = []; 71 | 72 | if ($file->exists()) { 73 | /** @var array $data */ 74 | $data = $file->read(); 75 | 76 | if (isset($data['config']['foxy']) && \is_array($data['config']['foxy'])) { 77 | $config = $data['config']['foxy']; 78 | 79 | if ($io instanceof IOInterface && $io->isDebug()) { 80 | $io->writeError('Loading Foxy config in file ' . $file->getPath()); 81 | } 82 | } 83 | } 84 | 85 | return $config; 86 | } 87 | 88 | /** 89 | * Get the home directory of composer. 90 | * 91 | * @param Composer $composer The composer 92 | */ 93 | private static function getComposerHome(Composer $composer): string 94 | { 95 | $composerHome = $composer->getConfig()->get('home'); 96 | 97 | return is_string($composerHome) ? $composerHome : ''; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Converter/SemverConverter.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Converter; 15 | 16 | /** 17 | * Converter for Semver syntax version to composer syntax version. 18 | * 19 | * @author François Pluchino 20 | */ 21 | final class SemverConverter implements VersionConverterInterface 22 | { 23 | public function convertVersion(string|null $version = null): string 24 | { 25 | if (\in_array($version, [null, '', 'latest'], true)) { 26 | return ('latest' === $version ? 'default || ' : '') . '*'; 27 | } 28 | 29 | $version = str_replace('–', '-', $version); 30 | $prefix = preg_match('/^[a-z]/', $version) && !str_starts_with($version, 'dev-') ? substr($version, 0, 1) : ''; 31 | $version = substr($version, \strlen($prefix)); 32 | $version = SemverUtil::convertVersionMetadata($version); 33 | $version = SemverUtil::convertDateVersion($version); 34 | 35 | return $prefix . $version; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Converter/SemverUtil.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Converter; 15 | 16 | use Composer\Package\Version\VersionParser; 17 | 18 | /** 19 | * Utils for semver converter. 20 | * 21 | * @author François Pluchino 22 | */ 23 | abstract class SemverUtil 24 | { 25 | /** 26 | * Converts the date or datetime version. 27 | * 28 | * @param string $version The version. 29 | */ 30 | public static function convertDateVersion(string $version): string 31 | { 32 | if (preg_match('/^\d{7,}\./', $version)) { 33 | $pos = strpos($version, '.'); 34 | 35 | if ($pos !== false) { 36 | $version = substr($version, 0, $pos) . self::convertDateMinorVersion(substr($version, $pos + 1)); 37 | } 38 | } 39 | 40 | return $version; 41 | } 42 | 43 | /** 44 | * Converts the version metadata. 45 | */ 46 | public static function convertVersionMetadata(string $version): string 47 | { 48 | $pattern = self::createPattern('([a-zA-Z]+|(\-|\+)[a-zA-Z]+|(\-|\+)[0-9]+)'); 49 | 50 | if (preg_match_all($pattern, $version, $matches, PREG_OFFSET_CAPTURE) > 0) { 51 | [$type, $version, $end] = self::cleanVersion(strtolower($version), $matches); 52 | [$version, $patchVersion] = self::matchVersion($version, $type); 53 | 54 | $matches = []; 55 | $hasPatchNumber = preg_match('/[0-9]+\.[0-9]+|[0-9]+|\.[0-9]+$/', $end, $matches); 56 | $end = $hasPatchNumber ? $matches[0] : '1'; 57 | 58 | if ($patchVersion) { 59 | $version .= $end; 60 | } 61 | } 62 | 63 | return static::cleanWildcard($version); 64 | } 65 | 66 | /** 67 | * Creates a pattern with the version prefix pattern. 68 | * 69 | * @param string $pattern The pattern without '/'. 70 | * 71 | * @return string The full pattern with '/'. 72 | * 73 | * @psalm-return non-empty-string 74 | */ 75 | public static function createPattern(string $pattern): string 76 | { 77 | $numVer = '([0-9]+|x|\*)'; 78 | $numVer2 = '(' . $numVer . '\.' . $numVer . ')'; 79 | $numVer3 = '(' . $numVer . '\.' . $numVer . '\.' . $numVer . ')'; 80 | 81 | return '/^(' . $numVer . '|' . $numVer2 . '|' . $numVer3 . ')' . $pattern . '/'; 82 | } 83 | 84 | /** 85 | * Clean the wildcard in version. 86 | * 87 | * @param string $version The version. 88 | * 89 | * @return string The cleaned version. 90 | */ 91 | private static function cleanWildcard(string $version): string 92 | { 93 | while (str_contains($version, '.x.x')) { 94 | $version = str_replace('.x.x', '.x', $version); 95 | } 96 | 97 | return $version; 98 | } 99 | 100 | /** 101 | * Clean the raw version. 102 | * 103 | * @param string $version The version. 104 | * @param array $matches The match of pattern asset version. 105 | * 106 | * @return array The list of $type, $version and $end. 107 | * 108 | * @psalm-suppress MixedArrayAccess 109 | * @psalm-suppress MixedOperand 110 | * 111 | * @psalm-return array{0: string, 1: string, 2: string} 112 | */ 113 | private static function cleanVersion(string $version, array $matches): array 114 | { 115 | $end = substr($version, \strlen((string) $matches[1][0][0])); 116 | $version = $matches[1][0][0] . '-'; 117 | 118 | $matches = []; 119 | if (preg_match('/^([-+])/', $end, $matches)) { 120 | $end = substr($end, 1); 121 | } 122 | 123 | $matches = []; 124 | preg_match('/^[a-z]+/', $end, $matches); 125 | $type = isset($matches[0]) ? self::normalizeStability($matches[0]) : ''; 126 | $end = substr($end, \strlen($type)); 127 | 128 | return [$type, $version, $end]; 129 | } 130 | 131 | /** 132 | * Normalize the stability. 133 | * 134 | * @param string $stability The stability. 135 | * 136 | * @return string The normalized stability. 137 | */ 138 | private static function normalizeStability(string $stability): string 139 | { 140 | $stability = strtolower($stability); 141 | 142 | return match ($stability) { 143 | 'a' => 'alpha', 144 | 'b', 'pre' => 'beta', 145 | 'build' => 'patch', 146 | 'rc' => 'RC', 147 | 'dev', 'snapshot' => 'dev', 148 | default => VersionParser::normalizeStability($stability), 149 | }; 150 | } 151 | 152 | /** 153 | * Match the version. 154 | * 155 | * @param string $version The version. 156 | * @param string $type The type of version. 157 | * 158 | * @return array The list of $version and $patchVersion. 159 | * 160 | * @psalm-return array{0: string, 1: bool} 161 | */ 162 | private static function matchVersion(string $version, string $type): array 163 | { 164 | $type = match ($type) { 165 | 'dev', 'snapshot' => 'dev', 166 | default => \in_array($type, ['alpha', 'beta', 'RC'], true) ? $type : 'patch', 167 | }; 168 | 169 | $patchVersion = !\in_array($type, ['dev'], true); 170 | 171 | $version .= $type; 172 | 173 | return [$version, $patchVersion]; 174 | } 175 | 176 | /** 177 | * Convert the minor version of date. 178 | * 179 | * @param string $minor The minor version. 180 | */ 181 | private static function convertDateMinorVersion(string $minor): string 182 | { 183 | $split = explode('.', $minor); 184 | $minor = (int) $split[0]; 185 | $revision = isset($split[1]) ? (int) $split[1] : 0; 186 | 187 | return '.' . sprintf('%03d', $minor) . sprintf('%03d', $revision); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Converter/VersionConverterInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Converter; 15 | 16 | /** 17 | * Interface for the converter for asset syntax version to composer syntax version. 18 | * 19 | * @author François Pluchino 20 | */ 21 | interface VersionConverterInterface 22 | { 23 | /** 24 | * Converts the asset version to composer version. 25 | * 26 | * @param string|null $version The asset version 27 | * 28 | * @return string The composer version 29 | */ 30 | public function convertVersion(string|null $version = null): string; 31 | } 32 | -------------------------------------------------------------------------------- /src/Event/AbstractSolveEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Event; 15 | 16 | use Composer\EventDispatcher\Event; 17 | use Composer\Package\PackageInterface; 18 | 19 | /** 20 | * Abstract event for solve event. 21 | * 22 | * @author François Pluchino 23 | */ 24 | abstract class AbstractSolveEvent extends Event 25 | { 26 | /** 27 | * @param string $name The event name. 28 | * @param string $assetDir The directory of mock assets. 29 | * @param array $packages All installed Composer packages. 30 | * 31 | * @psalm-param PackageInterface[] $packages All installed Composer packages. 32 | */ 33 | public function __construct(string $name, private readonly string $assetDir, private readonly array $packages = []) 34 | { 35 | parent::__construct($name, [], []); 36 | } 37 | 38 | /** 39 | * Get the directory of mock assets. 40 | */ 41 | public function getAssetDir(): string 42 | { 43 | return $this->assetDir; 44 | } 45 | 46 | /** 47 | * Get the installed Composer packages. 48 | * 49 | * @psalm-return PackageInterface[] All installed Composer packages. 50 | */ 51 | public function getPackages(): array 52 | { 53 | return $this->packages; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Event/GetAssetsEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Event; 15 | 16 | use Composer\Package\PackageInterface; 17 | use Foxy\FoxyEvents; 18 | 19 | /** 20 | * Get assets event. 21 | * 22 | * @author François Pluchino 23 | */ 24 | final class GetAssetsEvent extends AbstractSolveEvent 25 | { 26 | /** 27 | * @param string $assetDir The directory of mock assets. 28 | * @param array $packages All installed Composer packages. 29 | * @param array $assets The map of asset package name and the asset package path. 30 | * 31 | * @psalm-param PackageInterface[] $packages All installed Composer packages. 32 | */ 33 | public function __construct(string $assetDir, array $packages, private array $assets = []) 34 | { 35 | parent::__construct(FoxyEvents::GET_ASSETS, $assetDir, $packages); 36 | 37 | $this->assets = $assets; 38 | } 39 | 40 | /** 41 | * Check if the asset package is present. 42 | * 43 | * @param string $name The asset package name 44 | */ 45 | public function hasAsset(string $name): bool 46 | { 47 | return isset($this->assets[$name]); 48 | } 49 | 50 | /** 51 | * Add the asset package. 52 | * 53 | * @param string $name The asset package name. 54 | * @param string $path The asset package path (relative path form root project and started with `file:`). 55 | * 56 | * Example: 57 | * 58 | * For the Composer package `foo/bar`. 59 | * 60 | * $event->addAsset('@composer-asset/foo--bar', 'file:./vendor/foxy/composer-asset/foo/bar'); 61 | */ 62 | public function addAsset(string $name, string $path): self 63 | { 64 | $this->assets[$name] = $path; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Get the map of asset package name and the asset package path. 71 | */ 72 | public function getAssets(): array 73 | { 74 | return $this->assets; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Event/PostSolveEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Event; 15 | 16 | use Composer\Package\PackageInterface; 17 | use Foxy\FoxyEvents; 18 | 19 | /** 20 | * Post solve event. 21 | * 22 | * @author François Pluchino 23 | */ 24 | final class PostSolveEvent extends AbstractSolveEvent 25 | { 26 | /** 27 | * @param string $assetDir The directory of mock assets. 28 | * @param array $packages All installed Composer packages. 29 | * @param int $runResult The process result of asset manager execution. 30 | * 31 | * @psalm-param PackageInterface[] $packages All installed Composer packages. 32 | */ 33 | public function __construct($assetDir, array $packages, private readonly int $runResult) 34 | { 35 | parent::__construct(FoxyEvents::POST_SOLVE, $assetDir, $packages); 36 | } 37 | 38 | /** 39 | * Get the process result of asset manager execution. 40 | */ 41 | public function getRunResult(): int 42 | { 43 | return $this->runResult; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Event/PreSolveEvent.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Event; 15 | 16 | use Composer\Package\PackageInterface; 17 | use Foxy\FoxyEvents; 18 | 19 | /** 20 | * Pre solve event. 21 | * 22 | * @author François Pluchino 23 | */ 24 | final class PreSolveEvent extends AbstractSolveEvent 25 | { 26 | /** 27 | * @param string $assetDir The directory of mock assets. 28 | * @param array $packages All installed Composer packages. 29 | * 30 | * @psalm-param PackageInterface[] $packages All installed Composer packages. 31 | */ 32 | public function __construct(string $assetDir, array $packages = []) 33 | { 34 | parent::__construct(FoxyEvents::PRE_SOLVE, $assetDir, $packages); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Exception; 15 | 16 | /** 17 | * The interface of exception. 18 | * 19 | * @author François Pluchino 20 | */ 21 | interface ExceptionInterface 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Exception; 15 | 16 | /** 17 | * The Runtime Exception. 18 | * 19 | * @author François Pluchino 20 | */ 21 | class RuntimeException extends \RuntimeException implements ExceptionInterface 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /src/Fallback/AssetFallback.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Fallback; 15 | 16 | use Composer\IO\IOInterface; 17 | use Composer\Util\Filesystem; 18 | use Foxy\Config\Config; 19 | 20 | /** 21 | * Asset fallback. 22 | * 23 | * @author François Pluchino 24 | */ 25 | final class AssetFallback implements FallbackInterface 26 | { 27 | protected Filesystem $fs; 28 | protected string|null $originalContent = null; 29 | 30 | public function __construct( 31 | protected IOInterface $io, 32 | protected Config $config, 33 | protected string $path, 34 | Filesystem|null $fs = null 35 | ) { 36 | $this->fs = $fs ?: new Filesystem(); 37 | } 38 | 39 | public function save(): self 40 | { 41 | if (file_exists($this->path) && is_file($this->path)) { 42 | $this->originalContent = file_get_contents($this->path); 43 | } 44 | 45 | return $this; 46 | } 47 | 48 | public function restore(): void 49 | { 50 | if (!$this->config->get('fallback-asset')) { 51 | return; 52 | } 53 | 54 | $this->io->write('Fallback to previous state for the Asset package'); 55 | $this->fs->remove($this->path); 56 | 57 | if (null !== $this->originalContent) { 58 | file_put_contents($this->path, $this->originalContent); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Fallback/ComposerFallback.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Fallback; 15 | 16 | use Composer\Composer; 17 | use Composer\Factory; 18 | use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; 19 | use Composer\Installer; 20 | use Composer\IO\IOInterface; 21 | use Composer\Util\Filesystem; 22 | use Foxy\Config\Config; 23 | use Foxy\Util\ConsoleUtil; 24 | use Foxy\Util\LockerUtil; 25 | use Foxy\Util\PackageUtil; 26 | use Symfony\Component\Console\Input\InputInterface; 27 | 28 | /** 29 | * Composer fallback. 30 | * 31 | * @author François Pluchino 32 | */ 33 | final class ComposerFallback implements FallbackInterface 34 | { 35 | protected Filesystem $fs; 36 | protected array $lock = []; 37 | 38 | /** 39 | * @param Composer $composer The composer. 40 | * @param IOInterface $io The IO. 41 | * @param Config $config The config. 42 | * @param InputInterface $input The input. 43 | * @param Filesystem|null $fs The composer filesystem. 44 | * @param Installer|null $installer The installer. 45 | */ 46 | public function __construct( 47 | protected Composer $composer, 48 | protected IOInterface $io, 49 | protected Config $config, 50 | protected InputInterface $input, 51 | Filesystem|null $fs = null, 52 | protected Installer|null $installer = null 53 | ) { 54 | $this->fs = $fs ?: new Filesystem(); 55 | } 56 | 57 | public function save(): self 58 | { 59 | $rm = $this->composer->getRepositoryManager(); 60 | $im = $this->composer->getInstallationManager(); 61 | $composerFile = Factory::getComposerFile(); 62 | $locker = LockerUtil::getLocker($this->io, $im, $composerFile); 63 | 64 | try { 65 | $lock = $locker->getLockData(); 66 | $this->lock = PackageUtil::loadLockPackages($lock); 67 | } catch (\LogicException) { 68 | $this->lock = []; 69 | } 70 | 71 | return $this; 72 | } 73 | 74 | public function restore(): void 75 | { 76 | if (!$this->config->get('fallback-composer')) { 77 | return; 78 | } 79 | 80 | $this->io->write('Fallback to previous state for Composer'); 81 | $hasLock = $this->restoreLockData(); 82 | 83 | if ($hasLock) { 84 | $this->restorePreviousLockFile(); 85 | } else { 86 | /** @var string $vendorDir */ 87 | $vendorDir = $this->composer->getConfig()->get('vendor-dir'); 88 | 89 | $this->fs->remove($vendorDir); 90 | } 91 | } 92 | 93 | /** 94 | * Restore the data of lock file. 95 | */ 96 | protected function restoreLockData(): bool 97 | { 98 | /** @psalm-suppress MixedArgument */ 99 | $this->composer->getLocker()->setLockData( 100 | $this->getLockValue('packages', []), 101 | $this->getLockValue('packages-dev'), 102 | $this->getLockValue('platform', []), 103 | $this->getLockValue('platform-dev', []), 104 | $this->getLockValue('aliases', []), 105 | $this->getLockValue('minimum-stability', ''), 106 | $this->getLockValue('stability-flags', []), 107 | $this->getLockValue('prefer-stable', false), 108 | $this->getLockValue('prefer-lowest', false), 109 | $this->getLockValue('platform-overrides', []) 110 | ); 111 | 112 | $isLocked = $this->composer->getLocker()->isLocked(); 113 | $lockData = $isLocked ? $this->composer->getLocker()->getLockData() : null; 114 | $hasPackage = \is_array($lockData) && isset($lockData['packages']) && !empty($lockData['packages']); 115 | 116 | return $isLocked && $hasPackage; 117 | } 118 | 119 | /** 120 | * Restore the PHP dependencies with the previous lock file. 121 | */ 122 | protected function restorePreviousLockFile(): void 123 | { 124 | $config = $this->composer->getConfig(); 125 | [$preferSource, $preferDist] = ConsoleUtil::getPreferredInstallOptions($config, $this->input); 126 | $optimize = $this->input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); 127 | $authoritative = $this->input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); 128 | $apcu = $this->input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); 129 | $dispatcher = $this->composer->getEventDispatcher(); 130 | /** @var bool $verbose */ 131 | $verbose = $this->input->getOption('verbose'); 132 | 133 | $installer = $this->getInstaller() 134 | ->setVerbose($verbose) 135 | ->setPreferSource($preferSource) 136 | ->setPreferDist($preferDist) 137 | ->setDevMode(!$this->input->getOption('no-dev')) 138 | ->setDumpAutoloader(!$this->input->getOption('no-autoloader')) 139 | ->setOptimizeAutoloader($optimize) 140 | ->setClassMapAuthoritative($authoritative) 141 | ->setApcuAutoloader($apcu); 142 | 143 | $ignorePlatformReqs = $this->input->getOption('ignore-platform-reqs') ?: ($this->input->getOption('ignore-platform-req') ?: false); 144 | $installer->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); 145 | $dispatcher->setRunScripts(false); 146 | 147 | $installer->run(); 148 | 149 | $dispatcher->setRunScripts(!$this->input->getOption('no-scripts')); 150 | } 151 | 152 | /** 153 | * Get the lock value. 154 | * 155 | * @param string $key The key. 156 | * @param mixed $default The default value. 157 | */ 158 | private function getLockValue(string $key, mixed $default = null): mixed 159 | { 160 | return $this->lock[$key] ?? $default; 161 | } 162 | 163 | /** 164 | * Get the installer. 165 | */ 166 | private function getInstaller(): Installer 167 | { 168 | return $this->installer ?? Installer::create($this->io, $this->composer); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Fallback/FallbackInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Fallback; 15 | 16 | /** 17 | * Interface of fallback. 18 | * 19 | * @author François Pluchino 20 | */ 21 | interface FallbackInterface 22 | { 23 | /** 24 | * Save the state. 25 | */ 26 | public function save(): self; 27 | 28 | /** 29 | * Restore the state. 30 | */ 31 | public function restore(): void; 32 | } 33 | -------------------------------------------------------------------------------- /src/Foxy.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy; 15 | 16 | use Composer\Composer; 17 | use Composer\DependencyResolver\Operation\InstallOperation; 18 | use Composer\EventDispatcher\EventSubscriberInterface; 19 | use Composer\Installer\PackageEvent; 20 | use Composer\Installer\PackageEvents; 21 | use Composer\IO\IOInterface; 22 | use Composer\Plugin\PluginInterface; 23 | use Composer\Script\Event; 24 | use Composer\Script\ScriptEvents; 25 | use Composer\Util\Filesystem; 26 | use Composer\Util\ProcessExecutor; 27 | use Foxy\Asset\AssetManagerFinder; 28 | use Foxy\Asset\AssetManagerInterface; 29 | use Foxy\Config\Config; 30 | use Foxy\Config\ConfigBuilder; 31 | use Foxy\Exception\RuntimeException; 32 | use Foxy\Fallback\AssetFallback; 33 | use Foxy\Fallback\ComposerFallback; 34 | use Foxy\Solver\Solver; 35 | use Foxy\Solver\SolverInterface; 36 | use Foxy\Util\ComposerUtil; 37 | use Foxy\Util\ConsoleUtil; 38 | 39 | /** 40 | * Composer plugin. 41 | * 42 | * @author François Pluchino 43 | * 44 | * @psalm-suppress MissingConstructor 45 | */ 46 | final class Foxy implements PluginInterface, EventSubscriberInterface 47 | { 48 | final public const REQUIRED_COMPOSER_VERSION = '^2.0.0'; 49 | private Config $config; 50 | private AssetManagerInterface $assetManager; 51 | private AssetFallback $assetFallback; 52 | private ComposerFallback $composerFallback; 53 | private SolverInterface $solver; 54 | private bool $initialized = false; 55 | 56 | /** 57 | * The list of the classes of asset managers. 58 | * 59 | * @psalm-var list> 60 | */ 61 | private static $assetManagers = [ 62 | Asset\BunManager::class, 63 | Asset\NpmManager::class, 64 | Asset\PnpmManager::class, 65 | Asset\YarnManager::class, 66 | ]; 67 | 68 | /** 69 | * The default values of config. 70 | */ 71 | private static array $defaultConfig = [ 72 | 'enabled' => true, 73 | 'manager' => null, 74 | 'manager-version' => [ 75 | 'bun' => '>=1.1.0', 76 | 'npm' => '>=5.0.0', 77 | 'pnpm' => '>=7.0.0', 78 | 'yarn' => '>=1.0.0', 79 | ], 80 | 'manager-bin' => null, 81 | 'manager-options' => null, 82 | 'manager-install-options' => null, 83 | 'manager-update-options' => null, 84 | 'manager-timeout' => null, 85 | 'composer-asset-dir' => null, 86 | 'run-asset-manager' => true, 87 | 'fallback-asset' => true, 88 | 'fallback-composer' => true, 89 | 'enable-packages' => [], 90 | ]; 91 | 92 | public static function getSubscribedEvents(): array 93 | { 94 | return [ 95 | ComposerUtil::getInitEventName() => [['init', 100]], 96 | PackageEvents::POST_PACKAGE_INSTALL => [['initOnInstall', 100]], 97 | ScriptEvents::POST_INSTALL_CMD => [['solveAssets', 100]], 98 | ScriptEvents::POST_UPDATE_CMD => [['solveAssets', 100]], 99 | ]; 100 | } 101 | 102 | public function activate(Composer $composer, IOInterface $io): void 103 | { 104 | ComposerUtil::validateVersion(self::REQUIRED_COMPOSER_VERSION, Composer::VERSION); 105 | 106 | $input = ConsoleUtil::getInput($io); 107 | $executor = new ProcessExecutor($io); 108 | $fs = new Filesystem($executor); 109 | 110 | $this->config = ConfigBuilder::build($composer, self::$defaultConfig, $io); 111 | $this->assetManager = $this->getAssetManager($io, $this->config, $executor, $fs); 112 | $this->assetFallback = new AssetFallback($io, $this->config, $this->assetManager->getPackageName(), $fs); 113 | $this->composerFallback = new ComposerFallback($composer, $io, $this->config, $input, $fs); 114 | $this->solver = new Solver($this->assetManager, $this->config, $fs, $this->composerFallback); 115 | 116 | $this->assetManager->setFallback($this->assetFallback); 117 | } 118 | 119 | public function deactivate(Composer $composer, IOInterface $io): void 120 | { 121 | // Do nothing 122 | } 123 | 124 | public function uninstall(Composer $composer, IOInterface $io): void 125 | { 126 | // Do nothing 127 | } 128 | 129 | /** 130 | * Init the plugin just after the first installation. 131 | * 132 | * @param PackageEvent $event The package event 133 | */ 134 | public function initOnInstall(PackageEvent $event): void 135 | { 136 | $operation = $event->getOperation(); 137 | 138 | if ($operation instanceof InstallOperation && 'php-forge/foxy' === $operation->getPackage()->getName()) { 139 | $this->init(); 140 | } 141 | } 142 | 143 | /** 144 | * Init the plugin. 145 | */ 146 | public function init(): void 147 | { 148 | if (!$this->initialized) { 149 | $this->initialized = true; 150 | $this->assetFallback->save(); 151 | $this->composerFallback->save(); 152 | 153 | if ($this->config->get('enabled')) { 154 | $this->assetManager->validate(); 155 | } 156 | } 157 | } 158 | 159 | /** 160 | * Set the solver. 161 | * 162 | * @param SolverInterface $solver The solver instance. 163 | */ 164 | public function setSolver(SolverInterface $solver): void 165 | { 166 | $this->solver = $solver; 167 | } 168 | 169 | /** 170 | * Solve the assets. 171 | * 172 | * @param Event $event The composer script event. 173 | */ 174 | public function solveAssets(Event $event): void 175 | { 176 | $this->solver->setUpdatable(str_contains($event->getName(), 'update')); 177 | $this->solver->solve($event->getComposer(), $event->getIO()); 178 | } 179 | 180 | /** 181 | * Get the asset manager. 182 | * 183 | * @param IOInterface $io The IO interface. 184 | * @param Config $config The config of plugin. 185 | * @param ProcessExecutor $executor The process executor. 186 | * @param Filesystem $fs The composer filesystem. 187 | * 188 | * @throws RuntimeException When the asset manager is not found. 189 | */ 190 | protected function getAssetManager( 191 | IOInterface $io, 192 | Config $config, 193 | ProcessExecutor $executor, 194 | Filesystem $fs 195 | ): AssetManagerInterface { 196 | $amf = new AssetManagerFinder(); 197 | 198 | foreach (self::$assetManagers as $class) { 199 | $amf->addManager(new $class($io, $config, $executor, $fs)); 200 | } 201 | 202 | /** @var string|null $manager */ 203 | $manager = $config->get('manager'); 204 | 205 | return $amf->findManager($manager); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/FoxyEvents.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy; 15 | 16 | /** 17 | * Events of Foxy. 18 | * 19 | * @author François Pluchino 20 | */ 21 | abstract class FoxyEvents 22 | { 23 | /** 24 | * The "PRE_SOLVE" event is triggered before the `solve` action of asset packages. 25 | * 26 | * @Event("Foxy\Event\PreSolveEvent") 27 | */ 28 | final public const PRE_SOLVE = 'foxy.pre-solve'; 29 | 30 | /** 31 | * The "GET_ASSETS" event is triggered before the `solve` action of asset packages 32 | * and during the retrieves the map of the asset packages. 33 | * 34 | * @Event("Foxy\Event\GetAssetsEvent") 35 | */ 36 | final public const GET_ASSETS = 'foxy.get-assets'; 37 | 38 | /** 39 | * The "POST_SOLVE" event is triggered after the `solve` action of asset packages and before 40 | * the execution of the composer's fallback. 41 | * 42 | * @Event("Foxy\Event\PostSolveEvent") 43 | */ 44 | final public const POST_SOLVE = 'foxy.post-solve'; 45 | } 46 | -------------------------------------------------------------------------------- /src/Json/JsonFile.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Json; 15 | 16 | /** 17 | * The JSON file. 18 | * 19 | * @author François Pluchino 20 | */ 21 | final class JsonFile extends \Composer\Json\JsonFile 22 | { 23 | /** 24 | * @psalm-var string[] 25 | */ 26 | private array $arrayKeys = []; 27 | private int|null $indent = null; 28 | /** 29 | * @psalm-var string[] 30 | */ 31 | private static array $encodeArrayKeys = []; 32 | private static int $encodeIndent = JsonFormatter::DEFAULT_INDENT; 33 | 34 | /** 35 | * Get the list of keys to be retained with an array representation if they are empty. 36 | * 37 | * @psalm-return string[] 38 | */ 39 | public function getArrayKeys(): array 40 | { 41 | if ($this->arrayKeys === []) { 42 | $this->parseOriginalContent(); 43 | } 44 | 45 | return $this->arrayKeys; 46 | } 47 | 48 | /** 49 | * Get the indent for this json file. 50 | */ 51 | public function getIndent(): int 52 | { 53 | if ($this->indent === null) { 54 | $this->parseOriginalContent(); 55 | } 56 | 57 | return $this->indent ?? JsonFormatter::DEFAULT_INDENT; 58 | } 59 | 60 | public function read(): array 61 | { 62 | $data = parent::read(); 63 | 64 | $this->getArrayKeys(); 65 | $this->getIndent(); 66 | 67 | return is_array($data) ? $data : []; 68 | } 69 | 70 | public function write(array $hash, int $options = 448): void 71 | { 72 | self::$encodeArrayKeys = $this->getArrayKeys(); 73 | self::$encodeIndent = $this->getIndent(); 74 | parent::write($hash, $options); 75 | self::$encodeArrayKeys = []; 76 | self::$encodeIndent = 4; 77 | } 78 | 79 | public static function encode(mixed $data, int $options = 448, string $indent = self::INDENT_DEFAULT): string 80 | { 81 | $result = parent::encode($data, $options, $indent); 82 | 83 | return JsonFormatter::format($result, self::$encodeArrayKeys, self::$encodeIndent, false); 84 | } 85 | 86 | /** 87 | * Parse the original content. 88 | */ 89 | private function parseOriginalContent(): void 90 | { 91 | $content = $this->exists() ? file_get_contents($this->getPath()) : ''; 92 | $this->arrayKeys = JsonFormatter::getArrayKeys($content); 93 | $this->indent = JsonFormatter::getIndent($content); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Json/JsonFormatter.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Json; 15 | 16 | /** 17 | * Formats JSON strings with a custom indent. 18 | * 19 | * @author François Pluchino 20 | */ 21 | final class JsonFormatter 22 | { 23 | public const DEFAULT_INDENT = 4; 24 | public const ARRAY_KEYS_REGEX = '/["\']([\w\d_\-.]+)["\']:\s\[]/'; 25 | public const INDENT_REGEX = '/^[{\[][\r\n]([ ]+)["\']/'; 26 | 27 | /** 28 | * Get the list of keys to be retained with an array representation if they are empty. 29 | * 30 | * @param string $content The content. 31 | * 32 | * @psalm-return string[] The list of keys to be retained with an array representation if they are empty. 33 | */ 34 | public static function getArrayKeys(string $content): array 35 | { 36 | preg_match_all(self::ARRAY_KEYS_REGEX, trim($content), $matches); 37 | 38 | return !empty($matches) ? $matches[1] : []; 39 | } 40 | 41 | /** 42 | * Get the indent of file. 43 | * 44 | * @param string $content The content 45 | */ 46 | public static function getIndent(string $content): int 47 | { 48 | $indent = self::DEFAULT_INDENT; 49 | \preg_match(self::INDENT_REGEX, \trim($content), $matches); 50 | 51 | if (!empty($matches)) { 52 | $indent = \strlen($matches[1]); 53 | } 54 | 55 | return $indent; 56 | } 57 | 58 | /** 59 | * Format the data in JSON. 60 | * 61 | * @param string $json The original JSON. 62 | * @param array $arrayKeys The list of keys to be retained with an array representation if they are empty. 63 | * @param int $indent The space count for indent. 64 | * @param bool $formatJson Check if the json must be formatted. 65 | * 66 | * @psalm-param string[] $arrayKeys The list of keys to be retained with an array representation if they are empty. 67 | */ 68 | public static function format( 69 | string $json, 70 | array $arrayKeys = [], 71 | $indent = self::DEFAULT_INDENT, 72 | $formatJson = true 73 | ): string { 74 | if ($formatJson) { 75 | $json = self::formatInternal($json, true, true); 76 | } 77 | 78 | if (4 !== $indent) { 79 | $json = \str_replace(' ', \str_repeat(' ', $indent), $json); 80 | } 81 | 82 | return self::replaceArrayByMap($json, $arrayKeys); 83 | } 84 | 85 | /** 86 | * Format the data in JSON. 87 | * 88 | * @param bool $unescapeUnicode Un escape unicode. 89 | * @param bool $unescapeSlashes Un escape slashes. 90 | */ 91 | private static function formatInternal(string $json, bool $unescapeUnicode, bool $unescapeSlashes): string 92 | { 93 | $array = \json_decode($json, true); 94 | 95 | if (!is_array($array)) { 96 | return $json; 97 | } 98 | 99 | if ($unescapeUnicode) { 100 | \array_walk_recursive($array, function (mixed &$item): void { 101 | if (\is_string($item)) { 102 | $item = \preg_replace_callback( 103 | '/\\\\u([0-9a-fA-F]{4})/', 104 | static function (mixed $match) { 105 | $result = \mb_convert_encoding(\pack('H*', $match[1]), 'UTF-8', 'UCS-2BE'); 106 | return $result !== false ? $result : ''; 107 | }, 108 | $item, 109 | ); 110 | } 111 | }); 112 | } 113 | 114 | if ($unescapeSlashes) { 115 | \array_walk_recursive($array, function (mixed &$item): void { 116 | if (\is_string($item)) { 117 | $item = \str_replace('\\/', '/', $item); 118 | } 119 | }); 120 | } 121 | 122 | return \json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 123 | } 124 | 125 | /** 126 | * Replace the empty array by empty map. 127 | * 128 | * @param string $json The original JSON. 129 | * @param array $arrayKeys The list of keys to be retained with an array representation if they are empty. 130 | * 131 | * @psalm-param string[] $arrayKeys The list of keys to be retained with an array representation if they are empty. 132 | */ 133 | private static function replaceArrayByMap(string $json, array $arrayKeys): string 134 | { 135 | \preg_match_all(self::ARRAY_KEYS_REGEX, $json, $matches, PREG_SET_ORDER); 136 | 137 | foreach ($matches as $match) { 138 | if (!\in_array($match[1], $arrayKeys, true)) { 139 | $replace = \str_replace('[]', '{}', $match[0]); 140 | $json = \str_replace($match[0], $replace, $json); 141 | } 142 | } 143 | 144 | return $json; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Solver/Solver.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Solver; 15 | 16 | use Composer\Composer; 17 | use Composer\IO\IOInterface; 18 | use Composer\Json\JsonFile; 19 | use Composer\Package\PackageInterface; 20 | use Composer\Util\Filesystem; 21 | use Foxy\Asset\AssetManagerInterface; 22 | use Foxy\Config\Config; 23 | use Foxy\Event\GetAssetsEvent; 24 | use Foxy\Event\PostSolveEvent; 25 | use Foxy\Event\PreSolveEvent; 26 | use Foxy\Fallback\FallbackInterface; 27 | use Foxy\FoxyEvents; 28 | use Foxy\Util\AssetUtil; 29 | 30 | /** 31 | * Solver of asset dependencies. 32 | * 33 | * @author François Pluchino 34 | */ 35 | final class Solver implements SolverInterface 36 | { 37 | /** 38 | * @param AssetManagerInterface $assetManager The asset manager instance. 39 | * @param Config $config The config instance. 40 | * @param FallbackInterface|null $composerFallback The composer fallback instance. 41 | */ 42 | public function __construct( 43 | protected AssetManagerInterface $assetManager, 44 | protected Config $config, 45 | protected Filesystem $fs, 46 | protected FallbackInterface|null $composerFallback = null 47 | ) { 48 | } 49 | 50 | public function setUpdatable($updatable): self 51 | { 52 | $this->assetManager->setUpdatable($updatable); 53 | 54 | return $this; 55 | } 56 | 57 | public function solve(Composer $composer, IOInterface $io): void 58 | { 59 | if (!$this->config->get('enabled')) { 60 | return; 61 | } 62 | 63 | $dispatcher = $composer->getEventDispatcher(); 64 | $packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages(); 65 | /** @var string $vendorDir */ 66 | $vendorDir = $composer->getConfig()->get('vendor-dir'); 67 | /** @var string $assetDir */ 68 | $assetDir = $this->config->get('composer-asset-dir', $vendorDir . '/php-forge/composer-asset/'); 69 | $dispatcher->dispatch(FoxyEvents::PRE_SOLVE, new PreSolveEvent($assetDir, $packages)); 70 | $this->fs->remove($assetDir); 71 | $assets = $this->getAssets($composer, $assetDir, $packages); 72 | $this->assetManager->addDependencies($composer->getPackage(), $assets); 73 | $res = $this->assetManager->run(); 74 | $dispatcher->dispatch(FoxyEvents::POST_SOLVE, new PostSolveEvent($assetDir, $packages, $res)); 75 | 76 | if ($res > 0 && $this->composerFallback) { 77 | $this->composerFallback->restore(); 78 | 79 | throw new \RuntimeException('The asset manager ended with an error'); 80 | } 81 | } 82 | 83 | /** 84 | * Get the package of asset dependencies. 85 | * 86 | * @param Composer $composer The composer instance. 87 | * @param string $assetDir The asset directory. 88 | * @param array $packages The package dependencies. 89 | * 90 | * @psalm-param PackageInterface[] $packages The package dependencies. 91 | */ 92 | protected function getAssets(Composer $composer, string $assetDir, array $packages): array 93 | { 94 | $installationManager = $composer->getInstallationManager(); 95 | $configPackages = $this->config->getArray('enable-packages'); 96 | $assets = []; 97 | 98 | foreach ($packages as $package) { 99 | $filename = AssetUtil::getPath($installationManager, $this->assetManager, $package, $configPackages); 100 | 101 | if (null !== $filename) { 102 | [$packageName, $packagePath] = $this->getMockPackagePath($package, $assetDir, $filename); 103 | $assets[$packageName] = $packagePath; 104 | } 105 | } 106 | 107 | $assetsEvent = new GetAssetsEvent($assetDir, $packages, $assets); 108 | $composer->getEventDispatcher()->dispatch(FoxyEvents::GET_ASSETS, $assetsEvent); 109 | 110 | return $assetsEvent->getAssets(); 111 | } 112 | 113 | /** 114 | * Get the path of the mock package. 115 | * 116 | * @param PackageInterface $package The package dependency, 117 | * @param string $assetDir The asset directory. 118 | * @param string $filename The filename of asset package. 119 | * 120 | * @psalm-return string[] The package name and the relative package path from the current directory 121 | */ 122 | protected function getMockPackagePath(PackageInterface $package, string $assetDir, string $filename): array 123 | { 124 | $packageName = AssetUtil::getName($package); 125 | $packagePath = \rtrim($assetDir, '/') . '/' . $package->getName(); 126 | $newFilename = $packagePath . '/' . \basename($filename); 127 | 128 | \mkdir($packagePath, 0777, true); 129 | \copy($filename, $newFilename); 130 | 131 | $jsonFile = new JsonFile($newFilename); 132 | $packageValue = AssetUtil::formatPackage($package, $packageName, (array) $jsonFile->read()); 133 | 134 | $jsonFile->write($packageValue); 135 | 136 | return [$packageName, $this->fs->findShortestPath(getcwd(), $newFilename)]; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Solver/SolverInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Solver; 15 | 16 | use Composer\Composer; 17 | use Composer\IO\IOInterface; 18 | 19 | /** 20 | * Interface of solver. 21 | * 22 | * @author François Pluchino 23 | */ 24 | interface SolverInterface 25 | { 26 | /** 27 | * Define if the update action can be used. 28 | * 29 | * @param bool $updatable The value of updatable. 30 | */ 31 | public function setUpdatable(bool $updatable): self; 32 | 33 | /** 34 | * Solve the asset dependencies. 35 | * 36 | * @param Composer $composer The composer instance. 37 | * @param IOInterface $io The IO instance. 38 | */ 39 | public function solve(Composer $composer, IOInterface $io): void; 40 | } 41 | -------------------------------------------------------------------------------- /src/Util/AssetUtil.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Util; 15 | 16 | use Composer\Installer\InstallationManager; 17 | use Composer\Package\Link; 18 | use Composer\Package\PackageInterface; 19 | use Foxy\Asset\AssetManagerInterface; 20 | use Foxy\Asset\AssetPackage; 21 | 22 | /** 23 | * Helper for Foxy. 24 | * 25 | * @author François Pluchino 26 | */ 27 | final class AssetUtil 28 | { 29 | /** 30 | * Get the name for the asset dependency. 31 | * 32 | * @param PackageInterface $package The package instance. 33 | */ 34 | public static function getName(PackageInterface $package): string 35 | { 36 | return AssetPackage::COMPOSER_PREFIX . \str_replace(['/'], '--', $package->getName()); 37 | } 38 | 39 | /** 40 | * Get the path of asset file. 41 | * 42 | * @param InstallationManager $installationManager The installation manager. 43 | * @param AssetManagerInterface $assetManager The asset manager. 44 | * @param PackageInterface $package The package instance. 45 | * @param array $configPackages The packages defined in config. 46 | */ 47 | public static function getPath( 48 | InstallationManager $installationManager, 49 | AssetManagerInterface $assetManager, 50 | PackageInterface $package, 51 | array $configPackages = [] 52 | ): string|null { 53 | $path = null; 54 | 55 | 56 | if (self::isAsset($package, $configPackages)) { 57 | $composerJsonPath = null; 58 | $installPath = $installationManager->getInstallPath($package); 59 | 60 | if (null !== $installPath) { 61 | $composerJsonPath = $installPath . '/composer.json'; 62 | } 63 | 64 | if (null !== $composerJsonPath && \file_exists($composerJsonPath)) { 65 | /** @var array[] $composerJson */ 66 | $composerJson = \json_decode(\file_get_contents($composerJsonPath), true); 67 | $rootPackageDir = $composerJson['config']['foxy']['root-package-json-dir'] ?? null; 68 | 69 | if (null !== $installPath && \is_string($rootPackageDir)) { 70 | $installPath .= '/' . $rootPackageDir; 71 | } 72 | } 73 | 74 | 75 | if (null !== $installPath) { 76 | $filename = $installPath . '/' . $assetManager->getPackageName(); 77 | $path = \file_exists($filename) ? \str_replace('\\', '/', \realpath($filename)) : null; 78 | } 79 | } 80 | 81 | return $path; 82 | } 83 | 84 | /** 85 | * Check if the package is available for Foxy. 86 | * 87 | * @param PackageInterface $package The package instance. 88 | * @param array $configPackages The packages defined in config. 89 | */ 90 | public static function isAsset(PackageInterface $package, array $configPackages = []): bool 91 | { 92 | $projectConfig = self::getProjectActivation($package, $configPackages); 93 | $enabled = false !== $projectConfig; 94 | 95 | return $enabled && (self::hasExtraActivation($package) 96 | || self::hasPluginDependency($package->getRequires()) 97 | || self::hasPluginDependency($package->getDevRequires()) 98 | || true === $projectConfig); 99 | } 100 | 101 | /** 102 | * Check if foxy is enabled in extra section of package. 103 | * 104 | * @param PackageInterface $package The package instance. 105 | */ 106 | public static function hasExtraActivation(PackageInterface $package): bool 107 | { 108 | $extra = $package->getExtra(); 109 | 110 | return isset($extra['foxy']) && true === $extra['foxy']; 111 | } 112 | 113 | /** 114 | * Check if the package contains assets. 115 | * 116 | * @param Link[] $requires The require links. 117 | * 118 | * @psalm-param Link[] $requires The require links. 119 | */ 120 | public static function hasPluginDependency(array $requires): bool 121 | { 122 | $assets = false; 123 | 124 | foreach ($requires as $require) { 125 | if ('php-forge/foxy' === $require->getTarget()) { 126 | $assets = true; 127 | 128 | break; 129 | } 130 | } 131 | 132 | return $assets; 133 | } 134 | 135 | /** 136 | * Check if the package is enabled by the project config. 137 | * 138 | * @param PackageInterface $package The package instance. 139 | * @param array $configPackages The packages defined in config. 140 | */ 141 | public static function isProjectActivation(PackageInterface $package, array $configPackages): bool 142 | { 143 | return true === self::getProjectActivation($package, $configPackages); 144 | } 145 | 146 | /** 147 | * Format the asset package. 148 | * 149 | * @param PackageInterface $package The composer package instance. 150 | * @param string $packageName The package name of asset. 151 | * @param array $packageValue The package value of asset. 152 | */ 153 | public static function formatPackage(PackageInterface $package, string $packageName, array $packageValue): array 154 | { 155 | $packageValue['name'] = $packageName; 156 | 157 | if (!isset($packageValue['version'])) { 158 | $extra = $package->getExtra(); 159 | $version = $package->getPrettyVersion(); 160 | 161 | if (str_starts_with($version, 'dev-') && isset($extra['branch-alias'][$version])) { 162 | $version = $extra['branch-alias'][$version]; 163 | } 164 | 165 | $packageValue['version'] = self::formatVersion(\str_replace('-dev', '', (string) $version)); 166 | } 167 | 168 | return $packageValue; 169 | } 170 | 171 | /** 172 | * Format the version for the asset package. 173 | * 174 | * @param string $version The branch alias version. 175 | */ 176 | private static function formatVersion(string $version): string 177 | { 178 | $version = \str_replace(['x', 'X', '*'], '0', $version); 179 | $exp = \explode('.', $version); 180 | 181 | if (($size = \count($exp)) < 3) { 182 | for ($i = $size; $i < 3; ++$i) { 183 | $exp[] = '0'; 184 | } 185 | } 186 | 187 | return $exp[0] . '.' . $exp[1] . '.' . $exp[2]; 188 | } 189 | 190 | /** 191 | * Get the activation of the package defined in the project config. 192 | * 193 | * @param PackageInterface $package The package instance. 194 | * @param array $configPackages The packages defined in config. 195 | * 196 | * @return bool|null returns NULL, if the package isn't defined in the project config 197 | */ 198 | private static function getProjectActivation(PackageInterface $package, array $configPackages): bool|null 199 | { 200 | $name = $package->getName(); 201 | $value = null; 202 | 203 | /** 204 | * @var array $configPackages 205 | */ 206 | foreach ($configPackages as $pattern => $activation) { 207 | if (\is_int($pattern) && \is_string($activation)) { 208 | $pattern = $activation; 209 | $activation = true; 210 | } 211 | 212 | if ( 213 | \is_string($pattern) && 214 | ((str_starts_with($pattern, '/') && \preg_match($pattern, $name)) || \fnmatch($pattern, $name)) 215 | ) { 216 | $value = $activation; 217 | 218 | break; 219 | } 220 | } 221 | 222 | return is_bool($value) ? $value : null; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Util/ComposerUtil.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Util; 15 | 16 | use Composer\Installer\InstallerEvents; 17 | use Composer\Semver\Semver; 18 | use Foxy\Exception\RuntimeException; 19 | 20 | /** 21 | * Helper for Composer. 22 | * 23 | * @author François Pluchino 24 | */ 25 | final class ComposerUtil 26 | { 27 | /** 28 | * Get the event name to init the plugin. 29 | */ 30 | public static function getInitEventName(): string 31 | { 32 | return InstallerEvents::PRE_OPERATIONS_EXEC; 33 | } 34 | 35 | /** 36 | * Validate the composer version. 37 | * 38 | * @param string $requiredVersion The composer required version. 39 | * @param string $composerVersion The composer version. 40 | */ 41 | public static function validateVersion(string $requiredVersion, string $composerVersion): void 42 | { 43 | $isBranch = str_contains($composerVersion, '@'); 44 | $isSnapshot = (bool) preg_match('/^[0-9a-f]{40}$/i', $composerVersion); 45 | 46 | if (!$isBranch && !$isSnapshot && !Semver::satisfies($composerVersion, $requiredVersion)) { 47 | $msg = 'Foxy requires the Composer\'s minimum version "%s", current version is "%s"'; 48 | 49 | throw new RuntimeException(sprintf($msg, $requiredVersion, $composerVersion)); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Util/ConsoleUtil.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Util; 15 | 16 | use Composer\Config; 17 | use Composer\IO\IOInterface; 18 | use Symfony\Component\Console\Input\ArgvInput; 19 | use Symfony\Component\Console\Input\InputInterface; 20 | 21 | /** 22 | * Helper for console. 23 | * 24 | * @author François Pluchino 25 | */ 26 | final class ConsoleUtil 27 | { 28 | /** 29 | * Get the console input. 30 | * 31 | * @param IOInterface $io The IO 32 | */ 33 | public static function getInput(IOInterface $io): InputInterface 34 | { 35 | $ref = new \ReflectionClass($io); 36 | 37 | if ($ref->hasProperty('input')) { 38 | $prop = $ref->getProperty('input'); 39 | $prop->setAccessible(true); 40 | $input = $prop->getValue($io); 41 | 42 | if ($input instanceof InputInterface) { 43 | return $input; 44 | } 45 | } 46 | 47 | return new ArgvInput(); 48 | } 49 | 50 | /** 51 | * Returns preferSource and preferDist values based on the configuration. 52 | * 53 | * @param Config $config The composer config. 54 | * @param InputInterface $input The console input 55 | * 56 | * @psalm-return list{bool, bool} An array composed of the preferSource and preferDist values 57 | */ 58 | public static function getPreferredInstallOptions(Config $config, InputInterface $input): array 59 | { 60 | $preferSource = false; 61 | $preferDist = false; 62 | 63 | switch ($config->get('preferred-install')) { 64 | case 'source': 65 | $preferSource = true; 66 | 67 | break; 68 | 69 | case 'dist': 70 | $preferDist = true; 71 | 72 | break; 73 | 74 | case 'auto': 75 | default: 76 | break; 77 | } 78 | 79 | if ($input->getOption('prefer-source') || $input->getOption('prefer-dist')) { 80 | /** @var bool $preferSource */ 81 | $preferSource = $input->getOption('prefer-source'); 82 | /** @var bool $preferDist */ 83 | $preferDist = $input->getOption('prefer-dist'); 84 | } 85 | 86 | return [$preferSource, $preferDist]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Util/LockerUtil.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Util; 15 | 16 | use Composer\Installer\InstallationManager; 17 | use Composer\IO\IOInterface; 18 | use Composer\Json\JsonFile; 19 | use Composer\Package\Locker; 20 | 21 | /** 22 | * Helper for Locker. 23 | * 24 | * @author François Pluchino 25 | */ 26 | final class LockerUtil 27 | { 28 | /** 29 | * Get the locker. 30 | */ 31 | public static function getLocker( 32 | IOInterface $io, 33 | InstallationManager $im, 34 | string $composerFile 35 | ): Locker { 36 | $lockFile = str_replace('.json', '.lock', $composerFile); 37 | 38 | return new Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Util/PackageUtil.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Foxy\Util; 15 | 16 | use Composer\Package\AliasPackage; 17 | use Composer\Package\Loader\ArrayLoader; 18 | 19 | /** 20 | * Helper for package. 21 | * 22 | * @author François Pluchino 23 | */ 24 | final class PackageUtil 25 | { 26 | /** 27 | * Load all packages in the lock data of locker. 28 | * 29 | * @param array $lockData The lock data of locker. 30 | * 31 | * @return array The lock data. 32 | */ 33 | public static function loadLockPackages(array $lockData): array 34 | { 35 | $loader = new ArrayLoader(); 36 | $lockData = self::loadLockPackage($loader, $lockData); 37 | $lockData = self::loadLockPackage($loader, $lockData, true); 38 | return self::convertLockAlias($lockData); 39 | } 40 | 41 | /** 42 | * Load the packages in the packages section of the locker load data. 43 | * 44 | * @param ArrayLoader $loader The package loader of composer. 45 | * @param array $lockData The lock data of locker. 46 | * @param bool $dev Check if the dev packages must be loaded. 47 | * 48 | * @return array The lock data 49 | */ 50 | public static function loadLockPackage(ArrayLoader $loader, array $lockData, bool $dev = false): array 51 | { 52 | $key = $dev ? 'packages-dev' : 'packages'; 53 | 54 | $loadDataWithKeys = $lockData[$key] ?? []; 55 | 56 | if ($loadDataWithKeys === []) { 57 | return $lockData; 58 | } 59 | 60 | /** 61 | * @psalm-var array[] $loadDataWithKeys 62 | */ 63 | foreach ($loadDataWithKeys as $index => $package) { 64 | $package = $loader->load($package); 65 | $package = $package instanceof AliasPackage ? $package->getAliasOf() : $package; 66 | $loadDataWithKeys[$index] = $package; 67 | } 68 | 69 | $lockData[$key] = $loadDataWithKeys; 70 | 71 | return $lockData; 72 | } 73 | 74 | /** 75 | * Convert the package aliases of the locker load data. 76 | * 77 | * @param array $lockData The lock data of locker. 78 | * 79 | * @return array The lock data. 80 | */ 81 | public static function convertLockAlias(array $lockData): array 82 | { 83 | $loadDatawithaliases = $lockData['aliases'] ?? []; 84 | 85 | if ($loadDatawithaliases === []) { 86 | return $lockData; 87 | } 88 | 89 | $alias = []; 90 | 91 | /** 92 | * @psalm-var array{ 93 | * array{alias: string, alias_normalized: string, version: string, package: string} 94 | * } $loadDatawithaliases 95 | */ 96 | foreach ($loadDatawithaliases as $config) { 97 | $alias[$config['package']][$config['version']] = [ 98 | 'alias' => $config['alias'], 99 | 'alias_normalized' => $config['alias_normalized'], 100 | ]; 101 | } 102 | 103 | $lockData['aliases'] = $alias; 104 | 105 | return $lockData; 106 | } 107 | } 108 | --------------------------------------------------------------------------------