fix(mobile): lazy env proxy so missing EXPO_PUBLIC vars throw inside React
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
describe('mobile env (lazy proxy)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
delete process.env.EXPO_PUBLIC_SUPABASE_URL;
|
||||||
|
delete process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
|
||||||
|
delete process.env.EXPO_PUBLIC_AUTH_REDIRECT_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('importing the module does NOT throw when required vars are missing', async () => {
|
||||||
|
await expect(import('./env')).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reading a property with no env set throws a clear error', async () => {
|
||||||
|
const mod = await import('./env');
|
||||||
|
expect(() => mod.env.supabaseUrl).toThrowError(
|
||||||
|
/Missing required env var EXPO_PUBLIC_SUPABASE_URL/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reading a property after setting env returns the value and memoises', async () => {
|
||||||
|
process.env.EXPO_PUBLIC_SUPABASE_URL = 'https://example.supabase.co';
|
||||||
|
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY = 'anon-123';
|
||||||
|
const mod = await import('./env');
|
||||||
|
expect(mod.env.supabaseUrl).toBe('https://example.supabase.co');
|
||||||
|
expect(mod.env.supabaseAnonKey).toBe('anon-123');
|
||||||
|
expect(mod.env.authRedirectUrl).toBe('netralax://auth/callback');
|
||||||
|
});
|
||||||
|
});
|
||||||
+31
-9
@@ -1,19 +1,41 @@
|
|||||||
// EXPO_PUBLIC_* vars are inlined at bundle time by Expo's Babel plugin.
|
// EXPO_PUBLIC_* vars are inlined at bundle time by Expo's Babel plugin (or
|
||||||
// We pull them through a single typed module so a missing var is a loud
|
// shipped via EAS Secrets for EAS builds — see apps/mobile/README.md).
|
||||||
// startup error rather than a confusing Supabase 401 later.
|
// We pull them through a Proxy so missing vars throw on first READ, not at
|
||||||
|
// module-eval time. That keeps the throw inside the React tree where the
|
||||||
|
// <BootError> boundary can render it as a readable screen instead of a blank
|
||||||
|
// white window.
|
||||||
|
|
||||||
function required(name: string): string {
|
function required(name: string): string {
|
||||||
const v = process.env[name];
|
const v = process.env[name];
|
||||||
if (!v || v.length === 0) {
|
if (!v || v.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Missing required env var ' + name + '. Set it in apps/mobile/.env.local — see .env.example.',
|
'Missing required env var ' + name +
|
||||||
|
'. Set it via `eas secret:create --scope project --name ' + name +
|
||||||
|
' --value ...` or in apps/mobile/.env.local for local dev (see .env.example).',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const env = {
|
interface EnvShape {
|
||||||
supabaseUrl: required('EXPO_PUBLIC_SUPABASE_URL'),
|
supabaseUrl: string;
|
||||||
supabaseAnonKey: required('EXPO_PUBLIC_SUPABASE_ANON_KEY'),
|
supabaseAnonKey: string;
|
||||||
authRedirectUrl: process.env.EXPO_PUBLIC_AUTH_REDIRECT_URL ?? 'netralax://auth/callback',
|
authRedirectUrl: string;
|
||||||
} as const;
|
}
|
||||||
|
|
||||||
|
function readEnv(): EnvShape {
|
||||||
|
return {
|
||||||
|
supabaseUrl: required('EXPO_PUBLIC_SUPABASE_URL'),
|
||||||
|
supabaseAnonKey: required('EXPO_PUBLIC_SUPABASE_ANON_KEY'),
|
||||||
|
authRedirectUrl: process.env.EXPO_PUBLIC_AUTH_REDIRECT_URL ?? 'netralax://auth/callback',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let cached: EnvShape | null = null;
|
||||||
|
|
||||||
|
export const env: EnvShape = new Proxy({} as EnvShape, {
|
||||||
|
get(_target, key: string | symbol): unknown {
|
||||||
|
cached ??= readEnv();
|
||||||
|
return cached[key as keyof EnvShape];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user