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
|
- 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.
|
recipe dependencies without mandating the inclusion of all requirements directly.
|
||||||
- An `update-recipe` command to upgrade to a newer version of a recipe.
|
- 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
|
## Example output
|
||||||
|
|
||||||
|
@ -182,3 +183,19 @@ public/
|
||||||
blog.css
|
blog.css
|
||||||
composer.json
|
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/"
|
"lint-clean": "phpcbf src/"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"composer-plugin-api": "^1.1"
|
"composer-plugin-api": "^1.1",
|
||||||
|
"ext-json": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"composer/composer": "^1.2"
|
"composer/composer": "^1.2"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace SilverStripe\RecipePlugin;
|
namespace SilverStripe\RecipePlugin\Command;
|
||||||
|
|
||||||
use Composer\Command\RequireCommand;
|
use Composer\Command\RequireCommand;
|
||||||
use Composer\Command\UpdateCommand;
|
use Composer\Command\UpdateCommand;
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace SilverStripe\RecipePlugin;
|
namespace SilverStripe\RecipePlugin\Command;
|
||||||
|
|
||||||
use Composer\Command\BaseCommand;
|
use Composer\Command\BaseCommand;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
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
|
* Provides the 'require-recipe' command which allows a new recipe to be installed, but also
|
||||||
* soft-updates any existing recipe.
|
* soft-updates any existing recipe.
|
||||||
*/
|
*/
|
||||||
class RequireRecipeCommand extends BaseCommand
|
class RequireRecipe extends BaseCommand
|
||||||
{
|
{
|
||||||
use RecipeCommandBehaviour;
|
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
|
<?php
|
||||||
|
|
||||||
namespace SilverStripe\RecipePlugin;
|
namespace SilverStripe\RecipePlugin\Command;
|
||||||
|
|
||||||
use BadMethodCallException;
|
use BadMethodCallException;
|
||||||
use Composer\Command\BaseCommand;
|
use Composer\Command\BaseCommand;
|
||||||
|
@ -8,7 +8,7 @@ use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
class UpdateRecipeCommand extends BaseCommand
|
class UpdateRecipe extends BaseCommand
|
||||||
{
|
{
|
||||||
use RecipeCommandBehaviour;
|
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\Command\BaseCommand;
|
||||||
use Composer\Plugin\Capability\CommandProvider;
|
use Composer\Plugin\Capability\CommandProvider;
|
||||||
|
use SilverStripe\RecipePlugin\Command\RequireRecipe;
|
||||||
|
use SilverStripe\RecipePlugin\Command\UnpackCommand;
|
||||||
|
use SilverStripe\RecipePlugin\Command\UpdateRecipe;
|
||||||
|
|
||||||
class RecipeCommandProvider implements CommandProvider
|
class RecipeCommandProvider implements CommandProvider
|
||||||
{
|
{
|
||||||
|
@ -15,8 +18,9 @@ class RecipeCommandProvider implements CommandProvider
|
||||||
public function getCommands()
|
public function getCommands()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new RequireRecipeCommand(),
|
new RequireRecipe(),
|
||||||
new UpdateRecipeCommand(),
|
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