feat: runtime env

Support runtime environment variables using server components.

This will mean docker images can change env vars for runtime as required.
This commit is contained in:
Mythie
2023-11-12 13:10:30 +11:00
parent aec0d2ae97
commit 1cd60e1abb
29 changed files with 254 additions and 70 deletions

View File

@ -4,7 +4,7 @@ import {
TFeatureFlagValue,
ZFeatureFlagValueSchema,
} from '@documenso/lib/client-only/providers/feature-flag.types';
import { APP_BASE_URL } from '@documenso/lib/constants/app';
import { appBaseUrl } from '@documenso/lib/constants/app';
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
/**
@ -24,7 +24,7 @@ export const getFlag = async (
return LOCAL_FEATURE_FLAGS[flag] ?? true;
}
const url = new URL(`${APP_BASE_URL}/api/feature-flag/get`);
const url = new URL(`${appBaseUrl()}/api/feature-flag/get`);
url.searchParams.set('flag', flag);
const response = await fetch(url, {
@ -57,7 +57,7 @@ export const getAllFlags = async (
return LOCAL_FEATURE_FLAGS;
}
const url = new URL(`${APP_BASE_URL}/api/feature-flag/all`);
const url = new URL(`${appBaseUrl()}/api/feature-flag/all`);
return fetch(url, {
headers: {
@ -82,7 +82,7 @@ export const getAllAnonymousFlags = async (): Promise<Record<string, TFeatureFla
return LOCAL_FEATURE_FLAGS;
}
const url = new URL(`${APP_BASE_URL}/api/feature-flag/all`);
const url = new URL(`${appBaseUrl()}/api/feature-flag/all`);
return fetch(url, {
next: {

View File

@ -0,0 +1,26 @@
'use client';
import React, { useContext } from 'react';
import { PublicEnv } from './types';
export type RuntimeEnvClientProviderProps = {
value: PublicEnv;
children: React.ReactNode;
};
const RuntimeEnvContext = React.createContext<PublicEnv | null>(null);
export const useRuntimeEnv = () => {
const context = useContext(RuntimeEnvContext);
if (!context) {
throw new Error('useRuntimeEnv must be used within a RuntimeEnvProvider');
}
return context;
};
export const RuntimeEnvClientProvider = ({ value, children }: RuntimeEnvClientProviderProps) => {
return <RuntimeEnvContext.Provider value={value}>{children}</RuntimeEnvContext.Provider>;
};

View File

@ -0,0 +1,22 @@
import { PublicEnv } from './types';
declare global {
interface Window {
__unstable_runtimeEnv: PublicEnv;
}
}
export const getRuntimeEnv = () => {
if (typeof window === 'undefined') {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return Object.entries(process.env)
.filter(([key]) => key.startsWith('NEXT_PUBLIC_'))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) as PublicEnv;
}
if (typeof window !== 'undefined' && window.__unstable_runtimeEnv) {
return window.__unstable_runtimeEnv;
}
throw new Error('RuntimeEnv is not available');
};

View File

@ -0,0 +1 @@
export { RuntimeEnvProvider, type RuntimeEnvProviderProps } from './server';

View File

@ -0,0 +1,29 @@
'use server';
import React from 'react';
import { RuntimeEnvClientProvider } from './client';
import { PublicEnv } from './types';
export type RuntimeEnvProviderProps = {
children: React.ReactNode;
};
export const RuntimeEnvProvider = ({ children }: RuntimeEnvProviderProps) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const publicEnv = Object.entries(process.env)
.filter(([key]) => key.startsWith('NEXT_PUBLIC_'))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) as PublicEnv;
return (
<RuntimeEnvClientProvider value={publicEnv}>
{children}
<script
dangerouslySetInnerHTML={{
__html: `window.__unstable_runtimeEnv = ${JSON.stringify(publicEnv)}`,
}}
/>
</RuntimeEnvClientProvider>
);
};

View File

@ -0,0 +1,3 @@
import { PickStartsWith } from '../../types/pick-starts-with';
export type PublicEnv = PickStartsWith<typeof process.env, 'NEXT_PUBLIC_'>;

View File

@ -4,6 +4,7 @@ import { match } from 'ts-pattern';
import { DocumentDataType } from '@documenso/prisma/client';
import { createDocumentData } from '../../server-only/document-data/create-document-data';
import { getRuntimeEnv } from '../runtime-env/get-runtime-env';
type File = {
name: string;
@ -12,7 +13,9 @@ type File = {
};
export const putFile = async (file: File) => {
const { type, data } = await match(process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT)
const { NEXT_PUBLIC_UPLOAD_TRANSPORT } = getRuntimeEnv();
const { type, data } = await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
.with('s3', async () => putFileInS3(file))
.otherwise(async () => putFileInDatabase(file));

View File

@ -12,6 +12,7 @@ import path from 'node:path';
import { ONE_HOUR, ONE_SECOND } from '../../constants/time';
import { getServerComponentSession } from '../../next-auth/get-server-session';
import { alphaid } from '../id';
import { getRuntimeEnv } from '../runtime-env/get-runtime-env';
export const getPresignPostUrl = async (fileName: string, contentType: string) => {
const client = getS3Client();
@ -103,7 +104,9 @@ export const deleteS3File = async (key: string) => {
};
const getS3Client = () => {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
const { NEXT_PUBLIC_UPLOAD_TRANSPORT } = getRuntimeEnv();
if (NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
throw new Error('Invalid upload transport');
}

View File

@ -0,0 +1,20 @@
import { useRuntimeEnv } from './runtime-env/client';
/* eslint-disable turbo/no-undeclared-env-vars */
export const useBaseUrl = () => {
const { NEXT_PUBLIC_WEBAPP_URL } = useRuntimeEnv();
if (typeof window !== 'undefined') {
return '';
}
if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
}
if (NEXT_PUBLIC_WEBAPP_URL) {
return NEXT_PUBLIC_WEBAPP_URL;
}
return `http://localhost:${process.env.PORT ?? 3000}`;
};