diff --git a/Cargo.lock b/Cargo.lock index 88c6d014e47..7cee696f3a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,9 +29,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "ast_node" -version = "0.9.6" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e3e06ec6ac7d893a0db7127d91063ad7d9da8988f8a1a256f03729e6eec026" +checksum = "2ab31376d309dd3bfc9cfb3c11c93ce0e0741bbe0354b20e7f8c60b044730b79" dependencies = [ "proc-macro2", "quote", @@ -151,6 +151,15 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64-simd" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5" +dependencies = [ + "simd-abstraction", +] + [[package]] name = "better_scoped_tls" version = "0.1.1" @@ -168,9 +177,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bitvec" @@ -539,9 +548,9 @@ checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" [[package]] name = "either" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "elsa" @@ -670,9 +679,9 @@ checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" [[package]] name = "from_variant" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0b11eeb173ce52f84ebd943d42e58813a2ebb78a6a3ff0a243b71c5199cd7b" +checksum = "fdc9cc75639b041067353b9bce2450d6847e547276c6fbe4487d7407980e07db" dependencies = [ "proc-macro2", "swc_macros_common", @@ -797,7 +806,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.5.0", "ignore", "walkdir", ] @@ -923,7 +932,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2", "tokio", "tower-service", "tracing", @@ -1064,12 +1073,11 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-macro" -version = "0.3.0" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4467ed1321b310c2625c5aa6c1b1ffc5de4d9e42668cf697a08fb033ee8265e" +checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f" dependencies = [ "Inflector", - "pmutil", "proc-macro2", "quote", "syn", @@ -1360,7 +1368,7 @@ version = "2.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da1edd9510299935e4f52a24d1e69ebd224157e3e962c6c847edec5c2e4f786f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.5.0", "ctor", "napi-derive", "napi-sys", @@ -1535,7 +1543,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -1590,6 +1598,12 @@ version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +[[package]] +name = "outref" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" + [[package]] name = "oxipng" version = "8.0.0" @@ -1664,6 +1678,7 @@ dependencies = [ "crossbeam-channel", "dashmap", "getrandom", + "glob", "indexmap 1.9.3", "jemallocator", "libc", @@ -1681,6 +1696,7 @@ dependencies = [ "parcel-macros", "parcel-resolver", "parcel_filesystem", + "parcel_napi_helpers", "rayon", "sentry", "serde", @@ -1847,9 +1863,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2173,7 +2189,7 @@ version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -2547,6 +2563,15 @@ dependencies = [ "digest", ] +[[package]] +name = "simd-abstraction" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987" +dependencies = [ + "outref", +] + [[package]] name = "simd-adler32" version = "0.3.5" @@ -2585,16 +2610,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.6" @@ -2621,6 +2636,25 @@ dependencies = [ "url", ] +[[package]] +name = "sourcemap" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "208d40b9e8cad9f93613778ea295ed8f3c2b1824217c6cfc7219d3f6f45b96d4" +dependencies = [ + "base64-simd", + "bitvec", + "data-encoding", + "debugid", + "if_chain", + "rustc-hash", + "rustc_version 0.2.3", + "serde", + "serde_json", + "unicode-id-start", + "url", +] + [[package]] name = "spin" version = "0.9.8" @@ -2714,9 +2748,9 @@ dependencies = [ [[package]] name = "string_enum" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b650ea2087d32854a0f20b837fc56ec987a1cb4f758c9757e1171ee9812da63" +checksum = "05e383308aebc257e7d7920224fa055c632478d92744eca77f99be8fa1545b90" dependencies = [ "proc-macro2", "quote", @@ -2764,9 +2798,9 @@ dependencies = [ [[package]] name = "swc_common" -version = "0.33.15" +version = "0.33.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3792c10fa5d3e93a705b31f13fdea4a6e68c3c20d4351e84ed1741b7864399cd" +checksum = "a2f9706038906e66f3919028f9f7a37f3ed552f1b85578e93f4468742e2da438" dependencies = [ "ahash", "ast_node", @@ -2780,7 +2814,7 @@ dependencies = [ "rustc-hash", "serde", "siphasher", - "sourcemap", + "sourcemap 8.0.1", "swc_atoms", "swc_eq_ignore_macros", "swc_visit", @@ -2843,7 +2877,7 @@ version = "0.111.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e12b4d0f3b31d293dac16fc13a50f8a282a3bdb658f2a000ffe09b1b638f45c9" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.5.0", "is-macro", "num-bigint", "phf", @@ -2865,7 +2899,7 @@ dependencies = [ "once_cell", "rustc-hash", "serde", - "sourcemap", + "sourcemap 6.4.1", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -3149,7 +3183,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a491da2eaab98914d1f85bd81a35db6432ad0577ae64746bb9e5594cb0b79b47" dependencies = [ "better_scoped_tls", - "bitflags 2.3.3", + "bitflags 2.5.0", "indexmap 2.1.0", "once_cell", "phf", @@ -3369,9 +3403,9 @@ dependencies = [ [[package]] name = "swc_macros_common" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50176cfc1cbc8bb22f41c6fe9d1ec53fbe057001219b5954961b8ad0f336fce9" +checksum = "91745f3561057493d2da768437c427c0e979dff7396507ae02f16c981c4a8466" dependencies = [ "proc-macro2", "quote", @@ -3391,9 +3425,9 @@ dependencies = [ [[package]] name = "swc_visit" -version = "0.5.8" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b27078d8571abe23aa52ef608dd1df89096a37d867cf691cbb4f4c392322b7c9" +checksum = "043d11fe683dcb934583ead49405c0896a5af5face522e4682c16971ef7871b9" dependencies = [ "either", "swc_visit_macros", @@ -3401,12 +3435,11 @@ dependencies = [ [[package]] name = "swc_visit_macros" -version = "0.5.9" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8bb05975506741555ea4d10c3a3bdb0e2357cd58e1a4a4332b8ebb4b44c34d" +checksum = "4ae9ef18ff8daffa999f729db056d2821cd2f790f3a11e46422d19f46bb193e7" dependencies = [ "Inflector", - "pmutil", "proc-macro2", "quote", "swc_macros_common", @@ -3568,17 +3601,16 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.29.1" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "pin-project-lite", - "socket2 0.4.10", + "socket2", "windows-sys 0.48.0", ] @@ -3736,6 +3768,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d70b6494226b36008c8366c288d77190b3fad2eb4c10533139c1c1f461127f1a" +[[package]] +name = "unicode-id-start" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f73150333cb58412db36f2aca8f2875b013049705cc77b94ded70a1ab1f5da" + [[package]] name = "unicode-ident" version = "1.0.5" @@ -4186,18 +4224,18 @@ checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" [[package]] name = "zerocopy" -version = "0.7.25" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.25" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", diff --git a/crates/node-bindings/Cargo.toml b/crates/node-bindings/Cargo.toml index ad8cb0c2e38..8424d49af7f 100644 --- a/crates/node-bindings/Cargo.toml +++ b/crates/node-bindings/Cargo.toml @@ -18,6 +18,7 @@ napi-derive = "2.16.3" parcel-js-swc-core = { path = "../../packages/transformers/js/core" } parcel-resolver = { path = "../../packages/utils/node-resolver-rs" } parcel_filesystem = { path = "../parcel_filesystem" } +parcel_napi_helpers = { path = "../parcel_napi_helpers" } dashmap = "5.4.0" xxhash-rust = { version = "0.8.2", features = ["xxh3"] } log = "0.4.21" @@ -26,6 +27,7 @@ sentry = { version = "0.32.2", optional = true, default-features = false, featur once_cell = { version = "1.19.0", optional = true } whoami = { version = "1.5.1", optional = true } +glob = "0.3.1" serde = "1.0.198" serde_json = "1.0.116" toml = "0.8.12" diff --git a/crates/node-bindings/src/core/js_requests/config_request/mod.rs b/crates/node-bindings/src/core/js_requests/config_request/mod.rs new file mode 100644 index 00000000000..669f2e4b053 --- /dev/null +++ b/crates/node-bindings/src/core/js_requests/config_request/mod.rs @@ -0,0 +1,34 @@ +use std::rc::Rc; + +use napi::Env; +use napi::JsObject; +use napi_derive::napi; + +use crate::core::js_requests::request_options::input_fs_from_options; +use crate::core::js_requests::request_options::project_root_from_options; +use crate::core::requests::config_request::run_config_request; +use crate::core::requests::config_request::ConfigRequest; +use crate::core::requests::request_api::js_request_api::JSRequestApi; + +/// JavaScript API for running a config request. +/// At the moment the request fields themselves will be copied on call. +/// +/// This is not efficient but can be worked around when it becomes an issue. +/// +/// This should have exhaustive unit-tests on `packages/core/core/test/requests/ConfigRequest.test.js`. +#[napi] +fn napi_run_config_request( + env: Env, + config_request: ConfigRequest, + api: JsObject, + options: JsObject, +) -> napi::Result<()> { + // Technically we could move `env` to JSRequestAPI but in order to + // be able to use env on more places we rc it. + let env = Rc::new(env); + let api = JSRequestApi::new(env.clone(), api); + let input_fs = input_fs_from_options(env, &options)?; + let project_root = project_root_from_options(&options)?; + + run_config_request(&config_request, &api, &input_fs, &project_root) +} diff --git a/crates/node-bindings/src/core/js_requests/entry_request/mod.rs b/crates/node-bindings/src/core/js_requests/entry_request/mod.rs new file mode 100644 index 00000000000..ef1b5c152e6 --- /dev/null +++ b/crates/node-bindings/src/core/js_requests/entry_request/mod.rs @@ -0,0 +1,34 @@ +use std::rc::Rc; + +use napi::Env; +use napi::JsObject; +use napi_derive::napi; +use parcel_napi_helpers::anyhow_napi; + +use crate::core::js_requests::request_options::input_fs_from_options; +use crate::core::requests::entry_request::run_entry_request; +use crate::core::requests::entry_request::EntryRequestInput; +use crate::core::requests::entry_request::EntryResult; +use crate::core::requests::entry_request::RunEntryRequestParams; +use crate::core::requests::request_api::js_request_api::JSRequestApi; + +/// napi entry-point for `run_entry_request`. +#[napi] +fn napi_run_entry_request( + env: Env, + entry_request: EntryRequestInput, + api: JsObject, + options: JsObject, +) -> napi::Result { + let env = Rc::new(env); + let api = JSRequestApi::new(env.clone(), api); + let input_fs = input_fs_from_options(env, &options)?; + let result = run_entry_request(RunEntryRequestParams { + run_api: &api, + fs: &input_fs, + input: &entry_request, + }) + .map_err(anyhow_napi)?; + + Ok(result) +} diff --git a/crates/node-bindings/src/core/js_requests/mod.rs b/crates/node-bindings/src/core/js_requests/mod.rs new file mode 100644 index 00000000000..dd6c18d9314 --- /dev/null +++ b/crates/node-bindings/src/core/js_requests/mod.rs @@ -0,0 +1,3 @@ +pub mod config_request; +pub mod entry_request; +mod request_options; diff --git a/crates/node-bindings/src/core/js_requests/request_options.rs b/crates/node-bindings/src/core/js_requests/request_options.rs new file mode 100644 index 00000000000..1b408abac42 --- /dev/null +++ b/crates/node-bindings/src/core/js_requests/request_options.rs @@ -0,0 +1,43 @@ +//! Provides helpers to cast from a `JsObject` options object into a few common +//! options +//! +//! This corresponds to the `RequestOptions` javascript type. +//! +//! The options read are `options.inputFS` and `options.projectRoot`. +//! +//! This is either a no-copy (for inputFS) or a copy on read operation +//! (for projectRoot). +use std::rc::Rc; + +use napi::Env; +use napi::JsObject; +use napi::JsString; +use parcel_filesystem::js_delegate_file_system::JSDelegateFileSystem; + +pub fn project_root_from_options(options: &JsObject) -> napi::Result { + let Some(project_root): Option = options.get("projectRoot")? else { + return Err(napi::Error::from_reason( + "[napi] Missing required projectRoot options field", + )); + }; + let project_root = project_root.into_utf8()?; + let project_root = project_root.as_str()?; + Ok(project_root.to_string()) +} + +pub fn input_fs_from_options( + env: Rc, + options: &JsObject, +) -> napi::Result { + let Some(input_fs) = options + .get("inputFS")? + .map(|input_fs| JSDelegateFileSystem::new(env, input_fs)) + else { + // We need to make the `FileSystem` trait object-safe, so we can use dynamic + // dispatch. + return Err(napi::Error::from_reason( + "[napi] Missing required inputFS options field", + )); + }; + Ok(input_fs) +} diff --git a/crates/node-bindings/src/core/mod.rs b/crates/node-bindings/src/core/mod.rs index 9a926cd401d..74a9650e47f 100644 --- a/crates/node-bindings/src/core/mod.rs +++ b/crates/node-bindings/src/core/mod.rs @@ -1,4 +1,8 @@ //! Core re-implementation in Rust +/// napi versions of `crate::core::requests` +mod js_requests; +/// New-type for paths relative to a project-root +mod project_path; /// Request types and run functions mod requests; diff --git a/crates/node-bindings/src/core/project_path.rs b/crates/node-bindings/src/core/project_path.rs new file mode 100644 index 00000000000..588c0995014 --- /dev/null +++ b/crates/node-bindings/src/core/project_path.rs @@ -0,0 +1,76 @@ +use std::path::Path; +use std::path::PathBuf; + +use napi::bindgen_prelude::FromNapiValue; +use napi::bindgen_prelude::ToNapiValue; +use napi::sys::napi_env; +use napi::sys::napi_value; + +/// Similar to opaque type ProjectPath in Rust. +/// +/// The main purpose of this new-type is to allow us to return PathBuf from rust and have +/// napi auto convert it to string - for now. +/// +/// For JavaScript this is just a String +/// +/// ## Example +/// +/// Use `ProjectPath` on your input type: +/// +/// ``` +/// #[napi(object)] +/// struct MyRequestInput { +/// path: ProjectPath, +/// } +/// +/// #[napi] +/// fn napi_run_my_request(input: MyRequestInput) {/* ... */} +/// ``` +/// +/// Then consume it from javascript +/// ```skip +/// napiRunMyRequest({ path: '/path/to/project' }); +/// ``` +/// +#[derive(Debug, Clone, PartialEq)] +pub struct ProjectPath { + path: PathBuf, +} + +impl From<&str> for ProjectPath { + fn from(path: &str) -> Self { + Self { + path: PathBuf::from(path), + } + } +} + +impl From for ProjectPath { + fn from(path: PathBuf) -> Self { + Self { path } + } +} + +impl AsRef for ProjectPath { + fn as_ref(&self) -> &Path { + &self.path + } +} + +impl ToNapiValue for ProjectPath { + unsafe fn to_napi_value(env: napi_env, val: Self) -> napi::Result { + let path_str = val + .path + .to_str() + .ok_or_else(|| napi::Error::from_reason("Invalid path can't be converted into JS string"))?; + + ToNapiValue::to_napi_value(env, path_str) + } +} + +impl FromNapiValue for ProjectPath { + unsafe fn from_napi_value(env: napi_env, napi_val: napi_value) -> napi::Result { + let path_str: &str = FromNapiValue::from_napi_value(env, napi_val)?; + Ok(ProjectPath::from(PathBuf::from(path_str))) + } +} diff --git a/crates/node-bindings/src/core/requests/config_request/mod.rs b/crates/node-bindings/src/core/requests/config_request/mod.rs index 7370cb39c0f..82365331a40 100644 --- a/crates/node-bindings/src/core/requests/config_request/mod.rs +++ b/crates/node-bindings/src/core/requests/config_request/mod.rs @@ -4,13 +4,12 @@ //! file. use std::path::Path; +use crate::core::project_path::ProjectPath; use napi_derive::napi; use parcel_resolver::FileSystem; use crate::core::requests::request_api::RequestApi; -pub type ProjectPath = String; - pub type InternalGlob = String; #[napi(object)] @@ -20,7 +19,7 @@ pub struct ConfigKeyChange { } #[napi(object)] -#[derive(Clone, PartialEq)] +#[derive(Default, Clone, PartialEq)] pub struct InternalFileCreateInvalidation { // file pub file_path: Option, @@ -81,7 +80,7 @@ fn get_config_key_content_hash( config_key: &str, input_fs: &impl FileSystem, project_root: &str, - file_path: &str, + file_path: &Path, ) -> napi::Result { let mut path = Path::new(project_root).to_path_buf(); path.push(file_path); @@ -108,7 +107,7 @@ pub fn run_config_request( project_root: &str, ) -> napi::Result<()> { for file_path in &config_request.invalidate_on_file_change { - let file_path = Path::new(file_path); + let file_path = file_path.as_ref(); api.invalidate_on_file_update(file_path)?; api.invalidate_on_file_delete(file_path)?; } @@ -118,10 +117,10 @@ pub fn run_config_request( &config_key_change.config_key, input_fs, &project_root, - &config_key_change.file_path, + config_key_change.file_path.as_ref(), )?; api.invalidate_on_config_key_change( - Path::new(&config_key_change.file_path), + config_key_change.file_path.as_ref(), &config_key_change.config_key, &content_hash, )?; @@ -155,12 +154,11 @@ struct RequestOptions {} #[cfg(test)] mod test { - use parcel_filesystem::in_memory_file_system::InMemoryFileSystem; - use parcel_filesystem::os_file_system::OsFileSystem; - use super::*; use crate::core::requests::config_request::run_config_request; use crate::core::requests::request_api::MockRequestApi; + use parcel_filesystem::in_memory_file_system::InMemoryFileSystem; + use parcel_filesystem::os_file_system::OsFileSystem; #[test] fn test_run_empty_config_request_does_nothing() { @@ -186,7 +184,7 @@ mod test { fn test_run_config_request_with_invalidate_on_file_change() { let config_request = ConfigRequest { id: "".to_string(), - invalidate_on_file_change: vec!["path1".to_string(), "path2".to_string()], + invalidate_on_file_change: vec!["path1".into(), "path2".into()], invalidate_on_config_key_change: vec![], invalidate_on_file_create: vec![], invalidate_on_env_change: vec![], @@ -220,7 +218,7 @@ mod test { invalidate_on_file_change: vec![], invalidate_on_config_key_change: vec![], invalidate_on_file_create: vec![InternalFileCreateInvalidation { - file_path: Some("path1".to_string()), + file_path: Some("path1".into()), glob: None, file_name: None, above_file_path: None, @@ -240,7 +238,7 @@ mod test { .times(1) .withf(|p| { *p == InternalFileCreateInvalidation { - file_path: Some("path1".to_string()), + file_path: Some("path1".into()), glob: None, file_name: None, above_file_path: None, diff --git a/crates/node-bindings/src/core/requests/entry_request/mod.rs b/crates/node-bindings/src/core/requests/entry_request/mod.rs new file mode 100644 index 00000000000..e5b1ba3fe71 --- /dev/null +++ b/crates/node-bindings/src/core/requests/entry_request/mod.rs @@ -0,0 +1,237 @@ +//! Entry request corresponds to `EntryRequest.js` and is used to resolve entry points. +//! +//! Effectively when we get an "entry"; we try to find it as a file, project +//! directory or glob, then resolve files +use std::path::Path; + +use anyhow::anyhow; +use napi_derive::napi; +use parcel_resolver::FileSystem; + +use crate::core::project_path::ProjectPath; +use crate::core::requests::config_request::InternalFileCreateInvalidation; +use crate::core::requests::request_api::RequestApi; + +#[napi(object)] +#[derive(Debug, Clone, PartialEq)] +pub struct Entry { + pub file_path: ProjectPath, + pub package_path: ProjectPath, +} + +#[napi(object)] +#[derive(Debug, Default, PartialEq)] +pub struct EntryResult { + pub entries: Vec, + pub files: Vec, + pub globs: Vec, +} + +fn merge_results(target: &mut EntryResult, source: EntryResult) { + target.entries.extend(source.entries); + target.files.extend(source.files); + target.globs.extend(source.globs); +} + +/// This function should check if a string is a glob pattern. +/// +/// TODO: This is not a sufficient implementation +fn is_glob(path: &Path) -> bool { + path.to_str().unwrap().contains("*") +} + +/// Params object for resolve functions +struct ResolveEntryParams<'a, FS: FileSystem> { + path: &'a Path, + fs: &'a FS, + project_root: &'a Path, +} + +/// Resolve an entry-point +fn resolve_entry( + ResolveEntryParams { + path, + fs, + project_root, + }: ResolveEntryParams, +) -> anyhow::Result { + if is_glob(path) { + resolve_entry_glob(ResolveEntryParams { + path, + fs, + project_root, + }) + } else if fs.is_file(path) { + resolve_entry_file(ResolveEntryParams { + path, + fs, + project_root, + }) + } else if fs.is_dir(path) { + todo!("directory entries are not implemented") + } else { + Err(anyhow!("[napi] Invalid entry, file not found")) + } +} + +/// Resolve an entry-point that is a glob by expanding the glob then resolving each of its matches. +fn resolve_entry_glob( + ResolveEntryParams { + path, + fs, + project_root, + }: ResolveEntryParams, +) -> anyhow::Result { + let pattern = path.to_str().unwrap(); + let results = glob::glob(pattern)?; + let mut result = EntryResult::default(); + for path in results { + let path = path?; + merge_results( + &mut result, + resolve_entry(ResolveEntryParams { + path: &path, + fs, + project_root, + })?, + ); + } + Ok(result) +} + +/// Resolve an entrypoint that is a file +fn resolve_entry_file( + ResolveEntryParams { + path, + fs, + project_root, + }: ResolveEntryParams, +) -> anyhow::Result { + let project_root = fs.canonicalize_base(project_root)?; + let path = fs.canonicalize_base(path)?; + let cwd = fs.cwd()?; + // TODO: What is this for???? Why do we ignore project root depending on the CWD at this level? + // Probably this is not the right place to handle this feature. Note that this is all this code + // does so if this was handled at a CLI level we don't need any of this. + let package_path = if project_root.starts_with(&cwd) { + cwd + } else { + project_root + }; + + Ok(EntryResult { + entries: vec![Entry { + file_path: path.into(), + package_path: package_path.into(), + }], + ..Default::default() + }) +} + +#[napi(object)] +pub struct EntryRequestInput { + pub project_path: String, +} + +pub struct RunEntryRequestParams<'a, RA: RequestApi, FS: FileSystem> { + pub run_api: &'a RA, + pub fs: &'a FS, + pub input: &'a EntryRequestInput, +} + +/// Run entry-request. Corresponds to `EntryRequest.js`. +pub fn run_entry_request( + RunEntryRequestParams { run_api, fs, input }: RunEntryRequestParams< + impl RequestApi, + impl FileSystem, + >, +) -> anyhow::Result { + let result = resolve_entry(ResolveEntryParams { + path: Path::new(&input.project_path), + fs, + project_root: Path::new(&input.project_path), + })?; + + for file in &result.files { + run_api.invalidate_on_file_update(file.as_ref())?; + run_api.invalidate_on_file_delete(file.as_ref())?; + } + + for glob in &result.globs { + run_api.invalidate_on_file_create(&InternalFileCreateInvalidation { + glob: Some(glob.clone()), + ..Default::default() + })?; + } + + for entry in &result.entries { + run_api.invalidate_on_file_delete(entry.file_path.as_ref())?; + } + + Ok(result) +} + +#[cfg(test)] +mod test { + use super::*; + use parcel_filesystem::in_memory_file_system::InMemoryFileSystem; + + #[test] + fn test_merge_results() { + let entry1 = Entry { + file_path: ProjectPath::from("file1"), + package_path: ProjectPath::from("package1"), + }; + let file2 = ProjectPath::from("file2"); + let glob1 = "glob1".to_string(); + let mut result1 = EntryResult { + entries: vec![entry1.clone()], + files: vec![file2.clone()], + globs: vec![glob1.clone()], + }; + let entry2 = Entry { + file_path: ProjectPath::from("file3"), + package_path: ProjectPath::from("package2"), + }; + let file4 = ProjectPath::from("file4"); + let glob2 = "glob2".to_string(); + let result2 = EntryResult { + entries: vec![entry2.clone()], + files: vec![file4.clone()], + globs: vec![glob2.clone()], + }; + + merge_results(&mut result1, result2); + assert_eq!( + result1, + EntryResult { + entries: vec![entry1, entry2,], + files: vec![file2, file4,], + globs: vec![glob1, glob2,], + } + ); + } + + #[test] + fn test_resolve_entry_file() { + let mut fs = InMemoryFileSystem::default(); + fs.set_current_working_directory("/project".into()); + let project_root = Path::new("/project"); + let path = Path::new("/project/file"); + let result = resolve_entry_file(ResolveEntryParams { + path, + fs: &fs, + project_root, + }); + assert_eq!( + result.unwrap(), + EntryResult { + entries: vec![Entry { + file_path: ProjectPath::from("/project/file"), + package_path: ProjectPath::from("/project"), + }], + ..Default::default() + } + ); + } +} diff --git a/crates/node-bindings/src/core/requests/mod.rs b/crates/node-bindings/src/core/requests/mod.rs index 2454c2fa031..ece5fbea573 100644 --- a/crates/node-bindings/src/core/requests/mod.rs +++ b/crates/node-bindings/src/core/requests/mod.rs @@ -1,111 +1,3 @@ -use std::rc::Rc; - -use napi::bindgen_prelude::FromNapiValue; -use napi::Env; -use napi::JsFunction; -use napi::JsObject; -use napi::JsString; -use napi::JsUnknown; -use napi::NapiRaw; -use napi_derive::napi; - -use crate::core::requests::config_request::ConfigRequest; -use crate::core::requests::request_api::js_request_api::JSRequestApi; -use parcel_filesystem::js_delegate_file_system::JSDelegateFileSystem; - -mod config_request; -mod request_api; - -/// Get an object field as a JSFunction. Will error out if the field is not present or isn't an -/// instance of the global `"Function"`. -/// -/// ## Safety -/// Uses raw NAPI casts, but checks that object field is a function -pub fn get_function(env: &Env, js_object: &JsObject, field_name: &str) -> napi::Result { - let Some(method): Option = js_object.get(field_name)? else { - return Err(napi::Error::from_reason(format!( - "[napi] Method not found: {}", - field_name - ))); - }; - let function_class: JsUnknown = env.get_global()?.get_named_property("Function")?; - let is_function = method.instanceof(function_class)?; - if !is_function { - return Err(napi::Error::from_reason(format!( - "[napi] Method is not a function: {}", - field_name - ))); - } - - let method_fn = unsafe { JsFunction::from_napi_value(env.raw(), method.raw()) }?; - Ok(method_fn) -} - -/// Call a method on an object with a set of arguments. -/// -/// Will error out if the method doesn't exist or if the field is not a function. -/// -/// This does some redundant work ; so you may want to call `get_function` -/// directly if calling a method on a loop. -/// -/// The function takes `JsUnknown` references so any type can be used as an -/// argument. -/// -/// ## Safety -/// Uses raw NAPI casts, but checks that object field is a function -/// -/// ## Example -/// ```skip -/// let string_parameter = env.create_string(path.to_str().unwrap())?; -/// let args = [&string_parameter.into_unknown()]; -/// let field_name = "method"; -/// -/// call_method(&self.env, &js_object, field_name, &args)?; -/// ``` -pub fn call_method( - env: &Env, - js_object: &JsObject, - field_name: &str, - args: &[&JsUnknown], -) -> napi::Result { - let method_fn = get_function(env, js_object, field_name)?; - let result = method_fn.call(Some(&js_object), &args)?; - Ok(result) -} - -/// JavaScript API for running a config request. -/// At the moment the request fields themselves will be copied on call. -/// -/// This is not efficient but can be worked around when it becomes an issue. -/// -/// This should have exhaustive unit-tests on `packages/core/core/test/requests/ConfigRequest.test.js`. -#[napi] -fn napi_run_config_request( - env: Env, - config_request: ConfigRequest, - api: JsObject, - options: JsObject, -) -> napi::Result<()> { - // Technically we could move `env` to JSRequestAPI but in order to - // be able to use env on more places we rc it. - let env = Rc::new(env); - let api = JSRequestApi::new(env.clone(), api); - let input_fs = options.get("inputFS")?; - let Some(input_fs) = input_fs.map(|input_fs| JSDelegateFileSystem::new(env, input_fs)) else { - // We need to make the `FileSystem` trait object-safe so we can use dynamic - // dispatch. - return Err(napi::Error::from_reason( - "[napi] Missing required inputFS options field", - )); - }; - let Some(project_root): Option = options.get("projectRoot")? else { - return Err(napi::Error::from_reason( - "[napi] Missing required projectRoot options field", - )); - }; - // TODO: what if the string is UTF16 or latin? - let project_root = project_root.into_utf8()?; - let project_root = project_root.as_str()?; - - config_request::run_config_request(&config_request, &api, &input_fs, project_root) -} +pub mod config_request; +pub mod entry_request; +pub mod request_api; diff --git a/crates/node-bindings/src/core/requests/request_api/js_request_api.rs b/crates/node-bindings/src/core/requests/request_api/js_request_api.rs index a6a76356c7e..cd015ffa76b 100644 --- a/crates/node-bindings/src/core/requests/request_api/js_request_api.rs +++ b/crates/node-bindings/src/core/requests/request_api/js_request_api.rs @@ -4,12 +4,16 @@ use std::rc::Rc; use napi::Env; use napi::JsObject; use napi::JsUnknown; +use parcel_napi_helpers::call_method; -use crate::core::requests::call_method; use crate::core::requests::config_request::InternalFileCreateInvalidation; use crate::core::requests::request_api::RequestApi; use crate::core::requests::request_api::RequestApiResult; +/// This is a "delegate" implementation of `RequestApi` that delegates calls to a +/// JavaScript object. +/// +/// It has exhaustive tests on `core/test/requests/ConfigRequest.test.js`. pub struct JSRequestApi { // TODO: Make sure it is safe to hold the environment like this env: Rc,