|
import { |
|
merge, |
|
optional, |
|
flatten, |
|
object, |
|
string, |
|
type Input, |
|
safeParse, |
|
ValiError, |
|
type BaseSchema, |
|
parse, |
|
nullable, |
|
union, |
|
literal, |
|
} from 'valibot'; |
|
|
|
const ServerSchema = object({ |
|
AWS_S3_BUCKET: string('AWS_S3_BUCKET is not set'), |
|
}); |
|
|
|
/** |
|
* Careful what you put in here, it's going to be publicly available |
|
*/ |
|
const ClientSchema = object({ |
|
APP_ENV: union( |
|
[literal('dev'), literal('local'), literal('qa'), literal('prod')], |
|
'APP_ENV should be one of dev, local, qa, prod', |
|
), |
|
BASE_URL: string('BASE_URL is not set'), |
|
npm_package_version: maybeString('npm_package_version is not set'), |
|
}); |
|
|
|
/** |
|
* Sending undefined value over JSON is impossible, so when value is missing (undefined) |
|
* we are converting it to null. Otherwise it's going to be omitted from JSON and our Proxy |
|
* check will fail. |
|
*/ |
|
function maybeString(message: string) { |
|
return optional(nullable(string(message)), null); |
|
} |
|
|
|
export type ClientEnv = Input<typeof ClientSchema>; |
|
|
|
type ServerEnv = Input<typeof ServerSchema>; |
|
|
|
function serverInit() { |
|
validateSchema(ServerSchema); |
|
validateSchema(ClientSchema); |
|
return parse(merge([ServerSchema, ClientSchema]), process.env); |
|
} |
|
|
|
function validateSchema(schema: BaseSchema) { |
|
const parsed = safeParse(schema, process.env); |
|
|
|
if (parsed.success === false) { |
|
console.error( |
|
'❌ Invalid environment variables:', |
|
flatten(parsed.issues).nested, |
|
); |
|
|
|
throw new ValiError(parsed.issues); |
|
} |
|
} |
|
|
|
// We probably don't want people to use process.env.stuff and window.ENV.stuff directly, but |
|
// let's make good types for it anyway |
|
|
|
declare global { |
|
namespace NodeJS { |
|
interface ProcessEnv extends ServerEnv, ClientEnv {} |
|
} |
|
interface Window { |
|
ENV: Readonly<ClientEnv>; |
|
} |
|
} |
|
|
|
/** |
|
* Use this when accessing from server-only modules. Use `clientEnv` for everythign else (browser + ssr) |
|
* @example |
|
* ``` |
|
* import { serverEnv } from './env'; |
|
* ``` |
|
*/ |
|
export const serverEnv = serverInit(); |
|
|
|
/** |
|
* Internal API to prepare hydration script. Use this in your code instead: |
|
* ``` |
|
* import { clientEnv } from './env'; |
|
* ``` |
|
*/ |
|
export function prepareClientEnv() { |
|
return parse( |
|
ClientSchema, |
|
process.env, |
|
) satisfies Readonly<ClientEnv> as Readonly<ClientEnv>; |
|
} |