From 8019e54ddf30ac2dc1599f028fb0333f8944a5cc Mon Sep 17 00:00:00 2001 From: Saeed Vaziry Date: Mon, 9 Feb 2026 21:39:46 +0100 Subject: [PATCH] Add Bun site type --- app/Enums/NodePackageManager.php | 6 +- app/Providers/SiteTypeServiceProvider.php | 43 ++++ app/SiteTypes/MiseBun.php | 232 ++++++++++++++++++++++ tests/Feature/SitesTest.php | 27 +++ tests/Unit/MiseBunSiteTypeTest.php | 232 ++++++++++++++++++++++ 5 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 app/SiteTypes/MiseBun.php create mode 100644 tests/Unit/MiseBunSiteTypeTest.php diff --git a/app/Enums/NodePackageManager.php b/app/Enums/NodePackageManager.php index ec7018ac..7ce4b166 100644 --- a/app/Enums/NodePackageManager.php +++ b/app/Enums/NodePackageManager.php @@ -30,9 +30,9 @@ public function getText(): string public function installCommand(): string { return match ($this) { - self::Npm => 'npm install', - self::Pnpm => 'pnpm install', - self::Yarn => 'yarn install', + self::Npm => 'npm ci', + self::Pnpm => 'pnpm install --frozen-lockfile', + self::Yarn => 'yarn install --frozen-lockfile', }; } diff --git a/app/Providers/SiteTypeServiceProvider.php b/app/Providers/SiteTypeServiceProvider.php index 3b32962c..adb0a4ab 100644 --- a/app/Providers/SiteTypeServiceProvider.php +++ b/app/Providers/SiteTypeServiceProvider.php @@ -11,6 +11,7 @@ use App\Plugins\RegisterSiteType; use App\SiteTypes\Laravel; use App\SiteTypes\LoadBalancer; +use App\SiteTypes\MiseBun; use App\SiteTypes\MiseNodeJS; use App\SiteTypes\NodeJS; use App\SiteTypes\PHPBlank; @@ -30,6 +31,7 @@ public function boot(): void $this->laravel(); $this->nodeJS(); $this->miseNodeJS(); + $this->miseBun(); $this->loadBalancer(); $this->phpMyAdmin(); $this->wordpress(); @@ -208,6 +210,47 @@ private function miseNodeJS(): void ->register(); } + private function miseBun(): void + { + RegisterSiteType::make(MiseBun::id()) + ->label('Bun') + ->handler(MiseBun::class) + ->form(DynamicForm::make([ + DynamicField::make('bun_version') + ->select() + ->label('Bun Version') + ->options(MiseBun::BUN_VERSIONS) + ->default('1.2'), + DynamicField::make('source_control') + ->component() + ->label('Source Control'), + DynamicField::make('port') + ->text() + ->label('Port') + ->placeholder('3000') + ->description('On which port your app will be running'), + DynamicField::make('repository') + ->text() + ->label('Repository') + ->placeholder('organization/repository'), + DynamicField::make('branch') + ->text() + ->label('Branch') + ->default('main'), + DynamicField::make('build_command') + ->text() + ->label('Build Command') + ->placeholder('e.g., bun run build') + ->description('Command to build your application. Leave empty to use the build script of package.json'), + DynamicField::make('start_command') + ->text() + ->label('Start Command') + ->placeholder('e.g., bun run start') + ->description('Command to start your application. Leave empty to use the start script of package.json'), + ])) + ->register(); + } + public function loadBalancer(): void { RegisterSiteType::make(LoadBalancer::id()) diff --git a/app/SiteTypes/MiseBun.php b/app/SiteTypes/MiseBun.php new file mode 100644 index 00000000..99b0c85f --- /dev/null +++ b/app/SiteTypes/MiseBun.php @@ -0,0 +1,232 @@ +site->type_data['bun_version'] ?? '1.2'; + } + + public static function make(): self + { + return new self(new Site(['type' => self::id()])); + } + + public function createRules(array $input): array + { + return [ + 'source_control' => [ + 'required', + Rule::exists('source_controls', 'id'), + ], + 'repository' => [ + 'required', + ], + 'branch' => [ + 'required', + ], + 'port' => [ + 'required', + 'numeric', + 'between:1,65535', + ], + 'bun_version' => [ + 'required', + Rule::in(self::BUN_VERSIONS), + ], + 'build_command' => [ + 'nullable', + 'string', + ], + 'start_command' => [ + 'nullable', + 'string', + ], + ]; + } + + public function createFields(array $input): array + { + return [ + 'source_control_id' => $input['source_control'] ?? '', + 'repository' => $input['repository'] ?? '', + 'branch' => $input['branch'] ?? '', + 'port' => $input['port'] ?? '', + ]; + } + + public function data(array $input): array + { + return [ + 'bun_version' => $input['bun_version'] ?? '1.2', + 'build_command' => ! empty($input['build_command']) ? $input['build_command'] : 'bun run build', + 'start_command' => ! empty($input['start_command']) ? $input['start_command'] : 'bun run start', + ]; + } + + protected function buildCommand(): string + { + return $this->site->type_data['build_command'] ?? 'bun run build'; + } + + protected function startCommand(): string + { + return $this->site->type_data['start_command'] ?? 'bun run start'; + } + + /** + * @throws FailedToDeployGitKey + * @throws SSHError + */ + public function install(): void + { + $this->isolate(); + $this->progress(10); + + $this->setupRuntime(); + $this->progress(25); + + $this->site->webserver()->createVHost($this->site); + $this->progress(35); + + $this->deployKey(); + $this->progress(45); + + app(Git::class)->clone($this->site); + $this->progress(55); + + $this->runInstall(); + $this->progress(70); + + $this->runBuild(); + $this->progress(85); + + $this->createWorker(); + $this->progress(100); + } + + /** + * @throws SSHError + */ + protected function runInstall(): void + { + $this->site->server->ssh($this->site->user)->exec( + $this->wrapCommand('bun install --frozen-lockfile', true), + 'bun-install', + $this->site->id + ); + } + + /** + * @throws SSHError + */ + protected function runBuild(): void + { + $this->site->server->ssh($this->site->user)->exec( + $this->wrapCommand($this->buildCommand(), true), + 'build', + $this->site->id + ); + } + + protected function createWorker(): void + { + /** @var ?Worker $worker */ + $worker = $this->site->workers()->where('name', 'app')->first(); + if ($worker) { + app(ManageWorker::class)->restart($worker); + } else { + app(CreateWorker::class)->create( + $this->site->server, + [ + 'name' => 'app', + 'command' => $this->workerCommand(), + 'user' => $this->site->user ?? $this->site->server->getSshUser(), + 'auto_start' => true, + 'auto_restart' => true, + 'numprocs' => 1, + 'environment' => $this->workerEnvironment(), + ], + $this->site, + ); + } + } + + public function baseCommands(): array + { + return []; + } + + public function vhost(string $webserver): string|View + { + if ($webserver === 'nginx') { + return view('ssh.services.webserver.nginx.vhost', [ + 'header' => [ + view('ssh.services.webserver.nginx.vhost-blocks.force-ssl', ['site' => $this->site]), + ], + 'main' => [ + view('ssh.services.webserver.nginx.vhost-blocks.port', ['site' => $this->site]), + view('ssh.services.webserver.nginx.vhost-blocks.core', ['site' => $this->site]), + view('ssh.services.webserver.nginx.vhost-blocks.reverse-proxy', ['site' => $this->site]), + view('ssh.services.webserver.nginx.vhost-blocks.redirects', ['site' => $this->site]), + ], + ]); + } + + if ($webserver === 'caddy') { + return view('ssh.services.webserver.caddy.vhost', [ + 'site' => $this->site, + 'main' => [ + view('ssh.services.webserver.caddy.vhost-blocks.force-ssl', ['site' => $this->site]), + view('ssh.services.webserver.caddy.vhost-blocks.port', ['site' => $this->site]), + view('ssh.services.webserver.caddy.vhost-blocks.core', ['site' => $this->site]), + view('ssh.services.webserver.caddy.vhost-blocks.reverse-proxy', ['site' => $this->site]), + view('ssh.services.webserver.caddy.vhost-blocks.redirects', ['site' => $this->site]), + ], + ]); + } + + return ''; + } +} diff --git a/tests/Feature/SitesTest.php b/tests/Feature/SitesTest.php index 932bebe1..39c062ea 100644 --- a/tests/Feature/SitesTest.php +++ b/tests/Feature/SitesTest.php @@ -11,6 +11,7 @@ use App\Models\SourceControl; use App\SiteTypes\Laravel; use App\SiteTypes\LoadBalancer; +use App\SiteTypes\MiseBun; use App\SiteTypes\MiseNodeJS; use App\SiteTypes\NodeJS; use App\SiteTypes\PHPBlank; @@ -710,6 +711,32 @@ public static function create_data(): array 'user' => 'example', ], ], + [ + [ + 'type' => MiseBun::id(), + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'bun_version' => '1.2', + 'port' => '3000', + 'repository' => 'test/test', + 'branch' => 'main', + 'user' => 'example', + ], + ], + [ + [ + 'type' => MiseBun::id(), + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'bun_version' => '1.1', + 'port' => '3000', + 'repository' => 'test/test', + 'branch' => 'main', + 'build_command' => 'bun run build:prod', + 'start_command' => 'bun run start:prod', + 'user' => 'example', + ], + ], ]; } diff --git a/tests/Unit/MiseBunSiteTypeTest.php b/tests/Unit/MiseBunSiteTypeTest.php new file mode 100644 index 00000000..4e37bb01 --- /dev/null +++ b/tests/Unit/MiseBunSiteTypeTest.php @@ -0,0 +1,232 @@ +create(); + $this->miseSite = Site::factory()->create([ + 'server_id' => $server->id, + 'user' => 'testuser', + 'path' => '/home/testuser/example.com', + 'type' => MiseBun::id(), + 'type_data' => [ + 'bun_version' => '1.2', + 'build_command' => 'bun run build', + 'start_command' => 'bun run start', + ], + ]); + $this->siteType = new MiseBun($this->miseSite); + } + + public function test_id(): void + { + $this->assertEquals('mise_bun', MiseBun::id()); + } + + public function test_language(): void + { + $this->assertEquals('bun', $this->siteType->language()); + } + + public function test_required_services(): void + { + $this->assertEquals(['webserver', 'process_manager'], $this->siteType->requiredServices()); + } + + public function test_runtime_version_from_type_data(): void + { + $reflection = new \ReflectionMethod($this->siteType, 'runtimeVersion'); + + $this->assertEquals('1.2', $reflection->invoke($this->siteType)); + } + + public function test_runtime_version_defaults_to_1_2(): void + { + $server = Server::factory()->create(); + $site = Site::factory()->create([ + 'server_id' => $server->id, + 'user' => 'testuser', + 'path' => '/home/testuser/example.com', + 'type' => MiseBun::id(), + 'type_data' => [], + ]); + $siteType = new MiseBun($site); + $reflection = new \ReflectionMethod($siteType, 'runtimeVersion'); + + $this->assertEquals('1.2', $reflection->invoke($siteType)); + } + + public function test_runtime(): void + { + $reflection = new \ReflectionMethod($this->siteType, 'runtime'); + + $this->assertEquals('bun', $reflection->invoke($this->siteType)); + } + + public function test_worker_command_returns_start_command(): void + { + $reflection = new \ReflectionMethod($this->siteType, 'workerCommand'); + + $command = $reflection->invoke($this->siteType); + + $this->assertEquals('bun run start', $command); + } + + public function test_build_command_from_type_data(): void + { + $reflection = new \ReflectionMethod($this->siteType, 'buildCommand'); + + $this->assertEquals('bun run build', $reflection->invoke($this->siteType)); + } + + public function test_build_command_defaults(): void + { + $server = Server::factory()->create(); + $site = Site::factory()->create([ + 'server_id' => $server->id, + 'user' => 'testuser', + 'path' => '/home/testuser/example.com', + 'type' => MiseBun::id(), + 'type_data' => [], + ]); + $siteType = new MiseBun($site); + $reflection = new \ReflectionMethod($siteType, 'buildCommand'); + + $this->assertEquals('bun run build', $reflection->invoke($siteType)); + } + + public function test_start_command_from_type_data(): void + { + $reflection = new \ReflectionMethod($this->siteType, 'startCommand'); + + $this->assertEquals('bun run start', $reflection->invoke($this->siteType)); + } + + public function test_start_command_defaults(): void + { + $server = Server::factory()->create(); + $site = Site::factory()->create([ + 'server_id' => $server->id, + 'user' => 'testuser', + 'path' => '/home/testuser/example.com', + 'type' => MiseBun::id(), + 'type_data' => [], + ]); + $siteType = new MiseBun($site); + $reflection = new \ReflectionMethod($siteType, 'startCommand'); + + $this->assertEquals('bun run start', $reflection->invoke($siteType)); + } + + public function test_data_with_defaults(): void + { + $data = $this->siteType->data([]); + + $this->assertEquals([ + 'bun_version' => '1.2', + 'build_command' => 'bun run build', + 'start_command' => 'bun run start', + ], $data); + } + + public function test_data_with_custom_commands(): void + { + $data = $this->siteType->data([ + 'bun_version' => '1.1', + 'build_command' => 'bun run build:prod', + 'start_command' => 'bun run start:prod', + ]); + + $this->assertEquals([ + 'bun_version' => '1.1', + 'build_command' => 'bun run build:prod', + 'start_command' => 'bun run start:prod', + ], $data); + } + + public function test_create_fields(): void + { + $fields = $this->siteType->createFields([ + 'source_control' => '1', + 'repository' => 'org/repo', + 'branch' => 'main', + 'port' => '3000', + ]); + + $this->assertEquals([ + 'source_control_id' => '1', + 'repository' => 'org/repo', + 'branch' => 'main', + 'port' => '3000', + ], $fields); + } + + public function test_create_rules_contain_bun_version(): void + { + $rules = $this->siteType->createRules([]); + + $this->assertArrayHasKey('bun_version', $rules); + $this->assertArrayHasKey('source_control', $rules); + $this->assertArrayHasKey('repository', $rules); + $this->assertArrayHasKey('branch', $rules); + $this->assertArrayHasKey('port', $rules); + $this->assertArrayHasKey('build_command', $rules); + $this->assertArrayHasKey('start_command', $rules); + $this->assertArrayNotHasKey('package_manager', $rules); + } + + public function test_wrap_command_with_cd(): void + { + $reflection = new \ReflectionMethod($this->siteType, 'wrapCommand'); + + $command = $reflection->invoke($this->siteType, 'bun install', true); + + $this->assertStringStartsWith('bash -c "export PATH=', $command); + $this->assertStringContainsString('cd /home/testuser/example.com &&', $command); + $this->assertStringContainsString('bun install', $command); + } + + public function test_wrap_command_without_cd(): void + { + $reflection = new \ReflectionMethod($this->siteType, 'wrapCommand'); + + $command = $reflection->invoke($this->siteType, 'bun install', false); + + $this->assertStringStartsWith('bash -c "export PATH=', $command); + $this->assertStringContainsString('bun install', $command); + $this->assertStringNotContainsString('cd /home/testuser/example.com', $command); + } + + public function test_worker_environment_contains_path(): void + { + $reflection = new \ReflectionMethod($this->siteType, 'workerEnvironment'); + + $environment = $reflection->invoke($this->siteType); + + $this->assertIsArray($environment); + $this->assertArrayHasKey('PATH', $environment); + $this->assertStringContainsString('/home/testuser/.local/share/mise/shims', $environment['PATH']); + } + + public function test_base_commands_returns_empty_array(): void + { + $this->assertEquals([], $this->siteType->baseCommands()); + } +}