From 134382890cb2d69a743f839a719201faf921196d Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Wed, 8 Oct 2025 14:04:41 +0200 Subject: [PATCH 01/14] deprecated `native:serve` --- src/Drivers/Electron/Commands/RunCommand.php | 2 +- .../Electron/Commands/ServeCommand.php | 23 +++++++++++++++++++ .../Electron/ElectronServiceProvider.php | 2 ++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/Drivers/Electron/Commands/ServeCommand.php diff --git a/src/Drivers/Electron/Commands/RunCommand.php b/src/Drivers/Electron/Commands/RunCommand.php index 7289360..5c0d71f 100644 --- a/src/Drivers/Electron/Commands/RunCommand.php +++ b/src/Drivers/Electron/Commands/RunCommand.php @@ -16,7 +16,7 @@ #[AsCommand( name: 'native:run', - description: 'Start the NativePHP development server with the Electron app', + description: 'Start the NativePHP development server', )] class RunCommand extends Command { diff --git a/src/Drivers/Electron/Commands/ServeCommand.php b/src/Drivers/Electron/Commands/ServeCommand.php new file mode 100644 index 0000000..586b3fb --- /dev/null +++ b/src/Drivers/Electron/Commands/ServeCommand.php @@ -0,0 +1,23 @@ + Date: Wed, 8 Oct 2025 14:45:59 +0200 Subject: [PATCH 02/14] move dist inside `nativephp/electron` directory --- resources/electron/.gitignore | 1 + resources/electron/electron-builder.mjs | 2 +- resources/electron/electron-plugin/src/server/php.ts | 1 + src/Drivers/Electron/Commands/InstallCommand.php | 2 +- src/Drivers/Electron/ElectronServiceProvider.php | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/electron/.gitignore b/resources/electron/.gitignore index 375353d..9d8f3dc 100644 --- a/resources/electron/.gitignore +++ b/resources/electron/.gitignore @@ -3,3 +3,4 @@ node_modules out *.log !build +dist diff --git a/resources/electron/electron-builder.mjs b/resources/electron/electron-builder.mjs index e6037d9..7ba3e5e 100644 --- a/resources/electron/electron-builder.mjs +++ b/resources/electron/electron-builder.mjs @@ -56,7 +56,7 @@ export default { copyright: appCopyright, directories: { buildResources: 'build', - output: isBuilding ? join(process.env.APP_PATH, 'dist') : undefined, + output: isBuilding ? join(process.env.APP_PATH, 'nativephp', 'electron', 'dist') : undefined, }, files: [ '!**/.vscode/*', diff --git a/resources/electron/electron-plugin/src/server/php.ts b/resources/electron/electron-plugin/src/server/php.ts index 913dee3..0f48c57 100644 --- a/resources/electron/electron-plugin/src/server/php.ts +++ b/resources/electron/electron-plugin/src/server/php.ts @@ -196,6 +196,7 @@ function getArgumentEnv() { } function getAppPath() { + // This is relative to Contents/Resources/app.asar/out/main and points to the app's location inside the bundle let appPath = join(import.meta.dirname, '../../../build/app/') if (process.env.NODE_ENV === 'development' || argumentEnv.TESTING == 1) { diff --git a/src/Drivers/Electron/Commands/InstallCommand.php b/src/Drivers/Electron/Commands/InstallCommand.php index 044a259..3c50602 100644 --- a/src/Drivers/Electron/Commands/InstallCommand.php +++ b/src/Drivers/Electron/Commands/InstallCommand.php @@ -63,7 +63,7 @@ public function handle(): void // Install `native:install` script with a --publish flag // if either publishing now or already published - $publish || is_dir(base_path('nativephp/electron')) + $publish || file_exists(base_path('nativephp/electron/package.json')) ? Composer::installUpdateScript(publish: true) : Composer::installUpdateScript(); diff --git a/src/Drivers/Electron/ElectronServiceProvider.php b/src/Drivers/Electron/ElectronServiceProvider.php index a048e43..816be6b 100644 --- a/src/Drivers/Electron/ElectronServiceProvider.php +++ b/src/Drivers/Electron/ElectronServiceProvider.php @@ -23,7 +23,7 @@ public static function electronPath(string $path = '') // Will use the published electron project, or fallback to the vendor default $publishedProjectPath = base_path("nativephp/electron/{$path}"); - return is_dir($publishedProjectPath) + return file_exists("{$publishedProjectPath}/package.json") ? $publishedProjectPath : Composer::desktopPackagePath("resources/electron/{$path}"); } From 22f186a332dd9477244b15b79c504be9e26d4238 Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Thu, 9 Oct 2025 09:56:00 +0200 Subject: [PATCH 03/14] pass app_path to the electron plugin --- .../electron/electron-plugin/src/index.ts | 4 +++- .../electron-plugin/src/server/php.ts | 23 +++++++++++-------- .../electron-plugin/src/server/state.ts | 2 ++ resources/electron/src/main/index.js | 5 ++-- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/resources/electron/electron-plugin/src/index.ts b/resources/electron/electron-plugin/src/index.ts index 33b85b5..bfd0ea8 100644 --- a/resources/electron/electron-plugin/src/index.ts +++ b/resources/electron/electron-plugin/src/index.ts @@ -31,7 +31,8 @@ class NativePHP { app: CrossProcessExports.App, icon: string, phpBinary: string, - cert: string + cert: string, + appPath: string ) { initialize(); @@ -39,6 +40,7 @@ class NativePHP { state.icon = icon; state.php = phpBinary; state.caCert = cert; + state.appPath = appPath; this.bootstrapApp(app); this.addEventListeners(app); diff --git a/resources/electron/electron-plugin/src/server/php.ts b/resources/electron/electron-plugin/src/server/php.ts index 0f48c57..5463fdc 100644 --- a/resources/electron/electron-plugin/src/server/php.ts +++ b/resources/electron/electron-plugin/src/server/php.ts @@ -19,7 +19,6 @@ 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); mkdirpSync(join(storagePath, 'logs')); @@ -29,7 +28,7 @@ mkdirpSync(join(storagePath, 'framework', 'views')); mkdirpSync(join(storagePath, 'framework', 'testing')); function runningSecureBuild() { - return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) + return existsSync(join(getAppPath(), 'build', '__nativephp_app_bundle')) && process.env.NODE_ENV !== 'development'; } @@ -90,6 +89,7 @@ function canBindToPort(port: number): Promise { async function retrievePhpIniSettings() { const env = getDefaultEnvironmentVariables() as any; + const appPath = getAppPath(); const phpOptions = { cwd: appPath, @@ -107,6 +107,7 @@ async function retrievePhpIniSettings() { async function retrieveNativePHPConfig() { const env = getDefaultEnvironmentVariables() as any; + const appPath = getAppPath() const phpOptions = { cwd: appPath, @@ -125,7 +126,7 @@ async function retrieveNativePHPConfig() { function callPhp(args, options, phpIniSettings = {}) { if (args[0] === 'artisan' && runningSecureBuild()) { - args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + args.unshift(join(getAppPath(), 'build', '__nativephp_app_bundle')); } let iniSettings = Object.assign(getDefaultPhpIniSettings(), phpIniSettings); @@ -154,7 +155,7 @@ function callPhp(args, options, phpIniSettings = {}) { function callPhpSync(args, options, phpIniSettings = {}) { if (args[0] === 'artisan' && runningSecureBuild()) { - args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + args.unshift(join(getAppPath(), 'build', '__nativephp_app_bundle')); } let iniSettings = Object.assign(getDefaultPhpIniSettings(), phpIniSettings); @@ -196,8 +197,7 @@ function getArgumentEnv() { } function getAppPath() { - // This is relative to Contents/Resources/app.asar/out/main and points to the app's location inside the bundle - let appPath = join(import.meta.dirname, '../../../build/app/') + let appPath = state.appPath if (process.env.NODE_ENV === 'development' || argumentEnv.TESTING == 1) { appPath = process.env.APP_PATH || argumentEnv.APP_PATH; @@ -210,10 +210,12 @@ function ensureAppFoldersAreAvailable() { // 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) - } + + if (!existsSync(storagePath) || process.env.NODE_ENV === 'development') { + const appPath = getAppPath(); + console.log("App path:", appPath); + copySync(join(appPath, 'storage'), storagePath) + } // } mkdirSync(databasePath, {recursive: true}) @@ -228,6 +230,7 @@ function ensureAppFoldersAreAvailable() { function startScheduler(secret, apiPort, phpIniSettings = {}) { const env = getDefaultEnvironmentVariables(secret, apiPort); + const appPath = getAppPath(); const phpOptions = { cwd: appPath, diff --git a/resources/electron/electron-plugin/src/server/state.ts b/resources/electron/electron-plugin/src/server/state.ts index e1f21e1..503485e 100644 --- a/resources/electron/electron-plugin/src/server/state.ts +++ b/resources/electron/electron-plugin/src/server/state.ts @@ -26,6 +26,7 @@ interface State { phpPort: number | null; phpIni: any; caCert: string | null; + appPath: string | null; icon: string | null; processes: Record}>; windows: Record; @@ -56,6 +57,7 @@ export default { phpPort: null, phpIni: null, caCert: null, + appPath: null, icon: null, store: settingsStore, randomSecret: generateRandomString(32), diff --git a/resources/electron/src/main/index.js b/resources/electron/src/main/index.js index 0f6cb6e..309cc28 100644 --- a/resources/electron/src/main/index.js +++ b/resources/electron/src/main/index.js @@ -3,12 +3,12 @@ import NativePHP from '#plugin' import path from 'path' const buildPath = path.resolve(import.meta.dirname, import.meta.env.MAIN_VITE_NATIVEPHP_BUILD_PATH); - const defaultIcon = path.join(buildPath, 'icon.png') const certificate = path.join(buildPath, 'cacert.pem') const executable = process.platform === 'win32' ? 'php.exe' : 'php'; const phpBinary = path.join(buildPath,'php', executable); +const appPath = path.join(buildPath, 'app') /** * Turn on the lights for the NativePHP app. @@ -17,5 +17,6 @@ NativePHP.bootstrap( app, defaultIcon, phpBinary, - certificate + certificate, + appPath ); From 18b2ab544b95f24fe60b7b51189879571f439b6f Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Thu, 9 Oct 2025 10:50:56 +0200 Subject: [PATCH 04/14] improve cmd string parsing --- src/ChildProcess.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ChildProcess.php b/src/ChildProcess.php index ab0f70f..eac7355 100644 --- a/src/ChildProcess.php +++ b/src/ChildProcess.php @@ -65,7 +65,7 @@ public function start( ?array $env = null, bool $persistent = false ): self { - $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; + $cmd = $this->parseCommand($cmd); $process = $this->client->post('child-process/start', [ 'alias' => $alias, @@ -84,7 +84,7 @@ public function start( */ public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self { - $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; + $cmd = $this->parseCommand($cmd); $process = $this->client->post('child-process/start-php', [ 'alias' => $alias, @@ -100,7 +100,7 @@ public function php(string|array $cmd, string $alias, ?array $env = null, ?bool public function node(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self { - $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; + $cmd = $this->parseCommand($cmd); $process = $this->client->post('child-process/start-node', [ 'alias' => $alias, @@ -119,7 +119,7 @@ public function node(string|array $cmd, string $alias, ?array $env = null, ?bool */ public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self { - $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; + $cmd = $this->parseCommand($cmd); $cmd = ['artisan', ...$cmd]; @@ -173,4 +173,16 @@ protected function fromRuntimeProcess($process) return $this; } + + /* Convert a cmd string to array representation (explode on space, except within quotes) */ + protected function parseCommand(string|array $cmd): array + { + if (is_array($cmd)) { + return array_values($cmd); + } + + preg_match_all('/"[^"]*"|\'[^\']*\'|[^\s]+/', $cmd, $matches); + + return array_filter($matches[0]); + } } From 765a083781cff3d96659ded4274a02b7b22201a4 Mon Sep 17 00:00:00 2001 From: gwleuverink <17123491+gwleuverink@users.noreply.github.com> Date: Thu, 9 Oct 2025 08:54:17 +0000 Subject: [PATCH 05/14] Build plugin --- resources/electron/electron-plugin/dist/index.js | 3 ++- .../electron-plugin/dist/server/api/childProcess.js | 8 ++++++-- .../electron-plugin/dist/server/childProcess.js | 11 +++++++++-- .../electron/electron-plugin/dist/server/php.js | 13 ++++++++----- .../electron/electron-plugin/dist/server/state.js | 1 + 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/resources/electron/electron-plugin/dist/index.js b/resources/electron/electron-plugin/dist/index.js index 04d48a7..7d811ea 100644 --- a/resources/electron/electron-plugin/dist/index.js +++ b/resources/electron/electron-plugin/dist/index.js @@ -25,11 +25,12 @@ class NativePHP { this.mainWindow = null; this.schedulerInterval = undefined; } - bootstrap(app, icon, phpBinary, cert) { + bootstrap(app, icon, phpBinary, cert, appPath) { initialize(); state.icon = icon; state.php = phpBinary; state.caCert = cert; + state.appPath = appPath; this.bootstrapApp(app); this.addEventListeners(app); } diff --git a/resources/electron/electron-plugin/dist/server/api/childProcess.js b/resources/electron/electron-plugin/dist/server/api/childProcess.js index c6e6752..9d698f3 100644 --- a/resources/electron/electron-plugin/dist/server/api/childProcess.js +++ b/resources/electron/electron-plugin/dist/server/api/childProcess.js @@ -16,7 +16,7 @@ import killSync from "kill-sync"; import { fileURLToPath } from "url"; import { join } from "path"; const router = express.Router(); -function startProcess(settings) { +function startProcess(settings, useNodeRuntime = false) { const { alias, cmd, cwd, env, persistent, spawnTimeout = 30000 } = settings; if (getProcess(alias) !== undefined) { return state.processes[alias]; @@ -26,7 +26,7 @@ function startProcess(settings) { cwd, stdio: 'pipe', serviceName: alias, - env: Object.assign(Object.assign({}, process.env), env) + env: Object.assign(Object.assign(Object.assign({}, process.env), env), { USE_NODE_RUNTIME: useNodeRuntime ? '1' : '0' }) }); const startTimeout = setTimeout(() => { if (!state.processes[alias] || !state.processes[alias].pid) { @@ -157,6 +157,10 @@ router.post('/start', (req, res) => { const proc = startProcess(req.body); res.json(proc); }); +router.post('/start-node', (req, res) => { + const proc = startProcess(req.body, true); + res.json(proc); +}); router.post('/start-php', (req, res) => { const proc = startPhpProcess(req.body); res.json(proc); diff --git a/resources/electron/electron-plugin/dist/server/childProcess.js b/resources/electron/electron-plugin/dist/server/childProcess.js index 819e218..01d99de 100644 --- a/resources/electron/electron-plugin/dist/server/childProcess.js +++ b/resources/electron/electron-plugin/dist/server/childProcess.js @@ -1,5 +1,12 @@ -import { spawn } from "child_process"; -const proc = spawn(process.argv[2], process.argv.slice(3)); +import { spawn, fork } from "child_process"; +const useNodeRuntime = process.env.USE_NODE_RUNTIME === '1'; +const [command, ...args] = process.argv.slice(2); +const proc = useNodeRuntime + ? fork(command, args, { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + execPath: process.execPath + }) + : spawn(command, args); process.parentPort.on('message', (message) => { proc.stdin.write(message.data); }); diff --git a/resources/electron/electron-plugin/dist/server/php.js b/resources/electron/electron-plugin/dist/server/php.js index 50123a6..dc2b817 100644 --- a/resources/electron/electron-plugin/dist/server/php.js +++ b/resources/electron/electron-plugin/dist/server/php.js @@ -23,7 +23,6 @@ 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); mkdirpSync(join(storagePath, 'logs')); mkdirpSync(join(storagePath, 'framework', 'cache')); @@ -31,7 +30,7 @@ mkdirpSync(join(storagePath, 'framework', 'sessions')); mkdirpSync(join(storagePath, 'framework', 'views')); mkdirpSync(join(storagePath, 'framework', 'testing')); function runningSecureBuild() { - return existsSync(join(appPath, 'build', '__nativephp_app_bundle')) + return existsSync(join(getAppPath(), 'build', '__nativephp_app_bundle')) && process.env.NODE_ENV !== 'development'; } function shouldMigrateDatabase(store) { @@ -73,6 +72,7 @@ function canBindToPort(port) { function retrievePhpIniSettings() { return __awaiter(this, void 0, void 0, function* () { const env = getDefaultEnvironmentVariables(); + const appPath = getAppPath(); const phpOptions = { cwd: appPath, env @@ -87,6 +87,7 @@ function retrievePhpIniSettings() { function retrieveNativePHPConfig() { return __awaiter(this, void 0, void 0, function* () { const env = getDefaultEnvironmentVariables(); + const appPath = getAppPath(); const phpOptions = { cwd: appPath, env @@ -100,7 +101,7 @@ function retrieveNativePHPConfig() { } function callPhp(args, options, phpIniSettings = {}) { if (args[0] === 'artisan' && runningSecureBuild()) { - args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + args.unshift(join(getAppPath(), 'build', '__nativephp_app_bundle')); } let iniSettings = Object.assign(getDefaultPhpIniSettings(), phpIniSettings); Object.keys(iniSettings).forEach(key => { @@ -116,7 +117,7 @@ function callPhp(args, options, phpIniSettings = {}) { } function callPhpSync(args, options, phpIniSettings = {}) { if (args[0] === 'artisan' && runningSecureBuild()) { - args.unshift(join(appPath, 'build', '__nativephp_app_bundle')); + args.unshift(join(getAppPath(), 'build', '__nativephp_app_bundle')); } let iniSettings = Object.assign(getDefaultPhpIniSettings(), phpIniSettings); Object.keys(iniSettings).forEach(key => { @@ -140,7 +141,7 @@ function getArgumentEnv() { return env; } function getAppPath() { - let appPath = join(import.meta.dirname, '../../resources/app/').replace('app.asar', 'app.asar.unpacked'); + let appPath = state.appPath; if (process.env.NODE_ENV === 'development' || argumentEnv.TESTING == 1) { appPath = process.env.APP_PATH || argumentEnv.APP_PATH; } @@ -150,6 +151,7 @@ function ensureAppFoldersAreAvailable() { console.log('Copying storage folder...'); console.log('Storage path:', storagePath); if (!existsSync(storagePath) || process.env.NODE_ENV === 'development') { + const appPath = getAppPath(); console.log("App path:", appPath); copySync(join(appPath, 'storage'), storagePath); } @@ -163,6 +165,7 @@ function ensureAppFoldersAreAvailable() { } function startScheduler(secret, apiPort, phpIniSettings = {}) { const env = getDefaultEnvironmentVariables(secret, apiPort); + const appPath = getAppPath(); const phpOptions = { cwd: appPath, env diff --git a/resources/electron/electron-plugin/dist/server/state.js b/resources/electron/electron-plugin/dist/server/state.js index 08bc2d9..cb565b5 100644 --- a/resources/electron/electron-plugin/dist/server/state.js +++ b/resources/electron/electron-plugin/dist/server/state.js @@ -30,6 +30,7 @@ export default { phpPort: null, phpIni: null, caCert: null, + appPath: null, icon: null, store: settingsStore, randomSecret: generateRandomString(32), From 0cbb2cb3b089bcd6c0cd45e24a9940017e99adf0 Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Thu, 9 Oct 2025 12:16:09 +0200 Subject: [PATCH 06/14] add node process tests and fix some failing tests --- tests/ChildProcess/ChildProcessTest.php | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/ChildProcess/ChildProcessTest.php b/tests/ChildProcess/ChildProcessTest.php index 6bb6ce2..bb5b9bb 100644 --- a/tests/ChildProcess/ChildProcessTest.php +++ b/tests/ChildProcess/ChildProcessTest.php @@ -54,9 +54,21 @@ }); }); +it('can start a node process', function () { + ChildProcess::node('path/to/file.js', 'some-alias', ['baz' => 'zah']); + + Http::assertSent(function (Request $request) { + return $request->url() === 'http://localhost:4000/api/child-process/start-node' && + $request['alias'] === 'some-alias' && + $request['cmd'] === ['path/to/file.js'] && + $request['cwd'] === base_path() && + $request['env'] === ['baz' => 'zah']; + }); +}); + it('accepts either a string or a array as start command argument', function () { ChildProcess::start('foo bar', 'some-alias'); - Http::assertSent(fn (Request $request) => $request['cmd'] === ['foo bar']); + Http::assertSent(fn (Request $request) => $request['cmd'] === ['foo', 'bar']); ChildProcess::start(['foo', 'baz'], 'some-alias'); Http::assertSent(fn (Request $request) => $request['cmd'] === ['foo', 'baz']); @@ -64,7 +76,7 @@ it('accepts either a string or a array as php command argument', function () { ChildProcess::php("-r 'sleep(5);'", 'some-alias'); - Http::assertSent(fn (Request $request) => $request['cmd'] === ["-r 'sleep(5);'"]); + Http::assertSent(fn (Request $request) => $request['cmd'] === ['-r', "'sleep(5);'"]); ChildProcess::php(['-r', "'sleep(5);'"], 'some-alias'); Http::assertSent(fn (Request $request) => $request['cmd'] === ['-r', "'sleep(5);'"]); @@ -78,6 +90,14 @@ Http::assertSent(fn (Request $request) => $request['cmd'] === ['artisan', 'foo:baz']); }); +it('accepts either a string or a array as node process argument', function () { + ChildProcess::node('path/to/file.js --some-option', 'some-alias'); + Http::assertSent(fn (Request $request) => $request['cmd'] === ['path/to/file.js', '--some-option']); + + ChildProcess::node(['path/to/other-file.js', '--some-option'], 'some-alias'); + Http::assertSent(fn (Request $request) => $request['cmd'] === ['path/to/other-file.js', '--some-option']); +}); + it('sets the cwd to the base path if none was given', function () { ChildProcess::start(['foo', 'bar'], 'some-alias', cwd: 'path/to/dir'); Http::assertSent(fn (Request $request) => $request['cwd'] === 'path/to/dir'); From c9c72c0c14db927e4c70b4d3d1a79016170c5b3a Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Thu, 9 Oct 2025 12:18:48 +0200 Subject: [PATCH 07/14] fix tests --- tests/ChildProcess/ChildProcessTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ChildProcess/ChildProcessTest.php b/tests/ChildProcess/ChildProcessTest.php index bb5b9bb..c4a00c2 100644 --- a/tests/ChildProcess/ChildProcessTest.php +++ b/tests/ChildProcess/ChildProcessTest.php @@ -24,7 +24,7 @@ Http::assertSent(function (Request $request) { return $request->url() === 'http://localhost:4000/api/child-process/start' && $request['alias'] === 'some-alias' && - $request['cmd'] === ['foo bar'] && + $request['cmd'] === ['foo', 'bar'] && $request['cwd'] === 'path/to/dir' && $request['env'] === ['baz' => 'zah']; }); @@ -36,7 +36,7 @@ Http::assertSent(function (Request $request) { return $request->url() === 'http://localhost:4000/api/child-process/start-php' && $request['alias'] === 'some-alias' && - $request['cmd'] === ["-r 'sleep(5);'"] && + $request['cmd'] === ['-r', "'sleep(5);'"] && $request['cwd'] === base_path() && $request['env'] === ['baz' => 'zah']; }); @@ -48,7 +48,7 @@ Http::assertSent(function (Request $request) { return $request->url() === 'http://localhost:4000/api/child-process/start-php' && $request['alias'] === 'some-alias' && - $request['cmd'] === ['artisan', 'foo:bar --verbose'] && + $request['cmd'] === ['artisan', 'foo:bar', '--verbose'] && $request['cwd'] === base_path() && $request['env'] === ['baz' => 'zah']; }); From 514098281e049c4d783c4eff355566e700719161 Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Thu, 9 Oct 2025 16:11:40 +0200 Subject: [PATCH 08/14] fix static analysis errors --- src/Builder/Concerns/PrunesVendorDirectory.php | 3 ++- src/Drivers/Electron/Commands/InstallCommand.php | 14 ++++++++------ src/Facades/ChildProcess.php | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Builder/Concerns/PrunesVendorDirectory.php b/src/Builder/Concerns/PrunesVendorDirectory.php index ad2a8c2..5535e81 100644 --- a/src/Builder/Concerns/PrunesVendorDirectory.php +++ b/src/Builder/Concerns/PrunesVendorDirectory.php @@ -26,7 +26,8 @@ public function pruneVendorDirectory() // Remove custom php binary package directory $binaryPackageDirectory = $this->binaryPackageDirectory(); if (! empty($binaryPackageDirectory) && $filesystem->exists($this->buildPath($binaryPackageDirectory))) { - $filesystem->remove($this->buildPath('app', $binaryPackageDirectory)); + $binariesInBuildPath = $this->buildPath("app/{$binaryPackageDirectory}"); + $filesystem->remove($binariesInBuildPath); } } } diff --git a/src/Drivers/Electron/Commands/InstallCommand.php b/src/Drivers/Electron/Commands/InstallCommand.php index 3c50602..01c689c 100644 --- a/src/Drivers/Electron/Commands/InstallCommand.php +++ b/src/Drivers/Electron/Commands/InstallCommand.php @@ -34,12 +34,14 @@ public function handle(): void $withoutInteraction = $this->option('no-interaction'); // Prompt for publish - $shouldPromptForPublish = ! $force || ! $withoutInteraction; - $publish = $publish ?? $shouldPromptForPublish ?: confirm( - label: 'Would you like to publish the Electron project?', - hint: 'You\'ll only need this if you\'d like to customize NativePHP\'s inner workings.', - default: false - ); + $shouldPromptForPublish = ! $force && ! $withoutInteraction; + if (! $publish && $shouldPromptForPublish) { + $publish = confirm( + label: 'Would you like to publish the Electron project?', + hint: 'You\'ll only need this if you\'d like to customize NativePHP\'s inner workings.', + default: false + ); + } // Prompt to install NPM Dependencies $installer = $this->getInstaller($this->option('installer')); diff --git a/src/Facades/ChildProcess.php b/src/Facades/ChildProcess.php index 7d71664..6fe7381 100644 --- a/src/Facades/ChildProcess.php +++ b/src/Facades/ChildProcess.php @@ -13,8 +13,8 @@ * @method static \Native\Desktop\ChildProcess restart(string $alias = null) * @method static \Native\Desktop\ChildProcess start(string|array $cmd, string $alias, string $cwd = null, array $env = null, bool $persistent = false) * @method static \Native\Desktop\ChildProcess node(string|array $cmd, string $alias, array $env = null, bool $persistent = false) - * @method static \Native\Desktop\ChildProcess php(string|array $cmd, string $alias, array $env = null, bool $persistent = false, array iniSettings = null) - * @method static \Native\Desktop\ChildProcess artisan(string|array $cmd, string $alias, array $env = null, bool $persistent = false, array iniSettings = null) + * @method static \Native\Desktop\ChildProcess php(string|array $cmd, string $alias, array $env = null, bool $persistent = false, ?array $iniSettings = null) + * @method static \Native\Desktop\ChildProcess artisan(string|array $cmd, string $alias, array $env = null, bool $persistent = false, ?array $iniSettings = null) * @method static void stop(string $alias = null) */ class ChildProcess extends Facade From ea53f8c81d8671bbeb0526bdce9402918b5f5b64 Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Thu, 9 Oct 2025 22:44:15 +0200 Subject: [PATCH 09/14] exclude `dist` folder from builds When the electron project is published, the `dist` directory lives inside the project directory. Always exclude it from builds. --- resources/electron/electron-builder.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/electron/electron-builder.mjs b/resources/electron/electron-builder.mjs index 7ba3e5e..8cb1558 100644 --- a/resources/electron/electron-builder.mjs +++ b/resources/electron/electron-builder.mjs @@ -61,6 +61,7 @@ export default { files: [ '!**/.vscode/*', '!src/*', + '!dist/*', '!electron.vite.config.{js,ts,mjs,cjs}', '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}', '!{.env,.env.*,.npmrc,pnpm-lock.yaml}', From 607884fc92d4f1cf5864caa7a0b2889905efa9c6 Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Fri, 10 Oct 2025 00:08:53 +0200 Subject: [PATCH 10/14] add `assertNode` to childprocess fake --- src/Contracts/ChildProcess.php | 2 ++ src/Fakes/ChildProcessFake.php | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Contracts/ChildProcess.php b/src/Contracts/ChildProcess.php index 796f9a9..c11313d 100644 --- a/src/Contracts/ChildProcess.php +++ b/src/Contracts/ChildProcess.php @@ -20,6 +20,8 @@ public function php(string|array $cmd, string $alias, ?array $env = null, ?bool public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false, ?array $iniSettings = null): self; + public function node(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self; + public function stop(?string $alias = null): void; public function restart(?string $alias = null): ?self; diff --git a/src/Fakes/ChildProcessFake.php b/src/Fakes/ChildProcessFake.php index adf9a1b..aae1ba3 100644 --- a/src/Fakes/ChildProcessFake.php +++ b/src/Fakes/ChildProcessFake.php @@ -28,6 +28,11 @@ class ChildProcessFake implements ChildProcessContract */ public array $artisans = []; + /** + * @var array + */ + public array $nodes = []; + /** * @var array */ @@ -109,6 +114,23 @@ public function artisan( return $this; } + public function node( + string|array $cmd, + string $alias, + ?array $env = null, + ?bool $persistent = false + ): self { + $this->nodes[] = [ + 'cmd' => $cmd, + 'alias' => $alias, + 'env' => $env, + 'persistent' => $persistent, + ]; + + return $this; + } + + public function stop(?string $alias = null): void { $this->stops[] = $alias; @@ -197,6 +219,21 @@ public function assertArtisan(Closure $callback): void PHPUnit::assertTrue($hit); } + /** + * @param Closure(array|string $cmd, string $alias, ?array $env, ?bool $persistent): bool $callback + */ + public function assertNode(Closure $callback): void + { + $hit = empty( + array_filter( + $this->nodes, + fn (array $node) => $callback(...$node) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + /** * @param string|Closure(string): bool $alias */ From a0dc93edeb8fb92cfd7b4f3343eb8baae2e0f8a9 Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Fri, 10 Oct 2025 00:37:19 +0200 Subject: [PATCH 11/14] add `Shell` fake --- src/Contracts/Shell.php | 16 +++++++ src/Facades/Shell.php | 13 +++++- src/Fakes/ShellFake.php | 80 +++++++++++++++++++++++++++++++++++ src/NativeServiceProvider.php | 6 +++ src/Shell.php | 4 +- 5 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/Contracts/Shell.php create mode 100644 src/Fakes/ShellFake.php diff --git a/src/Contracts/Shell.php b/src/Contracts/Shell.php new file mode 100644 index 0000000..fa7c863 --- /dev/null +++ b/src/Contracts/Shell.php @@ -0,0 +1,16 @@ +make(ShellFake::class), function ($fake) { + static::swap($fake); + }); + } + protected static function getFacadeAccessor() { - return \Native\Desktop\Shell::class; + self::clearResolvedInstance(ShellContract::class); + + return ShellContract::class; } } diff --git a/src/Fakes/ShellFake.php b/src/Fakes/ShellFake.php new file mode 100644 index 0000000..b3e64e2 --- /dev/null +++ b/src/Fakes/ShellFake.php @@ -0,0 +1,80 @@ + + */ + public array $showInFolderCalls = []; + + /** + * @var array + */ + public array $openFileCalls = []; + + /** + * @var array + */ + public array $trashFileCalls = []; + + /** + * @var array + */ + public array $openExternalCalls = []; + + public function showInFolder(string $path): void + { + $this->showInFolderCalls[] = $path; + } + + public function openFile(string $path): string + { + $this->openFileCalls[] = $path; + + return ''; + } + + public function trashFile(string $path): void + { + $this->trashFileCalls[] = $path; + } + + public function openExternal(string $url): void + { + $this->openExternalCalls[] = $url; + } + + public function assertShowInFolder(string $path): void + { + PHPUnit::assertContains($path, $this->showInFolderCalls); + + return; + } + + public function assertOpenFile(string $path): void + { + PHPUnit::assertContains($path, $this->openFileCalls); + + return; + } + + public function assertTrashFile(string $path): void + { + PHPUnit::assertContains($path, $this->trashFileCalls); + + return; + } + + public function assertOpenExternal(string $url): void + { + PHPUnit::assertContains($url, $this->openExternalCalls); + + return; + } +} diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 09a2ca4..500b849 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -8,6 +8,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; +use Native\Desktop\Shell as ShellImplementation; use Native\Desktop\ChildProcess as ChildProcessImplementation; use Native\Desktop\Commands\DebugCommand; use Native\Desktop\Commands\FreshCommand; @@ -16,6 +17,7 @@ use Native\Desktop\Commands\MigrateCommand; use Native\Desktop\Commands\SeedDatabaseCommand; use Native\Desktop\Commands\WipeDatabaseCommand; +use Native\Desktop\Contracts\Shell as ShellContract; use Native\Desktop\Contracts\ChildProcess as ChildProcessContract; use Native\Desktop\Contracts\GlobalShortcut as GlobalShortcutContract; use Native\Desktop\Contracts\PowerMonitor as PowerMonitorContract; @@ -74,6 +76,10 @@ public function packageRegistered() return $app->make(ChildProcessImplementation::class); }); + $this->app->bind(ShellContract::class, function (Foundation $app) { + return $app->make(ShellImplementation::class); + }); + $this->app->bind(GlobalShortcutContract::class, function (Foundation $app) { return $app->make(GlobalShortcutImplementation::class); }); diff --git a/src/Shell.php b/src/Shell.php index 584fde5..e51d67f 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -4,7 +4,9 @@ use Native\Desktop\Client\Client; -class Shell +use Native\Desktop\Contracts\Shell as ShellContract; + +class Shell implements ShellContract { public function __construct(protected Client $client) {} From 8333bcf28815663085cc0e3ea475d3f52ef42c4e Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Fri, 10 Oct 2025 00:54:10 +0200 Subject: [PATCH 12/14] improve assertion naming --- src/Fakes/ShellFake.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Fakes/ShellFake.php b/src/Fakes/ShellFake.php index b3e64e2..09a3978 100644 --- a/src/Fakes/ShellFake.php +++ b/src/Fakes/ShellFake.php @@ -57,21 +57,21 @@ public function assertShowInFolder(string $path): void return; } - public function assertOpenFile(string $path): void + public function assertOpenedFile(string $path): void { PHPUnit::assertContains($path, $this->openFileCalls); return; } - public function assertTrashFile(string $path): void + public function assertTrashedFile(string $path): void { PHPUnit::assertContains($path, $this->trashFileCalls); return; } - public function assertOpenExternal(string $url): void + public function assertOpenedExternal(string $url): void { PHPUnit::assertContains($url, $this->openExternalCalls); From a18042926bdbac6433906b956776b83a6cbb050b Mon Sep 17 00:00:00 2001 From: gwleuverink Date: Fri, 10 Oct 2025 01:01:12 +0200 Subject: [PATCH 13/14] fix GitHub private repo releases Fixes: https://github.com/NativePHP/laravel/issues/668 --- src/Drivers/Electron/Updater/GitHubProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Drivers/Electron/Updater/GitHubProvider.php b/src/Drivers/Electron/Updater/GitHubProvider.php index 476f921..149d114 100644 --- a/src/Drivers/Electron/Updater/GitHubProvider.php +++ b/src/Drivers/Electron/Updater/GitHubProvider.php @@ -25,6 +25,7 @@ public function builderOptions(): array 'private' => $this->config['private'], 'channel' => $this->config['channel'], 'releaseType' => $this->config['releaseType'], + 'token' => $this->config['token'], ]; } } From fe414c668241f4dc0d3234e2ad73138d5e2ca25e Mon Sep 17 00:00:00 2001 From: gwleuverink <17123491+gwleuverink@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:47:48 +0000 Subject: [PATCH 14/14] Fix styling --- src/Contracts/Shell.php | 2 -- src/Facades/Shell.php | 2 +- src/Fakes/ChildProcessFake.php | 1 - src/Fakes/ShellFake.php | 5 ----- src/NativeServiceProvider.php | 4 ++-- src/Shell.php | 1 - 6 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Contracts/Shell.php b/src/Contracts/Shell.php index fa7c863..42f6106 100644 --- a/src/Contracts/Shell.php +++ b/src/Contracts/Shell.php @@ -2,8 +2,6 @@ namespace Native\Desktop\Contracts; -use Native\Desktop\Client\Client; - interface Shell { public function showInFolder(string $path): void; diff --git a/src/Facades/Shell.php b/src/Facades/Shell.php index 2cf94bc..fb4d7ef 100644 --- a/src/Facades/Shell.php +++ b/src/Facades/Shell.php @@ -2,9 +2,9 @@ namespace Native\Desktop\Facades; -use Native\Desktop\Fakes\ShellFake; use Illuminate\Support\Facades\Facade; use Native\Desktop\Contracts\Shell as ShellContract; +use Native\Desktop\Fakes\ShellFake; /** * @method static void showInFolder(string $path) diff --git a/src/Fakes/ChildProcessFake.php b/src/Fakes/ChildProcessFake.php index aae1ba3..324c092 100644 --- a/src/Fakes/ChildProcessFake.php +++ b/src/Fakes/ChildProcessFake.php @@ -130,7 +130,6 @@ public function node( return $this; } - public function stop(?string $alias = null): void { $this->stops[] = $alias; diff --git a/src/Fakes/ShellFake.php b/src/Fakes/ShellFake.php index 09a3978..3468d51 100644 --- a/src/Fakes/ShellFake.php +++ b/src/Fakes/ShellFake.php @@ -2,7 +2,6 @@ namespace Native\Desktop\Fakes; -use Closure; use Native\Desktop\Contracts\Shell as ShellContract; use PHPUnit\Framework\Assert as PHPUnit; @@ -54,27 +53,23 @@ public function assertShowInFolder(string $path): void { PHPUnit::assertContains($path, $this->showInFolderCalls); - return; } public function assertOpenedFile(string $path): void { PHPUnit::assertContains($path, $this->openFileCalls); - return; } public function assertTrashedFile(string $path): void { PHPUnit::assertContains($path, $this->trashFileCalls); - return; } public function assertOpenedExternal(string $url): void { PHPUnit::assertContains($url, $this->openExternalCalls); - return; } } diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 500b849..f4d6cb4 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -8,7 +8,6 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; -use Native\Desktop\Shell as ShellImplementation; use Native\Desktop\ChildProcess as ChildProcessImplementation; use Native\Desktop\Commands\DebugCommand; use Native\Desktop\Commands\FreshCommand; @@ -17,11 +16,11 @@ use Native\Desktop\Commands\MigrateCommand; use Native\Desktop\Commands\SeedDatabaseCommand; use Native\Desktop\Commands\WipeDatabaseCommand; -use Native\Desktop\Contracts\Shell as ShellContract; use Native\Desktop\Contracts\ChildProcess as ChildProcessContract; use Native\Desktop\Contracts\GlobalShortcut as GlobalShortcutContract; use Native\Desktop\Contracts\PowerMonitor as PowerMonitorContract; use Native\Desktop\Contracts\QueueWorker as QueueWorkerContract; +use Native\Desktop\Contracts\Shell as ShellContract; use Native\Desktop\Contracts\WindowManager as WindowManagerContract; use Native\Desktop\DataObjects\QueueConfig; use Native\Desktop\Drivers\Electron\ElectronServiceProvider; @@ -31,6 +30,7 @@ use Native\Desktop\Http\Middleware\PreventRegularBrowserAccess; use Native\Desktop\Logging\LogWatcher; use Native\Desktop\PowerMonitor as PowerMonitorImplementation; +use Native\Desktop\Shell as ShellImplementation; use Native\Desktop\Windows\WindowManager as WindowManagerImplementation; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; diff --git a/src/Shell.php b/src/Shell.php index e51d67f..d555111 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -3,7 +3,6 @@ namespace Native\Desktop; use Native\Desktop\Client\Client; - use Native\Desktop\Contracts\Shell as ShellContract; class Shell implements ShellContract