diff --git a/config/nativephp-internal.php b/config/nativephp-internal.php index 4210df92..a05783ae 100644 --- a/config/nativephp-internal.php +++ b/config/nativephp-internal.php @@ -29,4 +29,10 @@ * The URL to the NativePHP API. */ 'api_url' => env('NATIVEPHP_API_URL', 'http://localhost:4000/api/'), + + 'zephpyr' => [ + 'host' => env('ZEPHPYR_HOST', 'zephpyr.com'), + 'token' => env('ZEPHPYR_TOKEN'), + 'key' => env('ZEPHPYR_KEY'), + ], ]; diff --git a/config/nativephp.php b/config/nativephp.php index b24afec0..08b0099b 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -6,7 +6,7 @@ * It is used to determine if the app needs to be updated. * Increment this value every time you release a new version of your app. */ - 'version' => env('NATIVEPHP_APP_VERSION', '1.0.0'), + 'version' => env('NATIVEPHP_APP_VERSION', 1), /** * The ID of your application. This should be a unique identifier @@ -47,6 +47,7 @@ 'AWS_*', 'GITHUB_*', 'DO_SPACES_*', + 'ZEPHPYR_*', '*_SECRET', 'NATIVEPHP_UPDATER_PATH', 'NATIVEPHP_APPLE_ID', diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php new file mode 100644 index 00000000..e2e2e76e --- /dev/null +++ b/src/Commands/BundleCommand.php @@ -0,0 +1,167 @@ +key = config('nativephp-internal.zephpyr.key'); + + if (! $this->key) { + $this->line(''); + $this->warn('No ZEPHPYR_KEY found. Cannot bundle!'); + $this->line(''); + $this->line('Add this app\'s ZEPHPYR_KEY to its .env file:'); + $this->line(base_path('.env')); + $this->line(''); + $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); + $this->info('Check out https://zephpyr.com'); + $this->line(''); + + return static::FAILURE; + } + + if ($this->option('fetch')) { + if (! $this->fetchLatestBundle()) { + $this->warn('Latest bundle not yet available. Try again soon.'); + + return static::FAILURE; + } + + $this->info('Latest bundle downloaded.'); + + return static::SUCCESS; + } + + // Package the app up into a zip + if (! $this->zipApplication()) { + $this->error("Failed to create zip archive at {$this->zipPath}."); + + return static::FAILURE; + } + + // Send the zip file + dd($result = $this->sendToZephpyr()); + + if ($result->failed()) { + $this->error("Failed to upload zip [{$this->zipPath}] to Zephpyr."); + + return static::FAILURE; + } + + @unlink($this->zipPath); + + $this->info('Successfully uploaded to Zephpyr.'); + $this->line('Use native:bundle --fetch to retrieve the latest bundle.'); + + return static::SUCCESS; + } + + private function zipApplication(): bool + { + $this->zipName = 'app_'.str()->random(8).'.zip'; + $this->zipPath = storage_path($this->zipName); + + $zip = new ZipArchive; + + if ($zip->open($this->zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + return false; + } + + $this->prepareNativeEnv(); + + $this->addFilesToZip($zip); + + $zip->close(); + + $this->restoreWebEnv(); + + return true; + } + + private function addFilesToZip(ZipArchive $zip): void + { + // TODO: Check the composer.json to make sure there are no symlinked or private packages as these will be a + // pain later + + $app = (new Finder)->files() + ->followLinks() + ->ignoreVCSIgnored(true) + ->in(base_path()) + ->exclude([ + 'vendor', + 'dist', + 'build', + 'tests', + ...config('nativephp.cleanup_exclude_files', []), + ]); + + $this->finderToZip($app, $zip); + + $vendor = (new Finder)->files() + ->exclude([ + 'nativephp/php-bin', + 'nativephp/electron/resources/js', + 'nativephp/*/vendor', + ]) + ->in(base_path('vendor')); + + $this->finderToZip($vendor, $zip, 'vendor'); + + $nodeModules = (new Finder)->files() + ->in(base_path('node_modules')); + + $this->finderToZip($nodeModules, $zip, 'node_modules'); + } + + private function finderToZip(Finder $finder, ZipArchive $zip, ?string $path = null): void + { + foreach ($finder as $file) { + if ($file->getRealPath() === false) { + continue; + } + + $zip->addFile($file->getRealPath(), str($path)->finish(DIRECTORY_SEPARATOR).$file->getRelativePathname()); + } + } + + private function sendToZephpyr() + { + return Http::withToken(config('nativephp-internal.zephpyr.token')) + ->attach('archive', fopen($this->zipPath, 'r'), $this->zipName) + ->post(str(config('nativephp-internal.zephpyr.host'))->finish('/').'api/build/'.$this->key); + } + + private function fetchLatestBundle(): bool + { + $response = Http::withToken(config('nativephp-internal.zephpyr.token')) + ->get(str(config('nativephp-internal.zephpyr.host'))->finish('/').'api/download/'.$this->key); + + if ($response->failed()) { + return false; + } + + file_put_contents(base_path('build/__nativephp_app_bundle'), $response->body()); + + return true; + } +} diff --git a/src/Commands/MinifyApplicationCommand.php b/src/Commands/MinifyApplicationCommand.php deleted file mode 100644 index 37d8378b..00000000 --- a/src/Commands/MinifyApplicationCommand.php +++ /dev/null @@ -1,111 +0,0 @@ -argument('app')); - - if (! is_dir($appPath)) { - $this->error('The app path is not a directory'); - - return; - } - - $this->info('Minifying application…'); - - $this->cleanUpEnvFile($appPath); - $this->removeIgnoredFilesAndFolders($appPath); - - $compactor = new Php; - - $phpFiles = Finder::create() - ->files() - ->name('*.php') - ->in($appPath); - - foreach ($phpFiles as $phpFile) { - $minifiedContent = $compactor->compact($phpFile->getRealPath(), $phpFile->getContents()); - file_put_contents($phpFile->getRealPath(), $minifiedContent); - } - } - - protected function cleanUpEnvFile(string $appPath): void - { - $envFile = $appPath.'/.env'; - - if (! file_exists($envFile)) { - return; - } - - $this->info('Cleaning up .env file…'); - - $cleanUpKeys = config('nativephp.cleanup_env_keys', []); - - $envContent = file_get_contents($envFile); - $envValues = collect(explode("\n", $envContent)) - ->filter(function (string $line) use ($cleanUpKeys) { - $key = Str::before($line, '='); - - return ! Str::is($cleanUpKeys, $key); - }) - ->join("\n"); - - file_put_contents($envFile, $envValues); - } - - protected function removeIgnoredFilesAndFolders(string $appPath): void - { - $this->info('Cleaning up ignored files and folders…'); - - $itemsToRemove = config('nativephp.cleanup_exclude_files', []); - - foreach ($itemsToRemove as $item) { - $fullPath = $appPath.'/'.$item; - - if (file_exists($fullPath)) { - if (is_dir($fullPath)) { - $this->deleteDirectoryRecursive($fullPath); - } else { - array_map('unlink', glob($fullPath)); - } - } else { - foreach (glob($item) as $pathFound) { - unlink($pathFound); - } - } - } - } - - private function deleteDirectoryRecursive(string $directory): bool - { - if (! file_exists($directory)) { - return true; - } - - if (! is_dir($directory)) { - return unlink($directory); - } - - foreach (scandir($directory) as $item) { - if ($item == '.' || $item == '..') { - continue; - } - - if (! $this->deleteDirectoryRecursive($directory.'/'.$item)) { - return false; - } - } - - return rmdir($directory); - } -} diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 97017662..9d14d56b 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -8,11 +8,11 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Native\Laravel\ChildProcess as ChildProcessImplementation; +use Native\Laravel\Commands\BundleCommand; use Native\Laravel\Commands\FreshCommand; use Native\Laravel\Commands\LoadPHPConfigurationCommand; use Native\Laravel\Commands\LoadStartupConfigurationCommand; use Native\Laravel\Commands\MigrateCommand; -use Native\Laravel\Commands\MinifyApplicationCommand; use Native\Laravel\Commands\SeedDatabaseCommand; use Native\Laravel\Contracts\ChildProcess as ChildProcessContract; use Native\Laravel\Contracts\GlobalShortcut as GlobalShortcutContract; @@ -35,7 +35,7 @@ public function configurePackage(Package $package): void MigrateCommand::class, FreshCommand::class, SeedDatabaseCommand::class, - MinifyApplicationCommand::class, + BundleCommand::class, ]) ->hasConfigFile() ->hasRoute('api')