Last active
April 23, 2022 03:06
-
-
Save jrr/d89128fd6514660d424393c1b01741f1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Runtime Config | |
Create React App provides a mechanism for _build-time_ configuration, | |
via REACT_APP_ env vars. | |
In order to make a single build of the frontend portable across environments, | |
it's desirable to also be able to provide _run-time_ configuration. | |
So we built a way to do it. It works like this: | |
1) a specially-named env var is set server-side, e.g. REACT_RUNTIME_FOOBAR | |
2) a server endpoint (/api/runtime-config.js) provides a small snippet of javascript | |
that, when run on the client, stores configuration on the window object. | |
3) the client runs this script very early in execution (`<script src=` in index.html) | |
4) client code accesses configuration through getRuntimeConfig() below | |
*/ | |
import camelCase from "lodash/camelCase"; | |
import difference from "lodash/difference"; | |
/** | |
* The set of server-side env vars necessary to populate the run-time configuration | |
* needed by the client. This is the source of truth; add new things here. | |
* (camel-case client types are derived from it) | |
*/ | |
const runtimeConfigEnvVars = [ | |
"REACT_RUNTIME_IDENTITY_PROVIDER_ID", | |
"REACT_RUNTIME_EXTERNAL_LINK_URL", | |
"REACT_RUNTIME_NAV_COLOR", | |
] as const; | |
export const runtimeConfigKeyName = "runtimeConfig"; | |
const ENV_VAR_PREFIX = "REACT_RUNTIME_"; | |
const envVarToShortName = (s: string) => | |
camelCase(s.replace(ENV_VAR_PREFIX, "")); | |
const shortenedConfigNames = runtimeConfigEnvVars.map(envVarToShortName); | |
type KnownConfigName = typeof runtimeConfigEnvVars[number]; | |
type CamelCase<S extends string> = | |
// https://stackoverflow.com/a/65015868 | |
S extends `${infer P1}_${infer P2}${infer P3}` | |
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}` | |
: Lowercase<S>; | |
type RemovePrefix< | |
S extends string | |
> = S extends `${typeof ENV_VAR_PREFIX}${infer P}` ? P : S; | |
type RuntimeConfigDict = Record< | |
CamelCase<RemovePrefix<KnownConfigName>>, | |
string | |
>; | |
/** Build config dict from environment variables. (pass in `process.env`) */ | |
export function buildRuntimeConfigObject(input: unknown): RuntimeConfigDict { | |
if (typeof input !== "object" || input === null) { | |
throw new Error("invalid input to buildConfigObject()"); | |
} | |
const matchingEnvVars = Object.entries(input).filter(([k, _]) => | |
k.startsWith(ENV_VAR_PREFIX) | |
); | |
logDifferences( | |
runtimeConfigEnvVars, | |
matchingEnvVars.map(([a, _]) => a) | |
); | |
const obj = Object.fromEntries( | |
matchingEnvVars.map(([k, v]) => [envVarToShortName(k), v]) | |
); | |
return obj as RuntimeConfigDict; | |
} | |
function hasConfig(x: unknown): x is { [runtimeConfigKeyName]: unknown } { | |
if (typeof x == "object" && x != null) { | |
return runtimeConfigKeyName in x; | |
} | |
return false; | |
} | |
/** Load runtime config on client. (pass in the 'window' object) */ | |
export function getRuntimeConfig(window: unknown): RuntimeConfigDict { | |
if (!hasConfig(window)) { | |
throw new Error( | |
`invalid window given. (expected ${runtimeConfigKeyName} property)` | |
); | |
} | |
const runtimeConfig = window[runtimeConfigKeyName]; | |
if (typeof runtimeConfig != "object" || runtimeConfig === null) { | |
throw new Error(`invalid ${runtimeConfigKeyName} (expected object)`); | |
} | |
logDifferences(shortenedConfigNames, Object.keys(runtimeConfig)); | |
return runtimeConfig as RuntimeConfigDict; | |
} | |
function logDifferences( | |
expected: readonly string[], | |
actual: readonly string[] | |
) { | |
const missing = difference(expected, actual); | |
if (missing.length > 0) { | |
console.warn( | |
`Runtime config: the following item(s) are missing (expected by client): ${missing.join( | |
", " | |
)}` | |
); | |
} | |
const extra = difference(actual, expected); | |
if (extra.length > 0) { | |
console.warn( | |
`Runtime config: the following item(s) are extraneous (not expected by client): ${extra.join( | |
", " | |
)}` | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This goes with a blog post: Runtime Configuration for SPAs