├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
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 |
--------------------------------------------------------------------------------