diff --git a/.gitignore b/.gitignore index 324c57f..4a17ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +.jpm/ target/ **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index f64b70e..bf9bd39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,17 +84,60 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "8c6f84b74db2535ebae81eede2f39b947dcbf01d093ae5f791e5dd414a1bf289" [[package]] name = "arrayvec" @@ -456,8 +499,10 @@ version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" dependencies = [ + "anstream", "anstyle", "clap_lex 0.5.0", + "strsim", "terminal_size 0.2.6", ] @@ -500,6 +545,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "console" version = "0.15.7" @@ -1185,7 +1236,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -1399,11 +1450,13 @@ dependencies = [ "jpm_common", "jpm_compiler", "jpm_package", + "jpm_workspace", "miette 5.10.0", "mimalloc", "starbase", "starbase_styles", "tokio", + "tracing", ] [[package]] @@ -1442,6 +1495,10 @@ dependencies = [ "tracing", ] +[[package]] +name = "jpm_lockfile" +version = "0.1.0" + [[package]] name = "jpm_manifest" version = "0.1.0" @@ -1474,6 +1531,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "jpm_workspace" +version = "0.1.0" +dependencies = [ + "jpm_common", + "jpm_lockfile", + "jpm_manifest", + "jpm_package", + "miette 5.10.0", + "once_cell", + "petgraph", + "starbase", + "starbase_sandbox", + "starbase_styles", + "starbase_utils", + "thiserror", + "tracing", +] + [[package]] name = "js-sys" version = "0.3.64" @@ -1970,9 +2046,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -2524,9 +2600,9 @@ dependencies = [ [[package]] name = "schematic" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47119c16c048894c2003db42c9ec28cd502ba29155a813d46c68ea82f0847ec6" +checksum = "c9a4dae95c5191faf0e295682fc79b05fdae0d7896e307ad462110ea218d4709" dependencies = [ "garde", "indexmap 2.0.0", @@ -2544,9 +2620,9 @@ dependencies = [ [[package]] name = "schematic_macros" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d830e5344f49890b62354565ad355f77af511c298eadd70cfd96d7f077820252" +checksum = "8c08b6bc89333f39160933804c3eeb3c458b42c5bd2895e0e5f051c4d5ac0551" dependencies = [ "convert_case", "darling", @@ -2557,9 +2633,13 @@ dependencies = [ [[package]] name = "schematic_types" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e76f3ad9eccc2a3f57dc0d27c20f04f8e3911b789f31eb2559c7a3597a10e6c" +checksum = "a7f109b11cf481e14d14aedcc8a9525031280963b9280b4d537476d28949f7cc" +dependencies = [ + "relative-path", + "url", +] [[package]] name = "scoped-tls" @@ -2781,6 +2861,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "sourcemap" version = "6.2.3" @@ -2916,9 +3006,9 @@ dependencies = [ [[package]] name = "starbase_utils" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bdab61b7ab5c4185c6aaa6f62464a62bd792b9979f6e48f2855e324dbe334f2" +checksum = "1018714a63bfd48b31085fbf71f2c69b039b5cf0e140594ca8ff2088cbf697ca" dependencies = [ "dirs", "miette 5.10.0", @@ -3063,9 +3153,9 @@ dependencies = [ [[package]] name = "swc" -version = "0.264.46" +version = "0.264.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb2abc1bec52fbc408d89a5a474bde798cb959e14e4fd246e301e3e1e1d6eda" +checksum = "3640a381deb436cd4663b42cbfb4acacbdbbad243372b18004c784d96f8fc1d9" dependencies = [ "ahash 0.8.3", "anyhow", @@ -3200,9 +3290,9 @@ dependencies = [ [[package]] name = "swc_core" -version = "0.79.49" +version = "0.79.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9feeb54bc406f6c0fc226c7535ac76c85eaa674ed52545efefe6853764bb7d3" +checksum = "478993899f8721fb3d4ea171baf06ac934fbfadc61e6dbc6c66d3f62b01ce8d2" dependencies = [ "once_cell", "swc_atoms", @@ -3220,9 +3310,9 @@ dependencies = [ [[package]] name = "swc_ecma_ast" -version = "0.107.3" +version = "0.107.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce6ba552db5098e9e7b5e7fcc30ef1e9f2b33ec930a94489288e21f8e95b213" +checksum = "ae06f8db55b8920aa5b5362216ee8319a1edb131412fe06090892bbc99cc1237" dependencies = [ "bitflags 2.3.3", "bytecheck", @@ -3239,9 +3329,9 @@ dependencies = [ [[package]] name = "swc_ecma_codegen" -version = "0.142.9" +version = "0.142.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b9c1920bee89a90cfd100d5c2d22e0557760a86cc00726935209635301b482" +checksum = "464b5760b58ae12de4859802efb3ead96a740c06a96b2bf9659367790f2374fd" dependencies = [ "memchr", "num-bigint", @@ -3271,9 +3361,9 @@ dependencies = [ [[package]] name = "swc_ecma_ext_transforms" -version = "0.106.11" +version = "0.106.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b6986d4b99d8cf7ee77f06014e0061d2ba542fb6bfd24bd469bc865b9c572b" +checksum = "39ab8f980ccc923b9c17ddae8080b1d962bf66e5920dcbe6d439f572f96e797b" dependencies = [ "phf", "swc_atoms", @@ -3285,9 +3375,9 @@ dependencies = [ [[package]] name = "swc_ecma_lints" -version = "0.85.14" +version = "0.85.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf49c1ffa05de504255a398412036805c28b7dca62f76ff4d5a5fb762258f93" +checksum = "cd923958e5478225b810bf9e5c70abd67c3d88f164f43eb85b2b3fd2d81d9cc4" dependencies = [ "ahash 0.8.3", "auto_impl", @@ -3328,9 +3418,9 @@ dependencies = [ [[package]] name = "swc_ecma_minifier" -version = "0.184.38" +version = "0.184.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fbfc08cb59808a45b489d970af1d7c930761e7249bae54250bd0e1f6291478" +checksum = "733d3780f23f746895960efcc78d6648b8d0d599c442fbc893c6cc12668b0b1b" dependencies = [ "ahash 0.8.3", "arrayvec", @@ -3363,9 +3453,9 @@ dependencies = [ [[package]] name = "swc_ecma_parser" -version = "0.137.8" +version = "0.137.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eaccdcfd630dfa46e90d2b9ea20260e20ad275361b747ad8c9b93300470627" +checksum = "c1f80d1df900264b5187fcf03aa56ccf11924e428421f559e818d2639b087d09" dependencies = [ "either", "num-bigint", @@ -3383,9 +3473,9 @@ dependencies = [ [[package]] name = "swc_ecma_preset_env" -version = "0.198.27" +version = "0.198.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533d5e317fe62b22951825d64e804f10ccfba70ce017d3412595cf211e375d90" +checksum = "2e2b73b05ea9dfbbbc8231a8c16a25f9a6b73d4e2c69afb21bda5a35e1d269e4" dependencies = [ "ahash 0.8.3", "anyhow", @@ -3422,9 +3512,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms" -version = "0.221.25" +version = "0.221.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37070c0154dea4be194d6140f6cc02ead6b771b6d1fe7c62b5e8815d1d4c6093" +checksum = "d2e33b7da6d3f98189b567c9b94b0626fed228fca7176a30134d12da4cabeda1" dependencies = [ "swc_atoms", "swc_common", @@ -3442,9 +3532,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_base" -version = "0.130.14" +version = "0.130.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f477cba9333b082dc67c66fff068decf3678c572b55175678bc57fde582af72f" +checksum = "43664f6ba532c7e41b552c878ecd7d28735904f24ca64f06abf6e20380821f9c" dependencies = [ "better_scoped_tls", "bitflags 2.3.3", @@ -3465,9 +3555,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_classes" -version = "0.119.14" +version = "0.119.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9d19ee27d7b4d528420b93ec7364dd3f6081fd0f675d181bb76a204788cd2d" +checksum = "587685226834c8fe16a390d06e67ee2a6d5fc798a412416482b805b75887695d" dependencies = [ "swc_atoms", "swc_common", @@ -3479,9 +3569,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_compat" -version = "0.156.20" +version = "0.156.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46ce9506a94efad6c2c945701cfdfbce261fdbb91d6f5d7a26ee6d78955f88c4" +checksum = "5082d4d782cb3336c6a1b7478b9db046a38863fe8084d75ccef6deda05ef2152" dependencies = [ "ahash 0.8.3", "arrayvec", @@ -3518,9 +3608,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_module" -version = "0.173.23" +version = "0.173.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39717a981938ad1790fe5702f656a45760809cd36c74eeaaecbc53fb3071b8c" +checksum = "9209f82a9b171a106de5c8a372bf7a0f39e7be8208f345322d755530d8b78762" dependencies = [ "Inflector", "ahash 0.8.3", @@ -3546,9 +3636,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_optimization" -version = "0.190.25" +version = "0.190.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d53fbea5d8d9e04985de298290ffcb9d74ef55bb3b53a81186aec291e96c31" +checksum = "9e0731c1ea271f436f311167c98a600a86e60e814df21bdb830d0a0ee6545962" dependencies = [ "ahash 0.8.3", "dashmap", @@ -3571,9 +3661,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_proposal" -version = "0.164.20" +version = "0.164.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e347becf9bb6811e98459bbc666d2b3b746b631d19793704e9d5a4b5f2d0d8d1" +checksum = "4f8ac16e9c0e6d23d948946f06e1ce4e323ad8d10fc92c061414e785a55ac079" dependencies = [ "either", "rustc-hash", @@ -3591,9 +3681,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_react" -version = "0.176.23" +version = "0.176.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a832b8388cea63687cbc4d5c85d5953738d85d82e9988a07bd7471e2ea75107f" +checksum = "99406fef0faa742319e8ca6779d1163857b4c04826bdba98840428b5627261c3" dependencies = [ "ahash 0.8.3", "base64 0.13.1", @@ -3616,9 +3706,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_testing" -version = "0.133.14" +version = "0.133.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c58533aa8ed009af31adf1fbcd4b4cfb437721122e1c2a4505bede26eb0cc50" +checksum = "a0117f13ae2a39c704385429fc53bd14f535caa9311416be2a619de807d8a24c" dependencies = [ "ansi_term", "anyhow", @@ -3642,9 +3732,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_typescript" -version = "0.180.24" +version = "0.180.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1094987474bf889747bafe4951f05bdc58493c9402d15b3b899578fb481e99ef" +checksum = "56f835b69cef35ef88afe5cb296e911f252cf0a5b8df4abb7de09eb30acd6664" dependencies = [ "serde", "swc_atoms", @@ -3658,9 +3748,9 @@ dependencies = [ [[package]] name = "swc_ecma_usage_analyzer" -version = "0.16.15" +version = "0.16.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e4d2948720c9aa83915e01dd49f69837bd13a3eb60965a1ed5ba4e97555c" +checksum = "1148f22869265abb7d492aa11829b7d240170b2c0d387db1feba72d8e61e5128" dependencies = [ "ahash 0.8.3", "indexmap 1.9.3", @@ -3676,9 +3766,9 @@ dependencies = [ [[package]] name = "swc_ecma_utils" -version = "0.120.11" +version = "0.120.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d757778f6789a54bf2739007057af2917a6ac3b1d686fb06b56b88feda24e4bc" +checksum = "43549d81f9dda15afce48d81a0b2c3191fe462da00560abdfa72e004e261f903" dependencies = [ "indexmap 1.9.3", "num_cpus", @@ -3694,9 +3784,9 @@ dependencies = [ [[package]] name = "swc_ecma_visit" -version = "0.93.3" +version = "0.93.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b701da2f2b16308865e2ab18db13df60cdf563b243430ca99e8ad6f8b760d83a" +checksum = "8fb77e49a630356fe2ba77db102294fc0bbe1050e85c962f5b4aa3de06c4d51a" dependencies = [ "num-bigint", "swc_atoms", @@ -3789,9 +3879,9 @@ dependencies = [ [[package]] name = "swc_plugin_proxy" -version = "0.36.3" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61b44c091eed6107b5d5970edb63acbe872e885495aae90de8450672d4b9868" +checksum = "995382d2e4a75e813e6c023d94cc70ef36e1cfe8a8dc3330c295550a439b52e4" dependencies = [ "better_scoped_tls", "rkyv", @@ -4060,11 +4150,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", @@ -4073,7 +4162,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -4328,6 +4417,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 1076bbd..ff87f6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,12 +3,16 @@ resolver = "2" members = ["crates/*"] [workspace.dependencies] -clap = { version = "4.3.21", default-features = false, features = ["std"] } +clap = { version = "4.3.21" } miette = "5.10.0" once_cell = "1.18.0" +once_map = "0.4.8" +petgraph = "0.6.3" relative-path = { version = "1.8.0", features = ["serde"] } -schematic = { version = "0.11.1", default-features = false, features = [ +schematic = { version = "0.11.2", default-features = false, features = [ "toml", + "type_relative_path", + "type_url", "valid_url", ] } semver = "1.0.18" @@ -16,10 +20,10 @@ serde = "1.0.183" starbase = "0.2.0" starbase_sandbox = { version = "0.1.8" } starbase_styles = "0.1.12" -starbase_utils = { version = "0.2.17", default-features = false, features = [ +starbase_utils = { version = "0.2.18", default-features = false, features = [ "glob", ] } thiserror = "1.0.44" -tokio = { version = "1.29.1", features = ["full", "tracing"] } +tokio = { version = "1.31.0", features = ["full", "tracing"] } tracing = "0.1.37" url = { version = "2.4.0", features = ["serde"] } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index c882e2e..efb70f4 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -12,9 +12,11 @@ path = "src/main.rs" jpm_common = { path = "../common" } jpm_compiler = { path = "../compiler" } jpm_package = { path = "../package" } +jpm_workspace = { path = "../workspace" } clap = { workspace = true, features = ["derive", "env", "wrap_help"] } miette = { workspace = true } mimalloc = { version = "0.1.37", default-features = false } starbase = { workspace = true } starbase_styles = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index 1f29495..5cd4d5b 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -1,9 +1,42 @@ -use clap::{Parser, Subcommand}; -use jpm_common::EsTarget; +use clap::{Args, Parser, Subcommand}; +use jpm_common::{EsTarget, PackageName}; +use jpm_workspace::SelectQuery; pub const BIN_NAME: &str = if cfg!(windows) { "jpm.exe" } else { "jpm" }; -#[derive(Debug, Subcommand)] +static HEADING_FILTER: &str = "Package filtering"; + +#[derive(Clone, Debug, Args)] +pub struct GlobalArgs { + pub filters: Option>, + pub packages: Option>, + pub workspace: bool, +} + +impl GlobalArgs { + pub fn to_package_select_query(&self) -> SelectQuery { + SelectQuery { + all: self.workspace, + filters: self.filters.as_ref(), + names: self.packages.as_ref(), + } + } +} + +#[derive(Clone, Debug, Args)] +pub struct BuildArgs { + #[arg( + value_enum, + short = 't', + long, + env = "JPM_TARGET", + help = "ECMAScript target to transform source code to.", + default_value_t + )] + pub target: EsTarget, +} + +#[derive(Clone, Debug, Subcommand)] pub enum Commands { #[command( name = "build", @@ -11,22 +44,18 @@ pub enum Commands { long_about = "Build a package by transforming source files (from the package's `src` directory) to the `.jpm/` output directory.", rename_all = "camelCase" )] - Build { - #[arg(help = "Package path, relative from the current working directory.")] - path: Option, - - #[arg( - value_enum, - long, - env = "JPM_TARGET", - help = "ECMAScript target to transform source code to.", - default_value_t - )] - target: EsTarget, - }, + Build(BuildArgs), + + #[command( + name = "debug", + about = "Debug jpm instance.", + rename_all = "camelCase", + hide = true + )] + Debug, } -#[derive(Debug, Parser)] +#[derive(Clone, Debug, Parser)] #[command( bin_name = BIN_NAME, name = "jpm", @@ -38,7 +67,48 @@ pub enum Commands { next_line_help = false, rename_all = "camelCase" )] -pub struct App { +#[allow(clippy::upper_case_acronyms)] +pub struct CLI { #[command(subcommand)] pub command: Commands, + + #[arg( + short = 'f', + long, + global = true, + help = "Select packages by name using a filter glob. Can be specified multiple times.", + help_heading = HEADING_FILTER, + group = "package-filter" + )] + pub filter: Option>, + + #[arg( + short = 'p', + long, + global = true, + help = "Select packages by name. Can be specified multiple times.", + help_heading = HEADING_FILTER, + group = "package-filter" + )] + pub package: Option>, + + #[arg( + short = 'w', + long, + global = true, + help = "Select all packages in the workspace.", + help_heading = HEADING_FILTER, + group = "package-filter" + )] + pub workspace: bool, +} + +impl CLI { + pub fn global_args(&self) -> GlobalArgs { + GlobalArgs { + filters: self.filter.clone(), + packages: self.package.clone(), + workspace: self.workspace, + } + } } diff --git a/crates/cli/src/commands/build.rs b/crates/cli/src/commands/build.rs index d48c369..4a5c492 100644 --- a/crates/cli/src/commands/build.rs +++ b/crates/cli/src/commands/build.rs @@ -1,20 +1,28 @@ -use jpm_common::EsTarget; +use crate::app::{BuildArgs, GlobalArgs}; +use crate::helpers::loop_packages; use jpm_compiler::Compiler; -use jpm_package::Package; +use jpm_workspace::Workspace; use starbase::SystemResult; use starbase_styles::color; -use std::env; -pub async fn build(path: Option, target: EsTarget) -> SystemResult { - let cwd = env::current_dir().expect("Unable to get working directory!"); +#[tracing::instrument(skip_all)] +pub async fn build( + workspace: &Workspace, + args: &BuildArgs, + global_args: &GlobalArgs, +) -> SystemResult { + let packages = workspace.select_packages(global_args.to_package_select_query())?; - let package_root = cwd.join(path.unwrap_or(".".into())); - let package = Package::new(package_root)?; + loop_packages(packages, |package| async { + println!("Building target {}", color::symbol(args.target.to_string())); - let compiler = Compiler::new(&package)?; - let out_dir = compiler.compile(target).await?; + let out_dir = Compiler::new(package)?.compile(args.target).await?; - println!("Package successfully built to {}", color::path(out_dir)); + println!("Built to {}", color::path(out_dir)); + + Ok(()) + }) + .await?; Ok(()) } diff --git a/crates/cli/src/commands/debug.rs b/crates/cli/src/commands/debug.rs new file mode 100644 index 0000000..5db36fe --- /dev/null +++ b/crates/cli/src/commands/debug.rs @@ -0,0 +1,12 @@ +use jpm_workspace::Workspace; +use starbase::SystemResult; + +#[tracing::instrument(skip_all)] +pub async fn debug(workspace: &Workspace) -> SystemResult { + dbg!(workspace); + + dbg!("LOAD PACKAGES"); + dbg!(workspace.load_packages()?); + + Ok(()) +} diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 110d33e..279fe03 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -1,3 +1,5 @@ mod build; +mod debug; pub use build::*; +pub use debug::*; diff --git a/crates/cli/src/helpers.rs b/crates/cli/src/helpers.rs new file mode 100644 index 0000000..4afbb7f --- /dev/null +++ b/crates/cli/src/helpers.rs @@ -0,0 +1,34 @@ +use jpm_package::Package; +use starbase_styles::color::{create_style, Color, OwoStyle}; +use std::future::Future; + +pub fn start_checkpoint>(label: T) { + println!( + "{} {}", + create_style(Color::Yellow as u8).bold().style("===>"), + OwoStyle::new().bold().style(label.as_ref()), + ); +} + +pub async fn loop_packages<'pkg, F, Fut>( + packages: Vec<&'pkg Package>, + func: F, +) -> miette::Result<()> +where + F: Fn(&'pkg Package) -> Fut, + Fut: Future>, +{ + let last_index = packages.len() - 1; + + for (index, package) in packages.iter().enumerate() { + start_checkpoint(package.name()); + + func(package).await?; + + if index != last_index { + println!(); + } + } + + Ok(()) +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 9023960..234c01c 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -2,12 +2,16 @@ mod app; mod commands; +mod helpers; +mod states; +mod systems; -use app::{App as CLI, Commands}; +use app::CLI; use clap::Parser; use mimalloc::MiMalloc; use starbase::tracing::TracingOptions; use starbase::{App, MainResult}; +use states::RunningCommand; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; @@ -17,6 +21,8 @@ static GLOBAL: MiMalloc = MiMalloc; async fn main() -> MainResult { App::setup_diagnostics(); + let args = CLI::parse(); + App::setup_tracing_with_options(TracingOptions { filter_modules: vec!["jpm".into(), "schematic".into(), "starbase".into()], // log_env: "STARBASE_LOG".into(), @@ -25,11 +31,11 @@ async fn main() -> MainResult { ..TracingOptions::default() }); - let args = CLI::parse(); - - match args.command { - Commands::Build { path, target } => commands::build(path, target).await?, - }; + let mut app = App::new(); + app.set_state(RunningCommand(args)); + app.startup(systems::detect_workspace); + app.execute(systems::run_command); + app.run().await?; Ok(()) } diff --git a/crates/cli/src/states.rs b/crates/cli/src/states.rs new file mode 100644 index 0000000..7fdead3 --- /dev/null +++ b/crates/cli/src/states.rs @@ -0,0 +1,5 @@ +use crate::app::CLI; +use starbase::State; + +#[derive(State)] +pub struct RunningCommand(pub CLI); diff --git a/crates/cli/src/systems/detect_workspace.rs b/crates/cli/src/systems/detect_workspace.rs new file mode 100644 index 0000000..74f3eac --- /dev/null +++ b/crates/cli/src/systems/detect_workspace.rs @@ -0,0 +1,11 @@ +use jpm_workspace::Workspace; +use starbase::system; +use std::env; + +#[system] +pub fn detect_workspace(resources: ResourcesMut) -> SystemResult { + let working_dir = env::current_dir().expect("Unable to determine current working directory!"); + let workspace = Workspace::load_from(&working_dir)?; + + resources.set(workspace); +} diff --git a/crates/cli/src/systems/mod.rs b/crates/cli/src/systems/mod.rs new file mode 100644 index 0000000..21c25cb --- /dev/null +++ b/crates/cli/src/systems/mod.rs @@ -0,0 +1,5 @@ +mod detect_workspace; +mod run_command; + +pub use detect_workspace::*; +pub use run_command::*; diff --git a/crates/cli/src/systems/run_command.rs b/crates/cli/src/systems/run_command.rs new file mode 100644 index 0000000..fff2c41 --- /dev/null +++ b/crates/cli/src/systems/run_command.rs @@ -0,0 +1,19 @@ +use crate::app::Commands; +use crate::commands; +use crate::states::RunningCommand; +use jpm_workspace::Workspace; +use starbase::system; + +#[system] +pub fn run_command( + cli: StateRef, + workspace: ResourceRef, +) -> SystemResult { + let global_args = cli.global_args(); + + match &cli.command { + Commands::Build(args) => commands::build(workspace, args, &global_args).await?, + + Commands::Debug => commands::debug(workspace).await?, + }; +} diff --git a/crates/common/src/license_type.rs b/crates/common/src/license_type.rs index d6a4aef..8a0dcc5 100644 --- a/crates/common/src/license_type.rs +++ b/crates/common/src/license_type.rs @@ -1,12 +1,19 @@ use schematic::{SchemaType, Schematic}; use serde::{Deserialize, Serialize}; use spdx::Expression; +use std::fmt::Display; use std::ops::Deref; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(try_from = "String", into = "String")] pub struct LicenseType(Expression); +impl Display for LicenseType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.0.to_string().as_str()) + } +} + impl LicenseType { pub fn parse(value: &str) -> Result { Ok(Self(Expression::parse(value)?)) diff --git a/crates/common/src/package_name.rs b/crates/common/src/package_name.rs index c0e1826..782e287 100644 --- a/crates/common/src/package_name.rs +++ b/crates/common/src/package_name.rs @@ -3,6 +3,8 @@ use once_cell::sync::Lazy; use regex::Regex; use schematic::{validate::HasLength, SchemaType, Schematic}; use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::FromStr; use thiserror::Error; #[derive(Debug, Diagnostic, Error)] @@ -103,6 +105,20 @@ impl PackageName { } } +impl Display for PackageName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl FromStr for PackageName { + type Err = PackageNameError; + + fn from_str(s: &str) -> Result { + PackageName::parse(s) + } +} + impl TryFrom for PackageName { type Error = PackageNameError; diff --git a/crates/compiler/Cargo.toml b/crates/compiler/Cargo.toml index ce3ccb0..2223fff 100644 --- a/crates/compiler/Cargo.toml +++ b/crates/compiler/Cargo.toml @@ -8,12 +8,12 @@ license = "MIT" jpm_common = { path = "../common" } jpm_manifest = { path = "../manifest" } jpm_package = { path = "../package" } -anyhow = "1.0.72" +anyhow = "1.0.74" futures = "0.3.28" miette = { workspace = true } oxipng = "8.0.0" -swc = "0.264.46" -swc_core = { version = "0.79.49", default-features = false, features = [ +swc = "0.264.56" +swc_core = { version = "0.79.59", default-features = false, features = [ "common", "ecma_ast", "ecma_parser", diff --git a/crates/lockfile/Cargo.toml b/crates/lockfile/Cargo.toml new file mode 100644 index 0000000..e131951 --- /dev/null +++ b/crates/lockfile/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "jpm_lockfile" +version = "0.1.0" +edition = "2021" +license = "MIT" diff --git a/crates/lockfile/src/lib.rs b/crates/lockfile/src/lib.rs new file mode 100644 index 0000000..1fd1aea --- /dev/null +++ b/crates/lockfile/src/lib.rs @@ -0,0 +1 @@ +pub static LOCKFILE_NAME: &str = "jpm.lock"; diff --git a/crates/manifest/src/common_settings.rs b/crates/manifest/src/common_settings.rs index 2877363..52ad9c2 100644 --- a/crates/manifest/src/common_settings.rs +++ b/crates/manifest/src/common_settings.rs @@ -1,9 +1,9 @@ use jpm_common::{EsTarget, PackageName}; use schematic::{derive_enum, Config, ConfigEnum}; use semver::VersionReq; -use std::collections::HashMap; +use std::collections::BTreeMap; -pub type ManifestDependencies = HashMap; +pub type ManifestDependencies = BTreeMap; derive_enum!( #[derive(ConfigEnum, Default)] diff --git a/crates/manifest/src/manifest_error.rs b/crates/manifest/src/manifest_error.rs index 1c35b96..b6a4db3 100644 --- a/crates/manifest/src/manifest_error.rs +++ b/crates/manifest/src/manifest_error.rs @@ -1,4 +1,4 @@ -use crate::manifest_loader::MANIFEST_FILE; +use crate::manifest_loader::MANIFEST_NAME; use miette::Diagnostic; use starbase_styles::{Style, Stylize}; use std::path::PathBuf; @@ -18,7 +18,7 @@ pub enum ManifestError { #[diagnostic(code(manifest::missing_file))] #[error( "No {} manifest file found in {}.", - MANIFEST_FILE.style(Style::File), + MANIFEST_NAME.style(Style::File), .path.style(Style::Path), )] MissingFile { path: PathBuf }, diff --git a/crates/manifest/src/manifest_loader.rs b/crates/manifest/src/manifest_loader.rs index 22dbc85..0920452 100644 --- a/crates/manifest/src/manifest_loader.rs +++ b/crates/manifest/src/manifest_loader.rs @@ -6,8 +6,9 @@ use starbase_utils::fs; use std::path::{Path, PathBuf}; use tracing::debug; -pub const MANIFEST_FILE: &str = "jpm.toml"; +pub const MANIFEST_NAME: &str = "jpm.toml"; +#[derive(Debug)] pub enum Manifest { Workspace(Box), Package(Box), @@ -17,10 +18,10 @@ pub struct ManifestLoader; impl ManifestLoader { pub fn resolve_path(path: &Path) -> miette::Result { - let file_path = if path.ends_with(MANIFEST_FILE) { + let file_path = if path.ends_with(MANIFEST_NAME) { path.to_path_buf() } else { - path.join(MANIFEST_FILE) + path.join(MANIFEST_NAME) }; if file_path.exists() { diff --git a/crates/manifest/tests/manifest_loader_test.rs b/crates/manifest/tests/manifest_loader_test.rs index 9ae3134..8c6ad53 100644 --- a/crates/manifest/tests/manifest_loader_test.rs +++ b/crates/manifest/tests/manifest_loader_test.rs @@ -11,7 +11,7 @@ mod manifest_loader { let sandbox = create_empty_sandbox(); sandbox.create_file("jpm.toml", ""); - ManifestLoader::load(sandbox.path().join(MANIFEST_FILE)).unwrap(); + ManifestLoader::load(sandbox.path().join(MANIFEST_NAME)).unwrap(); } #[test] @@ -25,7 +25,7 @@ name = "ns/pkg" "#, ); - let manifest = ManifestLoader::load(sandbox.path().join(MANIFEST_FILE)).unwrap(); + let manifest = ManifestLoader::load(sandbox.path().join(MANIFEST_NAME)).unwrap(); if let Manifest::Package(package) = manifest { assert_eq!(package.package.name, PackageName::parse("ns/pkg").unwrap()); @@ -45,7 +45,7 @@ packages = ["*"] "#, ); - let manifest = ManifestLoader::load(sandbox.path().join(MANIFEST_FILE)).unwrap(); + let manifest = ManifestLoader::load(sandbox.path().join(MANIFEST_NAME)).unwrap(); if let Manifest::Workspace(workspace) = manifest { assert_eq!(workspace.workspace.packages, vec!["*".to_owned()]); diff --git a/crates/manifest/tests/package_manifest_test.rs b/crates/manifest/tests/package_manifest_test.rs index b685945..6e86e55 100644 --- a/crates/manifest/tests/package_manifest_test.rs +++ b/crates/manifest/tests/package_manifest_test.rs @@ -2,7 +2,7 @@ use jpm_common::*; use jpm_manifest::*; use semver::{Version, VersionReq}; use starbase_sandbox::create_empty_sandbox; -use std::collections::HashMap; +use std::collections::BTreeMap; use url::Url; mod package_manifest { @@ -30,8 +30,8 @@ name = "ns/pkg" optimize_png: true, optimize_svg: true, }, - dependencies: HashMap::new(), - dev_dependencies: HashMap::new(), + dependencies: BTreeMap::new(), + dev_dependencies: BTreeMap::new(), install: ManifestInstall { linker: ManifestInstallLinker::NodeModules, target: EsTarget::Es2018, @@ -132,7 +132,7 @@ name = "ns/pkg" assert_eq!( manifest.dependencies, - HashMap::from_iter([ + BTreeMap::from_iter([ ( PackageName::parse("ns/a1").unwrap(), VersionReq::parse("1.2.3").unwrap() diff --git a/crates/manifest/tests/workspace_manifest_test.rs b/crates/manifest/tests/workspace_manifest_test.rs index 1985732..3f67933 100644 --- a/crates/manifest/tests/workspace_manifest_test.rs +++ b/crates/manifest/tests/workspace_manifest_test.rs @@ -1,6 +1,6 @@ use jpm_manifest::*; use starbase_sandbox::create_empty_sandbox; -use std::collections::HashMap; +use std::collections::BTreeMap; mod workspace_manifest { use super::*; @@ -21,8 +21,8 @@ packages = ["*"] assert_eq!( manifest, WorkspaceManifest { - dependencies: HashMap::new(), - dev_dependencies: HashMap::new(), + dependencies: BTreeMap::new(), + dev_dependencies: BTreeMap::new(), install: ManifestInstall { linker: ManifestInstallLinker::NodeModules, target: EsTarget::Es2018, diff --git a/crates/package/src/package.rs b/crates/package/src/package.rs index d6a2e9d..3e2367a 100644 --- a/crates/package/src/package.rs +++ b/crates/package/src/package.rs @@ -7,6 +7,7 @@ use starbase_utils::{fs, glob}; use std::path::{Path, PathBuf}; use tracing::{debug, trace}; +#[derive(Debug)] pub struct Package { pub manifest: PackageManifest, pub root: PathBuf, @@ -18,7 +19,7 @@ impl Package { pub fn new>(root: P) -> miette::Result { let root = root.as_ref().to_path_buf(); - debug!(root = ?root, "Loading package"); + debug!(package_root = ?root, "Loading package from directory"); if !root.exists() { return Err(PackageError::MissingPackage { path: root }.into()); @@ -37,7 +38,7 @@ impl Package { } pub fn load_source_files(&self) -> miette::Result { - debug!(src_dir = ?self.src_dir, "Loading source files"); + debug!(package = self.name(), src_dir = ?self.src_dir, "Loading source files"); if !self.src_dir.exists() { return Err(PackageError::MissingSourceDir { @@ -62,7 +63,11 @@ impl Package { // Exclude files first if exclude.is_match(rel_file.as_str()) { - trace!(file = ?rel_file, "Excluding source file as it matches an exclude pattern"); + trace!( + package = self.name(), + file = ?rel_file, + "Excluding source file as it matches an exclude pattern", + ); sources.excluded.push(rel_file); continue; @@ -70,7 +75,11 @@ impl Package { // Filter out test files if SourceFiles::is_test_file(rel_file.as_ref()) { - trace!(file = ?rel_file, "Filtering source file as it was detected as a test file"); + trace!( + package = self.name(), + file = ?rel_file, + "Filtering source file as it was detected as a test file", + ); sources.tests.push(rel_file); continue; @@ -81,17 +90,29 @@ impl Package { return Err(PackageError::NoCommonJS { path: file }.into()); } Some(ext) if ext == "js" || ext == "jsx" || ext == "mjs" => { - trace!(file = ?rel_file, "Using JavaScript file"); + trace!( + package = self.name(), + file = ?rel_file, + "Using JavaScript file", + ); sources.modules.push(rel_file); } Some(ext) if ext == "ts" || ext == "tsx" || ext == "mts" => { if rel_file.as_str().contains(".d.") { - trace!(file = ?rel_file, "Ignoring TypeScript declaration"); + trace!( + package = self.name(), + file = ?rel_file, + "Ignoring TypeScript declaration", + ); sources.excluded.push(rel_file); } else { - trace!(file = ?rel_file, "Using TypeScript file"); + trace!( + package = self.name(), + file = ?rel_file, + "Using TypeScript file", + ); sources.modules.push(rel_file); sources.typescript = true; diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml new file mode 100644 index 0000000..7e1cfd0 --- /dev/null +++ b/crates/workspace/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "jpm_workspace" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +jpm_common = { path = "../common" } +jpm_lockfile = { path = "../lockfile" } +jpm_manifest = { path = "../manifest" } +jpm_package = { path = "../package" } +miette = { workspace = true } +once_cell = { workspace = true } +petgraph = { workspace = true } +starbase = { workspace = true } +starbase_styles = { workspace = true } +starbase_utils = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +starbase_sandbox = { workspace = true } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs new file mode 100644 index 0000000..f29a7e3 --- /dev/null +++ b/crates/workspace/src/lib.rs @@ -0,0 +1,7 @@ +mod package_graph; +mod workspace; +mod workspace_error; + +pub use package_graph::*; +pub use workspace::*; +pub use workspace_error::*; diff --git a/crates/workspace/src/package_graph.rs b/crates/workspace/src/package_graph.rs new file mode 100644 index 0000000..14471ab --- /dev/null +++ b/crates/workspace/src/package_graph.rs @@ -0,0 +1,107 @@ +use crate::workspace_error::WorkspaceError; +use jpm_common::PackageName; +use jpm_package::Package; +use petgraph::algo::toposort; +use petgraph::stable_graph::{NodeIndex, StableDiGraph}; +use starbase_styles::color; +use std::collections::BTreeMap; +use tracing::{debug, trace}; + +// This is a simple DAG that represents the dependency graph of local +// packages within the workspace. Its primary use is for running processes +// in order (builds, etc), and is not used for dependency install. +pub struct PackageGraph<'ws> { + graph: StableDiGraph<&'ws PackageName, ()>, + indices: BTreeMap<&'ws PackageName, NodeIndex>, + packages: &'ws BTreeMap, +} + +impl<'ws> PackageGraph<'ws> { + pub fn new(packages: &BTreeMap) -> PackageGraph { + debug!("Creating a package graph with {} packages", packages.len()); + + let mut graph = PackageGraph { + graph: StableDiGraph::new(), + indices: BTreeMap::new(), + packages, + }; + graph.add_packages(); + graph + } + + pub fn toposort(&self) -> miette::Result> { + debug!("Sorting package graph topologically"); + + match toposort(&self.graph, None) { + Ok(indices) => { + let names = indices + .into_iter() + .rev() // From most depended on to least + .map(|i| *self.graph.node_weight(i).unwrap()) + .collect::>(); + + debug!( + "Sorted to: {}", + names + .iter() + .map(|n| color::id(n.as_str())) + .collect::>() + .join(", ") + ); + + Ok(names) + } + Err(cycle) => Err(WorkspaceError::PackageGraphCycle { + dep: (*self.graph.node_weight(cycle.node_id()).unwrap()).to_owned(), + })?, + } + } + + fn add_packages(&mut self) { + for (name, package) in self.packages { + self.add_package(name, package); + } + + self.indices.clear(); + } + + fn add_package(&mut self, name: &'ws PackageName, package: &'ws Package) -> NodeIndex { + // Already inserted, skip + if let Some(index) = self.indices.get(name) { + return *index; + } + + // Insert into the graph + trace!(package = name.as_str(), "Adding package to graph"); + + let index = self.graph.add_node(name); + + self.indices.insert(name, index); + + // Loop through dependencies and find packages in the current workspace + let mut edges = vec![]; + + let mut dependencies = BTreeMap::new(); + dependencies.extend(&package.manifest.dependencies); + dependencies.extend(&package.manifest.dev_dependencies); + + for dep_name in dependencies.keys() { + if let Some(dep_package) = self.packages.get(dep_name) { + edges.push(self.add_package(dep_name, dep_package)); + + trace!( + dependency = dep_name.as_str(), + package = name.as_str(), + "Linking dependency to package" + ); + } + } + + // Connect edges to the original index + for edge in edges { + self.graph.add_edge(index, edge, ()); + } + + index + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs new file mode 100644 index 0000000..3e9f152 --- /dev/null +++ b/crates/workspace/src/workspace.rs @@ -0,0 +1,191 @@ +use crate::package_graph::PackageGraph; +use crate::workspace_error::WorkspaceError; +use jpm_common::PackageName; +use jpm_lockfile::LOCKFILE_NAME; +use jpm_manifest::{Manifest, ManifestLoader, MANIFEST_NAME}; +use jpm_package::Package; +use once_cell::sync::OnceCell; +use starbase::Resource; +use starbase_styles::color; +use starbase_utils::{fs, glob}; +use std::collections::{BTreeMap, HashSet}; +use std::fmt; +use std::path::{Path, PathBuf}; +use tracing::debug; + +#[derive(Default)] +pub struct SelectQuery<'app> { + pub all: bool, + pub filters: Option<&'app Vec>, + pub names: Option<&'app Vec>, +} + +#[derive(Resource)] +pub struct Workspace { + pub manifest: Manifest, + pub monorepo: bool, + pub root: PathBuf, + pub working_dir: PathBuf, + + packages: OnceCell>, +} + +impl Workspace { + pub fn load_from(working_dir: &Path) -> miette::Result { + debug!( + working_dir = ?working_dir, + lockfile = LOCKFILE_NAME, + "Attempting to find workspace root by locating a lockfile", + ); + + let mut root = fs::find_upwards_root(LOCKFILE_NAME, working_dir); + + if root.is_none() { + debug!( + manifest = MANIFEST_NAME, + "No lockfile found, locating closest manifest instead" + ); + + root = fs::find_upwards_root(MANIFEST_NAME, working_dir); + } + + let Some(root) = root else { + return Err(WorkspaceError::NoRootDetected)?; + }; + + debug!(root = ?root, "Found a possible root!"); + + let manifest = ManifestLoader::load(&root)?; + + Ok(Workspace { + monorepo: matches!(manifest, Manifest::Workspace(_)), + manifest, + packages: OnceCell::new(), + root, + working_dir: working_dir.to_path_buf(), + }) + } + + pub fn load_packages(&self) -> miette::Result<&BTreeMap> { + self.packages.get_or_try_init(|| { + let mut packages = BTreeMap::new(); + + debug!(workspace_root = ?self.root, "Loading package(s)"); + + let mut add_package = |root: &Path| -> miette::Result<()> { + let package = Package::new(root)?; + + debug!( + package = package.name(), + package_root = ?root, + "Loaded package {}", + color::id(package.name()), + ); + + packages.insert(package.manifest.package.name.clone(), package); + + Ok(()) + }; + + match &self.manifest { + // Multi package repository + Manifest::Workspace(manifest) => { + debug!( + packages = ?manifest.workspace.packages, + "Detected a multi package repository (monorepo), locating packages with a manifest", + ); + + for package_root in glob::walk(&self.root, &manifest.workspace.packages)? { + // Only include directories that have a manifest + if package_root.is_dir() && package_root.join(MANIFEST_NAME).exists() { + add_package(&package_root)?; + } + } + } + // Single package repository + Manifest::Package(_) => { + debug!( + "Detected a single package repository (polyrepo), using workspace root as package root" + ); + + add_package(&self.root)?; + } + }; + + Ok::, miette::Report>(packages) + }) + } + + pub fn select_packages(&self, query: SelectQuery) -> miette::Result> { + let packages = self.load_packages()?; + let mut selected_names = HashSet::new(); + + // If a polyrepo, always use the root package + if let Manifest::Package(root_package) = &self.manifest { + selected_names.insert(&root_package.package.name); + + // Select packages with filters + } else if let Some(filters) = query.filters { + let globset = glob::GlobSet::new(filters)?; + + for package_name in packages.keys() { + if globset.matches(package_name.as_str()) { + selected_names.insert(package_name); + } + } + + // Select packages by name + } else if let Some(select_by) = query.names { + for name in select_by { + if !packages.contains_key(name) { + return Err(WorkspaceError::UnknownPackage { + name: name.to_owned(), + })?; + } + + selected_names.insert(name); + } + + // Select all packages + } else if query.all { + selected_names.extend(packages.keys()); + } + + if selected_names.is_empty() { + return Err(WorkspaceError::NoPackagesSelected)?; + } + + // Sort the filtered packages topologically + let mut results = vec![]; + + for name in PackageGraph::new(packages).toposort()? { + if selected_names.contains(name) { + results.push(packages.get(name).unwrap()); + } + } + + if selected_names.len() != packages.len() { + debug!( + "Filtered to: {}", + selected_names + .iter() + .map(|n| color::id(n.as_str())) + .collect::>() + .join(", ") + ); + } + + Ok(results) + } +} + +impl fmt::Debug for Workspace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Workspace") + .field("manifest", &self.manifest) + .field("monorepo", &self.monorepo) + .field("root", &self.root) + .field("working_dir", &self.working_dir) + .finish() + } +} diff --git a/crates/workspace/src/workspace_error.rs b/crates/workspace/src/workspace_error.rs new file mode 100644 index 0000000..a712067 --- /dev/null +++ b/crates/workspace/src/workspace_error.rs @@ -0,0 +1,40 @@ +use jpm_common::PackageName; +use jpm_lockfile::LOCKFILE_NAME; +use jpm_manifest::MANIFEST_NAME; +use miette::Diagnostic; +use starbase_styles::{Style, Stylize}; +use thiserror::Error; + +#[derive(Debug, Diagnostic, Error)] +pub enum WorkspaceError { + #[diagnostic(code(workspace::package_graph::none_selected))] + #[error( + "No packages have been selected. Pass {} to select all packages in the workspace, {} for each package by name, or {} to filter by name.", + "--workspace".style(Style::Label), + "--package".style(Style::Label), + "--filter".style(Style::Label), + )] + NoPackagesSelected, + + #[diagnostic(code(workspace::no_root_detected))] + #[error( + "Unable to detect a package workspace root. Either generate a {} by installing dependencies, or run this command from a directory with a {} manifest.", + LOCKFILE_NAME.style(Style::File), + MANIFEST_NAME.style(Style::File), + )] + NoRootDetected, + + #[diagnostic(code(workspace::package_graph::cycle_detected))] + #[error( + "Unable to continue, detected a dependency cycle for packages in the local workspace. The package {} was involved in the cycle.", + .dep.to_string().style(Style::Id), + )] + PackageGraphCycle { dep: PackageName }, + + #[diagnostic(code(workspace::package_graph::unknown_package))] + #[error( + "The package {} doesn't exist within the current workspace.", + .name.to_string().style(Style::Id), + )] + UnknownPackage { name: PackageName }, +} diff --git a/crates/workspace/tests/__fixtures__/graph-cycle/a/jpm.toml b/crates/workspace/tests/__fixtures__/graph-cycle/a/jpm.toml new file mode 100644 index 0000000..0a97443 --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph-cycle/a/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "graph/aa" + +[dependencies] +"graph/bb" = "*" diff --git a/crates/workspace/tests/__fixtures__/graph-cycle/b/jpm.toml b/crates/workspace/tests/__fixtures__/graph-cycle/b/jpm.toml new file mode 100644 index 0000000..b5bce0f --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph-cycle/b/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "graph/bb" + +[dependencies] +"graph/cc" = "*" diff --git a/crates/workspace/tests/__fixtures__/graph-cycle/c/jpm.toml b/crates/workspace/tests/__fixtures__/graph-cycle/c/jpm.toml new file mode 100644 index 0000000..e6d8ecc --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph-cycle/c/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "graph/cc" + +[dependencies] +"graph/aa" = "*" diff --git a/crates/workspace/tests/__fixtures__/graph-cycle/jpm.toml b/crates/workspace/tests/__fixtures__/graph-cycle/jpm.toml new file mode 100644 index 0000000..7e08b9f --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph-cycle/jpm.toml @@ -0,0 +1,2 @@ +[workspace] +packages = ["*"] diff --git a/crates/workspace/tests/__fixtures__/graph/a/jpm.toml b/crates/workspace/tests/__fixtures__/graph/a/jpm.toml new file mode 100644 index 0000000..c3e6c8f --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph/a/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "graph/aa" + +[dependencies] +"graph/dd" = "*" diff --git a/crates/workspace/tests/__fixtures__/graph/b/jpm.toml b/crates/workspace/tests/__fixtures__/graph/b/jpm.toml new file mode 100644 index 0000000..b5bce0f --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph/b/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "graph/bb" + +[dependencies] +"graph/cc" = "*" diff --git a/crates/workspace/tests/__fixtures__/graph/c/jpm.toml b/crates/workspace/tests/__fixtures__/graph/c/jpm.toml new file mode 100644 index 0000000..6c7677c --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph/c/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "graph/cc" + +[devDependencies] +"graph/ff" = "*" diff --git a/crates/workspace/tests/__fixtures__/graph/d/jpm.toml b/crates/workspace/tests/__fixtures__/graph/d/jpm.toml new file mode 100644 index 0000000..ffc0d4c --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph/d/jpm.toml @@ -0,0 +1,2 @@ +[package] +name = "graph/dd" diff --git a/crates/workspace/tests/__fixtures__/graph/e/jpm.toml b/crates/workspace/tests/__fixtures__/graph/e/jpm.toml new file mode 100644 index 0000000..8339733 --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph/e/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "graph/ee" + +[dependencies] +"graph/dd" = "*" diff --git a/crates/workspace/tests/__fixtures__/graph/f/jpm.toml b/crates/workspace/tests/__fixtures__/graph/f/jpm.toml new file mode 100644 index 0000000..efc6be9 --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph/f/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "graph/ff" + +[dependencies] +"graph/ee" = "*" diff --git a/crates/workspace/tests/__fixtures__/graph/jpm.toml b/crates/workspace/tests/__fixtures__/graph/jpm.toml new file mode 100644 index 0000000..7e08b9f --- /dev/null +++ b/crates/workspace/tests/__fixtures__/graph/jpm.toml @@ -0,0 +1,2 @@ +[workspace] +packages = ["*"] diff --git a/crates/workspace/tests/__fixtures__/monorepo/apps/client/jpm.toml b/crates/workspace/tests/__fixtures__/monorepo/apps/client/jpm.toml new file mode 100644 index 0000000..486374e --- /dev/null +++ b/crates/workspace/tests/__fixtures__/monorepo/apps/client/jpm.toml @@ -0,0 +1,2 @@ +[package] +name = "app/client" diff --git a/crates/workspace/tests/__fixtures__/monorepo/apps/server/jpm.toml b/crates/workspace/tests/__fixtures__/monorepo/apps/server/jpm.toml new file mode 100644 index 0000000..b7f94e0 --- /dev/null +++ b/crates/workspace/tests/__fixtures__/monorepo/apps/server/jpm.toml @@ -0,0 +1,2 @@ +[package] +name = "app/server" diff --git a/crates/workspace/tests/__fixtures__/monorepo/jpm.toml b/crates/workspace/tests/__fixtures__/monorepo/jpm.toml new file mode 100644 index 0000000..2f613b1 --- /dev/null +++ b/crates/workspace/tests/__fixtures__/monorepo/jpm.toml @@ -0,0 +1,2 @@ +[workspace] +packages = ["packages/*"] diff --git a/crates/workspace/tests/__fixtures__/monorepo/packages/bar/jpm.toml b/crates/workspace/tests/__fixtures__/monorepo/packages/bar/jpm.toml new file mode 100644 index 0000000..99f4ae2 --- /dev/null +++ b/crates/workspace/tests/__fixtures__/monorepo/packages/bar/jpm.toml @@ -0,0 +1,2 @@ +[package] +name = "mono/bar" diff --git a/crates/workspace/tests/__fixtures__/monorepo/packages/baz/jpm.toml b/crates/workspace/tests/__fixtures__/monorepo/packages/baz/jpm.toml new file mode 100644 index 0000000..29654bf --- /dev/null +++ b/crates/workspace/tests/__fixtures__/monorepo/packages/baz/jpm.toml @@ -0,0 +1,2 @@ +[package] +name = "mono/baz" diff --git a/crates/workspace/tests/__fixtures__/monorepo/packages/foo/jpm.toml b/crates/workspace/tests/__fixtures__/monorepo/packages/foo/jpm.toml new file mode 100644 index 0000000..011e060 --- /dev/null +++ b/crates/workspace/tests/__fixtures__/monorepo/packages/foo/jpm.toml @@ -0,0 +1,2 @@ +[package] +name = "mono/foo" diff --git a/crates/workspace/tests/__fixtures__/monorepo/packages/no-manifest/empty b/crates/workspace/tests/__fixtures__/monorepo/packages/no-manifest/empty new file mode 100644 index 0000000..e69de29 diff --git a/crates/workspace/tests/__fixtures__/polyrepo/jpm.toml b/crates/workspace/tests/__fixtures__/polyrepo/jpm.toml new file mode 100644 index 0000000..611549b --- /dev/null +++ b/crates/workspace/tests/__fixtures__/polyrepo/jpm.toml @@ -0,0 +1,2 @@ +[package] +name = "poly/root" diff --git a/crates/workspace/tests/package_graph_test.rs b/crates/workspace/tests/package_graph_test.rs new file mode 100644 index 0000000..6c4b06e --- /dev/null +++ b/crates/workspace/tests/package_graph_test.rs @@ -0,0 +1,36 @@ +use jpm_workspace::{PackageGraph, Workspace}; +use starbase_sandbox::create_sandbox; + +mod package_graph { + use super::*; + + #[test] + #[should_panic(expected = "detected a dependency cycle")] + fn errors_for_cycle() { + let sandbox = create_sandbox("graph-cycle"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + PackageGraph::new(workspace.load_packages().unwrap()) + .toposort() + .unwrap(); + } + + #[test] + fn sorts_topologically() { + let sandbox = create_sandbox("graph"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + let graph = PackageGraph::new(workspace.load_packages().unwrap()); + let names = graph + .toposort() + .unwrap() + .iter() + .map(|n| n.as_str()) + .collect::>(); + + assert_eq!( + names, + vec!["graph/dd", "graph/aa", "graph/ee", "graph/ff", "graph/cc", "graph/bb"] + ); + } +} diff --git a/crates/workspace/tests/workspace_test.rs b/crates/workspace/tests/workspace_test.rs new file mode 100644 index 0000000..1743098 --- /dev/null +++ b/crates/workspace/tests/workspace_test.rs @@ -0,0 +1,219 @@ +use jpm_common::PackageName; +use jpm_workspace::{SelectQuery, Workspace}; +use starbase_sandbox::{create_empty_sandbox, create_sandbox}; + +mod workspace { + use super::*; + + #[test] + #[should_panic(expected = "Unable to detect a package workspace root")] + fn errors_no_root_found() { + let sandbox = create_empty_sandbox(); + + Workspace::load_from(sandbox.path()).unwrap(); + } + + #[test] + fn finds_root_via_lockfile() { + let sandbox = create_empty_sandbox(); + sandbox.create_file("jpm.lock", "{}"); + sandbox.create_file("jpm.toml", "[package]\nname = \"ns/root\""); + sandbox.create_file("some/nested/jpm.toml", "[package]\nname = \"ns/branch\""); + + let workspace = Workspace::load_from(&sandbox.path().join("some/nested/path")).unwrap(); + + assert_eq!(workspace.root, sandbox.path()); + } + + #[test] + fn finds_root_via_manifest() { + let sandbox = create_empty_sandbox(); + sandbox.create_file("jpm.toml", "[package]\nname = \"ns/test\""); + + let workspace = Workspace::load_from(&sandbox.path().join("some/nested/path")).unwrap(); + + assert_eq!(workspace.root, sandbox.path()); + } + + mod polyrepo { + use super::*; + + #[test] + fn marks_as_polyrepo() { + let sandbox = create_sandbox("polyrepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + assert!(!workspace.monorepo); + } + + #[test] + fn loads_a_single_package() { + let sandbox = create_sandbox("polyrepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + let packages = workspace.load_packages().unwrap(); + let root = packages + .get(&PackageName::parse("poly/root").unwrap()) + .unwrap(); + + assert_eq!(root.root, sandbox.path()); + } + + #[test] + fn always_selects_one_package_regardless_of_filters() { + let sandbox = create_sandbox("polyrepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + let packages = workspace.select_packages(SelectQuery::default()).unwrap(); + + assert_eq!(packages.len(), 1); + + let packages = workspace + .select_packages(SelectQuery { + all: true, + ..SelectQuery::default() + }) + .unwrap(); + + assert_eq!(packages.len(), 1); + + let packages = workspace + .select_packages(SelectQuery { + names: Some(&vec![PackageName::parse("poly/root").unwrap()]), + ..SelectQuery::default() + }) + .unwrap(); + + assert_eq!(packages.len(), 1); + + let packages = workspace + .select_packages(SelectQuery { + filters: Some(&vec!["poly/*".into()]), + ..SelectQuery::default() + }) + .unwrap(); + + assert_eq!(packages.len(), 1); + } + } + + mod monorepo { + use super::*; + + #[test] + fn marks_as_monorepo() { + let sandbox = create_sandbox("monorepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + assert!(workspace.monorepo); + } + + #[test] + fn loads_all_packages_matching_glob() { + let sandbox = create_sandbox("monorepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + let packages = workspace.load_packages().unwrap(); + + // Only packages + assert_eq!(packages.len(), 3); + assert!(packages.contains_key(&PackageName::parse("mono/foo").unwrap())); + assert!(packages.contains_key(&PackageName::parse("mono/bar").unwrap())); + assert!(packages.contains_key(&PackageName::parse("mono/baz").unwrap())); + } + + #[test] + fn selects_all() { + let sandbox = create_sandbox("monorepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + let packages = workspace + .select_packages(SelectQuery { + all: true, + ..SelectQuery::default() + }) + .unwrap(); + + assert_eq!(packages.len(), 3); + } + + #[test] + fn selects_by_name() { + let sandbox = create_sandbox("monorepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + let packages = workspace + .select_packages(SelectQuery { + names: Some(&vec![ + PackageName::parse("mono/foo").unwrap(), + PackageName::parse("mono/baz").unwrap(), + ]), + ..SelectQuery::default() + }) + .unwrap(); + + assert_eq!(packages.len(), 2); + assert_eq!(packages[0].name(), "mono/baz"); + assert_eq!(packages[1].name(), "mono/foo"); + } + + #[test] + fn selects_by_filter() { + let sandbox = create_sandbox("monorepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + let packages = workspace + .select_packages(SelectQuery { + filters: Some(&vec!["*/ba{z,r}".into()]), + ..SelectQuery::default() + }) + .unwrap(); + + assert_eq!(packages.len(), 2); + assert_eq!(packages[0].name(), "mono/bar"); + assert_eq!(packages[1].name(), "mono/baz"); + } + + #[test] + fn selects_by_filter_with_negated() { + let sandbox = create_sandbox("monorepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + let packages = workspace + .select_packages(SelectQuery { + filters: Some(&vec!["*/ba{z,r}".into(), "!*/bar".into()]), + ..SelectQuery::default() + }) + .unwrap(); + + assert_eq!(packages.len(), 1); + assert_eq!(packages[0].name(), "mono/baz"); + } + + #[test] + #[should_panic(expected = "No packages have been selected.")] + fn errors_none_selected() { + let sandbox = create_sandbox("monorepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + workspace + .select_packages(SelectQuery { + filters: Some(&vec!["*/unknown".into()]), + ..SelectQuery::default() + }) + .unwrap(); + } + + #[test] + #[should_panic(expected = "The package mono/unknown doesn't exist")] + fn errors_unknown_name() { + let sandbox = create_sandbox("monorepo"); + let workspace = Workspace::load_from(sandbox.path()).unwrap(); + + workspace + .select_packages(SelectQuery { + names: Some(&vec![PackageName::parse("mono/unknown").unwrap()]), + ..SelectQuery::default() + }) + .unwrap(); + } + } +} diff --git a/jpm.toml b/jpm.toml new file mode 100644 index 0000000..2f613b1 --- /dev/null +++ b/jpm.toml @@ -0,0 +1,2 @@ +[workspace] +packages = ["packages/*"] diff --git a/packages/bar/jpm.toml b/packages/bar/jpm.toml new file mode 100644 index 0000000..907e4b4 --- /dev/null +++ b/packages/bar/jpm.toml @@ -0,0 +1,2 @@ +[package] +name = "jpm-example/bar" diff --git a/packages/bar/src/index.ts b/packages/bar/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/baz/jpm.toml b/packages/baz/jpm.toml new file mode 100644 index 0000000..c465290 --- /dev/null +++ b/packages/baz/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "jpm-example/baz" + +[dependencies] +"jpm-example/foo" = "*" diff --git a/packages/baz/src/index.ts b/packages/baz/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/foo/jpm.toml b/packages/foo/jpm.toml new file mode 100644 index 0000000..384f00e --- /dev/null +++ b/packages/foo/jpm.toml @@ -0,0 +1,5 @@ +[package] +name = "jpm-example/foo" + +[dependencies] +"jpm-example/bar" = "*" diff --git a/packages/foo/src/index.ts b/packages/foo/src/index.ts new file mode 100644 index 0000000..e69de29