diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a7802..f8ca1fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +1.3.0 +===== + +* (internal) Clean up internal call definitions. +* (feature) Improve merging of `composer.json` scripts. + + 1.2.0 ===== diff --git a/bin/janus b/bin/janus index 103fe19..04bfff2 100755 --- a/bin/janus +++ b/bin/janus @@ -1,281 +1,20 @@ #!/usr/bin/env php null, -]; +$allowedCommands = ["init-symfony", "init-library"]; - -(new SingleCommandApplication()) - ->setName("Janus") - ->addArgument( - "command", - InputArgument::REQUIRED, - suggestedValues: array_keys($allowedCommands), - ) - ->setCode( - function (InputInterface $input, OutputInterface $output) : int - { - $io = new SymfonyStyle($input, $output); - $io->title("Janus"); - $command = $input->getArgument("command"); - - try - { - return match ($command) - { - "init-symfony" => initializeSymfony($io), - "init-library" => initializeLibrary($io), - null => printUsage(), - default => printError($io, "Unknown command: {$command}"), - }; - } - catch (Throwable $exception) - { - $io->error("Running janus failed: {$exception->getMessage()}"); - return 2; - } - } - ) - ->run(); - -return; - - -// region Commands -/** - */ -function initializeSymfony (SymfonyStyle $io) : int -{ - $io->writeln("• Copying config files to the project..."); - copyFilesIntoProject($io, "symfony"); - - $io->writeln("• Updating composer.json..."); - addToProjectComposerJson([ - "config" => [ - "allow-plugins" => [ - "bamarni/composer-bin-plugin" => true, - ], - "sort-packages" => true, - ], - "extra" => [ - "bamarni-bin" => [ - "bin-links" => false, - "forward-command" => true, - ], - ], - "require-dev" => [ - "bamarni/composer-bin-plugin" => "^1.8", - "roave/security-advisories" => "dev-latest", - ], - "scripts" => [ - "fix-lint" => [ - "vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", - "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --ansi", - ], - "lint" => [ - "bin/console lint:yaml config --parse-tags", - "bin/console lint:twig templates", - "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --dry-run --ansi", - "vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --dry-run --no-interaction --ansi", - ], - "test" => [ - "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi", - ], - ], - ]); - - $io->writeln("• Running composer update..."); - runComposerInProject($io, ["update"]); - - return 0; -} - -/** - */ -function initializeLibrary (SymfonyStyle $io) : int -{ - $io->writeln("• Copying config files to the project..."); - copyFilesIntoProject($io, "library"); - - $io->writeln("• Updating composer.json..."); - addToProjectComposerJson([ - "config" => [ - "allow-plugins" => [ - "bamarni/composer-bin-plugin" => true, - ], - "sort-packages" => true, - ], - "extra" => [ - "bamarni-bin" => [ - "bin-links" => false, - "forward-command" => true, - ], - ], - "require-dev" => [ - "bamarni/composer-bin-plugin" => "^1.8", - "roave/security-advisories" => "dev-latest", - ], - "scripts" => [ - "fix-lint" => [ - "vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", - "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --ansi", - ], - "lint" => [ - "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --dry-run --ansi", - "vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --dry-run --no-interaction --ansi", - ], - "test" => [ - "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi", - ], - ], - ]); - - $io->writeln("• Running composer update..."); - runComposerInProject($io, ["update"]); - - return 0; -} -// endregion - - -// region CommandHelpers -function copyFilesIntoProject (SymfonyStyle $io, string $directory) : void -{ - $sourceDir = __DIR__ . "/../_init/{$directory}/."; - - runProcessInProject($io, [ - "cp", - "-a", - $sourceDir, - ".", - ]); -} - -/** - * - */ -function runProcessInProject (SymfonyStyle $io, array $cmd) : void -{ - $io->writeln(sprintf( - "$> Running command %s", - implode(" ", $cmd), - )); - - $process = new Process( - $cmd, - cwd: getcwd(), - ); - $process->mustRun(); - - $output = trim(sprintf("%s\n%s", $process->getErrorOutput(), $process->getOutput())); - - if ("" !== $output) - { - $io->block( - trim(sprintf("%s\n%s", $process->getErrorOutput(), $process->getOutput())), - prefix: " │ ", - ); - } -} - - -function runComposerInProject (SymfonyStyle $io, array $cmd) : void -{ - $finder = new ExecutableFinder(); - $composer = $finder->find("composer"); - - if (null === $composer) - { - throw new Exception("Could not find locally installed composer"); - } - - array_unshift($cmd, $composer); - $cmd[] = "--ansi"; - runProcessInProject($io, $cmd); -} - -/** - */ -function addToProjectComposerJson (array $config) : void -{ - $filePath = getcwd() . "/composer.json"; - - $jsonContent = json_decode( - file_get_contents($filePath), - true, - flags: JSON_THROW_ON_ERROR - ); - - $jsonContent = array_replace_recursive($jsonContent, $config); - file_put_contents( - $filePath, - json_encode( - $jsonContent, - JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES, - JSON_THROW_ON_ERROR - ), - ); -} -// endregion - - -// region Output Helpers -/** - * Prints an error message + usage and returns an error status code - */ -function printError (SymfonyStyle $io, string $message) : int -{ - global $allowedCommands; - - $io->error($message); - $io->writeln("Allowed commands:"); - $io->listing( - array_map( - static fn (string $command) => "{$command}", - array_keys($allowedCommands), - ), - ); - - return 1; -} - - -/** - * Prints the usage and returns a success status code - */ -function printUsage () : int -{ - echo " ~~~~~~~\n"; - echo " Janus \n"; - echo " ~~~~~~~\n"; - echo "\n"; - echo "Usage:\n"; - echo " janus [command] [argument]\n"; - echo "\n"; - echo "\n"; - echo "Commands:\n"; - echo "\n"; - echo " init-symfony .. to initialize a symfony application\n"; - echo " init-library .. to initialize a library\n"; - echo "\n"; - echo "\n"; - - return 0; -} -// endregion +$application = new Application("Janus"); +$application->add(new InitializeCommand()); +$application->add(new LegacyCommand()); +$application->run(); diff --git a/composer.json b/composer.json index 64f62ce..be9086d 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ ], "require": { "php": ">= 8.3", + "21torr/cli": "^1.2", "symfony/console": "^7.0", "symfony/process": "^7.0" }, @@ -21,6 +22,11 @@ "bin": [ "bin/janus" ], + "autoload": { + "psr-4": { + "Janus\\": "src/" + } + }, "config": { "sort-packages": true, "allow-plugins": { diff --git a/src/Command/InitializeCommand.php b/src/Command/InitializeCommand.php new file mode 100644 index 0000000..49f84c1 --- /dev/null +++ b/src/Command/InitializeCommand.php @@ -0,0 +1,83 @@ +setDescription("Initializes a given command") + ->addArgument( + "type", + InputArgument::REQUIRED, + "The project type to initialize", + suggestedValues: [ + "symfony", + "library", + ], + ); + } + + /** + * @inheritDoc + */ + protected function execute (InputInterface $input, OutputInterface $output) : int + { + $io = new TorrStyle($input, $output); + $io->title("Janus: Initialize"); + + + try + { + $type = $input->getArgument("type"); + + $initializer = match ($type) + { + "symfony" => new SymfonyInitializer(), + "library" => new LibraryInitializer(), + default => null, + }; + + if (null === $initializer) + { + return $this->printError($io, $type); + } + + return $initializer->initialize($io); + } + catch (\Throwable $exception) + { + $io->error("Running janus failed: {$exception->getMessage()}"); + return 2; + } + } + + /** + * Prints an error + */ + private function printError (TorrStyle $io, string $type) : int + { + $io->error("Unknown type: {$type}"); + + return self::FAILURE; + } +} diff --git a/src/Command/LegacyCommand.php b/src/Command/LegacyCommand.php new file mode 100644 index 0000000..6754579 --- /dev/null +++ b/src/Command/LegacyCommand.php @@ -0,0 +1,63 @@ +setDescription("Deprecated command to ") + ->setAliases(["init-library"]); + } + + /** + * @inheritDoc + */ + #[\Override] + protected function execute (InputInterface $input, OutputInterface $output) : int + { + $io = new TorrStyle($input, $output); + + $calledCommand = $input->getFirstArgument(); + + $io->caution("This command is deprecated"); + $io->comment(\sprintf( + "The command %s is deprecated, use %s instead.", + $calledCommand, + \strtr($calledCommand, ["-" => " "]), + )); + + $fakedInput = new ArrayInput([ + "command" => "init", + "type" => match ($calledCommand) + { + "init-symfony" => "symfony", + "init-library" => "library", + default => \assert(false), + }, + ]); + $application = $this->getApplication(); + \assert(null !== $application); + + return $application->run($fakedInput, $output); + } +} diff --git a/src/Exception/InvalidSetupException.php b/src/Exception/InvalidSetupException.php new file mode 100644 index 0000000..efe1aa4 --- /dev/null +++ b/src/Exception/InvalidSetupException.php @@ -0,0 +1,7 @@ +cwd = \getcwd(); + } + + /** + * Copies the files from the given init dir to the project dir + */ + public function copyFilesIntoProject (string $directory) : void + { + $sourceDir = self::INIT_DIR . "/{$directory}/."; + + $this->runProcessInProject([ + "cp", + "-a", + $sourceDir, + ".", + ]); + } + + + /** + * Add the given config to the projects composer.json + */ + public function addToProjectComposerJson (array $config) : void + { + $jsonContent = array_replace_recursive( + $this->readProjectComposerJson(), + $config, + ); + + $this->writeProjectComposerJson($jsonContent); + } + + /** + * Takes a list of scripts to replace and updates the configs. + * + * The $scripts array has a keywords as key, and replaces the line containing that keyword. + * So for example the key "phpunit" would replace the line that contains "phpunit". + * If there are multiple lines matching, all will be replaced. + * If there are no lines matching, the call will just be appended. + * + * + * @param string $key The scripts key to update. + * @param array $scripts The scripts to replace. + */ + function updateProjectComposerJsonScripts (string $key, array $scripts) : void + { + $jsonContent = $this->readProjectComposerJson(); + $existingScripts = $jsonContent["scripts"][$key] ?? []; + // keep existing scripts + $result = []; + + foreach ($existingScripts as $line) + { + foreach ($scripts as $replacedKeyword => $newLine) + { + if (str_contains($line, $replacedKeyword)) + { + continue 2; + } + } + + // append the line if no replacement matches + $result[] = $line; + } + + // append all new lines + foreach ($scripts as $newLine) + { + $result[] = $newLine; + } + + $jsonContent["scripts"][$key] = $result; + $this->writeProjectComposerJson($jsonContent); + } + + /** + */ + private function writeProjectComposerJson (array $config) : void + { + $filePath = "{$this->cwd}/composer.json"; + + file_put_contents( + $filePath, + \json_encode( + $config, + \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR, + ), + ); + } + + /** + */ + private function readProjectComposerJson () : array + { + $filePath = "{$this->cwd}/composer.json"; + + return \json_decode( + \file_get_contents($filePath), + true, + flags: \JSON_THROW_ON_ERROR + ); + } + + + /** + * Runs a composer command in the project + */ + public function runComposerInProject (array $cmd) : void + { + $finder = new ExecutableFinder(); + $composer = $finder->find("composer"); + + if (null === $composer) + { + throw new InvalidSetupException("Could not find locally installed composer"); + } + + array_unshift($cmd, $composer); + $cmd[] = "--ansi"; + $this->runProcessInProject($cmd); + } + + + /** + * Runs the given command in the project directory + */ + public function runProcessInProject (array $cmd) : void + { + $this->io->writeln(sprintf( + "$> Running command %s", + implode(" ", $cmd), + )); + + $process = new Process( + $cmd, + cwd: $this->cwd, + ); + $process->mustRun(); + + $output = trim(sprintf("%s\n%s", $process->getErrorOutput(), $process->getOutput())); + + if ("" !== $output) + { + $this->io->block( + trim(sprintf("%s\n%s", $process->getErrorOutput(), $process->getOutput())), + prefix: " │ ", + ); + } + } + +} diff --git a/src/Initializer/LibraryInitializer.php b/src/Initializer/LibraryInitializer.php new file mode 100644 index 0000000..781824f --- /dev/null +++ b/src/Initializer/LibraryInitializer.php @@ -0,0 +1,55 @@ +writeln("• Copying config files to the project..."); + $helper->copyFilesIntoProject("library"); + + $io->writeln("• Updating composer.json..."); + $helper->addToProjectComposerJson([ + "config" => [ + "allow-plugins" => [ + "bamarni/composer-bin-plugin" => true, + ], + "sort-packages" => true, + ], + "extra" => [ + "bamarni-bin" => [ + "bin-links" => false, + "forward-command" => true, + ], + ], + "require-dev" => [ + "bamarni/composer-bin-plugin" => "^1.8", + "roave/security-advisories" => "dev-latest", + ], + ]); + $helper->updateProjectComposerJsonScripts("fix-lint", [ + "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --ansi", + "cs-fixer" => "vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", + ]); + $helper->updateProjectComposerJsonScripts("lint", [ + "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --dry-run --ansi", + "cs-fixer" => "vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --dry-run --no-interaction --ansi", + ]); + $helper->updateProjectComposerJsonScripts("test", [ + "phpstan" => "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi", + ]); + + $io->writeln("• Running composer update..."); + $helper->runComposerInProject(["update"]); + + return 0; + } +} diff --git a/src/Initializer/SymfonyInitializer.php b/src/Initializer/SymfonyInitializer.php new file mode 100644 index 0000000..1dbc1ef --- /dev/null +++ b/src/Initializer/SymfonyInitializer.php @@ -0,0 +1,57 @@ +writeln("• Copying config files to the project..."); + $helper->copyFilesIntoProject("symfony"); + + $io->writeln("• Updating composer.json..."); + $helper->addToProjectComposerJson([ + "config" => [ + "allow-plugins" => [ + "bamarni/composer-bin-plugin" => true, + ], + "sort-packages" => true, + ], + "extra" => [ + "bamarni-bin" => [ + "bin-links" => false, + "forward-command" => true, + ], + ], + "require-dev" => [ + "bamarni/composer-bin-plugin" => "^1.8", + "roave/security-advisories" => "dev-latest", + ], + ]); + $helper->updateProjectComposerJsonScripts("fix-lint", [ + "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --ansi", + "cs-fixer" => "vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", + ]); + $helper->updateProjectComposerJsonScripts("lint", [ + "lint:yaml" => "bin/console lint:yaml config --parse-tags", + "lint:twig" => "bin/console lint:twig templates", + "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --dry-run --ansi", + "cs-fixer" => "vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --dry-run --no-interaction --ansi", + ]); + $helper->updateProjectComposerJsonScripts("test", [ + "phpstan" => "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi", + ]); + + $io->writeln("• Running composer update..."); + $helper->runComposerInProject(["update"]); + + return 0; + } +}