import react from "@vitejs/plugin-react" import dotenv from "dotenv" import { readFileSync } from "fs" import { resolve } from "path" import { defineConfig, type Plugin } from "vite" import mkcert from "vite-plugin-mkcert" dotenv.config({ path: resolve(__dirname, ".env") }) const packageJSON = JSON.parse(readFileSync(resolve(__dirname, "package.json"), "utf8")) const getBuildNumber = () => { const d = new Date() const pad = (n: number) => (n < 10 ? `0${n}` : `${n}`) return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.${process.env.GITHUB_BUILD_NUMBER ?? "dev"}` } const getRestBaseUrl = () => { const env = process.env.CLOUD_ENV const prefix = env === "production" ? "" : env === "staging" ? "staging." : env === "QA" ? "qa." : process.env.REST_BASE_API_DEV_SERVER_PREFIX ? `${process.env.REST_BASE_API_DEV_SERVER_PREFIX}.` : "dev." return `https://api.${prefix}jmapcloud.io` } const getHtmlInputPath = () => resolve(__dirname, process.env.BUILD_TARGET === "cloud" ? "build/index.html" : "index.html") const JMAP_OPTIONS_PLACEHOLDER = "/* __JMAP_OPTIONS_PLACEHOLDER__ */" function jmapOptionsPlugin(): Plugin { return { name: "jmap-options", transformIndexHtml(html: string) { const replacement = `window.JMAP_OPTIONS = { restBaseUrl: "${getRestBaseUrl()}", map: {}, application: {}, hideMainLayout: true }` return html.replace(JMAP_OPTIONS_PLACEHOLDER, replacement) } } } function inlineCssInLegacyBuildPlugin(isLegacyProductionBuild: boolean): Plugin { return { name: "inline-css-in-legacy-build", apply: "build", enforce: "post", generateBundle(_outputOptions, bundle) { if (!isLegacyProductionBuild) { return } const cssAssets = Object.entries(bundle).flatMap(([fileName, output]) => { if (output.type !== "asset" || !output.fileName.endsWith(".css")) { return [] } const source = output.source const cssText = typeof source === "string" ? source : Buffer.from(source).toString("utf8") return [{ fileName, cssText }] }) const combinedCss = cssAssets.map(css => css.cssText).join("\n") for (const output of Object.values(bundle)) { if (output.type !== "chunk" || !output.isEntry) { continue } if (!combinedCss) { continue } const packageStyleNamespace = packageJSON.name.replace(/[^a-zA-Z0-9_-]/g, "-") const styleId = `vite-inline-css-${packageStyleNamespace}-${output.fileName.replace(/[^a-zA-Z0-9_-]/g, "-")}` const injectionCode = `(function(){var id=${JSON.stringify(styleId)};if(document.getElementById(id)){return;}var s=document.createElement("style");s.id=id;s.appendChild(document.createTextNode(${JSON.stringify(combinedCss)}));document.head.appendChild(s);}());` output.code = `${injectionCode}\n${output.code}` } for (const css of cssAssets) { const cssFileName = css.fileName delete bundle[cssFileName] } for (const output of Object.values(bundle)) { if (output.type !== "asset" || !output.fileName.endsWith(".html")) { continue } const source = output.source const html = typeof source === "string" ? source : Buffer.from(source).toString("utf8") output.source = html .replace(/]*rel=["']stylesheet["'][^>]*>\s*/g, "") // Legacy mode uses classic scripts; strip module type first. .replace(/]*?)\stype=["']module["']([^>]*)>/g, (_match: string, before: string, after: string) => ``) // Enforce defer on every script tag in legacy HTML. .replace(/]*\bdefer\b)([^>]*)>/g, (_match: string, attrs: string) => ``) } } } } // eslint-disable-next-line @typescript-eslint/no-explicit-any export default defineConfig(async ({ command }): Promise => { const postcssParentSelector = (await import("postcss-parent-selector")).default const keepGlobalRootSelectorsPlugin: any = { postcssPlugin: "keep-global-root-selectors", Rule(rule: { selectors?: string[] }) { if (!rule.selectors) { return } rule.selectors = rule.selectors.map(selector => selector.replace(/^\.jmap_wrapper\s+(html|body|:root)(?=[\s.:#[>+~]|$)/, "$1")) } } const isLegacyProductionBuild = command === "build" && process.env.NODE_ENV === "production" const coreIndexFileUrl = process.env.CORE_INDEX_FILE_URL && process.env.CORE_INDEX_FILE_URL.trim().length > 0 ? process.env.CORE_INDEX_FILE_URL : command === "serve" ? `https://localhost:${process.env.CORE_DEV_PORT ?? "8085"}/src/core.ts` : "./ng-core/index.js" const plugins: any[] = [react(), jmapOptionsPlugin(), inlineCssInLegacyBuildPlugin(isLegacyProductionBuild)] if (command === "serve") { const { default: checker } = await import("vite-plugin-checker") plugins.push(mkcert(), checker({ typescript: true })) } return { root: __dirname, resolve: { alias: { "@": resolve(__dirname, "src") } }, define: { TYPESCRIPT_APP_VERSION: JSON.stringify(getBuildNumber()), APP_VERSION: process.env.BUILD_TARGET === "cloud" ? JSON.stringify(getBuildNumber()) : JSON.stringify(packageJSON.version), IS_DEV: command === "serve" ? "true" : "false", CORE_INDEX_FILE_URL: JSON.stringify(coreIndexFileUrl), MUI_X_PRO_LICENSE: JSON.stringify(process.env.MUI_X_PRO_LICENSE ?? "") }, css: { postcss: { plugins: [ postcssParentSelector({ selector: ".jmap_wrapper" }), keepGlobalRootSelectorsPlugin ] } }, plugins, server: { port: Number(process.env.DEV_PORT) || 8086, cors: true }, build: { outDir: process.env.BUILD_DIR || resolve(__dirname, "public"), emptyOutDir: true, target: isLegacyProductionBuild ? "es2015" : "esnext", sourcemap: command === "serve", cssCodeSplit: !isLegacyProductionBuild, // TODO: feature/JMAP8-3471 - replace terser by esbuild minify: "terser", terserOptions: { compress: {}, ecma: 2015, keep_classnames: true, keep_fnames: true, toplevel: false, format: { comments: false, beautify: false } }, rollupOptions: { input: getHtmlInputPath(), output: { ...(isLegacyProductionBuild ? { format: "iife" as const, name: "JMapNgBundle" } : {}), inlineDynamicImports: isLegacyProductionBuild, entryFileNames: process.env.BUILD_TARGET === "cloud" ? "[name].[hash].js" : "[name].js", chunkFileNames: "[name].[hash].js", assetFileNames: "[name].[hash][extname]" } } } } })