From 1aa97a8a7ad5f4ab484245eb4d79e60035d1e5f1 Mon Sep 17 00:00:00 2001 From: aki Date: Fri, 25 Apr 2025 00:41:40 +0800 Subject: [PATCH] refactor: Update import statements and configurations for ES module compatibility --- deploy-commands.ts | 7 +- package.json | 1 + scripts/fix-imports.cjs | 60 +++++++++++++++++ scripts/fix-imports.js | 60 +++++++++++++++++ src/commands/join.ts | 4 +- src/commands/leave.ts | 4 +- src/commands/play.ts | 4 +- src/events/interactionCreate.ts | 4 +- src/events/ready.ts | 6 +- src/events/voiceStateUpdate.ts | 4 +- src/index.ts | 80 +++++++++++------------ src/structures/ShoukakuEvents.ts | 107 +++++++++++++++---------------- tsconfig.deploy.json | 4 +- tsconfig.json | 7 +- 14 files changed, 237 insertions(+), 115 deletions(-) create mode 100755 scripts/fix-imports.cjs create mode 100755 scripts/fix-imports.js diff --git a/deploy-commands.ts b/deploy-commands.ts index cef66a4..c069bc8 100644 --- a/deploy-commands.ts +++ b/deploy-commands.ts @@ -1,7 +1,7 @@ import { REST, Routes, APIApplicationCommand } from "discord.js"; import fs from "node:fs"; import path from "node:path"; -import logger from "./src/utils/logger"; // Use default import now +import logger from "./src/utils/logger.js"; // Added .js extension for ES modules import dotenv from "dotenv"; // --- Setup --- @@ -33,8 +33,9 @@ const loadCommandsForDeployment = async () => { for (const file of commandFiles) { const filePath = path.join(commandsPath, file); try { - // Use dynamic import - const commandModule = await import(filePath); + // Use dynamic import with file:// protocol for ES modules + const fileUrl = new URL(`file://${filePath}`); + const commandModule = await import(fileUrl.href); // Assuming commands export default or have a 'default' property const command = commandModule.default || commandModule; diff --git a/package.json b/package.json index b148664..feaff7f 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "", "main": "dist/index.js", + "type": "module", "scripts": { "build": "tsc -p tsconfig.json", "build:deploy": "tsc -p tsconfig.deploy.json", diff --git a/scripts/fix-imports.cjs b/scripts/fix-imports.cjs new file mode 100755 index 0000000..7266e6c --- /dev/null +++ b/scripts/fix-imports.cjs @@ -0,0 +1,60 @@ +const fs = require('fs'); +const path = require('path'); + +// Get all TypeScript files in a directory recursively +function getTypeScriptFiles(dir) { + const files = []; + + function traverse(currentDir) { + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentDir, entry.name); + + if (entry.isDirectory()) { + traverse(fullPath); + } else if (entry.isFile() && entry.name.endsWith('.ts')) { + files.push(fullPath); + } + } + } + + traverse(dir); + return files; +} + +// Fix imports in a file +function fixImportsInFile(filePath) { + console.log(`Processing ${filePath}`); + let content = fs.readFileSync(filePath, 'utf8'); + + // Regular expression to match relative imports without file extensions + const importRegex = /(import\s+(?:[^'"]*\s+from\s+)?['"])(\.\.[^'"]*?)(['"])/g; + + // Add .js extension to relative imports + content = content.replace(importRegex, (match, start, importPath, end) => { + // Don't add extension if it already has one or ends with a directory + if (importPath.endsWith('.js') || importPath.endsWith('/')) { + return match; + } + return `${start}${importPath}.js${end}`; + }); + + fs.writeFileSync(filePath, content); +} + +// Main function +function main() { + const srcDir = path.join(__dirname, '..', 'src'); + const files = getTypeScriptFiles(srcDir); + + console.log(`Found ${files.length} TypeScript files`); + + for (const file of files) { + fixImportsInFile(file); + } + + console.log('Done'); +} + +main(); diff --git a/scripts/fix-imports.js b/scripts/fix-imports.js new file mode 100755 index 0000000..7266e6c --- /dev/null +++ b/scripts/fix-imports.js @@ -0,0 +1,60 @@ +const fs = require('fs'); +const path = require('path'); + +// Get all TypeScript files in a directory recursively +function getTypeScriptFiles(dir) { + const files = []; + + function traverse(currentDir) { + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentDir, entry.name); + + if (entry.isDirectory()) { + traverse(fullPath); + } else if (entry.isFile() && entry.name.endsWith('.ts')) { + files.push(fullPath); + } + } + } + + traverse(dir); + return files; +} + +// Fix imports in a file +function fixImportsInFile(filePath) { + console.log(`Processing ${filePath}`); + let content = fs.readFileSync(filePath, 'utf8'); + + // Regular expression to match relative imports without file extensions + const importRegex = /(import\s+(?:[^'"]*\s+from\s+)?['"])(\.\.[^'"]*?)(['"])/g; + + // Add .js extension to relative imports + content = content.replace(importRegex, (match, start, importPath, end) => { + // Don't add extension if it already has one or ends with a directory + if (importPath.endsWith('.js') || importPath.endsWith('/')) { + return match; + } + return `${start}${importPath}.js${end}`; + }); + + fs.writeFileSync(filePath, content); +} + +// Main function +function main() { + const srcDir = path.join(__dirname, '..', 'src'); + const files = getTypeScriptFiles(srcDir); + + console.log(`Found ${files.length} TypeScript files`); + + for (const file of files) { + fixImportsInFile(file); + } + + console.log('Done'); +} + +main(); diff --git a/src/commands/join.ts b/src/commands/join.ts index a5bd6bf..b7951f4 100644 --- a/src/commands/join.ts +++ b/src/commands/join.ts @@ -6,8 +6,8 @@ import { GuildMember, // Import GuildMember type VoiceBasedChannel, // Import VoiceBasedChannel type } from "discord.js"; -import logger from "../utils/logger"; // Use default import -import { BotClient } from "../index"; // Import the BotClient interface +import logger from "../utils/logger.js"; // Use default import +import { BotClient } from "../index.js"; // Import the BotClient interface import { Player } from "shoukaku"; // Import the Player type explicitly export default { diff --git a/src/commands/leave.ts b/src/commands/leave.ts index 6efa9e5..804cd88 100644 --- a/src/commands/leave.ts +++ b/src/commands/leave.ts @@ -3,8 +3,8 @@ import { ChatInputCommandInteraction, // Import the specific _interaction type GuildMember, // Import GuildMember type } from "discord.js"; -import logger from "../utils/logger"; // Use default import -import { BotClient } from "../index"; // Import the BotClient interface +import logger from "../utils/logger.js"; // Use default import +import { BotClient } from "../index.js"; // Import the BotClient interface // No need to import Player explicitly if we just check connection export default { diff --git a/src/commands/play.ts b/src/commands/play.ts index bc910b1..29b8ae0 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -8,8 +8,8 @@ import { GuildMember, VoiceBasedChannel, } from "discord.js"; -import logger from "../utils/logger"; -import { BotClient } from "../index"; +import logger from "../utils/logger.js"; +import { BotClient } from "../index.js"; // Import necessary Shoukaku types - LavalinkResponse might need a local definition if not exported import { Player, Node, Track, SearchResult, Connection } from "shoukaku"; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 05deeed..0acb793 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,6 +1,6 @@ import { Events, Interaction } from "discord.js"; -import { BotClient } from "../types/botClient"; -import logger from "../utils/logger"; +import { BotClient } from "../types/botClient.js"; +import logger from "../utils/logger.js"; export default { name: Events.InteractionCreate, diff --git a/src/events/ready.ts b/src/events/ready.ts index 0a47e47..0b5782a 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,7 +1,7 @@ import { Events, ActivityType, Client } from "discord.js"; // Import base Client type -import logger from "../utils/logger"; // Use default import -import { initializeShoukaku } from "../structures/ShoukakuEvents"; // Import the correct setup function -import { BotClient } from "../index"; // Import BotClient type +import logger from "../utils/logger.js"; // Use default import +import { initializeShoukaku } from "../structures/ShoukakuEvents.js"; // Import the correct setup function +import { BotClient } from "../index.js"; // Import BotClient type export default { // Use export default diff --git a/src/events/voiceStateUpdate.ts b/src/events/voiceStateUpdate.ts index b7dc0e6..f2c35fb 100644 --- a/src/events/voiceStateUpdate.ts +++ b/src/events/voiceStateUpdate.ts @@ -1,6 +1,6 @@ import { Events, VoiceState, ChannelType } from "discord.js"; // Added ChannelType -import logger from "../utils/logger"; -import { BotClient } from "../index"; // Assuming BotClient is exported from index +import logger from "../utils/logger.js"; +import { BotClient } from "../index.js"; // Assuming BotClient is exported from index export default { // Use export default for ES modules diff --git a/src/index.ts b/src/index.ts index 437e19f..7427f86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,38 +4,40 @@ import { GatewayIntentBits, Collection, Events, - BaseInteraction, // Use a base type for now, refine later if needed - SlashCommandBuilder, // Assuming commands use this + BaseInteraction, + SlashCommandBuilder, } from "discord.js"; import { Shoukaku, Connectors, NodeOption, ShoukakuOptions } from "shoukaku"; -import logger from "./utils/logger"; // Assuming logger uses export default or similar +import logger from "./utils/logger.js"; // Add .js extension import fs from "fs"; import path from "path"; -// import { fileURLToPath } from 'url'; // Needed for __dirname in ES Modules if module is not CommonJS +import { fileURLToPath } from 'url'; // Needed for __dirname in ES Modules + +// Get __dirname equivalent in ES Modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); // Define Command structure interface Command { - data: Omit; // Or appropriate type for your command data - execute: (_interaction: BaseInteraction, _client: BotClient) => Promise; // Adjust _interaction type if needed + data: Omit; + execute: (_interaction: BaseInteraction, _client: BotClient) => Promise; } // Define Event structure interface BotEvent { - name: string; // Should match discord.js event names or custom names + name: string; once?: boolean; - execute: (..._args: any[]) => void; // Use specific types later if possible + execute: (..._args: any[]) => void; } // Extend the discord.js Client class to include custom properties export interface BotClient extends Client { - // Add export keyword commands: Collection; shoukaku: Shoukaku; } // --- Setup --- dotenv.config(); -// __dirname is available in CommonJS modules, which is set in tsconfig.json // Validate essential environment variables if (!process.env.DISCORD_TOKEN) { @@ -46,8 +48,6 @@ if (!process.env.LAVALINK_HOST || !process.env.LAVALINK_PORT || !process.env.LAV logger.warn( "Lavalink connection details (HOST, PORT, PASSWORD) are missing or incomplete in .env. Music functionality will be limited.", ); - // Decide if the bot should exit or continue without music - // process.exit(1); // Uncomment to exit if Lavalink is mandatory } // Create a new Discord _client instance with necessary intents @@ -55,28 +55,27 @@ const _client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, - GatewayIntentBits.GuildMessages, // Add if needed for prefix commands or message content - GatewayIntentBits.MessageContent, // Add if needed for message content + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, ], -}) as BotClient; // Assert the type here +}) as BotClient; // Define Shoukaku nodes const Nodes: NodeOption[] = [ { - name: process.env.LAVALINK_NAME || "lavalink-node-1", // Use an env var or default name - url: `${process.env.LAVALINK_HOST || "localhost"}:${process.env.LAVALINK_PORT || 2333}`, // Use || 2333 for default port number - auth: process.env.LAVALINK_PASSWORD || "youshallnotpass", // Password from your Lavalink server config - secure: process.env.LAVALINK_SECURE === "true", // Set to true if using HTTPS/WSS + name: process.env.LAVALINK_NAME || "lavalink-node-1", + url: `${process.env.LAVALINK_HOST || "localhost"}:${process.env.LAVALINK_PORT || 2333}`, + auth: process.env.LAVALINK_PASSWORD || "youshallnotpass", + secure: process.env.LAVALINK_SECURE === "true", }, ]; // Shoukaku _options const shoukakuOptions: ShoukakuOptions = { - moveOnDisconnect: false, // Whether to move players to another node when a node disconnects - resume: true, // Whether to resume players session after Lavalink restarts - reconnectTries: 3, // Number of attempts to reconnect to Lavalink - reconnectInterval: 5000, // Interval between reconnect attempts in milliseconds - // Add other _options as needed + moveOnDisconnect: false, + resume: true, + reconnectTries: 3, + reconnectInterval: 5000, }; // Initialize Shoukaku @@ -84,7 +83,7 @@ _client.shoukaku = new Shoukaku(new Connectors.DiscordJS(_client), Nodes, shouka // Show the actual Lavalink connection details (without exposing the actual password) logger.info( - `Lavalink connection configured to: ${process.env.LAVALINK_HOST}:${process.env.LAVALINK_PORT} (Password: ${process.env.LAVALINK_PASSWORD ? "[SET]" : "[NOT SET]"})`, + `Lavalink connection configured to: ${process.env.LAVALINK_HOST || "localhost"}:${process.env.LAVALINK_PORT || 2333} (Password: ${process.env.LAVALINK_PASSWORD ? "[SET]" : "[NOT SET]"})`, ); // Collections for commands @@ -92,16 +91,17 @@ _client.commands = new Collection(); // --- Command Loading --- const commandsPath = path.join(__dirname, "commands"); -// Read .ts files now -const commandFiles = fs.readdirSync(commandsPath).filter((file: string) => file.endsWith(".ts")); +// Read .js files instead of .ts after compilation +const commandFiles = fs.readdirSync(commandsPath).filter((file: string) => file.endsWith(".js")); const loadCommands = async () => { for (const file of commandFiles) { const filePath = path.join(commandsPath, file); try { - // Use dynamic import for ES Modules/CommonJS interop - const commandModule = await import(filePath); - const command: Command = commandModule.default || commandModule; // Handle default exports + // Use dynamic import with file:// protocol for ES Modules + const fileUrl = new URL(`file://${filePath}`).href; + const commandModule = await import(fileUrl); + const command: Command = commandModule.default || commandModule; if (command && typeof command === "object" && "data" in command && "execute" in command) { _client.commands.set(command.data.name, command); @@ -112,7 +112,6 @@ const loadCommands = async () => { ); } } catch (error: unknown) { - // Type the error as unknown const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Error loading command at ${filePath}: ${errorMessage}`, error); } @@ -121,22 +120,24 @@ const loadCommands = async () => { // --- Event Handling --- const eventsPath = path.join(__dirname, "events"); -// Read .ts files now -const eventFiles = fs.readdirSync(eventsPath).filter((file: string) => file.endsWith(".ts")); +// Read .js files instead of .ts after compilation +const eventFiles = fs.readdirSync(eventsPath).filter((file: string) => file.endsWith(".js")); const loadEvents = async () => { for (const file of eventFiles) { const filePath = path.join(eventsPath, file); try { - const eventModule = await import(filePath); - const event: BotEvent = eventModule.default || eventModule; // Handle default exports + // Use dynamic import with file:// protocol for ES Modules + const fileUrl = new URL(`file://${filePath}`).href; + const eventModule = await import(fileUrl); + const event: BotEvent = eventModule.default || eventModule; if (event && typeof event === "object" && "name" in event && "execute" in event) { if (event.once) { - _client.once(event.name, (..._args: any[]) => event.execute(..._args, _client)); // Pass _client + _client.once(event.name, (..._args: any[]) => event.execute(..._args, _client)); logger.info(`Loaded event ${event.name} (once)`); } else { - _client.on(event.name, (..._args: any[]) => event.execute(..._args, _client)); // Pass _client + _client.on(event.name, (..._args: any[]) => event.execute(..._args, _client)); logger.info(`Loaded event ${event.name}`); } } else { @@ -161,7 +162,6 @@ _client.shoukaku.on("error", (name: string, error: Error) => _client.shoukaku.on("close", (name: string, code: number, reason: string | undefined) => logger.warn(`Lavalink Node: ${name} closed with code ${code}. Reason: ${reason || "No reason"}`), ); -// Corrected disconnect event signature based on common usage and error TS148 _client.shoukaku.on("disconnect", (name: string, count: number) => { logger.warn( `Lavalink Node: ${name} disconnected. ${count} players were disconnected from this node.`, @@ -180,7 +180,7 @@ async function main() { } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Failed to log in: ${errorMessage}`); - process.exit(1); // Exit if login fails + process.exit(1); } } @@ -197,6 +197,4 @@ process.on("unhandledRejection", (reason: unknown, promise: Promise) => { }); process.on("uncaughtException", (error: Error, origin: NodeJS.UncaughtExceptionOrigin) => { logger.error(`Uncaught exception: ${error.message}`, { error, origin }); - // Optional: exit process on critical uncaught exceptions - // process.exit(1); }); diff --git a/src/structures/ShoukakuEvents.ts b/src/structures/ShoukakuEvents.ts index ee301fd..7d9abbd 100644 --- a/src/structures/ShoukakuEvents.ts +++ b/src/structures/ShoukakuEvents.ts @@ -1,67 +1,66 @@ -import { Shoukaku, NodeOption, ShoukakuOptions, Player, Connectors } from "shoukaku"; // Removed player event types, Added Connectors -// import { Connectors } from 'shoukaku-discord.js'; // Use the discord.js connector - Removed this line -import logger from "../utils/logger"; -import { BotClient } from "../index"; +import { Shoukaku, NodeOption, ShoukakuOptions, Player, Connectors } from 'shoukaku'; +import logger from '../utils/logger.js'; +import { BotClient } from '../index.js'; // Removed imports from play.ts for now as player listeners are removed -// Define Node _options (replace with your actual Lavalink details from .env) +// Define Node options (replace with your actual Lavalink details from .env) const nodes: NodeOption[] = [ - { - name: process.env.LAVALINK_NAME || "Lavalink-Node-1", - url: process.env.LAVALINK_URL || "lavalink:2333", // Use service name for Docker Compose if applicable - auth: process.env.LAVALINK_AUTH || "youshallnotpass", - secure: process.env.LAVALINK_SECURE === "true" || false, - }, + { + name: process.env.LAVALINK_NAME || 'Lavalink-Node-1', + url: process.env.LAVALINK_URL || 'lavalink:2333', // Use service name for Docker Compose if applicable + auth: process.env.LAVALINK_AUTH || 'youshallnotpass', + secure: process.env.LAVALINK_SECURE === 'true' || false, + }, ]; -// Define Shoukaku _options +// Define Shoukaku options const shoukakuOptions: ShoukakuOptions = { - moveOnDisconnect: false, - resume: false, // Resume doesn't work reliably across restarts/disconnects without session persistence - reconnectTries: 3, - reconnectInterval: 5, // In seconds - restTimeout: 15000, // In milliseconds - voiceConnectionTimeout: 15, // In seconds + moveOnDisconnect: false, + resume: false, // Resume doesn't work reliably across restarts/disconnects without session persistence + reconnectTries: 3, + reconnectInterval: 5, // In seconds + restTimeout: 15000, // In milliseconds + voiceConnectionTimeout: 15, // In seconds }; // Function to initialize Shoukaku and attach listeners -export function initializeShoukaku(_client: BotClient): Shoukaku { - if (!_client) { - throw new Error("initializeShoukaku requires a _client instance."); - } - - const shoukaku = new Shoukaku(new Connectors.DiscordJS(_client), nodes, shoukakuOptions); - - // --- Shoukaku Node Event Listeners --- - shoukaku.on("ready", (name, resumed) => - logger.info(`Lavalink Node '${name}' ready. Resumed: ${resumed}`), - ); - - shoukaku.on("error", (name, error) => - logger.error(`Lavalink Node '${name}' error: ${error.message}`, error), - ); - - shoukaku.on("close", (name, code, reason) => - logger.warn(`Lavalink Node '${name}' closed. Code: ${code}. Reason: ${reason || "No reason"}`), - ); - - // Fix: Correct disconnect listener signature - shoukaku.on("disconnect", (name, count) => { - // count = count of players disconnected from the node - logger.warn(`Lavalink Node '${name}' disconnected. ${count} players disconnected.`); - // If players were not moved, you might want to attempt to reconnect them or clean them up. - }); - - shoukaku.on("debug", (name, info) => { - // Only log debug messages if not in production or if explicitly enabled - if (process.env.NODE_ENV !== "production" || process.env.LAVALINK_DEBUG === "true") { - logger.debug(`Lavalink Node '${name}' debug: ${info}`); +export function initializeShoukaku(client: BotClient): Shoukaku { + if (!client) { + throw new Error("initializeShoukaku requires a client instance."); } - }); - // --- Shoukaku Player Event Listeners --- - // REMOVED - These need to be attached differently in Shoukaku v4 (e.g., when player is created) + const shoukaku = new Shoukaku(new Connectors.DiscordJS(client), nodes, shoukakuOptions); - logger.info("Shoukaku instance created and node event listeners attached."); - return shoukaku; + // --- Shoukaku Node Event Listeners --- + shoukaku.on('ready', (name, resumed) => + logger.info(`Lavalink Node '${name}' ready. Resumed: ${resumed}`) + ); + + shoukaku.on('error', (name, error) => + logger.error(`Lavalink Node '${name}' error: ${error.message}`, error) + ); + + shoukaku.on('close', (name, code, reason) => + logger.warn(`Lavalink Node '${name}' closed. Code: ${code}. Reason: ${reason || 'No reason'}`) + ); + + // Fix: Correct disconnect listener signature + shoukaku.on('disconnect', (name, count) => { + // count = count of players disconnected from the node + logger.warn(`Lavalink Node '${name}' disconnected. ${count} players disconnected.`); + // If players were not moved, you might want to attempt to reconnect them or clean them up. + }); + + shoukaku.on('debug', (name, info) => { + // Only log debug messages if not in production or if explicitly enabled + if (process.env.NODE_ENV !== 'production' || process.env.LAVALINK_DEBUG === 'true') { + logger.debug(`Lavalink Node '${name}' debug: ${info}`); + } + }); + + // --- Shoukaku Player Event Listeners --- + // REMOVED - These need to be attached differently in Shoukaku v4 (e.g., when player is created) + + logger.info("Shoukaku instance created and node event listeners attached."); + return shoukaku; } diff --git a/tsconfig.deploy.json b/tsconfig.deploy.json index 9a8a925..16f939d 100644 --- a/tsconfig.deploy.json +++ b/tsconfig.deploy.json @@ -1,7 +1,9 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "rootDir": "." + "rootDir": ".", + "module": "NodeNext", + "moduleResolution": "NodeNext" }, "include": ["deploy-commands.ts"] } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 4ae8103..787cbab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2020", - "module": "ESNext", - "moduleResolution": "Node", + "module": "NodeNext", + "moduleResolution": "NodeNext", "lib": ["ES2020"], "outDir": "dist", "rootDir": "src", @@ -13,7 +13,8 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, - "sourceMap": true + "sourceMap": true, + "allowSyntheticDefaultImports": true }, "include": [ "src/**/*"