From 2bdf295d141f0fbce963bd3fb3384ef780eccb89 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 4 Mar 2025 17:18:01 +0100 Subject: [PATCH 01/28] feat: native:bundle command --- composer.json | 3 +- phpstan.neon | 3 +- src/Commands/BundleCommand.php | 376 ++++++++++++++++++++++ src/ElectronServiceProvider.php | 2 + src/Traits/CopiesCertificateAuthority.php | 1 + src/Traits/HandleApiRequests.php | 62 ++++ 6 files changed, 444 insertions(+), 3 deletions(-) create mode 100644 src/Commands/BundleCommand.php create mode 100644 src/Traits/HandleApiRequests.php diff --git a/composer.json b/composer.json index 224f70e3..776d2ce3 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "nativephp/laravel": "dev-main", "nativephp/php-bin": "^0.5.1", "spatie/laravel-package-tools": "^1.16.4", - "symfony/filesystem": "^6.4|^7.2" + "symfony/filesystem": "^6.4|^7.2", + "ext-zip": "*" }, "require-dev": { "laravel/pint": "^1.0", diff --git a/phpstan.neon b/phpstan.neon index b02c336c..9c47a6b8 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,6 @@ parameters: - src - config - database - tmpDir: build/phpstan checkOctaneCompatibility: true checkModelProperties: true - + noEnvCallsOutsideOfConfig: false diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php new file mode 100644 index 00000000..b060a366 --- /dev/null +++ b/src/Commands/BundleCommand.php @@ -0,0 +1,376 @@ +checkForZephpyrKey()) { + return static::FAILURE; + } + + // Check for ZEPHPYR_TOKEN + if (! $this->checkForZephpyrToken()) { + return static::FAILURE; + } + + // Check if the token is valid + if (! $this->checkAuthenticated()) { + $this->error('Invalid API token: check your ZEPHPYR_TOKEN on '.$this->baseUrl().'user/api-tokens'); + + return static::FAILURE; + } + + // Download the latest bundle if requested + if ($this->option('fetch')) { + if (! $this->fetchLatestBundle()) { + + return static::FAILURE; + } + + $this->info('Latest bundle downloaded.'); + + return static::SUCCESS; + } + + $this->preProcess(); + + $this->setAppName(slugify: true); + intro('Copying App to build directory...'); + + // We update composer.json later, + $this->copyToBuildDirectory(); + + $this->newLine(); + intro('Cleaning .env file...'); + $this->cleanEnvFile(); + + $this->newLine(); + intro('Copying app icons...'); + $this->installIcon(); + + $this->newLine(); + intro('Pruning vendor directory'); + $this->pruneVendorDirectory(); + + // Check composer.json for symlinked or private packages + if (! $this->checkComposerJson()) { + return static::FAILURE; + } + + // 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 + $result = $this->sendToZephpyr(); + $this->handleApiErrors($result); + + // Success + $this->info('Successfully uploaded to Zephpyr.'); + $this->line('Use native:bundle --fetch to retrieve the latest bundle.'); + + // Clean up temp files + $this->cleanUp(); + + return static::SUCCESS; + } + + private function zipApplication(): bool + { + $this->zipName = 'app_'.str()->random(8).'.zip'; + $this->zipPath = $this->zipPath($this->zipName); + + // Create zip path + if (! @mkdir(dirname($this->zipPath), recursive: true) && ! is_dir(dirname($this->zipPath))) { + return false; + } + + $zip = new ZipArchive; + + if ($zip->open($this->zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + return false; + } + + $this->cleanEnvFile(); + + $this->addFilesToZip($zip); + + $zip->close(); + + return true; + } + + private function checkComposerJson(): bool + { + $composerJson = json_decode(file_get_contents($this->buildPath('composer.json')), true); + + // // Fail if there is symlinked packages + // foreach ($composerJson['repositories'] ?? [] as $repository) { + // + // $symlinked = $repository['options']['symlink'] ?? true; + // if ($repository['type'] === 'path' && $symlinked) { + // $this->error('Symlinked packages are not supported. Please remove them from your composer.json.'); + // + // return false; + // } + // // Work with private packages but will not in the future + // // elseif ($repository['type'] === 'composer') { + // // if (! $this->checkComposerPackageAuth($repository['url'])) { + // // $this->error('Cannot authenticate with '.$repository['url'].'.'); + // // $this->error('Go to '.$this->baseUrl().' and add your composer package credentials.'); + // // + // // return false; + // // } + // // } + // } + + // Remove repositories with type path + if (! empty($composerJson['repositories'])) { + + $this->newLine(); + intro('Patching composer.json in development mode…'); + + $filteredRepo = array_filter($composerJson['repositories'], fn ($repository) => $repository['type'] !== 'path'); + + if (count($filteredRepo) !== count($composerJson['repositories'])) { + $composerJson['repositories'] = $filteredRepo; + file_put_contents($this->buildPath('composer.json'), json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + Process::path($this->buildPath()) + ->run('composer update --no-dev', function (string $type, string $output) { + echo $output; + }); + } + + } + + return true; + } + + // private function checkComposerPackageAuth(string $repositoryUrl): bool + // { + // // Check if the user has authenticated the package on Zephpyr + // $host = parse_url($repositoryUrl, PHP_URL_HOST); + // $this->line('Checking '.$host.' authentication…'); + // + // return Http::acceptJson() + // ->withToken(config('nativephp-internal.zephpyr.token')) + // ->get($this->baseUrl().'api/v1/project/'.$this->key.'/composer/auth/'.$host) + // ->successful(); + // } + + private function addFilesToZip(ZipArchive $zip): void + { + $this->newLine(); + intro('Creating zip archive…'); + + $app = (new Finder)->files() + ->followLinks() + ->ignoreVCSIgnored(true) + ->in($this->buildPath()) + ->exclude([ + // We add those a few lines below + 'vendor', + 'node_modules', + + // Exclude the following directories + 'dist', // Compiled nativephp assets + 'build', // Compiled box assets + 'temp', // Temp files + 'tests', // Tests + + // TODO: include everything in the .gitignore file + + ...config('nativephp.cleanup_exclude_files', []), // User defined + ]); + + $this->finderToZip($app, $zip); + + // Add .env file manually because Finder ignores hidden files + $zip->addFile($this->buildPath('.env'), '.env'); + + // Add auth.json file to support private packages + // WARNING: Only for testing purposes, don't uncomment this + // $zip->addFile($this->buildPath('auth.json'), 'auth.json'); + + // Custom binaries + $binaryPath = Str::replaceStart($this->buildPath('vendor'), '', config('nativephp.binary_path')); + + // Add composer dependencies without unnecessary files + $vendor = (new Finder)->files() + ->exclude(array_filter([ + 'nativephp/php-bin', + 'nativephp/electron/resources/js', + '*/*/vendor', // Exclude sub-vendor directories + $binaryPath, + ])) + ->in($this->buildPath('vendor')); + + $this->finderToZip($vendor, $zip, 'vendor'); + + // Add javascript dependencies + if (file_exists($this->buildPath('node_modules'))) { + $nodeModules = (new Finder)->files() + ->in($this->buildPath('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() + { + intro('Uploading zip to Zephpyr…'); + + return Http::acceptJson() + ->timeout(300) // 5 minutes + ->withoutRedirecting() // Upload won't work if we follow redirects (it transform POST to GET) + ->withToken(config('nativephp-internal.zephpyr.token')) + ->attach('archive', fopen($this->zipPath, 'r'), $this->zipName) + ->post($this->baseUrl().'api/v1/project/'.$this->key.'/build/'); + } + + private function fetchLatestBundle(): bool + { + intro('Fetching latest bundle…'); + + $response = Http::acceptJson() + ->withToken(config('nativephp-internal.zephpyr.token')) + ->get($this->baseUrl().'api/v1/project/'.$this->key.'/build/download'); + + if ($response->failed()) { + + if ($response->status() === 404) { + $this->error('Project or bundle not found.'); + } elseif ($response->status() === 500) { + $this->error('Build failed. Please try again later.'); + } elseif ($response->status() === 503) { + $this->warn('Bundle not ready. Please try again in '.now()->addSeconds(intval($response->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.'); + } else { + $this->handleApiErrors($response); + } + + return false; + } + + // Save the bundle + @mkdir(base_path('build'), recursive: true); + file_put_contents(base_path('build/__nativephp_app_bundle'), $response->body()); + + return true; + } + + protected function exitWithMessage(string $message): void + { + $this->error($message); + $this->cleanUp(); + + exit(static::FAILURE); + } + + private function handleApiErrors(Response $result): void + { + if ($result->status() === 413) { + $fileSize = Number::fileSize(filesize($this->zipPath)); + $this->exitWithMessage('File is too large to upload ('.$fileSize.'). Please contact support.'); + } elseif ($result->status() === 422) { + $this->error('Request refused:'.$result->json('message')); + } elseif ($result->status() === 429) { + $this->exitWithMessage('Too many requests. Please try again in '.now()->addSeconds(intval($result->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.'); + } elseif ($result->failed()) { + $this->exitWithMessage("Request failed. Error code: {$result->status()}"); + } + } + + protected function cleanUp(): void + { + $this->postProcess(); + + if ($this->option('without-cleanup')) { + return; + } + + $previousBuilds = glob($this->zipPath().'/app_*.zip'); + $failedZips = glob($this->zipPath().'/app_*.part'); + + $deleteFiles = array_merge($previousBuilds, $failedZips); + + if (empty($deleteFiles)) { + return; + } + + $this->line('Cleaning up…'); + + foreach ($deleteFiles as $file) { + @unlink($file); + } + } + + protected function buildPath(string $path = ''): string + { + return base_path('temp/build/'.$path); + } + + protected function zipPath(string $path = ''): string + { + return base_path('temp/zip/'.$path); + } + + protected function sourcePath(string $path = ''): string + { + return base_path($path); + } +} diff --git a/src/ElectronServiceProvider.php b/src/ElectronServiceProvider.php index 9e609362..0d69cff5 100644 --- a/src/ElectronServiceProvider.php +++ b/src/ElectronServiceProvider.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Application; use Native\Electron\Commands\BuildCommand; +use Native\Electron\Commands\BundleCommand; use Native\Electron\Commands\DevelopCommand; use Native\Electron\Commands\InstallCommand; use Native\Electron\Commands\PublishCommand; @@ -23,6 +24,7 @@ public function configurePackage(Package $package): void DevelopCommand::class, BuildCommand::class, PublishCommand::class, + BundleCommand::class, ]); } diff --git a/src/Traits/CopiesCertificateAuthority.php b/src/Traits/CopiesCertificateAuthority.php index b8ed6486..ce627cce 100644 --- a/src/Traits/CopiesCertificateAuthority.php +++ b/src/Traits/CopiesCertificateAuthority.php @@ -18,6 +18,7 @@ protected function copyCertificateAuthorityCertificate(): void $phpBinaryDirectory = base_path('vendor/nativephp/php-bin/'); // Check if the class this trait is used in also uses LocatesPhpBinary + /* @phpstan-ignore-next-line */ if (method_exists($this, 'phpBinaryPath')) { // Get binary directory but up one level $phpBinaryDirectory = dirname(base_path($this->phpBinaryPath())); diff --git a/src/Traits/HandleApiRequests.php b/src/Traits/HandleApiRequests.php new file mode 100644 index 00000000..5a40fd23 --- /dev/null +++ b/src/Traits/HandleApiRequests.php @@ -0,0 +1,62 @@ +finish('/'); + } + + private function checkAuthenticated() + { + $this->line('Checking authentication…'); + + return Http::acceptJson() + ->withToken(config('nativephp-internal.zephpyr.token')) + ->get($this->baseUrl().'api/v1/user')->successful(); + } + + private function checkForZephpyrKey() + { + $this->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 '.$this->baseUrl().''); + $this->line(''); + + return false; + } + + return true; + } + + private function checkForZephpyrToken() + { + if (! config('nativephp-internal.zephpyr.token')) { + $this->line(''); + $this->warn('No ZEPHPYR_TOKEN found. Cannot bundle!'); + $this->line(''); + $this->line('Add your api ZEPHPYR_TOKEN 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 '.$this->baseUrl().''); + $this->line(''); + + return false; + } + + return true; + } +} From 08a37269a3fc7483dce8070288ae9933cd259301 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 4 Mar 2025 17:26:11 +0100 Subject: [PATCH 02/28] feat: native:bundle --clear --- src/Commands/BundleCommand.php | 13 ++++++++++++- src/Traits/HandleApiRequests.php | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index b060a366..68ae4563 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -31,7 +31,7 @@ class BundleCommand extends Command use PrunesVendorDirectory; use SetsAppName; - protected $signature = 'native:bundle {--fetch} {--without-cleanup}'; + protected $signature = 'native:bundle {--fetch} {--clear} {--without-cleanup}'; protected $description = 'Bundle your application for distribution.'; @@ -43,6 +43,17 @@ class BundleCommand extends Command public function handle(): int { + // Remove the bundle + if ($this->option('clear')) { + if (file_exists(base_path('build/__nativephp_app_bundle'))) { + unlink(base_path('build/__nativephp_app_bundle')); + } + + $this->info('Bundle removed. Building in this state would be unsecure.'); + + return static::SUCCESS; + } + // Check for ZEPHPYR_KEY if (! $this->checkForZephpyrKey()) { return static::FAILURE; diff --git a/src/Traits/HandleApiRequests.php b/src/Traits/HandleApiRequests.php index 5a40fd23..9c5e2b5e 100644 --- a/src/Traits/HandleApiRequests.php +++ b/src/Traits/HandleApiRequests.php @@ -4,6 +4,8 @@ use Illuminate\Support\Facades\Http; +use function Laravel\Prompts\intro; + trait HandleApiRequests { private function baseUrl(): string @@ -13,7 +15,7 @@ private function baseUrl(): string private function checkAuthenticated() { - $this->line('Checking authentication…'); + intro('Checking authentication…'); return Http::acceptJson() ->withToken(config('nativephp-internal.zephpyr.token')) From a2f98889a18d00a5b9e9342f06d51b6c6bf09db6 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 4 Mar 2025 20:12:58 +0100 Subject: [PATCH 03/28] feat: php.ts run with bundle --- resources/js/electron-plugin/dist/index.js | 28 +-- .../dist/server/api/notification.js | 43 ++++- .../js/electron-plugin/dist/server/php.js | 59 ++++-- .../js/electron-plugin/src/server/php.ts | 180 +++++++++++------- 4 files changed, 210 insertions(+), 100 deletions(-) diff --git a/resources/js/electron-plugin/dist/index.js b/resources/js/electron-plugin/dist/index.js index a85a2cd5..a63ba8ad 100644 --- a/resources/js/electron-plugin/dist/index.js +++ b/resources/js/electron-plugin/dist/index.js @@ -67,16 +67,6 @@ class NativePHP { } event.preventDefault(); }); - if (process.platform === 'win32') { - app.on('second-instance', (event, commandLine, workingDirectory) => { - if (this.mainWindow) { - if (this.mainWindow.isMinimized()) - this.mainWindow.restore(); - this.mainWindow.focus(); - } - this.handleDeepLink(commandLine.pop()); - }); - } } bootstrapApp(app) { return __awaiter(this, void 0, void 0, function* () { @@ -135,12 +125,28 @@ class NativePHP { else { app.setAsDefaultProtocolClient(deepLinkProtocol); } - if (process.platform === 'win32') { + if (process.platform !== "darwin") { const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); return; } + else { + app.on("second-instance", (event, commandLine, workingDirectory) => { + if (this.mainWindow) { + if (this.mainWindow.isMinimized()) + this.mainWindow.restore(); + this.mainWindow.focus(); + } + notifyLaravel("events", { + event: "\\Native\\Laravel\\Events\\App\\OpenedFromURL", + payload: { + url: commandLine[commandLine.length - 1], + workingDirectory: workingDirectory, + }, + }); + }); + } } } } diff --git a/resources/js/electron-plugin/dist/server/api/notification.js b/resources/js/electron-plugin/dist/server/api/notification.js index 8433dcce..6058a5df 100644 --- a/resources/js/electron-plugin/dist/server/api/notification.js +++ b/resources/js/electron-plugin/dist/server/api/notification.js @@ -3,8 +3,9 @@ import { Notification } from 'electron'; import { notifyLaravel } from "../utils.js"; const router = express.Router(); router.post('/', (req, res) => { - const { title, body, subtitle, silent, icon, hasReply, timeoutType, replyPlaceholder, sound, urgency, actions, closeButtonText, toastXml, event: customEvent } = req.body; + const { title, body, subtitle, silent, icon, hasReply, timeoutType, replyPlaceholder, sound, urgency, actions, closeButtonText, toastXml, event: customEvent, reference, } = req.body; const eventName = customEvent !== null && customEvent !== void 0 ? customEvent : '\\Native\\Laravel\\Events\\Notifications\\NotificationClicked'; + const notificationReference = reference !== null && reference !== void 0 ? reference : (Date.now() + '.' + Math.random().toString(36).slice(2, 9)); const notification = new Notification({ title, body, @@ -22,11 +23,45 @@ router.post('/', (req, res) => { }); notification.on("click", (event) => { notifyLaravel('events', { - event: eventName, - payload: JSON.stringify(event) + event: eventName || '\\Native\\Laravel\\Events\\Notifications\\NotificationClicked', + payload: { + reference: notificationReference, + event: JSON.stringify(event), + }, + }); + }); + notification.on("action", (event, index) => { + notifyLaravel('events', { + event: '\\Native\\Laravel\\Events\\Notifications\\NotificationActionClicked', + payload: { + reference: notificationReference, + index, + event: JSON.stringify(event), + }, + }); + }); + notification.on("reply", (event, reply) => { + notifyLaravel('events', { + event: '\\Native\\Laravel\\Events\\Notifications\\NotificationReply', + payload: { + reference: notificationReference, + reply, + event: JSON.stringify(event), + }, + }); + }); + notification.on("close", (event) => { + notifyLaravel('events', { + event: '\\Native\\Laravel\\Events\\Notifications\\NotificationClosed', + payload: { + reference: notificationReference, + event: JSON.stringify(event), + }, }); }); notification.show(); - res.sendStatus(200); + res.status(200).json({ + reference: notificationReference, + }); }); export default router; diff --git a/resources/js/electron-plugin/dist/server/php.js b/resources/js/electron-plugin/dist/server/php.js index 06742711..b6c077ee 100644 --- a/resources/js/electron-plugin/dist/server/php.js +++ b/resources/js/electron-plugin/dist/server/php.js @@ -22,6 +22,13 @@ const databasePath = join(app.getPath('userData'), 'database'); const databaseFile = join(databasePath, 'database.sqlite'); const argumentEnv = getArgumentEnv(); const appPath = getAppPath(); +function runningSecureBuild() { + return existsSync(join(appPath, 'build', '__nativephp_app_bundle')); +} +function shouldMigrateDatabase(store) { + return store.get('migrated_version') !== app.getVersion() + && process.env.NODE_ENV !== 'development'; +} function getPhpPort() { return __awaiter(this, void 0, void 0, function* () { return yield getPort({ @@ -41,7 +48,11 @@ function retrievePhpIniSettings() { cwd: appPath, env }; - return yield promisify(execFile)(state.php, ['artisan', 'native:php-ini'], phpOptions); + let command = ['artisan', 'native:php-ini']; + if (runningSecureBuild()) { + command.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } + return yield promisify(execFile)(state.php, command, phpOptions); }); } function retrieveNativePHPConfig() { @@ -55,14 +66,24 @@ function retrieveNativePHPConfig() { cwd: appPath, env }; - return yield promisify(execFile)(state.php, ['artisan', 'native:config'], phpOptions); + let command = ['artisan', 'native:config']; + if (runningSecureBuild()) { + command.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } + return yield promisify(execFile)(state.php, command, phpOptions); }); } function callPhp(args, options, phpIniSettings = {}) { + if (args[0] === 'artisan' && runningSecureBuild()) { + args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } let iniSettings = Object.assign(getDefaultPhpIniSettings(), phpIniSettings); Object.keys(iniSettings).forEach(key => { args.unshift('-d', `${key}=${iniSettings[key]}`); }); + if (parseInt(process.env.SHELL_VERBOSITY) > 0) { + console.log('Calling PHP', state.php, args); + } return spawn(state.php, args, { cwd: options.cwd, env: Object.assign(Object.assign({}, process.env), options.env), @@ -116,6 +137,7 @@ function getDefaultEnvironmentVariables(secret, apiPort) { return { APP_ENV: process.env.NODE_ENV === 'development' ? 'local' : 'production', APP_DEBUG: process.env.NODE_ENV === 'development' ? 'true' : 'false', + LARAVEL_STORAGE_PATH: storagePath, NATIVEPHP_STORAGE_PATH: storagePath, NATIVEPHP_DATABASE_PATH: databaseFile, NATIVEPHP_API_URL: `http://localhost:${apiPort}/api/`, @@ -152,8 +174,10 @@ function serveApp(secret, apiPort, phpIniSettings) { env }; const store = new Store(); - callPhp(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings); - if (store.get('migrated_version') !== app.getVersion() && process.env.NODE_ENV !== 'development') { + if (!runningSecureBuild()) { + callPhp(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings); + } + if (shouldMigrateDatabase(store)) { console.log('Migrating database...'); callPhp(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings); store.set('migrated_version', app.getVersion()); @@ -162,40 +186,36 @@ function serveApp(secret, apiPort, phpIniSettings) { console.log('Skipping Database migration while in development.'); console.log('You may migrate manually by running: php artisan native:migrate'); } + console.log('Starting PHP server...'); const phpPort = yield getPhpPort(); - const serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php'); + let serverPath = join(appPath, 'build', '__nativephp_app_bundle'); + if (!runningSecureBuild()) { + console.log('* * * Running from source * * *'); + serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php'); + } const phpServer = callPhp(['-S', `127.0.0.1:${phpPort}`, serverPath], { cwd: join(appPath, 'public'), env }, phpIniSettings); const portRegex = /Development Server \(.*:([0-9]+)\) started/gm; phpServer.stdout.on('data', (data) => { - const match = portRegex.exec(data.toString()); - if (match) { - console.log("PHP Server started on port: ", match[1]); - const port = parseInt(match[1]); - resolve({ - port, - process: phpServer - }); - } }); phpServer.stderr.on('data', (data) => { const error = data.toString(); - const match = portRegex.exec(error); + const match = portRegex.exec(data.toString()); if (match) { const port = parseInt(match[1]); console.log("PHP Server started on port: ", port); resolve({ port, - process: phpServer + process: phpServer, }); } else { - if (error.startsWith('[NATIVE_EXCEPTION]: ', 27)) { + if (error.includes('[NATIVE_EXCEPTION]:')) { console.log(); console.error('Error in PHP:'); - console.error(' ' + error.slice(47)); + console.error(' ' + error.split('[NATIVE_EXCEPTION]:')[1].trim()); console.log('Please check your log file:'); console.log(' ' + join(appPath, 'storage', 'logs', 'laravel.log')); console.log(); @@ -205,6 +225,9 @@ function serveApp(secret, apiPort, phpIniSettings) { phpServer.on('error', (error) => { reject(error); }); + phpServer.on('close', (code) => { + console.log(`PHP server exited with code ${code}`); + }); })); } export { startScheduler, serveApp, getAppPath, retrieveNativePHPConfig, retrievePhpIniSettings, getDefaultEnvironmentVariables, getDefaultPhpIniSettings }; diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index 48b08a51..a1eb11e2 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -1,6 +1,7 @@ import {mkdirSync, statSync, writeFileSync, existsSync} from 'fs' import fs_extra from 'fs-extra'; -const { copySync } = fs_extra; + +const {copySync} = fs_extra; import Store from 'electron-store' import {promisify} from 'util' @@ -9,7 +10,7 @@ import {app} from 'electron' import {execFile, spawn} from 'child_process' import state from "./state.js"; import getPort, {portNumbers} from 'get-port'; -import { ProcessResult } from "./ProcessResult.js"; +import {ProcessResult} from "./ProcessResult.js"; const storagePath = join(app.getPath('userData'), 'storage') const databasePath = join(app.getPath('userData'), 'database') @@ -17,6 +18,15 @@ const databaseFile = join(databasePath, 'database.sqlite') const argumentEnv = getArgumentEnv(); const appPath = getAppPath(); +function runningSecureBuild() { + return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) +} + +function shouldMigrateDatabase(store) { + return store.get('migrated_version') !== app.getVersion() + && process.env.NODE_ENV !== 'development'; +} + async function getPhpPort() { return await getPort({ host: '127.0.0.1', @@ -25,18 +35,24 @@ async function getPhpPort() { } async function retrievePhpIniSettings() { - const env = { - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_STORAGE_PATH: storagePath, - NATIVEPHP_DATABASE_PATH: databaseFile, - }; - - const phpOptions = { - cwd: appPath, - env - }; - - return await promisify(execFile)(state.php, ['artisan', 'native:php-ini'], phpOptions); + const env = { + NATIVEPHP_RUNNING: 'true', + NATIVEPHP_STORAGE_PATH: storagePath, + NATIVEPHP_DATABASE_PATH: databaseFile, + }; + + const phpOptions = { + cwd: appPath, + env + }; + + let command = ['artisan', 'native:php-ini']; + + if (runningSecureBuild()) { + command.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } + + return await promisify(execFile)(state.php, command, phpOptions); } async function retrieveNativePHPConfig() { @@ -51,17 +67,31 @@ async function retrieveNativePHPConfig() { env }; - return await promisify(execFile)(state.php, ['artisan', 'native:config'], phpOptions); + let command = ['artisan', 'native:config']; + + if (runningSecureBuild()) { + command.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } + + return await promisify(execFile)(state.php, command, phpOptions); } function callPhp(args, options, phpIniSettings = {}) { + if (args[0] === 'artisan' && runningSecureBuild()) { + args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } + let iniSettings = Object.assign(getDefaultPhpIniSettings(), phpIniSettings); Object.keys(iniSettings).forEach(key => { - args.unshift('-d', `${key}=${iniSettings[key]}`); + args.unshift('-d', `${key}=${iniSettings[key]}`); }); + if (parseInt(process.env.SHELL_VERBOSITY) > 0) { + console.log('Calling PHP', state.php, args); + } + return spawn( state.php, args, @@ -79,11 +109,9 @@ function getArgumentEnv() { const envArgs = process.argv.filter(arg => arg.startsWith('--env.')); const env: { - TESTING?: number, - APP_PATH?: string - } = { - - }; + TESTING?: number, + APP_PATH?: string + } = {}; envArgs.forEach(arg => { const [key, value] = arg.slice(6).split('='); env[key] = value; @@ -102,7 +130,7 @@ function getAppPath() { } function ensureAppFoldersAreAvailable() { - if (! existsSync(storagePath) || process.env.NODE_ENV === 'development') { + if (!existsSync(storagePath) || process.env.NODE_ENV === 'development') { copySync(join(appPath, 'storage'), storagePath) } @@ -128,34 +156,35 @@ function startScheduler(secret, apiPort, phpIniSettings = {}) { } function getPath(name: string) { - try { - // @ts-ignore - return app.getPath(name); - } catch (error) { - return ''; - } + try { + // @ts-ignore + return app.getPath(name); + } catch (error) { + return ''; + } } function getDefaultEnvironmentVariables(secret, apiPort) { - return { - APP_ENV: process.env.NODE_ENV === 'development' ? 'local' : 'production', - APP_DEBUG: process.env.NODE_ENV === 'development' ? 'true' : 'false', - NATIVEPHP_STORAGE_PATH: storagePath, - NATIVEPHP_DATABASE_PATH: databaseFile, - NATIVEPHP_API_URL: `http://localhost:${apiPort}/api/`, - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_SECRET: secret, - NATIVEPHP_USER_HOME_PATH: getPath('home'), - NATIVEPHP_APP_DATA_PATH: getPath('appData'), - NATIVEPHP_USER_DATA_PATH: getPath('userData'), - NATIVEPHP_DESKTOP_PATH: getPath('desktop'), - NATIVEPHP_DOCUMENTS_PATH: getPath('documents'), - NATIVEPHP_DOWNLOADS_PATH: getPath('downloads'), - NATIVEPHP_MUSIC_PATH: getPath('music'), - NATIVEPHP_PICTURES_PATH: getPath('pictures'), - NATIVEPHP_VIDEOS_PATH: getPath('videos'), - NATIVEPHP_RECENT_PATH: getPath('recent'), - }; + return { + APP_ENV: process.env.NODE_ENV === 'development' ? 'local' : 'production', + APP_DEBUG: process.env.NODE_ENV === 'development' ? 'true' : 'false', + LARAVEL_STORAGE_PATH: storagePath, + NATIVEPHP_STORAGE_PATH: storagePath, + NATIVEPHP_DATABASE_PATH: databaseFile, + NATIVEPHP_API_URL: `http://localhost:${apiPort}/api/`, + NATIVEPHP_RUNNING: 'true', + NATIVEPHP_SECRET: secret, + NATIVEPHP_USER_HOME_PATH: getPath('home'), + NATIVEPHP_APP_DATA_PATH: getPath('appData'), + NATIVEPHP_USER_DATA_PATH: getPath('userData'), + NATIVEPHP_DESKTOP_PATH: getPath('desktop'), + NATIVEPHP_DOCUMENTS_PATH: getPath('documents'), + NATIVEPHP_DOWNLOADS_PATH: getPath('downloads'), + NATIVEPHP_MUSIC_PATH: getPath('music'), + NATIVEPHP_PICTURES_PATH: getPath('pictures'), + NATIVEPHP_VIDEOS_PATH: getPath('videos'), + NATIVEPHP_RECENT_PATH: getPath('recent'), + }; } function getDefaultPhpIniSettings() { @@ -187,10 +216,12 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { // Make sure the storage path is linked - as people can move the app around, we // need to run this every time the app starts - callPhp(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings) + if (!runningSecureBuild()) { + callPhp(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings) + } // Migrate the database - if (store.get('migrated_version') !== app.getVersion() && process.env.NODE_ENV !== 'development') { + if (shouldMigrateDatabase(store)) { console.log('Migrating database...') callPhp(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings) store.set('migrated_version', app.getVersion()) @@ -201,9 +232,17 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { console.log('You may migrate manually by running: php artisan native:migrate') } + console.log('Starting PHP server...'); const phpPort = await getPhpPort(); - const serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php') + + let serverPath = join(appPath, 'build', '__nativephp_app_bundle'); + + if (!runningSecureBuild()) { + console.log('* * * Running from source * * *'); + serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php'); + } + const phpServer = callPhp(['-S', `127.0.0.1:${phpPort}`, serverPath], { cwd: join(appPath, 'public'), env @@ -212,34 +251,29 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { const portRegex = /Development Server \(.*:([0-9]+)\) started/gm phpServer.stdout.on('data', (data) => { - const match = portRegex.exec(data.toString()) - if (match) { - console.log("PHP Server started on port: ", match[1]) - const port = parseInt(match[1]) - resolve({ - port, - process: phpServer - }) - } + // [Tue Jan 14 19:51:00 2025] 127.0.0.1:52779 [POST] URI: /_native/api/events + console.log('D:', data.toString()); }) + phpServer.stderr.on('data', (data) => { const error = data.toString(); - const match = portRegex.exec(error); - + const match = portRegex.exec(data.toString()); + // console.log('E:', error); if (match) { const port = parseInt(match[1]); console.log("PHP Server started on port: ", port); resolve({ port, - process: phpServer + process: phpServer, }); } else { - // 27 is the length of the php -S output preamble - if (error.startsWith('[NATIVE_EXCEPTION]: ', 27)) { + + // Starting at [NATIVE_EXCEPTION]: + if (error.includes('[NATIVE_EXCEPTION]:')) { console.log(); console.error('Error in PHP:'); - console.error(' ' + error.slice(47)); + console.error(' ' + error.split('[NATIVE_EXCEPTION]:')[1].trim()); console.log('Please check your log file:'); console.log(' ' + join(appPath, 'storage', 'logs', 'laravel.log')); console.log(); @@ -249,8 +283,20 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { phpServer.on('error', (error) => { reject(error) - }) + }); + + phpServer.on('close', (code) => { + console.log(`PHP server exited with code ${code}`); + }); }) } -export {startScheduler, serveApp, getAppPath, retrieveNativePHPConfig, retrievePhpIniSettings, getDefaultEnvironmentVariables, getDefaultPhpIniSettings} +export { + startScheduler, + serveApp, + getAppPath, + retrieveNativePHPConfig, + retrievePhpIniSettings, + getDefaultEnvironmentVariables, + getDefaultPhpIniSettings +} From 3535d22c76dec9610d7090f55d627146a040f876 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 4 Mar 2025 20:13:27 +0100 Subject: [PATCH 04/28] fix: require nativephp/laravel on dev-feat/bundle-builds --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 776d2ce3..fa2e3c46 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "php": "^8.1", "illuminate/contracts": "^10.0|^11.0|^12.0", "laravel/prompts": "^0.1.1|^0.2|^0.3", - "nativephp/laravel": "dev-main", + "nativephp/laravel": "dev-feat/bundle-builds", "nativephp/php-bin": "^0.5.1", "spatie/laravel-package-tools": "^1.16.4", "symfony/filesystem": "^6.4|^7.2", From b9c6ccf592d2fba306f680b235e52119538993db Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 4 Mar 2025 20:15:30 +0100 Subject: [PATCH 05/28] wip: bundle cached services? --- src/Commands/BundleCommand.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 68ae4563..84f78ce9 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -103,6 +103,8 @@ public function handle(): int intro('Pruning vendor directory'); $this->pruneVendorDirectory(); + $this->cleanEnvFile(); + // Check composer.json for symlinked or private packages if (! $this->checkComposerJson()) { return static::FAILURE; @@ -145,8 +147,6 @@ private function zipApplication(): bool return false; } - $this->cleanEnvFile(); - $this->addFilesToZip($zip); $zip->close(); @@ -184,11 +184,13 @@ private function checkComposerJson(): bool $this->newLine(); intro('Patching composer.json in development mode…'); - $filteredRepo = array_filter($composerJson['repositories'], fn ($repository) => $repository['type'] !== 'path'); + $filteredRepo = array_filter($composerJson['repositories'], + fn ($repository) => $repository['type'] !== 'path'); if (count($filteredRepo) !== count($composerJson['repositories'])) { $composerJson['repositories'] = $filteredRepo; - file_put_contents($this->buildPath('composer.json'), json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + file_put_contents($this->buildPath('composer.json'), + json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); Process::path($this->buildPath()) ->run('composer update --no-dev', function (string $type, string $output) { @@ -218,7 +220,7 @@ private function addFilesToZip(ZipArchive $zip): void $this->newLine(); intro('Creating zip archive…'); - $app = (new Finder)->files() + $finder = (new Finder)->files() ->followLinks() ->ignoreVCSIgnored(true) ->in($this->buildPath()) @@ -232,16 +234,15 @@ private function addFilesToZip(ZipArchive $zip): void 'build', // Compiled box assets 'temp', // Temp files 'tests', // Tests + ]) + ->exclude(config('nativephp.cleanup_exclude_files', [])); - // TODO: include everything in the .gitignore file - - ...config('nativephp.cleanup_exclude_files', []), // User defined - ]); - - $this->finderToZip($app, $zip); + $this->finderToZip($finder, $zip); - // Add .env file manually because Finder ignores hidden files + // Add .env file manually because Finder ignores VSC ignored files $zip->addFile($this->buildPath('.env'), '.env'); + $zip->addFile($this->buildPath('bootstrap/cache/services.php'), 'bootstrap/cache/services.php'); + $zip->addFile($this->buildPath('bootstrap/cache/packages.php'), 'bootstrap/cache/packages.php'); // Add auth.json file to support private packages // WARNING: Only for testing purposes, don't uncomment this From 37eca6ac5dbfdb4c65be455fa68948c4a22a2cfc Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 4 Mar 2025 20:20:19 +0100 Subject: [PATCH 06/28] wip: try with everything first --- src/Commands/BundleCommand.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 84f78ce9..8519c7dc 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -222,7 +222,7 @@ private function addFilesToZip(ZipArchive $zip): void $finder = (new Finder)->files() ->followLinks() - ->ignoreVCSIgnored(true) + // ->ignoreVCSIgnored(true) // TODO: Make our own list of ignored files ->in($this->buildPath()) ->exclude([ // We add those a few lines below @@ -240,9 +240,9 @@ private function addFilesToZip(ZipArchive $zip): void $this->finderToZip($finder, $zip); // Add .env file manually because Finder ignores VSC ignored files - $zip->addFile($this->buildPath('.env'), '.env'); - $zip->addFile($this->buildPath('bootstrap/cache/services.php'), 'bootstrap/cache/services.php'); - $zip->addFile($this->buildPath('bootstrap/cache/packages.php'), 'bootstrap/cache/packages.php'); + // $zip->addFile($this->buildPath('.env'), '.env'); + // $zip->addFile($this->buildPath('bootstrap/cache/services.php'), 'bootstrap/cache/services.php'); + // $zip->addFile($this->buildPath('bootstrap/cache/packages.php'), 'bootstrap/cache/packages.php'); // Add auth.json file to support private packages // WARNING: Only for testing purposes, don't uncomment this From 613d9558fb327e54694772e22d5d1cb3fe8680c5 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 4 Mar 2025 20:23:00 +0100 Subject: [PATCH 07/28] wip: fix .env missing --- src/Commands/BundleCommand.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 8519c7dc..3e29aee9 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -239,10 +239,8 @@ private function addFilesToZip(ZipArchive $zip): void $this->finderToZip($finder, $zip); - // Add .env file manually because Finder ignores VSC ignored files - // $zip->addFile($this->buildPath('.env'), '.env'); - // $zip->addFile($this->buildPath('bootstrap/cache/services.php'), 'bootstrap/cache/services.php'); - // $zip->addFile($this->buildPath('bootstrap/cache/packages.php'), 'bootstrap/cache/packages.php'); + // Add .env file manually because Finder ignores VCS and dot files + $zip->addFile($this->buildPath('.env'), '.env'); // Add auth.json file to support private packages // WARNING: Only for testing purposes, don't uncomment this From 666f68b06185c261dcfc5e8a6ade17b34ea4ca62 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Wed, 5 Mar 2025 14:16:24 +0100 Subject: [PATCH 08/28] fix: bundling --- .../js/electron-plugin/src/server/php.ts | 5 +++- src/Commands/BundleCommand.php | 27 ++++++++++++++----- src/Traits/CopiesToBuildDirectory.php | 4 +++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index a1eb11e2..5f25452f 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -252,7 +252,10 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { phpServer.stdout.on('data', (data) => { // [Tue Jan 14 19:51:00 2025] 127.0.0.1:52779 [POST] URI: /_native/api/events - console.log('D:', data.toString()); + + if (parseInt(process.env.SHELL_VERBOSITY) > 0) { + console.log(data.toString()); + } }) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 3e29aee9..8bf9941c 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -221,8 +221,8 @@ private function addFilesToZip(ZipArchive $zip): void intro('Creating zip archive…'); $finder = (new Finder)->files() - ->followLinks() - // ->ignoreVCSIgnored(true) // TODO: Make our own list of ignored files + // ->followLinks() + ->ignoreVCSIgnored(true) // TODO: Make our own list of ignored files ->in($this->buildPath()) ->exclude([ // We add those a few lines below @@ -239,6 +239,12 @@ private function addFilesToZip(ZipArchive $zip): void $this->finderToZip($finder, $zip); + // Why do I have to force this? please someone explain. + $this->finderToZip( + (new Finder)->files() + ->followLinks() + ->in($this->buildPath('public/build')), $zip, 'public/build'); + // Add .env file manually because Finder ignores VCS and dot files $zip->addFile($this->buildPath('.env'), '.env'); @@ -306,9 +312,18 @@ private function fetchLatestBundle(): bool if ($response->status() === 404) { $this->error('Project or bundle not found.'); } elseif ($response->status() === 500) { - $this->error('Build failed. Please try again later.'); + $url = $response->json('url'); + + if ($url) { + $this->error('Build failed. Inspect the build here: '.$url); + } else { + $this->error('Build failed. Please try again later.'); + } } elseif ($response->status() === 503) { - $this->warn('Bundle not ready. Please try again in '.now()->addSeconds(intval($response->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.'); + $retryAfter = intval($response->header('Retry-After')); + $diff = now()->addSeconds($retryAfter); + $diffMessage = $retryAfter <= 60 ? 'a minute' : $diff->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE); + $this->warn('Bundle not ready. Please try again in '.$diffMessage.'.'); } else { $this->handleApiErrors($response); } @@ -371,12 +386,12 @@ protected function cleanUp(): void protected function buildPath(string $path = ''): string { - return base_path('temp/build/'.$path); + return base_path('build/app/'.$path); } protected function zipPath(string $path = ''): string { - return base_path('temp/zip/'.$path); + return base_path('build/zip/'.$path); } protected function sourcePath(string $path = ''): string diff --git a/src/Traits/CopiesToBuildDirectory.php b/src/Traits/CopiesToBuildDirectory.php index f9627934..94c3f0e5 100644 --- a/src/Traits/CopiesToBuildDirectory.php +++ b/src/Traits/CopiesToBuildDirectory.php @@ -51,6 +51,10 @@ abstract protected function sourcePath(string $path = ''): string; // Also deleted in PrunesVendorDirectory after fresh composer install 'vendor/bin', + + // Exlude build & temp directory + 'build', + 'temp', ]; public function copyToBuildDirectory() From 130323a9af092f482aaaf616ec32e46128209158 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Wed, 5 Mar 2025 15:42:35 +0100 Subject: [PATCH 09/28] =?UTF-8?q?feat:=20bundling=20a=20base=20L12=20app?= =?UTF-8?q?=20works=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/electron-builder.js | 8 +--- src/Commands/BuildCommand.php | 7 +-- src/Commands/BundleCommand.php | 4 +- src/Traits/CopiesBundleToBuildDirectory.php | 50 +++++++++++++++++++++ src/Traits/CopiesToBuildDirectory.php | 15 ++++--- 5 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 src/Traits/CopiesBundleToBuildDirectory.php diff --git a/resources/js/electron-builder.js b/resources/js/electron-builder.js index ef6defdb..28bc1e6e 100644 --- a/resources/js/electron-builder.js +++ b/resources/js/electron-builder.js @@ -40,13 +40,7 @@ try { } if (isBuilding) { - console.log(); - console.log('==================================================================='); - console.log(' Building for ' + targetOs); - console.log('==================================================================='); - console.log(); - console.log('Updater config', updaterConfig); - console.log(); + console.log(' • updater config', updaterConfig); } export default { diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php index 20d6fd62..b582e86d 100644 --- a/src/Commands/BuildCommand.php +++ b/src/Commands/BuildCommand.php @@ -7,8 +7,8 @@ use Illuminate\Support\Str; use Native\Electron\Facades\Updater; use Native\Electron\Traits\CleansEnvFile; +use Native\Electron\Traits\CopiesBundleToBuildDirectory; use Native\Electron\Traits\CopiesCertificateAuthority; -use Native\Electron\Traits\CopiesToBuildDirectory; use Native\Electron\Traits\HasPreAndPostProcessing; use Native\Electron\Traits\InstallsAppIcon; use Native\Electron\Traits\LocatesPhpBinary; @@ -22,8 +22,8 @@ class BuildCommand extends Command { use CleansEnvFile; + use CopiesBundleToBuildDirectory; use CopiesCertificateAuthority; - use CopiesToBuildDirectory; use HasPreAndPostProcessing; use InstallsAppIcon; use LocatesPhpBinary; @@ -79,7 +79,8 @@ public function handle(): void $this->newLine(); intro('Copying App to build directory...'); - $this->copyToBuildDirectory(); + + $this->copyBundleToBuildDirectory(); $this->newLine(); $this->copyCertificateAuthorityCertificate(); diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 8bf9941c..3b13d672 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -222,10 +222,10 @@ private function addFilesToZip(ZipArchive $zip): void $finder = (new Finder)->files() // ->followLinks() - ->ignoreVCSIgnored(true) // TODO: Make our own list of ignored files + // ->ignoreVCSIgnored(true) // TODO: Make our own list of ignored files ->in($this->buildPath()) ->exclude([ - // We add those a few lines below + // We add those a few lines below and they are ignored by most .gitignore anyway 'vendor', 'node_modules', diff --git a/src/Traits/CopiesBundleToBuildDirectory.php b/src/Traits/CopiesBundleToBuildDirectory.php new file mode 100644 index 00000000..f005490f --- /dev/null +++ b/src/Traits/CopiesBundleToBuildDirectory.php @@ -0,0 +1,50 @@ +exists($this->sourcePath(self::$bundlePath)); + } + + public function copyBundleToBuildDirectory(): bool + { + if ($this->hasBundled()) { + + $this->line('Copying secure app bundle to build directory...'); + $this->line('From: '.realpath(dirname($this->sourcePath(self::$bundlePath)))); + $this->line('To: '.realpath(dirname($this->buildPath(self::$bundlePath)))); + + (new Filesystem)->copy( + $this->sourcePath(self::$bundlePath), + $this->buildPath(self::$bundlePath), + ); + + return true; + } + + $this->warnUnsecureBuild(); + + return $this->copyToBuildDirectory(); + } + + public function warnUnsecureBuild(): void + { + warning('==================================================================='); + warning(' * * * INSECURE BUILD * * *'); + warning('==================================================================='); + warning('Secure app bundle not found! Building with exposed source files.'); + warning('See https://nativephp.com/docs/publishing/building#security'); + warning('==================================================================='); + } +} diff --git a/src/Traits/CopiesToBuildDirectory.php b/src/Traits/CopiesToBuildDirectory.php index 94c3f0e5..4c9cf9d5 100644 --- a/src/Traits/CopiesToBuildDirectory.php +++ b/src/Traits/CopiesToBuildDirectory.php @@ -27,6 +27,8 @@ abstract protected function sourcePath(string $path = ''): string; // .git and dev directories '.git', 'dist', + 'build', + 'temp', 'docker', 'packages', '**/.github', @@ -51,13 +53,9 @@ abstract protected function sourcePath(string $path = ''): string; // Also deleted in PrunesVendorDirectory after fresh composer install 'vendor/bin', - - // Exlude build & temp directory - 'build', - 'temp', ]; - public function copyToBuildDirectory() + public function copyToBuildDirectory(): bool { $sourcePath = $this->sourcePath(); $buildPath = $this->buildPath(); @@ -73,7 +71,10 @@ public function copyToBuildDirectory() $filesystem->mkdir($buildPath); // A filtered iterator that will exclude files matching our skip patterns - $directory = new RecursiveDirectoryIterator($sourcePath, RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS); + $directory = new RecursiveDirectoryIterator( + $sourcePath, + RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS + ); $filter = new RecursiveCallbackFilterIterator($directory, function ($current) use ($patterns) { $relativePath = substr($current->getPathname(), strlen($this->sourcePath()) + 1); @@ -113,6 +114,8 @@ public function copyToBuildDirectory() } $this->keepRequiredDirectories(); + + return true; } private function keepRequiredDirectories() From e920c441ff815a8eb49a684b49b7f4f81c023140 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Mon, 10 Mar 2025 13:19:31 +0100 Subject: [PATCH 10/28] feat: native:reset command --- src/Commands/BuildCommand.php | 2 +- src/Commands/ResetCommand.php | 46 +++++++++++++++++++++ src/ElectronServiceProvider.php | 2 + src/Traits/CopiesBundleToBuildDirectory.php | 12 ++++-- 4 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 src/Commands/ResetCommand.php diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php index b582e86d..d27d2e91 100644 --- a/src/Commands/BuildCommand.php +++ b/src/Commands/BuildCommand.php @@ -40,7 +40,7 @@ class BuildCommand extends Command protected function buildPath(string $path = ''): string { - return __DIR__.'/../../resources/js/resources/app/'.$path; + return realpath(__DIR__.'/../../resources/js/resources/app/'.$path); } protected function sourcePath(string $path = ''): string diff --git a/src/Commands/ResetCommand.php b/src/Commands/ResetCommand.php new file mode 100644 index 00000000..4b99c8c3 --- /dev/null +++ b/src/Commands/ResetCommand.php @@ -0,0 +1,46 @@ +line('Clearing: '.$nativeServeResourcePath); + + $filesystem = new Filesystem; + $filesystem->remove($nativeServeResourcePath); + $filesystem->mkdir($nativeServeResourcePath); + + // Removing the bundling directories + $bundlingPath = base_path('build/'); + $this->line('Clearing: '.$bundlingPath); + + if ($filesystem->exists($bundlingPath)) { + $filesystem->remove($bundlingPath); + } + + // Removing the built path + $builtPath = base_path('dist/'); + $this->line('Clearing: '.$builtPath); + + if ($filesystem->exists($builtPath)) { + $filesystem->remove($builtPath); + } + + return 0; + } +} diff --git a/src/ElectronServiceProvider.php b/src/ElectronServiceProvider.php index 0d69cff5..568bdf2a 100644 --- a/src/ElectronServiceProvider.php +++ b/src/ElectronServiceProvider.php @@ -8,6 +8,7 @@ use Native\Electron\Commands\DevelopCommand; use Native\Electron\Commands\InstallCommand; use Native\Electron\Commands\PublishCommand; +use Native\Electron\Commands\ResetCommand; use Native\Electron\Updater\UpdaterManager; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -25,6 +26,7 @@ public function configurePackage(Package $package): void BuildCommand::class, PublishCommand::class, BundleCommand::class, + ResetCommand::class, ]); } diff --git a/src/Traits/CopiesBundleToBuildDirectory.php b/src/Traits/CopiesBundleToBuildDirectory.php index f005490f..0c84fe5e 100644 --- a/src/Traits/CopiesBundleToBuildDirectory.php +++ b/src/Traits/CopiesBundleToBuildDirectory.php @@ -25,10 +25,14 @@ public function copyBundleToBuildDirectory(): bool $this->line('From: '.realpath(dirname($this->sourcePath(self::$bundlePath)))); $this->line('To: '.realpath(dirname($this->buildPath(self::$bundlePath)))); - (new Filesystem)->copy( - $this->sourcePath(self::$bundlePath), - $this->buildPath(self::$bundlePath), - ); + $filesToCopy = [ + self::$bundlePath, + '.env', + ]; + $filesystem = new Filesystem; + foreach ($filesToCopy as $file) { + $filesystem->copy($this->sourcePath($file), $this->buildPath($file), true); + } return true; } From b3ecbf6df2220770df5a4ee8d7479809137c319f Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Mon, 10 Mar 2025 13:38:40 +0100 Subject: [PATCH 11/28] fix: realpath() returns false if the directory does not exists --- src/Commands/BuildCommand.php | 3 +-- src/Commands/ResetCommand.php | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php index d27d2e91..87134c47 100644 --- a/src/Commands/BuildCommand.php +++ b/src/Commands/BuildCommand.php @@ -40,7 +40,7 @@ class BuildCommand extends Command protected function buildPath(string $path = ''): string { - return realpath(__DIR__.'/../../resources/js/resources/app/'.$path); + return __DIR__.'/../../resources/js/resources/app/'.$path; } protected function sourcePath(string $path = ''): string @@ -79,7 +79,6 @@ public function handle(): void $this->newLine(); intro('Copying App to build directory...'); - $this->copyBundleToBuildDirectory(); $this->newLine(); diff --git a/src/Commands/ResetCommand.php b/src/Commands/ResetCommand.php index 4b99c8c3..3d94effc 100644 --- a/src/Commands/ResetCommand.php +++ b/src/Commands/ResetCommand.php @@ -9,7 +9,7 @@ class ResetCommand extends Command { - protected $signature = 'native:reset'; + protected $signature = 'native:reset {--with-app-data : Clear the app data as well}'; protected $description = 'Clear all build and dist files'; @@ -41,6 +41,38 @@ public function handle(): int $filesystem->remove($builtPath); } + if ($this->option('with-app-data')) { + + // Fetch last generated app name + $packageJsonPath = __DIR__.'/../../resources/js/package.json'; + $packageJson = json_decode(file_get_contents($packageJsonPath), true); + $appName = $packageJson['name']; + + $appDataPath = $this->appDataDirectory($appName); + $this->line('Clearing: '.$appDataPath); + + if ($filesystem->exists($appDataPath)) { + $filesystem->remove($appDataPath); + } + } + return 0; } + + protected function appDataDirectory(string $name): string + { + /* + * Platform Location + * macOS ~/Library/Application Support + * Linux $XDG_CONFIG_HOME or ~/.config + * Windows %APPDATA% + */ + + return match (PHP_OS_FAMILY) { + 'Darwin' => $_SERVER['HOME'].'/Library/Application Support/'.$name, + 'Linux' => $_SERVER['XDG_CONFIG_HOME'] ?? $_SERVER['HOME'].'/.config/'.$name, + 'Windows' => $_SERVER['APPDATA'].'/'.$name, + default => $_SERVER['HOME'].'/.config/'.$name, + }; + } } From bb7b06a5a5f9eca978d2efc5d7bb4450f23b5a45 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Mon, 10 Mar 2025 16:21:08 +0100 Subject: [PATCH 12/28] feat: failsafe --- src/Commands/ResetCommand.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Commands/ResetCommand.php b/src/Commands/ResetCommand.php index 3d94effc..36b3fe34 100644 --- a/src/Commands/ResetCommand.php +++ b/src/Commands/ResetCommand.php @@ -48,11 +48,14 @@ public function handle(): int $packageJson = json_decode(file_get_contents($packageJsonPath), true); $appName = $packageJson['name']; - $appDataPath = $this->appDataDirectory($appName); - $this->line('Clearing: '.$appDataPath); - - if ($filesystem->exists($appDataPath)) { - $filesystem->remove($appDataPath); + // Eh, just in case, I don't want to delete all user data by accident. + if (! empty($appName)) { + $appDataPath = $this->appDataDirectory($appName); + $this->line('Clearing: '.$appDataPath); + + if ($filesystem->exists($appDataPath)) { + $filesystem->remove($appDataPath); + } } } From 15de455e514ed2695c410b023d0a9b1d05607fcc Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Mon, 10 Mar 2025 19:00:01 +0100 Subject: [PATCH 13/28] wip: it works, but need some minor cleaning/refactoring/testing --- .../js/electron-plugin/dist/server/php.js | 46 ++++++++- .../js/electron-plugin/src/server/php.ts | 97 +++++++++++++++++-- src/Traits/CopiesBundleToBuildDirectory.php | 3 + 3 files changed, 135 insertions(+), 11 deletions(-) diff --git a/resources/js/electron-plugin/dist/server/php.js b/resources/js/electron-plugin/dist/server/php.js index b6c077ee..64f331f1 100644 --- a/resources/js/electron-plugin/dist/server/php.js +++ b/resources/js/electron-plugin/dist/server/php.js @@ -9,19 +9,24 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }; import { mkdirSync, statSync, writeFileSync, existsSync } from 'fs'; import fs_extra from 'fs-extra'; -const { copySync } = fs_extra; +const { copySync, mkdirpSync } = fs_extra; import Store from 'electron-store'; import { promisify } from 'util'; import { join } from 'path'; import { app } from 'electron'; -import { execFile, spawn } from 'child_process'; +import { execFile, spawn, spawnSync } from 'child_process'; import state from "./state.js"; import getPort, { portNumbers } from 'get-port'; const storagePath = join(app.getPath('userData'), 'storage'); const databasePath = join(app.getPath('userData'), 'database'); const databaseFile = join(databasePath, 'database.sqlite'); +const bootstrapCache = join(app.getPath('userData'), 'bootstrap', 'cache'); const argumentEnv = getArgumentEnv(); const appPath = getAppPath(); +mkdirpSync(bootstrapCache); +function runningProdVersion() { + return existsSync(join(appPath, 'build', '__nativephp_app_bundle')); +} function runningSecureBuild() { return existsSync(join(appPath, 'build', '__nativephp_app_bundle')); } @@ -89,6 +94,22 @@ function callPhp(args, options, phpIniSettings = {}) { env: Object.assign(Object.assign({}, process.env), options.env), }); } +function callPhpSync(args, options, phpIniSettings = {}) { + if (args[0] === 'artisan' && runningSecureBuild()) { + args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } + let iniSettings = Object.assign(getDefaultPhpIniSettings(), phpIniSettings); + Object.keys(iniSettings).forEach(key => { + args.unshift('-d', `${key}=${iniSettings[key]}`); + }); + if (parseInt(process.env.SHELL_VERBOSITY) > 0) { + console.log('Calling PHP', state.php, args); + } + return spawnSync(state.php, args, { + cwd: options.cwd, + env: Object.assign(Object.assign({}, process.env), options.env) + }); +} function getArgumentEnv() { const envArgs = process.argv.filter(arg => arg.startsWith('--env.')); const env = {}; @@ -134,7 +155,7 @@ function getPath(name) { } } function getDefaultEnvironmentVariables(secret, apiPort) { - return { + let variables = { APP_ENV: process.env.NODE_ENV === 'development' ? 'local' : 'production', APP_DEBUG: process.env.NODE_ENV === 'development' ? 'true' : 'false', LARAVEL_STORAGE_PATH: storagePath, @@ -154,6 +175,14 @@ function getDefaultEnvironmentVariables(secret, apiPort) { NATIVEPHP_VIDEOS_PATH: getPath('videos'), NATIVEPHP_RECENT_PATH: getPath('recent'), }; + if (runningProdVersion()) { + variables.APP_SERVICES_CACHE = join(bootstrapCache, 'services.php'); + variables.APP_PACKAGES_CACHE = join(bootstrapCache, 'packages.php'); + variables.APP_CONFIG_CACHE = join(bootstrapCache, 'config.php'); + variables.APP_ROUTES_CACHE = join(bootstrapCache, 'routes.php'); + variables.APP_EVENTS_CACHE = join(bootstrapCache, 'events.php'); + } + return variables; } function getDefaultPhpIniSettings() { return { @@ -175,11 +204,15 @@ function serveApp(secret, apiPort, phpIniSettings) { }; const store = new Store(); if (!runningSecureBuild()) { - callPhp(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings); + callPhpSync(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings); + } + if (runningProdVersion()) { + console.log('Caching view and routes...'); + callPhpSync(['artisan', 'optimize'], phpOptions, phpIniSettings); } if (shouldMigrateDatabase(store)) { console.log('Migrating database...'); - callPhp(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings); + callPhpSync(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings); store.set('migrated_version', app.getVersion()); } if (process.env.NODE_ENV === 'development') { @@ -199,6 +232,9 @@ function serveApp(secret, apiPort, phpIniSettings) { }, phpIniSettings); const portRegex = /Development Server \(.*:([0-9]+)\) started/gm; phpServer.stdout.on('data', (data) => { + if (parseInt(process.env.SHELL_VERBOSITY) > 0) { + console.log(data.toString()); + } }); phpServer.stderr.on('data', (data) => { const error = data.toString(); diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index 5f25452f..382b29a2 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -1,13 +1,13 @@ import {mkdirSync, statSync, writeFileSync, existsSync} from 'fs' import fs_extra from 'fs-extra'; -const {copySync} = fs_extra; +const {copySync, mkdirpSync} = fs_extra; import Store from 'electron-store' import {promisify} from 'util' import {join} from 'path' import {app} from 'electron' -import {execFile, spawn} from 'child_process' +import {execFile, spawn, spawnSync} from 'child_process' import state from "./state.js"; import getPort, {portNumbers} from 'get-port'; import {ProcessResult} from "./ProcessResult.js"; @@ -15,9 +15,17 @@ import {ProcessResult} from "./ProcessResult.js"; const storagePath = join(app.getPath('userData'), 'storage') const databasePath = join(app.getPath('userData'), 'database') const databaseFile = join(databasePath, 'database.sqlite') +const bootstrapCache = join(app.getPath('userData'), 'bootstrap', 'cache') const argumentEnv = getArgumentEnv(); const appPath = getAppPath(); +mkdirpSync(bootstrapCache); + +function runningProdVersion() { + //TODO: Check if the app is running the production version + return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) +} + function runningSecureBuild() { return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) } @@ -105,6 +113,35 @@ function callPhp(args, options, phpIniSettings = {}) { ); } +function callPhpSync(args, options, phpIniSettings = {}) { + + if (args[0] === 'artisan' && runningSecureBuild()) { + args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + } + + let iniSettings = Object.assign(getDefaultPhpIniSettings(), phpIniSettings); + + Object.keys(iniSettings).forEach(key => { + args.unshift('-d', `${key}=${iniSettings[key]}`); + }); + + if (parseInt(process.env.SHELL_VERBOSITY) > 0) { + console.log('Calling PHP', state.php, args); + } + + return spawnSync( + state.php, + args, + { + cwd: options.cwd, + env: { + ...process.env, + ...options.env + } + } + ); +} + function getArgumentEnv() { const envArgs = process.argv.filter(arg => arg.startsWith('--env.')); @@ -164,8 +201,37 @@ function getPath(name: string) { } } -function getDefaultEnvironmentVariables(secret, apiPort) { - return { +// Define an interface for the environment variables +interface EnvironmentVariables { + APP_ENV: string; + APP_DEBUG: string; + LARAVEL_STORAGE_PATH: string; + NATIVEPHP_STORAGE_PATH: string; + NATIVEPHP_DATABASE_PATH: string; + NATIVEPHP_API_URL: string; + NATIVEPHP_RUNNING: string; + NATIVEPHP_SECRET: string; + NATIVEPHP_USER_HOME_PATH: string; + NATIVEPHP_APP_DATA_PATH: string; + NATIVEPHP_USER_DATA_PATH: string; + NATIVEPHP_DESKTOP_PATH: string; + NATIVEPHP_DOCUMENTS_PATH: string; + NATIVEPHP_DOWNLOADS_PATH: string; + NATIVEPHP_MUSIC_PATH: string; + NATIVEPHP_PICTURES_PATH: string; + NATIVEPHP_VIDEOS_PATH: string; + NATIVEPHP_RECENT_PATH: string; + // Cache variables + APP_SERVICES_CACHE?: string; + APP_PACKAGES_CACHE?: string; + APP_CONFIG_CACHE?: string; + APP_ROUTES_CACHE?: string; + APP_EVENTS_CACHE?: string; +} + +function getDefaultEnvironmentVariables(secret, apiPort): EnvironmentVariables { + // Base variables with string values (no null values) + let variables: EnvironmentVariables = { APP_ENV: process.env.NODE_ENV === 'development' ? 'local' : 'production', APP_DEBUG: process.env.NODE_ENV === 'development' ? 'true' : 'false', LARAVEL_STORAGE_PATH: storagePath, @@ -185,6 +251,17 @@ function getDefaultEnvironmentVariables(secret, apiPort) { NATIVEPHP_VIDEOS_PATH: getPath('videos'), NATIVEPHP_RECENT_PATH: getPath('recent'), }; + + // Only add cache paths if in production mode + if(runningProdVersion()) { + variables.APP_SERVICES_CACHE = join(bootstrapCache, 'services.php'); + variables.APP_PACKAGES_CACHE = join(bootstrapCache, 'packages.php'); + variables.APP_CONFIG_CACHE = join(bootstrapCache, 'config.php'); + variables.APP_ROUTES_CACHE = join(bootstrapCache, 'routes.php'); + variables.APP_EVENTS_CACHE = join(bootstrapCache, 'events.php'); + } + + return variables; } function getDefaultPhpIniSettings() { @@ -217,13 +294,21 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { // Make sure the storage path is linked - as people can move the app around, we // need to run this every time the app starts if (!runningSecureBuild()) { - callPhp(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings) + callPhpSync(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings) + } + + // Cache the project + if (runningProdVersion()) { + console.log('Caching view and routes...'); + // TODO: once per version + callPhpSync(['artisan', 'optimize'], phpOptions, phpIniSettings) } // Migrate the database if (shouldMigrateDatabase(store)) { console.log('Migrating database...') - callPhp(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings) + callPhpSync(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings) + // TODO: fail if callPhp fails and don't store migrated version store.set('migrated_version', app.getVersion()) } diff --git a/src/Traits/CopiesBundleToBuildDirectory.php b/src/Traits/CopiesBundleToBuildDirectory.php index 0c84fe5e..ff59f212 100644 --- a/src/Traits/CopiesBundleToBuildDirectory.php +++ b/src/Traits/CopiesBundleToBuildDirectory.php @@ -25,6 +25,8 @@ public function copyBundleToBuildDirectory(): bool $this->line('From: '.realpath(dirname($this->sourcePath(self::$bundlePath)))); $this->line('To: '.realpath(dirname($this->buildPath(self::$bundlePath)))); + // TODO: copy only the files that you need. + $this->copyToBuildDirectory(); $filesToCopy = [ self::$bundlePath, '.env', @@ -33,6 +35,7 @@ public function copyBundleToBuildDirectory(): bool foreach ($filesToCopy as $file) { $filesystem->copy($this->sourcePath($file), $this->buildPath($file), true); } + // $this->keepRequiredDirectories(); return true; } From 118c85e8914a4f9e40134832694a744cb35151ee Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 11 Mar 2025 11:17:08 +0100 Subject: [PATCH 14/28] perf: fixes and perf of Laravel on subsequent launches --- .../js/electron-plugin/dist/server/php.js | 34 +++++++++++----- .../js/electron-plugin/src/server/php.ts | 40 ++++++++++++------- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/resources/js/electron-plugin/dist/server/php.js b/resources/js/electron-plugin/dist/server/php.js index 64f331f1..1bf14a1f 100644 --- a/resources/js/electron-plugin/dist/server/php.js +++ b/resources/js/electron-plugin/dist/server/php.js @@ -24,9 +24,6 @@ const bootstrapCache = join(app.getPath('userData'), 'bootstrap', 'cache'); const argumentEnv = getArgumentEnv(); const appPath = getAppPath(); mkdirpSync(bootstrapCache); -function runningProdVersion() { - return existsSync(join(appPath, 'build', '__nativephp_app_bundle')); -} function runningSecureBuild() { return existsSync(join(appPath, 'build', '__nativephp_app_bundle')); } @@ -34,6 +31,10 @@ function shouldMigrateDatabase(store) { return store.get('migrated_version') !== app.getVersion() && process.env.NODE_ENV !== 'development'; } +function shouldOptimize(store) { + return store.get('optimized_version') !== app.getVersion() + && process.env.NODE_ENV !== 'development'; +} function getPhpPort() { return __awaiter(this, void 0, void 0, function* () { return yield getPort({ @@ -175,11 +176,11 @@ function getDefaultEnvironmentVariables(secret, apiPort) { NATIVEPHP_VIDEOS_PATH: getPath('videos'), NATIVEPHP_RECENT_PATH: getPath('recent'), }; - if (runningProdVersion()) { + if (runningSecureBuild()) { variables.APP_SERVICES_CACHE = join(bootstrapCache, 'services.php'); variables.APP_PACKAGES_CACHE = join(bootstrapCache, 'packages.php'); variables.APP_CONFIG_CACHE = join(bootstrapCache, 'config.php'); - variables.APP_ROUTES_CACHE = join(bootstrapCache, 'routes.php'); + variables.APP_ROUTES_CACHE = join(bootstrapCache, 'routes-v7.php'); variables.APP_EVENTS_CACHE = join(bootstrapCache, 'events.php'); } return variables; @@ -202,18 +203,31 @@ function serveApp(secret, apiPort, phpIniSettings) { cwd: appPath, env }; - const store = new Store(); + const store = new Store({ + name: 'nativephp', + }); if (!runningSecureBuild()) { callPhpSync(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings); } - if (runningProdVersion()) { + if (shouldOptimize(store)) { console.log('Caching view and routes...'); - callPhpSync(['artisan', 'optimize'], phpOptions, phpIniSettings); + let result = callPhpSync(['artisan', 'optimize'], phpOptions, phpIniSettings); + if (result.status !== 0) { + console.error('Failed to cache view and routes:', result.stderr.toString()); + } + else { + store.set('optimized_version', app.getVersion()); + } } if (shouldMigrateDatabase(store)) { console.log('Migrating database...'); - callPhpSync(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings); - store.set('migrated_version', app.getVersion()); + let result = callPhpSync(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings); + if (result.status !== 0) { + console.error('Failed to migrate database:', result.stderr.toString()); + } + else { + store.set('migrated_version', app.getVersion()); + } } if (process.env.NODE_ENV === 'development') { console.log('Skipping Database migration while in development.'); diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index 382b29a2..0755981b 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -21,11 +21,6 @@ const appPath = getAppPath(); mkdirpSync(bootstrapCache); -function runningProdVersion() { - //TODO: Check if the app is running the production version - return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) -} - function runningSecureBuild() { return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) } @@ -35,6 +30,11 @@ function shouldMigrateDatabase(store) { && process.env.NODE_ENV !== 'development'; } +function shouldOptimize(store) { + return store.get('optimized_version') !== app.getVersion() + && process.env.NODE_ENV !== 'development'; +} + async function getPhpPort() { return await getPort({ host: '127.0.0.1', @@ -253,11 +253,11 @@ function getDefaultEnvironmentVariables(secret, apiPort): EnvironmentVariables { }; // Only add cache paths if in production mode - if(runningProdVersion()) { + if(runningSecureBuild()) { variables.APP_SERVICES_CACHE = join(bootstrapCache, 'services.php'); variables.APP_PACKAGES_CACHE = join(bootstrapCache, 'packages.php'); variables.APP_CONFIG_CACHE = join(bootstrapCache, 'config.php'); - variables.APP_ROUTES_CACHE = join(bootstrapCache, 'routes.php'); + variables.APP_ROUTES_CACHE = join(bootstrapCache, 'routes-v7.php'); variables.APP_EVENTS_CACHE = join(bootstrapCache, 'events.php'); } @@ -289,7 +289,9 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { env }; - const store = new Store(); + const store = new Store({ + name: 'nativephp', // So it doesn't conflict with settings of the app + }); // Make sure the storage path is linked - as people can move the app around, we // need to run this every time the app starts @@ -298,18 +300,28 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { } // Cache the project - if (runningProdVersion()) { + if (shouldOptimize(store)) { console.log('Caching view and routes...'); - // TODO: once per version - callPhpSync(['artisan', 'optimize'], phpOptions, phpIniSettings) + + let result = callPhpSync(['artisan', 'optimize'], phpOptions, phpIniSettings); + + if (result.status !== 0) { + console.error('Failed to cache view and routes:', result.stderr.toString()); + } else { + store.set('optimized_version', app.getVersion()) + } } // Migrate the database if (shouldMigrateDatabase(store)) { console.log('Migrating database...') - callPhpSync(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings) - // TODO: fail if callPhp fails and don't store migrated version - store.set('migrated_version', app.getVersion()) + let result = callPhpSync(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings); + + if (result.status !== 0) { + console.error('Failed to migrate database:', result.stderr.toString()); + } else { + store.set('migrated_version', app.getVersion()) + } } if (process.env.NODE_ENV === 'development') { From 5724b184e235db776fe2d0e9239e7d1af475a688 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 11 Mar 2025 12:07:31 +0100 Subject: [PATCH 15/28] fix: running the bundle in dev mode --- .../js/electron-plugin/dist/server/php.js | 20 +++++++----- .../js/electron-plugin/src/server/php.ts | 31 ++++++++++++------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/resources/js/electron-plugin/dist/server/php.js b/resources/js/electron-plugin/dist/server/php.js index 1bf14a1f..bd65878a 100644 --- a/resources/js/electron-plugin/dist/server/php.js +++ b/resources/js/electron-plugin/dist/server/php.js @@ -32,8 +32,7 @@ function shouldMigrateDatabase(store) { && process.env.NODE_ENV !== 'development'; } function shouldOptimize(store) { - return store.get('optimized_version') !== app.getVersion() - && process.env.NODE_ENV !== 'development'; + return store.get('optimized_version') !== app.getVersion(); } function getPhpPort() { return __awaiter(this, void 0, void 0, function* () { @@ -207,6 +206,7 @@ function serveApp(secret, apiPort, phpIniSettings) { name: 'nativephp', }); if (!runningSecureBuild()) { + console.log('Linking storage path...'); callPhpSync(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings); } if (shouldOptimize(store)) { @@ -235,19 +235,24 @@ function serveApp(secret, apiPort, phpIniSettings) { } console.log('Starting PHP server...'); const phpPort = yield getPhpPort(); - let serverPath = join(appPath, 'build', '__nativephp_app_bundle'); - if (!runningSecureBuild()) { + let serverPath; + let cwd; + if (runningSecureBuild()) { + serverPath = join(appPath, 'build', '__nativephp_app_bundle'); + } + else { console.log('* * * Running from source * * *'); serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php'); + cwd = join(appPath, 'public'); } const phpServer = callPhp(['-S', `127.0.0.1:${phpPort}`, serverPath], { - cwd: join(appPath, 'public'), + cwd: cwd, env }, phpIniSettings); const portRegex = /Development Server \(.*:([0-9]+)\) started/gm; phpServer.stdout.on('data', (data) => { if (parseInt(process.env.SHELL_VERBOSITY) > 0) { - console.log(data.toString()); + console.log(data.toString().trim()); } }); phpServer.stderr.on('data', (data) => { @@ -263,11 +268,12 @@ function serveApp(secret, apiPort, phpIniSettings) { } else { if (error.includes('[NATIVE_EXCEPTION]:')) { + let logFile = join(storagePath, 'logs', 'laravel.log'); console.log(); console.error('Error in PHP:'); console.error(' ' + error.split('[NATIVE_EXCEPTION]:')[1].trim()); console.log('Please check your log file:'); - console.log(' ' + join(appPath, 'storage', 'logs', 'laravel.log')); + console.log(' ' + logFile); console.log(); } } diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index 0755981b..d0834aad 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -31,8 +31,7 @@ function shouldMigrateDatabase(store) { } function shouldOptimize(store) { - return store.get('optimized_version') !== app.getVersion() - && process.env.NODE_ENV !== 'development'; + return store.get('optimized_version') !== app.getVersion(); } async function getPhpPort() { @@ -253,7 +252,7 @@ function getDefaultEnvironmentVariables(secret, apiPort): EnvironmentVariables { }; // Only add cache paths if in production mode - if(runningSecureBuild()) { + if (runningSecureBuild()) { variables.APP_SERVICES_CACHE = join(bootstrapCache, 'services.php'); variables.APP_PACKAGES_CACHE = join(bootstrapCache, 'packages.php'); variables.APP_CONFIG_CACHE = join(bootstrapCache, 'config.php'); @@ -296,6 +295,7 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { // Make sure the storage path is linked - as people can move the app around, we // need to run this every time the app starts if (!runningSecureBuild()) { + console.log('Linking storage path...'); callPhpSync(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings) } @@ -333,33 +333,38 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { const phpPort = await getPhpPort(); - let serverPath = join(appPath, 'build', '__nativephp_app_bundle'); + let serverPath: string; + let cwd: string; - if (!runningSecureBuild()) { + if (runningSecureBuild()) { + serverPath = join(appPath, 'build', '__nativephp_app_bundle'); + } else { console.log('* * * Running from source * * *'); serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php'); + cwd = join(appPath, 'public'); } const phpServer = callPhp(['-S', `127.0.0.1:${phpPort}`, serverPath], { - cwd: join(appPath, 'public'), + cwd: cwd, env }, phpIniSettings) const portRegex = /Development Server \(.*:([0-9]+)\) started/gm + // Show urls called phpServer.stdout.on('data', (data) => { // [Tue Jan 14 19:51:00 2025] 127.0.0.1:52779 [POST] URI: /_native/api/events if (parseInt(process.env.SHELL_VERBOSITY) > 0) { - console.log(data.toString()); + console.log(data.toString().trim()); } }) - + // Show PHP errors and indicate which port the server is running on phpServer.stderr.on('data', (data) => { const error = data.toString(); const match = portRegex.exec(data.toString()); - // console.log('E:', error); + if (match) { const port = parseInt(match[1]); console.log("PHP Server started on port: ", port); @@ -368,23 +373,25 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { process: phpServer, }); } else { - - // Starting at [NATIVE_EXCEPTION]: if (error.includes('[NATIVE_EXCEPTION]:')) { + let logFile = join(storagePath, 'logs', 'laravel.log'); + console.log(); console.error('Error in PHP:'); console.error(' ' + error.split('[NATIVE_EXCEPTION]:')[1].trim()); console.log('Please check your log file:'); - console.log(' ' + join(appPath, 'storage', 'logs', 'laravel.log')); + console.log(' ' + logFile); console.log(); } } }); + // Log when any error occurs (not started, not killed, couldn't send message, etc) phpServer.on('error', (error) => { reject(error) }); + // Log when the PHP server exits phpServer.on('close', (code) => { console.log(`PHP server exited with code ${code}`); }); From 72b29df94f25a22304f72dd7db33daa5dd572433 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 11 Mar 2025 14:14:51 +0100 Subject: [PATCH 16/28] fix: consistent appData directory --- src/Commands/BuildCommand.php | 2 +- src/Commands/BundleCommand.php | 2 +- src/Commands/DevelopCommand.php | 14 +++----------- src/Traits/SetsAppName.php | 13 +++++++++---- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php index 87134c47..2c76288c 100644 --- a/src/Commands/BuildCommand.php +++ b/src/Commands/BuildCommand.php @@ -66,7 +66,7 @@ public function handle(): void $this->preProcess(); - $this->setAppName(slugify: true); + $this->setAppName(); $this->newLine(); intro('Updating Electron dependencies...'); diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 3b13d672..8a00464e 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -85,7 +85,7 @@ public function handle(): int $this->preProcess(); - $this->setAppName(slugify: true); + $this->setAppName(); intro('Copying App to build directory...'); // We update composer.json later, diff --git a/src/Commands/DevelopCommand.php b/src/Commands/DevelopCommand.php index f855cfa6..e2ffadfe 100644 --- a/src/Commands/DevelopCommand.php +++ b/src/Commands/DevelopCommand.php @@ -7,6 +7,7 @@ use Native\Electron\Traits\Developer; use Native\Electron\Traits\Installer; use Native\Electron\Traits\InstallsAppIcon; +use Native\Electron\Traits\SetsAppName; use function Laravel\Prompts\intro; use function Laravel\Prompts\note; @@ -17,6 +18,7 @@ class DevelopCommand extends Command use Developer; use Installer; use InstallsAppIcon; + use SetsAppName; protected $signature = 'native:serve {--no-queue} {--D|no-dependencies} {--installer=npm}'; @@ -40,7 +42,7 @@ public function handle(): void $this->patchPlist(); } - $this->patchPackageJson(); + $this->setAppName(developmentMode: true); $this->installIcon(); @@ -70,14 +72,4 @@ protected function patchPlist(): void file_put_contents(__DIR__.'/../../resources/js/node_modules/electron/dist/Electron.app/Contents/Info.plist', $pList); } - - protected function patchPackageJson(): void - { - $packageJsonPath = __DIR__.'/../../resources/js/package.json'; - $packageJson = json_decode(file_get_contents($packageJsonPath), true); - - $packageJson['name'] = config('app.name'); - - file_put_contents($packageJsonPath, json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } } diff --git a/src/Traits/SetsAppName.php b/src/Traits/SetsAppName.php index 5281db64..f256936d 100644 --- a/src/Traits/SetsAppName.php +++ b/src/Traits/SetsAppName.php @@ -4,15 +4,20 @@ trait SetsAppName { - protected function setAppName(bool $slugify = false): void + protected function setAppName($developmentMode = false): void { $packageJsonPath = __DIR__.'/../../resources/js/package.json'; $packageJson = json_decode(file_get_contents($packageJsonPath), true); - $name = config('app.name'); + $name = str(config('app.name'))->slug(); - if ($slugify) { - $name = str($name)->lower()->kebab(); + /* + * Suffix the app name with '-dev' if it's a development build + * this way, when the developer test his freshly built app, + * configs, migrations won't be mixed up with the production app + */ + if ($developmentMode) { + $name .= '-dev'; } $packageJson['name'] = $name; From ecb8a036ecd167aa214bb31ff872ad5db985377f Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 11 Mar 2025 14:16:46 +0100 Subject: [PATCH 17/28] wip: fix running artisan commands in bundle --- .../dist/server/api/childProcess.js | 137 +++++++----- .../js/electron-plugin/dist/server/php.js | 6 +- .../src/server/api/childProcess.ts | 197 ++++++++++++------ .../js/electron-plugin/src/server/php.ts | 7 +- 4 files changed, 227 insertions(+), 120 deletions(-) diff --git a/resources/js/electron-plugin/dist/server/api/childProcess.js b/resources/js/electron-plugin/dist/server/api/childProcess.js index 94b28de5..6dbec1e2 100644 --- a/resources/js/electron-plugin/dist/server/api/childProcess.js +++ b/resources/js/electron-plugin/dist/server/api/childProcess.js @@ -11,73 +11,111 @@ import express from 'express'; import { utilityProcess } from 'electron'; import state from '../state.js'; import { notifyLaravel } from "../utils.js"; -import { getDefaultEnvironmentVariables, getDefaultPhpIniSettings } from "../php.js"; +import { getAppPath, getDefaultEnvironmentVariables, getDefaultPhpIniSettings, runningSecureBuild } from "../php.js"; import killSync from "kill-sync"; import { fileURLToPath } from "url"; +import { join } from "path"; const router = express.Router(); function startProcess(settings) { - const { alias, cmd, cwd, env, persistent } = settings; + const { alias, cmd, cwd, env, persistent, spawnTimeout = 30000 } = settings; if (getProcess(alias) !== undefined) { return state.processes[alias]; } - const proc = utilityProcess.fork(fileURLToPath(new URL('../../electron-plugin/dist/server/childProcess.js', import.meta.url)), cmd, { - cwd, - stdio: 'pipe', - serviceName: alias, - env: Object.assign(Object.assign({}, process.env), env) - }); - proc.stdout.on('data', (data) => { - notifyLaravel('events', { - event: 'Native\\Laravel\\Events\\ChildProcess\\MessageReceived', - payload: { - alias, - data: data.toString(), + try { + const proc = utilityProcess.fork(fileURLToPath(new URL('../../electron-plugin/dist/server/childProcess.js', import.meta.url)), cmd, { + cwd, + stdio: 'pipe', + serviceName: alias, + env: Object.assign(Object.assign({}, process.env), env) + }); + const startTimeout = setTimeout(() => { + if (!state.processes[alias] || !state.processes[alias].pid) { + console.error(`Process [${alias}] failed to start within timeout period`); + try { + proc.kill(); + } + catch (e) { + } + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\StartupError', + payload: { + alias, + error: 'Startup timeout exceeded', + } + }); } + }, spawnTimeout); + proc.stdout.on('data', (data) => { + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\MessageReceived', + payload: { + alias, + data: data.toString(), + } + }); }); - }); - proc.stderr.on('data', (data) => { - console.error('Error received from process [' + alias + ']:', data.toString()); - notifyLaravel('events', { - event: 'Native\\Laravel\\Events\\ChildProcess\\ErrorReceived', - payload: { - alias, - data: data.toString(), + proc.stderr.on('data', (data) => { + console.error('Error received from process [' + alias + ']:', data.toString()); + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\ErrorReceived', + payload: { + alias, + data: data.toString(), + } + }); + }); + proc.on('spawn', () => { + clearTimeout(startTimeout); + console.log('Process [' + alias + '] spawned!'); + state.processes[alias] = { + pid: proc.pid, + proc, + settings + }; + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessSpawned', + payload: [alias, proc.pid] + }); + }); + proc.on('exit', (code) => { + clearTimeout(startTimeout); + console.log(`Process [${alias}] exited with code [${code}].`); + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessExited', + payload: { + alias, + code, + } + }); + const settings = Object.assign({}, getSettings(alias)); + delete state.processes[alias]; + if (settings === null || settings === void 0 ? void 0 : settings.persistent) { + console.log('Process [' + alias + '] watchdog restarting...'); + setTimeout(() => startProcess(settings), 1000); } }); - }); - proc.on('spawn', () => { - console.log('Process [' + alias + '] spawned!'); - state.processes[alias] = { - pid: proc.pid, + return { + pid: null, proc, settings }; + } + catch (error) { + console.error(`Failed to create process [${alias}]: ${error.message}`); notifyLaravel('events', { - event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessSpawned', - payload: [alias, proc.pid] - }); - }); - proc.on('exit', (code) => { - console.log(`Process [${alias}] exited with code [${code}].`); - notifyLaravel('events', { - event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessExited', + event: 'Native\\Laravel\\Events\\ChildProcess\\StartupError', payload: { alias, - code, + error: error.toString(), } }); - const settings = Object.assign({}, getSettings(alias)); - delete state.processes[alias]; - if (settings.persistent) { - console.log('Process [' + alias + '] watchdog restarting...'); - startProcess(settings); - } - }); - return { - pid: null, - proc, - settings - }; + return { + pid: null, + proc: null, + settings, + error: error.message + }; + } } function startPhpProcess(settings) { const defaultEnv = getDefaultEnvironmentVariables(state.randomSecret, state.electronApiPort); @@ -85,6 +123,9 @@ function startPhpProcess(settings) { const iniArgs = Object.keys(iniSettings).map(key => { return ['-d', `${key}=${iniSettings[key]}`]; }).flat(); + if (settings.cmd[0] === 'artisan' && runningSecureBuild()) { + settings.cmd.unshift(join(getAppPath(), 'build', '__nativephp_app_bundle')); + } settings = Object.assign(Object.assign({}, settings), { cmd: [state.php, ...iniArgs, ...settings.cmd], env: Object.assign(Object.assign({}, settings.env), defaultEnv) }); return startProcess(settings); } diff --git a/resources/js/electron-plugin/dist/server/php.js b/resources/js/electron-plugin/dist/server/php.js index bd65878a..73ff0449 100644 --- a/resources/js/electron-plugin/dist/server/php.js +++ b/resources/js/electron-plugin/dist/server/php.js @@ -268,11 +268,11 @@ function serveApp(secret, apiPort, phpIniSettings) { } else { if (error.includes('[NATIVE_EXCEPTION]:')) { - let logFile = join(storagePath, 'logs', 'laravel.log'); + let logFile = join(storagePath, 'logs'); console.log(); console.error('Error in PHP:'); console.error(' ' + error.split('[NATIVE_EXCEPTION]:')[1].trim()); - console.log('Please check your log file:'); + console.log('Please check your log files:'); console.log(' ' + logFile); console.log(); } @@ -286,4 +286,4 @@ function serveApp(secret, apiPort, phpIniSettings) { }); })); } -export { startScheduler, serveApp, getAppPath, retrieveNativePHPConfig, retrievePhpIniSettings, getDefaultEnvironmentVariables, getDefaultPhpIniSettings }; +export { startScheduler, serveApp, getAppPath, retrieveNativePHPConfig, retrievePhpIniSettings, getDefaultEnvironmentVariables, getDefaultPhpIniSettings, runningSecureBuild }; diff --git a/resources/js/electron-plugin/src/server/api/childProcess.ts b/resources/js/electron-plugin/src/server/api/childProcess.ts index bb19c114..157e79fa 100644 --- a/resources/js/electron-plugin/src/server/api/childProcess.ts +++ b/resources/js/electron-plugin/src/server/api/childProcess.ts @@ -1,99 +1,156 @@ import express from 'express'; -import { utilityProcess } from 'electron'; +import {utilityProcess} from 'electron'; import state from '../state.js'; -import { notifyLaravel } from "../utils.js"; -import { getDefaultEnvironmentVariables, getDefaultPhpIniSettings } from "../php.js"; +import {notifyLaravel} from "../utils.js"; +import {getAppPath, getDefaultEnvironmentVariables, getDefaultPhpIniSettings, runningSecureBuild} from "../php.js"; import killSync from "kill-sync"; import {fileURLToPath} from "url"; +import {join} from "path"; const router = express.Router(); function startProcess(settings) { - const {alias, cmd, cwd, env, persistent} = settings; + const {alias, cmd, cwd, env, persistent, spawnTimeout = 30000} = settings; if (getProcess(alias) !== undefined) { return state.processes[alias]; } - const proc = utilityProcess.fork( - fileURLToPath(new URL('../../electron-plugin/dist/server/childProcess.js', import.meta.url)), - cmd, - { - cwd, - stdio: 'pipe', - serviceName: alias, - env: { - ...process.env, - ...env, + try { + const proc = utilityProcess.fork( + fileURLToPath(new URL('../../electron-plugin/dist/server/childProcess.js', import.meta.url)), + cmd, + { + cwd, + stdio: 'pipe', + serviceName: alias, + env: { + ...process.env, + ...env, + } } - } - ); - - proc.stdout.on('data', (data) => { - notifyLaravel('events', { - event: 'Native\\Laravel\\Events\\ChildProcess\\MessageReceived', - payload: { - alias, - data: data.toString(), + ); + + // Set timeout to detect if process never spawns + const startTimeout = setTimeout(() => { + if (!state.processes[alias] || !state.processes[alias].pid) { + console.error(`Process [${alias}] failed to start within timeout period`); + + // Attempt to clean up + try { + proc.kill(); + } catch (e) { + // Ignore kill errors + } + + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\StartupError', + payload: { + alias, + error: 'Startup timeout exceeded', + } + }); } + }, spawnTimeout); + + proc.stdout.on('data', (data) => { + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\MessageReceived', + payload: { + alias, + data: data.toString(), + } + }); }); - }); - proc.stderr.on('data', (data) => { - console.error('Error received from process [' + alias + ']:', data.toString()); + proc.stderr.on('data', (data) => { + console.error('Error received from process [' + alias + ']:', data.toString()); - notifyLaravel('events', { - event: 'Native\\Laravel\\Events\\ChildProcess\\ErrorReceived', - payload: { - alias, - data: data.toString(), - } + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\ErrorReceived', + payload: { + alias, + data: data.toString(), + } + }); }); - }); - proc.on('spawn', () => { - console.log('Process [' + alias + '] spawned!'); + // proc.on('error', (error) => { + // clearTimeout(startTimeout); + // console.error(`Process [${alias}] error: ${error.message}`); + // + // notifyLaravel('events', { + // event: 'Native\\Laravel\\Events\\ChildProcess\\StartupError', + // payload: { + // alias, + // error: error.toString(), + // } + // }); + // }); + + proc.on('spawn', () => { + clearTimeout(startTimeout); + console.log('Process [' + alias + '] spawned!'); + + state.processes[alias] = { + pid: proc.pid, + proc, + settings + }; + + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessSpawned', + payload: [alias, proc.pid] + }); + }); + + proc.on('exit', (code) => { + clearTimeout(startTimeout); + console.log(`Process [${alias}] exited with code [${code}].`); + + notifyLaravel('events', { + event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessExited', + payload: { + alias, + code, + } + }); + + const settings = {...getSettings(alias)}; + delete state.processes[alias]; + + if (settings?.persistent) { + console.log('Process [' + alias + '] watchdog restarting...'); + // Add delay to prevent rapid restart loops + setTimeout(() => startProcess(settings), 1000); + } + }); - state.processes[alias] = { - pid: proc.pid, + return { + pid: null, proc, settings }; + } catch (error) { + console.error(`Failed to create process [${alias}]: ${error.message}`); notifyLaravel('events', { - event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessSpawned', - payload: [alias, proc.pid] - }); - }); - - proc.on('exit', (code) => { - console.log(`Process [${alias}] exited with code [${code}].`); - - notifyLaravel('events', { - event: 'Native\\Laravel\\Events\\ChildProcess\\ProcessExited', + event: 'Native\\Laravel\\Events\\ChildProcess\\StartupError', payload: { alias, - code, + error: error.toString(), } }); - const settings = {...getSettings(alias)}; - - delete state.processes[alias]; - - if (settings.persistent) { - console.log('Process [' + alias + '] watchdog restarting...'); - startProcess(settings); - } - }); - - return { - pid: null, - proc, - settings - }; + return { + pid: null, + proc: null, + settings, + error: error.message + }; + } } function startPhpProcess(settings) { @@ -103,18 +160,26 @@ function startPhpProcess(settings) { ); // Construct command args from ini settings - const iniSettings = { ...getDefaultPhpIniSettings(), ...state.phpIni }; + const iniSettings = {...getDefaultPhpIniSettings(), ...state.phpIni}; const iniArgs = Object.keys(iniSettings).map(key => { return ['-d', `${key}=${iniSettings[key]}`]; }).flat(); + // if (args[0] === 'artisan' && runningSecureBuild()) { + // args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + // } + + if (settings.cmd[0] === 'artisan' && runningSecureBuild()) { + settings.cmd.unshift(join(getAppPath(), 'build', '__nativephp_app_bundle')); + } + settings = { ...settings, // Prepend cmd with php executable path & ini settings - cmd: [ state.php, ...iniArgs, ...settings.cmd ], + cmd: [state.php, ...iniArgs, ...settings.cmd], // Mix in the internal NativePHP env - env: { ...settings.env, ...defaultEnv } + env: {...settings.env, ...defaultEnv} }; return startProcess(settings); diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index d0834aad..2d0119ba 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -374,12 +374,12 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { }); } else { if (error.includes('[NATIVE_EXCEPTION]:')) { - let logFile = join(storagePath, 'logs', 'laravel.log'); + let logFile = join(storagePath, 'logs'); console.log(); console.error('Error in PHP:'); console.error(' ' + error.split('[NATIVE_EXCEPTION]:')[1].trim()); - console.log('Please check your log file:'); + console.log('Please check your log files:'); console.log(' ' + logFile); console.log(); } @@ -405,5 +405,6 @@ export { retrieveNativePHPConfig, retrievePhpIniSettings, getDefaultEnvironmentVariables, - getDefaultPhpIniSettings + getDefaultPhpIniSettings, + runningSecureBuild } From 029bd2ec37c332d8ff1208902520f0ad4240421c Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 11 Mar 2025 14:40:02 +0100 Subject: [PATCH 18/28] feat: artisan commands in bundles --- .../js/electron-plugin/dist/server/api/childProcess.js | 5 +++-- .../js/electron-plugin/src/server/api/childProcess.ts | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/js/electron-plugin/dist/server/api/childProcess.js b/resources/js/electron-plugin/dist/server/api/childProcess.js index 6dbec1e2..3f804587 100644 --- a/resources/js/electron-plugin/dist/server/api/childProcess.js +++ b/resources/js/electron-plugin/dist/server/api/childProcess.js @@ -55,7 +55,7 @@ function startProcess(settings) { }); }); proc.stderr.on('data', (data) => { - console.error('Error received from process [' + alias + ']:', data.toString()); + console.error('Process [' + alias + '] ERROR:', data.toString().trim()); notifyLaravel('events', { event: 'Native\\Laravel\\Events\\ChildProcess\\ErrorReceived', payload: { @@ -119,7 +119,8 @@ function startProcess(settings) { } function startPhpProcess(settings) { const defaultEnv = getDefaultEnvironmentVariables(state.randomSecret, state.electronApiPort); - const iniSettings = Object.assign(Object.assign({}, getDefaultPhpIniSettings()), state.phpIni); + const customIniSettings = settings.phpIni || {}; + const iniSettings = Object.assign(Object.assign(Object.assign({}, getDefaultPhpIniSettings()), state.phpIni), customIniSettings); const iniArgs = Object.keys(iniSettings).map(key => { return ['-d', `${key}=${iniSettings[key]}`]; }).flat(); diff --git a/resources/js/electron-plugin/src/server/api/childProcess.ts b/resources/js/electron-plugin/src/server/api/childProcess.ts index 157e79fa..5c893864 100644 --- a/resources/js/electron-plugin/src/server/api/childProcess.ts +++ b/resources/js/electron-plugin/src/server/api/childProcess.ts @@ -66,7 +66,7 @@ function startProcess(settings) { }); proc.stderr.on('data', (data) => { - console.error('Error received from process [' + alias + ']:', data.toString()); + console.error('Process [' + alias + '] ERROR:', data.toString().trim()); notifyLaravel('events', { event: 'Native\\Laravel\\Events\\ChildProcess\\ErrorReceived', @@ -77,6 +77,8 @@ function startProcess(settings) { }); }); + // Experimental feature on Electron, + // I keep this here to remember and retry when we upgrade // proc.on('error', (error) => { // clearTimeout(startTimeout); // console.error(`Process [${alias}] error: ${error.message}`); @@ -160,7 +162,8 @@ function startPhpProcess(settings) { ); // Construct command args from ini settings - const iniSettings = {...getDefaultPhpIniSettings(), ...state.phpIni}; + const customIniSettings = settings.phpIni || {}; + const iniSettings = {...getDefaultPhpIniSettings(), ...state.phpIni, ...customIniSettings}; const iniArgs = Object.keys(iniSettings).map(key => { return ['-d', `${key}=${iniSettings[key]}`]; }).flat(); From 3e660a73896d8713659c40b0103b0b7ecb9f0c10 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 11 Mar 2025 15:01:40 +0100 Subject: [PATCH 19/28] feat: artisan commands in bundles --- resources/js/electron-plugin/dist/server/api/childProcess.js | 2 +- resources/js/electron-plugin/src/server/api/childProcess.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/electron-plugin/dist/server/api/childProcess.js b/resources/js/electron-plugin/dist/server/api/childProcess.js index 3f804587..64ec1dad 100644 --- a/resources/js/electron-plugin/dist/server/api/childProcess.js +++ b/resources/js/electron-plugin/dist/server/api/childProcess.js @@ -119,7 +119,7 @@ function startProcess(settings) { } function startPhpProcess(settings) { const defaultEnv = getDefaultEnvironmentVariables(state.randomSecret, state.electronApiPort); - const customIniSettings = settings.phpIni || {}; + const customIniSettings = settings.iniSettings || {}; const iniSettings = Object.assign(Object.assign(Object.assign({}, getDefaultPhpIniSettings()), state.phpIni), customIniSettings); const iniArgs = Object.keys(iniSettings).map(key => { return ['-d', `${key}=${iniSettings[key]}`]; diff --git a/resources/js/electron-plugin/src/server/api/childProcess.ts b/resources/js/electron-plugin/src/server/api/childProcess.ts index 5c893864..1853b670 100644 --- a/resources/js/electron-plugin/src/server/api/childProcess.ts +++ b/resources/js/electron-plugin/src/server/api/childProcess.ts @@ -162,7 +162,7 @@ function startPhpProcess(settings) { ); // Construct command args from ini settings - const customIniSettings = settings.phpIni || {}; + const customIniSettings = settings.iniSettings || {}; const iniSettings = {...getDefaultPhpIniSettings(), ...state.phpIni, ...customIniSettings}; const iniArgs = Object.keys(iniSettings).map(key => { return ['-d', `${key}=${iniSettings[key]}`]; From b8f54b914e522493d55b0b130a8493c3ce8a6ba0 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 11 Mar 2025 18:56:06 +0100 Subject: [PATCH 20/28] fix: native:serve will no longer run the bundle --- resources/js/electron-plugin/dist/server/php.js | 5 +++-- resources/js/electron-plugin/src/server/php.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/resources/js/electron-plugin/dist/server/php.js b/resources/js/electron-plugin/dist/server/php.js index 73ff0449..04b2a920 100644 --- a/resources/js/electron-plugin/dist/server/php.js +++ b/resources/js/electron-plugin/dist/server/php.js @@ -25,14 +25,15 @@ const argumentEnv = getArgumentEnv(); const appPath = getAppPath(); mkdirpSync(bootstrapCache); function runningSecureBuild() { - return existsSync(join(appPath, 'build', '__nativephp_app_bundle')); + return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) + && process.env.NODE_ENV !== 'development'; } function shouldMigrateDatabase(store) { return store.get('migrated_version') !== app.getVersion() && process.env.NODE_ENV !== 'development'; } function shouldOptimize(store) { - return store.get('optimized_version') !== app.getVersion(); + return runningSecureBuild(); } function getPhpPort() { return __awaiter(this, void 0, void 0, function* () { diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index 2d0119ba..1597d5fe 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -23,6 +23,7 @@ mkdirpSync(bootstrapCache); function runningSecureBuild() { return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) + && process.env.NODE_ENV !== 'development'; } function shouldMigrateDatabase(store) { @@ -31,7 +32,13 @@ function shouldMigrateDatabase(store) { } function shouldOptimize(store) { - return store.get('optimized_version') !== app.getVersion(); + /* + * For some weird reason, + * the cached config is not picked up on subsequent launches, + * so we'll just rebuilt it every time for now + */ + return runningSecureBuild(); + // return runningSecureBuild() && store.get('optimized_version') !== app.getVersion(); } async function getPhpPort() { From cba40633c890b6101b8b30dff41a65f6f937aedb Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Wed, 12 Mar 2025 16:27:52 +0100 Subject: [PATCH 21/28] refactor: even more consistency + reducing the size of the bundled binary --- .../js/electron-plugin/dist/server/php.js | 28 +++--- .../js/electron-plugin/src/server/php.ts | 53 ++++++---- src/Commands/BuildCommand.php | 97 ++++++++++++++----- src/Traits/CopiesBundleToBuildDirectory.php | 40 ++++---- 4 files changed, 135 insertions(+), 83 deletions(-) diff --git a/resources/js/electron-plugin/dist/server/php.js b/resources/js/electron-plugin/dist/server/php.js index 04b2a920..2334f17d 100644 --- a/resources/js/electron-plugin/dist/server/php.js +++ b/resources/js/electron-plugin/dist/server/php.js @@ -33,7 +33,7 @@ function shouldMigrateDatabase(store) { && process.env.NODE_ENV !== 'development'; } function shouldOptimize(store) { - return runningSecureBuild(); + return true; } function getPhpPort() { return __awaiter(this, void 0, void 0, function* () { @@ -45,11 +45,7 @@ function getPhpPort() { } function retrievePhpIniSettings() { return __awaiter(this, void 0, void 0, function* () { - const env = { - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_STORAGE_PATH: storagePath, - NATIVEPHP_DATABASE_PATH: databaseFile, - }; + const env = getDefaultEnvironmentVariables(); const phpOptions = { cwd: appPath, env @@ -63,11 +59,7 @@ function retrievePhpIniSettings() { } function retrieveNativePHPConfig() { return __awaiter(this, void 0, void 0, function* () { - const env = { - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_STORAGE_PATH: storagePath, - NATIVEPHP_DATABASE_PATH: databaseFile, - }; + const env = getDefaultEnvironmentVariables(); const phpOptions = { cwd: appPath, env @@ -128,7 +120,10 @@ function getAppPath() { return appPath; } function ensureAppFoldersAreAvailable() { + console.log('Copying storage folder...'); + console.log('Storage path:', storagePath); if (!existsSync(storagePath) || process.env.NODE_ENV === 'development') { + console.log("App path:", appPath); copySync(join(appPath, 'storage'), storagePath); } mkdirSync(databasePath, { recursive: true }); @@ -160,11 +155,9 @@ function getDefaultEnvironmentVariables(secret, apiPort) { APP_ENV: process.env.NODE_ENV === 'development' ? 'local' : 'production', APP_DEBUG: process.env.NODE_ENV === 'development' ? 'true' : 'false', LARAVEL_STORAGE_PATH: storagePath, + NATIVEPHP_RUNNING: 'true', NATIVEPHP_STORAGE_PATH: storagePath, NATIVEPHP_DATABASE_PATH: databaseFile, - NATIVEPHP_API_URL: `http://localhost:${apiPort}/api/`, - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_SECRET: secret, NATIVEPHP_USER_HOME_PATH: getPath('home'), NATIVEPHP_APP_DATA_PATH: getPath('appData'), NATIVEPHP_USER_DATA_PATH: getPath('userData'), @@ -176,6 +169,10 @@ function getDefaultEnvironmentVariables(secret, apiPort) { NATIVEPHP_VIDEOS_PATH: getPath('videos'), NATIVEPHP_RECENT_PATH: getPath('recent'), }; + if (secret && apiPort) { + variables.NATIVEPHP_API_URL = `http://localhost:${apiPort}/api/`; + variables.NATIVEPHP_SECRET = secret; + } if (runningSecureBuild()) { variables.APP_SERVICES_CACHE = join(bootstrapCache, 'services.php'); variables.APP_PACKAGES_CACHE = join(bootstrapCache, 'packages.php'); @@ -222,6 +219,9 @@ function serveApp(secret, apiPort, phpIniSettings) { } if (shouldMigrateDatabase(store)) { console.log('Migrating database...'); + if (parseInt(process.env.SHELL_VERBOSITY) > 0) { + console.log('Database path:', databaseFile); + } let result = callPhpSync(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings); if (result.status !== 0) { console.error('Failed to migrate database:', result.stderr.toString()); diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index 1597d5fe..8b5fa081 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -12,6 +12,7 @@ import state from "./state.js"; import getPort, {portNumbers} from 'get-port'; import {ProcessResult} from "./ProcessResult.js"; +// TODO: maybe in dev, don't go to the userData folder and stay in the Laravel app folder const storagePath = join(app.getPath('userData'), 'storage') const databasePath = join(app.getPath('userData'), 'database') const databaseFile = join(databasePath, 'database.sqlite') @@ -37,7 +38,8 @@ function shouldOptimize(store) { * the cached config is not picked up on subsequent launches, * so we'll just rebuilt it every time for now */ - return runningSecureBuild(); + return true; + // return runningSecureBuild(); // return runningSecureBuild() && store.get('optimized_version') !== app.getVersion(); } @@ -49,11 +51,7 @@ async function getPhpPort() { } async function retrievePhpIniSettings() { - const env = { - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_STORAGE_PATH: storagePath, - NATIVEPHP_DATABASE_PATH: databaseFile, - }; + const env = getDefaultEnvironmentVariables() as any; const phpOptions = { cwd: appPath, @@ -70,11 +68,7 @@ async function retrievePhpIniSettings() { } async function retrieveNativePHPConfig() { - const env = { - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_STORAGE_PATH: storagePath, - NATIVEPHP_DATABASE_PATH: databaseFile, - }; + const env = getDefaultEnvironmentVariables() as any; const phpOptions = { cwd: appPath, @@ -173,9 +167,15 @@ function getAppPath() { } function ensureAppFoldersAreAvailable() { - if (!existsSync(storagePath) || process.env.NODE_ENV === 'development') { - copySync(join(appPath, 'storage'), storagePath) - } + + // if (!runningSecureBuild()) { + console.log('Copying storage folder...'); + console.log('Storage path:', storagePath); + if (!existsSync(storagePath) || process.env.NODE_ENV === 'development') { + console.log("App path:", appPath); + copySync(join(appPath, 'storage'), storagePath) + } + // } mkdirSync(databasePath, {recursive: true}) @@ -214,9 +214,9 @@ interface EnvironmentVariables { LARAVEL_STORAGE_PATH: string; NATIVEPHP_STORAGE_PATH: string; NATIVEPHP_DATABASE_PATH: string; - NATIVEPHP_API_URL: string; + NATIVEPHP_API_URL?: string; NATIVEPHP_RUNNING: string; - NATIVEPHP_SECRET: string; + NATIVEPHP_SECRET?: string; NATIVEPHP_USER_HOME_PATH: string; NATIVEPHP_APP_DATA_PATH: string; NATIVEPHP_USER_DATA_PATH: string; @@ -233,19 +233,18 @@ interface EnvironmentVariables { APP_CONFIG_CACHE?: string; APP_ROUTES_CACHE?: string; APP_EVENTS_CACHE?: string; + VIEW_COMPILED_PATH?: string; } -function getDefaultEnvironmentVariables(secret, apiPort): EnvironmentVariables { +function getDefaultEnvironmentVariables(secret?: string, apiPort?: number): EnvironmentVariables { // Base variables with string values (no null values) let variables: EnvironmentVariables = { APP_ENV: process.env.NODE_ENV === 'development' ? 'local' : 'production', APP_DEBUG: process.env.NODE_ENV === 'development' ? 'true' : 'false', LARAVEL_STORAGE_PATH: storagePath, + NATIVEPHP_RUNNING: 'true', NATIVEPHP_STORAGE_PATH: storagePath, NATIVEPHP_DATABASE_PATH: databaseFile, - NATIVEPHP_API_URL: `http://localhost:${apiPort}/api/`, - NATIVEPHP_RUNNING: 'true', - NATIVEPHP_SECRET: secret, NATIVEPHP_USER_HOME_PATH: getPath('home'), NATIVEPHP_APP_DATA_PATH: getPath('appData'), NATIVEPHP_USER_DATA_PATH: getPath('userData'), @@ -258,6 +257,12 @@ function getDefaultEnvironmentVariables(secret, apiPort): EnvironmentVariables { NATIVEPHP_RECENT_PATH: getPath('recent'), }; + // Only if the server has already started + if (secret && apiPort) { + variables.NATIVEPHP_API_URL = `http://localhost:${apiPort}/api/`; + variables.NATIVEPHP_SECRET = secret; + } + // Only add cache paths if in production mode if (runningSecureBuild()) { variables.APP_SERVICES_CACHE = join(bootstrapCache, 'services.php'); @@ -265,6 +270,7 @@ function getDefaultEnvironmentVariables(secret, apiPort): EnvironmentVariables { variables.APP_CONFIG_CACHE = join(bootstrapCache, 'config.php'); variables.APP_ROUTES_CACHE = join(bootstrapCache, 'routes-v7.php'); variables.APP_EVENTS_CACHE = join(bootstrapCache, 'events.php'); + // variables.VIEW_COMPILED_PATH; // TODO: keep those in the phar file if we can. } return variables; @@ -321,7 +327,12 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { // Migrate the database if (shouldMigrateDatabase(store)) { - console.log('Migrating database...') + console.log('Migrating database...'); + + if(parseInt(process.env.SHELL_VERBOSITY) > 0) { + console.log('Database path:', databaseFile); + } + let result = callPhpSync(['artisan', 'migrate', '--force'], phpOptions, phpIniSettings); if (result.status !== 0) { diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php index 2c76288c..82d24112 100644 --- a/src/Commands/BuildCommand.php +++ b/src/Commands/BuildCommand.php @@ -36,7 +36,11 @@ class BuildCommand extends Command {arch? : The Processor Architecture to build for (x64, x86, arm64)} {--publish : to publish the app}'; - protected $availableOs = ['win', 'linux', 'mac', 'all']; + protected array $availableOs = ['win', 'linux', 'mac', 'all']; + + private string $buildCommand; + + private string $buildOS; protected function buildPath(string $path = ''): string { @@ -50,36 +54,60 @@ protected function sourcePath(string $path = ''): string public function handle(): void { - $os = $this->selectOs($this->argument('os')); + $this->buildOS = $this->selectOs($this->argument('os')); - $buildCommand = 'build'; - if ($os != 'all') { - $arch = $this->selectArchitectureForOs($os, $this->argument('arch')); + $this->buildCommand = 'build'; + if ($this->buildOS != 'all') { + $arch = $this->selectArchitectureForOs($this->buildOS, $this->argument('arch')); - $os .= $arch != 'all' ? "-{$arch}" : ''; + $this->buildOS .= $arch != 'all' ? "-{$arch}" : ''; // Should we publish? - if ($publish = $this->option('publish')) { - $buildCommand = 'publish'; + if ($this->option('publish')) { + $this->buildCommand = 'publish'; } } - $this->preProcess(); + if ($this->hasBundled()) { + $this->buildBundle(); + } else { + $this->warnUnsecureBuild(); + $this->buildUnsecure(); + } + } + private function buildBundle(): void + { $this->setAppName(); + $this->updateElectronDependencies(); + $this->newLine(); - intro('Updating Electron dependencies...'); - Process::path(__DIR__.'/../../resources/js/') - ->env($this->getEnvironmentVariables()) - ->forever() - ->run('npm ci', function (string $type, string $output) { - echo $output; - }); + intro('Copying Bundle to build directory...'); + $this->copyBundleToBuildDirectory(); + $this->keepRequiredDirectories(); + + $this->newLine(); + $this->copyCertificateAuthorityCertificate(); + + $this->newLine(); + intro('Copying app icons...'); + $this->installIcon(); + + $this->buildOrPublish(); + } + + private function buildUnsecure(): void + { + $this->preProcess(); + + $this->setAppName(); + + $this->updateElectronDependencies(); $this->newLine(); intro('Copying App to build directory...'); - $this->copyBundleToBuildDirectory(); + $this->copyToBuildDirectory(); $this->newLine(); $this->copyCertificateAuthorityCertificate(); @@ -96,15 +124,7 @@ public function handle(): void intro('Pruning vendor directory'); $this->pruneVendorDirectory(); - $this->newLine(); - intro((($publish ?? false) ? 'Publishing' : 'Building')." for {$os}"); - Process::path(__DIR__.'/../../resources/js/') - ->env($this->getEnvironmentVariables()) - ->forever() - ->tty(SymfonyProcess::isTtySupported() && ! $this->option('no-interaction')) - ->run("npm run {$buildCommand}:{$os}", function (string $type, string $output) { - echo $output; - }); + $this->buildOrPublish(); $this->postProcess(); } @@ -129,4 +149,29 @@ protected function getEnvironmentVariables(): array Updater::environmentVariables(), ); } + + private function updateElectronDependencies(): void + { + $this->newLine(); + intro('Updating Electron dependencies...'); + Process::path(__DIR__.'/../../resources/js/') + ->env($this->getEnvironmentVariables()) + ->forever() + ->run('npm ci', function (string $type, string $output) { + echo $output; + }); + } + + private function buildOrPublish(): void + { + $this->newLine(); + intro((($this->buildCommand == 'publish') ? 'Publishing' : 'Building')." for {$this->buildOS}"); + Process::path(__DIR__.'/../../resources/js/') + ->env($this->getEnvironmentVariables()) + ->forever() + ->tty(SymfonyProcess::isTtySupported() && ! $this->option('no-interaction')) + ->run("npm run {$this->buildCommand}:{$this->buildOS}", function (string $type, string $output) { + echo $output; + }); + } } diff --git a/src/Traits/CopiesBundleToBuildDirectory.php b/src/Traits/CopiesBundleToBuildDirectory.php index ff59f212..5d0f0989 100644 --- a/src/Traits/CopiesBundleToBuildDirectory.php +++ b/src/Traits/CopiesBundleToBuildDirectory.php @@ -19,30 +19,26 @@ protected function hasBundled(): bool public function copyBundleToBuildDirectory(): bool { - if ($this->hasBundled()) { - - $this->line('Copying secure app bundle to build directory...'); - $this->line('From: '.realpath(dirname($this->sourcePath(self::$bundlePath)))); - $this->line('To: '.realpath(dirname($this->buildPath(self::$bundlePath)))); - - // TODO: copy only the files that you need. - $this->copyToBuildDirectory(); - $filesToCopy = [ - self::$bundlePath, - '.env', - ]; - $filesystem = new Filesystem; - foreach ($filesToCopy as $file) { - $filesystem->copy($this->sourcePath($file), $this->buildPath($file), true); - } - // $this->keepRequiredDirectories(); - - return true; + $filesystem = new Filesystem; + + $this->line('Copying secure app bundle to build directory...'); + $this->line('From: '.realpath(dirname($this->sourcePath(self::$bundlePath)))); + $this->line('To: '.realpath(dirname($this->buildPath(self::$bundlePath)))); + + // Clean and create build directory + $filesystem->remove($this->buildPath()); + $filesystem->mkdir($this->buildPath()); + + $filesToCopy = [ + self::$bundlePath, + // '.env', + ]; + foreach ($filesToCopy as $file) { + $filesystem->copy($this->sourcePath($file), $this->buildPath($file), true); } + // $this->keepRequiredDirectories(); - $this->warnUnsecureBuild(); - - return $this->copyToBuildDirectory(); + return true; } public function warnUnsecureBuild(): void From d28ed6b2061f52c6dc299def7ec07fa212e2a7df Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Fri, 14 Mar 2025 10:25:29 +0100 Subject: [PATCH 22/28] feat: Better reset command --- src/Commands/ResetCommand.php | 22 ++++++++++++---------- src/Traits/SetsAppName.php | 4 +++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Commands/ResetCommand.php b/src/Commands/ResetCommand.php index 36b3fe34..fad506b6 100644 --- a/src/Commands/ResetCommand.php +++ b/src/Commands/ResetCommand.php @@ -3,12 +3,15 @@ namespace Native\Electron\Commands; use Illuminate\Console\Command; +use Native\Electron\Traits\SetsAppName; use Symfony\Component\Filesystem\Filesystem; use function Laravel\Prompts\intro; class ResetCommand extends Command { + use SetsAppName; + protected $signature = 'native:reset {--with-app-data : Clear the app data as well}'; protected $description = 'Clear all build and dist files'; @@ -43,18 +46,17 @@ public function handle(): int if ($this->option('with-app-data')) { - // Fetch last generated app name - $packageJsonPath = __DIR__.'/../../resources/js/package.json'; - $packageJson = json_decode(file_get_contents($packageJsonPath), true); - $appName = $packageJson['name']; + foreach ([true, false] as $developmentMode) { + $appName = $this->setAppName($developmentMode); - // Eh, just in case, I don't want to delete all user data by accident. - if (! empty($appName)) { - $appDataPath = $this->appDataDirectory($appName); - $this->line('Clearing: '.$appDataPath); + // Eh, just in case, I don't want to delete all user data by accident. + if ( ! empty($appName)) { + $appDataPath = $this->appDataDirectory($appName); + $this->line('Clearing: '.$appDataPath); - if ($filesystem->exists($appDataPath)) { - $filesystem->remove($appDataPath); + if ($filesystem->exists($appDataPath)) { + $filesystem->remove($appDataPath); + } } } } diff --git a/src/Traits/SetsAppName.php b/src/Traits/SetsAppName.php index f256936d..c97f148c 100644 --- a/src/Traits/SetsAppName.php +++ b/src/Traits/SetsAppName.php @@ -4,7 +4,7 @@ trait SetsAppName { - protected function setAppName($developmentMode = false): void + protected function setAppName($developmentMode = false): string { $packageJsonPath = __DIR__.'/../../resources/js/package.json'; $packageJson = json_decode(file_get_contents($packageJsonPath), true); @@ -23,5 +23,7 @@ protected function setAppName($developmentMode = false): void $packageJson['name'] = $name; file_put_contents($packageJsonPath, json_encode($packageJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + return $name; } } From 41561531b416ed415c671444c7d50a3fe5d2c383 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Fri, 14 Mar 2025 10:25:42 +0100 Subject: [PATCH 23/28] chore: upgrade outdated packages --- resources/js/package.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/resources/js/package.json b/resources/js/package.json index fdf42c94..a1c241d7 100644 --- a/resources/js/package.json +++ b/resources/js/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@electron-toolkit/preload": "^3.0.1", - "@electron-toolkit/utils": "^3.0.0", + "@electron-toolkit/utils": "^4.0.0", "@electron/remote": "^2.1.2", "axios": "^1.7.9", "body-parser": "^1.20.3", @@ -76,29 +76,28 @@ "@typescript-eslint/parser": "^8.18.1", "@vue/eslint-config-prettier": "^10.1.0", "cross-env": "^7.0.3", - "electron": "^32.2.7", + "electron": "^32.2.6", "electron-builder": "^25.1.8", "electron-chromedriver": "^32.2.6", - "electron-vite": "^2.3.0", + "electron-vite": "^3.0.0", "eslint": "^9.17.0", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-unicorn": "^56.0.1", - "eslint-plugin-vue": "^9.32.0", - "globals": "^15.14.0", + "eslint-plugin-unicorn": "^57.0.0", + "globals": "^16.0.0", "jest": "^29.7.0", "less": "^4.2.1", "prettier": "^3.4.2", "rimraf": "^6.0.1", "stylelint": "^16.12.0", - "stylelint-config-recommended": "^14.0.1", + "stylelint-config-recommended": "^15.0.0", "stylelint-config-sass-guidelines": "^12.1.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tslib": "^2.8.1", "typescript": "^5.7.2", "typescript-eslint": "^8.18.1", - "vite": "^5.0.0" + "vite": "^6.2.1" }, "exports": "./electron-plugin/dist/index.js", "imports": { From a85d895ddccf33eeff2e40904b92d75e3bb2af42 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Fri, 14 Mar 2025 10:26:31 +0100 Subject: [PATCH 24/28] docs: added some comments --- resources/js/electron-plugin/src/server/php.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index 8b5fa081..9efd6eaa 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -265,8 +265,8 @@ function getDefaultEnvironmentVariables(secret?: string, apiPort?: number): Envi // Only add cache paths if in production mode if (runningSecureBuild()) { - variables.APP_SERVICES_CACHE = join(bootstrapCache, 'services.php'); - variables.APP_PACKAGES_CACHE = join(bootstrapCache, 'packages.php'); + variables.APP_SERVICES_CACHE = join(bootstrapCache, 'services.php'); // Should be present and writable + variables.APP_PACKAGES_CACHE = join(bootstrapCache, 'packages.php'); // Should be present and writable variables.APP_CONFIG_CACHE = join(bootstrapCache, 'config.php'); variables.APP_ROUTES_CACHE = join(bootstrapCache, 'routes-v7.php'); variables.APP_EVENTS_CACHE = join(bootstrapCache, 'events.php'); From 6f6fd36e7cc3db3a42ebc86ca491334d4446e4a8 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Fri, 14 Mar 2025 10:27:21 +0100 Subject: [PATCH 25/28] feat: include symlinked composer packages --- src/Commands/BundleCommand.php | 12 ++++++------ src/Commands/ResetCommand.php | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index 8a00464e..aea32ca2 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -178,7 +178,7 @@ private function checkComposerJson(): bool // // } // } - // Remove repositories with type path + // Remove repositories with type path, we include symlinked packages if (! empty($composerJson['repositories'])) { $this->newLine(); @@ -192,10 +192,10 @@ private function checkComposerJson(): bool file_put_contents($this->buildPath('composer.json'), json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - Process::path($this->buildPath()) - ->run('composer update --no-dev', function (string $type, string $output) { - echo $output; - }); + // Process::path($this->buildPath()) + // ->run('composer install --no-dev', function (string $type, string $output) { + // echo $output; + // }); } } @@ -221,7 +221,7 @@ private function addFilesToZip(ZipArchive $zip): void intro('Creating zip archive…'); $finder = (new Finder)->files() - // ->followLinks() + ->followLinks() // ->ignoreVCSIgnored(true) // TODO: Make our own list of ignored files ->in($this->buildPath()) ->exclude([ diff --git a/src/Commands/ResetCommand.php b/src/Commands/ResetCommand.php index fad506b6..bf5261a6 100644 --- a/src/Commands/ResetCommand.php +++ b/src/Commands/ResetCommand.php @@ -50,7 +50,7 @@ public function handle(): int $appName = $this->setAppName($developmentMode); // Eh, just in case, I don't want to delete all user data by accident. - if ( ! empty($appName)) { + if (! empty($appName)) { $appDataPath = $this->appDataDirectory($appName); $this->line('Clearing: '.$appDataPath); From 22122a5095b2a471630af8a5d9a45cf466deacf2 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Sat, 15 Mar 2025 14:02:03 +0100 Subject: [PATCH 26/28] refactor: per Simon's review --- resources/js/electron-plugin/src/server/api/childProcess.ts | 6 +----- resources/js/electron-plugin/src/server/php.ts | 5 +++++ resources/js/package.json | 2 +- src/Commands/BundleCommand.php | 4 ++-- src/Traits/{HandleApiRequests.php => HandlesZephpyr.php} | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) rename src/Traits/{HandleApiRequests.php => HandlesZephpyr.php} (94%) diff --git a/resources/js/electron-plugin/src/server/api/childProcess.ts b/resources/js/electron-plugin/src/server/api/childProcess.ts index 1853b670..c7fd6b5f 100644 --- a/resources/js/electron-plugin/src/server/api/childProcess.ts +++ b/resources/js/electron-plugin/src/server/api/childProcess.ts @@ -79,6 +79,7 @@ function startProcess(settings) { // Experimental feature on Electron, // I keep this here to remember and retry when we upgrade + // https://www.electronjs.org/docs/latest/api/utility-process#event-error-experimental // proc.on('error', (error) => { // clearTimeout(startTimeout); // console.error(`Process [${alias}] error: ${error.message}`); @@ -168,11 +169,6 @@ function startPhpProcess(settings) { return ['-d', `${key}=${iniSettings[key]}`]; }).flat(); - - // if (args[0] === 'artisan' && runningSecureBuild()) { - // args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); - // } - if (settings.cmd[0] === 'artisan' && runningSecureBuild()) { settings.cmd.unshift(join(getAppPath(), 'build', '__nativephp_app_bundle')); } diff --git a/resources/js/electron-plugin/src/server/php.ts b/resources/js/electron-plugin/src/server/php.ts index 9efd6eaa..5bda7b24 100644 --- a/resources/js/electron-plugin/src/server/php.ts +++ b/resources/js/electron-plugin/src/server/php.ts @@ -308,6 +308,11 @@ function serveApp(secret, apiPort, phpIniSettings): Promise { // Make sure the storage path is linked - as people can move the app around, we // need to run this every time the app starts if (!runningSecureBuild()) { + /* + * Simon: Note for later that we should strip out using storage:link + * all of the necessary files for the app to function should be a part of the bundle + * (whether it's a secured bundle or not), so symlinking feels redundant + */ console.log('Linking storage path...'); callPhpSync(['artisan', 'storage:link', '--force'], phpOptions, phpIniSettings) } diff --git a/resources/js/package.json b/resources/js/package.json index a1c241d7..d5a4d51e 100644 --- a/resources/js/package.json +++ b/resources/js/package.json @@ -76,7 +76,7 @@ "@typescript-eslint/parser": "^8.18.1", "@vue/eslint-config-prettier": "^10.1.0", "cross-env": "^7.0.3", - "electron": "^32.2.6", + "electron": "^32.2.7", "electron-builder": "^25.1.8", "electron-chromedriver": "^32.2.6", "electron-vite": "^3.0.0", diff --git a/src/Commands/BundleCommand.php b/src/Commands/BundleCommand.php index aea32ca2..a2762056 100644 --- a/src/Commands/BundleCommand.php +++ b/src/Commands/BundleCommand.php @@ -11,7 +11,7 @@ use Illuminate\Support\Str; use Native\Electron\Traits\CleansEnvFile; use Native\Electron\Traits\CopiesToBuildDirectory; -use Native\Electron\Traits\HandleApiRequests; +use Native\Electron\Traits\HandlesZephpyr; use Native\Electron\Traits\HasPreAndPostProcessing; use Native\Electron\Traits\InstallsAppIcon; use Native\Electron\Traits\PrunesVendorDirectory; @@ -25,7 +25,7 @@ class BundleCommand extends Command { use CleansEnvFile; use CopiesToBuildDirectory; - use HandleApiRequests; + use HandlesZephpyr; use HasPreAndPostProcessing; use InstallsAppIcon; use PrunesVendorDirectory; diff --git a/src/Traits/HandleApiRequests.php b/src/Traits/HandlesZephpyr.php similarity index 94% rename from src/Traits/HandleApiRequests.php rename to src/Traits/HandlesZephpyr.php index 9c5e2b5e..26749968 100644 --- a/src/Traits/HandleApiRequests.php +++ b/src/Traits/HandlesZephpyr.php @@ -6,7 +6,7 @@ use function Laravel\Prompts\intro; -trait HandleApiRequests +trait HandlesZephpyr { private function baseUrl(): string { @@ -49,7 +49,7 @@ private function checkForZephpyrToken() $this->line(''); $this->warn('No ZEPHPYR_TOKEN found. Cannot bundle!'); $this->line(''); - $this->line('Add your api ZEPHPYR_TOKEN to its .env file:'); + $this->line('Add your Zephpyr API token to your .env file (ZEPHPYR_TOKEN):'); $this->line(base_path('.env')); $this->line(''); $this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!'); From ed761712bf1f1e1de006c1e079346dc807d06cca Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 18 Mar 2025 03:42:37 +0000 Subject: [PATCH 27/28] Attempt to understand --- tests/Feature/BootingTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Feature/BootingTest.php b/tests/Feature/BootingTest.php index c90f942c..665e8768 100644 --- a/tests/Feature/BootingTest.php +++ b/tests/Feature/BootingTest.php @@ -19,15 +19,16 @@ // $process->wait(); // Uncomment this line to debug try { - retry(30, function () use ($output) { + retry(10, function () use ($output) { // Wait until port 8100 is open dump('Waiting for port 8100 to open...'); $fp = @fsockopen('localhost', 8100, $errno, $errstr, 1); if ($fp === false) { throw new Exception(sprintf( - 'Port 8100 is not open yet. Output: "%s"', + 'Port 8100 is not open yet. Output: "%s", Errstr: "%s"', $output, + $errstr )); } }, 5000); From 45231cdeaff13853e6fd5ff6b99d141407865e9a Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 18 Mar 2025 03:49:07 +0000 Subject: [PATCH 28/28] Attempted fix --- tests/Feature/BootingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/BootingTest.php b/tests/Feature/BootingTest.php index 665e8768..8f5c9f79 100644 --- a/tests/Feature/BootingTest.php +++ b/tests/Feature/BootingTest.php @@ -23,7 +23,7 @@ // Wait until port 8100 is open dump('Waiting for port 8100 to open...'); - $fp = @fsockopen('localhost', 8100, $errno, $errstr, 1); + $fp = @fsockopen('127.0.0.1', 8100, $errno, $errstr, 1); if ($fp === false) { throw new Exception(sprintf( 'Port 8100 is not open yet. Output: "%s", Errstr: "%s"',