feat: add initial support web via wasm (#35)

Reviewed-on: #35
This commit was merged in pull request #35.
This commit is contained in:
2026-07-25 03:43:51 +00:00
parent 4b57a1d2a6
commit b518c729f6
57 changed files with 1550 additions and 1463 deletions

View File

@@ -5,6 +5,7 @@ edition.workspace = true
publish.workspace = true
[dependencies]
workspace = { path = "../crates/workspace" }
assets = { path = "../crates/assets" }
ui = { path = "../crates/ui" }
theme = { path = "../crates/theme" }
@@ -14,29 +15,21 @@ device = { path = "../crates/device" }
chat = { path = "../crates/chat" }
settings = { path = "../crates/settings" }
person = { path = "../crates/person" }
relay_auth = { path = "../crates/relay_auth" }
gpui.workspace = true
gpui_platform.workspace = true
gpui_tokio.workspace = true
gpui_web = { git = "https://github.com/zed-industries/zed" }
nostr-connect.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
itertools.workspace = true
log.workspace = true
smallvec.workspace = true
smol.workspace = true
futures.workspace = true
oneshot.workspace = true
webbrowser.workspace = true
tracing-subscriber.workspace = true
console_error_panic_hook = "0.1"
tracing-wasm = "0.2"
console_log = "1.0"
wasm-bindgen = "0.2"
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom_0_2 = { package = "getrandom", version = "0.2", features = ["js"] }
getrandom_0_3 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
getrandom_0_4 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
tokio = { version = "1", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] }
errno = { version = "0.3.14", default-features = false }

46
web/Makefile Normal file
View File

