Przeglądaj źródła

feat(core): allow `dev_path`, `dist_dir` as array of paths, fixes #1897 (#1926)

* feat(core): allow `dev_path`, `dist_dir` as array of paths, fixes #1897

* fix: clippy
Lucas Fernandes Nogueira 4 lat temu
rodzic
commit
6ec54c53b5

+ 7 - 0
.changes/dev-path-dist-dir-array.md

@@ -0,0 +1,7 @@
+---
+"tauri": patch
+"tauri-codegen": patch
+"tauri-utils": patch
+---
+
+Allow `dev_path` and `dist_dir` to be an array of root files and directories to embed.

+ 33 - 30
core/tauri-codegen/src/context.rs

@@ -6,7 +6,7 @@ use crate::embedded_assets::{AssetOptions, EmbeddedAssets, EmbeddedAssetsError};
 use proc_macro2::TokenStream;
 use quote::quote;
 use std::path::PathBuf;
-use tauri_utils::config::{Config, WindowUrl};
+use tauri_utils::config::{AppUrl, Config, WindowUrl};
 
 /// Necessary data needed by [`context_codegen`] to generate code for a Tauri application context.
 pub struct ContextData {
@@ -24,44 +24,47 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     config_parent,
     root,
   } = data;
+
+  let mut options = AssetOptions::new();
+  if let Some(csp) = &config.tauri.security.csp {
+    options = options.csp(csp.clone());
+  }
+
   let app_url = if dev {
     &config.build.dev_path
   } else {
     &config.build.dist_dir
   };
