mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
feat(web): support GITNEXUS_BACKEND_URL env var for Docker deployments
This commit is contained in:
parent
368049576b
commit
397488df02
6 changed files with 152 additions and 13 deletions
|
|
@ -30,6 +30,13 @@ services:
|
|||
container_name: ${WEB_CONTAINER_NAME:-gitnexus-web}
|
||||
ports:
|
||||
- '${WEB_HOST_PORT:-4173}:4173'
|
||||
# Optional: override the backend URL served to the browser.
|
||||
# Required when the gitnexus-server is not reachable at http://localhost:4747
|
||||
# from the user's browser (e.g. remote server deployments).
|
||||
# Use http://host.docker.internal:4747 on Docker Desktop (Mac/Windows),
|
||||
# or the server's LAN IP for Linux hosts.
|
||||
# environment:
|
||||
# - GITNEXUS_BACKEND_URL=http://host.docker.internal:4747
|
||||
depends_on:
|
||||
gitnexus-server:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createReadStream } from 'node:fs';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import { createServer } from 'node:http';
|
||||
import { extname, join, normalize, sep } from 'node:path';
|
||||
|
||||
|
|
@ -7,6 +7,26 @@ const host = '0.0.0.0';
|
|||
const port = Number(process.env.PORT || '4173');
|
||||
const root = join(process.cwd(), 'dist');
|
||||
|
||||
function isValidUrl(value) {
|
||||
try {
|
||||
const u = new URL(value);
|
||||
return u.protocol === 'http:' || u.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const rawBackendUrl = process.env.GITNEXUS_BACKEND_URL ?? null;
|
||||
if (rawBackendUrl && !isValidUrl(rawBackendUrl)) {
|
||||
console.warn(
|
||||
`[gitnexus-web] GITNEXUS_BACKEND_URL "${rawBackendUrl}" is not a valid http/https URL — ignoring.`,
|
||||
);
|
||||
}
|
||||
const backendUrl = rawBackendUrl && isValidUrl(rawBackendUrl) ? rawBackendUrl : null;
|
||||
const configScript = backendUrl
|
||||
? `<script>window.__GITNEXUS_CONFIG__=${JSON.stringify({ backendUrl })};</script>`
|
||||
: '';
|
||||
|
||||
const contentTypes = {
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
|
|
@ -59,17 +79,38 @@ const server = createServer(async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, {
|
||||
'Cache-Control': filePath.includes('/assets/')
|
||||
? 'public, max-age=31536000, immutable'
|
||||
: 'no-cache',
|
||||
'Content-Type': contentTypes[extname(filePath)] || 'application/octet-stream',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
});
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on('error', () => res.destroy());
|
||||
stream.pipe(res);
|
||||
const isHtml = extname(filePath) === '.html' || !extname(filePath);
|
||||
const cacheControl = filePath.includes('/assets/')
|
||||
? 'public, max-age=31536000, immutable'
|
||||
: 'no-cache';
|
||||
const contentType = contentTypes[extname(filePath)] || 'application/octet-stream';
|
||||
|
||||
if (isHtml && configScript) {
|
||||
const raw = await readFile(filePath, 'utf8');
|
||||
if (!raw.includes('</head>')) {
|
||||
console.warn('[gitnexus-web] Could not inject config: no </head> tag found in HTML');
|
||||
}
|
||||
const html = raw.includes('</head>') ? raw.replace('</head>', `${configScript}</head>`) : raw;
|
||||
const buf = Buffer.from(html, 'utf8');
|
||||
res.writeHead(200, {
|
||||
'Cache-Control': cacheControl,
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Content-Length': buf.length,
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
});
|
||||
res.end(buf);
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Cache-Control': cacheControl,
|
||||
'Content-Type': contentType,
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
});
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on('error', () => res.destroy());
|
||||
stream.pipe(res);
|
||||
}
|
||||
} catch (error) {
|
||||
res.writeHead(500);
|
||||
res.end(error instanceof Error ? error.message : 'Internal server error');
|
||||
|
|
|
|||
|
|
@ -105,3 +105,61 @@ it('returns 404 when dist/index.html is missing', async () => {
|
|||
const res = await rawGet(serverPort, '/nonexistent-page');
|
||||
assert.equal(res.status, 404);
|
||||
});
|
||||
|
||||
// ── Config injection logic tests (inline, independent of server process) ─────
|
||||
|
||||
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
|
||||
function isValidUrl(value) {
|
||||
try {
|
||||
const u = new URL(value);
|
||||
return u.protocol === 'http:' || u.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function makeInjectedHtml(envBackendUrl) {
|
||||
const rawHtml = '<!doctype html><html><head></head><body></body></html>';
|
||||
const backendUrl = envBackendUrl && isValidUrl(envBackendUrl) ? envBackendUrl : null;
|
||||
const configScript = backendUrl
|
||||
? `<script>window.__GITNEXUS_CONFIG__=${JSON.stringify({ backendUrl })};</script>`
|
||||
: '';
|
||||
return configScript ? rawHtml.replace('</head>', `${configScript}</head>`) : rawHtml;
|
||||
}
|
||||
|
||||
it('injects __GITNEXUS_CONFIG__ when GITNEXUS_BACKEND_URL is a valid URL', () => {
|
||||
const html = makeInjectedHtml('http://10.0.0.1:4747');
|
||||
assert.ok(
|
||||
html.includes(
|
||||
'<script>window.__GITNEXUS_CONFIG__={"backendUrl":"http://10.0.0.1:4747"};</script>',
|
||||
),
|
||||
'Expected config script to be injected into index.html',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not inject when GITNEXUS_BACKEND_URL is not set', () => {
|
||||
const raw = '<!doctype html><html><head></head><body></body></html>';
|
||||
const html = makeInjectedHtml(null);
|
||||
assert.equal(html, raw, 'Expected index.html to be unchanged when no env var is set');
|
||||
});
|
||||
|
||||
it('injects config script before </head>', () => {
|
||||
const html = makeInjectedHtml('http://10.0.0.1:4747');
|
||||
const headCloseIdx = html.indexOf('</head>');
|
||||
const scriptIdx = html.indexOf('<script>window.__GITNEXUS_CONFIG__');
|
||||
assert.ok(scriptIdx !== -1, 'Script tag must be present');
|
||||
assert.ok(scriptIdx < headCloseIdx, 'Script must appear before </head>');
|
||||
});
|
||||
|
||||
it('does not inject when GITNEXUS_BACKEND_URL is not a valid URL', () => {
|
||||
const raw = '<!doctype html><html><head></head><body></body></html>';
|
||||
const html = makeInjectedHtml('not-a-url');
|
||||
assert.equal(html, raw, 'Expected index.html to be unchanged for an invalid URL');
|
||||
});
|
||||
|
||||
it('does not inject when GITNEXUS_BACKEND_URL uses a non-http protocol', () => {
|
||||
const raw = '<!doctype html><html><head></head><body></body></html>';
|
||||
const html = makeInjectedHtml('ftp://somehost:21');
|
||||
assert.equal(html, raw, 'Expected index.html to be unchanged for non-http protocol');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
export const ERROR_RESET_DELAY_MS = 3000;
|
||||
export const BACKEND_URL_DEBOUNCE_MS = 500;
|
||||
|
||||
export const DEFAULT_BACKEND_URL = 'http://localhost:4747';
|
||||
export const DEFAULT_BACKEND_URL =
|
||||
(typeof window !== 'undefined' && window.__GITNEXUS_CONFIG__?.backendUrl) ||
|
||||
'http://localhost:4747';
|
||||
export const DEFAULT_OLLAMA_BASE_URL = 'http://localhost:11434';
|
||||
export const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
||||
|
||||
|
|
|
|||
6
gitnexus-web/src/vite-env.d.ts
vendored
6
gitnexus-web/src/vite-env.d.ts
vendored
|
|
@ -1 +1,7 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface Window {
|
||||
__GITNEXUS_CONFIG__?: {
|
||||
backendUrl?: string;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,3 +165,28 @@ describe('fetchGraph', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_BACKEND_URL resolution', () => {
|
||||
afterEach(() => {
|
||||
delete window.__GITNEXUS_CONFIG__;
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('falls back to localhost:4747 when no config is injected', async () => {
|
||||
delete window.__GITNEXUS_CONFIG__;
|
||||
const { DEFAULT_BACKEND_URL } = await import('../../src/config/ui-constants');
|
||||
expect(DEFAULT_BACKEND_URL).toBe('http://localhost:4747');
|
||||
});
|
||||
|
||||
it('uses window.__GITNEXUS_CONFIG__.backendUrl when set', async () => {
|
||||
window.__GITNEXUS_CONFIG__ = { backendUrl: 'http://10.0.0.1:4747' };
|
||||
const { DEFAULT_BACKEND_URL } = await import('../../src/config/ui-constants');
|
||||
expect(DEFAULT_BACKEND_URL).toBe('http://10.0.0.1:4747');
|
||||
});
|
||||
|
||||
it('falls back to localhost:4747 when config object has no backendUrl', async () => {
|
||||
window.__GITNEXUS_CONFIG__ = {};
|
||||
const { DEFAULT_BACKEND_URL } = await import('../../src/config/ui-constants');
|
||||
expect(DEFAULT_BACKEND_URL).toBe('http://localhost:4747');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue