Skip to content

Commit c8bf353

Browse files
committed
implemented module functionality
1 parent 7af4b8e commit c8bf353

File tree

6 files changed

+293
-12
lines changed

6 files changed

+293
-12
lines changed

src/module.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,52 @@
1-
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
1+
import {
2+
defineNuxtModule,
3+
addPlugin,
4+
createResolver,
5+
useLogger,
6+
addImportsDir,
7+
} from '@nuxt/kit'
8+
import { defu } from 'defu'
9+
import type { ModuleOptions } from './runtime/types'
210

3-
export interface ModuleOptions {}
11+
const MODULE_NAME = 'nuxt-laravel-echo'
12+
13+
export type ModulePublicRuntimeConfig = { echo: ModuleOptions }
14+
15+
const defaultModuleOptions: ModuleOptions = {
16+
broadcaster: 'reverb',
17+
host: 'localhost',
18+
port: 8080,
19+
scheme: 'https',
20+
transports: ['ws', 'wss'],
21+
authentication: {
22+
baseUrl: 'http://localhost:80',
23+
authEndpoint: '/broadcasting/auth',
24+
csrfEndpoint: '/sanctum/csrf-cookie',
25+
csrfCookie: 'XSRF-TOKEN',
26+
csrfHeader: 'X-XSRF-TOKEN',
27+
},
28+
logLevel: 3,
29+
}
430

531
export default defineNuxtModule<ModuleOptions>({
632
meta: {
7-
name: 'nuxt-laravel-echo',
33+
name: MODULE_NAME,
834
configKey: 'echo',
935
},
10-
// Default configuration options of the Nuxt module
11-
defaults: {},
36+
defaults: defaultModuleOptions,
1237
setup(_options, _nuxt) {
1338
const resolver = createResolver(import.meta.url)
39+
const logger = useLogger(MODULE_NAME, { level: _options.logLevel })
40+
41+
_nuxt.options.build.transpile.push(resolver.resolve('./runtime'))
42+
_nuxt.options.runtimeConfig.public.echo = defu(
43+
_nuxt.options.runtimeConfig.public.echo,
44+
_options
45+
)
46+
47+
addPlugin(resolver.resolve('./runtime/plugin.client'))
48+
addImportsDir(resolver.resolve('./runtime/composables'))
1449

15-
// Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack`
16-
addPlugin(resolver.resolve('./runtime/plugin'))
50+
logger.info('Laravel Echo module initialized!')
1751
},
1852
})

