mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix(client): read theme environment variables from the build-time env
getThemeFromEnv read process.env, which vite-plugin-node-polyfills replaces with an empty shim in the browser, so every REACT_APP_THEME_* value was dropped and the loader always returned undefined. Read import.meta.env instead and register the REACT_APP_THEME_ prefix with Vite so the values are inlined at build time. The env source is now a parameter, which lets the tests cover the mapping without mutating globals.
This commit is contained in:
parent
25b949bbfa
commit
fc3d82d3c3
4 changed files with 55 additions and 52 deletions
|
|
@ -79,12 +79,15 @@ const THEME_TOKENS = [
|
|||
const toEnvName = (token) => `REACT_APP_THEME_${token.toUpperCase().replace(/-/g, '_')}`;
|
||||
|
||||
/**
|
||||
* Loads theme configuration from environment variables
|
||||
* Loads theme configuration from build-time environment variables. Values are
|
||||
* inlined by Vite, so `REACT_APP_THEME_*` must be present when the client is
|
||||
* built; changing them afterwards has no effect until the next build.
|
||||
* @param {Record<string, string | undefined>} [env] Environment source, defaults to the build-time env
|
||||
* @returns {import('@librechat/client').IThemeRGB | undefined}
|
||||
*/
|
||||
export function getThemeFromEnv() {
|
||||
export function getThemeFromEnv(env = import.meta.env) {
|
||||
const theme = THEME_TOKENS.reduce((acc, token) => {
|
||||
const value = process.env[toEnvName(token)];
|
||||
const value = env[toEnvName(token)];
|
||||
if (value) {
|
||||
acc[`rgb-${token}`] = value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,16 @@
|
|||
import { getThemeFromEnv } from './getThemeFromEnv';
|
||||
|
||||
const originalThemeEnv = Object.fromEntries(
|
||||
Object.entries(process.env).filter(([key]) => key.startsWith('REACT_APP_THEME_')),
|
||||
);
|
||||
|
||||
const clearThemeEnv = () => {
|
||||
Object.keys(process.env)
|
||||
.filter((key) => key.startsWith('REACT_APP_THEME_'))
|
||||
.forEach((key) => delete process.env[key]);
|
||||
};
|
||||
|
||||
beforeEach(clearThemeEnv);
|
||||
|
||||
afterAll(() => {
|
||||
clearThemeEnv();
|
||||
Object.assign(process.env, originalThemeEnv);
|
||||
});
|
||||
|
||||
describe('getThemeFromEnv', () => {
|
||||
it('loads link and accent colors', () => {
|
||||
process.env.REACT_APP_THEME_LINK = '1 2 3';
|
||||
process.env.REACT_APP_THEME_LINK_HOVER = '4 5 6';
|
||||
process.env.REACT_APP_THEME_LINK_VISITED = '7 8 9';
|
||||
process.env.REACT_APP_THEME_ACCENT_PRIMARY = '10 11 12';
|
||||
process.env.REACT_APP_THEME_ACCENT_PRIMARY_HOVER = '13 14 15';
|
||||
|
||||
expect(getThemeFromEnv()).toEqual({
|
||||
expect(
|
||||
getThemeFromEnv({
|
||||
REACT_APP_THEME_LINK: '1 2 3',
|
||||
REACT_APP_THEME_LINK_HOVER: '4 5 6',
|
||||
REACT_APP_THEME_LINK_VISITED: '7 8 9',
|
||||
REACT_APP_THEME_ACCENT_PRIMARY: '10 11 12',
|
||||
REACT_APP_THEME_ACCENT_PRIMARY_HOVER: '13 14 15',
|
||||
}),
|
||||
).toEqual({
|
||||
'rgb-link': '1 2 3',
|
||||
'rgb-link-hover': '4 5 6',
|
||||
'rgb-link-visited': '7 8 9',
|
||||
|
|
@ -35,17 +20,19 @@ describe('getThemeFromEnv', () => {
|
|||
});
|
||||
|
||||
it('loads status, inverted, fixed and destructive colors', () => {
|
||||
process.env.REACT_APP_THEME_STATUS_ERROR = '1 2 3';
|
||||
process.env.REACT_APP_THEME_STATUS_SUCCESS_SUBTLE = '4 5 6';
|
||||
process.env.REACT_APP_THEME_STATUS_NEUTRAL_BORDER = '7 8 9';
|
||||
process.env.REACT_APP_THEME_TEXT_DESTRUCTIVE = '10 11 12';
|
||||
process.env.REACT_APP_THEME_BORDER_DESTRUCTIVE = '13 14 15';
|
||||
process.env.REACT_APP_THEME_SURFACE_INVERTED = '16 17 18';
|
||||
process.env.REACT_APP_THEME_TEXT_INVERTED = '19 20 21';
|
||||
process.env.REACT_APP_THEME_SURFACE_FIXED_HOVER = '22 23 24';
|
||||
process.env.REACT_APP_THEME_TEXT_FIXED = '25 26 27';
|
||||
|
||||
expect(getThemeFromEnv()).toEqual({
|
||||
expect(
|
||||
getThemeFromEnv({
|
||||
REACT_APP_THEME_STATUS_ERROR: '1 2 3',
|
||||
REACT_APP_THEME_STATUS_SUCCESS_SUBTLE: '4 5 6',
|
||||
REACT_APP_THEME_STATUS_NEUTRAL_BORDER: '7 8 9',
|
||||
REACT_APP_THEME_TEXT_DESTRUCTIVE: '10 11 12',
|
||||
REACT_APP_THEME_BORDER_DESTRUCTIVE: '13 14 15',
|
||||
REACT_APP_THEME_SURFACE_INVERTED: '16 17 18',
|
||||
REACT_APP_THEME_TEXT_INVERTED: '19 20 21',
|
||||
REACT_APP_THEME_SURFACE_FIXED_HOVER: '22 23 24',
|
||||
REACT_APP_THEME_TEXT_FIXED: '25 26 27',
|
||||
}),
|
||||
).toEqual({
|
||||
'rgb-status-error': '1 2 3',
|
||||
'rgb-status-success-subtle': '4 5 6',
|
||||
'rgb-status-neutral-border': '7 8 9',
|
||||
|
|
@ -58,7 +45,22 @@ describe('getThemeFromEnv', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('ignores unrelated and empty values', () => {
|
||||
expect(
|
||||
getThemeFromEnv({
|
||||
MODE: 'test',
|
||||
REACT_APP_THEME_TEXT_PRIMARY: '',
|
||||
REACT_APP_THEME_UNKNOWN_TOKEN: '1 2 3',
|
||||
REACT_APP_THEME_SURFACE_PRIMARY: '4 5 6',
|
||||
}),
|
||||
).toEqual({ 'rgb-surface-primary': '4 5 6' });
|
||||
});
|
||||
|
||||
it('returns undefined when no theme variables are set', () => {
|
||||
expect(getThemeFromEnv({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it('reads the build-time environment when no source is given', () => {
|
||||
expect(getThemeFromEnv()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export default defineConfig(({ command }) => ({
|
|||
},
|
||||
// Set the directory where environment variables are loaded from and restrict prefixes
|
||||
envDir: '../',
|
||||
envPrefix: ['VITE_', 'SCRIPT_', 'DOMAIN_', 'ALLOW_'],
|
||||
envPrefix: ['VITE_', 'SCRIPT_', 'DOMAIN_', 'ALLOW_', 'REACT_APP_THEME_'],
|
||||
plugins: [
|
||||
react(),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -244,24 +244,22 @@ Every `IThemeRGB` key is configurable this way: drop the `rgb-` prefix and
|
|||
upper-snake-case the rest, so `rgb-status-error-border` becomes
|
||||
`REACT_APP_THEME_STATUS_ERROR_BORDER`.
|
||||
|
||||
The prefix must be listed in the bundler's `envPrefix` (Vite) or equivalent, and
|
||||
values are inlined at build time, so the client has to be rebuilt after changing
|
||||
them.
|
||||
|
||||
### 2. Create a Theme Loader
|
||||
|
||||
```tsx
|
||||
function getThemeFromEnv(): IThemeRGB | undefined {
|
||||
// Check if any theme environment variables are set
|
||||
const hasThemeEnvVars = Object.keys(process.env).some((key) =>
|
||||
key.startsWith('REACT_APP_THEME_'),
|
||||
);
|
||||
|
||||
if (!hasThemeEnvVars) {
|
||||
return undefined; // Use default themes
|
||||
}
|
||||
|
||||
return {
|
||||
'rgb-text-primary': process.env.REACT_APP_THEME_TEXT_PRIMARY || '33 33 33',
|
||||
'rgb-brand-purple': process.env.REACT_APP_THEME_BRAND_PURPLE || '171 104 255',
|
||||
function getThemeFromEnv(env = import.meta.env): IThemeRGB | undefined {
|
||||
const theme = {
|
||||
'rgb-text-primary': env.REACT_APP_THEME_TEXT_PRIMARY,
|
||||
'rgb-brand-purple': env.REACT_APP_THEME_BRAND_PURPLE,
|
||||
// ... other colors
|
||||
};
|
||||
|
||||
const set = Object.fromEntries(Object.entries(theme).filter(([, value]) => value));
|
||||
return Object.keys(set).length > 0 ? set : undefined; // Fall back to default themes
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue