diff --git a/.github/actions/tauri-linux-setup/action.yaml b/.github/actions/tauri-linux-setup/action.yaml new file mode 100644 index 0000000..716ad6e --- /dev/null +++ b/.github/actions/tauri-linux-setup/action.yaml @@ -0,0 +1,17 @@ +name: "tauri linux setup" +description: "tauri linux setup" +runs: + using: "composite" + steps: + - run: | + sudo apt update && sudo apt install -y \ + libwebkit2gtk-4.1-dev \ + build-essential \ + curl \ + wget \ + file \ + libxdo-dev \ + libssl-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev + shell: bash diff --git a/.github/workflows/prepare_release.yaml b/.github/workflows/prepare_release.yaml index b8c17ad..4c0b809 100644 --- a/.github/workflows/prepare_release.yaml +++ b/.github/workflows/prepare_release.yaml @@ -13,6 +13,8 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: "0" + - name: Setup tauri for linux + uses: ./.github/actions/tauri-linux-setup - name: Setup Node.js uses: ./.github/actions/node-setup - name: Setup Rust diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 0b5a942..f099689 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -50,6 +50,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]')" steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/tauri-linux-setup - uses: ./.github/actions/rust-setup with: components: clippy @@ -74,6 +75,8 @@ jobs: runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v4 + - if: ${{ matrix.settings.host == 'ubuntu-latest' }} + uses: ./.github/actions/tauri-linux-setup - uses: ./.github/actions/rust-setup with: github-token: ${{ github.token }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b5c78e2..dac5824 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -53,6 +53,9 @@ jobs: steps: - name: Git checkout uses: actions/checkout@v4 + - name: Setup tauri for linux + if: ${{ matrix.settings.host == 'ubuntu-latest' }} + uses: ./.github/actions/tauri-linux-setup - name: Setup Node.js uses: ./.github/actions/node-setup if: ${{ !matrix.settings.docker }} @@ -98,6 +101,8 @@ jobs: steps: - name: Git checkout uses: actions/checkout@v4 + - name: Setup tauri for linux + uses: ./.github/actions/tauri-linux-setup - name: Setup Node.js uses: ./.github/actions/node-setup - name: Setup Rust diff --git a/Cargo.toml b/Cargo.toml index f6f49e8..0c77bac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,30 @@ [workspace] -members = ["crates/*", "packages/cli", "packages/node-binding"] +members = ["crates/*", "packages/cli", "packages/node-binding", "examples/tauri-simple"] resolver = "2" [workspace.dependencies] anyhow = "1" +async-trait = "0.1.83" bincode = "2.0.0-rc.3" biome_console = "0.5.7" bpaf = { version = "0.9.14", features = ["derive"] } lz4_flex = "0.11.3" +mime_guess = "2.0.5" napi = { version = "2.16.11", default-features = false, features = ["napi4", "async"] } napi-build = "2.1.3" napi-derive = "2.16.12" serde = { version = "1", features = ["derive"] } serde_json = "1" +tauri = "2" +tauri-build = "2" thiserror = "1" tokio = "1.40.0" tracing = { version = "0.1.40", default-features = false, features = ["std"] } tracing-subscriber = "0.3.18" -# crates -webview-bundle = { version = "0.0.0", path = "./crates/webview-bundle" } -webview-bundle-cli = { version = "0.0.0", path = "./crates/webview-bundle-cli" } +webview-bundle = { version = "0.0.0", path = "./crates/webview-bundle" } +webview-bundle-cli = { version = "0.0.0", path = "./crates/webview-bundle-cli" } +webview-bundle-tauri = { version = "0.0.0", path = "./crates/webview-bundle-tauri" } [profile.release] lto = true diff --git a/crates/webview-bundle-tauri/CHANGELOG.md b/crates/webview-bundle-tauri/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/crates/webview-bundle-tauri/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/crates/webview-bundle-tauri/Cargo.toml b/crates/webview-bundle-tauri/Cargo.toml new file mode 100644 index 0000000..588da26 --- /dev/null +++ b/crates/webview-bundle-tauri/Cargo.toml @@ -0,0 +1,22 @@ +[package] +authors = ["Seokju Na "] +description = "TBD" +edition = "2021" +license = "MIT" +name = "webview-bundle-tauri" +repository = "https://github.com/seokju-na/webview-bundle" +version = "0.0.0" + +[dependencies] +webview-bundle = { workspace = true } + +async-trait = { workspace = true } +buildstructor = "0.5.4" +lru = { version = "0.12.5", optional = true } +mime_guess = { workspace = true } +tauri = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } + +[features] +cache-lru = ["dep:lru"] diff --git a/crates/webview-bundle-tauri/README.md b/crates/webview-bundle-tauri/README.md new file mode 100644 index 0000000..e2949a4 --- /dev/null +++ b/crates/webview-bundle-tauri/README.md @@ -0,0 +1 @@ +# webview-bundle-tauri diff --git a/crates/webview-bundle-tauri/src/cache.rs b/crates/webview-bundle-tauri/src/cache.rs new file mode 100644 index 0000000..8793c5e --- /dev/null +++ b/crates/webview-bundle-tauri/src/cache.rs @@ -0,0 +1,61 @@ +use std::hash::Hash; +use webview_bundle::Bundle; + +pub trait Cache { + fn has(&self, key: &K) -> bool; + fn get(&mut self, key: &K) -> Option<&V>; + fn set(&mut self, key: K, value: V); +} + +#[derive(Clone, Default)] +pub struct NoopCache; + +impl Cache for NoopCache { + fn has(&self, _key: &String) -> bool { + false + } + + fn get(&mut self, _key: &String) -> Option<&Bundle> { + None + } + + fn set(&mut self, _key: String, _value: Bundle) {} +} + +#[cfg(feature = "cache-lru")] +#[derive(Clone)] +pub struct LruCache { + cache: lru::LruCache, +} + +#[cfg(feature = "cache-lru")] +impl LruCache { + pub fn new(size: usize) -> Self { + Self { + cache: lru::LruCache::::new( + std::num::NonZeroUsize::new(size).expect("size is not non zero"), + ), + } + } + + pub fn unbounded() -> Self { + Self { + cache: lru::LruCache::::unbounded(), + } + } +} + +#[cfg(feature = "cache-lru")] +impl Cache for LruCache { + fn has(&self, key: &String) -> bool { + self.cache.contains(key) + } + + fn get(&mut self, key: &String) -> Option<&Bundle> { + self.cache.get(key) + } + + fn set(&mut self, key: String, value: Bundle) { + self.cache.put(key, value); + } +} diff --git a/crates/webview-bundle-tauri/src/config.rs b/crates/webview-bundle-tauri/src/config.rs new file mode 100644 index 0000000..0468b38 --- /dev/null +++ b/crates/webview-bundle-tauri/src/config.rs @@ -0,0 +1,32 @@ +use crate::cache::Cache; +use crate::loader::Loader; +use webview_bundle::Bundle; + +pub struct Config +where + L: Loader + Send + Sync, + C: Cache + Send + Sync, +{ + loader: L, + cache: C, +} + +#[buildstructor::buildstructor] +impl Config +where + L: Loader + Send + Sync, + C: Cache + Send + Sync, +{ + #[builder] + pub fn new(loader: L, cache: C) -> Self { + Self { loader, cache } + } + + pub fn loader(&self) -> &L { + &self.loader + } + + pub fn cache(&self) -> &C { + &self.cache + } +} diff --git a/crates/webview-bundle-tauri/src/error.rs b/crates/webview-bundle-tauri/src/error.rs new file mode 100644 index 0000000..7f4038d --- /dev/null +++ b/crates/webview-bundle-tauri/src/error.rs @@ -0,0 +1,7 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + WebviewBundle(#[from] webview_bundle::Error), +} diff --git a/crates/webview-bundle-tauri/src/lib.rs b/crates/webview-bundle-tauri/src/lib.rs new file mode 100644 index 0000000..ddd4e4a --- /dev/null +++ b/crates/webview-bundle-tauri/src/lib.rs @@ -0,0 +1,74 @@ +pub mod cache; +pub mod config; +pub mod error; +pub mod loader; + +use crate::cache::Cache; +use crate::config::Config; +use crate::loader::Loader; +use std::path::Path; +use tauri::http::{Method, Response, Uri}; +use tauri::plugin::{PluginApi, TauriPlugin}; +use tauri::{plugin, AppHandle, Manager, Runtime}; +use webview_bundle::Bundle; + +pub fn init(scheme: &'static str, config: F) -> TauriPlugin +where + R: Runtime, + L: Loader + Send + Sync + 'static, + C: Cache + Send + Sync + 'static, + F: FnOnce(&AppHandle, PluginApi) -> Result, Box> + + Send + + 'static, +{ + plugin::Builder::::new("webview-bundle") + .setup(|app, api| { + let config = config(app, api)?; + app.manage(config); + Ok(()) + }) + .register_asynchronous_uri_scheme_protocol(scheme, move |ctx, request, responder| { + let method = request.method(); + if method != Method::GET { + responder.respond(Response::builder().status(405).body(vec![]).unwrap()); + return; + } + let uri = request.uri().clone(); + let app = ctx.app_handle().clone(); + tauri::async_runtime::spawn(async move { + let config = app.state::>(); + let bundle = config.loader().load(&uri).await.unwrap(); + let filepath = uri_to_filepath(&uri); + let buf = bundle.read_file(&filepath).unwrap(); // TODO: handle file not found error + responder.respond( + Response::builder() + .header("content-type", mime_types_from_filepath(&filepath)) + .header("content-length", buf.len()) + .status(200) + .body(buf) + .unwrap(), + ); + }); + }) + .build() +} + +fn uri_to_filepath(uri: &Uri) -> String { + let filepath = uri.path()[1..].to_string(); + if Path::new(&filepath).extension().is_some() { + return filepath; + } + let index_html = "index.html".to_string(); + if filepath.is_empty() { + return index_html; + } + [filepath, index_html].join("/") +} + +fn mime_types_from_filepath(filepath: &String) -> String { + let guess = mime_guess::from_path(filepath); + guess + .first() + .map(|x| x.to_string()) + .unwrap_or("text/plain".to_string()) +} diff --git a/crates/webview-bundle-tauri/src/loader.rs b/crates/webview-bundle-tauri/src/loader.rs new file mode 100644 index 0000000..c69550f --- /dev/null +++ b/crates/webview-bundle-tauri/src/loader.rs @@ -0,0 +1,59 @@ +use async_trait::async_trait; +use std::path::{Path, PathBuf}; +use tauri::http::Uri; +use webview_bundle::Bundle; + +#[async_trait] +pub trait Loader: Send + Sync { + type Error: std::error::Error; + fn get_bundle_name(&self, _: &Uri) -> Option { + None + } + async fn load(&self, uri: &Uri) -> Result; +} + +pub struct FSLoader { + pub resolve_file_path: Box PathBuf + Send + Sync>, +} + +impl FSLoader { + pub fn new PathBuf + Send + Sync + 'static>(resolve_file_path: R) -> Self { + Self { + resolve_file_path: Box::new(resolve_file_path), + } + } + + pub fn from_dir>(dir: P) -> Self { + let dir_path_buf = dir.as_ref().to_path_buf(); + Self::new(move |uri| { + let host = uri.host().unwrap_or_default(); + let filename = match host.ends_with(".wvb") { + true => host.to_string(), + false => format!("{host}.wvb"), + }; + let mut filepath = dir_path_buf.clone(); + filepath.push(filename); + filepath + }) + } +} + +#[async_trait] +impl Loader for FSLoader { + type Error = crate::error::Error; + + fn get_bundle_name(&self, uri: &Uri) -> Option { + let filepath_buf = (self.resolve_file_path)(uri); + let filepath = Path::new(&filepath_buf); + filepath + .file_name() + .map(|x| x.to_string_lossy().to_string()) + } + + async fn load(&self, uri: &Uri) -> Result { + let filepath_buf = (self.resolve_file_path)(uri); + let buf = tokio::fs::read(&filepath_buf).await?; + let bundle = webview_bundle::decode(buf)?; + Ok(bundle) + } +} diff --git a/examples/tauri-simple/.gitignore b/examples/tauri-simple/.gitignore new file mode 100644 index 0000000..e507aab --- /dev/null +++ b/examples/tauri-simple/.gitignore @@ -0,0 +1,2 @@ +gen/schemas/ +bundle.wvb diff --git a/examples/tauri-simple/Cargo.toml b/examples/tauri-simple/Cargo.toml new file mode 100644 index 0000000..5076c36 --- /dev/null +++ b/examples/tauri-simple/Cargo.toml @@ -0,0 +1,15 @@ +[package] +edition = "2021" +name = "example-tauri-simple" +publish = false +version = "0.1.0" + +[build-dependencies] +tauri-build = { workspace = true } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true, features = ["unstable", "tracing"] } +url = { version = "2", features = ["serde"] } +webview-bundle-tauri = { workspace = true } diff --git a/examples/tauri-simple/build.rs b/examples/tauri-simple/build.rs new file mode 100644 index 0000000..795b9b7 --- /dev/null +++ b/examples/tauri-simple/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/examples/tauri-simple/bundle/index.html b/examples/tauri-simple/bundle/index.html new file mode 100644 index 0000000..c8e39c8 --- /dev/null +++ b/examples/tauri-simple/bundle/index.html @@ -0,0 +1,13 @@ + + + + + Hello World + + +

