mirror of
https://github.com/Xevion/banner.git
synced 2026-01-31 00:23:31 -06:00
feat: add conditional asset embedding with dev/prod build separation
- Add embed-assets feature flag to make rust-embed/mime_guess optional - Update Justfile with backend-dev command for development (no embedded assets) - Add CORS middleware when assets not embedded (for Vite proxy) - Improve db recipe with Bun script supporting start/reset/rm commands
This commit is contained in:
+6
-2
@@ -4,6 +4,10 @@ version = "0.3.4"
|
||||
edition = "2024"
|
||||
default-run = "banner"
|
||||
|
||||
[features]
|
||||
default = ["embed-assets"]
|
||||
embed-assets = ["dep:rust-embed", "dep:mime_guess"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.99"
|
||||
async-trait = "0.1"
|
||||
@@ -46,8 +50,8 @@ once_cell = "1.21.3"
|
||||
serde_path_to_error = "0.1.17"
|
||||
num-format = "0.4.4"
|
||||
tower-http = { version = "0.6.0", features = ["fs", "cors", "trace", "timeout"] }
|
||||
rust-embed = { version = "8.0", features = ["debug-embed", "include-exclude"] }
|
||||
mime_guess = "2.0"
|
||||
rust-embed = { version = "8.0", features = ["include-exclude"], optional = true }
|
||||
mime_guess = { version = "2.0", optional = true }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
rapidhash = "4.1.0"
|
||||
yansi = "1.0.1"
|
||||
|
||||
@@ -22,36 +22,72 @@ format-check:
|
||||
bun run --cwd web format:check
|
||||
|
||||
# Start PostgreSQL in Docker and update .env with connection string
|
||||
db:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Commands: start (default), reset, rm
|
||||
[script("bun")]
|
||||
db cmd="start":
|
||||
const fs = await import("fs/promises");
|
||||
const { spawnSync } = await import("child_process");
|
||||
|
||||
# Find available port
|
||||
PORT=$(shuf -i 49152-65535 -n 1)
|
||||
while ss -tlnp 2>/dev/null | grep -q ":$PORT "; do
|
||||
PORT=$(shuf -i 49152-65535 -n 1)
|
||||
done
|
||||
const NAME = "banner-postgres";
|
||||
const USER = "banner";
|
||||
const PASS = "banner";
|
||||
const DB = "banner";
|
||||
const PORT = "59489";
|
||||
const ENV_FILE = ".env";
|
||||
const CMD = "{{cmd}}";
|
||||
|
||||
# Start PostgreSQL container
|
||||
docker run -d \
|
||||
--name banner-postgres \
|
||||
-e POSTGRES_PASSWORD=banner \
|
||||
-e POSTGRES_USER=banner \
|
||||
-e POSTGRES_DB=banner \
|
||||
-p "$PORT:5432" \
|
||||
postgres:17-alpine
|
||||
const run = (args) => spawnSync("docker", args, { encoding: "utf8" });
|
||||
const getContainer = () => {
|
||||
const res = run(["ps", "-a", "--filter", `name=^${NAME}$`, "--format", "json"]);
|
||||
return res.stdout.trim() ? JSON.parse(res.stdout) : null;
|
||||
};
|
||||
|
||||
# Update .env file
|
||||
DB_URL="postgresql://banner:banner@localhost:$PORT/banner"
|
||||
if [ -f .env ]; then
|
||||
sed -i.bak "s|^DATABASE_URL=.*|DATABASE_URL=$DB_URL|" .env
|
||||
else
|
||||
echo "DATABASE_URL=$DB_URL" > .env
|
||||
fi
|
||||
const updateEnv = async () => {
|
||||
const url = `postgresql://${USER}:${PASS}@localhost:${PORT}/${DB}`;
|
||||
try {
|
||||
let content = await fs.readFile(ENV_FILE, "utf8");
|
||||
content = content.includes("DATABASE_URL=")
|
||||
? content.replace(/DATABASE_URL=.*$/m, `DATABASE_URL=${url}`)
|
||||
: content.trim() + `\nDATABASE_URL=${url}\n`;
|
||||
await fs.writeFile(ENV_FILE, content);
|
||||
} catch {
|
||||
await fs.writeFile(ENV_FILE, `DATABASE_URL=${url}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
echo "PostgreSQL started on port $PORT"
|
||||
echo "DATABASE_URL=$DB_URL"
|
||||
echo "Run: sqlx migrate run"
|
||||
const create = () => {
|
||||
run(["run", "-d", "--name", NAME, "-e", `POSTGRES_USER=${USER}`,
|
||||
"-e", `POSTGRES_PASSWORD=${PASS}`, "-e", `POSTGRES_DB=${DB}`,
|
||||
"-p", `${PORT}:5432`, "postgres:17-alpine"]);
|
||||
console.log("created");
|
||||
};
|
||||
|
||||
const container = getContainer();
|
||||
|
||||
if (CMD === "rm") {
|
||||
if (!container) process.exit(0);
|
||||
run(["stop", NAME]);
|
||||
run(["rm", NAME]);
|
||||
console.log("removed");
|
||||
} else if (CMD === "reset") {
|
||||
if (!container) create();
|
||||
else {
|
||||
run(["exec", NAME, "psql", "-U", USER, "-d", "postgres", "-c", `DROP DATABASE IF EXISTS ${DB}`]);
|
||||
run(["exec", NAME, "psql", "-U", USER, "-d", "postgres", "-c", `CREATE DATABASE ${DB}`]);
|
||||
console.log("reset");
|
||||
}
|
||||
await updateEnv();
|
||||
} else {
|
||||
if (!container) {
|
||||
create();
|
||||
} else if (container.State !== "running") {
|
||||
run(["start", NAME]);
|
||||
console.log("started");
|
||||
} else {
|
||||
console.log("running");
|
||||
}
|
||||
await updateEnv();
|
||||
}
|
||||
|
||||
# Auto-reloading frontend server
|
||||
frontend:
|
||||
@@ -61,10 +97,14 @@ frontend:
|
||||
build-frontend:
|
||||
bun run --cwd web build
|
||||
|
||||
# Auto-reloading backend server
|
||||
# Auto-reloading backend server (with embedded assets)
|
||||
backend *ARGS:
|
||||
bacon --headless run -- -- {{ARGS}}
|
||||
|
||||
# Auto-reloading backend server (no embedded assets, for dev proxy mode)
|
||||
backend-dev *ARGS:
|
||||
bacon --headless run -- --no-default-features -- {{ARGS}}
|
||||
|
||||
# Production build
|
||||
build:
|
||||
bun run --cwd web build
|
||||
@@ -74,6 +114,6 @@ build:
|
||||
dev-build *ARGS='--services web --tracing pretty': build-frontend
|
||||
bacon --headless run -- --profile dev-release -- {{ARGS}}
|
||||
|
||||
# Auto-reloading development build for both frontend and backend
|
||||
# Auto-reloading development build: Vite frontend + backend (no embedded assets, proxies to Vite)
|
||||
[parallel]
|
||||
dev *ARGS='--services web,bot': frontend (backend ARGS)
|
||||
dev *ARGS='--services web,bot': frontend (backend-dev ARGS)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Web API module for the banner application.
|
||||
|
||||
#[cfg(feature = "embed-assets")]
|
||||
pub mod assets;
|
||||
pub mod routes;
|
||||
|
||||
|
||||
+24
-12
@@ -4,25 +4,29 @@ use axum::{
|
||||
Router,
|
||||
body::Body,
|
||||
extract::{Request, State},
|
||||
http::{HeaderMap, HeaderValue, StatusCode, Uri},
|
||||
response::{Html, IntoResponse, Json, Response},
|
||||
response::{Json, Response},
|
||||
routing::get,
|
||||
};
|
||||
#[cfg(feature = "embed-assets")]
|
||||
use axum::{
|
||||
http::{HeaderMap, HeaderValue, StatusCode, Uri},
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
#[cfg(feature = "embed-assets")]
|
||||
use http::header;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
use tower_http::timeout::TimeoutLayer;
|
||||
use tower_http::{
|
||||
classify::ServerErrorsFailureClass,
|
||||
cors::{Any, CorsLayer},
|
||||
trace::TraceLayer,
|
||||
};
|
||||
#[cfg(not(feature = "embed-assets"))]
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::{classify::ServerErrorsFailureClass, timeout::TimeoutLayer, trace::TraceLayer};
|
||||
use tracing::{Span, debug, info, warn};
|
||||
|
||||
#[cfg(feature = "embed-assets")]
|
||||
use crate::web::assets::{WebAssets, get_asset_metadata_cached};
|
||||
|
||||
/// Set appropriate caching headers based on asset type
|
||||
#[cfg(feature = "embed-assets")]
|
||||
fn set_caching_headers(response: &mut Response, path: &str, etag: &str) {
|
||||
let headers = response.headers_mut();
|
||||
|
||||
@@ -72,15 +76,21 @@ pub fn create_router(state: BannerState) -> Router {
|
||||
|
||||
let mut router = Router::new().nest("/api", api_router);
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
// When embed-assets feature is enabled, serve embedded static assets
|
||||
#[cfg(feature = "embed-assets")]
|
||||
{
|
||||
router = router.fallback(fallback);
|
||||
}
|
||||
|
||||
// Without embed-assets, enable CORS for dev proxy to Vite
|
||||
#[cfg(not(feature = "embed-assets"))]
|
||||
{
|
||||
router = router.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any),
|
||||
)
|
||||
} else {
|
||||
router = router.fallback(fallback);
|
||||
);
|
||||
}
|
||||
|
||||
router.layer((
|
||||
@@ -131,6 +141,7 @@ pub fn create_router(state: BannerState) -> Router {
|
||||
}
|
||||
|
||||
/// Handler that extracts request information for caching
|
||||
#[cfg(feature = "embed-assets")]
|
||||
async fn fallback(request: Request) -> Response {
|
||||
let uri = request.uri().clone();
|
||||
let headers = request.headers().clone();
|
||||
@@ -139,6 +150,7 @@ async fn fallback(request: Request) -> Response {
|
||||
|
||||
/// Handles SPA routing by serving index.html for non-API, non-asset requests
|
||||
/// This version includes HTTP caching headers and ETag support
|
||||
#[cfg(feature = "embed-assets")]
|
||||
async fn handle_spa_fallback_with_headers(uri: Uri, request_headers: HeaderMap) -> Response {
|
||||
let path = uri.path().trim_start_matches('/');
|
||||
|
||||
|
||||
+26
-26
@@ -8,52 +8,52 @@
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths: "/";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to: "/";
|
||||
id: "__root__" | "/";
|
||||
fileRoutesById: FileRoutesById;
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute;
|
||||
IndexRoute: typeof IndexRoute
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
};
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
Reference in New Issue
Block a user