refactor: add prefix to relative assets

refactor: add prefix to server entrypoints
refactor: add prefix to express router
refactor: add prefix to router basename
This commit is contained in:
Carlos Valente
2024-12-02 15:49:28 +01:00
committed by Carlos Valente
parent b3ce247f54
commit b9ba416366
8 changed files with 86 additions and 60 deletions
+25 -29
View File
@@ -1,31 +1,27 @@
<!doctype html> <!doctype html>
<html lang='en'> <html lang="en">
<head> <head>
<meta charset='utf-8' /> <meta charset="utf-8" />
<base href="/"> <base href="/" />
<link rel='icon' href='/favicon.ico' /> <link rel="icon" href="/favicon.ico" />
<meta name='viewport' content='width=device-width, initial-scale=1' /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name='theme-color' content='#101010' /> <meta name="theme-color" content="#101010" />
<meta name='ontime' content='ontime - time keeping for live events' /> <meta name="ontime" content="ontime - time keeping for live events" />
<link rel='apple-touch-icon' href='/ontime-logo.png' /> <link rel="apple-touch-icon" href="/ontime-logo.png" />
<link <link rel="icon" type="image/png" href="/ontime-logo.png" />
rel='icon' <link rel="manifest" href="/site.webmanifest" />
type='image/png' <link rel="manifest" href="/manifest.json" />
href='ontime-logo.png' <title>ontime</title>
/> <style>
<link rel='manifest' href='/site.webmanifest' /> body,
<link rel='manifest' href='/manifest.json' /> html {
<title>ontime</title> background-color: rgba(0, 0, 0, 0) !important;
<style> }
body, </style>
html { </head>
background-color: rgba(0, 0, 0, 0) !important; <body>
} <noscript>You need to enable JavaScript to run this app.</noscript>
</style> <div id="root"></div>
</head> <script type="module" src="/src/index.tsx"></script>
<body> </body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id='root'></div>
<script type='module' src='/src/index.tsx'></script>
</body>
</html> </html>
+2 -1
View File
@@ -11,6 +11,7 @@ import { connectSocket } from './common/utils/socket';
import theme from './theme/theme'; import theme from './theme/theme';
import { TranslationProvider } from './translation/TranslationProvider'; import { TranslationProvider } from './translation/TranslationProvider';
import AppRouter from './AppRouter'; import AppRouter from './AppRouter';
import { baseURI } from './externals';
connectSocket(); connectSocket();
@@ -19,7 +20,7 @@ function App() {
<ChakraProvider disableGlobalStyle resetCSS theme={theme}> <ChakraProvider disableGlobalStyle resetCSS theme={theme}>
<QueryClientProvider client={ontimeQueryClient}> <QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider> <AppContextProvider>
<BrowserRouter> <BrowserRouter basename={baseURI}>
<div className='App'> <div className='App'>
<ErrorBoundary> <ErrorBoundary>
<TranslationProvider> <TranslationProvider>
+21 -3
View File
@@ -22,7 +22,25 @@ export const isOntimeCloud = Boolean(import.meta.env.VITE_IS_CLOUD);
const socketProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const socketProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
// resolve port // resolve port
const STATIC_PORT = 4001; const STATIC_PORT = 4001; // this is used as a fallback port for development
export const serverPort = isProduction ? window.location.port : STATIC_PORT; export const serverPort = isProduction ? window.location.port : STATIC_PORT;
export const serverURL = `${window.location.protocol}//${location.hostname}:${serverPort}`; export const baseURI = resolveBaseURI();
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`; export const serverURL = `${window.location.protocol}//${window.location.hostname}:${serverPort}${baseURI}`;
export const websocketUrl = `${socketProtocol}://${window.location.hostname}:${serverPort}${baseURI}/ws`;
/**
* Resolves a base URI for a client that is not at the root segment
* ie: https://cloud.getontime.com/client-hash/timer
* This is necessary for ontime cloud and should otherwise not affect the client
*/
function resolveBaseURI() {
if (!isOntimeCloud) {
return '';
}
const [_, base, location] = window.location.pathname.split('/');
if (!location) {
return '';
}
return `/${base}`;
}
+2 -1
View File
@@ -11,6 +11,7 @@ const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
const isDev = process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'development';
export default defineConfig({ export default defineConfig({
base: './', // Ontime cloud: we use relative paths to allow them to reference a dynamic base set at runtime
plugins: [ plugins: [
react(), react(),
svgrPlugin(), svgrPlugin(),
@@ -33,7 +34,7 @@ export default defineConfig({
}), }),
compression({ compression({
algorithm: 'brotliCompress', algorithm: 'brotliCompress',
exclude: /\.(html)$/, // Exclude HTML files from compression so we can change the base property at runtime exclude: /\.(html)$/, // Ontime cloud: Exclude HTML files from compression so we can change the base property at runtime
}), }),
], ],
server: { server: {
+2 -2
View File
@@ -47,8 +47,8 @@ export class SocketServer implements IAdapter {
this.wss = null; this.wss = null;
} }
init(server: Server) { init(server: Server, prefix?: string) {
this.wss = new WebSocketServer({ path: '/ws', server, maxPayload: this.MAX_PAYLOAD }); this.wss = new WebSocketServer({ path: `${prefix}/ws`, server, maxPayload: this.MAX_PAYLOAD });
this.wss.on('connection', (ws) => { this.wss.on('connection', (ws) => {
const clientId = generateId(); const clientId = generateId();
+22 -16
View File
@@ -6,10 +6,10 @@ import expressStaticGzip from 'express-static-gzip';
import http, { type Server } from 'http'; import http, { type Server } from 'http';
import cors from 'cors'; import cors from 'cors';
import serverTiming from 'server-timing'; import serverTiming from 'server-timing';
import { extname, resolve } from 'path'; import { extname } from 'node:path';
// import utils // import utils
import { publicDir, srcDir } from './setup/index.js'; import { publicDir, srcDir, srcFiles } from './setup/index.js';
import { environment, isProduction, updateRouterPrefix } from './externals.js'; import { environment, isProduction, updateRouterPrefix } from './externals.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js'; import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js'; import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
@@ -53,8 +53,14 @@ if (!canLog) {
console.log(`Ontime public directory at ${publicDir.root} `); console.log(`Ontime public directory at ${publicDir.root} `);
} }
// calls an update to the client router prefix /**
updateRouterPrefix(); * When running in Ontime cloud, the client is not at the root segment
* ie: https://cloud.getontime.com/client-hash/timer
* This means:
* - changing the base path in the index.html file
* - prepending all express routes with the given prefix
*/
const prefix = updateRouterPrefix();
// Create express APP // Create express APP
const app = express(); const app = express();
@@ -75,20 +81,20 @@ app.use(express.urlencoded({ extended: true }));
app.use(express.json({ limit: '1mb' })); app.use(express.json({ limit: '1mb' }));
// Implement route endpoints // Implement route endpoints
app.use('/data', appRouter); // router for application data app.use(`${prefix}/data`, appRouter); // router for application data
app.use('/api', integrationRouter); // router for integrations app.use(`${prefix}/api`, integrationRouter); // router for integrations
// serve static external files // serve static external files
app.use('/external', express.static(publicDir.externalDir)); app.use(`${prefix}/external`, express.static(publicDir.externalDir));
app.use('/user', express.static(publicDir.userDir)); app.use(`${prefix}/external`, (req, res) => {
// if the user reaches to the root, we show a 404
// if the user reaches to the root, we show a 404
app.use('/external', (req, res) => {
res.status(404).send(`${req.originalUrl} not found`); res.status(404).send(`${req.originalUrl} not found`);
}); });
app.use(`${prefix}/user`, express.static(publicDir.userDir));
// serve static - react, in dev/test mode we fetch the React app from module // serve static - react, in dev/test mode we fetch the React app from module
app.use( app.use(
prefix,
expressStaticGzip(srcDir.clientDir, { expressStaticGzip(srcDir.clientDir, {
enableBrotli: true, enableBrotli: true,
orderPreference: ['br'], orderPreference: ['br'],
@@ -111,8 +117,8 @@ app.use(
}), }),
); );
app.get('*', (_req, res) => { app.get(`${prefix}/*`, (_req, res) => {
res.sendFile(resolve(srcDir.clientDir, 'index.html')); res.sendFile(srcFiles.clientIndexHtml);
}); });
// Implement catch all // Implement catch all
@@ -176,7 +182,7 @@ export const startServer = async (
const { serverPort } = getDataProvider().getSettings(); const { serverPort } = getDataProvider().getSettings();
expressServer = http.createServer(app); expressServer = http.createServer(app);
socket.init(expressServer); socket.init(expressServer, prefix);
/** /**
* Module initialises the services and provides initial payload for the store * Module initialises the services and provides initial payload for the store
@@ -221,10 +227,10 @@ export const startServer = async (
expressServer.listen(serverPort, '0.0.0.0', () => { expressServer.listen(serverPort, '0.0.0.0', () => {
const nif = getNetworkInterfaces(); const nif = getNetworkInterfaces();
consoleSuccess(`Local: http://localhost:${serverPort}/editor`); consoleSuccess(`Local: http://localhost:${serverPort}${prefix}/editor`);
for (const key in nif) { for (const key in nif) {
const address = nif[key].address; const address = nif[key].address;
consoleSuccess(`Network: http://${address}:${serverPort}/editor`); consoleSuccess(`Network: http://${address}:${serverPort}${prefix}/editor`);
} }
}); });
+10 -8
View File
@@ -3,7 +3,8 @@
*/ */
import { readFileSync, writeFileSync } from 'node:fs'; import { readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { srcFiles } from './setup/index.js';
// ================================================= // =================================================
// resolve running environment // resolve running environment
@@ -19,19 +20,20 @@ export const isProduction = isDocker || (env === 'production' && !isTest);
* This is only needed in the cloud environment where the client is not at the root segment * This is only needed in the cloud environment where the client is not at the root segment
* ie: https://cloud.getontime.com/client-hash/timer * ie: https://cloud.getontime.com/client-hash/timer
*/ */
export function updateRouterPrefix(prefix: string | undefined = process.env.ROUTER_PREFIX) { export function updateRouterPrefix(prefix: string | undefined = process.env.ROUTER_PREFIX): string {
if (!prefix) { if (!prefix) {
return; return '';
} }
const indexFile = resolve('.', 'client', 'index.html');
try { try {
const data = readFileSync(indexFile, { encoding: 'utf-8', flag: 'r' }).replace( const data = readFileSync(srcFiles.clientIndexHtml, { encoding: 'utf-8', flag: 'r' }).replace(
/<base href="[^"]*">/g, '<base href="/" />',
`<base href="${prefix}" />`, `<base href="/${prefix}/" />`,
); );
writeFileSync(indexFile, data, { encoding: 'utf-8', flag: 'w' }); writeFileSync(srcFiles.clientIndexHtml, data, { encoding: 'utf-8', flag: 'w' });
} catch (_error) { } catch (_error) {
/** unhandled */ /** unhandled */
} }
return `/${prefix}`;
} }
+2
View File
@@ -77,6 +77,8 @@ export const srcDir = {
} as const; } as const;
export const srcFiles = { export const srcFiles = {
/** Path to start index.html */
clientIndexHtml: join(srcDir.clientDir, 'index.html'),
/** Path to bundled CSS */ /** Path to bundled CSS */
cssOverride: join(srcDir.root, config.user, config.styles.directory, config.styles.filename), cssOverride: join(srcDir.root, config.user, config.styles.directory, config.styles.filename),
/** Path to bundled external readme */ /** Path to bundled external readme */