-  let assets_path = match app_url {
-    WindowUrl::External(_) => None,
-    WindowUrl::App(path) => {
-      if path.components().count() == 0 {
-        panic!(
-          "The `{}` configuration cannot be empty",
-          if dev { "devPath" } else { "distDir" }
-        )
-      }
-      let assets_path = config_parent.join(path);
-      if !assets_path.exists() {
-        panic!(
-          "The `{}` configuration is set to `{:?}` but this path doesn't exist",
-          if dev { "devPath" } else { "distDir" },
-          path
-        )
+
+  let assets = match app_url {
+    AppUrl::Url(url) => match url {
+      WindowUrl::External(_) => Default::default(),
+      WindowUrl::App(path) => {
+        if path.components().count() == 0 {
+          panic!(
+            "The `{}` configuration cannot be empty",
+            if dev { "devPath" } else { "distDir" }
+          )
+        }
+        let assets_path = config_parent.join(path);
+        if !assets_path.exists() {
+          panic!(
+            "The `{}` configuration is set to `{:?}` but this path doesn't exist",
+            if dev { "devPath" } else { "distDir" },
+            path
+          )
+        }
+        EmbeddedAssets::new(&assets_path, options)?
       }
-      Some(assets_path)
-    }
+      _ => unimplemented!(),
+    },
+    AppUrl::Files(files) => EmbeddedAssets::load_paths(
+      files.iter().map(|p| config_parent.join(p)).collect(),
+      options,
+    )?,
     _ => unimplemented!(),
   };
 
-  // generate the assets inside the dist dir into a perfect hash function
-  let assets = if let Some(assets_path) = assets_path {
-    let mut options = AssetOptions::new();
-    if let Some(csp) = &config.tauri.security.csp {
-      options = options.csp(csp.clone());
-    }
-    EmbeddedAssets::new(&assets_path, options)?
-  } else {
-    Default::default()
-  };
-
   // handle default window icons for Windows targets
   let default_window_icon = if cfg!(windows) {
     let icon_path = config

+ 44 - 0
core/tauri-codegen/src/embedded_assets.rs

@@ -106,6 +106,50 @@ impl EmbeddedAssets {
       .map(Self)
   }
 
+  /// Compress a list of files and directories.
+  pub fn load_paths(
+    paths: Vec<PathBuf>,
+    options: AssetOptions,
+  ) -> Result<Self, EmbeddedAssetsError> {
+    Ok(Self(
+      paths
+        .iter()
+        .map(|path| {
+          let is_file = path.is_file();
+          WalkDir::new(&path)
+            .follow_links(true)
+            .into_iter()
+            .filter_map(|entry| {
+              match entry {
+                // we only serve files, not directory listings
+                Ok(entry) if entry.file_type().is_dir() => None,
+
+                // compress all files encountered
+                Ok(entry) => Some(Self::compress_file(
+                  if is_file {
+                    path.parent().unwrap()
+                  } else {
+                    path
+                  },
+                  entry.path(),
+                  &options,
+                )),
+
+                // pass down error through filter to fail when encountering any error
+                Err(error) => Some(Err(EmbeddedAssetsError::Walkdir {
+                  path: path.to_path_buf(),
+                  error,
+                })),
+              }
+            })
+            .collect::<Result<Vec<Asset>, _>>()
+        })
+        .flatten()
+        .flatten()
+        .collect::<_>(),
+    ))
+  }
+
   /// Use highest compression level for release, the fastest one for everything else
   fn compression_level() -> i32 {
     let levels = zstd::compression_level_range();

+ 55 - 14
core/tauri-utils/src/config.rs

@@ -393,27 +393,40 @@ impl Default for TauriConfig {
   }
 }
 
+/// The `dev_path` and `dist_dir` options.
+#[derive(PartialEq, Debug, Clone, Deserialize)]
+#[serde(untagged)]
+#[non_exhaustive]
+pub enum AppUrl {
+  /// A url or file path.
+  Url(WindowUrl),
+  /// An array of files.
+  Files(Vec<PathBuf>),
+}
+
 /// The Build configuration object.
 #[derive(PartialEq, Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 pub struct BuildConfig {
   /// the devPath config.
   #[serde(default = "default_dev_path")]
-  pub dev_path: WindowUrl,
+  pub dev_path: AppUrl,
   /// the dist config.
   #[serde(default = "default_dist_path")]
-  pub dist_dir: WindowUrl,
+  pub dist_dir: AppUrl,
   /// Whether we should inject the Tauri API on `window.__TAURI__` or not.
   #[serde(default)]
   pub with_global_tauri: bool,
 }
 
-fn default_dev_path() -> WindowUrl {
-  WindowUrl::External(Url::parse("http://localhost:8080").unwrap())
+fn default_dev_path() -> AppUrl {
+  AppUrl::Url(WindowUrl::External(
+    Url::parse("http://localhost:8080").unwrap(),
+  ))
 }
 
-fn default_dist_path() -> WindowUrl {
-  WindowUrl::App("../dist".into())
+fn default_dist_path() -> AppUrl {
+  AppUrl::Url(WindowUrl::App("../dist".into()))
 }
 
 impl Default for BuildConfig {
@@ -465,7 +478,7 @@ pub struct PluginConfig(pub HashMap<String, JsonValue>);
 /// application using tauri while only parsing it once (in the build script).
 #[cfg(feature = "build")]
 mod build {
-  use std::convert::identity;
+  use std::{convert::identity, path::Path};
 
   use proc_macro2::TokenStream;
   use quote::{quote, ToTokens, TokenStreamExt};
@@ -511,6 +524,15 @@ mod build {
     quote! { vec![#(#items),*] }
   }
 
+  /// Create a `PathBuf` constructor `TokenStream`.
+  ///
+  /// e.g. `"Hello World" -> String::from("Hello World").
+  /// This takes a `&String` to reduce casting all the `&String` -> `&str` manually.
+  fn path_buf_lit(s: impl AsRef<Path>) -> TokenStream {
+    let s = s.as_ref().to_string_lossy().into_owned();
+    quote! { ::std::path::PathBuf::from(#s) }
+  }
+
   /// Create a map constructor, mapping keys and values with other `TokenStream`s.
   ///
   /// This function is pretty generic because the types of keys AND values get transformed.
@@ -612,8 +634,8 @@ mod build {
 
       tokens.append_all(match self {
         Self::App(path) => {
-          let path = path.to_string_lossy().to_string();
-          quote! { #prefix::App(::std::path::PathBuf::from(#path)) }
+          let path = path_buf_lit(&path);
+          quote! { #prefix::App(#path) }
         }
         Self::External(url) => {
           let url = url.as_str();
@@ -779,6 +801,22 @@ mod build {
     }
   }
 
+  impl ToTokens for AppUrl {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let prefix = quote! { ::tauri::api::config::AppUrl };
+
+      tokens.append_all(match self {
+        Self::Url(url) => {
+          quote! { #prefix::Url(#url) }
+        }
+        Self::Files(files) => {
+          let files = vec_lit(files, path_buf_lit);
+          quote! { #prefix::Files(#files) }
+        }
+      })
+    }
+  }
+
   impl ToTokens for BuildConfig {
     fn to_tokens(&self, tokens: &mut TokenStream) {
       let dev_path = &self.dev_path;
@@ -810,8 +848,7 @@ mod build {
 
   impl ToTokens for SystemTrayConfig {
     fn to_tokens(&self, tokens: &mut TokenStream) {
-      let icon_path = self.icon_path.to_string_lossy().to_string();
-      let icon_path = quote! { ::std::path::PathBuf::from(#icon_path) };
+      let icon_path = path_buf_lit(&self.icon_path);
       literal_struct!(tokens, SystemTrayConfig, icon_path);
     }
   }
@@ -936,8 +973,10 @@ mod test {
 
     // create a build config
     let build = BuildConfig {
-      dev_path: WindowUrl::External(Url::parse("http://localhost:8080").unwrap()),
-      dist_dir: WindowUrl::App("../dist".into()),
+      dev_path: AppUrl::Url(WindowUrl::External(
+        Url::parse("http://localhost:8080").unwrap(),
+      )),
+      dist_dir: AppUrl::Url(WindowUrl::App("../dist".into())),
       with_global_tauri: false,
     };
 
@@ -948,7 +987,9 @@ mod test {
     assert_eq!(d_updater, tauri.updater);
     assert_eq!(
       d_path,
-      WindowUrl::External(Url::parse("http://localhost:8080").unwrap())
+      AppUrl::Url(WindowUrl::External(
+        Url::parse("http://localhost:8080").unwrap()
+      ))
     );
     assert_eq!(d_title, tauri.windows[0].title);
     assert_eq!(d_windows, tauri.windows);

+ 3 - 3
core/tauri/src/manager.rs

@@ -8,7 +8,7 @@
 use crate::{
   api::{
     assets::Assets,
-    config::{Config, WindowUrl},
+    config::{AppUrl, Config, WindowUrl},
     path::{resolve_path, BaseDirectory},
     PackageInfo,
   },
@@ -282,7 +282,7 @@ impl<P: Params> WindowManager<P> {
   #[cfg(dev)]
   fn get_url(&self) -> String {
     match &self.inner.config.build.dev_path {
-      WindowUrl::External(url) => url.to_string(),
+      AppUrl::Url(WindowUrl::External(url)) => url.to_string(),
       _ => "tauri://localhost".into(),
     }
   }
@@ -290,7 +290,7 @@ impl<P: Params> WindowManager<P> {
   #[cfg(custom_protocol)]
   fn get_url(&self) -> String {
     match &self.inner.config.build.dist_dir {
-      WindowUrl::External(url) => url.to_string(),
+      AppUrl::Url(WindowUrl::External(url)) => url.to_string(),
       _ => "tauri://localhost".into(),
     }
   }

+ 0 - 1
core/tauri/test/fixture/src-tauri/tauri.conf.json

@@ -3,7 +3,6 @@
     "distDir": "../dist",
     "devPath": "http://localhost:4000"
   },
-  "ctx": {},
   "tauri": {
     "bundle": {
       "identifier": "studio.tauri.example",

+ 0 - 0
examples/commands/public/index.html → examples/commands/index.html


+ 2 - 2
examples/commands/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "build": {
-    "distDir": "../public",
-    "devPath": "../public",
+    "distDir": ["../index.html"],
+    "devPath": ["../index.html"],
     "beforeDevCommand": "",
     "beforeBuildCommand": ""
   },

+ 0 - 0
examples/helloworld/public/index.html → examples/helloworld/index.html


+ 2 - 2
examples/helloworld/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "build": {
-    "distDir": "../public",
-    "devPath": "../public",
+    "distDir": ["../index.html"],
+    "devPath": ["../index.html"],
     "beforeDevCommand": "",
     "beforeBuildCommand": ""
   },

+ 0 - 0
examples/multiwindow/dist/index.html → examples/multiwindow/index.html


+ 3 - 3
examples/multiwindow/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "build": {
-    "distDir": "../dist",
-    "devPath": "../dist",
+    "distDir": ["../index.html"],
+    "devPath": ["../index.html"],
     "withGlobalTauri": true
   },
   "tauri": {
@@ -45,4 +45,4 @@
       "active": false
     }
   }
-}
+}

+ 0 - 0
examples/params/public/index.html → examples/params/index.html


+ 2 - 2
examples/params/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "build": {
-    "distDir": "../public",
-    "devPath": "../public",
+    "distDir": ["../index.html"],
+    "devPath": ["../index.html"],
     "beforeDevCommand": "",
     "beforeBuildCommand": ""
   },

+ 0 - 0
examples/state/public/index.html → examples/state/index.html


+ 2 - 2
examples/state/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "build": {
-    "distDir": "../public",
-    "devPath": "../public",
+    "distDir": ["../index.html"],
+    "devPath": ["../index.html"],
     "beforeDevCommand": "",
     "beforeBuildCommand": ""
   },

+ 0 - 0
examples/updater/public/index.html → examples/updater/index.html


+ 0 - 329
examples/updater/public/__tauri.js

@@ -1,329 +0,0 @@
-// polyfills
-if (!String.prototype.startsWith) {
-  String.prototype.startsWith = function (searchString, position) {
-    position = position || 0;
-    return this.substr(position, searchString.length) === searchString;
-  };
-}
-
-(function () {
-  function s4() {
-    return Math.floor((1 + Math.random()) * 0x10000)
-      .toString(16)
-      .substring(1);
-  }
-
-  var uid = function () {
-    return (
-      s4() +
-      s4() +
-      "-" +
-      s4() +
-      "-" +
-      s4() +
-      "-" +
-      s4() +
-      "-" +
-      s4() +
-      s4() +
-      s4()
-    );
-  };
-
-  function ownKeys(object, enumerableOnly) {
-    var keys = Object.keys(object);
-    if (Object.getOwnPropertySymbols) {
-      var symbols = Object.getOwnPropertySymbols(object);
-      if (enumerableOnly)
-        symbols = symbols.filter(function (sym) {
-          return Object.getOwnPropertyDescriptor(object, sym).enumerable;
-        });
-      keys.push.apply(keys, symbols);
-    }
-    return keys;
-  }
-
-  function _objectSpread(target) {
-    for (var i = 1; i < arguments.length; i++) {
-      var source = arguments[i] != null ? arguments[i] : {};
-      if (i % 2) {
-        ownKeys(source, true).forEach(function (key) {
-          _defineProperty(target, key, source[key]);
-        });
-      } else if (Object.getOwnPropertyDescriptors) {
-        Object.defineProperties(
-          target,
-          Object.getOwnPropertyDescriptors(source)
-        );
-      } else {
-        ownKeys(source).forEach(function (key) {
-          Object.defineProperty(
-            target,
-            key,
-            Object.getOwnPropertyDescriptor(source, key)
-          );
-        });
-      }
-    }
-    return target;
-  }
-
-  function _defineProperty(obj, key, value) {
-    if (key in obj) {
-      Object.defineProperty(obj, key, {
-        value: value,
-        enumerable: true,
-        configurable: true,
-        writable: true,
-      });
-    } else {
-      obj[key] = value;
-    }
-    return obj;
-  }
-
-  if (!window.__TAURI__) {
-    window.__TAURI__ = {};
-  }
-
-  window.__TAURI__.transformCallback = function transformCallback(
-    callback,
-    once
-  ) {
-    var identifier = uid();
-
-    window[identifier] = function (result) {
-      if (once) {
-        delete window[identifier];
-      }
-
-      return callback && callback(result);
-    };
-
-    return identifier;
-  };
-
-  window.__TAURI__.invoke = function invoke(cmd, args = {}) {
-    var _this = this;
-
-    return new Promise(function (resolve, reject) {
-      var callback = _this.transformCallback(function (r) {
-        resolve(r);
-        delete window[error];
-      }, true);
-      var error = _this.transformCallback(function (e) {
-        reject(e);
-        delete window[callback];
-      }, true);
-
-      if (typeof cmd === "string") {
-        args.cmd = cmd;
-      } else if (typeof cmd === "object") {
-        args = cmd;
-      } else {
-        return reject(new Error("Invalid argument type."));
-      }
-
-      if (window.rpc) {
-        window.rpc.notify(
-          cmd,
-          _objectSpread(
-            {
-              callback: callback,
-              error: error,
-            },
-            args
-          )
-        );
-      } else {
-        window.addEventListener("DOMContentLoaded", function () {
-          window.rpc.notify(
-            cmd,
-            _objectSpread(
-              {
-                callback: callback,
-                error: error,
-              },
-              args
-            )
-          );
-        });
-      }
-    });
-  };
-
-  // open <a href="..."> links with the Tauri API
-  function __openLinks() {
-    document.querySelector("body").addEventListener(
-      "click",
-      function (e) {
-        var target = e.target;
-        while (target != null) {
-          if (
-            target.matches ? target.matches("a") : target.msMatchesSelector("a")
-          ) {
-            if (
-              target.href &&
-              target.href.startsWith("http") &&
-              target.target === "_blank"
-            ) {
-              window.__TAURI__.invoke('tauri', {
-                __tauriModule: "Shell",
-                message: {
-                  cmd: "open",
-                  uri: target.href,
-                },
-              });
-              e.preventDefault();
-            }
-            break;
-          }
-          target = target.parentElement;
-        }
-      },
-      true
-    );
-  }
-
-  if (
-    document.readyState === "complete" ||
-    document.readyState === "interactive"
-  ) {
-    __openLinks();
-  } else {
-    window.addEventListener(
-      "DOMContentLoaded",
-      function () {
-        __openLinks();
-      },
-      true
-    );
-  }
-
-  window.__TAURI__.invoke('tauri', {
-    __tauriModule: "Event",
-    message: {
-      cmd: "listen",
-      event: "tauri://window-created",
-      handler: window.__TAURI__.transformCallback(function (event) {
-        if (event.payload) {
-          var windowLabel = event.payload.label;
-          window.__TAURI__.__windows.push({ label: windowLabel });
-        }
-      }),
-    },
-  });
-
-  let permissionSettable = false;
-  let permissionValue = "default";
-
-  function isPermissionGranted() {
-    if (window.Notification.permission !== "default") {
-      return Promise.resolve(window.Notification.permission === "granted");
-    }
-    return window.__TAURI__.invoke('tauri', {
-      __tauriModule: "Notification",
-      message: {
-        cmd: "isNotificationPermissionGranted",
-      },
-    });
-  }
-
-  function setNotificationPermission(value) {
-    permissionSettable = true;
-    window.Notification.permission = value;
-    permissionSettable = false;
-  }
-
-  function requestPermission() {
-    return window.__TAURI__
-      .invoke('tauri', {
-        __tauriModule: "Notification",
-        mainThread: true,
-        message: {
-          cmd: "requestNotificationPermission",
-        },
-      })
-      .then(function (permission) {
-        setNotificationPermission(permission);
-        return permission;
-      });
-  }
-
-  function sendNotification(options) {
-    if (typeof options === "object") {
-      Object.freeze(options);
-    }
-
-    isPermissionGranted().then(function (permission) {
-      if (permission) {
-        return window.__TAURI__.invoke('tauri', {
-          __tauriModule: "Notification",
-          message: {
-            cmd: "notification",
-            options:
-              typeof options === "string"
-                ? {
-                    title: options,
-                  }
-                : options,
-          },
-        });
-      }
-    });
-  }
-
-  window.Notification = function (title, options) {
-    var opts = options || {};
-    sendNotification(
-      Object.assign(opts, {
-        title: title,
-      })
-    );
-  };
-
-  window.Notification.requestPermission = requestPermission;
-
-  Object.defineProperty(window.Notification, "permission", {
-    enumerable: true,
-    get: function () {
-      return permissionValue;
-    },
-    set: function (v) {
-      if (!permissionSettable) {
-        throw new Error("Readonly property");
-      }
-      permissionValue = v;
-    },
-  });
-
-  isPermissionGranted().then(function (response) {
-    if (response === null) {
-      setNotificationPermission("default");
-    } else {
-      setNotificationPermission(response ? "granted" : "denied");
-    }
-  });
-
-  window.alert = function (message) {
-    window.__TAURI__.invoke('tauri', {
-      __tauriModule: "Dialog",
-      mainThread: true,
-      message: {
-        cmd: "messageDialog",
-        message: message,
-      },
-    });
-  };
-
-  window.confirm = function (message) {
-    return window.__TAURI__.invoke('tauri', {
-      __tauriModule: "Dialog",
-      mainThread: true,
-      message: {
-        cmd: "askDialog",
-        message: message,
-      },
-    });
-  };
-})();

+ 2 - 2
examples/updater/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "build": {
-    "distDir": "../public",
-    "devPath": "../public",
+    "distDir": ["../index.html"],
+    "devPath": ["../index.html"],
     "beforeDevCommand": "",
     "beforeBuildCommand": ""
   },

+ 26 - 7
tooling/cli.rs/config_definition.rs

@@ -590,6 +590,25 @@ fn default_dialog() -> Option<bool> {
   Some(true)
 }
 
+/// The `dev_path` and `dist_dir` options.
+#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
+#[serde(untagged, deny_unknown_fields)]
+pub enum AppUrl {
+  /// The app's external URL, or the path to the directory containing the app assets.
+  Url(String),
+  /// An array of files to embed on the app.
+  Files(Vec<PathBuf>),
+}
+
+impl std::fmt::Display for AppUrl {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    match self {
+      Self::Url(url) => write!(f, "{}", url),
+      Self::Files(files) => write!(f, "{}", serde_json::to_string(files).unwrap()),
+    }
+  }
+}
+
 /// The Build configuration object.
 #[skip_serializing_none]
 #[derive(Debug, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
@@ -597,12 +616,12 @@ fn default_dialog() -> Option<bool> {
 pub struct BuildConfig {
   /// The binary used to build and run the application.
   pub runner: Option<String>,
-  /// the app's dev server URL, or the path to the directory containing an index.html file
+  /// The path or URL to use on development.
   #[serde(default = "default_dev_path")]
-  pub dev_path: String,
+  pub dev_path: AppUrl,
   /// the path to the app's dist dir. This path must contain your index.html file.
   #[serde(default = "default_dist_dir")]
-  pub dist_dir: String,
+  pub dist_dir: AppUrl,
   /// a shell command to run before `tauri dev` kicks in
   pub before_dev_command: Option<String>,
   /// a shell command to run before `tauri build` kicks in
@@ -614,12 +633,12 @@ pub struct BuildConfig {
   pub with_global_tauri: bool,
 }
 
-fn default_dev_path() -> String {
-  "".to_string()
+fn default_dev_path() -> AppUrl {
+  AppUrl::Url("".to_string())
 }
 
-fn default_dist_dir() -> String {
-  "../dist".to_string()
+fn default_dist_dir() -> AppUrl {
+  AppUrl::Url("../dist".to_string())
 }
 
 type JsonObject = HashMap<String, JsonValue>;

+ 27 - 3
tooling/cli.rs/schema.json

@@ -206,6 +206,22 @@
       },
       "additionalProperties": false
     },
+    "AppUrl": {
+      "description": "The `dev_path` and `dist_dir` options.",
+      "anyOf": [
+        {
+          "description": "The app's external URL, or the path to the directory containing the app assets.",
+          "type": "string"
+        },
+        {
+          "description": "An array of files to embed on the app.",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        }
+      ]
+    },
     "BuildConfig": {
       "description": "The Build configuration object.",
       "type": "object",
@@ -225,14 +241,22 @@
           ]
         },
         "devPath": {
-          "description": "the app's dev server URL, or the path to the directory containing an index.html file",
+          "description": "The path or URL to use on development.",
           "default": "",
-          "type": "string"
+          "allOf": [
+            {
+              "$ref": "#/definitions/AppUrl"
+            }
+          ]
         },
         "distDir": {
           "description": "the path to the app's dist dir. This path must contain your index.html file.",
           "default": "../dist",
-          "type": "string"
+          "allOf": [
+            {
+              "$ref": "#/definitions/AppUrl"
+            }
+          ]
         },
         "features": {
           "description": "features passed to `cargo` commands",

+ 9 - 7
tooling/cli.rs/src/build.rs

@@ -7,7 +7,7 @@ use tauri_bundler::bundle::{bundle_project, PackageType, SettingsBuilder};
 
 use crate::helpers::{
   app_paths::{app_dir, tauri_dir},
-  config::get as get_config,
+  config::{get as get_config, AppUrl},
   execute_with_output,
   manifest::rewrite_manifest,
   updater_signature::sign_file_from_env_variables,
@@ -103,12 +103,14 @@ impl Build {
       }
     }
 
-    let web_asset_path = PathBuf::from(&config_.build.dist_dir);
-    if !web_asset_path.exists() {
-      return Err(anyhow::anyhow!(
-        "Unable to find your web assets, did you forget to build your web app? Your distDir is set to \"{:?}\".",
-        web_asset_path
-      ));
+    if let AppUrl::Url(url) = &config_.build.dist_dir {
+      let web_asset_path = PathBuf::from(url);
+      if !web_asset_path.exists() {
+        return Err(anyhow::anyhow!(
+          "Unable to find your web assets, did you forget to build your web app? Your distDir is set to \"{:?}\".",
+          web_asset_path
+        ));
+      }
     }
 
     let runner_from_config = config_.build.runner.clone();

+ 2 - 2
tooling/cli.rs/src/info.rs

@@ -545,10 +545,10 @@ impl Info {
           })
           .display();
         InfoBlock::new("distDir")
-          .value(config.build.dist_dir.clone())
+          .value(config.build.dist_dir.to_string())
           .display();
         InfoBlock::new("devPath")
-          .value(config.build.dev_path.clone())
+          .value(config.build.dev_path.to_string())
           .display();
       }
       if let Ok(package_json) = read_to_string(app_dir.join("package.json")) {