fix: build a mount-independent artifact with runtime config

This commit is contained in:
Bhanu Prakash Sai Potteri 2026-09-04 13:15:31 +05:30
parent ccb6993d69
commit 0222f68691
8 changed files with 63 additions and 13 deletions

View File

@ -3,11 +3,15 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/zino.svg" /> <!--BASE_HREF-->
<link rel="icon" type="image/svg+xml" href="zino.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<title>P2P</title> <title>P2P</title>
<!-- Written by frontgen at placement time; absent under `npm run dev`,
where resolveConfigValue falls back to import.meta.env. -->
<script src="./config.js"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -1,9 +1,10 @@
import { APP_ID } from "../config"; import { APP_ID } from "../config";
import { resolveConfigValue, basePath } from "../runtimeConfig";
const TOKEN_KEY = "p2p_token"; const TOKEN_KEY = "p2p_token";
function baseUrl(): string { 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<string, string> { function headers(): Record<string, string> {
@ -19,7 +20,7 @@ async function parseResponse<T>(res: Response): Promise<T> {
if (res.status === 401) { if (res.status === 401) {
localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem("p2p_user"); localStorage.removeItem("p2p_user");
window.location.href = `${import.meta.env.BASE_URL}login`; window.location.href = `${basePath()}login`;
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
if (!res.ok) { if (!res.ok) {
@ -622,7 +623,7 @@ export async function downloadFieldFile(blobPath: string, originalName?: string)
if (!res.ok) { if (!res.ok) {
if (res.status === 401) { if (res.status === 401) {
localStorage.removeItem(TOKEN_KEY); 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})`); throw new Error(`Could not load the document (${res.status})`);
} }

View File

@ -1,4 +1,5 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { basePath } from "@/runtimeConfig";
import { import {
FileText, FileText,
HelpCircle, HelpCircle,
@ -151,7 +152,7 @@ export default function DynamicHeader({
here would double it up. */} here would double it up. */}
{sidebar.isMobile && ( {sidebar.isMobile && (
<img <img
src={import.meta.env.BASE_URL + "zino.svg"} src={basePath() + "zino.svg"}
alt="Zino" alt="Zino"
className="h-5 w-auto dark:brightness-0 dark:invert" className="h-5 w-auto dark:brightness-0 dark:invert"
/> />
@ -270,7 +271,7 @@ function SidebarBody({
{/* 43×20 wordmark — fits the 56px rail as-is, so it needs no separate {/* 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. */} collapsed mark. Inverted in dark mode like the top-bar copy. */}
<img <img
src={import.meta.env.BASE_URL + "zino.svg"} src={basePath() + "zino.svg"}
alt="Zino" alt="Zino"
className="h-5 w-auto shrink-0 dark:brightness-0 dark:invert" className="h-5 w-auto shrink-0 dark:brightness-0 dark:invert"
/> />

View File

@ -1,5 +1,6 @@
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import type { User } from "../zino-sdk/types"; import type { User } from "../zino-sdk/types";
import { resolveConfigValue } from "../runtimeConfig";
const TOKEN_KEY = "p2p_token"; const TOKEN_KEY = "p2p_token";
const USER_KEY = "p2p_user"; const USER_KEY = "p2p_user";
@ -31,7 +32,7 @@ export function useAuth() {
}); });
const apiUrl = ( const apiUrl = (
import.meta.env.VITE_ZINO_API_URL || "https://studio.getzino.in" resolveConfigValue("VITE_ZINO_API_URL", "https://studio.getzino.in")
).replace(/\/+$/, ""); ).replace(/\/+$/, "");
const login = useCallback( const login = useCallback(

View File

@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import { NuqsAdapter } from 'nuqs/adapters/react-router/v6' import { NuqsAdapter } from 'nuqs/adapters/react-router/v6'
import { ZinoProvider } from './zino-sdk' import { ZinoProvider } from './zino-sdk'
import { resolveConfigValue, basePath } from './runtimeConfig'
import { AuthProvider } from './hooks/AuthContext' import { AuthProvider } from './hooks/AuthContext'
import { ThemeProvider } from './hooks/ThemeContext' import { ThemeProvider } from './hooks/ThemeContext'
import { TooltipProvider } from './components/ui/tooltip' import { TooltipProvider } from './components/ui/tooltip'
@ -10,13 +11,15 @@ import { Toaster } from './components/ui/sonner'
import './index.css' import './index.css'
import App from './App' 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( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<ThemeProvider> <ThemeProvider>
<ZinoProvider baseUrl={apiUrl}> <ZinoProvider baseUrl={apiUrl}>
<BrowserRouter basename={import.meta.env.BASE_URL}> {/* Not import.meta.env.BASE_URL — with a relative Vite base that is
'./'. The mount path comes from the document's <base href>. */}
<BrowserRouter basename={basePath()}>
{/* NuqsAdapter must sit inside the router — it reads and writes the {/* NuqsAdapter must sit inside the router — it reads and writes the
router's search params, so table state can live in the URL. */} router's search params, so table state can live in the URL. */}
<NuqsAdapter> <NuqsAdapter>

View File

@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useAuthContext } from "@/hooks/AuthContext"; import { useAuthContext } from "@/hooks/AuthContext";
import { ORG_ID } from "@/config"; import { ORG_ID } from "@/config";
import { basePath } from "@/runtimeConfig";
const HIGHLIGHTS = [ const HIGHLIGHTS = [
{ {
@ -76,7 +77,7 @@ export default function Login() {
</div> </div>
<img <img
src={import.meta.env.BASE_URL + "zino.svg"} src={basePath() + "zino.svg"}
alt="Zino" alt="Zino"
className="absolute left-16 top-10 z-10 h-5 w-auto brightness-0 invert" className="absolute left-16 top-10 z-10 h-5 w-auto brightness-0 invert"
/> />
@ -118,7 +119,7 @@ export default function Login() {
<div className="flex flex-1 items-center justify-center px-6"> <div className="flex flex-1 items-center justify-center px-6">
<div className="w-full max-w-[340px]"> <div className="w-full max-w-[340px]">
<img <img
src={import.meta.env.BASE_URL + "zino.svg"} src={basePath() + "zino.svg"}
alt="Zino" alt="Zino"
className="mb-10 h-5 w-auto dark:invert lg:hidden" className="mb-10 h-5 w-auto dark:invert lg:hidden"
/> />
@ -201,7 +202,7 @@ export default function Login() {
<div className="mt-10 flex items-center justify-center gap-1.5"> <div className="mt-10 flex items-center justify-center gap-1.5">
<span className="text-xs text-muted-foreground">Powered by</span> <span className="text-xs text-muted-foreground">Powered by</span>
<img <img
src={import.meta.env.BASE_URL + "zino.svg"} src={basePath() + "zino.svg"}
alt="Zino" alt="Zino"
className="h-3.5 w-auto dark:invert" className="h-3.5 w-auto dark:invert"
/> />

36
src/runtimeConfig.ts Normal file
View File

@ -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
// <!--BASE_HREF--> placeholder into a real <base href>. 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<string, string>;
}
}
// 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 <base href> placement
// writes. Drives the router basename and every public-asset URL.
//
// The <base> 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;
}

View File

@ -11,7 +11,10 @@ export default defineConfig({
resolve: { resolve: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }, 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
// <base href> into index.html, so one artifact serves any mount without a
// rebuild. An absolute base fails internal/builder/assert.go.
base: './',
server: { server: {
port: 3007, port: 3007,
proxy: { proxy: {