diff --git a/.github/workflows/php-functional-tests.yml b/.github/workflows/php-functional-tests.yml new file mode 100644 index 0000000..39b8df9 --- /dev/null +++ b/.github/workflows/php-functional-tests.yml @@ -0,0 +1,53 @@ +name: PHP Functional Tests + +on: + push: + paths: + - '**workflows/php-functional-tests.yml' + - '**.php' + - '**phpunit.xml.dist' + - '**composer.json' + pull_request: + paths: + - '**workflows/php-functional-tests.yml' + - '**.php' + - '**phpunit.xml.dist' + - '**composer.json' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit-tests: + runs-on: ubuntu-latest + if: ${{ !contains(github.event.head_commit.message, 'skip tests') }} + strategy: + fail-fast: true + matrix: + php-ver: [ '8.0', '8.1', '8.2', '8.3' ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-ver }} + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + coverage: 'none' + tools: cs2pr, composer:v2 + + - name: Adjust Composer dependencies + run: | + composer remove --dev --no-update "inpsyde/php-coding-standards" + composer remove --dev --no-update "vimeo/psalm" + composer require --dev --no-update "composer/composer:^2" + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Run functional tests + run: ./vendor/bin/phpunit --testsuite=functional --no-coverage diff --git a/.github/workflows/php-static-analysis.yml b/.github/workflows/php-static-analysis.yml index 607ab0b..8056d5c 100644 --- a/.github/workflows/php-static-analysis.yml +++ b/.github/workflows/php-static-analysis.yml @@ -35,17 +35,25 @@ concurrency: jobs: lint-php: uses: inpsyde/reusable-workflows/.github/workflows/lint-php.yml@main + if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run lint only') || (github.event.inputs.jobs == 'Run all')) }} strategy: matrix: - php: ["7.2", "7.3", "7.4", "8.0", "8.1", "8.2"] - if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run lint only')) }} + php: [ '8.0', '8.1', '8.2', '8.3' ] with: PHP_VERSION: ${{ matrix.php }} coding-standards-analysis-php: - if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run PHPCS only')) }} + if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run PHPCS only') || (github.event.inputs.jobs == 'Run all')) }} uses: inpsyde/reusable-workflows/.github/workflows/coding-standards-php.yml@main + with: + PHP_VERSION: '8.0' static-code-analysis-php: - if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run Psalm only')) }} + if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run Psalm only') || (github.event.inputs.jobs == 'Run all')) }} uses: inpsyde/reusable-workflows/.github/workflows/static-analysis-php.yml@main + strategy: + matrix: + php: [ '8.0', '8.1', '8.2', '8.3' ] + with: + PHP_VERSION: ${{ matrix.php }} + PSALM_ARGS: '--no-suggestions --report-show-info=false --find-unused-psalm-suppress --no-diff --no-cache --no-file-cache --long-progress' diff --git a/.github/workflows/php-unit-tests.yml b/.github/workflows/php-unit-tests.yml index 7c3547d..c245c8b 100644 --- a/.github/workflows/php-unit-tests.yml +++ b/.github/workflows/php-unit-tests.yml @@ -28,19 +28,8 @@ jobs: strategy: fail-fast: false matrix: - php-ver: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2' ] - composer-ver: [ '^1', '~2.0.0', '~2.1.0', '~2.2.0', '~2.3.0', '~2.4.0', '~2.5.0' ] - exclude: - - php-ver: '8.1' - composer-ver: '^1' - - php-ver: '8.1' - composer-ver: '~2.0.0' - - php-ver: '8.2' - composer-ver: '^1' - - php-ver: '8.2' - composer-ver: '~2.0.0' - - php-ver: '8.2' - composer-ver: '~2.1.0' + php-ver: [ '8.0', '8.1', '8.2', '8.3' ] + composer-ver: [ '2.3', '2.4', '2.5', '2.6', '2' ] steps: - name: Update "USE_COVERAGE" env var based on matrix @@ -48,7 +37,7 @@ jobs: run: echo "USE_COVERAGE=yes" >> $GITHUB_ENV - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -56,24 +45,22 @@ jobs: php-version: ${{ matrix.php-ver }} ini-values: zend.assertions=1, error_reporting=-1, display_errors=On coverage: ${{ ((env.USE_COVERAGE == 'yes') && 'xdebug') || 'none' }} - tools: cs2pr + tools: cs2pr, composer:v${{ matrix.composer-ver }} - name: Adjust Composer dependencies run: | composer remove --dev --no-update "inpsyde/php-coding-standards" composer remove --dev --no-update "vimeo/psalm" - composer require --dev --no-update "composer/composer:${{ matrix.composer-ver }}" + composer require --dev --no-update "composer/composer:~${{ matrix.composer-ver }}.0" - name: Install dependencies - uses: ramsey/composer-install@v2 + uses: ramsey/composer-install@v3 - name: Run unit tests - run: | - ./vendor/bin/phpunit --atleast-version 9 && ./vendor/bin/phpunit --migrate-configuration || echo 'Config does not need updates.' - ./vendor/bin/phpunit --testsuite=unit ${{ ((env.USE_COVERAGE == 'yes') && '--coverage-html=coverage-report') || '--no-coverage' }} + run: ./vendor/bin/phpunit --testsuite=unit ${{ ((env.USE_COVERAGE == 'yes') && '--coverage-html=coverage-report') || '--no-coverage' }} - name: Upload coverage report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ env.USE_COVERAGE == 'yes' }} with: name: coverage-report diff --git a/LICENSE b/LICENSE index 52c635b..f0fc3a6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Inpsyde GmbH +Copyright (c) 2024 Syde GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 43487ff..823fdfd 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Let's assume we have a website project having a `composer.json` that looks like "require": { "acme/foo": "^1", "acme/bar": "^2", - "inpsyde/composer-assets-compiler": "^3" + "inpsyde/composer-assets-compiler": "^4" }, "extra": { "composer-asset-compiler": { "auto-run": true } @@ -65,27 +65,41 @@ The example above is the simplest use case, but the plugin has many possible con ## Documentation -- [Introduction](docs/001-Introduction.md) -- [Compiling Assets](docs/002-Compiling_Assets.md) -- [Script](docs/003-Script.md) -- [Dependencies](docs/004-Dependencies.md) -- [Package Manager](docs/005-Package_Manager.md) -- [Pre-compilation](docs/006-Pre-compilation.md) -- [Hash and Lock](docs/007-Hash_and_Lock.md) -- [Execution Mode](docs/008-Execution_Mode.md) -- [Configuration File](docs/009-Configuration_File.md) -- [Packages Configuration in Root](docs/010-Packages_Configuration_in_Root.md) -- [Verbosity](docs/011-Verbosity.md) -- [Isolated Cache](docs/012-Isolated_Cache.md) -- [Parallel Assets Processing](docs/013-Parallel_Assets_Processing.md) -- [Configuration Cheat-Sheet](docs/014-Configuration-Cheat-Sheet.md) -- [CLI Parameters](docs/015-CLI-Parameters.md) -- [Environment Variables](docs/016-Environment_Variables.md) +- [Why bother](./docs/001-Why_Bother.md) +- [Compiling Assets](./docs/002-Compiling_Assets.md) +- [Script](./docs/003-Script.md) +- [Dependencies](./docs/004-Dependencies.md) +- [Package Manager](./docs/005-Package_Manager.md) +- [Pre-compilation](./docs/006-Pre-compilation.md) +- [Hash and Lock](./docs/007-Hash_Lock.md) +- [Execution Mode](./docs/008-Execution_Mode.md) +- [Configuration File](./docs/009-Configuration_File.md) +- [Packages Configuration in Root](./docs/010-Packages_Configuration_Root.md) +- [Verbosity](./docs/011-Verbosity.md) +- [Isolated Cache](./docs/012-Isolated_Cache.md) +- [Parallel Assets Processing](./docs/013-Parallel_Assets_Processing.md) +- [Configuration Cheat-Sheet](./docs/014-Configuration_Cheat-Sheet.md) +- [CLI Commands and Parameters](./docs/015-CLI_Commands_Parameters.md) +- [Environment Variables](./docs/016-Environment_Variables.md) + +## Requirements + +- PHP 8.0+ +- Composer 2.3+ + +When installed with "dev" dependencies, via Composer the following packages are directly required: + +- `composer/composer` (MIT) +- `phpunit/phpunit` (BSD-3-Clause) +- `mockery/mockery` (BSD-3-Clause) +- `inpsyde/php-coding-standards` (MIT) +- `vimeo/psalm` (MIT) +- `mikey179/vfsstream` (BSD-3-Clause) -## License and Copyright -Copyright (c) 2022 [Inpsyde GmbH](https://inpsyde.com/en/). +## License and Copyright -_Composer Asset Compiler_ code is licensed under [MIT license](./LICENSE). +_Composer Asset Compiler_ is a free software, and is released under the terms of the MIT license. +See [LICENSE](./LICENSE) for complete license. -The team at Inpsyde is engineering the Web since 2006. +The team at Syde is engineering the Web since 2006. diff --git a/composer.json b/composer.json index 99c3d6c..52f356b 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,8 @@ "authors": [ { "name": "Inpsyde GmbH", - "homepage": "https://inpsyde.com/", - "email": "hello@inpsyde.com", + "homepage": "https://syde.com/", + "email": "hello@syde.com", "role": "Company" }, { @@ -18,9 +18,9 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": ">= 7.2 < 8.3", + "php": ">= 8.0 < 8.4", "ext-json": "*", - "composer-plugin-api": "^1 || ^2" + "composer-plugin-api": "^2.3" }, "autoload": { "psr-4": { @@ -33,12 +33,12 @@ } }, "require-dev": { - "composer/composer": "^1.10.24 || ^2.5.5", - "phpunit/phpunit": "^8.5.33 || ^9.6.7", - "mockery/mockery": "^1.3.5 || ^1.4.4", - "inpsyde/php-coding-standards": "^1.0.0", - "vimeo/psalm": ">=4.30.0", - "mikey179/vfsstream": "^1.6.11" + "composer/composer": "^2.3", + "phpunit/phpunit": "^9.6.7", + "mockery/mockery": "^1.6.7", + "inpsyde/php-coding-standards": "^2.0.0-beta.3", + "vimeo/psalm": "^5.22.2", + "mikey179/vfsstream": "^v1.6.11" }, "scripts": { "cs": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", @@ -56,7 +56,8 @@ "dev-master": "2.x-dev", "dev-v1.x": "1.x-dev", "dev-v2.x": "2.x-dev", - "dev-v3.x": "3.x-dev" + "dev-v3.x": "3.x-dev", + "dev-v4.x": "3.x-dev" } }, "config": { diff --git a/docs/001-Why-Bother.md b/docs/001-Why_Bother.md similarity index 100% rename from docs/001-Why-Bother.md rename to docs/001-Why_Bother.md diff --git a/docs/007-Hash_and_Lock.md b/docs/007-Hash_Lock.md similarity index 99% rename from docs/007-Hash_and_Lock.md rename to docs/007-Hash_Lock.md index 4dc952b..7b3b6d4 100644 --- a/docs/007-Hash_and_Lock.md +++ b/docs/007-Hash_Lock.md @@ -66,7 +66,7 @@ composer compile-assets --ignore-lock=* The hash of the package can also be calculated programmatically using the command: ```shell -composer assets-hash +composer asset-hash ``` That can be useful to generate the filename of the archive during pre-compilation, in case we want to use the `${hash}` placeholder. diff --git a/docs/008-Execution_Mode.md b/docs/008-Execution_Mode.md index 02ec2cc..e33859b 100644 --- a/docs/008-Execution_Mode.md +++ b/docs/008-Execution_Mode.md @@ -116,14 +116,4 @@ For example: } ``` -In the example above, `pre-compiled` configuration is the same regardless execution mode, whereas the `script` configuration is mode-specific. - - - -## Deprecated "env" - -In _Composer Assets Compiler_ < 3.0, the `"$mode"` property was called `"env"`. - -That generated confusion because it has nothing to do with _environment_ variables nor with the `default-env` configuration. That's why it was replaced by `"$mode"` in _Composer Assets Compiler_ 3. - -However, to facilitate migration for packages previously using a lower version, the `"env"` key is still supported in version 3, but support might be removed in future versions. +In the example above, `pre-compiled` configuration is the same regardless execution mode, whereas the `script` configuration is mode-specific. \ No newline at end of file diff --git a/docs/010-Packages_Configuration_in_Root.md b/docs/010-Packages_Configuration_Root.md similarity index 100% rename from docs/010-Packages_Configuration_in_Root.md rename to docs/010-Packages_Configuration_Root.md diff --git a/docs/011-Verbosity.md b/docs/011-Verbosity.md index 7adabc7..8cede3e 100644 --- a/docs/011-Verbosity.md +++ b/docs/011-Verbosity.md @@ -15,8 +15,8 @@ Composer options that control `verbosity` have multiple effects on _Composer Ass | Composer flag | _npm_ | _Yarn_ | |--------------------|------------|-------------| -| `-v` / `--verbose` | `-d` | -| `-vv` | `-dd` | +| `-v` / `--verbose` | `-d` | | +| `-vv` | `-dd` | | | `-vvv` | `-ddd` | `--verbose` | | `-q` / `-quite` | `--silent` | `--silent` | diff --git a/docs/014-Configuration-Cheat-Sheet.md b/docs/014-Configuration_Cheat-Sheet.md similarity index 100% rename from docs/014-Configuration-Cheat-Sheet.md rename to docs/014-Configuration_Cheat-Sheet.md diff --git a/docs/015-CLI-Parameters.md b/docs/015-CLI_Commands_Parameters.md similarity index 52% rename from docs/015-CLI-Parameters.md rename to docs/015-CLI_Commands_Parameters.md index 312791c..df795c3 100644 --- a/docs/015-CLI-Parameters.md +++ b/docs/015-CLI_Commands_Parameters.md @@ -1,9 +1,9 @@ --- -title: CLI parameters +title: CLI commands and parameters nav_order: 16 --- -# CLI parameters +# CLI commands and parameters ## Command: `compile-assets` @@ -16,9 +16,22 @@ nav_order: 16 -## Command: `assets-hash` +## Command: `asset-hash` | Parameter | Description | |-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| | `--mode=` | Sets the "execution mode" which might affect the hash of the assets. | | `--no-dev` | Simulate auto-run on Composer installation/update with the `--no-dev` flag.
This causes the plugin to check for `$default-no-dev` as the default "mode". | + + + +## Command: `assets-info` + +| Parameter | Description | +|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `... ` | Variadic number of asset names to limit information to. Can not be used together with `--root`. | +| `--mode=` | Sets the "execution mode" which might affect the hash of the assets. | +| `--no-dev` | Simulate auto-run on Composer installation/update with the `--no-dev` flag.
This causes the plugin to check for `$default-no-dev` as the default "mode". | +| `--root` | Get information only for root package. Can not be used if any asset name is given as argument. | +| `--table` | Format output as table. | +| `--fields` | Comma-separated properties to print. | diff --git a/docs/016-Environment_Variables.md b/docs/016-Environment_Variables.md index da22ad6..157d2c6 100644 --- a/docs/016-Environment_Variables.md +++ b/docs/016-Environment_Variables.md @@ -21,8 +21,8 @@ nav_order: 17 | `GITHUB_API_USER` | Alternative to `GITHUB_USER_NAME`. | | `GITHUB_ACTOR` | Alternative to `GITHUB_USER_NAME`. | | `GITHUB_USER_TOKEN` | Set the GitHub Personal Access Token to be used for GitHub pre-compilation adapters. | -| `GITHUB_API_TOKEN` | Alternative to `GITHUB_USER_TOKEN`. | -| `GITHUB_TOKEN` | Alternative to `GITHUB_USER_TOKEN`. | +| `GITHUB_API_TOKEN` | Alternative to `GITHUB_USER_TOKEN`. | +| `GITHUB_TOKEN` | Alternative to `GITHUB_USER_TOKEN`. | | `GITHUB_API_REPOSITORY` | Set the GitHub repository to be used for GitHub pre-compilation adapters. Alternative to `pre-compiled.config.repository` setting. | | `GITHUB_REPOSITORY` | Alternative to `GITHUB_API_REPOSITORY`. | diff --git a/docs/index.md b/docs/index.md index f0200c6..2b50d9c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ Let's assume we have a website project having a `composer.json` that looks like "require": { "acme/foo": "^1", "acme/bar": "^2", - "inpsyde/composer-assets-compiler": "^3" + "inpsyde/composer-assets-compiler": "^4" }, "extra": { "composer-asset-compiler": { "auto-run": true } @@ -65,19 +65,19 @@ The example above is the simplest use case, but the Composer Assets Compiler has ## Documentation -- [Why bother](./001-Why-Bother.md) +- [Why bother](./001-Why_Bother.md) - [Compiling Assets](./002-Compiling_Assets.md) - [Script](./003-Script.md) - [Dependencies](./004-Dependencies.md) - [Package Manager](./005-Package_Manager.md) - [Pre-compilation](./006-Pre-compilation.md) -- [Hash and Lock](./007-Hash_and_Lock.md) +- [Hash and Lock](./007-Hash_Lock.md) - [Execution Mode](./008-Execution_Mode.md) - [Configuration File](./009-Configuration_File.md) -- [Packages Configuration in Root](./010-Packages_Configuration_in_Root.md) +- [Packages Configuration in Root](./010-Packages_Configuration_Root.md) - [Verbosity](./011-Verbosity.md) - [Isolated Cache](./012-Isolated_Cache.md) - [Parallel Assets Processing](./013-Parallel_Assets_Processing.md) -- [Configuration Cheat-Sheet](./014-Configuration-Cheat-Sheet.md) -- [CLI Parameters](./015-CLI-Parameters.md) +- [Configuration Cheat-Sheet](./014-Configuration_Cheat-Sheet.md) +- [CLI Commands and Parameters](./015-CLI_Commands_Parameters.md) - [Environment Variables](./016-Environment_Variables.md) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index a9e6bef..0077ca9 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -7,7 +7,7 @@ - + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 64d43f9..1330b92 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,11 +1,20 @@ + convertWarningsToExceptions="true" + xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"> + + + + src + + + ./tests/unit @@ -14,9 +23,4 @@ ./tests/functional - - - src - - diff --git a/psalm.xml b/psalm.xml index 1eca58e..f350826 100644 --- a/psalm.xml +++ b/psalm.xml @@ -20,6 +20,7 @@ + diff --git a/src/Asset/Asset.php b/src/Asset/Asset.php index 514c85e..5b2e150 100644 --- a/src/Asset/Asset.php +++ b/src/Asset/Asset.php @@ -16,35 +16,12 @@ final class Asset { - /** - * @var string - */ - private $name; - - /** - * @var Config|null - */ - private $config = null; - - /** - * @var string|null - */ - private $folder = null; - - /** - * @var bool|null - */ - private $valid; - - /** - * @var string|null - */ - private $version; - - /** - * @var string|null - */ - private $reference; + private Config|null $config = null; + private string|null $folder = null; + private bool|null $valid = null; + private string|null $version = null; + private string|null $reference = null; + private bool $isRoot = false; /** * @param string $name @@ -52,6 +29,7 @@ final class Asset * @param string|null $folder * @param string|null $version * @param string|null $reference + * @param bool $isRoot * @return Asset */ public static function new( @@ -59,14 +37,16 @@ public static function new( Config $config, ?string $folder = null, ?string $version = null, - ?string $reference = null + ?string $reference = null, + bool $isRoot = false ): Asset { $asset = new static($name); - $asset->folder = $folder ? rtrim($folder, '/') : null; + $asset->folder = (($folder !== null) && ($folder !== '')) ? rtrim($folder, '/') : null; $asset->config = $config; $asset->version = $version; $asset->reference = $reference; + $asset->isRoot = $isRoot; return $asset; } @@ -74,16 +54,15 @@ public static function new( /** * @param string $name */ - private function __construct(string $name) + private function __construct(private string $name) { - $this->name = $name; } /** * @return bool * - * @psalm-assert-if-true string $this->name - * @psalm-assert-if-true string $this->folder + * @psalm-assert-if-true non-empty-string $this->name + * @psalm-assert-if-true non-empty-string $this->folder */ public function isValid(): bool { @@ -91,13 +70,13 @@ public function isValid(): bool return $this->valid; } - if (!$this->name || !$this->folder) { + if (($this->name === '') || ($this->folder === null) || ($this->folder === '')) { $this->valid = false; return false; } - if (!$this->config || !$this->config->isRunnable()) { + if (($this->config === null) || !$this->config->isRunnable()) { $this->valid = false; return false; @@ -191,11 +170,13 @@ public function env(): array */ public function preCompilationConfig(): PreCompilation\Config { - if (!$this->config || Env::readEnv('COMPOSER_ASSET_COMPILER_PRECOMPILING', $this->env())) { - return PreCompilation\Config::invalid(); + $env = Env::readEnv('COMPOSER_ASSET_COMPILER_PRECOMPILING', $this->env()) ?? ''; + + if (($this->config === null) || ($env !== '')) { + return PreCompilation\Config::newInvalid(); } - return $this->config->preCompilationConfig() ?? PreCompilation\Config::invalid(); + return $this->config->preCompilationConfig() ?? PreCompilation\Config::newInvalid(); } /** @@ -203,7 +184,7 @@ public function preCompilationConfig(): PreCompilation\Config */ public function isolatedCache(): ?bool { - return $this->config ? $this->config->isolatedCache() : null; + return $this->config?->isolatedCache(); } /** @@ -214,6 +195,14 @@ public function srcPaths(): array return $this->config ? $this->config->srcPaths() : []; } + /** + * @return bool + */ + public function isRoot(): bool + { + return $this->isRoot; + } + /** * @return array */ diff --git a/src/Asset/Config.php b/src/Asset/Config.php index 2bc7bad..301bc7f 100644 --- a/src/Asset/Config.php +++ b/src/Asset/Config.php @@ -52,50 +52,12 @@ class Config self::SRC_PATHS => [], ]; - /** - * @var bool - */ - private $byPackage = false; - - /** - * @var bool - */ - private $byRootPackage = false; - - /** - * @var ModeResolver - */ - private $modeResolver; - - /** - * @var array - */ - private $raw; - - /** - * @var RootConfig|null - */ - private $rootConfig = null; - - /** - * @var bool - */ - private $dataWasParsed = false; - - /** - * @var bool - */ - private $valid = false; - - /** - * @var array - */ - private $data = self::BASE_DATA; - - /** - * @var array - */ - private $rootEnv; + private bool $byPackage = false; + private bool $byRootPackage = false; + private RootConfig|null $rootConfig = null; + private bool $dataWasParsed = false; + private bool $valid = false; + private array $data = self::BASE_DATA; /** * @param array $data @@ -115,7 +77,7 @@ public static function new(array $data, ModeResolver $modeResolver, array $rootE * @return Config */ public static function forAssetConfigInRoot( - $config, + mixed $config, ModeResolver $modeResolver, array $rootEnv = [] ): Config { @@ -144,9 +106,10 @@ public static function forComposerPackage( ): Config { $path = $filesystem->normalizePath($path); + $configFile = "{$path}/" . self::CONFIG_FILE; - $raw = file_exists($configFile) - ? JsonFile::parseJson(file_get_contents($configFile) ?: '') + $raw = file_exists($configFile) + ? JsonFile::parseJson((string) file_get_contents($configFile)) : $package->getExtra()[self::EXTRA_KEY] ?? []; $isRoot = $package instanceof RootPackageInterface; @@ -176,7 +139,7 @@ public static function forComposerPackage( * @param ModeResolver $modeResolver * @return array */ - private static function parseRaw($raw, ModeResolver $modeResolver): array + private static function parseRaw(mixed $raw, ModeResolver $modeResolver): array { $config = $raw; @@ -188,24 +151,20 @@ private static function parseRaw($raw, ModeResolver $modeResolver): array $config = ($byMode === null) ? $noEnv : $byMode; } - if (is_bool($config)) { - $config = $config ? [self::BY_PACKAGE_OR_DEFAULTS => true] : [self::DISABLED => true]; - } - - switch (true) { - case ($config === self::DISABLED): - case ($config === self::FORCE_DEFAULTS): - case ($config === self::BY_PACKAGE_OR_DEFAULTS): - $config = [$config => true]; - break; - case (is_string($config)): - $config = [self::DEPENDENCIES => self::INSTALL, self::SCRIPT => $raw]; - break; - } - - is_array($config) or $config = []; - - return ($byMode && $noEnv) ? array_merge($noEnv, $config) : $config; + $calculatedConfig = match (true) { + ($config === self::DISABLED), + ($config === self::FORCE_DEFAULTS), + ($config === self::BY_PACKAGE_OR_DEFAULTS) => [$config => true], + ($config === true) => [self::BY_PACKAGE_OR_DEFAULTS => true], + ($config === false) => [self::DISABLED => true], + is_string($config) => [self::DEPENDENCIES => self::INSTALL, self::SCRIPT => $config], + is_array($config) => $config, + default => [], + }; + + return (($byMode !== null) && is_array($noEnv)) + ? array_merge($noEnv, $calculatedConfig) + : $calculatedConfig; } /** @@ -213,11 +172,11 @@ private static function parseRaw($raw, ModeResolver $modeResolver): array * @param ModeResolver $modeResolver * @param array $rootEnv */ - final private function __construct(array $raw, ModeResolver $modeResolver, array $rootEnv = []) - { - $this->raw = $raw; - $this->modeResolver = $modeResolver; - $this->rootEnv = $rootEnv; + final private function __construct( + private array $raw, + private ModeResolver $modeResolver, + private array $rootEnv = [] + ) { } /** @@ -257,7 +216,7 @@ public function isDisabled(): bool $this->parseData(); - return (bool)$this->data[self::DISABLED]; + return (bool) $this->data[self::DISABLED]; } /** @@ -271,7 +230,7 @@ public function usePackageLevelOrDefault(): bool $this->parseData(); - return (bool)$this->data[self::BY_PACKAGE_OR_DEFAULTS]; + return (bool) $this->data[self::BY_PACKAGE_OR_DEFAULTS]; } /** @@ -285,7 +244,7 @@ public function isForcedDefault(): bool $this->parseData(); - return (bool)$this->data[self::FORCE_DEFAULTS]; + return (bool) $this->data[self::FORCE_DEFAULTS]; } /** @@ -293,7 +252,8 @@ public function isForcedDefault(): bool */ public function isRunnable(): bool { - return $this->isValid() && ($this->dependencies() || $this->scripts()); + return $this->isValid() + && (($this->dependencies() !== null) || ($this->scripts() !== null)); } /** @@ -302,7 +262,7 @@ public function isRunnable(): bool public function dependencies(): ?string { $this->parseData(); - $deps = $this->valid ? (string)$this->data[self::DEPENDENCIES] : null; + $deps = $this->valid ? (string) $this->data[self::DEPENDENCIES] : null; ($deps === self::NONE) and $deps = null; return $deps; @@ -389,7 +349,7 @@ public function defaultEnv(): array $config = $this->raw[self::DEF_ENV] ?? null; $this->data[self::DEF_ENV] = (is_array($config) || ($config instanceof \stdClass)) - ? Env::sanitizeEnvVars((array)$config) + ? Env::sanitizeEnvVars((array) $config) : []; return $this->data[self::DEF_ENV]; @@ -461,7 +421,7 @@ private function parseData(): void $config = $this->raw; - if (!$config) { + if ($config === []) { $this->valid = false; return; @@ -469,78 +429,78 @@ private function parseData(): void $this->data = self::BASE_DATA; $scripts = $this->parseScripts($config); - $this->data[self::DEPENDENCIES] = $this->parseDependencies($config, (bool)$scripts); + $this->data[self::DEPENDENCIES] = $this->parseDependencies($config, $scripts !== null); $this->data[self::SCRIPT] = $scripts; $this->data[self::PRE_COMPILED] = $this->parsePreCompiled($config); $this->data[self::PACKAGE_MANAGER] = $this->parsePackageManager($config); $this->data[self::ISOLATED_CACHE] = $this->parseIsolatedCache($config); $this->data[self::SRC_PATHS] = $this->parseLockPaths($config); - - $this->valid = $this->data[self::DEPENDENCIES] || $this->data[self::SCRIPT]; + $this->valid = ($this->data[self::DEPENDENCIES] !== null) || ($scripts !== null); } /** * @param array $config * @param bool $haveScripts - * @return string|null + * @return value-of|null */ private function parseDependencies(array $config, bool $haveScripts): ?string { $default = $haveScripts ? self::INSTALL : null; $dependencies = $config[self::DEPENDENCIES] ?? $default; - if ($dependencies === false || $dependencies === null) { + if (($dependencies === false) || ($dependencies === null)) { $dependencies = self::NONE; } if (is_array($dependencies)) { $byMode = $this->modeResolver->resolveConfig($dependencies); - $dependencies = ($byMode && is_string($byMode)) ? $byMode : null; + $dependencies = (($byMode !== '') && is_string($byMode)) ? $byMode : null; } is_string($dependencies) and $dependencies = strtolower($dependencies); - if (!in_array($dependencies, self::DEPENDENCIES_OPTIONS, true)) { - $dependencies = null; + if (in_array($dependencies, self::DEPENDENCIES_OPTIONS, true)) { + return $dependencies; } - return $dependencies; + return null; } /** * @param array $config - * @return array|null + * @return array|null */ private function parseScripts(array $config): ?array { - $scripts = $config[self::SCRIPT] ?? null; - - $oneScript = $scripts && is_string($scripts); - if (!$scripts || (!$oneScript && !is_array($scripts))) { - $scripts = null; + $scripts = $config[self::SCRIPT] ?? []; + $oneScript = is_string($scripts) && ($scripts !== ''); + if ($oneScript) { + $scripts = [$scripts]; } - if ($scripts === null || $oneScript) { - /** @var string $scripts */ - return $oneScript ? [$scripts] : null; + if (($scripts === []) || !is_array($scripts)) { + return null; } - /** @var array $scripts */ + if ($oneScript) { + /** @var array $scripts */ + return $scripts; + } $byMode = $this->modeResolver->resolveConfig($scripts); - if ($byMode && (is_array($byMode) || is_string($byMode))) { - $scripts = (array)$byMode; + if (($byMode !== []) && ($byMode !== '') && (is_array($byMode) || is_string($byMode))) { + $scripts = (array) $byMode; } elseif ($byMode === null) { $scripts = $this->modeResolver->removeModeConfig($scripts); } $allScripts = []; foreach ($scripts as $script) { - ($script && is_string($script)) and $allScripts[$script] = 1; + (($script !== '') && is_string($script)) and $allScripts[] = $script; } - /** @var list $keys */ - $keys = $allScripts ? array_keys($allScripts) : null; + /** @var list|null $scripts */ + $scripts = ($allScripts !== []) ? array_values(array_unique($allScripts)) : null; - return $keys; + return $scripts; } /** @@ -550,8 +510,8 @@ private function parseScripts(array $config): ?array private function parsePreCompiled(array $config): PreCompilation\Config { $raw = $config[self::PRE_COMPILED] ?? null; - if (!$raw || !is_array($raw)) { - return PreCompilation\Config::invalid(); + if (($raw === []) || !is_array($raw)) { + return PreCompilation\Config::newInvalid(); } return PreCompilation\Config::new($raw, $this->modeResolver); @@ -563,12 +523,11 @@ private function parsePreCompiled(array $config): PreCompilation\Config */ private function parsePackageManager(array $config): ?PackageManager\PackageManager { - // 'commands' is deprecated, but we don't have an IO instance here to inform the user - $manager = $config[self::PACKAGE_MANAGER] ?? $config['commands'] ?? null; + $manager = $config[self::PACKAGE_MANAGER] ?? null; - if (!$manager) { - $byEnv = $this->resolveByEnv('PACKAGE_MANAGER'); - if (!$byEnv) { + if (($manager === null) || ($manager === []) || ($manager === '')) { + $byEnv = $this->resolveByEnv('PACKAGE_MANAGER') ?? ''; + if ($byEnv === '') { return null; } @@ -577,7 +536,7 @@ private function parsePackageManager(array $config): ?PackageManager\PackageMana if (is_array($manager)) { $byMode = $this->modeResolver->resolveConfig($manager); - if ($byMode && (is_array($byMode) || is_string($byMode))) { + if (($byMode !== []) && ($byMode !== '') && (is_array($byMode) || is_string($byMode))) { $manager = $byMode; } elseif ($byMode === null) { $manager = $this->modeResolver->removeModeConfig($manager); @@ -585,7 +544,7 @@ private function parsePackageManager(array $config): ?PackageManager\PackageMana } if (is_string($manager)) { - return PackageManager\PackageManager::fromDefault(strtolower($manager)); + return PackageManager\PackageManager::fromDefault($manager); } return is_array($manager) ? PackageManager\PackageManager::new($manager) : null; @@ -602,16 +561,17 @@ private function parseIsolatedCache(array $config): ?bool return null; } - if (is_array($isolated)) { - $byMode = $this->modeResolver->resolveConfig($isolated); - if ($byMode && (is_bool($byMode) || is_string($byMode))) { - $isolated = $byMode; - } elseif ($byMode === null) { - return false; - } + if (!is_array($isolated)) { + return false; } - return (bool)filter_var($isolated, FILTER_VALIDATE_BOOLEAN); + $byMode = $this->modeResolver->resolveConfig($isolated); + + return match (true) { + is_bool($byMode) => $byMode, + is_string($byMode) => filter_var($byMode, FILTER_VALIDATE_BOOLEAN), + default => false, + }; } /** @@ -624,7 +584,7 @@ private function parseLockPaths(array $config): array if (is_array($paths)) { $byMode = $this->modeResolver->resolveConfig($paths); - if ($byMode && (is_array($byMode) || is_string($byMode))) { + if (($byMode !== '') && ($byMode !== []) && (is_array($byMode) || is_string($byMode))) { $paths = $byMode; } elseif ($byMode === null) { $paths = $this->modeResolver->removeModeConfig($paths); diff --git a/src/Asset/Defaults.php b/src/Asset/Defaults.php index a5bdb5c..ba47f86 100644 --- a/src/Asset/Defaults.php +++ b/src/Asset/Defaults.php @@ -12,26 +12,21 @@ namespace Inpsyde\AssetsCompiler\Asset; /** - * @template T of Config|null + * @template-covariant T of Config|null */ final class Defaults { /** - * @var T + * @return Defaults */ - private $config; - - /** - * @return Defaults - */ - public static function empty(): Defaults + public static function newEmpty(): Defaults { return new self(null); } /** * @param Config $config - * @return Defaults + * @return Defaults */ public static function new(Config $config): Defaults { @@ -39,12 +34,10 @@ public static function new(Config $config): Defaults } /** - * @param Config|null $config + * @param T $config */ - private function __construct(?Config $config) + private function __construct(private ?Config $config) { - /** @var T config */ - $this->config = $config; } /** diff --git a/src/Asset/Factory.php b/src/Asset/Factory.php index fd3d13e..2b816fe 100644 --- a/src/Asset/Factory.php +++ b/src/Asset/Factory.php @@ -13,37 +13,13 @@ use Composer\Installer\InstallationManager; use Composer\Package\PackageInterface; +use Composer\Package\RootPackage; use Composer\Package\RootPackageInterface; use Composer\Util\Filesystem; use Inpsyde\AssetsCompiler\Util\ModeResolver; class Factory { - /** - * @var ModeResolver - */ - private $modeResolver; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var InstallationManager - */ - private $installationManager; - - /** - * @var string - */ - private $rootDir; - - /** - * @var array - */ - private $rootEnv; - /** * @param ModeResolver $modeResolver * @param Filesystem $filesystem @@ -71,18 +47,12 @@ public static function new( * @param array $rootEnv */ final private function __construct( - ModeResolver $modeResolver, - Filesystem $filesystem, - InstallationManager $installationManager, - string $rootDir, - array $rootEnv + private ModeResolver $modeResolver, + private Filesystem $filesystem, + private InstallationManager $installationManager, + private string $rootDir, + private array $rootEnv ) { - - $this->modeResolver = $modeResolver; - $this->filesystem = $filesystem; - $this->installationManager = $installationManager; - $this->rootDir = $rootDir; - $this->rootEnv = $rootEnv; } /** @@ -108,7 +78,7 @@ public function attemptFactory( $isRoot = ($package instanceof RootPackageInterface); $path = $isRoot ? $this->rootDir - : ($this->installationManager->getInstallPath($package) ?? ''); + : (string) $this->installationManager->getInstallPath($package); if (!$config && (!$rootLevelPackageConfig || $packageOrDefaultAllowed)) { $packageLevelConfig = Config::forComposerPackage( @@ -125,16 +95,35 @@ public function attemptFactory( $config = $defaultConfig; } - if (!$config || !$config->isRunnable()) { + return $this->createAssetForConfig($config, $package, $path); + } + + /** + * @param Config|null $config + * @param PackageInterface $package + * @param string $path + * @return Asset|null + */ + private function createAssetForConfig( + ?Config $config, + PackageInterface $package, + string $path + ): ?Asset { + + if (($config === null) || !$config->isRunnable()) { return null; } + $sourceRef = $package->getSourceReference() ?? ''; + $distRef = $package->getDistReference() ?? ''; + return Asset::new( $package->getName(), $config, $this->filesystem->normalizePath($path), $package->getPrettyVersion(), - $package->getSourceReference() ?: $package->getDistReference() + ($sourceRef !== '') ? $sourceRef : ($distRef !== '' ? $distRef : null), + $package instanceof RootPackage ); } diff --git a/src/Asset/Finder.php b/src/Asset/Finder.php index 20010e6..f806123 100644 --- a/src/Asset/Finder.php +++ b/src/Asset/Finder.php @@ -21,27 +21,7 @@ class Finder /** * @var array */ - private $packagesData; - - /** - * @var ModeResolver - */ - private $modeResolver; - - /** - * @var Defaults - */ - private $defaults; - - /** - * @var Config - */ - private $rootPackageConfig; - - /** - * @var bool - */ - private $stopOnFailure; + private array $packagesData; /** * @param array $packageData @@ -79,17 +59,13 @@ public static function new( */ private function __construct( array $packageData, - ModeResolver $modeResolver, - Defaults $defaults, - Config $rootPackageConfig, - bool $stopOnFailure + private ModeResolver $modeResolver, + private Defaults $defaults, + private Config $rootPackageConfig, + private bool $stopOnFailure ) { $this->packagesData = $modeResolver->removeModeConfig($packageData); - $this->modeResolver = $modeResolver; - $this->defaults = $defaults; - $this->rootPackageConfig = $rootPackageConfig; - $this->stopOnFailure = $stopOnFailure; } /** @@ -125,14 +101,14 @@ public function find( if ( ($package === $root) || isset($found[$name]) - || $this->nameMatches($name, ...$excludeNames)[0] + || ($this->nameMatches($name, ...$excludeNames)[0] !== null) ) { continue; } [, $rootLevelPackagePattern] = $this->nameMatches($name, ...$rootLevelIncludePatterns); - $rootLevelPackageConfig = $rootLevelPackagePattern + $rootLevelPackageConfig = ($rootLevelPackagePattern !== null) ? ($rootLevelPackagesConfig[$rootLevelPackagePattern] ?? null) : null; @@ -184,7 +160,7 @@ private function attemptFactoryRootPackageAsset( $rootPackage = $assetFactory->attemptFactory( $root, $this->rootPackageConfig, - Defaults::empty() + Defaults::newEmpty() ); if ($rootPackage && $rootPackage->isValid()) { return $rootPackage; @@ -194,8 +170,7 @@ private function attemptFactoryRootPackageAsset( } /** - * @return array{array, array} - * @throws \Exception + * @return list{array, array} */ private function extractRootLevelPackagesData(): array { @@ -203,7 +178,7 @@ private function extractRootLevelPackagesData(): array $exclude = []; foreach ($this->packagesData as $pattern => $packageData) { - $config = $this->factoryRootLevelPackageConfig((string)$pattern, $packageData); + $config = $this->factoryRootLevelPackageConfig((string) $pattern, $packageData); if (!$config) { continue; } @@ -226,7 +201,7 @@ private function extractRootLevelPackagesData(): array * @param mixed $packageData * @return Config|null */ - private function factoryRootLevelPackageConfig(string $pattern, $packageData): ?Config + private function factoryRootLevelPackageConfig(string $pattern, mixed $packageData): ?Config { if (!$pattern) { if ($this->stopOnFailure) { @@ -252,13 +227,13 @@ private function factoryRootLevelPackageConfig(string $pattern, $packageData): ? /** * @param string $name * @param string[] $patterns - * @return array{0:null, 1:null}|array{0:string, 1:string} + * @return list{null,null}|list{string,string} */ private function nameMatches(string $name, string ...$patterns): array { foreach ($patterns as $pattern) { if ( - $pattern === $name + ($pattern === $name) || fnmatch($pattern, $name, FNM_PATHNAME | FNM_PERIOD | FNM_CASEFOLD) ) { return [$name, $pattern]; diff --git a/src/Asset/HashBuilder.php b/src/Asset/HashBuilder.php index d825b6d..08bde4d 100644 --- a/src/Asset/HashBuilder.php +++ b/src/Asset/HashBuilder.php @@ -11,143 +11,70 @@ namespace Inpsyde\AssetsCompiler\Asset; -use Composer\Util\Filesystem; use Inpsyde\AssetsCompiler\Util\Env; use Inpsyde\AssetsCompiler\Util\Io; -use Symfony\Component\Finder\Finder as FileFinder; final class HashBuilder { - private const PATTERN_REGEX = '~^(.+?)/([^/]+)$~'; - /** - * @var array - */ - private $hashes = []; - - /** - * @var Filesystem - */ - private $filesystem; + /** @var array */ + private array $hashes = []; /** - * @var Io - */ - private $io; - - /** - * @param Filesystem $filesystem + * @param PathsFinder $pathsFinder * @param Io $io * @return HashBuilder */ - public static function new(Filesystem $filesystem, Io $io): HashBuilder + public static function new(PathsFinder $pathsFinder, Io $io): HashBuilder { - return new static($filesystem, $io); + return new static($pathsFinder, $io); } /** - * @param Filesystem $filesystem + * @param PathsFinder $pathsFinder * @param Io $io */ - private function __construct(Filesystem $filesystem, Io $io) - { - $this->filesystem = $filesystem; - $this->io = $io; + private function __construct( + private PathsFinder $pathsFinder, + private Io $io + ) { } /** * @param Asset $asset - * @return string|null + * @return non-falsy-string|null */ public function forAsset(Asset $asset): ?string { - $basePath = $asset->isValid() ? $asset->path() : null; - if (!$basePath) { - return null; - } - $key = $asset->name(); - if ($this->hashes[$key] ?? null) { + if (array_key_exists($key, $this->hashes)) { return $this->hashes[$key]; } - $files = $this->mergeFilesInPatterns( - $asset, - $basePath, - [ - $basePath . '/package.json', - $basePath . '/package-lock.json', - $basePath . '/npm-shrinkwrap.json', - $basePath . '/yarn.lock', - ] - ); + $files = $this->pathsFinder->findAssetPaths($asset); + + if ($this->io->isVerbose()) { + foreach ($files as $file) { + $this->io->write("Will use '{$file}' file to calculate package hash"); + } + } - $hashes = ''; + $script = Env::replaceEnvVariables(implode(' ', $asset->script()), $asset->env()); + $hashes = $asset->isInstall() ? "|install|{$script}" : "|update|{$script}"; $done = []; foreach ($files as $file) { if (isset($done[$file])) { continue; } - $done[$file] = 1; + $done[$file] = true; if (file_exists($file) && is_readable($file)) { - $hashes .= @(md5_file($file) ?: ''); + $hashes .= (string) md5_file($file); } } - $hashes .= $asset->isInstall() ? '|install|' : '|update|'; - $hashes .= Env::replaceEnvVariables(implode(' ', $asset->script()), $asset->env()); - - $this->hashes[$key] = sha1($hashes); - - return $this->hashes[$key]; - } - - /** - * @param Asset $asset - * @param string $basePath - * @param list $files - * @return list - */ - private function mergeFilesInPatterns(Asset $asset, string $basePath, array $files): array - { - $patterns = $asset->srcPaths(); - if (!$patterns) { - return $files; - } - - /** @var FileFinder|null $finder */ - $finder = null; - foreach ($patterns as $pattern) { - try { - $pathfinder = FileFinder::create()->ignoreUnreadableDirs(true)->ignoreVCS(true)->sortByName(); - $hasFile = preg_match(self::PATTERN_REGEX, $pattern, $matches); - $dir = "{$basePath}/" . ltrim($hasFile ? $matches[1] : $pattern, './'); - $hasFile - ? $pathfinder->in("{$dir}/")->name($matches[2]) - : $pathfinder->in($dir); - $finder = $finder ? $finder->append($pathfinder) : $pathfinder; - } catch (\Throwable $throwable) { - $this->io->writeError($throwable->getMessage()); - continue; - } - } - - if (!$finder) { - $patternsStr = implode("', '", $patterns); - $this->io->writeError("Error building Symfony Finder for '{$patternsStr}'."); - - return $files; - } - - foreach ($finder->files() as $fileInfo) { - $path = $fileInfo->getRealPath(); - if ($path) { - $rel = $fileInfo->getRelativePath() . '/' . $fileInfo->getBasename(); - $normalized = $this->filesystem->normalizePath($rel); - $this->io->writeVerbose("Will use '{$normalized}' file to calculate package hash"); - $files[] = $this->filesystem->normalizePath($path); - } - } + /** @var non-falsy-string $hash */ + $hash = sha1($hashes); + $this->hashes[$key] = $hash; - return $files; + return $hash; } } diff --git a/src/Asset/Info.php b/src/Asset/Info.php new file mode 100644 index 0000000..d4fda12 --- /dev/null +++ b/src/Asset/Info.php @@ -0,0 +1,315 @@ +> */ + private array $cache = []; + + /** + * @param \Iterator $assets + * @param HashBuilder $hashBuilder + * @param Handler $preCompilation + * @param PathsFinder $pathsFinder + * @param ModeResolver $modeResolver + * @param RootConfig $rootConfig, + * @param Io $io + * @return static + */ + public static function new( + \Iterator $assets, + HashBuilder $hashBuilder, + PreCompilation\Handler $preCompilation, + PathsFinder $pathsFinder, + ModeResolver $modeResolver, + RootConfig $rootConfig, + Io $io + ): static { + + return new static( + $assets, + $hashBuilder, + $preCompilation, + $pathsFinder, + $modeResolver, + $rootConfig, + $io + ); + } + + /** + * @param \Iterator $assets + * @param HashBuilder $hashBuilder + * @param Handler $preCompilation + * @param PathsFinder $pathsFinder + * @param ModeResolver $modeResolver + * @param RootConfig $rootConfig + * @param Io $io + */ + final private function __construct( + private \Iterator $assets, + private HashBuilder $hashBuilder, + private PreCompilation\Handler $preCompilation, + private PathsFinder $pathsFinder, + private ModeResolver $modeResolver, + private RootConfig $rootConfig, + private Io $io + ) { + } + + /** + * @param Asset|string $asset + * @param Asset|string ...$assets + * @return ($assets is non-empty-array + * ? non-empty-list> + * : non-empty-array) + */ + public function assetInfo(Asset|string $asset, Asset|string ...$assets): array + { + $isSingle = $assets === []; + array_unshift($assets, $asset); + + $names = []; + $successes = []; + $errors = []; + foreach ($assets as $oneAsset) { + $name = is_string($oneAsset) ? $oneAsset : $oneAsset->name(); + $names[] = $name; + + try { + $info = $this->oneAssetInfo($oneAsset); + $successes[] = $info; + } catch (\Throwable) { + $errors[] = $name; + continue; + } + } + /** @psalm-suppress RedundantCondition */ + $this->maybeFail($successes, $errors, $names); + /** @var non-empty-list> $successes */ + return $isSingle ? $successes[0] : $successes; + } + + /** + * @param Asset $asset + * @return non-empty-list> + */ + public function allAssetsInfo(): array + { + $successes = []; + $errors = []; + $names = []; + $root = null; + /** @var Asset $asset */ + foreach ($this->assets as $asset) { + $name = $asset->name(); + $names[] = $name; + try { + $info = $this->oneAssetInfo($asset); + $successes[] = $info; + } catch (\Throwable) { + $errors[] = $name; + continue; + } + if ($asset->isRoot()) { + $root = $info; + continue; + } + + $successes[] = $info; + } + + $root or $root = $this->oneAssetInfo($this->rootAsset()); + array_unshift($successes, $root); + + /** @psalm-suppress RedundantCondition */ + $this->maybeFail($successes, $errors, $names); + /** @var non-empty-list> $successes */ + return $successes; + } + + /** + * @param Asset $asset + * @return list + */ + public function allAssetsPaths(): array + { + return $this->pathsFinder->findAllAssetsPaths(); + } + + /** + * @param Asset $asset + * @return non-empty-array + */ + public function rootAssetInfo(): array + { + $info = []; + $found = false; + foreach ($this->assets as $asset) { + if ($asset->isRoot()) { + $found = true; + $info = $this->oneAssetInfo($asset); + break; + } + } + + $found or $info = $this->oneAssetInfo($this->rootAsset()); + + /** @psalm-suppress RedundantCondition */ + $this->maybeFail($info, [], 'root'); + + return $info; + } + + /** + * @param Asset|string $asset + * @return non-empty-array + */ + private function oneAssetInfo(Asset|string $asset): array + { + if (is_string($asset)) { + $assetName = $asset; + $asset = $this->findByName($asset); + if ($asset === null) { + throw new \Error("No asset found for '{$assetName}'."); + } + } + + $key = $asset->name(); + if (isset($this->cache[$key])) { + return $this->cache[$key]; + } + + $hash = $this->hashBuilder->forAsset($asset); + + $scripts = []; + foreach ($asset->script() as $script) { + $scripts[] = Env::replaceEnvVariables($script, $asset->env()); + } + + $this->cache[$key] = [ + 'name' => $key, + 'is_root' => $asset->isRoot(), + 'to_be_compiled' => $asset->isValid(), + 'dependencies' => match (true) { + $asset->isUpdate() => Config::UPDATE, + $asset->isInstall() => Config::INSTALL, + default => Config::NONE, + }, + 'script' => $scripts, + 'hash' => $hash, + 'install_path' => $asset->path(), + 'watchable_paths' => $this->pathsFinder->findAssetPaths($asset), + 'precompilation' => $this->preCompilationConfig($asset, $hash ?? ''), + 'version' => $asset->version(), + 'isolated_cache' => $asset->isolatedCache(), + 'reference' => $asset->reference(), + 'environment' => $asset->env(), + ]; + + return $this->cache[$key]; + } + + /** + * @param Asset $asset + * @param string $hash + * @return array + */ + private function preCompilationConfig(Asset $asset, string $hash): array + { + $preCompilationConfig = $asset->preCompilationConfig(); + $mode = $this->modeResolver->mode(); + $placeholders = PreCompilation\Placeholders::new($asset, $mode, $hash); + $adapter = $this->preCompilation->findAdapter($preCompilationConfig, $placeholders); + if (!$adapter) { + return [ + 'is_valid' => false, + 'adapter' => null, + 'source' => null, + 'target' => null, + 'params' => null, + ]; + } + + $environment = $asset->env(); + + return [ + 'is_valid' => $preCompilationConfig->isValid(), + 'adapter' => $adapter->id(), + 'source' => $preCompilationConfig->source($placeholders, $environment), + 'target' => $preCompilationConfig->target($placeholders), + 'params' => $preCompilationConfig->config($placeholders, $environment), + ]; + } + + /** + * @param string $name + * @return Asset|null + */ + private function findByName(string $name): ?Asset + { + foreach ($this->assets as $asset) { + if ($name === $asset->name()) { + return $asset; + } + } + + if ($name === $this->rootConfig->name()) { + return $this->rootAsset(); + } + + return null; + } + + /** + * @param array $successes + * @param list $errors + * @param list|string $names + * @return void + * + * @psalm-assert non-empty-array $successes + */ + private function maybeFail(array $successes, array $errors, array|string $names = []): void + { + if (($successes !== []) && (($errors === []))) { + return; + } + + $allFailed = $successes === []; + $errorMessage = "Failed obtaining assets compilation info for %s."; + $reason = match (true) { + ($names === 'root') => 'root asset', + $allFailed => 'any asset', + ($errors === []) => 'given assets', + default => sprintf(': "%s"', implode('", "', $errors)), + }; + + if ($allFailed) { + throw new \Error(sprintf($errorMessage, $reason)); + } + + $this->io->writeError(sprintf($errorMessage, $reason)); + } + + /** + * @return Asset + */ + private function rootAsset(): Asset + { + return Asset::new( + $this->rootConfig->name(), + $this->rootConfig->config(), + $this->rootConfig->path(), + isRoot: true + ); + } +} diff --git a/src/Asset/Locker.php b/src/Asset/Locker.php index d3d6131..ae5840a 100644 --- a/src/Asset/Locker.php +++ b/src/Asset/Locker.php @@ -18,35 +18,22 @@ class Locker public const LOCK_FILE = '.composer_compiled_assets'; public const IGNORE_ALL = '*'; - /** - * @var Io - */ - private $io; + private bool $ignoreAll; - /** - * @var HashBuilder - */ - private $hashBuilder; - - /** - * @var boolean - */ - private $ignoreAll; - - /** - * @var list - */ - private $ignored = []; + /** @var list */ + private array $ignored = []; /** * @param Io $io * @param HashBuilder $hashBuilder * @param string $ignoreLock */ - public function __construct(Io $io, HashBuilder $hashBuilder, string $ignoreLock = '') - { - $this->io = $io; - $this->hashBuilder = $hashBuilder; + public function __construct( + private Io $io, + private HashBuilder $hashBuilder, + string $ignoreLock = '' + ) { + $this->ignoreAll = ($ignoreLock === self::IGNORE_ALL); if (!$this->ignoreAll && $ignoreLock) { $names = array_map('trim', explode(',', $ignoreLock)); @@ -65,14 +52,14 @@ public function isLocked(Asset $asset): bool } $file = ($asset->path() ?? '') . '/' . self::LOCK_FILE; - if (!@file_exists($file)) { + if (!file_exists($file) || !is_readable($file)) { return false; } $name = $asset->name(); foreach ($this->ignored as $ignored) { if ( - $ignored === $name + ($ignored === $name) || fnmatch($ignored, $name, FNM_PATHNAME | FNM_PERIOD | FNM_CASEFOLD) ) { $this->io->writeVerboseComment(" Ignoring lock file for {$name}."); @@ -82,7 +69,8 @@ public function isLocked(Asset $asset): bool } $content = @file_get_contents($file); - if (!$content) { + + if (($content === false) || ($content === '')) { $this->io->writeVerboseError(" Could not read content of lock file {$file}."); @unlink($file); @@ -90,9 +78,9 @@ public function isLocked(Asset $asset): bool return false; } - $hash = $this->hashBuilder->forAsset($asset); + $hash = $this->hashBuilder->forAsset($asset) ?? ''; - return $hash && trim($content) === $hash; + return ($hash !== '') && (trim($content) === $hash); } /** @@ -103,9 +91,9 @@ public function lock(Asset $asset): void { $file = ($asset->path() ?? '') . '/' . self::LOCK_FILE; $name = $asset->name(); - $content = $this->hashBuilder->forAsset($asset); + $content = $this->hashBuilder->forAsset($asset) ?? ''; - if ($content && !@file_put_contents($file, $content)) { + if (($content !== '') && (@file_put_contents($file, $content) === false)) { $this->io->writeVerboseError(" Could not write lock file {$file} for {$name}."); } } diff --git a/src/Asset/PathsFinder.php b/src/Asset/PathsFinder.php new file mode 100644 index 0000000..c895b7c --- /dev/null +++ b/src/Asset/PathsFinder.php @@ -0,0 +1,185 @@ +> */ + private array $cache = []; + + /** + * @param \Iterator $assets + * @param Filesystem $filesystem + * @param Io $io + * @param string $cwd + * @return static + */ + public static function new( + \Iterator $assets, + Filesystem $filesystem, + Io $io, + string $cwd + ): static { + + return new static($assets, $filesystem, $io, $cwd); + } + + /** + * @param \Iterator $assets + * @param Filesystem $filesystem + * @param Io $io, + * @param string $cwd + */ + final private function __construct( + private \Iterator $assets, + private Filesystem $filesystem, + private Io $io, + private string $cwd + ) { + } + + /** + * @param Asset $asset + * @return list + */ + public function findAssetPaths(Asset $asset): array + { + $key = $asset->name(); + if (isset($this->cache[$key])) { + return $this->cache[$key]; + } + + $basePath = ($asset->isValid() ? $asset->path() : null) ?? ''; + if ($basePath === '') { + $this->cache[$key] = []; + + return []; + } + + /** @var list $files */ + $files = []; + foreach (self::DEFAULT_FILES as $defaultFilePath) { + if (file_exists($basePath . $defaultFilePath)) { + $files[] = $this->relativePath($basePath . $defaultFilePath); + } + } + + $finder = $this->createFinder($asset); + + if ($finder === null) { + return $files; + } + + foreach ($finder->files() as $fileInfo) { + /** @var non-empty-string $path */ + $fullpath = $this->normalizePath($fileInfo); + if ($fullpath !== null) { + $files[] = $this->relativePath($fullpath); + } + } + + return $files; + } + + /** + * @return list + */ + public function findAllAssetsPaths(): array + { + $found = []; + foreach ($this->assets as $asset) { + $assetPaths = $this->findAssetPaths($asset); + if ($assetPaths !== []) { + $found = array_merge($found, $assetPaths); + } + } + + return array_values(array_unique($found)); + } + + /** + * @param SplFileInfo $fileInfo + * @return non-empty-string|null + */ + private function normalizePath(SplFileInfo $fileInfo): ?string + { + $path = $fileInfo->getRealPath(); + if ($path === false) { + return null; + } + $rel = $fileInfo->getRelativePath() . '/' . $fileInfo->getBasename(); + /** @var non-empty-string */ + return $this->filesystem->normalizePath($rel); + } + + /** + * @param Asset $asset + * @return SymfonyFinder|null + */ + private function createFinder(Asset $asset): ?SymfonyFinder + { + $patterns = $asset->srcPaths(); + if (!$patterns) { + return null; + } + + /** @var SymfonyFinder|null $finder */ + $finder = null; + $basePath = $asset->path(); + foreach ($patterns as $pattern) { + try { + $itemFinder = SymfonyFinder::create() + ->ignoreUnreadableDirs(true) + ->ignoreVCS(true) + ->sortByName(); + $hasFile = preg_match(self::PATTERN_REGEX, $pattern, $matches); + $dir = "{$basePath}/" . ltrim($hasFile ? $matches[1] : $pattern, './'); + $hasFile + ? $itemFinder->in("{$dir}/")->name($matches[2]) + : $itemFinder->in($dir); + $finder = $finder?->append($itemFinder) ?? $itemFinder; + } catch (\Throwable $throwable) { + $this->io->writeError($throwable->getMessage()); + continue; + } + } + + if ($finder === null) { + $patternsStr = implode("', '", $patterns); + $this->io->writeError("Error building Symfony Finder for '{$patternsStr}'."); + } + + return $finder; + } + + /** + * @param non-empty-string $fullpath + * @return non-empty-string + */ + private function relativePath(string $fullpath): string + { + if (!$this->filesystem->isAbsolutePath($fullpath)) { + return $fullpath; + } + + $relative = $this->filesystem->findShortestPath($this->cwd, $fullpath); + + return str_starts_with($relative, '.') ? $relative : "./{$relative}"; + } +} diff --git a/src/Asset/Processor.php b/src/Asset/Processor.php index e2ac961..f752146 100644 --- a/src/Asset/Processor.php +++ b/src/Asset/Processor.php @@ -25,60 +25,15 @@ */ class Processor { - /** - * @var Io - */ - private $io; - - /** - * @var Config - */ - private $config; - - /** - * @var Finder - */ - private $packageManagerFinder; - - /** - * @var ProcessExecutor - */ - private $executor; - - /** - * @var Locker - */ - private $locker; - - /** - * @var ParallelManager - */ - private $parallelManager; - - /** - * @var PreCompilation\Handler - */ - private $preCompiler; - - /** - * @var callable - */ + /** @var callable */ private $outputHandler; - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var PackageManager|null - */ - private $defaultPackageManager; + private ?PackageManager $defaultPackageManager = null; /** * @var array{bool, string|null} */ - private $tempDir = [false, null]; + private array $tempDir = [false, null]; /** * @param Io $io @@ -112,8 +67,8 @@ public static function new( $parallelManager, $locker, $preCompiler, - $outputHandler, - $filesystem + $filesystem, + $outputHandler ); } @@ -125,30 +80,22 @@ public static function new( * @param ParallelManager $parallelManager * @param Locker $locker * @param PreCompilation\Handler $preCompiler - * @param callable $outputHandler * @param Filesystem $filesystem + * @param callable $outputHandler */ private function __construct( - Io $io, - Config $config, - Finder $packageManagerFinder, - ProcessExecutor $executor, - ParallelManager $parallelManager, - Locker $locker, - PreCompilation\Handler $preCompiler, - callable $outputHandler, - Filesystem $filesystem + private Io $io, + private Config $config, + private Finder $packageManagerFinder, + private ProcessExecutor $executor, + private ParallelManager $parallelManager, + private Locker $locker, + private PreCompilation\Handler $preCompiler, + private Filesystem $filesystem, + callable $outputHandler ) { - $this->io = $io; - $this->config = $config; - $this->packageManagerFinder = $packageManagerFinder; - $this->executor = $executor; - $this->parallelManager = $parallelManager; - $this->locker = $locker; - $this->preCompiler = $preCompiler; $this->outputHandler = $outputHandler; - $this->filesystem = $filesystem; } /** @@ -163,72 +110,91 @@ public function process(\Iterator $assets): bool } $toWipe = []; - $stopOnFailure = $rootConfig->stopOnFailure(); - $return = true; - $processManager = $this->parallelManager; - + $manager = $this->parallelManager; foreach ($assets as $asset) { - if (!($asset instanceof Asset) && $stopOnFailure) { - throw new \Exception('Invalid data to process.'); + [$ok, $manager, $toWipe] = $this->processAsset($asset, $rootConfig, $manager, $toWipe); + if ($ok === false) { + return false; } + } - [$name, $path, $shouldWipe] = ($asset instanceof Asset) - ? $this->assetProcessInfo($asset, $rootConfig) - : [null, null, null]; - if (!$name || !$path || ($shouldWipe === null)) { - continue; - } + $results = $manager->execute($this->io, $rootConfig->stopOnFailure()); - /** @var Asset $asset */ - if ($this->maybeSkipAsset($asset)) { - continue; - } + return $this->handleResults($results, $toWipe); + } - try { - $commands = $this->findCommandsForAsset($asset, $rootConfig); - } catch (\Throwable $throwable) { - $this->io->writeError("Could not find a package manager on the system."); + /** + * @param mixed $asset + * @param RootConfig $rootConfig + * @param ParallelManager $manager + * @param array $toWipe + * @param bool $stopOnFailure + * @return list{bool, ParallelManager, array} + */ + private function processAsset( + mixed $asset, + RootConfig $rootConfig, + ParallelManager $manager, + array $toWipe + ): array { - return false; - } + if (!($asset instanceof Asset) && $rootConfig->stopOnFailure()) { + throw new \Exception('Invalid data to process.'); + } + + [$name, $path, $shouldWipe] = ($asset instanceof Asset) + ? $this->assetProcessInfo($asset, $rootConfig) + : [null, null, null]; + if (($name === null) || ($path === null) || ($shouldWipe === null)) { + return [true, $manager, $toWipe]; + } - $installedDeps = $this->doDependencies($asset, $commands, $rootConfig); + /** @var Asset $asset */ + if ($this->maybeSkipAsset($asset)) { + return [true, $manager, $toWipe]; + } - if (!$installedDeps && $stopOnFailure) { - return false; - } + try { + $commands = $this->findCommandsForAsset($asset, $rootConfig); + } catch (\Throwable) { + $this->io->writeError("Could not find a package manager on the system."); - $return = $installedDeps && $return; - $commandStrings = $this->buildScriptCommands($asset, $commands); + return [false, $manager, $toWipe]; + } - // No script, we can lock already - if (!$commandStrings) { - $this->locker->lock($asset); - $shouldWipe and $this->wipeNodeModules($path); + $installedDeps = $this->doDependencies($asset, $commands, $rootConfig); - continue; - } + if (!$installedDeps) { + return [!$rootConfig->stopOnFailure(), $manager, $toWipe]; + } - $processManager = $processManager->pushAssetToProcess($asset, ...$commandStrings); - $shouldWipe and $toWipe[$name] = $shouldWipe; + $commandStrings = $this->buildScriptCommands($asset, $commands) ?? []; + + // No script, we can lock already + if ($commandStrings === []) { + $this->locker->lock($asset); + $shouldWipe and $this->wipeNodeModules($path); + + return [true, $manager, $toWipe]; } - $results = $processManager->execute($this->io, $stopOnFailure); + $manager = $manager->pushAssetToProcess($asset, ...$commandStrings); + $shouldWipe and $toWipe[$name] = $shouldWipe; - return $this->handleResults($results, $toWipe) && $return; + return [true, $manager, $toWipe]; } /** * @param Asset $asset * @param RootConfig $root - * @return array{string|null, string|null, bool|null} + * @return list{non-empty-string,non-empty-string,bool}|list{null,null,null} */ private function assetProcessInfo(Asset $asset, RootConfig $root): array { $name = $asset->name(); - $path = $asset->path(); + $path = $asset->path() ?? ''; - if (!$name || !$path) { + if (($name === '') || ($path === '')) { return [null, null, null]; } @@ -319,8 +285,8 @@ private function doDependencies( return true; } - $cwd = $asset->path(); - if (!$cwd || !is_dir($cwd)) { + $cwd = $asset->path() ?? ''; + if (($cwd === '') || !is_dir($cwd)) { return false; } @@ -328,7 +294,7 @@ private function doDependencies( ? $packageManager->updateCmd($this->io) : $packageManager->installCmd($this->io); - if (!$command) { + if (($command === null) || ($command === '')) { return false; } @@ -377,7 +343,7 @@ private function handleIsolatedCache( $isYarn = $packageManager->isYarn(); $cmdName = $packageManager->name(); $cacheParam = $isYarn ? 'cache-folder' : 'cache'; - if (strpos($command, " --{$cacheParam}") !== false) { + if (str_contains($command, " --{$cacheParam}")) { return $command; } @@ -387,7 +353,7 @@ private function handleIsolatedCache( try { $fullPath and $this->filesystem->ensureDirectoryExists($fullPath); - } catch (\Throwable $throwable) { + } catch (\Throwable) { $flushCache = true; } @@ -460,8 +426,8 @@ private function handleResults(Results $results, array $toWipe): bool [, $asset] = $success; $this->locker->lock($asset); if (!empty($toWipe[$asset->name()])) { - $path = $asset->path(); - $path and $this->wipeNodeModules($path); + $path = $asset->path() ?? ''; + ($path !== '') and $this->wipeNodeModules($path); } } @@ -482,8 +448,8 @@ private function buildScriptCommands(Asset $asset, PackageManager $packageManage $assetCommands = []; foreach ($scripts as $script) { - $command = $packageManager->scriptCmd($script, $asset->env()); - $command and $assetCommands[] = $command; + $command = $packageManager->scriptCmd($script, $asset->env()) ?? ''; + ($command !== '') and $assetCommands[] = $command; } $commandsStr = implode(' && ', $assetCommands); diff --git a/src/Asset/RootConfig.php b/src/Asset/RootConfig.php index b10c3d6..05ef209 100644 --- a/src/Asset/RootConfig.php +++ b/src/Asset/RootConfig.php @@ -37,36 +37,6 @@ final class RootConfig self::TIMEOUT_INCR => 'COMPOSER_ASSET_COMPILER_TIMEOUT_INCR', ]; - /** - * @var string - */ - private $name; - - /** - * @var string - */ - private $path; - - /** - * @var array - */ - private $raw; - - /** - * @var ModeResolver - */ - private $modeResolver; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var array - */ - private $defaultEnv; - /** * @param string $name * @param string $path @@ -85,32 +55,25 @@ public static function new( array $defaultEnv = [] ): RootConfig { - return new static($name, $path, $data, $modeResolver, $filesystem, $defaultEnv); + return new static($name, rtrim($path, '/'), $data, $modeResolver, $filesystem, $defaultEnv); } /** * @param string $name * @param string $path - * @param array $data + * @param array $raw * @param ModeResolver $modeResolver * @param Filesystem $filesystem * @param array $defaultEnv */ private function __construct( - string $name, - string $path, - array $data, - ModeResolver $modeResolver, - Filesystem $filesystem, - array $defaultEnv + private string $name, + private string $path, + private array $raw, + private ModeResolver $modeResolver, + private Filesystem $filesystem, + private array $defaultEnv ) { - - $this->name = $name; - $this->path = rtrim($path, '/'); - $this->raw = $data; - $this->modeResolver = $modeResolver; - $this->filesystem = $filesystem; - $this->defaultEnv = $defaultEnv; } /** @@ -154,7 +117,7 @@ public function autoDiscover(): bool { $config = $this->resolveByMode(self::AUTO_DISCOVER, false, true); - return (bool)filter_var($config, FILTER_VALIDATE_BOOLEAN); + return filter_var($config, FILTER_VALIDATE_BOOLEAN); } /** @@ -164,7 +127,7 @@ public function autoRun(): bool { $config = $this->resolveByMode(self::AUTO_RUN, false, false); - return (bool)filter_var($config, FILTER_VALIDATE_BOOLEAN); + return filter_var($config, FILTER_VALIDATE_BOOLEAN); } /** @@ -185,7 +148,7 @@ public function stopOnFailure(): bool { $config = $this->resolveByMode(self::STOP_ON_FAILURE, false, true); - return (bool)filter_var($config, FILTER_VALIDATE_BOOLEAN); + return filter_var($config, FILTER_VALIDATE_BOOLEAN); } /** @@ -194,7 +157,7 @@ public function stopOnFailure(): bool public function maxProcesses(): int { $config = $this->resolveByMode(self::MAX_PROCESSES, false, 4); - $maxProcesses = is_numeric($config) ? (int)$config : 4; + $maxProcesses = is_numeric($config) ? (int) $config : 4; ($maxProcesses < 1) and $maxProcesses = 1; return $maxProcesses; @@ -206,7 +169,7 @@ public function maxProcesses(): int public function processesPoll(): int { $config = $this->resolveByMode(self::PROCESSES_POLL, false, 100000); - $poll = is_numeric($config) ? (int)$config : 100000; + $poll = is_numeric($config) ? (int) $config : 100000; ($poll <= 10000) and $poll = 100000; return $poll; @@ -219,7 +182,7 @@ public function timeoutIncrement(): int { $config = $this->resolveByMode(self::TIMEOUT_INCR, false, null); - $incr = is_numeric($config) ? (int)$config : 300; + $incr = is_numeric($config) ? (int) $config : 300; return min(max(30, $incr), 3600); } @@ -259,7 +222,7 @@ public function isWipeAllowedFor(string $packageFolder): bool * @param mixed $default * @return mixed */ - private function resolveByMode(string $key, bool $allowedArray, $default) + private function resolveByMode(string $key, bool $allowedArray, mixed $default): mixed { $config = $this->raw[$key] ?? null; if (($config === null) && (self::BY_ENV[$key] ?? null)) { diff --git a/src/Composer/Command/AssetHash.php b/src/Composer/Command/AssetHash.php index aa28628..00d8df9 100644 --- a/src/Composer/Command/AssetHash.php +++ b/src/Composer/Command/AssetHash.php @@ -11,71 +11,38 @@ namespace Inpsyde\AssetsCompiler\Composer\Command; -use Composer\Command\BaseCommand; -use Inpsyde\AssetsCompiler\Util\Factory; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; final class AssetHash extends BaseCommand { - use LowLevelErrorWriteTrait; - use ModeOptionTrait; - use ObtainComposerTrait; - /** * @return void */ - protected function configure() + protected function configure(): void { $this - ->setName('assets-hash') - ->setDescription('Calculate assets hash for root package in current environment.') - ->addOption( - 'no-dev', - null, - InputOption::VALUE_NONE, - 'Tell the command to fallback to no-dev mode configuration.' - ) - ->addOption( - 'mode', - null, - InputOption::VALUE_REQUIRED, - 'Set the mode to run command in. ' - . 'Overrides value of COMPOSER_ASSETS_COMPILER, if set.' - ) - ->addOption( - 'env', - null, - InputOption::VALUE_REQUIRED, - 'DEPRECATED. Use "mode" instead' - ); + ->configureCommon() + ->setName('asset-hash') + ->setDescription('Calculate assets hash for root package in current environment.'); } /** * @param InputInterface $input * @param OutputInterface $output - * @return int - * - * phpcs:disable Inpsyde.CodeQuality.ReturnTypeDeclaration + * @return 0|1 */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - // phpcs:enable - try { - $composer = $this->obtainComposer(); - $io = $this->getIO(); - $noDev = $input->hasOption('no-dev'); - $mode = $this->determineMode($input, $output); + $factory = $this->createFactory($input); - $factory = Factory::new($composer, $io, $mode, !$noDev); - $package = $composer->getPackage(); + $package = $this->requireComposer(false)->getPackage(); $defaults = $factory->defaults(); $asset = $factory->assetFactory()->attemptFactory($package, null, $defaults); $hash = $asset ? $factory->hashBuilder()->forAsset($asset) : null; - if (!$hash) { + if ($hash === null) { throw new \Error('Could not generate a hash for the package.'); } diff --git a/src/Composer/Command/AssetsInfo.php b/src/Composer/Command/AssetsInfo.php new file mode 100644 index 0000000..a5a7794 --- /dev/null +++ b/src/Composer/Command/AssetsInfo.php @@ -0,0 +1,311 @@ +configureCommon() + ->setName('assets-info') + ->setDescription('Gets assets compilation information.') + ->addArgument( + 'asset', + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'Limits info to given asset(s) only. Can not be used with --root flag.', + null + ) + ->addOption( + 'root', + null, + InputOption::VALUE_NONE, + 'Limits info to root asset only. Can not be used when passing asset names.' + ) + ->addOption( + 'table', + null, + InputOption::VALUE_NONE, + 'Prints output as table. Use --fields flag to get readable results.' + ) + ->addOption( + 'fields', + null, + InputOption::VALUE_REQUIRED, + 'Comma-separated fields to print.' + ) + ->addOption( + 'all-watchable-paths', + null, + InputOption::VALUE_NONE, + 'Return only watchable paths for all assets.' + . ' Can not be used in combination with asset names or --fields flag.' + ); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return 0|1 + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + $factory = $this->createFactory($input); + [$pathsOnly, $doRoot, $doAssets, $doOneAsset, $format] = $this->parseParams($input); + + $info = $factory->assetsInfo(); + + if ($pathsOnly) { + return $this->executePathsOnly($factory, $format, $output); + } + + /** @var non-empty-list $data */ + $data = match (true) { + ($doOneAsset !== null) => [$info->assetInfo($doOneAsset)], + ($doAssets !== []) => $info->assetInfo(...$doAssets), + $doRoot => [$info->rootAssetInfo()], + default => $info->allAssetsInfo(), + }; + + $data = $this->filterData($data, $input); + $this->format($factory->io(), $output, $format, $data); + + return 0; + } catch (\Throwable $throwable) { + $this->writeError($output, $throwable->getMessage()); + + return 1; + } + } + + /** + * @param InputInterface $input + * @return list{bool, bool, list, non-empty-string|null, 'table'|'json'} + */ + private function parseParams(InputInterface $input): array + { + $pathsOnly = $input->hasParameterOption('--all-watchable-paths'); + $doRoot = $input->hasParameterOption('--root'); + $doAssets = $this->targetAssetNames($input); + $doOneAsset = (count($doAssets) === 1) ? $doAssets[0] : null; + $format = $input->hasParameterOption('--table') ? 'table' : 'json'; + + if ( + $pathsOnly + && ($doRoot || ($doAssets !== []) || $input->hasParameterOption('--fields')) + ) { + throw new \Error( + '--all-watchable-paths flag can not be used when passing asset names nor' + . ' when using --fields --root flags.' + ); + } + + if ($doRoot && ($doAssets !== [])) { + throw new \Error('--root flag can not be used when passing asset names.'); + } + + return [$pathsOnly, $doRoot, $doAssets, $doOneAsset, $format]; + } + + /** + * @param Factory $factory + * @param string $format + * @param OutputInterface $output + * @return 0 + */ + private function executePathsOnly( + Factory $factory, + string $format, + OutputInterface $output + ): int { + + $paths = $factory->assetsPathsFinder()->findAllAssetsPaths(); + $paths = ($format === 'table') ? [compact('paths')] : [$paths]; + + $this->format($factory->io(), $output, $format, $paths); + + return 0; + } + + /** + * @param non-empty-list $data + * @param InputInterface $input + * @return non-empty-list + */ + private function filterData(array $data, InputInterface $input): array + { + if (!$input->hasParameterOption('--fields')) { + return $data; + } + + $input = $input->getOption('fields'); + if (($input === '') || !is_string($input)) { + return $data; + } + + $fields = []; + foreach (explode(',', $input) as $field) { + $field = strtolower(trim($field)); + if (($field !== '') && !isset($fields[$field])) { + $fields[$field] = true; + } + } + + if ($fields === []) { + return $data; + } + + $filtered = []; + foreach ($data as $item) { + $filteredItem = array_intersect_key($item, $fields); + if ($filteredItem === []) { + throw new \Error("Invalid --fields flag: '{$input}'."); + } + $filtered[] = $filteredItem; + } + + return $filtered; + } + + /** + * @param InputInterface $input + * @return list + */ + private function targetAssetNames(InputInterface $input): array + { + if (!$input->hasArgument('asset')) { + return []; + } + + $input = $input->getArgument('asset'); + if (is_string($input) && ($input !== '')) { + return [$input]; + } + + if (!is_array($input)) { + return []; + } + + $inputs = []; + foreach ($input as $asset) { + if (is_string($asset) && ($asset !== '')) { + $inputs[] = $asset; + } + } + + return $inputs; + } + + /** + * @param Io $io + * @param OutputInterface $output + * @param string $format + * @param non-empty-list $data + * @return void + */ + private function format(Io $io, OutputInterface $output, string $format, array $data): void + { + match ($format) { + 'json' => $this->printJson($data, $io), + default => $this->printTable($data, $output), + }; + } + + /** + * @param non-empty-list $data + * @param Io $io + * @return void + */ + private function printJson(array $data, Io $io): void + { + if (count($data) === 1) { + $data = array_pop($data); + } + + $json = json_encode( + $data, + \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES + ); + $io->writeRaw($json); + } + + /** + * @param non-empty-list $data + * @param OutputInterface $output + * @return void + */ + private function printTable(array $data, OutputInterface $output): void + { + $table = new Table($output); + $table->setHeaders(array_keys($data[0])); + foreach ($data as $row) { + $table->addRow($this->formatTableRow($row)); + } + + $table->render(); + } + + /** + * @param array $row + * @return list + */ + private function formatTableRow(array $row): array + { + $cols = []; + + foreach ($row as $col) { + if (($col === null) || is_scalar($col)) { + $cols[] = $col; + continue; + } + if (!is_array($col)) { + $cols[] = get_debug_type($col); + continue; + } + + if ($col === []) { + $cols[] = '[]'; + continue; + } + + $safeCol = []; + foreach ($col as $key => $value) { + $safeCol[$key] = is_scalar($value) ? (string) $value : get_debug_type($value); + } + + if (str_starts_with((string) json_encode($safeCol), '[')) { + $cols[] = '- ' . implode("\n- ", $safeCol); + continue; + } + + $formatted = ''; + foreach ($safeCol as $key => $value) { + $formatted .= sprintf("- %s: %s\n", $key, $value); + } + $cols[] = rtrim($formatted); + } + + return $cols; + } +} diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php new file mode 100644 index 0000000..5574774 --- /dev/null +++ b/src/Composer/Command/BaseCommand.php @@ -0,0 +1,102 @@ +addOption( + 'no-dev', + null, + InputOption::VALUE_NONE, + 'Tell the command to fallback to no-dev mode configuration.' + ); + $this->addOption( + 'mode', + null, + InputOption::VALUE_REQUIRED, + 'Set the mode to run command in. Overrides value of COMPOSER_ASSETS_COMPILER, if set.' + ); + + return $this; + } + + /** + * @param InputInterface $input + * @return Factory + */ + protected function createFactory(InputInterface $input): Factory + { + return Factory::new( + $this->requireComposer(false), + $this->getIO(), + $this->determineMode($input), + !$input->hasOption('no-dev') + ); + } + + /** + * @param InputInterface $input + * @return string|null + */ + protected function determineMode(InputInterface $input): ?string + { + $mode = $input->hasParameterOption('--mode') ? $input->getOption('mode') : null; + is_string($mode) or $mode = null; + + return $mode; + } + + /** + * @param OutputInterface $output + * @param string $message + * @return void + */ + protected function writeError(OutputInterface $output, string $message): void + { + $words = explode(' ', $message); + $lines = []; + $line = ''; + foreach ($words as $word) { + if (strlen($line . $word) < 60) { + $line .= $line ? " {$word}" : $word; + continue; + } + + $lines[] = " {$line} "; + $line = $word; + } + + $line and $lines[] = " {$line} "; + + $lenMax = $lines ? max(array_map('strlen', $lines)) : 1; + $empty = '' . str_repeat(' ', $lenMax) . ''; + $errors = ['', $empty]; + foreach ($lines as $line) { + $lineLen = strlen($line); + ($lineLen < $lenMax) and $line .= str_repeat(' ', $lenMax - $lineLen); + $errors[] = "{$line}"; + } + + $errors[] = $empty; + $errors[] = ''; + + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + $output->writeln($errors); + } +} diff --git a/src/Composer/Command/CompileAssets.php b/src/Composer/Command/CompileAssets.php index 0d1e6c2..5464ff2 100644 --- a/src/Composer/Command/CompileAssets.php +++ b/src/Composer/Command/CompileAssets.php @@ -11,8 +11,6 @@ namespace Inpsyde\AssetsCompiler\Composer\Command; -use Composer\Command\BaseCommand; -use Composer\Composer; use Inpsyde\AssetsCompiler\Asset\Locker; use Inpsyde\AssetsCompiler\Composer\Plugin; use Symfony\Component\Console\Input\InputInterface; @@ -21,37 +19,15 @@ final class CompileAssets extends BaseCommand { - use LowLevelErrorWriteTrait; - use ModeOptionTrait; - use ObtainComposerTrait; - /** * @return void */ - protected function configure() + protected function configure(): void { $this + ->configureCommon() ->setName('compile-assets') ->setDescription('Run assets compilation workflow.') - ->addOption( - 'no-dev', - null, - InputOption::VALUE_NONE, - 'Tell the command to fallback to no-dev mode configuration.' - ) - ->addOption( - 'mode', - null, - InputOption::VALUE_REQUIRED, - 'Set the mode to run command in. ' - . 'Overrides value of COMPOSER_ASSETS_COMPILER, if set.' - ) - ->addOption( - 'env', - null, - InputOption::VALUE_REQUIRED, - 'DEPRECATED. Use "mode" instead' - ) ->addOption( 'ignore-lock', null, @@ -64,28 +40,24 @@ protected function configure() /** * @param InputInterface $input * @param OutputInterface $output - * @return int - * - * phpcs:disable Inpsyde.CodeQuality.ReturnTypeDeclaration + * @return 0|1 */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - // phpcs:enable Inpsyde.CodeQuality.ReturnTypeDeclaration - try { - $composer = $this->obtainComposer(); + $composer = $this->requireComposer(false); $io = $this->getIO(); $plugin = new Plugin(); $plugin->activate($composer, $io); $noDev = $input->hasOption('no-dev'); - $mode = $this->determineMode($input, $output); + $mode = $this->determineMode($input); $ignoreLockRaw = $input->hasParameterOption('--ignore-lock', true) ? $input->getOption('ignore-lock') : null; - $ignoreLock = ($ignoreLockRaw && is_string($ignoreLockRaw)) ? $ignoreLockRaw : ''; + $ignoreLock = is_string($ignoreLockRaw) ? $ignoreLockRaw : ''; ($ignoreLock === '*/*') and $ignoreLock = Locker::IGNORE_ALL; $plugin->runByCommand( diff --git a/src/Composer/Command/LowLevelErrorWriteTrait.php b/src/Composer/Command/LowLevelErrorWriteTrait.php deleted file mode 100644 index 6136762..0000000 --- a/src/Composer/Command/LowLevelErrorWriteTrait.php +++ /dev/null @@ -1,59 +0,0 @@ -' . str_repeat(' ', $lenMax) . ''; - $errors = ['', $empty]; - foreach ($lines as $line) { - $lineLen = strlen($line); - ($lineLen < $lenMax) and $line .= str_repeat(' ', $lenMax - $lineLen); - $errors[] = "{$line}"; - } - - $errors[] = $empty; - $errors[] = ''; - - if ($output instanceof ConsoleOutputInterface) { - $output = $output->getErrorOutput(); - } - - $output->writeln($errors); - } -} diff --git a/src/Composer/Command/ModeOptionTrait.php b/src/Composer/Command/ModeOptionTrait.php deleted file mode 100644 index 6062f21..0000000 --- a/src/Composer/Command/ModeOptionTrait.php +++ /dev/null @@ -1,45 +0,0 @@ -hasOption('mode') ? $input->getOption('mode') : null; - is_string($mode) or $mode = null; - if ($mode !== null) { - return $mode; - } - - if (!$input->hasOption('env')) { - return null; - } - - $env = $input->getOption('env'); - is_string($env) or $env = null; - - if ($env !== null) { - $output->writeln("Option 'env' is deprecated, please use 'mode' instead"); - } - - return $env; - } -} diff --git a/src/Composer/Command/ObtainComposerTrait.php b/src/Composer/Command/ObtainComposerTrait.php deleted file mode 100644 index 1bfa56f..0000000 --- a/src/Composer/Command/ObtainComposerTrait.php +++ /dev/null @@ -1,28 +0,0 @@ -requireComposer(false); - } - - /** - * @psalm-suppress DeprecatedMethod - * @var Composer $composer - */ - $composer = $this->getComposer(true, false); - - return $composer; - } -} diff --git a/src/Composer/Plugin.php b/src/Composer/Plugin.php index 18b1a68..9ffa60d 100644 --- a/src/Composer/Plugin.php +++ b/src/Composer/Plugin.php @@ -38,23 +38,12 @@ final class Plugin implements private const MODE_COMPOSER_INSTALL = 4; private const MODE_COMPOSER_UPDATE = 8; - /** - * @var IOInterface - */ - private $io; - - /** - * @var Composer - */ - private $composer; + private IOInterface $io; + private Composer $composer; + private int $mode = self::MODE_NONE; /** - * @var int - */ - private $mode = self::MODE_NONE; - - /** - * @return array> + * @return array> * * @see Plugin::onAutorunBecauseInstall() * @see Plugin::onAutorunBecauseUpdate() @@ -72,7 +61,7 @@ public static function getSubscribedEvents(): array } /** - * @return array + * @return array */ public function getCapabilities(): array { @@ -80,18 +69,22 @@ public function getCapabilities(): array } /** - * @return array + * @return list */ public function getCommands(): array { - return [new Command\CompileAssets(), new Command\AssetHash()]; + return [ + new Command\CompileAssets(), + new Command\AssetHash(), + new Command\AssetsInfo(), + ]; } /** * @param Composer $composer * @param IOInterface $io */ - public function activate(Composer $composer, IOInterface $io) + public function activate(Composer $composer, IOInterface $io): void { $this->composer = $composer; $this->io = $io; @@ -120,19 +113,19 @@ public function onAutorunBecauseUpdate(Event $event): void } /** - * @param string|null $env + * @param string|null $mode * @param bool $isDev * @param string $ignoreLock * @return void */ public function runByCommand( - ?string $env, + ?string $mode, bool $isDev, string $ignoreLock = '' ): void { $this->mode = self::MODE_COMMAND; - $this->run(Factory::new($this->composer, $this->io, $env, $isDev, $ignoreLock)); + $this->run(Factory::new($this->composer, $this->io, $mode, $isDev, $ignoreLock)); } /** @@ -224,7 +217,7 @@ static function (int $severity, string $msg, string $file = '', int $line = 0): * @param IOInterface $io * @return void */ - public function deactivate(Composer $composer, IOInterface $io) + public function deactivate(Composer $composer, IOInterface $io): void { // noop } @@ -234,7 +227,7 @@ public function deactivate(Composer $composer, IOInterface $io) * @param IOInterface $io * @return void */ - public function uninstall(Composer $composer, IOInterface $io) + public function uninstall(Composer $composer, IOInterface $io): void { // noop } diff --git a/src/PackageManager/Finder.php b/src/PackageManager/Finder.php index 9021773..861733c 100644 --- a/src/PackageManager/Finder.php +++ b/src/PackageManager/Finder.php @@ -20,30 +20,7 @@ class Finder { - /** - * @var ProcessExecutor - */ - private $executor; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var Io - */ - private $io; - - /** - * @var array - */ - private $defaultEnv; - - /** - * @var PackageManager|null - */ - private $discovered; + private PackageManager|null $discovered = null; /** * @param ProcessExecutor $executor @@ -70,16 +47,11 @@ public static function new( * @param array $defaultEnv */ private function __construct( - ProcessExecutor $executor, - Filesystem $filesystem, - Io $io, - array $defaultEnv + private ProcessExecutor $executor, + private Filesystem $filesystem, + private Io $io, + private array $defaultEnv ) { - - $this->executor = $executor; - $this->filesystem = $filesystem; - $this->io = $io; - $this->defaultEnv = $defaultEnv; } /** @@ -95,22 +67,21 @@ public function findForConfig(Config $config, string $name, string $path): Packa $manager = $config->packageManager(); if ($manager && $this->checkIsValid($manager, $name, $path, true, true)) { - return $manager->withDefaultEnv($this->defaultEnv); + return $manager->withDefaultEnv(); } $manager = null; - switch (true) { - case file_exists("{$path}/package-lock.json"): - case file_exists("{$path}/npm-shrinkwrap.json"): - $manager = PackageManager::fromDefault(PackageManager::NPM); - break; - case file_exists("{$path}/yarn.lock"): - $manager = PackageManager::fromDefault(PackageManager::YARN); - break; + if ( + file_exists("{$path}/npm-shrinkwrap.json") + || file_exists("{$path}/package-lock.json") + ) { + $manager = PackageManager::fromDefault(PackageManager::NPM); + } elseif (file_exists("{$path}/yarn.lock")) { + $manager = PackageManager::fromDefault(PackageManager::YARN); } if ($manager && $this->checkIsValid($manager, $name, $path, false, true)) { - return $manager->withDefaultEnv($this->defaultEnv); + return $manager->withDefaultEnv(); } $this->io->writeVerbose("Will use default package manager for {$name}."); @@ -119,7 +90,7 @@ public function findForConfig(Config $config, string $name, string $path): Packa $manager = $this->discovered; if ($this->checkIsValid($manager, $name, $path, true, false)) { - return $manager->withDefaultEnv($this->defaultEnv); + return $manager->withDefaultEnv(); } $this->io->writeError( @@ -136,10 +107,10 @@ public function findForConfig(Config $config, string $name, string $path): Packa */ public function findForAsset(Asset $asset): PackageManager { - $path = $asset->path(); + $path = $asset->path() ?? ''; $config = $asset->config(); - if (!$path || !$config) { - return PackageManager::new([], []); + if (($path === '') || ($config === null)) { + return PackageManager::new([]); } return $this->findForConfig($config, $asset->name(), $path); @@ -179,7 +150,7 @@ private function checkIsValid( $error .= "but that package manager is not available on the system."; } - if (!$valid && $error) { + if (!$valid && ($error !== null)) { $this->io->writeVerboseError($error); } diff --git a/src/PackageManager/PackageManager.php b/src/PackageManager/PackageManager.php index 45e8340..619d5de 100644 --- a/src/PackageManager/PackageManager.php +++ b/src/PackageManager/PackageManager.php @@ -49,35 +49,14 @@ final class PackageManager ], ]; - /** - * @var list|null - */ - private static $tested = null; - - /** - * @var array{update: null|string, install: null|string} - */ - private $dependencies; - - /** - * @var string|null - */ - private $script; + /** @var list|null */ + private static array|null $tested = null; - /** - * @var string - */ - private $cacheClean; - - /** - * @var array - */ - private $defaultEnvironment; - - /** - * @var string|null - */ - private $name; + /** @var array{update: null|string, install: null|string} */ + private array $dependencies; + private string|null $script; + private string $cacheClean = ''; + private string|null $name = null; /** * @param ProcessExecutor $executor @@ -104,58 +83,52 @@ public static function test(ProcessExecutor $executor, ?string $workingDir = nul /** * @param string $manager - * @param array $defaultEnv * @return PackageManager */ - public static function fromDefault(string $manager, array $defaultEnv = []): PackageManager + public static function fromDefault(string $manager): PackageManager { $manager = strtolower($manager); if (!array_key_exists($manager, self::SUPPORTED_DEFAULTS)) { - return new self([], $defaultEnv); + return new self([]); } - return new self(self::SUPPORTED_DEFAULTS[$manager], $defaultEnv); + return new self(self::SUPPORTED_DEFAULTS[$manager]); } /** * @param ProcessExecutor $executor * @param string $workingDir - * @param array $defaultEnvironment * @return PackageManager */ public static function discover( ProcessExecutor $executor, - string $workingDir, - array $defaultEnvironment = [] + string $workingDir ): PackageManager { $tested = self::test($executor, $workingDir); if ($tested === []) { - return new self([], $defaultEnvironment); + return new self([]); } - return static::fromDefault(reset($tested), $defaultEnvironment); + return static::fromDefault(reset($tested)); } /** * @param array $config - * @param array $defaultEnvironment * @return PackageManager */ - public static function new(array $config, array $defaultEnvironment = []): PackageManager + public static function new(array $config): PackageManager { - return new self($config, $defaultEnvironment); + return new self($config); } /** * @param array $config - * @param array $defaultEnvironment */ - private function __construct(array $config, array $defaultEnvironment = []) + private function __construct(array $config) { $this->reset(); - $this->defaultEnvironment = $defaultEnvironment; $this->dependencies = $this->parseDependencies($config); $this->script = $this->parseScript($config); @@ -179,17 +152,27 @@ private function __construct(array $config, array $defaultEnvironment = []) $clean = $config[self::CLEAN_CACHE] ?? null; $defaults = self::SUPPORTED_DEFAULTS[$isYarn ? self::YARN : self::NPM]; - $this->cacheClean = ($clean && is_string($clean)) ? $clean : $defaults[self::CLEAN_CACHE]; + $this->cacheClean = (($clean !== '') && is_string($clean)) + ? $clean + : $defaults[self::CLEAN_CACHE]; } /** * @return bool * * @psalm-assert-if-true non-empty-string $this->script + * @psalm-assert-if-true array{ + * install:non-empty-string, + * update:non-empty-string + * } $this->dependencies */ public function isValid(): bool { - return !empty($this->dependencies[self::DEPS_INSTALL]) && $this->scriptCmd('test'); + $script = $this->scriptCmd('test') ?? ''; + $install = $this->dependencies[self::DEPS_INSTALL] ?? ''; + $update = $this->dependencies[self::DEPS_UPDATE] ?? ''; + + return ($script !== '') && ($install !== '') && ($update !== ''); } /** @@ -201,17 +184,14 @@ public function isNpm(): bool return false; } - if ($this->name) { - return $this->name === self::NPM; - } - - if ($this->script && stripos($this->script, 'npm') !== false) { - $this->name = self::NPM; - + if ($this->name === self::NPM) { return true; } - return false; + $isNpm = str_contains($this->script, 'npm'); + $isNpm and $this->name = self::NPM; + + return $isNpm; } /** @@ -223,17 +203,14 @@ public function isYarn(): bool return false; } - if ($this->name) { - return $this->name === self::YARN; - } - - if ($this->script && stripos($this->script, 'yarn') !== false) { - $this->name = self::YARN; - + if ($this->name === self::YARN) { return true; } - return false; + $isYarn = str_contains($this->script, 'yarn'); + $isYarn and $this->name = self::YARN; + + return $isYarn; } /** @@ -252,12 +229,9 @@ public function name(): string * @param array $environment * @return PackageManager */ - public function withDefaultEnv(array $environment): PackageManager + public function withDefaultEnv(): PackageManager { - return new self( - [self::DEPENDENCIES => $this->dependencies, self::SCRIPT => $this->script], - $environment - ); + return new self([self::DEPENDENCIES => $this->dependencies, self::SCRIPT => $this->script]); } /** @@ -293,7 +267,7 @@ public function cleanCacheCmd(): string */ public function scriptCmd(string $command, array $env = []): ?string { - if (!$this->script) { + if (($this->script === null) || ($this->script === '')) { return null; } @@ -350,7 +324,6 @@ private function reset(): void $this->dependencies = [self::DEPS_INSTALL => null, self::DEPS_UPDATE => null]; $this->name = null; $this->cacheClean = ''; - $this->defaultEnvironment = []; } /** @@ -362,7 +335,7 @@ private function parseDependencies(array $config): array $dependencies = $config[self::DEPENDENCIES] ?? null; $install = null; $update = null; - if ($dependencies && is_array($dependencies)) { + if (($dependencies !== []) && is_array($dependencies)) { $install = $dependencies[self::DEPS_INSTALL] ?? null; $update = $dependencies[self::DEPS_UPDATE] ?? null; (($install !== '') && is_string($install)) or $install = null; @@ -372,9 +345,9 @@ private function parseDependencies(array $config): array /** @var non-empty-string|null $install */ /** @var non-empty-string|null $update */ - if (($install === null) && $update) { + if (($install === null) && ($update !== null)) { $install = $update; - } elseif (($update === null) && $install) { + } elseif (($update === null) && ($install !== null)) { $update = $install; } @@ -402,7 +375,7 @@ private function parseScript(array $config): ?string */ private function maybeVerbose(?string $cmd, Io $io): ?string { - if (!$cmd) { + if (($cmd === null) || ($cmd === '')) { return $cmd; } @@ -455,15 +428,11 @@ private function maybeVerboseNpm(string $cmd, Io $io): string return $cmd; } - switch (true) { - case $io->isQuiet(): - return "{$cmd} --silent"; - case $io->isVeryVeryVerbose(): - return "{$cmd} -ddd"; - case $io->isVeryVerbose(): - return "{$cmd} -dd"; - } - - return $io->isVerbose() ? "{$cmd} -d" : $cmd; + return match (true) { + $io->isQuiet() => "{$cmd} --silent", + $io->isVeryVeryVerbose() => "{$cmd} -ddd", + $io->isVeryVerbose() => "{$cmd} -dd", + default => $io->isVerbose() ? "{$cmd} -d" : $cmd, + }; } } diff --git a/src/PreCompilation/Adapter.php b/src/PreCompilation/Adapter.php index 1abf383..0851df9 100644 --- a/src/PreCompilation/Adapter.php +++ b/src/PreCompilation/Adapter.php @@ -7,6 +7,8 @@ * file that was distributed with this source code. */ +declare(strict_types=1); + namespace Inpsyde\AssetsCompiler\PreCompilation; use Inpsyde\AssetsCompiler\Asset\Asset; diff --git a/src/PreCompilation/ArchiveDownloaderAdapter.php b/src/PreCompilation/ArchiveDownloaderAdapter.php index eec2465..7468433 100644 --- a/src/PreCompilation/ArchiveDownloaderAdapter.php +++ b/src/PreCompilation/ArchiveDownloaderAdapter.php @@ -19,16 +19,6 @@ class ArchiveDownloaderAdapter implements Adapter { - /** - * @var Io - */ - private $io; - - /** - * @var ArchiveDownloaderFactory - */ - private $downloaderFactory; - /** * @param Io $io * @param ArchiveDownloaderFactory $downloaderFactory @@ -47,12 +37,9 @@ public static function new( * @param ArchiveDownloaderFactory $downloaderFactory */ private function __construct( - Io $io, - ArchiveDownloaderFactory $downloaderFactory + private Io $io, + private ArchiveDownloaderFactory $downloaderFactory ) { - - $this->io = $io; - $this->downloaderFactory = $downloaderFactory; } /** @@ -95,9 +82,11 @@ public function tryPrecompiled( /** @var string $type */ - $safeSource = $this->sanitizeSource($source); - [$distUrl, $auth] = $safeSource ? $this->extractAuth($safeSource, $config) : [null, null]; - if (!$distUrl) { + $safeSource = $this->sanitizeSource($source) ?? ''; + [$distUrl, $auth] = ($safeSource !== '') + ? $this->extractAuth($safeSource, $config) + : [null, null]; + if (($distUrl === null) || ($distUrl === '')) { return false; } @@ -106,7 +95,7 @@ public function tryPrecompiled( $package->setDistType($type); $package->setDistUrl($distUrl); $package->setTargetDir($targetDir); - if ($auth) { + if (($auth !== null) && ($auth !== '')) { $package->setTransportOptions(['http' => ['header' => ["Authorization: {$auth}"]]]); } @@ -130,16 +119,12 @@ private function determineType(array $config, string $source): ?string return is_string($type) ? strtolower($type) : null; } - switch (strtolower(pathinfo($source, PATHINFO_EXTENSION) ?: '')) { - case 'rar': - return ArchiveDownloader::RAR; - case 'tar': - return ArchiveDownloader::TAR; - case 'xz': - return ArchiveDownloader::XZ; - } - - return ArchiveDownloader::ZIP; + return match (strtolower(pathinfo($source, PATHINFO_EXTENSION) ?: '')) { + 'rar' => ArchiveDownloader::RAR, + 'tar' => ArchiveDownloader::TAR, + 'xz' => ArchiveDownloader::XZ, + default => ArchiveDownloader::ZIP, + }; } /** @@ -152,7 +137,7 @@ private function sanitizeSource(string $source): ?string ? filter_var($source, FILTER_SANITIZE_URL) : false; - if (!$safeSource || !is_string($safeSource)) { + if (($safeSource === '') || !is_string($safeSource)) { $this->io->writeError(" '{$source}' is not a valid URL."); return null; @@ -169,10 +154,10 @@ private function sanitizeSource(string $source): ?string private function extractAuth(string $source, array $config): array { preg_match('~^(https?://)(?:([^:]+)(?::([^@]+))?@)?(.+)~i', $source, $matches); - $schema = $matches[1] ?? null; - $url = $matches[4] ?? null; + $schema = $matches[1] ?? ''; + $url = $matches[4] ?? ''; - if (!$schema || !$url) { + if (($schema === '') || ($url === '')) { $this->io->writeError(" '{$source}' is not a valid URL."); return [null, null]; @@ -180,7 +165,7 @@ private function extractAuth(string $source, array $config): array $auth = $config['auth'] ?? null; is_array($auth) and $auth = $this->extractBasicAuth($auth); - if (is_string($auth) && preg_match('~^[^\s]+\s.+$~', $auth)) { + if (is_string($auth) && preg_match('~^\S+\s.+$~', $auth)) { return [$source, $auth]; } @@ -193,7 +178,7 @@ private function extractAuth(string $source, array $config): array /** * @param array $config - * @return string|null + * @return non-empty-string|null */ private function extractBasicAuth(array $config): ?string { @@ -205,10 +190,12 @@ private function extractBasicAuth(array $config): ?string ?? $config['token'] ?? ''; - if (!$user || !is_string($user) || !is_string($pass)) { + if (($user === '') || !is_string($user) || !is_string($pass)) { return null; } - return 'Basic ' . base64_encode("{$user}:{$pass}"); + return ($pass === '') + ? 'Basic ' . base64_encode($user) + : 'Basic ' . base64_encode("{$user}:{$pass}"); } } diff --git a/src/PreCompilation/Config.php b/src/PreCompilation/Config.php index 743c612..a5c5b90 100644 --- a/src/PreCompilation/Config.php +++ b/src/PreCompilation/Config.php @@ -24,35 +24,18 @@ final class Config private const STABILITY_DEV = 'dev'; private const STABILITY_ALL = '*'; - /** - * @var array - */ - private $raw; + /** @var list> */ + private array $parsed = []; - /** - * @var list> - */ - private $parsed = []; + /** @var array> */ + private array $selected = []; - /** - * @var array> - */ - private $selected = []; - - /** - * @var bool|null - */ - private $valid = null; - - /** - * @var ModeResolver|null - */ - private $modeResolver; + private bool|null $valid = null; /** * @return Config */ - public static function invalid(): Config + public static function newInvalid(): Config { return new static([]); } @@ -71,10 +54,10 @@ public static function new(array $raw, ModeResolver $modeResolver): Config * @param array $raw * @param ModeResolver|null $modeResolver */ - private function __construct(array $raw, ?ModeResolver $modeResolver = null) - { - $this->raw = $raw; - $this->modeResolver = $modeResolver; + private function __construct( + private array $raw, + private ?ModeResolver $modeResolver = null + ) { } /** @@ -90,7 +73,7 @@ public function isValid(): bool /** * @param Placeholders $placeholders * @param array $environment - * @return string|null + * @return non-empty-string|null */ public function source(Placeholders $placeholders, array $environment): ?string { @@ -102,13 +85,18 @@ public function source(Placeholders $placeholders, array $environment): ?string /** @var string|null $rawSource */ $rawSource = $this->selected[$placeholders->uuid()][self::SOURCE] ?? null; + if (($rawSource === null) || ($rawSource === '')) { + return null; + } - return $rawSource ? ($placeholders->replace($rawSource, $environment) ?: null) : null; + $replaced = $placeholders->replace($rawSource, $environment); + + return ($replaced === '') ? null : $replaced; } /** * @param Placeholders $placeholders - * @return string|null + * @return non-empty-string|null */ public function target(Placeholders $placeholders): ?string { @@ -121,12 +109,12 @@ public function target(Placeholders $placeholders): ?string /** @var string|null $target */ $target = $this->selected[$placeholders->uuid()][self::TARGET] ?? null; - return $target; + return ($target === '') ? null : $target; } /** * @param Placeholders $placeholders - * @return string|null + * @return non-empty-string|null */ public function adapter(Placeholders $placeholders): ?string { @@ -139,7 +127,7 @@ public function adapter(Placeholders $placeholders): ?string /** @var string|null $adapter */ $adapter = $this->selected[$placeholders->uuid()][self::ADAPTER] ?? null; - return $adapter; + return ($adapter === '') ? null : $adapter; } /** @@ -187,7 +175,7 @@ private function deepReplace(iterable $data, Placeholders $placeholders, array $ /** @psalm-suppress MixedArgument */ $stdClass and $value = get_object_vars($value); $replaced = $this->deepReplace($value, $placeholders, $env); - $stdClass and $replaced = (object)$replaced; + $stdClass and $replaced = (object) $replaced; /** @psalm-suppress MixedArrayOffset */ $config[$key] = $replaced; } @@ -216,7 +204,7 @@ private function parse(): bool } $byMode = $this->modeResolver->resolveConfig($config); - if ($byMode && is_array($byMode)) { + if (($byMode !== []) && is_array($byMode)) { $config = $byMode; } if ($byMode === null) { @@ -224,9 +212,9 @@ private function parse(): bool } $settings = []; - $isNumeric = strpos(@json_encode($config) ?: '', '[') === 0; + $arrayIsList = str_starts_with((string) @json_encode($config), '['); $hasSource = array_key_exists(self::SOURCE, $config); - if ($isNumeric && !$hasSource) { + if ($arrayIsList && !$hasSource) { $settings = $config; } elseif ($hasSource) { $settings = [$config]; @@ -253,11 +241,11 @@ private function parseSetting(array $setting): void $stability = $setting[self::STABILITY] ?? null; $config = $setting[self::CONFIG] ?? []; - $valid = $source - && $target + $valid = ($source !== '') + && ($target !== '') && is_string($source) && is_string($target) - && (($adapter === null) || ($adapter && is_string($adapter))) + && (($adapter === null) || (($adapter !== '') && is_string($adapter))) && is_array($config); if (!$valid) { @@ -272,7 +260,7 @@ private function parseSetting(array $setting): void $this->parsed[] = [ self::SOURCE => $source, self::TARGET => $target, - self::ADAPTER => $adapter ? strtolower($adapter) : null, + self::ADAPTER => ($adapter !== null) ? strtolower($adapter) : null, self::STABILITY => $stability, self::CONFIG => $config, ]; @@ -311,13 +299,16 @@ private function selectBestSettings(Placeholders $placeholders): void continue; } - if (!$exactStability && ($settings[self::STABILITY] === $acceptedStability)) { + if (($exactStability === null) && ($settings[self::STABILITY] === $acceptedStability)) { $exactStability = $settings; - } elseif (!$fallbackStability && ($settings[self::STABILITY] === self::STABILITY_ALL)) { + } elseif ( + ($fallbackStability === null) + && ($settings[self::STABILITY] === self::STABILITY_ALL) + ) { $fallbackStability = $settings; } - if ($exactStability && $fallbackStability) { + if (($exactStability !== null) && ($fallbackStability !== null)) { break; } } diff --git a/src/PreCompilation/GitHubConfig.php b/src/PreCompilation/GitHubConfig.php index 5a25ad6..b477f8b 100644 --- a/src/PreCompilation/GitHubConfig.php +++ b/src/PreCompilation/GitHubConfig.php @@ -20,10 +20,8 @@ class GitHubConfig private const TOKEN = 'token'; private const TOKEN_USER = 'user'; - /** - * @var array - */ - private $config; + /** @var array */ + private array $config; /** * @param array $config @@ -64,10 +62,10 @@ private function __construct(array $config, array $env) ?? null; $this->config = [ - self::TOKEN => $token && is_string($token) ? $token : null, - self::TOKEN_USER => $user && is_string($user) ? $user : null, - self::REPO => $repo && is_string($repo) ? $repo : null, - self::REF => $ref && is_string($ref) ? $ref : null, + self::TOKEN => ($token !== '') && is_string($token) ? $token : null, + self::TOKEN_USER => ($user !== '') && is_string($user) ? $user : null, + self::REPO => ($repo !== '') && is_string($repo) ? $repo : null, + self::REF => ($ref !== '') && is_string($ref) ? $ref : null, ]; } @@ -108,10 +106,10 @@ public function ref(): ?string */ public function basicAuth(): ?string { - $user = $this->user(); - $token = $this->token(); + $user = $this->user() ?? ''; + $token = $this->token() ?? ''; - if ($user && $token) { + if (($user !== '') && ($token !== '')) { return 'Basic ' . base64_encode("{$user}:{$token}"); } diff --git a/src/PreCompilation/GithubActionArtifactAdapter.php b/src/PreCompilation/GithubActionArtifactAdapter.php index f0c5ecd..15456c7 100644 --- a/src/PreCompilation/GithubActionArtifactAdapter.php +++ b/src/PreCompilation/GithubActionArtifactAdapter.php @@ -20,50 +20,31 @@ class GithubActionArtifactAdapter implements Adapter { - /** - * @var Io - */ - private $io; - - /** - * @var HttpClient - */ - private $client; - - /** - * @var ArchiveDownloaderFactory - */ - private $downloaderFactory; - /** * @param Io $io * @param HttpClient $client - * @param ArchiveDownloaderFactory $archiveDownloaderFactory + * @param ArchiveDownloaderFactory $downloaderFactory * @return GithubActionArtifactAdapter */ public static function new( Io $io, HttpClient $client, - ArchiveDownloaderFactory $archiveDownloaderFactory + ArchiveDownloaderFactory $downloaderFactory ): GithubActionArtifactAdapter { - return new self($io, $client, $archiveDownloaderFactory); + return new self($io, $client, $downloaderFactory); } /** * @param Io $io * @param HttpClient $client - * @param ArchiveDownloaderFactory $archiveDownloaderFactory + * @param ArchiveDownloaderFactory $downloaderFactory */ private function __construct( - Io $io, - HttpClient $client, - ArchiveDownloaderFactory $archiveDownloaderFactory + private Io $io, + private HttpClient $client, + private ArchiveDownloaderFactory $downloaderFactory ) { - - $this->io = $io; - $this->client = $client; - $this->downloaderFactory = $archiveDownloaderFactory; } /** @@ -95,19 +76,21 @@ public function tryPrecompiled( try { $ghConfig = GitHubConfig::new($config, $environment); [$endpoint, $owner] = $this->buildArtifactsEndpoint($source, $ghConfig); - if (!$endpoint || !$owner) { + $endpoint ??= ''; + $owner ??= ''; + if (($endpoint === '') || ($owner === '')) { $this->io->writeError(' Invalid configuration for GitHub artifact.'); return false; } $distUrl = $this->retrieveArtifactUrl($source, $endpoint, $ghConfig); - if (!$distUrl) { + if ($distUrl === null) { return false; } - $auth = $ghConfig->basicAuth(); - $headers = $auth ? ["Authorization: {$auth}"] : []; + $auth = $ghConfig->basicAuth() ?? ''; + $headers = ($auth !== '') ? ["Authorization: {$auth}"] : []; $type = ArchiveDownloader::ZIP; $package = new Package($asset->name() . '-assets', 'artifact', 'artifact'); @@ -131,13 +114,13 @@ public function tryPrecompiled( */ private function buildArtifactsEndpoint(string $source, GitHubConfig $config): array { - if (!$source) { + if ($source === '') { return [null, null]; } $repo = $config->repo(); - $userRepo = $repo ? explode('/', $repo) : null; - if (!$userRepo || count($userRepo) !== 2) { + $userRepo = ($repo !== null) ? explode('/', $repo) : null; + if (!is_array($userRepo) || (count($userRepo) !== 2) || ($userRepo[0] === '')) { return [null, null]; } @@ -148,16 +131,16 @@ private function buildArtifactsEndpoint(string $source, GitHubConfig $config): a } $safe = filter_var($endpoint, FILTER_SANITIZE_URL); - $safe && is_string($safe) or $safe = null; + ($safe === '') and $safe = null; - return $safe ? [$safe, reset($userRepo) ?: ''] : [null, null]; + return ($safe === null) ? [$safe, $userRepo[0]] : [null, null]; } /** * @param string $name * @param non-empty-string $endpoint * @param GitHubConfig $config - * @return string|null + * @return non-empty-string|null */ private function retrieveArtifactUrl( string $name, @@ -166,22 +149,24 @@ private function retrieveArtifactUrl( ): ?string { $response = $this->client->get($endpoint, [], $config->basicAuth()); - $json = $response ? json_decode($response, true) : null; - if (!$json || !is_array($json) || empty($json['artifacts'])) { + $json = ($response !== '') ? json_decode($response, true) : null; + if (!is_array($json) || !isset($json['artifacts']) || !is_array($json['artifacts'])) { throw new \Exception("Could not obtain a valid API response from {$endpoint}."); } - /** @var string|null $artifactUrl */ + /** @var non-empty-string|null $artifactUrl */ $artifactUrl = null; - foreach ((array)$json['artifacts'] as $item) { + foreach ($json['artifacts'] as $item) { $artifactUrl = is_array($item) ? $this->artifactUrl($item, $name) : null; - if ($artifactUrl) { + if ($artifactUrl !== null) { break; } } $repo = $config->repo() ?? ''; - $artifactUrl or $this->io->writeError(" Artifact '{$name}' not found in '{$repo}'."); + if ($artifactUrl === null) { + $this->io->writeError(" Artifact '{$name}' not found in '{$repo}'."); + } return $artifactUrl; } @@ -189,7 +174,7 @@ private function retrieveArtifactUrl( /** * @param array $data * @param string $targetName - * @return string|null + * @return non-empty-string|null */ private function artifactUrl(array $data, string $targetName): ?string { @@ -198,9 +183,9 @@ private function artifactUrl(array $data, string $targetName): ?string return null; } - /** @var string|null $url */ - $url = $data['archive_download_url'] ?? null; - if ($url && filter_var($url, FILTER_VALIDATE_URL)) { + /** @var string $url */ + $url = $data['archive_download_url'] ?? ''; + if (($url !== '') && filter_var($url, FILTER_VALIDATE_URL)) { return $url; } diff --git a/src/PreCompilation/GithubReleaseZipAdapter.php b/src/PreCompilation/GithubReleaseZipAdapter.php index 10bbb79..1ab08de 100644 --- a/src/PreCompilation/GithubReleaseZipAdapter.php +++ b/src/PreCompilation/GithubReleaseZipAdapter.php @@ -20,50 +20,31 @@ class GithubReleaseZipAdapter implements Adapter { - /** - * @var Io - */ - private $io; - - /** - * @var HttpClient - */ - private $client; - - /** - * @var ArchiveDownloaderFactory - */ - private $downloaderFactory; - /** * @param Io $io * @param HttpClient $client - * @param ArchiveDownloaderFactory $archiveDownloaderFactory + * @param ArchiveDownloaderFactory $downloaderFactory * @return GithubReleaseZipAdapter */ public static function new( Io $io, HttpClient $client, - ArchiveDownloaderFactory $archiveDownloaderFactory + ArchiveDownloaderFactory $downloaderFactory ): GithubReleaseZipAdapter { - return new self($io, $client, $archiveDownloaderFactory); + return new self($io, $client, $downloaderFactory); } /** * @param Io $io * @param HttpClient $client - * @param ArchiveDownloaderFactory $archiveDownloaderFactory + * @param ArchiveDownloaderFactory $downloaderFactory */ private function __construct( - Io $io, - HttpClient $client, - ArchiveDownloaderFactory $archiveDownloaderFactory + private Io $io, + private HttpClient $client, + private ArchiveDownloaderFactory $downloaderFactory ) { - - $this->io = $io; - $this->client = $client; - $this->downloaderFactory = $archiveDownloaderFactory; } /** @@ -92,10 +73,10 @@ public function tryPrecompiled( array $environment ): bool { - $version = $asset->version(); + $version = $asset->version() ?? ''; try { - if (!$version) { + if ($version === '') { $this->io->writeError(' Invalid configuration for GitHub release zip.'); return false; @@ -104,20 +85,22 @@ public function tryPrecompiled( $ghConfig = GitHubConfig::new($config, $environment); [$endpoint, $owner] = $this->buildReleaseEndpoint($source, $ghConfig, $version); - if (!$endpoint || !$owner) { + $endpoint ??= ''; + $owner ??= ''; + if (($endpoint === '') || ($owner === '')) { $this->io->writeError(' Invalid GitHub release configuration.'); return false; } - $distUrl = $this->retrieveArchiveUrl($source, $endpoint, $ghConfig, $version); - if (!$distUrl) { + $distUrl = $this->retrieveArchiveUrl($source, $endpoint, $ghConfig, $version) ?? ''; + if ($distUrl === '') { return false; } $headers = ['Accept: application/octet-stream']; - $auth = $ghConfig->basicAuth(); - $auth and $headers[] = "Authorization: {$auth}"; + $auth = $ghConfig->basicAuth() ?? ''; + ($auth !== '') and $headers[] = "Authorization: {$auth}"; $type = ArchiveDownloader::ZIP; $package = new Package($asset->name() . '-assets', 'release-zip', 'release-zip'); @@ -140,15 +123,19 @@ public function tryPrecompiled( * @param string $version * @return array{string,string}|array{null,null} */ - private function buildReleaseEndpoint(string $source, GitHubConfig $config, string $version): array - { + private function buildReleaseEndpoint( + string $source, + GitHubConfig $config, + string $version + ): array { + if (!$source) { return [null, null]; } $repo = $config->repo(); - $userRepo = $repo ? explode('/', $repo) : null; - if (!$userRepo || count($userRepo) !== 2) { + $userRepo = ($repo !== null) ? explode('/', $repo) : null; + if (!is_array($userRepo) || (count($userRepo) !== 2) || ($userRepo[0] === '')) { return [null, null]; } @@ -159,9 +146,9 @@ private function buildReleaseEndpoint(string $source, GitHubConfig $config, stri } $safe = filter_var($endpoint, FILTER_SANITIZE_URL); - $safe && is_string($safe) or $safe = null; + ($safe === '') and $safe = null; - return $safe ? [$safe, reset($userRepo) ?: ''] : [null, null]; + return ($safe !== null) ? [$safe, $userRepo[0]] : [null, null]; } /** @@ -180,12 +167,12 @@ private function retrieveArchiveUrl( $response = $this->client->get($endpoint, [], $config->basicAuth()); $json = $response ? json_decode($response, true) : null; - if (!$json || !is_array($json)) { + if (($json === null) || ($json === []) || !is_array($json)) { throw new \Exception("Could not obtain a valid API response from {$endpoint}."); } - $assets = $json['assets'] ?? null; - if (!$assets || !is_array($assets)) { + $assets = $json['assets'] ?? []; + if (($assets === []) || !is_array($assets)) { $this->io->write(" Release '{$version}' has no binary assets."); return null; @@ -195,11 +182,15 @@ private function retrieveArchiveUrl( $targetName .= '.zip'; } - $id = $this->findBinaryId($assets, $targetName); - $repo = $config->repo() ?: ''; - $id or $this->io->writeError(" Release binary '{$targetName}' not found."); + $id = $this->findBinaryId($assets, $targetName) ?? ''; + $repo = $config->repo() ?? ''; + if ($id === '') { + $this->io->writeError(" Release binary '{$targetName}' not found."); + + return null; + } - return $id ? "https://api.github.com/repos/{$repo}/releases/assets/{$id}" : null; + return "https://api.github.com/repos/{$repo}/releases/assets/{$id}"; } /** @@ -212,12 +203,12 @@ private function findBinaryId(array $items, string $targetName): ?string foreach ($items as $item) { if ( is_array($item) - && !empty($item['name']) + && array_key_exists('name', $item) && ($item['name'] === $targetName) - && !empty($item['id']) + && array_key_exists('id', $item) && is_scalar($item['id']) ) { - return (string)$item['id']; + return (string) $item['id']; } } diff --git a/src/PreCompilation/Handler.php b/src/PreCompilation/Handler.php index 27344dc..240da28 100644 --- a/src/PreCompilation/Handler.php +++ b/src/PreCompilation/Handler.php @@ -19,35 +19,9 @@ class Handler { - /** - * @var HashBuilder - */ - private $hashBuilder; - - /** - * @var array - */ - private $adapters = []; - - /** - * @var string|null - */ - private $defaultAdapterId; - - /** - * @var Io - */ - private $io; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var ModeResolver - */ - private $modeResolver; + /** @var array */ + private array $adapters = []; + private string|null $defaultAdapterId = null; /** * @param HashBuilder $hashBuilder @@ -73,16 +47,11 @@ public static function new( * @param ModeResolver $modeResolver */ final private function __construct( - HashBuilder $hashBuilder, - Io $io, - Filesystem $filesystem, - ModeResolver $modeResolver + private HashBuilder $hashBuilder, + private Io $io, + private Filesystem $filesystem, + private ModeResolver $modeResolver ) { - - $this->hashBuilder = $hashBuilder; - $this->io = $io; - $this->filesystem = $filesystem; - $this->modeResolver = $modeResolver; } /** @@ -115,12 +84,12 @@ public function tryPrecompiled(Asset $asset): bool } $environment = $asset->env(); - $source = $config->source($placeholders, $environment); - $path = $asset->path(); - $target = $config->target($placeholders); + $source = $config->source($placeholders, $environment) ?? ''; + $path = $asset->path() ?? ''; + $target = $config->target($placeholders) ?? ''; $name = $asset->name(); - if (!$source || !$path || !$target) { + if (($source === '') || ($path === '') || ($target === '')) { $this->io->writeVerboseComment("Found no pre-processed configuration for '{$name}'."); return false; } @@ -164,6 +133,8 @@ public function findAdapter(Config $config, Placeholders $placeholders): ?Adapte $adapterId = $config->adapter($placeholders) ?? $this->defaultAdapterId; - return $adapterId ? ($this->adapters[$adapterId] ?? null) : null; + return (($adapterId !== null) && ($adapterId !== '')) + ? ($this->adapters[$adapterId] ?? null) + : null; } } diff --git a/src/PreCompilation/Placeholders.php b/src/PreCompilation/Placeholders.php index 58ae8a7..cce515d 100644 --- a/src/PreCompilation/Placeholders.php +++ b/src/PreCompilation/Placeholders.php @@ -22,30 +22,7 @@ class Placeholders public const VERSION = 'version'; public const REFERENCE = 'ref'; - /** - * @var string - */ - private $mode; - - /** - * @var string|null - */ - private $hash; - - /** - * @var string|null - */ - private $version; - - /** - * @var string|null - */ - private $reference; - - /** - * @var string - */ - private $uid; + private string $uid; /** * @param Asset $asset @@ -59,17 +36,17 @@ public static function new(Asset $asset, string $env, ?string $hash): Placeholde } /** - * @param string $env + * @param string $mode * @param string|null $hash * @param string|null $version * @param string|null $reference */ - private function __construct(string $env, ?string $hash, ?string $version, ?string $reference) - { - $this->mode = $env; - $this->hash = $hash; - $this->version = $version; - $this->reference = $reference; + private function __construct( + private string $mode, + private ?string $hash, + private ?string $version, + private ?string $reference + ) { $values = [$hash ?? '', $version ?? '', $reference ?? '', random_bytes(32)]; $base = sha1(implode('|', $values)); @@ -97,7 +74,9 @@ public function uuid(): string */ public function hasStableVersion(): bool { - return $this->version && VersionParser::parseStability($this->version) === 'stable'; + return ($this->version !== null) + && ($this->version !== '') + && (VersionParser::parseStability($this->version) === 'stable'); } /** @@ -107,7 +86,7 @@ public function hasStableVersion(): bool */ public function replace(string $original, array $environment): string { - if (!$original || (strpos($original, '${') === false)) { + if (($original === '') || (!str_contains($original, '${'))) { return $original; } @@ -128,6 +107,8 @@ static function (array $matches) use ($replace): string { $original ); - return $replaced ? Env::replaceEnvVariables($replaced, $environment) : ''; + return ($replaced !== null) && ($replaced !== '') + ? Env::replaceEnvVariables($replaced, $environment) + : ''; } } diff --git a/src/Process/Factory.php b/src/Process/Factory.php index 59ae468..a2f254d 100644 --- a/src/Process/Factory.php +++ b/src/Process/Factory.php @@ -15,38 +15,23 @@ class Factory { - /** - * @var bool - */ - private $newMethod; - - /** - * @var float - */ - private $timeout; - - /** - * @var callable|null - */ - private $factory; + private bool $newMethod; + private float $timeout; /** - * @param callable(string, ?string=):?Process|null $factory * @return Factory */ - public static function new(callable $factory = null): Factory + public static function new(): Factory { - return new self($factory); + return new self(); } /** - * @param callable(string, ?string=):?Process $factory */ - private function __construct(callable $factory = null) + private function __construct() { - $this->newMethod = !$factory && method_exists(Process::class, 'fromShellCommandline'); + $this->newMethod = method_exists(Process::class, 'fromShellCommandline'); $this->timeout = 86400.0; - $this->factory = $factory; } /** @@ -56,25 +41,12 @@ private function __construct(callable $factory = null) */ public function create(string $command, string $cwd): Process { - if ($this->factory) { - $process = ($this->factory)($command, $cwd); - if (!$process instanceof Process) { - throw new \Exception('Could not factory a process from given factory.'); - } - - return $process; - } - - if ($this->newMethod) { - $process = Process::fromShellCommandline($command, $cwd, null, null, $this->timeout); - - return $process; - } - /** * @psalm-suppress InvalidArgument * @noinspection PhpParamsInspection */ - return new Process($command, $cwd, null, null, $this->timeout); + return $this->newMethod + ? Process::fromShellCommandline($command, $cwd, null, null, $this->timeout) + : new Process($command, $cwd, null, null, $this->timeout); } } diff --git a/src/Process/ParallelManager.php b/src/Process/ParallelManager.php index 9afa44f..c43494d 100644 --- a/src/Process/ParallelManager.php +++ b/src/Process/ParallelManager.php @@ -17,55 +17,20 @@ class ParallelManager { - /** - * @var callable - */ + /** @var callable */ private $outputHandler; - - /** - * @var Factory - */ - private $factory; - - /** - * @var positive-int - */ - private $maxParallel; - - /** - * @var positive-int - */ - private $poll; - - /** - * @var \SplQueue - */ - private $stack; - - /** - * @var int - */ - private $total = 0; - - /** - * @var int - */ - private $timeout = 0; - - /** - * @var int - */ - private $timeoutIncrement; - - /** - * @var float - */ - private $executionStarted; - - /** - * @var array - */ - private $commands; + /** @var positive-int */ + private int $maxParallel; + /** @var positive-int */ + private int $poll; + private int $timeoutIncrement; + /** @var \SplQueue */ + private \SplQueue $stack; + private int $timeout = 0; + private float $executionStarted; + /** @var array */ + private array $commands; + private int $total = 0; /** * @param callable $outputHandler @@ -83,27 +48,26 @@ public static function new( int $timeoutIncrement = 300 ): ParallelManager { - return new self($outputHandler, $factory, $maxParallel, $poll, $timeoutIncrement); + return new self($factory, $outputHandler, $maxParallel, $poll, $timeoutIncrement); } /** - * @param callable $outputHandler * @param Factory $factory + * @param callable $outputHandler * @param int $maxParallel * @param int $poll * @param int $timeoutIncrement */ private function __construct( + private Factory $factory, callable $outputHandler, - Factory $factory, int $maxParallel, int $poll, int $timeoutIncrement ) { $this->outputHandler = $outputHandler; - $this->factory = $factory; - $this->maxParallel = $maxParallel >= 1 ? $maxParallel : 4; + $this->maxParallel = ($maxParallel >= 1) ? $maxParallel : 4; // sanity: between 0.005 and 2 seconds $this->poll = min(max($poll, 5000), 2000000); // sanity: between 30 and 3600 seconds @@ -127,7 +91,7 @@ public function pushAssetToProcess( array_unshift($commands, $command); $command = implode(' && ', $commands); - $process = $this->factory->create($command, (string)$asset->path()); + $process = $this->factory->create($command, (string) $asset->path()); $this->commands[$asset->name()] = $command; $this->stack->enqueue([$process, $asset]); $this->total++; @@ -144,7 +108,7 @@ public function pushAssetToProcess( public function execute(Io $io, bool $stopOnFailure): Results { if ($this->total <= 0) { - return Results::empty(); + return Results::newEmpty(); } $this->executionStarted = microtime(true); @@ -165,7 +129,7 @@ public function execute(Io $io, bool $stopOnFailure): Results ); $results = $timedOut - ? Results::timeout($this->total, $successful, $erroneous) + ? Results::newWithTimeout($this->total, $successful, $erroneous) : Results::new($this->total, $successful, $erroneous); $this->resetStatus(); diff --git a/src/Process/Results.php b/src/Process/Results.php index 264943c..760fcea 100644 --- a/src/Process/Results.php +++ b/src/Process/Results.php @@ -16,30 +16,12 @@ final class Results { - /** - * @var int - */ - private $total; - - /** - * @var \SplQueue|null - */ - private $successful; - - /** - * @var \SplQueue|null - */ - private $erroneous; - - /** - * @var bool - */ - private $timedOut = false; + private bool $timedOut = false; /** * @return Results */ - public static function empty(): Results + public static function newEmpty(): Results { $instance = new static(0, null, null); $instance->timedOut = false; @@ -71,7 +53,7 @@ public static function new( * @param \SplQueue|null $erroneous * @return Results */ - public static function timeout( + public static function newWithTimeout( int $total, ?\SplQueue $successful, ?\SplQueue $erroneous @@ -89,14 +71,10 @@ public static function timeout( * @param \SplQueue|null $erroneous */ private function __construct( - int $total, - ?\SplQueue $successful, - ?\SplQueue $erroneous + private int $total, + private ?\SplQueue $successful, + private ?\SplQueue $erroneous ) { - - $this->total = $total; - $this->successful = $successful; - $this->erroneous = $erroneous; } /** @@ -166,7 +144,7 @@ public function notExecutedCount(): int } /** - * @return \SplQueue|null + * @return \SplQueue|null */ public function successes(): ?\SplQueue { diff --git a/src/Util/ArchiveDownloader.php b/src/Util/ArchiveDownloader.php index 980383a..fd85fdf 100644 --- a/src/Util/ArchiveDownloader.php +++ b/src/Util/ArchiveDownloader.php @@ -25,21 +25,6 @@ class ArchiveDownloader public const XZ = 'xz'; // phpcs:ignore public const TAR = 'tar'; - /** - * @var callable(PackageInterface,string):void - */ - private $downloadCallback; - - /** - * @var Io - */ - private $io; - - /** - * @var Filesystem - */ - private $filesystem; - /** * @param Loop $loop * @param DownloaderInterface $downloader @@ -47,63 +32,28 @@ class ArchiveDownloader * @param Filesystem $filesystem * @return ArchiveDownloader */ - public static function viaLoop( + public static function new( Loop $loop, DownloaderInterface $downloader, Io $io, Filesystem $filesystem ): ArchiveDownloader { - $downloadCallback = static function ( - PackageInterface $package, - string $path - ) use ( - $loop, - $downloader - ): void { - - SyncHelper::downloadAndInstallPackageSync($loop, $downloader, $path, $package); - }; - - return new self($downloadCallback, $io, $filesystem); + return new self($loop, $downloader, $io, $filesystem); } /** + * @param Loop $loop * @param DownloaderInterface $downloader * @param Io $io * @param Filesystem $filesystem - * @return ArchiveDownloader - */ - public static function forV1( - DownloaderInterface $downloader, - Io $io, - Filesystem $filesystem - ): ArchiveDownloader { - - $downloadCallback = static function ( - PackageInterface $package, - string $path - ) use ($downloader): void { - $downloader->download($package, $path); - }; - - return new self($downloadCallback, $io, $filesystem); - } - - /** - * @param callable(PackageInterface,string):void $downloadCallback - * @param Io $io - * @param Filesystem $filesystem */ private function __construct( - callable $downloadCallback, - Io $io, - Filesystem $filesystem + private Loop $loop, + private DownloaderInterface $downloader, + private Io $io, + private Filesystem $filesystem ) { - - $this->downloadCallback = $downloadCallback; - $this->io = $io; - $this->filesystem = $filesystem; } /** @@ -123,7 +73,7 @@ public function download(PackageInterface $package, string $path): bool $this->io->writeVerbose( "Downloading and unpack '{$distUrl}' in new directory '{$path}'..." ); - ($this->downloadCallback)($package, $path); + $this->downloadAndInstall($package, $path); return true; } @@ -136,13 +86,13 @@ public function download(PackageInterface $package, string $path): bool // download there, or Composer will delete every existing file in it. // So we first unpack in a temporary folder and then move unpacked files from the temp // dir to final target dir. That's surely slower, but necessary. - $tempDir = dirname($path) . '/.tmp' . (string)substr(md5(uniqid($path, true)), 0, 8); + $tempDir = dirname($path) . '/.tmp' . substr(md5(uniqid($path, true)), 0, 8); $this->io->writeVerbose( "Archive target path '{$path}' is an existing directory.", "Downloading and unpacking '{$distUrl}' in the temporary folder '{$tempDir}'..." ); $this->filesystem->ensureDirectoryExists($tempDir); - ($this->downloadCallback)($package, $tempDir); + $this->downloadAndInstall($package, $tempDir); $this->filesystem->ensureDirectoryExists($path); $finder = Finder::create()->in($tempDir)->ignoreVCS(true)->files(); @@ -176,4 +126,19 @@ public function download(PackageInterface $package, string $path): bool } } } + + /** + * @param PackageInterface $package + * @param string $path + * @return void + */ + private function downloadAndInstall(PackageInterface $package, string $path): void + { + SyncHelper::downloadAndInstallPackageSync( + $this->loop, + $this->downloader, + $path, + $package + ); + } } diff --git a/src/Util/ArchiveDownloaderFactory.php b/src/Util/ArchiveDownloaderFactory.php index 46440b4..72c95c4 100644 --- a/src/Util/ArchiveDownloaderFactory.php +++ b/src/Util/ArchiveDownloaderFactory.php @@ -11,12 +11,12 @@ namespace Inpsyde\AssetsCompiler\Util; -use Composer\Composer; use Composer\Downloader\DownloaderInterface; +use Composer\Downloader\DownloadManager; use Composer\Downloader\FileDownloader; use Composer\IO\ConsoleIO; use Composer\Util\Filesystem; -use Composer\Util\SyncHelper; +use Composer\Util\Loop; class ArchiveDownloaderFactory { @@ -27,30 +27,8 @@ class ArchiveDownloaderFactory ArchiveDownloader::TAR, ]; - /** - * @var array - */ - private $downloaders = []; - - /** - * @var Io - */ - private $io; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var \Composer\Downloader\DownloadManager - */ - private $downloadManager; - - /** - * @var \Composer\Util\Loop|null - */ - private $loop; + /** @var array */ + private array $downloaders = []; /** * @param string $type @@ -62,38 +40,34 @@ public static function isValidArchiveType(string $type): bool } /** - * @param Io $io - * @param Composer $composer + * @param Loop $loop + * @param DownloadManager $downloadManager * @param Filesystem $filesystem + * @param Io $io * @return ArchiveDownloaderFactory */ public static function new( + Loop $loop, + DownloadManager $downloadManager, + Filesystem $filesystem, Io $io, - Composer $composer, - Filesystem $filesystem ): ArchiveDownloaderFactory { - return new self($io, $composer, $filesystem); + return new self($loop, $downloadManager, $filesystem, $io); } /** - * @param Io $io - * @param Composer $composer + * @param Loop $loop + * @param DownloadManager $downloadManager * @param Filesystem $filesystem + * @param Io $io */ private function __construct( - Io $io, - Composer $composer, - Filesystem $filesystem + private Loop $loop, + private DownloadManager $downloadManager, + private Filesystem $filesystem, + private Io $io ) { - - $this->io = $io; - $this->downloadManager = $composer->getDownloadManager(); - /** @psalm-suppress RedundantCondition */ - if (is_callable([$composer, 'getLoop']) && class_exists(SyncHelper::class)) { - $this->loop = $composer->getLoop(); - } - $this->filesystem = $filesystem; } /** @@ -102,7 +76,7 @@ private function __construct( */ public function create(string $type): ArchiveDownloader { - if (!empty($this->downloaders[$type])) { + if (isset($this->downloaders[$type])) { return $this->downloaders[$type]; } @@ -110,11 +84,12 @@ public function create(string $type): ArchiveDownloader throw new \Exception(sprintf("Invalid archive type: '%s'.", $type)); } - $downloader = $this->factoryDownloader($type); - - $this->downloaders[$type] = $this->loop - ? ArchiveDownloader::viaLoop($this->loop, $downloader, $this->io, $this->filesystem) - : ArchiveDownloader::forV1($downloader, $this->io, $this->filesystem); + $this->downloaders[$type] = ArchiveDownloader::new( + $this->loop, + $this->factoryDownloader($type), + $this->io, + $this->filesystem + ); return $this->downloaders[$type]; } diff --git a/src/Util/Env.php b/src/Util/Env.php index 6a00a93..3eef692 100644 --- a/src/Util/Env.php +++ b/src/Util/Env.php @@ -13,11 +13,6 @@ class Env { - /** - * @var \ArrayAccess|null - */ - private static $getenvWrap = null; - /** * @return string|null */ @@ -33,7 +28,13 @@ public static function assetsCompilerMode(): ?string */ public static function readEnv(string $name, array $defaults = []): ?string { - $env = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name) ?: ($defaults[$name] ?? null); + $env = $_SERVER[$name] ?? $_ENV[$name] ?? null; + if ($env === null) { + $env = getenv($name); + ($env === false) and $env = null; + } + + $env ??= ($defaults[$name] ?? null); return is_string($env) ? $env : null; } @@ -45,14 +46,14 @@ public static function readEnv(string $name, array $defaults = []): ?string */ public static function replaceEnvVariables(string $string, array $defaultEnv): string { - if (!$string || (strpos($string, '${') === false)) { + if (!$string || !str_contains($string, '${')) { return $string; } - return (string)preg_replace_callback( + return (string) preg_replace_callback( '~\$\{([a-z0-9_]+)\}~i', static function (array $var) use ($defaultEnv): string { - return (string)static::readEnv($var[1], $defaultEnv); + return (string) static::readEnv($var[1], $defaultEnv); }, $string ); diff --git a/src/Util/Factory.php b/src/Util/Factory.php index f0cdedd..d5c7ebc 100644 --- a/src/Util/Factory.php +++ b/src/Util/Factory.php @@ -16,6 +16,7 @@ use Composer\Package\RootPackageInterface; use Composer\Repository\RepositoryInterface; use Composer\Util\Filesystem; +use Composer\Util\HttpDownloader; use Composer\Util\ProcessExecutor; use Inpsyde\AssetsCompiler\Asset\Config; use Inpsyde\AssetsCompiler\Asset\Defaults; @@ -24,6 +25,8 @@ use Inpsyde\AssetsCompiler\Asset\HashBuilder; use Inpsyde\AssetsCompiler\Asset\Locker; use Inpsyde\AssetsCompiler\Asset\Asset; +use Inpsyde\AssetsCompiler\Asset\Info; +use Inpsyde\AssetsCompiler\Asset\PathsFinder; use Inpsyde\AssetsCompiler\Asset\Processor; use Inpsyde\AssetsCompiler\Asset\RootConfig; use Inpsyde\AssetsCompiler\PackageManager; @@ -37,35 +40,8 @@ final class Factory { - /** - * @var Composer - */ - private $composer; - - /** - * @var IOInterface - */ - private $io; - - /** - * @var string|null - */ - private $mode; - - /** - * @var bool - */ - private $isDev; - - /** - * @var string - */ - private $ignoreLock; - - /** - * @var array - */ - private $objects = []; + /** var array */ + private array $objects = []; /** * @param Composer $composer @@ -83,7 +59,7 @@ public static function new( string $ignoreLock = '' ): Factory { - return new self($composer, $io, $mode, $isDev, $ignoreLock); + return new self($composer, $io, $mode ?? Env::assetsCompilerMode(), $isDev, $ignoreLock); } /** @@ -94,18 +70,12 @@ public static function new( * @param string $ignoreLock */ private function __construct( - Composer $composer, - IOInterface $io, - ?string $mode, - bool $isDev, - string $ignoreLock = '' + private Composer $composer, + private IOInterface $io, + private ?string $mode, + private bool $isDev, + private string $ignoreLock ) { - - $this->composer = $composer; - $this->io = $io; - $this->mode = $mode ?? Env::assetsCompilerMode(); - $this->isDev = $isDev; - $this->ignoreLock = $ignoreLock; } /** @@ -129,7 +99,7 @@ public function composerIo(): IOInterface */ public function composerConfig(): \Composer\Config { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = $this->composer->getConfig(); } @@ -144,7 +114,7 @@ public function composerConfig(): \Composer\Config */ public function composerRootPackage(): RootPackageInterface { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = $this->composer->getPackage(); } @@ -159,7 +129,7 @@ public function composerRootPackage(): RootPackageInterface */ public function composerRepository(): RepositoryInterface { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $manager = $this->composer->getRepositoryManager(); $this->objects[__FUNCTION__] = $manager->getLocalRepository(); } @@ -170,12 +140,27 @@ public function composerRepository(): RepositoryInterface return $repo; } + /** + * @return HttpDownloader + */ + public function composerHttpDownloader(): HttpDownloader + { + if (!isset($this->objects[__FUNCTION__])) { + $this->objects[__FUNCTION__] = \Composer\Factory::createHttpDownloader( + $this->composerIo(), + $this->composerConfig() + ); + } + /** @var HttpDownloader */ + return $this->objects[__FUNCTION__]; + } + /** * @return Filesystem */ public function filesystem(): Filesystem { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = new Filesystem(); } @@ -190,7 +175,7 @@ public function filesystem(): Filesystem */ public function io(): Io { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = Io::new($this->io); } @@ -205,7 +190,7 @@ public function io(): Io */ public function modeResolver(): ModeResolver { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = ModeResolver::new($this->mode, $this->isDev); } @@ -220,7 +205,7 @@ public function modeResolver(): ModeResolver */ public function config(): Config { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = Config::forComposerPackage( $this->composerRootPackage(), $this->rootFolder(), @@ -240,7 +225,7 @@ public function config(): Config */ public function rootConfig(): RootConfig { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { /** @var RootConfig $root */ $root = $this->config()->rootConfig(); $this->objects[__FUNCTION__] = $root; @@ -257,9 +242,11 @@ public function rootConfig(): RootConfig */ public function defaults(): Defaults { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $defaults = $this->rootConfig()->defaults(); - $this->objects[__FUNCTION__] = $defaults ? Defaults::new($defaults) : Defaults::empty(); + $this->objects[__FUNCTION__] = $defaults + ? Defaults::new($defaults) + : Defaults::newEmpty(); } /** @var Defaults $defaults */ @@ -273,7 +260,7 @@ public function defaults(): Defaults */ public function processExecutor(): ProcessExecutor { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = new ProcessExecutor($this->io); } @@ -288,7 +275,7 @@ public function processExecutor(): ProcessExecutor */ public function assets(): \Iterator { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $assets = $this->assetsFinder()->find( $this->composerRepository(), $this->composerRootPackage(), @@ -311,7 +298,7 @@ public function assets(): \Iterator */ public function commandsFinder(): PackageManager\Finder { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = PackageManager\Finder::new( $this->processExecutor(), $this->filesystem(), @@ -331,7 +318,7 @@ public function commandsFinder(): PackageManager\Finder */ public function assetsFinder(): AssetFinder { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $config = $this->rootConfig(); $this->objects[__FUNCTION__] = AssetFinder::new( $config->packagesData(), @@ -363,7 +350,7 @@ public function rootFolder(): string */ public function assetFactory(): AssetFactory { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = AssetFactory::new( $this->modeResolver(), $this->filesystem(), @@ -384,9 +371,9 @@ public function assetFactory(): AssetFactory */ public function hashBuilder(): HashBuilder { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = HashBuilder::new( - $this->filesystem(), + $this->assetsPathsFinder(), $this->io() ); } @@ -402,8 +389,11 @@ public function hashBuilder(): HashBuilder */ public function httpClient(): HttpClient { - if (empty($this->objects[__FUNCTION__])) { - $this->objects[__FUNCTION__] = HttpClient::new($this->io(), $this->composer()); + if (!isset($this->objects[__FUNCTION__])) { + $this->objects[__FUNCTION__] = HttpClient::new( + $this->composerHttpDownloader(), + $this->io() + ); } /** @var HttpClient $client */ @@ -417,11 +407,12 @@ public function httpClient(): HttpClient */ public function archiveDownloaderFactory(): ArchiveDownloaderFactory { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = ArchiveDownloaderFactory::new( - $this->io(), - $this->composer(), - $this->filesystem() + $this->composer()->getLoop(), + $this->composer()->getDownloadManager(), + $this->filesystem(), + $this->io() ); } @@ -436,7 +427,7 @@ public function archiveDownloaderFactory(): ArchiveDownloaderFactory */ public function archiveDownloaderAdapter(): ArchiveDownloaderAdapter { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = ArchiveDownloaderAdapter::new( $this->io(), $this->archiveDownloaderFactory() @@ -454,7 +445,7 @@ public function archiveDownloaderAdapter(): ArchiveDownloaderAdapter */ public function githubArtifactAdapter(): GithubActionArtifactAdapter { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = GithubActionArtifactAdapter::new( $this->io(), $this->httpClient(), @@ -473,7 +464,7 @@ public function githubArtifactAdapter(): GithubActionArtifactAdapter */ public function githubReleaseZipAdapter(): GithubReleaseZipAdapter { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = GithubReleaseZipAdapter::new( $this->io(), $this->httpClient(), @@ -492,7 +483,7 @@ public function githubReleaseZipAdapter(): GithubReleaseZipAdapter */ public function preCompilationHandler(): Handler { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $handler = Handler::new( $this->hashBuilder(), $this->io(), @@ -517,7 +508,7 @@ public function preCompilationHandler(): Handler */ public function locker(): Locker { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = new Locker( $this->io(), $this->hashBuilder(), @@ -536,7 +527,7 @@ public function locker(): Locker */ public function processOutputHandler(): callable { - if (!empty($this->objects[__FUNCTION__])) { + if (!!isset($this->objects[__FUNCTION__])) { /** @var callable(string,string):void $handler */ $handler = $this->objects[__FUNCTION__]; @@ -563,7 +554,7 @@ public function processOutputHandler(): callable */ public function processFactory(): ProcessFactory { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = ProcessFactory::new(); } @@ -578,7 +569,7 @@ public function processFactory(): ProcessFactory */ public function processManager(): ParallelManager { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $config = $this->rootConfig(); $this->objects[__FUNCTION__] = ParallelManager::new( $this->processOutputHandler(), @@ -600,7 +591,7 @@ public function processManager(): ParallelManager */ public function assetsProcessor(): Processor { - if (empty($this->objects[__FUNCTION__])) { + if (!isset($this->objects[__FUNCTION__])) { $this->objects[__FUNCTION__] = Processor::new( $this->io(), $this->config(), @@ -619,4 +610,47 @@ public function assetsProcessor(): Processor return $processor; } + + /** + * @return PathsFinder + */ + public function assetsPathsFinder(): PathsFinder + { + if (!isset($this->objects[__FUNCTION__])) { + $this->objects[__FUNCTION__] = PathsFinder::new( + $this->assets(), + $this->filesystem(), + $this->io(), + $this->rootConfig()->path() + ); + } + + /** @var PathsFinder $finder */ + $finder = $this->objects[__FUNCTION__]; + + return $finder; + } + + /** + * @return Info + */ + public function assetsInfo(): Info + { + if (!isset($this->objects[__FUNCTION__])) { + $this->objects[__FUNCTION__] = Info::new( + $this->assets(), + $this->hashBuilder(), + $this->preCompilationHandler(), + $this->assetsPathsFinder(), + $this->modeResolver(), + $this->rootConfig(), + $this->io() + ); + } + + /** @var Info $info */ + $info = $this->objects[__FUNCTION__]; + + return $info; + } } diff --git a/src/Util/HttpClient.php b/src/Util/HttpClient.php index 756b71b..27c6614 100644 --- a/src/Util/HttpClient.php +++ b/src/Util/HttpClient.php @@ -13,51 +13,27 @@ use Composer\Composer; use Composer\Util\HttpDownloader; -use Composer\Util\RemoteFilesystem; class HttpClient { - /** - * @var Io - */ - private $io; - - /** - * @var HttpDownloader|RemoteFilesystem|mixed - */ - private $client; - /** * @param Io $io * @param Composer $composer * @return HttpClient */ - public static function new(Io $io, Composer $composer): HttpClient + public static function new(HttpDownloader $httpDownloader, Io $io): HttpClient { - return new self($io, $composer); + return new self($httpDownloader, $io); } /** * @param Io $io * @param Composer $composer */ - private function __construct(Io $io, Composer $composer) - { - $this->io = $io; - if (is_callable([\Composer\Factory::class, 'createHttpDownloader'])) { - $this->client = \Composer\Factory::createHttpDownloader( - $io->composerIo(), - $composer->getConfig() - ); - - return; - } - - /** @noinspection PhpUndefinedMethodInspection */ - $this->client = \Composer\Factory::createRemoteFilesystem( - $io->composerIo(), - $composer->getConfig() - ); + private function __construct( + private HttpDownloader $httpDownloader, + private Io $io + ) { } /** @@ -69,7 +45,7 @@ private function __construct(Io $io, Composer $composer) public function get(string $url, array $options = [], ?string $authorization = null): string { try { - if ($authorization) { + if (($authorization !== null) && ($authorization !== '')) { isset($options['http']) or $options['http'] = []; /** @psalm-suppress MixedArrayAssignment */ isset($options['http']['header']) or $options['http']['header'] = []; @@ -78,20 +54,13 @@ public function get(string $url, array $options = [], ?string $authorization = n } $result = null; - if ($this->client instanceof HttpDownloader) { - $response = $this->client->get($url, $options); - $statusCode = $response->getStatusCode(); - if ($statusCode > 199 && $statusCode < 300) { - $result = $response->getBody(); - } - } elseif ($this->client instanceof RemoteFilesystem) { - /** @psalm-suppress UndefinedMethod */ - $origin = (string)RemoteFilesystem::getOrigin($url); - /** @psalm-suppress InternalMethod */ - $result = $this->client->getContents($origin, $url, false, $options); + $response = $this->httpDownloader->get($url, $options); + $statusCode = $response->getStatusCode(); + if (($statusCode > 199) && ($statusCode < 300)) { + $result = $response->getBody(); } - if (!$result || !is_string($result)) { + if (($result === '') || !is_string($result)) { throw new \Exception("Could not obtain a response from '{$url}'."); } diff --git a/src/Util/Io.php b/src/Util/Io.php index 4702d7e..20ce4c3 100644 --- a/src/Util/Io.php +++ b/src/Util/Io.php @@ -19,15 +19,7 @@ class Io { private const SPACER = ' '; - /** - * @var IOInterface - */ - private $io; - - /** - * @var bool|null - */ - private $quiet; + private bool|null $quiet = null; /** * @param IOInterface $io @@ -41,17 +33,8 @@ public static function new(IOInterface $io): Io /** * @param IOInterface $io */ - private function __construct(IOInterface $io) - { - $this->io = $io; - } - - /** - * @return IOInterface - */ - public function composerIo(): IOInterface + private function __construct(private IOInterface $io) { - return $this->io; } /** @@ -131,6 +114,16 @@ public function write(string ...$messages): void } } + /** + * @param string ...$messages + */ + public function writeRaw(string ...$messages): void + { + foreach ($messages as $message) { + $this->io->write($message); + } + } + /** * @param string ...$messages */ diff --git a/src/Util/ModeResolver.php b/src/Util/ModeResolver.php index 4ce0044..5618e52 100644 --- a/src/Util/ModeResolver.php +++ b/src/Util/ModeResolver.php @@ -20,16 +20,6 @@ class ModeResolver private const MODE_KEY = '$mode'; private const MODE_KEY_LEGACY = 'env'; - /** - * @var string|null - */ - private $mode; - - /** - * @var bool - */ - private $isDev; - /** * @param string|null $env * @param bool $isDev @@ -44,10 +34,10 @@ public static function new(?string $env, bool $isDev): ModeResolver * @param string|null $mode * @param bool $isDev */ - final private function __construct(?string $mode, bool $isDev) - { - $this->mode = $mode; - $this->isDev = $isDev; + final private function __construct( + private ?string $mode, + private bool $isDev + ) { } /** @@ -55,7 +45,7 @@ final private function __construct(?string $mode, bool $isDev) */ public function mode(): string { - if ($this->mode) { + if (($this->mode !== null) && ($this->mode !== '')) { return $this->mode; } @@ -64,20 +54,16 @@ public function mode(): string /** * @param array $config - * @return mixed|null - * - * phpcs:disable Inpsyde.CodeQuality.ReturnTypeDeclaration + * @return mixed */ - public function resolveConfig(array $config) + public function resolveConfig(array $config): mixed { - // phpcs:enable Inpsyde.CodeQuality.ReturnTypeDeclaration - $envs = $config[self::MODE_KEY] ?? $config[self::MODE_KEY_LEGACY] ?? null; - if (!$envs || !is_array($envs)) { + if (($envs === null) || ($envs === []) || !is_array($envs)) { return null; } - $envCandidates = $this->mode ? [$this->mode] : []; + $envCandidates = ($this->mode !== null) ? [$this->mode] : []; $this->isDev or $envCandidates[] = self::MODE_DEFAULT_NO_DEV; $envCandidates[] = self::MODE_DEFAULT; diff --git a/src/Util/SilentConsoleIo.php b/src/Util/SilentConsoleIo.php index 493d266..0d98ca2 100644 --- a/src/Util/SilentConsoleIo.php +++ b/src/Util/SilentConsoleIo.php @@ -15,9 +15,6 @@ /** * @psalm-suppress PropertyNotSetInConstructor - * - * phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration - * phpcs:disable Inpsyde.CodeQuality.ReturnTypeDeclaration */ class SilentConsoleIo extends ConsoleIO { @@ -41,7 +38,7 @@ private function __construct(ConsoleIO $io) /** * @return bool */ - public function isVerbose() + public function isVerbose(): bool { return false; } @@ -49,7 +46,7 @@ public function isVerbose() /** * @return bool */ - public function isVeryVerbose() + public function isVeryVerbose(): bool { return false; } @@ -57,7 +54,7 @@ public function isVeryVerbose() /** * @return bool */ - public function isDebug() + public function isDebug(): bool { return false; } @@ -68,8 +65,11 @@ public function isDebug() * @param int $verbosity * @return void */ - public function write($messages, $newline = true, $verbosity = self::NORMAL) - { + public function write( + mixed $messages, + bool $newline = true, + int $verbosity = self::NORMAL + ): void { } /** @@ -78,8 +78,11 @@ public function write($messages, $newline = true, $verbosity = self::NORMAL) * @param int $verbosity * @return void */ - public function writeError($messages, $newline = true, $verbosity = self::NORMAL) - { + public function writeError( + mixed $messages, + bool $newline = true, + int $verbosity = self::NORMAL + ): void { } /** @@ -88,8 +91,11 @@ public function writeError($messages, $newline = true, $verbosity = self::NORMAL * @param int $verbosity * @return void */ - public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL) - { + public function writeRaw( + mixed $messages, + bool $newline = true, + int $verbosity = self::NORMAL + ): void { } /** @@ -98,7 +104,10 @@ public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL) * @param int $verbosity * @return void */ - public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL) - { + public function writeErrorRaw( + mixed $messages, + bool $newline = true, + int $verbosity = self::NORMAL + ): void { } } diff --git a/tests/resources/02/composer.json b/tests/resources/02/composer.json index 2a48748..82bf4fa 100644 --- a/tests/resources/02/composer.json +++ b/tests/resources/02/composer.json @@ -6,7 +6,7 @@ "require": {}, "extra": { "composer-asset-compiler": { - "commands": "yarn", + "package-manager": "yarn", "script": "run" } } diff --git a/tests/resources/04/assets-compiler.json b/tests/resources/04/assets-compiler.json index cfeca58..cdf0ed1 100644 --- a/tests/resources/04/assets-compiler.json +++ b/tests/resources/04/assets-compiler.json @@ -1,5 +1,5 @@ { - "commands": "npm", + "package-manager": "npm", "wipe-node-modules": false, "auto-discover": false, "script": "gulp", diff --git a/tests/resources/assets-compiler.json b/tests/resources/assets-compiler.json index cfeca58..cdf0ed1 100644 --- a/tests/resources/assets-compiler.json +++ b/tests/resources/assets-compiler.json @@ -1,5 +1,5 @@ { - "commands": "npm", + "package-manager": "npm", "wipe-node-modules": false, "auto-discover": false, "script": "gulp", diff --git a/tests/src/FunctionalTestCase.php b/tests/src/FunctionalTestCase.php index ed24af8..c30706f 100644 --- a/tests/src/FunctionalTestCase.php +++ b/tests/src/FunctionalTestCase.php @@ -14,27 +14,17 @@ use Composer\Composer; use Composer\Factory as ComposerFactory; use Composer\IO\IOInterface; -use Composer\IO\NullIO; use Composer\Util\Filesystem; use Inpsyde\AssetsCompiler\Process\Factory as ProcessFactory; use Inpsyde\AssetsCompiler\Util\Factory; abstract class FunctionalTestCase extends \PHPUnit\Framework\TestCase { - /** @var string|null */ - protected $cwd = null; - - /** @var string|null */ - protected $baseDir = null; - - /** @var Factory|null */ - protected $factory = null; - - /** @var TestIo|null */ - protected $io = null; - - /** @var bool */ - private $composerInstalled = false; + protected string|null $cwd = null; + protected string|null $baseDir = null; + protected Factory|null $factory = null; + protected TestIo|null $io = null; + private bool $composerInstalled = false; /** * @return void @@ -120,7 +110,7 @@ protected function factoryFactory( * @param string $dir * @return void */ - protected function moveDir(string $dir) + protected function moveDir(string $dir): void { $this->baseDir = $dir; chdir($dir); diff --git a/tests/src/TestIo.php b/tests/src/TestIo.php index 09b85c2..6391bfa 100644 --- a/tests/src/TestIo.php +++ b/tests/src/TestIo.php @@ -13,32 +13,26 @@ use Composer\IO\BaseIO; -/* - * phpcs:disable Inpsyde.CodeQuality.ReturnTypeDeclaration - * phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration - */ class TestIo extends BaseIO { - /** - * @var int - */ - private $verbosity; + /** @var list */ + public array $outputs = []; - /** - * @var list - */ - public $outputs = []; + /** @var list */ + public array $errors = []; /** - * @var list + * @param int $verbosity */ - public $errors = []; - - public function __construct(int $verbosity = self::NORMAL) - { - $this->verbosity = $verbosity; + public function __construct( + private int $verbosity = self::NORMAL + ) { } + /** + * @param string $regex + * @return bool + */ public function hasOutputThatMatches(string $regex): bool { foreach ($this->outputs as $output) { @@ -50,6 +44,10 @@ public function hasOutputThatMatches(string $regex): bool return false; } + /** + * @param string $regex + * @return bool + */ public function hasErrorThatMatches(string $regex): bool { foreach ($this->errors as $output) { @@ -61,89 +59,183 @@ public function hasErrorThatMatches(string $regex): bool return false; } - public function isInteractive() + /** + * @return false + */ + public function isInteractive(): bool { return false; } - public function isVerbose() + /** + * @return bool + */ + public function isVerbose(): bool { return $this->verbosity > self::NORMAL; } - public function isVeryVerbose() + /** + * @return bool + */ + public function isVeryVerbose(): bool { return $this->verbosity > self::VERBOSE; } - public function isDebug() + /** + * @return bool + */ + public function isDebug(): bool { return $this->verbosity > self::VERY_VERBOSE; } - public function isDecorated() + /** + * @return false + */ + public function isDecorated(): bool { return false; } - public function write($messages, $newline = true, $verbosity = self::NORMAL) - { + /** + * @param string|list $messages + * @param bool $newline + * @param int $verbosity + * @return void + */ + public function write( + mixed $messages, + bool $newline = true, + int $verbosity = self::NORMAL + ): void { + if ($verbosity > $this->verbosity) { return; } - foreach ((array)$messages as $message) { + foreach ((array) $messages as $message) { $this->outputs[] = $message; } } - public function writeError($messages, $newline = true, $verbosity = self::NORMAL) - { + /** + * @param string|list $messages + * @param bool $newline + * @param int $verbosity + * @return void + */ + public function writeError( + mixed $messages, + bool $newline = true, + int $verbosity = self::NORMAL + ): void { + if ($verbosity > $this->verbosity) { return; } - foreach ((array)$messages as $message) { + foreach ((array) $messages as $message) { $this->errors[] = $message; } } - public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL) - { + /** + * @param string|list $messages + * @param bool $newline + * @param int|null $size + * @param int $verbosity + * @return void + */ + public function overwrite( + mixed $messages, + bool $newline = true, + ?int $size = null, + int $verbosity = self::NORMAL + ): void { + // TODO: Implement overwrite() method. } - public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL) - { + /** + * @param string|list $messages + * @param bool $newline + * @param int|null $size + * @param int $verbosity + * @return void + */ + public function overwriteError( + mixed $messages, + bool $newline = true, + ?int $size = null, + int $verbosity = self::NORMAL + ): void { + // TODO: Implement overwriteError() method. } - public function ask($question, $default = null) + /** + * @param string $question + * @param mixed|null $default + * @return mixed + */ + public function ask(string $question, mixed $default = null): mixed { return $default; } - public function askConfirmation($question, $default = true) + /** + * @param string $question + * @param mixed $default + * @return bool + */ + public function askConfirmation(string $question, bool $default = true): bool { return $default; } - public function askAndValidate($question, $validator, $attempts = null, $default = null) - { + /** + * @param string $question + * @param callable $validator + * @param int|null $attempts + * @param mixed $default + * @return mixed + */ + public function askAndValidate( + string $question, + callable $validator, + ?int $attempts = null, + mixed $default = null + ): mixed { + return $default; } - public function askAndHideAnswer($question) + /** + * @param string $question + * @return string|null + */ + public function askAndHideAnswer(string $question): ?string { return null; } + /** + * @param string $question + * @param array $choices + * @param mixed $default + * @param mixed $attempts + * @param string $errorMessage + * @param bool $multiselect + * @return mixed + */ public function select( - $question, - $choices, - $default, - $attempts = false, - $errorMessage = 'Value "%s" is invalid', - $multiselect = false - ) { + string $question, + array $choices, + mixed $default, + mixed $attempts = false, + string $errorMessage = 'Value "%s" is invalid', + bool $multiselect = false + ): mixed { return $default; } diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index a6b1552..a59c327 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -33,10 +33,12 @@ protected function factoryIo( ): Io { $input = \Mockery::mock(InputInterface::class); - $input->allows('isInteractive')->andReturns($interactive); + $input->allows('isInteractive') + ->andReturns($interactive); $output = \Mockery::mock(OutputInterface::class); - $output->allows('getVerbosity')->andReturn($verbosity); + $output->allows('getVerbosity') + ->andReturn($verbosity); $output->allows('isQuiet') ->andReturn($verbosity === OutputInterface::VERBOSITY_QUIET); $output->allows('isDebug') @@ -47,8 +49,6 @@ protected function factoryIo( ->andReturn($verbosity === OutputInterface::VERBOSITY_VERBOSE); $output->allows('write'); - $composerIo = new ConsoleIO($input, $output, new HelperSet()); - - return Io::new($composerIo); + return Io::new(new ConsoleIO($input, $output, new HelperSet())); } } diff --git a/tests/unit/Asset/ConfigUnitTest.php b/tests/unit/Asset/ConfigUnitTest.php index 2cc675f..267db84 100644 --- a/tests/unit/Asset/ConfigUnitTest.php +++ b/tests/unit/Asset/ConfigUnitTest.php @@ -190,39 +190,39 @@ private function assertConfig(Config $config, bool $byPackage): void private function configSample(): array { $json = <<<'JSON' -{ - "default-env": { - "foo": "bar" - }, - "env": { - "production": false, - "$default-no-dev": { - "package-manager": "npm", - "script": { - "env": { - "test": "build-test", - "$default": "build" - } + { + "default-env": { + "foo": "bar" }, - "pre-compiled": { - "env": { - "$default": { - "target": "./assets/", - "adapter": "gh-action-artifact", - "source": "assets-${ref}", - "config": { - "repository": "acme/some-theme" + "env": { + "production": false, + "$default-no-dev": { + "package-manager": "npm", + "script": { + "env": { + "test": "build-test", + "$default": "build" } }, - "local": { - "adapter": false + "pre-compiled": { + "env": { + "$default": { + "target": "./assets/", + "adapter": "gh-action-artifact", + "source": "assets-${ref}", + "config": { + "repository": "acme/some-theme" + } + }, + "local": { + "adapter": false + } + } } } } } - } -} -JSON; + JSON; return json_decode($json, true); } } diff --git a/tests/unit/Asset/FactoryUnitTest.php b/tests/unit/Asset/FactoryUnitTest.php index d5a3f1a..70f4d44 100644 --- a/tests/unit/Asset/FactoryUnitTest.php +++ b/tests/unit/Asset/FactoryUnitTest.php @@ -40,11 +40,11 @@ public function testCreateWithConfigAllowedPackageLevelAndDefaults(): void $factory = $this->factoryFactory(); $json = <<<'JSON' -{ - "dependencies": "update", - "script": "destroy" -} -JSON; + { + "dependencies": "update", + "script": "destroy" + } + JSON; $package = new Package('test/test-package', '1.0.0.0', 'v1'); $asset = $factory->attemptFactory( @@ -65,20 +65,20 @@ public function testCreateWithConfigAllowedPackageLevelAndDefaults(): void public function testCreateWithConfigByEnvAllowedPackageLevelAndDefaults(): void { $json = <<<'JSON' -{ - "composer-asset-compiler": { - "env": { - "meh": { - "script": ["hello", "world"] - }, - "$default": { - "dependencies": "update", - "script": "test" - } + { + "composer-asset-compiler": { + "env": { + "meh": { + "script": ["hello", "world"] + }, + "$default": { + "dependencies": "update", + "script": "test" + } + } + } } - } -} -JSON; + JSON; $factory = $this->factoryFactory('meh'); @@ -106,11 +106,11 @@ public function testCreateWithConfigNotAllowedPackageLevelAndDefaults(): void $factory = $this->factoryFactory(); $json = <<<'JSON' -{ - "dependencies": "update", - "script": "destroy" -} -JSON; + { + "dependencies": "update", + "script": "destroy" + } + JSON; $package = new Package('test/test-package', '1.0.0.0', 'v1'); $package->setExtra(json_decode($json, true)); @@ -129,22 +129,22 @@ public function testCreateWithConfigNotAllowedPackageLevelAndDefaults(): void /** * @test */ - public function testCreateWithConfigAllowedPackageLevelAndNoDefaults() + public function testCreateWithConfigAllowedPackageLevelAndNoDefaults(): void { $factory = $this->factoryFactory(); $json = <<<'JSON' -{ - "dependencies": "update", - "script": "destroy" -} -JSON; + { + "dependencies": "update", + "script": "destroy" + } + JSON; $package = new Package('test/test-package', '1.0.0.0', 'v1'); $asset = $factory->attemptFactory( $package, $this->factoryConfig($json), - Defaults::empty() + Defaults::newEmpty() ); static::assertTrue($asset->isValid()); @@ -161,17 +161,17 @@ public function testCreateWithConfigNotAllowedPackageLevelAndNoDefaults(): void $factory = $this->factoryFactory(); $json = <<<'JSON' -{ - "dependencies": "update", - "script": "destroy" -} -JSON; + { + "dependencies": "update", + "script": "destroy" + } + JSON; $package = new Package('test/test-package', '1.0.0.0', 'v1'); $asset = $factory->attemptFactory( $package, $this->factoryConfig($json), - Defaults::empty() + Defaults::newEmpty() ); static::assertTrue($asset->isValid()); @@ -218,7 +218,7 @@ public function testCreateWithoutConfigAllowedPackageLevelAndNoDefaults(): void ] ); - $asset = $factory->attemptFactory($package, null, Defaults::empty()); + $asset = $factory->attemptFactory($package, null, Defaults::newEmpty()); static::assertTrue($asset->isValid()); static::assertSame(['this_is_nice'], $asset->script()); @@ -270,14 +270,14 @@ public function testCreateWithoutConfigAllowedPackageLevelButNoPackageConfigAndD /** * @test */ - public function testCreateWithoutConfigAllowedPackageLevelButNoPackageConfigAndNoDefaults(): void + public function testCreateWithoutConfigAllowedPackageLevelNoPackageConfigAndNoDefaults(): void { $factory = $this->factoryFactory('develop'); $package = new Package('test/test-package', '1.0.0.0', 'v1'); $package->setExtra([]); - $asset = $factory->attemptFactory($package, null, Defaults::empty()); + $asset = $factory->attemptFactory($package, null, Defaults::newEmpty()); static::assertNull($asset); } @@ -308,7 +308,7 @@ public function testCreateWithoutConfigAllowedPackageLevelByEnvAndPackageEnv(): ] ); - $asset = $factory->attemptFactory($package, null, Defaults::empty()); + $asset = $factory->attemptFactory($package, null, Defaults::newEmpty()); static::assertTrue($asset->isValid()); static::assertSame('prod', $asset->env()['ENCORE_ENV']); @@ -317,10 +317,10 @@ public function testCreateWithoutConfigAllowedPackageLevelByEnvAndPackageEnv(): static::assertSame(['encore ${ENCORE_ENV}'], $scripts); $script = array_pop($scripts); - $yarnNoEnv = PackageManager::fromDefault('yarn', []); + $yarnNoEnv = PackageManager::fromDefault('yarn'); static::assertSame('yarn encore prod', $yarnNoEnv->scriptCmd($script, $asset->env())); - $yarnWithEnv = PackageManager::fromDefault('yarn', ['ENCORE_ENV' => 'dev']); + $yarnWithEnv = PackageManager::fromDefault('yarn'); static::assertSame( 'yarn encore prod', $yarnWithEnv->scriptCmd($script, $asset->env()) @@ -337,7 +337,7 @@ public function testCreateWithOnlyScript(): void $package = new Package('test/test-package', '1.0.0.0', 'v1'); $package->setExtra(['composer-asset-compiler' => "build"]); - $asset = $factory->attemptFactory($package, null, Defaults::empty()); + $asset = $factory->attemptFactory($package, null, Defaults::newEmpty()); static::assertTrue($asset->isValid()); static::assertSame(['build'], $asset->script()); @@ -361,7 +361,7 @@ public function testCreateWithScriptAnNoDependencies(): void ] ); - $asset = $factory->attemptFactory($package, null, Defaults::empty()); + $asset = $factory->attemptFactory($package, null, Defaults::newEmpty()); static::assertTrue($asset->isValid()); static::assertSame(['build'], $asset->script()); @@ -377,11 +377,11 @@ public function testForRootPackage(): void $factory = $this->factoryFactory(); $json = <<<'JSON' -{ - "dependencies": "update", - "script": "test" -} -JSON; + { + "dependencies": "update", + "script": "test" + } + JSON; $rootPackage = new RootPackage('test/root-package', '1.0.0.0', 'v1'); $noRootPackage = new Package('test/some-package', '1.0.0.0', 'v1'); diff --git a/tests/unit/Asset/FinderUnitTest.php b/tests/unit/Asset/FinderUnitTest.php index 364a4b1..be0304d 100644 --- a/tests/unit/Asset/FinderUnitTest.php +++ b/tests/unit/Asset/FinderUnitTest.php @@ -211,18 +211,18 @@ private function findPackages(?array $settings): array $finder = Finder::new( $rootConfig->packagesData(), $envResolver, - $defaults ? Defaults::new($defaults) : Defaults::empty(), + $defaults ? Defaults::new($defaults) : Defaults::newEmpty(), $config, $rootConfig->stopOnFailure() ); - return $finder->find($this->composerRepo(), $root, $factory, $rootConfig->autoDiscover()); + return $finder->find($this->factoryComposerRepo(), $root, $factory, $rootConfig->autoDiscover()); } /** * @return RepositoryInterface */ - private function composerRepo(): RepositoryInterface + private function factoryComposerRepo(): RepositoryInterface { $loader = new ArrayLoader(); diff --git a/tests/unit/Asset/LockerUnitTest.php b/tests/unit/Asset/LockerUnitTest.php index c17070d..f3bcf34 100644 --- a/tests/unit/Asset/LockerUnitTest.php +++ b/tests/unit/Asset/LockerUnitTest.php @@ -17,6 +17,7 @@ use Inpsyde\AssetsCompiler\Asset\HashBuilder; use Inpsyde\AssetsCompiler\Asset\Locker; use Inpsyde\AssetsCompiler\Asset\Asset; +use Inpsyde\AssetsCompiler\Asset\PathsFinder; use Inpsyde\AssetsCompiler\Util\ModeResolver; use Inpsyde\AssetsCompiler\Util\Io; use Inpsyde\AssetsCompiler\Tests\UnitTestCase; @@ -41,10 +42,11 @@ public function testIsLockedIsFalseIfNoFileExists(): void public function testIsLockedIsFalseForEmptyFileAndErrorWritten(): void { $io = \Mockery::mock(Io::class); + $io->allows('isVerbose')->andReturn(true); $io ->expects('writeVerboseError') ->andReturnUsing( - static function (string $arg) { + static function (string $arg): void { static::assertStringContainsString('lock file', $arg); } ); @@ -88,7 +90,6 @@ public function testIsLockedIsFalseIfHashDiffers(): void public function testIsLockedIsFalseBeforeLockAndTrueAfterThat(): void { $packagesJson = (new vfsStreamFile('package.json', 0777))->withContent('{}'); - $dir = vfsStream::setup('exampleDir'); $dir->addChild($packagesJson); @@ -131,10 +132,11 @@ public function testIsLockedIsFalseIfIgnoreByName(): void ]); $io = \Mockery::mock(Io::class); + $io->allows('isVerbose')->andReturn(false); $io ->expects('writeVerboseComment') ->andReturnUsing( - static function (string $arg) { + static function (string $arg): void { static::assertStringContainsString('ignoring', strtolower($arg)); static::assertStringContainsString('test/x-y', $arg); } @@ -162,11 +164,17 @@ static function (string $arg) { */ private function factoryLocker(?Io $io = null, string $ignoreLock = ''): Locker { - $io = $io ?? Io::new(\Mockery::mock(IOInterface::class)); + if ($io === null) { + $cIo = \Mockery::mock(IOInterface::class); + $cIo->allows('isVerbose')->andReturn(false); + $io = Io::new($cIo); + } + + $finder = PathsFinder::new(new \EmptyIterator(), new Filesystem(), $io, __DIR__); return new Locker( $io, - HashBuilder::new(new Filesystem(), $io), + HashBuilder::new($finder, $io), $ignoreLock ); } diff --git a/tests/unit/Asset/PackageUnitTest.php b/tests/unit/Asset/PackageUnitTest.php index f59516b..bba070e 100644 --- a/tests/unit/Asset/PackageUnitTest.php +++ b/tests/unit/Asset/PackageUnitTest.php @@ -26,11 +26,11 @@ class PackageUnitTest extends UnitTestCase public function testCreatePackageFromJson(): void { $json = <<factoryPackage($json); static::assertTrue($package->isValid()); @@ -46,10 +46,10 @@ public function testCreatePackageFromJson(): void public function testCreatePackageFromJsonDependenciesOnly(): void { $json = <<factoryPackage($json); static::assertTrue($package->isValid()); @@ -64,10 +64,10 @@ public function testCreatePackageFromJsonDependenciesOnly(): void public function testCreatePackageFromScriptOnly(): void { $json = <<factoryPackage($json); static::assertTrue($package->isValid()); @@ -82,10 +82,10 @@ public function testCreatePackageFromScriptOnly(): void public function testInvalidScriptsAreStrippedOut(): void { $json = <<factoryPackage($json); static::assertTrue($package->isValid()); diff --git a/tests/unit/Asset/RootConfigUnitTest.php b/tests/unit/Asset/RootConfigUnitTest.php index de1ae6b..cc43c36 100644 --- a/tests/unit/Asset/RootConfigUnitTest.php +++ b/tests/unit/Asset/RootConfigUnitTest.php @@ -28,18 +28,18 @@ class RootConfigUnitTest extends UnitTestCase public function testBoolSettingsTrue(): void { $json = <<<'JSON' -{ - "composer-asset-compiler": { - "auto-discover": true, - "auto-run": "true", - "wipe-node-modules": true, - "stop-on-failure": "yes", - "packages": [], - "defaults": [], - "commands": null - } -} -JSON; + { + "composer-asset-compiler": { + "auto-discover": true, + "auto-run": "true", + "wipe-node-modules": true, + "stop-on-failure": "yes", + "packages": [], + "defaults": [], + "commands": null + } + } + JSON; $config = $this->factoryConfig($json); @@ -54,18 +54,18 @@ public function testBoolSettingsTrue(): void public function testBoolSettingsFalse(): void { $json = <<<'JSON' -{ - "composer-asset-compiler": { - "auto-discover": false, - "auto-run": "false", - "wipe-node-modules": true, - "stop-on-failure": "no", - "packages": [], - "defaults": [], - "commands": null - } -} -JSON; + { + "composer-asset-compiler": { + "auto-discover": false, + "auto-run": "false", + "wipe-node-modules": true, + "stop-on-failure": "no", + "packages": [], + "defaults": [], + "commands": null + } + } + JSON; $config = $this->factoryConfig($json); @@ -80,18 +80,18 @@ public function testBoolSettingsFalse(): void public function testStopOnFailureAdvanced(): void { $json = <<<'JSON' -{ - "composer-asset-compiler": { - "packages": [], - "stop-on-failure": { - "env": { - "$default": true, - "test": "false" + { + "composer-asset-compiler": { + "packages": [], + "stop-on-failure": { + "env": { + "$default": true, + "test": "false" + } + } } } - } -} -JSON; + JSON; $stopForTest = $this->factoryConfig($json, 'test')->stopOnFailure(); $stopForProd = $this->factoryConfig($json, 'production')->stopOnFailure(); @@ -105,18 +105,18 @@ public function testStopOnFailureAdvanced(): void public function testMaxProcesses(): void { $json = <<<'JSON' -{ - "composer-asset-compiler": { - "packages": [], - "max-processes": { - "env": { - "$default": 4, - "test": "10" + { + "composer-asset-compiler": { + "packages": [], + "max-processes": { + "env": { + "$default": 4, + "test": "10" + } + } } } - } -} -JSON; + JSON; $forTest = $this->factoryConfig($json, 'test')->maxProcesses(); $forProd = $this->factoryConfig($json, 'production')->maxProcesses(); @@ -130,19 +130,19 @@ public function testMaxProcesses(): void public function testProcessesPoll(): void { $json = <<<'JSON' -{ - "composer-asset-compiler": { - "packages": [], - "processes-poll": { - "env": { - "$default": 100000, - "test": 500000, - "low": 500 + { + "composer-asset-compiler": { + "packages": [], + "processes-poll": { + "env": { + "$default": 100000, + "test": 500000, + "low": 500 + } + } } } - } -} -JSON; + JSON; $forTest = $this->factoryConfig($json, 'test')->processesPoll(); $forProd = $this->factoryConfig($json, 'production')->processesPoll(); @@ -159,13 +159,13 @@ public function testProcessesPoll(): void public function testWipeNotAllowedForSymlinkedPackages(): void { $json = <<<'JSON' -{ - "composer-asset-compiler": { - "packages": [], - "wipe-node-modules": "force" - } -} -JSON; + { + "composer-asset-compiler": { + "packages": [], + "wipe-node-modules": "force" + } + } + JSON; $filesystem = \Mockery::mock(Filesystem::class)->makePartial(); $filesystem ->expects('isSymlinkedDirectory') @@ -185,13 +185,13 @@ public function testWipeNotAllowedForSymlinkedPackages(): void public function testWipeNotAllowedIfNodeModulesExistsAndConfigIsTrue(): void { $json = <<<'JSON' -{ - "composer-asset-compiler": { - "packages": [], - "wipe-node-modules": true - } -} -JSON; + { + "composer-asset-compiler": { + "packages": [], + "wipe-node-modules": true + } + } + JSON; $config = $this->factoryConfig($json); $dir = vfsStream::setup('exampleDir'); @@ -207,19 +207,19 @@ public function testWipeNotAllowedIfNodeModulesExistsAndConfigIsTrue(): void public function testWipeAllowedAdvanced(): void { $json = <<<'JSON' -{ - "composer-asset-compiler": { - "packages": [], - "wipe-node-modules": { - "env": { - "test": true, - "prod": "force", - "$default": false + { + "composer-asset-compiler": { + "packages": [], + "wipe-node-modules": { + "env": { + "test": true, + "prod": "force", + "$default": false + } + } } } - } -} -JSON; + JSON; $configTest = $this->factoryConfig($json, 'test'); $configProd = $this->factoryConfig($json, 'prod'); $configStaging = $this->factoryConfig($json, 'staging'); @@ -276,7 +276,7 @@ private function factoryConfig( ): RootConfig { $package = new RootPackage('company/my-root-package', '1.0.0.0', '1.0'); - $package->setExtra($json ? (array)json_decode($json, true) : []); + $package->setExtra($json ? (array) json_decode($json, true) : []); $config = Config::forComposerPackage( $package, diff --git a/tests/unit/PackageManager/FinderUnitTest.php b/tests/unit/PackageManager/FinderUnitTest.php index d1e3bf1..cd0259d 100644 --- a/tests/unit/PackageManager/FinderUnitTest.php +++ b/tests/unit/PackageManager/FinderUnitTest.php @@ -338,7 +338,7 @@ public function factoryAsset(string $dir): Asset $config->defaultEnv() ); - return $assetFactory->attemptFactory($package, null, Defaults::empty()); + return $assetFactory->attemptFactory($package, null, Defaults::newEmpty()); } /** diff --git a/tests/unit/PackageManager/PackageManagerUnitTest.php b/tests/unit/PackageManager/PackageManagerUnitTest.php index 3fed9f0..6cd56af 100644 --- a/tests/unit/PackageManager/PackageManagerUnitTest.php +++ b/tests/unit/PackageManager/PackageManagerUnitTest.php @@ -18,7 +18,10 @@ class PackageManagerUnitTest extends UnitTestCase { - public function testFromDefaultFailsForUnknown() + /** + * @test + */ + public function testFromDefaultFailsForUnknown(): void { $commands = PackageManager::fromDefault('foo'); $io = $this->factoryIo(); @@ -29,7 +32,10 @@ public function testFromDefaultFailsForUnknown() static::assertNull($commands->scriptCmd('x')); } - public function testFromDefaultWorksForKnown() + /** + * @test + */ + public function testFromDefaultWorksForKnown(): void { $yarn = PackageManager::fromDefault('Yarn'); $npm = PackageManager::fromDefault('NPM'); @@ -47,9 +53,10 @@ public function testFromDefaultWorksForKnown() } /** + * @test * @runInSeparateProcess */ - public function testDiscoverYarn() + public function testDiscoverYarn(): void { $executor = \Mockery::mock(ProcessExecutor::class); $executor @@ -69,9 +76,10 @@ public function testDiscoverYarn() } /** + * @test * @runInSeparateProcess */ - public function testDiscoverNpm() + public function testDiscoverNpm(): void { $executor = \Mockery::mock(ProcessExecutor::class); @@ -93,9 +101,10 @@ public function testDiscoverNpm() } /** + * @test * @runInSeparateProcess */ - public function testDiscoverNothing() + public function testDiscoverNothing(): void { $executor = \Mockery::mock(ProcessExecutor::class); @@ -117,9 +126,9 @@ public function testDiscoverNothing() } /** - * @return void + * @test */ - public function testYarnVerbosity() + public function testYarnVerbosity(): void { $commands = PackageManager::fromDefault('Yarn'); @@ -139,9 +148,9 @@ public function testYarnVerbosity() } /** - * @return void + * @test */ - public function testNpmVerbosity() + public function testNpmVerbosity(): void { $commands = PackageManager::fromDefault('npm'); @@ -161,9 +170,9 @@ public function testNpmVerbosity() } /** - * @return void + * @test */ - public function testNpmVerbosityWhenVerbosityInCommandDefined() + public function testNpmVerbosityWhenVerbosityInCommandDefined(): void { $commands = PackageManager::new( [ @@ -197,7 +206,10 @@ public function testNpmVerbosityWhenVerbosityInCommandDefined() static::assertSame('npm update -d', $commands->updateCmd($quietNoInt)); } - public function testAdditionalArguments() + /** + * @test + */ + public function testAdditionalArguments(): void { $yarn = PackageManager::fromDefault('yarn'); $npm = PackageManager::fromDefault('npm');