@@ -0,0 +1,46 @@
.PHONY: help dev build-wasm build-web build clean install
help: ## Show help information
@echo "Coop - Available commands:"
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
install: ## Install all dependencies
@echo "Checking Rust WASM target..."
@rustup target add wasm32-unknown-unknown || true
@echo "Checking wasm-bindgen-cli..."
@cargo install wasm-bindgen-cli || true
@echo "Installing frontend dependencies..."
@cd www && deno install
build-wasm: ## Build WASM (release mode)
@./script/build-wasm.sh --release
build-wasm-dev: ## Build WASM (debug mode)
@./script/build-wasm.sh
build-web: ## Build frontend
@cd www && deno task build
build-web-prod: ## Build frontend for production (GitHub Pages)
@cd www && deno install && NODE_ENV=production deno task build
build: build-wasm build-web ## Build complete project (WASM + frontend)
build-prod: build-wasm build-web-prod ## Build complete project for production
dev: build-wasm-dev ## Start development server
@cd www && deno install && deno task dev
preview: ## Preview production build
@cd www && deno task preview
clean: ## Clean build artifacts
@echo "Cleaning build artifacts..."
@rm -rf www/dist
@rm -rf www/src/wasm/*.js www/src/wasm/*.wasm
@cargo clean
watch-wasm: ## Watch Rust code changes and auto-rebuild WASM
@echo "Watching WASM changes..."
@cargo watch -x 'build --target wasm32-unknown-unknown' -s './script/build-wasm.sh'

4
web/rust-toolchain.toml Normal file
View File

@@ -0,0 +1,4 @@
[toolchain]
channel = "nightly"
components = ["rustfmt", "clippy"]
targets = ["wasm32-unknown-unknown"]

56
web/script/build-wasm.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}Building Coop Web...${NC}"
# Get the script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$SCRIPT_DIR/.."
# Parse arguments
RELEASE_FLAG=""
if [[ "$1" == "--release" ]]; then
RELEASE_FLAG="--release"
echo -e "${YELLOW}Building in release mode${NC}"
fi
# Step 1: Build WASM
echo -e "${GREEN}Step 1: Building WASM...${NC}"
cd "$PROJECT_ROOT"
cargo build --target wasm32-unknown-unknown $RELEASE_FLAG
# Determine the build directory
if [[ "$RELEASE_FLAG" == "--release" ]]; then
BUILD_MODE="release"
else
BUILD_MODE="debug"
fi
# WASM file is in workspace target directory
WORKSPACE_ROOT="$PROJECT_ROOT/../.."
WASM_PATH="$WORKSPACE_ROOT/target/wasm32-unknown-unknown/$BUILD_MODE/coop_web.wasm"
# Check if WASM file exists
if [[ ! -f "$WASM_PATH" ]]; then
echo -e "${RED}Error: WASM file not found at: $WASM_PATH${NC}"
exit 1
fi
# Step 2: Generate JavaScript bindings
echo -e "${GREEN}Step 2: Generating JavaScript bindings...${NC}"
wasm-bindgen "$WASM_PATH" \
--out-dir "$PROJECT_ROOT/www/src/wasm" \
--target web \
--no-typescript
echo -e "${GREEN}✓ Build completed successfully!${NC}"
echo -e "${YELLOW}Next steps:${NC}"
echo -e " cd www"
echo -e " deno install"
echo -e " deno run dev"

View File

@@ -1,4 +1,5 @@
use gpui::*;
use ui::Root;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
@@ -14,13 +15,39 @@ pub fn run() -> Result<(), JsValue> {
#[cfg(target_family = "wasm")]
gpui_platform::web_init();
#[cfg(not(target_family = "wasm"))]
let app = gpui_platform::application();
gpui_platform::application().run(|cx| {
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| {
// Initialize components
ui::init(cx);
#[cfg(target_family = "wasm")]
let app = gpui_platform::single_threaded_web();
// Initialize theme registry
theme::init(cx);
app.run(|_cx| {});
// Initialize settings
settings::init(window, cx);
// Initialize the nostr client
state::init(window, cx);
// Initialize person registry
person::init(window, cx);
// Initialize device signer
//
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
device::init(window, cx);
// Initialize app registry
chat::init(window, cx);
// Root Entity
Root::new(workspace::init(window, cx).into(), window, cx)
})
})
.expect("Failed to open window. Please restart the application.");
cx.activate(true);
});
Ok(())
}

View File

@@ -72,6 +72,6 @@
<p>Loading Coop...</p>
</div>
</div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>

35
web/src/www/main.js Normal file
View File

@@ -0,0 +1,35 @@
async function init() {
const loadingEl = document.getElementById('loading');
const appEl = document.getElementById('app');
try {
// Import the WASM module
const wasm = await import('./wasm/coop_web.js');
await wasm.default();
// Initialize the story gallery
await wasm.run();
// Hide loading indicator
if (appEl) {
appEl.remove();
}
} catch (error) {
console.error('Failed to initialize:', error);
// Show error message
if (loadingEl) {
loadingEl.innerHTML = `
<div class="error">
<h2>Failed to load the application</h2>
<p>${error.message || error}</p>
<p style="margin-top: 10px; font-size: 14px;">
Please check the console for more details.
</p>
</div>
`;
}
}
}
init();

17
web/src/www/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "coop-web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"wasm": "cargo build --target wasm32-unknown-unknown --release && wasm-bindgen ../../../target/wasm32-unknown-unknown/release/gpui_component_story_web.wasm --out-dir ./src/wasm --target web --no-typescript"
},
"devDependencies": {
"vite": "^8",
"vite-plugin-static-copy": "^3.2.0",
"vite-plugin-wasm": "^3.3.0"
}
}

View File

@@ -0,0 +1,75 @@
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";
import { viteStaticCopy } from "vite-plugin-static-copy";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
plugins: [
wasm(),
viteStaticCopy({
targets: [
{
src: path.resolve(__dirname, "../../../assets/icons"),
dest: "assets",
},
],
}),
{
name: "serve-assets",
configureServer(server) {
server.middlewares.use(
"/coop/assets",
(req, res, next) => {
const assetsPath = path.resolve(__dirname, "../../../assets");
const filePath = path.join(
assetsPath,
req.url.replace("/assets", ""),
);
// Try to serve the file
import("fs").then(({ default: fs }) => {
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
res.setHeader("Access-Control-Allow-Origin", "*");
if (filePath.endsWith(".svg")) {
res.setHeader("Content-Type", "image/svg+xml");
}
fs.createReadStream(filePath).pipe(res);
} else {
next();
}
});
},
);
},
},
],
build: {
target: "esnext",
minify: true,
sourcemap: false,
rollupOptions: {
output: {
manualChunks: undefined,
},
},
},
server: {
port: 3000,
open: true,
fs: {
strict: false,
allow: [".."],
},
headers: {
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
},
},
optimizeDeps: {
exclude: ["./src/wasm"],
},
base: "/",
});