diff --git a/frontend/.dockerignore b/frontend/.dockerignore index b396fa6..03f2ba5 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -3,5 +3,4 @@ node_modules build __mocks__ .vscode -helm -private \ No newline at end of file +helm \ No newline at end of file diff --git a/frontend/.env b/frontend/.env index 410e106..394f743 100644 --- a/frontend/.env +++ b/frontend/.env @@ -5,4 +5,7 @@ VITE_APP_NETWORK_RESURRECTOR_API_URL=http://####### VITE_APP_MACHINE_PING_INTERVAL=600000 #300000 milliseconds = 5 minutes -VITE_APP_MACHINE_STARTING_TIME=300000 \ No newline at end of file +VITE_APP_MACHINE_STARTING_TIME=300000 + +# VITE_APP_BASE_URL=/network-resurrector/ +VITE_APP_BASE_URL= \ No newline at end of file diff --git a/frontend/.env.production b/frontend/.env.production index 5b406e0..82e1f2d 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -1,6 +1,6 @@ -PUBLIC_URL= -VITE_APP_TUITIO_URL=https://####### -VITE_APP_NETWORK_RESURRECTOR_API_URL=https://####### +VITE_APP_BASE_URL=/@RUNTIME_BASE_URL@/ +VITE_APP_TUITIO_URL=https:// +VITE_APP_NETWORK_RESURRECTOR_API_URL=https:// #900000 milliseconds = 15 minutes VITE_APP_MACHINE_PING_INTERVAL=900000 diff --git a/frontend/dockerfile b/frontend/dockerfile index 7444b2a..7894edc 100644 --- a/frontend/dockerfile +++ b/frontend/dockerfile @@ -2,8 +2,6 @@ FROM node:23-slim AS builder WORKDIR /app -ARG APP_SUBFOLDER="" - COPY .npmrc .npmrc COPY package*.json ./ RUN npm install @@ -12,24 +10,22 @@ RUN rm -f .npmrc COPY . ./ # build the react app -RUN if [ -z "$APP_SUBFOLDER" ]; then npm run build; else PUBLIC_URL=/${APP_SUBFOLDER}/ npm run build; fi +RUN npm run build # PRODUCTION ENVIRONMENT FROM node:23-slim -ARG APP_SUBFOLDER="" - RUN printf '\n\n- Copy application files\n' -COPY --from=builder /app/build ./application/${APP_SUBFOLDER} -COPY --from=builder /app/build/index.html ./application/ -COPY --from=builder /app/setenv.js ./application/setenv.js +COPY --from=builder /app/build ./application +COPY --from=builder /app/runtimeSetup.js ./application/runtimeSetup.js #install static server RUN npm install -g serve # environment variables ENV AUTHOR="Tudor Stanciu" -ENV PUBLIC_URL=/${APP_SUBFOLDER}/ +ENV APP_NAME="Network resurrector UI" + ARG APP_VERSION=0.0.0 ENV APP_VERSION=${APP_VERSION} @@ -41,4 +37,4 @@ WORKDIR / EXPOSE 80 -CMD ["sh", "-c", "node application/setenv.js && serve -s application -p 80"] \ No newline at end of file +CMD ["sh", "-c", "node application/runtimeSetup.js && serve -s application -p 80"] \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index b458363..988d46f 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -7,7 +7,7 @@ import tseslint from "typescript-eslint"; import prettier from "eslint-plugin-prettier"; export default tseslint.config( - { ignores: ["node_modules", "dist", "build", "**/public", "setenv.js"] }, + { ignores: ["node_modules", "dist", "build", "**/public", "runtimeSetup.js"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{js,jsx,ts,tsx}"], diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 12dbc88..3b27721 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -36,6 +36,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@eslint/js": "^9.25.1", "@types/lodash": "^4.17.16", + "@types/node": "^22.15.2", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@types/react-world-flags": "^1.6.0", @@ -2024,6 +2025,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.15.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", + "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -6586,6 +6597,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/unicorn-magic": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a603a18..9e624ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,6 +42,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@eslint/js": "^9.25.1", "@types/lodash": "^4.17.16", + "@types/node": "^22.15.2", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@types/react-world-flags": "^1.6.0", diff --git a/frontend/runtimeSetup.js b/frontend/runtimeSetup.js new file mode 100644 index 0000000..6f86d2c --- /dev/null +++ b/frontend/runtimeSetup.js @@ -0,0 +1,101 @@ +"use strict"; +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); + +const prefix = "VITE_APP_"; +const RUNTIME_BASE_URL_PLACEHOLDER = "/@RUNTIME_BASE_URL@"; +const APP_DIR = "./application"; +const ENV_JS_DIR = APP_DIR; + +function generateScriptContent() { + const prefixRegex = new RegExp(`^${prefix}`); + const env = process.env; + const config = Object.keys(env) + .filter(key => prefixRegex.test(key)) + .reduce((c, key) => Object.assign({}, c, { [key]: env[key] }), {}); + + return `window.env=${JSON.stringify(config)};`; +} + +function getSha256Hash(input) { + const hash = crypto.createHash("sha256"); + hash.update(input); + return hash.digest("hex"); +} + +function updateIndexHtml(envFileName, basePath) { + const indexPath = path.join(APP_DIR, "index.html"); + + if (!fs.existsSync(indexPath)) { + console.error(`Error: ${indexPath} not found`); + return; + } + + let indexContent = fs.readFileSync(indexPath, "utf8"); + + // Replace base path placeholder with actual value + indexContent = indexContent.replace(new RegExp(RUNTIME_BASE_URL_PLACEHOLDER, "g"), basePath || "/"); + + // Replace any existing env script with the new one + const envScriptRegex = /`; + + if (envScriptRegex.test(indexContent)) { + indexContent = indexContent.replace(envScriptRegex, newEnvScript); + } else { + // If no existing env script, add it before the first script tag + const insertPoint = indexContent.indexOf(" /^env(\.\w+)?\.js$/.test(file) && file !== newEnvFileName); + + for (const file of envFiles) { + const filePath = path.join(ENV_JS_DIR, file); + fs.unlinkSync(filePath); + console.log(`Removed old env file: ${filePath}`); + } +} + +function main() { + console.log("Setting environment variables..."); + + // Generate env script content + const scriptContent = generateScriptContent(); + + // Compute hash for cache busting + const hash = getSha256Hash(scriptContent); + const fragment = hash.substring(0, 8); + const envFileName = `env.${fragment}.js`; + const envFilePath = path.join(ENV_JS_DIR, envFileName); + + // Ensure build directory exists + if (!fs.existsSync(APP_DIR)) { + console.log(`Creating build directory: ${APP_DIR}`); + fs.mkdirSync(APP_DIR, { recursive: true }); + } + + // Write new env.js file + fs.writeFileSync(envFilePath, scriptContent, "utf8"); + console.log(`Updated ${envFilePath} with ${prefix}* environment variables`); + + // Clean up old env.js files + cleanupOldEnvFiles(envFileName); + + // Get base path from environment and update index.html + const basePath = process.env.VITE_APP_BASE_URL || "/"; + updateIndexHtml(envFileName, basePath); +} + +main(); diff --git a/frontend/setenv.js b/frontend/setenv.js deleted file mode 100644 index 3e43cdd..0000000 --- a/frontend/setenv.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; -const fs = require("fs"); -const path = require("path"); - -const prefix = "VITE_APP_"; -const publicUrl = import.meta.env.PUBLIC_URL || ""; -const scriptPath = path.join("./application", publicUrl, "env.js"); - -function generateScriptContent() { - const prefixRegex = new RegExp(`^${prefix}`); - const env = import.meta.env; - const config = Object.keys(env) - .filter(key => prefixRegex.test(key)) - .reduce((c, key) => Object.assign({}, c, { [key]: env[key] }), {}); - return `window.env=${JSON.stringify(config)};`; -} - -function saveScriptContent(scriptContents) { - fs.writeFile(scriptPath, scriptContents, "utf8", function (err) { - if (err) throw err; - }); -} - -console.log("Setting environment variables..."); -const scriptContent = generateScriptContent(); -saveScriptContent(scriptContent); -console.log(`Updated ${scriptPath} with ${prefix}* environment variables: ${scriptContent}.`); diff --git a/frontend/src/components/AppRouter.tsx b/frontend/src/components/AppRouter.tsx index 4a39b79..296b7be 100644 --- a/frontend/src/components/AppRouter.tsx +++ b/frontend/src/components/AppRouter.tsx @@ -3,6 +3,7 @@ import App from "./App"; import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom"; import { useTuitioToken } from "@flare/tuitio-client-react"; import LoginContainer from "../features/login/components/LoginContainer"; +import env from "utils/env"; const PrivateRoute = ({ children }: { children: React.ReactElement }): React.ReactElement => { const { valid } = useTuitioToken(); @@ -43,7 +44,7 @@ const PublicRoute = ({ children }: { children: React.ReactElement }): React.Reac const AppRouter: React.FC = () => { return ( - + } /> { diff --git a/frontend/src/utils/url.ts b/frontend/src/utils/url.ts new file mode 100644 index 0000000..f6f1dfe --- /dev/null +++ b/frontend/src/utils/url.ts @@ -0,0 +1,9 @@ +const ensureTrailingSlash = (url: string) => { + if (url === null || url === undefined) return url; + if (url.endsWith("/")) { + return url; + } + return `${url}/`; +}; + +export { ensureTrailingSlash }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 82c382d..7cccdea 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -19,7 +19,7 @@ "noUnusedLocals": false, "noUnusedParameters": true, "allowUnreachableCode": false, - "types": ["vite/client"] + "types": ["node", "vite/client"] }, "include": ["src"], "exclude": ["node_modules"] diff --git a/frontend/vite.config.mts b/frontend/vite.config.mts index 4b766a0..65ceb8d 100644 --- a/frontend/vite.config.mts +++ b/frontend/vite.config.mts @@ -1,30 +1,38 @@ -import { defineConfig } from "vite"; +/// + +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; import viteTsconfigPaths from "vite-tsconfig-paths"; import eslintPlugin from "vite-plugin-eslint"; import checker from "vite-plugin-checker"; +import { ensureTrailingSlash } from "./src/utils/url"; -export default defineConfig({ - // depending on your application, base can also be "/" - base: "/", - plugins: [ - react({ - jsxImportSource: "@emotion/react", - babel: { - plugins: ["@emotion/babel-plugin"] - } - }), - viteTsconfigPaths(), - eslintPlugin({ - cache: false - }), - checker({ typescript: true }) - ], - server: { - open: true, - port: 3000 - }, - build: { - outDir: "build" - } +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + const baseUrl = ensureTrailingSlash(env.VITE_APP_BASE_URL || "/"); + + return { + // depending on your application, base can also be "/" + base: baseUrl, + plugins: [ + react({ + jsxImportSource: "@emotion/react", + babel: { + plugins: ["@emotion/babel-plugin"] + } + }), + viteTsconfigPaths(), + eslintPlugin({ + cache: false + }), + checker({ typescript: true }) + ], + server: { + open: true, + port: 3000 + }, + build: { + outDir: "build" + } + }; });