From a87a3c7d4491bf25589a0b24c285fcd5df3d6337 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 22 Sep 2026 11:30:47 -0300 Subject: [PATCH 01/29] chore: update documentation --- .changes/update-docs.md | 61 +++ plugins/autostart/guest-js/index.ts | 37 ++ plugins/autostart/src/lib.rs | 59 ++- plugins/barcode-scanner/guest-js/index.ts | 131 +++++- plugins/barcode-scanner/src/error.rs | 5 + plugins/barcode-scanner/src/lib.rs | 5 + plugins/biometric/guest-js/index.ts | 54 ++- plugins/biometric/src/error.rs | 6 + plugins/biometric/src/lib.rs | 11 + plugins/biometric/src/models.rs | 13 + plugins/cli/guest-js/index.ts | 11 + plugins/cli/src/error.rs | 4 + plugins/cli/src/lib.rs | 10 + plugins/clipboard-manager/guest-js/index.ts | 8 + plugins/clipboard-manager/src/desktop.rs | 25 + plugins/clipboard-manager/src/error.rs | 5 + plugins/clipboard-manager/src/lib.rs | 1 + plugins/clipboard-manager/src/mobile.rs | 40 ++ plugins/deep-link/guest-js/index.ts | 21 +- plugins/deep-link/src/error.rs | 13 + plugins/deep-link/src/lib.rs | 9 + plugins/dialog/guest-js/index.ts | 37 +- plugins/dialog/guest-js/init.ts | 7 + plugins/dialog/src/error.rs | 7 + plugins/dialog/src/lib.rs | 10 + plugins/dialog/src/models.rs | 5 + plugins/fs/guest-js/index.ts | 188 +++++++- plugins/fs/src/android.rs | 6 + plugins/fs/src/desktop.rs | 3 + plugins/fs/src/error.rs | 8 + plugins/fs/src/ios.rs | 5 + plugins/fs/src/lib.rs | 70 ++- plugins/geolocation/guest-js/index.ts | 105 ++++- plugins/geolocation/src/desktop.rs | 5 + plugins/geolocation/src/error.rs | 3 + plugins/geolocation/src/lib.rs | 1 + plugins/geolocation/src/mobile.rs | 4 + plugins/geolocation/src/models.rs | 12 +- plugins/global-shortcut/guest-js/index.ts | 12 +- plugins/global-shortcut/src/error.rs | 7 + plugins/global-shortcut/src/lib.rs | 65 ++- plugins/haptics/api-iife.js | 2 +- plugins/haptics/guest-js/bindings.ts | 14 + plugins/haptics/guest-js/index.ts | 100 +++- plugins/haptics/src/desktop.rs | 8 + plugins/haptics/src/error.rs | 5 + plugins/haptics/src/lib.rs | 1 + plugins/haptics/src/mobile.rs | 29 ++ plugins/haptics/src/models.rs | 18 + plugins/http/guest-js/index.ts | 21 + plugins/http/src/error.rs | 27 ++ plugins/http/src/lib.rs | 10 + plugins/localhost/src/lib.rs | 46 +- plugins/log/guest-js/index.ts | 100 ++-- plugins/log/src/lib.rs | 63 ++- plugins/nfc/guest-js/index.ts | 227 ++++++++- plugins/nfc/src/error.rs | 9 + plugins/nfc/src/lib.rs | 38 ++ plugins/nfc/src/models.rs | 76 +++ plugins/notification/guest-js/index.ts | 370 ++++++++++++++- plugins/notification/guest-js/init.ts | 11 + plugins/notification/src/desktop.rs | 41 ++ plugins/notification/src/error.rs | 8 + plugins/notification/src/lib.rs | 23 +- plugins/notification/src/mobile.rs | 54 ++- plugins/notification/src/models.rs | 258 +++++++++++ plugins/opener/guest-js/init.ts | 11 + plugins/opener/src/error.rs | 28 +- plugins/opener/src/lib.rs | 23 + plugins/os/guest-js/index.ts | 29 ++ plugins/os/src/error.rs | 3 + plugins/os/src/lib.rs | 7 + plugins/persisted-scope/src/lib.rs | 8 + plugins/positioner/guest-js/index.ts | 80 ++++ plugins/positioner/src/ext.rs | 25 + plugins/positioner/src/lib.rs | 10 + plugins/process/src/lib.rs | 1 + plugins/shell/guest-js/index.ts | 360 ++++++++++++++- plugins/shell/guest-js/init.ts | 11 + plugins/shell/src/error.rs | 11 + plugins/shell/src/lib.rs | 30 ++ plugins/single-instance/src/lib.rs | 31 ++ plugins/sql/guest-js/index.ts | 72 ++- plugins/sql/src/error.rs | 14 + plugins/sql/src/lib.rs | 56 +++ plugins/sql/src/wrapper.rs | 10 + plugins/store/guest-js/index.ts | 483 +++++++++++++++++++- plugins/store/src/error.rs | 3 + plugins/store/src/lib.rs | 20 + plugins/store/src/store.rs | 23 + plugins/stronghold/guest-js/index.ts | 390 ++++++++++++++-- plugins/stronghold/src/kdf.rs | 19 +- plugins/stronghold/src/lib.rs | 41 +- plugins/stronghold/src/stronghold.rs | 32 ++ plugins/updater/guest-js/index.ts | 140 +++++- plugins/updater/src/error.rs | 13 + plugins/updater/src/lib.rs | 46 ++ plugins/updater/src/updater.rs | 115 +++++ plugins/upload/guest-js/index.ts | 96 +++- plugins/upload/src/lib.rs | 16 + plugins/websocket/guest-js/index.ts | 99 ++++ plugins/websocket/src/lib.rs | 9 + plugins/window-state/guest-js/index.ts | 58 +++ plugins/window-state/src/lib.rs | 26 ++ 104 files changed, 4875 insertions(+), 222 deletions(-) create mode 100644 .changes/update-docs.md diff --git a/.changes/update-docs.md b/.changes/update-docs.md new file mode 100644 index 000000000..1d491b11d --- /dev/null +++ b/.changes/update-docs.md @@ -0,0 +1,61 @@ +--- +"autostart": patch +"barcode-scanner": patch +"biometric": patch +"cli": patch +"clipboard-manager": patch +"deep-link": patch +"dialog": patch +"fs": patch +"global-shortcut": patch +"http": patch +"localhost": patch +"log": patch +"nfc": patch +"notification": patch +"opener": patch +"os": patch +"persisted-scope": patch +"positioner": patch +"process": patch +"shell": patch +"single-instance": patch +"sql": patch +"store": patch +"stronghold": patch +"updater": patch +"upload": patch +"websocket": patch +"window-state": patch +"autostart-js": patch +"barcode-scanner-js": patch +"biometric-js": patch +"cli-js": patch +"clipboard-manager-js": patch +"deep-link-js": patch +"dialog-js": patch +"fs-js": patch +"global-shortcut-js": patch +"http-js": patch +"log-js": patch +"nfc-js": patch +"notification-js": patch +"opener-js": patch +"os-js": patch +"positioner-js": patch +"process-js": patch +"shell-js": patch +"sql-js": patch +"store-js": patch +"stronghold-js": patch +"updater-js": patch +"upload-js": patch +"websocket-js": patch +"window-state-js": patch +"haptics": patch +"haptics-js": patch +"geolocation": patch +"geolocation-js": patch +--- + +Update documentation. diff --git a/plugins/autostart/guest-js/index.ts b/plugins/autostart/guest-js/index.ts index fca8344ff..063066b7f 100644 --- a/plugins/autostart/guest-js/index.ts +++ b/plugins/autostart/guest-js/index.ts @@ -2,16 +2,53 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Automatically launch your application at startup. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' +/** + * Checks whether autostart is enabled for the application. + * @example + * ```typescript + * import { isEnabled } from '@tauri-apps/plugin-autostart'; + * const enabled = await isEnabled(); + * ``` + * + * @returns A promise resolving to `true` if the application launches at startup, `false` otherwise. + * @since 2.0.0 + */ export async function isEnabled(): Promise { return await invoke('plugin:autostart|is_enabled') } +/** + * Enables autostart for the application. + * @example + * ```typescript + * import { enable } from '@tauri-apps/plugin-autostart'; + * await enable(); + * ``` + * + * @since 2.0.0 + */ export async function enable(): Promise { await invoke('plugin:autostart|enable') } +/** + * Disables autostart for the application. + * @example + * ```typescript + * import { disable } from '@tauri-apps/plugin-autostart'; + * await disable(); + * ``` + * + * @since 2.0.0 + */ export async function disable(): Promise { await invoke('plugin:autostart|disable') } diff --git a/plugins/autostart/src/lib.rs b/plugins/autostart/src/lib.rs index 82fc56fc9..4ff9503d1 100644 --- a/plugins/autostart/src/lib.rs +++ b/plugins/autostart/src/lib.rs @@ -22,17 +22,28 @@ use std::env::current_exe; type Result = std::result::Result; +/// The strategy used to register the application for auto start on macOS. +/// +/// The builder's default is [`MacosLauncher::LaunchAgent`]. #[derive(Debug, Default, Copy, Clone)] pub enum MacosLauncher { + /// Auto start by installing a Launch Agent plist under `~/Library/LaunchAgents`, + /// which macOS starts automatically at login. #[default] LaunchAgent, + /// Auto start by adding a login item through an AppleScript command sent to the + /// "System Events" application. AppleScript, } +/// The error type of this plugin. #[derive(Debug, thiserror::Error)] pub enum Error { + /// An I/O error, for example while resolving the current executable's path. #[error(transparent)] Io(#[from] std::io::Error), + /// An error forwarded from the underlying `auto_launch` operation, converted to its + /// string representation. #[error("{0}")] Anyhow(String), } @@ -46,9 +57,19 @@ impl Serialize for Error { } } +/// Manages the auto start (launch at login) state of the application. +/// +/// An instance is created and managed as Tauri state when the plugin is built; access it +/// through [`ManagerExt::autolaunch`]. pub struct AutoLaunchManager(AutoLaunch); impl AutoLaunchManager { + /// Enables auto start, registering the application to launch at login. + /// + /// ## Errors + /// + /// Returns [`Error::Anyhow`] if the platform-specific registration fails, for example + /// when the application path does not exist or is not absolute. pub fn enable(&self) -> Result<()> { self.0 .enable() @@ -56,6 +77,11 @@ impl AutoLaunchManager { .map_err(Error::Anyhow) } + /// Disables auto start, removing the application from the list of programs launched at login. + /// + /// ## Errors + /// + /// Returns [`Error::Anyhow`] if the platform-specific removal fails. pub fn disable(&self) -> Result<()> { self.0 .disable() @@ -63,6 +89,11 @@ impl AutoLaunchManager { .map_err(Error::Anyhow) } + /// Returns whether auto start is currently enabled for the application. + /// + /// ## Errors + /// + /// Returns [`Error::Anyhow`] if the platform-specific check fails. pub fn is_enabled(&self) -> Result { self.0 .is_enabled() @@ -71,6 +102,8 @@ impl AutoLaunchManager { } } +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] +/// and [`tauri::Window`] to access the autostart APIs. pub trait ManagerExt { /// TODO: Rename these to `autostart` or `auto_start` in v3 fn autolaunch(&self) -> State<'_, AutoLaunchManager>; @@ -98,6 +131,8 @@ async fn is_enabled(manager: State<'_, AutoLaunchManager>) -> Result { manager.is_enabled() } +/// Builder for the autostart plugin, used to configure the startup arguments, application +/// name, and — on macOS — the launch strategy before calling [`Builder::build`]. #[derive(Default)] pub struct Builder { #[cfg(target_os = "macos")] @@ -116,11 +151,10 @@ impl Builder { /// /// ## Examples /// - /// ```no_run - /// Builder::new() + /// ``` + /// tauri_plugin_autostart::Builder::new() /// .arg("--from-autostart") - /// .arg("--hey") - /// .build(); + /// .arg("--hey"); /// ``` pub fn arg>(mut self, arg: S) -> Self { self.args.push(arg.into()); @@ -131,10 +165,8 @@ impl Builder { /// /// ## Examples /// - /// ```no_run - /// Builder::new() - /// .args(["--from-autostart", "--hey"]) - /// .build(); + /// ``` + /// tauri_plugin_autostart::Builder::new().args(["--from-autostart", "--hey"]); /// ``` pub fn args(mut self, args: I) -> Self where @@ -159,16 +191,19 @@ impl Builder { /// /// ## Examples /// - /// ```no_run - /// Builder::new() - /// .app_name("My Custom Name") - /// .build(); + /// ``` + /// tauri_plugin_autostart::Builder::new().app_name("My Custom Name"); /// ``` pub fn app_name>(mut self, app_name: S) -> Self { self.app_name = Some(app_name.into()); self } + /// Builds the autostart [`TauriPlugin`] with the configured options. + /// + /// On setup, it resolves the current executable's path (or the AppImage path on Linux, + /// when available) and manages an [`AutoLaunchManager`] built from it, so that + /// [`ManagerExt::autolaunch`] can be used from anywhere in the application. pub fn build(self) -> TauriPlugin { PluginBuilder::new("autostart") .invoke_handler(tauri::generate_handler![enable, disable, is_enabled]) diff --git a/plugins/barcode-scanner/guest-js/index.ts b/plugins/barcode-scanner/guest-js/index.ts index 9781b4bae..f74b32a72 100644 --- a/plugins/barcode-scanner/guest-js/index.ts +++ b/plugins/barcode-scanner/guest-js/index.ts @@ -2,6 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Scan QR codes, EAN-13 and other kinds of barcodes with the device's camera on Android and iOS. + * + * @module + */ + import { invoke, requestPermissions as requestPermissions_, @@ -10,55 +16,136 @@ import { export type { PermissionState } from '@tauri-apps/api/core' +/** + * The barcode symbologies that can be scanned, or used to restrict a scan via {@link ScanOptions.formats}. + */ export enum Format { + /** + * QR code, a two-dimensional matrix barcode. + */ QRCode = 'QR_CODE', /** + * UPC-A, a 12-digit numeric barcode commonly used on retail products in North America. + * * Not supported on iOS. */ UPC_A = 'UPC_A', + /** + * UPC-E, a compressed 6-digit variant of UPC-A used on small packaging. + */ UPC_E = 'UPC_E', + /** + * EAN-8, an 8-digit numeric barcode used on small retail packaging. + */ EAN8 = 'EAN_8', + /** + * EAN-13, a 13-digit numeric barcode used worldwide on retail products. + */ EAN13 = 'EAN_13', + /** + * Code 39, an alphanumeric barcode used in logistics and inventory tracking. + */ Code39 = 'CODE_39', + /** + * Code 93, a compact alphanumeric barcode similar to Code 39. + */ Code93 = 'CODE_93', + /** + * Code 128, a high-density alphanumeric barcode used in shipping and packaging. + */ Code128 = 'CODE_128', /** + * Codabar, a numeric barcode commonly used by libraries and blood banks. + * * Not supported on iOS. */ Codabar = 'CODABAR', + /** + * ITF (Interleaved 2 of 5), a numeric barcode encoding an even number of digits. + */ ITF = 'ITF', + /** + * Aztec code, a two-dimensional matrix barcode often used on tickets and boarding passes. + */ Aztec = 'AZTEC', + /** + * Data Matrix, a two-dimensional matrix barcode used to encode small amounts of data. + */ DataMatrix = 'DATA_MATRIX', + /** + * PDF417, a stacked linear barcode used on IDs, boarding passes and shipping labels. + */ PDF417 = 'PDF_417', /** + * GS1 DataBar, a compact barcode used to mark variable-measure items such as fresh food. + * * Not supported on Android. Requires iOS 15.4+ */ GS1DataBar = 'GS1_DATA_BAR', /** + * The limited variant of {@link Format.GS1DataBar}, encoding fewer digits in a smaller symbol. + * * Not supported on Android. Requires iOS 15.4+ */ GS1DataBarLimited = 'GS1_DATA_BAR_LIMITED', /** + * The expanded variant of {@link Format.GS1DataBar}, capable of encoding additional data such as weight. + * * Not supported on Android. Requires iOS 15.4+ */ GS1DataBarExpanded = 'GS1_DATA_BAR_EXPANDED' } +/** + * Options to configure a {@link scan} call. + */ export interface ScanOptions { + /** + * Which camera to use for scanning. Defaults to `back`. + */ cameraDirection?: 'back' | 'front' + /** + * The barcode formats to scan for. Defaults to all supported formats. + */ formats?: Format[] + /** + * Whether to show the camera in a small window instead of taking over the whole screen. Defaults to `false`. + */ windowed?: boolean } +/** + * The result of a successful {@link scan} call. + */ export interface Scanned { + /** + * The decoded content of the barcode. + */ content: string + /** + * The format of the scanned barcode. + */ format: Format + /** + * The bounding box of the scanned barcode within the camera frame, when reported by the platform. + */ bounds: unknown } /** - * Start scanning. - * @param options + * Start scanning, opening the device's camera. The returned promise resolves once a barcode + * matching the given options has been scanned, or rejects if the scan is cancelled. + * + * @example + * ```typescript + * import { scan, Format } from '@tauri-apps/plugin-barcode-scanner'; + * + * const scanned = await scan({ windowed: true, formats: [Format.QRCode] }); + * ``` + * + * @param options Configuration for the scan. + * @returns A promise resolving to the scanned barcode. + * @since 2.0.0 */ export async function scan(options?: ScanOptions): Promise { return await invoke('plugin:barcode-scanner|scan', { ...options }) @@ -66,13 +153,32 @@ export async function scan(options?: ScanOptions): Promise { /** * Cancel the current scan process. + * + * @example + * ```typescript + * import { cancel } from '@tauri-apps/plugin-barcode-scanner'; + * + * await cancel(); + * ``` + * + * @since 2.0.0 */ export async function cancel(): Promise { await invoke('plugin:barcode-scanner|cancel') } /** - * Get permission state. + * Get the current state of the camera permission. + * + * @example + * ```typescript + * import { checkPermissions } from '@tauri-apps/plugin-barcode-scanner'; + * + * const permissionState = await checkPermissions(); + * ``` + * + * @returns A promise resolving to the current state of the camera permission. + * @since 2.0.0 */ export async function checkPermissions(): Promise { return await checkPermissions_<{ camera: PermissionState }>( @@ -82,6 +188,16 @@ export async function checkPermissions(): Promise { /** * Request permissions to use the camera. + * + * @example + * ```typescript + * import { requestPermissions } from '@tauri-apps/plugin-barcode-scanner'; + * + * const permissionState = await requestPermissions(); + * ``` + * + * @returns A promise resolving to the new state of the camera permission. + * @since 2.0.0 */ export async function requestPermissions(): Promise { return await requestPermissions_<{ camera: PermissionState }>( @@ -91,6 +207,15 @@ export async function requestPermissions(): Promise { /** * Open application settings. Useful if permission was denied and the user must manually enable it. + * + * @example + * ```typescript + * import { openAppSettings } from '@tauri-apps/plugin-barcode-scanner'; + * + * await openAppSettings(); + * ``` + * + * @since 2.0.0 */ export async function openAppSettings(): Promise { await invoke('plugin:barcode-scanner|open_app_settings') diff --git a/plugins/barcode-scanner/src/error.rs b/plugins/barcode-scanner/src/error.rs index 339e763b1..5aabafd92 100644 --- a/plugins/barcode-scanner/src/error.rs +++ b/plugins/barcode-scanner/src/error.rs @@ -4,12 +4,17 @@ use serde::{ser::Serializer, Serialize}; +/// Alias for the result type returned by this crate's functions. pub type Result = std::result::Result; +/// The error type returned by this crate's APIs. Serialized as its [`Display`](std::fmt::Display) +/// string when it crosses the IPC boundary. #[derive(Debug, thiserror::Error)] pub enum Error { + /// An I/O error occurred. #[error(transparent)] Io(#[from] std::io::Error), + /// Failed to run a command on the mobile plugin implementation (Kotlin on Android, Swift on iOS). #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), diff --git a/plugins/barcode-scanner/src/lib.rs b/plugins/barcode-scanner/src/lib.rs index 2f2e7ee95..0266b7e38 100644 --- a/plugins/barcode-scanner/src/lib.rs +++ b/plugins/barcode-scanner/src/lib.rs @@ -2,6 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! Scan QR codes, EAN-13 and other kinds of barcodes with the device's camera. +//! +//! - Supported platforms: Android and iOS. + #![cfg(mobile)] use tauri::{ @@ -29,6 +33,7 @@ impl BarcodeScanner {} /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the barcode scanner APIs. pub trait BarcodeScannerExt { + /// Returns the [`BarcodeScanner`] instance managed by the app. fn barcode_scanner(&self) -> &BarcodeScanner; } diff --git a/plugins/biometric/guest-js/index.ts b/plugins/biometric/guest-js/index.ts index 5c3eb8df2..cf17bc69b 100644 --- a/plugins/biometric/guest-js/index.ts +++ b/plugins/biometric/guest-js/index.ts @@ -2,22 +2,40 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Prompt the user for biometric authentication on Android and iOS. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' +/** + * The kind of biometry hardware detected on the device. + */ export enum BiometryType { + /** No biometry hardware is available, or it is not enrolled with the operating system. */ None = 0, - // Apple TouchID or Android fingerprint + /** Apple TouchID or Android fingerprint. */ TouchID = 1, - // Apple FaceID or Android face authentication + /** Apple FaceID or Android face authentication. */ FaceID = 2, - // Android iris authentication + /** Android iris authentication. */ Iris = 3 } +/** + * The result of {@linkcode checkStatus}, describing whether biometric authentication can + * currently be used. + */ export interface Status { + /** Whether the device can currently authenticate using biometrics. */ isAvailable: boolean + /** The kind of biometry hardware detected on the device, even when {@linkcode isAvailable} is `false`. */ biometryType: BiometryType + /** A human-readable reason why biometric authentication is unavailable. Only set when {@linkcode isAvailable} is `false`. */ error?: string + /** A platform-specific error code describing why biometric authentication is unavailable. Only set when {@linkcode isAvailable} is `false`. */ errorCode?: | 'appCancel' | 'authenticationFailed' @@ -32,23 +50,43 @@ export interface Status { | 'biometryNotEnrolled' } +/** + * Options for the {@linkcode authenticate} biometric prompt. + */ export interface AuthOptions { + /** Enables authentication using the device's password or PIN. Available on both Android and iOS. */ allowDeviceCredential?: boolean + /** Label for the cancel button. Available on both Android and iOS. */ cancelTitle?: string // iOS options + /** Text displayed on the fallback button if biometric authentication fails. **iOS only.** */ fallbackTitle?: string // android options + /** Title indicating the purpose of the biometric verification. **Android only.** */ title?: string + /** Subtitle providing contextual information of the biometric verification. **Android only.** */ subtitle?: string + /** Whether additional user confirmation is required, such as pressing a button, after successful biometric authentication. **Android only.** */ confirmationRequired?: boolean + /** Maximum number of attempts allowed before the prompt is dismissed. Defaults to `3`. **Android only.** */ maxAttemps?: number } /** * Checks if the biometric authentication is available. + * @example + * ```typescript + * import { checkStatus } from '@tauri-apps/plugin-biometric'; + * + * const status = await checkStatus(); + * if (status.isAvailable) { + * // do something + * } + * ``` * @returns a promise resolving to an object containing all the information about the status of the biometry. + * @since 2.0.0 */ export async function checkStatus(): Promise { return await invoke('plugin:biometric|status') @@ -58,13 +96,15 @@ export async function checkStatus(): Promise { * Prompts the user for authentication using the system interface (touchID, faceID or Android Iris). * Rejects if the authentication fails. * - * ```javascript + * @example + * ```typescript * import { authenticate } from "@tauri-apps/plugin-biometric"; * await authenticate('Open your wallet'); * ``` - * @param reason - * @param options - * @returns + * @param reason A message shown to the user explaining why authentication is requested. + * @param options Configuration for the biometric prompt. + * @returns a promise resolving to `void` once the user is successfully authenticated. + * @since 2.0.0 */ export async function authenticate( reason: string, diff --git a/plugins/biometric/src/error.rs b/plugins/biometric/src/error.rs index 339e763b1..1be5a6639 100644 --- a/plugins/biometric/src/error.rs +++ b/plugins/biometric/src/error.rs @@ -4,12 +4,18 @@ use serde::{ser::Serializer, Serialize}; +/// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. pub type Result = std::result::Result; +/// The error types returned by this plugin. #[derive(Debug, thiserror::Error)] pub enum Error { + /// An I/O error occurred. #[error(transparent)] Io(#[from] std::io::Error), + /// The invocation of the underlying Android or iOS plugin failed, for example because + /// biometric authentication was unavailable, was not enrolled, failed, or was canceled by + /// the user. #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), diff --git a/plugins/biometric/src/lib.rs b/plugins/biometric/src/lib.rs index f79a104d3..01303d514 100644 --- a/plugins/biometric/src/lib.rs +++ b/plugins/biometric/src/lib.rs @@ -2,6 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! Prompt the user for biometric authentication. +//! +//! - Supported platforms: Android and iOS. + #![cfg(mobile)] use serde::Serialize; @@ -34,10 +38,16 @@ struct AuthenticatePayload { } impl Biometric { + /// Checks the device's availability and type of biometric authentication, as reported by the + /// operating system. Errors if the underlying mobile plugin invocation fails. pub fn status(&self) -> crate::Result { self.0.run_mobile_plugin("status", ()).map_err(Into::into) } + /// Prompts the user for biometric authentication using the system UI (Android + /// `BiometricPrompt` or iOS `LocalAuthentication`), showing `reason` as the purpose of the + /// request. Resolves once the user is authenticated and errors if authentication fails, is + /// canceled, or the underlying mobile plugin invocation fails. pub fn authenticate(&self, reason: String, options: AuthOptions) -> crate::Result<()> { self.0 .run_mobile_plugin("authenticate", AuthenticatePayload { reason, options }) @@ -47,6 +57,7 @@ impl Biometric { /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the biometric APIs. pub trait BiometricExt { + /// Returns the [`Biometric`] instance managed by the app. fn biometric(&self) -> &Biometric; } diff --git a/plugins/biometric/src/models.rs b/plugins/biometric/src/models.rs index 49c843004..33547d364 100644 --- a/plugins/biometric/src/models.rs +++ b/plugins/biometric/src/models.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; +/// Options for [`Biometric::authenticate`](crate::Biometric::authenticate). #[derive(Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct AuthOptions { @@ -21,19 +22,31 @@ pub struct AuthOptions { pub confirmation_required: Option, } +/// The kind of biometry hardware detected on the device. #[derive(Debug, Clone, serde_repr::Deserialize_repr)] #[repr(u8)] pub enum BiometryType { + /// No biometry hardware is available, or it is not enrolled with the operating system. None = 0, + /// Fingerprint authentication (Apple Touch ID or Android fingerprint). TouchID = 1, + /// Face authentication (Apple Face ID or Android face authentication). FaceID = 2, } +/// The result of [`Biometric::status`](crate::Biometric::status), describing whether biometric +/// authentication can currently be used. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Status { + /// Whether the device can currently authenticate using biometrics. pub is_available: bool, + /// The kind of biometry hardware detected on the device, even when [`Self::is_available`] is `false`. pub biometry_type: BiometryType, + /// A human-readable reason why biometric authentication is unavailable. Only set when + /// [`Self::is_available`] is `false`. pub error: Option, + /// A platform-specific error code describing why biometric authentication is unavailable. + /// Only set when [`Self::is_available`] is `false`. pub error_code: Option, } diff --git a/plugins/cli/guest-js/index.ts b/plugins/cli/guest-js/index.ts index 7a7f4acf3..153a4ffe2 100644 --- a/plugins/cli/guest-js/index.ts +++ b/plugins/cli/guest-js/index.ts @@ -11,6 +11,8 @@ import { invoke } from '@tauri-apps/api/core' /** + * The resolution of a single CLI argument match. + * * @since 2.0.0 */ interface ArgMatch { @@ -27,18 +29,26 @@ interface ArgMatch { } /** + * The result of matching a subcommand of the CLI definition. + * * @since 2.0.0 */ interface SubcommandMatch { + /** The name of the matched subcommand. */ name: string + /** The argument matches of the subcommand, resolved the same way as the parent command's matches. */ matches: CliMatches } /** + * The resolved matches of the CLI arguments and, if any, its matched subcommand. + * * @since 2.0.0 */ interface CliMatches { + /** The matched arguments, keyed by argument name. */ args: Record + /** The matched subcommand, or `null` if no subcommand was invoked. */ subcommand: SubcommandMatch | null } @@ -61,6 +71,7 @@ interface CliMatches { * } * ``` * + * @returns A promise resolving to the parsed CLI matches. * @since 2.0.0 */ async function getMatches(): Promise { diff --git a/plugins/cli/src/error.rs b/plugins/cli/src/error.rs index 2b5e16029..66455352a 100644 --- a/plugins/cli/src/error.rs +++ b/plugins/cli/src/error.rs @@ -4,8 +4,11 @@ use serde::{Serialize, Serializer}; +/// Errors that can be returned from the CLI plugin's commands. #[derive(Debug, thiserror::Error)] pub enum Error { + /// Parsing the process arguments against the CLI definition in `tauri.conf.json` failed, + /// e.g. because a required argument is missing or an unknown flag was passed. #[error("failed to parse arguments: {0}")] ParseCli(#[from] clap::Error), } @@ -19,4 +22,5 @@ impl Serialize for Error { } } +/// Alias for a [`std::result::Result`] with the error type [`Error`]. pub type Result = std::result::Result; diff --git a/plugins/cli/src/lib.rs b/plugins/cli/src/lib.rs index e927a348b..ad133672f 100644 --- a/plugins/cli/src/lib.rs +++ b/plugins/cli/src/lib.rs @@ -25,19 +25,27 @@ use config::{Arg, Config}; pub use error::{Error, Result}; pub use parser::{ArgData, Matches, SubcommandMatches}; +/// Access to the CLI APIs, managed by the app once the plugin is initialized. pub struct Cli(PluginApi); impl Cli { + /// Parses the arguments the current process was started with against the CLI definition + /// configured under `plugins.cli` in `tauri.conf.json` and returns the resolved + /// [`parser::Matches`]. Errors if the arguments do not satisfy the CLI definition. pub fn matches(&self) -> Result { parser::get_matches(self.0.config(), self.0.app().package_info(), None) } + /// Same as [`Self::matches`], but parses the given `args` instead of the current process' + /// arguments. Errors if `args` does not satisfy the CLI definition. pub fn matches_from(&self, args: Vec) -> Result { parser::get_matches(self.0.config(), self.0.app().package_info(), Some(args)) } } +/// Extension trait to access the CLI APIs. pub trait CliExt { + /// Returns the [`Cli`] instance managed by the app. fn cli(&self) -> &Cli; } @@ -52,6 +60,8 @@ fn cli_matches(_app: AppHandle, cli: State<'_, Cli>) -> Result cli.matches() } +/// Initializes the plugin, reading the CLI definition from the `plugins.cli` object in +/// `tauri.conf.json`. pub fn init() -> TauriPlugin { Builder::new("cli") .invoke_handler(tauri::generate_handler![cli_matches]) diff --git a/plugins/clipboard-manager/guest-js/index.ts b/plugins/clipboard-manager/guest-js/index.ts index a37bbfab1..bea789ee1 100644 --- a/plugins/clipboard-manager/guest-js/index.ts +++ b/plugins/clipboard-manager/guest-js/index.ts @@ -20,6 +20,9 @@ import { Image, transformImage } from '@tauri-apps/api/image' * assert(await readText(), 'Tauri is awesome!'); * ``` * + * @param text The plain text to write to the clipboard. + * @param opts Additional configuration for the write operation. + * @param opts.label A label describing the copied content. **Android only**, ignored on other platforms. * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 @@ -41,6 +44,7 @@ async function writeText( * import { readText } from '@tauri-apps/plugin-clipboard-manager'; * const clipboardText = await readText(); * ``` + * @returns A promise resolving to the clipboard contents as plain text. * @since 2.0.0 */ async function readText(): Promise { @@ -67,6 +71,7 @@ async function readText(): Promise { * await writeImage(buffer); * ``` * + * @param image The image to write, as a path, raw RGBA bytes, or an existing {@link Image}. * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 @@ -94,6 +99,7 @@ async function writeImage( * const blob = new Blob([await clipboardImage.rgba()], { type: 'image' }) * const url = URL.createObjectURL(blob) * ``` + * @returns A promise resolving to the clipboard contents as an {@link Image}. * @since 2.0.0 */ async function readImage(): Promise { @@ -119,6 +125,8 @@ async function readImage(): Promise { * assert(await readText(), '

Tauri is awesome!

'); * ``` * + * @param html The HTML markup to write to the clipboard. + * @param altText The plain text fallback written alongside the HTML, used by targets that cannot render it. * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 diff --git a/plugins/clipboard-manager/src/desktop.rs b/plugins/clipboard-manager/src/desktop.rs index f3570cc0c..b31b509cd 100644 --- a/plugins/clipboard-manager/src/desktop.rs +++ b/plugins/clipboard-manager/src/desktop.rs @@ -28,6 +28,12 @@ pub struct Clipboard { } impl Clipboard { + /// Writes plain text to the system clipboard. + /// + /// # Errors + /// + /// Returns [`crate::Error::Clipboard`] if the clipboard could not be initialized or the + /// underlying [`arboard`] operation fails. pub fn write_text<'a, T: Into>>(&self, text: T) -> crate::Result<()> { match &self.clipboard { Ok(clipboard) => clipboard @@ -41,6 +47,12 @@ impl Clipboard { } } + /// Writes an image to the system clipboard as RGBA data. + /// + /// # Errors + /// + /// Returns [`crate::Error::Clipboard`] if the clipboard could not be initialized or the + /// underlying [`arboard`] operation fails. pub fn write_image(&self, image: &Image<'_>) -> crate::Result<()> { match &self.clipboard { Ok(clipboard) => clipboard @@ -69,6 +81,13 @@ impl Clipboard { } } + /// Writes HTML to the system clipboard, with an optional plain text fallback for targets + /// that cannot render it. + /// + /// # Errors + /// + /// Returns [`crate::Error::Clipboard`] if the clipboard could not be initialized or the + /// underlying [`arboard`] operation fails. pub fn write_html<'a, T: Into>>( &self, html: T, @@ -86,6 +105,12 @@ impl Clipboard { } } + /// Clears the system clipboard. + /// + /// # Errors + /// + /// Returns [`crate::Error::Clipboard`] if the clipboard could not be initialized or the + /// underlying [`arboard`] operation fails. pub fn clear(&self) -> crate::Result<()> { match &self.clipboard { Ok(clipboard) => clipboard diff --git a/plugins/clipboard-manager/src/error.rs b/plugins/clipboard-manager/src/error.rs index 1b8cf482b..6f7e6c4d9 100644 --- a/plugins/clipboard-manager/src/error.rs +++ b/plugins/clipboard-manager/src/error.rs @@ -4,15 +4,20 @@ use serde::{ser::Serializer, Serialize}; +/// Alias for `Result` used throughout this crate. pub type Result = std::result::Result; +/// Errors that can occur while interacting with the system clipboard. #[derive(Debug, thiserror::Error)] pub enum Error { + /// Forwarding a request to, or receiving a response from, the mobile plugin failed. #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + /// The underlying clipboard operation failed, or the operation is not supported on this platform. #[error("{0}")] Clipboard(String), + /// An error forwarded from the Tauri core. #[error(transparent)] Tauri(#[from] tauri::Error), } diff --git a/plugins/clipboard-manager/src/lib.rs b/plugins/clipboard-manager/src/lib.rs index 0cbb4e41e..84ea87e43 100644 --- a/plugins/clipboard-manager/src/lib.rs +++ b/plugins/clipboard-manager/src/lib.rs @@ -31,6 +31,7 @@ pub use mobile::Clipboard; /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the clipboard APIs. pub trait ClipboardExt { + /// Returns a handle to the [`Clipboard`] APIs. fn clipboard(&self) -> &Clipboard; } diff --git a/plugins/clipboard-manager/src/mobile.rs b/plugins/clipboard-manager/src/mobile.rs index 72d5f6e06..7eae15552 100644 --- a/plugins/clipboard-manager/src/mobile.rs +++ b/plugins/clipboard-manager/src/mobile.rs @@ -34,6 +34,11 @@ pub fn init( pub struct Clipboard(PluginHandle); impl Clipboard { + /// Writes plain text to the system clipboard, without a label. + /// + /// # Errors + /// + /// Returns [`crate::Error::PluginInvoke`] if the underlying mobile plugin call fails. pub fn write_text<'a, T: Into>>(&self, text: T) -> crate::Result<()> { let text = text.into().to_string(); self.0 @@ -41,6 +46,13 @@ impl Clipboard { .map_err(Into::into) } + /// Writes plain text to the system clipboard along with a label describing the content. + /// + /// The label is only used on Android (it becomes the `ClipData` label); iOS ignores it. + /// + /// # Errors + /// + /// Returns [`crate::Error::PluginInvoke`] if the underlying mobile plugin call fails. pub fn write_text_with_label<'a, T: Into>>( &self, text: T, @@ -59,12 +71,22 @@ impl Clipboard { .map_err(Into::into) } + /// Not supported on mobile. + /// + /// # Errors + /// + /// Always returns [`crate::Error::Clipboard`]. pub fn write_image(&self, _image: &Image<'_>) -> crate::Result<()> { Err(crate::Error::Clipboard( "Unsupported on this platform".to_string(), )) } + /// Reads the system clipboard as plain text. + /// + /// # Errors + /// + /// Returns [`crate::Error::PluginInvoke`] if the underlying mobile plugin call fails. pub fn read_text(&self) -> crate::Result { self.0 .run_mobile_plugin("readText", ()) @@ -74,6 +96,11 @@ impl Clipboard { .map_err(Into::into) } + /// Not supported on mobile. + /// + /// # Errors + /// + /// Always returns [`crate::Error::Clipboard`]. pub fn read_image(&self) -> crate::Result> { Err(crate::Error::Clipboard( "Unsupported on this platform".to_string(), @@ -81,6 +108,11 @@ impl Clipboard { } // Treat HTML as unsupported on mobile until tested + /// Not supported on mobile. + /// + /// # Errors + /// + /// Always returns [`crate::Error::Clipboard`]. pub fn write_html<'a, T: Into>>( &self, _html: T, @@ -91,6 +123,14 @@ impl Clipboard { )) } + /// Clears the system clipboard. + /// + /// On Android this only works on SDK 28 and above; on older versions the clipboard is + /// instead overwritten with an empty string. + /// + /// # Errors + /// + /// Returns [`crate::Error::PluginInvoke`] if the underlying mobile plugin call fails. pub fn clear(&self) -> crate::Result<()> { self.0.run_mobile_plugin("clear", ()).map_err(Into::into) } diff --git a/plugins/deep-link/guest-js/index.ts b/plugins/deep-link/guest-js/index.ts index e766a4547..41b2fe882 100644 --- a/plugins/deep-link/guest-js/index.ts +++ b/plugins/deep-link/guest-js/index.ts @@ -2,6 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Set your Tauri application as the default handler for a URL, or check which URL(s) it was opened with. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' import { type UnlistenFn, listen } from '@tauri-apps/api/event' @@ -17,9 +23,10 @@ import { type UnlistenFn, listen } from '@tauri-apps/api/event' * #### Platform-specific * * - **Windows / Linux:** This function reads the command line arguments and checks if there's only one value, which must be an URL with scheme matching one of the configured values. - * Note that you must manually check the arguments when registering deep link schemes dynamically with [`Self::register`]. + * Note that you must manually check the arguments when registering deep link schemes dynamically with {@link register}. * Additionally, the deep link might have been provided as a CLI argument so you should check if its format matches what you expect. * + * @returns A promise resolving to the list of URLs that triggered the deep link, or `null` if the app was not started via a deep link. * @since 2.0.0 */ export async function getCurrent(): Promise { @@ -41,6 +48,7 @@ export async function getCurrent(): Promise { * * - **macOS / Android / iOS:** Unsupported. * + * @returns A promise that resolves once the protocol has been registered. * @since 2.0.0 */ export async function register(protocol: string): Promise { @@ -60,8 +68,11 @@ export async function register(protocol: string): Promise { * * #### Platform-specific * - * - **macOS / Linux / Android / iOS:** Unsupported. + * - **Windows:** Requires admin rights if the protocol is registered on the local machine (this can happen when registered from the NSIS installer when the install mode is set to both or per machine). + * - **Linux:** Can only unregister the scheme if it was initially registered with {@link register}. May not work on older distros. + * - **macOS / Android / iOS:** Unsupported. * + * @returns A promise that resolves once the protocol has been unregistered. * @since 2.0.0 */ export async function unregister(protocol: string): Promise { @@ -83,6 +94,7 @@ export async function unregister(protocol: string): Promise { * * - **macOS / Android / iOS:** Unsupported. * + * @returns A promise resolving to `true` if the app is the default handler for the protocol, `false` otherwise. * @since 2.0.0 */ export async function isRegistered(protocol: string): Promise { @@ -90,9 +102,9 @@ export async function isRegistered(protocol: string): Promise { } /** - * Helper function for the `deep-link://new-url` event to run a function each time the protocol is triggered while the app is running. Use `getCurrent` on app load to check whether your app was started via a deep link. + * Helper function for the `deep-link://new-url` event to run a function each time the protocol is triggered while the app is running. Use {@link getCurrent} on app load to check whether your app was started via a deep link. * - * @param protocol The name of the protocol without `://`. + * @param handler The function to call with the list of URLs the app was requested to open, every time this happens while the app is running. * * @example * ```typescript @@ -104,6 +116,7 @@ export async function isRegistered(protocol: string): Promise { * * - **Windows / Linux:** Unsupported without the single-instance plugin. The OS will spawn a new app instance passing the URL as a CLI argument. * + * @returns A promise resolving to a function that unregisters the event listener. * @since 2.0.0 */ export async function onOpenUrl( diff --git a/plugins/deep-link/src/error.rs b/plugins/deep-link/src/error.rs index 41eb764f1..7832995fe 100644 --- a/plugins/deep-link/src/error.rs +++ b/plugins/deep-link/src/error.rs @@ -4,25 +4,38 @@ use serde::{ser::Serializer, Serialize}; +/// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. pub type Result = std::result::Result; +/// The error type for this plugin. #[derive(Debug, thiserror::Error)] pub enum Error { + /// The requested operation (usually registering or unregistering a protocol scheme at + /// runtime) is not supported on the current platform. #[error("unsupported platform")] UnsupportedPlatform, + /// Transparent wrapper around an [`std::io::Error`]. #[error(transparent)] Io(#[from] std::io::Error), + /// Transparent wrapper around a [`tauri::Error`]. #[error(transparent)] Tauri(#[from] tauri::Error), + /// Transparent wrapper around a [`windows_result::Error`]. Only used on Windows. #[cfg(target_os = "windows")] #[error(transparent)] Windows(#[from] windows_result::Error), + /// Transparent wrapper around an [`ini::Error`], returned when reading or writing the + /// `.desktop` file used to register a protocol scheme. Only used on Linux. #[cfg(target_os = "linux")] #[error(transparent)] Ini(#[from] ini::Error), + /// Transparent wrapper around an [`ini::ParseError`], returned when parsing the + /// `.desktop` file used to register a protocol scheme. Only used on Linux. #[cfg(target_os = "linux")] #[error(transparent)] ParseIni(#[from] ini::ParseError), + /// Transparent wrapper around a [`tauri::plugin::mobile::PluginInvokeError`], returned + /// when the underlying mobile plugin invocation fails. Only used on mobile. #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), diff --git a/plugins/deep-link/src/lib.rs b/plugins/deep-link/src/lib.rs index 55476bd78..eb5443159 100644 --- a/plugins/deep-link/src/lib.rs +++ b/plugins/deep-link/src/lib.rs @@ -2,6 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! Set your Tauri application as the default handler for a URL, or check which URL(s) it was +//! opened with. +//! +//! On Windows and Linux, protocol schemes can additionally be registered and unregistered at +//! runtime with [`DeepLink::register`] and [`DeepLink::unregister`]. On macOS, Android and iOS +//! the schemes declared in the Tauri configuration are registered at build time instead, so +//! calling those methods returns [`Error::UnsupportedPlatform`]. + use tauri::{ plugin::{Builder, PluginApi, TauriPlugin}, AppHandle, EventId, Listener, Manager, Runtime, @@ -479,6 +487,7 @@ use url::Url; /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the deep-link APIs. pub trait DeepLinkExt { + /// Returns a reference to the [`DeepLink`] API. fn deep_link(&self) -> &DeepLink; } diff --git a/plugins/dialog/guest-js/index.ts b/plugins/dialog/guest-js/index.ts index 90d6b26ae..2ca57810a 100644 --- a/plugins/dialog/guest-js/index.ts +++ b/plugins/dialog/guest-js/index.ts @@ -2,6 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Native system dialogs for opening and saving files, along with message, ask and confirm dialogs. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' /** @@ -10,7 +16,7 @@ import { invoke } from '@tauri-apps/api/core' * @since 2.0.0 */ interface DialogFilter { - /** Filter name. */ + /** The name of the filter, shown to the user in the dialog's filter selector. */ name: string /** * Extensions to filter, without a `.` prefix. @@ -171,11 +177,11 @@ type BanExcept = Partial< * @since 2.4.0 */ export type MessageDialogButtonsYesNoCancel = { - /** The Yes button. */ + /** The label of the Yes button. */ yes: string - /** The No button. */ + /** The label of the No button. */ no: string - /** The Cancel button. */ + /** The label of the Cancel button. */ cancel: string } & BanExcept<'yes' | 'no' | 'cancel'> @@ -185,9 +191,9 @@ export type MessageDialogButtonsYesNoCancel = { * @since 2.4.0 */ export type MessageDialogButtonsOkCancel = { - /** The Ok button. */ + /** The label of the Ok button. */ ok: string - /** The Cancel button. */ + /** The label of the Cancel button. */ cancel: string } & BanExcept<'ok' | 'cancel'> @@ -197,7 +203,7 @@ export type MessageDialogButtonsOkCancel = { * @since 2.4.0 */ export type MessageDialogButtonsOk = { - /** The Ok button. */ + /** The label of the Ok button. */ ok: string } & BanExcept<'ok'> @@ -221,6 +227,8 @@ export type MessageDialogButtons = | MessageDialogCustomButtons /** + * Options for the message dialog. + * * @since 2.0.0 */ interface MessageDialogOptions { @@ -282,6 +290,11 @@ function buttonsToRust(buttons: MessageDialogButtons | undefined) { return undefined } +/** + * Options for the {@linkcode ask} and {@linkcode confirm} dialogs. + * + * @since 2.0.0 + */ interface ConfirmDialogOptions { /** The title of the dialog. Defaults to the app name. */ title?: string @@ -293,6 +306,12 @@ interface ConfirmDialogOptions { cancelLabel?: string } +/** + * The return type of {@linkcode open}, derived from its options: a single path, an array of + * paths when {@linkcode OpenDialogOptions.multiple} is `true`, or `null` when the user cancels. + * + * @since 2.0.0 + */ type OpenDialogReturn = T['directory'] extends true ? T['multiple'] extends true ? string[] | null @@ -349,6 +368,8 @@ type OpenDialogReturn = T['directory'] extends true * } * ``` * + * @param options The dialog's options. + * * @returns A promise resolving to the selected path(s) * * @since 2.0.0 @@ -383,6 +404,8 @@ async function open( * }); * ``` * + * @param options The dialog's options. + * * @returns A promise resolving to the selected path. * * @since 2.0.0 diff --git a/plugins/dialog/guest-js/init.ts b/plugins/dialog/guest-js/init.ts index 520a469a7..f272c1bd5 100644 --- a/plugins/dialog/guest-js/init.ts +++ b/plugins/dialog/guest-js/init.ts @@ -2,6 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Injected script that replaces `window.alert` and `window.confirm` with implementations backed + * by native dialogs. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' window.alert = function (message: string) { diff --git a/plugins/dialog/src/error.rs b/plugins/dialog/src/error.rs index 0c3ed5b86..dac05b179 100644 --- a/plugins/dialog/src/error.rs +++ b/plugins/dialog/src/error.rs @@ -4,21 +4,28 @@ use serde::{ser::Serializer, Serialize}; +/// Alias for `Result` used throughout this crate. pub type Result = std::result::Result; +/// Errors that can occur while showing or interacting with a dialog. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { + /// An error forwarded from the Tauri core. #[error(transparent)] Tauri(#[from] tauri::Error), + /// An I/O error, for example while resolving a picked path. #[error(transparent)] Io(#[from] std::io::Error), + /// Forwarding a request to, or receiving a response from, the mobile plugin failed. #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + /// The folder picker was requested through the `open` command, but folder picking is not implemented on mobile. #[cfg(mobile)] #[error("Folder picker is not implemented on mobile")] FolderPickerNotImplemented, + /// An error forwarded from the `fs` plugin, returned when granting filesystem scope to a picked path fails. #[error(transparent)] Fs(#[from] tauri_plugin_fs::Error), } diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index cfb344509..024df8ce8 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -49,19 +49,28 @@ pub use desktop::Dialog; #[cfg(mobile)] pub use mobile::Dialog; +/// The preferred mode of the file picker on mobile platforms (iOS and Android), which have +/// distinct file and media pickers. On desktop, this option is ignored. #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "lowercase")] pub enum PickerMode { + /// Show the generic document picker. Document, + /// Show the media picker, allowing both images and videos to be selected. Media, + /// Show the media picker restricted to images. Image, + /// Show the media picker restricted to videos. Video, } +/// The file access mode of the dialog, used to control how a picked file is exposed to the app on iOS. #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "lowercase")] pub enum FileAccessMode { + /// Copy the picked file into the app's sandbox so it can be freely read, edited or deleted. Copy, + /// Keep the file at its original location and let the system manage security-scoped access to it. Scoped, } @@ -86,6 +95,7 @@ macro_rules! blocking_fn { /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the dialog APIs. pub trait DialogExt { + /// Returns the [`Dialog`] instance associated with this app/window. fn dialog(&self) -> &Dialog; } diff --git a/plugins/dialog/src/models.rs b/plugins/dialog/src/models.rs index 330388f8e..2c4f6ec78 100644 --- a/plugins/dialog/src/models.rs +++ b/plugins/dialog/src/models.rs @@ -69,11 +69,16 @@ pub enum MessageDialogButtons { /// Result of a message dialog #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] pub enum MessageDialogResult { + /// The user pressed the `Yes` button. Yes, + /// The user pressed the `No` button. No, + /// The user pressed the `Ok` button. Ok, + /// The user pressed the `Cancel` button, or closed the dialog without pressing a button. #[default] Cancel, + /// The user pressed a button with a custom label, holding that label's text. #[serde(untagged)] Custom(String), } diff --git a/plugins/fs/guest-js/index.ts b/plugins/fs/guest-js/index.ts index 1bbe0ec6d..2bea26943 100644 --- a/plugins/fs/guest-js/index.ts +++ b/plugins/fs/guest-js/index.ts @@ -88,9 +88,15 @@ import { BaseDirectory } from '@tauri-apps/api/path' import { Channel, invoke, Resource } from '@tauri-apps/api/core' +/** + * Defines how the offset given to {@linkcode FileHandle.seek} is interpreted. + */ enum SeekMode { + /** The offset is relative to the start of the file. */ Start = 0, + /** The offset is relative to the current cursor position. */ Current = 1, + /** The offset is relative to the end of the file. */ End = 2 } @@ -322,6 +328,8 @@ class FileHandle extends Resource { * await file.close(); * ``` * + * @param buffer The buffer the file contents are read into. + * @returns A promise resolving to the number of bytes read, or `null` when the end of the file was reached. * @since 2.0.0 */ async read(buffer: Uint8Array): Promise { @@ -377,6 +385,9 @@ class FileHandle extends Resource { * await file.close(); * ``` * + * @param offset The number of bytes the cursor is moved by. + * @param whence Defines the position the `offset` is relative to. + * @returns A promise resolving to the new cursor position, relative to the start of the file. * @since 2.0.0 */ async seek(offset: number, whence: SeekMode): Promise { @@ -399,6 +410,7 @@ class FileHandle extends Resource { * await file.close(); * ``` * + * @returns A promise resolving to the metadata of this file. * @since 2.0.0 */ async stat(): Promise { @@ -431,6 +443,7 @@ class FileHandle extends Resource { * await file.close(); * ``` * + * @param len The length the file is truncated or extended to, in bytes. When not provided the entire file contents are truncated. * @since 2.0.0 */ async truncate(len?: number): Promise { @@ -458,6 +471,8 @@ class FileHandle extends Resource { * await file.close(); * ``` * + * @param data The bytes written to the file. + * @returns A promise resolving to the number of bytes written. * @since 2.0.0 */ async write(data: Uint8Array): Promise { @@ -469,6 +484,8 @@ class FileHandle extends Resource { } /** + * Options for the `create` function, which creates or truncates a file. + * * @since 2.0.0 */ interface CreateOptions { @@ -488,6 +505,9 @@ interface CreateOptions { * await file.close(); * ``` * + * @param path The path of the file, relative to `options.baseDir` when it is provided. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the handle of the created file. * @since 2.0.0 */ async function create( @@ -507,6 +527,8 @@ async function create( } /** + * Options for the `open` function, defining how the file is opened and which operations are allowed on it. + * * @since 2.0.0 */ interface OpenOptions { @@ -573,6 +595,9 @@ interface OpenOptions { * await file.close(); * ``` * + * @param path The path of the file, relative to `options.baseDir` when it is provided. + * @param options Options defining the base directory of `path` and how the file is opened. + * @returns A promise resolving to the handle of the open file. * @since 2.0.0 */ async function open( @@ -592,6 +617,8 @@ async function open( } /** + * Options for the `copyFile` function, defining the base directory of each path. + * * @since 2.0.0 */ interface CopyFileOptions { @@ -609,6 +636,9 @@ interface CopyFileOptions { * await copyFile('app.conf', 'app.conf.bk', { fromPathBaseDir: BaseDirectory.AppConfig, toPathBaseDir: BaseDirectory.AppConfig }); * ``` * + * @param fromPath The path of the file to copy from. + * @param toPath The path of the file to copy to. + * @param options Options defining the base directory of each path. * @since 2.0.0 */ async function copyFile( @@ -631,6 +661,8 @@ async function copyFile( } /** + * Options for the `mkdir` function, which creates a directory. + * * @since 2.0.0 */ interface MkdirOptions { @@ -652,6 +684,8 @@ interface MkdirOptions { * await mkdir('users', { baseDir: BaseDirectory.AppLocalData }); * ``` * + * @param path The path of the directory to create. + * @param options Options defining the base directory of `path`, the directory permissions and whether intermediate directories are created. * @since 2.0.0 */ async function mkdir( @@ -669,6 +703,8 @@ async function mkdir( } /** + * Options for the `readDir` function, which lists the entries of a directory. + * * @since 2.0.0 */ interface ReadDirOptions { @@ -700,20 +736,23 @@ interface DirEntry { * ```typescript * import { readDir, BaseDirectory } from '@tauri-apps/plugin-fs'; * import { join } from '@tauri-apps/api/path'; - * const dir = "users" - * const entries = await readDir('users', { baseDir: BaseDirectory.AppLocalData }); - * processEntriesRecursively(dir, entries); + * const dir = 'users'; + * const entries = await readDir(dir, { baseDir: BaseDirectory.AppLocalData }); + * await processEntriesRecursively(dir, entries); * async function processEntriesRecursively(parent, entries) { * for (const entry of entries) { * console.log(`Entry: ${entry.name}`); * if (entry.isDirectory) { - * const dir = await join(parent, entry.name); - * processEntriesRecursively(dir, await readDir(dir, { baseDir: BaseDirectory.AppLocalData })) + * const entryPath = await join(parent, entry.name); + * await processEntriesRecursively(entryPath, await readDir(entryPath, { baseDir: BaseDirectory.AppLocalData })); * } * } * } * ``` * + * @param path The path of the directory to read. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the list of entries in the directory. * @since 2.0.0 */ async function readDir( @@ -731,6 +770,8 @@ async function readDir( } /** + * Options for the functions that read a file, such as `readFile` and `readTextFile`. + * * @since 2.0.0 */ interface ReadFileOptions { @@ -749,6 +790,9 @@ interface ReadFileOptions { * const contents = await readFile('avatar.png', { baseDir: BaseDirectory.Resource }); * ``` * + * @param path The path of the file to read. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the contents of the file as bytes. * @since 2.0.0 */ async function readFile( @@ -775,6 +819,9 @@ async function readFile( * const contents = await readTextFile('app.conf', { baseDir: BaseDirectory.AppConfig }); * ``` * + * @param path The path of the file to read. + * @param options Options defining the base directory of `path` and the text encoding. + * @returns A promise resolving to the contents of the file as a string. * @since 2.0.0 */ async function readTextFile( @@ -808,6 +855,9 @@ async function readTextFile( * You could also call {@linkcode AsyncIterableIterator.next} to advance the * iterator so you can lazily read the next line whenever you want. * + * @param path The path of the file to read. + * @param options Options defining the base directory of `path` and the text encoding. + * @returns A promise resolving to an iterator over the lines of the file. * @since 2.0.0 */ async function readTextFileLines( @@ -873,6 +923,8 @@ async function readTextFileLines( } /** + * Options for the `remove` function, which deletes a file or a directory. + * * @since 2.0.0 */ interface RemoveOptions { @@ -892,6 +944,8 @@ interface RemoveOptions { * await remove('users', { baseDir: BaseDirectory.AppLocalData }); * ``` * + * @param path The path of the file or directory to remove. + * @param options Options defining the base directory of `path` and whether directories are removed recursively. * @since 2.0.0 */ async function remove( @@ -909,6 +963,8 @@ async function remove( } /** + * Options for the `rename` function, defining the base directory of each path. + * * @since 2.0.0 */ interface RenameOptions { @@ -931,6 +987,9 @@ interface RenameOptions { * await rename('avatar.png', 'deleted.png', { oldPathBaseDir: BaseDirectory.App, newPathBaseDir: BaseDirectory.AppLocalData }); * ``` * + * @param oldPath The path of the file or directory to rename. + * @param newPath The path the file or directory is renamed to. + * @param options Options defining the base directory of each path. * @since 2.0.0 */ async function rename( @@ -953,6 +1012,8 @@ async function rename( } /** + * Options for the `stat` and `lstat` functions, which read the metadata of a path. + * * @since 2.0.0 */ interface StatOptions { @@ -971,6 +1032,9 @@ interface StatOptions { * console.log(fileInfo.isFile); // true * ``` * + * @param path The path of the file or directory to inspect. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the metadata of the file or directory. * @since 2.0.0 */ async function stat( @@ -997,6 +1061,9 @@ async function stat( * console.log(fileInfo.isFile); // true * ``` * + * @param path The path of the file, directory or symlink to inspect. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to the metadata of the path itself. * @since 2.0.0 */ async function lstat( @@ -1012,6 +1079,8 @@ async function lstat( } /** + * Options for the `truncate` function, which truncates or extends a file. + * * @since 2.0.0 */ interface TruncateOptions { @@ -1037,6 +1106,9 @@ interface TruncateOptions { * console.log(data); // "Hello W" * ``` * + * @param path The path of the file to truncate or extend. + * @param len The length the file is resized to, in bytes. Defaults to `0`. + * @param options Options defining the base directory of `path`. * @since 2.0.0 */ async function truncate( @@ -1056,6 +1128,8 @@ async function truncate( } /** + * Options for the `writeFile` and `writeTextFile` functions, defining how the file is opened before writing to it. + * * @since 2.0.0 */ interface WriteFileOptions { @@ -1082,6 +1156,9 @@ interface WriteFileOptions { * await writeFile('file.txt', data, { baseDir: BaseDirectory.AppLocalData }); * ``` * + * @param path The path of the file to write to. + * @param data The bytes written to the file, either as a buffer or as a stream of chunks. + * @param options Options defining the base directory of `path` and how the file is opened. * @since 2.0.0 */ async function writeFile( @@ -1123,16 +1200,20 @@ async function writeFile( } /** - * Writes UTF-8 string `data` to the given `path`, by default creating a new file if needed, else overwriting. - @example - * ```typescript - * import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; - * - * await writeTextFile('file.txt', "Hello world", { baseDir: BaseDirectory.AppLocalData }); - * ``` - * - * @since 2.0.0 - */ + * Writes UTF-8 string `data` to the given `path`, by default creating a new file if needed, else overwriting. + * + * @example + * ```typescript + * import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * await writeTextFile('file.txt', "Hello world", { baseDir: BaseDirectory.AppLocalData }); + * ``` + * + * @param path The path of the file to write to. + * @param data The UTF-8 string written to the file. + * @param options Options defining the base directory of `path` and how the file is opened. + * @since 2.0.0 + */ async function writeTextFile( path: string | URL, data: string, @@ -1153,6 +1234,8 @@ async function writeTextFile( } /** + * Options for the `exists` function, which checks whether a path exists. + * * @since 2.0.0 */ interface ExistsOptions { @@ -1169,6 +1252,9 @@ interface ExistsOptions { * await exists('avatar.png', { baseDir: BaseDirectory.AppData }); * ``` * + * @param path The path to check. + * @param options Options defining the base directory of `path`. + * @returns A promise resolving to `true` when the path exists, `false` otherwise. * @since 2.0.0 */ async function exists( @@ -1186,6 +1272,8 @@ async function exists( } /** + * Options for the `watchImmediate` function, which reports file system changes as they happen. + * * @since 2.0.0 */ interface WatchOptions { @@ -1196,23 +1284,35 @@ interface WatchOptions { } /** + * Options for the `watch` function, which reports file system changes after a debounce delay. + * * @since 2.0.0 */ interface DebouncedWatchOptions extends WatchOptions { - /** Debounce delay */ + /** + * The debounce delay in milliseconds. Changes that happen within this + * window are grouped and reported together. Defaults to `2000`. + */ delayMs?: number } /** + * A file system change reported to the callback of `watch` or `watchImmediate`. + * * @since 2.0.0 */ interface WatchEvent { + /** The kind of change that was detected. */ type: WatchEventKind + /** The paths affected by the change. */ paths: string[] + /** Additional attributes reported by the underlying file system watcher. */ attrs: unknown } /** + * The kind of file system change described by a `WatchEvent`. + * * @since 2.0.0 */ type WatchEventKind = @@ -1224,6 +1324,8 @@ type WatchEventKind = | 'other' /** + * Describes how a file or directory was accessed. + * * @since 2.0.0 */ type WatchEventKindAccess = @@ -1233,6 +1335,8 @@ type WatchEventKindAccess = | { kind: 'other' } /** + * Describes which kind of entry was created. + * * @since 2.0.0 */ type WatchEventKindCreate = @@ -1242,6 +1346,8 @@ type WatchEventKindCreate = | { kind: 'other' } /** + * Describes what was modified on a file or directory. + * * @since 2.0.0 */ type WatchEventKindModify = @@ -1262,6 +1368,8 @@ type WatchEventKindModify = | { kind: 'other' } /** + * Describes which kind of entry was removed. + * * @since 2.0.0 */ type WatchEventKindRemove = @@ -1272,6 +1380,8 @@ type WatchEventKindRemove = // TODO: Remove this in v3, return `Watcher` instead /** + * Stops watching the paths it was created for. Returned by `watch` and `watchImmediate`. + * * @since 2.0.0 */ type UnwatchFn = () => void @@ -1311,6 +1421,27 @@ async function watchInternal( /** * Watch changes (after a delay) on files or directories. * + * Events that happen within the `delayMs` window are grouped and delivered in a single callback call. + * Requires the `watch` Cargo feature of the Rust plugin to be enabled. + * + * @example + * ```typescript + * import { watch, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * const unwatch = await watch( + * 'app.conf', + * (event) => console.log(event.type, event.paths), + * { baseDir: BaseDirectory.AppConfig, delayMs: 500 } + * ); + * + * // stop watching when you are done + * unwatch(); + * ``` + * + * @param paths The path or list of paths to watch. Each path can be a string or a `file://` URL. + * @param cb The callback executed for each batch of file system changes. + * @param options Options defining the base directory of the paths, the debounce delay and whether directories are watched recursively. + * @returns A promise resolving to a function that stops watching the given paths. * @since 2.0.0 */ async function watch( @@ -1328,6 +1459,27 @@ async function watch( /** * Watch changes on files or directories. * + * Unlike `watch`, changes are reported as soon as they are detected, without a debounce delay. + * Requires the `watch` Cargo feature of the Rust plugin to be enabled. + * + * @example + * ```typescript + * import { watchImmediate, BaseDirectory } from '@tauri-apps/plugin-fs'; + * + * const unwatch = await watchImmediate( + * 'logs', + * (event) => console.log(event.type, event.paths), + * { baseDir: BaseDirectory.AppLog, recursive: true } + * ); + * + * // stop watching when you are done + * unwatch(); + * ``` + * + * @param paths The path or list of paths to watch. Each path can be a string or a `file://` URL. + * @param cb The callback executed for each file system change. + * @param options Options defining the base directory of the paths and whether directories are watched recursively. + * @returns A promise resolving to a function that stops watching the given paths. * @since 2.0.0 */ async function watchImmediate( @@ -1354,6 +1506,8 @@ async function watchImmediate( * console.log(dirSize); // 1024 * ``` * + * @param path The path of the file or directory to measure. + * @returns A promise resolving to the size in bytes. * @since 2.1.0 */ async function size(path: string | URL): Promise { @@ -1390,6 +1544,7 @@ async function size(path: string | URL): Promise { * // ... use the resource ... * ``` * + * @param path The path or `file://` URL of the resource to start accessing. * @since 2.5.0 */ async function startAccessingSecurityScopedResource( @@ -1425,6 +1580,7 @@ async function startAccessingSecurityScopedResource( * await stopAccessingSecurityScopedResource(filePath); * ``` * + * @param path The path or `file://` URL of the resource to stop accessing. * @since 2.5.0 */ async function stopAccessingSecurityScopedResource( diff --git a/plugins/fs/src/android.rs b/plugins/fs/src/android.rs index 54cd22ef5..d00b4fc52 100644 --- a/plugins/fs/src/android.rs +++ b/plugins/fs/src/android.rs @@ -9,6 +9,12 @@ use crate::{models::*, FilePath, OpenOptions}; const PLUGIN_IDENTIFIER: &str = "com.plugin.fs"; +/// Access to the file system APIs on Android. +/// +/// In addition to regular file system paths, it can read `content://` URIs +/// and Android asset paths by resolving them with the Android plugin implementation. +/// +/// Retrieved with [`crate::FsExt::fs`]. pub struct Fs(tauri::plugin::PluginHandle); pub fn init( diff --git a/plugins/fs/src/desktop.rs b/plugins/fs/src/desktop.rs index 1dc77fd70..68baa2cdc 100644 --- a/plugins/fs/src/desktop.rs +++ b/plugins/fs/src/desktop.rs @@ -8,6 +8,9 @@ use tauri::{AppHandle, Runtime}; use crate::{FilePath, OpenOptions}; +/// Access to the file system APIs on desktop. +/// +/// Retrieved with [`crate::FsExt::fs`]. pub struct Fs(pub(crate) AppHandle); fn path_or_err>(p: P) -> std::io::Result { diff --git a/plugins/fs/src/error.rs b/plugins/fs/src/error.rs index 0c98e83fc..cb9685fcb 100644 --- a/plugins/fs/src/error.rs +++ b/plugins/fs/src/error.rs @@ -6,15 +6,20 @@ use std::path::PathBuf; use serde::{Serialize, Serializer}; +/// Errors that can happen while using the file system plugin. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { + /// JSON serialization or deserialization error. #[error(transparent)] Json(#[from] serde_json::Error), + /// Error from the Tauri APIs, usually raised while resolving a path or a scope entry. #[error(transparent)] Tauri(#[from] tauri::Error), + /// Error from the underlying file system operation. #[error(transparent)] Io(#[from] std::io::Error), + /// The path is denied by the plugin scope or is not allowed by it. #[error("forbidden path: {0}")] PathForbidden(PathBuf), /// Invalid glob pattern. @@ -24,11 +29,14 @@ pub enum Error { #[cfg(feature = "watch")] #[error(transparent)] Watch(#[from] notify::Error), + /// Error invoking the Android plugin implementation. #[cfg(target_os = "android")] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + /// The URL cannot be converted to a file system path. #[error("URL is not a valid path")] InvalidPathUrl, + /// The path is not safe to use, for instance because it traverses parent directories. #[error("Unsafe PathBuf: {0}")] UnsafePathBuf(&'static str), } diff --git a/plugins/fs/src/ios.rs b/plugins/fs/src/ios.rs index c4a433542..fa8936181 100644 --- a/plugins/fs/src/ios.rs +++ b/plugins/fs/src/ios.rs @@ -7,6 +7,11 @@ use tauri::{plugin::PluginApi, AppHandle, Runtime}; use crate::{FilePath, OpenOptions}; +/// Access to the file system APIs on iOS. +/// +/// Opening a `file://` URL automatically starts accessing the matching security-scoped resource. +/// +/// Retrieved with [`crate::FsExt::fs`]. pub struct Fs { _phantom: std::marker::PhantomData R>, } diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index 61579bef5..9b37604ba 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -58,6 +58,13 @@ pub use file_path::SafeFilePath; type Result = std::result::Result; +/// Options and flags which can be used to configure how a file is opened. +/// +/// This builder exposes the ability to configure how a [`std::fs::File`] is opened and +/// what operations are permitted on the open file. Build it with [`OpenOptions::new`], +/// chain calls to the setter methods and pass it to [`Fs::open`]. +/// +/// The `read` option defaults to `true`, every other option defaults to `false`. #[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OpenOptions { @@ -199,12 +206,14 @@ impl OpenOptions { /// This function doesn't create the file if it doesn't exist. Use the /// [`OpenOptions::create`] method to do so. /// - /// [`write()`]: Write::write "io::Write::write" - /// [`flush()`]: Write::flush "io::Write::flush" - /// [stream_position]: Seek::stream_position "io::Seek::stream_position" - /// [seek]: Seek::seek "io::Seek::seek" - /// [Current]: SeekFrom::Current "io::SeekFrom::Current" - /// [End]: SeekFrom::End "io::SeekFrom::End" + /// [`write()`]: std::io::Write::write "io::Write::write" + /// [`flush()`]: std::io::Write::flush "io::Write::flush" + /// [Seek]: std::io::Seek "io::Seek" + /// [stream_position]: std::io::Seek::stream_position "io::Seek::stream_position" + /// [seek]: std::io::Seek::seek "io::Seek::seek" + /// [SeekFrom]: std::io::SeekFrom "io::SeekFrom" + /// [Current]: std::io::SeekFrom::Current "io::SeekFrom::Current" + /// [End]: std::io::SeekFrom::End "io::SeekFrom::End" /// /// # Examples /// @@ -260,7 +269,7 @@ impl OpenOptions { /// No file is allowed to exist at the target location, also no (dangling) symlink. In this /// way, if the call succeeds, the file returned is guaranteed to be new. /// If a file exists at the target location, creating a new file will fail with [`AlreadyExists`] - /// or another error based on the situation. See [`OpenOptions::open`] for a + /// or another error based on the situation. See [`std::fs::OpenOptions::open`] for a /// non-exhaustive list of likely errors. /// /// This option is useful because it is atomic. Otherwise between checking @@ -275,7 +284,7 @@ impl OpenOptions { /// /// [`.create()`]: OpenOptions::create /// [`.truncate()`]: OpenOptions::truncate - /// [`AlreadyExists`]: io::ErrorKind::AlreadyExists + /// [`AlreadyExists`]: std::io::ErrorKind::AlreadyExists /// /// # Examples /// @@ -328,6 +337,14 @@ impl OpenOptions { } impl Fs { + /// Reads the entire contents of a file into a string. + /// + /// The file is opened in read-only mode with [`Fs::open`]. + /// + /// # Errors + /// + /// Returns an error if `path` cannot be opened for reading or if its + /// contents are not valid UTF-8. pub fn read_to_string>(&self, path: P) -> std::io::Result { let mut s = String::new(); self.open( @@ -341,6 +358,13 @@ impl Fs { Ok(s) } + /// Reads the entire contents of a file into a bytes vector. + /// + /// The file is opened in read-only mode with [`Fs::open`]. + /// + /// # Errors + /// + /// Returns an error if `path` cannot be opened for reading. pub fn read>(&self, path: P) -> std::io::Result> { let mut buf = Vec::new(); self.open( @@ -432,8 +456,37 @@ impl SecurityScopedResources { pub(crate) fn remove(&self, _url: &str) {} } +/// Extension trait implemented by every [`Manager`] (the app handle, windows, webviews, ...) +/// to access the file system plugin APIs. +/// +/// # Examples +/// +/// ```rust,no_run +/// use std::path::Path; +/// use tauri::Runtime; +/// use tauri_plugin_fs::FsExt; +/// +/// fn setup(app: &tauri::App) -> Result<(), Box> { +/// // allow the app to access a directory that is not part of the static scope +/// app.fs_scope().allow_directory(Path::new("/path/to/directory"), true)?; +/// +/// let contents = app.fs().read_to_string(Path::new("/path/to/directory/file.txt"))?; +/// println!("{contents}"); +/// +/// Ok(()) +/// } +/// ``` pub trait FsExt { + /// Returns the file system scope, which can be used to dynamically + /// allow or deny paths at runtime. + /// + /// # Panics + /// + /// Panics if the plugin is not registered in the app. + /// Use [`FsExt::try_fs_scope`] if the plugin might not be registered. fn fs_scope(&self) -> tauri::fs::Scope; + + /// Returns the file system scope, or `None` if the plugin is not registered in the app. fn try_fs_scope(&self) -> Option; /// Cross platform file system APIs that also support manipulating Android files. @@ -454,6 +507,7 @@ impl> FsExt for T { } } +/// Initializes the plugin. pub fn init() -> TauriPlugin> { PluginBuilder::>::new("fs") .invoke_handler(tauri::generate_handler![ diff --git a/plugins/geolocation/guest-js/index.ts b/plugins/geolocation/guest-js/index.ts index 8ef5c5330..2c05bdae2 100644 --- a/plugins/geolocation/guest-js/index.ts +++ b/plugins/geolocation/guest-js/index.ts @@ -2,6 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Get and track the device's current position, mirroring the W3C Geolocation API. + * + * @module + */ + import { Channel, invoke, @@ -9,6 +15,9 @@ import { checkPermissions as checkPluginPermissions } from '@tauri-apps/api/core' +/** + * The GPS coordinates of a {@link Position}, along with the accuracy of each reading. + */ export type Coordinates = { /** * Latitude in decimal degrees. @@ -31,6 +40,9 @@ export type Coordinates = { * The altitude the user is at, if available. */ altitude: number | null + /** + * The speed the user is traveling, in meters per second, if available. + */ speed: number | null /** * The heading the user is facing, if available. @@ -38,6 +50,9 @@ export type Coordinates = { heading: number | null } +/** + * The current permission state for the geolocation APIs. + */ export type PermissionStatus = { /** * Permission state for the location alias. @@ -59,19 +74,32 @@ export type PermissionStatus = { coarseLocation: PermissionState } +/** + * The individual permission aliases that can be requested with {@link requestPermissions}. + * + * `location` maps to both the coarse and fine location permissions on Android and to the standard + * location permission on iOS. `coarseLocation` maps to the coarse location permission only on + * Android, and behaves the same as `location` on iOS. + */ export type PermissionType = 'location' | 'coarseLocation' +/** + * A geolocation reading, as returned by {@link getCurrentPosition} and passed to the callback of {@link watchPosition}. + */ export type Position = { /** - * Creation time for these coordinates. + * Creation time for these coordinates, in milliseconds since the Unix epoch. */ timestamp: number /** - * The GPD coordinates along with the accuracy of the data. + * The GPS coordinates along with the accuracy of the data. */ coords: Coordinates } +/** + * Options used to configure a {@link getCurrentPosition} or {@link watchPosition} request. + */ export type PositionOptions = { /** * High accuracy mode (such as GPS, if available) @@ -92,6 +120,29 @@ export type PositionOptions = { maximumAge: number } +/** + * Registers a callback that is invoked with the device's position whenever it changes, similar to the W3C `navigator.geolocation.watchPosition` API. Pass the returned id to {@link clearWatch} to stop watching. + * + * @example + * ```typescript + * import { watchPosition } from '@tauri-apps/plugin-geolocation'; + * const watchId = await watchPosition( + * { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }, + * (position, error) => { + * if (error) { + * console.error(error) + * } else { + * console.log(position) + * } + * } + * ); + * ``` + * + * @param options Configuration for the position watcher. + * @param cb Callback invoked with the new {@link Position} on success, or `null` and an error message when a read fails. + * @returns A promise resolving to the id of the registered watcher. + * @since 2.0.0 + */ export async function watchPosition( options: PositionOptions, cb: (location: Position | null, error?: string) => void @@ -111,6 +162,19 @@ export async function watchPosition( return channel.id } +/** + * Returns the device's current position, similar to the W3C `navigator.geolocation.getCurrentPosition` API. + * + * @example + * ```typescript + * import { getCurrentPosition } from '@tauri-apps/plugin-geolocation'; + * const position = await getCurrentPosition(); + * ``` + * + * @param options Configuration for the position request. + * @returns A promise resolving to the current {@link Position}. + * @since 2.0.0 + */ export async function getCurrentPosition( options?: PositionOptions ): Promise { @@ -119,16 +183,53 @@ export async function getCurrentPosition( }) } +/** + * Stops the position watcher registered with {@link watchPosition}. + * + * @example + * ```typescript + * import { clearWatch } from '@tauri-apps/plugin-geolocation'; + * await clearWatch(watchId); + * ``` + * + * @param channelId The id returned by {@link watchPosition}. + * @since 2.0.0 + */ export async function clearWatch(channelId: number): Promise { await invoke('plugin:geolocation|clear_watch', { channelId }) } +/** + * Returns the current state of the geolocation permissions. Rejects if location services are disabled on the device. + * + * @example + * ```typescript + * import { checkPermissions } from '@tauri-apps/plugin-geolocation'; + * const permission = await checkPermissions(); + * ``` + * + * @returns A promise resolving to the current {@link PermissionStatus}. + * @since 2.0.0 + */ export async function checkPermissions(): Promise { return await checkPluginPermissions('geolocation') } +/** + * Requests the given geolocation permissions, prompting the user if needed. Rejects if location services are disabled on the device. + * + * @example + * ```typescript + * import { requestPermissions } from '@tauri-apps/plugin-geolocation'; + * const permission = await requestPermissions(['location']); + * ``` + * + * @param permissions The permissions to request, or `null` to request all of them. + * @returns A promise resolving to the resulting {@link PermissionStatus}. + * @since 2.0.0 + */ export async function requestPermissions( permissions: PermissionType[] | null ): Promise { diff --git a/plugins/geolocation/src/desktop.rs b/plugins/geolocation/src/desktop.rs index 00da1fadb..3ca41db5f 100644 --- a/plugins/geolocation/src/desktop.rs +++ b/plugins/geolocation/src/desktop.rs @@ -22,6 +22,7 @@ pub fn init( pub struct Geolocation(AppHandle); impl Geolocation { + /// Not implemented on desktop platforms; always resolves to a default, zeroed [`Position`] without reading any real location. pub fn get_current_position( &self, _options: Option, @@ -29,6 +30,7 @@ impl Geolocation { Ok(Position::default()) } + /// Not implemented on desktop platforms. Registers a channel for `channel_id` bookkeeping, but `callback` is never invoked with a real [`WatchEvent`]. pub fn watch_position( &self, options: PositionOptions, @@ -64,14 +66,17 @@ impl Geolocation { Ok(()) } + /// Not implemented on desktop platforms; always succeeds without doing anything. pub fn clear_watch(&self, _channel_id: u32) -> crate::Result<()> { Ok(()) } + /// Not implemented on desktop platforms; always resolves to the default [`PermissionStatus`] (both permissions in the [`Prompt`](tauri::plugin::PermissionState::Prompt) state). pub fn check_permissions(&self) -> crate::Result { Ok(PermissionStatus::default()) } + /// Not implemented on desktop platforms; always resolves to the default [`PermissionStatus`] without prompting the user. pub fn request_permissions( &self, _permissions: Option>, diff --git a/plugins/geolocation/src/error.rs b/plugins/geolocation/src/error.rs index f59037de8..1c4f69517 100644 --- a/plugins/geolocation/src/error.rs +++ b/plugins/geolocation/src/error.rs @@ -4,13 +4,16 @@ use serde::{ser::Serializer, Serialize}; +/// Alias for the result type returned by the geolocation APIs. pub type Result = std::result::Result; // TODO: Improve Error handling (different typed errors instead of one (stringified) PluginInvokeError for all mobile errors) +/// Errors that can occur while interacting with the geolocation APIs. #[derive(Debug, thiserror::Error)] #[cfg_attr(feature = "specta", derive(specta::Type))] pub enum Error { + /// Forwards an error returned by the underlying Android or iOS plugin invocation. #[cfg(mobile)] #[error(transparent)] PluginInvoke( diff --git a/plugins/geolocation/src/lib.rs b/plugins/geolocation/src/lib.rs index a1e5d2cea..2c87c13f1 100644 --- a/plugins/geolocation/src/lib.rs +++ b/plugins/geolocation/src/lib.rs @@ -33,6 +33,7 @@ pub use mobile::Geolocation; /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the geolocation APIs. pub trait GeolocationExt { + /// Returns the handle to the geolocation APIs. fn geolocation(&self) -> &Geolocation; } diff --git a/plugins/geolocation/src/mobile.rs b/plugins/geolocation/src/mobile.rs index 48a3f5ded..4de9676a9 100644 --- a/plugins/geolocation/src/mobile.rs +++ b/plugins/geolocation/src/mobile.rs @@ -33,6 +33,7 @@ pub fn init( pub struct Geolocation(PluginHandle); impl Geolocation { + /// Returns the device's current [`Position`]. On Android this returns the last known location immediately if it is still within `maximum_age`, otherwise it requests a fresh reading. Errors if location services are disabled or the required permission was not granted. pub fn get_current_position( &self, options: Option, @@ -81,18 +82,21 @@ impl Geolocation { .map_err(Into::into) } + /// Removes the position watcher registered with the given `channel_id`, as returned by [`Self::watch_position`]. Stops the platform location updates once no watcher remains. pub fn clear_watch(&self, channel_id: u32) -> crate::Result<()> { self.0 .run_mobile_plugin("clearWatch", ClearWatchPayload { channel_id }) .map_err(Into::into) } + /// Returns the current [`PermissionStatus`] for the geolocation APIs. Errors if location services are disabled on the device. pub fn check_permissions(&self) -> crate::Result { self.0 .run_mobile_plugin("checkPermissions", ()) .map_err(Into::into) } + /// Requests the given `permissions` (or all of them when `None`) and returns the resulting [`PermissionStatus`]. Errors if location services are disabled on the device. pub fn request_permissions( &self, permissions: Option>, diff --git a/plugins/geolocation/src/models.rs b/plugins/geolocation/src/models.rs index c08bb27a2..9dd7d3ab3 100644 --- a/plugins/geolocation/src/models.rs +++ b/plugins/geolocation/src/models.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use tauri::plugin::PermissionState; +/// The current permission state for the geolocation APIs. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] @@ -25,6 +26,7 @@ pub struct PermissionStatus { pub coarse_location: PermissionState, } +/// Options used to configure a [`get_current_position`](crate::Geolocation::get_current_position) or [`watch_position`](crate::Geolocation::watch_position) request. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] @@ -47,14 +49,18 @@ pub struct PositionOptions { pub maximum_age: u32, } +/// The individual permission aliases that can be requested with [`request_permissions`](crate::Geolocation::request_permissions). #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub enum PermissionType { + /// The `location` alias. On Android this maps to both `ACCESS_COARSE_LOCATION` and `ACCESS_FINE_LOCATION`. On iOS it maps to the standard location permission. Location, + /// The `coarseLocation` alias. On Android this maps to `ACCESS_COARSE_LOCATION` only. On iOS it behaves the same as [`Location`](Self::Location). CoarseLocation, } +/// The GPS coordinates of a [`Position`], along with the accuracy of each reading. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] @@ -70,12 +76,13 @@ pub struct Coordinates { pub altitude_accuracy: Option, /// The altitude the user is at, if available. pub altitude: Option, - // The speed the user is traveling, if available. + /// The speed the user is traveling, in meters per second, if available. pub speed: Option, /// The heading the user is facing, if available. pub heading: Option, } +/// A geolocation reading, as returned by [`get_current_position`](crate::Geolocation::get_current_position) and reported through [`WatchEvent::Position`]. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] @@ -87,10 +94,13 @@ pub struct Position { pub coords: Coordinates, } +/// A single update sent through the channel callback registered with [`watch_position`](crate::Geolocation::watch_position). #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(untagged)] pub enum WatchEvent { + /// A new position was read successfully. Position(Position), + /// The platform failed to read a position; the string is the platform-provided error message. Error(String), } diff --git a/plugins/global-shortcut/guest-js/index.ts b/plugins/global-shortcut/guest-js/index.ts index 13e8e50e8..8f30a6919 100644 --- a/plugins/global-shortcut/guest-js/index.ts +++ b/plugins/global-shortcut/guest-js/index.ts @@ -10,12 +10,19 @@ import { invoke, Channel } from '@tauri-apps/api/core' +/** + * Payload sent to a shortcut handler when a registered shortcut is pressed or released. + */ export interface ShortcutEvent { + /** The shortcut definition that triggered this event, e.g. `CommandOrControl+Shift+C`. */ shortcut: string + /** Numeric identifier derived from the shortcut's modifiers and key. */ id: number + /** Whether the shortcut's key combination was pressed down or released. */ state: 'Released' | 'Pressed' } +/** Callback invoked with a {@link ShortcutEvent} whenever a registered shortcut changes state. */ export type ShortcutHandler = (event: ShortcutEvent) => void /** @@ -43,7 +50,7 @@ export type ShortcutHandler = (event: ShortcutEvent) => void * }); * ``` * - * @param shortcut Shortcut definition, modifiers and key separated by "+" e.g. CmdOrControl+Q + * @param shortcuts A shortcut definition, or a list of shortcut definitions, with modifiers and key separated by "+" e.g. CmdOrControl+Q * @param handler Shortcut handler callback - takes the triggered shortcut as argument * * @since 2.0.0 @@ -75,7 +82,7 @@ async function register( * await unregister(['CmdOrControl+Space', 'Alt+A']); * ``` * - * @param shortcut shortcut definition (modifiers and key separated by "+" e.g. CmdOrControl+Q), also accepts a list of shortcuts + * @param shortcuts A shortcut definition, or a list of shortcut definitions, with modifiers and key separated by "+" e.g. CmdOrControl+Q * * @since 2.0.0 */ @@ -111,6 +118,7 @@ async function unregisterAll(): Promise { * ``` * * @param shortcut shortcut definition, modifiers and key separated by "+" e.g. CmdOrControl+Q + * @returns A promise resolving to whether the shortcut is currently registered by this application. * * @since 2.0.0 */ diff --git a/plugins/global-shortcut/src/error.rs b/plugins/global-shortcut/src/error.rs index 37392b321..9b8e5cbab 100644 --- a/plugins/global-shortcut/src/error.rs +++ b/plugins/global-shortcut/src/error.rs @@ -4,13 +4,20 @@ use serde::{Serialize, Serializer}; +/// Errors that can happen while registering, unregistering or parsing global shortcuts. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { + /// An error returned by the underlying `global_hotkey` crate, for example when the OS + /// refuses to register or unregister a shortcut, or when a shortcut string fails to parse. #[error("{0}")] GlobalHotkey(String), + /// Failed to receive the result of an operation dispatched to the main thread because the + /// sending end of the channel was dropped before it could reply. #[error(transparent)] RecvError(#[from] std::sync::mpsc::RecvError), + /// An error returned by the Tauri runtime, for example when dispatching a closure to run on + /// the main thread fails. #[error(transparent)] Tauri(#[from] tauri::Error), } diff --git a/plugins/global-shortcut/src/lib.rs b/plugins/global-shortcut/src/lib.rs index 450bb5985..19a4fa121 100644 --- a/plugins/global-shortcut/src/lib.rs +++ b/plugins/global-shortcut/src/lib.rs @@ -5,6 +5,8 @@ //! Register global shortcuts. //! //! - Supported platforms: Windows, Linux and macOS. +//! - On macOS, registering and unregistering shortcuts must happen on the main thread, so every +//! [`GlobalShortcut`] operation dispatches to it and blocks until it completes. #![doc( html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", @@ -38,6 +40,9 @@ type Result = std::result::Result; type HotKeyId = u32; type HandlerFn = Box, &Shortcut, ShortcutEvent) + Send + Sync + 'static>; +/// Internal wrapper around a [`Shortcut`] that lets the shortcut-accepting APIs on +/// [`GlobalShortcut`] and [`Builder`] be generic over an already-parsed [`Shortcut`] or a +/// string accelerator such as `"CmdOrControl+Q"`. pub struct ShortcutWrapper(Shortcut); impl From for ShortcutWrapper { @@ -65,6 +70,11 @@ unsafe impl Send for GlobalHotKeyManager {} /// SAFETY: we ensure it is run on main thread only unsafe impl Sync for GlobalHotKeyManager {} +/// The global shortcut APIs, accessible through [`GlobalShortcutExt::global_shortcut`]. +/// +/// Registering, unregistering and checking a shortcut all dispatch the underlying +/// `global_hotkey` call to the app's main thread and block until it finishes, since macOS +/// requires the hotkey manager to run on the main thread. pub struct GlobalShortcut { #[allow(dead_code)] app: AppHandle, @@ -127,7 +137,8 @@ impl GlobalShortcut { } impl GlobalShortcut { - /// Register a shortcut. + /// Register a shortcut. Returns an error if the shortcut is invalid or already registered + /// by this [`GlobalShortcut`] instance. pub fn register(&self, shortcut: S) -> Result<()> where S: TryInto, @@ -139,7 +150,10 @@ impl GlobalShortcut { ) } - /// Register a shortcut with a handler. + /// Register a shortcut with a handler that is called with the app handle, the triggered + /// [`Shortcut`] and the [`ShortcutEvent`] (whose `state` is [`ShortcutState::Pressed`] or + /// [`ShortcutState::Released`]) whenever the shortcut's key combination is pressed or + /// released. pub fn on_shortcut(&self, shortcut: S, handler: F) -> Result<()> where S: TryInto, @@ -241,7 +255,28 @@ impl GlobalShortcut { } } +/// Extension trait for [`Manager`] implementors (e.g. [`AppHandle`]) that exposes access to the +/// global shortcut APIs. +/// +/// # Examples +/// +/// ```rust,no_run +/// use tauri::Runtime; +/// use tauri_plugin_global_shortcut::{GlobalShortcutExt, ShortcutState}; +/// +/// fn setup(app: &tauri::App) -> Result<(), Box> { +/// app.global_shortcut() +/// .on_shortcut("CommandOrControl+Shift+C", |_app, shortcut, event| { +/// if event.state == ShortcutState::Pressed { +/// println!("{shortcut:?} pressed"); +/// } +/// })?; +/// +/// Ok(()) +/// } +/// ``` pub trait GlobalShortcutExt { + /// Returns the [`GlobalShortcut`] instance managed by this plugin. fn global_shortcut(&self) -> &GlobalShortcut; } @@ -333,6 +368,26 @@ fn is_registered( Ok(global_shortcut.is_registered(parse_shortcut(shortcut)?)) } +/// Builder for the global shortcut plugin, letting you configure shortcuts and a handler that +/// are registered as soon as the plugin is set up, before [`Builder::build`] is called. +/// +/// # Examples +/// +/// ```rust,no_run +/// use tauri::Runtime; +/// +/// fn setup(builder: tauri::Builder) -> tauri::Builder { +/// builder.plugin( +/// tauri_plugin_global_shortcut::Builder::new() +/// .with_shortcut("CommandOrControl+Shift+C") +/// .unwrap() +/// .with_handler(|_app, shortcut, event| { +/// println!("{shortcut:?}: {event:?}"); +/// }) +/// .build(), +/// ) +/// } +/// ``` pub struct Builder { shortcuts: Vec, handler: Option>, @@ -348,6 +403,7 @@ impl Default for Builder { } impl Builder { + /// Creates a new [`Builder`] with no shortcuts or handler configured. pub fn new() -> Self { Self::default() } @@ -385,6 +441,11 @@ impl Builder { self } + /// Builds the plugin. Shortcuts configured with [`Builder::with_shortcut`] / + /// [`Builder::with_shortcuts`] are registered when the plugin is set up, and the handler + /// configured with [`Builder::with_handler`] (if any) is called for every hotkey event, in + /// addition to any handler passed to [`GlobalShortcut::on_shortcut`] / + /// [`GlobalShortcut::on_shortcuts`] for that specific shortcut. pub fn build(self) -> TauriPlugin { let handler = self.handler; let shortcuts = self.shortcuts; diff --git a/plugins/haptics/api-iife.js b/plugins/haptics/api-iife.js index 857ed2764..0f50b70ca 100644 --- a/plugins/haptics/api-iife.js +++ b/plugins/haptics/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_HAPTICS__=function(t,r){"use strict";const e={async vibrate(t){try{return{status:"ok",data:await r.invoke("plugin:haptics|vibrate",{duration:t})}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}},async impactFeedback(t){try{return{status:"ok",data:await r.invoke("plugin:haptics|impact_feedback",{style:t})}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}},async notificationFeedback(t){try{return{status:"ok",data:await r.invoke("plugin:haptics|notification_feedback",{type:t})}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}},async selectionFeedback(){try{return{status:"ok",data:await r.invoke("plugin:haptics|selection_feedback")}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}}},{vibrate:a,impactFeedback:i,notificationFeedback:c,selectionFeedback:n}=e;return t.impactFeedback=i,t.notificationFeedback=c,t.selectionFeedback=n,t.vibrate=a,t}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"haptics",{value:__TAURI_PLUGIN_HAPTICS__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_HAPTICS__=function(t,r){"use strict";const e={async vibrate(t){try{return{status:"ok",data:await r.invoke("plugin:haptics|vibrate",{duration:t})}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}},async impactFeedback(t){try{return{status:"ok",data:await r.invoke("plugin:haptics|impact_feedback",{style:t})}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}},async notificationFeedback(t){try{return{status:"ok",data:await r.invoke("plugin:haptics|notification_feedback",{type:t})}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}},async selectionFeedback(){try{return{status:"ok",data:await r.invoke("plugin:haptics|selection_feedback")}}catch(t){if(t instanceof Error)throw t;return{status:"error",error:t}}}};return t.impactFeedback=async function(t){return e.impactFeedback(t)},t.notificationFeedback=async function(t){return e.notificationFeedback(t)},t.selectionFeedback=async function(){return e.selectionFeedback()},t.vibrate=async function(t){return e.vibrate(t)},t}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"haptics",{value:__TAURI_PLUGIN_HAPTICS__})} diff --git a/plugins/haptics/guest-js/bindings.ts b/plugins/haptics/guest-js/bindings.ts index d12920d85..040e27861 100644 --- a/plugins/haptics/guest-js/bindings.ts +++ b/plugins/haptics/guest-js/bindings.ts @@ -73,12 +73,26 @@ export const commands = { /** user-defined types **/ export type Error = never +/** + * The style of an impact-feedback haptic. + * + * On iOS this maps directly to a `UIImpactFeedbackGenerator.FeedbackStyle` case. On Android, + * which has no equivalent system API, each style instead plays a distinct vibration waveform of + * increasing intensity. Has no effect on desktop platforms. Defaults to `Medium`. + */ export type ImpactFeedbackStyle = | 'light' | 'medium' | 'heavy' | 'soft' | 'rigid' +/** + * The type of notification feedback, indicating the outcome of a task or action. + * + * On iOS this maps directly to a `UINotificationFeedbackGenerator.FeedbackType` case. On + * Android, which has no equivalent system API, each type instead plays a distinct vibration + * waveform. Has no effect on desktop platforms. Defaults to `Success`. + */ export type NotificationFeedbackType = 'success' | 'warning' | 'error' //export type RandomNumber = number; diff --git a/plugins/haptics/guest-js/index.ts b/plugins/haptics/guest-js/index.ts index 23485bdfb..9c477aaba 100644 --- a/plugins/haptics/guest-js/index.ts +++ b/plugins/haptics/guest-js/index.ts @@ -2,16 +2,102 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -/* eslint-disable @typescript-eslint/unbound-method */ +/** + * Trigger haptic feedback on Android and iOS. + * + * @module + */ import { commands } from './bindings' +import type { + ImpactFeedbackStyle, + NotificationFeedbackType, + Result, + Error +} from './bindings' -export const { - vibrate, - impactFeedback, - notificationFeedback, - selectionFeedback -} = commands +/** + * Triggers a vibration for `duration` milliseconds. + * + * Has no effect on desktop platforms. + * + * @example + * ```typescript + * import { vibrate } from '@tauri-apps/plugin-haptics' + * await vibrate(300) + * ``` + * + * @param duration Duration of the vibration, in milliseconds. + * @returns A promise resolving to the {@link Result} of the operation. + * @since 2.0.0 + */ +export async function vibrate(duration: number): Promise> { + return commands.vibrate(duration) +} + +/** + * Triggers an impact-feedback haptic, indicating a collision between user interface elements. + * + * On iOS this maps to a `UIImpactFeedbackGenerator` of the given style. On Android, which has + * no equivalent system API, each style plays a distinct vibration waveform. Has no effect on + * desktop platforms. + * + * @example + * ```typescript + * import { impactFeedback } from '@tauri-apps/plugin-haptics' + * await impactFeedback('medium') + * ``` + * + * @param style The style of the impact. + * @returns A promise resolving to the {@link Result} of the operation. + * @since 2.0.0 + */ +export async function impactFeedback( + style: ImpactFeedbackStyle +): Promise> { + return commands.impactFeedback(style) +} + +/** + * Triggers a notification-feedback haptic, indicating the outcome of a task or action. + * + * On iOS this maps to a `UINotificationFeedbackGenerator` of the given type. On Android, which + * has no equivalent system API, each type plays a distinct vibration waveform. Has no effect on + * desktop platforms. + * + * @example + * ```typescript + * import { notificationFeedback } from '@tauri-apps/plugin-haptics' + * await notificationFeedback('success') + * ``` + * + * @param type The outcome to convey. + * @returns A promise resolving to the {@link Result} of the operation. + * @since 2.0.0 + */ +export async function notificationFeedback( + type: NotificationFeedbackType +): Promise> { + return commands.notificationFeedback(type) +} + +/** + * Triggers a haptic indicating that a selection changed, e.g. the value of a picker control. + * + * Has no effect on desktop platforms. + * + * @example + * ```typescript + * import { selectionFeedback } from '@tauri-apps/plugin-haptics' + * await selectionFeedback() + * ``` + * + * @returns A promise resolving to the {@link Result} of the operation. + * @since 2.0.0 + */ +export async function selectionFeedback(): Promise> { + return commands.selectionFeedback() +} export { ImpactFeedbackStyle, NotificationFeedbackType } from './bindings' diff --git a/plugins/haptics/src/desktop.rs b/plugins/haptics/src/desktop.rs index b04b75676..d036eddad 100644 --- a/plugins/haptics/src/desktop.rs +++ b/plugins/haptics/src/desktop.rs @@ -18,18 +18,26 @@ pub fn init( pub struct Haptics(AppHandle); impl Haptics { + /// No-op on desktop; haptic feedback is not supported on Linux, macOS or Windows. Always + /// returns `Ok`. pub fn vibrate(&self, _duration: u32) -> crate::Result<()> { Ok(()) } + /// No-op on desktop; haptic feedback is not supported on Linux, macOS or Windows. Always + /// returns `Ok`. pub fn impact_feedback(&self, _style: ImpactFeedbackStyle) -> crate::Result<()> { Ok(()) } + /// No-op on desktop; haptic feedback is not supported on Linux, macOS or Windows. Always + /// returns `Ok`. pub fn notification_feedback(&self, _type: NotificationFeedbackType) -> crate::Result<()> { Ok(()) } + /// No-op on desktop; haptic feedback is not supported on Linux, macOS or Windows. Always + /// returns `Ok`. pub fn selection_feedback(&self) -> crate::Result<()> { Ok(()) } diff --git a/plugins/haptics/src/error.rs b/plugins/haptics/src/error.rs index f59037de8..3a24755e8 100644 --- a/plugins/haptics/src/error.rs +++ b/plugins/haptics/src/error.rs @@ -4,13 +4,18 @@ use serde::{ser::Serializer, Serialize}; +/// Alias for a [`std::result::Result`] with the error type [`Error`]. pub type Result = std::result::Result; // TODO: Improve Error handling (different typed errors instead of one (stringified) PluginInvokeError for all mobile errors) +/// Errors returned by the haptics APIs. #[derive(Debug, thiserror::Error)] #[cfg_attr(feature = "specta", derive(specta::Type))] pub enum Error { + /// The call to the Android or iOS plugin implementation failed, either because the + /// arguments could not be serialized, the response could not be deserialized, or the + /// native side rejected the call. Only produced on Android and iOS. #[cfg(mobile)] #[error(transparent)] PluginInvoke( diff --git a/plugins/haptics/src/lib.rs b/plugins/haptics/src/lib.rs index f297a7488..7984bebb0 100644 --- a/plugins/haptics/src/lib.rs +++ b/plugins/haptics/src/lib.rs @@ -33,6 +33,7 @@ pub use mobile::Haptics; /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the haptics APIs. pub trait HapticsExt { + /// Returns a handle to the [`Haptics`] APIs. fn haptics(&self) -> &Haptics; } diff --git a/plugins/haptics/src/mobile.rs b/plugins/haptics/src/mobile.rs index 2b1e2036c..42ae2459a 100644 --- a/plugins/haptics/src/mobile.rs +++ b/plugins/haptics/src/mobile.rs @@ -32,18 +32,40 @@ pub fn init( pub struct Haptics(PluginHandle); impl Haptics { + /// Triggers a vibration for `duration` milliseconds. + /// + /// On iOS this plays a continuous [Core Haptics](https://developer.apple.com/documentation/corehaptics) + /// pattern when the device supports it, falling back to the system alert vibration + /// otherwise. On Android it uses [`Vibrator`](https://developer.android.com/reference/android/os/Vibrator). + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) if the underlying Android or + /// iOS plugin invocation fails. pub fn vibrate(&self, duration: u32) -> crate::Result<()> { self.0 .run_mobile_plugin("vibrate", VibratePayload { duration }) .map_err(Into::into) } + /// Triggers an impact-feedback haptic with the given [`ImpactFeedbackStyle`]. + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) if the underlying Android or + /// iOS plugin invocation fails. pub fn impact_feedback(&self, style: ImpactFeedbackStyle) -> crate::Result<()> { self.0 .run_mobile_plugin("impactFeedback", ImpactFeedbackPayload { style }) .map_err(Into::into) } + /// Triggers a notification-feedback haptic for the given [`NotificationFeedbackType`]. + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) if the underlying Android or + /// iOS plugin invocation fails. pub fn notification_feedback(&self, r#type: NotificationFeedbackType) -> crate::Result<()> { self.0 .run_mobile_plugin( @@ -53,6 +75,13 @@ impl Haptics { .map_err(Into::into) } + /// Triggers a haptic indicating that a selection changed, e.g. when the value of a picker + /// control changes. + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) if the underlying Android or + /// iOS plugin invocation fails. pub fn selection_feedback(&self) -> crate::Result<()> { self.0 .run_mobile_plugin("selectionFeedback", ()) diff --git a/plugins/haptics/src/models.rs b/plugins/haptics/src/models.rs index 50a1fb163..d59cc2029 100644 --- a/plugins/haptics/src/models.rs +++ b/plugins/haptics/src/models.rs @@ -13,24 +13,42 @@ pub struct HapticsOptions { } */ +/// The style of an impact-feedback haptic. +/// +/// On iOS this maps directly to a `UIImpactFeedbackGenerator.FeedbackStyle` case. On Android, +/// which has no equivalent system API, each style instead plays a distinct vibration waveform of +/// increasing intensity. Has no effect on desktop platforms. Defaults to `Medium`. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub enum ImpactFeedbackStyle { + /// A collision between small, light user interface elements. Light, + /// A collision between moderately sized user interface elements. #[default] Medium, + /// A collision between large, heavy user interface elements. Heavy, + /// A soft, muted impact between user interface elements. Soft, + /// A sharp, rigid impact between user interface elements. Rigid, } +/// The type of notification feedback, indicating the outcome of a task or action. +/// +/// On iOS this maps directly to a `UINotificationFeedbackGenerator.FeedbackType` case. On +/// Android, which has no equivalent system API, each type instead plays a distinct vibration +/// waveform. Has no effect on desktop platforms. Defaults to `Success`. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "specta", derive(specta::Type))] #[serde(rename_all = "camelCase")] pub enum NotificationFeedbackType { + /// A task or action has completed successfully. #[default] Success, + /// A task or action has produced a warning. Warning, + /// A task or action has failed. Error, } diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index 5406e03f2..5d4e0945f 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -48,6 +48,11 @@ export interface Proxy { https?: string | ProxyConfig } +/** + * Detailed configuration of a single proxy server, used when a plain URL string is not enough. + * + * @since 2.0.0 + */ export interface ProxyConfig { /** * The URL of the proxy server. @@ -57,7 +62,13 @@ export interface ProxyConfig { * Set the `Proxy-Authorization` header using Basic auth. */ basicAuth?: { + /** + * The user name sent to the proxy server. + */ username: string + /** + * The password sent to the proxy server. + */ password: string } /** @@ -116,14 +127,24 @@ const ERROR_REQUEST_CANCELLED = 'Request cancelled' * Fetch a resource from the network. It returns a `Promise` that resolves to the * `Response` to that `Request`, whether it is successful or not. * + * The request is performed by the Rust backend instead of the webview, so it is not subject to + * CORS, but the URL must be allowed by the plugin scope. + * * @example * ```typescript + * import { fetch } from '@tauri-apps/plugin-http'; * const response = await fetch("http://my.json.host/data.json"); * console.log(response.status); // e.g. 200 * console.log(response.statusText); // e.g. "OK" * const jsonData = await response.json(); * ``` * + * @param input The resource to fetch, as a URL, a string or a `Request` object. + * @param init The standard `fetch` request options, extended with the Rust client options from + * {@linkcode ClientOptions}: `maxRedirections`, `connectTimeout`, `proxy` and `danger`. The + * `signal` option can be used to abort the request. + * @returns A promise resolving to the `Response` of the request. + * * @since 2.0.0 */ export async function fetch( diff --git a/plugins/http/src/error.rs b/plugins/http/src/error.rs index ef8de0c52..2b974e692 100644 --- a/plugins/http/src/error.rs +++ b/plugins/http/src/error.rs @@ -5,42 +5,68 @@ use serde::{Serialize, Serializer}; use url::Url; +/// Errors that can happen while using the HTTP plugin. +/// +/// The error is serialized to the frontend as its [`Display`](std::fmt::Display) string. #[derive(Debug, thiserror::Error)] pub enum Error { + /// JSON serialization or deserialization error. #[error(transparent)] Json(#[from] serde_json::Error), + /// File system error, raised for instance when the cookie store file cannot be created or + /// opened on startup. #[error(transparent)] Io(#[from] std::io::Error), + /// Error from the underlying [`reqwest`] client, raised while building the client, sending the + /// request or reading the response body. #[error(transparent)] Network(#[from] reqwest::Error), + /// Error from the [`http`] crate, raised while building the response of a `data:` URL. #[error(transparent)] Http(#[from] http::Error), + /// A header name given by the frontend is not a valid HTTP header name. #[error(transparent)] HttpInvalidHeaderName(#[from] http::header::InvalidHeaderName), + /// A header value is not a valid HTTP header value. #[error(transparent)] HttpInvalidHeaderValue(#[from] http::header::InvalidHeaderValue), /// URL not allowed by the scope. + /// + /// Raised for the URL requested by the frontend, and - when the + /// [`scope_redirects`](crate::Config::scope_redirects) configuration is enabled - for any + /// redirect target that is not allowed by the scope. #[error("url not allowed on the configured scope: {0}")] UrlNotAllowed(Url), + /// Failed to parse a URL. #[error(transparent)] UrlParseError(#[from] url::ParseError), /// HTTP method error. #[error(transparent)] HttpMethod(#[from] http::method::InvalidMethod), + /// The requested URL uses an unsupported scheme. Only `http`, `https` and `data` are handled. #[error("scheme {0} not supported")] SchemeNotSupport(String), + /// The request was aborted by the frontend before the response was received. #[error("Request canceled")] RequestCanceled, + /// Error from the file system plugin. #[error(transparent)] FsError(#[from] tauri_plugin_fs::Error), + /// The `data:` URL could not be processed. #[error("failed to process data url")] DataUrlError, + /// The body of the `data:` URL could not be decoded into bytes. #[error("failed to decode data url into bytes")] DataUrlDecodeError, + /// Error from the Tauri APIs, raised for instance while resolving a path or while reading the + /// webview resource table. #[error(transparent)] Tauri(#[from] tauri::Error), + /// A response header value is not valid UTF-8, so it cannot be forwarded to the frontend. #[error(transparent)] Utf8(#[from] std::string::FromUtf8Error), + /// The frontend requested dangerous client settings, but the `dangerous-settings` Cargo + /// feature is not enabled. #[error("dangerous settings used but are not enabled")] DangerousSettings, } @@ -54,4 +80,5 @@ impl Serialize for Error { } } +/// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. pub type Result = std::result::Result; diff --git a/plugins/http/src/lib.rs b/plugins/http/src/lib.rs index e33275476..6444ef99e 100644 --- a/plugins/http/src/lib.rs +++ b/plugins/http/src/lib.rs @@ -84,6 +84,16 @@ pub(crate) struct Http { cookies_jar: std::sync::Arc, } +/// Initializes the plugin. +/// +/// The plugin reads its [`Config`] from the `plugins > http` object of the `tauri.conf.json` file; +/// when that object is missing, [`Config::default`] is used. +/// +/// With the `cookies` Cargo feature (enabled by default), a cookie jar is loaded from a `.cookies` +/// file in the application cache directory on setup and written back to it when the application +/// exits. A jar that cannot be read is replaced by an empty one. +/// +/// Register it on the Tauri builder with `.plugin(tauri_plugin_http::init())`. pub fn init() -> TauriPlugin> { Builder::>::new("http") .setup(|app, api| { diff --git a/plugins/localhost/src/lib.rs b/plugins/localhost/src/lib.rs index 5d948b015..aba034b94 100644 --- a/plugins/localhost/src/lib.rs +++ b/plugins/localhost/src/lib.rs @@ -20,21 +20,28 @@ use tauri::{ }; use tiny_http::{Header, Response as HttpResponse, Server}; +/// An incoming HTTP request received by the localhost server, passed to the +/// [`Builder::on_request`] hook. pub struct Request { url: String, } impl Request { + /// The request URL (path and, if present, query string), exactly as sent by the client. pub fn url(&self) -> &str { &self.url } } +/// The HTTP response the localhost server is about to send back for a matched asset. Passed +/// mutably to the [`Builder::on_request`] hook so it can add or override headers before the +/// response is written to the client. pub struct Response { headers: HashMap, } impl Response { + /// Adds a header to the response, replacing any existing header with the same name. pub fn add_header, V: Into>(&mut self, header: H, value: V) { self.headers.insert(header.into(), value.into()); } @@ -42,6 +49,11 @@ impl Response { type OnRequest = Option>; +/// Builds the localhost plugin. +/// +/// **Note: This plugin brings considerable security risks and you should only use it if you know +/// what you are doing. Because the server has no authentication, any local process can connect to +/// it and read the assets it serves. If in doubt, use the default custom protocol implementation.** pub struct Builder { port: u16, host: Option, @@ -49,6 +61,23 @@ pub struct Builder { } impl Builder { + /// Creates a new [`Builder`] that will serve the app's assets on the given `port`, bound to + /// `localhost` unless [`Self::host`] is called. + /// + /// # Examples + /// + /// ```no_run + /// fn setup(builder: tauri::Builder) -> tauri::Builder { + /// builder.plugin( + /// tauri_plugin_localhost::Builder::new(9527) + /// .host("127.0.0.1") + /// .on_request(|request, response| { + /// println!("{}", request.url()); + /// }) + /// .build(), + /// ) + /// } + /// ``` pub fn new(port: u16) -> Self { Self { port, @@ -57,12 +86,15 @@ impl Builder { } } - // Change the host the plugin binds to. Defaults to `localhost`. + /// Sets the host the localhost server binds to. Defaults to `localhost`. pub fn host>(mut self, host: H) -> Self { self.host = Some(host.into()); self } + /// Sets a hook that is called for every request that resolves to a known frontend asset, + /// right before the response is sent. Use it to inspect the [`Request`] and add or override + /// headers on the [`Response`], for example to append custom CORS or caching headers. pub fn on_request( mut self, f: F, @@ -71,6 +103,18 @@ impl Builder { self } + /// Builds the plugin, ready to be registered with [`tauri::Builder::plugin`]. + /// + /// On setup it spawns a background thread that starts a `tiny_http` server bound to + /// `host:port`. For every incoming request whose path resolves to a known frontend asset, the + /// server responds with that asset's bytes, automatically setting the `Content-Type`, the + /// `Content-Security-Policy` (when the asset has one) and a `Cache-Control: no-cache` header, + /// then invoking the [`Self::on_request`] hook (if any) before writing the response. + /// + /// # Panics + /// + /// Panics on the background thread if the server fails to bind to `host:port`, or if it fails + /// to send a response for a request. pub fn build(mut self) -> TauriPlugin { let port = self.port; let host = self.host.unwrap_or("localhost".to_string()); diff --git a/plugins/log/guest-js/index.ts b/plugins/log/guest-js/index.ts index 93022a97b..87537e2e1 100644 --- a/plugins/log/guest-js/index.ts +++ b/plugins/log/guest-js/index.ts @@ -2,15 +2,30 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Configure the Tauri application log system, and access the JavaScript-side log functions. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' import { listen, type UnlistenFn, type Event } from '@tauri-apps/api/event' +/** + * Options to associate extra metadata with a log entry. + */ export interface LogOptions { + /** The name of the file that emitted the log entry. Included in the log record's target when set. */ file?: string + /** The line number in {@linkcode LogOptions.file} that emitted the log entry. */ line?: number + /** Additional structured key-value pairs to attach to the log entry. */ keyValues?: Record } +/** + * The verbosity level of a log entry, matching the levels of the Rust `log` crate. + */ export enum LogLevel { /** * The "trace" level. @@ -132,11 +147,8 @@ async function log( /** * Logs a message at the error level. * - * @param message - * - * # Examples - * - * ```js + * @example + * ```typescript * import { error } from '@tauri-apps/plugin-log'; * * const err_info = "No connection"; @@ -144,6 +156,10 @@ async function log( * * error(`Error: ${err_info} on port ${port}`); * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 */ export async function error( message: string, @@ -155,17 +171,18 @@ export async function error( /** * Logs a message at the warn level. * - * @param message - * - * # Examples - * - * ```js + * @example + * ```typescript * import { warn } from '@tauri-apps/plugin-log'; * * const warn_description = "Invalid Input"; * * warn(`Warning! {warn_description}!`); * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 */ export async function warn( message: string, @@ -177,17 +194,18 @@ export async function warn( /** * Logs a message at the info level. * - * @param message - * - * # Examples - * - * ```js + * @example + * ```typescript * import { info } from '@tauri-apps/plugin-log'; * * const conn_info = { port: 40, speed: 3.20 }; * * info(`Connected to port {conn_info.port} at {conn_info.speed} Mb/s`); * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 */ export async function info( message: string, @@ -199,17 +217,18 @@ export async function info( /** * Logs a message at the debug level. * - * @param message - * - * # Examples - * - * ```js + * @example + * ```typescript * import { debug } from '@tauri-apps/plugin-log'; * * const pos = { x: 3.234, y: -1.223 }; * * debug(`New position: x: {pos.x}, y: {pos.y}`); * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 */ export async function debug( message: string, @@ -221,17 +240,18 @@ export async function debug( /** * Logs a message at the trace level. * - * @param message - * - * # Examples - * - * ```js + * @example + * ```typescript * import { trace } from '@tauri-apps/plugin-log'; * * let pos = { x: 3.234, y: -1.223 }; * * trace(`Position is: x: {pos.x}, y: {pos.y}`); * ``` + * + * @param message the message to log. + * @param options additional metadata (file, line, key-values) to attach to the log entry. + * @since 2.0.0 */ export async function trace( message: string, @@ -249,9 +269,22 @@ type LoggerFn = (fn: RecordPayload) => void /** * Attaches a listener for the log, and calls the passed function for each log entry. - * @param fn * - * @returns a function to cancel the listener. + * @example + * ```typescript + * import { attachLogger } from '@tauri-apps/plugin-log'; + * + * const detach = await attachLogger(({ level, message }) => { + * console.log(`[${level}] ${message}`); + * }); + * + * // detach the listener later + * detach(); + * ``` + * + * @param fn a function to call for every log entry emitted by the Rust side. + * @returns a promise resolving to a function to cancel the listener. + * @since 2.0.0 */ export async function attachLogger(fn: LoggerFn): Promise { return await listen('log://log', (event: Event) => { @@ -272,7 +305,18 @@ export async function attachLogger(fn: LoggerFn): Promise { /** * Attaches a listener that writes log entries to the console as they come in. * - * @returns a function to cancel the listener. + * @example + * ```typescript + * import { attachConsole } from '@tauri-apps/plugin-log'; + * + * const detach = await attachConsole(); + * + * // detach the listener later + * detach(); + * ``` + * + * @returns a promise resolving to a function to cancel the listener. + * @since 2.0.0 */ export async function attachConsole(): Promise { return await attachLogger(({ level, message }: RecordPayload) => { diff --git a/plugins/log/src/lib.rs b/plugins/log/src/lib.rs index 9366f8584..18e872758 100644 --- a/plugins/log/src/lib.rs +++ b/plugins/log/src/lib.rs @@ -6,7 +6,7 @@ //! //! ## Cargo features //! -//! - **colored**: Enables [`Builder::with_colors`] `fern`'s `colored` feature for ANSI-colored outputs. +//! - **colored**: Enables `Builder::with_colors`, which uses `fern`'s `colored` feature for ANSI-colored outputs. //! - **tracing**: Emit both log and tracing for the JavaScript log commands. #![doc( @@ -39,6 +39,8 @@ pub use log; mod commands; +/// The log target prefix used for log records emitted through the JavaScript `trace`/`debug`/`info`/`warn`/`error` +/// bindings, optionally followed by `::{location}` when the caller's source location could be determined. pub const WEBVIEW_TARGET: &str = "webview"; #[cfg(target_os = "ios")] @@ -59,16 +61,23 @@ const DEFAULT_LOG_TARGETS: [Target; 2] = [ const LOG_DATE_FORMAT: &[time::format_description::FormatItem<'_>] = format_description!("[year]-[month]-[day]_[hour]-[minute]-[second]"); +/// The error type returned by this plugin's fallible operations. #[derive(Debug, thiserror::Error)] pub enum Error { + /// Forwarded from a call into the Tauri API, e.g. resolving the app log directory. #[error(transparent)] Tauri(#[from] tauri::Error), + /// Forwarded from a filesystem operation performed while creating, writing to or rotating a log file. #[error(transparent)] Io(#[from] std::io::Error), + /// Forwarded from formatting a timestamp with the configured [`TimezoneStrategy`]. #[error(transparent)] TimeFormat(#[from] time::error::Format), + /// Forwarded from parsing an invalid `time` format description. #[error(transparent)] InvalidFormatDescription(#[from] time::error::InvalidFormatDescription), + /// Returned by [`Builder::split`] when [`Builder::skip_logger`] was called on the builder, since no logger was + /// created for it to return. #[error("Internal logger disabled and cannot be acquired or attached")] LoggerNotInitialized, } @@ -125,6 +134,7 @@ impl From for LogLevel { } } +/// The strategy applied to a log file when it exceeds [`Builder::max_file_size`]. #[derive(Debug, Clone)] pub enum RotationStrategy { /// Will keep all the logs, renaming them to include the date. @@ -135,13 +145,17 @@ pub enum RotationStrategy { KeepSome(usize), } +/// The timezone used to compute the current time for log message timestamps and rotated log file names. #[derive(Debug, Clone)] pub enum TimezoneStrategy { + /// Use UTC time. UseUtc, + /// Use the local timezone. Falls back to UTC if the local offset cannot be determined. UseLocal, } impl TimezoneStrategy { + /// Returns the current time according to this strategy. pub fn get_now(&self) -> OffsetDateTime { match self { TimezoneStrategy::UseUtc => OffsetDateTime::now_utc(), @@ -152,6 +166,7 @@ impl TimezoneStrategy { } } +/// The strategy used to open a log file when a [`TargetKind::Folder`] or [`TargetKind::LogDir`] target is set up. #[derive(Debug, Clone, PartialEq)] pub enum FileOpenStrategy { /// Open existing file from last session and append, if any. @@ -351,7 +366,9 @@ pub enum TargetKind { /// /// The plugin will ensure the directory exists before writing logs. Folder { + /// Path of the directory to write the log file to. path: PathBuf, + /// Name of the log file, without extension. Defaults to the app's package name when `None`. file_name: Option, }, /// Write logs to the OS specific logs directory. @@ -364,7 +381,10 @@ pub enum TargetKind { /// | macOS/iOS | `{homeDir}/Library/Logs/{bundleIdentifier}` | `/Users/Alice/Library/Logs/com.tauri.dev` | /// | Windows | `{FOLDERID_LocalAppData}/{bundleIdentifier}/logs` | `C:\Users\Alice\AppData\Local\com.tauri.dev\logs` | /// | Android | `{ConfigDir}/logs` | `/data/data/com.tauri.dev/files/logs` | - LogDir { file_name: Option }, + LogDir { + /// Name of the log file, without extension. Defaults to the app's package name when `None`. + file_name: Option, + }, /// Forward logs to the webview (via the `log://log` event). /// /// This requires the webview to subscribe to log events, via this plugins `attachConsole` function. @@ -385,6 +405,7 @@ pub struct Target { } impl Target { + /// Creates a new [`Target`] of the given [`TargetKind`], with no filters and the [`Builder`]'s formatter. #[inline] pub const fn new(kind: TargetKind) -> Self { Self { @@ -394,6 +415,8 @@ impl Target { } } + /// Adds a filter that can reject log records from being sent to this target. Multiple filters may be added by + /// calling this method more than once. #[inline] pub fn filter(mut self, filter: F) -> Self where @@ -403,6 +426,8 @@ impl Target { self } + /// Sets a formatter for log records sent to this target, overriding the [`Builder`]'s formatter for this + /// target only. #[inline] pub fn format(mut self, formatter: F) -> Self where @@ -413,6 +438,8 @@ impl Target { } } +/// Builds the log plugin, configuring targets, rotation, formatting and level filters before attaching the +/// resulting logger as the global logger for the [`log`] crate. pub struct Builder { dispatch: fern::Dispatch, rotation_strategy: RotationStrategy, @@ -454,6 +481,10 @@ impl Default for Builder { } impl Builder { + /// Creates a new [`Builder`] with the default configuration: targets [`TargetKind::Stdout`] and + /// [`TargetKind::LogDir`] (using the app's package name as the file name), [`RotationStrategy::KeepOne`], + /// [`TimezoneStrategy::UseUtc`], [`FileOpenStrategy::Append`], a maximum log file size of `40_000` bytes + /// (see [`Self::max_file_size`]) and the default `fern` formatter. pub fn new() -> Self { Default::default() } @@ -604,14 +635,13 @@ impl Builder { /// Example usage: /// /// ``` - /// # fn main() { /// tauri_plugin_log::Builder::new() /// .level(log::LevelFilter::Info) /// .filter(|metadata| { /// // Reject messages with the `Error` log level. - /// metadata.level() != log::LevelFilter::Error - /// }) - /// # } + /// metadata.level() != log::Level::Error + /// }); + /// ``` pub fn filter(mut self, filter: F) -> Self where F: Fn(&log::Metadata) -> bool + Send + Sync + 'static, @@ -661,10 +691,16 @@ impl Builder { /// Both scenarios require calling this method. /// /// ```rust + /// # struct SimpleLogger; + /// # impl log::Log for SimpleLogger { + /// # fn enabled(&self, _metadata: &log::Metadata) -> bool { true } + /// # fn log(&self, _record: &log::Record) {} + /// # fn flush(&self) {} + /// # } /// static LOGGER: SimpleLogger = SimpleLogger; /// - /// log::set_logger(&SimpleLogger)?; - /// log::set_max_level(LevelFilter::Info); + /// log::set_logger(&LOGGER).expect("a logger was already set"); + /// log::set_max_level(log::LevelFilter::Info); /// tauri_plugin_log::Builder::new() /// .skip_logger(); /// ``` @@ -824,6 +860,15 @@ impl Builder { plugin::Builder::new("log").invoke_handler(tauri::generate_handler![commands::log]) } + /// Builds the [`TauriPlugin`] and returns it together with the configured [`log::LevelFilter`] and [`log::Log`] + /// implementation, instead of registering them as the global logger. + /// + /// Use this instead of [`Self::build`] when this logger needs to be combined with another one, e.g. when + /// forwarding logs to `tracing` via `tracing-log`, since only a single global logger can be installed. + /// + /// # Errors + /// + /// Returns [`Error::LoggerNotInitialized`] if [`Self::skip_logger`] was called on this builder. #[allow(clippy::type_complexity)] pub fn split( self, @@ -846,6 +891,8 @@ impl Builder { Ok((plugin.build(), max_level, log)) } + /// Builds the [`TauriPlugin`], attaching the configured logger as the global logger for the [`log`] crate on + /// setup, unless [`Self::skip_logger`] was called on this builder. pub fn build(self) -> TauriPlugin { Self::plugin_builder() .setup(move |app_handle, _api| { diff --git a/plugins/nfc/guest-js/index.ts b/plugins/nfc/guest-js/index.ts index 9c1fa10b2..35489b9cb 100644 --- a/plugins/nfc/guest-js/index.ts +++ b/plugins/nfc/guest-js/index.ts @@ -2,39 +2,95 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Read and write NFC tags on Android and iOS. + * + * This plugin is mobile only, the APIs reject on desktop platforms. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' +/** + * Record Type Definition (RTD) of an NDEF well known text record, the `"T"` (`0x54`) byte. + * + * @since 2.0.0 + */ export const RTD_TEXT = [0x54] // "T" +/** + * Record Type Definition (RTD) of an NDEF well known URI record, the `"U"` (`0x55`) byte. + * + * @since 2.0.0 + */ export const RTD_URI = [0x55] // "U" +/** + * Filters the tags to scan by the URI of their payload. + * + * Every property is optional and only the ones that are set take part in the filter. + * **Android only**, the iOS implementation ignores this filter. + */ export interface UriFilter { + /** Only match URIs with this scheme, e.g. `https`. */ scheme?: string + /** Only match URIs with this authority (host), e.g. `tauri.app`. */ host?: string + /** Only match URIs whose path starts with this prefix, e.g. `/docs`. */ pathPrefix?: string } +/** + * The NFC technologies a tag can support, mirroring the `android.nfc.tech` classes. + * + * **Android only**, used by the `techLists` filter of an `ndef` {@link ScanKind}. + */ export enum TechKind { + /** ISO-DEP (ISO 14443-4) properties and I/O operations. */ IsoDep, + /** MIFARE Classic properties and I/O operations. */ MifareClassic, + /** MIFARE Ultralight and MIFARE Ultralight C properties and I/O operations. */ MifareUltralight, + /** NDEF data and operations on tags that are already formatted as NDEF. */ Ndef, + /** Formatting operations on tags that can be formatted as NDEF but are not yet. */ NdefFormatable, + /** NFC-A (ISO 14443-3A) properties and I/O operations. */ NfcA, + /** NFC-B (ISO 14443-3B) properties and I/O operations. */ NfcB, + /** NFC Barcode (Kovio NFC Barcode) properties and I/O operations. */ NfcBarcode, + /** NFC-F (JIS 6319-4) properties and I/O operations. */ NfcF, + /** NFC-V (ISO 15693) properties and I/O operations. */ NfcV } +/** + * The kind of scan to perform, which defines which tags are matched. + * + * Use `tag` to match any discovered tag and `ndef` to only match tags carrying an NDEF message. + */ export type ScanKind = | { + /** Matches any tag that is discovered, whether it carries an NDEF message or not. */ type: 'tag' + /** Only match tags whose payload URI matches this filter. **Android only**. */ uri?: UriFilter + /** Only match tags whose payload has this MIME type, e.g. `text/plain`. **Android only**. */ mimeType?: string } | { + /** Only matches tags that carry an NDEF message. */ type: 'ndef' + /** Only match tags whose NDEF payload URI matches this filter. **Android only**. */ uri?: UriFilter + /** + * Only match tags whose NDEF payload has this MIME type, e.g. `text/plain`. + * **Android only**. + */ mimeType?: string /** * Each of the tech-lists is considered independently and the activity is considered a match if @@ -59,7 +115,13 @@ export type ScanKind = techLists?: TechKind[][] } +/** Options for the {@link scan} function. */ export interface ScanOptions { + /** + * Whether the connection to the scanned tag must be kept open after the scan resolves, + * so that a following {@link write} call writes to that tag instead of scanning a new one. + * Defaults to `false`. + */ keepSessionAlive?: boolean /** Message displayed in the UI. iOS only. */ message?: string @@ -67,7 +129,12 @@ export interface ScanOptions { successMessage?: string } +/** Options for the {@link write} function. */ export interface WriteOptions { + /** + * The kind of scan to perform to find the tag to write to. + * Required on Android unless a {@link scan} session is kept alive. + */ kind?: ScanKind /** Message displayed in the UI when reading the tag. iOS only. */ message?: string @@ -77,36 +144,101 @@ export interface WriteOptions { successMessage?: string } +/** + * The Type Name Format (TNF) of an NDEF record, + * which defines how the record type is interpreted. + */ export enum NFCTypeNameFormat { + /** The record is empty: type, identifier and payload must be empty. */ Empty = 0, + /** + * The record type is an NFC Forum well known type, defined by a Record Type Definition (RTD) + * such as {@link RTD_TEXT} or {@link RTD_URI}. + */ NfcWellKnown = 1, + /** The record type is a MIME media type as defined in RFC 2046, e.g. `text/plain`. */ Media = 2, + /** The record type is an absolute URI as defined in RFC 3986. */ AbsoluteURI = 3, + /** The record type is an NFC Forum external type, i.e. a type namespaced by its issuer. */ NfcExternal = 4, + /** + * The record type is unknown: the type must be empty and the payload interpretation + * is left to the application. + */ Unknown = 5, + /** + * The record is a middle or last chunk of a chunked record and inherits the type of the + * first chunk, so its own type must be empty. + */ Unchanged = 6 } +/** An NDEF record read from a scanned tag. */ export interface TagRecord { + /** + * The Type Name Format (TNF) of the record, + * which defines how {@link TagRecord.kind} is interpreted. + */ tnf: NFCTypeNameFormat + /** The record type bytes. */ kind: number[] + /** The record identifier bytes. Can be empty. */ id: number[] + /** The record payload bytes. */ payload: number[] } +/** An NFC tag that has been scanned. */ export interface Tag { + /** The tag identifier bytes, as reported by the operating system. */ id: number[] + /** The technologies the tag supports, e.g. `["android.nfc.tech.Ndef"]` on Android. */ kind: string[] + /** The NDEF records stored on the tag. Empty when the tag holds no NDEF message. */ records: TagRecord[] } +/** + * An NDEF record to be written to a tag. + * + * Use {@link record}, {@link textRecord} or {@link uriRecord} to create one. + */ export interface NFCRecord { + /** The Type Name Format (TNF) of the record. */ format: NFCTypeNameFormat + /** + * The record type, interpreted according to {@link NFCRecord.format}. + * For well known records this is a Record Type Definition (RTD) value + * such as {@link RTD_TEXT} or {@link RTD_URI}. + */ kind: number[] + /** The record identifier. Can be empty. */ id: number[] + /** The record payload bytes. */ payload: number[] } +/** + * Creates an NDEF record with the given type name format, type, identifier and payload. + * + * Strings are encoded as UTF-8 byte arrays. + * + * @example + * ```typescript + * import { record, NFCTypeNameFormat, write } from '@tauri-apps/plugin-nfc'; + * const mimeRecord = record(NFCTypeNameFormat.Media, 'text/plain', '', 'hello world'); + * await write([mimeRecord], { kind: { type: 'ndef' } }); + * ``` + * + * @param format The Type Name Format (TNF) of the record. + * @param kind The record type, interpreted according to `format`. + * @param id The record identifier. Use an empty string or array when the record has none. + * @param payload The record payload. + * @returns The NDEF record, ready to be written with {@link write}. + * + * @since 2.0.0 + */ export function record( format: NFCTypeNameFormat, kind: string | number[], @@ -127,6 +259,25 @@ export function record( } } +/** + * Creates an NDEF well known text record ({@link RTD_TEXT}). + * + * The payload is the UTF-8 encoded text prefixed by the language code + * and by a status byte holding the length of that language code. + * + * @example + * ```typescript + * import { textRecord, write } from '@tauri-apps/plugin-nfc'; + * await write([textRecord('hello world')], { kind: { type: 'ndef' } }); + * ``` + * + * @param text The text to store in the record. + * @param id The record identifier. Defaults to an empty identifier. + * @param language The IANA language code of the text. Defaults to `en`. + * @returns The NDEF record, ready to be written with {@link write}. + * + * @since 2.0.0 + */ export function textRecord( text: string, id?: string | number[], @@ -200,6 +351,24 @@ function encodeURI(uri: string): number[] { return encoded } +/** + * Creates an NDEF well known URI record ({@link RTD_URI}). + * + * The URI is encoded with the NDEF URI abbreviation scheme: a known prefix such as + * `https://` is replaced by the identifier code byte that starts the payload. + * + * @example + * ```typescript + * import { uriRecord, write } from '@tauri-apps/plugin-nfc'; + * await write([uriRecord('https://tauri.app')], { kind: { type: 'ndef' } }); + * ``` + * + * @param uri The URI to store in the record. + * @param id The record identifier. Defaults to an empty identifier. + * @returns The NDEF record, ready to be written with {@link write}. + * + * @since 2.0.0 + */ export function uriRecord(uri: string, id?: string | number[]): NFCRecord { return record( NFCTypeNameFormat.NfcWellKnown, @@ -215,18 +384,22 @@ function mapScanKind(kind: ScanKind): Record { } /** - * Scans an NFC tag. - * - * ```javascript - * import { scan } from "@tauri-apps/plugin-nfc"; - * await scan({ type: "tag" }); - * ``` + * Scans an NFC tag, resolving when a tag matching the given filters is read. * * See for more information. * - * @param kind - * @param options - * @returns + * @example + * ```typescript + * import { scan } from '@tauri-apps/plugin-nfc'; + * const tag = await scan({ type: 'tag' }); + * ``` + * + * @param kind The kind of scan to perform, which defines how tags are matched. + * @param options Additional scan options such as the iOS UI messages and whether the + * session must be kept alive for a following {@link write} call. + * @returns A promise resolving to the tag that has been scanned. + * + * @since 2.0.0 */ export async function scan( kind: ScanKind, @@ -241,17 +414,20 @@ export async function scan( /** * Write to an NFC tag. * - * ```javascript - * import { uriRecord, write } from "@tauri-apps/plugin-nfc"; - * await write([uriRecord("https://tauri.app")], { kind: { type: "ndef" } }); - * ``` - * * If you did not previously call {@link scan} with {@link ScanOptions.keepSessionAlive} set to true, * it will first scan the tag then write to it. * - * @param records - * @param options - * @returns + * @example + * ```typescript + * import { uriRecord, write } from '@tauri-apps/plugin-nfc'; + * await write([uriRecord('https://tauri.app')], { kind: { type: 'ndef' } }); + * ``` + * + * @param records The NDEF records to write to the tag. + * @param options Additional write options such as the kind of scan used to find the tag + * and the iOS UI messages. + * + * @since 2.0.0 */ export async function write( records: NFCRecord[], @@ -268,6 +444,23 @@ export async function write( }) } +/** + * Checks whether NFC is supported by the device and currently usable by the app. + * + * Resolves to `false` on Android when the device has no NFC adapter or NFC is disabled in the + * device settings, and on iOS when the `NFCReaderUsageDescription` entry is missing from the + * `Info.plist` file or NFC tag reading is unavailable. + * + * @example + * ```typescript + * import { isAvailable } from '@tauri-apps/plugin-nfc'; + * const canScan = await isAvailable(); + * ``` + * + * @returns A promise resolving to whether NFC is available on the device. + * + * @since 2.0.0 + */ export async function isAvailable(): Promise { const { available }: { available: boolean } = await invoke( 'plugin:nfc|is_available' diff --git a/plugins/nfc/src/error.rs b/plugins/nfc/src/error.rs index 339e763b1..75d21ea92 100644 --- a/plugins/nfc/src/error.rs +++ b/plugins/nfc/src/error.rs @@ -4,12 +4,21 @@ use serde::{ser::Serializer, Serialize}; +/// Alias for a [`std::result::Result`] with the error type [`Error`]. pub type Result = std::result::Result; +/// Errors returned by the NFC plugin. +/// +/// Serializes to the error message string, so it can be returned directly from a command. #[derive(Debug, thiserror::Error)] pub enum Error { + /// An I/O error happened. #[error(transparent)] Io(#[from] std::io::Error), + /// The call to the Android or iOS plugin implementation failed, + /// either because the arguments could not be serialized, the response could not be + /// deserialized or the native side rejected the call (e.g. NFC is unavailable + /// or the tag could not be read or written). #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), diff --git a/plugins/nfc/src/lib.rs b/plugins/nfc/src/lib.rs index 33a47a96a..0396b7733 100644 --- a/plugins/nfc/src/lib.rs +++ b/plugins/nfc/src/lib.rs @@ -2,6 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! Read and write NFC tags on Android and iOS. +//! +//! This plugin is mobile only: the whole crate is gated behind `#[cfg(mobile)]`, +//! so it expands to nothing when compiling for Linux, macOS or Windows. +//! +//! Register the plugin with `init` and use the `NfcExt` trait to reach the +//! `Nfc` instance from any `tauri::Manager` implementation (the app handle, a window, ...). + #![cfg(mobile)] use serde::{Deserialize, Serialize}; @@ -37,6 +45,12 @@ struct WriteRequest { } impl Nfc { + /// Checks whether NFC is supported by the device and currently usable by the app. + /// + /// On Android this is `false` when the device has no NFC adapter or when NFC is + /// disabled in the device settings. + /// On iOS this is `false` when the `NFCReaderUsageDescription` entry is missing from the + /// `Info.plist` file or when NFC tag reading is not available on the device. pub fn is_available(&self) -> crate::Result { self.0 .run_mobile_plugin::("isAvailable", ()) @@ -44,6 +58,16 @@ impl Nfc { .map_err(Into::into) } + /// Scans an NFC tag, blocking until a tag matching the given [`ScanRequest::kind`] filters + /// is read or the scan fails. + /// + /// Set [`ScanRequest::keep_session_alive`] to `true` to keep the connection to the tag open + /// after it has been scanned, so that a following [`Self::write`] call writes to that tag. + /// + /// # Errors + /// + /// Returns an error when NFC is not available (see [`Self::is_available`]) + /// or when the tag could not be read. pub fn scan(&self, payload: ScanRequest) -> crate::Result { self.0 .run_mobile_plugin("scan", payload) @@ -51,6 +75,19 @@ impl Nfc { .map_err(Into::into) } + /// Writes the given NDEF records to an NFC tag, blocking until the write completes or fails. + /// + /// Because this API does not take a scan kind, on Android it can only write to the tag of an + /// ongoing session, so it must be preceded by a [`Self::scan`] call with + /// [`ScanRequest::keep_session_alive`] set to `true`. + /// On iOS an NDEF reader session is started when there is no ongoing session, and the + /// records are written to the first tag that is scanned. + /// + /// # Errors + /// + /// Returns an error when NFC is not available (see [`Self::is_available`]), when there is no + /// connected tag on Android, when the tag is read-only, when it cannot hold the message or + /// when it does not support the NDEF format. pub fn write(&self, records: Vec) -> crate::Result<()> { self.0 .run_mobile_plugin("write", WriteRequest { records }) @@ -60,6 +97,7 @@ impl Nfc { /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the NFC APIs. pub trait NfcExt { + /// Returns the [`Nfc`] instance managed by the plugin. fn nfc(&self) -> &Nfc; } diff --git a/plugins/nfc/src/models.rs b/plugins/nfc/src/models.rs index eb05cf7ac..ccb4c55e1 100644 --- a/plugins/nfc/src/models.rs +++ b/plugins/nfc/src/models.rs @@ -5,72 +5,132 @@ use serde::{Deserialize, Serialize, Serializer}; use std::fmt::Display; +/// Arguments of the [`Nfc::scan`](crate::Nfc::scan) API. #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct ScanRequest { + /// The kind of scan to perform, which defines how tags are matched. pub kind: ScanKind, + /// Whether the connection to the scanned tag must be kept open after the scan resolves. + /// + /// When `true`, a following [`Nfc::write`](crate::Nfc::write) call writes to the tag that was + /// scanned instead of starting a new session. pub keep_session_alive: bool, } +/// An NDEF record to be written to a tag. +/// +/// Use [`NFCTypeNameFormat`] to describe how [`Self::kind`] must be interpreted. #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct NfcRecord { + /// The Type Name Format (TNF) of the record. pub format: NFCTypeNameFormat, + /// The record type, interpreted according to [`Self::format`]. + /// + /// For [`NFCTypeNameFormat::NfcWellKnown`] records this is a Record Type Definition (RTD) + /// value such as `[0x54]` (`RTD_TEXT`) or `[0x55]` (`RTD_URI`). pub kind: Vec, + /// The record identifier. Can be empty. pub id: Vec, + /// The record payload bytes. pub payload: Vec, } +/// The Type Name Format (TNF) of an NDEF record, which defines how the record type is interpreted. +/// +/// Serialized as its numeric value. #[derive(serde_repr::Deserialize_repr, serde_repr::Serialize_repr)] #[repr(u8)] pub enum NFCTypeNameFormat { + /// The record is empty: type, identifier and payload must be empty. Empty = 0, + /// The record type is an NFC Forum well known type, defined by a Record Type Definition (RTD) + /// such as `RTD_TEXT` (`[0x54]`) or `RTD_URI` (`[0x55]`). NfcWellKnown = 1, + /// The record type is a MIME media type as defined in RFC 2046, e.g. `text/plain`. Media = 2, + /// The record type is an absolute URI as defined in RFC 3986. AbsoluteURI = 3, + /// The record type is an NFC Forum external type, i.e. a type namespaced by its issuer. NfcExternal = 4, + /// The record type is unknown: the type must be empty and the payload interpretation is + /// left to the application. Unknown = 5, + /// The record is a middle or last chunk of a chunked record and inherits the type of the + /// first chunk, so its own type must be empty. Unchanged = 6, } +/// An NDEF record read from a scanned tag. #[derive(Deserialize)] pub struct NfcTagRecord { + /// The Type Name Format (TNF) of the record, which defines how [`Self::kind`] is interpreted. pub tnf: NFCTypeNameFormat, + /// The record type bytes. pub kind: Vec, + /// The record identifier bytes. Can be empty. pub id: Vec, + /// The record payload bytes. pub payload: Vec, } +/// An NFC tag that has been scanned. #[derive(Deserialize)] pub struct NfcTag { + /// The tag identifier, as reported by the operating system. pub id: String, + /// The technology the tag supports. pub kind: String, + /// The NDEF records stored on the tag. Empty when the tag holds no NDEF message. pub records: Vec, } +/// Response of the [`Nfc::scan`](crate::Nfc::scan) API. #[derive(Deserialize)] pub struct ScanResponse { + /// The tag that has been scanned. pub tag: NfcTag, } +/// Filters the tags to scan by the URI of their NDEF payload. +/// +/// Every field is optional and only the ones that are set take part in the filter. +/// **Android only**: the iOS implementation ignores this filter. #[derive(Debug, Default, Serialize)] pub struct UriFilter { + /// Only match URIs with this scheme, e.g. `https`. scheme: Option, + /// Only match URIs with this authority (host), e.g. `tauri.app`. host: Option, + /// Only match URIs whose path starts with this prefix, e.g. `/docs`. path_prefix: Option, } +/// The NFC technologies a tag can support, mirroring the `android.nfc.tech` classes. +/// +/// **Android only**. Serialized as the technology name, e.g. `"IsoDep"`. #[derive(Debug)] pub enum TechKind { + /// ISO-DEP (ISO 14443-4) properties and I/O operations. IsoDep, + /// MIFARE Classic properties and I/O operations. MifareClassic, + /// MIFARE Ultralight and MIFARE Ultralight C properties and I/O operations. MifareUltralight, + /// NDEF data and operations on tags that are already formatted as NDEF. Ndef, + /// Formatting operations on tags that can be formatted as NDEF but are not yet. NdefFormatable, + /// NFC-A (ISO 14443-3A) properties and I/O operations. NfcA, + /// NFC-B (ISO 14443-3B) properties and I/O operations. NfcB, + /// NFC Barcode (Kovio NFC Barcode) properties and I/O operations. NfcBarcode, + /// NFC-F (JIS 6319-4) properties and I/O operations. NfcF, + /// NFC-V (ISO 15693) properties and I/O operations. NfcV, } @@ -104,16 +164,32 @@ impl Serialize for TechKind { } } +/// The kind of scan to perform, which defines which tags are matched. #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub enum ScanKind { + /// Only match tags that carry an NDEF message. Ndef { + /// Only match tags whose NDEF payload has this MIME type, e.g. `text/plain`. + /// **Android only**. mime_type: Option, + /// Only match tags whose NDEF payload URI matches this filter. **Android only**. uri: Option, + /// Only match tags supporting the listed technologies. + /// + /// Each tech list is considered independently and the tag matches when any single tech + /// list matches it, which provides AND (inside a list) and OR (between lists) semantics. + /// + /// **Android only**. See + /// + /// for more information. tech_list: Option>>, }, + /// Match any tag that is discovered, whether it carries an NDEF message or not. Tag { + /// Only match tags whose payload has this MIME type, e.g. `text/plain`. **Android only**. mime_type: Option, + /// Only match tags whose payload URI matches this filter. **Android only**. uri: Option, }, } diff --git a/plugins/notification/guest-js/index.ts b/plugins/notification/guest-js/index.ts index 7045ac66e..ee9c24c91 100644 --- a/plugins/notification/guest-js/index.ts +++ b/plugins/notification/guest-js/index.ts @@ -35,7 +35,7 @@ interface Options { */ channelId?: string /** - * Notification title. + * The title displayed on the notification. */ title: string /** @@ -139,11 +139,28 @@ interface Options { number?: number } +/** + * The set of date fields a scheduled notification must match to be delivered. + * + * Fields that are not set match any value, so the notification fires on every date + * whose remaining components match. + */ interface ScheduleInterval { + /** + * The year the notification fires on. + */ year?: number + /** + * The month of the year the notification fires on. + */ month?: number + /** + * The day of the month the notification fires on. + */ day?: number /** + * The day of the week the notification fires on. + * * 1 - Sunday * 2 - Monday * 3 - Tuesday @@ -153,26 +170,77 @@ interface ScheduleInterval { * 7 - Saturday */ weekday?: number + /** + * The hour of the day the notification fires on, in the 24-hour clock. + */ hour?: number + /** + * The minute of the hour the notification fires on. + */ minute?: number + /** + * The second of the minute the notification fires on. + */ second?: number } +/** + * The unit of the repeating interval used by {@link Schedule.every}. + */ enum ScheduleEvery { + /** + * The notification repeats every year. + * + * On Android a year is approximated as 52 weeks. + */ Year = 'year', + /** + * The notification repeats every month. + * + * On Android a month is approximated as 30 days. + */ Month = 'month', + /** + * The notification repeats every two weeks. + */ TwoWeeks = 'twoWeeks', + /** + * The notification repeats every week. + */ Week = 'week', + /** + * The notification repeats every day. + */ Day = 'day', + /** + * The notification repeats every hour. + */ Hour = 'hour', + /** + * The notification repeats every minute. + */ Minute = 'minute', /** - * Not supported on iOS. + * The notification repeats every second. + * + * Not supported on iOS, where repeating triggers must be at least a minute apart. */ Second = 'second' } +/** + * Defines when a scheduled notification is delivered. + * + * Build one with the static {@link Schedule.at}, {@link Schedule.interval} and + * {@link Schedule.every} helpers, then pass it to the `schedule` option of a notification. + * Scheduling is only supported on mobile; desktop notifications are always shown immediately. + * + * @since 2.0.0 + */ class Schedule { + /** + * Set when the notification fires at a fixed date and time. + */ at: | { date: Date @@ -181,6 +249,9 @@ class Schedule { } | undefined + /** + * Set when the notification fires whenever the current date matches the given fields. + */ interval: | { interval: ScheduleInterval @@ -188,6 +259,9 @@ class Schedule { } | undefined + /** + * Set when the notification repeats on a fixed interval. + */ every: | { interval: ScheduleEvery @@ -196,6 +270,22 @@ class Schedule { } | undefined + /** + * Creates a schedule that fires the notification at the given date and time. + * + * @example + * ```typescript + * import { Schedule, sendNotification } from '@tauri-apps/plugin-notification'; + * const schedule = Schedule.at(new Date(Date.now() + 60 * 1000)); + * sendNotification({ title: 'Tauri', body: 'One minute later', schedule }); + * ``` + * + * @param date The date and time the notification fires at. It must be in the future. + * @param repeating Whether the notification keeps repeating, using the duration between the moment it is scheduled and `date` as the interval. The interval must be at least one minute on iOS. + * @param allowWhileIdle Whether the notification is allowed to fire while the device is in low-power idle (Doze) mode. Android only. + * + * @returns A schedule that can be assigned to the `schedule` option of a notification. + */ static at(date: Date, repeating = false, allowWhileIdle = false): Schedule { return { at: { date, repeating, allowWhileIdle }, @@ -204,6 +294,23 @@ class Schedule { } } + /** + * Creates a schedule that fires the notification whenever the current date matches + * every field set on the given interval. + * + * @example + * ```typescript + * import { Schedule, sendNotification } from '@tauri-apps/plugin-notification'; + * // fires every day at 9:00 + * const schedule = Schedule.interval({ hour: 9, minute: 0 }); + * sendNotification({ title: 'Tauri', body: 'Good morning', schedule }); + * ``` + * + * @param interval The date fields the current date must match for the notification to fire. + * @param allowWhileIdle Whether the notification is allowed to fire while the device is in low-power idle (Doze) mode. Android only. + * + * @returns A schedule that can be assigned to the `schedule` option of a notification. + */ static interval( interval: ScheduleInterval, allowWhileIdle = false @@ -215,6 +322,22 @@ class Schedule { } } + /** + * Creates a schedule that repeatedly fires the notification, once every `count` interval units. + * + * @example + * ```typescript + * import { Schedule, ScheduleEvery, sendNotification } from '@tauri-apps/plugin-notification'; + * const schedule = Schedule.every(ScheduleEvery.Hour, 2); + * sendNotification({ title: 'Tauri', body: 'Every two hours', schedule }); + * ``` + * + * @param kind The unit of the repeating interval. + * @param count How many interval units elapse between each notification. + * @param allowWhileIdle Whether the notification is allowed to fire while the device is in low-power idle (Doze) mode. Android only. + * + * @returns A schedule that can be assigned to the `schedule` option of a notification. + */ static every( kind: ScheduleEvery, count: number, @@ -238,17 +361,53 @@ interface Attachment { url: string } +/** + * A button the user can tap on a notification, belonging to an {@link ActionType}. + * + * Only used on mobile. On Android only the identifier, the title and the input flag are used. + */ interface Action { + /** + * The identifier of this action, reported back when the user triggers it. + */ id: string + /** + * The text displayed on the action button. + */ title: string + /** + * Whether the device must be unlocked for the action to run. iOS only. + */ requiresAuthentication?: boolean + /** + * Whether the app is brought to the foreground when the action is triggered. iOS only. + */ foreground?: boolean + /** + * Whether the action is displayed as destructive, usually in red. iOS only. + */ destructive?: boolean + /** + * Whether triggering the action lets the user type a text response. + */ input?: boolean + /** + * The text displayed on the button that submits the text input. iOS only. + */ inputButtonTitle?: string + /** + * The placeholder displayed on the empty text input field. iOS only. + */ inputPlaceholder?: string } +/** + * A group of {@link Action}s a notification can display, referenced by the + * `actionTypeId` option of a notification. + * + * Register it with {@link registerActionTypes} before sending a notification that uses it. + * Only used on mobile. + */ interface ActionType { /** * The identifier of this action type @@ -258,58 +417,206 @@ interface ActionType { * The list of associated actions */ actions: Action[] + /** + * The placeholder shown instead of the notification body when previews are hidden. iOS only. + */ hiddenPreviewsBodyPlaceholder?: string + /** + * Whether the app is notified when the user dismisses the notification. iOS only. + */ customDismissAction?: boolean + /** + * Whether the notification can be displayed in a CarPlay environment. iOS only. + */ allowInCarPlay?: boolean + /** + * Whether the notification title is shown even when previews are hidden. iOS only. + */ hiddenPreviewsShowTitle?: boolean + /** + * Whether the notification subtitle is shown even when previews are hidden. iOS only. + */ hiddenPreviewsShowSubtitle?: boolean } +/** + * A notification that was scheduled and has not been delivered yet. + * + * Returned by {@link pending}, which is only supported on mobile. + */ interface PendingNotification { + /** + * The identifier of the notification. + */ id: number + /** + * The title of the notification, if it was set. + */ title?: string + /** + * The body of the notification, if it was set. + */ body?: string + /** + * The schedule that determines when the notification is delivered. + */ schedule: Schedule } +/** + * A notification that was delivered and is still visible in the notification center. + * + * Returned by {@link active}, which is only supported on mobile. Which fields are + * populated depends on the platform, since Android and iOS expose different + * information about delivered notifications. + */ interface ActiveNotification { + /** + * The identifier of the notification. + */ id: number + /** + * The tag the notification was posted with. Android only. + */ tag?: string + /** + * The title of the notification, if it was set. + */ title?: string + /** + * The body of the notification, if it was set. + */ body?: string + /** + * The identifier of the group the notification belongs to. Android only. + */ group?: string + /** + * Whether the notification is the summary of its group. Android only. + */ groupSummary: boolean + /** + * The platform extras attached to the notification, as string values. Android only. + */ data: Record + /** + * The extra payload that was stored in the notification. + */ extra: Record + /** + * The attachments of the notification. iOS only. + */ attachments: Attachment[] + /** + * The identifier of the action type the notification was registered with. iOS only. + */ actionTypeId?: string + /** + * The schedule the notification was delivered with, if it was scheduled. + */ schedule?: Schedule + /** + * The sound resource name of the notification. iOS only. + */ sound?: string } +/** + * How much the notifications of a {@link Channel} interrupt the user. + * + * It maps to the Android `NotificationManager.IMPORTANCE_*` constants and is only used on Android. + */ enum Importance { + /** + * The notifications are not shown. + */ None = 0, + /** + * The notifications are only shown in the shade, below the fold, without a status bar icon. + */ Min, + /** + * The notifications are shown without a sound. + */ Low, + /** + * The notifications are shown and make a sound. + * + * This is the value used when the channel does not define an importance. + */ Default, + /** + * The notifications are shown, make a sound and pop up as a heads-up notification. + */ High } +/** + * How much of a notification is shown on the lock screen. + * + * It maps to the Android `Notification.VISIBILITY_*` constants and is only used on Android. + */ enum Visibility { + /** + * The notification is not shown on the lock screen at all. + */ Secret = -1, + /** + * The notification is shown on the lock screen with its sensitive content hidden. + * + * This is the value used when the channel does not define a visibility. + */ Private, + /** + * The notification is shown in full on the lock screen. + */ Public } +/** + * A notification channel, the category users configure notification behavior on. + * + * Notifications reference a channel through their `channelId` option and are not delivered + * when the channel does not exist. Channels are only supported on Android. + */ interface Channel { + /** + * The identifier of this channel. + */ id: string + /** + * The user visible name of this channel. + */ name: string + /** + * The user visible description of this channel. + */ description?: string + /** + * The name of the sound resource played by the notifications of this channel. + * + * The resource must be placed in the app's `res/raw` folder. + */ sound?: string + /** + * Whether the notifications of this channel blink the device light. + */ lights?: boolean + /** + * The color of the device light, as a color string such as `#ff0000`. + */ lightColor?: string + /** + * Whether the notifications of this channel vibrate the device. + */ vibration?: boolean + /** + * How much the notifications of this channel interrupt the user. + */ importance?: Importance + /** + * How much of the notifications of this channel is shown on the lock screen. + */ visibility?: Visibility } @@ -321,6 +628,8 @@ interface Channel { * const permissionGranted = await isPermissionGranted(); * ``` * + * @returns A promise resolving to whether the permission to send notifications is granted. + * * @since 2.0.0 */ async function isPermissionGranted(): Promise { @@ -366,6 +675,8 @@ async function requestPermission(): Promise { * } * ``` * + * @param options The notification content, or the notification title when a string is given. + * * @since 2.0.0 */ function sendNotification(options: Options | string): void { @@ -391,6 +702,8 @@ function sendNotification(options: Options | string): void { * }]) * ``` * + * @param types The action types to register. + * * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 @@ -425,6 +738,8 @@ async function pending(): Promise { * await cancel([-34234, 23432, 4311]); * ``` * + * @param notifications The identifiers of the pending notifications to cancel. + * * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 @@ -472,10 +787,12 @@ async function active(): Promise { * * @example * ```typescript - * import { cancel } from '@tauri-apps/plugin-notification'; - * await cancel([-34234, 23432, 4311]) + * import { removeActive } from '@tauri-apps/plugin-notification'; + * await removeActive([{ id: -34234 }, { id: 23432 }, { id: 4311 }]) * ``` * + * @param notifications The active notifications to remove, identified by their id and, on Android, their optional tag. + * * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 @@ -519,6 +836,8 @@ async function removeAllActive(): Promise { * }); * ``` * + * @param channel The channel to create. + * * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 @@ -533,9 +852,11 @@ async function createChannel(channel: Channel): Promise { * @example * ```typescript * import { removeChannel } from '@tauri-apps/plugin-notification'; - * await removeChannel(); + * await removeChannel('new-messages'); * ``` * + * @param id The identifier of the channel to remove. + * * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 @@ -561,12 +882,51 @@ async function channels(): Promise { return await invoke('plugin:notification|listChannels') } +/** + * Listens to notifications that are delivered while the app is running. + * + * Only emitted on mobile. + * + * @example + * ```typescript + * import { onNotificationReceived } from '@tauri-apps/plugin-notification'; + * const unlisten = await onNotificationReceived((notification) => { + * console.log(`received notification: ${notification.title}`); + * }); + * ``` + * + * @param cb The closure called with the notification that was delivered. + * + * @returns A promise resolving to a listener that can be used to stop listening for the event. + * + * @since 2.0.0 + */ async function onNotificationReceived( cb: (notification: Options) => void ): Promise { return await addPluginListener('notification', 'notification', cb) } +/** + * Listens to the actions the user performs on a notification. + * + * Only emitted on mobile, for notifications that reference an action type + * registered with {@link registerActionTypes}. + * + * @example + * ```typescript + * import { onAction } from '@tauri-apps/plugin-notification'; + * const unlisten = await onAction((notification) => { + * console.log(`user acted on notification: ${notification.title}`); + * }); + * ``` + * + * @param cb The closure called with the notification the action was performed on. + * + * @returns A promise resolving to a listener that can be used to stop listening for the event. + * + * @since 2.0.0 + */ async function onAction( cb: (notification: Options) => void ): Promise { diff --git a/plugins/notification/guest-js/init.ts b/plugins/notification/guest-js/init.ts index 42d65fd99..9e8933e48 100644 --- a/plugins/notification/guest-js/init.ts +++ b/plugins/notification/guest-js/init.ts @@ -2,6 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Webview script injected by the notification plugin. + * + * It replaces `window.Notification` with an implementation backed by the plugin, so the + * standard Notification Web API sends OS notifications, and keeps + * `window.Notification.permission` and `window.Notification.requestPermission` in sync + * with the permission state reported by the plugin. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' import type { PermissionState } from '@tauri-apps/api/core' import type { Options } from './index' diff --git a/plugins/notification/src/desktop.rs b/plugins/notification/src/desktop.rs index 4ceb83088..5b41a6c4b 100644 --- a/plugins/notification/src/desktop.rs +++ b/plugins/notification/src/desktop.rs @@ -10,6 +10,7 @@ use tauri::{ use crate::NotificationBuilder; +/// Initializes the desktop implementation of the notification APIs. pub fn init( app: &AppHandle, _api: PluginApi, @@ -23,6 +24,22 @@ pub fn init( pub struct Notification(AppHandle); impl crate::NotificationBuilder { + /// Shows the notification. + /// + /// When no title was set with [`Self::title`], the `productName` from the Tauri configuration is used instead. + /// Only the title, body, icon and sound of the notification are used on desktop; + /// the scheduling, grouping and action related options are ignored. + /// + /// The notification is dispatched on a background task, so this returns as soon as the payload is prepared. + /// + /// # Errors + /// + /// Returns an error when the notification could not be prepared, + /// e.g. when the path of the running executable cannot be resolved on Windows. + /// + /// ## Platform-specific + /// + /// - **Windows**: Not supported on Windows 7 unless the `windows7-compat` Cargo feature is enabled. pub fn show(self) -> crate::Result<()> { let mut notification = imp::Notification::new(self.app.config().identifier.clone()); @@ -54,14 +71,38 @@ impl crate::NotificationBuilder { } impl Notification { + /// Creates a new builder for a notification. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_notification::NotificationExt; + /// + /// fn notify(app: &tauri::AppHandle) { + /// app.notification() + /// .builder() + /// .title("Tauri") + /// .body("Tauri is awesome!") + /// .show() + /// .unwrap(); + /// } + /// ``` pub fn builder(&self) -> NotificationBuilder { NotificationBuilder::new(self.0.clone()) } + /// Requests the permission to send notifications. + /// + /// Desktop applications do not need to ask for this permission, + /// so this always resolves to [`PermissionState::Granted`] without prompting the user. pub fn request_permission(&self) -> crate::Result { Ok(PermissionState::Granted) } + /// Checks whether the permission to send notifications was granted. + /// + /// Desktop applications do not need to ask for this permission, + /// so this always resolves to [`PermissionState::Granted`]. pub fn permission_state(&self) -> crate::Result { Ok(PermissionState::Granted) } diff --git a/plugins/notification/src/error.rs b/plugins/notification/src/error.rs index 339e763b1..f43da5823 100644 --- a/plugins/notification/src/error.rs +++ b/plugins/notification/src/error.rs @@ -4,12 +4,20 @@ use serde::{ser::Serializer, Serialize}; +/// Alias for a [`std::result::Result`] with the error type set to [`Error`]. pub type Result = std::result::Result; +/// Errors returned by the notification plugin. +/// +/// The error is serialized to its [`Display`](std::fmt::Display) string when it crosses the IPC boundary. #[derive(Debug, thiserror::Error)] pub enum Error { + /// An I/O operation failed, e.g. resolving the path of the running executable on Windows. #[error(transparent)] Io(#[from] std::io::Error), + /// Failed to run a command on the mobile plugin implementation (Kotlin on Android, Swift on iOS). + /// + /// Only available on mobile. #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), diff --git a/plugins/notification/src/lib.rs b/plugins/notification/src/lib.rs index 7a0062bac..32c2e61ec 100644 --- a/plugins/notification/src/lib.rs +++ b/plugins/notification/src/lib.rs @@ -75,10 +75,13 @@ impl NotificationBuilder { self } - /// Identifier of the {@link Channel} that deliveres this notification. + /// Sets the identifier of the notification channel that delivers this notification. /// /// If the channel does not exist, the notification won't fire. - /// Make sure the channel exists with {@link listChannels} and {@link createChannel}. + /// Make sure the channel exists with `Notification::list_channels` and + /// `Notification::create_channel`. + /// + /// Only used on Android. pub fn channel_id(mut self, id: impl Into) -> Self { self.data.channel_id.replace(id.into()); self @@ -213,6 +216,22 @@ impl NotificationBuilder { /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the notification APIs. pub trait NotificationExt { + /// Returns the notification APIs managed by the plugin. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_notification::NotificationExt; + /// + /// fn notify(app: &tauri::AppHandle) { + /// app.notification() + /// .builder() + /// .title("Tauri") + /// .body("Tauri is awesome!") + /// .show() + /// .unwrap(); + /// } + /// ``` fn notification(&self) -> &Notification; } diff --git a/plugins/notification/src/mobile.rs b/plugins/notification/src/mobile.rs index edfef728a..250532d4a 100644 --- a/plugins/notification/src/mobile.rs +++ b/plugins/notification/src/mobile.rs @@ -18,7 +18,8 @@ const PLUGIN_IDENTIFIER: &str = "app.tauri.notification"; #[cfg(target_os = "ios")] tauri::ios_plugin_binding!(init_plugin_notification); -// initializes the Kotlin or Swift plugin classes +/// Initializes the mobile implementation of the notification APIs by registering +/// the Kotlin (Android) or Swift (iOS) plugin class. pub fn init( _app: &AppHandle, api: PluginApi, @@ -31,6 +32,12 @@ pub fn init( } impl crate::NotificationBuilder { + /// Shows the notification, or schedules it when [`Self::schedule`] was called. + /// + /// # Errors + /// + /// Returns [`Error::PluginInvoke`](crate::Error::PluginInvoke) when the mobile plugin + /// rejects the notification, e.g. when the scheduled date is in the past. pub fn show(self) -> crate::Result<()> { self.handle .run_mobile_plugin::("show", self.data) @@ -45,10 +52,29 @@ impl crate::NotificationBuilder { pub struct Notification(PluginHandle); impl Notification { + /// Creates a new builder for a notification. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_notification::NotificationExt; + /// + /// fn notify(app: &tauri::AppHandle) { + /// app.notification() + /// .builder() + /// .title("Tauri") + /// .body("Tauri is awesome!") + /// .show() + /// .unwrap(); + /// } + /// ``` pub fn builder(&self) -> crate::NotificationBuilder { crate::NotificationBuilder::new(self.0.clone()) } + /// Requests the permission to send notifications, prompting the user when it was not decided yet. + /// + /// On Android this requests the `POST_NOTIFICATIONS` runtime permission. pub fn request_permission(&self) -> crate::Result { self.0 .run_mobile_plugin::("requestPermissions", ()) @@ -56,6 +82,7 @@ impl Notification { .map_err(Into::into) } + /// Checks the current state of the permission to send notifications without prompting the user. pub fn permission_state(&self) -> crate::Result { self.0 .run_mobile_plugin::("checkPermissions", ()) @@ -63,6 +90,13 @@ impl Notification { .map_err(Into::into) } + /// Registers the action types a notification can reference + /// through [`NotificationBuilder::action_type_id`](crate::NotificationBuilder::action_type_id). + /// + /// ## Platform-specific + /// + /// - **Android**: only the identifier, title and input flag of each [`Action`] are used. + /// - **iOS**: each action type is registered as a `UNNotificationCategory`. pub fn register_action_types(&self, types: Vec) -> crate::Result<()> { let mut args = HashMap::new(); args.insert("types", types); @@ -71,6 +105,9 @@ impl Notification { .map_err(Into::into) } + /// Removes the delivered notifications with the given identifiers from the notification center. + /// + /// Use [`Self::remove_all_active`] to remove every delivered notification. pub fn remove_active(&self, notifications: Vec) -> crate::Result<()> { let mut args = HashMap::new(); args.insert( @@ -89,18 +126,21 @@ impl Notification { .map_err(Into::into) } + /// Lists the notifications that were delivered and are still visible in the notification center. pub fn active(&self) -> crate::Result> { self.0 .run_mobile_plugin("getActive", ()) .map_err(Into::into) } + /// Removes all delivered notifications from the notification center. pub fn remove_all_active(&self) -> crate::Result<()> { self.0 .run_mobile_plugin("removeActive", ()) .map_err(Into::into) } + /// Lists the scheduled notifications that have not been delivered yet. pub fn pending(&self) -> crate::Result> { self.0 .run_mobile_plugin("getPending", ()) @@ -119,6 +159,12 @@ impl Notification { self.0.run_mobile_plugin("cancel", ()).map_err(Into::into) } + /// Creates a notification channel, which notifications can target + /// through [`NotificationBuilder::channel_id`](crate::NotificationBuilder::channel_id). + /// + /// Notifications that reference a channel that does not exist are not delivered. + /// + /// Only available on Android. #[cfg(target_os = "android")] pub fn create_channel(&self, channel: Channel) -> crate::Result<()> { self.0 @@ -126,6 +172,9 @@ impl Notification { .map_err(Into::into) } + /// Deletes the notification channel with the given identifier. + /// + /// Only available on Android. #[cfg(target_os = "android")] pub fn delete_channel(&self, id: impl Into) -> crate::Result<()> { let mut args = HashMap::new(); @@ -135,6 +184,9 @@ impl Notification { .map_err(Into::into) } + /// Lists the notification channels that are currently registered for the app. + /// + /// Only available on Android. #[cfg(target_os = "android")] pub fn list_channels(&self) -> crate::Result> { self.0 diff --git a/plugins/notification/src/models.rs b/plugins/notification/src/models.rs index e93b1e078..ce5993a0f 100644 --- a/plugins/notification/src/models.rs +++ b/plugins/notification/src/models.rs @@ -8,6 +8,9 @@ use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializ use url::Url; +/// A media file attached to a notification. +/// +/// Attachments are only used on mobile; desktop notifications ignore them. #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Attachment { @@ -16,32 +19,65 @@ pub struct Attachment { } impl Attachment { + /// Creates a new attachment with the given identifier and URL. + /// + /// The URL accepts the `asset` and `file` protocols. pub fn new(id: impl Into, url: Url) -> Self { Self { id: id.into(), url } } } +/// The set of date fields a notification must match to be delivered. +/// +/// Fields left as [`None`] match any value, so the notification fires on every date +/// whose remaining components match. Used by [`Schedule::Interval`]. #[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ScheduleInterval { + /// The year the notification fires on. pub year: Option, + /// The month of the year the notification fires on. pub month: Option, + /// The day of the month the notification fires on. pub day: Option, + /// The day of the week the notification fires on. + /// + /// 1 - Sunday, 2 - Monday, 3 - Tuesday, 4 - Wednesday, 5 - Thursday, 6 - Friday, 7 - Saturday. pub weekday: Option, + /// The hour of the day the notification fires on, in the 24-hour clock. pub hour: Option, + /// The minute of the hour the notification fires on. pub minute: Option, + /// The second of the minute the notification fires on. pub second: Option, } +/// The unit of the repeating interval used by [`Schedule::Every`]. +/// +/// It is serialized as its lowercase camelCase name, e.g. `twoWeeks`. #[derive(Debug)] pub enum ScheduleEvery { + /// Repeats every year. + /// + /// On Android a year is approximated as 52 weeks. Year, + /// Repeats every month. + /// + /// On Android a month is approximated as 30 days. Month, + /// Repeats every two weeks. TwoWeeks, + /// Repeats every week. Week, + /// Repeats every day. Day, + /// Repeats every hour. Hour, + /// Repeats every minute. Minute, + /// Repeats every second. + /// + /// Not supported on iOS, where repeating triggers must be at least a minute apart. Second, } @@ -93,31 +129,58 @@ impl<'de> Deserialize<'de> for ScheduleEvery { } } +/// Defines when a notification is delivered. +/// +/// Scheduling is only implemented on mobile; the desktop implementation delivers the +/// notification immediately and ignores the schedule. #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Schedule { + /// Fires at a specific date and time, which must be in the future. #[serde(rename_all = "camelCase")] At { + /// The date and time the notification fires at, serialized as an ISO-8601 string. #[serde( serialize_with = "iso8601::serialize", deserialize_with = "time::serde::iso8601::deserialize" )] date: time::OffsetDateTime, + /// Whether the notification keeps repeating, using the duration between the moment it is + /// scheduled and `date` as the interval. Defaults to `false`. + /// + /// The interval must be at least one minute on iOS. #[serde(default)] repeating: bool, + /// Whether the notification is allowed to fire while the device is in low-power idle + /// (Doze) mode. Defaults to `false`. + /// + /// Only used on Android. #[serde(default)] allow_while_idle: bool, }, + /// Fires whenever the current date matches every field set on the given interval. #[serde(rename_all = "camelCase")] Interval { + /// The date fields the current date must match for the notification to fire. interval: ScheduleInterval, + /// Whether the notification is allowed to fire while the device is in low-power idle + /// (Doze) mode. Defaults to `false`. + /// + /// Only used on Android. #[serde(default)] allow_while_idle: bool, }, + /// Fires repeatedly, once every `count` times the given interval unit. #[serde(rename_all = "camelCase")] Every { + /// The unit of the repeating interval. interval: ScheduleEvery, + /// How many interval units elapse between each notification. count: u8, + /// Whether the notification is allowed to fire while the device is in low-power idle + /// (Doze) mode. Defaults to `false`. + /// + /// Only used on Android. #[serde(default)] allow_while_idle: bool, }, @@ -145,6 +208,10 @@ mod iso8601 { } } +/// The payload of a notification, as sent to the platform implementation. +/// +/// Build it with [`NotificationBuilder`](crate::NotificationBuilder) rather than constructing it directly. +/// The identifier defaults to a random 32-bit integer when it is not provided. #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotificationData { @@ -209,6 +276,9 @@ impl Default for NotificationData { } } +/// A notification that was scheduled and has not been delivered yet. +/// +/// Returned by `Notification::pending`, which is only available on mobile. #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PendingNotification { @@ -219,23 +289,32 @@ pub struct PendingNotification { } impl PendingNotification { + /// The notification identifier. pub fn id(&self) -> i32 { self.id } + /// The notification title, if it was set. pub fn title(&self) -> Option<&str> { self.title.as_deref() } + /// The notification body, if it was set. pub fn body(&self) -> Option<&str> { self.body.as_deref() } + /// The schedule that determines when the notification is delivered. pub fn schedule(&self) -> &Schedule { &self.schedule } } +/// A notification that was delivered and is still visible in the notification center. +/// +/// Returned by `Notification::active`, which is only available on mobile. +/// Which fields are populated depends on the platform, since Android and iOS expose +/// different information about delivered notifications. #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ActiveNotification { @@ -258,55 +337,88 @@ pub struct ActiveNotification { } impl ActiveNotification { + /// The notification identifier. pub fn id(&self) -> i32 { self.id } + /// The tag the notification was posted with. + /// + /// Only set on Android. pub fn tag(&self) -> Option<&str> { self.tag.as_deref() } + /// The notification title, if it was set. pub fn title(&self) -> Option<&str> { self.title.as_deref() } + /// The notification body, if it was set. pub fn body(&self) -> Option<&str> { self.body.as_deref() } + /// The identifier of the group the notification belongs to. + /// + /// Only set on Android. pub fn group(&self) -> Option<&str> { self.group.as_deref() } + /// Whether the notification is the summary of its group. + /// + /// Only set on Android. Defaults to `false`. pub fn group_summary(&self) -> bool { self.group_summary } + /// The platform extras attached to the notification, as string values. + /// + /// Only set on Android, where it holds the `android.app.Notification` extras bundle. pub fn data(&self) -> &HashMap { &self.data } + /// The extra payload that was stored in the notification. pub fn extra(&self) -> &HashMap { &self.extra } + /// The attachments of the notification. + /// + /// Only set on iOS. pub fn attachments(&self) -> &[Attachment] { &self.attachments } + /// The identifier of the action type the notification was registered with. + /// + /// Only set on iOS. pub fn action_type_id(&self) -> Option<&str> { self.action_type_id.as_deref() } + /// The schedule the notification was delivered with, if it was scheduled. pub fn schedule(&self) -> Option<&Schedule> { self.schedule.as_ref() } + /// The sound resource name of the notification. + /// + /// Only set on iOS. pub fn sound(&self) -> Option<&str> { self.sound.as_deref() } } +/// A group of [`Action`]s a notification can display, referenced by +/// [`NotificationBuilder::action_type_id`](crate::NotificationBuilder::action_type_id). +/// +/// Register it with `Notification::register_action_types` before sending a notification that uses it. +/// It maps to a `UNNotificationCategory` on iOS and to an action group on Android. +/// +/// Only available on mobile. Use [`ActionType::builder`] to construct one. #[cfg(mobile)] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -320,12 +432,19 @@ pub struct ActionType { hidden_previews_show_subtitle: bool, } +/// Builder for an [`ActionType`], created with [`ActionType::builder`]. +/// +/// Only available on mobile. #[cfg(mobile)] #[derive(Debug)] pub struct ActionTypeBuilder(ActionType); #[cfg(mobile)] impl ActionType { + /// Creates a builder for an action type with the given identifier. + /// + /// All the optional settings default to `false` or [`None`]; + /// call [`ActionTypeBuilder::build`] to get the [`ActionType`]. pub fn builder(id: impl Into) -> ActionTypeBuilder { ActionTypeBuilder(Self { id: id.into(), @@ -338,30 +457,47 @@ impl ActionType { }) } + /// The identifier of this action type. pub fn id(&self) -> &str { &self.id } + /// The actions associated with this action type. pub fn actions(&self) -> &[Action] { &self.actions } + /// The placeholder shown instead of the notification body when previews are hidden. + /// + /// Only used on iOS. pub fn hidden_previews_body_placeholder(&self) -> Option<&str> { self.hidden_previews_body_placeholder.as_deref() } + /// Whether the app is notified when the user dismisses the notification. + /// + /// Only used on iOS. pub fn custom_dismiss_action(&self) -> bool { self.custom_dismiss_action } + /// Whether the notification can be displayed in a CarPlay environment. + /// + /// Only used on iOS. pub fn allow_in_car_play(&self) -> bool { self.allow_in_car_play } + /// Whether the notification title is shown even when previews are hidden. + /// + /// Only used on iOS. pub fn hidden_previews_show_title(&self) -> bool { self.hidden_previews_show_title } + /// Whether the notification subtitle is shown even when previews are hidden. + /// + /// Only used on iOS. pub fn hidden_previews_show_subtitle(&self) -> bool { self.hidden_previews_show_subtitle } @@ -369,11 +505,15 @@ impl ActionType { #[cfg(mobile)] impl ActionTypeBuilder { + /// Sets the actions associated with this action type. pub fn actions(mut self, actions: Vec) -> Self { self.0.actions = actions; self } + /// Sets the placeholder shown instead of the notification body when previews are hidden. + /// + /// Only used on iOS. pub fn hidden_previews_body_placeholder( mut self, hidden_previews_body_placeholder: impl Into, @@ -384,31 +524,50 @@ impl ActionTypeBuilder { self } + /// Sets whether the app is notified when the user dismisses the notification. + /// + /// Only used on iOS. pub fn custom_dismiss_action(mut self, custom_dismiss_action: bool) -> Self { self.0.custom_dismiss_action = custom_dismiss_action; self } + /// Sets whether the notification can be displayed in a CarPlay environment. + /// + /// Only used on iOS. pub fn allow_in_car_play(mut self, allow_in_car_play: bool) -> Self { self.0.allow_in_car_play = allow_in_car_play; self } + /// Sets whether the notification title is shown even when previews are hidden. + /// + /// Only used on iOS. pub fn hidden_previews_show_title(mut self, hidden_previews_show_title: bool) -> Self { self.0.hidden_previews_show_title = hidden_previews_show_title; self } + /// Sets whether the notification subtitle is shown even when previews are hidden. + /// + /// Only used on iOS. pub fn hidden_previews_show_subtitle(mut self, hidden_previews_show_subtitle: bool) -> Self { self.0.hidden_previews_show_subtitle = hidden_previews_show_subtitle; self } + /// Builds the [`ActionType`]. pub fn build(self) -> ActionType { self.0 } } +/// A button the user can tap on a notification, belonging to an [`ActionType`]. +/// +/// It maps to a `UNNotificationAction` on iOS. On Android only the identifier, the title +/// and the input flag are used. +/// +/// Only available on mobile. Use [`Action::builder`] to construct one. #[cfg(mobile)] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -423,12 +582,19 @@ pub struct Action { input_placeholder: Option, } +/// Builder for an [`Action`], created with [`Action::builder`]. +/// +/// Only available on mobile. #[cfg(mobile)] #[derive(Debug)] pub struct ActionBuilder(Action); #[cfg(mobile)] impl Action { + /// Creates a builder for an action with the given identifier and button title. + /// + /// All the optional settings default to `false` or [`None`]; + /// call [`ActionBuilder::build`] to get the [`Action`]. pub fn builder(id: impl Into, title: impl Into) -> ActionBuilder { ActionBuilder(Self { id: id.into(), @@ -442,34 +608,52 @@ impl Action { }) } + /// The identifier of this action, reported back when the user triggers it. pub fn id(&self) -> &str { &self.id } + /// The text displayed on the action button. pub fn title(&self) -> &str { &self.title } + /// Whether the device must be unlocked for the action to run. + /// + /// Only used on iOS. pub fn requires_authentication(&self) -> bool { self.requires_authentication } + /// Whether the app is brought to the foreground when the action is triggered. + /// + /// Only used on iOS. pub fn foreground(&self) -> bool { self.foreground } + /// Whether the action is displayed as destructive, usually in red. + /// + /// Only used on iOS. pub fn destructive(&self) -> bool { self.destructive } + /// Whether triggering the action lets the user type a text response. pub fn input(&self) -> bool { self.input } + /// The text displayed on the button that submits the text input. + /// + /// Only used on iOS. pub fn input_button_title(&self) -> Option<&str> { self.input_button_title.as_deref() } + /// The placeholder displayed on the empty text input field. + /// + /// Only used on iOS. pub fn input_placeholder(&self) -> Option<&str> { self.input_placeholder.as_deref() } @@ -477,36 +661,53 @@ impl Action { #[cfg(mobile)] impl ActionBuilder { + /// Sets whether the device must be unlocked for the action to run. + /// + /// Only used on iOS. pub fn requires_authentication(mut self, requires_authentication: bool) -> Self { self.0.requires_authentication = requires_authentication; self } + /// Sets whether the app is brought to the foreground when the action is triggered. + /// + /// Only used on iOS. pub fn foreground(mut self, foreground: bool) -> Self { self.0.foreground = foreground; self } + /// Sets whether the action is displayed as destructive, usually in red. + /// + /// Only used on iOS. pub fn destructive(mut self, destructive: bool) -> Self { self.0.destructive = destructive; self } + /// Sets whether triggering the action lets the user type a text response. pub fn input(mut self, input: bool) -> Self { self.0.input = input; self } + /// Sets the text displayed on the button that submits the text input. + /// + /// Only used on iOS. pub fn input_button_title(mut self, input_button_title: impl Into) -> Self { self.0.input_button_title.replace(input_button_title.into()); self } + /// Sets the placeholder displayed on the empty text input field. + /// + /// Only used on iOS. pub fn input_placeholder(mut self, input_placeholder: impl Into) -> Self { self.0.input_placeholder.replace(input_placeholder.into()); self } + /// Builds the [`Action`]. pub fn build(self) -> Action { self.0 } @@ -520,13 +721,24 @@ mod android { use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; + /// How much the notifications of a [`Channel`] interrupt the user. + /// + /// It maps to the `NotificationManager.IMPORTANCE_*` constants and is serialized as its + /// integer value. Only available on Android. #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)] #[repr(u8)] pub enum Importance { + /// The notifications are not shown. None = 0, + /// The notifications are only shown in the shade, below the fold, without a status bar icon. Min = 1, + /// The notifications are shown without a sound. Low = 2, + /// The notifications are shown and make a sound. + /// + /// This is the value used when the channel does not define an importance. Default = 3, + /// The notifications are shown, make a sound and pop up as a heads-up notification. High = 4, } @@ -536,14 +748,29 @@ mod android { } } + /// How much of a notification is shown on the lock screen. + /// + /// It maps to the `Notification.VISIBILITY_*` constants and is serialized as its + /// integer value. Only available on Android. #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)] #[repr(i8)] pub enum Visibility { + /// The notification is not shown on the lock screen at all. Secret = -1, + /// The notification is shown on the lock screen with its sensitive content hidden. + /// + /// This is the value used when the channel does not define a visibility. Private = 0, + /// The notification is shown in full on the lock screen. Public = 1, } + /// A notification channel, the category users configure notification behavior on. + /// + /// Notifications reference a channel through + /// [`NotificationBuilder::channel_id`](crate::NotificationBuilder::channel_id) and are not + /// delivered when the channel does not exist. Only available on Android. + /// Use [`Channel::builder`] to construct one. #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Channel { @@ -558,10 +785,18 @@ mod android { visibility: Option, } + /// Builder for a [`Channel`], created with [`Channel::builder`]. + /// + /// Only available on Android. #[derive(Debug)] pub struct ChannelBuilder(Channel); impl Channel { + /// Creates a builder for a channel with the given identifier and user visible name. + /// + /// Lights and vibration are disabled, the importance defaults to [`Importance::Default`] + /// and the remaining settings default to [`None`]; + /// call [`ChannelBuilder::build`] to get the [`Channel`]. pub fn builder(id: impl Into, name: impl Into) -> ChannelBuilder { ChannelBuilder(Self { id: id.into(), @@ -576,79 +811,102 @@ mod android { }) } + /// The identifier of this channel. pub fn id(&self) -> &str { &self.id } + /// The user visible name of this channel. pub fn name(&self) -> &str { &self.name } + /// The user visible description of this channel. pub fn description(&self) -> Option<&str> { self.description.as_deref() } + /// The name of the sound resource played by the notifications of this channel. + /// + /// The resource must be placed in the app's `res/raw` folder. pub fn sound(&self) -> Option<&str> { self.sound.as_deref() } + /// Whether the notifications of this channel blink the device light. pub fn lights(&self) -> bool { self.lights } + /// The color of the device light, as a color string such as `#ff0000`. pub fn light_color(&self) -> Option<&str> { self.light_color.as_deref() } + /// Whether the notifications of this channel vibrate the device. pub fn vibration(&self) -> bool { self.vibration } + /// How much the notifications of this channel interrupt the user. pub fn importance(&self) -> Importance { self.importance } + /// How much of the notifications of this channel is shown on the lock screen. + /// + /// [`Visibility::Private`] is used when this is [`None`]. pub fn visibility(&self) -> Option { self.visibility } } impl ChannelBuilder { + /// Sets the user visible description of the channel. pub fn description(mut self, description: impl Into) -> Self { self.0.description.replace(description.into()); self } + /// Sets the name of the sound resource played by the notifications of this channel. + /// + /// The resource must be placed in the app's `res/raw` folder. pub fn sound(mut self, sound: impl Into) -> Self { self.0.sound.replace(sound.into()); self } + /// Sets whether the notifications of this channel blink the device light. pub fn lights(mut self, lights: bool) -> Self { self.0.lights = lights; self } + /// Sets the color of the device light, as a color string such as `#ff0000`. pub fn light_color(mut self, color: impl Into) -> Self { self.0.light_color.replace(color.into()); self } + /// Sets whether the notifications of this channel vibrate the device. pub fn vibration(mut self, vibration: bool) -> Self { self.0.vibration = vibration; self } + /// Sets how much the notifications of this channel interrupt the user. pub fn importance(mut self, importance: Importance) -> Self { self.0.importance = importance; self } + /// Sets how much of the notifications of this channel is shown on the lock screen. pub fn visibility(mut self, visibility: Visibility) -> Self { self.0.visibility.replace(visibility); self } + /// Builds the [`Channel`]. pub fn build(self) -> Channel { self.0 } diff --git a/plugins/opener/guest-js/init.ts b/plugins/opener/guest-js/init.ts index 4c2b631fb..d01238625 100644 --- a/plugins/opener/guest-js/init.ts +++ b/plugins/opener/guest-js/init.ts @@ -2,6 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Webview script injected by the opener plugin on every page load. + * + * It installs a `click` listener on `window` that intercepts clicks on `` elements whose + * `target` is `_blank` (or that are clicked while holding `Ctrl` or `Shift`) and whose `href` + * uses the `http:`, `https:`, `mailto:` or `tel:` protocol, cancels the navigation and opens the + * link with the system's default browser through the `plugin:opener|open_url` command instead. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' // open links with the API diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 23cd2a8aa..05642fb3f 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -6,37 +6,61 @@ use std::path::PathBuf; use serde::{Serialize, Serializer}; +/// The error type returned by the opener plugin's commands and APIs. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { + /// Forwarded from a failed call into the mobile plugin runtime. #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + /// Forwarded from a [`tauri::Error`], e.g. when resolving a scoped path fails. #[error(transparent)] Tauri(#[from] tauri::Error), + /// Forwarded from an [`std::io::Error`] raised while opening or revealing a path. #[error(transparent)] Io(#[from] std::io::Error), + /// Forwarded from a [`serde_json::Error`] raised while (de)serializing a mobile plugin payload. #[error(transparent)] Json(#[from] serde_json::Error), + /// The program passed as `with` is not a program name known by the underlying opener. #[error("unknown program {0}")] UnknownProgramName(String), + /// The path is not allowed by the opener scope, optionally together with the program it was requested to be opened with. #[error("Not allowed to open path {}{}", .path, .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] - ForbiddenPath { path: String, with: Option }, + ForbiddenPath { + /// The path that was rejected by the scope. + path: String, + /// The program the path was requested to be opened with, if any. + with: Option, + }, + /// The URL is not allowed by the opener scope, optionally together with the program it was requested to be opened with. #[error("Not allowed to open url {}{}", .url, .with.as_ref().map(|w| format!(" with {w}")).unwrap_or_default())] - ForbiddenUrl { url: String, with: Option }, + ForbiddenUrl { + /// The URL that was rejected by the scope. + url: String, + /// The program the URL was requested to be opened with, if any. + with: Option, + }, + /// The requested API is not supported on the current platform, e.g. [`crate::reveal_item_in_dir`] on Android and iOS. #[error("API not supported on the current platform")] UnsupportedPlatform, + /// Forwarded from a Win32 API call, see [`windows::core::Error`]. #[error(transparent)] #[cfg(windows)] Win32Error(#[from] windows::core::Error), + /// The given path has no parent directory, so it cannot be revealed in its containing folder. #[error("Path doesn't have a parent: {0}")] NoParent(PathBuf), // TODO: Add the underlying io::Error to this variant + /// Failed to convert the path to a Windows `ITEMIDLIST` while preparing it to be revealed in the file explorer. #[cfg(windows)] #[error("Failed to convert path '{0}' to ITEMIDLIST")] FailedToConvertPathToItemIdList(PathBuf), + /// Failed to convert the path to a `file://` URL, which is required to reveal it via D-Bus on Linux and BSD. #[error("Failed to convert path to file:// url")] FailedToConvertPathToFileUrl, + /// Forwarded from a [`zbus::Error`] raised while talking to the file manager or the desktop portal over D-Bus. #[error(transparent)] #[cfg(any( target_os = "linux", diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 622a65097..13f6efb85 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -2,6 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! Open files and URLs using their default application, and reveal files in the system's file explorer. +//! +//! Use the [`OpenerExt`] trait to access [`Opener::open_url`] and [`Opener::open_path`] from a +//! running Tauri app; the plugin's `open_url` and `open_path` commands enforce the scope +//! configured for the plugin before delegating to them. The [`open_url`] and [`open_path`] free +//! functions and the [`Opener`] methods themselves do not perform any scope check. +//! [`reveal_item_in_dir`] and [`reveal_items_in_dir`] return [`Error::UnsupportedPlatform`] on +//! Android and iOS. + use std::path::Path; use tauri::{plugin::TauriPlugin, Manager, Runtime}; @@ -29,6 +38,9 @@ type Result = std::result::Result; pub use open::{open_path, open_url}; pub use reveal_item_in_dir::{reveal_item_in_dir, reveal_items_in_dir}; +/// Access to the opener APIs, managed by the plugin as app state. +/// +/// Obtain an instance via [`OpenerExt::opener`]. pub struct Opener { // we use `fn() -> R` to silence the unused generic error // while keeping this struct `Send + Sync` without requiring `R` to be @@ -153,10 +165,20 @@ impl Opener { .map_err(Into::into) } + /// Reveal a path in the system's default explorer. See [`reveal_item_in_dir`] for details. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported, returns [`Error::UnsupportedPlatform`]. pub fn reveal_item_in_dir>(&self, p: P) -> Result<()> { reveal_item_in_dir(p) } + /// Reveal multiple paths in the system's default explorer. See [`reveal_items_in_dir`] for details. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported, returns [`Error::UnsupportedPlatform`]. pub fn reveal_items_in_dir(&self, paths: I) -> Result<()> where I: IntoIterator, @@ -168,6 +190,7 @@ impl Opener { /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the opener APIs. pub trait OpenerExt { + /// Returns the [`Opener`] instance managed by the plugin. fn opener(&self) -> &Opener; } diff --git a/plugins/os/guest-js/index.ts b/plugins/os/guest-js/index.ts index 697ae8ed3..21a5777b2 100644 --- a/plugins/os/guest-js/index.ts +++ b/plugins/os/guest-js/index.ts @@ -25,6 +25,9 @@ declare global { } } +/** + * A string describing the specific operating system in use, as returned by {@link platform}. + */ type Platform = | 'linux' | 'macos' @@ -37,8 +40,14 @@ type Platform = | 'android' | 'windows' +/** + * A coarse-grained operating system category, as returned by {@link type}. + */ type OsType = 'linux' | 'windows' | 'macos' | 'ios' | 'android' +/** + * A string describing the specific operating system architecture in use, as returned by {@link arch}. + */ type Arch = | 'x86' | 'x86_64' @@ -57,6 +66,13 @@ type Arch = * - `\n` on POSIX * - `\r\n` on Windows * + * @example + * ```typescript + * import { eol } from '@tauri-apps/plugin-os'; + * const eolChar = eol(); + * ``` + * + * @returns The end-of-line marker for the current platform. * @since 2.0.0 * */ function eol(): string { @@ -73,6 +89,7 @@ function eol(): string { * const platformName = platform(); * ``` * + * @returns The platform name. * @since 2.0.0 * */ @@ -88,12 +105,16 @@ function platform(): Platform { * const osVersion = version(); * ``` * + * @returns The operating system version. * @since 2.0.0 */ function version(): string { return window.__TAURI_OS_PLUGIN_INTERNALS__.version } +/** + * A string describing the operating system family, as returned by {@link family}. + */ type Family = 'unix' | 'windows' /** @@ -104,6 +125,7 @@ type Family = 'unix' | 'windows' * const family = family(); * ``` * + * @returns The operating system family. * @since 2.0.0 */ function family(): Family { @@ -118,6 +140,7 @@ function family(): Family { * const osType = type(); * ``` * + * @returns The operating system type. * @since 2.0.0 */ function type(): OsType { @@ -133,6 +156,7 @@ function type(): OsType { * const archName = arch(); * ``` * + * @returns The operating system architecture. * @since 2.0.0 */ function arch(): Arch { @@ -147,6 +171,7 @@ function arch(): Arch { * const exeExt = exeExtension(); * ``` * + * @returns The file extension used for executable binaries on this platform. * @since 2.0.0 */ function exeExtension(): string { @@ -164,6 +189,7 @@ function exeExtension(): string { * } * ``` * + * @returns A promise resolving to the `BCP-47` language tag, or `null` if it could not be obtained. * @since 2.0.0 */ async function locale(): Promise { @@ -177,6 +203,9 @@ async function locale(): Promise { * import { hostname } from '@tauri-apps/plugin-os'; * const hostname = await hostname(); * ``` + * + * @returns A promise resolving to the host name of the operating system. + * @since 2.0.0 */ async function hostname(): Promise { return await invoke('plugin:os|hostname') diff --git a/plugins/os/src/error.rs b/plugins/os/src/error.rs index f5d8816af..7c752ba03 100644 --- a/plugins/os/src/error.rs +++ b/plugins/os/src/error.rs @@ -4,6 +4,9 @@ use serde::{Serialize, Serializer}; +/// All errors that can occur while running the os plugin. +/// +/// This enum currently has no variants: none of the plugin's commands can fail. #[derive(Debug, thiserror::Error)] pub enum Error {} diff --git a/plugins/os/src/lib.rs b/plugins/os/src/lib.rs index 50ab89eea..853dbdf69 100644 --- a/plugins/os/src/lib.rs +++ b/plugins/os/src/lib.rs @@ -23,11 +23,17 @@ mod error; pub use error::Error; +/// The type of the current operating system, as returned by [`type_`]. pub enum OsType { + /// Linux and Linux-based systems such as FreeBSD, DragonFly BSD, NetBSD and OpenBSD. Linux, + /// Windows. Windows, + /// macOS. Macos, + /// iOS. IOS, + /// Android. Android, } @@ -127,6 +133,7 @@ impl InitJavascript<'_> { } } +/// Initializes the plugin. pub fn init() -> TauriPlugin { let init_js = InitJavascript::new() .render_default(&Default::default()) diff --git a/plugins/persisted-scope/src/lib.rs b/plugins/persisted-scope/src/lib.rs index 602d90864..96331651e 100644 --- a/plugins/persisted-scope/src/lib.rs +++ b/plugins/persisted-scope/src/lib.rs @@ -163,6 +163,14 @@ fn save_scopes(scope: &tauri::fs::Scope, app_dir: &Path, scope_state_path: &Path }); } +/// Initializes the plugin. +/// +/// On setup, this restores the filesystem scope, and the `asset://` protocol scope when the +/// `protocol-asset` feature is enabled, from the state persisted during a previous run, then +/// listens for further scope changes to persist them again. Scopes are stored under the app's +/// data directory; nothing is restored or persisted if that directory cannot be resolved. The +/// `fs` plugin must be registered before this plugin, otherwise the filesystem scope is not +/// restored or persisted (a warning is printed in debug builds). pub fn init() -> TauriPlugin { Builder::new("persisted-scope") .setup(|app, _api| { diff --git a/plugins/positioner/guest-js/index.ts b/plugins/positioner/guest-js/index.ts index 74d0295ec..ba3548078 100644 --- a/plugins/positioner/guest-js/index.ts +++ b/plugins/positioner/guest-js/index.ts @@ -3,6 +3,12 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Move the window to well-known positions, including positions relative to the tray icon. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' import type { TrayIconEvent } from '@tauri-apps/api/tray' @@ -10,20 +16,53 @@ import type { TrayIconEvent } from '@tauri-apps/api/tray' * Well known window positions. */ export enum Position { + /** Top left corner of the screen. */ TopLeft = 0, + /** Top right corner of the screen. */ TopRight, + /** Bottom left corner of the screen. */ BottomLeft, + /** Bottom right corner of the screen. */ BottomRight, + /** Top center of the screen. */ TopCenter, + /** Bottom center of the screen. */ BottomCenter, + /** Vertically centered on the left edge of the screen. */ LeftCenter, + /** Vertically centered on the right edge of the screen. */ RightCenter, + /** Center of the screen. */ Center, + /** + * Above the tray icon, aligned with its left edge. Requires the `tray-icon` feature and the + * tray icon's events to be forwarded through {@link handleIconState}. + */ TrayLeft, + /** + * Directly below the tray icon, aligned with its left edge. Requires the `tray-icon` feature + * and the tray icon's events to be forwarded through {@link handleIconState}. + */ TrayBottomLeft, + /** + * Above the tray icon, aligned with its right edge. Requires the `tray-icon` feature and the + * tray icon's events to be forwarded through {@link handleIconState}. + */ TrayRight, + /** + * Directly below the tray icon, aligned with its right edge. Requires the `tray-icon` feature + * and the tray icon's events to be forwarded through {@link handleIconState}. + */ TrayBottomRight, + /** + * Above the tray icon, horizontally centered on it. Requires the `tray-icon` feature and the + * tray icon's events to be forwarded through {@link handleIconState}. + */ TrayCenter, + /** + * Directly below the tray icon, horizontally centered on it. Requires the `tray-icon` feature + * and the tray icon's events to be forwarded through {@link handleIconState}. + */ TrayBottomCenter } @@ -31,7 +70,15 @@ export enum Position { * Moves the `Window` to the given {@link Position} using `WindowExt.move_window()` * All positions are relative to the **current** screen. * + * @example + * ```typescript + * import { moveWindow, Position } from '@tauri-apps/plugin-positioner' + * + * await moveWindow(Position.TopRight) + * ``` + * * @param to The {@link Position} to move to. + * @since 2.0.0 */ export async function moveWindow(to: Position): Promise { await invoke('plugin:positioner|move_window', { @@ -44,7 +91,16 @@ export async function moveWindow(to: Position): Promise { * * This move operation constrains the window to the screen dimensions in case of * tray-icon positions. + * + * @example + * ```typescript + * import { moveWindowConstrained, Position } from '@tauri-apps/plugin-positioner' + * + * await moveWindowConstrained(Position.TrayCenter) + * ``` + * * @param to The (tray) {@link Position} to move to. + * @since 2.1.0 */ export async function moveWindowConstrained(to: Position): Promise { await invoke('plugin:positioner|move_window_constrained', { @@ -52,6 +108,30 @@ export async function moveWindowConstrained(to: Position): Promise { }) } +/** + * Reports the tray icon's current position and size so the `Tray*` {@link Position} variants + * can be resolved. + * + * This is an internal helper meant to be called from the tray icon's event handler (the + * `action` callback passed to `TrayIcon.new()`), forwarding every {@link TrayIconEvent} it + * receives so that {@link moveWindow} and {@link moveWindowConstrained} can later position the + * window relative to the tray icon. + * + * @example + * ```typescript + * import { handleIconState } from '@tauri-apps/plugin-positioner' + * import { TrayIcon, type TrayIconEvent } from '@tauri-apps/api/tray' + * + * const action = async (event: TrayIconEvent) => { + * await handleIconState(event) + * } + * + * const tray = await TrayIcon.new({ id: 'main', action }) + * ``` + * + * @param event The tray icon event to read the position and size from. + * @since 2.0.0 + */ export async function handleIconState(event: TrayIconEvent): Promise { await invoke('plugin:positioner|set_tray_icon_state', { position: event.rect.position, diff --git a/plugins/positioner/src/ext.rs b/plugins/positioner/src/ext.rs index f9d6a80d3..a27c18a96 100644 --- a/plugins/positioner/src/ext.rs +++ b/plugins/positioner/src/ext.rs @@ -13,28 +13,53 @@ use tauri::Monitor; use tauri::{PhysicalPosition, PhysicalSize, Result, Runtime, WebviewWindow, Window}; /// Well known window positions. +/// +/// The `Tray*` variants require the `tray-icon` feature and only resolve once the tray icon has +/// reported its position (see `on_tray_event`); using one before that happens returns an error. #[derive(Debug, Deserialize_repr)] #[repr(u16)] pub enum Position { + /// Top left corner of the current screen. TopLeft = 0, + /// Top right corner of the current screen. TopRight, + /// Bottom left corner of the current screen. BottomLeft, + /// Bottom right corner of the current screen. BottomRight, + /// Top center of the current screen. TopCenter, + /// Bottom center of the current screen. BottomCenter, + /// Vertically centered on the left edge of the current screen. LeftCenter, + /// Vertically centered on the right edge of the current screen. RightCenter, + /// Center of the current screen. Center, + /// Above the tray icon, aligning the window's left edge with the tray icon's left edge. On + /// Windows and macOS the window moves below the icon instead when there is not enough room + /// above. #[cfg(feature = "tray-icon")] TrayLeft, + /// Directly below the tray icon, aligning the window's left edge with the tray icon's left + /// edge. #[cfg(feature = "tray-icon")] TrayBottomLeft, + /// Above the tray icon, aligning the window's left edge with the tray icon's right edge. On + /// Windows and macOS the window moves below the icon instead when there is not enough room + /// above. #[cfg(feature = "tray-icon")] TrayRight, + /// Directly below the tray icon, aligning the window's left edge with the tray icon's right + /// edge. #[cfg(feature = "tray-icon")] TrayBottomRight, + /// Above the tray icon, horizontally centered on it. On Windows and macOS the window moves + /// below the icon instead when there is not enough room above. #[cfg(feature = "tray-icon")] TrayCenter, + /// Directly below the tray icon, horizontally centered on it. #[cfg(feature = "tray-icon")] TrayBottomCenter, } diff --git a/plugins/positioner/src/lib.rs b/plugins/positioner/src/lib.rs index 8d9594028..fd5e82638 100644 --- a/plugins/positioner/src/lib.rs +++ b/plugins/positioner/src/lib.rs @@ -31,6 +31,16 @@ use tauri::{tray::TrayIconEvent, AppHandle, Manager, PhysicalPosition, PhysicalS #[cfg(feature = "tray-icon")] struct Tray(std::sync::Mutex, PhysicalSize)>>); +/// Records the tray icon's latest position and size so that the `Tray*` [`Position`] variants +/// (e.g. [`Position::TrayLeft`]) can be resolved by [`WindowExt::move_window`] and +/// [`WindowExt::move_window_constrained`]. +/// +/// Call this from your tray icon's event handler. Only [`TrayIconEvent::Click`], +/// [`TrayIconEvent::Enter`], [`TrayIconEvent::Leave`] and [`TrayIconEvent::Move`] carry the +/// icon's position and update the tracked value; other events are ignored. Until this has been +/// called at least once, moving a window to a `Tray*` position fails. +/// +/// Requires the `tray-icon` feature. #[cfg(feature = "tray-icon")] pub fn on_tray_event(app: &AppHandle, event: &TrayIconEvent) { let (position, size) = { diff --git a/plugins/process/src/lib.rs b/plugins/process/src/lib.rs index b83d89648..d7f05f481 100644 --- a/plugins/process/src/lib.rs +++ b/plugins/process/src/lib.rs @@ -16,6 +16,7 @@ use tauri::{ mod commands; +/// Initializes the plugin, exposing commands to exit or restart the current process. pub fn init() -> TauriPlugin { Builder::new("process") .invoke_handler(tauri::generate_handler![commands::exit, commands::restart]) diff --git a/plugins/shell/guest-js/index.ts b/plugins/shell/guest-js/index.ts index 081d54c13..afac3026e 100644 --- a/plugins/shell/guest-js/index.ts +++ b/plugins/shell/guest-js/index.ts @@ -66,6 +66,8 @@ import { invoke, Channel } from '@tauri-apps/api/core' /** + * Options that configure how a child process is spawned. + * * @since 2.0.0 */ interface SpawnOptions { @@ -87,6 +89,8 @@ interface InternalSpawnOptions extends SpawnOptions { } /** + * The output collected from a child process that ran to completion. + * * @since 2.0.0 */ interface ChildProcess { @@ -101,6 +105,9 @@ interface ChildProcess { } /** + * A minimal event emitter modeled after Node.js' `EventEmitter`, used by + * {@link Command} and by its `stdout` and `stderr` streams. + * * @since 2.0.0 */ // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -113,6 +120,18 @@ class EventEmitter> { /** * Alias for `emitter.on(eventName, listener)`. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.addListener('error', (error) => console.error(error)); + * ``` + * + * @param eventName The name of the event to listen to. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * * @since 2.0.0 */ addListener( @@ -125,6 +144,20 @@ class EventEmitter> { /** * Alias for `emitter.off(eventName, listener)`. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * const listener = (error: string) => console.error(error); + * command.addListener('error', listener); + * command.removeListener('error', listener); + * ``` + * + * @param eventName The name of the event to stop listening to. + * @param listener The exact callback that was registered before. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * * @since 2.0.0 */ removeListener( @@ -142,6 +175,20 @@ class EventEmitter> { * * Returns a reference to the `EventEmitter`, so that calls can be chained. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.on('close', (data) => { + * console.log(`command finished with code ${data.code}`); + * }); + * ``` + * + * @param eventName The name of the event to listen to. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * * @since 2.0.0 */ on( @@ -164,6 +211,20 @@ class EventEmitter> { * * Returns a reference to the `EventEmitter`, so that calls can be chained. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.once('close', (data) => { + * console.log(`command finished with code ${data.code}`); + * }); + * ``` + * + * @param eventName The name of the event to listen to once. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * * @since 2.0.0 */ once( @@ -181,6 +242,20 @@ class EventEmitter> { * Removes the all specified listener from the listener array for the event eventName * Returns a reference to the `EventEmitter`, so that calls can be chained. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * const listener = (error: string) => console.error(error); + * command.on('error', listener); + * command.off('error', listener); + * ``` + * + * @param eventName The name of the event to stop listening to. + * @param listener The exact callback that was registered before. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * * @since 2.0.0 */ off( @@ -201,6 +276,19 @@ class EventEmitter> { * * Returns a reference to the `EventEmitter`, so that calls can be chained. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.on('error', (error) => console.error(error)); + * command.removeAllListeners('error'); + * ``` + * + * @param event The name of the event to remove the listeners of. + * When omitted, the listeners of every event are removed. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * * @since 2.0.0 */ removeAllListeners(event?: N): this { @@ -215,10 +303,23 @@ class EventEmitter> { } /** - * @ignore - * Synchronously calls each of the listeners registered for the event named`eventName`, in the order they were registered, passing the supplied arguments + * Synchronously calls each of the listeners registered for the event named + * `eventName`, in the order they were registered, passing the supplied arguments * to each. * + * @ignore + * + * @example + * ```typescript + * import { EventEmitter } from '@tauri-apps/plugin-shell'; + * const emitter = new EventEmitter<{ data: string }>(); + * emitter.on('data', (line) => console.log(line)); + * emitter.emit('data', 'hello'); + * ``` + * + * @param eventName The name of the event to emit. + * @param arg The payload passed to every registered listener. + * * @returns `true` if the event had listeners, `false` otherwise. * * @since 2.0.0 @@ -236,6 +337,18 @@ class EventEmitter> { /** * Returns the number of listeners listening to the event named `eventName`. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.on('close', () => {}); + * console.log(command.listenerCount('close')); // 1 + * ``` + * + * @param eventName The name of the event to count the listeners of. + * + * @returns The number of listeners registered for the given event. + * * @since 2.0.0 */ listenerCount(eventName: N): number { @@ -253,6 +366,18 @@ class EventEmitter> { * * Returns a reference to the `EventEmitter`, so that calls can be chained. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.prependListener('error', (error) => console.error(error)); + * ``` + * + * @param eventName The name of the event to listen to. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * * @since 2.0.0 */ prependListener( @@ -275,6 +400,18 @@ class EventEmitter> { * * Returns a reference to the `EventEmitter`, so that calls can be chained. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.prependOnceListener('error', (error) => console.error(error)); + * ``` + * + * @param eventName The name of the event to listen to once. + * @param listener The callback invoked with the event payload. + * + * @returns A reference to the `EventEmitter`, so that calls can be chained. + * * @since 2.0.0 */ prependOnceListener( @@ -292,12 +429,30 @@ class EventEmitter> { } /** + * A handle to a child process spawned with {@link Command.spawn}, + * which can be used to write to its `stdin` or to kill it. + * * @since 2.0.0 */ class Child { /** The child process `pid`. */ pid: number + /** + * Creates a handle to the child process with the given process id. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * // a `Child` is usually obtained by spawning a command: + * const child = await Command.create('node').spawn(); + * console.log(child.pid); + * ``` + * + * @param pid The process id of the child process. + * + * @since 2.0.0 + */ constructor(pid: number) { this.pid = pid } @@ -329,6 +484,14 @@ class Child { /** * Kills the child process. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * const child = await command.spawn(); + * await child.kill(); + * ``` + * * @returns A promise indicating the success or failure of the operation. * * @since 2.0.0 @@ -341,12 +504,25 @@ class Child { } } +/** + * The events emitted by a {@link Command} instance. + * + * @since 2.0.0 + */ interface CommandEvents { + /** Emitted when the child process terminated, carrying its exit code and signal. */ close: TerminatedPayload + /** Emitted when the child process could not be spawned or failed unexpectedly, carrying the error message. */ error: string } +/** + * The events emitted by the `stdout` and `stderr` streams of a {@link Command}. + * + * @since 2.0.0 + */ interface OutputEvents { + /** Emitted for each line the process wrote to the stream, or for each raw chunk when the `raw` encoding is used. */ data: O } @@ -403,20 +579,9 @@ class Command extends EventEmitter { this.options = options ?? {} } - static create(program: string, args?: string | string[]): Command - static create( - program: string, - args?: string | string[], - options?: SpawnOptions & { encoding: 'raw' } - ): Command - static create( - program: string, - args?: string | string[], - options?: SpawnOptions - ): Command - /** - * Creates a command to execute the given program. + * Creates a command to execute the given program, decoding its output as text. + * * @example * ```typescript * import { Command } from '@tauri-apps/plugin-shell'; @@ -426,6 +591,77 @@ class Command extends EventEmitter { * * @param program The program to execute. * It must be configured in your project's capabilities. + * @param args The arguments to pass to the program. Defaults to no arguments. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static create(program: string, args?: string | string[]): Command + /** + * Creates a command to execute the given program, keeping its output as raw bytes. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('my-app', ['run', 'tauri'], { encoding: 'raw' }); + * const output = await command.execute(); + * console.log(output.stdout); // a Uint8Array + * ``` + * + * @param program The program to execute. + * It must be configured in your project's capabilities. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options using the `raw` encoding, which makes the process + * output be delivered as `Uint8Array` instead of `string`. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static create( + program: string, + args?: string | string[], + options?: SpawnOptions & { encoding: 'raw' } + ): Command + /** + * Creates a command to execute the given program with the given spawn options. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('my-app', ['run', 'tauri'], { cwd: '/path/to/project' }); + * const output = await command.execute(); + * ``` + * + * @param program The program to execute. + * It must be configured in your project's capabilities. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options such as the working directory, the environment + * variables and the character encoding of the process output. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ + static create( + program: string, + args?: string | string[], + options?: SpawnOptions + ): Command + + /** + * Creates a command to execute the given program. + * + * @param program The program to execute. + * It must be configured in your project's capabilities. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options such as the working directory, the environment + * variables and the character encoding of the process output. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 */ static create( program: string, @@ -435,12 +671,74 @@ class Command extends EventEmitter { return new Command(program, args, options) } + /** + * Creates a command to execute the given sidecar program, decoding its output as text. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.sidecar('my-sidecar'); + * const output = await command.execute(); + * ``` + * + * @param program The sidecar program to execute. + * It must be configured in your project's capabilities + * and defined on `tauri.conf.json > bundle > externalBin`. + * @param args The arguments to pass to the program. Defaults to no arguments. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ static sidecar(program: string, args?: string | string[]): Command + /** + * Creates a command to execute the given sidecar program, keeping its output as raw bytes. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.sidecar('my-sidecar', [], { encoding: 'raw' }); + * const output = await command.execute(); + * console.log(output.stdout); // a Uint8Array + * ``` + * + * @param program The sidecar program to execute. + * It must be configured in your project's capabilities + * and defined on `tauri.conf.json > bundle > externalBin`. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options using the `raw` encoding, which makes the process + * output be delivered as `Uint8Array` instead of `string`. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ static sidecar( program: string, args?: string | string[], options?: SpawnOptions & { encoding: 'raw' } ): Command + /** + * Creates a command to execute the given sidecar program with the given spawn options. + * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.sidecar('my-sidecar', [], { cwd: '/path/to/project' }); + * const output = await command.execute(); + * ``` + * + * @param program The sidecar program to execute. + * It must be configured in your project's capabilities + * and defined on `tauri.conf.json > bundle > externalBin`. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options such as the working directory, the environment + * variables and the character encoding of the process output. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 + */ static sidecar( program: string, args?: string | string[], @@ -449,15 +747,17 @@ class Command extends EventEmitter { /** * Creates a command to execute the given sidecar program. - * @example - * ```typescript - * import { Command } from '@tauri-apps/plugin-shell'; - * const command = Command.sidecar('my-sidecar'); - * const output = await command.execute(); - * ``` * - * @param program The program to execute. - * It must be configured in your project's capabilities. + * @param program The sidecar program to execute. + * It must be configured in your project's capabilities + * and defined on `tauri.conf.json > bundle > externalBin`. + * @param args The arguments to pass to the program. Defaults to no arguments. + * @param options Spawn options such as the working directory, the environment + * variables and the character encoding of the process output. + * + * @returns The command instance, ready to be spawned or executed. + * + * @since 2.0.0 */ static sidecar( program: string, @@ -472,6 +772,15 @@ class Command extends EventEmitter { /** * Executes the command as a child process, returning a handle to it. * + * @example + * ```typescript + * import { Command } from '@tauri-apps/plugin-shell'; + * const command = Command.create('node'); + * command.stdout.on('data', (line) => console.log(line)); + * const child = await command.spawn(); + * console.log('pid:', child.pid); + * ``` + * * @returns A promise resolving to the child process handle. * * @since 2.0.0 @@ -562,7 +871,10 @@ interface TerminatedPayload { signal: number | null } -/** Event payload type */ +/** + * The type of the data a child process writes to `stdout` and `stderr`: + * a `string`, or a `Uint8Array` when the `raw` encoding is configured. + */ type IOPayload = string | Uint8Array /** Events emitted by the child process. */ diff --git a/plugins/shell/guest-js/init.ts b/plugins/shell/guest-js/init.ts index 117ba778d..d0ed650d2 100644 --- a/plugins/shell/guest-js/init.ts +++ b/plugins/shell/guest-js/init.ts @@ -2,6 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Webview script injected by the shell plugin on every page load. + * + * It installs a `click` listener on `document.body` that intercepts clicks on + * `` elements whose `href` starts with `http://`, `https://`, + * `mailto:` or `tel:`, cancels the navigation and opens the link with the + * system's default application through the `plugin:shell|open` command instead. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' // open links with the API diff --git a/plugins/shell/src/error.rs b/plugins/shell/src/error.rs index 652421b8f..1e2edcf87 100644 --- a/plugins/shell/src/error.rs +++ b/plugins/shell/src/error.rs @@ -6,17 +6,27 @@ use std::path::PathBuf; use serde::{Serialize, Serializer}; +/// Errors returned by the shell plugin. #[derive(Debug, thiserror::Error)] pub enum Error { + /// Failed to invoke the mobile plugin implementation. + /// + /// Only available on mobile. #[cfg(mobile)] #[error(transparent)] PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + /// I/O error, usually raised when spawning a process or reading its output. #[error(transparent)] Io(#[from] std::io::Error), + /// The path of the current executable has no parent directory, + /// so the path of a sidecar program could not be resolved. #[error("current executable path has no parent")] CurrentExeHasNoParent, + /// The string does not match any of the known programs + /// accepted by the deprecated `open` API. #[error("unknown program {0}")] UnknownProgramName(String), + /// The command is not allowed by the configured shell scope. #[error(transparent)] Scope(#[from] crate::scope::Error), /// Sidecar not allowed by the configuration. @@ -25,6 +35,7 @@ pub enum Error { /// Program not allowed by the scope. #[error("program not allowed on the configured shell scope: {0}")] ProgramNotAllowed(PathBuf), + /// The `encoding` spawn option is neither `raw` nor a label of a known character encoding. #[error("unknown encoding {0}")] UnknownEncoding(String), /// JSON error. diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index c9503731a..2615b12f4 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -29,6 +29,7 @@ mod error; #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] #[allow(deprecated)] pub mod open; +/// Types and helpers to spawn and interact with child processes. pub mod process; mod scope; mod scope_entry; @@ -45,6 +46,9 @@ tauri::ios_plugin_binding!(init_plugin_shell); type ChildStore = Arc>>; +/// Access to the shell APIs. +/// +/// Get an instance of this type with [`ShellExt::shell`]. pub struct Shell { #[allow(dead_code)] app: AppHandle, @@ -90,7 +94,21 @@ impl Shell { } } +/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], +/// [`tauri::Webview`] and [`tauri::Window`] to access the shell APIs. pub trait ShellExt { + /// Gets the shell APIs. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_shell::ShellExt; + /// + /// async fn run_echo(app: &tauri::AppHandle) { + /// let output = app.shell().command("echo").args(["hello"]).output().await.unwrap(); + /// println!("{}", String::from_utf8_lossy(&output.stdout)); + /// } + /// ``` fn shell(&self) -> &Shell; } @@ -100,6 +118,18 @@ impl> ShellExt for T { } } +/// Initializes the shell plugin. +/// +/// The plugin state can be accessed with [`ShellExt::shell`], +/// and all spawned child processes are killed when the application exits. +/// +/// # Examples +/// +/// ```no_run +/// fn setup(builder: tauri::Builder) -> tauri::Builder { +/// builder.plugin(tauri_plugin_shell::init()) +/// } +/// ``` pub fn init() -> TauriPlugin> { Builder::>::new("shell") .js_init_script(include_str!("init-iife.js").to_string()) diff --git a/plugins/single-instance/src/lib.rs b/plugins/single-instance/src/lib.rs index c41a0fa10..9a254a517 100644 --- a/plugins/single-instance/src/lib.rs +++ b/plugins/single-instance/src/lib.rs @@ -33,16 +33,39 @@ mod semver_compat; pub(crate) type SingleInstanceCallback = dyn FnMut(&AppHandle, Vec, String) + Send + Sync + 'static; +/// Initializes the plugin, calling `f` whenever a second instance of the app is started. +/// +/// This is a shortcut for [`Builder::new`] with [`Builder::callback`] set to `f`, then +/// [`Builder::build`]. Use [`Builder`] directly if you need to set a custom [`Builder::dbus_id`]. +/// +/// `f` is called with the app handle, the second instance's command line arguments +/// (as collected by [`std::env::args`], so the first element is the executable path) and its +/// current working directory. If the `deep-link` feature is enabled, the arguments are first +/// forwarded to [`tauri-plugin-deep-link`](https://crates.io/crates/tauri-plugin-deep-link) +/// before `f` runs. +/// +/// The second instance never reaches [`tauri::Builder::run`]: it hands its arguments and working +/// directory off to the first instance and exits immediately. pub fn init, Vec, String) + Send + Sync + 'static>( f: F, ) -> TauriPlugin { Builder::new().callback(f).build() } +/// Releases the resources this plugin uses to detect other instances (the named mutex on +/// Windows, the D-Bus name on Linux or the Unix socket on macOS). +/// +/// The plugin calls this automatically on [`tauri::RunEvent::Exit`], so you normally don't need +/// to call it yourself. Call it manually before terminating the process through means that skip +/// that event, such as [`std::process::exit`], so a future instance of the app isn't mistaken +/// for a still-running one. pub fn destroy>(manager: &M) { platform_impl::destroy(manager) } +/// Builds the single-instance plugin. +/// +/// Created with [`Builder::new`] and consumed by [`Builder::build`]. pub struct Builder { callback: Box>, dbus_id: Option, @@ -63,6 +86,10 @@ impl Default for Builder { } impl Builder { + /// Creates a new builder with a no-op callback (or, when the `deep-link` feature is enabled, + /// a callback that only forwards the arguments to the deep-link plugin) and no custom D-Bus + /// ID. Use [`Builder::callback`] and [`Builder::dbus_id`] to configure it, then + /// [`Builder::build`] to create the plugin. pub fn new() -> Self { Default::default() } @@ -93,6 +120,10 @@ impl Builder { self } + /// Builds the single-instance [`TauriPlugin`]. + /// + /// Register it first among your app's plugins, since the plugins run in the order they were + /// added and a second instance must be detected before the rest of your setup runs. pub fn build(self) -> TauriPlugin { platform_impl::init( self.callback, diff --git a/plugins/sql/guest-js/index.ts b/plugins/sql/guest-js/index.ts index 11d39e70b..19e40afac 100644 --- a/plugins/sql/guest-js/index.ts +++ b/plugins/sql/guest-js/index.ts @@ -2,8 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Interface with SQL databases through [sqlx](https://github.com/launchbadge/sqlx). + * Which database engines can be used depends on the drivers enabled on the Rust + * side of the plugin: SQLite, MySQL and PostgreSQL. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' +/** + * The outcome of a statement run through {@link Database.execute}. + */ export interface QueryResult { /** The number of rows affected by the query. */ rowsAffected: number @@ -23,9 +34,30 @@ export interface QueryResult { * * The `Database` class serves as the primary interface for * communicating with the rust side of the sql plugin. + * + * @since 2.0.0 */ export default class Database { + /** + * The connection string identifying the database on the Rust side, + * for instance `sqlite:test.db`, `mysql://user:pass@host/database` + * or `postgres://user:pass@host/database`. + */ path: string + + /** + * Creates a `Database` instance for the given connection string without + * opening a connection to it. Use {@link Database.load} to connect to the + * database, or {@link Database.get} for a database that is already loaded. + * + * @param path The database connection string, such as `sqlite:test.db`. + * + * @example + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = new Database('sqlite:test.db') + * ``` + */ constructor(path: string) { this.path = path } @@ -41,9 +73,13 @@ export default class Database { * The path is relative to `tauri::path::BaseDirectory::App` and must start with `sqlite:`. * * @example - * ```ts - * const db = await Database.load("sqlite:test.db"); + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = await Database.load('sqlite:test.db') * ``` + * + * @param path The database connection string, such as `sqlite:test.db`. The database is created if it does not exist yet, and any migration registered for it on the Rust side is run. + * @returns A promise resolving to a `Database` instance connected to the given database. */ static async load(path: string): Promise { const _path = await invoke('plugin:sql|load', { @@ -65,9 +101,13 @@ export default class Database { * The path is relative to `tauri::path::BaseDirectory::App` and must start with `sqlite:`. * * @example - * ```ts - * const db = Database.get("sqlite:test.db"); + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = Database.get('sqlite:test.db') * ``` + * + * @param path The database connection string, such as `sqlite:test.db`. + * @returns A `Database` instance bound to the given connection string. */ static get(path: string): Database { return new Database(path) @@ -79,7 +119,10 @@ export default class Database { * Passes a SQL expression to the database for execution. * * @example - * ```ts + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = await Database.load('sqlite:test.db') + * * // for sqlite & postgres * // INSERT example * const result = await db.execute( @@ -104,6 +147,10 @@ export default class Database { * [ todos.title, todos.status, todos.id ] * ); * ``` + * + * @param query The SQL statement to run, using `$1`, `$2`, ... placeholders on SQLite and PostgreSQL and `?` placeholders on MySQL. + * @param bindValues The values bound to the query placeholders, in the order they appear in the statement. Defaults to no values. + * @returns A promise resolving to the number of rows affected by the statement and the last inserted id. */ async execute(query: string, bindValues?: unknown[]): Promise { const [rowsAffected, lastInsertId] = await invoke<[number, number]>( @@ -126,7 +173,10 @@ export default class Database { * Passes in a SELECT query to the database for execution. * * @example - * ```ts + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = await Database.load('sqlite:test.db') + * * // for sqlite & postgres * const result = await db.select( * "SELECT * from todos WHERE id = $1", [ id ] @@ -137,6 +187,10 @@ export default class Database { * "SELECT * from todos WHERE id = ?", [ id ] * ); * ``` + * + * @param query The SQL query to run, using `$1`, `$2`, ... placeholders on SQLite and PostgreSQL and `?` placeholders on MySQL. + * @param bindValues The values bound to the query placeholders, in the order they appear in the query. Defaults to no values. + * @returns A promise resolving to the selected rows, each row being an object keyed by column name. */ async select(query: string, bindValues?: unknown[]): Promise { const result = await invoke('plugin:sql|select', { @@ -154,10 +208,14 @@ export default class Database { * Closes the database connection pool. * * @example - * ```ts + * ```typescript + * import Database from '@tauri-apps/plugin-sql' + * const db = await Database.load('sqlite:test.db') * const success = await db.close() * ``` + * * @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope. + * @returns A promise resolving to `true` once the matching connection pools have been closed. */ async close(db?: string): Promise { const success = await invoke('plugin:sql|close', { diff --git a/plugins/sql/src/error.rs b/plugins/sql/src/error.rs index 5ac845b8a..0f4f68198 100644 --- a/plugins/sql/src/error.rs +++ b/plugins/sql/src/error.rs @@ -4,16 +4,30 @@ use serde::{Serialize, Serializer}; +/// Errors that can happen while connecting to a database, running migrations +/// or executing a query. +/// +/// Serializes to its [`std::fmt::Display`] representation, which is what the +/// frontend receives when a command fails. #[derive(Debug, thiserror::Error)] pub enum Error { + /// An error reported by [`sqlx`], such as a failed connection or a query the database rejected. #[error(transparent)] Sql(#[from] sqlx::Error), + /// A migration registered with [`crate::Builder::add_migrations`] could not be resolved or applied. #[error(transparent)] Migration(#[from] sqlx::migrate::MigrateError), + /// The connection string is missing its `scheme:` prefix, or the scheme does not + /// match any of the enabled database drivers. Contains the offending connection string. #[error("invalid connection url: {0}")] InvalidDbUrl(String), + /// The requested database has not been connected to with the `load` command + /// and is not listed in the plugin's `preload` configuration. + /// Contains the connection string of the database. #[error("database {0} not loaded")] DatabaseNotLoaded(String), + /// A value selected from the database has a SQL type that cannot be converted + /// to JSON. Contains the name of that SQL type. #[error("unsupported datatype: {0}")] UnsupportedDatatype(String), } diff --git a/plugins/sql/src/lib.rs b/plugins/sql/src/lib.rs index e9df558a3..dca4928c5 100644 --- a/plugins/sql/src/lib.rs +++ b/plugins/sql/src/lib.rs @@ -40,6 +40,11 @@ use tokio::sync::{Mutex, RwLock}; use std::collections::HashMap; +/// The connection pools the plugin currently holds, keyed by the connection +/// string the database was loaded with. +/// +/// It is managed as Tauri state, so Rust code can reach the pools with +/// [`tauri::Manager::state`] and run its own queries. #[derive(Default)] pub struct DbInstances(pub RwLock>); @@ -58,15 +63,22 @@ pub(crate) enum LastInsertId { struct Migrations(Mutex>); +/// The `plugins > sql` section of the Tauri configuration file. #[derive(Default, Clone, Deserialize)] pub struct PluginConfig { + /// Connection strings of the databases to connect to when the application + /// starts. Empty by default. #[serde(default)] preload: Vec, } +/// The direction of a [`Migration`]. #[derive(Debug)] pub enum MigrationKind { + /// Moves the schema forward. Only migrations of this kind are executed by the plugin. Up, + /// Reverts an [`Up`](Self::Up) migration. Migrations of this kind are currently + /// never executed by the plugin. Down, } @@ -80,11 +92,23 @@ impl From for MigrationType { } /// A migration definition. +/// +/// Migrations are attached to a database with [`Builder::add_migrations`] and run +/// the first time that database is connected to - on startup for the databases +/// listed in the `preload` configuration, otherwise when the frontend loads it. +/// Only [`MigrationKind::Up`] migrations are executed, in ascending +/// [`version`](Self::version) order, and sqlx keeps track of the versions that +/// already ran so each one is applied at most once per database. #[derive(Debug)] pub struct Migration { + /// The version of this migration. Determines the order in which migrations + /// run and identifies the migration in the database. pub version: i64, + /// A human readable description of what the migration does. pub description: &'static str, + /// The SQL executed when the migration runs. pub sql: &'static str, + /// Whether this migration moves the schema forward or reverts it. pub kind: MigrationKind, } @@ -127,6 +151,11 @@ pub struct Builder { } impl Builder { + /// Creates a new builder with no migrations registered. + /// + /// Prints a message to stderr when none of the `sqlite`, `mysql` and + /// `postgres` Cargo features is enabled, since no database can be + /// connected to in that case. pub fn new() -> Self { #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))] eprintln!("No sql driver enabled. Please set at least one of the \"sqlite\", \"mysql\", \"postgres\" feature flags."); @@ -143,6 +172,33 @@ impl Builder { self } + /// Builds the SQL plugin. + /// + /// On setup the plugin connects to every database listed in the `preload` + /// array of its [configuration](PluginConfig), running the migrations + /// registered for them, and it closes all connection pools when the + /// application exits. + /// + /// # Examples + /// + /// ``` + /// use tauri_plugin_sql::{Builder, Migration, MigrationKind}; + /// + /// fn sql_plugin( + /// ) -> tauri::plugin::TauriPlugin> { + /// Builder::new() + /// .add_migrations( + /// "sqlite:mydatabase.db", + /// vec![Migration { + /// version: 1, + /// description: "create todos table", + /// sql: "CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT);", + /// kind: MigrationKind::Up, + /// }], + /// ) + /// .build() + /// } + /// ``` pub fn build(mut self) -> TauriPlugin> { PluginBuilder::>::new("sql") .invoke_handler(tauri::generate_handler![ diff --git a/plugins/sql/src/wrapper.rs b/plugins/sql/src/wrapper.rs index d47b2d1cb..f07a95e51 100644 --- a/plugins/sql/src/wrapper.rs +++ b/plugins/sql/src/wrapper.rs @@ -22,13 +22,23 @@ use sqlx::Sqlite; use crate::LastInsertId; +/// A connection pool for one of the supported database drivers. +/// +/// The variant is picked from the scheme of the connection string +/// (`sqlite:`, `mysql:` or `postgres:`) and only the variants whose Cargo +/// feature is enabled exist. pub enum DbPool { + /// A SQLite connection pool. Only available with the `sqlite` Cargo feature. #[cfg(feature = "sqlite")] Sqlite(Pool), + /// A MySQL connection pool. Only available with the `mysql` Cargo feature. #[cfg(feature = "mysql")] MySql(Pool), + /// A PostgreSQL connection pool. Only available with the `postgres` Cargo feature. #[cfg(feature = "postgres")] Postgres(Pool), + /// Placeholder used when none of the `sqlite`, `mysql` and `postgres` Cargo + /// features is enabled. Connecting always fails and every other operation is a no-op. #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))] None, } diff --git a/plugins/store/guest-js/index.ts b/plugins/store/guest-js/index.ts index ec8f11657..fdec3e8db 100644 --- a/plugins/store/guest-js/index.ts +++ b/plugins/store/guest-js/index.ts @@ -2,6 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Simple, persistent key-value store. + * + * A store is persisted to a file inside the application data directory and is shared with the + * Rust side of the application, which can read and write the same store through its own API. + * + * @module + */ + import { listen, type UnlistenFn } from '@tauri-apps/api/event' import { invoke, Resource } from '@tauri-apps/api/core' @@ -47,14 +56,20 @@ export type StoreOptions = { /** * Create a new Store or load the existing store with the path. * + * If the file at the given path does not exist yet, the store is created in memory with the + * configured defaults and the file is only written on the first save. + * * @example * ```typescript - * import { Store } from '@tauri-apps/api/store'; - * const store = await Store.load('store.json'); + * import { load } from '@tauri-apps/plugin-store'; + * const store = await load('store.json'); * ``` * * @param path Path to save the store in `app_data_dir` * @param options Store configuration options + * @returns A promise resolving to the loaded store. + * + * @since 2.1.0 */ export async function load( path: string, @@ -73,11 +88,14 @@ export async function load( * * @example * ```typescript - * import { getStore } from '@tauri-apps/api/store'; + * import { getStore } from '@tauri-apps/plugin-store'; * const store = await getStore('store.json'); * ``` * * @param path Path of the store. + * @returns A promise resolving to the store instance, or `null` if it is not loaded. + * + * @since 2.1.0 */ export async function getStore(path: string): Promise { return await Store.get(path) @@ -85,6 +103,11 @@ export async function getStore(path: string): Promise { /** * A lazy loaded key-value store persisted by the backend layer. + * + * The underlying {@linkcode Store} is only created or loaded when one of the methods of this + * class is called for the first time, and every call afterwards reuses that same instance. + * + * @since 2.1.0 */ export class LazyStore implements IStore { private _store?: Promise @@ -97,9 +120,18 @@ export class LazyStore implements IStore { } /** + * Creates a handle to the store at the given path without loading it yet. + * * Note that the options are not applied if someone else already created the store + * * @param path Path to save the store in `app_data_dir` * @param options Store configuration options + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * ``` */ constructor( private readonly path: string, @@ -108,59 +140,252 @@ export class LazyStore implements IStore { /** * Init/load the store if it's not loaded already + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.init(); + * ``` */ async init(): Promise { await this.store } + /** + * Inserts a key-value pair into the store, loading it first if needed. + * + * Delegates to {@linkcode Store.set} on the underlying store. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.set('some-key', { value: 5 }); + * ``` + * + * @param key The key to insert the value at. + * @param value The value to store, which must be serializable to JSON. + */ async set(key: string, value: unknown): Promise { return (await this.store).set(key, value) } + /** + * Returns the value for the given `key` or `undefined` if the key does not exist. + * + * Delegates to {@linkcode Store.get} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const value = await store.get<{ value: number }>('some-key'); + * ``` + * + * @param key The key to read the value of. + * @returns A promise resolving to the stored value, or `undefined` if the key does not exist. + */ async get(key: string): Promise { return (await this.store).get(key) } + /** + * Returns `true` if the given `key` exists in the store. + * + * Delegates to {@linkcode Store.has} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const exists = await store.has('some-key'); + * ``` + * + * @param key The key to check. + * @returns A promise resolving to `true` if the key exists in the store. + */ async has(key: string): Promise { return (await this.store).has(key) } + /** + * Removes a key-value pair from the store. + * + * Delegates to {@linkcode Store.delete} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const removed = await store.delete('some-key'); + * ``` + * + * @param key The key to remove. + * @returns A promise resolving to `true` if the key existed and was removed. + */ async delete(key: string): Promise { return (await this.store).delete(key) } + /** + * Clears the store, removing all key-value pairs. + * + * Note: To clear the storage and reset it to its `default` value, use {@linkcode reset} instead. + * Delegates to {@linkcode Store.clear} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.clear(); + * ``` + */ async clear(): Promise { await (await this.store).clear() } + /** + * Resets the store to its `default` value. + * + * If no default value has been set, this method behaves identical to {@linkcode clear}. + * Delegates to {@linkcode Store.reset} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json', { defaults: { 'some-key': 0 } }); + * await store.reset(); + * ``` + */ async reset(): Promise { await (await this.store).reset() } + /** + * Returns a list of all keys in the store. + * + * Delegates to {@linkcode Store.keys} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const keys = await store.keys(); + * ``` + * + * @returns A promise resolving to the list of keys, in arbitrary order. + */ async keys(): Promise { return (await this.store).keys() } + /** + * Returns a list of all values in the store. + * + * Delegates to {@linkcode Store.values} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const values = await store.values(); + * ``` + * + * @returns A promise resolving to the list of values, in arbitrary order. + */ async values(): Promise { return (await this.store).values() } + /** + * Returns a list of all entries in the store. + * + * Delegates to {@linkcode Store.entries} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const entries = await store.entries(); + * ``` + * + * @returns A promise resolving to the list of key-value pairs, in arbitrary order. + */ async entries(): Promise> { return (await this.store).entries() } + /** + * Returns the number of key-value pairs in the store. + * + * Delegates to {@linkcode Store.length} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const length = await store.length(); + * ``` + * + * @returns A promise resolving to the number of key-value pairs in the store. + */ async length(): Promise { return (await this.store).length() } + /** + * Attempts to load the on-disk state at the store's `path` into memory. + * + * Delegates to {@linkcode Store.reload} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.reload({ ignoreDefaults: true }); + * ``` + * + * @param options Options to change how the on-disk state is merged into the store. + */ async reload(options?: ReloadOptions): Promise { await (await this.store).reload(options) } + /** + * Saves the store to disk at the store's `path`. + * + * Delegates to {@linkcode Store.save} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.save(); + * ``` + */ async save(): Promise { await (await this.store).save() } + /** + * Listen to changes on a store key. + * + * Delegates to {@linkcode Store.onKeyChange} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const unlisten = await store.onKeyChange<{ value: number }>('some-key', (value) => { + * console.log(value); + * }); + * ``` + * + * @param key The key to watch for changes. + * @param cb Callback invoked with the new value, or `undefined` when the key was removed. + * @returns A promise resolving to a function to unlisten to the event. + */ async onKeyChange( key: string, cb: (value: T | undefined) => void @@ -168,12 +393,43 @@ export class LazyStore implements IStore { return (await this.store).onKeyChange(key, cb) } + /** + * Listen to changes on the store. + * + * Delegates to {@linkcode Store.onChange} on the underlying store, loading it first if needed. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * const unlisten = await store.onChange<{ value: number }>((key, value) => { + * console.log(key, value); + * }); + * ``` + * + * @param cb Callback invoked with the changed key and its new value, which is `undefined` when the key was removed. + * @returns A promise resolving to a function to unlisten to the event. + */ async onChange( cb: (key: string, value: T | undefined) => void ): Promise { return (await this.store).onChange(cb) } + /** + * Close the store and cleans up this resource from memory. + * **You should not call any method on this object anymore and should drop any reference to it.** + * + * Delegates to {@linkcode Store.close} on the underlying store. + * If the store was never loaded, this method does nothing. + * + * @example + * ```typescript + * import { LazyStore } from '@tauri-apps/plugin-store'; + * const store = new LazyStore('store.json'); + * await store.close(); + * ``` + */ async close(): Promise { if (this._store) { await (await this._store).close() @@ -183,6 +439,12 @@ export class LazyStore implements IStore { /** * A key-value store persisted by the backend layer. + * + * The values are kept in memory and written to the store's file on {@linkcode Store.save}, + * and automatically after every modification unless auto save is disabled with + * {@linkcode StoreOptions.autoSave}. + * + * @since 2.0.0 */ export class Store extends Resource implements IStore { private constructor(rid: number) { @@ -192,14 +454,18 @@ export class Store extends Resource implements IStore { /** * Create a new Store or load the existing store with the path. * + * If the file at the given path does not exist yet, the store is created in memory with the + * configured defaults and the file is only written on the first save. + * * @example * ```typescript - * import { Store } from '@tauri-apps/api/store'; + * import { Store } from '@tauri-apps/plugin-store'; * const store = await Store.load('store.json'); * ``` * * @param path Path to save the store in `app_data_dir` * @param options Store configuration options + * @returns A promise resolving to the loaded store. */ static async load(path: string, options?: StoreOptions): Promise { const rid = await invoke('plugin:store|load', { @@ -219,7 +485,7 @@ export class Store extends Resource implements IStore { * * @example * ```typescript - * import { Store } from '@tauri-apps/api/store'; + * import { Store } from '@tauri-apps/plugin-store'; * let store = await Store.get('store.json'); * if (!store) { * store = await Store.load('store.json'); @@ -227,6 +493,7 @@ export class Store extends Resource implements IStore { * ``` * * @param path Path of the store. + * @returns A promise resolving to the store instance, or `null` if it is not loaded. */ static async get(path: string): Promise { return await invoke('plugin:store|get_store', { path }).then( @@ -234,6 +501,22 @@ export class Store extends Resource implements IStore { ) } + /** + * Inserts a key-value pair into the store. + * + * A change event is emitted for the key and, unless auto save is disabled, the store is + * scheduled to be written to disk. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * await store.set('some-key', { value: 5 }); + * ``` + * + * @param key The key to insert the value at. + * @param value The value to store, which must be serializable to JSON. + */ async set(key: string, value: unknown): Promise { await invoke('plugin:store|set', { rid: this.rid, @@ -242,6 +525,19 @@ export class Store extends Resource implements IStore { }) } + /** + * Returns the value for the given `key` or `undefined` if the key does not exist. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const value = await store.get<{ value: number }>('some-key'); + * ``` + * + * @param key The key to read the value of. + * @returns A promise resolving to the stored value, or `undefined` if the key does not exist. + */ async get(key: string): Promise { const [value, exists] = await invoke<[T, boolean]>('plugin:store|get', { rid: this.rid, @@ -250,6 +546,19 @@ export class Store extends Resource implements IStore { return exists ? value : undefined } + /** + * Returns `true` if the given `key` exists in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const exists = await store.has('some-key'); + * ``` + * + * @param key The key to check. + * @returns A promise resolving to `true` if the key exists in the store. + */ async has(key: string): Promise { return await invoke('plugin:store|has', { rid: this.rid, @@ -257,6 +566,22 @@ export class Store extends Resource implements IStore { }) } + /** + * Removes a key-value pair from the store. + * + * A change event is emitted when the key existed and, unless auto save is disabled, the store + * is scheduled to be written to disk. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const removed = await store.delete('some-key'); + * ``` + * + * @param key The key to remove. + * @returns A promise resolving to `true` if the key existed and was removed. + */ async delete(key: string): Promise { return await invoke('plugin:store|delete', { rid: this.rid, @@ -264,38 +589,165 @@ export class Store extends Resource implements IStore { }) } + /** + * Clears the store, removing all key-value pairs. + * + * Note: To clear the storage and reset it to its `default` value, use {@linkcode reset} instead. + * A change event is emitted for every removed key. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * await store.clear(); + * ``` + */ async clear(): Promise { await invoke('plugin:store|clear', { rid: this.rid }) } + /** + * Resets the store to its `default` value. + * + * If no default value has been set, this method behaves identical to {@linkcode clear}. + * A change event is emitted for every key whose value changed. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json', { defaults: { 'some-key': 0 } }); + * await store.reset(); + * ``` + */ async reset(): Promise { await invoke('plugin:store|reset', { rid: this.rid }) } + /** + * Returns a list of all keys in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const keys = await store.keys(); + * ``` + * + * @returns A promise resolving to the list of keys, in arbitrary order. + */ async keys(): Promise { return await invoke('plugin:store|keys', { rid: this.rid }) } + /** + * Returns a list of all values in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const values = await store.values(); + * ``` + * + * @returns A promise resolving to the list of values, in arbitrary order. + */ async values(): Promise { return await invoke('plugin:store|values', { rid: this.rid }) } + /** + * Returns a list of all entries in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const entries = await store.entries(); + * ``` + * + * @returns A promise resolving to the list of key-value pairs, in arbitrary order. + */ async entries(): Promise> { return await invoke('plugin:store|entries', { rid: this.rid }) } + /** + * Returns the number of key-value pairs in the store. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const length = await store.length(); + * ``` + * + * @returns A promise resolving to the number of key-value pairs in the store. + */ async length(): Promise { return await invoke('plugin:store|length', { rid: this.rid }) } + /** + * Attempts to load the on-disk state at the store's `path` into memory. + * + * This method is useful if the on-disk state was edited by the user and you want to synchronize the changes. + * + * Note: + * - This method loads the data and merges it with the current store, + * this behavior will be changed to resetting to default first and then merging with the on-disk state in v3, + * to fully match the store with the on-disk state, set {@linkcode ReloadOptions | ignoreDefaults} to `true` + * - This method does not emit change events. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * await store.reload({ ignoreDefaults: true }); + * ``` + * + * @param options Options to change how the on-disk state is merged into the store. + */ async reload(options?: ReloadOptions): Promise { await invoke('plugin:store|reload', { rid: this.rid, ...options }) } + /** + * Saves the store to disk at the store's `path`. + * + * Any pending auto save is cancelled, so the store is written exactly once by this call. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json', { autoSave: false }); + * await store.set('some-key', { value: 5 }); + * await store.save(); + * ``` + */ async save(): Promise { await invoke('plugin:store|save', { rid: this.rid }) } + /** + * Listen to changes on a store key. + * + * The callback is only invoked for changes made to this store instance. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const unlisten = await store.onKeyChange<{ value: number }>('some-key', (value) => { + * console.log(value); + * }); + * ``` + * + * @param key The key to watch for changes. + * @param cb Callback invoked with the new value, or `undefined` when the key was removed. + * @returns A promise resolving to a function to unlisten to the event. + * + * @since 2.0.0 + */ async onKeyChange( key: string, cb: (value: T | undefined) => void @@ -307,6 +759,25 @@ export class Store extends Resource implements IStore { }) } + /** + * Listen to changes on the store. + * + * The callback is only invoked for changes made to this store instance. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-store'; + * const store = await Store.load('store.json'); + * const unlisten = await store.onChange<{ value: number }>((key, value) => { + * console.log(key, value); + * }); + * ``` + * + * @param cb Callback invoked with the changed key and its new value, which is `undefined` when the key was removed. + * @returns A promise resolving to a function to unlisten to the event. + * + * @since 2.0.0 + */ async onChange( cb: (key: string, value: T | undefined) => void ): Promise { @@ -452,7 +923,7 @@ interface IStore { } /** - * Options to {@linkcode IStore.reload} a {@linkcode IStore} + * Options to change how a store is reloaded from its on-disk state. */ export type ReloadOptions = { /** diff --git a/plugins/store/src/error.rs b/plugins/store/src/error.rs index ef5ee5938..29abf9dbc 100644 --- a/plugins/store/src/error.rs +++ b/plugins/store/src/error.rs @@ -4,14 +4,17 @@ use serde::{Serialize, Serializer}; +/// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. pub type Result = std::result::Result; /// The error types. #[derive(thiserror::Error, Debug)] #[non_exhaustive] pub enum Error { + /// The store contents could not be serialized by the configured [`SerializeFn`](crate::SerializeFn). #[error("Failed to serialize store. {0}")] Serialize(Box), + /// The store contents could not be deserialized by the configured [`DeserializeFn`](crate::DeserializeFn). #[error("Failed to deserialize store. {0}")] Deserialize(Box), /// JSON error. diff --git a/plugins/store/src/lib.rs b/plugins/store/src/lib.rs index 458bc4b02..4a49dda01 100644 --- a/plugins/store/src/lib.rs +++ b/plugins/store/src/lib.rs @@ -240,6 +240,11 @@ async fn save(app: AppHandle, rid: ResourceId) -> Result<()> { store.save() } +/// Extension trait to access the store APIs on a [`Manager`] such as `App`, `AppHandle`, +/// `WebviewWindow` or `Window`. +/// +/// The plugin must be registered with [`Builder::build`] for these methods to work, +/// as they rely on the state it manages. pub trait StoreExt { /// Create a store or load an existing store with default settings at the given path. /// @@ -336,6 +341,17 @@ fn default_deserialize( serde_json::from_slice(bytes).map_err(Into::into) } +/// Builder for the store plugin. +/// +/// It is used to register custom serialize and deserialize functions the frontend can select by +/// name when loading a store, and to change the functions used by default (pretty printed JSON). +/// +/// # Examples +/// +/// ``` +/// tauri::Builder::default() +/// .plugin(tauri_plugin_store::Builder::default().build()); +/// ``` pub struct Builder { serialize_fns: HashMap, deserialize_fns: HashMap, @@ -355,6 +371,10 @@ impl Default for Builder { } impl Builder { + /// Creates a new builder using the default serialize and deserialize functions, + /// which read and write pretty printed JSON. + /// + /// This is the same as [`Builder::default`]. pub fn new() -> Self { Self::default() } diff --git a/plugins/store/src/store.rs b/plugins/store/src/store.rs index b597a4369..b13a34ab4 100644 --- a/plugins/store/src/store.rs +++ b/plugins/store/src/store.rs @@ -18,11 +18,25 @@ use tokio::{ time::sleep, }; +/// Function used to serialize the store cache to the bytes written to the store file. +/// +/// The default implementation writes pretty printed JSON. pub type SerializeFn = fn(&HashMap) -> Result, Box>; +/// Function used to deserialize the bytes read from the store file into the store cache. +/// +/// The default implementation parses JSON. pub type DeserializeFn = fn(&[u8]) -> Result, Box>; +/// Resolves the path of a store file, relative to the app data directory +/// ([`BaseDirectory::AppData`]). +/// +/// This is the path the [`Store`] created with the given `path` reads from and writes to. +/// +/// # Errors +/// +/// Returns an error if the app data directory cannot be resolved. pub fn resolve_store_path( app: &AppHandle, path: impl AsRef, @@ -428,6 +442,15 @@ impl std::fmt::Debug for StoreInner { } } +/// A key-value store, persisted to a file resolved with [`resolve_store_path`]. +/// +/// The values are kept in memory and written to disk on [`Store::save`], and also automatically +/// after each modification unless auto save has been disabled with +/// [`StoreBuilder::disable_auto_save`]. Any pending auto save is applied when the store is dropped. +/// +/// Create or load one with [`StoreExt::store`](crate::StoreExt::store) or [`StoreBuilder`]. +/// It is a [`Resource`], so it is also reachable from the frontend by its [`ResourceId`]; +/// closing that resource unregisters the store, meaning the next load creates a new instance. pub struct Store { auto_save: Option, auto_save_debounce_sender: Arc>>>, diff --git a/plugins/stronghold/guest-js/index.ts b/plugins/stronghold/guest-js/index.ts index c1945a210..ca751cbb1 100644 --- a/plugins/stronghold/guest-js/index.ts +++ b/plugins/stronghold/guest-js/index.ts @@ -2,72 +2,147 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Store secrets and keys using the [IOTA Stronghold](https://github.com/iotaledger/stronghold.rs) + * encrypted database and secure runtime. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' +/** + * The name of a Stronghold client, either as a UTF-8 string or as its raw byte representation. + */ export type ClientPath = | string | Iterable | ArrayLike | ArrayBuffer +/** + * The path of a vault inside a client, either as a UTF-8 string or as its raw byte representation. + */ export type VaultPath = | string | Iterable | ArrayLike | ArrayBuffer +/** + * The path of a record inside a vault, either as a UTF-8 string or as its raw byte representation. + */ export type RecordPath = | string | Iterable | ArrayLike | ArrayBuffer +/** + * The key of a record in a client store. Note that the store commands read the + * key as a string on the Rust side, so string keys are the safe choice. + */ export type StoreKey = | string | Iterable | ArrayLike | ArrayBuffer +/** + * The limits applied to the connections of a Stronghold peer-to-peer network. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ export interface ConnectionLimits { + /** The maximum number of incoming connections that can be pending at the same time. */ maxPendingIncoming?: number + /** The maximum number of outgoing connections that can be pending at the same time. */ maxPendingOutgoing?: number + /** The maximum number of established incoming connections. */ maxEstablishedIncoming?: number + /** The maximum number of established outgoing connections. */ maxEstablishedOutgoing?: number + /** The maximum number of established connections per peer. */ maxEstablishedPerPeer?: number + /** The maximum number of established connections, incoming and outgoing combined. */ maxEstablishedTotal?: number } +/** + * The addresses on which a remote peer can be reached. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ export interface PeerAddress { + /** The addresses that are known for the peer, in the multiaddr format. */ known: string[] // multiaddr + /** Whether the peer may be reached through a relay when its known addresses cannot be used. */ use_relay_fallback: boolean } +/** + * The address book of the peer-to-peer network. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ export interface AddressInfo { + /** The known addresses of each peer, keyed by the peer identifier. */ peers: Map + /** The identifiers of the peers that can be used as relays. */ relays: string[] // peers } +/** + * The operations a remote peer is allowed to perform on a client. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ export interface ClientAccess { + /** The default permission to use the secrets stored in a vault. */ useVaultDefault?: boolean + /** Per-vault overrides of {@link ClientAccess.useVaultDefault}. */ useVaultExceptions?: Map + /** The default permission to write secrets to a vault. */ writeVaultDefault?: boolean + /** Per-vault overrides of {@link ClientAccess.writeVaultDefault}. */ writeVaultExceptions?: Map + /** The default permission to clone the secrets of a vault. */ cloneVaultDefault?: boolean + /** Per-vault overrides of {@link ClientAccess.cloneVaultDefault}. */ cloneVaultExceptions?: Map + /** Whether the store of the client can be read. */ readStore?: boolean + /** Whether the store of the client can be written to. */ writeStore?: boolean } +/** + * The access a remote peer is granted on the clients of a snapshot. + * See {@link NetworkConfig} for a note on how these definitions are used. + */ export interface Permissions { + /** The access granted when no exception matches. */ default?: ClientAccess + /** The access granted for specific paths, overriding {@link Permissions.default}. */ exceptions?: Map } +/** + * The configuration of the Stronghold peer-to-peer network. + * + * These definitions mirror the networking options of IOTA Stronghold. The plugin + * does not currently expose a command that consumes them, so they are only useful + * as type definitions. + */ export interface NetworkConfig { + /** The maximum time to wait for the response to an outbound request. */ requestTimeout?: Duration + /** The maximum time to wait when establishing a connection to a peer. */ connectionTimeout?: Duration + /** The limits applied to pending and established connections. */ connectionsLimit?: ConnectionLimits + /** Whether peers on the local network are discovered through mDNS. */ enableMdns?: boolean + /** Whether connections relayed by another peer are enabled. */ enableRelay?: boolean + /** The addresses of the known peers and of the available relays. */ addresses?: AddressInfo + /** The permissions granted to specific peers, keyed by the peer identifier. */ peerPermissions?: Map + /** The permissions granted to the peers that have no entry in {@link NetworkConfig.peerPermissions}. */ permissionsDefault?: Permissions } @@ -79,15 +154,49 @@ export interface Duration { nanos: number } +/** + * A pointer to a record inside a vault, either addressed by its record path + * ({@link Location.generic}) or by a counter ({@link Location.counter}). + * + * @since 2.0.0 + */ export class Location { + /** The location kind, either `Generic` or `Counter`. */ type: string + /** The location data, holding the vault path and the record path or counter. */ payload: Record + /** + * Creates a location of the given kind. Prefer the {@link Location.generic} + * and {@link Location.counter} helpers, which fill the payload for you. + * + * @example + * ```typescript + * import { Location } from '@tauri-apps/plugin-stronghold'; + * const location = new Location('Generic', { vault: 'my-vault', record: 'my-record' }); + * ``` + * + * @param type The location kind, either `Generic` or `Counter`. + * @param payload The data identifying the record inside the vault. + */ constructor(type: string, payload: Record) { this.type = type this.payload = payload } + /** + * Creates a location addressing a record of a vault by its record path. + * + * @example + * ```typescript + * import { Location } from '@tauri-apps/plugin-stronghold'; + * const location = Location.generic('my-vault', 'my-record'); + * ``` + * + * @param vault The path of the vault holding the record. + * @param record The path of the record inside the vault. + * @returns The location of the record. + */ static generic(vault: VaultPath, record: RecordPath): Location { return new Location('Generic', { vault, @@ -95,6 +204,19 @@ export class Location { }) } + /** + * Creates a location addressing a record of a vault by a counter. + * + * @example + * ```typescript + * import { Location } from '@tauri-apps/plugin-stronghold'; + * const location = Location.counter('my-vault', 0); + * ``` + * + * @param vault The path of the vault holding the record. + * @param counter The counter identifying the record inside the vault. + * @returns The location of the record. + */ static counter(vault: VaultPath, counter: number): Location { return new Location('Counter', { vault, @@ -114,8 +236,7 @@ class ProcedureExecutor { * Generate a SLIP10 seed for the given location. * @param outputLocation Location of the record where the seed will be stored. * @param sizeBytes The size in bytes of the SLIP10 seed. - * @param hint The record hint. - * @returns + * @returns A promise resolving to the bytes returned by the procedure. */ async generateSLIP10Seed( outputLocation: Location, @@ -134,13 +255,13 @@ class ProcedureExecutor { } /** - * Derive a SLIP10 private key using a seed or key. + * Derive a SLIP10 private key using a seed or key. The derivation is always + * performed on the Ed25519 curve. * @param chain The chain path. * @param source The source type, either 'Seed' or 'Key'. * @param sourceLocation The source location, must be the `outputLocation` of a previous call to `generateSLIP10Seed` or `deriveSLIP10`. * @param outputLocation Location of the record where the private key will be stored. - * @param hint The record hint. - * @returns + * @returns A promise resolving to the bytes returned by the procedure. */ async deriveSLIP10( chain: number[], @@ -169,8 +290,7 @@ class ProcedureExecutor { * @param mnemonic The mnemonic string. * @param outputLocation The location of the record where the BIP39 mnemonic will be stored. * @param passphrase The optional mnemonic passphrase. - * @param hint The record hint. - * @returns + * @returns A promise resolving to the bytes returned by the procedure. */ async recoverBIP39( mnemonic: string, @@ -191,11 +311,10 @@ class ProcedureExecutor { } /** - * Generate a BIP39 seed. + * Generate a BIP39 seed. The mnemonic is generated in English. * @param outputLocation The location of the record where the BIP39 seed will be stored. * @param passphrase The optional mnemonic passphrase. - * @param hint The record hint. - * @returns + * @returns A promise resolving to the bytes returned by the procedure. */ async generateBIP39( outputLocation: Location, @@ -258,39 +377,118 @@ class ProcedureExecutor { } } +/** + * A client of a stronghold snapshot, owning a set of vaults and a key-value store. + * Clients are obtained with {@link Stronghold.loadClient} and {@link Stronghold.createClient}. + * + * @since 2.0.0 + */ export class Client { + /** The path of the snapshot file this client belongs to. */ path: string + /** The name identifying this client inside the snapshot. */ name: ClientPath + /** + * Creates a client handle for a client that was already loaded or created. + * Prefer {@link Stronghold.loadClient} and {@link Stronghold.createClient}, + * which also register the client on the Rust side. + * + * @example + * ```typescript + * import { Client } from '@tauri-apps/plugin-stronghold'; + * const client = new Client('/path/to/snapshot.hold', 'my-client'); + * ``` + * + * @param path The path of the snapshot file the client belongs to. + * @param name The name identifying the client inside the snapshot. + */ constructor(path: string, name: ClientPath) { this.path = path this.name = name } /** - * Get a vault by name. - * @param name - * @param flags - * @returns + * Gets a handle to the vault with the given name. The vault is created on the + * Rust side when the first secret is written to it. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const vault = client.getVault('my-vault'); + * ``` + * + * @param name The path of the vault. + * @returns The vault handle. */ getVault(name: VaultPath): Vault { return new Vault(this.path, this.name, name) } + /** + * Gets a handle to the key-value store of this client. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const store = client.getStore(); + * ``` + * + * @returns The store handle. + */ getStore(): Store { return new Store(this.path, this.name) } } +/** + * The key-value store of a {@link Client}. Unlike a {@link Vault}, the values + * stored here can be read back directly. + * + * @since 2.0.0 + */ export class Store { + /** The path of the snapshot file this store belongs to. */ path: string + /** The name of the client owning this store. */ client: ClientPath + /** + * Creates a store handle for a client that was already loaded or created. + * Prefer {@link Client.getStore}. + * + * @example + * ```typescript + * import { Store } from '@tauri-apps/plugin-stronghold'; + * const store = new Store('/path/to/snapshot.hold', 'my-client'); + * ``` + * + * @param path The path of the snapshot file the store belongs to. + * @param client The name of the client owning the store. + */ constructor(path: string, client: ClientPath) { this.path = path this.client = client } + /** + * Reads the value of a record of this store. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const value = await client.getStore().get('my-key'); + * ``` + * + * @param key The key of the record. + * @returns A promise resolving to the stored value, or `null` if the key does not exist. + */ async get(key: StoreKey): Promise { return await invoke('plugin:stronghold|get_store_record', { snapshotPath: this.path, @@ -299,6 +497,24 @@ export class Store { }).then((v) => v && Uint8Array.from(v)) } + /** + * Inserts a record in this store, replacing the previous value of the key. + * Note that the snapshot is only persisted when {@link Stronghold.save} is called. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const data = Array.from(new TextEncoder().encode('Hello, World!')); + * await client.getStore().insert('my-key', data); + * await stronghold.save(); + * ``` + * + * @param key The key of the record. + * @param value The value of the record, as an array of bytes. + * @param lifetime The optional duration after which the record expires. + */ async insert( key: StoreKey, value: number[], @@ -313,6 +529,20 @@ export class Store { }) } + /** + * Deletes a record from this store. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * await client.getStore().remove('my-key'); + * ``` + * + * @param key The key of the record. + * @returns A promise resolving to the deleted value, or `null` if the key did not exist. + */ async remove(key: StoreKey): Promise { return await invoke( 'plugin:stronghold|remove_store_record', @@ -329,14 +559,31 @@ export class Store { * A key-value storage that allows create, update and delete operations. * It does not allow reading the data, so one of the procedures must be used to manipulate * the stored data, allowing secure storage of secrets. + * + * @since 2.0.0 */ export class Vault extends ProcedureExecutor { - /** The vault path. */ + /** The path of the snapshot file this vault belongs to. */ path: string + /** The name of the client owning this vault. */ client: ClientPath - /** The vault name. */ + /** The path identifying this vault inside the client. */ name: VaultPath + /** + * Creates a vault handle for a client that was already loaded or created. + * Prefer {@link Client.getVault}. + * + * @example + * ```typescript + * import { Vault } from '@tauri-apps/plugin-stronghold'; + * const vault = new Vault('/path/to/snapshot.hold', 'my-client', 'my-vault'); + * ``` + * + * @param path The path of the snapshot file the vault belongs to. + * @param client The name of the client owning the vault. + * @param name The path identifying the vault inside the client. + */ constructor(path: string, client: ClientPath, name: VaultPath) { super({ snapshotPath: path, @@ -349,11 +596,21 @@ export class Vault extends ProcedureExecutor { } /** - * Insert a record to this vault. - * @param location The record location. - * @param record The record data. - * @param recordHint The record hint. - * @returns + * Writes a secret to this vault. Note that the snapshot is only persisted + * when {@link Stronghold.save} is called. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * const secret = Array.from(new TextEncoder().encode('secret value')); + * await client.getVault('my-vault').insert('my-record', secret); + * await stronghold.save(); + * ``` + * + * @param recordPath The path of the record inside this vault. + * @param secret The secret to store, as an array of bytes. */ async insert(recordPath: RecordPath, secret: number[]): Promise { await invoke('plugin:stronghold|save_secret', { @@ -366,10 +623,20 @@ export class Vault extends ProcedureExecutor { } /** - * Remove a record from the vault. - * @param location The record location. - * @param gc Whether to additionally perform the gargage collection or not. - * @returns + * Deletes a secret from this vault. Only the record path of the given + * location is used, the vault is always this one. + * + * @example + * ```typescript + * import { Stronghold, Location } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * await client + * .getVault('my-vault') + * .remove(Location.generic('my-vault', 'my-record')); + * ``` + * + * @param location The location of the record to delete. */ async remove(location: Location): Promise { await invoke('plugin:stronghold|remove_secret', { @@ -383,15 +650,16 @@ export class Vault extends ProcedureExecutor { /** * A representation of an access to a stronghold. + * + * @since 2.0.0 */ export class Stronghold { + /** The path of the snapshot file backing this stronghold. */ path: string /** - * Initializes a stronghold. - * If the snapshot path located at `path` exists, the password must match. - * @param path - * @param password + * Creates a handle to the stronghold initialized for the given snapshot path. + * @param path The path of the snapshot file. */ private constructor(path: string) { this.path = path @@ -399,8 +667,17 @@ export class Stronghold { /** * Load the snapshot if it exists (password must match), or start a fresh stronghold instance otherwise. - * @param password - * @returns + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * import { appDataDir } from '@tauri-apps/api/path'; + * const stronghold = await Stronghold.load(`${await appDataDir()}/vault.hold`, 'password'); + * ``` + * + * @param path The path of the snapshot file. + * @param password The password used to encrypt and decrypt the snapshot. + * @returns A promise resolving to the stronghold instance. */ static async load(path: string, password: string): Promise { return await invoke('plugin:stronghold|initialize', { @@ -410,7 +687,14 @@ export class Stronghold { } /** - * Remove this instance from the cache. + * Saves the snapshot and removes this instance from the cache. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * await stronghold.unload(); + * ``` */ async unload(): Promise { await invoke('plugin:stronghold|destroy', { @@ -418,6 +702,20 @@ export class Stronghold { }) } + /** + * Loads an existing client from the snapshot. The promise rejects if the + * client does not exist in the snapshot or was already loaded. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.loadClient('my-client'); + * ``` + * + * @param client The name of the client. + * @returns A promise resolving to the loaded client. + */ async loadClient(client: ClientPath): Promise { return await invoke('plugin:stronghold|load_client', { snapshotPath: this.path, @@ -425,6 +723,24 @@ export class Stronghold { }).then(() => new Client(this.path, client)) } + /** + * Creates a new empty client on this stronghold. + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * let client; + * try { + * client = await stronghold.loadClient('my-client'); + * } catch { + * client = await stronghold.createClient('my-client'); + * } + * ``` + * + * @param client The name of the client. + * @returns A promise resolving to the created client. + */ async createClient(client: ClientPath): Promise { return await invoke('plugin:stronghold|create_client', { snapshotPath: this.path, @@ -434,7 +750,15 @@ export class Stronghold { /** * Persists the stronghold state to the snapshot. - * @returns + * + * @example + * ```typescript + * import { Stronghold } from '@tauri-apps/plugin-stronghold'; + * const stronghold = await Stronghold.load('/path/to/snapshot.hold', 'password'); + * const client = await stronghold.createClient('my-client'); + * await client.getStore().insert('my-key', [1, 2, 3]); + * await stronghold.save(); + * ``` */ async save(): Promise { await invoke('plugin:stronghold|save', { diff --git a/plugins/stronghold/src/kdf.rs b/plugins/stronghold/src/kdf.rs index 2389b1c41..f0d3aa8cb 100644 --- a/plugins/stronghold/src/kdf.rs +++ b/plugins/stronghold/src/kdf.rs @@ -2,6 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! Key derivation used to turn the user password into the key that encrypts a snapshot. +//! +//! Only available when the **kdf** Cargo feature is enabled, which is the case by default. + use rand_chacha::ChaCha20Rng; use rand_core::{RngCore, SeedableRng}; use std::path::Path; @@ -10,12 +14,21 @@ use std::path::Path; /// This is a current limitation of Stronghold. const HASH_LENGTH: usize = 32; +/// Password hashing functions that can be used as the key derivation function of +/// [`Builder::new`](crate::Builder::new). pub struct KeyDerivation {} impl KeyDerivation { - /// Will create a key from [`password`] and a generated salt. - /// Salt will be generated to file [`salt_path`] or taken from it - /// if file already exists + /// Hashes `password` with Argon2 using the salt stored in `salt_path`, returning the + /// 32 bytes key used to encrypt a snapshot. + /// + /// The salt is read from `salt_path` when that file already exists, otherwise a new + /// random salt is generated and written to it. + /// + /// # Panics + /// + /// Panics when the salt file cannot be read or written, when its contents are not + /// 32 bytes long, or when hashing the password fails. pub fn argon2(password: &str, salt_path: &Path) -> Vec { let mut salt = [0u8; HASH_LENGTH]; create_or_get_salt(&mut salt, salt_path); diff --git a/plugins/stronghold/src/lib.rs b/plugins/stronghold/src/lib.rs index 020bcd21b..95ea71513 100644 --- a/plugins/stronghold/src/lib.rs +++ b/plugins/stronghold/src/lib.rs @@ -108,8 +108,13 @@ impl From for Slip10DeriveInput { } } +/// The type of a key pair handled by the plugin procedures. +/// +/// Deserialized from the strings `ed25519` and `x25519`, ignoring case. pub enum KeyType { + /// The Ed25519 signature scheme. Ed25519, + /// The X25519 key exchange scheme. X25519, } @@ -423,11 +428,34 @@ enum PasswordHashFunctionKind { Custom(Box), } +/// Builder for the stronghold plugin. +/// +/// It defines how the password sent by the frontend is hashed into the key that +/// encrypts the snapshot file. pub struct Builder { password_hash_function: PasswordHashFunctionKind, } impl Builder { + /// Initializes [`Self`] with a custom password hash function. + /// + /// The function is called with the password sent by the frontend and must return the + /// key used to encrypt the snapshot, which must be 32 bytes long. + /// + /// # Examples + /// + /// ```rust + /// fn init(builder: tauri::Builder) -> tauri::Builder { + /// builder.plugin( + /// tauri_plugin_stronghold::Builder::new(|_password| { + /// // hash the password with a secure algorithm such as argon2 or blake2b + /// // and return the resulting 32 bytes hash + /// unimplemented!() + /// }) + /// .build(), + /// ) + /// } + /// ``` pub fn new Vec + Send + Sync + 'static>(password_hash_function: F) -> Self { Self { password_hash_function: PasswordHashFunctionKind::Custom(Box::new( @@ -442,16 +470,19 @@ impl Builder { /// /// ```rust /// use tauri::Manager; - /// tauri::Builder::default() - /// .setup(|app| { + /// + /// fn init(builder: tauri::Builder) -> tauri::Builder { + /// builder.setup(|app| { /// let salt_path = app /// .path() /// .app_local_data_dir() /// .expect("could not resolve app local data path") /// .join("salt.txt"); - /// app.handle().plugin(tauri_plugin_stronghold::Builder::with_argon2(&salt_path).build())?; + /// app.handle() + /// .plugin(tauri_plugin_stronghold::Builder::with_argon2(&salt_path).build())?; /// Ok(()) - /// }); + /// }) + /// } /// ``` #[cfg(feature = "kdf")] pub fn with_argon2(salt_path: &std::path::Path) -> Self { @@ -460,6 +491,8 @@ impl Builder { } } + /// Builds the plugin, registering the password hash function and the commands used + /// by the JavaScript guest bindings. pub fn build(self) -> TauriPlugin { let password_hash_function = self.password_hash_function; diff --git a/plugins/stronghold/src/stronghold.rs b/plugins/stronghold/src/stronghold.rs index a5cadc26e..3e8568488 100644 --- a/plugins/stronghold/src/stronghold.rs +++ b/plugins/stronghold/src/stronghold.rs @@ -2,22 +2,35 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! Types to load, modify and persist an [IOTA Stronghold](https://github.com/iotaledger/stronghold.rs) +//! snapshot file. + use std::{convert::TryFrom, ops::Deref, path::Path}; use iota_stronghold::{KeyProvider, SnapshotPath}; use serde::{Serialize, Serializer}; use zeroize::Zeroizing; +/// Alias for a [`std::result::Result`] with the error type [`Error`]. pub type Result = std::result::Result; +/// Errors returned by the stronghold plugin. +/// +/// Serialized as the error message string when returned to the frontend. #[derive(Debug, thiserror::Error)] pub enum Error { + /// No stronghold was initialized for the given snapshot path. #[error("stronghold not initialized")] StrongholdNotInitialized, + /// An error from the underlying Stronghold client, e.g. when loading a snapshot + /// with the wrong password or when addressing a client that does not exist. #[error(transparent)] Stronghold(#[from] iota_stronghold::ClientError), + /// An error from the Stronghold secure memory implementation, e.g. when the + /// password hash is not a key size Stronghold accepts. #[error(transparent)] Memory(#[from] iota_stronghold::MemoryError), + /// A Stronghold procedure (key generation, key derivation, signing, ...) failed. #[error(transparent)] Procedure(#[from] iota_stronghold::procedures::ProcedureError), } @@ -31,6 +44,10 @@ impl Serialize for Error { } } +/// A Stronghold instance bound to a snapshot file and to the key it is encrypted with. +/// +/// Dereferences to the underlying [`iota_stronghold::Stronghold`], so all of its client +/// and vault operations are available on this type. pub struct Stronghold { inner: iota_stronghold::Stronghold, path: SnapshotPath, @@ -38,6 +55,18 @@ pub struct Stronghold { } impl Stronghold { + /// Creates a Stronghold instance for the snapshot file at `path`, encrypted with + /// `password` as the key. + /// + /// When the file already exists its snapshot is loaded, which requires `password` to + /// be the key it was encrypted with. Otherwise an empty instance is created and + /// nothing is written to disk until [`Self::save`] is called. + /// + /// # Errors + /// + /// Returns [`Error::Memory`] when `password` is not a key size Stronghold accepts + /// (it must be 32 bytes long) and [`Error::Stronghold`] when an existing snapshot + /// cannot be loaded with it. pub fn new>(path: P, password: Vec) -> Result { let path = SnapshotPath::from_path(path); let stronghold = iota_stronghold::Stronghold::default(); @@ -52,12 +81,15 @@ impl Stronghold { }) } + /// Writes the state of all clients to the snapshot file, encrypted with the key + /// this instance was created with. pub fn save(&self) -> Result<()> { self.inner .commit_with_keyprovider(&self.path, &self.keyprovider)?; Ok(()) } + /// Returns a reference to the underlying [`iota_stronghold::Stronghold`] instance. pub fn inner(&self) -> &iota_stronghold::Stronghold { &self.inner } diff --git a/plugins/updater/guest-js/index.ts b/plugins/updater/guest-js/index.ts index c0f023bbb..4afbb0f56 100644 --- a/plugins/updater/guest-js/index.ts +++ b/plugins/updater/guest-js/index.ts @@ -2,12 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * In-app updates for Tauri applications: check the configured endpoints for a new release, + * download it and install it. + * + * @module + */ + import { invoke, Channel, Resource } from '@tauri-apps/api/core' /** Options used when checking for updates */ interface CheckOptions { /** - * Request headers + * The headers to send along with the update check request. */ headers?: HeadersInit /** @@ -27,7 +34,7 @@ interface CheckOptions { /** Options used when downloading an update */ interface DownloadOptions { /** - * Request headers + * The headers to send along with the update download request. */ headers?: HeadersInit /** @@ -59,17 +66,58 @@ type DownloadEvent = | { event: 'Progress'; data: { chunkLength: number } } | { event: 'Finished' } +/** + * An update announced by the update server, as returned by {@linkcode check}. + * + * It holds a resource on the Rust side, so call {@linkcode Update.close} when you are done with it + * without installing it. + * + * @since 2.0.0 + */ class Update extends Resource { // TODO: remove this field in v3 - /** @deprecated This is always true, check if the return value is `null` instead when using {@linkcode check} */ + /** + * Whether an update is available. + * + * @deprecated This is always true, check if the return value is `null` instead when using {@linkcode check} + */ available: boolean + /** + * The version of the application that is currently running. + */ currentVersion: string + /** + * The version announced by the update server. + */ version: string + /** + * The publish date of the update as an RFC 3339 string, when the server provided one. + */ date?: string + /** + * The release notes of the update, when the server provided them. + */ body?: string + /** + * The raw update manifest returned by the server, useful when it contains + * additional fields that the updater itself does not handle. + */ rawJson: Record private downloadedBytes?: Resource + /** + * Creates an update from the metadata returned by the backend. + * You should not need to call this yourself, use {@linkcode check} instead. + * + * @param metadata The update information returned by the backend, including the resource identifier of the update. + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * // the update instance is created for you by `check` + * const update = await check(); + * ``` + */ constructor(metadata: UpdateMetadata) { super(metadata.rid) this.available = true @@ -80,7 +128,29 @@ class Update extends Resource { this.rawJson = metadata.rawJson } - /** Download the updater package. Call {@linkcode install} later to install it */ + /** + * Downloads the updater package. Call {@linkcode install} later to install it. + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * let downloaded = 0; + * await update.download((event) => { + * if (event.event === 'Progress') { + * downloaded += event.data.chunkLength; + * console.log(`downloaded ${downloaded} bytes`); + * } + * }); + * await update.install(); + * } + * ``` + * + * @param onEvent Callback invoked with a `Started` event when the first chunk is received, a `Progress` event for every downloaded chunk and a `Finished` event when the download completes. + * @param options The headers and the timeout to use for the download request. + */ async download( onEvent?: (progress: DownloadEvent) => void, options?: DownloadOptions @@ -105,6 +175,19 @@ class Update extends Resource { * * - **Windows:** This function exits the app after launching the updater installer successfully * - **macOS / Linux:** You need to relaunch the app to run the newly install version + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * await update.download(); + * await update.install(); + * } + * ``` + * + * @param options Options for the installation, such as whether the Windows installer should restart the app afterwards. */ async install(options?: InstallOptions): Promise { if (!this.downloadedBytes) { @@ -128,6 +211,21 @@ class Update extends Resource { * * - **Windows:** This function exits the app after launching the updater installer successfully * - **macOS / Linux:** You need to relaunch the app to run the newly install version + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * await update.downloadAndInstall((event) => { + * console.log(event.event); + * }); + * } + * ``` + * + * @param onEvent Callback invoked with a `Started` event when the first chunk is received, a `Progress` event for every downloaded chunk and a `Finished` event when the download completes. + * @param options The headers and the timeout to use for the download request, and the installation options. */ async downloadAndInstall( onEvent?: (progress: DownloadEvent) => void, @@ -145,13 +243,45 @@ class Update extends Resource { }) } + /** + * Releases the update resource and the downloaded bytes held by the backend. + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * await update.close(); + * } + * ``` + */ async close(): Promise { await this.downloadedBytes?.close() await super.close() } } -/** Check for updates, resolves to `null` if no updates are available */ +/** + * Checks the configured endpoints for an available update. + * + * @example + * ```typescript + * import { check } from '@tauri-apps/plugin-updater'; + * + * const update = await check(); + * if (update) { + * console.log(`update ${update.version} is available`); + * await update.downloadAndInstall(); + * } + * ``` + * + * @param options The headers, timeout, proxy and target to use for the update check request. + * + * @returns A promise resolving to the available {@linkcode Update}, or `null` when no update is available. + * + * @since 2.0.0 + */ async function check(options?: CheckOptions): Promise { convertToRustHeaders(options) diff --git a/plugins/updater/src/error.rs b/plugins/updater/src/error.rs index 6621fe553..916220be7 100644 --- a/plugins/updater/src/error.rs +++ b/plugins/updater/src/error.rs @@ -66,24 +66,35 @@ pub enum Error { /// Temp dir is not on same mount mount. This prevents our updater to rename the AppImage to a temp file. #[error("temp directory is not on the same mount point as the AppImage")] TempDirNotOnSameMountPoint, + /// The downloaded archive does not contain a binary for the current target. #[error("binary for the current target not found in the archive")] BinaryNotFoundInArchive, + /// Could not create a temporary directory to store the downloaded update. #[error("failed to create temporary directory")] TempDirNotFound, + /// The privilege escalation prompt shown before installing a Linux package + /// failed or was dismissed by the user. #[error("Authentication failed or was cancelled")] AuthenticationFailed, + /// Installing the downloaded `.deb` package failed. #[error("Failed to install .deb package")] DebInstallFailed, + /// The package manager could not install the downloaded Linux package. #[error("Failed to install package")] PackageInstallFailed, + /// The downloaded update is not in a format the updater can install on the current platform. #[error("invalid updater binary format")] InvalidUpdaterFormat, + /// `http` crate errors. #[error(transparent)] Http(#[from] http::Error), + /// A request header value is not valid. #[error(transparent)] InvalidHeaderValue(#[from] http::header::InvalidHeaderValue), + /// A request header name is not valid. #[error(transparent)] InvalidHeaderName(#[from] http::header::InvalidHeaderName), + /// The release publish date could not be formatted as an RFC 3339 string. #[error("Failed to format date")] FormatDate, /// The configured updater endpoint must use a secure protocol like `https` @@ -105,6 +116,7 @@ pub enum Error { "The update signature does not specify the version it was signed for, which `requireSignedVersion` requires. Re-sign and re-publish this release, or disable `requireSignedVersion`." )] MissingSignedVersion, + /// Tauri errors. #[error(transparent)] Tauri(#[from] tauri::Error), } @@ -118,4 +130,5 @@ impl Serialize for Error { } } +/// A [`Result`](std::result::Result) alias whose error type is the updater [`Error`](enum@Error). pub type Result = std::result::Result; diff --git a/plugins/updater/src/lib.rs b/plugins/updater/src/lib.rs index 18f2a11a5..8e58a4029 100644 --- a/plugins/updater/src/lib.rs +++ b/plugins/updater/src/lib.rs @@ -137,6 +137,21 @@ struct UpdaterState { headers: HeaderMap, } +/// Builder for the updater plugin. +/// +/// The values set here are the defaults used by every [`Updater`] created through +/// [`UpdaterExt::updater`] and [`UpdaterExt::updater_builder`]; they can still be overridden +/// per updater instance on the [`UpdaterBuilder`]. +/// +/// # Examples +/// +/// ```no_run +/// use tauri::Runtime; +/// +/// fn register_updater(builder: tauri::Builder) -> tauri::Builder { +/// builder.plugin(tauri_plugin_updater::Builder::new().build()) +/// } +/// ``` #[derive(Default)] pub struct Builder { target: Option, @@ -147,15 +162,26 @@ pub struct Builder { } impl Builder { + /// Creates a new builder with the default configuration. pub fn new() -> Self { Self::default() } + /// Sets the target name used when checking for updates. + /// + /// It replaces the `{{target}}` variable in the endpoint URLs and is used as the key to look + /// up the release in the `platforms` object of a static update manifest. + /// + /// When it is not set, the updater uses the current operating system name (`linux`, `darwin` + /// or `windows`) in the endpoint URLs and looks for `{os}-{arch}-{bundle_type}` then + /// `{os}-{arch}` in the manifest. pub fn target(mut self, target: impl Into) -> Self { self.target.replace(target.into()); self } + /// Sets the public key used to verify the update signature, + /// overriding the `pubkey` value of the plugin configuration. pub fn pubkey>(mut self, pubkey: S) -> Self { self.pubkey.replace(pubkey.into()); self @@ -189,6 +215,11 @@ impl Builder { self } + /// Adds a header to be sent on every updater request. + /// + /// # Errors + /// + /// Returns an error if the header name or the header value is not valid. pub fn header(mut self, key: K, value: V) -> Result where HeaderName: TryFrom, @@ -204,11 +235,22 @@ impl Builder { Ok(self) } + /// Replaces all the headers sent on updater requests with the given map, + /// discarding the ones previously added with [`Self::header`]. pub fn headers(mut self, headers: HeaderMap) -> Self { self.headers = headers; self } + /// Sets the default function used to decide whether a remote release should be installed. + /// + /// The closure receives the current application version and the remote release, + /// and must return `true` when the release should be treated as an update. + /// + /// It applies to every updater created through [`UpdaterExt`] and takes precedence over the + /// `allowDowngrades` configuration value; it can still be overridden per updater instance with + /// [`UpdaterBuilder::version_comparator`]. When no comparator is set at all, a release is + /// installed only if its version is greater than the current one. pub fn default_version_comparator< F: Fn(Version, RemoteRelease) -> bool + Send + Sync + 'static, >( @@ -219,6 +261,10 @@ impl Builder { self } + /// Builds the updater plugin, registering the `check`, `download`, `install` + /// and `download_and_install` commands used by the JavaScript API. + /// + /// Pass the returned plugin to [`tauri::Builder::plugin`]. pub fn build(self) -> TauriPlugin { let pubkey = self.pubkey; let target = self.target; diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index cf65df501..d04a19cdb 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -43,15 +43,26 @@ use crate::{ const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); +/// The kind of bundle the running application was installed from. +/// +/// Its name is appended to the updater target string (`{os}-{arch}-{bundle_type}`) when looking +/// up the release in the update manifest and replaces the `{{bundle_type}}` variable in the +/// endpoint URLs. #[derive(Copy, Clone)] pub enum Installer { + /// Linux AppImage bundle, named `appimage`. AppImage, + /// Debian package, named `deb`. Deb, + /// RPM package, named `rpm`. Rpm, + /// macOS application bundle, named `app`. Also used for applications distributed as DMG. App, + /// Windows WiX (MSI) installer, named `msi`. Msi, + /// Windows NSIS installer, named `nsis`. Nsis, } @@ -68,6 +79,7 @@ impl Installer { } } +/// The update information of a single platform in the update manifest. #[derive(Debug, Deserialize, Serialize, Clone)] pub struct ReleaseManifestPlatform { /// Download URL for the platform @@ -76,11 +88,18 @@ pub struct ReleaseManifestPlatform { pub signature: String, } +/// The platform specific data of a [`RemoteRelease`], in either of the two supported shapes. #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(untagged)] pub enum RemoteReleaseInner { + /// Server Format: the endpoint resolved the platform itself and returned a single + /// download URL and signature. Dynamic(ReleaseManifestPlatform), + /// Static Format: the manifest describes every platform it supports and the updater + /// picks the entry matching the current target. Static { + /// Update information for each platform, keyed by the updater target string + /// (e.g. `darwin-aarch64`). platforms: HashMap, }, } @@ -126,8 +145,16 @@ impl RemoteRelease { } } +/// Function executed right before the Windows installer is spawned and the app exits. +/// See [`UpdaterBuilder::on_before_exit`]. pub type OnBeforeExit = Arc; +/// Function that customizes the `reqwest` client builder used for the updater requests. +/// See [`UpdaterBuilder::configure_client`]. pub type OnBeforeRequest = Arc ClientBuilder + Send + Sync + 'static>; +/// Function that decides whether a remote release must be installed. +/// +/// It receives the current application version and the remote release, +/// and returns `true` when the release should be treated as an update. pub type VersionComparator = Arc bool + Send + Sync>; #[cfg(target_os = "macos")] type MainThreadClosure = Box; @@ -155,6 +182,10 @@ struct UpdaterContext { restart_after_install: bool, } +/// Builder for an [`Updater`] instance. +/// +/// Get one from [`crate::UpdaterExt::updater_builder`], which pre-fills it with the plugin +/// configuration, then call [`UpdaterBuilder::build`]. pub struct UpdaterBuilder { current_version: Version, pub(crate) version_comparator: Option, @@ -208,6 +239,12 @@ impl UpdaterBuilder { } } + /// Sets the function used to decide whether the remote release must be installed, + /// replacing the comparator set with [`crate::Builder::default_version_comparator`] + /// and the behavior of the `allowDowngrades` configuration value. + /// + /// When no comparator is set, a release is only installed if its version is greater + /// than the current application version. pub fn version_comparator bool + Send + Sync + 'static>( mut self, f: F, @@ -216,11 +253,29 @@ impl UpdaterBuilder { self } + /// Sets the target name used when checking for updates. + /// + /// It replaces the `{{target}}` variable in the endpoint URLs and is used as the key to look + /// up the release in the `platforms` object of a static update manifest. + /// + /// When it is not set, the updater uses the current operating system name (`linux`, `darwin` + /// or `windows`) in the endpoint URLs and looks for `{os}-{arch}-{bundle_type}` then + /// `{os}-{arch}` in the manifest. pub fn target(mut self, target: impl Into) -> Self { self.target.replace(target.into()); self } + /// Sets the endpoints to fetch the update manifest from, + /// overriding the `endpoints` configuration value. + /// + /// They are checked in order and the first one that returns a valid release wins. + /// + /// # Errors + /// + /// Returns [`Error::InsecureTransportProtocol`] on release builds if an endpoint does not use + /// the `https` protocol and the `dangerousInsecureTransportProtocol` configuration value is + /// not enabled. On debug builds a warning is printed instead. pub fn endpoints(mut self, endpoints: Vec) -> Result { crate::config::validate_endpoints( &endpoints, @@ -231,11 +286,19 @@ impl UpdaterBuilder { Ok(self) } + /// Sets the path of the application executable, which is used to determine where the update + /// must be installed. Defaults to the path of the current executable, or to the AppImage path + /// when the application runs as an AppImage. pub fn executable_path>(mut self, p: P) -> Self { self.executable_path.replace(p.as_ref().into()); self } + /// Adds a header to be sent on the update check and download requests. + /// + /// # Errors + /// + /// Returns an error if the header name or the header value is not valid. pub fn header(mut self, key: K, value: V) -> Result where HeaderName: TryFrom, @@ -251,21 +314,28 @@ impl UpdaterBuilder { Ok(self) } + /// Replaces all the headers sent on the update check and download requests with the given map, + /// discarding the ones previously added with [`Self::header`]. pub fn headers(mut self, headers: HeaderMap) -> Self { self.headers = headers; self } + /// Removes all the headers previously set on this builder. pub fn clear_headers(mut self) -> Self { self.headers.clear(); self } + /// Sets the timeout of the update check and download requests. + /// When it is not set, the requests do not time out. pub fn timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); self } + /// Sets the proxy used for the update check and download requests. + /// It is ignored when [`Self::no_proxy`] was called. pub fn proxy(mut self, proxy: Url) -> Self { self.proxy.replace(proxy); self @@ -277,6 +347,8 @@ impl UpdaterBuilder { self } + /// Sets the public key used to verify the update signature, + /// overriding the `pubkey` value of the plugin configuration. pub fn pubkey>(mut self, pubkey: S) -> Self { self.context.config.pubkey = pubkey.into(); self @@ -362,6 +434,15 @@ impl UpdaterBuilder { self } + /// Builds the [`Updater`]. + /// + /// # Errors + /// + /// - [`Error::EmptyEndpoints`]: neither [`Self::endpoints`] nor the `endpoints` + /// configuration value provided an endpoint to check. + /// - [`Error::UnsupportedArch`]: the updater does not support the current architecture. + /// - [`Error::FailedToDetermineExtractPath`]: the install directory could not be resolved + /// from the executable path. pub fn build(self) -> Result { let endpoints = self .endpoints @@ -412,6 +493,9 @@ impl UpdaterBuilder { } } +/// Checks the configured endpoints for an application update. +/// +/// Get one from [`crate::UpdaterExt::updater`] or by calling [`UpdaterBuilder::build`]. pub struct Updater { current_version: Version, version_comparator: Option, @@ -429,6 +513,25 @@ pub struct Updater { } impl Updater { + /// Checks the endpoints for an update, returning the first release that the version + /// comparator accepts. + /// + /// Each endpoint is requested in order, with the `{{current_version}}`, `{{target}}`, + /// `{{arch}}` and `{{bundle_type}}` variables replaced in its URL, until one of them + /// answers with a release manifest the updater can parse. + /// + /// Resolves to `None` when an endpoint replies with `204 No Content` or when the release it + /// announced is not considered an update - by default when its version is not greater than the + /// current application version. + /// + /// # Errors + /// + /// - [`Error::UnsupportedOs`]: no target was set and the updater does not support the + /// current operating system. + /// - [`Error::ReleaseNotFound`]: no endpoint returned a release manifest. + /// - The last request or deserialization error when every endpoint failed. + /// - [`Error::TargetNotFound`] or [`Error::TargetsNotFound`]: the manifest has no entry + /// for the current target. pub async fn check(&self) -> Result> { // we want JSON only let mut headers = self.headers.clone(); @@ -638,6 +741,10 @@ impl Updater { } } +/// An update announced by the remote server, returned by [`Updater::check`]. +/// +/// Use [`Update::download`] followed by [`Update::install`], or [`Update::download_and_install`], +/// to apply it. #[derive(Clone)] pub struct Update { /// Update description @@ -1427,6 +1534,14 @@ fn updater_arch() -> Option<&'static str> { } } +/// Resolves the path the update must be installed to from the path of the application executable. +/// +/// This is the directory holding the executable, except on macOS where the `.app` bundle path is +/// returned for executables living in `Contents/MacOS`. +/// +/// # Errors +/// +/// Returns [`Error::FailedToDetermineExtractPath`] when the path has no parent directory. pub fn extract_path_from_executable(executable_path: &Path) -> Result { // Return the path of the current executable by default // Example C:\Program Files\My App\ diff --git a/plugins/upload/guest-js/index.ts b/plugins/upload/guest-js/index.ts index c9f1863ad..2783cbe91 100644 --- a/plugins/upload/guest-js/index.ts +++ b/plugins/upload/guest-js/index.ts @@ -2,23 +2,89 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Upload files from disk to a remote server over HTTP, and download files from a remote HTTP + * server to disk. + * + * @module + */ + import { invoke, Channel } from '@tauri-apps/api/core' +/** + * The payload sent to a {@link ProgressHandler} while an upload or download is in progress. + */ interface ProgressPayload { + /** + * The number of bytes transferred since the previous progress event (i.e. the size of the + * last chunk), not the cumulative total. + */ progress: number + /** + * The cumulative number of bytes transferred so far. + */ progressTotal: number + /** + * The total size of the transfer in bytes. For a download this is the value of the response's + * `Content-Length` header, and is `0` if the server did not send it or the response body is + * compressed. For an upload this is the size of the file being uploaded. + */ total: number + /** + * The current transfer speed, approximately in bytes per second. It is recalculated about + * every 500 milliseconds and stays at `0` until then. + */ transferSpeed: number } +/** + * A callback invoked with a {@link ProgressPayload} every time a chunk of data is + * uploaded or downloaded. + */ type ProgressHandler = (progress: ProgressPayload) => void +/** + * The HTTP method used to send the file to the server in {@link upload}. + */ enum HttpMethod { + /** + * Send the file using an HTTP `POST` request. This is the default when no method is given. + */ Post = 'POST', + /** + * Send the file using an HTTP `PUT` request. + */ Put = 'PUT', + /** + * Send the file using an HTTP `PATCH` request. + */ Patch = 'PATCH' } +/** + * Uploads a file at the given path to a URL, using the file's contents as the request body. + * + * @example + * ```typescript + * import { upload } from '@tauri-apps/plugin-upload'; + * + * await upload( + * 'https://example.com/file-upload', + * './path/to/my/file.txt', + * ({ progress, total }) => console.log(`Uploaded ${progress} of ${total} bytes`), + * { 'Content-Type': 'text/plain' } + * ); + * ``` + * + * @param url The URL to upload the file to. + * @param filePath The path of the file to upload. + * @param progressHandler A callback invoked with upload progress updates. + * @param headers Additional request headers to send with the upload. + * @param method The HTTP method used to send the file. Defaults to {@link HttpMethod.Post}. + * @returns A promise resolving to the response body as text. + * + * @since 2.0.0 + */ async function upload( url: string, filePath: string, @@ -46,10 +112,32 @@ async function upload( }) } -/// Download file from given url. -/// -/// Note that `filePath` currently must include the file name. -/// Furthermore the progress events will report a total length of 0 if the server did not sent a `Content-Length` header or if the file is compressed. +/** + * Downloads a file from a given URL and writes it to the given path on disk. + * + * @example + * ```typescript + * import { download } from '@tauri-apps/plugin-upload'; + * + * await download( + * 'https://example.com/file-download-link', + * './path/to/save/my/file.txt', + * ({ progress, total }) => console.log(`Downloaded ${progress} of ${total} bytes`), + * { 'Content-Type': 'text/plain' } + * ); + * ``` + * + * @param url The URL to download the file from. + * @param filePath The path to save the file to. It must include the file name. + * @param progressHandler A callback invoked with download progress updates. The reported + * `total` will be `0` if the server did not send a `Content-Length` header or the response body + * is compressed. + * @param headers Additional request headers to send with the download request. + * @param body An optional request body. When provided, the download is requested with an HTTP + * `POST` request using this value as the body; otherwise an HTTP `GET` request is used. + * + * @since 2.0.0 + */ async function download( url: string, filePath: string, diff --git a/plugins/upload/src/lib.rs b/plugins/upload/src/lib.rs index 33f83288d..7cffb358f 100644 --- a/plugins/upload/src/lib.rs +++ b/plugins/upload/src/lib.rs @@ -38,24 +38,39 @@ use read_progress_stream::ReadProgressStream; use std::collections::HashMap; +/// The HTTP method used to send the file in the `upload` command. +/// +/// Serialized as an uppercase string (`"POST"`, `"PUT"` or `"PATCH"`) to match the JavaScript +/// guest bindings. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum HttpMethod { + /// Send the file with an HTTP `POST` request. This is the default when no method is given. Post, + /// Send the file with an HTTP `PUT` request. Put, + /// Send the file with an HTTP `PATCH` request. Patch, } type Result = std::result::Result; +/// The error type returned by this plugin's `upload` and `download` commands. #[derive(Debug, thiserror::Error)] pub enum Error { + /// An I/O error, for example failing to open, read or write the file at the given path, or + /// the background task that performs the transfer panicking. #[error(transparent)] Io(#[from] std::io::Error), + /// An error returned by the underlying [`reqwest`] HTTP client while sending the request or + /// reading the response. #[error(transparent)] Request(#[from] reqwest::Error), + /// The content length of the request or response could not be determined. #[error("{0}")] ContentLength(String), + /// The HTTP response did not indicate success. Contains the status code and the response + /// body text. #[error("request failed with status code {0}: {1}")] HttpErrorCode(u16, String), } @@ -193,6 +208,7 @@ fn file_to_body(channel: Channel, file: File, file_len: u64) -> )) } +/// Initializes the upload plugin, registering the `upload` and `download` commands. pub fn init() -> TauriPlugin { PluginBuilder::new("upload") .invoke_handler(tauri::generate_handler![download, upload]) diff --git a/plugins/websocket/guest-js/index.ts b/plugins/websocket/guest-js/index.ts index 7ea7b3265..ca0af4420 100644 --- a/plugins/websocket/guest-js/index.ts +++ b/plugins/websocket/guest-js/index.ts @@ -2,8 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Open a WebSocket connection using a Rust client in JS. + * + * @module + */ + import { invoke, Channel } from '@tauri-apps/api/core' +/** + * Configuration used to open a WebSocket connection, forwarded to the underlying `tungstenite` client. + */ export interface ConnectionConfig { /** * Read buffer capacity. The default value is 128 KiB. @@ -39,16 +48,29 @@ export interface ConnectionConfig { headers?: HeadersInit } +/** + * A tagged WebSocket message, discriminated by its `type` field. + */ export interface MessageKind { + /** The kind of message, e.g. `'Text'`, `'Binary'`, `'Ping'`, `'Pong'` or `'Close'`. */ type: T + /** The message payload, whose shape depends on {@link MessageKind.type}. */ data: D } +/** + * The code and reason sent (or received) when a WebSocket connection is closed. + */ export interface CloseFrame { + /** The WebSocket close status code, e.g. `1000` for a normal closure. */ code: number + /** A human-readable explanation for why the connection was closed. */ reason: string } +/** + * A message sent to or received from a WebSocket connection. + */ export type Message = | MessageKind<'Text', string> | MessageKind<'Binary', number[]> @@ -56,15 +78,52 @@ export type Message = | MessageKind<'Pong', number[]> | MessageKind<'Close', CloseFrame | null> +/** + * A WebSocket connection, created with {@link WebSocket.connect}. + * + * @since 2.0.0 + */ export default class WebSocket { + /** The identifier of the underlying connection managed by the Rust side. */ id: number private readonly listeners: Set<(arg: Message) => void> + /** + * Creates a {@link WebSocket} wrapper around an already-open connection. + * + * This is used internally by {@link WebSocket.connect}; use that instead of calling this + * constructor directly. + * + * @param id The identifier of the connection returned by the Rust side. + * @param listeners The set of callbacks to notify when a message is received. + * @example + * ```typescript + * import WebSocket from '@tauri-apps/plugin-websocket'; + * + * // internally used by `WebSocket.connect`; prefer calling that instead + * const ws = await WebSocket.connect('wss://example.com'); + * ``` + */ constructor(id: number, listeners: Set<(arg: Message) => void>) { this.id = id this.listeners = listeners } + /** + * Opens a WebSocket connection to the given URL. + * @example + * ```typescript + * import WebSocket from '@tauri-apps/plugin-websocket'; + * + * const ws = await WebSocket.connect('wss://example.com'); + * ``` + * + * @param url The URL to connect to, e.g. `ws://` or `wss://` (the latter requires one of the + * plugin's TLS Cargo features to be enabled). + * @param config Configuration forwarded to the underlying `tungstenite` client. + * @returns A promise resolving to the connected {@link WebSocket}. + * @since 2.0.0 + */ static async connect( url: string, config?: ConnectionConfig @@ -89,6 +148,20 @@ export default class WebSocket { }).then((id) => new WebSocket(id, listeners)) } + /** + * Adds a listener that is called whenever a message is received on this connection, including + * an error message (as a `'Close'` message) when the underlying stream fails. + * @example + * ```typescript + * import WebSocket from '@tauri-apps/plugin-websocket'; + * + * const ws = await WebSocket.connect('wss://example.com'); + * const unlisten = ws.addListener((message) => console.log(message)); + * ``` + * + * @param cb The callback invoked with each received {@link Message}. + * @returns A function that removes the listener when called. + */ addListener(cb: (arg: Message) => void): () => void { this.listeners.add(cb) @@ -97,6 +170,22 @@ export default class WebSocket { } } + /** + * Sends a message through the WebSocket connection. + * @example + * ```typescript + * import WebSocket from '@tauri-apps/plugin-websocket'; + * + * const ws = await WebSocket.connect('wss://example.com'); + * await ws.send('Hello World'); + * await ws.send([1, 2, 3]); + * await ws.send({ type: 'Text', data: 'Hello World' }); + * ``` + * + * @param message The message to send: a plain string (sent as a `'Text'` message), a numeric + * array (sent as a `'Binary'` message), or an explicit {@link Message} object. + * @returns A promise resolving when the message has been sent. + */ async send(message: Message | string | number[]): Promise { let m: Message if (typeof message === 'string') { @@ -116,6 +205,16 @@ export default class WebSocket { }) } + /** + * Closes the WebSocket connection, sending a normal closure (`1000`) close frame to the server. + * @example + * ```typescript + * import WebSocket from '@tauri-apps/plugin-websocket'; + * + * const ws = await WebSocket.connect('wss://example.com'); + * await ws.disconnect(); + * ``` + */ async disconnect(): Promise { await this.send({ type: 'Close', diff --git a/plugins/websocket/src/lib.rs b/plugins/websocket/src/lib.rs index 135be836c..15c7fa2b4 100644 --- a/plugins/websocket/src/lib.rs +++ b/plugins/websocket/src/lib.rs @@ -274,27 +274,36 @@ async fn send( } } +/// Initializes the plugin with the default [`Builder`], i.e. without a custom TLS [`Connector`]. pub fn init() -> TauriPlugin { Builder::default().build() } +/// Builder for the WebSocket plugin, used to configure a custom TLS [`Connector`] before calling [`Builder::build`]. #[derive(Default)] pub struct Builder { tls_connector: Option, } impl Builder { + /// Creates a new [`Builder`] with no custom TLS [`Connector`] configured. pub fn new() -> Self { Self { tls_connector: None, } } + /// Sets the TLS [`Connector`] used to establish `wss://` connections. + /// + /// When this is not called (or is called with [`Connector::Plain`]) and a `rustls-tls` or + /// `rustls-tls-native-roots` feature is enabled, [`Builder::build`] installs `rustls`'s `ring` + /// crypto provider as the process default if none is installed yet. pub fn tls_connector(mut self, connector: Connector) -> Self { self.tls_connector.replace(connector); self } + /// Builds the plugin, registering the `connect` and `send` commands and the shared connection state. pub fn build(self) -> TauriPlugin { PluginBuilder::new("websocket") .invoke_handler(tauri::generate_handler![connect, send]) diff --git a/plugins/window-state/guest-js/index.ts b/plugins/window-state/guest-js/index.ts index 1cbd57332..a2351a316 100644 --- a/plugins/window-state/guest-js/index.ts +++ b/plugins/window-state/guest-js/index.ts @@ -2,21 +2,48 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +/** + * Save and restore window positions and sizes. + * + * @module + */ + import { invoke } from '@tauri-apps/api/core' import { type WindowLabel, getCurrentWindow } from '@tauri-apps/api/window' +/** + * Flags controlling which parts of a window's state are saved and restored. + * Combine multiple flags with the bitwise OR operator (`|`). + */ export enum StateFlags { + /** Save and restore the window size. */ SIZE = 1 << 0, + /** Save and restore the window position. */ POSITION = 1 << 1, + /** Save and restore whether the window is maximized. */ MAXIMIZED = 1 << 2, + /** Save and restore whether the window is visible. */ VISIBLE = 1 << 3, + /** Save and restore whether the window has decorations. */ DECORATIONS = 1 << 4, + /** Save and restore whether the window is fullscreen. */ FULLSCREEN = 1 << 5, + /** Save and restore every flag above. */ ALL = SIZE | POSITION | MAXIMIZED | VISIBLE | DECORATIONS | FULLSCREEN } /** * Save the state of all open windows to disk. + * + * @example + * ```typescript + * import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'; + * + * await saveWindowState(StateFlags.ALL); + * ``` + * + * @param flags Which parts of the state to save. Defaults to the flags passed to the plugin `Builder` (all flags if none were set). + * @since 2.0.0 */ async function saveWindowState(flags?: StateFlags): Promise { await invoke('plugin:window-state|save_window_state', { flags }) @@ -24,6 +51,17 @@ async function saveWindowState(flags?: StateFlags): Promise { /** * Restore the state for the specified window from disk. + * + * @example + * ```typescript + * import { restoreState, StateFlags } from '@tauri-apps/plugin-window-state'; + * + * await restoreState('main', StateFlags.ALL); + * ``` + * + * @param label The label of the window to restore. + * @param flags Which parts of the state to restore. Defaults to the flags passed to the plugin `Builder` (all flags if none were set). + * @since 2.0.0 */ async function restoreState( label: WindowLabel, @@ -34,12 +72,32 @@ async function restoreState( /** * Restore the state for the current window from disk. + * + * @example + * ```typescript + * import { restoreStateCurrent, StateFlags } from '@tauri-apps/plugin-window-state'; + * + * await restoreStateCurrent(StateFlags.ALL); + * ``` + * + * @param flags Which parts of the state to restore. Defaults to the flags passed to the plugin `Builder` (all flags if none were set). + * @since 2.0.0 */ async function restoreStateCurrent(flags?: StateFlags): Promise { await restoreState(getCurrentWindow().label, flags) } /** * Get the name of the file used to store window state. + * + * @example + * ```typescript + * import { filename } from '@tauri-apps/plugin-window-state'; + * + * const name = await filename(); + * ``` + * + * @returns A promise resolving to the name of the file used to store window state. + * @since 2.0.0 */ async function filename(): Promise { return await invoke('plugin:window-state|filename') diff --git a/plugins/window-state/src/lib.rs b/plugins/window-state/src/lib.rs index f3e75848a..21c5a2350 100644 --- a/plugins/window-state/src/lib.rs +++ b/plugins/window-state/src/lib.rs @@ -35,26 +35,41 @@ type FilterCallbackFn = dyn Fn(&str) -> bool + Send + Sync; /// If using a custom filename, you should probably use [`AppHandleExt::filename`] instead. pub const DEFAULT_FILENAME: &str = ".window-state.json"; +/// The error type returned by this plugin's fallible functions. #[derive(Debug, thiserror::Error)] pub enum Error { + /// An I/O error occurred while reading or writing the window state file. #[error(transparent)] Io(#[from] std::io::Error), + /// An error occurred while calling into Tauri, e.g. while resolving the app config directory + /// or manipulating a window. #[error(transparent)] Tauri(#[from] tauri::Error), + /// An error occurred while serializing or deserializing the window state file. #[error(transparent)] SerdeJson(#[from] serde_json::Error), } +/// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. pub type Result = std::result::Result; bitflags! { + /// Flags controlling which parts of a window's state are saved and restored. + /// + /// Flags can be combined with the bitwise OR operator (`|`), e.g. `StateFlags::SIZE | StateFlags::POSITION`. #[derive(Clone, Copy, Debug)] pub struct StateFlags: u32 { + /// Save and restore the window size. const SIZE = 1 << 0; + /// Save and restore the window position. const POSITION = 1 << 1; + /// Save and restore whether the window is maximized. const MAXIMIZED = 1 << 2; + /// Save and restore whether the window is visible. const VISIBLE = 1 << 3; + /// Save and restore whether the window has decorations. const DECORATIONS = 1 << 4; + /// Save and restore whether the window is fullscreen. const FULLSCREEN = 1 << 5; } } @@ -110,6 +125,7 @@ struct WindowStateCache(Arc>>); /// Used to prevent deadlocks from resize and position event listeners setting the cached state on restoring states struct RestoringWindowState(Mutex<()>); +/// Extension trait for [`AppHandle`] exposing window state APIs. pub trait AppHandleExt { /// Saves all open windows state to disk fn save_window_state(&self, flags: StateFlags) -> Result<()>; @@ -151,6 +167,7 @@ impl AppHandleExt for tauri::AppHandle { } } +/// Extension trait for [`Window`] and [`WebviewWindow`] exposing window state APIs. pub trait WindowExt { /// Restores this window state from disk fn restore_state(&self, flags: StateFlags) -> tauri::Result<()>; @@ -321,6 +338,7 @@ impl WindowExtInternal for Window { } } +/// Builder for the window-state [plugin](TauriPlugin). #[derive(Default)] pub struct Builder { denylist: HashSet, @@ -332,6 +350,8 @@ pub struct Builder { } impl Builder { + /// Creates a new [`Builder`] with the default configuration: + /// all [`StateFlags`] enabled, no denylist, no filter, and the [`DEFAULT_FILENAME`]. pub fn new() -> Self { Self::default() } @@ -382,6 +402,12 @@ impl Builder { self } + /// Builds the [`TauriPlugin`]. + /// + /// The plugin loads the previously saved window state on setup, restores each window's + /// state when it becomes ready (unless denylisted, filtered out, or opted out with + /// [`Builder::skip_initial_state`]), tracks move/resize events to keep the state up to date, + /// and saves the state of all windows to disk when the app exits. pub fn build(self) -> TauriPlugin { let state_flags = self.state_flags; let filename = self.filename.unwrap_or_else(|| DEFAULT_FILENAME.into()); From 413b177f812fb5b8e01ce114720baccb8e261b51 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 22 Sep 2026 12:42:20 -0300 Subject: [PATCH 02/29] feat(ci): improve cache usage --- .github/workflows/audit-javascript.yml | 8 +--- .github/workflows/check-generated-files.yml | 8 +--- .github/workflows/fmt.yml | 8 +--- .github/workflows/integration-tests.yml | 14 +++++- .github/workflows/lint-javascript.yml | 8 +--- .github/workflows/lint-rust.yml | 22 ++++++++++ .github/workflows/sync.yml | 9 +--- .github/workflows/test-android.yml | 7 ++- .github/workflows/test-rust.yml | 48 ++++++++++++++++++++- 9 files changed, 89 insertions(+), 43 deletions(-) diff --git a/.github/workflows/audit-javascript.yml b/.github/workflows/audit-javascript.yml index 83c388777..151d913a2 100644 --- a/.github/workflows/audit-javascript.yml +++ b/.github/workflows/audit-javascript.yml @@ -38,18 +38,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - name: Cache pnpm modules - uses: actions/cache@v6 - with: - path: ~/.pnpm-store - key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}- - uses: actions/setup-node@v7 with: node-version: 'lts/*' - uses: pnpm/action-setup@v6 with: run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow - name: audit run: pnpm audit diff --git a/.github/workflows/check-generated-files.yml b/.github/workflows/check-generated-files.yml index 0ea70c49b..11f40d1b1 100644 --- a/.github/workflows/check-generated-files.yml +++ b/.github/workflows/check-generated-files.yml @@ -158,19 +158,13 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Cache pnpm modules - uses: actions/cache@v6 - with: - path: ~/.pnpm-store - key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}- - uses: actions/setup-node@v7 with: node-version: 'lts/*' - uses: pnpm/action-setup@v6 with: run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow - name: build api working-directory: plugins/${{ matrix.package }} diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index b6023261f..bc23cdcef 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -29,19 +29,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - name: Cache pnpm modules - uses: actions/cache@v6 - with: - path: ~/.pnpm-store - key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}- - uses: actions/setup-node@v7 with: node-version: 'lts/*' - uses: pnpm/action-setup@v6 with: run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow - run: pnpm format:check taplo: diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ff1373220..292172cab 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -24,6 +24,9 @@ on: permissions: contents: read +env: + CARGO_PROFILE_DEV_DEBUG: 0 # no debuginfo: faster builds and a much smaller target dir to cache + jobs: run-integration-tests: runs-on: ${{ matrix.platform }} @@ -48,9 +51,18 @@ jobs: sudo apt-get install -y webkit2gtk-4.0 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libfuse2 - uses: Swatinem/rust-cache@v2 + with: + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} - name: install Tauri CLI - run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch dev + # `--debug --target-dir target`: a dev-profile build in the workspace target dir reuses + # the cached dependencies (shared with the test build below) instead of a from-scratch + # release build in a temporary dir. `--locked` keeps the dependency versions stable + # between runs, so the cached artifacts stay valid. + run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch dev --locked --debug --target-dir target - name: run integration tests run: cargo test --test '*' -- --ignored diff --git a/.github/workflows/lint-javascript.yml b/.github/workflows/lint-javascript.yml index b5f05298f..c3fce74c1 100644 --- a/.github/workflows/lint-javascript.yml +++ b/.github/workflows/lint-javascript.yml @@ -40,18 +40,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - name: Cache pnpm modules - uses: actions/cache@v6 - with: - path: ~/.pnpm-store - key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}- - uses: actions/setup-node@v7 with: node-version: 'lts/*' - uses: pnpm/action-setup@v6 with: run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow - name: eslint run: pnpm lint diff --git a/.github/workflows/lint-rust.yml b/.github/workflows/lint-rust.yml index 26dacde74..ede7bf60a 100644 --- a/.github/workflows/lint-rust.yml +++ b/.github/workflows/lint-rust.yml @@ -38,6 +38,7 @@ jobs: pull-requests: read outputs: packages: ${{ steps.filter.outputs.changes }} + saver: ${{ steps.saver.outputs.saver }} steps: - uses: actions/checkout@v7 - uses: dorny/paths-filter@v2 @@ -129,6 +130,22 @@ jobs: - .github/workflows/lint-rust.yml - plugins/window-state/** + # Every package job for a platform restores the same rust-cache entry, and exactly one + # of them saves it (see the `save-if` of the rust-cache step). Prefer the packages whose + # dependency tree covers most of the other plugins' dependencies, so the entry is as + # useful as possible for the packages that did not save it. + - name: pick the package that saves the cache + id: saver + env: + PACKAGES: ${{ steps.filter.outputs.changes }} + run: | + saver="$(jq -rn --argjson packages "${PACKAGES:-[]}" ' + ["tauri-plugin-upload", "tauri-plugin-sql", "tauri-plugin-updater", "tauri-plugin-single-instance"] + | map(select(. as $p | $packages | index($p) != null)) + | first // $packages[0] // empty + ')" + echo "saver=$saver" >> "$GITHUB_OUTPUT" + clippy: needs: changes if: ${{ needs.changes.outputs.packages != '[]' && needs.changes.outputs.packages != '' }} @@ -152,6 +169,11 @@ jobs: components: clippy - uses: Swatinem/rust-cache@v2 + with: + # One entry shared by every package job (the default key is per job, not per + # matrix entry), only written from `v2`/`v3` and by a single package job; see + # test-rust.yml for the reasoning. + save-if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && matrix.package == needs.changes.outputs.saver }} - name: clippy ${{ matrix.package }} run: cargo clippy --package ${{ matrix.package }} --all-targets -- -D warnings diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 1b9e98413..002f5bdd4 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -27,14 +27,6 @@ jobs: - name: Fetch git tags run: git fetch origin 'refs/tags/*:refs/tags/*' - - name: Cache pnpm modules - uses: actions/cache@v6 - with: - path: ~/.pnpm-store - key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}- - - uses: actions/setup-node@v7 with: node-version: 'lts/*' @@ -42,6 +34,7 @@ jobs: - uses: pnpm/action-setup@v6 with: run_install: true + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow - name: Build packages run: pnpm build diff --git a/.github/workflows/test-android.yml b/.github/workflows/test-android.yml index 85cbd9a45..45fdc085c 100644 --- a/.github/workflows/test-android.yml +++ b/.github/workflows/test-android.yml @@ -27,6 +27,9 @@ jobs: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] + # No caches on purpose: this workflow only runs on pull requests, so anything it saved + # would live on the merge ref where no other run can restore it, while still counting + # against the repository's 10 GB cache limit and evicting entries that `v2`/`v3` saved. steps: - uses: actions/checkout@v7 @@ -44,14 +47,12 @@ jobs: with: distribution: temurin java-version: 21 - cache: gradle - name: Setup NDK uses: nttld/setup-ndk@v1 id: setup-ndk with: ndk-version: r25 - local-cache: true # TODO check after https://github.com/nttld/setup-ndk/issues/518 is fixed - name: Restore Android Symlinks @@ -67,8 +68,6 @@ jobs: echo "Changed $(basename "$link") from $current_target to $new_target" done - - uses: Swatinem/rust-cache@v2 - - name: build API run: pnpm build diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index a9e5ae26b..bfd84bf0d 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -26,6 +26,10 @@ on: - '**/Cargo.toml' - '**/Cargo.lock' +env: + CARGO_PROFILE_DEV_DEBUG: 0 # no debuginfo: faster builds and a much smaller target dir to cache + CROSS_REV: 51f46f296253d8122c927c5bb933e3c4f27cc317 + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -40,6 +44,7 @@ jobs: pull-requests: read outputs: packages: ${{ steps.filter.outputs.changes }} + saver: ${{ steps.saver.outputs.saver }} steps: - uses: actions/checkout@v7 - uses: dorny/paths-filter@v2 @@ -186,6 +191,22 @@ jobs: - Cargo.lock - plugins/window-state/** + # Every package job for a platform restores the same rust-cache entry, and exactly one + # of them saves it (see the `save-if` of the rust-cache step). Prefer the packages whose + # dependency tree covers most of the other plugins' dependencies, so the entry is as + # useful as possible for the packages that did not save it. + - name: pick the package that saves the cache + id: saver + env: + PACKAGES: ${{ steps.filter.outputs.changes }} + run: | + saver="$(jq -rn --argjson packages "${PACKAGES:-[]}" ' + ["tauri-plugin-upload", "tauri-plugin-sql", "tauri-plugin-updater", "tauri-plugin-single-instance"] + | map(select(. as $p | $packages | index($p) != null)) + | first // $packages[0] // empty + ')" + echo "saver=$saver" >> "$GITHUB_OUTPUT" + test: needs: changes if: ${{ needs.changes.outputs.packages != '[]' && needs.changes.outputs.packages != '' }} @@ -242,11 +263,34 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - key: cache-${{ matrix.package }}-${{ matrix.platform.target }} + # One entry per platform, shared by every package job: the plugins share most of + # their dependency tree (tauri, wry, tao...), and an entry per package and platform + # (26 × 5 entries of 500-800 MB each) never fits the repository's 10 GB cache limit. + key: ${{ matrix.platform.target }} + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + # A single package job saves the shared entry, so the jobs don't race for the key. + save-if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && matrix.package == needs.changes.outputs.saver }} + + # `cross` is built from a pinned git revision (its latest release predates the Android + # targets we need). Keep the binary in its own small cache entry (accessed by every run, + # so it stays hot) instead of rebuilding it from source on every run. + - name: restore cross + if: ${{ matrix.platform.runner == 'cross' }} + id: cross-cache + uses: actions/cache@v6 + with: + path: ${{ runner.tool_cache }}/cross + key: cross-${{ runner.os }}-${{ env.CROSS_REV }} - name: install cross + if: ${{ matrix.platform.runner == 'cross' && steps.cross-cache.outputs.cache-hit != 'true' }} + run: cargo +stable install cross --git https://github.com/cross-rs/cross --rev ${{ env.CROSS_REV }} --locked --root ${{ runner.tool_cache }}/cross + + - name: add cross to PATH if: ${{ matrix.platform.runner == 'cross' }} - run: cargo +stable install cross --git https://github.com/cross-rs/cross + run: echo "${{ runner.tool_cache }}/cross/bin" >> $GITHUB_PATH - name: test ${{ matrix.package }} if: ${{ matrix.package != 'tauri-plugin-http' && matrix.package != 'tauri-plugin-dialog' }} From 28d644572c79e318944577388d0bc14826c341e4 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Tue, 22 Sep 2026 13:18:21 -0300 Subject: [PATCH 03/29] test(docs): run doctest on CI, make sure doctests pass (#3620) * test(docs): run doctest on CI, make sure doctests pass * no_run on dialog doctests [skip ci] --- .github/workflows/test-rust.yml | 10 ++++ plugins/cli/Cargo.toml | 3 ++ plugins/dialog/src/lib.rs | 34 ++++++------ plugins/fs/Cargo.toml | 3 ++ plugins/fs/src/lib.rs | 94 ++++++++++++++++++++++++--------- plugins/notification/Cargo.toml | 3 ++ plugins/opener/Cargo.toml | 3 ++ plugins/shell/Cargo.toml | 3 ++ plugins/updater/Cargo.toml | 3 ++ 9 files changed, 115 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index bfd84bf0d..7fda7d355 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -299,3 +299,13 @@ jobs: - name: test ${{ matrix.package }} if: ${{ matrix.package == 'tauri-plugin-http' || matrix.package == 'tauri-plugin-dialog' }} run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets + + # `--all-targets` above does not include doc tests, so they are run separately. + # Only on the platforms we actually test on, since cargo skips doc tests when cross compiling. + - name: doc test ${{ matrix.package }} + if: ${{ matrix.platform.command == 'test' && matrix.package != 'tauri-plugin-http' && matrix.package != 'tauri-plugin-dialog' }} + run: ${{ matrix.platform.runner }} test --doc --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-features + + - name: doc test ${{ matrix.package }} + if: ${{ matrix.platform.command == 'test' && (matrix.package == 'tauri-plugin-http' || matrix.package == 'tauri-plugin-dialog') }} + run: ${{ matrix.platform.runner }} test --doc --package ${{ matrix.package }} --target ${{ matrix.platform.target }} diff --git a/plugins/cli/Cargo.toml b/plugins/cli/Cargo.toml index 5df36b185..871597211 100644 --- a/plugins/cli/Cargo.toml +++ b/plugins/cli/Cargo.toml @@ -27,6 +27,9 @@ ios = { level = "none", notes = "" } [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + [dependencies] serde = { workspace = true } serde_json = { workspace = true } diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index 024df8ce8..0e092af0a 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -113,7 +113,7 @@ impl Dialog { /// /// - Message dialog: /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// /// tauri::Builder::default() @@ -130,14 +130,14 @@ impl Dialog { /// /// - Ask dialog: /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; /// /// tauri::Builder::default() /// .setup(|app| { /// app.dialog() /// .message("Are you sure?") - /// .buttons(MessageDialogButtons::OkCancelCustom("Yes", "No")) + /// .buttons(MessageDialogButtons::OkCancelCustom("Yes".to_string(), "No".to_string())) /// .show(|yes| { /// println!("user said {}", if yes { "yes" } else { "no" }); /// }); @@ -147,7 +147,7 @@ impl Dialog { /// /// - Message dialog with OK button: /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; /// /// tauri::Builder::default() @@ -169,7 +169,7 @@ impl Dialog { /// To block the current thread until the user acted on the dialog, you can use `blocking_show`, /// but note that it cannot be executed on the main thread as it will freeze your application. /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; /// /// tauri::Builder::default() @@ -178,7 +178,7 @@ impl Dialog { /// std::thread::spawn(move || { /// let yes = handle.dialog() /// .message("Are you sure?") - /// .buttons(MessageDialogButtons::OkCancelCustom("Yes", "No")) + /// .buttons(MessageDialogButtons::OkCancelCustom("Yes".to_string(), "No".to_string())) /// .blocking_show(); /// }); /// @@ -532,7 +532,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { @@ -559,7 +559,7 @@ impl FileDialogBuilder { /// The file paths cannot be read directly on Android as they are behind a content URI. /// The recommended way to read the files is using the [`fs`](https://v2.tauri.app/plugin/file-system/) plugin: /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_fs::FsExt; /// tauri::Builder::default() @@ -580,7 +580,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { @@ -604,7 +604,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { @@ -629,7 +629,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { @@ -654,7 +654,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { @@ -681,7 +681,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { @@ -703,7 +703,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { @@ -725,7 +725,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { @@ -748,7 +748,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { @@ -771,7 +771,7 @@ impl FileDialogBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { diff --git a/plugins/fs/Cargo.toml b/plugins/fs/Cargo.toml index 0fb01028d..74f7f6ac9 100644 --- a/plugins/fs/Cargo.toml +++ b/plugins/fs/Cargo.toml @@ -30,6 +30,9 @@ serde = { workspace = true } toml = "1.0" tauri-utils = { workspace = true } +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + [dependencies] serde = { workspace = true } serde_json = { workspace = true } diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index 9b37604ba..cac582bb4 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -125,11 +125,17 @@ impl OpenOptions { /// /// # Examples /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; /// - /// let mut options = OpenOptions::new(); - /// let file = options.read(true).open("foo.txt"); + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.read(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); /// ``` #[must_use] pub fn new() -> Self { @@ -143,10 +149,17 @@ impl OpenOptions { /// /// # Examples /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; /// - /// let file = OpenOptions::new().read(true).open("foo.txt"); + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.read(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); /// ``` pub fn read(&mut self, read: bool) -> &mut Self { self.read = read; @@ -163,10 +176,17 @@ impl OpenOptions { /// /// # Examples /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; /// - /// let file = OpenOptions::new().write(true).open("foo.txt"); + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.write(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); /// ``` pub fn write(&mut self, write: bool) -> &mut Self { self.write = write; @@ -217,10 +237,17 @@ impl OpenOptions { /// /// # Examples /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; /// - /// let file = OpenOptions::new().append(true).open("foo.txt"); + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.append(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); /// ``` pub fn append(&mut self, append: bool) -> &mut Self { self.append = append; @@ -236,10 +263,17 @@ impl OpenOptions { /// /// # Examples /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; /// - /// let file = OpenOptions::new().write(true).truncate(true).open("foo.txt"); + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.write(true).truncate(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); /// ``` pub fn truncate(&mut self, truncate: bool) -> &mut Self { self.truncate = truncate; @@ -254,10 +288,17 @@ impl OpenOptions { /// /// # Examples /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; /// - /// let file = OpenOptions::new().write(true).create(true).open("foo.txt"); + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.write(true).create(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); /// ``` pub fn create(&mut self, create: bool) -> &mut Self { self.create = create; @@ -288,12 +329,17 @@ impl OpenOptions { /// /// # Examples /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; + /// ```rust,no_run + /// use std::path::Path; + /// use tauri_plugin_fs::{FsExt, OpenOptions}; /// - /// let file = OpenOptions::new().write(true) - /// .create_new(true) - /// .open("foo.txt"); + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut options = OpenOptions::new(); + /// options.write(true).create_new(true); + /// let file = app.fs().open(Path::new("foo.txt"), options)?; + /// Ok(()) + /// }); /// ``` pub fn create_new(&mut self, create_new: bool) -> &mut Self { self.create_new = create_new; diff --git a/plugins/notification/Cargo.toml b/plugins/notification/Cargo.toml index 1034932fa..b306b5fd5 100644 --- a/plugins/notification/Cargo.toml +++ b/plugins/notification/Cargo.toml @@ -30,6 +30,9 @@ ios = { level = "full", notes = "" } [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + [dependencies] serde = { workspace = true } serde_json = { workspace = true } diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index 553da64d1..e17b53093 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -26,6 +26,9 @@ tauri-plugin = { workspace = true, features = ["build"] } schemars = { workspace = true } serde = { workspace = true } +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + [dependencies] serde = { workspace = true } serde_json = { workspace = true } diff --git a/plugins/shell/Cargo.toml b/plugins/shell/Cargo.toml index 5b51d64a2..105ea5f1e 100644 --- a/plugins/shell/Cargo.toml +++ b/plugins/shell/Cargo.toml @@ -29,6 +29,9 @@ tauri-plugin = { workspace = true, features = ["build"] } schemars = { workspace = true } serde = { workspace = true } +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + [dependencies] serde = { workspace = true } serde_json = { workspace = true } diff --git a/plugins/updater/Cargo.toml b/plugins/updater/Cargo.toml index 01d777aa9..b14910e14 100644 --- a/plugins/updater/Cargo.toml +++ b/plugins/updater/Cargo.toml @@ -31,6 +31,9 @@ ios = { level = "none", notes = "" } [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } +[dev-dependencies] +tauri = { workspace = true, features = ["wry"] } + [dependencies] tauri = { workspace = true } serde = { workspace = true } From 8882934230c903e03d56021669bc146be7a117b7 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 22 Sep 2026 13:39:05 -0300 Subject: [PATCH 04/29] fix: change file --- .changes/fix-geo-haptics-specta-skip.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.changes/fix-geo-haptics-specta-skip.md b/.changes/fix-geo-haptics-specta-skip.md index c52adc335..78f11d581 100644 --- a/.changes/fix-geo-haptics-specta-skip.md +++ b/.changes/fix-geo-haptics-specta-skip.md @@ -1,6 +1,8 @@ --- -haptics: patch -geolocation: patch +"haptics": patch +"geolocation": patch +"haptics-js": patch +"geolocation-js": patch --- Fixed a build failure when the `specta` feature is enabled: the `Error` type used the `#[serde(skip)]` helper attribute, which recent `specta` versions no longer register. It now uses `#[specta(skip)]`. From 06485c5884a34c4b59acc44330f7ed7d67ca4d7a Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Tue, 22 Sep 2026 18:51:05 -0300 Subject: [PATCH 05/29] test(updater): also check .deb and .rpm on e2e test (#3619) * test(updater): also check .deb and .rpm on e2e test * fix test on mac * install rpm on ci * improve update server, fix rpm crash --- .github/workflows/integration-tests.yml | 4 +- .../updater/tests/app-updater/tests/update.rs | 800 +++++++++++++----- 2 files changed, 568 insertions(+), 236 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 292172cab..27993d2fc 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -13,6 +13,7 @@ on: paths: - '.github/workflows/integration-tests.yml' - 'plugins/updater/src/**' + - 'plugins/updater/tests/**' pull_request: branches: - v1 @@ -20,6 +21,7 @@ on: paths: - '.github/workflows/integration-tests.yml' - 'plugins/updater/src/**' + - 'plugins/updater/tests/**' permissions: contents: read @@ -48,7 +50,7 @@ jobs: if: matrix.platform == 'ubuntu-22.04' run: | sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libfuse2 + sudo apt-get install -y webkit2gtk-4.0 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libfuse2 rpm - uses: Swatinem/rust-cache@v2 with: diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index 34020578d..a04253f68 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -8,7 +8,7 @@ use std::{ collections::HashMap, fs::File, path::{Path, PathBuf}, - process::Command, + process::{Command, Stdio}, sync::{Arc, Mutex}, }; @@ -24,6 +24,54 @@ const UPDATED_EXIT_CODE: i32 = 0; const ERROR_EXIT_CODE: i32 = 1; const UP_TO_DATE_EXIT_CODE: i32 = 2; +/// The port the endpoint in the app's `tauri.conf.json` points at. +const UPDATE_PORT: u16 = 3007; + +/// The server a test serves its update manifest and bundle from, shut down when it goes out of +/// scope. +/// +/// Every test binds the same port and `BUILD_LOCK` keeps them from overlapping, so each one has to +/// leave the port free. Shutting the server down by hand at the end of the test does not: a panic +/// anywhere above it leaves the server listening, and because `localhost` resolves to both loopback +/// addresses the next test binds the other one instead of failing. It then answers with the +/// previous test's manifest and serves the previous test's bundle, so one broken test fails the +/// ones after it with an error about the wrong package. +struct UpdaterServer { + server: Arc, + requests: Option>, +} + +impl UpdaterServer { + /// Serves `localhost:port`, answering every request with `handler`. + fn spawn(port: u16, handler: impl Fn(tiny_http::Request) + Send + 'static) -> Self { + let server = Arc::new( + tiny_http::Server::http(format!("localhost:{port}")) + .unwrap_or_else(|e| panic!("failed to start updater server on {port}: {e}")), + ); + + let server_ = server.clone(); + Self { + requests: Some(std::thread::spawn(move || { + for request in server_.incoming_requests() { + handler(request); + } + })), + server, + } + } +} + +impl Drop for UpdaterServer { + fn drop(&mut self) { + // `unblock` only ends `incoming_requests`; the port is released when the last `Arc` to the + // server is dropped, so the request thread has to be gone before the next test binds it + self.server.unblock(); + if let Some(requests) = self.requests.take() { + let _ = requests.join(); + } + } +} + #[derive(Serialize)] struct Config { version: &'static str, @@ -38,6 +86,10 @@ struct Config { #[serde(rename_all = "camelCase")] struct BundleConfig { create_updater_artifacts: Updater, + /// Merged into `bundle.linux` of the app's `tauri.conf.json`. Only set by the RPM test, for + /// `rpm.provides` (see `rpm_self_provides`). + #[serde(skip_serializing_if = "Option::is_none")] + linux: Option, } #[derive(Serialize)] @@ -110,6 +162,154 @@ impl BundleTarget { } } +#[cfg(target_os = "linux")] +impl BundleTarget { + /// Where `cargo tauri build --bundles ` leaves the bundle of the given app version. + fn bundle_path(self, root_dir: &Path, version: &str) -> PathBuf { + root_dir.join(match self { + Self::AppImage => { + format!("target/release/bundle/appimage/app-updater_{version}_amd64.AppImage") + } + Self::Deb => format!("target/release/bundle/deb/app-updater_{version}_amd64.deb"), + Self::Rpm => format!("target/release/bundle/rpm/app-updater-{version}-1.x86_64.rpm"), + _ => unreachable!("{} is not a Linux bundle", self.name()), + }) + } + + /// The package manager the updater hands this bundle to, for the Linux packages. + fn package_manager(self) -> Option<&'static str> { + match self { + Self::Deb => Some("dpkg"), + Self::Rpm => Some("rpm"), + _ => None, + } + } + + /// Whether this host can install the package without asking anything: the package manager must + /// be there with a database it can read, the runtime the app links against must be installed, + /// and `sudo` must not need a password. The update itself escalates through `pkexec`, then a + /// graphical password prompt, then `sudo`, and in a non-interactive run only the last one can + /// succeed. + fn installable(self) -> Result<(), String> { + let Some(manager) = self.package_manager() else { + return Ok(()); + }; + // listing the installed packages tells both that the manager is there and that its database + // works. `rpm` passes this on a Debian host too, against the empty database `apt install + // rpm` leaves behind; `rpm_self_provides` is what keeps the install from resolving the + // app's dependencies against it. + let list_packages: &[&str] = match self { + Self::Deb => &["-l"], + _ => &["-qa"], + }; + if !command_succeeds(manager, list_packages) { + return Err(format!( + "{manager} is not installed or its database is not readable" + )); + } + // so nothing checks webkit2gtk at install time, but the installed app still needs it to + // run. `ldconfig` lists it whichever manager put it there, and lives in an `sbin` that is + // not always on `PATH` + let webkit_installed = ["ldconfig", "/usr/sbin/ldconfig", "/sbin/ldconfig"] + .iter() + .any(|ldconfig| command_stdout_contains(ldconfig, &["-p"], "libwebkit2gtk-4.1.so.0")); + if !webkit_installed { + return Err("the webkit2gtk 4.1 runtime is not installed".into()); + } + if !command_succeeds("sudo", &["-n", "true"]) { + return Err("sudo needs a password".into()); + } + Ok(()) + } +} + +#[cfg(target_os = "linux")] +fn command_succeeds(program: &str, args: &[&str]) -> bool { + Command::new(program) + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|status| status.success()) + .unwrap_or(false) +} + +#[cfg(target_os = "linux")] +fn command_stdout_contains(program: &str, args: &[&str], needle: &str) -> bool { + Command::new(program) + .args(args) + .stderr(Stdio::null()) + .output() + .map(|output| String::from_utf8_lossy(&output.stdout).contains(needle)) + .unwrap_or(false) +} + +/// The libraries the Tauri CLI declares as RPM `Requires` of an app using wry, as +/// `Runtime::linux_dependencies` names them, with the suffix it appends on a 64 bit target. +#[cfg(target_os = "linux")] +const RPM_RUNTIME_LIBRARIES: &[&str] = + &["libwebkit2gtk-4.1.so.0()(64bit)", "libgtk-3.so.0()(64bit)"]; + +/// `bundle.linux` declaring the RPM `Requires` of `RPM_RUNTIME_LIBRARIES` the host's RPM database +/// cannot resolve as `Provides` of the package itself, or `None` when it resolves all of them (and +/// for a bundle that is not an RPM). +/// +/// `rpm` resolves `Requires` against its own database, and on a Debian host that database is the +/// empty one `apt install rpm` leaves behind: the libraries are there, installed by `apt` - which +/// is what `installable` checks with `ldconfig` - but nothing in the RPM database provides them, so +/// both the `rpm -i` that installs the app under test and the `rpm -U` the updater runs fail on +/// them. A package's own `Provides` satisfy its own `Requires`, so declaring them is what tells +/// `rpm` what the dynamic linker already knows. On an RPM distro the real packages provide them and +/// nothing is declared. +#[cfg(target_os = "linux")] +fn rpm_self_provides(bundle_target: BundleTarget) -> Option { + if !matches!(bundle_target, BundleTarget::Rpm) { + return None; + } + + let unresolved = RPM_RUNTIME_LIBRARIES + .iter() + .filter(|library| !command_succeeds("rpm", &["-q", "--whatprovides", library])) + .collect::>(); + if unresolved.is_empty() { + return None; + } + + Some(serde_json::json!({ "rpm": { "provides": unresolved } })) +} + +/// Installs the package system wide, which is where the updater will later install the update over +/// it, and returns the path of the installed executable. +#[cfg(target_os = "linux")] +fn install_package(bundle_target: BundleTarget, package: &Path) -> PathBuf { + let manager = bundle_target + .package_manager() + .expect("not a Linux package"); + // whatever an earlier run left behind + remove_package(bundle_target); + let status = Command::new("sudo") + .args(["-n", manager, "-i"]) + .arg(package) + .status() + .expect("failed to run sudo"); + assert!(status.success(), "failed to install {}", package.display()); + PathBuf::from("/usr/bin/app-updater") +} + +#[cfg(target_os = "linux")] +fn remove_package(bundle_target: BundleTarget) { + let (manager, remove) = match bundle_target { + BundleTarget::Deb => ("dpkg", "-r"), + BundleTarget::Rpm => ("rpm", "-e"), + _ => return, + }; + let _ = Command::new("sudo") + .args(["-n", manager, remove, "app-updater"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); +} + impl Default for BundleTarget { fn default() -> Self { #[cfg(any(target_os = "macos", target_os = "ios"))] @@ -130,15 +330,26 @@ impl Default for BundleTarget { /// /// Windows drives the executable `cargo build` leaves in `target/release`, which no bundler /// touches, so there is nothing to copy there. -fn stage_app_under_test(root_dir: &Path, target: &str) -> PathBuf { +/// +/// A Linux package is not copied but installed: the updater hands the downloaded package to the +/// package manager, which installs it over the one the app runs from. +fn stage_app_under_test(root_dir: &Path, target: &str, bundle_target: BundleTarget) -> PathBuf { #[cfg(windows)] { - let _ = target; + let _ = (target, bundle_target); return root_dir.join("target/release/app-updater.exe"); } + #[cfg(target_os = "linux")] + { + if bundle_target.package_manager().is_some() { + return install_package(bundle_target, &bundle_target.bundle_path(root_dir, "0.1.0")); + } + } + #[cfg(not(windows))] { + let _ = bundle_target; let bundle_path = test_cases(root_dir, "0.1.0", target.to_string()) .first() .unwrap() @@ -186,7 +397,7 @@ fn target_to_platforms( platform, PlatformUpdate { signature, - url: "http://localhost:3007/download".into(), + url: format!("http://localhost:{UPDATE_PORT}/download"), with_elevated_task: false, }, ); @@ -195,49 +406,68 @@ fn target_to_platforms( platforms } +/// A bundle to build, the update platform key the server announces it under (none for a manifest +/// without an update) and the exit codes expected from running the app once per entry. +type TestCase = (BundleTarget, PathBuf, Option, Vec); + #[cfg(target_os = "linux")] -fn test_cases( - root_dir: &Path, - version: &str, - target: String, -) -> Vec<(BundleTarget, PathBuf, Option, Vec)> { +fn test_cases(root_dir: &Path, version: &str, target: String) -> Vec { + let appimage = BundleTarget::AppImage.bundle_path(root_dir, version); vec![ // update using fallback ( BundleTarget::AppImage, - root_dir.join(format!( - "target/release/bundle/appimage/app-updater_{version}_amd64.AppImage" - )), + appimage.clone(), Some(target.clone()), vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], ), // update using full name ( BundleTarget::AppImage, - root_dir.join(format!( - "target/release/bundle/appimage/app-updater_{version}_amd64.AppImage" - )), + appimage.clone(), Some(format!("{target}-{}", BundleTarget::AppImage.name())), vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], ), // no update ( BundleTarget::AppImage, - root_dir.join(format!( - "target/release/bundle/appimage/app-updater_{version}_amd64.AppImage" - )), + appimage, None, vec![ERROR_EXIT_CODE], ), ] } -#[cfg(target_os = "macos")] -fn test_cases( +/// The cases of a Linux package: an update announced under the generic target and one under the +/// package specific target. There is no "no update" case, the AppImage one covers that path. +#[cfg(target_os = "linux")] +fn package_test_cases( root_dir: &Path, - _version: &str, + version: &str, target: String, -) -> Vec<(BundleTarget, PathBuf, Option, Vec)> { + bundle_target: BundleTarget, +) -> Vec { + let package = bundle_target.bundle_path(root_dir, version); + vec![ + // update using fallback + ( + bundle_target, + package.clone(), + Some(target.clone()), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + // update using full name + ( + bundle_target, + package, + Some(format!("{target}-{}", bundle_target.name())), + vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE], + ), + ] +} + +#[cfg(target_os = "macos")] +fn test_cases(root_dir: &Path, _version: &str, target: String) -> Vec { vec![ ( BundleTarget::App, @@ -280,11 +510,7 @@ fn bundle_path(root_dir: &Path, _version: &str, v1compatible: bool) -> PathBuf { } #[cfg(windows)] -fn test_cases( - root_dir: &Path, - version: &str, - target: String, -) -> Vec<(BundleTarget, PathBuf, Option, Vec)> { +fn test_cases(root_dir: &Path, version: &str, target: String) -> Vec { vec![ ( BundleTarget::Nsis, @@ -338,6 +564,7 @@ fn test_cases( } #[test] +#[ignore = "needs the tauri CLI, a display and minutes of build time; the integration tests workflow runs it with --ignored"] fn update_app() { let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); @@ -346,11 +573,12 @@ fn update_app() { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let root_dir = manifest_dir.join("../../../.."); - for mut config in [ + for config in [ Config { version: "1.0.0", bundle: BundleConfig { create_updater_artifacts: Updater::Bool(true), + linux: None, }, plugins: None, }, @@ -358,162 +586,218 @@ fn update_app() { version: "1.0.0", bundle: BundleConfig { create_updater_artifacts: Updater::String(V1Compatible::V1Compatible), + linux: None, }, plugins: None, }, ] { - let v1_compatible = matches!( - config.bundle.create_updater_artifacts, - Updater::String(V1Compatible::V1Compatible) + let cases = test_cases(&root_dir, "1.0.0", target.clone()); + run_update_cases(&manifest_dir, &root_dir, &target, config, cases); + } +} + +/// The updater installs a deb through `dpkg` and an rpm through `rpm`, so this runs the installed +/// app and lets the update replace it system wide. It only runs where that can happen without any +/// prompt (see `BundleTarget::installable`) and removes the package when done. +#[cfg(target_os = "linux")] +#[test] +#[ignore = "needs the tauri CLI, a display and minutes of build time; the integration tests workflow runs it with --ignored"] +fn update_app_deb() { + update_linux_package(BundleTarget::Deb); +} + +/// See `update_app_deb`. +#[cfg(target_os = "linux")] +#[test] +#[ignore = "needs the tauri CLI, a display and minutes of build time; the integration tests workflow runs it with --ignored"] +fn update_app_rpm() { + update_linux_package(BundleTarget::Rpm); +} + +#[cfg(target_os = "linux")] +fn update_linux_package(bundle_target: BundleTarget) { + let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + + if let Err(reason) = bundle_target.installable() { + // the integration tests workflow installs everything this needs, so a skip there means the + // test quietly stopped covering the package - libtest swallows the message on a pass + assert!( + std::env::var_os("CI").is_none(), + "cannot run the {} update test: {reason}", + bundle_target.name() + ); + eprintln!( + "skipping the {} update test: {reason}", + bundle_target.name() + ); + return; + } + + let target = + tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root_dir = manifest_dir.join("../../../.."); + + // the legacy v1 updater never supported these packages, so only the v2 artifacts are tested + let config = Config { + version: "1.0.0", + bundle: BundleConfig { + create_updater_artifacts: Updater::Bool(true), + linux: rpm_self_provides(bundle_target), + }, + plugins: None, + }; + let cases = package_test_cases(&root_dir, "1.0.0", target.clone(), bundle_target); + run_update_cases(&manifest_dir, &root_dir, &target, config, cases); +} + +/// Bundles 1.0.0 as each case's target and serves it, then bundles 0.1.0, runs it once per expected +/// exit code and checks what it reports. +fn run_update_cases( + manifest_dir: &Path, + root_dir: &Path, + target: &str, + mut config: Config, + cases: Vec, +) { + let v1_compatible = matches!( + config.bundle.create_updater_artifacts, + Updater::String(V1Compatible::V1Compatible) + ); + + let updater_zip_ext = if v1_compatible { + if cfg!(windows) { + Some("zip") + } else { + Some("tar.gz") + } + } else if cfg!(target_os = "macos") { + Some("tar.gz") + } else { + None + }; + + for (bundle_target, out_bundle_path, update_platform, status_checks) in cases { + // bundle app update + config.version = "1.0.0"; + build_app(manifest_dir, &config, Some(bundle_target)); + + let bundle_updater_ext = if v1_compatible { + out_bundle_path + .extension() + .unwrap() + .to_str() + .unwrap() + .replace("exe", "nsis") + } else { + out_bundle_path + .extension() + .unwrap() + .to_str() + .unwrap() + .to_string() + }; + let updater_extension = if let Some(updater_zip_ext) = updater_zip_ext { + format!("{bundle_updater_ext}.{updater_zip_ext}") + } else { + bundle_updater_ext + }; + let signature_extension = format!("{updater_extension}.sig"); + let signature_path = out_bundle_path.with_extension(signature_extension); + let signature = std::fs::read_to_string(&signature_path).unwrap_or_else(|_| { + panic!("failed to read signature file {}", signature_path.display()) + }); + let out_updater_path = out_bundle_path.with_extension(updater_extension); + let updater_path = root_dir.join(format!( + "target/release/{}", + out_updater_path.file_name().unwrap().to_str().unwrap() + )); + std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); + + // start the updater server, shut down at the end of the iteration even if a case panics + let _server = UpdaterServer::spawn(UPDATE_PORT, move |request| match request.url() { + "/" => { + let platforms = target_to_platforms(update_platform.clone(), signature.clone()); + + let body = serde_json::to_vec(&Update { + version: "1.0.0".into(), + date: time::OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Rfc3339) + .unwrap(), + platforms, + }) + .unwrap(); + let len = body.len(); + let response = tiny_http::Response::new( + tiny_http::StatusCode(200), + Vec::new(), + std::io::Cursor::new(body), + Some(len), + None, + ); + let _ = request.respond(response); + } + "/download" => { + let _ = request.respond(tiny_http::Response::from_file( + File::open(&updater_path).unwrap_or_else(|_| { + panic!("failed to open updater bundle {}", updater_path.display()) + }), + )); + } + _ => (), + }); + + config.version = "0.1.0"; + + // bundle initial app version; Linux and macOS run the bundle itself, so it cannot be + // skipped there + build_app( + manifest_dir, + &config, + if cfg!(windows) { + None + } else { + Some(bundle_target) + }, ); - let updater_zip_ext = if v1_compatible { - if cfg!(windows) { - Some("zip") - } else { - Some("tar.gz") - } - } else if cfg!(target_os = "macos") { - Some("tar.gz") - } else { - None - }; + let app_path = stage_app_under_test(root_dir, target, bundle_target); - for (bundle_target, out_bundle_path, update_platform, status_checks) in - test_cases(&root_dir, "1.0.0", target.clone()) + for expected_exit_code in status_checks { + let mut binary_cmd = if cfg!(target_os = "macos") { + Command::new(app_path.join("Contents/MacOS/app-updater")) + } else if cfg!(target_os = "linux") + && std::env::var("CI").map(|v| v == "true").unwrap_or_default() + { + let mut c = Command::new("xvfb-run"); + c.arg("--auto-servernum").arg(&app_path); + c + } else { + Command::new(&app_path) + }; + + binary_cmd.env("TARGET", bundle_target.name()); + + let status = binary_cmd + .status() + .unwrap_or_else(|e| panic!("failed to run {}: {e}", app_path.display())); + let code = status.code().unwrap_or(-1); + + if code != expected_exit_code { + panic!( + "failed to run app bundled as {}, expected exit code {expected_exit_code}, got {code}", bundle_target.name() + ); + } + #[cfg(windows)] + if code == UPDATED_EXIT_CODE { + // wait for the update to finish + std::thread::sleep(std::time::Duration::from_secs(5)); + } + } + + #[cfg(target_os = "linux")] { - // bundle app update - config.version = "1.0.0"; - build_app(&manifest_dir, &config, Some(bundle_target)); - - let bundle_updater_ext = if v1_compatible { - out_bundle_path - .extension() - .unwrap() - .to_str() - .unwrap() - .replace("exe", "nsis") - } else { - out_bundle_path - .extension() - .unwrap() - .to_str() - .unwrap() - .to_string() - }; - let updater_extension = if let Some(updater_zip_ext) = updater_zip_ext { - format!("{bundle_updater_ext}.{updater_zip_ext}") - } else { - bundle_updater_ext - }; - let signature_extension = format!("{updater_extension}.sig"); - let signature_path = out_bundle_path.with_extension(signature_extension); - let signature = std::fs::read_to_string(&signature_path).unwrap_or_else(|_| { - panic!("failed to read signature file {}", signature_path.display()) - }); - let out_updater_path = out_bundle_path.with_extension(updater_extension); - let updater_path = root_dir.join(format!( - "target/release/{}", - out_updater_path.file_name().unwrap().to_str().unwrap() - )); - std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); - - // start the updater server - let server = Arc::new( - tiny_http::Server::http("localhost:3007").expect("failed to start updater server"), - ); - - let server_ = server.clone(); - std::thread::spawn(move || { - for request in server_.incoming_requests() { - match request.url() { - "/" => { - let platforms = - target_to_platforms(update_platform.clone(), signature.clone()); - - let body = serde_json::to_vec(&Update { - version: "1.0.0".into(), - date: time::OffsetDateTime::now_utc() - .format(&time::format_description::well_known::Rfc3339) - .unwrap(), - platforms, - }) - .unwrap(); - let len = body.len(); - let response = tiny_http::Response::new( - tiny_http::StatusCode(200), - Vec::new(), - std::io::Cursor::new(body), - Some(len), - None, - ); - let _ = request.respond(response); - } - "/download" => { - let _ = request.respond(tiny_http::Response::from_file( - File::open(&updater_path).unwrap_or_else(|_| { - panic!( - "failed to open updater bundle {}", - updater_path.display() - ) - }), - )); - } - _ => (), - } - } - }); - - config.version = "0.1.0"; - - // bundle initial app version; Linux and macOS run the bundle itself, so it cannot be - // skipped there - build_app( - &manifest_dir, - &config, - if cfg!(windows) { - None - } else { - Some(bundle_target) - }, - ); - - let app_path = stage_app_under_test(&root_dir, &target); - - for expected_exit_code in status_checks { - let mut binary_cmd = if cfg!(target_os = "macos") { - Command::new(app_path.join("Contents/MacOS/app-updater")) - } else if cfg!(target_os = "linux") - && std::env::var("CI").map(|v| v == "true").unwrap_or_default() - { - let mut c = Command::new("xvfb-run"); - c.arg("--auto-servernum").arg(&app_path); - c - } else { - Command::new(&app_path) - }; - - binary_cmd.env("TARGET", bundle_target.name()); - - let status = binary_cmd - .status() - .unwrap_or_else(|e| panic!("failed to run {}: {e}", app_path.display())); - let code = status.code().unwrap_or(-1); - - if code != expected_exit_code { - panic!( - "failed to run app bundled as {}, expected exit code {expected_exit_code}, got {code}", bundle_target.name() - ); - } - #[cfg(windows)] - if code == UPDATED_EXIT_CODE { - // wait for the update to finish - std::thread::sleep(std::time::Duration::from_secs(5)); - } - } - - // graceful shutdown - server.unblock(); + remove_package(bundle_target); } } } @@ -536,14 +820,65 @@ fn app_binary(root_dir: &Path) -> PathBuf { }) } -/// Runs the app against the update server, restoring the binary from `pristine` first. +/// How the app under test has to be built for an install to stay contained. /// -/// A case that clears the version check goes on to actually install, and on Linux installing means -/// writing the downloaded bytes over the running executable. Every case therefore has to start -/// from an untouched binary, or the first one that passes leaves the update behind as the app. +/// Windows and Linux install over the executable itself, so a bare `cargo build` is enough there. +/// macOS replaces the whole `.app` the running executable sits in, and for an executable that is +/// not in one the updater falls back to the directory merely holding it — `target/release`, which +/// is also where the bundle being served and the build cache live. So macOS bundles the app and +/// runs it out of a staged copy of that `.app`, which the install is welcome to replace. +#[cfg(target_os = "macos")] +const APP_UNDER_TEST_BUNDLE: Option = Some(BundleTarget::App); +#[cfg(not(target_os = "macos"))] +const APP_UNDER_TEST_BUNDLE: Option = None; + +/// Where `build_app` leaves the app under test. +fn built_app(root_dir: &Path) -> PathBuf { + #[cfg(target_os = "macos")] + return root_dir.join("target/release/bundle/macos/app-updater.app"); + #[cfg(not(target_os = "macos"))] + return app_binary(root_dir); +} + +/// The copy the test runs, kept out of the way of both the bundler and the build cache. +fn staged_app(root_dir: &Path) -> PathBuf { + #[cfg(target_os = "macos")] + return root_dir.join("target/release/app-under-test-signed/app-updater.app"); + #[cfg(not(target_os = "macos"))] + return app_binary(root_dir); +} + +/// The executable to launch inside the staged app. +fn staged_binary(root_dir: &Path) -> PathBuf { + #[cfg(target_os = "macos")] + return staged_app(root_dir).join("Contents/MacOS/app-updater"); + #[cfg(not(target_os = "macos"))] + return staged_app(root_dir); +} + +/// Puts the pristine 0.1.0 app back where the test runs it, replacing whatever an install left. +fn restore_app(root_dir: &Path, pristine: &Path) { + let app = staged_app(root_dir); + let _ = std::fs::remove_dir_all(&app); + let _ = std::fs::remove_file(&app); + std::fs::create_dir_all(app.parent().unwrap()).expect("failed to create the staging directory"); + copy_recursively(pristine, &app).unwrap_or_else(|e| { + panic!( + "failed to restore {} from {}: {e}", + app.display(), + pristine.display() + ) + }); +} + +/// Runs the app against the update server, restoring it from `pristine` first. +/// +/// A case that clears the version check goes on to actually install, and installing means writing +/// over what the app runs from. Every case therefore has to start from an untouched copy, or the +/// first one that passes leaves the update behind as the app. fn run_app(root_dir: &Path, pristine: &Path) -> String { - let binary = app_binary(root_dir); - std::fs::copy(pristine, &binary).expect("failed to restore the app binary"); + restore_app(root_dir, pristine); + let binary = staged_binary(root_dir); let mut command = if cfg!(target_os = "linux") && std::env::var("CI").map(|v| v == "true").unwrap_or_default() @@ -568,6 +903,7 @@ fn run_app(root_dir: &Path, pristine: &Path) -> String { /// reaches the installer while an accepted one does. Both exit non-zero, which is why these assert /// on the error message rather than the exit code. #[test] +#[ignore = "needs the tauri CLI, a display and minutes of build time; the integration tests workflow runs it with --ignored"] fn update_validates_signed_version() { let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); @@ -584,6 +920,7 @@ fn update_validates_signed_version() { version: "1.0.0", bundle: BundleConfig { create_updater_artifacts: Updater::Bool(true), + linux: None, }, plugins: None, }, @@ -624,57 +961,48 @@ fn update_validates_signed_version() { version: "1.0.0".into(), })); - let server = Arc::new( - tiny_http::Server::http(format!("localhost:{SIGNED_VERSION_PORT}")) - .expect("failed to start updater server"), - ); - - let server_ = server.clone(); let served_ = served.clone(); let target_ = target.clone(); let updater_path_ = updater_path.clone(); - std::thread::spawn(move || { - for request in server_.incoming_requests() { - match request.url() { - "/" => { - let mut platforms = HashMap::new(); - platforms.insert( - target_.clone(), - PlatformUpdate { - // always the genuine 1.0.0 signature; only the version above it moves - signature: signature.clone(), - url: format!("http://localhost:{SIGNED_VERSION_PORT}/download"), - with_elevated_task: false, - }, - ); + // shut down at the end of the test even if a check below panics + let _server = UpdaterServer::spawn(SIGNED_VERSION_PORT, move |request| match request.url() { + "/" => { + let mut platforms = HashMap::new(); + platforms.insert( + target_.clone(), + PlatformUpdate { + // always the genuine 1.0.0 signature; only the version above it moves + signature: signature.clone(), + url: format!("http://localhost:{SIGNED_VERSION_PORT}/download"), + with_elevated_task: false, + }, + ); - let body = serde_json::to_vec(&Update { - version: served_.lock().unwrap().version.clone(), - date: time::OffsetDateTime::now_utc() - .format(&time::format_description::well_known::Rfc3339) - .unwrap(), - platforms, - }) - .unwrap(); - let len = body.len(); - let _ = request.respond(tiny_http::Response::new( - tiny_http::StatusCode(200), - Vec::new(), - std::io::Cursor::new(body), - Some(len), - None, - )); - } - "/download" => { - let _ = request.respond(tiny_http::Response::from_file( - File::open(&updater_path_).unwrap_or_else(|_| { - panic!("failed to open updater bundle {}", updater_path_.display()) - }), - )); - } - _ => (), - } + let body = serde_json::to_vec(&Update { + version: served_.lock().unwrap().version.clone(), + date: time::OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Rfc3339) + .unwrap(), + platforms, + }) + .unwrap(); + let len = body.len(); + let _ = request.respond(tiny_http::Response::new( + tiny_http::StatusCode(200), + Vec::new(), + std::io::Cursor::new(body), + Some(len), + None, + )); } + "/download" => { + let _ = request.respond(tiny_http::Response::from_file( + File::open(&updater_path_).unwrap_or_else(|_| { + panic!("failed to open updater bundle {}", updater_path_.display()) + }), + )); + } + _ => (), }); // `requireSignedVersion` is baked in by `generate_context!`, so each value needs its own build @@ -686,6 +1014,7 @@ fn update_validates_signed_version() { version: "0.1.0", bundle: BundleConfig { create_updater_artifacts: Updater::Bool(true), + linux: None, }, plugins: Some(serde_json::json!({ "updater": { @@ -694,10 +1023,12 @@ fn update_validates_signed_version() { } })), }, - None, + APP_UNDER_TEST_BUNDLE, ); - std::fs::copy(app_binary(&root_dir), &pristine) - .expect("failed to keep a pristine copy of the app binary"); + let _ = std::fs::remove_dir_all(&pristine); + let _ = std::fs::remove_file(&pristine); + copy_recursively(&built_app(&root_dir), &pristine) + .expect("failed to keep a pristine copy of the app"); }; let check = |announced: &str, expected: Option<&str>, unexpected: Option<&str>| { @@ -730,9 +1061,8 @@ fn update_validates_signed_version() { // a signature that names a version is held to it whether or not the option is on check("1.5.0", Some(MISMATCH_ERROR), None); - server.unblock(); - - // leave a runnable binary behind rather than whichever update was installed last - let _ = std::fs::copy(&pristine, app_binary(&root_dir)); + // leave the 0.1.0 app behind rather than whichever update was installed last + restore_app(&root_dir, &pristine); + let _ = std::fs::remove_dir_all(&pristine); let _ = std::fs::remove_file(&pristine); } From 7f87091288080a38ce037262d5377761d802037c Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 22 Sep 2026 18:52:27 -0300 Subject: [PATCH 06/29] test(updater): fix e2e tests on Windows (missing bundle_type patch) --- .../updater/tests/app-updater/tests/update.rs | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index a04253f68..4fcf3de85 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -328,16 +328,23 @@ impl Default for BundleTarget { /// until the next `build_app` call. On Linux and macOS the update is also installed over the very /// bundle being run, which would otherwise leave a 1.0.0 app behind in the bundler output. /// -/// Windows drives the executable `cargo build` leaves in `target/release`, which no bundler -/// touches, so there is nothing to copy there. +/// Windows runs a copy of the executable `cargo build` leaves in `target/release`, patched with the +/// bundle type (see `patch_bundle_type`). The NSIS update installs into the directory of the +/// running executable, so the copy also keeps the install out of `target/release`. /// /// A Linux package is not copied but installed: the updater hands the downloaded package to the /// package manager, which installs it over the one the app runs from. fn stage_app_under_test(root_dir: &Path, target: &str, bundle_target: BundleTarget) -> PathBuf { #[cfg(windows)] { - let _ = (target, bundle_target); - return root_dir.join("target/release/app-updater.exe"); + let _ = target; + let staging_dir = root_dir.join("target/release/app-under-test"); + let _ = std::fs::remove_dir_all(&staging_dir); + std::fs::create_dir_all(&staging_dir).expect("failed to create the staging directory"); + + let staged = staging_dir.join("app-updater.exe"); + patch_bundle_type(&app_binary(root_dir), &staged, bundle_target); + return staged; } #[cfg(target_os = "linux")] @@ -373,6 +380,32 @@ fn stage_app_under_test(root_dir: &Path, target: &str, bundle_target: BundleTarg } } +/// Copies `from` to `to` with the bundle type the updater reads through `bundle_type()` set to +/// `bundle_target`, the way the Tauri CLI does it: by overwriting the first +/// `__TAURI_BUNDLE_TYPE_VAR_UNK` in the executable. +/// +/// The executable in `target/release` never carries it. The CLI patches it only for the time it +/// takes to put it in the installer, then restores the unpatched one, so without this the app +/// cannot resolve the `{target}-{bundle_type}` platform key and only finds `{target}`. +#[cfg(windows)] +fn patch_bundle_type(from: &Path, to: &Path, bundle_target: BundleTarget) { + const PLACEHOLDER: &[u8] = b"__TAURI_BUNDLE_TYPE_VAR_UNK"; + let bundle_type: &[u8] = match bundle_target { + BundleTarget::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS", + BundleTarget::Msi => b"__TAURI_BUNDLE_TYPE_VAR_MSI", + _ => unreachable!("{} is not a Windows bundle", bundle_target.name()), + }; + + let mut binary = std::fs::read(from) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", from.display())); + let index = binary + .windows(PLACEHOLDER.len()) + .position(|window| window == PLACEHOLDER) + .unwrap_or_else(|| panic!("no bundle type placeholder in {}", from.display())); + binary[index..index + PLACEHOLDER.len()].copy_from_slice(bundle_type); + std::fs::write(to, binary).unwrap_or_else(|e| panic!("failed to write {}: {e}", to.display())); +} + /// Copies a file, or a directory such as a macOS `.app`, preserving permissions. fn copy_recursively(from: &Path, to: &Path) -> std::io::Result<()> { if from.is_dir() { From 684feb2510f468f002c57f7bdebf675cf5c1b1d9 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Tue, 22 Sep 2026 18:58:14 -0300 Subject: [PATCH 07/29] feat(tests): add api e2e tests (#3617) * feat(tests): add api e2e tests * lockfile * mobile * try linux fix [skip ci] * ios fix * fix test on windows * improve cache * fix pnpm audit [skip ci] --- .../fix-notification-windows-permission.md | 6 + .github/workflows/lint-javascript.yml | 8 + .github/workflows/test-api-e2e-mobile.yml | 232 + .github/workflows/test-api-e2e.yml | 174 + .scripts/ci/collect-macos-crash-reports.sh | 50 + Cargo.lock | 94 +- examples/api/src-tauri/Cargo.toml | 10 + examples/api/src-tauri/capabilities/base.json | 39 +- .../api/src-tauri/capabilities/desktop.json | 2 + examples/api/src-tauri/gen/apple/Podfile | 2 +- .../gen/apple/api.xcodeproj/project.pbxproj | 12 +- examples/api/src-tauri/gen/apple/project.yml | 6 +- examples/api/src-tauri/src/lib.rs | 22 +- examples/api/src-tauri/tauri.conf.json | 2 +- examples/api/src/views/Store.svelte | 2 +- package.json | 4 + packages/api-e2e/README.md | 242 + packages/api-e2e/package.json | 54 + packages/api-e2e/tauri.e2e.conf.json | 13 + packages/api-e2e/test/helpers/index.ts | 329 + packages/api-e2e/test/helpers/server.ts | 116 + packages/api-e2e/test/specs/app.spec.ts | 71 + packages/api-e2e/test/specs/cli.spec.ts | 33 + .../test/specs/clipboard-manager.spec.ts | 153 + packages/api-e2e/test/specs/fs.spec.ts | 380 ++ .../test/specs/global-shortcut.spec.ts | 88 + packages/api-e2e/test/specs/http.spec.ts | 149 + packages/api-e2e/test/specs/log.spec.ts | 188 + .../api-e2e/test/specs/notification.spec.ts | 58 + packages/api-e2e/test/specs/opener.spec.ts | 46 + packages/api-e2e/test/specs/os.spec.ts | 120 + packages/api-e2e/test/specs/plugins.spec.ts | 215 + packages/api-e2e/test/specs/shell.spec.ts | 238 + packages/api-e2e/test/specs/store.spec.ts | 261 + packages/api-e2e/test/specs/updater.spec.ts | 77 + packages/api-e2e/test/specs/upload.spec.ts | 161 + .../api-e2e/test/specs/window-state.spec.ts | 130 + packages/api-e2e/test/types/global.d.ts | 18 + packages/api-e2e/tsconfig.json | 20 + packages/api-e2e/wdio.android.conf.ts | 10 + packages/api-e2e/wdio.conf.ts | 306 + packages/api-e2e/wdio.ios.conf.ts | 10 + packages/api-e2e/wdio.mobile.ts | 486 ++ plugins/notification/guest-js/init.ts | 10 +- plugins/notification/src/init-iife.js | 2 +- pnpm-lock.yaml | 5730 ++++++++++++++++- pnpm-workspace.yaml | 20 + 47 files changed, 10292 insertions(+), 107 deletions(-) create mode 100644 .changes/fix-notification-windows-permission.md create mode 100644 .github/workflows/test-api-e2e-mobile.yml create mode 100644 .github/workflows/test-api-e2e.yml create mode 100755 .scripts/ci/collect-macos-crash-reports.sh create mode 100644 packages/api-e2e/README.md create mode 100644 packages/api-e2e/package.json create mode 100644 packages/api-e2e/tauri.e2e.conf.json create mode 100644 packages/api-e2e/test/helpers/index.ts create mode 100644 packages/api-e2e/test/helpers/server.ts create mode 100644 packages/api-e2e/test/specs/app.spec.ts create mode 100644 packages/api-e2e/test/specs/cli.spec.ts create mode 100644 packages/api-e2e/test/specs/clipboard-manager.spec.ts create mode 100644 packages/api-e2e/test/specs/fs.spec.ts create mode 100644 packages/api-e2e/test/specs/global-shortcut.spec.ts create mode 100644 packages/api-e2e/test/specs/http.spec.ts create mode 100644 packages/api-e2e/test/specs/log.spec.ts create mode 100644 packages/api-e2e/test/specs/notification.spec.ts create mode 100644 packages/api-e2e/test/specs/opener.spec.ts create mode 100644 packages/api-e2e/test/specs/os.spec.ts create mode 100644 packages/api-e2e/test/specs/plugins.spec.ts create mode 100644 packages/api-e2e/test/specs/shell.spec.ts create mode 100644 packages/api-e2e/test/specs/store.spec.ts create mode 100644 packages/api-e2e/test/specs/updater.spec.ts create mode 100644 packages/api-e2e/test/specs/upload.spec.ts create mode 100644 packages/api-e2e/test/specs/window-state.spec.ts create mode 100644 packages/api-e2e/test/types/global.d.ts create mode 100644 packages/api-e2e/tsconfig.json create mode 100644 packages/api-e2e/wdio.android.conf.ts create mode 100644 packages/api-e2e/wdio.conf.ts create mode 100644 packages/api-e2e/wdio.ios.conf.ts create mode 100644 packages/api-e2e/wdio.mobile.ts diff --git a/.changes/fix-notification-windows-permission.md b/.changes/fix-notification-windows-permission.md new file mode 100644 index 000000000..8dc1a4034 --- /dev/null +++ b/.changes/fix-notification-windows-permission.md @@ -0,0 +1,6 @@ +--- +notification: patch +notification-js: patch +--- + +Fixed `isPermissionGranted()` always resolving to `false` on Windows. The initialization script short-circuited the permission check to avoid invoking the backend, but read the permission back before it had been set, so it settled on `denied` instead of `granted`. diff --git a/.github/workflows/lint-javascript.yml b/.github/workflows/lint-javascript.yml index c3fce74c1..c6dbce4d4 100644 --- a/.github/workflows/lint-javascript.yml +++ b/.github/workflows/lint-javascript.yml @@ -12,6 +12,7 @@ on: paths: - '.github/workflows/lint-javascript.yml' - 'plugins/*/guest-js/**' + - 'packages/**' - '.eslintignore' - '.eslintrc.json' - '.prettierignore' @@ -23,6 +24,7 @@ on: paths: - '.github/workflows/lint-javascript.yml' - 'plugins/*/guest-js/**' + - 'packages/**' - '.eslintignore' - '.eslintrc.json' - '.prettierignore' @@ -47,5 +49,11 @@ jobs: with: run_install: true cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + # the type-checked lint rules (and packages/api-e2e) resolve the plugins' + # types from their dist-js build output + - name: build + run: pnpm build - name: eslint run: pnpm lint + - name: ts:check + run: pnpm ts:check diff --git a/.github/workflows/test-api-e2e-mobile.yml b/.github/workflows/test-api-e2e-mobile.yml new file mode 100644 index 000000000..301a5a09c --- /dev/null +++ b/.github/workflows/test-api-e2e-mobile.yml @@ -0,0 +1,232 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: test plugins e2e (mobile) + +on: + workflow_dispatch: + push: + branches: + - v2 + paths: + - '.github/workflows/test-api-e2e-mobile.yml' + - 'packages/api-e2e/**' + - 'examples/api/**' + - 'plugins/*/guest-js/**' + - 'plugins/*/src/**' + - 'plugins/*/android/**' + - 'plugins/*/ios/**' + - 'plugins/*/permissions/**' + - 'plugins/*/build.rs' + pull_request: + branches: + - v2 + paths: + - '.github/workflows/test-api-e2e-mobile.yml' + - 'packages/api-e2e/**' + - 'examples/api/**' + - 'plugins/*/guest-js/**' + - 'plugins/*/src/**' + - 'plugins/*/android/**' + - 'plugins/*/ios/**' + - 'plugins/*/permissions/**' + - 'plugins/*/build.rs' + +env: + RUST_BACKTRACE: 1 + CARGO_PROFILE_DEV_DEBUG: 0 # keeps the target folder small for better cache efficiency + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + android: + runs-on: ubuntu-latest + env: + # The emulator runs the host architecture, so only that target is built. + E2E_ANDROID_TARGET: x86_64 + + steps: + - uses: actions/checkout@v7 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-linux-android + + - name: setup node + uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + # Same rule as the rust-cache step below: every run restores the Gradle cache and only + # `v2`/`v3` save it. setup-java's own `cache: gradle` has no such switch, so the restore + # and save steps are spelled out here (same paths and key inputs as setup-java uses). + - name: restore gradle cache + id: gradle-cache + uses: actions/cache/restore@v6 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: gradle-${{ runner.os }}- + + - name: Setup NDK + uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r25 + local-cache: true + + # TODO check after https://github.com/nttld/setup-ndk/issues/518 is fixed + - name: Restore Android Symlinks + run: | + directory="${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin" + find "$directory" -type l | while read link; do + current_target=$(readlink "$link") + new_target="$directory/$(basename "$current_target")" + ln -sf "$new_target" "$link" + echo "Changed $(basename "$link") from $current_target to $new_target" + done + + # Hardware acceleration for the emulator. + - name: enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - uses: Swatinem/rust-cache@v2 + with: + key: android + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} + + - name: install dependencies + run: pnpm i --frozen-lockfile + + - name: build plugins + run: pnpm build + + # Built before the emulator is up so it does not sit idle (and time out) + # during the Rust and Gradle builds; the suite then reuses the APK. + - name: build the APK + working-directory: ./examples/api + env: + NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + run: pnpm tauri android build --debug --apk --target $E2E_ANDROID_TARGET + + - name: save gradle cache + if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && steps.gradle-cache.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v6 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ steps.gradle-cache.outputs.cache-primary-key }} + + - name: run e2e tests + uses: reactivecircus/android-emulator-runner@v2 + timeout-minutes: 60 + with: + api-level: 35 + arch: x86_64 + # google_apis images ship an up-to-date Android System WebView. + target: google_apis + profile: pixel_6 + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: E2E_SKIP_BUILD=1 E2E_SPEC_RETRIES=1 pnpm test:api-e2e:android + + - name: upload appium logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: android-appium-logs + path: packages/api-e2e/logs + if-no-files-found: ignore + + ios: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v7 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-ios-sim + + - name: setup node + uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + + - uses: Swatinem/rust-cache@v2 + with: + key: ios + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} + + - name: install dependencies + run: pnpm i --frozen-lockfile + + - name: build plugins + run: pnpm build + + # Simulator build (`aarch64-sim` on the Apple Silicon runners), unsigned. + # Built ahead of the suite for symmetry with the Android job. + - name: build the simulator app + working-directory: ./examples/api + run: pnpm tauri ios build --debug --target aarch64-sim --no-sign + + # wdio.ios.conf picks the iPhone simulator on the newest installed runtime + # (no device name is hardcoded) and Appium boots it; the XCUITest driver + # compiles WebDriverAgent on the first session. + - name: run e2e tests + timeout-minutes: 60 + env: + E2E_SKIP_BUILD: '1' + E2E_SPEC_RETRIES: '1' + run: pnpm test:api-e2e:ios + + - name: upload appium logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: ios-appium-logs + path: packages/api-e2e/logs + if-no-files-found: ignore + + - name: collect app crash reports + if: failure() + run: .scripts/ci/collect-macos-crash-reports.sh "$RUNNER_TEMP/crash-reports" + + - name: upload app crash reports + if: failure() + uses: actions/upload-artifact@v7 + with: + name: ios-crash-reports + path: ${{ runner.temp }}/crash-reports + if-no-files-found: ignore diff --git a/.github/workflows/test-api-e2e.yml b/.github/workflows/test-api-e2e.yml new file mode 100644 index 000000000..f5316a362 --- /dev/null +++ b/.github/workflows/test-api-e2e.yml @@ -0,0 +1,174 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: test plugins e2e + +on: + workflow_dispatch: + push: + branches: + - v2 + paths: + - '.github/workflows/test-api-e2e.yml' + - 'packages/api-e2e/**' + - 'examples/api/**' + - 'plugins/*/guest-js/**' + - 'plugins/*/src/**' + - 'plugins/*/permissions/**' + - 'plugins/*/build.rs' + pull_request: + branches: + - v2 + paths: + - '.github/workflows/test-api-e2e.yml' + - 'packages/api-e2e/**' + - 'examples/api/**' + - 'plugins/*/guest-js/**' + - 'plugins/*/src/**' + - 'plugins/*/permissions/**' + - 'plugins/*/build.rs' + +env: + RUST_BACKTRACE: 1 + CARGO_PROFILE_DEV_DEBUG: 0 # keeps the target folder small for better cache efficiency + MSEDGEDRIVER_TOOL_REV: 8c4b34f51b45f5cf08013366d703de464ab871d1 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: ${{ matrix.platform.os }} + + strategy: + fail-fast: false + matrix: + platform: + - { name: Linux, os: ubuntu-latest } + - { name: Windows, os: windows-latest } + - { name: macOS, os: macos-latest } + + steps: + - uses: actions/checkout@v7 + + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: setup node + uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@v6 + with: + cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + + - name: install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + webkit2gtk-driver \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + dbus \ + xvfb \ + fluxbox + + # msedgedriver has to match the installed WebView2 runtime, so msedgedriver-tool + # downloads the right version at run time. The tool itself is built from a pinned git + # revision and kept in its own small cache entry (accessed by every Windows run, so it + # stays hot) instead of being rebuilt from source on every run. + - name: restore msedgedriver-tool (Windows) + if: runner.os == 'Windows' + id: msedgedriver-tool-cache + uses: actions/cache@v6 + with: + path: ${{ runner.tool_cache }}/msedgedriver-tool + key: msedgedriver-tool-${{ runner.os }}-${{ env.MSEDGEDRIVER_TOOL_REV }} + + - name: install msedgedriver-tool (Windows) + if: runner.os == 'Windows' && steps.msedgedriver-tool-cache.outputs.cache-hit != 'true' + run: cargo install --git https://github.com/chippers/msedgedriver-tool --rev ${{ env.MSEDGEDRIVER_TOOL_REV }} --locked --root ${{ runner.tool_cache }}/msedgedriver-tool + + - name: install msedgedriver (Windows) + if: runner.os == 'Windows' + run: | + & "${{ runner.tool_cache }}/msedgedriver-tool/bin/msedgedriver-tool.exe" + $PWD.Path >> $env:GITHUB_PATH + + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.platform.os }} + # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its + # merge ref, where nothing else can restore it, and every write evicts an entry + # that other runs would have hit (10 GB repository limit, LRU eviction). + save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} + + - name: install dependencies + run: pnpm i --frozen-lockfile + + - name: build plugins + run: pnpm build + + - name: run e2e tests (Linux) + if: runner.os == 'Linux' + timeout-minutes: 45 + # A real window manager (fluxbox) is started so that window-state tests + # (size restore) behave, and a session D-Bus so the notification and + # clipboard plugins can reach their desktop services. + env: + E2E_SPEC_RETRIES: '1' + run: | + xvfb-run --auto-servernum -- dbus-run-session -- bash -c ' + fluxbox >/dev/null 2>&1 & + sleep 1 + pnpm test:api-e2e + ' + + - name: run e2e tests (Windows) + if: runner.os == 'Windows' + timeout-minutes: 45 + shell: pwsh + # msedgedriver hands the app the `--remote-debugging-port` it attaches to through + # `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`, and WebView2 ignores that variable in an + # elevated process, which is what the runner gives us. gsudo runs the suite at medium + # integrity instead, and the workspace is opened up so the de-elevated process can use it. + # See https://github.com/tauri-apps/wry/issues/1782 and + # https://github.com/MicrosoftEdge/WebView2Feedback/issues/5645. + env: + E2E_SPEC_RETRIES: '1' + run: | + winget install gerardog.gsudo --accept-source-agreements --accept-package-agreements + icacls "$env:GITHUB_WORKSPACE" /grant "Everyone:(OI)(CI)F" + sudo --integrity Medium pnpm test:api-e2e + + - name: run e2e tests (macOS) + if: runner.os == 'macOS' && env.CN_API_KEY != '' + timeout-minutes: 45 + env: + CN_API_KEY: ${{ secrets.TAURI_E2E_CN_API_KEY }} + E2E_SPEC_RETRIES: '1' + # macOS has no native WebDriver for WKWebView, so the app is driven through the + # CrabNebula Webdriver (tauri-plugin-automation + test-runner-backend), which + # authenticates with CN_API_KEY. wdio.conf enables it automatically on darwin and + # builds the `.app` bundle it needs. + run: pnpm test:api-e2e + + - name: collect app crash reports (macOS) + if: runner.os == 'macOS' && failure() + run: .scripts/ci/collect-macos-crash-reports.sh "$RUNNER_TEMP/crash-reports" + + - name: upload app crash reports (macOS) + if: runner.os == 'macOS' && failure() + uses: actions/upload-artifact@v7 + with: + name: macos-crash-reports + path: ${{ runner.temp }}/crash-reports + if-no-files-found: ignore diff --git a/.scripts/ci/collect-macos-crash-reports.sh b/.scripts/ci/collect-macos-crash-reports.sh new file mode 100755 index 000000000..17067077a --- /dev/null +++ b/.scripts/ci/collect-macos-crash-reports.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +# Collects and prints macOS crash reports after a failed plugins e2e run. +# +# The CrabNebula Webdriver proxies every command to a server running *inside* the app +# process, so when the app dies the suite only ever reports `connection refused` — the +# actual cause is never in the job log. The app's stdout/stderr is inherited and no panic +# message is printed, which means it goes down on a signal. macOS records the reason in a +# crash report, and that is the only place it survives the run. +# +# Copies every report into $1 (default: $RUNNER_TEMP/crash-reports) for upload. + +set -euo pipefail + +dest="${1:-${RUNNER_TEMP:-/tmp}/crash-reports}" +mkdir -p "$dest" + +# ReportCrash writes asynchronously; give the last crash a moment to land. +sleep 5 + +found=0 +for f in "$HOME/Library/Logs/DiagnosticReports"/*.ips \ + "$HOME/Library/Logs/DiagnosticReports/Retired"/*.ips; do + [ -e "$f" ] || continue + found=$((found + 1)) + cp "$f" "$dest/" + echo "::group::$(basename "$f")" + # An .ips file is a one-line JSON header followed by the JSON report body. + head -n 1 "$f" + tail -n +2 "$f" | jq -r ' + "exception: \(.exception // {} | tojson)", + "termination: \(.termination // {} | tojson)", + "asi: \(.asi // {} | tojson)", + "faulting thread \(.faultingThread // 0):", + (. as $r + | $r.threads[$r.faultingThread // 0].frames[]? + | " \($r.usedImages[.imageIndex].name // "?") \(.symbol // "?") +\(.imageOffset)"), + "last ObjC exception:", + (. as $r + | $r.lastExceptionBacktrace[]? + | " \($r.usedImages[.imageIndex].name // "?") \(.symbol // "?") +\(.imageOffset)") + ' || cat "$f" + echo "::endgroup::" +done + +[ "$found" -gt 0 ] || echo "no crash reports under $HOME/Library/Logs/DiagnosticReports" diff --git a/Cargo.lock b/Cargo.lock index 9ede4439e..1830efd8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,6 +214,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-automation", "tauri-plugin-barcode-scanner", "tauri-plugin-biometric", "tauri-plugin-cli", @@ -972,7 +973,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -1416,6 +1417,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + [[package]] name = "deep-link-example" version = "0.0.0" @@ -1585,7 +1597,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading", + "libloading 0.7.4", ] [[package]] @@ -3329,7 +3341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -3339,6 +3351,15 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libflate" version = "2.1.0" @@ -3373,6 +3394,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libm" version = "0.2.11" @@ -3826,7 +3857,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.100", @@ -3875,10 +3906,15 @@ checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ "bitflags 2.9.0", "block2 0.6.2", + "libc", "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", "objc2-core-graphics", + "objc2-core-image", "objc2-foundation 0.3.0", + "objc2-quartz-core 0.3.0", ] [[package]] @@ -3898,6 +3934,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" dependencies = [ + "bitflags 2.9.0", "objc2 0.6.4", "objc2-foundation 0.3.0", ] @@ -6246,6 +6283,7 @@ dependencies = [ "core-foundation 0.10.0", "core-graphics 0.25.0", "crossbeam-channel", + "dbus", "dispatch2", "dlopen2", "dpi", @@ -6393,6 +6431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", + "brotli", "ico", "json-patch", "plist", @@ -6442,6 +6481,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-automation" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "314d4ba88ce4a5e17c5f1e2dad990edb5956643b6e04a6d127d39aa64d0f2a7d" +dependencies = [ + "libloading 0.8.9", + "objc2-app-kit", + "serde_json", + "tauri", + "tauri-plugin", + "tokio", + "tracing", +] + [[package]] name = "tauri-plugin-autostart" version = "2.5.1" @@ -6990,6 +7044,7 @@ checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "aes-gcm", "anyhow", + "brotli", "cargo_metadata", "ctor", "dom_query", @@ -8234,7 +8289,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.1", "windows-numerics", ] @@ -8255,7 +8310,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.1", "windows-result", "windows-strings", ] @@ -8267,7 +8322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -8298,6 +8353,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -8305,7 +8366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -8314,7 +8375,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e" dependencies = [ - "windows-link", + "windows-link 0.1.1", "windows-result", "windows-strings", ] @@ -8325,7 +8386,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -8334,7 +8395,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -8382,6 +8443,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -8450,7 +8520,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index dc15465c3..24f576f9f 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -40,6 +40,12 @@ tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.6" } tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.5" } tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" } +# WebDriver automation bridge, used by the plugins e2e suite (packages/api-e2e). +# Desktop-only and behind the off-by-default `automation` feature so it never ships in a +# regular build. +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +tauri-plugin-automation = { version = "0.1.4", optional = true } + [dependencies.tauri] workspace = true features = [ @@ -66,3 +72,7 @@ tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.3.6" } tauri-plugin-biometric = { path = "../../../plugins/biometric/", version = "2.3.3" } tauri-plugin-geolocation = { path = "../../../plugins/geolocation/", version = "2.3.3" } tauri-plugin-haptics = { path = "../../../plugins/haptics/", version = "2.3.3" } + +[features] +# Enables the WebDriver automation bridge; set by the e2e suite's build (packages/api-e2e). +automation = ["dep:tauri-plugin-automation"] diff --git a/examples/api/src-tauri/capabilities/base.json b/examples/api/src-tauri/capabilities/base.json index 637bfa496..773c0ec58 100644 --- a/examples/api/src-tauri/capabilities/base.json +++ b/examples/api/src-tauri/capabilities/base.json @@ -18,10 +18,13 @@ "core:app:allow-set-app-theme", "core:window:allow-minimize", "core:window:allow-toggle-maximize", + "core:window:allow-set-size", "core:window:allow-close", + "core:webview:allow-create-webview-window", "core:window:allow-start-dragging", "notification:default", - "os:allow-platform", + "os:default", + "os:allow-hostname", "dialog:default", { "identifier": "shell:allow-spawn", @@ -48,6 +51,31 @@ } ] }, + { + "identifier": "shell:allow-execute", + "allow": [ + { + "name": "sh", + "cmd": "sh", + "args": [ + "-c", + { + "validator": ".+" + } + ] + }, + { + "name": "cmd", + "cmd": "cmd", + "args": [ + "/C", + { + "validator": ".+" + } + ] + } + ] + }, "shell:default", "shell:allow-kill", "shell:allow-stdin-write", @@ -57,6 +85,8 @@ "clipboard-manager:allow-write-text", "clipboard-manager:allow-read-image", "clipboard-manager:allow-write-image", + "clipboard-manager:allow-write-html", + "clipboard-manager:allow-clear", "fs:default", "fs:read-meta", "fs:allow-open", @@ -66,6 +96,13 @@ "fs:allow-mkdir", "fs:allow-remove", "fs:allow-write-text-file", + "fs:allow-write-file", + "fs:allow-create", + "fs:allow-copy-file", + "fs:allow-seek", + "fs:allow-truncate", + "fs:allow-ftruncate", + "fs:allow-unwatch", "fs:scope-download-recursive", "fs:scope-resource-recursive", { diff --git a/examples/api/src-tauri/capabilities/desktop.json b/examples/api/src-tauri/capabilities/desktop.json index 2c6fcf8ad..425ecb973 100644 --- a/examples/api/src-tauri/capabilities/desktop.json +++ b/examples/api/src-tauri/capabilities/desktop.json @@ -10,6 +10,8 @@ "global-shortcut:allow-unregister", "global-shortcut:allow-register", "global-shortcut:allow-unregister-all", + "global-shortcut:allow-is-registered", + "window-state:default", { "identifier": "fs:allow-watch", "allow": ["*", "**/*"] } ] } diff --git a/examples/api/src-tauri/gen/apple/Podfile b/examples/api/src-tauri/gen/apple/Podfile index 24605d24a..f4d3303b8 100644 --- a/examples/api/src-tauri/gen/apple/Podfile +++ b/examples/api/src-tauri/gen/apple/Podfile @@ -1,7 +1,7 @@ # Uncomment the next line to define a global platform for your project target 'api_iOS' do -platform :ios, '14.0' +platform :ios, '15.0' # Pods for api_iOS end diff --git a/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj index 11be7126e..e01e3a610 100644 --- a/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj +++ b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj @@ -304,7 +304,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -366,7 +366,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -400,8 +400,8 @@ "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri API"; SDKROOT = iphoneos; @@ -432,8 +432,8 @@ "$(inherited)", "@executable_path/Frameworks", ); - "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; - "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; PRODUCT_BUNDLE_IDENTIFIER = com.tauri.api; PRODUCT_NAME = "Tauri API"; SDKROOT = iphoneos; diff --git a/examples/api/src-tauri/gen/apple/project.yml b/examples/api/src-tauri/gen/apple/project.yml index d789500eb..55c95eff2 100644 --- a/examples/api/src-tauri/gen/apple/project.yml +++ b/examples/api/src-tauri/gen/apple/project.yml @@ -2,7 +2,7 @@ name: api options: bundleIdPrefix: com.tauri.api deploymentTarget: - iOS: 14.0 + iOS: 15.0 fileGroups: [../../src] configs: debug: debug @@ -65,8 +65,8 @@ targets: ENABLE_BITCODE: false ARCHS: [arm64] VALID_ARCHS: arm64 - LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) - LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64 groups: [app] diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index 3c58f2c81..11a375684 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -23,10 +23,25 @@ pub type OnEvent = Box; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { #[allow(unused_mut)] - let mut builder = tauri::Builder::default() + let mut builder = tauri::Builder::default(); + + // WebDriver automation bridge for the plugins e2e suite (packages/api-e2e). + // Registered as early as possible per the plugin's docs, behind the + // off-by-default `automation` feature. + #[cfg(all(desktop, feature = "automation"))] + { + builder = builder.plugin(tauri_plugin_automation::init()); + } + + #[allow(unused_mut)] + let mut builder = builder .plugin( tauri_plugin_log::Builder::default() .level(log::LevelFilter::Info) + // forward records to the webview so `attachLogger`/`attachConsole` work + .target(tauri_plugin_log::Target::new( + tauri_plugin_log::TargetKind::Webview, + )) .build(), ) .plugin(tauri_plugin_fs::init()) @@ -86,10 +101,7 @@ pub fn run() { webview_window_builder = webview_window_builder.transparent(true); } - let webview = webview_window_builder.build().unwrap(); - - #[cfg(debug_assertions)] - webview.open_devtools(); + let _webview = webview_window_builder.build().unwrap(); std::thread::spawn(|| { let server = match tiny_http::Server::http("localhost:3003") { diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json index be4918de3..e30e3364a 100644 --- a/examples/api/src-tauri/tauri.conf.json +++ b/examples/api/src-tauri/tauri.conf.json @@ -107,7 +107,7 @@ } }, "iOS": { - "minimumSystemVersion": "14.0" + "minimumSystemVersion": "15.0" } } } diff --git a/examples/api/src/views/Store.svelte b/examples/api/src/views/Store.svelte index b03f563db..6ba9a9a59 100644 --- a/examples/api/src/views/Store.svelte +++ b/examples/api/src/views/Store.svelte @@ -61,7 +61,7 @@ async function reload() { try { - await store.reload({ overrideDefaults: true }) + await store.reload({ ignoreDefaults: true }) } catch (error) { onMessage(error) } diff --git a/package.json b/package.json index 7fe38ce34..73d79771f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,10 @@ "lint": "eslint .", "format": "prettier --write .", "format:check": "prettier --check .", + "ts:check": "pnpm run -r ts:check", + "test:api-e2e": "pnpm run --filter \"api-e2e\" e2e", + "test:api-e2e:android": "pnpm run --filter \"api-e2e\" e2e:android", + "test:api-e2e:ios": "pnpm run --filter \"api-e2e\" e2e:ios", "example:api:dev": "pnpm run --filter \"api\" tauri dev" }, "devDependencies": { diff --git a/packages/api-e2e/README.md b/packages/api-e2e/README.md new file mode 100644 index 000000000..b5e63929d --- /dev/null +++ b/packages/api-e2e/README.md @@ -0,0 +1,242 @@ +# Plugins end-to-end tests + +WebdriverIO suite that exercises the JavaScript API of the plugins in this +repository against a **real** Tauri app — the [`examples/api`](../../examples/api) +validation app — rather than a mocked backend, on desktop (Linux, macOS, Windows) +and mobile (Android, iOS). Each plugin has its own spec file, shared by every +platform, and adding coverage for a new plugin API is normally just dropping in +one more spec. + +It mirrors the [`@tauri-apps/api` e2e suite](https://github.com/tauri-apps/tauri/tree/dev/packages/api-e2e) +in the core repository; the `@tauri-apps/api` modules themselves are covered there. + +## How it works + +- The example app is built with `withGlobalTauri: true`, so the `@tauri-apps/api` surface + is reachable on `window.__TAURI__` inside the webview, and every plugin's `api-iife.js` + registers its API next to it (`window.__TAURI__.fs`, `window.__TAURI__.clipboardManager`, + …, the package name without the `@tauri-apps/plugin-` prefix, camel-cased). +- On desktop, WebdriverIO drives the app through [`@crabnebula/tauri-driver`](https://www.npmjs.com/package/@crabnebula/tauri-driver), + which bridges the WebDriver protocol to each platform's webview: + - **macOS** — the CrabNebula Webdriver, which needs [`tauri-plugin-automation`](https://crates.io/crates/tauri-plugin-automation) + (registered in `examples/api` behind its off-by-default `automation` Cargo feature, which + the suite's build enables) and a locally-running `@crabnebula/test-runner-backend`, + authenticated with `CN_API_KEY`. + - **Linux** — `webkit2gtk-driver` (`WebKitWebDriver` on `PATH`). + - **Windows** — `msedgedriver.exe` on `PATH`. It hands the app the `--remote-debugging-port` + it attaches to through `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`, which WebView2 ignores in an + elevated process ([wry#1782](https://github.com/tauri-apps/wry/issues/1782)), so the suite has + to run unelevated. +- On mobile, WebdriverIO drives the app through [Appium](https://appium.io) (started by + `@wdio/appium-service`; the drivers are plain devDependencies of this package, which Appium + picks up on its own): + - **Android** — the UiAutomator2 driver. The suite switches to the app's `WEBVIEW_*` + context, which chromedriver reaches through the WebView's debugging socket. Debug builds + turn that on (`setWebContentsDebuggingEnabled`), so the suite builds a debug APK. A + chromedriver matching the device's WebView is downloaded on demand (see `E2E_CHROMEDRIVER`). + - **iOS** — the XCUITest driver on a simulator, attaching to the WKWebView through the + WebKit remote inspector. Debug builds mark the webview `isInspectable`, so the suite + builds an unsigned debug simulator app. The inspector identifies an app by the + `application-identifier` entitlement that Xcode embeds when it code signs a simulator + build; an unsigned one has none and is listed as `process-` instead of + its bundle identifier, so the config has the driver match that name too + (`appium:additionalWebviewBundleIds`). The driver also starts with a script timeout of + 0, which the config raises to the 30s the other drivers default to, or every + `executeAsync` would time out at once. +- Specs never `eval` in the page. They pass a function to the [`tauri()`](test/helpers/index.ts) + helper, which serializes it and runs it via the driver's own (CSP-exempt) script injection, + handing it `window.__TAURI__` as the first argument and returning its JSON result. +- Each spec file gets its own session — a fresh `tauri-driver` (and therefore a fresh app + instance) on desktop, a fresh Appium session (which relaunches the app) on mobile — so + each plugin's suite runs in isolation. +- A small [fixture server](test/helpers/server.ts) is started for the whole run. It serves + the updater manifest the desktop e2e build points the updater at (see + [`tauri.e2e.conf.json`](tauri.e2e.conf.json)) and the upload/download fixtures. It listens + on the host's loopback; the iOS simulator shares that network stack, and on Android the + mobile config runs `adb reverse` so the same `127.0.0.1` URL works on the device. The + `http` specs use the echo server the example app itself spawns on port 3003, since that + is the only `http://` origin in the example's http scope. + +## What is (not) covered + +The example only registers each plugin on the platforms it supports, so the suites +split three ways: desktop-only plugins are skipped on mobile, mobile-only plugins are +skipped on desktop, and the rest run everywhere with the odd test gated. + +| Plugin | Coverage | On mobile | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| `cli` | `getMatches` shape for an argument-less launch. | Skipped — desktop-only plugin. | +| `clipboard-manager` | text, HTML and image round-trips, `clear`, error paths. | Text only; HTML and images are unsupported there. | +| `dialog` | Only that the API is registered — every dialog blocks on native UI the driver cannot operate. | Same. | +| `fs` | read/write/stat/copy/rename/remove/truncate, `FileHandle`, line iteration, watchers, scope. | All but the watchers (`fs:allow-watch` is desktop-only here). | +| `global-shortcut` | register/unregister/isRegistered/unregisterAll and error paths. Shortcuts cannot be triggered. | Skipped — desktop-only plugin. | +| `http` | `fetch` methods, headers, JSON/bytes/multipart bodies, the cookie jar, abort, scope, failures. | Same. | +| `log` | `attachLogger`/`attachConsole` for webview and Rust records, level filtering. | Same. | +| `notification` | Permission model, `window.Notification` override, `sendNotification`. Display is not observable. | Sending only — see the note below. | +| `opener` | Scope enforcement only — a successful open launches an external app the suite cannot close. | Same. | +| `os` | Every function, compared against what the app was built for. | Same, minus the hostname/host comparison. | +| `process` | Only that the API is registered — `exit`/`relaunch` terminate the app under test. | Same. | +| `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. | +| `store` | CRUD, persistence, auto-save, defaults/reset, reload, `getStore`, `LazyStore`, change events. | Same. | +| `updater` | `check` against the fixture manifest (update / 204 / older release). Installing is never exercised. | Skipped — desktop-only plugin. | +| `upload` | `download` and `upload` with progress, methods, headers and error paths. | Same (through `adb reverse` on Android). | +| `window-state` | `filename`, save/restore, and that a re-created window gets its saved size (WM dependent). | Skipped — desktop-only plugin. | +| mobile-only plugins | `barcode-scanner`, `biometric`, `geolocation`, `haptics` and `nfc` need hardware or native UI the driver cannot operate, so only their global API surface is asserted. | Only there. | + +The notification permission specs are desktop-only: a mobile app starts out ungranted and +`requestPermission` puts up a system dialog the session would then block on. + +The [`plugins.spec.ts`](test/specs/plugins.spec.ts) spec additionally asserts that every +plugin the platform registers injects its global API with its documented members, and that +the _other_ platform's plugins are absent. That is what catches a plugin whose +`global_api_script_path` is missing from its `build.rs`, or a crate that is not target-gated +in the example's `Cargo.toml`. + +## Prerequisites + +```sh +# from the repo root +pnpm install +pnpm build # examples/api resolves the plugins' JS from their dist-js output +``` + +Platform driver dependencies: + +| Platform | Requirement | +| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| macOS | `CN_API_KEY` env var (CrabNebula Cloud). The automation plugin and test-runner-backend are wired up already. | +| Linux | `webkit2gtk-driver` package (provides `WebKitWebDriver`). | +| Windows | `msedgedriver.exe` matching your Edge version, on `PATH`. Run the suite unelevated. | +| Android | The usual Tauri Android setup (`ANDROID_HOME`, `NDK_HOME`, a JDK), which also provides the `adb` the suite shells out to, plus a running emulator or a connected device with USB debugging. Network access the first time, for the chromedriver download. | +| iOS | macOS with Xcode and an iOS simulator runtime. The first session compiles WebDriverAgent (a few minutes). | + +## Running + +```sh +# desktop, from the repo root +pnpm test:api-e2e + +# or from this package +pnpm e2e + +# iterate without rebuilding the app every run +E2E_SKIP_BUILD=1 pnpm e2e + +# run a single plugin's spec +pnpm exec wdio run ./wdio.conf.ts --spec test/specs/fs.spec.ts + +# mobile (from the repo root; or `pnpm e2e:android` / `pnpm e2e:ios` from this package) +pnpm test:api-e2e:android +pnpm test:api-e2e:ios +``` + +The first desktop run builds the app with [`tauri.e2e.conf.json`](tauri.e2e.conf.json) as a +config override, which enables the example's `automation` feature and points the updater at +the fixture server; afterwards use `E2E_SKIP_BUILD=1` to reuse the existing binary. A binary +supplied through `E2E_SKIP_BUILD` or `E2E_APP_PATH` must have been built with that override: +the `updater` specs rely on its endpoint, and the CrabNebula Webdriver (always on macOS) +relies on the automation feature. + +The mobile configs ([`wdio.android.conf.ts`](wdio.android.conf.ts), [`wdio.ios.conf.ts`](wdio.ios.conf.ts), +sharing [`wdio.mobile.ts`](wdio.mobile.ts)) run `tauri android build --debug --apk` / +`tauri ios build --debug --target aarch64-sim --no-sign`, compiling only the Rust target the +device runs (the Android one is read from the connected device through `adb`). The Android +Studio and Xcode projects are committed under `examples/api/src-tauri/gen`, so they are only +initialized if that directory is missing. The app must be a **debug** build — release builds +have webview debugging off, and Appium cannot see the page. `E2E_SKIP_BUILD` and +`E2E_APP_PATH` (an `.apk` / simulator `.app`) work as on desktop. No config override is +passed there: both things it turns on belong to desktop-only plugins. + +## Environment variables + +| Variable | Purpose | +| ------------------- | ---------------------------------------------------------------------------------------- | +| `CN_API_KEY` | CrabNebula Cloud key. Required on macOS (and whenever `E2E_CN_WEBDRIVER=1`). | +| `E2E_SKIP_BUILD` | Skip the `tauri build` step and reuse the existing binary. | +| `E2E_APP_PATH` | Absolute path to a prebuilt app/binary to test (also implies skip-build). | +| `E2E_SKIP` | Comma-separated plugin names to skip, e.g. `E2E_SKIP=clipboard-manager,global-shortcut`. | +| `E2E_SKIP_WM` | Skip window-manager-dependent tests (the window-state size restore). | +| `E2E_SPEC_RETRIES` | Retry count for flaky spec files (default `0`). | +| `E2E_CN_WEBDRIVER` | Use the CrabNebula Webdriver on Linux/Windows too (instead of the native driver). | +| `E2E_NATIVE_DRIVER` | Path passed to `tauri-driver --native-driver` (e.g. a specific chromedriver). | +| `CARGO_TARGET_DIR` | Override the target dir the app binary is looked up in. | + +Mobile only: + +| Variable | Purpose | +| -------------------- | ---------------------------------------------------------------------------------------------------- | +| `E2E_ANDROID_TARGET` | Rust target for the APK (`aarch64`, `armv7`, `i686`, `x86_64`); default: the connected device's ABI. | +| `E2E_ANDROID_DEVICE` | `adb` serial of the device/emulator to use (`appium:udid`); default: the first connected one. | +| `E2E_ANDROID_AVD` | Name of an AVD for Appium to boot (`appium:avd`) instead of using an already-running emulator. | +| `E2E_CHROMEDRIVER` | chromedriver binary matching the device's WebView, instead of letting Appium download one. | +| `E2E_IOS_TARGET` | Rust target for the simulator app (`aarch64-sim` or `x86_64`); default: the host architecture. | +| `E2E_IOS_DEVICE` | Simulator UDID or name (as in `xcrun simctl list`); default: a booted iPhone, else the newest one. | +| `E2E_PLATFORM` | Set by the mobile configs for the spec workers (`android`/`ios`) — see `platform` in the helpers. | + +Appium's own log is written to `logs/wdio-appium.log` in this package. + +## Adding tests for a new plugin API + +1. **Add a spec.** Create `test/specs/.spec.ts` and use `describePlugin('', …)` + with the `tauri()` helper. It is picked up automatically by the `test/specs/**/*.spec.ts` + glob. Minimal example: + + ```ts + import { expect } from '@wdio/globals' + import { tauri, describePlugin } from '../helpers/index.js' + + describePlugin('os', () => { + it('reports the platform', async () => { + expect(await tauri((api) => api.os.platform())).toBe('linux') + }) + }) + ``` + + If the plugin is new to the example, register it in + [`examples/api/src-tauri/src/lib.rs`](../../examples/api/src-tauri/src/lib.rs), add it to + the example's `package.json`/`Cargo.toml`, add its `workspace:*` package to this package's + `devDependencies` and its namespace to the matching interface in + [`test/helpers/index.ts`](test/helpers/index.ts) — `CommonPluginApi`, `DesktopPluginApi` or + `MobilePluginApi` — plus the member list in `plugins.spec.ts`. + +2. **Grant permissions if needed.** If the API calls a command that the example does not + allow yet, add the permission to + [`examples/api/src-tauri/capabilities/base.json`](../../examples/api/src-tauri/capabilities/base.json) + (or `desktop.json`/`mobile.json` for platform-specific plugins). Each plugin documents its + permissions under `plugins//permissions/autogenerated/reference.md`. + +3. **Need a server?** Add a route to the [fixture server](test/helpers/server.ts) rather than + hitting the network; its URL is exported as `FIXTURE_SERVER_URL`. + +4. **Handle environment-sensitive cases.** Use `itWm` (instead of `it`) for assertions that + depend on a real window manager, and `eventually()` to poll for state that is applied + asynchronously. Branch on `platform` from the helpers (never `process.platform`, which is + the host running the emulator/simulator on mobile) for platform-specific behavior. + Files go under `scratchDir('')`, relative to `BaseDirectory.AppData`, which the + example's fs scope allows. + +5. **Gate what mobile does not have.** The same specs run on Android and iOS. Wrap tests of + desktop-only behavior — a command the mobile build does not expose, a mobile implementation + that answers "Unsupported on this platform", or a permission only the desktop capability + grants — in `itDesktop`, use `itOn('android', …)` / `itOn('ios', …)` for platform-specific + APIs, and pass `{ desktopOnly: true }` (or `{ mobileOnly: true }`) to `describePlugin` for + plugins the example does not register on the other side. Skipped tests show up as pending + rather than silently disappearing. + +### Rules for `tauri()` page functions + +The function you pass to `tauri()` runs **inside the webview**, serialized as a string: + +- It **cannot** close over anything from the spec module — pass every value it needs through + the trailing `tauri(fn, ...args)` arguments. +- It may only reference `api` (the `window.__TAURI__` object), those args, and browser + globals (`window`, `document`, `setTimeout`, `Promise`, …). +- Its return value must be JSON-serializable — return plain objects/primitives, not class + instances (call methods and return their results instead), and remember that `undefined` + values are dropped. +- Restore any app state you mutate (clipboard, registered shortcuts, window size, …); tests + within a spec file share the same app instance. +- For in-page waiting, wrap logic in a `Promise` with an explicit `setTimeout` rejection so a + failure surfaces as a message rather than an opaque driver timeout. + +Use `tauriError(fn, ...args)` to assert that a call rejects; it returns the rejection message. diff --git a/packages/api-e2e/package.json b/packages/api-e2e/package.json new file mode 100644 index 000000000..f0e29eac6 --- /dev/null +++ b/packages/api-e2e/package.json @@ -0,0 +1,54 @@ +{ + "name": "api-e2e", + "private": true, + "version": "0.0.0", + "description": "End-to-end test suite for the plugins' JavaScript APIs, driven against the examples/api app via WebdriverIO (tauri-driver on desktop, Appium on mobile).", + "license": "MIT OR Apache-2.0", + "type": "module", + "scripts": { + "e2e": "wdio run ./wdio.conf.ts", + "e2e:skip-build": "cross-env E2E_SKIP_BUILD=true wdio run ./wdio.conf.ts", + "e2e:android": "wdio run ./wdio.android.conf.ts", + "e2e:ios": "wdio run ./wdio.ios.conf.ts", + "ts:check": "tsc --noEmit" + }, + "devDependencies": { + "@crabnebula/tauri-driver": "^2.0.9", + "@crabnebula/test-runner-backend": "^0.2.9", + "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-barcode-scanner": "workspace:*", + "@tauri-apps/plugin-biometric": "workspace:*", + "@tauri-apps/plugin-cli": "workspace:*", + "@tauri-apps/plugin-clipboard-manager": "workspace:*", + "@tauri-apps/plugin-dialog": "workspace:*", + "@tauri-apps/plugin-fs": "workspace:*", + "@tauri-apps/plugin-geolocation": "workspace:*", + "@tauri-apps/plugin-global-shortcut": "workspace:*", + "@tauri-apps/plugin-haptics": "workspace:*", + "@tauri-apps/plugin-http": "workspace:*", + "@tauri-apps/plugin-log": "workspace:*", + "@tauri-apps/plugin-nfc": "workspace:*", + "@tauri-apps/plugin-notification": "workspace:*", + "@tauri-apps/plugin-opener": "workspace:*", + "@tauri-apps/plugin-os": "workspace:*", + "@tauri-apps/plugin-process": "workspace:*", + "@tauri-apps/plugin-shell": "workspace:*", + "@tauri-apps/plugin-store": "workspace:*", + "@tauri-apps/plugin-updater": "workspace:*", + "@tauri-apps/plugin-upload": "workspace:*", + "@tauri-apps/plugin-window-state": "workspace:*", + "@types/node": "^22.10.0", + "@wdio/appium-service": "^9.31.9", + "@wdio/cli": "^9.31.9", + "@wdio/globals": "^9.31.3", + "@wdio/local-runner": "^9.31.9", + "@wdio/mocha-framework": "^9.31.9", + "@wdio/spec-reporter": "^9.31.2", + "appium": "^3.7.0", + "appium-uiautomator2-driver": "^8.7.0", + "appium-xcuitest-driver": "^12.12.4", + "cross-env": "^10.1.0", + "typescript": "6.0.3", + "webdriverio": "^9.31.9" + } +} diff --git a/packages/api-e2e/tauri.e2e.conf.json b/packages/api-e2e/tauri.e2e.conf.json new file mode 100644 index 000000000..278624b3a --- /dev/null +++ b/packages/api-e2e/tauri.e2e.conf.json @@ -0,0 +1,13 @@ +{ + "$schema": "../../examples/api/node_modules/@tauri-apps/cli/config.schema.json", + "build": { + "features": ["automation"] + }, + "plugins": { + "updater": { + "endpoints": [ + "http://127.0.0.1:3004/updater/{{target}}/{{arch}}/{{current_version}}" + ] + } + } +} diff --git a/packages/api-e2e/test/helpers/index.ts b/packages/api-e2e/test/helpers/index.ts new file mode 100644 index 000000000..8177f5bba --- /dev/null +++ b/packages/api-e2e/test/helpers/index.ts @@ -0,0 +1,329 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { browser } from '@wdio/globals' +import type * as TauriApi from '@tauri-apps/api' +import type * as BarcodeScanner from '@tauri-apps/plugin-barcode-scanner' +import type * as Biometric from '@tauri-apps/plugin-biometric' +import type * as Cli from '@tauri-apps/plugin-cli' +import type * as ClipboardManager from '@tauri-apps/plugin-clipboard-manager' +import type * as Dialog from '@tauri-apps/plugin-dialog' +import type * as Fs from '@tauri-apps/plugin-fs' +import type * as Geolocation from '@tauri-apps/plugin-geolocation' +import type * as GlobalShortcut from '@tauri-apps/plugin-global-shortcut' +import type * as Haptics from '@tauri-apps/plugin-haptics' +import type * as Http from '@tauri-apps/plugin-http' +import type * as Log from '@tauri-apps/plugin-log' +import type * as Nfc from '@tauri-apps/plugin-nfc' +import type * as Notification from '@tauri-apps/plugin-notification' +import type * as Opener from '@tauri-apps/plugin-opener' +import type * as Os from '@tauri-apps/plugin-os' +import type * as Process from '@tauri-apps/plugin-process' +import type * as Shell from '@tauri-apps/plugin-shell' +import type * as Store from '@tauri-apps/plugin-store' +import type * as Updater from '@tauri-apps/plugin-updater' +import type * as Upload from '@tauri-apps/plugin-upload' +import type * as WindowState from '@tauri-apps/plugin-window-state' + +/** + * The plugin APIs the example registers on every platform, keyed by the name + * each plugin's `api-iife.js` defines on `window.__TAURI__` (the package name + * without the `@tauri-apps/plugin-` prefix, camel-cased). + */ +export interface CommonPluginApi { + clipboardManager: typeof ClipboardManager + dialog: typeof Dialog + fs: typeof Fs + http: typeof Http + log: typeof Log + notification: typeof Notification + opener: typeof Opener + os: typeof Os + process: typeof Process + shell: typeof Shell + store: typeof Store + upload: typeof Upload +} + +/** The plugin APIs the example only registers on desktop (`#[cfg(desktop)]`). */ +export interface DesktopPluginApi { + cli: typeof Cli + globalShortcut: typeof GlobalShortcut + updater: typeof Updater + windowState: typeof WindowState +} + +/** The plugin APIs the example only registers on mobile (`#[cfg(mobile)]`). */ +export interface MobilePluginApi { + barcodeScanner: typeof BarcodeScanner + biometric: typeof Biometric + geolocation: typeof Geolocation + haptics: typeof Haptics + nfc: typeof Nfc +} + +/** + * Every plugin API the example can register. Only the platform-appropriate + * half is actually on `window.__TAURI__` at runtime — see `describePlugin`'s + * `desktopOnly`/`mobileOnly` options and `plugins.spec.ts`. + */ +export type PluginApi = CommonPluginApi & DesktopPluginApi & MobilePluginApi + +/** The `@tauri-apps/api` surface plus every plugin, as exposed on `window.__TAURI__`. */ +export type Api = typeof TauriApi & PluginApi + +/** OS the app under test runs on. */ +export type Platform = NodeJS.Platform | 'android' | 'ios' + +/** + * The platform of the app under test. The desktop suite drives an app on the + * host, so it is `process.platform`; the mobile configs (`wdio.android.conf.ts`, + * `wdio.ios.conf.ts`) drive an emulator/simulator and set `E2E_PLATFORM` for + * the spec workers instead. + */ +export const platform: Platform = + (process.env.E2E_PLATFORM as Platform | undefined) ?? process.platform + +export const isMobile = platform === 'android' || platform === 'ios' + +type PageOutcome = + | { ok: true; value: T } + | { ok: false; error: string; stack?: string } + +/** Thrown when the function passed to {@link tauri} rejects inside the webview. */ +export class TauriPageError extends Error { + pageStack?: string + constructor(message: string, pageStack?: string) { + super(message) + this.name = 'TauriPageError' + this.pageStack = pageStack + } +} + +/** + * Runs `fn` inside the app's webview with `window.__TAURI__` as its first argument + * and resolves with its (JSON-serializable) return value. + * + * `fn` is serialized with `Function.prototype.toString`, so it **cannot close over + * anything in the spec module** — every value it needs must be passed through `args`, + * and it may only reference `api`, those args, and browser globals (`window`, + * `document`, `setTimeout`, `Promise`, ...). + * + * @example + * const platform = await tauri((api) => api.os.platform()) + * const sum = await tauri((api, a, b) => a + b, 2, 3) + */ +export async function tauri( + fn: (api: Api, ...args: A) => R, + ...args: A +): Promise> { + // A string body (rather than passing `fn` directly) keeps this working across + // both the classic and bidi WebDriver protocols and avoids any in-page eval of + // our own — the driver injects this script itself, which is exempt from the + // app's CSP. `executeAsync` is used because promise support in `execute` is not + // uniform across the platform drivers tauri-driver and Appium proxy to. + // + // The outcome crosses the driver as a JSON string rather than an object so no + // driver gets to interpret its shape: the Selenium atoms that Appium runs + // scripts through on iOS turn any object with a numeric `length` property + // into an array. + const script = ` + var done = arguments[arguments.length - 1]; + var args = Array.prototype.slice.call(arguments, 0, arguments.length - 1); + var fn = (${fn.toString()}); + Promise.resolve() + .then(function () { return fn.apply(null, [window.__TAURI__].concat(args)); }) + .then( + function (value) { return { ok: true, value: value === undefined ? null : value }; }, + function (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }; + } + ) + .then(function (outcome) { + try { + done(JSON.stringify(outcome)); + } catch (error) { + done(JSON.stringify({ ok: false, error: 'result is not JSON-serializable: ' + error })); + } + }); + ` + const raw: unknown = await browser.executeAsync(script, ...args) + const outcome = ( + typeof raw === 'string' ? JSON.parse(raw) : raw + ) as PageOutcome> | null + if (!outcome || typeof outcome !== 'object' || !('ok' in outcome)) { + throw new Error( + `tauri() bridge returned an unexpected value: ${JSON.stringify(outcome)}` + ) + } + if (!outcome.ok) { + throw new TauriPageError(outcome.error, outcome.stack) + } + return outcome.value +} + +/** + * Asserts that the page-side call rejects and returns the rejection message, + * so specs can assert on it. Throws if the call unexpectedly resolves. + */ +export async function tauriError( + fn: (api: Api, ...args: A) => unknown, + ...args: A +): Promise { + try { + await tauri(fn, ...args) + } catch (error) { + if (error instanceof TauriPageError) { + return error.message + } + // Some platform drivers (notably the Linux WebKitWebDriver) surface a + // page-side `invoke` rejection as a WebDriver-level error on the + // `execute/async` command instead of letting the in-page bridge report it + // as an `{ ok: false }` outcome. Fall back to that error's message so the + // backend rejection is still assertable. This is safe for error-path specs: + // they match the message against an expected pattern, so a genuine driver + // failure (whose message won't match) still fails the test. + if (error instanceof Error) { + return error.message + } + throw error + } + throw new Error('expected the API call to reject, but it resolved') +} + +/** + * Polls `check` until it returns without throwing or `timeout` elapses. + * Use for state that is applied asynchronously (window manager, file watcher, ...). + */ +export async function eventually( + check: () => T | Promise, + { + timeout = 10_000, + interval = 250 + }: { timeout?: number; interval?: number } = {} +): Promise { + const deadline = Date.now() + timeout + let lastError: unknown + for (;;) { + try { + return await check() + } catch (error) { + lastError = error + } + if (Date.now() > deadline) { + throw lastError instanceof Error + ? lastError + : new Error(String(lastError)) + } + await new Promise((resolve) => setTimeout(resolve, interval)) + } +} + +const skippedModules = (process.env.E2E_SKIP ?? '') + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + +export interface DescribePluginOptions { + /** + * The example only registers the plugin on desktop (`cli`, `global-shortcut`, + * `updater`, `window-state`), so the whole suite is skipped on mobile. + */ + desktopOnly?: boolean + /** + * The example only registers the plugin on mobile (`barcode-scanner`, + * `biometric`, `geolocation`, `haptics`, `nfc`), so the whole suite is + * skipped on desktop. + */ + mobileOnly?: boolean +} + +/** + * `describe` wrapper keyed by plugin name (the `@tauri-apps/plugin-*` suffix). + * Any plugin listed in the comma-separated `E2E_SKIP` env var + * (e.g. `E2E_SKIP=clipboard-manager,global-shortcut`) is skipped. + */ +export function describePlugin(plugin: string, fn: () => void): void +export function describePlugin( + plugin: string, + options: DescribePluginOptions, + fn: () => void +): void +export function describePlugin( + plugin: string, + optionsOrFn: DescribePluginOptions | (() => void), + maybeFn?: () => void +): void { + const [options, fn] = + typeof optionsOrFn === 'function' + ? [{} as DescribePluginOptions, optionsOrFn] + : [optionsOrFn, maybeFn!] + const title = `@tauri-apps/plugin-${plugin}` + if ( + skippedModules.includes(plugin) + || (options.desktopOnly && isMobile) + || (options.mobileOnly && !isMobile) + ) { + describe.skip(title, fn) + } else { + describe(title, fn) + } +} + +/** + * `it` restricted to the given platform(s); skipped (as pending) elsewhere. + * Use for behavior that only exists on one OS. + */ +export function itOn( + platforms: Platform | Platform[], + title: string, + fn: () => void | Promise +): void { + const list = Array.isArray(platforms) ? platforms : [platforms] + if (list.includes(platform)) { + it(title, fn) + } else { + it.skip(title, fn) + } +} + +/** + * `it` for desktop-only behavior of a plugin that *is* registered on mobile: + * a command the mobile build does not expose (`#[cfg(desktop)]`), a mobile + * implementation that answers "Unsupported on this platform", or a permission + * the example only grants in its desktop capability. + */ +export function itDesktop(title: string, fn: () => void | Promise): void { + if (isMobile) { + it.skip(title, fn) + } else { + it(title, fn) + } +} + +/** + * `it` for assertions that depend on a real window manager (window size and + * position restore, ...). Skipped entirely when `E2E_SKIP_WM` is set (e.g. + * bare headless CI), and on mobile, which has no window manager. + */ +export function itWm(title: string, fn: () => void | Promise): void { + if (process.env.E2E_SKIP_WM || isMobile) { + it.skip(title, fn) + } else { + it(title, fn) + } +} + +/** + * A scratch directory the fs-backed specs may freely write to. It is relative + * to `BaseDirectory.AppData` (`$APPDATA`), which the example's fs scope allows + * recursively; `name` keeps each spec file's files apart. + */ +export function scratchDir(name: string): string { + return `e2e/${name}` +} diff --git a/packages/api-e2e/test/helpers/server.ts b/packages/api-e2e/test/helpers/server.ts new file mode 100644 index 000000000..1c0f9d649 --- /dev/null +++ b/packages/api-e2e/test/helpers/server.ts @@ -0,0 +1,116 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import http from 'node:http' + +/** + * Where the fixture server listens. The port is fixed because the updater + * endpoint is baked into the app at build time (see `tauri.e2e.conf.json`). + */ +export const FIXTURE_SERVER_PORT = 3004 +export const FIXTURE_SERVER_URL = `http://127.0.0.1:${FIXTURE_SERVER_PORT}` + +/** Version the `/updater` endpoint advertises by default. */ +export const UPDATER_FIXTURE_VERSION = '2.1.0' +export const UPDATER_FIXTURE_NOTES = 'Test update from the e2e fixture server' +/** `{{target}}` values with special behavior on the `/updater` endpoint. */ +export const UPDATER_TARGET_NO_UPDATE = 'e2e-no-update' +export const UPDATER_TARGET_OLDER = 'e2e-older' +export const UPDATER_FIXTURE_OLDER_VERSION = '1.0.0' + +/** Body served by `GET /download`. */ +export const DOWNLOAD_FIXTURE_BODY = + 'hello from the plugins e2e fixture server\n'.repeat(64) + +export interface FixtureServer { + close(): void +} + +/** + * A tiny HTTP server the network-facing specs (updater, upload) talk to: + * + * - `GET /updater/{{target}}/{{arch}}/{{current_version}}` — an updater + * manifest in the dynamic format. Advertises {@link UPDATER_FIXTURE_VERSION}, + * or {@link UPDATER_FIXTURE_OLDER_VERSION} when the target is + * {@link UPDATER_TARGET_OLDER}, and replies `204 No Content` when it is + * {@link UPDATER_TARGET_NO_UPDATE}. + * - `GET /download` — {@link DOWNLOAD_FIXTURE_BODY} with a `Content-Length`. + * - `* /echo` — a JSON description of the request (`method`, `url`, `headers` + * and the utf-8 `body`). + */ +export function startFixtureServer(): Promise { + const server = http.createServer((req, res) => { + const chunks: Buffer[] = [] + req.on('data', (chunk: Buffer) => chunks.push(chunk)) + req.on('end', () => { + const body = Buffer.concat(chunks) + const url = new URL(req.url ?? '/', FIXTURE_SERVER_URL) + const [, route, ...rest] = url.pathname.split('/') + + if (route === 'updater' && req.method === 'GET') { + const [target] = rest + if (target === UPDATER_TARGET_NO_UPDATE) { + res.writeHead(204).end() + return + } + json(res, { + version: + target === UPDATER_TARGET_OLDER + ? UPDATER_FIXTURE_OLDER_VERSION + : UPDATER_FIXTURE_VERSION, + notes: UPDATER_FIXTURE_NOTES, + pub_date: '2026-03-01T14:04:20Z', + url: `${FIXTURE_SERVER_URL}/download`, + signature: '' + }) + return + } + + if (route === 'download' && req.method === 'GET') { + res + .writeHead(200, { + 'content-type': 'text/plain', + 'content-length': Buffer.byteLength(DOWNLOAD_FIXTURE_BODY) + }) + .end(DOWNLOAD_FIXTURE_BODY) + return + } + + if (route === 'echo') { + json(res, { + method: req.method, + url: req.url, + headers: req.headers, + body: body.toString('utf8') + }) + return + } + + res.writeHead(404).end() + }) + }) + + return new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(FIXTURE_SERVER_PORT, '127.0.0.1', () => { + server.off('error', reject) + resolve({ + close: () => { + server.closeAllConnections() + server.close() + } + }) + }) + }) +} + +function json(res: http.ServerResponse, value: unknown) { + const payload = JSON.stringify(value) + res + .writeHead(200, { + 'content-type': 'application/json', + 'content-length': Buffer.byteLength(payload) + }) + .end(payload) +} diff --git a/packages/api-e2e/test/specs/app.spec.ts b/packages/api-e2e/test/specs/app.spec.ts new file mode 100644 index 000000000..a76604ede --- /dev/null +++ b/packages/api-e2e/test/specs/app.spec.ts @@ -0,0 +1,71 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri } from '../helpers/index.js' + +// The example app's own commands and events, which its "Communication" view +// drives; they double as a check that the app under test is the right one. + +describe('examples/api', () => { + it('is built from the example config', async () => { + const info = await tauri(async (api) => ({ + name: await api.app.getName(), + version: await api.app.getVersion(), + identifier: await api.app.getIdentifier(), + label: api.window.getCurrentWindow().label + })) + expect(info).toEqual({ + name: 'Tauri API', + version: '2.0.0', + identifier: 'com.tauri.api', + label: 'main' + }) + }) + + it('perform_request returns the backend response', async () => { + const response = await tauri((api) => + api.core.invoke('perform_request', { + endpoint: 'dummy endpoint arg', + body: { id: 5, name: 'test' } + }) + ) + expect(response).toBe('message response') + }) + + it('log_operation accepts an optional payload', async () => { + await tauri(async (api) => { + await api.core.invoke('log_operation', { event: 'tauri-click' }) + await api.core.invoke('log_operation', { + event: 'tauri-click', + payload: 'from e2e' + }) + return null + }) + }) + + it('js-event is answered with rust-event', async () => { + const reply = await tauri( + (api) => + new Promise<{ data: string }>((resolve, reject) => { + const webview = api.webview.getCurrentWebview() + webview + .listen<{ data: string }>('rust-event', (event) => + resolve(event.payload) + ) + .then((unlisten) => { + webview + .emit('js-event', 'this is the payload string') + .catch(reject) + setTimeout(() => { + unlisten() + reject(new Error('no rust-event reply received')) + }, 5000) + }) + .catch(reject) + }) + ) + expect(reply).toEqual({ data: 'something else' }) + }) +}) diff --git a/packages/api-e2e/test/specs/cli.spec.ts b/packages/api-e2e/test/specs/cli.spec.ts new file mode 100644 index 000000000..e817bcc2b --- /dev/null +++ b/packages/api-e2e/test/specs/cli.spec.ts @@ -0,0 +1,33 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describePlugin } from '../helpers/index.js' + +// The driver launches the app without arguments, so the matches reflect the +// CLI definition in the example's `tauri.conf.json` with nothing set. The +// example only registers the plugin on desktop, so the suite is skipped on +// mobile (which has no command line to begin with). + +describePlugin('cli', { desktopOnly: true }, () => { + it('getMatches reports every defined argument as unset', async () => { + const matches = await tauri((api) => api.cli.getMatches()) + expect(Object.keys(matches.args).sort()).toEqual([ + 'config', + 'theme', + 'verbose' + ]) + // flags resolve to `false`, arguments taking a value to `null` + expect(matches.args.verbose).toEqual({ value: false, occurrences: 0 }) + expect(matches.args.config).toEqual({ value: null, occurrences: 0 }) + expect(matches.args.theme).toEqual({ value: null, occurrences: 0 }) + }) + + it('getMatches reports no subcommand', async () => { + const subcommand = await tauri( + async (api) => (await api.cli.getMatches()).subcommand + ) + expect(subcommand).toBeNull() + }) +}) diff --git a/packages/api-e2e/test/specs/clipboard-manager.spec.ts b/packages/api-e2e/test/specs/clipboard-manager.spec.ts new file mode 100644 index 000000000..e4be7c59a --- /dev/null +++ b/packages/api-e2e/test/specs/clipboard-manager.spec.ts @@ -0,0 +1,153 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + itDesktop +} from '../helpers/index.js' + +// The mobile implementation only carries plain text: `write_html`, `write_image` +// and `read_image` answer "Unsupported on this platform" there. +describePlugin('clipboard-manager', () => { + it('writeText and readText round-trip', async () => { + const text = 'clipboard text from e2e — ✓' + expect( + await tauri(async (api, text) => { + await api.clipboardManager.writeText(text) + return api.clipboardManager.readText() + }, text) + ).toBe(text) + }) + + it('writeText replaces the previous contents', async () => { + expect( + await tauri(async (api) => { + await api.clipboardManager.writeText('first') + await api.clipboardManager.writeText('second') + return api.clipboardManager.readText() + }) + ).toBe('second') + }) + + itDesktop('writeHtml exposes the alt text as plain text', async () => { + expect( + await tauri(async (api) => { + await api.clipboardManager.writeHtml( + 'bold from e2e', + 'bold from e2e (alt)' + ) + return api.clipboardManager.readText() + }) + ).toBe('bold from e2e (alt)') + }) + + itDesktop('writeImage and readImage round-trip pixels', async () => { + // a 2x2 PNG: red, green / blue, white + const png = [ + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 2, + 0, 0, 0, 2, 8, 6, 0, 0, 0, 114, 182, 13, 36, 0, 0, 0, 18, 73, 68, 65, 84, + 120, 218, 99, 248, 207, 192, 240, 31, 12, 129, 52, 24, 0, 0, 73, 200, 9, + 247, 3, 217, 100, 241, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 + ] + const rgba = [ + 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255 + ] + const result = await tauri(async (api, png) => { + // encoded bytes are decoded by the plugin before hitting the clipboard + await api.clipboardManager.writeImage(new Uint8Array(png)) + const read = await api.clipboardManager.readImage() + const size = await read.size() + const bytes = Array.from(await read.rgba()) + await read.close() + return { size, bytes } + }, png) + expect(result.size).toEqual({ width: 2, height: 2 }) + expect(result.bytes).toEqual(rgba) + }) + + itDesktop( + 'writeImage accepts an Image built with the core image API', + async () => { + // `window.__TAURI__.image.Image` and the class the plugin's global script + // sees must be the same one for `transformImage`'s `instanceof` to hold. + const rgba = [ + 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255 + ] + const result = await tauri(async (api, rgba) => { + const image = await api.image.Image.new(rgba, 2, 2) + await api.clipboardManager.writeImage(image) + const read = await api.clipboardManager.readImage() + const size = await read.size() + const bytes = Array.from(await read.rgba()) + await image.close() + await read.close() + return { size, bytes } + }, rgba) + expect(result.size).toEqual({ width: 2, height: 2 }) + expect(result.bytes).toEqual(rgba) + } + ) + + itDesktop( + 'an Image read from the clipboard can be written back', + async () => { + const result = await tauri(async (api) => { + const read = await api.clipboardManager.readImage() + await api.clipboardManager.writeText('replaced by text') + // `Image` instances are passed by resource id + await api.clipboardManager.writeImage(read) + const again = await api.clipboardManager.readImage() + const size = await again.size() + await read.close() + await again.close() + return size + }) + expect(result).toEqual({ width: 2, height: 2 }) + } + ) + + itDesktop('writeImage accepts a 1x1 image', async () => { + // a 1x1 transparent PNG + const png = [ + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, + 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, + 120, 156, 99, 96, 0, 2, 0, 0, 5, 0, 1, 122, 94, 171, 63, 0, 0, 0, 0, 73, + 69, 78, 68, 174, 66, 96, 130 + ] + const size = await tauri(async (api, png) => { + await api.clipboardManager.writeImage(new Uint8Array(png)) + const read = await api.clipboardManager.readImage() + const size = await read.size() + await read.close() + return size + }, png) + expect(size).toEqual({ width: 1, height: 1 }) + }) + + it('readImage rejects when the clipboard holds text', async () => { + const message = await tauriError(async (api) => { + await api.clipboardManager.writeText('not an image') + await api.clipboardManager.readImage() + }) + expect(message.length).toBeGreaterThan(0) + }) + + it('clear empties the clipboard', async () => { + // reading an empty clipboard rejects on some platforms and yields an + // empty string on others + const text = await tauri(async (api) => { + await api.clipboardManager.writeText('to be cleared') + await api.clipboardManager.clear() + try { + return await api.clipboardManager.readText() + } catch { + return '' + } + }) + expect(text).toBe('') + }) +}) diff --git a/packages/api-e2e/test/specs/fs.spec.ts b/packages/api-e2e/test/specs/fs.spec.ts new file mode 100644 index 000000000..5f1b39154 --- /dev/null +++ b/packages/api-e2e/test/specs/fs.spec.ts @@ -0,0 +1,380 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + itDesktop, + scratchDir +} from '../helpers/index.js' + +// Every path below is relative to `BaseDirectory.AppData`, which the example's +// fs scope allows recursively (`fs:scope-appdata-recursive`). +const dir = scratchDir('fs') + +describePlugin('fs', () => { + before(async () => { + await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + if (await api.fs.exists(dir, { baseDir })) { + await api.fs.remove(dir, { baseDir, recursive: true }) + } + await api.fs.mkdir(dir, { baseDir, recursive: true }) + }, dir) + }) + + after(async () => { + await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + if (await api.fs.exists(dir, { baseDir })) { + await api.fs.remove(dir, { baseDir, recursive: true }) + } + }, dir) + }) + + it('mkdir and exists report the scratch directory', async () => { + const result = await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + return { + dir: await api.fs.exists(dir, { baseDir }), + missing: await api.fs.exists(`${dir}/does-not-exist`, { baseDir }) + } + }, dir) + expect(result).toEqual({ dir: true, missing: false }) + }) + + it('writeTextFile and readTextFile round-trip utf-8 text', async () => { + const text = 'Hello from the e2e suite — olá, 世界! 🎉\nsecond line\n' + const read = await tauri( + async (api, path, text) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeTextFile(path, text, { baseDir }) + return api.fs.readTextFile(path, { baseDir }) + }, + `${dir}/text.txt`, + text + ) + expect(read).toBe(text) + }) + + it('writeTextFile appends when asked to', async () => { + const read = await tauri(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeTextFile(path, 'first', { baseDir }) + await api.fs.writeTextFile(path, ' second', { baseDir, append: true }) + return api.fs.readTextFile(path, { baseDir }) + }, `${dir}/append.txt`) + expect(read).toBe('first second') + }) + + it('writeFile and readFile round-trip binary data', async () => { + const bytes = [0, 1, 2, 3, 250, 251, 252, 253, 254, 255] + const read = await tauri( + async (api, path, bytes) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeFile(path, new Uint8Array(bytes), { baseDir }) + return Array.from(await api.fs.readFile(path, { baseDir })) + }, + `${dir}/binary.bin`, + bytes + ) + expect(read).toEqual(bytes) + }) + + it('stat, lstat and size describe files and directories', async () => { + const result = await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + const file = `${dir}/stat.txt` + await api.fs.writeTextFile(file, '0123456789', { baseDir }) + const fileStat = await api.fs.stat(file, { baseDir }) + const fileLstat = await api.fs.lstat(file, { baseDir }) + const dirStat = await api.fs.stat(dir, { baseDir }) + return { + file: { + isFile: fileStat.isFile, + isDirectory: fileStat.isDirectory, + isSymlink: fileStat.isSymlink, + size: fileStat.size, + hasMtime: fileStat.mtime instanceof Date + }, + lstatSize: fileLstat.size, + // `size` only takes absolute paths + size: await api.fs.size( + await api.path.join(await api.path.appDataDir(), file) + ), + dir: { isFile: dirStat.isFile, isDirectory: dirStat.isDirectory } + } + }, dir) + expect(result.file).toEqual({ + isFile: true, + isDirectory: false, + isSymlink: false, + size: 10, + hasMtime: true + }) + expect(result.lstatSize).toBe(10) + expect(result.size).toBe(10) + expect(result.dir).toEqual({ isFile: false, isDirectory: true }) + }) + + it('copyFile, rename and readDir', async () => { + const result = await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + const sub = `${dir}/tree` + await api.fs.mkdir(`${sub}/nested`, { baseDir, recursive: true }) + await api.fs.writeTextFile(`${sub}/a.txt`, 'a', { baseDir }) + await api.fs.copyFile(`${sub}/a.txt`, `${sub}/b.txt`, { + fromPathBaseDir: baseDir, + toPathBaseDir: baseDir + }) + await api.fs.rename(`${sub}/b.txt`, `${sub}/c.txt`, { + oldPathBaseDir: baseDir, + newPathBaseDir: baseDir + }) + const entries = await api.fs.readDir(sub, { baseDir }) + return { + entries: entries + .map((e) => ({ + name: e.name, + isFile: e.isFile, + isDirectory: e.isDirectory + })) + .sort((x, y) => x.name.localeCompare(y.name)), + copied: await api.fs.readTextFile(`${sub}/c.txt`, { baseDir }), + renamedAway: await api.fs.exists(`${sub}/b.txt`, { baseDir }) + } + }, dir) + expect(result.entries).toEqual([ + { name: 'a.txt', isFile: true, isDirectory: false }, + { name: 'c.txt', isFile: true, isDirectory: false }, + { name: 'nested', isFile: false, isDirectory: true } + ]) + expect(result.copied).toBe('a') + expect(result.renamedAway).toBe(false) + }) + + it('remove deletes files and (recursively) directories', async () => { + const result = await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + const sub = `${dir}/to-remove` + await api.fs.mkdir(`${sub}/nested`, { baseDir, recursive: true }) + await api.fs.writeTextFile(`${sub}/nested/file.txt`, 'x', { baseDir }) + await api.fs.remove(`${sub}/nested/file.txt`, { baseDir }) + const fileGone = !(await api.fs.exists(`${sub}/nested/file.txt`, { + baseDir + })) + await api.fs.writeTextFile(`${sub}/nested/other.txt`, 'x', { baseDir }) + let nonRecursiveError: string | null = null + try { + await api.fs.remove(sub, { baseDir }) + } catch (error) { + nonRecursiveError = String(error) + } + await api.fs.remove(sub, { baseDir, recursive: true }) + return { + fileGone, + nonRecursiveError, + dirGone: !(await api.fs.exists(sub, { baseDir })) + } + }, dir) + expect(result.fileGone).toBe(true) + // a non-empty directory cannot be removed without `recursive` + expect(result.nonRecursiveError).not.toBeNull() + expect(result.dirGone).toBe(true) + }) + + it('truncate shortens a file', async () => { + const result = await tauri(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeTextFile(path, '0123456789', { baseDir }) + await api.fs.truncate(path, 4, { baseDir }) + const shortened = await api.fs.readTextFile(path, { baseDir }) + await api.fs.truncate(path, undefined, { baseDir }) + return { + shortened, + emptied: await api.fs.readTextFile(path, { baseDir }) + } + }, `${dir}/truncate.txt`) + expect(result).toEqual({ shortened: '0123', emptied: '' }) + }) + + it('readTextFileLines iterates a file line by line', async () => { + const lines = await tauri(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeTextFile(path, 'one\ntwo\r\nthree', { baseDir }) + const result: string[] = [] + for await (const line of await api.fs.readTextFileLines(path, { + baseDir + })) { + result.push(line) + } + return result + }, `${dir}/lines.txt`) + expect(lines).toEqual(['one', 'two', 'three']) + }) + + it('FileHandle supports write, seek, read, stat and truncate', async () => { + const result = await tauri(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + const encoder = new TextEncoder() + const decoder = new TextDecoder() + + const created = await api.fs.create(path, { baseDir }) + const written = await created.write(encoder.encode('hello world')) + const statAfterWrite = await created.stat() + await created.close() + + const file = await api.fs.open(path, { baseDir, read: true, write: true }) + // seek past "hello " and read the rest + const position = await file.seek(6, api.fs.SeekMode.Start) + const buffer = new Uint8Array(32) + const read = await file.read(buffer) + const rest = decoder.decode(buffer.subarray(0, read ?? 0)) + // at EOF, read reports null + const atEof = await file.read(new Uint8Array(8)) + // relative and end-relative seeks + const fromEnd = await file.seek(-5, api.fs.SeekMode.End) + const relative = await file.seek(-1, api.fs.SeekMode.Current) + await file.truncate(5) + const statAfterTruncate = await file.stat() + await file.close() + + return { + written, + sizeAfterWrite: statAfterWrite.size, + position, + read, + rest, + atEof, + fromEnd, + relative, + sizeAfterTruncate: statAfterTruncate.size, + contents: await api.fs.readTextFile(path, { baseDir }) + } + }, `${dir}/handle.txt`) + expect(result).toEqual({ + written: 11, + sizeAfterWrite: 11, + position: 6, + read: 5, + rest: 'world', + atEof: null, + fromEnd: 6, + relative: 5, + sizeAfterTruncate: 5, + contents: 'hello' + }) + }) + + it('a closed FileHandle cannot be used again', async () => { + const message = await tauriError(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + const file = await api.fs.create(path, { baseDir }) + await file.close() + await file.stat() + }, `${dir}/closed.txt`) + expect(message.length).toBeGreaterThan(0) + }) + + it('rejects paths outside the configured scope', async () => { + // `$HOME` itself is not in the example's fs scope (only the app dirs, + // `$DOWNLOAD` and `$RESOURCE` are). + const message = await tauriError(async (api) => + api.fs.readTextFile('e2e-forbidden.txt', { + baseDir: api.fs.BaseDirectory.Home + }) + ) + expect(message).toMatch(/forbidden path/) + }) + + it('rejects paths escaping the scope through `..`', async () => { + const message = await tauriError(async (api) => + api.fs.writeTextFile('../../e2e-escape.txt', 'nope', { + baseDir: api.fs.BaseDirectory.AppData + }) + ) + expect(message).toMatch(/cannot traverse directory|forbidden path/) + }) + + // The watch specs are desktop-only: `fs:allow-watch` is granted in the + // example's desktop capability only. + itDesktop( + 'watchImmediate reports changes in a watched directory', + async () => { + const result = await tauri(async (api, watched) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.mkdir(watched, { baseDir, recursive: true }) + const events: { kind: string; paths: string[] }[] = [] + const unwatch = await api.fs.watchImmediate( + watched, + (event) => { + events.push({ + kind: + typeof event.type === 'string' + ? event.type + : Object.keys(event.type)[0], + paths: event.paths + }) + }, + { baseDir, recursive: true } + ) + await api.fs.writeTextFile(`${watched}/touched.txt`, 'watched', { + baseDir + }) + // give the notifier a moment to deliver + const deadline = Date.now() + 10_000 + while ( + !events.some((e) => e.paths.some((p) => p.endsWith('touched.txt'))) + ) { + if (Date.now() > deadline) { + throw new Error( + `no watch event for touched.txt, got ${JSON.stringify(events)}` + ) + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + unwatch() + return events + }, `${dir}/watched`) + expect(result.length).toBeGreaterThan(0) + expect(result.every((e) => typeof e.kind === 'string')).toBe(true) + } + ) + + itDesktop('watch debounces and unwatch stops delivery', async () => { + const result = await tauri(async (api, watched) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.mkdir(watched, { baseDir, recursive: true }) + let count = 0 + const unwatch = await api.fs.watch( + watched, + () => { + count++ + }, + { baseDir, delayMs: 200 } + ) + await api.fs.writeTextFile(`${watched}/debounced.txt`, 'a', { + baseDir + }) + const deadline = Date.now() + 10_000 + while (count === 0) { + if (Date.now() > deadline) { + throw new Error('no debounced watch event received') + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + const afterFirst = count + unwatch() + await api.fs.writeTextFile(`${watched}/after-unwatch.txt`, 'b', { + baseDir + }) + await new Promise((resolve) => setTimeout(resolve, 1000)) + return { afterFirst, afterUnwatch: count } + }, `${dir}/debounced`) + expect(result.afterFirst).toBeGreaterThan(0) + expect(result.afterUnwatch).toBe(result.afterFirst) + }) +}) diff --git a/packages/api-e2e/test/specs/global-shortcut.spec.ts b/packages/api-e2e/test/specs/global-shortcut.spec.ts new file mode 100644 index 000000000..c0b0980ee --- /dev/null +++ b/packages/api-e2e/test/specs/global-shortcut.spec.ts @@ -0,0 +1,88 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, tauriError, describePlugin } from '../helpers/index.js' + +// Triggering a shortcut needs OS-level synthetic input the WebDriver session +// cannot produce, so the specs cover the registry (register / isRegistered / +// unregister / unregisterAll) and the error paths. The plugin is desktop-only, +// so the whole suite is skipped on mobile. + +describePlugin('global-shortcut', { desktopOnly: true }, () => { + afterEach(async () => { + await tauri((api) => api.globalShortcut.unregisterAll()) + }) + + it('register and unregister update isRegistered', async () => { + const result = await tauri(async (api) => { + const shortcut = 'CommandOrControl+Shift+F9' + const before = await api.globalShortcut.isRegistered(shortcut) + await api.globalShortcut.register(shortcut, () => {}) + const registered = await api.globalShortcut.isRegistered(shortcut) + await api.globalShortcut.unregister(shortcut) + const after = await api.globalShortcut.isRegistered(shortcut) + return { before, registered, after } + }) + expect(result).toEqual({ before: false, registered: true, after: false }) + }) + + it('register accepts a list of shortcuts and unregister a list too', async () => { + const result = await tauri(async (api) => { + const shortcuts = ['Alt+Shift+F7', 'Alt+Shift+F8'] + await api.globalShortcut.register(shortcuts, () => {}) + const registered = await Promise.all( + shortcuts.map((s) => api.globalShortcut.isRegistered(s)) + ) + await api.globalShortcut.unregister(shortcuts) + const after = await Promise.all( + shortcuts.map((s) => api.globalShortcut.isRegistered(s)) + ) + return { registered, after } + }) + expect(result.registered).toEqual([true, true]) + expect(result.after).toEqual([false, false]) + }) + + it('unregisterAll clears every registration', async () => { + const result = await tauri(async (api) => { + await api.globalShortcut.register('Alt+Shift+F5', () => {}) + await api.globalShortcut.register('Alt+Shift+F6', () => {}) + await api.globalShortcut.unregisterAll() + return [ + await api.globalShortcut.isRegistered('Alt+Shift+F5'), + await api.globalShortcut.isRegistered('Alt+Shift+F6') + ] + }) + expect(result).toEqual([false, false]) + }) + + it('shortcut names are normalized when checking registrations', async () => { + const result = await tauri(async (api) => { + await api.globalShortcut.register('CmdOrCtrl+Alt+F10', () => {}) + return { + aliased: await api.globalShortcut.isRegistered( + 'CommandOrControl+Alt+F10' + ), + reordered: await api.globalShortcut.isRegistered('Alt+CmdOrCtrl+F10') + } + }) + expect(result).toEqual({ aliased: true, reordered: true }) + }) + + it('registering the same shortcut twice rejects', async () => { + const message = await tauriError(async (api) => { + await api.globalShortcut.register('Alt+Shift+F11', () => {}) + await api.globalShortcut.register('Alt+Shift+F11', () => {}) + }) + expect(message).toMatch(/already registered/i) + }) + + it('rejects shortcuts that cannot be parsed', async () => { + const message = await tauriError((api) => + api.globalShortcut.register('NotAKey+Nope', () => {}) + ) + expect(message).toMatch(/NotAKey/) + }) +}) diff --git a/packages/api-e2e/test/specs/http.spec.ts b/packages/api-e2e/test/specs/http.spec.ts new file mode 100644 index 000000000..9ccfbc656 --- /dev/null +++ b/packages/api-e2e/test/specs/http.spec.ts @@ -0,0 +1,149 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, tauriError, describePlugin } from '../helpers/index.js' + +// The example spawns an echo server on this port: it replies with the request +// body and the request headers, and sets a `session-token` cookie on requests +// that do not carry one. It is also the only `http://` origin in the example's +// http scope. +const echoServer = 'http://localhost:3003' + +describePlugin('http', () => { + it('fetch performs a GET and exposes status, url and headers', async () => { + const response = await tauri(async (api, url) => { + const response = await api.http.fetch(url, { + headers: { 'x-e2e-header': 'present' } + }) + return { + ok: response.ok, + status: response.status, + url: response.url, + // the echo server mirrors the request headers back + echoedHeader: response.headers.get('x-e2e-header'), + body: await response.text() + } + }, echoServer) + expect(response.ok).toBe(true) + expect(response.status).toBe(200) + expect(response.url).toBe(`${echoServer}/`) + expect(response.echoedHeader).toBe('present') + expect(response.body).toBe('') + }) + + it('fetch sends a JSON body and parses the echoed response', async () => { + const payload = { message: 'hello from e2e', nested: { list: [1, 2, 3] } } + const response = await tauri( + async (api, url, payload) => { + const response = await api.http.fetch(`${url}/json`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(payload) + }) + return { + status: response.status, + contentType: response.headers.get('content-type'), + body: (await response.json()) as unknown + } + }, + echoServer, + payload + ) + expect(response.status).toBe(200) + expect(response.contentType).toBe('application/json') + expect(response.body).toEqual(payload) + }) + + it('fetch sends binary bodies and reads them back as bytes', async () => { + const bytes = [0, 1, 2, 127, 128, 254, 255] + const echoed = await tauri( + async (api, url, bytes) => { + const response = await api.http.fetch(`${url}/bytes`, { + method: 'PUT', + body: new Uint8Array(bytes) + }) + return Array.from(new Uint8Array(await response.arrayBuffer())) + }, + echoServer, + bytes + ) + expect(echoed).toEqual(bytes) + }) + + it('fetch sends multipart form data', async () => { + const body = await tauri(async (api, url) => { + const form = new FormData() + form.append('foo', 'baz') + form.append('bar', 'qux') + const response = await api.http.fetch(`${url}/form`, { + method: 'POST', + body: form + }) + return { + contentType: response.headers.get('content-type'), + text: await response.text() + } + }, echoServer) + expect(body.contentType).toMatch(/^multipart\/form-data; boundary=/) + expect(body.text).toContain('name="foo"') + expect(body.text).toContain('baz') + expect(body.text).toContain('name="bar"') + expect(body.text).toContain('qux') + }) + + it('the cookie jar stores and replays cookies across requests', async () => { + const result = await tauri(async (api, url) => { + // The jar is persisted in the app data dir, so an earlier run (or the + // requests above) may already hold the cookie: the first request then + // replays it instead of being handed a new one. + const first = await api.http.fetch(`${url}/cookies`) + await first.text() + // Either way the jar attaches it to the next request, which the echo + // server mirrors back as a `cookie` header without setting a new one. + const second = await api.http.fetch(`${url}/cookies`) + await second.text() + return { + setCookie: first.headers.get('set-cookie'), + replayedFirst: first.headers.get('cookie'), + replayed: second.headers.get('cookie'), + setAgain: second.headers.get('set-cookie') + } + }, echoServer) + if (result.replayedFirst === null) { + expect(result.setCookie).toMatch(/^session-token=test-value/) + } else { + expect(result.replayedFirst).toContain('session-token=test-value') + } + expect(result.replayed).toContain('session-token=test-value') + expect(result.setAgain).toBeNull() + }) + + it('fetch can be aborted', async () => { + const message = await tauriError(async (api, url) => { + const controller = new AbortController() + controller.abort() + await api.http.fetch(`${url}/aborted`, { signal: controller.signal }) + }, echoServer) + expect(message).toMatch(/abort|cancel/i) + }) + + it('rejects URLs outside the configured scope', async () => { + const message = await tauriError((api) => + api.http.fetch('http://localhost:3999/not-in-scope') + ) + expect(message).toMatch(/url not allowed on the configured scope/) + }) + + it('network failures reject', async () => { + // Every in-scope origin is reachable, so route the request through a proxy + // nothing listens on to force a connection error. + const message = await tauriError( + (api, url) => + api.http.fetch(url, { proxy: { all: 'http://127.0.0.1:1' } }), + echoServer + ) + expect(message.length).toBeGreaterThan(0) + }) +}) diff --git a/packages/api-e2e/test/specs/log.spec.ts b/packages/api-e2e/test/specs/log.spec.ts new file mode 100644 index 000000000..74467f687 --- /dev/null +++ b/packages/api-e2e/test/specs/log.spec.ts @@ -0,0 +1,188 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describePlugin, isMobile } from '../helpers/index.js' + +// The example registers the plugin with a `Webview` target, so records logged +// from the page (and from Rust) are forwarded back to `attachLogger`. +// +// The plugin's default format differs per platform: `[date][time][target][level] +// message` on desktop, and just `[target] message` on mobile, where the platform +// logger (logcat / os_log) already stamps the time and level. + +interface Record { + level: number + message: string +} + +type Level = 'error' | 'warn' | 'info' | 'debug' | 'trace' + +/** Logs `message` at `level` and resolves with the records `attachLogger` saw. */ +function logAndCollect(level: Level, message: string) { + return tauri( + (api, level, message) => + new Promise((resolve, reject) => { + const records: Record[] = [] + api.log + .attachLogger((record) => { + if (record.message.includes(message)) { + records.push(record) + } + }) + .then(async (detach) => { + // eslint-disable-next-line security/detect-object-injection + await api.log[level](message) + setTimeout(() => { + detach() + resolve(records) + }, 1000) + }) + .catch(reject) + }), + level, + message + ) +} + +describePlugin('log', () => { + it('attachLogger receives records logged from the webview', async () => { + const records = await logAndCollect('info', 'info record from e2e') + expect(records).toHaveLength(1) + expect(records[0].level).toBe(3) // LogLevel.Info + expect(records[0].message).toMatch( + isMobile + ? /\[webview[^\]]*\] info record from e2e$/ + : /\[webview[^\]]*\]\[INFO\] info record from e2e$/ + ) + }) + + it('records carry the level they were logged at', async () => { + const error = await logAndCollect('error', 'error record from e2e') + const warn = await logAndCollect('warn', 'warn record from e2e') + expect(error[0].level).toBe(5) // LogLevel.Error + expect(warn[0].level).toBe(4) // LogLevel.Warn + if (!isMobile) { + // the level is only part of the formatted message on desktop + expect(error[0].message).toContain('[ERROR]') + expect(warn[0].message).toContain('[WARN]') + } + }) + + it('records below the configured level are dropped', async () => { + // the example sets the level filter to Info + const debug = await logAndCollect('debug', 'debug record from e2e') + const trace = await logAndCollect('trace', 'trace record from e2e') + expect(debug).toHaveLength(0) + expect(trace).toHaveLength(0) + }) + + it('log options are accepted', async () => { + const records = await tauri( + (api, needle) => + new Promise((resolve, reject) => { + const records: Record[] = [] + api.log + .attachLogger((record) => { + if (record.message.includes(needle)) records.push(record) + }) + .then(async (detach) => { + await api.log.info(needle, { + file: 'e2e.spec.ts', + line: 42, + keyValues: { suite: 'plugins-e2e' } + }) + setTimeout(() => { + detach() + resolve(records) + }, 1000) + }) + .catch(reject) + }), + 'record with options from e2e' + ) + expect(records).toHaveLength(1) + expect(records[0].level).toBe(3) + }) + + it('detaching the logger stops delivery', async () => { + const count = await tauri( + (api, needle) => + new Promise((resolve, reject) => { + let count = 0 + api.log + .attachLogger((record) => { + if (record.message.includes(needle)) count++ + }) + .then(async (detach) => { + await api.log.info(needle) + await new Promise((r) => setTimeout(r, 500)) + detach() + await api.log.info(needle) + await new Promise((r) => setTimeout(r, 1000)) + resolve(count) + }) + .catch(reject) + }), + 'detached record from e2e' + ) + expect(count).toBe(1) + }) + + it('attachConsole forwards records to the console', async () => { + const forwarded = await tauri( + (api, needle) => + new Promise((resolve, reject) => { + const seen: string[] = [] + // Info records are forwarded to `console.info` + const original = console.info + console.info = (...args: unknown[]) => { + const text = args.map(String).join(' ') + if (text.includes(needle)) seen.push(text) + original.apply(console, args) + } + api.log + .attachConsole() + .then(async (detach) => { + await api.log.info(needle) + setTimeout(() => { + detach() + console.info = original + resolve(seen) + }, 1000) + }) + .catch(reject) + }), + 'console record from e2e' + ) + expect(forwarded.length).toBeGreaterThan(0) + }) + + it('records logged from Rust are forwarded to the webview too', async () => { + // the example's `log_operation` command logs its arguments at Info + const message = await tauri( + (api, needle) => + new Promise((resolve, reject) => { + api.log + .attachLogger((record) => { + if (record.message.includes(needle)) resolve(record.message) + }) + .then(() => + api.core.invoke('log_operation', { + event: 'tauri-click', + payload: needle + }) + ) + .catch(reject) + setTimeout(() => reject(new Error('record not received')), 5000) + }), + 'rust log from e2e' + ) + expect(message).toContain( + isMobile ? '[api_lib::cmd] tauri-click' : '[INFO] tauri-click' + ) + expect(message).toContain('rust log from e2e') + expect(message).not.toContain('[webview') + }) +}) diff --git a/packages/api-e2e/test/specs/notification.spec.ts b/packages/api-e2e/test/specs/notification.spec.ts new file mode 100644 index 000000000..d4d95476f --- /dev/null +++ b/packages/api-e2e/test/specs/notification.spec.ts @@ -0,0 +1,58 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describePlugin, itDesktop } from '../helpers/index.js' + +// Whether a notification actually shows up depends on the desktop session +// (a notification daemon on Linux, the app's registration on Windows and +// macOS), which the suite cannot observe. The specs cover the permission +// model and that sending does not error out synchronously. +// +// The permission specs are desktop-only: mobile starts out ungranted and +// `requestPermission` puts up a system dialog the session would then block on. + +describePlugin('notification', () => { + itDesktop('permission is granted on desktop', async () => { + const result = await tauri(async (api) => ({ + granted: await api.notification.isPermissionGranted(), + requested: await api.notification.requestPermission() + })) + expect(result).toEqual({ granted: true, requested: 'granted' }) + }) + + itDesktop('the plugin overrides window.Notification', async () => { + const result = await tauri(async () => ({ + permission: window.Notification.permission, + requested: await window.Notification.requestPermission() + })) + expect(result).toEqual({ permission: 'granted', requested: 'granted' }) + }) + + it('sendNotification accepts a title string and an options object', async () => { + const result = await tauri((api) => { + api.notification.sendNotification('notification from e2e') + api.notification.sendNotification({ + title: 'notification from e2e', + body: 'with a body', + sound: 'default' + }) + return true + }) + expect(result).toBe(true) + }) + + it('new Notification() goes through the plugin', async () => { + const result = await tauri(() => { + const notification = new window.Notification( + 'window.Notification from e2e', + { + body: 'created through the DOM API' + } + ) + return typeof notification === 'object' + }) + expect(result).toBe(true) + }) +}) diff --git a/packages/api-e2e/test/specs/opener.spec.ts b/packages/api-e2e/test/specs/opener.spec.ts new file mode 100644 index 000000000..27201c0c8 --- /dev/null +++ b/packages/api-e2e/test/specs/opener.spec.ts @@ -0,0 +1,46 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauriError, describePlugin } from '../helpers/index.js' + +// A successful open launches an external application (browser, file manager) +// the suite cannot control or close, so only the scope enforcement is covered. +// The example allows `mailto:`, `tel:`, `http(s)://` URLs (opener:default), +// `https://` URLs specifically with `inAppBrowser`, and paths under `$APPDATA`. + +describePlugin('opener', () => { + it('openUrl rejects URL schemes outside the scope', async () => { + const message = await tauriError((api) => + api.opener.openUrl('ftp://example.com/file') + ) + expect(message).toMatch( + /Not allowed to open url ftp:\/\/example\.com\/file/ + ) + }) + + it('openUrl rejects an app that is not in the scope for the URL', async () => { + // `inAppBrowser` is only allowed for `https://` URLs + const message = await tauriError((api) => + api.opener.openUrl('http://example.com', 'inAppBrowser') + ) + expect(message).toMatch(/Not allowed to open url http:\/\/example\.com/) + }) + + it('openPath rejects paths outside the scope', async () => { + const message = await tauriError(async (api) => + api.opener.openPath(await api.path.join(await api.path.homeDir(), 'e2e')) + ) + expect(message).toMatch(/Not allowed to open path/) + }) + + it('revealItemInDir rejects paths that do not exist', async () => { + const message = await tauriError(async (api) => + api.opener.revealItemInDir( + await api.path.join(await api.path.appDataDir(), 'does-not-exist-e2e') + ) + ) + expect(message.length).toBeGreaterThan(0) + }) +}) diff --git a/packages/api-e2e/test/specs/os.spec.ts b/packages/api-e2e/test/specs/os.spec.ts new file mode 100644 index 000000000..fcad729df --- /dev/null +++ b/packages/api-e2e/test/specs/os.spec.ts @@ -0,0 +1,120 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import os from 'node:os' +import { expect } from '@wdio/globals' +import { + tauri, + describePlugin, + itDesktop, + platform, + isMobile +} from '../helpers/index.js' + +// Everything the plugin reports is baked in at compile time, so it describes +// the *app's* platform — the host for the desktop suite, but the emulator or +// simulator for the mobile ones, which is why nothing here compares against +// Node's view of the host unless the two are the same machine. + +const nodePlatformToTauri: Record = { + linux: 'linux', + win32: 'windows', + darwin: 'macos', + freebsd: 'freebsd', + openbsd: 'openbsd', + android: 'android', + ios: 'ios' +} + +const nodeArchToTauri: Record = { + x64: 'x86_64', + ia32: 'x86', + arm64: 'aarch64', + arm: 'arm', + riscv64: 'riscv64', + ppc64: 'powerpc64', + s390x: 's390x' +} + +// `Arch` in the plugin's guest-js. +const architectures = [ + 'x86', + 'x86_64', + 'arm', + 'aarch64', + 'mips', + 'mips64', + 'powerpc', + 'powerpc64', + 'riscv64', + 's390x', + 'sparc64' +] + +describePlugin('os', () => { + it('platform, type and family match the target', async () => { + const info = await tauri((api) => ({ + platform: api.os.platform(), + type: api.os.type(), + family: api.os.family() + })) + // eslint-disable-next-line security/detect-object-injection + expect(info.platform).toBe(nodePlatformToTauri[platform]) + // eslint-disable-next-line security/detect-object-injection + expect(info.type).toBe(nodePlatformToTauri[platform]) + expect(info.family).toBe(platform === 'win32' ? 'windows' : 'unix') + }) + + it('arch reports a known architecture', async () => { + const arch = await tauri((api) => api.os.arch()) + if (isMobile) { + // The device/simulator is not necessarily the host's architecture (see + // `E2E_ANDROID_TARGET` / `E2E_IOS_TARGET`). + expect(architectures).toContain(arch) + } else { + expect(arch).toBe(nodeArchToTauri[process.arch]) + } + }) + + it('eol and exeExtension match the platform conventions', async () => { + const info = await tauri((api) => ({ + eol: api.os.eol(), + exeExtension: api.os.exeExtension() + })) + expect(info.eol).toBe(platform === 'win32' ? '\r\n' : '\n') + expect(info.exeExtension).toBe(platform === 'win32' ? 'exe' : '') + }) + + it('version reports a non-empty OS version', async () => { + const version = await tauri((api) => api.os.version()) + expect(version.length).toBeGreaterThan(0) + }) + + it('locale is null or a language tag', async () => { + const locale = await tauri((api) => api.os.locale()) + // The plugin forwards the environment's POSIX locale as-is, so a host with + // no locale configured (CI runners default to `LANG=C.UTF-8`) reports the + // POSIX default instead of a language tag. + if (locale !== null && locale !== 'C' && locale !== 'POSIX') { + // e.g. `en-US` + const [language, ...subtags] = locale.split(/[-_]/) + expect(language).toMatch(/^[A-Za-z]{2,3}$/) + for (const subtag of subtags) { + expect(subtag).toMatch(/^[A-Za-z0-9]+$/) + } + } + }) + + it('hostname reports a name', async () => { + const hostname = await tauri((api) => api.os.hostname()) + expect(hostname).not.toBeNull() + expect(hostname!.length).toBeGreaterThan(0) + }) + + itDesktop('hostname matches the host', async () => { + const hostname = await tauri((api) => api.os.hostname()) + // Windows can report the name in a different case than Node does. + expect(hostname!.toLowerCase()).toBe(os.hostname().toLowerCase()) + }) +}) diff --git a/packages/api-e2e/test/specs/plugins.spec.ts b/packages/api-e2e/test/specs/plugins.spec.ts new file mode 100644 index 000000000..79a97a287 --- /dev/null +++ b/packages/api-e2e/test/specs/plugins.spec.ts @@ -0,0 +1,215 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + isMobile, + type CommonPluginApi, + type DesktopPluginApi, + type MobilePluginApi +} from '../helpers/index.js' + +/** + * The members each plugin's `api-iife.js` is expected to define on + * `window.__TAURI__.`. This is the one place the suite covers the + * plugins whose commands cannot be driven from a WebDriver session (dialog + * blocks on native UI, process terminates the app, the mobile plugins need + * hardware or native UI), and it catches a plugin whose global API script is + * missing from its `build.rs`. + */ +type Surface = { [P in keyof T]: (keyof T[P])[] } + +/** Plugins the example registers on every platform. */ +const commonSurface: Surface = { + clipboardManager: [ + 'writeText', + 'readText', + 'writeHtml', + 'clear', + 'readImage', + 'writeImage' + ], + dialog: ['open', 'save', 'message', 'ask', 'confirm'], + fs: [ + 'BaseDirectory', + 'FileHandle', + 'SeekMode', + 'create', + 'open', + 'copyFile', + 'mkdir', + 'readDir', + 'readFile', + 'readTextFile', + 'readTextFileLines', + 'remove', + 'rename', + 'stat', + 'lstat', + 'truncate', + 'writeFile', + 'writeTextFile', + 'exists', + 'watch', + 'watchImmediate', + 'size' + ], + http: ['fetch'], + log: [ + 'LogLevel', + 'error', + 'warn', + 'info', + 'debug', + 'trace', + 'attachLogger', + 'attachConsole' + ], + notification: [ + 'isPermissionGranted', + 'requestPermission', + 'sendNotification', + 'registerActionTypes', + 'pending', + 'cancel', + 'cancelAll', + 'active', + 'removeActive', + 'removeAllActive', + 'createChannel', + 'removeChannel', + 'channels', + 'onNotificationReceived', + 'onAction' + ], + opener: ['openUrl', 'openPath', 'revealItemInDir'], + os: [ + 'eol', + 'platform', + 'family', + 'version', + 'type', + 'arch', + 'locale', + 'exeExtension', + 'hostname' + ], + process: ['exit', 'relaunch'], + shell: ['Command', 'Child', 'EventEmitter', 'open'], + store: ['load', 'getStore', 'LazyStore', 'Store'], + upload: ['download', 'upload', 'HttpMethod'] +} + +/** Plugins the example only registers on desktop. */ +const desktopSurface: Surface = { + cli: ['getMatches'], + globalShortcut: ['register', 'unregister', 'unregisterAll', 'isRegistered'], + updater: ['check', 'Update'], + windowState: [ + 'StateFlags', + 'restoreState', + 'restoreStateCurrent', + 'saveWindowState', + 'filename' + ] +} + +/** Plugins the example only registers on mobile. */ +const mobileSurface: Surface = { + barcodeScanner: [ + 'Format', + 'scan', + 'cancel', + 'checkPermissions', + 'requestPermissions', + 'openAppSettings' + ], + biometric: ['BiometryType', 'checkStatus', 'authenticate'], + geolocation: [ + 'watchPosition', + 'getCurrentPosition', + 'clearWatch', + 'checkPermissions', + 'requestPermissions' + ], + // `ImpactFeedbackStyle` and `NotificationFeedbackType` are type aliases, so + // they are not part of the runtime namespace. + haptics: [ + 'vibrate', + 'impactFeedback', + 'notificationFeedback', + 'selectionFeedback' + ], + nfc: [ + 'NFCTypeNameFormat', + 'TechKind', + 'RTD_TEXT', + 'RTD_URI', + 'record', + 'textRecord', + 'uriRecord', + 'scan', + 'write', + 'isAvailable' + ] +} + +const surface = { + ...commonSurface, + ...(isMobile ? mobileSurface : desktopSurface) +} as Record + +/** The other platform's plugins, which must *not* be in this build. */ +const foreign = Object.keys(isMobile ? desktopSurface : mobileSurface) + +describe('plugin globals', () => { + it('every plugin this platform registers exposes its API on window.__TAURI__', async () => { + const missing = await tauri( + (api, plugins) => + plugins.filter( + (plugin) => + // eslint-disable-next-line security/detect-object-injection + typeof (api as unknown as Record)[plugin] + !== 'object' + ), + Object.keys(surface) + ) + expect(missing).toEqual([]) + }) + + it('the other platform’s plugins are not in the build', async () => { + // Their Rust crates are target-gated in the example's Cargo.toml, so their + // `global_api_script_path` is never injected either. + const present = await tauri( + (api, plugins) => + plugins.filter( + (plugin) => + // eslint-disable-next-line security/detect-object-injection + (api as unknown as Record)[plugin] !== undefined + ), + foreign + ) + expect(present).toEqual([]) + }) + + for (const [plugin, members] of Object.entries(surface)) { + it(`${plugin} exposes its documented members`, async () => { + const missing = await tauri( + (api, plugin, members) => { + const namespaces = api as unknown as Record< + string, + Record + > + // eslint-disable-next-line security/detect-object-injection + const namespace = namespaces[plugin] + return members.filter((member) => !(member in namespace)) + }, + plugin, + members + ) + expect(missing).toEqual([]) + }) + } +}) diff --git a/packages/api-e2e/test/specs/shell.spec.ts b/packages/api-e2e/test/specs/shell.spec.ts new file mode 100644 index 000000000..bfdedacd8 --- /dev/null +++ b/packages/api-e2e/test/specs/shell.spec.ts @@ -0,0 +1,238 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + platform +} from '../helpers/index.js' + +// The example's shell scope allows `sh -c + +
+ + Launching at login is {enabled ? 'enabled' : 'disabled'} +
diff --git a/examples/api/src/views/DeepLink.svelte b/examples/api/src/views/DeepLink.svelte new file mode 100644 index 000000000..9a1a19153 --- /dev/null +++ b/examples/api/src/views/DeepLink.svelte @@ -0,0 +1,67 @@ + + +
+
+ Current deep link: {current ? current.join(', ') : 'none'} +
+ +
+ + + + +
+
diff --git a/examples/api/src/views/Positioner.svelte b/examples/api/src/views/Positioner.svelte new file mode 100644 index 000000000..1f79167c1 --- /dev/null +++ b/examples/api/src/views/Positioner.svelte @@ -0,0 +1,40 @@ + + +
+

+ The tray positions only resolve once the tray icon has been clicked, which + reports its location to the plugin. +

+
+ + + +
+
diff --git a/examples/api/src/views/Sql.svelte b/examples/api/src/views/Sql.svelte new file mode 100644 index 000000000..45e656e44 --- /dev/null +++ b/examples/api/src/views/Sql.svelte @@ -0,0 +1,68 @@ + + +
+
+ + +
+ + {#each todos as todo (todo.id)} +
+ toggle(todo)} + /> + {todo.title} + +
+ {/each} +
diff --git a/examples/api/src/views/Stronghold.svelte b/examples/api/src/views/Stronghold.svelte new file mode 100644 index 000000000..900b2bbba --- /dev/null +++ b/examples/api/src/views/Stronghold.svelte @@ -0,0 +1,71 @@ + + +
+ {#if stronghold} +
+ + + +
+ {:else} +
+ + +
+ {/if} +
diff --git a/examples/api/src/views/WebSocket.svelte b/examples/api/src/views/WebSocket.svelte new file mode 100644 index 000000000..9a91f89ab --- /dev/null +++ b/examples/api/src/views/WebSocket.svelte @@ -0,0 +1,55 @@ + + +
+ {#if ws} +
+ + + +
+ {:else} +
+ + +
+ {/if} +
diff --git a/packages/api-e2e/README.md b/packages/api-e2e/README.md index b5e63929d..e39fc1781 100644 --- a/packages/api-e2e/README.md +++ b/packages/api-e2e/README.md @@ -55,7 +55,8 @@ in the core repository; the `@tauri-apps/api` modules themselves are covered the on the host's loopback; the iOS simulator shares that network stack, and on Android the mobile config runs `adb reverse` so the same `127.0.0.1` URL works on the device. The `http` specs use the echo server the example app itself spawns on port 3003, since that - is the only `http://` origin in the example's http scope. + is the only `http://` origin in the example's http scope. The fixture server also has a + WebSocket echo endpoint (`ws://127.0.0.1:3004/ws`) for the `websocket` specs. ## What is (not) covered @@ -63,28 +64,36 @@ The example only registers each plugin on the platforms it supports, so the suit split three ways: desktop-only plugins are skipped on mobile, mobile-only plugins are skipped on desktop, and the rest run everywhere with the odd test gated. -| Plugin | Coverage | On mobile | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -| `cli` | `getMatches` shape for an argument-less launch. | Skipped — desktop-only plugin. | -| `clipboard-manager` | text, HTML and image round-trips, `clear`, error paths. | Text only; HTML and images are unsupported there. | -| `dialog` | Only that the API is registered — every dialog blocks on native UI the driver cannot operate. | Same. | -| `fs` | read/write/stat/copy/rename/remove/truncate, `FileHandle`, line iteration, watchers, scope. | All but the watchers (`fs:allow-watch` is desktop-only here). | -| `global-shortcut` | register/unregister/isRegistered/unregisterAll and error paths. Shortcuts cannot be triggered. | Skipped — desktop-only plugin. | -| `http` | `fetch` methods, headers, JSON/bytes/multipart bodies, the cookie jar, abort, scope, failures. | Same. | -| `log` | `attachLogger`/`attachConsole` for webview and Rust records, level filtering. | Same. | -| `notification` | Permission model, `window.Notification` override, `sendNotification`. Display is not observable. | Sending only — see the note below. | -| `opener` | Scope enforcement only — a successful open launches an external app the suite cannot close. | Same. | -| `os` | Every function, compared against what the app was built for. | Same, minus the hostname/host comparison. | -| `process` | Only that the API is registered — `exit`/`relaunch` terminate the app under test. | Same. | -| `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. | -| `store` | CRUD, persistence, auto-save, defaults/reset, reload, `getStore`, `LazyStore`, change events. | Same. | -| `updater` | `check` against the fixture manifest (update / 204 / older release). Installing is never exercised. | Skipped — desktop-only plugin. | -| `upload` | `download` and `upload` with progress, methods, headers and error paths. | Same (through `adb reverse` on Android). | -| `window-state` | `filename`, save/restore, and that a re-created window gets its saved size (WM dependent). | Skipped — desktop-only plugin. | -| mobile-only plugins | `barcode-scanner`, `biometric`, `geolocation`, `haptics` and `nfc` need hardware or native UI the driver cannot operate, so only their global API surface is asserted. | Only there. | +| Plugin | Coverage | On mobile | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `autostart` | enable/disable/isEnabled, restoring the host's original state. | Skipped — desktop-only plugin. | +| `cli` | `getMatches` shape for an argument-less launch. | Skipped — desktop-only plugin. | +| `clipboard-manager` | text, HTML and image round-trips, `clear`, error paths. | Text only; HTML and images are unsupported there. | +| `deep-link` | `getCurrent` on a plain launch, `onOpenUrl` delivery, runtime `register`/`isRegistered`/`unregister` on Linux and Windows. | Only that runtime registration is unsupported. | +| `dialog` | Only that the API is registered — every dialog blocks on native UI the driver cannot operate. | Same. | +| `fs` | read/write/stat/copy/rename/remove/truncate, `FileHandle`, line iteration, watchers, scope. | All but the watchers (`fs:allow-watch` is desktop-only here). | +| `global-shortcut` | register/unregister/isRegistered/unregisterAll and error paths. Shortcuts cannot be triggered. | Skipped — desktop-only plugin. | +| `http` | `fetch` methods, headers, JSON/bytes/multipart bodies, the cookie jar, abort, scope, failures. | Same. | +| `log` | `attachLogger`/`attachConsole` for webview and Rust records, level filtering. | Same. | +| `notification` | Permission model and the `window.Notification` override. Sending is fire-and-forget and display is not observable. | Action types, pending/active lists after cancel/remove, channels, listeners — see below. | +| `opener` | Scope enforcement only — a successful open launches an external app the suite cannot close. | Same. | +| `os` | Every function but `version` (which has nothing to compare against), checked against what the app was built for and the host. | Same, minus `hostname`. | +| `positioner` | Screen and tray positions (with a tray rect handed in through `handleIconState`), `moveWindowConstrained`, the missing-tray error (WM dependent). | Skipped — desktop-only plugin. | +| `process` | Only that the API is registered — `exit`/`relaunch` terminate the app under test. | Same. | +| `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. | +| `sql` | SQLite `load`/`execute`/`select`/`close`, bound values, column types, app-registered migrations, error paths. | Same. | +| `store` | CRUD, persistence, auto-save, defaults/reset, reload, `getStore`, `LazyStore`, change events. | Same. | +| `stronghold` | Store records (persisted across reloads), wrong-password and unknown-client errors, vault secrets, BIP39/SLIP10 derivation and Ed25519 signing. | Same. | +| `updater` | `check` against the fixture manifest (update / 204 / older release). Installing is never exercised. | Skipped — desktop-only plugin. | +| `upload` | `download` and `upload` with progress, methods, headers and error paths. | Same (through `adb reverse` on Android). | +| `websocket` | Text/binary echo, ping/pong, handshake headers, listener removal, server and client close, connection and argument errors, against the fixture server. | Same (through `adb reverse` on Android). | +| `window-state` | `filename`, save/restore, and that a re-created window gets its saved size (WM dependent). | Skipped — desktop-only plugin. | +| mobile-only plugins | `barcode-scanner`, `biometric`, `geolocation`, `haptics` and `nfc` need hardware or native UI the driver cannot operate, so only their global API surface is asserted. | Only there. | The notification permission specs are desktop-only: a mobile app starts out ungranted and -`requestPermission` puts up a system dialog the session would then block on. +`requestPermission` puts up a system dialog the session would then block on. The rest of the +notification API (action types, channels, the pending/active lists, listeners) only exists on +mobile, and is covered there without the permission. The [`plugins.spec.ts`](test/specs/plugins.spec.ts) spec additionally asserts that every plugin the platform registers injects its global API with its documented members, and that diff --git a/packages/api-e2e/package.json b/packages/api-e2e/package.json index f0e29eac6..b8226b538 100644 --- a/packages/api-e2e/package.json +++ b/packages/api-e2e/package.json @@ -14,12 +14,14 @@ }, "devDependencies": { "@crabnebula/tauri-driver": "^2.0.9", - "@crabnebula/test-runner-backend": "^0.2.9", + "@crabnebula/test-runner-backend": "^0.3.0", "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-autostart": "workspace:*", "@tauri-apps/plugin-barcode-scanner": "workspace:*", "@tauri-apps/plugin-biometric": "workspace:*", "@tauri-apps/plugin-cli": "workspace:*", "@tauri-apps/plugin-clipboard-manager": "workspace:*", + "@tauri-apps/plugin-deep-link": "workspace:*", "@tauri-apps/plugin-dialog": "workspace:*", "@tauri-apps/plugin-fs": "workspace:*", "@tauri-apps/plugin-geolocation": "workspace:*", @@ -31,13 +33,18 @@ "@tauri-apps/plugin-notification": "workspace:*", "@tauri-apps/plugin-opener": "workspace:*", "@tauri-apps/plugin-os": "workspace:*", + "@tauri-apps/plugin-positioner": "workspace:*", "@tauri-apps/plugin-process": "workspace:*", "@tauri-apps/plugin-shell": "workspace:*", + "@tauri-apps/plugin-sql": "workspace:*", "@tauri-apps/plugin-store": "workspace:*", + "@tauri-apps/plugin-stronghold": "workspace:*", "@tauri-apps/plugin-updater": "workspace:*", "@tauri-apps/plugin-upload": "workspace:*", + "@tauri-apps/plugin-websocket": "workspace:*", "@tauri-apps/plugin-window-state": "workspace:*", "@types/node": "^22.10.0", + "@types/ws": "^8.18.1", "@wdio/appium-service": "^9.31.9", "@wdio/cli": "^9.31.9", "@wdio/globals": "^9.31.3", @@ -49,6 +56,7 @@ "appium-xcuitest-driver": "^12.12.4", "cross-env": "^10.1.0", "typescript": "6.0.3", - "webdriverio": "^9.31.9" + "webdriverio": "^9.31.9", + "ws": "^8.18.0" } } diff --git a/packages/api-e2e/test/helpers/index.ts b/packages/api-e2e/test/helpers/index.ts index 8177f5bba..6da724c30 100644 --- a/packages/api-e2e/test/helpers/index.ts +++ b/packages/api-e2e/test/helpers/index.ts @@ -4,10 +4,12 @@ import { browser } from '@wdio/globals' import type * as TauriApi from '@tauri-apps/api' +import type * as Autostart from '@tauri-apps/plugin-autostart' import type * as BarcodeScanner from '@tauri-apps/plugin-barcode-scanner' import type * as Biometric from '@tauri-apps/plugin-biometric' import type * as Cli from '@tauri-apps/plugin-cli' import type * as ClipboardManager from '@tauri-apps/plugin-clipboard-manager' +import type * as DeepLink from '@tauri-apps/plugin-deep-link' import type * as Dialog from '@tauri-apps/plugin-dialog' import type * as Fs from '@tauri-apps/plugin-fs' import type * as Geolocation from '@tauri-apps/plugin-geolocation' @@ -19,20 +21,26 @@ import type * as Nfc from '@tauri-apps/plugin-nfc' import type * as Notification from '@tauri-apps/plugin-notification' import type * as Opener from '@tauri-apps/plugin-opener' import type * as Os from '@tauri-apps/plugin-os' +import type * as Positioner from '@tauri-apps/plugin-positioner' import type * as Process from '@tauri-apps/plugin-process' import type * as Shell from '@tauri-apps/plugin-shell' +import type * as Sql from '@tauri-apps/plugin-sql' import type * as Store from '@tauri-apps/plugin-store' +import type * as Stronghold from '@tauri-apps/plugin-stronghold' import type * as Updater from '@tauri-apps/plugin-updater' import type * as Upload from '@tauri-apps/plugin-upload' +import type * as WebSocket from '@tauri-apps/plugin-websocket' import type * as WindowState from '@tauri-apps/plugin-window-state' /** * The plugin APIs the example registers on every platform, keyed by the name * each plugin's `api-iife.js` defines on `window.__TAURI__` (the package name - * without the `@tauri-apps/plugin-` prefix, camel-cased). + * without the `@tauri-apps/plugin-` prefix, camel-cased). `sql` and `websocket` + * only have a default export, so their global is that class itself. */ export interface CommonPluginApi { clipboardManager: typeof ClipboardManager + deepLink: typeof DeepLink dialog: typeof Dialog fs: typeof Fs http: typeof Http @@ -42,14 +50,19 @@ export interface CommonPluginApi { os: typeof Os process: typeof Process shell: typeof Shell + sql: typeof Sql.default store: typeof Store + stronghold: typeof Stronghold upload: typeof Upload + websocket: typeof WebSocket.default } /** The plugin APIs the example only registers on desktop (`#[cfg(desktop)]`). */ export interface DesktopPluginApi { + autostart: typeof Autostart cli: typeof Cli globalShortcut: typeof GlobalShortcut + positioner: typeof Positioner updater: typeof Updater windowState: typeof WindowState } @@ -231,8 +244,9 @@ const skippedModules = (process.env.E2E_SKIP ?? '') export interface DescribePluginOptions { /** - * The example only registers the plugin on desktop (`cli`, `global-shortcut`, - * `updater`, `window-state`), so the whole suite is skipped on mobile. + * The example only registers the plugin on desktop (`autostart`, `cli`, + * `global-shortcut`, `positioner`, `updater`, `window-state`), so the whole + * suite is skipped on mobile. */ desktopOnly?: boolean /** diff --git a/packages/api-e2e/test/helpers/server.ts b/packages/api-e2e/test/helpers/server.ts index 1c0f9d649..204f74107 100644 --- a/packages/api-e2e/test/helpers/server.ts +++ b/packages/api-e2e/test/helpers/server.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT import http from 'node:http' +import { WebSocketServer } from 'ws' /** * Where the fixture server listens. The port is fixed because the updater @@ -23,6 +24,13 @@ export const UPDATER_FIXTURE_OLDER_VERSION = '1.0.0' export const DOWNLOAD_FIXTURE_BODY = 'hello from the plugins e2e fixture server\n'.repeat(64) +/** WebSocket endpoint of the fixture server (the `ws://` twin of {@link FIXTURE_SERVER_URL}). */ +export const WEBSOCKET_FIXTURE_URL = `ws://127.0.0.1:${FIXTURE_SERVER_PORT}/ws` +/** Text message that makes the `/ws` endpoint close the connection with {@link WEBSOCKET_CLOSE_CODE}. */ +export const WEBSOCKET_CLOSE_REQUEST = 'close-me' +export const WEBSOCKET_CLOSE_CODE = 4000 +export const WEBSOCKET_CLOSE_REASON = 'closed by the fixture server' + export interface FixtureServer { close(): void } @@ -38,6 +46,10 @@ export interface FixtureServer { * - `GET /download` — {@link DOWNLOAD_FIXTURE_BODY} with a `Content-Length`. * - `* /echo` — a JSON description of the request (`method`, `url`, `headers` * and the utf-8 `body`). + * - `ws /ws` — a WebSocket echo endpoint: text and binary messages are sent + * back as-is, and {@link WEBSOCKET_CLOSE_REQUEST} makes the server close the + * connection with {@link WEBSOCKET_CLOSE_CODE}. On `/ws/headers` the server + * first sends the upgrade request's headers as a JSON text message. */ export function startFixtureServer(): Promise { const server = http.createServer((req, res) => { @@ -91,12 +103,39 @@ export function startFixtureServer(): Promise { }) }) + const wss = new WebSocketServer({ noServer: true }) + server.on('upgrade', (req, socket, head) => { + const { pathname } = new URL(req.url ?? '/', FIXTURE_SERVER_URL) + if (pathname !== '/ws' && pathname !== '/ws/headers') { + socket.destroy() + return + } + wss.handleUpgrade(req, socket, head, (ws) => { + if (pathname === '/ws/headers') { + ws.send(JSON.stringify(req.headers)) + } + ws.on('message', (data, isBinary) => { + if ( + !isBinary + && Buffer.isBuffer(data) + && data.toString('utf8') === WEBSOCKET_CLOSE_REQUEST + ) { + ws.close(WEBSOCKET_CLOSE_CODE, WEBSOCKET_CLOSE_REASON) + } else { + ws.send(data, { binary: isBinary }) + } + }) + }) + }) + return new Promise((resolve, reject) => { server.once('error', reject) server.listen(FIXTURE_SERVER_PORT, '127.0.0.1', () => { server.off('error', reject) resolve({ close: () => { + for (const client of wss.clients) client.terminate() + wss.close() server.closeAllConnections() server.close() } diff --git a/packages/api-e2e/test/specs/autostart.spec.ts b/packages/api-e2e/test/specs/autostart.spec.ts new file mode 100644 index 000000000..20a09a00c --- /dev/null +++ b/packages/api-e2e/test/specs/autostart.spec.ts @@ -0,0 +1,49 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describePlugin } from '../helpers/index.js' + +// Enabling autostart registers the app with the host for real (a Launch Agent +// on macOS, an XDG autostart entry on Linux, a `Run` registry value on +// Windows), so the spec puts back whatever state it found. +// +// `disable` is only called while autostart is enabled: on Windows it fails +// when there is no `Run` value to delete. + +describePlugin('autostart', { desktopOnly: true }, () => { + let initiallyEnabled = false + + before(async () => { + initiallyEnabled = await tauri((api) => api.autostart.isEnabled()) + }) + + after(async () => { + await tauri(async (api, enabled) => { + if ((await api.autostart.isEnabled()) === enabled) return + await (enabled ? api.autostart.enable() : api.autostart.disable()) + }, initiallyEnabled) + }) + + it('enable and disable toggle isEnabled', async () => { + const states = await tauri(async (api) => { + await api.autostart.enable() + const enabled = await api.autostart.isEnabled() + await api.autostart.disable() + return { enabled, disabled: await api.autostart.isEnabled() } + }) + expect(states).toEqual({ enabled: true, disabled: false }) + }) + + it('enabling twice keeps it enabled', async () => { + const states = await tauri(async (api) => { + await api.autostart.enable() + await api.autostart.enable() + const enabled = await api.autostart.isEnabled() + await api.autostart.disable() + return { enabled, disabled: await api.autostart.isEnabled() } + }) + expect(states).toEqual({ enabled: true, disabled: false }) + }) +}) diff --git a/packages/api-e2e/test/specs/clipboard-manager.spec.ts b/packages/api-e2e/test/specs/clipboard-manager.spec.ts index e4be7c59a..f5bf0e428 100644 --- a/packages/api-e2e/test/specs/clipboard-manager.spec.ts +++ b/packages/api-e2e/test/specs/clipboard-manager.spec.ts @@ -7,7 +7,8 @@ import { tauri, tauriError, describePlugin, - itDesktop + itDesktop, + isMobile } from '../helpers/index.js' // The mobile implementation only carries plain text: `write_html`, `write_image` @@ -133,7 +134,11 @@ describePlugin('clipboard-manager', () => { await api.clipboardManager.writeText('not an image') await api.clipboardManager.readImage() }) - expect(message.length).toBeGreaterThan(0) + expect(message).toMatch( + isMobile + ? /Unsupported on this platform/ + : /not available in the requested format/ + ) }) it('clear empties the clipboard', async () => { diff --git a/packages/api-e2e/test/specs/deep-link.spec.ts b/packages/api-e2e/test/specs/deep-link.spec.ts new file mode 100644 index 000000000..a818e8f17 --- /dev/null +++ b/packages/api-e2e/test/specs/deep-link.spec.ts @@ -0,0 +1,99 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + itOn, + platform +} from '../helpers/index.js' + +// The suite launches the app without a URL, so there is no current deep link, +// and it cannot open one through the OS either. `onOpenUrl` is exercised by +// emitting the event the plugin itself emits when the app is opened with one. +// +// Registering a scheme at runtime is only implemented on Linux (a `.desktop` +// handler plus `xdg-mime`) and Windows (the registry); macOS and mobile read +// the schemes from the bundle and report the runtime APIs as unsupported. + +/** A scheme no real app handles, so registering it cannot clobber anything. */ +const scheme = 'tauri-plugins-e2e' + +describePlugin('deep-link', () => { + it('getCurrent is null when the app was not opened with a URL', async () => { + expect(await tauri((api) => api.deepLink.getCurrent())).toBeNull() + }) + + it('onOpenUrl delivers the opened URLs until unlistened', async () => { + const received = await tauri( + async (api, urls) => { + const received: string[][] = [] + const unlisten = await api.deepLink.onOpenUrl((urls) => + received.push(urls) + ) + await api.event.emit('deep-link://new-url', urls) + await new Promise((resolve) => setTimeout(resolve, 500)) + unlisten() + await api.event.emit('deep-link://new-url', ['ignored://']) + await new Promise((resolve) => setTimeout(resolve, 500)) + return received + }, + [`${scheme}://open/path?query=1`] + ) + expect(received).toEqual([[`${scheme}://open/path?query=1`]]) + }) + + itOn( + ['linux', 'win32'], + 'register makes the app the scheme handler', + async () => { + const result = await tauri(async (api, scheme) => { + await api.deepLink.register(scheme) + const registered = await api.deepLink.isRegistered(scheme) + await api.deepLink.unregister(scheme) + return { + registered, + afterUnregister: await api.deepLink.isRegistered(scheme) + } + }, scheme) + expect(result.registered).toBe(true) + // On Linux `xdg-mime` falls back to the desktop database, which still lists + // the handler after its `mimeapps.list` default is removed. + if (platform === 'win32') { + expect(result.afterUnregister).toBe(false) + } + } + ) + + itOn('win32', 'isRegistered is false for an unknown scheme', async () => { + expect( + await tauri( + (api, scheme) => api.deepLink.isRegistered(scheme), + `${scheme}-unknown` + ) + ).toBe(false) + }) + + itOn( + ['darwin', 'android', 'ios'], + 'runtime registration is unsupported', + async () => { + for (const command of [ + 'register', + 'unregister', + 'isRegistered' + ] as const) { + const error = await tauriError( + // eslint-disable-next-line security/detect-object-injection + (api, command, scheme) => api.deepLink[command](scheme), + command, + scheme + ) + expect(error).toMatch(/unsupported platform/i) + } + } + ) +}) diff --git a/packages/api-e2e/test/specs/fs.spec.ts b/packages/api-e2e/test/specs/fs.spec.ts index 5f1b39154..d590e0395 100644 --- a/packages/api-e2e/test/specs/fs.spec.ts +++ b/packages/api-e2e/test/specs/fs.spec.ts @@ -276,7 +276,7 @@ describePlugin('fs', () => { await file.close() await file.stat() }, `${dir}/closed.txt`) - expect(message.length).toBeGreaterThan(0) + expect(message).toMatch(/resource id \d+ is invalid/) }) it('rejects paths outside the configured scope', async () => { @@ -339,8 +339,9 @@ describePlugin('fs', () => { unwatch() return events }, `${dir}/watched`) - expect(result.length).toBeGreaterThan(0) - expect(result.every((e) => typeof e.kind === 'string')).toBe(true) + expect( + result.some((e) => e.paths.some((p) => p.endsWith('touched.txt'))) + ).toBe(true) } ) diff --git a/packages/api-e2e/test/specs/global-shortcut.spec.ts b/packages/api-e2e/test/specs/global-shortcut.spec.ts index c0b0980ee..89aa30076 100644 --- a/packages/api-e2e/test/specs/global-shortcut.spec.ts +++ b/packages/api-e2e/test/specs/global-shortcut.spec.ts @@ -3,7 +3,12 @@ // SPDX-License-Identifier: MIT import { expect } from '@wdio/globals' -import { tauri, tauriError, describePlugin } from '../helpers/index.js' +import { + tauri, + tauriError, + describePlugin, + platform +} from '../helpers/index.js' // Triggering a shortcut needs OS-level synthetic input the WebDriver session // cannot produce, so the specs cover the registry (register / isRegistered / @@ -74,9 +79,19 @@ describePlugin('global-shortcut', { desktopOnly: true }, () => { it('registering the same shortcut twice rejects', async () => { const message = await tauriError(async (api) => { await api.globalShortcut.register('Alt+Shift+F11', () => {}) - await api.globalShortcut.register('Alt+Shift+F11', () => {}) + try { + await api.globalShortcut.register('Alt+Shift+F11', () => {}) + } finally { + await api.globalShortcut.unregister('Alt+Shift+F11') + } }) - expect(message).toMatch(/already registered/i) + // macOS does not check for duplicates itself: the OS rejects the second + // `RegisterEventHotKey` call and the error is the generic one + expect(message).toMatch( + platform === 'darwin' + ? /RegisterEventHotKey failed for F11/ + : /already registered/i + ) }) it('rejects shortcuts that cannot be parsed', async () => { diff --git a/packages/api-e2e/test/specs/http.spec.ts b/packages/api-e2e/test/specs/http.spec.ts index 9ccfbc656..cd283200a 100644 --- a/packages/api-e2e/test/specs/http.spec.ts +++ b/packages/api-e2e/test/specs/http.spec.ts @@ -144,6 +144,6 @@ describePlugin('http', () => { api.http.fetch(url, { proxy: { all: 'http://127.0.0.1:1' } }), echoServer ) - expect(message.length).toBeGreaterThan(0) + expect(message).toMatch(/error sending request/) }) }) diff --git a/packages/api-e2e/test/specs/notification.spec.ts b/packages/api-e2e/test/specs/notification.spec.ts index d4d95476f..c67256c44 100644 --- a/packages/api-e2e/test/specs/notification.spec.ts +++ b/packages/api-e2e/test/specs/notification.spec.ts @@ -3,15 +3,25 @@ // SPDX-License-Identifier: MIT import { expect } from '@wdio/globals' -import { tauri, describePlugin, itDesktop } from '../helpers/index.js' +import { + tauri, + tauriError, + describePlugin, + itDesktop, + itOn +} from '../helpers/index.js' // Whether a notification actually shows up depends on the desktop session // (a notification daemon on Linux, the app's registration on Windows and -// macOS), which the suite cannot observe. The specs cover the permission -// model and that sending does not error out synchronously. +// macOS), which the suite cannot observe, and sending is fire-and-forget, so +// only the permission model is covered on desktop. // // The permission specs are desktop-only: mobile starts out ungranted and // `requestPermission` puts up a system dialog the session would then block on. +// +// Scheduling, action types, channels, the pending/active lists and the +// listeners are only implemented on mobile. None of them need the permission, +// so they are covered there on an app that has not been granted it. describePlugin('notification', () => { itDesktop('permission is granted on desktop', async () => { @@ -30,29 +40,102 @@ describePlugin('notification', () => { expect(result).toEqual({ permission: 'granted', requested: 'granted' }) }) - it('sendNotification accepts a title string and an options object', async () => { - const result = await tauri((api) => { - api.notification.sendNotification('notification from e2e') - api.notification.sendNotification({ - title: 'notification from e2e', - body: 'with a body', - sound: 'default' + itOn( + ['android', 'ios'], + 'action types register, and cancel/remove leave nothing pending or active', + async () => { + const result = await tauri(async (api) => { + await api.notification.registerActionTypes([ + { + id: 'e2e-actions', + actions: [ + { id: 'reply', title: 'Reply', input: true }, + { id: 'dismiss', title: 'Dismiss', destructive: true } + ] + } + ]) + await api.notification.cancel([424242]) + await api.notification.cancelAll() + await api.notification.removeActive([{ id: 424242 }]) + await api.notification.removeAllActive() + return { + pending: await api.notification.pending(), + active: await api.notification.active() + } }) - return true + expect(result).toEqual({ pending: [], active: [] }) + } + ) + + itOn('android', 'channels can be created, listed and removed', async () => { + const result = await tauri(async (api) => { + const { Importance, Visibility } = api.notification + await api.notification.createChannel({ + id: 'e2e-channel', + name: 'e2e channel', + description: 'created by the e2e suite', + importance: Importance.High, + visibility: Visibility.Public, + vibration: true + }) + const created = (await api.notification.channels()).find( + (channel) => channel.id === 'e2e-channel' + ) + await api.notification.removeChannel('e2e-channel') + const removed = (await api.notification.channels()).some( + (channel) => channel.id === 'e2e-channel' + ) + return { + created: created && { + name: created.name, + description: created.description, + importance: created.importance, + vibration: created.vibration + }, + removed + } + }) + expect(result).toEqual({ + created: { + name: 'e2e channel', + description: 'created by the e2e suite', + importance: 4, // Importance.High + vibration: true + }, + removed: false }) - expect(result).toBe(true) }) - it('new Notification() goes through the plugin', async () => { - const result = await tauri(() => { - const notification = new window.Notification( - 'window.Notification from e2e', - { - body: 'created through the DOM API' - } + itOn('ios', 'channels are not implemented', async () => { + for (const call of ['create', 'remove', 'list'] as const) { + const error = await tauriError( + (api, call) => + call === 'create' + ? api.notification.createChannel({ id: 'e2e', name: 'e2e' }) + : call === 'remove' + ? api.notification.removeChannel('e2e') + : api.notification.channels(), + call ) - return typeof notification === 'object' - }) - expect(result).toBe(true) + expect(error).toMatch(/not implemented/) + } }) + + itOn( + ['android', 'ios'], + 'onNotificationReceived and onAction listeners register and unregister', + async () => { + const result = await tauri(async (api) => { + const received = await api.notification.onNotificationReceived(() => {}) + const action = await api.notification.onAction(() => {}) + await received.unregister() + await action.unregister() + return { received: received.event, action: action.event } + }) + expect(result).toEqual({ + received: 'notification', + action: 'actionPerformed' + }) + } + ) }) diff --git a/packages/api-e2e/test/specs/opener.spec.ts b/packages/api-e2e/test/specs/opener.spec.ts index 27201c0c8..d5e1b733c 100644 --- a/packages/api-e2e/test/specs/opener.spec.ts +++ b/packages/api-e2e/test/specs/opener.spec.ts @@ -41,6 +41,6 @@ describePlugin('opener', () => { await api.path.join(await api.path.appDataDir(), 'does-not-exist-e2e') ) ) - expect(message.length).toBeGreaterThan(0) + expect(message).toMatch(/os error 2/) }) }) diff --git a/packages/api-e2e/test/specs/os.spec.ts b/packages/api-e2e/test/specs/os.spec.ts index fcad729df..82484752e 100644 --- a/packages/api-e2e/test/specs/os.spec.ts +++ b/packages/api-e2e/test/specs/os.spec.ts @@ -86,11 +86,6 @@ describePlugin('os', () => { expect(info.exeExtension).toBe(platform === 'win32' ? 'exe' : '') }) - it('version reports a non-empty OS version', async () => { - const version = await tauri((api) => api.os.version()) - expect(version.length).toBeGreaterThan(0) - }) - it('locale is null or a language tag', async () => { const locale = await tauri((api) => api.os.locale()) // The plugin forwards the environment's POSIX locale as-is, so a host with @@ -106,12 +101,6 @@ describePlugin('os', () => { } }) - it('hostname reports a name', async () => { - const hostname = await tauri((api) => api.os.hostname()) - expect(hostname).not.toBeNull() - expect(hostname!.length).toBeGreaterThan(0) - }) - itDesktop('hostname matches the host', async () => { const hostname = await tauri((api) => api.os.hostname()) // Windows can report the name in a different case than Node does. diff --git a/packages/api-e2e/test/specs/plugins.spec.ts b/packages/api-e2e/test/specs/plugins.spec.ts index 79a97a287..7fdc00997 100644 --- a/packages/api-e2e/test/specs/plugins.spec.ts +++ b/packages/api-e2e/test/specs/plugins.spec.ts @@ -31,6 +31,13 @@ const commonSurface: Surface = { 'readImage', 'writeImage' ], + deepLink: [ + 'getCurrent', + 'register', + 'unregister', + 'isRegistered', + 'onOpenUrl' + ], dialog: ['open', 'save', 'message', 'ask', 'confirm'], fs: [ 'BaseDirectory', @@ -98,14 +105,26 @@ const commonSurface: Surface = { ], process: ['exit', 'relaunch'], shell: ['Command', 'Child', 'EventEmitter', 'open'], + // the `Database` class itself + sql: ['load', 'get'], store: ['load', 'getStore', 'LazyStore', 'Store'], - upload: ['download', 'upload', 'HttpMethod'] + stronghold: ['Location', 'Client', 'Store', 'Vault', 'Stronghold'], + upload: ['download', 'upload', 'HttpMethod'], + // the `WebSocket` class itself + websocket: ['connect'] } /** Plugins the example only registers on desktop. */ const desktopSurface: Surface = { + autostart: ['enable', 'disable', 'isEnabled'], cli: ['getMatches'], globalShortcut: ['register', 'unregister', 'unregisterAll', 'isRegistered'], + positioner: [ + 'Position', + 'moveWindow', + 'moveWindowConstrained', + 'handleIconState' + ], updater: ['check', 'Update'], windowState: [ 'StateFlags', @@ -168,12 +187,12 @@ describe('plugin globals', () => { it('every plugin this platform registers exposes its API on window.__TAURI__', async () => { const missing = await tauri( (api, plugins) => - plugins.filter( - (plugin) => - // eslint-disable-next-line security/detect-object-injection - typeof (api as unknown as Record)[plugin] - !== 'object' - ), + plugins.filter((plugin) => { + // eslint-disable-next-line security/detect-object-injection + const global = (api as unknown as Record)[plugin] + // a module namespace, or the class a default-export-only module (sql, websocket) defines + return typeof global !== 'object' && typeof global !== 'function' + }), Object.keys(surface) ) expect(missing).toEqual([]) diff --git a/packages/api-e2e/test/specs/positioner.spec.ts b/packages/api-e2e/test/specs/positioner.spec.ts new file mode 100644 index 000000000..f2cfadf9f --- /dev/null +++ b/packages/api-e2e/test/specs/positioner.spec.ts @@ -0,0 +1,137 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, tauriError, describePlugin, itWm } from '../helpers/index.js' + +// Positions are computed from the window's current monitor and outer size, so +// the specs compute the expected physical position the same way and compare +// once the window manager has applied the move. The tray positions need the +// tray icon's rect, which normally comes from a click on the icon; the spec +// hands one in through `handleIconState`, as a JS tray event handler would. +// The moved window is not put back: each spec file gets a fresh app. + +interface Geometry { + monitor: { x: number; y: number; width: number; height: number } + window: { width: number; height: number } +} + +/** Moves the main window and reports where it ended up along with the geometry the position is derived from. */ +function moveAndMeasure(position: string, constrained = false) { + return tauri( + async (api, position, constrained) => { + const win = api.window.getCurrentWindow() + const { Position } = api.positioner + const value = Position[position as keyof typeof Position] + await (constrained + ? api.positioner.moveWindowConstrained(value) + : api.positioner.moveWindow(value)) + await new Promise((resolve) => setTimeout(resolve, 500)) + const monitor = await api.window.currentMonitor() + const size = await win.outerSize() + const outer = await win.outerPosition() + return { + monitor: { + x: monitor!.position.x, + y: monitor!.position.y, + width: monitor!.size.width, + height: monitor!.size.height + }, + window: { width: size.width, height: size.height }, + position: { x: outer.x, y: outer.y } + } + }, + position, + constrained + ) +} + +function center({ monitor, window }: Geometry) { + return { + x: monitor.x + Math.trunc(monitor.width / 2) - Math.trunc(window.width / 2), + y: + monitor.y + Math.trunc(monitor.height / 2) - Math.trunc(window.height / 2) + } +} + +describePlugin('positioner', { desktopOnly: true }, () => { + // must run before any tray rect is handed in + it('tray positions are rejected until the tray icon reported its rect', async () => { + const error = await tauriError((api) => + api.positioner.moveWindow(api.positioner.Position.TrayCenter) + ) + expect(error).toMatch(/Tray position not set/) + }) + + it('the Position enum matches the plugin', async () => { + const names = await tauri((api) => + Object.keys(api.positioner.Position).filter((key) => isNaN(Number(key))) + ) + expect(names).toEqual([ + 'TopLeft', + 'TopRight', + 'BottomLeft', + 'BottomRight', + 'TopCenter', + 'BottomCenter', + 'LeftCenter', + 'RightCenter', + 'Center', + 'TrayLeft', + 'TrayBottomLeft', + 'TrayRight', + 'TrayBottomRight', + 'TrayCenter', + 'TrayBottomCenter' + ]) + }) + + itWm('moveWindow centers the window on its monitor', async () => { + const result = await moveAndMeasure('Center') + expect(result.position).toEqual(center(result)) + }) + + itWm( + 'moveWindowConstrained behaves like moveWindow for screen positions', + async () => { + const result = await moveAndMeasure('Center', true) + expect(result.position).toEqual(center(result)) + } + ) + + itWm('tray positions are relative to the reported tray rect', async () => { + // a tray "icon" in the middle of the window's monitor, far from any edge, + // so neither the OS-specific flips nor the constraint kick in + const tray = await tauri(async (api) => { + const monitor = await api.window.currentMonitor() + const rect = { + x: monitor!.position.x + Math.trunc(monitor!.size.width / 2), + y: monitor!.position.y + Math.trunc(monitor!.size.height / 2), + width: 20, + height: 20 + } + await api.positioner.handleIconState({ + type: 'Click', + id: 'e2e', + position: new api.dpi.PhysicalPosition(rect.x, rect.y), + rect: { + position: new api.dpi.PhysicalPosition(rect.x, rect.y), + size: new api.dpi.PhysicalSize(rect.width, rect.height) + }, + button: 'Left', + buttonState: 'Down' + }) + return rect + }) + + const bottomLeft = await moveAndMeasure('TrayBottomLeft') + expect(bottomLeft.position).toEqual({ x: tray.x, y: tray.y }) + + const bottomCenter = await moveAndMeasure('TrayBottomCenter', true) + expect(bottomCenter.position).toEqual({ + x: tray.x + tray.width / 2 - Math.trunc(bottomCenter.window.width / 2), + y: tray.y + }) + }) +}) diff --git a/packages/api-e2e/test/specs/shell.spec.ts b/packages/api-e2e/test/specs/shell.spec.ts index bfdedacd8..d43ce3020 100644 --- a/packages/api-e2e/test/specs/shell.spec.ts +++ b/packages/api-e2e/test/specs/shell.spec.ts @@ -233,6 +233,6 @@ describePlugin('shell', () => { const message = await tauriError((api) => api.shell.open('ftp://example.com') ) - expect(message.length).toBeGreaterThan(0) + expect(message).toMatch(/failed regex validation/) }) }) diff --git a/packages/api-e2e/test/specs/sql.spec.ts b/packages/api-e2e/test/specs/sql.spec.ts new file mode 100644 index 000000000..f20f28a31 --- /dev/null +++ b/packages/api-e2e/test/specs/sql.spec.ts @@ -0,0 +1,168 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, tauriError, describePlugin } from '../helpers/index.js' + +// SQLite databases live in the app config directory. The spec works on its +// own database, recreating its table before each test, and only reads the +// example's `sqlite:api.db` to check the migration the app registers for it. + +const db = 'sqlite:e2e-sql.db' + +interface Row { + id: number + name: string + score: number | null + ratio: number | null +} + +describePlugin('sql', () => { + beforeEach(async () => { + await tauri(async (api, db) => { + const database = await api.sql.load(db) + await database.execute('DROP TABLE IF EXISTS e2e') + await database.execute( + 'CREATE TABLE e2e (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, score INTEGER, ratio REAL)' + ) + }, db) + }) + + it('load resolves with a database for the given path', async () => { + const path = await tauri( + async (api, db) => (await api.sql.load(db)).path, + db + ) + expect(path).toBe(db) + }) + + it('migrations registered by the app run when their database is loaded', async () => { + const result = await tauri(async (api) => { + const database = await api.sql.load('sqlite:api.db') + return { + tables: await database.select<{ name: string }[]>( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'todos'" + ), + migrations: await database.select< + { version: number; description: string; success: number }[] + >('SELECT version, description, success FROM _sqlx_migrations') + } + }) + expect(result.tables).toEqual([{ name: 'todos' }]) + // `success` is declared BOOLEAN but stored, and read back, as an integer + expect(result.migrations).toEqual([ + { version: 1, description: 'create_todos_table', success: 1 } + ]) + }) + + it('execute reports affected rows and the last insert id', async () => { + const results = await tauri(async (api, db) => { + const database = api.sql.get(db) + const first = await database.execute( + 'INSERT INTO e2e (name, score) VALUES ($1, $2)', + ['first', 1] + ) + const second = await database.execute( + 'INSERT INTO e2e (name, score) VALUES ($1, $2)', + ['second', 2] + ) + const update = await database.execute('UPDATE e2e SET score = score + 10') + return { first, second, update } + }, db) + expect(results.first).toEqual({ rowsAffected: 1, lastInsertId: 1 }) + expect(results.second).toEqual({ rowsAffected: 1, lastInsertId: 2 }) + expect(results.update.rowsAffected).toBe(2) + }) + + it('select returns rows as objects, with bound values and column types', async () => { + const rows = await tauri(async (api, db) => { + const database = api.sql.get(db) + await database.execute( + 'INSERT INTO e2e (name, score, ratio) VALUES ($1, $2, $3), ($4, $5, $6)', + ['alpha', 42, 0.5, 'beta', null, null] + ) + return { + all: await database.select('SELECT * FROM e2e ORDER BY id'), + filtered: await database.select( + 'SELECT name FROM e2e WHERE score = $1', + [42] + ), + none: await database.select( + 'SELECT * FROM e2e WHERE name = $1', + ['missing'] + ) + } + }, db) + expect(rows.all).toEqual([ + { id: 1, name: 'alpha', score: 42, ratio: 0.5 }, + { id: 2, name: 'beta', score: null, ratio: null } + ]) + expect(rows.filtered).toEqual([{ name: 'alpha' }]) + expect(rows.none).toEqual([]) + }) + + it('column order follows the query', async () => { + const columns = await tauri(async (api, db) => { + const database = api.sql.get(db) + await database.execute("INSERT INTO e2e (name, score) VALUES ('x', 1)") + const [row] = await database.select[]>( + 'SELECT score, name, id FROM e2e' + ) + return Object.keys(row) + }, db) + expect(columns).toEqual(['score', 'name', 'id']) + }) + + it('invalid statements reject', async () => { + const error = await tauriError( + (api, db) => api.sql.get(db).execute('INSERT INTO missing VALUES (1)'), + db + ) + expect(error).toMatch(/no such table: missing/) + const constraint = await tauriError( + (api, db) => + api.sql.get(db).execute('INSERT INTO e2e (name) VALUES ($1)', [null]), + db + ) + expect(constraint).toMatch(/NOT NULL constraint failed/) + }) + + it('a database that was never loaded is rejected', async () => { + const error = await tauriError((api) => + api.sql.get('sqlite:never-loaded.db').select('SELECT 1') + ) + expect(error).toMatch(/database sqlite:never-loaded\.db not loaded/) + }) + + it('close shuts the pool down until the database is loaded again', async () => { + const result = await tauri(async (api, db) => { + const database = api.sql.get(db) + await database.execute("INSERT INTO e2e (name) VALUES ('persisted')") + const closed = await database.close(db) + let error = '' + try { + await database.select('SELECT * FROM e2e') + } catch (e) { + error = String(e) + } + const reloaded = await api.sql.load(db) + return { + closed, + error, + rows: await reloaded.select<{ name: string }[]>('SELECT name FROM e2e') + } + }, db) + expect(result.closed).toBe(true) + expect(result.error).toMatch(/closed/i) + // the data was written to disk, not just to the closed pool + expect(result.rows).toEqual([{ name: 'persisted' }]) + }) + + it('closing a database that was never loaded is rejected', async () => { + const error = await tauriError((api) => + api.sql.get('sqlite:never-loaded.db').close('sqlite:never-loaded.db') + ) + expect(error).toMatch(/not loaded/) + }) +}) diff --git a/packages/api-e2e/test/specs/stronghold.spec.ts b/packages/api-e2e/test/specs/stronghold.spec.ts new file mode 100644 index 000000000..4a4960125 --- /dev/null +++ b/packages/api-e2e/test/specs/stronghold.spec.ts @@ -0,0 +1,248 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + tauriError, + describePlugin, + scratchDir +} from '../helpers/index.js' + +// The example derives the snapshot key from the password with argon2. Each +// test works on its own snapshot file under the spec's scratch directory, +// which the fs plugin cleans up; the snapshot path is resolved inside the page. + +const dir = scratchDir('stronghold') +const password = 'e2e-password' +const clientName = 'e2e-client' + +/** An arbitrary, valid BIP39 mnemonic, so derived keys are deterministic. */ +const mnemonic = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' + +describePlugin('stronghold', () => { + before(async () => { + await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + if (await api.fs.exists(dir, { baseDir })) { + await api.fs.remove(dir, { baseDir, recursive: true }) + } + await api.fs.mkdir(dir, { baseDir, recursive: true }) + }, dir) + }) + + after(async () => { + await tauri(async (api, dir) => { + const baseDir = api.fs.BaseDirectory.AppData + if (await api.fs.exists(dir, { baseDir })) { + await api.fs.remove(dir, { baseDir, recursive: true }) + } + }, dir) + }) + + it('store records round-trip and persist in the snapshot', async () => { + const result = await tauri( + async (api, dir, password, clientName) => { + const path = await api.path.join( + await api.path.appDataDir(), + dir, + 'store.stronghold' + ) + const value = Array.from(new TextEncoder().encode('top secret')) + + const stronghold = await api.stronghold.Stronghold.load(path, password) + const client = await stronghold.createClient(clientName) + await client.getStore().insert('key', value) + const sameSession = await client.getStore().get('key') + await stronghold.save() + await stronghold.unload() + + const reopened = await api.stronghold.Stronghold.load(path, password) + const store = (await reopened.loadClient(clientName)).getStore() + const persisted = await store.get('key') + const missing = await store.get('missing') + await reopened.unload() + + return { + sameSession: sameSession && Array.from(sameSession), + persisted: persisted && new TextDecoder().decode(persisted), + missing, + fileExists: await api.fs.exists(path) + } + }, + dir, + password, + clientName + ) + expect(result.sameSession).toEqual( + Array.from(new TextEncoder().encode('top secret')) + ) + expect(result.persisted).toBe('top secret') + expect(result.missing).toBeNull() + expect(result.fileExists).toBe(true) + }) + + it('store.remove returns the removed value', async () => { + const result = await tauri( + async (api, dir, password, clientName) => { + const path = await api.path.join( + await api.path.appDataDir(), + dir, + 'remove.stronghold' + ) + const stronghold = await api.stronghold.Stronghold.load(path, password) + const store = (await stronghold.createClient(clientName)).getStore() + await store.insert('key', [1, 2, 3]) + const removed = await store.remove('key') + const after = await store.get('key') + await stronghold.unload() + return { removed: removed && Array.from(removed), after } + }, + dir, + password, + clientName + ) + expect(result).toEqual({ removed: [1, 2, 3], after: null }) + }) + + it('a snapshot cannot be opened with the wrong password', async () => { + const path = await tauri( + async (api, dir, password, clientName) => { + const path = await api.path.join( + await api.path.appDataDir(), + dir, + 'password.stronghold' + ) + const stronghold = await api.stronghold.Stronghold.load(path, password) + await stronghold.createClient(clientName) + await stronghold.unload() + return path + }, + dir, + password, + clientName + ) + const error = await tauriError( + (api, path) => api.stronghold.Stronghold.load(path, 'wrong-password'), + path + ) + expect(error).toMatch(/failed to decode\/decrypt/) + }) + + it('loading a client that was never created is rejected', async () => { + const error = await tauriError( + async (api, dir, password) => { + const path = await api.path.join( + await api.path.appDataDir(), + dir, + 'no-client.stronghold' + ) + const stronghold = await api.stronghold.Stronghold.load(path, password) + try { + await stronghold.loadClient('never-created') + } finally { + await stronghold.unload() + } + }, + dir, + password + ) + expect(error).toMatch(/error loading client data/) + }) + + it('vault procedures derive keys and sign without exposing secrets', async () => { + const result = await tauri( + async (api, dir, password, clientName, mnemonic) => { + const { Location } = api.stronghold + const path = await api.path.join( + await api.path.appDataDir(), + dir, + 'vault.stronghold' + ) + const stronghold = await api.stronghold.Stronghold.load(path, password) + const vault = (await stronghold.createClient(clientName)).getVault( + 'vault' + ) + + // the same mnemonic recovered twice derives the same key + const seedA = Location.generic('vault', 'seed-a') + const seedB = Location.generic('vault', 'seed-b') + await vault.recoverBIP39(mnemonic, seedA) + await vault.recoverBIP39(mnemonic, seedB) + // Ed25519 SLIP-10 only derives hardened indices (the high bit set). + // (No named helper: the transpiler would wrap it in a `__name` call + // that does not exist in the page.) + const chain = [44, 4218, 0, 0, 0].map((i) => (i | 0x80000000) >>> 0) + const otherChain = [44, 4218, 0, 0, 1].map( + (i) => (i | 0x80000000) >>> 0 + ) + const keyA = Location.generic('vault', 'key-a') + const keyB = Location.generic('vault', 'key-b') + const keyOther = Location.generic('vault', 'key-other') + await vault.deriveSLIP10(chain, 'Seed', seedA, keyA) + await vault.deriveSLIP10(chain, 'Seed', seedB, keyB) + await vault.deriveSLIP10(otherChain, 'Seed', seedA, keyOther) + + const publicA = Array.from(await vault.getEd25519PublicKey(keyA)) + const publicB = Array.from(await vault.getEd25519PublicKey(keyB)) + const publicOther = Array.from( + await vault.getEd25519PublicKey(keyOther) + ) + const signature = Array.from(await vault.signEd25519(keyA, 'message')) + const signatureAgain = Array.from( + await vault.signEd25519(keyA, 'message') + ) + + // a random seed and a raw secret can be stored and removed + const random = Location.generic('vault', 'random') + await vault.generateSLIP10Seed(random) + await vault.insert('raw', [9, 9, 9]) + await vault.remove(Location.generic('vault', 'raw')) + + // a generated mnemonic recovers the seed generateBIP39 stored + const generatedSeed = Location.generic('vault', 'generated') + const mnemonicBytes = await vault.generateBIP39(generatedSeed) + const recoveredSeed = Location.generic('vault', 'recovered') + await vault.recoverBIP39( + new TextDecoder().decode(mnemonicBytes), + recoveredSeed + ) + const generatedKey = Location.generic('vault', 'generated-key') + const recoveredKey = Location.generic('vault', 'recovered-key') + await vault.deriveSLIP10(chain, 'Seed', generatedSeed, generatedKey) + await vault.deriveSLIP10(chain, 'Seed', recoveredSeed, recoveredKey) + const generatedPublic = Array.from( + await vault.getEd25519PublicKey(generatedKey) + ) + const recoveredPublic = Array.from( + await vault.getEd25519PublicKey(recoveredKey) + ) + + await stronghold.unload() + return { + publicA, + publicB, + publicOther, + signature, + signatureAgain, + generatedPublic, + recoveredPublic + } + }, + dir, + password, + clientName, + mnemonic + ) + expect(result.publicA).toHaveLength(32) + expect(result.publicB).toEqual(result.publicA) + expect(result.publicOther).not.toEqual(result.publicA) + // Ed25519 signatures are 64 bytes and deterministic + expect(result.signature).toHaveLength(64) + expect(result.signatureAgain).toEqual(result.signature) + expect(result.recoveredPublic).toEqual(result.generatedPublic) + expect(result.generatedPublic).not.toEqual(result.publicA) + }) +}) diff --git a/packages/api-e2e/test/specs/upload.spec.ts b/packages/api-e2e/test/specs/upload.spec.ts index 6c98b9cfb..eaf6c1d85 100644 --- a/packages/api-e2e/test/specs/upload.spec.ts +++ b/packages/api-e2e/test/specs/upload.spec.ts @@ -156,6 +156,6 @@ describePlugin('upload', () => { `${FIXTURE_SERVER_URL}/echo`, `${dir}/does-not-exist.txt` ) - expect(message.length).toBeGreaterThan(0) + expect(message).toMatch(/os error 2/) }) }) diff --git a/packages/api-e2e/test/specs/websocket.spec.ts b/packages/api-e2e/test/specs/websocket.spec.ts new file mode 100644 index 000000000..dece2e3fe --- /dev/null +++ b/packages/api-e2e/test/specs/websocket.spec.ts @@ -0,0 +1,185 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, tauriError, describePlugin } from '../helpers/index.js' +import { + WEBSOCKET_FIXTURE_URL, + WEBSOCKET_CLOSE_REQUEST, + WEBSOCKET_CLOSE_CODE, + WEBSOCKET_CLOSE_REASON +} from '../helpers/server.js' + +// Every test talks to the fixture server's `/ws` echo endpoint (through +// `adb reverse` on Android). Messages reach the page through the plugin's +// channel, so each page function collects them until it has what it expects, +// with its own timeout so a missing message fails with a readable error. + +interface Message { + type: string + data: unknown +} + +describePlugin('websocket', () => { + it('echoes text and binary messages', async () => { + const messages = await tauri(async (api, url) => { + const ws = await api.websocket.connect(url) + const received: Message[] = [] + await new Promise((resolve, reject) => { + setTimeout(() => reject(new Error('echo not received')), 5000) + ws.addListener((message) => { + received.push(message) + if (received.length === 3) resolve() + }) + ws.send('hello') + .then(() => ws.send([1, 2, 3])) + .then(() => ws.send({ type: 'Text', data: 'explicit' })) + .catch(reject) + }) + await ws.disconnect() + return received + }, WEBSOCKET_FIXTURE_URL) + expect(messages).toEqual([ + { type: 'Text', data: 'hello' }, + { type: 'Binary', data: [1, 2, 3] }, + { type: 'Text', data: 'explicit' } + ]) + }) + + it('a ping is answered with a pong carrying its payload', async () => { + const message = await tauri(async (api, url) => { + const ws = await api.websocket.connect(url) + const pong = await new Promise((resolve, reject) => { + setTimeout(() => reject(new Error('pong not received')), 5000) + ws.addListener((message) => { + if (message.type === 'Pong') resolve(message) + }) + ws.send({ type: 'Ping', data: [7, 8, 9] }).catch(reject) + }) + await ws.disconnect() + return pong + }, WEBSOCKET_FIXTURE_URL) + expect(message).toEqual({ type: 'Pong', data: [7, 8, 9] }) + }) + + it('sends the configured headers with the upgrade request', async () => { + const headers = await tauri(async (api, url) => { + const ws = await api.websocket.connect(url, { + headers: { 'x-e2e-header': 'from the plugin' } + }) + const message = await new Promise((resolve, reject) => { + setTimeout(() => reject(new Error('headers not received')), 5000) + ws.addListener(resolve) + }) + await ws.disconnect() + return JSON.parse(message.data as string) as Record + }, `${WEBSOCKET_FIXTURE_URL}/headers`) + expect(headers['x-e2e-header']).toBe('from the plugin') + }) + + it('a listener stops receiving once removed', async () => { + const counts = await tauri(async (api, url) => { + const ws = await api.websocket.connect(url) + let removed = 0 + let kept = 0 + const remove = ws.addListener(() => removed++) + await new Promise((resolve, reject) => { + setTimeout(() => reject(new Error('echoes not received')), 5000) + ws.addListener(() => { + kept++ + if (kept === 1) { + remove() + ws.send('second').catch(reject) + } else { + resolve() + } + }) + ws.send('first').catch(reject) + }) + await ws.disconnect() + return { removed, kept } + }, WEBSOCKET_FIXTURE_URL) + expect(counts).toEqual({ removed: 1, kept: 2 }) + }) + + it('a close from the server is delivered and ends the connection', async () => { + const result = await tauri( + async (api, url, closeRequest) => { + const ws = await api.websocket.connect(url) + const close = await new Promise((resolve, reject) => { + setTimeout(() => reject(new Error('close not received')), 5000) + ws.addListener((message) => { + if (message.type === 'Close') resolve(message) + }) + ws.send(closeRequest).catch(reject) + }) + let sendError = '' + try { + await ws.send('after close') + } catch (error) { + sendError = String(error) + } + return { close, sendError } + }, + WEBSOCKET_FIXTURE_URL, + WEBSOCKET_CLOSE_REQUEST + ) + expect(result.close).toEqual({ + type: 'Close', + data: { code: WEBSOCKET_CLOSE_CODE, reason: WEBSOCKET_CLOSE_REASON } + }) + expect(result.sendError).toMatch(/connection not found/) + }) + + it('disconnect sends a normal close frame', async () => { + const close = await tauri(async (api, url) => { + const ws = await api.websocket.connect(url) + // the server acknowledges the close with the same frame + return await new Promise((resolve, reject) => { + setTimeout(() => reject(new Error('close not received')), 5000) + ws.addListener((message) => { + if (message.type === 'Close') resolve(message) + }) + ws.disconnect().catch(reject) + }) + }, WEBSOCKET_FIXTURE_URL) + expect(close).toEqual({ + type: 'Close', + data: { code: 1000, reason: 'Disconnected by client' } + }) + }) + + it('connecting to a closed port is rejected', async () => { + // port 1 (tcpmux) is never listening on the loopback interface + const error = await tauriError((api) => + api.websocket.connect('ws://127.0.0.1:1/') + ) + expect(error).toMatch(/refused|connect/i) + }) + + it('invalid URLs and header names are rejected', async () => { + const url = await tauriError((api) => api.websocket.connect('not a url')) + expect(url).toMatch(/invalid uri/i) + const header = await tauriError( + (api, url) => + api.websocket.connect(url, { headers: [['bad header', 'value']] }), + WEBSOCKET_FIXTURE_URL + ) + // Chromium (Android) already rejects the name in the `Headers` constructor; + // WebKit passes it through and the plugin rejects it + expect(header).toMatch(/invalid (header )?name/i) + }) + + it('sending an unsupported message type throws', async () => { + const error = await tauriError(async (api, url) => { + const ws = await api.websocket.connect(url) + try { + await ws.send(42 as unknown as string) + } finally { + await ws.disconnect() + } + }, WEBSOCKET_FIXTURE_URL) + expect(error).toMatch(/invalid `message` type/) + }) +}) diff --git a/packages/api-e2e/wdio.mobile.ts b/packages/api-e2e/wdio.mobile.ts index cadb15883..815802ac8 100644 --- a/packages/api-e2e/wdio.mobile.ts +++ b/packages/api-e2e/wdio.mobile.ts @@ -164,7 +164,8 @@ export function mobileConfig(platform: MobilePlatform): WebdriverIO.Config { '--apk', '--target', androidTarget() - ] + ], + ios ? {} : androidBuildEnv() ) } @@ -282,11 +283,12 @@ for (const signal of [ } /** Runs `pnpm tauri ` in examples/api, failing loudly. */ -function tauriCli(args: string[]): void { +function tauriCli(args: string[], env: NodeJS.ProcessEnv = {}): void { const result = spawnSync('pnpm', ['tauri', ...args], { cwd: appDir, stdio: 'inherit', - shell: true + shell: true, + env: { ...process.env, ...env } }) if (result.status !== 0) { throw new Error( @@ -297,6 +299,39 @@ function tauriCli(args: string[]): void { // --- Android ----------------------------------------------------------------- +/** + * Environment for the Android build. + * + * - No debug info: the suite needs a debug build (webview debugging follows + * `debug_assertions`), but with the stronghold, sql and websocket plugins the + * debug info alone grows the APK past what a default emulator can install + * ("not enough space"). + * - On a macOS host, `AR`/`RANLIB` point at the NDK's LLVM tools. Autotools-built + * C dependencies (libsodium, through the stronghold plugin) otherwise fall back + * to Apple's `ar`/`ranlib`, which silently produce an empty archive from the + * Android (ELF) objects, and the app then fails to load its library with an + * unresolved symbol. Linux hosts' GNU `ar` is fine. + */ +function androidBuildEnv(): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { CARGO_PROFILE_DEV_DEBUG: '0' } + const ndk = process.env.NDK_HOME ?? process.env.ANDROID_NDK_HOME + if (process.platform !== 'darwin' || !ndk) return env + // the NDK only ships an x86_64 (Rosetta-compatible) macOS toolchain + const bin = path.join( + ndk, + 'toolchains', + 'llvm', + 'prebuilt', + 'darwin-x86_64', + 'bin' + ) + return { + ...env, + AR: path.join(bin, 'llvm-ar'), + RANLIB: path.join(bin, 'llvm-ranlib') + } +} + /** `adb` from the Android SDK, else whatever is on `PATH`. */ function adb(args: string[]): SpawnSyncReturns { const sdk = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT @@ -329,6 +364,10 @@ function androidCapabilities(app: string): WebdriverIO.Capabilities { 'appium:app': app, 'appium:appPackage': appId, 'appium:appActivity': '.MainActivity', + // Every build has the same version code, and Appium otherwise skips + // installing an APK whose version is already on the device, so a rebuilt + // app would never replace the installed one. + 'appium:enforceAppInstall': true, // Which device/emulator to use; Appium picks the first connected one // otherwise. E2E_ANDROID_AVD instead boots that AVD. ...(process.env.E2E_ANDROID_DEVICE diff --git a/plugins/notification/android/src/main/java/NotificationPlugin.kt b/plugins/notification/android/src/main/java/NotificationPlugin.kt index 3ead31527..19a82833d 100644 --- a/plugins/notification/android/src/main/java/NotificationPlugin.kt +++ b/plugins/notification/android/src/main/java/NotificationPlugin.kt @@ -39,7 +39,8 @@ class BatchArgs { @InvokeArg class CancelArgs { - lateinit var notifications: List + // `cancelAll()` sends no list: cancel every pending notification + var notifications: List? = null } @InvokeArg @@ -152,7 +153,9 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) { @Command fun cancel(invoke: Invoke) { val args = invoke.parseArgs(CancelArgs::class.java) - manager.cancel(args.notifications) + manager.cancel( + args.notifications ?: notificationStorage.getSavedNotificationIds().mapNotNull { it.toIntOrNull() } + ) invoke.resolve() } @@ -192,32 +195,33 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) { @SuppressLint("ObsoleteSdkInt") @Command fun getActive(invoke: Invoke) { - val notifications = JSArray() + // plain lists and maps: until tauri serializes org.json values natively, + // `resolveObject` turns a `JSArray` into `{ "values": [...] }` + val notifications = mutableListOf>() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val activeNotifications = notificationManager.activeNotifications for (activeNotification in activeNotifications) { - val jsNotification = JSObject() - jsNotification.put("id", activeNotification.id) - jsNotification.put("tag", activeNotification.tag) + val jsNotification = mutableMapOf( + "id" to activeNotification.id, + "tag" to activeNotification.tag + ) val notification = activeNotification.notification if (notification != null) { - jsNotification.put("title", notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE)) - jsNotification.put("body", notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT)) - jsNotification.put("group", notification.group) - jsNotification.put( - "groupSummary", + jsNotification["title"] = notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE)?.toString() + jsNotification["body"] = notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT)?.toString() + jsNotification["group"] = notification.group + jsNotification["groupSummary"] = 0 != notification.flags and android.app.Notification.FLAG_GROUP_SUMMARY - ) - val extras = JSObject() + val extras = mutableMapOf() for (key in notification.extras.keySet()) { - extras.put(key!!, notification.extras.getString(key)) + extras[key] = notification.extras.getString(key) } - jsNotification.put("data", extras) + jsNotification["data"] = extras } - notifications.put(jsNotification) + notifications.add(jsNotification) } } - + invoke.resolveObject(notifications) } diff --git a/plugins/notification/api-iife.js b/plugins/notification/api-iife.js index 8eb7ac0e7..85f75b279 100644 --- a/plugins/notification/api-iife.js +++ b/plugins/notification/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(i,n){"use strict";var e,t,o;i.ScheduleEvery=void 0,(e=i.ScheduleEvery||(i.ScheduleEvery={})).Year="year",e.Month="month",e.TwoWeeks="twoWeeks",e.Week="week",e.Day="day",e.Hour="hour",e.Minute="minute",e.Second="second";return i.Importance=void 0,(t=i.Importance||(i.Importance={}))[t.None=0]="None",t[t.Min=1]="Min",t[t.Low=2]="Low",t[t.Default=3]="Default",t[t.High=4]="High",i.Visibility=void 0,(o=i.Visibility||(i.Visibility={}))[o.Secret=-1]="Secret",o[o.Private=0]="Private",o[o.Public=1]="Public",i.Schedule=class{static at(i,n=!1,e=!1){return{at:{date:i,repeating:n,allowWhileIdle:e},interval:void 0,every:void 0}}static interval(i,n=!1){return{at:void 0,interval:{interval:i,allowWhileIdle:n},every:void 0}}static every(i,n,e=!1){return{at:void 0,interval:void 0,every:{interval:i,count:n,allowWhileIdle:e}}}},i.active=async function(){return await n.invoke("plugin:notification|get_active")},i.cancel=async function(i){await n.invoke("plugin:notification|cancel",{notifications:i})},i.cancelAll=async function(){await n.invoke("plugin:notification|cancel")},i.channels=async function(){return await n.invoke("plugin:notification|listChannels")},i.createChannel=async function(i){await n.invoke("plugin:notification|create_channel",{...i})},i.isPermissionGranted=async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):await n.invoke("plugin:notification|is_permission_granted")},i.onAction=async function(i){return await n.addPluginListener("notification","actionPerformed",i)},i.onNotificationReceived=async function(i){return await n.addPluginListener("notification","notification",i)},i.pending=async function(){return await n.invoke("plugin:notification|get_pending")},i.registerActionTypes=async function(i){await n.invoke("plugin:notification|register_action_types",{types:i})},i.removeActive=async function(i){await n.invoke("plugin:notification|remove_active",{notifications:i})},i.removeAllActive=async function(){await n.invoke("plugin:notification|remove_active")},i.removeChannel=async function(i){await n.invoke("plugin:notification|delete_channel",{id:i})},i.requestPermission=async function(){return await window.Notification.requestPermission()},i.sendNotification=function(i){"string"==typeof i?new window.Notification(i):new window.Notification(i.title,i)},i}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(i,n){"use strict";var e,t,o;i.ScheduleEvery=void 0,(e=i.ScheduleEvery||(i.ScheduleEvery={})).Year="year",e.Month="month",e.TwoWeeks="twoWeeks",e.Week="week",e.Day="day",e.Hour="hour",e.Minute="minute",e.Second="second";return i.Importance=void 0,(t=i.Importance||(i.Importance={}))[t.None=0]="None",t[t.Min=1]="Min",t[t.Low=2]="Low",t[t.Default=3]="Default",t[t.High=4]="High",i.Visibility=void 0,(o=i.Visibility||(i.Visibility={}))[o.Secret=-1]="Secret",o[o.Private=0]="Private",o[o.Public=1]="Public",i.Schedule=class{static at(i,n=!1,e=!1){return{at:{date:i,repeating:n,allowWhileIdle:e},interval:void 0,every:void 0}}static interval(i,n=!1){return{at:void 0,interval:{interval:i,allowWhileIdle:n},every:void 0}}static every(i,n,e=!1){return{at:void 0,interval:void 0,every:{interval:i,count:n,allowWhileIdle:e}}}},i.active=async function(){return await n.invoke("plugin:notification|get_active")},i.cancel=async function(i){await n.invoke("plugin:notification|cancel",{notifications:i})},i.cancelAll=async function(){await n.invoke("plugin:notification|cancel")},i.channels=async function(){return await n.invoke("plugin:notification|list_channels")},i.createChannel=async function(i){await n.invoke("plugin:notification|create_channel",{...i})},i.isPermissionGranted=async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):await n.invoke("plugin:notification|is_permission_granted")},i.onAction=async function(i){return await n.addPluginListener("notification","actionPerformed",i)},i.onNotificationReceived=async function(i){return await n.addPluginListener("notification","notification",i)},i.pending=async function(){return await n.invoke("plugin:notification|get_pending")},i.registerActionTypes=async function(i){await n.invoke("plugin:notification|register_action_types",{types:i})},i.removeActive=async function(i){await n.invoke("plugin:notification|remove_active",{notifications:i})},i.removeAllActive=async function(){await n.invoke("plugin:notification|remove_active")},i.removeChannel=async function(i){await n.invoke("plugin:notification|delete_channel",{id:i})},i.requestPermission=async function(){return await window.Notification.requestPermission()},i.sendNotification=function(i){"string"==typeof i?new window.Notification(i):new window.Notification(i.title,i)},i}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})} diff --git a/plugins/notification/build.rs b/plugins/notification/build.rs index 4b24c755e..d7c0b3ed5 100644 --- a/plugins/notification/build.rs +++ b/plugins/notification/build.rs @@ -8,6 +8,7 @@ const COMMANDS: &[&str] = &[ "is_permission_granted", "register_action_types", "register_listener", + "remove_listener", "cancel", "get_pending", "remove_active", diff --git a/plugins/notification/guest-js/index.ts b/plugins/notification/guest-js/index.ts index ee9c24c91..534635f14 100644 --- a/plugins/notification/guest-js/index.ts +++ b/plugins/notification/guest-js/index.ts @@ -879,7 +879,7 @@ async function removeChannel(id: string): Promise { * @since 2.0.0 */ async function channels(): Promise { - return await invoke('plugin:notification|listChannels') + return await invoke('plugin:notification|list_channels') } /** diff --git a/plugins/notification/ios/Sources/NotificationPlugin.swift b/plugins/notification/ios/Sources/NotificationPlugin.swift index 6d8391bcc..8adbff9f5 100644 --- a/plugins/notification/ios/Sources/NotificationPlugin.swift +++ b/plugins/notification/ios/Sources/NotificationPlugin.swift @@ -120,7 +120,8 @@ func showNotification(invoke: Invoke, notification: Notification) } struct CancelArgs: Decodable { - let notifications: [Int] + // `cancelAll()` sends no list: cancel every pending notification + let notifications: [Int]? } struct Action: Decodable { @@ -216,9 +217,13 @@ class NotificationPlugin: Plugin { @objc func cancel(_ invoke: Invoke) throws { let args = try invoke.parseArgs(CancelArgs.self) - UNUserNotificationCenter.current().removePendingNotificationRequests( - withIdentifiers: args.notifications.map { String($0) } - ) + if let notifications = args.notifications { + UNUserNotificationCenter.current().removePendingNotificationRequests( + withIdentifiers: notifications.map { String($0) } + ) + } else { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + } invoke.resolve() } diff --git a/plugins/notification/permissions/autogenerated/commands/remove_listener.toml b/plugins/notification/permissions/autogenerated/commands/remove_listener.toml new file mode 100644 index 000000000..9f315e51e --- /dev/null +++ b/plugins/notification/permissions/autogenerated/commands/remove_listener.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-remove-listener" +description = "Enables the remove_listener command without any pre-configured scope." +commands.allow = ["remove_listener"] + +[[permission]] +identifier = "deny-remove-listener" +description = "Denies the remove_listener command without any pre-configured scope." +commands.deny = ["remove_listener"] diff --git a/plugins/notification/permissions/autogenerated/reference.md b/plugins/notification/permissions/autogenerated/reference.md index b0652545a..c79a441d8 100644 --- a/plugins/notification/permissions/autogenerated/reference.md +++ b/plugins/notification/permissions/autogenerated/reference.md @@ -14,6 +14,7 @@ It allows all notification related features. - `allow-notify` - `allow-register-action-types` - `allow-register-listener` +- `allow-remove-listener` - `allow-cancel` - `allow-get-pending` - `allow-remove-active` @@ -402,6 +403,32 @@ Denies the remove_active command without any pre-configured scope. +`notification:allow-remove-listener` + + + + +Enables the remove_listener command without any pre-configured scope. + + + + + + + +`notification:deny-remove-listener` + + + + +Denies the remove_listener command without any pre-configured scope. + + + + + + + `notification:allow-request-permission` diff --git a/plugins/notification/permissions/default.toml b/plugins/notification/permissions/default.toml index 00b4e1d0c..76d9e2757 100644 --- a/plugins/notification/permissions/default.toml +++ b/plugins/notification/permissions/default.toml @@ -16,6 +16,7 @@ permissions = [ "allow-notify", "allow-register-action-types", "allow-register-listener", + "allow-remove-listener", "allow-cancel", "allow-get-pending", "allow-remove-active", diff --git a/plugins/notification/permissions/schemas/schema.json b/plugins/notification/permissions/schemas/schema.json index 26703a8a1..bea41b7e3 100644 --- a/plugins/notification/permissions/schemas/schema.json +++ b/plugins/notification/permissions/schemas/schema.json @@ -462,6 +462,18 @@ "const": "deny-remove-active", "markdownDescription": "Denies the remove_active command without any pre-configured scope." }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, { "description": "Enables the request_permission command without any pre-configured scope.", "type": "string", @@ -487,10 +499,10 @@ "markdownDescription": "Denies the show command without any pre-configured scope." }, { - "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", + "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", "type": "string", "const": "default", - "markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`" + "markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`" } ] } diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index b5da19060..9578a2823 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -287,6 +287,8 @@ mod imp { use std::path::PathBuf; pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { + // these AppKit calls are only `unsafe` before objc2-app-kit 0.3.2 + #[allow(unused_unsafe)] unsafe { let mut urls = Vec::new(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef4315a0d..f445e0f2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,6 +156,9 @@ importers: '@tauri-apps/api': specifier: ^2.11.0 version: 2.11.0 + '@tauri-apps/plugin-autostart': + specifier: workspace:* + version: link:../../plugins/autostart '@tauri-apps/plugin-barcode-scanner': specifier: workspace:* version: link:../../plugins/barcode-scanner @@ -168,6 +171,9 @@ importers: '@tauri-apps/plugin-clipboard-manager': specifier: workspace:* version: link:../../plugins/clipboard-manager + '@tauri-apps/plugin-deep-link': + specifier: workspace:* + version: link:../../plugins/deep-link '@tauri-apps/plugin-dialog': specifier: workspace:* version: link:../../plugins/dialog @@ -198,21 +204,33 @@ importers: '@tauri-apps/plugin-os': specifier: workspace:* version: link:../../plugins/os + '@tauri-apps/plugin-positioner': + specifier: workspace:* + version: link:../../plugins/positioner '@tauri-apps/plugin-process': specifier: workspace:* version: link:../../plugins/process '@tauri-apps/plugin-shell': specifier: workspace:* version: link:../../plugins/shell + '@tauri-apps/plugin-sql': + specifier: workspace:* + version: link:../../plugins/sql '@tauri-apps/plugin-store': specifier: workspace:* version: link:../../plugins/store + '@tauri-apps/plugin-stronghold': + specifier: workspace:* + version: link:../../plugins/stronghold '@tauri-apps/plugin-updater': specifier: workspace:* version: link:../../plugins/updater '@tauri-apps/plugin-upload': specifier: workspace:* version: link:../../plugins/upload + '@tauri-apps/plugin-websocket': + specifier: workspace:* + version: link:../../plugins/websocket '@zerodevx/svelte-json-view': specifier: 2.0.0 version: 2.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0)) @@ -248,11 +266,14 @@ importers: specifier: ^2.0.9 version: 2.0.9 '@crabnebula/test-runner-backend': - specifier: ^0.2.9 - version: 0.2.9 + specifier: ^0.3.0 + version: 0.3.0 '@tauri-apps/api': specifier: ^2.11.0 version: 2.11.0 + '@tauri-apps/plugin-autostart': + specifier: workspace:* + version: link:../../plugins/autostart '@tauri-apps/plugin-barcode-scanner': specifier: workspace:* version: link:../../plugins/barcode-scanner @@ -265,6 +286,9 @@ importers: '@tauri-apps/plugin-clipboard-manager': specifier: workspace:* version: link:../../plugins/clipboard-manager + '@tauri-apps/plugin-deep-link': + specifier: workspace:* + version: link:../../plugins/deep-link '@tauri-apps/plugin-dialog': specifier: workspace:* version: link:../../plugins/dialog @@ -298,27 +322,42 @@ importers: '@tauri-apps/plugin-os': specifier: workspace:* version: link:../../plugins/os + '@tauri-apps/plugin-positioner': + specifier: workspace:* + version: link:../../plugins/positioner '@tauri-apps/plugin-process': specifier: workspace:* version: link:../../plugins/process '@tauri-apps/plugin-shell': specifier: workspace:* version: link:../../plugins/shell + '@tauri-apps/plugin-sql': + specifier: workspace:* + version: link:../../plugins/sql '@tauri-apps/plugin-store': specifier: workspace:* version: link:../../plugins/store + '@tauri-apps/plugin-stronghold': + specifier: workspace:* + version: link:../../plugins/stronghold '@tauri-apps/plugin-updater': specifier: workspace:* version: link:../../plugins/updater '@tauri-apps/plugin-upload': specifier: workspace:* version: link:../../plugins/upload + '@tauri-apps/plugin-websocket': + specifier: workspace:* + version: link:../../plugins/websocket '@tauri-apps/plugin-window-state': specifier: workspace:* version: link:../../plugins/window-state '@types/node': specifier: ^22.10.0 version: 22.20.3 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 '@wdio/appium-service': specifier: ^9.31.9 version: 9.32.0(supports-color@8.1.1) @@ -355,6 +394,9 @@ importers: webdriverio: specifier: ^9.31.9 version: 9.32.0(supports-color@8.1.1) + ws: + specifier: ^8.18.0 + version: 8.21.3 plugins/autostart: dependencies: @@ -657,33 +699,33 @@ packages: engines: {node: '>= 10'} hasBin: true - '@crabnebula/test-runner-backend-darwin-arm64@0.2.9': - resolution: {integrity: sha512-UAlL8VA23y/fp+MigNYFwGvtE9vJ518f846c0k2vgT/bLIpt+d3UXndjZCMYbEVFTmaq00wHoRPSPbyzHEMWlQ==} + '@crabnebula/test-runner-backend-darwin-arm64@0.3.0': + resolution: {integrity: sha512-XnZdSeew2ew/Eix+z1BwKt5PcrJSjL+VQ7dlrV1fvRj4WUTQtluzE1VhiVV0j2EIFJsRH2IExNj5OdCWxKSPiQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@crabnebula/test-runner-backend-darwin-x64@0.2.9': - resolution: {integrity: sha512-RUP9L4Lk8KsT4A64U4QFVy5rj99Y6iIichPLpZgTvpRybYKZBVXjUiU31aGUIlWnd4VHsWKLM2vNl90rKZm3lA==} + '@crabnebula/test-runner-backend-darwin-x64@0.3.0': + resolution: {integrity: sha512-dpogk8TxKd2CZb8BZ2Ni1mpbogD8KSoB8J92aw+9R24S8xBeTu8I+t4w9PVNIBvFaNAhEQelYhFffxH0FDs5+Q==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@crabnebula/test-runner-backend-linux-x64-gnu@0.2.9': - resolution: {integrity: sha512-js/haLE9OUsa0oDHNuVps5DY7av3iWG7PpAKjr9nMnIEXiLACDmvyUL/IplHMmu1lr6S0fztoudRf4JfTR+mfA==} + '@crabnebula/test-runner-backend-linux-x64-gnu@0.3.0': + resolution: {integrity: sha512-ZzDVKdY1ydv+lqfRYs6PAdmIc8AlfOciH3ntuUxyRJ8aSq0v8y+pow6b3/CRqmumkj9riVWsUriJr7Xw2YZFTg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@crabnebula/test-runner-backend-win32-x64-msvc@0.2.9': - resolution: {integrity: sha512-p4pc/GQ0bj3vVLuUyTHDnhaXO52EQNkaCUPhWXwEkG00loW93pvCBRrOWoYpJH9me/n2zMFv61meW9mQyvbByw==} + '@crabnebula/test-runner-backend-win32-x64-msvc@0.3.0': + resolution: {integrity: sha512-xzE5bZx6QpqCkGFTpOYGxMfYidfsvYQgp3yQ1hXwuxeC5vsabOngRtE6X2DxQU6U8JzzNaooN0afG/khzoaihQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@crabnebula/test-runner-backend@0.2.9': - resolution: {integrity: sha512-3z6qZbJtVyA1JymQU5hFqcxG6nFD5INgqBc3OxTbYXVzMpihlRGOcHWyPK72ndHK88YmfwQWPcEFN1R/H/Lvhw==} + '@crabnebula/test-runner-backend@0.3.0': + resolution: {integrity: sha512-72aldFCPV0Sflv8KGLeaNoUY4Vz8eRl7AvUsJXFwqJ7KKNDAfPKwovXjmZoqIWn6Icv01sEsd1b2w0O66Y3WEQ==} engines: {node: '>= 10'} hasBin: true @@ -4911,24 +4953,24 @@ snapshots: '@crabnebula/tauri-driver-linux-x64-gnu': 2.0.9 '@crabnebula/tauri-driver-win32-x64-msvc': 2.0.9 - '@crabnebula/test-runner-backend-darwin-arm64@0.2.9': + '@crabnebula/test-runner-backend-darwin-arm64@0.3.0': optional: true - '@crabnebula/test-runner-backend-darwin-x64@0.2.9': + '@crabnebula/test-runner-backend-darwin-x64@0.3.0': optional: true - '@crabnebula/test-runner-backend-linux-x64-gnu@0.2.9': + '@crabnebula/test-runner-backend-linux-x64-gnu@0.3.0': optional: true - '@crabnebula/test-runner-backend-win32-x64-msvc@0.2.9': + '@crabnebula/test-runner-backend-win32-x64-msvc@0.3.0': optional: true - '@crabnebula/test-runner-backend@0.2.9': + '@crabnebula/test-runner-backend@0.3.0': optionalDependencies: - '@crabnebula/test-runner-backend-darwin-arm64': 0.2.9 - '@crabnebula/test-runner-backend-darwin-x64': 0.2.9 - '@crabnebula/test-runner-backend-linux-x64-gnu': 0.2.9 - '@crabnebula/test-runner-backend-win32-x64-msvc': 0.2.9 + '@crabnebula/test-runner-backend-darwin-arm64': 0.3.0 + '@crabnebula/test-runner-backend-darwin-x64': 0.3.0 + '@crabnebula/test-runner-backend-linux-x64-gnu': 0.3.0 + '@crabnebula/test-runner-backend-win32-x64-msvc': 0.3.0 '@dabh/diagnostics@2.0.9': dependencies: From 5c7881422060055680b30544e91c0176cfc3ed94 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 22 Sep 2026 21:56:01 -0300 Subject: [PATCH 09/29] fix(autostart): handle disabling when the autostart is already disabled --- .changes/fix-autostart-windows-disable.md | 5 ++++ packages/api-e2e/test/specs/autostart.spec.ts | 29 ++++++++++++------- plugins/autostart/src/lib.rs | 11 ++++--- 3 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 .changes/fix-autostart-windows-disable.md diff --git a/.changes/fix-autostart-windows-disable.md b/.changes/fix-autostart-windows-disable.md new file mode 100644 index 000000000..1bae66871 --- /dev/null +++ b/.changes/fix-autostart-windows-disable.md @@ -0,0 +1,5 @@ +--- +autostart: patch +--- + +Fixed `disable()` failing on Windows when autostart is already disabled. It now succeeds, as it does on macOS and Linux. diff --git a/packages/api-e2e/test/specs/autostart.spec.ts b/packages/api-e2e/test/specs/autostart.spec.ts index 20a09a00c..b45a0af68 100644 --- a/packages/api-e2e/test/specs/autostart.spec.ts +++ b/packages/api-e2e/test/specs/autostart.spec.ts @@ -8,9 +8,6 @@ import { tauri, describePlugin } from '../helpers/index.js' // Enabling autostart registers the app with the host for real (a Launch Agent // on macOS, an XDG autostart entry on Linux, a `Run` registry value on // Windows), so the spec puts back whatever state it found. -// -// `disable` is only called while autostart is enabled: on Windows it fails -// when there is no `Run` value to delete. describePlugin('autostart', { desktopOnly: true }, () => { let initiallyEnabled = false @@ -20,10 +17,11 @@ describePlugin('autostart', { desktopOnly: true }, () => { }) after(async () => { - await tauri(async (api, enabled) => { - if ((await api.autostart.isEnabled()) === enabled) return - await (enabled ? api.autostart.enable() : api.autostart.disable()) - }, initiallyEnabled) + await tauri( + (api, enabled) => + enabled ? api.autostart.enable() : api.autostart.disable(), + initiallyEnabled + ) }) it('enable and disable toggle isEnabled', async () => { @@ -37,13 +35,24 @@ describePlugin('autostart', { desktopOnly: true }, () => { }) it('enabling twice keeps it enabled', async () => { - const states = await tauri(async (api) => { + const enabled = await tauri(async (api) => { await api.autostart.enable() await api.autostart.enable() const enabled = await api.autostart.isEnabled() await api.autostart.disable() - return { enabled, disabled: await api.autostart.isEnabled() } + return enabled }) - expect(states).toEqual({ enabled: true, disabled: false }) + expect(enabled).toBe(true) + }) + + it('disable resolves when autostart is already disabled', async () => { + // Windows used to fail here, having no `Run` registry value to delete + const disabled = await tauri(async (api) => { + await api.autostart.enable() + await api.autostart.disable() + await api.autostart.disable() + return !(await api.autostart.isEnabled()) + }) + expect(disabled).toBe(true) }) }) diff --git a/plugins/autostart/src/lib.rs b/plugins/autostart/src/lib.rs index 4ff9503d1..af9ef7564 100644 --- a/plugins/autostart/src/lib.rs +++ b/plugins/autostart/src/lib.rs @@ -83,10 +83,13 @@ impl AutoLaunchManager { /// /// Returns [`Error::Anyhow`] if the platform-specific removal fails. pub fn disable(&self) -> Result<()> { - self.0 - .disable() - .map_err(|e| e.to_string()) - .map_err(Error::Anyhow) + match self.0.disable() { + // On Windows, disabling deletes the app's `Run` registry value, which fails with + // "not found" when autostart is already disabled. macOS and Linux treat that as a + // no-op, so do the same here. + Err(auto_launch::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + result => result.map_err(|e| e.to_string()).map_err(Error::Anyhow), + } } /// Returns whether auto start is currently enabled for the application. From 710d097b6bccb8b4aa54991fafe845f5f75eacf0 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 22 Sep 2026 21:56:25 -0300 Subject: [PATCH 10/29] chore: fmt --- plugins/updater/tests/app-updater/tests/update.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index 4fcf3de85..20099d398 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -396,8 +396,8 @@ fn patch_bundle_type(from: &Path, to: &Path, bundle_target: BundleTarget) { _ => unreachable!("{} is not a Windows bundle", bundle_target.name()), }; - let mut binary = std::fs::read(from) - .unwrap_or_else(|e| panic!("failed to read {}: {e}", from.display())); + let mut binary = + std::fs::read(from).unwrap_or_else(|e| panic!("failed to read {}: {e}", from.display())); let index = binary .windows(PLACEHOLDER.len()) .position(|window| window == PLACEHOLDER) From 4a9435638663d08a67282dbc540d603d180d2841 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Tue, 22 Sep 2026 22:03:02 -0300 Subject: [PATCH 11/29] fix(deep-link): unregister must refresh the database --- .changes/fix-deep-link-linux-unregister.md | 5 ++ packages/api-e2e/test/specs/deep-link.spec.ts | 37 +++++------- plugins/deep-link/src/lib.rs | 58 +++++++++++++++---- 3 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 .changes/fix-deep-link-linux-unregister.md diff --git a/.changes/fix-deep-link-linux-unregister.md b/.changes/fix-deep-link-linux-unregister.md new file mode 100644 index 000000000..ab542f7ae --- /dev/null +++ b/.changes/fix-deep-link-linux-unregister.md @@ -0,0 +1,5 @@ +--- +deep-link: patch +--- + +Fixed `unregister` on Linux leaving the app as the scheme's handler: it now also removes the scheme from the `MimeType` of the handler's `.desktop` file and refreshes the desktop database, which `xdg-mime` falls back to. It also no longer fails when `mimeapps.list` does not exist. diff --git a/packages/api-e2e/test/specs/deep-link.spec.ts b/packages/api-e2e/test/specs/deep-link.spec.ts index a818e8f17..1deb711b5 100644 --- a/packages/api-e2e/test/specs/deep-link.spec.ts +++ b/packages/api-e2e/test/specs/deep-link.spec.ts @@ -3,13 +3,7 @@ // SPDX-License-Identifier: MIT import { expect } from '@wdio/globals' -import { - tauri, - tauriError, - describePlugin, - itOn, - platform -} from '../helpers/index.js' +import { tauri, tauriError, describePlugin, itOn } from '../helpers/index.js' // The suite launches the app without a URL, so there is no current deep link, // and it cannot open one through the OS either. `onOpenUrl` is exercised by @@ -48,7 +42,7 @@ describePlugin('deep-link', () => { itOn( ['linux', 'win32'], - 'register makes the app the scheme handler', + 'register and unregister toggle the scheme handler', async () => { const result = await tauri(async (api, scheme) => { await api.deepLink.register(scheme) @@ -59,23 +53,22 @@ describePlugin('deep-link', () => { afterUnregister: await api.deepLink.isRegistered(scheme) } }, scheme) - expect(result.registered).toBe(true) - // On Linux `xdg-mime` falls back to the desktop database, which still lists - // the handler after its `mimeapps.list` default is removed. - if (platform === 'win32') { - expect(result.afterUnregister).toBe(false) - } + expect(result).toEqual({ registered: true, afterUnregister: false }) } ) - itOn('win32', 'isRegistered is false for an unknown scheme', async () => { - expect( - await tauri( - (api, scheme) => api.deepLink.isRegistered(scheme), - `${scheme}-unknown` - ) - ).toBe(false) - }) + itOn( + ['linux', 'win32'], + 'isRegistered is false for an unknown scheme', + async () => { + expect( + await tauri( + (api, scheme) => api.deepLink.isRegistered(scheme), + `${scheme}-unknown` + ) + ).toBe(false) + } + ) itOn( ['darwin', 'android', 'ios'], diff --git a/plugins/deep-link/src/lib.rs b/plugins/deep-link/src/lib.rs index eb5443159..9796060a6 100644 --- a/plugins/deep-link/src/lib.rs +++ b/plugins/deep-link/src/lib.rs @@ -148,7 +148,7 @@ mod imp { /// /// ## Platform-specific: /// - /// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). May not work on older distros. + /// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). Needs the `update-desktop-database` command available on the system. May not work on older distros. /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). pub fn unregister>(&self, _protocol: S) -> crate::Result<()> { Err(crate::Error::UnsupportedPlatform) @@ -383,7 +383,7 @@ mod imp { /// /// - **Windows**: Requires admin rights if the protocol is registered on local machine /// (this can happen when registered from the NSIS installer when the install mode is set to both or per machine) - /// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). May not work on older distros. + /// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). Refreshes the desktop database with the `update-desktop-database` command; without it, [`is_registered`](`Self::is_registered`) may keep returning `true`. May not work on older distros. /// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`). pub fn unregister>(&self, _protocol: S) -> crate::Result<()> { #[cfg(windows)] @@ -401,9 +401,6 @@ mod imp { #[cfg(target_os = "linux")] { - let mimeapps_path = self.app.path().config_dir()?.join("mimeapps.list"); - let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path)?; - let file_name = format!( "{}-handler.desktop", tauri::utils::platform::current_exe()? @@ -411,16 +408,55 @@ mod imp { .unwrap() .to_string_lossy() ); + let mime_type = format!("x-scheme-handler/{}", _protocol.as_ref()); - if let Some(section) = mimeapps.section_mut(Some("Default Applications")) { - let scheme = format!("x-scheme-handler/{}", _protocol.as_ref()); - - if section.get(&scheme).unwrap_or_default() == file_name { - section.remove(scheme); + // stop being the default handler + let mimeapps_path = self.app.path().config_dir()?.join("mimeapps.list"); + if mimeapps_path.exists() { + let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path)?; + if let Some(section) = mimeapps.section_mut(Some("Default Applications")) { + if section.get(&mime_type).unwrap_or_default() == file_name { + section.remove(&mime_type); + } } + mimeapps.write_to_file(&mimeapps_path)?; } - mimeapps.write_to_file(mimeapps_path)?; + // Stop declaring the scheme in the handler's `.desktop` file too: the desktop + // database indexes it, and with no default set `xdg-mime` falls back to that + // index, so the app would otherwise still be the handler. + let applications = self.app.path().data_dir()?.join("applications"); + let desktop_file_path = applications.join(&file_name); + // Only the `MimeType` key is touched: the file may carry other changes. + if let Ok(mut desktop_file) = ini::Ini::load_from_file(&desktop_file_path) { + if let Some(section) = desktop_file.section_mut(Some("Desktop Entry")) { + let mime_types = section + .get("MimeType") + .unwrap_or_default() + .split(';') + .filter(|mime| !mime.is_empty() && *mime != mime_type) + .map(ToString::to_string) + .collect::>(); + if mime_types.is_empty() { + section.remove("MimeType"); + } else { + section.insert("MimeType", mime_types.join(";")); + } + } + desktop_file.write_to_file(&desktop_file_path)?; + + // Without the refreshed index `xdg-mime` may keep reporting the app as the + // handler, but the scheme is unregistered as far as the app can tell, so a + // missing command is not an error. + if let Err(e) = Command::new("update-desktop-database") + .arg(&applications) + .status() + { + tracing::warn!( + "Failed to run OS command `update-desktop-database`, the desktop database may still list the app as the `{mime_type}` handler: {e}" + ); + } + } Ok(()) } From 55f9645aa25ead7ea3f73dbc4a14069ff1b98fa3 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Wed, 23 Sep 2026 06:08:43 -0300 Subject: [PATCH 12/29] chore: update websocket test --- packages/api-e2e/test/specs/websocket.spec.ts | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/api-e2e/test/specs/websocket.spec.ts b/packages/api-e2e/test/specs/websocket.spec.ts index dece2e3fe..cb01f55a7 100644 --- a/packages/api-e2e/test/specs/websocket.spec.ts +++ b/packages/api-e2e/test/specs/websocket.spec.ts @@ -26,11 +26,13 @@ describePlugin('websocket', () => { const messages = await tauri(async (api, url) => { const ws = await api.websocket.connect(url) const received: Message[] = [] - await new Promise((resolve, reject) => { + // a copy taken once the echoes are in: the close acknowledgement that + // `disconnect` triggers may arrive before this function returns + const echoes = await new Promise((resolve, reject) => { setTimeout(() => reject(new Error('echo not received')), 5000) ws.addListener((message) => { received.push(message) - if (received.length === 3) resolve() + if (received.length === 3) resolve(received.slice()) }) ws.send('hello') .then(() => ws.send([1, 2, 3])) @@ -38,7 +40,7 @@ describePlugin('websocket', () => { .catch(reject) }) await ws.disconnect() - return received + return echoes }, WEBSOCKET_FIXTURE_URL) expect(messages).toEqual([ { type: 'Text', data: 'hello' }, @@ -84,21 +86,25 @@ describePlugin('websocket', () => { let removed = 0 let kept = 0 const remove = ws.addListener(() => removed++) - await new Promise((resolve, reject) => { - setTimeout(() => reject(new Error('echoes not received')), 5000) - ws.addListener(() => { - kept++ - if (kept === 1) { - remove() - ws.send('second').catch(reject) - } else { - resolve() - } - }) - ws.send('first').catch(reject) - }) + // counted once both echoes are in, before `disconnect`'s close + // acknowledgement can reach the listener still attached + const counts = await new Promise<{ removed: number; kept: number }>( + (resolve, reject) => { + setTimeout(() => reject(new Error('echoes not received')), 5000) + ws.addListener(() => { + kept++ + if (kept === 1) { + remove() + ws.send('second').catch(reject) + } else { + resolve({ removed, kept }) + } + }) + ws.send('first').catch(reject) + } + ) await ws.disconnect() - return { removed, kept } + return counts }, WEBSOCKET_FIXTURE_URL) expect(counts).toEqual({ removed: 1, kept: 2 }) }) From 101e1b068a62aeece725362c38f8fe4179b5fe5f Mon Sep 17 00:00:00 2001 From: Timofey Maykov <156189619+timofey-maykov@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:29:37 +0300 Subject: [PATCH 13/29] fix(updater): stop overriding SSL_CERT_FILE and SSL_CERT_DIR on Linux (#3615) check() set both variables to Debian paths when they were unset. On distros with a different layout, such as ALT Linux, the paths do not exist, and rustls-native-certs then loads roots only from them, so every TLS client created afterwards in the app got an empty root store. reqwest with rustls already finds the system store through rustls-platform-verifier and openssl-probe, so the override is not needed. Closes #3598 --- .changes/updater-remove-ssl-cert-env.md | 6 ++++++ plugins/updater/src/updater.rs | 10 ---------- 2 files changed, 6 insertions(+), 10 deletions(-) create mode 100644 .changes/updater-remove-ssl-cert-env.md diff --git a/.changes/updater-remove-ssl-cert-env.md b/.changes/updater-remove-ssl-cert-env.md new file mode 100644 index 000000000..263f3fbde --- /dev/null +++ b/.changes/updater-remove-ssl-cert-env.md @@ -0,0 +1,6 @@ +--- +"updater": patch:bug +"updater-js": patch:bug +--- + +On Linux, `check()` no longer sets `SSL_CERT_FILE` and `SSL_CERT_DIR` to Debian paths. The override pointed rustls to files that do not exist on distros with a different layout, such as ALT Linux, and left every TLS client in the app without root certificates. diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index d04a19cdb..809cd0065 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -539,16 +539,6 @@ impl Updater { headers.insert(ACCEPT, HeaderValue::from_static("application/json")); } - // Set SSL certs for linux if they aren't available. - #[cfg(target_os = "linux")] - { - if std::env::var_os("SSL_CERT_FILE").is_none() { - std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"); - } - if std::env::var_os("SSL_CERT_DIR").is_none() { - std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); - } - } let target = if let Some(target) = &self.target { target } else { From 0c84c917e32af391fed9eaac9e131e447d0a1148 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Wed, 23 Sep 2026 12:57:33 -0300 Subject: [PATCH 14/29] fix(fs): log file watcher errors (#3621) In plugins/fs/src/watcher.rs, notify errors were discarded (TODO), so a watch that died left no trace. They are now logged with log::error!. Reporting them to the JS callback would change the event payload and is deferred to v3. --- .changes/fix-fs-watch-errors.md | 6 ++++++ plugins/fs/src/watcher.rs | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 .changes/fix-fs-watch-errors.md diff --git a/.changes/fix-fs-watch-errors.md b/.changes/fix-fs-watch-errors.md new file mode 100644 index 000000000..418704489 --- /dev/null +++ b/.changes/fix-fs-watch-errors.md @@ -0,0 +1,6 @@ +--- +fs: patch +fs-js: patch +--- + +File watcher errors (e.g. the watched directory was removed or the inotify limit was reached) are now logged instead of being silently dropped. diff --git a/plugins/fs/src/watcher.rs b/plugins/fs/src/watcher.rs index de9a85d31..68f6a8019 100644 --- a/plugins/fs/src/watcher.rs +++ b/plugins/fs/src/watcher.rs @@ -69,13 +69,18 @@ pub fn watch( let mut debouncer = new_debouncer( Duration::from_millis(delay), None, - move |events: Result, Vec>| { - if let Ok(events) = events { + move |events: Result, Vec>| match events { + Ok(events) => { for event in events { - // TODO: Should errors be emitted too? let _ = on_event.send(event.event); } } + // TODO(v3): report the errors to the webview + Err(errors) => { + for error in errors { + log::error!("file watcher error: {error}"); + } + } }, )?; for path in &resolved_paths { @@ -84,11 +89,12 @@ pub fn watch( WatcherKind::Debouncer(debouncer) } else { let mut watcher = RecommendedWatcher::new( - move |event| { - if let Ok(event) = event { - // TODO: Should errors be emitted too? + move |event: notify::Result| match event { + Ok(event) => { let _ = on_event.send(event); } + // TODO(v3): report the errors to the webview + Err(error) => log::error!("file watcher error: {error}"), }, Config::default(), )?; From b46a88ff55948367e40a90f4fcec807f8e43cbc9 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Wed, 23 Sep 2026 12:57:49 -0300 Subject: [PATCH 15/29] fix(fs): reject malformed write_file options instead of ignoring them (#3622) In plugins/fs/src/commands.rs write_file_inner, a malformed or non-ASCII `options` header was dropped with .ok(), so baseDir/append/createNew were silently ignored and the call became a truncating write to the raw path; the JSON array body fallback truncated values above 255 and dropped non-numbers. The header is parsed with parse_write_file_options: empty, `undefined` (what fetch sends for JSON.stringify(undefined)) and `null` mean no options, anything else must parse. Array bodies must only contain bytes. Unit test for the header parser; e2e spec for writes without options, which is the case the `undefined` handling keeps working. --- .changes/fix-fs-write-options-header.md | 6 ++++ packages/api-e2e/test/specs/fs.spec.ts | 15 ++++++++ plugins/fs/src/commands.rs | 48 ++++++++++++++++++++++--- 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 .changes/fix-fs-write-options-header.md diff --git a/.changes/fix-fs-write-options-header.md b/.changes/fix-fs-write-options-header.md new file mode 100644 index 000000000..4949106f5 --- /dev/null +++ b/.changes/fix-fs-write-options-header.md @@ -0,0 +1,6 @@ +--- +fs: patch +fs-js: patch +--- + +`writeFile` and `writeTextFile` now fail when their options cannot be parsed instead of silently ignoring them (including `baseDir`, `append` and `createNew`) and writing to the raw path. Data sent as a JSON array is rejected if it contains values that are not bytes, instead of truncating them. diff --git a/packages/api-e2e/test/specs/fs.spec.ts b/packages/api-e2e/test/specs/fs.spec.ts index d590e0395..32767d606 100644 --- a/packages/api-e2e/test/specs/fs.spec.ts +++ b/packages/api-e2e/test/specs/fs.spec.ts @@ -70,6 +70,21 @@ describePlugin('fs', () => { expect(read).toBe('first second') }) + it('writeFile and writeTextFile work without options', async () => { + const result = await tauri(async (api, dir) => { + const path = await api.path.join( + await api.path.appDataDir(), + dir, + 'no-options.txt' + ) + await api.fs.writeTextFile(path, 'text') + const text = await api.fs.readTextFile(path) + await api.fs.writeFile(path, new Uint8Array([98, 121, 116, 101, 115])) + return { text, bytes: await api.fs.readTextFile(path) } + }, dir) + expect(result).toEqual({ text: 'text', bytes: 'bytes' }) + }) + it('writeFile and readFile round-trip binary data', async () => { const bytes = [0, 1, 2, 3, 250, 251, 252, 253, 254, 255] const read = await tauri( diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs index fcc38f6bb..1fb5a69d0 100644 --- a/plugins/fs/src/commands.rs +++ b/plugins/fs/src/commands.rs @@ -1093,11 +1093,12 @@ async fn write_file_inner( }) .and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?; - let options: Option = request + let options = request .headers() .get("options") - .and_then(|p| p.to_str().ok()) - .and_then(|opts| serde_json::from_str(opts).ok()); + .map(|options| parse_write_file_options(options.as_bytes())) + .transpose()? + .flatten(); let mut file_handle = resolve_file( permission, @@ -1140,8 +1141,12 @@ async fn write_file_inner( tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data), tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned( data.iter() - .flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8))) - .collect(), + .map(|v| { + v.as_u64() + .and_then(|v| u8::try_from(v).ok()) + .ok_or_else(|| anyhow::anyhow!("invalid byte in the data to write: {v}")) + }) + .collect::, _>>()?, ), _ => return Err(anyhow::anyhow!("unexpected invoke body").into()), }; @@ -1157,6 +1162,20 @@ async fn write_file_inner( .map_err(Into::into) } +/// Parses the `options` header of the `write_file` command. +/// +/// Fails instead of silently falling back to the defaults, which would e.g. ignore `baseDir`. +fn parse_write_file_options(header: &[u8]) -> CommandResult> { + let header = String::from_utf8_lossy(header); + match header.trim() { + // `JSON.stringify(undefined)` is sent as `undefined` by `fetch` + "" | "undefined" | "null" => Ok(None), + options => serde_json::from_str(options) + .map(Some) + .map_err(|e| anyhow::anyhow!("invalid write file options {options}: {e}").into()), + } +} + #[tauri::command] pub async fn write_file( webview: Webview, @@ -1810,6 +1829,25 @@ mod test { use super::LinesBytes; + #[test] + fn write_file_options_header() { + use super::parse_write_file_options; + + assert!(parse_write_file_options(b"undefined").unwrap().is_none()); + assert!(parse_write_file_options(b"null").unwrap().is_none()); + assert!(parse_write_file_options(b"").unwrap().is_none()); + + let options = parse_write_file_options(br#"{"baseDir":14,"append":true}"#) + .unwrap() + .unwrap(); + assert!(options.append); + assert!(options.create); + assert!(options.base.base_dir.is_some()); + + assert!(parse_write_file_options(b"{not json").is_err()); + assert!(parse_write_file_options(br#"{"append":"yes"}"#).is_err()); + } + #[test] fn safe_file_path_parse() { use super::SafeFilePath; From 97e865bc138925ba29b102a9cb99d406ce1fe48e Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Wed, 23 Sep 2026 12:58:03 -0300 Subject: [PATCH 16/29] feat(fs): accept baseDir in size (#3623) In plugins/fs/guest-js/index.ts, the size function only sent `path` although the Rust command accepts `options.baseDir` and the JSDoc example passed `{ baseDir: BaseDirectory.AppData }` (a TS error, silently dropped at runtime, so the relative path was rejected). Adds an optional `options?: SizeOptions` parameter (new exported type). api-iife.js regenerated. The e2e spec now measures a file and a directory relative to AppData instead of working around the missing option. --- .changes/feat-fs-size-options.md | 6 ++++++ packages/api-e2e/test/specs/fs.spec.ts | 7 +++++-- plugins/fs/api-iife.js | 2 +- plugins/fs/guest-js/index.ts | 20 ++++++++++++++++++-- 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 .changes/feat-fs-size-options.md diff --git a/.changes/feat-fs-size-options.md b/.changes/feat-fs-size-options.md new file mode 100644 index 000000000..463ae615e --- /dev/null +++ b/.changes/feat-fs-size-options.md @@ -0,0 +1,6 @@ +--- +fs: minor +fs-js: minor +--- + +Added an `options` parameter with `baseDir` to `size`, so relative paths can be measured, like the other functions and like the JSDoc example already showed. The Rust command already accepted it. diff --git a/packages/api-e2e/test/specs/fs.spec.ts b/packages/api-e2e/test/specs/fs.spec.ts index 32767d606..0d0d21993 100644 --- a/packages/api-e2e/test/specs/fs.spec.ts +++ b/packages/api-e2e/test/specs/fs.spec.ts @@ -116,10 +116,11 @@ describePlugin('fs', () => { hasMtime: fileStat.mtime instanceof Date }, lstatSize: fileLstat.size, - // `size` only takes absolute paths - size: await api.fs.size( + size: await api.fs.size(file, { baseDir }), + absoluteSize: await api.fs.size( await api.path.join(await api.path.appDataDir(), file) ), + dirSize: await api.fs.size(dir, { baseDir }), dir: { isFile: dirStat.isFile, isDirectory: dirStat.isDirectory } } }, dir) @@ -132,6 +133,8 @@ describePlugin('fs', () => { }) expect(result.lstatSize).toBe(10) expect(result.size).toBe(10) + expect(result.absoluteSize).toBe(10) + expect(result.dirSize).toBeGreaterThanOrEqual(10) expect(result.dir).toEqual({ isFile: false, isDirectory: true }) }) diff --git a/plugins/fs/api-iife.js b/plugins/fs/api-iife.js index ba0603886..fe949f75b 100644 --- a/plugins/fs/api-iife.js +++ b/plugins/fs/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_FS__=function(e,n,t){"use strict";var i;function o(e){return{isFile:e.isFile,isDirectory:e.isDirectory,isSymlink:e.isSymlink,size:e.size,mtime:null!==e.mtime?new Date(e.mtime):null,atime:null!==e.atime?new Date(e.atime):null,birthtime:null!==e.birthtime?new Date(e.birthtime):null,readonly:e.readonly,fileAttributes:e.fileAttributes,dev:e.dev,ino:e.ino,mode:e.mode,nlink:e.nlink,uid:e.uid,gid:e.gid,rdev:e.rdev,blksize:e.blksize,blocks:e.blocks}}e.SeekMode=void 0,(i=e.SeekMode||(e.SeekMode={}))[i.Start=0]="Start",i[i.Current=1]="Current",i[i.End=2]="End";class r extends t.Resource{async read(e){if(0===e.byteLength)return 0;const n=await t.invoke("plugin:fs|read",{rid:this.rid,len:e.byteLength}),i=function(e){const n=new Uint8ClampedArray(e),t=n.byteLength;let i=0;for(let e=0;ee instanceof URL?e.toString():e),options:i,onEvent:r}),c=new s(a);return()=>{c.close()}}return Object.defineProperty(e,"BaseDirectory",{enumerable:!0,get:function(){return n.BaseDirectory}}),e.FileHandle=r,e.copyFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol||n instanceof URL&&"file:"!==n.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|copy_file",{fromPath:e instanceof URL?e.toString():e,toPath:n instanceof URL?n.toString():n,options:i})},e.create=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|create",{path:e instanceof URL?e.toString():e,options:n});return new r(i)},e.exists=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|exists",{path:e instanceof URL?e.toString():e,options:n})},e.lstat=async function(e,n){return o(await t.invoke("plugin:fs|lstat",{path:e instanceof URL?e.toString():e,options:n}))},e.mkdir=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|mkdir",{path:e instanceof URL?e.toString():e,options:n})},e.open=a,e.readDir=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|read_dir",{path:e instanceof URL?e.toString():e,options:n})},e.readFile=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|read_file",{path:e instanceof URL?e.toString():e,options:n});return i instanceof ArrayBuffer?new Uint8Array(i):Uint8Array.from(i)},e.readTextFile=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|read_text_file",{path:e instanceof URL?e.toString():e,options:n}),o=i instanceof ArrayBuffer?i:Uint8Array.from(i);return new TextDecoder(n?.encoding??"utf-8").decode(o)},e.readTextFileLines=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=e instanceof URL?e.toString():e;return await Promise.resolve({path:i,rid:null,async next(){const e=new TextDecoder(n?.encoding??"utf-8");if(null===this.rid){const o=e.encoding;this.rid=await t.invoke("plugin:fs|read_text_file_lines",{path:i,options:null!=n?{...n,encoding:o}:void 0})}const o=await t.invoke("plugin:fs|read_text_file_lines_next",{rid:this.rid}),r=o instanceof ArrayBuffer?new Uint8Array(o):Uint8Array.from(o),a=1===r[r.byteLength-1];if(a)return this.rid=null,{value:null,done:a};return{value:e.decode(r.slice(0,r.byteLength-1)),done:a}},[Symbol.asyncIterator](){return this}})},e.remove=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|remove",{path:e instanceof URL?e.toString():e,options:n})},e.rename=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol||n instanceof URL&&"file:"!==n.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|rename",{oldPath:e instanceof URL?e.toString():e,newPath:n instanceof URL?n.toString():n,options:i})},e.size=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|size",{path:e instanceof URL?e.toString():e})},e.startAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|start_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.stat=async function(e,n){return o(await t.invoke("plugin:fs|stat",{path:e instanceof URL?e.toString():e,options:n}))},e.stopAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|stop_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.truncate=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|truncate",{path:e instanceof URL?e.toString():e,len:n,options:i})},e.watch=async function(e,n,t){return await c(e,n,{delayMs:2e3,...t})},e.watchImmediate=async function(e,n,t){return await c(e,n,{...t,delayMs:void 0})},e.writeFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");if(n instanceof ReadableStream){const t=await a(e,{read:!1,create:!0,write:!0,...i}),o=n.getReader();try{for(;;){const{done:e,value:n}=await o.read();if(e)break;await t.write(n)}}finally{o.releaseLock(),await t.close()}}else await t.invoke("plugin:fs|write_file",n,{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e.writeTextFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const o=new TextEncoder;await t.invoke("plugin:fs|write_text_file",o.encode(n),{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e}({},window.__TAURI__.path,window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"fs",{value:__TAURI_PLUGIN_FS__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_FS__=function(e,n,t){"use strict";var i;function o(e){return{isFile:e.isFile,isDirectory:e.isDirectory,isSymlink:e.isSymlink,size:e.size,mtime:null!==e.mtime?new Date(e.mtime):null,atime:null!==e.atime?new Date(e.atime):null,birthtime:null!==e.birthtime?new Date(e.birthtime):null,readonly:e.readonly,fileAttributes:e.fileAttributes,dev:e.dev,ino:e.ino,mode:e.mode,nlink:e.nlink,uid:e.uid,gid:e.gid,rdev:e.rdev,blksize:e.blksize,blocks:e.blocks}}e.SeekMode=void 0,(i=e.SeekMode||(e.SeekMode={}))[i.Start=0]="Start",i[i.Current=1]="Current",i[i.End=2]="End";class r extends t.Resource{async read(e){if(0===e.byteLength)return 0;const n=await t.invoke("plugin:fs|read",{rid:this.rid,len:e.byteLength}),i=function(e){const n=new Uint8ClampedArray(e),t=n.byteLength;let i=0;for(let e=0;ee instanceof URL?e.toString():e),options:i,onEvent:r}),c=new s(a);return()=>{c.close()}}return Object.defineProperty(e,"BaseDirectory",{enumerable:!0,get:function(){return n.BaseDirectory}}),e.FileHandle=r,e.copyFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol||n instanceof URL&&"file:"!==n.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|copy_file",{fromPath:e instanceof URL?e.toString():e,toPath:n instanceof URL?n.toString():n,options:i})},e.create=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|create",{path:e instanceof URL?e.toString():e,options:n});return new r(i)},e.exists=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|exists",{path:e instanceof URL?e.toString():e,options:n})},e.lstat=async function(e,n){return o(await t.invoke("plugin:fs|lstat",{path:e instanceof URL?e.toString():e,options:n}))},e.mkdir=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|mkdir",{path:e instanceof URL?e.toString():e,options:n})},e.open=a,e.readDir=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|read_dir",{path:e instanceof URL?e.toString():e,options:n})},e.readFile=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|read_file",{path:e instanceof URL?e.toString():e,options:n});return i instanceof ArrayBuffer?new Uint8Array(i):Uint8Array.from(i)},e.readTextFile=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|read_text_file",{path:e instanceof URL?e.toString():e,options:n}),o=i instanceof ArrayBuffer?i:Uint8Array.from(i);return new TextDecoder(n?.encoding??"utf-8").decode(o)},e.readTextFileLines=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=e instanceof URL?e.toString():e;return await Promise.resolve({path:i,rid:null,async next(){const e=new TextDecoder(n?.encoding??"utf-8");if(null===this.rid){const o=e.encoding;this.rid=await t.invoke("plugin:fs|read_text_file_lines",{path:i,options:null!=n?{...n,encoding:o}:void 0})}const o=await t.invoke("plugin:fs|read_text_file_lines_next",{rid:this.rid}),r=o instanceof ArrayBuffer?new Uint8Array(o):Uint8Array.from(o),a=1===r[r.byteLength-1];if(a)return this.rid=null,{value:null,done:a};return{value:e.decode(r.slice(0,r.byteLength-1)),done:a}},[Symbol.asyncIterator](){return this}})},e.remove=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|remove",{path:e instanceof URL?e.toString():e,options:n})},e.rename=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol||n instanceof URL&&"file:"!==n.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|rename",{oldPath:e instanceof URL?e.toString():e,newPath:n instanceof URL?n.toString():n,options:i})},e.size=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|size",{path:e instanceof URL?e.toString():e,options:n})},e.startAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|start_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.stat=async function(e,n){return o(await t.invoke("plugin:fs|stat",{path:e instanceof URL?e.toString():e,options:n}))},e.stopAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|stop_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.truncate=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|truncate",{path:e instanceof URL?e.toString():e,len:n,options:i})},e.watch=async function(e,n,t){return await c(e,n,{delayMs:2e3,...t})},e.watchImmediate=async function(e,n,t){return await c(e,n,{...t,delayMs:void 0})},e.writeFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");if(n instanceof ReadableStream){const t=await a(e,{read:!1,create:!0,write:!0,...i}),o=n.getReader();try{for(;;){const{done:e,value:n}=await o.read();if(e)break;await t.write(n)}}finally{o.releaseLock(),await t.close()}}else await t.invoke("plugin:fs|write_file",n,{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e.writeTextFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const o=new TextEncoder;await t.invoke("plugin:fs|write_text_file",o.encode(n),{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e}({},window.__TAURI__.path,window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"fs",{value:__TAURI_PLUGIN_FS__})} diff --git a/plugins/fs/guest-js/index.ts b/plugins/fs/guest-js/index.ts index 2bea26943..e3d39a754 100644 --- a/plugins/fs/guest-js/index.ts +++ b/plugins/fs/guest-js/index.ts @@ -1493,6 +1493,16 @@ async function watchImmediate( }) } +/** + * Options for the `size` function. + * + * @since 2.6.0 + */ +interface SizeOptions { + /** Base directory for `path`. */ + baseDir?: BaseDirectory +} + /** * Get the size of a file or directory. For files, the `stat` functions can be used as well. * @@ -1507,16 +1517,21 @@ async function watchImmediate( * ``` * * @param path The path of the file or directory to measure. + * @param options Options defining the base directory of `path` (since 2.6.0). * @returns A promise resolving to the size in bytes. * @since 2.1.0 */ -async function size(path: string | URL): Promise { +async function size( + path: string | URL, + options?: SizeOptions +): Promise { if (path instanceof URL && path.protocol !== 'file:') { throw new TypeError('Must be a file URL.') } return await invoke('plugin:fs|size', { - path: path instanceof URL ? path.toString() : path + path: path instanceof URL ? path.toString() : path, + options }) } @@ -1609,6 +1624,7 @@ export type { TruncateOptions, WriteFileOptions, ExistsOptions, + SizeOptions, FileInfo, WatchOptions, DebouncedWatchOptions, From 9106093f775a09f899561d7823518e76f9bd6b45 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Wed, 23 Sep 2026 12:58:28 -0300 Subject: [PATCH 17/29] fix(fs): map open options to valid Android modes (#3624) In plugins/fs/src/lib.rs OpenOptions::android_mode, the mode was built by concatenating r/w/t/a, so open(url, {append: true}) (read defaults to true) produced "ra" and write+truncate+append "wta", which ParcelFileDescriptor.parseMode / ContentResolver reject. The options now map to r, w, wt, wa, rw or rwt. append wins over the default read (no read+append mode exists). create/createNew still have no Android equivalent (documented on the function). Unit tests for the mapping, compiled on the host with cfg(test). --- .changes/fix-fs-android-modes.md | 6 +++ plugins/fs/src/lib.rs | 68 ++++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 .changes/fix-fs-android-modes.md diff --git a/.changes/fix-fs-android-modes.md b/.changes/fix-fs-android-modes.md new file mode 100644 index 000000000..dbe1af000 --- /dev/null +++ b/.changes/fix-fs-android-modes.md @@ -0,0 +1,6 @@ +--- +fs: patch +fs-js: patch +--- + +Fixed opening Android `content://` URIs with `append` or with `write`, `truncate` and `append` together: the options were turned into invalid modes such as `ra` or `wta`. They now map to the modes Android accepts (`r`, `w`, `wt`, `wa`, `rw`, `rwt`), `append` taking precedence over `read` since there is no read and append mode. diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index cac582bb4..7f57f11cb 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -361,24 +361,23 @@ impl std::os::unix::fs::OpenOptionsExt for OpenOptions { } impl OpenOptions { - #[cfg(target_os = "android")] - fn android_mode(&self) -> String { - let mut mode = String::new(); - - if self.read { - mode.push('r'); + /// The mode passed to `ContentResolver.openAssetFileDescriptor` / `ParcelFileDescriptor.parseMode`, + /// which only accept `r`, `w`, `wt`, `wa`, `rw` and `rwt`. + /// + /// `create` and `create_new` have no equivalent: whether a missing file is created + /// depends on the content provider. + #[cfg(any(target_os = "android", test))] + fn android_mode(&self) -> &'static str { + match (self.read, self.write || self.append) { + (_, false) => "r", + // there is no read + append mode, and `read` defaults to `true` from JavaScript: + // honor the explicit append + (_, true) if self.append => "wa", + (true, true) if self.truncate => "rwt", + (true, true) => "rw", + (false, true) if self.truncate => "wt", + (false, true) => "w", } - if self.write { - mode.push('w'); - } - if self.truncate { - mode.push('t'); - } - if self.append { - mode.push('a'); - } - - mode } } @@ -630,3 +629,38 @@ pub fn init() -> TauriPlugin> { }) .build() } + +#[cfg(test)] +mod tests { + use super::OpenOptions; + + #[test] + fn android_modes_are_valid() { + let mode = |json: &str| { + serde_json::from_str::(json) + .unwrap() + .android_mode() + }; + + // `read` defaults to true when deserialized + assert_eq!(mode(r#"{}"#), "r"); + assert_eq!(mode(r#"{ "read": false }"#), "r"); + assert_eq!(mode(r#"{ "write": true }"#), "rw"); + assert_eq!(mode(r#"{ "write": true, "truncate": true }"#), "rwt"); + assert_eq!(mode(r#"{ "append": true }"#), "wa"); + assert_eq!(mode(r#"{ "read": false, "write": true }"#), "w"); + assert_eq!( + mode(r#"{ "read": false, "write": true, "truncate": true }"#), + "wt" + ); + assert_eq!(mode(r#"{ "read": false, "append": true }"#), "wa"); + assert_eq!( + mode(r#"{ "read": false, "write": true, "truncate": true, "append": true }"#), + "wa" + ); + assert_eq!( + mode(r#"{ "read": false, "write": true, "create": true }"#), + "w" + ); + } +} From 1cae06a55b3398d409fba5ac17dbac63d9cf7652 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Wed, 23 Sep 2026 13:05:04 -0300 Subject: [PATCH 18/29] fix(fs): report read errors of readTextFileLines (#3625) * fix(fs): report read errors of readTextFileLines In plugins/fs/src/commands.rs read_text_file_lines_next, a read error was mapped to "not done, empty line", so a persistent error such as EISDIR (File::open of a directory succeeds on Unix) made the iterator yield "" forever and the error was swallowed. The command now closes the resource and returns the error; the JS iterator resets its rid so a new iteration starts over. api-iife.js regenerated. E2E spec iterates a directory and expects a rejection. * fix(fs): close the file when a readTextFileLines loop exits early In plugins/fs/guest-js/index.ts readTextFileLines, the async iterator did not implement return(), so breaking out of a for await loop left the StdLinesResource (an open file) in the webview resource table. return() now closes the resource and resets the iterator. api-iife.js regenerated. E2E spec breaks out of a loop, checks the resource id is no longer valid and that iterating again starts over. --- .changes/fix-fs-read-lines-error.md | 6 +++ .changes/fix-fs-read-lines-return.md | 6 +++ packages/api-e2e/test/specs/fs.spec.ts | 51 ++++++++++++++++++++++++++ plugins/fs/api-iife.js | 2 +- plugins/fs/guest-js/index.ts | 26 +++++++++++-- plugins/fs/src/commands.rs | 7 +++- 6 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 .changes/fix-fs-read-lines-error.md create mode 100644 .changes/fix-fs-read-lines-return.md diff --git a/.changes/fix-fs-read-lines-error.md b/.changes/fix-fs-read-lines-error.md new file mode 100644 index 000000000..e4618efd8 --- /dev/null +++ b/.changes/fix-fs-read-lines-error.md @@ -0,0 +1,6 @@ +--- +fs: patch +fs-js: patch +--- + +Fixed `readTextFileLines` yielding empty lines forever when reading fails (e.g. on a directory). The read error is now reported: the iterator rejects and the file is closed. diff --git a/.changes/fix-fs-read-lines-return.md b/.changes/fix-fs-read-lines-return.md new file mode 100644 index 000000000..5259efe48 --- /dev/null +++ b/.changes/fix-fs-read-lines-return.md @@ -0,0 +1,6 @@ +--- +fs: patch +fs-js: patch +--- + +Fixed `readTextFileLines` leaving the file open until the webview is destroyed when a `for await` loop over it exits early (`break`, `return` or `throw`). The iterator now implements `return()`, which closes the file. diff --git a/packages/api-e2e/test/specs/fs.spec.ts b/packages/api-e2e/test/specs/fs.spec.ts index 0d0d21993..738395aaa 100644 --- a/packages/api-e2e/test/specs/fs.spec.ts +++ b/packages/api-e2e/test/specs/fs.spec.ts @@ -234,6 +234,57 @@ describePlugin('fs', () => { expect(lines).toEqual(['one', 'two', 'three']) }) + it('readTextFileLines closes the file when a loop exits early', async () => { + const result = await tauri(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.writeTextFile(path, 'one\ntwo\nthree', { baseDir }) + const lines = await api.fs.readTextFileLines(path, { baseDir }) + // the iterator keeps the id of the open file in `rid` + const state = lines as unknown as { rid: number | null } + let rid: number | null = null + let first: string | null = null + for await (const line of lines) { + first = line + rid = state.rid + break + } + let closeError: string | null = null + try { + await api.core.invoke('plugin:resources|close', { rid }) + } catch (error) { + closeError = String(error) + } + // iterating again starts over + const all: string[] = [] + for await (const line of lines) { + all.push(line) + } + return { first, ridAfter: state.rid, closeError, all } + }, `${dir}/lines-break.txt`) + expect(result.first).toBe('one') + expect(result.ridAfter).toBeNull() + // the resource was already closed by the iterator + expect(result.closeError).toMatch(/resource id \d+ is invalid/) + expect(result.all).toEqual(['one', 'two', 'three']) + }) + + it('readTextFileLines rejects when the file cannot be read', async () => { + // a directory: opening it fails on Windows, reading it fails elsewhere, + // which used to yield empty lines forever + const message = await tauriError(async (api, path) => { + const baseDir = api.fs.BaseDirectory.AppData + await api.fs.mkdir(path, { baseDir, recursive: true }) + const lines = await api.fs.readTextFileLines(path, { baseDir }) + for (let i = 0; i < 1000; i++) { + const { done } = await lines.next() + if (done) return + } + throw new Error('readTextFileLines kept yielding lines for a directory') + }, `${dir}/lines-dir`) + expect(message).not.toMatch(/kept yielding/) + expect(message).toMatch(/failed to (read line|open file)/) + }) + it('FileHandle supports write, seek, read, stat and truncate', async () => { const result = await tauri(async (api, path) => { const baseDir = api.fs.BaseDirectory.AppData diff --git a/plugins/fs/api-iife.js b/plugins/fs/api-iife.js index fe949f75b..1cb8b78d0 100644 --- a/plugins/fs/api-iife.js +++ b/plugins/fs/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_FS__=function(e,n,t){"use strict";var i;function o(e){return{isFile:e.isFile,isDirectory:e.isDirectory,isSymlink:e.isSymlink,size:e.size,mtime:null!==e.mtime?new Date(e.mtime):null,atime:null!==e.atime?new Date(e.atime):null,birthtime:null!==e.birthtime?new Date(e.birthtime):null,readonly:e.readonly,fileAttributes:e.fileAttributes,dev:e.dev,ino:e.ino,mode:e.mode,nlink:e.nlink,uid:e.uid,gid:e.gid,rdev:e.rdev,blksize:e.blksize,blocks:e.blocks}}e.SeekMode=void 0,(i=e.SeekMode||(e.SeekMode={}))[i.Start=0]="Start",i[i.Current=1]="Current",i[i.End=2]="End";class r extends t.Resource{async read(e){if(0===e.byteLength)return 0;const n=await t.invoke("plugin:fs|read",{rid:this.rid,len:e.byteLength}),i=function(e){const n=new Uint8ClampedArray(e),t=n.byteLength;let i=0;for(let e=0;ee instanceof URL?e.toString():e),options:i,onEvent:r}),c=new s(a);return()=>{c.close()}}return Object.defineProperty(e,"BaseDirectory",{enumerable:!0,get:function(){return n.BaseDirectory}}),e.FileHandle=r,e.copyFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol||n instanceof URL&&"file:"!==n.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|copy_file",{fromPath:e instanceof URL?e.toString():e,toPath:n instanceof URL?n.toString():n,options:i})},e.create=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|create",{path:e instanceof URL?e.toString():e,options:n});return new r(i)},e.exists=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|exists",{path:e instanceof URL?e.toString():e,options:n})},e.lstat=async function(e,n){return o(await t.invoke("plugin:fs|lstat",{path:e instanceof URL?e.toString():e,options:n}))},e.mkdir=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|mkdir",{path:e instanceof URL?e.toString():e,options:n})},e.open=a,e.readDir=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|read_dir",{path:e instanceof URL?e.toString():e,options:n})},e.readFile=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|read_file",{path:e instanceof URL?e.toString():e,options:n});return i instanceof ArrayBuffer?new Uint8Array(i):Uint8Array.from(i)},e.readTextFile=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await t.invoke("plugin:fs|read_text_file",{path:e instanceof URL?e.toString():e,options:n}),o=i instanceof ArrayBuffer?i:Uint8Array.from(i);return new TextDecoder(n?.encoding??"utf-8").decode(o)},e.readTextFileLines=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=e instanceof URL?e.toString():e;return await Promise.resolve({path:i,rid:null,async next(){const e=new TextDecoder(n?.encoding??"utf-8");if(null===this.rid){const o=e.encoding;this.rid=await t.invoke("plugin:fs|read_text_file_lines",{path:i,options:null!=n?{...n,encoding:o}:void 0})}const o=await t.invoke("plugin:fs|read_text_file_lines_next",{rid:this.rid}),r=o instanceof ArrayBuffer?new Uint8Array(o):Uint8Array.from(o),a=1===r[r.byteLength-1];if(a)return this.rid=null,{value:null,done:a};return{value:e.decode(r.slice(0,r.byteLength-1)),done:a}},[Symbol.asyncIterator](){return this}})},e.remove=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|remove",{path:e instanceof URL?e.toString():e,options:n})},e.rename=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol||n instanceof URL&&"file:"!==n.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|rename",{oldPath:e instanceof URL?e.toString():e,newPath:n instanceof URL?n.toString():n,options:i})},e.size=async function(e,n){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await t.invoke("plugin:fs|size",{path:e instanceof URL?e.toString():e,options:n})},e.startAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|start_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.stat=async function(e,n){return o(await t.invoke("plugin:fs|stat",{path:e instanceof URL?e.toString():e,options:n}))},e.stopAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|stop_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.truncate=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await t.invoke("plugin:fs|truncate",{path:e instanceof URL?e.toString():e,len:n,options:i})},e.watch=async function(e,n,t){return await c(e,n,{delayMs:2e3,...t})},e.watchImmediate=async function(e,n,t){return await c(e,n,{...t,delayMs:void 0})},e.writeFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");if(n instanceof ReadableStream){const t=await a(e,{read:!1,create:!0,write:!0,...i}),o=n.getReader();try{for(;;){const{done:e,value:n}=await o.read();if(e)break;await t.write(n)}}finally{o.releaseLock(),await t.close()}}else await t.invoke("plugin:fs|write_file",n,{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e.writeTextFile=async function(e,n,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const o=new TextEncoder;await t.invoke("plugin:fs|write_text_file",o.encode(n),{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e}({},window.__TAURI__.path,window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"fs",{value:__TAURI_PLUGIN_FS__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_FS__=function(e,t,n){"use strict";var i;function o(e){return{isFile:e.isFile,isDirectory:e.isDirectory,isSymlink:e.isSymlink,size:e.size,mtime:null!==e.mtime?new Date(e.mtime):null,atime:null!==e.atime?new Date(e.atime):null,birthtime:null!==e.birthtime?new Date(e.birthtime):null,readonly:e.readonly,fileAttributes:e.fileAttributes,dev:e.dev,ino:e.ino,mode:e.mode,nlink:e.nlink,uid:e.uid,gid:e.gid,rdev:e.rdev,blksize:e.blksize,blocks:e.blocks}}e.SeekMode=void 0,(i=e.SeekMode||(e.SeekMode={}))[i.Start=0]="Start",i[i.Current=1]="Current",i[i.End=2]="End";class r extends n.Resource{async read(e){if(0===e.byteLength)return 0;const t=await n.invoke("plugin:fs|read",{rid:this.rid,len:e.byteLength}),i=function(e){const t=new Uint8ClampedArray(e),n=t.byteLength;let i=0;for(let e=0;ee instanceof URL?e.toString():e),options:i,onEvent:r}),c=new s(a);return()=>{c.close()}}return Object.defineProperty(e,"BaseDirectory",{enumerable:!0,get:function(){return t.BaseDirectory}}),e.FileHandle=r,e.copyFile=async function(e,t,i){if(e instanceof URL&&"file:"!==e.protocol||t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await n.invoke("plugin:fs|copy_file",{fromPath:e instanceof URL?e.toString():e,toPath:t instanceof URL?t.toString():t,options:i})},e.create=async function(e,t){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await n.invoke("plugin:fs|create",{path:e instanceof URL?e.toString():e,options:t});return new r(i)},e.exists=async function(e,t){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await n.invoke("plugin:fs|exists",{path:e instanceof URL?e.toString():e,options:t})},e.lstat=async function(e,t){return o(await n.invoke("plugin:fs|lstat",{path:e instanceof URL?e.toString():e,options:t}))},e.mkdir=async function(e,t){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await n.invoke("plugin:fs|mkdir",{path:e instanceof URL?e.toString():e,options:t})},e.open=a,e.readDir=async function(e,t){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await n.invoke("plugin:fs|read_dir",{path:e instanceof URL?e.toString():e,options:t})},e.readFile=async function(e,t){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await n.invoke("plugin:fs|read_file",{path:e instanceof URL?e.toString():e,options:t});return i instanceof ArrayBuffer?new Uint8Array(i):Uint8Array.from(i)},e.readTextFile=async function(e,t){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=await n.invoke("plugin:fs|read_text_file",{path:e instanceof URL?e.toString():e,options:t}),o=i instanceof ArrayBuffer?i:Uint8Array.from(i);return new TextDecoder(t?.encoding??"utf-8").decode(o)},e.readTextFileLines=async function(e,t){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const i=e instanceof URL?e.toString():e;return await Promise.resolve({path:i,rid:null,async next(){const e=new TextDecoder(t?.encoding??"utf-8");if(null===this.rid){const o=e.encoding;this.rid=await n.invoke("plugin:fs|read_text_file_lines",{path:i,options:null!=t?{...t,encoding:o}:void 0})}let o;try{o=await n.invoke("plugin:fs|read_text_file_lines_next",{rid:this.rid})}catch(e){throw this.rid=null,e}const r=o instanceof ArrayBuffer?new Uint8Array(o):Uint8Array.from(o),a=1===r[r.byteLength-1];if(a)return this.rid=null,{value:null,done:a};return{value:e.decode(r.slice(0,r.byteLength-1)),done:a}},async return(){if(null!==this.rid){const e=this.rid;this.rid=null,await new n.Resource(e).close()}return{value:null,done:!0}},[Symbol.asyncIterator](){return this}})},e.remove=async function(e,t){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await n.invoke("plugin:fs|remove",{path:e instanceof URL?e.toString():e,options:t})},e.rename=async function(e,t,i){if(e instanceof URL&&"file:"!==e.protocol||t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await n.invoke("plugin:fs|rename",{oldPath:e instanceof URL?e.toString():e,newPath:t instanceof URL?t.toString():t,options:i})},e.size=async function(e,t){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");return await n.invoke("plugin:fs|size",{path:e instanceof URL?e.toString():e,options:t})},e.startAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await n.invoke("plugin:fs|start_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.stat=async function(e,t){return o(await n.invoke("plugin:fs|stat",{path:e instanceof URL?e.toString():e,options:t}))},e.stopAccessingSecurityScopedResource=async function(e){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await n.invoke("plugin:fs|stop_accessing_security_scoped_resource",{path:e instanceof URL?e.toString():e})},e.truncate=async function(e,t,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await n.invoke("plugin:fs|truncate",{path:e instanceof URL?e.toString():e,len:t,options:i})},e.watch=async function(e,t,n){return await c(e,t,{delayMs:2e3,...n})},e.watchImmediate=async function(e,t,n){return await c(e,t,{...n,delayMs:void 0})},e.writeFile=async function(e,t,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");if(t instanceof ReadableStream){const n=await a(e,{read:!1,create:!0,write:!0,...i}),o=t.getReader();try{for(;;){const{done:e,value:t}=await o.read();if(e)break;await n.write(t)}}finally{o.releaseLock(),await n.close()}}else await n.invoke("plugin:fs|write_file",t,{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e.writeTextFile=async function(e,t,i){if(e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");const o=new TextEncoder;await n.invoke("plugin:fs|write_text_file",o.encode(t),{headers:{path:encodeURIComponent(e instanceof URL?e.toString():e),options:JSON.stringify(i)}})},e}({},window.__TAURI__.path,window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"fs",{value:__TAURI_PLUGIN_FS__})} diff --git a/plugins/fs/guest-js/index.ts b/plugins/fs/guest-js/index.ts index e3d39a754..6062c1912 100644 --- a/plugins/fs/guest-js/index.ts +++ b/plugins/fs/guest-js/index.ts @@ -887,10 +887,17 @@ async function readTextFileLines( }) } - const arr = await invoke( - 'plugin:fs|read_text_file_lines_next', - { rid: this.rid } - ) + let arr: ArrayBuffer | number[] + try { + arr = await invoke( + 'plugin:fs|read_text_file_lines_next', + { rid: this.rid } + ) + } catch (error) { + // the resource is closed on errors, the next iteration starts over + this.rid = null + throw error + } const bytes = arr instanceof ArrayBuffer ? new Uint8Array(arr) : Uint8Array.from(arr) @@ -916,6 +923,17 @@ async function readTextFileLines( } }, + // called when a `for await` loop exits early (`break`, `return` or `throw`) + async return(): Promise> { + if (this.rid !== null) { + const rid = this.rid + this.rid = null + // close the file, otherwise it stays open until the webview is destroyed + await new Resource(rid).close() + } + return { value: null, done: true } + }, + [Symbol.asyncIterator](): AsyncIterableIterator { return this } diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs index 1fb5a69d0..00cf98ad3 100644 --- a/plugins/fs/src/commands.rs +++ b/plugins/fs/src/commands.rs @@ -703,7 +703,12 @@ pub async fn read_text_file_lines_next( bytes.push(false as u8); Ok(bytes) } - Some(Err(_)) => Ok(vec![false as u8]), + Some(Err(e)) => { + // the error may be persistent (e.g. reading a directory), do not report + // an empty line and let the caller loop forever + resource_table.close(rid)?; + Err(format!("failed to read line with error: {e}").into()) + } None => { resource_table.close(rid)?; Ok(vec![true as u8]) From 1e166e93c8d0ebc9f470d17232fbbabd43ee2769 Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:55:25 +0800 Subject: [PATCH 19/29] ci: remove stronghold plugin from e2e tests (#3638) * ci: remove stronghold plugin from e2e tests * revert unrelated change --- Cargo.lock | 1 - Cargo.toml | 17 -- examples/api/package.json | 1 - examples/api/src-tauri/Cargo.toml | 1 - examples/api/src-tauri/capabilities/base.json | 4 - examples/api/src-tauri/src/lib.rs | 10 +- examples/api/src/App.svelte | 6 - examples/api/src/views/Stronghold.svelte | 71 ----- packages/api-e2e/README.md | 1 - packages/api-e2e/package.json | 1 - packages/api-e2e/test/helpers/index.ts | 2 - packages/api-e2e/test/specs/plugins.spec.ts | 1 - .../api-e2e/test/specs/stronghold.spec.ts | 248 ------------------ packages/api-e2e/wdio.mobile.ts | 25 +- pnpm-lock.yaml | 6 - 15 files changed, 3 insertions(+), 392 deletions(-) delete mode 100644 examples/api/src/views/Stronghold.svelte delete mode 100644 packages/api-e2e/test/specs/stronghold.spec.ts diff --git a/Cargo.lock b/Cargo.lock index f06cbed1d..725147b81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,7 +237,6 @@ dependencies = [ "tauri-plugin-shell", "tauri-plugin-sql", "tauri-plugin-store", - "tauri-plugin-stronghold", "tauri-plugin-updater", "tauri-plugin-upload", "tauri-plugin-websocket", diff --git a/Cargo.toml b/Cargo.toml index 23e38cbac..d297e4f71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,23 +32,6 @@ license = "Apache-2.0 OR MIT" rust-version = "1.77.2" repository = "https://github.com/tauri-apps/plugins-workspace" -# Stronghold encrypts its snapshots with scrypt: unoptimized, saving or loading one -# takes ~45s instead of ~1s, which stalls the (debug) example app and its e2e suite. -[profile.dev.package.scrypt] -opt-level = 3 -[profile.dev.package.salsa20] -opt-level = 3 -[profile.dev.package.pbkdf2] -opt-level = 3 -[profile.dev.package.sha2] -opt-level = 3 -[profile.dev.package.hmac] -opt-level = 3 -[profile.dev.package.iota-crypto] -opt-level = 3 -[profile.dev.package.rust-argon2] -opt-level = 3 - # default to small, optimized release binaries [profile.release] panic = "abort" diff --git a/examples/api/package.json b/examples/api/package.json index c788209c6..193e1f152 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -32,7 +32,6 @@ "@tauri-apps/plugin-shell": "workspace:*", "@tauri-apps/plugin-sql": "workspace:*", "@tauri-apps/plugin-store": "workspace:*", - "@tauri-apps/plugin-stronghold": "workspace:*", "@tauri-apps/plugin-updater": "workspace:*", "@tauri-apps/plugin-upload": "workspace:*", "@tauri-apps/plugin-websocket": "workspace:*", diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index db762fce2..2f0b84bb1 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -43,7 +43,6 @@ tauri-plugin-deep-link = { path = "../../../plugins/deep-link", version = "2.4.1 tauri-plugin-sql = { path = "../../../plugins/sql", version = "2.4.1", features = [ "sqlite", ] } -tauri-plugin-stronghold = { path = "../../../plugins/stronghold", version = "2.3.2" } tauri-plugin-websocket = { path = "../../../plugins/websocket", version = "2.4.3" } # WebDriver automation bridge, used by the plugins e2e suite (packages/api-e2e). diff --git a/examples/api/src-tauri/capabilities/base.json b/examples/api/src-tauri/capabilities/base.json index 90c6d976a..51bf9fd02 100644 --- a/examples/api/src-tauri/capabilities/base.json +++ b/examples/api/src-tauri/capabilities/base.json @@ -139,10 +139,6 @@ "deep-link:allow-is-registered", "sql:default", "sql:allow-execute", - "stronghold:default", - "stronghold:allow-destroy", - "stronghold:allow-remove-secret", - "stronghold:allow-remove-store-record", "websocket:default" ] } diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index 9a20067df..cb858201a 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -9,7 +9,7 @@ mod tray; use serde::Serialize; use tauri::{ webview::{PageLoadEvent, WebviewWindowBuilder}, - App, AppHandle, Emitter, Listener, Manager, RunEvent, WebviewUrl, + App, AppHandle, Emitter, Listener, RunEvent, WebviewUrl, }; #[derive(Clone, Serialize)] @@ -71,14 +71,6 @@ pub fn run() { .build(), ) .setup(move |app| { - // the argon2 salt lives next to the snapshots the frontend creates - let local_data_dir = app.path().app_local_data_dir()?; - std::fs::create_dir_all(&local_data_dir)?; - app.handle().plugin( - tauri_plugin_stronghold::Builder::with_argon2(&local_data_dir.join("salt.txt")) - .build(), - )?; - #[cfg(desktop)] { // registered before the tray, whose events it tracks diff --git a/examples/api/src/App.svelte b/examples/api/src/App.svelte index 573ff7e19..205b46f07 100644 --- a/examples/api/src/App.svelte +++ b/examples/api/src/App.svelte @@ -37,7 +37,6 @@ import DeepLink from './views/DeepLink.svelte' import Positioner from './views/Positioner.svelte' import Sql from './views/Sql.svelte' - import Stronghold from './views/Stronghold.svelte' import WebSocket from './views/WebSocket.svelte' import TitleBar from './lib/TitleBar.svelte' @@ -147,11 +146,6 @@ component: Sql, icon: 'i-ph-database' }, - { - label: 'Stronghold', - component: Stronghold, - icon: 'i-ph-lock-key' - }, { label: 'Deep link', component: DeepLink, diff --git a/examples/api/src/views/Stronghold.svelte b/examples/api/src/views/Stronghold.svelte deleted file mode 100644 index 900b2bbba..000000000 --- a/examples/api/src/views/Stronghold.svelte +++ /dev/null @@ -1,71 +0,0 @@ - - -
- {#if stronghold} -
- - - -
- {:else} -
- - -
- {/if} -
diff --git a/packages/api-e2e/README.md b/packages/api-e2e/README.md index e39fc1781..07e97920c 100644 --- a/packages/api-e2e/README.md +++ b/packages/api-e2e/README.md @@ -83,7 +83,6 @@ skipped on desktop, and the rest run everywhere with the odd test gated. | `shell` | `execute`, `spawn` with stdout/stderr/close events, stdin, `kill`, scope enforcement. | Scope only on iOS, which cannot spawn a process at all. | | `sql` | SQLite `load`/`execute`/`select`/`close`, bound values, column types, app-registered migrations, error paths. | Same. | | `store` | CRUD, persistence, auto-save, defaults/reset, reload, `getStore`, `LazyStore`, change events. | Same. | -| `stronghold` | Store records (persisted across reloads), wrong-password and unknown-client errors, vault secrets, BIP39/SLIP10 derivation and Ed25519 signing. | Same. | | `updater` | `check` against the fixture manifest (update / 204 / older release). Installing is never exercised. | Skipped — desktop-only plugin. | | `upload` | `download` and `upload` with progress, methods, headers and error paths. | Same (through `adb reverse` on Android). | | `websocket` | Text/binary echo, ping/pong, handshake headers, listener removal, server and client close, connection and argument errors, against the fixture server. | Same (through `adb reverse` on Android). | diff --git a/packages/api-e2e/package.json b/packages/api-e2e/package.json index b8226b538..b4ce4b2e7 100644 --- a/packages/api-e2e/package.json +++ b/packages/api-e2e/package.json @@ -38,7 +38,6 @@ "@tauri-apps/plugin-shell": "workspace:*", "@tauri-apps/plugin-sql": "workspace:*", "@tauri-apps/plugin-store": "workspace:*", - "@tauri-apps/plugin-stronghold": "workspace:*", "@tauri-apps/plugin-updater": "workspace:*", "@tauri-apps/plugin-upload": "workspace:*", "@tauri-apps/plugin-websocket": "workspace:*", diff --git a/packages/api-e2e/test/helpers/index.ts b/packages/api-e2e/test/helpers/index.ts index 6da724c30..f22992ab7 100644 --- a/packages/api-e2e/test/helpers/index.ts +++ b/packages/api-e2e/test/helpers/index.ts @@ -26,7 +26,6 @@ import type * as Process from '@tauri-apps/plugin-process' import type * as Shell from '@tauri-apps/plugin-shell' import type * as Sql from '@tauri-apps/plugin-sql' import type * as Store from '@tauri-apps/plugin-store' -import type * as Stronghold from '@tauri-apps/plugin-stronghold' import type * as Updater from '@tauri-apps/plugin-updater' import type * as Upload from '@tauri-apps/plugin-upload' import type * as WebSocket from '@tauri-apps/plugin-websocket' @@ -52,7 +51,6 @@ export interface CommonPluginApi { shell: typeof Shell sql: typeof Sql.default store: typeof Store - stronghold: typeof Stronghold upload: typeof Upload websocket: typeof WebSocket.default } diff --git a/packages/api-e2e/test/specs/plugins.spec.ts b/packages/api-e2e/test/specs/plugins.spec.ts index 7fdc00997..70b7bc0bc 100644 --- a/packages/api-e2e/test/specs/plugins.spec.ts +++ b/packages/api-e2e/test/specs/plugins.spec.ts @@ -108,7 +108,6 @@ const commonSurface: Surface = { // the `Database` class itself sql: ['load', 'get'], store: ['load', 'getStore', 'LazyStore', 'Store'], - stronghold: ['Location', 'Client', 'Store', 'Vault', 'Stronghold'], upload: ['download', 'upload', 'HttpMethod'], // the `WebSocket` class itself websocket: ['connect'] diff --git a/packages/api-e2e/test/specs/stronghold.spec.ts b/packages/api-e2e/test/specs/stronghold.spec.ts deleted file mode 100644 index 4a4960125..000000000 --- a/packages/api-e2e/test/specs/stronghold.spec.ts +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -import { expect } from '@wdio/globals' -import { - tauri, - tauriError, - describePlugin, - scratchDir -} from '../helpers/index.js' - -// The example derives the snapshot key from the password with argon2. Each -// test works on its own snapshot file under the spec's scratch directory, -// which the fs plugin cleans up; the snapshot path is resolved inside the page. - -const dir = scratchDir('stronghold') -const password = 'e2e-password' -const clientName = 'e2e-client' - -/** An arbitrary, valid BIP39 mnemonic, so derived keys are deterministic. */ -const mnemonic = - 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' - -describePlugin('stronghold', () => { - before(async () => { - await tauri(async (api, dir) => { - const baseDir = api.fs.BaseDirectory.AppData - if (await api.fs.exists(dir, { baseDir })) { - await api.fs.remove(dir, { baseDir, recursive: true }) - } - await api.fs.mkdir(dir, { baseDir, recursive: true }) - }, dir) - }) - - after(async () => { - await tauri(async (api, dir) => { - const baseDir = api.fs.BaseDirectory.AppData - if (await api.fs.exists(dir, { baseDir })) { - await api.fs.remove(dir, { baseDir, recursive: true }) - } - }, dir) - }) - - it('store records round-trip and persist in the snapshot', async () => { - const result = await tauri( - async (api, dir, password, clientName) => { - const path = await api.path.join( - await api.path.appDataDir(), - dir, - 'store.stronghold' - ) - const value = Array.from(new TextEncoder().encode('top secret')) - - const stronghold = await api.stronghold.Stronghold.load(path, password) - const client = await stronghold.createClient(clientName) - await client.getStore().insert('key', value) - const sameSession = await client.getStore().get('key') - await stronghold.save() - await stronghold.unload() - - const reopened = await api.stronghold.Stronghold.load(path, password) - const store = (await reopened.loadClient(clientName)).getStore() - const persisted = await store.get('key') - const missing = await store.get('missing') - await reopened.unload() - - return { - sameSession: sameSession && Array.from(sameSession), - persisted: persisted && new TextDecoder().decode(persisted), - missing, - fileExists: await api.fs.exists(path) - } - }, - dir, - password, - clientName - ) - expect(result.sameSession).toEqual( - Array.from(new TextEncoder().encode('top secret')) - ) - expect(result.persisted).toBe('top secret') - expect(result.missing).toBeNull() - expect(result.fileExists).toBe(true) - }) - - it('store.remove returns the removed value', async () => { - const result = await tauri( - async (api, dir, password, clientName) => { - const path = await api.path.join( - await api.path.appDataDir(), - dir, - 'remove.stronghold' - ) - const stronghold = await api.stronghold.Stronghold.load(path, password) - const store = (await stronghold.createClient(clientName)).getStore() - await store.insert('key', [1, 2, 3]) - const removed = await store.remove('key') - const after = await store.get('key') - await stronghold.unload() - return { removed: removed && Array.from(removed), after } - }, - dir, - password, - clientName - ) - expect(result).toEqual({ removed: [1, 2, 3], after: null }) - }) - - it('a snapshot cannot be opened with the wrong password', async () => { - const path = await tauri( - async (api, dir, password, clientName) => { - const path = await api.path.join( - await api.path.appDataDir(), - dir, - 'password.stronghold' - ) - const stronghold = await api.stronghold.Stronghold.load(path, password) - await stronghold.createClient(clientName) - await stronghold.unload() - return path - }, - dir, - password, - clientName - ) - const error = await tauriError( - (api, path) => api.stronghold.Stronghold.load(path, 'wrong-password'), - path - ) - expect(error).toMatch(/failed to decode\/decrypt/) - }) - - it('loading a client that was never created is rejected', async () => { - const error = await tauriError( - async (api, dir, password) => { - const path = await api.path.join( - await api.path.appDataDir(), - dir, - 'no-client.stronghold' - ) - const stronghold = await api.stronghold.Stronghold.load(path, password) - try { - await stronghold.loadClient('never-created') - } finally { - await stronghold.unload() - } - }, - dir, - password - ) - expect(error).toMatch(/error loading client data/) - }) - - it('vault procedures derive keys and sign without exposing secrets', async () => { - const result = await tauri( - async (api, dir, password, clientName, mnemonic) => { - const { Location } = api.stronghold - const path = await api.path.join( - await api.path.appDataDir(), - dir, - 'vault.stronghold' - ) - const stronghold = await api.stronghold.Stronghold.load(path, password) - const vault = (await stronghold.createClient(clientName)).getVault( - 'vault' - ) - - // the same mnemonic recovered twice derives the same key - const seedA = Location.generic('vault', 'seed-a') - const seedB = Location.generic('vault', 'seed-b') - await vault.recoverBIP39(mnemonic, seedA) - await vault.recoverBIP39(mnemonic, seedB) - // Ed25519 SLIP-10 only derives hardened indices (the high bit set). - // (No named helper: the transpiler would wrap it in a `__name` call - // that does not exist in the page.) - const chain = [44, 4218, 0, 0, 0].map((i) => (i | 0x80000000) >>> 0) - const otherChain = [44, 4218, 0, 0, 1].map( - (i) => (i | 0x80000000) >>> 0 - ) - const keyA = Location.generic('vault', 'key-a') - const keyB = Location.generic('vault', 'key-b') - const keyOther = Location.generic('vault', 'key-other') - await vault.deriveSLIP10(chain, 'Seed', seedA, keyA) - await vault.deriveSLIP10(chain, 'Seed', seedB, keyB) - await vault.deriveSLIP10(otherChain, 'Seed', seedA, keyOther) - - const publicA = Array.from(await vault.getEd25519PublicKey(keyA)) - const publicB = Array.from(await vault.getEd25519PublicKey(keyB)) - const publicOther = Array.from( - await vault.getEd25519PublicKey(keyOther) - ) - const signature = Array.from(await vault.signEd25519(keyA, 'message')) - const signatureAgain = Array.from( - await vault.signEd25519(keyA, 'message') - ) - - // a random seed and a raw secret can be stored and removed - const random = Location.generic('vault', 'random') - await vault.generateSLIP10Seed(random) - await vault.insert('raw', [9, 9, 9]) - await vault.remove(Location.generic('vault', 'raw')) - - // a generated mnemonic recovers the seed generateBIP39 stored - const generatedSeed = Location.generic('vault', 'generated') - const mnemonicBytes = await vault.generateBIP39(generatedSeed) - const recoveredSeed = Location.generic('vault', 'recovered') - await vault.recoverBIP39( - new TextDecoder().decode(mnemonicBytes), - recoveredSeed - ) - const generatedKey = Location.generic('vault', 'generated-key') - const recoveredKey = Location.generic('vault', 'recovered-key') - await vault.deriveSLIP10(chain, 'Seed', generatedSeed, generatedKey) - await vault.deriveSLIP10(chain, 'Seed', recoveredSeed, recoveredKey) - const generatedPublic = Array.from( - await vault.getEd25519PublicKey(generatedKey) - ) - const recoveredPublic = Array.from( - await vault.getEd25519PublicKey(recoveredKey) - ) - - await stronghold.unload() - return { - publicA, - publicB, - publicOther, - signature, - signatureAgain, - generatedPublic, - recoveredPublic - } - }, - dir, - password, - clientName, - mnemonic - ) - expect(result.publicA).toHaveLength(32) - expect(result.publicB).toEqual(result.publicA) - expect(result.publicOther).not.toEqual(result.publicA) - // Ed25519 signatures are 64 bytes and deterministic - expect(result.signature).toHaveLength(64) - expect(result.signatureAgain).toEqual(result.signature) - expect(result.recoveredPublic).toEqual(result.generatedPublic) - expect(result.generatedPublic).not.toEqual(result.publicA) - }) -}) diff --git a/packages/api-e2e/wdio.mobile.ts b/packages/api-e2e/wdio.mobile.ts index 815802ac8..4622ccfa6 100644 --- a/packages/api-e2e/wdio.mobile.ts +++ b/packages/api-e2e/wdio.mobile.ts @@ -303,33 +303,12 @@ function tauriCli(args: string[], env: NodeJS.ProcessEnv = {}): void { * Environment for the Android build. * * - No debug info: the suite needs a debug build (webview debugging follows - * `debug_assertions`), but with the stronghold, sql and websocket plugins the + * `debug_assertions`), but with the sql and websocket plugins the * debug info alone grows the APK past what a default emulator can install * ("not enough space"). - * - On a macOS host, `AR`/`RANLIB` point at the NDK's LLVM tools. Autotools-built - * C dependencies (libsodium, through the stronghold plugin) otherwise fall back - * to Apple's `ar`/`ranlib`, which silently produce an empty archive from the - * Android (ELF) objects, and the app then fails to load its library with an - * unresolved symbol. Linux hosts' GNU `ar` is fine. */ function androidBuildEnv(): NodeJS.ProcessEnv { - const env: NodeJS.ProcessEnv = { CARGO_PROFILE_DEV_DEBUG: '0' } - const ndk = process.env.NDK_HOME ?? process.env.ANDROID_NDK_HOME - if (process.platform !== 'darwin' || !ndk) return env - // the NDK only ships an x86_64 (Rosetta-compatible) macOS toolchain - const bin = path.join( - ndk, - 'toolchains', - 'llvm', - 'prebuilt', - 'darwin-x86_64', - 'bin' - ) - return { - ...env, - AR: path.join(bin, 'llvm-ar'), - RANLIB: path.join(bin, 'llvm-ranlib') - } + return { CARGO_PROFILE_DEV_DEBUG: '0' } } /** `adb` from the Android SDK, else whatever is on `PATH`. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f445e0f2c..224f427b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,9 +219,6 @@ importers: '@tauri-apps/plugin-store': specifier: workspace:* version: link:../../plugins/store - '@tauri-apps/plugin-stronghold': - specifier: workspace:* - version: link:../../plugins/stronghold '@tauri-apps/plugin-updater': specifier: workspace:* version: link:../../plugins/updater @@ -337,9 +334,6 @@ importers: '@tauri-apps/plugin-store': specifier: workspace:* version: link:../../plugins/store - '@tauri-apps/plugin-stronghold': - specifier: workspace:* - version: link:../../plugins/stronghold '@tauri-apps/plugin-updater': specifier: workspace:* version: link:../../plugins/updater From b870626a5ea0153e54705554404e0342e7ea1c18 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Fri, 25 Sep 2026 13:27:31 -0300 Subject: [PATCH 20/29] feat(ci): enforce e2e tests pass before publishing --- .../workflows/covector-version-or-publish.yml | 38 +++++++++++++++++++ .github/workflows/integration-tests.yml | 2 + .github/workflows/test-api-e2e-mobile.yml | 6 ++- .github/workflows/test-api-e2e.yml | 6 ++- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/.github/workflows/covector-version-or-publish.yml b/.github/workflows/covector-version-or-publish.yml index ca563b52d..0e58b55b7 100644 --- a/.github/workflows/covector-version-or-publish.yml +++ b/.github/workflows/covector-version-or-publish.yml @@ -19,7 +19,45 @@ permissions: pull-requests: write jobs: + # covector publishes once no change files are left (the version update PR consumed them) and + # otherwise opens that PR, which does not need the tests below. + check-publish: + runs-on: ubuntu-latest + outputs: + publish: ${{ steps.check.outputs.publish }} + steps: + - uses: actions/checkout@v7 + - id: check + run: | + if find .changes -maxdepth 1 -name '*.md' ! -iname 'readme.md' | grep -q .; then + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + + integration-tests: + needs: check-publish + if: needs.check-publish.outputs.publish == 'true' + uses: ./.github/workflows/integration-tests.yml + + test-api-e2e: + needs: check-publish + if: needs.check-publish.outputs.publish == 'true' + uses: ./.github/workflows/test-api-e2e.yml + secrets: inherit + + test-api-e2e-mobile: + needs: check-publish + if: needs.check-publish.outputs.publish == 'true' + uses: ./.github/workflows/test-api-e2e-mobile.yml + version-or-publish: + needs: + - integration-tests + - test-api-e2e + - test-api-e2e-mobile + # the tests are skipped when covector only versions + if: ${{ !cancelled() && !failure() }} runs-on: ubuntu-latest timeout-minutes: 65 outputs: diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 27993d2fc..7408c9997 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -6,6 +6,8 @@ name: integration tests on: workflow_dispatch: + # run before publishing, see covector-version-or-publish.yml + workflow_call: push: branches: - v1 diff --git a/.github/workflows/test-api-e2e-mobile.yml b/.github/workflows/test-api-e2e-mobile.yml index c73cb89c6..7a97a15be 100644 --- a/.github/workflows/test-api-e2e-mobile.yml +++ b/.github/workflows/test-api-e2e-mobile.yml @@ -6,6 +6,8 @@ name: test plugins e2e (mobile) on: workflow_dispatch: + # run before publishing, see covector-version-or-publish.yml + workflow_call: push: branches: - v2 @@ -38,7 +40,9 @@ env: CARGO_PROFILE_DEV_DEBUG: 0 # keeps the target folder small for better cache efficiency concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + # `github.workflow` is the caller's name in a `workflow_call` run, so the suffix keeps the + # desktop and mobile suites from sharing (and cancelling) one group there. + group: ${{ github.workflow }}-api-e2e-mobile-${{ github.ref }} cancel-in-progress: true permissions: diff --git a/.github/workflows/test-api-e2e.yml b/.github/workflows/test-api-e2e.yml index 3c18ad54b..248997345 100644 --- a/.github/workflows/test-api-e2e.yml +++ b/.github/workflows/test-api-e2e.yml @@ -6,6 +6,8 @@ name: test plugins e2e on: workflow_dispatch: + # run before publishing, see covector-version-or-publish.yml + workflow_call: push: branches: - v2 @@ -35,7 +37,9 @@ env: MSEDGEDRIVER_TOOL_REV: 8c4b34f51b45f5cf08013366d703de464ab871d1 concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + # `github.workflow` is the caller's name in a `workflow_call` run, so the suffix keeps the + # desktop and mobile suites from sharing (and cancelling) one group there. + group: ${{ github.workflow }}-api-e2e-${{ github.ref }} cancel-in-progress: true permissions: From 3a01943572ba0d00909857a62588d06159e709ba Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Fri, 25 Sep 2026 13:43:07 -0300 Subject: [PATCH 21/29] tests(e2e): fix e2e failures on Linux and Windows (#3642) --- packages/api-e2e/test/helpers/server.ts | 13 ++++++++----- packages/api-e2e/test/specs/opener.spec.ts | 3 ++- packages/api-e2e/test/specs/positioner.spec.ts | 16 ++++++++++++---- packages/api-e2e/test/specs/websocket.spec.ts | 1 + 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/api-e2e/test/helpers/server.ts b/packages/api-e2e/test/helpers/server.ts index 204f74107..230d92df3 100644 --- a/packages/api-e2e/test/helpers/server.ts +++ b/packages/api-e2e/test/helpers/server.ts @@ -49,7 +49,9 @@ export interface FixtureServer { * - `ws /ws` — a WebSocket echo endpoint: text and binary messages are sent * back as-is, and {@link WEBSOCKET_CLOSE_REQUEST} makes the server close the * connection with {@link WEBSOCKET_CLOSE_CODE}. On `/ws/headers` the server - * first sends the upgrade request's headers as a JSON text message. + * answers the first message with the upgrade request's headers as a JSON + * text message instead (not sent on connect, where it could arrive before + * the client attached a listener). */ export function startFixtureServer(): Promise { const server = http.createServer((req, res) => { @@ -111,11 +113,12 @@ export function startFixtureServer(): Promise { return } wss.handleUpgrade(req, socket, head, (ws) => { - if (pathname === '/ws/headers') { - ws.send(JSON.stringify(req.headers)) - } + let sendHeaders = pathname === '/ws/headers' ws.on('message', (data, isBinary) => { - if ( + if (sendHeaders) { + sendHeaders = false + ws.send(JSON.stringify(req.headers)) + } else if ( !isBinary && Buffer.isBuffer(data) && data.toString('utf8') === WEBSOCKET_CLOSE_REQUEST diff --git a/packages/api-e2e/test/specs/opener.spec.ts b/packages/api-e2e/test/specs/opener.spec.ts index d5e1b733c..6e2265eaa 100644 --- a/packages/api-e2e/test/specs/opener.spec.ts +++ b/packages/api-e2e/test/specs/opener.spec.ts @@ -41,6 +41,7 @@ describePlugin('opener', () => { await api.path.join(await api.path.appDataDir(), 'does-not-exist-e2e') ) ) - expect(message).toMatch(/os error 2/) + // the `std::io` NotFound message on Unix, the shell path lookup's on Windows + expect(message).toMatch(/os error 2|path doesn't exist/) }) }) diff --git a/packages/api-e2e/test/specs/positioner.spec.ts b/packages/api-e2e/test/specs/positioner.spec.ts index f2cfadf9f..d0e7986d3 100644 --- a/packages/api-e2e/test/specs/positioner.spec.ts +++ b/packages/api-e2e/test/specs/positioner.spec.ts @@ -101,13 +101,21 @@ describePlugin('positioner', { desktopOnly: true }, () => { ) itWm('tray positions are relative to the reported tray rect', async () => { - // a tray "icon" in the middle of the window's monitor, far from any edge, - // so neither the OS-specific flips nor the constraint kick in + // The example's 1000x800 window barely fits the CI screens (1024x768 on + // Windows, 1280x1024 on Xvfb), so wherever the tray "icon" is it sticks out + // of the monitor and `moveWindowConstrained` clamps it back on both axes. + // The window is shrunk and the tray placed near the monitor's top left + // corner, far enough from the edges that both positions keep the window on + // screen and neither the OS-specific flips nor the constraint kick in. const tray = await tauri(async (api) => { + const win = api.window.getCurrentWindow() + await win.setSize(new api.dpi.LogicalSize(600, 400)) + await new Promise((resolve) => setTimeout(resolve, 500)) const monitor = await api.window.currentMonitor() + const size = await win.outerSize() const rect = { - x: monitor!.position.x + Math.trunc(monitor!.size.width / 2), - y: monitor!.position.y + Math.trunc(monitor!.size.height / 2), + x: monitor!.position.x + Math.trunc(size.width / 2) + 50, + y: monitor!.position.y + 50, width: 20, height: 20 } diff --git a/packages/api-e2e/test/specs/websocket.spec.ts b/packages/api-e2e/test/specs/websocket.spec.ts index cb01f55a7..474ad4ee0 100644 --- a/packages/api-e2e/test/specs/websocket.spec.ts +++ b/packages/api-e2e/test/specs/websocket.spec.ts @@ -73,6 +73,7 @@ describePlugin('websocket', () => { const message = await new Promise((resolve, reject) => { setTimeout(() => reject(new Error('headers not received')), 5000) ws.addListener(resolve) + ws.send('headers').catch(reject) }) await ws.disconnect() return JSON.parse(message.data as string) as Record From 9b29b601f387ded1f4adb4386623a3c035a5598c Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sat, 26 Sep 2026 14:46:48 -0300 Subject: [PATCH 22/29] feat: update to tauri 2.12 - (MSRV bump - Android compiler options update - notification plugin windows7 feature deprecation --- .changes/android-kotlin-compiler-options.md | 16 + .changes/msrv-1.90.md | 61 ++ .../notification-deprecate-windows7-compat.md | 5 + .github/workflows/test-android.yml | 2 +- .github/workflows/test-rust.yml | 2 +- Cargo.lock | 777 ++++++++++++------ Cargo.toml | 8 +- README.md | 2 +- examples/api/package.json | 4 +- examples/api/src-tauri/Cargo.toml | 4 +- packages/api-e2e/package.json | 2 +- plugins/autostart/README.md | 2 +- plugins/autostart/package.json | 2 +- .../barcode-scanner/android/build.gradle.kts | 9 +- plugins/barcode-scanner/package.json | 2 +- plugins/biometric/Cargo.toml | 1 + plugins/biometric/android/build.gradle.kts | 9 +- plugins/biometric/package.json | 2 +- plugins/cli/README.md | 2 +- plugins/cli/package.json | 2 +- plugins/clipboard-manager/README.md | 2 +- .../android/build.gradle.kts | 9 +- plugins/clipboard-manager/package.json | 2 +- plugins/deep-link/README.md | 2 +- plugins/deep-link/android/build.gradle.kts | 9 +- plugins/deep-link/examples/app/package.json | 4 +- .../examples/app/src-tauri/Cargo.toml | 2 +- plugins/deep-link/package.json | 4 +- plugins/dialog/README.md | 2 +- plugins/dialog/android/build.gradle.kts | 9 +- plugins/dialog/package.json | 2 +- plugins/fs/README.md | 2 +- plugins/fs/android/build.gradle.kts | 9 +- plugins/fs/package.json | 2 +- plugins/geolocation/Cargo.toml | 1 + plugins/geolocation/README.md | 2 +- plugins/geolocation/android/build.gradle.kts | 9 +- plugins/geolocation/package.json | 2 +- plugins/global-shortcut/README.md | 2 +- plugins/global-shortcut/package.json | 2 +- plugins/haptics/Cargo.toml | 1 + plugins/haptics/README.md | 2 +- plugins/haptics/android/build.gradle.kts | 9 +- plugins/haptics/package.json | 2 +- plugins/http/README.md | 2 +- plugins/http/package.json | 2 +- plugins/localhost/README.md | 2 +- plugins/log/README.md | 2 +- plugins/log/package.json | 2 +- plugins/nfc/Cargo.toml | 1 + plugins/nfc/android/build.gradle.kts | 9 +- plugins/nfc/package.json | 2 +- plugins/notification/Cargo.toml | 10 +- plugins/notification/README.md | 2 +- plugins/notification/android/build.gradle.kts | 9 +- plugins/notification/package.json | 2 +- plugins/notification/src/desktop.rs | 83 +- plugins/notification/src/lib.rs | 2 +- plugins/opener/Cargo.toml | 1 + plugins/opener/README.md | 2 +- plugins/opener/android/build.gradle.kts | 9 +- plugins/opener/package.json | 2 +- plugins/os/README.md | 2 +- plugins/os/package.json | 2 +- plugins/persisted-scope/README.md | 2 +- plugins/positioner/README.md | 2 +- plugins/positioner/package.json | 2 +- plugins/process/README.md | 2 +- plugins/process/package.json | 2 +- plugins/shell/README.md | 2 +- plugins/shell/android/build.gradle.kts | 9 +- plugins/shell/package.json | 2 +- plugins/single-instance/README.md | 2 +- .../examples/vanilla/package.json | 2 +- .../examples/vanilla/src-tauri/Cargo.toml | 2 +- plugins/sql/README.md | 2 +- plugins/sql/package.json | 2 +- plugins/store/README.md | 2 +- .../examples/AppSettingsManager/package.json | 2 +- plugins/store/package.json | 2 +- plugins/stronghold/README.md | 2 +- plugins/stronghold/package.json | 2 +- plugins/updater/README.md | 2 +- plugins/updater/package.json | 2 +- plugins/upload/README.md | 2 +- plugins/upload/package.json | 2 +- plugins/websocket/README.md | 2 +- .../websocket/examples/tauri-app/package.json | 2 +- plugins/websocket/package.json | 2 +- plugins/window-state/README.md | 2 +- plugins/window-state/package.json | 2 +- pnpm-lock.yaml | 244 +++--- pnpm-workspace.yaml | 15 + shared/template/README.md | 2 +- 94 files changed, 939 insertions(+), 537 deletions(-) create mode 100644 .changes/android-kotlin-compiler-options.md create mode 100644 .changes/msrv-1.90.md create mode 100644 .changes/notification-deprecate-windows7-compat.md diff --git a/.changes/android-kotlin-compiler-options.md b/.changes/android-kotlin-compiler-options.md new file mode 100644 index 000000000..a98d859a3 --- /dev/null +++ b/.changes/android-kotlin-compiler-options.md @@ -0,0 +1,16 @@ +--- +"barcode-scanner": patch +"biometric": patch +"clipboard-manager": patch +"deep-link": patch +"dialog": patch +"fs": patch +"geolocation": patch +"haptics": patch +"nfc": patch +"notification": patch +"opener": patch +"shell": patch +--- + +Migrate the Android Gradle scripts from the deprecated `kotlinOptions` DSL to `compilerOptions`, which Kotlin Gradle Plugin 2.3+ requires, to match the Tauri 2.12 Android library and templates. diff --git a/.changes/msrv-1.90.md b/.changes/msrv-1.90.md new file mode 100644 index 000000000..898b9901f --- /dev/null +++ b/.changes/msrv-1.90.md @@ -0,0 +1,61 @@ +--- +"autostart": minor +"autostart-js": minor +"barcode-scanner": minor +"barcode-scanner-js": minor +"biometric": minor +"biometric-js": minor +"cli": minor +"cli-js": minor +"clipboard-manager": minor +"clipboard-manager-js": minor +"deep-link": minor +"deep-link-js": minor +"fs": minor +"fs-js": minor +"dialog": minor +"dialog-js": minor +"geolocation": minor +"geolocation-js": minor +"global-shortcut": minor +"global-shortcut-js": minor +"opener": minor +"opener-js": minor +"haptics": minor +"haptics-js": minor +"http": minor +"http-js": minor +"localhost": minor +"log": minor +"log-js": minor +"nfc": minor +"nfc-js": minor +"notification": minor +"notification-js": minor +"os": minor +"os-js": minor +"persisted-scope": minor +"positioner": minor +"positioner-js": minor +"process": minor +"process-js": minor +"shell": minor +"shell-js": minor +"single-instance": minor +"sql": minor +"sql-js": minor +"store": minor +"store-js": minor +"stronghold": minor +"stronghold-js": minor +"updater": minor +"updater-js": minor +"upload": minor +"upload-js": minor +"websocket": minor +"websocket-js": minor +"window-state": minor +"window-state-js": minor +--- + +Update MSRV to 1.90 to match tauri. diff --git a/.changes/notification-deprecate-windows7-compat.md b/.changes/notification-deprecate-windows7-compat.md new file mode 100644 index 000000000..92e9f22de --- /dev/null +++ b/.changes/notification-deprecate-windows7-compat.md @@ -0,0 +1,5 @@ +--- +"notification": minor +--- + +Deprecate the `windows7-compat` Cargo feature, which is now a no-op since Tauri 2.12 no longer supports Windows 7. `NotificationBuilder::notify` is deprecated in favor of `NotificationBuilder::show`, and the `win7-notifications` and `windows-version` dependencies were removed. diff --git a/.github/workflows/test-android.yml b/.github/workflows/test-android.yml index 45fdc085c..023d003bf 100644 --- a/.github/workflows/test-android.yml +++ b/.github/workflows/test-android.yml @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@1.77.2 + - uses: dtolnay/rust-toolchain@1.90 - uses: actions/setup-node@v7 with: diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 7fda7d355..6c942198a 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -257,7 +257,7 @@ jobs: sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev - - uses: dtolnay/rust-toolchain@1.77.2 + - uses: dtolnay/rust-toolchain@1.90 with: targets: ${{ matrix.platform.target }} diff --git a/Cargo.lock b/Cargo.lock index 725147b81..0438156a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -102,13 +102,28 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + [[package]] name = "alloc-stdlib" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ - "alloc-no-stdlib", + "alloc-no-stdlib 2.0.4", +] + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib 3.0.0", ] [[package]] @@ -390,7 +405,7 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40f6024f3f856663b45fd0c9b6f2024034a702f453549449e0d84a305900dad4" dependencies = [ - "brotli", + "brotli 8.0.1", "flate2", "futures-core", "memchr", @@ -757,9 +772,20 @@ version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", + "alloc-no-stdlib 2.0.4", + "alloc-stdlib 0.2.2", + "brotli-decompressor 5.0.0", +] + +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib 3.0.0", + "alloc-stdlib 0.3.0", + "brotli-decompressor 6.0.1", ] [[package]] @@ -768,8 +794,18 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", + "alloc-no-stdlib 2.0.4", + "alloc-stdlib 0.2.2", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" +dependencies = [ + "alloc-no-stdlib 3.0.0", + "alloc-stdlib 0.3.0", ] [[package]] @@ -886,12 +922,13 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.22.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" dependencies = [ + "semver", "serde", - "toml 0.8.20", + "toml 1.1.6+spec-1.1.0", ] [[package]] @@ -922,6 +959,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + [[package]] name = "cfg-expr" version = "0.15.8" @@ -1296,9 +1344,9 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" dependencies = [ "cssparser-macros", "dtoa-short", @@ -1309,29 +1357,19 @@ dependencies = [ [[package]] name = "cssparser-macros" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" dependencies = [ "quote", - "syn 2.0.100", + "syn 3.0.6", ] [[package]] name = "ctor" -version = "0.8.0" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" -dependencies = [ - "ctor-proc-macro", - "dtor", -] - -[[package]] -name = "ctor-proc-macro" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" [[package]] name = "ctr" @@ -1529,6 +1567,15 @@ dependencies = [ "dirs-sys 0.5.0", ] +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1648,9 +1695,9 @@ dependencies = [ [[package]] name = "dom_query" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" dependencies = [ "bit-set", "cssparser", @@ -1697,21 +1744,6 @@ dependencies = [ "dtoa", ] -[[package]] -name = "dtor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" -dependencies = [ - "dtor-proc-macro", -] - -[[package]] -name = "dtor-proc-macro" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" - [[package]] name = "dunce" version = "1.0.5" @@ -2081,9 +2113,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2481,7 +2513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e" dependencies = [ "crossbeam-channel", - "keyboard-types", + "keyboard-types 0.7.0", "objc2 0.6.4", "objc2-app-kit", "once_cell", @@ -2578,7 +2610,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.14.2", "slab", "tokio", "tokio-util", @@ -2615,6 +2647,12 @@ dependencies = [ "foldhash 0.1.5", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "hashlink" version = "0.10.0" @@ -2677,9 +2715,9 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" dependencies = [ "log", "markup5ever", @@ -2833,7 +2871,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.0", ] [[package]] @@ -2862,9 +2900,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", - "yoke", + "yoke 0.7.5", "zerofrom", - "zerovec", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke 0.8.1", + "zerofrom", + "zerovec 0.11.5", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap 0.8.3", + "tinystr 0.8.2", + "writeable 0.6.4", + "zerovec 0.11.5", ] [[package]] @@ -2874,10 +2938,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "litemap 0.7.4", + "tinystr 0.7.6", + "writeable 0.5.5", + "zerovec 0.10.4", ] [[package]] @@ -2889,9 +2953,9 @@ dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", + "icu_provider 1.5.0", + "tinystr 0.7.6", + "zerovec 0.10.4", ] [[package]] @@ -2907,15 +2971,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", - "icu_collections", + "icu_collections 1.5.0", "icu_normalizer_data", - "icu_properties", - "icu_provider", + "icu_properties 1.5.1", + "icu_provider 1.5.0", "smallvec", "utf16_iter", "utf8_iter", "write16", - "zerovec", + "zerovec 0.10.4", ] [[package]] @@ -2931,12 +2995,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", - "icu_collections", + "icu_collections 1.5.0", "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", + "icu_properties_data 1.5.1", + "icu_provider 1.5.0", + "tinystr 0.7.6", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections 2.1.1", + "icu_locale_core", + "icu_properties_data 2.1.2", + "icu_provider 2.1.1", + "zerotrie", + "zerovec 0.11.5", ] [[package]] @@ -2945,6 +3023,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + [[package]] name = "icu_provider" version = "1.5.0" @@ -2955,11 +3039,26 @@ dependencies = [ "icu_locid", "icu_provider_macros", "stable_deref_trait", - "tinystr", - "writeable", - "yoke", + "tinystr 0.7.6", + "writeable 0.5.5", + "yoke 0.7.5", "zerofrom", - "zerovec", + "zerovec 0.10.4", +] + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable 0.6.4", + "yoke 0.8.1", + "zerofrom", + "zerotrie", + "zerovec 0.11.5", ] [[package]] @@ -2981,9 +3080,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2997,7 +3096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", - "icu_properties", + "icu_properties 1.5.1", ] [[package]] @@ -3026,12 +3125,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3042,7 +3141,16 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" dependencies = [ - "cfb", + "cfb 0.7.3", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb 0.14.0", ] [[package]] @@ -3253,21 +3361,21 @@ dependencies = [ [[package]] name = "json-patch" -version = "3.0.1" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" dependencies = [ "jsonptr", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "jsonptr" -version = "0.6.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" dependencies = [ "serde", "serde_json", @@ -3297,6 +3405,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.9.0", + "serde", +] + [[package]] name = "kqueue" version = "1.1.1" @@ -3472,6 +3590,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "litrs" version = "0.4.1" @@ -3520,9 +3644,9 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" dependencies = [ "log", "tendril", @@ -3639,14 +3763,14 @@ dependencies = [ [[package]] name = "muda" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" dependencies = [ "crossbeam-channel", "dpi", "gtk", - "keyboard-types", + "keyboard-types 0.8.3", "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", @@ -3690,6 +3814,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -3982,9 +4112,9 @@ dependencies = [ [[package]] name = "objc2-core-location" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d31f4c5b5192304996badc466aeadffe1411d73a9bbd3b18b6b2ee9d048b07bd" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ "objc2 0.6.4", "objc2-foundation 0.3.2", @@ -4117,9 +4247,9 @@ dependencies = [ [[package]] name = "objc2-ui-kit" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.9.0", "block2 0.6.2", @@ -4130,6 +4260,7 @@ dependencies = [ "objc2-core-graphics", "objc2-core-image", "objc2-core-location", + "objc2-core-text", "objc2-foundation 0.3.2", "objc2-quartz-core 0.3.2", "objc2-user-notifications", @@ -4137,9 +4268,9 @@ dependencies = [ [[package]] name = "objc2-user-notifications" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670fe793adbf3b5e93686d48a05a7ed7ee53dfa65d106ced4805fae8969059b2" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ "objc2 0.6.4", "objc2-foundation 0.3.2", @@ -4147,9 +4278,9 @@ dependencies = [ [[package]] name = "objc2-web-kit" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ "bitflags 2.9.0", "block2 0.6.2", @@ -4394,9 +4525,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" @@ -4405,7 +4536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.11.4", + "indexmap 2.14.2", ] [[package]] @@ -4518,7 +4649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64 0.22.1", - "indexmap 2.11.4", + "indexmap 2.14.2", "quick-xml 0.32.0", "serde", "time", @@ -4594,6 +4725,15 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec 0.11.5", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -4793,9 +4933,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -5488,9 +5628,9 @@ dependencies = [ [[package]] name = "selectors" -version = "0.36.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" dependencies = [ "bitflags 2.9.0", "cssparser", @@ -5507,11 +5647,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -5600,9 +5741,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -5629,7 +5770,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.14.2", "serde", "serde_derive", "serde_json", @@ -5922,7 +6063,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.2", "hashlink", - "indexmap 2.11.4", + "indexmap 2.14.2", "log", "memchr", "once_cell", @@ -6212,9 +6353,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "swift-rs" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" dependencies = [ "base64 0.21.7", "serde", @@ -6243,6 +6384,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6263,6 +6415,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "sys-locale" version = "0.3.2" @@ -6308,9 +6471,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.35.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159" +checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" dependencies = [ "bitflags 2.9.0", "block2 0.6.2", @@ -6328,29 +6491,29 @@ dependencies = [ "libc", "log", "ndk", + "ndk-context", "ndk-sys", "objc2 0.6.4", "objc2-app-kit", "objc2-foundation 0.3.2", "objc2-ui-kit", - "once_cell", "parking_lot", "percent-encoding", "raw-window-handle", "tao-macros", "unicode-segmentation", "url", - "windows 0.61.1", - "windows-core", + "windows 0.62.2", + "windows-core 0.62.2", "windows-version", "x11-dl", ] [[package]] name = "tao-macros" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" dependencies = [ "proc-macro2", "quote", @@ -6382,14 +6545,14 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.5" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +checksum = "cb20ef8e6f97d6fe03039666cd82861f09a38b56291fa2f0f9fe140023f61c1a" dependencies = [ "anyhow", "bytes", "cookie", - "dirs 6.0.0", + "dirs 7.0.0", "dunce", "embed_plist", "getrandom 0.3.2", @@ -6432,18 +6595,18 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows 0.61.1", + "windows 0.62.2", ] [[package]] name = "tauri-build" -version = "2.6.3" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +checksum = "5b5ae674f48836f5dd2eeaf9e221c095bd3e78a8d0439c77ca93b48740a1b644" dependencies = [ "anyhow", "cargo_toml", - "dirs 6.0.0", + "dirs 7.0.0", "glob", "heck 0.5.0", "json-patch", @@ -6460,16 +6623,16 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.3" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +checksum = "3926b73ac01df7c14f19ff872cd56e67213a87acc0000ea37ddb300f3d11a3e3" dependencies = [ "base64 0.22.1", - "brotli", + "brotli 9.0.0", "ico", "json-patch", "plist", - "png 0.17.16", + "png 0.18.1", "proc-macro2", "quote", "semver", @@ -6487,9 +6650,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.3" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +checksum = "d21d0fa2c0529972ea6ecaf7404fcf40efac098eda0beb06ca682f41e33a6aa5" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -6501,9 +6664,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" +checksum = "c9ff3ebb9fda56ceb93d46f6cbb126bae82951bf43498543a5163dde5def49a1" dependencies = [ "anyhow", "glob", @@ -6610,7 +6773,7 @@ dependencies = [ "tracing", "url", "windows-registry", - "windows-result", + "windows-result 0.3.2", ] [[package]] @@ -6649,7 +6812,7 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.12", - "toml 1.0.7+spec-1.1.0", + "toml 1.1.6+spec-1.1.0", "url", ] @@ -6712,7 +6875,7 @@ dependencies = [ "tokio", "tracing", "url", - "urlpattern", + "urlpattern 0.3.0", ] [[package]] @@ -6776,8 +6939,6 @@ dependencies = [ "thiserror 2.0.12", "time", "url", - "win7-notifications", - "windows-version", ] [[package]] @@ -6891,7 +7052,7 @@ name = "tauri-plugin-sql" version = "2.4.1" dependencies = [ "futures-core", - "indexmap 2.11.4", + "indexmap 2.14.2", "log", "rust_decimal", "serde", @@ -6949,7 +7110,7 @@ dependencies = [ "flate2", "futures-util", "http", - "infer", + "infer 0.19.0", "log", "minisign-verify", "osakit", @@ -7022,9 +7183,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.11.3" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +checksum = "c9e6d5ef76985b32ff012a43b58df205d25a285e400ca878afcacddc5d5f3238" dependencies = [ "cookie", "dpi", @@ -7042,14 +7203,14 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.1", + "windows 0.62.2", ] [[package]] name = "tauri-runtime-wry" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +checksum = "fa829eb69860cf1fb0bedb21d2d5f1effce3527d054a68ca1cd7355c654400a3" dependencies = [ "gtk", "http", @@ -7067,19 +7228,19 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows 0.61.1", + "windows 0.62.2", "wry", ] [[package]] name = "tauri-utils" -version = "2.9.3" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +checksum = "4dd257082deff79550de3409ed146104aeaf86fe38649615b04a304144939677" dependencies = [ "aes-gcm", "anyhow", - "brotli", + "brotli 9.0.0", "cargo_metadata", "ctor", "dom_query", @@ -7087,7 +7248,7 @@ dependencies = [ "getrandom 0.3.2", "glob", "http", - "infer", + "infer 0.22.0", "json-patch", "log", "memchr", @@ -7105,9 +7266,9 @@ dependencies = [ "serialize-to-javascript", "swift-rs", "thiserror 2.0.12", - "toml 1.0.7+spec-1.1.0", + "toml 1.1.6+spec-1.1.0", "url", - "urlpattern", + "urlpattern 0.6.0", "uuid", "walkdir", ] @@ -7268,7 +7429,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", - "zerovec", + "zerovec 0.10.4", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec 0.11.5", ] [[package]] @@ -7393,14 +7564,14 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.7+spec-1.1.0" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.14.2", "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 1.0.1+spec-1.1.0", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", "winnow 1.0.2", @@ -7417,9 +7588,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.1+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -7430,7 +7601,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.14.2", "toml_datetime 0.6.8", "winnow 0.5.40", ] @@ -7441,7 +7612,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.14.2", "toml_datetime 0.6.8", "winnow 0.5.40", ] @@ -7452,7 +7623,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.14.2", "serde", "serde_spanned 0.6.8", "toml_datetime 0.6.8", @@ -7461,18 +7632,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.2", ] [[package]] name = "toml_writer" -version = "1.0.7+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -7558,12 +7729,12 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.24.1" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" dependencies = [ "crossbeam-channel", - "dirs 6.0.0", + "dirs 7.0.0", "libappindicator", "muda", "objc2 0.6.4", @@ -7768,14 +7939,15 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -7796,6 +7968,18 @@ dependencies = [ "url", ] +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties 2.1.2", + "regex", + "serde", + "url", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -8196,23 +8380,21 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.38.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.61.1", - "windows-core", - "windows-implement", - "windows-interface", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] name = "webview2-com-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", @@ -8221,13 +8403,13 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.38.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" dependencies = [ "thiserror 2.0.12", - "windows 0.61.1", - "windows-core", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -8246,16 +8428,6 @@ dependencies = [ "wasite", ] -[[package]] -name = "win7-notifications" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b4745047a00800bd8f2b8fb4b0eb6f7d96822084127f0ff7d68d07f692fe38" -dependencies = [ - "once_cell", - "windows-sys 0.59.0", -] - [[package]] name = "winapi" version = "0.3.9" @@ -8289,16 +8461,17 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window-vibrancy" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" dependencies = [ "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", "raw-window-handle", - "windows-sys 0.59.0", + "windows-sys 0.61.2", "windows-version", ] @@ -8321,11 +8494,23 @@ version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ - "windows-collections", - "windows-core", - "windows-future", + "windows-collections 0.2.0", + "windows-core 0.61.0", + "windows-future 0.2.0", "windows-link 0.1.1", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -8334,7 +8519,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.0", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", ] [[package]] @@ -8346,8 +8540,21 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.1", - "windows-result", - "windows-strings", + "windows-result 0.3.2", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -8356,15 +8563,26 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ - "windows-core", + "windows-core 0.61.0", "windows-link 0.1.1", ] [[package]] -name = "windows-implement" -version = "0.60.0" +name = "windows-future" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -8373,9 +8591,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -8400,10 +8618,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.0", "windows-link 0.1.1", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.5.1" @@ -8411,8 +8639,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e" dependencies = [ "windows-link 0.1.1", - "windows-result", - "windows-strings", + "windows-result 0.3.2", + "windows-strings 0.4.0", ] [[package]] @@ -8424,6 +8652,15 @@ dependencies = [ "windows-link 0.1.1", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.4.0" @@ -8433,6 +8670,15 @@ dependencies = [ "windows-link 0.1.1", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -8549,6 +8795,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.4" @@ -8852,16 +9107,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] -name = "wry" -version = "0.55.0" +name = "writeable" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" dependencies = [ "base64 0.22.1", "block2 0.6.2", "cookie", "crossbeam-channel", - "dirs 6.0.0", + "dirs 7.0.0", "dom_query", "dpi", "dunce", @@ -8889,8 +9150,8 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.61.1", - "windows-core", + "windows 0.62.2", + "windows-core 0.62.2", "windows-version", "x11-dl", ] @@ -8977,7 +9238,18 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", - "yoke-derive", + "yoke-derive 0.7.5", + "zerofrom", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive 0.8.3", "zerofrom", ] @@ -8990,7 +9262,19 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.100", - "synstructure", + "synstructure 0.13.1", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] @@ -9112,7 +9396,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.100", - "synstructure", + "synstructure 0.13.1", ] [[package]] @@ -9136,15 +9420,37 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke 0.8.1", + "zerofrom", +] + [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ - "yoke", + "yoke 0.7.5", "zerofrom", - "zerovec-derive", + "zerovec-derive 0.10.3", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke 0.8.1", + "zerofrom", + "zerovec-derive 0.11.6", ] [[package]] @@ -9158,6 +9464,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "zip" version = "2.6.1" @@ -9168,7 +9485,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "indexmap 2.11.4", + "indexmap 2.14.2", "memchr", "zopfli", ] @@ -9181,7 +9498,7 @@ checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" dependencies = [ "arbitrary", "crc32fast", - "indexmap 2.11.4", + "indexmap 2.14.2", "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index d297e4f71..0964d4845 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,10 @@ resolver = "2" serde = { version = "1", features = ["derive"] } tracing = "0.1" log = "0.4.21" -tauri = { version = "2.10", default-features = false } -tauri-build = "2.5" -tauri-plugin = "2.5" -tauri-utils = "2.8" +tauri = { version = "2.12", default-features = false } +tauri-build = "2.7" +tauri-plugin = "2.7" +tauri-utils = "2.10" serde_json = "1" thiserror = "2" url = "2" diff --git a/README.md b/README.md index d6f96c2be..a7be770fa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Official Tauri Plugins -This repo and all plugins require a Rust version of at least **1.77.2** +This repo and all plugins require a Rust version of at least **1.90** ## Plugins Found Here diff --git a/examples/api/package.json b/examples/api/package.json index 193e1f152..237a54cae 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -10,7 +10,7 @@ "tauri": "tauri" }, "dependencies": { - "@tauri-apps/api": "^2.11.0", + "@tauri-apps/api": "^2.12.0", "@tauri-apps/plugin-autostart": "workspace:*", "@tauri-apps/plugin-barcode-scanner": "workspace:*", "@tauri-apps/plugin-biometric": "workspace:*", @@ -41,7 +41,7 @@ "@iconify-json/codicon": "^1.2.49", "@iconify-json/ph": "^1.2.2", "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tauri-apps/cli": "2.11.4", + "@tauri-apps/cli": "2.12.0", "@unocss/extractor-svelte": "^66.6.7", "svelte": "^5.55.7", "unocss": "^66.6.7", diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 2f0b84bb1..0d3b3b1ca 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -30,9 +30,7 @@ tauri-plugin-http = { path = "../../../plugins/http", features = [ "multipart", "cookies", ], version = "2.7.0" } -tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.4.0", features = [ - "windows7-compat", -] } +tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.4.0" } tauri-plugin-os = { path = "../../../plugins/os", version = "2.3.2" } tauri-plugin-process = { path = "../../../plugins/process", version = "2.3.1" } tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.5" } diff --git a/packages/api-e2e/package.json b/packages/api-e2e/package.json index b4ce4b2e7..a323c5ee7 100644 --- a/packages/api-e2e/package.json +++ b/packages/api-e2e/package.json @@ -15,7 +15,7 @@ "devDependencies": { "@crabnebula/tauri-driver": "^2.0.9", "@crabnebula/test-runner-backend": "^0.3.0", - "@tauri-apps/api": "^2.11.0", + "@tauri-apps/api": "^2.12.0", "@tauri-apps/plugin-autostart": "workspace:*", "@tauri-apps/plugin-barcode-scanner": "workspace:*", "@tauri-apps/plugin-biometric": "workspace:*", diff --git a/plugins/autostart/README.md b/plugins/autostart/README.md index f9f9dce11..76af2442e 100644 --- a/plugins/autostart/README.md +++ b/plugins/autostart/README.md @@ -12,7 +12,7 @@ Automatically launch your application at startup. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/autostart/package.json b/plugins/autostart/package.json index 1c205a659..1e5e64d16 100644 --- a/plugins/autostart/package.json +++ b/plugins/autostart/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/barcode-scanner/android/build.gradle.kts b/plugins/barcode-scanner/android/build.gradle.kts index b3f32c0c6..def5fdfa2 100644 --- a/plugins/barcode-scanner/android/build.gradle.kts +++ b/plugins/barcode-scanner/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/barcode-scanner/package.json b/plugins/barcode-scanner/package.json index cc04f5583..45410dcc9 100644 --- a/plugins/barcode-scanner/package.json +++ b/plugins/barcode-scanner/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/biometric/Cargo.toml b/plugins/biometric/Cargo.toml index 2ac895301..0dc29e276 100644 --- a/plugins/biometric/Cargo.toml +++ b/plugins/biometric/Cargo.toml @@ -3,6 +3,7 @@ name = "tauri-plugin-biometric" version = "2.3.3" description = "Prompt the user for biometric authentication on Android and iOS." edition = { workspace = true } +rust-version = { workspace = true } authors = { workspace = true } license = { workspace = true } repository = { workspace = true } diff --git a/plugins/biometric/android/build.gradle.kts b/plugins/biometric/android/build.gradle.kts index 86ecccfff..546182102 100644 --- a/plugins/biometric/android/build.gradle.kts +++ b/plugins/biometric/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/biometric/package.json b/plugins/biometric/package.json index d8743f53c..9a3e41d5b 100644 --- a/plugins/biometric/package.json +++ b/plugins/biometric/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/cli/README.md b/plugins/cli/README.md index 30cd3449e..f4cf6920d 100644 --- a/plugins/cli/README.md +++ b/plugins/cli/README.md @@ -12,7 +12,7 @@ Parse arguments from your Command Line Interface. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/cli/package.json b/plugins/cli/package.json index d28035c22..2a3a6b510 100644 --- a/plugins/cli/package.json +++ b/plugins/cli/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/clipboard-manager/README.md b/plugins/clipboard-manager/README.md index b2353f971..f5719e86f 100644 --- a/plugins/clipboard-manager/README.md +++ b/plugins/clipboard-manager/README.md @@ -12,7 +12,7 @@ Read and write to the system clipboard. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/clipboard-manager/android/build.gradle.kts b/plugins/clipboard-manager/android/build.gradle.kts index e3640d3f9..87118e823 100644 --- a/plugins/clipboard-manager/android/build.gradle.kts +++ b/plugins/clipboard-manager/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/clipboard-manager/package.json b/plugins/clipboard-manager/package.json index 8261ee6cc..b83614956 100644 --- a/plugins/clipboard-manager/package.json +++ b/plugins/clipboard-manager/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/deep-link/README.md b/plugins/deep-link/README.md index 384dffecb..1d8c881b9 100644 --- a/plugins/deep-link/README.md +++ b/plugins/deep-link/README.md @@ -12,7 +12,7 @@ Set your Tauri application as the default handler for an URL. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/deep-link/android/build.gradle.kts b/plugins/deep-link/android/build.gradle.kts index e3d7a455f..828f5c391 100644 --- a/plugins/deep-link/android/build.gradle.kts +++ b/plugins/deep-link/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/deep-link/examples/app/package.json b/plugins/deep-link/examples/app/package.json index 595af974a..aa43ed40c 100644 --- a/plugins/deep-link/examples/app/package.json +++ b/plugins/deep-link/examples/app/package.json @@ -10,11 +10,11 @@ "tauri": "tauri" }, "dependencies": { - "@tauri-apps/api": "^2.11.0", + "@tauri-apps/api": "^2.12.0", "@tauri-apps/plugin-deep-link": "workspace:*" }, "devDependencies": { - "@tauri-apps/cli": "2.11.4", + "@tauri-apps/cli": "2.12.0", "typescript": "^6.0.3", "vite": "^8.0.16" } diff --git a/plugins/deep-link/examples/app/src-tauri/Cargo.toml b/plugins/deep-link/examples/app/src-tauri/Cargo.toml index 1ebb5ac8b..8fd27f851 100644 --- a/plugins/deep-link/examples/app/src-tauri/Cargo.toml +++ b/plugins/deep-link/examples/app/src-tauri/Cargo.toml @@ -6,7 +6,7 @@ authors = ["you"] license = "" repository = "" edition = "2021" -rust-version = "1.77.2" +rust-version = "1.90" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/plugins/deep-link/package.json b/plugins/deep-link/package.json index 3d21b7e00..fb2351dfa 100644 --- a/plugins/deep-link/package.json +++ b/plugins/deep-link/package.json @@ -25,9 +25,9 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" }, "devDependencies": { - "@tauri-apps/cli": "2.11.4" + "@tauri-apps/cli": "2.12.0" } } diff --git a/plugins/dialog/README.md b/plugins/dialog/README.md index e062c803f..d4d2e3666 100644 --- a/plugins/dialog/README.md +++ b/plugins/dialog/README.md @@ -12,7 +12,7 @@ Native system dialogs for opening and saving files along with message dialogs. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/dialog/android/build.gradle.kts b/plugins/dialog/android/build.gradle.kts index 96abbf572..e4e4c957c 100644 --- a/plugins/dialog/android/build.gradle.kts +++ b/plugins/dialog/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/dialog/package.json b/plugins/dialog/package.json index c7b8d5907..52e773082 100644 --- a/plugins/dialog/package.json +++ b/plugins/dialog/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/fs/README.md b/plugins/fs/README.md index d02c71d59..6072eb9a8 100644 --- a/plugins/fs/README.md +++ b/plugins/fs/README.md @@ -12,7 +12,7 @@ Access the file system. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/fs/android/build.gradle.kts b/plugins/fs/android/build.gradle.kts index b1b6af15d..4863e7772 100644 --- a/plugins/fs/android/build.gradle.kts +++ b/plugins/fs/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -19,8 +21,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/fs/package.json b/plugins/fs/package.json index 7fb9b3e8f..f5ea9f292 100644 --- a/plugins/fs/package.json +++ b/plugins/fs/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/geolocation/Cargo.toml b/plugins/geolocation/Cargo.toml index 3c2a50830..bfa109e21 100644 --- a/plugins/geolocation/Cargo.toml +++ b/plugins/geolocation/Cargo.toml @@ -3,6 +3,7 @@ name = "tauri-plugin-geolocation" description = "Get and track the device's current position" version = "2.3.3" edition = { workspace = true } +rust-version = { workspace = true } authors = { workspace = true } license = { workspace = true } repository = { workspace = true } diff --git a/plugins/geolocation/README.md b/plugins/geolocation/README.md index 3de41ef7f..9a1944a5a 100644 --- a/plugins/geolocation/README.md +++ b/plugins/geolocation/README.md @@ -12,7 +12,7 @@ This plugin provides APIs for getting and tracking the device's current position ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/geolocation/android/build.gradle.kts b/plugins/geolocation/android/build.gradle.kts index 022f11d7b..45ad0ed88 100644 --- a/plugins/geolocation/android/build.gradle.kts +++ b/plugins/geolocation/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/geolocation/package.json b/plugins/geolocation/package.json index 32bbb0e13..157fccfc8 100644 --- a/plugins/geolocation/package.json +++ b/plugins/geolocation/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/global-shortcut/README.md b/plugins/global-shortcut/README.md index a988826ee..161016ad8 100644 --- a/plugins/global-shortcut/README.md +++ b/plugins/global-shortcut/README.md @@ -12,7 +12,7 @@ Register global shortcuts. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/global-shortcut/package.json b/plugins/global-shortcut/package.json index 21d2920c3..5ecd41fe1 100644 --- a/plugins/global-shortcut/package.json +++ b/plugins/global-shortcut/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/haptics/Cargo.toml b/plugins/haptics/Cargo.toml index 524a4464f..3b368e675 100644 --- a/plugins/haptics/Cargo.toml +++ b/plugins/haptics/Cargo.toml @@ -3,6 +3,7 @@ name = "tauri-plugin-haptics" description = "Haptic feedback and vibrations on Android and iOS" version = "2.3.3" edition = { workspace = true } +rust-version = { workspace = true } authors = { workspace = true } license = { workspace = true } repository = { workspace = true } diff --git a/plugins/haptics/README.md b/plugins/haptics/README.md index 8a7cd8450..94877064b 100644 --- a/plugins/haptics/README.md +++ b/plugins/haptics/README.md @@ -14,7 +14,7 @@ There are no standards/requirements for vibration support on Android, so the `fe ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/haptics/android/build.gradle.kts b/plugins/haptics/android/build.gradle.kts index a178826ae..8b42d977d 100644 --- a/plugins/haptics/android/build.gradle.kts +++ b/plugins/haptics/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/haptics/package.json b/plugins/haptics/package.json index 0a848b5a7..c39d3a820 100644 --- a/plugins/haptics/package.json +++ b/plugins/haptics/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/http/README.md b/plugins/http/README.md index 7b8e3d2f8..32c619e1d 100644 --- a/plugins/http/README.md +++ b/plugins/http/README.md @@ -12,7 +12,7 @@ Access the HTTP client written in Rust. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/http/package.json b/plugins/http/package.json index 5d612e6a8..fce5f0d08 100644 --- a/plugins/http/package.json +++ b/plugins/http/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/localhost/README.md b/plugins/localhost/README.md index 8ab13b2bb..7e0cdd79f 100644 --- a/plugins/localhost/README.md +++ b/plugins/localhost/README.md @@ -14,7 +14,7 @@ Expose your apps assets through a localhost server instead of the default custom ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/log/README.md b/plugins/log/README.md index 81ca9fd8d..103d38705 100644 --- a/plugins/log/README.md +++ b/plugins/log/README.md @@ -12,7 +12,7 @@ Configurable logging for your Tauri app. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/log/package.json b/plugins/log/package.json index b21ff6cec..40347f20e 100644 --- a/plugins/log/package.json +++ b/plugins/log/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/nfc/Cargo.toml b/plugins/nfc/Cargo.toml index c0e7ed0a1..13bd3b061 100644 --- a/plugins/nfc/Cargo.toml +++ b/plugins/nfc/Cargo.toml @@ -3,6 +3,7 @@ name = "tauri-plugin-nfc" version = "2.3.6" description = "Read and write NFC tags on Android and iOS." edition = { workspace = true } +rust-version = { workspace = true } authors = { workspace = true } license = { workspace = true } repository = { workspace = true } diff --git a/plugins/nfc/android/build.gradle.kts b/plugins/nfc/android/build.gradle.kts index bf691ef3d..012d8994e 100644 --- a/plugins/nfc/android/build.gradle.kts +++ b/plugins/nfc/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/nfc/package.json b/plugins/nfc/package.json index 366bf1c43..1d8d9a75f 100644 --- a/plugins/nfc/package.json +++ b/plugins/nfc/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/notification/Cargo.toml b/plugins/notification/Cargo.toml index b306b5fd5..256503e8c 100644 --- a/plugins/notification/Cargo.toml +++ b/plugins/notification/Cargo.toml @@ -47,12 +47,12 @@ serde_repr = "0.1" [target.'cfg(target_os = "ios")'.dependencies] tauri = { workspace = true, features = ["wry"] } -[target."cfg(windows)".dependencies] -win7-notifications = { version = "0.4.5", optional = true } -windows-version = { version = "0.1", optional = true } - [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] notify-rust = "4.11" [features] -windows7-compat = ["win7-notifications", "windows-version"] +# Deprecated: tauri 2.12 no longer supports Windows 7, so this feature does nothing. +windows7-compat = [] +# Kept so the implicit features of the removed optional dependencies still resolve. +win7-notifications = [] +windows-version = [] diff --git a/plugins/notification/README.md b/plugins/notification/README.md index e17efe028..c5fc83959 100644 --- a/plugins/notification/README.md +++ b/plugins/notification/README.md @@ -12,7 +12,7 @@ Send message notifications (brief auto-expiring OS window element) to your user. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/notification/android/build.gradle.kts b/plugins/notification/android/build.gradle.kts index f2b4cf679..6b3773e2f 100644 --- a/plugins/notification/android/build.gradle.kts +++ b/plugins/notification/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/notification/package.json b/plugins/notification/package.json index 58d64a037..dafb8c116 100644 --- a/plugins/notification/package.json +++ b/plugins/notification/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/notification/src/desktop.rs b/plugins/notification/src/desktop.rs index 5b41a6c4b..cd8bddfb9 100644 --- a/plugins/notification/src/desktop.rs +++ b/plugins/notification/src/desktop.rs @@ -36,10 +36,6 @@ impl crate::NotificationBuilder { /// /// Returns an error when the notification could not be prepared, /// e.g. when the path of the running executable cannot be resolved on Windows. - /// - /// ## Platform-specific - /// - /// - **Windows**: Not supported on Windows 7 unless the `windows7-compat` Cargo feature is enabled. pub fn show(self) -> crate::Result<()> { let mut notification = imp::Notification::new(self.app.config().identifier.clone()); @@ -59,11 +55,6 @@ impl crate::NotificationBuilder { if let Some(sound) = self.data.sound { notification = notification.sound(sound); } - #[cfg(feature = "windows7-compat")] - { - notification.notify(&self.app)?; - } - #[cfg(not(feature = "windows7-compat"))] notification.show()?; Ok(()) @@ -209,14 +200,6 @@ mod imp { /// .run(tauri::generate_context!("test/tauri.conf.json")) /// .expect("error while running tauri application"); /// ``` - /// - /// ## Platform-specific - /// - /// - **Windows**: Not supported on Windows 7. If your app targets it, enable the `windows7-compat` feature and use [`Self::notify`]. - #[cfg_attr( - all(not(docsrs), feature = "windows7-compat"), - deprecated = "This function does not work on Windows 7. Use `Self::notify` instead." - )] pub fn show(self) -> crate::Result<()> { let mut notification = notify_rust::Notification::new(); if let Some(body) = self.body { @@ -261,70 +244,12 @@ mod imp { Ok(()) } - /// Shows the notification. This API is similar to [`Self::show`], but it also works on Windows 7. - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_notification::NotificationExt; - /// - /// tauri::Builder::default() - /// .setup(move |app| { - /// app.notification().builder() - /// .title("Tauri") - /// .body("Tauri is awesome!") - /// .show() - /// .unwrap(); - /// Ok(()) - /// }) - /// .run(tauri::generate_context!("test/tauri.conf.json")) - /// .expect("error while running tauri application"); - /// ``` + /// Shows the notification. Same as [`Self::show`]. #[cfg(feature = "windows7-compat")] #[cfg_attr(docsrs, doc(cfg(feature = "windows7-compat")))] - #[allow(unused_variables)] - pub fn notify(self, app: &tauri::AppHandle) -> crate::Result<()> { - #[cfg(windows)] - { - fn is_windows_7() -> bool { - let v = windows_version::OsVersion::current(); - // windows 7 is 6.1 - v.major == 6 && v.minor == 1 - } - - if is_windows_7() { - self.notify_win7(app) - } else { - #[allow(deprecated)] - self.show() - } - } - #[cfg(not(windows))] - { - #[allow(deprecated)] - self.show() - } - } - - /// Shows the notification on Windows 7. - #[cfg(all(windows, feature = "windows7-compat"))] - fn notify_win7(self, app: &tauri::AppHandle) -> crate::Result<()> { - let app_ = app.clone(); - let _ = app.clone().run_on_main_thread(move || { - let mut notification = win7_notifications::Notification::new(); - if let Some(body) = self.body { - notification.body(&body); - } - if let Some(title) = self.title { - notification.summary(&title); - } - if let Some(icon) = app_.default_window_icon() { - notification.icon(icon.rgba().to_vec(), icon.width(), icon.height()); - } - let _ = notification.show(); - }); - - Ok(()) + #[deprecated = "Tauri no longer supports Windows 7, use `Self::show` instead."] + pub fn notify(self, _app: &tauri::AppHandle) -> crate::Result<()> { + self.show() } } } diff --git a/plugins/notification/src/lib.rs b/plugins/notification/src/lib.rs index 32c2e61ec..9fd74406a 100644 --- a/plugins/notification/src/lib.rs +++ b/plugins/notification/src/lib.rs @@ -6,7 +6,7 @@ //! //! ## Cargo features //! -//! - **windows7-compat**: Adds support for the legacy Windows 7 notification implementation and Windows-version detection. +//! - **windows7-compat**: Deprecated and does nothing, since Tauri no longer supports Windows 7. #![doc( html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index e17b53093..1c3f7060e 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -3,6 +3,7 @@ name = "tauri-plugin-opener" version = "2.5.5" description = "Open files and URLs using their default application." edition = { workspace = true } +rust-version = { workspace = true } authors = { workspace = true } license = { workspace = true } repository = { workspace = true } diff --git a/plugins/opener/README.md b/plugins/opener/README.md index bf25c5b85..46d0e9992 100644 --- a/plugins/opener/README.md +++ b/plugins/opener/README.md @@ -12,7 +12,7 @@ ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/opener/android/build.gradle.kts b/plugins/opener/android/build.gradle.kts index 9ed0957f1..95d510f8c 100644 --- a/plugins/opener/android/build.gradle.kts +++ b/plugins/opener/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/opener/package.json b/plugins/opener/package.json index f51f27a1f..5ed03db3f 100644 --- a/plugins/opener/package.json +++ b/plugins/opener/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/os/README.md b/plugins/os/README.md index d3f58b269..578fd1124 100644 --- a/plugins/os/README.md +++ b/plugins/os/README.md @@ -12,7 +12,7 @@ Read information about the operating system. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/os/package.json b/plugins/os/package.json index dbaef8e2a..ebe97456b 100644 --- a/plugins/os/package.json +++ b/plugins/os/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/persisted-scope/README.md b/plugins/persisted-scope/README.md index c3ec88feb..c6a2bb888 100644 --- a/plugins/persisted-scope/README.md +++ b/plugins/persisted-scope/README.md @@ -12,7 +12,7 @@ Save filesystem and asset scopes and restore them when the app is reopened. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/positioner/README.md b/plugins/positioner/README.md index 8495b616f..a09193b37 100644 --- a/plugins/positioner/README.md +++ b/plugins/positioner/README.md @@ -14,7 +14,7 @@ This plugin is a port of [electron-positioner](https://github.com/jenslind/elect ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/positioner/package.json b/plugins/positioner/package.json index bd27bbaa9..5531c33c8 100644 --- a/plugins/positioner/package.json +++ b/plugins/positioner/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/process/README.md b/plugins/process/README.md index 13a1c4e9d..56c29a5a2 100644 --- a/plugins/process/README.md +++ b/plugins/process/README.md @@ -12,7 +12,7 @@ This plugin provides APIs to access the current process. To spawn child processe ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/process/package.json b/plugins/process/package.json index 2bde9de0e..7cd42a190 100644 --- a/plugins/process/package.json +++ b/plugins/process/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/shell/README.md b/plugins/shell/README.md index 132d1ccb8..50d6f32a4 100644 --- a/plugins/shell/README.md +++ b/plugins/shell/README.md @@ -12,7 +12,7 @@ Access the system shell. Allows you to spawn child processes and manage files an ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/shell/android/build.gradle.kts b/plugins/shell/android/build.gradle.kts index 3f3ca0ea2..7d8bf9aca 100644 --- a/plugins/shell/android/build.gradle.kts +++ b/plugins/shell/android/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -18,8 +20,11 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 } } diff --git a/plugins/shell/package.json b/plugins/shell/package.json index cf5ef5c2f..d7c259a18 100644 --- a/plugins/shell/package.json +++ b/plugins/shell/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/single-instance/README.md b/plugins/single-instance/README.md index e5f1fd8fb..3d865e8f5 100644 --- a/plugins/single-instance/README.md +++ b/plugins/single-instance/README.md @@ -12,7 +12,7 @@ Ensure a single instance of your tauri app is running. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/single-instance/examples/vanilla/package.json b/plugins/single-instance/examples/vanilla/package.json index d92ff0ddf..0b09e5fd8 100644 --- a/plugins/single-instance/examples/vanilla/package.json +++ b/plugins/single-instance/examples/vanilla/package.json @@ -9,6 +9,6 @@ "author": "", "license": "MIT", "devDependencies": { - "@tauri-apps/cli": "2.11.4" + "@tauri-apps/cli": "2.12.0" } } diff --git a/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml b/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml index c98659a25..59624cbc4 100644 --- a/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml +++ b/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml @@ -5,7 +5,7 @@ description = "A Tauri App" authors = ["You"] repository = "" edition = "2021" -rust-version = "1.77.2" +rust-version = "1.90" [dependencies] serde = { workspace = true } diff --git a/plugins/sql/README.md b/plugins/sql/README.md index 2e1e4ca46..cfa728d16 100644 --- a/plugins/sql/README.md +++ b/plugins/sql/README.md @@ -12,7 +12,7 @@ Interface with SQL databases through [sqlx](https://github.com/launchbadge/sqlx) ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/sql/package.json b/plugins/sql/package.json index b6eb93fbb..5c0858b33 100644 --- a/plugins/sql/package.json +++ b/plugins/sql/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/store/README.md b/plugins/store/README.md index 8070ddfaa..3de6751b5 100644 --- a/plugins/store/README.md +++ b/plugins/store/README.md @@ -12,7 +12,7 @@ Simple, persistent key-value store. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/store/examples/AppSettingsManager/package.json b/plugins/store/examples/AppSettingsManager/package.json index f3907b4ad..822f130fd 100644 --- a/plugins/store/examples/AppSettingsManager/package.json +++ b/plugins/store/examples/AppSettingsManager/package.json @@ -8,7 +8,7 @@ "tauri": "tauri" }, "devDependencies": { - "@tauri-apps/cli": "2.11.4", + "@tauri-apps/cli": "2.12.0", "typescript": "^6.0.3", "vite": "^8.0.16" } diff --git a/plugins/store/package.json b/plugins/store/package.json index 79f7924f4..81dc4fa87 100644 --- a/plugins/store/package.json +++ b/plugins/store/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/stronghold/README.md b/plugins/stronghold/README.md index eaa9d536e..8e669197f 100644 --- a/plugins/stronghold/README.md +++ b/plugins/stronghold/README.md @@ -12,7 +12,7 @@ Store secrets and keys using the [IOTA Stronghold](https://github.com/iotaledger ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/stronghold/package.json b/plugins/stronghold/package.json index 446254b81..e5de693bf 100644 --- a/plugins/stronghold/package.json +++ b/plugins/stronghold/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/updater/README.md b/plugins/updater/README.md index f122e74f1..ab0399335 100644 --- a/plugins/updater/README.md +++ b/plugins/updater/README.md @@ -12,7 +12,7 @@ In-app updates for Tauri applications. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/updater/package.json b/plugins/updater/package.json index 820b3a1b6..1d8a10d52 100644 --- a/plugins/updater/package.json +++ b/plugins/updater/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/upload/README.md b/plugins/upload/README.md index dc3699abf..c9f165a79 100644 --- a/plugins/upload/README.md +++ b/plugins/upload/README.md @@ -13,7 +13,7 @@ Download files from a remote HTTP server to disk. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/upload/package.json b/plugins/upload/package.json index b9b78d496..4694523fb 100644 --- a/plugins/upload/package.json +++ b/plugins/upload/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/websocket/README.md b/plugins/websocket/README.md index 3d9e7a848..20dce131b 100644 --- a/plugins/websocket/README.md +++ b/plugins/websocket/README.md @@ -12,7 +12,7 @@ Open a WebSocket connection using a Rust client in JS. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/websocket/examples/tauri-app/package.json b/plugins/websocket/examples/tauri-app/package.json index cc328b800..d9ed32926 100644 --- a/plugins/websocket/examples/tauri-app/package.json +++ b/plugins/websocket/examples/tauri-app/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "devDependencies": { - "@tauri-apps/cli": "2.11.4", + "@tauri-apps/cli": "2.12.0", "typescript": "^6.0.3", "vite": "^8.0.16" }, diff --git a/plugins/websocket/package.json b/plugins/websocket/package.json index c5fd867d0..b307dc0a2 100644 --- a/plugins/websocket/package.json +++ b/plugins/websocket/package.json @@ -24,6 +24,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/plugins/window-state/README.md b/plugins/window-state/README.md index 0d74413ee..d86d61e70 100644 --- a/plugins/window-state/README.md +++ b/plugins/window-state/README.md @@ -12,7 +12,7 @@ Save window positions and sizes and restore them when the app is reopened. ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. diff --git a/plugins/window-state/package.json b/plugins/window-state/package.json index 7f2747f21..1ac60b2b7 100644 --- a/plugins/window-state/package.json +++ b/plugins/window-state/package.json @@ -25,6 +25,6 @@ "LICENSE" ], "dependencies": { - "@tauri-apps/api": "^2.11.0" + "@tauri-apps/api": "^2.12.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 224f427b5..a385efc35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,8 +154,8 @@ importers: examples/api: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 '@tauri-apps/plugin-autostart': specifier: workspace:* version: link:../../plugins/autostart @@ -242,8 +242,8 @@ importers: specifier: ^7.0.0 version: 7.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))(vite@8.0.16(@types/node@22.20.3)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)(yaml@2.9.0)) '@tauri-apps/cli': - specifier: 2.11.4 - version: 2.11.4 + specifier: 2.12.0 + version: 2.12.0 '@unocss/extractor-svelte': specifier: ^66.6.7 version: 66.6.7 @@ -266,8 +266,8 @@ importers: specifier: ^0.3.0 version: 0.3.0 '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 '@tauri-apps/plugin-autostart': specifier: workspace:* version: link:../../plugins/autostart @@ -395,55 +395,55 @@ importers: plugins/autostart: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/barcode-scanner: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/biometric: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/cli: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/clipboard-manager: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/deep-link: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 devDependencies: '@tauri-apps/cli': - specifier: 2.11.4 - version: 2.11.4 + specifier: 2.12.0 + version: 2.12.0 plugins/deep-link/examples/app: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 '@tauri-apps/plugin-deep-link': specifier: workspace:* version: link:../.. devDependencies: '@tauri-apps/cli': - specifier: 2.11.4 - version: 2.11.4 + specifier: 2.12.0 + version: 2.12.0 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -454,110 +454,110 @@ importers: plugins/dialog: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/fs: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/geolocation: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/global-shortcut: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/haptics: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/http: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/log: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/nfc: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/notification: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/opener: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/os: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/positioner: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/process: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/shell: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/single-instance/examples/vanilla: devDependencies: '@tauri-apps/cli': - specifier: 2.11.4 - version: 2.11.4 + specifier: 2.12.0 + version: 2.12.0 plugins/sql: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/store: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/store/examples/AppSettingsManager: devDependencies: '@tauri-apps/cli': - specifier: 2.11.4 - version: 2.11.4 + specifier: 2.12.0 + version: 2.12.0 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -568,26 +568,26 @@ importers: plugins/stronghold: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/updater: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/upload: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/websocket: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 plugins/websocket/examples/tauri-app: dependencies: @@ -596,8 +596,8 @@ importers: version: link:../.. devDependencies: '@tauri-apps/cli': - specifier: 2.11.4 - version: 2.11.4 + specifier: 2.12.0 + version: 2.12.0 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -608,8 +608,8 @@ importers: plugins/window-state: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.12.0 + version: 2.12.0 packages: @@ -1782,82 +1782,82 @@ packages: svelte: ^5.46.4 vite: ^8.0.0-beta.7 || ^8.0.0 - '@tauri-apps/api@2.11.0': - resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} + '@tauri-apps/api@2.12.0': + resolution: {integrity: sha512-fUSxFX8VABYe9W9K5ROdEaKVkNUFRcMSRofjruvaxztXA+I0XnQHcYHF8m/a6iExRUxsUf0HsFWhPBuH9IgzJw==} - '@tauri-apps/cli-darwin-arm64@2.11.4': - resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==} + '@tauri-apps/cli-darwin-arm64@2.12.0': + resolution: {integrity: sha512-v5J+jISihLGtih1+3yWagbsL+5dbLZORu4vaSI0vhY0Heb+MAW+T/1s1RyKxrQrwEPGBG5kqwfDDTnSbmVkuqA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.11.4': - resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==} + '@tauri-apps/cli-darwin-x64@2.12.0': + resolution: {integrity: sha512-g27I07pfW/dMS8vcNqB9lamgKF0TqLlcy8e7t9qDGsB1iaeyjJWb2TgBF4tM9ZwLqPAZUXbsz28oXljO12WC4g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': - resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.12.0': + resolution: {integrity: sha512-gXyQ710fNc99YuxFxNyeX9PLSrIpbtgBBj7GQo9tSeRT6fIK40q3/gm7X5jqZo7mWLR2m0R/2ALd3vAWo0Z32Q==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.11.4': - resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==} + '@tauri-apps/cli-linux-arm64-gnu@2.12.0': + resolution: {integrity: sha512-H9cyXuq9WgxoHh2NZ+R+oN+W78x5UwaBDEp0/fmqUUkaBoioCuOZErM9xKiIV6bpNAhmw1RoWZ49u37q/vaiAg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-arm64-musl@2.11.4': - resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==} + '@tauri-apps/cli-linux-arm64-musl@2.12.0': + resolution: {integrity: sha512-5tFuk3JKMBa1M/iN4gN32OYlSfhY3aqb5PO3zCugxaT+sfecyMKpc3EM9VDbhmwWBE9QD4Tfj7wGhlqx2yy2pQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': - resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==} + '@tauri-apps/cli-linux-riscv64-gnu@2.12.0': + resolution: {integrity: sha512-LOrMyV+nb7pKq8P0BZtTBQuyzm1ORjn16R9MlupmxuT5LqxtuYJlw3VMwbaeXCToJ8jbP5u9JnkfmbqDlvETXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-gnu@2.11.4': - resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==} + '@tauri-apps/cli-linux-x64-gnu@2.12.0': + resolution: {integrity: sha512-aMIkI+X9qvD2+k80QZB3tuOYT41/cEheikC510CwAIZ31KLXORsRASYYzM4mi1/58ICmtzSLFhCC08keir7COw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-musl@2.11.4': - resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==} + '@tauri-apps/cli-linux-x64-musl@2.12.0': + resolution: {integrity: sha512-9gl3NhUbfuGhSKjeGCypKSdtoezOQRcDUrkhTWtLQEjnajets6peO6NadgI2DF0OzbiiFOzrnnIGMwBS+Ap6Iw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@tauri-apps/cli-win32-arm64-msvc@2.11.4': - resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==} + '@tauri-apps/cli-win32-arm64-msvc@2.12.0': + resolution: {integrity: sha512-1W+v86wsYLuyBDN0iDessD4CD9o+yNyO0m8Dp3+/vfjiZrJ/v7R7/aTRBeikaq2tLGgxeag0gxnFggBNBGw91A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.11.4': - resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==} + '@tauri-apps/cli-win32-ia32-msvc@2.12.0': + resolution: {integrity: sha512-SsBjvq4f7YaBGqcWoE3cycoItPf0SrQeqFqWWgfxoeZWAIz01E31LDSeZ2p/etJiIY0R9oi045chkFfb0O7WGw==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.11.4': - resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==} + '@tauri-apps/cli-win32-x64-msvc@2.12.0': + resolution: {integrity: sha512-9zzDKYUH4TKKBgRdDdVQX8fiRO5gJ8hD1nGsHU9DOHG5Jpm3arjbu9Xg1frR20EusG3Sl8X0dppoO/4EoUXUGg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.11.4': - resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==} + '@tauri-apps/cli@2.12.0': + resolution: {integrity: sha512-GIShnAR4C16kSTA8NO02fo8fOw/I5z5sAUEB5S2HlPHFnBkbihL4c9a/MpkxOm1+q5S+tFd3X4QUjHWxfyGzpQ==} engines: {node: '>= 10'} hasBin: true @@ -5735,54 +5735,54 @@ snapshots: vite: 8.0.16(@types/node@22.20.3)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)(yaml@2.9.0) vitefu: 1.1.2(vite@8.0.16(@types/node@22.20.3)(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)(yaml@2.9.0)) - '@tauri-apps/api@2.11.0': {} + '@tauri-apps/api@2.12.0': {} - '@tauri-apps/cli-darwin-arm64@2.11.4': + '@tauri-apps/cli-darwin-arm64@2.12.0': optional: true - '@tauri-apps/cli-darwin-x64@2.11.4': + '@tauri-apps/cli-darwin-x64@2.12.0': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': + '@tauri-apps/cli-linux-arm-gnueabihf@2.12.0': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.11.4': + '@tauri-apps/cli-linux-arm64-gnu@2.12.0': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.11.4': + '@tauri-apps/cli-linux-arm64-musl@2.12.0': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': + '@tauri-apps/cli-linux-riscv64-gnu@2.12.0': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.11.4': + '@tauri-apps/cli-linux-x64-gnu@2.12.0': optional: true - '@tauri-apps/cli-linux-x64-musl@2.11.4': + '@tauri-apps/cli-linux-x64-musl@2.12.0': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.11.4': + '@tauri-apps/cli-win32-arm64-msvc@2.12.0': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.11.4': + '@tauri-apps/cli-win32-ia32-msvc@2.12.0': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.11.4': + '@tauri-apps/cli-win32-x64-msvc@2.12.0': optional: true - '@tauri-apps/cli@2.11.4': + '@tauri-apps/cli@2.12.0': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.11.4 - '@tauri-apps/cli-darwin-x64': 2.11.4 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4 - '@tauri-apps/cli-linux-arm64-gnu': 2.11.4 - '@tauri-apps/cli-linux-arm64-musl': 2.11.4 - '@tauri-apps/cli-linux-riscv64-gnu': 2.11.4 - '@tauri-apps/cli-linux-x64-gnu': 2.11.4 - '@tauri-apps/cli-linux-x64-musl': 2.11.4 - '@tauri-apps/cli-win32-arm64-msvc': 2.11.4 - '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 - '@tauri-apps/cli-win32-x64-msvc': 2.11.4 + '@tauri-apps/cli-darwin-arm64': 2.12.0 + '@tauri-apps/cli-darwin-x64': 2.12.0 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.12.0 + '@tauri-apps/cli-linux-arm64-gnu': 2.12.0 + '@tauri-apps/cli-linux-arm64-musl': 2.12.0 + '@tauri-apps/cli-linux-riscv64-gnu': 2.12.0 + '@tauri-apps/cli-linux-x64-gnu': 2.12.0 + '@tauri-apps/cli-linux-x64-musl': 2.12.0 + '@tauri-apps/cli-win32-arm64-msvc': 2.12.0 + '@tauri-apps/cli-win32-ia32-msvc': 2.12.0 + '@tauri-apps/cli-win32-x64-msvc': 2.12.0 '@tootallnate/quickjs-emscripten@0.23.0': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 88a6e5654..3106fb374 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -28,3 +28,18 @@ auditConfig: # unpacks the browser drivers we never download (see allowBuilds above) - GHSA-jmr9-qjv8-65gv - GHSA-7pqw-9j4j-h8q3 + +minimumReleaseAgeExclude: + - '@tauri-apps/api@2.12.0' + - '@tauri-apps/cli-darwin-arm64@2.12.0' + - '@tauri-apps/cli-darwin-x64@2.12.0' + - '@tauri-apps/cli-linux-arm-gnueabihf@2.12.0' + - '@tauri-apps/cli-linux-arm64-gnu@2.12.0' + - '@tauri-apps/cli-linux-arm64-musl@2.12.0' + - '@tauri-apps/cli-linux-riscv64-gnu@2.12.0' + - '@tauri-apps/cli-linux-x64-gnu@2.12.0' + - '@tauri-apps/cli-linux-x64-musl@2.12.0' + - '@tauri-apps/cli-win32-arm64-msvc@2.12.0' + - '@tauri-apps/cli-win32-ia32-msvc@2.12.0' + - '@tauri-apps/cli-win32-x64-msvc@2.12.0' + - '@tauri-apps/cli@2.12.0' diff --git a/shared/template/README.md b/shared/template/README.md index c5b456bff..8e2cab3fd 100644 --- a/shared/template/README.md +++ b/shared/template/README.md @@ -12,7 +12,7 @@ ## Install -_This plugin requires a Rust version of at least **1.77.2**_ +_This plugin requires a Rust version of at least **1.90**_ There are three general methods of installation that we can recommend. From b6a38a5e32c64745d28267d1ab59c8bd02ba4d72 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sat, 26 Sep 2026 15:29:52 -0300 Subject: [PATCH 23/29] fix(ci): e2e test error reporting --- packages/api-e2e/test/helpers/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/api-e2e/test/helpers/index.ts b/packages/api-e2e/test/helpers/index.ts index f22992ab7..519f2aa96 100644 --- a/packages/api-e2e/test/helpers/index.ts +++ b/packages/api-e2e/test/helpers/index.ts @@ -148,9 +148,19 @@ export async function tauri( .then( function (value) { return { ok: true, value: value === undefined ? null : value }; }, function (error) { + // Mobile plugins reject with a \`{ message, code?, data? }\` object + // rather than a string or an Error. + var message = + error instanceof Error + ? error.message + : error && typeof error === 'object' && typeof error.message === 'string' + ? error.message + : typeof error === 'object' + ? JSON.stringify(error) + : String(error); return { ok: false, - error: error instanceof Error ? error.message : String(error), + error: message, stack: error instanceof Error ? error.stack : undefined }; } From b517cc7e858d0d45d6b0d67fddf5290ff2e7e2ca Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sat, 26 Sep 2026 15:33:39 -0300 Subject: [PATCH 24/29] fix: actually set rust-version to 1.90 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0964d4845..ab5202f4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ zbus = "5.9" edition = "2021" authors = ["Tauri Programme within The Commons Conservancy"] license = "Apache-2.0 OR MIT" -rust-version = "1.77.2" +rust-version = "1.90" repository = "https://github.com/tauri-apps/plugins-workspace" # default to small, optimized release binaries From 3d8a3c877b352726285b79be9450dd59ba78fc63 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sat, 26 Sep 2026 15:36:22 -0300 Subject: [PATCH 25/29] chore: edition 2024 --- Cargo.toml | 2 +- examples/api/src-tauri/Cargo.toml | 2 +- examples/api/src-tauri/src/lib.rs | 7 ++-- examples/api/src-tauri/src/tray.rs | 2 +- plugins/autostart/src/lib.rs | 5 ++- plugins/barcode-scanner/src/error.rs | 2 +- plugins/barcode-scanner/src/lib.rs | 2 +- plugins/biometric/src/error.rs | 2 +- plugins/biometric/src/lib.rs | 2 +- plugins/cli/src/lib.rs | 2 +- plugins/cli/src/parser.rs | 7 ++-- plugins/clipboard-manager/src/commands.rs | 2 +- plugins/clipboard-manager/src/desktop.rs | 2 +- plugins/clipboard-manager/src/error.rs | 2 +- plugins/clipboard-manager/src/lib.rs | 2 +- plugins/clipboard-manager/src/mobile.rs | 2 +- .../examples/app/src-tauri/Cargo.toml | 2 +- plugins/deep-link/src/commands.rs | 2 +- plugins/deep-link/src/config.rs | 9 ++--- plugins/deep-link/src/error.rs | 2 +- plugins/deep-link/src/lib.rs | 25 ++++++------- plugins/dialog/src/commands.rs | 17 ++++----- plugins/dialog/src/desktop.rs | 4 +-- plugins/dialog/src/error.rs | 2 +- plugins/dialog/src/lib.rs | 2 +- plugins/dialog/src/mobile.rs | 4 +-- plugins/fs/src/android.rs | 4 +-- plugins/fs/src/commands.rs | 27 ++++++++++---- plugins/fs/src/file_path.rs | 10 +++--- plugins/fs/src/ios.rs | 7 ++-- plugins/fs/src/lib.rs | 2 +- plugins/fs/src/watcher.rs | 8 ++--- plugins/geolocation/src/commands.rs | 2 +- plugins/geolocation/src/desktop.rs | 4 +-- plugins/geolocation/src/error.rs | 2 +- plugins/geolocation/src/lib.rs | 2 +- plugins/geolocation/src/mobile.rs | 4 +-- plugins/global-shortcut/src/lib.rs | 4 +-- plugins/haptics/src/commands.rs | 2 +- plugins/haptics/src/desktop.rs | 2 +- plugins/haptics/src/error.rs | 2 +- plugins/haptics/src/lib.rs | 2 +- plugins/haptics/src/mobile.rs | 4 +-- plugins/http/src/commands.rs | 33 ++++++++++------- plugins/http/src/lib.rs | 2 +- plugins/http/src/reqwest_cookie_store.rs | 2 +- plugins/http/src/scope.rs | 24 +++++++------ plugins/localhost/src/lib.rs | 2 +- plugins/log/src/lib.rs | 10 +++--- plugins/nfc/src/error.rs | 2 +- plugins/nfc/src/lib.rs | 2 +- plugins/notification/src/commands.rs | 2 +- plugins/notification/src/desktop.rs | 2 +- plugins/notification/src/error.rs | 2 +- plugins/notification/src/lib.rs | 6 ++-- plugins/notification/src/mobile.rs | 4 +-- plugins/notification/src/models.rs | 8 ++--- plugins/opener/src/commands.rs | 4 +-- plugins/opener/src/lib.rs | 2 +- plugins/opener/src/reveal_item_in_dir.rs | 6 ++-- plugins/opener/src/scope.rs | 4 +-- plugins/opener/src/windows_shell_path.rs | 2 +- plugins/os/src/lib.rs | 4 +-- plugins/persisted-scope/src/lib.rs | 9 +++-- plugins/positioner/src/lib.rs | 4 +-- plugins/process/src/lib.rs | 2 +- plugins/shell/src/commands.rs | 4 +-- plugins/shell/src/lib.rs | 2 +- plugins/shell/src/process/mod.rs | 4 +-- plugins/shell/src/scope.rs | 18 ++++++---- plugins/shell/src/scope_entry.rs | 2 +- .../examples/vanilla/src-tauri/Cargo.toml | 2 +- plugins/single-instance/src/lib.rs | 2 +- .../src/platform_impl/linux.rs | 7 ++-- .../src/platform_impl/macos.rs | 4 +-- .../src/platform_impl/windows.rs | 10 +++--- plugins/sql/src/commands.rs | 2 +- plugins/sql/src/decode/mysql.rs | 2 +- plugins/sql/src/decode/postgres.rs | 2 +- plugins/sql/src/decode/sqlite.rs | 2 +- plugins/sql/src/lib.rs | 6 ++-- plugins/sql/src/wrapper.rs | 2 +- .../AppSettingsManager/src-tauri/Cargo.toml | 2 +- plugins/store/src/lib.rs | 9 +++-- plugins/store/src/store.rs | 4 +-- plugins/stronghold/src/kdf.rs | 4 +-- plugins/stronghold/src/lib.rs | 11 +++--- plugins/updater/src/commands.rs | 2 +- plugins/updater/src/config.rs | 8 +++-- plugins/updater/src/error.rs | 12 ++++--- plugins/updater/src/lib.rs | 2 +- plugins/updater/src/updater.rs | 36 ++++++++----------- .../updater/tests/app-updater/tests/update.rs | 3 +- .../tests/updater-migration/tests/update.rs | 2 +- .../tests/updater-migration/v1-app/Cargo.toml | 2 +- plugins/upload/src/lib.rs | 5 ++- .../examples/tauri-app/src-tauri/Cargo.toml | 2 +- plugins/websocket/src/lib.rs | 10 +++--- plugins/window-state/src/cmd.rs | 2 +- plugins/window-state/src/lib.rs | 2 +- 100 files changed, 276 insertions(+), 253 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ab5202f4b..5ef77bfe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ glob = "0.3" zbus = "5.9" [workspace.package] -edition = "2021" +edition = "2024" authors = ["Tauri Programme within The Commons Conservancy"] license = "Apache-2.0 OR MIT" rust-version = "1.90" diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 0d3b3b1ca..8e06d3c4f 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -3,7 +3,7 @@ name = "api" publish = false version = "2.0.49" description = "An example Tauri Application showcasing the api" -edition = "2021" +edition = "2024" rust-version = { workspace = true } license = "Apache-2.0 OR MIT" diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index cb858201a..2cb8d927d 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -8,8 +8,8 @@ mod tray; use serde::Serialize; use tauri::{ - webview::{PageLoadEvent, WebviewWindowBuilder}, App, AppHandle, Emitter, Listener, RunEvent, WebviewUrl, + webview::{PageLoadEvent, WebviewWindowBuilder}, }; #[derive(Clone, Serialize)] @@ -204,12 +204,11 @@ pub fn run() { app.run(move |_app_handle, _event| { #[cfg(desktop)] - if let RunEvent::ExitRequested { code, api, .. } = &_event { - if code.is_none() { + if let RunEvent::ExitRequested { code, api, .. } = &_event + && code.is_none() { // Keep the event loop running even if all windows are closed // This allow us to catch system tray events when there is no window api.prevent_exit(); } - } }) } diff --git a/examples/api/src-tauri/src/tray.rs b/examples/api/src-tauri/src/tray.rs index ba4a5a003..f0fdff344 100644 --- a/examples/api/src-tauri/src/tray.rs +++ b/examples/api/src-tauri/src/tray.rs @@ -4,9 +4,9 @@ use std::sync::atomic::{AtomicBool, Ordering}; use tauri::{ + Manager, Runtime, WebviewUrl, WebviewWindowBuilder, menu::{Menu, MenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, - Manager, Runtime, WebviewUrl, WebviewWindowBuilder, }; pub fn create_tray(app: &tauri::AppHandle) -> tauri::Result<()> { diff --git a/plugins/autostart/src/lib.rs b/plugins/autostart/src/lib.rs index af9ef7564..e92fa62c0 100644 --- a/plugins/autostart/src/lib.rs +++ b/plugins/autostart/src/lib.rs @@ -11,11 +11,10 @@ #![cfg(not(any(target_os = "android", target_os = "ios")))] use auto_launch::{AutoLaunch, AutoLaunchBuilder}; -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; use tauri::{ - command, + Manager, Runtime, State, command, plugin::{Builder as PluginBuilder, TauriPlugin}, - Manager, Runtime, State, }; use std::env::current_exe; diff --git a/plugins/barcode-scanner/src/error.rs b/plugins/barcode-scanner/src/error.rs index 5aabafd92..17d502e6a 100644 --- a/plugins/barcode-scanner/src/error.rs +++ b/plugins/barcode-scanner/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; /// Alias for the result type returned by this crate's functions. pub type Result = std::result::Result; diff --git a/plugins/barcode-scanner/src/lib.rs b/plugins/barcode-scanner/src/lib.rs index 0266b7e38..fb6a89257 100644 --- a/plugins/barcode-scanner/src/lib.rs +++ b/plugins/barcode-scanner/src/lib.rs @@ -9,8 +9,8 @@ #![cfg(mobile)] use tauri::{ - plugin::{Builder, PluginHandle, TauriPlugin}, Manager, Runtime, + plugin::{Builder, PluginHandle, TauriPlugin}, }; pub use models::*; diff --git a/plugins/biometric/src/error.rs b/plugins/biometric/src/error.rs index 1be5a6639..8fbd0bd11 100644 --- a/plugins/biometric/src/error.rs +++ b/plugins/biometric/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; /// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. pub type Result = std::result::Result; diff --git a/plugins/biometric/src/lib.rs b/plugins/biometric/src/lib.rs index 01303d514..03f3fc1d7 100644 --- a/plugins/biometric/src/lib.rs +++ b/plugins/biometric/src/lib.rs @@ -10,8 +10,8 @@ use serde::Serialize; use tauri::{ - plugin::{Builder, PluginHandle, TauriPlugin}, Manager, Runtime, + plugin::{Builder, PluginHandle, TauriPlugin}, }; pub use models::*; diff --git a/plugins/cli/src/lib.rs b/plugins/cli/src/lib.rs index ad133672f..7857b0518 100644 --- a/plugins/cli/src/lib.rs +++ b/plugins/cli/src/lib.rs @@ -12,8 +12,8 @@ )] use tauri::{ - plugin::{Builder, PluginApi, TauriPlugin}, AppHandle, Manager, Runtime, State, + plugin::{Builder, PluginApi, TauriPlugin}, }; mod config; diff --git a/plugins/cli/src/parser.rs b/plugins/cli/src/parser.rs index 466885f14..1c66adb52 100644 --- a/plugins/cli/src/parser.rs +++ b/plugins/cli/src/parser.rs @@ -3,9 +3,9 @@ // SPDX-License-Identifier: MIT use clap::{ + Arg as ClapArg, ArgAction, ArgMatches, Command, builder::{PossibleValue, PossibleValuesParser}, error::ErrorKind, - Arg as ClapArg, ArgAction, ArgMatches, Command, }; use serde::Serialize; use serde_json::Value; @@ -134,8 +134,8 @@ fn get_matches_internal(config: &Config, matches: &ArgMatches) -> Matches { let mut cli_matches = Matches::default(); map_matches(config, matches, &mut cli_matches); - if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() { - if let Some(subcommand_config) = config + if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() + && let Some(subcommand_config) = config .subcommands .as_ref() .and_then(|s| s.get(subcommand_name)) @@ -145,7 +145,6 @@ fn get_matches_internal(config: &Config, matches: &ArgMatches) -> Matches { get_matches_internal(subcommand_config, subcommand_matches), ); } - } cli_matches } diff --git a/plugins/clipboard-manager/src/commands.rs b/plugins/clipboard-manager/src/commands.rs index a8dd94ac0..a903e9844 100644 --- a/plugins/clipboard-manager/src/commands.rs +++ b/plugins/clipboard-manager/src/commands.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use tauri::{command, image::JsImage, AppHandle, Manager, ResourceId, Runtime, State, Webview}; +use tauri::{AppHandle, Manager, ResourceId, Runtime, State, Webview, command, image::JsImage}; use crate::{Clipboard, Result}; diff --git a/plugins/clipboard-manager/src/desktop.rs b/plugins/clipboard-manager/src/desktop.rs index b31b509cd..6cb1ca9b7 100644 --- a/plugins/clipboard-manager/src/desktop.rs +++ b/plugins/clipboard-manager/src/desktop.rs @@ -4,7 +4,7 @@ use arboard::ImageData; use serde::de::DeserializeOwned; -use tauri::{image::Image, plugin::PluginApi, AppHandle, Runtime}; +use tauri::{AppHandle, Runtime, image::Image, plugin::PluginApi}; use std::{borrow::Cow, sync::Mutex}; diff --git a/plugins/clipboard-manager/src/error.rs b/plugins/clipboard-manager/src/error.rs index 6f7e6c4d9..e945f9874 100644 --- a/plugins/clipboard-manager/src/error.rs +++ b/plugins/clipboard-manager/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; /// Alias for `Result` used throughout this crate. pub type Result = std::result::Result; diff --git a/plugins/clipboard-manager/src/lib.rs b/plugins/clipboard-manager/src/lib.rs index 84ea87e43..d08d77c2c 100644 --- a/plugins/clipboard-manager/src/lib.rs +++ b/plugins/clipboard-manager/src/lib.rs @@ -10,8 +10,8 @@ )] use tauri::{ - plugin::{Builder, TauriPlugin}, Manager, RunEvent, Runtime, + plugin::{Builder, TauriPlugin}, }; #[cfg(desktop)] diff --git a/plugins/clipboard-manager/src/mobile.rs b/plugins/clipboard-manager/src/mobile.rs index 7eae15552..bb71b6ce7 100644 --- a/plugins/clipboard-manager/src/mobile.rs +++ b/plugins/clipboard-manager/src/mobile.rs @@ -5,9 +5,9 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use tauri::{ + AppHandle, Runtime, image::Image, plugin::{PluginApi, PluginHandle}, - AppHandle, Runtime, }; use std::borrow::Cow; diff --git a/plugins/deep-link/examples/app/src-tauri/Cargo.toml b/plugins/deep-link/examples/app/src-tauri/Cargo.toml index 8fd27f851..25947bb7e 100644 --- a/plugins/deep-link/examples/app/src-tauri/Cargo.toml +++ b/plugins/deep-link/examples/app/src-tauri/Cargo.toml @@ -5,7 +5,7 @@ description = "A Tauri App" authors = ["you"] license = "" repository = "" -edition = "2021" +edition = "2024" rust-version = "1.90" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/plugins/deep-link/src/commands.rs b/plugins/deep-link/src/commands.rs index 078adfb1e..31d852f9c 100644 --- a/plugins/deep-link/src/commands.rs +++ b/plugins/deep-link/src/commands.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use tauri::{command, AppHandle, Runtime, State, Window}; +use tauri::{AppHandle, Runtime, State, Window, command}; use crate::{DeepLink, Result}; diff --git a/plugins/deep-link/src/config.rs b/plugins/deep-link/src/config.rs index 49de68e3d..94aba9092 100644 --- a/plugins/deep-link/src/config.rs +++ b/plugins/deep-link/src/config.rs @@ -50,7 +50,9 @@ impl AssociatedDomain { } if self.scheme.iter().any(|s| s == "http") && !self.scheme.iter().any(|s| s == "https") { - eprintln!("Warning: AppLink uses only 'http' — allowed on Android but not secure for production."); + eprintln!( + "Warning: AppLink uses only 'http' — allowed on Android but not secure for production." + ); } } @@ -68,13 +70,12 @@ where D: Deserializer<'de>, { let opt = Option::::deserialize(deserializer)?; - if let Some(ref host) = opt { - if let Some((scheme, _)) = host.split_once("://") { + if let Some(ref host) = opt + && let Some((scheme, _)) = host.split_once("://") { return Err(serde::de::Error::custom(format!( "host `{host}` cannot start with a scheme, please remove the `{scheme}://` prefix" ))); } - } Ok(opt) } diff --git a/plugins/deep-link/src/error.rs b/plugins/deep-link/src/error.rs index 7832995fe..30138965f 100644 --- a/plugins/deep-link/src/error.rs +++ b/plugins/deep-link/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; /// Alias for a [`Result`](std::result::Result) with the error type [`Error`]. pub type Result = std::result::Result; diff --git a/plugins/deep-link/src/lib.rs b/plugins/deep-link/src/lib.rs index 9796060a6..1b7ca9e1d 100644 --- a/plugins/deep-link/src/lib.rs +++ b/plugins/deep-link/src/lib.rs @@ -11,8 +11,8 @@ //! calling those methods returns [`Error::UnsupportedPlatform`]. use tauri::{ - plugin::{Builder, PluginApi, TauriPlugin}, AppHandle, EventId, Listener, Manager, Runtime, + plugin::{Builder, PluginApi, TauriPlugin}, }; mod commands; @@ -33,8 +33,8 @@ fn init_deep_link( let _api = api; use tauri::{ - ipc::{Channel, InvokeResponseBody}, Emitter, + ipc::{Channel, InvokeResponseBody}, }; let handle = _api.register_android_plugin(PLUGIN_IDENTIFIER, "DeepLinkPlugin")?; @@ -94,7 +94,7 @@ fn init_deep_link( #[cfg(target_os = "android")] mod imp { - use tauri::{ipc::Channel, plugin::PluginHandle, AppHandle, Runtime}; + use tauri::{AppHandle, Runtime, ipc::Channel, plugin::PluginHandle}; use serde::{Deserialize, Serialize}; @@ -172,7 +172,7 @@ mod imp { use std::sync::Mutex; #[cfg(target_os = "linux")] use std::{ - fs::{create_dir_all, File}, + fs::{File, create_dir_all}, io::Write, process::Command, }; @@ -223,7 +223,9 @@ mod imp { current.replace(vec![url.clone()]); let _ = self.app.emit("deep-link://new-url", vec![url]); } else if cfg!(debug_assertions) { - tracing::warn!("argument {url} does not match any configured deep link scheme; skipping it"); + tracing::warn!( + "argument {url} does not match any configured deep link scheme; skipping it" + ); } } } @@ -414,11 +416,10 @@ mod imp { let mimeapps_path = self.app.path().config_dir()?.join("mimeapps.list"); if mimeapps_path.exists() { let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path)?; - if let Some(section) = mimeapps.section_mut(Some("Default Applications")) { - if section.get(&mime_type).unwrap_or_default() == file_name { + if let Some(section) = mimeapps.section_mut(Some("Default Applications")) + && section.get(&mime_type).unwrap_or_default() == file_name { section.remove(&mime_type); } - } mimeapps.write_to_file(&mimeapps_path)?; } @@ -558,16 +559,16 @@ impl DeepLink { /// /// Use `get_current` on app load to check whether your app was started via a deep link. pub fn on_open_url(&self, f: F) -> EventId { - let event_id = self.app.listen("deep-link://new-url", move |event| { + + + self.app.listen("deep-link://new-url", move |event| { if let Ok(urls) = serde_json::from_str(event.payload()) { f(OpenUrlEvent { id: event.id(), urls, }) } - }); - - event_id + }) } } diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs index 2564a1aa0..1411f909c 100644 --- a/plugins/dialog/src/commands.rs +++ b/plugins/dialog/src/commands.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use tauri::{command, Manager, Runtime, State, Window}; +use tauri::{Manager, Runtime, State, Window, command}; use tauri_plugin_fs::FsExt; use crate::{ @@ -171,14 +171,13 @@ pub(crate) async fn open( ) } else { let folder = dialog_builder.blocking_pick_folder(); - if let Some(folder) = &folder { - if let Ok(path) = folder.clone().into_path() { + if let Some(folder) = &folder + && let Ok(path) = folder.clone().into_path() { if let Some(s) = window.try_fs_scope() { s.allow_directory(&path, options.recursive)?; } tauri_scope.allow_directory(&path, options.directory)?; } - } OpenResponse::Folder(folder.map(|p| p.simplified())) } } @@ -204,14 +203,13 @@ pub(crate) async fn open( let tauri_scope = window.state::(); let file = dialog_builder.blocking_pick_file(); - if let Some(file) = &file { - if let Ok(path) = file.clone().into_path() { + if let Some(file) = &file + && let Ok(path) = file.clone().into_path() { if let Some(s) = window.try_fs_scope() { s.allow_file(&path)?; } tauri_scope.allow_file(&path)?; } - } OpenResponse::File(file.map(|f| f.simplified())) }; Ok(res) @@ -246,14 +244,13 @@ pub(crate) async fn save( let tauri_scope = window.state::(); let path = dialog_builder.blocking_save_file(); - if let Some(p) = &path { - if let Ok(path) = p.clone().into_path() { + if let Some(p) = &path + && let Ok(path) = p.clone().into_path() { if let Some(s) = window.try_fs_scope() { s.allow_file(&path)?; } tauri_scope.allow_file(&path)?; } - } Ok(path.map(|p| p.simplified())) } diff --git a/plugins/dialog/src/desktop.rs b/plugins/dialog/src/desktop.rs index 8d3b08f95..3a6c27270 100644 --- a/plugins/dialog/src/desktop.rs +++ b/plugins/dialog/src/desktop.rs @@ -11,9 +11,9 @@ use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; use rfd::{AsyncFileDialog, AsyncMessageDialog}; use serde::de::DeserializeOwned; -use tauri::{plugin::PluginApi, AppHandle, Runtime}; +use tauri::{AppHandle, Runtime, plugin::PluginApi}; -use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder}; +use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder, models::*}; pub fn init( app: &AppHandle, diff --git a/plugins/dialog/src/error.rs b/plugins/dialog/src/error.rs index dac05b179..97583e535 100644 --- a/plugins/dialog/src/error.rs +++ b/plugins/dialog/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; /// Alias for `Result` used throughout this crate. pub type Result = std::result::Result; diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index 0e092af0a..7cfad33b1 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -16,8 +16,8 @@ use serde::{Deserialize, Serialize}; use tauri::{ - plugin::{Builder, TauriPlugin}, Manager, Runtime, + plugin::{Builder, TauriPlugin}, }; use std::{ diff --git a/plugins/dialog/src/mobile.rs b/plugins/dialog/src/mobile.rs index 46ea3a276..7718af21e 100644 --- a/plugins/dialog/src/mobile.rs +++ b/plugins/dialog/src/mobile.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{de::DeserializeOwned, Deserialize}; +use serde::{Deserialize, de::DeserializeOwned}; use tauri::{ - plugin::{PluginApi, PluginHandle}, AppHandle, Runtime, + plugin::{PluginApi, PluginHandle}, }; use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogResult}; diff --git a/plugins/fs/src/android.rs b/plugins/fs/src/android.rs index d00b4fc52..a914b4d9c 100644 --- a/plugins/fs/src/android.rs +++ b/plugins/fs/src/android.rs @@ -3,9 +3,9 @@ // SPDX-License-Identifier: MIT use serde::de::DeserializeOwned; -use tauri::{plugin::PluginApi, AppHandle, Runtime}; +use tauri::{AppHandle, Runtime, plugin::PluginApi}; -use crate::{models::*, FilePath, OpenOptions}; +use crate::{FilePath, OpenOptions, models::*}; const PLUGIN_IDENTIFIER: &str = "com.plugin.fs"; diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs index 00cf98ad3..9065efa7c 100644 --- a/plugins/fs/src/commands.rs +++ b/plugins/fs/src/commands.rs @@ -6,10 +6,10 @@ use serde::{Deserialize, Serialize, Serializer}; use serde_repr::{Deserialize_repr, Serialize_repr}; use tauri::{ + Manager, Resource, ResourceId, Runtime, Webview, ipc::{CommandScope, GlobalScope}, path::BaseDirectory, utils::config::FsScope, - Manager, Resource, ResourceId, Runtime, Webview, }; use std::{ @@ -23,7 +23,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use crate::{scope::Entry, Error, SafeFilePath}; +use crate::{Error, SafeFilePath, scope::Entry}; #[derive(Debug, thiserror::Error)] pub enum CommandError { @@ -175,7 +175,9 @@ impl Drop for FileHandle { .stop_accessing_security_scoped_resource(FilePath::Url(url.clone())); security_scoped_resources.remove(url.as_str()); } else { - log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)"); + log::debug!( + "Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)" + ); } } } @@ -251,7 +253,9 @@ impl Drop for PathHandle { .stop_accessing_security_scoped_resource(FilePath::Url(url.clone())); security_scoped_resources.remove(url.as_str()); } else { - log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)"); + log::debug!( + "Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)" + ); } } } @@ -1530,7 +1534,10 @@ pub fn resolve_path( unsafe { let success = ns_url.startAccessingSecurityScopedResource(); if success { - log::debug!("Started accessing security-scoped resource for URL: {} (via resolve_path)", url.as_str()); + log::debug!( + "Started accessing security-scoped resource for URL: {} (via resolve_path)", + url.as_str() + ); // Track it so we know to clean it up security_scoped_resources.track_manually(url.as_str().to_string()); } else { @@ -1541,10 +1548,16 @@ pub fn resolve_path( } } } else { - log::debug!("Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url.as_str()); + log::debug!( + "Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", + url.as_str() + ); } } else { - log::debug!("Security-scoped resource already active for URL: {} (started via start_accessing_security_scoped_resource), skipping", url.as_str()); + log::debug!( + "Security-scoped resource already active for URL: {} (started via start_accessing_security_scoped_resource), skipping", + url.as_str() + ); } } } diff --git a/plugins/fs/src/file_path.rs b/plugins/fs/src/file_path.rs index 6316a2489..7252b0d3a 100644 --- a/plugins/fs/src/file_path.rs +++ b/plugins/fs/src/file_path.rs @@ -190,11 +190,10 @@ impl<'de> serde::Deserialize<'de> for SafeFilePath { impl FromStr for FilePath { type Err = Infallible; fn from_str(s: &str) -> std::result::Result { - if let Ok(url) = url::Url::from_str(s) { - if url.scheme().len() != 1 { + if let Ok(url) = url::Url::from_str(s) + && url.scheme().len() != 1 { return Ok(Self::Url(url)); } - } Ok(Self::Path(PathBuf::from(s))) } } @@ -202,11 +201,10 @@ impl FromStr for FilePath { impl FromStr for SafeFilePath { type Err = Error; fn from_str(s: &str) -> Result { - if let Ok(url) = url::Url::from_str(s) { - if url.scheme().len() != 1 { + if let Ok(url) = url::Url::from_str(s) + && url.scheme().len() != 1 { return Ok(Self::Url(url)); } - } SafePathBuf::new(s.into()) .map(SafeFilePath::Path) diff --git a/plugins/fs/src/ios.rs b/plugins/fs/src/ios.rs index fa8936181..7c1fc271b 100644 --- a/plugins/fs/src/ios.rs +++ b/plugins/fs/src/ios.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use serde::de::DeserializeOwned; -use tauri::{plugin::PluginApi, AppHandle, Runtime}; +use tauri::{AppHandle, Runtime, plugin::PluginApi}; use crate::{FilePath, OpenOptions}; @@ -69,7 +69,10 @@ impl Fs { } } } else { - log::debug!("Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url_string); + log::debug!( + "Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", + url_string + ); } // Convert URL to path and open the file diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index 7f57f11cb..ed7854c21 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -22,10 +22,10 @@ use std::sync::Mutex; use serde::Deserialize; use tauri::{ + AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent, ipc::ScopeObject, plugin::{Builder as PluginBuilder, TauriPlugin}, utils::{acl::Value, config::FsScope}, - AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent, }; #[cfg(target_os = "android")] diff --git a/plugins/fs/src/watcher.rs b/plugins/fs/src/watcher.rs index 68f6a8019..fbe1aa55e 100644 --- a/plugins/fs/src/watcher.rs +++ b/plugins/fs/src/watcher.rs @@ -3,20 +3,20 @@ // SPDX-License-Identifier: MIT use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; -use notify_debouncer_full::{new_debouncer, DebouncedEvent, Debouncer, RecommendedCache}; +use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer}; use serde::Deserialize; use tauri::{ + Manager, Resource, ResourceId, Runtime, Webview, ipc::{Channel, CommandScope, GlobalScope}, path::BaseDirectory, - Manager, Resource, ResourceId, Runtime, Webview, }; use std::time::Duration; use crate::{ - commands::{resolve_path, CommandResult}, - scope::Entry, SafeFilePath, + commands::{CommandResult, resolve_path}, + scope::Entry, }; #[allow(unused)] diff --git a/plugins/geolocation/src/commands.rs b/plugins/geolocation/src/commands.rs index d2cae848f..8da077850 100644 --- a/plugins/geolocation/src/commands.rs +++ b/plugins/geolocation/src/commands.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use tauri::{command, ipc::Channel, AppHandle, Runtime}; +use tauri::{AppHandle, Runtime, command, ipc::Channel}; use crate::{GeolocationExt, PermissionStatus, PermissionType, Position, PositionOptions, Result}; diff --git a/plugins/geolocation/src/desktop.rs b/plugins/geolocation/src/desktop.rs index 3ca41db5f..70b288033 100644 --- a/plugins/geolocation/src/desktop.rs +++ b/plugins/geolocation/src/desktop.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use tauri::{ + AppHandle, Runtime, ipc::{Channel, InvokeResponseBody}, plugin::PluginApi, - AppHandle, Runtime, }; use crate::models::*; diff --git a/plugins/geolocation/src/error.rs b/plugins/geolocation/src/error.rs index 1c4f69517..c8e959458 100644 --- a/plugins/geolocation/src/error.rs +++ b/plugins/geolocation/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; /// Alias for the result type returned by the geolocation APIs. pub type Result = std::result::Result; diff --git a/plugins/geolocation/src/lib.rs b/plugins/geolocation/src/lib.rs index 2c87c13f1..2c70a04dd 100644 --- a/plugins/geolocation/src/lib.rs +++ b/plugins/geolocation/src/lib.rs @@ -9,8 +9,8 @@ //! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/2.0.0-rc.25/specta/attr.specta.html) on structs like [`PermissionStatus`], [`PositionOptions`]. use tauri::{ - plugin::{Builder, TauriPlugin}, Manager, Runtime, + plugin::{Builder, TauriPlugin}, }; pub use models::*; diff --git a/plugins/geolocation/src/mobile.rs b/plugins/geolocation/src/mobile.rs index 4de9676a9..9e1262dd6 100644 --- a/plugins/geolocation/src/mobile.rs +++ b/plugins/geolocation/src/mobile.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use tauri::{ + AppHandle, Runtime, ipc::{Channel, InvokeResponseBody}, plugin::{PluginApi, PluginHandle}, - AppHandle, Runtime, }; use crate::models::*; diff --git a/plugins/global-shortcut/src/lib.rs b/plugins/global-shortcut/src/lib.rs index 19a4fa121..f65c1de1a 100644 --- a/plugins/global-shortcut/src/lib.rs +++ b/plugins/global-shortcut/src/lib.rs @@ -22,14 +22,14 @@ use std::{ use global_hotkey::GlobalHotKeyEvent; pub use global_hotkey::{ - hotkey::{Code, HotKey as Shortcut, Modifiers}, GlobalHotKeyEvent as ShortcutEvent, HotKeyState as ShortcutState, + hotkey::{Code, HotKey as Shortcut, Modifiers}, }; use serde::Serialize; use tauri::{ + AppHandle, Manager, Runtime, State, ipc::Channel, plugin::{Builder as PluginBuilder, TauriPlugin}, - AppHandle, Manager, Runtime, State, }; mod error; diff --git a/plugins/haptics/src/commands.rs b/plugins/haptics/src/commands.rs index 76f097c09..f91313b2a 100644 --- a/plugins/haptics/src/commands.rs +++ b/plugins/haptics/src/commands.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use tauri::{command, AppHandle, Runtime}; +use tauri::{AppHandle, Runtime, command}; use crate::{HapticsExt, ImpactFeedbackStyle, NotificationFeedbackType, Result}; diff --git a/plugins/haptics/src/desktop.rs b/plugins/haptics/src/desktop.rs index d036eddad..6011a606c 100644 --- a/plugins/haptics/src/desktop.rs +++ b/plugins/haptics/src/desktop.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use serde::de::DeserializeOwned; -use tauri::{plugin::PluginApi, AppHandle, Runtime}; +use tauri::{AppHandle, Runtime, plugin::PluginApi}; use crate::models::*; diff --git a/plugins/haptics/src/error.rs b/plugins/haptics/src/error.rs index 3a24755e8..faed672af 100644 --- a/plugins/haptics/src/error.rs +++ b/plugins/haptics/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; /// Alias for a [`std::result::Result`] with the error type [`Error`]. pub type Result = std::result::Result; diff --git a/plugins/haptics/src/lib.rs b/plugins/haptics/src/lib.rs index 7984bebb0..417343ac6 100644 --- a/plugins/haptics/src/lib.rs +++ b/plugins/haptics/src/lib.rs @@ -9,8 +9,8 @@ //! - **specta**: Add support for [`specta::specta`](https://docs.rs/specta/2.0.0-rc.25/specta/attr.specta.html) on structs like [`ImpactFeedbackStyle`], [`NotificationFeedbackType`]. use tauri::{ - plugin::{Builder, TauriPlugin}, Manager, Runtime, + plugin::{Builder, TauriPlugin}, }; pub use models::*; diff --git a/plugins/haptics/src/mobile.rs b/plugins/haptics/src/mobile.rs index 42ae2459a..e3446eb46 100644 --- a/plugins/haptics/src/mobile.rs +++ b/plugins/haptics/src/mobile.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use tauri::{ - plugin::{PluginApi, PluginHandle}, AppHandle, Runtime, + plugin::{PluginApi, PluginHandle}, }; use crate::models::*; diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index 4387bae30..e6c4d48db 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -4,20 +4,20 @@ use std::{future::Future, pin::Pin, str::FromStr, sync::Arc, time::Duration}; -use http::{header, HeaderMap, HeaderName, HeaderValue, Method, StatusCode}; -use reqwest::{redirect::Policy, NoProxy}; +use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, header}; +use reqwest::{NoProxy, redirect::Policy}; use serde::{Deserialize, Serialize}; use tauri::{ + Manager, ResourceId, ResourceTable, Runtime, State, Webview, async_runtime::Mutex, command, ipc::{CommandScope, GlobalScope}, - Manager, ResourceId, ResourceTable, Runtime, State, Webview, }; -use tokio::sync::oneshot::{channel, Receiver, Sender}; +use tokio::sync::oneshot::{Receiver, Sender, channel}; use crate::{ - scope::{Entry, Scope}, Error, Http, Result, + scope::{Entry, Scope}, }; const HTTP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -257,8 +257,12 @@ pub async fn fetch( if is_unsafe_header(&name) { #[cfg(debug_assertions)] { - eprintln!("[\x1b[33mWARNING\x1b[0m] Skipping {name} header as it is a forbidden header per fetch spec https://fetch.spec.whatwg.org/#terminology-headers"); - eprintln!("[\x1b[33mWARNING\x1b[0m] if keeping the header is a desired behavior, you can enable `unsafe-headers` feature flag in your Cargo.toml"); + eprintln!( + "[\x1b[33mWARNING\x1b[0m] Skipping {name} header as it is a forbidden header per fetch spec https://fetch.spec.whatwg.org/#terminology-headers" + ); + eprintln!( + "[\x1b[33mWARNING\x1b[0m] if keeping the header is a desired behavior, you can enable `unsafe-headers` feature flag in your Cargo.toml" + ); } continue; } @@ -294,7 +298,9 @@ pub async fn fetch( { #[cfg(debug_assertions)] { - eprintln!("[\x1b[33mWARNING\x1b[0m] using dangerous settings requires `dangerous-settings` feature flag in your Cargo.toml"); + eprintln!( + "[\x1b[33mWARNING\x1b[0m] using dangerous settings requires `dangerous-settings` feature flag in your Cargo.toml" + ); } let _ = danger_config; return Err(Error::DangerousSettings); @@ -342,8 +348,8 @@ pub async fn fetch( } // ensure we have an Origin header set - if cfg!(not(feature = "unsafe-headers")) || !headers.contains_key(header::ORIGIN) { - if let Ok(url) = webview.url() { + if (cfg!(not(feature = "unsafe-headers")) || !headers.contains_key(header::ORIGIN)) + && let Ok(url) = webview.url() { // The url crate returns OpaqueOrigin for tauri://localhost which serializes to "null" let origin = if url.scheme() == "tauri" { "tauri://localhost".to_string() @@ -352,7 +358,6 @@ pub async fn fetch( }; headers.append(header::ORIGIN, HeaderValue::from_str(&origin)?); } - } // In case empty origin is passed, remove it. Some services do not like Origin header // so this way we can remove it in explicit way. The default behaviour is still to set it @@ -593,8 +598,10 @@ mod tests { Some(location) => format!( "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" ), - None => "HTTP/1.1 200 OK\r\nContent-Length: 6\r\nConnection: close\r\n\r\nsecret" - .to_string(), + None => { + "HTTP/1.1 200 OK\r\nContent-Length: 6\r\nConnection: close\r\n\r\nsecret" + .to_string() + } }; let _ = stream.write_all(response.as_bytes()); diff --git a/plugins/http/src/lib.rs b/plugins/http/src/lib.rs index 6444ef99e..e6db850f0 100644 --- a/plugins/http/src/lib.rs +++ b/plugins/http/src/lib.rs @@ -61,8 +61,8 @@ pub use reqwest; use tauri::{ - plugin::{Builder, TauriPlugin}, Manager, Runtime, + plugin::{Builder, TauriPlugin}, }; pub use config::Config; diff --git a/plugins/http/src/reqwest_cookie_store.rs b/plugins/http/src/reqwest_cookie_store.rs index 0b71902bd..ef389807b 100644 --- a/plugins/http/src/reqwest_cookie_store.rs +++ b/plugins/http/src/reqwest_cookie_store.rs @@ -6,7 +6,7 @@ use std::{ path::PathBuf, - sync::{mpsc::Receiver, Mutex}, + sync::{Mutex, mpsc::Receiver}, }; use cookie_store::{CookieStore, RawCookie, RawCookieParseError}; diff --git a/plugins/http/src/scope.rs b/plugins/http/src/scope.rs index a7745c2cd..c39540e75 100644 --- a/plugins/http/src/scope.rs +++ b/plugins/http/src/scope.rs @@ -154,11 +154,13 @@ mod tests { assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/file.png#head".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/assets/file.png".parse().unwrap())); - assert!(scope.is_allowed( - &"http://localhost:8080/assets/file.png?width=100&height=200" - .parse() - .unwrap() - )); + assert!( + scope.is_allowed( + &"http://localhost:8080/assets/file.png?width=100&height=200" + .parse() + .unwrap() + ) + ); assert!(!scope.is_allowed(&"http://localhost:8080/file.jpeg".parse().unwrap())); } @@ -172,11 +174,13 @@ mod tests { assert!(scope.is_allowed(&"http://something.else#tauri".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else?rel=tauri".parse().unwrap())); - assert!(scope.is_allowed( - &"http://something.else/path/to/file.mp4?start=500" - .parse() - .unwrap() - )); + assert!( + scope.is_allowed( + &"http://something.else/path/to/file.mp4?start=500" + .parse() + .unwrap() + ) + ); assert!(!scope.is_allowed(&"https://something.else".parse().unwrap())); diff --git a/plugins/localhost/src/lib.rs b/plugins/localhost/src/lib.rs index aba034b94..5641f589b 100644 --- a/plugins/localhost/src/lib.rs +++ b/plugins/localhost/src/lib.rs @@ -15,8 +15,8 @@ use std::collections::HashMap; use http::Uri; use tauri::{ - plugin::{Builder as PluginBuilder, TauriPlugin}, Runtime, + plugin::{Builder as PluginBuilder, TauriPlugin}, }; use tiny_http::{Header, Response as HttpResponse, Server}; diff --git a/plugins/log/src/lib.rs b/plugins/log/src/lib.rs index 18e872758..a6740efd4 100644 --- a/plugins/log/src/lib.rs +++ b/plugins/log/src/lib.rs @@ -27,12 +27,12 @@ use std::{ iter::FromIterator, path::{Path, PathBuf}, }; -use tauri::{ - plugin::{self, TauriPlugin}, - Manager, Runtime, -}; use tauri::{AppHandle, Emitter}; -use time::{macros::format_description, OffsetDateTime}; +use tauri::{ + Manager, Runtime, + plugin::{self, TauriPlugin}, +}; +use time::{OffsetDateTime, macros::format_description}; pub use fern; pub use log; diff --git a/plugins/nfc/src/error.rs b/plugins/nfc/src/error.rs index 75d21ea92..51811c6da 100644 --- a/plugins/nfc/src/error.rs +++ b/plugins/nfc/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; /// Alias for a [`std::result::Result`] with the error type [`Error`]. pub type Result = std::result::Result; diff --git a/plugins/nfc/src/lib.rs b/plugins/nfc/src/lib.rs index 0396b7733..1bfb1b3a7 100644 --- a/plugins/nfc/src/lib.rs +++ b/plugins/nfc/src/lib.rs @@ -14,8 +14,8 @@ use serde::{Deserialize, Serialize}; use tauri::{ - plugin::{Builder, PluginHandle, TauriPlugin}, Manager, Runtime, + plugin::{Builder, PluginHandle, TauriPlugin}, }; pub use models::*; diff --git a/plugins/notification/src/commands.rs b/plugins/notification/src/commands.rs index 99b96c5b0..c38a842e2 100644 --- a/plugins/notification/src/commands.rs +++ b/plugins/notification/src/commands.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use tauri::{command, plugin::PermissionState, AppHandle, Runtime, State}; +use tauri::{AppHandle, Runtime, State, command, plugin::PermissionState}; use crate::{Notification, NotificationData, Result}; diff --git a/plugins/notification/src/desktop.rs b/plugins/notification/src/desktop.rs index cd8bddfb9..b86b74f2a 100644 --- a/plugins/notification/src/desktop.rs +++ b/plugins/notification/src/desktop.rs @@ -4,8 +4,8 @@ use serde::de::DeserializeOwned; use tauri::{ - plugin::{PermissionState, PluginApi}, AppHandle, Runtime, + plugin::{PermissionState, PluginApi}, }; use crate::NotificationBuilder; diff --git a/plugins/notification/src/error.rs b/plugins/notification/src/error.rs index f43da5823..ff5305bef 100644 --- a/plugins/notification/src/error.rs +++ b/plugins/notification/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{ser::Serializer, Serialize}; +use serde::{Serialize, ser::Serializer}; /// Alias for a [`std::result::Result`] with the error type set to [`Error`]. pub type Result = std::result::Result; diff --git a/plugins/notification/src/lib.rs b/plugins/notification/src/lib.rs index 9fd74406a..61042aeec 100644 --- a/plugins/notification/src/lib.rs +++ b/plugins/notification/src/lib.rs @@ -14,13 +14,13 @@ )] use serde::Serialize; -#[cfg(mobile)] -use tauri::plugin::PluginHandle; #[cfg(desktop)] use tauri::AppHandle; +#[cfg(mobile)] +use tauri::plugin::PluginHandle; use tauri::{ - plugin::{Builder, TauriPlugin}, Manager, Runtime, + plugin::{Builder, TauriPlugin}, }; pub use models::*; diff --git a/plugins/notification/src/mobile.rs b/plugins/notification/src/mobile.rs index 250532d4a..722518503 100644 --- a/plugins/notification/src/mobile.rs +++ b/plugins/notification/src/mobile.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{de::DeserializeOwned, Deserialize}; +use serde::{Deserialize, de::DeserializeOwned}; use tauri::{ - plugin::{PermissionState, PluginApi, PluginHandle}, AppHandle, Runtime, + plugin::{PermissionState, PluginApi, PluginHandle}, }; use crate::models::*; diff --git a/plugins/notification/src/models.rs b/plugins/notification/src/models.rs index ce5993a0f..b146dcb89 100644 --- a/plugins/notification/src/models.rs +++ b/plugins/notification/src/models.rs @@ -4,7 +4,7 @@ use std::{collections::HashMap, fmt::Display}; -use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError}; use url::Url; @@ -188,11 +188,11 @@ pub enum Schedule { // custom ISO-8601 serialization that does not use 6 digits for years. mod iso8601 { - use serde::{ser::Error as _, Serialize, Serializer}; + use serde::{Serialize, Serializer, ser::Error as _}; use time::{ - format_description::well_known::iso8601::{Config, EncodedConfig}, - format_description::well_known::Iso8601, OffsetDateTime, + format_description::well_known::Iso8601, + format_description::well_known::iso8601::{Config, EncodedConfig}, }; const SERDE_CONFIG: EncodedConfig = Config::DEFAULT.encode(); diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index e22b41d41..af66d9726 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -5,11 +5,11 @@ use std::path::{Path, PathBuf}; use tauri::{ - ipc::{CommandScope, GlobalScope}, AppHandle, Runtime, + ipc::{CommandScope, GlobalScope}, }; -use crate::{scope::Scope, Error, OpenerExt}; +use crate::{Error, OpenerExt, scope::Scope}; #[tauri::command] pub async fn open_url( diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 13f6efb85..5354c5fd3 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -13,7 +13,7 @@ use std::path::Path; -use tauri::{plugin::TauriPlugin, Manager, Runtime}; +use tauri::{Manager, Runtime, plugin::TauriPlugin}; #[cfg(mobile)] use tauri::plugin::PluginHandle; diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 9578a2823..1b593cf6b 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -93,18 +93,18 @@ mod imp { use windows::Win32::UI::Shell::Common::ITEMIDLIST; use windows::{ - core::{w, HSTRING, PCWSTR}, Win32::{ Foundation::ERROR_FILE_NOT_FOUND, System::Com::CoInitialize, UI::{ Shell::{ - ILCreateFromPathW, ILFree, SHOpenFolderAndSelectItems, ShellExecuteExW, - SHELLEXECUTEINFOW, + ILCreateFromPathW, ILFree, SHELLEXECUTEINFOW, SHOpenFolderAndSelectItems, + ShellExecuteExW, }, WindowsAndMessaging::SW_SHOWNORMAL, }, }, + core::{HSTRING, PCWSTR, w}, }; pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { diff --git a/plugins/opener/src/scope.rs b/plugins/opener/src/scope.rs index 275913d1b..393928b79 100644 --- a/plugins/opener/src/scope.rs +++ b/plugins/opener/src/scope.rs @@ -8,9 +8,9 @@ use std::{ sync::Arc, }; -use tauri::{ipc::ScopeObject, utils::acl::Value, AppHandle, Manager, Runtime}; +use tauri::{AppHandle, Manager, Runtime, ipc::ScopeObject, utils::acl::Value}; -use crate::{scope_entry::EntryRaw, Error}; +use crate::{Error, scope_entry::EntryRaw}; pub use crate::scope_entry::Application; diff --git a/plugins/opener/src/windows_shell_path.rs b/plugins/opener/src/windows_shell_path.rs index 8715641db..f3cb61529 100644 --- a/plugins/opener/src/windows_shell_path.rs +++ b/plugins/opener/src/windows_shell_path.rs @@ -10,7 +10,7 @@ use std::{ path::{Component, Path, PathBuf, Prefix, PrefixComponent}, }; -use windows::{core::HSTRING, Win32::Storage::FileSystem::GetFullPathNameW}; +use windows::{Win32::Storage::FileSystem::GetFullPathNameW, core::HSTRING}; pub fn absolute_and_check_exists(path: &Path) -> io::Result { let path = absolute(path)?; diff --git a/plugins/os/src/lib.rs b/plugins/os/src/lib.rs index 853dbdf69..12350c4de 100644 --- a/plugins/os/src/lib.rs +++ b/plugins/os/src/lib.rs @@ -12,10 +12,10 @@ use std::fmt::Display; pub use os_info::Version; -use serialize_to_javascript::{default_template, DefaultTemplate, Template}; +use serialize_to_javascript::{DefaultTemplate, Template, default_template}; use tauri::{ - plugin::{Builder, TauriPlugin}, Runtime, + plugin::{Builder, TauriPlugin}, }; mod commands; diff --git a/plugins/persisted-scope/src/lib.rs b/plugins/persisted-scope/src/lib.rs index 96331651e..57f84979d 100644 --- a/plugins/persisted-scope/src/lib.rs +++ b/plugins/persisted-scope/src/lib.rs @@ -17,13 +17,13 @@ use aho_corasick::AhoCorasick; use serde::{Deserialize, Serialize}; use tauri::{ - plugin::{Builder, TauriPlugin}, Manager, Runtime, + plugin::{Builder, TauriPlugin}, }; use tauri_plugin_fs::FsExt; use std::{ - fs::{create_dir_all, File}, + fs::{File, create_dir_all}, io::Write, path::Path, }; @@ -198,8 +198,8 @@ pub fn init() -> TauriPlugin { // We will still save some semi-broken values because the scope events are quite spammy and we don't want to reduce runtime performance any further. let ac = AhoCorasick::new(PATTERNS).unwrap(/* This should be impossible to fail since we're using a small static input */); - if let Some(fs_scope) = &fs_scope { - if fs_scope_state_path.exists() { + if let Some(fs_scope) = &fs_scope + && fs_scope_state_path.exists() { let scope: Scope = std::fs::read(&fs_scope_state_path) .map_err(Error::from) .and_then(|scope| bincode::deserialize(&scope).map_err(Into::into)) @@ -218,7 +218,6 @@ pub fn init() -> TauriPlugin { // This is needed to fix broken .peristed-scope files in case the app doesn't update the scope itself. save_scopes(fs_scope, &app_dir, &fs_scope_state_path); } - } #[cfg(feature = "protocol-asset")] if asset_scope_state_path.exists() { diff --git a/plugins/positioner/src/lib.rs b/plugins/positioner/src/lib.rs index fd5e82638..a437ffe36 100644 --- a/plugins/positioner/src/lib.rs +++ b/plugins/positioner/src/lib.rs @@ -21,12 +21,12 @@ mod ext; pub use ext::*; use tauri::{ - plugin::{self, TauriPlugin}, Result, Runtime, + plugin::{self, TauriPlugin}, }; #[cfg(feature = "tray-icon")] -use tauri::{tray::TrayIconEvent, AppHandle, Manager, PhysicalPosition, PhysicalSize}; +use tauri::{AppHandle, Manager, PhysicalPosition, PhysicalSize, tray::TrayIconEvent}; #[cfg(feature = "tray-icon")] struct Tray(std::sync::Mutex, PhysicalSize)>>); diff --git a/plugins/process/src/lib.rs b/plugins/process/src/lib.rs index d7f05f481..f122ef0b3 100644 --- a/plugins/process/src/lib.rs +++ b/plugins/process/src/lib.rs @@ -10,8 +10,8 @@ )] use tauri::{ - plugin::{Builder, TauriPlugin}, Runtime, + plugin::{Builder, TauriPlugin}, }; mod commands; diff --git a/plugins/shell/src/commands.rs b/plugins/shell/src/commands.rs index 0facce719..2d1a73edf 100644 --- a/plugins/shell/src/commands.rs +++ b/plugins/shell/src/commands.rs @@ -7,16 +7,16 @@ use std::{collections::HashMap, future::Future, path::PathBuf, pin::Pin, string: use encoding_rs::Encoding; use serde::{Deserialize, Serialize}; use tauri::{ - ipc::{Channel, CommandScope, GlobalScope}, Manager, Runtime, State, Window, + ipc::{Channel, CommandScope, GlobalScope}, }; #[allow(deprecated)] use crate::open::Program; use crate::{ + Shell, process::{CommandEvent, TerminatedPayload}, scope::ExecuteArgs, - Shell, }; type ChildId = u32; diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index 2615b12f4..dd5967456 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -19,8 +19,8 @@ use std::{ use process::{Command, CommandChild}; use regex::Regex; use tauri::{ - plugin::{Builder, TauriPlugin}, AppHandle, Manager, RunEvent, Runtime, + plugin::{Builder, TauriPlugin}, }; mod commands; diff --git a/plugins/shell/src/process/mod.rs b/plugins/shell/src/process/mod.rs index 7a1c2db24..45e165e61 100644 --- a/plugins/shell/src/process/mod.rs +++ b/plugins/shell/src/process/mod.rs @@ -20,10 +20,10 @@ use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x0800_0000; const NEWLINE_BYTE: u8 = b'\n'; -use tauri::async_runtime::{block_on as block_on_task, channel, Receiver, Sender}; +use tauri::async_runtime::{Receiver, Sender, block_on as block_on_task, channel}; pub use encoding_rs::Encoding; -use os_pipe::{pipe, PipeReader, PipeWriter}; +use os_pipe::{PipeReader, PipeWriter, pipe}; use serde::Serialize; use shared_child::SharedChild; use tauri::utils::platform; diff --git a/plugins/shell/src/scope.rs b/plugins/shell/src/scope.rs index 35fdeaffb..cbc096c43 100644 --- a/plugins/shell/src/scope.rs +++ b/plugins/shell/src/scope.rs @@ -9,8 +9,8 @@ use crate::open::Program; use crate::process::Command; use regex::Regex; -use tauri::ipc::ScopeObject; use tauri::Manager; +use tauri::ipc::ScopeObject; /// Allowed representation of `Execute` command arguments. #[derive(Debug, Clone, serde::Deserialize)] @@ -162,8 +162,8 @@ pub enum Error { /// The sidecar program validated but failed to find the sidecar path. #[error( - "The scoped sidecar command was validated, but failed to create the path to the command: {0}" - )] + "The scoped sidecar command was validated, but failed to create the path to the command: {0}" + )] Sidecar(String), /// The named command was not found in the scoped config. @@ -172,12 +172,14 @@ pub enum Error { /// A command variable has no value set in the arguments. #[error( - "Scoped command argument at position {0} must match regex validation {1} but it was not found" - )] + "Scoped command argument at position {0} must match regex validation {1} but it was not found" + )] MissingVar(usize, String), /// At least one argument did not pass input validation. - #[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")] + #[error( + "Scoped command argument at position {index} was found, but failed regex validation {validation}" + )] Validation { /// Index of the variable. index: usize, @@ -214,7 +216,9 @@ impl OpenScope { }); } } else { - log::warn!("open() command called but the plugin configuration denies calls from JavaScript; set `tauri.conf.json > plugins > shell > open` to true or a validation regex string"); + log::warn!( + "open() command called but the plugin configuration denies calls from JavaScript; set `tauri.conf.json > plugins > shell > open` to true or a validation regex string" + ); return Err(Error::Validation { index: 0, validation: "tauri^".to_string(), // purposefully impossible regex diff --git a/plugins/shell/src/scope_entry.rs b/plugins/shell/src/scope_entry.rs index 8a70fff79..61eff93cc 100644 --- a/plugins/shell/src/scope_entry.rs +++ b/plugins/shell/src/scope_entry.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{de::Error as DeError, Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, de::Error as DeError}; use std::path::PathBuf; diff --git a/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml b/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml index 59624cbc4..816aac04b 100644 --- a/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml +++ b/plugins/single-instance/examples/vanilla/src-tauri/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "A Tauri App" authors = ["You"] repository = "" -edition = "2021" +edition = "2024" rust-version = "1.90" [dependencies] diff --git a/plugins/single-instance/src/lib.rs b/plugins/single-instance/src/lib.rs index 9a254a517..eaf358486 100644 --- a/plugins/single-instance/src/lib.rs +++ b/plugins/single-instance/src/lib.rs @@ -15,7 +15,7 @@ )] #![cfg(not(any(target_os = "android", target_os = "ios")))] -use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime}; +use tauri::{AppHandle, Manager, Runtime, plugin::TauriPlugin}; #[cfg(target_os = "windows")] #[path = "platform_impl/windows.rs"] diff --git a/plugins/single-instance/src/platform_impl/linux.rs b/plugins/single-instance/src/platform_impl/linux.rs index 892baee7f..0cc7e8371 100644 --- a/plugins/single-instance/src/platform_impl/linux.rs +++ b/plugins/single-instance/src/platform_impl/linux.rs @@ -7,8 +7,8 @@ use crate::semver_compat::semver_compat_string; use crate::SingleInstanceCallback; use tauri::{ - plugin::{self, TauriPlugin}, AppHandle, Manager, RunEvent, Runtime, + plugin::{self, TauriPlugin}, }; use zbus::{blocking::Connection, interface, names::WellKnownName}; @@ -101,12 +101,11 @@ pub fn init( } pub fn destroy>(manager: &M) { - if let Some(connection) = manager.try_state::() { - if let Some(dbus_name) = manager + if let Some(connection) = manager.try_state::() + && let Some(dbus_name) = manager .try_state::() .and_then(|name| WellKnownName::try_from(name.0.clone()).ok()) { let _ = connection.0.release_name(dbus_name); } - } } diff --git a/plugins/single-instance/src/platform_impl/macos.rs b/plugins/single-instance/src/platform_impl/macos.rs index bd2c74a7e..ccd66f360 100644 --- a/plugins/single-instance/src/platform_impl/macos.rs +++ b/plugins/single-instance/src/platform_impl/macos.rs @@ -8,12 +8,12 @@ use std::{ path::PathBuf, }; +use crate::SingleInstanceCallback; #[cfg(feature = "semver")] use crate::semver_compat::semver_compat_string; -use crate::SingleInstanceCallback; use tauri::{ - plugin::{self, TauriPlugin}, AppHandle, Config, Manager, RunEvent, Runtime, + plugin::{self, TauriPlugin}, }; use tokio::io::AsyncReadExt; diff --git a/plugins/single-instance/src/platform_impl/windows.rs b/plugins/single-instance/src/platform_impl/windows.rs index 196f7851f..075845c59 100644 --- a/plugins/single-instance/src/platform_impl/windows.rs +++ b/plugins/single-instance/src/platform_impl/windows.rs @@ -8,20 +8,20 @@ use crate::semver_compat::semver_compat_string; use crate::SingleInstanceCallback; use std::ffi::CStr; use tauri::{ - plugin::{self, TauriPlugin}, AppHandle, Manager, RunEvent, Runtime, + plugin::{self, TauriPlugin}, }; use windows_sys::Win32::{ - Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HWND, LPARAM, LRESULT, WPARAM}, + Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GetLastError, HWND, LPARAM, LRESULT, WPARAM}, System::{ DataExchange::COPYDATASTRUCT, LibraryLoader::GetModuleHandleW, Threading::{CreateMutexW, ReleaseMutex}, }, UI::WindowsAndMessaging::{ - self as w32wm, AllowSetForegroundWindow, CreateWindowExW, DefWindowProcW, DestroyWindow, - FindWindowW, GetWindowThreadProcessId, RegisterClassExW, SendMessageW, CREATESTRUCTW, - GWLP_USERDATA, GWL_STYLE, WINDOW_LONG_PTR_INDEX, WM_COPYDATA, WM_CREATE, WM_DESTROY, + self as w32wm, AllowSetForegroundWindow, CREATESTRUCTW, CreateWindowExW, DefWindowProcW, + DestroyWindow, FindWindowW, GWL_STYLE, GWLP_USERDATA, GetWindowThreadProcessId, + RegisterClassExW, SendMessageW, WINDOW_LONG_PTR_INDEX, WM_COPYDATA, WM_CREATE, WM_DESTROY, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, }, diff --git a/plugins/sql/src/commands.rs b/plugins/sql/src/commands.rs index 760d00b2d..89b89bea3 100644 --- a/plugins/sql/src/commands.rs +++ b/plugins/sql/src/commands.rs @@ -5,7 +5,7 @@ use indexmap::IndexMap; use serde_json::Value as JsonValue; use sqlx::migrate::Migrator; -use tauri::{command, AppHandle, Runtime, State}; +use tauri::{AppHandle, Runtime, State, command}; use crate::{DbInstances, DbPool, Error, LastInsertId, Migrations}; diff --git a/plugins/sql/src/decode/mysql.rs b/plugins/sql/src/decode/mysql.rs index 53fd20c7c..c8eaeffbd 100644 --- a/plugins/sql/src/decode/mysql.rs +++ b/plugins/sql/src/decode/mysql.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use serde_json::Value as JsonValue; -use sqlx::{mysql::MySqlValueRef, TypeInfo, Value, ValueRef}; +use sqlx::{TypeInfo, Value, ValueRef, mysql::MySqlValueRef}; use time::{Date, OffsetDateTime, PrimitiveDateTime, Time}; use crate::Error; diff --git a/plugins/sql/src/decode/postgres.rs b/plugins/sql/src/decode/postgres.rs index 4af9b713d..083322df1 100644 --- a/plugins/sql/src/decode/postgres.rs +++ b/plugins/sql/src/decode/postgres.rs @@ -4,7 +4,7 @@ use rust_decimal::prelude::ToPrimitive; use serde_json::Value as JsonValue; -use sqlx::{postgres::PgValueRef, TypeInfo, Value, ValueRef}; +use sqlx::{TypeInfo, Value, ValueRef, postgres::PgValueRef}; use time::{Date, OffsetDateTime, PrimitiveDateTime, Time}; use uuid::Uuid; diff --git a/plugins/sql/src/decode/sqlite.rs b/plugins/sql/src/decode/sqlite.rs index 1f0808ae3..c310ac398 100644 --- a/plugins/sql/src/decode/sqlite.rs +++ b/plugins/sql/src/decode/sqlite.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use serde_json::Value as JsonValue; -use sqlx::{sqlite::SqliteValueRef, TypeInfo, Value, ValueRef}; +use sqlx::{TypeInfo, Value, ValueRef, sqlite::SqliteValueRef}; use time::{Date, PrimitiveDateTime, Time}; use crate::Error; diff --git a/plugins/sql/src/lib.rs b/plugins/sql/src/lib.rs index dca4928c5..20eee0337 100644 --- a/plugins/sql/src/lib.rs +++ b/plugins/sql/src/lib.rs @@ -33,8 +33,8 @@ use sqlx::{ migrate::{Migration as SqlxMigration, MigrationSource, MigrationType, Migrator}, }; use tauri::{ - plugin::{Builder as PluginBuilder, TauriPlugin}, Manager, RunEvent, Runtime, + plugin::{Builder as PluginBuilder, TauriPlugin}, }; use tokio::sync::{Mutex, RwLock}; @@ -158,7 +158,9 @@ impl Builder { /// connected to in that case. pub fn new() -> Self { #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))] - eprintln!("No sql driver enabled. Please set at least one of the \"sqlite\", \"mysql\", \"postgres\" feature flags."); + eprintln!( + "No sql driver enabled. Please set at least one of the \"sqlite\", \"mysql\", \"postgres\" feature flags." + ); Self::default() } diff --git a/plugins/sql/src/wrapper.rs b/plugins/sql/src/wrapper.rs index f07a95e51..da0589433 100644 --- a/plugins/sql/src/wrapper.rs +++ b/plugins/sql/src/wrapper.rs @@ -8,7 +8,7 @@ use std::fs::create_dir_all; use indexmap::IndexMap; use serde_json::Value as JsonValue; #[cfg(any(feature = "sqlite", feature = "mysql", feature = "postgres"))] -use sqlx::{migrate::MigrateDatabase, Column, Executor, Pool, Row}; +use sqlx::{Column, Executor, Pool, Row, migrate::MigrateDatabase}; #[cfg(any(feature = "sqlite", feature = "mysql", feature = "postgres"))] use tauri::Manager; use tauri::{AppHandle, Runtime}; diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/Cargo.toml b/plugins/store/examples/AppSettingsManager/src-tauri/Cargo.toml index d005f10ef..d6023a7a8 100644 --- a/plugins/store/examples/AppSettingsManager/src-tauri/Cargo.toml +++ b/plugins/store/examples/AppSettingsManager/src-tauri/Cargo.toml @@ -5,7 +5,7 @@ description = "A Tauri App" authors = ["you"] license = "" repository = "" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/plugins/store/src/lib.rs b/plugins/store/src/lib.rs index 4a49dda01..3c472accf 100644 --- a/plugins/store/src/lib.rs +++ b/plugins/store/src/lib.rs @@ -18,10 +18,10 @@ use std::{ sync::{Arc, RwLock}, time::Duration, }; -pub use store::{resolve_store_path, DeserializeFn, SerializeFn, Store, StoreBuilder}; +pub use store::{DeserializeFn, SerializeFn, Store, StoreBuilder, resolve_store_path}; use tauri::{ - plugin::{self, TauriPlugin}, AppHandle, Manager, ResourceId, RunEvent, Runtime, State, + plugin::{self, TauriPlugin}, }; mod error; @@ -470,11 +470,10 @@ impl Builder { let collection = app_handle.state::(); let stores = collection.stores.read().unwrap(); for (path, rid) in stores.iter() { - if let Ok(store) = app_handle.resources_table().get::>(*rid) { - if let Err(err) = store.save() { + if let Ok(store) = app_handle.resources_table().get::>(*rid) + && let Err(err) = store.save() { tracing::error!("failed to save store {path:?} with error {err:?}"); } - } } } }) diff --git a/plugins/store/src/store.rs b/plugins/store/src/store.rs index b13a34ab4..42ac2cbf8 100644 --- a/plugins/store/src/store.rs +++ b/plugins/store/src/store.rs @@ -11,10 +11,10 @@ use std::{ sync::{Arc, Mutex}, time::Duration, }; -use tauri::{path::BaseDirectory, AppHandle, Emitter, Manager, Resource, ResourceId, Runtime}; +use tauri::{AppHandle, Emitter, Manager, Resource, ResourceId, Runtime, path::BaseDirectory}; use tokio::{ select, - sync::mpsc::{unbounded_channel, UnboundedSender}, + sync::mpsc::{UnboundedSender, unbounded_channel}, time::sleep, }; diff --git a/plugins/stronghold/src/kdf.rs b/plugins/stronghold/src/kdf.rs index f0d3aa8cb..1bc2c6297 100644 --- a/plugins/stronghold/src/kdf.rs +++ b/plugins/stronghold/src/kdf.rs @@ -45,8 +45,8 @@ fn create_or_get_salt(salt: &mut [u8], salt_path: &Path) { salt.clone_from_slice(&tmp); } else { // Generate new salt - let mut gen = ChaCha20Rng::from_os_rng(); - gen.fill_bytes(salt); + let mut rng = ChaCha20Rng::from_os_rng(); + rng.fill_bytes(salt); std::fs::write(salt_path, salt).expect("Failed to write salt for Stronghold") } } diff --git a/plugins/stronghold/src/lib.rs b/plugins/stronghold/src/lib.rs index 95ea71513..4c98b664c 100644 --- a/plugins/stronghold/src/lib.rs +++ b/plugins/stronghold/src/lib.rs @@ -23,18 +23,18 @@ use std::{ use crypto::keys::bip39; use iota_stronghold::{ + Client, Location, procedures::{ BIP39Generate, BIP39Recover, Curve, Ed25519Sign, KeyType as StrongholdKeyType, MnemonicLanguage, PublicKey, Slip10Derive, Slip10DeriveInput, Slip10Generate, StrongholdProcedure, }, - Client, Location, }; -use serde::{de::Visitor, Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, de::Visitor}; use stronghold::{Error, Result, Stronghold}; use tauri::{ - plugin::{Builder as PluginBuilder, TauriPlugin}, Manager, Runtime, State, + plugin::{Builder as PluginBuilder, TauriPlugin}, }; use zeroize::{Zeroize, Zeroizing}; @@ -270,12 +270,11 @@ async fn destroy( snapshot_path: PathBuf, ) -> Result<()> { let mut collection = collection.0.lock().unwrap(); - if let Some(stronghold) = collection.remove(&snapshot_path) { - if let Err(e) = stronghold.save() { + if let Some(stronghold) = collection.remove(&snapshot_path) + && let Err(e) = stronghold.save() { collection.insert(snapshot_path, stronghold); return Err(e); } - } Ok(()) } diff --git a/plugins/updater/src/commands.rs b/plugins/updater/src/commands.rs index 72684e6d8..9657a6997 100644 --- a/plugins/updater/src/commands.rs +++ b/plugins/updater/src/commands.rs @@ -6,7 +6,7 @@ use crate::{Result, Update, UpdaterExt}; use http::{HeaderMap, HeaderName, HeaderValue}; use serde::Serialize; -use tauri::{ipc::Channel, Manager, Resource, ResourceId, Runtime, Webview}; +use tauri::{Manager, Resource, ResourceId, Runtime, Webview, ipc::Channel}; use std::{str::FromStr, time::Duration}; use url::Url; diff --git a/plugins/updater/src/config.rs b/plugins/updater/src/config.rs index 9bd941ad5..56f01c97a 100644 --- a/plugins/updater/src/config.rs +++ b/plugins/updater/src/config.rs @@ -204,8 +204,12 @@ pub(crate) fn validate_endpoints( if url.scheme() != "https" { #[cfg(debug_assertions)] { - eprintln!("[\x1b[33mWARNING\x1b[0m] The updater endpoint \"{url}\" doesn't use `https` protocol. This is allowed in development but will fail in release builds."); - eprintln!("[\x1b[33mWARNING\x1b[0m] if this is a desired behavior, you can enable `dangerousInsecureTransportProtocol` in the plugin configuration"); + eprintln!( + "[\x1b[33mWARNING\x1b[0m] The updater endpoint \"{url}\" doesn't use `https` protocol. This is allowed in development but will fail in release builds." + ); + eprintln!( + "[\x1b[33mWARNING\x1b[0m] if this is a desired behavior, you can enable `dangerousInsecureTransportProtocol` in the plugin configuration" + ); } #[cfg(not(debug_assertions))] return Err(crate::Error::InsecureTransportProtocol); diff --git a/plugins/updater/src/error.rs b/plugins/updater/src/error.rs index 916220be7..6c7c21534 100644 --- a/plugins/updater/src/error.rs +++ b/plugins/updater/src/error.rs @@ -25,7 +25,9 @@ pub enum Error { #[error("Could not fetch a valid release JSON from the remote")] ReleaseNotFound, /// Unsupported app architecture. - #[error("Unsupported application architecture, expected one of `x86`, `x86_64`, `arm` or `aarch64`.")] + #[error( + "Unsupported application architecture, expected one of `x86`, `x86_64`, `arm` or `aarch64`." + )] UnsupportedArch, /// Operating system is not supported. #[error("Unsupported OS, expected one of `linux`, `darwin` or `windows`.")] @@ -43,9 +45,7 @@ pub enum Error { #[error("the platform `{0}` was not found in the response `platforms` object")] TargetNotFound(String), /// Neither the platform nor the fallback platform was found in the updater JSON response. - #[error( - "None of the fallback platforms `{0:?}` were found in the response `platforms` object" - )] + #[error("None of the fallback platforms `{0:?}` were found in the response `platforms` object")] TargetsNotFound(Vec), /// Download failed #[error("`{0}`")] @@ -57,7 +57,9 @@ pub enum Error { #[error(transparent)] Base64(#[from] base64::DecodeError), /// UTF8 Errors in signature. - #[error("The signature {0} could not be decoded, please check if it is a valid base64 string. The signature must be the contents of the `.sig` file generated by the Tauri bundler, as a string.")] + #[error( + "The signature {0} could not be decoded, please check if it is a valid base64 string. The signature must be the contents of the `.sig` file generated by the Tauri bundler, as a string." + )] SignatureUtf8(String), #[cfg(all(target_os = "windows", feature = "zip"))] /// `zip` errors. diff --git a/plugins/updater/src/lib.rs b/plugins/updater/src/lib.rs index 8e58a4029..d4fd06c59 100644 --- a/plugins/updater/src/lib.rs +++ b/plugins/updater/src/lib.rs @@ -24,8 +24,8 @@ use std::{ffi::OsString, sync::Arc}; use http::{HeaderMap, HeaderName, HeaderValue}; use semver::Version; use tauri::{ - plugin::{Builder as PluginBuilder, TauriPlugin}, Manager, Runtime, + plugin::{Builder as PluginBuilder, TauriPlugin}, }; mod commands; diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index 809cd0065..515eb9399 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -17,28 +17,28 @@ use std::ffi::OsStr; use base64::Engine; use futures_util::StreamExt; -use http::{header::ACCEPT, HeaderName}; +use http::{HeaderName, header::ACCEPT}; use minisign_verify::{PublicKey, Signature}; use percent_encoding::{AsciiSet, CONTROLS}; use reqwest::{ - header::{HeaderMap, HeaderValue}, ClientBuilder, StatusCode, + header::{HeaderMap, HeaderValue}, }; use semver::Version; -use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError}; use tauri::{ + AppHandle, Resource, Runtime, utils::{ config::BundleType, platform::{bundle_type, current_exe}, }, - AppHandle, Resource, Runtime, }; use time::OffsetDateTime; use url::Url; use crate::{ - error::{Error, Result}, Config, + error::{Error, Result}, }; const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -937,8 +937,8 @@ impl Update { /// └── ... fn install_inner(&self, bytes: &[u8]) -> Result<()> { use windows_sys::{ - w, Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW}, + w, }; let updater_type = self.extract(bytes)?; @@ -1185,8 +1185,8 @@ impl Update { let decoder = flate2::read::GzDecoder::new(archive); let mut archive = tar::Archive::new(decoder); for mut entry in archive.entries()?.flatten() { - if let Ok(path) = entry.path() { - if path.extension() == Some(OsStr::new("AppImage")) { + if let Ok(path) = entry.path() + && path.extension() == Some(OsStr::new("AppImage")) { // if something went wrong during the extraction, we should restore previous app if let Err(err) = entry.unpack(&self.extract_path) { std::fs::rename(tmp_app_image, &self.extract_path)?; @@ -1195,7 +1195,6 @@ impl Update { // early finish we have everything we need here return Ok(()); } - } } // if we have not returned early we should restore the backup std::fs::rename(tmp_app_image, &self.extract_path)?; @@ -1289,20 +1288,17 @@ impl Update { .arg(install_arg) .arg(pkg_path) .status() - { - if status.success() { + && status.success() { log::debug!("installed {pkg_path:?} with pkexec"); return Ok(()); } - } // 2. Try zenity or kdialog for a graphical sudo experience - if let Ok(password) = self.get_password_graphically() { - if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? { + if let Ok(password) = self.get_password_graphically() + && self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? { log::debug!("installed {pkg_path:?} with GUI sudo"); return Ok(()); } - } // 3. Final fallback: terminal sudo let status = std::process::Command::new("sudo") @@ -1329,22 +1325,20 @@ impl Update { ]) .output(); - if let Ok(output) = zenity_result { - if output.status.success() { + if let Ok(output) = zenity_result + && output.status.success() { return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); } - } // Fall back to kdialog if zenity fails or isn't available let kdialog_result = std::process::Command::new("kdialog") .args(["--password", "Enter your password to install the update:"]) .output(); - if let Ok(output) = kdialog_result { - if output.status.success() { + if let Ok(output) = kdialog_result + && output.status.success() { return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); } - } Err(Error::AuthenticationFailed) } diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index 20099d398..6cf13a03e 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -818,7 +818,8 @@ fn run_update_cases( if code != expected_exit_code { panic!( - "failed to run app bundled as {}, expected exit code {expected_exit_code}, got {code}", bundle_target.name() + "failed to run app bundled as {}, expected exit code {expected_exit_code}, got {code}", + bundle_target.name() ); } #[cfg(windows)] diff --git a/plugins/updater/tests/updater-migration/tests/update.rs b/plugins/updater/tests/updater-migration/tests/update.rs index 813da3c61..49f1bc98e 100644 --- a/plugins/updater/tests/updater-migration/tests/update.rs +++ b/plugins/updater/tests/updater-migration/tests/update.rs @@ -32,7 +32,7 @@ fn npm_command() -> Command { } mod v1 { - use super::{npm_command, BundleTarget, UPDATER_PRIVATE_KEY}; + use super::{BundleTarget, UPDATER_PRIVATE_KEY, npm_command}; use serde::Serialize; use std::{ path::{Path, PathBuf}, diff --git a/plugins/updater/tests/updater-migration/v1-app/Cargo.toml b/plugins/updater/tests/updater-migration/v1-app/Cargo.toml index e99e4108d..510c4229f 100644 --- a/plugins/updater/tests/updater-migration/v1-app/Cargo.toml +++ b/plugins/updater/tests/updater-migration/v1-app/Cargo.toml @@ -3,7 +3,7 @@ workspace = {} [package] name = "app-updater-v1" version = "0.1.0" -edition = "2021" +edition = "2024" [build-dependencies] tauri-build = { version = "1", features = [] } diff --git a/plugins/upload/src/lib.rs b/plugins/upload/src/lib.rs index 7cffb358f..4ce1058de 100644 --- a/plugins/upload/src/lib.rs +++ b/plugins/upload/src/lib.rs @@ -21,12 +21,11 @@ mod transfer_stats; use transfer_stats::TransferStats; use futures_util::TryStreamExt; -use serde::{ser::Serializer, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, ser::Serializer}; use tauri::{ - command, + Runtime, command, ipc::Channel, plugin::{Builder as PluginBuilder, TauriPlugin}, - Runtime, }; use tokio::{ fs::File, diff --git a/plugins/websocket/examples/tauri-app/src-tauri/Cargo.toml b/plugins/websocket/examples/tauri-app/src-tauri/Cargo.toml index 7c719eb3b..c3613b64a 100644 --- a/plugins/websocket/examples/tauri-app/src-tauri/Cargo.toml +++ b/plugins/websocket/examples/tauri-app/src-tauri/Cargo.toml @@ -2,7 +2,7 @@ name = "websocket-example" version = "0.1.0" description = "A Tauri App" -edition = "2021" +edition = "2024" [dependencies] serde = { workspace = true } diff --git a/plugins/websocket/src/lib.rs b/plugins/websocket/src/lib.rs index 15c7fa2b4..35a0cb421 100644 --- a/plugins/websocket/src/lib.rs +++ b/plugins/websocket/src/lib.rs @@ -18,13 +18,13 @@ html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" )] -use futures_util::{stream::SplitSink, SinkExt, StreamExt}; +use futures_util::{SinkExt, StreamExt, stream::SplitSink}; use http::header::{HeaderName, HeaderValue}; -use serde::{ser::Serializer, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, ser::Serializer}; use tauri::{ + Manager, Runtime, State, Window, ipc::Channel, plugin::{Builder as PluginBuilder, TauriPlugin}, - Manager, Runtime, State, Window, }; use tokio::{net::TcpStream, sync::Mutex}; #[cfg(any( @@ -40,12 +40,12 @@ use tokio_tungstenite::connect_async_tls_with_config; )))] use tokio_tungstenite::connect_async_with_config; use tokio_tungstenite::{ + Connector, MaybeTlsStream, WebSocketStream, tungstenite::{ + Message, client::IntoClientRequest, protocol::{CloseFrame as ProtocolCloseFrame, WebSocketConfig}, - Message, }, - Connector, MaybeTlsStream, WebSocketStream, }; use std::collections::HashMap; diff --git a/plugins/window-state/src/cmd.rs b/plugins/window-state/src/cmd.rs index 8b376aa90..baadc01fb 100644 --- a/plugins/window-state/src/cmd.rs +++ b/plugins/window-state/src/cmd.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use crate::{AppHandleExt, StateFlags, WindowExt}; -use tauri::{command, AppHandle, Manager, Runtime}; +use tauri::{AppHandle, Manager, Runtime, command}; fn get_state_flags( app: &AppHandle, diff --git a/plugins/window-state/src/lib.rs b/plugins/window-state/src/lib.rs index 21c5a2350..8aeab6a44 100644 --- a/plugins/window-state/src/lib.rs +++ b/plugins/window-state/src/lib.rs @@ -13,9 +13,9 @@ use bitflags::bitflags; use serde::{Deserialize, Serialize}; use tauri::{ - plugin::{Builder as PluginBuilder, TauriPlugin}, AppHandle, Manager, Monitor, PhysicalPosition, PhysicalSize, RunEvent, Runtime, WebviewWindow, Window, WindowEvent, + plugin::{Builder as PluginBuilder, TauriPlugin}, }; use std::{ From a433ec09d13c6532f54256608d8e951283d64596 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sat, 26 Sep 2026 15:46:48 -0300 Subject: [PATCH 26/29] fix: mobile clippy warnings --- plugins/barcode-scanner/src/lib.rs | 1 - plugins/clipboard-manager/src/lib.rs | 4 +- plugins/fs/src/android.rs | 10 +- plugins/fs/src/commands.rs | 253 +++++++++--------- plugins/fs/src/ios.rs | 4 +- plugins/notification/src/models.rs | 9 +- plugins/opener/src/reveal_item_in_dir.rs | 4 +- plugins/shell/src/lib.rs | 1 + .../examples/vanilla/src-tauri/src/main.rs | 23 +- plugins/updater/src/updater.rs | 7 +- .../updater/tests/app-updater/tests/update.rs | 3 +- .../tests/updater-migration/tests/update.rs | 1 + 12 files changed, 157 insertions(+), 163 deletions(-) diff --git a/plugins/barcode-scanner/src/lib.rs b/plugins/barcode-scanner/src/lib.rs index fb6a89257..b84a7b60a 100644 --- a/plugins/barcode-scanner/src/lib.rs +++ b/plugins/barcode-scanner/src/lib.rs @@ -13,7 +13,6 @@ use tauri::{ plugin::{Builder, PluginHandle, TauriPlugin}, }; -pub use models::*; mod error; mod models; diff --git a/plugins/clipboard-manager/src/lib.rs b/plugins/clipboard-manager/src/lib.rs index d08d77c2c..024f6f593 100644 --- a/plugins/clipboard-manager/src/lib.rs +++ b/plugins/clipboard-manager/src/lib.rs @@ -10,7 +10,7 @@ )] use tauri::{ - Manager, RunEvent, Runtime, + Manager, Runtime, plugin::{Builder, TauriPlugin}, }; @@ -62,7 +62,7 @@ pub fn init() -> TauriPlugin { }) .on_event(|_app, _event| { #[cfg(desktop)] - if let RunEvent::Exit = _event { + if let tauri::RunEvent::Exit = _event { _app.clipboard().cleanup(); } }) diff --git a/plugins/fs/src/android.rs b/plugins/fs/src/android.rs index a914b4d9c..971c42811 100644 --- a/plugins/fs/src/android.rs +++ b/plugins/fs/src/android.rs @@ -43,10 +43,7 @@ impl Fs { FilePath::Url(u) => self .resolve_content_uri(u.to_string(), opts.android_mode()) .map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("failed to open file: {e}"), - ) + std::io::Error::other(format!("failed to open file: {e}")) }), FilePath::Path(p) => { // tauri::utils::platform::resources_dir() returns a PathBuf with the Android asset URI prefix @@ -56,10 +53,7 @@ impl Fs { { self.resolve_content_uri(p.to_string_lossy(), opts.android_mode()) .map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("failed to open file: {e}"), - ) + std::io::Error::other(format!("failed to open file: {e}")) }) } else { std::fs::OpenOptions::from(opts).open(p) diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs index 9065efa7c..dff4e6a9f 100644 --- a/plugins/fs/src/commands.rs +++ b/plugins/fs/src/commands.rs @@ -159,26 +159,26 @@ impl Drop for FileHandle { // Only clean up if we're tracking this resource // If start_accessing_security_scoped_resource was used, it won't be in our tracking // and we shouldn't interfere - if let FilePath::Url(url) = file_path { - if url.scheme() == "file" { - let security_scoped_resources = - self.app_handle.state::(); + if let FilePath::Url(url) = file_path + && url.scheme() == "file" + { + let security_scoped_resources = + self.app_handle.state::(); - // Only clean up if it's not tracked manually - if !security_scoped_resources.is_tracked_manually(url.as_str()) { - log::debug!( - "Stopping accessing security-scoped resource for URL: {url} on drop" - ); - let _ = self - .app_handle - .fs() - .stop_accessing_security_scoped_resource(FilePath::Url(url.clone())); - security_scoped_resources.remove(url.as_str()); - } else { - log::debug!( - "Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)" - ); - } + // Only clean up if it's not tracked manually + if !security_scoped_resources.is_tracked_manually(url.as_str()) { + log::debug!( + "Stopping accessing security-scoped resource for URL: {url} on drop" + ); + let _ = self + .app_handle + .fs() + .stop_accessing_security_scoped_resource(FilePath::Url(url.clone())); + security_scoped_resources.remove(url.as_str()); + } else { + log::debug!( + "Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)" + ); } } } @@ -237,26 +237,24 @@ impl Drop for PathHandle { // Only clean up if we're tracking this resource (i.e., resolve_path started it) // If start_accessing_security_scoped_resource was used, it won't be in our tracking // and we shouldn't interfere - if let FilePath::Url(url) = file_path { - if url.scheme() == "file" { - let security_scoped_resources = - self.app_handle.state::(); + if let FilePath::Url(url) = file_path + && url.scheme() == "file" + { + let security_scoped_resources = + self.app_handle.state::(); - // Only clean up if it's not tracked manually - if !security_scoped_resources.is_tracked_manually(url.as_str()) { - log::debug!( - "Stopping accessing security-scoped resource for URL: {url} on drop" - ); - let _ = self - .app_handle - .fs() - .stop_accessing_security_scoped_resource(FilePath::Url(url.clone())); - security_scoped_resources.remove(url.as_str()); - } else { - log::debug!( - "Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)" - ); - } + // Only clean up if it's not tracked manually + if !security_scoped_resources.is_tracked_manually(url.as_str()) { + log::debug!("Stopping accessing security-scoped resource for URL: {url} on drop"); + let _ = self + .app_handle + .fs() + .stop_accessing_security_scoped_resource(FilePath::Url(url.clone())); + security_scoped_resources.remove(url.as_str()); + } else { + log::debug!( + "Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)" + ); } } } @@ -1280,50 +1278,49 @@ pub fn start_accessing_security_scoped_resource( }; // Only handle file URLs - if let FilePath::Url(url) = &file_path { - if url.scheme() == "file" { - use objc2_foundation::{NSString, NSURL}; + if let FilePath::Url(url) = &file_path + && url.scheme() == "file" + { + use objc2_foundation::{NSString, NSURL}; - let url_nsstring = NSString::from_str(url.as_str()); - let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) }; - if let Some(ns_url) = ns_url { - // Check if already active - let security_scoped_resources = - webview.state::(); - if security_scoped_resources.is_tracked_manually(url.as_str()) { + let url_nsstring = NSString::from_str(url.as_str()); + let ns_url = NSURL::URLWithString(&url_nsstring); + if let Some(ns_url) = ns_url { + // Check if already active + let security_scoped_resources = webview.state::(); + if security_scoped_resources.is_tracked_manually(url.as_str()) { + log::debug!( + "Security-scoped resource already active for URL: {}", + url.as_str() + ); + return Ok(()); + } + + // Start accessing the security-scoped resource + unsafe { + let success = ns_url.startAccessingSecurityScopedResource(); + if success { log::debug!( - "Security-scoped resource already active for URL: {}", + "Started accessing security-scoped resource for URL: {}", url.as_str() ); - return Ok(()); + security_scoped_resources.track_manually(url.as_str().to_string()); + } else { + log::warn!( + "Failed to start accessing security-scoped resource for URL: {}", + url.as_str() + ); + return Err(CommandError::from(format!( + "Failed to start accessing security-scoped resource for URL: {}", + url.as_str() + ))); } - - // Start accessing the security-scoped resource - unsafe { - let success = ns_url.startAccessingSecurityScopedResource(); - if success { - log::debug!( - "Started accessing security-scoped resource for URL: {}", - url.as_str() - ); - security_scoped_resources.track_manually(url.as_str().to_string()); - } else { - log::warn!( - "Failed to start accessing security-scoped resource for URL: {}", - url.as_str() - ); - return Err(CommandError::from(format!( - "Failed to start accessing security-scoped resource for URL: {}", - url.as_str() - ))); - } - } - } else { - return Err(CommandError::from(format!( - "Failed to create NSURL from URL: {}", - url.as_str() - ))); } + } else { + return Err(CommandError::from(format!( + "Failed to create NSURL from URL: {}", + url.as_str() + ))); } } Ok(()) @@ -1352,31 +1349,31 @@ pub fn stop_accessing_security_scoped_resource( }; // Only handle file URLs - if let FilePath::Url(url) = file_path { - if url.scheme() == "file" { - let security_scoped_resources = webview.state::(); + if let FilePath::Url(url) = file_path + && url.scheme() == "file" + { + let security_scoped_resources = webview.state::(); - // Check if it's tracked - if !security_scoped_resources.is_tracked_manually(url.as_str()) { - log::debug!( - "Security-scoped resource not tracked as active for URL: {}", - url.as_str() - ); - return Ok(()); - } - - // Stop accessing the security-scoped resource - webview - .fs() - .stop_accessing_security_scoped_resource(FilePath::Url(url.clone()))?; - - // Remove from tracking - security_scoped_resources.remove(url.as_str()); + // Check if it's tracked + if !security_scoped_resources.is_tracked_manually(url.as_str()) { log::debug!( - "Stopped accessing security-scoped resource for URL: {}", + "Security-scoped resource not tracked as active for URL: {}", url.as_str() ); + return Ok(()); } + + // Stop accessing the security-scoped resource + webview + .fs() + .stop_accessing_security_scoped_resource(FilePath::Url(url.clone()))?; + + // Remove from tracking + security_scoped_resources.remove(url.as_str()); + log::debug!( + "Stopped accessing security-scoped resource for URL: {}", + url.as_str() + ); } Ok(()) } @@ -1518,47 +1515,47 @@ pub fn resolve_path( // On iOS, start accessing security-scoped resource if the path is a file URL // Only if it hasn't been started already via start_accessing_security_scoped_resource #[cfg(target_os = "ios")] - if let SafeFilePath::Url(url) = &path { - if url.scheme() == "file" { - use objc2_foundation::{NSString, NSURL}; + if let SafeFilePath::Url(url) = &path + && url.scheme() == "file" + { + use objc2_foundation::{NSString, NSURL}; - let security_scoped_resources = webview.state::(); + let security_scoped_resources = webview.state::(); - // Check if already active (started via start_accessing_security_scoped_resource) - if !security_scoped_resources.is_tracked_manually(url.as_str()) { - let url_nsstring = NSString::from_str(url.as_str()); - let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) }; - if let Some(ns_url) = ns_url { - // Start accessing the security-scoped resource - // This is required for files outside the app's sandbox (e.g., from file picker) - unsafe { - let success = ns_url.startAccessingSecurityScopedResource(); - if success { - log::debug!( - "Started accessing security-scoped resource for URL: {} (via resolve_path)", - url.as_str() - ); - // Track it so we know to clean it up - security_scoped_resources.track_manually(url.as_str().to_string()); - } else { - log::warn!( - "Failed to start accessing security-scoped resource for URL: {}", - url.as_str() - ); - } + // Check if already active (started via start_accessing_security_scoped_resource) + if !security_scoped_resources.is_tracked_manually(url.as_str()) { + let url_nsstring = NSString::from_str(url.as_str()); + let ns_url = NSURL::URLWithString(&url_nsstring); + if let Some(ns_url) = ns_url { + // Start accessing the security-scoped resource + // This is required for files outside the app's sandbox (e.g., from file picker) + unsafe { + let success = ns_url.startAccessingSecurityScopedResource(); + if success { + log::debug!( + "Started accessing security-scoped resource for URL: {} (via resolve_path)", + url.as_str() + ); + // Track it so we know to clean it up + security_scoped_resources.track_manually(url.as_str().to_string()); + } else { + log::warn!( + "Failed to start accessing security-scoped resource for URL: {}", + url.as_str() + ); } - } else { - log::debug!( - "Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", - url.as_str() - ); } } else { log::debug!( - "Security-scoped resource already active for URL: {} (started via start_accessing_security_scoped_resource), skipping", + "Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url.as_str() ); } + } else { + log::debug!( + "Security-scoped resource already active for URL: {} (started via start_accessing_security_scoped_resource), skipping", + url.as_str() + ); } } diff --git a/plugins/fs/src/ios.rs b/plugins/fs/src/ios.rs index 7c1fc271b..ff36b8b09 100644 --- a/plugins/fs/src/ios.rs +++ b/plugins/fs/src/ios.rs @@ -47,7 +47,7 @@ impl Fs { // Create NSURL from the URL string // URLWithString may return None for invalid URLs, but file:// URLs should be valid - let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) }; + let ns_url = NSURL::URLWithString(&url_nsstring); if let Some(ns_url) = ns_url { // Start accessing the security-scoped resource // This is required for files outside the app's sandbox (e.g., from file picker) @@ -127,7 +127,7 @@ impl Fs { }; let url_nsstring = NSString::from_str(&url_string); - let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) }; + let ns_url = NSURL::URLWithString(&url_nsstring); if let Some(ns_url) = ns_url { // Stop accessing the security-scoped resource unsafe { diff --git a/plugins/notification/src/models.rs b/plugins/notification/src/models.rs index b146dcb89..bb638f117 100644 --- a/plugins/notification/src/models.rs +++ b/plugins/notification/src/models.rs @@ -725,7 +725,7 @@ mod android { /// /// It maps to the `NotificationManager.IMPORTANCE_*` constants and is serialized as its /// integer value. Only available on Android. - #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)] + #[derive(Debug, Default, Clone, Copy, Serialize_repr, Deserialize_repr)] #[repr(u8)] pub enum Importance { /// The notifications are not shown. @@ -737,17 +737,12 @@ mod android { /// The notifications are shown and make a sound. /// /// This is the value used when the channel does not define an importance. + #[default] Default = 3, /// The notifications are shown, make a sound and pop up as a heads-up notification. High = 4, } - impl Default for Importance { - fn default() -> Self { - Self::Default - } - } - /// How much of a notification is shown on the lock screen. /// /// It maps to the `Notification.VISIBILITY_*` constants and is serialized as its diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 1b593cf6b..fa63b9508 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -10,7 +10,7 @@ use std::path::{Path, PathBuf}; /// /// - **Android / iOS:** Unsupported. pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { - let path = canonicalize(path.as_ref())?; + let _path = canonicalize(path.as_ref())?; #[cfg(any( windows, @@ -21,7 +21,7 @@ pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { target_os = "netbsd", target_os = "openbsd" ))] - return imp::reveal_items_in_dir(&[path]); + return imp::reveal_items_in_dir(&[_path]); #[cfg(not(any( windows, diff --git a/plugins/shell/src/lib.rs b/plugins/shell/src/lib.rs index dd5967456..60f881c5f 100644 --- a/plugins/shell/src/lib.rs +++ b/plugins/shell/src/lib.rs @@ -87,6 +87,7 @@ impl Shell { /// See [`crate::open::open`] for how it handles security-related measures. #[cfg(mobile)] #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")] + #[allow(deprecated)] pub fn open(&self, path: impl Into, _with: Option) -> Result<()> { self.mobile_plugin_handle .run_mobile_plugin("open", path.into()) diff --git a/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs b/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs index 835857c99..7177806d1 100644 --- a/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs +++ b/plugins/single-instance/examples/vanilla/src-tauri/src/main.rs @@ -8,15 +8,20 @@ )] fn main() { - tauri::Builder::default() - .plugin( - tauri_plugin_single_instance::Builder::new() - .callback(move |app, argv, cwd| { - println!("{}, {argv:?}, {cwd}", app.package_info().name); - }) - .dbus_id("org.Tauri.SIExampleApp".to_owned()) - .build(), - ) + let builder = tauri::Builder::default(); + + // single-instance is only available on desktop + #[cfg(desktop)] + let builder = builder.plugin( + tauri_plugin_single_instance::Builder::new() + .callback(move |app, argv, cwd| { + println!("{}, {argv:?}, {cwd}", app.package_info().name); + }) + .dbus_id("org.Tauri.SIExampleApp".to_owned()) + .build(), + ); + + builder .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index 515eb9399..8fc650356 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -5,15 +5,12 @@ use std::{ collections::HashMap, ffi::OsString, - io::Cursor, path::{Path, PathBuf}, str::FromStr, sync::Arc, time::Duration, }; -#[cfg(not(target_os = "macos"))] -use std::ffi::OsStr; use base64::Engine; use futures_util::StreamExt; @@ -26,6 +23,10 @@ use reqwest::{ }; use semver::Version; use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError}; +#[cfg(windows)] +use std::ffi::OsStr; +#[cfg(desktop)] +use std::io::Cursor; use tauri::{ AppHandle, Resource, Runtime, utils::{ diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index 6cf13a03e..77273836c 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +#![cfg(any(target_os = "linux", target_os = "macos", windows))] #![allow(dead_code, unused_imports)] use std::{ @@ -376,7 +377,7 @@ fn stage_app_under_test(root_dir: &Path, target: &str, bundle_target: BundleTarg ) }); - return staged; + staged } } diff --git a/plugins/updater/tests/updater-migration/tests/update.rs b/plugins/updater/tests/updater-migration/tests/update.rs index 49f1bc98e..5fa8b010d 100644 --- a/plugins/updater/tests/updater-migration/tests/update.rs +++ b/plugins/updater/tests/updater-migration/tests/update.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +#![cfg(any(target_os = "linux", target_os = "macos", windows))] #![allow(dead_code, unused_imports)] use std::{ From f460570b4423708fb3b5ab1e1213d889a4d7a9b5 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sat, 26 Sep 2026 15:53:40 -0300 Subject: [PATCH 27/29] chore: fmt --- examples/api/src-tauri/src/lib.rs | 11 ++-- plugins/barcode-scanner/src/lib.rs | 1 - plugins/cli/src/parser.rs | 12 ++--- plugins/deep-link/src/config.rs | 11 ++-- plugins/deep-link/src/lib.rs | 13 +++-- plugins/dialog/src/commands.rs | 33 ++++++------ plugins/fs/src/android.rs | 8 +-- plugins/fs/src/file_path.rs | 14 +++--- plugins/http/src/commands.rs | 19 +++---- .../src/platform_impl/linux.rs | 6 +-- plugins/store/src/lib.rs | 7 +-- plugins/stronghold/src/lib.rs | 9 ++-- plugins/updater/src/updater.rs | 50 ++++++++++--------- 13 files changed, 101 insertions(+), 93 deletions(-) diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index 2cb8d927d..5d51ba5bd 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -205,10 +205,11 @@ pub fn run() { app.run(move |_app_handle, _event| { #[cfg(desktop)] if let RunEvent::ExitRequested { code, api, .. } = &_event - && code.is_none() { - // Keep the event loop running even if all windows are closed - // This allow us to catch system tray events when there is no window - api.prevent_exit(); - } + && code.is_none() + { + // Keep the event loop running even if all windows are closed + // This allow us to catch system tray events when there is no window + api.prevent_exit(); + } }) } diff --git a/plugins/barcode-scanner/src/lib.rs b/plugins/barcode-scanner/src/lib.rs index b84a7b60a..5ca9d9751 100644 --- a/plugins/barcode-scanner/src/lib.rs +++ b/plugins/barcode-scanner/src/lib.rs @@ -13,7 +13,6 @@ use tauri::{ plugin::{Builder, PluginHandle, TauriPlugin}, }; - mod error; mod models; diff --git a/plugins/cli/src/parser.rs b/plugins/cli/src/parser.rs index 1c66adb52..ed94f0858 100644 --- a/plugins/cli/src/parser.rs +++ b/plugins/cli/src/parser.rs @@ -139,12 +139,12 @@ fn get_matches_internal(config: &Config, matches: &ArgMatches) -> Matches { .subcommands .as_ref() .and_then(|s| s.get(subcommand_name)) - { - cli_matches.set_subcommand( - subcommand_name.to_string(), - get_matches_internal(subcommand_config, subcommand_matches), - ); - } + { + cli_matches.set_subcommand( + subcommand_name.to_string(), + get_matches_internal(subcommand_config, subcommand_matches), + ); + } cli_matches } diff --git a/plugins/deep-link/src/config.rs b/plugins/deep-link/src/config.rs index 94aba9092..e4465719b 100644 --- a/plugins/deep-link/src/config.rs +++ b/plugins/deep-link/src/config.rs @@ -71,11 +71,12 @@ where { let opt = Option::::deserialize(deserializer)?; if let Some(ref host) = opt - && let Some((scheme, _)) = host.split_once("://") { - return Err(serde::de::Error::custom(format!( - "host `{host}` cannot start with a scheme, please remove the `{scheme}://` prefix" - ))); - } + && let Some((scheme, _)) = host.split_once("://") + { + return Err(serde::de::Error::custom(format!( + "host `{host}` cannot start with a scheme, please remove the `{scheme}://` prefix" + ))); + } Ok(opt) } diff --git a/plugins/deep-link/src/lib.rs b/plugins/deep-link/src/lib.rs index 1b7ca9e1d..13b323711 100644 --- a/plugins/deep-link/src/lib.rs +++ b/plugins/deep-link/src/lib.rs @@ -65,10 +65,10 @@ fn init_deep_link( }, )?; - return Ok(DeepLink { + Ok(DeepLink { app: app.clone(), plugin_handle: handle, - }); + }) } #[cfg(target_os = "ios")] @@ -417,9 +417,10 @@ mod imp { if mimeapps_path.exists() { let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path)?; if let Some(section) = mimeapps.section_mut(Some("Default Applications")) - && section.get(&mime_type).unwrap_or_default() == file_name { - section.remove(&mime_type); - } + && section.get(&mime_type).unwrap_or_default() == file_name + { + section.remove(&mime_type); + } mimeapps.write_to_file(&mimeapps_path)?; } @@ -559,8 +560,6 @@ impl DeepLink { /// /// Use `get_current` on app load to check whether your app was started via a deep link. pub fn on_open_url(&self, f: F) -> EventId { - - self.app.listen("deep-link://new-url", move |event| { if let Ok(urls) = serde_json::from_str(event.payload()) { f(OpenUrlEvent { diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs index 1411f909c..0117fefee 100644 --- a/plugins/dialog/src/commands.rs +++ b/plugins/dialog/src/commands.rs @@ -172,12 +172,13 @@ pub(crate) async fn open( } else { let folder = dialog_builder.blocking_pick_folder(); if let Some(folder) = &folder - && let Ok(path) = folder.clone().into_path() { - if let Some(s) = window.try_fs_scope() { - s.allow_directory(&path, options.recursive)?; - } - tauri_scope.allow_directory(&path, options.directory)?; + && let Ok(path) = folder.clone().into_path() + { + if let Some(s) = window.try_fs_scope() { + s.allow_directory(&path, options.recursive)?; } + tauri_scope.allow_directory(&path, options.directory)?; + } OpenResponse::Folder(folder.map(|p| p.simplified())) } } @@ -204,12 +205,13 @@ pub(crate) async fn open( let file = dialog_builder.blocking_pick_file(); if let Some(file) = &file - && let Ok(path) = file.clone().into_path() { - if let Some(s) = window.try_fs_scope() { - s.allow_file(&path)?; - } - tauri_scope.allow_file(&path)?; + && let Ok(path) = file.clone().into_path() + { + if let Some(s) = window.try_fs_scope() { + s.allow_file(&path)?; } + tauri_scope.allow_file(&path)?; + } OpenResponse::File(file.map(|f| f.simplified())) }; Ok(res) @@ -245,12 +247,13 @@ pub(crate) async fn save( let path = dialog_builder.blocking_save_file(); if let Some(p) = &path - && let Ok(path) = p.clone().into_path() { - if let Some(s) = window.try_fs_scope() { - s.allow_file(&path)?; - } - tauri_scope.allow_file(&path)?; + && let Ok(path) = p.clone().into_path() + { + if let Some(s) = window.try_fs_scope() { + s.allow_file(&path)?; } + tauri_scope.allow_file(&path)?; + } Ok(path.map(|p| p.simplified())) } diff --git a/plugins/fs/src/android.rs b/plugins/fs/src/android.rs index 971c42811..b9a829b95 100644 --- a/plugins/fs/src/android.rs +++ b/plugins/fs/src/android.rs @@ -42,9 +42,7 @@ impl Fs { match path.into() { FilePath::Url(u) => self .resolve_content_uri(u.to_string(), opts.android_mode()) - .map_err(|e| { - std::io::Error::other(format!("failed to open file: {e}")) - }), + .map_err(|e| std::io::Error::other(format!("failed to open file: {e}"))), FilePath::Path(p) => { // tauri::utils::platform::resources_dir() returns a PathBuf with the Android asset URI prefix // we must resolve that file with the Android API @@ -52,9 +50,7 @@ impl Fs { .is_ok() { self.resolve_content_uri(p.to_string_lossy(), opts.android_mode()) - .map_err(|e| { - std::io::Error::other(format!("failed to open file: {e}")) - }) + .map_err(|e| std::io::Error::other(format!("failed to open file: {e}"))) } else { std::fs::OpenOptions::from(opts).open(p) } diff --git a/plugins/fs/src/file_path.rs b/plugins/fs/src/file_path.rs index 7252b0d3a..86c2aa377 100644 --- a/plugins/fs/src/file_path.rs +++ b/plugins/fs/src/file_path.rs @@ -191,9 +191,10 @@ impl FromStr for FilePath { type Err = Infallible; fn from_str(s: &str) -> std::result::Result { if let Ok(url) = url::Url::from_str(s) - && url.scheme().len() != 1 { - return Ok(Self::Url(url)); - } + && url.scheme().len() != 1 + { + return Ok(Self::Url(url)); + } Ok(Self::Path(PathBuf::from(s))) } } @@ -202,9 +203,10 @@ impl FromStr for SafeFilePath { type Err = Error; fn from_str(s: &str) -> Result { if let Ok(url) = url::Url::from_str(s) - && url.scheme().len() != 1 { - return Ok(Self::Url(url)); - } + && url.scheme().len() != 1 + { + return Ok(Self::Url(url)); + } SafePathBuf::new(s.into()) .map(SafeFilePath::Path) diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index e6c4d48db..d4775c48f 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -349,15 +349,16 @@ pub async fn fetch( // ensure we have an Origin header set if (cfg!(not(feature = "unsafe-headers")) || !headers.contains_key(header::ORIGIN)) - && let Ok(url) = webview.url() { - // The url crate returns OpaqueOrigin for tauri://localhost which serializes to "null" - let origin = if url.scheme() == "tauri" { - "tauri://localhost".to_string() - } else { - url.origin().ascii_serialization() - }; - headers.append(header::ORIGIN, HeaderValue::from_str(&origin)?); - } + && let Ok(url) = webview.url() + { + // The url crate returns OpaqueOrigin for tauri://localhost which serializes to "null" + let origin = if url.scheme() == "tauri" { + "tauri://localhost".to_string() + } else { + url.origin().ascii_serialization() + }; + headers.append(header::ORIGIN, HeaderValue::from_str(&origin)?); + } // In case empty origin is passed, remove it. Some services do not like Origin header // so this way we can remove it in explicit way. The default behaviour is still to set it diff --git a/plugins/single-instance/src/platform_impl/linux.rs b/plugins/single-instance/src/platform_impl/linux.rs index 0cc7e8371..107f3e743 100644 --- a/plugins/single-instance/src/platform_impl/linux.rs +++ b/plugins/single-instance/src/platform_impl/linux.rs @@ -105,7 +105,7 @@ pub fn destroy>(manager: &M) { && let Some(dbus_name) = manager .try_state::() .and_then(|name| WellKnownName::try_from(name.0.clone()).ok()) - { - let _ = connection.0.release_name(dbus_name); - } + { + let _ = connection.0.release_name(dbus_name); + } } diff --git a/plugins/store/src/lib.rs b/plugins/store/src/lib.rs index 3c472accf..6f6dc0bbd 100644 --- a/plugins/store/src/lib.rs +++ b/plugins/store/src/lib.rs @@ -471,9 +471,10 @@ impl Builder { let stores = collection.stores.read().unwrap(); for (path, rid) in stores.iter() { if let Ok(store) = app_handle.resources_table().get::>(*rid) - && let Err(err) = store.save() { - tracing::error!("failed to save store {path:?} with error {err:?}"); - } + && let Err(err) = store.save() + { + tracing::error!("failed to save store {path:?} with error {err:?}"); + } } } }) diff --git a/plugins/stronghold/src/lib.rs b/plugins/stronghold/src/lib.rs index 4c98b664c..01fb9c54d 100644 --- a/plugins/stronghold/src/lib.rs +++ b/plugins/stronghold/src/lib.rs @@ -271,10 +271,11 @@ async fn destroy( ) -> Result<()> { let mut collection = collection.0.lock().unwrap(); if let Some(stronghold) = collection.remove(&snapshot_path) - && let Err(e) = stronghold.save() { - collection.insert(snapshot_path, stronghold); - return Err(e); - } + && let Err(e) = stronghold.save() + { + collection.insert(snapshot_path, stronghold); + return Err(e); + } Ok(()) } diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index 8fc650356..d46861070 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -11,7 +11,6 @@ use std::{ time::Duration, }; - use base64::Engine; use futures_util::StreamExt; use http::{HeaderName, header::ACCEPT}; @@ -1187,15 +1186,16 @@ impl Update { let mut archive = tar::Archive::new(decoder); for mut entry in archive.entries()?.flatten() { if let Ok(path) = entry.path() - && path.extension() == Some(OsStr::new("AppImage")) { - // if something went wrong during the extraction, we should restore previous app - if let Err(err) = entry.unpack(&self.extract_path) { - std::fs::rename(tmp_app_image, &self.extract_path)?; - return Err(err.into()); - } - // early finish we have everything we need here - return Ok(()); + && path.extension() == Some(OsStr::new("AppImage")) + { + // if something went wrong during the extraction, we should restore previous app + if let Err(err) = entry.unpack(&self.extract_path) { + std::fs::rename(tmp_app_image, &self.extract_path)?; + return Err(err.into()); } + // early finish we have everything we need here + return Ok(()); + } } // if we have not returned early we should restore the backup std::fs::rename(tmp_app_image, &self.extract_path)?; @@ -1289,17 +1289,19 @@ impl Update { .arg(install_arg) .arg(pkg_path) .status() - && status.success() { - log::debug!("installed {pkg_path:?} with pkexec"); - return Ok(()); - } + && status.success() + { + log::debug!("installed {pkg_path:?} with pkexec"); + return Ok(()); + } // 2. Try zenity or kdialog for a graphical sudo experience if let Ok(password) = self.get_password_graphically() - && self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? { - log::debug!("installed {pkg_path:?} with GUI sudo"); - return Ok(()); - } + && self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? + { + log::debug!("installed {pkg_path:?} with GUI sudo"); + return Ok(()); + } // 3. Final fallback: terminal sudo let status = std::process::Command::new("sudo") @@ -1327,9 +1329,10 @@ impl Update { .output(); if let Ok(output) = zenity_result - && output.status.success() { - return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); - } + && output.status.success() + { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } // Fall back to kdialog if zenity fails or isn't available let kdialog_result = std::process::Command::new("kdialog") @@ -1337,9 +1340,10 @@ impl Update { .output(); if let Ok(output) = kdialog_result - && output.status.success() { - return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); - } + && output.status.success() + { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } Err(Error::AuthenticationFailed) } From cbfd428bcc3ecb21aac9dd049779c9b9c7b90734 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:21:49 -0300 Subject: [PATCH 28/29] Publish New Versions (v2) (#3618) * publish new versions * clippy --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Lucas Nogueira --- .changes/android-kotlin-compiler-options.md | 16 ----- .changes/feat-fs-size-options.md | 6 -- .changes/fix-autostart-windows-disable.md | 5 -- .changes/fix-deep-link-linux-unregister.md | 5 -- .changes/fix-fs-android-modes.md | 6 -- .changes/fix-fs-read-lines-error.md | 6 -- .changes/fix-fs-read-lines-return.md | 6 -- .changes/fix-fs-watch-errors.md | 6 -- .changes/fix-fs-write-options-header.md | 6 -- .changes/fix-geo-haptics-specta-skip.md | 8 --- .../fix-notification-mobile-permissions.md | 11 ---- .../fix-notification-windows-permission.md | 6 -- .changes/global-api-script-reuse-core.md | 58 ----------------- .changes/msrv-1.90.md | 61 ------------------ .../notification-deprecate-windows7-compat.md | 5 -- .changes/update-docs.md | 61 ------------------ .changes/updater-remove-ssl-cert-env.md | 6 -- Cargo.lock | 62 +++++++++---------- examples/api/CHANGELOG.md | 22 +++++++ examples/api/package.json | 2 +- examples/api/src-tauri/CHANGELOG.md | 24 +++++++ examples/api/src-tauri/Cargo.toml | 40 ++++++------ plugins/autostart/CHANGELOG.md | 11 ++++ plugins/autostart/Cargo.toml | 2 +- plugins/autostart/package.json | 2 +- plugins/barcode-scanner/CHANGELOG.md | 10 +++ plugins/barcode-scanner/Cargo.toml | 2 +- plugins/barcode-scanner/package.json | 2 +- plugins/biometric/CHANGELOG.md | 10 +++ plugins/biometric/Cargo.toml | 2 +- plugins/biometric/package.json | 2 +- plugins/cli/CHANGELOG.md | 6 ++ plugins/cli/Cargo.toml | 2 +- plugins/cli/package.json | 2 +- plugins/clipboard-manager/CHANGELOG.md | 11 ++++ plugins/clipboard-manager/Cargo.toml | 2 +- plugins/clipboard-manager/package.json | 2 +- plugins/deep-link/CHANGELOG.md | 12 ++++ plugins/deep-link/Cargo.toml | 2 +- plugins/deep-link/examples/app/CHANGELOG.md | 6 ++ plugins/deep-link/examples/app/package.json | 2 +- plugins/deep-link/package.json | 2 +- plugins/dialog/CHANGELOG.md | 13 ++++ plugins/dialog/Cargo.toml | 4 +- plugins/dialog/package.json | 2 +- plugins/fs/CHANGELOG.md | 17 +++++ plugins/fs/Cargo.toml | 2 +- plugins/fs/package.json | 2 +- plugins/geolocation/CHANGELOG.md | 10 +++ plugins/geolocation/Cargo.toml | 2 +- plugins/geolocation/package.json | 2 +- plugins/global-shortcut/CHANGELOG.md | 6 ++ plugins/global-shortcut/Cargo.toml | 2 +- plugins/global-shortcut/package.json | 2 +- plugins/haptics/CHANGELOG.md | 8 +++ plugins/haptics/Cargo.toml | 2 +- plugins/haptics/package.json | 2 +- plugins/http/CHANGELOG.md | 10 +++ plugins/http/Cargo.toml | 4 +- plugins/http/package.json | 2 +- plugins/localhost/CHANGELOG.md | 5 ++ plugins/localhost/Cargo.toml | 2 +- plugins/log/CHANGELOG.md | 6 ++ plugins/log/Cargo.toml | 2 +- plugins/log/package.json | 2 +- plugins/nfc/CHANGELOG.md | 10 +++ plugins/nfc/Cargo.toml | 2 +- plugins/nfc/package.json | 2 +- plugins/notification/CHANGELOG.md | 19 ++++++ plugins/notification/Cargo.toml | 2 +- plugins/notification/package.json | 2 +- plugins/notification/src/desktop.rs | 1 + plugins/opener/CHANGELOG.md | 11 ++++ plugins/opener/Cargo.toml | 2 +- plugins/opener/package.json | 2 +- plugins/os/CHANGELOG.md | 6 ++ plugins/os/Cargo.toml | 2 +- plugins/os/package.json | 2 +- plugins/persisted-scope/CHANGELOG.md | 9 +++ plugins/persisted-scope/Cargo.toml | 4 +- plugins/positioner/CHANGELOG.md | 6 ++ plugins/positioner/Cargo.toml | 2 +- plugins/positioner/package.json | 2 +- plugins/process/CHANGELOG.md | 6 ++ plugins/process/Cargo.toml | 2 +- plugins/process/package.json | 2 +- plugins/shell/CHANGELOG.md | 11 ++++ plugins/shell/Cargo.toml | 2 +- plugins/shell/package.json | 2 +- plugins/single-instance/CHANGELOG.md | 9 +++ plugins/single-instance/Cargo.toml | 4 +- plugins/sql/CHANGELOG.md | 6 ++ plugins/sql/Cargo.toml | 2 +- plugins/sql/package.json | 2 +- plugins/store/CHANGELOG.md | 6 ++ plugins/store/Cargo.toml | 2 +- plugins/store/package.json | 2 +- plugins/stronghold/CHANGELOG.md | 6 ++ plugins/stronghold/Cargo.toml | 2 +- plugins/stronghold/package.json | 2 +- plugins/updater/CHANGELOG.md | 10 +++ plugins/updater/Cargo.toml | 2 +- plugins/updater/package.json | 2 +- plugins/updater/src/updater.rs | 2 +- plugins/upload/CHANGELOG.md | 6 ++ plugins/upload/Cargo.toml | 2 +- plugins/upload/package.json | 2 +- plugins/websocket/CHANGELOG.md | 6 ++ plugins/websocket/Cargo.toml | 2 +- plugins/websocket/package.json | 2 +- plugins/window-state/CHANGELOG.md | 6 ++ plugins/window-state/Cargo.toml | 2 +- plugins/window-state/package.json | 2 +- 113 files changed, 436 insertions(+), 393 deletions(-) delete mode 100644 .changes/android-kotlin-compiler-options.md delete mode 100644 .changes/feat-fs-size-options.md delete mode 100644 .changes/fix-autostart-windows-disable.md delete mode 100644 .changes/fix-deep-link-linux-unregister.md delete mode 100644 .changes/fix-fs-android-modes.md delete mode 100644 .changes/fix-fs-read-lines-error.md delete mode 100644 .changes/fix-fs-read-lines-return.md delete mode 100644 .changes/fix-fs-watch-errors.md delete mode 100644 .changes/fix-fs-write-options-header.md delete mode 100644 .changes/fix-geo-haptics-specta-skip.md delete mode 100644 .changes/fix-notification-mobile-permissions.md delete mode 100644 .changes/fix-notification-windows-permission.md delete mode 100644 .changes/global-api-script-reuse-core.md delete mode 100644 .changes/msrv-1.90.md delete mode 100644 .changes/notification-deprecate-windows7-compat.md delete mode 100644 .changes/update-docs.md delete mode 100644 .changes/updater-remove-ssl-cert-env.md diff --git a/.changes/android-kotlin-compiler-options.md b/.changes/android-kotlin-compiler-options.md deleted file mode 100644 index a98d859a3..000000000 --- a/.changes/android-kotlin-compiler-options.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"barcode-scanner": patch -"biometric": patch -"clipboard-manager": patch -"deep-link": patch -"dialog": patch -"fs": patch -"geolocation": patch -"haptics": patch -"nfc": patch -"notification": patch -"opener": patch -"shell": patch ---- - -Migrate the Android Gradle scripts from the deprecated `kotlinOptions` DSL to `compilerOptions`, which Kotlin Gradle Plugin 2.3+ requires, to match the Tauri 2.12 Android library and templates. diff --git a/.changes/feat-fs-size-options.md b/.changes/feat-fs-size-options.md deleted file mode 100644 index 463ae615e..000000000 --- a/.changes/feat-fs-size-options.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -fs: minor -fs-js: minor ---- - -Added an `options` parameter with `baseDir` to `size`, so relative paths can be measured, like the other functions and like the JSDoc example already showed. The Rust command already accepted it. diff --git a/.changes/fix-autostart-windows-disable.md b/.changes/fix-autostart-windows-disable.md deleted file mode 100644 index 1bae66871..000000000 --- a/.changes/fix-autostart-windows-disable.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -autostart: patch ---- - -Fixed `disable()` failing on Windows when autostart is already disabled. It now succeeds, as it does on macOS and Linux. diff --git a/.changes/fix-deep-link-linux-unregister.md b/.changes/fix-deep-link-linux-unregister.md deleted file mode 100644 index ab542f7ae..000000000 --- a/.changes/fix-deep-link-linux-unregister.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -deep-link: patch ---- - -Fixed `unregister` on Linux leaving the app as the scheme's handler: it now also removes the scheme from the `MimeType` of the handler's `.desktop` file and refreshes the desktop database, which `xdg-mime` falls back to. It also no longer fails when `mimeapps.list` does not exist. diff --git a/.changes/fix-fs-android-modes.md b/.changes/fix-fs-android-modes.md deleted file mode 100644 index dbe1af000..000000000 --- a/.changes/fix-fs-android-modes.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -fs: patch -fs-js: patch ---- - -Fixed opening Android `content://` URIs with `append` or with `write`, `truncate` and `append` together: the options were turned into invalid modes such as `ra` or `wta`. They now map to the modes Android accepts (`r`, `w`, `wt`, `wa`, `rw`, `rwt`), `append` taking precedence over `read` since there is no read and append mode. diff --git a/.changes/fix-fs-read-lines-error.md b/.changes/fix-fs-read-lines-error.md deleted file mode 100644 index e4618efd8..000000000 --- a/.changes/fix-fs-read-lines-error.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -fs: patch -fs-js: patch ---- - -Fixed `readTextFileLines` yielding empty lines forever when reading fails (e.g. on a directory). The read error is now reported: the iterator rejects and the file is closed. diff --git a/.changes/fix-fs-read-lines-return.md b/.changes/fix-fs-read-lines-return.md deleted file mode 100644 index 5259efe48..000000000 --- a/.changes/fix-fs-read-lines-return.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -fs: patch -fs-js: patch ---- - -Fixed `readTextFileLines` leaving the file open until the webview is destroyed when a `for await` loop over it exits early (`break`, `return` or `throw`). The iterator now implements `return()`, which closes the file. diff --git a/.changes/fix-fs-watch-errors.md b/.changes/fix-fs-watch-errors.md deleted file mode 100644 index 418704489..000000000 --- a/.changes/fix-fs-watch-errors.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -fs: patch -fs-js: patch ---- - -File watcher errors (e.g. the watched directory was removed or the inotify limit was reached) are now logged instead of being silently dropped. diff --git a/.changes/fix-fs-write-options-header.md b/.changes/fix-fs-write-options-header.md deleted file mode 100644 index 4949106f5..000000000 --- a/.changes/fix-fs-write-options-header.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -fs: patch -fs-js: patch ---- - -`writeFile` and `writeTextFile` now fail when their options cannot be parsed instead of silently ignoring them (including `baseDir`, `append` and `createNew`) and writing to the raw path. Data sent as a JSON array is rejected if it contains values that are not bytes, instead of truncating them. diff --git a/.changes/fix-geo-haptics-specta-skip.md b/.changes/fix-geo-haptics-specta-skip.md deleted file mode 100644 index 78f11d581..000000000 --- a/.changes/fix-geo-haptics-specta-skip.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"haptics": patch -"geolocation": patch -"haptics-js": patch -"geolocation-js": patch ---- - -Fixed a build failure when the `specta` feature is enabled: the `Error` type used the `#[serde(skip)]` helper attribute, which recent `specta` versions no longer register. It now uses `#[specta(skip)]`. diff --git a/.changes/fix-notification-mobile-permissions.md b/.changes/fix-notification-mobile-permissions.md deleted file mode 100644 index 8a8174a03..000000000 --- a/.changes/fix-notification-mobile-permissions.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -notification: patch -notification-js: patch ---- - -Fixed broken mobile APIs: - -- `channels()` invoked the `listChannels` command, which the `allow-list-channels` permission does not match; it now invokes `list_channels`. -- Unregistering an `onNotificationReceived` or `onAction` listener invokes the `remove_listener` command, which had no permission. It is now declared and part of the `default` permission set (`allow-remove-listener`). -- `cancelAll()` was rejected on Android and iOS, which required the list of notifications to cancel. It now cancels every pending notification. -- `active()` resolved with `{ values: [...] }` instead of an array on Android. diff --git a/.changes/fix-notification-windows-permission.md b/.changes/fix-notification-windows-permission.md deleted file mode 100644 index 8dc1a4034..000000000 --- a/.changes/fix-notification-windows-permission.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -notification: patch -notification-js: patch ---- - -Fixed `isPermissionGranted()` always resolving to `false` on Windows. The initialization script short-circuited the permission check to avoid invoking the backend, but read the permission back before it had been set, so it settled on `denied` instead of `granted`. diff --git a/.changes/global-api-script-reuse-core.md b/.changes/global-api-script-reuse-core.md deleted file mode 100644 index 7fd096bcf..000000000 --- a/.changes/global-api-script-reuse-core.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -"autostart": patch -"autostart-js": patch -"barcode-scanner": patch -"barcode-scanner-js": patch -"biometric": patch -"biometric-js": patch -"cli": patch -"cli-js": patch -"clipboard-manager": patch -"clipboard-manager-js": patch -"deep-link": patch -"deep-link-js": patch -"dialog": patch -"dialog-js": patch -"fs": patch -"fs-js": patch -"geolocation": patch -"geolocation-js": patch -"global-shortcut": patch -"global-shortcut-js": patch -"haptics": patch -"haptics-js": patch -"http": patch -"http-js": patch -"log": patch -"log-js": patch -"nfc": patch -"nfc-js": patch -"notification": patch -"notification-js": patch -"opener": patch -"opener-js": patch -"os": patch -"os-js": patch -"positioner": patch -"positioner-js": patch -"process": patch -"process-js": patch -"shell": patch -"shell-js": patch -"sql": patch -"sql-js": patch -"store": patch -"store-js": patch -"stronghold": patch -"stronghold-js": patch -"updater": patch -"updater-js": patch -"upload": patch -"upload-js": patch -"websocket": patch -"websocket-js": patch -"window-state": patch -"window-state-js": patch ---- - -The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. diff --git a/.changes/msrv-1.90.md b/.changes/msrv-1.90.md deleted file mode 100644 index 898b9901f..000000000 --- a/.changes/msrv-1.90.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -"autostart": minor -"autostart-js": minor -"barcode-scanner": minor -"barcode-scanner-js": minor -"biometric": minor -"biometric-js": minor -"cli": minor -"cli-js": minor -"clipboard-manager": minor -"clipboard-manager-js": minor -"deep-link": minor -"deep-link-js": minor -"fs": minor -"fs-js": minor -"dialog": minor -"dialog-js": minor -"geolocation": minor -"geolocation-js": minor -"global-shortcut": minor -"global-shortcut-js": minor -"opener": minor -"opener-js": minor -"haptics": minor -"haptics-js": minor -"http": minor -"http-js": minor -"localhost": minor -"log": minor -"log-js": minor -"nfc": minor -"nfc-js": minor -"notification": minor -"notification-js": minor -"os": minor -"os-js": minor -"persisted-scope": minor -"positioner": minor -"positioner-js": minor -"process": minor -"process-js": minor -"shell": minor -"shell-js": minor -"single-instance": minor -"sql": minor -"sql-js": minor -"store": minor -"store-js": minor -"stronghold": minor -"stronghold-js": minor -"updater": minor -"updater-js": minor -"upload": minor -"upload-js": minor -"websocket": minor -"websocket-js": minor -"window-state": minor -"window-state-js": minor ---- - -Update MSRV to 1.90 to match tauri. diff --git a/.changes/notification-deprecate-windows7-compat.md b/.changes/notification-deprecate-windows7-compat.md deleted file mode 100644 index 92e9f22de..000000000 --- a/.changes/notification-deprecate-windows7-compat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"notification": minor ---- - -Deprecate the `windows7-compat` Cargo feature, which is now a no-op since Tauri 2.12 no longer supports Windows 7. `NotificationBuilder::notify` is deprecated in favor of `NotificationBuilder::show`, and the `win7-notifications` and `windows-version` dependencies were removed. diff --git a/.changes/update-docs.md b/.changes/update-docs.md deleted file mode 100644 index 1d491b11d..000000000 --- a/.changes/update-docs.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -"autostart": patch -"barcode-scanner": patch -"biometric": patch -"cli": patch -"clipboard-manager": patch -"deep-link": patch -"dialog": patch -"fs": patch -"global-shortcut": patch -"http": patch -"localhost": patch -"log": patch -"nfc": patch -"notification": patch -"opener": patch -"os": patch -"persisted-scope": patch -"positioner": patch -"process": patch -"shell": patch -"single-instance": patch -"sql": patch -"store": patch -"stronghold": patch -"updater": patch -"upload": patch -"websocket": patch -"window-state": patch -"autostart-js": patch -"barcode-scanner-js": patch -"biometric-js": patch -"cli-js": patch -"clipboard-manager-js": patch -"deep-link-js": patch -"dialog-js": patch -"fs-js": patch -"global-shortcut-js": patch -"http-js": patch -"log-js": patch -"nfc-js": patch -"notification-js": patch -"opener-js": patch -"os-js": patch -"positioner-js": patch -"process-js": patch -"shell-js": patch -"sql-js": patch -"store-js": patch -"stronghold-js": patch -"updater-js": patch -"upload-js": patch -"websocket-js": patch -"window-state-js": patch -"haptics": patch -"haptics-js": patch -"geolocation": patch -"geolocation-js": patch ---- - -Update documentation. diff --git a/.changes/updater-remove-ssl-cert-env.md b/.changes/updater-remove-ssl-cert-env.md deleted file mode 100644 index 263f3fbde..000000000 --- a/.changes/updater-remove-ssl-cert-env.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"updater": patch:bug -"updater-js": patch:bug ---- - -On Linux, `check()` no longer sets `SSL_CERT_FILE` and `SSL_CERT_DIR` to Debian paths. The override pointed rustls to files that do not exist on distros with a different layout, such as ALT Linux, and left every TLS client in the app without root certificates. diff --git a/Cargo.lock b/Cargo.lock index 0438156a2..2a8a0740b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,7 +222,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "api" -version = "2.0.49" +version = "2.0.50" dependencies = [ "log", "serde", @@ -6696,7 +6696,7 @@ dependencies = [ [[package]] name = "tauri-plugin-autostart" -version = "2.5.1" +version = "2.6.0" dependencies = [ "auto-launch", "serde", @@ -6708,7 +6708,7 @@ dependencies = [ [[package]] name = "tauri-plugin-barcode-scanner" -version = "2.4.6" +version = "2.5.0" dependencies = [ "log", "serde", @@ -6720,7 +6720,7 @@ dependencies = [ [[package]] name = "tauri-plugin-biometric" -version = "2.3.3" +version = "2.4.0" dependencies = [ "log", "serde", @@ -6733,7 +6733,7 @@ dependencies = [ [[package]] name = "tauri-plugin-cli" -version = "2.4.1" +version = "2.5.0" dependencies = [ "clap", "log", @@ -6746,7 +6746,7 @@ dependencies = [ [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.3.3" +version = "2.4.0" dependencies = [ "arboard", "log", @@ -6759,7 +6759,7 @@ dependencies = [ [[package]] name = "tauri-plugin-deep-link" -version = "2.4.10" +version = "2.5.0" dependencies = [ "dunce", "plist", @@ -6778,7 +6778,7 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.7.3" +version = "2.8.0" dependencies = [ "log", "raw-window-handle", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.5.2" +version = "2.6.0" dependencies = [ "anyhow", "dunce", @@ -6818,7 +6818,7 @@ dependencies = [ [[package]] name = "tauri-plugin-geolocation" -version = "2.3.3" +version = "2.4.0" dependencies = [ "log", "serde", @@ -6831,7 +6831,7 @@ dependencies = [ [[package]] name = "tauri-plugin-global-shortcut" -version = "2.3.2" +version = "2.4.0" dependencies = [ "global-hotkey", "log", @@ -6844,7 +6844,7 @@ dependencies = [ [[package]] name = "tauri-plugin-haptics" -version = "2.3.3" +version = "2.4.0" dependencies = [ "log", "serde", @@ -6857,7 +6857,7 @@ dependencies = [ [[package]] name = "tauri-plugin-http" -version = "2.7.0" +version = "2.8.0" dependencies = [ "bytes", "cookie_store", @@ -6880,7 +6880,7 @@ dependencies = [ [[package]] name = "tauri-plugin-localhost" -version = "2.3.2" +version = "2.4.0" dependencies = [ "http", "log", @@ -6893,7 +6893,7 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.9.2" +version = "2.10.0" dependencies = [ "android_logger", "fern", @@ -6913,7 +6913,7 @@ dependencies = [ [[package]] name = "tauri-plugin-nfc" -version = "2.3.6" +version = "2.4.0" dependencies = [ "log", "serde", @@ -6926,7 +6926,7 @@ dependencies = [ [[package]] name = "tauri-plugin-notification" -version = "2.4.0" +version = "2.5.0" dependencies = [ "log", "notify-rust", @@ -6943,7 +6943,7 @@ dependencies = [ [[package]] name = "tauri-plugin-opener" -version = "2.5.5" +version = "2.6.0" dependencies = [ "dunce", "glob", @@ -6963,7 +6963,7 @@ dependencies = [ [[package]] name = "tauri-plugin-os" -version = "2.3.2" +version = "2.4.0" dependencies = [ "gethostname 1.0.1", "log", @@ -6979,7 +6979,7 @@ dependencies = [ [[package]] name = "tauri-plugin-persisted-scope" -version = "2.3.8" +version = "2.4.0" dependencies = [ "aho-corasick", "bincode", @@ -6993,7 +6993,7 @@ dependencies = [ [[package]] name = "tauri-plugin-positioner" -version = "2.3.4" +version = "2.4.0" dependencies = [ "log", "serde", @@ -7006,7 +7006,7 @@ dependencies = [ [[package]] name = "tauri-plugin-process" -version = "2.3.1" +version = "2.4.0" dependencies = [ "tauri", "tauri-plugin", @@ -7014,7 +7014,7 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.3.6" +version = "2.4.0" dependencies = [ "encoding_rs", "log", @@ -7033,7 +7033,7 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.4.5" +version = "2.5.0" dependencies = [ "semver", "serde", @@ -7049,7 +7049,7 @@ dependencies = [ [[package]] name = "tauri-plugin-sql" -version = "2.4.1" +version = "2.5.0" dependencies = [ "futures-core", "indexmap 2.14.2", @@ -7068,7 +7068,7 @@ dependencies = [ [[package]] name = "tauri-plugin-store" -version = "2.4.5" +version = "2.5.0" dependencies = [ "dunce", "serde", @@ -7082,7 +7082,7 @@ dependencies = [ [[package]] name = "tauri-plugin-stronghold" -version = "2.3.2" +version = "2.4.0" dependencies = [ "hex", "iota-crypto", @@ -7103,7 +7103,7 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" -version = "2.12.0" +version = "2.13.0" dependencies = [ "base64 0.22.1", "dirs 6.0.0", @@ -7134,7 +7134,7 @@ dependencies = [ [[package]] name = "tauri-plugin-upload" -version = "2.4.1" +version = "2.5.0" dependencies = [ "futures-util", "log", @@ -7152,7 +7152,7 @@ dependencies = [ [[package]] name = "tauri-plugin-websocket" -version = "2.4.3" +version = "2.5.0" dependencies = [ "futures-util", "http", @@ -7170,7 +7170,7 @@ dependencies = [ [[package]] name = "tauri-plugin-window-state" -version = "2.4.1" +version = "2.5.0" dependencies = [ "bitflags 2.9.0", "log", diff --git a/examples/api/CHANGELOG.md b/examples/api/CHANGELOG.md index 77b1396aa..111b2766b 100644 --- a/examples/api/CHANGELOG.md +++ b/examples/api/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [2.0.46] + +### Dependencies + +- Upgraded to `fs-js@2.6.0` +- Upgraded to `notification-js@2.5.0` +- Upgraded to `barcode-scanner-js@2.5.0` +- Upgraded to `biometric-js@2.4.0` +- Upgraded to `cli-js@2.5.0` +- Upgraded to `clipboard-manager-js@2.4.0` +- Upgraded to `dialog-js@2.8.0` +- Upgraded to `global-shortcut-js@2.4.0` +- Upgraded to `http-js@2.7.0` +- Upgraded to `log-js@2.10.0` +- Upgraded to `nfc-js@2.4.0` +- Upgraded to `opener-js@2.6.0` +- Upgraded to `os-js@2.4.0` +- Upgraded to `process-js@2.4.0` +- Upgraded to `shell-js@2.4.0` +- Upgraded to `store-js@2.5.0` +- Upgraded to `updater-js@2.13.0` + ## [2.0.45] ### Dependencies diff --git a/examples/api/package.json b/examples/api/package.json index 237a54cae..52e368b4f 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -1,7 +1,7 @@ { "name": "api", "private": true, - "version": "2.0.45", + "version": "2.0.46", "type": "module", "scripts": { "dev": "vite --clearScreen false", diff --git a/examples/api/src-tauri/CHANGELOG.md b/examples/api/src-tauri/CHANGELOG.md index cb78a36e2..592efe3c5 100644 --- a/examples/api/src-tauri/CHANGELOG.md +++ b/examples/api/src-tauri/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [2.0.50] + +### Dependencies + +- Upgraded to `barcode-scanner@2.5.0` +- Upgraded to `biometric@2.4.0` +- Upgraded to `clipboard-manager@2.4.0` +- Upgraded to `dialog@2.8.0` +- Upgraded to `fs@2.6.0` +- Upgraded to `geolocation@2.4.0` +- Upgraded to `haptics@2.4.0` +- Upgraded to `nfc@2.4.0` +- Upgraded to `notification@2.5.0` +- Upgraded to `opener@2.6.0` +- Upgraded to `shell@2.4.0` +- Upgraded to `cli@2.5.0` +- Upgraded to `global-shortcut@2.4.0` +- Upgraded to `http@2.8.0` +- Upgraded to `log@2.10.0` +- Upgraded to `os@2.4.0` +- Upgraded to `process@2.4.0` +- Upgraded to `store@2.5.0` +- Upgraded to `updater@2.13.0` + ## [2.0.49] ### Dependencies diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 8e06d3c4f..73a873ff8 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "api" publish = false -version = "2.0.49" +version = "2.0.50" description = "An example Tauri Application showcasing the api" edition = "2024" rust-version = { workspace = true } @@ -20,22 +20,22 @@ serde = { workspace = true } tiny_http = "0.12" time = "0.3" log = { workspace = true } -tauri-plugin-log = { path = "../../../plugins/log", version = "2.9.2" } -tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.5.2", features = [ +tauri-plugin-log = { path = "../../../plugins/log", version = "2.10.0" } +tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.6.0", features = [ "watch", ] } -tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.3.3" } -tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.7.3" } +tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.4.0" } +tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.8.0" } tauri-plugin-http = { path = "../../../plugins/http", features = [ "multipart", "cookies", -], version = "2.7.0" } -tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.4.0" } -tauri-plugin-os = { path = "../../../plugins/os", version = "2.3.2" } -tauri-plugin-process = { path = "../../../plugins/process", version = "2.3.1" } -tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.5" } -tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.6" } -tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.5" } +], version = "2.8.0" } +tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.5.0" } +tauri-plugin-os = { path = "../../../plugins/os", version = "2.4.0" } +tauri-plugin-process = { path = "../../../plugins/process", version = "2.4.0" } +tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.6.0" } +tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.4.0" } +tauri-plugin-store = { path = "../../../plugins/store", version = "2.5.0" } tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" } tauri-plugin-deep-link = { path = "../../../plugins/deep-link", version = "2.4.10" } tauri-plugin-sql = { path = "../../../plugins/sql", version = "2.4.1", features = [ @@ -65,20 +65,20 @@ features = [ [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] tauri-plugin-autostart = { path = "../../../plugins/autostart", version = "2.5.1" } -tauri-plugin-cli = { path = "../../../plugins/cli", version = "2.4.1" } -tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.3.2" } +tauri-plugin-cli = { path = "../../../plugins/cli", version = "2.5.0" } +tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.4.0" } tauri-plugin-positioner = { path = "../../../plugins/positioner", version = "2.3.4", features = [ "tray-icon", ] } -tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.12.0" } +tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.13.0" } tauri-plugin-window-state = { path = "../../../plugins/window-state", version = "2.2.0" } [target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies] -tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.4.6" } -tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.3.6" } -tauri-plugin-biometric = { path = "../../../plugins/biometric/", version = "2.3.3" } -tauri-plugin-geolocation = { path = "../../../plugins/geolocation/", version = "2.3.3" } -tauri-plugin-haptics = { path = "../../../plugins/haptics/", version = "2.3.3" } +tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.5.0" } +tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "2.4.0" } +tauri-plugin-biometric = { path = "../../../plugins/biometric/", version = "2.4.0" } +tauri-plugin-geolocation = { path = "../../../plugins/geolocation/", version = "2.4.0" } +tauri-plugin-haptics = { path = "../../../plugins/haptics/", version = "2.4.0" } [features] # Enables the WebDriver automation bridge; set by the e2e suite's build (packages/api-e2e). diff --git a/plugins/autostart/CHANGELOG.md b/plugins/autostart/CHANGELOG.md index 36fc8a4bd..83c584092 100644 --- a/plugins/autostart/CHANGELOG.md +++ b/plugins/autostart/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.6.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## \[2.5.1] - [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. @@ -108,3 +114,8 @@ ## \[2.0.0-alpha.0] - [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! +alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/autostart/Cargo.toml b/plugins/autostart/Cargo.toml index 596566843..c100fc130 100644 --- a/plugins/autostart/Cargo.toml +++ b/plugins/autostart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-autostart" -version = "2.5.1" +version = "2.6.0" description = "Automatically launch your application at startup." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/autostart/package.json b/plugins/autostart/package.json index 1e5e64d16..c4a3491bb 100644 --- a/plugins/autostart/package.json +++ b/plugins/autostart/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-autostart", - "version": "2.5.1", + "version": "2.6.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/barcode-scanner/CHANGELOG.md b/plugins/barcode-scanner/CHANGELOG.md index 02c6345ef..da00eb9a0 100644 --- a/plugins/barcode-scanner/CHANGELOG.md +++ b/plugins/barcode-scanner/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.5.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.4.6] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -153,3 +159,7 @@ . commit/454428cd50ce4962f0bad8e355aebc68af8cc61f)([#536](https://github.com/tauri-apps/plugins-workspace/pull/536)) Initial release. github.com/tauri-apps/plugins-workspace/pull/536)) Initial release. +ebc68af8cc61f)([#536](https://github.com/tauri-apps/plugins-workspace/pull/536)) Initial release. + . + commit/454428cd50ce4962f0bad8e355aebc68af8cc61f)([#536](https://github.com/tauri-apps/plugins-workspace/pull/536)) Initial release. + github.com/tauri-apps/plugins-workspace/pull/536)) Initial release. diff --git a/plugins/barcode-scanner/Cargo.toml b/plugins/barcode-scanner/Cargo.toml index 0ff34010c..89c6a2a1a 100644 --- a/plugins/barcode-scanner/Cargo.toml +++ b/plugins/barcode-scanner/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-barcode-scanner" -version = "2.4.6" +version = "2.5.0" description = "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS" edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/barcode-scanner/package.json b/plugins/barcode-scanner/package.json index 45410dcc9..6648568ab 100644 --- a/plugins/barcode-scanner/package.json +++ b/plugins/barcode-scanner/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-barcode-scanner", - "version": "2.4.6", + "version": "2.5.0", "description": "Scan QR codes, EAN-13 and other kinds of barcodes on Android and iOS", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/biometric/CHANGELOG.md b/plugins/biometric/CHANGELOG.md index 527ba3dcb..13bb1e131 100644 --- a/plugins/biometric/CHANGELOG.md +++ b/plugins/biometric/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.4.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.3.3] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -112,3 +118,7 @@ . commit/8df28a987519ecfa03dcb8635443025f8d010362)([#829](https://github.com/tauri-apps/plugins-workspace/pull/829)) Initial release. ithub.com/tauri-apps/plugins-workspace/pull/829)) Initial release. +43025f8d010362)([#829](https://github.com/tauri-apps/plugins-workspace/pull/829)) Initial release. + . + commit/8df28a987519ecfa03dcb8635443025f8d010362)([#829](https://github.com/tauri-apps/plugins-workspace/pull/829)) Initial release. + ithub.com/tauri-apps/plugins-workspace/pull/829)) Initial release. diff --git a/plugins/biometric/Cargo.toml b/plugins/biometric/Cargo.toml index 0dc29e276..fe2e8f6c1 100644 --- a/plugins/biometric/Cargo.toml +++ b/plugins/biometric/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-biometric" -version = "2.3.3" +version = "2.4.0" description = "Prompt the user for biometric authentication on Android and iOS." edition = { workspace = true } rust-version = { workspace = true } diff --git a/plugins/biometric/package.json b/plugins/biometric/package.json index 9a3e41d5b..acc3024f4 100644 --- a/plugins/biometric/package.json +++ b/plugins/biometric/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-biometric", - "version": "2.3.3", + "version": "2.4.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/cli/CHANGELOG.md b/plugins/cli/CHANGELOG.md index 8cd68039c..75c91d263 100644 --- a/plugins/cli/CHANGELOG.md +++ b/plugins/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.5.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## \[2.4.1] - [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. diff --git a/plugins/cli/Cargo.toml b/plugins/cli/Cargo.toml index 871597211..9d5a83302 100644 --- a/plugins/cli/Cargo.toml +++ b/plugins/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-cli" -version = "2.4.1" +version = "2.5.0" description = "Parse arguments from your Tauri application's command line interface." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/cli/package.json b/plugins/cli/package.json index 2a3a6b510..e8c18d8bc 100644 --- a/plugins/cli/package.json +++ b/plugins/cli/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-cli", - "version": "2.4.1", + "version": "2.5.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/clipboard-manager/CHANGELOG.md b/plugins/clipboard-manager/CHANGELOG.md index 5c6a54a6a..f5ffb9206 100644 --- a/plugins/clipboard-manager/CHANGELOG.md +++ b/plugins/clipboard-manager/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.4.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.3.3] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -150,3 +156,8 @@ ## \[2.0.0-alpha.0] - [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! +55](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/clipboard-manager/Cargo.toml b/plugins/clipboard-manager/Cargo.toml index 478cba484..a176796f3 100644 --- a/plugins/clipboard-manager/Cargo.toml +++ b/plugins/clipboard-manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-clipboard-manager" -version = "2.3.3" +version = "2.4.0" description = "Read and write to the system clipboard." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/clipboard-manager/package.json b/plugins/clipboard-manager/package.json index b83614956..bce2f84c0 100644 --- a/plugins/clipboard-manager/package.json +++ b/plugins/clipboard-manager/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-clipboard-manager", - "version": "2.3.3", + "version": "2.4.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/deep-link/CHANGELOG.md b/plugins/deep-link/CHANGELOG.md index d0f5e5301..2ad1bc7cd 100644 --- a/plugins/deep-link/CHANGELOG.md +++ b/plugins/deep-link/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.5.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.4.10] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -201,3 +207,9 @@ ]\(https://github.com/tauri-apps/plugins-workspace/commit/eccd6f977af7629255b6f5a5205666c9079a86ed)([#504](https://github.com/tauri-apps/plugins-workspace/pull/504)) Initial release. commit/eccd6f977af7629255b6f5a5205666c9079a86ed)([#504](https://github.com/tauri-apps/plugins-workspace/pull/504)) Initial release. ithub.com/tauri-apps/plugins-workspace/pull/504)) Initial release. +55b6f5a5205666c9079a86ed)([#504](https://github.com/tauri-apps/plugins-workspace/pull/504)) Initial release. + commit/eccd6f977af7629255b6f5a5205666c9079a86ed)([#504](https://github.com/tauri-apps/plugins-workspace/pull/504)) Initial release. + ithub.com/tauri-apps/plugins-workspace/pull/504)) Initial release. + ]\(https://github.com/tauri-apps/plugins-workspace/commit/eccd6f977af7629255b6f5a5205666c9079a86ed)([#504](https://github.com/tauri-apps/plugins-workspace/pull/504)) Initial release. + commit/eccd6f977af7629255b6f5a5205666c9079a86ed)([#504](https://github.com/tauri-apps/plugins-workspace/pull/504)) Initial release. + ithub.com/tauri-apps/plugins-workspace/pull/504)) Initial release. diff --git a/plugins/deep-link/Cargo.toml b/plugins/deep-link/Cargo.toml index ce07db300..11f92673c 100644 --- a/plugins/deep-link/Cargo.toml +++ b/plugins/deep-link/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-deep-link" -version = "2.4.10" +version = "2.5.0" description = "Set your Tauri application as the default handler for an URL" authors = { workspace = true } license = { workspace = true } diff --git a/plugins/deep-link/examples/app/CHANGELOG.md b/plugins/deep-link/examples/app/CHANGELOG.md index e7471efdb..df49cc072 100644 --- a/plugins/deep-link/examples/app/CHANGELOG.md +++ b/plugins/deep-link/examples/app/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.2.14] + +### Dependencies + +- Upgraded to `deep-link-js@2.5.0` + ## [2.2.13] ### Dependencies diff --git a/plugins/deep-link/examples/app/package.json b/plugins/deep-link/examples/app/package.json index aa43ed40c..31e1bd09b 100644 --- a/plugins/deep-link/examples/app/package.json +++ b/plugins/deep-link/examples/app/package.json @@ -1,7 +1,7 @@ { "name": "deep-link-example", "private": true, - "version": "2.2.13", + "version": "2.2.14", "type": "module", "scripts": { "dev": "vite", diff --git a/plugins/deep-link/package.json b/plugins/deep-link/package.json index fb2351dfa..cb4b66693 100644 --- a/plugins/deep-link/package.json +++ b/plugins/deep-link/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-deep-link", - "version": "2.4.10", + "version": "2.5.0", "description": "Set your Tauri application as the default handler for an URL", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/dialog/CHANGELOG.md b/plugins/dialog/CHANGELOG.md index 4af3e7d38..30e6e7354 100644 --- a/plugins/dialog/CHANGELOG.md +++ b/plugins/dialog/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [2.8.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + +### Dependencies + +- Upgraded to `fs-js@2.6.0` + ## [2.7.3] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -339,3 +349,6 @@ - [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! d6e80b)([#545](https://github.com/tauri-apps/plugins-workspace/pull/545)) Fixes docs.rs build by enabling the `tauri/dox` feature flag. g. +apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! + d6e80b)([#545](https://github.com/tauri-apps/plugins-workspace/pull/545)) Fixes docs.rs build by enabling the `tauri/dox` feature flag. +g. diff --git a/plugins/dialog/Cargo.toml b/plugins/dialog/Cargo.toml index 3064b1efa..ca573ee3a 100644 --- a/plugins/dialog/Cargo.toml +++ b/plugins/dialog/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-dialog" -version = "2.7.3" +version = "2.8.0" description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application." edition = { workspace = true } authors = { workspace = true } @@ -45,7 +45,7 @@ tauri = { workspace = true } log = { workspace = true } thiserror = { workspace = true } url = { workspace = true } -tauri-plugin-fs = { path = "../fs", version = "2.5.2" } +tauri-plugin-fs = { path = "../fs", version = "2.6.0" } [target.'cfg(target_os = "ios")'.dependencies] tauri = { workspace = true, features = ["wry"] } diff --git a/plugins/dialog/package.json b/plugins/dialog/package.json index 52e773082..8b4defecb 100644 --- a/plugins/dialog/package.json +++ b/plugins/dialog/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-dialog", - "version": "2.7.3", + "version": "2.8.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/fs/CHANGELOG.md b/plugins/fs/CHANGELOG.md index 2622d1359..d2dbe350b 100644 --- a/plugins/fs/CHANGELOG.md +++ b/plugins/fs/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [2.6.0] + +- [`97e865bc`](https://github.com/tauri-apps/plugins-workspace/commit/97e865bc138925ba29b102a9cb99d406ce1fe48e) ([#3623](https://github.com/tauri-apps/plugins-workspace/pull/3623)) Added an `options` parameter with `baseDir` to `size`, so relative paths can be measured, like the other functions and like the JSDoc example already showed. The Rust command already accepted it. +- [`9106093f`](https://github.com/tauri-apps/plugins-workspace/commit/9106093f775a09f899561d7823518e76f9bd6b45) ([#3624](https://github.com/tauri-apps/plugins-workspace/pull/3624)) Fixed opening Android `content://` URIs with `append` or with `write`, `truncate` and `append` together: the options were turned into invalid modes such as `ra` or `wta`. They now map to the modes Android accepts (`r`, `w`, `wt`, `wa`, `rw`, `rwt`), `append` taking precedence over `read` since there is no read and append mode. +- [`1cae06a5`](https://github.com/tauri-apps/plugins-workspace/commit/1cae06a55b3398d409fba5ac17dbac63d9cf7652) ([#3625](https://github.com/tauri-apps/plugins-workspace/pull/3625)) Fixed `readTextFileLines` yielding empty lines forever when reading fails (e.g. on a directory). The read error is now reported: the iterator rejects and the file is closed. +- [`1cae06a5`](https://github.com/tauri-apps/plugins-workspace/commit/1cae06a55b3398d409fba5ac17dbac63d9cf7652) ([#3625](https://github.com/tauri-apps/plugins-workspace/pull/3625)) Fixed `readTextFileLines` leaving the file open until the webview is destroyed when a `for await` loop over it exits early (`break`, `return` or `throw`). The iterator now implements `return()`, which closes the file. +- [`0c84c917`](https://github.com/tauri-apps/plugins-workspace/commit/0c84c917e32af391fed9eaac9e131e447d0a1148) ([#3621](https://github.com/tauri-apps/plugins-workspace/pull/3621)) File watcher errors (e.g. the watched directory was removed or the inotify limit was reached) are now logged instead of being silently dropped. +- [`b46a88ff`](https://github.com/tauri-apps/plugins-workspace/commit/b46a88ff55948367e40a90f4fcec807f8e43cbc9) ([#3622](https://github.com/tauri-apps/plugins-workspace/pull/3622)) `writeFile` and `writeTextFile` now fail when their options cannot be parsed instead of silently ignoring them (including `baseDir`, `append` and `createNew`) and writing to the raw path. Data sent as a JSON array is rejected if it contains values that are not bytes, instead of truncating them. +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.5.2] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -230,3 +242,8 @@ ## \[2.0.0-alpha.0] - [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! +55](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/fs/Cargo.toml b/plugins/fs/Cargo.toml index 74f7f6ac9..0240517af 100644 --- a/plugins/fs/Cargo.toml +++ b/plugins/fs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-fs" -version = "2.5.2" +version = "2.6.0" description = "Access the file system." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/fs/package.json b/plugins/fs/package.json index f5ea9f292..8fa619ff5 100644 --- a/plugins/fs/package.json +++ b/plugins/fs/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-fs", - "version": "2.5.2", + "version": "2.6.0", "description": "Access the file system.", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/geolocation/CHANGELOG.md b/plugins/geolocation/CHANGELOG.md index cbb3cef79..47d9ab4f6 100644 --- a/plugins/geolocation/CHANGELOG.md +++ b/plugins/geolocation/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.4.0] + +- [`d869c162`](https://github.com/tauri-apps/plugins-workspace/commit/d869c162a7d1e95629a3acbf0084bbeac3e10f77) Fixed a build failure when the `specta` feature is enabled: the `Error` type used the `#[serde(skip)]` helper attribute, which recent `specta` versions no longer register. It now uses `#[specta(skip)]`. +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.3.3] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -77,3 +84,6 @@ - [`9606089b`](https://github.com/tauri-apps/plugins-workspace/commit/9606089b2add4a17f80ed5a09d59ce94824bd672) ([#1599](https://github.com/tauri-apps/plugins-workspace/pull/1599)) Initial release. - [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. +ttps://github.com/tauri-apps/plugins-workspace/pull/1599)) Initial release. +- [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. + tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC. diff --git a/plugins/geolocation/Cargo.toml b/plugins/geolocation/Cargo.toml index bfa109e21..4f72e3409 100644 --- a/plugins/geolocation/Cargo.toml +++ b/plugins/geolocation/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tauri-plugin-geolocation" description = "Get and track the device's current position" -version = "2.3.3" +version = "2.4.0" edition = { workspace = true } rust-version = { workspace = true } authors = { workspace = true } diff --git a/plugins/geolocation/package.json b/plugins/geolocation/package.json index 157fccfc8..2f9db241a 100644 --- a/plugins/geolocation/package.json +++ b/plugins/geolocation/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-geolocation", - "version": "2.3.3", + "version": "2.4.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/global-shortcut/CHANGELOG.md b/plugins/global-shortcut/CHANGELOG.md index 4ac214112..eeaf73b31 100644 --- a/plugins/global-shortcut/CHANGELOG.md +++ b/plugins/global-shortcut/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.4.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## \[2.3.2] - [`cb863d57`](https://github.com/tauri-apps/plugins-workspace/commit/cb863d57c60790efc41b2bd77d065bd91b4ef2b1) ([#3408](https://github.com/tauri-apps/plugins-workspace/pull/3408) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update crate `global-hotkey` to 0.8 to fix cpu usage while pressing the keys on windows and to add F13-F24 mappings on Linux/X11. diff --git a/plugins/global-shortcut/Cargo.toml b/plugins/global-shortcut/Cargo.toml index 6d31703c9..9a244ee55 100644 --- a/plugins/global-shortcut/Cargo.toml +++ b/plugins/global-shortcut/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-global-shortcut" -version = "2.3.2" +version = "2.4.0" description = "Register global hotkeys listeners on your Tauri application." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/global-shortcut/package.json b/plugins/global-shortcut/package.json index 5ecd41fe1..fed190577 100644 --- a/plugins/global-shortcut/package.json +++ b/plugins/global-shortcut/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-global-shortcut", - "version": "2.3.2", + "version": "2.4.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/haptics/CHANGELOG.md b/plugins/haptics/CHANGELOG.md index e83e3efc8..56684cd55 100644 --- a/plugins/haptics/CHANGELOG.md +++ b/plugins/haptics/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.4.0] + +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Migrate the Android Gradle scripts from the deprecated `kotlinOptions` DSL to `compilerOptions`, which Kotlin Gradle Plugin 2.3+ requires, to match the Tauri 2.12 Android library and templates. +- [`d869c162`](https://github.com/tauri-apps/plugins-workspace/commit/d869c162a7d1e95629a3acbf0084bbeac3e10f77) Fixed a build failure when the `specta` feature is enabled: the `Error` type used the `#[serde(skip)]` helper attribute, which recent `specta` versions no longer register. It now uses `#[specta(skip)]`. +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.3.3] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. diff --git a/plugins/haptics/Cargo.toml b/plugins/haptics/Cargo.toml index 3b368e675..8a3c37f47 100644 --- a/plugins/haptics/Cargo.toml +++ b/plugins/haptics/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tauri-plugin-haptics" description = "Haptic feedback and vibrations on Android and iOS" -version = "2.3.3" +version = "2.4.0" edition = { workspace = true } rust-version = { workspace = true } authors = { workspace = true } diff --git a/plugins/haptics/package.json b/plugins/haptics/package.json index c39d3a820..20991c728 100644 --- a/plugins/haptics/package.json +++ b/plugins/haptics/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-haptics", - "version": "2.3.3", + "version": "2.4.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/http/CHANGELOG.md b/plugins/http/CHANGELOG.md index ed6b948ac..618d599a1 100644 --- a/plugins/http/CHANGELOG.md +++ b/plugins/http/CHANGELOG.md @@ -2,6 +2,16 @@ ## [2.7.0] +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + +### Dependencies + +- Upgraded to `fs-js@2.6.0` + +## [2.7.0] + - [`1198a524`](https://github.com/tauri-apps/plugins-workspace/commit/1198a524b710abf2abeb1d9bd7b252402d26ca6d) **Security:** Added the `scopeRedirects` plugin configuration option, which checks the URL scope on every hop of a redirect chain instead of only on the URL requested by the frontend. Without it, a server on an allowed origin can redirect the request to any other origin - including `localhost` services, internal hosts and cloud metadata endpoints - and the plugin follows it, returning the response to the webview. ```json diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml index f0eeadd3f..6e7a75c67 100644 --- a/plugins/http/Cargo.toml +++ b/plugins/http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-http" -version = "2.7.0" +version = "2.8.0" description = "Access an HTTP client written in Rust." edition = { workspace = true } authors = { workspace = true } @@ -37,7 +37,7 @@ serde_json = { workspace = true } tauri = { workspace = true } thiserror = { workspace = true } tokio = { version = "1", features = ["sync", "macros"] } -tauri-plugin-fs = { path = "../fs", version = "2.5.2" } +tauri-plugin-fs = { path = "../fs", version = "2.6.0" } urlpattern = "0.3" regex = "1" http = "1" diff --git a/plugins/http/package.json b/plugins/http/package.json index fce5f0d08..5b41ff8a7 100644 --- a/plugins/http/package.json +++ b/plugins/http/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-http", - "version": "2.6.1", + "version": "2.7.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/localhost/CHANGELOG.md b/plugins/localhost/CHANGELOG.md index c9e7c6474..6e0990466 100644 --- a/plugins/localhost/CHANGELOG.md +++ b/plugins/localhost/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [2.4.0] + +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## \[2.3.2] - [`8bfa4450`](https://github.com/tauri-apps/plugins-workspace/commit/8bfa4450230d6a00f836bd27944c34cd7fe43e08) ([#3112](https://github.com/tauri-apps/plugins-workspace/pull/3112) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Disable caching on responses. diff --git a/plugins/localhost/Cargo.toml b/plugins/localhost/Cargo.toml index c37476ab9..f2c627bcd 100644 --- a/plugins/localhost/Cargo.toml +++ b/plugins/localhost/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-localhost" -version = "2.3.2" +version = "2.4.0" description = "Expose your apps assets through a localhost server instead of the default custom protocol." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/log/CHANGELOG.md b/plugins/log/CHANGELOG.md index 11aea9e12..6901fbf0e 100644 --- a/plugins/log/CHANGELOG.md +++ b/plugins/log/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.10.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.9.2] - [`2df3d936`](https://github.com/tauri-apps/plugins-workspace/commit/2df3d9368187e1d2ce736d3e0b2d4bdcae35b011) ([#3574](https://github.com/tauri-apps/plugins-workspace/pull/3574)) Log webview log target locations after 2 colons instead of 1. (e.g. `webview:myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4` to `webview::myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4`) diff --git a/plugins/log/Cargo.toml b/plugins/log/Cargo.toml index 1bf62bb18..af265b080 100644 --- a/plugins/log/Cargo.toml +++ b/plugins/log/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-log" -version = "2.9.2" +version = "2.10.0" description = "Configurable logging for your Tauri app." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/log/package.json b/plugins/log/package.json index 40347f20e..faeb1220c 100644 --- a/plugins/log/package.json +++ b/plugins/log/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-log", - "version": "2.9.2", + "version": "2.10.0", "description": "Configurable logging for your Tauri app.", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/nfc/CHANGELOG.md b/plugins/nfc/CHANGELOG.md index 723981e96..6f75728b2 100644 --- a/plugins/nfc/CHANGELOG.md +++ b/plugins/nfc/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.4.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.3.6] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -114,3 +120,7 @@ . commit/fe79adb5c7febd0e912efb5581264d671709fbb0)([#830](https://github.com/tauri-apps/plugins-workspace/pull/830)) Initial release. ithub.com/tauri-apps/plugins-workspace/pull/830)) Initial release. +264d671709fbb0)([#830](https://github.com/tauri-apps/plugins-workspace/pull/830)) Initial release. + . + commit/fe79adb5c7febd0e912efb5581264d671709fbb0)([#830](https://github.com/tauri-apps/plugins-workspace/pull/830)) Initial release. + ithub.com/tauri-apps/plugins-workspace/pull/830)) Initial release. diff --git a/plugins/nfc/Cargo.toml b/plugins/nfc/Cargo.toml index 13bd3b061..159eaa7d4 100644 --- a/plugins/nfc/Cargo.toml +++ b/plugins/nfc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-nfc" -version = "2.3.6" +version = "2.4.0" description = "Read and write NFC tags on Android and iOS." edition = { workspace = true } rust-version = { workspace = true } diff --git a/plugins/nfc/package.json b/plugins/nfc/package.json index 1d8d9a75f..501267807 100644 --- a/plugins/nfc/package.json +++ b/plugins/nfc/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-nfc", - "version": "2.3.6", + "version": "2.4.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/notification/CHANGELOG.md b/plugins/notification/CHANGELOG.md index 48c2f4acc..d9a2a9ac9 100644 --- a/plugins/notification/CHANGELOG.md +++ b/plugins/notification/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [2.5.0] + +- [`c8c373b4`](https://github.com/tauri-apps/plugins-workspace/commit/c8c373b48bbf06b57c481dad4cf0a68638c68478) Fixed broken mobile APIs: + + - `channels()` invoked the `listChannels` command, which the `allow-list-channels` permission does not match; it now invokes `list_channels`. + - Unregistering an `onNotificationReceived` or `onAction` listener invokes the `remove_listener` command, which had no permission. It is now declared and part of the `default` permission set (`allow-remove-listener`). + - `cancelAll()` was rejected on Android and iOS, which required the list of notifications to cancel. It now cancels every pending notification. + - `active()` resolved with `{ values: [...] }` instead of an array on Android. +- [`684feb25`](https://github.com/tauri-apps/plugins-workspace/commit/684feb2510f468f002c57f7bdebf675cf5c1b1d9) ([#3617](https://github.com/tauri-apps/plugins-workspace/pull/3617)) Fixed `isPermissionGranted()` always resolving to `false` on Windows. The initialization script short-circuited the permission check to avoid invoking the backend, but read the permission back before it had been set, so it settled on `denied` instead of `granted`. +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.4.0] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -163,3 +176,9 @@ ## \[2.0.0-alpha.0] - [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! +`d8b4aca`](https://github.com/tauri-apps/plugins-workspace/commit/d8b4aca69f628b170804ecb982e2c319d026ef47)([#414](https://github.com/tauri-apps/plugins-workspace/pull/414)) Use `window.__TAURI_INVOKE__` instead of `window.__TAURI__` in init.js, fixes usage in apps without `withGlobalTauri` enabled. +- [`7d71ad4`](https://github.com/tauri-apps/plugins-workspace/commit/7d71ad4e587bcf47ea34645f5b226945e487b765) Play a default sound when showing a notification on Windows. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/notification/Cargo.toml b/plugins/notification/Cargo.toml index 256503e8c..f4f5ed281 100644 --- a/plugins/notification/Cargo.toml +++ b/plugins/notification/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-notification" -version = "2.4.0" +version = "2.5.0" description = "Send desktop and mobile notifications on your Tauri application." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/notification/package.json b/plugins/notification/package.json index dafb8c116..7bc877576 100644 --- a/plugins/notification/package.json +++ b/plugins/notification/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-notification", - "version": "2.4.0", + "version": "2.5.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/notification/src/desktop.rs b/plugins/notification/src/desktop.rs index b86b74f2a..ae115fdbd 100644 --- a/plugins/notification/src/desktop.rs +++ b/plugins/notification/src/desktop.rs @@ -246,6 +246,7 @@ mod imp { /// Shows the notification. Same as [`Self::show`]. #[cfg(feature = "windows7-compat")] + #[allow(dead_code)] #[cfg_attr(docsrs, doc(cfg(feature = "windows7-compat")))] #[deprecated = "Tauri no longer supports Windows 7, use `Self::show` instead."] pub fn notify(self, _app: &tauri::AppHandle) -> crate::Result<()> { diff --git a/plugins/opener/CHANGELOG.md b/plugins/opener/CHANGELOG.md index e11e047ba..34387a2fb 100644 --- a/plugins/opener/CHANGELOG.md +++ b/plugins/opener/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.6.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.5.5] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -78,3 +84,8 @@ ## \[2.0.0] - [`383e636a`](https://github.com/tauri-apps/plugins-workspace/commit/383e636a8e595aec1300999a8aeb7d9bf8c14632) ([#2019](https://github.com/tauri-apps/plugins-workspace/pull/2019) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Initial Release +h each other. + +## \[2.0.0] + +- [`383e636a`](https://github.com/tauri-apps/plugins-workspace/commit/383e636a8e595aec1300999a8aeb7d9bf8c14632) ([#2019](https://github.com/tauri-apps/plugins-workspace/pull/2019) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Initial Release diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index 1c3f7060e..9a6346419 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-opener" -version = "2.5.5" +version = "2.6.0" description = "Open files and URLs using their default application." edition = { workspace = true } rust-version = { workspace = true } diff --git a/plugins/opener/package.json b/plugins/opener/package.json index 5ed03db3f..8c59c74c9 100644 --- a/plugins/opener/package.json +++ b/plugins/opener/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-opener", - "version": "2.5.5", + "version": "2.6.0", "description": "Open files and URLs using their default application.", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/os/CHANGELOG.md b/plugins/os/CHANGELOG.md index 603b2433d..8e90ad250 100644 --- a/plugins/os/CHANGELOG.md +++ b/plugins/os/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.4.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## \[2.3.2] - [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. diff --git a/plugins/os/Cargo.toml b/plugins/os/Cargo.toml index c4b3e5a4c..9526cdcb1 100644 --- a/plugins/os/Cargo.toml +++ b/plugins/os/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-os" -version = "2.3.2" +version = "2.4.0" description = "Read information about the operating system." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/os/package.json b/plugins/os/package.json index ebe97456b..ddcce7094 100644 --- a/plugins/os/package.json +++ b/plugins/os/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-os", - "version": "2.3.2", + "version": "2.4.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/persisted-scope/CHANGELOG.md b/plugins/persisted-scope/CHANGELOG.md index 3dccf26b1..482561847 100644 --- a/plugins/persisted-scope/CHANGELOG.md +++ b/plugins/persisted-scope/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [2.4.0] + +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + +### Dependencies + +- Upgraded to `fs@2.6.0` + ## [2.3.8] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. diff --git a/plugins/persisted-scope/Cargo.toml b/plugins/persisted-scope/Cargo.toml index 507b5ca28..5f2795b50 100644 --- a/plugins/persisted-scope/Cargo.toml +++ b/plugins/persisted-scope/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-persisted-scope" -version = "2.3.8" +version = "2.4.0" description = "Save filesystem and asset scopes and restore them when the app is reopened." authors = { workspace = true } license = { workspace = true } @@ -24,7 +24,7 @@ log = { workspace = true } thiserror = { workspace = true } aho-corasick = "1" bincode = "1" -tauri-plugin-fs = { path = "../fs", version = "2.5.2" } +tauri-plugin-fs = { path = "../fs", version = "2.6.0" } [features] protocol-asset = ["tauri/protocol-asset"] diff --git a/plugins/positioner/CHANGELOG.md b/plugins/positioner/CHANGELOG.md index f4710bb91..abd9b4887 100644 --- a/plugins/positioner/CHANGELOG.md +++ b/plugins/positioner/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.4.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.3.4] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. diff --git a/plugins/positioner/Cargo.toml b/plugins/positioner/Cargo.toml index 634eaa046..4d5542c4b 100644 --- a/plugins/positioner/Cargo.toml +++ b/plugins/positioner/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-positioner" -version = "2.3.4" +version = "2.4.0" description = "Position your windows at well-known locations." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/positioner/package.json b/plugins/positioner/package.json index 5531c33c8..97e23a64e 100644 --- a/plugins/positioner/package.json +++ b/plugins/positioner/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-positioner", - "version": "2.3.4", + "version": "2.4.0", "description": "Position your windows at well-known locations.", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/process/CHANGELOG.md b/plugins/process/CHANGELOG.md index b5f88a609..dd75654fd 100644 --- a/plugins/process/CHANGELOG.md +++ b/plugins/process/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.4.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## \[2.3.1] - [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. diff --git a/plugins/process/Cargo.toml b/plugins/process/Cargo.toml index c586cb215..dd3b28607 100644 --- a/plugins/process/Cargo.toml +++ b/plugins/process/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-process" -version = "2.3.1" +version = "2.4.0" description = "Access the current process of your Tauri application." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/process/package.json b/plugins/process/package.json index 7cd42a190..752d26b21 100644 --- a/plugins/process/package.json +++ b/plugins/process/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-process", - "version": "2.3.1", + "version": "2.4.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/shell/CHANGELOG.md b/plugins/shell/CHANGELOG.md index e8657a19d..3f7cdaa86 100644 --- a/plugins/shell/CHANGELOG.md +++ b/plugins/shell/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.4.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.3.6] - [`db9c5998`](https://github.com/tauri-apps/plugins-workspace/commit/db9c5998feff9384f9cbbefcbe0d45937c00a1fc) ([#3531](https://github.com/tauri-apps/plugins-workspace/pull/3531) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix missing `consumer-rules.pro` on Gradle v9. @@ -157,3 +163,8 @@ ## \[2.0.0-alpha.0] - [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! +55](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/shell/Cargo.toml b/plugins/shell/Cargo.toml index 105ea5f1e..4c742ca54 100644 --- a/plugins/shell/Cargo.toml +++ b/plugins/shell/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-shell" -version = "2.3.6" +version = "2.4.0" description = "Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/shell/package.json b/plugins/shell/package.json index d7c259a18..038b2950c 100644 --- a/plugins/shell/package.json +++ b/plugins/shell/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-shell", - "version": "2.3.6", + "version": "2.4.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/single-instance/CHANGELOG.md b/plugins/single-instance/CHANGELOG.md index 3e4c94aab..606aef233 100644 --- a/plugins/single-instance/CHANGELOG.md +++ b/plugins/single-instance/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [2.5.0] + +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + +### Dependencies + +- Upgraded to `deep-link@2.5.0` + ## [2.4.5] - [`67cd25a1`](https://github.com/tauri-apps/plugins-workspace/commit/67cd25a10ce3deb1b935e725228e528766f24ad7) ([#3592](https://github.com/tauri-apps/plugins-workspace/pull/3592)) On Windows, the second instance now allows the first instance to bring its window to the front before exiting, so focusing a window from the callback no longer gets refused by Windows. diff --git a/plugins/single-instance/Cargo.toml b/plugins/single-instance/Cargo.toml index e636a7f57..d58fad59b 100644 --- a/plugins/single-instance/Cargo.toml +++ b/plugins/single-instance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-single-instance" -version = "2.4.5" +version = "2.5.0" description = "Ensure a single instance of your tauri app is running." authors = { workspace = true } license = { workspace = true } @@ -22,7 +22,7 @@ serde_json = { workspace = true } tauri = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } -tauri-plugin-deep-link = { path = "../deep-link", version = "2.4.10", optional = true } +tauri-plugin-deep-link = { path = "../deep-link", version = "2.5.0", optional = true } semver = { version = "1", optional = true } tokio = { version = "1", features = ["net"] } diff --git a/plugins/sql/CHANGELOG.md b/plugins/sql/CHANGELOG.md index 35c93c58b..ffc28d76c 100644 --- a/plugins/sql/CHANGELOG.md +++ b/plugins/sql/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.5.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.4.1] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. diff --git a/plugins/sql/Cargo.toml b/plugins/sql/Cargo.toml index 4cb324af8..38c2b5886 100644 --- a/plugins/sql/Cargo.toml +++ b/plugins/sql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-sql" -version = "2.4.1" +version = "2.5.0" description = "Interface with SQL databases." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/sql/package.json b/plugins/sql/package.json index 5c0858b33..c96e9ad21 100644 --- a/plugins/sql/package.json +++ b/plugins/sql/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-sql", - "version": "2.4.1", + "version": "2.5.0", "description": "Interface with SQL databases", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/store/CHANGELOG.md b/plugins/store/CHANGELOG.md index beb8d85f2..30a9541da 100644 --- a/plugins/store/CHANGELOG.md +++ b/plugins/store/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.5.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.4.5] - [`c050e073`](https://github.com/tauri-apps/plugins-workspace/commit/c050e073b68205bae7389fb86958cb9331e1a3be) ([#3572](https://github.com/tauri-apps/plugins-workspace/pull/3572) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `apply_pending_auto_save` can deadlock the store diff --git a/plugins/store/Cargo.toml b/plugins/store/Cargo.toml index 3d71d0fef..c13b8dfe4 100644 --- a/plugins/store/Cargo.toml +++ b/plugins/store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-store" -version = "2.4.5" +version = "2.5.0" description = "Simple, persistent key-value store." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/store/package.json b/plugins/store/package.json index 81dc4fa87..b35a6a98a 100644 --- a/plugins/store/package.json +++ b/plugins/store/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-store", - "version": "2.4.5", + "version": "2.5.0", "description": "Simple, persistent key-value store.", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/stronghold/CHANGELOG.md b/plugins/stronghold/CHANGELOG.md index 600f2eb12..8e7d54a97 100644 --- a/plugins/stronghold/CHANGELOG.md +++ b/plugins/stronghold/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.4.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.3.2] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. diff --git a/plugins/stronghold/Cargo.toml b/plugins/stronghold/Cargo.toml index e6038a007..7f88884b2 100644 --- a/plugins/stronghold/Cargo.toml +++ b/plugins/stronghold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-stronghold" -version = "2.3.2" +version = "2.4.0" description = "Store secrets and keys using the IOTA Stronghold secret management engine." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/stronghold/package.json b/plugins/stronghold/package.json index e5de693bf..881eacbed 100644 --- a/plugins/stronghold/package.json +++ b/plugins/stronghold/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-stronghold", - "version": "2.3.2", + "version": "2.4.0", "description": "Store secrets and keys using the IOTA Stronghold encrypted database.", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/updater/CHANGELOG.md b/plugins/updater/CHANGELOG.md index 6a5484a8b..11efba98f 100644 --- a/plugins/updater/CHANGELOG.md +++ b/plugins/updater/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [2.13.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + +### bug + +- [`101e1b06`](https://github.com/tauri-apps/plugins-workspace/commit/101e1b068a62aeece725362c38f8fe4179b5fe5f) ([#3615](https://github.com/tauri-apps/plugins-workspace/pull/3615)) On Linux, `check()` no longer sets `SSL_CERT_FILE` and `SSL_CERT_DIR` to Debian paths. The override pointed rustls to files that do not exist on distros with a different layout, such as ALT Linux, and left every TLS client in the app without root certificates. + ## [2.12.0] - [`1308bfa3`](https://github.com/tauri-apps/plugins-workspace/commit/1308bfa399b962b3977c767100a6339d1cbfdd20) **Breaking change:** the `allowDowngrades` option was removed from the `check` command and is now read from the plugin configuration instead. diff --git a/plugins/updater/Cargo.toml b/plugins/updater/Cargo.toml index b14910e14..e9272eb96 100644 --- a/plugins/updater/Cargo.toml +++ b/plugins/updater/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-updater" -version = "2.12.0" +version = "2.13.0" description = "In-app updates for Tauri applications." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/updater/package.json b/plugins/updater/package.json index 1d8a10d52..812d3a24d 100644 --- a/plugins/updater/package.json +++ b/plugins/updater/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-updater", - "version": "2.12.0", + "version": "2.13.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index d46861070..628b800e7 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -22,7 +22,7 @@ use reqwest::{ }; use semver::Version; use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError}; -#[cfg(windows)] +#[cfg(any(windows, target_os = "linux"))] use std::ffi::OsStr; #[cfg(desktop)] use std::io::Cursor; diff --git a/plugins/upload/CHANGELOG.md b/plugins/upload/CHANGELOG.md index ccd5dc3b0..dfc211a94 100644 --- a/plugins/upload/CHANGELOG.md +++ b/plugins/upload/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.5.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.4.1] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. diff --git a/plugins/upload/Cargo.toml b/plugins/upload/Cargo.toml index c46c6d2aa..6fb518253 100644 --- a/plugins/upload/Cargo.toml +++ b/plugins/upload/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-upload" -version = "2.4.1" +version = "2.5.0" description = "Upload files from disk to a remote server over HTTP." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/upload/package.json b/plugins/upload/package.json index 4694523fb..69df5bc64 100644 --- a/plugins/upload/package.json +++ b/plugins/upload/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-upload", - "version": "2.4.1", + "version": "2.5.0", "description": "Upload files from disk to a remote server over HTTP.", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/websocket/CHANGELOG.md b/plugins/websocket/CHANGELOG.md index bfb94e1b9..8de02456f 100644 --- a/plugins/websocket/CHANGELOG.md +++ b/plugins/websocket/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.5.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## [2.4.3] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. diff --git a/plugins/websocket/Cargo.toml b/plugins/websocket/Cargo.toml index 1e256d272..4516c5322 100644 --- a/plugins/websocket/Cargo.toml +++ b/plugins/websocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-websocket" -version = "2.4.3" +version = "2.5.0" description = "Expose a WebSocket server to your Tauri frontend." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/websocket/package.json b/plugins/websocket/package.json index b307dc0a2..1b2a52a59 100644 --- a/plugins/websocket/package.json +++ b/plugins/websocket/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-websocket", - "version": "2.4.3", + "version": "2.5.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/window-state/CHANGELOG.md b/plugins/window-state/CHANGELOG.md index 0c212a861..7510b45ce 100644 --- a/plugins/window-state/CHANGELOG.md +++ b/plugins/window-state/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [2.5.0] + +- [`ae3c808e`](https://github.com/tauri-apps/plugins-workspace/commit/ae3c808eb60086f32b1d67bb8ccc525b95333e77) ([#3602](https://github.com/tauri-apps/plugins-workspace/pull/3602)) The plugin's global API script (used with `app.withGlobalTauri`) now resolves the core API from `window.__TAURI__` instead of bundling its own copy of `@tauri-apps/api`. Values created with the core API are now accepted by plugin APIs in global mode (e.g. an `Image` from `window.__TAURI__.image` passed to `clipboardManager.writeImage`, which previously failed the `instanceof` check against the plugin's private copy), and the script is considerably smaller. +- [`9b29b601`](https://github.com/tauri-apps/plugins-workspace/commit/9b29b601f387ded1f4adb4386623a3c035a5598c) Update MSRV to 1.90 to match tauri. +- [`a87a3c7d`](https://github.com/tauri-apps/plugins-workspace/commit/a87a3c7d4491bf25589a0b24c285fcd5df3d6337) Update documentation. + ## \[2.4.1] - [`93426f85`](https://github.com/tauri-apps/plugins-workspace/commit/93426f85120f49beb9f40222bff45185a32d54a9) Fixed an issue that caused docs.rs builds to fail. No user facing changes. diff --git a/plugins/window-state/Cargo.toml b/plugins/window-state/Cargo.toml index 6f7592101..46b6aab38 100644 --- a/plugins/window-state/Cargo.toml +++ b/plugins/window-state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-window-state" -version = "2.4.1" +version = "2.5.0" description = "Save window positions and sizes and restore them when the app is reopened." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/window-state/package.json b/plugins/window-state/package.json index 1ac60b2b7..7b670cd93 100644 --- a/plugins/window-state/package.json +++ b/plugins/window-state/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-window-state", - "version": "2.4.1", + "version": "2.5.0", "description": "Save window positions and sizes and restore them when the app is reopened.", "license": "MIT OR Apache-2.0", "authors": [ From 67ebc1396bfd4b437530f9bbe8bed6845ade8fc3 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sat, 26 Sep 2026 17:29:03 -0300 Subject: [PATCH 29/29] fix(ci): check-publish job should check .changes/pre.json --- .github/workflows/covector-version-or-publish.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/covector-version-or-publish.yml b/.github/workflows/covector-version-or-publish.yml index 0e58b55b7..bddadb272 100644 --- a/.github/workflows/covector-version-or-publish.yml +++ b/.github/workflows/covector-version-or-publish.yml @@ -19,8 +19,9 @@ permissions: pull-requests: write jobs: - # covector publishes once no change files are left (the version update PR consumed them) and - # otherwise opens that PR, which does not need the tests below. + # covector publishes once no pending change files are left (the version update PR consumed them) + # and otherwise opens that PR, which does not need the tests below. In pre-release mode the + # change files stay in `.changes` and `pre.json` lists the ones already released. check-publish: runs-on: ubuntu-latest outputs: @@ -29,7 +30,12 @@ jobs: - uses: actions/checkout@v7 - id: check run: | - if find .changes -maxdepth 1 -name '*.md' ! -iname 'readme.md' | grep -q .; then + released=$(if [ -f .changes/pre.json ]; then jq -r '.changes[]' .changes/pre.json; fi) + # grep exits 1 when every change file was released; any other failure fails the step + pending=$(find .changes -maxdepth 1 -name '*.md' ! -iname 'readme.md' \ + | { grep -vxF -e "$released" || [ $? -eq 1 ]; }) + if [ -n "$pending" ]; then + printf 'pending change files:\n%s\n' "$pending" echo "publish=false" >> "$GITHUB_OUTPUT" else echo "publish=true" >> "$GITHUB_OUTPUT"