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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
1.6.0
2.0.0
=====

* (bc) Remove deprecated init commands
* (feature) Automatically detect the package type according to the type in `composer.json`.
* (feature) Write back the package type, if none was set.
* (improvement) Add option to not automatically run `composer update` after Janus finished.
* (feature) Add composer plugin to run composer automatically.

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 21TORR
Copyright (c) 2025 21TORR

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
132 changes: 99 additions & 33 deletions src/Command/InitializeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

namespace Janus\Command;

use Janus\Initializer\LibraryInitializer;
use Janus\Initializer\SymfonyInitializer;
use Janus\Composer\ComposerJson;
use Janus\Exception\InvalidCallException;
use Janus\Exception\JanusException;
use Janus\Package\PackageInitializer;
use Janus\Package\PackageManager;
use Janus\Package\PackageType;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -17,10 +21,6 @@ final class InitializeCommand extends Command
"symfony",
"library",
];
private const array LEGACY_COMMANDS = [
"init-symfony",
"init-library",
];

/**
*/
Expand All @@ -36,13 +36,12 @@ protected function configure () : void
{
$this
->setDescription("Initializes a given command")
->setAliases(self::LEGACY_COMMANDS)
->addArgument(
"type",
InputArgument::OPTIONAL,
"The project type to initialize",
default: null,
suggestedValues: self::ALLOWED_TYPES,
suggestedValues: PackageType::values(),
)
->addOption(
"no-auto-install",
Expand All @@ -57,48 +56,115 @@ protected function configure () : void
protected function execute (InputInterface $input, OutputInterface $output) : int
{
$io = new TorrStyle($input, $output);
$projectHelper = new PackageManager($io);
$packageInitializer = new PackageInitializer();

$io->title("Janus: Initialize");
$runComposerAutomatically = !$input->getOption('no-auto-install');

if (\in_array($input->getFirstArgument(), self::LEGACY_COMMANDS, true))
try
{
$io->caution("You are using a deprecated command. Use the `init` command instead.");
}
$composerJson = $projectHelper->loadComposerJson();
$packageType = $this->fetchPackageType($io, $composerJson, $input->getArgument("type"));

$type = $input->getArgument("type");
\assert(null === $type || \is_string($type));
$io->writeln(\sprintf(
"• Initializing janus for type <fg=magenta>%s</>",
$packageType->value,
));

if (!\in_array($type, self::ALLOWED_TYPES, true))
{
if (null !== $type)
$io->writeln("• Copying main init files");
$projectHelper->copyInitFilesIntoProject($packageType);

$io->writeln("• Updating <fg=yellow>composer.json</>");

// write basics
match ($packageType)
{
PackageType::Symfony => $packageInitializer->initializeSymfony($composerJson),
PackageType::Library => $packageInitializer->initializeLibrary($composerJson),
};

// set project type (only if it is not yet set. We want to keep even unknown values here, so only set it if it is unset)
if (!$composerJson->hasType())
{
$io->writeln("• Your composer.json has no type set");
$io->writeln(\sprintf(
"• Setting it to the type <fg=blue>%s</> (according to your selection <fg=magenta>%s</>)",
$packageType->getComposerType(),
$packageType->value,
));

$composerJson->replaceConfig([
"type" => $packageType->getComposerType(),
]);
}

$projectHelper->writeComposerJson($composerJson);

if ($runComposerAutomatically)
{
$io->writeln("• Running <fg=blue>composer update</>...");
$projectHelper->runComposerInProject(["update"]);
}
else
{
$io->error("Used invalid type: {$type}");
$io->caution("Your project was updated, you should run `composer update`.");
}

$type = $io->choice("Please select the type to initialize", self::ALLOWED_TYPES);
return self::SUCCESS;
}
catch (JanusException $exception)
{
$io->error($exception->getMessage());

\assert(\is_string($type));
return self::FAILURE;
}
}

$io->comment(\sprintf(
"Initializing janus for type <fg=blue>%s</>",
$type,
));
/**
*
*/
private function fetchPackageType (
TorrStyle $io,
ComposerJson $composerJson,
mixed $typeCliArgument,
) : PackageType
{
// first try CLI parameter
$packageType = \is_string($typeCliArgument)
? PackageType::tryFrom($typeCliArgument)
: null;

$runComposerAutomatically = !$input->getOption("no-auto-install");
if (null !== $packageType)
{
return $packageType;
}

try
// then error out if the user explicitly passed an invalid value
if (null !== $typeCliArgument)
{
return match ($type)
{
"symfony" => (new SymfonyInitializer())->initialize($io, $runComposerAutomatically),
"library" => (new LibraryInitializer())->initialize($io, $runComposerAutomatically),
};
throw new InvalidCallException(\sprintf(
"Invalid type selected: %s",
\is_scalar($typeCliArgument)
? $typeCliArgument
: get_debug_type($typeCliArgument),
));
}
catch (\Throwable $exception)

// no CLI parameter passed, so test if we can detect the type from composer.json
$packageType = $composerJson->getType();

if (null !== $packageType)
{
$io->error("Running janus failed: {$exception->getMessage()}");
$io->writeln("• Automatically detected type from the package type in your composer.json");

return 2;
return $packageType;
}

// could not detect, so just ask for it
$type = $io->choice("Please select the type to initialize", PackageType::values());
\assert(\is_string($type));

return PackageType::from($type);
}
}
102 changes: 102 additions & 0 deletions src/Composer/ComposerJson.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php declare(strict_types=1);

namespace Janus\Composer;

use Janus\Exception\InvalidSetupException;
use Janus\Package\PackageType;

/**
* @final
*/
class ComposerJson
{
/**
*/
public function __construct (
/** @var array<array-key, mixed|array> */
public private(set) array $content,
) {}

/**
* 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<string, string> $scripts the scripts to replace
*/
public function updateScripts (string $key, array $scripts) : void
{
$allExistingScripts = $this->content["scripts"] ?? [];

if (!\is_array($allExistingScripts))
{
throw new InvalidSetupException(\sprintf(
"Invalid composer.json: scripts must be an array, %s given",
get_debug_type($allExistingScripts),
));
}

$existingScripts = $allExistingScripts[$key] ?? [];
// keep existing scripts
$result = [];
\assert(\is_array($existingScripts));

foreach ($existingScripts as $line)
{
\assert(\is_string($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;
}

$allExistingScripts[$key] = $result;
$this->content["scripts"] = $allExistingScripts;
}

/**
* Add the given config to the projects composer.json
*
* @param array<array-key, mixed> $config
*/
public function replaceConfig (array $config) : void
{
$this->content = array_replace_recursive(
$this->content,
$config,
);
}

/**
*
*/
public function getType () : ?PackageType
{
return PackageType::tryFromComposerType($this->content["type"] ?? null);
}

/**
*
*/
public function hasType () : bool
{
return \array_key_exists("type", $this->content);
}
}
51 changes: 41 additions & 10 deletions src/Composer/JanusPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Janus\Command\InitializeCommand;
use Janus\Package\PackageType;
use Symfony\Component\Process\Process;

/**
Expand Down Expand Up @@ -95,17 +96,32 @@ public function afterAutoloadDump (Event $event) : void
$io = $event->getIO();
$io->write("\n<fg=magenta>Janus update detected, running janus update</>\n");

$selected = $io->select(
"What are you currently using?",
InitializeCommand::ALLOWED_TYPES,
"library",
// please note, that the detection can fail: composer defaults to "library", if it's not set
$packageType = PackageType::tryFromComposerType(
$event->getComposer()->getPackage()->getType(),
);
$type = InitializeCommand::ALLOWED_TYPES[$selected] ?? null;

if (null === $packageType)
{
$selected = $io->select(
"What are you currently using?",
InitializeCommand::ALLOWED_TYPES,
"library",
);
$packageType = PackageType::tryFromComposerType(InitializeCommand::ALLOWED_TYPES[$selected] ?? null);
}
else
{
$io->write(\sprintf(
"Detected package type <fg=yellow>%s</>",
$packageType->value,
));
}

$vendorDir = $event->getComposer()->getConfig()->get('vendor-dir');
\assert(\is_string($vendorDir));

$success = $this->runJanus($io, $vendorDir, $type);
$success = $this->runJanus($io, $vendorDir, $packageType);

if ($success)
{
Expand All @@ -123,17 +139,17 @@ public function afterAutoloadDump (Event $event) : void
private function runJanus (
IOInterface $io,
string $vendorDir,
?string $type,
?PackageType $type,
) : bool
{
$command = [
"{$vendorDir}/bin/janus",
$this->findJanusExecutable($vendorDir),
"init",
];

if (null !== $type)
{
$command[] = $type;
$command[] = $type->value;
}

$command[] = "--no-auto-install";
Expand All @@ -150,11 +166,26 @@ static function ($type, $buffer) use ($io) : void
return $output->isSuccessful();
}

/**
* Finds the path to the janus executable
*/
private function findJanusExecutable (string $vendorDir) : string
{
// first check if it's installed in the project via composer
if (is_dir("{$vendorDir}/bin/janus"))
{
return "{$vendorDir}/bin/janus";
}

// otherwise just fetch the executable from the library and run it
return \dirname(__DIR__, 2) . "/bin/janus";
}

/**
* @inheritDoc
*/
#[\Override]
public static function getSubscribedEvents ()
public static function getSubscribedEvents () : array
{
return [
PackageEvents::POST_PACKAGE_INSTALL => "checkForJanusOperations",
Expand Down
Loading