Browse Source

Remove hardcoded absolute URLs from the frontend source code

Achieving this is rather tricky due to the nature of our multi-page
project. We need to solve several problems:

1. We want a single npm project that produces two independent bundles.
2. We want our paths to be relative to the serving URL, that is `localhost:3000`
in development and in production, whereever the backend is hosted.
3. We have independent backends, hence requiring different `server.proxy`
configurations.

We solve (1) by using vite (already prior to this commit).
To solve (2), we simply remove all absolute URLs from the code and replace
them with absolute paths which will be relative to the serving host.
This creates a problem: Prior to this patch, we only have one devServer running
that would serve both frontends under a different sub-directory (/maker and /taker).
Even though this worked, it was impossible to create a proxy configuration that would:

- Forward API requests from `/maker` to `localhost:8001`
- Forward API requests from `/taker` to `localhost:8000`

Because in both cases, the API requests would simply start with `/api`, making
them indistinguishable from each other.

To solve this problem, we needed to serve each frontend separately. Doing so
would allow us to have dedicated proxy server configurations and forward the
requests to `/api` to the correct backend.

Unfortunately, the intuitive approach of solving this (have a `maker.html` and
`taker.html` file) does not work. With React being a client-side routing framework,
full page-reloads would be broken with this approach because they would be looking
for an `index.html` file which doesn't exist.

To work around this issue, our final solution is:

1. Use a dynamic ID to reference the desired app from within the `index.html`: `__app__`
2. Use a vite plugin to resolve this ID to the file in question: `maker.tsx` or `taker.tsx`

Fixes #6.
no-contract-setup-message
Thomas Eizinger 3 years ago
parent
commit
aa2b4fb0ce
No known key found for this signature in database GPG Key ID: 651AC83A6C6C8B96
  1. 8
      .github/workflows/ci.yml
  2. 11
      README.md
  3. 2
      daemon/src/maker.rs
  4. 2
      daemon/src/taker.rs
  5. 18
      frontend/dynamicApp.ts
  6. 21
      frontend/index.html
  7. 13
      frontend/maker.html
  8. 6
      frontend/maker.tsx
  9. 7
      frontend/src/Maker.tsx
  10. 9
      frontend/src/Taker.tsx
  11. 25
      frontend/src/index.tsx
  12. 21
      frontend/src/main_maker.tsx
  13. 13
      frontend/taker.html
  14. 8
      frontend/taker.tsx
  15. 50
      frontend/vite.config.ts

8
.github/workflows/ci.yml

@ -41,7 +41,8 @@ jobs:
cache-dependency-path: frontend/yarn.lock
- run: yarn install
- run: yarn run eslint
- run: yarn build
- run: APP=maker yarn build
- run: APP=taker yarn build
test_daemons:
strategy:
@ -68,6 +69,9 @@ jobs:
run: |
target/debug/maker --data-dir=/tmp/maker --generate-seed &
sleep 5s # Wait for maker to start
target/debug/taker --data-dir=/tmp/taker --generate-seed &
sleep 5s # Wait for taker to start
curl --fail http://localhost:8000/alive
curl --fail http://localhost:8000/api/alive
curl --fail http://localhost:8001/api/alive

11
README.md

