fix web build
This commit is contained in:
Generated
+2
@@ -1366,6 +1366,7 @@ dependencies = [
|
|||||||
name = "coop_web"
|
name = "coop_web"
|
||||||
version = "2.0.0-alpha2"
|
version = "2.0.0-alpha2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"assets",
|
||||||
"chat",
|
"chat",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
@@ -1377,6 +1378,7 @@ dependencies = [
|
|||||||
"gpui-pre-platform",
|
"gpui-pre-platform",
|
||||||
"gpui-pre-web",
|
"gpui-pre-web",
|
||||||
"instant",
|
"instant",
|
||||||
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"nip29",
|
"nip29",
|
||||||
"nip4e",
|
"nip4e",
|
||||||
|
|||||||
@@ -20,12 +20,39 @@ impl AssetSource for CombinedAssets {
|
|||||||
if let Ok(Some(data)) = Assets.load(path) {
|
if let Ok(Some(data)) = Assets.load(path) {
|
||||||
return Ok(Some(data));
|
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)
|
gpui_kit::assets::Assets.load(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||||
let mut paths = Assets.list(path).unwrap_or_default();
|
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)?);
|
paths.extend(gpui_kit::assets::Assets.list(path)?);
|
||||||
|
|
||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ publish.workspace = true
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
assets = { path = "../crates/assets" }
|
||||||
workspace = { path = "../crates/workspace" }
|
workspace = { path = "../crates/workspace" }
|
||||||
ui = { path = "../crates/ui" }
|
ui = { path = "../crates/ui" }
|
||||||
state = { path = "../crates/state" }
|
state = { path = "../crates/state" }
|
||||||
@@ -29,6 +30,7 @@ console_error_panic_hook = "0.1"
|
|||||||
tracing-wasm = "0.2"
|
tracing-wasm = "0.2"
|
||||||
console_log = "1.0"
|
console_log = "1.0"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
js-sys = "0.3"
|
||||||
universal-time = { git = "https://github.com/shadowylab/universal-time" }
|
universal-time = { git = "https://github.com/shadowylab/universal-time" }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
|||||||
+38
-17
@@ -11,7 +11,8 @@ echo -e "${GREEN}Building Coop Web...${NC}"
|
|||||||
|
|
||||||
# Get the script directory
|
# Get the script directory
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
PROJECT_ROOT="$SCRIPT_DIR/.."
|
PROJECT_ROOT="$SCRIPT_DIR/../.."
|
||||||
|
WEB_ROOT="$SCRIPT_DIR/.."
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
RELEASE_FLAG=""
|
RELEASE_FLAG=""
|
||||||
@@ -20,27 +21,47 @@ if [[ "$1" == "--release" ]]; then
|
|||||||
echo -e "${YELLOW}Building in release mode${NC}"
|
echo -e "${YELLOW}Building in release mode${NC}"
|
||||||
fi
|
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
|
if [[ "$(uname)" == "Darwin" ]]; then
|
||||||
echo -e "${GREEN}Detected macOS, setting up LLVM...${NC}"
|
# secp256k1-sys compiles C for wasm; Apple clang has the wasm32 target and
|
||||||
if brew list llvm &>/dev/null; then
|
# llvm-ar for archives comes from the rustup llvm-tools component
|
||||||
echo -e "${GREEN}LLVM already installed, skipping install${NC}"
|
if ! xcrun --find clang &>/dev/null; then
|
||||||
else
|
echo -e "${RED}Apple clang not found. Install the Xcode command line tools: xcode-select --install${NC}"
|
||||||
echo -e "${YELLOW}LLVM not found, installing via Homebrew...${NC}"
|
exit 1
|
||||||
brew install llvm
|
|
||||||
fi
|
fi
|
||||||
LLVM_PATH=$(brew --prefix llvm)
|
rustup component add llvm-tools-preview --toolchain nightly
|
||||||
export AR="${LLVM_PATH}/bin/llvm-ar"
|
|
||||||
export CC="${LLVM_PATH}/bin/clang"
|
RUSTUP_HOME=$(rustup show home)
|
||||||
echo -e "${GREEN}LLVM path: ${LLVM_PATH}${NC}"
|
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
|
fi
|
||||||
|
|
||||||
# Step 1: Build WASM
|
# Step 1: Build WASM
|
||||||
echo -e "${GREEN}Step 1: Building WASM...${NC}"
|
echo -e "${GREEN}Step 1: Building WASM...${NC}"
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
export CARGO_TARGET_DIR="$PROJECT_ROOT/target"
|
export CARGO_TARGET_DIR="$PROJECT_ROOT/target"
|
||||||
RUSTFLAGS='-C target-feature=+bulk-memory -C link-arg=--max-memory=4294967296' \
|
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
|
# Determine the build directory
|
||||||
if [[ "$RELEASE_FLAG" == "--release" ]]; then
|
if [[ "$RELEASE_FLAG" == "--release" ]]; then
|
||||||
@@ -61,12 +82,12 @@ fi
|
|||||||
# Step 2: Generate JavaScript bindings
|
# Step 2: Generate JavaScript bindings
|
||||||
echo -e "${GREEN}Step 2: Generating JavaScript bindings...${NC}"
|
echo -e "${GREEN}Step 2: Generating JavaScript bindings...${NC}"
|
||||||
wasm-bindgen "$WASM_PATH" \
|
wasm-bindgen "$WASM_PATH" \
|
||||||
--out-dir "$PROJECT_ROOT/www/wasm" \
|
--out-dir "$WEB_ROOT/www/wasm" \
|
||||||
--target web \
|
--target web \
|
||||||
--no-typescript
|
--no-typescript
|
||||||
|
|
||||||
echo -e "${GREEN}✓ Build completed successfully!${NC}"
|
echo -e "${GREEN}✓ Build completed successfully!${NC}"
|
||||||
echo -e "${YELLOW}Next steps:${NC}"
|
echo -e "${YELLOW}Next steps:${NC}"
|
||||||
echo -e " cd www"
|
echo -e " cd web/www"
|
||||||
echo -e " deno install"
|
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 gpui::*;
|
||||||
use universal_time::{Instant, MonotonicClock, SystemTime, WallClock, define_time_provider};
|
use universal_time::{Instant, MonotonicClock, SystemTime, WallClock, define_time_provider};
|
||||||
use wasm_bindgen::prelude::*;
|
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 {
|
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 {
|
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]
|
#[wasm_bindgen]
|
||||||
pub fn run() -> Result<(), JsValue> {
|
pub fn run() -> Result<(), JsValue> {
|
||||||
@@ -28,54 +39,116 @@ pub fn run() -> Result<(), JsValue> {
|
|||||||
// Also initialize tracing for WASM
|
// Also initialize tracing for WASM
|
||||||
tracing_wasm::set_as_global_default();
|
tracing_wasm::set_as_global_default();
|
||||||
|
|
||||||
#[cfg(target_family = "wasm")]
|
// Sets the panic hook and GPUI's console logger
|
||||||
gpui_platform::web_init();
|
gpui_platform::web_init();
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
// A single-threaded platform that draws emoji and CJK graphemes missing
|
||||||
let app = gpui_platform::application();
|
// 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")]
|
APPLICATION.with(|application| {
|
||||||
let app = {
|
*application.borrow_mut() = Some(app.run_embedded(|cx| {
|
||||||
let app = gpui_platform::single_threaded_web();
|
// Initialize the framework before anything renders
|
||||||
|
gpui_kit::init(cx);
|
||||||
|
|
||||||
// Temporary fix: intentionally leak the `Rc<AppCell>` to keep the application alive
|
// The web text system starts with no fonts, and GPUI panics when
|
||||||
struct WasmApplication(std::rc::Rc<AppCell>);
|
// it lays out text in a family that does not exist. Inter is both
|
||||||
let wasm_app = unsafe { std::mem::transmute::<Application, WasmApplication>(app) };
|
// bundled and named on the theme (including both mode configs, so
|
||||||
std::mem::forget(wasm_app.0.clone());
|
// theme reloads keep it) before any text is laid out
|
||||||
unsafe { std::mem::transmute::<WasmApplication, Application>(wasm_app) }
|
load_embedded_fonts(cx);
|
||||||
};
|
apply_web_fonts(cx);
|
||||||
|
|
||||||
app.run(|cx| {
|
// Initialize settings
|
||||||
// Initialize the framework before anything renders
|
settings::init(cx);
|
||||||
gpui_kit::init(cx);
|
|
||||||
|
|
||||||
// Initialize settings
|
// There is no native theme on the web; apply the persisted
|
||||||
settings::init(cx);
|
// 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
|
// Initialize the nostr client
|
||||||
state::init(cx, None);
|
state::init(cx, None);
|
||||||
|
|
||||||
// Initialize person registry
|
// Initialize person registry
|
||||||
person::init(cx);
|
person::init(cx);
|
||||||
|
|
||||||
// Initialize device signer
|
// Initialize device signer
|
||||||
//
|
//
|
||||||
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
nip4e::init(cx);
|
nip4e::init(cx);
|
||||||
|
|
||||||
// Initialize app registry
|
// Initialize app registry
|
||||||
chat::init(cx);
|
chat::init(cx);
|
||||||
|
|
||||||
// Initialize groups registry
|
// Initialize groups registry
|
||||||
nip29::init(cx);
|
nip29::init(cx);
|
||||||
|
|
||||||
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
|
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
|
||||||
workspace::init(window, cx)
|
workspace::init(window, cx)
|
||||||
})
|
})
|
||||||
.expect("Failed to open window. Please restart the application.");
|
.expect("Failed to open window. Please restart the application.");
|
||||||
|
|
||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
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