diff --git a/.env.example b/.env.example index 9517764..0bce986 100644 --- a/.env.example +++ b/.env.example @@ -29,6 +29,8 @@ REDIS_USERNAME= # The password, if nay, to connect to the Redis instance. # If there is no password, leave empty or put `nopass`. REDIS_PASSWORD= +REDIS_HOST= + # The DB that should be used for caching. REDIS_CACHE_DB= diff --git a/package.json b/package.json index d8cf06d..46bdb34 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ }, "scripts": { "format": "prettier --write \"src/**/*.ts\"", - "deploy": "bun .", - "deploy:dev": "tsc --noEmit && DEV_DEPLOYMENT=true bun ." + "deploy": "bun --env-file=.env .", + "deploy:dev": "tsc --noEmit && DEV_DEPLOYMENT=true bun --env-file=.env ." }, "dependencies": { "@sapphire/discord-utilities": "3.5.0", diff --git a/src/lib/setup/env.ts b/src/lib/setup/env.ts index d274743..252d5eb 100644 --- a/src/lib/setup/env.ts +++ b/src/lib/setup/env.ts @@ -1,3 +1,115 @@ -import dotenv from 'dotenv'; +import { z, type ZodTypeAny } from 'zod'; -dotenv.config(); +/** + * Validates and type-checks environment variables using a Zod schema. + * + * This function ensures that all required environment variables are present and correctly typed. + * If validation fails, the process will exit with an error message. + * + * @param schema - Zod schema defining the structure and types of environment variables + * @param env - Optional environment object to validate (defaults to process.env) + * @returns Validated and typed environment object + * @throws Exits process if validation fails + * @example + * ```typescript + * const schema = z.object({ + * DISCORD_TOKEN: z.string(), + * PORT: z.number() + * }); + * + * const env = Env(schema); + * console.log(env.DISCORD_TOKEN); // Fully typed access to env vars + * ``` + * + */ +export function Env(schema: T, env: Record = process.env): z.infer { + const result = schema.safeParse(env); + if (!result.success) { + console.error('❌ Invalid environment variables:', result.error.format()); + process.exit(1); // Exit the process if validation fails to prevent further execution. + } + + return result.data; +} + +const envVariables = z.object({ + /* The current deployment environment that the bot is running on. + * @default 'production' This is the default to prevent accidents. + */ + ENVIRONMENT: z.union([z.literal('production'), z.literal('development')]), + + /** + * The Discord token for the bot. + */ + DISCORD_TOKEN: z.string(), + + /** + * The Discord webhook url to push error information to. + */ + BOT_ERROR_WEBHOOK_URL: z.string(), + + /** + * The base url for the public experience api. + */ + PUBLIC_API_URL: z.string(), + + /** + * The base url for the bot's internal api. + */ + BOT_API_URL: z.string(), + + /** + * The authorization for `BOT_API_URL`. + */ + BOT_ENDPOINT_API_KEY: z.string(), + + /** + * The host for the Redis connection. + */ + REDIS_HOST: z.string(), + + /** + * The port for the Redis connection. + */ + REDIS_PORT: z.string(), + + /** + * The username to connect to the Redis instance. + */ + REDIS_USERNAME: z.string(), + + /** + * The password, if nay, to connect to the Redis instance. + * If there is no password, leave empty or put `nopass`. + */ + REDIS_PASSWORD: z.string().optional(), + + /** + * The DB that should be used for caching. + */ + REDIS_CACHE_DB: z.string(), + + /** + * The DB that should be used for @sapphire/plugin-scheduled-tasks + */ + REDIS_TASKS_DB: z.string(), + + /** + * The chrome websocket connection url. + */ + CHROME_WS_URL: z.string() +}); + +/** + * Currently using process.env only to maintain compatibility with existing files + * + * TODO: Consider refactoring to use the typed env object directly for better type safety and getting the jsdoc comments + * const env = Env(envVariables); + * console.log(env.DISCORD_TOKEN); // Fully typed access to env vars + */ +Env(envVariables); + +// Typed proccess.env +declare module 'bun' { + interface Env extends z.infer {} +} diff --git a/src/lib/types/env.d.ts b/src/lib/types/env.d.ts deleted file mode 100644 index 753dbcd..0000000 --- a/src/lib/types/env.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -declare module Bun { - interface Env { - /** - * The current deployment environment that the bot is running on. - * @default 'production' This is the default to prevent accidents. - */ - ENVIRONMENT: 'production' | 'development'; - /** - * The Discord token for the bot. - */ - DISCORD_TOKEN: string; - /** - * The Discord webhook url to push error information to. - */ - BOT_ERROR_WEBHOOK_URL: string; - - /** - * The base url for the public experience api. - */ - PUBLIC_API_URL: string; - - /** - * The base url for the bot's internal api. - */ - BOT_API_URL: string; - /** - * The authorization for `BOT_API_URL`. - */ - BOT_ENDPOINT_API_KEY: string; - - /** - * The host for the Redis connection. - */ - REDIS_HOST: string; - /** - * The port for the Redis connection. - */ - REDIS_PORT: string; - /** - * The username to connect to the Redis instance. - */ - REDIS_USERNAME: string; - /** - * The password, if nay, to connect to the Redis instance. - * If there is no password, leave empty or put `nopass`. - */ - REDIS_PASSWORD?: string; - - /** - * The DB that should be used for caching. - */ - - REDIS_CACHE_DB: string; - /** - * The DB that should be used for @sapphire/plugin-scheduled-tasks - */ - REDIS_TASKS_DB: string; - - /** - * The chrome websocket connection url. - */ - CHROME_WS_URL: string; - } -}