Inject MAP_TILE_URL into window.__ENV__ via html.tags + Docker build-arg
http://flights-ui.devwebzavod.ru/ru/flights-map was still hitting the same-origin tile path after adding the k8s env: Modern.js renders the <Suspense> fallback on the server (i18n isn't preloaded), so the route component that reads getEnv() never actually runs during SSR. The page hydrates client-side, where process.env is Rspack's empty stub and MAP_TILE_URL is never set — getEnv() falls back to the default. Move the value into window.__ENV__ instead: - modern.config.ts: inline a <script> into html.tags that sets window.__ENV__ = { MAP_TILE_URL: <value> } at SSR-server startup. The snippet is authored once at server boot, so the HTML template baked into dist/standalone/html/main/index.html always carries the pod's tile URL. - src/env/index.ts: merge window.__ENV__ on top of process.env so the browser prefers the injected value (process.env only has NODE_ENV after Rspack's polyfill). - Dockerfile.react: accept MAP_TILE_URL as a build ARG and expose it as ENV before `pnpm build:standalone`, so Modern.js picks it up when building the HTML template. k8s env still flows into the Node SSR process; the build-arg path guarantees correctness even when the runtime env is stripped. - deployment/build-docker.sh: forward MAP_TILE_URL through as a build-arg (default keeps the same-origin path). CI on the devwebzavod cluster can export MAP_TILE_URL=https://flights.test.aeroflot.ru/map/api/tile/{z}/{x}/{y}.jpeg before running build-docker.sh and the resulting image will serve tiles from the upstream the real Aeroflot ingress terminates.
This commit is contained in:
@@ -20,6 +20,13 @@ COPY src/ src/
|
||||
# broken backgrounds, missing tile icons).
|
||||
COPY config/ config/
|
||||
|
||||
# Runtime-tunable env vars. modern.config.ts reads these when Modern.js
|
||||
# boots the SSR server at container start, so values set via k8s env
|
||||
# propagate without a rebuild. The ARG line lets CI also bake a value
|
||||
# into the image if the cluster can't set env at runtime.
|
||||
ARG MAP_TILE_URL
|
||||
ENV MAP_TILE_URL=${MAP_TILE_URL}
|
||||
|
||||
RUN pnpm build:standalone
|
||||
|
||||
FROM node:24-slim AS runtime
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo -e "\E[1;34mCleanup docker build cache to reduce disk usage\E[1;0m"
|
||||
docker image prune -f
|
||||
docker container prune -f
|
||||
|
||||
function build {
|
||||
K8NAMESPACE="flights-ui"
|
||||
echo -e "\E[1;34mBuilding $2 ($1)\E[1;0m"
|
||||
export CopyRetryCount=100
|
||||
docker rmi -f $(docker images aeroflot.$K8NAMESPACE/$1 -a -q|uniq) >/dev/null 2>/dev/null
|
||||
ls -l "$2/Dockerfile"
|
||||
# MAP_TILE_URL must be a build-arg (not just a runtime env) because
|
||||
# Modern.js bakes html.tags into the HTML template at `pnpm build:standalone`
|
||||
# time. Setting it only on the pod won't change the served HTML.
|
||||
# Defaults here keep the prod-cluster behavior (same-origin) intact;
|
||||
# devwebzavod builds override with the flights.test.aeroflot.ru URL.
|
||||
: "${MAP_TILE_URL:=/map/api/tile/{z}/{x}/{y}.jpeg}"
|
||||
export MAP_TILE_URL
|
||||
docker build -t aeroflot.$K8NAMESPACE/$1:v$VERSION.$BUILD_NUMBER \
|
||||
--build-arg ENVIRONMENT=${ENVIRONMENT} \
|
||||
--build-arg=NPM_PROXY \
|
||||
--build-arg MAP_TILE_URL=${MAP_TILE_URL} \
|
||||
-f "$2/Dockerfile" $2
|
||||
}
|
||||
|
||||
build flights-ui "../Aeroflot.Flights.Front"
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
export K8NAMESPACE="flights-ui"
|
||||
|
||||
echo "Create namespace [$K8NAMESPACE] if doesn't exist"
|
||||
kubectl --kubeconfig $KUBECONFIG get namespace | grep -q "^$K8NAMESPACE " || kubectl --kubeconfig $KUBECONFIG create namespace $K8NAMESPACE
|
||||
|
||||
function deployImage {
|
||||
export AFL_PRJ_NAME=$1
|
||||
echo -e "\E[1;34mDeploy $AFL_PRJ_NAME\E[1;0m"
|
||||
|
||||
if [ "$IMAGE_PULL_SECRET" != "" ]
|
||||
then
|
||||
export IMAGE_PULL_SECRETS=$' imagePullSecrets:\n - name: '$IMAGE_PULL_SECRET$'\n'
|
||||
else
|
||||
export IMAGE_PULL_SECRETS=""
|
||||
fi
|
||||
|
||||
echo "Using k8s yml file as following:"
|
||||
envsubst < ./k8s/$AFL_PRJ_NAME.yaml
|
||||
|
||||
echo "Start kubectl apply..."
|
||||
envsubst < ./k8s/$AFL_PRJ_NAME.yaml | kubectl --kubeconfig $KUBECONFIG apply -n $K8NAMESPACE -f -
|
||||
|
||||
echo -e "\E[1;34m ...done\E[1;0m"
|
||||
}
|
||||
|
||||
function serve {
|
||||
NAME=$1
|
||||
PORT=$2
|
||||
echo -e "\E[1;34mService $1 at port $2\E[1;0m"
|
||||
|
||||
cat <<EOF | kubectl --kubeconfig $KUBECONFIG apply -n $K8NAMESPACE -f /dev/stdin
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: $NAME
|
||||
namespace: $K8NAMESPACE
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
name: $NAME
|
||||
type: ClusterIP
|
||||
EOF
|
||||
}
|
||||
|
||||
function serveLB {
|
||||
NAME=$1
|
||||
PORT=$2
|
||||
echo -e "\E[1;34mService $1 at port $2\E[1;0m"
|
||||
|
||||
cat <<EOF | kubectl --kubeconfig $KUBECONFIG apply -n $K8NAMESPACE -f /dev/stdin
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: $NAME
|
||||
namespace: $K8NAMESPACE
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
name: $NAME
|
||||
type: LoadBalancer
|
||||
EOF
|
||||
}
|
||||
|
||||
deployImage flights-ui
|
||||
serveLB flights-ui
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo -e "\E[1;34mCleanup docker build cache to reduce disk usage\E[1;0m"
|
||||
docker image prune -f
|
||||
docker container prune -f
|
||||
docker buildx prune --filter type=regular -f -a
|
||||
docker buildx prune --filter type=source.local -f -a
|
||||
|
||||
function pushImage {
|
||||
K8NAMESPACE="flights-ui"
|
||||
aspnetcore=${ASPNETCORE_ENVIRONMENT,,}
|
||||
|
||||
echo -e "\E[1;34mPushing $1\E[1;0m"
|
||||
docker image tag aeroflot.$K8NAMESPACE/$1:v$VERSION.$BUILD_NUMBER $REGISTRY_SERVER/aeroflot.$K8NAMESPACE.$ENVIRONMENT/$1:v$VERSION.$BUILD_NUMBER
|
||||
docker push $REGISTRY_SERVER/aeroflot.$K8NAMESPACE.$ENVIRONMENT/$1:v$VERSION.$BUILD_NUMBER
|
||||
docker rmi $REGISTRY_SERVER/aeroflot.$K8NAMESPACE.$ENVIRONMENT/$1:v$VERSION.$BUILD_NUMBER
|
||||
}
|
||||
|
||||
echo -e "\E[1;34mStarting pushImage stage\E[1;0m"
|
||||
|
||||
pushImage flights-ui
|
||||
@@ -4,6 +4,21 @@ import { moduleFederationPlugin } from "@module-federation/modern-js";
|
||||
const buildTarget = process.env["BUILD_TARGET"];
|
||||
const isRemote = buildTarget === "remote";
|
||||
|
||||
// Runtime env values that must reach the client bundle. Rspack resolves
|
||||
// `process.env` at BUILD time to an empty-ish polyfill in the browser, so
|
||||
// any value we want per-deployment (e.g. MAP_TILE_URL pointing at a tile
|
||||
// service that the local ingress doesn't proxy) has to be injected into
|
||||
// the HTML as a window global. getEnv() picks up window.__ENV__ on the
|
||||
// client. Evaluated once at Modern.js server start → pod env → HTML.
|
||||
const PUBLIC_ENV_KEYS = ["MAP_TILE_URL"] as const;
|
||||
const PUBLIC_ENV: Record<string, string> = {};
|
||||
for (const k of PUBLIC_ENV_KEYS) {
|
||||
const v = process.env[k];
|
||||
if (typeof v === "string" && v.length > 0) PUBLIC_ENV[k] = v;
|
||||
}
|
||||
const PUBLIC_ENV_SCRIPT =
|
||||
`window.__ENV__ = Object.assign(window.__ENV__ || {}, ${JSON.stringify(PUBLIC_ENV)});`;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [appTools({ bundler: "rspack" }), moduleFederationPlugin()],
|
||||
source: {
|
||||
@@ -12,6 +27,14 @@ export default defineConfig({
|
||||
html: {
|
||||
favicon: "./config/public/favicon.ico",
|
||||
tags: [
|
||||
// Inline script runs before the main bundle so client-side getEnv()
|
||||
// sees the pod's env (read when the SSR server starts).
|
||||
{
|
||||
tag: "script",
|
||||
head: true,
|
||||
append: false,
|
||||
children: PUBLIC_ENV_SCRIPT,
|
||||
},
|
||||
{
|
||||
tag: "link",
|
||||
attrs: {
|
||||
|
||||
Vendored
+17
-9
@@ -59,15 +59,23 @@ let cached: Env | undefined;
|
||||
export function getEnv(): Env {
|
||||
if (cached) return cached;
|
||||
|
||||
// In the browser, process.env is not available (Rspack doesn't replace
|
||||
// bracket-notation access). Fall back to an SSR-injected window.__ENV__
|
||||
// or sensible development defaults.
|
||||
const envSource =
|
||||
typeof process !== "undefined" && process.env
|
||||
? process.env
|
||||
: typeof window !== "undefined" && (window as unknown as Record<string, unknown>)["__ENV__"]
|
||||
? (window as unknown as Record<string, unknown>)["__ENV__"] as Record<string, string>
|
||||
: {};
|
||||
// In Node (SSR build) process.env is authoritative. In the browser,
|
||||
// Rspack injects a stub `process.env` that only carries NODE_ENV, so we
|
||||
// can't rely on it for per-deployment values — prefer `window.__ENV__`
|
||||
// (populated at SSR server start by modern.config.ts) and fall through
|
||||
// to the stub only for NODE_ENV / defaults.
|
||||
const isBrowser = typeof window !== "undefined";
|
||||
const windowEnv = isBrowser
|
||||
? ((window as unknown as Record<string, unknown>)["__ENV__"] as
|
||||
| Record<string, string>
|
||||
| undefined)
|
||||
: undefined;
|
||||
const processEnv =
|
||||
typeof process !== "undefined" && process.env ? process.env : undefined;
|
||||
const envSource: Record<string, string | undefined> = {
|
||||
...(processEnv ?? {}),
|
||||
...(windowEnv ?? {}),
|
||||
};
|
||||
|
||||
const parsed = EnvSchema.safeParse(envSource);
|
||||
if (!parsed.success) {
|
||||
|
||||
Reference in New Issue
Block a user