Skip to content

Instantly share code, notes, and snippets.

@JLarky
Last active January 22, 2024 08:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLarky/de14d71eb2075a8fafc2a99fdd9e36b0 to your computer and use it in GitHub Desktop.
Save JLarky/de14d71eb2075a8fafc2a99fdd9e36b0 to your computer and use it in GitHub Desktop.
Remix environment variables

This is a small home grown version of t3-env but with much smaller set of features and example is written using Remix (v1) and Valibot

Big limitations compared to t3-env is that it doesn't enforce client prefix

Also there are some random bits like importing .server files from client, that will probably require you to use vite-env-only, I will try to update this gist once I migrate to Vite :)

import type { LoaderArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { serverEnv } from '~/environment/env.server';
import { clientEnv } from '~/environment/env';
export async function loader({}: LoaderArgs) {
const { AWS_S3_BUCKET, BASE_URL, APP_ENV, npm_package_version } = serverEnv;
return json({});
}
export default () => {
const { BASE_URL } = clientEnv;
// try this:
// const { DOESNT_EXIST } = clientEnv;
// const { AWS_S3_BUCKET } = clientEnv;
return 'hello';
};
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>;
}
import { prepareClientEnv, type ClientEnv } from './env.server';
function initClientEnv() {
const isServer = typeof document === 'undefined';
const parsed = isServer ? prepareClientEnv() : window.ENV;
const env = new Proxy(parsed, {
get(target, name) {
if (name === 'toJSON') {
return () => target;
}
// from https://github.com/t3-oss/t3-env/blob/91f790c9592df56173bf12de76fe74bcf58ed00a/packages/core/src/index.ts#L236
if (
typeof name !== 'string' ||
name === '__esModule' ||
name === '$$typeof'
) {
return undefined;
}
// check if variable name is known
if (name in target) {
return target[name as keyof typeof target];
} else {
throw new Error(
`❌ clientEnv.${name} doesn't exist or is server-only, for server-only variables use serverEnv`,
);
}
},
ownKeys(...args) {
return Reflect.ownKeys(...args);
},
getOwnPropertyDescriptor(...args) {
return Reflect.getOwnPropertyDescriptor(...args);
},
});
return env satisfies ClientEnv;
}
export const clientEnv = initClientEnv();
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import { clientEnv } from './environment/env';
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
<script
dangerouslySetInnerHTML={{
__html: `window.ENV=${JSON.stringify(clientEnv)}`,
}}
/>
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment