Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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}
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 }