feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
This commit is contained in:
115
modules/system/console/BaseScaffoldCommand.php
Normal file
115
modules/system/console/BaseScaffoldCommand.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Winter\Storm\Parse\PHP\ArrayFile;
|
||||
use Winter\Storm\Scaffold\GeneratorCommand;
|
||||
|
||||
abstract class BaseScaffoldCommand extends GeneratorCommand
|
||||
{
|
||||
use Traits\HasPluginArgument;
|
||||
|
||||
/**
|
||||
* Prepare variables for stubs.
|
||||
*/
|
||||
protected function prepareVars(): array
|
||||
{
|
||||
/*
|
||||
* Extract the author and name from the plugin code
|
||||
*/
|
||||
$pluginCode = $this->getPluginIdentifier();
|
||||
$parts = explode('.', $pluginCode);
|
||||
|
||||
if (count($parts) !== 2) {
|
||||
throw new InvalidArgumentException("Invalid plugin name, either too many dots or not enough. Example: Author.PluginName");
|
||||
}
|
||||
|
||||
$pluginName = array_pop($parts);
|
||||
$authorName = array_pop($parts);
|
||||
|
||||
return [
|
||||
'name' => $this->getNameInput(),
|
||||
'plugin' => $pluginName,
|
||||
'author' => $authorName,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all variables to available modifier and case formats and adds plugin helpers
|
||||
*/
|
||||
protected function processVars(array $vars): array
|
||||
{
|
||||
$vars = parent::processVars($vars);
|
||||
|
||||
$vars['plugin_id'] = "{$vars['lower_author']}.{$vars['lower_plugin']}";
|
||||
$vars['plugin_code'] = "{$vars['studly_author']}.{$vars['studly_plugin']}";
|
||||
$vars['plugin_url'] = "{$vars['lower_author']}/{$vars['lower_plugin']}";
|
||||
$vars['plugin_folder'] = "{$vars['lower_author']}/{$vars['lower_plugin']}";
|
||||
$vars['plugin_namespace'] = "{$vars['studly_author']}\\{$vars['studly_plugin']}";
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base path to output generated stubs to
|
||||
*/
|
||||
protected function getDestinationPath(): string
|
||||
{
|
||||
$plugin = $this->getPlugin();
|
||||
if ($plugin) {
|
||||
return $plugin->getPluginPath();
|
||||
}
|
||||
|
||||
$parts = explode('.', $this->getPluginIdentifier());
|
||||
$name = array_pop($parts);
|
||||
$author = array_pop($parts);
|
||||
|
||||
return plugins_path(strtolower($author) . '/' . strtolower($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make all stubs.
|
||||
*/
|
||||
public function makeStubs(): void
|
||||
{
|
||||
parent::makeStubs();
|
||||
|
||||
// Get the language keys to be set
|
||||
$langKeys = $this->getLangKeys();
|
||||
if (empty($langKeys)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate the path to the localization file to modify
|
||||
$langFilePath = plugins_path(
|
||||
$this->vars['plugin_folder']
|
||||
. DIRECTORY_SEPARATOR
|
||||
. 'lang'
|
||||
. DIRECTORY_SEPARATOR
|
||||
. $this->laravel->getLocale()
|
||||
. DIRECTORY_SEPARATOR
|
||||
. 'lang.php'
|
||||
);
|
||||
|
||||
if (!file_exists($langFilePath)) {
|
||||
$this->makeDirectory($langFilePath);
|
||||
$comment = '<fg=green>File generated:</> ' . str_replace(base_path(), '', $langFilePath);
|
||||
} else {
|
||||
$comment = '<fg=yellow>File updated:</> ' . str_replace(base_path(), '', $langFilePath);
|
||||
}
|
||||
|
||||
// Store the localization messages to the determined file path
|
||||
ArrayFile::open($langFilePath)->set($langKeys)->write();
|
||||
|
||||
// Inform the user
|
||||
$this->output->writeLn($comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the localization keys and values to be stored in the plugin's localization files
|
||||
* Can reference $this->vars and $this->laravel->getLocale() internally
|
||||
*/
|
||||
protected function getLangKeys(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
77
modules/system/console/CreateCommand.php
Normal file
77
modules/system/console/CreateCommand.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use System\Console\BaseScaffoldCommand;
|
||||
|
||||
class CreateCommand extends BaseScaffoldCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'create:command';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'create:command
|
||||
{plugin : The name of the plugin. <info>(eg: Winter.Blog)</info>}
|
||||
{name : The name of the command to generate. <info>(eg: ImportPosts)</info>}
|
||||
{--command= : The terminal command that should be assigned. <info>(eg: blog:importposts)</info>}
|
||||
{--description= : The command description displayed in help.}
|
||||
{--f|force : Overwrite existing files with generated files.}
|
||||
{--uninspiring : Disable inspirational quotes}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Creates a new console command.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'make:command',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string The type of class being generated.
|
||||
*/
|
||||
protected $type = 'Command';
|
||||
|
||||
/**
|
||||
* @var array A mapping of stubs to generated files.
|
||||
*/
|
||||
protected $stubs = [
|
||||
'scaffold/command/command.stub' => 'console/{{studly_name}}.php',
|
||||
];
|
||||
|
||||
/**
|
||||
* Prepare variables for stubs.
|
||||
*/
|
||||
protected function prepareVars(): array
|
||||
{
|
||||
$parts = explode('.', $this->getPluginIdentifier());
|
||||
$plugin = array_pop($parts);
|
||||
$author = array_pop($parts);
|
||||
$name = $this->getNameInput();
|
||||
$command = trim($this->option('command') ?? strtolower("{$plugin}:{$name}"));
|
||||
$description = trim($this->option('description') ?? 'No description provided yet...');
|
||||
|
||||
// More strict than the base Symfony validateName()
|
||||
// method, make a PR if it's a problem for you
|
||||
// - Plugin and command names can contain a number, but they can't start with it.
|
||||
// - Command name can contain a dash (-), but it can't start with it.
|
||||
if (preg_match('/^[a-z][\w]++(:[a-z][\w\-]++)*$/', $command) !== 1) {
|
||||
throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $command));
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'command' => $command,
|
||||
'author' => $author,
|
||||
'plugin' => $plugin,
|
||||
'description' => $description,
|
||||
];
|
||||
}
|
||||
}
|
||||
61
modules/system/console/CreateFactory.php
Normal file
61
modules/system/console/CreateFactory.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use System\Console\BaseScaffoldCommand;
|
||||
|
||||
class CreateFactory extends BaseScaffoldCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'create:factory';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'create:factory
|
||||
{plugin : The name of the plugin. <info>(eg: Winter.Blog)</info>}
|
||||
{factory : The name of the factory to generate. <info>(eg: PostFactory)</info>}
|
||||
{--m|model= : The name of the model. <info>(eg: Post)</info>}
|
||||
{--f|force : Overwrite existing files with generated files.}
|
||||
|
||||
{--uninspiring : Disable inspirational quotes}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Creates a new factory.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'make:factory',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string The type of class being generated.
|
||||
*/
|
||||
protected $type = 'Factory';
|
||||
|
||||
/**
|
||||
* @var string The argument that the generated class name comes from
|
||||
*/
|
||||
protected $nameFrom = 'factory';
|
||||
|
||||
/**
|
||||
* @var array A mapping of stubs to generated files.
|
||||
*/
|
||||
protected $stubs = [
|
||||
'scaffold/factory/factory.stub' => 'database/factories/{{studly_name}}.php',
|
||||
];
|
||||
|
||||
protected function processVars($vars): array
|
||||
{
|
||||
$vars = parent::processVars($vars);
|
||||
|
||||
$vars['model'] = $this->option('model');
|
||||
|
||||
return $vars;
|
||||
}
|
||||
}
|
||||
71
modules/system/console/CreateJob.php
Normal file
71
modules/system/console/CreateJob.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use System\Console\BaseScaffoldCommand;
|
||||
|
||||
class CreateJob extends BaseScaffoldCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'create:job';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'create:job
|
||||
{plugin : The name of the plugin. <info>(eg: Winter.Blog)</info>}
|
||||
{name : The name of the job class to generate. <info>(eg: ImportPosts)</info>}
|
||||
{--b|batchable : Generates a batchable queue job.}
|
||||
{--s|sync : Generates a non-queueable job.}
|
||||
{--f|force : Overwrite existing files with generated files.}
|
||||
{--uninspiring : Disable inspirational quotes}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Creates a new job class.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'make:job',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string The type of class being generated.
|
||||
*/
|
||||
protected $type = 'Job';
|
||||
|
||||
/**
|
||||
* @var array A mapping of stubs to generated files.
|
||||
*/
|
||||
protected $jobStubs = [
|
||||
'sync' => [
|
||||
'scaffold/job/job.stub' => 'jobs/{{studly_name}}.php',
|
||||
],
|
||||
'batched' => [
|
||||
'scaffold/job/job.batched.stub' => 'jobs/{{studly_name}}.php',
|
||||
],
|
||||
'queued' => [
|
||||
'scaffold/job/job.queued.stub' => 'jobs/{{studly_name}}.php',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function prepareVars(): array
|
||||
{
|
||||
if ($this->option('sync')) {
|
||||
$this->stubs = $this->jobStubs['sync'];
|
||||
} elseif ($this->option('batchable')) {
|
||||
$this->stubs = $this->jobStubs['batched'];
|
||||
} else {
|
||||
$this->stubs = $this->jobStubs['queued'];
|
||||
}
|
||||
|
||||
return parent::prepareVars();
|
||||
}
|
||||
}
|
||||
358
modules/system/console/CreateMigration.php
Normal file
358
modules/system/console/CreateMigration.php
Normal file
@@ -0,0 +1,358 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use File;
|
||||
use InvalidArgumentException;
|
||||
use System\Classes\VersionManager;
|
||||
use System\Console\BaseScaffoldCommand;
|
||||
use Winter\Storm\Database\Model;
|
||||
use Winter\Storm\Support\Str;
|
||||
use Yaml;
|
||||
|
||||
/**
|
||||
* Scaffolds a new migration file
|
||||
*
|
||||
* @TODO:
|
||||
* - Add flag to either create a new version automatically in version.yaml or
|
||||
* add the migration to a specific version, would also put the migration in
|
||||
* a version specific folder
|
||||
*/
|
||||
class CreateMigration extends BaseScaffoldCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'create:migration';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'create:migration
|
||||
{plugin : The name of the plugin. <info>(eg: Winter.Blog)</info>}
|
||||
{--name= : The name of the migration}
|
||||
{--model= : The model to create a migration for. <info>(eg: Post)</info>}
|
||||
{--table= : The table to migrate, defaults to autogenerated from the provided model. <info>(eg: winter_blog_posts)</info>}
|
||||
{--for-version= : Generate a migration for a specific version}
|
||||
{--c|create : Generate a migration that creates the specified table}
|
||||
{--u|update : Generate a migration that updates the specified table}
|
||||
{--f|force : Overwrite existing files with generated files.}
|
||||
{--uninspiring : Disable inspirational quotes}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Creates a new migration.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'make:migration',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string The type of class being generated.
|
||||
*/
|
||||
protected $type = 'Migration';
|
||||
|
||||
/**
|
||||
* @var bool Validate the provided plugin input against the PluginManager.
|
||||
*/
|
||||
protected $validatePluginInput = true;
|
||||
|
||||
/**
|
||||
* @var array Available migration scaffolds and their types
|
||||
*/
|
||||
protected $migrationScaffolds = [
|
||||
'create' => [
|
||||
'scaffold/migration/migration.create.stub' => 'updates/{{version}}/{{snake_name}}.php',
|
||||
],
|
||||
'update' => [
|
||||
'scaffold/migration/migration.update.stub' => 'updates/{{version}}/{{snake_name}}.php',
|
||||
],
|
||||
'migrate' => [
|
||||
'scaffold/migration/migration.stub' => 'updates/{{version}}/{{snake_name}}.php',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Make all stubs.
|
||||
*/
|
||||
public function makeStubs(): void
|
||||
{
|
||||
parent::makeStubs();
|
||||
|
||||
$plugin = $this->getPlugin();
|
||||
|
||||
// Identify the changes to insert into the version.yaml file
|
||||
$changes = [$this->vars['name']];
|
||||
$stubs = array_keys($this->stubs);
|
||||
foreach ($stubs as $stub) {
|
||||
$changes[] = Str::after($this->getDestinationForStub($stub), $plugin->getPluginPath() . '/updates/');
|
||||
}
|
||||
|
||||
// Identify the version to insert these changes into
|
||||
$versions = $plugin->getPluginVersions();
|
||||
$firstVersion = array_keys($versions)[0];
|
||||
$usesV = Str::startsWith($firstVersion, 'v');
|
||||
$version = $usesV ? $this->vars['version'] : Str::after($this->vars['version'], 'v');
|
||||
|
||||
// Insert these changes into the identified version
|
||||
$changes = array_merge($versions[$version] ?? [], $changes);
|
||||
$versions[$version] = $changes;
|
||||
|
||||
// Render and save the updated version.yaml file
|
||||
$destinationFile = $plugin->getPluginPath() . '/updates/version.yaml';
|
||||
$this->files->put($destinationFile, Yaml::render($versions));
|
||||
$this->comment('File updated: ' . str_replace(base_path(), '', $destinationFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the desired class name from the input.
|
||||
*/
|
||||
protected function getNameInput(): string
|
||||
{
|
||||
$name = trim($this->option($this->nameFrom));
|
||||
if (empty($name)) {
|
||||
if ($this->option('create')) {
|
||||
$template = 'Create {ResourceName} Table';
|
||||
} elseif ($this->option('update')) {
|
||||
$template = 'Update {ResourceName} Table';
|
||||
} else {
|
||||
$template = now()->format('Y_m_d_His');
|
||||
}
|
||||
|
||||
$resourceName = (
|
||||
$this->option('model')
|
||||
? Str::plural($this->option('model'))
|
||||
: null
|
||||
)
|
||||
?? (
|
||||
$this->option('table')
|
||||
? Str::replace('_', ' ', $this->option('table'))
|
||||
: ''
|
||||
);
|
||||
$name = Str::replace('{ResourceName}', $resourceName, $template);
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare variables for stubs.
|
||||
*/
|
||||
protected function prepareVars(): array
|
||||
{
|
||||
$parts = explode('.', $this->getPluginIdentifier());
|
||||
$plugin = array_pop($parts);
|
||||
$author = array_pop($parts);
|
||||
$name = $this->getNameInput();
|
||||
$table = $this->option('table');
|
||||
$model = $this->option('model');
|
||||
|
||||
if (empty($table) && !empty($model)) {
|
||||
$modelClass = "\\{$author}\\{$plugin}\Models\\{$model}";
|
||||
if (class_exists($modelClass)) {
|
||||
$table = (new $modelClass)->getTable();
|
||||
} else {
|
||||
throw new InvalidArgumentException("The model [{$modelClass}] does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->option('create') && $this->option('update')) {
|
||||
throw new InvalidArgumentException('The create & update options cannot both be set at the same time');
|
||||
}
|
||||
|
||||
if ($this->option('create')) {
|
||||
$scaffold = 'create';
|
||||
} elseif ($this->option('update')) {
|
||||
$scaffold = 'update';
|
||||
} else {
|
||||
$scaffold = 'migrate';
|
||||
}
|
||||
|
||||
if (in_array($scaffold, ['create', 'update']) && empty($table)) {
|
||||
throw new InvalidArgumentException('The table or model options are required when using the create or update options');
|
||||
}
|
||||
|
||||
if (($table || $model) && !in_array($scaffold, ['create', 'update'])) {
|
||||
throw new InvalidArgumentException('One of create or update option is required when using the model or table options');
|
||||
}
|
||||
|
||||
$this->stubs = $this->migrationScaffolds[$scaffold];
|
||||
|
||||
if (!empty($this->option('for-version'))) {
|
||||
$version = $this->option('for-version');
|
||||
} else {
|
||||
$currentVersion = $this->getPlugin()->getPluginVersion();
|
||||
if ($currentVersion === VersionManager::NO_VERSION_VALUE) {
|
||||
throw new InvalidArgumentException('The plugin [' . $this->getPluginIdentifier() . '] does not have a version set and no --version option was provided. Please set a version in the plugin\'s updates/version.yaml file.');
|
||||
}
|
||||
$version = $this->getNextVersion($currentVersion);
|
||||
}
|
||||
|
||||
$vars = [
|
||||
'name' => $name,
|
||||
'author' => $author,
|
||||
'plugin' => $plugin,
|
||||
'version' => $version,
|
||||
];
|
||||
|
||||
if (!empty($model)) {
|
||||
$vars['model'] = $model;
|
||||
}
|
||||
if (!empty($table)) {
|
||||
$vars['table'] = $table;
|
||||
}
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create vars for model fields mappings so they can be used in update/create stubs
|
||||
*/
|
||||
protected function processVars(array $vars): array
|
||||
{
|
||||
$vars = parent::processVars($vars);
|
||||
|
||||
// --model option needed below
|
||||
if (empty($vars['model'])) {
|
||||
return $vars;
|
||||
}
|
||||
|
||||
$vars['fields'] = [];
|
||||
|
||||
$fields_path = plugins_path($vars['plugin_url'] . '/models/' . $vars['lower_model'] . '/fields.yaml');
|
||||
$fields = [];
|
||||
if (file_exists($fields_path)) {
|
||||
$fields = Yaml::parseFile(($fields_path));
|
||||
}
|
||||
|
||||
$modelName = $vars['plugin_namespace'] . '\\Models\\' . $vars['model'];
|
||||
|
||||
$vars['model'] = $model = new $modelName();
|
||||
|
||||
foreach (['fields', 'tabs', 'secondaryTabs'] as $type) {
|
||||
if (!isset($fields[$type])) {
|
||||
continue;
|
||||
}
|
||||
if ($type === 'fields') {
|
||||
$fieldList = $fields[$type];
|
||||
} else {
|
||||
$fieldList = $fields[$type]['fields'];
|
||||
}
|
||||
|
||||
foreach ($fieldList as $field => $config) {
|
||||
if (str_contains($field, '@')) {
|
||||
list($field, $context) = explode('@', $field);
|
||||
}
|
||||
|
||||
$type = $config['type'] ?? 'text';
|
||||
|
||||
if (str_starts_with($field, '_')
|
||||
|| $field === $model->getKeyName()
|
||||
|| str_contains($field, '[')
|
||||
|| in_array($type, ['fileupload', 'relation', 'relationmanager', 'repeater', 'section', 'hint'])
|
||||
|| in_array($field, $model->purgeable ?? [])
|
||||
|| $model->getRelationType($field)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$vars['fields'][$field] = $this->mapFieldType($field, $config, $model);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($model->getRelationDefinitions() as $relationType => $definitions) {
|
||||
if (in_array($relationType, ['belongsTo', 'hasOne'])) {
|
||||
foreach (array_keys($definitions) as $relation) {
|
||||
$vars['fields'][$relation . '_id'] = [
|
||||
'type' => 'foreignId',
|
||||
'index' => true,
|
||||
'required' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($model->methodExists('getSortOrderColumn')) {
|
||||
$field = $model->getSortOrderColumn();
|
||||
$vars['fields'][$field] = [
|
||||
'type' => 'unsignedinteger',
|
||||
'required' => false,
|
||||
'index' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$vars['primaryKey'] = $model->getKeyName();
|
||||
$vars['jsonable'] = $model->getJsonable();
|
||||
$vars['timestamps'] = $model->timestamps;
|
||||
|
||||
if ($morphable = $model->morphTo) {
|
||||
$vars['morphable'] = array_keys($morphable);
|
||||
}
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next version number based on the current number.
|
||||
*/
|
||||
protected function getNextVersion($currentVersion): string
|
||||
{
|
||||
$currentVersion = ltrim($currentVersion, 'v');
|
||||
$parts = explode('.', $currentVersion);
|
||||
$parts[count($parts) - 1] = (int) $parts[count($parts) - 1] + 1;
|
||||
return 'v' . implode('.', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps model fields config to DB Schema column types.
|
||||
*/
|
||||
protected function mapFieldType(string $fieldName, array $fieldConfig, ?Model $model = null) : array
|
||||
{
|
||||
switch ($fieldConfig['type'] ?? 'text') {
|
||||
case 'checkbox':
|
||||
case 'switch':
|
||||
$dbType = 'boolean';
|
||||
break;
|
||||
case 'number':
|
||||
$dbType = 'double';
|
||||
if (isset($fieldConfig['step']) && is_int($fieldConfig['step'])) {
|
||||
$dbType = 'integer';
|
||||
}
|
||||
if ($dbType === 'integer' && isset($fieldConfig['min']) && $fieldConfig['min'] >= 0) {
|
||||
$dbType = 'unsignedInteger';
|
||||
}
|
||||
break;
|
||||
case 'range':
|
||||
$dbType = 'unsignedInteger';
|
||||
break;
|
||||
case 'datepicker':
|
||||
$dbType = $fieldConfig['mode'] ?? 'datetime';
|
||||
break;
|
||||
case 'markdown':
|
||||
$dbType = 'mediumText';
|
||||
break;
|
||||
case 'textarea':
|
||||
$dbType = 'text';
|
||||
break;
|
||||
default:
|
||||
$dbType = 'string';
|
||||
}
|
||||
|
||||
if ($model) {
|
||||
$rule = array_get($model->rules ?? [], $fieldName, '');
|
||||
$rule = is_array($rule) ? implode(',', $rule) : $rule;
|
||||
|
||||
$required = str_contains($rule, 'required') ? true : $fieldConfig['required'] ?? false;
|
||||
} else {
|
||||
$required = $fieldConfig['required'] ?? false;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $dbType,
|
||||
'required' => $required,
|
||||
'index' => in_array($fieldName, ["slug"]) or str_ends_with($fieldName, "_id"),
|
||||
];
|
||||
}
|
||||
}
|
||||
177
modules/system/console/CreateModel.php
Normal file
177
modules/system/console/CreateModel.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Winter\Storm\Support\Str;
|
||||
use System\Console\BaseScaffoldCommand;
|
||||
|
||||
class CreateModel extends BaseScaffoldCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'create:model';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'create:model
|
||||
{plugin : The name of the plugin. <info>(eg: Winter.Blog)</info>}
|
||||
{model : The name of the model to generate. <info>(eg: Post)</info>}
|
||||
{--f|force : Overwrite existing files with generated files.}
|
||||
|
||||
{--a|all : Generate a controller, migration, & seeder for the model}
|
||||
{--c|controller : Create a new controller for the model}
|
||||
{--s|seed : Create a new seeder for the model}
|
||||
{--F|factory : Create a new factory for the model}
|
||||
{--p|pivot : Indicates if the generated model should be a custom intermediate table model}
|
||||
{--no-migration : Don\'t create a migration file for the model}
|
||||
{--uninspiring : Disable inspirational quotes}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Creates a new model.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'make:model',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string The type of class being generated.
|
||||
*/
|
||||
protected $type = 'Model';
|
||||
|
||||
/**
|
||||
* @var string The argument that the generated class name comes from
|
||||
*/
|
||||
protected $nameFrom = 'model';
|
||||
|
||||
/**
|
||||
* @var array A mapping of stubs to generated files.
|
||||
*/
|
||||
protected $stubs = [
|
||||
'scaffold/model/model.stub' => 'models/{{studly_name}}.php',
|
||||
'scaffold/model/fields.stub' => 'models/{{lower_name}}/fields.yaml',
|
||||
'scaffold/model/columns.stub' => 'models/{{lower_name}}/columns.yaml',
|
||||
];
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (parent::handle() === false && !$this->option('force')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->option('all')) {
|
||||
$this->input->setOption('controller', true);
|
||||
$this->input->setOption('seed', true);
|
||||
$this->input->setOption('factory', true);
|
||||
}
|
||||
|
||||
if ($this->option('controller')) {
|
||||
$this->createController();
|
||||
}
|
||||
|
||||
if ($this->option('seed')) {
|
||||
$this->createSeeder();
|
||||
}
|
||||
|
||||
if (!$this->option('no-migration')) {
|
||||
$this->createMigration();
|
||||
}
|
||||
|
||||
if ($this->option('factory')) {
|
||||
$this->createFactory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds controller & model lang helpers to the vars
|
||||
*/
|
||||
protected function processVars($vars): array
|
||||
{
|
||||
$vars = parent::processVars($vars);
|
||||
|
||||
$vars['table_name'] = "{$vars['lower_author']}_{$vars['lower_plugin']}_{$vars['snake_plural_name']}";
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the localization keys and values to be stored in the plugin's localization files
|
||||
* Can reference $this->vars and $this->laravel->getLocale() internally
|
||||
*/
|
||||
protected function getLangKeys(): array
|
||||
{
|
||||
return [
|
||||
'models.general.id' => 'ID',
|
||||
'models.general.created_at' => 'Created At',
|
||||
'models.general.updated_at' => 'Updated At',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a migration for the model.
|
||||
*/
|
||||
public function createMigration()
|
||||
{
|
||||
$this->call('create:migration', [
|
||||
'plugin' => $this->getPluginIdentifier(),
|
||||
'--model' => $this->getNameInput(),
|
||||
'--create' => true,
|
||||
'--force' => $this->option('force'),
|
||||
'--uninspiring' => $this->option('uninspiring'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a seeder for the model.
|
||||
*/
|
||||
public function createSeeder()
|
||||
{
|
||||
// @TODO: Implement this
|
||||
return;
|
||||
|
||||
$this->call('create:seeder', [
|
||||
'plugin' => $this->getPluginIdentifier(),
|
||||
'model' => $this->getNameInput(),
|
||||
'--force' => $this->option('force'),
|
||||
'--uninspiring' => $this->option('uninspiring'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a controller for the model.
|
||||
*/
|
||||
public function createController()
|
||||
{
|
||||
$this->call('create:controller', [
|
||||
'plugin' => $this->getPluginIdentifier(),
|
||||
'controller' => Str::plural($this->argument('model')),
|
||||
'--model' => $this->getNameInput(),
|
||||
'--force' => $this->option('force'),
|
||||
'--uninspiring' => $this->option('uninspiring'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a factory class for the model.
|
||||
*/
|
||||
public function createFactory(): void
|
||||
{
|
||||
$this->call('create:factory', [
|
||||
'plugin' => $this->getPluginIdentifier(),
|
||||
'factory' => "{$this->getNameInput()}Factory",
|
||||
'--model' => $this->getNameInput(),
|
||||
'--force' => $this->option('force'),
|
||||
'--uninspiring' => $this->option('uninspiring'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
69
modules/system/console/CreatePlugin.php
Normal file
69
modules/system/console/CreatePlugin.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use System\Console\BaseScaffoldCommand;
|
||||
|
||||
class CreatePlugin extends BaseScaffoldCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'create:plugin';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'create:plugin
|
||||
{plugin : The name of the plugin to create. <info>(eg: Winter.Blog)</info>}
|
||||
{--f|force : Overwrite existing files with generated files.}
|
||||
{--uninspiring : Disable inspirational quotes}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Creates a new plugin.';
|
||||
|
||||
/**
|
||||
* @var string The type of class being generated.
|
||||
*/
|
||||
protected $type = 'Plugin';
|
||||
|
||||
/**
|
||||
* @var string The argument that the generated class name comes from
|
||||
*/
|
||||
protected $nameFrom = 'plugin';
|
||||
|
||||
/**
|
||||
* @var array A mapping of stubs to generated files.
|
||||
*/
|
||||
protected $stubs = [
|
||||
'scaffold/plugin/plugin.stub' => 'Plugin.php',
|
||||
'scaffold/plugin/version.stub' => 'updates/version.yaml',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var bool Validate the provided plugin input against the PluginManager, default true.
|
||||
*/
|
||||
protected $validatePluginInput = false;
|
||||
|
||||
/**
|
||||
* Get the desired class name from the input.
|
||||
*/
|
||||
protected function getNameInput(): string
|
||||
{
|
||||
return explode('.', $this->getPluginIdentifier())[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the localization keys and values to be stored in the plugin's localization files
|
||||
* Can reference $this->vars and $this->laravel->getLocale() internally
|
||||
*/
|
||||
protected function getLangKeys(): array
|
||||
{
|
||||
return [
|
||||
'plugin.name' => $this->vars['name'],
|
||||
'plugin.description' => 'No description provided yet...',
|
||||
'permissions.some_permission' => 'Some permission',
|
||||
];
|
||||
}
|
||||
}
|
||||
52
modules/system/console/CreateSettings.php
Normal file
52
modules/system/console/CreateSettings.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use System\Console\BaseScaffoldCommand;
|
||||
|
||||
class CreateSettings extends BaseScaffoldCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'create:settings';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'create:settings
|
||||
{plugin : The name of the plugin. <info>(eg: Winter.Blog)</info>}
|
||||
{settings? : The name of the settings model to generate. <info>(eg: BlogSettings)</info>}
|
||||
{--f|force : Overwrite existing files with generated files.}
|
||||
{--uninspiring : Disable inspirational quotes}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Creates a new settings model.';
|
||||
|
||||
/**
|
||||
* @var string The type of class being generated.
|
||||
*/
|
||||
protected $type = 'Settings Model';
|
||||
|
||||
/**
|
||||
* @var string The argument that the generated class name comes from
|
||||
*/
|
||||
protected $nameFrom = 'settings';
|
||||
|
||||
/**
|
||||
* @var array A mapping of stubs to generated files.
|
||||
*/
|
||||
protected $stubs = [
|
||||
'scaffold/settings/model.stub' => 'models/{{studly_name}}.php',
|
||||
'scaffold/settings/fields.stub' => 'models/{{lower_name}}/fields.yaml'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the desired class name from the input.
|
||||
*/
|
||||
protected function getNameInput(): string
|
||||
{
|
||||
return parent::getNameInput() ?: 'Settings';
|
||||
}
|
||||
}
|
||||
119
modules/system/console/CreateTest.php
Normal file
119
modules/system/console/CreateTest.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console;
|
||||
|
||||
use System\Console\BaseScaffoldCommand;
|
||||
use Winter\Storm\Support\Str;
|
||||
|
||||
class CreateTest extends BaseScaffoldCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'create:test';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'create:test
|
||||
{plugin : The name of the plugin. <info>(eg: Winter.Blog)</info>}
|
||||
{name : The name of the test class to generate. Test will be automatically added to the end. Can also be a relative path to a class to generate a test for. <info>(eg: Components\Posts)</info>}
|
||||
{--u|unit : Generate a Unit test (defaults to generating Feature tests).}
|
||||
{--p|pest : Generate a Pest PHP test (defaults to generating PHPUnit tests).}
|
||||
{--f|force : Overwrite existing files with generated files.}
|
||||
{--uninspiring : Disable inspirational quotes}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Creates a new test class.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'make:test',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string The type of class being generated.
|
||||
*/
|
||||
protected $type = 'Test';
|
||||
|
||||
/**
|
||||
* @var array Stub files to make a plugin testable
|
||||
*/
|
||||
protected $pluginStubs = [
|
||||
'scaffold/test/test.plugin.stub' => 'tests/Unit/PluginTest.php',
|
||||
'scaffold/test/phpunit.stub' => 'phpunit.xml',
|
||||
];
|
||||
|
||||
/**
|
||||
* Adds controller & model lang helpers to the vars
|
||||
*/
|
||||
protected function processVars($vars): array
|
||||
{
|
||||
$vars = parent::processVars($vars);
|
||||
|
||||
// Enable testing on the plugin if it isn't already
|
||||
if (!$this->files->exists($this->getDestinationPath() . '/phpunit.xml')) {
|
||||
$this->stubs = array_merge($this->pluginStubs, $this->stubs);
|
||||
}
|
||||
|
||||
// Populate Pest.php if it doesn't exist
|
||||
$isPest = $this->option('pest');
|
||||
if ($isPest && !$this->files->exists($this->getDestinationPath() . '/tests/Pest.php')) {
|
||||
$this->stubs = array_merge($this->stubs, [
|
||||
'scaffold/test/pest.init.stub' => 'tests/Pest.php',
|
||||
]);
|
||||
}
|
||||
|
||||
$prefix = $isPest ? 'pest' : 'test';
|
||||
$type = $this->option('unit') ? 'Unit' : 'Feature';
|
||||
$suffix = $this->option('unit') ? '.unit.stub' : '.stub';
|
||||
|
||||
$name = $this->argument('name');
|
||||
$class = $vars['plugin_namespace'] . '\\' . $name;
|
||||
|
||||
// provided name is a class in the plugin
|
||||
if (class_exists($class)) {
|
||||
// Get the public methods to stub out tests for
|
||||
$reflection = new \ReflectionClass($class);
|
||||
$publicMethods = [];
|
||||
$methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC) ?: [];
|
||||
foreach ($methods as $method) {
|
||||
if ($method->class === $class) {
|
||||
$publicMethods[] = $method->name;
|
||||
}
|
||||
}
|
||||
$vars['public_methods'] = $publicMethods;
|
||||
|
||||
// Generate the necessary stub variables
|
||||
$namePieces = explode('\\', $name);
|
||||
$vars['tested_class_full'] = $class;
|
||||
$vars['tested_class'] = array_pop($namePieces);
|
||||
$testClass = $vars['tested_class'] . 'Test';
|
||||
$suffix = '.class.stub';
|
||||
if (count($namePieces)) {
|
||||
$type .= '\\' . implode('\\', $namePieces);
|
||||
}
|
||||
// sometimes a name is just a name. Move on.
|
||||
} else {
|
||||
$testClass = $name . 'Test';
|
||||
}
|
||||
|
||||
// Just in case :)
|
||||
$testClass = Str::replace('TestTest', 'Test', $testClass);
|
||||
|
||||
$folder = str_replace('\\', '/', $type);
|
||||
$vars['test_class'] = $testClass;
|
||||
$vars['test_namespace'] = "{$vars['plugin_namespace']}\\Tests\\$type";
|
||||
|
||||
$this->stubs["scaffold/test/{$prefix}{$suffix}"] = "tests/$folder/$testClass.php";
|
||||
|
||||
return array_merge($vars, [
|
||||
'test_namespace' => "{$vars['plugin_namespace']}\\Tests\\$type",
|
||||
]);
|
||||
}
|
||||
}
|
||||
47
modules/system/console/PluginDisable.php
Normal file
47
modules/system/console/PluginDisable.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Winter\Storm\Console\Command;
|
||||
use System\Classes\PluginManager;
|
||||
use System\Models\PluginVersion;
|
||||
|
||||
/**
|
||||
* Console command to disable a plugin.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Lucas Zamora
|
||||
*/
|
||||
class PluginDisable extends Command
|
||||
{
|
||||
use Traits\HasPluginArgument;
|
||||
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'plugin:disable';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'plugin:disable
|
||||
{plugin : The plugin to disable. <info>(eg: Winter.Blog)</info>}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Disable an existing plugin.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$pluginName = $this->getPluginIdentifier();
|
||||
$pluginManager = PluginManager::instance();
|
||||
|
||||
// Disable this plugin
|
||||
$pluginManager->disablePlugin($pluginName);
|
||||
|
||||
$this->output->writeln(sprintf('<info>%s:</info> disabled.', $pluginName));
|
||||
}
|
||||
}
|
||||
52
modules/system/console/PluginEnable.php
Normal file
52
modules/system/console/PluginEnable.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Winter\Storm\Console\Command;
|
||||
use System\Classes\PluginManager;
|
||||
use System\Models\PluginVersion;
|
||||
|
||||
/**
|
||||
* Console command to enable a plugin.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Lucas Zamora
|
||||
*/
|
||||
class PluginEnable extends Command
|
||||
{
|
||||
use Traits\HasPluginArgument;
|
||||
|
||||
/**
|
||||
* @var string Only suggest plugins that are disabled
|
||||
*/
|
||||
protected $hasPluginsFilter = 'disabled';
|
||||
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'plugin:enable';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'plugin:enable
|
||||
{plugin : The plugin to disable. <info>(eg: Winter.Blog)</info>}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Enable an existing plugin.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$pluginName = $this->getPluginIdentifier();
|
||||
$pluginManager = PluginManager::instance();
|
||||
|
||||
// Enable this plugin
|
||||
$pluginManager->enablePlugin($pluginName);
|
||||
|
||||
$this->output->writeln(sprintf('<info>%s:</info> enabled.', $pluginName));
|
||||
}
|
||||
}
|
||||
67
modules/system/console/PluginInstall.php
Normal file
67
modules/system/console/PluginInstall.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Winter\Storm\Console\Command;
|
||||
use System\Classes\UpdateManager;
|
||||
use System\Classes\PluginManager;
|
||||
|
||||
/**
|
||||
* Console command to install a new plugin.
|
||||
*
|
||||
* This adds a new plugin by requesting it from the Winter marketplace.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class PluginInstall extends Command
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'plugin:install';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'plugin:install
|
||||
{plugin : The plugin to install. <info>(eg: Winter.Blog)</info>}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Install a plugin from the Winter marketplace.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$pluginName = $this->argument('plugin');
|
||||
$manager = UpdateManager::instance()->setNotesOutput($this->output);
|
||||
|
||||
$pluginDetails = $manager->requestPluginDetails($pluginName);
|
||||
|
||||
$code = array_get($pluginDetails, 'code');
|
||||
$hash = array_get($pluginDetails, 'hash');
|
||||
|
||||
$this->output->writeln(sprintf('<info>Downloading plugin: %s</info>', $code));
|
||||
$manager->downloadPlugin($code, $hash, true);
|
||||
|
||||
$this->output->writeln(sprintf('<info>Unpacking plugin: %s</info>', $code));
|
||||
$manager->extractPlugin($code, $hash);
|
||||
|
||||
/*
|
||||
* Make sure plugin is registered
|
||||
*/
|
||||
$pluginManager = PluginManager::instance();
|
||||
$pluginManager->loadPlugins();
|
||||
$plugin = $pluginManager->findByIdentifier($code);
|
||||
$pluginManager->registerPlugin($plugin, $code);
|
||||
|
||||
/*
|
||||
* Migrate plugin
|
||||
*/
|
||||
$this->output->writeln(sprintf('<info>Migrating plugin...</info>', $code));
|
||||
$manager->updatePlugin($code);
|
||||
}
|
||||
}
|
||||
57
modules/system/console/PluginList.php
Normal file
57
modules/system/console/PluginList.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Helper\TableSeparator;
|
||||
use System\Models\PluginVersion;
|
||||
use Winter\Storm\Console\Command;
|
||||
|
||||
/**
|
||||
* Console command to list existing plugins.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Lucas Zamora
|
||||
*/
|
||||
class PluginList extends Command
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'plugin:list';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'plugin:list';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'List existing plugins.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$allPlugins = PluginVersion::all();
|
||||
$pluginsCount = count($allPlugins);
|
||||
|
||||
if ($pluginsCount <= 0) {
|
||||
$this->info('No plugin found');
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($allPlugins as $plugin) {
|
||||
$rows[] = [
|
||||
$plugin->code,
|
||||
$plugin->version,
|
||||
(!$plugin->is_frozen) ? '<info>Yes</info>': '<fg=red>No</>',
|
||||
(!$plugin->is_disabled) ? '<info>Yes</info>': '<fg=red>No</>',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table(['Plugin name', 'Version', 'Updates enabled', 'Plugin enabled'], $rows);
|
||||
}
|
||||
}
|
||||
63
modules/system/console/PluginRefresh.php
Normal file
63
modules/system/console/PluginRefresh.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Winter\Storm\Console\Command;
|
||||
use System\Classes\UpdateManager;
|
||||
|
||||
/**
|
||||
* Console command to refresh a plugin.
|
||||
*
|
||||
* This destroys all database tables for a specific plugin, then builds them up again.
|
||||
* It is a great way for developers to debug and develop new plugins.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class PluginRefresh extends Command
|
||||
{
|
||||
use \Winter\Storm\Console\Traits\ConfirmsWithInput;
|
||||
use Traits\HasPluginArgument;
|
||||
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'plugin:refresh';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'plugin:refresh
|
||||
{plugin : The plugin to refresh. <info>(eg: Winter.Blog)</info>}
|
||||
{--f|force : Force the operation to run and ignore production warnings and confirmation questions.}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Removes and re-adds an existing plugin.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$pluginName = $this->getPluginIdentifier();
|
||||
|
||||
if (!$this->confirmWithInput(
|
||||
"This will completely remove and reinstall $pluginName. This may result in potential data loss.",
|
||||
$pluginName
|
||||
)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Set the UpdateManager output stream to the CLI
|
||||
$manager = UpdateManager::instance()->setNotesOutput($this->output);
|
||||
|
||||
// Rollback the plugin
|
||||
$manager->rollbackPlugin($pluginName);
|
||||
|
||||
// Reinstall the plugin
|
||||
$this->output->writeln('<info>Reinstalling plugin...</info>');
|
||||
$manager->updatePlugin($pluginName);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
92
modules/system/console/PluginRemove.php
Normal file
92
modules/system/console/PluginRemove.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use File;
|
||||
use Winter\Storm\Console\Command;
|
||||
use System\Classes\UpdateManager;
|
||||
use System\Classes\PluginManager;
|
||||
use System\Classes\VersionManager;
|
||||
|
||||
/**
|
||||
* Console command to remove a plugin.
|
||||
*
|
||||
* This completely deletes an existing plugin, including database tables, files
|
||||
* and directories.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class PluginRemove extends Command
|
||||
{
|
||||
use \Winter\Storm\Console\Traits\ConfirmsWithInput;
|
||||
use Traits\HasPluginArgument;
|
||||
|
||||
/**
|
||||
* @var string Suggest all plugins
|
||||
*/
|
||||
protected $hasPluginsFilter = 'all';
|
||||
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'plugin:remove';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'plugin:remove
|
||||
{plugin : The plugin to remove. <info>(eg: Winter.Blog)</info>}
|
||||
{--f|force : Force the operation to run and ignore production warnings and confirmation questions.}
|
||||
{--r|no-rollback : Skip the rollback of the plugin migrations.}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Removes an existing plugin.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$pluginName = $this->getPluginIdentifier();
|
||||
$pluginManager = PluginManager::instance();
|
||||
|
||||
if (
|
||||
!$pluginManager->hasPlugin($pluginName)
|
||||
&& !VersionManager::instance()->getDatabaseHistory($pluginName)
|
||||
) {
|
||||
$this->error(sprintf('Plugin "%s" could not be found.', $pluginName));
|
||||
return 1;
|
||||
}
|
||||
|
||||
$confirmQuestion = sprintf('This will remove the files for the "%s" plugin.', $pluginName);
|
||||
if (!$this->option('no-rollback')) {
|
||||
$confirmQuestion = sprintf('This will remove the database tables and files for the "%s" plugin.', $pluginName);
|
||||
}
|
||||
|
||||
if (!$this->confirmWithInput(
|
||||
$confirmQuestion,
|
||||
$pluginName
|
||||
)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!$this->option('no-rollback')) {
|
||||
/*
|
||||
* Rollback plugin
|
||||
*/
|
||||
$manager = UpdateManager::instance()->setNotesOutput($this->output);
|
||||
$manager->rollbackPlugin($pluginName);
|
||||
}
|
||||
|
||||
/*
|
||||
* Delete from file system
|
||||
*/
|
||||
if ($pluginPath = $pluginManager->getPluginPath($pluginName)) {
|
||||
File::deleteDirectory($pluginPath);
|
||||
$this->output->writeln(sprintf('<info>Deleted: %s</info>', $pluginPath));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
95
modules/system/console/PluginRollback.php
Normal file
95
modules/system/console/PluginRollback.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Winter\Storm\Console\Command;
|
||||
use System\Classes\UpdateManager;
|
||||
use System\Classes\VersionManager;
|
||||
|
||||
/**
|
||||
* Console command to rollback a plugin.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class PluginRollback extends Command
|
||||
{
|
||||
use \Winter\Storm\Console\Traits\ConfirmsWithInput;
|
||||
use Traits\HasPluginArgument;
|
||||
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'plugin:rollback';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'plugin:rollback
|
||||
{plugin : The plugin to disable. <info>(eg: Winter.Blog)</info>}
|
||||
{version? : If this parameter is not specified the plugin will be completely rolled back; otherwise it will stop on the specified version. <info>(eg: 1.3.9)</info>}
|
||||
{--f|force : Force the operation to run and ignore production warnings and confirmation questions.}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Rollback an existing plugin.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* @throws Exception if the UpdateManager is unable to rollback the requested plugin to the requested version
|
||||
* @throws InvalidArgumentException if the requested rollback version can't be found
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$pluginName = $this->getPluginIdentifier();
|
||||
$stopOnVersion = ltrim(($this->argument('version') ?: null), 'v');
|
||||
|
||||
if ($stopOnVersion) {
|
||||
if (!VersionManager::instance()->hasDatabaseVersion($pluginName, $stopOnVersion)) {
|
||||
throw new InvalidArgumentException('Plugin version not found');
|
||||
}
|
||||
$confirmQuestion = "This will revert $pluginName to version $stopOnVersion - changes to the database and potential data loss may occur.";
|
||||
} else {
|
||||
$confirmQuestion = "This will completely rollback $pluginName. This may result in potential data loss.";
|
||||
}
|
||||
|
||||
if (!$this->confirmWithInput(
|
||||
$confirmQuestion,
|
||||
$pluginName
|
||||
)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$manager = UpdateManager::instance()->setNotesOutput($this->output);
|
||||
|
||||
try {
|
||||
$manager->rollbackPlugin($pluginName, $stopOnVersion);
|
||||
} catch (\Exception $exception) {
|
||||
$lastVersion = VersionManager::instance()->getCurrentVersion($pluginName);
|
||||
$this->output->writeln(sprintf("<comment>An exception occurred during the rollback and the process has been stopped. %s was rolled back to version v%s.</comment>", $pluginName, $lastVersion));
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest values for the optional version argument
|
||||
*/
|
||||
public function suggestVersionValues(?string $value, array $allInput): array
|
||||
{
|
||||
// Get the currently selected plugin
|
||||
$pluginName = $this->getPluginIdentifier($allInput['arguments']['plugin']);
|
||||
|
||||
// Get that plugin's versions from the database
|
||||
$history = VersionManager::instance()->getDatabaseHistory($pluginName);
|
||||
|
||||
// Compile a list of available versions to rollback to
|
||||
$availableVersions = [];
|
||||
foreach ($history as $record) {
|
||||
$availableVersions[] = $record->version;
|
||||
}
|
||||
|
||||
return $availableVersions;
|
||||
}
|
||||
}
|
||||
63
modules/system/console/WinterDown.php
Normal file
63
modules/system/console/WinterDown.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Winter\Storm\Console\Command;
|
||||
use System\Classes\UpdateManager;
|
||||
|
||||
/**
|
||||
* Console command to tear down the database.
|
||||
*
|
||||
* This destroys all database tables that are registered for Winter and all plugins.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class WinterDown extends Command
|
||||
{
|
||||
use \Winter\Storm\Console\Traits\ConfirmsWithInput;
|
||||
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'winter:down';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'winter:down
|
||||
{--f|force : Force the operation to run and ignore production warnings and confirmation questionss.}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Destroys all database tables for Winter and all plugins.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Register aliases for backwards compatibility with October & Laravel
|
||||
$this->setAliases(['october:down', 'migrate:reset']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if (!$this->confirmWithInput(
|
||||
"This will completely delete all database tables in use with your Winter installation.",
|
||||
"DELETE"
|
||||
)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
UpdateManager::instance()
|
||||
->setNotesOutput($this->output)
|
||||
->uninstall();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
278
modules/system/console/WinterEnv.php
Normal file
278
modules/system/console/WinterEnv.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console;
|
||||
|
||||
use App;
|
||||
use Winter\Storm\Parse\EnvFile;
|
||||
use Winter\Storm\Console\Command;
|
||||
use Winter\Storm\Parse\PHP\ArrayFile;
|
||||
|
||||
/**
|
||||
* Console command to convert configuration to use .env files.
|
||||
*
|
||||
* This creates an .env file with some default configuration values, it also converts
|
||||
* the existing PHP-based configuration files to use the `env` function for values.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class WinterEnv extends Command
|
||||
{
|
||||
use \Illuminate\Console\ConfirmableTrait;
|
||||
|
||||
/**
|
||||
* The console command name.
|
||||
*/
|
||||
protected $name = 'winter:env';
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'winter:env
|
||||
{--force : Force the operation to run when in production}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Creates .env file with default configuration values.';
|
||||
|
||||
/**
|
||||
* @var array The env keys that need to have their original values removed from the config files
|
||||
*/
|
||||
protected $protectedKeys = [
|
||||
'APP_KEY',
|
||||
'DB_USERNAME',
|
||||
'DB_PASSWORD',
|
||||
'MAIL_USERNAME',
|
||||
'MAIL_PASSWORD',
|
||||
'REDIS_PASSWORD',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Register aliases for backwards compatibility with October
|
||||
$this->setAliases(['october:env']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if (
|
||||
!$this->confirmToProceed(
|
||||
'The .env file already exists. Proceeding may overwrite some values!',
|
||||
function () {
|
||||
return file_exists($this->laravel->environmentFilePath()) && $this->getLaravel()->environment() === 'production';
|
||||
}
|
||||
)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->updateEnvFile();
|
||||
$this->updateConfigFiles();
|
||||
|
||||
$this->info('.env configuration file has been created.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path of a config file
|
||||
* @param string $config
|
||||
* @return string
|
||||
*/
|
||||
protected function getConfigPath(string $config): string
|
||||
{
|
||||
return rtrim(App::make('path.config'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $config . '.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set env keys to their config values within the EnvFile object
|
||||
*/
|
||||
protected function updateEnvFile(): void
|
||||
{
|
||||
$env = EnvFile::open($this->laravel->environmentFilePath());
|
||||
|
||||
foreach ($this->config() as $config => $items) {
|
||||
foreach ($items as $envKey => $configKey) {
|
||||
$env->set($envKey, config($config . '.' . $configKey));
|
||||
if ($config === 'database' && $envKey === 'DB_CONNECTION') {
|
||||
$default = config('database.default');
|
||||
$dbConfig = $this->dbConfig()[$default] ?? [];
|
||||
|
||||
foreach ($dbConfig as $dbEnvKey => $dbConfigKey) {
|
||||
$value = config(join('.', [$config, 'connections', $default, $dbConfigKey]));
|
||||
// Fix for https://github.com/wintercms/winter/issues/1242#issuecomment-2515344385
|
||||
if ($dbEnvKey === 'DB_DATABASE' && PHP_OS_FAMILY === 'Windows' && str_contains($value, '\\')) {
|
||||
$value = str_replace('\\', '\\\\', $value);
|
||||
}
|
||||
$env->set($dbEnvKey, $value);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config === 'mail' && $envKey === 'MAIL_MAILER') {
|
||||
$default = config('mail.default');
|
||||
$mailConfig = $this->mailConfig()[$default] ?? [];
|
||||
|
||||
foreach ($mailConfig as $mailEnvKey => $mailConfigKey) {
|
||||
$env->set($mailEnvKey, config(join('.', [$config, 'mailers', $default, $mailConfigKey])));
|
||||
}
|
||||
}
|
||||
}
|
||||
$env->addEmptyLine();
|
||||
}
|
||||
|
||||
$env->write();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update config files with env function calls
|
||||
*/
|
||||
protected function updateConfigFiles(): void
|
||||
{
|
||||
foreach ($this->config() as $config => $items) {
|
||||
$arrayFile = ArrayFile::open($this->getConfigPath($config));
|
||||
foreach ($items as $envKey => $configKey) {
|
||||
$arrayFile->set(
|
||||
$configKey,
|
||||
$arrayFile->function('env', $this->getKeyValuePair($envKey, $config . '.' . $configKey))
|
||||
);
|
||||
if ($config === 'database' && $envKey === 'DB_CONNECTION') {
|
||||
foreach ($this->dbConfig() as $connection => $keys) {
|
||||
foreach ($keys as $dbEnvKey => $dbConfigKey) {
|
||||
$path = sprintf('connections.%s.%s', $connection, $dbConfigKey);
|
||||
$arrayFile->set(
|
||||
$path,
|
||||
$arrayFile->function('env', $this->getKeyValuePair($dbEnvKey, $config . '.' . $path))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($config === 'mail' && $envKey === 'MAIL_MAILER') {
|
||||
foreach ($this->mailConfig() as $mailer => $keys) {
|
||||
foreach ($keys as $mailEnvKey => $mailConfigKey) {
|
||||
$path = sprintf('mailers.%s.%s', $mailer, $mailConfigKey);
|
||||
$arrayFile->set(
|
||||
$path,
|
||||
$arrayFile->function('env', $this->getKeyValuePair($mailEnvKey, $config . '.' . $path))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$arrayFile->write();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing the key as the first element and the value
|
||||
* as the second if the key is not a protected key; otherwise the value
|
||||
* will be an empty string
|
||||
*/
|
||||
protected function getKeyValuePair(string $envKey, string $configKey): array
|
||||
{
|
||||
$return = [$envKey, in_array($envKey, $this->protectedKeys) ? '' : config($configKey)];
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of env keys to php config keys for db configs
|
||||
* @return array
|
||||
*/
|
||||
protected function config(): array
|
||||
{
|
||||
return [
|
||||
'app' => [
|
||||
'APP_DEBUG' => 'debug',
|
||||
'APP_URL' => 'url',
|
||||
'APP_KEY' => 'key',
|
||||
],
|
||||
'database' => [
|
||||
'DB_CONNECTION' => 'default',
|
||||
],
|
||||
'cache' => [
|
||||
'CACHE_DRIVER' => 'default',
|
||||
],
|
||||
'session' => [
|
||||
'SESSION_DRIVER' => 'driver',
|
||||
],
|
||||
'queue' => [
|
||||
'QUEUE_CONNECTION' => 'default',
|
||||
],
|
||||
'mail' => [
|
||||
'MAIL_MAILER' => 'default',
|
||||
],
|
||||
'cms' => [
|
||||
'ROUTES_CACHE' => 'enableRoutesCache',
|
||||
'ASSET_CACHE' => 'enableAssetCache',
|
||||
'LINK_POLICY' => 'linkPolicy',
|
||||
'ENABLE_CSRF' => 'enableCsrfProtection',
|
||||
'DATABASE_TEMPLATES' => 'databaseTemplates',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of env keys to php config keys for db configs
|
||||
* @return array
|
||||
*/
|
||||
protected function dbConfig(): array
|
||||
{
|
||||
return [
|
||||
'sqlite' => [
|
||||
'DB_DATABASE' => 'database',
|
||||
],
|
||||
'mysql' => [
|
||||
'DB_HOST' => 'host',
|
||||
'DB_PORT' => 'port',
|
||||
'DB_DATABASE' => 'database',
|
||||
'DB_USERNAME' => 'username',
|
||||
'DB_PASSWORD' => 'password',
|
||||
],
|
||||
'pgsql' => [
|
||||
'DB_HOST' => 'host',
|
||||
'DB_PORT' => 'port',
|
||||
'DB_DATABASE' => 'database',
|
||||
'DB_USERNAME' => 'username',
|
||||
'DB_PASSWORD' => 'password',
|
||||
],
|
||||
'redis' => [
|
||||
'REDIS_HOST' => 'host',
|
||||
'REDIS_PASSWORD' => 'password',
|
||||
'REDIS_PORT' => 'port',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of env keys to php config keys for mail configs
|
||||
* @return array
|
||||
*/
|
||||
protected function mailConfig(): array
|
||||
{
|
||||
return [
|
||||
'smtp' => [
|
||||
'MAIL_ENCRYPTION' => 'encryption',
|
||||
'MAIL_HOST' => 'host',
|
||||
'MAIL_PASSWORD' => 'password',
|
||||
'MAIL_PORT' => 'port',
|
||||
'MAIL_USERNAME' => 'username',
|
||||
],
|
||||
'sendmail' => [
|
||||
'MAIL_SENDMAIL_PATH' => 'path',
|
||||
],
|
||||
'log' => [
|
||||
'MAIL_LOG_CHANNEL' => 'channel',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
86
modules/system/console/WinterFresh.php
Normal file
86
modules/system/console/WinterFresh.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use File;
|
||||
use Artisan;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Console command to remove boilerplate.
|
||||
*
|
||||
* This removes the demo theme and plugin. A great way to start a fresh project!
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class WinterFresh extends Command
|
||||
{
|
||||
use \Illuminate\Console\ConfirmableTrait;
|
||||
|
||||
/**
|
||||
* The console command name.
|
||||
*/
|
||||
protected $name = 'winter:fresh';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Removes the demo theme and plugin.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Register aliases for backwards compatibility with October
|
||||
$this->setAliases(['october:fresh']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->confirmToProceed('Are you sure?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$themeRemoved = false;
|
||||
$pluginRemoved = false;
|
||||
|
||||
$demoThemePath = themes_path().'/demo';
|
||||
if (File::exists($demoThemePath)) {
|
||||
File::deleteDirectory($demoThemePath);
|
||||
$themeRemoved = true;
|
||||
}
|
||||
|
||||
$demoPluginPath = plugins_path().'/winter/demo';
|
||||
if (File::exists($demoPluginPath)) {
|
||||
Artisan::call('plugin:remove', ['plugin' => 'Winter.Demo', '--force' => true]);
|
||||
$pluginRemoved = true;
|
||||
}
|
||||
|
||||
if ($themeRemoved && $pluginRemoved) {
|
||||
$this->info('Demo theme and plugin have been removed! Enjoy a fresh start.');
|
||||
} elseif ($themeRemoved) {
|
||||
$this->info('Demo theme has been removed! Enjoy a fresh start.');
|
||||
} elseif ($pluginRemoved) {
|
||||
$this->info('Demo plugin has been removed! Enjoy a fresh start.');
|
||||
} else {
|
||||
$this->info('Demo theme and plugin have already been removed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console command options.
|
||||
* @return array
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
return [
|
||||
['force', null, InputOption::VALUE_NONE, 'Force the operation to run.'],
|
||||
];
|
||||
}
|
||||
}
|
||||
463
modules/system/console/WinterInstall.php
Normal file
463
modules/system/console/WinterInstall.php
Normal file
@@ -0,0 +1,463 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Backend\Database\Seeds\SeedSetupAdmin;
|
||||
use Config;
|
||||
use Db;
|
||||
use Exception;
|
||||
use File;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use PDO;
|
||||
use Str;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use System\Classes\UpdateManager;
|
||||
use Winter\Storm\Config\ConfigWriter;
|
||||
use Winter\Storm\Console\Command;
|
||||
|
||||
/**
|
||||
* Console command to install Winter.
|
||||
*
|
||||
* This sets up Winter for the first time. It will prompt the user for several
|
||||
* configuration items, including application URL and database config, and then
|
||||
* perform a database migration.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class WinterInstall extends Command
|
||||
{
|
||||
use \Illuminate\Console\ConfirmableTrait;
|
||||
|
||||
/**
|
||||
* The console command name.
|
||||
*/
|
||||
protected $name = 'winter:install';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Set up Winter for the first time.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'october:install',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Winter\Storm\Config\ConfigWriter
|
||||
*/
|
||||
protected $configWriter;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->configWriter = new ConfigWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->displayIntro();
|
||||
|
||||
if (
|
||||
$this->laravel->hasDatabase() &&
|
||||
!$this->confirm('Application appears to be installed already. Continue anyway?', false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setupDatabaseConfig();
|
||||
$this->setupAdminUser();
|
||||
$this->setupCommonValues();
|
||||
|
||||
$chosenToInstall = [];
|
||||
|
||||
if ($this->confirm('Would you like to change the backend options? (URL, default locale, default timezone)')) {
|
||||
$this->setupBackendValues();
|
||||
}
|
||||
|
||||
if ($this->confirm('Configure advanced options?', false)) {
|
||||
$this->setupEncryptionKey();
|
||||
$this->setupAdvancedValues();
|
||||
$chosenToInstall = $this->askToInstallPlugins();
|
||||
}
|
||||
else {
|
||||
$this->setupEncryptionKey(true);
|
||||
}
|
||||
|
||||
$this->setupMigrateDatabase();
|
||||
|
||||
foreach ($chosenToInstall as $pluginCode) {
|
||||
$this->output->writeln('<info>Installing plugin ' . $pluginCode . '</info>');
|
||||
$this->callSilent('plugin:install', [
|
||||
'plugin' => $pluginCode
|
||||
]);
|
||||
$this->output->writeln('<info>' . $pluginCode . ' installed successfully.</info>');
|
||||
}
|
||||
|
||||
$this->displayOutro();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console command options.
|
||||
* @return array
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
return [
|
||||
['force', null, InputOption::VALUE_NONE, 'Force the operation to run.'],
|
||||
];
|
||||
}
|
||||
|
||||
//
|
||||
// Misc
|
||||
//
|
||||
|
||||
protected function setupCommonValues()
|
||||
{
|
||||
$url = $this->ask('Application URL', Config::get('app.url'));
|
||||
$this->writeToConfig('app', ['url' => $url]);
|
||||
}
|
||||
|
||||
protected function setupBackendValues()
|
||||
{
|
||||
// cms.backendUri
|
||||
$backendUri = $this->ask('Backend URL', Config::get('cms.backendUri'));
|
||||
$this->writeToConfig('cms', ['backendUri' => $backendUri]);
|
||||
|
||||
// app.locale
|
||||
$defaultLocale = Config::get('app.locale');
|
||||
try {
|
||||
$availableLocales = (new \Backend\Models\Preference)->getLocaleOptions();
|
||||
$localesByName = [];
|
||||
$i = $defaultLocaleIndex = 0;
|
||||
foreach ($availableLocales as $locale => $name) {
|
||||
$localesByName[$name[0]] = $locale;
|
||||
if ($locale === $defaultLocale) {
|
||||
$defaultLocaleIndex = $i;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
$localeName = $this->choice('Default Backend Locale', array_keys($localesByName), $defaultLocaleIndex);
|
||||
|
||||
$locale = $localesByName[$localeName];
|
||||
} catch (\Exception $e) {
|
||||
// Installation failed halfway through, recover gracefully
|
||||
$locale = $this->ask('Default Backend Locale', $defaultLocale);
|
||||
}
|
||||
$this->writeToConfig('app', ['locale' => $locale]);
|
||||
|
||||
// cms.backendTimezone
|
||||
$defaultTimezone = Config::get('cms.backendTimezone');
|
||||
try {
|
||||
$availableTimezones = (new \Backend\Models\Preference)->getTimezoneOptions();
|
||||
$longestTimezone = max(array_map('strlen', $availableTimezones));
|
||||
$padTo = $longestTimezone - 13 + 1; // (UTC +10:00)
|
||||
$timezonesByName = [];
|
||||
|
||||
foreach ($availableTimezones as $timezone => $name) {
|
||||
$nameParts = explode(') ', $name);
|
||||
$nameParts[0] .= ')';
|
||||
|
||||
$name = str_pad($nameParts[1], $padTo) . $nameParts[0];
|
||||
|
||||
$timezonesByName[$name] = $timezone;
|
||||
}
|
||||
|
||||
$timezonesByContinent = [];
|
||||
$defaultTimezoneIndex = 0;
|
||||
$defaultTimezoneGroupIndex = 0;
|
||||
foreach ($timezonesByName as $name => $zone) {
|
||||
$zone = explode('/', $zone)[0];
|
||||
$timezonesByContinent[$zone][$name] = $zone;
|
||||
if ($zone === $defaultTimezone) {
|
||||
$defaultTimezoneGroupIndex = count(array_keys($timezonesByContinent)) - 1;
|
||||
$defaultTimezoneIndex = count(array_keys($timezonesByContinent[$zone])) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
$timezoneGroup = $this->choice('Timezone Continent', array_keys($timezonesByContinent), $defaultTimezoneGroupIndex);
|
||||
|
||||
$timezoneName = $this->choice('Default Backend Timezone', array_keys($timezonesByContinent[$timezoneGroup]), $defaultTimezoneIndex);
|
||||
|
||||
$timezone = $timezonesByName[$timezoneName];
|
||||
} catch (\Exception $e) {
|
||||
// Installation failed halfway through, recover gracefully
|
||||
$timezone = $this->ask('Default Backend Timezone', $defaultTimezone);
|
||||
}
|
||||
$this->writeToConfig('cms', ['backendTimezone' => $timezone]);
|
||||
}
|
||||
|
||||
protected function setupAdvancedValues()
|
||||
{
|
||||
$defaultMask = $this->ask('File Permission Mask', Config::get('cms.defaultMask.file') ?: '777');
|
||||
$this->writeToConfig('cms', ['defaultMask.file' => $defaultMask]);
|
||||
|
||||
$defaultMask = $this->ask('Folder Permission Mask', Config::get('cms.defaultMask.folder') ?: '777');
|
||||
$this->writeToConfig('cms', ['defaultMask.folder' => $defaultMask]);
|
||||
|
||||
$debug = (bool) $this->confirm('Enable Debug Mode?', true);
|
||||
$this->writeToConfig('app', ['debug' => $debug]);
|
||||
}
|
||||
|
||||
protected function askToInstallPlugins()
|
||||
{
|
||||
$chosenToInstall = [];
|
||||
if ($this->confirm('Install the Winter.Builder plugin?', false)) {
|
||||
$chosenToInstall[] = 'Winter.Builder';
|
||||
}
|
||||
return $chosenToInstall;
|
||||
}
|
||||
|
||||
//
|
||||
// Encryption key
|
||||
//
|
||||
|
||||
protected function setupEncryptionKey($force = false)
|
||||
{
|
||||
$validKey = false;
|
||||
$cipher = Config::get('app.cipher');
|
||||
$keyLength = $this->getKeyLength($cipher);
|
||||
$randomKey = $this->getRandomKey($cipher);
|
||||
|
||||
if ($force) {
|
||||
$key = $randomKey;
|
||||
}
|
||||
else {
|
||||
$this->line(sprintf('Enter a new value of %s characters, or press ENTER to use the generated key', $keyLength));
|
||||
|
||||
while (!$validKey) {
|
||||
$key = $this->ask('Application key', $randomKey);
|
||||
$validKey = Encrypter::supported($key, $cipher);
|
||||
if (!$validKey) {
|
||||
$this->error(sprintf('[ERROR] Invalid key length for "%s" cipher. Supplied key must be %s characters in length.', $cipher, $keyLength));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToConfig('app', ['key' => $key]);
|
||||
|
||||
$this->info(sprintf('Application key [%s] set successfully.', $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random key for the application.
|
||||
*
|
||||
* @param string $cipher
|
||||
* @return string
|
||||
*/
|
||||
protected function getRandomKey($cipher)
|
||||
{
|
||||
return Str::random($this->getKeyLength($cipher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the supported length of a key for a cipher.
|
||||
*
|
||||
* @param string $cipher
|
||||
* @return int
|
||||
*/
|
||||
protected function getKeyLength($cipher)
|
||||
{
|
||||
return $cipher === 'AES-128-CBC' ? 16 : 32;
|
||||
}
|
||||
|
||||
//
|
||||
// Database config
|
||||
//
|
||||
|
||||
protected function setupDatabaseConfig()
|
||||
{
|
||||
$type = $this->choice('Database type', ['MySQL', 'Postgres', 'SQLite', 'SQL Server'], 'SQLite');
|
||||
|
||||
$typeMap = [
|
||||
'SQLite' => 'sqlite',
|
||||
'MySQL' => 'mysql',
|
||||
'Postgres' => 'pgsql',
|
||||
'SQL Server' => 'sqlsrv',
|
||||
];
|
||||
|
||||
$driver = array_get($typeMap, $type, 'sqlite');
|
||||
|
||||
$method = 'setupDatabase'.Str::studly($driver);
|
||||
|
||||
$newConfig = $this->$method();
|
||||
|
||||
$this->writeToConfig('database', ['default' => $driver]);
|
||||
|
||||
foreach ($newConfig as $config => $value) {
|
||||
$this->writeToConfig('database', ['connections.'.$driver.'.'.$config => $value]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function setupDatabaseMysql()
|
||||
{
|
||||
$result = [];
|
||||
$result['host'] = $this->ask('MySQL Host', Config::get('database.connections.mysql.host'));
|
||||
$result['port'] = $this->output->ask('MySQL Port', Config::get('database.connections.mysql.port') ?: false) ?: '';
|
||||
$result['database'] = $this->ask('Database Name', Config::get('database.connections.mysql.database'));
|
||||
$result['username'] = $this->ask('MySQL Login', Config::get('database.connections.mysql.username'));
|
||||
$result['password'] = $this->ask('MySQL Password', Config::get('database.connections.mysql.password') ?: false) ?: '';
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function setupDatabasePgsql()
|
||||
{
|
||||
$result = [];
|
||||
$result['host'] = $this->ask('Postgres Host', Config::get('database.connections.pgsql.host'));
|
||||
$result['port'] = $this->ask('Postgres Port', Config::get('database.connections.pgsql.port') ?: false) ?: '';
|
||||
$result['database'] = $this->ask('Database Name', Config::get('database.connections.pgsql.database'));
|
||||
$result['username'] = $this->ask('Postgres Login', Config::get('database.connections.pgsql.username'));
|
||||
$result['password'] = $this->ask('Postgres Password', Config::get('database.connections.pgsql.password') ?: false) ?: '';
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function setupDatabaseSqlite()
|
||||
{
|
||||
$filename = $this->ask('Database path', Config::get('database.connections.sqlite.database'));
|
||||
|
||||
try {
|
||||
if (!file_exists($filename)) {
|
||||
$directory = dirname($filename);
|
||||
if (!is_dir($directory)) {
|
||||
mkdir($directory, 0777, true);
|
||||
}
|
||||
|
||||
new PDO('sqlite:'.$filename);
|
||||
}
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->error($ex->getMessage());
|
||||
$this->setupDatabaseSqlite();
|
||||
}
|
||||
|
||||
return ['database' => Str::after($filename, base_path() . DIRECTORY_SEPARATOR)];
|
||||
}
|
||||
|
||||
protected function setupDatabaseSqlsrv()
|
||||
{
|
||||
$result = [];
|
||||
$result['host'] = $this->ask('SQL Host', Config::get('database.connections.sqlsrv.host'));
|
||||
$result['port'] = $this->ask('SQL Port', Config::get('database.connections.sqlsrv.port') ?: false) ?: '';
|
||||
$result['database'] = $this->ask('Database Name', Config::get('database.connections.sqlsrv.database'));
|
||||
$result['username'] = $this->ask('SQL Login', Config::get('database.connections.sqlsrv.username'));
|
||||
$result['password'] = $this->ask('SQL Password', Config::get('database.connections.sqlsrv.password') ?: false) ?: '';
|
||||
return $result;
|
||||
}
|
||||
|
||||
//
|
||||
// Migration
|
||||
//
|
||||
|
||||
protected function setupAdminUser()
|
||||
{
|
||||
$this->line('Enter a new value, or press ENTER for the default');
|
||||
|
||||
SeedSetupAdmin::$firstName = $this->ask('First Name', SeedSetupAdmin::$firstName);
|
||||
SeedSetupAdmin::$lastName = $this->ask('Last Name', SeedSetupAdmin::$lastName);
|
||||
SeedSetupAdmin::$email = $this->ask('Email Address', SeedSetupAdmin::$email);
|
||||
SeedSetupAdmin::$login = $this->ask('Admin Login', SeedSetupAdmin::$login);
|
||||
SeedSetupAdmin::$password = $this->ask('Admin Password', Str::random(22));
|
||||
|
||||
if (!$this->confirm('Is the information correct?', true)) {
|
||||
$this->setupAdminUser();
|
||||
}
|
||||
}
|
||||
|
||||
protected function setupMigrateDatabase()
|
||||
{
|
||||
$this->line('Migrating application and plugins...');
|
||||
|
||||
try {
|
||||
Db::purge();
|
||||
|
||||
UpdateManager::instance()
|
||||
->setNotesOutput($this->output)
|
||||
->update()
|
||||
;
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->error($ex->getMessage());
|
||||
$this->setupDatabaseConfig();
|
||||
$this->setupMigrateDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
protected function displayIntro()
|
||||
{
|
||||
$message = [
|
||||
".========================================================================.",
|
||||
" ",
|
||||
" db d8b db d888888b d8b db d888888b d88888b d8888b. \033[1;34m...\033[0m ",
|
||||
" 88 I8I 88 `88' 888o 88 `~~88~~' 88' 88 `8D \033[1;34m... ..... ...\033[0m ",
|
||||
" 88 I8I 88 88 88V8o 88 88 88ooooo 88oobY' \033[1;34m.. ... ..\033[0m ",
|
||||
" Y8 I8I 88 88 88 V8o88 88 88~~~~~ 88`8b \033[1;34m.. ... ..\033[0m ",
|
||||
" `8b d8'8b d8' .88. 88 V888 88 88. 88 `88. \033[1;34m... ..... ...\033[0m ",
|
||||
" `8b8' `8d8' Y888888P VP V8P YP Y88888P 88 YD \033[1;34m...\033[0m ",
|
||||
" ",
|
||||
"`============================= INSTALLATION =============================",
|
||||
"",
|
||||
];
|
||||
|
||||
$this->line($message);
|
||||
}
|
||||
|
||||
protected function displayOutro()
|
||||
{
|
||||
$message = [
|
||||
// Sourced from https://www.asciiart.eu/holiday-and-events/christmas/snowmen
|
||||
".===========================================================.",
|
||||
" * * *. * . * . ",
|
||||
" . . __ * . * . * ",
|
||||
" * * . . _|__|_ * __ . * ",
|
||||
" /\ /\ ('') * _|__|_ . ",
|
||||
" / \ * / \ * . <( . )> * . ('') * * ",
|
||||
" / \ / \ . _(__.__)_ _ ,--<( . )> . . ",
|
||||
"/ \ / \ * | | )),` ( . ) * ",
|
||||
" `||` .. `||` . *... ==========='` ... '--`-` ... * jb .",
|
||||
"`================== INSTALLATION COMPLETE =================='",
|
||||
"",
|
||||
];
|
||||
|
||||
$this->line($message);
|
||||
}
|
||||
|
||||
protected function writeToConfig($file, $values)
|
||||
{
|
||||
$configFile = $this->getConfigFile($file);
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
Config::set($file.'.'.$key, $value);
|
||||
}
|
||||
|
||||
$this->configWriter->toFile($configFile, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a config file and contents.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getConfigFile($name = 'app')
|
||||
{
|
||||
$env = $this->option('env') ? $this->option('env').'/' : '';
|
||||
|
||||
$name .= '.php';
|
||||
|
||||
$contents = File::get($path = $this->laravel['path.config']."/{$env}{$name}");
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
240
modules/system/console/WinterManifest.php
Normal file
240
modules/system/console/WinterManifest.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use ApplicationException;
|
||||
use Http;
|
||||
use ZipArchive;
|
||||
use System\Classes\FileManifest;
|
||||
use System\Classes\SourceManifest;
|
||||
|
||||
/**
|
||||
* Console command to generate a release/tag manifest for Winter CMS version checks.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Ben Thomson
|
||||
* @author Winter CMS
|
||||
*/
|
||||
class WinterManifest extends \Illuminate\Console\Command
|
||||
{
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Generates a build manifest of Winter CMS builds.';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'winter:manifest
|
||||
{target : Specifies the target file for the build manifest.}
|
||||
{--token= : Specifies a GitHub token, to get around rate limits.}
|
||||
{--minBuild= : Specifies the minimum build number to retrieve from the source.}
|
||||
{--maxBuild= : Specifies the maximum build number to retreive from the source.}';
|
||||
|
||||
/**
|
||||
* @var bool Indicates whether the command should be shown in the Artisan command list.
|
||||
*/
|
||||
protected $hidden = true;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Register aliases for backwards compatibility with October
|
||||
$this->setAliases(['october:manifest']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$minBuild = $this->getVersionInt($this->option('minBuild') ?? '1.0.420');
|
||||
$maxBuild = $this->getVersionInt($this->option('maxBuild') ?? '1.999.999');
|
||||
|
||||
$targetFile = (substr($this->argument('target'), 0, 1) === '/')
|
||||
? $this->argument('target')
|
||||
: getcwd() . '/' . $this->argument('target');
|
||||
|
||||
if (empty($targetFile)) {
|
||||
throw new ApplicationException(
|
||||
'A target argument must be specified for the generated manifest file.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($minBuild > $maxBuild) {
|
||||
throw new ApplicationException(
|
||||
'Minimum build specified is larger than the maximum build specified.'
|
||||
);
|
||||
}
|
||||
|
||||
if (file_exists($targetFile)) {
|
||||
$manifest = new SourceManifest($targetFile);
|
||||
} else {
|
||||
$manifest = new SourceManifest('', null, false);
|
||||
}
|
||||
|
||||
// Create temporary directory to hold builds
|
||||
$buildDir = storage_path('temp/builds/');
|
||||
if (!is_dir($buildDir)) {
|
||||
mkdir($buildDir, 0775, true);
|
||||
}
|
||||
|
||||
// Find all released builds
|
||||
$page = 0;
|
||||
$sourceBuilds = [];
|
||||
|
||||
while (true) {
|
||||
++$page;
|
||||
|
||||
if ($this->option('token')) {
|
||||
$url = 'https://' . $this->option('token') . '@api.github.com/repos/wintercms/winter/tags?per_page=100&page=' . $page;
|
||||
} else {
|
||||
$url = 'https://api.github.com/repos/wintercms/winter/tags?per_page=100&page=' . $page;
|
||||
}
|
||||
|
||||
$builds = Http::get($url, function ($http) {
|
||||
$http->header('User-Agent', 'Winter CMS');
|
||||
$http->header('Accept', 'application/vnd.github.v3+json');
|
||||
});
|
||||
|
||||
if ($builds->code !== 200) {
|
||||
break;
|
||||
}
|
||||
|
||||
$builds = json_decode($builds->body);
|
||||
|
||||
if (empty($builds)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($builds as $build) {
|
||||
$version = preg_replace('/[^0-9\.]+/', '', $build->name);
|
||||
$versionInt = $this->getVersionInt($version);
|
||||
|
||||
if ($versionInt >= $minBuild && $versionInt <= $maxBuild) {
|
||||
$sourceBuilds[] = [
|
||||
'version' => $version,
|
||||
'download' => $build->zipball_url
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by version number
|
||||
$sourceBuilds = array_sort($sourceBuilds, function ($item) {
|
||||
return $this->getVersionInt($item['version']);
|
||||
});
|
||||
|
||||
foreach ($sourceBuilds as $sourceBuild) {
|
||||
$build = $sourceBuild['version'];
|
||||
|
||||
// Download version from GitHub
|
||||
$this->comment('Processing build ' . $build);
|
||||
$this->line(' - Downloading...');
|
||||
|
||||
if (file_exists($buildDir . 'build-' . $build . '.zip') || is_dir($buildDir . $build . '/')) {
|
||||
$this->info(' - Already downloaded.');
|
||||
} else {
|
||||
Http::get($sourceBuild['download'], function ($http) use ($buildDir, $build) {
|
||||
$http->header('User-Agent', 'Winter CMS');
|
||||
$http->toFile($buildDir . 'build-' . $build . '.zip');
|
||||
});
|
||||
|
||||
$zipFile = @file_get_contents($buildDir . 'build-' . $build . '.zip');
|
||||
|
||||
if (empty($zipFile)) {
|
||||
$this->error(' - Not found (' . $sourceBuild['download'] . ').');
|
||||
break;
|
||||
}
|
||||
|
||||
$this->info(' - Downloaded.');
|
||||
}
|
||||
|
||||
// Extract version
|
||||
$this->line(' - Extracting...');
|
||||
if (is_dir($buildDir . $build . '/')) {
|
||||
$this->info(' - Already extracted.');
|
||||
} else {
|
||||
$zip = new ZipArchive;
|
||||
if ($zip->open($buildDir . 'build-' . $build . '.zip')) {
|
||||
$rootFolder = substr($zip->statIndex(0)['name'], 0, -1);
|
||||
|
||||
$toExtract = [];
|
||||
$paths = [
|
||||
$rootFolder . '/modules/backend/',
|
||||
$rootFolder . '/modules/cms/',
|
||||
$rootFolder . '/modules/system/',
|
||||
];
|
||||
|
||||
// Only get necessary files from the modules directory
|
||||
for ($i = 1; $i < $zip->numFiles; ++$i) {
|
||||
$filename = $zip->statIndex($i)['name'];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (strpos($filename, $path) === 0) {
|
||||
$toExtract[] = $filename;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!count($toExtract)) {
|
||||
$this->error(' - Unable to get valid files for extraction. Cancelled.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$zip->extractTo($buildDir . $build . '/', $toExtract);
|
||||
$zip->close();
|
||||
|
||||
// Rename root folder
|
||||
rename($buildDir . $build . '/' . $rootFolder, $buildDir . $build . '/winter-' . $build);
|
||||
|
||||
// Remove ZIP file
|
||||
unlink($buildDir . 'build-' . $build . '.zip');
|
||||
} else {
|
||||
$this->error(' - Unable to extract zip file. Cancelled.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$this->info(' - Extracted.');
|
||||
}
|
||||
|
||||
// Add build to manifest
|
||||
$this->line(' - Adding to manifest...');
|
||||
$buildManifest = new FileManifest($buildDir . $build . '/winter-' . $build);
|
||||
$manifest->addBuild($build, $buildManifest);
|
||||
$this->info(' - Added.');
|
||||
}
|
||||
|
||||
// Generate manifest
|
||||
$this->comment('Generating manifest...');
|
||||
file_put_contents($targetFile, $manifest->generate());
|
||||
|
||||
$this->comment('Completed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a version string into an integer for comparison.
|
||||
*
|
||||
* @param string $version
|
||||
* @throws ApplicationException if a version string does not match the format "major.minor.path"
|
||||
* @return int
|
||||
*/
|
||||
protected function getVersionInt(string $version)
|
||||
{
|
||||
// Get major.minor.patch versions
|
||||
if (!preg_match('/^v?([0-9]+)\.([0-9]+)\.([0-9]+)/', $version, $versionParts)) {
|
||||
throw new ApplicationException('Invalid version string - must be of the format "major.minor.path"');
|
||||
}
|
||||
|
||||
$int = $versionParts[1] * 1000000;
|
||||
$int += $versionParts[2] * 1000;
|
||||
$int += $versionParts[3];
|
||||
|
||||
return $int;
|
||||
}
|
||||
}
|
||||
272
modules/system/console/WinterMirror.php
Normal file
272
modules/system/console/WinterMirror.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use File;
|
||||
use Event;
|
||||
use StdClass;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
/**
|
||||
* Console command to implement a "public" folder.
|
||||
*
|
||||
* This command will create symbolic links to files and directories
|
||||
* that are commonly required to be publicly available.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class WinterMirror extends Command
|
||||
{
|
||||
/**
|
||||
* The console command name.
|
||||
*/
|
||||
protected $name = 'winter:mirror';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Generates a mirrored public folder using symbolic links.';
|
||||
|
||||
/**
|
||||
* @var array Files that should be mirrored
|
||||
*/
|
||||
protected $files = [
|
||||
'.htaccess',
|
||||
'.user.ini',
|
||||
'index.php',
|
||||
'favicon.ico',
|
||||
'robots.txt',
|
||||
'humans.txt',
|
||||
'sitemap.xml',
|
||||
'llms.txt',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array Directories that should be mirrored
|
||||
*/
|
||||
protected $directories = [
|
||||
'storage/app/uploads/public',
|
||||
'storage/app/media',
|
||||
'storage/app/resized',
|
||||
'storage/temp/public',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array Wildcard paths that should be mirrored
|
||||
*/
|
||||
protected $wildcards = [
|
||||
'modules/*/assets',
|
||||
'modules/*/resources',
|
||||
'modules/*/behaviors/*/assets',
|
||||
'modules/*/behaviors/*/resources',
|
||||
'modules/*/components/*/assets',
|
||||
'modules/*/components/*/resources',
|
||||
'modules/*/widgets/*/assets',
|
||||
'modules/*/widgets/*/resources',
|
||||
'modules/*/formwidgets/*/assets',
|
||||
'modules/*/formwidgets/*/resources',
|
||||
'modules/*/reportwidgets/*/assets',
|
||||
'modules/*/reportwidgets/*/resources',
|
||||
|
||||
'plugins/*/*/assets',
|
||||
'plugins/*/*/resources',
|
||||
'plugins/*/*/behaviors/*/assets',
|
||||
'plugins/*/*/behaviors/*/resources',
|
||||
'plugins/*/*/components/*/assets',
|
||||
'plugins/*/*/components/*/resources',
|
||||
'plugins/*/*/reportwidgets/*/assets',
|
||||
'plugins/*/*/reportwidgets/*/resources',
|
||||
'plugins/*/*/formwidgets/*/assets',
|
||||
'plugins/*/*/formwidgets/*/resources',
|
||||
'plugins/*/*/widgets/*/assets',
|
||||
'plugins/*/*/widgets/*/resources',
|
||||
|
||||
'themes/*/assets',
|
||||
'themes/*/resources',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string|null Local cache of the mirror destination path
|
||||
*/
|
||||
protected $destinationPath;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Register aliases for backwards compatibility with October
|
||||
$this->setAliases(['october:mirror']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->getDestinationPath();
|
||||
|
||||
$paths = new StdClass();
|
||||
$paths->files = $this->files;
|
||||
$paths->directories = $this->directories;
|
||||
$paths->wildcards = $this->wildcards;
|
||||
|
||||
/**
|
||||
* @event system.console.mirror.extendPaths
|
||||
* Enables extending the `php artisan winter:mirror` command
|
||||
*
|
||||
* You will have access to a $paths stdClass with `files`, `directories`, `wildcards` properties available for modifying.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* Event::listen('system.console.mirror.extendPaths', function ($paths) {
|
||||
* $paths->directories = array_merge($paths->directories, ['plugins/myauthor/myplugin/public']);
|
||||
* });
|
||||
*
|
||||
*/
|
||||
Event::fire('system.console.mirror.extendPaths', [$paths]);
|
||||
|
||||
foreach ($paths->files as $file) {
|
||||
$this->mirrorFile($file);
|
||||
}
|
||||
|
||||
foreach ($paths->directories as $directory) {
|
||||
$this->mirrorDirectory($directory);
|
||||
}
|
||||
|
||||
foreach ($paths->wildcards as $wildcard) {
|
||||
$this->mirrorWildcard($wildcard);
|
||||
}
|
||||
|
||||
$this->output->writeln('<info>Mirror complete!</info>');
|
||||
}
|
||||
|
||||
protected function mirrorFile($file)
|
||||
{
|
||||
$this->output->writeln(sprintf('<info> - Mirroring: %s</info>', $file));
|
||||
|
||||
$src = base_path().'/'.$file;
|
||||
|
||||
$dest = $this->getDestinationPath().'/'.$file;
|
||||
|
||||
if (!File::isFile($src) || File::isFile($dest)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->mirror($src, $dest);
|
||||
}
|
||||
|
||||
protected function mirrorDirectory($directory)
|
||||
{
|
||||
$this->output->writeln(sprintf('<info> - Mirroring: %s</info>', $directory));
|
||||
|
||||
$src = base_path().'/'.$directory;
|
||||
|
||||
$dest = $this->getDestinationPath().'/'.$directory;
|
||||
|
||||
if (!File::isDirectory($src) || File::isDirectory($dest)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File::isDirectory(dirname($dest))) {
|
||||
File::makeDirectory(dirname($dest), 0755, true);
|
||||
}
|
||||
|
||||
$this->mirror($src, $dest);
|
||||
}
|
||||
|
||||
protected function mirrorWildcard($wildcard)
|
||||
{
|
||||
if (strpos($wildcard, '*') === false) {
|
||||
return $this->mirrorDirectory($wildcard);
|
||||
}
|
||||
|
||||
list($start, $end) = explode('*', $wildcard, 2);
|
||||
|
||||
$startDir = base_path().'/'.$start;
|
||||
|
||||
if (!File::isDirectory($startDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (File::directories($startDir) as $directory) {
|
||||
$this->mirrorWildcard($start.basename($directory).$end);
|
||||
}
|
||||
}
|
||||
|
||||
protected function mirror($src, $dest)
|
||||
{
|
||||
if ($this->option('relative') && PHP_OS_FAMILY !== 'Windows') {
|
||||
$src = $this->getRelativePath($dest, $src);
|
||||
|
||||
if (strpos($src, '../') === 0) {
|
||||
$src = rtrim(substr($src, 3), '/');
|
||||
}
|
||||
}
|
||||
|
||||
File::link($src, $dest);
|
||||
}
|
||||
|
||||
protected function getDestinationPath()
|
||||
{
|
||||
if ($this->destinationPath !== null) {
|
||||
return $this->destinationPath;
|
||||
}
|
||||
|
||||
$destPath = $this->argument('destination');
|
||||
if (realpath($destPath) === false) {
|
||||
$destPath = base_path() . '/' . $destPath;
|
||||
}
|
||||
|
||||
if (!File::isDirectory($destPath)) {
|
||||
File::makeDirectory($destPath, 0755, true);
|
||||
}
|
||||
|
||||
$destPath = realpath($destPath);
|
||||
|
||||
$this->output->writeln(sprintf('<info>Destination: %s</info>', $destPath));
|
||||
|
||||
return $this->destinationPath = $destPath;
|
||||
}
|
||||
|
||||
protected function getRelativePath($from, $to)
|
||||
{
|
||||
$from = str_replace('\\', '/', $from);
|
||||
$to = str_replace('\\', '/', $to);
|
||||
|
||||
$dir = explode('/', is_file($from) ? dirname($from) : rtrim($from, '/'));
|
||||
$file = explode('/', $to);
|
||||
|
||||
while ($dir && $file && ($dir[0] == $file[0])) {
|
||||
array_shift($dir);
|
||||
array_shift($file);
|
||||
}
|
||||
|
||||
return str_repeat('../', count($dir)) . implode('/', $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console command arguments.
|
||||
* @return array
|
||||
*/
|
||||
protected function getArguments()
|
||||
{
|
||||
return [
|
||||
['destination', InputArgument::REQUIRED, 'The destination path relative to the current directory. Eg: public/'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console command options.
|
||||
* @return array
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
return [
|
||||
['relative', null, InputOption::VALUE_NONE, 'Create symlinks relative to the public directory.'],
|
||||
];
|
||||
}
|
||||
}
|
||||
275
modules/system/console/WinterTest.php
Normal file
275
modules/system/console/WinterTest.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Config;
|
||||
use Symfony\Component\Process\Exception\ProcessSignaledException;
|
||||
use Symfony\Component\Process\ExecutableFinder;
|
||||
use Symfony\Component\Process\Process;
|
||||
use System\Classes\PluginManager;
|
||||
use Winter\Storm\Console\Command;
|
||||
use Winter\Storm\Exception\ApplicationException;
|
||||
use Winter\Storm\Filesystem\PathResolver;
|
||||
use Winter\Storm\Support\Str;
|
||||
|
||||
/**
|
||||
* Console command to run tests for plugins and modules.
|
||||
*
|
||||
* If a plugin is provided, this command will search for a `phpunit.xml` file inside the plugin's directory and run its tests.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
*/
|
||||
class WinterTest extends Command
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'winter:test';
|
||||
|
||||
/**
|
||||
* @var string The console command name.
|
||||
*/
|
||||
protected $name = 'winter:test';
|
||||
|
||||
/**
|
||||
* @var string The console command signature as ignoreValidationErrors causes options not to be registered.
|
||||
*/
|
||||
protected $signature = 'winter:test
|
||||
{phpunitArgs?* : Arguments to pass through to PHPUnit}
|
||||
{?--c|configuration= : A specific phpunit xml file}
|
||||
{?--b|bootstrap= : A custom PHPUnit bootstrap file}
|
||||
{?--p|plugin=* : List of plugins to test}
|
||||
{?--m|module=* : List of modules to test}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Run tests for the Winter CMS core or an existing plugin.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'test',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var ?string Path to phpunit binary
|
||||
*/
|
||||
protected $phpUnitExec = null;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
/**
|
||||
* Ignore validation errors as option proxying is used by this command
|
||||
* @see https://github.com/nunomaduro/collision/blob/stable/src/Adapters/Laravel/Commands/TestCommand.php
|
||||
*/
|
||||
$this->ignoreValidationErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if Pest is being used.
|
||||
*/
|
||||
protected function usingPest(): bool
|
||||
{
|
||||
return class_exists(\Pest\Laravel\PestServiceProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @throws ApplicationException
|
||||
* @return int|void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$arguments = $this->argument('phpunitArgs');
|
||||
|
||||
if (($config = $this->option('configuration')) && file_exists($config)) {
|
||||
return $this->execPhpUnit($config, $arguments);
|
||||
}
|
||||
|
||||
$configs = $this->getPhpUnitConfigs();
|
||||
$exitCode = null;
|
||||
|
||||
// loop over arguments and run specified tests
|
||||
foreach (['module', 'plugin'] as $type) {
|
||||
if ($this->option($type)) {
|
||||
foreach ($this->option($type) as $target) {
|
||||
$target = strtolower($target);
|
||||
if (!isset($configs[$type . 's'][$target])) {
|
||||
throw new ApplicationException(sprintf(
|
||||
'Unable to find %s %s\'s phpunit.xml file',
|
||||
$type,
|
||||
$target
|
||||
));
|
||||
}
|
||||
$this->info(sprintf('Running tests for %s: %s', $type, $target));
|
||||
$exit = $this->execPhpUnit($configs[$type . 's'][$target], $arguments);
|
||||
// keep non 0 exit codes for return
|
||||
$exitCode = !$exitCode ? $exit : $exitCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we ran a specific test above we should have an exit code
|
||||
if (!is_null($exitCode)) {
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
// default to running all defined configs found
|
||||
foreach (['modules', 'plugins'] as $type) {
|
||||
foreach ($configs[$type] as $name => $config) {
|
||||
$this->info(
|
||||
$type === 'plugins'
|
||||
? 'Running tests for plugin: ' . PluginManager::instance()->normalizeIdentifier($name)
|
||||
: 'Running tests for module: ' . $name
|
||||
);
|
||||
$exit = $this->execPhpUnit($config, $arguments);
|
||||
// keep non 0 exit codes for return
|
||||
$exitCode = !$exitCode ? $exit : $exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
return $exitCode ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a phpunit test
|
||||
*
|
||||
* @param string $config Path to configuration file
|
||||
* @param array $args Array of params for PHPUnit
|
||||
* @return int Exit code from process
|
||||
*/
|
||||
protected function execPhpUnit(string $config, array $args): int
|
||||
{
|
||||
// Find and bind the phpunit executable
|
||||
if (!$this->phpUnitExec) {
|
||||
$bin = $this->usingPest() ? 'pest' : 'phpunit';
|
||||
$this->phpUnitExec = (new ExecutableFinder())
|
||||
->find($bin, base_path("vendor/bin/$bin"), [base_path('vendor')]);
|
||||
}
|
||||
|
||||
// Resolve the configuration path based on the current working directory
|
||||
$configPath = realpath($config);
|
||||
$bootstrapPath = (string) simplexml_load_file($configPath)['bootstrap'];
|
||||
|
||||
// Use a default bootstrap path if none is specified in the config
|
||||
if (empty($bootstrapPath)) {
|
||||
$bootstrapPath = base_path('modules/system/tests/bootstrap/app.php');
|
||||
} elseif ($this->option('bootstrap')) {
|
||||
$bootstrapPath = $this->option('bootstrap');
|
||||
} else {
|
||||
// Temporarily switch the working directory to the config path to account for relative paths.
|
||||
$cwd = getcwd();
|
||||
chdir(dirname($config));
|
||||
$bootstrapPath = PathResolver::resolve($bootstrapPath);
|
||||
chdir($cwd);
|
||||
}
|
||||
|
||||
if (!is_file($bootstrapPath)) {
|
||||
throw new ApplicationException(sprintf(
|
||||
'Unable to find the bootstrap file "%s"',
|
||||
$bootstrapPath,
|
||||
));
|
||||
}
|
||||
|
||||
$testDirectory = Str::after(dirname($config), base_path() . DIRECTORY_SEPARATOR) . '/tests';
|
||||
|
||||
$generatedArgs = [
|
||||
$this->phpUnitExec,
|
||||
'--configuration=' . $config,
|
||||
'--bootstrap=' . $bootstrapPath,
|
||||
];
|
||||
if ($this->usingPest()) {
|
||||
$generatedArgs[] = '--test-directory=' . $testDirectory;
|
||||
}
|
||||
|
||||
$process = new Process(
|
||||
array_merge($generatedArgs, $args),
|
||||
base_path(),
|
||||
[
|
||||
'APP_ENV' => 'testing',
|
||||
'CACHE_DRIVER' => 'array',
|
||||
'SESSION_DRIVER' => 'array',
|
||||
],
|
||||
null
|
||||
);
|
||||
|
||||
// Set an unlimited timeout
|
||||
$process->setTimeout(0);
|
||||
|
||||
// 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 ($type, $line) {
|
||||
$this->output->write($line);
|
||||
});
|
||||
} catch (ProcessSignaledException $e) {
|
||||
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all PHPUnit config files (core, lib, plugins)
|
||||
*/
|
||||
protected function getPhpUnitConfigs(): array
|
||||
{
|
||||
$configs = [
|
||||
'modules' => [],
|
||||
'plugins' => []
|
||||
];
|
||||
|
||||
foreach (Config::get('cms.loadModules', ['System', 'Cms', 'Backend']) as $module) {
|
||||
$module = strtolower($module);
|
||||
if ($path = $this->getPhpUnitXmlFile(base_path('modules/' . $module))) {
|
||||
$configs['modules'][$module] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (PluginManager::instance()->getPlugins() as $plugin) {
|
||||
if ($path = $this->getPhpUnitXmlFile($plugin->getPluginPath())) {
|
||||
$configs['plugins'][strtolower($plugin->getPluginIdentifier())] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
return $configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for the config file to use.
|
||||
* Priority order is: phpunit.xml, phpunit.xml.dist
|
||||
*/
|
||||
protected function getPhpUnitXmlFile(string $path): ?string
|
||||
{
|
||||
// If a phpunit.xml file exists, returns its path
|
||||
$configFilePath = $path . DIRECTORY_SEPARATOR . 'phpunit.xml';
|
||||
|
||||
if (file_exists($configFilePath)) {
|
||||
return $configFilePath;
|
||||
}
|
||||
|
||||
// Fallback to phpunit.xml.dist file path if it exists
|
||||
$distFilePath = $path . DIRECTORY_SEPARATOR . 'phpunit.xml.dist';
|
||||
if (file_exists($distFilePath)) {
|
||||
return $distFilePath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
52
modules/system/console/WinterUp.php
Normal file
52
modules/system/console/WinterUp.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Isolatable;
|
||||
use System\Classes\UpdateManager;
|
||||
|
||||
/**
|
||||
* Console command to migrate the database.
|
||||
*
|
||||
* This builds up all database tables that are registered for Winter and all plugins.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class WinterUp extends Command implements Isolatable
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'winter:up
|
||||
{--seed : Included for compatibility with Laravel default signature, no effect at this time}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Builds database tables for Winter and all plugins.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Register aliases for backwards compatibility with October
|
||||
$this->setAliases(['october:up', 'migrate']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->output->writeln('<info>Migrating application and plugins...</info>');
|
||||
|
||||
UpdateManager::instance()
|
||||
->setNotesOutput($this->output)
|
||||
->update();
|
||||
}
|
||||
}
|
||||
127
modules/system/console/WinterUpdate.php
Normal file
127
modules/system/console/WinterUpdate.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Str;
|
||||
use Illuminate\Console\Command;
|
||||
use System\Classes\UpdateManager;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Console command to perform a system update.
|
||||
*
|
||||
* This updates Winter CMS and all plugins, database and files. It uses the
|
||||
* Winter gateway to receive the files via a package manager, then saves
|
||||
* the latest build number to the system.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class WinterUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The console command name.
|
||||
*/
|
||||
protected $name = 'winter:update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Updates Winter CMS and all plugins, database and files.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Register aliases for backwards compatibility with October
|
||||
$this->setAliases(['october:update']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->output->writeln('<info>Updating Winter...</info>');
|
||||
$manager = UpdateManager::instance()->setNotesOutput($this->output);
|
||||
$forceUpdate = $this->option('force');
|
||||
|
||||
/*
|
||||
* Check for disabilities
|
||||
*/
|
||||
$disableCore = $disablePlugins = $disableThemes = false;
|
||||
|
||||
if ($this->option('plugins')) {
|
||||
$disableCore = true;
|
||||
$disableThemes = true;
|
||||
}
|
||||
|
||||
if ($this->option('core')) {
|
||||
$disablePlugins = true;
|
||||
$disableThemes = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform update
|
||||
*/
|
||||
$updateList = $manager->requestUpdateList($forceUpdate);
|
||||
$updates = (int) array_get($updateList, 'update', 0);
|
||||
|
||||
if ($updates == 0) {
|
||||
$this->output->writeln('<info>No new updates found</info>');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->writeln(sprintf('<info>Found %s new %s!</info>', $updates, Str::plural('update', $updates)));
|
||||
|
||||
$coreHash = $disableCore ? null : array_get($updateList, 'core.hash');
|
||||
$coreBuild = array_get($updateList, 'core.build');
|
||||
|
||||
if ($coreHash) {
|
||||
$this->output->writeln('<info>Downloading application files</info>');
|
||||
$manager->downloadCore($coreHash);
|
||||
}
|
||||
|
||||
$plugins = $disablePlugins ? [] : array_get($updateList, 'plugins');
|
||||
foreach ($plugins as $code => $plugin) {
|
||||
$pluginName = array_get($plugin, 'name');
|
||||
$pluginHash = array_get($plugin, 'hash');
|
||||
|
||||
$this->output->writeln(sprintf('<info>Downloading plugin: %s</info>', $pluginName));
|
||||
$manager->downloadPlugin($code, $pluginHash);
|
||||
}
|
||||
|
||||
if ($coreHash) {
|
||||
$this->output->writeln('<info>Unpacking application files</info>');
|
||||
$manager->extractCore();
|
||||
$manager->setBuild($coreBuild, $coreHash);
|
||||
}
|
||||
|
||||
foreach ($plugins as $code => $plugin) {
|
||||
$pluginName = array_get($plugin, 'name');
|
||||
$pluginHash = array_get($plugin, 'hash');
|
||||
|
||||
$this->output->writeln(sprintf('<info>Unpacking plugin: %s</info>', $pluginName));
|
||||
$manager->extractPlugin($code, $pluginHash);
|
||||
}
|
||||
|
||||
/*
|
||||
* Run migrations
|
||||
*/
|
||||
$this->call('winter:up');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console command options.
|
||||
* @return array
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
return [
|
||||
['force', null, InputOption::VALUE_NONE, 'Force updates.'],
|
||||
['core', null, InputOption::VALUE_NONE, 'Update core application files only.'],
|
||||
['plugins', null, InputOption::VALUE_NONE, 'Update plugin files only.'],
|
||||
];
|
||||
}
|
||||
}
|
||||
560
modules/system/console/WinterUtil.php
Normal file
560
modules/system/console/WinterUtil.php
Normal file
@@ -0,0 +1,560 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use Lang;
|
||||
use File;
|
||||
use Config;
|
||||
use DirectoryIterator;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File as Filesystem;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use System\Classes\UpdateManager;
|
||||
use System\Classes\CombineAssets;
|
||||
use System\Models\Parameter;
|
||||
use System\Models\File as FileModel;
|
||||
use Winter\Storm\Filesystem\Zip;
|
||||
use Winter\Storm\Network\Http as NetworkHttp;
|
||||
use Winter\Storm\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Console command for other utility commands.
|
||||
*
|
||||
* This provides functionality that doesn't quite deserve its own dedicated
|
||||
* console class. It is used mostly developer tools and maintenance tasks.
|
||||
*
|
||||
* Currently supported commands:
|
||||
*
|
||||
* - purge resized: Deletes all files in the resized directory.
|
||||
* - purge thumbs: Deletes all thumbnail files in the uploads directory.
|
||||
* - purge uploads: Deletes files in the uploads directory that do not exist in the "system_files" table.
|
||||
* - git pull: Perform "git pull" on all plugins and themes.
|
||||
* - compile assets: Compile registered Language, LESS and JS files.
|
||||
* - compile js: Compile registered JS files only.
|
||||
* - compile less: Compile registered LESS files only.
|
||||
* - compile scss: Compile registered SCSS files only.
|
||||
* - compile lang: Compile registered Language files only.
|
||||
* - set project --projectId=<id>: Set the projectId for this winter instance.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class WinterUtil extends Command
|
||||
{
|
||||
use \Illuminate\Console\ConfirmableTrait;
|
||||
|
||||
/**
|
||||
* The console command name.
|
||||
*/
|
||||
protected $name = 'winter:util';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Utility commands for Winter';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Register aliases for backwards compatibility with October
|
||||
$this->setAliases(['october:util']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$command = implode(' ', (array) $this->argument('name'));
|
||||
$method = 'util'.studly_case($command);
|
||||
|
||||
$methods = preg_grep('/^util/', get_class_methods(get_called_class()));
|
||||
$list = array_map(function ($item) {
|
||||
return "winter:".snake_case($item, " ");
|
||||
}, $methods);
|
||||
|
||||
if (!$this->argument('name')) {
|
||||
$message = 'There are no commands defined in the "util" namespace.';
|
||||
if (1 == count($list)) {
|
||||
$message .= "\n\nDid you mean this?\n ";
|
||||
} else {
|
||||
$message .= "\n\nDid you mean one of these?\n ";
|
||||
}
|
||||
|
||||
$message .= implode("\n ", $list);
|
||||
throw new \InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
if (!method_exists($this, $method)) {
|
||||
$this->error(sprintf('Utility command "%s" does not exist!', $command));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->$method();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console command arguments.
|
||||
* @return array
|
||||
*/
|
||||
protected function getArguments()
|
||||
{
|
||||
return [
|
||||
['name', InputArgument::IS_ARRAY, 'The utility command to perform, For more info, see "https://wintercms.com/docs/v1.2/docs/console/utilities#utility-runner".'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console command options.
|
||||
* @return array
|
||||
*/
|
||||
protected function getOptions()
|
||||
{
|
||||
return [
|
||||
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production.'],
|
||||
['debug', null, InputOption::VALUE_NONE, 'Run the operation in debug / development mode.'],
|
||||
['projectId', null, InputOption::VALUE_REQUIRED, 'Specify a projectId for set project'],
|
||||
['missing-files', null, InputOption::VALUE_NONE, 'Purge system_files records for missing storage files'],
|
||||
];
|
||||
}
|
||||
|
||||
//
|
||||
// Utilties
|
||||
//
|
||||
|
||||
protected function utilSetBuild()
|
||||
{
|
||||
$this->comment('NOTE: This command is now deprecated. Please use "php artisan winter:version" instead.');
|
||||
$this->comment('');
|
||||
|
||||
return $this->call('winter:version');
|
||||
}
|
||||
|
||||
protected function utilCompileJs()
|
||||
{
|
||||
$this->utilCompileAssets('js');
|
||||
}
|
||||
|
||||
protected function utilCompileLess()
|
||||
{
|
||||
$this->utilCompileAssets('less');
|
||||
}
|
||||
|
||||
protected function utilCompileScss()
|
||||
{
|
||||
$this->utilCompileAssets('scss');
|
||||
}
|
||||
|
||||
protected function utilCompileAssets($type = null)
|
||||
{
|
||||
// Download Font Awesome icons if they are missing and LESS files are being compiled
|
||||
if (
|
||||
(
|
||||
!is_dir(base_path('node_modules/@fortawesome/fontawesome-free'))
|
||||
|| !is_file(base_path('node_modules/@fortawesome/fontawesome-free/less/_variables.less'))
|
||||
)
|
||||
&& ($type === 'less' || $type === null)
|
||||
) {
|
||||
$this->comment('Downloading Font Awesome icons...');
|
||||
|
||||
$releases = Http::get('https://api.github.com/repos/FortAwesome/Font-Awesome/releases/191042913', function (NetworkHttp $http) {
|
||||
$http->header('Accept', 'application/json');
|
||||
$http->header('User-Agent', 'Winter CMS');
|
||||
});
|
||||
|
||||
if (!$releases->ok) {
|
||||
$this->error('Failed to download Font Awesome icons');
|
||||
return;
|
||||
}
|
||||
|
||||
$releases = json_decode($releases->body, true);
|
||||
$releaseName = null;
|
||||
$zipUrl = null;
|
||||
|
||||
foreach ($releases['assets'] as $asset) {
|
||||
if (
|
||||
str_starts_with($asset['name'], 'fontawesome-free-')
|
||||
&& str_ends_with($asset['name'], 'web.zip')
|
||||
) {
|
||||
$zipUrl = $asset['browser_download_url'];
|
||||
$releaseName = pathinfo($asset['name'], PATHINFO_FILENAME);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($zipUrl)) {
|
||||
$this->error('Failed to find Font Awesome icons download URL');
|
||||
return;
|
||||
}
|
||||
|
||||
Http::get($zipUrl, function (NetworkHttp $http) {
|
||||
$http->header('User-Agent', 'Winter CMS');
|
||||
$http->toFile(storage_path('temp/fontawesome.zip'));
|
||||
});
|
||||
|
||||
// Extract Font Awesome files
|
||||
if (is_dir(storage_path('temp/fontawesome'))) {
|
||||
$this->rimraf(storage_path('temp/fontawesome'));
|
||||
}
|
||||
|
||||
Zip::extract(storage_path('temp/fontawesome.zip'), storage_path('temp/fontawesome'));
|
||||
Filesystem::delete(storage_path('temp/fontawesome.zip'));
|
||||
|
||||
// Move Font Awesome LESS and font files into place
|
||||
Filesystem::makeDirectory(base_path('node_modules/@fortawesome/fontawesome-free/less'), 0755, true);
|
||||
Filesystem::moveDirectory(storage_path('temp/fontawesome/' . $releaseName . '/less'), base_path('node_modules/@fortawesome/fontawesome-free/less'));
|
||||
Filesystem::copyDirectory(storage_path('temp/fontawesome/' . $releaseName . '/webfonts'), base_path('modules/system/assets/ui/font'));
|
||||
|
||||
// Remove remaining files
|
||||
$this->rimraf(storage_path('temp/fontawesome'));
|
||||
}
|
||||
|
||||
$this->comment('Compiling registered asset bundles...');
|
||||
|
||||
Config::set('cms.enableAssetMinify', !$this->option('debug'));
|
||||
$combiner = CombineAssets::instance();
|
||||
$bundles = $combiner->getBundles($type);
|
||||
|
||||
if (!$bundles) {
|
||||
$this->comment('Nothing to compile!');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type) {
|
||||
$bundles = [$bundles];
|
||||
}
|
||||
|
||||
foreach ($bundles as $bundleType) {
|
||||
foreach ($bundleType as $destination => $assets) {
|
||||
$destination = File::symbolizePath($destination);
|
||||
$publicDest = File::localToPublic(realpath(dirname($destination))) . '/' . basename($destination);
|
||||
|
||||
$combiner->combineToFile($assets, $destination);
|
||||
$shortAssets = implode(', ', array_map('basename', $assets));
|
||||
$this->comment($shortAssets);
|
||||
$this->comment(sprintf(' -> %s', $publicDest));
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === null) {
|
||||
$this->utilCompileLang();
|
||||
}
|
||||
}
|
||||
|
||||
protected function utilCompileLang()
|
||||
{
|
||||
if (!$locales = Lang::get('system::lang.locale')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->comment('Compiling client-side language files...');
|
||||
|
||||
$locales = array_keys($locales);
|
||||
$stub = base_path() . '/modules/system/assets/js/lang/lang.stub';
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
/*
|
||||
* Generate messages
|
||||
*/
|
||||
$fallbackPath = base_path() . '/modules/system/lang/en/client.php';
|
||||
$srcPath = base_path() . '/modules/system/lang/'.$locale.'/client.php';
|
||||
|
||||
$messages = require $fallbackPath;
|
||||
|
||||
if (File::isFile($srcPath) && $fallbackPath != $srcPath) {
|
||||
$messages = array_replace_recursive($messages, require $srcPath);
|
||||
}
|
||||
|
||||
/*
|
||||
* Load possible replacements from /lang
|
||||
*/
|
||||
$overrides = [];
|
||||
$parentOverrides = [];
|
||||
|
||||
$overridePath = base_path() . '/lang/'.$locale.'/system/client.php';
|
||||
if (File::isFile($overridePath)) {
|
||||
$overrides = require $overridePath;
|
||||
}
|
||||
|
||||
if (str_contains($locale, '-')) {
|
||||
list($parentLocale, $country) = explode('-', $locale);
|
||||
|
||||
$parentOverridePath = base_path() . '/lang/'.$parentLocale.'/system/client.php';
|
||||
if (File::isFile($parentOverridePath)) {
|
||||
$parentOverrides = require $parentOverridePath;
|
||||
}
|
||||
}
|
||||
|
||||
$messages = array_replace_recursive($messages, $parentOverrides, $overrides);
|
||||
|
||||
/*
|
||||
* Compile from stub and save file
|
||||
*/
|
||||
$destPath = base_path() . '/modules/system/assets/js/lang/lang.'.$locale.'.js';
|
||||
|
||||
$contents = str_replace(
|
||||
['{{locale}}', '{{messages}}'],
|
||||
[$locale, json_encode($messages)],
|
||||
File::get($stub)
|
||||
);
|
||||
|
||||
/*
|
||||
* Include the moment localization data
|
||||
*/
|
||||
$momentPath = base_path() . '/modules/system/assets/ui/vendor/moment/locale/'.$locale.'.js';
|
||||
if (File::exists($momentPath)) {
|
||||
$contents .= PHP_EOL.PHP_EOL.File::get($momentPath).PHP_EOL;
|
||||
}
|
||||
|
||||
File::put($destPath, $contents);
|
||||
|
||||
/*
|
||||
* Output notes
|
||||
*/
|
||||
$publicDest = File::localToPublic(realpath(dirname($destPath))) . '/' . basename($destPath);
|
||||
|
||||
$this->comment($locale.'/'.basename($srcPath));
|
||||
$this->comment(sprintf(' -> %s', $publicDest));
|
||||
}
|
||||
}
|
||||
|
||||
protected function utilPurgeResized()
|
||||
{
|
||||
if (!$this->confirmToProceed('This will PERMANENTLY DELETE all files in the resized directory.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resizedDisk = Config::get('cms.storage.resized.disk', 'local');
|
||||
$resizedFolder = Config::get('cms.storage.resized.folder', 'resized');
|
||||
|
||||
$totalCount = count(Storage::disk($resizedDisk)->allFiles($resizedFolder));
|
||||
|
||||
foreach (Storage::disk($resizedDisk)->directories($resizedFolder, false) as $directory) {
|
||||
Storage::disk($resizedDisk)->deleteDirectory($directory);
|
||||
}
|
||||
|
||||
if ($totalCount > 0) {
|
||||
$this->comment(sprintf('Successfully deleted %d file(s)', $totalCount));
|
||||
} else {
|
||||
$this->comment('No files found to purge.');
|
||||
}
|
||||
}
|
||||
|
||||
protected function utilPurgeThumbs()
|
||||
{
|
||||
if (!$this->confirmToProceed('This will PERMANENTLY DELETE all thumbs in the uploads directory.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$totalCount = 0;
|
||||
$uploadsPath = Config::get('filesystems.disks.local.root', storage_path('app'));
|
||||
$uploadsPath .= '/uploads';
|
||||
|
||||
/*
|
||||
* Recursive function to scan the directory for files beginning
|
||||
* with "thumb_" and repeat itself on directories.
|
||||
*/
|
||||
$purgeFunc = function ($targetDir) use (&$purgeFunc, &$totalCount) {
|
||||
if ($files = File::glob($targetDir.'/thumb_*')) {
|
||||
foreach ($files as $file) {
|
||||
$this->info('Purged: '. basename($file));
|
||||
$totalCount++;
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
if ($dirs = File::directories($targetDir)) {
|
||||
foreach ($dirs as $dir) {
|
||||
$purgeFunc($dir);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$purgeFunc($uploadsPath);
|
||||
|
||||
if ($totalCount > 0) {
|
||||
$this->comment(sprintf('Successfully deleted %s thumbs', $totalCount));
|
||||
}
|
||||
else {
|
||||
$this->comment('No thumbs found to delete');
|
||||
}
|
||||
}
|
||||
|
||||
protected function utilPurgeUploads()
|
||||
{
|
||||
if (!$this->confirmToProceed('This will PERMANENTLY DELETE files in the uploads directory that do not exist in the "system_files" table.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uploadsDisk = Config::get('cms.storage.uploads.disk', 'local');
|
||||
|
||||
$uploadsFolder = Config::get('cms.storage.uploads.folder', 'uploads');
|
||||
|
||||
$totalCount = 0;
|
||||
|
||||
$validFiles = FileModel::pluck('disk_name')->all();
|
||||
|
||||
foreach (Storage::disk($uploadsDisk)->allFiles($uploadsFolder) as $filePath) {
|
||||
$fileName = basename($filePath);
|
||||
|
||||
// Skip .gitignore files
|
||||
if ($fileName === '.gitignore') {
|
||||
continue;
|
||||
}
|
||||
// Purge invalid files
|
||||
if (!in_array($fileName, $validFiles)) {
|
||||
// Purge invalid upload file
|
||||
Storage::disk($uploadsDisk)->delete($filePath);
|
||||
$this->info('Purged: ' . $filePath);
|
||||
// Purge parent directories
|
||||
$currentDir = dirname($filePath);
|
||||
while ($currentDir !== $uploadsFolder) {
|
||||
// Get parent directory children
|
||||
$children = Storage::disk($uploadsDisk)->allFiles($currentDir);
|
||||
// Parent directory is empty
|
||||
if (count($children) === 0) {
|
||||
Storage::disk($uploadsDisk)->deleteDirectory($currentDir);
|
||||
$this->info('Removed folder: ' . $currentDir);
|
||||
} else {
|
||||
// Parent directory is not empty
|
||||
// stop the iteration
|
||||
break;
|
||||
}
|
||||
$currentDir = dirname($currentDir);
|
||||
}
|
||||
$totalCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($totalCount > 0) {
|
||||
$this->comment(sprintf('Successfully deleted %d invalid file(s), leaving %d valid files', $totalCount, count($validFiles)));
|
||||
} else {
|
||||
$this->comment('No files found to purge.');
|
||||
}
|
||||
}
|
||||
|
||||
protected function utilPurgeOrphans()
|
||||
{
|
||||
if (!$this->confirmToProceed('This will PERMANENTLY DELETE files in "system_files" that do not belong to any other model.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isDebug = $this->option('debug');
|
||||
$orphanedFiles = 0;
|
||||
$isLocalStorage = Config::get('cms.storage.uploads.disk', 'local') === 'local';
|
||||
|
||||
$files = FileModel::whereDoesntHaveMorph('attachment', '*')
|
||||
->orWhereNull('attachment_id')
|
||||
->orWhereNull('attachment_type')
|
||||
->get();
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (!$isDebug) {
|
||||
$file->delete();
|
||||
}
|
||||
$orphanedFiles += 1;
|
||||
}
|
||||
|
||||
if ($this->option('missing-files') && $isLocalStorage) {
|
||||
foreach (FileModel::all() as $file) {
|
||||
if (!File::exists($file->getLocalPath())) {
|
||||
if (!$isDebug) {
|
||||
$file->delete();
|
||||
}
|
||||
$orphanedFiles += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($orphanedFiles > 0) {
|
||||
$this->comment(sprintf('Successfully deleted %d orphaned record(s).', $orphanedFiles));
|
||||
} else {
|
||||
$this->comment('No records to purge.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This command requires the git binary to be installed.
|
||||
*/
|
||||
protected function utilGitPull()
|
||||
{
|
||||
foreach (File::directories(plugins_path()) as $authorDir) {
|
||||
foreach (File::directories($authorDir) as $pluginDir) {
|
||||
if (!File::exists($pluginDir.'/.git')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exec = 'cd ' . $pluginDir . ' && ';
|
||||
$exec .= 'git pull 2>&1';
|
||||
echo 'Updating plugin: '. basename(dirname($pluginDir)) .'.'. basename($pluginDir) . PHP_EOL;
|
||||
echo shell_exec($exec);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (File::directories(themes_path()) as $themeDir) {
|
||||
if (!File::exists($themeDir.'/.git')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exec = 'cd ' . $themeDir . ' && ';
|
||||
$exec .= 'git pull 2>&1';
|
||||
echo 'Updating theme: '. basename($themeDir) . PHP_EOL;
|
||||
echo shell_exec($exec);
|
||||
}
|
||||
}
|
||||
|
||||
protected function utilSetProject()
|
||||
{
|
||||
$projectId = $this->option('projectId');
|
||||
|
||||
if (empty($projectId)) {
|
||||
$this->error("No projectId defined, use --projectId=<id> to set a projectId");
|
||||
return;
|
||||
}
|
||||
|
||||
$manager = UpdateManager::instance();
|
||||
$result = $manager->requestProjectDetails($projectId);
|
||||
|
||||
Parameter::set([
|
||||
'system::project.id' => $projectId,
|
||||
'system::project.name' => $result['name'],
|
||||
'system::project.owner' => $result['owner'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP-based "rm -rf" command.
|
||||
*
|
||||
* Recursively removes a directory and all files and subdirectories within.
|
||||
*/
|
||||
protected function rimraf(string $path): void
|
||||
{
|
||||
if (!file_exists($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
return;
|
||||
}
|
||||
|
||||
$dir = new DirectoryIterator($path);
|
||||
|
||||
foreach ($dir as $item) {
|
||||
if ($item->isDot()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($item->isDir()) {
|
||||
$this->rimraf($item->getPathname());
|
||||
}
|
||||
|
||||
@unlink($item->getPathname());
|
||||
}
|
||||
|
||||
@rmdir($path);
|
||||
}
|
||||
}
|
||||
110
modules/system/console/WinterVersion.php
Normal file
110
modules/system/console/WinterVersion.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php namespace System\Console;
|
||||
|
||||
use System\Classes\UpdateManager;
|
||||
|
||||
/**
|
||||
* Detects the version of Winter CMS installed.
|
||||
*
|
||||
* This checks against a central manifest on Winter CMS's GitHub account to determine the version. If any files have
|
||||
* been modified, this will be indicated when detecting the version.
|
||||
*
|
||||
* To get a list of modified files, simply add the "--changes" parameter.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Ben Thomson
|
||||
* @author Winter CMS
|
||||
*/
|
||||
class WinterVersion extends \Winter\Storm\Console\Command
|
||||
{
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Detects the build number (version) of this Winter CMS instance.';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'winter:version
|
||||
{--changes : Include the list of changes between this install and the expected files for the detected build.}
|
||||
{--o|only-version : Return only the build version number.}
|
||||
';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Register aliases for backwards compatibility with October
|
||||
$this->setAliases(['october:version']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->option('only-version')) {
|
||||
$this->comment('*** Detecting Winter CMS build...');
|
||||
}
|
||||
|
||||
if (!$this->laravel->hasDatabase()) {
|
||||
$build = UpdateManager::instance()->getBuildNumberManually($this->option('changes'));
|
||||
|
||||
// Skip setting the build number if no database is detected to set it within
|
||||
if (!$this->option('only-version')) {
|
||||
$this->comment('*** No database detected - skipping setting the build number.');
|
||||
}
|
||||
} else {
|
||||
$build = UpdateManager::instance()->setBuildNumberManually($this->option('changes'));
|
||||
}
|
||||
|
||||
if ($this->option('only-version')) {
|
||||
$this->line($build['build']);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!$build['confident']) {
|
||||
$this->warn('*** We could not accurately determine your Winter CMS build due to the number of modifications. The closest detected build is Winter CMS build ' . $build['build'] . '.');
|
||||
} elseif ($build['modified']) {
|
||||
$this->info('*** Detected a modified version of Winter CMS build ' . $build['build'] . '.');
|
||||
} else {
|
||||
$this->info('*** Detected Winter CMS build ' . $build['build'] . '.');
|
||||
}
|
||||
|
||||
if (!empty($build['changes']) && $this->option('changes')) {
|
||||
$this->line('');
|
||||
$this->comment('We have detected the following modifications:');
|
||||
|
||||
if (count($build['changes']['added'] ?? [])) {
|
||||
$this->line('');
|
||||
$this->info('Files added:');
|
||||
|
||||
foreach (array_keys($build['changes']['added']) as $file) {
|
||||
$this->line(' - ' . $file);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($build['changes']['modified'] ?? [])) {
|
||||
$this->line('');
|
||||
$this->info('Files modified:');
|
||||
|
||||
foreach (array_keys($build['changes']['modified']) as $file) {
|
||||
$this->line(' - ' . $file);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($build['changes']['removed'] ?? [])) {
|
||||
$this->line('');
|
||||
$this->info('Files removed:');
|
||||
|
||||
foreach ($build['changes']['removed'] as $file) {
|
||||
$this->line(' - ' . $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
272
modules/system/console/asset/AssetCompile.php
Normal file
272
modules/system/console/asset/AssetCompile.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
use System\Classes\Asset\PackageManager;
|
||||
use System\Classes\Asset\PackageJson;
|
||||
use Winter\Storm\Console\Command;
|
||||
use Winter\Storm\Support\Facades\File;
|
||||
use Winter\Storm\Support\Str;
|
||||
|
||||
abstract class AssetCompile extends Command
|
||||
{
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Mix and compile assets';
|
||||
|
||||
/**
|
||||
* PackageJson object holding the contents of the active package.json
|
||||
*/
|
||||
protected PackageJson $packageJson;
|
||||
|
||||
/**
|
||||
* Name of config file i.e. mix.webpack.js, vite.config.js
|
||||
*/
|
||||
protected string $configFile;
|
||||
|
||||
/**
|
||||
* File path being watched, used for cleanup by mix:watch
|
||||
*/
|
||||
protected string $watchingFilePath;
|
||||
|
||||
public function compileHandle(string $type): int
|
||||
{
|
||||
// Exit early if node_modules isn't available yet
|
||||
if (!File::exists(base_path('node_modules'))) {
|
||||
$this->error(sprintf(
|
||||
'The Node dependencies are not available, try running %s:install first.',
|
||||
$type
|
||||
));
|
||||
return 1;
|
||||
}
|
||||
|
||||
$compilableAssets = PackageManager::instance();
|
||||
$compilableAssets->fireCallbacks();
|
||||
|
||||
$registeredPackages = $compilableAssets->getPackages($type);
|
||||
$requestedPackages = $this->option('package') ?: [];
|
||||
|
||||
// Calling commands in unit tests can cause the option casting to not work correctly,
|
||||
// ensure that the option value is always an array
|
||||
if (is_string($requestedPackages)) {
|
||||
$requestedPackages = [$requestedPackages];
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
// Get an updated list of packages including any newly added packages
|
||||
$registeredPackages = $compilableAssets->getPackages($type);
|
||||
|
||||
// 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 compilation.');
|
||||
return 1;
|
||||
} else {
|
||||
$this->info('No packages registered for mixing.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
$exits = [];
|
||||
foreach ($registeredPackages as $name => $package) {
|
||||
$relativeMixJsPath = $package['config'];
|
||||
if (!$this->isPackageWithinWorkspace($relativeMixJsPath)) {
|
||||
$this->error(sprintf(
|
||||
'Unable to compile "%s", %s was not found in the package.json\'s workspaces.packages property.'
|
||||
. ' Try running %s:install first.',
|
||||
$name,
|
||||
$relativeMixJsPath,
|
||||
$type
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->option('silent')) {
|
||||
$this->info(sprintf('Compiling package "%s"', $name));
|
||||
}
|
||||
|
||||
$exitCode = $this->executeProcess(base_path($relativeMixJsPath));
|
||||
|
||||
if ($exitCode > 0) {
|
||||
$this->error(sprintf('Unable to compile package "%s"', $name));
|
||||
}
|
||||
|
||||
if ($this->option('stop-on-error') && $exitCode > 0) {
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
$exits[] = $exitCode;
|
||||
}
|
||||
|
||||
return (int) !empty(array_filter($exits));
|
||||
}
|
||||
|
||||
public function watchHandle(string $type): int
|
||||
{
|
||||
$compilableAssets = PackageManager::instance();
|
||||
$compilableAssets->fireCallbacks();
|
||||
|
||||
$packages = $compilableAssets->getPackages($type);
|
||||
$name = $this->argument('package');
|
||||
$nameLower = strtolower($name);
|
||||
|
||||
if (!in_array($nameLower, array_keys($packages))) {
|
||||
$this->error(
|
||||
sprintf('Package "%s" is not a registered package.', $name)
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
$package = $packages[$nameLower];
|
||||
|
||||
$relativeConfigPath = $package['config'];
|
||||
if (!$this->isPackageWithinWorkspace($relativeConfigPath)) {
|
||||
$this->error(sprintf(
|
||||
'Unable to watch "%s", %s was not found in the package.json\'s workspaces.packages property. Try running %s:install first.',
|
||||
$name,
|
||||
$relativeConfigPath,
|
||||
$type
|
||||
));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!$this->option('silent')) {
|
||||
$this->info(sprintf('Watching package "%s" for changes', $name));
|
||||
}
|
||||
|
||||
$this->watchingFilePath = $relativeConfigPath;
|
||||
|
||||
if ($this->executeProcess(base_path($relativeConfigPath)) !== 0) {
|
||||
$this->error(sprintf('Unable to compile package "%s"', $name));
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package path for the provided winter.mix.js file
|
||||
*/
|
||||
protected function getPackagePath(string $path): string
|
||||
{
|
||||
return pathinfo($path, PATHINFO_DIRNAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the mix.webpack.js file for the provided winter.mix.js file
|
||||
*/
|
||||
protected function getJsConfigPath(string $path): string
|
||||
{
|
||||
return $this->getPackagePath($path) . DIRECTORY_SEPARATOR . $this->configFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Mix is able to compile the provided winter.mix.js file
|
||||
*/
|
||||
protected function isPackageWithinWorkspace(string $mixJsPath): bool
|
||||
{
|
||||
if (!isset($this->packageJson)) {
|
||||
// Load the main package.json for the project
|
||||
$this->packageJson = $this->getNpmPackageManifest();
|
||||
}
|
||||
|
||||
return $this->packageJson->hasWorkspace(
|
||||
Str::replace(DIRECTORY_SEPARATOR, '/', $this->getPackagePath($mixJsPath))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the package.json file for the project, path configurable with the
|
||||
* `--manifest` option
|
||||
*/
|
||||
protected function getNpmPackageManifest(): PackageJson
|
||||
{
|
||||
return new PackageJson(base_path($this->option('manifest') ?? 'package.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the mix command against the provided package
|
||||
*/
|
||||
protected function executeProcess(string $configPath): int
|
||||
{
|
||||
$this->beforeExecution($configPath);
|
||||
$command = $this->createCommand($configPath);
|
||||
$commandEnv = $this->createCommandEnv($configPath);
|
||||
|
||||
$process = new Process(
|
||||
$command,
|
||||
$this->getPackagePath($configPath),
|
||||
[
|
||||
'NODE_ENV' => $this->option('production', false) ? 'production' : 'development',
|
||||
...$commandEnv
|
||||
],
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
if (!$this->option('disable-tty')) {
|
||||
try {
|
||||
$process->setTty(true);
|
||||
} catch (\Throwable $e) {
|
||||
// This will fail on unsupported systems
|
||||
}
|
||||
}
|
||||
|
||||
$exitCode = $process->run(function ($status, $stdout) {
|
||||
if (!$this->option('silent')) {
|
||||
$this->getOutput()->write($stdout);
|
||||
}
|
||||
});
|
||||
|
||||
$this->afterExecution($configPath);
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ran before dispatching the compile process, use for setting up
|
||||
*/
|
||||
protected function beforeExecution(string $configPath): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Ran after dispatching the compile process, use for tearing down
|
||||
*/
|
||||
protected function afterExecution(string $configPath): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the command array to create a Process object with
|
||||
*/
|
||||
abstract protected function createCommand(string $configPath): array;
|
||||
|
||||
/**
|
||||
* Return values to append to the command env
|
||||
*/
|
||||
protected function createCommandEnv(string $configPath): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
263
modules/system/console/asset/AssetCreate.php
Normal file
263
modules/system/console/asset/AssetCreate.php
Normal file
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset;
|
||||
|
||||
use Closure;
|
||||
use Cms\Classes\Theme;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use System\Classes\Asset\BundleManager;
|
||||
use System\Classes\Asset\PackageJson;
|
||||
use System\Classes\Asset\PackageManager;
|
||||
use System\Classes\PluginManager;
|
||||
use Winter\Storm\Console\Command;
|
||||
use Winter\Storm\Support\Facades\File;
|
||||
|
||||
abstract class AssetCreate extends Command
|
||||
{
|
||||
protected const TYPE_MODULE = 'module';
|
||||
protected const TYPE_PLUGIN = 'plugin';
|
||||
protected const TYPE_THEME = 'theme';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Creates the compiler configuration files for the provided package and optionally installs the necessary dependencies for any selected asset bundles.';
|
||||
|
||||
/**
|
||||
* Local cache of fixture path
|
||||
*/
|
||||
private string $fixturePath;
|
||||
|
||||
/**
|
||||
* The type of compilable to configure
|
||||
*/
|
||||
protected string $assetType;
|
||||
|
||||
/**
|
||||
* The name of the config file
|
||||
*/
|
||||
protected string $configFile;
|
||||
|
||||
/**
|
||||
* Dynamically generate options for all available bundles
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
foreach (BundleManager::instance()->getBundles() as $bundle) {
|
||||
$this->addOption($bundle, null, InputOption::VALUE_NONE, 'Create ' . $bundle . ' configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$package = $this->argument('packageName');
|
||||
|
||||
$this->fixturePath = __DIR__ . '/fixtures/config';
|
||||
|
||||
$compilableAssets = PackageManager::instance();
|
||||
$compilableAssets->fireCallbacks();
|
||||
|
||||
$packages = $compilableAssets->getPackages($this->assetType, true);
|
||||
|
||||
if (
|
||||
// We have the package already
|
||||
isset($packages[$package])
|
||||
// If the user has requested `force` & `no-interaction` then we do not ask for confirmation and continue
|
||||
&& !($this->option('force') && $this->option('no-interaction'))
|
||||
// If the user has forced but with interaction, then we do not ask for confirmation, else we do
|
||||
&& !($this->option('force') || $this->confirm('Package `' . $package . '` has already been configured, are you sure you wish to continue?'))
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
[$path, $type] = $this->getPackagePathType($package);
|
||||
|
||||
if (is_null($path) || is_null($type)) {
|
||||
$this->error('Package `' . $package . '` could not be resolved');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$packageJson = new PackageJson($path . '/package.json');
|
||||
if (!$packageJson->getName()) {
|
||||
$packageJson->setName(strtolower(str_replace('.', '-', $package)));
|
||||
}
|
||||
|
||||
$this->installConfigs($packageJson, $package, $type, $path);
|
||||
|
||||
$verb = File::exists($packageJson->getPath()) ? 'updated' : 'generated';
|
||||
$packageJson->save();
|
||||
|
||||
if (!$this->option('silent')) {
|
||||
$this->warn("File $verb: " . str_after($packageJson->getPath(), base_path()));
|
||||
$this->info(ucfirst($this->assetType) . ' configuration complete.');
|
||||
}
|
||||
|
||||
$this->afterExecution();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the path and type of the package by name
|
||||
*/
|
||||
protected function getPackagePathType(string $package): array
|
||||
{
|
||||
if (str_starts_with($package, 'theme-')) {
|
||||
if (($theme = Theme::load(str_after($package, 'theme-'))) && File::exists($theme->getPath())) {
|
||||
return [$theme->getPath(), static::TYPE_THEME];
|
||||
}
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if (str_starts_with($package, 'module-')) {
|
||||
if (
|
||||
($modulePath = base_path('modules') . '/' . str_after($package, 'module-'))
|
||||
&& File::exists($modulePath)
|
||||
) {
|
||||
return [$modulePath, static::TYPE_MODULE];
|
||||
}
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if ($plugin = PluginManager::instance()->findByIdentifier($package)) {
|
||||
return [$plugin->getPluginPath(), static::TYPE_PLUGIN];
|
||||
}
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write out config files based on assetType and the requested options
|
||||
*/
|
||||
protected function installConfigs(
|
||||
PackageJson $packageJson,
|
||||
string $packageName,
|
||||
string $packageType,
|
||||
string $packagePath
|
||||
): void {
|
||||
// Normalize package name
|
||||
$packageName = $this->makePackageName($packageName);
|
||||
// Bind the bundleManager instance
|
||||
$bundleManager = BundleManager::instance();
|
||||
|
||||
// Get the default config
|
||||
$config = $this->getFixture(
|
||||
$this->assetType . '/' . pathinfo($this->configFile, PATHINFO_BASENAME) . '.fixture'
|
||||
);
|
||||
|
||||
// For each bundle offered by node packages
|
||||
foreach ($bundleManager->getBundles() as $bundle) {
|
||||
// If the bundle was not selected exit
|
||||
if (!$this->option($bundle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Require all packages specified by the bundle
|
||||
foreach ($bundleManager->getBundlePackages($bundle, $this->assetType) as $dependency => $version) {
|
||||
$packageJson->addDependency($dependency, $version, dev: true);
|
||||
}
|
||||
|
||||
// Fire any setup handlers required
|
||||
$setupHandler = $bundleManager->getSetupHandler($bundle);
|
||||
if ($setupHandler) {
|
||||
Closure::bind($setupHandler, $this)->call($this, $packagePath, $packageType);
|
||||
}
|
||||
|
||||
// Loop through all the scaffold handlers to build configs / stubs
|
||||
$scaffoldHandler = $bundleManager->getScaffoldHandler($bundle);
|
||||
if ($scaffoldHandler) {
|
||||
// Generate the config
|
||||
$config = Closure::bind($scaffoldHandler, $this)->call($this, $config, $this->assetType);
|
||||
// Generate stub files if required
|
||||
if (!$this->option('no-stubs')) {
|
||||
$css = Closure::bind($scaffoldHandler, $this)->call($this, $css ?? '', 'css');
|
||||
$js = Closure::bind($scaffoldHandler, $this)->call($this, $js ?? '', 'js');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create stub files if required
|
||||
if (!$this->option('no-stubs')) {
|
||||
foreach (['css', 'js'] as $asset) {
|
||||
// Create asset dist dir so laravel-vite-plugin doesn't complain
|
||||
if (!File::exists($packagePath . '/assets/dist')) {
|
||||
File::makeDirectory($packagePath . '/assets/dist/', recursive: true);
|
||||
}
|
||||
// Create asset src dirs for stubs
|
||||
if (!File::exists($packagePath . '/assets/src/' . $asset)) {
|
||||
File::makeDirectory($packagePath . '/assets/src/' . $asset, recursive: true);
|
||||
}
|
||||
$this->writeFile(
|
||||
sprintf('%1$s/assets/src/%2$s/%3$s.%2$s', $packagePath, $asset, $packageName),
|
||||
$$asset ?? null ? $$asset : $this->getFixture(sprintf('%1$s/default.%1$s.fixture', $asset))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Write out the config file
|
||||
$this->writeFile($packagePath . '/' . $this->configFile, str_replace(
|
||||
'{{packageName}}',
|
||||
$packageName,
|
||||
$config
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a file but ask for conformation before overwriting
|
||||
*/
|
||||
protected function writeFile(string $path, string $content): int
|
||||
{
|
||||
if (
|
||||
// If forced, ignore file existing and overwrite
|
||||
(!$this->option('force') && File::exists($path))
|
||||
&& (
|
||||
// If no interaction requested, then skip the confirm check and return
|
||||
$this->option('no-interaction')
|
||||
// else ask the user if they want overwriting
|
||||
|| !$this->confirm(sprintf('%s already exists, overwrite?', basename($path)))
|
||||
)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
File::ensureDirectoryExists(pathinfo($path, PATHINFO_DIRNAME));
|
||||
$result = File::put($path, $content);
|
||||
|
||||
if (!$this->option('silent')) {
|
||||
$this->warn('File generated: ' . str_after($path, base_path()));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for loading fixtures from the default library
|
||||
*/
|
||||
protected function getFixture(string $path): string
|
||||
{
|
||||
return File::get($this->fixturePath . '/' . $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the user supplied package name into a consistent internal format
|
||||
*/
|
||||
protected function makePackageName(string $package): string
|
||||
{
|
||||
return strtolower(str_replace('.', '-', $package));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ran after configuration is complete, use for tearing down / reporting
|
||||
*/
|
||||
protected function afterExecution(): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
409
modules/system/console/asset/AssetInstall.php
Normal file
409
modules/system/console/asset/AssetInstall.php
Normal file
@@ -0,0 +1,409 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset;
|
||||
|
||||
use Cms\Classes\Theme;
|
||||
use Symfony\Component\Process\Exception\ProcessSignaledException;
|
||||
use Symfony\Component\Process\Process;
|
||||
use System\Classes\Asset\PackageJson;
|
||||
use System\Classes\Asset\PackageManager;
|
||||
use System\Classes\PluginManager;
|
||||
use Winter\Storm\Console\Command;
|
||||
use Winter\Storm\Exception\SystemException;
|
||||
use Winter\Storm\Support\Facades\Config;
|
||||
use Winter\Storm\Support\Facades\File;
|
||||
use Winter\Storm\Support\Str;
|
||||
|
||||
abstract class AssetInstall extends Command
|
||||
{
|
||||
/**
|
||||
* The path to the "npm" executable.
|
||||
*/
|
||||
protected string $npmPath = 'npm';
|
||||
|
||||
/**
|
||||
* Terms used in messages.
|
||||
*/
|
||||
protected array $terms = [
|
||||
'complete' => 'install',
|
||||
'completed' => 'installed',
|
||||
];
|
||||
|
||||
/**
|
||||
* Path to package json, if null use base_path.
|
||||
*/
|
||||
protected ?string $packageJsonPath = null;
|
||||
|
||||
/**
|
||||
* Type of asset to be installed, @see PackageManager
|
||||
*/
|
||||
protected string $assetType;
|
||||
|
||||
/**
|
||||
* The asset config file
|
||||
*/
|
||||
protected string $configFile;
|
||||
|
||||
/**
|
||||
* The required dependencies for this compiler
|
||||
*/
|
||||
protected array $requiredDependencies = [];
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if ($npmPath = $this->option('npm')) {
|
||||
if (!File::exists($npmPath) || !is_executable($npmPath)) {
|
||||
$this->error('The supplied --npm path does not exist or is not executable.');
|
||||
return 1;
|
||||
}
|
||||
$this->npmPath = $npmPath;
|
||||
}
|
||||
|
||||
if (!version_compare($this->getNpmVersion(), '7', '>')) {
|
||||
$this->error('"npm" version 7 or above must be installed to run this command.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If a custom path is passed, then validate it
|
||||
if ($packageJsonPath = $this->option('package-json')) {
|
||||
// If this is not an absolute path, then make it relative
|
||||
if (!str_starts_with($packageJsonPath, '/')) {
|
||||
$packageJsonPath = base_path($packageJsonPath);
|
||||
}
|
||||
|
||||
if (!File::exists($packageJsonPath)) {
|
||||
$this->error('The supplied --package-json path does not exist.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->packageJsonPath = $packageJsonPath;
|
||||
}
|
||||
|
||||
// Get any packages the user has requested
|
||||
$requestedPackages = $this->argument('assetPackage') ?: [];
|
||||
|
||||
$registeredPackages = $this->getRegisteredPackages($requestedPackages);
|
||||
|
||||
if (!$registeredPackages) {
|
||||
if ($requestedPackages) {
|
||||
$this->error('No registered packages matched the requested packages for installation.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info('No packages registered for mixing.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get base package.json
|
||||
$packageJson = new PackageJson($this->packageJsonPath ?? base_path('package.json'));
|
||||
|
||||
// Ensure asset compiling packages are set in package.json, then save
|
||||
$this->validateRequireDependenciesPresent($packageJson)
|
||||
->save();
|
||||
|
||||
// Process compilable asset packages, then save
|
||||
$this->processPackages($registeredPackages, $packageJson)
|
||||
->save();
|
||||
|
||||
if (!$this->option('no-install')) {
|
||||
// Ensure separation between package.json modification messages and rest of output
|
||||
$this->info('');
|
||||
|
||||
if ($this->runNpmInstall() !== 0) {
|
||||
$this->error("Unable to {$this->terms['complete']} dependencies.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("Dependencies successfully {$this->terms['completed']}!");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all packages registered by the system filtered by requestedPackages if defined
|
||||
* @throws SystemException
|
||||
*/
|
||||
protected function getRegisteredPackages(array $requestedPackages = []): array
|
||||
{
|
||||
$packageManager = $this->getPackageManager();
|
||||
|
||||
$registeredPackages = $packageManager->getPackages($this->assetType, true);
|
||||
|
||||
// Normalize the requestedPackages option
|
||||
$requestedPackages = array_map(fn ($name) => strtolower($name), $requestedPackages);
|
||||
|
||||
// Filter the registered packages to only include requested packages
|
||||
if (count($requestedPackages) && count($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 (isset($registeredPackages[$package])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($packageManager->getPackageTypeFromName($package)) {
|
||||
case PackageManager::TYPE_MODULE:
|
||||
$packageManager->registerPackage(
|
||||
$package,
|
||||
base_path('modules/' . Str::after($package, 'module-') . '/' . $this->configFile),
|
||||
$this->assetType
|
||||
);
|
||||
break;
|
||||
case PackageManager::TYPE_THEME:
|
||||
if (!$cmsEnabled) {
|
||||
break;
|
||||
}
|
||||
$theme = Theme::load(Str::after($package, 'theme-'));
|
||||
$packageManager->registerPackage(
|
||||
$package,
|
||||
$theme->getPath() . '/' . $this->configFile,
|
||||
$this->assetType
|
||||
);
|
||||
break;
|
||||
case PackageManager::TYPE_PLUGIN:
|
||||
$packageManager->registerPackage(
|
||||
$package,
|
||||
PluginManager::instance()->getPluginPath($package) . '/' . $this->configFile,
|
||||
$this->assetType
|
||||
);
|
||||
break;
|
||||
case null:
|
||||
throw new SystemException(sprintf(
|
||||
'PackageNotFoundException: The package `%s` does not exist.',
|
||||
$package
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Get an updated list of packages including any newly added packages
|
||||
$registeredPackages = $packageManager->getPackages($this->assetType, true);
|
||||
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $registeredPackages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the package.json of a package has the dependencies required for this command and asks the user if
|
||||
* they want to install them if not present.
|
||||
*/
|
||||
protected function validateRequireDependenciesPresent(PackageJson $packageJson): PackageJson
|
||||
{
|
||||
// Check to see if required packages are already present as a dependency
|
||||
foreach ($this->requiredDependencies as $dependency => $version) {
|
||||
if (
|
||||
!$packageJson->hasDependency($dependency)
|
||||
&& $this->confirm($dependency . ' was not found as a dependency in package.json, would you like to add it?', true)
|
||||
) {
|
||||
$packageJson->addDependency($dependency, $version, dev: true);
|
||||
}
|
||||
}
|
||||
|
||||
return $packageJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the packages passed can be installed and if possible, installs them.
|
||||
* @throws SystemException
|
||||
* @throws PackageNotFoundException
|
||||
*/
|
||||
protected function processPackages(array $registeredPackages, PackageJson $packageJson): PackageJson
|
||||
{
|
||||
// Check if the user requested a specific package for install
|
||||
if ($requestedPackages = array_map(fn ($name) => strtolower($name), $this->argument('assetPackage'))) {
|
||||
$packageManager = $this->getPackageManager();
|
||||
foreach ($requestedPackages as $requestedPackage) {
|
||||
// We did not find the package, exit
|
||||
if (!isset($registeredPackages[$requestedPackage])) {
|
||||
if ($detected = $packageManager->getPackage($requestedPackage, true)) {
|
||||
switch (count($detected)) {
|
||||
case 1:
|
||||
if ($detected[0]['type'] !== $this->assetType) {
|
||||
throw new SystemException(sprintf(
|
||||
'PackageNotConfiguredException: The requested package `%s` is only configured for %s. Run `php artisan %s:create %1$s`',
|
||||
$requestedPackage,
|
||||
$detected[0]['type'],
|
||||
$this->assetType
|
||||
));
|
||||
}
|
||||
|
||||
if ($detected[0]['ignored']) {
|
||||
throw new SystemException(sprintf(
|
||||
'PackageIgnoredException: The requested package `%s` is ignored, remove it from package.json to continue',
|
||||
$requestedPackage,
|
||||
));
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
default:
|
||||
if (($detected[0]['ignored'] ?? false) || ($detected[1]['ignored'] ?? false)) {
|
||||
throw new SystemException(sprintf(
|
||||
'PackageIgnoredException: The requested package `%s` is ignored, remove it from package.json to continue',
|
||||
$requestedPackage,
|
||||
));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw new SystemException(sprintf(
|
||||
'PackageNotFoundException: The requested package `%s` could not be found.',
|
||||
$requestedPackage,
|
||||
));
|
||||
}
|
||||
|
||||
$this->processPackage($packageJson, $requestedPackage, $registeredPackages[$requestedPackage], true);
|
||||
}
|
||||
|
||||
return $packageJson;
|
||||
}
|
||||
|
||||
// Process each found package
|
||||
foreach ($registeredPackages as $name => $package) {
|
||||
$this->processPackage($packageJson, $name, $package);
|
||||
}
|
||||
|
||||
return $packageJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a package to the project workspace or mark it as ignored based on user input
|
||||
*/
|
||||
protected function processPackage(PackageJson $packageJson, string $name, array $package, bool $force = false): bool
|
||||
{
|
||||
// Normalize package path across OS types
|
||||
$packagePath = Str::replace(DIRECTORY_SEPARATOR, '/', $package['path']);
|
||||
|
||||
// Nicely report if the package is already in the workspace
|
||||
if ($packageJson->hasWorkspace($packagePath)) {
|
||||
$this->warn(sprintf(
|
||||
'Package %s (%s) is already included in workspaces.packages.',
|
||||
$name,
|
||||
$packagePath
|
||||
));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($packageJson->hasIgnoredPackage($packagePath)) {
|
||||
$this->warn(sprintf(
|
||||
'The requested package %s (%s) is ignored, remove it from package.json to continue.',
|
||||
$name,
|
||||
$packagePath
|
||||
));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add the package path to the instance's package.json->workspaces->packages property if not present
|
||||
if (!$packageJson->hasWorkspace($packagePath) && !$packageJson->hasIgnoredPackage($packagePath)) {
|
||||
if (
|
||||
$force
|
||||
|| $this->confirm(
|
||||
sprintf(
|
||||
"Detected %s (%s), would you like to add it to package.json to include it in your project workspace? Answer no to ignore it.",
|
||||
$name,
|
||||
$packagePath
|
||||
),
|
||||
true
|
||||
)
|
||||
) {
|
||||
$packageJson->addWorkspace($packagePath);
|
||||
$this->info(sprintf(
|
||||
'Adding %s (%s) to the workspaces.packages property in package.json',
|
||||
$name,
|
||||
$packagePath
|
||||
));
|
||||
} else {
|
||||
$packageJson->addIgnoredPackage($packagePath);
|
||||
$this->warn(
|
||||
sprintf('Ignoring %s (%s)', $name, $packagePath)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect missing config files and provide feedback
|
||||
if (!File::exists($package['config'])) {
|
||||
$this->info(sprintf(
|
||||
'No config file found for %s, you should run %s:config',
|
||||
$name,
|
||||
$this->assetType
|
||||
));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the dependencies for the given package.
|
||||
*/
|
||||
protected function runNpmInstall(): int
|
||||
{
|
||||
$process = new Process(
|
||||
command: [$this->npmPath, 'install'],
|
||||
cwd: $this->packageJsonPath ? dirname($this->packageJsonPath) : base_path(),
|
||||
timeout: null
|
||||
);
|
||||
|
||||
if (!$this->option('disable-tty')) {
|
||||
try {
|
||||
$process->setTty(true);
|
||||
} catch (\Throwable $e) {
|
||||
// This will fail on unsupported systems
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return $process->run(function ($status, $stdout) {
|
||||
if (!$this->option('silent')) {
|
||||
$this->getOutput()->write($stdout);
|
||||
}
|
||||
});
|
||||
} catch (ProcessSignaledException $e) {
|
||||
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root package.json as a PackageManager object
|
||||
*/
|
||||
protected function getPackageManager(): PackageManager
|
||||
{
|
||||
// Flush the instance
|
||||
$packageManager = PackageManager::instance()->fireCallbacks();
|
||||
// Ensure the instance follows any custom package.json
|
||||
if ($this->packageJsonPath) {
|
||||
$packageManager->setPackageJsonPath($this->packageJsonPath);
|
||||
}
|
||||
|
||||
return $packageManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the installed NPM version.
|
||||
*/
|
||||
protected function getNpmVersion(): string
|
||||
{
|
||||
$process = new Process([$this->npmPath, '--version']);
|
||||
$process->run();
|
||||
return $process->getOutput();
|
||||
}
|
||||
}
|
||||
62
modules/system/console/asset/AssetList.php
Normal file
62
modules/system/console/asset/AssetList.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset;
|
||||
|
||||
use System\Classes\Asset\PackageManager;
|
||||
use Winter\Storm\Console\Command;
|
||||
use Winter\Storm\Support\Facades\File;
|
||||
|
||||
abstract class AssetList extends Command
|
||||
{
|
||||
protected string $assetType;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$compilableAssets = PackageManager::instance();
|
||||
$compilableAssets->fireCallbacks();
|
||||
|
||||
$packages = $compilableAssets->getPackages($this->assetType, true);
|
||||
|
||||
if (count($packages) === 0) {
|
||||
$this->info('No packages have been registered.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
|
||||
$rows = [];
|
||||
foreach ($packages as $name => $package) {
|
||||
$rows[] = [
|
||||
'name' => $name,
|
||||
'active' => !$package['ignored'],
|
||||
'path' => $package['path'],
|
||||
'configuration' => $package['config'],
|
||||
];
|
||||
|
||||
if (!File::exists($package['config'])) {
|
||||
$errors[] = "The config file for $name doesn't exist, try running artisan $this->assetType:install";
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->option('json')) {
|
||||
$this->line(json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
} else {
|
||||
$this->line('');
|
||||
$this->info('Packages registered:');
|
||||
$this->line('');
|
||||
$this->table(['Name', 'Active', 'Path', 'Configuration'], array_map(function ($row) {
|
||||
$row['active'] = ($row['active']) ? '<info>Yes</info>' : '<fg=red>No</>';
|
||||
return $row;
|
||||
}, $rows));
|
||||
$this->line('');
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
foreach ($errors as $error) {
|
||||
$this->error($error);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
/**
|
||||
* Css here
|
||||
*/
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1 @@
|
||||
console.log('hello world!');
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createApp } from "vue";
|
||||
// import Example from "./components/Example.vue";
|
||||
//
|
||||
// const app = createApp({
|
||||
// components: {Example}
|
||||
// });
|
||||
//
|
||||
// app.mount("#example");
|
||||
@@ -0,0 +1,4 @@
|
||||
const mix = require('laravel-mix');
|
||||
mix.setPublicPath(__dirname);
|
||||
|
||||
mix.js('assets/src/js/{{packageName}}.js', 'assets/dist/js/{{packageName}}.js');
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const App = () => {
|
||||
return <h1>Hello from React in Winter CMS!</h1>;
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1 @@
|
||||
import('./{{packageName}}.jsx');
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./components/App.jsx";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
import forms from '@tailwindcss/forms';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./assets/src/js/**/*.{js,jsx}',
|
||||
'./blocks/**/*.block',
|
||||
'./components/**/*.{htm,php}',
|
||||
'./controllers/**/*.{htm,php}',
|
||||
'./formwidgets/**/*.{htm,php}',
|
||||
'./widgets/**/*.{htm,php}',
|
||||
],
|
||||
plugins: [forms],
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
import forms from '@tailwindcss/forms';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./assets/src/js/**/*.{js,jsx}',
|
||||
'./blocks/**/*.block',
|
||||
'./layouts/**/*.htm',
|
||||
'./pages/**/*.htm',
|
||||
'./partials/**/*.htm',
|
||||
'./content/**/*.htm',
|
||||
],
|
||||
plugins: [forms],
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
|
||||
const defaultOutDir = 'assets/dist';
|
||||
|
||||
export default defineConfig({
|
||||
base: `${process.env.VITE_BASE}/${defaultOutDir}`,
|
||||
build: {
|
||||
outDir: defaultOutDir,
|
||||
assetsDir: '',
|
||||
},
|
||||
plugins: [
|
||||
laravel({
|
||||
publicDirectory: defaultOutDir,
|
||||
input: [
|
||||
'assets/src/css/{{packageName}}.css',
|
||||
'assets/src/js/{{packageName}}.js',
|
||||
],
|
||||
refresh: {
|
||||
paths: [
|
||||
'./**/*.htm',
|
||||
'./**/*.block',
|
||||
'assets/src/**/*.css',
|
||||
'assets/src/**/*.js',
|
||||
'assets/src/**/*.jsx',
|
||||
]
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
87
modules/system/console/asset/fixtures/mix.webpack.js.fixture
Normal file
87
modules/system/console/asset/fixtures/mix.webpack.js.fixture
Normal file
@@ -0,0 +1,87 @@
|
||||
const basePath = '%base%';
|
||||
const { assertSupportedNodeVersion } = require(basePath + '/node_modules/laravel-mix/src/Engine');
|
||||
|
||||
module.exports = async () => {
|
||||
assertSupportedNodeVersion();
|
||||
|
||||
const mix = require(basePath + '/node_modules/laravel-mix/src/Mix').primary;
|
||||
|
||||
mix.listen('init', function (_mix) {
|
||||
// Setup Winter path aliases
|
||||
_mix._api.alias({
|
||||
'$': '%pluginsPath%',
|
||||
'~': '%appPath%',
|
||||
});
|
||||
// disable notifications if not in watch
|
||||
%notificationInject%
|
||||
// define options
|
||||
_mix._api.options({
|
||||
processCssUrls: false,
|
||||
clearConsole: false,
|
||||
cssNano: {
|
||||
discardComments: {removeAll: true},
|
||||
}
|
||||
});
|
||||
// enable source maps for dev builds
|
||||
if (!_mix._api.inProduction()) {
|
||||
_mix._api.webpackConfig({
|
||||
devtool: 'inline-source-map'
|
||||
});
|
||||
_mix._api.sourceMaps();
|
||||
}
|
||||
|
||||
// Disable default manifest, allow for custom manifest
|
||||
if (_mix.config.manifest === 'mix-manifest.json') {
|
||||
mix.manifest.refresh = _ => void 0;
|
||||
} else {
|
||||
if (_mix.config.manifest === true) {
|
||||
_mix.config.manifest = 'mix-manifest.json';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// override default mix output
|
||||
mix.listen("loading-plugins", function (plugins) {
|
||||
plugins.forEach(function (plugin, index) {
|
||||
switch (plugin.constructor.name) {
|
||||
case "BuildOutputPlugin":
|
||||
plugins[index].apply = function (compiler) {
|
||||
compiler.hooks.done.tap('BuildOutputPlugin', stats => {
|
||||
if (stats.hasErrors()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.options.clearConsole) {
|
||||
this.clearConsole();
|
||||
}
|
||||
|
||||
let data = stats.toJson({
|
||||
assets: true,
|
||||
builtAt: true,
|
||||
hash: true,
|
||||
performance: true,
|
||||
relatedAssets: this.options.showRelated
|
||||
});
|
||||
|
||||
if (data.assets.length && !%silent%) {
|
||||
console.log(this.statsTable(data));
|
||||
}
|
||||
});
|
||||
};
|
||||
break;
|
||||
case "WebpackBarPlugin":
|
||||
if (%silent% || %noProgress%) {
|
||||
plugins[index].apply = _ => void 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
require('%mixConfigPath%');
|
||||
|
||||
await mix.installDependencies();
|
||||
await mix.init();
|
||||
|
||||
return mix.build();
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
const mix = require('laravel-mix');
|
||||
mix.setPublicPath(__dirname + '/assets');
|
||||
|
||||
// Your mix configuration below
|
||||
103
modules/system/console/asset/mix/MixCompile.php
Normal file
103
modules/system/console/asset/mix/MixCompile.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Mix;
|
||||
|
||||
use System\Console\Asset\AssetCompile;
|
||||
use Winter\Storm\Support\Facades\File;
|
||||
use Winter\Storm\Support\Str;
|
||||
|
||||
class MixCompile extends AssetCompile
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'mix:compile';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'mix:compile
|
||||
{webpackArgs?* : Arguments to pass through to the Webpack CLI}
|
||||
{--f|production : Runs compilation in "production" mode}
|
||||
{--s|silent : Enables silent mode, no output will be shown.}
|
||||
{--d|disable-tty : Disable tty mode}
|
||||
{--e|stop-on-error : Exit once an error is encountered}
|
||||
{--m|manifest= : Defines package.json to use for compile}
|
||||
{--p|package=* : Defines one or more packages to compile}
|
||||
{--no-progress : Do not show mix progress}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Mix and compile assets';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'mix:build'
|
||||
];
|
||||
|
||||
/**
|
||||
* Name of config file i.e. mix.webpack.js, vite.config.js
|
||||
*/
|
||||
protected string $configFile = 'mix.webpack.js';
|
||||
|
||||
/**
|
||||
* Call the AssetCompile::compileHandle with the mix type
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
return $this->compileHandle('mix');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the command array to create a Process object with
|
||||
*/
|
||||
protected function createCommand(string $configPath): array
|
||||
{
|
||||
$basePath = base_path();
|
||||
$command = $this->argument('webpackArgs') ?? [];
|
||||
array_unshift(
|
||||
$command,
|
||||
$basePath . sprintf('%1$snode_modules%1$s.bin%1$swebpack', DIRECTORY_SEPARATOR),
|
||||
'build',
|
||||
$this->option('silent') ? '--stats=none' : '--progress',
|
||||
'--config=' . $this->getJsConfigPath($configPath)
|
||||
);
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the temporary mix.webpack.js config file to run webpack with
|
||||
*/
|
||||
protected function beforeExecution(string $configPath): void
|
||||
{
|
||||
$basePath = base_path();
|
||||
$fixture = File::get(__DIR__ . '/../fixtures/mix.webpack.js.fixture');
|
||||
|
||||
$config = Str::swap([
|
||||
'%base%' => addslashes($basePath),
|
||||
'%notificationInject%' => 'mix._api.disableNotifications();',
|
||||
'%mixConfigPath%' => addslashes($configPath),
|
||||
'%pluginsPath%' => addslashes(plugins_path()),
|
||||
'%appPath%' => addslashes(base_path()),
|
||||
'%silent%' => (int) $this->option('silent'),
|
||||
'%noProgress%' => (int) $this->option('no-progress')
|
||||
], $fixture);
|
||||
|
||||
File::put($this->getJsConfigPath($configPath), $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the temporary mix.webpack.js file
|
||||
*/
|
||||
protected function afterExecution(string $configPath): void
|
||||
{
|
||||
$webpackConfigPath = $this->getJsConfigPath($configPath);
|
||||
if (File::exists($webpackConfigPath) && File::isFile($webpackConfigPath)) {
|
||||
File::delete($webpackConfigPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
modules/system/console/asset/mix/MixCreate.php
Normal file
37
modules/system/console/asset/mix/MixCreate.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php namespace System\Console\Asset\Mix;
|
||||
|
||||
use System\Console\Asset\AssetCreate;
|
||||
|
||||
class MixCreate extends AssetCreate
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'mix:create';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'mix:create
|
||||
{packageName : The package name to add configuration for}
|
||||
{--no-stubs : Disable stub file generation}
|
||||
{--s|silent : Enables silent mode, no output will be shown.}
|
||||
{--f|force : Force file overwrites}';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'mix:config',
|
||||
];
|
||||
|
||||
/**
|
||||
* The type of compilable to configure
|
||||
*/
|
||||
protected string $assetType = 'mix';
|
||||
|
||||
/**
|
||||
* The name of the config file
|
||||
*/
|
||||
protected string $configFile = 'winter.mix.js';
|
||||
}
|
||||
44
modules/system/console/asset/mix/MixInstall.php
Normal file
44
modules/system/console/asset/mix/MixInstall.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php namespace System\Console\Asset\Mix;
|
||||
|
||||
use System\Console\Asset\AssetInstall;
|
||||
|
||||
class MixInstall extends AssetInstall
|
||||
{
|
||||
/**
|
||||
* @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
|
||||
{assetPackage?* : The asset package name to install.}
|
||||
{--no-install : Tells Winter not to run npm install after config update.}
|
||||
{--npm= : Defines a custom path to the "npm" binary.}
|
||||
{--d|disable-tty : Disable tty mode.}
|
||||
{--s|silent : Enables silent mode, no output will be shown.}
|
||||
{--p|package-json= : Defines a custom path to "package.json" file. Must be above the workspace path.}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Install Node.js dependencies required for mixed assets';
|
||||
|
||||
/**
|
||||
* The asset compiler being used
|
||||
*/
|
||||
protected string $assetType = 'mix';
|
||||
|
||||
/**
|
||||
* The asset config file
|
||||
*/
|
||||
protected string $configFile = 'winter.mix.js';
|
||||
|
||||
/**
|
||||
* The required packages for this compiler
|
||||
*/
|
||||
protected array $requiredDependencies = [
|
||||
'laravel-mix' => '^6.0.41',
|
||||
];
|
||||
}
|
||||
29
modules/system/console/asset/mix/MixList.php
Normal file
29
modules/system/console/asset/mix/MixList.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Mix;
|
||||
|
||||
use System\Console\Asset\AssetList;
|
||||
|
||||
class MixList extends AssetList
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'mix:list';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'mix:list
|
||||
{--json : Output as JSON}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'List all registered Mix packages in this project.';
|
||||
|
||||
/**
|
||||
* The asset compiler being used
|
||||
*/
|
||||
protected string $assetType = 'mix';
|
||||
}
|
||||
66
modules/system/console/asset/mix/MixWatch.php
Normal file
66
modules/system/console/asset/mix/MixWatch.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Mix;
|
||||
|
||||
class MixWatch extends MixCompile
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'mix:watch';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'mix:watch
|
||||
{package : Defines the package to watch for changes}
|
||||
{webpackArgs?* : Arguments to pass through to the Webpack CLI}
|
||||
{--f|production : Runs compilation in "production" mode}
|
||||
{--m|manifest= : Defines package.json to use for compile}
|
||||
{--s|silent : Enables silent mode, no output will be shown.}
|
||||
{--d|disable-tty : Disable tty mode}
|
||||
{--no-progress : Do not show mix progress}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Mix and compile assets on-the-fly as changes are made.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'mix:dev'
|
||||
];
|
||||
|
||||
/**
|
||||
* Call the AssetCompile::watchHandle with the mix type
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
return $this->watchHandle('mix');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the command array to create a Process object with
|
||||
*/
|
||||
protected function createCommand(string $configPath): array
|
||||
{
|
||||
$command = parent::createCommand($configPath);
|
||||
|
||||
// @TODO: Detect Homestead running on Windows to switch to watch-poll-options instead, see https://laravel-mix.com/docs/6.0/cli#polling
|
||||
$command[] = '--watch';
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the cleanup of this command if a termination signal is received
|
||||
*/
|
||||
public function handleCleanup(): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info('Cleaning up: ' . $this->getPackagePath($this->watchingFilePath));
|
||||
$this->afterExecution(base_path($this->watchingFilePath));
|
||||
}
|
||||
}
|
||||
87
modules/system/console/asset/npm/NpmCommand.php
Normal file
87
modules/system/console/asset/npm/NpmCommand.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Npm;
|
||||
|
||||
use Symfony\Component\Process\Exception\ProcessSignaledException;
|
||||
use Symfony\Component\Process\Process;
|
||||
use System\Classes\Asset\PackageJson;
|
||||
use System\Classes\Asset\PackageManager;
|
||||
use Winter\Storm\Console\Command;
|
||||
use Winter\Storm\Exception\SystemException;
|
||||
|
||||
abstract class NpmCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Gets a package config and its PackageJson file based on the `package` argument.
|
||||
* @throws SystemException
|
||||
* @throws \JsonException
|
||||
*/
|
||||
protected function getPackage(): ?array
|
||||
{
|
||||
$compilableAssets = PackageManager::instance();
|
||||
$compilableAssets->fireCallbacks();
|
||||
|
||||
$name = $this->argument('package');
|
||||
|
||||
if (!$name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$compilableAssets->hasPackage($name, true)) {
|
||||
throw new SystemException(sprintf('Package "%s" is not a registered package.', $name));
|
||||
}
|
||||
|
||||
$package = $compilableAssets->getPackage($name, true)[0] ?? [];
|
||||
|
||||
// Assume that packages with matching names have matching package.json files
|
||||
$packageJson = new PackageJson($package['package'] ?? null);
|
||||
|
||||
return [$package, $packageJson];
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a npm process with the command and cwd provided
|
||||
*/
|
||||
protected function npmRun(array $command, string $path): int
|
||||
{
|
||||
$process = new Process(
|
||||
$command,
|
||||
base_path($path),
|
||||
['NODE_ENV' => $this->getNodeEnv()],
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
if (!$this->option('disable-tty') && !$this->option('silent')) {
|
||||
try {
|
||||
$process->setTty(true);
|
||||
} catch (ProcessSignaledException $e) {
|
||||
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return 1;
|
||||
} catch (\Throwable $e) {
|
||||
// This will fail on unsupported systems
|
||||
}
|
||||
}
|
||||
|
||||
return $process->run(function ($status, $stdout) {
|
||||
if (!$this->option('silent')) {
|
||||
$this->getOutput()->write($stdout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the env env to provide to node
|
||||
*/
|
||||
protected function getNodeEnv(): string
|
||||
{
|
||||
if (!$this->hasOption('production')) {
|
||||
return 'development';
|
||||
}
|
||||
|
||||
return $this->option('production', false) ? 'production' : 'development';
|
||||
}
|
||||
}
|
||||
55
modules/system/console/asset/npm/NpmInstall.php
Normal file
55
modules/system/console/asset/npm/NpmInstall.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Npm;
|
||||
|
||||
use System\Console\Asset\Npm\NpmCommand;
|
||||
use Winter\Storm\Exception\SystemException;
|
||||
|
||||
class NpmInstall extends NpmCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'npm:install';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $description = 'Install Node.js dependencies for a package';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $signature = 'npm:install
|
||||
{package? : The package name to add configuration for}
|
||||
{npmArgs?* : Arguments to pass through to the "npm" binary}
|
||||
{--npm= : Defines a custom path to the "npm" binary}
|
||||
{--d|dev : Install packages in devDependencies}
|
||||
{--s|silent : Silent mode.}
|
||||
{--disable-tty : Disable tty mode}';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$command = ($this->argument('npmArgs')) ?? [];
|
||||
|
||||
try {
|
||||
[$package, $packageJson] = $this->getPackage();
|
||||
} catch (SystemException $e) {
|
||||
if (!str_contains($e->getMessage(), 'is not a registered package.')) {
|
||||
throw $e;
|
||||
}
|
||||
array_unshift($command, $this->argument('package'));
|
||||
}
|
||||
|
||||
array_unshift($command, 'npm', 'install');
|
||||
|
||||
if ($this->option('dev')) {
|
||||
$command[] = '--save-dev';
|
||||
}
|
||||
|
||||
return $this->npmRun($command, $package['path'] ?? '');
|
||||
}
|
||||
}
|
||||
66
modules/system/console/asset/npm/NpmRun.php
Normal file
66
modules/system/console/asset/npm/NpmRun.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Npm;
|
||||
|
||||
use System\Console\Asset\Npm\NpmCommand;
|
||||
|
||||
class NpmRun extends NpmCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'npm:run';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'npm:run
|
||||
{package : Defines the package where the script is located.}
|
||||
{script : The name of the script to run, as defined in the package.json "scripts" config.}
|
||||
{additionalArgs?* : Arguments to pass through to the script being run.}
|
||||
{--f|production : Runs the script in "production" mode.}
|
||||
{--s|silent : Silent mode.}
|
||||
{--disable-tty : Disable tty mode}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Runs a script in a given package.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'mix:run'
|
||||
];
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
[$package, $packageJson] = $this->getPackage();
|
||||
|
||||
$script = $this->argument('script');
|
||||
|
||||
if (!$packageJson->hasScript($script)) {
|
||||
$this->error(
|
||||
sprintf('Script "%s" is not defined in package "%s".', $script, $this->argument('package'))
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!$this->option('silent')) {
|
||||
$this->info(sprintf('Running script "%s" in package "%s"', $script, $this->argument('package')));
|
||||
}
|
||||
|
||||
$command = ($this->argument('additionalArgs')) ?? [];
|
||||
if (count($command)) {
|
||||
array_unshift($command, 'npm', 'run', $script, '--');
|
||||
} else {
|
||||
array_unshift($command, 'npm', 'run', $script);
|
||||
}
|
||||
|
||||
return $this->npmRun($command, $package['path']);
|
||||
}
|
||||
}
|
||||
68
modules/system/console/asset/npm/NpmUpdate.php
Normal file
68
modules/system/console/asset/npm/NpmUpdate.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Npm;
|
||||
|
||||
use System\Console\Asset\Npm\NpmCommand;
|
||||
use Winter\Storm\Exception\SystemException;
|
||||
|
||||
class NpmUpdate extends NpmCommand
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'npm:update';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $description = 'Update Node.js dependencies required for mixed assets';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $signature = 'npm:update
|
||||
{package? : The package name to add configuration for.}
|
||||
{npmArgs?* : Arguments to pass through to the "npm" binary.}
|
||||
{--npm= : Defines a custom path to the "npm" binary.}
|
||||
{--a|save : Tell npm to update package.json.}
|
||||
{--s|silent : Silent mode.}
|
||||
{--disable-tty : Disable tty mode}';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public $replaces = [
|
||||
'mix:update'
|
||||
];
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$command = ($this->argument('npmArgs')) ?? [];
|
||||
|
||||
try {
|
||||
[$package, $packageJson] = $this->getPackage();
|
||||
} catch (SystemException $e) {
|
||||
if (!str_contains($e->getMessage(), 'is not a registered package.')) {
|
||||
throw $e;
|
||||
}
|
||||
array_unshift($command, $this->argument('package'));
|
||||
}
|
||||
|
||||
$args = ['npm', 'update'];
|
||||
|
||||
if ($this->option('save')) {
|
||||
$args[] = '--save';
|
||||
}
|
||||
|
||||
if (count($command)) {
|
||||
$args[] = '--';
|
||||
}
|
||||
|
||||
array_unshift($command, ...$args);
|
||||
|
||||
return $this->npmRun($command, $package['path'] ?? '');
|
||||
}
|
||||
}
|
||||
74
modules/system/console/asset/npm/NpmVersion.php
Normal file
74
modules/system/console/asset/npm/NpmVersion.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Npm;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
use System\Console\Asset\Npm\NpmCommand;
|
||||
|
||||
class NpmVersion extends NpmCommand
|
||||
{
|
||||
const NPM_MINIMUM_SUPPORTED_VERSION = '7.0';
|
||||
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'npm:version';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'npm:version
|
||||
{--c|compatible : Report compatible version via exit code.}
|
||||
{--s|silent : Silent mode.}
|
||||
{--disable-tty : Disable tty mode}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Runs a script in a given package.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$process = new Process(
|
||||
['npm', '--version'],
|
||||
base_path(),
|
||||
['NODE_ENV' => $this->getNodeEnv()],
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
$output = '';
|
||||
|
||||
$exit = $process->run(function ($status, $stdout) use (&$output) {
|
||||
$output .= $stdout;
|
||||
});
|
||||
|
||||
$output = trim($output);
|
||||
|
||||
// Npm failed for some reason, report to user
|
||||
if ($exit !== 0) {
|
||||
$this->error('NPM exited with error: ' . $output);
|
||||
return $exit;
|
||||
}
|
||||
|
||||
// Report the version to user
|
||||
if (!$this->option('silent')) {
|
||||
$this->info($output);
|
||||
}
|
||||
|
||||
// If the user has not requested a compatibility check, then return 0
|
||||
if (!$this->option('compatible')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If the version of npm is less than the required minimum, then return fail
|
||||
if (version_compare($output, static::NPM_MINIMUM_SUPPORTED_VERSION, '<')) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
79
modules/system/console/asset/vite/ViteCompile.php
Normal file
79
modules/system/console/asset/vite/ViteCompile.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Vite;
|
||||
|
||||
use System\Console\Asset\AssetCompile;
|
||||
use Winter\Storm\Support\Str;
|
||||
|
||||
class ViteCompile extends AssetCompile
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'vite:compile';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'vite:compile
|
||||
{viteArgs?* : Arguments to pass through to the Vite CLI}
|
||||
{--f|production : Runs compilation in "production" mode}
|
||||
{--s|silent : Enables silent mode, no output will be shown.}
|
||||
{--d|disable-tty : Disable tty mode}
|
||||
{--e|stop-on-error : Exit once an error is encountered}
|
||||
{--m|manifest= : Defines package.json to use for compile}
|
||||
{--p|package=* : Defines one or more packages to compile}
|
||||
';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Vite and compile assets';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'vite:build'
|
||||
];
|
||||
|
||||
/**
|
||||
* Name of config file i.e. mix.webpack.js, vite.config.js
|
||||
*/
|
||||
protected string $configFile = 'vite.config.mjs';
|
||||
|
||||
/**
|
||||
* Call the AssetCompile::compileHandle with the vite type
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
return $this->compileHandle('vite');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the command array to create a Process object with
|
||||
*/
|
||||
protected function createCommand(string $configPath): array
|
||||
{
|
||||
$basePath = base_path();
|
||||
$command = $this->argument('viteArgs') ?? [];
|
||||
array_unshift(
|
||||
$command,
|
||||
$basePath . sprintf('%1$snode_modules%1$s.bin%1$svite', DIRECTORY_SEPARATOR),
|
||||
'build',
|
||||
$this->option('silent') ? '--logLevel=silent' : '',
|
||||
);
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return values to append to the command env
|
||||
*/
|
||||
protected function createCommandEnv(string $configPath): array
|
||||
{
|
||||
return [
|
||||
'VITE_BASE' => Str::after($this->getPackagePath($configPath), base_path()),
|
||||
];
|
||||
}
|
||||
}
|
||||
64
modules/system/console/asset/vite/ViteCreate.php
Normal file
64
modules/system/console/asset/vite/ViteCreate.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Vite;
|
||||
|
||||
use System\Console\Asset\AssetCreate;
|
||||
|
||||
class ViteCreate extends AssetCreate
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'vite:create';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'vite:create
|
||||
{packageName : The package name to add configuration for}
|
||||
{--no-stubs : Disable stub file generation}
|
||||
{--s|silent : Enables silent mode, no output will be shown.}
|
||||
{--f|force : Force file overwrites}';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'vite:config',
|
||||
];
|
||||
|
||||
/**
|
||||
* The type of compilable to configure
|
||||
*/
|
||||
protected string $assetType = 'vite';
|
||||
|
||||
/**
|
||||
* The name of the config file
|
||||
*/
|
||||
protected string $configFile = 'vite.config.mjs';
|
||||
|
||||
/**
|
||||
* Output a helpful message with the twig code to set up vite after config generation is complete
|
||||
*/
|
||||
public function afterExecution(): void
|
||||
{
|
||||
if ($this->option('silent')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$packageName = $this->makePackageName($this->argument('packageName'));
|
||||
$this->output->writeln('');
|
||||
$this->info('Add the following to your twig to enable asset loading:');
|
||||
if ($this->option('react')) {
|
||||
$this->output->writeln(sprintf(
|
||||
'<fg=blue>{{ viteReactRefresh(\'%1$s\') }}</>',
|
||||
strtolower($this->argument('packageName'))
|
||||
));
|
||||
}
|
||||
$this->output->writeln(sprintf(
|
||||
'<fg=blue>{{ vite([\'assets/src/css/%1$s.css\', \'assets/src/js/%1$s.js\'], \'%2$s\') }}</>',
|
||||
$packageName,
|
||||
strtolower($this->argument('packageName'))
|
||||
));
|
||||
}
|
||||
}
|
||||
47
modules/system/console/asset/vite/ViteInstall.php
Normal file
47
modules/system/console/asset/vite/ViteInstall.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Vite;
|
||||
|
||||
use System\Console\Asset\AssetInstall;
|
||||
|
||||
class ViteInstall extends AssetInstall
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'vite:install';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'vite:install
|
||||
{assetPackage?* : The asset package name to install.}
|
||||
{--no-install : Tells Winter not to run npm install after config update.}
|
||||
{--npm= : Defines a custom path to the "npm" binary.}
|
||||
{--d|disable-tty : Disable tty mode.}
|
||||
{--s|silent : Enables silent mode, no output will be shown.}
|
||||
{--p|package-json= : Defines a custom path to "package.json" file. Must be above the workspace path.}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Install Node.js dependencies required for vite assets';
|
||||
|
||||
/**
|
||||
* The type of compilable to configure
|
||||
*/
|
||||
protected string $assetType = 'vite';
|
||||
|
||||
/**
|
||||
* The asset config file
|
||||
*/
|
||||
protected string $configFile = 'vite.config.mjs';
|
||||
|
||||
/**
|
||||
* The required packages for this compiler
|
||||
*/
|
||||
protected array $requiredDependencies = [
|
||||
'vite' => '^6.0.0',
|
||||
'laravel-vite-plugin' => '^1.1.0',
|
||||
];
|
||||
}
|
||||
29
modules/system/console/asset/vite/ViteList.php
Normal file
29
modules/system/console/asset/vite/ViteList.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Vite;
|
||||
|
||||
use System\Console\Asset\AssetList;
|
||||
|
||||
class ViteList extends AssetList
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'vite:list';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'vite:list
|
||||
{--json : Output as JSON}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'List all registered Vite packages in this project.';
|
||||
|
||||
/**
|
||||
* The asset compiler being used
|
||||
*/
|
||||
protected string $assetType = 'vite';
|
||||
}
|
||||
72
modules/system/console/asset/vite/ViteWatch.php
Normal file
72
modules/system/console/asset/vite/ViteWatch.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace System\Console\Asset\Vite;
|
||||
|
||||
use Winter\Storm\Support\Facades\File;
|
||||
|
||||
class ViteWatch extends ViteCompile
|
||||
{
|
||||
/**
|
||||
* @var string|null The default command name for lazy loading.
|
||||
*/
|
||||
protected static $defaultName = 'vite:watch';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = 'vite:watch
|
||||
{package : The package to watch for changes (ex. author.plugin, theme-code, etc)}
|
||||
{viteArgs?* : Arguments to pass through to the Webpack CLI}
|
||||
{--f|production : Runs compilation in "production" mode}
|
||||
{--m|manifest= : Defines package.json to use for compile}
|
||||
{--s|silent : Enables silent mode, no output will be shown.}
|
||||
{--d|disable-tty : Disable tty mode}
|
||||
{--no-progress : Do not show mix progress}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = 'Vite and compile assets on-the-fly as changes are made.';
|
||||
|
||||
/**
|
||||
* @var array List of commands that this command replaces (aliases)
|
||||
*/
|
||||
protected $replaces = [
|
||||
'vite:dev'
|
||||
];
|
||||
|
||||
/**
|
||||
* Call the AssetCompile::watchHandle with the vite type
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
return $this->watchHandle('vite');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the command array to create a Process object with
|
||||
*/
|
||||
protected function createCommand(string $configPath): array
|
||||
{
|
||||
$command = parent::createCommand($configPath);
|
||||
$key = array_search('build', $command);
|
||||
unset($command[$key]);
|
||||
|
||||
$command[] = '--host';
|
||||
|
||||
return array_values($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the cleanup of this command if a termination signal is received
|
||||
*/
|
||||
public function handleCleanup(): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info('Running compile to ensure files exist after exit');
|
||||
|
||||
$this->call('vite:compile', [
|
||||
'--package' => $this->argument('package'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
42
modules/system/console/scaffold/command/command.stub
Normal file
42
modules/system/console/scaffold/command/command.stub
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace {{ plugin_namespace }}\Console;
|
||||
|
||||
use Winter\Storm\Console\Command;
|
||||
|
||||
class {{studly_name}} extends Command
|
||||
{
|
||||
/**
|
||||
* @var string The console command name.
|
||||
*/
|
||||
protected static $defaultName = '{{ lower_command }}';
|
||||
|
||||
/**
|
||||
* @var string The name and signature of this command.
|
||||
*/
|
||||
protected $signature = '{{ lower_command }}
|
||||
{myCustomArgument : Example argument. <info>Additional information</info>}
|
||||
{--f|force : Force the operation to run and ignore production warnings and confirmation questions.}';
|
||||
|
||||
/**
|
||||
* @var string The console command description.
|
||||
*/
|
||||
protected $description = '{{ description }}';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->output->writeln('Hello world!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide autocomplete suggestions for the "myCustomArgument" argument
|
||||
*/
|
||||
// public function suggestMyCustomArgumentValues(): array
|
||||
// {
|
||||
// return ['value', 'another'];
|
||||
// }
|
||||
}
|
||||
27
modules/system/console/scaffold/factory/factory.stub
Normal file
27
modules/system/console/scaffold/factory/factory.stub
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace {{ plugin_namespace }}\Database\Factories;
|
||||
|
||||
use Winter\Storm\Database\Factories\Factory;
|
||||
|
||||
/**
|
||||
* {{ name }} Factory
|
||||
*
|
||||
{% if model %}
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\{{ plugin_namespace }}\Models\{{ model }}>
|
||||
{% endif %}
|
||||
*/
|
||||
class {{ studly_name }} extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
46
modules/system/console/scaffold/job/job.batched.stub
Normal file
46
modules/system/console/scaffold/job/job.batched.stub
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace {{ plugin_namespace }}\Jobs;
|
||||
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class {{studly_name}} implements ShouldQueue
|
||||
{
|
||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the middleware the job should pass through.
|
||||
*/
|
||||
public function middleware()
|
||||
{
|
||||
return []; // Remove this line to activate
|
||||
|
||||
return [
|
||||
// Instruct Laravel to not process the job if
|
||||
// its corresponding batch has been cancelled
|
||||
new SkipIfBatchCancelled()
|
||||
];
|
||||
}
|
||||
}
|
||||
30
modules/system/console/scaffold/job/job.queued.stub
Normal file
30
modules/system/console/scaffold/job/job.queued.stub
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace {{ plugin_namespace }}\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class {{studly_name}} implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
26
modules/system/console/scaffold/job/job.stub
Normal file
26
modules/system/console/scaffold/job/job.stub
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace {{ plugin_namespace }}\Jobs;
|
||||
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class {{studly_name}}
|
||||
{
|
||||
use Dispatchable;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Winter\Storm\Database\Schema\Blueprint;
|
||||
use Winter\Storm\Database\Updates\Migration;
|
||||
use Winter\Storm\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('{{ table }}', function (Blueprint $table) {
|
||||
{% if primaryKey %}
|
||||
$table->increments('{{ primaryKey }}');
|
||||
{% else %}
|
||||
$table->id();
|
||||
{% endif %}
|
||||
{% for field,config in fields %}
|
||||
$table->{{ config.type }}('{{ field }}'){{ config.required == false ? '->nullable()' }}{{ config.index ? '->index()' }};
|
||||
{% endfor %}
|
||||
{% for field in jsonable %}
|
||||
$table->mediumText('{{ field }}')->nullable();
|
||||
{% endfor %}
|
||||
{% for field in morphable %}
|
||||
$table->nullableMorphs('{{ field }}', 'morphable_index');
|
||||
{% endfor %}
|
||||
{% if not model or timestamps %}
|
||||
$table->timestamps();
|
||||
{% endif %}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('{{ table }}');
|
||||
}
|
||||
};
|
||||
28
modules/system/console/scaffold/migration/migration.stub
Normal file
28
modules/system/console/scaffold/migration/migration.stub
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Winter\Storm\Database\Schema\Blueprint;
|
||||
use Winter\Storm\Database\Updates\Migration;
|
||||
use Winter\Storm\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Winter\Storm\Database\Schema\Blueprint;
|
||||
use Winter\Storm\Database\Updates\Migration;
|
||||
use Winter\Storm\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('{{ table }}', function (Blueprint $table) {
|
||||
{% for field,config in fields %}
|
||||
$table->{{ config.type }}('{{ field }}'){{ config.required == false ? '->nullable()' }}{{ config.index ? '->index()' }};
|
||||
{% endfor %}
|
||||
{% for field in jsonable %}
|
||||
$table->mediumText('{{ field }}')->nullable();
|
||||
{% endfor %}
|
||||
{% for field in morphable %}
|
||||
$table->nullableMorphs('{{ field }}', 'morphable_index');
|
||||
{% endfor %}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('{{ table }}', function (Blueprint $table) {
|
||||
{% for field,config in fields %}
|
||||
$table->dropColumn('{{ field }}');
|
||||
{% endfor %}
|
||||
{% for field in jsonable %}
|
||||
$table->dropColumn('{{ field }}');
|
||||
{% endfor %}
|
||||
{% for field in morphable %}
|
||||
$table->dropColumn('{{ field }}');
|
||||
{% endfor %}
|
||||
});
|
||||
}
|
||||
};
|
||||
19
modules/system/console/scaffold/model/columns.stub
Normal file
19
modules/system/console/scaffold/model/columns.stub
Normal file
@@ -0,0 +1,19 @@
|
||||
# ===================================
|
||||
# List Column Definitions
|
||||
# ===================================
|
||||
|
||||
columns:
|
||||
id:
|
||||
label: '{{ plugin_id }}::lang.models.general.id'
|
||||
searchable: true
|
||||
created_at:
|
||||
label: '{{ plugin_id }}::lang.models.general.created_at'
|
||||
type: datetime
|
||||
searchable: true
|
||||
sortable: true
|
||||
invisible: true
|
||||
updated_at:
|
||||
label: '{{ plugin_id }}::lang.models.general.updated_at'
|
||||
type: datetime
|
||||
searchable: true
|
||||
sortable: true
|
||||
21
modules/system/console/scaffold/model/create_table.stub
Normal file
21
modules/system/console/scaffold/model/create_table.stub
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
use Winter\Storm\Database\Schema\Blueprint;
|
||||
use Winter\Storm\Database\Updates\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('{{ table_name }}', function (Blueprint $table) {
|
||||
$table->engine = 'InnoDB';
|
||||
$table->increments('id');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('{{ table_name }}');
|
||||
}
|
||||
};
|
||||
8
modules/system/console/scaffold/model/fields.stub
Normal file
8
modules/system/console/scaffold/model/fields.stub
Normal file
@@ -0,0 +1,8 @@
|
||||
# ===================================
|
||||
# Form Field Definitions
|
||||
# ===================================
|
||||
|
||||
fields:
|
||||
id:
|
||||
label: '{{ plugin_id }}::lang.models.general.id'
|
||||
disabled: true
|
||||
76
modules/system/console/scaffold/model/model.stub
Normal file
76
modules/system/console/scaffold/model/model.stub
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace {{ plugin_namespace }}\Models;
|
||||
|
||||
use Winter\Storm\Database\Model;
|
||||
|
||||
/**
|
||||
* {{ name }} Model
|
||||
*/
|
||||
class {{ studly_name }} extends Model
|
||||
{
|
||||
use \Winter\Storm\Database\Traits\Validation;
|
||||
|
||||
/**
|
||||
* @var string The database table used by the model.
|
||||
*/
|
||||
public $table = '{{ table_name }}';
|
||||
|
||||
/**
|
||||
* @var array Guarded fields
|
||||
*/
|
||||
protected $guarded = ['*'];
|
||||
|
||||
/**
|
||||
* @var array Fillable fields
|
||||
*/
|
||||
protected $fillable = [];
|
||||
|
||||
/**
|
||||
* @var array Validation rules for attributes
|
||||
*/
|
||||
public $rules = [];
|
||||
|
||||
/**
|
||||
* @var array Attributes to be cast to native types
|
||||
*/
|
||||
protected $casts = [];
|
||||
|
||||
/**
|
||||
* @var array Attributes to be cast to JSON
|
||||
*/
|
||||
protected $jsonable = [];
|
||||
|
||||
/**
|
||||
* @var array Attributes to be appended to the API representation of the model (ex. toArray())
|
||||
*/
|
||||
protected $appends = [];
|
||||
|
||||
/**
|
||||
* @var array Attributes to be removed from the API representation of the model (ex. toArray())
|
||||
*/
|
||||
protected $hidden = [];
|
||||
|
||||
/**
|
||||
* @var array Attributes to be cast to Argon (Carbon) instances
|
||||
*/
|
||||
protected $dates = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array Relations
|
||||
*/
|
||||
public $hasOne = [];
|
||||
public $hasMany = [];
|
||||
public $hasOneThrough = [];
|
||||
public $hasManyThrough = [];
|
||||
public $belongsTo = [];
|
||||
public $belongsToMany = [];
|
||||
public $morphTo = [];
|
||||
public $morphOne = [];
|
||||
public $morphMany = [];
|
||||
public $attachOne = [];
|
||||
public $attachMany = [];
|
||||
}
|
||||
88
modules/system/console/scaffold/plugin/plugin.stub
Normal file
88
modules/system/console/scaffold/plugin/plugin.stub
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace {{ plugin_namespace }};
|
||||
|
||||
use Backend\Facades\Backend;
|
||||
use Backend\Models\UserRole;
|
||||
use System\Classes\PluginBase;
|
||||
|
||||
/**
|
||||
* {{ name }} Plugin Information File
|
||||
*/
|
||||
class Plugin extends PluginBase
|
||||
{
|
||||
/**
|
||||
* Returns information about this plugin.
|
||||
*/
|
||||
public function pluginDetails(): array
|
||||
{
|
||||
return [
|
||||
'name' => '{{ plugin_id }}::lang.plugin.name',
|
||||
'description' => '{{ plugin_id }}::lang.plugin.description',
|
||||
'author' => '{{ author }}',
|
||||
'icon' => 'icon-leaf'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register method, called when the plugin is first registered.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot method, called right before the request route.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers any frontend components implemented in this plugin.
|
||||
*/
|
||||
public function registerComponents(): array
|
||||
{
|
||||
return []; // Remove this line to activate
|
||||
|
||||
return [
|
||||
\{{ plugin_namespace }}\Components\MyComponent::class => 'myComponent',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers any backend permissions used by this plugin.
|
||||
*/
|
||||
public function registerPermissions(): array
|
||||
{
|
||||
return []; // Remove this line to activate
|
||||
|
||||
return [
|
||||
'{{ plugin_id }}.some_permission' => [
|
||||
'tab' => '{{ plugin_id }}::lang.plugin.name',
|
||||
'label' => '{{ plugin_id }}::lang.permissions.some_permission',
|
||||
'roles' => [UserRole::CODE_DEVELOPER, UserRole::CODE_PUBLISHER],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers backend navigation items for this plugin.
|
||||
*/
|
||||
public function registerNavigation(): array
|
||||
{
|
||||
return []; // Remove this line to activate
|
||||
|
||||
return [
|
||||
'{{ lower_name }}' => [
|
||||
'label' => '{{ plugin_id }}::lang.plugin.name',
|
||||
'url' => Backend::url('{{ plugin_url }}/mycontroller'),
|
||||
'icon' => 'icon-leaf',
|
||||
'permissions' => ['{{ plugin_id }}.*'],
|
||||
'order' => 500,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
2
modules/system/console/scaffold/plugin/version.stub
Normal file
2
modules/system/console/scaffold/plugin/version.stub
Normal file
@@ -0,0 +1,2 @@
|
||||
'1.0.0':
|
||||
- 'First version of {{ name }}'
|
||||
7
modules/system/console/scaffold/settings/fields.stub
Normal file
7
modules/system/console/scaffold/settings/fields.stub
Normal file
@@ -0,0 +1,7 @@
|
||||
# ===================================
|
||||
# Form Field Definitions
|
||||
# ===================================
|
||||
|
||||
fields:
|
||||
settings_option:
|
||||
label: This is a sample settings field used by {{author}}.{{plugin}}
|
||||
33
modules/system/console/scaffold/settings/model.stub
Normal file
33
modules/system/console/scaffold/settings/model.stub
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace {{ plugin_namespace }}\Models;
|
||||
|
||||
use Model;
|
||||
|
||||
/**
|
||||
* {{name}} Model
|
||||
*/
|
||||
class {{studly_name}} extends Model
|
||||
{
|
||||
use \Winter\Storm\Database\Traits\Validation;
|
||||
|
||||
/**
|
||||
* @var array Behaviors implemented by this model.
|
||||
*/
|
||||
public $implement = [\System\Behaviors\SettingsModel::class];
|
||||
|
||||
/**
|
||||
* @var string Unique code
|
||||
*/
|
||||
public $settingsCode = '{{lower_author}}_{{lower_plugin}}_{{lower_name}}';
|
||||
|
||||
/**
|
||||
* @var mixed Settings form field definitions
|
||||
*/
|
||||
public $settingsFields = 'fields.yaml';
|
||||
|
||||
/**
|
||||
* @var array Validation rules
|
||||
*/
|
||||
public $rules = [];
|
||||
}
|
||||
13
modules/system/console/scaffold/test/pest.class.stub
Normal file
13
modules/system/console/scaffold/test/pest.class.stub
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use {{ tested_class_full }};
|
||||
|
||||
beforeEach(function () {
|
||||
$this->instance = $this->app->make({{ tested_class }}::class);
|
||||
});
|
||||
|
||||
{% for method in public_methods %}
|
||||
it('the {{ method }} method does something', function () {
|
||||
// $this->assertTrue(method_exists($this->instance, '{{ method }}'));
|
||||
});
|
||||
{% endfor %}
|
||||
45
modules/system/console/scaffold/test/pest.init.stub
Normal file
45
modules/system/console/scaffold/test/pest.init.stub
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
||||
| need to change it using the "uses()" function to bind a different classes or traits.
|
||||
|
|
||||
*/
|
||||
|
||||
uses(\System\Tests\Bootstrap\TestCase::class)->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
||||
|
|
||||
*/
|
||||
|
||||
// expect()->extend('toBeOne', function () {
|
||||
// return $this->toBe(1);
|
||||
// });
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Functions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
|
||||
| project that you don't want to repeat in every file. Here you can also expose helpers as
|
||||
| global functions to help you to reduce the number of lines of code in your test files.
|
||||
|
|
||||
*/
|
||||
|
||||
// function something()
|
||||
// {
|
||||
// // ..
|
||||
// }
|
||||
7
modules/system/console/scaffold/test/pest.stub
Normal file
7
modules/system/console/scaffold/test/pest.stub
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
test('example', function () {
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
5
modules/system/console/scaffold/test/pest.unit.stub
Normal file
5
modules/system/console/scaffold/test/pest.unit.stub
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
test('example', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
25
modules/system/console/scaffold/test/phpunit.stub
Normal file
25
modules/system/console/scaffold/test/phpunit.stub
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="../../../modules/system/tests/bootstrap/app.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>./tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>./tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
34
modules/system/console/scaffold/test/test.class.stub
Normal file
34
modules/system/console/scaffold/test/test.class.stub
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace {{ test_namespace }};
|
||||
|
||||
use {{ tested_class_full }};
|
||||
use System\Tests\Bootstrap\TestCase as WinterTestCase;
|
||||
|
||||
class {{ test_class }} extends WinterTestCase
|
||||
{
|
||||
/**
|
||||
* An instance of the class being tested
|
||||
*/
|
||||
protected ?{{ tested_class }} $instance = null;
|
||||
|
||||
/**
|
||||
* Initialize the {{ tested_class }}
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->instance = $this->app->make({{ tested_class }}::class);
|
||||
}
|
||||
{% for method in public_methods %}
|
||||
|
||||
/**
|
||||
* Test for the {{ method }} method
|
||||
*/
|
||||
public function test_{{ method }}()
|
||||
{
|
||||
$this->assertTrue(method_exists($this->instance, '{{ method }}'));
|
||||
}
|
||||
{% endfor %}
|
||||
}
|
||||
37
modules/system/console/scaffold/test/test.plugin.stub
Normal file
37
modules/system/console/scaffold/test/test.plugin.stub
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace {{ plugin_namespace }}\Tests\Unit;
|
||||
|
||||
use {{ plugin_namespace }}\Plugin;
|
||||
use System\Classes\PluginBase;
|
||||
use System\Tests\Bootstrap\PluginTestCase;
|
||||
|
||||
class PluginTest extends PluginTestCase
|
||||
{
|
||||
protected PluginBase $plugin;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->plugin = new Plugin($this->createApplication());
|
||||
}
|
||||
|
||||
public function testPluginDetails()
|
||||
{
|
||||
$details = $this->plugin->pluginDetails();
|
||||
|
||||
$this->assertIsArray($details);
|
||||
$this->assertArrayHasKey('name', $details);
|
||||
$this->assertArrayHasKey('description', $details);
|
||||
$this->assertArrayHasKey('icon', $details);
|
||||
$this->assertArrayHasKey('author', $details);
|
||||
|
||||
$this->assertEquals('{{ author }}', $details['author']);
|
||||
}
|
||||
|
||||
public function testRegisterPermissions()
|
||||
{
|
||||
$permissions = $this->plugin->registerPermissions();
|
||||
|
||||
$this->assertIsArray($permissions);
|
||||
}
|
||||
}
|
||||
18
modules/system/console/scaffold/test/test.stub
Normal file
18
modules/system/console/scaffold/test/test.stub
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace {{ test_namespace }};
|
||||
|
||||
use System\Tests\Bootstrap\PluginTestCase;
|
||||
|
||||
class {{ test_class }} extends PluginTestCase
|
||||
{
|
||||
/**
|
||||
* A basic feature test example.
|
||||
*/
|
||||
public function test_example(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
16
modules/system/console/scaffold/test/test.unit.stub
Normal file
16
modules/system/console/scaffold/test/test.unit.stub
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace {{ test_namespace }};
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class {{ test_class }} extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic unit test example.
|
||||
*/
|
||||
public function test_example(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
79
modules/system/console/traits/HasPluginArgument.php
Normal file
79
modules/system/console/traits/HasPluginArgument.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php namespace System\Console\Traits;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use System\Classes\PluginBase;
|
||||
use System\Classes\PluginManager;
|
||||
|
||||
/**
|
||||
* Console Command Trait that provides autocompletion for the "plugin" argument
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Luke Towers
|
||||
*/
|
||||
trait HasPluginArgument
|
||||
{
|
||||
/**
|
||||
* @var string What type of plugins to suggest in the CLI autocompletion. Valid values: "enabled", "disabled", "all"
|
||||
*/
|
||||
// protected $hasPluginsFilter = 'enabled';
|
||||
|
||||
/**
|
||||
* @var bool Validate the provided plugin input against the PluginManager, default true.
|
||||
*/
|
||||
// protected $validatePluginInput = true;
|
||||
|
||||
/**
|
||||
* Return available plugins for autocompletion of the "plugin" argument
|
||||
*/
|
||||
public function suggestPluginValues()
|
||||
{
|
||||
$manager = PluginManager::instance();
|
||||
$plugins = array_keys($manager->getAllPlugins());
|
||||
$filter = $this->hasPluginsFilter ?? 'enabled';
|
||||
|
||||
// Apply the hasPluginsFilter on the list of plugins to return
|
||||
if ($filter !== 'all') {
|
||||
foreach ($plugins as $i => $identifier) {
|
||||
$disabled = $manager->isDisabled($identifier);
|
||||
|
||||
if (
|
||||
(!$disabled && $filter === 'disabled')
|
||||
|| ($disabled && $filter === 'enabled')
|
||||
) {
|
||||
unset($plugins[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the desired plugin name from the input.
|
||||
* @throws InvalidArgumentException if the provided plugin name is invalid
|
||||
*/
|
||||
public function getPluginIdentifier($identifier = null): string
|
||||
{
|
||||
$pluginManager = PluginManager::instance();
|
||||
$pluginName = $identifier ?? $this->argument('plugin');
|
||||
$pluginName = $pluginManager->normalizeIdentifier($pluginName);
|
||||
|
||||
if (
|
||||
(isset($this->validatePluginInput) && $this->validatePluginInput !== false)
|
||||
&& !$pluginManager->hasPlugin($pluginName)
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf('Plugin "%s" could not be found.', $pluginName));
|
||||
}
|
||||
|
||||
return $pluginName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin instance for the input.
|
||||
* @throws InvalidArgumentException if the provided plugin name is invalid
|
||||
*/
|
||||
public function getPlugin($identifier = null): ?PluginBase
|
||||
{
|
||||
return PluginManager::instance()->findByIdentifier($this->getPluginIdentifier($identifier));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user