diff --git a/.changeset/cyan-beers-boil.md b/.changeset/cyan-beers-boil.md new file mode 100644 index 0000000..4977e3f --- /dev/null +++ b/.changeset/cyan-beers-boil.md @@ -0,0 +1,6 @@ +--- +"@djs-core/runtime": minor +"@djs-core/dev": minor +--- + +Add support of client.config (managed by djs-core) diff --git a/app/config.json b/app/config.json new file mode 100644 index 0000000..cf4e126 --- /dev/null +++ b/app/config.json @@ -0,0 +1,3 @@ +{ + "apiKeys": ["key1", "key2"] +} diff --git a/app/djs.config.ts b/app/djs.config.ts index ad0d4f6..46bfc69 100644 --- a/app/djs.config.ts +++ b/app/djs.config.ts @@ -13,5 +13,6 @@ export default { }, experimental: { cron: true, + userConfig: true, }, } satisfies Config; diff --git a/app/tsconfig.json b/app/tsconfig.json index 7a8c1b3..02cbc49 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -8,5 +8,5 @@ "@events/*": ["./src/events/*"] } }, - "include": ["src/**/*"] + "include": ["src/**/*", ".djscore/**/*.d.ts"] } diff --git a/biome.json b/biome.json index 4f86130..9807869 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/bun.lock b/bun.lock index 7b449e8..8cc4ad7 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "djs-core", "devDependencies": { - "@biomejs/biome": "2.3.10", + "@biomejs/biome": "^2.3.14", "@changesets/cli": "^2.29.8", "@types/node": "^25.0.3", "knip": "^5.76.0", @@ -27,12 +27,12 @@ }, "packages/dev": { "name": "@djs-core/dev", - "version": "2.0.0", + "version": "3.0.0", "bin": { "djs-core": "dist/index.js", }, "dependencies": { - "@clack/prompts": "^0.11.0", + "@clack/prompts": "^1.0.0", "cac": "^6.7.14", "chokidar": "^5.0.0", "picocolors": "^1.1.1", @@ -49,7 +49,7 @@ }, "packages/runtime": { "name": "@djs-core/runtime", - "version": "1.3.0", + "version": "1.4.0", "dependencies": { "cron": "^4.4.0", }, @@ -66,23 +66,23 @@ "packages": { "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], - "@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="], + "@biomejs/biome": ["@biomejs/biome@2.3.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.14", "@biomejs/cli-darwin-x64": "2.3.14", "@biomejs/cli-linux-arm64": "2.3.14", "@biomejs/cli-linux-arm64-musl": "2.3.14", "@biomejs/cli-linux-x64": "2.3.14", "@biomejs/cli-linux-x64-musl": "2.3.14", "@biomejs/cli-win32-arm64": "2.3.14", "@biomejs/cli-win32-x64": "2.3.14" }, "bin": { "biome": "bin/biome" } }, "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ=="], "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.0.14", "", { "dependencies": { "@changesets/config": "^3.1.2", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA=="], @@ -118,9 +118,9 @@ "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], - "@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="], + "@clack/core": ["@clack/core@1.0.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ=="], - "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + "@clack/prompts": ["@clack/prompts@1.0.0", "", { "dependencies": { "@clack/core": "1.0.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A=="], "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], @@ -454,6 +454,8 @@ "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@djs-core/runtime/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -466,6 +468,8 @@ "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "@djs-core/runtime/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], } } diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 2c06ff9..0000000 --- a/docs/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Docus Default Starter - -> A beautiful, minimal starter for creating documentation with Docus - -This is the default Docus starter template that provides everything you need to build beautiful documentation sites with Markdown and Vue components. - -> [!TIP] -> If you're looking for i18n support, check out the [i18n starter](https://github.com/nuxt-themes/docus/tree/main/.starters/i18n). - -## ✨ Features - -- 🎨 **Beautiful Design** - Clean, modern documentation theme -- 📱 **Responsive** - Mobile-first responsive design -- 🌙 **Dark Mode** - Built-in dark/light mode support -- 🔍 **Search** - Full-text search functionality -- 📝 **Markdown Enhanced** - Extended markdown with custom components -- 🎨 **Customizable** - Easy theming and brand customization -- ⚡ **Fast** - Optimized for performance with Nuxt 4 -- 🔧 **TypeScript** - Full TypeScript support - -## 🚀 Quick Start - -```bash -# Install dependencies -npm install - -# Start development server -npm run dev -``` - -Your documentation site will be running at `http://localhost:3000` - -## 📁 Project Structure - -``` -my-docs/ -├── content/ # Your markdown content -│ ├── index.md # Homepage -│ ├── 1.getting-started/ # Getting started section -│ └── 2.essentials/ # Essential documentation -├── public/ # Static assets -└── package.json # Dependencies and scripts -``` - -## ⚡ Built with - -This starter comes pre-configured with: - -- [Nuxt 4](https://nuxt.com) - The web framework -- [Nuxt Content](https://content.nuxt.com/) - File-based CMS -- [Nuxt UI](https://ui.nuxt.com) - UI components -- [Nuxt Image](https://image.nuxt.com/) - Optimized images -- [Tailwind CSS 4](https://tailwindcss.com/) - Utility-first CSS -- [Docus Layer](https://www.npmjs.com/package/docus) - Documentation theme - -## 📖 Documentation - -For detailed documentation on customizing your Docus project, visit the [Docus Documentation](https://docus.dev) - -## 🚀 Deployment - -Build for production: - -```bash -npm run build -``` - -The built files will be in the `.output` directory, ready for deployment to any hosting provider that supports Node.js. - -## 📄 License - -[MIT License](https://opensource.org/licenses/MIT) \ No newline at end of file diff --git a/package.json b/package.json index 189ff0e..0f85cf1 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "app" ], "devDependencies": { - "@biomejs/biome": "2.3.10", + "@biomejs/biome": "^2.3.14", "@changesets/cli": "^2.29.8", "@types/node": "^25.0.3", "knip": "^5.76.0", diff --git a/packages/dev/commands/build.ts b/packages/dev/commands/build.ts index 6764f9c..06b9dce 100644 --- a/packages/dev/commands/build.ts +++ b/packages/dev/commands/build.ts @@ -4,6 +4,7 @@ import fs from "fs/promises"; import path from "path"; import pc from "picocolors"; import { banner, PATH_ALIASES } from "../utils/common"; +import { autoGenerateConfigTypes } from "../utils/config-type-generator"; declare const Bun: typeof import("bun"); @@ -76,6 +77,7 @@ function buildGeneratedEntry(opts: { eventFiles: string[]; cronFiles: string[]; hasCronEnabled: boolean; + hasUserConfigEnabled: boolean; }): string { const { genDir, @@ -169,6 +171,7 @@ function buildGeneratedEntry(opts: { import config from "../djs.config.ts"; import { DjsClient, type Route } from "@djs-core/runtime"; import { Events } from "discord.js"; +${opts.hasUserConfigEnabled ? 'import type { UserConfig } from "./config.types.ts";\nimport userConfigData from "../config.json" with { type: "json" };' : ""} ${imports.join("\n")} @@ -222,7 +225,11 @@ ${sortedCrons.map((c) => ` [${JSON.stringify(c.id)}, ${c.varName}],`).join("\ : "" } - const client = new DjsClient({ djsConfig: config }); + // Load user config if enabled. The type assertion is safe because: + // 1. The config.json is parsed and validated at build time + // 2. The UserConfig type is auto-generated from config.json structure + // 3. Any runtime mismatch will be caught during bot initialization +${opts.hasUserConfigEnabled ? " const client = new DjsClient({ djsConfig: config, userConfig: userConfigData as UserConfig });" : " const client = new DjsClient({ djsConfig: config });"} client.eventsHandler.set(events); @@ -305,9 +312,15 @@ export function registerBuildCommand(cli: CAC) { const configModule = await import(path.join(botRoot, "djs.config.ts")); const config = configModule.default as { - experimental?: { cron?: boolean }; + experimental?: { cron?: boolean; userConfig?: boolean }; }; const hasCronEnabled = config.experimental?.cron === true; + const hasUserConfigEnabled = config.experimental?.userConfig === true; + + // Auto-generate config types if userConfig is enabled + if (hasUserConfigEnabled) { + await autoGenerateConfigTypes(botRoot, true); + } const code = buildGeneratedEntry({ genDir, @@ -322,6 +335,7 @@ export function registerBuildCommand(cli: CAC) { eventFiles, cronFiles, hasCronEnabled, + hasUserConfigEnabled, }); await fs.writeFile(entryPath, code, "utf8"); diff --git a/packages/dev/commands/dev.ts b/packages/dev/commands/dev.ts index 557b23c..6095982 100644 --- a/packages/dev/commands/dev.ts +++ b/packages/dev/commands/dev.ts @@ -13,10 +13,11 @@ import { UserSelectMenu, } from "@djs-core/runtime"; import type { CAC } from "cac"; -import chokidar from "chokidar"; +import chokidar, { type FSWatcher } from "chokidar"; import path from "path"; import pc from "picocolors"; import { banner, PATH_ALIASES, runBot } from "../utils/common"; +import { autoGenerateConfigTypes } from "../utils/config-type-generator"; type SelectMenu = | StringSelectMenu @@ -392,9 +393,34 @@ export function registerDevCommand(cli: CAC) { .on("change", (p) => processFile("change", p)) .on("unlink", (p) => processFile("unlink", p)); + let configWatcher: FSWatcher | null = null; + if (config.experimental?.userConfig) { + const configJsonPath = path.join(root, "config.json"); + configWatcher = chokidar.watch(configJsonPath, { + ignoreInitial: true, + }); + + configWatcher.on("change", async () => { + console.log( + `${pc.cyan("ℹ")} config.json changed, regenerating types...`, + ); + await autoGenerateConfigTypes(root); + }); + + configWatcher.on("add", async () => { + console.log( + `${pc.green("✓")} config.json created, generating types...`, + ); + await autoGenerateConfigTypes(root); + }); + } + process.on("SIGINT", async () => { console.log(pc.dim("\nShutting down...")); await watcher.close(); + if (configWatcher) { + await configWatcher.close(); + } await client.destroy(); process.exit(0); }); diff --git a/packages/dev/commands/generate-config-types.ts b/packages/dev/commands/generate-config-types.ts new file mode 100644 index 0000000..7c06520 --- /dev/null +++ b/packages/dev/commands/generate-config-types.ts @@ -0,0 +1,45 @@ +import type { CAC } from "cac"; +import fs from "fs/promises"; +import path from "path"; +import pc from "picocolors"; +import { banner } from "../utils/common"; +import { generateTypesFromJson } from "../utils/config-type-generator"; + +export function registerGenerateConfigTypesCommand(cli: CAC) { + cli + .command( + "generate-config-types", + "Generate TypeScript types from config.json", + ) + .option("-p, --path ", "Custom project path", { default: "." }) + .action(async (options: { path: string }) => { + console.log(banner); + console.log(`${pc.cyan("ℹ")} Generating config types...`); + + const projectRoot = path.resolve(process.cwd(), options.path); + const configJsonPath = path.join(projectRoot, "config.json"); + const outputPath = path.join(projectRoot, ".djscore", "config.types.ts"); + + try { + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.access(configJsonPath); + } catch { + console.error( + pc.red( + `❌ config.json not found at ${configJsonPath}\n Create a config.json file first.`, + ), + ); + process.exit(1); + } + + try { + await generateTypesFromJson(configJsonPath, outputPath); + console.log( + pc.green(`✓ Types generated successfully at ${outputPath}`), + ); + } catch (error: unknown) { + console.error(pc.red("❌ Error generating types:"), error); + process.exit(1); + } + }); +} diff --git a/packages/dev/index.ts b/packages/dev/index.ts index bf87ddf..ec66766 100644 --- a/packages/dev/index.ts +++ b/packages/dev/index.ts @@ -3,6 +3,7 @@ import { cac } from "cac"; import type { Config } from "../utils/types/config"; import { registerBuildCommand } from "./commands/build"; import { registerDevCommand } from "./commands/dev"; +import { registerGenerateConfigTypesCommand } from "./commands/generate-config-types"; import { registerStartCommand } from "./commands/start"; export type { Config }; @@ -12,5 +13,6 @@ const cli = cac("djs-core").version("2.0.0").help(); registerStartCommand(cli); registerDevCommand(cli); registerBuildCommand(cli); +registerGenerateConfigTypesCommand(cli); cli.parse(); diff --git a/packages/dev/utils/common.ts b/packages/dev/utils/common.ts index 8b59b4c..961c331 100644 --- a/packages/dev/utils/common.ts +++ b/packages/dev/utils/common.ts @@ -26,6 +26,7 @@ import fs from "fs/promises"; import path, { resolve } from "path"; import pc from "picocolors"; import type { Config } from "../../utils/types/config"; +import { autoGenerateConfigTypes } from "./config-type-generator"; export const banner = ` ${pc.bold(pc.blue("djs-core"))} ${pc.dim(`v1.0.0`)} @@ -54,6 +55,11 @@ export async function runBot(projectPath: string) { console.log(`${pc.green("✓")} Config loaded`); + // Auto-generate config types if userConfig is enabled + if (config.experimental?.userConfig) { + await autoGenerateConfigTypes(root); + } + const commands: Route[] = []; const buttons: Button[] = []; const contextMenus: ContextMenu[] = []; @@ -137,7 +143,23 @@ export async function runBot(projectPath: string) { console.log(`${pc.green("✓")} Loaded ${pc.bold(tasks.size)} cron tasks`); } - const client = new DjsClient({ djsConfig: config }); + let userConfig: unknown; + if (config.experimental?.userConfig) { + try { + const configJsonPath = path.join(root, "config.json"); + const configJsonContent = await fs.readFile(configJsonPath, "utf-8"); + userConfig = JSON.parse(configJsonContent); + console.log(`${pc.green("✓")} User config loaded`); + } catch (_error) { + console.warn( + pc.yellow( + "⚠️ userConfig is enabled but config.json not found or invalid", + ), + ); + } + } + + const client = new DjsClient({ djsConfig: config, userConfig }); client.eventsHandler.set(events); diff --git a/packages/dev/utils/config-type-generator.ts b/packages/dev/utils/config-type-generator.ts new file mode 100644 index 0000000..42c339e --- /dev/null +++ b/packages/dev/utils/config-type-generator.ts @@ -0,0 +1,204 @@ +import fs from "fs/promises"; +import path from "path"; +import pc from "picocolors"; + +function inferType(value: unknown): string { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") return "string"; + if (typeof value === "number") return "number"; + if (typeof value === "boolean") return "boolean"; + + if (Array.isArray(value)) { + if (value.length === 0) return "unknown[]"; + const firstElement = value[0]; + const elementType = inferType(firstElement); + const allSameType = value.every((v) => inferType(v) === elementType); + return allSameType ? `${elementType}[]` : "unknown[]"; + } + + if (typeof value === "object") { + const entries = Object.entries(value); + if (entries.length === 0) return "Record"; + + const properties = entries + .map(([key, val]) => { + const valueType = inferType(val); + return `${key}: ${valueType}`; + }) + .join("; "); + + return `{ ${properties} }`; + } + + return "unknown"; +} + +function generateTypeDefinition( + obj: unknown, + typeName: string, + indent = 0, +): string { + if (obj === null) return "null"; + if (obj === undefined) return "undefined"; + + const indentStr = " ".repeat(indent); + const nextIndentStr = " ".repeat(indent + 1); + + if (Array.isArray(obj)) { + if (obj.length === 0) return "unknown[]"; + const firstElement = obj[0]; + const elementType = inferType(firstElement); + return `${elementType}[]`; + } + + if (typeof obj === "object") { + const entries = Object.entries(obj); + if (entries.length === 0) return "Record"; + + const properties = entries + .map(([key, value]) => { + const valueType = inferType(value); + return `${nextIndentStr}${key}: ${valueType};`; + }) + .join("\n"); + + return `${indent === 0 ? "" : "\n"}${indentStr}interface ${typeName} {\n${properties}\n${indentStr}}`; + } + + return inferType(obj); +} + +export async function generateTypesFromJson( + configJsonPath: string, + outputPath: string, +): Promise { + const jsonContent = await fs.readFile(configJsonPath, "utf-8"); + const config = JSON.parse(jsonContent); + + const typeDefinition = generateTypeDefinition(config, "UserConfig"); + + const fileContent = `// Auto-generated from config.json. Do not edit manually. + +${typeDefinition} + +export type { UserConfig }; +`; + + await fs.writeFile(outputPath, fileContent, "utf-8"); +} + +const DISCORD_D_TS_CONTENT = `import type { UserConfig } from "./config.types"; + +declare module "discord.js" { + interface Client { + config?: UserConfig; + } +} +`; + +const TSCONFIG_INCLUDE_ENTRY = ".djscore/**/*.d.ts"; + +/** + * Creates .djscore/discord.d.ts and ensures tsconfig.json include contains the .djscore types entry. + */ +export async function ensureDiscordAugmentation( + projectRoot: string, + silent = false, +): Promise { + const djscoreDir = path.join(projectRoot, ".djscore"); + const discordDtsPath = path.join(djscoreDir, "discord.d.ts"); + const tsconfigPath = path.join(projectRoot, "tsconfig.json"); + + try { + await fs.mkdir(djscoreDir, { recursive: true }); + await fs.writeFile( + discordDtsPath, + DISCORD_D_TS_CONTENT.trimStart(), + "utf-8", + ); + } catch (error: unknown) { + if (!silent) { + console.warn( + pc.yellow("⚠️ Could not write .djscore/discord.d.ts"), + error instanceof Error ? error.message : error, + ); + } + return; + } + + try { + const raw = await fs.readFile(tsconfigPath, "utf-8"); + const tsconfig = JSON.parse(raw) as { include?: string[] }; + const include = tsconfig.include; + if (!Array.isArray(include)) { + return; + } + if (include.includes(TSCONFIG_INCLUDE_ENTRY)) { + return; + } + include.push(TSCONFIG_INCLUDE_ENTRY); + tsconfig.include = include; + await fs.writeFile( + tsconfigPath, + JSON.stringify(tsconfig, null, 2), + "utf-8", + ); + if (!silent) { + console.log( + pc.green("✓ tsconfig.json include updated for .djscore types"), + ); + } + } catch { + // tsconfig not found or invalid: skip, no need to warn every time + } +} + +/** + * Auto-generate config types if userConfig is enabled and config.json exists + * This is called automatically by dev/build/start commands + */ +export async function autoGenerateConfigTypes( + projectRoot: string, + silent = false, +): Promise { + const configJsonPath = path.join(projectRoot, "config.json"); + const outputPath = path.join(projectRoot, ".djscore", "config.types.ts"); + + try { + await fs.access(configJsonPath); + } catch { + // config.json doesn't exist, skip generation + if (!silent) { + console.log( + pc.yellow( + "⚠️ userConfig enabled but config.json not found. Skipping type generation.", + ), + ); + } + return false; + } + + try { + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await generateTypesFromJson(configJsonPath, outputPath); + await ensureDiscordAugmentation(projectRoot, silent); + if (!silent) { + console.log(pc.green("✓ Config types auto-generated")); + } + return true; + } catch (error: unknown) { + if (!silent) { + console.warn( + pc.yellow(`⚠️ Error generating config types from ${configJsonPath}`), + ); + console.warn( + pc.dim(" Possible causes: invalid JSON syntax, file permissions"), + ); + if (error instanceof Error) { + console.warn(pc.dim(` ${error.message}`)); + } + } + return false; + } +} diff --git a/packages/runtime/DjsClient.ts b/packages/runtime/DjsClient.ts index d5b7e40..b3d891c 100644 --- a/packages/runtime/DjsClient.ts +++ b/packages/runtime/DjsClient.ts @@ -25,7 +25,7 @@ import ModalHandler from "./handler/ModalHandler"; import SelectMenuHandler from "./handler/SelectMenuHandler"; import { cleanupExpiredTokens } from "./store/DataStore"; -export default class DjsClient extends Client { +export default class DjsClient extends Client { public eventsHandler: EventHandler = new EventHandler(this); public commandsHandler: CommandHandler = new CommandHandler(this); public buttonsHandler: ButtonHandler = new ButtonHandler(this); @@ -36,8 +36,12 @@ export default class DjsClient extends Client { new ApplicationCommandHandler(this); public cronHandler: CronHandler = new CronHandler(this); private readonly djsConfig: Config; + public readonly config?: UserConfig; - constructor({ djsConfig }: { djsConfig: Config }) { + constructor({ + djsConfig, + userConfig, + }: { djsConfig: Config; userConfig?: UserConfig }) { super({ intents: [ IntentsBitField.Flags.Guilds, @@ -47,6 +51,7 @@ export default class DjsClient extends Client { ], }); this.djsConfig = djsConfig; + this.config = userConfig as UserConfig; if (djsConfig.servers && djsConfig.servers.length > 0) { this.commandsHandler.setGuilds(djsConfig.servers); diff --git a/packages/utils/types/config.d.ts b/packages/utils/types/config.d.ts index 50c3511..39ab646 100644 --- a/packages/utils/types/config.d.ts +++ b/packages/utils/types/config.d.ts @@ -8,5 +8,6 @@ export interface Config { }; experimental?: { cron?: boolean; + userConfig?: boolean; }; }