From 7219bf8f42aa33f79d08acac77062803f1f9c740 Mon Sep 17 00:00:00 2001 From: vrdons Date: Sun, 8 Mar 2026 12:44:52 +0300 Subject: [PATCH 1/3] refactor!: remove global context, update command/event attachment BREAKING CHANGE: The global context module has been removed. Commands and events must now access the client and logger via instance properties after being attached. The `prefix` option is now a function. Update your usage accordingly. --- .npmignore | 3 +- examples/basic_client/commands/ping.cjs | 23 ++ .../events/{ready.js => ready.cjs} | 4 +- examples/basic_client/index.js | 31 +- .../basic_client/locales/en-US/error.json | 6 +- package-lock.json | 300 +++++++++++++++++- package.json | 18 +- scripts/build.js | 3 + scripts/createCjsTypes.js | 10 + src/context.ts | 12 - src/events/interaction.ts | 7 +- src/events/message.ts | 10 +- src/events/ready.ts | 10 +- src/index.ts | 2 +- src/structures/builder/Command.ts | 92 ++++-- src/structures/builder/Context.ts | 40 ++- src/structures/builder/Event.ts | 31 +- src/structures/core/Client.ts | 105 ++++-- src/utils/Files.ts | 2 +- src/utils/logger/ILogger.ts | 2 +- src/utils/logger/Logger.ts | 26 +- src/utils/util.ts | 74 ++++- tsconfig.json | 1 - types/client.d.ts | 49 ++- types/logger.d.ts | 17 +- 25 files changed, 713 insertions(+), 165 deletions(-) create mode 100644 examples/basic_client/commands/ping.cjs rename examples/basic_client/events/{ready.js => ready.cjs} (59%) create mode 100644 scripts/createCjsTypes.js delete mode 100644 src/context.ts diff --git a/.npmignore b/.npmignore index d4f867f..57f8e0b 100644 --- a/.npmignore +++ b/.npmignore @@ -13,5 +13,6 @@ src scripts tsconfig.json .oxfmtrc.json +.prettierrc package-lock.json -.swcrc \ No newline at end of file +.swcrc diff --git a/examples/basic_client/commands/ping.cjs b/examples/basic_client/commands/ping.cjs new file mode 100644 index 0000000..53d5db5 --- /dev/null +++ b/examples/basic_client/commands/ping.cjs @@ -0,0 +1,23 @@ +const { + Command, + ApplicationCommandBuilder, +} = require("../../../dist/index.cjs"); + +module.exports = new Command({ + data: new ApplicationCommandBuilder() + .setName("ping") + .setDescription("Replies with pong") + .setAliases("p") + .setPrefixSupport(true) + .setSlashSupport(true), + onMessage: (ctx) => + ctx.message.reply( + ctx.t("test:hello", { + user: ctx.message.author.username, + }) + ), + onInteraction: (ctx) => + ctx.interaction.reply( + ctx.t("test:hello", { user: ctx.interaction.user.username }) + ), +}); diff --git a/examples/basic_client/events/ready.js b/examples/basic_client/events/ready.cjs similarity index 59% rename from examples/basic_client/events/ready.js rename to examples/basic_client/events/ready.cjs index da525f6..73350f5 100644 --- a/examples/basic_client/events/ready.js +++ b/examples/basic_client/events/ready.cjs @@ -1,6 +1,6 @@ -import { EventBuilder } from "../../../dist/index.js"; +const { EventBuilder } = require("../../../dist/index.cjs"); -new EventBuilder("clientReady", false, (context) => { +module.exports = new EventBuilder("clientReady", false, (context) => { context.logger.log("Client connected!"); context.logger.warn(`Current user: ${context.client.user.username}`); context.logger.warn(`Current prefix: ${context.client.prefix ?? "none"}`); diff --git a/examples/basic_client/index.js b/examples/basic_client/index.js index 329b073..3641c87 100644 --- a/examples/basic_client/index.js +++ b/examples/basic_client/index.js @@ -9,7 +9,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const myinstance = i18next.createInstance({ - supportedLngs: ["en-US", "tr"], + supportedLngs: ["en-US", "tr"], // Required to be `${Locale}` (LocaleString) fallbackLng: "en-US", defaultNS: "translation", ns: ["translation", "test", "error"], @@ -28,7 +28,11 @@ const client = new arox.Client({ IntentsBitField.Flags.GuildMessages, IntentsBitField.Flags.MessageContent, ], - prefix: { enabled: true, prefix: "a!" }, + prefix: () => "a!", + includePaths: [ + path.join(__dirname, "events"), + path.join(__dirname, "commands"), + ], logger: { level: arox.LogLevel.Debug, }, @@ -36,29 +40,6 @@ const client = new arox.Client({ i18n: myinstance, }); -arox.setClient(client); -const command = new arox.CommandBuilder( - new arox.ApplicationCommandBuilder() - .setName("arox") - .setDescription("Arox Test Command") - .addAliases("a") -); -arox.clearClient(); - -command - .onMessage(function (ctx) { - const { message, t, author } = ctx; - void message.reply( - t("test:hello", { user: author?.username ?? "Unknown" }) - ); - }) - .onInteraction(function (ctx) { - const { interaction, t, author } = ctx; - void interaction.reply( - t("test:hello", { user: author?.username ?? "Unknown" }) - ); - }); - async function init() { const token = process.env.DISCORD_TOKEN ?? process.env.BOT_TOKEN; await client.login(token); diff --git a/examples/basic_client/locales/en-US/error.json b/examples/basic_client/locales/en-US/error.json index 55bf77b..686816b 100644 --- a/examples/basic_client/locales/en-US/error.json +++ b/examples/basic_client/locales/en-US/error.json @@ -1,4 +1,6 @@ { - "command.notfound": "Command not found or disabled1", - "command.disabled": "Command not found or disabled2" + "command": { + "notfound": "Command not found or disabled1", + "disabled": "Command not found or disabled2" + } } diff --git a/package-lock.json b/package-lock.json index be74376..856e30f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "framework", - "version": "0.1.2-beta.1", + "version": "0.1.2-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "framework", - "version": "0.1.2-beta.1", + "version": "0.1.2-beta.2", "license": "Apache-2.0", "dependencies": { "@sapphire/timestamp": "^1.0.5", @@ -22,6 +22,7 @@ "oxfmt": "^0.36.0", "oxlint": "^1.39.0", "oxlint-tsgolint": "^0.16.0", + "tsc-alias": "^1.8.16", "typescript": "~5", "unplugin-oxc": "^0.6.0" }, @@ -3128,6 +3129,43 @@ "node": ">= 14" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/bin-links": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.0.tgz", @@ -3145,6 +3183,19 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -3180,6 +3231,31 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -3206,6 +3282,16 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/common-ancestor-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", @@ -3247,6 +3333,19 @@ } } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/discord-api-types": { "version": "0.38.37", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.37.tgz", @@ -3426,6 +3525,34 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -3495,6 +3622,27 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3622,6 +3770,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/ignore-walk": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", @@ -3704,6 +3862,19 @@ "node": ">= 12" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4022,6 +4193,20 @@ "dev": true, "license": "MIT" }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -4099,6 +4284,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-bundled": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", @@ -4483,6 +4678,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -4496,6 +4701,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/postcss-selector-parser": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", @@ -4574,6 +4792,16 @@ "node": ">= 4" } }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4604,6 +4832,42 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -4686,6 +4950,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -4838,6 +5112,28 @@ "license": "MIT", "peer": true }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index c0dd459..c5bdf03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "framework", - "version": "0.1.2-beta.1", + "version": "0.1.2-beta.2", "description": "", "keywords": [ "arox", @@ -24,11 +24,20 @@ "main": "dist/index.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", + "imports": { + "#types/*": "./types/*.d.ts" + }, "exports": { ".": { "types": "./dist/index.d.ts", - "require": "./dist/index.cjs", - "import": "./dist/index.js", + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, "default": "./dist/index.js" } }, @@ -38,7 +47,7 @@ "check": "oxfmt --check && oxlint --type-aware --type-check", "format": "oxfmt", "build:js": "node ./scripts/build.js", - "build:dts": "tsc --emitDeclarationOnly", + "build:dts": "tsc --emitDeclarationOnly && tsc-alias -p tsconfig.json && node ./scripts/createCjsTypes.js", "build": "npm run build:js && npm run build:dts" }, "dependencies": { @@ -55,6 +64,7 @@ "oxfmt": "^0.36.0", "oxlint": "^1.39.0", "oxlint-tsgolint": "^0.16.0", + "tsc-alias": "^1.8.16", "typescript": "~5", "unplugin-oxc": "^0.6.0" }, diff --git a/scripts/build.js b/scripts/build.js index a252cb9..43e8405 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -13,9 +13,12 @@ const peerDependencies = Object.keys(packageJson.peerDependencies ?? {}); const JS_EXTENSIONS = [".js", ".mjs", ".cjs"]; async function build() { + await fs.rm(path.resolve("dist"), { recursive: true, force: true }); + const baseBuildOptions = { entryPoints: ["src/index.ts"], bundle: true, + minify: true, platform: "node", target: "node25", sourcemap: false, diff --git a/scripts/createCjsTypes.js b/scripts/createCjsTypes.js new file mode 100644 index 0000000..4731ae4 --- /dev/null +++ b/scripts/createCjsTypes.js @@ -0,0 +1,10 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +const filePath = path.resolve("dist/index.d.cts"); +const content = `import type * as Arox from "./index.js"; +export = Arox; +`; + +await fs.writeFile(filePath, content, "utf8"); +console.log("Generated dist/index.d.cts"); diff --git a/src/context.ts b/src/context.ts deleted file mode 100644 index 49f6d5e..0000000 --- a/src/context.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Client } from "./structures/index.js"; - -// Birden fazla client olursa hata çıkartabilir ama aklıma gelen tek şey bu -export let currentClient: Client | null = null; - -export function setClient(client: Client) { - currentClient = client; -} - -export function clearClient() { - currentClient = null; -} diff --git a/src/events/interaction.ts b/src/events/interaction.ts index 04a0d79..ac983d7 100644 --- a/src/events/interaction.ts +++ b/src/events/interaction.ts @@ -5,7 +5,7 @@ import { } from "@constants/messages.js"; import { EventBuilder, Context } from "@structures/index.js"; -new EventBuilder(Events.InteractionCreate, false).onExecute( +export default new EventBuilder(Events.InteractionCreate, false).onExecute( async function (context, interaction) { if (!interaction.isChatInputCommand()) return; @@ -14,7 +14,7 @@ new EventBuilder(Events.InteractionCreate, false).onExecute( if (!command) { await interaction.reply({ - content: ctx.t("error.command.notfound", { + content: ctx.t("error:command.notfound", { defaultValue: COMMAND_DISABLED_MESSAGE, }), flags: MessageFlags.Ephemeral, @@ -23,7 +23,7 @@ new EventBuilder(Events.InteractionCreate, false).onExecute( } if (!command.supportsSlash) { await interaction.reply({ - content: ctx.t("error.command.disabled", { + content: ctx.t("error:command.disabled", { defaultValue: COMMAND_DISABLED_MESSAGE, }), flags: MessageFlags.Ephemeral, @@ -31,7 +31,6 @@ new EventBuilder(Events.InteractionCreate, false).onExecute( return; } try { - ctx.locale = interaction.locale; context.logger.debug( `${ctx.author?.tag ?? "Unknown"} used ${command.data.name}(interaction)` ); diff --git a/src/events/message.ts b/src/events/message.ts index 19bad98..9b55dd5 100644 --- a/src/events/message.ts +++ b/src/events/message.ts @@ -3,12 +3,14 @@ import { COMMAND_DISABLED_MESSAGE } from "@constants/messages.js"; import { EventBuilder, Context } from "@structures/index.js"; import { deleteMessageAfterSent } from "@utils/index.js"; -new EventBuilder( +export default new EventBuilder( Events.MessageCreate, false, async function (context, message) { if (message.author.bot) return; - const prefix = context.client.prefix; + const prefix = context.client.prefix( + new Context(context.client, { message }) + ); if ( typeof prefix !== "string" || prefix.length === 0 || @@ -30,7 +32,7 @@ new EventBuilder( if (!command) { await message .reply({ - content: ctx.t("error.command.notfound", { + content: ctx.t("error:command.notfound", { defaultValue: COMMAND_DISABLED_MESSAGE, }), allowedMentions: { repliedUser: false }, @@ -42,7 +44,7 @@ new EventBuilder( if (!command.supportsPrefix) { await message .reply({ - content: ctx.t("error.command.disabled", { + content: ctx.t("error:command.disabled", { defaultValue: COMMAND_DISABLED_MESSAGE, }), allowedMentions: { repliedUser: false }, diff --git a/src/events/ready.ts b/src/events/ready.ts index e34a284..b6b2cd2 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,8 +1,10 @@ import { Events } from "discord.js"; import { EventBuilder } from "@structures/index.js"; -new EventBuilder(Events.ClientReady).onExecute(async function (context) { - if (context.client.options.autoRegisterCommands) { - await context.client.registerCommands(); +export default new EventBuilder(Events.ClientReady).onExecute( + async function (context) { + if (context.client.options.autoRegisterCommands) { + await context.client.registerCommands(); + } } -}); +); diff --git a/src/index.ts b/src/index.ts index 12625ca..d4057d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from "./structures/index.js"; +export * from "./utils/index.js"; export * from "./utils/logger/Logger.js"; -export * from "./context.js"; export const version = "[VI]{{version}}[/VI]"; diff --git a/src/structures/builder/Command.ts b/src/structures/builder/Command.ts index ef55ed0..6745574 100644 --- a/src/structures/builder/Command.ts +++ b/src/structures/builder/Command.ts @@ -1,23 +1,35 @@ -import { ChatInputCommandInteraction, Message } from "discord.js"; -import { currentClient } from "@context"; -import { Context, Client } from "@structures/index.js"; +import { Client } from "@structures/index.js"; import { Logger } from "@utils/index.js"; import type { MaybePromise } from "#types/extra.js"; import { ApplicationCommandBuilder } from "@structures/builder/Builder.js"; +import type { + InteractionContextJSON, + MessageContextJSON, +} from "@structures/builder/Context.js"; -type MessageContext = NonNullable["toJSON"]>>; -type InteractionContext = NonNullable< - ReturnType["toJSON"]> ->; +type MessageContext = MessageContextJSON; +type InteractionContext = InteractionContextJSON; +type CommandContext = MessageContext | InteractionContext; export class CommandBuilder { - public readonly client: Client; - public readonly logger: Logger; + #client: Client | null = null; + #logger: Logger | null = null; #supportsSlash: boolean; #supportsPrefix: boolean; + #attached = false; _onMessage?: (ctx: MessageContext) => MaybePromise; _onInteraction?: (ctx: InteractionContext) => MaybePromise; + get client(): Client { + if (!this.#client) throw new Error("Command is not attached to a client"); + return this.#client; + } + + get logger(): Logger { + if (!this.#logger) throw new Error("Command is not attached to a client"); + return this.#logger; + } + get supportsSlash() { return this.#supportsSlash && this._onInteraction; } @@ -26,12 +38,8 @@ export class CommandBuilder { } constructor(public readonly data: ApplicationCommandBuilder) { - const client = currentClient; - if (!client) throw new Error("Client is not defined"); - this.client = client; - this.logger = client.logger; const commandJSON = data.toJSON(); - const { name, aliases } = commandJSON; + const { name } = commandJSON; this.#supportsPrefix = commandJSON.prefix_support ?? false; this.#supportsSlash = commandJSON.slash_support ?? false; @@ -40,12 +48,21 @@ export class CommandBuilder { `Command ${name} must support either slash or prefix commands.` ); } + } + + attach(client: Client) { + if (this.#attached) return this; + const commandJSON = this.data.toJSON(); + const { name, aliases } = commandJSON; + + this.#client = client; + this.#logger = client.logger; - if (this.client.commands.has(name)) { + if (client.commands.has(name)) { throw new Error(`Command name "${name}" is already registered.`); } - const existingAliasOwner = this.client.aliases.findKey((aliases) => + const existingAliasOwner = client.aliases.findKey((aliases) => aliases.has(name) ); if (existingAliasOwner) { @@ -55,12 +72,12 @@ export class CommandBuilder { } for (const alias of aliases) { - if (this.client.commands.has(alias)) { + if (client.commands.has(alias)) { throw new Error( `Alias "${alias}" is already registered as a command name.` ); } - const conflictingCommand = this.client.aliases.findKey((aliases) => + const conflictingCommand = client.aliases.findKey((aliases) => aliases.has(alias) ); if (conflictingCommand) { @@ -70,11 +87,13 @@ export class CommandBuilder { } } - this.client.commands.set(name, this); + client.commands.set(name, this); if (aliases.length > 0) { - this.client.aliases.set(name, new Set(aliases)); + client.aliases.set(name, new Set(aliases)); } this.logger.debug(`Loaded Command ${name}`); + this.#attached = true; + return this; } onMessage(func: (ctx: MessageContext) => MaybePromise) { @@ -87,3 +106,36 @@ export class CommandBuilder { return this; } } + +export interface CommandOptions { + data: ApplicationCommandBuilder; + execute?: (ctx: CommandContext) => MaybePromise; + onMessage?: (ctx: MessageContext) => MaybePromise; + onInteraction?: (ctx: InteractionContext) => MaybePromise; +} + +export class Command extends CommandBuilder { + constructor(options: CommandOptions) { + super(options.data); + const commandJSON = options.data.toJSON(); + const execute = options.execute; + if ( + (commandJSON.prefix_support ?? false) && + (options.onMessage || execute) + ) { + this.onMessage((ctx) => { + if (options.onMessage) return options.onMessage(ctx); + return execute?.(ctx); + }); + } + if ( + (commandJSON.slash_support ?? false) && + (options.onInteraction || execute) + ) { + this.onInteraction((ctx) => { + if (options.onInteraction) return options.onInteraction(ctx); + return execute?.(ctx); + }); + } + } +} diff --git a/src/structures/builder/Context.ts b/src/structures/builder/Context.ts index 6ec3db5..25c1248 100644 --- a/src/structures/builder/Context.ts +++ b/src/structures/builder/Context.ts @@ -1,4 +1,4 @@ -import { Message, User, ChatInputCommandInteraction, Locale } from "discord.js"; +import { ChatInputCommandInteraction, Locale, Message, User } from "discord.js"; import type { TOptions } from "i18next"; import type { Client } from "@structures/index.js"; @@ -6,6 +6,25 @@ type ContextPayload = T extends ChatInputCommandInteraction ? { interaction: T; args?: string[] } : { message: T; args?: string[] }; +type TranslateFn = ( + key: string, + options?: TOptions & { defaultValue?: string } +) => string; + +export interface InteractionContextJSON { + kind: "interaction"; + interaction: ChatInputCommandInteraction; + author: User | null; + t: TranslateFn; +} + +export interface MessageContextJSON { + kind: "message"; + message: Message; + args: string[]; + author: User | null; + t: TranslateFn; +} export class Context { readonly args: string[]; @@ -23,6 +42,10 @@ export class Context { } else { this.data = payload.message as T; } + + this.locale = this.client.options.getDefaultLang?.( + this as Context + ) as `${Locale}` | undefined; } isInteraction(): this is Context { @@ -50,10 +73,13 @@ export class Context { const locale = this.locale ?? + this.client.options.getDefaultLang?.( + this as Context + ) ?? (Array.isArray(this.client.i18n.options.fallbackLng) ? this.client.i18n.options.fallbackLng[0] : this.client.i18n.options.fallbackLng) ?? - "en"; + Locale.EnglishUS; const t = this.client.i18n.getFixedT(locale); @@ -66,15 +92,17 @@ export class Context { return result; } - toJSON() { + toJSON(this: Context): InteractionContextJSON; + toJSON(this: Context): MessageContextJSON; + toJSON(): InteractionContextJSON | MessageContextJSON { const { data, args, author } = this; if (this.isInteraction()) { return { kind: "interaction" as const, - interaction: data, + interaction: data as ChatInputCommandInteraction, author, - t: this.t.bind(this), + t: (key, options) => this.t(key, options), }; } @@ -83,7 +111,7 @@ export class Context { message: data as Message, args, author, - t: (this as Context).t.bind(this), + t: (key, options) => this.t(key, options), }; } } diff --git a/src/structures/builder/Event.ts b/src/structures/builder/Event.ts index 7547ba5..faf97ab 100644 --- a/src/structures/builder/Event.ts +++ b/src/structures/builder/Event.ts @@ -1,6 +1,5 @@ import type { ClientEvents } from "discord.js"; import type { MaybePromise } from "#types/extra.js"; -import { currentClient } from "@context"; import { Client } from "@structures/index.js"; import { Logger } from "@utils/index.js"; @@ -11,12 +10,23 @@ type EventHandler = ( ) => MaybePromise; export class EventBuilder { - readonly client: Client; - readonly logger: Logger; + #client: Client | null = null; + #logger: Logger | null = null; #handler?: EventHandler; #bound = false; + get client(): Client { + if (!this.#client) throw new Error("Event is not attached to a client"); + return this.#client; + } + + get logger(): Logger { + if (!this.#logger) throw new Error("Event is not attached to a client"); + return this.#logger; + } + #listener = async (...args: EventArgs) => { + if (!this.#client) return; if (!this.#handler) return; try { await this.#handler(this, ...args); @@ -33,18 +43,13 @@ export class EventBuilder { public readonly once: boolean = false, _handler?: EventHandler ) { - if (!currentClient) throw new Error("Client is not defined"); - this.client = currentClient; - this.logger = currentClient.logger; - if (_handler) { this.#handler = _handler; - this.#register(); } } #register(): void { - if (this.#bound || !this.#handler) return; + if (this.#bound || !this.#handler || !this.#client || !this.#logger) return; if (this.once) { this.client.once(this.name as string, this.#listener); @@ -56,6 +61,14 @@ export class EventBuilder { this.logger.debug(`Loaded Event ${String(this.name)}`); } + public attach(client: Client) { + if (this.#client) return this; + this.#client = client; + this.#logger = client.logger; + this.#register(); + return this; + } + public onExecute(func: EventHandler) { this.#handler = func; this.#register(); diff --git a/src/structures/core/Client.ts b/src/structures/core/Client.ts index c702ab2..cf93baf 100644 --- a/src/structures/core/Client.ts +++ b/src/structures/core/Client.ts @@ -9,20 +9,26 @@ import { existsSync } from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import type { i18n } from "i18next"; -import type { FrameworkOptions } from "#types/client.js"; -import { clearClient, setClient } from "@context"; +import type { + AttachableExport, + FrameworkOptions, + ModuleExport, + ModuleExportFactory, + PrefixFn, +} from "#types/client.js"; import { + getDefaultLang, getFiles, getProjectRoot, - getPrefix, I18nLoggerAdapter, Logger, } from "@utils/index.js"; -import { CommandBuilder } from "@structures/index.js"; +import { CommandBuilder, EventBuilder } from "@structures/index.js"; const defaultOpts: Omit = { includePaths: ["events", "commands"], autoRegisterCommands: true, + getDefaultLang, }; export class Client< @@ -31,7 +37,7 @@ export class Client< readonly logger: Logger; commands: Collection; aliases: Collection>; - readonly prefix: string | false; + readonly prefix: PrefixFn; i18n: i18n | undefined; declare options: Omit & { @@ -43,7 +49,7 @@ export class Client< this.logger = new Logger(opts.logger); this.commands = new Collection(); this.aliases = new Collection(); - this.prefix = getPrefix(this.options.prefix ?? { enabled: false }); + this.prefix = this.options.prefix ?? (() => false); if (this.options.i18n) { this.i18n = this.options.i18n; this.i18n.use(new I18nLoggerAdapter(this.logger)); @@ -53,7 +59,10 @@ export class Client< await this.#loadCoreEvents(); for (const includePath of this.options.includePaths) { try { - await this.#loadDir(path.join(getProjectRoot(), includePath)); + const resolvedDir = path.isAbsolute(includePath) + ? includePath + : path.join(getProjectRoot(), includePath); + await this.#loadDir(resolvedDir); } catch (error) { this.logger.error(`Error loading ${includePath}:`, error); } @@ -76,36 +85,82 @@ export class Client< } async #loadCoreEvents() { - setClient(this); - try { - const coreEventLoaders = [ - () => import("../../events/ready.js"), - () => import("../../events/interaction.js"), - ] as const; - for (const load of coreEventLoaders) { - await load(); - } - if (this.prefix) { - await import("../../events/message.js"); - } - } finally { - clearClient(); + const coreEventLoaders = [ + () => import("../../events/ready.js"), + () => import("../../events/interaction.js"), + ] as const; + for (const load of coreEventLoaders) { + const mod = await load(); + await this.#registerModuleExports(mod, "[core]"); + } + if (this.options.prefix) { + const mod = await import("../../events/message.js"); + await this.#registerModuleExports(mod, "[core]"); } } async #loadFile(file: string) { try { - setClient(this); const resolvedFileUrl = pathToFileURL(file); resolvedFileUrl.searchParams.set("ts", Date.now().toString(36)); - await import(resolvedFileUrl.href); + const mod = await import(resolvedFileUrl.href); + await this.#registerModuleExports(mod, file); } catch (error) { this.logger.error(`Error loading file ${file}:`, error); - } finally { - clearClient(); } } + async #registerModuleExports(mod: T, source: string) { + const unique = new Set(); + for (const value of Object.values(mod) as ModuleExport[]) { + unique.add(value); + } + + for (const value of unique) { + await this.registerExport(value, source); + } + } + + async registerExport(exported: ModuleExport, source: string) { + if (exported == null) return; + + if (Array.isArray(exported)) { + for (const item of exported) { + await this.registerExport(item, source); + } + return; + } + + if (exported instanceof CommandBuilder) { + exported.attach(this); + return; + } + + if (exported instanceof EventBuilder) { + exported.attach(this); + return; + } + + if ( + typeof exported === "object" && + "attach" in exported && + typeof (exported as Partial).attach === "function" + ) { + await (exported as AttachableExport).attach(this); + return; + } + + if (typeof exported === "function") { + const maybeBuilt = await (exported as ModuleExportFactory)(this); + await this.registerExport(maybeBuilt, source); + return; + } + + this.logger.debug( + `Skipped unsupported export type in ${source}: ${typeof exported}` + ); + } + public async registerCommands() { if (!this.token) { this.logger.warn("registerCommands skipped: client token is not set."); diff --git a/src/utils/Files.ts b/src/utils/Files.ts index 9047168..ee89739 100644 --- a/src/utils/Files.ts +++ b/src/utils/Files.ts @@ -3,7 +3,7 @@ import { existsSync } from "node:fs"; import path from "path"; export function getFiles(baseDir: string): string[] { - return FastGlob.sync(["**/*.ts", "**/*.js"], { + return FastGlob.sync(["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs"], { cwd: baseDir, absolute: true, ignore: [ diff --git a/src/utils/logger/ILogger.ts b/src/utils/logger/ILogger.ts index 776bd50..96e04e8 100644 --- a/src/utils/logger/ILogger.ts +++ b/src/utils/logger/ILogger.ts @@ -35,7 +35,7 @@ export enum LogLevel { Fatal = 60, /** - * An unknown or uncategorized level. + * An uncategorized level. */ None = 100, } diff --git a/src/utils/logger/Logger.ts b/src/utils/logger/Logger.ts index fbecc01..90d2c85 100644 --- a/src/utils/logger/Logger.ts +++ b/src/utils/logger/Logger.ts @@ -12,6 +12,7 @@ import type { LoggerOptions, LoggerStyleOptions, LoggerStyleResolvable, + LoggerValues, LoggerTimestampFormatter, LoggerTimestampOptions, } from "#types/logger.js"; @@ -24,6 +25,7 @@ export type { LoggerOptions, LoggerStyleOptions, LoggerStyleResolvable, + LoggerValues, LoggerTimestampFormatter, LoggerTimestampOptions, } from "#types/logger.js"; @@ -100,35 +102,35 @@ export class Logger implements ILogger { return level >= this.level; } - trace(...values: readonly unknown[]): void { + trace(...values: LoggerValues): void { this.write(LogLevel.Trace, ...values); } - debug(...values: readonly unknown[]): void { + debug(...values: LoggerValues): void { this.write(LogLevel.Debug, ...values); } - info(...values: readonly unknown[]): void { + info(...values: LoggerValues): void { this.write(LogLevel.Info, ...values); } - log(...values: readonly unknown[]): void { + log(...values: LoggerValues): void { this.write(LogLevel.Info, ...values); } - warn(...values: readonly unknown[]): void { + warn(...values: LoggerValues): void { this.write(LogLevel.Warn, ...values); } - error(...values: readonly unknown[]): void { + error(...values: LoggerValues): void { this.write(LogLevel.Error, ...values); } - fatal(...values: readonly unknown[]): void { + fatal(...values: LoggerValues): void { this.write(LogLevel.Fatal, ...values); } - write(level: LogLevel, ...values: readonly unknown[]): void { + write(level: LogLevel, ...values: LoggerValues): void { if (level < this.level) return; const method = Logger.LOG_METHODS.get(level); @@ -157,7 +159,7 @@ export class Logger implements ILogger { } } - protected preprocess(values: readonly unknown[]): string { + protected preprocess(values: LoggerValues): string { const inspectOptions: InspectOptions = { colors: colorette.isColorSupported, depth: this.depth, @@ -369,15 +371,15 @@ export enum LoggerStyleBackground { export class I18nLoggerAdapter implements LoggerModule { public readonly type = "logger"; constructor(private readonly logger: Logger) {} - log(...args: unknown[]): void { + log(...args: Parameters): void { this.logger.debug("[i18next]", ...args); } - warn(...args: unknown[]): void { + warn(...args: Parameters): void { this.logger.warn("[i18next]", ...args); } - error(...args: unknown[]): void { + error(...args: Parameters): void { this.logger.error("[i18next]", ...args); } } diff --git a/src/utils/util.ts b/src/utils/util.ts index 28604ef..dfcf3ea 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,5 +1,47 @@ -import type { PrefixOptions } from "#types/client.js"; -import { InteractionResponse, Message } from "discord.js"; +import { + ChatInputCommandInteraction, + InteractionResponse, + Locale, + Message, +} from "discord.js"; +import type { Context } from "@structures/index.js"; + +export const allowedLocales = [ + "id", + "en-US", + "en-GB", + "bg", + "zh-CN", + "zh-TW", + "hr", + "cs", + "da", + "nl", + "fi", + "fr", + "de", + "el", + "hi", + "hu", + "it", + "ja", + "ko", + "lt", + "no", + "pl", + "pt-BR", + "ro", + "ru", + "es-ES", + "es-419", + "sv-SE", + "th", + "tr", + "uk", + "vi", +] as const satisfies readonly `${Locale}`[]; + +const allowedLocalesSet = new Set(allowedLocales); export function deleteMessageAfterSent( message: Message | InteractionResponse, @@ -13,14 +55,26 @@ export function deleteMessageAfterSent( }); } -export function getPrefix(opts: PrefixOptions): string | false { - if (typeof opts === "string") { - return opts; - } +export function toAllowedLocale( + locale: string | null | undefined +): `${Locale}` | undefined { + if (!locale) return undefined; + if (!allowedLocalesSet.has(locale)) return undefined; + return locale as `${Locale}`; +} - if (opts.enabled && opts.prefix) { - return opts.prefix; +export function getDefaultLang( + ctx: Context +): `${Locale}` { + if (ctx.isInteraction()) { + return ( + toAllowedLocale(ctx.data.locale) ?? + toAllowedLocale(ctx.data.guildLocale) ?? + Locale.EnglishUS + ); } - - return false; + if (ctx.isMessage()) { + return toAllowedLocale(ctx.data.guild?.preferredLocale) ?? Locale.EnglishUS; + } + return Locale.EnglishUS; } diff --git a/tsconfig.json b/tsconfig.json index 006b559..06005f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "#types/*": ["./types/*"], "@types/*": ["./types/*"], "@constants/*": ["./src/constants/*"], - "@context": ["./src/context.ts"], "@structures/*": ["./src/structures/*"], "@utils/*": ["./src/utils/*"] }, diff --git a/types/client.d.ts b/types/client.d.ts index de0d0a0..b995683 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -1,22 +1,47 @@ -import { ClientOptions } from "discord.js"; +import { + ChatInputCommandInteraction, + ClientOptions, + Locale, + Message, +} from "discord.js"; import type { LoggerOptions } from "./logger.js"; import { i18n } from "i18next"; +import type { Context } from "../src/structures/builder/Context.js"; +import type { CommandBuilder } from "../src/structures/builder/Command.js"; +import type { EventBuilder } from "../src/structures/builder/Event.js"; +import type { Client } from "../src/structures/core/Client.js"; -export interface FrameworkPaths { - events?: string; - commands?: string; - locales?: string; -} - -export type PrefixOptions = - | { enabled: true; prefix: string } - | { enabled: false } - | string; +export type PrefixFn = (ctx: Context) => string | false; +export type GetDefaultLangFn = ( + ctx: Context +) => `${Locale}` | undefined; export interface FrameworkOptions extends ClientOptions { logger?: LoggerOptions; - prefix?: PrefixOptions; + prefix?: PrefixFn; + getDefaultLang?: GetDefaultLangFn; autoRegisterCommands?: boolean; includePaths: string[]; i18n?: i18n; } + +export type AttachableExport = { + attach: (client: Client) => void | Promise; +}; + +export type ModuleExport = + | CommandBuilder + | EventBuilder + | AttachableExport + | ModuleExportFactory + | readonly ModuleExport[] + | null + | undefined; + +/** + * Factory function that produces module exports. + * The implementation must handle nested factories with appropriate depth limiting. + */ +export type ModuleExportFactory = ( + client: Client +) => ModuleExport | Promise; diff --git a/types/logger.d.ts b/types/logger.d.ts index 95fc4ae..6fed685 100644 --- a/types/logger.d.ts +++ b/types/logger.d.ts @@ -1,4 +1,5 @@ import type { Color } from "colorette"; +import type { Console } from "node:console"; import type { LogLevel } from "../src/utils/logger/ILogger.js"; import type { LoggerStyleBackground, @@ -8,13 +9,13 @@ import type { export interface ILogger { has(level: LogLevel): boolean; - trace(...values: readonly unknown[]): void; - debug(...values: readonly unknown[]): void; - info(...values: readonly unknown[]): void; - warn(...values: readonly unknown[]): void; - error(...values: readonly unknown[]): void; - fatal(...values: readonly unknown[]): void; - write(level: LogLevel, ...values: readonly unknown[]): void; + trace(...values: LoggerValues): void; + debug(...values: LoggerValues): void; + info(...values: LoggerValues): void; + warn(...values: LoggerValues): void; + error(...values: LoggerValues): void; + fatal(...values: LoggerValues): void; + write(level: LogLevel, ...values: LoggerValues): void; } export interface LoggerOptions { @@ -60,4 +61,6 @@ export interface LoggerStyleOptions { background?: LoggerStyleBackground; } +export type LoggerValues = Parameters; + export type LoggerStyleResolvable = Color | LoggerStyleOptions; From e0b2c168950ef1d688e7a0ca28540273f9aa7bed Mon Sep 17 00:00:00 2001 From: vrdons Date: Sun, 8 Mar 2026 12:53:47 +0300 Subject: [PATCH 2/3] refactor(core): limit export factory recursion depth Add MAX_EXPORT_FACTORY_DEPTH constant and enforce it in registerExport to prevent infinite recursion when registering export factories. Rename messages.ts to lang.ts and update imports. Remove unused prefix log and package.json imports field. --- examples/basic_client/events/ready.cjs | 1 - package.json | 3 --- src/constants/exports.ts | 1 + src/constants/{messages.ts => lang.ts} | 0 src/events/interaction.ts | 2 +- src/events/message.ts | 2 +- src/structures/core/Client.ts | 18 ++++++++++++++---- 7 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 src/constants/exports.ts rename src/constants/{messages.ts => lang.ts} (100%) diff --git a/examples/basic_client/events/ready.cjs b/examples/basic_client/events/ready.cjs index 73350f5..2b10c68 100644 --- a/examples/basic_client/events/ready.cjs +++ b/examples/basic_client/events/ready.cjs @@ -3,5 +3,4 @@ const { EventBuilder } = require("../../../dist/index.cjs"); module.exports = new EventBuilder("clientReady", false, (context) => { context.logger.log("Client connected!"); context.logger.warn(`Current user: ${context.client.user.username}`); - context.logger.warn(`Current prefix: ${context.client.prefix ?? "none"}`); }); diff --git a/package.json b/package.json index c5bdf03..69d45c6 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,6 @@ "main": "dist/index.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", - "imports": { - "#types/*": "./types/*.d.ts" - }, "exports": { ".": { "types": "./dist/index.d.ts", diff --git a/src/constants/exports.ts b/src/constants/exports.ts new file mode 100644 index 0000000..a603aa9 --- /dev/null +++ b/src/constants/exports.ts @@ -0,0 +1 @@ +export const MAX_EXPORT_FACTORY_DEPTH = 16; diff --git a/src/constants/messages.ts b/src/constants/lang.ts similarity index 100% rename from src/constants/messages.ts rename to src/constants/lang.ts diff --git a/src/events/interaction.ts b/src/events/interaction.ts index ac983d7..d353771 100644 --- a/src/events/interaction.ts +++ b/src/events/interaction.ts @@ -2,7 +2,7 @@ import { Events, MessageFlags } from "discord.js"; import { COMMAND_DISABLED_MESSAGE, COMMAND_EXECUTE_ERROR_MESSAGE, -} from "@constants/messages.js"; +} from "@constants/lang.js"; import { EventBuilder, Context } from "@structures/index.js"; export default new EventBuilder(Events.InteractionCreate, false).onExecute( diff --git a/src/events/message.ts b/src/events/message.ts index 9b55dd5..8c948fd 100644 --- a/src/events/message.ts +++ b/src/events/message.ts @@ -1,5 +1,5 @@ import { Events } from "discord.js"; -import { COMMAND_DISABLED_MESSAGE } from "@constants/messages.js"; +import { COMMAND_DISABLED_MESSAGE } from "@constants/lang.js"; import { EventBuilder, Context } from "@structures/index.js"; import { deleteMessageAfterSent } from "@utils/index.js"; diff --git a/src/structures/core/Client.ts b/src/structures/core/Client.ts index cf93baf..02f49f2 100644 --- a/src/structures/core/Client.ts +++ b/src/structures/core/Client.ts @@ -23,6 +23,7 @@ import { I18nLoggerAdapter, Logger, } from "@utils/index.js"; +import { MAX_EXPORT_FACTORY_DEPTH } from "@constants/exports.js"; import { CommandBuilder, EventBuilder } from "@structures/index.js"; const defaultOpts: Omit = { @@ -30,7 +31,6 @@ const defaultOpts: Omit = { autoRegisterCommands: true, getDefaultLang, }; - export class Client< Ready extends boolean = boolean, > extends DiscordClient { @@ -121,12 +121,16 @@ export class Client< } } - async registerExport(exported: ModuleExport, source: string) { + async registerExport( + exported: ModuleExport, + source: string, + factoryDepth: number = 0 + ) { if (exported == null) return; if (Array.isArray(exported)) { for (const item of exported) { - await this.registerExport(item, source); + await this.registerExport(item, source, factoryDepth); } return; } @@ -151,8 +155,14 @@ export class Client< } if (typeof exported === "function") { + if (factoryDepth >= MAX_EXPORT_FACTORY_DEPTH) { + this.logger.error( + `Skipped export factory in ${source}: max depth (${MAX_EXPORT_FACTORY_DEPTH}) exceeded.` + ); + return; + } const maybeBuilt = await (exported as ModuleExportFactory)(this); - await this.registerExport(maybeBuilt, source); + await this.registerExport(maybeBuilt, source, factoryDepth + 1); return; } From 1c627d80c9e53048125267cab0036e30ce3c9940 Mon Sep 17 00:00:00 2001 From: vrdons Date: Sun, 8 Mar 2026 12:56:56 +0300 Subject: [PATCH 3/3] chore(utils): simplify getDefaultLang return logic --- src/utils/util.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/util.ts b/src/utils/util.ts index dfcf3ea..ceea871 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -73,8 +73,5 @@ export function getDefaultLang( Locale.EnglishUS ); } - if (ctx.isMessage()) { - return toAllowedLocale(ctx.data.guild?.preferredLocale) ?? Locale.EnglishUS; - } - return Locale.EnglishUS; + return toAllowedLocale(ctx.data.guild?.preferredLocale) ?? Locale.EnglishUS; }