From 0222f686915fdf89cc8c057486e005d340ae408f Mon Sep 17 00:00:00 2001 From: Bhanu Prakash Sai Potteri Date: Fri, 4 Sep 2026 13:15:31 +0530 Subject: [PATCH] fix: build a mount-independent artifact with runtime config --- index.html | 6 +++++- src/api/viewService.ts | 7 ++++--- src/components/DynamicHeader.tsx | 5 +++-- src/hooks/useAuth.ts | 3 ++- src/main.tsx | 7 +++++-- src/pages/Login.tsx | 7 ++++--- src/runtimeConfig.ts | 36 ++++++++++++++++++++++++++++++++ vite.config.ts | 5 ++++- 8 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 src/runtimeConfig.ts diff --git a/index.html b/index.html index f1ad924..644a3a2 100644 --- a/index.html +++ b/index.html @@ -3,11 +3,15 @@ - + + P2P + +
diff --git a/src/api/viewService.ts b/src/api/viewService.ts index fcae6f0..dc7fec4 100644 --- a/src/api/viewService.ts +++ b/src/api/viewService.ts @@ -1,9 +1,10 @@ import { APP_ID } from "../config"; +import { resolveConfigValue, basePath } from "../runtimeConfig"; const TOKEN_KEY = "p2p_token"; function baseUrl(): string { - return (import.meta.env.VITE_ZINO_API_URL || "https://studio.getzino.in").replace(/\/+$/, ""); + return resolveConfigValue("VITE_ZINO_API_URL", "https://studio.getzino.in").replace(/\/+$/, ""); } function headers(): Record { @@ -19,7 +20,7 @@ async function parseResponse(res: Response): Promise { if (res.status === 401) { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem("p2p_user"); - window.location.href = `${import.meta.env.BASE_URL}login`; + window.location.href = `${basePath()}login`; throw new Error("Unauthorized"); } if (!res.ok) { @@ -622,7 +623,7 @@ export async function downloadFieldFile(blobPath: string, originalName?: string) if (!res.ok) { if (res.status === 401) { localStorage.removeItem(TOKEN_KEY); - window.location.href = `${import.meta.env.BASE_URL}login`; + window.location.href = `${basePath()}login`; } throw new Error(`Could not load the document (${res.status})`); } diff --git a/src/components/DynamicHeader.tsx b/src/components/DynamicHeader.tsx index ae09c8d..4dd4a16 100644 --- a/src/components/DynamicHeader.tsx +++ b/src/components/DynamicHeader.tsx @@ -1,4 +1,5 @@ import { useNavigate } from "react-router-dom"; +import { basePath } from "@/runtimeConfig"; import { FileText, HelpCircle, @@ -151,7 +152,7 @@ export default function DynamicHeader({ here would double it up. */} {sidebar.isMobile && ( Zino @@ -270,7 +271,7 @@ function SidebarBody({ {/* 43×20 wordmark — fits the 56px rail as-is, so it needs no separate collapsed mark. Inverted in dark mode like the top-bar copy. */} Zino diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index a2d8698..adf24ab 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -1,5 +1,6 @@ import { useState, useCallback } from "react"; import type { User } from "../zino-sdk/types"; +import { resolveConfigValue } from "../runtimeConfig"; const TOKEN_KEY = "p2p_token"; const USER_KEY = "p2p_user"; @@ -31,7 +32,7 @@ export function useAuth() { }); const apiUrl = ( - import.meta.env.VITE_ZINO_API_URL || "https://studio.getzino.in" + resolveConfigValue("VITE_ZINO_API_URL", "https://studio.getzino.in") ).replace(/\/+$/, ""); const login = useCallback( diff --git a/src/main.tsx b/src/main.tsx index 705969d..56d5ffe 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import { NuqsAdapter } from 'nuqs/adapters/react-router/v6' import { ZinoProvider } from './zino-sdk' +import { resolveConfigValue, basePath } from './runtimeConfig' import { AuthProvider } from './hooks/AuthContext' import { ThemeProvider } from './hooks/ThemeContext' import { TooltipProvider } from './components/ui/tooltip' @@ -10,13 +11,15 @@ import { Toaster } from './components/ui/sonner' import './index.css' import App from './App' -const apiUrl = import.meta.env.VITE_ZINO_API_URL || 'https://studio.getzino.in' +const apiUrl = resolveConfigValue('VITE_ZINO_API_URL', 'https://studio.getzino.in') createRoot(document.getElementById('root')!).render( - + {/* Not import.meta.env.BASE_URL — with a relative Vite base that is + './'. The mount path comes from the document's . */} + {/* NuqsAdapter must sit inside the router — it reads and writes the router's search params, so table state can live in the URL. */} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index e1f5d09..f5e93b3 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useAuthContext } from "@/hooks/AuthContext"; import { ORG_ID } from "@/config"; +import { basePath } from "@/runtimeConfig"; const HIGHLIGHTS = [ { @@ -76,7 +77,7 @@ export default function Login() { Zino @@ -118,7 +119,7 @@ export default function Login() {
Zino @@ -201,7 +202,7 @@ export default function Login() {
Powered by Zino diff --git a/src/runtimeConfig.ts b/src/runtimeConfig.ts new file mode 100644 index 0000000..efd96aa --- /dev/null +++ b/src/runtimeConfig.ts @@ -0,0 +1,36 @@ +// Runtime configuration, per frontgen's placement contract. +// +// A deployed artifact carries no environment values. When frontgen places a +// build it writes config.js next to index.html and rewrites the +// placeholder into a real . That is what lets one +// artifact serve any mount path — /p2p/ here, something else after promotion — +// without being rebuilt. See sm2/frontgen internal/deploy/place.go and the +// checks in internal/builder/assert.go. + +declare global { + interface Window { + __RUNTIME_CONFIG__?: Record; + } +} + +// SAME_ORIGIN is what placement writes for a custom-domain mount, where the +// platform is proxied under the serving host. An empty string could not express +// it — it is falsy, so it would fall through the || chain below. +export function resolveConfigValue(key: string, fallback = ""): string { + const runtime = (window.__RUNTIME_CONFIG__ || {})[key]; + const devFallback = import.meta.env.DEV ? (import.meta.env[key] as string) : ""; + const value = runtime || devFallback || fallback; + return value === "SAME_ORIGIN" ? window.location.origin.replace(/\/$/, "") : value; +} + +// The mount path this app is served under, read from the placement +// writes. Drives the router basename and every public-asset URL. +// +// The guard is for `npm run dev`, where the placeholder is still an HTML +// comment: without it document.baseURI is just the current page URL, so a deep +// link like /invoices/12 would yield "/invoices/12" as the basename and the +// router would match nothing. +export function basePath(): string { + if (!document.querySelector("base")) return "/"; + return new URL(document.baseURI).pathname; +} diff --git a/vite.config.ts b/vite.config.ts index ab7e91f..248462c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,10 @@ export default defineConfig({ resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }, }, - base: process.env.VITE_BASE_URL ?? '/p2p/', + // Relative, never a mount path: frontgen's placement step writes the real + // into index.html, so one artifact serves any mount without a + // rebuild. An absolute base fails internal/builder/assert.go. + base: './', server: { port: 3007, proxy: {