fix: build a mount-independent artifact with runtime config
This commit is contained in:
parent
ccb6993d69
commit
0222f68691
@ -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>
|
||||||
|
|||||||
@ -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})`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
36
src/runtimeConfig.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user