feat(examples): add desktop/web application example (#5537)

This commit is contained in:
Lucas Fernandes Nogueira 2022-11-03 18:01:47 -03:00 committed by GitHub
parent 2d9c2b4724
commit 6c13840cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 5193 additions and 27 deletions

View File

@ -20,7 +20,8 @@ exclude = [
"examples/api/src-tauri",
"examples/updater/src-tauri",
"examples/resources/src-tauri",
"examples/sidecar/src-tauri"
"examples/sidecar/src-tauri",
"examples/web/core"
]
# default to small, optimized workspace release binaries

View File

@ -57,7 +57,7 @@ fn map_core_assets(
let mut hasher = Sha256::new();
hasher.update(&script);
let hash = hasher.finalize();
scripts.push(format!("'sha256-{}'", base64::encode(&hash)));
scripts.push(format!("'sha256-{}'", base64::encode(hash)));
}
csp_hashes
.inline_scripts
@ -76,7 +76,7 @@ fn map_core_assets(
let hash = hasher.finalize();
csp_hashes
.styles
.push(format!("'sha256-{}'", base64::encode(&hash)));
.push(format!("'sha256-{}'", base64::encode(hash)));
}
}
@ -457,7 +457,7 @@ fn ico_icon<P: AsRef<Path>>(
path: P,
) -> Result<TokenStream, EmbeddedAssetsError> {
let path = path.as_ref();
let bytes = std::fs::read(&path)
let bytes = std::fs::read(path)
.unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e))
.to_vec();
let icon_dir = ico::IconDir::read(std::io::Cursor::new(bytes))
@ -485,7 +485,7 @@ fn ico_icon<P: AsRef<Path>>(
fn raw_icon<P: AsRef<Path>>(out_dir: &Path, path: P) -> Result<TokenStream, EmbeddedAssetsError> {
let path = path.as_ref();
let bytes = std::fs::read(&path)
let bytes = std::fs::read(path)
.unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e))
.to_vec();
@ -507,7 +507,7 @@ fn png_icon<P: AsRef<Path>>(
path: P,
) -> Result<TokenStream, EmbeddedAssetsError> {
let path = path.as_ref();
let bytes = std::fs::read(&path)
let bytes = std::fs::read(path)
.unwrap_or_else(|e| panic!("failed to read icon {}: {}", path.display(), e))
.to_vec();
let decoder = png::Decoder::new(std::io::Cursor::new(bytes));
@ -537,13 +537,13 @@ fn write_if_changed(out_path: &Path, data: &[u8]) -> std::io::Result<()> {
use std::fs::File;
use std::io::Write;
if let Ok(curr) = std::fs::read(&out_path) {
if let Ok(curr) = std::fs::read(out_path) {
if curr == data {
return Ok(());
}
}
let mut out_file = File::create(&out_path)?;
let mut out_file = File::create(out_path)?;
out_file.write_all(data)
}

View File

@ -2981,7 +2981,7 @@ mod build {
tokens.append_all(match self {
Self::App(path) => {
let path = path_buf_lit(&path);
let path = path_buf_lit(path);
quote! { #prefix::App(#path) }
}
Self::External(url) => {
@ -3211,7 +3211,7 @@ mod build {
quote! { #prefix::OfflineInstaller { silent: #silent } }
}
Self::FixedRuntime { path } => {
let path = path_buf_lit(&path);
let path = path_buf_lit(path);
quote! { #prefix::FixedRuntime { path: #path } }
}
})

View File

@ -15,7 +15,7 @@ pub enum ArchiveReader<R: Read + Seek> {
/// A plain reader.
Plain(R),
/// A GZ- compressed reader (decoder).
GzCompressed(flate2::read::GzDecoder<R>),
GzCompressed(Box<flate2::read::GzDecoder<R>>),
}
impl<R: Read + Seek> Read for ArchiveReader<R> {
@ -161,7 +161,9 @@ impl<'a, R: Read + Seek> Extract<'a, R> {
};
Extract {
reader: match compression {
Some(Compression::Gz) => ArchiveReader::GzCompressed(flate2::read::GzDecoder::new(reader)),
Some(Compression::Gz) => {
ArchiveReader::GzCompressed(Box::new(flate2::read::GzDecoder::new(reader)))
}
_ => ArchiveReader::Plain(reader),
},
archive_format,
@ -248,7 +250,7 @@ impl<'a, R: Read + Seek> Extract<'a, R> {
fs::create_dir_all(&out_path)?;
} else {
if let Some(out_path_parent) = out_path.parent() {
fs::create_dir_all(&out_path_parent)?;
fs::create_dir_all(out_path_parent)?;
}
let mut out_file = fs::File::create(&out_path)?;
io::copy(&mut file, &mut out_file)?;

View File

@ -98,7 +98,7 @@ fn walkdir_and_copy(source: &path::Path, dest: &path::Path) -> crate::api::Resul
let element = entry?;
let metadata = element.metadata()?;
let destination = dest.join(element.path().strip_prefix(&source)?);
let destination = dest.join(element.path().strip_prefix(source)?);
// we make sure it's a directory and destination doesnt exist
if metadata.is_dir() && !&destination.exists() {

View File

@ -436,7 +436,7 @@ mod test {
#[test]
fn test_cmd_output() {
// create a command to run cat.
let cmd = Command::new("cat").args(&["test/api/test.txt"]);
let cmd = Command::new("cat").args(["test/api/test.txt"]);
let (mut rx, _) = cmd.spawn().unwrap();
crate::async_runtime::block_on(async move {
@ -458,7 +458,7 @@ mod test {
#[test]
// test the failure case
fn test_cmd_fail() {
let cmd = Command::new("cat").args(&["test/api/"]);
let cmd = Command::new("cat").args(["test/api/"]);
let (mut rx, _) = cmd.spawn().unwrap();
crate::async_runtime::block_on(async move {

View File

@ -103,7 +103,7 @@ impl Cmd {
} = value
{
if crate::api::file::SafePathBuf::new(path.clone()).is_err()
|| !scopes.fs.is_allowed(&path)
|| !scopes.fs.is_allowed(path)
{
return Err(crate::Error::PathNotAllowed(path.clone()).into_anyhow());
}

View File

@ -1066,7 +1066,7 @@ impl<R: Runtime> WindowManager<R> {
// ignore "index.html" just to simplify the url
if path.to_str() != Some("index.html") {
url
.join(&*path.to_string_lossy())
.join(&path.to_string_lossy())
.map_err(crate::Error::InvalidUrl)
// this will never fail
.unwrap()

View File

@ -148,9 +148,9 @@ impl Scope {
let mut list = self.allowed_patterns.lock().unwrap();
// allow the directory to be read
push_pattern(&mut list, &path, escaped_pattern)?;
push_pattern(&mut list, path, escaped_pattern)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, &path, |p| {
push_pattern(&mut list, path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
@ -165,7 +165,7 @@ impl Scope {
let path = path.as_ref();
push_pattern(
&mut self.allowed_patterns.lock().unwrap(),
&path,
path,
escaped_pattern,
)?;
self.trigger(Event::PathAllowed(path.to_path_buf()));
@ -181,9 +181,9 @@ impl Scope {
let mut list = self.forbidden_patterns.lock().unwrap();
// allow the directory to be read
push_pattern(&mut list, &path, escaped_pattern)?;
push_pattern(&mut list, path, escaped_pattern)?;
// allow its files and subdirectories to be read
push_pattern(&mut list, &path, |p| {
push_pattern(&mut list, path, |p| {
escaped_pattern_with(p, if recursive { "**" } else { "*" })
})?;
}
@ -198,7 +198,7 @@ impl Scope {
let path = path.as_ref();
push_pattern(
&mut self.forbidden_patterns.lock().unwrap(),
&path,
path,
escaped_pattern,
)?;
self.trigger(Event::PathForbidden(path.to_path_buf()));

View File

@ -306,8 +306,8 @@ impl Scope {
// The prevention of argument escaping is handled by the usage of std::process::Command::arg by
// the `open` dependency. This behavior should be re-confirmed during upgrades of `open`.
match with.map(Program::name) {
Some(program) => ::open::with(&path, program),
None => ::open::that(&path),
Some(program) => ::open::with(path, program),
None => ::open::that(path),
}
.map_err(Into::into)
}

View File

@ -177,7 +177,7 @@ async fn async_stateful_command_with_result(
state: State<'_, MyState>,
) -> Result<String, MyError> {
println!("{:?} {:?}", the_argument, state.inner());
Ok(the_argument.unwrap_or_else(|| "".to_string()))
Ok(the_argument.unwrap_or_default())
}
// Non-Ident command function arguments

8
examples/web/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example

1
examples/web/.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

35
examples/web/README.md Normal file
View File

@ -0,0 +1,35 @@
# Desktop / Web Example
This example showcases an application that has shares code between a desktop and a Web target.
The Web application uses WASM to communicate with the Rust backend, while the desktop app leverages Tauri commands.
## Architecture
The Rust code lives in the `core/` folder and it is a Cargo workspace with three crates:
- tauri: desktop application. Contains the commands that are used by the frontend to access the Rust APIs;
- wasm: library that is compiled to WASM to be used by the Web application;
- api: code shared between the Tauri and the WASM crates. Most of the logic should live here, with only the specifics in the tauri and wasm crates.
The Rust code bridge is defined in the `src/api/` folder, which defines `desktop/index.js` and a `web/index.js` interfaces.
To access the proper interface according to the build target, a resolve alias is defined in vite.config.js, so the API can be imported
with `import * as api from '$api'`.
## Running the desktop application
Use the following commands to run the desktop application:
```
yarn
yarn tauri dev
```
## Running the Web application
Use the following commands to run the Web application:
```
yarn
yarn dev:web
```

3395
examples/web/core/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
[workspace]
members = [
"api",
"tauri",
"wasm"
]

View File

@ -0,0 +1,6 @@
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]

View File

@ -0,0 +1,3 @@
pub fn greet(name: &str) -> String {
format!("Hello, {name}!")
}

3
examples/web/core/tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/

View File

@ -0,0 +1,25 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.59"
[build-dependencies]
tauri-build = { path = "../../../../core/tauri-build", features = [] }
[dependencies]
api = { path = "../api" }
tauri = { path = "../../../../core/tauri", features = ["dialog"] }
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = [ "custom-protocol" ]
# this feature is used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ]

View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,16 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#[tauri::command]
fn greet(window: tauri::Window, name: String) {
tauri::api::dialog::message(Some(&window), "Tauri Example", api::greet(&name));
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -0,0 +1,65 @@
{
"build": {
"beforeBuildCommand": "yarn build:tauri",
"beforeDevCommand": "yarn dev:tauri",
"devPath": "http://127.0.0.1:5173",
"distDir": "../../build"
},
"package": {
"productName": "app",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"../../../.icons/32x32.png",
"../../../.icons/128x128.png",
"../../../.icons/128x128@2x.png",
"../../../.icons/icon.icns",
"../../../.icons/icon.ico"
],
"identifier": "com.tauri.app",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"height": 600,
"resizable": true,
"title": "Tauri Example",
"width": 800
}
]
}
}

View File

@ -0,0 +1,11 @@
[package]
name = "wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
api = { path = "../api" }
wasm-bindgen = "0.2"

View File

@ -0,0 +1,11 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&api::greet(name));
}

37
examples/web/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "web",
"version": "0.0.1",
"private": true,
"scripts": {
"preinstall": "yarn wasm",
"dev:web": "yarn wasm && cross-env TARGET=web vite dev",
"build:web": "yarn wasm && cross-env TARGET=web vite build",
"preview": "vite preview",
"dev:tauri": "cross-env TARGET=tauri vite dev",
"build:tauri": "cross-env TARGET=tauri vite build",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"tauri": "node ../../tooling/cli/node/tauri.js",
"wasm": "wasm-pack build ./core/wasm --target web"
},
"dependencies": {
"@tauri-apps/api": "../../tooling/api/dist",
"wasm": "core/wasm/pkg"
},
"devDependencies": {
"@sveltejs/adapter-auto": "next",
"@sveltejs/adapter-static": "^1.0.0-next.47",
"@sveltejs/kit": "next",
"cross-env": "^7.0.3",
"svelte": "^3.44.0",
"svelte-check": "^2.7.1",
"svelte-preprocess": "^4.10.6",
"tslib": "^2.3.1",
"typescript": "^4.7.4",
"vite": "^3.1.0",
"vite-plugin-static-copy": "^0.11.1",
"vite-plugin-top-level-await": "^1.2.1",
"vite-plugin-wasm": "^3.1.0"
},
"type": "module"
}

View File

@ -0,0 +1,12 @@
import { invoke } from '@tauri-apps/api/tauri'
export const NAME = 'Tauri'
/**
* Greets someone.
* @param {string} name
* @returns
*/
export async function greet(name) {
return invoke('greet', { name })
}

View File

@ -0,0 +1,16 @@
import init, * as wasm from 'wasm'
function initialize() {
return init('wasm/wasm_bg.wasm')
}
export const NAME = 'WEB'
/**
* Greets someone.
* @param {string} name
* @returns
*/
export async function greet(name) {
return initialize().then(() => wasm.greet(name))
}

9
examples/web/src/app.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface Error {}
// interface Platform {}
}

12
examples/web/src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1,2 @@
// TODO: how can we disable SSR on Tauri but keep it on the Web target?
export const ssr = false

View File

@ -0,0 +1,14 @@
<script>
import * as api from '$api'
let name = api.NAME
function greet() {
api.greet(name)
}
</script>
<div>
<input bind:value={name} />
<button on:click={greet}>Greet</button>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,23 @@
import autoAdapter from '@sveltejs/adapter-auto'
import staticAdapter from '@sveltejs/adapter-static'
import preprocess from 'svelte-preprocess'
const TARGET = process.env.TARGET
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
adapter:
TARGET === 'tauri'
? staticAdapter({
fallback: 'index.html'
})
: autoAdapter()
}
}
export default config

View File

@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@ -0,0 +1,39 @@
import { resolve } from 'path'
import { sveltekit } from '@sveltejs/kit/vite'
import wasm from 'vite-plugin-wasm'
import topLevelAwait from 'vite-plugin-top-level-await'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import type { UserConfig } from 'vite'
const TARGET = process.env.TARGET
const plugins = [sveltekit()]
if (TARGET === 'web') {
plugins.push(wasm())
plugins.push(topLevelAwait())
plugins.push(
viteStaticCopy({
targets: [
{
src: 'core/wasm/pkg/wasm_bg.wasm',
dest: 'wasm'
}
]
})
)
}
const config: UserConfig = {
plugins,
resolve: {
alias: {
$api:
TARGET === 'tauri'
? resolve('./src/api/desktop')
: resolve('./src/api/web')
}
}
}
export default config

1394
examples/web/yarn.lock Normal file

File diff suppressed because it is too large Load Diff