From 2e49b4e6ea536cb049e2e75fc5cfef5741de3984 Mon Sep 17 00:00:00 2001 From: Monica Olejniczak Date: Wed, 19 Jun 2024 10:09:20 +1000 Subject: [PATCH] Add target request --- Cargo.lock | 12 + crates/parcel/Cargo.toml | 3 + crates/parcel/src/plugins.rs | 12 +- crates/parcel/src/request_tracker/request.rs | 3 +- crates/parcel/src/requests.rs | 1 + crates/parcel/src/requests/path_request.rs | 13 +- crates/parcel/src/requests/target_request.rs | 1112 +++++++++++++++++ .../requests/target_request/package_json.rs | 91 ++ crates/parcel_core/Cargo.toml | 2 +- .../plugin_config.rs => config_loader.rs} | 93 +- crates/parcel_core/src/lib.rs | 1 + crates/parcel_core/src/plugin.rs | 6 +- .../src/plugin/validator_plugin.rs | 14 +- crates/parcel_core/src/types/environment.rs | 42 +- .../src/types/environment/browsers.rs | 43 +- .../src/types/environment/engines.rs | 30 +- .../src/types/environment/output_format.rs | 25 +- .../parcel_core/src/types/parcel_options.rs | 61 +- crates/parcel_core/src/types/target.rs | 25 +- crates/parcel_plugin_resolver/src/resolver.rs | 9 +- .../parcel_plugin_transformer_js/Cargo.toml | 3 - .../src/transformer.rs | 3 +- 22 files changed, 1452 insertions(+), 152 deletions(-) create mode 100644 crates/parcel/src/requests/target_request.rs create mode 100644 crates/parcel/src/requests/target_request/package_json.rs rename crates/parcel_core/src/{plugin/plugin_config.rs => config_loader.rs} (81%) diff --git a/Cargo.lock b/Cargo.lock index 2f163208bb7..9850b9b4b68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1733,6 +1733,9 @@ dependencies = [ "parcel_plugin_rpc", "parcel_plugin_transformer_js", "petgraph", + "serde", + "serde-bool", + "serde_json", "xxhash-rust", ] @@ -2747,6 +2750,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-bool" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af14f9242b0beec13757cf161feb2d600c11a764310425c3429dca9925b7a92" +dependencies = [ + "serde", +] + [[package]] name = "serde-value" version = "0.7.0" diff --git a/crates/parcel/Cargo.toml b/crates/parcel/Cargo.toml index 82111b98ae7..91c4764e036 100644 --- a/crates/parcel/Cargo.toml +++ b/crates/parcel/Cargo.toml @@ -20,5 +20,8 @@ parcel-resolver = { path = "../../packages/utils/node-resolver-rs" } anyhow = "1.0.82" dyn-hash = "0.x" petgraph = "0.x" +serde = { version = "1.0.200", features = ["derive"] } +serde-bool = "0.1.3" +serde_json = "1.0.116" xxhash-rust = { version = "0.8.2", features = ["xxh3"] } num_cpus = "1.16.0" diff --git a/crates/parcel/src/plugins.rs b/crates/parcel/src/plugins.rs index 673d13082a8..883b3748a6a 100644 --- a/crates/parcel/src/plugins.rs +++ b/crates/parcel/src/plugins.rs @@ -221,7 +221,7 @@ mod tests { use std::sync::Arc; use parcel_config::parcel_config_fixtures::default_config; - use parcel_core::plugin::PluginConfig; + use parcel_core::config_loader::ConfigLoader; use parcel_core::plugin::PluginLogger; use parcel_core::plugin::PluginOptions; use parcel_filesystem::in_memory_file_system::InMemoryFileSystem; @@ -230,11 +230,11 @@ mod tests { fn make_test_plugin_context() -> PluginContext { PluginContext { - config: PluginConfig::new( - Arc::new(InMemoryFileSystem::default()), - PathBuf::default(), - PathBuf::default(), - ), + config: ConfigLoader { + fs: Arc::new(InMemoryFileSystem::default()), + project_root: PathBuf::default(), + search_path: PathBuf::default(), + }, options: Arc::new(PluginOptions::default()), logger: PluginLogger::default(), } diff --git a/crates/parcel/src/request_tracker/request.rs b/crates/parcel/src/request_tracker/request.rs index 62e52fc2121..17a4dec0f0f 100644 --- a/crates/parcel/src/request_tracker/request.rs +++ b/crates/parcel/src/request_tracker/request.rs @@ -1,5 +1,4 @@ use std::fmt::Debug; -use std::hash::DefaultHasher; use std::hash::Hash; use std::hash::Hasher; @@ -46,7 +45,7 @@ pub type RunRequestError = anyhow::Error; pub trait Request: DynHash { fn id(&self) -> u64 { - let mut hasher = DefaultHasher::default(); + let mut hasher = parcel_core::hash::IdentifierHasher::default(); std::any::type_name::().hash(&mut hasher); self.dyn_hash(&mut hasher); hasher.finish() diff --git a/crates/parcel/src/requests.rs b/crates/parcel/src/requests.rs index d66726316ac..53cb9390d2c 100644 --- a/crates/parcel/src/requests.rs +++ b/crates/parcel/src/requests.rs @@ -1,2 +1,3 @@ mod asset_request; mod path_request; +mod target_request; diff --git a/crates/parcel/src/requests/path_request.rs b/crates/parcel/src/requests/path_request.rs index 90cff606f64..dc0ca0224df 100644 --- a/crates/parcel/src/requests/path_request.rs +++ b/crates/parcel/src/requests/path_request.rs @@ -1,5 +1,4 @@ use std::hash::Hash; -use std::hash::Hasher; use std::path::PathBuf; use std::sync::Arc; @@ -42,16 +41,6 @@ pub enum PathResolution { // TODO tracing, dev deps impl Request for PathRequest { - fn id(&self) -> u64 { - let mut hasher = parcel_core::hash::IdentifierHasher::default(); - - self.dependency.hash(&mut hasher); - self.named_pipelines.hash(&mut hasher); - self.resolvers.hash(&mut hasher); - - hasher.finish() - } - fn run( &self, request_context: RunRequestContext, @@ -190,7 +179,7 @@ mod tests { } impl Hash for ResolvedResolverPlugin { - fn hash(&self, _state: &mut H) {} + fn hash(&self, _state: &mut H) {} } impl ResolverPlugin for ResolvedResolverPlugin { diff --git a/crates/parcel/src/requests/target_request.rs b/crates/parcel/src/requests/target_request.rs new file mode 100644 index 00000000000..c848693271e --- /dev/null +++ b/crates/parcel/src/requests/target_request.rs @@ -0,0 +1,1112 @@ +use std::collections::HashMap; +use std::ffi::OsStr; +use std::hash::Hash; +use std::path::Path; +use std::path::PathBuf; + +use anyhow::anyhow; +use package_json::BrowserField; +use package_json::BrowsersList; +use package_json::BuiltInTargetDescriptor; +use package_json::ModuleFormat; +use package_json::PackageJson; +use package_json::SourceMapField; +use package_json::TargetDescriptor; +use parcel_core::config_loader::ConfigLoader; +use parcel_core::types::engines::Engines; +use parcel_core::types::BuildMode; +use parcel_core::types::DefaultTargetOptions; +use parcel_core::types::Entry; +use parcel_core::types::Environment; +use parcel_core::types::EnvironmentContext; +use parcel_core::types::OutputFormat; +use parcel_core::types::SourceType; +use parcel_core::types::Target; +use parcel_core::types::TargetSourceMapOptions; +use parcel_resolver::IncludeNodeModules; + +use crate::request_tracker::Request; +use crate::request_tracker::RequestResult; +use crate::request_tracker::RunRequestContext; +use crate::request_tracker::RunRequestError; + +mod package_json; + +/// Infers how and where source code is outputted +/// +/// Targets will be generated from the project package.json file and input Parcel options. +/// +pub struct TargetRequest { + // TODO Either pass in package.json directly or make config available on the req context + pub config: ConfigLoader, + pub default_target_options: DefaultTargetOptions, + pub env: Option>, + pub exclusive_target: Option, + pub mode: BuildMode, +} + +impl Hash for TargetRequest { + fn hash(&self, state: &mut H) { + self.default_target_options.hash(state); + self.exclusive_target.hash(state); + self.mode.hash(state); + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Targets(Vec); + +struct BuiltInTarget<'a> { + descriptor: BuiltInTargetDescriptor, + dist: Option, + extensions: Vec<&'a str>, + name: &'a str, +} + +struct CustomTarget<'a> { + descriptor: &'a TargetDescriptor, + name: &'a str, +} + +impl TargetRequest { + fn builtin_target_descriptor(&self) -> TargetDescriptor { + TargetDescriptor { + include_node_modules: Some(IncludeNodeModules::Bool(false)), + is_library: Some(true), + scope_hoist: Some(true), + ..TargetDescriptor::default() + } + } + + fn builtin_browser_target( + &self, + descriptor: Option, + dist: Option, + name: Option, + ) -> BuiltInTarget { + BuiltInTarget { + descriptor: descriptor.unwrap_or_else(|| { + BuiltInTargetDescriptor::TargetDescriptor(TargetDescriptor { + context: Some(EnvironmentContext::Browser), + ..self.builtin_target_descriptor() + }) + }), + dist: dist.and_then(|browser| match browser { + BrowserField::EntryPoint(entrypoint) => Some(entrypoint.clone()), + BrowserField::ReplacementBySpecifier(replacements) => { + name.and_then(|name| replacements.get(&name).map(|v| v.into())) + } + }), + extensions: vec!["cjs", "js", "mjs"], + name: "browser", + } + } + + fn builtin_main_target( + &self, + descriptor: Option, + dist: Option, + ) -> BuiltInTarget { + BuiltInTarget { + descriptor: descriptor.unwrap_or_else(|| { + BuiltInTargetDescriptor::TargetDescriptor(TargetDescriptor { + context: Some(EnvironmentContext::Node), + ..self.builtin_target_descriptor() + }) + }), + dist, + extensions: vec!["cjs", "js", "mjs"], + name: "main", + } + } + + fn builtin_module_target( + &self, + descriptor: Option, + dist: Option, + ) -> BuiltInTarget { + BuiltInTarget { + descriptor: descriptor.unwrap_or_else(|| { + BuiltInTargetDescriptor::TargetDescriptor(TargetDescriptor { + context: Some(EnvironmentContext::Node), + ..self.builtin_target_descriptor() + }) + }), + dist, + extensions: vec!["js", "mjs"], + name: "module", + } + } + + fn builtin_types_target( + &self, + descriptor: Option, + dist: Option, + ) -> BuiltInTarget { + BuiltInTarget { + descriptor: descriptor.unwrap_or_else(|| { + BuiltInTargetDescriptor::TargetDescriptor(TargetDescriptor { + context: Some(EnvironmentContext::Node), + ..self.builtin_target_descriptor() + }) + }), + dist, + extensions: vec!["ts"], + name: "types", + } + } + + fn default_dist_dir(&self, package_path: &Path) -> PathBuf { + package_path + .parent() + .unwrap_or_else(|| &package_path) + .join("dist") + } + + fn infer_environment_context(&self, package_json: &PackageJson) -> EnvironmentContext { + // If there is a separate `browser` target, or an `engines.node` field but no browser + // targets, then the target refers to node, otherwise browser. + if package_json.browser.is_some() || package_json.targets.browser.is_some() { + if package_json + .engines + .as_ref() + .is_some_and(|e| e.node.is_some() && e.browsers.is_empty()) + { + return EnvironmentContext::Node; + } else { + return EnvironmentContext::Browser; + } + } + + if package_json + .engines + .as_ref() + .is_some_and(|e| e.node.is_some()) + { + return EnvironmentContext::Node; + } + + EnvironmentContext::Browser + } + + fn infer_output_format( + &self, + module_format: &Option, + target: &TargetDescriptor, + ) -> Result, anyhow::Error> { + let ext = target + .dist_entry + .as_ref() + .and_then(|e| e.extension()) + .unwrap_or_default() + .to_str(); + + let inferred_output_format = match ext { + Some("cjs") => Some(OutputFormat::CommonJS), + Some("mjs") => Some(OutputFormat::EsModule), + Some("js") => module_format.as_ref().and_then(|format| match format { + ModuleFormat::CommonJS => Some(OutputFormat::CommonJS), + ModuleFormat::Module => Some(OutputFormat::EsModule), + }), + _ => None, + }; + + if let Some(inferred_output_format) = inferred_output_format { + if let Some(output_format) = target.output_format { + if output_format != inferred_output_format { + return Err(anyhow!( + "Declared output format {} does not match expected output format {}", + output_format, + inferred_output_format + )); + } + } + } + + Ok(inferred_output_format) + } + + fn load_package_json(&self) -> Result<(PathBuf, PackageJson), anyhow::Error> { + // TODO Invalidations + let (package_path, mut package_json) = self.config.load_package_json_config::()?; + + if package_json + .engines + .as_ref() + .is_some_and(|e| !e.browsers.is_empty()) + { + return Ok((package_path, package_json)); + } + + let env = self + .env + .as_ref() + .and_then(|env| env.get("BROWSERSLIST_ENV").or_else(|| env.get("NODE_ENV"))) + .map(|e| e.to_owned()) + .unwrap_or_else(|| self.mode.to_string()); + + match package_json.browserslist.clone() { + // TODO Process browserslist config file + None => {} + Some(browserslist) => { + let browserslist = match browserslist { + BrowsersList::Browsers(browsers) => browsers, + BrowsersList::BrowsersByEnv(browsers_by_env) => browsers_by_env + .get(&env) + .map(|b| b.clone()) + .unwrap_or_default(), + }; + + package_json.engines = Some(Engines { + browsers: Engines::from_browserslist(browserslist), + ..match package_json.engines { + None => Engines::default(), + Some(engines) => engines, + } + }); + } + }; + + Ok((package_path, package_json)) + } + + fn resolve_package_targets(&self) -> Result>, anyhow::Error> { + let (package_path, package_json) = self.load_package_json()?; + let mut targets: Vec> = Vec::new(); + + let builtin_targets = [ + self.builtin_browser_target( + package_json.targets.browser.clone(), + package_json.browser.clone(), + package_json.name.clone(), + ), + self.builtin_main_target(package_json.targets.main.clone(), package_json.main.clone()), + self.builtin_module_target( + package_json.targets.module.clone(), + package_json.module.clone(), + ), + self.builtin_types_target( + package_json.targets.types.clone(), + package_json.types.clone(), + ), + ]; + + for builtin_target in builtin_targets { + if builtin_target.dist.is_none() { + continue; + } + + match builtin_target.descriptor { + BuiltInTargetDescriptor::Disabled(_disabled) => continue, + BuiltInTargetDescriptor::TargetDescriptor(builtin_target_descriptor) => { + if builtin_target_descriptor + .output_format + .is_some_and(|f| f == OutputFormat::Global) + { + return Err(anyhow!( + "The \"global\" output format is not supported in the {} target", + builtin_target.name + )); + } + + if let Some(target_dist) = builtin_target.dist.as_ref() { + let target_dist_ext = target_dist + .extension() + .unwrap_or(OsStr::new("")) + .to_string_lossy() + .into_owned(); + + if builtin_target + .extensions + .iter() + .all(|ext| &target_dist_ext != ext) + { + return Err(anyhow!( + "Unexpected file type {:?} in \"{}\" target", + target_dist.file_name().unwrap_or(OsStr::new(&target_dist)), + builtin_target.name + )); + } + } + + targets.push(self.target_from_descriptor( + builtin_target.dist, + &package_json, + &package_path, + builtin_target_descriptor, + builtin_target.name, + )?); + } + } + } + + let custom_targets = package_json + .targets + .custom_targets + .iter() + .map(|(name, descriptor)| CustomTarget { descriptor, name }); + + for custom_target in custom_targets { + let mut dist = None; + if let Some(value) = package_json.fields.get(custom_target.name) { + match value { + serde_json::Value::String(str) => { + dist = Some(PathBuf::from(str)); + } + _ => return Err(anyhow!("Invalid path for target {}", custom_target.name)), + }; + } + + targets.push(self.target_from_descriptor( + dist, + &package_json, + &package_path, + custom_target.descriptor.clone(), + &custom_target.name, + )?); + } + + if targets.is_empty() { + let context = self.infer_environment_context(&package_json); + + targets.push(Some(Target { + dist_dir: self + .default_target_options + .dist_dir + .clone() + .unwrap_or_else(|| self.default_dist_dir(&package_path)), + dist_entry: None, + env: Environment { + context, + engines: package_json + .engines + .unwrap_or_else(|| self.default_target_options.engines.clone()), + include_node_modules: IncludeNodeModules::from(context), + is_library: self.default_target_options.is_library, + loc: None, + output_format: self + .default_target_options + .output_format + .unwrap_or_else(|| fallback_output_format(context)), + should_optimize: self.default_target_options.should_optimize, + should_scope_hoist: self.default_target_options.should_scope_hoist + && self.mode == BuildMode::Production + && !self.default_target_options.is_library, + source_map: self + .default_target_options + .source_maps + .then(|| TargetSourceMapOptions::default()), + source_type: SourceType::Module, + }, + loc: None, + name: String::from("default"), + public_url: self.default_target_options.public_url.clone(), + })); + } + + Ok(targets) + } + + fn skip_target(&self, target_name: &str, source: &Option) -> bool { + // We skip targets if they have a descriptor.source that does not match the current + // exclusiveTarget. They will be handled by a separate resolvePackageTargets call from their + // Entry point but with exclusiveTarget set. + match self.exclusive_target.as_ref() { + None => source.is_some(), + Some(exclusive_target) => target_name != exclusive_target, + } + } + + fn target_from_descriptor( + &self, + dist: Option, + package_json: &PackageJson, + package_path: &Path, + target_descriptor: TargetDescriptor, + target_name: &str, + ) -> Result, anyhow::Error> { + if self.skip_target(&target_name, &target_descriptor.source) { + return Ok(None); + } + + if target_descriptor.is_library.is_some_and(|l| l == true) + && target_descriptor.scope_hoist.is_some_and(|s| s == false) + { + return Err(anyhow!( + "Scope hoisting cannot be disabled for \"{}\" library target", + target_name + )); + } + + // TODO LOC + let context = target_descriptor + .context + .unwrap_or_else(|| self.infer_environment_context(&package_json)); + + let inferred_output_format = + self.infer_output_format(&package_json.module_format, &target_descriptor)?; + + let output_format = target_descriptor + .output_format + .or(self.default_target_options.output_format) + .or(inferred_output_format) + .unwrap_or_else(|| match target_name { + "browser" => OutputFormat::CommonJS, + "main" => OutputFormat::CommonJS, + "module" => OutputFormat::EsModule, + "types" => OutputFormat::CommonJS, + _ => match context { + EnvironmentContext::ElectronMain => OutputFormat::CommonJS, + EnvironmentContext::ElectronRenderer => OutputFormat::CommonJS, + EnvironmentContext::Node => OutputFormat::CommonJS, + _ => OutputFormat::Global, + }, + }); + + if target_name == "main" + && output_format == OutputFormat::EsModule + && inferred_output_format.is_some_and(|f| f != OutputFormat::EsModule) + { + return Err(anyhow!("Output format \"esmodule\" cannot be used in the \"main\" target without a .mjs extension or \"type\": \"module\" field")); + } + + let is_library = target_descriptor + .is_library + .unwrap_or_else(|| self.default_target_options.is_library); + + Ok(Some(Target { + dist_dir: match dist.as_ref() { + None => self + .default_target_options + .dist_dir + .clone() + .unwrap_or_else(|| self.default_dist_dir(&package_path).join(target_name)), + Some(target_dist) => { + let package_dir = package_path.parent().unwrap_or_else(|| &package_path); + let dir = target_dist + .parent() + .map(|dir| dir.strip_prefix("./").ok().unwrap_or(dir)) + .and_then(|dir| { + if dir == PathBuf::from("") { + None + } else { + Some(dir) + } + }); + + match dir { + None => PathBuf::from(package_dir), + Some(dir) => { + println!("got a dir {}", dir.display()); + package_dir.join(dir) + } + } + } + }, + dist_entry: target_descriptor.dist_entry.clone().or_else(|| { + dist + .as_ref() + .and_then(|d| d.file_name().map(|f| PathBuf::from(f))) + }), + env: Environment { + context, + engines: target_descriptor + .engines + .clone() + .or_else(|| package_json.engines.clone()) + .unwrap_or_else(|| self.default_target_options.engines.clone()), + include_node_modules: target_descriptor + .include_node_modules + .unwrap_or_else(|| IncludeNodeModules::from(context)), + is_library, + loc: None, // TODO + output_format, + should_optimize: self.default_target_options.should_optimize + && if is_library { + // Libraries are not optimized by default, users must explicitly configure this. + target_descriptor.optimize.is_some_and(|o| o == true) + } else { + target_descriptor.optimize.is_none() + || target_descriptor.optimize.is_some_and(|o| o != false) + }, + should_scope_hoist: (is_library || self.default_target_options.should_scope_hoist) + && (target_descriptor.scope_hoist.is_none() + || target_descriptor.scope_hoist.is_some_and(|s| s != false)), + source_map: match self.default_target_options.source_maps { + false => None, + true => target_descriptor.source_map.as_ref().and_then(|s| match s { + SourceMapField::Bool(source_maps) => { + source_maps.then(|| TargetSourceMapOptions::default()) + } + SourceMapField::Options(source_maps) => Some(source_maps.clone()), + }), + }, + ..Environment::default() + }, + loc: None, // TODO + name: String::from(target_name), + public_url: target_descriptor + .public_url + .clone() + .unwrap_or(self.default_target_options.public_url.clone()), + })) + } +} + +fn fallback_output_format(context: EnvironmentContext) -> OutputFormat { + match context { + EnvironmentContext::Node => OutputFormat::CommonJS, + EnvironmentContext::ElectronMain => OutputFormat::CommonJS, + EnvironmentContext::ElectronRenderer => OutputFormat::CommonJS, + _ => OutputFormat::Global, + } +} + +impl Request for TargetRequest { + fn run( + &self, + _request_context: RunRequestContext, + ) -> Result, RunRequestError> { + // TODO options.targets, should this still be supported? + // TODO serve options + let package_targets = self.resolve_package_targets()?; + + Ok(RequestResult { + invalidations: Vec::new(), + result: Targets( + package_targets + .into_iter() + .filter_map(std::convert::identity) + .collect(), + ), + }) + } +} + +// TODO Add more tests when revisiting targets config structure +#[cfg(test)] +mod tests { + use std::{num::NonZeroU16, path::PathBuf, sync::Arc}; + + use parcel_core::types::{browsers::Browsers, version::Version}; + use parcel_filesystem::in_memory_file_system::InMemoryFileSystem; + + use crate::request_tracker::RequestTracker; + + use super::*; + + const BUILT_IN_TARGETS: [&str; 4] = ["browser", "main", "module", "types"]; + + fn default_target() -> Target { + Target { + dist_dir: PathBuf::from("packages/test/dist"), + env: Environment { + output_format: OutputFormat::Global, + ..Environment::default() + }, + name: String::from("default"), + ..Target::default() + } + } + + fn package_dir() -> PathBuf { + PathBuf::from("packages").join("test") + } + + fn targets_from_package_json( + package_json: String, + ) -> Result, anyhow::Error> { + let fs = InMemoryFileSystem::default(); + let project_root = PathBuf::default(); + let package_dir = package_dir(); + + fs.write_file( + &project_root.join(&package_dir).join("package.json"), + package_json, + ); + + let request = TargetRequest { + config: ConfigLoader { + fs: Arc::new(fs), + project_root: project_root.clone(), + search_path: project_root.join(&package_dir), + }, + default_target_options: DefaultTargetOptions::default(), + env: None, + exclusive_target: None, + mode: BuildMode::Development, + }; + + request.run(RunRequestContext::new(None, &mut RequestTracker::new())) + } + + #[test] + fn returns_error_when_builtin_target_is_true() { + for builtin_target in BUILT_IN_TARGETS { + let targets = targets_from_package_json(format!( + r#"{{ "targets": {{ "{}": true }} }}"#, + builtin_target, + )); + + assert!(targets + .map_err(|e| e.to_string()) + .unwrap_err() + .starts_with("data did not match any variant")); + } + } + + #[test] + fn returns_error_when_builtin_target_does_not_reference_expected_extension() { + for builtin_target in BUILT_IN_TARGETS { + let targets = + targets_from_package_json(format!(r#"{{ "{}": "dist/main.rs" }}"#, builtin_target,)); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Err(format!( + "Unexpected file type \"main.rs\" in \"{}\" target", + builtin_target + )) + ); + } + } + + #[test] + fn returns_error_when_scope_hoisting_disabled_for_library_targets() { + let assert_error = |name, package_json| { + let targets = targets_from_package_json(package_json); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Err(format!( + "Scope hoisting cannot be disabled for \"{}\" library target", + name + )) + ); + }; + + for builtin_target in BUILT_IN_TARGETS { + assert_error( + builtin_target, + format!( + r#" + {{ + "{}": "dist/target.{}", + "targets": {{ + "{}": {{ + "isLibrary": true, + "scopeHoist": false + }} + }} + }} + "#, + builtin_target, + if builtin_target == "types" { + "ts" + } else { + "js" + }, + builtin_target, + ), + ); + } + + assert_error( + "custom", + String::from( + r#" + { + "targets": { + "custom": { + "isLibrary": true, + "scopeHoist": false + } + } + } + "#, + ), + ); + } + + #[test] + fn returns_default_target_when_builtin_targets_are_disabled() { + for builtin_target in BUILT_IN_TARGETS { + let targets = targets_from_package_json(format!( + r#"{{ "targets": {{ "{}": false }} }}"#, + builtin_target, + )); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![default_target()]), + invalidations: Vec::new() + }) + ); + } + } + + #[test] + fn returns_default_target_when_no_targets_are_specified() { + let targets = targets_from_package_json(String::from("{}")); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![default_target()]), + invalidations: Vec::new(), + }) + ); + } + + fn builtin_default_env() -> Environment { + Environment { + include_node_modules: IncludeNodeModules::Bool(false), + is_library: true, + should_optimize: false, + should_scope_hoist: true, + ..Environment::default() + } + } + + #[test] + fn returns_builtin_browser_target() { + let targets = targets_from_package_json(String::from(r#"{ "browser": "build/browser.js" }"#)); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![Target { + dist_dir: package_dir().join("build"), + dist_entry: Some(PathBuf::from("browser.js")), + env: Environment { + context: EnvironmentContext::Browser, + output_format: OutputFormat::CommonJS, + ..builtin_default_env() + }, + name: String::from("browser"), + ..Target::default() + },]), + invalidations: Vec::new(), + }) + ); + } + + #[test] + fn returns_builtin_main_target() { + let targets = targets_from_package_json(String::from(r#"{ "main": "./build/main.js" }"#)); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![Target { + dist_dir: package_dir().join("build"), + dist_entry: Some(PathBuf::from("main.js")), + env: Environment { + context: EnvironmentContext::Node, + output_format: OutputFormat::CommonJS, + ..builtin_default_env() + }, + name: String::from("main"), + ..Target::default() + },]), + invalidations: Vec::new(), + }) + ); + } + + #[test] + fn returns_builtin_module_target() { + let targets = targets_from_package_json(String::from(r#"{ "module": "module.js" }"#)); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![Target { + dist_dir: package_dir(), + dist_entry: Some(PathBuf::from("module.js")), + env: Environment { + context: EnvironmentContext::Node, + output_format: OutputFormat::EsModule, + ..builtin_default_env() + }, + name: String::from("module"), + ..Target::default() + },]), + invalidations: Vec::new(), + }) + ); + } + + #[test] + fn returns_builtin_types_target() { + let targets = targets_from_package_json(String::from(r#"{ "types": "./types.d.ts" }"#)); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![Target { + dist_dir: package_dir(), + dist_entry: Some(PathBuf::from("types.d.ts")), + env: Environment { + context: EnvironmentContext::Node, + output_format: OutputFormat::CommonJS, + ..builtin_default_env() + }, + name: String::from("types"), + ..Target::default() + },]), + invalidations: Vec::new(), + }) + ); + } + + #[test] + fn returns_builtin_targets() { + let targets = targets_from_package_json(String::from( + r#" + { + "browser": "build/browser.js", + "main": "./build/main.js", + "module": "module.js", + "types": "./types.d.ts", + "browserslist": ["chrome 20"] + } + "#, + )); + + let env = || Environment { + engines: Engines { + browsers: Browsers { + chrome: Some(Version::new(NonZeroU16::new(20).unwrap(), 0)), + ..Browsers::default() + }, + ..Engines::default() + }, + ..builtin_default_env() + }; + + let package_dir = package_dir(); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![ + Target { + dist_dir: package_dir.join("build"), + dist_entry: Some(PathBuf::from("browser.js")), + env: Environment { + context: EnvironmentContext::Browser, + output_format: OutputFormat::CommonJS, + ..env() + }, + name: String::from("browser"), + ..Target::default() + }, + Target { + dist_dir: package_dir.join("build"), + dist_entry: Some(PathBuf::from("main.js")), + env: Environment { + context: EnvironmentContext::Node, + output_format: OutputFormat::CommonJS, + ..env() + }, + name: String::from("main"), + ..Target::default() + }, + Target { + dist_dir: package_dir.clone(), + dist_entry: Some(PathBuf::from("module.js")), + env: Environment { + context: EnvironmentContext::Node, + output_format: OutputFormat::EsModule, + ..env() + }, + name: String::from("module"), + ..Target::default() + }, + Target { + dist_dir: package_dir, + dist_entry: Some(PathBuf::from("types.d.ts")), + env: Environment { + context: EnvironmentContext::Node, + output_format: OutputFormat::CommonJS, + ..env() + }, + name: String::from("types"), + ..Target::default() + }, + ]), + invalidations: Vec::new(), + }) + ); + } + + #[test] + fn returns_custom_targets_with_defaults() { + let targets = targets_from_package_json(String::from(r#"{ "targets": { "custom": {} } } "#)); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![Target { + dist_dir: package_dir().join("dist").join("custom"), + dist_entry: None, + env: Environment { + context: EnvironmentContext::Browser, + is_library: false, + output_format: OutputFormat::Global, + should_optimize: false, + should_scope_hoist: false, + ..Environment::default() + }, + name: String::from("custom"), + ..Target::default() + },]), + invalidations: Vec::new(), + }) + ); + } + + #[test] + fn returns_custom_targets() { + let targets = targets_from_package_json(String::from( + r#" + { + "custom": "dist/custom.js", + "targets": { + "custom": { + "context": "node", + "includeNodeModules": true, + "outputFormat": "commonjs" + } + } + } + "#, + )); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![Target { + dist_dir: package_dir().join("dist"), + dist_entry: Some(PathBuf::from("custom.js")), + env: Environment { + context: EnvironmentContext::Node, + include_node_modules: IncludeNodeModules::Bool(true), + is_library: false, + output_format: OutputFormat::CommonJS, + ..Environment::default() + }, + name: String::from("custom"), + ..Target::default() + },]), + invalidations: Vec::new(), + }) + ); + } + + #[test] + fn returns_inferred_custom_browser_target() { + let targets = targets_from_package_json(String::from( + r#" + { + "custom": "dist/custom.js", + "browserslist": ["chrome 20", "firefox > 1"], + "targets": { + "custom": {} + } + } + "#, + )); + + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![Target { + dist_dir: package_dir().join("dist"), + dist_entry: Some(PathBuf::from("custom.js")), + env: Environment { + context: EnvironmentContext::Browser, + engines: Engines { + browsers: Browsers { + chrome: Some(Version::new(NonZeroU16::new(20).unwrap(), 0)), + firefox: Some(Version::new(NonZeroU16::new(2).unwrap(), 0)), + ..Browsers::default() + }, + ..Engines::default() + }, + include_node_modules: IncludeNodeModules::Bool(true), + output_format: OutputFormat::Global, + ..Environment::default() + }, + name: String::from("custom"), + ..Target::default() + },]), + invalidations: Vec::new(), + }) + ); + } + + #[test] + fn returns_inferred_custom_node_target() { + let assert_targets = |targets: Result, anyhow::Error>, engines| { + assert_eq!( + targets.map_err(|e| e.to_string()), + Ok(RequestResult { + result: Targets(vec![Target { + dist_dir: package_dir().join("dist"), + dist_entry: Some(PathBuf::from("custom.js")), + env: Environment { + context: EnvironmentContext::Node, + engines, + include_node_modules: IncludeNodeModules::Bool(false), + output_format: OutputFormat::CommonJS, + ..Environment::default() + }, + name: String::from("custom"), + ..Target::default() + },]), + invalidations: Vec::new(), + }) + ); + }; + + assert_targets( + targets_from_package_json(String::from( + r#" + { + "custom": "dist/custom.js", + "engines": { "node": "^1.0.0" }, + "targets": { "custom": {} } + } + "#, + )), + Engines { + node: Some(Version::new(NonZeroU16::new(1).unwrap(), 0)), + ..Engines::default() + }, + ); + + assert_targets( + targets_from_package_json(String::from( + r#" + { + "custom": "dist/custom.js", + "engines": { "node": "^1.0.0" }, + "browserslist": ["chrome 20"], + "targets": { "custom": {} } + } + "#, + )), + Engines { + browsers: Browsers { + chrome: Some(Version::new(NonZeroU16::new(20).unwrap(), 0)), + ..Browsers::default() + }, + node: Some(Version::new(NonZeroU16::new(1).unwrap(), 0)), + ..Engines::default() + }, + ); + } +} diff --git a/crates/parcel/src/requests/target_request/package_json.rs b/crates/parcel/src/requests/target_request/package_json.rs new file mode 100644 index 00000000000..1e822d760a3 --- /dev/null +++ b/crates/parcel/src/requests/target_request/package_json.rs @@ -0,0 +1,91 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use parcel_core::types::engines::Engines; +use parcel_core::types::Entry; +use parcel_core::types::EnvironmentContext; +use parcel_core::types::OutputFormat; +use parcel_core::types::TargetSourceMapOptions; +use parcel_resolver::IncludeNodeModules; +use serde::Deserialize; + +#[derive(Clone, Deserialize)] +#[serde(untagged)] +pub enum BrowserField { + EntryPoint(PathBuf), + // TODO false value + ReplacementBySpecifier(HashMap), +} + +#[derive(Clone, Deserialize)] +#[serde(untagged)] +pub enum BuiltInTargetDescriptor { + Disabled(serde_bool::False), + TargetDescriptor(TargetDescriptor), +} + +#[derive(Clone, Default, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct TargetDescriptor { + pub context: Option, + pub dist_dir: Option, + pub dist_entry: Option, + pub engines: Option, + pub include_node_modules: Option, + pub is_library: Option, + pub optimize: Option, + pub output_format: Option, + pub public_url: Option, + pub scope_hoist: Option, + pub source: Option, + pub source_map: Option, +} + +#[derive(Clone, Deserialize)] +#[serde(untagged)] +pub enum BrowsersList { + Browsers(Vec), + BrowsersByEnv(HashMap>), +} + +#[derive(Default, Deserialize)] +pub struct TargetsField { + pub browser: Option, + pub main: Option, + pub module: Option, + pub types: Option, + + #[serde(flatten)] + pub custom_targets: HashMap, +} + +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ModuleFormat { + CommonJS, + Module, +} + +#[derive(Deserialize)] +pub struct PackageJson { + pub name: Option, + #[serde(rename = "type")] + pub module_format: Option, + pub browser: Option, + pub main: Option, + pub module: Option, + pub types: Option, + #[serde(default)] + pub engines: Option, + pub browserslist: Option, + #[serde(default)] + pub targets: TargetsField, + #[serde(flatten)] + pub fields: HashMap, +} + +#[derive(Clone, Deserialize)] +pub enum SourceMapField { + Bool(bool), + Options(TargetSourceMapOptions), +} diff --git a/crates/parcel_core/Cargo.toml b/crates/parcel_core/Cargo.toml index da7513b8f3d..bc1ce1ec5b3 100644 --- a/crates/parcel_core/Cargo.toml +++ b/crates/parcel_core/Cargo.toml @@ -15,7 +15,7 @@ bitflags = "2.5.0" browserslist-rs = "0.15.0" dyn-hash = "0.x" nodejs-semver = "4.0.0" -serde = { version = "1.0.200", features = ["derive"] } +serde = { version = "1.0.200", features = ["derive", "rc"] } serde_json = { version = "1.0.116", features = ["preserve_order"] } serde_repr = "0.1.19" serde-value = "0.7.0" diff --git a/crates/parcel_core/src/plugin/plugin_config.rs b/crates/parcel_core/src/config_loader.rs similarity index 81% rename from crates/parcel_core/src/plugin/plugin_config.rs rename to crates/parcel_core/src/config_loader.rs index a06cac501ad..d5a32b3efac 100644 --- a/crates/parcel_core/src/plugin/plugin_config.rs +++ b/crates/parcel_core/src/config_loader.rs @@ -1,28 +1,19 @@ use std::path::PathBuf; +use anyhow::anyhow; use parcel_filesystem::search::find_ancestor_file; use parcel_filesystem::FileSystemRef; use serde::de::DeserializeOwned; -use crate::types::JSONObject; - -/// Enables plugins to load config in various formats -pub struct PluginConfig { +/// Enables config to be loaded in various formats +pub struct ConfigLoader { pub fs: FileSystemRef, pub project_root: PathBuf, pub search_path: PathBuf, } // TODO JavaScript configs, invalidations, dev deps, etc -impl PluginConfig { - pub fn new(fs: FileSystemRef, project_root: PathBuf, search_path: PathBuf) -> Self { - Self { - fs, - project_root, - search_path, - } - } - +impl ConfigLoader { pub fn load_json_config( &self, filename: &str, @@ -33,30 +24,23 @@ impl PluginConfig { &self.search_path, &self.project_root, ) - .ok_or(anyhow::Error::msg(format!( + .ok_or(anyhow!( "Unable to locate {} config file from {}", filename, self.search_path.display() - )))?; + ))?; let config = self.fs.read_to_string(&config_path)?; - let config = serde_json::from_str::(&config)?; + let config = serde_json::from_str::(&config) + .map_err(|error| anyhow!("{} in {}", error, config_path.display(),))?; Ok((config_path, config)) } - pub fn load_package_json_config( + pub fn load_package_json_config( &self, - key: &str, - ) -> Result<(PathBuf, serde_json::Value), anyhow::Error> { - let (config_path, config) = self.load_json_config::("package.json")?; - let config = config.get(key).ok_or(anyhow::Error::msg(format!( - "Unable to locate {} config key in {}", - key, - config_path.display() - )))?; - - Ok((config_path, config.clone())) + ) -> Result<(PathBuf, Config), anyhow::Error> { + self.load_json_config::("package.json") } } @@ -81,7 +65,7 @@ mod tests { let project_root = PathBuf::from("/project-root"); let search_path = project_root.join("index"); - let config = PluginConfig { + let config = ConfigLoader { fs: Arc::new(InMemoryFileSystem::default()), project_root, search_path: search_path.clone(), @@ -109,7 +93,7 @@ mod tests { String::from("{}"), ); - let config = PluginConfig { + let config = ConfigLoader { fs, project_root: PathBuf::default(), search_path: search_path.clone(), @@ -134,7 +118,7 @@ mod tests { fs.write_file(&PathBuf::from("config.json"), String::from("{}")); - let config = PluginConfig { + let config = ConfigLoader { fs, project_root, search_path: search_path.clone(), @@ -160,7 +144,7 @@ mod tests { fs.write_file(&config_path, String::from("{}")); - let config = PluginConfig { + let config = ConfigLoader { fs, project_root, search_path, @@ -183,7 +167,7 @@ mod tests { fs.write_file(&config_path, String::from("{}")); - let config = PluginConfig { + let config = ConfigLoader { fs, project_root, search_path, @@ -201,15 +185,14 @@ mod tests { mod load_package_json_config { use std::sync::Arc; - use serde_json::Map; - use serde_json::Value; - use super::*; fn package_json() -> String { String::from( r#" { + "name": "parcel", + "version": "1.0.0", "plugin": { "enabled": true } @@ -218,12 +201,20 @@ mod tests { ) } - fn package_config() -> Value { - let mut map = Map::new(); + fn package_config() -> PackageJsonConfig { + PackageJsonConfig { + plugin: PluginConfig { enabled: true }, + } + } - map.insert(String::from("enabled"), Value::Bool(true)); + #[derive(Debug, PartialEq, serde::Deserialize)] + struct PluginConfig { + enabled: bool, + } - Value::Object(map) + #[derive(Debug, PartialEq, serde::Deserialize)] + struct PackageJsonConfig { + plugin: PluginConfig, } #[test] @@ -231,7 +222,7 @@ mod tests { let project_root = PathBuf::from("/project-root"); let search_path = project_root.join("index"); - let config = PluginConfig { + let config = ConfigLoader { fs: Arc::new(InMemoryFileSystem::default()), project_root, search_path: search_path.clone(), @@ -239,7 +230,7 @@ mod tests { assert_eq!( config - .load_package_json_config("plugin") + .load_package_json_config::() .map_err(|err| err.to_string()), Err(format!( "Unable to locate package.json config file from {}", @@ -258,7 +249,7 @@ mod tests { fs.write_file(&package_path, String::from("{}")); fs.write_file(&project_root.join("package.json"), package_json()); - let config = PluginConfig { + let config = ConfigLoader { fs, project_root, search_path, @@ -266,10 +257,10 @@ mod tests { assert_eq!( config - .load_package_json_config("plugin") + .load_package_json_config::() .map_err(|err| err.to_string()), Err(format!( - "Unable to locate plugin config key in {}", + "missing field `plugin` at line 1 column 2 in {}", package_path.display() )) ) @@ -284,7 +275,7 @@ mod tests { fs.write_file(&package_path, String::from("{}")); - let config = PluginConfig { + let config = ConfigLoader { fs, project_root, search_path, @@ -292,10 +283,10 @@ mod tests { assert_eq!( config - .load_package_json_config("plugin") + .load_package_json_config::() .map_err(|err| err.to_string()), Err(format!( - "Unable to locate plugin config key in {}", + "missing field `plugin` at line 1 column 2 in {}", package_path.display() )) ) @@ -310,7 +301,7 @@ mod tests { fs.write_file(&package_path, package_json()); - let config = PluginConfig { + let config = ConfigLoader { fs, project_root, search_path, @@ -318,7 +309,7 @@ mod tests { assert_eq!( config - .load_package_json_config("plugin") + .load_package_json_config::() .map_err(|err| err.to_string()), Ok((package_path, package_config())) ) @@ -333,7 +324,7 @@ mod tests { fs.write_file(&package_path, package_json()); - let config = PluginConfig { + let config = ConfigLoader { fs, project_root, search_path, @@ -341,7 +332,7 @@ mod tests { assert_eq!( config - .load_package_json_config("plugin") + .load_package_json_config::() .map_err(|err| err.to_string()), Ok((package_path, package_config())) ) diff --git a/crates/parcel_core/src/lib.rs b/crates/parcel_core/src/lib.rs index 1e59003dece..fda45ec44a6 100644 --- a/crates/parcel_core/src/lib.rs +++ b/crates/parcel_core/src/lib.rs @@ -1,5 +1,6 @@ pub mod bundle_graph; pub mod cache; +pub mod config_loader; pub mod hash; pub mod plugin; pub mod types; diff --git a/crates/parcel_core/src/plugin.rs b/crates/parcel_core/src/plugin.rs index 6d8b7fc3c67..cfdcaf7d6f1 100644 --- a/crates/parcel_core/src/plugin.rs +++ b/crates/parcel_core/src/plugin.rs @@ -16,9 +16,6 @@ pub use optimizer_plugin::*; mod packager_plugin; pub use packager_plugin::*; -mod plugin_config; -pub use plugin_config::*; - mod reporter_plugin; pub use reporter_plugin::*; @@ -34,10 +31,11 @@ pub use transformer_plugin::*; mod validator_plugin; pub use validator_plugin::*; +use crate::config_loader::ConfigLoader; use crate::types::BuildMode; pub struct PluginContext { - pub config: PluginConfig, + pub config: ConfigLoader, pub options: Arc, pub logger: PluginLogger, } diff --git a/crates/parcel_core/src/plugin/validator_plugin.rs b/crates/parcel_core/src/plugin/validator_plugin.rs index 21db11a1ccf..4805cb02bd5 100644 --- a/crates/parcel_core/src/plugin/validator_plugin.rs +++ b/crates/parcel_core/src/plugin/validator_plugin.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use super::PluginConfig; +use super::ConfigLoader; use crate::types::Asset; pub struct Validation { @@ -25,7 +25,7 @@ pub trait ValidatorPlugin: Debug { /// /// This function will run once, shortly after the plugin is initialised. /// - fn load_config(&mut self, config: &PluginConfig) -> Result<(), anyhow::Error>; + fn load_config(&mut self, config: &ConfigLoader) -> Result<(), anyhow::Error>; /// Validates a single asset at a time /// @@ -33,7 +33,7 @@ pub trait ValidatorPlugin: Debug { /// fn validate_asset( &mut self, - config: &PluginConfig, + config: &ConfigLoader, asset: &Asset, ) -> Result; @@ -49,7 +49,7 @@ pub trait ValidatorPlugin: Debug { /// fn validate_assets( &mut self, - config: &PluginConfig, + config: &ConfigLoader, assets: Vec<&Asset>, ) -> Result; } @@ -62,13 +62,13 @@ mod tests { struct TestValidatorPlugin {} impl ValidatorPlugin for TestValidatorPlugin { - fn load_config(&mut self, _config: &PluginConfig) -> Result<(), anyhow::Error> { + fn load_config(&mut self, _config: &ConfigLoader) -> Result<(), anyhow::Error> { todo!() } fn validate_asset( &mut self, - _config: &PluginConfig, + _config: &ConfigLoader, _asset: &Asset, ) -> Result { todo!() @@ -76,7 +76,7 @@ mod tests { fn validate_assets( &mut self, - _config: &PluginConfig, + _config: &ConfigLoader, _assets: Vec<&Asset>, ) -> Result { todo!() diff --git a/crates/parcel_core/src/types/environment.rs b/crates/parcel_core/src/types/environment.rs index b887aa67f69..bd55e135dae 100644 --- a/crates/parcel_core/src/types/environment.rs +++ b/crates/parcel_core/src/types/environment.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; +use std::hash::Hash; +use std::hash::Hasher; use std::num::NonZeroU32; use serde::Deserialize; @@ -68,8 +70,8 @@ pub struct Environment { pub source_type: SourceType, } -impl std::hash::Hash for Environment { - fn hash(&self, state: &mut H) { +impl Hash for Environment { + fn hash(&self, state: &mut H) { // Hashing intentionally does not include loc self.context.hash(state); self.engines.hash(state); @@ -102,17 +104,17 @@ impl PartialEq for Environment { /// /// This informs Parcel what environment-specific APIs are available. /// -#[derive(Clone, Copy, Debug, Default, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)] -#[repr(u8)] +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] pub enum EnvironmentContext { #[default] - Browser = 0, - ElectronMain = 1, - ElectronRenderer = 2, - Node = 3, - ServiceWorker = 4, - WebWorker = 5, - Worklet = 6, + Browser, + ElectronMain, + ElectronRenderer, + Node, + ServiceWorker, + WebWorker, + Worklet, } impl EnvironmentContext { @@ -141,6 +143,7 @@ impl EnvironmentContext { } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(untagged)] pub enum IncludeNodeModules { Bool(bool), Array(Vec), @@ -153,8 +156,19 @@ impl Default for IncludeNodeModules { } } -impl std::hash::Hash for IncludeNodeModules { - fn hash(&self, state: &mut H) { +impl From for IncludeNodeModules { + fn from(context: EnvironmentContext) -> Self { + match context { + EnvironmentContext::Browser => IncludeNodeModules::Bool(true), + EnvironmentContext::ServiceWorker => IncludeNodeModules::Bool(true), + EnvironmentContext::WebWorker => IncludeNodeModules::Bool(true), + _ => IncludeNodeModules::Bool(false), + } + } +} + +impl Hash for IncludeNodeModules { + fn hash(&self, state: &mut H) { match self { IncludeNodeModules::Bool(b) => b.hash(state), IncludeNodeModules::Array(a) => a.hash(state), @@ -177,7 +191,7 @@ pub enum SourceType { } /// Source map options for the target output -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TargetSourceMapOptions { /// Inlines the source map as a data URL into the bundle, rather than link to it as a separate output file diff --git a/crates/parcel_core/src/types/environment/browsers.rs b/crates/parcel_core/src/types/environment/browsers.rs index 3bc1907d0a7..c53366b6085 100644 --- a/crates/parcel_core/src/types/environment/browsers.rs +++ b/crates/parcel_core/src/types/environment/browsers.rs @@ -1,6 +1,7 @@ +use std::fmt::Display; +use std::fmt::Formatter; + use browserslist::Distrib; -use serde::Deserialize; -use serde::Serialize; use super::version::Version; @@ -18,8 +19,22 @@ pub struct Browsers { pub samsung: Option, } -impl std::fmt::Display for Browsers { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Browsers { + pub fn is_empty(&self) -> bool { + self.android.is_none() + && self.chrome.is_none() + && self.edge.is_none() + && self.firefox.is_none() + && self.ie.is_none() + && self.ios_saf.is_none() + && self.opera.is_none() + && self.safari.is_none() + && self.samsung.is_none() + } +} + +impl Display for Browsers { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { macro_rules! browsers { ( $( $b:ident ),* ) => { // Bypass unused_assignments false positive @@ -73,16 +88,7 @@ impl From> for Browsers { } } -impl Serialize for Browsers { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - format!("{}", self).serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for Browsers { +impl<'de> serde::Deserialize<'de> for Browsers { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -97,6 +103,15 @@ impl<'de> Deserialize<'de> for Browsers { } } +impl serde::Serialize for Browsers { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + format!("{}", self).serialize(serializer) + } +} + #[cfg(test)] mod tests { use std::num::NonZeroU16; diff --git a/crates/parcel_core/src/types/environment/engines.rs b/crates/parcel_core/src/types/environment/engines.rs index 1372d62eed5..a755764e121 100644 --- a/crates/parcel_core/src/types/environment/engines.rs +++ b/crates/parcel_core/src/types/environment/engines.rs @@ -4,8 +4,8 @@ use serde::Deserialize; use serde::Serialize; use super::browsers::Browsers; -use super::output_format::OutputFormat; use super::version::Version; +use super::OutputFormat; /// The engines field in package.json #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] @@ -62,7 +62,7 @@ impl EnvironmentFeature { /// List of browsers to exclude when the esmodule target is specified based on /// https://caniuse.com/#feat=es6-module -const ESMODULE_BROWSERS: &'static [&'static str] = &[ +const _ESMODULE_BROWSERS: &'static [&'static str] = &[ "not ie <= 11", "not edge < 16", "not firefox < 60", @@ -85,24 +85,16 @@ const ESMODULE_BROWSERS: &'static [&'static str] = &[ ]; impl Engines { - pub fn from_browserslist(browserslist: &str, output_format: OutputFormat) -> Engines { - let browsers = if output_format == OutputFormat::EsModule { - // If the output format is esmodule, exclude browsers - // that support them natively so that we transpile less. - browserslist::resolve( - std::iter::once(browserslist).chain(ESMODULE_BROWSERS.iter().map(|s| *s)), - &Default::default(), - ) - } else { - browserslist::resolve(std::iter::once(browserslist), &Default::default()) - }; + pub fn from_browserslist(browserslist: Vec) -> Browsers { + browserslist::resolve(browserslist, &Default::default()) + .map(|b| b.into()) + .unwrap_or_default() + } - Engines { - browsers: browsers.map(|b| b.into()).unwrap_or_default(), - electron: None, - node: None, - parcel: None, - } + // TODO Reinstate this so that engines.browsers are filtered out with ESMODULE_BROWSERS when + // we are using an esmodule output format + pub fn optimize(_engines: Engines, _output_format: OutputFormat) -> Engines { + todo!() } pub fn supports(&self, feature: EnvironmentFeature) -> bool { diff --git a/crates/parcel_core/src/types/environment/output_format.rs b/crates/parcel_core/src/types/environment/output_format.rs index db5f15dd437..05ac90315f5 100644 --- a/crates/parcel_core/src/types/environment/output_format.rs +++ b/crates/parcel_core/src/types/environment/output_format.rs @@ -1,26 +1,37 @@ -use serde_repr::Deserialize_repr; -use serde_repr::Serialize_repr; +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; /// The JavaScript bundle output format -#[derive(Clone, Copy, Debug, Default, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)] -#[repr(u8)] +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] pub enum OutputFormat { /// A classic script that can be loaded in a