diff --git a/.env.example b/.env.example index 0ecd089..a9ddf3c 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,4 @@ VITE_API_BASE_URL=http://127.0.0.1:4242 VITE_KEYCLOAK_URL=http://localhost:8080/ VITE_KEYCLOAK_REALM=dev VITE_KEYCLOAK_CLIENT_ID=bff-dashboard +VITE_API_MOCKING_ENABLED=false \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 68ec519..78b8322 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,13 @@ RUN npm run build -- --mode=${VITE_MODE} FROM nginx:1.27.5-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10 +RUN apk add --no-cache jq + COPY --from=builder /app/dist /usr/share/nginx/html +COPY docker/entrypoint.d/ /docker-entrypoint.d/ +RUN chmod +x /docker-entrypoint.d/*.sh + COPY docker/nginx/snippets/proxy-params.conf /etc/nginx/snippets/proxy-params.conf # each time nginx is started it will perform variable substition in all template # files found in `/etc/nginx/templates/*.template`, and copy the results (without diff --git a/docker-compose.yml b/docker-compose.yml index ca25048..3c7d97f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: - VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL} - VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM} - VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID} + - VITE_API_MOCKING_ENABLED=${VITE_API_MOCKING_ENABLED} + - VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING=${VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING} healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80"] interval: 30s diff --git a/docker/entrypoint.d/00-env-config.sh b/docker/entrypoint.d/00-env-config.sh new file mode 100644 index 0000000..1fda8df --- /dev/null +++ b/docker/entrypoint.d/00-env-config.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh +set -e + +JSON_ENV=$(jq -n \ + --arg VITE_API_BASE_URL "$VITE_API_BASE_URL" \ + --arg VITE_API_MOCKING_ENABLED "$VITE_API_MOCKING_ENABLED" \ + --arg VITE_KEYCLOAK_URL "$VITE_KEYCLOAK_URL" \ + --arg VITE_KEYCLOAK_REALM "$VITE_KEYCLOAK_REALM" \ + --arg VITE_KEYCLOAK_CLIENT_ID "$VITE_KEYCLOAK_CLIENT_ID" \ + --arg VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING "$VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING" \ + '{VITE_API_BASE_URL: $VITE_API_BASE_URL, VITE_API_MOCKING_ENABLED: $VITE_API_MOCKING_ENABLED, VITE_KEYCLOAK_URL: $VITE_KEYCLOAK_URL, VITE_KEYCLOAK_REALM: $VITE_KEYCLOAK_REALM, VITE_KEYCLOAK_CLIENT_ID: $VITE_KEYCLOAK_CLIENT_ID, VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING: $VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING}') + +cat < /usr/share/nginx/html/config.js +window.__ENV__ = ${JSON_ENV}; +EOF diff --git a/docker/nginx/templates/default.conf.template b/docker/nginx/templates/default.conf.template index 9b20461..c0c3b86 100644 --- a/docker/nginx/templates/default.conf.template +++ b/docker/nginx/templates/default.conf.template @@ -49,6 +49,10 @@ server { add_header Cache-Control "no-store"; } + location = /config.js { + add_header Cache-Control "no-store"; + } + location ~* \.(?:json)$ { expires 1d; add_header Cache-Control "public"; diff --git a/index.html b/index.html index 3dc45c5..c5f713b 100644 --- a/index.html +++ b/index.html @@ -10,6 +10,7 @@
+ diff --git a/public/config.js b/public/config.js new file mode 100644 index 0000000..68a8529 --- /dev/null +++ b/public/config.js @@ -0,0 +1 @@ +window.__ENV__ = window.__ENV__ || {} diff --git a/src/constants/meta.ts b/src/constants/meta.ts index 354ac2b..647852c 100644 --- a/src/constants/meta.ts +++ b/src/constants/meta.ts @@ -1,6 +1,8 @@ +import { env } from "@/lib/env" + export default { - devModeEnabled: import.meta.env.DEV, - apiBaseUrl: import.meta.env.VITE_API_BASE_URL as string, - apiMocksEnabled: import.meta.env.VITE_API_MOCKING_ENABLED === "true", - crowdinInContextToolingEnabled: import.meta.env.VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING === "true", + devModeEnabled: env.devModeEnabled, + apiBaseUrl: env.apiBaseUrl, + apiMocksEnabled: env.apiMocksEnabled, + crowdinInContextToolingEnabled: env.crowdinInContextToolingEnabled, } diff --git a/src/keycloak.tsx b/src/keycloak.tsx index c106bd8..558b4d6 100644 --- a/src/keycloak.tsx +++ b/src/keycloak.tsx @@ -1,9 +1,10 @@ import Keycloak from "keycloak-js" +import { env } from "@/lib/env" const keycloak = new Keycloak({ - url: import.meta.env.VITE_KEYCLOAK_URL as string, - realm: import.meta.env.VITE_KEYCLOAK_REALM as string, - clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID as string, + url: env.keycloakUrl, + realm: env.keycloakRealm, + clientId: env.keycloakClientId, }) export const initKeycloak = async (): Promise => { diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index e3464d4..58fb3d9 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -1,9 +1,10 @@ import { client as heyApiClient } from "@/generated/client/client.gen" import * as sdk from "@/generated/client/sdk.gen" +import { env } from "@/lib/env" import keycloak from "../keycloak" heyApiClient.setConfig({ - baseUrl: import.meta.env.VITE_API_BASE_URL as string, + baseUrl: env.apiBaseUrl, }) // Add the auth token interceptor diff --git a/src/lib/env.test.ts b/src/lib/env.test.ts new file mode 100644 index 0000000..9df5ab1 --- /dev/null +++ b/src/lib/env.test.ts @@ -0,0 +1,86 @@ +import { afterEach, describe, expect, it, vi } from "vitest" + +const loadEnv = async () => { + const module = await import("./env") + return module.env +} + +afterEach(() => { + vi.resetModules() + vi.unstubAllGlobals() + vi.unstubAllEnvs() +}) + +describe("env runtime resolution", () => { + it("prefers runtime env values when provided", async () => { + vi.stubEnv("VITE_API_BASE_URL", "https://fallback.example.com") + vi.stubEnv("VITE_KEYCLOAK_URL", "https://fallback-keycloak.example.com") + vi.stubEnv("VITE_KEYCLOAK_REALM", "fallback-realm") + vi.stubEnv("VITE_KEYCLOAK_CLIENT_ID", "fallback-client") + + vi.stubGlobal("window", { + __ENV__: { + VITE_API_BASE_URL: "https://runtime.example.com", + VITE_API_MOCKING_ENABLED: "true", + VITE_KEYCLOAK_URL: "https://runtime-keycloak.example.com", + VITE_KEYCLOAK_REALM: "runtime-realm", + VITE_KEYCLOAK_CLIENT_ID: "runtime-client", + VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING: "true", + }, + }) + + const env = await loadEnv() + + expect(env.apiBaseUrl).toBe("https://runtime.example.com") + expect(env.apiMocksEnabled).toBe(true) + expect(env.keycloakUrl).toBe("https://runtime-keycloak.example.com") + expect(env.keycloakRealm).toBe("runtime-realm") + expect(env.keycloakClientId).toBe("runtime-client") + expect(env.crowdinInContextToolingEnabled).toBe(true) + }) + + it("falls back to build-time env when runtime values are empty", async () => { + vi.stubEnv("VITE_API_BASE_URL", "https://fallback.example.com") + vi.stubEnv("VITE_API_MOCKING_ENABLED", "true") + vi.stubEnv("VITE_KEYCLOAK_URL", "https://fallback-keycloak.example.com") + vi.stubEnv("VITE_KEYCLOAK_REALM", "fallback-realm") + vi.stubEnv("VITE_KEYCLOAK_CLIENT_ID", "fallback-client") + vi.stubEnv("VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING", "false") + + vi.stubGlobal("window", { + __ENV__: { + VITE_API_BASE_URL: "", + VITE_API_MOCKING_ENABLED: "", + VITE_KEYCLOAK_URL: "", + VITE_KEYCLOAK_REALM: "", + VITE_KEYCLOAK_CLIENT_ID: "", + VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING: "", + }, + }) + + const env = await loadEnv() + + expect(env.apiBaseUrl).toBe("https://fallback.example.com") + expect(env.apiMocksEnabled).toBe(true) + expect(env.keycloakUrl).toBe("https://fallback-keycloak.example.com") + expect(env.keycloakRealm).toBe("fallback-realm") + expect(env.keycloakClientId).toBe("fallback-client") + expect(env.crowdinInContextToolingEnabled).toBe(false) + }) + + it("handles SSR where window is undefined", async () => { + vi.stubEnv("VITE_API_BASE_URL", "https://fallback.example.com") + vi.stubEnv("VITE_KEYCLOAK_URL", "https://fallback-keycloak.example.com") + vi.stubEnv("VITE_KEYCLOAK_REALM", "fallback-realm") + vi.stubEnv("VITE_KEYCLOAK_CLIENT_ID", "fallback-client") + + vi.stubGlobal("window", undefined) + + const env = await loadEnv() + + expect(env.apiBaseUrl).toBe("https://fallback.example.com") + expect(env.keycloakUrl).toBe("https://fallback-keycloak.example.com") + expect(env.keycloakRealm).toBe("fallback-realm") + expect(env.keycloakClientId).toBe("fallback-client") + }) +}) diff --git a/src/lib/env.ts b/src/lib/env.ts new file mode 100644 index 0000000..9f5d5b4 --- /dev/null +++ b/src/lib/env.ts @@ -0,0 +1,34 @@ +type RuntimeEnv = Partial<{ + VITE_API_BASE_URL: string + VITE_API_MOCKING_ENABLED: string + VITE_KEYCLOAK_URL: string + VITE_KEYCLOAK_REALM: string + VITE_KEYCLOAK_CLIENT_ID: string + VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING: string +}> + +const runtimeEnv: RuntimeEnv = + typeof window !== "undefined" ? (window as { __ENV__?: RuntimeEnv }).__ENV__ ?? {} : {} + +const fallbackEnv = import.meta.env as ImportMetaEnv & RuntimeEnv + +const getEnvValue = (key: K): RuntimeEnv[K] | undefined => { + const value = runtimeEnv[key] + + if (value !== undefined && value !== null && value !== "") { + return value + } + + return fallbackEnv[key] +} + +export const env = { + devModeEnabled: fallbackEnv.DEV, + apiBaseUrl: getEnvValue("VITE_API_BASE_URL")!, + apiMocksEnabled: (getEnvValue("VITE_API_MOCKING_ENABLED") ?? "false") === "true", + keycloakUrl: getEnvValue("VITE_KEYCLOAK_URL")!, + keycloakRealm: getEnvValue("VITE_KEYCLOAK_REALM")!, + keycloakClientId: getEnvValue("VITE_KEYCLOAK_CLIENT_ID")!, + crowdinInContextToolingEnabled: + (getEnvValue("VITE_BITCR_DEV_INCLUDE_CROWDIN_IN_CONTEXT_TOOLING") ?? "false") === "true", +} diff --git a/src/main.tsx b/src/main.tsx index c047c76..cf47978 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -58,3 +58,5 @@ void prepare().then(() => { , ) }) + +export { App }