diff --git a/src/build/mod.rs b/src/build/mod.rs index 969060a36..155b25471 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -12,10 +12,30 @@ use std::process::Command; pub fn build_target(target: &Target) -> Result { let target_type = &target.target_type; match target_type { - TargetType::JavaScript => { - let msg = "JavaScript project found. Skipping unnecessary build!".to_string(); - Ok(msg) - } + TargetType::JavaScript => match &target.build { + None => { + let msg = "Basic JavaScript project found. Skipping unnecessary build!".to_string(); + Ok(msg) + } + Some(config) => { + if let Some((cmd_str, mut cmd)) = config.build_command() { + StdErr::working(format!("Running {}", cmd_str).as_ref()); + let build_result = cmd.spawn()?.wait()?; + if build_result.success() { + Ok(String::from("Build completed successfully!")) + } else if let Some(code) = build_result.code() { + Err(failure::err_msg(format!( + "Build failed! Status Code: {}", + code + ))) + } else { + Err(failure::err_msg("Build failed.")) + } + } else { + Ok(String::from("No build command specified, skipping build.")) + } + } + }, TargetType::Rust => { let _ = which::which("rustc").map_err(|e| { failure::format_err!( @@ -31,6 +51,7 @@ pub fn build_target(target: &Target) -> Result { let command = command(&args, &binary_path); let command_name = format!("{:?}", command); + StdErr::working("Compiling your project to WebAssembly..."); commands::run(command, &command_name)?; let msg = "Build succeeded".to_string(); Ok(msg) @@ -49,8 +70,6 @@ pub fn build_target(target: &Target) -> Result { } pub fn command(args: &[&str], binary_path: &PathBuf) -> Command { - StdErr::working("Compiling your project to WebAssembly..."); - let mut c = if cfg!(target_os = "windows") { let mut c = Command::new("cmd"); c.arg("/C"); diff --git a/src/commands/kv/mod.rs b/src/commands/kv/mod.rs index 0e533fd57..d3ee322b9 100644 --- a/src/commands/kv/mod.rs +++ b/src/commands/kv/mod.rs @@ -125,6 +125,7 @@ mod tests { site: None, vars: None, text_blobs: None, + build: None, }; assert!(kv::get_namespace_id(&target_with_dup_kv_bindings, "").is_err()); } diff --git a/src/commands/publish.rs b/src/commands/publish.rs index fa1640db4..920b64db8 100644 --- a/src/commands/publish.rs +++ b/src/commands/publish.rs @@ -72,6 +72,12 @@ pub fn publish( } Err(e) => Err(e), }?; + + // We verify early here, so we don't perform pre-upload tasks if the upload will fail + if let Some(build_config) = &target.build { + build_config.verify_upload_dir()?; + } + if let Some(site_config) = &target.site { let path = &site_config.bucket.clone(); validate_bucket_location(path)?; diff --git a/src/settings/toml/builder.rs b/src/settings/toml/builder.rs new file mode 100644 index 000000000..95c7b4bef --- /dev/null +++ b/src/settings/toml/builder.rs @@ -0,0 +1,113 @@ +use std::env; +use std::path::PathBuf; +use std::process::Command; + +use serde::{Deserialize, Serialize}; + +use super::ScriptFormat; + +const UPLOAD_DIR: &str = "dist"; +const WATCH_DIR: &str = "src"; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Builder { + command: Option, + #[serde(default = "project_root")] + pub cwd: PathBuf, + #[serde(default = "upload_dir")] + pub upload_dir: PathBuf, + pub upload_format: ScriptFormat, + #[serde(default = "watch_dir")] + pub watch_dir: PathBuf, +} + +fn project_root() -> PathBuf { + env::current_dir().unwrap() +} + +fn upload_dir() -> PathBuf { + project_root().join(UPLOAD_DIR) +} + +fn watch_dir() -> PathBuf { + project_root().join(WATCH_DIR) +} + +impl Builder { + pub fn verify_watch_dir(&self) -> Result<(), failure::Error> { + let watch_canonical = match self.watch_dir.canonicalize() { + Ok(path) => path, + Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => failure::bail!( + "Your provided watch_dir {} does not exist.", + self.watch_dir.display() + ), + Err(e) => failure::bail!( + "Error encountered when verifying watch_dir: {}, provided path: {}", + e, + self.watch_dir.display() + ), + }; + let root_canonical = project_root().canonicalize()?; + if watch_canonical == root_canonical { + failure::bail!("Wrangler doesn't support using the project root as the watch_dir."); + } + if !self.watch_dir.is_dir() { + failure::bail!(format!( + "A path was provided for watch_dir that is not a directory: {}", + self.watch_dir.display() + )); + } + Ok(()) + } + + pub fn verify_upload_dir(&self) -> Result<(), failure::Error> { + let upload_canonical = match self.upload_dir.canonicalize() { + Ok(path) => path, + Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => failure::bail!( + "Your provided upload_dir {} does not exist.", + self.upload_dir.display() + ), + Err(e) => failure::bail!( + "Error encountered when verifying upload_dir: {}, provided path: {}", + e, + self.upload_dir.display() + ), + }; + let root_canonical = project_root().canonicalize()?; + if upload_canonical == root_canonical { + failure::bail!("Wrangler doesn't support using the project root as the upload_dir."); + } + if !self.upload_dir.is_dir() { + failure::bail!(format!( + "A path was provided for upload_dir that is not a directory: {}", + self.upload_dir.display() + )); + } + Ok(()) + } + + pub fn build_command(&self) -> Option<(&str, Command)> { + match &self.command { + Some(cmd) => { + let mut c = if cfg!(target_os = "windows") { + let args: Vec<&str> = cmd.split_whitespace().collect(); + let mut c = Command::new("cmd"); + c.arg("/C"); + c.args(args.as_slice()); + c + } else { + let mut c = Command::new("sh"); + c.arg("-c"); + c.arg(cmd); + c + }; + + c.current_dir(&self.cwd); + + Some((cmd, c)) + } + None => None, + } + } +} diff --git a/src/settings/toml/environment.rs b/src/settings/toml/environment.rs index 1ae426216..1f85a6f11 100644 --- a/src/settings/toml/environment.rs +++ b/src/settings/toml/environment.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use serde_with::rust::string_empty_as_none; +use crate::settings::toml::builder::Builder; use crate::settings::toml::kv_namespace::ConfigKvNamespace; use crate::settings::toml::route::RouteConfig; use crate::settings::toml::site::Site; @@ -21,6 +22,7 @@ pub struct Environment { #[serde(default, with = "string_empty_as_none")] pub zone_id: Option, pub webpack_config: Option, + pub build: Option, pub private: Option, pub site: Option, #[serde(alias = "kv-namespaces")] diff --git a/src/settings/toml/manifest.rs b/src/settings/toml/manifest.rs index fd48508c3..924c6c37e 100644 --- a/src/settings/toml/manifest.rs +++ b/src/settings/toml/manifest.rs @@ -11,6 +11,7 @@ use serde_with::rust::string_empty_as_none; use crate::commands::{validate_worker_name, DEFAULT_CONFIG_PATH}; use crate::deploy::{self, DeployTarget, DeploymentSet}; +use crate::settings::toml::builder::Builder; use crate::settings::toml::dev::Dev; use crate::settings::toml::environment::Environment; use crate::settings::toml::kv_namespace::{ConfigKvNamespace, KvNamespace}; @@ -40,6 +41,7 @@ pub struct Manifest { #[serde(default, with = "string_empty_as_none")] pub zone_id: Option, pub webpack_config: Option, + pub build: Option, pub private: Option, // TODO: maybe one day, serde toml support will allow us to serialize sites // as a TOML inline table (this would prevent confusion with environments too!) @@ -290,6 +292,7 @@ impl Manifest { target_type, // Top level account_id: self.account_id.clone(), // Inherited webpack_config: self.webpack_config.clone(), // Inherited + build: self.build.clone(), // Inherited // importantly, the top level name will be modified // to include the name of the environment name: self.name.clone(), // Inherited @@ -309,6 +312,9 @@ impl Manifest { if let Some(webpack_config) = &environment.webpack_config { target.webpack_config = Some(webpack_config.clone()); } + if let Some(build) = &environment.build { + target.build = Some(build.clone()); + } // don't inherit kv namespaces because it is an anti-pattern to use the same namespaces across multiple environments target.kv_namespaces = get_namespaces(environment.kv_namespaces.clone(), preview)?; diff --git a/src/settings/toml/mod.rs b/src/settings/toml/mod.rs index 0528328cc..3008c299a 100644 --- a/src/settings/toml/mod.rs +++ b/src/settings/toml/mod.rs @@ -1,17 +1,21 @@ +mod builder; mod dev; mod environment; mod kv_namespace; mod manifest; mod route; +mod script_format; mod site; mod target; mod target_type; mod triggers; +pub use builder::Builder; pub use environment::Environment; pub use kv_namespace::{ConfigKvNamespace, KvNamespace}; pub use manifest::Manifest; pub use route::{Route, RouteConfig}; +pub use script_format::ScriptFormat; pub use site::Site; pub use target::Target; pub use target_type::TargetType; diff --git a/src/settings/toml/script_format.rs b/src/settings/toml/script_format.rs new file mode 100644 index 000000000..1ff4eb3e5 --- /dev/null +++ b/src/settings/toml/script_format.rs @@ -0,0 +1,34 @@ +use std::fmt; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub enum ScriptFormat { + #[serde(rename = "service-worker")] + ServiceWorker, + #[serde(rename = "modules")] + Modules, +} + +impl fmt::Display for ScriptFormat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let printable = match *self { + Self::ServiceWorker => "service-worker", + Self::Modules => "modules", + }; + write!(f, "{}", printable) + } +} + +impl FromStr for ScriptFormat { + type Err = failure::Error; + + fn from_str(s: &str) -> Result { + match s { + "service-worker" => Ok(Self::ServiceWorker), + "modules" => Ok(Self::Modules), + _ => failure::bail!("{} is not a valid script format!", s), + } + } +} diff --git a/src/settings/toml/target.rs b/src/settings/toml/target.rs index f02a825df..8cda79767 100644 --- a/src/settings/toml/target.rs +++ b/src/settings/toml/target.rs @@ -1,3 +1,4 @@ +use super::builder::Builder; use super::kv_namespace::KvNamespace; use super::site::Site; use super::target_type::TargetType; @@ -14,6 +15,7 @@ pub struct Target { pub name: String, pub target_type: TargetType, pub webpack_config: Option, + pub build: Option, pub site: Option, pub vars: Option>, pub text_blobs: Option>, diff --git a/src/sites/mod.rs b/src/sites/mod.rs index c52f6d7c1..9adcc325c 100644 --- a/src/sites/mod.rs +++ b/src/sites/mod.rs @@ -318,6 +318,7 @@ mod tests { target_type: TargetType::JavaScript, webpack_config: None, site: Some(site), + build: None, vars: None, text_blobs: None, } diff --git a/src/watch/mod.rs b/src/watch/mod.rs index c29e983fd..b8845347f 100644 --- a/src/watch/mod.rs +++ b/src/watch/mod.rs @@ -3,10 +3,10 @@ use ignore::overrides::OverrideBuilder; use ignore::WalkBuilder; pub use watcher::wait_for_changes; -use crate::build::command; use crate::settings::toml::{Target, TargetType}; use crate::terminal::message::{Message, StdOut}; use crate::wranglerjs; +use crate::{build::command, build_target}; use crate::{commands, install}; use notify::{self, RecursiveMode, Watcher}; @@ -28,27 +28,54 @@ pub fn watch_and_build( tx: Option>, ) -> Result<(), failure::Error> { let target_type = &target.target_type; + let build = target.build.clone(); match target_type { TargetType::JavaScript => { - thread::spawn(move || { + let target = target.clone(); + thread::spawn::<_, Result<(), failure::Error>>(move || { let (watcher_tx, watcher_rx) = mpsc::channel(); - let mut watcher = notify::watcher(watcher_tx, Duration::from_secs(1)).unwrap(); + let mut watcher = notify::watcher(watcher_tx, Duration::from_secs(1))?; - watcher - .watch(JAVASCRIPT_PATH, RecursiveMode::Recursive) - .unwrap(); - StdOut::info(&format!("watching {:?}", &JAVASCRIPT_PATH)); + match build { + None => { + watcher.watch(JAVASCRIPT_PATH, RecursiveMode::Recursive)?; + StdOut::info(&format!("watching {:?}", &JAVASCRIPT_PATH)); - loop { - match wait_for_changes(&watcher_rx, COOLDOWN_PERIOD) { - Ok(_path) => { - if let Some(tx) = tx.clone() { - tx.send(()).expect("--watch change message failed to send"); + loop { + match wait_for_changes(&watcher_rx, COOLDOWN_PERIOD) { + Ok(_path) => { + if let Some(tx) = tx.clone() { + tx.send(()).expect("--watch change message failed to send"); + } + } + Err(e) => { + log::debug!("{:?}", e); + StdOut::user_error("Something went wrong while watching.") + } } } - Err(e) => { - log::debug!("{:?}", e); - StdOut::user_error("Something went wrong while watching.") + } + Some(config) => { + config.verify_watch_dir()?; + watcher.watch(config.watch_dir, notify::RecursiveMode::Recursive)?; + + loop { + match wait_for_changes(&watcher_rx, COOLDOWN_PERIOD) { + Ok(_path) => match build_target(&target) { + Ok(output) => { + StdOut::success(&output); + if let Some(tx) = tx.clone() { + tx.send(()) + .expect("--watch change message failed to send"); + } + } + Err(e) => StdOut::user_error(&e.to_string()), + }, + Err(e) => { + log::debug!("{:?}", e); + StdOut::user_error("Something went wrong while watching.") + } + } } } }