Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"database": {
"host": "localhost",
"port": 5432,
"name": "mydb"
},
"features": {
"premium": true,
"maxUsers": 100
},
"apiKeys": ["key1", "key2"]
}
9 changes: 9 additions & 0 deletions app/config.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Auto-generated from config.json. Do not edit manually.

interface UserConfig {
database: { host: string; port: number; name: string };
features: { premium: boolean; maxUsers: number };
apiKeys: string[];
}

export type { UserConfig };
1 change: 1 addition & 0 deletions app/djs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export default {
},
experimental: {
cron: true,
userConfig: true,
},
} satisfies Config;
20 changes: 20 additions & 0 deletions app/src/events/ready-with-config.ts.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { EventListner } from "@djs-core/runtime";
import type { UserConfig } from "../../config.types";
import { ActivityType, Events } from "discord.js";

export default new EventListner<UserConfig>()
.event(Events.ClientReady)
.run((client) => {
client.user?.setActivity({ name: "🥖 bread", type: ActivityType.Custom });

// Example: Access user config
if (client.conf) {
console.log("User config loaded:");
console.log(
` Database: ${client.conf.database.host}:${client.conf.database.port}`,
);
console.log(` Features - Premium: ${client.conf.features.premium}`);
console.log(` Features - Max Users: ${client.conf.features.maxUsers}`);
console.log(` API Keys: ${client.conf.apiKeys.length} configured`);
}
});
114 changes: 114 additions & 0 deletions docs/USER_CONFIG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# User Config Feature

This feature allows you to define custom configuration in a `config.json` file and access it with full TypeScript type safety through `client.conf`.

## Setup

### 1. Enable User Config

In your `djs.config.ts`, enable the experimental `userConfig` feature:

```typescript
import type { Config } from "@djs-core/dev";

export default {
token: process.env.TOKEN,
servers: ["your-server-id"],
experimental: {
userConfig: true, // Enable user config
},
} satisfies Config;
```

### 2. Create config.json

Create a `config.json` file in your project root with your custom configuration:

```json
{
"database": {
"host": "localhost",
"port": 5432,
"name": "mydb"
},
"features": {
"premium": true,
"maxUsers": 100
},
"apiKeys": ["key1", "key2"]
}
```

### 3. Types Auto-Generated

When you run `dev`, `build`, or `start`, the TypeScript types are **automatically generated** from your `config.json`:

```bash
bun djs-core dev # Types auto-generated
bun djs-core build # Types auto-generated
bun djs-core start # Types auto-generated
```

A `config.types.ts` file will be created automatically:

```typescript
// Auto-generated from config.json. Do not edit manually.

interface UserConfig {
database: { host: string; port: number; name: string };
features: { premium: boolean; maxUsers: number };
apiKeys: string[];
}

export type { UserConfig };
```

**In dev mode**, types are regenerated automatically when you modify `config.json`.

### 4. Use the Config in Your Code

Access your config through `client.conf` with full type safety:

```typescript
import { EventListner } from "@djs-core/runtime";
import type { UserConfig } from "../config.types";
import { Events } from "discord.js";

export default new EventListner<UserConfig>()
.event(Events.ClientReady)
.run((client) => {
// Access your config with full TypeScript autocomplete and type checking
console.log(`Database: ${client.conf.database.host}:${client.conf.database.port}`);
console.log(`Premium enabled: ${client.conf.features.premium}`);
console.log(`Max users: ${client.conf.features.maxUsers}`);
console.log(`API Keys: ${client.conf.apiKeys.length}`);
});
```

## Manual Type Generation (Optional)

If you need to generate types manually (e.g., for IDE refresh), you can use:

```bash
bun djs-core generate-config-types
```

This is optional since types are auto-generated when running dev/build/start.

## Notes

- The `config.json` file is loaded at runtime
- The `config.types.ts` file is auto-generated and should not be edited manually
- Types are **automatically regenerated** when you run `dev`, `build`, or `start`
- In **dev mode**, types regenerate when you modify `config.json`
- The `client.conf` property is typed as `UserConfig` when using the generic type parameter
- If `userConfig` is not enabled, `client.conf` will be `undefined`
- Empty arrays in config.json are typed as `unknown[]` - add at least one element for better type inference

## Example Use Cases

- Database connection settings
- Feature flags
- API keys and credentials (ensure proper security measures)
- Custom bot configuration per environment
- Application-specific settings
18 changes: 16 additions & 2 deletions packages/dev/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -76,6 +77,7 @@ function buildGeneratedEntry(opts: {
eventFiles: string[];
cronFiles: string[];
hasCronEnabled: boolean;
hasUserConfigEnabled: boolean;
}): string {
const {
genDir,
Expand Down Expand Up @@ -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")}

Expand Down Expand Up @@ -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<UserConfig>({ djsConfig: config, userConfig: userConfigData as UserConfig });" : " const client = new DjsClient({ djsConfig: config });"}

client.eventsHandler.set(events);

Expand Down Expand Up @@ -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,
Expand All @@ -322,6 +335,7 @@ export function registerBuildCommand(cli: CAC) {
eventFiles,
cronFiles,
hasCronEnabled,
hasUserConfigEnabled,
});

await fs.writeFile(entryPath, code, "utf8");
Expand Down
27 changes: 27 additions & 0 deletions packages/dev/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import chokidar 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
Expand Down Expand Up @@ -392,9 +393,35 @@ export function registerDevCommand(cli: CAC) {
.on("change", (p) => processFile("change", p))
.on("unlink", (p) => processFile("unlink", p));

// Watch config.json for changes if userConfig is enabled
let configWatcher: chokidar.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);
});
Expand Down
41 changes: 41 additions & 0 deletions packages/dev/commands/generate-config-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 <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, "config.types.ts");

try {
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);
}
});
}
2 changes: 2 additions & 0 deletions packages/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -12,5 +13,6 @@ const cli = cac("djs-core").version("2.0.0").help();
registerStartCommand(cli);
registerDevCommand(cli);
registerBuildCommand(cli);
registerGenerateConfigTypesCommand(cli);

cli.parse();
24 changes: 23 additions & 1 deletion packages/dev/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)}
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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 = undefined;
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);

Expand Down
Loading