diff --git a/resources/electron/.gitignore b/resources/electron/.gitignore index 375353d9..9d8f3dcc 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 e6037d9c..8cb1558c 100644 --- a/resources/electron/electron-builder.mjs +++ b/resources/electron/electron-builder.mjs @@ -56,11 +56,12 @@ 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/*', '!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}', diff --git a/resources/electron/electron-plugin/dist/index.js b/resources/electron/electron-plugin/dist/index.js index 04d48a7c..7d811ea5 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 c6e67527..9d698f3f 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 819e218f..01d99dea 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 50123a67..dc2b817c 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 08bc2d98..cb565b50 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), diff --git a/resources/electron/electron-plugin/src/index.ts b/resources/electron/electron-plugin/src/index.ts index 33b85b57..bfd0ea82 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 913dee32..5463fdca 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,7 +197,7 @@ function getArgumentEnv() { } function getAppPath() { - 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; @@ -209,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}) @@ -227,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 e1f21e16..503485e2 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 0f6cb6e7..309cc289 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 ); diff --git a/src/Builder/Concerns/PrunesVendorDirectory.php b/src/Builder/Concerns/PrunesVendorDirectory.php index ad2a8c2a..5535e815 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/ChildProcess.php b/src/ChildProcess.php index ab0f70fb..eac73553 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]); + } } diff --git a/src/Contracts/ChildProcess.php b/src/Contracts/ChildProcess.php index 796f9a93..c11313dd 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/Contracts/Shell.php b/src/Contracts/Shell.php new file mode 100644 index 00000000..42f61067 --- /dev/null +++ b/src/Contracts/Shell.php @@ -0,0 +1,14 @@ +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')); @@ -63,7 +65,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/Commands/RunCommand.php b/src/Drivers/Electron/Commands/RunCommand.php index 7289360a..5c0d71f2 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 00000000..586b3fb0 --- /dev/null +++ b/src/Drivers/Electron/Commands/ServeCommand.php @@ -0,0 +1,23 @@ + $this->config['private'], 'channel' => $this->config['channel'], 'releaseType' => $this->config['releaseType'], + 'token' => $this->config['token'], ]; } } diff --git a/src/Facades/ChildProcess.php b/src/Facades/ChildProcess.php index 7d71664f..6fe73811 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 diff --git a/src/Facades/Shell.php b/src/Facades/Shell.php index 00336be3..fb4d7ef6 100644 --- a/src/Facades/Shell.php +++ b/src/Facades/Shell.php @@ -3,6 +3,8 @@ namespace Native\Desktop\Facades; use Illuminate\Support\Facades\Facade; +use Native\Desktop\Contracts\Shell as ShellContract; +use Native\Desktop\Fakes\ShellFake; /** * @method static void showInFolder(string $path) @@ -12,8 +14,17 @@ */ class Shell extends Facade { + public static function fake() + { + return tap(static::getFacadeApplication()->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/ChildProcessFake.php b/src/Fakes/ChildProcessFake.php index adf9a1b3..324c0921 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,22 @@ 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 +218,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 */ diff --git a/src/Fakes/ShellFake.php b/src/Fakes/ShellFake.php new file mode 100644 index 00000000..3468d51c --- /dev/null +++ b/src/Fakes/ShellFake.php @@ -0,0 +1,75 @@ + + */ + 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); + + } + + public function assertOpenedFile(string $path): void + { + PHPUnit::assertContains($path, $this->openFileCalls); + + } + + public function assertTrashedFile(string $path): void + { + PHPUnit::assertContains($path, $this->trashFileCalls); + + } + + public function assertOpenedExternal(string $url): void + { + PHPUnit::assertContains($url, $this->openExternalCalls); + + } +} diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 09a2ca4f..f4d6cb40 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -20,6 +20,7 @@ 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; @@ -29,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; @@ -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 584fde5a..d555111d 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -3,8 +3,9 @@ namespace Native\Desktop; use Native\Desktop\Client\Client; +use Native\Desktop\Contracts\Shell as ShellContract; -class Shell +class Shell implements ShellContract { public function __construct(protected Client $client) {} diff --git a/tests/ChildProcess/ChildProcessTest.php b/tests/ChildProcess/ChildProcessTest.php index 6bb6ce2e..c4a00c2f 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,19 @@ 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']; + }); +}); + +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']; }); @@ -56,7 +68,7 @@ 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');