Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions docker/entrypoint.d/00-env-config.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF > /usr/share/nginx/html/config.js
window.__ENV__ = ${JSON_ENV};
EOF
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated config.js file is placed in /usr/share/nginx/html/config.js, but there's no verification that the file was successfully created or that the jq command succeeded. Consider adding error handling to ensure the config.js file is generated correctly. For example:

if [ ! -f /usr/share/nginx/html/config.js ]; then
  echo "Error: Failed to generate config.js"
  exit 1
fi
Suggested change
EOF
EOF
if [ ! -f /usr/share/nginx/html/config.js ]; then
echo "Error: Failed to generate config.js"
exit 1
fi

Copilot uses AI. Check for mistakes.
4 changes: 4 additions & 0 deletions docker/nginx/templates/default.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</head>
<body>
<div id="root"></div>
<script src="/config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
1 change: 1 addition & 0 deletions public/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window.__ENV__ = window.__ENV__ || {}
10 changes: 6 additions & 4 deletions src/constants/meta.ts
Original file line number Diff line number Diff line change
@@ -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,
}
7 changes: 4 additions & 3 deletions src/keycloak.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean> => {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/api-client.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
86 changes: 86 additions & 0 deletions src/lib/env.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
34 changes: 34 additions & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -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 = <K extends keyof RuntimeEnv>(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",
}
2 changes: 2 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ void prepare().then(() => {
</StrictMode>,
)
})

export { App }