Serve the SPA by the server
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
target/
|
||||
crates/*/target
|
||||
crates/*/node_modules
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
docs/
|
||||
.devcontainer/
|
||||
.git/
|
||||
|
||||
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -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"
|
||||
|
||||
32
Dockerfile
32
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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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<B>(state: &Arc<AppState>, resources: &[HttpResource]) -> Router<AppState, B>
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
20
crates/spa/Cargo.toml
Normal file
20
crates/spa/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "mas-spa"
|
||||
version = "0.1.0"
|
||||
authors = ["Quentin Gliech <quenting@element.io>"]
|
||||
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"
|
||||
32
crates/spa/src/bin/render.rs
Normal file
32
crates/spa/src/bin/render.rs
Normal file
@@ -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}");
|
||||
}
|
||||
95
crates/spa/src/lib.rs
Normal file
95
crates/spa/src/lib.rs
Normal file
@@ -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<T> {
|
||||
manifest: Utf8PathBuf,
|
||||
assets_base: Utf8PathBuf,
|
||||
config: T,
|
||||
}
|
||||
|
||||
impl<T> ViteManifestService<T> {
|
||||
#[must_use]
|
||||
pub const fn new(manifest: Utf8PathBuf, assets_base: Utf8PathBuf, config: T) -> Self {
|
||||
Self {
|
||||
manifest,
|
||||
assets_base,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R> Service<R> for ViteManifestService<T>
|
||||
where
|
||||
T: Clone + Serialize + Send + Sync + 'static,
|
||||
{
|
||||
type Error = std::io::Error;
|
||||
type Response = Response<String>;
|
||||
type Future =
|
||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + Sync + 'static>>;
|
||||
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
218
crates/spa/src/vite.rs
Normal file
218
crates/spa/src/vite.rs
Normal file
@@ -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<Utf8PathBuf>,
|
||||
|
||||
file: Utf8PathBuf,
|
||||
|
||||
css: Option<Vec<Utf8PathBuf>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
assets: Option<Vec<Utf8PathBuf>>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
is_entry: Option<bool>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
is_dynamic_entry: Option<bool>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
imports: Option<Vec<Utf8PathBuf>>,
|
||||
|
||||
dynamic_imports: Option<Vec<Utf8PathBuf>>,
|
||||
}
|
||||
|
||||
/// Render the HTML template
|
||||
fn template(head: impl Iterator<Item = String>, config: &impl serde::Serialize) -> String {
|
||||
// This should be kept in sync with `../../../frontend/index.html`
|
||||
|
||||
// Render the items to insert in the <head>
|
||||
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 <head> which manages the dark mode class on the <html> 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#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>matrix-authentication-service</title>
|
||||
<script>window.APP_CONFIG = {config};</script>
|
||||
<script>{dark_mode_script}</script>
|
||||
{head}</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>"#
|
||||
)
|
||||
}
|
||||
|
||||
impl ManifestEntry {
|
||||
/// Get a list of items to insert in the `<head>`
|
||||
fn head<'a>(&'a self, assets_base: &'a Utf8Path) -> impl Iterator<Item = String> + 'a {
|
||||
let css = self.css.iter().flat_map(|css| {
|
||||
css.iter().map(|href| {
|
||||
let href = assets_base.join(href);
|
||||
format!(r#"<link rel="stylesheet" href="{href}" />"#)
|
||||
})
|
||||
});
|
||||
|
||||
let script = assets_base.join(&self.file);
|
||||
let script = format!(r#"<script type="module" crossorigin src="{script}"></script>"#);
|
||||
|
||||
css.chain(std::iter::once(script))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct Manifest {
|
||||
#[serde(flatten)]
|
||||
inner: HashMap<Utf8PathBuf, ManifestEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum FileType {
|
||||
Script,
|
||||
Stylesheet,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
fn from_name(name: &Utf8Path) -> Option<Self> {
|
||||
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 `<link>` 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#"<link rel="preload" href="{href}" as="style" />"#)
|
||||
}
|
||||
FileType::Script => format!(
|
||||
r#"<link rel="preload" href="{href}" as="script" crossorigin="anonymous" />"#
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<String, InvalidManifest> {
|
||||
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<BTreeSet<Preload<'a>>, 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<Preload<'a>>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -24,17 +24,18 @@ limitations under the License.
|
||||
<title>matrix-authentication-service</title>
|
||||
|
||||
<script>
|
||||
window.APP_CONFIG = {root: "/app/"};
|
||||
(function () {
|
||||
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
function handleChange(e) {
|
||||
if (e.matches) {
|
||||
document.documentElement.classList.add("dark")
|
||||
function handleChange(list) {
|
||||
if (list.matches) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark")
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
|
||||
query.addListener(handleChange);
|
||||
query.addEventListener("change", handleChange);
|
||||
handleChange(query);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,7 +22,8 @@ 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: (
|
||||
@@ -51,7 +52,11 @@ export const router = createHashRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
],
|
||||
{
|
||||
basename: window.APP_CONFIG.root,
|
||||
}
|
||||
);
|
||||
|
||||
const Router = () => <RouterProvider router={router} />;
|
||||
|
||||
|
||||
21
frontend/src/config.d.ts
vendored
Normal file
21
frontend/src/config.d.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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(
|
||||
<React.StrictMode>
|
||||
|
||||
@@ -19,6 +19,11 @@ import relay from "vite-plugin-relay";
|
||||
|
||||
export default defineConfig({
|
||||
base: "/app/",
|
||||
build: {
|
||||
manifest: true,
|
||||
assetsDir: "",
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
eslint({
|
||||
|
||||
Reference in New Issue
Block a user