From 52c8182ce9088356d6c5c0e18e459a989cf6679e Mon Sep 17 00:00:00 2001 From: denchiklut <ollylut@gmail.com> Date: Sat, 28 Sep 2024 19:45:14 -0700 Subject: [PATCH] chore: cleanup --- README.md | 2 +- config/spec/setup.ts | 4 +- src/client/components/@shared/error/index.tsx | 3 +- src/common/env/create-env.ts | 42 ------------- src/common/env/env.util.ts | 62 ++++++++++--------- src/common/env/index.ts | 38 +++++++++++- src/common/env/parse.util.ts | 4 +- 7 files changed, 77 insertions(+), 78 deletions(-) delete mode 100644 src/common/env/create-env.ts diff --git a/README.md b/README.md index 5c8a0bb..450a780 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ You can use .env file to specify environment variables. This file is ignored by #### Adding new `env` variable 1. Add it to `.env` file -2. For TS completion and validation add it to `envSchema` in `src/common/env/env.util.ts` +2. For TS completion and validation add it to `envSchema` in `src/common/env/index.ts` 3. If this variable needs to be accessible from both `client` & `server` make sure it's name starts with prefix `CLIENT_` 4. You can access environment variable via `getENV` function. This function will return a proper value based on environment (client/server) and cast it to a proper type based on `envSchema` from `step 2` (string/number/boolean) diff --git a/config/spec/setup.ts b/config/spec/setup.ts index 7e7d177..271f630 100644 --- a/config/spec/setup.ts +++ b/config/spec/setup.ts @@ -5,8 +5,8 @@ window.IS_DEV = false window.IS_SPA = true window.clientPrefix = 'PUBLIC_' -jest.mock('src/common/env/create-env', () => ({ - ...jest.requireActual('src/common/env/create-env'), +jest.mock('src/common/env/env.util', () => ({ + ...jest.requireActual('src/common/env/env.util'), createEnv: jest.fn(() => ({ CLIENT_HOST: 'http://localhost:3000', CLIENT_PUBLIC_PATH: '/', diff --git a/src/client/components/@shared/error/index.tsx b/src/client/components/@shared/error/index.tsx index d7341c3..8fc9d1c 100644 --- a/src/client/components/@shared/error/index.tsx +++ b/src/client/components/@shared/error/index.tsx @@ -1,8 +1,9 @@ import { useRouteError } from 'react-router' +import { logger } from 'src/common' export const Fallback = () => { const error = useRouteError() - console.error(error) + logger.error(error) return <p>Something went wrong</p> } diff --git a/src/common/env/create-env.ts b/src/common/env/create-env.ts deleted file mode 100644 index 9597ce6..0000000 --- a/src/common/env/create-env.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { ObjectSchema, InferType, AnyObject, AnySchema } from 'yup' -import { parse } from 'src/common/env/parse.util' - -interface Props<T extends AnyObject> { - schema: ObjectSchema<T> - envs: Partial<Record<keyof InferType<ObjectSchema<T>>, unknown>> - clientPrefix?: string -} -export function createEnv<S extends AnyObject>({ - schema, - envs, - clientPrefix = 'CLIENT_' -}: Props<S>) { - const client = schema.pick(Object.keys(schema.shape).filter(k => k.startsWith(clientPrefix))) - const { data, error } = parse((IS_SERVER ? schema : client) as AnySchema<S>, envs) - - if (error) { - console.error('❌ Invalid environment variables:', error.errors) - - throw new Error('Invalid environment variables') - } - - return new Proxy(data, { - get(target, prop, receiver) { - if (typeof prop !== 'string') { - return undefined - } - - if ( - !IS_SERVER && - (!clientPrefix || !prop.startsWith(clientPrefix)) && - !['toJSON', 'toString'].includes(prop) - ) { - throw new Error( - `❌ Attempted to access a server-side environment variable "${prop}" on the client` - ) - } - - return Reflect.get(target, prop, receiver) - } - }) as InferType<ObjectSchema<S>> -} diff --git a/src/common/env/env.util.ts b/src/common/env/env.util.ts index bcf5ac3..66fd647 100644 --- a/src/common/env/env.util.ts +++ b/src/common/env/env.util.ts @@ -1,35 +1,39 @@ -import { string, mixed, object, type InferType } from 'yup' -import { getOrDefault } from './get.util' -import { createEnv } from './create-env' +import type { ObjectSchema, InferType, AnyObject } from 'yup' +import { parse } from './parse.util' -if (IS_SERVER) require('dotenv/config') - -const envSchema = object({ - CLIENT_HOST: string().default('http://localhost:3000'), - CLIENT_PUBLIC_PATH: string().default('0.0.0'), - APP_VERSION: string().default('0.0.0'), - NODE_ENV: mixed<'production' | 'development' | 'test'>() - .oneOf(['production', 'development', 'test']) - .default('development') -}) +interface Props<T extends AnyObject> { + schema: ObjectSchema<T> + envs: Partial<Record<keyof InferType<ObjectSchema<T>>, unknown>> + clientPrefix?: string +} +export function createEnv<S extends AnyObject>({ + schema, + envs, + clientPrefix = 'CLIENT_' +}: Props<S>) { + const client = schema.pick(Object.keys(schema.shape).filter(k => k.startsWith(clientPrefix))) + const { data, error } = parse((IS_SERVER ? schema : client) as ObjectSchema<S>, envs) -export type Env = InferType<typeof envSchema> + if (error) { + console.error('❌ Invalid environment variables:', error.errors) + throw new Error('Invalid environment variables') + } -export const getENV = getOrDefault( - createEnv({ - clientPrefix, - schema: envSchema, - envs: IS_SERVER ? process.env : window.env_vars - }) -) + return new Proxy(data, { + get(target, prop, receiver) { + if (typeof prop !== 'string') return undefined -export const setEnvVars = (nonce: string) => { - const clientEnv = Object.entries(getENV()) - .filter(([k]) => k.startsWith(clientPrefix)) - .reduce<Collection<string, unknown>>((res, [k, v]) => { - res[k] = v - return res - }, {}) + if ( + !IS_SERVER && + (!clientPrefix || !prop.startsWith(clientPrefix)) && + !['toJSON', 'toString'].includes(prop) + ) { + throw new Error( + `❌ Attempted to access a server-side environment variable "${prop}" on the client` + ) + } - return `<script nonce='${nonce}'>window.env_vars = Object.freeze(${JSON.stringify(clientEnv)})</script>` + return Reflect.get(target, prop, receiver) + } + }) as InferType<ObjectSchema<S>> } diff --git a/src/common/env/index.ts b/src/common/env/index.ts index bebcc62..a891630 100644 --- a/src/common/env/index.ts +++ b/src/common/env/index.ts @@ -1 +1,37 @@ -export * from './env.util' +import { string, mixed, object, type InferType } from 'yup' +import { getOrDefault } from './get.util' +import { createEnv } from './env.util' + +if (IS_SERVER) require('dotenv/config') + +// you can find implementation for a `zod` library +// in the `feat/pipable-stream` git branch +const envSchema = object({ + CLIENT_HOST: string().default('http://localhost:3000'), + CLIENT_PUBLIC_PATH: string().default('0.0.0'), + APP_VERSION: string().default('0.0.0'), + NODE_ENV: mixed<'production' | 'development' | 'test'>() + .oneOf(['production', 'development', 'test']) + .default('development') +}) + +export type Env = InferType<typeof envSchema> + +export const getENV = getOrDefault( + createEnv({ + clientPrefix, + schema: envSchema, + envs: IS_SERVER ? process.env : window.env_vars + }) +) + +export const setEnvVars = (nonce: string) => { + const clientEnv = Object.entries(getENV()) + .filter(([k]) => k.startsWith(clientPrefix)) + .reduce<Collection<string, unknown>>((res, [k, v]) => { + res[k] = v + return res + }, {}) + + return `<script nonce='${nonce}'>window.env_vars=Object.freeze(${JSON.stringify(clientEnv)})</script>` +} diff --git a/src/common/env/parse.util.ts b/src/common/env/parse.util.ts index da721ab..7ae3b6b 100644 --- a/src/common/env/parse.util.ts +++ b/src/common/env/parse.util.ts @@ -1,8 +1,8 @@ -import { type AnySchema, ValidationError } from 'yup' +import { type AnyObject, type ObjectSchema, ValidationError } from 'yup' type ParseResult<T> = { data: T; error: null } | { data: null; error: ValidationError } -export function parse<T>(schema: AnySchema<T>, envs: unknown): ParseResult<T> { +export function parse<T extends AnyObject>(schema: ObjectSchema<T>, envs: unknown): ParseResult<T> { try { const data = schema.validateSync(envs, { abortEarly: false }) return { data, error: null } as ParseResult<T>