From 4ffc0834563ca73dcf0edc3e47439e9296b81a1b Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 21 Mar 2025 11:44:23 -0400 Subject: [PATCH 01/12] Adding initial trial for the framework specific echo lib --- .../Console/BroadcastingInstallCommand.php | 63 ++++++--- .../Foundation/Console/stubs/use-echo-ts.stub | 127 ++++++++++++++++++ 2 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index b23689671d18..1eb7dfb1a7a9 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -26,7 +26,8 @@ class BroadcastingInstallCommand extends Command {--composer=global : Absolute path to the Composer binary which should be used to install packages} {--force : Overwrite any existing broadcasting routes file} {--without-reverb : Do not prompt to install Laravel Reverb} - {--without-node : Do not prompt to install Node dependencies}'; + {--without-node : Do not prompt to install Node dependencies} + {--react : Use React TypeScript Echo implementation instead of JavaScript}'; /** * The console command description. @@ -55,24 +56,45 @@ public function handle() $this->enableBroadcastServiceProvider(); // Install bootstrapping... - if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { - if (! is_dir($directory = $this->laravel->resourcePath('js'))) { - mkdir($directory, 0755, true); + if ($this->option('react')) { + // For React, use the TypeScript implementation + $hooksDirectory = $this->laravel->resourcePath('js/hooks'); + $echoScriptPath = $hooksDirectory.'/use-echo.ts'; + + if (! file_exists($echoScriptPath)) { + // Create the hooks directory if it doesn't exist + if (! is_dir($hooksDirectory)) { + if (! is_dir($this->laravel->resourcePath('js'))) { + mkdir($this->laravel->resourcePath('js'), 0755, true); + } + mkdir($hooksDirectory, 0755, true); + } + + copy(__DIR__.'/stubs/use-echo-ts.stub', $echoScriptPath); + $this->components->info("Created React TypeScript Echo implementation at [resources/js/hooks/use-echo.ts]."); } + } else { + // Standard JavaScript implementation + if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { + if (! is_dir($directory = $this->laravel->resourcePath('js'))) { + mkdir($directory, 0755, true); + } - copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); - } - - if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { - $bootstrapScript = file_get_contents( - $bootstrapScriptPath - ); + copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); + } - if (! str_contains($bootstrapScript, './echo')) { - file_put_contents( - $bootstrapScriptPath, - trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + // Only add the bootstrap import for the standard JS implementation + if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { + $bootstrapScript = file_get_contents( + $bootstrapScriptPath ); + + if (! str_contains($bootstrapScript, './echo')) { + file_put_contents( + $bootstrapScriptPath, + trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + ); + } } } @@ -177,24 +199,27 @@ protected function installNodeDependencies() $this->components->info('Installing and building Node dependencies.'); + // Node dependencies are the same regardless of whether --react flag is set + $packages = 'laravel-echo pusher-js'; + if (file_exists(base_path('pnpm-lock.yaml'))) { $commands = [ - 'pnpm add --save-dev laravel-echo pusher-js', + "pnpm add --save-dev {$packages}", 'pnpm run build', ]; } elseif (file_exists(base_path('yarn.lock'))) { $commands = [ - 'yarn add --dev laravel-echo pusher-js', + "yarn add --dev {$packages}", 'yarn run build', ]; } elseif (file_exists(base_path('bun.lock')) || file_exists(base_path('bun.lockb'))) { $commands = [ - 'bun add --dev laravel-echo pusher-js', + "bun add --dev {$packages}", 'bun run build', ]; } else { $commands = [ - 'npm install --save-dev laravel-echo pusher-js', + "npm install --save-dev {$packages}", 'npm run build', ]; } diff --git a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub new file mode 100644 index 000000000000..96f53989d76c --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub @@ -0,0 +1,127 @@ +import { useEffect, useRef } from 'react'; +import Echo from 'laravel-echo'; +import Pusher from 'pusher-js'; + +// Define types for Echo channels +interface Channel { + listen(event: string, callback: (payload: any) => void): Channel; + stopListening(event: string, callback?: (payload: any) => void): Channel; +} + +interface EchoInstance extends Echo { + channel(channel: string): Channel; + private(channel: string): Channel; + leaveChannel(channel: string): void; +} + +interface ChannelData { + count: number; + channel: Channel; +} + +interface Channels { + [channelName: string]: ChannelData; +} + +// Create a singleton Echo instance +let echoInstance: EchoInstance | null = null; + +// Initialize Echo only once +const getEchoInstance = (): EchoInstance => { + if (!echoInstance) { + // Temporarily add Pusher to window object for Echo initialization + // This is a compromise - we're still avoiding permanent global namespace pollution + // by only adding it temporarily during initialization + const originalPusher = (window as any).Pusher; + (window as any).Pusher = Pusher; + + // Configure Echo with Reverb + echoInstance = new Echo({ + broadcaster: 'reverb', + key: import.meta.env.VITE_REVERB_APP_KEY, + wsHost: import.meta.env.VITE_REVERB_HOST, + wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, + wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, + forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', + enabledTransports: ['ws', 'wss'], + }) as EchoInstance; + + // Restore the original Pusher value to avoid side effects + if (originalPusher) { + (window as any).Pusher = originalPusher; + } else { + delete (window as any).Pusher; + } + } + return echoInstance; +}; + +// Keep track of all active channels +const channels: Channels = {}; + +// Export Echo instance for direct access if needed +export const echo = getEchoInstance(); + +// Helper functions to interact with Echo +export const subscribeToChannel = (channelName: string, isPrivate = false): Channel => { + return isPrivate ? echo.private(channelName) : echo.channel(channelName); +}; + +export const leaveChannel = (channelName: string): void => { + echo.leaveChannel(channelName); +}; + +// The main hook for using Echo in React components +export default function useEcho( + channel: string, + event: string | string[], + callback: (payload: any) => void, + dependencies = [], + visibility: 'private' | 'public' = 'private' +) { + const eventRef = useRef(callback); + + useEffect(() => { + // Always use the latest callback + eventRef.current = callback; + + const channelName = visibility === 'public' ? channel : `${visibility}-${channel}`; + const isPrivate = visibility === 'private'; + + // Reuse existing channel subscription or create a new one + if (!channels[channelName]) { + channels[channelName] = { + count: 1, + channel: subscribeToChannel(channel, isPrivate), + }; + } else { + channels[channelName].count += 1; + } + + const subscription = channels[channelName].channel; + + const listener = (payload: any) => { + eventRef.current(payload); + }; + + const events = Array.isArray(event) ? event : [event]; + + // Subscribe to all events + events.forEach((e) => { + subscription.listen(e, listener); + }); + + // Cleanup function + return () => { + events.forEach((e) => { + subscription.stopListening(e, listener); + }); + + channels[channelName].count -= 1; + if (channels[channelName].count === 0) { + leaveChannel(channelName); + delete channels[channelName]; + } + }; + }, [...dependencies]); // eslint-disable-line +} \ No newline at end of file From f1da2c559f9b1e0c27de22eaed6c3656ee35064b Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 21 Mar 2025 11:46:45 -0400 Subject: [PATCH 02/12] few more updates --- .../Foundation/Console/BroadcastingInstallCommand.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 1eb7dfb1a7a9..559dd36e608e 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -199,27 +199,24 @@ protected function installNodeDependencies() $this->components->info('Installing and building Node dependencies.'); - // Node dependencies are the same regardless of whether --react flag is set - $packages = 'laravel-echo pusher-js'; - if (file_exists(base_path('pnpm-lock.yaml'))) { $commands = [ - "pnpm add --save-dev {$packages}", + 'pnpm add --save-dev laravel-echo pusher-js', 'pnpm run build', ]; } elseif (file_exists(base_path('yarn.lock'))) { $commands = [ - "yarn add --dev {$packages}", + 'yarn add --dev laravel-echo pusher-js', 'yarn run build', ]; } elseif (file_exists(base_path('bun.lock')) || file_exists(base_path('bun.lockb'))) { $commands = [ - "bun add --dev {$packages}", + 'bun add --dev laravel-echo pusher-js', 'bun run build', ]; } else { $commands = [ - "npm install --save-dev {$packages}", + 'npm install --save-dev laravel-echo pusher-js', 'npm run build', ]; } From b828985179537b5d165bafe2ffaba0d3ceb49493 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Tue, 25 Mar 2025 16:46:22 -0400 Subject: [PATCH 03/12] Adding functionality for vue composable --- .../Console/BroadcastingInstallCommand.php | 97 +++++++-- .../Foundation/Console/stubs/useEcho-ts.stub | 185 ++++++++++++++++++ 2 files changed, 262 insertions(+), 20 deletions(-) create mode 100644 src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 559dd36e608e..dfab93fbd3b2 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -26,8 +26,7 @@ class BroadcastingInstallCommand extends Command {--composer=global : Absolute path to the Composer binary which should be used to install packages} {--force : Overwrite any existing broadcasting routes file} {--without-reverb : Do not prompt to install Laravel Reverb} - {--without-node : Do not prompt to install Node dependencies} - {--react : Use React TypeScript Echo implementation instead of JavaScript}'; + {--without-node : Do not prompt to install Node dependencies}'; /** * The console command description. @@ -48,30 +47,19 @@ public function handle() // Install channel routes file... if (! file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) || $this->option('force')) { $this->components->info("Published 'channels' route file."); - copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); } $this->uncommentChannelsRoutesFile(); $this->enableBroadcastServiceProvider(); - // Install bootstrapping... - if ($this->option('react')) { - // For React, use the TypeScript implementation - $hooksDirectory = $this->laravel->resourcePath('js/hooks'); - $echoScriptPath = $hooksDirectory.'/use-echo.ts'; - - if (! file_exists($echoScriptPath)) { - // Create the hooks directory if it doesn't exist - if (! is_dir($hooksDirectory)) { - if (! is_dir($this->laravel->resourcePath('js'))) { - mkdir($this->laravel->resourcePath('js'), 0755, true); - } - mkdir($hooksDirectory, 0755, true); - } - - copy(__DIR__.'/stubs/use-echo-ts.stub', $echoScriptPath); - $this->components->info("Created React TypeScript Echo implementation at [resources/js/hooks/use-echo.ts]."); + // We have a specific echo version for React and Vue with Typescript, + // so check if this app contains React or Vue with Typescript + if ($reactOrVue = $this->appContainsReactOrVueWithTypescript()) { + if($reactOrVue === 'react') { + $this->installReactTypescriptEcho(); + } elseif($reactOrVue === 'vue') { + $this->installVueTypescriptEcho(); } } else { // Standard JavaScript implementation @@ -103,6 +91,27 @@ public function handle() $this->installNodeDependencies(); } + /** + * Detect if the user is using React or Vue with Typescript and then install the corresponding Echo implementation + * + * @return null | 'react' | 'vue' + */ + protected function appContainsReactOrVueWithTypescript() + { + $packageJsonPath = $this->laravel->basePath('package.json'); + if (!file_exists($packageJsonPath)) { + return null; + } + $packageJson = json_decode(file_get_contents($packageJsonPath), true); + if (isset($packageJson['dependencies']['react']) || isset($packageJson['dependencies']['vue'])) { + // Check if dependencies also contains typescript + if (isset($packageJson['dependencies']['typescript'])) { + return isset($packageJson['dependencies']['react']) ? 'react' : 'vue'; + } + } + return null; + } + /** * Uncomment the "channels" routes file in the application bootstrap file. * @@ -156,6 +165,54 @@ protected function enableBroadcastServiceProvider() } } + /** + * Install the React TypeScript Echo implementation. + * + * @return void + */ + protected function installReactTypescriptEcho() + { + $hooksDirectory = $this->laravel->resourcePath('js/hooks'); + $echoScriptPath = $hooksDirectory.'/use-echo.ts'; + + if (! file_exists($echoScriptPath)) { + // Create the hooks directory if it doesn't exist + if (! is_dir($hooksDirectory)) { + if (! is_dir($this->laravel->resourcePath('js'))) { + mkdir($this->laravel->resourcePath('js'), 0755, true); + } + mkdir($hooksDirectory, 0755, true); + } + + copy(__DIR__.'/stubs/use-echo-ts.stub', $echoScriptPath); + $this->components->info("Created React TypeScript Echo implementation at [resources/js/hooks/use-echo.ts]."); + } + } + + /** + * Install the Vue TypeScript Echo implementation. + * + * @return void + */ + protected function installVueTypescriptEcho() + { + $echoScriptPath = $this->laravel->resourcePath('js/composables/useEcho.ts'); + + if (! file_exists($echoScriptPath)) { + $composablesDirectory = $this->laravel->resourcePath('js/composables'); + + if (! is_dir($composablesDirectory)) { + if (! is_dir($this->laravel->resourcePath('js'))) { + mkdir($this->laravel->resourcePath('js'), 0755, true); + } + mkdir($composablesDirectory, 0755, true); + } + + copy(__DIR__.'/stubs/useEcho-ts.stub', $echoScriptPath); + $this->components->info("Created Vue TypeScript Echo implementation at [resources/js/composables/useEcho.ts]."); + } + } + /** * Install Laravel Reverb into the application if desired. * diff --git a/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub b/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub new file mode 100644 index 000000000000..3d2eb40f99f9 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub @@ -0,0 +1,185 @@ +import { ref, onMounted, onUnmounted, watch } from 'vue'; +import Echo, { EchoOptions } from 'laravel-echo'; +import Pusher from 'pusher-js'; + +// Define types for Echo channels +interface Channel { + listen(event: string, callback: (payload: any) => void): Channel; + stopListening(event: string, callback?: (payload: any) => void): Channel; +} + +interface EchoInstance extends Echo { + channel(channel: string): Channel; + private(channel: string): Channel; + leaveChannel(channel: string): void; +} + +interface ChannelData { + count: number; + channel: Channel; +} + +interface Channels { + [channelName: string]: ChannelData; +} + +// Create a singleton Echo instance +let echoInstance: EchoInstance | null = null; +let echoConfig: EchoOptions | null = null; + +// Configure Echo with custom options +export const configureEcho = (config: EchoOptions): void => { + echoConfig = config; + // Reset the instance if it was already created + if (echoInstance) { + echoInstance = null; + } +}; + +// Initialize Echo only once +const getEchoInstance = (): EchoInstance | null => { + if (!echoInstance) { + if (!echoConfig) { + console.error('Echo has not been configured. Please call configureEcho() with your configuration options before using Echo.'); + return null; + } + + // Temporarily add Pusher to window object for Echo initialization + // This is a compromise - we're still avoiding permanent global namespace pollution + // by only adding it temporarily during initialization + const originalPusher = (window as any).Pusher; + (window as any).Pusher = Pusher; + + // Configure Echo with provided config + echoInstance = new Echo(echoConfig) as EchoInstance; + + // Restore the original Pusher value to avoid side effects + if (originalPusher) { + (window as any).Pusher = originalPusher; + } else { + delete (window as any).Pusher; + } + } + return echoInstance; +}; + +// Keep track of all active channels +const channels: Channels = {}; + +// Export Echo instance for direct access if needed +export const echo = (): EchoInstance | null => getEchoInstance(); + +// Helper functions to interact with Echo +export const subscribeToChannel = (channelName: string, isPrivate = false): Channel | null => { + const instance = getEchoInstance(); + if (!instance) return null; + return isPrivate ? instance.private(channelName) : instance.channel(channelName); +}; + +export const leaveChannel = (channelName: string): void => { + const instance = getEchoInstance(); + if (!instance) return; + instance.leaveChannel(channelName); +}; + +// The main composable for using Echo in Vue components +export const useEcho = ( + channelName: string, + event: string | string[], + callback: (payload: any) => void, + dependencies: any[] = [], + visibility: 'private' | 'public' = 'private' +) => { + // Use ref to store the current callback + const eventCallback = ref(callback); + + // Track subscription for cleanup + let subscription: Channel | null = null; + let events: string[] = []; + let fullChannelName = ''; + + // Setup function to handle subscription + const setupSubscription = () => { + // Update callback ref + eventCallback.value = callback; + + // Format channel name based on visibility + fullChannelName = visibility === 'public' ? channelName : `${visibility}-${channelName}`; + const isPrivate = visibility === 'private'; + + // Reuse existing channel subscription or create a new one + if (!channels[fullChannelName]) { + const channel = subscribeToChannel(channelName, isPrivate); + if (!channel) return; + channels[fullChannelName] = { + count: 1, + channel, + }; + } else { + channels[fullChannelName].count += 1; + } + + subscription = channels[fullChannelName].channel; + + // Create listener function + const listener = (payload: any) => { + eventCallback.value(payload); + }; + + // Convert event to array if it's a single string + events = Array.isArray(event) ? event : [event]; + + // Subscribe to all events + events.forEach((e) => { + subscription?.listen(e, listener); + }); + }; + + // Cleanup function + const cleanup = () => { + if (subscription && events.length > 0) { + events.forEach((e) => { + subscription?.stopListening(e); + }); + + if (fullChannelName && channels[fullChannelName]) { + channels[fullChannelName].count -= 1; + if (channels[fullChannelName].count === 0) { + leaveChannel(fullChannelName); + delete channels[fullChannelName]; + } + } + } + }; + + // Setup subscription when component is mounted + onMounted(() => { + setupSubscription(); + }); + + // Clean up subscription when component is unmounted + onUnmounted(() => { + cleanup(); + }); + + // Watch dependencies and re-subscribe when they change + if (dependencies.length > 0) { + // Create a watch effect for each dependency + dependencies.forEach((dep, index) => { + watch(() => dependencies[index], () => { + // Clean up old subscription + cleanup(); + // Setup new subscription + setupSubscription(); + }, { deep: true }); + }); + } + + // Return the Echo instance for additional control if needed + return { + echo: getEchoInstance(), + leaveChannel: () => { + cleanup(); + } + }; +} \ No newline at end of file From f9ac68a39ed7b0b531f0c58d7f702632e6e452d2 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Tue, 25 Mar 2025 17:22:56 -0400 Subject: [PATCH 04/12] updating the react hook with updated config options --- .../Foundation/Console/stubs/use-echo-ts.stub | 79 +++++++++++++------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub index 96f53989d76c..310d97fa5616 100644 --- a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub +++ b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import Echo from 'laravel-echo'; +import Echo, { EchoOptions } from 'laravel-echo'; import Pusher from 'pusher-js'; // Define types for Echo channels @@ -25,26 +25,33 @@ interface Channels { // Create a singleton Echo instance let echoInstance: EchoInstance | null = null; +let echoConfig: EchoOptions | null = null; + +// Configure Echo with custom options +export const configureEcho = (config: EchoOptions): void => { + echoConfig = config; + // Reset the instance if it was already created + if (echoInstance) { + echoInstance = null; + } +}; // Initialize Echo only once -const getEchoInstance = (): EchoInstance => { +const getEchoInstance = (): EchoInstance | null => { if (!echoInstance) { + if (!echoConfig) { + console.error('Echo has not been configured. Please call configureEcho() with your configuration options before using Echo.'); + return null; + } + // Temporarily add Pusher to window object for Echo initialization // This is a compromise - we're still avoiding permanent global namespace pollution // by only adding it temporarily during initialization const originalPusher = (window as any).Pusher; (window as any).Pusher = Pusher; - // Configure Echo with Reverb - echoInstance = new Echo({ - broadcaster: 'reverb', - key: import.meta.env.VITE_REVERB_APP_KEY, - wsHost: import.meta.env.VITE_REVERB_HOST, - wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, - wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, - forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', - enabledTransports: ['ws', 'wss'], - }) as EchoInstance; + // Configure Echo with provided config + echoInstance = new Echo(echoConfig) as EchoInstance; // Restore the original Pusher value to avoid side effects if (originalPusher) { @@ -60,25 +67,29 @@ const getEchoInstance = (): EchoInstance => { const channels: Channels = {}; // Export Echo instance for direct access if needed -export const echo = getEchoInstance(); +export const echo = (): EchoInstance | null => getEchoInstance(); // Helper functions to interact with Echo -export const subscribeToChannel = (channelName: string, isPrivate = false): Channel => { - return isPrivate ? echo.private(channelName) : echo.channel(channelName); +export const subscribeToChannel = (channelName: string, isPrivate = false): Channel | null => { + const instance = getEchoInstance(); + if (!instance) return null; + return isPrivate ? instance.private(channelName) : instance.channel(channelName); }; export const leaveChannel = (channelName: string): void => { - echo.leaveChannel(channelName); + const instance = getEchoInstance(); + if (!instance) return; + instance.leaveChannel(channelName); }; // The main hook for using Echo in React components -export default function useEcho( +export const useEcho = ( channel: string, event: string | string[], callback: (payload: any) => void, dependencies = [], visibility: 'private' | 'public' = 'private' -) { +) => { const eventRef = useRef(callback); useEffect(() => { @@ -90,9 +101,12 @@ export default function useEcho( // Reuse existing channel subscription or create a new one if (!channels[channelName]) { + const channelSubscription = subscribeToChannel(channel, isPrivate); + if (!channelSubscription) return; + channels[channelName] = { count: 1, - channel: subscribeToChannel(channel, isPrivate), + channel: channelSubscription, }; } else { channels[channelName].count += 1; @@ -117,11 +131,28 @@ export default function useEcho( subscription.stopListening(e, listener); }); - channels[channelName].count -= 1; - if (channels[channelName].count === 0) { - leaveChannel(channelName); - delete channels[channelName]; + if (channels[channelName]) { + channels[channelName].count -= 1; + if (channels[channelName].count === 0) { + leaveChannel(channelName); + delete channels[channelName]; + } } }; }, [...dependencies]); // eslint-disable-line -} \ No newline at end of file + + // Return the Echo instance for additional control if needed + return { + echo: getEchoInstance(), + leaveChannel: () => { + const channelName = visibility === 'public' ? channel : `${visibility}-${channel}`; + if (channels[channelName]) { + channels[channelName].count -= 1; + if (channels[channelName].count === 0) { + leaveChannel(channelName); + delete channels[channelName]; + } + } + } + }; +}; \ No newline at end of file From 1b0fe3fb6883d5d4315cc453a8cafefd03e722b7 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Wed, 26 Mar 2025 08:58:12 -0400 Subject: [PATCH 05/12] Adding the configure code injection step --- .../Console/BroadcastingInstallCommand.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index dfab93fbd3b2..dbe6953a7f18 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -61,6 +61,9 @@ public function handle() } elseif($reactOrVue === 'vue') { $this->installVueTypescriptEcho(); } + + // Inject Echo configuration for both React and Vue applications + $this->injectEchoConfigurationInApp($reactOrVue); } else { // Standard JavaScript implementation if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { @@ -213,6 +216,74 @@ protected function installVueTypescriptEcho() } } + /** + * Inject Echo configuration into the application's main file. + * + * @param string|null $appType The application type ('react', 'vue', or null) + * @return void + */ + protected function injectEchoConfigurationInApp(?string $appType = null) + { + // If app type is not provided, detect it + if ($appType === null) { + $appType = $this->appContainsReactOrVueWithTypescript(); + } + + // Determine file path and import path based on app type + if ($appType === 'vue') { + $filePath = resource_path('js/app.ts'); + $importPath = './composables/useEcho'; + $fileExtension = 'ts'; + } else { // Default to React + $filePath = resource_path('js/app.tsx'); + $importPath = './hooks/use-echo'; + $fileExtension = 'tsx'; + } + + // Check if file exists + if (!file_exists($filePath)) { + $this->components->warn("Could not find {$filePath}. Echo configuration not added."); + return; + } + + $contents = file_get_contents($filePath); + + // Prepare Echo configuration code + $echoCode = <<components->info("Echo configuration added to app.{$fileExtension} after imports."); + } + } else { + // Add the Echo configuration to the top of the file if no import statements are found + $newContents = $echoCode . "\n" . $contents; + file_put_contents($filePath, $newContents); + $this->components->info("Echo configuration added to the top of app.{$fileExtension}."); + } + } + + /** * Install Laravel Reverb into the application if desired. * From 14f498396e88951280fff05176832112ed7ee644 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Wed, 26 Mar 2025 09:34:59 -0400 Subject: [PATCH 06/12] Getting styleCI to pass --- .../Foundation/Console/BroadcastingInstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index dbe6953a7f18..89ad0ee69739 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -222,7 +222,7 @@ protected function installVueTypescriptEcho() * @param string|null $appType The application type ('react', 'vue', or null) * @return void */ - protected function injectEchoConfigurationInApp(?string $appType = null) + protected function injectEchoConfigurationInApp(string $appType = null) { // If app type is not provided, detect it if ($appType === null) { From 582b10bae72b9de54b6fdc3df13aafa81d729dff Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:03:24 -0400 Subject: [PATCH 07/12] removing the useEcho stubs, instead will be added to laravel-echo npm package --- .../Console/BroadcastingInstallCommand.php | 56 +----- .../Foundation/Console/stubs/use-echo-ts.stub | 158 --------------- .../Foundation/Console/stubs/useEcho-ts.stub | 185 ------------------ 3 files changed, 1 insertion(+), 398 deletions(-) delete mode 100644 src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub delete mode 100644 src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 89ad0ee69739..6f2df676ffa9 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -56,13 +56,7 @@ public function handle() // We have a specific echo version for React and Vue with Typescript, // so check if this app contains React or Vue with Typescript if ($reactOrVue = $this->appContainsReactOrVueWithTypescript()) { - if($reactOrVue === 'react') { - $this->installReactTypescriptEcho(); - } elseif($reactOrVue === 'vue') { - $this->installVueTypescriptEcho(); - } - - // Inject Echo configuration for both React and Vue applications + // Inject Echo configuration for both React or Vue typescript applications $this->injectEchoConfigurationInApp($reactOrVue); } else { // Standard JavaScript implementation @@ -168,54 +162,6 @@ protected function enableBroadcastServiceProvider() } } - /** - * Install the React TypeScript Echo implementation. - * - * @return void - */ - protected function installReactTypescriptEcho() - { - $hooksDirectory = $this->laravel->resourcePath('js/hooks'); - $echoScriptPath = $hooksDirectory.'/use-echo.ts'; - - if (! file_exists($echoScriptPath)) { - // Create the hooks directory if it doesn't exist - if (! is_dir($hooksDirectory)) { - if (! is_dir($this->laravel->resourcePath('js'))) { - mkdir($this->laravel->resourcePath('js'), 0755, true); - } - mkdir($hooksDirectory, 0755, true); - } - - copy(__DIR__.'/stubs/use-echo-ts.stub', $echoScriptPath); - $this->components->info("Created React TypeScript Echo implementation at [resources/js/hooks/use-echo.ts]."); - } - } - - /** - * Install the Vue TypeScript Echo implementation. - * - * @return void - */ - protected function installVueTypescriptEcho() - { - $echoScriptPath = $this->laravel->resourcePath('js/composables/useEcho.ts'); - - if (! file_exists($echoScriptPath)) { - $composablesDirectory = $this->laravel->resourcePath('js/composables'); - - if (! is_dir($composablesDirectory)) { - if (! is_dir($this->laravel->resourcePath('js'))) { - mkdir($this->laravel->resourcePath('js'), 0755, true); - } - mkdir($composablesDirectory, 0755, true); - } - - copy(__DIR__.'/stubs/useEcho-ts.stub', $echoScriptPath); - $this->components->info("Created Vue TypeScript Echo implementation at [resources/js/composables/useEcho.ts]."); - } - } - /** * Inject Echo configuration into the application's main file. * diff --git a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub b/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub deleted file mode 100644 index 310d97fa5616..000000000000 --- a/src/Illuminate/Foundation/Console/stubs/use-echo-ts.stub +++ /dev/null @@ -1,158 +0,0 @@ -import { useEffect, useRef } from 'react'; -import Echo, { EchoOptions } from 'laravel-echo'; -import Pusher from 'pusher-js'; - -// Define types for Echo channels -interface Channel { - listen(event: string, callback: (payload: any) => void): Channel; - stopListening(event: string, callback?: (payload: any) => void): Channel; -} - -interface EchoInstance extends Echo { - channel(channel: string): Channel; - private(channel: string): Channel; - leaveChannel(channel: string): void; -} - -interface ChannelData { - count: number; - channel: Channel; -} - -interface Channels { - [channelName: string]: ChannelData; -} - -// Create a singleton Echo instance -let echoInstance: EchoInstance | null = null; -let echoConfig: EchoOptions | null = null; - -// Configure Echo with custom options -export const configureEcho = (config: EchoOptions): void => { - echoConfig = config; - // Reset the instance if it was already created - if (echoInstance) { - echoInstance = null; - } -}; - -// Initialize Echo only once -const getEchoInstance = (): EchoInstance | null => { - if (!echoInstance) { - if (!echoConfig) { - console.error('Echo has not been configured. Please call configureEcho() with your configuration options before using Echo.'); - return null; - } - - // Temporarily add Pusher to window object for Echo initialization - // This is a compromise - we're still avoiding permanent global namespace pollution - // by only adding it temporarily during initialization - const originalPusher = (window as any).Pusher; - (window as any).Pusher = Pusher; - - // Configure Echo with provided config - echoInstance = new Echo(echoConfig) as EchoInstance; - - // Restore the original Pusher value to avoid side effects - if (originalPusher) { - (window as any).Pusher = originalPusher; - } else { - delete (window as any).Pusher; - } - } - return echoInstance; -}; - -// Keep track of all active channels -const channels: Channels = {}; - -// Export Echo instance for direct access if needed -export const echo = (): EchoInstance | null => getEchoInstance(); - -// Helper functions to interact with Echo -export const subscribeToChannel = (channelName: string, isPrivate = false): Channel | null => { - const instance = getEchoInstance(); - if (!instance) return null; - return isPrivate ? instance.private(channelName) : instance.channel(channelName); -}; - -export const leaveChannel = (channelName: string): void => { - const instance = getEchoInstance(); - if (!instance) return; - instance.leaveChannel(channelName); -}; - -// The main hook for using Echo in React components -export const useEcho = ( - channel: string, - event: string | string[], - callback: (payload: any) => void, - dependencies = [], - visibility: 'private' | 'public' = 'private' -) => { - const eventRef = useRef(callback); - - useEffect(() => { - // Always use the latest callback - eventRef.current = callback; - - const channelName = visibility === 'public' ? channel : `${visibility}-${channel}`; - const isPrivate = visibility === 'private'; - - // Reuse existing channel subscription or create a new one - if (!channels[channelName]) { - const channelSubscription = subscribeToChannel(channel, isPrivate); - if (!channelSubscription) return; - - channels[channelName] = { - count: 1, - channel: channelSubscription, - }; - } else { - channels[channelName].count += 1; - } - - const subscription = channels[channelName].channel; - - const listener = (payload: any) => { - eventRef.current(payload); - }; - - const events = Array.isArray(event) ? event : [event]; - - // Subscribe to all events - events.forEach((e) => { - subscription.listen(e, listener); - }); - - // Cleanup function - return () => { - events.forEach((e) => { - subscription.stopListening(e, listener); - }); - - if (channels[channelName]) { - channels[channelName].count -= 1; - if (channels[channelName].count === 0) { - leaveChannel(channelName); - delete channels[channelName]; - } - } - }; - }, [...dependencies]); // eslint-disable-line - - // Return the Echo instance for additional control if needed - return { - echo: getEchoInstance(), - leaveChannel: () => { - const channelName = visibility === 'public' ? channel : `${visibility}-${channel}`; - if (channels[channelName]) { - channels[channelName].count -= 1; - if (channels[channelName].count === 0) { - leaveChannel(channelName); - delete channels[channelName]; - } - } - } - }; -}; \ No newline at end of file diff --git a/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub b/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub deleted file mode 100644 index 3d2eb40f99f9..000000000000 --- a/src/Illuminate/Foundation/Console/stubs/useEcho-ts.stub +++ /dev/null @@ -1,185 +0,0 @@ -import { ref, onMounted, onUnmounted, watch } from 'vue'; -import Echo, { EchoOptions } from 'laravel-echo'; -import Pusher from 'pusher-js'; - -// Define types for Echo channels -interface Channel { - listen(event: string, callback: (payload: any) => void): Channel; - stopListening(event: string, callback?: (payload: any) => void): Channel; -} - -interface EchoInstance extends Echo { - channel(channel: string): Channel; - private(channel: string): Channel; - leaveChannel(channel: string): void; -} - -interface ChannelData { - count: number; - channel: Channel; -} - -interface Channels { - [channelName: string]: ChannelData; -} - -// Create a singleton Echo instance -let echoInstance: EchoInstance | null = null; -let echoConfig: EchoOptions | null = null; - -// Configure Echo with custom options -export const configureEcho = (config: EchoOptions): void => { - echoConfig = config; - // Reset the instance if it was already created - if (echoInstance) { - echoInstance = null; - } -}; - -// Initialize Echo only once -const getEchoInstance = (): EchoInstance | null => { - if (!echoInstance) { - if (!echoConfig) { - console.error('Echo has not been configured. Please call configureEcho() with your configuration options before using Echo.'); - return null; - } - - // Temporarily add Pusher to window object for Echo initialization - // This is a compromise - we're still avoiding permanent global namespace pollution - // by only adding it temporarily during initialization - const originalPusher = (window as any).Pusher; - (window as any).Pusher = Pusher; - - // Configure Echo with provided config - echoInstance = new Echo(echoConfig) as EchoInstance; - - // Restore the original Pusher value to avoid side effects - if (originalPusher) { - (window as any).Pusher = originalPusher; - } else { - delete (window as any).Pusher; - } - } - return echoInstance; -}; - -// Keep track of all active channels -const channels: Channels = {}; - -// Export Echo instance for direct access if needed -export const echo = (): EchoInstance | null => getEchoInstance(); - -// Helper functions to interact with Echo -export const subscribeToChannel = (channelName: string, isPrivate = false): Channel | null => { - const instance = getEchoInstance(); - if (!instance) return null; - return isPrivate ? instance.private(channelName) : instance.channel(channelName); -}; - -export const leaveChannel = (channelName: string): void => { - const instance = getEchoInstance(); - if (!instance) return; - instance.leaveChannel(channelName); -}; - -// The main composable for using Echo in Vue components -export const useEcho = ( - channelName: string, - event: string | string[], - callback: (payload: any) => void, - dependencies: any[] = [], - visibility: 'private' | 'public' = 'private' -) => { - // Use ref to store the current callback - const eventCallback = ref(callback); - - // Track subscription for cleanup - let subscription: Channel | null = null; - let events: string[] = []; - let fullChannelName = ''; - - // Setup function to handle subscription - const setupSubscription = () => { - // Update callback ref - eventCallback.value = callback; - - // Format channel name based on visibility - fullChannelName = visibility === 'public' ? channelName : `${visibility}-${channelName}`; - const isPrivate = visibility === 'private'; - - // Reuse existing channel subscription or create a new one - if (!channels[fullChannelName]) { - const channel = subscribeToChannel(channelName, isPrivate); - if (!channel) return; - channels[fullChannelName] = { - count: 1, - channel, - }; - } else { - channels[fullChannelName].count += 1; - } - - subscription = channels[fullChannelName].channel; - - // Create listener function - const listener = (payload: any) => { - eventCallback.value(payload); - }; - - // Convert event to array if it's a single string - events = Array.isArray(event) ? event : [event]; - - // Subscribe to all events - events.forEach((e) => { - subscription?.listen(e, listener); - }); - }; - - // Cleanup function - const cleanup = () => { - if (subscription && events.length > 0) { - events.forEach((e) => { - subscription?.stopListening(e); - }); - - if (fullChannelName && channels[fullChannelName]) { - channels[fullChannelName].count -= 1; - if (channels[fullChannelName].count === 0) { - leaveChannel(fullChannelName); - delete channels[fullChannelName]; - } - } - } - }; - - // Setup subscription when component is mounted - onMounted(() => { - setupSubscription(); - }); - - // Clean up subscription when component is unmounted - onUnmounted(() => { - cleanup(); - }); - - // Watch dependencies and re-subscribe when they change - if (dependencies.length > 0) { - // Create a watch effect for each dependency - dependencies.forEach((dep, index) => { - watch(() => dependencies[index], () => { - // Clean up old subscription - cleanup(); - // Setup new subscription - setupSubscription(); - }, { deep: true }); - }); - } - - // Return the Echo instance for additional control if needed - return { - echo: getEchoInstance(), - leaveChannel: () => { - cleanup(); - } - }; -} \ No newline at end of file From fdd1a2611f778793059ebd67079d6feb72814491 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:04:07 -0400 Subject: [PATCH 08/12] fix spacing --- src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 6f2df676ffa9..00075882dcd0 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -47,6 +47,7 @@ public function handle() // Install channel routes file... if (! file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) || $this->option('force')) { $this->components->info("Published 'channels' route file."); + copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); } From 44da9f81f746bf1e46b3c7082f61cc479d318d2b Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:04:32 -0400 Subject: [PATCH 09/12] fix spacing --- .../Foundation/Console/BroadcastingInstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 00075882dcd0..be41b7e378c1 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -47,7 +47,7 @@ public function handle() // Install channel routes file... if (! file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) || $this->option('force')) { $this->components->info("Published 'channels' route file."); - + copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); } From b96f130020a9cbb1115befe611bcf56ed0daf249 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:11:23 -0400 Subject: [PATCH 10/12] fix spacing --- .../Console/BroadcastingInstallCommand.php | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index be41b7e378c1..f69bea87a569 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -98,7 +98,7 @@ protected function appContainsReactOrVueWithTypescript() { $packageJsonPath = $this->laravel->basePath('package.json'); if (!file_exists($packageJsonPath)) { - return null; + return; } $packageJson = json_decode(file_get_contents($packageJsonPath), true); if (isset($packageJson['dependencies']['react']) || isset($packageJson['dependencies']['vue'])) { @@ -107,7 +107,7 @@ protected function appContainsReactOrVueWithTypescript() return isset($packageJson['dependencies']['react']) ? 'react' : 'vue'; } } - return null; + return; } /** @@ -166,24 +166,19 @@ protected function enableBroadcastServiceProvider() /** * Inject Echo configuration into the application's main file. * - * @param string|null $appType The application type ('react', 'vue', or null) + * @param string $appType The application type ('react' or 'vue') * @return void */ - protected function injectEchoConfigurationInApp(string $appType = null) - { - // If app type is not provided, detect it - if ($appType === null) { - $appType = $this->appContainsReactOrVueWithTypescript(); - } - + protected function injectEchoConfigurationInApp(string $appType = 'react') + { // Determine file path and import path based on app type if ($appType === 'vue') { $filePath = resource_path('js/app.ts'); - $importPath = './composables/useEcho'; + $importPath = 'laravel-echo/vue'; $fileExtension = 'ts'; } else { // Default to React $filePath = resource_path('js/app.tsx'); - $importPath = './hooks/use-echo'; + $importPath = 'laravel-echo/react'; $fileExtension = 'tsx'; } From c1eac4db6557f68ee3ed2170d6af7c0763fb0f01 Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:26:18 -0400 Subject: [PATCH 11/12] making methods more efficient --- .../Console/BroadcastingInstallCommand.php | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index f69bea87a569..34a183b29f7e 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -56,9 +56,9 @@ public function handle() // We have a specific echo version for React and Vue with Typescript, // so check if this app contains React or Vue with Typescript - if ($reactOrVue = $this->appContainsReactOrVueWithTypescript()) { - // Inject Echo configuration for both React or Vue typescript applications - $this->injectEchoConfigurationInApp($reactOrVue); + if ($this->appContainsReactWithTypescript() || $this->appContainsVueWithTypescript()) { + // If this is a React/Vue app with typescript, inject the Echo configuration in the app.tsx or app.ts file + $this->injectEchoConfigurationInApp(); } else { // Standard JavaScript implementation if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { @@ -90,24 +90,35 @@ public function handle() } /** - * Detect if the user is using React or Vue with Typescript and then install the corresponding Echo implementation + * Detect if the user is using React with TypeScript * - * @return null | 'react' | 'vue' + * @return bool */ - protected function appContainsReactOrVueWithTypescript() + protected function appContainsReactWithTypescript(): bool { $packageJsonPath = $this->laravel->basePath('package.json'); if (!file_exists($packageJsonPath)) { - return; + return false; } $packageJson = json_decode(file_get_contents($packageJsonPath), true); - if (isset($packageJson['dependencies']['react']) || isset($packageJson['dependencies']['vue'])) { - // Check if dependencies also contains typescript - if (isset($packageJson['dependencies']['typescript'])) { - return isset($packageJson['dependencies']['react']) ? 'react' : 'vue'; - } + return isset($packageJson['dependencies']['react']) && + isset($packageJson['dependencies']['typescript']); + } + + /** + * Detect if the user is using Vue with TypeScript + * + * @return bool + */ + protected function appContainsVueWithTypescript(): bool + { + $packageJsonPath = $this->laravel->basePath('package.json'); + if (!file_exists($packageJsonPath)) { + return false; } - return; + $packageJson = json_decode(file_get_contents($packageJsonPath), true); + return isset($packageJson['dependencies']['vue']) && + isset($packageJson['dependencies']['typescript']); } /** @@ -166,17 +177,17 @@ protected function enableBroadcastServiceProvider() /** * Inject Echo configuration into the application's main file. * - * @param string $appType The application type ('react' or 'vue') * @return void */ - protected function injectEchoConfigurationInApp(string $appType = 'react') - { - // Determine file path and import path based on app type - if ($appType === 'vue') { + protected function injectEchoConfigurationInApp() + { + // Detect which stack we are using and set appropriate configuration + if ($this->appContainsVueWithTypescript()) { $filePath = resource_path('js/app.ts'); $importPath = 'laravel-echo/vue'; $fileExtension = 'ts'; - } else { // Default to React + } else { + // Default to React $filePath = resource_path('js/app.tsx'); $importPath = 'laravel-echo/react'; $fileExtension = 'tsx'; @@ -215,7 +226,7 @@ protected function injectEchoConfigurationInApp(string $appType = 'react') $insertPos = $pos + strlen($lastImport); $newContents = substr($contents, 0, $insertPos) . "\n" . $echoCode . substr($contents, $insertPos); file_put_contents($filePath, $newContents); - $this->components->info("Echo configuration added to app.{$fileExtension} after imports."); + $this->components->info("Echo configuration added to app.{$fileExtension}."); } } else { // Add the Echo configuration to the top of the file if no import statements are found From ca7b4ac8c49c55615c76894add451572fcb0557a Mon Sep 17 00:00:00 2001 From: Tony Lea Date: Fri, 4 Apr 2025 13:28:54 -0400 Subject: [PATCH 12/12] making methods more efficient --- .../Foundation/Console/BroadcastingInstallCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index 34a183b29f7e..e5b6e6fecddf 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -216,9 +216,10 @@ protected function injectEchoConfigurationInApp() }); JS; - // Match all imports + // Find all imports preg_match_all('/^import .+;$/m', $contents, $matches); + // Add Echo configuration after the last import if (!empty($matches[0])) { $lastImport = end($matches[0]); $pos = strrpos($contents, $lastImport);