From 22de153dca99c94b412b68f9ad354b3cee023bd3 Mon Sep 17 00:00:00 2001 From: Herpaderp Aldent Date: Tue, 1 Nov 2022 15:57:05 +0100 Subject: [PATCH] Replace fswatch with spatie/file-system-watcher (#1) * Update PHP version requirement to 8.1 * Remove ReactPHP dependencies and replace with Symfony Process component * Replace fswatch dependency with spatie/file-system-watcher package (which uses inotify) * Add Termwind for pretty output when watching files, instead of using the default Pest console output which is not very readable on a terminal that supports ANSI colors --- composer.json | 8 +-- src/Plugin.php | 147 +++++++++++++++++++++++++++++++++---------------- src/Watch.php | 67 ---------------------- 3 files changed, 104 insertions(+), 118 deletions(-) delete mode 100644 src/Watch.php diff --git a/composer.json b/composer.json index 517bb5f..6c59fda 100644 --- a/composer.json +++ b/composer.json @@ -19,10 +19,10 @@ } ], "require": { - "php": "^7.3 || ^8.0", + "php": "^8.1", "pestphp/pest-plugin": "^1.0", - "react/child-process": "^0.6.1", - "react/event-loop": "^1.1" + "nunomaduro/termwind": "^1.13", + "spatie/file-system-watcher": "^1.1" }, "conflict": { "evenement/evenement": "^1.0", @@ -35,7 +35,7 @@ }, "require-dev": { "pestphp/pest": "^1.0", - "pestphp/pest-dev-tools": "dev-master" + "pestphp/pest-dev-tools": "^1.0.0" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/src/Plugin.php b/src/Plugin.php index 8edffe0..bbf9b59 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -6,12 +6,14 @@ use Pest\Contracts\Plugins\HandlesArguments; use Pest\Support\Str; -use React\ChildProcess\Process; -use React\EventLoop\Factory; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; + +use function Termwind\render; +use function Termwind\terminal; /** * @internal @@ -22,20 +24,40 @@ final class Plugin implements HandlesArguments private const WATCH_OPTION = 'watch'; - /** - * @var OutputInterface - */ - private $output; + private string $command = 'vendor/bin/pest'; + + public Process $pestProcess; /** @var array */ - private $watchedDirectories = self::WATCHED_DIRECTORIES; + private array|string $watchedDirectories; - public function __construct(OutputInterface $output) - { - $this->output = $output; + public function __construct( + private OutputInterface $output + ) { + // remove non-existing directories from watched directories + $this->watchedDirectories = array_filter(self::WATCHED_DIRECTORIES, fn ($directory) => is_dir($directory)); } public function handleArguments(array $originals): array + { + if (!$this->userWantsToWatch($originals)) { + return $originals; + } + + $this->info('Watching for changes...'); + + // dd('end', $this->getCommand()); + $processStarted = $this->startProcess(); + + // if the process failed to start, exit + if (!$processStarted) { + exit(1); + } + + $this->listenForChanges(); + } + + private function userWantsToWatch(array $originals): bool { $arguments = array_merge([''], array_values(array_filter($originals, function ($original): bool { return $original === sprintf('--%s', self::WATCH_OPTION) || Str::startsWith($original, sprintf('--%s=', self::WATCH_OPTION)); @@ -45,7 +67,6 @@ public function handleArguments(array $originals): array foreach ($arguments as $argument) { unset($originals[$argument]); } - $originals = array_flip($originals); $inputs = []; $inputs[] = new InputOption(self::WATCH_OPTION, null, InputOption::VALUE_OPTIONAL, '', true); @@ -53,62 +74,94 @@ public function handleArguments(array $originals): array $input = new ArgvInput($arguments, new InputDefinition($inputs)); if (!$input->hasParameterOption(sprintf('--%s', self::WATCH_OPTION))) { - return $originals; + return false; } - $this->checkFswatchIsAvailable(); - + // set the watched directories if ($input->getOption(self::WATCH_OPTION) !== null) { /* @phpstan-ignore-next-line */ $this->watchedDirectories = explode(',', $input->getOption(self::WATCH_OPTION)); } - $loop = Factory::create(); - $watcher = new Watch($loop, $this->watchedDirectories); - $watcher->run(); + // set command to run + $this->setCommand(implode(' ', array_flip($originals))); + + return true; + } + + private function listenForChanges(): self + { + \Spatie\Watcher\Watch::paths(...$this->watchedDirectories) + ->onAnyChange(function (string $event, string $path) { + if ($this->changedPathShouldRestartPest($path)) { + $this->restartProcess(); + } + }) + ->start(); + + return $this; + } + + private function startProcess(): bool + { + terminal()->clear(); + + $this->pestProcess = Process::fromShellCommandline($this->getCommand()); + + $this->pestProcess->setTty(true)->setTimeout(null); - $command = implode(' ', $originals); + $this->pestProcess->start(fn ($type, $output) => $this->output->write($output)); - $output = $this->output; + sleep(1); - $watcher->on('change', static function () use ($command, $output): void { - $loop = Factory::create(); - $process = new Process($command); - $process->start($loop); - // @phpstan-ignore-next-line - $process->stdout->on('data', function ($line) use ($output): void { - $output->write($line); - }); - $process->on('exit', function () use ($output): void { - $output->writeln(''); - }); - $loop->run(); - }); + return !$this->pestProcess->isTerminated(); + } + + private function restartProcess(): self + { + $this->info('Change detected! Restarting Pest...'); - $watcher->emit('change'); + $this->pestProcess->stop(0); - $loop->run(); + $this->startProcess(); - exit(0); + return $this; } - private function checkFswatchIsAvailable(): void + private function changedPathShouldRestartPest(string $path): bool { - exec('fswatch 2>&1', $output); + if ($this->isPhpFile($path)) { + return true; + } - if (strpos(implode(' ', $output), 'command not found') === false) { - return; + foreach ($this->watchedDirectories as $configuredPath) { + if ($path === $configuredPath) { + return true; + } } - $this->output->writeln(sprintf( - "\n ERROR fswatch was not found.", - )); + return false; + } + + private function isPhpFile(string $path): bool + { + return str_ends_with(strtolower($path), '.php'); + } + + public function getCommand(): string + { + return $this->command; + } + + public function setCommand(string $command): void + { + $this->command = $command; + } - $this->output->writeln(sprintf( - "\n Install it from: %s", - 'https://github.com/emcrisostomo/fswatch#getting-fswatch', - )); + private function info(string $message): void + { + $html = "
info{$message}
"; - exit(1); + render($html); } } diff --git a/src/Watch.php b/src/Watch.php deleted file mode 100644 index 79d137a..0000000 --- a/src/Watch.php +++ /dev/null @@ -1,67 +0,0 @@ - $folders - * - * @return void - */ - public function __construct(LoopInterface $loop, array $folders) - { - $this->loop = $loop; - $this->command = sprintf('fswatch --recursive %s', implode(' ', $folders)); - } - - /** - * Run the ReactPHP loop function with the change - * event listener. - */ - public function run(): void - { - $this->process = new Process($this->command); - - $this->process->start($this->loop); - - // @phpstan-ignore-next-line - $this->process->stderr->on('data', function ($data): void { - $this->emit('error', [$data]); - }); - - // @phpstan-ignore-next-line - $this->process->stdout->on('data', function ($data): void { - $this->emit('change', [$data]); - }); - } -}