winter/modules/system/console/MixInstall.php
2022-09-04 13:24:33 -06:00

281 lines
9.7 KiB
PHP

<?php namespace System\Console;
use File;
use Config;
use Cms\Classes\Theme;
use Winter\Storm\Support\Str;
use Winter\Storm\Console\Command;
use Symfony\Component\Process\Process;
use System\Classes\MixAssets;
use System\Classes\PluginManager;
use Symfony\Component\Process\Exception\ProcessSignaledException;
class MixInstall extends Command
{
/**
* @var string|null The default command name for lazy loading.
*/
protected static $defaultName = 'mix:install';
/**
* @var string The name and signature of this command.
*/
protected $signature = 'mix:install
{npmArgs?* : Arguments to pass through to the "npm" binary}
{--npm= : Defines a custom path to the "npm" binary}
{--p|package=* : Defines one or more packages to install}';
/**
* @var string The console command description.
*/
protected $description = 'Install Node.js dependencies required for mixed assets';
/**
* @var string The path to the "npm" executable.
*/
protected $npmPath = 'npm';
/**
* @var string Default version of Laravel Mix to install
*/
protected $defaultMixVersion = '^6.0.41';
/**
* @return array Terms used in messages.
*/
protected $terms = [
'complete' => 'install',
'completed' => 'installed',
];
/**
* @var string The NPM command to run.
*/
protected $npmCommand = 'install';
/**
* Execute the console command.
* @return int
*/
public function handle(): int
{
if ($this->option('npm')) {
$this->npmPath = $this->option('npm', 'npm');
}
if (!version_compare($this->getNpmVersion(), '7', '>')) {
$this->error('"npm" version 7 or above must be installed to run this command.');
return 1;
}
$mixedAssets = MixAssets::instance();
$mixedAssets->fireCallbacks();
$registeredPackages = $mixedAssets->getPackages();
$requestedPackages = $this->option('package') ?: [];
// Normalize the requestedPackages option
if (count($requestedPackages)) {
foreach ($requestedPackages as &$name) {
$name = strtolower($name);
}
unset($name);
}
// Filter the registered packages to only include requested packages
if (count($requestedPackages) && count($registeredPackages)) {
$availablePackages = array_keys($registeredPackages);
$cmsEnabled = in_array('Cms', Config::get('cms.loadModules'));
// Autogenerate config files for packages that don't exist but can be autodiscovered
foreach ($requestedPackages as $package) {
// Check if the package is already registered
if (in_array($package, $availablePackages)) {
continue;
}
// Check if package could be a module (but explicitly ignore core Winter modules)
if (Str::startsWith($package, 'module-') && !in_array($package, ['system', 'backend', 'cms'])) {
$mixedAssets->registerPackage($package, base_path('modules/' . Str::after($package, 'module-') . '/winter.mix.js'));
continue;
}
// Check if package could be a theme
if (
$cmsEnabled
&& Str::startsWith($package, 'theme-')
&& Theme::exists(Str::after($package, 'theme-'))
) {
$theme = Theme::load(Str::after($package, 'theme-'));
$mixedAssets->registerPackage($package, $theme->getPath() . '/winter.mix.js');
continue;
}
// Check if a package could be a plugin
if (PluginManager::instance()->exists($package)) {
$mixedAssets->registerPackage($package, PluginManager::instance()->getPluginPath($package) . '/winter.mix.js');
continue;
}
}
// Get an updated list of packages including any newly added packages
$registeredPackages = $mixedAssets->getPackages();
// Filter the registered packages to only deal with the requested packages
foreach (array_keys($registeredPackages) as $name) {
if (!in_array($name, $requestedPackages)) {
unset($registeredPackages[$name]);
}
}
}
if (!count($registeredPackages)) {
if (count($requestedPackages)) {
$this->error('No registered packages matched the requested packages for installation.');
return 1;
} else {
$this->info('No packages registered for mixing.');
return 0;
}
}
// Load the main package.json for the project
$packageJsonPath = base_path('package.json');
$packageJson = [];
if (File::exists($packageJsonPath)) {
$packageJson = json_decode(File::get($packageJsonPath), true);
}
$workspacesPackages = $packageJson['workspaces']['packages'] ?? [];
$ignoredPackages = $packageJson['workspaces']['ignoredPackages'] ?? [];
// Check to see if Laravel Mix is already present as a dependency
if (
(
!isset($packageJson['dependencies']['laravel-mix'])
&& !isset($packageJson['devDependencies']['laravel-mix'])
)
&& $this->confirm('laravel-mix was not found as a dependency in package.json, would you like to add it?', true)
) {
$packageJson['devDependencies'] = array_merge($packageJson['devDependencies'] ?? [], ['laravel-mix' => $this->defaultMixVersion]);
$this->writePackageJson($packageJsonPath, $packageJson);
}
// Process each package
foreach ($registeredPackages as $name => $package) {
// Normalize package path across OS types
$packagePath = Str::replace(DIRECTORY_SEPARATOR, '/', $package['path']);
// Add the package path to the instance's package.json->workspaces->packages property if not present
if (
!in_array($packagePath, $workspacesPackages)
&& !in_array($packagePath, $ignoredPackages)
) {
if (
$this->confirm(
sprintf(
"Detected %s (%s), should it be added to your package.json?",
$name,
$packagePath
),
true
)
) {
$workspacesPackages[] = $packagePath;
$this->info(sprintf(
'Adding %s (%s) to the workspaces.packages property in package.json',
$name,
$packagePath
));
} else {
$ignoredPackages[] = $packagePath;
$this->warn(
sprintf('Ignoring %s (%s)', $name, $packagePath)
);
}
asort($workspacesPackages);
asort($ignoredPackages);
$packageJson['workspaces']['packages'] = array_values($workspacesPackages);
$packageJson['workspaces']['ignoredPackages'] = array_values($ignoredPackages);
$this->writePackageJson($packageJsonPath, $packageJson);
}
// Detect missing winter.mix.js files and install them
if (!File::exists($package['mix'])) {
$this->info(sprintf(
'No Mix file found for %s, creating one at %s...',
$name,
$package['mix']
));
File::put($package['mix'], File::get(__DIR__ . '/fixtures/winter.mix.js.fixture'));
}
}
// Ensure separation between package.json modification messages and rest of output
$this->info('');
if ($this->installPackageDeps() !== 0) {
$this->error("Unable to {$this->terms['complete']} dependencies.");
} else {
$this->info("Dependencies successfully {$this->terms['completed']}!");
}
return 0;
}
/**
* Write to the package.json file
*/
protected function writePackageJson(string $path, array $data): void
{
File::put($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
/**
* Installs the dependencies for the given package.
*
* @return int
*/
protected function installPackageDeps()
{
$command = $this->argument('npmArgs') ?? [];
array_unshift($command, 'npm', $this->npmCommand);
$process = new Process($command, base_path(), null, null, null);
// Attempt to set tty mode, catch and warn with the exception message if unsupported
try {
$process->setTty(true);
} catch (\Throwable $e) {
$this->warn($e->getMessage());
}
try {
return $process->run(function ($status, $stdout) {
$this->getOutput()->write($stdout);
});
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}
return 1;
}
$this->info('');
return $process->getExitCode();
}
/**
* Gets the install NPM version.
*
* @return string
*/
protected function getNpmVersion()
{
$process = new Process(['npm', '--version']);
$process->run();
return $process->getOutput();
}
}