diff --git a/CHANGELOG.md b/CHANGELOG.md
index c67c849a..56435d99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [5.2.0](https://github.com/userfrosting/sprinkle-core/compare/5.1.0...5.2.0)
+- [New Feature] Add [Vite](https://vitejs.dev) support :
+ - New Vite Bakery command, `assets:vite`. This command can be used to
+ - Add [Vite](https://vitejs.dev) Twig function : `vite_js`, `vite_css` and `vite_preload` to include Vite entrypoints into any Twig template.
+ - The default bundler (Webpack or Vite) used by `assets:build` command can be defined using the `assets.bundler` config, or `ASSETS_BUNDLER` env variable. Webpack is used by default.
+ - Added `assets.vite` config array in `app/config/default.php` to configure Twig integration.
+ - `assets.vite.dev` (bool) : Indicates whether the application is running in development mode (i.e. using vite server). Defaults to false. Tied to `VITE_DEV_ENABLED` env variable by default too.
+ - `assets.vite.base` (string) : Public base path from which Vite's published assets are served. The assets paths will be relative to the `outDir` in your vite configuration.
+ - `assets.vite.server` (string) : The vite server url, including port.
+- [Bakery] The default sub commands in `AssetsBuildCommand` are now in `AssetsBuildCommandListener`
+- [Bakery] Added the server option to `assets:webpack` to run HMR server (`npm run webpack:server`) plus use new npm command syntax.
+- [Bakery] `AbstractAggregateCommandEvent` construction is now optional. Added `addCommands` and `prependCommands`. All setters methods return `$this`.
## [5.1.1](https://github.com/userfrosting/sprinkle-core/compare/5.1.0...5.1.1)
- Fix issue with sprunje using multiple listable fetched from database ([Chat Reference](https://chat.userfrosting.com/channel/support?msg=sgMq8sbAjsCN2ZGXj))
diff --git a/app/config/default.php b/app/config/default.php
index 8b1c25d9..8c39f57e 100755
--- a/app/config/default.php
+++ b/app/config/default.php
@@ -44,6 +44,22 @@
'key' => 'site.alerts', // the key to use to store flash messages
],
+ /*
+ * ----------------------------------------------------------------------
+ * Asset bundler Config
+ * ----------------------------------------------------------------------
+ * Frontend assets can be handle either by Vite or Webpack. This section
+ * is used to define which bundler is used, and their configuration.
+ */
+ 'assets' => [
+ 'bundler' => env('ASSETS_BUNDLER'), // Either 'vite' or 'webpack'
+ 'vite' => [
+ 'dev' => env('VITE_DEV_ENABLED'),
+ 'base' => '',
+ 'server' => 'http://[::1]:3000/'
+ ]
+ ],
+
/*
* ----------------------------------------------------------------------
* Bakery Config
diff --git a/app/src/Bakery/AssetsBuildCommand.php b/app/src/Bakery/AssetsBuildCommand.php
index 14071a4c..aac31496 100644
--- a/app/src/Bakery/AssetsBuildCommand.php
+++ b/app/src/Bakery/AssetsBuildCommand.php
@@ -28,14 +28,6 @@ final class AssetsBuildCommand extends Command
{
use WithSymfonyStyle;
- /**
- * @var string[] Commands to run
- */
- protected array $commands = [
- 'assets:install',
- 'assets:webpack',
- ];
-
/**
* @param \UserFrosting\Event\EventDispatcher $eventDispatcher
*/
@@ -53,9 +45,9 @@ protected function configure(): void
$list = implode(', ', $this->aggregateCommands());
$this->setName('assets:build')
- ->setDescription('Build the assets using npm and Webpack Encore')
+ ->setDescription('Build the assets using npm and Webpack Encore or Vite')
->addOption('production', 'p', InputOption::VALUE_NONE, 'Create a production build')
- ->addOption('watch', 'w', InputOption::VALUE_NONE, 'Watch for changes and recompile automatically')
+ ->addOption('watch', 'w', InputOption::VALUE_NONE, 'Watch for changes and recompile automatically (Webpack only)')
->setHelp("This command combine the following commands : {$list}. For more info, see https://learn.userfrosting.com/asset-management.")
->setAliases(['build-assets', 'webpack']);
}
@@ -87,7 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
*/
protected function aggregateCommands(): array
{
- $event = new AssetsBuildCommandEvent($this->commands);
+ $event = new AssetsBuildCommandEvent();
$event = $this->eventDispatcher->dispatch($event);
return $event->getCommands();
diff --git a/app/src/Bakery/AssetsViteCommand.php b/app/src/Bakery/AssetsViteCommand.php
new file mode 100644
index 00000000..bcdb48a0
--- /dev/null
+++ b/app/src/Bakery/AssetsViteCommand.php
@@ -0,0 +1,114 @@
+Vite, using the config defined in vite.config.js.',
+ 'It will automatically compile the frontend dependencies in the public/assets/ directory, or use the Vite development server',
+ 'Everything will be executed in the same dir the bakery command is executed.',
+ 'For more info, see https://learn.userfrosting.com/asset-management',
+ ];
+
+ $this->setName('assets:vite')
+ ->setDescription('Alias for `npm run vite:dev` or `npm run vite:build` commands.')
+ ->addOption('production', 'p', InputOption::VALUE_NONE, 'Force the creation of a production build using `vite:build`')
+ ->setHelp(implode(' ', $help));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title('Running Vite');
+
+ // Get options
+ $production = (bool) $input->getOption('production');
+
+ // Validate dependencies
+ try {
+ $this->nodeVersionValidator->validate();
+ $this->npmVersionValidator->validate();
+ } catch (VersionCompareException $e) {
+ $this->io->error($e->getMessage());
+
+ return self::FAILURE;
+ }
+
+ // Get path
+ $path = getcwd();
+ if ($path === false) {
+ $this->io->error('Error getting working directory');
+
+ return self::FAILURE;
+ }
+
+ // Execute Vite
+ if (!file_exists($path . '/vite.config.js') && !file_exists($path . '/vite.config.ts')) {
+ $this->io->warning('Vite config not found. Skipping.');
+
+ return self::SUCCESS;
+ }
+
+ // Select command based on command arguments
+ $command = match (true) {
+ ($production || $this->envMode === 'production') => 'npm run vite:build',
+ default => 'npm run vite:dev',
+ };
+
+ $this->io->info("Running command: $command");
+ if ($this->executeCommand($command) !== 0) {
+ $this->io->error('Vite command has failed');
+
+ return self::FAILURE;
+ }
+
+ // If all went well and there's no fatal errors, we are successful
+ $this->io->success('Vite command completed');
+
+ return self::SUCCESS;
+ }
+}
diff --git a/app/src/Bakery/AssetsWebpackCommand.php b/app/src/Bakery/AssetsWebpackCommand.php
index b5b53f14..b8e9aaa6 100644
--- a/app/src/Bakery/AssetsWebpackCommand.php
+++ b/app/src/Bakery/AssetsWebpackCommand.php
@@ -24,7 +24,8 @@
use UserFrosting\Sprinkle\Core\Validators\NpmVersionValidator;
/**
- * Alias for `npm run dev`, `npm run build` and `npm run watch` commands.
+ * Alias for `npm run webpack:dev`, `npm run webpack:build`,
+ * `npm run webpack:server` and `npm run webpack:watch` commands.
*/
final class AssetsWebpackCommand extends Command
{
@@ -56,6 +57,7 @@ protected function configure(): void
->setDescription('Alias for `npm run dev`, `npm run build` or `npm run dev` command')
->addOption('production', 'p', InputOption::VALUE_NONE, 'Create a production build')
->addOption('watch', 'w', InputOption::VALUE_NONE, 'Watch for changes and recompile automatically')
+ ->addOption('server', 's', InputOption::VALUE_NONE, 'Run the development server with Hot Module Replacement (HMR)')
->setHelp(implode(' ', $help));
}
@@ -69,6 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
// Get options
$production = (bool) $input->getOption('production');
$watch = (bool) $input->getOption('watch');
+ $server = (bool) $input->getOption('server');
// Validate dependencies
try {
@@ -98,9 +101,10 @@ protected function execute(InputInterface $input, OutputInterface $output)
// Select command based on command arguments
$command = match (true) {
- ($production || $this->envMode === 'production') => 'npm run build',
- $watch => 'npm run watch',
- default => 'npm run dev',
+ ($production || $this->envMode === 'production') => 'npm run webpack:build',
+ $server => 'npm run webpack:server',
+ $watch => 'npm run webpack:watch',
+ default => 'npm run webpack:dev',
};
$this->io->info("Running command: $command");
diff --git a/app/src/Bakery/Event/AbstractAggregateCommandEvent.php b/app/src/Bakery/Event/AbstractAggregateCommandEvent.php
index 3c4fc37b..c889b83e 100644
--- a/app/src/Bakery/Event/AbstractAggregateCommandEvent.php
+++ b/app/src/Bakery/Event/AbstractAggregateCommandEvent.php
@@ -23,7 +23,7 @@ abstract class AbstractAggregateCommandEvent
/**
* @param string[] $commands
*/
- public function __construct(protected array $commands)
+ public function __construct(protected array $commands = [])
{
}
@@ -37,25 +37,61 @@ public function getCommands(): array
/**
* @param string[] $commands
+ *
+ * @return self
*/
- public function setCommands(array $commands): void
+ public function setCommands(array $commands): self
{
$this->commands = $commands;
+
+ return $this;
}
/**
* @param string $command
+ *
+ * @return self
*/
- public function addCommand(string $command): void
+ public function addCommand(string $command): self
{
$this->commands[] = $command;
+
+ return $this;
+ }
+
+ /**
+ * @param string[] $commands
+ *
+ * @return self
+ */
+ public function addCommands(array $commands): self
+ {
+ $this->commands = array_merge($this->commands, $commands);
+
+ return $this;
}
/**
* @param string $command
+ *
+ * @return self
*/
- public function prependCommand(string $command): void
+ public function prependCommand(string $command): self
{
array_unshift($this->commands, $command);
+
+ return $this;
+ }
+
+ /**
+ * @param string[] $commands
+ *
+ * @return self
+ */
+ public function prependCommands(array $commands): self
+ {
+ $this->commands = array_merge($commands, $this->commands);
+
+ return $this;
}
}
diff --git a/app/src/Core.php b/app/src/Core.php
index 29436369..a5021819 100644
--- a/app/src/Core.php
+++ b/app/src/Core.php
@@ -21,6 +21,7 @@
use UserFrosting\Sprinkle\Core\Bakery\AssetsBuildCommand;
use UserFrosting\Sprinkle\Core\Bakery\AssetsInstallCommand;
use UserFrosting\Sprinkle\Core\Bakery\AssetsUpdateCommand;
+use UserFrosting\Sprinkle\Core\Bakery\AssetsViteCommand;
use UserFrosting\Sprinkle\Core\Bakery\AssetsWebpackCommand;
use UserFrosting\Sprinkle\Core\Bakery\BakeCommand;
use UserFrosting\Sprinkle\Core\Bakery\ClearCacheCommand;
@@ -32,6 +33,7 @@
use UserFrosting\Sprinkle\Core\Bakery\DebugMailCommand;
use UserFrosting\Sprinkle\Core\Bakery\DebugTwigCommand;
use UserFrosting\Sprinkle\Core\Bakery\DebugVersionCommand;
+use UserFrosting\Sprinkle\Core\Bakery\Event\AssetsBuildCommandEvent;
use UserFrosting\Sprinkle\Core\Bakery\LocaleCompareCommand;
use UserFrosting\Sprinkle\Core\Bakery\LocaleDictionaryCommand;
use UserFrosting\Sprinkle\Core\Bakery\LocaleInfoCommand;
@@ -58,6 +60,7 @@
use UserFrosting\Sprinkle\Core\Error\ExceptionHandlerMiddleware;
use UserFrosting\Sprinkle\Core\Error\RegisterShutdownHandler;
use UserFrosting\Sprinkle\Core\Event\ResourceLocatorInitiatedEvent;
+use UserFrosting\Sprinkle\Core\Listeners\AssetsBuildCommandListener;
use UserFrosting\Sprinkle\Core\Listeners\ModelInitiated;
use UserFrosting\Sprinkle\Core\Listeners\ResourceLocatorInitiated;
use UserFrosting\Sprinkle\Core\Listeners\SetRouteCaching;
@@ -136,6 +139,7 @@ public function getBakeryCommands(): array
AssetsUpdateCommand::class,
AssetsInstallCommand::class,
AssetsWebpackCommand::class,
+ AssetsViteCommand::class,
BakeCommand::class,
ClearCacheCommand::class,
DebugCommand::class,
@@ -288,6 +292,9 @@ public function getEventListeners(): array
ResourceLocatorInitiatedEvent::class => [
ResourceLocatorInitiated::class,
],
+ AssetsBuildCommandEvent::class => [
+ AssetsBuildCommandListener::class,
+ ],
];
}
}
diff --git a/app/src/Listeners/AssetsBuildCommandListener.php b/app/src/Listeners/AssetsBuildCommandListener.php
new file mode 100644
index 00000000..f5e51dbe
--- /dev/null
+++ b/app/src/Listeners/AssetsBuildCommandListener.php
@@ -0,0 +1,59 @@
+config->getString('assets.bundler', 'webpack');
+ $commands = match ($bundler) {
+ 'vite' => $this->viteCommands,
+ 'webpack' => $this->webpackCommands,
+ default => $this->webpackCommands,
+ };
+
+ $event->addCommands($commands);
+ }
+}
diff --git a/app/tests/Integration/Bakery/DebugCommandTest.php b/app/tests/Integration/Bakery/DebugCommandTest.php
index fbb0afb8..0da1806b 100644
--- a/app/tests/Integration/Bakery/DebugCommandTest.php
+++ b/app/tests/Integration/Bakery/DebugCommandTest.php
@@ -19,8 +19,15 @@
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
-use UserFrosting\Bakery\SprinkleCommandsRepository;
use UserFrosting\Sprinkle\Core\Bakery\DebugCommand;
+use UserFrosting\Sprinkle\Core\Bakery\DebugConfigCommand;
+use UserFrosting\Sprinkle\Core\Bakery\DebugDbCommand;
+use UserFrosting\Sprinkle\Core\Bakery\DebugEventsCommand;
+use UserFrosting\Sprinkle\Core\Bakery\DebugLocatorCommand;
+use UserFrosting\Sprinkle\Core\Bakery\DebugMailCommand;
+use UserFrosting\Sprinkle\Core\Bakery\DebugTwigCommand;
+use UserFrosting\Sprinkle\Core\Bakery\DebugVersionCommand;
+use UserFrosting\Sprinkle\Core\Bakery\SprinkleListCommand;
use UserFrosting\Sprinkle\Core\Exceptions\VersionCompareException;
use UserFrosting\Sprinkle\Core\Tests\CoreTestCase;
use UserFrosting\Sprinkle\Core\Validators\PhpDeprecationValidator;
@@ -93,11 +100,21 @@ private function getCommandTester(): CommandTester
// add the sub-command to the app
$app = new Application();
- // Add all registered commands to make it simple
- /** @var \Symfony\Component\Console\Command\Command[] */
- $commands = $this->ci->get(SprinkleCommandsRepository::class);
+ // Add all required commands
+ $commands = [
+ DebugCommand::class,
+ DebugVersionCommand::class,
+ DebugVersionCommand::class,
+ SprinkleListCommand::class,
+ DebugConfigCommand::class,
+ DebugDbCommand::class,
+ DebugMailCommand::class,
+ DebugLocatorCommand::class,
+ DebugEventsCommand::class,
+ DebugTwigCommand::class,
+ ];
foreach ($commands as $command) {
- $app->add($command);
+ $app->add($command = $this->ci->get($command));
}
// Get command to test
diff --git a/app/tests/Unit/Bakery/AssetsBuildCommandTest.php b/app/tests/Unit/Bakery/AssetsBuildCommandTest.php
index cfc182cf..0be0e5da 100644
--- a/app/tests/Unit/Bakery/AssetsBuildCommandTest.php
+++ b/app/tests/Unit/Bakery/AssetsBuildCommandTest.php
@@ -23,11 +23,16 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
+use UserFrosting\Bakery\WithSymfonyStyle;
use UserFrosting\Event\EventDispatcher;
use UserFrosting\Sprinkle\Core\Bakery\AssetsBuildCommand;
use UserFrosting\Sprinkle\Core\Bakery\Event\AssetsBuildCommandEvent;
use UserFrosting\Testing\ContainerStub;
+/**
+ * N.B.: This test doesn't actually call the predefined sub-commands. It only
+ * tests the listener can overwrite them, and the stub command are called.
+ */
class AssetsBuildCommandTest extends TestCase
{
use MockeryPHPUnitIntegration;
@@ -55,6 +60,7 @@ public function testBaseCommand(): void
// Assert some output
$this->assertSame(0, $commandTester->getStatusCode());
+ $this->assertSame('SUCCESS', $commandTester->getDisplay());
}
public function testOneCommandFails(): void
@@ -80,6 +86,7 @@ public function testOneCommandFails(): void
// Assert some output
$this->assertSame(1, $commandTester->getStatusCode());
+ $this->assertSame('FAILURE', $commandTester->getDisplay());
}
public function testArgumentPassthrough(): void
@@ -138,6 +145,8 @@ public function __invoke(AssetsBuildCommandEvent $event): void
class AssetsStubCommand extends Command
{
+ use WithSymfonyStyle;
+
protected function configure(): void
{
$this->setName('stub');
@@ -145,6 +154,8 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output): int
{
+ $this->io->write('SUCCESS');
+
return self::SUCCESS;
}
}
@@ -173,6 +184,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
class AssetsStubFailCommand extends Command
{
+ use WithSymfonyStyle;
+
protected function configure(): void
{
$this->setName('fail');
@@ -180,6 +193,8 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output): int
{
+ $this->io->write('FAILURE');
+
return self::FAILURE;
}
}
diff --git a/app/tests/Unit/Bakery/AssetsViteCommandTest.php b/app/tests/Unit/Bakery/AssetsViteCommandTest.php
new file mode 100644
index 00000000..f394ad54
--- /dev/null
+++ b/app/tests/Unit/Bakery/AssetsViteCommandTest.php
@@ -0,0 +1,305 @@
+getNamespaceName();
+ PHPMockery::mock($namespace, 'getcwd')->andReturn('foo');
+ PHPMockery::mock($namespace, 'file_exists')->andReturn(true, false);
+
+ // Mock passthru, from ShellCommandHelper
+ $reflection_class = new ReflectionClass(ShellCommandHelper::class);
+ $namespace = $reflection_class->getNamespaceName();
+ PHPMockery::mock($namespace, 'passthru')->andReturn(null);
+
+ // Set Validator mock
+ $node = Mockery::mock(NodeVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+ $npm = Mockery::mock(NpmVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+
+ // Set mock in CI and run command
+ $ci = ContainerStub::create();
+ $ci->set(NodeVersionValidator::class, $node);
+ $ci->set(NpmVersionValidator::class, $npm);
+ $ci->set('UF_MODE', '');
+
+ /** @var AssetsViteCommand */
+ $command = $ci->get(AssetsViteCommand::class);
+ $result = BakeryTester::runCommand($command);
+
+ // Assert some output
+ $this->assertSame(0, $result->getStatusCode());
+ $this->assertStringContainsString('npm run vite:dev', $result->getDisplay());
+ $this->assertStringContainsString('Vite command completed', $result->getDisplay());
+ }
+
+ public function testCommandProductionEnv(): void
+ {
+ // Mock built-in function from main class
+ $reflection_class = new ReflectionClass(AssetsViteCommand::class);
+ $namespace = $reflection_class->getNamespaceName();
+ PHPMockery::mock($namespace, 'getcwd')->andReturn('foo');
+ PHPMockery::mock($namespace, 'file_exists')->andReturn(true, false);
+
+ // Mock passthru, from ShellCommandHelper
+ $reflection_class = new ReflectionClass(ShellCommandHelper::class);
+ $namespace = $reflection_class->getNamespaceName();
+ PHPMockery::mock($namespace, 'passthru')->andReturn(null);
+
+ // Set Validator mock
+ $node = Mockery::mock(NodeVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+ $npm = Mockery::mock(NpmVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+
+ // Set mock in CI and run command
+ $ci = ContainerStub::create();
+ $ci->set(NodeVersionValidator::class, $node);
+ $ci->set(NpmVersionValidator::class, $npm);
+ $ci->set('UF_MODE', 'production'); // Set production mode
+
+ /** @var AssetsViteCommand */
+ $command = $ci->get(AssetsViteCommand::class);
+ $result = BakeryTester::runCommand($command);
+
+ // Assert some output
+ $this->assertSame(0, $result->getStatusCode());
+ $this->assertStringContainsString('npm run vite:build', $result->getDisplay());
+ $this->assertStringContainsString('Vite command completed', $result->getDisplay());
+ }
+
+ public function testCommandProduction(): void
+ {
+ // Mock built-in function from main class
+ $reflection_class = new ReflectionClass(AssetsViteCommand::class);
+ $namespace = $reflection_class->getNamespaceName();
+ PHPMockery::mock($namespace, 'getcwd')->andReturn('foo');
+ PHPMockery::mock($namespace, 'file_exists')->andReturn(true, false);
+
+ // Mock passthru, from ShellCommandHelper
+ $reflection_class = new ReflectionClass(ShellCommandHelper::class);
+ $namespace = $reflection_class->getNamespaceName();
+ PHPMockery::mock($namespace, 'passthru')->andReturn(null);
+
+ // Set Validator mock
+ $node = Mockery::mock(NodeVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+ $npm = Mockery::mock(NpmVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+
+ // Set mock in CI and run command
+ $ci = ContainerStub::create();
+ $ci->set(NodeVersionValidator::class, $node);
+ $ci->set(NpmVersionValidator::class, $npm);
+ $ci->set('UF_MODE', '');
+
+ /** @var AssetsViteCommand */
+ $command = $ci->get(AssetsViteCommand::class);
+ $result = BakeryTester::runCommand($command, input: ['--production' => true]);
+
+ // Assert some output
+ $this->assertSame(0, $result->getStatusCode());
+ $this->assertStringContainsString('npm run vite:build', $result->getDisplay());
+ $this->assertStringContainsString('Vite command completed', $result->getDisplay());
+ }
+
+ public function testCommandWithMissingFiles(): void
+ {
+ // Mock built-in error_get_last
+ $reflection_class = new ReflectionClass(AssetsViteCommand::class);
+ $namespace = $reflection_class->getNamespaceName();
+ PHPMockery::mock($namespace, 'getcwd')->andReturn('./foo');
+ PHPMockery::mock($namespace, 'file_exists')->andReturn(false);
+
+ // Set Validator mock
+ $node = Mockery::mock(NodeVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+ $npm = Mockery::mock(NpmVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+
+ // Set mock in CI and run command
+ $ci = ContainerStub::create();
+ $ci->set(NodeVersionValidator::class, $node);
+ $ci->set(NpmVersionValidator::class, $npm);
+ $ci->set('UF_MODE', '');
+
+ /** @var AssetsViteCommand */
+ $command = $ci->get(AssetsViteCommand::class);
+ $result = BakeryTester::runCommand($command);
+
+ // Assert some output
+ $this->assertSame(0, $result->getStatusCode());
+ $this->assertStringContainsString('Vite config not found. Skipping.', $result->getDisplay());
+ }
+
+ public function testCommandWithErrorInGetcwd(): void
+ {
+ // Mock built-in error_get_last
+ $reflection_class = new ReflectionClass(AssetsViteCommand::class);
+ $namespace = $reflection_class->getNamespaceName();
+ PHPMockery::mock($namespace, 'getcwd')->andReturn(false);
+
+ // Set Validator mock
+ $node = Mockery::mock(NodeVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+ $npm = Mockery::mock(NpmVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+
+ // Set mock in CI and run command
+ $ci = ContainerStub::create();
+ $ci->set(NodeVersionValidator::class, $node);
+ $ci->set(NpmVersionValidator::class, $npm);
+ $ci->set('UF_MODE', '');
+
+ /** @var AssetsViteCommand */
+ $command = $ci->get(AssetsViteCommand::class);
+ $result = BakeryTester::runCommand($command);
+
+ // Assert some output
+ $this->assertSame(1, $result->getStatusCode());
+ $this->assertStringContainsString('Error getting working directory', $result->getDisplay());
+ }
+
+ public function testCommandWithNodeError(): void
+ {
+ // Set Validator mock
+ $node = Mockery::mock(NodeVersionValidator::class)
+ ->shouldReceive('validate')->andThrow(new VersionCompareException())
+ ->getMock();
+ $npm = Mockery::mock(NpmVersionValidator::class);
+
+ // Set mock in CI and run command
+ $ci = ContainerStub::create();
+ $ci->set(NodeVersionValidator::class, $node);
+ $ci->set(NpmVersionValidator::class, $npm);
+ $ci->set('UF_MODE', '');
+
+ /** @var AssetsViteCommand */
+ $command = $ci->get(AssetsViteCommand::class);
+ $result = BakeryTester::runCommand($command);
+
+ // Assert some output
+ $this->assertSame(1, $result->getStatusCode());
+ }
+
+ public function testCommandWithNpmError(): void
+ {
+ // Set Validator mock
+ $node = Mockery::mock(NodeVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+ $npm = Mockery::mock(NpmVersionValidator::class)
+ ->shouldReceive('validate')->andThrow(new VersionCompareException())
+ ->getMock();
+
+ // Set mock in CI and run command
+ $ci = ContainerStub::create();
+ $ci->set(NodeVersionValidator::class, $node);
+ $ci->set(NpmVersionValidator::class, $npm);
+ $ci->set('UF_MODE', '');
+
+ /** @var AssetsViteCommand */
+ $command = $ci->get(AssetsViteCommand::class);
+ $result = BakeryTester::runCommand($command);
+
+ // Assert some output
+ $this->assertSame(1, $result->getStatusCode());
+ }
+
+ public function testCommandWithNpmPassthruError(): void
+ {
+ // Mock built-in error_get_last
+ $reflection_class = new ReflectionClass(AssetsViteCommand::class);
+ $namespace = $reflection_class->getNamespaceName();
+ PHPMockery::mock($namespace, 'getcwd')->andReturn('foo');
+ PHPMockery::mock($namespace, 'file_exists')->andReturn(true);
+
+ // Mock passthru, from ShellCommandHelper
+ $reflection_class = new ReflectionClass(ShellCommandHelper::class);
+ $shellNamespace = $reflection_class->getNamespaceName();
+
+ // Use `MockBuilder` for more control
+ $builder = new MockBuilder();
+ $builder->setNamespace($shellNamespace)
+ ->setName('passthru')
+ ->setFunction(
+ function (string $command, int &$exitCode) {
+ $exitCode = 1;
+ }
+ );
+ $mock = $builder->build();
+ $mock->enable();
+
+ // Set Validator mock
+ $node = Mockery::mock(NodeVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+ $npm = Mockery::mock(NpmVersionValidator::class)
+ ->shouldReceive('validate')->andReturn(true)
+ ->getMock();
+
+ // Set mock in CI and run command
+ $ci = ContainerStub::create();
+ $ci->set(NodeVersionValidator::class, $node);
+ $ci->set(NpmVersionValidator::class, $npm);
+ $ci->set('UF_MODE', '');
+
+ /** @var AssetsViteCommand */
+ $command = $ci->get(AssetsViteCommand::class);
+ $result = BakeryTester::runCommand($command);
+
+ // Assert some output
+ $this->assertSame(1, $result->getStatusCode());
+ $this->assertStringContainsString('Vite command has failed', $result->getDisplay());
+
+ // Disable mock manually
+ $mock->disable();
+ }
+}
diff --git a/app/tests/Unit/Bakery/AssetsWebpackCommandTest.php b/app/tests/Unit/Bakery/AssetsWebpackCommandTest.php
index 1e4f5d6d..492e2459 100644
--- a/app/tests/Unit/Bakery/AssetsWebpackCommandTest.php
+++ b/app/tests/Unit/Bakery/AssetsWebpackCommandTest.php
@@ -68,7 +68,7 @@ public function testCommand(): void
// Assert some output
$this->assertSame(0, $result->getStatusCode());
- $this->assertStringContainsString('npm run dev', $result->getDisplay());
+ $this->assertStringContainsString('npm run webpack:dev', $result->getDisplay());
$this->assertStringContainsString('Webpack Encore run completed', $result->getDisplay());
}
@@ -101,11 +101,11 @@ public function testCommandProductionEnv(): void
/** @var AssetsWebpackCommand */
$command = $ci->get(AssetsWebpackCommand::class);
- $result = BakeryTester::runCommand($command, input: ['--production' => true]);
+ $result = BakeryTester::runCommand($command);
// Assert some output
$this->assertSame(0, $result->getStatusCode());
- $this->assertStringContainsString('npm run build', $result->getDisplay());
+ $this->assertStringContainsString('npm run webpack:build', $result->getDisplay());
$this->assertStringContainsString('Webpack Encore run completed', $result->getDisplay());
}
@@ -142,7 +142,7 @@ public function testCommandProduction(): void
// Assert some output
$this->assertSame(0, $result->getStatusCode());
- $this->assertStringContainsString('npm run build', $result->getDisplay());
+ $this->assertStringContainsString('npm run webpack:build', $result->getDisplay());
$this->assertStringContainsString('Webpack Encore run completed', $result->getDisplay());
}
@@ -179,7 +179,7 @@ public function testCommandWatch(): void
// Assert some output
$this->assertSame(0, $result->getStatusCode());
- $this->assertStringContainsString('npm run watch', $result->getDisplay());
+ $this->assertStringContainsString('npm run webpack:watch', $result->getDisplay());
$this->assertStringContainsString('Webpack Encore run completed', $result->getDisplay());
}
@@ -217,7 +217,7 @@ public function testCommandWatchAndProduction(): void
// Assert some output
// N.B.: When both production & watch are used, production has priority
$this->assertSame(0, $result->getStatusCode());
- $this->assertStringContainsString('npm run build', $result->getDisplay());
+ $this->assertStringContainsString('npm run webpack:build', $result->getDisplay());
$this->assertStringContainsString('Webpack Encore run completed', $result->getDisplay());
}
diff --git a/app/tests/Unit/Bakery/Event/AbstractAggregateCommandEventTest.php b/app/tests/Unit/Bakery/Event/AbstractAggregateCommandEventTest.php
index b4e7f141..d9d36e8c 100644
--- a/app/tests/Unit/Bakery/Event/AbstractAggregateCommandEventTest.php
+++ b/app/tests/Unit/Bakery/Event/AbstractAggregateCommandEventTest.php
@@ -30,6 +30,12 @@ public function testBaseCommand(): void
$event->prependCommand('bar');
$this->assertSame(['bar', 'foo'], $event->getCommands());
+
+ $event->addCommands(['foobar', '123']);
+ $this->assertSame(['bar', 'foo', 'foobar', '123'], $event->getCommands());
+
+ $event->prependCommands(['owl', 'egg']);
+ $this->assertSame(['owl', 'egg', 'bar', 'foo', 'foobar', '123'], $event->getCommands());
}
}
diff --git a/app/tests/Unit/Listeners/AssetsBuildCommandListenerTest.php b/app/tests/Unit/Listeners/AssetsBuildCommandListenerTest.php
new file mode 100644
index 00000000..5dac2511
--- /dev/null
+++ b/app/tests/Unit/Listeners/AssetsBuildCommandListenerTest.php
@@ -0,0 +1,55 @@
+shouldReceive('getString')->with('assets.bundler', 'webpack')->once()->andReturn($bundler)
+ ->getMock();
+
+ $event = new AssetsBuildCommandEvent();
+
+ $listener = new AssetsBuildCommandListener($config);
+ $listener($event);
+
+ $this->assertSame($expected, $event->getCommands());
+ }
+
+ /**
+ * @return array[]
+ */
+ public static function bundlerProvider(): array
+ {
+ return [
+ ['webpack', ['assets:install', 'assets:webpack']],
+ ['foobar', ['assets:install', 'assets:webpack']],
+ ['vite', ['assets:install', 'assets:vite']],
+ ];
+ }
+}
diff --git a/composer.json b/composer.json
index 075fcc9c..338ac1f3 100644
--- a/composer.json
+++ b/composer.json
@@ -43,7 +43,8 @@
"slim/csrf": "^1.3",
"slim/twig-view": "^3.0",
"vlucas/phpdotenv": "^5.3",
- "userfrosting/framework": "~5.2.0@dev"
+ "userfrosting/framework": "~5.2.0@dev",
+ "userfrosting/vite-php-twig": "^1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",