fix web build
This commit is contained in:
Generated
+2
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
+113
-40
@@ -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,54 +39,116 @@ 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();
|
||||
APPLICATION.with(|application| {
|
||||
*application.borrow_mut() = Some(app.run_embedded(|cx| {
|
||||
// Initialize the framework before anything renders
|
||||
gpui_kit::init(cx);
|
||||
|
||||
// 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) }
|
||||
};
|
||||
// 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);
|
||||
|
||||
app.run(|cx| {
|
||||
// Initialize the framework before anything renders
|
||||
gpui_kit::init(cx);
|
||||
// Initialize settings
|
||||
settings::init(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);
|
||||
// Initialize the nostr client
|
||||
state::init(cx, None);
|
||||
|
||||
// Initialize person registry
|
||||
person::init(cx);
|
||||
// Initialize person registry
|
||||
person::init(cx);
|
||||
|
||||
// Initialize device signer
|
||||
//
|
||||
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||
nip4e::init(cx);
|
||||
// Initialize device signer
|
||||
//
|
||||
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||
nip4e::init(cx);
|
||||
|
||||
// Initialize app registry
|
||||
chat::init(cx);
|
||||
// Initialize app registry
|
||||
chat::init(cx);
|
||||
|
||||
// Initialize groups registry
|
||||
nip29::init(cx);
|
||||
// Initialize groups registry
|
||||
nip29::init(cx);
|
||||
|
||||
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
|
||||
workspace::init(window, cx)
|
||||
})
|
||||
.expect("Failed to open window. Please restart the application.");
|
||||
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
|
||||
workspace::init(window, cx)
|
||||
})
|
||||
.expect("Failed to open window. Please restart the application.");
|
||||
|
||||
cx.activate(true);
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user