From 933022850b58f477bc46dea3ff3222056da5c432 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 18 Nov 2022 13:09:10 +0100 Subject: [PATCH] Serve the SPA by the server --- .dockerignore | 2 + Cargo.lock | 26 ++++ Dockerfile | 32 ++++- crates/cli/Cargo.toml | 6 +- crates/cli/src/server.rs | 34 ++++- crates/config/Cargo.toml | 1 + crates/config/src/sections/http.rs | 21 +++ crates/spa/Cargo.toml | 20 +++ crates/spa/src/bin/render.rs | 32 +++++ crates/spa/src/lib.rs | 95 +++++++++++++ crates/spa/src/vite.rs | 218 +++++++++++++++++++++++++++++ docs/config.schema.json | 30 ++++ frontend/index.html | 11 +- frontend/package.json | 2 +- frontend/src/Router.tsx | 65 +++++---- frontend/src/config.d.ts | 21 +++ frontend/src/main.tsx | 5 +- frontend/vite.config.ts | 5 + 18 files changed, 581 insertions(+), 45 deletions(-) create mode 100644 crates/spa/Cargo.toml create mode 100644 crates/spa/src/bin/render.rs create mode 100644 crates/spa/src/lib.rs create mode 100644 crates/spa/src/vite.rs create mode 100644 frontend/src/config.d.ts diff --git a/.dockerignore b/.dockerignore index aea2924d..32b5db6f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,8 @@ target/ crates/*/target crates/*/node_modules +frontend/node_modules +frontend/dist docs/ .devcontainer/ .git/ diff --git a/Cargo.lock b/Cargo.lock index b5cabad9..eee9dcbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -850,6 +850,15 @@ dependencies = [ "either", ] +[[package]] +name = "camino" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" +dependencies = [ + "serde", +] + [[package]] name = "cbc" version = "0.1.2" @@ -2504,6 +2513,7 @@ dependencies = [ "mas-listener", "mas-policy", "mas-router", + "mas-spa", "mas-static-files", "mas-storage", "mas-tasks", @@ -2523,6 +2533,7 @@ dependencies = [ "serde_yaml", "tokio", "tower", + "tower-http", "tracing", "tracing-appender", "tracing-opentelemetry", @@ -2830,6 +2841,21 @@ dependencies = [ "url", ] +[[package]] +name = "mas-spa" +version = "0.1.0" +dependencies = [ + "camino", + "headers", + "http", + "serde", + "serde_json", + "thiserror", + "tokio", + "tower-http", + "tower-service", +] + [[package]] name = "mas-static-files" version = "0.1.0" diff --git a/Dockerfile b/Dockerfile index 5918bc23..544215b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,9 +17,9 @@ ARG ZIG_VERSION=0.9.1 ARG NODEJS_VERSION=18 ARG OPA_VERSION=0.45.0 -####################################################### -## Build stage that builds the static files/frontend ## -####################################################### +############################################## +## Build stage that builds the static files ## +############################################## FROM --platform=${BUILDPLATFORM} docker.io/library/node:${NODEJS_VERSION}-${DEBIAN_VERSION_NAME}-slim AS static-files @@ -34,6 +34,30 @@ RUN npm run build # Change the timestamp of built files for better caching RUN find public -type f -exec touch -t 197001010000.00 {} + +########################################## +## Build stage that builds the frontend ## +########################################## + +FROM --platform=${BUILDPLATFORM} docker.io/library/node:${NODEJS_VERSION}-${DEBIAN_VERSION_NAME}-slim AS frontend + +WORKDIR /app/frontend + +COPY ./frontend/package.json ./frontend/package-lock.json /app/frontend/ +RUN npm ci + +COPY ./frontend/ /app/frontend/ +RUN npm run build + +# Move the built files +RUN \ + mkdir -p /usr/local/share/mas-cli/frontend-assets && \ + cp ./dist/manifest.json /usr/local/share/mas-cli/frontend-manifest.json && \ + rm -f ./dist/index.html* ./dist/manifest.json* && \ + cp ./dist/* /usr/local/share/mas-cli/frontend-assets/ + +# Change the timestamp of built files for better caching +RUN find /usr/local/share/mas-cli -exec touch -t 197001010000.00 {} + + ############################################## ## Build stage that builds the OPA policies ## ############################################## @@ -143,6 +167,7 @@ RUN mv target/$(/docker-arch-to-rust-target.sh "${TARGETPLATFORM}")/release/mas- FROM --platform=${TARGETPLATFORM} gcr.io/distroless/cc-debian${DEBIAN_VERSION}:debug-nonroot AS debug COPY --from=builder /usr/local/bin/mas-cli /usr/local/bin/mas-cli +COPY --from=frontend /usr/local/share/mas-cli /usr/local/share/mas-cli WORKDIR / ENTRYPOINT ["/usr/local/bin/mas-cli"] @@ -152,5 +177,6 @@ ENTRYPOINT ["/usr/local/bin/mas-cli"] FROM --platform=${TARGETPLATFORM} gcr.io/distroless/cc-debian${DEBIAN_VERSION}:nonroot COPY --from=builder /usr/local/bin/mas-cli /usr/local/bin/mas-cli +COPY --from=frontend /usr/local/share/mas-cli /usr/local/share/mas-cli WORKDIR / ENTRYPOINT ["/usr/local/bin/mas-cli"] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 7a8a7634..c376b8d6 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -23,6 +23,7 @@ serde_json = "1.0.87" serde_yaml = "0.9.14" tokio = { version = "1.21.2", features = ["full"] } tower = { version = "0.4.13", features = ["full"] } +tower-http = { version = "0.3.4", features = ["fs"] } url = "2.3.1" watchman_client = "0.8.0" @@ -43,13 +44,14 @@ mas-config = { path = "../config" } mas-email = { path = "../email" } mas-handlers = { path = "../handlers", default-features = false } mas-http = { path = "../http", default-features = false, features = ["axum", "client"] } +mas-listener = { path = "../listener" } mas-policy = { path = "../policy" } mas-router = { path = "../router" } +mas-spa = { path = "../spa" } mas-static-files = { path = "../static-files" } mas-storage = { path = "../storage" } mas-tasks = { path = "../tasks" } mas-templates = { path = "../templates" } -mas-listener = { path = "../listener" } [dev-dependencies] indoc = "1.0.7" @@ -58,7 +60,7 @@ indoc = "1.0.7" default = ["otlp", "jaeger", "zipkin", "prometheus", "webpki-roots", "policy-cache"] # Features used in the Docker image -docker = ["otlp", "jaeger", "zipkin", "prometheus", "native-roots"] +docker = ["otlp", "jaeger", "zipkin", "prometheus", "native-roots", "mas-config/docker"] # Enable wasmtime compilation cache policy-cache = ["mas-policy/cache"] diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index abc6680c..6046b4b1 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -13,19 +13,24 @@ // limitations under the License. use std::{ + future::ready, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, ToSocketAddrs}, os::unix::net::UnixListener, sync::Arc, }; use anyhow::Context; -use axum::{body::HttpBody, Extension, Router}; +use axum::{body::HttpBody, error_handling::HandleErrorLayer, Extension, Router}; +use hyper::StatusCode; use listenfd::ListenFd; use mas_config::{HttpBindConfig, HttpResource, HttpTlsConfig, UnixOrTcp}; use mas_handlers::AppState; use mas_listener::{unix_or_tcp::UnixOrTcpListener, ConnectionInfo}; use mas_router::Route; +use mas_spa::ViteManifestService; use rustls::ServerConfig; +use tower::Layer; +use tower_http::services::ServeDir; #[allow(clippy::trait_duplication_in_bounds)] pub fn build_router(state: &Arc, resources: &[HttpResource]) -> Router @@ -70,6 +75,33 @@ where format!("{connection:?}") }), ), + + mas_config::HttpResource::Spa { assets, manifest } => { + let error_layer = + HandleErrorLayer::new(|_e| ready(StatusCode::INTERNAL_SERVER_ERROR)); + + // TODO: split the assets service and the index service, and make those paths + // configurable + let assets_base = "/app-assets/"; + let app_base = "/app/"; + + // TODO: make that config typed and configurable + let config = serde_json::json!({ + "root": app_base, + }); + + let index_service = ViteManifestService::new( + manifest.clone().try_into().unwrap(), + assets_base.into(), + config, + ); + + let static_service = ServeDir::new(assets).append_index_html_on_directories(false); + + router + .nest(app_base, error_layer.layer(index_service)) + .nest(assets_base, error_layer.layer(static_service)) + } } } diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 677e0a22..00520d4d 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -40,6 +40,7 @@ mas-email = { path = "../email" } [features] native-roots = ["mas-email/native-roots"] webpki-roots = ["mas-email/webpki-roots"] +docker = [] [[bin]] name = "schema" diff --git a/crates/config/src/sections/http.rs b/crates/config/src/sections/http.rs index 72190523..f9a938dc 100644 --- a/crates/config/src/sections/http.rs +++ b/crates/config/src/sections/http.rs @@ -259,6 +259,15 @@ pub enum Resource { /// the upstream connection #[serde(rename = "connection-info")] ConnectionInfo, + + /// Mount the single page app + Spa { + /// Path to the vite manifest + manifest: PathBuf, + + /// Path to the assets to server + assets: PathBuf, + }, } /// Configuration of a listener @@ -309,6 +318,18 @@ impl Default for HttpConfig { Resource::Compat, Resource::GraphQL { playground: true }, Resource::Static { web_root: None }, + #[cfg(not(feature = "docker"))] + Resource::Spa { + manifest: "./frontend/dist/manifest.json".into(), + assets: "./frontend/dist/".into(), + }, + #[cfg(feature = "docker")] + Resource::Spa { + // This is where the frontend files are mounted in the docker image by + // default + manifest: "/usr/local/share/mas-cli/frontend-manifest.json".into(), + assets: "/usr/local/share/mas-cli/frontend-assets/".into(), + }, ], tls: None, proxy_protocol: false, diff --git a/crates/spa/Cargo.toml b/crates/spa/Cargo.toml new file mode 100644 index 00000000..9b7eaa14 --- /dev/null +++ b/crates/spa/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mas-spa" +version = "0.1.0" +authors = ["Quentin Gliech "] +edition = "2021" +license = "Apache-2.0" + +[dependencies] +serde = { version = "1.0.147", features = ["derive"] } +serde_json = "1.0.87" +thiserror = "1.0.37" +camino = { version = "1.1.1", features = ["serde1"] } +headers = "0.3.2" +http = "0.2.8" +tower-service = "0.3.2" +tower-http = { version = "0.3.4", features = ["fs"] } +tokio = { version = "1.21.2", features = ["fs"] } + +[[bin]] +name = "render" diff --git a/crates/spa/src/bin/render.rs b/crates/spa/src/bin/render.rs new file mode 100644 index 00000000..a06a9523 --- /dev/null +++ b/crates/spa/src/bin/render.rs @@ -0,0 +1,32 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use camino::Utf8Path; +use mas_spa::ViteManifest; + +fn main() { + let mut stdin = std::io::stdin(); + let manifest: ViteManifest = + serde_json::from_reader(&mut stdin).expect("failed to read manifest from stdin"); + let assets_base = Utf8Path::new("/assets/"); + + let config = serde_json::json!({ + "root": "/app/", + }); + + let html = manifest + .render(assets_base, &config) + .expect("failed to render"); + println!("{html}"); +} diff --git a/crates/spa/src/lib.rs b/crates/spa/src/lib.rs new file mode 100644 index 00000000..58838f28 --- /dev/null +++ b/crates/spa/src/lib.rs @@ -0,0 +1,95 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![forbid(unsafe_code)] +#![deny( + clippy::all, + clippy::str_to_string, + rustdoc::missing_crate_level_docs, + rustdoc::broken_intra_doc_links +)] +#![warn(clippy::pedantic)] + +mod vite; + +use std::{future::Future, pin::Pin}; + +use camino::Utf8PathBuf; +use headers::{ContentType, HeaderMapExt}; +use http::Response; +use serde::Serialize; +use tower_service::Service; + +pub use self::vite::Manifest as ViteManifest; + +/// Service which renders an `index.html` based on the files in the manifest +#[derive(Debug, Clone)] +pub struct ViteManifestService { + manifest: Utf8PathBuf, + assets_base: Utf8PathBuf, + config: T, +} + +impl ViteManifestService { + #[must_use] + pub const fn new(manifest: Utf8PathBuf, assets_base: Utf8PathBuf, config: T) -> Self { + Self { + manifest, + assets_base, + config, + } + } +} + +impl Service for ViteManifestService +where + T: Clone + Serialize + Send + Sync + 'static, +{ + type Error = std::io::Error; + type Response = Response; + type Future = + Pin> + Send + Sync + 'static>>; + + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + + fn call(&mut self, _req: R) -> Self::Future { + let manifest = self.manifest.clone(); + let assets_base = self.assets_base.clone(); + let config = self.config.clone(); + + Box::pin(async move { + // Read the manifest from disk + let manifest = tokio::fs::read(manifest).await?; + + // Parse it + let manifest: ViteManifest = serde_json::from_slice(&manifest) + .map_err(|error| std::io::Error::new(std::io::ErrorKind::Other, error))?; + + // Render the HTML out of the manifest + let html = manifest + .render(&assets_base, &config) + .map_err(|error| std::io::Error::new(std::io::ErrorKind::Other, error))?; + + let mut response = Response::new(html); + response.headers_mut().typed_insert(ContentType::html()); + + Ok(response) + }) + } +} diff --git a/crates/spa/src/vite.rs b/crates/spa/src/vite.rs new file mode 100644 index 00000000..80c684d4 --- /dev/null +++ b/crates/spa/src/vite.rs @@ -0,0 +1,218 @@ +use std::collections::{BTreeSet, HashMap}; + +use camino::{Utf8Path, Utf8PathBuf}; +use thiserror::Error; + +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ManifestEntry { + #[allow(dead_code)] + src: Option, + + file: Utf8PathBuf, + + css: Option>, + + #[allow(dead_code)] + assets: Option>, + + #[allow(dead_code)] + is_entry: Option, + + #[allow(dead_code)] + is_dynamic_entry: Option, + + #[allow(dead_code)] + imports: Option>, + + dynamic_imports: Option>, +} + +/// Render the HTML template +fn template(head: impl Iterator, config: &impl serde::Serialize) -> String { + // This should be kept in sync with `../../../frontend/index.html` + + // Render the items to insert in the + let head: String = head.map(|f| format!(" {f}\n")).collect(); + // Serialize the config + let config = serde_json::to_string(config).expect("failed to serialize config"); + + // Script in the which manages the dark mode class on the element + let dark_mode_script = r#" + (function () { + const query = window.matchMedia("(prefers-color-scheme: dark)"); + function handleChange(e) { + if (e.matches) { + document.documentElement.classList.add("dark") + } else { + document.documentElement.classList.remove("dark") + } + } + + query.addListener(handleChange); + handleChange(query); + })(); + "#; + + format!( + r#" + + + + + matrix-authentication-service + + +{head} + +
+ +"# + ) +} + +impl ManifestEntry { + /// Get a list of items to insert in the `` + fn head<'a>(&'a self, assets_base: &'a Utf8Path) -> impl Iterator + 'a { + let css = self.css.iter().flat_map(|css| { + css.iter().map(|href| { + let href = assets_base.join(href); + format!(r#""#) + }) + }); + + let script = assets_base.join(&self.file); + let script = format!(r#""#); + + css.chain(std::iter::once(script)) + } +} + +#[derive(serde::Deserialize, Debug)] +pub struct Manifest { + #[serde(flatten)] + inner: HashMap, +} + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +enum FileType { + Script, + Stylesheet, +} + +impl FileType { + fn from_name(name: &Utf8Path) -> Option { + match name.extension() { + Some("css") => Some(Self::Stylesheet), + Some("js") => Some(Self::Script), + _ => None, + } + } +} + +#[derive(Debug, Error)] +#[error("Invalid Vite manifest")] +pub enum InvalidManifest { + #[error("No index.html")] + NoIndex, + + #[error("Can't find preloaded entry")] + CantFindPreload, + + #[error("Invalid file type")] + InvalidFileType, +} + +/// Represents an entry which should be preloaded +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +struct Preload<'name> { + name: &'name Utf8Path, + file_type: FileType, +} + +impl<'a> Preload<'a> { + /// Generate a `` tag for this entry + fn link(&self, assets_base: &Utf8Path) -> String { + let href = assets_base.join(self.name); + match self.file_type { + FileType::Stylesheet => { + format!(r#""#) + } + FileType::Script => format!( + r#""# + ), + } + } +} + +impl Manifest { + /// Render an `index.html` page + /// + /// # Errors + /// + /// Returns an error if the manifest is invalid. + pub fn render( + &self, + assets_base: &Utf8Path, + config: &impl serde::Serialize, + ) -> Result { + let entrypoint = Utf8Path::new("index.html"); + let entry = self.inner.get(entrypoint).ok_or(InvalidManifest::NoIndex)?; + + // Find the items that should be pre-loaded + let preload = self.find_preload(entrypoint)?; + let head = preload + .iter() + .map(|p| p.link(assets_base)) + .chain(entry.head(assets_base)); + + let html = template(head, config); + + Ok(html) + } + + /// Find entries to preload + fn find_preload<'a>( + &'a self, + entrypoint: &Utf8Path, + ) -> Result>, InvalidManifest> { + // TODO: we're preoading the whole tree. We should instead guess which component + // should be loaded based on the route. + let mut entries = BTreeSet::new(); + self.find_preload_rec(entrypoint, &mut entries)?; + Ok(entries) + } + + fn find_preload_rec<'a>( + &'a self, + entrypoint: &Utf8Path, + entries: &mut BTreeSet>, + ) -> Result<(), InvalidManifest> { + let entry = self + .inner + .get(entrypoint) + .ok_or(InvalidManifest::CantFindPreload)?; + let name = &entry.file; + let file_type = FileType::from_name(name).ok_or(InvalidManifest::InvalidFileType)?; + let preload = Preload { name, file_type }; + let inserted = entries.insert(preload); + + if inserted { + if let Some(css) = &entry.css { + let file_type = FileType::Stylesheet; + for name in css { + let preload = Preload { name, file_type }; + entries.insert(preload); + } + } + + if let Some(dynamic_imports) = &entry.dynamic_imports { + for import in dynamic_imports { + self.find_preload_rec(import, entries)?; + } + } + } + + Ok(()) + } +} diff --git a/docs/config.schema.json b/docs/config.schema.json index bba57e79..91e4120c 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -86,6 +86,11 @@ }, { "name": "static" + }, + { + "assets": "./frontend/dist/", + "manifest": "./frontend/dist/manifest.json", + "name": "spa" } ] }, @@ -1420,6 +1425,31 @@ ] } } + }, + { + "description": "Mount the single page app", + "type": "object", + "required": [ + "assets", + "manifest", + "name" + ], + "properties": { + "assets": { + "description": "Path to the assets to server", + "type": "string" + }, + "manifest": { + "description": "Path to the vite manifest", + "type": "string" + }, + "name": { + "type": "string", + "enum": [ + "spa" + ] + } + } } ] }, diff --git a/frontend/index.html b/frontend/index.html index 8907c2d3..92a635b1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -24,17 +24,18 @@ limitations under the License. matrix-authentication-service diff --git a/frontend/package.json b/frontend/package.json index acac4c9f..8c5b66c5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,7 @@ "generate": "relay-compiler && eslint --fix .", "lint": "relay-compiler --validate && eslint . && tsc", "relay": "relay-compiler", - "build": "npm run lint && vite build", + "build": "npm run lint && vite build --base=./", "preview": "vite preview", "test": "jest", "storybook": "storybook dev -p 6006", diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index e65ab3e0..c92fae80 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -13,7 +13,7 @@ // limitations under the License. import { lazy, Suspense } from "react"; -import { createHashRouter, Outlet, RouterProvider } from "react-router-dom"; +import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom"; import Layout from "./components/Layout"; import LoadingSpinner from "./components/LoadingSpinner"; @@ -22,36 +22,41 @@ const Home = lazy(() => import("./pages/Home")); const OAuth2Client = lazy(() => import("./pages/OAuth2Client")); const BrowserSession = lazy(() => import("./pages/BrowserSession")); -export const router = createHashRouter([ +export const router = createBrowserRouter( + [ + { + path: "/", + element: ( + + }> + + + + ), + children: [ + { + index: true, + element: , + }, + { + path: "dumb", + element: <>Hello from another dumb page., + }, + { + path: "client/:id", + element: , + }, + { + path: "session/:id", + element: , + }, + ], + }, + ], { - path: "/", - element: ( - - }> - - - - ), - children: [ - { - index: true, - element: , - }, - { - path: "dumb", - element: <>Hello from another dumb page., - }, - { - path: "client/:id", - element: , - }, - { - path: "session/:id", - element: , - }, - ], - }, -]); + basename: window.APP_CONFIG.root, + } +); const Router = () => ; diff --git a/frontend/src/config.d.ts b/frontend/src/config.d.ts new file mode 100644 index 00000000..a1864813 --- /dev/null +++ b/frontend/src/config.d.ts @@ -0,0 +1,21 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +type AppConfig = { + root: string; +}; + +interface Window { + APP_CONFIG: AppConfig; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 8d4a944e..a1e3c8ec 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -12,14 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { lazy } from "react"; +import React from "react"; import ReactDOM from "react-dom/client"; import { RelayEnvironmentProvider } from "react-relay"; import LoadingScreen from "./components/LoadingScreen"; import RelayEnvironment from "./RelayEnvironment"; - -const Router = lazy(() => import("./Router")); +import Router from "./Router"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 25a4b587..fca8eeab 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -19,6 +19,11 @@ import relay from "vite-plugin-relay"; export default defineConfig({ base: "/app/", + build: { + manifest: true, + assetsDir: "", + sourcemap: true, + }, plugins: [ react(), eslint({