Add an unpack command to allow ejection of code from recipes.
This commit is contained in:
parent
88cd7ed3a0
commit
54857a20a3
17
README.md
17
README.md
|
@ -15,6 +15,7 @@ These recipes allow for the following features:
|
|||
- A `require-recipe` command to inline a recipe into the root composer.json, allowing the developer to customise the
|
||||
recipe dependencies without mandating the inclusion of all requirements directly.
|
||||
- An `update-recipe` command to upgrade to a newer version of a recipe.
|
||||
- An `unpack` command to eject the recipe from the current project.
|
||||
|
||||
## Example output
|
||||
|
||||
|
@ -182,3 +183,19 @@ public/
|
|||
blog.css
|
||||
composer.json
|
||||
```
|
||||
|
||||
## Unpacking a recipe
|
||||
Sometimes you need to remove the recipe configuration for more control over your dependency set. In these situations
|
||||
you can unpack a SilverStripe recipe.
|
||||
|
||||
To unpack a recipe you can run the following command
|
||||
```shell
|
||||
composer unpack silverstripe/recipe-core
|
||||
```
|
||||
|
||||
This commands moves the composer requirements out of the recipe and puts them into the root `composer.json`. This allows
|
||||
the ability to easily modify recipe requirements when you need to.
|
||||
|
||||
###Credit
|
||||
Parts of the unpack system were influenced by, or taken completely, from the work of Fabien Potencier on
|
||||
[Symfony Flex](https://github.com/symfony/flex/blob/master/LICENSE).
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
"lint-clean": "phpcbf src/"
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^1.1"
|
||||
"composer-plugin-api": "^1.1",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "^1.2"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\RecipePlugin;
|
||||
namespace SilverStripe\RecipePlugin\Command;
|
||||
|
||||
use Composer\Command\RequireCommand;
|
||||
use Composer\Command\UpdateCommand;
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\RecipePlugin;
|
||||
namespace SilverStripe\RecipePlugin\Command;
|
||||
|
||||
use Composer\Command\BaseCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
@ -11,7 +11,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||
* Provides the 'require-recipe' command which allows a new recipe to be installed, but also
|
||||
* soft-updates any existing recipe.
|
||||
*/
|
||||
class RequireRecipeCommand extends BaseCommand
|
||||
class RequireRecipe extends BaseCommand
|
||||
{
|
||||
use RecipeCommandBehaviour;
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\RecipePlugin\Command;
|
||||
|
||||
use Composer\Command\BaseCommand;
|
||||
use Composer\Config\JsonConfigSource;
|
||||
use Composer\Factory;
|
||||
use Composer\Installer;
|
||||
use Composer\Json\JsonFile;
|
||||
use Composer\Package\Locker;
|
||||
use Composer\Package\Version\VersionParser;
|
||||
use SilverStripe\RecipePlugin\PackageResolver;
|
||||
use SilverStripe\RecipePlugin\Unpack\Unpacker;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com> - See README.md for details
|
||||
*/
|
||||
class UnpackCommand extends BaseCommand
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('silverstripe:unpack')
|
||||
->setAliases(['unpack'])
|
||||
->setDescription('Unpacks a SilverStripe recipe.')
|
||||
->addArgument(
|
||||
'packages',
|
||||
InputArgument::IS_ARRAY | InputArgument::REQUIRED,
|
||||
'Installed packages to unpack.'
|
||||
)
|
||||
->addUsage('silverstripe/recipe-core')
|
||||
->setHelp(
|
||||
<<<HELP
|
||||
This command unpacks a SilverStripe recipe and removes it from your requirements. Useful when you want to eject
|
||||
from a recipe for more control over version constraints.
|
||||
HELP
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$composer = $this->getComposer();
|
||||
$io = $this->getIO();
|
||||
|
||||
$resolver = new PackageResolver();
|
||||
$packages = $resolver->resolve($input->getArgument('packages'), true);
|
||||
$json = new JsonFile(Factory::getComposerFile());
|
||||
$manipulator = new JsonConfigSource($json);
|
||||
$locker = $composer->getLocker();
|
||||
$lockData = $locker->getLockData();
|
||||
$installedRepo = $composer->getRepositoryManager()->getLocalRepository();
|
||||
$versionParser = new VersionParser();
|
||||
|
||||
$data = [];
|
||||
foreach ($versionParser->parseNameVersionPairs($packages) as $package) {
|
||||
if (null === $pkg = $installedRepo->findPackage($package['name'], '*')) {
|
||||
$io->writeError(sprintf('<error>Package %s is not installed</error>', $package['name']));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$dev = false;
|
||||
foreach ($lockData['packages-dev'] as $p) {
|
||||
if ($package['name'] === $p['name']) {
|
||||
$dev = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
$data[] = [
|
||||
'pkg' => $pkg,
|
||||
'dev' => $dev,
|
||||
];
|
||||
}
|
||||
|
||||
$unpacker = new Unpacker($composer, $resolver);
|
||||
$result = $unpacker->unpack($data);
|
||||
|
||||
|
||||
// remove the packages themselves
|
||||
if (!$result->getUnpacked()) {
|
||||
$io->writeError('<info>Nothing to unpack</info>');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($result->getUnpacked() as $pkg) {
|
||||
$io->writeError(sprintf('<info>Unpacked %s dependencies</info>', $pkg->getName()));
|
||||
}
|
||||
|
||||
foreach ($result->getUnpacked() as $package) {
|
||||
$manipulator->removeLink('require-dev', $package->getName());
|
||||
foreach ($lockData['packages-dev'] as $i => $pkg) {
|
||||
if ($package->getName() === $pkg['name']) {
|
||||
unset($lockData['packages-dev'][$i]);
|
||||
}
|
||||
}
|
||||
$manipulator->removeLink('require', $package->getName());
|
||||
foreach ($lockData['packages'] as $i => $pkg) {
|
||||
if ($package->getName() === $pkg['name']) {
|
||||
unset($lockData['packages'][$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$lockData['packages'] = array_values($lockData['packages']);
|
||||
$lockData['packages-dev'] = array_values($lockData['packages-dev']);
|
||||
$lockData['content-hash'] = $locker->getContentHash(file_get_contents($json->getPath()));
|
||||
$lockFile = new JsonFile(substr($json->getPath(), 0, -4) . 'lock', null, $io);
|
||||
$lockFile->write($lockData);
|
||||
|
||||
// force removal of files under vendor/
|
||||
$locker = new Locker($io, $lockFile, $composer->getRepositoryManager(), $composer->getInstallationManager(), file_get_contents($json->getPath()));
|
||||
$composer->setLocker($locker);
|
||||
$install = Installer::create($io, $composer);
|
||||
$install
|
||||
->setDevMode(true)
|
||||
->setDumpAutoloader(false)
|
||||
->setRunScripts(false)
|
||||
->setSkipSuggest(true)
|
||||
->setIgnorePlatformRequirements(true);
|
||||
|
||||
return $install->run();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\RecipePlugin;
|
||||
namespace SilverStripe\RecipePlugin\Command;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Composer\Command\BaseCommand;
|
||||
|
@ -8,7 +8,7 @@ use Symfony\Component\Console\Input\InputArgument;
|
|||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class UpdateRecipeCommand extends BaseCommand
|
||||
class UpdateRecipe extends BaseCommand
|
||||
{
|
||||
use RecipeCommandBehaviour;
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\RecipePlugin;
|
||||
|
||||
use Composer\Package\Version\VersionParser;
|
||||
use Composer\Repository\PlatformRepository;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com> - See README.md for details
|
||||
*/
|
||||
class PackageResolver
|
||||
{
|
||||
private static $aliases = [
|
||||
'core' => 'silverstripe/recipe-core'
|
||||
];
|
||||
|
||||
public function resolve(array $arguments = []): array
|
||||
{
|
||||
$versionParser = new VersionParser();
|
||||
|
||||
// first pass split on : and = to separate package names and versions
|
||||
$explodedArguments = [];
|
||||
foreach ($arguments as $argument) {
|
||||
if ((false !== $pos = strpos($argument, ':')) || (false !== $pos = strpos($argument, '='))) {
|
||||
$explodedArguments[] = substr($argument, 0, $pos);
|
||||
$explodedArguments[] = substr($argument, $pos + 1);
|
||||
} else {
|
||||
$explodedArguments[] = $argument;
|
||||
}
|
||||
}
|
||||
|
||||
// second pass to resolve package names
|
||||
$packages = [];
|
||||
foreach ($explodedArguments as $i => $argument) {
|
||||
if (false === strpos($argument, '/') && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $argument) && !\in_array($argument, ['mirrors', 'nothing'])) {
|
||||
|
||||
if (isset(self::$aliases[$argument])) {
|
||||
$argument = self::$aliases[$argument];
|
||||
} else {
|
||||
$versionParser->parseConstraints($argument);
|
||||
}
|
||||
}
|
||||
|
||||
$packages[] = $argument;
|
||||
}
|
||||
|
||||
// third pass to resolve versions
|
||||
$requires = [];
|
||||
foreach ($versionParser->parseNameVersionPairs($packages) as $package) {
|
||||
$requires[] = $package['name'];
|
||||
}
|
||||
|
||||
return array_unique($requires);
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,9 @@ namespace SilverStripe\RecipePlugin;
|
|||
|
||||
use Composer\Command\BaseCommand;
|
||||
use Composer\Plugin\Capability\CommandProvider;
|
||||
use SilverStripe\RecipePlugin\Command\RequireRecipe;
|
||||
use SilverStripe\RecipePlugin\Command\UnpackCommand;
|
||||
use SilverStripe\RecipePlugin\Command\UpdateRecipe;
|
||||
|
||||
class RecipeCommandProvider implements CommandProvider
|
||||
{
|
||||
|
@ -15,8 +18,9 @@ class RecipeCommandProvider implements CommandProvider
|
|||
public function getCommands()
|
||||
{
|
||||
return [
|
||||
new RequireRecipeCommand(),
|
||||
new UpdateRecipeCommand(),
|
||||
new RequireRecipe(),
|
||||
new UpdateRecipe(),
|
||||
new UnpackCommand(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\RecipePlugin\Unpack;
|
||||
|
||||
use Composer\Package\PackageInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com> - See README.md for details
|
||||
*/
|
||||
class Result
|
||||
{
|
||||
private $unpacked = [];
|
||||
private $required = [];
|
||||
|
||||
public function addUnpacked(PackageInterface $package)
|
||||
{
|
||||
$this->unpacked[] = $package;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PackageInterface[]
|
||||
*/
|
||||
public function getUnpacked(): array
|
||||
{
|
||||
return $this->unpacked;
|
||||
}
|
||||
|
||||
public function addRequired(string $package)
|
||||
{
|
||||
$this->required[] = $package;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getRequired(): array
|
||||
{
|
||||
// we need at least one package for the command to work properly
|
||||
return $this->required ?: ['silverstripe/recipe-plugin'];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace SilverStripe\RecipePlugin\Unpack;
|
||||
|
||||
use Composer\Composer;
|
||||
use Composer\Factory;
|
||||
use Composer\Json\JsonFile;
|
||||
use Composer\Json\JsonManipulator;
|
||||
use SilverStripe\RecipePlugin\PackageResolver;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com> - See README.md for details
|
||||
*/
|
||||
class Unpacker
|
||||
{
|
||||
private $composer;
|
||||
private $resolver;
|
||||
|
||||
public function __construct(Composer $composer, PackageResolver $resolver)
|
||||
{
|
||||
$this->composer = $composer;
|
||||
$this->resolver = $resolver;
|
||||
}
|
||||
|
||||
public function unpack(array $packages): Result
|
||||
{
|
||||
$result = new Result();
|
||||
$json = new JsonFile(Factory::getComposerFile());
|
||||
$manipulator = new JsonManipulator(file_get_contents($json->getPath()));
|
||||
foreach ($packages as $package) {
|
||||
|
||||
$dev = $package['dev'];
|
||||
$package = $package['pkg'];
|
||||
|
||||
// not unpackable or no --unpack flag or empty packs (markers)
|
||||
if (
|
||||
null === $package ||
|
||||
'silverstripe-recipe' !== $package->getType() ||
|
||||
0 === \count($package->getRequires()) + \count($package->getDevRequires())
|
||||
) {
|
||||
$result->addRequired($package->getName().($package->getVersion() ? ':'.$package->getVersion() : ''));
|
||||
continue;
|
||||
}
|
||||
|
||||
$result->addUnpacked($package);
|
||||
foreach ($package->getRequires() as $link) {
|
||||
if ('php' === $link->getTarget()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$constraint = $link->getPrettyConstraint();
|
||||
|
||||
if (!$manipulator->addLink($dev ? 'require-dev' : 'require', $link->getTarget(), $constraint)) {
|
||||
throw new \RuntimeException(sprintf('Unable to unpack package "%s".', $link->getTarget()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($json->getPath(), $manipulator->getContents());
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue