diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 976d5ac3..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - root: true, - env: { - browser: true, - jest: true, - node: true, - }, - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint"], - extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - rules: { - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "prefer-const": "off", - }, -}; diff --git a/.gitignore b/.gitignore index a5792287..d354db80 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ build/ npm-debug.log package-lock.json +.idea diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..209e3ef4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..c7cacc54 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,41 @@ +import tsParser from "@typescript-eslint/parser"; +import tsPlugin from "@typescript-eslint/eslint-plugin"; + +const config = [ + { + files: [ + "typings/**/*.ts", + "src/**/*.ts" + ], + languageOptions: { + parser: tsParser, // Use the imported parser object + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + project: "./tsconfig.json", // Path to your TypeScript configuration file + }, + }, + plugins: { + "@typescript-eslint": tsPlugin, + }, + rules: { + ...tsPlugin.configs.recommended.rules, + ...tsPlugin.configs["recommended-requiring-type-checking"].rules, + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-function-type": "off", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/no-unsafe-return": "warn", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "no-console": "warn", + "prefer-const": "off", + }, + }, +]; + +export default config; diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 00000000..f2461437 --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,12 @@ +module.exports = { + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + isolatedModules: true + } + ], + }, + extensionsToTreatAsEsm: [".ts"], + testEnvironment: "node", +}; diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index f91c7324..00000000 --- a/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - transform: { - '^.+\\.tsx?$': 'ts-jest', - }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], -}; diff --git a/package.json b/package.json index c846ccff..5afa38cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "laravel-echo", - "version": "1.19.0", + "version": "2.0.0", "description": "Laravel Echo library for beautiful Pusher and Socket.IO integration", "keywords": [ "laravel", @@ -16,41 +16,59 @@ "author": { "name": "Taylor Otwell" }, + "type": "module", "main": "dist/echo.common.js", "module": "dist/echo.js", "types": "dist/echo.d.ts", "scripts": { "build": "npm run compile && npm run declarations", - "compile": "./node_modules/.bin/rollup -c", - "declarations": "./node_modules/.bin/tsc --emitDeclarationOnly", - "lint": "eslint --ext .js,.ts ./src ./tests", + "compile": "rollup -c", + "declarations": "tsc --emitDeclarationOnly", + "lint": "eslint --config eslint.config.mjs", "prepublish": "npm run build", "release": "npm run test && standard-version && git push --follow-tags && npm publish", "test": "jest" }, "devDependencies": { - "@babel/plugin-proposal-decorators": "^7.17.2", - "@babel/plugin-proposal-export-namespace-from": "^7.16.7", - "@babel/plugin-proposal-function-sent": "^7.16.7", - "@babel/plugin-proposal-numeric-separator": "^7.16.7", - "@babel/plugin-proposal-throw-expressions": "^7.16.7", - "@babel/plugin-transform-object-assign": "^7.16.7", - "@babel/preset-env": "^7.16.11", - "@rollup/plugin-babel": "^5.3.1", - "@types/jest": "^27.4.1", - "@types/node": "^18.11.9", - "@typescript-eslint/eslint-plugin": "^5.14.0", - "@typescript-eslint/parser": "^5.14.0", - "eslint": "^8.11.0", - "jest": "^27.5.1", - "rollup": "^2.70.1", - "rollup-plugin-typescript2": "^0.31.2", + "@babel/core": "^7.26.7", + "@babel/plugin-proposal-decorators": "^7.25.9", + "@babel/plugin-proposal-function-sent": "^7.25.9", + "@babel/plugin-proposal-throw-expressions": "^7.25.9", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-assign": "^7.25.9", + "@babel/preset-env": "^7.26.7", + "@rollup/plugin-babel": "^6.0.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", + "@types/jest": "^29.5", + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "eslint": "^9.0.0", + "jest": "^29.7.0", + "pusher-js": "^8.0", + "rollup": "^3.0.0", + "socket.io-client": "^4.0", "standard-version": "^9.3.2", - "ts-jest": "^27.1.3", - "tslib": "^2.3.1", - "typescript": "^4.6.2" + "ts-jest": "^29.2.5", + "typescript": "^5.7.0" }, "engines": { - "node": ">=10" + "node": ">=20" + }, + "exports": { + ".": { + "import": "./dist/echo.js", + "require": "./dist/echo.common.js", + "types": "./dist/echo.d.ts" + }, + "./iife": "./dist/echo.iife.js" + }, + "overrides": { + "glob": "^9.0.0" + }, + "dependencies": { + "tslib": "^2.8.1" } } diff --git a/rollup.config.js b/rollup.config.js index b62c8ce7..d8e2b737 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,23 +1,6 @@ import babel from '@rollup/plugin-babel'; -import typescript from 'rollup-plugin-typescript2'; - -const plugins = [ - typescript(), - babel({ - babelHelpers: 'bundled', - exclude: 'node_modules/**', - extensions: ['.ts'], - presets: ['@babel/preset-env'], - plugins: [ - ['@babel/plugin-proposal-decorators', { legacy: true }], - '@babel/plugin-proposal-function-sent', - '@babel/plugin-proposal-export-namespace-from', - '@babel/plugin-proposal-numeric-separator', - '@babel/plugin-proposal-throw-expressions', - '@babel/plugin-transform-object-assign', - ], - }), -]; +import typescript from '@rollup/plugin-typescript'; +import resolve from '@rollup/plugin-node-resolve'; export default [ { @@ -26,11 +9,42 @@ export default [ { file: './dist/echo.js', format: 'esm' }, { file: './dist/echo.common.js', format: 'cjs' }, ], - plugins, + plugins: [ + resolve(), + typescript({ + tsconfig: './tsconfig.json', // Ensures Rollup aligns with your TS settings + }), + babel({ + babelHelpers: 'bundled', + extensions: ['.ts'], + exclude: 'node_modules/**', + presets: ['@babel/preset-env'], + plugins: [ + '@babel/plugin-transform-numeric-separator', + '@babel/plugin-transform-export-namespace-from', + ['@babel/plugin-proposal-decorators', { legacy: true }], + '@babel/plugin-proposal-function-sent', + '@babel/plugin-proposal-throw-expressions', + '@babel/plugin-transform-object-assign', + ], + }), + ], + external: ['jquery', 'axios', 'vue', '@hotwired/turbo', 'tslib'], // Compatible packages not included in the bundle }, { input: './src/index.iife.ts', output: [{ file: './dist/echo.iife.js', format: 'iife', name: 'Echo' }], - plugins, + plugins: [ + resolve(), + typescript({ + tsconfig: './tsconfig.json', + }), + babel({ + babelHelpers: 'bundled', + extensions: ['.ts'], + exclude: 'node_modules/**', + }), + ], + external: ['jquery', 'axios', 'vue', '@hotwired/turbo', 'tslib'], // Compatible packages not included in the bundle }, ]; diff --git a/src/channel/channel.ts b/src/channel/channel.ts index aeb3b03b..53b324d5 100644 --- a/src/channel/channel.ts +++ b/src/channel/channel.ts @@ -1,3 +1,6 @@ +import type { EchoOptionsWithDefaults } from '../connector'; +import type { BroadcastDriver } from '../echo'; + /** * This class represents a basic channel. */ @@ -5,46 +8,46 @@ export abstract class Channel { /** * The Echo options. */ - options: any; + options: EchoOptionsWithDefaults; /** * Listen for an event on the channel instance. */ - abstract listen(event: string, callback: Function): this; + abstract listen(event: string, callback: CallableFunction): this; /** * Listen for a whisper event on the channel instance. */ - listenForWhisper(event: string, callback: Function): this { + listenForWhisper(event: string, callback: CallableFunction): this { return this.listen('.client-' + event, callback); } /** * Listen for an event on the channel instance. */ - notification(callback: Function): this { + notification(callback: CallableFunction): this { return this.listen('.Illuminate\\Notifications\\Events\\BroadcastNotificationCreated', callback); } /** * Stop listening to an event on the channel instance. */ - abstract stopListening(event: string, callback?: Function): this; + abstract stopListening(event: string, callback?: CallableFunction): this; /** * Stop listening for a whisper event on the channel instance. */ - stopListeningForWhisper(event: string, callback?: Function): this { + stopListeningForWhisper(event: string, callback?: CallableFunction): this { return this.stopListening('.client-' + event, callback); } /** * Register a callback to be called anytime a subscription succeeds. */ - abstract subscribed(callback: Function): this; + abstract subscribed(callback: CallableFunction): this; /** * Register a callback to be called anytime an error occurs. */ - abstract error(callback: Function): this; + abstract error(callback: CallableFunction): this; } diff --git a/src/channel/null-channel.ts b/src/channel/null-channel.ts index e79b1fa4..4b16f974 100644 --- a/src/channel/null-channel.ts +++ b/src/channel/null-channel.ts @@ -7,7 +7,7 @@ export class NullChannel extends Channel { /** * Subscribe to a channel. */ - subscribe(): any { + subscribe(): void { // } @@ -21,42 +21,42 @@ export class NullChannel extends Channel { /** * Listen for an event on the channel instance. */ - listen(event: string, callback: Function): this { + listen(_event: string, _callback: CallableFunction): this { return this; } /** * Listen for all events on the channel instance. */ - listenToAll(callback: Function): this { + listenToAll(_callback: CallableFunction): this { return this; } /** * Stop listening for an event on the channel instance. */ - stopListening(event: string, callback?: Function): this { + stopListening(_event: string, _callback?: CallableFunction): this { return this; } /** * Register a callback to be called anytime a subscription succeeds. */ - subscribed(callback: Function): this { + subscribed(_callback: CallableFunction): this { return this; } /** * Register a callback to be called anytime an error occurs. */ - error(callback: Function): this { + error(_callback: CallableFunction): this { return this; } /** * Bind a channel to an event. */ - on(event: string, callback: Function): this { + on(_event: string, _callback: CallableFunction): this { return this; } } diff --git a/src/channel/null-encrypted-private-channel.ts b/src/channel/null-encrypted-private-channel.ts index fc918117..4f967d11 100644 --- a/src/channel/null-encrypted-private-channel.ts +++ b/src/channel/null-encrypted-private-channel.ts @@ -7,7 +7,7 @@ export class NullEncryptedPrivateChannel extends NullChannel { /** * Send a whisper event to other clients in the channel. */ - whisper(eventName: string, data: any): this { + whisper(_eventName: string, _data: Record): this { return this; } } diff --git a/src/channel/null-presence-channel.ts b/src/channel/null-presence-channel.ts index 7bf64cb9..6f1abdc4 100644 --- a/src/channel/null-presence-channel.ts +++ b/src/channel/null-presence-channel.ts @@ -1,5 +1,5 @@ import { NullPrivateChannel } from './null-private-channel'; -import { PresenceChannel } from './presence-channel'; +import type { PresenceChannel } from './presence-channel'; /** * This class represents a null presence channel. @@ -8,28 +8,28 @@ export class NullPresenceChannel extends NullPrivateChannel implements PresenceC /** * Register a callback to be called anytime the member list changes. */ - here(callback: Function): this { + here(_callback: CallableFunction): this { return this; } /** * Listen for someone joining the channel. */ - joining(callback: Function): this { + joining(_callback: CallableFunction): this { return this; } /** * Send a whisper event to other clients in the channel. */ - whisper(eventName: string, data: any): this { + whisper(_eventName: string, _data: Record): this { return this; } /** * Listen for someone leaving the channel. */ - leaving(callback: Function): this { + leaving(_callback: CallableFunction): this { return this; } } diff --git a/src/channel/null-private-channel.ts b/src/channel/null-private-channel.ts index 862ac5c2..5e61a226 100644 --- a/src/channel/null-private-channel.ts +++ b/src/channel/null-private-channel.ts @@ -7,7 +7,7 @@ export class NullPrivateChannel extends NullChannel { /** * Send a whisper event to other clients in the channel. */ - whisper(eventName: string, data: any): this { + whisper(_eventName: string, _data: Record): this { return this; } } diff --git a/src/channel/presence-channel.ts b/src/channel/presence-channel.ts index 2b2063e2..9202b55a 100644 --- a/src/channel/presence-channel.ts +++ b/src/channel/presence-channel.ts @@ -1,4 +1,4 @@ -import { Channel } from './channel'; +import type { Channel } from './channel'; /** * This interface represents a presence channel. @@ -7,20 +7,20 @@ export interface PresenceChannel extends Channel { /** * Register a callback to be called anytime the member list changes. */ - here(callback: Function): this; + here(callback: CallableFunction): this; /** * Listen for someone joining the channel. */ - joining(callback: Function): this; + joining(callback: CallableFunction): this; /** * Send a whisper event to other clients in the channel. */ - whisper(eventName: string, data: any): this; + whisper(eventName: string, data: Record): this; /** * Listen for someone leaving the channel. */ - leaving(callback: Function): this; + leaving(callback: CallableFunction): this; } diff --git a/src/channel/pusher-channel.ts b/src/channel/pusher-channel.ts index 07234239..e9894115 100644 --- a/src/channel/pusher-channel.ts +++ b/src/channel/pusher-channel.ts @@ -1,24 +1,23 @@ import { EventFormatter } from '../util'; import { Channel } from './channel'; +import type Pusher from 'pusher-js'; +import type { Channel as BasePusherChannel } from 'pusher-js'; +import type { EchoOptionsWithDefaults } from '../connector'; +import type { BroadcastDriver } from '../echo'; /** * This class represents a Pusher channel. */ -export class PusherChannel extends Channel { +export class PusherChannel extends Channel { /** * The Pusher client instance. */ - pusher: any; + pusher: Pusher; /** * The name of the channel. */ - name: any; - - /** - * Channel options. - */ - options: any; + name: string; /** * The event formatter. @@ -28,12 +27,12 @@ export class PusherChannel extends Channel { /** * The subscription of the channel. */ - subscription: any; + subscription: BasePusherChannel; /** * Create a new class instance. */ - constructor(pusher: any, name: any, options: any) { + constructor(pusher: Pusher, name: string, options: EchoOptionsWithDefaults) { super(); this.name = name; @@ -47,7 +46,7 @@ export class PusherChannel extends Channel { /** * Subscribe to a Pusher channel. */ - subscribe(): any { + subscribe(): void { this.subscription = this.pusher.subscribe(this.name); } @@ -61,7 +60,7 @@ export class PusherChannel extends Channel { /** * Listen for an event on the channel instance. */ - listen(event: string, callback: Function): this { + listen(event: string, callback: CallableFunction): this { this.on(this.eventFormatter.format(event), callback); return this; @@ -70,13 +69,13 @@ export class PusherChannel extends Channel { /** * Listen for all events on the channel instance. */ - listenToAll(callback: Function): this { - this.subscription.bind_global((event, data) => { + listenToAll(callback: CallableFunction): this { + this.subscription.bind_global((event: string, data: unknown) => { if (event.startsWith('pusher:')) { return; } - let namespace = this.options.namespace.replace(/\./g, '\\'); + let namespace = String(this.options.namespace ?? '').replace(/\./g, '\\'); let formattedEvent = event.startsWith(namespace) ? event.substring(namespace.length + 1) : '.' + event; @@ -89,7 +88,7 @@ export class PusherChannel extends Channel { /** * Stop listening for an event on the channel instance. */ - stopListening(event: string, callback?: Function): this { + stopListening(event: string, callback?: CallableFunction): this { if (callback) { this.subscription.unbind(this.eventFormatter.format(event), callback); } else { @@ -102,7 +101,7 @@ export class PusherChannel extends Channel { /** * Stop listening for all events on the channel instance. */ - stopListeningToAll(callback?: Function): this { + stopListeningToAll(callback?: CallableFunction): this { if (callback) { this.subscription.unbind_global(callback); } else { @@ -115,7 +114,7 @@ export class PusherChannel extends Channel { /** * Register a callback to be called anytime a subscription succeeds. */ - subscribed(callback: Function): this { + subscribed(callback: CallableFunction): this { this.on('pusher:subscription_succeeded', () => { callback(); }); @@ -126,8 +125,8 @@ export class PusherChannel extends Channel { /** * Register a callback to be called anytime a subscription error occurs. */ - error(callback: Function): this { - this.on('pusher:subscription_error', (status) => { + error(callback: CallableFunction): this { + this.on('pusher:subscription_error', (status: Record) => { callback(status); }); @@ -137,7 +136,7 @@ export class PusherChannel extends Channel { /** * Bind a channel to an event. */ - on(event: string, callback: Function): this { + on(event: string, callback: CallableFunction): this { this.subscription.bind(event, callback); return this; diff --git a/src/channel/pusher-encrypted-private-channel.ts b/src/channel/pusher-encrypted-private-channel.ts index 299e8da9..f3c2068f 100644 --- a/src/channel/pusher-encrypted-private-channel.ts +++ b/src/channel/pusher-encrypted-private-channel.ts @@ -1,13 +1,16 @@ import { PusherChannel } from './pusher-channel'; +import type { BroadcastDriver } from '../echo'; /** * This class represents a Pusher private channel. */ -export class PusherEncryptedPrivateChannel extends PusherChannel { +export class PusherEncryptedPrivateChannel< + TBroadcastDriver extends BroadcastDriver +> extends PusherChannel { /** * Send a whisper event to other clients in the channel. */ - whisper(eventName: string, data: any): this { + whisper(eventName: string, data: Record): this { this.pusher.channels.channels[this.name].trigger(`client-${eventName}`, data); return this; diff --git a/src/channel/pusher-presence-channel.ts b/src/channel/pusher-presence-channel.ts index a1fc8cf6..83720aae 100644 --- a/src/channel/pusher-presence-channel.ts +++ b/src/channel/pusher-presence-channel.ts @@ -1,15 +1,19 @@ -import { PresenceChannel } from './presence-channel'; +import type { PresenceChannel } from './presence-channel'; import { PusherPrivateChannel } from './pusher-private-channel'; +import type { BroadcastDriver } from '../echo'; /** * This class represents a Pusher presence channel. */ -export class PusherPresenceChannel extends PusherPrivateChannel implements PresenceChannel { +export class PusherPresenceChannel + extends PusherPrivateChannel + implements PresenceChannel +{ /** * Register a callback to be called anytime the member list changes. */ - here(callback: Function): this { - this.on('pusher:subscription_succeeded', (data) => { + here(callback: CallableFunction): this { + this.on('pusher:subscription_succeeded', (data: Record) => { callback(Object.keys(data.members).map((k) => data.members[k])); }); @@ -19,8 +23,8 @@ export class PusherPresenceChannel extends PusherPrivateChannel implements Prese /** * Listen for someone joining the channel. */ - joining(callback: Function): this { - this.on('pusher:member_added', (member) => { + joining(callback: CallableFunction): this { + this.on('pusher:member_added', (member: Record) => { callback(member.info); }); @@ -30,7 +34,7 @@ export class PusherPresenceChannel extends PusherPrivateChannel implements Prese /** * Send a whisper event to other clients in the channel. */ - whisper(eventName: string, data: any): this { + whisper(eventName: string, data: Record): this { this.pusher.channels.channels[this.name].trigger(`client-${eventName}`, data); return this; @@ -39,8 +43,8 @@ export class PusherPresenceChannel extends PusherPrivateChannel implements Prese /** * Listen for someone leaving the channel. */ - leaving(callback: Function): this { - this.on('pusher:member_removed', (member) => { + leaving(callback: CallableFunction): this { + this.on('pusher:member_removed', (member: Record) => { callback(member.info); }); diff --git a/src/channel/pusher-private-channel.ts b/src/channel/pusher-private-channel.ts index 7952886d..59f36e92 100644 --- a/src/channel/pusher-private-channel.ts +++ b/src/channel/pusher-private-channel.ts @@ -1,13 +1,14 @@ import { PusherChannel } from './pusher-channel'; +import type { BroadcastDriver } from '../echo'; /** * This class represents a Pusher private channel. */ -export class PusherPrivateChannel extends PusherChannel { +export class PusherPrivateChannel extends PusherChannel { /** * Send a whisper event to other clients in the channel. */ - whisper(eventName: string, data: any): this { + whisper(eventName: string, data: Record): this { this.pusher.channels.channels[this.name].trigger(`client-${eventName}`, data); return this; diff --git a/src/channel/socketio-channel.ts b/src/channel/socketio-channel.ts index 47dc00c1..2aed222e 100644 --- a/src/channel/socketio-channel.ts +++ b/src/channel/socketio-channel.ts @@ -1,5 +1,8 @@ import { EventFormatter } from '../util'; import { Channel } from './channel'; +import type { Socket } from 'socket.io-client'; +import type { EchoOptionsWithDefaults } from '../connector'; +import type { BroadcastDriver } from '../echo'; /** * This class represents a Socket.io channel. @@ -8,17 +11,12 @@ export class SocketIoChannel extends Channel { /** * The Socket.io client instance. */ - socket: any; + socket: Socket; /** * The name of the channel. */ - name: any; - - /** - * Channel options. - */ - options: any; + name: string; /** * The event formatter. @@ -28,17 +26,17 @@ export class SocketIoChannel extends Channel { /** * The event callbacks applied to the socket. */ - events: any = {}; + events: Record = {}; /** * User supplied callbacks for events on this channel. */ - private listeners: any = {}; + private listeners: Record = {}; /** * Create a new class instance. */ - constructor(socket: any, name: string, options: any) { + constructor(socket: Socket, name: string, options: EchoOptionsWithDefaults) { super(); this.name = name; @@ -74,7 +72,7 @@ export class SocketIoChannel extends Channel { /** * Listen for an event on the channel instance. */ - listen(event: string, callback: Function): this { + listen(event: string, callback: CallableFunction): this { this.on(this.eventFormatter.format(event), callback); return this; @@ -83,7 +81,7 @@ export class SocketIoChannel extends Channel { /** * Stop listening for an event on the channel instance. */ - stopListening(event: string, callback?: Function): this { + stopListening(event: string, callback?: CallableFunction): this { this.unbindEvent(this.eventFormatter.format(event), callback); return this; @@ -92,8 +90,8 @@ export class SocketIoChannel extends Channel { /** * Register a callback to be called anytime a subscription succeeds. */ - subscribed(callback: Function): this { - this.on('connect', (socket) => { + subscribed(callback: CallableFunction): this { + this.on('connect', (socket: Socket) => { callback(socket); }); @@ -103,18 +101,18 @@ export class SocketIoChannel extends Channel { /** * Register a callback to be called anytime an error occurs. */ - error(callback: Function): this { + error(_callback: CallableFunction): this { return this; } /** * Bind the channel's socket to an event and store the callback. */ - on(event: string, callback: Function): this { + on(event: string, callback: CallableFunction): this { this.listeners[event] = this.listeners[event] || []; if (!this.events[event]) { - this.events[event] = (channel, data) => { + this.events[event] = (channel: string, data: unknown) => { if (this.name === channel && this.listeners[event]) { this.listeners[event].forEach((cb) => cb(data)); } @@ -140,7 +138,7 @@ export class SocketIoChannel extends Channel { /** * Unbind the listeners for the given event. */ - protected unbindEvent(event: string, callback?: Function): void { + protected unbindEvent(event: string, callback?: CallableFunction): void { this.listeners[event] = this.listeners[event] || []; if (callback) { diff --git a/src/channel/socketio-presence-channel.ts b/src/channel/socketio-presence-channel.ts index cf0110fb..a12a1348 100644 --- a/src/channel/socketio-presence-channel.ts +++ b/src/channel/socketio-presence-channel.ts @@ -1,4 +1,4 @@ -import { PresenceChannel } from './presence-channel'; +import type { PresenceChannel } from './presence-channel'; import { SocketIoPrivateChannel } from './socketio-private-channel'; /** @@ -8,8 +8,8 @@ export class SocketIoPresenceChannel extends SocketIoPrivateChannel implements P /** * Register a callback to be called anytime the member list changes. */ - here(callback: Function): this { - this.on('presence:subscribed', (members: any[]) => { + here(callback: CallableFunction): this { + this.on('presence:subscribed', (members: Record[]) => { callback(members.map((m) => m.user_info)); }); @@ -19,8 +19,8 @@ export class SocketIoPresenceChannel extends SocketIoPrivateChannel implements P /** * Listen for someone joining the channel. */ - joining(callback: Function): this { - this.on('presence:joining', (member) => callback(member.user_info)); + joining(callback: CallableFunction): this { + this.on('presence:joining', (member: Record) => callback(member.user_info)); return this; } @@ -28,7 +28,7 @@ export class SocketIoPresenceChannel extends SocketIoPrivateChannel implements P /** * Send a whisper event to other clients in the channel. */ - whisper(eventName: string, data: any): this { + whisper(eventName: string, data: unknown): this { this.socket.emit('client event', { channel: this.name, event: `client-${eventName}`, @@ -41,8 +41,8 @@ export class SocketIoPresenceChannel extends SocketIoPrivateChannel implements P /** * Listen for someone leaving the channel. */ - leaving(callback: Function): this { - this.on('presence:leaving', (member) => callback(member.user_info)); + leaving(callback: CallableFunction): this { + this.on('presence:leaving', (member: Record) => callback(member.user_info)); return this; } diff --git a/src/channel/socketio-private-channel.ts b/src/channel/socketio-private-channel.ts index aa66ed3b..3d45884b 100644 --- a/src/channel/socketio-private-channel.ts +++ b/src/channel/socketio-private-channel.ts @@ -7,7 +7,7 @@ export class SocketIoPrivateChannel extends SocketIoChannel { /** * Send a whisper event to other clients in the channel. */ - whisper(eventName: string, data: any): this { + whisper(eventName: string, data: unknown): this { this.socket.emit('client event', { channel: this.name, event: `client-${eventName}`, diff --git a/src/connector/connector.ts b/src/connector/connector.ts index 0291ef70..dfd2053f 100644 --- a/src/connector/connector.ts +++ b/src/connector/connector.ts @@ -1,10 +1,35 @@ -import { Channel, PresenceChannel } from '../channel'; +import type { Channel, PresenceChannel } from '../channel'; +import type { BroadcastDriver, EchoOptions } from '../echo'; -export abstract class Connector { +export type EchoOptionsWithDefaults = { + broadcaster: TBroadcaster; + auth: { + headers: Record; + }; + authEndpoint: string; + userAuthentication: { + endpoint: string; + headers: Record; + }; + csrfToken: string | null; + bearerToken: string | null; + host: string | null; + key: string | null; + namespace: string | false; + + [key: string]: any; +}; + +export abstract class Connector< + TBroadcastDriver extends BroadcastDriver, + TPublic extends Channel, + TPrivate extends Channel, + TPresence extends PresenceChannel +> { /** * Default connector options. */ - private _defaultOptions: any = { + public static readonly _defaultOptions = { auth: { headers: {}, }, @@ -13,23 +38,22 @@ export abstract class Connector; /** * Create a new class instance. */ - constructor(options: any) { + constructor(options: EchoOptions) { this.setOptions(options); this.connect(); } @@ -37,8 +61,12 @@ export abstract class Connector): void { + this.options = { + ...Connector._defaultOptions, + ...options, + broadcaster: options.broadcaster as TBroadcastDriver, + }; let token = this.csrfToken(); @@ -53,8 +81,6 @@ export abstract class Connector { +export class NullConnector extends Connector<'null', NullChannel, NullPrivateChannel, NullPresenceChannel> { /** * All of the subscribed channel names. */ @@ -20,49 +20,49 @@ export class NullConnector extends Connector + | PusherPrivateChannel + | PusherEncryptedPrivateChannel + | PusherPresenceChannel; /** * This class creates a connector to Pusher. */ -export class PusherConnector extends Connector { +export class PusherConnector extends Connector< + TBroadcastDriver, + PusherChannel, + PusherPrivateChannel, + PusherPresenceChannel +> { /** * The Pusher instance. */ - pusher: any; + pusher: Pusher; /** * All of the subscribed channel names. */ - channels: any = {}; + channels: Record = {}; + + options: EchoOptionsWithDefaults & { + key: string; + Pusher?: typeof Pusher; + } & PusherOptions; /** * Create a fresh Pusher connection. */ connect(): void { if (typeof this.options.client !== 'undefined') { - this.pusher = this.options.client; + this.pusher = this.options.client as Pusher; } else if (this.options.Pusher) { this.pusher = new this.options.Pusher(this.options.key, this.options); + } else if (typeof window !== 'undefined' && typeof window.Pusher !== 'undefined') { + this.pusher = new window.Pusher(this.options.key, this.options); } else { - this.pusher = new Pusher(this.options.key, this.options); + throw new Error('Pusher client not found. Should be globally available or passed via options.client'); } } @@ -45,7 +59,7 @@ export class PusherConnector extends Connector { if (!this.channels['private-' + name]) { this.channels['private-' + name] = new PusherPrivateChannel(this.pusher, 'private-' + name, this.options); } - return this.channels['private-' + name]; + return this.channels['private-' + name] as PusherPrivateChannel; } /** * Get a private encrypted channel instance by name. */ - encryptedPrivateChannel(name: string): PusherEncryptedPrivateChannel { + encryptedPrivateChannel(name: string): PusherEncryptedPrivateChannel { if (!this.channels['private-encrypted-' + name]) { this.channels['private-encrypted-' + name] = new PusherEncryptedPrivateChannel( this.pusher, @@ -83,13 +97,13 @@ export class PusherConnector extends Connector; } /** * Get a presence channel instance by name. */ - presenceChannel(name: string): PusherPresenceChannel { + presenceChannel(name: string): PusherPresenceChannel { if (!this.channels['presence-' + name]) { this.channels['presence-' + name] = new PusherPresenceChannel( this.pusher, @@ -98,7 +112,7 @@ export class PusherConnector extends Connector; } /** @@ -107,7 +121,7 @@ export class PusherConnector extends Connector { + channels.forEach((name: string) => { this.leaveChannel(name); }); } diff --git a/src/connector/socketio-connector.ts b/src/connector/socketio-connector.ts index 11d038cf..ad4fdc8a 100644 --- a/src/connector/socketio-connector.ts +++ b/src/connector/socketio-connector.ts @@ -1,16 +1,22 @@ import { Connector } from './connector'; -import { SocketIoChannel, SocketIoPrivateChannel, SocketIoPresenceChannel } from './../channel'; +import { SocketIoChannel, SocketIoPrivateChannel, SocketIoPresenceChannel } from '../channel'; +import type { io, ManagerOptions, Socket, SocketOptions } from 'socket.io-client'; type AnySocketIoChannel = SocketIoChannel | SocketIoPrivateChannel | SocketIoPresenceChannel; /** - * This class creates a connnector to a Socket.io server. + * This class creates a connector to a Socket.io server. */ -export class SocketIoConnector extends Connector { +export class SocketIoConnector extends Connector< + 'socket.io', + SocketIoChannel, + SocketIoPrivateChannel, + SocketIoPresenceChannel +> { /** * The Socket.io connection instance. */ - socket: any; + socket: Socket; /** * All of the subscribed channel names. @@ -23,27 +29,25 @@ export class SocketIoConnector extends Connector); this.socket.on('reconnect', () => { Object.values(this.channels).forEach((channel) => { channel.subscribe(); }); }); - - return this.socket; } /** * Get socket.io module from global scope or options. */ - getSocketIO(): any { + getSocketIO(): typeof io { if (typeof this.options.client !== 'undefined') { - return this.options.client; + return this.options.client as typeof io; } - if (typeof io !== 'undefined') { - return io; + if (typeof window !== 'undefined' && typeof window.io !== 'undefined') { + return window.io; } throw new Error('Socket.io client not found. Should be globally available or passed via options.client'); @@ -52,7 +56,7 @@ export class SocketIoConnector extends Connector { /** * The broadcasting connector. */ - connector: Broadcaster[T]['connector']; + connector: Broadcaster[Exclude]['connector']; /** * The Echo options. @@ -53,19 +53,22 @@ export default class Echo { * Create a new connection. */ connect(): void { - if (this.options.broadcaster == 'reverb') { - this.connector = new PusherConnector({ ...this.options, cluster: '' }); - } else if (this.options.broadcaster == 'pusher') { - this.connector = new PusherConnector(this.options); - } else if (this.options.broadcaster == 'socket.io') { - this.connector = new SocketIoConnector(this.options); - } else if (this.options.broadcaster == 'null') { - this.connector = new NullConnector(this.options); - } else if (typeof this.options.broadcaster == 'function' && isConstructor(this.options.broadcaster)) { + if (this.options.broadcaster === 'reverb') { + this.connector = new PusherConnector({ + ...this.options, + cluster: '', + } as EchoOptions<'reverb'>); + } else if (this.options.broadcaster === 'pusher') { + this.connector = new PusherConnector(this.options as EchoOptions<'pusher'>); + } else if (this.options.broadcaster === 'socket.io') { + this.connector = new SocketIoConnector(this.options as EchoOptions<'socket.io'>); + } else if (this.options.broadcaster === 'null') { + this.connector = new NullConnector(this.options as EchoOptions<'null'>); + } else if (typeof this.options.broadcaster === 'function' && isConstructor(this.options.broadcaster)) { this.connector = new this.options.broadcaster(this.options as EchoOptions<'function'>); } else { throw new Error( - `Broadcaster ${typeof this.options.broadcaster} ${this.options.broadcaster} is not supported.` + `Broadcaster ${typeof this.options.broadcaster} ${String(this.options.broadcaster)} is not supported.` ); } } @@ -110,7 +113,7 @@ export default class Echo { /** * Listen for an event on a channel instance. */ - listen(channel: string, event: string, callback: Function): Broadcaster[T]['public'] { + listen(channel: string, event: string, callback: CallableFunction): Broadcaster[T]['public'] { return this.connector.listen(channel, event, callback); } @@ -125,21 +128,27 @@ export default class Echo { * Get a private encrypted channel instance by name. */ encryptedPrivate(channel: string): Broadcaster[T]['encrypted'] { - if ((this.connector as any) instanceof SocketIoConnector) { - throw new Error( - `Broadcaster ${typeof this.options.broadcaster} ${ - this.options.broadcaster - } does not support encrypted private channels.` - ); + if (this.connectorSupportsEncryptedPrivateChannels(this.connector)) { + return this.connector.encryptedPrivateChannel(channel); } - return this.connector.encryptedPrivateChannel(channel); + throw new Error( + `Broadcaster ${typeof this.options.broadcaster} ${String( + this.options.broadcaster + )} does not support encrypted private channels.` + ); + } + + private connectorSupportsEncryptedPrivateChannels( + connector: unknown + ): connector is PusherConnector | NullConnector { + return connector instanceof PusherConnector || connector instanceof NullConnector; } /** * Get the Socket ID for the connection. */ - socketId(): string { + socketId(): string | undefined { return this.connector.socketId(); } @@ -169,7 +178,7 @@ export default class Echo { * Register a Vue HTTP interceptor to add the X-Socket-ID header. */ registerVueRequestInterceptor(): void { - Vue.http.interceptors.push((request, next) => { + Vue.http.interceptors.push((request: Record, next: CallableFunction) => { if (this.socketId()) { request.headers.set('X-Socket-ID', this.socketId()); } @@ -182,7 +191,7 @@ export default class Echo { * Register an Axios HTTP interceptor to add the X-Socket-ID header. */ registerAxiosRequestInterceptor(): void { - axios.interceptors.request.use((config) => { + axios.interceptors.request.use((config: Record) => { if (this.socketId()) { config.headers['X-Socket-Id'] = this.socketId(); } @@ -196,7 +205,7 @@ export default class Echo { */ registerjQueryAjaxSetup(): void { if (typeof jQuery.ajax != 'undefined') { - jQuery.ajaxPrefilter((options, originalOptions, xhr) => { + jQuery.ajaxPrefilter((_options: any, _originalOptions: any, xhr: Record) => { if (this.socketId()) { xhr.setRequestHeader('X-Socket-Id', this.socketId()); } @@ -208,7 +217,7 @@ export default class Echo { * Register the Turbo Request interceptor to add the X-Socket-ID header. */ registerTurboRequestInterceptor(): void { - document.addEventListener('turbo:before-fetch-request', (event: any) => { + document.addEventListener('turbo:before-fetch-request', (event: Record) => { event.detail.fetchOptions.headers['X-Socket-Id'] = this.socketId(); }); } @@ -217,27 +226,27 @@ export default class Echo { /** * Export channel classes for TypeScript. */ -export { Connector, Channel, PresenceChannel }; +export { Connector, Channel, type PresenceChannel }; export { EventFormatter } from './util'; /** * Specifies the broadcaster */ -type Broadcaster = { +export type Broadcaster = { reverb: { - connector: PusherConnector; - public: PusherChannel; - private: PusherPrivateChannel; - encrypted: PusherEncryptedPrivateChannel; - presence: PusherPresenceChannel; + connector: PusherConnector<'reverb'>; + public: PusherChannel<'reverb'>; + private: PusherPrivateChannel<'reverb'>; + encrypted: PusherEncryptedPrivateChannel<'reverb'>; + presence: PusherPresenceChannel<'reverb'>; }; pusher: { - connector: PusherConnector; - public: PusherChannel; - private: PusherPrivateChannel; - encrypted: PusherEncryptedPrivateChannel; - presence: PusherPresenceChannel; + connector: PusherConnector<'pusher'>; + public: PusherChannel<'pusher'>; + private: PusherPrivateChannel<'pusher'>; + encrypted: PusherEncryptedPrivateChannel<'pusher'>; + presence: PusherPresenceChannel<'pusher'>; }; 'socket.io': { connector: SocketIoConnector; @@ -264,11 +273,29 @@ type Broadcaster = { type Constructor = new (...args: any[]) => T; -type EchoOptions = { +export type BroadcastDriver = Exclude; + +export type EchoOptions = { /** * The broadcast connector. */ - broadcaster: T extends 'function' ? Constructor> : T; + broadcaster: TBroadcaster extends 'function' + ? Constructor> + : TBroadcaster; + + auth?: { + headers: Record; + }; + authEndpoint?: string; + userAuthentication?: { + endpoint: string; + headers: Record; + }; + csrfToken?: string | null; + bearerToken?: string | null; + host?: string | null; + key?: string | null; + namespace?: string | false; [key: string]: any; }; diff --git a/src/util/event-formatter.ts b/src/util/event-formatter.ts index 122c9120..27f624c4 100644 --- a/src/util/event-formatter.ts +++ b/src/util/event-formatter.ts @@ -5,7 +5,7 @@ export class EventFormatter { /** * Create a new class instance. */ - constructor(private namespace: string | boolean) { + constructor(private namespace: string | boolean | undefined) { // } diff --git a/src/util/index.ts b/src/util/index.ts index cf233c22..2635f064 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,9 +1,12 @@ -function isConstructor(obj: any): obj is new (...args: any[]) => any { +function isConstructor(obj: unknown): obj is new (...args: any[]) => any { try { - new obj(); + new (obj as new (...args: any[]) => any)(); } catch (err) { - if (err.message.includes('is not a constructor')) return false; + if (err instanceof Error && err.message.includes('is not a constructor')) { + return false; + } } + return true; } diff --git a/tests/channel/socketio-channel.test.ts b/tests/channel/socketio-channel.test.ts index ead16fdf..1211ddaa 100644 --- a/tests/channel/socketio-channel.test.ts +++ b/tests/channel/socketio-channel.test.ts @@ -1,21 +1,27 @@ import { SocketIoChannel } from '../../src/channel'; +import type { Socket } from 'socket.io-client'; +import { Connector } from '../../src/connector'; describe('SocketIoChannel', () => { - let channel; - let socket; + let channel: SocketIoChannel; + let socket: Socket; beforeEach(() => { const channelName = 'some.channel'; - let listeners = []; + let listeners: any[] = []; socket = { - emit: (event, data) => listeners.filter(([e]) => e === event).forEach(([, fn]) => fn(channelName, data)), - on: (event, fn) => listeners.push([event, fn]), - removeListener: (event, fn) => { + emit: (event: any, data: unknown) => { + listeners.filter(([e]) => e === event).forEach(([, fn]) => fn(channelName, data)); + }, + on: (event: any, fn): any => listeners.push([event, fn]), + removeListener: (event: any, fn: any) => { listeners = listeners.filter(([e, f]) => (!fn ? e !== event : e !== event || f !== fn)); }, - }; + } as Socket; channel = new SocketIoChannel(socket, channelName, { + broadcaster: 'socket.io', + ...Connector._defaultOptions, namespace: false, }); }); diff --git a/tests/util/event-formatter.test.ts b/tests/util/event-formatter.test.ts index 1c7314ce..49492ab1 100644 --- a/tests/util/event-formatter.test.ts +++ b/tests/util/event-formatter.test.ts @@ -1,7 +1,7 @@ import { EventFormatter } from '../../src/util'; describe('EventFormatter', () => { - let eventFormatter; + let eventFormatter: EventFormatter; beforeEach(() => { eventFormatter = new EventFormatter('App.Events'); diff --git a/tsconfig.json b/tsconfig.json index 04662b81..65af0a48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,37 @@ { "compilerOptions": { "declaration": true, - "module": "es6", - "moduleResolution": "node", + "declarationDir": "./dist", + "emitDeclarationOnly": true, + "module": "ES2020", + "moduleResolution": "bundler", "outDir": "./dist", "sourceMap": false, - "target": "es6", - "isolatedModules": false, + "target": "ES2020", + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "importHelpers": true, + "strictPropertyInitialization": false, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "esModuleInterop": true, + "strict": true, "typeRoots": [ "node_modules/@types" ], "lib": [ - "es2017", - "dom" + "dom", + "es2020" ] }, "include": [ - "./typings/*.ts", - "./src/*.ts" + "./typings/**/*.ts", + "./src/**/*.ts" + ], + "exclude": [ + "./node_modules", + "./tests/**/*.ts" ] } diff --git a/typings/index.d.ts b/typings/index.d.ts index cb04f2c1..35d91a31 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,3 @@ -declare let Pusher: any; -declare let io: any; declare let Vue: any; declare let axios: any; declare let jQuery: any; diff --git a/typings/window.d.ts b/typings/window.d.ts new file mode 100644 index 00000000..2976ba82 --- /dev/null +++ b/typings/window.d.ts @@ -0,0 +1,16 @@ +import type { io } from 'socket.io-client'; +import type Pusher from 'pusher-js'; + +export {}; + +declare global { + interface Window { + Laravel?: { + csrfToken?: string; + }; + + io?: typeof io; + + Pusher?: typeof Pusher; + } +}