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
This commit is contained in:
Lucas Fernandes Nogueira
2021-05-31 11:42:10 -03:00
committed by GitHub
parent cb6c807ac8
commit 6ec54c53b5
23 changed files with 220 additions and 410 deletions

View File

@@ -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.

View File

@@ -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,42 +24,45 @@ 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
)
}
Some(assets_path)
}
_ => 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()
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)?
}
_ => unimplemented!(),
},
AppUrl::Files(files) => EmbeddedAssets::load_paths(
files.iter().map(|p| config_parent.join(p)).collect(),
options,
)?,
_ => unimplemented!(),
};
// handle default window icons for Windows targets

View File

@@ -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();

View File

@@ -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);

View File

@@ -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(),
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
},
});
};
})();

View File

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

View File

@@ -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>;

View File

@@ -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",

View File

@@ -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();

View File

@@ -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")) {