fix web build

This commit is contained in:
2026-10-24 19:37:04 +07:00
parent 8929e3137b
commit f829a2774d
5 changed files with 182 additions and 57 deletions
Generated
+2
View File
@@ -1366,6 +1366,7 @@ dependencies = [
name = "coop_web"
version = "2.0.0-alpha2"
dependencies = [
"assets",
"chat",
"console_error_panic_hook",
"console_log",
@@ -1377,6 +1378,7 @@ dependencies = [
"gpui-pre-platform",
"gpui-pre-web",
"instant",
"js-sys",
"log",
"nip29",
"nip4e",
+27
View File
@@ -20,12 +20,39 @@ impl AssetSource for CombinedAssets {
if let Ok(Some(data)) = Assets.load(path) {
return Ok(Some(data));
}
#[cfg(target_arch = "wasm32")]
{
// The kit bundle fetches icons over HTTP on the web; a single
// instance keeps its download cache alive across loads.
use std::sync::OnceLock;
static KIT_ASSETS: OnceLock<gpui_kit::assets::Assets> = OnceLock::new();
KIT_ASSETS
.get_or_init(gpui_kit::assets::Assets::default)
.load(path)
}
#[cfg(not(target_arch = "wasm32"))]
gpui_kit::assets::Assets.load(path)
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
let mut paths = Assets.list(path).unwrap_or_default();
#[cfg(target_arch = "wasm32")]
{
use std::sync::OnceLock;
static KIT_ASSETS: OnceLock<gpui_kit::assets::Assets> = OnceLock::new();
paths.extend(
KIT_ASSETS
.get_or_init(gpui_kit::assets::Assets::default)
.list(path)?,
);
}
#[cfg(not(target_arch = "wasm32"))]
paths.extend(gpui_kit::assets::Assets.list(path)?);
Ok(paths)
}
}
+2
View File
@@ -8,6 +8,7 @@ publish.workspace = true
crate-type = ["cdylib", "rlib"]
[dependencies]
assets = { path = "../crates/assets" }
workspace = { path = "../crates/workspace" }
ui = { path = "../crates/ui" }
state = { path = "../crates/state" }
@@ -29,6 +30,7 @@ console_error_panic_hook = "0.1"
tracing-wasm = "0.2"
console_log = "1.0"
wasm-bindgen = "0.2"
js-sys = "0.3"
universal-time = { git = "https://github.com/shadowylab/universal-time" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
+38 -17
View File
@@ -11,7 +11,8 @@ echo -e "${GREEN}Building Coop Web...${NC}"
# Get the script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$SCRIPT_DIR/.."
PROJECT_ROOT="$SCRIPT_DIR/../.."
WEB_ROOT="$SCRIPT_DIR/.."
# Parse arguments
RELEASE_FLAG=""
@@ -20,27 +21,47 @@ if [[ "$1" == "--release" ]]; then
echo -e "${YELLOW}Building in release mode${NC}"
fi
# macOS: ensure LLVM from Homebrew is available
# wasm_thread requires a nightly compiler for its atomics features
CARGO="cargo +nightly"
# Step 0: Toolchain prerequisites
echo -e "${GREEN}Step 0: Checking toolchain...${NC}"
rustup target add wasm32-unknown-unknown --toolchain nightly
if [[ "$(uname)" == "Darwin" ]]; then
echo -e "${GREEN}Detected macOS, setting up LLVM...${NC}"
if brew list llvm &>/dev/null; then
echo -e "${GREEN}LLVM already installed, skipping install${NC}"
else
echo -e "${YELLOW}LLVM not found, installing via Homebrew...${NC}"
brew install llvm
# secp256k1-sys compiles C for wasm; Apple clang has the wasm32 target and
# llvm-ar for archives comes from the rustup llvm-tools component
if ! xcrun --find clang &>/dev/null; then
echo -e "${RED}Apple clang not found. Install the Xcode command line tools: xcode-select --install${NC}"
exit 1
fi
LLVM_PATH=$(brew --prefix llvm)
export AR="${LLVM_PATH}/bin/llvm-ar"
export CC="${LLVM_PATH}/bin/clang"
echo -e "${GREEN}LLVM path: ${LLVM_PATH}${NC}"
rustup component add llvm-tools-preview --toolchain nightly
RUSTUP_HOME=$(rustup show home)
if [[ "$(uname -m)" == "arm64" ]]; then
HOST_ARCH="aarch64-apple-darwin"
else
HOST_ARCH="x86_64-apple-darwin"
fi
LLVM_BIN="$RUSTUP_HOME/toolchains/nightly-$HOST_ARCH/lib/rustlib/$HOST_ARCH/bin"
export CC_wasm32_unknown_unknown=clang
export AR_wasm32_unknown_unknown="$LLVM_BIN/llvm-ar"
echo -e "${GREEN}Using Apple clang + rustup llvm-ar for wasm32 C code${NC}"
fi
# Match wasm-bindgen-cli to the version in Cargo.lock
LOCKED_BINDGEN=$(grep -A1 '^name = "wasm-bindgen"$' "$PROJECT_ROOT/Cargo.lock" | grep version | cut -d'"' -f2)
if [[ "$(wasm-bindgen --version 2>/dev/null | awk '{print $2}')" != "$LOCKED_BINDGEN" ]]; then
echo -e "${YELLOW}Installing wasm-bindgen-cli $LOCKED_BINDGEN to match Cargo.lock...${NC}"
cargo install -f wasm-bindgen-cli --version "$LOCKED_BINDGEN"
fi
# Step 1: Build WASM
echo -e "${GREEN}Step 1: Building WASM...${NC}"
cd "$PROJECT_ROOT"
export CARGO_TARGET_DIR="$PROJECT_ROOT/target"
RUSTFLAGS='-C target-feature=+bulk-memory -C link-arg=--max-memory=4294967296' \
cargo build --target wasm32-unknown-unknown $RELEASE_FLAG
$CARGO build -p coop_web --target wasm32-unknown-unknown $RELEASE_FLAG
# Determine the build directory
if [[ "$RELEASE_FLAG" == "--release" ]]; then
@@ -61,12 +82,12 @@ fi
# Step 2: Generate JavaScript bindings
echo -e "${GREEN}Step 2: Generating JavaScript bindings...${NC}"
wasm-bindgen "$WASM_PATH" \
--out-dir "$PROJECT_ROOT/www/wasm" \
--out-dir "$WEB_ROOT/www/wasm" \
--target web \
--no-typescript
echo -e "${GREEN}✓ Build completed successfully!${NC}"
echo -e "${YELLOW}Next steps:${NC}"
echo -e " cd www"
echo -e " cd web/www"
echo -e " deno install"
echo -e " deno run dev"
echo -e " deno task dev"
+94 -21
View File
@@ -1,22 +1,33 @@
use std::cell::RefCell;
use std::rc::Rc;
use gpui::*;
use universal_time::{Instant, MonotonicClock, SystemTime, WallClock, define_time_provider};
use wasm_bindgen::prelude::*;
struct CustomTimeProvider;
thread_local! {
/// Dropping the handle releases the app, so the page's lifetime must own it.
static APPLICATION: RefCell<Option<ApplicationHandle>> = const { RefCell::new(None) };
}
impl WallClock for CustomTimeProvider {
/// Serves `Date.now()` as the wall clock and `performance.now()` as the monotonic clock.
struct BrowserTimeProvider;
impl WallClock for BrowserTimeProvider {
fn system_time(&self) -> SystemTime {
SystemTime::from_unix_duration(instant::Duration::from_secs(0))
SystemTime::from_unix_duration(core::time::Duration::from_secs_f64(
js_sys::Date::now() / 1000.0,
))
}
}
impl MonotonicClock for CustomTimeProvider {
impl MonotonicClock for BrowserTimeProvider {
fn instant(&self) -> Instant {
Instant::from_ticks(instant::Duration::from_secs(0))
Instant::from_ticks(core::time::Duration::from_secs_f64(instant::now() / 1000.0))
}
}
define_time_provider!(CustomTimeProvider);
define_time_provider!(BrowserTimeProvider);
#[wasm_bindgen]
pub fn run() -> Result<(), JsValue> {
@@ -28,30 +39,42 @@ pub fn run() -> Result<(), JsValue> {
// Also initialize tracing for WASM
tracing_wasm::set_as_global_default();
#[cfg(target_family = "wasm")]
// Sets the panic hook and GPUI's console logger
gpui_platform::web_init();
#[cfg(not(target_family = "wasm"))]
let app = gpui_platform::application();
// A single-threaded platform that draws emoji and CJK graphemes missing
// from the bundled fonts with the browser's own fonts, plus a fetch-based
// HTTP client so remote images and content work like on desktop
let platform = std::rc::Rc::new(gpui_web::WebPlatform::new_with_backend_and_font_fallback(
false,
gpui_web::WebBackendPreference::Auto,
gpui_web::CanvasFontFallback::EmojiAndCjk,
));
let http_client = std::sync::Arc::new(platform.fetch_http_client());
let app = Application::with_platform(platform)
.with_http_client(http_client)
.with_assets(assets::CombinedAssets);
#[cfg(target_family = "wasm")]
let app = {
let app = gpui_platform::single_threaded_web();
// Temporary fix: intentionally leak the `Rc<AppCell>` to keep the application alive
struct WasmApplication(std::rc::Rc<AppCell>);
let wasm_app = unsafe { std::mem::transmute::<Application, WasmApplication>(app) };
std::mem::forget(wasm_app.0.clone());
unsafe { std::mem::transmute::<WasmApplication, Application>(wasm_app) }
};
app.run(|cx| {
APPLICATION.with(|application| {
*application.borrow_mut() = Some(app.run_embedded(|cx| {
// Initialize the framework before anything renders
gpui_kit::init(cx);
// The web text system starts with no fonts, and GPUI panics when
// it lays out text in a family that does not exist. Inter is both
// bundled and named on the theme (including both mode configs, so
// theme reloads keep it) before any text is laid out
load_embedded_fonts(cx);
apply_web_fonts(cx);
// Initialize settings
settings::init(cx);
// There is no native theme on the web; apply the persisted
// appearance directly, then the user's theme tuning
apply_appearance(settings::persisted_appearance_mode(), cx);
settings::AppSettings::apply_theme_style(cx);
// Initialize the nostr client
state::init(cx, None);
@@ -75,7 +98,57 @@ pub fn run() -> Result<(), JsValue> {
.expect("Failed to open window. Please restart the application.");
cx.activate(true);
}));
});
Ok(())
}
/// Register every bundled font with the text system before any text is laid out
fn load_embedded_fonts(cx: &mut App) {
let font_paths = assets::Assets::iter()
.filter(|path| path.starts_with("fonts/") && path.ends_with(".ttf"))
.collect::<Vec<_>>();
let fonts = font_paths
.iter()
.filter_map(|path| assets::Assets::get(path.as_ref()))
.map(|font| font.data)
.collect::<Vec<_>>();
cx.text_system()
.add_fonts(fonts)
.expect("Failed to load bundled fonts");
}
/// Name Inter on the theme and both mode configs so every text run renders in
/// it and theme reloads keep it. Without an explicit family the theme falls
/// back to `.SystemUIFont`, which the web platform resolves to a family that
/// is not bundled
fn apply_web_fonts(cx: &mut App) {
use gpui_kit::component::Theme;
const UI_FONT: &str = "Inter";
Theme::update(cx, |theme| {
theme.font_family = UI_FONT.into();
theme.mono_font_family = UI_FONT.into();
for config in [&mut theme.light_theme, &mut theme.dark_theme] {
let config = Rc::make_mut(config);
config.font_family = Some(UI_FONT.into());
config.mono_font_family = Some(UI_FONT.into());
}
});
}
/// Honor the appearance mode against the default palettes
fn apply_appearance(appearance: settings::AppearanceMode, cx: &mut App) {
use gpui_kit::component::{Theme, ThemeMode};
match appearance {
settings::AppearanceMode::System => Theme::sync_system_appearance(None, cx),
settings::AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
settings::AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
}
}