Files
flights_web/modern.config.ts
T

205 lines
6.4 KiB
TypeScript

import { appTools, defineConfig } from "@modern-js/app-tools";
import { moduleFederationPlugin } from "@module-federation/modern-js";
const buildTarget = process.env["BUILD_TARGET"];
const isRemote = buildTarget === "remote";
const plugins = [
appTools({ bundler: "rspack" }),
...(isRemote ? [moduleFederationPlugin()] : []),
];
// 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 build time → pod env → HTML.
//
// IMPORTANT: Rspack's HTML plugin preprocesses <script> children through
// a template engine that eats '{token}' pairs AND decodes \u007B / \u007D
// Unicode escapes before running the engine, so neither raw braces nor
// escaped braces survive. The Leaflet tile template literally contains
// '{z}/{x}/{y}', so we serialize the whole payload to base64 and decode
// it in the browser at runtime. Base64 output is A-Z/a-z/0-9/+/=/ — no
// braces for the template engine to grab.
const PUBLIC_ENV_KEYS = [
"MAP_TILE_URL",
"API_BASE_URL",
"SIGNALR_HUB_URL",
"REFRESH_PAUSE_MIN",
"REFRESH_STOP_MIN",
] 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 publicEnvB64 = Buffer.from(
JSON.stringify(PUBLIC_ENV),
"utf8",
).toString("base64");
// Pure-expression form — NO '{' or '}' anywhere in the source. The
// base64 payload round-trips `{…}` without exposing braces to the
// Rspack HTML plugin. `Object.create(null)` replaces an empty `{}`.
const PUBLIC_ENV_SCRIPT =
`window.__ENV__=Object.assign(window.__ENV__||Object.create(null),JSON.parse(atob("${publicEnvB64}")));`;
const SUPPRESS_AEROFLOT_LOADER_WARNINGS_SCRIPT = `
window.__AFL_SHELL_CONSOLE_WARN__=window.__AFL_SHELL_CONSOLE_WARN__||console.warn.bind(console);
console.warn=function(){
var first=arguments[0]==null?"":String(arguments[0]);
if(first.indexOf("Cannot find module './afl-frontend-lib/locales/")!==-1){return;}
return window.__AFL_SHELL_CONSOLE_WARN__.apply(console,arguments);
};`;
const modernCommand = process.argv.join(" ");
const npmLifecycleEvent = process.env["npm_lifecycle_event"] ?? "";
const isDevServer =
/\bdev\b/.test(modernCommand) || npmLifecycleEvent.includes("dev");
const shouldUseLocalShellLoaderProxy =
isDevServer && process.env["AEROFLOT_SHELL_LOADER_PROXY"] === "1";
// Angular uses full `afl-component` loader assets in production
// `index.html`, but its local `index.dev.html` keeps only shell
// placeholders. `dev:full` exposes a same-origin static proxy so local
// Chrome can hydrate the placeholders without CORS failures.
const shouldLoadStandaloneShellLoader =
!isRemote && (!isDevServer || shouldUseLocalShellLoaderProxy);
const standaloneShellLoaderMode = shouldLoadStandaloneShellLoader
? shouldUseLocalShellLoaderProxy
? "proxy"
: "external"
: "placeholder";
const aeroflotStaticBase = shouldUseLocalShellLoaderProxy
? "/frontend/static"
: "https://www.aeroflot.ru/frontend/static";
const standaloneShellTags = isRemote
? []
: [
...(shouldLoadStandaloneShellLoader
? [
...(shouldUseLocalShellLoaderProxy
? [
{
tag: "script",
head: true,
append: true,
children: SUPPRESS_AEROFLOT_LOADER_WARNINGS_SCRIPT,
},
]
: []),
{
tag: "link",
head: true,
append: true,
attrs: {
rel: "stylesheet",
href: `${aeroflotStaticBase}/css/afl-frontend-loader.bundle.css`,
},
},
{
tag: "script",
head: false,
append: true,
attrs: {
async: true,
src: `${aeroflotStaticBase}/js/afl-frontend-loader.bundle.js`,
},
},
]
: []),
{
tag: "meta",
head: true,
append: true,
attrs: {
name: "aeroflot-shell-loader",
content: standaloneShellLoaderMode,
},
},
];
export default defineConfig({
plugins,
source: {
entriesDir: "./src",
},
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,
},
...standaloneShellTags,
{
tag: "link",
attrs: {
rel: "icon",
type: "image/png",
sizes: "32x32",
href: "/assets/img/favicon-32x32.png",
},
},
{
tag: "link",
attrs: {
rel: "icon",
type: "image/png",
sizes: "16x16",
href: "/assets/img/favicon-16x16.png",
},
},
{
tag: "link",
attrs: {
rel: "apple-touch-icon",
href: "/assets/img/favicon-touch.png",
},
},
],
},
runtime: {
router: {
future: {
v7_startTransition: true,
v7_relativeSplatPath: true,
v7_fetcherPersist: true,
v7_normalizeFormMethod: true,
v7_partialHydration: true,
v7_skipActionErrorRevalidation: true,
},
},
},
server: {
ssr: {
mode: "stream",
},
},
tools: {
rspack(config) {
config.cache = false;
},
cssLoader: {
url: false,
},
// Explicit dev-server CORS headers. Silences the MF modern-js warning
// about empty devServer.headers (auto-assigning "*"). Production is
// unaffected — the real reverse proxy manages CORS.
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"Content-Type, Authorization, Accept, Accept-Language",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
},
},
},
output: {
distPath: { root: isRemote ? "dist/remote" : "dist/standalone" },
},
});