Allow overriding configuration at run-time

This commit is contained in:
Rūdolfs Ošiņš
2025-04-25 08:55:09 +02:00
parent af334bb0e4
commit 775cbd1c37
16 changed files with 105 additions and 22 deletions
+40 -10
View File
@@ -42,24 +42,54 @@ There are several ways to deploy the UI publicly. Here are two common options:
1. Fork this repository to create your own version
2. Configure your Vercel account to deploy the forked repository
## Configuration
There's two ways to configure the UI:
There are two ways to configure the UI: **at build time** and **at run time**.
**Create a `local.json` config file**
### Build-Time Configuration
1. Copy [default.json][def] to a new file in the same folder called
This method is recommended when deploying to static hosting platforms such
as Vercel.
#### Option 1: Create a `local.json` file
1. Copy [`default.json`][def] to a new file in the same directory called
`local.json`.
2. Modify the properties in `local.json` to your preference.
2. Modify the properties in `local.json` to suit your setup.
**Environment variables**
#### Option 2: Use environment variables
1. Check [custom-environment-variables.json][env] for all available environment
variables.
2. Set the desired environment variables when building the UI.
1. Refer to [`custom-environment-variables.json`][env] for a list of supported
environment variables.
2. Set the desired variables in your environment before building the UI.
> For advanced configuration options, refer to the [`node-config`][nco]
> documentation.
### Run-Time Configuration
This method is useful when the app is distributed as a precompiled static
JS/HTML bundle, such as when installed via a package manager.
You can build the app in a mode that loads configuration dynamically from the
server it's deployed to, instead of bundling it at build time.
To enable this behavior, set the environment variable `VITE_RUNTIME_CONFIG=true`
during the build:
```bash
VITE_RUNTIME_CONFIG=true npm run build
```
This will inject a blocking script into the `index.html` that attempts to load
the configuration from a pre-defined location (`/config.json`) on the server.
The config file must be served as static content and must be publicly accessible.
The structure of the runtime `config.json` must match the shape of the
application's base configuration defined in `config/default.json`.
> For advanced configuration options, have a look at the [`node-config`][nco]
> package.
## Contributing
Vendored
+8
View File
@@ -0,0 +1,8 @@
import type { Config } from "./module.d.ts";
declare global {
// eslint-disable-next-line no-var, @typescript-eslint/naming-convention
var __CONFIG__: Config;
}
export {};
+1 -1
View File
@@ -3,7 +3,7 @@
import type { ZodIssue, ZodType, TypeOf } from "zod";
import config from "virtual:config";
import config from "@app/lib/config";
import { satisfies } from "compare-versions";
export interface BaseUrl {
+1 -1
View File
@@ -2,7 +2,7 @@
import type { ComponentProps } from "svelte";
import type { ErrorParam } from "@app/lib/router/definitions";
import config from "virtual:config";
import config from "@app/lib/config";
import Command from "./Command.svelte";
import ExternalLink from "./ExternalLink.svelte";
import IconLarge from "./IconLarge.svelte";
+19
View File
@@ -0,0 +1,19 @@
import buildTimeConfig from "virtual:config";
import mergeWith from "lodash/mergeWith";
import { isArray } from "lodash";
const customMerge = <T>(objValue: T, srcValue: T): T | undefined => {
if (isArray(objValue)) {
return srcValue;
}
return undefined;
};
const config = mergeWith(
{},
buildTimeConfig,
globalThis.__CONFIG__ || {},
customMerge,
) as typeof buildTimeConfig;
export default config;
+1 -1
View File
@@ -5,7 +5,7 @@ import { get, writable } from "svelte/store";
import * as mutexExecutor from "@app/lib/mutexExecutor";
import * as utils from "@app/lib/utils";
import config from "virtual:config";
import config from "@app/lib/config";
import {
repoRouteToPath,
repoTitle,
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import config from "virtual:config";
import config from "@app/lib/config";
import Command from "@app/components/Command.svelte";
import ExternalLink from "@app/components/ExternalLink.svelte";
+1 -1
View File
@@ -4,7 +4,7 @@
import isEqual from "lodash/isEqual";
import some from "lodash/some";
import config from "virtual:config";
import config from "@app/lib/config";
import { HttpdClient } from "@http-client";
import {
addBookmark,
+1 -1
View File
@@ -5,7 +5,7 @@ import storedWritable from "@app/lib/localStore";
import { array, number, string, object } from "zod";
import { get } from "svelte/store";
import config from "virtual:config";
import config from "@app/lib/config";
const seedSchema = object({
hostname: string(),
+1 -1
View File
@@ -1,7 +1,7 @@
import type { BaseUrl, Node, NodeStats } from "@http-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import config from "virtual:config";
import config from "@app/lib/config";
import { HttpdClient } from "@http-client";
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
import { baseUrlToString, isLocal } from "@app/lib/utils";
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import type { BaseUrl } from "@http-client";
import config from "virtual:config";
import config from "@app/lib/config";
import { parseRepositoryId } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
+1 -1
View File
@@ -9,7 +9,7 @@
} from "@http-client";
import type { RepoRoute } from "./router";
import config from "virtual:config";
import config from "@app/lib/config";
import { HttpdClient } from "@http-client";
import { baseUrlToString } from "@app/lib/utils";
import { groupCommits } from "@app/lib/commit";
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import config from "virtual:config";
import config from "@app/lib/config";
import debounce from "lodash/debounce";
import { toClipboard } from "@app/lib/utils";
+1 -1
View File
@@ -24,7 +24,7 @@ import type {
} from "@http-client";
import * as Syntax from "@app/lib/syntax";
import config from "virtual:config";
import config from "@app/lib/config";
import { HttpdClient } from "@http-client";
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
import { cached } from "@app/lib/cache";
+1 -1
View File
@@ -2,7 +2,7 @@ import type { BaseUrl, NodeIdentity, NodeStats } from "@http-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import * as utils from "@app/lib/utils";
import config from "virtual:config";
import config from "@app/lib/config";
import { HttpdClient } from "@http-client";
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
import { handleError } from "@app/views/nodes/error";
+26
View File
@@ -27,6 +27,32 @@ export default defineConfig({
},
compilerOptions: { dev: process.env.NODE_ENV !== "production" },
}),
{
name: "inject-config-loader",
transformIndexHtml() {
if (process.env.VITE_RUNTIME_CONFIG === "true") {
return [
{
tag: "script",
attrs: {
type: "text/javascript",
},
children: `
try {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/config.json", false);
xhr.send(null);
window.__CONFIG__ = JSON.parse(xhr.responseText);
} catch {
console.warn("Couldn't load config.json from the server, using built-in fallback config.");
}
`,
injectTo: "head-prepend",
},
];
}
},
},
],
server: {
host: "localhost",