Hello World

+

Test paragraph

+ + + + diff --git a/examples/tauri-simple/bundle/index.js b/examples/tauri-simple/bundle/index.js new file mode 100644 index 0000000..33a6c83 --- /dev/null +++ b/examples/tauri-simple/bundle/index.js @@ -0,0 +1,5 @@ +console.log('Hello World'); + +document.getElementById('btn').addEventListener('click', () => { + alert('Clicked!'); +}); diff --git a/examples/tauri-simple/icons/icon.ico b/examples/tauri-simple/icons/icon.ico new file mode 100644 index 0000000..b3636e4 Binary files /dev/null and b/examples/tauri-simple/icons/icon.ico differ diff --git a/examples/tauri-simple/icons/icon.png b/examples/tauri-simple/icons/icon.png new file mode 100644 index 0000000..e1cd261 Binary files /dev/null and b/examples/tauri-simple/icons/icon.png differ diff --git a/examples/tauri-simple/package.json b/examples/tauri-simple/package.json new file mode 100644 index 0000000..0e79e47 --- /dev/null +++ b/examples/tauri-simple/package.json @@ -0,0 +1,16 @@ +{ + "name": "example-tauri-simple", + "private": true, + "scripts": { + "dev": "tauri dev", + "make-bundle": "webview-bundle pack ./bundle -o bundle.wvb --truncate" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.2" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.3", + "@webview-bundle/cli": "workspace:^", + "typescript": "5.6.2" + } +} diff --git a/examples/tauri-simple/src/main.rs b/examples/tauri-simple/src/main.rs new file mode 100644 index 0000000..dfaad0f --- /dev/null +++ b/examples/tauri-simple/src/main.rs @@ -0,0 +1,36 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri::{Manager, WebviewUrl}; +use webview_bundle_tauri::cache::NoopCache; +use webview_bundle_tauri::config::Config; +use webview_bundle_tauri::loader::FSLoader; + +fn main() { + tauri::Builder::default() + .plugin(webview_bundle_tauri::init("app", |app, _api| { + let mut dir = app.path().resource_dir()?; + dir.pop(); + dir.pop(); + dir.push("examples/tauri-simple"); + let config = Config::builder() + .cache(NoopCache::default()) + .loader(FSLoader::from_dir(dir)) + .build(); + Ok(config) + })) + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "primary").build()?; + let webview_builder = tauri::webview::WebviewBuilder::new( + "primary", + WebviewUrl::CustomProtocol(url::Url::parse("app://bundle").unwrap()), + ); + let _webview = window.add_child( + webview_builder, + tauri::LogicalPosition::new(0, 0), + window.inner_size().unwrap(), + ); + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/examples/tauri-simple/tauri.conf.json b/examples/tauri-simple/tauri.conf.json new file mode 100644 index 0000000..3fa6d09 --- /dev/null +++ b/examples/tauri-simple/tauri.conf.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "hello-tauri", + "version": "0.1.0", + "identifier": "me.seokju.hello-tauri", + "app": { + "windows": [], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [] + } +} diff --git a/releases.json b/releases.json index eb66af0..1697092 100644 --- a/releases.json +++ b/releases.json @@ -62,6 +62,13 @@ ], "changelog": "packages/electron/CHANGELOG.md", "scopes": ["electron"] + }, + "tauri": { + "versionedFiles": [ + "crates/webview-bundle-tauri/Cargo.toml" + ], + "changelog": "crates/webview-bundle-tauri/CHANGELOG.md", + "scopes": ["tauri"] } }, "github": { diff --git a/yarn.lock b/yarn.lock index c7e7f50..3467a61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1119,6 +1119,124 @@ __metadata: languageName: node linkType: hard +"@tauri-apps/api@npm:^2.0.2": + version: 2.0.2 + resolution: "@tauri-apps/api@npm:2.0.2" + checksum: 10c0/6647cc00cf521bcfaf83e4f397b15aca6fd03db27e2afd883ee9e9ea6b14bc3b90c3df0756aaa0636bd9be791799320e44e99a2e83f2865916b0447879d00682 + languageName: node + linkType: hard + +"@tauri-apps/cli-darwin-arm64@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-darwin-arm64@npm:2.0.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@tauri-apps/cli-darwin-x64@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-darwin-x64@npm:2.0.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@tauri-apps/cli-linux-arm-gnueabihf@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-linux-arm-gnueabihf@npm:2.0.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@tauri-apps/cli-linux-arm64-gnu@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-linux-arm64-gnu@npm:2.0.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@tauri-apps/cli-linux-arm64-musl@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-linux-arm64-musl@npm:2.0.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@tauri-apps/cli-linux-x64-gnu@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-linux-x64-gnu@npm:2.0.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@tauri-apps/cli-linux-x64-musl@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-linux-x64-musl@npm:2.0.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@tauri-apps/cli-win32-arm64-msvc@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-win32-arm64-msvc@npm:2.0.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@tauri-apps/cli-win32-ia32-msvc@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-win32-ia32-msvc@npm:2.0.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@tauri-apps/cli-win32-x64-msvc@npm:2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli-win32-x64-msvc@npm:2.0.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@tauri-apps/cli@npm:^2.0.3": + version: 2.0.3 + resolution: "@tauri-apps/cli@npm:2.0.3" + dependencies: + "@tauri-apps/cli-darwin-arm64": "npm:2.0.3" + "@tauri-apps/cli-darwin-x64": "npm:2.0.3" + "@tauri-apps/cli-linux-arm-gnueabihf": "npm:2.0.3" + "@tauri-apps/cli-linux-arm64-gnu": "npm:2.0.3" + "@tauri-apps/cli-linux-arm64-musl": "npm:2.0.3" + "@tauri-apps/cli-linux-x64-gnu": "npm:2.0.3" + "@tauri-apps/cli-linux-x64-musl": "npm:2.0.3" + "@tauri-apps/cli-win32-arm64-msvc": "npm:2.0.3" + "@tauri-apps/cli-win32-ia32-msvc": "npm:2.0.3" + "@tauri-apps/cli-win32-x64-msvc": "npm:2.0.3" + dependenciesMeta: + "@tauri-apps/cli-darwin-arm64": + optional: true + "@tauri-apps/cli-darwin-x64": + optional: true + "@tauri-apps/cli-linux-arm-gnueabihf": + optional: true + "@tauri-apps/cli-linux-arm64-gnu": + optional: true + "@tauri-apps/cli-linux-arm64-musl": + optional: true + "@tauri-apps/cli-linux-x64-gnu": + optional: true + "@tauri-apps/cli-linux-x64-musl": + optional: true + "@tauri-apps/cli-win32-arm64-msvc": + optional: true + "@tauri-apps/cli-win32-ia32-msvc": + optional: true + "@tauri-apps/cli-win32-x64-msvc": + optional: true + bin: + tauri: tauri.js + checksum: 10c0/ec176049d83a8333005f17d9afa2732f4e9367efbdfab1563d4a936744424a6555659406400a1946dec4e5f78b788994a5980d460e73b26185a5038c49980e6b + languageName: node + linkType: hard + "@types/cacheable-request@npm:^6.0.1": version: 6.0.3 resolution: "@types/cacheable-request@npm:6.0.3" @@ -2059,6 +2177,17 @@ __metadata: languageName: unknown linkType: soft +"example-tauri-simple@workspace:examples/tauri-simple": + version: 0.0.0-use.local + resolution: "example-tauri-simple@workspace:examples/tauri-simple" + dependencies: + "@tauri-apps/api": "npm:^2.0.2" + "@tauri-apps/cli": "npm:^2.0.3" + "@webview-bundle/cli": "workspace:^" + typescript: "npm:5.6.2" + languageName: unknown + linkType: soft + "execa@npm:^9.4.0": version: 9.4.0 resolution: "execa@npm:9.4.0"