diff --git a/.github/workflows/wasm_ci.yml b/.github/workflows/wasm_ci.yml new file mode 100644 index 000000000..96a0c0ae0 --- /dev/null +++ b/.github/workflows/wasm_ci.yml @@ -0,0 +1,40 @@ +name: WASM CI + +on: + push: + branches: [ main ] + pull_request: + types: [ opened, synchronize, reopened ] + branches: [ main ] + +env: + NODE_VERSION: '20.12.0' + RUST_TOOLCHAIN: "1.75" + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + AppFlowy-Cloud + + - name: Install wasm-pack + run: npm install -g wasm-pack + + - name: Build with wasm-pack + run: wasm-pack build + working-directory: ./libs/client-api-wasm \ No newline at end of file diff --git a/.github/workflows/wasm_publish.yml b/.github/workflows/wasm_publish.yml new file mode 100644 index 000000000..7ca2d3ee4 --- /dev/null +++ b/.github/workflows/wasm_publish.yml @@ -0,0 +1,61 @@ +name: Manual NPM Package Publish + +on: + workflow_dispatch: + inputs: + working_directory: + description: 'Working directory (e.g., libs/client-api-wasm)' + required: true + default: 'libs/client-api-wasm' + package_name: + description: 'Package name' + required: true + default: '@appflowyinc/client-api-wasm' + package_version: + description: 'Package version' + required: true +env: + NODE_VERSION: '20.12.0' + RUST_TOOLCHAIN: "1.75" +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + AppFlowy-Cloud + + - name: Install wasm-pack + run: npm install -g wasm-pack + + - name: Build with wasm-pack + run: wasm-pack build + working-directory: ${{ github.event.inputs.working_directory }} + + - name: Update package.json + run: | + cd ${{ github.event.inputs.working_directory }}/pkg + jq '.name = "${{ github.event.inputs.package_name }}" | .version = "${{ github.event.inputs.package_version }}"' package.json > package.json.tmp + mv package.json.tmp package.json + + - name: Configure npm for wasm-pack + run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ${{ github.event.inputs.working_directory }}/pkg/.npmrc + + - name: Publish package + run: | + npm config set access public + wasm-pack publish + working-directory: ${{ github.event.inputs.working_directory }}/pkg + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 40550c897..bf973252b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -512,9 +512,11 @@ dependencies = [ "sqlx", "thiserror", "tokio", + "tsify", "url", "uuid", "validator", + "wasm-bindgen", ] [[package]] @@ -1381,6 +1383,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "client-api-wasm" +version = "0.1.0" +dependencies = [ + "client-api", + "console_error_panic_hook", + "lazy_static", + "log", + "serde", + "serde_json", + "tracing", + "tracing-core", + "tracing-wasm", + "tsify", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", +] + [[package]] name = "client-websocket" version = "0.1.0" @@ -2493,6 +2515,19 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gotrue" version = "0.1.0" @@ -4924,18 +4959,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.195" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_derive_internals" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", @@ -5967,6 +6013,17 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + [[package]] name = "triomphe" version = "0.1.11" @@ -5979,6 +6036,31 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils", + "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.48", +] + [[package]] name = "tungstenite" version = "0.20.1" diff --git a/Cargo.toml b/Cargo.toml index a822d8c38..2ca42bd09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,6 +144,7 @@ members = [ # services "services/collab-history", "services/realtime", + "libs/client-api-wasm" ] [workspace.dependencies] diff --git a/libs/app-error/Cargo.toml b/libs/app-error/Cargo.toml index 3c282f1f8..54244a9a5 100644 --- a/libs/app-error/Cargo.toml +++ b/libs/app-error/Cargo.toml @@ -34,4 +34,6 @@ gotrue_error= [] bincode_error = ["bincode"] [target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2", features = ["js"]} \ No newline at end of file +getrandom = { version = "0.2", features = ["js"]} +tsify = "0.4.5" +wasm-bindgen = "0.2.84" \ No newline at end of file diff --git a/libs/app-error/src/lib.rs b/libs/app-error/src/lib.rs index 9cb97f186..8f30f583d 100644 --- a/libs/app-error/src/lib.rs +++ b/libs/app-error/src/lib.rs @@ -231,6 +231,7 @@ impl From for AppError { } } +#[cfg_attr(target_arch = "wasm32", derive(tsify::Tsify))] #[derive( Eq, PartialEq, diff --git a/libs/client-api-wasm/.gitignore b/libs/client-api-wasm/.gitignore new file mode 100644 index 000000000..4e301317e --- /dev/null +++ b/libs/client-api-wasm/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/libs/client-api-wasm/Cargo.toml b/libs/client-api-wasm/Cargo.toml new file mode 100644 index 000000000..c5fb6e796 --- /dev/null +++ b/libs/client-api-wasm/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "client-api-wasm" +version = "0.1.0" +authors = ["Admin"] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + + +[features] +default = [] + + +[dependencies] +wasm-bindgen = "0.2.84" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } +log = "0.4.20" +serde = "1.0.197" +serde_json = "1.0.64" +client-api = { path = "../client-api" } +lazy_static = "1.4.0" +wasm-bindgen-futures = "0.4.20" +tsify = "0.4.5" +tracing.workspace = true +tracing-core = { version = "0.1.32" } +tracing-wasm = "0.2.1" +uuid.workspace = true + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/libs/client-api-wasm/README.md b/libs/client-api-wasm/README.md new file mode 100644 index 000000000..30cb5dcfc --- /dev/null +++ b/libs/client-api-wasm/README.md @@ -0,0 +1,65 @@ +
+ +

Client API WASM

+ + Client-API to WebAssembly Compiler + +
+ +## 🚴 Usage + +### 🐑 Prepare + +```bash +# Clone the repository (if you haven't already) +git clone https://github.com/AppFlowy-IO/AppFlowy-Cloud.git + +# Navigate to the client-for-wasm directory +cd libs/client-api-wasm + +# Install the dependencies (if you haven't already) +cargo install wasm-pack +``` + +### 🛠️ Build with `wasm-pack build` + +``` +wasm-pack build +``` + +### 🔬 Test in Headless Browsers with `wasm-pack test` + +```bash +# Ensure you have geckodriver installed +wasm-pack test --headless --firefox + +# or +# Ensure you have chromedriver installed +# https://googlechromelabs.github.io/chrome-for-testing/ +# Example (Linux): +# 1. wget https://storage.googleapis.com/chrome-for-testing-public/123.0.6312.86/linux64/chromedriver-linux64.zip +# 2. unzip chromedriver-linux64.zip +# 3. sudo mv chromedriver /usr/local/bin +# 4. chromedriver -v +# If you see the version, then you have successfully installed chromedriver +# Note: the version of chromedriver should match the version of chrome installed on your system +wasm-pack test --headless --chrome +``` + +### 🎁 Publish to NPM with ~~`wasm-pack publish`~~ + +##### Don't publish in local development, only publish in github actions + +``` +wasm-pack publish +``` + +### 📦 Use your package as a dependency + +``` +npm install --save @appflowy/client-api-for-wasm +``` + +### 📝 How to use the package in development? + +See the [README.md](https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/appflowy_web_app/README.md) in the AppFlowy Repository. diff --git a/libs/client-api-wasm/src/entities.rs b/libs/client-api-wasm/src/entities.rs new file mode 100644 index 000000000..db397504e --- /dev/null +++ b/libs/client-api-wasm/src/entities.rs @@ -0,0 +1,41 @@ +use client_api::error::ErrorCode; +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use wasm_bindgen::JsValue; + +macro_rules! from_struct_for_jsvalue { + ($type:ty) => { + impl From<$type> for JsValue { + fn from(value: $type) -> Self { + JsValue::from_str(&serde_json::to_string(&value).unwrap()) + } + } + }; +} + +#[derive(Tsify, Serialize, Deserialize, Default, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Configuration { + pub compression_quality: u32, + pub compression_buffer_size: usize, +} + +#[derive(Tsify, Serialize, Deserialize, Default, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ClientAPIConfig { + pub base_url: String, + pub ws_addr: String, + pub gotrue_url: String, + pub device_id: String, + pub configuration: Option, + pub client_id: String, +} + +#[derive(Tsify, Serialize, Deserialize, Default, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ClientResponse { + pub code: ErrorCode, + pub message: String, +} + +from_struct_for_jsvalue!(ClientResponse); diff --git a/libs/client-api-wasm/src/lib.rs b/libs/client-api-wasm/src/lib.rs new file mode 100644 index 000000000..2ba26c1be --- /dev/null +++ b/libs/client-api-wasm/src/lib.rs @@ -0,0 +1,111 @@ +pub mod entities; +use crate::entities::{ClientAPIConfig, ClientResponse}; +use client_api::{Client, ClientConfiguration}; +use tracing; +use wasm_bindgen::prelude::*; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(msg: &str); + + #[wasm_bindgen(js_namespace = console)] + fn error(msg: &str); + + #[wasm_bindgen(js_namespace = console)] + fn info(msg: &str); + + #[wasm_bindgen(js_namespace = console)] + fn debug(msg: &str); + + #[wasm_bindgen(js_namespace = console)] + fn warn(msg: &str); + + #[wasm_bindgen(js_namespace = console)] + fn trace(msg: &str); + +} + +#[wasm_bindgen] +pub struct ClientAPI { + client: Client, +} + +#[wasm_bindgen] +impl ClientAPI { + pub fn new(config: ClientAPIConfig) -> ClientAPI { + tracing_wasm::set_as_global_default(); + let configuration = ClientConfiguration::default(); + + if let Some(compression) = &config.configuration { + configuration + .to_owned() + .with_compression_buffer_size(compression.compression_buffer_size) + .with_compression_quality(compression.compression_quality); + } + + let client = Client::new( + config.base_url.as_str(), + config.ws_addr.as_str(), + config.gotrue_url.as_str(), + config.device_id.as_str(), + configuration, + config.client_id.as_str(), + ); + tracing::debug!("Client API initialized, config: {:?}", config); + ClientAPI { client } + } + + // pub async fn get_user(&self) -> ClientResponse { + // if let Err(err) = self.client.get_profile().await { + // log::error!("Get user failed: {:?}", err); + // return ClientResponse { + // code: ClientErrorCode::from(err.code), + // message: err.message.to_string(), + // data: None + // } + // } + // + // log::info!("Get user success"); + // ClientResponse { + // code: ClientErrorCode::Ok, + // message: "Get user success".to_string(), + // } + // } + + pub async fn sign_up_email_verified( + &self, + email: &str, + password: &str, + ) -> Result { + if let Err(err) = self.client.sign_up(email, password).await { + return Err(ClientResponse { + code: err.code, + message: err.message.to_string(), + }); + } + + Ok(true) + } + + pub async fn sign_in_password( + &self, + email: &str, + password: &str, + ) -> Result { + if let Err(err) = self.client.sign_in_password(email, password).await { + return Err(ClientResponse { + code: err.code, + message: err.message.to_string(), + }); + } + + Ok(true) + } +} diff --git a/libs/wasm-test/tests/main.rs b/libs/wasm-test/tests/main.rs index 631ca531e..9d1c849e2 100644 --- a/libs/wasm-test/tests/main.rs +++ b/libs/wasm-test/tests/main.rs @@ -1,3 +1,4 @@ +extern crate wasm_bindgen_test; use wasm_bindgen_test::wasm_bindgen_test_configure; wasm_bindgen_test_configure!(run_in_browser); diff --git a/libs/wasm-test/tests/user_test.rs b/libs/wasm-test/tests/user_test.rs index 7216b1f29..acc7e7d99 100644 --- a/libs/wasm-test/tests/user_test.rs +++ b/libs/wasm-test/tests/user_test.rs @@ -1,4 +1,4 @@ -use client_api_test_util::{generate_unique_email, localhost_client}; +use client_api_test_util::{generate_unique_email, localhost_client, TestClient}; use wasm_bindgen_test::wasm_bindgen_test; #[wasm_bindgen_test] @@ -8,3 +8,20 @@ async fn wasm_sign_up_success() { let c = localhost_client(); c.sign_up(&email, password).await.unwrap(); } + +#[wasm_bindgen_test] +async fn wasm_sign_in_success() { + let test_client = TestClient::new_user().await; + let user = test_client.user; + + let res = test_client + .api_client + .sign_in_password(user.email.as_str(), user.password.as_str()) + .await; + + assert!(res.ok()); + + let val = res.unwrap(); + + assert!(val); +}