Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 0.4.1 Under development

- docs: add AI guidance and the Foxy integration skill.
- docs: complete Foxy PHPDoc descriptions for the hosted API reference.

## 0.4.0 October 11, 2026

Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</testsuite>
</testsuites>

<source>
<source restrictWarnings="true">
<include>
<directory suffix=".php">./src</directory>
</include>
Expand Down
27 changes: 27 additions & 0 deletions src/Asset/AbstractAssetManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@
use function sprintf;
use function trim;

/**
* Runs an external frontend package manager and validates its executable version.
*/
abstract class AbstractAssetManager extends AbstractManifestAssetManager
{
private string|null $version = '';

/**
* Creates a manager that executes frontend commands for the configured asset manifest.
*
* @param IOInterface $io Composer output used for progress messages.
* @param Config $config Foxy configuration.
* @param ProcessExecutor $executor Process executor used to invoke the manager.
* @param Filesystem $fs Filesystem used by manifest handling.
* @param VersionConverterInterface|null $versionConverter Converter for manager version output.
*/
public function __construct(
IOInterface $io,
Config $config,
Expand All @@ -41,6 +53,9 @@ public function __construct(
*/
abstract protected function getAuditCommand(bool $noDev): string;

/**
* Runs the manager audit command against the current lock file.
*/
public function audit(bool $noDev): AuditProcessResult
{
if (!$this->hasLockFile()) {
Expand Down Expand Up @@ -72,11 +87,17 @@ function () use ($noDev): AuditProcessResult {
);
}

/**
* Returns whether the manager executable reports a detectable version.
*/
public function isAvailable(): bool
{
return null !== $this->getVersion();
}

/**
* Installs or updates frontend dependencies according to the manager state and configuration.
*/
public function run(): int
{
if (!$this->config->isEnabled('run-asset-manager')) {
Expand All @@ -100,6 +121,9 @@ public function run(): int
);
}

/**
* Validates the installed manager version against Foxy's supported and configured constraints.
*/
public function validate(): void
{
$version = $this->getVersion();
Expand Down Expand Up @@ -209,6 +233,9 @@ protected function getUpdateCommand(): string
return $this->buildCommand('update', 'update');
}

/**
* Returns the normalized manager version, or `null` when it cannot be detected.
*/
protected function getVersion(): string|null
{
if ($this->version === '' && $this->versionConverter !== null) {
Expand Down
47 changes: 47 additions & 0 deletions src/Asset/AbstractManifestAssetManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,27 @@ abstract class AbstractManifestAssetManager implements AssetManagerInterface
{
final public const string NODE_MODULES_PATH = './node_modules';

/**
* Whether an existing frontend installation may be updated.
*/
protected bool $updatable = true;

/**
* Creates a manager with the services shared by asset manifest operations.
*
* @param IOInterface $io Composer output used for progress messages.
* @param Config $config Foxy configuration.
* @param Filesystem $fs Filesystem used to resolve and update manifest paths.
*/
public function __construct(
protected IOInterface $io,
protected Config $config,
protected Filesystem $fs,
) {}

/**
* Merges Composer asset dependencies into the frontend package manifest.
*
* @throws Exception|ParsingException
*/
public function addDependencies(RootPackageInterface $rootPackage, array $dependencies): AssetPackageInterface
Expand All @@ -64,31 +76,49 @@ public function addDependencies(RootPackageInterface $rootPackage, array $depend
return $assetPackage->write();
}

/**
* Returns the frontend package manifest path.
*/
public function getPackageJsonPath(): string
{
return $this->getRootPackagePath($this->getPackageName());
}

/**
* Returns the frontend package manifest filename.
*/
public function getPackageName(): string
{
return 'package.json';
}

/**
* Returns whether the manager lock file exists.
*/
public function hasLockFile(): bool
{
return file_exists($this->getLockFilePath());
}

/**
* Returns whether both the dependency directory and package manifest exist.
*/
public function isInstalled(): bool
{
return is_dir($this->getNodeModulesPath()) && file_exists($this->getPackageJsonPath());
}

/**
* Returns whether an existing frontend installation may be updated.
*/
public function isUpdatable(): bool
{
return $this->updatable && $this->isInstalled();
}

/**
* Enables or disables updates of an existing frontend installation.
*/
public function setUpdatable(bool $updatable): static
{
$this->updatable = $updatable;
Expand All @@ -97,6 +127,8 @@ public function setUpdatable(bool $updatable): static
}

/**
* Handles dependencies already present in the frontend installation.
*
* @param list<string> $names the asset package name of composer dependencies.
*/
protected function actionWhenComposerDependenciesAreAlreadyInstalled(array $names): void
Expand All @@ -117,6 +149,9 @@ protected function actionWhenComposerDependenciesAreMerged(
// do nothing by default
}

/**
* Returns the manager lock-file path under the root package directory.
*/
protected function getLockFilePath(): string
{
return $this->getRootPackagePath($this->getLockPackageName());
Expand All @@ -132,6 +167,9 @@ protected function getManagerConfig(): Config
return $this->config->forManager($this->getName());
}

/**
* Returns the configured manager working directory, or `null` to use the process default.
*/
protected function getManagerWorkingDirectory(): string|null
{
if (null === $this->getConfiguredRootPackageDir()) {
Expand All @@ -149,11 +187,17 @@ protected function getManagerWorkingDirectory(): string|null
return $rootPackageDir;
}

/**
* Returns the `node_modules` path under the root package directory.
*/
protected function getNodeModulesPath(): string
{
return $this->getRootPackagePath(ltrim(self::NODE_MODULES_PATH, './'));
}

/**
* Returns the resolved root package directory.
*/
protected function getRootPackageDir(): string
{
$rootPackageDir = $this->getConfiguredRootPackageDir();
Expand All @@ -177,6 +221,9 @@ protected function getRootPackageDir(): string
return rtrim($this->getCurrentDirectory(), '/\\') . DIRECTORY_SEPARATOR . $rootPackageDir;
}

/**
* Resolves a path under the root package directory.
*/
protected function getRootPackagePath(string $path): string
{
return rtrim($this->getRootPackageDir(), '/\\') . DIRECTORY_SEPARATOR . $path;
Expand Down
8 changes: 8 additions & 0 deletions src/Asset/AssetManagerFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

use function count;

/**
* Selects a configured asset manager by name, lock file, or executable availability.
*/
final class AssetManagerFinder
{
/**
Expand All @@ -16,6 +19,8 @@ final class AssetManagerFinder
private array $managers = [];

/**
* Registers the asset managers available for selection.
*
* @param AssetManagerInterface[] $managers The asset managers
*/
public function __construct(array $managers = [])
Expand All @@ -25,6 +30,9 @@ public function __construct(array $managers = [])
}
}

/**
* Registers an asset manager by its unique name.
*/
public function addManager(AssetManagerInterface $manager): void
{
$this->managers[$manager->getName()] = $manager;
Expand Down
3 changes: 3 additions & 0 deletions src/Asset/AssetManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use Foxy\Audit\{AuditParserInterface, AuditProcessResult};
use Foxy\Exception\RuntimeException;

/**
* Defines frontend dependency manager operations used by the Foxy solver.
*/
interface AssetManagerInterface
{
/**
Expand Down
20 changes: 20 additions & 0 deletions src/Asset/AssetPackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

use const DIRECTORY_SEPARATOR;

/**
* Maintains Composer-provided dependencies in a frontend package manifest.
*/
final class AssetPackage implements AssetPackageInterface
{
public const string COMPOSER_PREFIX = '@composer-asset/';
Expand All @@ -32,6 +35,8 @@ final class AssetPackage implements AssetPackageInterface
private array $package = [];

/**
* Loads the frontend package manifest and injects metadata inherited from the Composer root package.
*
* @param RootPackageInterface $rootPackage The composer root package
* @param JsonFile $jsonFile The JSON file
*
Expand All @@ -52,6 +57,8 @@ public function __construct(
}

/**
* Adds Composer asset dependencies and returns the names that were already installed.
*
* @param array $dependencies The dependencies
*
* @return list<string> The existing packages.
Expand Down Expand Up @@ -80,6 +87,9 @@ public function addNewDependencies(array $dependencies): array
return $existingPackages;
}

/**
* Returns Composer-managed dependencies currently declared in the package manifest.
*/
public function getInstalledDependencies(): array
{
$installedAssets = [];
Expand All @@ -99,12 +109,17 @@ public function getInstalledDependencies(): array
return $installedAssets;
}

/**
* Returns the complete frontend package manifest.
*/
public function getPackage(): array
{
return $this->package;
}

/**
* Removes Composer-managed dependencies absent from the current dependency set.
*
* @psalm-suppress MixedArrayAccess
*/
public function removeUnusedDependencies(array $dependencies): self
Expand All @@ -120,6 +135,9 @@ public function removeUnusedDependencies(array $dependencies): self
return $this;
}

/**
* Replaces the in-memory frontend package manifest.
*/
public function setPackage(array $package): self
{
$this->package = $package;
Expand All @@ -128,6 +146,8 @@ public function setPackage(array $package): self
}

/**
* Writes the in-memory package manifest to its JSON file.
*
* @throws Exception
*/
public function write(): self
Expand Down
3 changes: 3 additions & 0 deletions src/Asset/AssetPackageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Foxy\Asset;

/**
* Defines mutable access to a frontend package manifest and its Composer-managed dependencies.
*/
interface AssetPackageInterface
{
/**
Expand Down
Loading
Loading