From c8842390c7d9803fe11a4c2fb043034c05a31cd1 Mon Sep 17 00:00:00 2001 From: suddenlyGiovanni <15946771+suddenlyGiovanni@users.noreply.github.com> Date: Wed, 7 May 2025 18:48:18 +0200 Subject: [PATCH 1/2] chore(.idea): update biome and tailwindcss configurations Updated biome settings to use manual configuration mode and set executable path correctly. Removed the Tailwind CSS LSP server package reference for cleanup. --- .idea/biome.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.idea/biome.xml b/.idea/biome.xml index aff3c1eb0..f64dbeeb8 100644 --- a/.idea/biome.xml +++ b/.idea/biome.xml @@ -3,8 +3,9 @@ From 4d9d928f1156e48837a7ebbc8c39775577b16bd2 Mon Sep 17 00:00:00 2001 From: suddenlyGiovanni <15946771+suddenlyGiovanni@users.noreply.github.com> Date: Wed, 7 May 2025 18:48:55 +0200 Subject: [PATCH 2/2] refactor(app): replace env handling with Config class Replaced `init` and `getEnv` functions with a singleton `Config` class for environment variable management. Centralizes initialization, improves encapsulation, and ensures separation of server and client environment variables. Updated related imports and usage throughout the app. --- packages/app/src/entry.server.tsx | 5 +- packages/app/src/root.tsx | 4 +- packages/app/src/utils/env.server.ts | 110 ++++++++++++++++++--------- 3 files changed, 76 insertions(+), 43 deletions(-) diff --git a/packages/app/src/entry.server.tsx b/packages/app/src/entry.server.tsx index 85e30c9e5..99d40afac 100644 --- a/packages/app/src/entry.server.tsx +++ b/packages/app/src/entry.server.tsx @@ -6,12 +6,11 @@ import { isbot } from 'isbot' import { renderToPipeableStream } from 'react-dom/server' import { type AppLoadContext, type EntryContext, ServerRouter } from 'react-router' -import { getEnv, init } from '#root/src/utils/env.server.ts' +import { Config } from '#root/src/utils/env.server.ts' const ABORT_DELAY = 5_000 -init() -global.ENV = getEnv() +global.ENV = Config.instance.serverEnv export default function handleRequest( request: Request, diff --git a/packages/app/src/root.tsx b/packages/app/src/root.tsx index 7b603d9ba..800d2b764 100644 --- a/packages/app/src/root.tsx +++ b/packages/app/src/root.tsx @@ -19,7 +19,7 @@ import { useOptionalTheme, useTheme } from '#root/src/routes/resources/theme-swi import { Footer, GeneralErrorBoundary, Header } from '#root/src/shell/index.tsx' import tailwindStyleSheetUrl from '#root/src/styles/tailwind.css?url' import { ClientHintCheck, getHints } from '#root/src/utils/client-hints.tsx' -import { getEnv } from '#root/src/utils/env.server.ts' +import { Config } from '#root/client/utils/env.server.ts' import { getDomainUrl } from '#root/src/utils/misc.ts' import { getTheme } from '#root/src/utils/theme.server.ts' @@ -67,7 +67,7 @@ export function meta({ location }: Route.MetaArgs) { export function loader({ request }: Route.LoaderArgs) { return { - ENV: getEnv(), + ENV: Config.instance.clientEnv, requestInfo: { hints: getHints(request), diff --git a/packages/app/src/utils/env.server.ts b/packages/app/src/utils/env.server.ts index 36d5f0c93..54a4d8ffa 100644 --- a/packages/app/src/utils/env.server.ts +++ b/packages/app/src/utils/env.server.ts @@ -1,7 +1,6 @@ import * as process from 'node:process' -import { Either, Schema } from 'effect' -import { TreeFormatter } from 'effect/ParseResult' +import { Either, ParseResult, pipe, Schema } from 'effect' const envSchema = Schema.Struct({ ALLOW_INDEXING: Schema.optionalWith( @@ -27,6 +26,11 @@ const envSchema = Schema.Struct({ exact: true, }, ), + + APP_ENV: Schema.optionalWith(Schema.Literal('development', 'staging', 'production'), { + default: () => 'development', + }), + GITHUB_TOKEN: Schema.optionalWith( Schema.NonEmptyString.annotations({ description: 'GitHub token', @@ -43,50 +47,80 @@ const envSchema = Schema.Struct({ NODE_ENV: Schema.Literal('production', 'development', 'test'), }) -declare global { - namespace NodeJS { - interface ProcessEnv extends Schema.Schema.Encoded {} +export class Config { + static #instance: Config + #env: Schema.Schema.Type + + private constructor() { + this.#init() } -} -/** - * - * Validates the environment variables. - * If the environment variables are invalid, an error is thrown... - * Invoke this function at the edge of your application to ensure that the environment variables are set. - * Later make sure to use {@link getEnv} to access the environment variables. - */ -export function init(): void { - const maybeEnv = Schema.decodeUnknownEither(envSchema)(process.env, { errors: 'all' }) - if (Either.isLeft(maybeEnv)) { - // biome-ignore lint/suspicious/noConsole: - console.error('❌ Invalid environment variables:', TreeFormatter.formatError(maybeEnv.left)) - throw new Error('Invalid environment variables') + static get instance(): Config { + if (!Config.#instance) { + Config.#instance = new Config() + } + + return Config.#instance } -} -/** - * This is used in both `entry.server.ts` and `root.tsx` to ensure that - * the environment variables are set and globally available before the app is - * started. - * - * NOTE: Do *not* add any environment variables in here that you do not wish to - * be included in the src. - * @returns all public ENV variables - */ - -export function getEnv() { - return { - ALLOW_INDEXING: process.env.ALLOW_INDEXING, - MODE: process.env.NODE_ENV, - } as const + /** + * Initialize the environment variables. + */ + #init(): void { + pipe( + process.env, + Schema.decodeUnknownEither(envSchema, { errors: 'all' }), + Either.match({ + onLeft: (parseError): void => { + console.error( + '❌ Invalid environment variables:', + ParseResult.TreeFormatter.formatErrorSync(parseError), + ) + throw new Error('Invalid environment variables') + }, + onRight: (env): void => { + this.#env = env + Object.freeze(this.#env) + + // Do not log the message when running tests + if (this.#env.NODE_ENV !== 'test') { + // biome-ignore lint/suspicious/noConsole: We want this to be logged + console.log('✅ Environment variables loaded successfully') + } + }, + }), + ) + } + + get serverEnv() { + return this.#env + } + + /** + * Helper getter which returns a subset of the environment vars which are safe expose to the client. + * Don't expose any secrets or sensitive data here. + * Otherwise, you would expose your server vars to the client if you returned them from here as this is + * directly sent in the root to the client and set on the window.env + * @returns Subset of the whole process.env to be passed to the client and used there + */ + get clientEnv() { + const serverEnv = this.serverEnv + return { + NODE_ENV: serverEnv.NODE_ENV, + } + } } -export type Env = ReturnType +type ServerEnvVars = typeof Config.instance.serverEnv +type ClientEnvVars = typeof Config.instance.clientEnv declare global { - var ENV: Env + namespace NodeJS { + interface ProcessEnv extends ServerEnvVars {} + } + + var ENV: ClientEnvVars interface Window { - ENV: Env + ENV: ClientEnvVars } }