From aa2b4fb0cef65f88ad5563cfa665739b46e71698 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Thu, 16 Sep 2021 18:51:39 +1000 Subject: [PATCH] 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. --- .github/workflows/ci.yml | 8 +++- README.md | 11 +++-- daemon/src/maker.rs | 2 +- daemon/src/taker.rs | 2 +- frontend/dynamicApp.ts | 18 ++++++++ frontend/index.html | 21 ++++----- frontend/maker.html | 13 ------ frontend/{src/main.tsx => maker.tsx} | 6 +-- frontend/src/Maker.tsx | 7 +-- frontend/src/Taker.tsx | 9 ++-- frontend/src/index.tsx | 25 ----------- frontend/src/main_maker.tsx | 21 --------- frontend/taker.html | 13 ------ frontend/{src/main_taker.tsx => taker.tsx} | 8 ++-- frontend/vite.config.ts | 50 ++++++++-------------- 15 files changed, 73 insertions(+), 141 deletions(-) create mode 100644 frontend/dynamicApp.ts delete mode 100644 frontend/maker.html rename frontend/{src/main.tsx => maker.tsx} (85%) delete mode 100644 frontend/src/index.tsx delete mode 100644 frontend/src/main_maker.tsx delete mode 100644 frontend/taker.html rename frontend/{src/main_taker.tsx => taker.tsx} (81%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 165e4a7..569885c 100644 --- a/.github/workflows/ci.yml +++ b/.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 diff --git a/README.md b/README.md index 5f1ceeb..65b4b0d 100644 --- a/README.md +++ b/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. diff --git a/daemon/src/maker.rs b/daemon/src/maker.rs index eb7b3c6..b87ce46 100644 --- a/daemon/src/maker.rs +++ b/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, diff --git a/daemon/src/taker.rs b/daemon/src/taker.rs index 43295c2..3ae790b 100644 --- a/daemon/src/taker.rs +++ b/daemon/src/taker.rs @@ -155,7 +155,7 @@ async fn main() -> Result<()> { }, )) .mount( - "/", + "/api", rocket::routes![ routes_taker::feed, routes_taker::post_cfd, diff --git a/frontend/dynamicApp.ts b/frontend/dynamicApp.ts new file mode 100644 index 0000000..8161555 --- /dev/null +++ b/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; + }, + }; +} diff --git a/frontend/index.html b/frontend/index.html index f71b270..8546184 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,14 @@ - - - - - Hermes Main page - - -
- - + + + + + Hermes + + +
+ + diff --git a/frontend/maker.html b/frontend/maker.html deleted file mode 100644 index 6d9dc93..0000000 --- a/frontend/maker.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Hermes Maker - - -
- - - diff --git a/frontend/src/main.tsx b/frontend/maker.tsx similarity index 85% rename from frontend/src/main.tsx rename to frontend/maker.tsx index a0dc897..5c000ef 100644 --- a/frontend/src/main.tsx +++ b/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( diff --git a/frontend/src/Maker.tsx b/frontend/src/Maker.tsx index 94e97d1..e629a80 100644 --- a/frontend/src/Maker.tsx +++ b/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(source, "cfds"); const order = useLatestEvent(source, "order"); diff --git a/frontend/src/Taker.tsx b/frontend/src/Taker.tsx index bfa859b..acbea83 100644 --- a/frontend/src/Taker.tsx +++ b/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 { - 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 } export default function App() { - let source = useEventSource({ source: BASE_URL + "/feed" }); + let source = useEventSource({ source: "/api/feed" }); const cfds = useLatestEvent(source, "cfds"); const order = useLatestEvent(source, "order"); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx deleted file mode 100644 index dc2746c..0000000 --- a/frontend/src/index.tsx +++ /dev/null @@ -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( - - - - - - } /> - } /> - - - - - , - document.getElementById("root"), -); diff --git a/frontend/src/main_maker.tsx b/frontend/src/main_maker.tsx deleted file mode 100644 index a90ce15..0000000 --- a/frontend/src/main_maker.tsx +++ /dev/null @@ -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( - - - - - - - - - , - document.getElementById("root"), -); diff --git a/frontend/taker.html b/frontend/taker.html deleted file mode 100644 index f15130b..0000000 --- a/frontend/taker.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Hermes Taker - - -
- - - diff --git a/frontend/src/main_taker.tsx b/frontend/taker.tsx similarity index 81% rename from frontend/src/main_taker.tsx rename to frontend/taker.tsx index daba5be..7076c3a 100644 --- a/frontend/src/main_taker.tsx +++ b/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( - + diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b531056..3003308 100644 --- a/frontend/vite.config.ts +++ b/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' - // } - // } - // } - // } });