src/runtime/composables/useEcho.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type Echo from 'laravel-echo'
2+
import { useNuxtApp } from '#app'
3+
4+
export const useEcho = (): Echo => {
5+
const { $echo } = useNuxtApp()
6+
7+
return $echo as Echo
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { ModuleOptions } from '../types'
2+
import { useRuntimeConfig } from '#imports'
3+
4+
export const useEchoConfig = (): ModuleOptions => {
5+
return useRuntimeConfig().public.echo as ModuleOptions
6+
}

src/runtime/plugin.client.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import Echo from 'laravel-echo'
2+
import Pusher from 'pusher-js'
3+
import type { Channel, Options, ChannelAuthorizationCallback } from 'pusher-js'
4+
import type { ChannelAuthorizationData } from 'pusher-js/types/src/core/auth/options'
5+
import { createConsola, type ConsolaInstance } from 'consola'
6+
import type { FetchOptions } from 'ofetch'
7+
import { useEchoConfig } from './composables/useEchoConfig'
8+
import type { Authentication, ModuleOptions } from './types'
9+
import { defineNuxtPlugin, createError, useCookie } from '#app'
10+
11+
declare global {
12+
interface Window {
13+
Echo: Echo
14+
Pusher: typeof Pusher
15+
}
16+
}
17+
18+
const MODULE_NAME = 'nuxt-laravel-echo'
19+
20+
function createEchoLogger(logLevel: number) {
21+
return createConsola({ level: logLevel }).withTag(MODULE_NAME)
22+
}
23+
24+
const readCsrfCookie = (name: string) => useCookie(name, { readonly: true })
25+
26+
function createFetchClient(
27+
authentication: Required<Authentication>,
28+
logger: ConsolaInstance
29+
) {
30+
const fetchOptions: FetchOptions = {
31+
baseURL: authentication.baseUrl,
32+
credentials: 'include',
33+
retry: false,
34+
35+
async onRequest(context) {
36+
let csrfToken = readCsrfCookie(authentication.csrfCookie)
37+
38+
if (!csrfToken.value) {
39+
await $fetch(authentication.csrfEndpoint, {
40+
baseURL: authentication.baseUrl,
41+
credentials: 'include',
42+
retry: false,
43+
})
44+
45+
csrfToken = readCsrfCookie(authentication.csrfCookie)
46+
}
47+
48+
if (!csrfToken.value) {
49+
logger.warn(
50+
`${authentication.csrfCookie} cookie is missing, unable to set ${authentication.csrfHeader} header`
51+
)
52+
53+
return
54+
}
55+
56+
context.options.headers = {
57+
...context.options.headers,
58+
[authentication.csrfHeader]: csrfToken.value,
59+
}
60+
},
61+
}
62+
63+
return $fetch.create(fetchOptions)
64+
}
65+
66+
function createAuthorizer(
67+
authentication: Required<Authentication>,
68+
logger: ConsolaInstance
69+
) {
70+
const client = createFetchClient(authentication, logger)
71+
72+
const authorizer = (channel: Channel, _: Options) => {
73+
return {
74+
authorize: (socketId: string, callback: ChannelAuthorizationCallback) => {
75+
const payload = JSON.stringify({
76+
socket_id: socketId,
77+
channel_name: channel.name,
78+
})
79+
80+
client<ChannelAuthorizationData>(authentication.authEndpoint, {
81+
method: 'post',
82+
body: payload,
83+
})
84+
.then((response: ChannelAuthorizationData) =>
85+
callback(null, response)
86+
)
87+
.catch((error: Error | null) => callback(error, null))
88+
},
89+
}
90+
}
91+
92+
return authorizer
93+
}
94+
95+
function prepareEchoOptions(config: ModuleOptions, logger: ConsolaInstance) {
96+
const forceTLS = config.scheme === 'https'
97+
const additionalOptions = config.properties || {}
98+
99+
const authorizer = config.authentication
100+
? createAuthorizer(
101+
config.authentication as Required<Authentication>,
102+
logger
103+
)
104+
: undefined
105+
106+
// Create a Pusher instance
107+
if (config.broadcaster === 'pusher') {
108+
if (forceTLS === false) {
109+
throw createError('Pusher requires a secure connection (schema: "https")')
110+
}
111+
112+
return {
113+
broadcaster: config.broadcaster,
114+
key: config.key,
115+
cluster: config.cluster,
116+
forceTLS,
117+
authorizer,
118+
...additionalOptions,
119+
}
120+
}
121+
122+
// Create a Reverb instance
123+
return {
124+
broadcaster: config.broadcaster,
125+
key: config.key,
126+
wsHost: config.host,
127+
wsPort: config.port,
128+
wssPort: config.port,
129+
forceTLS,
130+
enabledTransports: config.transports,
131+
authorizer,
132+
...additionalOptions,
133+
}
134+
}
135+
136+
export default defineNuxtPlugin((_nuxtApp) => {
137+
const config = useEchoConfig()
138+
const logger = createEchoLogger(config.logLevel)
139+
140+
window.Pusher = Pusher
141+
window.Echo = new Echo(prepareEchoOptions(config, logger))
142+
143+
logger.debug('Laravel Echo client initialized')
144+
145+
return {
146+
provide: {
147+
echo: window.Echo,
148+
},
149+
}
150+
})

src/runtime/plugin.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/runtime/types.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
export interface Authentication {
2+
/**
3+
* The base URL of Laravel application.
4+
* @default 'http://localhost:80'
5+
*/
6+
baseUrl: string
7+
/**
8+
* The endpoint used for channels authentication.
9+
* @default '/broadcasting/auth'
10+
*/
11+
authEndpoint?: string
12+
/**
13+
* The endpoint used for CSRF token retrieval.
14+
* @default '/sanctum/csrf-cookie'
15+
*/
16+
csrfEndpoint?: string
17+
/**
18+
* The name of the CSRF cookie.
19+
* @default 'XSRF-TOKEN'
20+
*/
21+
csrfCookie?: string
22+
/**
23+
* The name of the CSRF header.
24+
* @default 'X-XSRF-TOKEN'
25+
*/
26+
csrfHeader?: string
27+
}
28+
29+
export interface ModuleOptions {
30+
/**
31+
* The Laravel Broadcasting app key for a secure connection.
32+
* @default undefined
33+
*/
34+
key?: string
35+
/**
36+
* The Laravel broadcaster type to use.
37+
* @default 'reverb'
38+
*/
39+
broadcaster: 'reverb' | 'pusher'
40+
/**
41+
* The host to connect to WebSocket.
42+
* @default 'localhost'
43+
*/
44+
host?: string
45+
/**
46+
* The port to connect to WebSocket.
47+
* @default 8080
48+
*/
49+
port?: number
50+
/**
51+
* The scheme to use for the connection.
52+
* @default 'https'
53+
*/
54+
scheme: 'http' | 'https'
55+
/**
56+
* The application cluster to connect to.
57+
* @default undefined
58+
*/
59+
cluster?: string
60+
/**
61+
* The transports to enable for the connection.
62+
* @default ['ws','wss']
63+
*/
64+
transports?: string[]
65+
/**
66+
* Authentication options for Private and Presence channels.
67+
*/
68+
authentication?: Authentication
69+
/**
70+
* The log level to use for the logger.
71+
*
72+
* 0: Fatal and Error
73+
* 1: Warnings
74+
* 2: Normal logs
75+
* 3: Informational logs
76+
* 4: Debug logs
77+
* 5: Trace logs
78+
*
79+
* More details at https://github.com/unjs/consola?tab=readme-ov-file#log-level
80+
* @default 3
81+
*/
82+
logLevel: number
83+
/**
84+
* Additional properties to extend the Echo instance options.
85+
* @default undefined
86+
*/
87+
properties?: object
88+
}

0 commit comments

Comments
 (0)