@ -27,16 +27,15 @@ Note: The sqlite databases for maker and taker are currently created in the proj
## Starting the maker and taker frontend
We use a single react project for hosting both the taker and the maker frontends.
To start it in development mode:
However, the development environment still needs to be start twice!
Which frontend to start is configured via the `APP` environment variable.
```bash
cd frontend && yarn dev
cd frontend;
APP=taker yarn dev
APP=maker yarn dev
```
- To access maker: [Maker](http://localhost:3000/maker)
- To access taker: [Taker](http://localhost:3000/taker)
Bundling the web frontend and serving it from the respective daemon is yet to be configured.
At the moment you will need a browser extension to allow CORS headers like `CORS Everywhere` ([Firefox Extension](https://addons.mozilla.org/en-US/firefox/addon/cors-everywhere/)) to use the frontends.

2
daemon/src/maker.rs

@ -145,7 +145,7 @@ async fn main() -> Result<()> {
},
))
.mount(
"/",
"/api",
rocket::routes![
routes_maker::maker_feed,
routes_maker::post_sell_order,

2
daemon/src/taker.rs

@ -155,7 +155,7 @@ async fn main() -> Result<()> {
},
))
.mount(
"/",
"/api",
rocket::routes![
routes_taker::feed,
routes_taker::post_cfd,

18
frontend/dynamicApp.ts

@ -0,0 +1,18 @@
import { Plugin } from "vite";
export default function dynamicApp(app: string): Plugin {
return {
name: "dynamicApp", // required, will show up in warnings and errors
resolveId: (id) => {
// For some reason these are different?
const productionBuildId = "./__app__.tsx";
const devBuildId = "/__app__.tsx";
if (id === productionBuildId || id === devBuildId) {
return `${__dirname}/${app}.tsx`;
}
return null;
},
};
}

21
frontend/index.html

@ -1,13 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hermes Main page</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/index.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hermes</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./__app__.tsx"> // `__app__` is dynamically resolved by the `dynamicApp` vite plugin
</script>
</body>
</html>

13
frontend/maker.html

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hermes Maker</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main_maker.tsx"></script>
</body>
</html>

6
frontend/src/main.tsx → frontend/maker.tsx

@ -3,9 +3,9 @@ import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { EventSourceProvider } from "react-sse-hooks";
import "./index.css";
import App from "./Maker";
import theme from "./theme";
import "./src/index.css";
import App from "./src/Maker";
import theme from "./src/theme";
ReactDOM.render(
<React.StrictMode>

7
frontend/src/Maker.tsx

@ -24,9 +24,6 @@ import NavLink from "./components/NavLink";
import OrderTile from "./components/OrderTile";
import { Cfd, Order } from "./components/Types";
/* TODO: Change from localhost:8001 */
const BASE_URL = "http://localhost:8001";
interface CfdSellOrderPayload {
price: number;
min_quantity: number;
@ -34,7 +31,7 @@ interface CfdSellOrderPayload {
}
async function postCfdSellOrderRequest(payload: CfdSellOrderPayload) {
let res = await axios.post(BASE_URL + `/order/sell`, JSON.stringify(payload));
let res = await axios.post(`/api/order/sell`, JSON.stringify(payload));
if (!res.status.toString().startsWith("2")) {
console.log("Status: " + res.status + ", " + res.statusText);
@ -43,7 +40,7 @@ async function postCfdSellOrderRequest(payload: CfdSellOrderPayload) {
}
export default function App() {
let source = useEventSource({ source: BASE_URL + "/maker-feed" });
let source = useEventSource({ source: "/api/maker-feed" });
const cfds = useLatestEvent<Cfd[]>(source, "cfds");
const order = useLatestEvent<Order>(source, "order");

9
frontend/src/Taker.tsx

@ -11,9 +11,6 @@ import useLatestEvent from "./components/Hooks";
import NavLink from "./components/NavLink";
import { Cfd, Order } from "./components/Types";
/* TODO: Change from localhost:8000 */
const BASE_URL = "http://localhost:8000";
interface CfdTakeRequestPayload {
order_id: string;
quantity: number;
@ -30,7 +27,7 @@ interface MarginResponse {
}
async function postCfdTakeRequest(payload: CfdTakeRequestPayload) {
let res = await axios.post(BASE_URL + `/cfd`, JSON.stringify(payload));
let res = await axios.post(`/api/cfd`, JSON.stringify(payload));
if (!res.status.toString().startsWith("2")) {
throw new Error("failed to create new CFD take request: " + res.status + ", " + res.statusText);
@ -38,7 +35,7 @@ async function postCfdTakeRequest(payload: CfdTakeRequestPayload) {
}
async function getMargin(payload: MarginRequestPayload): Promise<MarginResponse> {
let res = await axios.post(BASE_URL + `/calculate/margin`, JSON.stringify(payload));
let res = await axios.post(`/api/calculate/margin`, JSON.stringify(payload));
if (!res.status.toString().startsWith("2")) {
throw new Error("failed to create new CFD take request: " + res.status + ", " + res.statusText);
@ -48,7 +45,7 @@ async function getMargin(payload: MarginRequestPayload): Promise<MarginResponse>
}
export default function App() {
let source = useEventSource({ source: BASE_URL + "/feed" });
let source = useEventSource({ source: "/api/feed" });
const cfds = useLatestEvent<Cfd[]>(source, "cfds");
const order = useLatestEvent<Order>(source, "order");

25
frontend/src/index.tsx

@ -1,25 +0,0 @@
import { ChakraProvider } from "@chakra-ui/react";
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { EventSourceProvider } from "react-sse-hooks";
import "./index.css";
import Maker from "./Maker";
import Taker from "./Taker";
import theme from "./theme";
ReactDOM.render(
<React.StrictMode>
<ChakraProvider theme={theme}>
<EventSourceProvider>
<BrowserRouter>
<Routes>
<Route path="/maker/*" element={<Maker />} />
<Route path="/taker/*" element={<Taker />} />
</Routes>
</BrowserRouter>
</EventSourceProvider>
</ChakraProvider>
</React.StrictMode>,
document.getElementById("root"),
);

21
frontend/src/main_maker.tsx

@ -1,21 +0,0 @@
import { ChakraProvider } from "@chakra-ui/react";
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { EventSourceProvider } from "react-sse-hooks";
import "./index.css";
import Maker from "./Maker";
import theme from "./theme";
ReactDOM.render(
<React.StrictMode>
<ChakraProvider theme={theme}>
<EventSourceProvider>
<BrowserRouter>
<Maker />
</BrowserRouter>
</EventSourceProvider>
</ChakraProvider>
</React.StrictMode>,
document.getElementById("root"),
);

13
frontend/taker.html

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hermes Taker</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main_taker.tsx"></script>
</body>
</html>

8
frontend/src/main_taker.tsx → frontend/taker.tsx

@ -3,16 +3,16 @@ import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { EventSourceProvider } from "react-sse-hooks";
import "./index.css";
import Taker from "./Taker";
import theme from "./theme";
import "./src/index.css";
import App from "./src/Taker";
import theme from "./src/theme";
ReactDOM.render(
<React.StrictMode>
<ChakraProvider theme={theme}>
<EventSourceProvider>
<BrowserRouter>
<Taker />
<App />
</BrowserRouter>
</EventSourceProvider>
</ChakraProvider>

50
frontend/vite.config.ts

@ -1,7 +1,18 @@
import { resolve } from "path";
import reactRefresh from "@vitejs/plugin-react-refresh";
import { resolve } from "path";
import { defineConfig } from "vite";
import dynamicApp from "./dynamicApp";
const app = process.env.APP;
if (!app || (app !== "maker" && app !== "taker")) {
throw new Error("APP environment variable needs to be set to `maker` `taker`");
}
const backendPorts = {
"taker": 8000,
"maker": 8001,
};
// https://vitejs.dev/config/
export default defineConfig({
@ -11,40 +22,17 @@ export default defineConfig({
? [reactRefresh()]
: []
),
dynamicApp(app),
],
build: {
rollupOptions: {
input: {
maker: resolve(__dirname, "maker.html"),
taker: resolve(__dirname, "taker.html"),
},
input: resolve(__dirname, `index.html`),
},
outDir: `dist/${app}`,
},
server: {
open: "/maker",
proxy: {
"/api": `http://localhost:${backendPorts[app]}`,
},
},
// server: {
// proxy: {
// '/foo': 'http://localhost:4567',
// '/api': {
// target: 'http://jsonplaceholder.typicode.com',
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, '')
// },
// // with RegEx
// '^/fallback/.*': {
// target: 'http://jsonplaceholder.typicode.com',
// changeOrigin: true,
// rewrite: (path) => path.replace(/^\/fallback/, '')
// },
// // Using the proxy instance
// '/api': {
// target: 'http://jsonplaceholder.typicode.com',
// changeOrigin: true,
// configure: (proxy, options) => {
// // proxy will be an instance of 'http-proxy'
// }
// }
// }
// }
});

Loading…
Cancel
Save