From a4a4033001633489e9d1075719978510f3245891 Mon Sep 17 00:00:00 2001 From: Monica Date: Tue, 14 May 2024 15:08:43 +1000 Subject: [PATCH] Add core rust types (#9714) * Add rust types * Remove bitflags * Use ahash for better platform support * Reexport core types --- Cargo.lock | 105 +++++++- crates/parcel_core/Cargo.toml | 11 +- crates/parcel_core/src/lib.rs | 3 + crates/parcel_core/src/types.rs | 29 ++ crates/parcel_core/src/types/asset.rs | 102 +++++++ crates/parcel_core/src/types/bundle.rs | 92 +++++++ crates/parcel_core/src/types/dependency.rs | 199 ++++++++++++++ crates/parcel_core/src/types/environment.rs | 168 ++++++++++++ .../src/types/environment/browsers.rs | 144 ++++++++++ .../src/types/environment/engines.rs | 133 ++++++++++ .../src/types/environment/output_format.rs | 25 ++ .../src/types/environment/version.rs | 249 ++++++++++++++++++ crates/parcel_core/src/types/file_type.rs | 61 +++++ crates/parcel_core/src/types/json.rs | 1 + .../parcel_core/src/types/parcel_options.rs | 49 ++++ crates/parcel_core/src/types/source.rs | 27 ++ crates/parcel_core/src/types/symbol.rs | 12 + crates/parcel_core/src/types/target.rs | 38 +++ packages/utils/node-resolver-rs/src/lib.rs | 19 +- .../node-resolver-rs/src/package_json.rs | 19 ++ 20 files changed, 1479 insertions(+), 7 deletions(-) create mode 100644 crates/parcel_core/src/types.rs create mode 100644 crates/parcel_core/src/types/asset.rs create mode 100644 crates/parcel_core/src/types/bundle.rs create mode 100644 crates/parcel_core/src/types/dependency.rs create mode 100644 crates/parcel_core/src/types/environment.rs create mode 100644 crates/parcel_core/src/types/environment/browsers.rs create mode 100644 crates/parcel_core/src/types/environment/engines.rs create mode 100644 crates/parcel_core/src/types/environment/output_format.rs create mode 100644 crates/parcel_core/src/types/environment/version.rs create mode 100644 crates/parcel_core/src/types/file_type.rs create mode 100644 crates/parcel_core/src/types/json.rs create mode 100644 crates/parcel_core/src/types/parcel_options.rs create mode 100644 crates/parcel_core/src/types/source.rs create mode 100644 crates/parcel_core/src/types/symbol.rs create mode 100644 crates/parcel_core/src/types/target.rs diff --git a/Cargo.lock b/Cargo.lock index 2cd0a3b5cd6..bc873b34805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,6 +223,28 @@ dependencies = [ "thiserror", ] +[[package]] +name = "browserslist-rs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "405bbd46590a441abe5db3e5c8af005aa42e640803fecb51912703e93e4ce8d3" +dependencies = [ + "ahash", + "anyhow", + "chrono", + "either", + "indexmap 2.2.6", + "itertools 0.12.0", + "nom", + "once_cell", + "quote", + "serde", + "serde_json", + "string_cache", + "string_cache_codegen", + "thiserror", +] + [[package]] name = "bstr" version = "1.2.0" @@ -239,6 +261,12 @@ version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + [[package]] name = "bytemuck" version = "1.12.3" @@ -1346,6 +1374,29 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miette" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" +dependencies = [ + "cfg-if", + "miette-derive", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "mimalloc" version = "0.1.32" @@ -1527,6 +1578,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nodejs-semver" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b8b91a6d799a23136abcf1a43ae9faed89298047272eee353eb701d195b18c" +dependencies = [ + "bytecount", + "miette", + "thiserror", + "winnow", +] + [[package]] name = "nom" version = "7.1.3" @@ -1654,6 +1717,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "os_info" version = "3.8.1" @@ -1823,16 +1895,21 @@ dependencies = [ name = "parcel_core" version = "0.1.0" dependencies = [ + "ahash", "anyhow", + "browserslist-rs 0.15.0", "glob", "mockall", "napi", "napi-derive", + "nodejs-semver", "parcel-resolver", "parcel_filesystem", "parcel_napi_helpers", "serde", + "serde-value", "serde_json", + "serde_repr", "toml", "xxhash-rust", ] @@ -2117,7 +2194,7 @@ checksum = "d4e9bedef66806cb32828719aa5cad298e363ad50d190538db40b5631b89d456" dependencies = [ "ahash", "anyhow", - "browserslist-rs", + "browserslist-rs 0.14.0", "dashmap", "from_variant", "once_cell", @@ -2661,6 +2738,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.7" @@ -2687,6 +2774,7 @@ version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -2703,6 +2791,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -3986,9 +4085,9 @@ checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "untrusted" diff --git a/crates/parcel_core/Cargo.toml b/crates/parcel_core/Cargo.toml index ad815fd0c45..9d3cf720a1c 100644 --- a/crates/parcel_core/Cargo.toml +++ b/crates/parcel_core/Cargo.toml @@ -13,12 +13,17 @@ parcel_filesystem = { path = "../parcel_filesystem" } parcel_napi_helpers = { path = "../parcel_napi_helpers" } parcel-resolver = { path = "../../packages/utils/node-resolver-rs" } +ahash = "0.8.11" anyhow = "1.0.82" +browserslist-rs = "0.15.0" glob = "0.3.1" mockall = "0.12.1" napi = "2.16.4" napi-derive = { version = "2.16.3" } -serde = "1.0.200" -serde_json = "1.0.116" +nodejs-semver = "4.0.0" +serde = { version = "1.0.200", features = ["derive"] } +serde_json = { version = "1.0.116", features = ["preserve_order"] } +serde_repr = "0.1.19" +serde-value = "0.7.0" toml = "0.8.12" -xxhash-rust = { version = "0.8.2", features = ["xxh3"] } \ No newline at end of file +xxhash-rust = { version = "0.8.2", features = ["xxh3"] } diff --git a/crates/parcel_core/src/lib.rs b/crates/parcel_core/src/lib.rs index b32f41ff0cc..db6ba20f96a 100644 --- a/crates/parcel_core/src/lib.rs +++ b/crates/parcel_core/src/lib.rs @@ -1,7 +1,10 @@ //! Core re-implementation in Rust pub mod hash; +pub mod types; + /// New-type for paths relative to a project-root pub mod project_path; + /// Request types and run functions pub mod requests; diff --git a/crates/parcel_core/src/types.rs b/crates/parcel_core/src/types.rs new file mode 100644 index 00000000000..b2d80615472 --- /dev/null +++ b/crates/parcel_core/src/types.rs @@ -0,0 +1,29 @@ +mod asset; +pub use self::asset::*; + +mod bundle; +pub use self::bundle::*; + +mod dependency; +pub use self::dependency::*; + +mod environment; +pub use self::environment::*; + +mod file_type; +pub use self::file_type::*; + +mod json; +pub use self::json::*; + +mod parcel_options; +pub use self::parcel_options::*; + +mod source; +pub use self::source::*; + +mod symbol; +pub use self::symbol::*; + +mod target; +pub use self::target::*; diff --git a/crates/parcel_core/src/types/asset.rs b/crates/parcel_core/src/types/asset.rs new file mode 100644 index 00000000000..7160f09b15e --- /dev/null +++ b/crates/parcel_core/src/types/asset.rs @@ -0,0 +1,102 @@ +use std::hash::Hash; +use std::hash::Hasher; +use std::num::NonZeroU32; +use std::path::PathBuf; + +use ahash::AHasher; +use serde::Deserialize; +use serde::Serialize; + +use super::bundle::BundleBehavior; +use super::environment::Environment; +use super::file_type::FileType; +use super::json::JSONObject; +use super::symbol::Symbol; + +#[derive(PartialEq, Hash, Clone, Copy, Debug)] +pub struct AssetId(pub NonZeroU32); + +/// An asset is a file or part of a file that may represent any data type including source code, binary data, etc. +/// +/// Note that assets may exist in the file system or virtually. +/// +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Asset { + /// The file type of the asset, which may change during transformation + #[serde(rename = "type")] + pub asset_type: FileType, + + /// Controls which bundle the asset is placed into + pub bundle_behavior: BundleBehavior, + + /// The environment of the asset + pub env: Environment, + + /// The file path to the asset + pub file_path: PathBuf, + + /// Indicates if the asset is used as a bundle entry + /// + /// This controls whether a bundle can be split into multiple, or whether all of the + /// dependencies must be placed in a single bundle. + /// + pub is_bundle_splittable: bool, + + /// Whether this asset is part of the project, and not an external dependency + /// + /// This indicates that transformation using the project configuration should be applied. + /// + pub is_source: bool, + + /// Plugin specific metadata for the asset + pub meta: JSONObject, + + /// The pipeline defined in .parcelrc that the asset should be processed with + pub pipeline: Option, + + /// The transformer options for the asset from the dependency query string + pub query: Option, + + /// Whether this asset can be omitted if none of its exports are being used + /// + /// This is initially set by the resolver, but can be overridden by transformers. + /// + pub side_effects: bool, + + /// Statistics about the asset + pub stats: AssetStats, + + /// The symbols that the asset exports + pub symbols: Vec, + + /// A unique key that identifies an asset + /// + /// When a transformer returns multiple assets, it can give them unique keys to identify them. + /// This can be used to find assets during packaging, or to create dependencies between multiple + /// assets returned by a transformer by using the unique key as the dependency specifier. + /// + pub unique_key: Option, +} + +impl Asset { + pub fn id(&self) -> u64 { + let mut hasher = AHasher::default(); + + self.asset_type.hash(&mut hasher); + self.env.hash(&mut hasher); + self.file_path.hash(&mut hasher); + self.pipeline.hash(&mut hasher); + self.query.hash(&mut hasher); + self.unique_key.hash(&mut hasher); + + hasher.finish() + } +} + +/// Statistics that pertain to an asset +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct AssetStats { + pub size: u32, + pub time: u32, +} diff --git a/crates/parcel_core/src/types/bundle.rs b/crates/parcel_core/src/types/bundle.rs new file mode 100644 index 00000000000..ccbd37740ef --- /dev/null +++ b/crates/parcel_core/src/types/bundle.rs @@ -0,0 +1,92 @@ +use serde::Deserialize; +use serde::Serialize; +use serde_repr::Deserialize_repr; +use serde_repr::Serialize_repr; + +use super::environment::Environment; +use super::file_type::FileType; +use super::target::Target; + +#[derive(Clone, Debug, Deserialize, Hash, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Bundle { + /// Controls the behavior of the bundle to determine when the bundle loads + pub bundle_behavior: BundleBehavior, + + /// The type of the bundle + #[serde(rename = "type")] + pub bundle_type: FileType, + + /// The list of assets executed immediately when the bundle is loaded + /// + /// Some bundles may not have any entry assets, like shared bundles. + /// + pub entry_asset_ids: Vec, + + /// The environment of the bundle + pub env: Environment, + + /// A placeholder for the bundle content hash + /// + /// It can be used in the bundle's name or the contents of another bundle. Hash references are replaced + /// with a content hash of the bundle after packaging and optimizing. + /// + pub hash_reference: String, + + /// The bundle id + pub id: String, + + /// Whether the bundle can be split + /// + /// If false, then all dependencies of the bundle will be kept internal to the bundle, rather + /// than referring to other bundles. This may result in assets being duplicated between + /// multiple bundles, but can be useful for things like server side rendering. + /// + pub is_splittable: bool, + + /// The main entry of the bundle, which will provide the bundle exports + /// + /// Some bundles, such as shared bundles, may not have a main entry. + /// + pub main_entry_id: Option, + + pub manual_shared_bundle: Option, + + /// The name of the bundle, which is a file path relative to the bundle target directory + /// + /// The bundle name may include a hash reference, but not the final content hash. + /// + pub name: Option, + + /// Indicates that the name should be stable over time, even when the content of the bundle changes + pub needs_stable_name: bool, + + /// The pipeline associated with the bundle + pub pipeline: Option, + + /// A shortened version of the bundle id that is used to refer to the bundle at runtime + pub public_id: Option, + + /// The output target for the bundle + pub target: Target, +} + +/// Determines when the bundle loads +#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)] +#[repr(u8)] +pub enum BundleBehavior { + /// Embeds an asset into the parent bundle by creating an inline bundle + Inline = 0, + + /// The asset will be isolated from its parents in a separate bundle, and shared assets will be duplicated + Isolated = 1, + + /// Unspecified bundling behavior + None = 255, +} + +impl Default for BundleBehavior { + fn default() -> Self { + BundleBehavior::None + } +} diff --git a/crates/parcel_core/src/types/dependency.rs b/crates/parcel_core/src/types/dependency.rs new file mode 100644 index 00000000000..48b69196b18 --- /dev/null +++ b/crates/parcel_core/src/types/dependency.rs @@ -0,0 +1,199 @@ +use std::hash::Hash; +use std::hash::Hasher; +use std::path::PathBuf; + +use ahash::AHasher; +use parcel_resolver::ExportsCondition; +use serde::Deserialize; +use serde::Serialize; +use serde_repr::Deserialize_repr; +use serde_repr::Serialize_repr; + +use super::bundle::BundleBehavior; +use super::environment::Environment; +use super::json::JSONObject; +use super::source::SourceLocation; +use super::symbol::Symbol; +use super::target::Target; + +/// A dependency denotes a connection between two assets +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Dependency { + /// Controls the behavior of the bundle the resolved asset is placed into + /// + /// This option is used in combination with priority to determine when the bundle is loaded. + /// + pub bundle_behavior: BundleBehavior, + + /// The environment of the dependency + pub env: Environment, + + /// Whether the dependency is an entry + pub is_entry: bool, + + /// Whether the dependency is optional + /// + /// If an optional dependency cannot be resolved, it will not fail the build. + /// + pub is_optional: bool, + + /// The location within the source file where the dependency was found + #[serde(default)] + pub loc: Option, + + /// Plugin-specific metadata for the dependency + #[serde(default)] + pub meta: JSONObject, + + /// Indicates that the name should be stable over time, even when the content of the bundle changes + /// + /// When the dependency is a bundle entry (priority is "parallel" or "lazy"), this controls the + /// naming of that bundle. + /// + /// This is useful for entries that a user would manually enter the URL for, as well as for + /// things like service workers or RSS feeds, where the URL must remain consistent over time. + /// + pub needs_stable_name: bool, + + /// A list of custom conditions to use when resolving package.json "exports" and "imports" + /// + /// This will be combined with the conditions from the environment. However, it overrides the default "import" and "require" conditions inferred from the specifierType. To include those in addition to custom conditions, explicitly add them to this list. + /// + #[serde(default)] + pub package_conditions: ExportsCondition, + + /// The pipeline defined in .parcelrc that the dependency should be processed with + #[serde(default)] + pub pipeline: Option, + + /// Determines when the dependency should be loaded + pub priority: Priority, + + /// The semver version range expected for the dependency + pub range: Option, + + /// The file path where the dependency should be resolved from + /// + /// By default, this is the path of the source file where the dependency was specified. + /// + pub resolve_from: Option, + + /// The id of the asset with this dependency + pub source_asset_id: Option, + + /// The file path of the asset with this dependency + pub source_path: Option, + + /// The import or export specifier that connects two assets together + pub specifier: String, + + /// How the specifier should be interpreted + pub specifier_type: SpecifierType, + + #[serde(default)] + pub symbols: Vec, + + /// The target associated with an entry, if any + #[serde(default)] + pub target: Option>, +} + +impl Dependency { + pub fn new(specifier: String, env: Environment) -> Dependency { + Dependency { + bundle_behavior: BundleBehavior::None, + env, + is_entry: false, + is_optional: false, + loc: None, + meta: JSONObject::new(), + needs_stable_name: false, + package_conditions: ExportsCondition::empty(), + pipeline: None, + priority: Priority::default(), + range: None, + resolve_from: None, + source_asset_id: None, + source_path: None, + specifier, + specifier_type: SpecifierType::default(), + symbols: Vec::new(), + target: None, + } + } + + pub fn id(&self) -> u64 { + // Compute hashed dependency id + let mut hasher = AHasher::default(); + + self.bundle_behavior.hash(&mut hasher); + self.env.hash(&mut hasher); + self.package_conditions.hash(&mut hasher); + self.pipeline.hash(&mut hasher); + self.priority.hash(&mut hasher); + self.source_path.hash(&mut hasher); + self.specifier.hash(&mut hasher); + self.specifier_type.hash(&mut hasher); + + hasher.finish() + } +} + +#[derive(Clone, Debug, Deserialize, Hash, Serialize)] +pub struct ImportAttribute { + pub key: String, + pub value: bool, +} + +/// Determines when a dependency should load +#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)] +#[serde(rename_all = "lowercase")] +#[repr(u8)] +pub enum Priority { + /// Resolves the dependency synchronously, placing the resolved asset in the same bundle as the parent or another bundle that is already on the page + Sync = 0, + /// Places the dependency in a separate bundle loaded in parallel with the current bundle + Parallel = 1, + /// The dependency should be placed in a separate bundle that is loaded later + Lazy = 2, +} + +impl Default for Priority { + fn default() -> Self { + Priority::Sync + } +} + +/// The type of the import specifier +#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)] +#[serde(rename_all = "lowercase")] +#[repr(u8)] +pub enum SpecifierType { + /// An ES Module specifier + /// + /// This is parsed as an URL, but bare specifiers are treated as node_modules. + /// + Esm = 0, + + /// A CommonJS specifier + /// + /// This is not parsed as an URL. + /// + CommonJS = 1, + + /// A URL that works as in a browser + /// + /// Bare specifiers are treated as relative URLs. + /// + Url = 2, + + /// A custom specifier that must be handled by a custom resolver plugin + Custom = 3, +} + +impl Default for SpecifierType { + fn default() -> Self { + SpecifierType::Esm + } +} diff --git a/crates/parcel_core/src/types/environment.rs b/crates/parcel_core/src/types/environment.rs new file mode 100644 index 00000000000..449d1a6c278 --- /dev/null +++ b/crates/parcel_core/src/types/environment.rs @@ -0,0 +1,168 @@ +use std::num::NonZeroU32; + +use parcel_resolver::IncludeNodeModules; +use serde::Deserialize; +use serde::Serialize; +use serde_repr::Deserialize_repr; +use serde_repr::Serialize_repr; + +use self::engines::Engines; +use super::source::SourceLocation; + +pub mod browsers; +pub mod engines; +mod output_format; +pub mod version; + +pub use output_format::OutputFormat; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct EnvironmentId(pub NonZeroU32); + +/// The environment the built code will run in +/// +/// This influences how Parcel compiles your code, including what syntax to transpile. +/// +#[derive(Clone, Debug, Deserialize, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Environment { + /// The environment the output should run in + pub context: EnvironmentContext, + + /// The engines supported by the environment + pub engines: Engines, + + /// Describes which node_modules should be included in the output + pub include_node_modules: IncludeNodeModules, + + /// Whether this is a library build + /// + /// Treats the target as a library that would be published to npm and consumed by another tool, + /// rather than used directly in a browser or other target environment. + /// + /// Library targets must enable scope hoisting, and use a non-global output format. + /// + pub is_library: bool, + + pub loc: Option, + + /// Determines what type of module to output + pub output_format: OutputFormat, + + /// Determines whether scope hoisting should be enabled + /// + /// By default, scope hoisting is enabled for production builds. + /// + pub should_scope_hoist: bool, + + /// Determines whether the output should be optimised + /// + /// The exact behavior of this flag is determined by plugins. By default, optimization is + /// enabled during production builds for application targets. + /// + pub should_optimize: bool, + + /// Configures source maps, which are enabled by default + pub source_map: Option, + + pub source_type: SourceType, +} + +impl std::hash::Hash for Environment { + fn hash(&self, state: &mut H) { + // Hashing intentionally does not include loc + self.context.hash(state); + self.engines.hash(state); + self.include_node_modules.hash(state); + self.is_library.hash(state); + self.output_format.hash(state); + self.should_scope_hoist.hash(state); + self.should_optimize.hash(state); + self.source_map.hash(state); + self.source_type.hash(state); + } +} + +impl PartialEq for Environment { + fn eq(&self, other: &Self) -> bool { + // Equality intentionally does not include loc + self.context == other.context + && self.engines == other.engines + && self.include_node_modules == other.include_node_modules + && self.is_library == other.is_library + && self.output_format == other.output_format + && self.should_scope_hoist == other.should_scope_hoist + && self.should_optimize == other.should_optimize + && self.source_map == other.source_map + && self.source_type == other.source_type + } +} + +/// The environment the output should run in +/// +/// This informs Parcel what environment-specific APIs are available. +/// +#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)] +#[repr(u8)] +pub enum EnvironmentContext { + Browser = 0, + ElectronMain = 1, + ElectronRenderer = 2, + Node = 3, + ServiceWorker = 4, + WebWorker = 5, + Worklet = 6, +} + +impl EnvironmentContext { + pub fn is_node(&self) -> bool { + use EnvironmentContext::*; + matches!(self, Node | ElectronMain | ElectronRenderer) + } + + pub fn is_browser(&self) -> bool { + use EnvironmentContext::*; + matches!( + self, + Browser | WebWorker | ServiceWorker | Worklet | ElectronRenderer + ) + } + + pub fn is_worker(&self) -> bool { + use EnvironmentContext::*; + matches!(self, WebWorker | ServiceWorker) + } + + pub fn is_electron(&self) -> bool { + use EnvironmentContext::*; + matches!(self, ElectronMain | ElectronRenderer) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, Deserialize_repr, Hash, Serialize_repr)] +#[repr(u8)] +pub enum SourceType { + Module = 0, + Script = 1, +} + +/// Source map options for the target output +#[derive(Clone, Debug, 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 + inline: Option, + + /// Inlines the original source code into the source map, rather than loading them from the source root + /// + /// This is set to true by default when building browser targets for production. + /// + inline_sources: Option, + + /// The URL to load the original source code from + /// + /// This is set automatically in development when using the builtin Parcel development server. + /// Otherwise, it defaults to a relative path to the bundle from the project root. + /// + source_root: Option, +} diff --git a/crates/parcel_core/src/types/environment/browsers.rs b/crates/parcel_core/src/types/environment/browsers.rs new file mode 100644 index 00000000000..3bc1907d0a7 --- /dev/null +++ b/crates/parcel_core/src/types/environment/browsers.rs @@ -0,0 +1,144 @@ +use browserslist::Distrib; +use serde::Deserialize; +use serde::Serialize; + +use super::version::Version; + +/// List of targeted browsers +#[derive(Clone, Default, Debug, Eq, Hash, PartialEq)] +pub struct Browsers { + pub android: Option, + pub chrome: Option, + pub edge: Option, + pub firefox: Option, + pub ie: Option, + pub ios_saf: Option, + pub opera: Option, + pub safari: Option, + pub samsung: Option, +} + +impl std::fmt::Display for Browsers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + macro_rules! browsers { + ( $( $b:ident ),* ) => { + // Bypass unused_assignments false positive + let mut _is_first_write = true; + $( + if let Some(version) = self.$b { + if !_is_first_write { + write!(f, ", ")?; + } + _is_first_write = false; + write!(f, "{} {}", stringify!($b), version)?; + } + )* + }; + } + + browsers![android, chrome, edge, firefox, ie, ios_saf, opera, safari, samsung]; + Ok(()) + } +} + +impl From> for Browsers { + fn from(distribs: Vec) -> Self { + let mut browsers = Browsers::default(); + for distrib in distribs { + macro_rules! browser { + ($browser: ident) => {{ + if let Ok(v) = distrib.version().parse() { + if browsers.$browser.is_none() || v < browsers.$browser.unwrap() { + browsers.$browser = Some(v); + } + } + }}; + } + + match distrib.name() { + "android" => browser!(android), + "chrome" | "and_chr" => browser!(chrome), + "edge" => browser!(edge), + "firefox" | "and_ff" => browser!(firefox), + "ie" => browser!(ie), + "ios_saf" => browser!(ios_saf), + "opera" | "op_mob" => browser!(opera), + "safari" => browser!(safari), + "samsung" => browser!(samsung), + _ => {} + } + } + + 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 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = serde_value::Value::deserialize(deserializer)?; + let browsers = match value { + serde_value::Value::String(s) => vec![s], + value => Vec::::deserialize(serde_value::ValueDeserializer::new(value))?, + }; + let distribs = browserslist::resolve(browsers, &Default::default()).unwrap_or(Vec::new()); + Ok(distribs.into()) + } +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU16; + + use super::*; + + #[test] + fn display() { + assert_eq!(format!("{}", Browsers::default()), ""); + + assert_eq!( + format!( + "{}", + Browsers { + chrome: Some(Version::new(NonZeroU16::new(100).unwrap(), 0)), + ..Browsers::default() + } + ), + "chrome >= 100" + ); + + assert_eq!( + format!( + "{}", + Browsers { + chrome: Some(Version::new(NonZeroU16::new(1).unwrap(), 20)), + ..Browsers::default() + } + ), + "chrome >= 1.20" + ); + + assert_eq!( + format!( + "{}", + Browsers { + chrome: Some(Version::new(NonZeroU16::new(1).unwrap(), 20)), + firefox: Some(Version::new(NonZeroU16::new(100).unwrap(), 5)), + ..Browsers::default() + } + ), + "chrome >= 1.20, firefox >= 100.5" + ); + } +} diff --git a/crates/parcel_core/src/types/environment/engines.rs b/crates/parcel_core/src/types/environment/engines.rs new file mode 100644 index 00000000000..1372d62eed5 --- /dev/null +++ b/crates/parcel_core/src/types/environment/engines.rs @@ -0,0 +1,133 @@ +use std::num::NonZeroU16; + +use serde::Deserialize; +use serde::Serialize; + +use super::browsers::Browsers; +use super::output_format::OutputFormat; +use super::version::Version; + +/// The engines field in package.json +#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct Engines { + #[serde(default)] + pub browsers: Browsers, + pub electron: Option, + pub node: Option, + pub parcel: Option, +} + +/// List of environment features that may be supported by an engine +pub enum EnvironmentFeature { + ArrowFunctions, + DynamicImport, + Esmodules, + GlobalThis, + ImportMetaUrl, + ServiceWorkerModule, + WorkerModule, +} + +impl EnvironmentFeature { + pub fn engines(&self) -> Engines { + match self { + EnvironmentFeature::WorkerModule => Engines { + browsers: Browsers { + edge: Some(Version::new(NonZeroU16::new(80).unwrap(), 0)), + chrome: Some(Version::new(NonZeroU16::new(80).unwrap(), 0)), + opera: Some(Version::new(NonZeroU16::new(67).unwrap(), 0)), + android: Some(Version::new(NonZeroU16::new(81).unwrap(), 0)), + ..Default::default() + }, + ..Default::default() + }, + EnvironmentFeature::DynamicImport => Engines { + browsers: Browsers { + edge: Some(Version::new(NonZeroU16::new(76).unwrap(), 0)), + firefox: Some(Version::new(NonZeroU16::new(67).unwrap(), 0)), + chrome: Some(Version::new(NonZeroU16::new(63).unwrap(), 0)), + safari: Some(Version::new(NonZeroU16::new(11).unwrap(), 1)), + opera: Some(Version::new(NonZeroU16::new(50).unwrap(), 0)), + ios_saf: Some(Version::new(NonZeroU16::new(11).unwrap(), 3)), + android: Some(Version::new(NonZeroU16::new(63).unwrap(), 0)), + samsung: Some(Version::new(NonZeroU16::new(8).unwrap(), 0)), + ..Default::default() + }, + ..Default::default() + }, + _ => todo!(), + } + } +} + +/// 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] = &[ + "not ie <= 11", + "not edge < 16", + "not firefox < 60", + "not chrome < 61", + "not safari < 11", + "not opera < 48", + "not ios_saf < 11", + "not op_mini all", + "not android < 76", + "not blackberry > 0", + "not op_mob > 0", + "not and_chr < 76", + "not and_ff < 68", + "not ie_mob > 0", + "not and_uc > 0", + "not samsung < 8.2", + "not and_qq > 0", + "not baidu > 0", + "not kaios > 0", +]; + +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()) + }; + + Engines { + browsers: browsers.map(|b| b.into()).unwrap_or_default(), + electron: None, + node: None, + parcel: None, + } + } + + pub fn supports(&self, feature: EnvironmentFeature) -> bool { + let min = feature.engines(); + macro_rules! check { + ($p: ident$(. $x: ident)*) => {{ + if let Some(v) = self.$p$(.$x)* { + match min.$p$(.$x)* { + None => return false, + Some(v2) if v < v2 => return false, + _ => {} + } + } + }}; + } + + check!(browsers.android); + check!(browsers.chrome); + check!(browsers.edge); + check!(browsers.firefox); + check!(browsers.ie); + check!(browsers.ios_saf); + check!(browsers.opera); + check!(browsers.safari); + check!(browsers.samsung); + true + } +} diff --git a/crates/parcel_core/src/types/environment/output_format.rs b/crates/parcel_core/src/types/environment/output_format.rs new file mode 100644 index 00000000000..ee9362660af --- /dev/null +++ b/crates/parcel_core/src/types/environment/output_format.rs @@ -0,0 +1,25 @@ +use serde_repr::Deserialize_repr; +use serde_repr::Serialize_repr; + +/// The JavaScript bundle output format +#[derive(PartialEq, Eq, Clone, Copy, Debug, Deserialize_repr, Hash, Serialize_repr)] +#[repr(u8)] +pub enum OutputFormat { + /// A classic script that can be loaded in a